rakshi719 commited on
Commit
f32c034
Β·
verified Β·
1 Parent(s): c7d3bd7

Add TabuLM training and evaluation code

Browse files
code/__pycache__/eval_baselines.cpython-310.pyc ADDED
Binary file (14.6 kB). View file
 
code/__pycache__/eval_llm_baseline.cpython-310.pyc ADDED
Binary file (8.65 kB). View file
 
code/__pycache__/finetune_tabqa.cpython-310.pyc ADDED
Binary file (19.9 kB). View file
 
code/__pycache__/kinyabert_utils.cpython-310.pyc ADDED
Binary file (801 Bytes). View file
 
code/__pycache__/lamb.cpython-310.pyc ADDED
Binary file (3.87 kB). View file
 
code/__pycache__/morpho_common.cpython-310.pyc ADDED
Binary file (12.4 kB). View file
 
code/__pycache__/morpho_data_loaders.cpython-310.pyc ADDED
Binary file (25.7 kB). View file
 
code/__pycache__/morpho_learning_rates.cpython-310.pyc ADDED
Binary file (2.15 kB). View file
 
code/__pycache__/morpho_model.cpython-310.pyc ADDED
Binary file (21.7 kB). View file
 
code/__pycache__/morpho_stub.cpython-310.pyc ADDED
Binary file (1.94 kB). View file
 
code/__pycache__/morpho_transformer.cpython-310.pyc ADDED
Binary file (20.6 kB). View file
 
code/__pycache__/tabular_data_loaders.cpython-310.pyc ADDED
Binary file (14.9 kB). View file
 
code/__pycache__/tabular_serializer.cpython-310.pyc ADDED
Binary file (4.88 kB). View file
 
code/__pycache__/tabulm_model.cpython-310.pyc ADDED
Binary file (12.7 kB). View file
 
code/adamw.py ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math
2
+
3
+ import torch
4
+ from torch.optim import Optimizer
5
+
6
+ # From: https://github.com/uds-lsv/bert-stable-fine-tuning
7
+ # Paper: On the Stability of Fine-tuning BERT: Misconceptions, Explanations, and Strong Baselines
8
+ # Marius Mosbach, Maksym Andriushchenko, Dietrich Klakow
9
+ # https://arxiv.org/abs/2006.04884
10
+
11
+ class mAdamW(Optimizer):
12
+ """ Implements Adam algorithm with weight decay fix.
13
+ Parameters:
14
+ lr (float): learning rate. Default 1e-3.
15
+ betas (tuple of 2 floats): Adams beta parameters (b1, b2). Default: (0.9, 0.999)
16
+ eps (float): Adams epsilon. Default: 1e-6
17
+ weight_decay (float): Weight decay. Default: 0.0
18
+ correct_bias (bool): can be set to False to avoid correcting bias in Adam (e.g. like in Bert TF repository). Default True.
19
+ local_normalization (bool):
20
+ max_grad_norm (bool):
21
+ """
22
+
23
+ def __init__(self, params, lr=1e-3, betas=(0.9, 0.999), eps=1e-6, weight_decay=0.0, correct_bias=True,
24
+ local_normalization=False, max_grad_norm=-1):
25
+ if lr < 0.0:
26
+ raise ValueError("Invalid learning rate: {} - should be >= 0.0".format(lr))
27
+ if not 0.0 <= betas[0] < 1.0:
28
+ raise ValueError("Invalid beta parameter: {} - should be in [0.0, 1.0[".format(betas[0]))
29
+ if not 0.0 <= betas[1] < 1.0:
30
+ raise ValueError("Invalid beta parameter: {} - should be in [0.0, 1.0[".format(betas[1]))
31
+ if not 0.0 <= eps:
32
+ raise ValueError("Invalid epsilon value: {} - should be >= 0.0".format(eps))
33
+ defaults = dict(lr=lr, betas=betas, eps=eps, weight_decay=weight_decay, correct_bias=correct_bias,
34
+ local_normalization=local_normalization, max_grad_norm=max_grad_norm)
35
+ super().__init__(params, defaults)
36
+
37
+ def step(self, closure=None):
38
+ """Performs a single optimization step.
39
+ Arguments:
40
+ closure (callable, optional): A closure that reevaluates the model
41
+ and returns the loss.
42
+ """
43
+ loss = None
44
+ if closure is not None:
45
+ loss = closure()
46
+
47
+ updates = []
48
+
49
+ for group in self.param_groups:
50
+
51
+ group_updates = []
52
+
53
+ for p in group["params"]:
54
+ if p.grad is None:
55
+ continue
56
+
57
+ # Normalize gradients locally (layer-wise)
58
+ if group["local_normalization"]:
59
+ torch.nn.utils.clip_grad_norm_(p, group["max_grad_norm"])
60
+
61
+ grad = p.grad.data
62
+ if grad.is_sparse:
63
+ raise RuntimeError("Adam does not support sparse gradients, please consider SparseAdam instead")
64
+
65
+ state = self.state[p]
66
+
67
+ # State initialization
68
+ if len(state) == 0:
69
+ state["step"] = 0
70
+ # Exponential moving average of gradient values
71
+ state["exp_avg"] = torch.zeros_like(p.data)
72
+ # Exponential moving average of squared gradient values
73
+ state["exp_avg_sq"] = torch.zeros_like(p.data)
74
+
75
+ exp_avg, exp_avg_sq = state["exp_avg"], state["exp_avg_sq"]
76
+ beta1, beta2 = group["betas"]
77
+
78
+ state["step"] += 1
79
+
80
+ # Decay the first and second moment running average coefficient
81
+ # In-place operations to update the averages at the same time
82
+ exp_avg.mul_(beta1).add_(grad, alpha=1.0 - beta1)
83
+ exp_avg_sq.mul_(beta2).addcmul_(grad, grad, value=1.0 - beta2)
84
+ denom = exp_avg_sq.sqrt().add_(group["eps"])
85
+
86
+ step_size = group["lr"]
87
+ if group["correct_bias"]: # No bias correction for Bert
88
+ bias_correction1 = 1.0 - beta1 ** state["step"]
89
+ bias_correction2 = 1.0 - beta2 ** state["step"]
90
+ step_size = step_size * math.sqrt(bias_correction2) / bias_correction1
91
+
92
+ p.data.addcdiv_(-step_size, exp_avg, denom)
93
+
94
+ # Just adding the square of the weights to the loss function is *not*
95
+ # the correct way of using L2 regularization/weight decay with Adam,
96
+ # since that will interact with the m and v parameters in strange ways.
97
+ #
98
+ # Instead we want to decay the weights in a manner that doesn't interact
99
+ # with the m/v parameters. This is equivalent to adding the square
100
+ # of the weights to the loss with plain (non-momentum) SGD.
101
+ # Add weight decay at the end (fixed version)
102
+ if group["weight_decay"] > 0.0:
103
+ p.data.add_(p.data, alpha=-group["lr"] * group["weight_decay"])
104
+
105
+ group_updates.append((exp_avg, denom))
106
+
107
+ updates.append(group_updates)
108
+
109
+ return loss, updates
code/eval_baselines.py ADDED
@@ -0,0 +1,423 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Baseline evaluation on TabQA-kin using mBERT, XLM-R, and KinyaBERT.
3
+
4
+ Each baseline uses the same cell-selection approach as TabuLM:
5
+ - Serialize table as flat text, track (row, col) token positions
6
+ - Pool hidden states per cell β†’ linear cell scorer
7
+ - Same 80/20 train/dev split (seed=42), 20 epochs, lr=2e-5
8
+ - Gold cell found by text-matching (same as finetune_tabqa.py)
9
+ """
10
+
11
+ import argparse
12
+ import json
13
+ import os
14
+ import random
15
+ import sys
16
+ from datetime import datetime
17
+ from typing import Dict, List, Optional, Tuple
18
+
19
+ import numpy as np
20
+ import torch
21
+ import torch.nn as nn
22
+ from torch.nn.utils import clip_grad_norm_
23
+ from transformers import AutoTokenizer, AutoModel
24
+
25
+ DATA_DIR = '/shared/scratch/0/tmp/v_ireddi_rakshitha_results/tabulm/data'
26
+ CODE_DIR = '/shared/scratch/0/tmp/v_ireddi_rakshitha_results/tabulm/code'
27
+ TABQA_FILE = os.path.join(DATA_DIR, 'tabqa_kin.json')
28
+ CSV_DIR = os.path.join(DATA_DIR, 'tables')
29
+
30
+ sys.path.insert(0, CODE_DIR)
31
+ from tabular_serializer import serialize_csv, TableCell
32
+
33
+ # ── Model registry ─────────────────────────────────────────────────────────────
34
+ MODELS = {
35
+ 'mbert': 'bert-base-multilingual-cased',
36
+ 'xlmr': 'xlm-roberta-base',
37
+ 'kinyabert': 'jean-paul/KinyaBERT-large',
38
+ }
39
+
40
+
41
+ # ── Gold cell lookup (same as finetune_tabqa.py) ───────────────────────────────
42
+
43
+ def find_gold_cell(cells: List[TableCell], answer_text: str,
44
+ question_text: str = '') -> Optional[Tuple[int, int]]:
45
+ answer_norm = answer_text.strip().lower()
46
+ matches = [(c.row_id, c.col_id) for c in cells
47
+ if c.row_id > 1 and c.col_id > 0 and c.content.strip() == answer_text.strip()]
48
+ if not matches:
49
+ matches = [(c.row_id, c.col_id) for c in cells
50
+ if c.row_id > 1 and c.col_id > 0 and c.content.strip().lower() == answer_norm]
51
+ if not matches:
52
+ return None
53
+ if len(matches) == 1:
54
+ return matches[0]
55
+ if question_text:
56
+ q_words = set(question_text.lower().split())
57
+ row_labels = {c.row_id: c.content.strip().lower()
58
+ for c in cells if c.col_id == 1 and c.row_id > 1}
59
+ col_headers = {c.col_id: c.content.strip().lower()
60
+ for c in cells if c.row_id == 1 and c.col_id > 0}
61
+ best, best_score = None, (-1, -1)
62
+ for (row_id, col_id) in matches:
63
+ r_sc = len(q_words & set(row_labels.get(row_id, '').split()))
64
+ c_sc = len(q_words & set(col_headers.get(col_id, '').split()))
65
+ if (r_sc, c_sc) > best_score:
66
+ best_score, best = (r_sc, c_sc), (row_id, col_id)
67
+ if best_score[0] > 0 or best_score[1] > 0:
68
+ return best
69
+ return min(matches, key=lambda rc: (rc[0], rc[1]))
70
+
71
+
72
+ # ── Table serialization for HF tokenizers ─────────────────────────────────────
73
+
74
+ def serialize_table_for_bert(cells: List[TableCell], question: str,
75
+ tokenizer, max_seq_len: int = 512):
76
+ """
77
+ Linearize table + question for a BERT-style model.
78
+ Format: [CLS] h1 | h2 | ... [SEP] v1 | v2 | ... [SEP] ... question [SEP]
79
+ Tracks which token positions belong to each (row_id, col_id) data cell.
80
+ Returns (input_ids, attn_mask, ordered_cells, cell_to_positions) or None.
81
+ """
82
+ rows: Dict[int, Dict[int, str]] = {}
83
+ for c in cells:
84
+ if c.row_id > 0 and c.col_id > 0:
85
+ rows.setdefault(c.row_id, {})[c.col_id] = c.content.strip()
86
+
87
+ if not rows:
88
+ return None
89
+
90
+ sorted_rows = sorted(rows.keys())
91
+ col_ids_sorted = sorted(rows[sorted_rows[0]].keys())
92
+
93
+ tokens: List[int] = [tokenizer.cls_token_id]
94
+ cell_to_positions: Dict[Tuple[int, int], List[int]] = {}
95
+
96
+ pipe_id = tokenizer.convert_tokens_to_ids('|')
97
+ if pipe_id == tokenizer.unk_token_id:
98
+ pipe_id = None # skip pipe separator if not in vocab
99
+
100
+ for row_id in sorted_rows:
101
+ for i, col_id in enumerate(col_ids_sorted):
102
+ content = rows[row_id].get(col_id, '')
103
+ sub = tokenizer.encode(content, add_special_tokens=False)
104
+ if sub:
105
+ start = len(tokens)
106
+ tokens.extend(sub)
107
+ if row_id > 1: # data rows only (header row_id=1 not scored)
108
+ cell_to_positions.setdefault((row_id, col_id), []).extend(
109
+ range(start, len(tokens))
110
+ )
111
+ if pipe_id is not None and i < len(col_ids_sorted) - 1:
112
+ tokens.append(pipe_id)
113
+ tokens.append(tokenizer.sep_token_id)
114
+
115
+ # Question
116
+ q_ids = tokenizer.encode(question, add_special_tokens=False)
117
+ tokens.extend(q_ids)
118
+ tokens.append(tokenizer.sep_token_id)
119
+
120
+ # Truncate
121
+ if len(tokens) > max_seq_len:
122
+ tokens = tokens[:max_seq_len - 1] + [tokenizer.sep_token_id]
123
+ cell_to_positions = {
124
+ k: [p for p in v if p < max_seq_len]
125
+ for k, v in cell_to_positions.items()
126
+ }
127
+ cell_to_positions = {k: v for k, v in cell_to_positions.items() if v}
128
+
129
+ input_ids = torch.tensor(tokens, dtype=torch.long)
130
+ attn_mask = torch.ones(len(tokens), dtype=torch.long)
131
+ ordered_cells = sorted(cell_to_positions.keys(),
132
+ key=lambda rc: min(cell_to_positions[rc]))
133
+
134
+ return input_ids, attn_mask, ordered_cells, cell_to_positions
135
+
136
+
137
+ # ── Cell-scoring model ─────────────────────────────────────────────────────────
138
+
139
+ class BaselineCellScorer(nn.Module):
140
+ def __init__(self, encoder, d_model: int):
141
+ super().__init__()
142
+ self.encoder = encoder
143
+ self.cell_head = nn.Linear(d_model, 1)
144
+ nn.init.normal_(self.cell_head.weight, std=0.02)
145
+ nn.init.zeros_(self.cell_head.bias)
146
+
147
+ def forward(self, input_ids, attention_mask, ordered_cells,
148
+ cell_to_positions, device):
149
+ out = self.encoder(
150
+ input_ids=input_ids.unsqueeze(0).to(device),
151
+ attention_mask=attention_mask.unsqueeze(0).to(device),
152
+ )
153
+ hidden = out.last_hidden_state[0] # (S, d)
154
+ S = hidden.size(0)
155
+
156
+ cell_embeds, valid_cells = [], []
157
+ for rc in ordered_cells:
158
+ positions = [p for p in cell_to_positions[rc] if p < S]
159
+ if not positions:
160
+ continue
161
+ h = hidden[positions].mean(0)
162
+ cell_embeds.append(h)
163
+ valid_cells.append(rc)
164
+
165
+ if not cell_embeds:
166
+ return None, []
167
+
168
+ scores = self.cell_head(torch.stack(cell_embeds)).squeeze(-1)
169
+ return scores, valid_cells
170
+
171
+
172
+ # ── Lookup prediction helper ───────────────────────────────────────────────────
173
+
174
+ def _predict_lookup(scores, valid_cells, cells, question_text):
175
+ q_words = set(question_text.lower().split())
176
+ row_labels = {c.row_id: c.content.strip().lower()
177
+ for c in cells if c.col_id == 1 and c.row_id > 1}
178
+ col_headers = {c.col_id: c.content.strip().lower()
179
+ for c in cells if c.row_id == 1 and c.col_id > 0}
180
+
181
+ if not q_words:
182
+ return valid_cells[scores.argmax().item()]
183
+
184
+ row_score = {r: len(q_words & set(lbl.split())) for r, lbl in row_labels.items()}
185
+ col_score = {c: len(q_words & set(hdr.split())) for c, hdr in col_headers.items()}
186
+
187
+ best_row = max(row_score, key=row_score.get) if row_score else None
188
+ best_col = max(col_score, key=col_score.get) if col_score else None
189
+ has_row = best_row is not None and row_score[best_row] > 0
190
+ has_col = best_col is not None and col_score[best_col] > 0
191
+
192
+ if not has_row and not has_col:
193
+ return valid_cells[scores.argmax().item()]
194
+
195
+ if has_row:
196
+ row_indices = [i for i, rc in enumerate(valid_cells) if rc[0] == best_row]
197
+ else:
198
+ row_indices = list(range(len(valid_cells)))
199
+
200
+ if not row_indices:
201
+ return valid_cells[scores.argmax().item()]
202
+
203
+ if has_col:
204
+ both = [i for i in row_indices if valid_cells[i][1] == best_col]
205
+ if both:
206
+ return valid_cells[max(both, key=lambda i: scores[i].item())]
207
+
208
+ return valid_cells[max(row_indices, key=lambda i: scores[i].item())]
209
+
210
+
211
+ def _predict_comparison(scores, valid_cells, cells, question_text):
212
+ """For comparison questions: restrict to col-1 (entity name) cells of the
213
+ top-2 question-relevant rows. The answer is the winning entity name."""
214
+ q_words = set(question_text.lower().split())
215
+ row_labels = {c.row_id: c.content.strip().lower()
216
+ for c in cells if c.col_id == 1 and c.row_id > 1}
217
+
218
+ if not q_words or not row_labels:
219
+ return valid_cells[scores.argmax().item()]
220
+
221
+ row_score = {r: len(q_words & set(lbl.split())) for r, lbl in row_labels.items()}
222
+ top2 = sorted([r for r, s in row_score.items() if s > 0],
223
+ key=lambda r: row_score[r], reverse=True)[:2]
224
+
225
+ if not top2:
226
+ return valid_cells[scores.argmax().item()]
227
+
228
+ cand = [i for i, (r, c) in enumerate(valid_cells) if r in top2 and c == 1]
229
+ if not cand:
230
+ cand = [i for i, (r, c) in enumerate(valid_cells) if r in top2]
231
+ if not cand:
232
+ return valid_cells[scores.argmax().item()]
233
+
234
+ return valid_cells[max(cand, key=lambda i: scores[i].item())]
235
+
236
+
237
+ # ── Evaluation ─────────────────────────────────────────────────────────────────
238
+
239
+ def evaluate(model, tokenizer, items, csv_dir, device, split='dev'):
240
+ model.eval()
241
+ correct, total, skipped = 0, 0, 0
242
+ by_type: Dict[str, List[int]] = {}
243
+
244
+ with torch.no_grad():
245
+ for item in items:
246
+ csv_path = os.path.join(csv_dir, item['table_file'])
247
+ if not os.path.exists(csv_path):
248
+ skipped += 1; continue
249
+ cells = serialize_csv(csv_path)
250
+ if not cells:
251
+ skipped += 1; continue
252
+
253
+ question = item['question']
254
+ atype = item.get('answer_type', '?')
255
+
256
+ gold_rc = find_gold_cell(cells, item['answer'], question_text=question)
257
+ if gold_rc is None:
258
+ skipped += 1; continue
259
+
260
+ enc = serialize_table_for_bert(cells, question, tokenizer)
261
+ if enc is None:
262
+ skipped += 1; continue
263
+ input_ids, attn_mask, ordered_cells, cell_to_positions = enc
264
+
265
+ if gold_rc not in cell_to_positions:
266
+ skipped += 1; continue
267
+
268
+ scores, valid_cells = model(input_ids, attn_mask, ordered_cells,
269
+ cell_to_positions, device)
270
+ if scores is None or gold_rc not in valid_cells:
271
+ skipped += 1; continue
272
+
273
+ if atype == 'lookup':
274
+ pred_rc = _predict_lookup(scores, valid_cells, cells, question)
275
+ elif atype == 'comparison':
276
+ pred_rc = _predict_comparison(scores, valid_cells, cells, question)
277
+ else:
278
+ pred_rc = valid_cells[scores.argmax().item()]
279
+ hit = int(pred_rc == gold_rc)
280
+ correct += hit; total += 1
281
+ by_type.setdefault(atype, []).append(hit)
282
+
283
+ em = correct / total if total > 0 else 0.0
284
+ ts = datetime.now().strftime('%H:%M:%S')
285
+ print(f' [{ts}] {split} EM={em:.4f} ({correct}/{total}, {skipped} skipped)')
286
+ for t, hits in sorted(by_type.items()):
287
+ print(f' {t}: {sum(hits)}/{len(hits)} = {sum(hits)/len(hits):.3f}')
288
+ return em, {t: round(sum(h)/len(h), 4) for t, h in by_type.items()}
289
+
290
+
291
+ # ── Training ───────────────────────────────────────────────────────────────────
292
+
293
+ def train_baseline(model_name, hf_name, train_items, dev_items, csv_dir,
294
+ device, num_epochs=20, lr=2e-5):
295
+ print(f'\n[BL] ===== {model_name.upper()} ({hf_name}) =====')
296
+ tokenizer = AutoTokenizer.from_pretrained(hf_name)
297
+ encoder = AutoModel.from_pretrained(hf_name)
298
+ d_model = encoder.config.hidden_size
299
+ model = BaselineCellScorer(encoder, d_model).to(device)
300
+
301
+ print(f'[BL] d_model={d_model} '
302
+ f'params={sum(p.numel() for p in model.parameters()):,}')
303
+
304
+ print('[BL] Zero-shot baseline:')
305
+ best_em, best_by_type = evaluate(model, tokenizer, dev_items,
306
+ csv_dir, device, 'zero-shot')
307
+
308
+ optimizer = torch.optim.AdamW(model.parameters(), lr=lr, weight_decay=0.01)
309
+ criterion = nn.CrossEntropyLoss()
310
+ history = []
311
+
312
+ for epoch in range(1, num_epochs + 1):
313
+ model.train()
314
+ random.shuffle(train_items)
315
+ total_loss, n_ok, n_skip = 0.0, 0, 0
316
+
317
+ for item in train_items:
318
+ csv_path = os.path.join(csv_dir, item['table_file'])
319
+ if not os.path.exists(csv_path):
320
+ n_skip += 1; continue
321
+ cells = serialize_csv(csv_path)
322
+ if not cells:
323
+ n_skip += 1; continue
324
+
325
+ gold_rc = find_gold_cell(cells, item['answer'])
326
+ if gold_rc is None:
327
+ n_skip += 1; continue
328
+
329
+ enc = serialize_table_for_bert(cells, item['question'], tokenizer)
330
+ if enc is None:
331
+ n_skip += 1; continue
332
+ input_ids, attn_mask, ordered_cells, cell_to_positions = enc
333
+
334
+ if gold_rc not in cell_to_positions:
335
+ n_skip += 1; continue
336
+
337
+ scores, valid_cells = model(input_ids, attn_mask, ordered_cells,
338
+ cell_to_positions, device)
339
+ if scores is None or gold_rc not in valid_cells:
340
+ n_skip += 1; continue
341
+
342
+ gold_idx = torch.tensor([valid_cells.index(gold_rc)],
343
+ dtype=torch.long, device=device)
344
+ loss = criterion(scores.unsqueeze(0), gold_idx)
345
+ optimizer.zero_grad()
346
+ loss.backward()
347
+ clip_grad_norm_(model.parameters(), 1.0)
348
+ optimizer.step()
349
+
350
+ total_loss += loss.item(); n_ok += 1
351
+
352
+ ts = datetime.now().strftime('%H:%M:%S')
353
+ avg = total_loss / max(n_ok, 1)
354
+ print(f'[{ts}] Epoch {epoch}/{num_epochs} loss={avg:.4f} '
355
+ f'trained={n_ok} skipped={n_skip}')
356
+
357
+ em, by_type = evaluate(model, tokenizer, dev_items, csv_dir, device)
358
+ history.append({'epoch': epoch, 'loss': avg, 'dev_em': em})
359
+
360
+ if em > best_em:
361
+ best_em, best_by_type = em, by_type
362
+ sp = os.path.join(DATA_DIR, f'baseline_{model_name}_best.pt')
363
+ torch.save({'model_state_dict': model.state_dict(), 'em': em}, sp)
364
+ print(f' ** New best EM={best_em:.4f}')
365
+
366
+ print(f'\n[BL] {model_name.upper()} done. Best EM={best_em:.4f}')
367
+ return best_em, best_by_type, history
368
+
369
+
370
+ # ── Main ───────────────────────────────────────────────────────────────────────
371
+
372
+ def main():
373
+ parser = argparse.ArgumentParser()
374
+ parser.add_argument('--models', nargs='+', default=['mbert'],
375
+ choices=list(MODELS.keys()))
376
+ parser.add_argument('--epochs', type=int, default=20)
377
+ parser.add_argument('--lr', type=float, default=2e-5)
378
+ args = parser.parse_args()
379
+
380
+ random.seed(42); np.random.seed(42); torch.manual_seed(42)
381
+ device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
382
+ print(f'[BL] Device: {device}')
383
+
384
+ with open(TABQA_FILE) as f:
385
+ all_items = json.load(f)
386
+ random.shuffle(all_items)
387
+ split_n = int(0.8 * len(all_items))
388
+ train_items = all_items[:split_n]
389
+ dev_items = all_items[split_n:]
390
+ print(f'[BL] {len(all_items)} items Train={len(train_items)} Dev={len(dev_items)}')
391
+
392
+ all_results = {}
393
+ for model_name in args.models:
394
+ hf_name = MODELS[model_name]
395
+ best_em, best_by_type, history = train_baseline(
396
+ model_name, hf_name, train_items, dev_items, CSV_DIR, device,
397
+ num_epochs=args.epochs, lr=args.lr,
398
+ )
399
+ all_results[model_name] = {
400
+ 'hf_name': hf_name, 'best_em': best_em,
401
+ 'by_type': best_by_type, 'history': history,
402
+ }
403
+
404
+ out = os.path.join(DATA_DIR, 'baseline_results.json')
405
+ with open(out, 'w') as f:
406
+ json.dump(all_results, f, indent=2)
407
+ print(f'\n[BL] Results saved to {out}')
408
+
409
+ print('\n' + '='*58)
410
+ print(f'{"Model":<18} {"EM":>6} {"Agg":>5} {"Cmp":>5} {"Lkp":>5}')
411
+ print('-'*58)
412
+ for name, r in all_results.items():
413
+ bt = r['by_type']
414
+ print(f'{name:<18} {r["best_em"]:>6.3f} '
415
+ f'{bt.get("aggregation",0):>5.3f} '
416
+ f'{bt.get("comparison",0):>5.3f} '
417
+ f'{bt.get("lookup",0):>5.3f}')
418
+ print(f'{"TabuLM (ours)":<18} {"0.560":>6} '
419
+ f'{"0.792":>5} {"0.417":>5} {"0.286":>5}')
420
+
421
+
422
+ if __name__ == '__main__':
423
+ main()
code/eval_compare.py ADDED
@@ -0,0 +1,377 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Paired per-item evaluation: TabuLM vs KinyaBERT on the same dev items.
3
+
4
+ Outputs:
5
+ data/compare_results.json β€” per-item predictions for both models
6
+ Prints bootstrap 95% CI for both models
7
+ Prints comparison examples (TabuLM correct, KinyaBERT wrong) for error analysis
8
+ """
9
+
10
+ import argparse
11
+ import json
12
+ import os
13
+ import random
14
+ import sys
15
+ from typing import Dict, List, Optional, Tuple
16
+
17
+ import numpy as np
18
+ import torch
19
+ import torch.nn as nn
20
+ from transformers import AutoTokenizer, AutoModel
21
+
22
+ DATA_DIR = '/shared/scratch/0/tmp/v_ireddi_rakshitha_results/tabulm/data'
23
+ CODE_DIR = '/shared/scratch/0/tmp/v_ireddi_rakshitha_results/tabulm/code'
24
+ TABQA_FILE = os.path.join(DATA_DIR, 'tabqa_kin.json')
25
+ CSV_DIR = os.path.join(DATA_DIR, 'tables')
26
+
27
+ TABULM_CKPT = os.path.join(DATA_DIR, 'finetune_tabqa_v3_best.pt')
28
+ KINYABERT_CKPT = os.path.join(DATA_DIR, 'baseline_kinyabert_best.pt')
29
+
30
+ sys.path.insert(0, CODE_DIR)
31
+
32
+ import youtokentome as yttm
33
+ from morpho_data_loaders import KBVocab
34
+ from tabular_serializer import serialize_csv, table_cells_to_text, TableCell
35
+ from morpho_stub import parse_text_stub
36
+ from tabulm_model import TabuLM, tabulm_base
37
+
38
+
39
+ # ── Shared gold-cell lookup ────────────────────────────────────────────────────
40
+
41
+ def find_gold_cell(cells: List[TableCell], answer_text: str,
42
+ question_text: str = '') -> Optional[Tuple[int, int]]:
43
+ answer_norm = answer_text.strip().lower()
44
+ matches = [(c.row_id, c.col_id) for c in cells
45
+ if c.row_id > 1 and c.col_id > 0
46
+ and c.content.strip() == answer_text.strip()]
47
+ if not matches:
48
+ matches = [(c.row_id, c.col_id) for c in cells
49
+ if c.row_id > 1 and c.col_id > 0
50
+ and c.content.strip().lower() == answer_norm]
51
+ if not matches:
52
+ return None
53
+ if len(matches) == 1:
54
+ return matches[0]
55
+ if question_text:
56
+ q_words = set(question_text.lower().split())
57
+ row_labels = {c.row_id: c.content.strip().lower()
58
+ for c in cells if c.col_id == 1 and c.row_id > 1}
59
+ col_headers = {c.col_id: c.content.strip().lower()
60
+ for c in cells if c.row_id == 1 and c.col_id > 0}
61
+ best, best_score = None, (-1, -1)
62
+ for (row_id, col_id) in matches:
63
+ r_sc = len(q_words & set(row_labels.get(row_id, '').split()))
64
+ c_sc = len(q_words & set(col_headers.get(col_id, '').split()))
65
+ if (r_sc, c_sc) > best_score:
66
+ best_score, best = (r_sc, c_sc), (row_id, col_id)
67
+ if best_score[0] > 0 or best_score[1] > 0:
68
+ return best
69
+ return min(matches, key=lambda rc: (rc[0], rc[1]))
70
+
71
+
72
+ def _predict_lookup(scores, valid_cells, cells, question_text):
73
+ q_words = set(question_text.lower().split())
74
+ row_labels = {c.row_id: c.content.strip().lower()
75
+ for c in cells if c.col_id == 1 and c.row_id > 1}
76
+ col_headers = {c.col_id: c.content.strip().lower()
77
+ for c in cells if c.row_id == 1 and c.col_id > 0}
78
+ if not q_words:
79
+ return valid_cells[scores.argmax().item()]
80
+ row_score = {r: len(q_words & set(lbl.split())) for r, lbl in row_labels.items()}
81
+ col_score = {c: len(q_words & set(hdr.split())) for c, hdr in col_headers.items()}
82
+ best_row = max(row_score, key=row_score.get) if row_score else None
83
+ best_col = max(col_score, key=col_score.get) if col_score else None
84
+ has_row = best_row is not None and row_score[best_row] > 0
85
+ has_col = best_col is not None and col_score[best_col] > 0
86
+ if not has_row and not has_col:
87
+ return valid_cells[scores.argmax().item()]
88
+ row_indices = [i for i, rc in enumerate(valid_cells) if rc[0] == best_row] if has_row \
89
+ else list(range(len(valid_cells)))
90
+ if not row_indices:
91
+ return valid_cells[scores.argmax().item()]
92
+ if has_col:
93
+ both = [i for i in row_indices if valid_cells[i][1] == best_col]
94
+ if both:
95
+ return valid_cells[max(both, key=lambda i: scores[i].item())]
96
+ return valid_cells[max(row_indices, key=lambda i: scores[i].item())]
97
+
98
+
99
+ # ── TabuLM inference ───────────────────────────────────────────────────────────
100
+
101
+ def load_tabulm(device):
102
+ from finetune_tabqa import (detect_arch_from_state, encode_item, TabQAModel)
103
+ bpe = yttm.BPE(model=os.path.join(DATA_DIR, 'BPE-30k.mdl'))
104
+ kv = KBVocab()
105
+ kv.load_state_dict(torch.load(
106
+ os.path.join(DATA_DIR, 'kb_vocab_state_dict_2021-02-07.pt'),
107
+ map_location='cpu'))
108
+
109
+ saved = torch.load(TABULM_CKPT, map_location='cpu')
110
+ state = saved['model_state_dict']
111
+ pretrain_state = {k: v for k, v in state.items()
112
+ if not k.startswith('cell_head')}
113
+ if all(k.startswith('module.') for k in pretrain_state):
114
+ pretrain_state = {k[7:]: v for k, v in pretrain_state.items()}
115
+
116
+ args = detect_arch_from_state(pretrain_state)
117
+ tabulm_obj = tabulm_base(kv, None, None, device, args, saved_model_file=None)
118
+ model = TabQAModel(tabulm_obj).to(device)
119
+ model.load_state_dict(saved['model_state_dict'], strict=True)
120
+ model.eval()
121
+ return model, args, kv, bpe, encode_item
122
+
123
+
124
+ def tabulm_predict(model, args, kv, bpe, encode_item, cells, question, atype, device):
125
+ from finetune_tabqa import encode_item as _enc
126
+ enc = _enc(cells, question, kv, bpe)
127
+ if enc is None:
128
+ return None
129
+ (pos_tags, stems, affixes, tokens_lengths,
130
+ row_ids, col_ids, cell_types, ordered_cells, cell_to_positions) = enc
131
+ with torch.no_grad():
132
+ scores, valid_cells = model(
133
+ args, pos_tags, stems, affixes,
134
+ tokens_lengths, row_ids, col_ids, cell_types,
135
+ ordered_cells, cell_to_positions, device)
136
+ if scores is None:
137
+ return None
138
+ if atype == 'lookup':
139
+ return _predict_lookup(scores, valid_cells, cells, question)
140
+ return valid_cells[scores.argmax().item()]
141
+
142
+
143
+ # ── KinyaBERT inference ────────────────────────────────────────────────────────
144
+
145
+ class KBCellScorer(nn.Module):
146
+ def __init__(self, encoder, d_model):
147
+ super().__init__()
148
+ self.encoder = encoder
149
+ self.cell_head = nn.Linear(d_model, 1)
150
+
151
+ def forward(self, input_ids, attention_mask, ordered_cells,
152
+ cell_to_positions, device):
153
+ out = self.encoder(
154
+ input_ids=input_ids.unsqueeze(0).to(device),
155
+ attention_mask=attention_mask.unsqueeze(0).to(device))
156
+ hidden = out.last_hidden_state[0]
157
+ S = hidden.size(0)
158
+ cell_embeds, valid_cells = [], []
159
+ for rc in ordered_cells:
160
+ positions = [p for p in cell_to_positions[rc] if p < S]
161
+ if not positions:
162
+ continue
163
+ cell_embeds.append(hidden[positions].mean(0))
164
+ valid_cells.append(rc)
165
+ if not cell_embeds:
166
+ return None, []
167
+ scores = self.cell_head(torch.stack(cell_embeds)).squeeze(-1)
168
+ return scores, valid_cells
169
+
170
+
171
+ def load_kinyabert(device):
172
+ from eval_baselines import serialize_table_for_bert
173
+ hf_name = 'jean-paul/KinyaBERT-large'
174
+ tokenizer = AutoTokenizer.from_pretrained(hf_name)
175
+ encoder = AutoModel.from_pretrained(hf_name)
176
+ d_model = encoder.config.hidden_size
177
+ model = KBCellScorer(encoder, d_model).to(device)
178
+ saved = torch.load(KINYABERT_CKPT, map_location='cpu')
179
+ model.load_state_dict(saved['model_state_dict'], strict=False)
180
+ model.eval()
181
+ return model, tokenizer, serialize_table_for_bert
182
+
183
+
184
+ def kb_predict(model, tokenizer, serialize_fn, cells, question, atype, device):
185
+ enc = serialize_fn(cells, question, tokenizer)
186
+ if enc is None:
187
+ return None
188
+ input_ids, attn_mask, ordered_cells, cell_to_positions = enc
189
+ with torch.no_grad():
190
+ scores, valid_cells = model(
191
+ input_ids, attn_mask, ordered_cells, cell_to_positions, device)
192
+ if scores is None:
193
+ return None
194
+ if atype == 'lookup':
195
+ return _predict_lookup(scores, valid_cells, cells, question)
196
+ return valid_cells[scores.argmax().item()]
197
+
198
+
199
+ # ── Bootstrap CI ──────────────────────────────────────────────────────────────
200
+
201
+ def bootstrap_ci(hits: List[int], n_boot: int = 10000, alpha: float = 0.05):
202
+ hits_arr = np.array(hits, dtype=float)
203
+ n = len(hits_arr)
204
+ boot_means = [np.mean(np.random.choice(hits_arr, size=n, replace=True))
205
+ for _ in range(n_boot)]
206
+ boot_means = np.array(boot_means)
207
+ lo = np.percentile(boot_means, 100 * alpha / 2)
208
+ hi = np.percentile(boot_means, 100 * (1 - alpha / 2))
209
+ return float(np.mean(hits_arr)), lo, hi
210
+
211
+
212
+ def paired_bootstrap_pvalue(hits_a: List[int], hits_b: List[int],
213
+ n_boot: int = 10000) -> float:
214
+ """One-sided test: P(A > B) under H0 that A and B have same mean."""
215
+ a = np.array(hits_a, dtype=float)
216
+ b = np.array(hits_b, dtype=float)
217
+ n = len(a)
218
+ assert len(b) == n
219
+ obs_diff = np.mean(a) - np.mean(b)
220
+ diffs = []
221
+ for _ in range(n_boot):
222
+ idx = np.random.choice(n, size=n, replace=True)
223
+ diffs.append(np.mean(a[idx]) - np.mean(b[idx]))
224
+ diffs = np.array(diffs)
225
+ # two-sided p-value
226
+ p = float(np.mean(np.abs(diffs) >= np.abs(obs_diff)))
227
+ return p
228
+
229
+
230
+ # ── Main ───────────────────────────────────────────────────────────────────────
231
+
232
+ def main():
233
+ parser = argparse.ArgumentParser()
234
+ parser.add_argument('--n-boot', type=int, default=10000)
235
+ parser.add_argument('--out', default=os.path.join(DATA_DIR, 'compare_results.json'))
236
+ args_cli = parser.parse_args()
237
+
238
+ random.seed(42); np.random.seed(42); torch.manual_seed(42)
239
+ device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
240
+ print(f'[CMP] Device: {device}')
241
+
242
+ print('[CMP] Loading TabuLM...')
243
+ tabulm_model, tabulm_args, kv, bpe, enc_fn = load_tabulm(device)
244
+ print('[CMP] Loading KinyaBERT...')
245
+ kb_model, kb_tok, kb_ser = load_kinyabert(device)
246
+
247
+ with open(TABQA_FILE) as f:
248
+ all_items = json.load(f)
249
+ random.shuffle(all_items)
250
+ dev_items = all_items[int(0.8 * len(all_items)):]
251
+ print(f'[CMP] {len(dev_items)} dev items')
252
+
253
+ records = []
254
+ for item in dev_items:
255
+ csv_path = os.path.join(CSV_DIR, item['table_file'])
256
+ atype = item.get('answer_type', '?')
257
+ if atype == 'count' or not os.path.exists(csv_path):
258
+ continue
259
+
260
+ cells = serialize_csv(csv_path)
261
+ if not cells:
262
+ continue
263
+
264
+ gold_rc = find_gold_cell(cells, item['answer'], item['question'])
265
+ if gold_rc is None:
266
+ continue
267
+
268
+ # TabuLM prediction
269
+ t_pred = tabulm_predict(tabulm_model, tabulm_args, kv, bpe, enc_fn,
270
+ cells, item['question'], atype, device)
271
+ t_hit = int(t_pred == gold_rc) if t_pred is not None else None
272
+
273
+ # KinyaBERT prediction
274
+ k_pred = kb_predict(kb_model, kb_tok, kb_ser,
275
+ cells, item['question'], atype, device)
276
+ k_hit = int(k_pred == gold_rc) if k_pred is not None else None
277
+
278
+ # only count items both models can evaluate
279
+ if t_hit is None or k_hit is None:
280
+ continue
281
+
282
+ # get cell text for readable output
283
+ cell_map = {(c.row_id, c.col_id): c.content.strip() for c in cells}
284
+ records.append({
285
+ 'question': item['question'],
286
+ 'table': item['table_file'],
287
+ 'answer_type': atype,
288
+ 'gold': item['answer'],
289
+ 'gold_rc': list(gold_rc),
290
+ 'tabulm_rc': list(t_pred),
291
+ 'tabulm_pred': cell_map.get(t_pred, '?'),
292
+ 'tabulm_hit': t_hit,
293
+ 'kb_rc': list(k_pred),
294
+ 'kb_pred': cell_map.get(k_pred, '?'),
295
+ 'kb_hit': k_hit,
296
+ })
297
+
298
+ print(f'\n[CMP] Paired items: {len(records)}')
299
+
300
+ # ── Per-type breakdown ──────────────────────────────────────────────────
301
+ by_type: Dict[str, Dict] = {}
302
+ for r in records:
303
+ t = r['answer_type']
304
+ by_type.setdefault(t, {'tabulm': [], 'kb': []})
305
+ by_type[t]['tabulm'].append(r['tabulm_hit'])
306
+ by_type[t]['kb'].append(r['kb_hit'])
307
+
308
+ print(f'\n{"Type":<14} {"N":>4} {"TabuLM":>8} {"KinyaBERT":>10}')
309
+ print('-' * 44)
310
+ all_t, all_k = [], []
311
+ for t, d in sorted(by_type.items()):
312
+ n = len(d['tabulm'])
313
+ t_em = sum(d['tabulm']) / n
314
+ k_em = sum(d['kb']) / n
315
+ all_t.extend(d['tabulm']); all_k.extend(d['kb'])
316
+ print(f'{t:<14} {n:>4} {t_em:>8.3f} {k_em:>10.3f}')
317
+ n_all = len(all_t)
318
+ print(f'{"Overall":<14} {n_all:>4} {sum(all_t)/n_all:>8.3f} {sum(all_k)/n_all:>10.3f}')
319
+
320
+ # ── Bootstrap CI ───────────────────────────────────────────────────────
321
+ print(f'\n[CMP] Bootstrap CI (n_boot={args_cli.n_boot}):')
322
+ t_em, t_lo, t_hi = bootstrap_ci(all_t, args_cli.n_boot)
323
+ k_em, k_lo, k_hi = bootstrap_ci(all_k, args_cli.n_boot)
324
+ p_val = paired_bootstrap_pvalue(all_t, all_k, args_cli.n_boot)
325
+ print(f' TabuLM: {t_em:.3f} 95% CI [{t_lo:.3f}, {t_hi:.3f}]')
326
+ print(f' KinyaBERT: {k_em:.3f} 95% CI [{k_lo:.3f}, {k_hi:.3f}]')
327
+ print(f' Two-sided paired bootstrap p-value: {p_val:.4f}')
328
+
329
+ # ── Error analysis: TabuLM correct, KinyaBERT wrong ────────────────────
330
+ wins = [r for r in records if r['tabulm_hit'] == 1 and r['kb_hit'] == 0]
331
+ losses = [r for r in records if r['tabulm_hit'] == 0 and r['kb_hit'] == 1]
332
+ print(f'\n[CMP] TabuLM wins (T=1, KB=0): {len(wins)}')
333
+ print(f'[CMP] TabuLM losses (T=0, KB=1): {len(losses)}')
334
+
335
+ print('\n=== TabuLM WINS (comparison type preferred) ===')
336
+ cmp_wins = [r for r in wins if r['answer_type'] == 'comparison']
337
+ for r in cmp_wins[:5]:
338
+ print(f'\n Table : {r["table"]}')
339
+ print(f' Q : {r["question"]}')
340
+ print(f' Type : {r["answer_type"]}')
341
+ print(f' Gold : {r["gold"]} (row={r["gold_rc"][0]}, col={r["gold_rc"][1]})')
342
+ print(f' TabuLM: {r["tabulm_pred"]} βœ“')
343
+ print(f' KinyaB: {r["kb_pred"]} βœ—')
344
+
345
+ print('\n=== TabuLM LOSSES ===')
346
+ for r in losses[:3]:
347
+ print(f'\n Table : {r["table"]}')
348
+ print(f' Q : {r["question"]}')
349
+ print(f' Type : {r["answer_type"]}')
350
+ print(f' Gold : {r["gold"]}')
351
+ print(f' TabuLM: {r["tabulm_pred"]} βœ—')
352
+ print(f' KinyaB: {r["kb_pred"]} βœ“')
353
+
354
+ # ── Save ───────────────────────────────────────────────────────────────
355
+ out_data = {
356
+ 'n_paired': n_all,
357
+ 'tabulm_em': round(t_em, 4),
358
+ 'kb_em': round(k_em, 4),
359
+ 'tabulm_ci': [round(t_lo, 4), round(t_hi, 4)],
360
+ 'kb_ci': [round(k_lo, 4), round(k_hi, 4)],
361
+ 'p_value': round(p_val, 4),
362
+ 'by_type': {t: {
363
+ 'n': len(d['tabulm']),
364
+ 'tabulm_em': round(sum(d['tabulm'])/len(d['tabulm']), 4),
365
+ 'kb_em': round(sum(d['kb'])/len(d['kb']), 4),
366
+ } for t, d in by_type.items()},
367
+ 'tabulm_wins': wins,
368
+ 'tabulm_losses': losses,
369
+ 'records': records,
370
+ }
371
+ with open(args_cli.out, 'w') as f:
372
+ json.dump(out_data, f, indent=2, ensure_ascii=False)
373
+ print(f'\n[CMP] Saved to {args_cli.out}')
374
+
375
+
376
+ if __name__ == '__main__':
377
+ main()
code/eval_llm_agg_fewshot.py ADDED
@@ -0,0 +1,141 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """3-shot GPT-4o experiment on aggregation items only.
3
+ Tests whether few-shot prompting closes the aggregation gap vs TabuLM.
4
+
5
+ Usage:
6
+ python eval_llm_agg_fewshot.py --api-key sk-...
7
+ """
8
+
9
+ import argparse
10
+ import csv
11
+ import json
12
+ import os
13
+ import random
14
+ import sys
15
+ import time
16
+ from typing import List, Optional
17
+
18
+ import requests
19
+
20
+ DATA_DIR = '/shared/scratch/0/tmp/v_ireddi_rakshitha_results/tabulm/data'
21
+ CODE_DIR = '/shared/scratch/0/tmp/v_ireddi_rakshitha_results/tabulm/code'
22
+ TABQA_FILE = os.path.join(DATA_DIR, 'tabqa_kin.json')
23
+ CSV_DIR = os.path.join(DATA_DIR, 'tables')
24
+ OPENAI_URL = "https://api.openai.com/v1/chat/completions"
25
+
26
+ sys.path.insert(0, CODE_DIR)
27
+ from tabular_serializer import serialize_csv, TableCell
28
+ from eval_llm_baseline import csv_to_markdown, find_gold_cell, em_match
29
+
30
+
31
+ FEW_SHOT_EXAMPLES = [
32
+ {
33
+ "table": "| Inzego | Umubare_w_Abaturage | Umubare_w_Inzu |\n|---|---|---|\n| Gasabo | 542000 | 89000 |\n| Kicukiro | 312000 | 54000 |\n| Nyarugenge | 274000 | 48000 |",
34
+ "question": "Ni iki gifite Umubare_w_Abaturage nyinshi?",
35
+ "answer": "Gasabo",
36
+ "explanation": "Gasabo has the highest Umubare_w_Abaturage (542000).",
37
+ },
38
+ {
39
+ "table": "| Igihingwa | Umusaruro_Toni | Akarere |\n|---|---|---|\n| Ibigori | 45200 | Musanze |\n| Ibishyimbo | 12300 | Musanze |\n| Ibirayi | 89000 | Musanze |",
40
+ "question": "Ni iki gifite Umusaruro_Toni nke cyane?",
41
+ "answer": "Ibishyimbo",
42
+ "explanation": "Ibishyimbo has the lowest Umusaruro_Toni (12300).",
43
+ },
44
+ {
45
+ "table": "| Umwaka | Umusaruro_Miliyari_Frw | Impinduka_% |\n|---|---|---|\n| 2020 | 1100.2 | +3.1 |\n| 2021 | 1189.5 | +8.1 |\n| 2022 | 1209.3 | +1.7 |",
46
+ "question": "Ni iki gifite Impinduka_% ntoya muri bose?",
47
+ "answer": "2022",
48
+ "explanation": "2022 has the lowest Impinduka_% (+1.7).",
49
+ },
50
+ ]
51
+
52
+
53
+ def make_fewshot_prompt(table_md: str, question: str) -> str:
54
+ examples = ""
55
+ for ex in FEW_SHOT_EXAMPLES:
56
+ examples += (
57
+ f"Table:\n{ex['table']}\n\n"
58
+ f"Question: {ex['question']}\n"
59
+ f"Answer: {ex['answer']}\n\n"
60
+ )
61
+ return (
62
+ f"You are answering questions about data tables written in Kinyarwanda.\n"
63
+ f"The answer is always the NAME of an entity from the first column β€” NOT a number.\n"
64
+ f"Identify which entity has the highest or lowest value in the relevant column.\n\n"
65
+ f"Examples:\n\n{examples}"
66
+ f"Now answer the following:\n\n"
67
+ f"Table:\n{table_md}\n\n"
68
+ f"Question: {question}\n"
69
+ f"Answer:"
70
+ )
71
+
72
+
73
+ def call_openai(prompt: str, api_key: str, retries: int = 3) -> Optional[str]:
74
+ for attempt in range(retries):
75
+ try:
76
+ headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
77
+ payload = {"model": "gpt-4o", "max_tokens": 64, "temperature": 0,
78
+ "messages": [{"role": "user", "content": prompt}]}
79
+ r = requests.post(OPENAI_URL, headers=headers, json=payload, timeout=30)
80
+ r.raise_for_status()
81
+ return r.json()["choices"][0]["message"]["content"].strip()
82
+ except Exception as e:
83
+ if attempt < retries - 1:
84
+ time.sleep(2 ** attempt)
85
+ else:
86
+ print(f" [API error] {e}")
87
+ return None
88
+
89
+
90
+ def main():
91
+ parser = argparse.ArgumentParser()
92
+ parser.add_argument('--api-key', required=True)
93
+ parser.add_argument('--out', default=os.path.join(DATA_DIR, 'llm_agg_fewshot_results.json'))
94
+ args = parser.parse_args()
95
+
96
+ random.seed(42)
97
+ with open(TABQA_FILE) as f:
98
+ all_items = json.load(f)
99
+ random.shuffle(all_items)
100
+ dev_items = [x for x in all_items[int(0.8 * len(all_items)):]
101
+ if x.get('answer_type') == 'aggregation']
102
+ print(f'[FewShot-Agg] {len(dev_items)} aggregation dev items')
103
+
104
+ correct, total, skipped = 0, 0, 0
105
+ records = []
106
+
107
+ for i, item in enumerate(dev_items):
108
+ csv_path = os.path.join(CSV_DIR, item['table_file'])
109
+ if not os.path.exists(csv_path):
110
+ skipped += 1; continue
111
+ cells = serialize_csv(csv_path)
112
+ if not cells:
113
+ skipped += 1; continue
114
+ gold_rc = find_gold_cell(cells, item['answer'], item['question'])
115
+ if gold_rc is None:
116
+ skipped += 1; continue
117
+ table_md = csv_to_markdown(csv_path)
118
+ if not table_md:
119
+ skipped += 1; continue
120
+
121
+ prompt = make_fewshot_prompt(table_md, item['question'])
122
+ pred = call_openai(prompt, args.api_key)
123
+ if pred is None:
124
+ skipped += 1; continue
125
+
126
+ hit = int(em_match(pred, item['answer']))
127
+ correct += hit; total += 1
128
+ records.append({'question': item['question'], 'gold': item['answer'],
129
+ 'pred': pred, 'hit': hit})
130
+ print(f' [{i+1}/{len(dev_items)}] gold={item["answer"]!r:20s} pred={pred!r:20s} hit={hit}')
131
+
132
+ em = correct / total if total else 0.0
133
+ print(f'\n[FewShot-Agg] EM = {em:.4f} ({correct}/{total}, {skipped} skipped)')
134
+ with open(args.out, 'w') as f:
135
+ json.dump({'em': round(em, 4), 'correct': correct, 'total': total,
136
+ 'skipped': skipped, 'records': records}, f, indent=2, ensure_ascii=False)
137
+ print(f'[FewShot-Agg] Saved to {args.out}')
138
+
139
+
140
+ if __name__ == '__main__':
141
+ main()
code/eval_llm_baseline.py ADDED
@@ -0,0 +1,261 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Zero-shot LLM baseline on TabQA-kin dev set. Supports OpenAI and Anthropic.
3
+
4
+ Usage:
5
+ # OpenAI (GPT-4o)
6
+ python eval_llm_baseline.py --provider openai --api-key sk-...
7
+ python eval_llm_baseline.py --provider openai --api-key sk-... --model gpt-4o-mini
8
+
9
+ # Anthropic (Claude Haiku)
10
+ python eval_llm_baseline.py --provider anthropic --api-key sk-ant-...
11
+ """
12
+
13
+ import argparse
14
+ import csv
15
+ import json
16
+ import os
17
+ import random
18
+ import sys
19
+ import time
20
+ from typing import Dict, List, Optional, Tuple
21
+
22
+ import requests
23
+
24
+ DATA_DIR = '/shared/scratch/0/tmp/v_ireddi_rakshitha_results/tabulm/data'
25
+ CODE_DIR = '/shared/scratch/0/tmp/v_ireddi_rakshitha_results/tabulm/code'
26
+ TABQA_FILE = os.path.join(DATA_DIR, 'tabqa_kin.json')
27
+ CSV_DIR = os.path.join(DATA_DIR, 'tables')
28
+
29
+ sys.path.insert(0, CODE_DIR)
30
+ from tabular_serializer import serialize_csv, TableCell
31
+
32
+ ANTHROPIC_URL = "https://api.anthropic.com/v1/messages"
33
+ OPENAI_URL = "https://api.openai.com/v1/chat/completions"
34
+
35
+
36
+ # ── Table formatting ───────────────────────────────────────────────────────────
37
+
38
+ def csv_to_markdown(csv_path: str) -> Optional[str]:
39
+ try:
40
+ with open(csv_path, encoding='utf-8') as f:
41
+ rows = list(csv.reader(f))
42
+ if not rows:
43
+ return None
44
+ lines = [' | '.join(r) for r in rows]
45
+ # insert separator after header
46
+ lines.insert(1, ' | '.join(['---'] * len(rows[0])))
47
+ return '\n'.join(lines)
48
+ except Exception:
49
+ return None
50
+
51
+
52
+ # ── Gold cell lookup (same logic as finetune_tabqa.py v3) ─────────────────────
53
+
54
+ def find_gold_cell(cells: List[TableCell], answer_text: str,
55
+ question_text: str = '') -> Optional[Tuple[int, int]]:
56
+ answer_norm = answer_text.strip().lower()
57
+ matches = [(c.row_id, c.col_id) for c in cells
58
+ if c.row_id > 1 and c.col_id > 0
59
+ and c.content.strip() == answer_text.strip()]
60
+ if not matches:
61
+ matches = [(c.row_id, c.col_id) for c in cells
62
+ if c.row_id > 1 and c.col_id > 0
63
+ and c.content.strip().lower() == answer_norm]
64
+ if not matches:
65
+ return None
66
+ if len(matches) == 1:
67
+ return matches[0]
68
+ if question_text:
69
+ q_words = set(question_text.lower().split())
70
+ row_labels = {c.row_id: c.content.strip().lower()
71
+ for c in cells if c.col_id == 1 and c.row_id > 1}
72
+ col_headers = {c.col_id: c.content.strip().lower()
73
+ for c in cells if c.row_id == 1 and c.col_id > 0}
74
+ best, best_score = None, (-1, -1)
75
+ for (row_id, col_id) in matches:
76
+ r_sc = len(q_words & set(row_labels.get(row_id, '').split()))
77
+ c_sc = len(q_words & set(col_headers.get(col_id, '').split()))
78
+ if (r_sc, c_sc) > best_score:
79
+ best_score, best = (r_sc, c_sc), (row_id, col_id)
80
+ if best_score[0] > 0 or best_score[1] > 0:
81
+ return best
82
+ return min(matches, key=lambda rc: (rc[0], rc[1]))
83
+
84
+
85
+ # ── Prompt construction ────────────────────────────────────────────────────────
86
+
87
+ TYPE_HINTS = {
88
+ 'lookup': 'The answer is a specific value (number or text) from the table. Find the row matching the entity named in the question, then return the value in the relevant column.',
89
+ 'comparison': 'The answer is the NAME of one of the two entities mentioned in the question β€” NOT a number. Find both entities in the first column, compare their values in the specified data column, and return ONLY the entity name that has the higher or lower value as asked.',
90
+ 'aggregation': 'The answer is the NAME of an entity from the first column of the table β€” NOT a number. Identify which entity has the highest or lowest value in the relevant column, then return that entity\'s name exactly as it appears in the table.',
91
+ }
92
+
93
+ def make_prompt(table_md: str, question: str, answer_type: str) -> str:
94
+ hint = TYPE_HINTS.get(answer_type, '')
95
+ return (
96
+ f"You are answering a question about a data table written in Kinyarwanda.\n\n"
97
+ f"Table:\n{table_md}\n\n"
98
+ f"Question: {question}\n\n"
99
+ f"{hint}\n\n"
100
+ f"Reply with ONLY the exact value from the table that answers the question. "
101
+ f"No explanation, no added words."
102
+ )
103
+
104
+
105
+ # ── API call ───────────────────────────────────────────────────────────────────
106
+
107
+ def call_llm(prompt: str, api_key: str, model: str, provider: str,
108
+ retries: int = 3) -> Optional[str]:
109
+ for attempt in range(retries):
110
+ try:
111
+ if provider == 'openai':
112
+ headers = {
113
+ "Authorization": f"Bearer {api_key}",
114
+ "Content-Type": "application/json",
115
+ }
116
+ payload = {
117
+ "model": model,
118
+ "max_tokens": 64,
119
+ "messages": [{"role": "user", "content": prompt}],
120
+ "temperature": 0,
121
+ }
122
+ r = requests.post(OPENAI_URL, headers=headers,
123
+ json=payload, timeout=30)
124
+ r.raise_for_status()
125
+ return r.json()["choices"][0]["message"]["content"].strip()
126
+ else: # anthropic
127
+ headers = {
128
+ "x-api-key": api_key,
129
+ "anthropic-version": "2023-06-01",
130
+ "content-type": "application/json",
131
+ }
132
+ payload = {
133
+ "model": model,
134
+ "max_tokens": 64,
135
+ "messages": [{"role": "user", "content": prompt}],
136
+ }
137
+ r = requests.post(ANTHROPIC_URL, headers=headers,
138
+ json=payload, timeout=30)
139
+ if r.status_code == 529:
140
+ time.sleep(10 * (attempt + 1))
141
+ continue
142
+ r.raise_for_status()
143
+ return r.json()["content"][0]["text"].strip()
144
+ except Exception as e:
145
+ if attempt < retries - 1:
146
+ time.sleep(2 ** attempt)
147
+ else:
148
+ print(f" [API error] {e}")
149
+ return None
150
+
151
+
152
+ # ── EM with normalization ──────────────────────────────────────────────────────
153
+
154
+ def em_match(pred: str, gold: str) -> bool:
155
+ def norm(s: str) -> str:
156
+ s = s.strip().lower().replace(',', '').replace(' ', '')
157
+ try:
158
+ return f'{float(s):g}'
159
+ except ValueError:
160
+ return s
161
+ return norm(pred) == norm(gold)
162
+
163
+
164
+ # ── Main ───────────────────────────────────────────────────────────────────────
165
+
166
+ def main():
167
+ parser = argparse.ArgumentParser()
168
+ parser.add_argument('--api-key', required=True, help='OpenAI or Anthropic API key')
169
+ parser.add_argument('--provider', default='openai', choices=['openai', 'anthropic'])
170
+ parser.add_argument('--model', default=None,
171
+ help='Model name (default: gpt-4o for OpenAI, claude-haiku-4-5-20251001 for Anthropic)')
172
+ parser.add_argument('--out', default=os.path.join(DATA_DIR, 'llm_baseline_results.json'))
173
+ parser.add_argument('--max-items', type=int, default=None,
174
+ help='Limit number of dev items (for testing)')
175
+ args = parser.parse_args()
176
+ if args.model is None:
177
+ args.model = 'gpt-4o' if args.provider == 'openai' else 'claude-haiku-4-5-20251001'
178
+
179
+ random.seed(42)
180
+ with open(TABQA_FILE) as f:
181
+ all_items = json.load(f)
182
+ random.shuffle(all_items)
183
+ dev_items = all_items[int(0.8 * len(all_items)):]
184
+ if args.max_items:
185
+ dev_items = dev_items[:args.max_items]
186
+ print(f'[LLM] Provider: {args.provider} Model: {args.model}')
187
+ print(f'[LLM] {len(dev_items)} dev items')
188
+
189
+ correct, total, skipped = 0, 0, 0
190
+ by_type: Dict[str, List[int]] = {}
191
+ records = []
192
+
193
+ for i, item in enumerate(dev_items):
194
+ csv_path = os.path.join(CSV_DIR, item['table_file'])
195
+ atype = item.get('answer_type', '?')
196
+
197
+ # skip count questions (answer not a cell value)
198
+ if atype == 'count':
199
+ skipped += 1; continue
200
+
201
+ if not os.path.exists(csv_path):
202
+ skipped += 1; continue
203
+
204
+ cells = serialize_csv(csv_path)
205
+ if not cells:
206
+ skipped += 1; continue
207
+
208
+ # verify gold cell exists (same filter as our fine-tuning eval)
209
+ gold_rc = find_gold_cell(cells, item['answer'], item['question'])
210
+ if gold_rc is None:
211
+ skipped += 1; continue
212
+
213
+ table_md = csv_to_markdown(csv_path)
214
+ if not table_md:
215
+ skipped += 1; continue
216
+
217
+ prompt = make_prompt(table_md, item['question'], atype)
218
+ pred = call_llm(prompt, args.api_key, args.model, args.provider)
219
+ if pred is None:
220
+ skipped += 1; continue
221
+
222
+ hit = int(em_match(pred, item['answer']))
223
+ correct += hit
224
+ total += 1
225
+ by_type.setdefault(atype, []).append(hit)
226
+ records.append({
227
+ 'idx': i,
228
+ 'question': item['question'],
229
+ 'table': item['table_file'],
230
+ 'answer_type': atype,
231
+ 'gold': item['answer'],
232
+ 'pred': pred,
233
+ 'hit': hit,
234
+ })
235
+
236
+ if (i + 1) % 10 == 0:
237
+ running_em = correct / total if total else 0.0
238
+ print(f' [{i+1:3d}/{len(dev_items)}] running EM={running_em:.3f} '
239
+ f'correct={correct}/{total} skipped={skipped}')
240
+
241
+ em = correct / total if total else 0.0
242
+ print(f'\n[LLM] Final EM = {em:.4f} ({correct}/{total}, {skipped} skipped)')
243
+ for t, hits in sorted(by_type.items()):
244
+ print(f' {t}: {sum(hits)}/{len(hits)} = {sum(hits)/len(hits):.3f}')
245
+
246
+ result = {
247
+ 'model': args.model,
248
+ 'em': round(em, 4),
249
+ 'correct': correct,
250
+ 'total': total,
251
+ 'skipped': skipped,
252
+ 'by_type': {t: round(sum(h)/len(h), 4) for t, h in by_type.items()},
253
+ 'records': records,
254
+ }
255
+ with open(args.out, 'w') as f:
256
+ json.dump(result, f, indent=2, ensure_ascii=False)
257
+ print(f'[LLM] Results saved to {args.out}')
258
+
259
+
260
+ if __name__ == '__main__':
261
+ main()
code/finetune_tableqa.py ADDED
@@ -0,0 +1,244 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ finetune_tableqa.py β€” Fine-tune TabuLM on TabQA-kin benchmark.
3
+
4
+ Fine-tuning approach:
5
+ - Serialize table + question β†’ token string via tabular_serializer
6
+ - Append question tokens after [SEP]
7
+ - Predict answer as span (start/end token indices) for lookup/comparison/aggregation
8
+ - Predict integer for count questions
9
+ - Metrics: exact match (EM) and token-level F1
10
+
11
+ Usage:
12
+ python code/finetune_tableqa.py \
13
+ --checkpoint checkpoints/tabulm_step120000.pt \
14
+ --tabqa data/tabqa_kin.json \
15
+ --tables data/tables/ \
16
+ --output checkpoints/tabulm_tableqa/
17
+ """
18
+
19
+ import argparse
20
+ import json
21
+ import os
22
+ import random
23
+ from pathlib import Path
24
+ from typing import Dict, List, Tuple
25
+
26
+ import torch
27
+ import torch.nn as nn
28
+ from torch.utils.data import DataLoader, Dataset
29
+
30
+ random.seed(42)
31
+ torch.manual_seed(42)
32
+
33
+ # ── Span prediction head ──────────────────────────────────────────────────────
34
+
35
+ class SpanHead(nn.Module):
36
+ def __init__(self, hidden_size: int):
37
+ super().__init__()
38
+ self.start_linear = nn.Linear(hidden_size, 1)
39
+ self.end_linear = nn.Linear(hidden_size, 1)
40
+
41
+ def forward(self, hidden_states):
42
+ start_logits = self.start_linear(hidden_states).squeeze(-1)
43
+ end_logits = self.end_linear(hidden_states).squeeze(-1)
44
+ return start_logits, end_logits
45
+
46
+
47
+ class TabuLM_QA(nn.Module):
48
+ def __init__(self, encoder, hidden_size: int = 768):
49
+ super().__init__()
50
+ self.encoder = encoder
51
+ self.span_head = SpanHead(hidden_size)
52
+ self.count_head = nn.Linear(hidden_size, 100)
53
+
54
+ def forward(self, batch, answer_type: str = 'lookup'):
55
+ hidden = self.encoder(batch)['last_hidden_state']
56
+ if answer_type == 'count':
57
+ cls_hidden = hidden[:, 0, :]
58
+ return self.count_head(cls_hidden)
59
+ else:
60
+ return self.span_head(hidden)
61
+
62
+
63
+ # ── Dataset ───────────────────────────────────────────────────────────────────
64
+
65
+ class TabQADataset(Dataset):
66
+ def __init__(self, items: List[Dict], tables_dir: Path, kb_vocab, bpe):
67
+ from tabular_serializer import serialize_csv, table_cells_to_text
68
+ from morpho_stub import parse_text_stub
69
+
70
+ self.samples = []
71
+ for item in items:
72
+ table_path = tables_dir / item['table_file']
73
+ if not table_path.exists():
74
+ continue
75
+ cells = serialize_csv(str(table_path))
76
+ table_text, word_meta = table_cells_to_text(cells)
77
+
78
+ question = item['question']
79
+ full_text = table_text + ' [SEP] ' + question
80
+
81
+ tokens = parse_text_stub(full_text, kb_vocab, bpe)
82
+ self.samples.append({
83
+ 'tokens': tokens,
84
+ 'word_meta': word_meta,
85
+ 'answer': item['answer'],
86
+ 'answer_type': item['answer_type'],
87
+ 'answer_row': item.get('answer_row', -1),
88
+ 'answer_col': item.get('answer_col', -1),
89
+ 'id': item['id'],
90
+ })
91
+
92
+ def __len__(self):
93
+ return len(self.samples)
94
+
95
+ def __getitem__(self, idx):
96
+ return self.samples[idx]
97
+
98
+
99
+ # ── Metrics ───────────────────────────────────────────────────────────────────
100
+
101
+ def exact_match(pred: str, gold: str) -> float:
102
+ return float(pred.strip().lower() == gold.strip().lower())
103
+
104
+
105
+ def token_f1(pred: str, gold: str) -> float:
106
+ pred_toks = pred.strip().lower().split()
107
+ gold_toks = gold.strip().lower().split()
108
+ common = set(pred_toks) & set(gold_toks)
109
+ if not common:
110
+ return 0.0
111
+ p = len(common) / len(pred_toks) if pred_toks else 0
112
+ r = len(common) / len(gold_toks) if gold_toks else 0
113
+ if p + r == 0:
114
+ return 0.0
115
+ return 2 * p * r / (p + r)
116
+
117
+
118
+ def evaluate(model, dataloader, device) -> Dict[str, float]:
119
+ model.eval()
120
+ em_sum = f1_sum = total = 0
121
+ with torch.no_grad():
122
+ for batch in dataloader:
123
+ answer_type = batch['answer_type'][0]
124
+ preds = model(batch, answer_type=answer_type)
125
+
126
+ if answer_type == 'count':
127
+ pred_counts = preds.argmax(dim=-1).tolist()
128
+ for pred, gold in zip(pred_counts, batch['answer']):
129
+ em_sum += exact_match(str(pred), gold)
130
+ f1_sum += token_f1(str(pred), gold)
131
+ total += 1
132
+ else:
133
+ start_logits, end_logits = preds
134
+ for i in range(len(batch['answer'])):
135
+ s = start_logits[i].argmax().item()
136
+ e = end_logits[i].argmax().item()
137
+ tokens = batch['tokens'][i]
138
+ pred_text = ' '.join(t.stem for t in tokens[s:e+1]) if s <= e else ''
139
+ gold_text = batch['answer'][i]
140
+ em_sum += exact_match(pred_text, gold_text)
141
+ f1_sum += token_f1(pred_text, gold_text)
142
+ total += 1
143
+
144
+ return {
145
+ 'exact_match': em_sum / total if total else 0,
146
+ 'f1': f1_sum / total if total else 0,
147
+ 'total': total,
148
+ }
149
+
150
+
151
+ # ── Training loop ─────────────────────────────────────────────────────────────
152
+
153
+ def train(args):
154
+ device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
155
+ print(f'Device: {device}')
156
+
157
+ with open(args.tabqa, encoding='utf-8') as f:
158
+ all_items = json.load(f)
159
+
160
+ random.shuffle(all_items)
161
+ split = int(0.8 * len(all_items))
162
+ train_items = all_items[:split]
163
+ dev_items = all_items[split:]
164
+ print(f'Train: {len(train_items)} Dev: {len(dev_items)}')
165
+
166
+ import sys; sys.path.insert(0, 'code')
167
+ from morpho_common import KBVocab
168
+ from morpho_stub import BPEFallback
169
+
170
+ kb_vocab = KBVocab.load(args.kb_vocab)
171
+ bpe = BPEFallback(args.bpe_codes)
172
+
173
+ train_ds = TabQADataset(train_items, Path(args.tables), kb_vocab, bpe)
174
+ dev_ds = TabQADataset(dev_items, Path(args.tables), kb_vocab, bpe)
175
+
176
+ checkpoint = torch.load(args.checkpoint, map_location='cpu')
177
+ from tabulm_model import tabulm_base
178
+ model_core = tabulm_base(kb_vocab)
179
+ model_core.load_state_dict(checkpoint['model'], strict=False)
180
+
181
+ model = TabuLM_QA(model_core.encoder).to(device)
182
+
183
+ optimizer = torch.optim.AdamW(model.parameters(), lr=args.lr, weight_decay=0.01)
184
+ ce_loss = nn.CrossEntropyLoss()
185
+
186
+ Path(args.output).mkdir(parents=True, exist_ok=True)
187
+ best_em = 0.0
188
+
189
+ for epoch in range(args.epochs):
190
+ model.train()
191
+ total_loss = 0
192
+ for step, batch in enumerate(DataLoader(train_ds, batch_size=args.batch_size, shuffle=True)):
193
+ optimizer.zero_grad()
194
+ answer_type = batch['answer_type'][0]
195
+ preds = model(batch, answer_type=answer_type)
196
+
197
+ if answer_type == 'count':
198
+ labels = torch.tensor([int(a) for a in batch['answer']], device=device)
199
+ loss = ce_loss(preds, labels)
200
+ else:
201
+ start_logits, end_logits = preds
202
+ start_labels = torch.tensor(batch['answer_row'], device=device)
203
+ end_labels = torch.tensor(batch['answer_row'], device=device)
204
+ loss = ce_loss(start_logits, start_labels) + ce_loss(end_logits, end_labels)
205
+
206
+ loss.backward()
207
+ torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
208
+ optimizer.step()
209
+ total_loss += loss.item()
210
+
211
+ if step % 50 == 0:
212
+ print(f' epoch {epoch+1} step {step} loss={total_loss/(step+1):.4f}')
213
+
214
+ metrics = evaluate(model, DataLoader(dev_ds, batch_size=32), device)
215
+ print(f'Epoch {epoch+1} EM={metrics["exact_match"]:.4f} F1={metrics["f1"]:.4f}')
216
+
217
+ if metrics['exact_match'] > best_em:
218
+ best_em = metrics['exact_match']
219
+ ckpt_path = Path(args.output) / 'best_tableqa.pt'
220
+ torch.save({'model': model.state_dict(), 'metrics': metrics}, ckpt_path)
221
+ print(f' β†’ saved best checkpoint EM={best_em:.4f}')
222
+
223
+ print(f'\nBest EM: {best_em:.4f}')
224
+
225
+
226
+ # ── Entry point ───────────────────────────────────────────────────────────────
227
+
228
+ def main():
229
+ parser = argparse.ArgumentParser()
230
+ parser.add_argument('--checkpoint', required=True)
231
+ parser.add_argument('--tabqa', default='data/tabqa_kin.json')
232
+ parser.add_argument('--tables', default='data/tables')
233
+ parser.add_argument('--output', default='checkpoints/tabulm_tableqa')
234
+ parser.add_argument('--kb_vocab', default='conf/kb_vocab_state_dict_2021-02-07.pt')
235
+ parser.add_argument('--bpe_codes', default='conf/bpe_codes.txt')
236
+ parser.add_argument('--epochs', type=int, default=5)
237
+ parser.add_argument('--batch_size', type=int, default=16)
238
+ parser.add_argument('--lr', type=float, default=2e-5)
239
+ args = parser.parse_args()
240
+ train(args)
241
+
242
+
243
+ if __name__ == '__main__':
244
+ main()
code/finetune_tabqa.py ADDED
@@ -0,0 +1,641 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Fine-tune TabuLM for cell-selection on TabQA-kin (526 Kinyarwanda table QA pairs).
3
+
4
+ Strategy: Given a (table, question) pair, encode both through the TabuLM encoder
5
+ and predict which table cell (row_id, col_id) contains the answer via a linear
6
+ scoring head over per-cell pooled hidden states.
7
+ """
8
+
9
+ import argparse
10
+ import json
11
+ import os
12
+ import random
13
+ import sys
14
+ from datetime import datetime
15
+ from typing import Dict, List, Optional, Tuple
16
+
17
+ import numpy as np
18
+ import torch
19
+ import torch.nn as nn
20
+ from torch.nn.utils import clip_grad_norm_
21
+
22
+ # ── Server paths ───────────────────────────────────────────────────────────────
23
+ CODE_DIR = '/shared/scratch/0/tmp/v_ireddi_rakshitha_results/tabulm/code'
24
+ DATA_DIR = '/shared/scratch/0/tmp/v_ireddi_rakshitha_results/tabulm/data'
25
+ CHECKPOINT = os.path.join(DATA_DIR, 'tabulm_model_2026-05-13_pos@1_stem@1_afsets@False@10000.pt')
26
+ TABQA_FILE = os.path.join(DATA_DIR, 'tabqa_kin.json')
27
+ CSV_DIR = os.path.join(DATA_DIR, 'tables')
28
+ BEST_MODEL = os.path.join(DATA_DIR, 'finetune_tabqa_best.pt')
29
+ RESULTS = os.path.join(DATA_DIR, 'finetune_tabqa_results.json')
30
+
31
+ sys.path.insert(0, CODE_DIR)
32
+
33
+ import youtokentome as yttm
34
+ from morpho_data_loaders import KBVocab
35
+ from tabular_serializer import serialize_csv, table_cells_to_text, TableCell
36
+ from morpho_stub import parse_text_stub
37
+ from tabulm_model import TabuLM, tabulm_base
38
+
39
+
40
+ # ── Architecture auto-detection ────────────────────────────────────────────────
41
+
42
+ def detect_arch_from_state(state: dict) -> argparse.Namespace:
43
+ """Infer all architecture hyperparameters by inspecting checkpoint tensor shapes."""
44
+ ns = argparse.Namespace()
45
+
46
+ # Sequence transformer model dimension
47
+ ns.seq_tr_d_model = int(state['encoder.row_embedding.weight'].shape[1])
48
+ d = ns.seq_tr_d_model
49
+
50
+ # Stem / morpho dims
51
+ ns.stem_dim = int(state['encoder.s_stem_embedding.weight'].shape[1])
52
+ if 'encoder.m1_pos_embedding.weight' in state:
53
+ ns.morpho_dim = int(state['encoder.m1_pos_embedding.weight'].shape[1])
54
+ elif 'encoder.m_stem_embedding.weight' in state:
55
+ ns.morpho_dim = int(state['encoder.m_stem_embedding.weight'].shape[1])
56
+ else:
57
+ ns.morpho_dim = 128
58
+
59
+ # Sequence transformer layers
60
+ layer_ids = {
61
+ int(k.split('.layers.')[1].split('.')[0])
62
+ for k in state
63
+ if 'encoder.seq_transformer_encoder.layers.' in k and '.layers.' in k
64
+ }
65
+ ns.seq_tr_nlayers = max(layer_ids) + 1 if layer_ids else 12
66
+
67
+ # Heads from tabular bias parameter
68
+ if 'encoder.row_attn_bias' in state:
69
+ ns.seq_tr_nhead = int(state['encoder.row_attn_bias'].shape[0])
70
+ else:
71
+ ns.seq_tr_nhead = 8
72
+
73
+ # Feed-forward dim
74
+ ff_key = 'encoder.seq_transformer_encoder.layers.0.linear1.weight'
75
+ ns.seq_tr_dim_feedforward = int(state[ff_key].shape[0]) if ff_key in state else d * 4
76
+
77
+ # Morpho transformer layers
78
+ m_ids = {
79
+ int(k.split('.layers.')[1].split('.')[0])
80
+ for k in state
81
+ if 'encoder.morpho_transformer_encoder.layers.' in k and '.layers.' in k
82
+ }
83
+ ns.morpho_tr_nlayers = max(m_ids) + 1 if m_ids else 4
84
+
85
+ mff_key = 'encoder.morpho_transformer_encoder.layers.0.linear1.weight'
86
+ ns.morpho_tr_dim_feedforward = int(state[mff_key].shape[0]) if mff_key in state else ns.morpho_dim * 4
87
+
88
+ ns.morpho_tr_nhead = max(1, ns.morpho_dim // 32) # head_dim=32 default
89
+ ns.morpho_tr_dropout = 0.1
90
+ ns.seq_tr_dropout = 0.1
91
+ ns.layernorm_epsilon = 1e-6
92
+ ns.max_seq_len = 512
93
+
94
+ # Tabular embedding flags
95
+ ns.num_pos_m_embeddings = 1
96
+ ns.num_stem_m_embeddings = 1
97
+ ns.use_afsets = False
98
+ ns.afset_dict_size = 10000
99
+ ns.use_morpho_encoder = True
100
+
101
+ morpho_part = d - ns.stem_dim
102
+ tot_morpho_vecs = morpho_part // ns.morpho_dim if ns.morpho_dim else 2
103
+ tot_morpho_idx = ns.num_pos_m_embeddings + ns.num_stem_m_embeddings
104
+ ns.use_affix_bow_m_embedding = (tot_morpho_vecs > tot_morpho_idx)
105
+
106
+ ns.use_tupe_rel_pos_bias = 'encoder.tupe_rel_pos_bias.weight' in state \
107
+ or any('tupe' in k for k in state)
108
+ ns.use_pos_aware_rel_pos_bias = any('pos_aware' in k for k in state)
109
+ ns.use_pos_aware_rel = ns.use_pos_aware_rel_pos_bias
110
+ ns.predict_affixes = False
111
+
112
+ # Needed by tabulm_base factory
113
+ ns.gpus = 0
114
+ ns.world_size = 1
115
+ ns.exploratory_model_load = None
116
+
117
+ return ns
118
+
119
+
120
+ # ── Gold cell lookup via text matching ────────────────────────────────────────
121
+
122
+ def find_gold_cell(cells: List[TableCell], answer_text: str,
123
+ question_text: str = '') -> Optional[Tuple[int, int]]:
124
+ """
125
+ Find the (row_id, col_id) of the cell whose content matches answer_text.
126
+ When multiple cells share the same value, uses question-word overlap against
127
+ both first-column row labels AND header-row column names to disambiguate.
128
+ """
129
+ answer_norm = answer_text.strip().lower()
130
+
131
+ # Pass 1: exact match
132
+ matches = [(c.row_id, c.col_id) for c in cells
133
+ if c.row_id > 1 and c.col_id > 0 and c.content.strip() == answer_text.strip()]
134
+ if len(matches) == 1:
135
+ return matches[0]
136
+
137
+ # Pass 2: case-insensitive
138
+ if not matches:
139
+ matches = [(c.row_id, c.col_id) for c in cells
140
+ if c.row_id > 1 and c.col_id > 0 and c.content.strip().lower() == answer_norm]
141
+
142
+ if not matches:
143
+ return None
144
+ if len(matches) == 1:
145
+ return matches[0]
146
+
147
+ # Multiple cells match β€” score by (row overlap, col overlap) jointly
148
+ if question_text:
149
+ q_words = set(question_text.lower().split())
150
+ row_labels = {c.row_id: c.content.strip().lower()
151
+ for c in cells if c.col_id == 1 and c.row_id > 1}
152
+ col_headers = {c.col_id: c.content.strip().lower()
153
+ for c in cells if c.row_id == 1 and c.col_id > 0}
154
+ best, best_score = None, (-1, -1)
155
+ for (row_id, col_id) in matches:
156
+ r_sc = len(q_words & set(row_labels.get(row_id, '').split()))
157
+ c_sc = len(q_words & set(col_headers.get(col_id, '').split()))
158
+ if (r_sc, c_sc) > best_score:
159
+ best_score, best = (r_sc, c_sc), (row_id, col_id)
160
+ if best_score[0] > 0 or best_score[1] > 0:
161
+ return best
162
+
163
+ return min(matches, key=lambda rc: (rc[0], rc[1]))
164
+
165
+
166
+ # ── Sequence encoding (no masking) ────────────────────────────────────────────
167
+
168
+ def _add_special(key, kv, pos_tags, stems, tokens_lengths, row_ids, col_ids, cell_types):
169
+ pos_tags.append(kv.pos_tag_vocab[key])
170
+ stems.append(kv.reduced_stem_vocab[key])
171
+ tokens_lengths.append(0)
172
+ row_ids.append(0)
173
+ col_ids.append(0)
174
+ cell_types.append(0)
175
+
176
+
177
+ def encode_item(
178
+ cells,
179
+ question_text: str,
180
+ kv: KBVocab,
181
+ bpe,
182
+ max_seq_len: int = 512,
183
+ ) -> Optional[Tuple]:
184
+ """
185
+ Encode a table (List[TableCell]) + question string into flat lists for the
186
+ TabuLM encoder. Returns None if the table serialization fails.
187
+
188
+ Returns:
189
+ pos_tags, stems, affixes, tokens_lengths,
190
+ row_ids, col_ids, cell_types,
191
+ ordered_cells, cell_to_positions
192
+ where ordered_cells is a sorted list of unique (row_id, col_id) tuples, and
193
+ cell_to_positions maps each (row_id, col_id) β†’ [token_idx, ...].
194
+ """
195
+ text, word_meta = table_cells_to_text(cells)
196
+ parsed_table = parse_text_stub(text, kv, bpe)
197
+ if len(parsed_table) != len(word_meta):
198
+ return None
199
+
200
+ parsed_question = parse_text_stub(question_text, kv, bpe)
201
+
202
+ pos_tags, stems, affixes, tokens_lengths = [], [], [], []
203
+ row_ids, col_ids, cell_types = [], [], []
204
+ cell_to_positions: Dict[Tuple[int, int], List[int]] = {}
205
+
206
+ # CLS
207
+ _add_special('<CLS>', kv, pos_tags, stems, tokens_lengths, row_ids, col_ids, cell_types)
208
+
209
+ # Table tokens β€” each ParsedToken may have multiple sub-stems (BPE)
210
+ for pt, (r, c, ct) in zip(parsed_table, word_meta):
211
+ for sidx in pt.stem_idx:
212
+ seq_idx = len(pos_tags)
213
+ pos_tags.append(pt.pos_tag_idx)
214
+ stems.append(kv.mapped_stem_vocab_idx[sidx])
215
+ affixes.extend(pt.affixes_idx)
216
+ tokens_lengths.append(len(pt.affixes_idx))
217
+ row_ids.append(r)
218
+ col_ids.append(c)
219
+ cell_types.append(ct)
220
+ if r > 0 and c > 0:
221
+ cell_to_positions.setdefault((r, c), []).append(seq_idx)
222
+
223
+ # SEP between table and question
224
+ _add_special('<SEP>', kv, pos_tags, stems, tokens_lengths, row_ids, col_ids, cell_types)
225
+
226
+ # Question tokens (row_id=0, col_id=0 β†’ not scored as table cells)
227
+ for pt in parsed_question:
228
+ for sidx in pt.stem_idx:
229
+ pos_tags.append(pt.pos_tag_idx)
230
+ stems.append(kv.mapped_stem_vocab_idx[sidx])
231
+ affixes.extend(pt.affixes_idx)
232
+ tokens_lengths.append(len(pt.affixes_idx))
233
+ row_ids.append(0)
234
+ col_ids.append(0)
235
+ cell_types.append(0)
236
+
237
+ # Final SEP
238
+ _add_special('<SEP>', kv, pos_tags, stems, tokens_lengths, row_ids, col_ids, cell_types)
239
+
240
+ # Truncate to max_seq_len
241
+ if len(pos_tags) > max_seq_len:
242
+ pos_tags = pos_tags[:max_seq_len]
243
+ stems = stems[:max_seq_len]
244
+ tokens_lengths = tokens_lengths[:max_seq_len]
245
+ row_ids = row_ids[:max_seq_len]
246
+ col_ids = col_ids[:max_seq_len]
247
+ cell_types = cell_types[:max_seq_len]
248
+ affixes = affixes[:sum(tokens_lengths)]
249
+ cell_to_positions = {
250
+ k: [p for p in v if p < max_seq_len]
251
+ for k, v in cell_to_positions.items()
252
+ }
253
+ cell_to_positions = {k: v for k, v in cell_to_positions.items() if v}
254
+
255
+ ordered_cells = sorted(cell_to_positions.keys(),
256
+ key=lambda rc: min(cell_to_positions[rc]))
257
+
258
+ return (pos_tags, stems, affixes, tokens_lengths,
259
+ row_ids, col_ids, cell_types,
260
+ ordered_cells, cell_to_positions)
261
+
262
+
263
+ # ── Fine-tuning model ──────────────────────────────────────────────────────────
264
+
265
+ class TabQAModel(nn.Module):
266
+ """TabuLM encoder + linear cell-selection head."""
267
+
268
+ def __init__(self, tabulm: TabuLM):
269
+ super().__init__()
270
+ self.encoder = tabulm.encoder
271
+ d = self.encoder.seq_tr_d_model
272
+ self.cell_head = nn.Linear(d, 1)
273
+ nn.init.normal_(self.cell_head.weight, std=0.02)
274
+ nn.init.zeros_(self.cell_head.bias)
275
+
276
+ def get_hidden(self, args, pos_tags, stems, affixes,
277
+ tokens_lengths, row_ids, col_ids, cell_types, device):
278
+ pos_t = torch.tensor(pos_tags, dtype=torch.long, device=device)
279
+ stem_t = torch.tensor(stems, dtype=torch.long, device=device)
280
+ afx_t = (torch.tensor(affixes, dtype=torch.long, device=device)
281
+ if affixes else torch.zeros(0, dtype=torch.long, device=device))
282
+ row_t = torch.tensor(row_ids, dtype=torch.long, device=device)
283
+ col_t = torch.tensor(col_ids, dtype=torch.long, device=device)
284
+ ct_t = torch.tensor(cell_types, dtype=torch.long, device=device)
285
+
286
+ hidden = self.encoder.forward(
287
+ args,
288
+ rel_pos_arr=None,
289
+ tokens_lengths=tokens_lengths,
290
+ input_sequence_lengths=[len(pos_tags)],
291
+ pos_tags=pos_t, stems=stem_t, afsets=None, affixes=afx_t,
292
+ row_ids=row_t, col_ids=col_t, cell_types=ct_t,
293
+ ) # (S, 1, d)
294
+ return hidden[:, 0, :] # (S, d)
295
+
296
+ def forward(self, args, pos_tags, stems, affixes,
297
+ tokens_lengths, row_ids, col_ids, cell_types,
298
+ ordered_cells, cell_to_positions, device):
299
+ """
300
+ Returns (scores, valid_cells) where scores is a (C,) tensor of logits
301
+ and valid_cells is the subset of ordered_cells that had token positions.
302
+ """
303
+ hidden = self.get_hidden(args, pos_tags, stems, affixes,
304
+ tokens_lengths, row_ids, col_ids, cell_types, device)
305
+ S = hidden.size(0)
306
+
307
+ cell_embeds, valid_cells = [], []
308
+ for rc in ordered_cells:
309
+ positions = [p for p in cell_to_positions[rc] if p < S]
310
+ if not positions:
311
+ continue
312
+ h = hidden[positions].mean(0)
313
+ cell_embeds.append(h)
314
+ valid_cells.append(rc)
315
+
316
+ if not cell_embeds:
317
+ return None, []
318
+
319
+ cell_embeds = torch.stack(cell_embeds) # (C, d)
320
+ scores = self.cell_head(cell_embeds).squeeze(-1) # (C,)
321
+ return scores, valid_cells
322
+
323
+
324
+ # ── Evaluation ────────────────────────────────────────────────────────────────
325
+
326
+ def _predict_lookup(scores, valid_cells, cells, question_text):
327
+ """For lookup questions: restrict to (best_row, best_col) using question-word
328
+ overlap against row labels (first column) and column headers (header row)."""
329
+ q_words = set(question_text.lower().split())
330
+ row_labels = {c.row_id: c.content.strip().lower()
331
+ for c in cells if c.col_id == 1 and c.row_id > 1}
332
+ col_headers = {c.col_id: c.content.strip().lower()
333
+ for c in cells if c.row_id == 1 and c.col_id > 0}
334
+
335
+ if not q_words:
336
+ return valid_cells[scores.argmax().item()]
337
+
338
+ row_score = {r: len(q_words & set(lbl.split())) for r, lbl in row_labels.items()}
339
+ col_score = {c: len(q_words & set(hdr.split())) for c, hdr in col_headers.items()}
340
+
341
+ best_row = max(row_score, key=row_score.get) if row_score else None
342
+ best_col = max(col_score, key=col_score.get) if col_score else None
343
+ has_row = best_row is not None and row_score[best_row] > 0
344
+ has_col = best_col is not None and col_score[best_col] > 0
345
+
346
+ if not has_row and not has_col:
347
+ return valid_cells[scores.argmax().item()]
348
+
349
+ # Narrow to best row first
350
+ if has_row:
351
+ row_indices = [i for i, rc in enumerate(valid_cells) if rc[0] == best_row]
352
+ else:
353
+ row_indices = list(range(len(valid_cells)))
354
+
355
+ if not row_indices:
356
+ return valid_cells[scores.argmax().item()]
357
+
358
+ # Further narrow to best column if signal available
359
+ if has_col:
360
+ both = [i for i in row_indices if valid_cells[i][1] == best_col]
361
+ if both:
362
+ return valid_cells[max(both, key=lambda i: scores[i].item())]
363
+
364
+ return valid_cells[max(row_indices, key=lambda i: scores[i].item())]
365
+
366
+
367
+ def _predict_comparison(scores, valid_cells, cells, question_text):
368
+ """For comparison questions: restrict to col-1 (entity name) cells of the
369
+ top-2 question-relevant rows. The answer is the winning entity name."""
370
+ q_words = set(question_text.lower().split())
371
+ row_labels = {c.row_id: c.content.strip().lower()
372
+ for c in cells if c.col_id == 1 and c.row_id > 1}
373
+
374
+ if not q_words or not row_labels:
375
+ return valid_cells[scores.argmax().item()]
376
+
377
+ row_score = {r: len(q_words & set(lbl.split())) for r, lbl in row_labels.items()}
378
+ top2 = sorted([r for r, s in row_score.items() if s > 0],
379
+ key=lambda r: row_score[r], reverse=True)[:2]
380
+
381
+ if not top2:
382
+ return valid_cells[scores.argmax().item()]
383
+
384
+ # Prefer col-1 cells (entity names) in those rows; fall back to any cell
385
+ cand = [i for i, (r, c) in enumerate(valid_cells) if r in top2 and c == 1]
386
+ if not cand:
387
+ cand = [i for i, (r, c) in enumerate(valid_cells) if r in top2]
388
+ if not cand:
389
+ return valid_cells[scores.argmax().item()]
390
+
391
+ return valid_cells[max(cand, key=lambda i: scores[i].item())]
392
+
393
+
394
+ def evaluate(model, args, items, kv, bpe, csv_dir, device, split='dev'):
395
+ model.eval()
396
+ correct = 0
397
+ total = 0
398
+ skipped = 0
399
+ by_type: Dict[str, List[int]] = {}
400
+
401
+ with torch.no_grad():
402
+ for item in items:
403
+ csv_path = os.path.join(csv_dir, item['table_file'])
404
+ if not os.path.exists(csv_path):
405
+ skipped += 1
406
+ continue
407
+ cells = serialize_csv(csv_path)
408
+ if not cells:
409
+ skipped += 1
410
+ continue
411
+
412
+ question = item['question']
413
+ atype = item.get('answer_type', 'unknown')
414
+
415
+ # Find gold cell β€” use question context to resolve duplicate values
416
+ gold_rc = find_gold_cell(cells, item['answer'], question_text=question)
417
+ if gold_rc is None:
418
+ skipped += 1
419
+ continue
420
+
421
+ enc = encode_item(cells, question, kv, bpe)
422
+ if enc is None:
423
+ skipped += 1
424
+ continue
425
+
426
+ (pos_tags, stems, affixes, tokens_lengths,
427
+ row_ids, col_ids, cell_types, ordered_cells, cell_to_positions) = enc
428
+
429
+ if gold_rc not in cell_to_positions:
430
+ skipped += 1
431
+ continue
432
+
433
+ scores, valid_cells = model(args, pos_tags, stems, affixes,
434
+ tokens_lengths, row_ids, col_ids, cell_types,
435
+ ordered_cells, cell_to_positions, device)
436
+ if scores is None or gold_rc not in valid_cells:
437
+ skipped += 1
438
+ continue
439
+
440
+ if atype == 'lookup':
441
+ pred_rc = _predict_lookup(scores, valid_cells, cells, question)
442
+ elif atype == 'comparison':
443
+ pred_rc = _predict_comparison(scores, valid_cells, cells, question)
444
+ else:
445
+ pred_rc = valid_cells[scores.argmax().item()]
446
+ hit = int(pred_rc == gold_rc)
447
+ correct += hit
448
+ total += 1
449
+ by_type.setdefault(atype, []).append(hit)
450
+
451
+ em = correct / total if total > 0 else 0.0
452
+ ts = datetime.now().strftime('%H:%M:%S')
453
+ print(f' [{ts}] {split} EM={em:.4f} ({correct}/{total} correct, {skipped} skipped)')
454
+ for atype, hits in sorted(by_type.items()):
455
+ print(f' {atype}: {sum(hits)}/{len(hits)} = {sum(hits)/len(hits):.3f}')
456
+ return em
457
+
458
+
459
+ # ── Training ──────────────────────────────────────────────────────────────────
460
+
461
+ def train_finetune(args, model, train_items, dev_items, kv, bpe, csv_dir, device,
462
+ num_epochs=20, lr=2e-5, best_model_path=BEST_MODEL):
463
+ # Fine-tune top-4 seq transformer layers + cell head; freeze the rest
464
+ for name, p in model.named_parameters():
465
+ if 'cell_head' in name:
466
+ p.requires_grad = True
467
+ elif 'seq_transformer_encoder.layers.' in name:
468
+ # Extract layer index
469
+ try:
470
+ layer_idx = int(name.split('seq_transformer_encoder.layers.')[1].split('.')[0])
471
+ total_layers = model.encoder.seq_tr_nlayers \
472
+ if hasattr(model.encoder, 'seq_tr_nlayers') else 12
473
+ p.requires_grad = (layer_idx >= total_layers - 4)
474
+ except (IndexError, ValueError):
475
+ p.requires_grad = False
476
+ else:
477
+ p.requires_grad = False
478
+
479
+ trainable = sum(p.numel() for p in model.parameters() if p.requires_grad)
480
+ print(f'[FT] Trainable parameters: {trainable:,}')
481
+
482
+ optimizer = torch.optim.AdamW(
483
+ [p for p in model.parameters() if p.requires_grad],
484
+ lr=lr, weight_decay=0.01,
485
+ )
486
+ criterion = nn.CrossEntropyLoss()
487
+
488
+ best_em = 0.0
489
+ history = []
490
+
491
+ for epoch in range(1, num_epochs + 1):
492
+ model.train()
493
+ random.shuffle(train_items)
494
+ total_loss = 0.0
495
+ n_ok = 0
496
+ n_skip = 0
497
+
498
+ for item in train_items:
499
+ csv_path = os.path.join(csv_dir, item['table_file'])
500
+ if not os.path.exists(csv_path):
501
+ n_skip += 1
502
+ continue
503
+ cells = serialize_csv(csv_path)
504
+ if not cells:
505
+ n_skip += 1
506
+ continue
507
+
508
+ # Find gold cell by text matching (bypasses broken coord system)
509
+ gold_rc = find_gold_cell(cells, item['answer'])
510
+ if gold_rc is None:
511
+ n_skip += 1
512
+ continue
513
+
514
+ enc = encode_item(cells, item['question'], kv, bpe)
515
+ if enc is None:
516
+ n_skip += 1
517
+ continue
518
+
519
+ (pos_tags, stems, affixes, tokens_lengths,
520
+ row_ids, col_ids, cell_types, ordered_cells, cell_to_positions) = enc
521
+
522
+ if gold_rc not in cell_to_positions:
523
+ n_skip += 1
524
+ continue
525
+
526
+ scores, valid_cells = model(args, pos_tags, stems, affixes,
527
+ tokens_lengths, row_ids, col_ids, cell_types,
528
+ ordered_cells, cell_to_positions, device)
529
+ if scores is None or gold_rc not in valid_cells:
530
+ n_skip += 1
531
+ continue
532
+
533
+ gold_idx = torch.tensor([valid_cells.index(gold_rc)],
534
+ dtype=torch.long, device=device)
535
+ loss = criterion(scores.unsqueeze(0), gold_idx)
536
+
537
+ optimizer.zero_grad()
538
+ loss.backward()
539
+ clip_grad_norm_(model.parameters(), 1.0)
540
+ optimizer.step()
541
+
542
+ total_loss += loss.item()
543
+ n_ok += 1
544
+
545
+ ts = datetime.now().strftime('%H:%M:%S')
546
+ avg_loss = total_loss / max(n_ok, 1)
547
+ print(f'[{ts}] Epoch {epoch}/{num_epochs} loss={avg_loss:.4f} '
548
+ f'trained={n_ok} skipped={n_skip}')
549
+
550
+ em = evaluate(model, args, dev_items, kv, bpe, csv_dir, device, split='dev')
551
+ history.append({'epoch': epoch, 'train_loss': avg_loss, 'dev_em': em})
552
+
553
+ if em > best_em:
554
+ best_em = em
555
+ torch.save({
556
+ 'epoch': epoch,
557
+ 'model_state_dict': model.state_dict(),
558
+ 'dev_em': em,
559
+ }, best_model_path)
560
+ print(f' ** New best EM={best_em:.4f} saved to {best_model_path}')
561
+
562
+ return best_em, history
563
+
564
+
565
+ # ── Main ──────────────────────────────────────────────────────────────────────
566
+
567
+ def main():
568
+ parser = argparse.ArgumentParser()
569
+ parser.add_argument('--checkpoint', type=str, default=CHECKPOINT,
570
+ help='Path to pre-trained TabuLM checkpoint (.pt)')
571
+ parser.add_argument('--output-prefix', type=str, default='finetune_tabqa',
572
+ help='Prefix for best-model and results files (no extension)')
573
+ cli = parser.parse_args()
574
+
575
+ best_model_path = os.path.join(DATA_DIR, f'{cli.output_prefix}_best.pt')
576
+ results_path = os.path.join(DATA_DIR, f'{cli.output_prefix}_results.json')
577
+
578
+ random.seed(42)
579
+ np.random.seed(42)
580
+ torch.manual_seed(42)
581
+
582
+ device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
583
+ print(f'[FT] Device: {device}')
584
+
585
+ # Load vocab + BPE
586
+ print('[FT] Loading vocab and BPE...')
587
+ bpe = yttm.BPE(model=os.path.join(DATA_DIR, 'BPE-30k.mdl'))
588
+ kv = KBVocab()
589
+ kv.load_state_dict(torch.load(os.path.join(DATA_DIR, 'kb_vocab_state_dict_2021-02-07.pt'),
590
+ map_location='cpu'))
591
+
592
+ # Load checkpoint and detect architecture
593
+ print(f'[FT] Loading checkpoint from {cli.checkpoint}')
594
+ ckpt = torch.load(cli.checkpoint, map_location='cpu')
595
+ state = ckpt['model_state_dict']
596
+ if all(k.startswith('module.') for k in state):
597
+ state = {k[len('module.'):]: v for k, v in state.items()}
598
+
599
+ args = detect_arch_from_state(state)
600
+ print(f'[FT] Detected: d_model={args.seq_tr_d_model} nlayers={args.seq_tr_nlayers} '
601
+ f'nhead={args.seq_tr_nhead} ff={args.seq_tr_dim_feedforward} '
602
+ f'morpho_dim={args.morpho_dim} stem_dim={args.stem_dim} '
603
+ f'bow={args.use_affix_bow_m_embedding}')
604
+
605
+ # Build model and load weights
606
+ tabulm = tabulm_base(kv, None, None, device, args, saved_model_file=None)
607
+ missing, unexpected = tabulm.load_state_dict(state, strict=False)
608
+ print(f'[FT] Loaded encoder: {len(missing)} missing, {len(unexpected)} unexpected keys')
609
+
610
+ model = TabQAModel(tabulm).to(device)
611
+
612
+ # Load and split TabQA-kin
613
+ print(f'[FT] Loading TabQA-kin from {TABQA_FILE}')
614
+ with open(TABQA_FILE) as f:
615
+ all_items = json.load(f)
616
+ print(f'[FT] {len(all_items)} QA items total')
617
+
618
+ random.shuffle(all_items)
619
+ split_n = int(0.8 * len(all_items))
620
+ train_items = all_items[:split_n]
621
+ dev_items = all_items[split_n:]
622
+ print(f'[FT] Train={len(train_items)} Dev={len(dev_items)}')
623
+
624
+ print('[FT] Baseline (pre-training weights, no task training):')
625
+ evaluate(model, args, dev_items, kv, bpe, CSV_DIR, device, split='dev-baseline')
626
+
627
+ best_em, history = train_finetune(
628
+ args, model, train_items, dev_items, kv, bpe, CSV_DIR, device,
629
+ num_epochs=20, lr=2e-5, best_model_path=best_model_path,
630
+ )
631
+
632
+ print(f'\n[FT] Done. Best dev EM = {best_em:.4f}')
633
+
634
+ with open(results_path, 'w') as f:
635
+ json.dump({'best_em': best_em, 'epochs': history,
636
+ 'train_size': len(train_items), 'dev_size': len(dev_items)}, f, indent=2)
637
+ print(f'[FT] Results saved to {results_path}')
638
+
639
+
640
+ if __name__ == '__main__':
641
+ main()
code/kinyabert_utils.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Antoine Nzeyimana.
2
+ #
3
+ # This source code is licensed under the MIT license found in the
4
+ # LICENSE file in the root directory of this source tree.
5
+
6
+ from datetime import datetime
7
+
8
+ def read_lines(file_name):
9
+ f = open(file_name, 'r')
10
+ lines = [line.rstrip('\n') for line in f]
11
+ if len(lines[-1]) == 0:
12
+ lines = lines[:-1]
13
+ if len(lines[-1]) == 0:
14
+ lines = lines[:-1]
15
+ if len(lines[-1]) == 0:
16
+ lines = lines[:-1]
17
+ if len(lines[-1]) == 0:
18
+ lines = lines[:-1]
19
+ f.close()
20
+ return lines
21
+
22
+ def time_now():
23
+ return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
code/lamb.py ADDED
@@ -0,0 +1,148 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # MIT License
2
+ #
3
+ # Copyright (c) 2019 cybertronai
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in all
13
+ # copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ # SOFTWARE.
22
+
23
+ """Lamb optimizer."""
24
+ # From: https://github.com/cybertronai/pytorch-lamb
25
+
26
+ import collections
27
+ import math
28
+
29
+ import torch
30
+ from tensorboardX import SummaryWriter
31
+ from torch.optim import Optimizer
32
+
33
+
34
+ def log_lamb_rs(optimizer: Optimizer, event_writer: SummaryWriter, token_count: int):
35
+ """Log a histogram of trust ratio scalars in across layers."""
36
+ results = collections.defaultdict(list)
37
+ for group in optimizer.param_groups:
38
+ for p in group['params']:
39
+ state = optimizer.state[p]
40
+ for i in ('weight_norm', 'adam_norm', 'trust_ratio'):
41
+ if i in state:
42
+ results[i].append(state[i])
43
+
44
+ for k, v in results.items():
45
+ event_writer.add_histogram(f'lamb/{k}', torch.tensor(v), token_count)
46
+
47
+ class Lamb(Optimizer):
48
+ r"""Implements Lamb algorithm.
49
+
50
+ It has been proposed in `Large Batch Optimization for Deep Learning: Training BERT in 76 minutes`_.
51
+
52
+ Arguments:
53
+ params (iterable): iterable of parameters to optimize or dicts defining
54
+ parameter groups
55
+ lr (float, optional): learning rate (default: 1e-3)
56
+ betas (Tuple[float, float], optional): coefficients used for computing
57
+ running averages of gradient and its square (default: (0.9, 0.999))
58
+ eps (float, optional): term added to the denominator to improve
59
+ numerical stability (default: 1e-8)
60
+ weight_decay (float, optional): weight decay (L2 penalty) (default: 0)
61
+ adam (bool, optional): always use trust ratio = 1, which turns this into
62
+ Adam. Useful for comparison purposes.
63
+
64
+ .. _Large Batch Optimization for Deep Learning: Training BERT in 76 minutes:
65
+ https://arxiv.org/abs/1904.00962
66
+ """
67
+
68
+ def __init__(self, params, lr=1e-3, betas=(0.9, 0.999), eps=1e-6,
69
+ weight_decay=0, adam=False):
70
+ if not 0.0 <= lr:
71
+ raise ValueError("Invalid learning rate: {}".format(lr))
72
+ if not 0.0 <= eps:
73
+ raise ValueError("Invalid epsilon value: {}".format(eps))
74
+ if not 0.0 <= betas[0] < 1.0:
75
+ raise ValueError("Invalid beta parameter at index 0: {}".format(betas[0]))
76
+ if not 0.0 <= betas[1] < 1.0:
77
+ raise ValueError("Invalid beta parameter at index 1: {}".format(betas[1]))
78
+ defaults = dict(lr=lr, betas=betas, eps=eps,
79
+ weight_decay=weight_decay)
80
+ self.adam = adam
81
+ super(Lamb, self).__init__(params, defaults)
82
+
83
+ def step(self, closure=None):
84
+ """Performs a single optimization step.
85
+
86
+ Arguments:
87
+ closure (callable, optional): A closure that reevaluates the model
88
+ and returns the loss.
89
+ """
90
+ loss = None
91
+ if closure is not None:
92
+ loss = closure()
93
+
94
+ for group in self.param_groups:
95
+ for p in group['params']:
96
+ if p.grad is None:
97
+ continue
98
+ grad = p.grad.data
99
+ if grad.is_sparse:
100
+ raise RuntimeError('Lamb does not support sparse gradients, consider SparseAdam instad.')
101
+
102
+ state = self.state[p]
103
+
104
+ # State initialization
105
+ if len(state) == 0:
106
+ state['step'] = 0
107
+ # Exponential moving average of gradient values
108
+ state['exp_avg'] = torch.zeros_like(p.data)
109
+ # Exponential moving average of squared gradient values
110
+ state['exp_avg_sq'] = torch.zeros_like(p.data)
111
+
112
+ exp_avg, exp_avg_sq = state['exp_avg'], state['exp_avg_sq']
113
+ beta1, beta2 = group['betas']
114
+
115
+ state['step'] += 1
116
+
117
+ # Decay the first and second moment running average coefficient
118
+ # m_t
119
+ exp_avg.mul_(beta1).add_(grad, alpha=1 - beta1)
120
+ # v_t
121
+ exp_avg_sq.mul_(beta2).addcmul_(grad, grad, value=1 - beta2)
122
+
123
+ # Paper v3 does not use debiasing.
124
+ # bias_correction1 = 1 - beta1 ** state['step']
125
+ # bias_correction2 = 1 - beta2 ** state['step']
126
+ # Apply bias to lr to avoid broadcast.
127
+ step_size = group['lr'] # * math.sqrt(bias_correction2) / bias_correction1
128
+
129
+ weight_norm = p.data.pow(2).sum().sqrt().clamp(0, 10)
130
+
131
+ adam_step = exp_avg / exp_avg_sq.sqrt().add(group['eps'])
132
+ if group['weight_decay'] != 0:
133
+ adam_step.add_(p.data, alpha=group['weight_decay'])
134
+
135
+ adam_norm = adam_step.pow(2).sum().sqrt()
136
+ if weight_norm == 0 or adam_norm == 0:
137
+ trust_ratio = 1
138
+ else:
139
+ trust_ratio = weight_norm / adam_norm
140
+ state['weight_norm'] = weight_norm
141
+ state['adam_norm'] = adam_norm
142
+ state['trust_ratio'] = trust_ratio
143
+ if self.adam:
144
+ trust_ratio = 1
145
+
146
+ p.data.add_(adam_step, alpha=-step_size * trust_ratio)
147
+
148
+ return loss
code/morpho_common.py ADDED
@@ -0,0 +1,391 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Antoine Nzeyimana.
2
+ #
3
+ # This source code is licensed under the MIT license found in the
4
+ # LICENSE file in the root directory of this source tree.
5
+
6
+ import argparse
7
+ from cffi import FFI
8
+ import youtokentome as yttm
9
+
10
+ class SimpleKBVocab:
11
+
12
+ def __init__(self):
13
+ self.pos_tag_vocab = dict()
14
+ self.pos_tag_vocab_idx = dict()
15
+ self.pos_tag_vocab_idx_counts = dict()
16
+ self.pos_tag_vocab_idx_subsample_weights = dict()
17
+ self.pos_tag_vocab_idx_subsample_weights_max = 0.1
18
+ self.pos_tag_vocab_idx_subsample_weights_min = 0.1
19
+
20
+ self._stem_vocab = dict()
21
+ self._stem_vocab_idx = dict()
22
+ self._stem_vocab_idx_counts = dict()
23
+
24
+ self.reduced_stem_vocab = dict()
25
+ self.reduced_stem_vocab_idx = dict()
26
+ self.mapped_stem_vocab_idx = dict()
27
+ self.reduced_stem_vocab_idx_counts = dict()
28
+ self.reduced_stem_vocab_idx_subsample_weights = dict()
29
+ self.reduced_stem_vocab_idx_subsample_weights_max = 0.1
30
+ self.reduced_stem_vocab_idx_subsample_weights_min = 0.1
31
+
32
+ self.morpheme_slot_vocab = dict()
33
+ self.morpheme_slot_vocab_idx = dict()
34
+ self.morpheme_slot_vocab_idx_counts = dict()
35
+ self.morpheme_slot_vocab_idx_subsample_weights = dict()
36
+ self.morpheme_slot_vocab_idx_subsample_weights_max = 0.1
37
+ self.morpheme_slot_vocab_idx_subsample_weights_min = 0.1
38
+
39
+ self.affix_vocab = dict()
40
+ self.affix_vocab_idx = dict()
41
+ self.affix_vocab_idx_counts = dict()
42
+ self.affix_vocab_idx_subsample_weights = dict()
43
+ self.affix_vocab_idx_subsample_weights_max = 0.1
44
+ self.affix_vocab_idx_subsample_weights_min = 0.1
45
+
46
+ def load_state_dict(self, d):
47
+ self.pos_tag_vocab = d['pos_tag_vocab']
48
+ self.pos_tag_vocab_idx = d['pos_tag_vocab_idx']
49
+ self.pos_tag_vocab_idx_counts = d['pos_tag_vocab_idx_counts']
50
+ self._stem_vocab = d['_stem_vocab']
51
+ self._stem_vocab_idx = d['_stem_vocab_idx']
52
+ self._stem_vocab_idx_counts = d['_stem_vocab_idx_counts']
53
+ self.reduced_stem_vocab = d['reduced_stem_vocab']
54
+ self.mapped_stem_vocab_idx = d['mapped_stem_vocab_idx']
55
+ self.reduced_stem_vocab_idx_counts = d['reduced_stem_vocab_idx_counts']
56
+ self.morpheme_slot_vocab = d['morpheme_slot_vocab']
57
+ self.morpheme_slot_vocab_idx = d['morpheme_slot_vocab_idx']
58
+ self.morpheme_slot_vocab_idx_counts = d['morpheme_slot_vocab_idx_counts']
59
+ self.affix_vocab = d['affix_vocab']
60
+ self.affix_vocab_idx = d['affix_vocab_idx']
61
+ self.affix_vocab_idx_counts = d['affix_vocab_idx_counts']
62
+
63
+ self.morpheme_slot_vocab_idx_subsample_weights = d['morpheme_slot_vocab_idx_subsample_weights']
64
+ self.morpheme_slot_vocab_idx_subsample_weights_max = d['morpheme_slot_vocab_idx_subsample_weights_max']
65
+ self.morpheme_slot_vocab_idx_subsample_weights_min = d['morpheme_slot_vocab_idx_subsample_weights_min']
66
+
67
+ self.pos_tag_vocab_idx_subsample_weights = d['pos_tag_vocab_idx_subsample_weights']
68
+ self.pos_tag_vocab_idx_subsample_weights_max = d['pos_tag_vocab_idx_subsample_weights_max']
69
+ self.pos_tag_vocab_idx_subsample_weights_min = d['pos_tag_vocab_idx_subsample_weights_min']
70
+
71
+ self.affix_vocab_idx_subsample_weights = d['affix_vocab_idx_subsample_weights']
72
+ self.affix_vocab_idx_subsample_weights_max = d['affix_vocab_idx_subsample_weights_max']
73
+ self.affix_vocab_idx_subsample_weights_min = d['affix_vocab_idx_subsample_weights_min']
74
+
75
+ self.reduced_stem_vocab_idx_subsample_weights = d['reduced_stem_vocab_idx_subsample_weights']
76
+ self.reduced_stem_vocab_idx_subsample_weights_max = d['reduced_stem_vocab_idx_subsample_weights_max']
77
+ self.reduced_stem_vocab_idx_subsample_weights_min = d['reduced_stem_vocab_idx_subsample_weights_min']
78
+
79
+ for k in self.reduced_stem_vocab:
80
+ self.reduced_stem_vocab_idx[self.reduced_stem_vocab[k]] = k
81
+ self.reduced_stem_vocab_idx_counts = dict()
82
+ for i in self._stem_vocab_idx_counts:
83
+ self.reduced_stem_vocab_idx_counts[self.mapped_stem_vocab_idx[i]] = self._stem_vocab_idx_counts[i]
84
+
85
+ class RichParsedToken:
86
+ def __init__(self, surface_form, raw_surface_form, is_apostrophed, parsed_token=None, decode_prob=None, tf_idf=0.0, pos_tag_id=None, stem_ids=None, line_num=0):
87
+ self.surface_form = surface_form
88
+ self.raw_surface_form = raw_surface_form
89
+ self.is_apostrophed = is_apostrophed
90
+ self.tf_idf = tf_idf
91
+ self.morpho_tokens = []
92
+ if parsed_token is not None:
93
+ parts = parsed_token.split('/')
94
+ self.decode_prob = float(parts[0])
95
+ self.tf_idf = float(parts[1])
96
+ morphs = parts[2].split(',')
97
+ pos_stem = morphs[0].split(':')
98
+ stem_parts = pos_stem[1].split('*')
99
+ if(len(stem_parts[0]) < 1):
100
+ # print('\nParsing wrong token: /{}/ at line # {}'.format(parsed_token, line_num))
101
+ self.pos_tag_idx = int(pos_stem[0])
102
+ self.stem_idx = [6]
103
+ self.morpho_slots_idx = []
104
+ self.affixes_idx = []
105
+ else:
106
+ self.pos_tag_idx = int(pos_stem[0])
107
+ self.stem_idx = [int(v) for v in stem_parts]
108
+ self.morpho_slots_idx = [int(morphs[i].split(':')[0]) for i in range(1, len(morphs))]
109
+ self.affixes_idx = [int(morphs[i].split(':')[1]) for i in range(1, len(morphs))]
110
+ else:
111
+ self.decode_prob = decode_prob
112
+ self.pos_tag_idx = pos_tag_id
113
+ self.stem_idx = stem_ids
114
+ self.morpho_slots_idx = []
115
+ self.affixes_idx = []
116
+
117
+ def append_morpheme(self, morpho_slot_id, affix_id):
118
+ self.morpho_slots_idx.append(morpho_slot_id)
119
+ self.affixes_idx.append(affix_id)
120
+
121
+ def to_parsed_format(self):
122
+ st = ['{}:{}'.format(self.pos_tag_idx,'*'.join([str(i) for i in self.stem_idx]))]
123
+ for i in range(len(self.morpho_slots_idx)):
124
+ st.append('{}:{}'.format(self.morpho_slots_idx[i], self.affixes_idx[i]))
125
+ return '{:.3g}/{:.3g}/{}'.format(self.decode_prob, self.tf_idf, ','.join(st))
126
+
127
+ def affix_set_key(self):
128
+ key = '-'.join([str(af) for af in self.affixes_idx]) if (len(self.affixes_idx) > 0) else 'N/A'
129
+ return key
130
+
131
+ def build_kinlp_morpho_lib():
132
+ ffibuilder = FFI()
133
+
134
+ ffibuilder.cdef("""
135
+
136
+ typedef struct _snt_morpheme {
137
+ int slot_id;
138
+ int morph_id;
139
+ int morph_token_len;
140
+ char * morph_token;
141
+ } snt_morpheme_t;
142
+
143
+ typedef struct _snt_word {
144
+ char * pos_tag;
145
+ char * pos_group;
146
+ char * surface_form;
147
+ char * raw_surface_form;
148
+ char * stem;
149
+ snt_morpheme_t * morphemes;
150
+ double decode_prob;
151
+ double tf_idf;
152
+ int pos_tag_id;
153
+ int word_type;
154
+ int stem_start_index;
155
+ int stem_end_index;
156
+ int stem_start_slot_id;
157
+ int stem_end_slot_id;
158
+ int morphemes_len;
159
+ int apostrophed;
160
+ } snt_word_t;
161
+
162
+ typedef struct _snt_sentence {
163
+ snt_word_t * words;
164
+ int words_len;
165
+ } snt_sentence_t;
166
+
167
+ void release_sentence(snt_sentence_t * sentences, int num_sent[1]);
168
+ snt_sentence_t * parse_sentences_batch(const char * text, int num_sent[1]);
169
+
170
+ void start_kinlp_lib(const char * config_file);
171
+ void stop_kinlp_lib(void);
172
+
173
+ int is_word_morphological(const char * word);
174
+
175
+ """)
176
+
177
+ ffibuilder.set_source("kinlpmorpholib",
178
+ """
179
+ #include "/home/user/projects/user/kinlp/kinlp/lib.h"
180
+ #include "/home/user/projects/user/kinlp/kinlp/snt.h"
181
+ """,
182
+ extra_compile_args=['-fopenmp', '-D use_openmp', '-O3', '-march=native', '-ffast-math'],
183
+ extra_link_args=['-fopenmp'],
184
+ libraries=['kinlp']) # library name, for the linker
185
+
186
+ ffibuilder.compile(verbose=True)
187
+
188
+ def parse_raw_text_lines(doc_lines, kb_vocab, bpe):
189
+ from kinlpmorpholib import ffi, lib
190
+ parsed_tokens = []
191
+ grouped_parsed_tokens = []
192
+ num_sent = ffi.new("int[1]")
193
+ sentences = lib.parse_sentences_batch(' '.join(doc_lines).encode('utf-8'), num_sent)
194
+ for i in range(num_sent[0]):
195
+ sent = sentences[i]
196
+ sentence_parsed_tokens = []
197
+ for j in range(sent.words_len):
198
+ w = sent.words[j]
199
+ POS_TAG = ffi.string(w.pos_tag).decode("utf-8")
200
+ WORD_TYPE = ffi.string(w.pos_group).decode("utf-8")
201
+ STEM = ffi.string(w.stem).decode("utf-8")
202
+ SURFACE_FORM = ffi.string(w.surface_form).decode("utf-8")
203
+ RAW_SURFACE_FORM = ffi.string(w.raw_surface_form).decode("utf-8")
204
+ DECODE_PROB = w.decode_prob
205
+ IS_APOSTROPHED = not (w.apostrophed == 0)
206
+ if IS_APOSTROPHED and (RAW_SURFACE_FORM[-1] == 'a'):
207
+ RAW_SURFACE_FORM = RAW_SURFACE_FORM[:-1]+"'"
208
+ RAW_SURFACE_FORM = RAW_SURFACE_FORM.replace('β€œ','"').replace('β€˜β€˜', '"').replace('’’', '"').replace('’', '\'').replace('β€˜','\'')
209
+ TF_IDF = w.tf_idf
210
+ pti = kb_vocab.pos_tag_vocab['<UNK>']
211
+ if POS_TAG in kb_vocab.pos_tag_vocab.keys():
212
+ pti = kb_vocab.pos_tag_vocab[POS_TAG]
213
+
214
+ sids = []
215
+ morpho_tokens = []
216
+ stem_tokens = []
217
+ if ((STEM == SURFACE_FORM) and (w.morphemes_len <= 0)):
218
+ list_sub_words = bpe.encode(SURFACE_FORM, output_type=yttm.OutputType.SUBWORD)
219
+ for sub_word in list_sub_words:
220
+ stem_key = WORD_TYPE + ":" + sub_word
221
+ morpho_tokens.append(stem_key)
222
+ stem_tokens.append(stem_key)
223
+ si = kb_vocab._stem_vocab['<UNK>']
224
+ if stem_key in kb_vocab._stem_vocab.keys():
225
+ si = kb_vocab._stem_vocab[stem_key]
226
+ sids.append(si)
227
+ else:
228
+ stem_key = WORD_TYPE + ":" + STEM
229
+ morpho_tokens.append(stem_key)
230
+ stem_tokens.append(stem_key)
231
+ si = kb_vocab._stem_vocab['<UNK>']
232
+ if stem_key in kb_vocab._stem_vocab.keys():
233
+ si = kb_vocab._stem_vocab[stem_key]
234
+ sids.append(si)
235
+ ptoken = RichParsedToken(SURFACE_FORM, RAW_SURFACE_FORM, IS_APOSTROPHED, parsed_token=None, decode_prob=DECODE_PROB, tf_idf=TF_IDF, pos_tag_id=pti, stem_ids=sids)
236
+ if (w.morphemes_len > 0):
237
+ for k in range(w.morphemes_len):
238
+ if ((k != w.stem_start_index) and (k != w.stem_end_index)):
239
+ MORPHEME_SLOT = WORD_TYPE + ":" + str(w.morphemes[k].slot_id)
240
+ MORPHEME = MORPHEME_SLOT + ":" + ffi.string(w.morphemes[k].morph_token).decode("utf-8")
241
+
242
+ if (k < w.stem_start_index):
243
+ morpho_tokens = [MORPHEME] + morpho_tokens
244
+ elif (k < w.stem_end_index) and (len(stem_tokens) == 1) and (w.stem_start_index != w.stem_end_index):
245
+ # For reduplication for verbs, a bit rare
246
+ morpho_tokens = morpho_tokens + [MORPHEME] + stem_tokens
247
+ else:
248
+ morpho_tokens = morpho_tokens + [MORPHEME]
249
+
250
+ msi = kb_vocab.morpheme_slot_vocab['<UNK>']
251
+ if MORPHEME_SLOT in kb_vocab.morpheme_slot_vocab.keys():
252
+ msi = kb_vocab.morpheme_slot_vocab[MORPHEME_SLOT]
253
+
254
+ mi = kb_vocab.affix_vocab['<UNK>']
255
+ if MORPHEME in kb_vocab.affix_vocab.keys():
256
+ mi = kb_vocab.affix_vocab[MORPHEME]
257
+
258
+ ptoken.append_morpheme(msi, mi)
259
+ ptoken.morpho_tokens.extend(morpho_tokens)
260
+ parsed_tokens.append(ptoken)
261
+ sentence_parsed_tokens.append(ptoken)
262
+ grouped_parsed_tokens.append(sentence_parsed_tokens)
263
+ lib.release_sentence(sentences, num_sent)
264
+
265
+ return parsed_tokens, grouped_parsed_tokens
266
+
267
+ def str2bool(v):
268
+ if isinstance(v, bool):
269
+ return v
270
+ if v.lower() in ('yes', 'true', 't', 'y', '1'):
271
+ return True
272
+ elif v.lower() in ('no', 'false', 'f', 'n', '0'):
273
+ return False
274
+ else:
275
+ raise argparse.ArgumentTypeError('Boolean value expected.')
276
+
277
+
278
+ def setup_common_args(list_args=None, silent=False):
279
+ parser = argparse.ArgumentParser()
280
+ parser.add_argument('-g', '--gpus', default=1, type=int, help='number of gpus')
281
+ parser.add_argument('-p', '--pos', default=1, type=int, help='number of POS embeddings for morphology')
282
+ parser.add_argument('-s', '--stem', default=1, type=int, help='number of Stem embeddings for morphology')
283
+ parser.add_argument('-ii', '--inference-iters', default=1, type=int, help='number of MLM Inference iterations')
284
+ parser.add_argument('-ir', '--inference-runs', default=20, type=int,
285
+ help='number of MLM Inference runes per iteration')
286
+ parser.add_argument("--use-affix-bow", type=str2bool, default=False,
287
+ help="Use affix embeddings sum (BOW) for morphology")
288
+ parser.add_argument("--use-pos-aware-rel", type=str2bool, default=True,
289
+ help="Use POS-aware relative position embedding")
290
+ parser.add_argument("--use-tupe-rel", type=str2bool, default=False, help="Use TUPE relative position embedding")
291
+ parser.add_argument("--resume-from-best-saved", type=str2bool, default=False, help="Resume training from best saved model")
292
+
293
+ parser.add_argument("--use-afsets", type=str2bool, default=False)
294
+ parser.add_argument("--predict-affixes", type=str2bool, default=True)
295
+
296
+ # KinyaBERT_base architecture hyper-parameters
297
+ parser.add_argument("--seq-tr-dropout", type=float, default=0.1)
298
+ parser.add_argument("--layernorm-epsilon", type=float, default=1e-6)
299
+ parser.add_argument("--morpho-tr-dropout", type=float, default=0.1)
300
+ parser.add_argument("--morpho-tr-nhead", type=int, default=4)
301
+ parser.add_argument("--morpho-tr-nlayers", type=int, default=4)
302
+ parser.add_argument("--morpho-dim", type=int, default=128)
303
+ parser.add_argument("--morpho-tr-dim-feedforward", type=int, default=512)
304
+ parser.add_argument("--stem-dim", type=int, default=256)
305
+ parser.add_argument("--max-seq-len", type=int, default=512)
306
+ parser.add_argument("--seq-tr-nhead", type=int, default=12)
307
+ parser.add_argument("--seq-tr-nlayers", type=int, default=12)
308
+ parser.add_argument("--seq-tr-dim-feedforward", type=int, default=3072)
309
+
310
+ parser.add_argument("--batch-size", type=int, default=20)
311
+ parser.add_argument("--num-epochs", type=int, default=20)
312
+ parser.add_argument("--accumulation-steps", type=int, default=128)
313
+ parser.add_argument("--number-of-load-batches", type=int, default=384)
314
+ parser.add_argument("--max-input-lines", type=int, default=999999)
315
+ parser.add_argument("--warmup-ratio", type=float, default=0.06)
316
+
317
+ parser.add_argument("--num-iters", type=int, default=200000)
318
+ parser.add_argument("--warmup-iter", type=int, default=2000)
319
+
320
+ parser.add_argument("--peak-lr", type=float, default=4e-4)
321
+ parser.add_argument("--wd", type=float, default=0.01)
322
+
323
+ parser.add_argument("--cls-labels", type=str, default="0,1")
324
+
325
+ parser.add_argument("--cls-train-input0", type=str, default=None)
326
+ parser.add_argument("--cls-train-input1", type=str, default=None)
327
+ parser.add_argument("--cls-train-label", type=str, default=None)
328
+
329
+ parser.add_argument("--cls-dev-input0", type=str, default=None)
330
+ parser.add_argument("--cls-dev-input1", type=str, default=None)
331
+ parser.add_argument("--cls-dev-label", type=str, default=None)
332
+
333
+ parser.add_argument("--cls-test-input0", type=str, default=None)
334
+ parser.add_argument("--cls-test-input1", type=str, default=None)
335
+ parser.add_argument("--cls-test-label", type=str, default=None)
336
+
337
+ parser.add_argument("--pretrained-model-file", type=str, default=None)
338
+ parser.add_argument("--devbest-cls-model-save-file-path", type=str, default=None)
339
+ parser.add_argument("--final-cls-model-save-file-path", type=str, default=None)
340
+
341
+ parser.add_argument("--devbest-cls-output-file", type=str, default=None)
342
+ parser.add_argument("--final-cls-output-file", type=str, default=None)
343
+
344
+ parser.add_argument("--home-path", type=str, default="./")
345
+
346
+ parser.add_argument("--regression-target", type=str2bool, default=False)
347
+ parser.add_argument("--regression-scale-factor", type=float, default=5.0)
348
+
349
+ parser.add_argument("--pretrained-roberta-model-dir", type=str, default="data/")
350
+ parser.add_argument("--pretrained-roberta-checkpoint-file", type=str, default="checkpoint_best.pt")
351
+ parser.add_argument("--xlmr", type=str2bool, default=False)
352
+
353
+ parser.add_argument("--pooler-dropout", type=float, default=0.1)
354
+
355
+ parser.add_argument("--embed-dim", type=int, default=768)
356
+
357
+ parser.add_argument("--inference-model-file", type=str, default=None)
358
+
359
+ parser.add_argument("--model-keyword", type=str, default=None)
360
+ parser.add_argument("--task-keyword", type=str, default=None)
361
+ parser.add_argument("--input-format", type=str, default=None)
362
+
363
+ parser.add_argument("--afset-dict-size", type=int, default=10000)
364
+
365
+ parser.add_argument("--debug", type=str2bool, default=False)
366
+
367
+ parser.add_argument("--use-morpho-encoder", type=str2bool, default=True)
368
+
369
+ parser.add_argument("--exploratory-model-load", type=str, default=None)
370
+
371
+ parser.add_argument("-f", type=str, default=None)
372
+
373
+
374
+ if list_args is not None:
375
+ args = parser.parse_args(list_args)
376
+ else:
377
+ args = parser.parse_args()
378
+
379
+ args.world_size = args.gpus
380
+ args.num_pos_m_embeddings = args.pos
381
+ args.num_stem_m_embeddings = args.stem
382
+ args.use_affix_bow_m_embedding = args.use_affix_bow
383
+ args.use_pos_aware_rel_pos_bias = args.use_pos_aware_rel
384
+ args.use_tupe_rel_pos_bias = args.use_tupe_rel
385
+ args.num_inference_iters = args.inference_iters
386
+ args.num_inference_runs = args.inference_runs
387
+
388
+ if not silent:
389
+ print('Call arguments:\n', args)
390
+
391
+ return args
code/morpho_data_loaders.py ADDED
@@ -0,0 +1,958 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Antoine Nzeyimana.
2
+ #
3
+ # This source code is licensed under the MIT license found in the
4
+ # LICENSE file in the root directory of this source tree.
5
+
6
+
7
+ from __future__ import print_function, division
8
+
9
+ import math
10
+ import random
11
+ import sys
12
+ # Ignore warnings
13
+ import warnings
14
+ from typing import List
15
+
16
+ import numpy as np
17
+ import progressbar
18
+ import torch
19
+ import youtokentome as yttm
20
+ from torch.utils.data import Dataset
21
+
22
+ from kinyabert_utils import time_now
23
+
24
+ warnings.filterwarnings("ignore")
25
+
26
+ def read_corpus(fn):
27
+ f = open(fn, 'r+')
28
+ corpus_lines = [line.rstrip('\n') for line in f]
29
+ f.close()
30
+ corpus_lines.append("\n")
31
+ return corpus_lines
32
+
33
+ def read_vocab_idx(fn, voc, voc_idx):
34
+ f = open(fn, 'r')
35
+ v_lines = f.readlines()
36
+ f.close()
37
+ for v in v_lines:
38
+ if(len(v)>1):
39
+ vt = v.split('\t')
40
+ if(len(vt) == 2):
41
+ voc_idx[int(vt[1])] = vt[0]
42
+ voc[vt[0]] = int(vt[1])
43
+ return voc, voc_idx
44
+
45
+ def read_vocab_counts(fn, voc, voc_idx_counts):
46
+ f = open(fn, 'r')
47
+ v_lines = f.readlines()
48
+ f.close()
49
+ for v in v_lines:
50
+ if(len(v)>1):
51
+ vt = v.split('\t')
52
+ if(len(vt) == 2):
53
+ id = voc[vt[0]]
54
+ voc_idx_counts[id] = int(vt[1])
55
+ return voc, voc_idx_counts
56
+
57
+ def reduce_stem_vocab(_stem_vocab, _stem_vocab_idx, _stem_vocab_idx_counts,
58
+ noun_min_count, verb_min_count, np_min_count,
59
+ other_morpho_min_count, other_cls_min_count, other_token_min_count):
60
+ reduced_stem_vocab = dict()
61
+ mapped_stem_vocab_idx = dict()
62
+ reduced_stem_vocab_idx_counts = dict()
63
+ unk_idx = _stem_vocab['<UNK>']
64
+ for i in range(1,len(_stem_vocab_idx)+1):
65
+ if (i <= unk_idx):
66
+ mapped_stem_vocab_idx[i] = i
67
+ reduced_stem_vocab[_stem_vocab_idx[i]] = i
68
+ reduced_stem_vocab_idx_counts[i] = 1000000
69
+ else:
70
+ key = _stem_vocab_idx[i]
71
+ if (key.startswith('N:')):
72
+ min_count = noun_min_count
73
+ elif (key.startswith('V:')):
74
+ min_count = verb_min_count
75
+ elif (key.startswith('QA:') or key.startswith('PO:') or key.startswith('DE:') or key.startswith('NU:') or key.startswith('OT:')):
76
+ min_count = other_morpho_min_count
77
+ elif (key.startswith('NP:')):
78
+ min_count = np_min_count
79
+ elif (key.startswith('T:')):
80
+ min_count = other_token_min_count
81
+ else:
82
+ min_count = other_cls_min_count
83
+ if _stem_vocab_idx_counts[i] >= min_count:
84
+ idx = len(reduced_stem_vocab)+1
85
+ mapped_stem_vocab_idx[i] = idx
86
+ reduced_stem_vocab[_stem_vocab_idx[i]] = idx
87
+ reduced_stem_vocab_idx_counts[idx] = _stem_vocab_idx_counts[i]
88
+ else:
89
+ mapped_stem_vocab_idx[i] = reduced_stem_vocab['<UNK>']
90
+
91
+ return reduced_stem_vocab, mapped_stem_vocab_idx, reduced_stem_vocab_idx_counts
92
+
93
+ def sigmoid_score(x, min_val, max_val):
94
+ s = pow((1.0 + (math.exp(((-8.0) * (x - min_val)) / (max_val - min_val)))), -8.0)
95
+ return s
96
+
97
+ class KBVocab:
98
+
99
+ def __init__(self, noun_min_count = 200,
100
+ verb_min_count = 100,
101
+ np_min_count = 200,
102
+ other_morpho_min_count = 20,
103
+ other_cls_min_count = 20,
104
+ other_token_min_count = 200,
105
+ pos_tag_vocab_idx_tsv="data/pos_tag_vocab_idx_2021-02-07.tsv",
106
+ pos_tag_vocab_tsv="data/pos_tag_vocab_2021-02-07.tsv",
107
+ stem_vocab_idx_tsv="data/stem_vocab_idx_2021-02-07.tsv",
108
+ stem_vocab_tsv="data/stem_vocab_2021-02-07.tsv",
109
+ morpheme_slot_vocab_idx_tsv="data/morpheme_slot_vocab_idx_2021-02-07.tsv",
110
+ morpheme_slot_vocab_tsv="data/morpheme_slot_vocab_2021-02-07.tsv",
111
+ affix_vocab_idx_tsv="data/affix_vocab_idx_2021-02-07.tsv",
112
+ affix_vocab_tsv="data/affix_vocab_2021-02-07.tsv",
113
+ read_vocab_files = False):
114
+ self.pos_tag_vocab = dict()
115
+ self.pos_tag_vocab_idx = dict()
116
+ self.pos_tag_vocab_idx_counts = dict()
117
+ self.pos_tag_vocab_idx_subsample_weights = dict()
118
+ self.pos_tag_vocab_idx_subsample_weights_max = 0.1
119
+ self.pos_tag_vocab_idx_subsample_weights_min = 0.1
120
+ if(read_vocab_files):
121
+ read_vocab_idx(pos_tag_vocab_idx_tsv, self.pos_tag_vocab, self.pos_tag_vocab_idx)
122
+ read_vocab_counts(pos_tag_vocab_tsv, self.pos_tag_vocab, self.pos_tag_vocab_idx_counts)
123
+
124
+ self._stem_vocab = dict()
125
+ self._stem_vocab_idx = dict()
126
+ self._stem_vocab_idx_counts = dict()
127
+ if(read_vocab_files):
128
+ read_vocab_idx(stem_vocab_idx_tsv, self._stem_vocab, self._stem_vocab_idx)
129
+ read_vocab_counts(stem_vocab_tsv, self._stem_vocab, self._stem_vocab_idx_counts)
130
+
131
+ self.reduced_stem_vocab = dict()
132
+ self.reduced_stem_vocab_idx = dict()
133
+ self.mapped_stem_vocab_idx = dict()
134
+ self.reduced_stem_vocab_idx_counts = dict()
135
+ self.reduced_stem_vocab_idx_subsample_weights = dict()
136
+ self.reduced_stem_vocab_idx_subsample_weights_max = 0.1
137
+ self.reduced_stem_vocab_idx_subsample_weights_min = 0.1
138
+ if(read_vocab_files):
139
+ (self.reduced_stem_vocab,
140
+ self.mapped_stem_vocab_idx,
141
+ self.reduced_stem_vocab_idx_counts) = reduce_stem_vocab(self._stem_vocab,
142
+ self._stem_vocab_idx,
143
+ self._stem_vocab_idx_counts,
144
+ noun_min_count,
145
+ verb_min_count,
146
+ np_min_count,
147
+ other_morpho_min_count,
148
+ other_cls_min_count,
149
+ other_token_min_count)
150
+ for k in self.reduced_stem_vocab:
151
+ self.reduced_stem_vocab_idx[self.reduced_stem_vocab[k]] = k
152
+
153
+ self.morpheme_slot_vocab = dict()
154
+ self.morpheme_slot_vocab_idx = dict()
155
+ self.morpheme_slot_vocab_idx_counts = dict()
156
+ self.morpheme_slot_vocab_idx_subsample_weights = dict()
157
+ self.morpheme_slot_vocab_idx_subsample_weights_max = 0.1
158
+ self.morpheme_slot_vocab_idx_subsample_weights_min = 0.1
159
+ if(read_vocab_files):
160
+ read_vocab_idx(morpheme_slot_vocab_idx_tsv, self.morpheme_slot_vocab, self.morpheme_slot_vocab_idx)
161
+ read_vocab_counts(morpheme_slot_vocab_tsv, self.morpheme_slot_vocab, self.morpheme_slot_vocab_idx_counts)
162
+
163
+ self.affix_vocab = dict()
164
+ self.affix_vocab_idx = dict()
165
+ self.affix_vocab_idx_counts = dict()
166
+ self.affix_vocab_idx_subsample_weights = dict()
167
+ self.affix_vocab_idx_subsample_weights_max = 0.1
168
+ self.affix_vocab_idx_subsample_weights_min = 0.1
169
+ if(read_vocab_files):
170
+ read_vocab_idx(affix_vocab_idx_tsv, self.affix_vocab, self.affix_vocab_idx)
171
+ read_vocab_counts(affix_vocab_tsv, self.affix_vocab, self.affix_vocab_idx_counts)
172
+
173
+ def compute_subsampling_weights(self):
174
+ eps = 1e-5
175
+ sum_pos_tags = sum([self.pos_tag_vocab_idx_counts[k] for k in self.pos_tag_vocab_idx_counts])
176
+ for k in self.pos_tag_vocab_idx_counts:
177
+ z = float(self.pos_tag_vocab_idx_counts[k])/float(sum_pos_tags)
178
+ z = z + eps
179
+ self.pos_tag_vocab_idx_subsample_weights[k] = (math.sqrt(z/0.001)+1.0)*(0.001/z)
180
+ self.pos_tag_vocab_idx_subsample_weights_max = max([self.pos_tag_vocab_idx_subsample_weights[k] for k in self.pos_tag_vocab_idx_subsample_weights])
181
+ self.pos_tag_vocab_idx_subsample_weights_min = min([self.pos_tag_vocab_idx_subsample_weights[k] for k in self.pos_tag_vocab_idx_subsample_weights])
182
+ for k in self.pos_tag_vocab_idx_subsample_weights:
183
+ if (k <= self.pos_tag_vocab['<UNK>']):
184
+ self.pos_tag_vocab_idx_subsample_weights[k] = self.pos_tag_vocab_idx_subsample_weights_min
185
+
186
+ sum_morpheme_slots = sum([self.morpheme_slot_vocab_idx_counts[k] for k in self.morpheme_slot_vocab_idx_counts])
187
+ for k in self.morpheme_slot_vocab_idx_counts:
188
+ z = float(self.morpheme_slot_vocab_idx_counts[k])/float(sum_morpheme_slots)
189
+ z = z + eps
190
+ self.morpheme_slot_vocab_idx_subsample_weights[k] = (math.sqrt(z/0.001)+1.0)*(0.001/z)
191
+ self.morpheme_slot_vocab_idx_subsample_weights_max = max([self.morpheme_slot_vocab_idx_subsample_weights[k] for k in self.morpheme_slot_vocab_idx_subsample_weights])
192
+ self.morpheme_slot_vocab_idx_subsample_weights_min = min([self.morpheme_slot_vocab_idx_subsample_weights[k] for k in self.morpheme_slot_vocab_idx_subsample_weights])
193
+ for k in self.morpheme_slot_vocab_idx_subsample_weights:
194
+ if (k <= self.morpheme_slot_vocab['<UNK>']):
195
+ self.morpheme_slot_vocab_idx_subsample_weights[k] = self.morpheme_slot_vocab_idx_subsample_weights_min
196
+
197
+ sum_affixes = sum([self.affix_vocab_idx_counts[k] for k in self.affix_vocab_idx_counts])
198
+ for k in self.affix_vocab_idx_counts:
199
+ z = float(self.affix_vocab_idx_counts[k])/float(sum_affixes)
200
+ z = z + eps
201
+ self.affix_vocab_idx_subsample_weights[k] = (math.sqrt(z/0.001)+1.0)*(0.001/z)
202
+ self.affix_vocab_idx_subsample_weights_max = max([self.affix_vocab_idx_subsample_weights[k] for k in self.affix_vocab_idx_subsample_weights])
203
+ self.affix_vocab_idx_subsample_weights_min = min([self.affix_vocab_idx_subsample_weights[k] for k in self.affix_vocab_idx_subsample_weights])
204
+ for k in self.affix_vocab_idx_subsample_weights:
205
+ if (k <= self.affix_vocab['<UNK>']):
206
+ self.affix_vocab_idx_subsample_weights[k] = self.affix_vocab_idx_subsample_weights_min
207
+
208
+ sum_stems = sum([self.reduced_stem_vocab_idx_counts[k] for k in self.reduced_stem_vocab_idx_counts])
209
+ for k in self.reduced_stem_vocab_idx_counts:
210
+ z = float(self.reduced_stem_vocab_idx_counts[k])/float(sum_stems)
211
+ z = z + eps
212
+ self.reduced_stem_vocab_idx_subsample_weights[k] = (math.sqrt(z/0.001)+1.0)*(0.001/z)
213
+ self.reduced_stem_vocab_idx_subsample_weights_max = max([self.reduced_stem_vocab_idx_subsample_weights[k] for k in self.reduced_stem_vocab_idx_subsample_weights])
214
+ self.reduced_stem_vocab_idx_subsample_weights_min = min([self.reduced_stem_vocab_idx_subsample_weights[k] for k in self.reduced_stem_vocab_idx_subsample_weights])
215
+ for k in self.reduced_stem_vocab_idx_subsample_weights:
216
+ if (k <= self.reduced_stem_vocab['<UNK>']):
217
+ self.reduced_stem_vocab_idx_subsample_weights[k] = self.reduced_stem_vocab_idx_subsample_weights_min
218
+
219
+ def state_dict(self):
220
+ return {'pos_tag_vocab':self.pos_tag_vocab,
221
+ 'pos_tag_vocab_idx':self.pos_tag_vocab_idx,
222
+ 'pos_tag_vocab_idx_counts':self.pos_tag_vocab_idx_counts,
223
+ '_stem_vocab':self._stem_vocab,
224
+ '_stem_vocab_idx':self._stem_vocab_idx,
225
+ '_stem_vocab_idx_counts':self._stem_vocab_idx_counts,
226
+ 'reduced_stem_vocab':self.reduced_stem_vocab,
227
+ 'mapped_stem_vocab_idx':self.mapped_stem_vocab_idx,
228
+ 'reduced_stem_vocab_idx_counts':self.reduced_stem_vocab_idx_counts,
229
+ 'morpheme_slot_vocab':self.morpheme_slot_vocab,
230
+ 'morpheme_slot_vocab_idx':self.morpheme_slot_vocab_idx,
231
+ 'morpheme_slot_vocab_idx_counts':self.morpheme_slot_vocab_idx_counts,
232
+ 'affix_vocab':self.affix_vocab,
233
+ 'affix_vocab_idx':self.affix_vocab_idx,
234
+ 'affix_vocab_idx_counts':self.affix_vocab_idx_counts,
235
+
236
+ 'morpheme_slot_vocab_idx_subsample_weights': self.morpheme_slot_vocab_idx_subsample_weights,
237
+ 'morpheme_slot_vocab_idx_subsample_weights_max': self.morpheme_slot_vocab_idx_subsample_weights_max,
238
+ 'morpheme_slot_vocab_idx_subsample_weights_min': self.morpheme_slot_vocab_idx_subsample_weights_min,
239
+
240
+ 'affix_vocab_idx_subsample_weights':self.affix_vocab_idx_subsample_weights,
241
+ 'affix_vocab_idx_subsample_weights_max':self.affix_vocab_idx_subsample_weights_max,
242
+ 'affix_vocab_idx_subsample_weights_min':self.affix_vocab_idx_subsample_weights_min,
243
+
244
+ 'pos_tag_vocab_idx_subsample_weights': self.pos_tag_vocab_idx_subsample_weights,
245
+ 'pos_tag_vocab_idx_subsample_weights_max': self.pos_tag_vocab_idx_subsample_weights_max,
246
+ 'pos_tag_vocab_idx_subsample_weights_min': self.pos_tag_vocab_idx_subsample_weights_min,
247
+
248
+ 'reduced_stem_vocab_idx_subsample_weights':self.reduced_stem_vocab_idx_subsample_weights,
249
+ 'reduced_stem_vocab_idx_subsample_weights_max':self.reduced_stem_vocab_idx_subsample_weights_max,
250
+ 'reduced_stem_vocab_idx_subsample_weights_min':self.reduced_stem_vocab_idx_subsample_weights_min}
251
+
252
+ def load_state_dict(self, d):
253
+ self.pos_tag_vocab = d['pos_tag_vocab']
254
+ self.pos_tag_vocab_idx = d['pos_tag_vocab_idx']
255
+ self.pos_tag_vocab_idx_counts = d['pos_tag_vocab_idx_counts']
256
+ self._stem_vocab = d['_stem_vocab']
257
+ self._stem_vocab_idx = d['_stem_vocab_idx']
258
+ self._stem_vocab_idx_counts = d['_stem_vocab_idx_counts']
259
+ self.reduced_stem_vocab = d['reduced_stem_vocab']
260
+ self.mapped_stem_vocab_idx = d['mapped_stem_vocab_idx']
261
+ self.reduced_stem_vocab_idx_counts = d['reduced_stem_vocab_idx_counts']
262
+ self.morpheme_slot_vocab = d['morpheme_slot_vocab']
263
+ self.morpheme_slot_vocab_idx = d['morpheme_slot_vocab_idx']
264
+ self.morpheme_slot_vocab_idx_counts = d['morpheme_slot_vocab_idx_counts']
265
+ self.affix_vocab = d['affix_vocab']
266
+ self.affix_vocab_idx = d['affix_vocab_idx']
267
+ self.affix_vocab_idx_counts = d['affix_vocab_idx_counts']
268
+
269
+ self.morpheme_slot_vocab_idx_subsample_weights = d['morpheme_slot_vocab_idx_subsample_weights']
270
+ self.morpheme_slot_vocab_idx_subsample_weights_max = d['morpheme_slot_vocab_idx_subsample_weights_max']
271
+ self.morpheme_slot_vocab_idx_subsample_weights_min = d['morpheme_slot_vocab_idx_subsample_weights_min']
272
+
273
+ self.pos_tag_vocab_idx_subsample_weights = d['pos_tag_vocab_idx_subsample_weights']
274
+ self.pos_tag_vocab_idx_subsample_weights_max = d['pos_tag_vocab_idx_subsample_weights_max']
275
+ self.pos_tag_vocab_idx_subsample_weights_min = d['pos_tag_vocab_idx_subsample_weights_min']
276
+
277
+ self.affix_vocab_idx_subsample_weights = d['affix_vocab_idx_subsample_weights']
278
+ self.affix_vocab_idx_subsample_weights_max = d['affix_vocab_idx_subsample_weights_max']
279
+ self.affix_vocab_idx_subsample_weights_min = d['affix_vocab_idx_subsample_weights_min']
280
+
281
+ self.reduced_stem_vocab_idx_subsample_weights = d['reduced_stem_vocab_idx_subsample_weights']
282
+ self.reduced_stem_vocab_idx_subsample_weights_max = d['reduced_stem_vocab_idx_subsample_weights_max']
283
+ self.reduced_stem_vocab_idx_subsample_weights_min = d['reduced_stem_vocab_idx_subsample_weights_min']
284
+
285
+ for k in self.reduced_stem_vocab:
286
+ self.reduced_stem_vocab_idx[self.reduced_stem_vocab[k]] = k
287
+ self.reduced_stem_vocab_idx_counts = dict()
288
+ for i in self._stem_vocab_idx_counts:
289
+ self.reduced_stem_vocab_idx_counts[self.mapped_stem_vocab_idx[i]] = self._stem_vocab_idx_counts[i]
290
+
291
+ class AffixSetVocab:
292
+ def __init__(self, reduced_affix_dict_file = None, reduced_affix_dict_map_file = None):
293
+ self.affix_set_vocab_idx = dict()
294
+ self.affix_set_vocab = dict()
295
+ self.reduced_affix_dict_counts = dict()
296
+ self.reduced_affix_dict_map = dict()
297
+
298
+ if reduced_affix_dict_file is not None:
299
+ f = open(reduced_affix_dict_file, 'r')
300
+ dict_lines = [line.rstrip('\n') for line in f]
301
+ f.close()
302
+ idx = 1
303
+ for l in dict_lines:
304
+ if len(l) > 0:
305
+ spl = l.split(',')
306
+ if (len(spl) == 2):
307
+ self.reduced_affix_dict_counts[spl[0]] = int(spl[1])
308
+ self.affix_set_vocab_idx[spl[0]] = idx
309
+ self.affix_set_vocab[idx] = spl[0]
310
+ idx += 1
311
+
312
+ if reduced_affix_dict_map_file is not None:
313
+ f = open(reduced_affix_dict_map_file, 'r')
314
+ dict_lines = [line.rstrip('\n') for line in f]
315
+ f.close()
316
+ for l in dict_lines:
317
+ if len(l) > 0:
318
+ spl = l.split(',')
319
+ if (len(spl) == 2):
320
+ self.reduced_affix_dict_map[spl[0]] = spl[1]
321
+ if (spl[1]) in self.affix_set_vocab_idx:
322
+ self.affix_set_vocab_idx[spl[0]] = self.affix_set_vocab_idx[spl[1]]
323
+ else:
324
+ self.affix_set_vocab_idx[spl[0]] = 1
325
+
326
+ def affix_set_to_idx(self, key):
327
+ if key in self.affix_set_vocab_idx:
328
+ return self.affix_set_vocab_idx[key]
329
+ else:
330
+ return 1 # N/A
331
+
332
+ def affix_set_idx_to_txt(self, idx, kb_vocab: KBVocab):
333
+ if idx == 1:
334
+ return "N/A"
335
+ elif idx in self.affix_set_vocab:
336
+ return '-'.join([kb_vocab.affix_vocab_idx[int(x)] for x in self.affix_set_vocab[idx].split('-')])
337
+ else:
338
+ return "UNK" # N/A
339
+
340
+ def random_idx(self):
341
+ return random.randint(1, len(self.affix_set_vocab_idx))
342
+
343
+ def state_dict(self):
344
+ return {'affix_set_vocab_idx':self.affix_set_vocab_idx,
345
+ 'reduced_affix_dict_counts':self.reduced_affix_dict_counts,
346
+ 'reduced_affix_dict_map':self.reduced_affix_dict_map}
347
+
348
+ def load_state_dict(self, d):
349
+ self.affix_set_vocab_idx = d['affix_set_vocab_idx']
350
+ self.reduced_affix_dict_counts = d['reduced_affix_dict_counts']
351
+ self.reduced_affix_dict_map = d['reduced_affix_dict_map']
352
+
353
+ class ParsedToken:
354
+ def __init__(self, surface_form, parsed_token=None, decode_prob=None, tf_idf=0.0, pos_tag_id=None, stem_ids=None, line_num=0):
355
+ self.surface_form = surface_form
356
+ self.tf_idf = tf_idf
357
+ if parsed_token is not None:
358
+ parts = parsed_token.split('/')
359
+ self.decode_prob = float(parts[0])
360
+ self.tf_idf = float(parts[1])
361
+ morphs = parts[2].split(',')
362
+ pos_stem = morphs[0].split(':')
363
+ stem_parts = pos_stem[1].split('*')
364
+ if(len(stem_parts[0]) < 1):
365
+ # print('\nParsing wrong token: /{}/ at line # {}'.format(parsed_token, line_num))
366
+ self.pos_tag_idx = int(pos_stem[0])
367
+ self.stem_idx = [6]
368
+ self.morpho_slots_idx = []
369
+ self.affixes_idx = []
370
+ else:
371
+ self.pos_tag_idx = int(pos_stem[0])
372
+ self.stem_idx = [int(v) for v in stem_parts]
373
+ self.morpho_slots_idx = [int(morphs[i].split(':')[0]) for i in range(1, len(morphs))]
374
+ self.affixes_idx = [int(morphs[i].split(':')[1]) for i in range(1, len(morphs))]
375
+ else:
376
+ self.decode_prob = decode_prob
377
+ self.pos_tag_idx = pos_tag_id
378
+ self.stem_idx = stem_ids
379
+ self.morpho_slots_idx = []
380
+ self.affixes_idx = []
381
+
382
+ def append_morpheme(self, morpho_slot_id, affix_id):
383
+ self.morpho_slots_idx.append(morpho_slot_id)
384
+ self.affixes_idx.append(affix_id)
385
+
386
+ def to_parsed_format(self):
387
+ st = ['{}:{}'.format(self.pos_tag_idx,'*'.join([str(i) for i in self.stem_idx]))]
388
+ for i in range(len(self.morpho_slots_idx)):
389
+ st.append('{}:{}'.format(self.morpho_slots_idx[i], self.affixes_idx[i]))
390
+ return '{:.3g}/{:.3g}/{}'.format(self.decode_prob, self.tf_idf, ','.join(st))
391
+
392
+ def affix_set_key(self):
393
+ key = '-'.join([str(af) for af in self.affixes_idx]) if (len(self.affixes_idx) > 0) else 'N/A'
394
+ return key
395
+
396
+ def update_tf_idf_from_idf(doc_sentences):
397
+ doc_voc = dict()
398
+ doc_size = 0.0
399
+ for parsed_tokens in doc_sentences:
400
+ for t in parsed_tokens:
401
+ doc_size += 1.0
402
+ v = 0.0
403
+ if(t.stem_idx[0] in doc_voc):
404
+ v = doc_voc[t.stem_idx[0]]
405
+ doc_voc[t.stem_idx[0]] = v + 1.0
406
+ for parsed_tokens in doc_sentences:
407
+ for t in parsed_tokens:
408
+ t.tf_idf = sigmoid_score(t.tf_idf * doc_voc[t.stem_idx[0]] / doc_size, 0.01, 0.24)
409
+
410
+ def pre_process_parsed_corpus_compute_tfidf(input_corpus, output_corpus):
411
+ f = open(input_corpus, 'r')
412
+ Lines = f.readlines()
413
+ f.close()
414
+
415
+ outfile = open(output_corpus, 'w')
416
+ doc_idx = [i for i in range(len(Lines)) if (len(Lines[i]) == 1)]
417
+ if doc_idx[-1] < (len(Lines) - 1):
418
+ doc_idx.append(len(Lines))
419
+ start_idx = 0
420
+ all_docs = len(doc_idx)
421
+ print_docs = 0
422
+
423
+ tot = 0
424
+ for end_idx in doc_idx:
425
+ tot = tot + 1
426
+ lines_batch = Lines[start_idx:end_idx]
427
+ start_idx = end_idx + 1
428
+ if (len(lines_batch) > 0):
429
+ doc_sentences = []
430
+ for ln in lines_batch:
431
+ line = ln.strip()
432
+ line = line.strip('\n')
433
+ line = line.strip('\t')
434
+ line = line.strip('\r')
435
+ splits = line.split('; ')
436
+ if (len(splits) > 0):
437
+ if ((len(splits[0]) > 4) and ('/' in splits[0]) and (':' in splits[0])):
438
+ doc_sentences.append([ParsedToken('_', parsed_token=t) for t in splits])
439
+ update_tf_idf_from_idf(doc_sentences)
440
+ for sent in doc_sentences:
441
+ outfile.write('; '.join([tok.to_parsed_format() for tok in sent]) + "\n")
442
+ outfile.write("\n")
443
+ outfile.flush()
444
+ print_docs += 1
445
+ outfile.close()
446
+ print('Exported: {} / {} / {}'.format(print_docs, tot, all_docs))
447
+
448
+ def parse_raw_text_lines(doc_lines, kb_vocab, bpe):
449
+ from kinlpmorpholib import ffi, lib
450
+ parsed_tokens = []
451
+ num_sent = ffi.new("int[1]")
452
+ sentences = lib.parse_sentences_batch(doc_lines.encode('utf-8'), num_sent)
453
+ for i in range(num_sent[0]):
454
+ sent = sentences[i]
455
+ for j in range(sent.words_len):
456
+ w = sent.words[j]
457
+ POS_TAG = ffi.string(w.pos_tag).decode("utf-8")
458
+ WORD_TYPE = ffi.string(w.pos_group).decode("utf-8")
459
+ STEM = ffi.string(w.stem).decode("utf-8")
460
+ SURFACE_FORM = ffi.string(w.surface_form).decode("utf-8")
461
+ DECODE_PROB = w.decode_prob
462
+ TF_IDF = w.tf_idf
463
+ pti = kb_vocab.pos_tag_vocab['<UNK>']
464
+ if POS_TAG in kb_vocab.pos_tag_vocab.keys():
465
+ pti = kb_vocab.pos_tag_vocab[POS_TAG]
466
+
467
+ sids = []
468
+ if ((STEM == SURFACE_FORM) and (w.morphemes_len <= 0)):
469
+ list_sub_words = bpe.encode(SURFACE_FORM, output_type=yttm.OutputType.SUBWORD)
470
+ for sub_word in list_sub_words:
471
+ stem_key = WORD_TYPE + ":" + sub_word
472
+ si = kb_vocab._stem_vocab['<UNK>']
473
+ if stem_key in kb_vocab._stem_vocab.keys():
474
+ si = kb_vocab._stem_vocab[stem_key]
475
+ sids.append(si)
476
+ else:
477
+ stem_key = WORD_TYPE + ":" + STEM
478
+ si = kb_vocab._stem_vocab['<UNK>']
479
+ if stem_key in kb_vocab._stem_vocab.keys():
480
+ si = kb_vocab._stem_vocab[stem_key]
481
+ sids.append(si)
482
+ ptoken = ParsedToken(SURFACE_FORM, parsed_token=None, decode_prob=DECODE_PROB, tf_idf=TF_IDF, pos_tag_id=pti, stem_ids=sids)
483
+ if (w.morphemes_len > 0):
484
+ for k in range(w.morphemes_len):
485
+ if ((k != w.stem_start_index) and (k != w.stem_end_index)):
486
+ MORPHEME_SLOT = WORD_TYPE + ":" + str(w.morphemes[k].slot_id)
487
+ MORPHEME = MORPHEME_SLOT + ":" + ffi.string(w.morphemes[k].morph_token).decode("utf-8")
488
+
489
+ msi = kb_vocab.morpheme_slot_vocab['<UNK>']
490
+ if MORPHEME_SLOT in kb_vocab.morpheme_slot_vocab.keys():
491
+ msi = kb_vocab.morpheme_slot_vocab[MORPHEME_SLOT]
492
+
493
+ mi = kb_vocab.affix_vocab['<UNK>']
494
+ if MORPHEME in kb_vocab.affix_vocab.keys():
495
+ mi = kb_vocab.affix_vocab[MORPHEME]
496
+
497
+ ptoken.append_morpheme(msi, mi)
498
+ parsed_tokens.append(ptoken)
499
+ lib.release_sentence(sentences, num_sent)
500
+
501
+ return parsed_tokens
502
+
503
+ def process_parsed_sentence(args, parsed_tokens_list: List[ParsedToken], add_cls, kv : KBVocab, affix_set_vocab : AffixSetVocab, rel_pos_dict, rel_pos_dmax):
504
+ # Input data
505
+ pos_tags = []
506
+ stems = []
507
+ afsets = [] if args.use_afsets else None
508
+ affixes = []
509
+ tokens_lengths = []
510
+
511
+ # Predicted data
512
+ predicted_stems = []
513
+ predicted_afsets = [] if args.use_afsets else None
514
+ predicted_affixes = [] if args.predict_affixes else None
515
+
516
+ predicted_tokens_idx = []
517
+ predicted_tokens_affixes_idx = [] if args.predict_affixes else None
518
+ predicted_tokens_affixes_lengths = [] if args.predict_affixes else None
519
+ # Add <CLS> Token
520
+ if add_cls:
521
+ pos_tags.append(kv.pos_tag_vocab['<CLS>'])
522
+ stems.append(kv.reduced_stem_vocab['<CLS>'])
523
+ if args.use_afsets:
524
+ afsets.append(affix_set_vocab.affix_set_to_idx('<CLS>'))
525
+ tokens_lengths.append(0)
526
+
527
+ if (len(parsed_tokens_list) == 0): # New document
528
+ pos_tags.append(kv.pos_tag_vocab['<SEP>'])
529
+ stems.append(kv.reduced_stem_vocab['<SEP>'])
530
+ if args.use_afsets:
531
+ afsets.append(affix_set_vocab.affix_set_to_idx('<SEP>'))
532
+ tokens_lengths.append(0)
533
+
534
+ else:
535
+ for ptoken in parsed_tokens_list:
536
+ for sidx in ptoken.stem_idx:
537
+ unchanged = True
538
+ predict = False
539
+ rval = random.random()
540
+ if (rval <= 0.15): # 15% of tokens are predicted
541
+ predict = True
542
+ rval /= 0.15
543
+ if(rval < 0.8): # 80% of predicted tokens are masked
544
+ unchanged = False
545
+ pos_tags.append(kv.pos_tag_vocab['<MSK>'])
546
+ stems.append(kv.reduced_stem_vocab['<MSK>'])
547
+ if args.use_afsets:
548
+ afsets.append(affix_set_vocab.affix_set_to_idx('<MSK>'))
549
+
550
+ vv = rval/0.8
551
+ if vv < 0.3: # Include Affixes for 30% of the time to enforce morphology learning
552
+ affixes.extend([(v) for v in ptoken.affixes_idx])
553
+ tokens_lengths.append(len(ptoken.affixes_idx))
554
+ else:
555
+ tokens_lengths.append(0)
556
+ elif (rval < 0.9): # 10% are replaced by random tokens, 10% are left unchanged
557
+ unchanged = False
558
+ rnd_pos = random.randint(kv.pos_tag_vocab['<UNK>'], len(kv.pos_tag_vocab)-1)
559
+ rnd_stem = random.randint(kv.reduced_stem_vocab['<UNK>'], len(kv.reduced_stem_vocab)-1)
560
+
561
+ pos_tags.append(rnd_pos)
562
+ stems.append(rnd_stem)
563
+ if args.use_afsets:
564
+ afsets.append(affix_set_vocab.random_idx())
565
+ elif args.inference_model_file is not None:
566
+ affix_set_vocab.random_idx()
567
+ vv = rval/0.8
568
+ if vv < 0.3: # Include Affixes for 30% of the time to enforce morphology learning
569
+ affixes.extend([(v) for v in ptoken.affixes_idx])
570
+ tokens_lengths.append(len(ptoken.affixes_idx))
571
+ else:
572
+ tokens_lengths.append(0)
573
+ if(unchanged):
574
+ pos_tags.append(ptoken.pos_tag_idx)
575
+ stems.append(kv.mapped_stem_vocab_idx[sidx])
576
+ if args.use_afsets:
577
+ afsets.append(affix_set_vocab.affix_set_to_idx(ptoken.affix_set_key()))
578
+
579
+ affixes.extend([(v) for v in ptoken.affixes_idx])
580
+ tokens_lengths.append(len(ptoken.affixes_idx))
581
+
582
+ if(predict):
583
+ predicted_stems.append(kv.mapped_stem_vocab_idx[sidx])
584
+ predicted_tokens_idx.append(len(tokens_lengths) - 1)
585
+ if args.use_afsets:
586
+ predicted_afsets.append(affix_set_vocab.affix_set_to_idx(ptoken.affix_set_key()))
587
+ if args.predict_affixes:
588
+ predicted_affixes.extend([(v) for v in ptoken.affixes_idx])
589
+ if(len(ptoken.affixes_idx) > 0):
590
+ predicted_tokens_affixes_idx.append(len(predicted_tokens_idx) - 1)
591
+ predicted_tokens_affixes_lengths.append(len(ptoken.affixes_idx))
592
+
593
+ rel_pos_arr = np.zeros((len(pos_tags), len(pos_tags))).astype(int) if (rel_pos_dict is not None) else None
594
+ if rel_pos_arr is not None:
595
+ for i,pi in enumerate(pos_tags):
596
+ for j,pj in enumerate(pos_tags):
597
+ d = i - j
598
+ if ((d != 0) and (d >= -rel_pos_dmax) and (d <= rel_pos_dmax) and ((not add_cls) or ((i != 0) and (j != 0)))):
599
+ rel_pos_arr[i, j] = rel_pos_dict[(pi, pj, d)]
600
+ return (rel_pos_arr,
601
+ pos_tags,
602
+ stems,
603
+ afsets,
604
+ affixes,
605
+ tokens_lengths,
606
+ predicted_stems,
607
+ predicted_afsets,
608
+ predicted_affixes,
609
+ predicted_tokens_idx,
610
+ predicted_tokens_affixes_idx,
611
+ predicted_tokens_affixes_lengths)
612
+
613
+ def gather_replicated_itemized_data(args, corpus_lines, doc_ends, is_corpus_parsed, max_seq_len, start_line, max_batch_items, kb_vocab : KBVocab, affix_set_vocab : AffixSetVocab, bpe: yttm.BPE, rel_pos_dict, rel_pos_dmax, rank=0, bar=None, num_lines=sys.maxsize,shuffle=False,are_parsed_tokens_split=False):
614
+ itemized_data = []
615
+ itemized_parsed_tokens = []
616
+
617
+ seq_pos_tags = []
618
+ seq_stems = []
619
+ seq_afsets = [] if args.use_afsets else None
620
+ seq_affixes = []
621
+ seq_tokens_lengths = []
622
+ seq_predicted_stems = []
623
+ seq_predicted_afsets = [] if args.use_afsets else None
624
+ seq_predicted_affixes = [] if args.predict_affixes else None
625
+ seq_predicted_tokens_idx = []
626
+ seq_predicted_tokens_affixes_idx = [] if args.predict_affixes else None
627
+ seq_predicted_tokens_affixes_lengths = [] if args.predict_affixes else None
628
+
629
+ seq_rel_pos_arr = np.zeros((max_seq_len, max_seq_len)).astype(int) if (rel_pos_dict is not None) else None
630
+
631
+ seq_parsed_tokens = []
632
+
633
+ # if (rank==0):
634
+ # print(time_now(), 'Gathering itemized input sequence data from line # {}'.format(start_line + 1), flush=True)
635
+ lcount = 0
636
+ while (True):
637
+ lcount += 1
638
+ add_cls = (len(seq_parsed_tokens) == 0)
639
+ if add_cls:
640
+ ptoken = ParsedToken('<CLS>', decode_prob=1.0, tf_idf=0.001, pos_tag_id=kb_vocab.pos_tag_vocab['<CLS>'], stem_ids=[kb_vocab.reduced_stem_vocab['<CLS>']])
641
+ ptoken.append_morpheme(kb_vocab.morpheme_slot_vocab['<EOT>'], kb_vocab.affix_vocab['<EOT>'])
642
+ seq_parsed_tokens.append(ptoken)
643
+
644
+ line = corpus_lines[start_line % len(corpus_lines)]
645
+ start_line = (start_line + 1) % len(corpus_lines)
646
+ parsed_tokens_line = []
647
+ if (is_corpus_parsed == True):
648
+ if are_parsed_tokens_split:
649
+ parsed_tokens_line = line
650
+ else:
651
+ splits = line.split('; ')
652
+ if (len(splits) > 0):
653
+ if ((len(splits[0]) > 4) and ('/' in splits[0]) and (':' in splits[0])):
654
+ parsed_tokens_line = [ParsedToken('_', parsed_token=t, line_num=(start_line + 1)) for t in splits]
655
+ else:
656
+ parsed_tokens_line = parse_raw_text_lines(line, kb_vocab, bpe)
657
+
658
+ if (len(parsed_tokens_line) == 0):
659
+ ptoken = ParsedToken('<SEP>', decode_prob=1.0, tf_idf=0.001, pos_tag_id=kb_vocab.pos_tag_vocab['<SEP>'], stem_ids=[kb_vocab.reduced_stem_vocab['<SEP>']])
660
+ ptoken.append_morpheme(kb_vocab.morpheme_slot_vocab['<EOT>'], kb_vocab.affix_vocab['<EOT>'])
661
+ seq_parsed_tokens.append(ptoken)
662
+ else:
663
+ seq_parsed_tokens.extend(parsed_tokens_line)
664
+
665
+ (rel_pos_arr,
666
+ pos_tags,
667
+ stems,
668
+ afsets,
669
+ affixes,
670
+ tokens_lengths,
671
+ predicted_stems,
672
+ predicted_afsets,
673
+ predicted_affixes,
674
+ predicted_tokens_idx,
675
+ predicted_tokens_affixes_idx,
676
+ predicted_tokens_affixes_lengths) = process_parsed_sentence(args, parsed_tokens_line, add_cls, kb_vocab, affix_set_vocab, rel_pos_dict, rel_pos_dmax)
677
+ if (len(seq_tokens_lengths) + len(tokens_lengths)) > max_seq_len:
678
+ data_item = (max_seq_len,
679
+ seq_rel_pos_arr,
680
+ seq_pos_tags,
681
+ seq_stems,
682
+ seq_afsets,
683
+ seq_affixes,
684
+ seq_tokens_lengths,
685
+ seq_predicted_stems,
686
+ seq_predicted_afsets,
687
+ seq_predicted_affixes,
688
+ seq_predicted_tokens_idx,
689
+ seq_predicted_tokens_affixes_idx,
690
+ seq_predicted_tokens_affixes_lengths)
691
+ itemized_data.append(data_item)
692
+ itemized_parsed_tokens.append(seq_parsed_tokens)
693
+ if (shuffle):
694
+ dcx = random.randint(0, len(doc_ends) - 1) % len(doc_ends)
695
+ start_line = (doc_ends[dcx]+1) % len(corpus_lines)
696
+ if random.random() < 0.8: # 80% of the time, start from anywhere within the corpus.
697
+ start_line = random.randint(0, len(corpus_lines) - 1) % len(corpus_lines)
698
+
699
+ if (len(itemized_data) >= max_batch_items):
700
+ if ((rank == 0) and (bar is not None)):
701
+ bar.update(len(itemized_data))
702
+ sys.stdout.flush()
703
+ return itemized_data, itemized_parsed_tokens, start_line
704
+
705
+ if(lcount >= num_lines):
706
+ return itemized_data, itemized_parsed_tokens, start_line
707
+
708
+ seq_pos_tags = []
709
+ seq_stems = []
710
+ seq_afsets = [] if args.use_afsets else None
711
+ seq_affixes = []
712
+ seq_tokens_lengths = []
713
+ seq_predicted_stems = []
714
+ seq_predicted_afsets = [] if args.use_afsets else None
715
+ seq_predicted_affixes = [] if args.predict_affixes else None
716
+ seq_predicted_tokens_idx = []
717
+ seq_predicted_tokens_affixes_idx = [] if args.predict_affixes else None
718
+ seq_predicted_tokens_affixes_lengths = [] if args.predict_affixes else None
719
+
720
+ seq_rel_pos_arr = np.zeros((max_seq_len, max_seq_len)).astype(int) if (rel_pos_dict is not None) else None
721
+
722
+ seq_parsed_tokens = []
723
+
724
+ if ((rank == 0) and ((len(itemized_data) % (math.floor(0.1 * max_batch_items) + 1)) == 0) and (bar is not None)):
725
+ bar.update(len(itemized_data))
726
+ sys.stdout.flush()
727
+ else:
728
+ if args.predict_affixes:
729
+ seq_predicted_tokens_affixes_idx.extend([len(seq_predicted_tokens_idx) + idx for idx in predicted_tokens_affixes_idx])
730
+
731
+ seq_predicted_tokens_idx.extend([len(seq_tokens_lengths)+idx for idx in predicted_tokens_idx])
732
+
733
+ lt = len(seq_tokens_lengths)
734
+ if seq_rel_pos_arr is not None:
735
+ seq_rel_pos_arr[lt:(lt+rel_pos_arr.shape[0]), lt:(lt+rel_pos_arr.shape[1])] = rel_pos_arr
736
+
737
+ seq_pos_tags.extend(pos_tags)
738
+ seq_stems.extend(stems)
739
+ if args.use_afsets:
740
+ seq_afsets.extend(afsets)
741
+ seq_affixes.extend(affixes)
742
+ seq_tokens_lengths.extend(tokens_lengths)
743
+ seq_predicted_stems.extend(predicted_stems)
744
+ if args.use_afsets:
745
+ seq_predicted_afsets.extend(predicted_afsets)
746
+ if args.predict_affixes:
747
+ seq_predicted_affixes.extend(predicted_affixes)
748
+ seq_predicted_tokens_affixes_lengths.extend(predicted_tokens_affixes_lengths)
749
+
750
+ if(lcount >= num_lines):
751
+ data_item = (max_seq_len,
752
+ seq_rel_pos_arr,
753
+ seq_pos_tags,
754
+ seq_stems,
755
+ seq_afsets,
756
+ seq_affixes,
757
+ seq_tokens_lengths,
758
+ seq_predicted_stems,
759
+ seq_predicted_afsets,
760
+ seq_predicted_affixes,
761
+ seq_predicted_tokens_idx,
762
+ seq_predicted_tokens_affixes_idx,
763
+ seq_predicted_tokens_affixes_lengths)
764
+ itemized_data.append(data_item)
765
+ itemized_parsed_tokens.append(seq_parsed_tokens)
766
+ return itemized_data, itemized_parsed_tokens, start_line
767
+
768
+ def morpho_seq_collate_wrapper(batch_items):
769
+ batch_pos_tags = []
770
+ batch_stems = []
771
+ batch_afsets = []
772
+ batch_affixes = []
773
+ batch_tokens_lengths = []
774
+ batch_predicted_stems = []
775
+ batch_predicted_afsets = []
776
+ batch_predicted_affixes = []
777
+ batch_predicted_tokens_idx = []
778
+ batch_predicted_tokens_affixes_idx = []
779
+ batch_predicted_tokens_affixes_lengths = []
780
+
781
+ batch_input_sequence_lengths = []
782
+
783
+ max_sequence_len = batch_items[0][0]
784
+ first_seq_rel_pos_arr = batch_items[0][1]
785
+
786
+ batch_rel_pos_arr = np.zeros((len(batch_items), max_sequence_len, max_sequence_len)).astype(int) if (first_seq_rel_pos_arr is not None) else None
787
+
788
+ for bidx,data_item in enumerate(batch_items):
789
+ (max_seq_len,
790
+ seq_rel_pos_arr,
791
+ seq_pos_tags,
792
+ seq_stems,
793
+ seq_afsets,
794
+ seq_affixes,
795
+ seq_tokens_lengths,
796
+ seq_predicted_stems,
797
+ seq_predicted_afsets,
798
+ seq_predicted_affixes,
799
+ seq_predicted_tokens_idx,
800
+ seq_predicted_tokens_affixes_idx,
801
+ seq_predicted_tokens_affixes_lengths) = data_item
802
+
803
+ if batch_rel_pos_arr is not None:
804
+ batch_rel_pos_arr[bidx,:,:] = seq_rel_pos_arr
805
+ # Need to offset from total number of predicted token indices
806
+ if seq_predicted_tokens_affixes_idx is not None:
807
+ batch_predicted_tokens_affixes_idx.extend([(len(batch_predicted_tokens_idx) + t) for t in seq_predicted_tokens_affixes_idx])
808
+
809
+ batch_predicted_tokens_idx.extend([(t, len(batch_input_sequence_lengths)) for t in seq_predicted_tokens_idx])
810
+
811
+ batch_pos_tags.extend(seq_pos_tags)
812
+ batch_stems.extend(seq_stems)
813
+ if seq_afsets is not None:
814
+ batch_afsets.extend(seq_afsets)
815
+ batch_affixes.extend(seq_affixes)
816
+ batch_tokens_lengths.extend(seq_tokens_lengths)
817
+ batch_predicted_stems.extend(seq_predicted_stems)
818
+ if seq_predicted_afsets is not None:
819
+ batch_predicted_afsets.extend(seq_predicted_afsets)
820
+ if seq_predicted_affixes is not None:
821
+ batch_predicted_affixes.extend(seq_predicted_affixes)
822
+ batch_predicted_tokens_affixes_lengths.extend(seq_predicted_tokens_affixes_lengths)
823
+
824
+ batch_input_sequence_lengths.append(len(seq_tokens_lengths))
825
+
826
+ data_item = (batch_input_sequence_lengths,
827
+ batch_rel_pos_arr,
828
+ batch_pos_tags,
829
+ batch_stems,
830
+ batch_afsets,
831
+ batch_affixes,
832
+ batch_tokens_lengths,
833
+ batch_predicted_stems,
834
+ batch_predicted_afsets,
835
+ batch_predicted_affixes,
836
+ batch_predicted_tokens_idx,
837
+ batch_predicted_tokens_affixes_idx,
838
+ batch_predicted_tokens_affixes_lengths)
839
+ return data_item
840
+
841
+ class KBCorpusDataset(Dataset):
842
+
843
+ def __init__(self,args,
844
+ kb_vocab : KBVocab, affix_set_vocab : AffixSetVocab, bpe_encoder: yttm.BPE,
845
+ rel_pos_dict, rel_pos_dmax,
846
+ parsed_corpus_lines, doc_ends, is_corpus_parsed,
847
+ start_line, max_batch_items,
848
+ max_seq_len = 512,
849
+ rank = 0):
850
+ self.max_seq_len = max_seq_len
851
+ self.start_line = start_line
852
+ self.max_batch_items = max_batch_items
853
+ if (rank==0):
854
+ with progressbar.ProgressBar(max_value=max_batch_items, redirect_stdout=True) as bar:
855
+ self.itemized_data, self.itemized_parsed_tokens, self.start_line = gather_replicated_itemized_data(args, parsed_corpus_lines, doc_ends,
856
+ is_corpus_parsed,
857
+ self.max_seq_len,
858
+ self.start_line,
859
+ self.max_batch_items,
860
+ kb_vocab, affix_set_vocab, bpe_encoder,
861
+ rel_pos_dict, rel_pos_dmax,
862
+ rank = rank, bar=bar,
863
+ shuffle = True)
864
+ else:
865
+ self.itemized_data, self.itemized_parsed_tokens, self.start_line = gather_replicated_itemized_data(args, parsed_corpus_lines, doc_ends,
866
+ is_corpus_parsed,
867
+ self.max_seq_len,
868
+ self.start_line,
869
+ self.max_batch_items,
870
+ kb_vocab, affix_set_vocab, bpe_encoder,
871
+ rel_pos_dict, rel_pos_dmax,
872
+ rank=rank, bar=None,
873
+ shuffle = True)
874
+ def __len__(self):
875
+ return len(self.itemized_data)
876
+
877
+ def __getitem__(self, idx):
878
+ return self.itemized_data[idx]
879
+
880
+ from itertools import accumulate
881
+
882
+ from morpho_model import KinyaBERT
883
+
884
+ def morpho_model_forward(args, data_item, model : KinyaBERT, device, tot_num_affixes):
885
+ (batch_input_sequence_lengths,
886
+ batch_rel_pos_arr,
887
+ batch_pos_tags,
888
+ batch_stems,
889
+ batch_afsets,
890
+ batch_affixes,
891
+ batch_tokens_lengths,
892
+ batch_predicted_stems,
893
+ batch_predicted_afsets,
894
+ batch_predicted_affixes,
895
+ batch_predicted_tokens_idx,
896
+ batch_predicted_tokens_affixes_idx,
897
+ batch_predicted_tokens_affixes_lengths) = data_item
898
+
899
+ tokens_lengths = batch_tokens_lengths # torch.tensor(batch_tokens_lengths).to(device)
900
+ input_sequence_lengths = batch_input_sequence_lengths # torch.tensor(batch_input_sequence_lengths).to(device)
901
+ pos_tags = torch.tensor(batch_pos_tags).to(device)
902
+ stems = torch.tensor(batch_stems).to(device)
903
+ afsets = torch.tensor(batch_afsets).to(device) if args.use_afsets else None
904
+ affixes = torch.tensor(batch_affixes).to(device)
905
+
906
+ predicted_tokens_idx = torch.tensor([s * max(batch_input_sequence_lengths) + t for t, s in batch_predicted_tokens_idx]).to(device)
907
+ predicted_tokens_affixes_idx = torch.tensor(batch_predicted_tokens_affixes_idx).to(device) if args.predict_affixes else None
908
+
909
+ predicted_affixes_prob = None
910
+ if args.predict_affixes:
911
+ pred_affixes_list = [batch_predicted_affixes[x - y: x] for x, y in zip(accumulate(batch_predicted_tokens_affixes_lengths), batch_predicted_tokens_affixes_lengths)]
912
+ afx_prob = torch.zeros(len(pred_affixes_list), tot_num_affixes)
913
+ for i,lst in enumerate(pred_affixes_list):
914
+ assert (len(lst) > 0)
915
+ afx_prob[i,lst] = 1 / len(lst)
916
+ predicted_affixes_prob = afx_prob.to(device)
917
+
918
+ predicted_stems = torch.tensor(batch_predicted_stems).to(device)
919
+ predicted_afsets = torch.tensor(batch_predicted_afsets).to(device) if args.use_afsets else None
920
+ rel_pos_arr = torch.from_numpy(batch_rel_pos_arr).to(device) if (batch_rel_pos_arr is not None) else None
921
+
922
+ return model(args, rel_pos_arr, tokens_lengths, input_sequence_lengths, pos_tags, stems, afsets, affixes,
923
+ predicted_tokens_idx,
924
+ predicted_tokens_affixes_idx,
925
+ predicted_stems,
926
+ predicted_afsets,
927
+ predicted_affixes_prob)
928
+
929
+ def morpho_model_seq_predict(args, data_item, model : KinyaBERT, device,
930
+ max_predict_affixes, proposed_stem_ids=None):
931
+ (max_seq_len,
932
+ seq_rel_pos_arr,
933
+ seq_pos_tags,
934
+ seq_stems,
935
+ seq_afsets,
936
+ seq_affixes,
937
+ seq_tokens_lengths,
938
+ seq_predicted_stems,
939
+ seq_predicted_afsets,
940
+ seq_predicted_affixes,
941
+ seq_predicted_tokens_idx,
942
+ seq_predicted_tokens_affixes_idx,
943
+ seq_predicted_tokens_affixes_lengths) = data_item
944
+
945
+ tokens_lengths = seq_tokens_lengths
946
+ input_sequence_lengths = [len(seq_tokens_lengths)]
947
+ pos_tags = torch.tensor(seq_pos_tags).to(device)
948
+ stems = torch.tensor(seq_stems).to(device)
949
+ afsets = torch.tensor(seq_afsets).to(device) if args.use_afsets else None
950
+ affixes = torch.tensor(seq_affixes).to(device)
951
+
952
+ seq_predicted_token_idx = torch.tensor(seq_predicted_tokens_idx).to(device)
953
+
954
+ rel_pos_arr = torch.from_numpy(seq_rel_pos_arr).unsqueeze(0).to(device) if (seq_rel_pos_arr is not None) else None
955
+
956
+ return model.predict(args, rel_pos_arr, tokens_lengths, input_sequence_lengths, pos_tags, stems, afsets, affixes,
957
+ seq_predicted_token_idx,
958
+ max_predict_affixes, proposed_stem_ids=proposed_stem_ids)
code/morpho_learning_rates.py ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding=utf-8
2
+ # Copyright (c) 2019, NVIDIA CORPORATION. All rights reserved.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ """PyTorch DataLoader for TFRecords"""
16
+
17
+ import torch
18
+
19
+ from torch.optim.lr_scheduler import _LRScheduler
20
+ import math
21
+
22
+ class AnnealingLR(_LRScheduler):
23
+ """Anneals the learning rate from start to zero along a cosine curve."""
24
+
25
+ DECAY_STYLES = ['linear', 'cosine', 'exponential', 'constant', 'None']
26
+
27
+ def __init__(self, optimizer, start_lr, warmup_iter, num_iters, decay_style=None, last_iter=-1):
28
+ self.optimizer = optimizer
29
+ self.start_lr = start_lr
30
+ self.warmup_iter = warmup_iter
31
+ self.num_iters = last_iter + 1
32
+ self.end_iter = num_iters
33
+ self.decay_style = decay_style.lower() if isinstance(decay_style, str) else None
34
+ self.step(self.num_iters)
35
+ self.gamma = 0.995
36
+ self.iters_to_steps = 80.0
37
+ #print('learning rate decaying', decay_style)
38
+
39
+ def get_lr(self):
40
+ # https://openreview.net/pdf?id=BJYwwY9ll pg. 4
41
+ if self.warmup_iter > 0 and self.num_iters <= self.warmup_iter:
42
+ return float(self.start_lr) * self.num_iters / self.warmup_iter
43
+ else:
44
+ if self.decay_style == self.DECAY_STYLES[0]:
45
+ return self.start_lr*((self.end_iter-(self.num_iters-self.warmup_iter))/self.end_iter)
46
+ elif self.decay_style == self.DECAY_STYLES[1]:
47
+ return self.start_lr / 2.0 * (math.cos(math.pi * (self.num_iters - self.warmup_iter) / self.end_iter) + 1)
48
+ elif self.decay_style == self.DECAY_STYLES[2]:
49
+ #TODO: implement exponential decay
50
+ return self.start_lr * pow(self.gamma, (self.num_iters - self.warmup_iter) / self.iters_to_steps)
51
+ else:
52
+ return self.start_lr
53
+
54
+ def step(self, step_num=None):
55
+ if step_num is None:
56
+ step_num = self.num_iters + 1
57
+ self.num_iters = step_num
58
+ new_lr = self.get_lr()
59
+ for group in self.optimizer.param_groups:
60
+ group['lr'] = new_lr
61
+
62
+ def state_dict(self):
63
+ sd = {
64
+ 'start_lr': self.start_lr,
65
+ 'warmup_iter': self.warmup_iter,
66
+ 'num_iters': self.num_iters,
67
+ 'decay_style': self.decay_style,
68
+ 'end_iter': self.end_iter
69
+ }
70
+ return sd
71
+
72
+ def load_state_dict(self, sd):
73
+ self.start_lr = sd['start_lr']
74
+ self.warmup_iter = sd['warmup_iter']
75
+ self.num_iters = sd['num_iters']
76
+ self.end_iter = sd['end_iter']
77
+ self.decay_style = sd['decay_style']
78
+ self.step(self.num_iters)
code/morpho_model.py ADDED
@@ -0,0 +1,850 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Antoine Nzeyimana.
2
+ #
3
+ # This source code is licensed under the MIT license found in the
4
+ # LICENSE file in the root directory of this source tree.
5
+
6
+ from __future__ import print_function, division
7
+
8
+ import math
9
+ # Ignore warnings
10
+ import warnings
11
+
12
+ import torch
13
+ import torch.nn as nn
14
+ import torch.nn.functional as F
15
+ from torch.cuda.amp import custom_fwd
16
+ from torch.nn.utils.rnn import pad_sequence
17
+
18
+ from morpho_transformer import TransformerEncoder, TransformerEncoderLayer, MultiheadAttention
19
+
20
+ warnings.filterwarnings("ignore")
21
+
22
+ def gelu(x):
23
+ """Implementation of the gelu activation function.
24
+ For information: OpenAI GPT's gelu is slightly different (and gives slightly different results):
25
+ 0.5 * x * (1 + torch.tanh(math.sqrt(2 / math.pi) * (x + 0.044715 * torch.pow(x, 3))))
26
+ """
27
+ return x * 0.5 * (1.0 + torch.erf(x / math.sqrt(2.0)))
28
+
29
+
30
+ def swish(x):
31
+ return x * torch.sigmoid(x)
32
+
33
+ def init_bert_params(module):
34
+ """
35
+ Initialize the weights specific to the BERT Model.
36
+ This overrides the default initializations depending on the specified arguments.
37
+ 1. If normal_init_linear_weights is set then weights of linear
38
+ layer will be initialized using the normal distribution and
39
+ bais will be set to the specified value.
40
+ 2. If normal_init_embed_weights is set then weights of embedding
41
+ layer will be initialized using the normal distribution.
42
+ 3. If normal_init_proj_weights is set then weights of
43
+ in_project_weight for MultiHeadAttention initialized using
44
+ the normal distribution (to be validated).
45
+ """
46
+
47
+ if isinstance(module, nn.Linear):
48
+ module.weight.data.normal_(mean=0.0, std=0.02)
49
+ if module.bias is not None:
50
+ module.bias.data.zero_()
51
+ if isinstance(module, nn.Embedding):
52
+ module.weight.data.normal_(mean=0.0, std=0.02)
53
+ if module.padding_idx is not None:
54
+ module.weight.data[module.padding_idx].zero_()
55
+ if isinstance(module, MultiheadAttention):
56
+ module.in_proj_weight.data.normal_(mean=0.0, std=0.02)
57
+
58
+ # From: https://github.com/guolinke/TUPE/blob/master/fairseq/modules/transformer_sentence_encoder.py
59
+ # this is from T5
60
+ def tupe_relative_position_bucket(relative_position, bidirectional=True, num_buckets=32, max_distance=128):
61
+ ret = 0
62
+ n = -relative_position
63
+ if bidirectional:
64
+ num_buckets //= 2
65
+ ret += (n < 0).to(torch.long) * num_buckets # mtf.to_int32(mtf.less(n, 0)) * num_buckets
66
+ n = torch.abs(n)
67
+ else:
68
+ n = torch.max(n, torch.zeros_like(n))
69
+ # now n is in the range [0, inf)
70
+
71
+ # half of the buckets are for exact increments in positions
72
+ max_exact = num_buckets // 2
73
+ is_small = n < max_exact
74
+
75
+ # The other half of the buckets are for logarithmically bigger bins in positions up to max_distance
76
+ val_if_large = max_exact + (
77
+ torch.log(n.float() / max_exact) / math.log(max_distance / max_exact) * (num_buckets - max_exact)
78
+ ).to(torch.long)
79
+ val_if_large = torch.min(val_if_large, torch.full_like(val_if_large, num_buckets - 1))
80
+
81
+ ret += torch.where(is_small, n, val_if_large)
82
+ return ret
83
+
84
+ class BertLayerNorm(nn.Module):
85
+ def __init__(self, hidden_size, eps=1e-12):
86
+ """Construct a layernorm module in the TF style (epsilon inside the square root).
87
+ """
88
+ super(BertLayerNorm, self).__init__()
89
+ self.weight = nn.Parameter(torch.ones(hidden_size))
90
+ self.bias = nn.Parameter(torch.zeros(hidden_size))
91
+ self.variance_epsilon = eps
92
+
93
+ def forward(self, x):
94
+ u = x.mean(-1, keepdim=True)
95
+ s = (x - u).pow(2).mean(-1, keepdim=True)
96
+ x = (x - u) / torch.sqrt(s + self.variance_epsilon)
97
+ return self.weight * x + self.bias
98
+
99
+ class BertHeadTransform(nn.Module):
100
+ def __init__(self, tr_d_model, cls_ctxt_size, layernorm_epsilon):
101
+ super(BertHeadTransform, self).__init__()
102
+ self.dense = nn.Linear(tr_d_model, cls_ctxt_size)
103
+ self.layerNorm = BertLayerNorm(cls_ctxt_size, eps=layernorm_epsilon)
104
+
105
+ def forward(self, hidden_states):
106
+ hidden_states = self.dense(hidden_states)
107
+ hidden_states = gelu(hidden_states)
108
+ hidden_states = self.layerNorm(hidden_states)
109
+ return hidden_states
110
+
111
+ class TokenClassificationHead(nn.Module):
112
+ def __init__(self, input_dim, inner_dim, num_classes, pooler_dropout=0.3):
113
+ super(TokenClassificationHead, self).__init__()
114
+ self.input_dim = input_dim
115
+
116
+ # Experimental
117
+ #------------------
118
+ self.dense = nn.Linear(input_dim, inner_dim)
119
+ self.layerNorm = torch.nn.LayerNorm(inner_dim)
120
+ self.activation_fn = torch.tanh
121
+ self.in_dropout = nn.Dropout(p=pooler_dropout)
122
+ #------------------
123
+
124
+ self.out_dropout = nn.Dropout(p=pooler_dropout)
125
+ self.out_proj = nn.Linear(inner_dim, num_classes)
126
+
127
+ self.apply(init_bert_params)
128
+
129
+ @custom_fwd
130
+ def forward(self, features, input_sequence_lengths):
131
+ # features.shape = S x N x E
132
+ # Remove [CLS]
133
+ # len already includes [CLS] in the sequence length count, so number of normal tokens here is (len-1)
134
+ inputs = [features[1:len, i, :].contiguous().view(-1, self.input_dim) for i,len in enumerate(input_sequence_lengths)]
135
+ x = torch.cat(inputs, 0) # B x E
136
+
137
+ # Experimental
138
+ #------------------
139
+ x = self.in_dropout(x)
140
+ x = self.dense(x)
141
+ x = self.layerNorm(x)
142
+ x = self.activation_fn(x)
143
+ #------------------
144
+
145
+ x = self.out_dropout(x)
146
+ x = self.out_proj(x)
147
+ return x
148
+
149
+
150
+ class ClassificationHead(nn.Module):
151
+ def __init__(self, input_dim, inner_dim, num_classes, pooler_dropout=0.0):
152
+ super(ClassificationHead, self).__init__()
153
+ self.input_dim = input_dim
154
+
155
+ # Experimental
156
+ #------------------
157
+ self.dense = nn.Linear(input_dim, inner_dim)
158
+ self.layerNorm = torch.nn.LayerNorm(inner_dim)
159
+ self.activation_fn = torch.tanh
160
+ self.in_dropout = nn.Dropout(p=pooler_dropout)
161
+ # ------------------
162
+
163
+ self.out_dropout = nn.Dropout(p=pooler_dropout)
164
+ self.out_proj = nn.Linear(inner_dim, num_classes)
165
+
166
+ self.apply(init_bert_params)
167
+
168
+ def forward(self, features):
169
+ # features.shape = S x N x E
170
+ x = features[0, :, :] # Take [CLS]
171
+
172
+ # Experimental
173
+ # ------------------
174
+ x = self.in_dropout(x)
175
+ x = self.dense(x)
176
+ x = self.layerNorm(x)
177
+ x = self.activation_fn(x)
178
+ # ------------------
179
+
180
+ x = self.out_dropout(x)
181
+ x = self.out_proj(x)
182
+ return x
183
+
184
+ class MorphoHeadPredictor(nn.Module):
185
+ def __init__(self, args, stem_embedding_weights, afset_embedding_weights, affix_embedding_weights, tr_d_model, tr_dropout, layernorm_epsilon):
186
+ super(MorphoHeadPredictor, self).__init__()
187
+
188
+ self.stem_transform = BertHeadTransform(tr_d_model, stem_embedding_weights.size(1), layernorm_epsilon)
189
+ self.stem_decoder = nn.Linear(stem_embedding_weights.size(1), stem_embedding_weights.size(0), bias=False)
190
+ self.stem_decoder.weight = stem_embedding_weights
191
+ self.stem_decoder_bias = nn.Parameter(torch.zeros(stem_embedding_weights.size(0)))
192
+
193
+ if args.use_afsets:
194
+ self.afset_transform = BertHeadTransform(tr_d_model, afset_embedding_weights.size(1), layernorm_epsilon)
195
+ self.afset_decoder = nn.Linear(afset_embedding_weights.size(1), afset_embedding_weights.size(0), bias=False)
196
+ self.afset_decoder.weight = afset_embedding_weights
197
+ self.afset_decoder_bias = nn.Parameter(torch.zeros(afset_embedding_weights.size(0)))
198
+
199
+ if args.predict_affixes:
200
+ self.affix_transform = BertHeadTransform(tr_d_model, affix_embedding_weights.size(1), layernorm_epsilon)
201
+ self.affix_decoder = nn.Linear(affix_embedding_weights.size(1), affix_embedding_weights.size(0), bias=False)
202
+ self.affix_decoder.weight = affix_embedding_weights
203
+ self.affix_decoder_bias = nn.Parameter(torch.zeros(affix_embedding_weights.size(0)))
204
+
205
+ self.apply(init_bert_params)
206
+
207
+ @custom_fwd
208
+ def forward(self, args, tr_hidden_state,
209
+ predicted_tokens_idx,
210
+ predicted_tokens_affixes_idx,
211
+ predicted_stems,
212
+ predicted_afsets,
213
+ predicted_affixes_prob):
214
+
215
+ # print('tr_hidden_state.shape',tr_hidden_state.shape)
216
+ # 1. Crop together predicted tokens
217
+ # tr_hidden_state.shape = S x N x E
218
+ token_hidden_state = tr_hidden_state.permute(1,0,2)
219
+ # N x S x E
220
+ # print('token_hidden_state.shape',token_hidden_state.shape)
221
+ token_hidden_state = token_hidden_state.reshape(-1, token_hidden_state.shape[2])
222
+ # predicted_state.shape: NS x E or B x E
223
+ # print('token_hidden_state.shape',token_hidden_state.shape)
224
+ # print('batch_predicted_token_idx',batch_predicted_token_idx)
225
+ token_hidden_state = torch.index_select(token_hidden_state, 0, index=predicted_tokens_idx)
226
+ # predicted_state.shape: B x E
227
+ stem_predicted_state = self.stem_transform(token_hidden_state)
228
+ # predicted_state.shape: B x F ==> Only containing the 15% tokens to be predicted
229
+
230
+ # 3. Propagate to Stem Prediction
231
+ stem_scores = self.stem_decoder(stem_predicted_state) + self.stem_decoder_bias
232
+ stem_scores = F.log_softmax(stem_scores, dim=1)
233
+ stem_loss_avg = F.nll_loss(stem_scores, predicted_stems)
234
+
235
+ loss = torch.tensor(0.0).to(stem_loss_avg.device)
236
+ loss += stem_loss_avg
237
+
238
+ # 3*. Propagate to AffixSet Prediction
239
+ afset_loss_avg = torch.tensor(0.0).to(stem_loss_avg.device)
240
+ if args.use_afsets and (predicted_afsets is not None):
241
+ afset_predicted_state = self.afset_transform(token_hidden_state)
242
+ afset_scores = self.afset_decoder(afset_predicted_state) + self.afset_decoder_bias
243
+ afset_scores = F.log_softmax(afset_scores, dim=1)
244
+ afset_loss_avg = F.nll_loss(afset_scores, predicted_afsets)
245
+ loss += afset_loss_avg
246
+
247
+ # 6. Propagate to Affix prediction
248
+ affix_loss_avg = torch.tensor(0.0).to(stem_loss_avg.device)
249
+ if args.predict_affixes and (predicted_tokens_affixes_idx is not None) and (predicted_affixes_prob is not None):
250
+ if predicted_tokens_affixes_idx.nelement() > 0:
251
+ affix_hidden_state = torch.index_select(token_hidden_state, 0, index=predicted_tokens_affixes_idx)
252
+ affix_predicted_state = self.affix_transform(affix_hidden_state)
253
+ affix_scores = self.affix_decoder(affix_predicted_state) + self.affix_decoder_bias
254
+ affix_scores = F.log_softmax(affix_scores, dim=1)
255
+ affix_loss_avg = F.kl_div(affix_scores, predicted_affixes_prob, reduction='batchmean')
256
+
257
+ loss += affix_loss_avg
258
+
259
+ return loss, stem_loss_avg, afset_loss_avg, affix_loss_avg
260
+
261
+ def predict(self, args, tr_hidden_state,
262
+ seq_predicted_token_idx,
263
+ max_predict_affixes, proposed_stem_ids=None):
264
+ # print('tr_hidden_state.shape',tr_hidden_state.shape)
265
+ # 1. Crop together predicted tokens
266
+ # tr_hidden_state.shape = S x N x E
267
+ token_hidden_state = tr_hidden_state.permute(1,0,2)
268
+ # N x S x E
269
+ # print('token_hidden_state.shape',token_hidden_state.shape)
270
+ token_hidden_state = token_hidden_state.reshape(-1, token_hidden_state.shape[2])
271
+ # predicted_state.shape: NS x E = B x E
272
+ # print('token_hidden_state.shape',token_hidden_state.shape)
273
+ # print('batch_predicted_token_idx',batch_predicted_token_idx)
274
+ token_hidden_state = torch.index_select(token_hidden_state, 0, index=seq_predicted_token_idx)
275
+ # predicted_state.shape: B x E
276
+ stem_predicted_state = self.stem_transform(token_hidden_state)
277
+ # predicted_state.shape: B x F ==> Only containing the 15% tokens to be predicted
278
+
279
+ # 3. Propagate to Stem Prediction
280
+ stem_scores = self.stem_decoder(stem_predicted_state) + self.stem_decoder_bias
281
+ stem_scores = F.softmax(stem_scores, dim=1)
282
+ # stem_predictions = stem_scores.argmax(dim=1)
283
+ if proposed_stem_ids is not None:
284
+ selection = stem_scores[:, proposed_stem_ids]
285
+ stem_predictions_prob, pred_ids = selection.max(dim=1)
286
+ pred_list = pred_ids.tolist()
287
+ stem_predictions = torch.tensor([proposed_stem_ids[ii] for ii in pred_list])
288
+ else:
289
+ stem_predictions_prob, stem_predictions = stem_scores.max(dim=1)
290
+
291
+ afset_predictions = None
292
+ afset_predictions_prob = None
293
+ if args.use_afsets:
294
+ afset_predicted_state = self.afset_transform(token_hidden_state)
295
+ afset_scores = self.afset_decoder(afset_predicted_state) + self.afset_decoder_bias
296
+ afset_scores = F.softmax(afset_scores, dim=1)
297
+ afset_predictions_prob, afset_predictions = afset_scores.max(dim=1)
298
+
299
+ # 5. Predict affix
300
+ affix_predictions = None
301
+ if args.predict_affixes:
302
+ affix_predicted_state = self.affix_transform(token_hidden_state)
303
+ affix_scores = self.affix_decoder(affix_predicted_state) + self.affix_decoder_bias
304
+ affix_scores = F.log_softmax(affix_scores, dim=1)
305
+ _, top_affixes = torch.topk(affix_scores, max_predict_affixes, dim=1)
306
+ affix_predictions = []
307
+ for batch in range(top_affixes.shape[0]):
308
+ affix_predictions.append(top_affixes[batch].tolist())
309
+
310
+ return stem_predictions, stem_predictions_prob, afset_predictions, afset_predictions_prob, affix_predictions
311
+
312
+ class KinyaBERT_MorphoEncoder(nn.Module):
313
+ def __init__(self, args,
314
+ num_stems, num_afsets, num_pos_tags, num_affixes,
315
+ num_pos_aware_rel_pos_dict_size,
316
+ num_pos_m_embeddings,
317
+ num_stem_m_embeddings,
318
+ use_affix_bow_m_embedding,
319
+ use_pos_aware_rel_pos_bias,
320
+ use_tupe_rel_pos_bias,
321
+ max_seq_len = 512,
322
+ morpho_dim = 80, stem_dim = 160,
323
+ morpho_tr_nhead = 4, morpho_tr_nlayers=4,
324
+ morpho_tr_dim_feedforward=512, morpho_tr_dropout=0.1, morpho_tr_activation='gelu',
325
+ seq_tr_nhead=8, seq_tr_nlayers=8,
326
+ seq_tr_dim_feedforward=2048, seq_tr_dropout=0.1, seq_tr_activation='gelu',
327
+ tupe_rel_pos_bins: int = 32,
328
+ tupe_max_rel_pos: int = 128):
329
+ super(KinyaBERT_MorphoEncoder, self).__init__()
330
+
331
+ self.seq_tr_nhead = seq_tr_nhead
332
+ self.max_seq_len = max_seq_len
333
+ self.tot_num_affixes = num_affixes
334
+ self.num_pos_m_embeddings = num_pos_m_embeddings
335
+ self.num_stem_m_embeddings = num_stem_m_embeddings
336
+ self.use_affix_bow_m_embedding = use_affix_bow_m_embedding
337
+
338
+ self.seq_tr_d_model = stem_dim
339
+ self.morpho_dim = morpho_dim
340
+ self.attn_scale_factor = 2
341
+
342
+ self.tot_morpho_idx = 0
343
+ if self.num_pos_m_embeddings > 0:
344
+ self.m1_pos_embedding = nn.Embedding(num_pos_tags, self.morpho_dim, padding_idx=0)
345
+ self.tot_morpho_idx += 1
346
+
347
+ if self.num_pos_m_embeddings > 1:
348
+ self.m2_pos_embedding = nn.Embedding(num_pos_tags, self.morpho_dim, padding_idx=0)
349
+ self.tot_morpho_idx += 1
350
+
351
+ if self.num_pos_m_embeddings > 2:
352
+ self.m3_pos_embedding = nn.Embedding(num_pos_tags, self.morpho_dim, padding_idx=0)
353
+ self.tot_morpho_idx += 1
354
+
355
+ if self.num_stem_m_embeddings > 0:
356
+ self.m_stem_embedding = nn.Embedding(num_stems, self.morpho_dim, padding_idx=0)
357
+ self.tot_morpho_idx += 1
358
+
359
+ if args.use_afsets and (num_afsets > 0):
360
+ self.m_afset_embedding = nn.Embedding(num_afsets, self.morpho_dim, padding_idx=0)
361
+ self.tot_morpho_idx += 1
362
+
363
+ self.seq_tr_d_model += (self.morpho_dim * self.tot_morpho_idx)
364
+
365
+ if self.use_affix_bow_m_embedding:
366
+ self.seq_tr_d_model += self.morpho_dim
367
+
368
+ self.s_stem_embedding = nn.Embedding(num_stems, stem_dim, padding_idx=0)
369
+
370
+ if args.use_morpho_encoder and (self.seq_tr_d_model > stem_dim):
371
+ self.m_affix_embedding = nn.Embedding(num_affixes, self.morpho_dim, padding_idx=0)
372
+ morpho_encoder_layers = TransformerEncoderLayer(self.morpho_dim, morpho_tr_nhead, dim_feedforward=morpho_tr_dim_feedforward, dropout=morpho_tr_dropout, activation=morpho_tr_activation)
373
+ self.morpho_transformer_encoder = TransformerEncoder(morpho_encoder_layers, morpho_tr_nlayers)
374
+
375
+ sequence_encoder_layers = TransformerEncoderLayer(self.seq_tr_d_model, self.seq_tr_nhead, dim_feedforward=seq_tr_dim_feedforward, dropout=seq_tr_dropout, activation=seq_tr_activation)
376
+ self.seq_transformer_encoder = TransformerEncoder(sequence_encoder_layers, seq_tr_nlayers)
377
+
378
+ self.use_pos_aware_rel_pos_bias = use_pos_aware_rel_pos_bias
379
+ if self.use_pos_aware_rel_pos_bias:
380
+ self.rel_pos_embedding = nn.Embedding(num_pos_aware_rel_pos_dict_size+1, self.seq_tr_nhead, padding_idx=0)
381
+
382
+ # This is from TUPE
383
+ self.pos = nn.Embedding(self.max_seq_len + 1, self.seq_tr_d_model)
384
+ self.pos_q_linear = nn.Linear(self.seq_tr_d_model, self.seq_tr_d_model)
385
+ self.pos_k_linear = nn.Linear(self.seq_tr_d_model, self.seq_tr_d_model)
386
+ self.pos_scaling = float(self.seq_tr_d_model / self.seq_tr_nhead * self.attn_scale_factor) ** -0.5
387
+ self.pos_ln = nn.LayerNorm(self.seq_tr_d_model)
388
+
389
+ self.use_tupe_rel_pos_bias = use_tupe_rel_pos_bias
390
+ if self.use_tupe_rel_pos_bias:
391
+ assert tupe_rel_pos_bins % 2 == 0
392
+ self.tupe_rel_pos_bins = tupe_rel_pos_bins
393
+ self.tupe_max_rel_pos = tupe_max_rel_pos
394
+ self.relative_attention_bias = nn.Embedding(self.tupe_rel_pos_bins + 1, self.seq_tr_nhead)
395
+ seq_len = self.max_seq_len
396
+ context_position = torch.arange(seq_len, dtype=torch.long)[:, None]
397
+ memory_position = torch.arange(seq_len, dtype=torch.long)[None, :]
398
+ relative_position = memory_position - context_position
399
+ self.rp_bucket = tupe_relative_position_bucket(
400
+ relative_position,
401
+ num_buckets=self.tupe_rel_pos_bins,
402
+ max_distance=self.tupe_max_rel_pos
403
+ )
404
+ # others to [CLS]
405
+ self.rp_bucket[:, 0] = self.tupe_rel_pos_bins
406
+ # [CLS] to others, Note: self.tupe_rel_pos_bins // 2 is not used in relative_position_bucket
407
+ self.rp_bucket[0, :] = self.tupe_rel_pos_bins // 2
408
+
409
+ self.apply(init_bert_params)
410
+
411
+ def get_tupe_rel_pos_bias(self, seq_len, device):
412
+ # Assume the input is ordered. If your input token is permuted, you may need to update this accordingly
413
+ if self.rp_bucket.device != device:
414
+ self.rp_bucket = self.rp_bucket.to(device)
415
+ # Adjusted because final x's shape is L x B X E
416
+ rp_bucket = self.rp_bucket[:seq_len, :seq_len]
417
+ values = F.embedding(rp_bucket, self.relative_attention_bias.weight)
418
+ values = values.permute([2, 0, 1])
419
+ return values.contiguous()
420
+
421
+ def get_pos_aware_rel_pos_bias(self, rel_pos_arr, seq_len):
422
+ rel_pos_idx = rel_pos_arr[:, :seq_len, :seq_len]
423
+ rel_pos = self.rel_pos_embedding(rel_pos_idx)
424
+ # N x L x L x h --> N x h x L x L
425
+ rel_pos = rel_pos.permute([0, 3, 1, 2])
426
+ rel_pos = rel_pos.contiguous()
427
+ rel_pos = rel_pos.reshape(-1, seq_len, seq_len)
428
+ return rel_pos.contiguous()
429
+
430
+ def get_position_attn_bias(self, rel_pos_arr, seq_len, batch_size, device):
431
+ tupe_rel_pos_bias = self.get_tupe_rel_pos_bias(seq_len, device) if self.use_tupe_rel_pos_bias else None
432
+ pos_aware_rel_pos_bias = self.get_pos_aware_rel_pos_bias(rel_pos_arr, seq_len) if self.use_pos_aware_rel_pos_bias else None
433
+
434
+ # This is from TUPE
435
+ # https://github.com/guolinke/TUPE/blob/master/fairseq/modules/transformer_sentence_encoder.py
436
+ # 0 is for other-to-cls 1 is for cls-to-other
437
+ # Assume the input is ordered. If your input token is permuted, you may need to update this accordingly
438
+ weight = self.pos_ln(self.pos.weight[:seq_len + 1, :])
439
+ pos_q = self.pos_q_linear(weight).view(seq_len + 1, self.seq_tr_nhead, -1).transpose(0, 1) * self.pos_scaling
440
+ pos_k = self.pos_k_linear(weight).view(seq_len + 1, self.seq_tr_nhead, -1).transpose(0, 1)
441
+ abs_pos_bias = torch.bmm(pos_q, pos_k.transpose(1, 2))
442
+ # p_0 \dot p_0 is cls to others
443
+ cls_2_other = abs_pos_bias[:, 0, 0]
444
+ # p_1 \dot p_1 is others to cls
445
+ other_2_cls = abs_pos_bias[:, 1, 1]
446
+ # offset
447
+ abs_pos_bias = abs_pos_bias[:, 1:, 1:]
448
+ abs_pos_bias[:, :, 0] = other_2_cls.view(-1, 1)
449
+ abs_pos_bias[:, 0, :] = cls_2_other.view(-1, 1)
450
+
451
+ if tupe_rel_pos_bias is not None:
452
+ abs_pos_bias += tupe_rel_pos_bias
453
+
454
+ abs_pos_bias = abs_pos_bias.unsqueeze(0).expand(batch_size, -1, -1, -1).reshape(-1, seq_len, seq_len)
455
+
456
+ if pos_aware_rel_pos_bias is not None:
457
+ abs_pos_bias += pos_aware_rel_pos_bias
458
+
459
+ return abs_pos_bias
460
+
461
+
462
+ @custom_fwd
463
+ def forward(self, args, rel_pos_arr, tokens_lengths, input_sequence_lengths, pos_tags, stems, afsets, affixes):
464
+ # L = len(tokens_lengths)
465
+ # affixes: (N)
466
+ # stems: (L)
467
+ # pos_tags: (L)
468
+
469
+ device = stems.device
470
+ x_embed = None
471
+ if self.num_pos_m_embeddings > 0:
472
+ xm_pos1 = self.m1_pos_embedding(pos_tags)
473
+ x_embed = torch.unsqueeze(xm_pos1,0)
474
+
475
+ if self.num_pos_m_embeddings > 1:
476
+ xm_pos2 = self.m2_pos_embedding(pos_tags)
477
+ xm_pos2 = torch.unsqueeze(xm_pos2,0)
478
+ x_embed = torch.cat((x_embed, xm_pos2), 0)
479
+
480
+ if self.num_pos_m_embeddings > 2:
481
+ xm_pos3 = self.m3_pos_embedding(pos_tags)
482
+ xm_pos3 = torch.unsqueeze(xm_pos3,0)
483
+ x_embed = torch.cat((x_embed, xm_pos3), 0)
484
+
485
+ if self.num_stem_m_embeddings > 0:
486
+ xm_stem = self.m_stem_embedding(stems)
487
+ xm_stem = torch.unsqueeze(xm_stem,0)
488
+ if x_embed is not None:
489
+ x_embed = torch.cat((x_embed, xm_stem), 0)
490
+ else:
491
+ x_embed = xm_stem
492
+
493
+ if (args.use_afsets) and (afsets is not None):
494
+ xm_afset = self.m_afset_embedding(afsets)
495
+ xm_afset = torch.unsqueeze(xm_afset,0)
496
+ if x_embed is not None:
497
+ x_embed = torch.cat((x_embed, xm_afset), 0)
498
+ else:
499
+ x_embed = xm_afset
500
+ # All above: (1,L,E)
501
+
502
+ #x_embed = torch.cat((xm_pos1, xm_pos2, xm_pos3, xm_stem, xm_afset), 0)
503
+ # x_embed: (4,L,E)
504
+
505
+ m_masks_padded = None
506
+ has_morphemes = False
507
+ if args.use_morpho_encoder:
508
+ afx = affixes.split(tokens_lengths)
509
+ # [[2,4,5], [6,7]]
510
+ afx_padded = pad_sequence(afx, batch_first=False)
511
+ # afx_padded: (M,L), M: max morphological length
512
+ if afx_padded.nelement() > 0:
513
+ has_morphemes = True
514
+ xm_affix = self.m_affix_embedding(afx_padded)
515
+ # xa_embed: (M,L,E)
516
+ if x_embed is not None:
517
+ x_embed = torch.cat((x_embed, xm_affix), 0)
518
+ else:
519
+ x_embed = xm_affix
520
+
521
+ m_masks = [torch.zeros((x+(self.tot_morpho_idx)), dtype=torch.bool, device = device) for x in tokens_lengths]
522
+ m_masks_padded = pad_sequence(m_masks, batch_first=True, padding_value=1) # Shape: (L, M+4)
523
+
524
+ # x_embed: (4+M,L,E)
525
+ morpho_input = None
526
+ if (x_embed is not None) and args.use_morpho_encoder:
527
+ morpho_transformer_output = self.morpho_transformer_encoder(x_embed, src_key_padding_mask=m_masks_padded) # Shape: (M+4, L, E)
528
+ if (self.tot_morpho_idx > 0) and (self.use_affix_bow_m_embedding):
529
+ heads = morpho_transformer_output[:(self.tot_morpho_idx), :, :]
530
+ affixes_bow = torch.sum(morpho_transformer_output[(self.tot_morpho_idx):, :, :], 0, keepdim=True) if has_morphemes else torch.zeros((1, stems.size(0), self.morpho_dim), device = device)
531
+ morpho_input = torch.cat((heads, affixes_bow), 0)
532
+ elif (self.tot_morpho_idx > 0):
533
+ morpho_input = morpho_transformer_output[:(self.tot_morpho_idx), :, :]
534
+ elif (self.use_affix_bow_m_embedding):
535
+ morpho_input = torch.sum(morpho_transformer_output[(self.tot_morpho_idx):, :, :], 0, keepdim=True) if has_morphemes else torch.zeros((1, stems.size(0), self.morpho_dim), device = device)
536
+ elif (self.use_affix_bow_m_embedding):
537
+ # No POS, no STEM, No afset, No affixes, but allows affixes_bow
538
+ morpho_input = torch.zeros((1, stems.size(0), self.morpho_dim), device = device)
539
+
540
+ input_sequences = self.s_stem_embedding(stems) # (L, E')
541
+ if morpho_input is not None:
542
+ # 4 x L x E ==> L x 4 x E
543
+ #i.e. K x L x E ==> L x K x E, K: number of morpho components from the morpho encoder tier
544
+ morpho_input = morpho_input.permute(1, 0, 2)
545
+ L = morpho_input.size(0)
546
+ morpho_input = morpho_input.contiguous().view(L,-1) # (L, 4E), i.e. (L, KE)
547
+ input_sequences = torch.cat((morpho_input, input_sequences), 1)
548
+
549
+ lists = input_sequences.split(input_sequence_lengths, 0) # len(input_sequence_lengths) = N (i.e. Batch Size, e.g. 32)
550
+ tr_padded = pad_sequence(lists, batch_first=False)
551
+
552
+ seq_len = tr_padded.size(0)
553
+ batch_size = tr_padded.size(1)
554
+
555
+ abs_pos_bias = self.get_position_attn_bias(rel_pos_arr, seq_len, batch_size, device)
556
+
557
+ masks = [torch.zeros(x, dtype=torch.bool, device = device) for x in input_sequence_lengths]
558
+ masks_padded = pad_sequence(masks, batch_first=True, padding_value=1) # Shape: N x S
559
+
560
+ transformer_output = self.seq_transformer_encoder(tr_padded, attn_bias = abs_pos_bias, src_key_padding_mask = masks_padded) # Shape: L x N x E, with L = max sequence length
561
+
562
+ return transformer_output
563
+
564
+
565
+ class KinyaBERT(nn.Module):
566
+ def __init__(self, args,
567
+ num_stems, num_afsets, num_pos_tags, num_affixes,
568
+ num_rel_pos_dict_size,
569
+ num_pos_m_embeddings,
570
+ num_stem_m_embeddings,
571
+ use_affix_bow_m_embedding,
572
+ use_pos_aware_rel_pos_bias,
573
+ use_tupe_rel_pos_bias,
574
+ max_seq_len = 512,
575
+ morpho_dim=80, stem_dim=160,
576
+ morpho_tr_nhead=4, morpho_tr_nlayers=4,
577
+ morpho_tr_dim_feedforward=512, morpho_tr_dropout=0.1, morpho_tr_activation='gelu',
578
+ seq_tr_nhead=8, seq_tr_nlayers=8,
579
+ seq_tr_dim_feedforward=2048, seq_tr_dropout=0.1, seq_tr_activation='gelu',
580
+ layernorm_epsilon = 1e-6,
581
+ tupe_rel_pos_bins: int = 32,
582
+ tupe_max_rel_pos: int = 128):
583
+ super(KinyaBERT, self).__init__()
584
+
585
+ self.encoder = KinyaBERT_MorphoEncoder(args, num_stems, num_afsets, num_pos_tags, num_affixes,
586
+ num_rel_pos_dict_size,
587
+ num_pos_m_embeddings,
588
+ num_stem_m_embeddings,
589
+ use_affix_bow_m_embedding,
590
+ use_pos_aware_rel_pos_bias,
591
+ use_tupe_rel_pos_bias,
592
+ max_seq_len = max_seq_len,
593
+ morpho_dim = morpho_dim, stem_dim = stem_dim,
594
+ morpho_tr_nhead = morpho_tr_nhead, morpho_tr_nlayers=morpho_tr_nlayers,
595
+ morpho_tr_dim_feedforward=morpho_tr_dim_feedforward, morpho_tr_dropout=morpho_tr_dropout, morpho_tr_activation=morpho_tr_activation,
596
+ seq_tr_nhead=seq_tr_nhead, seq_tr_nlayers=seq_tr_nlayers,
597
+ seq_tr_dim_feedforward=seq_tr_dim_feedforward, seq_tr_dropout=seq_tr_dropout, seq_tr_activation=seq_tr_activation,
598
+ tupe_rel_pos_bins = tupe_rel_pos_bins,
599
+ tupe_max_rel_pos = tupe_max_rel_pos)
600
+
601
+ self.predictor = MorphoHeadPredictor(args, self.encoder.s_stem_embedding.weight,
602
+ self.encoder.m_afset_embedding.weight if (num_afsets > 0) else None,
603
+ self.encoder.m_affix_embedding.weight if args.predict_affixes else None,
604
+ self.encoder.seq_tr_d_model, seq_tr_dropout, layernorm_epsilon)
605
+
606
+ @custom_fwd
607
+ def forward(self, args, rel_pos_arr, tokens_lengths, input_sequence_lengths, pos_tags, stems, afsets, affixes,
608
+ predicted_tokens_idx,
609
+ predicted_tokens_affixes_idx,
610
+ predicted_stems,
611
+ predicted_afsets,
612
+ predicted_affixes_prob):
613
+
614
+ tr_hidden_state = self.encoder(args, rel_pos_arr, tokens_lengths, input_sequence_lengths, pos_tags, stems, afsets, affixes)
615
+
616
+ return self.predictor(args, tr_hidden_state,
617
+ predicted_tokens_idx,
618
+ predicted_tokens_affixes_idx,
619
+ predicted_stems,
620
+ predicted_afsets,
621
+ predicted_affixes_prob)
622
+
623
+ def predict(self, args, rel_pos_arr, tokens_lengths, input_sequence_lengths, pos_tags, stems, afsets, affixes,
624
+ seq_predicted_token_idx,
625
+ max_predict_affixes, proposed_stem_ids=None):
626
+
627
+ tr_hidden_state = self.encoder(args, rel_pos_arr, tokens_lengths, input_sequence_lengths, pos_tags, stems, afsets, affixes)
628
+
629
+ return self.predictor.predict(args, tr_hidden_state,
630
+ seq_predicted_token_idx,
631
+ max_predict_affixes, proposed_stem_ids=proposed_stem_ids)
632
+
633
+ class KinyaBERTClassifier(nn.Module):
634
+ def __init__(self, args,
635
+ num_stems, num_afsets, num_pos_tags, num_affixes,
636
+ num_rel_pos_dict_size,
637
+ num_classes,
638
+ num_pos_m_embeddings,
639
+ num_stem_m_embeddings,
640
+ use_affix_bow_m_embedding,
641
+ use_pos_aware_rel_pos_bias,
642
+ use_tupe_rel_pos_bias,
643
+ max_seq_len = 512,
644
+ morpho_dim=80, stem_dim=160,
645
+ morpho_tr_nhead=4, morpho_tr_nlayers=4,
646
+ morpho_tr_dim_feedforward=512, morpho_tr_dropout=0.1, morpho_tr_activation='gelu',
647
+ seq_tr_nhead=8, seq_tr_nlayers=8,
648
+ seq_tr_dim_feedforward=2048, seq_tr_dropout=0.1, pooler_dropout=0.0, seq_tr_activation='gelu',
649
+ tupe_rel_pos_bins: int = 32,
650
+ tupe_max_rel_pos: int = 128):
651
+ super(KinyaBERTClassifier, self).__init__()
652
+
653
+ self.encoder = KinyaBERT_MorphoEncoder(args, num_stems, num_afsets, num_pos_tags, num_affixes,
654
+ num_rel_pos_dict_size,
655
+ num_pos_m_embeddings,
656
+ num_stem_m_embeddings,
657
+ use_affix_bow_m_embedding,
658
+ use_pos_aware_rel_pos_bias,
659
+ use_tupe_rel_pos_bias,
660
+ max_seq_len = max_seq_len,
661
+ morpho_dim = morpho_dim, stem_dim = stem_dim,
662
+ morpho_tr_nhead = morpho_tr_nhead, morpho_tr_nlayers=morpho_tr_nlayers,
663
+ morpho_tr_dim_feedforward=morpho_tr_dim_feedforward, morpho_tr_dropout=morpho_tr_dropout, morpho_tr_activation=morpho_tr_activation,
664
+ seq_tr_nhead=seq_tr_nhead, seq_tr_nlayers=seq_tr_nlayers,
665
+ seq_tr_dim_feedforward=seq_tr_dim_feedforward, seq_tr_dropout=seq_tr_dropout, seq_tr_activation=seq_tr_activation,
666
+ tupe_rel_pos_bins = tupe_rel_pos_bins,
667
+ tupe_max_rel_pos = tupe_max_rel_pos)
668
+
669
+ self.cls_head = ClassificationHead(self.encoder.seq_tr_d_model, num_classes * 32, num_classes, pooler_dropout=pooler_dropout)
670
+
671
+ @custom_fwd
672
+ def forward(self, args, rel_pos_arr, tokens_lengths, input_sequence_lengths, pos_tags, stems, afsets, affixes):
673
+ tr_hidden_state = self.encoder(args, rel_pos_arr, tokens_lengths, input_sequence_lengths, pos_tags, stems, afsets, affixes)
674
+ return self.cls_head(tr_hidden_state)
675
+
676
+ class KinyaBERTSequenceTagger(nn.Module):
677
+ def __init__(self, args,
678
+ num_stems, num_afsets, num_pos_tags, num_affixes,
679
+ num_rel_pos_dict_size,
680
+ num_classes,
681
+ num_pos_m_embeddings,
682
+ num_stem_m_embeddings,
683
+ use_affix_bow_m_embedding,
684
+ use_pos_aware_rel_pos_bias,
685
+ use_tupe_rel_pos_bias,
686
+ max_seq_len = 512,
687
+ morpho_dim=80, stem_dim=160,
688
+ morpho_tr_nhead=4, morpho_tr_nlayers=4,
689
+ morpho_tr_dim_feedforward=512, morpho_tr_dropout=0.1, morpho_tr_activation='gelu',
690
+ seq_tr_nhead=8, seq_tr_nlayers=8,
691
+ seq_tr_dim_feedforward=2048, seq_tr_dropout=0.1, pooler_dropout=0.0, seq_tr_activation='gelu',
692
+ tupe_rel_pos_bins: int = 32,
693
+ tupe_max_rel_pos: int = 128):
694
+ super(KinyaBERTSequenceTagger, self).__init__()
695
+
696
+ self.encoder = KinyaBERT_MorphoEncoder(args, num_stems, num_afsets, num_pos_tags, num_affixes,
697
+ num_rel_pos_dict_size,
698
+ num_pos_m_embeddings,
699
+ num_stem_m_embeddings,
700
+ use_affix_bow_m_embedding,
701
+ use_pos_aware_rel_pos_bias,
702
+ use_tupe_rel_pos_bias,
703
+ max_seq_len = max_seq_len,
704
+ morpho_dim = morpho_dim, stem_dim = stem_dim,
705
+ morpho_tr_nhead = morpho_tr_nhead, morpho_tr_nlayers=morpho_tr_nlayers,
706
+ morpho_tr_dim_feedforward=morpho_tr_dim_feedforward, morpho_tr_dropout=morpho_tr_dropout, morpho_tr_activation=morpho_tr_activation,
707
+ seq_tr_nhead=seq_tr_nhead, seq_tr_nlayers=seq_tr_nlayers,
708
+ seq_tr_dim_feedforward=seq_tr_dim_feedforward, seq_tr_dropout=seq_tr_dropout, seq_tr_activation=seq_tr_activation,
709
+ tupe_rel_pos_bins = tupe_rel_pos_bins,
710
+ tupe_max_rel_pos = tupe_max_rel_pos)
711
+
712
+ self.cls_head = TokenClassificationHead(self.encoder.seq_tr_d_model, num_classes * 32, num_classes, pooler_dropout=pooler_dropout)
713
+
714
+ @custom_fwd
715
+ def forward(self, args, rel_pos_arr, tokens_lengths, input_sequence_lengths, pos_tags, stems, afsets, affixes):
716
+ tr_hidden_state = self.encoder(args, rel_pos_arr, tokens_lengths, input_sequence_lengths, pos_tags, stems, afsets, affixes)
717
+ return self.cls_head(tr_hidden_state, input_sequence_lengths)
718
+
719
+ from morpho_data_loaders import KBVocab, AffixSetVocab
720
+
721
+ def kinyabert_base(kb_vocab : KBVocab, affix_set_vocab : AffixSetVocab, rel_pos_dict, device, args, saved_model_file = None) -> KinyaBERT:
722
+ num_pos_tags = len(kb_vocab.pos_tag_vocab) + 1
723
+ num_stems = len(kb_vocab.reduced_stem_vocab) + 1
724
+ num_afsets = (len(affix_set_vocab.affix_set_vocab_idx) + 1) if args.use_afsets else 0
725
+ num_affixes = len(kb_vocab.affix_vocab) + 1
726
+
727
+ num_rel_pos_dict_size = (len(rel_pos_dict)+1) if (rel_pos_dict is not None) else 0
728
+
729
+ activation_fn = 'gelu'
730
+
731
+ kb_model = KinyaBERT(args, num_stems, num_afsets, num_pos_tags, num_affixes,
732
+ num_rel_pos_dict_size,
733
+ args.num_pos_m_embeddings,
734
+ args.num_stem_m_embeddings,
735
+ args.use_affix_bow_m_embedding,
736
+ args.use_pos_aware_rel_pos_bias,
737
+ args.use_tupe_rel_pos_bias,
738
+ max_seq_len = args.max_seq_len,
739
+ morpho_dim = args.morpho_dim, stem_dim = args.stem_dim,
740
+ morpho_tr_nhead = args.morpho_tr_nhead, morpho_tr_nlayers=args.morpho_tr_nlayers,
741
+ morpho_tr_dim_feedforward=args.morpho_tr_dim_feedforward, morpho_tr_dropout=args.morpho_tr_dropout, morpho_tr_activation=activation_fn,
742
+ seq_tr_nhead=args.seq_tr_nhead, seq_tr_nlayers=args.seq_tr_nlayers,
743
+ seq_tr_dim_feedforward=args.seq_tr_dim_feedforward, seq_tr_dropout=args.seq_tr_dropout, seq_tr_activation=activation_fn,
744
+ layernorm_epsilon = args.layernorm_epsilon).to(device)
745
+
746
+ if saved_model_file is not None:
747
+ kb_state_dict = torch.load(saved_model_file, map_location=device)
748
+ kb_model.load_state_dict(kb_state_dict['model_state_dict'])
749
+
750
+ return kb_model
751
+
752
+ def kinyabert_base_classifier(num_classes, kb_vocab : KBVocab, affix_set_vocab : AffixSetVocab, rel_pos_dict, device, args, saved_model_file = None, pooler_dropout=0.0) -> KinyaBERTClassifier:
753
+ num_pos_tags = len(kb_vocab.pos_tag_vocab) + 1
754
+ num_stems = len(kb_vocab.reduced_stem_vocab) + 1
755
+ num_afsets = (len(affix_set_vocab.affix_set_vocab_idx) + 1) if args.use_afsets else 0
756
+ num_affixes = len(kb_vocab.affix_vocab) + 1
757
+
758
+ num_rel_pos_dict_size = (len(rel_pos_dict)+1) if (rel_pos_dict is not None) else 0
759
+
760
+ activation_fn = 'gelu'
761
+
762
+ kb_model = KinyaBERTClassifier(args, num_stems, num_afsets, num_pos_tags, num_affixes,
763
+ num_rel_pos_dict_size,
764
+ num_classes,
765
+ args.num_pos_m_embeddings,
766
+ args.num_stem_m_embeddings,
767
+ args.use_affix_bow_m_embedding,
768
+ args.use_pos_aware_rel_pos_bias,
769
+ args.use_tupe_rel_pos_bias,
770
+ max_seq_len = args.max_seq_len,
771
+ morpho_dim = args.morpho_dim, stem_dim = args.stem_dim,
772
+ morpho_tr_nhead = args.morpho_tr_nhead, morpho_tr_nlayers=args.morpho_tr_nlayers,
773
+ morpho_tr_dim_feedforward=args.morpho_tr_dim_feedforward, morpho_tr_dropout=args.morpho_tr_dropout, morpho_tr_activation=activation_fn,
774
+ seq_tr_nhead=args.seq_tr_nhead, seq_tr_nlayers=args.seq_tr_nlayers,
775
+ seq_tr_dim_feedforward=args.seq_tr_dim_feedforward, seq_tr_dropout=args.seq_tr_dropout, seq_tr_activation=activation_fn,
776
+ pooler_dropout = pooler_dropout).to(device)
777
+
778
+ if saved_model_file is not None:
779
+ kb_state_dict = torch.load(saved_model_file, map_location=device)
780
+ kb_model.load_state_dict(kb_state_dict['model_state_dict'])
781
+
782
+ return kb_model
783
+
784
+ def kinyabert_base_classifier_from_pretrained(num_classes, kb_vocab : KBVocab, affix_set_vocab : AffixSetVocab, rel_pos_dict, device, args, pretrained_model_file, ddp = False, pooler_dropout=0.0)\
785
+ -> KinyaBERTClassifier:
786
+ from torch.nn.parallel import DistributedDataParallel as DDP
787
+ pretrained_model = kinyabert_base(kb_vocab, affix_set_vocab, rel_pos_dict, device, args)
788
+ classifier_model = kinyabert_base_classifier(num_classes, kb_vocab, affix_set_vocab, rel_pos_dict, device, args, pooler_dropout=pooler_dropout)
789
+
790
+ kb_state_dict = torch.load(pretrained_model_file, map_location=device)
791
+ if ddp:
792
+ ddp_model = DDP(pretrained_model)
793
+ ddp_model.load_state_dict(kb_state_dict['model_state_dict'])
794
+ pretrained_model = ddp_model.module
795
+ else:
796
+ pretrained_model.load_state_dict(kb_state_dict['model_state_dict'])
797
+
798
+ classifier_model.encoder.load_state_dict(pretrained_model.encoder.state_dict())
799
+
800
+ return classifier_model
801
+
802
+ def kinyabert_base_sequence_tagger(num_classes, kb_vocab : KBVocab, affix_set_vocab : AffixSetVocab, rel_pos_dict, device, args, saved_model_file = None, pooler_dropout=0.0)\
803
+ -> KinyaBERTSequenceTagger:
804
+ num_pos_tags = len(kb_vocab.pos_tag_vocab) + 1
805
+ num_stems = len(kb_vocab.reduced_stem_vocab) + 1
806
+ num_afsets = (len(affix_set_vocab.affix_set_vocab_idx) + 1) if args.use_afsets else 0
807
+ num_affixes = len(kb_vocab.affix_vocab) + 1
808
+
809
+ num_rel_pos_dict_size = (len(rel_pos_dict)+1) if (rel_pos_dict is not None) else 0
810
+
811
+ activation_fn = 'gelu'
812
+
813
+ kb_model = KinyaBERTSequenceTagger(args, num_stems, num_afsets, num_pos_tags, num_affixes,
814
+ num_rel_pos_dict_size,
815
+ num_classes,
816
+ args.num_pos_m_embeddings,
817
+ args.num_stem_m_embeddings,
818
+ args.use_affix_bow_m_embedding,
819
+ args.use_pos_aware_rel_pos_bias,
820
+ args.use_tupe_rel_pos_bias,
821
+ max_seq_len = args.max_seq_len,
822
+ morpho_dim = args.morpho_dim, stem_dim = args.stem_dim,
823
+ morpho_tr_nhead = args.morpho_tr_nhead, morpho_tr_nlayers=args.morpho_tr_nlayers,
824
+ morpho_tr_dim_feedforward=args.morpho_tr_dim_feedforward, morpho_tr_dropout=args.morpho_tr_dropout, morpho_tr_activation=activation_fn,
825
+ seq_tr_nhead=args.seq_tr_nhead, seq_tr_nlayers=args.seq_tr_nlayers,
826
+ seq_tr_dim_feedforward=args.seq_tr_dim_feedforward, seq_tr_dropout=args.seq_tr_dropout, pooler_dropout=pooler_dropout, seq_tr_activation=activation_fn).to(device)
827
+
828
+ if saved_model_file is not None:
829
+ kb_state_dict = torch.load(saved_model_file, map_location=device)
830
+ kb_model.load_state_dict(kb_state_dict['model_state_dict'])
831
+
832
+ return kb_model
833
+
834
+ def kinyabert_base_tagger_from_pretrained(num_classes, kb_vocab : KBVocab, affix_set_vocab : AffixSetVocab, rel_pos_dict, device, args, pretrained_model_file, ddp = False, pooler_dropout=0.0)\
835
+ -> KinyaBERTSequenceTagger:
836
+ from torch.nn.parallel import DistributedDataParallel as DDP
837
+ pretrained_model = kinyabert_base(kb_vocab, affix_set_vocab, rel_pos_dict, device, args)
838
+ sequence_tagger_model = kinyabert_base_sequence_tagger(num_classes, kb_vocab, affix_set_vocab, rel_pos_dict, device, args, pooler_dropout=pooler_dropout)
839
+
840
+ kb_state_dict = torch.load(pretrained_model_file, map_location=device)
841
+ if ddp:
842
+ ddp_model = DDP(pretrained_model)
843
+ ddp_model.load_state_dict(kb_state_dict['model_state_dict'])
844
+ pretrained_model = ddp_model.module
845
+ else:
846
+ pretrained_model.load_state_dict(kb_state_dict['model_state_dict'])
847
+
848
+ sequence_tagger_model.encoder.load_state_dict(pretrained_model.encoder.state_dict())
849
+
850
+ return sequence_tagger_model
code/morpho_stub.py ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # TabuLM β€” morphological analyzer stub
2
+ # Drop-in replacement for parse_raw_text_lines that works without libkinlp.so.
3
+ # Uses regex heuristics to assign word types and falls back to BPE for all stems.
4
+
5
+ import re
6
+ from typing import List
7
+
8
+ import youtokentome as yttm
9
+
10
+ _NUMERIC_RE = re.compile(
11
+ r'^[\d,.\s]+(%|Frw|RWF|km|kg|ha|m|mΒ²|L|MW|USD|acres|ha)?$', re.IGNORECASE
12
+ )
13
+ _DATE_RE = re.compile(
14
+ r'^\d{4}(-\d{2}(-\d{2})?)?$|^\d{1,2}/\d{1,2}/\d{2,4}$'
15
+ )
16
+ _SPECIAL_TOKEN_RE = re.compile(r'^\[.*\]$')
17
+ _UPPER_START_RE = re.compile(r'^[A-Z][a-z]')
18
+
19
+
20
+ def _classify_word_type(word: str) -> str:
21
+ """Return the KBVocab word-type prefix for a given surface form."""
22
+ if _SPECIAL_TOKEN_RE.match(word):
23
+ return 'T'
24
+ if _DATE_RE.match(word) or _NUMERIC_RE.match(word):
25
+ return 'NU'
26
+ if _UPPER_START_RE.match(word):
27
+ return 'NP'
28
+ return 'T'
29
+
30
+
31
+ def parse_text_stub(text: str, kb_vocab, bpe: yttm.BPE) -> List:
32
+ """
33
+ Stub replacement for parse_raw_text_lines from morpho_data_loaders.
34
+ Takes a space-separated text string and returns a list of ParsedToken objects.
35
+ No libkinlp.so / CFFI required β€” all stems resolved via BPE fallback.
36
+ """
37
+ from morpho_data_loaders import ParsedToken
38
+
39
+ unk_pos = kb_vocab.pos_tag_vocab.get('<UNK>', 1)
40
+ unk_stem = kb_vocab._stem_vocab.get('<UNK>', 1)
41
+
42
+ parsed_tokens: List[ParsedToken] = []
43
+
44
+ for word in text.split():
45
+ if not word:
46
+ continue
47
+
48
+ word_type = _classify_word_type(word)
49
+
50
+ if word_type == 'NU':
51
+ stem_key = f'NU:{word}'
52
+ si = kb_vocab._stem_vocab.get(stem_key, unk_stem)
53
+ ptoken = ParsedToken(
54
+ word, decode_prob=1.0, tf_idf=0.001,
55
+ pos_tag_id=unk_pos, stem_ids=[si]
56
+ )
57
+ parsed_tokens.append(ptoken)
58
+ continue
59
+
60
+ try:
61
+ subwords = bpe.encode(word, output_type=yttm.OutputType.SUBWORD)
62
+ except Exception:
63
+ subwords = []
64
+
65
+ if not subwords:
66
+ stem_key = f'{word_type}:{word}'
67
+ si = kb_vocab._stem_vocab.get(stem_key, unk_stem)
68
+ subwords_sids = [si]
69
+ else:
70
+ subwords_sids = []
71
+ for sw in subwords:
72
+ stem_key = f'{word_type}:{sw}'
73
+ si = kb_vocab._stem_vocab.get(stem_key, unk_stem)
74
+ subwords_sids.append(si)
75
+
76
+ ptoken = ParsedToken(
77
+ word, decode_prob=1.0, tf_idf=0.001,
78
+ pos_tag_id=unk_pos, stem_ids=subwords_sids
79
+ )
80
+ parsed_tokens.append(ptoken)
81
+
82
+ return parsed_tokens
code/morpho_transformer.py ADDED
@@ -0,0 +1,642 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Antoine Nzeyimana.
2
+ #
3
+ # This source code is licensed under the MIT license found in the
4
+ # LICENSE file in the root directory of this source tree.
5
+
6
+ # MIT License
7
+ #
8
+ # Copyright (c) Facebook, Inc. and its affiliates.
9
+ #
10
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ # of this software and associated documentation files (the "Software"), to deal
12
+ # in the Software without restriction, including without limitation the rights
13
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ # copies of the Software, and to permit persons to whom the Software is
15
+ # furnished to do so, subject to the following conditions:
16
+ #
17
+ # The above copyright notice and this permission notice shall be included in all
18
+ # copies or substantial portions of the Software.
19
+ #
20
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ # SOFTWARE.
27
+
28
+ r"""Functional interface"""
29
+ from typing import Callable, List, Optional, Tuple
30
+ import warnings
31
+
32
+ import torch
33
+
34
+ from torch import Tensor
35
+
36
+ from torch.nn.init import xavier_uniform_
37
+ from torch.nn.init import constant_
38
+ from torch.nn.init import xavier_normal_
39
+ from torch.nn.parameter import Parameter
40
+ from torch.nn import Module, Linear, LayerNorm, Dropout, ModuleList
41
+ import copy
42
+ from typing import Optional, Any
43
+
44
+ import torch.nn.functional as F
45
+ from torch.nn.functional import linear, pad, softmax, dropout
46
+
47
+ def multi_head_attention_forward(
48
+ query: Tensor,
49
+ key: Tensor,
50
+ value: Tensor,
51
+ embed_dim_to_check: int,
52
+ num_heads: int,
53
+ in_proj_weight: Tensor,
54
+ in_proj_bias: Optional[Tensor],
55
+ bias_k: Optional[Tensor],
56
+ bias_v: Optional[Tensor],
57
+ add_zero_attn: bool,
58
+ dropout_p: float,
59
+ out_proj_weight: Tensor,
60
+ out_proj_bias: Optional[Tensor],
61
+ training: bool = True,
62
+ key_padding_mask: Optional[Tensor] = None,
63
+ need_weights: bool = True,
64
+ attn_mask: Optional[Tensor] = None,
65
+ use_separate_proj_weight: bool = False,
66
+ q_proj_weight: Optional[Tensor] = None,
67
+ k_proj_weight: Optional[Tensor] = None,
68
+ v_proj_weight: Optional[Tensor] = None,
69
+ static_k: Optional[Tensor] = None,
70
+ static_v: Optional[Tensor] = None,
71
+ attn_bias: Optional[Tensor] = None,
72
+ scale_factor = 1,
73
+ ) -> Tuple[Tensor, Optional[Tensor]]:
74
+ r"""
75
+ Args:
76
+ query, key, value: map a query and a set of key-value pairs to an output.
77
+ See "Attention Is All You Need" for more details.
78
+ embed_dim_to_check: total dimension of the model.
79
+ num_heads: parallel attention heads.
80
+ in_proj_weight, in_proj_bias: input projection weight and bias.
81
+ bias_k, bias_v: bias of the key and value sequences to be added at dim=0.
82
+ add_zero_attn: add a new batch of zeros to the key and
83
+ value sequences at dim=1.
84
+ dropout_p: probability of an element to be zeroed.
85
+ out_proj_weight, out_proj_bias: the output projection weight and bias.
86
+ training: apply dropout if is ``True``.
87
+ key_padding_mask: if provided, specified padding elements in the key will
88
+ be ignored by the attention. This is an binary mask. When the value is True,
89
+ the corresponding value on the attention layer will be filled with -inf.
90
+ need_weights: output attn_output_weights.
91
+ attn_mask: 2D or 3D mask that prevents attention to certain positions. A 2D mask will be broadcasted for all
92
+ the batches while a 3D mask allows to specify a different mask for the entries of each batch.
93
+ use_separate_proj_weight: the function accept the proj. weights for query, key,
94
+ and value in different forms. If false, in_proj_weight will be used, which is
95
+ a combination of q_proj_weight, k_proj_weight, v_proj_weight.
96
+ q_proj_weight, k_proj_weight, v_proj_weight, in_proj_bias: input projection weight and bias.
97
+ static_k, static_v: static key and value used for attention operators.
98
+
99
+
100
+ Shape:
101
+ Inputs:
102
+ - query: :math:`(L, N, E)` where L is the target sequence length, N is the batch size, E is
103
+ the embedding dimension.
104
+ - key: :math:`(S, N, E)`, where S is the source sequence length, N is the batch size, E is
105
+ the embedding dimension.
106
+ - value: :math:`(S, N, E)` where S is the source sequence length, N is the batch size, E is
107
+ the embedding dimension.
108
+ - key_padding_mask: :math:`(N, S)` where N is the batch size, S is the source sequence length.
109
+ If a ByteTensor is provided, the non-zero positions will be ignored while the zero positions
110
+ will be unchanged. If a BoolTensor is provided, the positions with the
111
+ value of ``True`` will be ignored while the position with the value of ``False`` will be unchanged.
112
+ - attn_mask: 2D mask :math:`(L, S)` where L is the target sequence length, S is the source sequence length.
113
+ 3D mask :math:`(N*num_heads, L, S)` where N is the batch size, L is the target sequence length,
114
+ S is the source sequence length. attn_mask ensures that position i is allowed to attend the unmasked
115
+ positions. If a ByteTensor is provided, the non-zero positions are not allowed to attend
116
+ while the zero positions will be unchanged. If a BoolTensor is provided, positions with ``True``
117
+ are not allowed to attend while ``False`` values will be unchanged. If a FloatTensor
118
+ is provided, it will be added to the attention weight.
119
+ - static_k: :math:`(N*num_heads, S, E/num_heads)`, where S is the source sequence length,
120
+ N is the batch size, E is the embedding dimension. E/num_heads is the head dimension.
121
+ - static_v: :math:`(N*num_heads, S, E/num_heads)`, where S is the source sequence length,
122
+ N is the batch size, E is the embedding dimension. E/num_heads is the head dimension.
123
+
124
+ Outputs:
125
+ - attn_output: :math:`(L, N, E)` where L is the target sequence length, N is the batch size,
126
+ E is the embedding dimension.
127
+ - attn_output_weights: :math:`(N, L, S)` where N is the batch size,
128
+ L is the target sequence length, S is the source sequence length.
129
+ """
130
+ # tens_ops = (query, key, value, in_proj_weight, in_proj_bias, bias_k, bias_v, out_proj_weight, out_proj_bias)
131
+ # if torch.overrides.has_torch_function(tens_ops):
132
+ # return torch.overrides.handle_torch_function(
133
+ # multi_head_attention_forward,
134
+ # tens_ops,
135
+ # query,
136
+ # key,
137
+ # value,
138
+ # embed_dim_to_check,
139
+ # num_heads,
140
+ # in_proj_weight,
141
+ # in_proj_bias,
142
+ # bias_k,
143
+ # bias_v,
144
+ # add_zero_attn,
145
+ # dropout_p,
146
+ # out_proj_weight,
147
+ # out_proj_bias,
148
+ # training=training,
149
+ # key_padding_mask=key_padding_mask,
150
+ # need_weights=need_weights,
151
+ # attn_mask=attn_mask,
152
+ # use_separate_proj_weight=use_separate_proj_weight,
153
+ # q_proj_weight=q_proj_weight,
154
+ # k_proj_weight=k_proj_weight,
155
+ # v_proj_weight=v_proj_weight,
156
+ # static_k=static_k,
157
+ # static_v=static_v,
158
+ # attn_bias=attn_bias,
159
+ # )
160
+ tgt_len, bsz, embed_dim = query.size()
161
+ assert embed_dim == embed_dim_to_check
162
+ # allow MHA to have different sizes for the feature dimension
163
+ assert key.size(0) == value.size(0) and key.size(1) == value.size(1)
164
+
165
+ if isinstance(embed_dim, torch.Tensor):
166
+ # embed_dim can be a tensor when JIT tracing
167
+ head_dim = embed_dim.div(num_heads, rounding_mode='trunc')
168
+ else:
169
+ head_dim = embed_dim // num_heads
170
+ assert head_dim * num_heads == embed_dim, "embed_dim must be divisible by num_heads"
171
+ scaling = float(head_dim * scale_factor) ** -0.5
172
+
173
+ if not use_separate_proj_weight:
174
+ if (query is key or torch.equal(query, key)) and (key is value or torch.equal(key, value)):
175
+ # self-attention
176
+ q, k, v = linear(query, in_proj_weight, in_proj_bias).chunk(3, dim=-1)
177
+
178
+ elif key is value or torch.equal(key, value):
179
+ # encoder-decoder attention
180
+ # This is inline in_proj function with in_proj_weight and in_proj_bias
181
+ _b = in_proj_bias
182
+ _start = 0
183
+ _end = embed_dim
184
+ _w = in_proj_weight[_start:_end, :]
185
+ if _b is not None:
186
+ _b = _b[_start:_end]
187
+ q = linear(query, _w, _b)
188
+
189
+ if key is None:
190
+ assert value is None
191
+ k = None
192
+ v = None
193
+ else:
194
+
195
+ # This is inline in_proj function with in_proj_weight and in_proj_bias
196
+ _b = in_proj_bias
197
+ _start = embed_dim
198
+ _end = None
199
+ _w = in_proj_weight[_start:, :]
200
+ if _b is not None:
201
+ _b = _b[_start:]
202
+ k, v = linear(key, _w, _b).chunk(2, dim=-1)
203
+
204
+ else:
205
+ # This is inline in_proj function with in_proj_weight and in_proj_bias
206
+ _b = in_proj_bias
207
+ _start = 0
208
+ _end = embed_dim
209
+ _w = in_proj_weight[_start:_end, :]
210
+ if _b is not None:
211
+ _b = _b[_start:_end]
212
+ q = linear(query, _w, _b)
213
+
214
+ # This is inline in_proj function with in_proj_weight and in_proj_bias
215
+ _b = in_proj_bias
216
+ _start = embed_dim
217
+ _end = embed_dim * 2
218
+ _w = in_proj_weight[_start:_end, :]
219
+ if _b is not None:
220
+ _b = _b[_start:_end]
221
+ k = linear(key, _w, _b)
222
+
223
+ # This is inline in_proj function with in_proj_weight and in_proj_bias
224
+ _b = in_proj_bias
225
+ _start = embed_dim * 2
226
+ _end = None
227
+ _w = in_proj_weight[_start:, :]
228
+ if _b is not None:
229
+ _b = _b[_start:]
230
+ v = linear(value, _w, _b)
231
+ else:
232
+ q_proj_weight_non_opt = torch.jit._unwrap_optional(q_proj_weight)
233
+ len1, len2 = q_proj_weight_non_opt.size()
234
+ assert len1 == embed_dim and len2 == query.size(-1)
235
+
236
+ k_proj_weight_non_opt = torch.jit._unwrap_optional(k_proj_weight)
237
+ len1, len2 = k_proj_weight_non_opt.size()
238
+ assert len1 == embed_dim and len2 == key.size(-1)
239
+
240
+ v_proj_weight_non_opt = torch.jit._unwrap_optional(v_proj_weight)
241
+ len1, len2 = v_proj_weight_non_opt.size()
242
+ assert len1 == embed_dim and len2 == value.size(-1)
243
+
244
+ if in_proj_bias is not None:
245
+ q = linear(query, q_proj_weight_non_opt, in_proj_bias[0:embed_dim])
246
+ k = linear(key, k_proj_weight_non_opt, in_proj_bias[embed_dim : (embed_dim * 2)])
247
+ v = linear(value, v_proj_weight_non_opt, in_proj_bias[(embed_dim * 2) :])
248
+ else:
249
+ q = linear(query, q_proj_weight_non_opt, in_proj_bias)
250
+ k = linear(key, k_proj_weight_non_opt, in_proj_bias)
251
+ v = linear(value, v_proj_weight_non_opt, in_proj_bias)
252
+ q = q * scaling
253
+
254
+ if attn_mask is not None:
255
+ assert (
256
+ attn_mask.dtype == torch.float32
257
+ or attn_mask.dtype == torch.float64
258
+ or attn_mask.dtype == torch.float16
259
+ or attn_mask.dtype == torch.uint8
260
+ or attn_mask.dtype == torch.bool
261
+ ), "Only float, byte, and bool types are supported for attn_mask, not {}".format(attn_mask.dtype)
262
+ if attn_mask.dtype == torch.uint8:
263
+ warnings.warn("Byte tensor for attn_mask in nn.MultiheadAttention is deprecated. Use bool tensor instead.")
264
+ attn_mask = attn_mask.to(torch.bool)
265
+
266
+ if attn_mask.dim() == 2:
267
+ attn_mask = attn_mask.unsqueeze(0)
268
+ if list(attn_mask.size()) != [1, query.size(0), key.size(0)]:
269
+ raise RuntimeError("The size of the 2D attn_mask is not correct.")
270
+ elif attn_mask.dim() == 3:
271
+ if list(attn_mask.size()) != [bsz * num_heads, query.size(0), key.size(0)]:
272
+ raise RuntimeError("The size of the 3D attn_mask is not correct.")
273
+ else:
274
+ raise RuntimeError("attn_mask's dimension {} is not supported".format(attn_mask.dim()))
275
+ # attn_mask's dim is 3 now.
276
+
277
+ # convert ByteTensor key_padding_mask to bool
278
+ if key_padding_mask is not None and key_padding_mask.dtype == torch.uint8:
279
+ warnings.warn(
280
+ "Byte tensor for key_padding_mask in nn.MultiheadAttention is deprecated. Use bool tensor instead."
281
+ )
282
+ key_padding_mask = key_padding_mask.to(torch.bool)
283
+
284
+ if bias_k is not None and bias_v is not None:
285
+ if static_k is None and static_v is None:
286
+ k = torch.cat([k, bias_k.repeat(1, bsz, 1)])
287
+ v = torch.cat([v, bias_v.repeat(1, bsz, 1)])
288
+ if attn_mask is not None:
289
+ attn_mask = pad(attn_mask, (0, 1))
290
+ if key_padding_mask is not None:
291
+ key_padding_mask = pad(key_padding_mask, (0, 1))
292
+ else:
293
+ assert static_k is None, "bias cannot be added to static key."
294
+ assert static_v is None, "bias cannot be added to static value."
295
+ else:
296
+ assert bias_k is None
297
+ assert bias_v is None
298
+
299
+ q = q.contiguous().view(tgt_len, bsz * num_heads, head_dim).transpose(0, 1)
300
+ if k is not None:
301
+ k = k.contiguous().view(-1, bsz * num_heads, head_dim).transpose(0, 1)
302
+ if v is not None:
303
+ v = v.contiguous().view(-1, bsz * num_heads, head_dim).transpose(0, 1)
304
+
305
+ if static_k is not None:
306
+ assert static_k.size(0) == bsz * num_heads
307
+ assert static_k.size(2) == head_dim
308
+ k = static_k
309
+
310
+ if static_v is not None:
311
+ assert static_v.size(0) == bsz * num_heads
312
+ assert static_v.size(2) == head_dim
313
+ v = static_v
314
+
315
+ src_len = k.size(1)
316
+
317
+ if key_padding_mask is not None:
318
+ assert key_padding_mask.size(0) == bsz
319
+ assert key_padding_mask.size(1) == src_len
320
+
321
+ if add_zero_attn:
322
+ src_len += 1
323
+ k = torch.cat([k, torch.zeros((k.size(0), 1) + k.size()[2:], dtype=k.dtype, device=k.device)], dim=1)
324
+ v = torch.cat([v, torch.zeros((v.size(0), 1) + v.size()[2:], dtype=v.dtype, device=v.device)], dim=1)
325
+ if attn_mask is not None:
326
+ attn_mask = pad(attn_mask, (0, 1))
327
+ if key_padding_mask is not None:
328
+ key_padding_mask = pad(key_padding_mask, (0, 1))
329
+
330
+ attn_output_weights = torch.bmm(q, k.transpose(1, 2))
331
+ assert list(attn_output_weights.size()) == [bsz * num_heads, tgt_len, src_len]
332
+
333
+ if attn_bias is not None:
334
+ assert list(attn_bias.size()) == [bsz * num_heads, tgt_len, src_len]
335
+ attn_output_weights += attn_bias
336
+
337
+ if attn_mask is not None:
338
+ if attn_mask.dtype == torch.bool:
339
+ attn_output_weights.masked_fill_(attn_mask, float("-inf"))
340
+ else:
341
+ attn_output_weights += attn_mask
342
+
343
+ if key_padding_mask is not None:
344
+ attn_output_weights = attn_output_weights.view(bsz, num_heads, tgt_len, src_len)
345
+ attn_output_weights = attn_output_weights.masked_fill(
346
+ key_padding_mask.unsqueeze(1).unsqueeze(2),
347
+ float("-inf"),
348
+ )
349
+ attn_output_weights = attn_output_weights.view(bsz * num_heads, tgt_len, src_len)
350
+
351
+ attn_output_weights = softmax(attn_output_weights, dim=-1)
352
+ attn_output_weights = dropout(attn_output_weights, p=dropout_p, training=training)
353
+
354
+ attn_output = torch.bmm(attn_output_weights, v)
355
+ assert list(attn_output.size()) == [bsz * num_heads, tgt_len, head_dim]
356
+ attn_output = attn_output.transpose(0, 1).contiguous().view(tgt_len, bsz, embed_dim)
357
+ attn_output = linear(attn_output, out_proj_weight, out_proj_bias)
358
+
359
+ if need_weights:
360
+ # average attention weights over heads
361
+ attn_output_weights = attn_output_weights.view(bsz, num_heads, tgt_len, src_len)
362
+ return attn_output, attn_output_weights.sum(dim=1) / num_heads
363
+ else:
364
+ return attn_output, None
365
+
366
+
367
+ class MultiheadAttention(Module):
368
+ r"""Allows the model to jointly attend to information
369
+ from different representation subspaces.
370
+ See `Attention Is All You Need <https://arxiv.org/abs/1706.03762>`_
371
+
372
+ .. math::
373
+ \text{MultiHead}(Q, K, V) = \text{Concat}(head_1,\dots,head_h)W^O
374
+
375
+ where :math:`head_i = \text{Attention}(QW_i^Q, KW_i^K, VW_i^V)`.
376
+
377
+ Args:
378
+ embed_dim: total dimension of the model.
379
+ num_heads: parallel attention heads.
380
+ dropout: a Dropout layer on attn_output_weights. Default: 0.0.
381
+ bias: add bias as module parameter. Default: True.
382
+ add_bias_kv: add bias to the key and value sequences at dim=0.
383
+ add_zero_attn: add a new batch of zeros to the key and
384
+ value sequences at dim=1.
385
+ kdim: total number of features in key. Default: None.
386
+ vdim: total number of features in value. Default: None.
387
+
388
+ Note that if :attr:`kdim` and :attr:`vdim` are None, they will be set
389
+ to :attr:`embed_dim` such that query, key, and value have the same
390
+ number of features.
391
+
392
+ Examples::
393
+
394
+ >>> multihead_attn = nn.MultiheadAttention(embed_dim, num_heads)
395
+ >>> attn_output, attn_output_weights = multihead_attn(query, key, value)
396
+ """
397
+ bias_k: Optional[torch.Tensor]
398
+ bias_v: Optional[torch.Tensor]
399
+
400
+ def __init__(self, embed_dim, num_heads, dropout=0., bias=True, add_bias_kv=False, scale_factor=1, add_zero_attn=False, kdim=None, vdim=None):
401
+ super(MultiheadAttention, self).__init__()
402
+ self.embed_dim = embed_dim
403
+ self.kdim = kdim if kdim is not None else embed_dim
404
+ self.vdim = vdim if vdim is not None else embed_dim
405
+ self._qkv_same_embed_dim = self.kdim == embed_dim and self.vdim == embed_dim
406
+ self.scale_factor = scale_factor
407
+
408
+ self.num_heads = num_heads
409
+ self.dropout = dropout
410
+ self.head_dim = embed_dim // num_heads
411
+ assert self.head_dim * num_heads == self.embed_dim, "embed_dim must be divisible by num_heads"
412
+
413
+ if self._qkv_same_embed_dim is False:
414
+ self.q_proj_weight = Parameter(torch.empty(embed_dim, embed_dim))
415
+ self.k_proj_weight = Parameter(torch.empty(embed_dim, self.kdim))
416
+ self.v_proj_weight = Parameter(torch.empty(embed_dim, self.vdim))
417
+ self.register_parameter('in_proj_weight', None)
418
+ else:
419
+ self.in_proj_weight = Parameter(torch.empty(3 * embed_dim, embed_dim))
420
+ self.register_parameter('q_proj_weight', None)
421
+ self.register_parameter('k_proj_weight', None)
422
+ self.register_parameter('v_proj_weight', None)
423
+
424
+ if bias:
425
+ self.in_proj_bias = Parameter(torch.empty(3 * embed_dim))
426
+ else:
427
+ self.register_parameter('in_proj_bias', None)
428
+ self.out_proj = Linear(embed_dim, embed_dim, bias=bias)
429
+
430
+ if add_bias_kv:
431
+ self.bias_k = Parameter(torch.empty(1, 1, embed_dim))
432
+ self.bias_v = Parameter(torch.empty(1, 1, embed_dim))
433
+ else:
434
+ self.bias_k = self.bias_v = None
435
+
436
+ self.add_zero_attn = add_zero_attn
437
+
438
+ self._reset_parameters()
439
+
440
+ def _reset_parameters(self):
441
+ if self._qkv_same_embed_dim:
442
+ xavier_uniform_(self.in_proj_weight)
443
+ else:
444
+ xavier_uniform_(self.q_proj_weight)
445
+ xavier_uniform_(self.k_proj_weight)
446
+ xavier_uniform_(self.v_proj_weight)
447
+
448
+ if self.in_proj_bias is not None:
449
+ constant_(self.in_proj_bias, 0.)
450
+ constant_(self.out_proj.bias, 0.)
451
+ if self.bias_k is not None:
452
+ xavier_normal_(self.bias_k)
453
+ if self.bias_v is not None:
454
+ xavier_normal_(self.bias_v)
455
+
456
+ def __setstate__(self, state):
457
+ # Support loading old MultiheadAttention checkpoints generated by v1.1.0
458
+ if '_qkv_same_embed_dim' not in state:
459
+ state['_qkv_same_embed_dim'] = True
460
+
461
+ super(MultiheadAttention, self).__setstate__(state)
462
+
463
+ def forward(self, query: Tensor, key: Tensor, value: Tensor, key_padding_mask: Optional[Tensor] = None,
464
+ need_weights: bool = True, attn_mask: Optional[Tensor] = None, attn_bias: Optional[Tensor] = None) -> Tuple[Tensor, Optional[Tensor]]:
465
+ r"""
466
+ Args:
467
+ query, key, value: map a query and a set of key-value pairs to an output.
468
+ See "Attention Is All You Need" for more details.
469
+ key_padding_mask: if provided, specified padding elements in the key will
470
+ be ignored by the attention. When given a binary mask and a value is True,
471
+ the corresponding value on the attention layer will be ignored. When given
472
+ a byte mask and a value is non-zero, the corresponding value on the attention
473
+ layer will be ignored
474
+ need_weights: output attn_output_weights.
475
+ attn_mask: 2D or 3D mask that prevents attention to certain positions. A 2D mask will be broadcasted for all
476
+ the batches while a 3D mask allows to specify a different mask for the entries of each batch.
477
+
478
+ Shapes for inputs:
479
+ - query: :math:`(L, N, E)` where L is the target sequence length, N is the batch size, E is
480
+ the embedding dimension.
481
+ - key: :math:`(S, N, E)`, where S is the source sequence length, N is the batch size, E is
482
+ the embedding dimension.
483
+ - value: :math:`(S, N, E)` where S is the source sequence length, N is the batch size, E is
484
+ the embedding dimension.
485
+ - key_padding_mask: :math:`(N, S)` where N is the batch size, S is the source sequence length.
486
+ If a ByteTensor is provided, the non-zero positions will be ignored while the position
487
+ with the zero positions will be unchanged. If a BoolTensor is provided, the positions with the
488
+ value of ``True`` will be ignored while the position with the value of ``False`` will be unchanged.
489
+ - attn_mask: if a 2D mask: :math:`(L, S)` where L is the target sequence length, S is the
490
+ source sequence length.
491
+
492
+ If a 3D mask: :math:`(N\cdot\text{num\_heads}, L, S)` where N is the batch size, L is the target sequence
493
+ length, S is the source sequence length. ``attn_mask`` ensure that position i is allowed to attend
494
+ the unmasked positions. If a ByteTensor is provided, the non-zero positions are not allowed to attend
495
+ while the zero positions will be unchanged. If a BoolTensor is provided, positions with ``True``
496
+ is not allowed to attend while ``False`` values will be unchanged. If a FloatTensor
497
+ is provided, it will be added to the attention weight.
498
+
499
+ Shapes for outputs:
500
+ - attn_output: :math:`(L, N, E)` where L is the target sequence length, N is the batch size,
501
+ E is the embedding dimension.
502
+ - attn_output_weights: :math:`(N, L, S)` where N is the batch size,
503
+ L is the target sequence length, S is the source sequence length.
504
+ """
505
+ if not self._qkv_same_embed_dim:
506
+ return multi_head_attention_forward(
507
+ query, key, value, self.embed_dim, self.num_heads,
508
+ self.in_proj_weight, self.in_proj_bias,
509
+ self.bias_k, self.bias_v, self.add_zero_attn,
510
+ self.dropout, self.out_proj.weight, self.out_proj.bias,
511
+ training=self.training,
512
+ key_padding_mask=key_padding_mask, need_weights=need_weights,
513
+ attn_mask=attn_mask, attn_bias=attn_bias,
514
+ use_separate_proj_weight=True,
515
+ q_proj_weight=self.q_proj_weight, k_proj_weight=self.k_proj_weight,
516
+ v_proj_weight=self.v_proj_weight)
517
+ else:
518
+ return multi_head_attention_forward(
519
+ query, key, value, self.embed_dim, self.num_heads,
520
+ self.in_proj_weight, self.in_proj_bias,
521
+ self.bias_k, self.bias_v, self.add_zero_attn,
522
+ self.dropout, self.out_proj.weight, self.out_proj.bias,
523
+ training=self.training,
524
+ key_padding_mask=key_padding_mask, need_weights=need_weights,
525
+ attn_mask=attn_mask, attn_bias=attn_bias)
526
+
527
+ class TransformerEncoderLayer(Module):
528
+ r"""TransformerEncoderLayer is made up of self-attn and feedforward network.
529
+ This standard encoder layer is based on the paper "Attention Is All You Need".
530
+ Ashish Vaswani, Noam Shazeer, Niki Parmar, Jakob Uszkoreit, Llion Jones, Aidan N Gomez,
531
+ Lukasz Kaiser, and Illia Polosukhin. 2017. Attention is all you need. In Advances in
532
+ Neural Information Processing Systems, pages 6000-6010. Users may modify or implement
533
+ in a different way during application.
534
+
535
+ Args:
536
+ d_model: the number of expected features in the input (required).
537
+ nhead: the number of heads in the multiheadattention models (required).
538
+ dim_feedforward: the dimension of the feedforward network model (default=2048).
539
+ dropout: the dropout value (default=0.1).
540
+ activation: the activation function of intermediate layer, relu or gelu (default=relu).
541
+
542
+ Examples::
543
+ >>> encoder_layer = TransformerEncoderLayer(d_model=512, nhead=8)
544
+ >>> src = torch.rand(10, 32, 512)
545
+ >>> out = encoder_layer(src)
546
+ """
547
+
548
+ def __init__(self, d_model, nhead, attn_scale_factor=1, dim_feedforward=2048, dropout=0.1, activation="relu"):
549
+ super(TransformerEncoderLayer, self).__init__()
550
+ self.self_attn = MultiheadAttention(d_model, nhead, scale_factor=attn_scale_factor, dropout=dropout)
551
+ # Implementation of Feedforward model
552
+ self.linear1 = Linear(d_model, dim_feedforward)
553
+ self.dropout = Dropout(dropout)
554
+ self.linear2 = Linear(dim_feedforward, d_model)
555
+
556
+ self.norm1 = LayerNorm(d_model)
557
+ self.norm2 = LayerNorm(d_model)
558
+ self.dropout1 = Dropout(dropout)
559
+ self.dropout2 = Dropout(dropout)
560
+
561
+ self.activation = _get_activation_fn(activation)
562
+
563
+ def __setstate__(self, state):
564
+ if 'activation' not in state:
565
+ state['activation'] = F.relu
566
+ super(TransformerEncoderLayer, self).__setstate__(state)
567
+
568
+ def forward(self, src: Tensor, src_mask: Optional[Tensor] = None, attn_bias: Optional[Tensor] = None, src_key_padding_mask: Optional[Tensor] = None) -> Tensor:
569
+ r"""Pass the input through the encoder layer.
570
+
571
+ Args:
572
+ src: the sequence to the encoder layer (required).
573
+ src_mask: the mask for the src sequence (optional).
574
+ src_key_padding_mask: the mask for the src keys per batch (optional).
575
+
576
+ Shape:
577
+ see the docs in Transformer class.
578
+ """
579
+ src2 = self.self_attn(src, src, src, attn_mask=src_mask, attn_bias=attn_bias,
580
+ key_padding_mask=src_key_padding_mask)[0]
581
+ src = src + self.dropout1(src2)
582
+ src = self.norm1(src)
583
+ src2 = self.linear2(self.dropout(self.activation(self.linear1(src))))
584
+ src = src + self.dropout2(src2)
585
+ src = self.norm2(src)
586
+ return src
587
+
588
+ def _get_clones(module, N):
589
+ return ModuleList([copy.deepcopy(module) for i in range(N)])
590
+
591
+
592
+ def _get_activation_fn(activation):
593
+ if activation == "relu":
594
+ return F.relu
595
+ elif activation == "gelu":
596
+ return F.gelu
597
+
598
+ raise RuntimeError("activation should be relu/gelu, not {}".format(activation))
599
+
600
+
601
+ class TransformerEncoder(Module):
602
+ r"""TransformerEncoder is a stack of N encoder layers
603
+
604
+ Args:
605
+ encoder_layer: an instance of the TransformerEncoderLayer() class (required).
606
+ num_layers: the number of sub-encoder-layers in the encoder (required).
607
+ norm: the layer normalization component (optional).
608
+
609
+ Examples::
610
+ >>> encoder_layer = TransformerEncoderLayer(d_model=512, nhead=8)
611
+ >>> transformer_encoder = TransformerEncoder(encoder_layer, num_layers=6)
612
+ >>> src = torch.rand(10, 32, 512)
613
+ >>> out = transformer_encoder(src)
614
+ """
615
+ __constants__ = ['norm']
616
+
617
+ def __init__(self, encoder_layer, num_layers, norm=None):
618
+ super(TransformerEncoder, self).__init__()
619
+ self.layers = _get_clones(encoder_layer, num_layers)
620
+ self.num_layers = num_layers
621
+ self.norm = norm
622
+
623
+ def forward(self, src: Tensor, mask: Optional[Tensor] = None, attn_bias: Optional[Tensor] = None, src_key_padding_mask: Optional[Tensor] = None) -> Tensor:
624
+ r"""Pass the input through the encoder layers in turn.
625
+
626
+ Args:
627
+ src: the sequence to the encoder (required).
628
+ mask: the mask for the src sequence (optional).
629
+ src_key_padding_mask: the mask for the src keys per batch (optional).
630
+
631
+ Shape:
632
+ see the docs in Transformer class.
633
+ """
634
+ output = src
635
+
636
+ for mod in self.layers:
637
+ output = mod(output, src_mask=mask, attn_bias=attn_bias, src_key_padding_mask=src_key_padding_mask)
638
+
639
+ if self.norm is not None:
640
+ output = self.norm(output)
641
+
642
+ return output
code/tabular_data_loaders.py ADDED
@@ -0,0 +1,676 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # TabuLM β€” tabular dataset and data loading utilities
2
+ # Extends KinyaBERT's data pipeline with:
3
+ # - TabularParsedToken: ParsedToken + (row_id, col_id, cell_type)
4
+ # - process_tabular_sentence: masking aware of cell boundaries
5
+ # - TabularKBCorpusDataset: loads CSV tables, no libkinlp.so required
6
+ # - tabular_collate_wrapper: extends morpho_seq_collate_wrapper
7
+ # - tabulm_model_forward: unpacks tabular batch for the model
8
+
9
+ from __future__ import print_function, division
10
+
11
+ import math
12
+ import os
13
+ import random
14
+ import sys
15
+ import warnings
16
+ from typing import List, Optional, Tuple
17
+
18
+ import numpy as np
19
+ import torch
20
+ import youtokentome as yttm
21
+ from torch.utils.data import Dataset
22
+
23
+ from kinyabert_utils import time_now
24
+ from morpho_data_loaders import (
25
+ KBVocab, AffixSetVocab, ParsedToken, morpho_seq_collate_wrapper
26
+ )
27
+ from tabular_serializer import (
28
+ CellType, NUM_CELL_TYPES, TableCell,
29
+ serialize_csv, table_cells_to_text, WordMeta
30
+ )
31
+ from morpho_stub import parse_text_stub
32
+
33
+ warnings.filterwarnings("ignore")
34
+
35
+
36
+ # ── TabularParsedToken ────────────────────────────────────────────────────────
37
+
38
+ class TabularParsedToken(ParsedToken):
39
+ """ParsedToken extended with table-grid coordinates."""
40
+
41
+ def __init__(self, row_id: int, col_id: int, cell_type: int, **kwargs):
42
+ super().__init__(**kwargs)
43
+ self.row_id = row_id
44
+ self.col_id = col_id
45
+ self.cell_type = cell_type
46
+
47
+
48
+ # ── process_tabular_sentence ─────────────────────────────────────────────────
49
+
50
+ def process_tabular_sentence(
51
+ args,
52
+ parsed_tokens_list: List[TabularParsedToken],
53
+ add_cls: bool,
54
+ kv: KBVocab,
55
+ affix_set_vocab: AffixSetVocab,
56
+ mcr_masked_cells: set, # set of (row_id, col_id) selected for MCR masking
57
+ ctp_col_labels: dict, # {col_id: cell_type_int} for CTP targets
58
+ ):
59
+ """
60
+ Extended version of process_parsed_sentence that additionally:
61
+ - Tracks row_id / col_id / cell_type per sequence position
62
+ - Applies MCR (cell-level) masking in addition to word-level masking
63
+ - Collects CTP prediction targets (column type labels)
64
+ """
65
+ pos_tags = []
66
+ stems = []
67
+ afsets = [] if args.use_afsets else None
68
+ affixes = []
69
+ tokens_lengths = []
70
+ row_ids = []
71
+ col_ids = []
72
+ cell_types = []
73
+
74
+ predicted_stems = []
75
+ predicted_afsets = [] if args.use_afsets else None
76
+ predicted_affixes = [] if args.predict_affixes else None
77
+ predicted_tokens_idx = []
78
+ predicted_tokens_affixes_idx = [] if args.predict_affixes else None
79
+ predicted_tokens_affixes_lengths = [] if args.predict_affixes else None
80
+
81
+ # MCR-specific bookkeeping
82
+ mcr_predicted_stems = []
83
+ mcr_predicted_tokens_idx = []
84
+
85
+ def _add_special(vocab_key: str, r: int = 0, c: int = 0, ct: int = 0):
86
+ pos_tags.append(kv.pos_tag_vocab[vocab_key])
87
+ stems.append(kv.reduced_stem_vocab[vocab_key])
88
+ if args.use_afsets:
89
+ afsets.append(affix_set_vocab.affix_set_to_idx(vocab_key))
90
+ tokens_lengths.append(0)
91
+ row_ids.append(r)
92
+ col_ids.append(c)
93
+ cell_types.append(ct)
94
+
95
+ if add_cls:
96
+ _add_special('<CLS>')
97
+
98
+ if not parsed_tokens_list:
99
+ _add_special('<SEP>')
100
+ return _pack_result(
101
+ pos_tags, stems, afsets, affixes, tokens_lengths,
102
+ row_ids, col_ids, cell_types,
103
+ predicted_stems, predicted_afsets, predicted_affixes,
104
+ predicted_tokens_idx, predicted_tokens_affixes_idx,
105
+ predicted_tokens_affixes_lengths,
106
+ mcr_predicted_stems, mcr_predicted_tokens_idx,
107
+ )
108
+
109
+ for ptoken in parsed_tokens_list:
110
+ is_mcr_masked = (ptoken.row_id, ptoken.col_id) in mcr_masked_cells
111
+ r, c, ct = ptoken.row_id, ptoken.col_id, ptoken.cell_type
112
+
113
+ for sidx in ptoken.stem_idx:
114
+ unchanged = True
115
+ predict_word = False
116
+ rval = random.random()
117
+
118
+ if is_mcr_masked:
119
+ # MCR: mask the entire cell unconditionally
120
+ pos_tags.append(kv.pos_tag_vocab['<MSK>'])
121
+ stems.append(kv.reduced_stem_vocab['<MSK>'])
122
+ if args.use_afsets:
123
+ afsets.append(affix_set_vocab.affix_set_to_idx('<MSK>'))
124
+ tokens_lengths.append(0)
125
+ mcr_predicted_stems.append(kv.mapped_stem_vocab_idx[sidx])
126
+ mcr_predicted_tokens_idx.append(len(tokens_lengths) - 1)
127
+ unchanged = False
128
+
129
+ else:
130
+ # Standard word-level masking (15%)
131
+ if rval <= 0.15:
132
+ predict_word = True
133
+ rval /= 0.15
134
+ if rval < 0.8:
135
+ unchanged = False
136
+ pos_tags.append(kv.pos_tag_vocab['<MSK>'])
137
+ stems.append(kv.reduced_stem_vocab['<MSK>'])
138
+ if args.use_afsets:
139
+ afsets.append(affix_set_vocab.affix_set_to_idx('<MSK>'))
140
+ if (rval / 0.8) < 0.3:
141
+ affixes.extend(ptoken.affixes_idx)
142
+ tokens_lengths.append(len(ptoken.affixes_idx))
143
+ else:
144
+ tokens_lengths.append(0)
145
+ elif rval < 0.9:
146
+ unchanged = False
147
+ rnd_pos = random.randint(kv.pos_tag_vocab['<UNK>'], len(kv.pos_tag_vocab) - 1)
148
+ rnd_stem = random.randint(kv.reduced_stem_vocab['<UNK>'], len(kv.reduced_stem_vocab) - 1)
149
+ pos_tags.append(rnd_pos)
150
+ stems.append(rnd_stem)
151
+ if args.use_afsets:
152
+ afsets.append(affix_set_vocab.random_idx())
153
+ tokens_lengths.append(0)
154
+
155
+ if unchanged:
156
+ pos_tags.append(ptoken.pos_tag_idx)
157
+ stems.append(kv.mapped_stem_vocab_idx[sidx])
158
+ if args.use_afsets:
159
+ afsets.append(affix_set_vocab.affix_set_to_idx(ptoken.affix_set_key()))
160
+ affixes.extend(ptoken.affixes_idx)
161
+ tokens_lengths.append(len(ptoken.affixes_idx))
162
+
163
+ row_ids.append(r)
164
+ col_ids.append(c)
165
+ cell_types.append(ct)
166
+
167
+ if predict_word:
168
+ predicted_stems.append(kv.mapped_stem_vocab_idx[sidx])
169
+ predicted_tokens_idx.append(len(tokens_lengths) - 1)
170
+ if args.use_afsets:
171
+ predicted_afsets.append(affix_set_vocab.affix_set_to_idx(ptoken.affix_set_key()))
172
+ if args.predict_affixes:
173
+ predicted_affixes.extend(ptoken.affixes_idx)
174
+ if ptoken.affixes_idx:
175
+ predicted_tokens_affixes_idx.append(len(predicted_tokens_idx) - 1)
176
+ predicted_tokens_affixes_lengths.append(len(ptoken.affixes_idx))
177
+
178
+ return _pack_result(
179
+ pos_tags, stems, afsets, affixes, tokens_lengths,
180
+ row_ids, col_ids, cell_types,
181
+ predicted_stems, predicted_afsets, predicted_affixes,
182
+ predicted_tokens_idx, predicted_tokens_affixes_idx,
183
+ predicted_tokens_affixes_lengths,
184
+ mcr_predicted_stems, mcr_predicted_tokens_idx,
185
+ )
186
+
187
+
188
+ def _pack_result(pos_tags, stems, afsets, affixes, tokens_lengths,
189
+ row_ids, col_ids, cell_types,
190
+ predicted_stems, predicted_afsets, predicted_affixes,
191
+ predicted_tokens_idx, predicted_tokens_affixes_idx,
192
+ predicted_tokens_affixes_lengths,
193
+ mcr_predicted_stems, mcr_predicted_tokens_idx):
194
+ return (
195
+ pos_tags, stems, afsets, affixes, tokens_lengths,
196
+ row_ids, col_ids, cell_types,
197
+ predicted_stems, predicted_afsets, predicted_affixes,
198
+ predicted_tokens_idx, predicted_tokens_affixes_idx,
199
+ predicted_tokens_affixes_lengths,
200
+ mcr_predicted_stems, mcr_predicted_tokens_idx,
201
+ )
202
+
203
+
204
+ # ── gather_tabular_itemized_data ─────────────────────────────────────────────
205
+
206
+ def gather_tabular_itemized_data(
207
+ args,
208
+ table_cells_list: List[List[TableCell]], # list of tables, each a list of cells
209
+ kb_vocab: KBVocab,
210
+ affix_set_vocab: AffixSetVocab,
211
+ bpe: yttm.BPE,
212
+ max_seq_len: int,
213
+ max_batch_items: int,
214
+ mcr_cell_mask_rate: float = 0.15,
215
+ ctp_mask_rate: float = 0.50,
216
+ ):
217
+ """
218
+ Builds a list of itemized training data items from a list of tables.
219
+ Each item is a tuple compatible with tabular_collate_wrapper.
220
+ """
221
+ itemized_data = []
222
+ table_idx = 0
223
+
224
+ seq_pos_tags, seq_stems, seq_afsets, seq_affixes, seq_tokens_lengths = [], [], [], [], []
225
+ seq_row_ids, seq_col_ids, seq_cell_types = [], [], []
226
+ seq_predicted_stems = []
227
+ seq_predicted_afsets = [] if args.use_afsets else None
228
+ seq_predicted_affixes = [] if args.predict_affixes else None
229
+ seq_predicted_tokens_idx = []
230
+ seq_predicted_tokens_affixes_idx = [] if args.predict_affixes else None
231
+ seq_predicted_tokens_affixes_lengths = [] if args.predict_affixes else None
232
+ seq_mcr_predicted_stems, seq_mcr_predicted_tokens_idx = [], []
233
+ seq_ctp_labels: List[Tuple[int, int]] = [] # (position_in_sequence, cell_type_label)
234
+ seq_rrp_pair: Optional[Tuple[int, int, int]] = None # (rowA_start, rowB_start, label)
235
+
236
+ add_cls = True
237
+ random.shuffle(table_cells_list)
238
+
239
+ for cells in table_cells_list:
240
+ if not cells:
241
+ continue
242
+
243
+ text, word_meta = table_cells_to_text(cells)
244
+ parsed_tokens_raw = parse_text_stub(text, kb_vocab, bpe)
245
+
246
+ if len(parsed_tokens_raw) != len(word_meta):
247
+ # Length mismatch can happen with empty cells β€” skip
248
+ continue
249
+
250
+ # Attach table coordinates to each ParsedToken
251
+ tabular_tokens: List[TabularParsedToken] = []
252
+ for pt, (r, c, ct) in zip(parsed_tokens_raw, word_meta):
253
+ tp = TabularParsedToken(
254
+ row_id=r, col_id=c, cell_type=ct,
255
+ surface_form=pt.surface_form,
256
+ decode_prob=pt.decode_prob,
257
+ tf_idf=pt.tf_idf,
258
+ pos_tag_id=pt.pos_tag_idx,
259
+ stem_ids=pt.stem_idx,
260
+ )
261
+ tp.morpho_slots_idx = pt.morpho_slots_idx
262
+ tp.affixes_idx = pt.affixes_idx
263
+ tabular_tokens.append(tp)
264
+
265
+ # Choose MCR cells to mask (15% of unique cells by (row_id, col_id))
266
+ unique_cells = set((t.row_id, t.col_id) for t in tabular_tokens if t.row_id > 0)
267
+ num_mcr = max(1, int(mcr_cell_mask_rate * len(unique_cells)))
268
+ mcr_masked_cells = set(random.sample(sorted(unique_cells), min(num_mcr, len(unique_cells))))
269
+
270
+ # CTP: determine column type labels (majority vote per column)
271
+ col_type_votes: dict = {}
272
+ for t in tabular_tokens:
273
+ if t.row_id > 1 and t.col_id > 0 and t.cell_type != int(CellType.PAD):
274
+ col_type_votes.setdefault(t.col_id, []).append(t.cell_type)
275
+
276
+ ctp_labels_for_table = {}
277
+ for col_id, votes in col_type_votes.items():
278
+ from collections import Counter
279
+ majority = Counter(votes).most_common(1)[0][0]
280
+ ctp_labels_for_table[col_id] = majority
281
+
282
+ (pos_tags, stems, afsets, affixes, tokens_lengths,
283
+ row_ids, col_ids, cell_types,
284
+ predicted_stems, predicted_afsets, predicted_affixes,
285
+ predicted_tokens_idx, predicted_tokens_affixes_idx,
286
+ predicted_tokens_affixes_lengths,
287
+ mcr_predicted_stems, mcr_predicted_tokens_idx) = process_tabular_sentence(
288
+ args, tabular_tokens, add_cls, kb_vocab, affix_set_vocab,
289
+ mcr_masked_cells, ctp_labels_for_table,
290
+ )
291
+ add_cls = False
292
+
293
+ if (len(seq_tokens_lengths) + len(tokens_lengths)) > max_seq_len:
294
+ # Flush existing buffer (only if non-empty)
295
+ if seq_tokens_lengths:
296
+ item = _pack_tabular_item(
297
+ max_seq_len,
298
+ seq_pos_tags, seq_stems, seq_afsets, seq_affixes, seq_tokens_lengths,
299
+ seq_row_ids, seq_col_ids, seq_cell_types,
300
+ seq_predicted_stems, seq_predicted_afsets, seq_predicted_affixes,
301
+ seq_predicted_tokens_idx, seq_predicted_tokens_affixes_idx,
302
+ seq_predicted_tokens_affixes_lengths,
303
+ seq_mcr_predicted_stems, seq_mcr_predicted_tokens_idx,
304
+ seq_ctp_labels,
305
+ )
306
+ itemized_data.append(item)
307
+ if len(itemized_data) >= max_batch_items:
308
+ return itemized_data
309
+
310
+ # Reset buffer then fall through to add current tokens below
311
+ seq_pos_tags, seq_stems, seq_afsets, seq_affixes, seq_tokens_lengths = [], [], [], [], []
312
+ seq_row_ids, seq_col_ids, seq_cell_types = [], [], []
313
+ seq_predicted_stems = []
314
+ seq_predicted_afsets = [] if args.use_afsets else None
315
+ seq_predicted_affixes = [] if args.predict_affixes else None
316
+ seq_predicted_tokens_idx = []
317
+ seq_predicted_tokens_affixes_idx = [] if args.predict_affixes else None
318
+ seq_predicted_tokens_affixes_lengths = [] if args.predict_affixes else None
319
+ seq_mcr_predicted_stems, seq_mcr_predicted_tokens_idx = [], []
320
+ seq_ctp_labels = []
321
+ add_cls = True
322
+ # Truncate current table to max_seq_len before adding to fresh buffer
323
+ tokens_lengths = tokens_lengths[:max_seq_len]
324
+ pos_tags = pos_tags[:max_seq_len]
325
+ stems = stems[:max_seq_len]
326
+ row_ids = row_ids[:max_seq_len]
327
+ col_ids = col_ids[:max_seq_len]
328
+ cell_types = cell_types[:max_seq_len]
329
+ if afsets is not None:
330
+ afsets = afsets[:max_seq_len]
331
+ # Re-compute affix flat list up to the truncated tokens
332
+ affix_end = sum(tokens_lengths)
333
+ affixes = affixes[:affix_end]
334
+ # Trim prediction indices that are out of range
335
+ predicted_tokens_idx = [i for i in predicted_tokens_idx if i < max_seq_len]
336
+ predicted_stems = predicted_stems[:len(predicted_tokens_idx)]
337
+ if predicted_afsets is not None:
338
+ predicted_afsets = predicted_afsets[:len(predicted_tokens_idx)]
339
+ if predicted_tokens_affixes_idx is not None:
340
+ predicted_tokens_affixes_idx = [i for i in predicted_tokens_affixes_idx if i < len(predicted_tokens_idx)]
341
+ predicted_tokens_affixes_lengths = predicted_tokens_affixes_lengths[:len(predicted_tokens_affixes_idx)] if predicted_tokens_affixes_lengths else []
342
+ predicted_affixes = predicted_affixes[:sum(predicted_tokens_affixes_lengths)] if predicted_affixes else []
343
+ mcr_predicted_tokens_idx = [i for i in mcr_predicted_tokens_idx if i < max_seq_len]
344
+ mcr_predicted_stems = mcr_predicted_stems[:len(mcr_predicted_tokens_idx)]
345
+
346
+ offset = len(seq_predicted_tokens_idx)
347
+ if args.predict_affixes and predicted_tokens_affixes_idx:
348
+ seq_predicted_tokens_affixes_idx.extend(
349
+ [offset + i for i in predicted_tokens_affixes_idx]
350
+ )
351
+ seq_predicted_tokens_idx.extend(
352
+ [len(seq_tokens_lengths) + i for i in predicted_tokens_idx]
353
+ )
354
+
355
+ # CTP: record (absolute_position_of_first_header_token_in_col, label)
356
+ header_positions = {}
357
+ for abs_i, (r, c, ct_int) in enumerate(zip(row_ids, col_ids, cell_types)):
358
+ if ct_int == int(CellType.HEADER) and c not in header_positions:
359
+ header_positions[c] = len(seq_tokens_lengths) + abs_i
360
+ for col_id, label in ctp_labels_for_table.items():
361
+ if random.random() < ctp_mask_rate and col_id in header_positions:
362
+ seq_ctp_labels.append((header_positions[col_id], label))
363
+
364
+ seq_pos_tags.extend(pos_tags)
365
+ seq_stems.extend(stems)
366
+ if args.use_afsets and afsets is not None:
367
+ seq_afsets.extend(afsets)
368
+ seq_affixes.extend(affixes)
369
+ seq_tokens_lengths.extend(tokens_lengths)
370
+ seq_row_ids.extend(row_ids)
371
+ seq_col_ids.extend(col_ids)
372
+ seq_cell_types.extend(cell_types)
373
+ seq_predicted_stems.extend(predicted_stems)
374
+ if args.use_afsets and predicted_afsets is not None:
375
+ seq_predicted_afsets.extend(predicted_afsets)
376
+ if args.predict_affixes and predicted_affixes is not None:
377
+ seq_predicted_affixes.extend(predicted_affixes)
378
+ if predicted_tokens_affixes_lengths:
379
+ seq_predicted_tokens_affixes_lengths.extend(predicted_tokens_affixes_lengths)
380
+ seq_mcr_predicted_stems.extend(mcr_predicted_stems)
381
+ seq_mcr_predicted_tokens_idx.extend(
382
+ [len(seq_tokens_lengths) - len(tokens_lengths) + i for i in mcr_predicted_tokens_idx]
383
+ )
384
+
385
+ return itemized_data
386
+
387
+
388
+ def _pack_tabular_item(
389
+ max_seq_len,
390
+ pos_tags, stems, afsets, affixes, tokens_lengths,
391
+ row_ids, col_ids, cell_types,
392
+ predicted_stems, predicted_afsets, predicted_affixes,
393
+ predicted_tokens_idx, predicted_tokens_affixes_idx,
394
+ predicted_tokens_affixes_lengths,
395
+ mcr_predicted_stems, mcr_predicted_tokens_idx,
396
+ ctp_labels,
397
+ ):
398
+ return (
399
+ max_seq_len,
400
+ # Original KinyaBERT fields
401
+ None, # rel_pos_arr (unused for tabular)
402
+ pos_tags, stems, afsets, affixes, tokens_lengths,
403
+ predicted_stems, predicted_afsets, predicted_affixes,
404
+ predicted_tokens_idx, predicted_tokens_affixes_idx,
405
+ predicted_tokens_affixes_lengths,
406
+ # Tabular-specific fields
407
+ row_ids, col_ids, cell_types,
408
+ mcr_predicted_stems, mcr_predicted_tokens_idx,
409
+ ctp_labels,
410
+ )
411
+
412
+
413
+ # ── TabularKBCorpusDataset ────────────────────────────────────────────────────
414
+
415
+ class TabularKBCorpusDataset(Dataset):
416
+ """
417
+ Dataset that loads CSV tables from a directory and builds
418
+ tabular training items. Compatible with tabular_collate_wrapper.
419
+ """
420
+
421
+ def __init__(
422
+ self,
423
+ args,
424
+ kb_vocab: KBVocab,
425
+ affix_set_vocab: AffixSetVocab,
426
+ bpe_encoder: yttm.BPE,
427
+ csv_dir: str,
428
+ max_batch_items: int,
429
+ max_seq_len: int = 512,
430
+ max_rows: int = 64,
431
+ max_cols: int = 24,
432
+ rank: int = 0,
433
+ ):
434
+ self.max_seq_len = max_seq_len
435
+ self.max_batch_items = max_batch_items
436
+
437
+ # Discover CSV files
438
+ csv_files = [
439
+ os.path.join(csv_dir, f)
440
+ for f in os.listdir(csv_dir)
441
+ if f.lower().endswith('.csv')
442
+ ]
443
+ if not csv_files:
444
+ raise FileNotFoundError(f'No CSV files found in {csv_dir}')
445
+
446
+ if rank == 0:
447
+ print(time_now(), f'Loading {len(csv_files)} CSV tables from {csv_dir}')
448
+
449
+ # Load all tables
450
+ all_tables: List[List[TableCell]] = []
451
+ for fp in csv_files:
452
+ cells = serialize_csv(fp, max_rows=max_rows, max_cols=max_cols)
453
+ if cells:
454
+ all_tables.append(cells)
455
+
456
+ if rank == 0:
457
+ print(time_now(), f'{len(all_tables)} tables loaded successfully')
458
+
459
+ self.itemized_data = gather_tabular_itemized_data(
460
+ args, all_tables, kb_vocab, affix_set_vocab, bpe_encoder,
461
+ max_seq_len=max_seq_len,
462
+ max_batch_items=max_batch_items,
463
+ )
464
+
465
+ if rank == 0:
466
+ print(time_now(), f'{len(self.itemized_data)} training items prepared')
467
+
468
+ def __len__(self):
469
+ return len(self.itemized_data)
470
+
471
+ def __getitem__(self, idx):
472
+ return self.itemized_data[idx]
473
+
474
+
475
+ # ── tabular_collate_wrapper ───────────────────────────────────────────────────
476
+
477
+ def tabular_collate_wrapper(batch_items):
478
+ """
479
+ Collate function for TabularKBCorpusDataset.
480
+ Extends morpho_seq_collate_wrapper with tabular fields.
481
+ """
482
+ batch_input_sequence_lengths = []
483
+ batch_pos_tags, batch_stems, batch_afsets = [], [], []
484
+ batch_affixes, batch_tokens_lengths = [], []
485
+ batch_predicted_stems, batch_predicted_afsets = [], []
486
+ batch_predicted_affixes = []
487
+ batch_predicted_tokens_idx = []
488
+ batch_predicted_tokens_affixes_idx = []
489
+ batch_predicted_tokens_affixes_lengths = []
490
+
491
+ batch_row_ids, batch_col_ids, batch_cell_types = [], [], []
492
+ batch_mcr_predicted_stems, batch_mcr_predicted_tokens_idx = [], []
493
+ batch_ctp_labels: List[Tuple[int, int]] = []
494
+
495
+ for bidx, item in enumerate(batch_items):
496
+ (max_seq_len, _rel_pos_arr,
497
+ pos_tags, stems, afsets, affixes, tokens_lengths,
498
+ predicted_stems, predicted_afsets, predicted_affixes,
499
+ predicted_tokens_idx, predicted_tokens_affixes_idx,
500
+ predicted_tokens_affixes_lengths,
501
+ row_ids, col_ids, cell_types,
502
+ mcr_predicted_stems, mcr_predicted_tokens_idx,
503
+ ctp_labels) = item
504
+
505
+ # Offset affix-prediction indices
506
+ if predicted_tokens_affixes_idx is not None:
507
+ batch_predicted_tokens_affixes_idx.extend(
508
+ [(len(batch_predicted_tokens_idx) + t) for t in predicted_tokens_affixes_idx]
509
+ )
510
+
511
+ batch_predicted_tokens_idx.extend(
512
+ [(t, len(batch_input_sequence_lengths)) for t in predicted_tokens_idx]
513
+ )
514
+
515
+ # Offset MCR indices the same way
516
+ batch_mcr_predicted_tokens_idx.extend(
517
+ [(t, len(batch_input_sequence_lengths)) for t in mcr_predicted_tokens_idx]
518
+ )
519
+
520
+ # CTP labels: offset the absolute position
521
+ seq_start = sum(batch_input_sequence_lengths) # not directly, but captured below
522
+ batch_ctp_labels.extend(ctp_labels)
523
+
524
+ batch_pos_tags.extend(pos_tags)
525
+ batch_stems.extend(stems)
526
+ if afsets is not None:
527
+ batch_afsets.extend(afsets)
528
+ batch_affixes.extend(affixes)
529
+ batch_tokens_lengths.extend(tokens_lengths)
530
+ batch_predicted_stems.extend(predicted_stems)
531
+ if predicted_afsets is not None:
532
+ batch_predicted_afsets.extend(predicted_afsets)
533
+ if predicted_affixes is not None:
534
+ batch_predicted_affixes.extend(predicted_affixes)
535
+ if predicted_tokens_affixes_lengths is not None:
536
+ batch_predicted_tokens_affixes_lengths.extend(predicted_tokens_affixes_lengths)
537
+
538
+ batch_row_ids.extend(row_ids)
539
+ batch_col_ids.extend(col_ids)
540
+ batch_cell_types.extend(cell_types)
541
+ batch_mcr_predicted_stems.extend(mcr_predicted_stems)
542
+ batch_input_sequence_lengths.append(len(tokens_lengths))
543
+
544
+ return (
545
+ batch_input_sequence_lengths,
546
+ None, # rel_pos_arr unused
547
+ batch_pos_tags, batch_stems, batch_afsets,
548
+ batch_affixes, batch_tokens_lengths,
549
+ batch_predicted_stems, batch_predicted_afsets, batch_predicted_affixes,
550
+ batch_predicted_tokens_idx,
551
+ batch_predicted_tokens_affixes_idx,
552
+ batch_predicted_tokens_affixes_lengths,
553
+ # Tabular additions
554
+ batch_row_ids, batch_col_ids, batch_cell_types,
555
+ batch_mcr_predicted_stems, batch_mcr_predicted_tokens_idx,
556
+ batch_ctp_labels,
557
+ )
558
+
559
+
560
+ # ── tabulm_model_forward ──────────────────────────────────────────────────────
561
+
562
+ def tabulm_model_forward(args, data_item, model, device, tot_num_affixes):
563
+ """
564
+ Unpacks a tabular data_item and runs one forward pass through TabuLM.
565
+ Returns (total_loss, stem_loss, afset_loss, affix_loss, mcr_loss, ctp_loss, rrp_loss).
566
+ """
567
+ (batch_input_sequence_lengths,
568
+ _rel_pos_arr,
569
+ batch_pos_tags, batch_stems, batch_afsets,
570
+ batch_affixes, batch_tokens_lengths,
571
+ batch_predicted_stems, batch_predicted_afsets, batch_predicted_affixes,
572
+ batch_predicted_tokens_idx,
573
+ batch_predicted_tokens_affixes_idx,
574
+ batch_predicted_tokens_affixes_lengths,
575
+ batch_row_ids, batch_col_ids, batch_cell_types,
576
+ batch_mcr_predicted_stems, batch_mcr_predicted_tokens_idx,
577
+ batch_ctp_labels) = data_item
578
+
579
+ pos_tags = torch.tensor(batch_pos_tags, dtype=torch.long).to(device)
580
+ stems = torch.tensor(batch_stems, dtype=torch.long).to(device)
581
+ afsets = torch.tensor(batch_afsets, dtype=torch.long).to(device) if args.use_afsets and batch_afsets else None
582
+ affixes = torch.tensor(batch_affixes, dtype=torch.long).to(device)
583
+ row_ids = torch.tensor(batch_row_ids, dtype=torch.long).to(device)
584
+ col_ids = torch.tensor(batch_col_ids, dtype=torch.long).to(device)
585
+ cell_types = torch.tensor(batch_cell_types, dtype=torch.long).to(device)
586
+
587
+ max_seq = max(batch_input_sequence_lengths)
588
+ predicted_tokens_idx = torch.tensor(
589
+ [s * max_seq + t for t, s in batch_predicted_tokens_idx], dtype=torch.long
590
+ ).to(device)
591
+
592
+ predicted_tokens_affixes_idx = (
593
+ torch.tensor(batch_predicted_tokens_affixes_idx, dtype=torch.long).to(device)
594
+ if args.predict_affixes and batch_predicted_tokens_affixes_idx
595
+ else None
596
+ )
597
+
598
+ predicted_affixes_prob = None
599
+ if args.predict_affixes and batch_predicted_affixes and predicted_tokens_affixes_idx is not None:
600
+ from itertools import accumulate
601
+ lengths = batch_predicted_tokens_affixes_lengths
602
+ affix_groups = []
603
+ prev = 0
604
+ for ln in lengths:
605
+ sub = batch_predicted_affixes[prev: prev + ln]
606
+ vec = torch.zeros(tot_num_affixes)
607
+ for idx in sub:
608
+ if 0 < idx < tot_num_affixes:
609
+ vec[idx] = 1.0
610
+ affix_groups.append(vec)
611
+ prev += ln
612
+ if affix_groups:
613
+ predicted_affixes_prob = torch.stack(affix_groups).to(device)
614
+ predicted_affixes_prob = predicted_affixes_prob / (predicted_affixes_prob.sum(1, keepdim=True).clamp(min=1.0))
615
+
616
+ predicted_stems = torch.tensor(batch_predicted_stems, dtype=torch.long).to(device)
617
+ predicted_afsets = (
618
+ torch.tensor(batch_predicted_afsets, dtype=torch.long).to(device)
619
+ if args.use_afsets and batch_predicted_afsets else None
620
+ )
621
+
622
+ # MCR indices and targets
623
+ mcr_tokens_idx = (
624
+ torch.tensor([s * max_seq + t for t, s in batch_mcr_predicted_tokens_idx], dtype=torch.long).to(device)
625
+ if batch_mcr_predicted_tokens_idx else None
626
+ )
627
+ mcr_stems = (
628
+ torch.tensor(batch_mcr_predicted_stems, dtype=torch.long).to(device)
629
+ if batch_mcr_predicted_stems else None
630
+ )
631
+
632
+ # CTP indices and labels
633
+ ctp_positions = (
634
+ torch.tensor([pos for pos, _ in batch_ctp_labels], dtype=torch.long).to(device)
635
+ if batch_ctp_labels else None
636
+ )
637
+ ctp_labels_t = (
638
+ torch.tensor([lbl for _, lbl in batch_ctp_labels], dtype=torch.long).to(device)
639
+ if batch_ctp_labels else None
640
+ )
641
+
642
+ # Ablation flags: disable objectives or structural embeddings
643
+ if getattr(args, 'no_mcr', False):
644
+ mcr_tokens_idx = None
645
+ mcr_stems = None
646
+ if getattr(args, 'no_ctp', False):
647
+ ctp_positions = None
648
+ ctp_labels_t = None
649
+ if getattr(args, 'no_tabular_emb', False):
650
+ # padding_idx=0 β†’ embedding lookup returns zeros, so zeroing IDs kills all structural signal
651
+ row_ids = torch.zeros_like(row_ids)
652
+ col_ids = torch.zeros_like(col_ids)
653
+ cell_types = torch.zeros_like(cell_types)
654
+
655
+ return model(
656
+ args,
657
+ rel_pos_arr=None,
658
+ tokens_lengths=batch_tokens_lengths,
659
+ input_sequence_lengths=batch_input_sequence_lengths,
660
+ pos_tags=pos_tags,
661
+ stems=stems,
662
+ afsets=afsets,
663
+ affixes=affixes,
664
+ row_ids=row_ids,
665
+ col_ids=col_ids,
666
+ cell_types=cell_types,
667
+ predicted_tokens_idx=predicted_tokens_idx,
668
+ predicted_tokens_affixes_idx=predicted_tokens_affixes_idx,
669
+ predicted_stems=predicted_stems,
670
+ predicted_afsets=predicted_afsets,
671
+ predicted_affixes_prob=predicted_affixes_prob,
672
+ mcr_tokens_idx=mcr_tokens_idx,
673
+ mcr_stems=mcr_stems,
674
+ ctp_positions=ctp_positions,
675
+ ctp_labels=ctp_labels_t,
676
+ )
code/tabular_serializer.py ADDED
@@ -0,0 +1,175 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # TabuLM β€” tabular data serializer
2
+ # Converts CSV / dict-of-records tables into a flat token string
3
+ # plus a parallel word_metadata list for row/column tracking.
4
+
5
+ import csv
6
+ import re
7
+ from enum import IntEnum
8
+ from typing import List, Tuple
9
+
10
+ # ── Cell type taxonomy ────────────────────────────────────────────────────────
11
+
12
+ class CellType(IntEnum):
13
+ PAD = 0 # padding / special tokens with no cell identity
14
+ HEADER = 1 # column header row
15
+ NUMERIC = 2 # quantity, count, percentage, measurement
16
+ TEXT = 3 # free-form text longer than a label
17
+ CATEGORICAL = 4 # short label / enum value
18
+ DATE = 5 # year or full date
19
+
20
+ NUM_CELL_TYPES = 6
21
+
22
+ # ── Regex heuristics ──────────────────────────────────────────────────────────
23
+
24
+ _NUMERIC_RE = re.compile(
25
+ r'^[\d,.\s]+(%|Frw|RWF|km|kg|ha|m|mΒ²|L|MW|USD|acres|ha)?$',
26
+ re.IGNORECASE,
27
+ )
28
+ _DATE_RE = re.compile(
29
+ r'^(1[89]\d{2}|2[012]\d{2})(-\d{2}(-\d{2})?)?$|^\d{1,2}/\d{1,2}/\d{2,4}$'
30
+ )
31
+ _NULL_RE = re.compile(r'^[-–]$|^n/?a$|^null$|^none$|^$', re.IGNORECASE)
32
+
33
+
34
+ # ── Core data structures ──────────────────────────────────────────────────────
35
+
36
+ class TableCell:
37
+ """One cell in a table, with its grid coordinates and semantic type."""
38
+ __slots__ = ('content', 'row_id', 'col_id', 'cell_type')
39
+
40
+ def __init__(self, content: str, row_id: int, col_id: int, cell_type: CellType):
41
+ self.content = content.strip()
42
+ self.row_id = row_id # 1-based; 1 = header row
43
+ self.col_id = col_id # 1-based
44
+ self.cell_type = cell_type
45
+
46
+ def __repr__(self):
47
+ return (f'TableCell(r={self.row_id}, c={self.col_id}, '
48
+ f'type={self.cell_type.name}, "{self.content[:20]}")')
49
+
50
+
51
+ # ── Cell type detection ───────────────────────────────────────────────────────
52
+
53
+ def detect_cell_type(value: str, is_header: bool = False) -> CellType:
54
+ if is_header:
55
+ return CellType.HEADER
56
+ v = value.strip()
57
+ if _NULL_RE.match(v):
58
+ return CellType.TEXT
59
+ if _DATE_RE.match(v):
60
+ return CellType.DATE
61
+ if _NUMERIC_RE.match(v):
62
+ return CellType.NUMERIC
63
+ if len(v) <= 40 and '\n' not in v and ' ' not in v:
64
+ return CellType.CATEGORICAL
65
+ return CellType.TEXT
66
+
67
+
68
+ # ── Table loading ─────────────────────────────────────────────────────────────
69
+
70
+ def serialize_csv(filepath: str,
71
+ max_rows: int = 64,
72
+ max_cols: int = 24) -> List[TableCell]:
73
+ """Read a CSV file and return an ordered list of TableCell objects."""
74
+ cells: List[TableCell] = []
75
+ try:
76
+ with open(filepath, newline='', encoding='utf-8-sig') as f:
77
+ rows = [r for r in csv.reader(f) if any(c.strip() for c in r)]
78
+ except Exception:
79
+ return cells
80
+
81
+ if not rows:
82
+ return cells
83
+
84
+ header = rows[0][:max_cols]
85
+ for col_id, h in enumerate(header, start=1):
86
+ cells.append(TableCell(
87
+ h or f'col_{col_id}', row_id=1, col_id=col_id,
88
+ cell_type=CellType.HEADER,
89
+ ))
90
+
91
+ for row_offset, row in enumerate(rows[1: max_rows + 1], start=2):
92
+ for col_id, val in enumerate(row[:max_cols], start=1):
93
+ cells.append(TableCell(
94
+ val, row_id=row_offset, col_id=col_id,
95
+ cell_type=detect_cell_type(val),
96
+ ))
97
+
98
+ return cells
99
+
100
+
101
+ def serialize_records(records: List[dict],
102
+ max_rows: int = 64,
103
+ max_cols: int = 24) -> List[TableCell]:
104
+ """Convert a list of dicts (e.g. from pandas .to_dict('records')) to cells."""
105
+ if not records:
106
+ return []
107
+ cells: List[TableCell] = []
108
+ keys = list(records[0].keys())[:max_cols]
109
+
110
+ for col_id, k in enumerate(keys, start=1):
111
+ cells.append(TableCell(
112
+ str(k), row_id=1, col_id=col_id,
113
+ cell_type=CellType.HEADER,
114
+ ))
115
+
116
+ for row_offset, rec in enumerate(records[:max_rows], start=2):
117
+ for col_id, k in enumerate(keys, start=1):
118
+ val = str(rec.get(k, ''))
119
+ cells.append(TableCell(
120
+ val, row_id=row_offset, col_id=col_id,
121
+ cell_type=detect_cell_type(val),
122
+ ))
123
+
124
+ return cells
125
+
126
+
127
+ # ── Serialization ────────────────────────────────────────────────────────────���
128
+
129
+ # WordMeta: (row_id, col_id, cell_type_int)
130
+ WordMeta = Tuple[int, int, int]
131
+
132
+
133
+ def table_cells_to_text(
134
+ cells: List[TableCell],
135
+ ) -> Tuple[str, List[WordMeta]]:
136
+ """
137
+ Flatten table cells into a single space-separated string with structure
138
+ tokens, plus a parallel per-token metadata list.
139
+
140
+ Special tokens emitted:
141
+ [TAB] β€” start of header row
142
+ [ROW] β€” start of any data row
143
+ [CEL] β€” start of each individual cell
144
+
145
+ Returns:
146
+ text β€” string ready for morpho_stub.parse_text_stub()
147
+ word_meta β€” list of (row_id, col_id, cell_type) with one entry per
148
+ space-separated token in `text` (including special tokens,
149
+ which get row_id=0, col_id=0, cell_type=PAD)
150
+ """
151
+ parts: List[str] = []
152
+ word_meta: List[WordMeta] = []
153
+
154
+ PAD = (0, 0, int(CellType.PAD))
155
+ cur_row = -1
156
+
157
+ for cell in cells:
158
+ if cell.row_id != cur_row:
159
+ sep = '[TAB]' if cell.row_id == 1 else '[ROW]'
160
+ parts.append(sep)
161
+ word_meta.append(PAD)
162
+ cur_row = cell.row_id
163
+
164
+ parts.append('[CEL]')
165
+ word_meta.append((cell.row_id, cell.col_id, int(cell.cell_type)))
166
+
167
+ content_words = cell.content.split() if cell.content else ['[EMPTY]']
168
+ if not content_words:
169
+ content_words = ['[EMPTY]']
170
+
171
+ for w in content_words:
172
+ parts.append(w)
173
+ word_meta.append((cell.row_id, cell.col_id, int(cell.cell_type)))
174
+
175
+ return ' '.join(parts), word_meta
code/tabulm_model.py ADDED
@@ -0,0 +1,468 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # TabuLM β€” Table-Aware Morphologically-Rich Language Model
2
+ # Extends KinyaBERT_MorphoEncoder with:
3
+ # 1. Row / column / cell-type embeddings (additive, same dim as seq_tr_d_model)
4
+ # 2. Table structure attention bias (same-row, same-col, header bias per head)
5
+ # 3. MCR head β€” Masked Cell Recovery (reuses stem decoder)
6
+ # 4. CTP head β€” Column Type Prediction (4-class linear)
7
+ #
8
+ # Architecture change summary vs KinyaBERT:
9
+ # seq_tr_d_model unchanged (768 with default args)
10
+ # attn_bias = position_bias + table_structure_bias (injected at morpho_transformer.py:333)
11
+ # input to seq-tr = morpho_output + stem_embed + row_embed + col_embed + cell_type_embed (additive)
12
+ # new losses = MCR (NLL) + CTP (NLL)
13
+
14
+ from __future__ import print_function, division
15
+
16
+ import math
17
+ import warnings
18
+
19
+ import torch
20
+ import torch.nn as nn
21
+ import torch.nn.functional as F
22
+ from torch.cuda.amp import custom_fwd
23
+ from torch.nn.utils.rnn import pad_sequence
24
+
25
+ from morpho_transformer import TransformerEncoder, TransformerEncoderLayer, MultiheadAttention
26
+ from morpho_model import (
27
+ gelu, init_bert_params, BertLayerNorm,
28
+ KinyaBERT_MorphoEncoder,
29
+ )
30
+ from tabular_serializer import NUM_CELL_TYPES
31
+
32
+ warnings.filterwarnings("ignore")
33
+
34
+ # ── Maximum table dimensions supported ───────────────────────────────────────
35
+ MAX_ROWS = 128
36
+ MAX_COLS = 64
37
+
38
+
39
+ # ── Column type label set for CTP ────────────────────────────────────────────
40
+ # We predict: NUMERIC(0) / TEXT(1) / CATEGORICAL(2) / DATE(3)
41
+ # (HEADER is the header row itself, not a prediction target)
42
+ CTP_NUM_LABELS = 4
43
+ CTP_TYPE_MAP = {2: 0, 3: 1, 4: 2, 5: 3} # CellType int β†’ CTP label index
44
+
45
+
46
+ class TabuLM_Encoder(KinyaBERT_MorphoEncoder):
47
+ """
48
+ KinyaBERT_MorphoEncoder extended with tabular structure awareness.
49
+
50
+ Three new components are added to the sequence transformer tier:
51
+
52
+ (a) Additive embeddings: row_embedding, col_embedding, cell_type_embedding
53
+ β€” same dimension as seq_tr_d_model, added element-wise to each word
54
+ vector before it enters the sequence transformer (same way BERT adds
55
+ absolute position embeddings).
56
+
57
+ (b) Table structure attention bias: a per-head learned scalar that is added
58
+ to the pre-softmax attention weights whenever two tokens share the same
59
+ row, same column, or when either token is in the header row.
60
+ Injected at morpho_transformer.py line 333 on top of position bias.
61
+
62
+ (c) MCR prediction head: reuses the inherited stem decoder to score
63
+ cell-level masked tokens (no new parameters needed).
64
+
65
+ (d) CTP prediction head: a 2-layer linear that maps from seq_tr_d_model
66
+ to CTP_NUM_LABELS classes for column type prediction.
67
+ """
68
+
69
+ def __init__(self, *args,
70
+ max_rows: int = MAX_ROWS,
71
+ max_cols: int = MAX_COLS,
72
+ **kwargs):
73
+ super().__init__(*args, **kwargs)
74
+
75
+ d = self.seq_tr_d_model
76
+
77
+ # (a) Tabular embeddings (additive, padding_idx=0 is the PAD/special token slot)
78
+ self.row_embedding = nn.Embedding(max_rows + 2, d, padding_idx=0)
79
+ self.col_embedding = nn.Embedding(max_cols + 2, d, padding_idx=0)
80
+ self.cell_type_embedding = nn.Embedding(NUM_CELL_TYPES + 1, d, padding_idx=0)
81
+
82
+ # (b) Table structure bias: one scalar per attention head, for each relation
83
+ self.row_attn_bias = nn.Parameter(torch.zeros(self.seq_tr_nhead))
84
+ self.col_attn_bias = nn.Parameter(torch.zeros(self.seq_tr_nhead))
85
+ self.header_attn_bias = nn.Parameter(torch.zeros(self.seq_tr_nhead))
86
+
87
+ # (d) CTP head
88
+ self.ctp_head = nn.Sequential(
89
+ nn.Linear(d, d // 2),
90
+ nn.GELU(),
91
+ BertLayerNorm(d // 2, eps=1e-6),
92
+ nn.Linear(d // 2, CTP_NUM_LABELS),
93
+ )
94
+
95
+ self.apply(init_bert_params)
96
+
97
+ # ── Position bias override (safe for rel_pos_arr=None) ───────────────────
98
+
99
+ def get_position_attn_bias(self, rel_pos_arr, seq_len, batch_size, device):
100
+ # When rel_pos_arr is None (no POS-aware dictionary available),
101
+ # skip that term rather than crashing.
102
+ tupe_bias = self.get_tupe_rel_pos_bias(seq_len, device) if self.use_tupe_rel_pos_bias else None
103
+
104
+ weight = self.pos_ln(self.pos.weight[:seq_len + 1, :])
105
+ pos_q = self.pos_q_linear(weight).view(seq_len + 1, self.seq_tr_nhead, -1).transpose(0, 1) * self.pos_scaling
106
+ pos_k = self.pos_k_linear(weight).view(seq_len + 1, self.seq_tr_nhead, -1).transpose(0, 1)
107
+ abs_pos_bias = torch.bmm(pos_q, pos_k.transpose(1, 2))
108
+
109
+ cls_2_other = abs_pos_bias[:, 0, 0]
110
+ other_2_cls = abs_pos_bias[:, 1, 1]
111
+ abs_pos_bias = abs_pos_bias[:, 1:, 1:]
112
+ abs_pos_bias[:, :, 0] = other_2_cls.view(-1, 1)
113
+ abs_pos_bias[:, 0, :] = cls_2_other.view(-1, 1)
114
+
115
+ if tupe_bias is not None:
116
+ abs_pos_bias = abs_pos_bias + tupe_bias
117
+
118
+ abs_pos_bias = abs_pos_bias.unsqueeze(0).expand(batch_size, -1, -1, -1).reshape(-1, seq_len, seq_len)
119
+
120
+ if self.use_pos_aware_rel_pos_bias and rel_pos_arr is not None:
121
+ abs_pos_bias = abs_pos_bias + self.get_pos_aware_rel_pos_bias(rel_pos_arr, seq_len)
122
+
123
+ return abs_pos_bias
124
+
125
+ # ── Table structure bias ──────────────────────────────────────────────────
126
+
127
+ def get_table_structure_bias(
128
+ self,
129
+ row_ids_padded: torch.Tensor, # (N, S) β€” padded row ids
130
+ col_ids_padded: torch.Tensor, # (N, S)
131
+ seq_len: int,
132
+ batch_size: int,
133
+ ) -> torch.Tensor:
134
+ """
135
+ Returns (N*H, S, S) tensor to be added to the position attn_bias.
136
+ row_ids_padded == 1 is the header row.
137
+ """
138
+ H = self.seq_tr_nhead
139
+ # (N, S, S) boolean: do positions i and j share the same row / col?
140
+ row_match = (row_ids_padded.unsqueeze(2) == row_ids_padded.unsqueeze(1)).float()
141
+ col_match = (col_ids_padded.unsqueeze(2) == col_ids_padded.unsqueeze(1)).float()
142
+ # Header: either token i or token j is in the header row (row_id == 1)
143
+ is_header = (row_ids_padded == 1) # (N, S)
144
+ header_match = (is_header.unsqueeze(2) | is_header.unsqueeze(1)).float() # (N, S, S)
145
+
146
+ # Broadcast head-specific scalars: (H,) β†’ (H, 1, 1)
147
+ rb = self.row_attn_bias.view(H, 1, 1)
148
+ cb = self.col_attn_bias.view(H, 1, 1)
149
+ hb = self.header_attn_bias.view(H, 1, 1)
150
+
151
+ # (N, 1, S, S) * (H, 1, 1) broadcast β†’ (N, H, S, S)
152
+ bias = (
153
+ rb * row_match.unsqueeze(1) +
154
+ cb * col_match.unsqueeze(1) +
155
+ hb * header_match.unsqueeze(1)
156
+ )
157
+ return bias.reshape(batch_size * H, seq_len, seq_len).contiguous()
158
+
159
+ # ── Forward ───────────────────────────────────────────────────────────────
160
+
161
+ @custom_fwd
162
+ def forward(self, args, rel_pos_arr,
163
+ tokens_lengths, input_sequence_lengths,
164
+ pos_tags, stems, afsets, affixes,
165
+ row_ids=None, col_ids=None, cell_types=None):
166
+ """
167
+ All parameters identical to KinyaBERT_MorphoEncoder.forward() plus:
168
+ row_ids, col_ids, cell_types β€” flat tensors of length L (total tokens).
169
+ """
170
+ device = stems.device
171
+
172
+ # ── Tier 1: Morpho transformer (unchanged from KinyaBERT) ─────────────
173
+ x_embed = None
174
+ if self.num_pos_m_embeddings > 0:
175
+ xm_pos1 = self.m1_pos_embedding(pos_tags)
176
+ x_embed = torch.unsqueeze(xm_pos1, 0)
177
+ if self.num_pos_m_embeddings > 1:
178
+ xm_pos2 = self.m2_pos_embedding(pos_tags)
179
+ x_embed = torch.cat((x_embed, torch.unsqueeze(xm_pos2, 0)), 0)
180
+ if self.num_pos_m_embeddings > 2:
181
+ xm_pos3 = self.m3_pos_embedding(pos_tags)
182
+ x_embed = torch.cat((x_embed, torch.unsqueeze(xm_pos3, 0)), 0)
183
+ if self.num_stem_m_embeddings > 0:
184
+ xm_stem = self.m_stem_embedding(stems)
185
+ xm_stem = torch.unsqueeze(xm_stem, 0)
186
+ x_embed = torch.cat((x_embed, xm_stem), 0) if x_embed is not None else xm_stem
187
+ if args.use_afsets and afsets is not None:
188
+ xm_afset = self.m_afset_embedding(afsets)
189
+ xm_afset = torch.unsqueeze(xm_afset, 0)
190
+ x_embed = torch.cat((x_embed, xm_afset), 0) if x_embed is not None else xm_afset
191
+
192
+ m_masks_padded = None
193
+ has_morphemes = False
194
+ morpho_input = None
195
+
196
+ if args.use_morpho_encoder:
197
+ afx = [a for a in affixes.split(tokens_lengths) if a.numel() > 0]
198
+ afx_padded = pad_sequence(afx, batch_first=False) if afx else torch.empty(0, device=device)
199
+ if afx_padded.nelement() > 0:
200
+ has_morphemes = True
201
+ xm_affix = self.m_affix_embedding(afx_padded)
202
+ x_embed = torch.cat((x_embed, xm_affix), 0) if x_embed is not None else xm_affix
203
+ m_masks = [torch.zeros(x + self.tot_morpho_idx, dtype=torch.bool, device=device)
204
+ for x in tokens_lengths]
205
+ m_masks_padded = pad_sequence(m_masks, batch_first=True, padding_value=1)
206
+
207
+ if x_embed is not None and args.use_morpho_encoder:
208
+ morpho_out = self.morpho_transformer_encoder(x_embed, src_key_padding_mask=m_masks_padded)
209
+ if self.tot_morpho_idx > 0 and self.use_affix_bow_m_embedding:
210
+ heads = morpho_out[:self.tot_morpho_idx]
211
+ bow = torch.sum(morpho_out[self.tot_morpho_idx:], 0, keepdim=True) if has_morphemes \
212
+ else torch.zeros((1, stems.size(0), self.morpho_dim), device=device)
213
+ morpho_input = torch.cat((heads, bow), 0)
214
+ elif self.tot_morpho_idx > 0:
215
+ morpho_input = morpho_out[:self.tot_morpho_idx]
216
+ elif self.use_affix_bow_m_embedding:
217
+ morpho_input = torch.sum(morpho_out[self.tot_morpho_idx:], 0, keepdim=True) if has_morphemes \
218
+ else torch.zeros((1, stems.size(0), self.morpho_dim), device=device)
219
+ elif self.use_affix_bow_m_embedding:
220
+ morpho_input = torch.zeros((1, stems.size(0), self.morpho_dim), device=device)
221
+
222
+ # ── Build input to sequence transformer ──────────────────────────────
223
+ input_sequences = self.s_stem_embedding(stems) # (L, stem_dim)
224
+
225
+ if morpho_input is not None:
226
+ morpho_input = morpho_input.permute(1, 0, 2).contiguous()
227
+ L = morpho_input.size(0)
228
+ morpho_input = morpho_input.view(L, -1)
229
+ input_sequences = torch.cat((morpho_input, input_sequences), 1) # (L, seq_tr_d_model)
230
+
231
+ lists = input_sequences.split(input_sequence_lengths, 0)
232
+ tr_padded = pad_sequence(lists, batch_first=False) # (S, N, d)
233
+
234
+ seq_len = tr_padded.size(0)
235
+ batch_size = tr_padded.size(1)
236
+
237
+ # ── Tabular additive embeddings ───────────────────────────────────────
238
+ row_ids_padded = None
239
+ col_ids_padded = None
240
+ if row_ids is not None:
241
+ row_lists = row_ids.split(input_sequence_lengths, 0)
242
+ row_ids_padded = pad_sequence(row_lists, batch_first=True, padding_value=0) # (N, S)
243
+ col_lists = col_ids.split(input_sequence_lengths, 0)
244
+ col_ids_padded = pad_sequence(col_lists, batch_first=True, padding_value=0)
245
+ ct_lists = cell_types.split(input_sequence_lengths, 0)
246
+ ct_padded = pad_sequence(ct_lists, batch_first=True, padding_value=0)
247
+
248
+ # (N, S, d) β†’ (S, N, d)
249
+ row_embed = self.row_embedding(row_ids_padded).permute(1, 0, 2)
250
+ col_embed = self.col_embedding(col_ids_padded).permute(1, 0, 2)
251
+ ct_embed = self.cell_type_embedding(ct_padded).permute(1, 0, 2)
252
+ tr_padded = tr_padded + row_embed + col_embed + ct_embed
253
+
254
+ # ── Position bias (TUPE + POS-aware) β€” unchanged ─────────────────────
255
+ abs_pos_bias = self.get_position_attn_bias(rel_pos_arr, seq_len, batch_size, device)
256
+
257
+ # ── Table structure bias ──────────────────────────────────────────────
258
+ if row_ids_padded is not None and not getattr(args, 'no_bias', False):
259
+ table_bias = self.get_table_structure_bias(
260
+ row_ids_padded, col_ids_padded, seq_len, batch_size
261
+ )
262
+ abs_pos_bias = abs_pos_bias + table_bias
263
+
264
+ # ── Tier 2: Sequence transformer ──────────────────────────────────────
265
+ masks = [torch.zeros(x, dtype=torch.bool, device=device) for x in input_sequence_lengths]
266
+ masks_padded = pad_sequence(masks, batch_first=True, padding_value=1)
267
+
268
+ transformer_output = self.seq_transformer_encoder(
269
+ tr_padded,
270
+ attn_bias=abs_pos_bias,
271
+ src_key_padding_mask=masks_padded,
272
+ ) # (S, N, d)
273
+
274
+ return transformer_output
275
+
276
+ # ── CTP prediction ────────────────────────────────────────────────────────
277
+
278
+ def predict_ctp(self, hidden_states: torch.Tensor,
279
+ ctp_positions: torch.Tensor) -> torch.Tensor:
280
+ """
281
+ hidden_states: (S, N, d)
282
+ ctp_positions: (num_ctp,) β€” flat indices into the (N*S) token space
283
+ Returns: (num_ctp, CTP_NUM_LABELS) logits
284
+ """
285
+ flat = hidden_states.permute(1, 0, 2).reshape(-1, self.seq_tr_d_model)
286
+ selected = torch.index_select(flat, 0, ctp_positions)
287
+ return self.ctp_head(selected)
288
+
289
+
290
+ # ── TabuLM (full model with all prediction heads) ─────────────────────────────
291
+
292
+ class TabuLM(nn.Module):
293
+ """Full TabuLM pre-training model."""
294
+
295
+ def __init__(self, args,
296
+ num_stems, num_afsets, num_pos_tags, num_affixes,
297
+ num_rel_pos_dict_size,
298
+ num_pos_m_embeddings, num_stem_m_embeddings,
299
+ use_affix_bow_m_embedding,
300
+ use_pos_aware_rel_pos_bias, use_tupe_rel_pos_bias,
301
+ max_seq_len=512,
302
+ morpho_dim=128, stem_dim=256,
303
+ morpho_tr_nhead=4, morpho_tr_nlayers=4,
304
+ morpho_tr_dim_feedforward=512, morpho_tr_dropout=0.1,
305
+ morpho_tr_activation='gelu',
306
+ seq_tr_nhead=12, seq_tr_nlayers=12,
307
+ seq_tr_dim_feedforward=3072, seq_tr_dropout=0.1,
308
+ seq_tr_activation='gelu',
309
+ layernorm_epsilon=1e-6,
310
+ tupe_rel_pos_bins=32, tupe_max_rel_pos=128,
311
+ max_rows=MAX_ROWS, max_cols=MAX_COLS):
312
+ super().__init__()
313
+
314
+ self.encoder = TabuLM_Encoder(
315
+ args,
316
+ num_stems, num_afsets, num_pos_tags, num_affixes,
317
+ num_rel_pos_dict_size,
318
+ num_pos_m_embeddings, num_stem_m_embeddings,
319
+ use_affix_bow_m_embedding,
320
+ use_pos_aware_rel_pos_bias, use_tupe_rel_pos_bias,
321
+ max_seq_len=max_seq_len,
322
+ morpho_dim=morpho_dim, stem_dim=stem_dim,
323
+ morpho_tr_nhead=morpho_tr_nhead, morpho_tr_nlayers=morpho_tr_nlayers,
324
+ morpho_tr_dim_feedforward=morpho_tr_dim_feedforward,
325
+ morpho_tr_dropout=morpho_tr_dropout, morpho_tr_activation=morpho_tr_activation,
326
+ seq_tr_nhead=seq_tr_nhead, seq_tr_nlayers=seq_tr_nlayers,
327
+ seq_tr_dim_feedforward=seq_tr_dim_feedforward,
328
+ seq_tr_dropout=seq_tr_dropout, seq_tr_activation=seq_tr_activation,
329
+ tupe_rel_pos_bins=tupe_rel_pos_bins, tupe_max_rel_pos=tupe_max_rel_pos,
330
+ max_rows=max_rows, max_cols=max_cols,
331
+ )
332
+
333
+ d = self.encoder.seq_tr_d_model
334
+
335
+ from morpho_model import MorphoHeadPredictor
336
+ self.head_predictor = MorphoHeadPredictor(
337
+ args,
338
+ stem_embedding_weights=self.encoder.s_stem_embedding.weight,
339
+ afset_embedding_weights=(
340
+ self.encoder.m_afset_embedding.weight
341
+ if hasattr(self.encoder, 'm_afset_embedding') else
342
+ torch.zeros(1, morpho_dim)
343
+ ),
344
+ affix_embedding_weights=(
345
+ self.encoder.m_affix_embedding.weight
346
+ if hasattr(self.encoder, 'm_affix_embedding') else
347
+ torch.zeros(1, morpho_dim)
348
+ ),
349
+ tr_d_model=d,
350
+ tr_dropout=seq_tr_dropout,
351
+ layernorm_epsilon=layernorm_epsilon,
352
+ )
353
+
354
+ def forward(self, args, rel_pos_arr,
355
+ tokens_lengths, input_sequence_lengths,
356
+ pos_tags, stems, afsets, affixes,
357
+ row_ids, col_ids, cell_types,
358
+ predicted_tokens_idx, predicted_tokens_affixes_idx,
359
+ predicted_stems, predicted_afsets, predicted_affixes_prob,
360
+ mcr_tokens_idx=None, mcr_stems=None,
361
+ ctp_positions=None, ctp_labels=None):
362
+
363
+ hidden = self.encoder(
364
+ args, rel_pos_arr, tokens_lengths, input_sequence_lengths,
365
+ pos_tags, stems, afsets, affixes,
366
+ row_ids=row_ids, col_ids=col_ids, cell_types=cell_types,
367
+ )
368
+
369
+ # ── Standard KinyaBERT losses (stem + afset + affix) ──────────────────
370
+ n_stems = self.head_predictor.stem_decoder.weight.size(0)
371
+ predicted_stems = predicted_stems.clamp(0, n_stems - 1)
372
+ loss, stem_loss, afset_loss, affix_loss = self.head_predictor(
373
+ args, hidden,
374
+ predicted_tokens_idx, predicted_tokens_affixes_idx,
375
+ predicted_stems, predicted_afsets, predicted_affixes_prob,
376
+ )
377
+
378
+ # ── MCR loss β€” reuse stem decoder on cell-masked tokens ───────────────
379
+ mcr_loss = torch.tensor(0.0, device=loss.device)
380
+ if mcr_tokens_idx is not None and mcr_stems is not None and mcr_tokens_idx.numel() > 0:
381
+ flat = hidden.permute(1, 0, 2).reshape(-1, hidden.size(-1))
382
+ mcr_hidden = torch.index_select(flat, 0, mcr_tokens_idx)
383
+ mcr_state = self.head_predictor.stem_transform(mcr_hidden)
384
+ mcr_scores = self.head_predictor.stem_decoder(mcr_state) + self.head_predictor.stem_decoder_bias
385
+ n_stem_cls = mcr_scores.size(1)
386
+ mcr_stems_clamped = mcr_stems.clamp(0, n_stem_cls - 1)
387
+ mcr_loss = F.nll_loss(F.log_softmax(mcr_scores, dim=1), mcr_stems_clamped)
388
+ loss = loss + mcr_loss
389
+
390
+ # ── CTP loss ──────────────────────────────────────────────────────────
391
+ ctp_loss = torch.tensor(0.0, device=loss.device)
392
+ if ctp_positions is not None and ctp_labels is not None and ctp_positions.numel() > 0:
393
+ ctp_logits = self.encoder.predict_ctp(hidden, ctp_positions)
394
+ n_ctp_cls = ctp_logits.size(1)
395
+ ctp_labels_clamped = ctp_labels.clamp(0, n_ctp_cls - 1)
396
+ ctp_loss = F.cross_entropy(ctp_logits, ctp_labels_clamped)
397
+ loss = loss + ctp_loss
398
+
399
+ return loss, stem_loss, afset_loss, affix_loss, mcr_loss, ctp_loss
400
+
401
+ def state_dict(self, *args, **kwargs):
402
+ return super().state_dict(*args, **kwargs)
403
+
404
+ def load_state_dict(self, *args, **kwargs):
405
+ return super().load_state_dict(*args, **kwargs)
406
+
407
+
408
+ # ── Factory ───────────────────────────────────────────────────────────────────
409
+
410
+ def tabulm_base(kb_vocab, affix_set_vocab, morpho_rel_pos_dict,
411
+ device, args, saved_model_file=None):
412
+ """
413
+ Construct TabuLM-base model. Mirrors kinyabert_base() from morpho_model.py.
414
+ All default hyperparameters match KinyaBERT-base so a KinyaBERT checkpoint
415
+ can be used to warm-start the encoder (tabular components start from scratch).
416
+ """
417
+ num_stems = len(kb_vocab.reduced_stem_vocab) + 1
418
+ num_afsets = (len(affix_set_vocab.affix_set_vocab_idx) + 1) if affix_set_vocab is not None else 0
419
+ num_pos_tags = len(kb_vocab.pos_tag_vocab) + 1
420
+ num_affixes = len(kb_vocab.affix_vocab) + 1
421
+ num_rel_pos_dict_size = (len(morpho_rel_pos_dict) + 1) if morpho_rel_pos_dict is not None else 0
422
+
423
+ model = TabuLM(
424
+ args,
425
+ num_stems=num_stems,
426
+ num_afsets=num_afsets,
427
+ num_pos_tags=num_pos_tags,
428
+ num_affixes=num_affixes,
429
+ num_rel_pos_dict_size=num_rel_pos_dict_size,
430
+ num_pos_m_embeddings=args.num_pos_m_embeddings,
431
+ num_stem_m_embeddings=args.num_stem_m_embeddings,
432
+ use_affix_bow_m_embedding=args.use_affix_bow_m_embedding,
433
+ use_pos_aware_rel_pos_bias=args.use_pos_aware_rel_pos_bias,
434
+ use_tupe_rel_pos_bias=args.use_tupe_rel_pos_bias,
435
+ max_seq_len=args.max_seq_len,
436
+ morpho_dim=args.morpho_dim,
437
+ stem_dim=args.stem_dim,
438
+ morpho_tr_nhead=args.morpho_tr_nhead,
439
+ morpho_tr_nlayers=args.morpho_tr_nlayers,
440
+ morpho_tr_dim_feedforward=args.morpho_tr_dim_feedforward,
441
+ morpho_tr_dropout=args.morpho_tr_dropout,
442
+ seq_tr_nhead=args.seq_tr_nhead,
443
+ seq_tr_nlayers=args.seq_tr_nlayers,
444
+ seq_tr_dim_feedforward=args.seq_tr_dim_feedforward,
445
+ seq_tr_dropout=args.seq_tr_dropout,
446
+ layernorm_epsilon=args.layernorm_epsilon,
447
+ ).to(device)
448
+
449
+ if saved_model_file is not None:
450
+ state = torch.load(saved_model_file, map_location=device)
451
+ # Warm-start from KinyaBERT checkpoint (ignore tabular-specific keys)
452
+ own_state = model.state_dict()
453
+ pretrained = state.get('model_state_dict', state)
454
+ matched, skipped = 0, 0
455
+ for k, v in pretrained.items():
456
+ # Map KinyaBERT encoder keys to TabuLM encoder keys
457
+ new_k = k.replace('module.encoder.', 'encoder.')
458
+ if new_k not in own_state:
459
+ new_k = k
460
+ if new_k in own_state and own_state[new_k].shape == v.shape:
461
+ own_state[new_k].copy_(v)
462
+ matched += 1
463
+ else:
464
+ skipped += 1
465
+ print(f'[tabulm_base] warm-started {matched} layers, skipped {skipped}')
466
+ model.load_state_dict(own_state)
467
+
468
+ return model
code/train_tabulm.py ADDED
@@ -0,0 +1,379 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # TabuLM β€” pre-training script
2
+ # Extends train_exploratory_distributed_model.py for tabular data.
3
+ # Logs: STEM | AFSET | AFFIX | MCR | CTP losses separately.
4
+
5
+ from __future__ import print_function, division
6
+
7
+ import gc
8
+ import math
9
+ import os
10
+ import random
11
+ from datetime import datetime
12
+ from shutil import copyfile
13
+
14
+ import numpy as np
15
+ import progressbar
16
+ import psutil
17
+ import torch
18
+ import torch.distributed as dist
19
+ import torch.multiprocessing as mp
20
+ from torch.nn.parallel import DistributedDataParallel as DDP
21
+ from torch.utils.data import DataLoader
22
+
23
+
24
+ def time_now():
25
+ return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
26
+
27
+ def date_now():
28
+ return datetime.now().strftime("%Y-%m-%d")
29
+
30
+ def set_random_seeds(seed=0):
31
+ torch.manual_seed(seed)
32
+ torch.backends.cudnn.deterministic = True
33
+ torch.backends.cudnn.benchmark = False
34
+ np.random.seed(seed)
35
+ random.seed(seed)
36
+
37
+
38
+ def train_loop(args, rank, scaler, device, data_loader,
39
+ model, optimizer, lr_scheduler, save_file_path,
40
+ accumulation_steps, loop, num_loops, bar,
41
+ total_steps, total_loss,
42
+ stem_loss_acc, afset_loss_acc, affix_loss_acc,
43
+ mcr_loss_acc, ctp_loss_acc,
44
+ save_every=50):
45
+
46
+ from tabular_data_loaders import tabulm_model_forward
47
+
48
+ for batch_idx, data_item in enumerate(data_loader):
49
+ if scaler is not None:
50
+ with torch.cuda.amp.autocast():
51
+ loss, sl, al, fxl, ml, cl = tabulm_model_forward(
52
+ args, data_item, model, device,
53
+ model.module.encoder.tot_num_affixes if hasattr(model, 'module')
54
+ else model.encoder.tot_num_affixes,
55
+ )
56
+ loss = loss / accumulation_steps
57
+ scaler.scale(loss).backward()
58
+ else:
59
+ loss, sl, al, fxl, ml, cl = tabulm_model_forward(
60
+ args, data_item, model, device,
61
+ model.module.encoder.tot_num_affixes if hasattr(model, 'module')
62
+ else model.encoder.tot_num_affixes,
63
+ )
64
+ loss = loss / accumulation_steps
65
+ loss.backward()
66
+
67
+ total_loss += loss.item()
68
+ stem_loss_acc += sl.item() / accumulation_steps
69
+ afset_loss_acc += al.item() / accumulation_steps
70
+ affix_loss_acc += fxl.item() / accumulation_steps
71
+ mcr_loss_acc += ml.item() / accumulation_steps
72
+ ctp_loss_acc += cl.item() / accumulation_steps
73
+ total_steps += 1
74
+
75
+ if (total_steps % accumulation_steps) == 0:
76
+ if scaler is not None:
77
+ scaler.step(optimizer)
78
+ scaler.update()
79
+ else:
80
+ optimizer.step()
81
+ optimizer.zero_grad()
82
+ lr_scheduler.step()
83
+
84
+ if rank == 0:
85
+ print(
86
+ time_now(),
87
+ f'Loop:{loop}/{num_loops}',
88
+ f'Batch:{batch_idx+1}/{len(data_loader)}',
89
+ f'TOTAL:{total_loss:.4f}',
90
+ f'STEM:{stem_loss_acc:.4f}',
91
+ f'AFSET:{afset_loss_acc:.4f}',
92
+ f'AFFIX:{affix_loss_acc:.4f}',
93
+ f'MCR:{mcr_loss_acc:.4f}',
94
+ f'CTP:{ctp_loss_acc:.4f}',
95
+ f'LR:{lr_scheduler.get_lr():.8f}',
96
+ f'iter:{lr_scheduler.num_iters}',
97
+ )
98
+ bar.update(lr_scheduler.num_iters)
99
+
100
+ total_loss = stem_loss_acc = afset_loss_acc = 0.0
101
+ affix_loss_acc = mcr_loss_acc = ctp_loss_acc = 0.0
102
+
103
+ if rank == 0 and (((loop + 1) % save_every) == 0 or loop == num_loops - 1):
104
+ if os.path.exists(save_file_path):
105
+ copyfile(save_file_path, save_file_path + '_prev_checkpoint.pt')
106
+
107
+ _model = model.module if hasattr(model, 'module') else model
108
+ _model.eval()
109
+ torch.save({
110
+ 'iter': total_steps,
111
+ 'model_state_dict': model.state_dict(),
112
+ 'optimizer_state_dict': optimizer.state_dict(),
113
+ 'lr_scheduler_state_dict': lr_scheduler.state_dict(),
114
+ 'loop': loop,
115
+ 'num_loops': num_loops,
116
+ }, save_file_path)
117
+ _model.train()
118
+
119
+ return (total_steps, total_loss,
120
+ stem_loss_acc, afset_loss_acc, affix_loss_acc,
121
+ mcr_loss_acc, ctp_loss_acc)
122
+
123
+
124
+ def train_fn(rank, args):
125
+ import youtokentome as yttm
126
+ from morpho_learning_rates import AnnealingLR
127
+ from morpho_data_loaders import KBVocab, AffixSetVocab
128
+ from tabular_data_loaders import TabularKBCorpusDataset, tabular_collate_wrapper
129
+ from tabulm_model import tabulm_base
130
+
131
+ USE_GPU = args.gpus > 0 and torch.cuda.is_available()
132
+
133
+ device = torch.device('cuda' if USE_GPU else 'cpu')
134
+
135
+ if USE_GPU:
136
+ dist.init_process_group('nccl', init_method='env://',
137
+ world_size=args.world_size, rank=rank)
138
+ torch.cuda.set_device(rank)
139
+ scaler = torch.cuda.amp.GradScaler()
140
+ else:
141
+ dist.init_process_group('gloo', init_method='env://',
142
+ world_size=args.world_size, rank=rank)
143
+ scaler = None
144
+
145
+ home = args.home_path
146
+
147
+ bpe = yttm.BPE(model=home + 'data/BPE-30k.mdl')
148
+
149
+ kb_vocab = KBVocab()
150
+ kb_vocab.load_state_dict(torch.load(home + 'data/kb_vocab_state_dict_2021-02-07.pt'))
151
+
152
+ affix_set_vocab = None
153
+ if args.use_afsets:
154
+ affix_set_vocab = AffixSetVocab(
155
+ reduced_affix_dict_file=home + f'data/reduced_affix_dict_{args.afset_dict_size}.csv',
156
+ reduced_affix_dict_map_file=home + f'data/reduced_affix_dict_map_{args.afset_dict_size}.csv',
157
+ )
158
+
159
+ morpho_rel_pos_dict = None
160
+ morpho_rel_pos_dmax = 5
161
+ if args.use_pos_aware_rel_pos_bias:
162
+ rel_pos_file = home + 'data/morpho_rel_pos_dict_2021-03-24.pt'
163
+ if os.path.exists(rel_pos_file):
164
+ saved = torch.load(rel_pos_file)
165
+ morpho_rel_pos_dict = saved['morpho_rel_pos_dict']
166
+ morpho_rel_pos_dmax = saved['morpho_rel_pos_dmax']
167
+ else:
168
+ print(f'[WARN] morpho_rel_pos_dict not found, disabling pos_aware_rel_pos_bias')
169
+ args.use_pos_aware_rel_pos_bias = False
170
+ args.use_pos_aware_rel = False
171
+
172
+ num_iters = args.num_iters
173
+ warmup_iter = args.warmup_iter
174
+ peak_lr = args.peak_lr
175
+ wd = args.wd
176
+
177
+ if rank == 0:
178
+ print(time_now(), 'Building TabuLM model ...')
179
+
180
+ model = tabulm_base(kb_vocab, affix_set_vocab, morpho_rel_pos_dict,
181
+ device, args, saved_model_file=args.exploratory_model_load)
182
+
183
+ if USE_GPU:
184
+ model = DDP(model, device_ids=[rank], find_unused_parameters=True)
185
+ try:
186
+ import apex
187
+ optimizer = apex.optimizers.FusedLAMB(
188
+ model.parameters(), lr=peak_lr, betas=(0.9, 0.98),
189
+ eps=1e-06, weight_decay=wd,
190
+ )
191
+ except ImportError:
192
+ from lamb import Lamb
193
+ optimizer = Lamb(model.parameters(), lr=peak_lr, betas=(0.9, 0.98),
194
+ eps=1e-06, weight_decay=wd)
195
+ else:
196
+ from lamb import Lamb
197
+ model = DDP(model, device_ids=[])
198
+ optimizer = Lamb(model.parameters(), lr=peak_lr, betas=(0.9, 0.98),
199
+ eps=1e-06, weight_decay=wd)
200
+
201
+ lr_scheduler = AnnealingLR(optimizer,
202
+ start_lr=peak_lr,
203
+ warmup_iter=warmup_iter,
204
+ num_iters=num_iters,
205
+ decay_style='linear',
206
+ last_iter=0)
207
+
208
+ # ── Resume from checkpoint if provided ────────────────────────────────────
209
+ resume_file = getattr(args, 'resume_checkpoint', None)
210
+ curr_loops = 0
211
+ total_steps = 0
212
+ total_loss = stem_loss_acc = afset_loss_acc = 0.0
213
+ affix_loss_acc = mcr_loss_acc = ctp_loss_acc = 0.0
214
+
215
+ if resume_file and os.path.exists(resume_file):
216
+ if rank == 0:
217
+ print(f'[RESUME] Loading checkpoint from {resume_file}')
218
+ ckpt = torch.load(resume_file, map_location=device)
219
+ # Strip DDP 'module.' prefix if present
220
+ state = ckpt['model_state_dict']
221
+ if all(k.startswith('module.') for k in state):
222
+ state = {k[len('module.'):]: v for k, v in state.items()}
223
+ _model = model.module if hasattr(model, 'module') else model
224
+ _model.load_state_dict(state, strict=False)
225
+ optimizer.load_state_dict(ckpt['optimizer_state_dict'])
226
+ lr_scheduler.load_state_dict(ckpt['lr_scheduler_state_dict'])
227
+ curr_loops = ckpt.get('loop', 0) + 1
228
+ total_steps = ckpt.get('iter', 0)
229
+ if rank == 0:
230
+ print(f'[RESUME] Resuming from loop {curr_loops}, iter {total_steps}')
231
+
232
+ csv_dir = args.tabulm_csv_dir if hasattr(args, 'tabulm_csv_dir') and args.tabulm_csv_dir \
233
+ else home + 'data/tables/'
234
+
235
+ num_train_loops = math.ceil(
236
+ num_iters * args.accumulation_steps / args.number_of_load_batches
237
+ )
238
+
239
+ save_path = (
240
+ home + f'data/tabulm_model_{date_now()}'
241
+ f'_pos@{args.num_pos_m_embeddings}'
242
+ f'_stem@{args.num_stem_m_embeddings}'
243
+ f'_afsets@{args.use_afsets}@{args.afset_dict_size}'
244
+ f'{getattr(args, "ablation_tag", "")}.pt'
245
+ )
246
+
247
+ total_params = sum(p.numel() for p in model.parameters())
248
+ trainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
249
+
250
+ if rank == 0:
251
+ print('─' * 60)
252
+ print(f'Total params: {total_params:,} Trainable: {trainable_params:,}')
253
+ print(f'Saving to: {save_path}')
254
+ print(f'CSV tables from: {csv_dir}')
255
+ print(f'num_iters={num_iters} warmup={warmup_iter} loops={num_train_loops}')
256
+ print(f'batch_size={args.batch_size} accum={args.accumulation_steps}')
257
+ print(f'peak_lr={peak_lr} wd={wd}')
258
+ print('─' * 60)
259
+
260
+ model.train()
261
+ model.zero_grad()
262
+
263
+ with progressbar.ProgressBar(
264
+ initial_value=lr_scheduler.num_iters,
265
+ max_value=lr_scheduler.end_iter,
266
+ redirect_stdout=True,
267
+ ) as bar:
268
+ if rank == 0:
269
+ bar.update(lr_scheduler.num_iters)
270
+
271
+ for loop in range(curr_loops, num_train_loops):
272
+ if rank == 0:
273
+ print(time_now(), 'Loading tabular dataset ...')
274
+
275
+ dataset = TabularKBCorpusDataset(
276
+ args, kb_vocab, affix_set_vocab, bpe,
277
+ csv_dir=csv_dir,
278
+ max_batch_items=args.number_of_load_batches * args.batch_size,
279
+ max_seq_len=512,
280
+ rank=rank,
281
+ )
282
+
283
+ data_loader = DataLoader(
284
+ dataset,
285
+ batch_size=args.batch_size,
286
+ collate_fn=tabular_collate_wrapper,
287
+ shuffle=True,
288
+ )
289
+
290
+ if rank == 0:
291
+ print(time_now(), 'Memory:', psutil.virtual_memory())
292
+
293
+ (total_steps, total_loss,
294
+ stem_loss_acc, afset_loss_acc, affix_loss_acc,
295
+ mcr_loss_acc, ctp_loss_acc) = train_loop(
296
+ args, rank, scaler, device, data_loader,
297
+ model, optimizer, lr_scheduler, save_path,
298
+ args.accumulation_steps, loop, num_train_loops, bar,
299
+ total_steps, total_loss,
300
+ stem_loss_acc, afset_loss_acc, affix_loss_acc,
301
+ mcr_loss_acc, ctp_loss_acc,
302
+ save_every=getattr(args, 'save_every', 50),
303
+ )
304
+
305
+ if rank == 0:
306
+ print(time_now(), f'{loop+1}/{num_train_loops} loops complete')
307
+
308
+ del data_loader, dataset
309
+ gc.collect()
310
+
311
+
312
+ def main():
313
+ import argparse
314
+ from morpho_common import setup_common_args
315
+
316
+ # Pull out --resume-checkpoint and ablation flags before setup_common_args sees sys.argv
317
+ import sys
318
+ resume_checkpoint = None
319
+ no_mcr = False
320
+ no_ctp = False
321
+ no_tabular_emb = False
322
+ no_bias = False
323
+ ablation_tag = ''
324
+ filtered = []
325
+ i = 0
326
+ while i < len(sys.argv[1:]):
327
+ arg = sys.argv[1:][i]
328
+ if arg == '--resume-checkpoint':
329
+ resume_checkpoint = sys.argv[1:][i + 1]
330
+ i += 2
331
+ elif arg.startswith('--resume-checkpoint='):
332
+ resume_checkpoint = arg.split('=', 1)[1]
333
+ i += 1
334
+ elif arg == '--no-mcr':
335
+ no_mcr = True
336
+ ablation_tag += '_noMCR'
337
+ i += 1
338
+ elif arg == '--no-ctp':
339
+ no_ctp = True
340
+ ablation_tag += '_noCTP'
341
+ i += 1
342
+ elif arg == '--no-tabular-emb':
343
+ no_tabular_emb = True
344
+ ablation_tag += '_noTabEmb'
345
+ i += 1
346
+ elif arg == '--no-bias':
347
+ no_bias = True
348
+ ablation_tag += '_noBias'
349
+ i += 1
350
+ else:
351
+ filtered.append(arg)
352
+ i += 1
353
+ sys.argv = [sys.argv[0]] + filtered
354
+
355
+ args = setup_common_args()
356
+ args.resume_checkpoint = resume_checkpoint
357
+ args.no_mcr = no_mcr
358
+ args.no_ctp = no_ctp
359
+ args.no_tabular_emb = no_tabular_emb
360
+ args.no_bias = no_bias
361
+ args.ablation_tag = ablation_tag
362
+
363
+ # Extra args not in morpho_common.setup_common_args
364
+ if not hasattr(args, 'tabulm_csv_dir'):
365
+ args.tabulm_csv_dir = os.environ.get('TABULM_CSV_DIR', None)
366
+ if not hasattr(args, 'resume_checkpoint'):
367
+ args.resume_checkpoint = None
368
+
369
+ os.environ['MASTER_ADDR'] = 'localhost'
370
+ os.environ['MASTER_PORT'] = os.environ.get('MASTER_PORT', '29602')
371
+
372
+ if args.gpus == 0:
373
+ args.world_size = 1
374
+
375
+ mp.spawn(train_fn, nprocs=args.world_size, args=(args,))
376
+
377
+
378
+ if __name__ == '__main__':
379
+ main()