PersianCorefUD-CorPipe / corpipe25.py
Mnsjn's picture
Upload corpipe25.py with huggingface_hub
0f96601 verified
Raw
History Blame Contribute Delete
36.1 kB
#!/usr/bin/env python3
# This file is part of CorPipe <https://github.com/ufal/crac2025-corpipe>.
#
# Copyright 2025 Institute of Formal and Applied Linguistics, Faculty of
# Mathematics and Physics, Charles University in Prague, Czech Republic.
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
import argparse
import datetime
import json
import os
import pickle
import shutil
import re
from typing import Callable
import huggingface_hub
import minnt
import numpy as np
import torch
import transformers
import udapi
import udapi.block.corefud.movehead
import udapi.block.corefud.removemisc
minnt.require_version("1.0")
parser = argparse.ArgumentParser()
parser.add_argument("--adafactor", default=False, action="store_true", help="Use Adafactor.")
parser.add_argument("--batch_size", default=8, type=int, help="Batch size.")
parser.add_argument("--compile", default=False, action="store_true", help="Compile the model.")
parser.add_argument("--depth", default=5, type=int, help="Constrained decoding depth.")
parser.add_argument("--dev", default=None, nargs="*", type=str, help="Predict dev (treebanks).")
parser.add_argument("--encoder", default="google/mt5-large", type=str, help="MLM encoder model.")
parser.add_argument("--epochs", default=15, type=int, help="Number of epochs.")
parser.add_argument("--exp", default="", type=str, help="Exp name.")
parser.add_argument("--label_smoothing", default=0.2, type=float, help="Label smoothing.")
parser.add_argument("--learning_rate", default=5e-4, type=float, help="Learning rate.")
parser.add_argument("--learning_rate_decay", default=False, action="store_true", help="Decay LR.")
parser.add_argument("--load", default=[], type=str, nargs="*", help="Models to load.")
parser.add_argument("--right", default=50, type=int, help="Reserved space for right context, if any.")
parser.add_argument("--sampling_exponent", default=0.5, type=float, help="Sampling exponent during training.")
parser.add_argument("--sampling_mode", default="sentences", choices=["sentences", "words"], help="Sampling mode during training.")
parser.add_argument("--seed", default=42, type=int, help="Random seed.")
parser.add_argument("--segment", default=512, type=int, help="Segment size")
parser.add_argument("--steps_per_epoch", default=10_000, type=int, help="Update steps (batches) per epoch.")
parser.add_argument("--test", default=None, nargs="*", type=str, help="Predict test (treebanks).")
parser.add_argument("--threads", default=2, type=int, help="Maximum number of threads to use.")
parser.add_argument("--train", default=False, action="store_true", help="Perform training.")
parser.add_argument("--treebanks", default=[], nargs="+", type=str, help="Data.")
parser.add_argument("--warmup", default=0.1, type=float, help="Warmup ratio.")
class Dataset:
TOKEN_EMPTY = "[TOKEN_EMPTY]"
TOKEN_CLS = "[TOKEN_CLS]"
def __init__(self, path: str, tokenizer: transformers.PreTrainedTokenizerFast) -> None:
self._cls = tokenizer.cls_token_id
self._sep = tokenizer.sep_token_id if tokenizer.sep_token_id is not None else tokenizer.eos_token_id
self._path = path
if self._cls is None:
self._cls = tokenizer.vocab[self.TOKEN_CLS]
# Create the tokenized documents if they do not exist
cache_path = f"{path}.mentions.{os.path.basename(tokenizer.name_or_path)}"
if not os.path.exists(cache_path) or os.path.getmtime(cache_path) <= os.path.getmtime(path):
# Create flat representation
if not os.path.exists(f"{path}.flat") or os.path.getmtime(f"{path}.flat") <= os.path.getmtime(path):
with open(path, "r", encoding="utf-8-sig") as data_file:
data_original = [line.rstrip("\r\n") for line in data_file.readlines()]
# Remove multi-word tokens
data = [line for line in data_original if not re.match(r"^\d+-", line)]
# Flatten the representation
flat, i = [], 0
for line in data:
if not line:
i = 0
elif not line.startswith("#"):
columns = line.split("\t")
assert len(columns) == 10
if "." in columns[0]:
deprel = (columns[8].split("|", maxsplit=1)[0] + ":").split(":", maxsplit=2)[1] or "_"
columns[1] = self.TOKEN_EMPTY + " " + deprel
columns[0] = str(i + 1)
columns[6] = "0"
line = "\t".join(columns)
i += 1
flat.append(line)
with open(f"{path}.flat", "w", encoding="utf-8") as data_file:
for line in flat:
print(line, file=data_file)
# Parse with Udapi
if not os.path.exists(f"{path}.mentions") or os.path.getmtime(f"{path}.mentions") <= os.path.getmtime(path):
docs, new_doc = [], []
for doc in udapi.block.read.conllu.Conllu(files=[f"{path}.flat"]).read_documents():
for tree in doc.trees:
if tree.newdoc is not None and new_doc:
docs.append(new_doc)
new_doc = []
words, coref_mentions = [], set()
for node in tree.descendants:
words.append(node.form)
coref_mentions.update(node.coref_mentions)
dense_mentions = []
for mention in coref_mentions:
span = mention.words
start = end = span.index(mention.head)
while start > 0 and span[start - 1].ord + 1 == span[start].ord:
start -= 1
while end < len(span) - 1 and span[end].ord + 1 == span[end + 1].ord:
end += 1
dense_mentions.append(((span[start].ord - 1, span[end].ord - 1), mention.entity.eid, start > 0 or end + 1 < len(span)))
dense_mentions = sorted(dense_mentions, key=lambda x: (x[0][0], -x[0][1], x[2]))
mentions = []
for i, mention in enumerate(dense_mentions):
if i and dense_mentions[i - 1][0] == mention[0]:
print(f"Multiple same mentions {mention[2]}/{dense_mentions[i-1][2]} in sent_id {tree.sent_id}: {tree.get_sentence()}", flush=True)
continue
mentions.append((mention[0][0], mention[0][1], mention[1]))
new_doc.append((words, mentions))
if new_doc:
docs.append(new_doc)
with open(f"{path}.mentions", "wb") as cache_file:
pickle.dump(docs, cache_file, protocol=3)
with open(f"{path}.mentions", "rb") as cache_file:
docs = pickle.load(cache_file)
# Tokenize the data, generate stack operations and subword mentions
self.docs = []
for doc in docs:
new_doc = []
for words, mentions in doc:
subwords, word_indices, word_tags, subword_mentions, stack = [], [], [], [], []
for i in range(len(words)):
word_indices.append(len(subwords))
word = (" " if "robeczech" in tokenizer.name_or_path or "t5gemma" in tokenizer.name_or_path else "") + words[i]
subword = tokenizer.encode(word, add_special_tokens=False)
assert len(subword) > 0
if subword[0] == 6 and "xlm-r" in tokenizer.name_or_path: # Hack: remove the space-only token in XLM-R
subword = subword[1:]
assert len(subword) > 0
subwords.extend(subword)
tag = []
for _ in range(2):
for j in reversed(range(len(stack))):
start, end, eid = stack[j]
if end == i:
tag.append(f"POP:{len(stack)-j}")
subword_mentions.append((start, word_indices[-1], eid))
stack.pop(j)
while mentions and mentions[0][0] == i:
tag.append("PUSH")
stack.append((word_indices[-1], mentions[0][1], mentions[0][2]))
mentions = mentions[1:]
word_tags.append(",".join(tag))
assert len(stack) == 0
subword_mentions = sorted(subword_mentions, key=lambda x: (x[0], -x[1]))
new_doc.append((subwords, word_indices, word_tags, subword_mentions))
self.docs.append(new_doc)
with open(cache_path, "wb") as cache_file:
pickle.dump(self.docs, cache_file, protocol=3)
with open(cache_path, "rb") as cache_file:
self.docs = pickle.load(cache_file)
@staticmethod
def create_tags(trains: list["Dataset"]) -> list[str]:
tags = set()
for train in trains:
for doc in train.docs:
for _, _, word_tags, _ in doc:
tags.update(word_tags)
return sorted(tags)
@staticmethod
def allowed_tag_transitions(tags: list[str], depth: int) -> torch.Tensor:
tags = [f"{d}{',' if tag else ''}{tag}" for d in range(depth) for tag in tags]
allowed = torch.empty(len(tags), len(tags), dtype=torch.float32)
for i, tag_i in enumerate(tags):
for j, tag_j in enumerate(tags):
i_parts = tag_i.split(",")
i_depth = int(i_parts[0])
j_depth = int(tag_j.split(",")[0])
for command in i_parts[1:]:
i_depth += 1 if command == "PUSH" and i_depth >= 0 else -1
allowed[i, j] = 0 if i_depth == j_depth else -torch.inf
return allowed
def dataset(self, tags_map: dict[str, int], train: bool, args: argparse.Namespace) -> list:
segment_size = args.segment
if "proiel" in self._path: # hard-code maximum segment size for Proiel to 512
segment_size = min(512, args.segment)
dataset = []
for doc in self.docs:
p_subwords, p_subword_mentions = [], []
for doc_i, (subwords, word_indices, word_tags, subword_mentions) in enumerate(doc):
if not train and len(subwords) + 4 > segment_size:
print("Truncating a long sentence during prediction")
subwords = subwords[:segment_size - 4]
assert train or len(subwords) + 4 <= segment_size
if len(subwords) + 4 <= segment_size:
right_reserve = min((segment_size - 4 - len(subwords)) // 2, args.right or 0)
context = min(segment_size - 4 - len(subwords) - right_reserve, len(p_subwords))
word_indices = [context + 2 + i for i in word_indices + [len(subwords)]]
e_subwords = [self._cls, *p_subwords[len(p_subwords) - context:], self._sep, *subwords, self._sep]
if args.right is not None:
i = doc_i + 1
while i < len(doc) and len(e_subwords) + 1 < segment_size:
e_subwords.extend(doc[i][0][:segment_size - len(e_subwords) - 1])
i += 1
e_subwords.append(self._sep)
output = (torch.tensor(e_subwords), torch.tensor(word_indices))
if train:
offset = len(p_subwords) - context
prev = [(s - offset + 1, e - offset + 1, eid) for s, e, eid in p_subword_mentions if s >= offset]
prev_pos = np.array([[s, e] for s, e, _ in prev], dtype=np.int64).reshape([-1, 2])
prev_eid = np.array([eid for _, _, eid in prev], dtype=str)
curr = [(context + 2 + s, context + 2 + e, eid) for s, e, eid in subword_mentions]
curr_pos = np.array([[s, e] for s, e, _ in curr], dtype=np.int64).reshape([-1, 2])
curr_eid = np.array([eid for _, _, eid in curr], dtype=str)
mask = curr_pos[:, 0, None] > np.concatenate([prev_pos[:, 0], curr_pos[:, 0]])[None, :]
diag = np.pad(np.eye(len(curr_pos), dtype=np.bool), [[0, 0], [len(prev_pos), 0]])
gold = (curr_eid[:, None] == np.concatenate([prev_eid, curr_eid])[None, :]) * mask
gold = np.where(np.sum(gold, axis=1, keepdims=True) > 0, gold, diag)
gold = gold / np.sum(gold, axis=1, keepdims=True, dtype=np.float32)
mask = mask | diag
if args.label_smoothing:
gold = (1 - args.label_smoothing) * gold + args.label_smoothing * (mask / np.sum(mask, axis=1, keepdims=True, dtype=np.float32))
gold = np.where(mask, gold, -1)
word_tags = [tags_map[tag] for tag in word_tags]
output = (output, tuple(map(torch.as_tensor, (word_tags, np.concatenate([prev_pos, curr_pos], axis=0), curr_pos, gold))))
dataset.append(output)
p_subword_mentions.extend((s + len(p_subwords), e + len(p_subwords), eid) for s, e, eid in subword_mentions)
p_subwords.extend(subwords)
return dataset
@staticmethod
def padded_batch(train: bool) -> Callable[[list], tuple]:
def collate(batch: list) -> tuple:
if train:
batch, outputs = zip(*batch)
subwords, word_indices = zip(*batch)
subwords = torch.nn.utils.rnn.pad_sequence(subwords, batch_first=True, padding_value=-1)
word_indices = torch.nn.utils.rnn.pad_sequence(word_indices, batch_first=True, padding_value=-1)
batch = (subwords, word_indices)
if train:
word_tags, ment_pos, curr_pos, gold = zip(*outputs)
word_tags = torch.nn.utils.rnn.pad_sequence(word_tags, batch_first=True, padding_value=-1)
ment_pos = torch.nn.utils.rnn.pad_sequence(ment_pos, batch_first=True, padding_value=0)
curr_pos = torch.nn.utils.rnn.pad_sequence(curr_pos, batch_first=True, padding_value=0)
gold = torch.stack(
[torch.nn.functional.pad(item, (0, ment_pos.shape[1] - item.shape[1] + 1, 0, curr_pos.shape[1] - item.shape[0] + 1), value=-1) for item in gold])[:, :-1, :-1]
batch = (batch, (word_tags, ment_pos, curr_pos, gold))
return batch
return collate
def save_mentions(self, path: str, mentions: list[list[tuple[int, int, int]]]) -> None:
doc = udapi.block.read.conllu.Conllu(files=[self._path]).read_documents()[0]
udapi.block.corefud.removemisc.RemoveMisc(attrnames="Entity,SplitAnte,Bridge").apply_on_document(doc)
entities = {}
for i, tree in enumerate(doc.trees):
nodes = tree.descendants_and_empty
for start, end, eid in mentions[i]:
if eid not in entities:
entities[eid] = udapi.core.coref.CorefEntity(f"c{eid}")
udapi.core.coref.CorefMention(nodes[start:end + 1], entity=entities[eid])
doc._eid_to_entity = {entity._eid: entity for entity in sorted(entities.values())}
udapi.block.corefud.movehead.MoveHead(bugs='ignore').apply_on_document(doc)
udapi.block.write.conllu.Conllu(files=[path]).apply_on_document(doc)
class TrainDataset(torch.utils.data.Dataset):
def __init__(self, datasets: list[torch.utils.data.Dataset]) -> None:
self._data = []
self._ranges = [0]
for dataset in datasets:
self._data.extend(dataset)
self._ranges.append(len(self._data))
def __len__(self) -> int:
return self._ranges[-1]
def __getitem__(self, index: int):
return self._data[index]
def sampler(self, args: argparse.Namespace) -> torch.utils.data.Sampler:
class TrainSampler(torch.utils.data.Sampler):
def __init__(self, train_dataset) -> None:
self._data = train_dataset._data
self._ranges = train_dataset._ranges
self._examples_per_epoch = args.steps_per_epoch * args.batch_size
self._generator = torch.Generator().manual_seed(args.seed)
if args.sampling_mode == "sentences":
dataset_weights = np.array([self._ranges[i + 1] - self._ranges[i] for i in range(len(self._ranges) - 1)], np.float32)
elif args.sampling_mode == "words":
dataset_weights = np.array([sum(len(s[0][1]) - 1 for s in self._data[self._ranges[i]:self._ranges[i + 1]]) for i in range(len(self._ranges) - 1)], np.float32)
else:
raise ValueError(f"Unknown sampling mode '{args.sampling_mode}'")
dataset_weights = dataset_weights ** args.sampling_exponent
dataset_weights /= np.sum(dataset_weights)
print(*(f"{100*weight:.1f}" for weight in dataset_weights), flush=True)
self._dataset_sizes = np.array(dataset_weights * self._examples_per_epoch, np.int32)
self._dataset_sizes[:self._examples_per_epoch - np.sum(self._dataset_sizes)] += 1
self._dataset_indices = [[] for _ in self._dataset_sizes]
def __len__(self) -> int:
return self._examples_per_epoch
def __iter__(self) -> iter:
indices = []
for i in range(len(self._dataset_sizes)):
required = self._dataset_sizes[i]
while required:
if not len(self._dataset_indices[i]):
self._dataset_indices[i] = self._ranges[i] + torch.randperm(
self._ranges[i + 1] - self._ranges[i], generator=self._generator)
indices.append(self._dataset_indices[i][:required])
self._dataset_indices[i] = self._dataset_indices[i][required:]
required -= len(indices[-1])
indices = torch.cat(indices, dim=0)
return iter(indices[torch.randperm(len(indices), generator=self._generator)].tolist())
return TrainSampler(self)
class Model(minnt.TrainableModule):
def __init__(self, tokenizer: transformers.PreTrainedTokenizer, tags: list[str], args: argparse.Namespace) -> None:
super().__init__()
self._tags = tags
self._args = args
assert tags[0] == "" # Index 0 is used as a boundary condition during decoding
self.register_buffer("_allowed_tag_transitions", Dataset.allowed_tag_transitions(tags, args.depth), persistent=False)
config_overrides = {}
if "umt5" in args.encoder:
self._encoder = transformers.UMT5EncoderModel
elif "mt5" in args.encoder:
self._encoder = transformers.MT5EncoderModel
elif "t5gemma" in args.encoder:
self._encoder = transformers.T5GemmaEncoderModel
config_overrides["is_encoder_decoder"] = False
else:
self._encoder = transformers.AutoModel
if not args.load:
self._encoder = self._encoder.from_pretrained(args.encoder, **config_overrides)
else:
self._encoder = getattr(self._encoder, "from_config", self._encoder)(transformers.AutoConfig.from_pretrained(args.encoder, **config_overrides))
if hasattr(self._encoder.config, "hidden_size"):
encoder_hidden_size = self._encoder.config.hidden_size
elif hasattr(self._encoder.config, "encoder") and hasattr(self._encoder.config.encoder, "hidden_size"):
encoder_hidden_size = self._encoder.config.encoder.hidden_size
else:
raise ValueError("Cannot determine the encoder hidden size from the model configuration.")
self._encoder.resize_token_embeddings(len(tokenizer.vocab))
self._dense_hidden_q = torch.nn.Linear(2 * encoder_hidden_size, 4 * encoder_hidden_size)
self._dense_hidden_k = torch.nn.Linear(2 * encoder_hidden_size, 4 * encoder_hidden_size)
self._dense_hidden_tags = torch.nn.Linear(encoder_hidden_size, 4 * encoder_hidden_size)
self._dense_q = torch.nn.Linear(4 * encoder_hidden_size, encoder_hidden_size, bias=False)
self._dense_k = torch.nn.Linear(4 * encoder_hidden_size, encoder_hidden_size, bias=False)
self._dense_tags = torch.nn.Linear(4 * encoder_hidden_size, len(tags))
def configure(self, train: torch.utils.data.DataLoader) -> None:
args = self._args
if args.adafactor:
optimizer = minnt.optimizers.Adafactor(self.parameters(), lr=args.learning_rate, relative_step=False)
else:
optimizer = torch.optim.Adam(self.parameters(), lr=args.learning_rate)
scheduler = minnt.schedulers.GenericDecay(optimizer, args.epochs * len(train), "cosine" if args.learning_rate_decay else "none", warmup=args.warmup)
super().configure(optimizer=optimizer, scheduler=scheduler, logdir=args.logdir)
def forward(self, subwords: torch.Tensor, word_indices: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
attention_mask = subwords >= 0
embeddings = self._encoder(torch.relu(subwords), attention_mask=attention_mask).last_hidden_state
words = torch.gather(embeddings, 1, torch.relu(word_indices[:, :-1]).unsqueeze(-1).expand(-1, -1, embeddings.shape[-1]))
tag_logits = self._dense_tags(torch.relu(self._dense_hidden_tags(words)))
return embeddings, tag_logits
def compute_antecedents(self, embeddings, mentions, current) -> torch.Tensor:
mentions_embedded = torch.gather(
embeddings[:, :, torch.newaxis, :].expand(-1, -1, 2, -1), 1,
mentions[:, :, :, torch.newaxis].expand(-1, -1, -1, embeddings.shape[-1]),
).flatten(2)
keys = self._dense_k(torch.relu(self._dense_hidden_k(mentions_embedded)))
current_embedded = torch.gather(
embeddings[:, :, torch.newaxis, :].expand(-1, -1, 2, -1), 1,
current[:, :, :, torch.newaxis].expand(-1, -1, -1, embeddings.shape[-1]),
).flatten(2)
queries = self._dense_q(torch.relu(self._dense_hidden_q(current_embedded)))
weights = (queries @ keys.mT) / (self._dense_q.out_features ** 0.5)
return weights
def compute_loss(self, y_pred, y_true, subwords, word_indices) -> torch.Tensor:
embeddings, tag_logits = y_pred
word_tags, all_mentions, current_mentions, gold_mentions = y_true
# Tagging part
tag_loss = torch.nn.functional.cross_entropy(tag_logits.movedim(-1, 1), word_tags, ignore_index=-1, label_smoothing=self._args.label_smoothing)
# Antecedent part
antecedent_logits = self.compute_antecedents(embeddings, all_mentions, current_mentions)
gold_mask = gold_mentions >= 0
antecedent_logits = gold_mask * antecedent_logits + (~gold_mask) * -1e9
current_mentions_valid = torch.any(gold_mask, dim=-1, keepdim=True)
antecedent_loss = torch.nn.functional.cross_entropy(
antecedent_logits.masked_select(current_mentions_valid).view(-1, antecedent_logits.shape[-1]),
torch.relu(gold_mentions).masked_select(current_mentions_valid).view(-1, gold_mentions.shape[-1]))
return {"tag_loss": tag_loss, "antecedent_loss": antecedent_loss}
def decode_mentions(self, logits: torch.Tensor, valid_mask: torch.Tensor) -> torch.Tensor:
# Prepare logits and correct boundary conditions.
logits = logits.tile([1, 1, self._args.depth]) # duplicate logits according to depth
logits.masked_fill_((~valid_mask.unsqueeze(-1)) & (torch.arange(logits.shape[-1], device=logits.device) >= 1), -1e9) # force tag 0 for padding positions
logits[:, 0, self._allowed_tag_transitions[0, :] == -torch.inf] = -1e9 # the first tag must be such that it can follow tag 0
logits[:, -1, self._allowed_tag_transitions[:, 0] == -torch.inf] = -1e9 # the last tag must be such that it leads to tag 0
# Alpha and beta computation.
alphas = torch.zeros_like(logits)
betas = torch.zeros_like(alphas, dtype=torch.int64)
for t in range(logits.shape[1]):
alphas[:, t] = logits[:, t]
if t > 0:
betas[:, t] = torch.argmax(alphas[:, t - 1, :, torch.newaxis] + self._allowed_tag_transitions, dim=1)
alphas[:, t] += alphas[:, t - 1].gather(1, betas[:, t])
# Reconstuction of the most likely sequence.
predictions = torch.zeros_like(valid_mask, dtype=torch.int64)
predictions[:, -1] = torch.argmax(alphas[:, -1], dim=-1)
for t in reversed(range(logits.shape[1] - 1)):
predictions[:, t] = betas[:, t + 1].gather(1, predictions[:, t + 1].unsqueeze(-1)).squeeze(-1)
return predictions
@torch.inference_mode()
def predict(self, dataset: Dataset, dataloader: torch.utils.data.DataLoader) -> list[list[tuple[int, int, int]]]:
self.eval()
results, entities = [], 0
doc_mentions, doc_subwords = [], 0
for b_subwords, b_word_indices in minnt.ProgressLogger(dataloader, f"Predicting {dataset._path}"):
b_subwords, b_word_indices = b_subwords.to(self.device), b_word_indices.to(self.device)
b_size = b_word_indices.shape[0]
# Compute tag logits
b_embeddings, b_logits = self(b_subwords, b_word_indices)
b_tags = self.decode_mentions(b_logits, b_word_indices[:, :-1] >= 0)
del b_logits
b_word_indices, b_tags = b_word_indices.numpy(force=True), b_tags.numpy(force=True)
b_previous, b_mentions, b_refs = [], [], []
for b in range(b_size):
word_indices, tags = b_word_indices[b, b_word_indices[b] >= 0], b_tags[b, b_word_indices[b, 1:] >= 0]
if word_indices[0] == 2:
doc_mentions, doc_subwords = [], 0
# Decode mentions
mentions, stack = [], []
for i, tag in enumerate(self._tags[tag % len(self._tags)] for tag in tags):
for command in tag.split(","):
if command == "PUSH":
stack.append(i)
elif command.startswith("POP:"):
j = int(command.removeprefix("POP:"))
if len(stack):
j = len(stack) - (j if j <= len(stack) else 1)
mentions.append((stack.pop(j), i))
elif command:
raise ValueError(f"Unknown command '{command}'")
while len(stack):
mentions.append((stack.pop(), len(tags) - 1))
mentions = [[s, e, None] for s, e in sorted(set(mentions), key=lambda x: (x[0], -x[1]))]
# Prepare inputs for antecedent prediction
offset = doc_subwords - (word_indices[0] - 2)
results.append([]), b_previous.append([]), b_mentions.append([]), b_refs.append([])
for doc_mention in doc_mentions:
if doc_mention[0] < offset:
continue
b_previous[-1].append([doc_mention[0] - offset + 1, doc_mention[1] - offset + 1])
b_refs[-1].append(doc_mention[2])
for mention in mentions:
results[-1].append(mention)
b_refs[-1].append(mention)
b_mentions[-1].append([word_indices[mention[0]], word_indices[mention[1]]])
doc_mentions.append([doc_subwords + word_indices[mention[0]] - word_indices[0],
doc_subwords + word_indices[mention[1]] - word_indices[0], mention])
doc_subwords += word_indices[-1] - word_indices[0]
# Decode antecedents
if sum(len(mentions) for mentions in b_mentions) == 0:
continue
b_all_mentions = [previous + mentions for previous, mentions in zip(b_previous, b_mentions)]
b_antecedents = self.compute_antecedents(
b_embeddings,
torch.nn.utils.rnn.pad_sequence([torch.as_tensor(m, dtype=torch.int64).view(-1, 2) for m in b_all_mentions], batch_first=True, padding_value=0).to(self.device),
torch.nn.utils.rnn.pad_sequence([torch.as_tensor(m, dtype=torch.int64).view(-1, 2) for m in b_mentions], batch_first=True, padding_value=0).to(self.device),
).numpy(force=True)
del b_embeddings
for b in range(b_size):
len_prev, mentions, refs, antecedents = len(b_previous[b]), b_mentions[b], b_refs[b], b_antecedents[b]
for i in range(len(mentions)):
j = i - 1
while j >= 0 and mentions[j][0] == mentions[i][0]:
antecedents[i, j + len_prev] = antecedents[i, i + len_prev] - 1
j -= 1
j = np.argmax(antecedents[i, :i + len_prev + 1])
if j == i + len_prev:
entities += 1
refs[i + len_prev][2] = entities
else:
refs[i + len_prev][2] = refs[j][2]
return results
def process(self, epoch: int, datasets: list[tuple[Dataset, torch.utils.data.DataLoader]], evaluate: bool) -> None:
for dataset, dataloader in datasets:
mentions = self.predict(dataset, dataloader)
path = os.path.join(self._args.logdir, f"{os.path.splitext(os.path.basename(dataset._path))[0]}.{epoch:02d}.conllu")
dataset.save_mentions(path, mentions)
if evaluate:
# You might want to run the evaluation in parallel if you can; we used `sbatch` during development.
os.system(f"./corefud-score.sh '{dataset._path}' '{path}'")
def main(params: list[str] | None = None) -> None:
args = parser.parse_args(params)
# Set the random seed and the number of threads
minnt.startup(args.seed, args.threads)
# If supplied, load configuration from a trained model
if args.load:
resolved_load_path = args.load[0] if os.path.exists(args.load[0]) else huggingface_hub.snapshot_download(args.load[0])
with open(os.path.join(resolved_load_path, "options.json"), mode="r") as options_file:
args = argparse.Namespace(**{k: v for k, v in json.load(options_file).items() if k in [
"batch_size", "depth", "encoder", "right", "segment", "treebanks"]})
args = parser.parse_args(params, namespace=args)
args.load = [resolved_load_path]
args.logdir = args.exp if args.exp else "."
else:
if not args.train:
raise ValueError("Either --load or --train must be set.")
args.logdir = os.path.join("logs", "{}{}-{}-{}-{}".format(
args.exp + (args.exp and "-"),
os.path.splitext(os.path.basename(globals().get("__file__", "notebook")))[0],
os.environ.get("SLURM_JOB_ID", ""),
datetime.datetime.now().strftime("%y%m%d_%H%M%S"),
",".join(("{}={}".format(
re.sub("(.)[^_]*_?", r"\1", k),
",".join(re.sub(r"^.*/", "", str(x)) for x in ((v if len(v) <= 1 else [v[0], "..."]) if isinstance(v, list) else [v])),
) for k, v in sorted(vars(args).items()) if k not in ["compile", "dev", "test", "exp", "load", "threads"]))
))
print(json.dumps(vars(args), sort_keys=True, ensure_ascii=False, indent=2))
# Create the tokenizer, using a hack to allow sharing tokenized data among models with the same tokenizers.
if "t5gemma" in args.encoder:
tokenizer_name = "google/t5gemma-l-l-ul2"
elif "umt5" in args.encoder:
tokenizer_name = "google/umt5-xl"
elif "mt5" in args.encoder:
tokenizer_name = "google/mt5-xl"
else:
tokenizer_name = args.encoder
tokenizer = transformers.AutoTokenizer.from_pretrained(tokenizer_name, legacy=False) # The legacy does not change things, but silences a warning.
tokenizer.add_special_tokens({"additional_special_tokens": [Dataset.TOKEN_EMPTY] + ([Dataset.TOKEN_CLS] if tokenizer.cls_token_id is None else [])})
# Load the data
trains = [Dataset(path, tokenizer) for path in args.treebanks] if args.train else []
devs = [Dataset(path.replace("-train.conllu", "-minidev.conllu"), tokenizer) for path in ([] if args.dev is None else (args.dev or args.treebanks)) if path]
tests = [Dataset(path.replace("-train.conllu", "-minitest.conllu"), tokenizer) for path in ([] if args.test is None else (args.test or args.treebanks)) if path]
if args.load:
with open(os.path.join(args.load[0], "tags.txt"), mode="r") as tags_file:
tags = [line.rstrip("\r\n") for line in tags_file]
else:
tags = Dataset.create_tags(trains)
tags_map = {tag: i for i, tag in enumerate(tags)}
# Create dataloaders
if args.train:
train = TrainDataset([train.dataset(tags_map, True, args) for train in trains])
train = torch.utils.data.DataLoader(train, batch_size=args.batch_size, collate_fn=Dataset.padded_batch(True), sampler=train.sampler(args))
devs = [(dev, torch.utils.data.DataLoader(
dev.dataset(tags_map, False, args), batch_size=args.batch_size, collate_fn=Dataset.padded_batch(False))) for dev in devs]
tests = [(test, torch.utils.data.DataLoader(
test.dataset(tags_map, False, args), batch_size=args.batch_size, collate_fn=Dataset.padded_batch(False))) for test in tests]
model = Model(tokenizer, tags, args)
if args.load:
model.load_weights(os.path.join(args.load[0], "model.pt"))
if args.compile:
model.compile(dynamic=True)
if args.train:
# Create logdir with the source, options, and tags
os.makedirs(args.logdir)
shutil.copy2(__file__, os.path.join(args.logdir, os.path.basename(__file__)))
with open(os.path.join(args.logdir, "options.json"), "w") as json_file:
json.dump(vars(args), json_file, sort_keys=True, ensure_ascii=False, indent=2)
with open(os.path.join(args.logdir, "tags.txt"), "w") as tags_file:
for tag in tags:
print(tag, file=tags_file)
# Configure the model and train
model.configure(train)
model.fit(train, epochs=args.epochs, callbacks=[
lambda model, epoch, logs: model.save_weights(f"{args.logdir}/model{epoch:02d}.pt"),
lambda model, epoch, logs: model.process(epoch, devs, evaluate=True),
lambda model, epoch, logs: model.process(epoch, tests, evaluate=False),
])
elif args.dev is not None or args.test is not None:
os.makedirs(args.logdir, exist_ok=True)
if args.dev is not None:
model.process(args.epochs, devs, evaluate=True)
if args.test is not None:
model.process(args.epochs, tests, evaluate=False)
if __name__ == "__main__":
main([] if "__file__" not in globals() else None)