Update README.md
Browse files
README.md
CHANGED
|
@@ -8,22 +8,51 @@ tags:
|
|
| 8 |
---
|
| 9 |
|
| 10 |
# narrative-event-relation-roberta
|
| 11 |
-
|
| 12 |
-
RoBERTa-base fine-tuned for event
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
## Loading
|
| 17 |
-
|
| 18 |
Download `model.pt` and `tokenizer/` from this repo, then:
|
| 19 |
-
|
| 20 |
```python
|
| 21 |
import torch
|
| 22 |
from transformers import AutoModel, AutoTokenizer
|
| 23 |
from torch import nn
|
| 24 |
-
|
| 25 |
ENTITY_MARKERS = ["[E1]", "[/E1]", "[E2]", "[/E2]"]
|
| 26 |
-
|
| 27 |
class EventRelationRoBERTa(nn.Module):
|
| 28 |
def __init__(self, model_name):
|
| 29 |
super().__init__()
|
|
@@ -31,19 +60,36 @@ class EventRelationRoBERTa(nn.Module):
|
|
| 31 |
hidden = self.backbone.config.hidden_size
|
| 32 |
self.temporal_head = nn.Linear(hidden, 1)
|
| 33 |
self.causal_head = nn.Linear(hidden, 1)
|
| 34 |
-
|
| 35 |
def forward(self, input_ids, attention_mask):
|
| 36 |
cls = self.backbone(input_ids=input_ids, attention_mask=attention_mask).last_hidden_state[:, 0, :]
|
| 37 |
return self.temporal_head(cls), self.causal_head(cls)
|
| 38 |
-
|
| 39 |
tokenizer = AutoTokenizer.from_pretrained("tokenizer/")
|
| 40 |
model = EventRelationRoBERTa("roberta-base")
|
| 41 |
model.load_state_dict(torch.load("model.pt", map_location="cpu", weights_only=True))
|
| 42 |
model.eval()
|
| 43 |
```
|
| 44 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
## Config
|
| 46 |
-
|
| 47 |
```json
|
| 48 |
{
|
| 49 |
"model_name": "roberta-base",
|
|
@@ -52,7 +98,7 @@ model.eval()
|
|
| 52 |
"temporal_sequential",
|
| 53 |
"causal"
|
| 54 |
],
|
| 55 |
-
"data_source": "
|
| 56 |
"n_train": 6219,
|
| 57 |
"n_val": 690,
|
| 58 |
"val_frac": 0.1,
|
|
@@ -60,4 +106,4 @@ model.eval()
|
|
| 60 |
"seed": 42,
|
| 61 |
"test_f1_gold": 0.805
|
| 62 |
}
|
| 63 |
-
```
|
|
|
|
| 8 |
---
|
| 9 |
|
| 10 |
# narrative-event-relation-roberta
|
| 11 |
+
|
| 12 |
+
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]`:
|
| 13 |
+
|
| 14 |
+
- **temporal** β whether the two events stand in a temporal/sequential relation
|
| 15 |
+
- **causal** β whether the two events stand in a causal relation
|
| 16 |
+
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*.
|
| 17 |
+
|
| 18 |
+
> **Note:** Extended model card with full training details coming soon.
|
| 19 |
+
|
| 20 |
+
## β οΈ Performance & intended use
|
| 21 |
+
|
| 22 |
+
This is the **weakest model in the NarraBert suite**, and predictions should be treated as noisy.
|
| 23 |
+
|
| 24 |
+
- 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.
|
| 25 |
+
- 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.
|
| 26 |
+
- **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.
|
| 27 |
+
## Prerequisites: detect event spans first
|
| 28 |
+
|
| 29 |
+
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.
|
| 30 |
+
|
| 31 |
+
The pipeline used in the paper:
|
| 32 |
+
|
| 33 |
+
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.
|
| 34 |
+
2. **Add verb spans** via spaCy `en_core_web_trf`, discarding any that overlap a detected event span.
|
| 35 |
+
3. **Select one adjacent span pair** and wrap each span: `[E1]β¦[/E1]` for the first, `[E2]β¦[/E2]` for the second.
|
| 36 |
+
4. **Run this model** on the marked text.
|
| 37 |
+
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.
|
| 38 |
+
|
| 39 |
+
## Input format
|
| 40 |
+
|
| 41 |
+
```
|
| 42 |
+
She [E1]dropped[/E1] her phone. The screen [E2]cracked[/E2].
|
| 43 |
+
```
|
| 44 |
+
|
| 45 |
## Loading
|
| 46 |
+
|
| 47 |
Download `model.pt` and `tokenizer/` from this repo, then:
|
| 48 |
+
|
| 49 |
```python
|
| 50 |
import torch
|
| 51 |
from transformers import AutoModel, AutoTokenizer
|
| 52 |
from torch import nn
|
| 53 |
+
|
| 54 |
ENTITY_MARKERS = ["[E1]", "[/E1]", "[E2]", "[/E2]"]
|
| 55 |
+
|
| 56 |
class EventRelationRoBERTa(nn.Module):
|
| 57 |
def __init__(self, model_name):
|
| 58 |
super().__init__()
|
|
|
|
| 60 |
hidden = self.backbone.config.hidden_size
|
| 61 |
self.temporal_head = nn.Linear(hidden, 1)
|
| 62 |
self.causal_head = nn.Linear(hidden, 1)
|
|
|
|
| 63 |
def forward(self, input_ids, attention_mask):
|
| 64 |
cls = self.backbone(input_ids=input_ids, attention_mask=attention_mask).last_hidden_state[:, 0, :]
|
| 65 |
return self.temporal_head(cls), self.causal_head(cls)
|
| 66 |
+
|
| 67 |
tokenizer = AutoTokenizer.from_pretrained("tokenizer/")
|
| 68 |
model = EventRelationRoBERTa("roberta-base")
|
| 69 |
model.load_state_dict(torch.load("model.pt", map_location="cpu", weights_only=True))
|
| 70 |
model.eval()
|
| 71 |
```
|
| 72 |
+
|
| 73 |
+
## Inference
|
| 74 |
+
|
| 75 |
+
```python
|
| 76 |
+
# 1. run your event detector, pick an adjacent span pair
|
| 77 |
+
# 2. wrap the two spans with the markers:
|
| 78 |
+
text = "She [E1]dropped[/E1] her phone. The screen [E2]cracked[/E2]."
|
| 79 |
+
|
| 80 |
+
enc = tokenizer(text, return_tensors="pt", truncation=True, max_length=256)
|
| 81 |
+
with torch.no_grad():
|
| 82 |
+
temporal_logit, causal_logit = model(enc["input_ids"], enc["attention_mask"])
|
| 83 |
+
|
| 84 |
+
temporal = torch.sigmoid(temporal_logit).item() # P(temporal/sequential relation)
|
| 85 |
+
causal = torch.sigmoid(causal_logit).item() # P(causal relation)
|
| 86 |
+
print(f"temporal={temporal:.2f} causal={causal:.2f}")
|
| 87 |
+
```
|
| 88 |
+
|
| 89 |
+
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.
|
| 90 |
+
|
| 91 |
## Config
|
| 92 |
+
|
| 93 |
```json
|
| 94 |
{
|
| 95 |
"model_name": "roberta-base",
|
|
|
|
| 98 |
"temporal_sequential",
|
| 99 |
"causal"
|
| 100 |
],
|
| 101 |
+
"data_source": "gemma-4-31b-it pseudo-labels (internal)",
|
| 102 |
"n_train": 6219,
|
| 103 |
"n_val": 690,
|
| 104 |
"val_frac": 0.1,
|
|
|
|
| 106 |
"seed": 42,
|
| 107 |
"test_f1_gold": 0.805
|
| 108 |
}
|
| 109 |
+
```
|