fernandofernandes commited on
Commit
13bc427
·
verified ·
1 Parent(s): 6078685

Upload folder using huggingface_hub

Browse files
config.json ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "architectures": [
3
+ "Lfm2BidirForRuleMatching"
4
+ ],
5
+ "auto_map": {
6
+ "AutoModel": "modeling_lfm2_bidir_theirs.Lfm2BidirectionalModel_theirs",
7
+ "AutoModelForMaskedLM": "modeling_lfm2_bidir_theirs.Lfm2BidirForMaskedLM_theirs"
8
+ },
9
+ "block_auto_adjust_ff_dim": true,
10
+ "block_dim": 1024,
11
+ "block_ffn_dim_multiplier": 1.0,
12
+ "block_mlp_init_scale": 1.0,
13
+ "block_multiple_of": 256,
14
+ "block_norm_eps": 1e-05,
15
+ "block_out_init_scale": 1.0,
16
+ "block_use_swiglu": true,
17
+ "block_use_xavier_init": true,
18
+ "bos_token_id": 1,
19
+ "conv_L_cache": 3,
20
+ "conv_bias": false,
21
+ "conv_dim": 1024,
22
+ "conv_dim_out": 1024,
23
+ "conv_use_xavier_init": true,
24
+ "dtype": "float32",
25
+ "eos_token_id": 7,
26
+ "hidden_size": 1024,
27
+ "initializer_range": 0.02,
28
+ "intermediate_size": 6656,
29
+ "layer_types": [
30
+ "conv",
31
+ "conv",
32
+ "full_attention",
33
+ "conv",
34
+ "conv",
35
+ "full_attention",
36
+ "conv",
37
+ "conv",
38
+ "full_attention",
39
+ "conv",
40
+ "full_attention",
41
+ "conv",
42
+ "full_attention",
43
+ "conv",
44
+ "full_attention",
45
+ "conv"
46
+ ],
47
+ "max_position_embeddings": 128000,
48
+ "model_type": "lfm2",
49
+ "norm_eps": 1e-05,
50
+ "num_attention_heads": 16,
51
+ "num_heads": 16,
52
+ "num_hidden_layers": 16,
53
+ "num_key_value_heads": 8,
54
+ "pad_token_id": 0,
55
+ "rope_parameters": {
56
+ "rope_theta": 1000000.0,
57
+ "rope_type": "default"
58
+ },
59
+ "rule_proj_dim": 256,
60
+ "tie_word_embeddings": true,
61
+ "transformers_version": "5.1.0",
62
+ "use_cache": false,
63
+ "use_pos_enc": true,
64
+ "vocab_size": 65536
65
+ }
model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:96c70cb285aa8515361b695cd18330a381a52122ebe91e1be9f224b6537ab4a9
3
+ size 1420051844
modeling_lfm2_bidir_theirs.py ADDED
@@ -0,0 +1,236 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Self-contained inference modeling for the Lfm2 bidirectional encoder MLM
3
+ (BiEnc-preview shortconv variant, aka "bidirectional-2-exp" / mlm-bidir2).
4
+
5
+ Designed to be SHIPPED ALONGSIDE THE CHECKPOINT via `trust_remote_code`:
6
+
7
+ >>> from transformers import AutoModelForMaskedLM, AutoTokenizer, AutoModel
8
+ >>> tok = AutoTokenizer.from_pretrained("LiquidAI/mlm-bidir2", trust_remote_code=True)
9
+ >>> mlm = AutoModelForMaskedLM.from_pretrained("LiquidAI/mlm-bidir2", trust_remote_code=True)
10
+ >>> body = AutoModel.from_pretrained("LiquidAI/mlm-bidir2", trust_remote_code=True)
11
+
12
+ This file:
13
+ 1. Installs the BiEnc-preview-style bidirectional patches:
14
+ * `create_causal_mask` -> non-causal padding-only additive mask (with an
15
+ FA2 path that returns the 2D padding mask)
16
+ * `Lfm2ShortConv.forward` -> full pipeline:
17
+ in_proj -> chunk(B,C,x) -> B*x -> conv1d(symmetric pad) -> C*conv_out -> out_proj
18
+ (UNLIKE the "bidirectional-1-exp" variant which used depthwise-only conv1d
19
+ on hidden_states.)
20
+ 2. Exposes `Lfm2BidirectionalModel_theirs(Lfm2Model)` — the encoder base, with
21
+ `Lfm2Attention.is_causal = False`.
22
+ 3. Exposes `Lfm2BidirForMaskedLM_theirs(Lfm2PreTrainedModel)` — adds an MLM
23
+ head tied to `embed_tokens.weight`.
24
+
25
+ Compatible with `transformers >= 5.0`. AutoModelForMaskedLM dispatches via the
26
+ `auto_map` in config.json. The forward signature absorbs kwargs to stay
27
+ compatible across upstream signature drift between transformers minor versions.
28
+ """
29
+
30
+ from typing import Optional
31
+
32
+ import torch
33
+ import torch.nn as nn
34
+ import torch.nn.functional as F
35
+ from transformers.configuration_utils import PretrainedConfig
36
+ from transformers.modeling_outputs import BaseModelOutput, MaskedLMOutput
37
+ from transformers.modeling_utils import PreTrainedModel
38
+ from transformers.models.lfm2 import modeling_lfm2 as _lfm2_mod
39
+ from transformers.models.lfm2.configuration_lfm2 import Lfm2Config
40
+ from transformers.models.lfm2.modeling_lfm2 import (
41
+ Lfm2Attention,
42
+ Lfm2Model,
43
+ Lfm2PreTrainedModel,
44
+ Lfm2ShortConv,
45
+ apply_mask_to_padding_states,
46
+ )
47
+
48
+
49
+ # --------------------------------------------------------------------------- #
50
+ # Patch 1: bidirectional attention mask #
51
+ # --------------------------------------------------------------------------- #
52
+ def _bidirectional_mask(
53
+ config,
54
+ input_embeds: torch.Tensor = None,
55
+ attention_mask: Optional[torch.Tensor] = None,
56
+ cache_position: Optional[torch.LongTensor] = None,
57
+ past_key_values=None,
58
+ position_ids: Optional[torch.LongTensor] = None,
59
+ **kwargs,
60
+ ) -> Optional[torch.Tensor]:
61
+ # transformers 5.x renamed input_embeds -> inputs_embeds; absorb both.
62
+ if input_embeds is None:
63
+ input_embeds = kwargs.get("inputs_embeds")
64
+
65
+ if config._attn_implementation == "flash_attention_2":
66
+ # FA2 only consumes the 2D padding mask to unpad; causality is
67
+ # controlled by Lfm2Attention.is_causal = False (set below).
68
+ if attention_mask is not None and not attention_mask.all():
69
+ return attention_mask
70
+ return None
71
+
72
+ device = input_embeds.device
73
+ dtype = input_embeds.dtype
74
+ bsz, q_len = input_embeds.shape[:2]
75
+ past = past_key_values.get_seq_length() if past_key_values is not None else 0
76
+ kv_len = past + q_len
77
+
78
+ mask = torch.zeros((bsz, 1, q_len, kv_len), device=device, dtype=dtype)
79
+ if attention_mask is not None:
80
+ cur_len = attention_mask.size(-1)
81
+ key_pad_flags = (attention_mask == 0).to(device=device, dtype=torch.float32)
82
+ pad_vec = torch.zeros((bsz, kv_len), device=device, dtype=torch.float32)
83
+ if cur_len > 0:
84
+ pad_vec[:, past:past + cur_len] = key_pad_flags * -1e9
85
+ mask = mask + pad_vec.to(dtype)[:, None, None, :]
86
+ return mask
87
+
88
+
89
+ # --------------------------------------------------------------------------- #
90
+ # Patch 2: BiEnc-preview shortconv forward (full pipeline) #
91
+ # --------------------------------------------------------------------------- #
92
+ def _noncausal_shortconv_forward(
93
+ self,
94
+ hidden_states: torch.Tensor,
95
+ past_key_values=None,
96
+ cache_position=None,
97
+ attention_mask: Optional[torch.Tensor] = None,
98
+ **kwargs,
99
+ ) -> torch.Tensor:
100
+ x = apply_mask_to_padding_states(hidden_states, attention_mask)
101
+
102
+ BCx = self.in_proj(x).transpose(-1, -2)
103
+ B, C, x = BCx.chunk(3, dim=-2)
104
+ Bx = B * x
105
+
106
+ k = self.conv.weight.shape[-1]
107
+ pad = k // 2
108
+ conv_out = F.conv1d(
109
+ Bx, weight=self.conv.weight, bias=self.conv.bias,
110
+ stride=1, padding=pad, dilation=1, groups=Bx.shape[1],
111
+ )
112
+ if conv_out.shape[-1] > Bx.shape[-1]:
113
+ conv_out = conv_out[..., :Bx.shape[-1]]
114
+ elif conv_out.shape[-1] < Bx.shape[-1]:
115
+ conv_out = F.pad(conv_out, (0, Bx.shape[-1] - conv_out.shape[-1]))
116
+
117
+ y = C * conv_out
118
+ y = y.transpose(-1, -2).contiguous()
119
+ return self.out_proj(y)
120
+
121
+
122
+ def _shortconv_forward(self, *args, **kwargs):
123
+ return self.slow_forward(*args, **kwargs)
124
+
125
+
126
+ _PATCHED = False
127
+
128
+
129
+ def _install_patches() -> None:
130
+ global _PATCHED
131
+ if _PATCHED:
132
+ return
133
+ _lfm2_mod.create_causal_mask = _bidirectional_mask
134
+ Lfm2ShortConv.slow_forward = _noncausal_shortconv_forward
135
+ Lfm2ShortConv.forward = _shortconv_forward
136
+ _PATCHED = True
137
+
138
+
139
+ _install_patches()
140
+
141
+
142
+ def _set_attention_noncausal(model) -> None:
143
+ for module in model.modules():
144
+ if isinstance(module, Lfm2Attention):
145
+ module.is_causal = False
146
+
147
+
148
+ # --------------------------------------------------------------------------- #
149
+ # Base model — Lfm2 backbone, encoder-style #
150
+ # --------------------------------------------------------------------------- #
151
+ class Lfm2BidirectionalModel_theirs(Lfm2Model):
152
+ """LFM2 backbone patched for encoder-style use:
153
+ full bidirectional attention + BiEnc-preview non-causal short-conv."""
154
+
155
+ def __init__(self, config):
156
+ _install_patches()
157
+ super().__init__(config)
158
+ _set_attention_noncausal(self)
159
+
160
+
161
+ # --------------------------------------------------------------------------- #
162
+ # AutoModelForMaskedLM head #
163
+ # --------------------------------------------------------------------------- #
164
+ class Lfm2BidirForMaskedLM_theirs(Lfm2PreTrainedModel):
165
+ """Lfm2 bidirectional encoder + MLM head (tied to embed_tokens.weight)."""
166
+
167
+ config_class = Lfm2Config
168
+ base_model_prefix = "lfm2"
169
+ _tied_weights_keys = {"lm_head.weight": "lfm2.embed_tokens.weight"}
170
+
171
+ def __init__(self, config: Lfm2Config):
172
+ _install_patches()
173
+ # MLM never uses KV cache
174
+ config = type(config).from_dict({**config.to_dict(), "use_cache": False})
175
+ super().__init__(config)
176
+ self.lfm2 = Lfm2BidirectionalModel_theirs(config)
177
+ self.lm_head = nn.Linear(config.hidden_size, config.vocab_size, bias=False)
178
+ self.post_init()
179
+ # tie weights
180
+ self.lm_head.weight = self.lfm2.embed_tokens.weight
181
+
182
+ def get_input_embeddings(self):
183
+ return self.lfm2.embed_tokens
184
+
185
+ def set_input_embeddings(self, value):
186
+ self.lfm2.embed_tokens = value
187
+
188
+ def get_output_embeddings(self):
189
+ return self.lm_head
190
+
191
+ def set_output_embeddings(self, new_embeddings):
192
+ self.lm_head = new_embeddings
193
+
194
+ def forward(
195
+ self,
196
+ input_ids: Optional[torch.LongTensor] = None,
197
+ attention_mask: Optional[torch.Tensor] = None,
198
+ position_ids: Optional[torch.LongTensor] = None,
199
+ inputs_embeds: Optional[torch.FloatTensor] = None,
200
+ labels: Optional[torch.LongTensor] = None,
201
+ output_hidden_states: Optional[bool] = None,
202
+ output_attentions: Optional[bool] = None,
203
+ return_dict: Optional[bool] = None,
204
+ **kwargs,
205
+ ) -> MaskedLMOutput:
206
+ return_dict = True if return_dict is None else return_dict
207
+ outputs = self.lfm2(
208
+ input_ids=input_ids,
209
+ attention_mask=attention_mask,
210
+ position_ids=position_ids,
211
+ inputs_embeds=inputs_embeds,
212
+ use_cache=False,
213
+ output_attentions=output_attentions,
214
+ output_hidden_states=output_hidden_states,
215
+ return_dict=True,
216
+ )
217
+ hidden = outputs.last_hidden_state
218
+ logits = self.lm_head(hidden)
219
+
220
+ loss = None
221
+ if labels is not None:
222
+ loss = F.cross_entropy(
223
+ logits.view(-1, self.config.vocab_size),
224
+ labels.view(-1),
225
+ ignore_index=-100,
226
+ )
227
+
228
+ if not return_dict:
229
+ out = (logits,) + outputs[1:]
230
+ return ((loss,) + out) if loss is not None else out
231
+ return MaskedLMOutput(
232
+ loss=loss,
233
+ logits=logits,
234
+ hidden_states=outputs.hidden_states,
235
+ attentions=outputs.attentions,
236
+ )
tokenizer.json ADDED
The diff for this file is too large to render. See raw diff
 
tokenizer_config.json ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "backend": "tokenizers",
3
+ "bos_token": "<|startoftext|>",
4
+ "clean_up_tokenization_spaces": false,
5
+ "eos_token": "<|im_end|>",
6
+ "is_local": false,
7
+ "mask_token": "<|mask|>",
8
+ "model_max_length": 1000000000000000019884624838656,
9
+ "pad_token": "<|pad|>",
10
+ "tokenizer_class": "TokenizersBackend"
11
+ }
train_bizlint_v02.py ADDED
@@ -0,0 +1,237 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """bizlint v02 — GLiNER-style rule matching on the LFM2.5 bidirectional encoder.
3
+
4
+ No fixed classes: each policy RULE in the input is a label. Rule representations
5
+ are mean-pooled from the same forward pass; every text token is scored against
6
+ every rule via projected dot-product; sigmoid per (token, rule). Neutral is the
7
+ default (no rule above threshold), and new rule types need no retraining.
8
+
9
+ score[t, r] = <P_tok(h_t), P_rule(mean(h[rule_r tokens]))> / sqrt(d) + b
10
+
11
+ Input: "Policy:\n- <rule 1>\n- <rule 2>\n\nText:\n<doc>"
12
+ Labels: (T_text_tokens x R) binary matrix from span<->rule-idx supervision.
13
+ Eval: span-level F1 (span + correct rule) at sigmoid > 0.5.
14
+ """
15
+ import argparse
16
+ import json
17
+ import os
18
+
19
+ import numpy as np
20
+ import torch
21
+ import torch.nn as nn
22
+ from transformers import AutoTokenizer, Trainer, TrainingArguments
23
+ from transformers.models.lfm2.configuration_lfm2 import Lfm2Config
24
+ from transformers.models.lfm2.modeling_lfm2 import Lfm2PreTrainedModel
25
+
26
+ import modeling_lfm2_bidir_theirs as bidir
27
+
28
+ PROJ_D = 256
29
+
30
+
31
+ class Lfm2BidirForRuleMatching(Lfm2PreTrainedModel):
32
+ config_class = Lfm2Config
33
+ base_model_prefix = "lfm2"
34
+
35
+ def __init__(self, config):
36
+ super().__init__(config)
37
+ self.lfm2 = bidir.Lfm2BidirectionalModel_theirs(config)
38
+ d = getattr(config, "rule_proj_dim", PROJ_D)
39
+ self.tok_proj = nn.Linear(config.hidden_size, d)
40
+ self.rule_proj = nn.Linear(config.hidden_size, d)
41
+ self.score_bias = nn.Parameter(torch.tensor(-2.0)) # start conservative
42
+ self.post_init()
43
+
44
+ def forward(self, input_ids=None, attention_mask=None, rule_pool=None,
45
+ labels=None, label_mask=None, **kw):
46
+ # rule_pool: (B, R, T) normalized pooling weights over rule token ranges
47
+ h = self.lfm2(input_ids=input_ids, attention_mask=attention_mask,
48
+ use_cache=False, return_dict=True).last_hidden_state # (B,T,H)
49
+ rule_rep = torch.bmm(rule_pool, h) # (B,R,H)
50
+ tp = self.tok_proj(h) # (B,T,d)
51
+ rp = self.rule_proj(rule_rep) # (B,R,d)
52
+ scores = torch.einsum("btd,brd->btr", tp, rp) / (tp.shape[-1] ** 0.5) + self.score_bias
53
+ loss = None
54
+ if labels is not None:
55
+ m = label_mask.bool()
56
+ if m.any():
57
+ pos_w = torch.tensor(8.0, device=scores.device)
58
+ loss = nn.functional.binary_cross_entropy_with_logits(
59
+ scores[m], labels[m], pos_weight=pos_w)
60
+ else:
61
+ loss = scores.sum() * 0.0
62
+ return {"loss": loss, "logits": scores}
63
+
64
+
65
+ def build_prompt(policies):
66
+ return "Policy:\n" + "\n".join(f"- {p}" for p in policies) + "\n\nText:\n"
67
+
68
+
69
+ def encode_row(row, tok, max_len):
70
+ pols = row["policies"] if row["policies"] else ["(none)"]
71
+ prefix = build_prompt(pols)
72
+ full = prefix + row["text"]
73
+ enc = tok(full, truncation=True, max_length=max_len, return_offsets_mapping=True)
74
+ off = enc["offset_mapping"]
75
+ t0 = len(prefix)
76
+
77
+ # char ranges of each rule inside the prefix
78
+ ranges = []
79
+ pos = len("Policy:\n")
80
+ for ptxt in pols:
81
+ start = pos + 2 # after "- "
82
+ ranges.append((start, start + len(ptxt)))
83
+ pos = start + len(ptxt) + 1 # + newline
84
+
85
+ T = len(off)
86
+ R = len(pols)
87
+ # rule token-pooling sets
88
+ pool = np.zeros((R, T), dtype=np.float32)
89
+ for ri, (rs, re_) in enumerate(ranges):
90
+ idxs = [i for i, (a, b) in enumerate(off) if a < re_ and b > rs and a != b]
91
+ for i in idxs:
92
+ pool[ri, i] = 1.0 / max(len(idxs), 1)
93
+
94
+ # labels over text tokens
95
+ spans = [(s + t0, e + t0, ri) for s, e, ri in row["spans"]]
96
+ labels = np.zeros((T, R), dtype=np.float32)
97
+ lmask = np.zeros((T, R), dtype=np.float32)
98
+ for i, (a, b) in enumerate(off):
99
+ if b <= t0 or a == b:
100
+ continue
101
+ lmask[i, :] = 1.0
102
+ for (s, e, ri) in spans:
103
+ if a < e and b > s and 0 <= ri < R:
104
+ labels[i, ri] = 1.0
105
+ return {"input_ids": enc["input_ids"], "attention_mask": enc["attention_mask"],
106
+ "pool": pool, "labels": labels, "label_mask": lmask}
107
+
108
+
109
+ def decode_rule_spans(score_row, lmask_row, thr=0.5):
110
+ """(T,R) sigmoid scores -> set of (start,end,rule) spans over masked tokens."""
111
+ T, R = score_row.shape
112
+ spans = set()
113
+ for r in range(R):
114
+ cur = None
115
+ for i in range(T + 1):
116
+ on = i < T and lmask_row[i, r] > 0 and score_row[i, r] > thr
117
+ if on:
118
+ cur = (cur[0], i + 1, r) if cur else (i, i + 1, r)
119
+ else:
120
+ if cur: spans.add(cur)
121
+ cur = None
122
+ return spans
123
+
124
+
125
+ def gold_rule_spans(labels_row, lmask_row):
126
+ return decode_rule_spans(labels_row, lmask_row, thr=0.5)
127
+
128
+
129
+ def compute_metrics(eval_pred):
130
+ (scores, labels, lmask) = eval_pred.predictions if isinstance(eval_pred.predictions, tuple) else (eval_pred.predictions, None, None)
131
+ # Trainer passes logits only; labels via label_ids is our labels tensor
132
+ scores = 1 / (1 + np.exp(-scores))
133
+ labels, lmask = eval_pred.label_ids
134
+ tp = fp = fn = 0
135
+ for s_row, l_row, m_row in zip(scores, labels, lmask):
136
+ ps = decode_rule_spans(s_row, m_row)
137
+ gs = gold_rule_spans(l_row, m_row)
138
+ tp += len(ps & gs); fp += len(ps - gs); fn += len(gs - ps)
139
+ p = tp / max(tp + fp, 1); r = tp / max(tp + fn, 1)
140
+ return {"span_precision": p, "span_recall": r,
141
+ "span_f1": 2 * p * r / max(p + r, 1e-9)}
142
+
143
+
144
+ def main():
145
+ ap = argparse.ArgumentParser()
146
+ ap.add_argument("--data", required=True)
147
+ ap.add_argument("--base", default="LiquidAI/LFM2.5-Encoder-350M")
148
+ ap.add_argument("--out", default="bizlint_v02_ckpt")
149
+ ap.add_argument("--max-len", type=int, default=320)
150
+ ap.add_argument("--epochs", type=float, default=4)
151
+ ap.add_argument("--bsz", type=int, default=48)
152
+ ap.add_argument("--lr", type=float, default=3e-5)
153
+ ap.add_argument("--max-steps", type=int, default=-1)
154
+ args = ap.parse_args()
155
+
156
+ tok = AutoTokenizer.from_pretrained(args.base, trust_remote_code=True)
157
+ rows = [json.loads(l) for l in open(args.data)]
158
+ ds = {"train": [], "val": []}
159
+ for r in rows:
160
+ if r["split"] in ds:
161
+ ds[r["split"]].append(encode_row(r, tok, args.max_len))
162
+ print(f"train {len(ds['train'])} val {len(ds['val'])}")
163
+
164
+ cfg = Lfm2Config.from_pretrained(args.base)
165
+ cfg.rule_proj_dim = PROJ_D
166
+ model, info = Lfm2BidirForRuleMatching.from_pretrained(
167
+ args.base, config=cfg, torch_dtype=torch.float32, output_loading_info=True)
168
+ missing = [k for k in info["missing_keys"]
169
+ if not (k.startswith("tok_proj") or k.startswith("rule_proj") or k.startswith("score_bias"))]
170
+ assert not missing, f"body weights missing: {missing[:5]}"
171
+ print("load gate OK — fresh:", sorted(info["missing_keys"]))
172
+
173
+ class Collator:
174
+ def __call__(self, feats):
175
+ B = len(feats)
176
+ T = max(len(f["input_ids"]) for f in feats)
177
+ R = max(f["pool"].shape[0] for f in feats)
178
+ pad = tok.pad_token_id or 0
179
+ ii = np.full((B, T), pad, dtype=np.int64)
180
+ am = np.zeros((B, T), dtype=np.int64)
181
+ pool = np.zeros((B, R, T), dtype=np.float32)
182
+ lab = np.zeros((B, T, R), dtype=np.float32)
183
+ lm = np.zeros((B, T, R), dtype=np.float32)
184
+ for i, f in enumerate(feats):
185
+ n = len(f["input_ids"]); r = f["pool"].shape[0]
186
+ ii[i, :n] = f["input_ids"]; am[i, :n] = f["attention_mask"]
187
+ pool[i, :r, :n] = f["pool"]
188
+ lab[i, :n, :r] = f["labels"]; lm[i, :n, :r] = f["label_mask"]
189
+ return {"input_ids": torch.from_numpy(ii), "attention_mask": torch.from_numpy(am),
190
+ "rule_pool": torch.from_numpy(pool),
191
+ "labels": torch.from_numpy(lab), "label_mask": torch.from_numpy(lm)}
192
+
193
+ class RuleTrainer(Trainer):
194
+ def prediction_step(self, model, inputs, prediction_loss_only, ignore_keys=None):
195
+ with torch.no_grad():
196
+ out = model(**{k: v.to(model.device) for k, v in inputs.items()})
197
+ return (out["loss"].detach() if out["loss"] is not None else None,
198
+ out["logits"].detach().float().cpu(),
199
+ (inputs["labels"].cpu(), inputs["label_mask"].cpu()))
200
+
201
+ targs = TrainingArguments(
202
+ output_dir=args.out,
203
+ num_train_epochs=args.epochs, max_steps=args.max_steps,
204
+ per_device_train_batch_size=args.bsz, per_device_eval_batch_size=args.bsz,
205
+ learning_rate=args.lr, warmup_ratio=0.06, weight_decay=0.01,
206
+ lr_scheduler_type="cosine", bf16=True,
207
+ logging_steps=20, eval_strategy="epoch", save_strategy="no",
208
+ report_to=[], remove_unused_columns=False, seed=0,
209
+ eval_do_concat_batches=False,
210
+ )
211
+
212
+ def cm(eval_pred):
213
+ # eval_do_concat_batches=False: predictions/label_ids are LISTS of batches
214
+ tp = fp = fn = 0
215
+ for scores, (labels, lmask) in zip(eval_pred.predictions, eval_pred.label_ids):
216
+ sc = 1 / (1 + np.exp(-np.asarray(scores)))
217
+ for s_row, l_row, m_row in zip(sc, np.asarray(labels), np.asarray(lmask)):
218
+ ps = decode_rule_spans(s_row, m_row)
219
+ gs = gold_rule_spans(l_row, m_row)
220
+ tp += len(ps & gs); fp += len(ps - gs); fn += len(gs - ps)
221
+ p = tp / max(tp + fp, 1); r = tp / max(tp + fn, 1)
222
+ return {"span_precision": p, "span_recall": r, "span_f1": 2 * p * r / max(p + r, 1e-9)}
223
+
224
+ trainer = RuleTrainer(model=model, args=targs, train_dataset=ds["train"],
225
+ eval_dataset=ds["val"], data_collator=Collator(),
226
+ compute_metrics=cm)
227
+ trainer.train()
228
+ m = trainer.evaluate()
229
+ print("FINAL_VAL:", json.dumps({k: round(v, 4) for k, v in m.items() if isinstance(v, float)}))
230
+
231
+ model.save_pretrained(os.path.join(args.out, "final"))
232
+ tok.save_pretrained(os.path.join(args.out, "final"))
233
+ print("SAVED", os.path.join(args.out, "final"))
234
+
235
+
236
+ if __name__ == "__main__":
237
+ main()