Glazkov commited on
Commit
2c02558
·
verified ·
1 Parent(s): 0b28d57

Add inference.py

Browse files
Files changed (1) hide show
  1. inference.py +169 -0
inference.py ADDED
@@ -0,0 +1,169 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """High-level inference helper for the MARS shared-cross-attention checkpoint.
2
+
3
+ Uses parallel multi-mask decoding: one forward pass per token-in-span across
4
+ all masks simultaneously. Matches the recipe used to score MARSv2 = 57.02
5
+ on the CNN/DailyMail benchmark (best single-model result in the project).
6
+ """
7
+
8
+ from typing import Optional
9
+
10
+ import torch
11
+ import torch.nn.functional as F
12
+
13
+ from modeling_mars import (
14
+ ENTEND,
15
+ ENTMASK,
16
+ ENTSTART,
17
+ TYPED_ENTMASK_TOKENS,
18
+ load_model_from_checkpoint,
19
+ )
20
+
21
+
22
+ class MarsInference:
23
+ def __init__(
24
+ self,
25
+ repo_or_path: str,
26
+ base_model: str = "answerdotai/ModernBERT-base",
27
+ device: Optional[str] = None,
28
+ ):
29
+ self.model, self.tokenizer, self.device = load_model_from_checkpoint(
30
+ repo_or_path, base_model=base_model, device=device
31
+ )
32
+ self.entmask_id = self.tokenizer.convert_tokens_to_ids(ENTMASK)
33
+ self.entstart_id = self.tokenizer.convert_tokens_to_ids(ENTSTART)
34
+ self.entend_id = self.tokenizer.convert_tokens_to_ids(ENTEND)
35
+ self.typed_entmask_ids = {
36
+ t: self.tokenizer.convert_tokens_to_ids(tok)
37
+ for t, tok in TYPED_ENTMASK_TOKENS.items()
38
+ }
39
+
40
+ def _replace_masks(self, masked_text: str, entity_types: Optional[list[str]]) -> str:
41
+ if not entity_types:
42
+ return masked_text.replace("<mask>", f"{ENTSTART} {ENTMASK}")
43
+ parts = masked_text.split("<mask>")
44
+ out = parts[0]
45
+ for j in range(len(parts) - 1):
46
+ tok = TYPED_ENTMASK_TOKENS.get(entity_types[j], ENTMASK) if j < len(entity_types) else ENTMASK
47
+ out += f"{ENTSTART} {tok}" + parts[j + 1]
48
+ return out
49
+
50
+ def _is_mask(self, ids: torch.Tensor) -> torch.Tensor:
51
+ m = ids == self.entmask_id
52
+ for tid in self.typed_entmask_ids.values():
53
+ m = m | (ids == tid)
54
+ return m
55
+
56
+ @torch.no_grad()
57
+ def predict(
58
+ self,
59
+ summary: str,
60
+ masked_text: str,
61
+ max_length: int = 512,
62
+ max_span_len: int = 8,
63
+ entity_types: Optional[list[str]] = None,
64
+ return_confidence: bool = False,
65
+ ):
66
+ """Predict the entities that fill each `<mask>` in `masked_text`.
67
+
68
+ Args:
69
+ summary: short summary text providing the grounding context.
70
+ masked_text: original text with each entity replaced by `<mask>`.
71
+ entity_types: optional spaCy NER types per mask (e.g. "PERSON",
72
+ "ORG", ...). When given, the model uses the matching typed
73
+ mask token which improves accuracy.
74
+
75
+ Returns: list of predicted entity strings, one per `<mask>` (and
76
+ optionally a parallel list of mean-token confidences).
77
+ """
78
+ processed = self._replace_masks(masked_text, entity_types)
79
+
80
+ summary_enc = self.tokenizer(
81
+ summary, padding=True, truncation=True,
82
+ max_length=max_length // 2, return_tensors="pt",
83
+ ).to(self.device)
84
+ m_tok = self.tokenizer(processed, add_special_tokens=False, return_tensors="pt")
85
+ masked_ids = m_tok["input_ids"].squeeze(0)
86
+
87
+ end_ids = {self.entend_id, self.tokenizer.sep_token_id}
88
+ if self.tokenizer.eos_token_id is not None:
89
+ end_ids.add(self.tokenizer.eos_token_id)
90
+
91
+ positions = self._is_mask(masked_ids).nonzero(as_tuple=False).squeeze(-1).tolist()
92
+ n = len(positions)
93
+ if n == 0:
94
+ return ([], []) if return_confidence else []
95
+
96
+ spans: list[list[int]] = [[] for _ in range(n)]
97
+ confs: list[list[float]] = [[] for _ in range(n)]
98
+ done: list[bool] = [False] * n
99
+
100
+ for _ in range(max_span_len):
101
+ active = [(i, positions[i]) for i in range(n) if not done[i]]
102
+ if not active:
103
+ break
104
+
105
+ inp = masked_ids.unsqueeze(0).to(self.device)
106
+ attn = torch.ones_like(inp)
107
+ out = self.model(
108
+ summary_input_ids=summary_enc["input_ids"],
109
+ summary_attention_mask=summary_enc["attention_mask"],
110
+ masked_input_ids=inp,
111
+ masked_attention_mask=attn,
112
+ )
113
+ logits = out.logits
114
+
115
+ insertions = []
116
+ for ent_idx, pos in active:
117
+ tl = logits[0, pos, :].clone()
118
+ tl[self.entmask_id] = float("-inf")
119
+ tl[self.entstart_id] = float("-inf")
120
+ for tid in self.typed_entmask_ids.values():
121
+ tl[tid] = float("-inf")
122
+ p = F.softmax(tl, dim=-1)
123
+ nid = int(torch.argmax(p).item())
124
+ c = float(p[nid].item())
125
+ if nid in end_ids:
126
+ done[ent_idx] = True
127
+ else:
128
+ insertions.append((ent_idx, pos, nid, c))
129
+
130
+ if not insertions:
131
+ break
132
+
133
+ for ent_idx, pos, tok_id, c in sorted(insertions, key=lambda x: x[1], reverse=True):
134
+ new_tok = torch.tensor([tok_id], dtype=masked_ids.dtype)
135
+ masked_ids = torch.cat([masked_ids[:pos], new_tok, masked_ids[pos:]])
136
+ spans[ent_idx].append(tok_id)
137
+ confs[ent_idx].append(c)
138
+ positions[ent_idx] = pos + 1
139
+ for j in range(n):
140
+ if j != ent_idx and positions[j] >= pos:
141
+ positions[j] += 1
142
+
143
+ texts = [
144
+ self.tokenizer.decode(s, skip_special_tokens=True, clean_up_tokenization_spaces=True).strip()
145
+ for s in spans
146
+ ]
147
+ if return_confidence:
148
+ avg = [sum(c) / max(1, len(c)) for c in confs]
149
+ return texts, avg
150
+ return texts
151
+
152
+
153
+ if __name__ == "__main__":
154
+ import sys
155
+ repo = sys.argv[1] if len(sys.argv) > 1 else "Glazkov/mars-shared-cross-attention-modernbert"
156
+ summary = (
157
+ "The president announced a new climate policy in Washington on Tuesday, "
158
+ "promising to cut emissions by 40% by 2030."
159
+ )
160
+ masked = (
161
+ "<mask> announced a new climate policy in <mask> on <mask>, "
162
+ "promising to cut emissions by <mask> by <mask>."
163
+ )
164
+ types = ["PERSON", "GPE", "DATE", "PERCENT", "DATE"]
165
+
166
+ inf = MarsInference(repo)
167
+ preds, c = inf.predict(summary, masked, entity_types=types, return_confidence=True)
168
+ for m, p, x in zip(types, preds, c):
169
+ print(f" [{m}] -> {p!r} (conf={x:.2f})")