Fill-Mask
Transformers
Safetensors
bert
masked-language-modeling
historical-nlp
temporal-language-model
Instructions to use TextMachineProject/NewsBERT_1800-1920-Temporal with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use TextMachineProject/NewsBERT_1800-1920-Temporal with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("fill-mask", model="TextMachineProject/NewsBERT_1800-1920-Temporal")# Load model directly from transformers import AutoTokenizer, AutoModelForMaskedLM tokenizer = AutoTokenizer.from_pretrained("TextMachineProject/NewsBERT_1800-1920-Temporal") model = AutoModelForMaskedLM.from_pretrained("TextMachineProject/NewsBERT_1800-1920-Temporal", device_map="auto") - Notebooks
- Google Colab
- Kaggle
Upload continuous-time NewsBERT (full fine-tune, 1 epoch)
Browse files- .DS_Store +0 -0
- README.md +104 -0
- continuous_time_embedding.py +117 -0
- full_model/config.json +30 -0
- full_model/model.safetensors +3 -0
- optimizer.pt +3 -0
- post_inject_norm.pt +3 -0
- rng_state.pth +3 -0
- scheduler.pt +3 -0
- time_embed.pt +3 -0
- trainer_state.json +0 -0
.DS_Store
ADDED
|
Binary file (6.15 kB). View file
|
|
|
README.md
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
base_model: TextMachineProject/NewsBERT_1800-1920
|
| 3 |
+
library_name: transformers
|
| 4 |
+
tags:
|
| 5 |
+
- bert
|
| 6 |
+
- masked-language-modeling
|
| 7 |
+
- historical-nlp
|
| 8 |
+
- temporal-language-model
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
# NewsBERT_1800-1920-Temporal
|
| 12 |
+
|
| 13 |
+
A fine-tuned version of [TextMachineProject/NewsBERT_1800-1920](https://huggingface.co/TextMachineProject/NewsBERT_1800-1920), conditioned on each item's publication year as a continuous input. A single model was trained across the full 1800-1920 span of the original NewsBERT_1800-1920's dataset, with each training example's year injected as a sinusoidal (Fourier) feature added to the token embeddings. Nearby years produce nearby embeddings by construction, so the model can be queried at any year in-range.
|
| 14 |
+
|
| 15 |
+
## Architecture
|
| 16 |
+
|
| 17 |
+
- **Base**: `TextMachineProject/NewsBERT_1800-1920` (BERT-base architecture)
|
| 18 |
+
- **Time conditioning**: a small `ContinuousTimeEmbedding` module maps a
|
| 19 |
+
normalized year to a `hidden_size`-dimensional vector via sinusoidal
|
| 20 |
+
features at 24 log-spaced frequencies, spanning:
|
| 21 |
+
- lowest frequency: 1 cycle over the full 120-year corpus span
|
| 22 |
+
- highest frequency: 1 cycle per 5 years
|
| 23 |
+
- This vector is added to every token's input embedding, then passed
|
| 24 |
+
through a `LayerNorm` before entering the transformer stack (this re-normalization step
|
| 25 |
+
was added after an earlier version without it let the time signal's magnitude
|
| 26 |
+
dominate the token embeddings it was added to, which suppressed learning).
|
| 27 |
+
|
| 28 |
+
You need the accompanying `continuous_time_embedding.py` in this repo to
|
| 29 |
+
load this model, as it is **not** loadable via a plain
|
| 30 |
+
`AutoModelForMaskedLM.from_pretrained(...)` call, since the time-injection
|
| 31 |
+
logic is outside the standard BERT forward pass.
|
| 32 |
+
|
| 33 |
+
## Training data
|
| 34 |
+
|
| 35 |
+
9.28M items (`text`, `year`) from the LwM and HMD collections (see [TextMachineProject/NewsBERT_1800-1920](https://huggingface.co/TextMachineProject/NewsBERT_1800-1920)), spanning 1800-1920, split into
|
| 36 |
+
overlapping 126-token windows (stride 96), i.e. 94.0M training windows.
|
| 37 |
+
|
| 38 |
+
## Training procedure
|
| 39 |
+
|
| 40 |
+
- 1 epoch, batch size 256, linear LR decay, 500 warmup steps
|
| 41 |
+
- Learning rate: `2e-5` for the base model's existing (pretrained) layers,
|
| 42 |
+
and a higher rate of `1e-4` for the ContinuousTimeEmbedding module
|
| 43 |
+
since that module is new and starts from scratch
|
| 44 |
+
- bf16 mixed precision
|
| 45 |
+
|
| 46 |
+
### Hardware / environment
|
| 47 |
+
|
| 48 |
+
### Hardware / environment
|
| 49 |
+
|
| 50 |
+
This research utilised Queen Mary's Apocrita HPC facility, supported by
|
| 51 |
+
QMUL Research-IT. http://doi.org/10.5281/zenodo.438045
|
| 52 |
+
|
| 53 |
+
- Single GPU per training job
|
| 54 |
+
- GPU: NVIDIA A100-PCIE-40GB (40GB VRAM)
|
| 55 |
+
- CUDA: 12.6 (as built into the `torch` version used)
|
| 56 |
+
- Python 3.11.7 (GCC 12.2.0 build)
|
| 57 |
+
- `torch` 2.11.0+cu126
|
| 58 |
+
- `transformers` 5.12.1
|
| 59 |
+
|
| 60 |
+
## Usage
|
| 61 |
+
|
| 62 |
+
```python
|
| 63 |
+
from continuous_time_embedding import load_continuous_time_model
|
| 64 |
+
import torch.nn.functional as F
|
| 65 |
+
import math
|
| 66 |
+
|
| 67 |
+
tokenizer, model = load_time_aware_model("npedrazzini/NewsBERT_1800-1920-Temporal")
|
| 68 |
+
device = next(model.parameters()).device
|
| 69 |
+
|
| 70 |
+
def get_target_word_probability(sentence, target_word, year):
|
| 71 |
+
target_ids = tokenizer.encode(target_word, add_special_tokens=False)
|
| 72 |
+
mask_str = " ".join([tokenizer.mask_token] * len(target_ids))
|
| 73 |
+
masked_sentence = sentence.replace(target_word, mask_str, 1)
|
| 74 |
+
|
| 75 |
+
enc = tokenizer(masked_sentence, return_tensors="pt").to(device)
|
| 76 |
+
mask_positions = (enc["input_ids"][0] == tokenizer.mask_token_id).nonzero(as_tuple=True)[0].tolist()
|
| 77 |
+
|
| 78 |
+
embeddings_module = model.get_input_embeddings_module()
|
| 79 |
+
tok_embeds = embeddings_module(enc["input_ids"])
|
| 80 |
+
time_vec = model.time_embed(torch.tensor([float(year)]).to(device)).unsqueeze(1)
|
| 81 |
+
tok_embeds = model.post_inject_norm(tok_embeds + time_vec)
|
| 82 |
+
|
| 83 |
+
out = model.model(inputs_embeds=tok_embeds, attention_mask=enc["attention_mask"])
|
| 84 |
+
|
| 85 |
+
logprob_sum = 0.0
|
| 86 |
+
for pos, tgt_id in zip(mask_positions, target_ids):
|
| 87 |
+
log_probs = F.log_softmax(out.logits[0, pos].float(), dim=-1)
|
| 88 |
+
logprob_sum += log_probs[tgt_id].item()
|
| 89 |
+
|
| 90 |
+
return math.exp(logprob_sum) if len(target_ids) == 1 else None, logprob_sum
|
| 91 |
+
|
| 92 |
+
# Example: how does P("train") in this sentence change across years?
|
| 93 |
+
sentence = "The train arrived at the station on time"
|
| 94 |
+
for year in [1800, 1830, 1860, 1890, 1920]:
|
| 95 |
+
prob, logprob_sum = get_target_word_probability(sentence, "train", year)
|
| 96 |
+
print(f"year={year}: P(train)={prob:.4f}" if prob else f"year={year}: logprob_sum={logprob_sum:.4f}")
|
| 97 |
+
```
|
| 98 |
+
|
| 99 |
+
## Known limitations
|
| 100 |
+
|
| 101 |
+
- Trained for 1 epoch only. Treat results as preliminary.
|
| 102 |
+
- No year/decade-reweighting was used for this specific run, so the model's
|
| 103 |
+
competence will not be uniform across the full 1800-1920 span and will reflect
|
| 104 |
+
whatever the skewed training corpus's year distribution is.
|
continuous_time_embedding.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Companion module for loading the continuous-time NewsBERT model.
|
| 3 |
+
|
| 4 |
+
This model is not a plain AutoModelForMaskedLM. It wraps a full
|
| 5 |
+
fine-tuned BERT with a continuous sinusoidal time embedding injected at the
|
| 6 |
+
input layer. You need this file (or an equivalent copy of these two classes)
|
| 7 |
+
to load and query it; `AutoModelForMaskedLM.from_pretrained(...)` alone will
|
| 8 |
+
not work.
|
| 9 |
+
|
| 10 |
+
Usage:
|
| 11 |
+
from continuous_time_embedding import load_continuous_time_model
|
| 12 |
+
|
| 13 |
+
tokenizer, model = load_continuous_time_model("npedrazzini/NewsBERT_1800-1920-Temporal")
|
| 14 |
+
"""
|
| 15 |
+
|
| 16 |
+
import os
|
| 17 |
+
import math
|
| 18 |
+
import torch
|
| 19 |
+
import torch.nn as nn
|
| 20 |
+
from transformers import AutoTokenizer, AutoModelForMaskedLM
|
| 21 |
+
from huggingface_hub import snapshot_download
|
| 22 |
+
|
| 23 |
+
MIN_YEAR = 1800.0
|
| 24 |
+
MAX_YEAR = 1920.0
|
| 25 |
+
MIN_PERIOD_YEARS = 5.0
|
| 26 |
+
N_TIME_FREQS = 24
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class ContinuousTimeEmbedding(nn.Module):
|
| 30 |
+
"""Sinusoidal (Fourier) features over normalized year, projected to
|
| 31 |
+
hidden_size. Nearby years produce nearby embeddings by construction.
|
| 32 |
+
Frequency band: lowest = 1 cycle over the full 1800-1920 span, highest =
|
| 33 |
+
1 cycle per MIN_PERIOD_YEARS (5 years) -- see training script / model
|
| 34 |
+
card for the reasoning."""
|
| 35 |
+
|
| 36 |
+
def __init__(self, hidden_size, n_freqs=N_TIME_FREQS, min_year=MIN_YEAR, max_year=MAX_YEAR,
|
| 37 |
+
min_period_years=MIN_PERIOD_YEARS):
|
| 38 |
+
super().__init__()
|
| 39 |
+
self.min_year = min_year
|
| 40 |
+
self.max_year = max_year
|
| 41 |
+
span_years = max_year - min_year
|
| 42 |
+
low_freq_per_year = 1.0 / span_years
|
| 43 |
+
high_freq_per_year = 1.0 / min_period_years
|
| 44 |
+
freqs_per_year = torch.exp(torch.linspace(
|
| 45 |
+
math.log(low_freq_per_year), math.log(high_freq_per_year), n_freqs
|
| 46 |
+
))
|
| 47 |
+
angular_freqs = freqs_per_year * span_years * 2 * math.pi
|
| 48 |
+
self.register_buffer("freqs", angular_freqs)
|
| 49 |
+
self.proj = nn.Linear(2 * n_freqs, hidden_size)
|
| 50 |
+
|
| 51 |
+
def forward(self, years: torch.Tensor) -> torch.Tensor:
|
| 52 |
+
t = (years - self.min_year) / (self.max_year - self.min_year)
|
| 53 |
+
t = t.clamp(0.0, 1.0).unsqueeze(-1)
|
| 54 |
+
angles = t * self.freqs
|
| 55 |
+
feats = torch.cat([torch.sin(angles), torch.cos(angles)], dim=-1)
|
| 56 |
+
return self.proj(feats)
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
class ContinuousTimeBertForMLM(nn.Module):
|
| 60 |
+
"""Full fine-tuned BERT + continuous time embedding, injected into every
|
| 61 |
+
token's input embedding, re-normalized via LayerNorm before entering the
|
| 62 |
+
transformer stack."""
|
| 63 |
+
|
| 64 |
+
def __init__(self, model, hidden_size, n_time_freqs=N_TIME_FREQS,
|
| 65 |
+
min_year=MIN_YEAR, max_year=MAX_YEAR, inject_mode="all_tokens"):
|
| 66 |
+
super().__init__()
|
| 67 |
+
self.model = model
|
| 68 |
+
self.time_embed = ContinuousTimeEmbedding(hidden_size, n_time_freqs, min_year, max_year)
|
| 69 |
+
assert inject_mode in ("all_tokens", "cls_only")
|
| 70 |
+
self.inject_mode = inject_mode
|
| 71 |
+
self.post_inject_norm = nn.LayerNorm(hidden_size)
|
| 72 |
+
|
| 73 |
+
def get_input_embeddings_module(self):
|
| 74 |
+
return self.model.bert.embeddings
|
| 75 |
+
|
| 76 |
+
def forward(self, input_ids, attention_mask, years, labels=None):
|
| 77 |
+
embeddings_module = self.get_input_embeddings_module()
|
| 78 |
+
tok_embeds = embeddings_module(input_ids)
|
| 79 |
+
time_vec = self.time_embed(years).unsqueeze(1)
|
| 80 |
+
|
| 81 |
+
if self.inject_mode == "all_tokens":
|
| 82 |
+
tok_embeds = self.post_inject_norm(tok_embeds + time_vec)
|
| 83 |
+
else:
|
| 84 |
+
tok_embeds = tok_embeds.clone()
|
| 85 |
+
tok_embeds[:, 0, :] = self.post_inject_norm(tok_embeds[:, 0, :] + time_vec.squeeze(1))
|
| 86 |
+
|
| 87 |
+
return self.model(inputs_embeds=tok_embeds, attention_mask=attention_mask, labels=labels)
|
| 88 |
+
|
| 89 |
+
def save_pretrained(self, save_dir):
|
| 90 |
+
os.makedirs(save_dir, exist_ok=True)
|
| 91 |
+
self.model.save_pretrained(os.path.join(save_dir, "full_model"))
|
| 92 |
+
torch.save(self.time_embed.state_dict(), os.path.join(save_dir, "time_embed.pt"))
|
| 93 |
+
torch.save(self.post_inject_norm.state_dict(), os.path.join(save_dir, "post_inject_norm.pt"))
|
| 94 |
+
|
| 95 |
+
@classmethod
|
| 96 |
+
def load_pretrained(cls, save_dir, hidden_size=768, **kwargs):
|
| 97 |
+
model = AutoModelForMaskedLM.from_pretrained(os.path.join(save_dir, "full_model"))
|
| 98 |
+
obj = cls(model, hidden_size, **kwargs)
|
| 99 |
+
obj.time_embed.load_state_dict(torch.load(os.path.join(save_dir, "time_embed.pt"), map_location="cpu"))
|
| 100 |
+
obj.post_inject_norm.load_state_dict(torch.load(os.path.join(save_dir, "post_inject_norm.pt"), map_location="cpu"))
|
| 101 |
+
return obj
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def load_continuous_time_model(repo_id_or_path, device=None, **kwargs):
|
| 105 |
+
"""Convenience loader. Works with either a local checkpoint directory or
|
| 106 |
+
a Hugging Face Hub repo id (downloads it locally first)."""
|
| 107 |
+
if os.path.isdir(repo_id_or_path):
|
| 108 |
+
local_dir = repo_id_or_path
|
| 109 |
+
else:
|
| 110 |
+
local_dir = snapshot_download(repo_id_or_path)
|
| 111 |
+
|
| 112 |
+
tokenizer = AutoTokenizer.from_pretrained(os.path.join(local_dir, "full_model"))
|
| 113 |
+
model = ContinuousTimeBertForMLM.load_pretrained(local_dir, **kwargs)
|
| 114 |
+
|
| 115 |
+
device = device or ("cuda" if torch.cuda.is_available() else "cpu")
|
| 116 |
+
model.to(device).eval()
|
| 117 |
+
return tokenizer, model
|
full_model/config.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"add_cross_attention": false,
|
| 3 |
+
"architectures": [
|
| 4 |
+
"BertForMaskedLM"
|
| 5 |
+
],
|
| 6 |
+
"attention_probs_dropout_prob": 0.1,
|
| 7 |
+
"bos_token_id": null,
|
| 8 |
+
"classifier_dropout": null,
|
| 9 |
+
"dtype": "float32",
|
| 10 |
+
"eos_token_id": null,
|
| 11 |
+
"gradient_checkpointing": false,
|
| 12 |
+
"hidden_act": "gelu",
|
| 13 |
+
"hidden_dropout_prob": 0.1,
|
| 14 |
+
"hidden_size": 768,
|
| 15 |
+
"initializer_range": 0.02,
|
| 16 |
+
"intermediate_size": 3072,
|
| 17 |
+
"is_decoder": false,
|
| 18 |
+
"layer_norm_eps": 1e-12,
|
| 19 |
+
"max_position_embeddings": 512,
|
| 20 |
+
"model_type": "bert",
|
| 21 |
+
"num_attention_heads": 12,
|
| 22 |
+
"num_hidden_layers": 12,
|
| 23 |
+
"pad_token_id": 0,
|
| 24 |
+
"position_embedding_type": "absolute",
|
| 25 |
+
"tie_word_embeddings": true,
|
| 26 |
+
"transformers_version": "5.12.1",
|
| 27 |
+
"type_vocab_size": 2,
|
| 28 |
+
"use_cache": true,
|
| 29 |
+
"vocab_size": 30522
|
| 30 |
+
}
|
full_model/model.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:177baf21c1b2d907a1ce6a2e831e22334f11038576aa91b25e6becfc9029e690
|
| 3 |
+
size 438080896
|
optimizer.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:54a8d18ccefc04f1469723e885ed4801bdc00f70f587c708083f5dd0c296f026
|
| 3 |
+
size 876600395
|
post_inject_norm.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:e782d66c99a643197f1ec655cbc2f23a4d5f3efabc3d8a69d9afc06adb054e65
|
| 3 |
+
size 8173
|
rng_state.pth
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:6d6b98a51dcacc8dd6d5b6f856d73c38d9abf7918ceaf8c5c7fefdcf7134062e
|
| 3 |
+
size 14645
|
scheduler.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:89940e4161de6ed7f9b35686e39169e5a582679d61badafe238e6e4fafe390bb
|
| 3 |
+
size 1465
|
time_embed.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:02f20ee39ce2f0b8759379fe6e0ea93ed8c0540ccad77becc6ab4f310268da7d
|
| 3 |
+
size 152828
|
trainer_state.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|