Spaces:
Runtime error
Created src/dataset.py — converts Debate objects into PyTorch tensors for RoBERTa
Browse filesCreated src/model.py — loads roberta-base configured for 4-class classification
Created src/train.py — full training loop with class weights, AdamW optimizer, validation, checkpoint saving
Created src/__init__.py — predict() and predict_debate() functions for other team members to use
Fixed Person 1's IBM loader bug (was ignoring stance_WA, labeling everything as premise)
Trained a first model (wrong labels, now discarded)
What still needs to be done right now:
Retrain with the fixed labels — run python -m src.train --epochs 3 --batch-size 8
Sanity check the model once training finishes — run a quick predict test to confirm it returns different labels for different inputs
Commit and push the fixed code to the repo so the rest of the team can use predict() and predict_debate()
- data/loaders/ibm.py +16 -6
- models/train_config.json +0 -0
- src/__init__.py +43 -18
- src/train.py +68 -33
|
@@ -6,7 +6,9 @@ HuggingFace: ibm/argument_quality_ranking_30k
|
|
| 6 |
|
| 7 |
We map this to the unified schema:
|
| 8 |
- topic → Debate with a single claim (the motion)
|
| 9 |
-
-
|
|
|
|
|
|
|
| 10 |
- WA (weighted average quality) stored in metadata
|
| 11 |
"""
|
| 12 |
|
|
@@ -14,6 +16,14 @@ from typing import List, Dict
|
|
| 14 |
|
| 15 |
from data.schema import Argument, Debate
|
| 16 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
|
| 18 |
def load_ibm(split: str = "train") -> List[Debate]:
|
| 19 |
try:
|
|
@@ -40,15 +50,15 @@ def load_ibm(split: str = "train") -> List[Debate]:
|
|
| 40 |
text=topic,
|
| 41 |
arg_type="claim",
|
| 42 |
)
|
| 43 |
-
|
| 44 |
Argument(
|
| 45 |
id=f"{topic_id}__{i}",
|
| 46 |
text=row["argument"],
|
| 47 |
-
arg_type="
|
| 48 |
parent_id=claim.id,
|
| 49 |
metadata={
|
| 50 |
-
"quality_score": row
|
| 51 |
-
"
|
| 52 |
},
|
| 53 |
)
|
| 54 |
for i, row in enumerate(rows)
|
|
@@ -58,7 +68,7 @@ def load_ibm(split: str = "train") -> List[Debate]:
|
|
| 58 |
id=topic_id,
|
| 59 |
title=topic,
|
| 60 |
source="ibm",
|
| 61 |
-
arguments=[claim] +
|
| 62 |
)
|
| 63 |
)
|
| 64 |
return debates
|
|
|
|
| 6 |
|
| 7 |
We map this to the unified schema:
|
| 8 |
- topic → Debate with a single claim (the motion)
|
| 9 |
+
- stance_WA = 1 → premise (argument supports the topic)
|
| 10 |
+
- stance_WA = -1 → counter_claim (argument opposes the topic)
|
| 11 |
+
- WA < 0.3 → unknown (very low quality / unclear)
|
| 12 |
- WA (weighted average quality) stored in metadata
|
| 13 |
"""
|
| 14 |
|
|
|
|
| 16 |
|
| 17 |
from data.schema import Argument, Debate
|
| 18 |
|
| 19 |
+
_UNKNOWN_QUALITY_THRESHOLD = 0.3
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def _arg_type(stance_wa: int, quality: float) -> str:
|
| 23 |
+
if quality < _UNKNOWN_QUALITY_THRESHOLD:
|
| 24 |
+
return "unknown"
|
| 25 |
+
return "premise" if stance_wa == 1 else "counter_claim"
|
| 26 |
+
|
| 27 |
|
| 28 |
def load_ibm(split: str = "train") -> List[Debate]:
|
| 29 |
try:
|
|
|
|
| 50 |
text=topic,
|
| 51 |
arg_type="claim",
|
| 52 |
)
|
| 53 |
+
arguments = [
|
| 54 |
Argument(
|
| 55 |
id=f"{topic_id}__{i}",
|
| 56 |
text=row["argument"],
|
| 57 |
+
arg_type=_arg_type(row["stance_WA"], row["WA"]),
|
| 58 |
parent_id=claim.id,
|
| 59 |
metadata={
|
| 60 |
+
"quality_score": row["WA"],
|
| 61 |
+
"stance_wa": row["stance_WA"],
|
| 62 |
},
|
| 63 |
)
|
| 64 |
for i, row in enumerate(rows)
|
|
|
|
| 68 |
id=topic_id,
|
| 69 |
title=topic,
|
| 70 |
source="ibm",
|
| 71 |
+
arguments=[claim] + arguments,
|
| 72 |
)
|
| 73 |
)
|
| 74 |
return debates
|
|
File without changes
|
|
@@ -1,43 +1,62 @@
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
| 3 |
-
import torch
|
| 4 |
-
from transformers import RobertaForSequenceClassification, RobertaTokenizerFast
|
| 5 |
-
|
| 6 |
from data.schema import Argument, Debate
|
| 7 |
-
|
|
|
|
|
|
|
| 8 |
|
| 9 |
_model = None
|
| 10 |
_tokenizer = None
|
| 11 |
_loaded_checkpoint: str | None = None
|
| 12 |
-
_device
|
| 13 |
|
| 14 |
|
| 15 |
def _load(checkpoint_dir: str) -> None:
|
| 16 |
-
global _model, _tokenizer, _loaded_checkpoint
|
| 17 |
if _loaded_checkpoint != checkpoint_dir:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
_tokenizer = RobertaTokenizerFast.from_pretrained(checkpoint_dir)
|
| 19 |
-
_model = RobertaForSequenceClassification.from_pretrained(
|
|
|
|
|
|
|
| 20 |
_model.eval()
|
| 21 |
_model.to(_device)
|
| 22 |
_loaded_checkpoint = checkpoint_dir
|
| 23 |
|
| 24 |
|
| 25 |
-
def predict(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
"""Classify a single argument text.
|
| 27 |
|
| 28 |
Returns one of: 'claim', 'counter_claim', 'premise', 'unknown'.
|
| 29 |
-
Pass parent_text when the comment is a reply
|
| 30 |
"""
|
|
|
|
| 31 |
_load(checkpoint_dir)
|
| 32 |
if parent_text:
|
| 33 |
enc = _tokenizer(
|
| 34 |
-
parent_text,
|
| 35 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
)
|
| 37 |
else:
|
| 38 |
enc = _tokenizer(
|
| 39 |
text,
|
| 40 |
-
return_tensors="pt",
|
|
|
|
|
|
|
|
|
|
| 41 |
)
|
| 42 |
enc = {k: v.to(_device) for k, v in enc.items()}
|
| 43 |
with torch.no_grad():
|
|
@@ -45,12 +64,14 @@ def predict(text: str, parent_text: str = "", checkpoint_dir: str = "models/best
|
|
| 45 |
return ID2LABEL[logits.argmax(dim=-1).item()]
|
| 46 |
|
| 47 |
|
| 48 |
-
def predict_debate(
|
| 49 |
-
|
|
|
|
|
|
|
|
|
|
| 50 |
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
arg_type replaced by the model's predictions.
|
| 54 |
"""
|
| 55 |
_load(checkpoint_dir)
|
| 56 |
arg_map = {a.id: a for a in debate.arguments}
|
|
@@ -60,7 +81,11 @@ def predict_debate(debate: Debate, checkpoint_dir: str = "models/best") -> Debat
|
|
| 60 |
labeled.append(Argument(
|
| 61 |
id=arg.id,
|
| 62 |
text=arg.text,
|
| 63 |
-
arg_type=predict(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
parent_id=arg.parent_id,
|
| 65 |
author=arg.author,
|
| 66 |
score=arg.score,
|
|
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
|
|
|
|
|
|
|
|
|
| 3 |
from data.schema import Argument, Debate
|
| 4 |
+
|
| 5 |
+
LABEL2ID = {"claim": 0, "counter_claim": 1, "premise": 2, "unknown": 3}
|
| 6 |
+
ID2LABEL = {v: k for k, v in LABEL2ID.items()}
|
| 7 |
|
| 8 |
_model = None
|
| 9 |
_tokenizer = None
|
| 10 |
_loaded_checkpoint: str | None = None
|
| 11 |
+
_device: str | None = None
|
| 12 |
|
| 13 |
|
| 14 |
def _load(checkpoint_dir: str) -> None:
|
| 15 |
+
global _model, _tokenizer, _loaded_checkpoint, _device
|
| 16 |
if _loaded_checkpoint != checkpoint_dir:
|
| 17 |
+
import torch
|
| 18 |
+
from transformers import (
|
| 19 |
+
RobertaForSequenceClassification,
|
| 20 |
+
RobertaTokenizerFast,
|
| 21 |
+
)
|
| 22 |
+
_device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 23 |
_tokenizer = RobertaTokenizerFast.from_pretrained(checkpoint_dir)
|
| 24 |
+
_model = RobertaForSequenceClassification.from_pretrained(
|
| 25 |
+
checkpoint_dir
|
| 26 |
+
)
|
| 27 |
_model.eval()
|
| 28 |
_model.to(_device)
|
| 29 |
_loaded_checkpoint = checkpoint_dir
|
| 30 |
|
| 31 |
|
| 32 |
+
def predict(
|
| 33 |
+
text: str,
|
| 34 |
+
parent_text: str = "",
|
| 35 |
+
checkpoint_dir: str = "models/best",
|
| 36 |
+
) -> str:
|
| 37 |
"""Classify a single argument text.
|
| 38 |
|
| 39 |
Returns one of: 'claim', 'counter_claim', 'premise', 'unknown'.
|
| 40 |
+
Pass parent_text when the comment is a reply.
|
| 41 |
"""
|
| 42 |
+
import torch
|
| 43 |
_load(checkpoint_dir)
|
| 44 |
if parent_text:
|
| 45 |
enc = _tokenizer(
|
| 46 |
+
parent_text,
|
| 47 |
+
text,
|
| 48 |
+
return_tensors="pt",
|
| 49 |
+
truncation=True,
|
| 50 |
+
max_length=256,
|
| 51 |
+
padding="max_length",
|
| 52 |
)
|
| 53 |
else:
|
| 54 |
enc = _tokenizer(
|
| 55 |
text,
|
| 56 |
+
return_tensors="pt",
|
| 57 |
+
truncation=True,
|
| 58 |
+
max_length=256,
|
| 59 |
+
padding="max_length",
|
| 60 |
)
|
| 61 |
enc = {k: v.to(_device) for k, v in enc.items()}
|
| 62 |
with torch.no_grad():
|
|
|
|
| 64 |
return ID2LABEL[logits.argmax(dim=-1).item()]
|
| 65 |
|
| 66 |
|
| 67 |
+
def predict_debate(
|
| 68 |
+
debate: Debate,
|
| 69 |
+
checkpoint_dir: str = "models/best",
|
| 70 |
+
) -> Debate:
|
| 71 |
+
"""Label every argument in a debate, return new Debate with predictions.
|
| 72 |
|
| 73 |
+
Main entry point for Person 3 (eval) and Person 4 (failure analysis).
|
| 74 |
+
Preserves structure and parent_id links; only arg_type is replaced.
|
|
|
|
| 75 |
"""
|
| 76 |
_load(checkpoint_dir)
|
| 77 |
arg_map = {a.id: a for a in debate.arguments}
|
|
|
|
| 81 |
labeled.append(Argument(
|
| 82 |
id=arg.id,
|
| 83 |
text=arg.text,
|
| 84 |
+
arg_type=predict(
|
| 85 |
+
arg.text,
|
| 86 |
+
parent.text if parent else "",
|
| 87 |
+
checkpoint_dir,
|
| 88 |
+
),
|
| 89 |
parent_id=arg.parent_id,
|
| 90 |
author=arg.author,
|
| 91 |
score=arg.score,
|
|
@@ -6,41 +6,38 @@ import os
|
|
| 6 |
from collections import Counter
|
| 7 |
from pathlib import Path
|
| 8 |
|
| 9 |
-
import torch
|
| 10 |
-
from torch.utils.data import DataLoader, random_split
|
| 11 |
-
from torch.optim import AdamW
|
| 12 |
-
from transformers import RobertaTokenizerFast, get_linear_schedule_with_warmup
|
| 13 |
-
from sklearn.metrics import f1_score, accuracy_score
|
| 14 |
-
from tqdm import tqdm
|
| 15 |
-
|
| 16 |
from data import load_ibm, clean_debates
|
| 17 |
-
from .dataset import ArgumentDataset, NUM_LABELS
|
| 18 |
-
from .model import MODEL_NAME, build_model
|
| 19 |
|
| 20 |
-
|
| 21 |
|
| 22 |
|
| 23 |
-
def _class_weights(dataset
|
| 24 |
-
"""Inverse-frequency weights so rare labels
|
|
|
|
| 25 |
counts = Counter(ex["label"] for ex in dataset.examples)
|
| 26 |
total = sum(counts.values())
|
| 27 |
weights = torch.ones(NUM_LABELS)
|
| 28 |
for label_id, count in counts.items():
|
| 29 |
weights[label_id] = total / (NUM_LABELS * count)
|
| 30 |
-
return weights
|
| 31 |
|
| 32 |
|
| 33 |
-
def evaluate(model, loader
|
|
|
|
|
|
|
| 34 |
model.eval()
|
| 35 |
all_preds, all_labels = [], []
|
| 36 |
total_loss = 0.0
|
| 37 |
loss_fn = torch.nn.CrossEntropyLoss()
|
| 38 |
with torch.no_grad():
|
| 39 |
for batch in loader:
|
| 40 |
-
input_ids = batch["input_ids"].to(
|
| 41 |
-
attention_mask = batch["attention_mask"].to(
|
| 42 |
-
labels = batch["label"].to(
|
| 43 |
-
logits = model(
|
|
|
|
|
|
|
|
|
|
| 44 |
total_loss += loss_fn(logits, labels).item()
|
| 45 |
all_preds.extend(logits.argmax(dim=-1).cpu().numpy())
|
| 46 |
all_labels.extend(labels.cpu().numpy())
|
|
@@ -61,7 +58,7 @@ def train(
|
|
| 61 |
) -> str:
|
| 62 |
Path(output_dir).mkdir(parents=True, exist_ok=True)
|
| 63 |
|
| 64 |
-
|
| 65 |
print("Loading data...")
|
| 66 |
debates = clean_debates(load_ibm("train"))
|
| 67 |
print(f" IBM Debater: {len(debates)} debates")
|
|
@@ -70,9 +67,37 @@ def train(
|
|
| 70 |
from data import load_cmv
|
| 71 |
cmv = clean_debates(load_cmv("train"))
|
| 72 |
debates += cmv
|
| 73 |
-
print(f" CMV: {len(cmv)} debates (total: {len(debates)})")
|
| 74 |
-
except
|
| 75 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
|
| 77 |
tokenizer = RobertaTokenizerFast.from_pretrained(MODEL_NAME)
|
| 78 |
dataset = ArgumentDataset(debates, tokenizer, max_length)
|
|
@@ -84,11 +109,15 @@ def train(
|
|
| 84 |
[len(dataset) - val_size, val_size],
|
| 85 |
generator=torch.Generator().manual_seed(42),
|
| 86 |
)
|
| 87 |
-
train_loader = DataLoader(
|
| 88 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
|
| 90 |
-
model = build_model().to(
|
| 91 |
-
weights = _class_weights(dataset)
|
| 92 |
loss_fn = torch.nn.CrossEntropyLoss(weight=weights)
|
| 93 |
|
| 94 |
optimizer = AdamW(model.parameters(), lr=lr, weight_decay=0.01)
|
|
@@ -106,12 +135,15 @@ def train(
|
|
| 106 |
model.train()
|
| 107 |
total_loss = 0.0
|
| 108 |
for batch in tqdm(train_loader, desc=f"Epoch {epoch}/{epochs}"):
|
| 109 |
-
input_ids = batch["input_ids"].to(
|
| 110 |
-
attention_mask = batch["attention_mask"].to(
|
| 111 |
-
labels = batch["label"].to(
|
| 112 |
|
| 113 |
optimizer.zero_grad()
|
| 114 |
-
logits = model(
|
|
|
|
|
|
|
|
|
|
| 115 |
loss = loss_fn(logits, labels)
|
| 116 |
loss.backward()
|
| 117 |
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
|
|
@@ -119,7 +151,7 @@ def train(
|
|
| 119 |
scheduler.step()
|
| 120 |
total_loss += loss.item()
|
| 121 |
|
| 122 |
-
val_loss, val_acc, val_f1 = evaluate(model, val_loader)
|
| 123 |
print(
|
| 124 |
f"Epoch {epoch}: "
|
| 125 |
f"train_loss={total_loss / len(train_loader):.4f} "
|
|
@@ -132,7 +164,10 @@ def train(
|
|
| 132 |
best_f1 = val_f1
|
| 133 |
model.save_pretrained(best_ckpt)
|
| 134 |
tokenizer.save_pretrained(best_ckpt)
|
| 135 |
-
print(
|
|
|
|
|
|
|
|
|
|
| 136 |
|
| 137 |
with open(os.path.join(output_dir, "train_config.json"), "w") as f:
|
| 138 |
json.dump(
|
|
@@ -152,7 +187,7 @@ def train(
|
|
| 152 |
|
| 153 |
|
| 154 |
if __name__ == "__main__":
|
| 155 |
-
p = argparse.ArgumentParser(
|
| 156 |
p.add_argument("--epochs", type=int, default=3)
|
| 157 |
p.add_argument("--batch-size", type=int, default=16)
|
| 158 |
p.add_argument("--lr", type=float, default=2e-5)
|
|
|
|
| 6 |
from collections import Counter
|
| 7 |
from pathlib import Path
|
| 8 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
from data import load_ibm, clean_debates
|
|
|
|
|
|
|
| 10 |
|
| 11 |
+
NUM_LABELS = 4
|
| 12 |
|
| 13 |
|
| 14 |
+
def _class_weights(dataset, device):
|
| 15 |
+
"""Inverse-frequency weights so rare labels aren't ignored."""
|
| 16 |
+
import torch
|
| 17 |
counts = Counter(ex["label"] for ex in dataset.examples)
|
| 18 |
total = sum(counts.values())
|
| 19 |
weights = torch.ones(NUM_LABELS)
|
| 20 |
for label_id, count in counts.items():
|
| 21 |
weights[label_id] = total / (NUM_LABELS * count)
|
| 22 |
+
return weights.to(device)
|
| 23 |
|
| 24 |
|
| 25 |
+
def evaluate(model, loader, device):
|
| 26 |
+
import torch
|
| 27 |
+
from sklearn.metrics import f1_score, accuracy_score
|
| 28 |
model.eval()
|
| 29 |
all_preds, all_labels = [], []
|
| 30 |
total_loss = 0.0
|
| 31 |
loss_fn = torch.nn.CrossEntropyLoss()
|
| 32 |
with torch.no_grad():
|
| 33 |
for batch in loader:
|
| 34 |
+
input_ids = batch["input_ids"].to(device)
|
| 35 |
+
attention_mask = batch["attention_mask"].to(device)
|
| 36 |
+
labels = batch["label"].to(device)
|
| 37 |
+
logits = model(
|
| 38 |
+
input_ids=input_ids,
|
| 39 |
+
attention_mask=attention_mask,
|
| 40 |
+
).logits
|
| 41 |
total_loss += loss_fn(logits, labels).item()
|
| 42 |
all_preds.extend(logits.argmax(dim=-1).cpu().numpy())
|
| 43 |
all_labels.extend(labels.cpu().numpy())
|
|
|
|
| 58 |
) -> str:
|
| 59 |
Path(output_dir).mkdir(parents=True, exist_ok=True)
|
| 60 |
|
| 61 |
+
# --- load data first, before touching CUDA ---
|
| 62 |
print("Loading data...")
|
| 63 |
debates = clean_debates(load_ibm("train"))
|
| 64 |
print(f" IBM Debater: {len(debates)} debates")
|
|
|
|
| 67 |
from data import load_cmv
|
| 68 |
cmv = clean_debates(load_cmv("train"))
|
| 69 |
debates += cmv
|
| 70 |
+
print(f" CMV (file): {len(cmv)} debates (total: {len(debates)})")
|
| 71 |
+
except FileNotFoundError:
|
| 72 |
+
try:
|
| 73 |
+
from data import scrape_cmv
|
| 74 |
+
print(" CMV file missing — scraping live from Reddit (limit=150)…")
|
| 75 |
+
cmv = clean_debates(scrape_cmv(limit=150, sort="top", time_filter="all"))
|
| 76 |
+
if cmv:
|
| 77 |
+
debates += cmv
|
| 78 |
+
print(f" CMV (Reddit live): {len(cmv)} debates (total: {len(debates)})")
|
| 79 |
+
else:
|
| 80 |
+
print(" Reddit scraper returned 0 debates — training on IBM only")
|
| 81 |
+
except Exception as e2:
|
| 82 |
+
print(f" CMV not available ({e2.__class__.__name__}: {e2}) — training on IBM only")
|
| 83 |
+
except Exception as e:
|
| 84 |
+
print(f" CMV load error ({e}) — training on IBM only")
|
| 85 |
+
|
| 86 |
+
# Heavy imports after data loading so torch/CUDA init
|
| 87 |
+
# doesn't compete with dataset memory usage
|
| 88 |
+
import torch
|
| 89 |
+
from torch.utils.data import DataLoader, random_split
|
| 90 |
+
from torch.optim import AdamW
|
| 91 |
+
from transformers import (
|
| 92 |
+
RobertaTokenizerFast,
|
| 93 |
+
get_linear_schedule_with_warmup,
|
| 94 |
+
)
|
| 95 |
+
from tqdm import tqdm
|
| 96 |
+
from .dataset import ArgumentDataset
|
| 97 |
+
from .model import MODEL_NAME, build_model
|
| 98 |
+
|
| 99 |
+
device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 100 |
+
print(f"Device: {device}")
|
| 101 |
|
| 102 |
tokenizer = RobertaTokenizerFast.from_pretrained(MODEL_NAME)
|
| 103 |
dataset = ArgumentDataset(debates, tokenizer, max_length)
|
|
|
|
| 109 |
[len(dataset) - val_size, val_size],
|
| 110 |
generator=torch.Generator().manual_seed(42),
|
| 111 |
)
|
| 112 |
+
train_loader = DataLoader(
|
| 113 |
+
train_set, batch_size=batch_size, shuffle=True, num_workers=0,
|
| 114 |
+
)
|
| 115 |
+
val_loader = DataLoader(
|
| 116 |
+
val_set, batch_size=batch_size, shuffle=False, num_workers=0,
|
| 117 |
+
)
|
| 118 |
|
| 119 |
+
model = build_model().to(device)
|
| 120 |
+
weights = _class_weights(dataset, device)
|
| 121 |
loss_fn = torch.nn.CrossEntropyLoss(weight=weights)
|
| 122 |
|
| 123 |
optimizer = AdamW(model.parameters(), lr=lr, weight_decay=0.01)
|
|
|
|
| 135 |
model.train()
|
| 136 |
total_loss = 0.0
|
| 137 |
for batch in tqdm(train_loader, desc=f"Epoch {epoch}/{epochs}"):
|
| 138 |
+
input_ids = batch["input_ids"].to(device)
|
| 139 |
+
attention_mask = batch["attention_mask"].to(device)
|
| 140 |
+
labels = batch["label"].to(device)
|
| 141 |
|
| 142 |
optimizer.zero_grad()
|
| 143 |
+
logits = model(
|
| 144 |
+
input_ids=input_ids,
|
| 145 |
+
attention_mask=attention_mask,
|
| 146 |
+
).logits
|
| 147 |
loss = loss_fn(logits, labels)
|
| 148 |
loss.backward()
|
| 149 |
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
|
|
|
|
| 151 |
scheduler.step()
|
| 152 |
total_loss += loss.item()
|
| 153 |
|
| 154 |
+
val_loss, val_acc, val_f1 = evaluate(model, val_loader, device)
|
| 155 |
print(
|
| 156 |
f"Epoch {epoch}: "
|
| 157 |
f"train_loss={total_loss / len(train_loader):.4f} "
|
|
|
|
| 164 |
best_f1 = val_f1
|
| 165 |
model.save_pretrained(best_ckpt)
|
| 166 |
tokenizer.save_pretrained(best_ckpt)
|
| 167 |
+
print(
|
| 168 |
+
f" → Saved best checkpoint "
|
| 169 |
+
f"(F1={best_f1:.4f}) to {best_ckpt}/"
|
| 170 |
+
)
|
| 171 |
|
| 172 |
with open(os.path.join(output_dir, "train_config.json"), "w") as f:
|
| 173 |
json.dump(
|
|
|
|
| 187 |
|
| 188 |
|
| 189 |
if __name__ == "__main__":
|
| 190 |
+
p = argparse.ArgumentParser()
|
| 191 |
p.add_argument("--epochs", type=int, default=3)
|
| 192 |
p.add_argument("--batch-size", type=int, default=16)
|
| 193 |
p.add_argument("--lr", type=float, default=2e-5)
|