npedrazzini commited on
Commit
7d7be4a
·
verified ·
1 Parent(s): 97aa5b8

Upload continuous-time NewsBERT (full fine-tune, 1 epoch)

Browse files
.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