File size: 6,708 Bytes
d3fd67f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
"""InfoMax reranker demo (Gradio, ZeroGPU).

Neural scoring (MS MARCO MiniLM-L6 cross-encoder + all-MiniLM-L6-v2 embeddings) runs inside the
@spaces.GPU function; every selector then runs on CPU from those cached scores.
"""
import spaces  # import first on ZeroGPU

import os

import gradio as gr
import numpy as np
import pandas as pd
import torch
from sentence_transformers import CrossEncoder, SentenceTransformer

from infomax_rerank import by_score, fused, infomax, mmr, zfuse

CE_ID = os.environ.get("CE_MODEL", "cross-encoder/ms-marco-MiniLM-L6-v2")
DENSE_ID = os.environ.get("DENSE_MODEL", "sentence-transformers/all-MiniLM-L6-v2")
ce = CrossEncoder(CE_ID, max_length=256, device="cpu")
enc = SentenceTransformer(DENSE_ID, device="cpu")

# Settings tuned on dev splits only (SciFact train, NFCorpus dev, FiQA dev); one setting for all datasets.
TUNED = {
    "InfoMax": dict(rank=1, gamma=4.0, rho=0.3, sigma2=0.03, beta=0.0, delta=0.0),
    "InfoMax+F": dict(rank=4, gamma=8.0, rho=1.0, sigma2=0.03, beta=3.0, delta=0.0, alpha=0.5),
    "MMR": dict(lam=0.95),
    "Fusion": dict(alpha=0.5),
}
METHODS = ["CE", "InfoMax", "InfoMax+F", "Dense", "MMR", "Fusion"]


@spaces.GPU(duration=60)
def neural_scores(query, passages):
    dev = "cuda" if torch.cuda.is_available() else "cpu"
    ce.model.to(dev)
    enc.to(dev)
    logits = np.asarray(ce.predict([(query, p) for p in passages], batch_size=64,
                                   show_progress_bar=False), np.float32).reshape(-1)
    E = enc.encode([query] + passages, batch_size=64, normalize_embeddings=True,
                   convert_to_numpy=True, show_progress_bar=False).astype(np.float32)
    return logits, E[1:], E[0]


def split_passages(text):
    blocks = [b.strip() for b in text.replace("\r\n", "\n").split("\n\n")]
    return [b for b in blocks if b]


def rank_all(query, passages_text, k, rank, gamma, sigma2, beta, delta, alpha):
    query = (query or "").strip()
    passages = split_passages(passages_text or "")
    if not query:
        raise gr.Error("Enter a query.")
    if len(passages) < 2:
        raise gr.Error("Enter at least two passages, separated by a blank line.")
    if len(passages) > 200:
        raise gr.Error("At most 200 passages per request.")
    k = int(min(k, len(passages)))
    r, E, q = neural_scores(query, passages)
    user_f = dict(rank=int(rank), gamma=gamma, rho=1.0, sigma2=sigma2, beta=beta,
                  delta=np.inf if delta >= 10 else delta)
    orders, gains = {}, {}
    orders["CE"] = by_score(r)
    orders["Dense"] = by_score(E @ q)
    orders["MMR"] = mmr(r, E, K=k, **TUNED["MMR"])
    orders["Fusion"] = zfuse(r, E, q, **TUNED["Fusion"])
    orders["InfoMax"], gains["InfoMax"] = infomax(r, E, q, K=k, return_gains=True, **TUNED["InfoMax"])
    orders["InfoMax+F"], gains["InfoMax+F"] = infomax(fused(r, E, q, alpha), E, q, K=k, return_gains=True,
                                                       **user_f)
    short = lambda i: f"[{i + 1}] " + (passages[i][:90] + ("…" if len(passages[i]) > 90 else ""))
    table = pd.DataFrame({m: [short(i) for i in orders[m][:k]] for m in METHODS})
    table.insert(0, "Rank", list(range(1, k + 1)))
    detail = pd.DataFrame({
        "Passage": [f"[{i + 1}]" for i in range(len(passages))],
        "CE logit": np.round(r, 3),
        "Dense cosine": np.round(E @ q, 3),
        "InfoMax+F step": [orders["InfoMax+F"][:k].index(i) + 1 if i in orders["InfoMax+F"][:k] else "" for i in
                           range(len(passages))],
    })
    g = gains["InfoMax+F"]
    note = ("**InfoMax+F information gain per step (nats):** " +
            ", ".join(f"{t + 1}: {v:.3f}" for t, v in enumerate(g)))
    return table, detail, note


with open(os.path.join(os.path.dirname(__file__), "results", "beir_results.md")) as f:
    RESULTS_MD = f.read()

EX_PASSAGES = """Metformin commonly causes gastrointestinal side effects such as nausea, diarrhoea and abdominal discomfort, especially when treatment starts.

Gastrointestinal upset, including nausea and diarrhoea, is the most frequent side effect reported by patients starting metformin.

Long-term metformin use is associated with reduced vitamin B12 absorption, so periodic B12 monitoring is recommended.

Lactic acidosis is a rare but serious adverse effect of metformin, mainly in patients with severe kidney impairment.

Metformin lowers hepatic glucose production and improves insulin sensitivity in type 2 diabetes.

Taking metformin with meals or using an extended-release formulation can reduce stomach upset.

Sulfonylureas such as glipizide can cause hypoglycaemia and weight gain."""

with gr.Blocks(title="InfoMax reranker") as demo:
    gr.Markdown("# InfoMax reranker\nGreedy information-maximising selection over a candidate pool, compared with "
                "the standard MS MARCO MiniLM-L6 cross-encoder. Paste a query and passages separated by blank lines.")
    with gr.Tab("Rerank"):
        with gr.Row():
            with gr.Column(scale=1):
                query = gr.Textbox(label="Query", value="What are the side effects of metformin?")
                passages = gr.Textbox(label="Passages (blank line between passages)", lines=16, value=EX_PASSAGES)
                k = gr.Slider(1, 20, value=5, step=1, label="K (passages to select)")
                with gr.Accordion("InfoMax+F settings (defaults = dev-tuned)", open=False):
                    rank = gr.Slider(1, 16, value=4, step=1, label="Aspect rank r")
                    gamma = gr.Slider(0.5, 16, value=8.0, step=0.5, label="Relevance exponent γ")
                    sigma2 = gr.Slider(0.01, 1.0, value=0.03, step=0.01, label="Observation noise σ²")
                    beta = gr.Slider(0.0, 10.0, value=3.0, step=0.1, label="Shared nuisance variance β")
                    delta = gr.Slider(0.0, 10.0, value=0.0, step=0.1,
                                      label="Anchor margin δ (10 = never leave the fused ranking)")
                    alpha = gr.Slider(0.0, 1.0, value=0.5, step=0.05, label="Dense weight α in relevance fusion")
                btn = gr.Button("Rerank", variant="primary")
            with gr.Column(scale=2):
                out = gr.Dataframe(label="Top-K per method", wrap=True)
                note = gr.Markdown()
                detail = gr.Dataframe(label="Per-passage scores", wrap=True)
        btn.click(rank_all, [query, passages, k, rank, gamma, sigma2, beta, delta, alpha], [out, detail, note])
    with gr.Tab("BEIR results"):
        gr.Markdown(RESULTS_MD)
    with gr.Tab("Method"):
        gr.Markdown(open(os.path.join(os.path.dirname(__file__), "METHOD.md")).read())

if __name__ == "__main__":
    demo.queue().launch()