Add TabuLM training and evaluation code
Browse files- code/__pycache__/eval_baselines.cpython-310.pyc +0 -0
- code/__pycache__/eval_llm_baseline.cpython-310.pyc +0 -0
- code/__pycache__/finetune_tabqa.cpython-310.pyc +0 -0
- code/__pycache__/kinyabert_utils.cpython-310.pyc +0 -0
- code/__pycache__/lamb.cpython-310.pyc +0 -0
- code/__pycache__/morpho_common.cpython-310.pyc +0 -0
- code/__pycache__/morpho_data_loaders.cpython-310.pyc +0 -0
- code/__pycache__/morpho_learning_rates.cpython-310.pyc +0 -0
- code/__pycache__/morpho_model.cpython-310.pyc +0 -0
- code/__pycache__/morpho_stub.cpython-310.pyc +0 -0
- code/__pycache__/morpho_transformer.cpython-310.pyc +0 -0
- code/__pycache__/tabular_data_loaders.cpython-310.pyc +0 -0
- code/__pycache__/tabular_serializer.cpython-310.pyc +0 -0
- code/__pycache__/tabulm_model.cpython-310.pyc +0 -0
- code/adamw.py +109 -0
- code/eval_baselines.py +423 -0
- code/eval_compare.py +377 -0
- code/eval_llm_agg_fewshot.py +141 -0
- code/eval_llm_baseline.py +261 -0
- code/finetune_tableqa.py +244 -0
- code/finetune_tabqa.py +641 -0
- code/kinyabert_utils.py +23 -0
- code/lamb.py +148 -0
- code/morpho_common.py +391 -0
- code/morpho_data_loaders.py +958 -0
- code/morpho_learning_rates.py +78 -0
- code/morpho_model.py +850 -0
- code/morpho_stub.py +82 -0
- code/morpho_transformer.py +642 -0
- code/tabular_data_loaders.py +676 -0
- code/tabular_serializer.py +175 -0
- code/tabulm_model.py +468 -0
- code/train_tabulm.py +379 -0
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()
|