| --- |
| base_model: roberta-base |
| language: en |
| tags: |
| - narrative |
| - text-classification |
| - pytorch |
| license: odc-by |
| datasets: |
| - teagrjohnson/narradolma |
| --- |
| |
| - **Paper:** [arXiv:2606.19468](https://arxiv.org/abs/2606.19468) |
| - **Collection:** [Narratives in LLM Pretraining Data](https://huggingface.co/collections/teagrjohnson/narratives-in-llm-pretraining-data) |
|
|
| # narrative-event-relation-roberta |
| |
| RoBERTa-base fine-tuned for event-relation classification. It has two binary heads for a **pair of event spans** that you mark in the text with `[E1]β¦[/E1]` and `[E2]β¦[/E2]`: |
| |
| - **temporal** β whether the two events stand in a temporal/sequential relation |
| - **causal** β whether the two events stand in a causal relation |
| Trained on LLM (Gemma) pseudo-labels and evaluated against held-out human gold. Part of the NarraBert suite from *Characterizing Narrative Content in Web-Scale LLM Pretraining Data*. |
| |
| > **Note:** Extended model card with full training details coming soon. |
| |
| ## β οΈ Performance & intended use |
| |
| The initial v0.1 version of the event relation NarraBERT model is the **weakest model in the NarraBert suite**, and predictions should be treated as noisy. |
| |
| - Against held-out human gold (paper, Tab. A3), it reaches **F1 β 0.58 (temporal)** and **β 0.68 (causal)** β macro F1 β 0.63 β below its Gemma teacher (β 0.78). The single `test_f1_gold` (0.805) in the config below is a weighted aggregate that is inflated by the dominant temporal class; the per-task figures are the better guide. |
| - The gap is driven by **severe class imbalance** in the training labels: ~95% of event pairs are temporally related and ~75% are *not* causally related. The minority classes (non-temporal, causal) are therefore the least reliable. |
| - **Recommended use:** aggregate, corpus-level signals (e.g., mean causal density over many passages), *not* high-stakes per-pair decisions. Individual predictions β especially minority-class ones β carry real noise. |
| ## Prerequisites: detect event spans first |
| |
| This model **classifies the relation between two event spans that you provide**; it does **not** detect events itself. You must run an event-span detector first, then wrap an adjacent pair of spans in the marker tokens before calling this model. |
| |
| The pipeline used in the paper: |
| |
| 1. **Detect event-trigger spans** with a DeBERTa event detector fine-tuned on LitBank (Sims et al., 2019); β F1 0.85 on our web-scale data. |
| 2. **Add verb spans** via spaCy `en_core_web_trf`, discarding any that overlap a detected event span. |
| 3. **Select one adjacent span pair** and wrap each span: `[E1]β¦[/E1]` for the first, `[E2]β¦[/E2]` for the second. |
| 4. **Run this model** on the marked text. |
| Any reasonable event/trigger detector works β the only requirement is that the two spans are wrapped in the `[E1]`/`[E2]` markers exactly as during training. The provided `tokenizer/` is set up to match that training format; if you reconstruct the tokenizer, make sure the four marker strings tokenize the same way they did in training. |
| |
| ## Input format |
| |
| ``` |
| She [E1]dropped[/E1] her phone. The screen [E2]cracked[/E2]. |
| ``` |
| |
| ## Loading |
| |
| Download `model.pt` and `tokenizer/` from this repo, then: |
| |
| ```python |
| import torch |
| from transformers import AutoModel, AutoTokenizer |
| from torch import nn |
| |
| ENTITY_MARKERS = ["[E1]", "[/E1]", "[E2]", "[/E2]"] |
| |
| class EventRelationRoBERTa(nn.Module): |
| def __init__(self, model_name): |
| super().__init__() |
| self.backbone = AutoModel.from_pretrained(model_name) |
| hidden = self.backbone.config.hidden_size |
| self.temporal_head = nn.Linear(hidden, 1) |
| self.causal_head = nn.Linear(hidden, 1) |
| def forward(self, input_ids, attention_mask): |
| cls = self.backbone(input_ids=input_ids, attention_mask=attention_mask).last_hidden_state[:, 0, :] |
| return self.temporal_head(cls), self.causal_head(cls) |
| |
| tokenizer = AutoTokenizer.from_pretrained("tokenizer/") |
| model = EventRelationRoBERTa("roberta-base") |
| model.load_state_dict(torch.load("model.pt", map_location="cpu", weights_only=True)) |
| model.eval() |
| ``` |
| |
| ## Inference |
| |
| ```python |
| # 1. run your event detector, pick an adjacent span pair |
| # 2. wrap the two spans with the markers: |
| text = "She [E1]dropped[/E1] her phone. The screen [E2]cracked[/E2]." |
| |
| enc = tokenizer(text, return_tensors="pt", truncation=True, max_length=256) |
| with torch.no_grad(): |
| temporal_logit, causal_logit = model(enc["input_ids"], enc["attention_mask"]) |
| |
| temporal = torch.sigmoid(temporal_logit).item() # P(temporal/sequential relation) |
| causal = torch.sigmoid(causal_logit).item() # P(causal relation) |
| print(f"temporal={temporal:.2f} causal={causal:.2f}") |
| ``` |
| |
| To reproduce the paper's passage-level scores, run this over every adjacent event pair in a passage: **temporal sequencing** is the fraction of pairs with a temporal relation, and **causal density** is the fraction with a causal relation. Because per-pair predictions are noisy, these are most meaningful averaged over many pairs/passages. |
| |
| ## Config |
| |
| ```json |
| { |
| "model_name": "roberta-base", |
| "max_len": 256, |
| "dims": [ |
| "temporal_sequential", |
| "causal" |
| ], |
| "data_source": "gemma-4-31b-it pseudo-labels (internal)", |
| "n_train": 6219, |
| "n_val": 690, |
| "val_frac": 0.1, |
| "best_epoch": 4, |
| "seed": 42, |
| "test_f1_gold": 0.805 |
| } |
| ``` |