lamossta commited on
Commit
15bf845
·
1 Parent(s): 71d7b3a

training ntbs

Browse files
src/notebooks/train_bert.ipynb ADDED
@@ -0,0 +1,211 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "id": "m0",
6
+ "metadata": {},
7
+ "source": [
8
+ "# Train & Evaluate BERT\n",
9
+ "\n",
10
+ "Sentence-pair boundary classification on combined PubMed + Wikipedia + Gutenberg data."
11
+ ]
12
+ },
13
+ {
14
+ "cell_type": "code",
15
+ "id": "c0",
16
+ "execution_count": null,
17
+ "metadata": {},
18
+ "outputs": [],
19
+ "source": [
20
+ "import os, sys\n",
21
+ "os.chdir(os.path.join(os.path.dirname(os.getcwd()), \"..\"))\n",
22
+ "print(\"Working dir:\", os.getcwd())\n",
23
+ "import wandb\n",
24
+ "from dotenv import load_dotenv\n",
25
+ "\n",
26
+ "\n",
27
+ "load_dotenv(\"env.txt\")\n",
28
+ "wandb.login(key=os.getenv(\"WB_TOKEN\"))"
29
+ ]
30
+ },
31
+ {
32
+ "cell_type": "code",
33
+ "id": "c1",
34
+ "execution_count": null,
35
+ "metadata": {},
36
+ "outputs": [],
37
+ "source": [
38
+ "import json\n",
39
+ "import logging\n",
40
+ "from pathlib import Path\n",
41
+ "\n",
42
+ "import numpy as np\n",
43
+ "import torch\n",
44
+ "import torch.nn as nn\n",
45
+ "from sklearn.metrics import classification_report, confusion_matrix, f1_score, matthews_corrcoef\n",
46
+ "from transformers import AutoModelForSequenceClassification, AutoTokenizer, EarlyStoppingCallback, Trainer, TrainingArguments\n",
47
+ "\n",
48
+ "from src.datasets.combined_pairs_dataset import CombinedPairsDataset, CombinedPairsConfig, NUM_LABELS, ID2LABEL, LABEL2ID\n",
49
+ "from src.models.bert import load_bert, load_bert_tokenizer\n",
50
+ "from src.models.train import WeightedTrainer, compute_metrics\n",
51
+ "from src.schemas.training_args import BertTrainingArgs\n",
52
+ "\n",
53
+ "logging.basicConfig(level=logging.INFO, format=\"%(asctime)s %(levelname)s %(message)s\")\n",
54
+ "\n",
55
+ "device = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n",
56
+ "print(f\"Device: {device}\")"
57
+ ]
58
+ },
59
+ {
60
+ "cell_type": "markdown",
61
+ "id": "m1",
62
+ "metadata": {},
63
+ "source": [
64
+ "## 1. Configuration & Data"
65
+ ]
66
+ },
67
+ {
68
+ "cell_type": "code",
69
+ "id": "c2",
70
+ "execution_count": null,
71
+ "metadata": {},
72
+ "outputs": [],
73
+ "source": [
74
+ "args = BertTrainingArgs()\n",
75
+ "print(f\"epochs: {args.epochs}, lr: {args.lr}, batch_size: {args.batch_size}, patience: {args.patience}\")\n",
76
+ "\n",
77
+ "os.environ[\"WANDB_PROJECT\"] = \"bottlecap\"\n",
78
+ "os.environ[\"WANDB_RUN_NAME\"] = \"bert\""
79
+ ]
80
+ },
81
+ {
82
+ "cell_type": "code",
83
+ "id": "c3",
84
+ "execution_count": null,
85
+ "metadata": {},
86
+ "outputs": [],
87
+ "source": [
88
+ "cfg = CombinedPairsConfig(data_root=\"data\", gutenberg_train_cap=args.gutenberg_cap, seed=args.seed, max_length=args.max_length)\n",
89
+ "builder = CombinedPairsDataset(cfg)\n",
90
+ "raw_splits = builder.build_splits()\n",
91
+ "class_weights = builder.compute_class_weights(raw_splits[\"train\"])\n",
92
+ "print(f\"Class weights: {class_weights.tolist()}\")"
93
+ ]
94
+ },
95
+ {
96
+ "cell_type": "markdown",
97
+ "id": "m2",
98
+ "metadata": {},
99
+ "source": [
100
+ "## 2. Train"
101
+ ]
102
+ },
103
+ {
104
+ "cell_type": "code",
105
+ "id": "c4",
106
+ "execution_count": null,
107
+ "metadata": {},
108
+ "outputs": [],
109
+ "source": [
110
+ "model = load_bert()\n",
111
+ "tokenizer = load_bert_tokenizer()\n",
112
+ "dd = builder.build_hf_dataset_dict(tokenizer, raw_splits=raw_splits)\n",
113
+ "\n",
114
+ "print(f\"Params: {sum(p.numel() for p in model.parameters()):,}\"\n",
115
+ " f\" Train: {len(dd['train']):,} Val: {len(dd['val']):,} Test: {len(dd['test']):,}\")"
116
+ ]
117
+ },
118
+ {
119
+ "cell_type": "code",
120
+ "id": "c5",
121
+ "execution_count": null,
122
+ "metadata": {},
123
+ "outputs": [],
124
+ "source": [
125
+ "trainer = WeightedTrainer(\n",
126
+ " class_weights=class_weights,\n",
127
+ " model=model,\n",
128
+ " args=args.to_training_arguments(),\n",
129
+ " train_dataset=dd[\"train\"],\n",
130
+ " eval_dataset=dd[\"val\"],\n",
131
+ " compute_metrics=compute_metrics,\n",
132
+ " callbacks=[EarlyStoppingCallback(early_stopping_patience=args.patience)] if args.patience > 0 else [],\n",
133
+ ")\n",
134
+ "\n",
135
+ "trainer.train()"
136
+ ]
137
+ },
138
+ {
139
+ "cell_type": "markdown",
140
+ "id": "m3",
141
+ "metadata": {},
142
+ "source": [
143
+ "## 3. Loss Curves"
144
+ ]
145
+ },
146
+ {
147
+ "cell_type": "code",
148
+ "id": "c6",
149
+ "execution_count": null,
150
+ "metadata": {},
151
+ "outputs": [],
152
+ "source": "import matplotlib.pyplot as plt\n\nplots_dir = Path(args.output_dir) / \"plots\"\nplots_dir.mkdir(parents=True, exist_ok=True)\n\nhistory = trainer.state.log_history\ntrain_steps = [e[\"step\"] for e in history if \"loss\" in e and \"eval_loss\" not in e]\ntrain_loss = [e[\"loss\"] for e in history if \"loss\" in e and \"eval_loss\" not in e]\neval_steps = [e[\"step\"] for e in history if \"eval_loss\" in e]\neval_loss = [e[\"eval_loss\"] for e in history if \"eval_loss\" in e]\neval_epochs = [e.get(\"epoch\", 0) for e in history if \"eval_loss\" in e]\neval_f1 = [e.get(\"eval_macro_f1\", 0) for e in history if \"eval_loss\" in e]\n\nfig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 4))\nax1.plot(train_steps, train_loss, label=\"Train\", alpha=0.7)\nax1.plot(eval_steps, eval_loss, \"o-\", label=\"Val\")\nax1.set_xlabel(\"Step\"); ax1.set_ylabel(\"Loss\"); ax1.set_title(\"BERT — Loss\"); ax1.legend(); ax1.grid(True, alpha=0.3)\nax2.plot(eval_epochs, eval_f1, \"s-\", label=\"Macro F1\")\nax2.set_xlabel(\"Epoch\"); ax2.set_ylabel(\"F1\"); ax2.set_title(\"BERT — Macro F1\"); ax2.legend(); ax2.grid(True, alpha=0.3)\nplt.tight_layout()\nfig.savefig(plots_dir / \"loss_curves.png\", dpi=150)\nplt.show()\nprint(f\"Saved to {plots_dir / 'loss_curves.png'}\")"
153
+ },
154
+ {
155
+ "cell_type": "markdown",
156
+ "id": "m4",
157
+ "metadata": {},
158
+ "source": [
159
+ "## 4. Save Best Model"
160
+ ]
161
+ },
162
+ {
163
+ "cell_type": "code",
164
+ "id": "c7",
165
+ "execution_count": null,
166
+ "metadata": {},
167
+ "outputs": [],
168
+ "source": [
169
+ "best_dir = Path(args.output_dir) / \"best\"\n",
170
+ "best_dir.mkdir(parents=True, exist_ok=True)\n",
171
+ "trainer.save_model(str(best_dir))\n",
172
+ "tokenizer.save_pretrained(str(best_dir))\n",
173
+ "torch.save(class_weights, best_dir / \"class_weights.pt\")\n",
174
+ "\n",
175
+ "train_config = {\"model_type\": \"bert\", \"pretrained\": model.config._name_or_path, \"epochs\": args.epochs, \"batch_size\": args.batch_size, \"lr\": args.lr, \"max_length\": args.max_length, \"class_weights\": class_weights.tolist(), \"num_labels\": NUM_LABELS, \"id2label\": ID2LABEL, \"label2id\": LABEL2ID}\n",
176
+ "with open(best_dir / \"train_config.json\", \"w\") as f:\n",
177
+ " json.dump(train_config, f, indent=2)\n",
178
+ "print(f\"Saved to {best_dir}\")"
179
+ ]
180
+ },
181
+ {
182
+ "cell_type": "markdown",
183
+ "id": "m5",
184
+ "metadata": {},
185
+ "source": [
186
+ "## 5. Evaluate"
187
+ ]
188
+ },
189
+ {
190
+ "cell_type": "code",
191
+ "id": "c8",
192
+ "execution_count": null,
193
+ "metadata": {},
194
+ "outputs": [],
195
+ "source": "import matplotlib.pyplot as plt\nfrom sklearn.metrics import ConfusionMatrixDisplay\n\ntarget_names = [ID2LABEL[i] for i in range(3)]\n\nfor split in [\"val\", \"test\"]:\n eval_model = AutoModelForSequenceClassification.from_pretrained(str(best_dir))\n eval_trainer = Trainer(model=eval_model, args=TrainingArguments(output_dir=\"/tmp/eval\", per_device_eval_batch_size=args.batch_size*2, report_to=\"none\"))\n preds_out = eval_trainer.predict(dd[split])\n preds = np.argmax(preds_out.predictions, axis=-1)\n labels = preds_out.label_ids\n\n print(f\"\\n{'='*60}\")\n print(f\" BERT — {split} ({len(dd[split]):,} samples)\")\n print(f\"{'='*60}\")\n print(f\" Macro F1: {f1_score(labels, preds, average='macro'):.4f}\")\n print(f\" MCC: {matthews_corrcoef(labels, preds):.4f}\\n\")\n print(classification_report(labels, preds, target_names=target_names, digits=4))\n\n cm = confusion_matrix(labels, preds, labels=[0, 1, 2])\n fig, ax = plt.subplots(figsize=(6, 5))\n ConfusionMatrixDisplay(cm, display_labels=target_names).plot(ax=ax, cmap=\"Blues\", values_format=\",\")\n ax.set_title(f\"BERT — {split}\")\n plt.tight_layout()\n fig.savefig(plots_dir / f\"confusion_matrix_{split}.png\", dpi=150)\n plt.show()\n print(f\"Saved to {plots_dir / f'confusion_matrix_{split}.png'}\")"
196
+ }
197
+ ],
198
+ "metadata": {
199
+ "kernelspec": {
200
+ "display_name": "Python 3",
201
+ "language": "python",
202
+ "name": "python3"
203
+ },
204
+ "language_info": {
205
+ "name": "python",
206
+ "version": "3.11.0"
207
+ }
208
+ },
209
+ "nbformat": 4,
210
+ "nbformat_minor": 5
211
+ }
src/notebooks/train_deberta.ipynb ADDED
@@ -0,0 +1,211 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "id": "m0",
6
+ "metadata": {},
7
+ "source": [
8
+ "# Train & Evaluate DeBERTa\n",
9
+ "\n",
10
+ "Sentence-pair boundary classification on combined PubMed + Wikipedia + Gutenberg data."
11
+ ]
12
+ },
13
+ {
14
+ "cell_type": "code",
15
+ "id": "c0",
16
+ "execution_count": null,
17
+ "metadata": {},
18
+ "outputs": [],
19
+ "source": [
20
+ "import os, sys\n",
21
+ "os.chdir(os.path.join(os.path.dirname(os.getcwd()), \"..\"))\n",
22
+ "print(\"Working dir:\", os.getcwd())\n",
23
+ "import wandb\n",
24
+ "from dotenv import load_dotenv\n",
25
+ "\n",
26
+ "\n",
27
+ "load_dotenv(\"env.txt\")\n",
28
+ "wandb.login(key=os.getenv(\"WB_TOKEN\"))"
29
+ ]
30
+ },
31
+ {
32
+ "cell_type": "code",
33
+ "id": "c1",
34
+ "execution_count": null,
35
+ "metadata": {},
36
+ "outputs": [],
37
+ "source": [
38
+ "import json\n",
39
+ "import logging\n",
40
+ "from pathlib import Path\n",
41
+ "\n",
42
+ "import numpy as np\n",
43
+ "import torch\n",
44
+ "import torch.nn as nn\n",
45
+ "from sklearn.metrics import classification_report, confusion_matrix, f1_score, matthews_corrcoef\n",
46
+ "from transformers import AutoModelForSequenceClassification, AutoTokenizer, EarlyStoppingCallback, Trainer, TrainingArguments\n",
47
+ "\n",
48
+ "from src.datasets.combined_pairs_dataset import CombinedPairsDataset, CombinedPairsConfig, NUM_LABELS, ID2LABEL, LABEL2ID\n",
49
+ "from src.models.deberta import load_deberta, load_deberta_tokenizer\n",
50
+ "from src.models.train import WeightedTrainer, compute_metrics\n",
51
+ "from src.schemas.training_args import DebertaTrainingArgs\n",
52
+ "\n",
53
+ "logging.basicConfig(level=logging.INFO, format=\"%(asctime)s %(levelname)s %(message)s\")\n",
54
+ "\n",
55
+ "device = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n",
56
+ "print(f\"Device: {device}\")"
57
+ ]
58
+ },
59
+ {
60
+ "cell_type": "markdown",
61
+ "id": "m1",
62
+ "metadata": {},
63
+ "source": [
64
+ "## 1. Configuration & Data"
65
+ ]
66
+ },
67
+ {
68
+ "cell_type": "code",
69
+ "id": "c2",
70
+ "execution_count": null,
71
+ "metadata": {},
72
+ "outputs": [],
73
+ "source": [
74
+ "args = DebertaTrainingArgs()\n",
75
+ "print(f\"epochs: {args.epochs}, lr: {args.lr}, batch_size: {args.batch_size}, patience: {args.patience}\")\n",
76
+ "\n",
77
+ "os.environ[\"WANDB_PROJECT\"] = \"bottlecap\"\n",
78
+ "os.environ[\"WANDB_RUN_NAME\"] = \"deberta\""
79
+ ]
80
+ },
81
+ {
82
+ "cell_type": "code",
83
+ "id": "c3",
84
+ "execution_count": null,
85
+ "metadata": {},
86
+ "outputs": [],
87
+ "source": [
88
+ "cfg = CombinedPairsConfig(data_root=\"data\", gutenberg_train_cap=args.gutenberg_cap, seed=args.seed, max_length=args.max_length)\n",
89
+ "builder = CombinedPairsDataset(cfg)\n",
90
+ "raw_splits = builder.build_splits()\n",
91
+ "class_weights = builder.compute_class_weights(raw_splits[\"train\"])\n",
92
+ "print(f\"Class weights: {class_weights.tolist()}\")"
93
+ ]
94
+ },
95
+ {
96
+ "cell_type": "markdown",
97
+ "id": "m2",
98
+ "metadata": {},
99
+ "source": [
100
+ "## 2. Train"
101
+ ]
102
+ },
103
+ {
104
+ "cell_type": "code",
105
+ "id": "c4",
106
+ "execution_count": null,
107
+ "metadata": {},
108
+ "outputs": [],
109
+ "source": [
110
+ "model = load_deberta()\n",
111
+ "tokenizer = load_deberta_tokenizer()\n",
112
+ "dd = builder.build_hf_dataset_dict(tokenizer, raw_splits=raw_splits)\n",
113
+ "\n",
114
+ "print(f\"Params: {sum(p.numel() for p in model.parameters()):,}\"\n",
115
+ " f\" Train: {len(dd['train']):,} Val: {len(dd['val']):,} Test: {len(dd['test']):,}\")"
116
+ ]
117
+ },
118
+ {
119
+ "cell_type": "code",
120
+ "id": "c5",
121
+ "execution_count": null,
122
+ "metadata": {},
123
+ "outputs": [],
124
+ "source": [
125
+ "trainer = WeightedTrainer(\n",
126
+ " class_weights=class_weights,\n",
127
+ " model=model,\n",
128
+ " args=args.to_training_arguments(),\n",
129
+ " train_dataset=dd[\"train\"],\n",
130
+ " eval_dataset=dd[\"val\"],\n",
131
+ " compute_metrics=compute_metrics,\n",
132
+ " callbacks=[EarlyStoppingCallback(early_stopping_patience=args.patience)] if args.patience > 0 else [],\n",
133
+ ")\n",
134
+ "\n",
135
+ "trainer.train()"
136
+ ]
137
+ },
138
+ {
139
+ "cell_type": "markdown",
140
+ "id": "m3",
141
+ "metadata": {},
142
+ "source": [
143
+ "## 3. Loss Curves"
144
+ ]
145
+ },
146
+ {
147
+ "cell_type": "code",
148
+ "id": "c6",
149
+ "execution_count": null,
150
+ "metadata": {},
151
+ "outputs": [],
152
+ "source": "import matplotlib.pyplot as plt\n\nplots_dir = Path(args.output_dir) / \"plots\"\nplots_dir.mkdir(parents=True, exist_ok=True)\n\nhistory = trainer.state.log_history\ntrain_steps = [e[\"step\"] for e in history if \"loss\" in e and \"eval_loss\" not in e]\ntrain_loss = [e[\"loss\"] for e in history if \"loss\" in e and \"eval_loss\" not in e]\neval_steps = [e[\"step\"] for e in history if \"eval_loss\" in e]\neval_loss = [e[\"eval_loss\"] for e in history if \"eval_loss\" in e]\neval_epochs = [e.get(\"epoch\", 0) for e in history if \"eval_loss\" in e]\neval_f1 = [e.get(\"eval_macro_f1\", 0) for e in history if \"eval_loss\" in e]\n\nfig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 4))\nax1.plot(train_steps, train_loss, label=\"Train\", alpha=0.7)\nax1.plot(eval_steps, eval_loss, \"o-\", label=\"Val\")\nax1.set_xlabel(\"Step\"); ax1.set_ylabel(\"Loss\"); ax1.set_title(\"DeBERTa — Loss\"); ax1.legend(); ax1.grid(True, alpha=0.3)\nax2.plot(eval_epochs, eval_f1, \"s-\", label=\"Macro F1\")\nax2.set_xlabel(\"Epoch\"); ax2.set_ylabel(\"F1\"); ax2.set_title(\"DeBERTa — Macro F1\"); ax2.legend(); ax2.grid(True, alpha=0.3)\nplt.tight_layout()\nfig.savefig(plots_dir / \"loss_curves.png\", dpi=150)\nplt.show()\nprint(f\"Saved to {plots_dir / 'loss_curves.png'}\")"
153
+ },
154
+ {
155
+ "cell_type": "markdown",
156
+ "id": "m4",
157
+ "metadata": {},
158
+ "source": [
159
+ "## 4. Save Best Model"
160
+ ]
161
+ },
162
+ {
163
+ "cell_type": "code",
164
+ "id": "c7",
165
+ "execution_count": null,
166
+ "metadata": {},
167
+ "outputs": [],
168
+ "source": [
169
+ "best_dir = Path(args.output_dir) / \"best\"\n",
170
+ "best_dir.mkdir(parents=True, exist_ok=True)\n",
171
+ "trainer.save_model(str(best_dir))\n",
172
+ "tokenizer.save_pretrained(str(best_dir))\n",
173
+ "torch.save(class_weights, best_dir / \"class_weights.pt\")\n",
174
+ "\n",
175
+ "train_config = {\"model_type\": \"deberta\", \"pretrained\": model.config._name_or_path, \"epochs\": args.epochs, \"batch_size\": args.batch_size, \"lr\": args.lr, \"max_length\": args.max_length, \"class_weights\": class_weights.tolist(), \"num_labels\": NUM_LABELS, \"id2label\": ID2LABEL, \"label2id\": LABEL2ID}\n",
176
+ "with open(best_dir / \"train_config.json\", \"w\") as f:\n",
177
+ " json.dump(train_config, f, indent=2)\n",
178
+ "print(f\"Saved to {best_dir}\")"
179
+ ]
180
+ },
181
+ {
182
+ "cell_type": "markdown",
183
+ "id": "m5",
184
+ "metadata": {},
185
+ "source": [
186
+ "## 5. Evaluate"
187
+ ]
188
+ },
189
+ {
190
+ "cell_type": "code",
191
+ "id": "c8",
192
+ "execution_count": null,
193
+ "metadata": {},
194
+ "outputs": [],
195
+ "source": "import matplotlib.pyplot as plt\nfrom sklearn.metrics import ConfusionMatrixDisplay\n\ntarget_names = [ID2LABEL[i] for i in range(3)]\n\nfor split in [\"val\", \"test\"]:\n eval_model = AutoModelForSequenceClassification.from_pretrained(str(best_dir))\n eval_trainer = Trainer(model=eval_model, args=TrainingArguments(output_dir=\"/tmp/eval\", per_device_eval_batch_size=args.batch_size*2, report_to=\"none\"))\n preds_out = eval_trainer.predict(dd[split])\n preds = np.argmax(preds_out.predictions, axis=-1)\n labels = preds_out.label_ids\n\n print(f\"\\n{'='*60}\")\n print(f\" DeBERTa — {split} ({len(dd[split]):,} samples)\")\n print(f\"{'='*60}\")\n print(f\" Macro F1: {f1_score(labels, preds, average='macro'):.4f}\")\n print(f\" MCC: {matthews_corrcoef(labels, preds):.4f}\\n\")\n print(classification_report(labels, preds, target_names=target_names, digits=4))\n\n cm = confusion_matrix(labels, preds, labels=[0, 1, 2])\n fig, ax = plt.subplots(figsize=(6, 5))\n ConfusionMatrixDisplay(cm, display_labels=target_names).plot(ax=ax, cmap=\"Blues\", values_format=\",\")\n ax.set_title(f\"DeBERTa — {split}\")\n plt.tight_layout()\n fig.savefig(plots_dir / f\"confusion_matrix_{split}.png\", dpi=150)\n plt.show()\n print(f\"Saved to {plots_dir / f'confusion_matrix_{split}.png'}\")"
196
+ }
197
+ ],
198
+ "metadata": {
199
+ "kernelspec": {
200
+ "display_name": "Python 3",
201
+ "language": "python",
202
+ "name": "python3"
203
+ },
204
+ "language_info": {
205
+ "name": "python",
206
+ "version": "3.11.0"
207
+ }
208
+ },
209
+ "nbformat": 4,
210
+ "nbformat_minor": 5
211
+ }
src/notebooks/train_distilbert.ipynb ADDED
@@ -0,0 +1,211 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "id": "m0",
6
+ "metadata": {},
7
+ "source": [
8
+ "# Train & Evaluate DistilBERT\n",
9
+ "\n",
10
+ "Sentence-pair boundary classification on combined PubMed + Wikipedia + Gutenberg data."
11
+ ]
12
+ },
13
+ {
14
+ "cell_type": "code",
15
+ "id": "c0",
16
+ "execution_count": null,
17
+ "metadata": {},
18
+ "outputs": [],
19
+ "source": [
20
+ "import os, sys\n",
21
+ "os.chdir(os.path.join(os.path.dirname(os.getcwd()), \"..\"))\n",
22
+ "print(\"Working dir:\", os.getcwd())\n",
23
+ "import wandb\n",
24
+ "from dotenv import load_dotenv\n",
25
+ "\n",
26
+ "\n",
27
+ "load_dotenv(\"env.txt\")\n",
28
+ "wandb.login(key=os.getenv(\"WB_TOKEN\"))"
29
+ ]
30
+ },
31
+ {
32
+ "cell_type": "code",
33
+ "id": "c1",
34
+ "execution_count": null,
35
+ "metadata": {},
36
+ "outputs": [],
37
+ "source": [
38
+ "import json\n",
39
+ "import logging\n",
40
+ "from pathlib import Path\n",
41
+ "\n",
42
+ "import numpy as np\n",
43
+ "import torch\n",
44
+ "import torch.nn as nn\n",
45
+ "from sklearn.metrics import classification_report, confusion_matrix, f1_score, matthews_corrcoef\n",
46
+ "from transformers import AutoModelForSequenceClassification, AutoTokenizer, EarlyStoppingCallback, Trainer, TrainingArguments\n",
47
+ "\n",
48
+ "from src.datasets.combined_pairs_dataset import CombinedPairsDataset, CombinedPairsConfig, NUM_LABELS, ID2LABEL, LABEL2ID\n",
49
+ "from src.models.distilbert import load_distilbert, load_distilbert_tokenizer\n",
50
+ "from src.models.train import WeightedTrainer, compute_metrics\n",
51
+ "from src.schemas.training_args import DistilBertTrainingArgs\n",
52
+ "\n",
53
+ "logging.basicConfig(level=logging.INFO, format=\"%(asctime)s %(levelname)s %(message)s\")\n",
54
+ "\n",
55
+ "device = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n",
56
+ "print(f\"Device: {device}\")"
57
+ ]
58
+ },
59
+ {
60
+ "cell_type": "markdown",
61
+ "id": "m1",
62
+ "metadata": {},
63
+ "source": [
64
+ "## 1. Configuration & Data"
65
+ ]
66
+ },
67
+ {
68
+ "cell_type": "code",
69
+ "id": "c2",
70
+ "execution_count": null,
71
+ "metadata": {},
72
+ "outputs": [],
73
+ "source": [
74
+ "args = DistilBertTrainingArgs()\n",
75
+ "print(f\"epochs: {args.epochs}, lr: {args.lr}, batch_size: {args.batch_size}, patience: {args.patience}\")\n",
76
+ "\n",
77
+ "os.environ[\"WANDB_PROJECT\"] = \"bottlecap\"\n",
78
+ "os.environ[\"WANDB_RUN_NAME\"] = \"distilbert\""
79
+ ]
80
+ },
81
+ {
82
+ "cell_type": "code",
83
+ "id": "c3",
84
+ "execution_count": null,
85
+ "metadata": {},
86
+ "outputs": [],
87
+ "source": [
88
+ "cfg = CombinedPairsConfig(data_root=\"data\", gutenberg_train_cap=args.gutenberg_cap, seed=args.seed, max_length=args.max_length)\n",
89
+ "builder = CombinedPairsDataset(cfg)\n",
90
+ "raw_splits = builder.build_splits()\n",
91
+ "class_weights = builder.compute_class_weights(raw_splits[\"train\"])\n",
92
+ "print(f\"Class weights: {class_weights.tolist()}\")"
93
+ ]
94
+ },
95
+ {
96
+ "cell_type": "markdown",
97
+ "id": "m2",
98
+ "metadata": {},
99
+ "source": [
100
+ "## 2. Train"
101
+ ]
102
+ },
103
+ {
104
+ "cell_type": "code",
105
+ "id": "c4",
106
+ "execution_count": null,
107
+ "metadata": {},
108
+ "outputs": [],
109
+ "source": [
110
+ "model = load_distilbert()\n",
111
+ "tokenizer = load_distilbert_tokenizer()\n",
112
+ "dd = builder.build_hf_dataset_dict(tokenizer, raw_splits=raw_splits)\n",
113
+ "\n",
114
+ "print(f\"Params: {sum(p.numel() for p in model.parameters()):,}\"\n",
115
+ " f\" Train: {len(dd['train']):,} Val: {len(dd['val']):,} Test: {len(dd['test']):,}\")"
116
+ ]
117
+ },
118
+ {
119
+ "cell_type": "code",
120
+ "id": "c5",
121
+ "execution_count": null,
122
+ "metadata": {},
123
+ "outputs": [],
124
+ "source": [
125
+ "trainer = WeightedTrainer(\n",
126
+ " class_weights=class_weights,\n",
127
+ " model=model,\n",
128
+ " args=args.to_training_arguments(),\n",
129
+ " train_dataset=dd[\"train\"],\n",
130
+ " eval_dataset=dd[\"val\"],\n",
131
+ " compute_metrics=compute_metrics,\n",
132
+ " callbacks=[EarlyStoppingCallback(early_stopping_patience=args.patience)] if args.patience > 0 else [],\n",
133
+ ")\n",
134
+ "\n",
135
+ "trainer.train()"
136
+ ]
137
+ },
138
+ {
139
+ "cell_type": "markdown",
140
+ "id": "m3",
141
+ "metadata": {},
142
+ "source": [
143
+ "## 3. Loss Curves"
144
+ ]
145
+ },
146
+ {
147
+ "cell_type": "code",
148
+ "id": "c6",
149
+ "execution_count": null,
150
+ "metadata": {},
151
+ "outputs": [],
152
+ "source": "import matplotlib.pyplot as plt\n\nplots_dir = Path(args.output_dir) / \"plots\"\nplots_dir.mkdir(parents=True, exist_ok=True)\n\nhistory = trainer.state.log_history\ntrain_steps = [e[\"step\"] for e in history if \"loss\" in e and \"eval_loss\" not in e]\ntrain_loss = [e[\"loss\"] for e in history if \"loss\" in e and \"eval_loss\" not in e]\neval_steps = [e[\"step\"] for e in history if \"eval_loss\" in e]\neval_loss = [e[\"eval_loss\"] for e in history if \"eval_loss\" in e]\neval_epochs = [e.get(\"epoch\", 0) for e in history if \"eval_loss\" in e]\neval_f1 = [e.get(\"eval_macro_f1\", 0) for e in history if \"eval_loss\" in e]\n\nfig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 4))\nax1.plot(train_steps, train_loss, label=\"Train\", alpha=0.7)\nax1.plot(eval_steps, eval_loss, \"o-\", label=\"Val\")\nax1.set_xlabel(\"Step\"); ax1.set_ylabel(\"Loss\"); ax1.set_title(\"DistilBERT — Loss\"); ax1.legend(); ax1.grid(True, alpha=0.3)\nax2.plot(eval_epochs, eval_f1, \"s-\", label=\"Macro F1\")\nax2.set_xlabel(\"Epoch\"); ax2.set_ylabel(\"F1\"); ax2.set_title(\"DistilBERT — Macro F1\"); ax2.legend(); ax2.grid(True, alpha=0.3)\nplt.tight_layout()\nfig.savefig(plots_dir / \"loss_curves.png\", dpi=150)\nplt.show()\nprint(f\"Saved to {plots_dir / 'loss_curves.png'}\")"
153
+ },
154
+ {
155
+ "cell_type": "markdown",
156
+ "id": "m4",
157
+ "metadata": {},
158
+ "source": [
159
+ "## 4. Save Best Model"
160
+ ]
161
+ },
162
+ {
163
+ "cell_type": "code",
164
+ "id": "c7",
165
+ "execution_count": null,
166
+ "metadata": {},
167
+ "outputs": [],
168
+ "source": [
169
+ "best_dir = Path(args.output_dir) / \"best\"\n",
170
+ "best_dir.mkdir(parents=True, exist_ok=True)\n",
171
+ "trainer.save_model(str(best_dir))\n",
172
+ "tokenizer.save_pretrained(str(best_dir))\n",
173
+ "torch.save(class_weights, best_dir / \"class_weights.pt\")\n",
174
+ "\n",
175
+ "train_config = {\"model_type\": \"distilbert\", \"pretrained\": model.config._name_or_path, \"epochs\": args.epochs, \"batch_size\": args.batch_size, \"lr\": args.lr, \"max_length\": args.max_length, \"class_weights\": class_weights.tolist(), \"num_labels\": NUM_LABELS, \"id2label\": ID2LABEL, \"label2id\": LABEL2ID}\n",
176
+ "with open(best_dir / \"train_config.json\", \"w\") as f:\n",
177
+ " json.dump(train_config, f, indent=2)\n",
178
+ "print(f\"Saved to {best_dir}\")"
179
+ ]
180
+ },
181
+ {
182
+ "cell_type": "markdown",
183
+ "id": "m5",
184
+ "metadata": {},
185
+ "source": [
186
+ "## 5. Evaluate"
187
+ ]
188
+ },
189
+ {
190
+ "cell_type": "code",
191
+ "id": "c8",
192
+ "execution_count": null,
193
+ "metadata": {},
194
+ "outputs": [],
195
+ "source": "import matplotlib.pyplot as plt\nfrom sklearn.metrics import ConfusionMatrixDisplay\n\ntarget_names = [ID2LABEL[i] for i in range(3)]\n\nfor split in [\"val\", \"test\"]:\n eval_model = AutoModelForSequenceClassification.from_pretrained(str(best_dir))\n eval_trainer = Trainer(model=eval_model, args=TrainingArguments(output_dir=\"/tmp/eval\", per_device_eval_batch_size=args.batch_size*2, report_to=\"none\"))\n preds_out = eval_trainer.predict(dd[split])\n preds = np.argmax(preds_out.predictions, axis=-1)\n labels = preds_out.label_ids\n\n print(f\"\\n{'='*60}\")\n print(f\" DistilBERT — {split} ({len(dd[split]):,} samples)\")\n print(f\"{'='*60}\")\n print(f\" Macro F1: {f1_score(labels, preds, average='macro'):.4f}\")\n print(f\" MCC: {matthews_corrcoef(labels, preds):.4f}\\n\")\n print(classification_report(labels, preds, target_names=target_names, digits=4))\n\n # Confusion matrix\n cm = confusion_matrix(labels, preds, labels=[0, 1, 2])\n fig, ax = plt.subplots(figsize=(6, 5))\n ConfusionMatrixDisplay(cm, display_labels=target_names).plot(ax=ax, cmap=\"Blues\", values_format=\",\")\n ax.set_title(f\"DistilBERT — {split}\")\n plt.tight_layout()\n fig.savefig(plots_dir / f\"confusion_matrix_{split}.png\", dpi=150)\n plt.show()\n print(f\"Saved to {plots_dir / f'confusion_matrix_{split}.png'}\")"
196
+ }
197
+ ],
198
+ "metadata": {
199
+ "kernelspec": {
200
+ "display_name": "Python 3",
201
+ "language": "python",
202
+ "name": "python3"
203
+ },
204
+ "language_info": {
205
+ "name": "python",
206
+ "version": "3.11.0"
207
+ }
208
+ },
209
+ "nbformat": 4,
210
+ "nbformat_minor": 5
211
+ }
src/notebooks/train_models.ipynb ADDED
@@ -0,0 +1,280 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "metadata": {},
6
+ "source": "# Train & Evaluate DistilBERT / BERT / DeBERTa\n\nSentence-pair boundary classification on combined PubMed + Wikipedia + Gutenberg data.\n\n**Metrics:** per-class F1, macro F1, weighted F1, MCC (Matthews Correlation Coefficient)."
7
+ },
8
+ {
9
+ "cell_type": "code",
10
+ "execution_count": null,
11
+ "metadata": {},
12
+ "outputs": [],
13
+ "source": [
14
+ "import os, sys\n",
15
+ "os.chdir(os.path.join(os.path.dirname(os.getcwd()), \"..\"))\n",
16
+ "print(\"Working dir:\", os.getcwd())\n",
17
+ "import wandb\n",
18
+ "from dotenv import load_dotenv\n",
19
+ "\n",
20
+ "\n",
21
+ "load_dotenv()\n",
22
+ "wandb.login(key=os.getenv(\"WB_TOKEN\"))"
23
+ ]
24
+ },
25
+ {
26
+ "cell_type": "code",
27
+ "execution_count": null,
28
+ "metadata": {},
29
+ "outputs": [],
30
+ "source": "import json\nimport logging\nfrom pathlib import Path\n\nimport numpy as np\nimport torch\nimport torch.nn as nn\nfrom sklearn.metrics import (\n classification_report,\n confusion_matrix,\n f1_score,\n matthews_corrcoef,\n)\nfrom transformers import (\n AutoModelForSequenceClassification,\n AutoTokenizer,\n EarlyStoppingCallback,\n Trainer,\n TrainingArguments,\n)\n\nfrom src.datasets.combined_pairs_dataset import (\n CombinedPairsDataset,\n CombinedPairsConfig,\n NUM_LABELS,\n ID2LABEL,\n LABEL2ID,\n)\nfrom src.models.bert import load_bert, load_bert_tokenizer\nfrom src.models.deberta import load_deberta, load_deberta_tokenizer\nfrom src.models.distilbert import load_distilbert, load_distilbert_tokenizer\nfrom src.models.train import WeightedTrainer, compute_metrics\nfrom src.schemas.training_args import BertTrainingArgs, DebertaTrainingArgs, DistilBertTrainingArgs\n\nlogging.basicConfig(level=logging.INFO, format=\"%(asctime)s %(levelname)s %(message)s\")\n\ndevice = \"cuda\" if torch.cuda.is_available() else \"cpu\"\nprint(f\"Device: {device}\")\nprint(f\"PyTorch: {torch.__version__}\")"
31
+ },
32
+ {
33
+ "cell_type": "markdown",
34
+ "metadata": {},
35
+ "source": [
36
+ "## 1. Configuration"
37
+ ]
38
+ },
39
+ {
40
+ "cell_type": "code",
41
+ "execution_count": null,
42
+ "metadata": {},
43
+ "outputs": [],
44
+ "source": "# ── Training args from dataclasses (edit fields to override defaults) ──\ndistilbert_args = DistilBertTrainingArgs()\nbert_args = BertTrainingArgs()\ndeberta_args = DebertaTrainingArgs()\n\nfor name, args in [(\"DistilBERT\", distilbert_args), (\"BERT\", bert_args), (\"DeBERTa\", deberta_args)]:\n print(f\"{name} config:\")\n print(f\" output_dir: {args.output_dir}\")\n print(f\" epochs: {args.epochs}\")\n print(f\" batch_size: {args.batch_size}\")\n print(f\" lr: {args.lr}\")\n print(f\" max_length: {args.max_length}\")\n print(f\" gutenberg_cap: {args.gutenberg_cap}\")\n print()"
45
+ },
46
+ {
47
+ "cell_type": "markdown",
48
+ "metadata": {},
49
+ "source": [
50
+ "## 2. Build dataset splits"
51
+ ]
52
+ },
53
+ {
54
+ "cell_type": "code",
55
+ "execution_count": null,
56
+ "metadata": {},
57
+ "outputs": [],
58
+ "source": "cfg = CombinedPairsConfig(\n data_root=\"data\",\n gutenberg_train_cap=distilbert_args.gutenberg_cap,\n seed=distilbert_args.seed,\n max_length=distilbert_args.max_length,\n)\nbuilder = CombinedPairsDataset(cfg)\nraw_splits = builder.build_splits()\nclass_weights = builder.compute_class_weights(raw_splits[\"train\"])\nprint(f\"\\nClass weights: {class_weights.tolist()}\")"
59
+ },
60
+ {
61
+ "cell_type": "markdown",
62
+ "metadata": {},
63
+ "source": [
64
+ "## 3. Helper functions"
65
+ ]
66
+ },
67
+ {
68
+ "cell_type": "code",
69
+ "execution_count": null,
70
+ "metadata": {},
71
+ "outputs": [],
72
+ "source": "import matplotlib.pyplot as plt\nfrom sklearn.metrics import ConfusionMatrixDisplay\n\n\ndef plot_loss_curves(trainer, model_name: str, output_dir: str):\n \"\"\"Plot training and validation loss curves from Trainer log history.\"\"\"\n plots_dir = Path(output_dir) / \"plots\"\n plots_dir.mkdir(parents=True, exist_ok=True)\n\n history = trainer.state.log_history\n\n train_steps, train_loss = [], []\n eval_steps, eval_loss = [], []\n eval_epochs, eval_macro_f1, eval_weighted_f1, eval_mcc = [], [], [], []\n\n for entry in history:\n if \"loss\" in entry and \"eval_loss\" not in entry:\n train_steps.append(entry[\"step\"])\n train_loss.append(entry[\"loss\"])\n if \"eval_loss\" in entry:\n eval_steps.append(entry[\"step\"])\n eval_loss.append(entry[\"eval_loss\"])\n eval_epochs.append(entry.get(\"epoch\", 0))\n eval_macro_f1.append(entry.get(\"eval_macro_f1\", 0))\n eval_weighted_f1.append(entry.get(\"eval_weighted_f1\", 0))\n eval_mcc.append(entry.get(\"eval_mcc\", 0))\n\n fig, axes = plt.subplots(1, 2, figsize=(14, 4))\n\n # Loss curves\n ax = axes[0]\n ax.plot(train_steps, train_loss, label=\"Train loss\", alpha=0.7)\n ax.plot(eval_steps, eval_loss, \"o-\", label=\"Val loss\", markersize=6)\n ax.set_xlabel(\"Step\")\n ax.set_ylabel(\"Loss\")\n ax.set_title(f\"{model_name} — Loss\")\n ax.legend()\n ax.grid(True, alpha=0.3)\n\n # Metrics per epoch\n ax = axes[1]\n ax.plot(eval_epochs, eval_macro_f1, \"s-\", label=\"Macro F1\", markersize=6)\n ax.plot(eval_epochs, eval_weighted_f1, \"^-\", label=\"Weighted F1\", markersize=6)\n ax.plot(eval_epochs, eval_mcc, \"D-\", label=\"MCC\", markersize=6)\n ax.set_xlabel(\"Epoch\")\n ax.set_ylabel(\"Score\")\n ax.set_title(f\"{model_name} — Eval Metrics\")\n ax.set_ylim(0, 1)\n ax.legend()\n ax.grid(True, alpha=0.3)\n\n plt.tight_layout()\n fig.savefig(plots_dir / \"loss_curves.png\", dpi=150)\n plt.show()\n print(f\"Saved to {plots_dir / 'loss_curves.png'}\")\n\n\ndef save_best(trainer, model, tokenizer, train_args, model_type: str):\n best_dir = Path(train_args.output_dir) / \"best\"\n best_dir.mkdir(parents=True, exist_ok=True)\n\n trainer.save_model(str(best_dir))\n tokenizer.save_pretrained(str(best_dir))\n torch.save(class_weights, best_dir / \"class_weights.pt\")\n\n train_config = {\n \"model_type\": model_type,\n \"pretrained\": model.config._name_or_path,\n \"epochs\": train_args.epochs,\n \"batch_size\": train_args.batch_size,\n \"lr\": train_args.lr,\n \"max_length\": train_args.max_length,\n \"class_weights\": class_weights.tolist(),\n \"num_labels\": NUM_LABELS,\n \"id2label\": ID2LABEL,\n \"label2id\": LABEL2ID,\n }\n with open(best_dir / \"train_config.json\", \"w\") as f:\n json.dump(train_config, f, indent=2)\n\n print(f\"Best model saved to {best_dir}\")\n\n\ndef evaluate_model(model_dir: Path, dd, model_name: str, split=\"test\", batch_size=32, seed=42):\n \"\"\"Run Trainer.predict() and print full metrics report.\"\"\"\n plots_dir = model_dir.parent / \"plots\"\n plots_dir.mkdir(parents=True, exist_ok=True)\n\n model = AutoModelForSequenceClassification.from_pretrained(str(model_dir))\n ds = dd[split]\n\n eval_args = TrainingArguments(\n output_dir=\"/tmp/eval_output\",\n per_device_eval_batch_size=batch_size,\n report_to=\"none\",\n seed=seed,\n )\n trainer = Trainer(model=model, args=eval_args)\n predictions = trainer.predict(ds)\n\n preds = np.argmax(predictions.predictions, axis=-1)\n labels = predictions.label_ids\n target_names = [ID2LABEL[i] for i in range(3)]\n\n weighted_f1 = f1_score(labels, preds, average=\"weighted\")\n macro_f1 = f1_score(labels, preds, average=\"macro\")\n mcc = matthews_corrcoef(labels, preds)\n\n print(f\"\\n{'='*60}\")\n print(f\" {model_name} — {split} split ({len(ds):,} samples)\")\n print(f\"{'='*60}\\n\")\n print(f\" Weighted F1: {weighted_f1:.4f}\")\n print(f\" Macro F1: {macro_f1:.4f}\")\n print(f\" MCC: {mcc:.4f}\\n\")\n\n print(classification_report(labels, preds, target_names=target_names, digits=4))\n\n # Confusion matrix\n cm = confusion_matrix(labels, preds, labels=[0, 1, 2])\n fig, ax = plt.subplots(figsize=(6, 5))\n ConfusionMatrixDisplay(cm, display_labels=target_names).plot(ax=ax, cmap=\"Blues\", values_format=\",\")\n ax.set_title(f\"{model_name} — {split}\")\n plt.tight_layout()\n fig.savefig(plots_dir / f\"confusion_matrix_{split}.png\", dpi=150)\n plt.show()\n print(f\"Saved to {plots_dir / f'confusion_matrix_{split}.png'}\")\n\n # save metrics json\n report_dict = classification_report(labels, preds, target_names=target_names, output_dict=True)\n report_dict[\"weighted_f1\"] = weighted_f1\n report_dict[\"macro_f1\"] = macro_f1\n report_dict[\"mcc\"] = mcc\n\n out_path = model_dir / f\"{split}_metrics.json\"\n with open(out_path, \"w\") as f:\n json.dump(report_dict, f, indent=2)\n print(f\"Metrics saved to {out_path}\")\n\n return report_dict"
73
+ },
74
+ {
75
+ "cell_type": "markdown",
76
+ "metadata": {},
77
+ "source": [
78
+ "---\n",
79
+ "## 4. Train DistilBERT"
80
+ ]
81
+ },
82
+ {
83
+ "cell_type": "code",
84
+ "execution_count": null,
85
+ "metadata": {},
86
+ "outputs": [],
87
+ "source": "distilbert_model = load_distilbert()\ndistilbert_tokenizer = load_distilbert_tokenizer()\n\ndd_distilbert = builder.build_hf_dataset_dict(distilbert_tokenizer, raw_splits=raw_splits)\n\nprint(f\"Params: {sum(p.numel() for p in distilbert_model.parameters()):,}\")\nprint(f\"Train: {len(dd_distilbert['train']):,} Val: {len(dd_distilbert['val']):,} Test: {len(dd_distilbert['test']):,}\")"
88
+ },
89
+ {
90
+ "cell_type": "code",
91
+ "execution_count": null,
92
+ "metadata": {},
93
+ "outputs": [],
94
+ "source": "distilbert_trainer = WeightedTrainer(\n class_weights=class_weights,\n model=distilbert_model,\n args=distilbert_args.to_training_arguments(),\n train_dataset=dd_distilbert[\"train\"],\n eval_dataset=dd_distilbert[\"val\"],\n compute_metrics=compute_metrics,\n callbacks=[EarlyStoppingCallback(early_stopping_patience=distilbert_args.patience)]\n if distilbert_args.patience > 0 else [],\n)\n\ndistilbert_trainer.train()"
95
+ },
96
+ {
97
+ "cell_type": "code",
98
+ "source": "plot_loss_curves(distilbert_trainer, \"DistilBERT\", distilbert_args.output_dir)",
99
+ "metadata": {},
100
+ "execution_count": null,
101
+ "outputs": []
102
+ },
103
+ {
104
+ "cell_type": "code",
105
+ "execution_count": null,
106
+ "metadata": {},
107
+ "outputs": [],
108
+ "source": "save_best(distilbert_trainer, distilbert_model, distilbert_tokenizer, distilbert_args, \"distilbert\")"
109
+ },
110
+ {
111
+ "cell_type": "markdown",
112
+ "metadata": {},
113
+ "source": [
114
+ "### 4.1 Evaluate DistilBERT"
115
+ ]
116
+ },
117
+ {
118
+ "cell_type": "code",
119
+ "execution_count": null,
120
+ "metadata": {},
121
+ "outputs": [],
122
+ "source": "distilbert_best = Path(distilbert_args.output_dir) / \"best\"\ndistilbert_val_metrics = evaluate_model(distilbert_best, dd_distilbert, \"DistilBERT\", split=\"val\",\n batch_size=distilbert_args.batch_size * 2,\n seed=distilbert_args.seed)"
123
+ },
124
+ {
125
+ "cell_type": "code",
126
+ "execution_count": null,
127
+ "metadata": {},
128
+ "outputs": [],
129
+ "source": "distilbert_test_metrics = evaluate_model(distilbert_best, dd_distilbert, \"DistilBERT\", split=\"test\",\n batch_size=distilbert_args.batch_size * 2,\n seed=distilbert_args.seed)"
130
+ },
131
+ {
132
+ "cell_type": "markdown",
133
+ "metadata": {},
134
+ "source": [
135
+ "---\n",
136
+ "## 5. Train BERT"
137
+ ]
138
+ },
139
+ {
140
+ "cell_type": "code",
141
+ "execution_count": null,
142
+ "metadata": {},
143
+ "outputs": [],
144
+ "source": "bert_model = load_bert()\nbert_tokenizer = load_bert_tokenizer()\n\ndd_bert = builder.build_hf_dataset_dict(bert_tokenizer, raw_splits=raw_splits)\n\nprint(f\"Params: {sum(p.numel() for p in bert_model.parameters()):,}\")\nprint(f\"Train: {len(dd_bert['train']):,} Val: {len(dd_bert['val']):,} Test: {len(dd_bert['test']):,}\")"
145
+ },
146
+ {
147
+ "cell_type": "code",
148
+ "execution_count": null,
149
+ "metadata": {},
150
+ "outputs": [],
151
+ "source": "bert_trainer = WeightedTrainer(\n class_weights=class_weights,\n model=bert_model,\n args=bert_args.to_training_arguments(),\n train_dataset=dd_bert[\"train\"],\n eval_dataset=dd_bert[\"val\"],\n compute_metrics=compute_metrics,\n callbacks=[EarlyStoppingCallback(early_stopping_patience=bert_args.patience)]\n if bert_args.patience > 0 else [],\n)\n\nbert_trainer.train()"
152
+ },
153
+ {
154
+ "cell_type": "code",
155
+ "source": "plot_loss_curves(bert_trainer, \"BERT\", bert_args.output_dir)",
156
+ "metadata": {},
157
+ "execution_count": null,
158
+ "outputs": []
159
+ },
160
+ {
161
+ "cell_type": "code",
162
+ "execution_count": null,
163
+ "metadata": {},
164
+ "outputs": [],
165
+ "source": "save_best(bert_trainer, bert_model, bert_tokenizer, bert_args, \"bert\")"
166
+ },
167
+ {
168
+ "cell_type": "markdown",
169
+ "metadata": {},
170
+ "source": [
171
+ "### 5.1 Evaluate BERT"
172
+ ]
173
+ },
174
+ {
175
+ "cell_type": "code",
176
+ "execution_count": null,
177
+ "metadata": {},
178
+ "outputs": [],
179
+ "source": "bert_best = Path(bert_args.output_dir) / \"best\"\nbert_val_metrics = evaluate_model(bert_best, dd_bert, \"BERT\", split=\"val\",\n batch_size=bert_args.batch_size * 2,\n seed=bert_args.seed)"
180
+ },
181
+ {
182
+ "cell_type": "code",
183
+ "execution_count": null,
184
+ "metadata": {},
185
+ "outputs": [],
186
+ "source": "bert_test_metrics = evaluate_model(bert_best, dd_bert, \"BERT\", split=\"test\",\n batch_size=bert_args.batch_size * 2,\n seed=bert_args.seed)"
187
+ },
188
+ {
189
+ "cell_type": "markdown",
190
+ "source": "---\n## 6. Train DeBERTa",
191
+ "metadata": {}
192
+ },
193
+ {
194
+ "cell_type": "code",
195
+ "source": "deberta_model = load_deberta()\ndeberta_tokenizer = load_deberta_tokenizer()\n\ndd_deberta = builder.build_hf_dataset_dict(deberta_tokenizer, raw_splits=raw_splits)\n\nprint(f\"Params: {sum(p.numel() for p in deberta_model.parameters()):,}\")\nprint(f\"Train: {len(dd_deberta['train']):,} Val: {len(dd_deberta['val']):,} Test: {len(dd_deberta['test']):,}\")",
196
+ "metadata": {},
197
+ "execution_count": null,
198
+ "outputs": []
199
+ },
200
+ {
201
+ "cell_type": "code",
202
+ "source": "deberta_trainer = WeightedTrainer(\n class_weights=class_weights,\n model=deberta_model,\n args=deberta_args.to_training_arguments(),\n train_dataset=dd_deberta[\"train\"],\n eval_dataset=dd_deberta[\"val\"],\n compute_metrics=compute_metrics,\n callbacks=[EarlyStoppingCallback(early_stopping_patience=deberta_args.patience)]\n if deberta_args.patience > 0 else [],\n)\n\ndeberta_trainer.train()",
203
+ "metadata": {},
204
+ "execution_count": null,
205
+ "outputs": []
206
+ },
207
+ {
208
+ "cell_type": "code",
209
+ "source": "plot_loss_curves(deberta_trainer, \"DeBERTa\", deberta_args.output_dir)",
210
+ "metadata": {},
211
+ "execution_count": null,
212
+ "outputs": []
213
+ },
214
+ {
215
+ "cell_type": "code",
216
+ "source": "save_best(deberta_trainer, deberta_model, deberta_tokenizer, deberta_args, \"deberta\")",
217
+ "metadata": {},
218
+ "execution_count": null,
219
+ "outputs": []
220
+ },
221
+ {
222
+ "cell_type": "markdown",
223
+ "source": "### 6.1 Evaluate DeBERTa",
224
+ "metadata": {}
225
+ },
226
+ {
227
+ "cell_type": "code",
228
+ "source": "deberta_best = Path(deberta_args.output_dir) / \"best\"\ndeberta_val_metrics = evaluate_model(deberta_best, dd_deberta, \"DeBERTa\", split=\"val\",\n batch_size=deberta_args.batch_size * 2,\n seed=deberta_args.seed)",
229
+ "metadata": {},
230
+ "execution_count": null,
231
+ "outputs": []
232
+ },
233
+ {
234
+ "cell_type": "code",
235
+ "source": "deberta_test_metrics = evaluate_model(deberta_best, dd_deberta, \"DeBERTa\", split=\"test\",\n batch_size=deberta_args.batch_size * 2,\n seed=deberta_args.seed)",
236
+ "metadata": {},
237
+ "execution_count": null,
238
+ "outputs": []
239
+ },
240
+ {
241
+ "cell_type": "markdown",
242
+ "metadata": {},
243
+ "source": "---\n## 7. Compare models"
244
+ },
245
+ {
246
+ "cell_type": "code",
247
+ "execution_count": null,
248
+ "metadata": {},
249
+ "outputs": [],
250
+ "source": "import pandas as pd\n\ncomparison = pd.DataFrame({\n \"Metric\": [\"Weighted F1\", \"Macro F1\", \"MCC\",\n \"F1 SAME_PARA\", \"F1 NEW_PARA\", \"F1 NEWLINE\"],\n \"DistilBERT\": [\n distilbert_test_metrics[\"weighted_f1\"],\n distilbert_test_metrics[\"macro_f1\"],\n distilbert_test_metrics[\"mcc\"],\n distilbert_test_metrics[\"SAME_PARAGRAPH\"][\"f1-score\"],\n distilbert_test_metrics[\"NEW_PARAGRAPH\"][\"f1-score\"],\n distilbert_test_metrics[\"NEWLINE\"][\"f1-score\"],\n ],\n \"BERT\": [\n bert_test_metrics[\"weighted_f1\"],\n bert_test_metrics[\"macro_f1\"],\n bert_test_metrics[\"mcc\"],\n bert_test_metrics[\"SAME_PARAGRAPH\"][\"f1-score\"],\n bert_test_metrics[\"NEW_PARAGRAPH\"][\"f1-score\"],\n bert_test_metrics[\"NEWLINE\"][\"f1-score\"],\n ],\n \"DeBERTa\": [\n deberta_test_metrics[\"weighted_f1\"],\n deberta_test_metrics[\"macro_f1\"],\n deberta_test_metrics[\"mcc\"],\n deberta_test_metrics[\"SAME_PARAGRAPH\"][\"f1-score\"],\n deberta_test_metrics[\"NEW_PARAGRAPH\"][\"f1-score\"],\n deberta_test_metrics[\"NEWLINE\"][\"f1-score\"],\n ],\n})\n\ncomparison = comparison.set_index(\"Metric\")\ncomparison = comparison.round(4)\ncomparison"
251
+ },
252
+ {
253
+ "cell_type": "code",
254
+ "execution_count": null,
255
+ "metadata": {},
256
+ "outputs": [],
257
+ "source": "import matplotlib.pyplot as plt\n\ncomparison_dir = Path(\"checkpoints/plots\")\ncomparison_dir.mkdir(parents=True, exist_ok=True)\n\nmetrics_to_plot = [\"Weighted F1\", \"Macro F1\", \"MCC\"]\nplot_data = comparison.loc[metrics_to_plot]\n\nax = plot_data.plot.bar(rot=0, figsize=(10, 4))\nax.set_ylim(0, 1)\nax.set_ylabel(\"Score\")\nax.set_title(\"DistilBERT vs BERT vs DeBERTa — Test Set\")\nax.legend(loc=\"lower right\")\n\nfor container in ax.containers:\n ax.bar_label(container, fmt=\"%.3f\", fontsize=8, padding=2)\n\nplt.tight_layout()\nplt.savefig(comparison_dir / \"comparison_main_metrics.png\", dpi=150)\nplt.show()\nprint(f\"Saved to {comparison_dir / 'comparison_main_metrics.png'}\")"
258
+ },
259
+ {
260
+ "cell_type": "code",
261
+ "execution_count": null,
262
+ "metadata": {},
263
+ "outputs": [],
264
+ "source": "per_class = [\"F1 SAME_PARA\", \"F1 NEW_PARA\", \"F1 NEWLINE\"]\nplot_data = comparison.loc[per_class]\n\nax = plot_data.plot.bar(rot=0, figsize=(10, 4))\nax.set_ylim(0, 1)\nax.set_ylabel(\"F1 Score\")\nax.set_title(\"Per-class F1 — Test Set\")\nax.legend(loc=\"lower right\")\n\nfor container in ax.containers:\n ax.bar_label(container, fmt=\"%.3f\", fontsize=8, padding=2)\n\nplt.tight_layout()\nplt.savefig(comparison_dir / \"comparison_per_class_f1.png\", dpi=150)\nplt.show()\nprint(f\"Saved to {comparison_dir / 'comparison_per_class_f1.png'}\")"
265
+ }
266
+ ],
267
+ "metadata": {
268
+ "kernelspec": {
269
+ "display_name": "Python 3",
270
+ "language": "python",
271
+ "name": "python3"
272
+ },
273
+ "language_info": {
274
+ "name": "python",
275
+ "version": "3.10.0"
276
+ }
277
+ },
278
+ "nbformat": 4,
279
+ "nbformat_minor": 4
280
+ }