kltn-experiments / 20_sampled_instead_all_27 /20_sampled_instead_all_27.py
SS3M's picture
Upload 20_sampled_instead_all_27's state dict
1d59734 verified
Raw
History Blame Contribute Delete
80.4 kB
# %% [code]
get_ipython().system('pip install evaluate seqeval underthesea positional-encodings[pytorch] pytorch-crf')
# %% [code]
import warnings
warnings.filterwarnings('ignore')
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import Dataset, TensorDataset, DataLoader
import torch.nn.functional as F
import albumentations as albu
from transformers import AutoTokenizer, AutoModel
import torch.distributed as dist
from torch.nn.parallel import DistributedDataParallel as DDP
from positional_encodings.torch_encodings import PositionalEncoding1D
from torchcrf import CRF
from sklearn.metrics import f1_score
from sklearn.preprocessing import MinMaxScaler, StandardScaler
from scipy.spatial.transform import Rotation as R
from sklearn.model_selection import KFold, StratifiedGroupKFold, GroupKFold, StratifiedKFold
from sklearn.metrics import precision_recall_fscore_support
from timm.utils import ModelEmaV3
import timm
import os
import gc
import json
from pathlib import Path
import pickle
from tqdm.auto import tqdm
import copy
import numpy as np
import pandas as pd
import polars as pl
from PIL import Image
import time
from tqdm import tqdm
from matplotlib import pyplot as plt
import seaborn as sns
from multiprocessing import Manager as MemoryManager
from functools import lru_cache
import shutil
import glob
import cv2
import random
import re
import joblib
import math
from huggingface_hub import HfApi, snapshot_download
import evaluate
from underthesea import word_tokenize as vi_tokenize_tool
import spacy
en_tokenize_tool = spacy.load("en_core_web_sm")
from collections import defaultdict, Counter
# %% [code]
# Global config
SEEDS = [26092004]
topk = 1
nfolds = 5
only_fold_idx = 0
test_only = 0
debug_only = 0
# Config thư mục
dataset = 'kltn/only_entities' # conll003, ontonotes, phoner, vietbio, vietmed, vimed, kltn/only_entities, kltn/raw
root_dir = f'/kaggle/input/notebooks/sambui22022517/kltn-data/{dataset}' ## Thư mục chứa file train, val, test
train_dir = f'{root_dir}'
# val_dir = f'{root_dir}/val'
test_dir = f'{root_dir}'
# Config checkpoints
# Config training
epochs = 18 if not debug_only else 2
batch_size = 32
device = "cuda" if torch.cuda.is_available() else "cpu"
# # Thêm biến toàn cục nào đó vào đây
repo_name = 'SS3M/kltn-experiments'
state_dict_save_name = "20_sampled_instead_all_27"
checkpoints_dir = state_dict_save_name
pretrained_dir = "/kaggle/working"
os.makedirs(f'{checkpoints_dir}', exist_ok=True)
backbone_model_name = "bert-base-uncased" if dataset in ["conll003", "ontonotes"] else "vinai/phobert-base"
word_tokenize = lambda text: [token.text for token in en_tokenize_tool(text)] if dataset == dataset in ["conll003", "ontonotes"] else vi_tokenize_tool(text)
max_len_dict = {
'kltn/raw': 256,
'kltn/only_entities': 68,
'conll003': 46,
'ontonotes': 61,
'phoner': 68,
'vietbio': 125,
'vietmed': 36,
'vimed': 100,
}
zero_entities_rate_dict = {
'kltn/raw': 1000,
'kltn/only_entities': 0.2,
'conll003': 1000, # mean keep all zero-entities samples
'ontonotes': 1000,
'phoner': 1000,
'vietbio': 1000,
'vietmed': 1000,
'vimed': 1000,
}
max_len = max_len_dict[dataset]
max_n_parts = 3 if dataset in ['kltn/raw'] else 1
max_span_len = 10
zero_entities_rate = zero_entities_rate_dict[dataset]
topk_spans = 200
# Trainer
trainer_params = {
"training_time": "00:11:30:00",
"eval_mode": "max",
"topk": topk,
"save_name": state_dict_save_name,
"save_best": True,
"save_last": True,
"device": device,
"logging": True,
"logging_file": True,
"checkpoints_dir": checkpoints_dir,
"early_stopping": 30,
"eval_from_ratio": 0.4,
"eval_every": 1,
"schedule_in_step": False,
"use_ema": True,
"ema_from_ratio": 0.3,
"ema_decay": 0.9995,
"max_grad_norm": 200.0,
"return_best": True,
"return_last": True,
}
# Memory
train_memory_params = {
'n_spans': topk_spans,
'max_len': max_len,
'max_n_parts': max_n_parts,
'max_span_len': max_span_len,
'weight_rate': 3,
}
val_memory_params = {
'n_spans': topk_spans,
'max_len': max_len,
'max_n_parts': max_n_parts,
'max_span_len': max_span_len,
'weight_rate': 3,
}
# Data Loader
def seed_worker(worker_id):
worker_seed = torch.initial_seed() % 2**32
np.random.seed(worker_seed)
random.seed(worker_seed)
train_loader_params = {
'batch_size': batch_size,
'shuffle': True,
'pin_memory':True,
'num_workers': 2,
'drop_last': False,
'worker_init_fn': seed_worker,
'persistent_workers': False,
}
val_loader_params = {
'batch_size': batch_size,
'shuffle': False,
'pin_memory':True,
'num_workers': 1,
'drop_last': False,
'worker_init_fn': seed_worker,
'persistent_workers': False,
}
# Model
model_params = {
'backbone_model_name': backbone_model_name,
'max_span_len': max_span_len,
'topk_spans': topk_spans,
'keep_neighbor': 2,
}
# Loss Func
loss_func_params = {
'lambda_ce': 1.0,
}
eval_func_params = {}
# Optim
optim_params = {
'name': 'AdamW',
'lr': 1e-4,
'weight_decay': 1e-4,
}
scheduler_params = {
'name': 'CosineAnnealingLR',
'T_max': 20, # Số epoch để hoàn thành một chu kỳ giảm LR
'eta_min': 1e-6 # Learning rate nhỏ nhất trong chu kỳ
}
# %% [code]
def set_seed(seed=42):
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
torch.cuda.manual_seed(seed)
torch.cuda.manual_seed_all(seed) # if using multi-GPU
torch.use_deterministic_algorithms(False)
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False
os.environ['PYTHONHASHSEED'] = str(seed)
# %% [code]
class CustomLoss(nn.Module):
def __init__(self, lambda_ce=1.0):
super().__init__()
self.lambda_ce = lambda_ce
self.ce = nn.CrossEntropyLoss(
ignore_index=-100,
reduction='none'
)
def forward(
self,
logits, labels, weights, # weights: (B, N)
start_logits, start_labels,
end_logits, end_labels,
):
# =====================================
# SPAN LOSS
# =====================================
B, N, C = logits.shape
flat_logits = logits.reshape(-1, C)
flat_labels = labels.reshape(-1)
flat_weights = weights.reshape(-1)
valid_mask = flat_labels != -100
if valid_mask.any():
ce_loss = self.ce(flat_logits, flat_labels) # (B*N,)
ce_loss = ce_loss[valid_mask]
valid_weights = flat_weights[valid_mask]
loss = (ce_loss * valid_weights).sum() / valid_weights.sum().clamp(min=1e-8)
else:
loss = logits.sum() * 0.0
# =====================================
# START LOSS
# =====================================
B, L, C = start_logits.shape
start_logits_flat = start_logits.reshape(B * L, C)
start_labels_flat = start_labels.reshape(-1)
start_loss = self.ce(start_logits_flat, start_labels_flat)
start_valid = start_labels_flat != -100
if start_valid.any():
start_loss = start_loss[start_valid].mean()
else:
start_loss = logits.sum() * 0.0
# =====================================
# END LOSS
# =====================================
end_logits_flat = end_logits.reshape(B * L, C)
end_labels_flat = end_labels.reshape(-1)
end_loss = self.ce(end_logits_flat, end_labels_flat)
end_valid = end_labels_flat != -100
if end_valid.any():
end_loss = end_loss[end_valid].mean()
else:
end_loss = logits.sum() * 0.0
return {
"total": loss + start_loss + end_loss,
"span_loss": loss,
"start_loss": start_loss,
"end_loss": end_loss,
}
# %% [code]
## Viết eval_fn vào đây
# Bỏ hết eval_fn và trọng số vào đây
class CustomEvalFn(nn.Module):
def __init__(self):
super().__init__()
def compute_f1(self, tp, fp, fn):
precision = tp / (tp + fp + 1e-8)
recall = tp / (tp + fn + 1e-8)
f1 = 2 * precision * recall / (precision + recall + 1e-8)
return precision, recall, f1
def forward(self, pred, gold):
pred_set = set(pred)
gold_set = set(gold)
tp = len(pred_set & gold_set)
fp = len(pred_set - gold_set)
fn = len(gold_set - pred_set)
precision, recall, f1 = self.compute_f1(tp, fp, fn)
return {
f"precision": precision,
f"recall": recall,
f"f1": f1,
}
class SpanErrorAnalyzer:
def __init__(self, pad_token_id=0):
self.pad_token_id = pad_token_id
# ===== helper =====
def _to_set(self, data):
"""
data: list of (b, tuple(ids))
-> dict[b] = set(tuple(ids))
"""
res = defaultdict(set)
for b, ids in data:
ids = tuple([i for i in ids if i != self.pad_token_id])
if len(ids) > 0:
res[b].add(ids)
return res
def _iou(self, a, b):
"""
a, b: tuple(ids)
"""
set_a, set_b = set(a), set(b)
inter = len(set_a & set_b)
union = len(set_a | set_b)
if union == 0:
return 0.0
return inter / union
def _boundary_error(self, pred, gold):
"""
đo lệch boundary dựa trên overlap prefix/suffix
"""
# left match
left = 0
for i in range(min(len(pred), len(gold))):
if pred[i] == gold[i]:
left += 1
else:
break
# right match
right = 0
for i in range(1, min(len(pred), len(gold)) + 1):
if pred[-i] == gold[-i]:
right += 1
else:
break
return {
"left_match": left,
"right_match": right,
"pred_len": len(pred),
"gold_len": len(gold),
}
# ===== main =====
def analyze(self, preds, golds):
pred_map = self._to_set(preds)
gold_map = self._to_set(golds)
all_batches = set(pred_map.keys()) | set(gold_map.keys())
stats = Counter()
detailed_errors = []
for b in all_batches:
pset = pred_map.get(b, set())
gset = gold_map.get(b, set())
matched_gold = set()
# ===== check predictions =====
for p in pset:
if p in gset:
stats["exact_match"] += 1
matched_gold.add(p)
else:
# tìm gold gần nhất
best_iou = 0
best_g = None
for g in gset:
iou = self._iou(p, g)
if iou > best_iou:
best_iou = iou
best_g = g
if best_iou > 0:
stats["partial_match"] += 1
boundary = self._boundary_error(p, best_g)
detailed_errors.append({
"type": "boundary_error",
"batch": b,
"pred": p,
"gold": best_g,
"iou": best_iou,
**boundary
})
else:
if b not in gold_map:
stats["no_event_sample"] += 1
err_type = "no_event_sample"
else:
stats["completely_wrong"] += 1
err_type = "completely_wrong"
detailed_errors.append({
"type": err_type,
"batch": b,
"pred": p
})
# ===== check missing =====
for g in gset:
if g not in matched_gold:
# check if any pred overlaps
overlap = any(self._iou(p, g) > 0 for p in pset)
if overlap:
stats["miss_with_overlap"] += 1
else:
stats["miss"] += 1
detailed_errors.append({
"type": "miss",
"batch": b,
"gold": g
})
return {
"summary": {
"exact_match": (stats["exact_match"], stats["exact_match"] / len(preds)),
"partial_match": (stats["partial_match"], stats["partial_match"] / len(preds)),
"no_event_sample": (stats["no_event_sample"], stats["no_event_sample"] / len(preds)),
"completely_wrong": (stats["completely_wrong"], stats["completely_wrong"] / len(preds)),
"miss": (stats["miss"], stats["miss"] / len(golds)),
"miss_with_overlap": (stats["miss_with_overlap"], stats["miss_with_overlap"] / len(golds)),
},
"details": detailed_errors
}
# %% [code]
class DataParallelProxy(nn.DataParallel):
def __getattr__(self, name):
try:
return super().__getattr__(name)
except AttributeError:
attr = getattr(self.module, name)
if callable(attr):
def wrapper(*args, **kwargs):
return self._parallel_apply_method(
name,
*args,
**kwargs
)
return wrapper
return attr
# =========================================================
# parallel custom method
# =========================================================
def _parallel_apply_method(self, method_name, *inputs, **kwargs):
if not self.device_ids:
return getattr(self.module, method_name)(*inputs, **kwargs)
inputs_scattered, kwargs_scattered = self.scatter(
inputs,
kwargs,
self.device_ids
)
replicas = self.replicate(
self.module,
self.device_ids[:len(inputs_scattered)]
)
outputs = self.parallel_apply(
[getattr(replica, method_name) for replica in replicas],
inputs_scattered,
kwargs_scattered
)
return self._custom_gather(
outputs,
self.output_device
)
# =========================================================
# OVERRIDE FORWARD GATHER
# =========================================================
def gather(self, outputs, output_device):
return self._custom_gather(
outputs,
output_device
)
# =========================================================
# recursive gather
# =========================================================
def _custom_gather(self, outputs, output_device):
first = outputs[0]
# =====================================================
# tensor
# =====================================================
if torch.is_tensor(first):
return self._gather_tensor(
outputs,
output_device
)
# =====================================================
# tuple
# =====================================================
if isinstance(first, tuple):
return tuple(
self._custom_gather(
list(items),
output_device
)
for items in zip(*outputs)
)
# =====================================================
# list
# =====================================================
if isinstance(first, list):
# list[tensor]
if len(first) > 0 and torch.is_tensor(first[0]):
return self._gather_tensor_list(
outputs,
output_device
)
merged = []
for out in outputs:
merged.extend(out)
return merged
# =====================================================
# dict
# =====================================================
if isinstance(first, dict):
return {
k: self._custom_gather(
[o[k] for o in outputs],
output_device
)
for k in first.keys()
}
# =====================================================
# fallback
# =====================================================
return outputs
# =========================================================
# gather tensor with auto pad
# =========================================================
def _gather_tensor(self, tensors, output_device):
# move same device first
tensors = [
t.to(output_device)
for t in tensors
]
# =====================================================
# fast path
# =====================================================
try:
return torch.cat(tensors, dim=0)
except RuntimeError:
pass
# =====================================================
# auto max shape
# =====================================================
max_shape = list(tensors[0].shape)
for t in tensors[1:]:
for d in range(len(max_shape)):
max_shape[d] = max(
max_shape[d],
t.shape[d]
)
# =====================================================
# pad tensors
# =====================================================
padded = []
for t in tensors:
pad = []
# reverse order for F.pad
for d in reversed(range(len(max_shape))):
# never pad batch dim
if d == 0:
pad.extend([0, 0])
continue
diff = max_shape[d] - t.shape[d]
pad.extend([0, diff])
t = F.pad(t, pad)
padded.append(t)
return torch.cat(padded, dim=0)
# =========================================================
# list[tensor]
# =========================================================
def _gather_tensor_list(self, outputs, output_device):
merged = []
for out in outputs:
merged.extend(out)
return self._gather_tensor(
merged,
output_device
)
# %% [code]
## Viết cấu trúc model vào đây
def get_span_reprs(hidden, spans):
"""
Args:
hidden: (B, L, H)
spans: (B, N, 2)
Return:
span_repr: (B, N, 4*H)
"""
B, N, _ = spans.shape
H = hidden.size(-1)
batch_idx = torch.arange(B, device=hidden.device).unsqueeze(1)
start_idx = spans[..., 0] # (B, N)
end_idx = spans[..., 1] # (B, N)
start_h = hidden[batch_idx, start_idx]
end_h = hidden[batch_idx, end_idx]
span_repr = torch.cat(
[start_h, end_h, end_h - start_h, end_h * start_h],
dim=-1
)
return span_repr
def filter_spans(
start_logits, # (B, L, C)
end_logits, # (B, L, C)
attn_mask, # (B, L)
max_span_length=10,
topk_spans=20,
keep_neighbor=1
):
"""
Return:
spans: (B, N, 2)
N là số span lớn nhất trong batch sau expand + remove duplicate.
Padding bằng (0, 0)
"""
device = start_logits.device
B, L, C = start_logits.shape
# Clone
start_logits = start_logits.detach()
end_logits = end_logits.detach()
# Prob
prob_s = F.softmax(start_logits, dim=-1) # (B, L, C)
prob_e = F.softmax(end_logits, dim=-1) # (B, L, C)
all_batch_spans = []
for b in range(B):
valid_len = int(attn_mask[b].sum().item())
candidate_spans = []
# Enumerate all valid spans
for s in range(1, valid_len):
max_e = min(valid_len - 1, s + max_span_length - 1)
for e in range(s, max_e + 1):
length = e - s + 1
score = (
(1.0 - prob_s[b, s, 0].item())
+ (1.0 - prob_e[b, e, 0].item())
- (length / max_span_length)
)
candidate_spans.append((score, s, e))
# Top-k
candidate_spans = sorted(
candidate_spans,
key=lambda x: x[0],
reverse=True
)[:topk_spans]
# Expand neighbors
final_spans = set()
for _, s, e in candidate_spans:
for ds in range(-keep_neighbor, keep_neighbor + 1):
for de in range(-keep_neighbor, keep_neighbor + 1):
ns = s + ds
ne = e + de
if ns <= 0:
continue
if ne >= valid_len:
continue
if ns > ne:
continue
if (ne - ns + 1) > max_span_length:
continue
final_spans.add((ns, ne))
final_spans = sorted(list(final_spans))
if len(final_spans) == 0:
final_spans = [(0, 0)]
all_batch_spans.append(final_spans)
# Padding
max_num_spans = max(len(x) for x in all_batch_spans)
padded_spans = []
for spans in all_batch_spans:
pad_size = max_num_spans - len(spans)
spans = spans + [(0, 0)] * pad_size
padded_spans.append(spans)
spans = torch.tensor(
padded_spans,
dtype=torch.long,
device=device
) # (B, N, 2)
return spans
class MLP(nn.Module):
def __init__(self, in_size, hid_size, out_size):
super().__init__()
self.mlp = nn.Sequential(
nn.Linear(in_size, hid_size),
nn.ReLU(),
nn.Linear(hid_size, out_size)
)
def forward(self, x):
return self.mlp(x)
class IEModel(nn.Module):
def __init__(self, backbone_model_name, num_labels, max_span_len, topk_spans, keep_neighbor):
super().__init__()
self.max_span_len = max_span_len
self.topk_spans = topk_spans
self.keep_neighbor = keep_neighbor
self.encoder = AutoModel.from_pretrained(backbone_model_name)
hidden_size = self.encoder.config.hidden_size
self.start_classifier = MLP(hidden_size, hidden_size, num_labels)
self.end_classifier = MLP(hidden_size, hidden_size, num_labels)
self.span_classifier = MLP(4*hidden_size, hidden_size, num_labels)
def encode(self, input_ids, attention_mask):
B, n_parts, L = input_ids.shape
input_ids = input_ids.view(-1, L)
attention_mask = attention_mask.view(-1, L)
outputs = self.encoder(input_ids=input_ids, attention_mask=attention_mask)
hidden_states = outputs.last_hidden_state # B * n_parts, L, H
hidden_states = hidden_states.view(B, n_parts, L, -1).reshape(B, n_parts*L, -1) # B, L, H
attention_mask = attention_mask.view(B, n_parts, L).reshape(B, n_parts*L) # B, L, H
return hidden_states, attention_mask
def get_token_logits(self, hidden_states):
start_logits = self.start_classifier(hidden_states) # B, N, classes
end_logits = self.end_classifier(hidden_states) # B, N, classes
return start_logits, end_logits
def get_logits(self, span_reprs):
logits = self.span_classifier(span_reprs) # N, classes
return logits
def forward(self, input_ids, attention_mask, spans=None):
hidden_states, attention_mask = self.encode(input_ids, attention_mask)
start_logits, end_logits = self.get_token_logits(hidden_states)
if spans is None:
spans = filter_spans(start_logits, end_logits, attention_mask, self.max_span_len, self.topk_spans, self.keep_neighbor)
span_reprs = get_span_reprs(hidden_states, spans)
logits = self.get_logits(span_reprs)
return start_logits, end_logits, logits, spans
def test_model():
model = DataParallelProxy(IEModel(backbone_model_name, 7, 10, 100, 2)).to(device)
model.eval()
total_params = sum(p.numel() for p in model.parameters())
print(f"Total params: {total_params:,}")
vocab_size = model.module.encoder.config.vocab_size
max_len = model.module.encoder.config.max_position_embeddings
bz = 32
i = torch.randint(0, vocab_size, (bz, 5, 10)).to(device)
a = torch.ones(bz, 5, 10).to(device)
s = torch.ones(bz, 3, 2, dtype=torch.long).to(device)
with torch.no_grad():
r = model(i, a)
if type(r) == tuple:
print([r[i].shape if type(r[i]) == type(torch.Tensor()) else len(r[i]) for i in range(len(r))])
else:
print(r.shape)
test_model()
# %% [code]
def configure_optimizers(network, optim_params, scheduler_params):
try:
optim_params = copy.copy(optim_params)
scheduler_params = copy.copy(scheduler_params)
optim_name = optim_params.pop('name')
scheduler_name = scheduler_params.pop('name')
optimizer_cls = globals().get(optim_name) or getattr(optim, optim_name, None)
scheduler_cls = globals().get(scheduler_name) or getattr(optim.lr_scheduler, scheduler_name, None)
if optimizer_cls is None:
raise ValueError(f"Optimizer '{optim_name}' is not available!")
optimizer = optimizer_cls(network.parameters(), **optim_params)
scheduler = None
if scheduler_params and scheduler_cls: # Chỉ tạo scheduler nếu có tham số
scheduler = scheduler_cls(optimizer, **scheduler_params)
return optimizer, scheduler
except KeyError as e:
raise ValueError(f"Missing {e} in config!!")
def freeze(self, model):
model.eval()
for param in model.parameters():
param.requires_grad = False
def unfreeze(self, model):
model.train()
for param in model.parameters():
param.requires_grad = True
def reduce_batch_size(loader, ratio=0.5):
new_bs = max(1, int(loader.batch_size * ratio))
shuffle = isinstance(loader.sampler, RandomSampler)
new_loader = DataLoader(
dataset=loader.dataset,
batch_size=new_bs,
shuffle=shuffle,
sampler=None if shuffle else loader.sampler,
num_workers=loader.num_workers,
collate_fn=loader.collate_fn,
pin_memory=loader.pin_memory,
drop_last=loader.drop_last,
timeout=loader.timeout,
worker_init_fn=loader.worker_init_fn,
multiprocessing_context=loader.multiprocessing_context,
generator=loader.generator,
prefetch_factor=loader.prefetch_factor if loader.num_workers > 0 else None,
persistent_workers=loader.persistent_workers,
pin_memory_device=loader.pin_memory_device
)
return new_loader
def list_to_tuple(x):
if isinstance(x, (list, tuple)):
return tuple(list_to_tuple(i) for i in x)
return x
def fmt(x):
if isinstance(x, float):
return round(x, 5)
if isinstance(x, dict):
return {k: fmt(v) for k, v in x.items()}
if isinstance(x, list):
return [fmt(v) for v in x]
return x
class ModelEmaV3Proxy(ModelEmaV3):
def __getattr__(self, name):
try:
return super().__getattr__(name)
except AttributeError:
# tránh recursion
module = object.__getattribute__(self, "module")
return getattr(module, name)
def align(
all_spans, # (B, N, 2)
pred_spans, # (B, M, 2)
obj, # (B, N)
pad_value=0
):
"""
Return:
align_obj: (B, M)
"""
B, N, _ = all_spans.shape
M = pred_spans.shape[1]
device = all_spans.device
# =========================================================
# Compare spans
# =========================================================
# (B, M, 1, 2)
pred_expand = pred_spans.unsqueeze(2)
# (B, 1, N, 2)
all_expand = all_spans.unsqueeze(1)
# (B, M, N)
match = (pred_expand == all_expand).all(dim=-1)
# =========================================================
# Gather obj
# =========================================================
# default = pad_value
align_obj = torch.full(
(B, M),
pad_value,
dtype=obj.dtype,
device=device
)
# matched index
has_match = match.any(dim=-1) # (B, M)
matched_idx = match.float().argmax(dim=-1) # (B, M)
gathered = torch.gather(
obj,
dim=1,
index=matched_idx
) # (B, M)
align_obj[has_match] = gathered[has_match]
# =========================================================
# Force padding spans -> pad_value
# =========================================================
pad_mask = (pred_spans == 0).all(dim=-1)
align_obj[pad_mask] = pad_value
return align_obj
def extract_spans(
all_spans, # (B, N, 2)
all_label, # (B, N)
pred_spans # (B, M, 2)
):
"""
Return:
pred_list:
[(bidx, (s, e)), ...]
gold_list:
[(bidx, (s, e)), ...]
"""
# =========================================================
# Gold spans
# =========================================================
gold_mask = all_label > 0 # (B, N)
gold_indices = gold_mask.nonzero(as_tuple=False)
gold_list = [
(
int(b),
(
int(all_spans[b, n, 0]),
int(all_spans[b, n, 1])
)
)
for b, n in gold_indices
]
# =========================================================
# Pred spans
# =========================================================
pred_mask = (pred_spans > 0).all(dim=-1) # (B, M)
pred_indices = pred_mask.nonzero(as_tuple=False)
pred_list = [
(
int(b),
(
int(pred_spans[b, m, 0]),
int(pred_spans[b, m, 1])
)
)
for b, m in pred_indices
]
return gold_list, pred_list
def extract_entities(
input_ids, # (B, L)
start_logits, # (B, L, C)
end_logits, # (B, L, C)
logits, # (B, N, C)
pred_spans, # (B, N, 2)
id2label,
alpha=1.0
):
"""
Return:
[
(batch_idx, ([token_ids], label_name)),
...
]
"""
# =========================================================
# Log-softmax
# =========================================================
start_logprob = F.log_softmax(start_logits, dim=-1) # (B, L, C)
end_logprob = F.log_softmax(end_logits, dim=-1) # (B, L, C)
span_logprob = F.log_softmax(logits, dim=-1) # (B, N, C)
# =========================================================
# Gather start/end score for pred spans
# =========================================================
start_idx = pred_spans[..., 0] # (B, N)
end_idx = pred_spans[..., 1] # (B, N)
B, N = start_idx.shape
C = logits.shape[-1]
# (B, N, C)
start_score = torch.gather(
start_logprob,
dim=1,
index=start_idx.unsqueeze(-1).expand(-1, -1, C)
)
# (B, N, C)
end_score = torch.gather(
end_logprob,
dim=1,
index=end_idx.unsqueeze(-1).expand(-1, -1, C)
)
# =========================================================
# Ensemble score
# =========================================================
score = (
span_logprob
+ alpha * (start_score + end_score)
) # (B, N, C)
# =========================================================
# Predict label
# =========================================================
pred_labels = score.argmax(dim=-1) # (B, N)
keep = (
(pred_labels > 0) &
(start_idx > 0) &
(end_idx > 0)
)
# =========================================================
# Extract entities
# =========================================================
results = []
for bidx in range(B):
valid_idxes = keep[bidx].nonzero(as_tuple=False).squeeze(-1)
for idx in valid_idxes:
lb = pred_labels[bidx, idx]
s, e = pred_spans[bidx, idx].tolist()
token_ids = input_ids[bidx, s:e+1].tolist()
results.append(
(
bidx,
(
token_ids,
id2label[lb.item()]
)
)
)
return results
class Trainer:
def __init__(
self, training_time="00:11:30:00", eval_mode="max", topk=1, save_name="network", save_best=True, save_last=False, max_grad_norm=200.0,
logging=0, logging_file=False, checkpoints_dir="", early_stopping=False, eval_from_ratio=-1, eval_every=1, device='cpu',
schedule_in_step=True, use_ema=True, ema_from_ratio=-1, ema_decay=0.999, return_best=True, return_last=True
):
self.ema_net = None
self.training_time = self._time_str_to_seconds(training_time)
self.mode = eval_mode
self.topk = topk
self.device = device
self.logging = logging if logging < epochs else 1
self.logging_file = logging_file
self.checkpoints_dir = checkpoints_dir
self.early_stopping = early_stopping
self.eval_from_ratio = eval_from_ratio
self.eval_every = eval_every
self.save_name = save_name
self.save_best = save_best
self.save_last = save_last
self.return_best = return_best
self.return_last = return_last
self.max_grad_norm = max_grad_norm
self.schedule_in_step = schedule_in_step
self.use_ema = use_ema
self.ema_from_ratio = ema_from_ratio
self.ema_decay = ema_decay
self.best_stage = [[float('-inf') if self.mode == 'max' else float('inf'), None, None]]
self.grad_scaler = torch.amp.GradScaler(self.device, init_scale=1024.0)
def fit(self, network, optimizer, scheduler, loss_fn, epochs, train_loader, val_loader=None, eval_fn=None, start_epoch=1, start_training_time=None, id2label=None):
if eval_fn is None:
if self.mode == "max":
eval_fn = lambda *x: -loss_fn(*x)
else:
eval_fn = lambda *x: loss_fn(*x)
if torch.cuda.device_count() > 1:
network = DataParallelProxy(network)
network = network.to(self.device)
if not start_training_time:
start_training_time = time.time()
start_ema = int(epochs * self.ema_from_ratio)
start_eval = int(epochs * self.eval_from_ratio)
if val_loader is None:
print(f'[Trainer CallBack] 📢 Không có Val Set, không thể đánh giá và Early Stopping!')
else:
model_to_use_str = 'mô hình EMA' if self.use_ema else 'mô hình gốc'
start_model_update_str = f'Bắt đầu cập nhật EMA từ epoch {start_epoch + start_ema}!' if self.use_ema else ''
print(f'[Trainer CallBack] 📢 Đánh giá bằng {model_to_use_str} từ epoch {start_epoch + start_eval}!', start_model_update_str)
training_log = {}
for epoch in range(start_epoch, epochs+start_epoch):
if self.use_ema and self.ema_net is None and epoch - start_epoch >= start_ema:
self.ema_net = ModelEmaV3Proxy(network, self.ema_decay, device=self.device)
try:
teaching_rate = math.cos(math.pi / 2 * epoch / epochs)
train_loss_epoch, train_loss_epoch_dict = self._train_epoch(network, train_loader, optimizer, scheduler, loss_fn, teaching_rate)
logging_dict = {'lr': [group['lr'] for group in optimizer.param_groups], 'train_loss': train_loss_epoch}
logging_dict.update(train_loss_epoch_dict)
if val_loader is not None and epoch - start_epoch >= start_eval and (epoch - start_epoch - start_eval) % self.eval_every == 0:
eval_net = self.ema_net.module if (self.use_ema and self.ema_net is not None) else network
val_score, val_score_dict, _ = self._eval_epoch(eval_net, val_loader, eval_fn, id2label)
update = self._update_best_network(eval_net, val_score, epoch)
logging_dict.update({'val_score': val_score, 'best_score': self.best_stage[0][0], 'new_best_model': update})
logging_dict.update(val_score_dict)
if not self.schedule_in_step and scheduler:
scheduler.step()
except RuntimeError as e:
if "out of memory" in str(e).lower():
print(f"[Trainer CallBack] ⚠️ Epoch {epoch}/{epochs}: CUDA Out of Memory! Clearing GPU cache...")
torch.cuda.empty_cache()
gc.collect()
if torch.cuda.is_available():
torch.cuda.synchronize()
print(f"[Trainer CallBack] ✅ Epoch {epoch}/{epochs}: GPU memory cleared")
train_loader = reduce_batch_size(train_loader, ratio=0.5)
if val_loader is not None:
val_loader = reduce_batch_size(val_loader, ratio=0.5)
logging_dict = {'lr': [group['lr'] for group in optimizer.param_groups], 'train_loss': float('inf')}
else:
raise
training_log[epoch] = logging_dict
if self.is_early_stopping(epoch):
print(f'[Trainer CallBack] 📢 Epoch {epoch}/{epochs}: Detect Overfitting! Breaking Training Process...')
break
if self.logging:
if epoch % self.logging == 0:
print(f'[Trainer CallBack] 📢 Epoch {epoch}/{epochs}:', fmt(logging_dict))
else:
print(f'{epoch}...', end=' ')
if self._at_time_limit(start_training_time):
print(f'[Trainer CallBack] ⚠️ Epoch {epoch}/{epochs}: Thời gian training giới hạn là {self.training_time}, hết giờ tại epoch {epoch}/{epochs}')
break
if self.logging_file:
os.makedirs(f'{self.checkpoints_dir}/logs', exist_ok=True)
with open(f"{self.checkpoints_dir}/logs/{self.save_name}_logging.json", "a", encoding="utf-8") as f:
f.write(json.dumps(training_log))
if self.use_ema and self.ema_net is not None:
self._save_state_dict(self.ema_net.module)
else:
self._save_state_dict(network)
print(f'[Trainer CallBack] 📢 Kết thúc training.\n')
best_model, last_model = None, None
eval_net = self.ema_net.module if (self.use_ema and self.ema_net is not None) else network
if self.return_best :
best_model = self.best_stage[0][2] if self.best_stage[0][2] is not None else eval_net.state_dict()
best_model = {k.replace("module.", ""): v.detach().cpu().clone() for k, v in best_model.items()}
if self.return_last:
last_model = eval_net.state_dict()
last_model = {k.replace("module.", ""): v.detach().cpu().clone() for k, v in last_model.items()}
del network
torch.cuda.empty_cache()
gc.collect()
return training_log, best_model, last_model
def _time_str_to_seconds(self, time_str):
days, hours, minutes, seconds = map(int, time_str.split(":"))
return days * 86400 + hours * 3600 + minutes * 60 + seconds
def _update_best_network(self, network, val_score, epoch):
topk = max(1, self.topk)
self.best_stage.append([val_score, epoch, {k: v.detach().cpu().clone() for k, v in network.state_dict().items()}])
self.best_stage = sorted(self.best_stage, reverse=(self.mode == 'max'), key=lambda x: x[0])[:topk]
if val_score in [x[0] for x in self.best_stage]:
return True
return False
def is_early_stopping(self, epoch):
if self.best_stage[0][1] is None:
return False
if not self.early_stopping:
return False
return epoch - self.best_stage[0][1] >= self.early_stopping
def _at_time_limit(self, start_training_time):
return time.time() - start_training_time >= self.training_time
def _save_state_dict(self, network):
if self.topk <= 0:
return
if self.save_best:
for r in range(self.topk):
os.makedirs(f'{self.checkpoints_dir}/r{r+1}s', exist_ok=True)
for rank, (score, epoch, state_dict) in enumerate(self.best_stage):
if state_dict is None:
continue
state_dict = {k.replace("module.", ""): v.detach().cpu().clone() for k, v in state_dict.items()}
torch.save(state_dict, f'{self.checkpoints_dir}/r{rank+1}s/{self.save_name}_r{rank+1}_vs{score:.5f}_{"ema" if self.ema_net is not None else ""}.pth')
if self.save_last:
os.makedirs(f'{self.checkpoints_dir}/lasts', exist_ok=True)
state_dict = {k.replace("module.", ""): v.detach().cpu().clone() for k, v in network.state_dict().items()}
torch.save(state_dict, f'{self.checkpoints_dir}/lasts/{self.save_name}_last_{"ema" if self.ema_net is not None else ""}.pth')
def _train_epoch(self, network, train_loader, optimizer, scheduler, loss_fn, teaching_rate):
network.train()
total_loss = 0
total_loss_dict = {}
for batch_idx, batch in enumerate(train_loader):
optimizer.zero_grad()
with torch.autocast(device_type=self.device, dtype=torch.float16):
loss, loss_dict = self._cal_loss(network, batch, batch_idx, loss_fn, teaching_rate)
for k, v in loss_dict.items():
t = total_loss_dict.get(k, 0)
total_loss_dict[k] = t + v
self.grad_scaler.scale(loss).backward()
self.grad_scaler.unscale_(optimizer)
grad_norm = nn.utils.clip_grad_norm_(network.parameters(), self.max_grad_norm)
# print(grad_norm) # Bỏ cmt dòng này để biết nên chọn max_grad_norm bằng bao nhiêu...
self.grad_scaler.step(optimizer)
self.grad_scaler.update()
if self.schedule_in_step and scheduler:
scheduler.step()
if self.use_ema and self.ema_net is not None:
self.ema_net.update(network)
total_loss += loss
return (total_loss / len(train_loader)).item(), {k: v.item() / len(train_loader) for k, v in total_loss_dict.items()}
def _eval_epoch(self, network, val_loader, eval_fn, id2label):
network.eval()
total_score = 0.0
total_score_dict = {}
object_lists = None # sẽ init sau
with torch.no_grad():
for batch_idx, batch in enumerate(val_loader):
score, score_dict, objects = self._cal_val_score(network, batch, batch_idx, eval_fn, id2label)
total_score += score
for k, v in score_dict.items():
t = total_score_dict.get(k, 0)
total_score_dict[k] = t + v
if objects:
if object_lists is None:
object_lists = [[] for _ in range(len(objects))]
for i, obj in enumerate(objects):
object_lists[i].append(obj.detach())
if object_lists is not None:
object_arrays = [
torch.concat(obj_list, dim=0).cpu().numpy()
for obj_list in object_lists
]
else:
object_arrays = []
return total_score / len(val_loader), {k: v / len(val_loader) for k, v in total_score_dict.items()}, object_arrays
def _cal_loss(self, network, batch, batch_idx, loss_fn, teaching_rate):
# Bạn cần override _cal_loss để tính loss
input_ids = batch['input_ids'].to(self.device)
attention_mask = batch['attention_mask'].to(self.device)
all_spans = batch['all_spans'].to(self.device)
all_labels = batch['all_labels'].to(self.device)
all_weights = batch['all_weights'].to(self.device)
start_labels = batch['start_labels'].to(self.device)
end_labels = batch['end_labels'].to(self.device)
choice = random.random()
if choice < teaching_rate:
start_logits, end_logits, logits, pred_spans = network(input_ids, attention_mask, all_spans)
else:
start_logits, end_logits, logits, pred_spans = network(input_ids, attention_mask)
align_labels = align(all_spans, pred_spans, all_labels, -100)
align_weights = align(all_spans, pred_spans, all_weights, 0)
loss_dict = loss_fn(
logits, align_labels, align_weights,
start_logits, start_labels,
end_logits, end_labels,
)
return loss_dict['total'], loss_dict
def _cal_val_score(self, network, batch, batch_idx, eval_fn, id2label):
# Bạn cần override _cal_val_score để tính val score, list bên cạnh là để trả về y hay pred gì đó (nếu cần)
input_ids = batch['input_ids'].to(self.device)
attention_mask = batch['attention_mask'].to(self.device)
all_spans = batch['all_spans'].to(self.device)
all_labels = batch['all_labels'].to(self.device)
gold_entities = batch['gold_entities']
B, _, _ = input_ids.shape
start_logits, end_logits, logits, pred_spans = network(input_ids, attention_mask)
gold_list, pred_list = extract_spans(all_spans, all_labels, pred_spans)
gold_list = list_to_tuple(gold_list)
pred_list = list_to_tuple(pred_list)
span_score = eval_fn(pred_list, gold_list)['recall']
pred_ids = extract_entities(input_ids.reshape(B, -1), start_logits, end_logits, logits, pred_spans, id2label, alpha=0.5)
pred_ids = list_to_tuple(pred_ids)
gold_ids = list_to_tuple(gold_entities)
score_dict = eval_fn(pred_ids, gold_ids)
score_dict.update({'span_recall': span_score})
return score_dict['f1'] + score_dict['span_recall'], score_dict, []
# %% [code]
class PhoBERTSpanAligner:
def __init__(self, tokenizer, max_len):
self.tokenizer = tokenizer
self.max_len = max_len
# ===== 1. Extract discontinuous spans =====
def extract_spans(self, sample):
entity_spans = []
for event in sample["entities"]:
entity_type = event["label"]
spans = [tuple(event["offset"])]
entity_spans.append({
"spans": spans,
"label": entity_type
})
return entity_spans
# ===== 2. Word offsets =====
def build_word_offsets(self, text, words):
offsets = []
pointer = 0
for word in words:
start = text.find(word, pointer)
end = start + len(word)
offsets.append((start, end))
pointer = end
return offsets
# ===== 3. Char → word =====
def char_span_to_word_span(self, word_offsets, start, end):
start_word = None
end_word = None
for i, (w_start, w_end) in enumerate(word_offsets):
if w_start <= start < w_end:
start_word = i
if w_start < end <= w_end:
end_word = i
return start_word, end_word
# ===== 4. Word → subword =====
def word_to_subword_map(self, words):
mapping = []
subword_index = 1 # <s>
for word in words:
sub_tokens = self.tokenizer.tokenize(word)
start = subword_index
end = subword_index + len(sub_tokens) - 1
mapping.append((start, end))
subword_index += len(sub_tokens)
return mapping
# ===== 5. Span → subword =====
def span_to_subword(self, word_offsets, word_subword_map, spans):
sub_spans = []
for span_start, span_end in spans:
w_start, w_end = self.char_span_to_word_span(
word_offsets, span_start, span_end
)
if w_start is None or w_end is None:
continue
sub_start = word_subword_map[w_start][0]
sub_end = word_subword_map[w_end][1]
sub_spans.append((sub_start, sub_end))
return sub_spans
def extract_valid_spans(self, sub_spans):
valid_spans = []
for s, e in sub_spans:
if s < 0 or e < 0 or s >= self.max_len or e >= self.max_len or s > e:
continue
valid_spans.append((s, e))
return valid_spans
def encode(self, sample):
text = sample["text"]
entities = self.extract_spans(sample)
# ===== 1. Word tokenize =====
words = word_tokenize(text)
sentence = " ".join(words)
# ===== 2. Mapping =====
word_offsets = self.build_word_offsets(text, words)
word_subword_map = self.word_to_subword_map(words)
# ===== 3. Tokenize FULL =====
encoding = self.tokenizer(
sentence,
max_length=self.max_len,
truncation=True,
padding="max_length",
return_tensors="pt"
)
input_ids = encoding["input_ids"][0]
attention_mask = encoding["attention_mask"][0]
# ===== 5. Convert spans =====
entities_gold_spans = []
for ent in entities:
label = ent["label"]
sub_spans = self.span_to_subword(
word_offsets,
word_subword_map,
ent["spans"]
)
valid_spans = self.extract_valid_spans(sub_spans)
if len(valid_spans) == 0:
continue
entities_gold_spans.append((tuple(valid_spans), label))
return {
"input_ids": input_ids,
"attention_mask": attention_mask,
"entities_gold_spans": entities_gold_spans,
}
def generate_spans(attention_mask, max_span_len):
seq_len = attention_mask.sum().item() - 2
spans = []
for i in range(1, seq_len+1):
for j in range(i, min(i+max_span_len, seq_len+1)):
spans.append((i, j))
return spans
def match_gold_labels(
gold_spans, # (N, 2)
gold_labels, # (N,)
pred_spans, # (M, 2)
default_label=-100
):
"""
Return:
pred_labels: (M,)
"""
pred_labels = torch.full(
(pred_spans.size(0),),
default_label,
dtype=gold_labels.dtype,
device=gold_labels.device
)
if gold_spans.size(0) == 0:
return pred_labels
# (M, N)
matched = (pred_spans[:, None, :] == gold_spans[None, :, :]).all(dim=-1)
has_match = matched.any(dim=1)
# lấy index gold đầu tiên match
gold_idx = matched.float().argmax(dim=1)
pred_labels[has_match] = gold_labels[gold_idx[has_match]]
return pred_labels
class KLTNDataset(Dataset):
def __init__(self, all_data, using_idxes, label2id, tokenizer, n_spans, max_len, max_n_parts, max_span_len, weight_rate):
super().__init__()
self.tokenizer = tokenizer
self.aligner = PhoBERTSpanAligner(tokenizer, max_len*max_n_parts)
self.all_data = all_data
self.using_idxes = using_idxes
self.label2id = label2id
self.n_spans = n_spans
self.max_len = max_len
self.max_n_parts = max_n_parts
self.max_span_len = max_span_len
self.weight_rate = weight_rate
def __len__(self):
return len(self.using_idxes)
def span_iou(self, span1, span2):
s1, e1 = span1
s2, e2 = span2
# intersection
inter_left = max(s1, s2)
inter_right = min(e1, e2)
intersection = max(0, inter_right - inter_left + 1)
# lengths
len1 = e1 - s1 + 1
len2 = e2 - s2 + 1
# union
union = len1 + len2 - intersection
if union == 0:
return 0.0
return intersection / union
def get_weights(self, spans, pos_spans):
# spans: (N, 2), pos_spans: (K, 2)
N, K = spans.size(0), pos_spans.size(0)
device = spans.device
# ===== edge case =====
if K == 0:
weights = torch.ones(N, device=device, dtype=torch.float)
return weights
# ===== IoU =====
s1, e1 = spans[:, None, 0], spans[:, None, 1]
s2, e2 = pos_spans[None, :, 0], pos_spans[None, :, 1]
inter_s = torch.maximum(s1, s2)
inter_e = torch.minimum(e1, e2)
inter = (inter_e - inter_s + 1).clamp(min=0)
len1 = (e1 - s1 + 1)
len2 = (e2 - s2 + 1)
union = len1 + len2 - inter
iou = inter / (union + 1e-8) # (N, K)
# ===== weights: IoU=0 -> 1, else 10*IoU =====
max_iou, _ = iou.max(dim=1)
if self.weight_rate is not None:
weights = torch.where(max_iou > 0, 1 + self.weight_rate * max_iou, torch.ones_like(max_iou))
else:
weights = torch.ones_like(max_iou)
return weights
def to_span_label_tensors(self, data, label_map):
if len(data) == 0:
return (
torch.zeros((0, 2), dtype=torch.long),
torch.zeros((0,), dtype=torch.long)
)
spans = torch.tensor([list(spans[0]) for spans, _ in data], dtype=torch.long)
labels = torch.tensor([label_map[label] for _, label in data], dtype=torch.long)
return spans, labels
def __getitem__(self, idx):
ridx = self.using_idxes[idx]
sample = self.all_data[ridx]
result = self.aligner.encode(sample)
input_ids = result["input_ids"].squeeze(0)
attention_mask = result["attention_mask"].squeeze(0)
entities_gold_spans = result["entities_gold_spans"]
# Get all spans
gold_spans = torch.tensor([spans[0] for spans, _ in entities_gold_spans], dtype=torch.long) if entities_gold_spans else torch.empty(0, 2, dtype=torch.long)
gold_labels = torch.tensor([self.label2id[label] for _, label in entities_gold_spans], dtype=torch.long) if entities_gold_spans else torch.empty(0, dtype=torch.long)
all_spans = torch.tensor(generate_spans(attention_mask, self.max_span_len))
all_labels = match_gold_labels(
gold_spans, # (N, 2)
gold_labels, # (N,)
all_spans, # (M, 2)
default_label=0
)
all_weights = self.get_weights(all_spans, gold_spans)
# Sample
num_spans = all_spans.size(0)
if num_spans > self.n_spans:
# ưu tiên giữ positive
pos_mask = all_labels != 0
neg_mask = ~pos_mask
pos_idx = torch.nonzero(pos_mask).squeeze(-1)
neg_idx = torch.nonzero(neg_mask).squeeze(-1)
n_pos = min(len(pos_idx), self.n_spans)
# lấy toàn bộ hoặc random positive
if len(pos_idx) > n_pos:
perm = torch.randperm(len(pos_idx), device=pos_idx.device)
pos_idx = pos_idx[perm[:n_pos]]
remaining = self.n_spans - len(pos_idx)
# sample negative
if remaining > 0 and len(neg_idx) > 0:
perm = torch.randperm(len(neg_idx), device=neg_idx.device)
neg_idx = neg_idx[perm[:remaining]]
keep_idx = torch.cat([pos_idx, neg_idx], dim=0)
else:
keep_idx = pos_idx
# shuffle lại
perm = torch.randperm(len(keep_idx), device=keep_idx.device)
keep_idx = keep_idx[perm]
all_spans = all_spans[keep_idx]
all_labels = all_labels[keep_idx]
all_weights = all_weights[keep_idx]
# Get label
gold_entities = []
start_labels = torch.ones_like(input_ids) * (1-attention_mask) * (-100)
end_labels = torch.ones_like(input_ids) * (1-attention_mask) * (-100)
for spans, label in entities_gold_spans:
s, e = spans[0]
start_labels[s] = self.label2id[f'{label}']
end_labels[e] = self.label2id[f'{label}']
gold_entities.append((tuple(input_ids[s:e+1].tolist()), label))
input_ids = input_ids.reshape(self.max_n_parts, self.max_len)
attention_mask = attention_mask.reshape(self.max_n_parts, self.max_len)
n_valid_parts = math.ceil(attention_mask.sum().item() / self.max_len)
input_ids = input_ids[:n_valid_parts]
attention_mask = attention_mask[:n_valid_parts]
start_labels = start_labels[:n_valid_parts*self.max_len]
end_labels = end_labels[:n_valid_parts*self.max_len]
return {
"input_ids": input_ids,
"attention_mask": attention_mask,
"all_spans": all_spans,
"all_labels": all_labels,
"all_weights": all_weights,
"start_labels": start_labels,
"end_labels": end_labels,
"gold_entities": gold_entities,
}
def _pad_batch(tensor_list, pad_value=0):
"""
tensor_list: list of tensors
mỗi tensor shape: (Nk, n_parts_i, max_len_i)
return:
padded tensor shape: (B, max_Nk, max_n_parts, max_len)
"""
# lấy max toàn batch
max_Nk = max(t.size(0) for t in tensor_list)
max_n_parts = max(t.size(1) for t in tensor_list)
max_len = max(t.size(2) for t in tensor_list)
padded = []
for t in tensor_list:
Nk, n_parts_i, max_len_i = t.shape
# pad chiều n_parts và max_len trước
if n_parts_i < max_n_parts or max_len_i < max_len:
new_t = t.new_full(
(Nk, max_n_parts, max_len),
pad_value
)
new_t[:, :n_parts_i, :max_len_i] = t
t = new_t
# pad chiều Nk
if Nk < max_Nk:
pad_tensor = t.new_full(
(max_Nk - Nk, max_n_parts, max_len),
pad_value
)
t = torch.cat([t, pad_tensor], dim=0)
padded.append(t)
return torch.stack(padded) # (B, max_Nk, max_n_parts, max_len)
def collate_fn(batch):
gold_entities = []
for bidx, b in enumerate(batch):
for entity in b['gold_entities']:
gold_entities.append([bidx, entity])
input_ids = [b["input_ids"].unsqueeze(-1) for b in batch]
attention_mask = [b["attention_mask"].unsqueeze(-1) for b in batch]
all_spans = [b["all_spans"].unsqueeze(-1) for b in batch]
all_labels = [b["all_labels"].unsqueeze(-1).unsqueeze(-1) for b in batch]
all_weights = [b["all_weights"].unsqueeze(-1).unsqueeze(-1) for b in batch]
start_labels = [b["start_labels"].unsqueeze(-1).unsqueeze(-1) for b in batch]
end_labels = [b["end_labels"].unsqueeze(-1).unsqueeze(-1) for b in batch]
# pad theo Nk
input_ids = _pad_batch(input_ids, pad_value=0).squeeze(-1)
attention_mask = _pad_batch(attention_mask, pad_value=0).squeeze(-1)
all_spans = _pad_batch(all_spans, pad_value=0).squeeze(-1)
all_labels = _pad_batch(all_labels, pad_value=-100).squeeze(-1).squeeze(-1)
all_weights = _pad_batch(all_weights, pad_value=0).squeeze(-1).squeeze(-1)
start_labels = _pad_batch(start_labels, pad_value=-100).squeeze(-1).squeeze(-1)
end_labels = _pad_batch(end_labels, pad_value=-100).squeeze(-1).squeeze(-1)
return {
"input_ids": input_ids,
"attention_mask": attention_mask,
"all_spans": all_spans,
"all_labels": all_labels,
"all_weights": all_weights,
"start_labels": start_labels,
"end_labels": end_labels,
"gold_entities": gold_entities,
}
# %% [code]
def shift_bidx(spans, batch_idx):
shifted = []
for bidx, ent in spans:
new_bidx = bidx + batch_idx * batch_size
shifted.append((new_bidx, ent))
return shifted
def refactor_entities(entities, save_dict):
i, c = [], []
for bidx, (ids, lb) in entities:
if (bidx, ids) not in i:
i.append((bidx, ids))
if (bidx, (ids, lb)) not in c:
c.append((bidx, (ids, lb)))
save_dict['Ent-I'].extend(i)
save_dict['Ent-C'].extend(c)
def test(
network, state_dicts, test_loader, eval_fn, analyzer, device, id2label, tokenizer,
alphas=[0.0, 0.2, 0.4, 0.5, 0.6, 0.8, 1.0], keep_neighbors=[0, 1, 2, 3]
):
if torch.cuda.device_count() > 1:
network = DataParallelProxy(network)
network = network.to(device)
network.eval()
eval_types = ['Ent-I', 'Ent-C']
all_pred = {(keep_neighbor, alpha): {eval_type: [] for eval_type in eval_types} for alpha in alphas for keep_neighbor in keep_neighbors}
all_gold = {eval_type: [] for eval_type in eval_types}
list_input_ids = []
with torch.no_grad():
for batch_idx, batch in enumerate(test_loader):
input_ids = batch['input_ids'].to(device)
attention_mask = batch['attention_mask'].to(device)
all_spans = batch['all_spans'].to(device)
gold_entities = batch['gold_entities']
B, _, _ = input_ids.shape
list_input_ids.extend(input_ids.reshape(B, -1).tolist())
list_hidden_states = []
list_start_logits = []
list_end_logits = []
for sd in state_dicts:
if torch.cuda.device_count() > 1:
network.module.load_state_dict(sd)
else:
network.load_state_dict(sd)
hidden_states, attention_mask = network.encode(input_ids, attention_mask)
start_logits, end_logits = network.get_token_logits(hidden_states)
list_hidden_states.append(hidden_states)
list_start_logits.append(start_logits)
list_end_logits.append(end_logits)
ensemble_start_logits = torch.stack(list_start_logits, dim=0).mean(dim=0)
ensemble_end_logits = torch.stack(list_end_logits, dim=0).mean(dim=0)
for keep_neighbor in keep_neighbors:
list_logits = []
spans = filter_spans(ensemble_start_logits, ensemble_end_logits, attention_mask, network.max_span_len, network.topk_spans, keep_neighbor)
for sd, hidden_states in zip(state_dicts, list_hidden_states):
if torch.cuda.device_count() > 1:
network.module.load_state_dict(sd)
else:
network.load_state_dict(sd)
span_reprs = get_span_reprs(hidden_states, spans)
logits = network.get_logits(span_reprs)
list_logits.append(logits)
ensemble_logits = torch.stack(list_logits, dim=0).mean(dim=0)
for alpha in alphas:
pred_entities = extract_entities(input_ids.reshape(B, -1), ensemble_start_logits, ensemble_end_logits, ensemble_logits, spans, id2label, alpha=alpha)
pred_entities = shift_bidx(pred_entities, batch_idx)
refactor_entities(pred_entities, all_pred[keep_neighbor, alpha])
gold_entities = shift_bidx(gold_entities, batch_idx)
refactor_entities(gold_entities, all_gold)
# ===== GLOBAL EVAL =====
final_score = {}
max_key = -1
max_scores = -1
for key in all_pred.keys():
final_score[key] = {}
for eval_type in eval_types:
score = eval_fn(list_to_tuple(all_pred[key][eval_type]), list_to_tuple(all_gold[eval_type]))
final_score[key][eval_type] = score
if max_scores < final_score[key]['Ent-C']['f1']:
max_scores = final_score[key]['Ent-C']['f1']
max_key = key
analyze_result = analyzer.analyze(list_to_tuple(all_pred[max_key]['Ent-I']), list_to_tuple(all_gold['Ent-I']))
# ===== PREDICT =====
predictions = []
for input_ids in list_input_ids:
predictions.append([tokenizer.decode(input_ids, skip_special_tokens=True, clean_up_tokenization_spaces=True)])
for bidx, (ids, lb) in all_pred[max_key]['Ent-C']:
predictions[bidx].append((tokenizer.decode(ids, skip_special_tokens=True, clean_up_tokenization_spaces=True), lb))
return final_score, analyze_result, predictions
# %% [code]
with open(f'{train_dir}/train.json', "r", encoding="utf-8") as f:
data_train = json.load(f)
with open(f'{test_dir}/test.json', "r", encoding="utf-8") as f:
data_test = json.load(f)
print('Train:', len(data_train))
print('Test:', len(data_test))
# %% [code]
entity_types = ['O'] + sorted(list(set([e['label'] for d in data_train + data_test for e in d['entities']])))
# bio_entity_type = ['O'] + [f'{prefix}-{ent}' for ent in entity_types for prefix in ['B', 'I']]
label2id = {l: i for i, l in enumerate(entity_types)}
id2label = {i: l for l, i in label2id.items()}
# %% [code]
zero_entities_idxes = []
for idx, d in enumerate(data_train):
if len(d['entities']) == 0:
zero_entities_idxes.append(idx)
n_zero_entities_samples = len(zero_entities_idxes)
n_has_entities_samples = len(data_train) - n_zero_entities_samples
random.seed(42)
k = min(int(n_has_entities_samples * zero_entities_rate), len(zero_entities_idxes))
sampled_zero_entities_idxes = random.sample(zero_entities_idxes, k)
new_data_train = []
for idx, d in enumerate(data_train):
if len(d['entities']) == 0:
if idx in sampled_zero_entities_idxes:
new_data_train.append(d)
else:
new_data_train.append(d)
data_train = new_data_train
print('Train:', len(data_train))
# %% [code]
if debug_only:
data_train = data_train[:10]
data_test = data_test[:10]
print('Train:', len(data_train))
print('Test:', len(data_test))
# %% [code]
tokenizer = AutoTokenizer.from_pretrained(backbone_model_name)
# %% [code]
print('Experiment name:', state_dict_save_name)
# %% [code]
# trainset = KLTNDataset(data_train, np.array(range(len(data_train))), label2id, tokenizer, **train_memory_params)
# train_loader = DataLoader(trainset, collate_fn=collate_fn, **train_loader_params)
# for b in train_loader:
# break
# %% [code]
if not test_only:
full_idxes = np.array(range(len(data_train)))
training_logs, best_models, last_models = [], [], []
start_training_time = time.time()
for seed in SEEDS:
kf = KFold(n_splits=nfolds, shuffle=True, random_state=seed)
for fold_idx, (tr_idx, va_idx) in enumerate(kf.split(full_idxes)):
if only_fold_idx is not None and only_fold_idx >= 0 and only_fold_idx != fold_idx:
continue
set_seed(seed)
train_idxes, val_idxes = full_idxes[tr_idx], full_idxes[va_idx]
trainset = KLTNDataset(data_train, train_idxes, label2id, tokenizer, **train_memory_params)
valset = KLTNDataset(data_train, val_idxes, label2id, tokenizer, **val_memory_params)
generator = torch.Generator()
generator.manual_seed(seed)
train_loader = DataLoader(trainset, generator=generator, collate_fn=collate_fn, **train_loader_params)
val_loader = DataLoader(valset, generator=generator, collate_fn=collate_fn, **val_loader_params)
my_model = IEModel(
num_labels=len(label2id),
**model_params
)
total_params = sum(p.numel() for p in my_model.parameters())
print(f"Total params: {total_params:,}")
# optimizer, scheduler = configure_optimizers(my_model, optim_params, scheduler_params)
encoder_params = set(map(id, my_model.encoder.parameters()))
other_params = [
p for p in my_model.parameters()
if id(p) not in encoder_params
]
optimizer = optim.AdamW([
{"params": my_model.encoder.parameters(), "lr": 2e-5},
{"params": other_params}
], lr=5e-4)
scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=20, eta_min=1e-6)
loss_fn = CustomLoss(
**loss_func_params
)
eval_fn = CustomEvalFn(**eval_func_params)
trainer_params['save_name'] = f'{state_dict_save_name}_s{seed}_f{fold_idx}'
trainer = Trainer(**trainer_params)
print(f'Start Training Fold {fold_idx}...')
training_log, best_model, last_model = trainer.fit(
my_model, optimizer, scheduler, loss_fn, epochs, train_loader, val_loader, eval_fn,
start_epoch=1, start_training_time=start_training_time, id2label=id2label
)
training_logs.append(training_log)
best_models.append(best_model)
last_models.append(last_model)
# %% [code]
def load_all_state_dicts(folder):
files = []
for file in os.listdir(folder):
if file.endswith(".pt") or file.endswith(".pth"):
m = re.search(r"f(\d+)", file) # tìm f<số>
if m:
fold = int(m.group(1))
files.append((fold, file))
# sort theo fold
files.sort(key=lambda x: x[0])
state_dicts = []
for fold, file in files:
path = os.path.join(folder, file)
print(f"Loading fold {fold}: {file}")
state_dict = torch.load(path, map_location="cpu")
state_dicts.append(state_dict)
return state_dicts
if test_only:
snapshot_download(repo_id=repo_name, local_dir="", repo_type="model", allow_patterns=[f"{state_dict_save_name}/**"])
get_ipython().system('rm -rf .cache .gitattributes')
best_models = load_all_state_dicts(f"{state_dict_save_name}/r1s")
last_models = load_all_state_dicts(f"{state_dict_save_name}/lasts")
# %% [code]
def dict_to_df(data, row_names):
"""
Input:
{
model_name: {
(level1, ..., level(n-1)): {
leveln: {
metric: value
}
}
}
}
Output:
- level1 -> level(n-1): columns thường
- MultiIndex columns:
(model_name, leveln, metric)
"""
rows = {}
for model_name, model_data in data.items():
for upper_levels, last_level_dict in model_data.items():
# đảm bảo tuple
if not isinstance(upper_levels, tuple):
upper_levels = (upper_levels,)
# ===== tạo key row =====
row_key = upper_levels
if row_key not in rows:
row = {}
for i, lv in enumerate(upper_levels):
row[row_names[i]] = lv
rows[row_key] = row
# ===== add metrics =====
for last_level, metrics in last_level_dict.items():
for metric, value in metrics.items():
rows[row_key][
(model_name, last_level, metric)
] = value
# ===== dataframe =====
df = pd.DataFrame(rows.values())
# ===== split columns =====
normal_cols = [
c for c in df.columns
if not isinstance(c, tuple)
]
metric_cols = [
c for c in df.columns
if isinstance(c, tuple)
]
df = df[normal_cols + metric_cols]
# ===== build multi columns =====
df.columns = pd.MultiIndex.from_tuples([
("", "", c) if not isinstance(c, tuple) else c
for c in df.columns
])
return df
def dict_to_records(data):
"""
Input:
{
model_name: {
(level1, level2, ..., leveln): {
metric: value
}
}
}
Output:
[
{
"model": model_name,
"levels": [...],
"metrics": {...}
},
...
]
"""
records = []
for model_name, model_data in data.items():
for levels, metrics in model_data.items():
# ensure tuple/list compatible
if not isinstance(levels, (tuple, list)):
levels = [levels]
records.append(
{
"model": model_name,
"levels": list(levels),
"metrics": metrics
}
)
return records
# %% [code]
os.makedirs(f'{checkpoints_dir}/results', exist_ok=True)
testset = KLTNDataset(data_test, range(len(data_test)), label2id, tokenizer, **val_memory_params)
generator = torch.Generator()
test_loader = DataLoader(testset, generator=generator, collate_fn=collate_fn, **val_loader_params)
eval_fn = CustomEvalFn(**eval_func_params)
analyzer = SpanErrorAnalyzer()
my_model = IEModel(
num_labels=len(label2id),
**model_params
)
total_params = sum(p.numel() for p in my_model.parameters())
print(f"Total params: {total_params:,}")
# %% [code]
start_time = time.time()
best_score, best_analyze_result, best_pred_test = test(my_model, best_models, test_loader, eval_fn, analyzer, device, id2label, tokenizer)
last_score, last_analyze_result, last_pred_test = test(my_model, last_models, test_loader, eval_fn, analyzer, device, id2label, tokenizer)
result_test = {"Best model": best_score, "Last model": last_score}
analyze_result = {"Best model": best_analyze_result, "Last model": last_analyze_result}
analyze_result_sumary = {"Best model": best_analyze_result['summary'], "Last model": last_analyze_result['summary']}
pred_test = {"Best model": best_pred_test, "Last model": last_pred_test}
with open(f"{checkpoints_dir}/results/{state_dict_save_name}_test_{my_model.keep_neighbor}.json", "w", encoding="utf-8") as f:
json.dump(dict_to_records(result_test), f, ensure_ascii=False, indent=2)
with open(f"{checkpoints_dir}/results/{state_dict_save_name}_error_analyze_result_{my_model.keep_neighbor}.json", "w", encoding="utf-8") as f:
json.dump(analyze_result, f, ensure_ascii=False, indent=2)
with open(f"{checkpoints_dir}/results/{state_dict_save_name}_pred_test_{my_model.keep_neighbor}.json", "w", encoding="utf-8") as f:
json.dump(pred_test, f, ensure_ascii=False, indent=2)
print('Test:', time.time() - start_time, 's --> Done!')
print(json.dumps(analyze_result_sumary, ensure_ascii=False, indent=4))
# %% [code]
result_test_df = dict_to_df(result_test, row_names=['keep_neightbor', 'alpha'])
col = ("Best model", "Ent-C", "f1")
sorted_df = result_test_df.sort_values(
by=col,
ascending=False
).reset_index(drop=True)
sorted_df.to_excel(f"{checkpoints_dir}/results/{state_dict_save_name}_test_df_.xlsx")
sorted_df
# %% [code]
def get_avg_best_score(logs):
return float(np.mean([list(log.values())[-1]['best_score'] for log in logs]))
def get_avg_log(logs, epochs):
avg_log = {}
for epoch in range(1, epochs + 1):
val_score = 0.0
train_loss = 0.0
n_eval = 0
for idx in range(len(logs)):
log = logs[idx].get(epoch, logs[idx].get(str(epoch)))
if log is None:
continue
val_score += log.get('val_score', 0.0)
train_loss += log.get('train_loss', 0.0)
n_eval += 1
if n_eval == 0:
continue
avg_log[epoch] = {
'train_loss': train_loss / n_eval,
'val_score': val_score / n_eval if val_score != 0 else float('inf')
}
return avg_log
def parse_label_key(label: str):
try:
first = float(label.split('_', 1)[0]) # số đầu: trước dấu _
last = float(re.findall(r'_(\d+(?:\.\d+)?)$', label)[0])
return first, last
except:
return (0, 0)
def plot_training_logs(logs_dict, save_path=None, figsize=(24, 10)):
fig, axes = plt.subplots(1, 2, figsize=figsize)
# ===== Plot Train Loss =====
for name, log in logs_dict.items():
epochs = sorted(log.keys())
train_loss = [log[e]['train_loss'] for e in epochs]
axes[0].plot(epochs, train_loss, label=name)
axes[0].set_xlabel('Epoch')
axes[0].set_ylabel('Train Loss')
axes[0].set_title('Training Loss')
axes[0].grid(True)
# ===== Plot Validation Score =====
for name, log in logs_dict.items():
epochs = sorted(log.keys())
val_score = [log[e]['val_score'] for e in epochs]
axes[1].plot(epochs, val_score, label=name)
axes[1].set_xlabel('Epoch')
axes[1].set_ylabel('Validation Score')
axes[1].set_title('Validation Score')
axes[1].grid(True)
# ===== Shared Legend =====
handles, labels = axes[0].get_legend_handles_labels()
pairs = list(zip(handles, labels))
pairs_sorted = sorted(
pairs,
key=lambda x: parse_label_key(x[1])
)
handles_sorted, labels_sorted = zip(*pairs_sorted)
axes[0].legend(
handles_sorted,
labels_sorted,
loc='center left',
bbox_to_anchor=(1.01, 0.5),
borderaxespad=0.
)
plt.tight_layout(rect=[0, 0, 1, 1])
if save_path is not None:
os.makedirs(os.path.dirname(save_path), exist_ok=True) if os.path.dirname(save_path) else None
plt.savefig(save_path, dpi=300, bbox_inches='tight')
plt.show()
# %% [code]
if not test_only:
snapshot_download(repo_id=repo_name, local_dir="", repo_type="model", allow_patterns=["**/*ent*.json"], ignore_patterns=["**/*crf*.json"])
get_ipython().system('rm -rf .cache .gitattributes')
# %% [code]
if not test_only:
experiments = {}
for experiment in os.listdir(pretrained_dir):
if '.virtual_documents' in experiment:
continue
experiment_logs = []
try:
for seed in SEEDS:
for fold_idx in range(nfolds):
with open(f"{pretrained_dir}/{experiment}/logs/{experiment}_s{seed}_f{fold_idx}_logging.json", "r", encoding="utf-8") as f:
experiment_log = json.load(f)
experiment_logs.append(experiment_log)
except:
pass
experiments[experiment] = get_avg_log(experiment_logs, 1000)
experiments[state_dict_save_name] = get_avg_log(training_logs, 1000)
# %% [code]
if not test_only:
score = get_avg_best_score(training_logs)
state_dict_save_name, score
# %% [code]
if not test_only:
plot_training_logs(experiments, save_path=f'{checkpoints_dir}/logs/{state_dict_save_name}_log_plot.jpg', figsize=(18, 7.5))