lamossta commited on
Commit
b12b2c4
·
1 Parent(s): b6d276f

training notebooks

Browse files
notebooks/train_fasttext.ipynb ADDED
@@ -0,0 +1,161 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "id": "f83ef6b7",
6
+ "source": "# Train: fastText Baseline\n\nTrain a fastText supervised classifier on entity-sentiment marker-mode text.\n\n**Structure**\n1. Load & Prepare Data\n2. Training\n3. Evaluation\n4. Training & Evaluation — Deduplicated",
7
+ "metadata": {}
8
+ },
9
+ {
10
+ "cell_type": "code",
11
+ "id": "1225a248",
12
+ "source": "import os\nimport sys\nfrom collections import Counter\n\nimport numpy as np\nimport matplotlib.pyplot as plt\nimport pandas as pd\nfrom sklearn.metrics import f1_score, classification_report\n\nsys.path.insert(0, os.path.abspath(\"..\"))\n\nfrom src.models.fasttext import (\n prepare_data,\n train,\n evaluate,\n evaluate_entity_level,\n predict_samples,\n MODE,\n LABEL_PREFIX,\n _write_fasttext_file,\n)\nfrom src.models.dataset import deduplicate_positions, flatten_to_examples, load_data, split_data\nfrom src.schemas.labels import SENTIMENT_LABELS",
13
+ "metadata": {},
14
+ "execution_count": null,
15
+ "outputs": []
16
+ },
17
+ {
18
+ "cell_type": "markdown",
19
+ "id": "d2d3f288",
20
+ "source": "## 1. Load & Prepare Data",
21
+ "metadata": {}
22
+ },
23
+ {
24
+ "cell_type": "code",
25
+ "id": "d7e4bf2f",
26
+ "source": "DATA_PATH = \"../data/data_augmented_256.jsonl\"\nOUTPUT_DIR = \"../models/fasttext\"\nSEED = 42\nVAL_SPLIT = 0.1\nTEST_SPLIT = 0.1\n\nLR = 0.5\nEPOCH = 25\nWORD_NGRAMS = 2\nDIM = 100",
27
+ "metadata": {},
28
+ "execution_count": null,
29
+ "outputs": []
30
+ },
31
+ {
32
+ "cell_type": "code",
33
+ "id": "ec7421a5",
34
+ "source": "samples = load_data(DATA_PATH)\nprint(f\"Loaded {len(samples)} samples\")",
35
+ "metadata": {},
36
+ "execution_count": null,
37
+ "outputs": []
38
+ },
39
+ {
40
+ "cell_type": "code",
41
+ "id": "f405ae7f",
42
+ "source": "examples = flatten_to_examples(samples, mode=MODE)\n\nn_ents = len({(e[\"sample_id\"], e[\"entity_id\"]) for e in examples})\nprint(f\"{len(samples)} samples -> {n_ents} entities -> {len(examples)} examples\")\n\ndist = Counter(SENTIMENT_LABELS.id2label[e[\"label\"]] for e in examples)\nprint(f\"Label distribution: {dict(dist)}\")",
43
+ "metadata": {},
44
+ "execution_count": null,
45
+ "outputs": []
46
+ },
47
+ {
48
+ "cell_type": "code",
49
+ "id": "f1cff37c",
50
+ "source": "train_exs, val_exs, test_exs = split_data(\n examples, VAL_SPLIT, TEST_SPLIT, seed=SEED\n)\nprint(f\"Train: {len(train_exs)} | Val: {len(val_exs)} | Test: {len(test_exs)}\")",
51
+ "metadata": {},
52
+ "execution_count": null,
53
+ "outputs": []
54
+ },
55
+ {
56
+ "cell_type": "markdown",
57
+ "id": "381fe572",
58
+ "source": "## 2. Training",
59
+ "metadata": {}
60
+ },
61
+ {
62
+ "cell_type": "code",
63
+ "id": "cb664641",
64
+ "source": "model = train(\n train_exs,\n val_exs,\n output_dir=OUTPUT_DIR,\n lr=LR,\n epoch=EPOCH,\n word_ngrams=WORD_NGRAMS,\n dim=DIM,\n)",
65
+ "metadata": {},
66
+ "execution_count": null,
67
+ "outputs": []
68
+ },
69
+ {
70
+ "cell_type": "markdown",
71
+ "id": "5fc12804",
72
+ "source": "## 3. Evaluation\n\n### Per-position evaluation",
73
+ "metadata": {}
74
+ },
75
+ {
76
+ "cell_type": "code",
77
+ "id": "281f15be",
78
+ "source": "macro_f1 = evaluate(model, test_exs, split_name=\"test\")",
79
+ "metadata": {},
80
+ "execution_count": null,
81
+ "outputs": []
82
+ },
83
+ {
84
+ "cell_type": "markdown",
85
+ "id": "776cc993",
86
+ "source": "### Entity-level evaluation (highest-confidence aggregation)\n\nWhen an entity has multiple positions, each produces a separate prediction.\nHere we aggregate by keeping the prediction with the highest confidence per entity,\nmatching the production inference behavior.",
87
+ "metadata": {}
88
+ },
89
+ {
90
+ "cell_type": "code",
91
+ "id": "a3658e91",
92
+ "source": "macro_f1_entity = evaluate_entity_level(model, test_exs, split_name=\"test\")",
93
+ "metadata": {},
94
+ "execution_count": null,
95
+ "outputs": []
96
+ },
97
+ {
98
+ "cell_type": "markdown",
99
+ "id": "53f6d155",
100
+ "source": "## 4. Training & Evaluation — Deduplicated",
101
+ "metadata": {}
102
+ },
103
+ {
104
+ "cell_type": "code",
105
+ "id": "67d96471",
106
+ "source": "deduped_samples = deduplicate_positions(samples)\ndedup_examples = flatten_to_examples(deduped_samples, mode=MODE)\n\nn_ents_dedup = len({(e[\"sample_id\"], e[\"entity_id\"]) for e in dedup_examples})\nprint(f\"{len(deduped_samples)} samples -> {n_ents_dedup} entities -> {len(dedup_examples)} examples (deduplicated)\")\n\ndist_dedup = Counter(SENTIMENT_LABELS.id2label[e[\"label\"]] for e in dedup_examples)\nprint(f\"Label distribution: {dict(dist_dedup)}\")\n\ntrain_dedup, val_dedup, test_dedup = split_data(\n dedup_examples, VAL_SPLIT, TEST_SPLIT, seed=SEED\n)\nprint(f\"Train: {len(train_dedup)} | Val: {len(val_dedup)} | Test: {len(test_dedup)}\")",
107
+ "metadata": {},
108
+ "execution_count": null,
109
+ "outputs": []
110
+ },
111
+ {
112
+ "cell_type": "code",
113
+ "id": "44bd734f",
114
+ "source": "model_dedup = train(\n train_dedup,\n val_dedup,\n output_dir=os.path.join(OUTPUT_DIR, \"dedup\"),\n lr=LR,\n epoch=EPOCH,\n word_ngrams=WORD_NGRAMS,\n dim=DIM,\n)",
115
+ "metadata": {},
116
+ "execution_count": null,
117
+ "outputs": []
118
+ },
119
+ {
120
+ "cell_type": "markdown",
121
+ "id": "471dd000",
122
+ "source": "### Dedup test evaluation",
123
+ "metadata": {}
124
+ },
125
+ {
126
+ "cell_type": "code",
127
+ "id": "b0b5232d",
128
+ "source": "macro_f1_dedup = evaluate(model_dedup, test_dedup, split_name=\"test (deduplicated)\")",
129
+ "metadata": {},
130
+ "execution_count": null,
131
+ "outputs": []
132
+ },
133
+ {
134
+ "cell_type": "markdown",
135
+ "id": "b9288376",
136
+ "source": "### Comparison: All Positions vs Deduplicated",
137
+ "metadata": {}
138
+ },
139
+ {
140
+ "cell_type": "code",
141
+ "id": "60ecd7da",
142
+ "source": "comparison = pd.DataFrame({\n \"variant\": [\n \"all positions (per-position)\",\n \"all positions (entity-level)\",\n \"deduplicated (per-position)\",\n ],\n \"train_examples\": [len(train_exs), len(train_exs), len(train_dedup)],\n \"test_macro_f1\": [\n round(macro_f1, 4),\n round(macro_f1_entity, 4),\n round(macro_f1_dedup, 4),\n ],\n})\ncomparison",
143
+ "metadata": {},
144
+ "execution_count": null,
145
+ "outputs": []
146
+ }
147
+ ],
148
+ "metadata": {
149
+ "kernelspec": {
150
+ "display_name": "Python 3",
151
+ "language": "python",
152
+ "name": "python3"
153
+ },
154
+ "language_info": {
155
+ "name": "python",
156
+ "version": "3.13.0"
157
+ }
158
+ },
159
+ "nbformat": 4,
160
+ "nbformat_minor": 5
161
+ }
notebooks/train_marker.ipynb ADDED
@@ -0,0 +1,417 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "id": "a1",
6
+ "metadata": {},
7
+ "source": [
8
+ "# Train: Marker Mode\n",
9
+ "\n",
10
+ "Fine-tune DistilBERT with `[E]...[/E]` entity-wrapping (single-sequence input).\n",
11
+ "\n",
12
+ "**Structure**\n",
13
+ "1. Load Config & Data\n",
14
+ "2. Tokenizer & Model\n",
15
+ "3. Training\n",
16
+ "4. Evaluation"
17
+ ]
18
+ },
19
+ {
20
+ "cell_type": "code",
21
+ "id": "a2",
22
+ "metadata": {
23
+ "ExecuteTime": {
24
+ "end_time": "2026-04-18T23:17:44.162106Z",
25
+ "start_time": "2026-04-18T23:17:41.253237Z"
26
+ }
27
+ },
28
+ "source": [
29
+ "import os\n",
30
+ "import sys\n",
31
+ "import random\n",
32
+ "from collections import Counter\n",
33
+ "import matplotlib.pyplot as plt\n",
34
+ "import numpy as np\n",
35
+ "import torch\n",
36
+ "torch.backends.cuda.enable_cudnn_sdp(False)\n",
37
+ "from sklearn.metrics import classification_report, f1_score\n",
38
+ "from transformers import (\n",
39
+ " AutoModelForSequenceClassification,\n",
40
+ " AutoTokenizer,\n",
41
+ " EarlyStoppingCallback,\n",
42
+ ")\n",
43
+ "\n",
44
+ "sys.path.insert(0, os.path.abspath(\"..\"))\n",
45
+ "\n",
46
+ "from src.schemas.config import TrainingConfig\n",
47
+ "from src.schemas.labels import MODES, SENTIMENT_LABELS\n",
48
+ "from src.models.dataset import (\n",
49
+ " EntitySentimentDataset,\n",
50
+ " deduplicate_positions,\n",
51
+ " flatten_to_examples,\n",
52
+ " load_data,\n",
53
+ " split_data,\n",
54
+ ")\n",
55
+ "from src.models.distillbert import (\n",
56
+ " WeightedLossTrainer,\n",
57
+ " compute_class_weights,\n",
58
+ " make_compute_metrics,\n",
59
+ ")"
60
+ ],
61
+ "outputs": [],
62
+ "execution_count": null
63
+ },
64
+ {
65
+ "cell_type": "markdown",
66
+ "id": "a3",
67
+ "metadata": {},
68
+ "source": [
69
+ "## 1. Load Config & Data"
70
+ ]
71
+ },
72
+ {
73
+ "cell_type": "code",
74
+ "id": "a4",
75
+ "metadata": {
76
+ "ExecuteTime": {
77
+ "end_time": "2026-04-18T23:17:44.232796Z",
78
+ "start_time": "2026-04-18T23:17:44.227568Z"
79
+ }
80
+ },
81
+ "source": [
82
+ "cfg = TrainingConfig.from_json(\"../data/config_marker.json\")\n",
83
+ "mode_cfg = MODES[cfg.mode]\n",
84
+ "\n",
85
+ "random.seed(cfg.seed)\n",
86
+ "np.random.seed(cfg.seed)\n",
87
+ "torch.manual_seed(cfg.seed)\n",
88
+ "\n",
89
+ "device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n",
90
+ "print(f\"Device: {device} | Mode: {cfg.mode}\")"
91
+ ],
92
+ "outputs": [
93
+ {
94
+ "name": "stdout",
95
+ "output_type": "stream",
96
+ "text": [
97
+ "Device: cpu | Mode: marker\n"
98
+ ]
99
+ }
100
+ ],
101
+ "execution_count": 2
102
+ },
103
+ {
104
+ "cell_type": "code",
105
+ "id": "a5",
106
+ "metadata": {
107
+ "ExecuteTime": {
108
+ "end_time": "2026-04-18T23:17:52.714033Z",
109
+ "start_time": "2026-04-18T23:17:52.556959Z"
110
+ }
111
+ },
112
+ "source": "samples = load_data(os.path.join(\"..\", cfg.data_path))\nexamples = flatten_to_examples(samples, mode=cfg.mode)\n\nn_ents = len({(e[\"sample_id\"], e[\"entity_id\"]) for e in examples})\nprint(f\"{len(samples)} samples -> {n_ents} entities -> {len(examples)} examples\")\n\ndist = Counter(SENTIMENT_LABELS.id2label[e[\"label\"]] for e in examples)\nprint(f\"Label distribution: {dict(dist)}\")",
113
+ "outputs": [],
114
+ "execution_count": null
115
+ },
116
+ {
117
+ "cell_type": "code",
118
+ "id": "a6",
119
+ "metadata": {
120
+ "ExecuteTime": {
121
+ "end_time": "2026-04-18T23:17:57.689676Z",
122
+ "start_time": "2026-04-18T23:17:57.680885Z"
123
+ }
124
+ },
125
+ "source": [
126
+ "train_exs, val_exs, test_exs = split_data(\n",
127
+ " examples, cfg.val_split, cfg.test_split, seed=cfg.seed\n",
128
+ ")\n",
129
+ "print(f\"Train: {len(train_exs)} | Val: {len(val_exs)} | Test: {len(test_exs)}\")"
130
+ ],
131
+ "outputs": [
132
+ {
133
+ "name": "stdout",
134
+ "output_type": "stream",
135
+ "text": [
136
+ "Train: 21313 | Val: 2796 | Test: 2667\n"
137
+ ]
138
+ }
139
+ ],
140
+ "execution_count": 5
141
+ },
142
+ {
143
+ "cell_type": "markdown",
144
+ "id": "7ac38d1e",
145
+ "source": "### Deduplicated dataset (one position per entity)",
146
+ "metadata": {}
147
+ },
148
+ {
149
+ "cell_type": "code",
150
+ "id": "31e27ba1",
151
+ "source": "deduped_samples = deduplicate_positions(samples)\ndedup_examples = flatten_to_examples(deduped_samples, mode=cfg.mode)\n\nn_ents_dedup = len({(e[\"sample_id\"], e[\"entity_id\"]) for e in dedup_examples})\nprint(f\"{len(deduped_samples)} samples -> {n_ents_dedup} entities -> {len(dedup_examples)} examples (deduplicated)\")\n\ndist_dedup = Counter(SENTIMENT_LABELS.id2label[e[\"label\"]] for e in dedup_examples)\nprint(f\"Label distribution: {dict(dist_dedup)}\")\n\ntrain_dedup, val_dedup, test_dedup = split_data(\n dedup_examples, cfg.val_split, cfg.test_split, seed=cfg.seed\n)\nprint(f\"Train: {len(train_dedup)} | Val: {len(val_dedup)} | Test: {len(test_dedup)}\")",
152
+ "metadata": {},
153
+ "execution_count": null,
154
+ "outputs": []
155
+ },
156
+ {
157
+ "cell_type": "markdown",
158
+ "id": "a7",
159
+ "metadata": {},
160
+ "source": [
161
+ "## 2. Tokenizer & Model"
162
+ ]
163
+ },
164
+ {
165
+ "cell_type": "code",
166
+ "execution_count": null,
167
+ "id": "a8",
168
+ "metadata": {},
169
+ "outputs": [],
170
+ "source": [
171
+ "tokenizer = AutoTokenizer.from_pretrained(cfg.model_name)\n",
172
+ "tokenizer.add_special_tokens(\n",
173
+ " {\"additional_special_tokens\": [mode_cfg.entity_start, mode_cfg.entity_end]}\n",
174
+ ")\n",
175
+ "\n",
176
+ "model = AutoModelForSequenceClassification.from_pretrained(\n",
177
+ " cfg.model_name,\n",
178
+ " num_labels=mode_cfg.labels.num_labels,\n",
179
+ " id2label=mode_cfg.labels.id2label,\n",
180
+ " label2id=mode_cfg.labels.label2id,\n",
181
+ ")\n",
182
+ "model.resize_token_embeddings(len(tokenizer))\n",
183
+ "print(f\"Model vocab size: {len(tokenizer)}\")"
184
+ ]
185
+ },
186
+ {
187
+ "cell_type": "code",
188
+ "id": "33b27fd2",
189
+ "source": "special_tokens = [\n t.content for t in tokenizer.added_tokens_decoder.values()\n if t.special and t.content not in {\n tokenizer.cls_token, tokenizer.sep_token,\n tokenizer.pad_token, tokenizer.unk_token, tokenizer.mask_token,\n }\n]\nprint(\"Special tokens:\", special_tokens)\nprint()\nfor token in special_tokens:\n token_id = tokenizer.convert_tokens_to_ids(token)\n print(f\" {token!r} -> token_id={token_id}\")\n\nsample_text = f\"{mode_cfg.entity_start} Apple {mode_cfg.entity_end} released a new product.\"\nencoded = tokenizer(sample_text, add_special_tokens=False)\ndecoded_tokens = tokenizer.convert_ids_to_tokens(encoded[\"input_ids\"])\nprint(f\"\\nSample: {sample_text!r}\")\nprint(f\"Tokens: {decoded_tokens}\")",
190
+ "metadata": {},
191
+ "execution_count": null,
192
+ "outputs": []
193
+ },
194
+ {
195
+ "cell_type": "markdown",
196
+ "id": "ac72d17e",
197
+ "source": "### Truncation Analysis",
198
+ "metadata": {}
199
+ },
200
+ {
201
+ "cell_type": "code",
202
+ "id": "13f4e56e",
203
+ "source": "lengths = []\nfor ex in examples:\n enc = tokenizer(ex[\"seg_a\"], truncation=False)\n lengths.append(len(enc[\"input_ids\"]))\n\nlengths = np.array(lengths)\nn_truncated = (lengths > cfg.max_len).sum()\nprint(f\"Max length: {cfg.max_len}\")\nprint(f\"Total examples: {len(lengths)}\")\nprint(f\"Truncated: {n_truncated} ({100 * n_truncated / len(lengths):.1f}%)\")\nprint(f\"Token length — min: {lengths.min()}, median: {int(np.median(lengths))}, \"\n f\"mean: {lengths.mean():.0f}, max: {lengths.max()}\")\n\nfig, ax = plt.subplots(figsize=(10, 3))\nax.hist(lengths, bins=50, edgecolor=\"black\", linewidth=0.5)\nax.axvline(cfg.max_len, color=\"red\", linestyle=\"--\", label=f\"max_len={cfg.max_len}\")\nax.set_xlabel(\"Token count\")\nax.set_ylabel(\"Examples\")\nax.set_title(\"Input Length Distribution (marker)\")\nax.legend()\nplt.tight_layout()\nplt.show()",
204
+ "metadata": {},
205
+ "execution_count": null,
206
+ "outputs": []
207
+ },
208
+ {
209
+ "cell_type": "markdown",
210
+ "id": "a9",
211
+ "metadata": {},
212
+ "source": [
213
+ "## 3. Training"
214
+ ]
215
+ },
216
+ {
217
+ "cell_type": "code",
218
+ "execution_count": null,
219
+ "id": "a10",
220
+ "metadata": {},
221
+ "outputs": [],
222
+ "source": [
223
+ "train_ds = EntitySentimentDataset(train_exs, tokenizer, cfg.max_len)\n",
224
+ "val_ds = EntitySentimentDataset(val_exs, tokenizer, cfg.max_len)\n",
225
+ "\n",
226
+ "training_args = cfg.to_training_arguments()\n",
227
+ "training_args.output_dir = os.path.join(\"..\", cfg.output_dir)\n",
228
+ "training_args.logging_dir = os.path.join(\"..\", cfg.output_dir, \"logs\")\n",
229
+ "\n",
230
+ "class_weights = compute_class_weights(train_exs, n_classes=mode_cfg.labels.num_labels)\n",
231
+ "print(f\"Class weights: {class_weights.tolist()}\")"
232
+ ]
233
+ },
234
+ {
235
+ "cell_type": "code",
236
+ "execution_count": null,
237
+ "id": "a11",
238
+ "metadata": {},
239
+ "outputs": [],
240
+ "source": [
241
+ "trainer = WeightedLossTrainer(\n",
242
+ " model=model,\n",
243
+ " args=training_args,\n",
244
+ " train_dataset=train_ds,\n",
245
+ " eval_dataset=val_ds,\n",
246
+ " compute_metrics=make_compute_metrics(cfg.mode),\n",
247
+ " callbacks=[EarlyStoppingCallback(\n",
248
+ " early_stopping_patience=cfg.early_stopping_patience\n",
249
+ " )],\n",
250
+ " class_weights=class_weights,\n",
251
+ ")\n",
252
+ "\n",
253
+ "trainer.train()"
254
+ ]
255
+ },
256
+ {
257
+ "cell_type": "code",
258
+ "execution_count": null,
259
+ "id": "a12",
260
+ "metadata": {},
261
+ "outputs": [],
262
+ "source": [
263
+ "output_dir = os.path.join(\"..\", cfg.output_dir)\n",
264
+ "trainer.save_model(output_dir)\n",
265
+ "tokenizer.save_pretrained(output_dir)\n",
266
+ "\n",
267
+ "with open(os.path.join(output_dir, \"mode.txt\"), \"w\") as f:\n",
268
+ " f.write(cfg.mode)\n",
269
+ "\n",
270
+ "print(f\"Model saved to '{output_dir}'\")"
271
+ ]
272
+ },
273
+ {
274
+ "cell_type": "markdown",
275
+ "id": "2e7ed82a",
276
+ "source": "### Validation Metrics per Epoch",
277
+ "metadata": {}
278
+ },
279
+ {
280
+ "cell_type": "code",
281
+ "id": "ad91c2dd",
282
+ "source": "import pandas as pd\n\neval_logs = [l for l in trainer.state.log_history if \"eval_loss\" in l]\n\nrows = []\nfor l in eval_logs:\n rows.append({\n \"epoch\": int(l[\"epoch\"]),\n \"eval_loss\": round(l[\"eval_loss\"], 4),\n \"macro_f1\": round(l[\"eval_macro_f1\"], 4),\n \"f1_negative\": round(l.get(\"eval_f1_negative\", 0), 4),\n \"f1_neutral\": round(l.get(\"eval_f1_neutral\", 0), 4),\n \"f1_positive\": round(l.get(\"eval_f1_positive\", 0), 4),\n })\n\ndf_metrics = pd.DataFrame(rows)\ndf_metrics",
283
+ "metadata": {},
284
+ "execution_count": null,
285
+ "outputs": []
286
+ },
287
+ {
288
+ "cell_type": "markdown",
289
+ "id": "a13",
290
+ "metadata": {},
291
+ "source": [
292
+ "## 4. Evaluation"
293
+ ]
294
+ },
295
+ {
296
+ "cell_type": "code",
297
+ "execution_count": null,
298
+ "id": "a14",
299
+ "metadata": {},
300
+ "outputs": [],
301
+ "source": [
302
+ "test_ds = EntitySentimentDataset(test_exs, tokenizer, cfg.max_len)\n",
303
+ "pred_output = trainer.predict(test_ds)\n",
304
+ "\n",
305
+ "test_preds = np.argmax(pred_output.predictions, axis=-1).tolist()\n",
306
+ "test_labels = pred_output.label_ids.tolist()\n",
307
+ "macro_f1 = f1_score(test_labels, test_preds, average=\"macro\")\n",
308
+ "\n",
309
+ "print(f\"Test macro-F1: {macro_f1:.4f}\")\n",
310
+ "print()\n",
311
+ "print(classification_report(\n",
312
+ " test_labels, test_preds,\n",
313
+ " target_names=list(SENTIMENT_LABELS.classes),\n",
314
+ "))"
315
+ ]
316
+ },
317
+ {
318
+ "cell_type": "markdown",
319
+ "id": "bf46fe96",
320
+ "source": "### Entity-level Evaluation (highest-confidence aggregation)\n\nWhen an entity has multiple positions, each produces a separate prediction.\nHere we aggregate by keeping the prediction with the highest confidence per entity,\nmatching the production inference behavior.",
321
+ "metadata": {}
322
+ },
323
+ {
324
+ "cell_type": "code",
325
+ "id": "951b95c0",
326
+ "source": "import torch.nn.functional as F\n\nprobs = F.softmax(torch.tensor(pred_output.predictions), dim=-1).numpy()\nconfidences = probs.max(axis=-1)\npreds_all = np.argmax(probs, axis=-1)\n\nsentiments = list(SENTIMENT_LABELS.classes)\nentity_best: dict[tuple, tuple[int, int, float]] = {}\nfor ex, pred_id, conf, label in zip(test_exs, preds_all, confidences, test_labels):\n key = (ex[\"sample_id\"], ex[\"entity_id\"])\n if key not in entity_best or conf > entity_best[key][2]:\n entity_best[key] = (int(pred_id), int(label), float(conf))\n\nent_preds = [v[0] for v in entity_best.values()]\nent_labels = [v[1] for v in entity_best.values()]\n\nmacro_f1_entity = f1_score(ent_labels, ent_preds, average=\"macro\")\nprint(f\"Entity-level test macro-F1: {macro_f1_entity:.4f}\")\nprint(f\"({len(entity_best)} unique entities from {len(test_exs)} position-level examples)\")\nprint()\nprint(classification_report(\n ent_labels, ent_preds,\n target_names=sentiments,\n))",
327
+ "metadata": {},
328
+ "execution_count": null,
329
+ "outputs": []
330
+ },
331
+ {
332
+ "cell_type": "markdown",
333
+ "id": "c5aa45e7",
334
+ "source": "## 5. Training & Evaluation — Deduplicated",
335
+ "metadata": {}
336
+ },
337
+ {
338
+ "cell_type": "code",
339
+ "id": "2d95ad6b",
340
+ "source": "model_dedup = AutoModelForSequenceClassification.from_pretrained(\n cfg.model_name,\n num_labels=mode_cfg.labels.num_labels,\n id2label=mode_cfg.labels.id2label,\n label2id=mode_cfg.labels.label2id,\n)\nmodel_dedup.resize_token_embeddings(len(tokenizer))\n\ntrain_ds_dedup = EntitySentimentDataset(train_dedup, tokenizer, cfg.max_len)\nval_ds_dedup = EntitySentimentDataset(val_dedup, tokenizer, cfg.max_len)\n\ntraining_args_dedup = cfg.to_training_arguments()\ntraining_args_dedup.output_dir = os.path.join(\"..\", cfg.output_dir + \"_dedup\")\ntraining_args_dedup.logging_dir = os.path.join(\"..\", cfg.output_dir + \"_dedup\", \"logs\")\n\nclass_weights_dedup = compute_class_weights(train_dedup, n_classes=mode_cfg.labels.num_labels)\nprint(f\"Class weights (dedup): {class_weights_dedup.tolist()}\")",
341
+ "metadata": {},
342
+ "execution_count": null,
343
+ "outputs": []
344
+ },
345
+ {
346
+ "cell_type": "code",
347
+ "id": "e3283201",
348
+ "source": "trainer_dedup = WeightedLossTrainer(\n model=model_dedup,\n args=training_args_dedup,\n train_dataset=train_ds_dedup,\n eval_dataset=val_ds_dedup,\n compute_metrics=make_compute_metrics(cfg.mode),\n callbacks=[EarlyStoppingCallback(\n early_stopping_patience=cfg.early_stopping_patience\n )],\n class_weights=class_weights_dedup,\n)\n\ntrainer_dedup.train()",
349
+ "metadata": {},
350
+ "execution_count": null,
351
+ "outputs": []
352
+ },
353
+ {
354
+ "cell_type": "code",
355
+ "id": "98b3e53d",
356
+ "source": "output_dir_dedup = os.path.join(\"..\", cfg.output_dir + \"_dedup\")\ntrainer_dedup.save_model(output_dir_dedup)\ntokenizer.save_pretrained(output_dir_dedup)\n\nwith open(os.path.join(output_dir_dedup, \"mode.txt\"), \"w\") as f:\n f.write(cfg.mode)\n\nprint(f\"Dedup model saved to '{output_dir_dedup}'\")",
357
+ "metadata": {},
358
+ "execution_count": null,
359
+ "outputs": []
360
+ },
361
+ {
362
+ "cell_type": "markdown",
363
+ "id": "c774bc8b",
364
+ "source": "### Dedup Validation Metrics per Epoch",
365
+ "metadata": {}
366
+ },
367
+ {
368
+ "cell_type": "code",
369
+ "id": "880964ef",
370
+ "source": "eval_logs_d = [l for l in trainer_dedup.state.log_history if \"eval_loss\" in l]\n\nrows_d = []\nfor l in eval_logs_d:\n rows_d.append({\n \"epoch\": int(l[\"epoch\"]),\n \"eval_loss\": round(l[\"eval_loss\"], 4),\n \"macro_f1\": round(l[\"eval_macro_f1\"], 4),\n \"f1_negative\": round(l.get(\"eval_f1_negative\", 0), 4),\n \"f1_neutral\": round(l.get(\"eval_f1_neutral\", 0), 4),\n \"f1_positive\": round(l.get(\"eval_f1_positive\", 0), 4),\n })\n\ndf_metrics_dedup = pd.DataFrame(rows_d)\ndf_metrics_dedup",
371
+ "metadata": {},
372
+ "execution_count": null,
373
+ "outputs": []
374
+ },
375
+ {
376
+ "cell_type": "markdown",
377
+ "id": "c72ca8f2",
378
+ "source": "### Dedup Test Evaluation",
379
+ "metadata": {}
380
+ },
381
+ {
382
+ "cell_type": "code",
383
+ "id": "c14d2477",
384
+ "source": "test_ds_dedup = EntitySentimentDataset(test_dedup, tokenizer, cfg.max_len)\npred_output_dedup = trainer_dedup.predict(test_ds_dedup)\n\ntest_preds_dedup = np.argmax(pred_output_dedup.predictions, axis=-1).tolist()\ntest_labels_dedup = pred_output_dedup.label_ids.tolist()\nmacro_f1_dedup = f1_score(test_labels_dedup, test_preds_dedup, average=\"macro\")\n\nprint(f\"Test macro-F1 (dedup): {macro_f1_dedup:.4f}\")\nprint()\nprint(classification_report(\n test_labels_dedup, test_preds_dedup,\n target_names=list(SENTIMENT_LABELS.classes),\n))",
385
+ "metadata": {},
386
+ "execution_count": null,
387
+ "outputs": []
388
+ },
389
+ {
390
+ "cell_type": "markdown",
391
+ "id": "9c10beee",
392
+ "source": "### Comparison: All Positions vs Deduplicated",
393
+ "metadata": {}
394
+ },
395
+ {
396
+ "cell_type": "code",
397
+ "id": "381b8bde",
398
+ "source": "comparison = pd.DataFrame({\n \"variant\": [\"all positions (per-position)\", \"all positions (entity-level)\", \"deduplicated\"],\n \"train_examples\": [len(train_exs), len(train_exs), len(train_dedup)],\n \"test_examples\": [len(test_exs), f\"{len(test_exs)} -> {len(entity_best)} entities\", len(test_dedup)],\n \"test_macro_f1\": [round(macro_f1, 4), round(macro_f1_entity, 4), round(macro_f1_dedup, 4)],\n})\ncomparison",
399
+ "metadata": {},
400
+ "execution_count": null,
401
+ "outputs": []
402
+ }
403
+ ],
404
+ "metadata": {
405
+ "kernelspec": {
406
+ "display_name": "Python 3",
407
+ "language": "python",
408
+ "name": "python3"
409
+ },
410
+ "language_info": {
411
+ "name": "python",
412
+ "version": "3.11.0"
413
+ }
414
+ },
415
+ "nbformat": 4,
416
+ "nbformat_minor": 5
417
+ }
notebooks/train_qa_b.ipynb ADDED
@@ -0,0 +1,426 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "id": "c1",
6
+ "metadata": {},
7
+ "source": [
8
+ "# Train: QA-B Mode\n",
9
+ "\n",
10
+ "Fine-tune DistilBERT with sentence-pair QA-B formulation (Sun et al. 2019).\n",
11
+ "\n",
12
+ "- Seg A: entity-centred context window\n",
13
+ "- Seg B: \"The polarity of the {entity_type} {entity} is {sentiment} .\"\n",
14
+ "- Label: binary (no=0, yes=1) per hypothesis\n",
15
+ "- 3-class prediction: argmax over P(yes) across (negative, neutral, positive) triplet\n",
16
+ "\n",
17
+ "**Structure**\n",
18
+ "1. Load Config & Data\n",
19
+ "2. Tokenizer & Model\n",
20
+ "3. Training\n",
21
+ "4. Evaluation (binary + reconstructed 3-class)"
22
+ ]
23
+ },
24
+ {
25
+ "cell_type": "code",
26
+ "id": "c2",
27
+ "metadata": {
28
+ "ExecuteTime": {
29
+ "end_time": "2026-04-19T00:10:30.098089Z",
30
+ "start_time": "2026-04-19T00:10:27.289423Z"
31
+ }
32
+ },
33
+ "source": [
34
+ "import os\n",
35
+ "import sys\n",
36
+ "import random\n",
37
+ "from collections import Counter\n",
38
+ "\n",
39
+ "import numpy as np\n",
40
+ "import torch\n",
41
+ "torch.backends.cuda.enable_cudnn_sdp(False)\n",
42
+ "from sklearn.metrics import classification_report, f1_score\n",
43
+ "from transformers import (\n",
44
+ " AutoModelForSequenceClassification,\n",
45
+ " AutoTokenizer,\n",
46
+ " EarlyStoppingCallback,\n",
47
+ ")\n",
48
+ "\n",
49
+ "sys.path.insert(0, os.path.abspath(\"..\"))\n",
50
+ "\n",
51
+ "from src.schemas.config import TrainingConfig\n",
52
+ "from src.schemas.labels import MODES, SENTIMENT_LABELS\n",
53
+ "from src.models.dataset import (\n",
54
+ " EntitySentimentDataset,\n",
55
+ " deduplicate_positions,\n",
56
+ " flatten_to_examples,\n",
57
+ " load_data,\n",
58
+ " split_data,\n",
59
+ ")\n",
60
+ "from src.models.distillbert import (\n",
61
+ " WeightedLossTrainer,\n",
62
+ " compute_class_weights,\n",
63
+ " evaluate_qa_b_test,\n",
64
+ " make_compute_metrics,\n",
65
+ ")"
66
+ ],
67
+ "outputs": [],
68
+ "execution_count": null
69
+ },
70
+ {
71
+ "cell_type": "markdown",
72
+ "id": "c3",
73
+ "metadata": {},
74
+ "source": [
75
+ "## 1. Load Config & Data"
76
+ ]
77
+ },
78
+ {
79
+ "cell_type": "code",
80
+ "id": "c4",
81
+ "metadata": {
82
+ "ExecuteTime": {
83
+ "end_time": "2026-04-19T00:10:30.106930Z",
84
+ "start_time": "2026-04-19T00:10:30.101538Z"
85
+ }
86
+ },
87
+ "source": [
88
+ "cfg = TrainingConfig.from_json(\"../data/config_qa_b.json\")\n",
89
+ "mode_cfg = MODES[cfg.mode]\n",
90
+ "\n",
91
+ "random.seed(cfg.seed)\n",
92
+ "np.random.seed(cfg.seed)\n",
93
+ "torch.manual_seed(cfg.seed)\n",
94
+ "\n",
95
+ "device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n",
96
+ "print(f\"Device: {device} | Mode: {cfg.mode}\")"
97
+ ],
98
+ "outputs": [
99
+ {
100
+ "name": "stdout",
101
+ "output_type": "stream",
102
+ "text": [
103
+ "Device: cpu | Mode: qa_b\n"
104
+ ]
105
+ }
106
+ ],
107
+ "execution_count": 2
108
+ },
109
+ {
110
+ "cell_type": "code",
111
+ "id": "c5",
112
+ "metadata": {
113
+ "ExecuteTime": {
114
+ "end_time": "2026-04-19T00:10:34.294830Z",
115
+ "start_time": "2026-04-19T00:10:34.114481Z"
116
+ }
117
+ },
118
+ "source": "samples = load_data(os.path.join(\"..\", cfg.data_path))\nexamples = flatten_to_examples(samples, mode=cfg.mode)\n\nn_ents = len({(e[\"sample_id\"], e[\"entity_id\"]) for e in examples})\nprint(f\"{len(samples)} samples -> {n_ents} entities -> {len(examples)} examples\")\n\ndist = Counter(e[\"label\"] for e in examples)\nprint(f\"Binary labels: yes={dist[1]} no={dist[0]}\")",
119
+ "outputs": [
120
+ {
121
+ "name": "stdout",
122
+ "output_type": "stream",
123
+ "text": [
124
+ "1629 samples -> 10550 entities -> 80328 examples\n",
125
+ "Binary labels: yes=26776 no=53552\n"
126
+ ]
127
+ }
128
+ ],
129
+ "execution_count": 3
130
+ },
131
+ {
132
+ "cell_type": "code",
133
+ "id": "c6",
134
+ "metadata": {
135
+ "ExecuteTime": {
136
+ "end_time": "2026-04-19T00:10:34.443682Z",
137
+ "start_time": "2026-04-19T00:10:34.432031Z"
138
+ }
139
+ },
140
+ "source": [
141
+ "train_exs, val_exs, test_exs = split_data(\n",
142
+ " examples, cfg.val_split, cfg.test_split, seed=cfg.seed\n",
143
+ ")\n",
144
+ "print(f\"Train: {len(train_exs)} | Val: {len(val_exs)} | Test: {len(test_exs)}\")"
145
+ ],
146
+ "outputs": [
147
+ {
148
+ "name": "stdout",
149
+ "output_type": "stream",
150
+ "text": [
151
+ "Train: 63939 | Val: 8388 | Test: 8001\n"
152
+ ]
153
+ }
154
+ ],
155
+ "execution_count": 4
156
+ },
157
+ {
158
+ "cell_type": "markdown",
159
+ "id": "fc2c6912",
160
+ "source": "### Deduplicated dataset (one position per entity)",
161
+ "metadata": {}
162
+ },
163
+ {
164
+ "cell_type": "code",
165
+ "id": "08b48902",
166
+ "source": "deduped_samples = deduplicate_positions(samples)\ndedup_examples = flatten_to_examples(deduped_samples, mode=cfg.mode)\n\nn_ents_dedup = len({(e[\"sample_id\"], e[\"entity_id\"]) for e in dedup_examples})\nprint(f\"{len(deduped_samples)} samples -> {n_ents_dedup} entities -> {len(dedup_examples)} examples (deduplicated)\")\n\ndist_dedup = Counter(e[\"label\"] for e in dedup_examples)\nprint(f\"Binary labels: yes={dist_dedup[1]} no={dist_dedup[0]}\")\n\ntrain_dedup, val_dedup, test_dedup = split_data(\n dedup_examples, cfg.val_split, cfg.test_split, seed=cfg.seed\n)\nprint(f\"Train: {len(train_dedup)} | Val: {len(val_dedup)} | Test: {len(test_dedup)}\")",
167
+ "metadata": {},
168
+ "execution_count": null,
169
+ "outputs": []
170
+ },
171
+ {
172
+ "cell_type": "markdown",
173
+ "id": "c7",
174
+ "metadata": {},
175
+ "source": [
176
+ "## 2. Tokenizer & Model"
177
+ ]
178
+ },
179
+ {
180
+ "cell_type": "code",
181
+ "execution_count": null,
182
+ "id": "c8",
183
+ "metadata": {},
184
+ "outputs": [],
185
+ "source": [
186
+ "tokenizer = AutoTokenizer.from_pretrained(cfg.model_name)\n",
187
+ "\n",
188
+ "model = AutoModelForSequenceClassification.from_pretrained(\n",
189
+ " cfg.model_name,\n",
190
+ " num_labels=mode_cfg.labels.num_labels,\n",
191
+ " id2label=mode_cfg.labels.id2label,\n",
192
+ " label2id=mode_cfg.labels.label2id,\n",
193
+ ")\n",
194
+ "print(f\"Num labels: {mode_cfg.labels.num_labels} (binary)\")"
195
+ ]
196
+ },
197
+ {
198
+ "cell_type": "markdown",
199
+ "id": "30b7d3a0",
200
+ "source": "### Truncation Analysis",
201
+ "metadata": {}
202
+ },
203
+ {
204
+ "cell_type": "code",
205
+ "id": "5096fa98",
206
+ "source": "import matplotlib.pyplot as plt\n\nlengths = []\nfor ex in examples:\n enc = tokenizer(ex[\"seg_a\"], ex[\"seg_b\"], truncation=False)\n lengths.append(len(enc[\"input_ids\"]))\n\nlengths = np.array(lengths)\nn_truncated = (lengths > cfg.max_len).sum()\nprint(f\"Max length: {cfg.max_len}\")\nprint(f\"Total examples: {len(lengths)}\")\nprint(f\"Truncated: {n_truncated} ({100 * n_truncated / len(lengths):.1f}%)\")\nprint(f\"Token length — min: {lengths.min()}, median: {int(np.median(lengths))}, \"\n f\"mean: {lengths.mean():.0f}, max: {lengths.max()}\")\n\nfig, ax = plt.subplots(figsize=(10, 3))\nax.hist(lengths, bins=50, edgecolor=\"black\", linewidth=0.5)\nax.axvline(cfg.max_len, color=\"red\", linestyle=\"--\", label=f\"max_len={cfg.max_len}\")\nax.set_xlabel(\"Token count\")\nax.set_ylabel(\"Examples\")\nax.set_title(\"Input Length Distribution (qa_b)\")\nax.legend()\nplt.tight_layout()\nplt.show()",
207
+ "metadata": {},
208
+ "execution_count": null,
209
+ "outputs": []
210
+ },
211
+ {
212
+ "cell_type": "markdown",
213
+ "id": "c9",
214
+ "metadata": {},
215
+ "source": [
216
+ "## 3. Training"
217
+ ]
218
+ },
219
+ {
220
+ "cell_type": "code",
221
+ "execution_count": null,
222
+ "id": "c10",
223
+ "metadata": {},
224
+ "outputs": [],
225
+ "source": [
226
+ "train_ds = EntitySentimentDataset(train_exs, tokenizer, cfg.max_len)\n",
227
+ "val_ds = EntitySentimentDataset(val_exs, tokenizer, cfg.max_len)\n",
228
+ "\n",
229
+ "training_args = cfg.to_training_arguments()\n",
230
+ "training_args.output_dir = os.path.join(\"..\", cfg.output_dir)\n",
231
+ "training_args.logging_dir = os.path.join(\"..\", cfg.output_dir, \"logs\")\n",
232
+ "\n",
233
+ "class_weights = compute_class_weights(train_exs, n_classes=mode_cfg.labels.num_labels)\n",
234
+ "print(f\"Class weights: {class_weights.tolist()}\")"
235
+ ]
236
+ },
237
+ {
238
+ "cell_type": "code",
239
+ "execution_count": null,
240
+ "id": "c11",
241
+ "metadata": {},
242
+ "outputs": [],
243
+ "source": [
244
+ "trainer = WeightedLossTrainer(\n",
245
+ " model=model,\n",
246
+ " args=training_args,\n",
247
+ " train_dataset=train_ds,\n",
248
+ " eval_dataset=val_ds,\n",
249
+ " compute_metrics=make_compute_metrics(cfg.mode),\n",
250
+ " callbacks=[EarlyStoppingCallback(\n",
251
+ " early_stopping_patience=cfg.early_stopping_patience\n",
252
+ " )],\n",
253
+ " class_weights=class_weights,\n",
254
+ ")\n",
255
+ "\n",
256
+ "trainer.train()"
257
+ ]
258
+ },
259
+ {
260
+ "cell_type": "code",
261
+ "execution_count": null,
262
+ "id": "c12",
263
+ "metadata": {},
264
+ "outputs": [],
265
+ "source": [
266
+ "output_dir = os.path.join(\"..\", cfg.output_dir)\n",
267
+ "trainer.save_model(output_dir)\n",
268
+ "tokenizer.save_pretrained(output_dir)\n",
269
+ "\n",
270
+ "with open(os.path.join(output_dir, \"mode.txt\"), \"w\") as f:\n",
271
+ " f.write(cfg.mode)\n",
272
+ "\n",
273
+ "print(f\"Model saved to '{output_dir}'\")"
274
+ ]
275
+ },
276
+ {
277
+ "cell_type": "markdown",
278
+ "id": "fb8fe847",
279
+ "source": "### Validation Metrics per Epoch",
280
+ "metadata": {}
281
+ },
282
+ {
283
+ "cell_type": "code",
284
+ "id": "393bc3ea",
285
+ "source": "import pandas as pd\n\neval_logs = [l for l in trainer.state.log_history if \"eval_loss\" in l]\n\nrows = []\nfor l in eval_logs:\n rows.append({\n \"epoch\": int(l[\"epoch\"]),\n \"eval_loss\": round(l[\"eval_loss\"], 4),\n \"macro_f1_3class\": round(l[\"eval_macro_f1\"], 4),\n \"bin_accuracy\": round(l.get(\"eval_bin_accuracy\", 0), 4),\n \"bin_f1_yes\": round(l.get(\"eval_bin_f1_yes\", 0), 4),\n })\n\ndf_metrics = pd.DataFrame(rows)\ndf_metrics",
286
+ "metadata": {},
287
+ "execution_count": null,
288
+ "outputs": []
289
+ },
290
+ {
291
+ "cell_type": "markdown",
292
+ "id": "c13",
293
+ "metadata": {},
294
+ "source": [
295
+ "## 4. Evaluation\n",
296
+ "\n",
297
+ "QA-B uses binary classification per hypothesis. To get 3-class predictions,\n",
298
+ "we group consecutive triplets (negative, neutral, positive) and take the\n",
299
+ "argmax of P(yes) across the three."
300
+ ]
301
+ },
302
+ {
303
+ "cell_type": "code",
304
+ "execution_count": null,
305
+ "id": "c14",
306
+ "metadata": {},
307
+ "outputs": [],
308
+ "source": [
309
+ "macro_f1, test_preds, test_labels = evaluate_qa_b_test(\n",
310
+ " model=model,\n",
311
+ " tokenizer=tokenizer,\n",
312
+ " test_exs=test_exs,\n",
313
+ " max_len=cfg.max_len,\n",
314
+ " batch_size=cfg.per_device_eval_batch_size,\n",
315
+ " device=device,\n",
316
+ ")\n",
317
+ "\n",
318
+ "print(f\"Test macro-F1 (3-class reconstructed): {macro_f1:.4f}\")\n",
319
+ "print()\n",
320
+ "print(classification_report(\n",
321
+ " test_labels, test_preds,\n",
322
+ " target_names=list(SENTIMENT_LABELS.classes),\n",
323
+ "))"
324
+ ]
325
+ },
326
+ {
327
+ "cell_type": "markdown",
328
+ "id": "f7778e63",
329
+ "source": "### Entity-level Evaluation (highest-confidence aggregation)\n\nWhen an entity has multiple positions, each triplet produces a separate 3-class prediction.\nHere we aggregate by keeping the triplet with the highest P(yes) per entity,\nmatching the production inference behavior.",
330
+ "metadata": {}
331
+ },
332
+ {
333
+ "cell_type": "code",
334
+ "id": "1b3b49a9",
335
+ "source": "import torch.nn.functional as F\nfrom torch.utils.data import DataLoader\nfrom src.models.distillbert import reconstruct_triplets\n\nds_test = EntitySentimentDataset(test_exs, tokenizer, cfg.max_len)\nloader = DataLoader(ds_test, batch_size=cfg.per_device_eval_batch_size, shuffle=False)\n\nall_yes_probs, all_bin_labels = [], []\nmodel.eval()\nwith torch.no_grad():\n for batch in loader:\n logits = model(\n input_ids=batch[\"input_ids\"].to(device),\n attention_mask=batch[\"attention_mask\"].to(device),\n ).logits\n all_yes_probs.extend(F.softmax(logits, dim=-1)[:, 1].cpu().tolist())\n all_bin_labels.extend(batch[\"labels\"].tolist())\n\nsentiments = list(SENTIMENT_LABELS.classes)\nentity_best: dict[tuple, tuple[int, int, float]] = {}\n\nfor i in range(0, len(test_exs) - 2, 3):\n triplet_probs = all_yes_probs[i:i + 3]\n triplet_labels = all_bin_labels[i:i + 3]\n pred_3class = int(np.argmax(triplet_probs))\n label_3class = int(np.argmax(triplet_labels))\n conf = max(triplet_probs)\n\n ex = test_exs[i]\n key = (ex[\"sample_id\"], ex[\"entity_id\"])\n if key not in entity_best or conf > entity_best[key][2]:\n entity_best[key] = (pred_3class, label_3class, conf)\n\nent_preds = [v[0] for v in entity_best.values()]\nent_labels = [v[1] for v in entity_best.values()]\n\nmacro_f1_entity = f1_score(ent_labels, ent_preds, average=\"macro\")\nprint(f\"Entity-level test macro-F1: {macro_f1_entity:.4f}\")\nprint(f\"({len(entity_best)} unique entities from {len(test_exs) // 3} position-level triplets)\")\nprint()\nprint(classification_report(\n ent_labels, ent_preds,\n target_names=sentiments,\n))",
336
+ "metadata": {},
337
+ "execution_count": null,
338
+ "outputs": []
339
+ },
340
+ {
341
+ "cell_type": "markdown",
342
+ "id": "e7ba646b",
343
+ "source": "## 5. Training & Evaluation — Deduplicated",
344
+ "metadata": {}
345
+ },
346
+ {
347
+ "cell_type": "code",
348
+ "id": "225f6ffb",
349
+ "source": "model_dedup = AutoModelForSequenceClassification.from_pretrained(\n cfg.model_name,\n num_labels=mode_cfg.labels.num_labels,\n id2label=mode_cfg.labels.id2label,\n label2id=mode_cfg.labels.label2id,\n)\n\ntrain_ds_dedup = EntitySentimentDataset(train_dedup, tokenizer, cfg.max_len)\nval_ds_dedup = EntitySentimentDataset(val_dedup, tokenizer, cfg.max_len)\n\ntraining_args_dedup = cfg.to_training_arguments()\ntraining_args_dedup.output_dir = os.path.join(\"..\", cfg.output_dir + \"_dedup\")\ntraining_args_dedup.logging_dir = os.path.join(\"..\", cfg.output_dir + \"_dedup\", \"logs\")\n\nclass_weights_dedup = compute_class_weights(train_dedup, n_classes=mode_cfg.labels.num_labels)\nprint(f\"Class weights (dedup): {class_weights_dedup.tolist()}\")",
350
+ "metadata": {},
351
+ "execution_count": null,
352
+ "outputs": []
353
+ },
354
+ {
355
+ "cell_type": "code",
356
+ "id": "3ff92fc7",
357
+ "source": "trainer_dedup = WeightedLossTrainer(\n model=model_dedup,\n args=training_args_dedup,\n train_dataset=train_ds_dedup,\n eval_dataset=val_ds_dedup,\n compute_metrics=make_compute_metrics(cfg.mode),\n callbacks=[EarlyStoppingCallback(\n early_stopping_patience=cfg.early_stopping_patience\n )],\n class_weights=class_weights_dedup,\n)\n\ntrainer_dedup.train()",
358
+ "metadata": {},
359
+ "execution_count": null,
360
+ "outputs": []
361
+ },
362
+ {
363
+ "cell_type": "code",
364
+ "id": "7c4c73e3",
365
+ "source": "output_dir_dedup = os.path.join(\"..\", cfg.output_dir + \"_dedup\")\ntrainer_dedup.save_model(output_dir_dedup)\ntokenizer.save_pretrained(output_dir_dedup)\n\nwith open(os.path.join(output_dir_dedup, \"mode.txt\"), \"w\") as f:\n f.write(cfg.mode)\n\nprint(f\"Dedup model saved to '{output_dir_dedup}'\")",
366
+ "metadata": {},
367
+ "execution_count": null,
368
+ "outputs": []
369
+ },
370
+ {
371
+ "cell_type": "markdown",
372
+ "id": "075b4cd2",
373
+ "source": "### Dedup Validation Metrics per Epoch",
374
+ "metadata": {}
375
+ },
376
+ {
377
+ "cell_type": "code",
378
+ "id": "b92449e1",
379
+ "source": "eval_logs_d = [l for l in trainer_dedup.state.log_history if \"eval_loss\" in l]\n\nrows_d = []\nfor l in eval_logs_d:\n rows_d.append({\n \"epoch\": int(l[\"epoch\"]),\n \"eval_loss\": round(l[\"eval_loss\"], 4),\n \"macro_f1_3class\": round(l[\"eval_macro_f1\"], 4),\n \"bin_accuracy\": round(l.get(\"eval_bin_accuracy\", 0), 4),\n \"bin_f1_yes\": round(l.get(\"eval_bin_f1_yes\", 0), 4),\n })\n\ndf_metrics_dedup = pd.DataFrame(rows_d)\ndf_metrics_dedup",
380
+ "metadata": {},
381
+ "execution_count": null,
382
+ "outputs": []
383
+ },
384
+ {
385
+ "cell_type": "markdown",
386
+ "id": "36eee903",
387
+ "source": "### Dedup Test Evaluation",
388
+ "metadata": {}
389
+ },
390
+ {
391
+ "cell_type": "code",
392
+ "id": "4b86af50",
393
+ "source": "macro_f1_dedup, test_preds_dedup, test_labels_dedup = evaluate_qa_b_test(\n model=model_dedup,\n tokenizer=tokenizer,\n test_exs=test_dedup,\n max_len=cfg.max_len,\n batch_size=cfg.per_device_eval_batch_size,\n device=device,\n)\n\nprint(f\"Test macro-F1 (3-class reconstructed, dedup): {macro_f1_dedup:.4f}\")\nprint()\nprint(classification_report(\n test_labels_dedup, test_preds_dedup,\n target_names=list(SENTIMENT_LABELS.classes),\n))",
394
+ "metadata": {},
395
+ "execution_count": null,
396
+ "outputs": []
397
+ },
398
+ {
399
+ "cell_type": "markdown",
400
+ "id": "f4b3d712",
401
+ "source": "### Comparison: All Positions vs Deduplicated",
402
+ "metadata": {}
403
+ },
404
+ {
405
+ "cell_type": "code",
406
+ "id": "5dd74e22",
407
+ "source": "comparison = pd.DataFrame({\n \"variant\": [\"all positions (per-position)\", \"all positions (entity-level)\", \"deduplicated\"],\n \"train_examples\": [len(train_exs), len(train_exs), len(train_dedup)],\n \"test_examples\": [len(test_exs), f\"{len(test_exs) // 3} -> {len(entity_best)} entities\", len(test_dedup)],\n \"test_macro_f1\": [round(macro_f1, 4), round(macro_f1_entity, 4), round(macro_f1_dedup, 4)],\n})\ncomparison",
408
+ "metadata": {},
409
+ "execution_count": null,
410
+ "outputs": []
411
+ }
412
+ ],
413
+ "metadata": {
414
+ "kernelspec": {
415
+ "display_name": "Python 3",
416
+ "language": "python",
417
+ "name": "python3"
418
+ },
419
+ "language_info": {
420
+ "name": "python",
421
+ "version": "3.11.0"
422
+ }
423
+ },
424
+ "nbformat": 4,
425
+ "nbformat_minor": 5
426
+ }
notebooks/train_qa_m.ipynb ADDED
@@ -0,0 +1,373 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "id": "b1",
6
+ "metadata": {},
7
+ "source": [
8
+ "# Train: QA-M Mode\n",
9
+ "\n",
10
+ "Fine-tune DistilBERT with sentence-pair QA-M formulation (Sun et al. 2019).\n",
11
+ "\n",
12
+ "- Seg A: entity-centred context window\n",
13
+ "- Seg B: \"What do you think of the sentiment of the {entity_type} {entity} ?\"\n",
14
+ "- Label: 3-way (negative, neutral, positive)\n",
15
+ "\n",
16
+ "**Structure**\n",
17
+ "1. Load Config & Data\n",
18
+ "2. Tokenizer & Model\n",
19
+ "3. Training\n",
20
+ "4. Evaluation"
21
+ ]
22
+ },
23
+ {
24
+ "cell_type": "code",
25
+ "execution_count": null,
26
+ "id": "b2",
27
+ "metadata": {},
28
+ "outputs": [],
29
+ "source": [
30
+ "import os\n",
31
+ "import sys\n",
32
+ "import random\n",
33
+ "from collections import Counter\n",
34
+ "\n",
35
+ "import numpy as np\n",
36
+ "import torch\n",
37
+ "torch.backends.cuda.enable_cudnn_sdp(False)\n",
38
+ "from sklearn.metrics import classification_report, f1_score\n",
39
+ "from transformers import (\n",
40
+ " AutoModelForSequenceClassification,\n",
41
+ " AutoTokenizer,\n",
42
+ " EarlyStoppingCallback,\n",
43
+ ")\n",
44
+ "\n",
45
+ "sys.path.insert(0, os.path.abspath(\"..\"))\n",
46
+ "\n",
47
+ "from src.schemas.config import TrainingConfig\n",
48
+ "from src.schemas.labels import MODES, SENTIMENT_LABELS\n",
49
+ "from src.models.dataset import (\n",
50
+ " EntitySentimentDataset,\n",
51
+ " deduplicate_positions,\n",
52
+ " flatten_to_examples,\n",
53
+ " load_data,\n",
54
+ " split_data,\n",
55
+ ")\n",
56
+ "from src.models.distillbert import (\n",
57
+ " WeightedLossTrainer,\n",
58
+ " compute_class_weights,\n",
59
+ " make_compute_metrics,\n",
60
+ ")"
61
+ ]
62
+ },
63
+ {
64
+ "cell_type": "markdown",
65
+ "id": "b3",
66
+ "metadata": {},
67
+ "source": [
68
+ "## 1. Load Config & Data"
69
+ ]
70
+ },
71
+ {
72
+ "cell_type": "code",
73
+ "execution_count": null,
74
+ "id": "b4",
75
+ "metadata": {},
76
+ "outputs": [],
77
+ "source": [
78
+ "cfg = TrainingConfig.from_json(\"../data/config_qa_m.json\")\n",
79
+ "mode_cfg = MODES[cfg.mode]\n",
80
+ "\n",
81
+ "random.seed(cfg.seed)\n",
82
+ "np.random.seed(cfg.seed)\n",
83
+ "torch.manual_seed(cfg.seed)\n",
84
+ "\n",
85
+ "device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n",
86
+ "print(f\"Device: {device} | Mode: {cfg.mode}\")"
87
+ ]
88
+ },
89
+ {
90
+ "cell_type": "code",
91
+ "execution_count": null,
92
+ "id": "b5",
93
+ "metadata": {},
94
+ "outputs": [],
95
+ "source": "samples = load_data(os.path.join(\"..\", cfg.data_path))\nexamples = flatten_to_examples(samples, mode=cfg.mode)\n\nn_ents = len({(e[\"sample_id\"], e[\"entity_id\"]) for e in examples})\nprint(f\"{len(samples)} samples -> {n_ents} entities -> {len(examples)} examples\")\n\ndist = Counter(SENTIMENT_LABELS.id2label[e[\"label\"]] for e in examples)\nprint(f\"Label distribution: {dict(dist)}\")"
96
+ },
97
+ {
98
+ "cell_type": "code",
99
+ "execution_count": null,
100
+ "id": "b6",
101
+ "metadata": {},
102
+ "outputs": [],
103
+ "source": [
104
+ "train_exs, val_exs, test_exs = split_data(\n",
105
+ " examples, cfg.val_split, cfg.test_split, seed=cfg.seed\n",
106
+ ")\n",
107
+ "print(f\"Train: {len(train_exs)} | Val: {len(val_exs)} | Test: {len(test_exs)}\")"
108
+ ]
109
+ },
110
+ {
111
+ "cell_type": "markdown",
112
+ "id": "4f074039",
113
+ "source": "### Deduplicated dataset (one position per entity)",
114
+ "metadata": {}
115
+ },
116
+ {
117
+ "cell_type": "code",
118
+ "id": "417c40e2",
119
+ "source": "deduped_samples = deduplicate_positions(samples)\ndedup_examples = flatten_to_examples(deduped_samples, mode=cfg.mode)\n\nn_ents_dedup = len({(e[\"sample_id\"], e[\"entity_id\"]) for e in dedup_examples})\nprint(f\"{len(deduped_samples)} samples -> {n_ents_dedup} entities -> {len(dedup_examples)} examples (deduplicated)\")\n\ndist_dedup = Counter(SENTIMENT_LABELS.id2label[e[\"label\"]] for e in dedup_examples)\nprint(f\"Label distribution: {dict(dist_dedup)}\")\n\ntrain_dedup, val_dedup, test_dedup = split_data(\n dedup_examples, cfg.val_split, cfg.test_split, seed=cfg.seed\n)\nprint(f\"Train: {len(train_dedup)} | Val: {len(val_dedup)} | Test: {len(test_dedup)}\")",
120
+ "metadata": {},
121
+ "execution_count": null,
122
+ "outputs": []
123
+ },
124
+ {
125
+ "cell_type": "markdown",
126
+ "id": "b7",
127
+ "metadata": {},
128
+ "source": [
129
+ "## 2. Tokenizer & Model"
130
+ ]
131
+ },
132
+ {
133
+ "cell_type": "code",
134
+ "execution_count": null,
135
+ "id": "b8",
136
+ "metadata": {},
137
+ "outputs": [],
138
+ "source": [
139
+ "tokenizer = AutoTokenizer.from_pretrained(cfg.model_name)\n",
140
+ "\n",
141
+ "model = AutoModelForSequenceClassification.from_pretrained(\n",
142
+ " cfg.model_name,\n",
143
+ " num_labels=mode_cfg.labels.num_labels,\n",
144
+ " id2label=mode_cfg.labels.id2label,\n",
145
+ " label2id=mode_cfg.labels.label2id,\n",
146
+ ")\n",
147
+ "print(f\"Num labels: {mode_cfg.labels.num_labels}\")"
148
+ ]
149
+ },
150
+ {
151
+ "cell_type": "markdown",
152
+ "id": "08b28688",
153
+ "source": "### Truncation Analysis",
154
+ "metadata": {}
155
+ },
156
+ {
157
+ "cell_type": "code",
158
+ "id": "a69f3e30",
159
+ "source": "import matplotlib.pyplot as plt\n\nlengths = []\nfor ex in examples:\n enc = tokenizer(ex[\"seg_a\"], ex[\"seg_b\"], truncation=False)\n lengths.append(len(enc[\"input_ids\"]))\n\nlengths = np.array(lengths)\nn_truncated = (lengths > cfg.max_len).sum()\nprint(f\"Max length: {cfg.max_len}\")\nprint(f\"Total examples: {len(lengths)}\")\nprint(f\"Truncated: {n_truncated} ({100 * n_truncated / len(lengths):.1f}%)\")\nprint(f\"Token length — min: {lengths.min()}, median: {int(np.median(lengths))}, \"\n f\"mean: {lengths.mean():.0f}, max: {lengths.max()}\")\n\nfig, ax = plt.subplots(figsize=(10, 3))\nax.hist(lengths, bins=50, edgecolor=\"black\", linewidth=0.5)\nax.axvline(cfg.max_len, color=\"red\", linestyle=\"--\", label=f\"max_len={cfg.max_len}\")\nax.set_xlabel(\"Token count\")\nax.set_ylabel(\"Examples\")\nax.set_title(\"Input Length Distribution (qa_m)\")\nax.legend()\nplt.tight_layout()\nplt.show()",
160
+ "metadata": {},
161
+ "execution_count": null,
162
+ "outputs": []
163
+ },
164
+ {
165
+ "cell_type": "markdown",
166
+ "id": "b9",
167
+ "metadata": {},
168
+ "source": [
169
+ "## 3. Training"
170
+ ]
171
+ },
172
+ {
173
+ "cell_type": "code",
174
+ "execution_count": null,
175
+ "id": "b10",
176
+ "metadata": {},
177
+ "outputs": [],
178
+ "source": [
179
+ "train_ds = EntitySentimentDataset(train_exs, tokenizer, cfg.max_len)\n",
180
+ "val_ds = EntitySentimentDataset(val_exs, tokenizer, cfg.max_len)\n",
181
+ "\n",
182
+ "training_args = cfg.to_training_arguments()\n",
183
+ "training_args.output_dir = os.path.join(\"..\", cfg.output_dir)\n",
184
+ "training_args.logging_dir = os.path.join(\"..\", cfg.output_dir, \"logs\")\n",
185
+ "\n",
186
+ "class_weights = compute_class_weights(train_exs, n_classes=mode_cfg.labels.num_labels)\n",
187
+ "print(f\"Class weights: {class_weights.tolist()}\")"
188
+ ]
189
+ },
190
+ {
191
+ "cell_type": "code",
192
+ "execution_count": null,
193
+ "id": "b11",
194
+ "metadata": {},
195
+ "outputs": [],
196
+ "source": [
197
+ "trainer = WeightedLossTrainer(\n",
198
+ " model=model,\n",
199
+ " args=training_args,\n",
200
+ " train_dataset=train_ds,\n",
201
+ " eval_dataset=val_ds,\n",
202
+ " compute_metrics=make_compute_metrics(cfg.mode),\n",
203
+ " callbacks=[EarlyStoppingCallback(\n",
204
+ " early_stopping_patience=cfg.early_stopping_patience\n",
205
+ " )],\n",
206
+ " class_weights=class_weights,\n",
207
+ ")\n",
208
+ "\n",
209
+ "trainer.train()"
210
+ ]
211
+ },
212
+ {
213
+ "cell_type": "code",
214
+ "execution_count": null,
215
+ "id": "b12",
216
+ "metadata": {},
217
+ "outputs": [],
218
+ "source": [
219
+ "output_dir = os.path.join(\"..\", cfg.output_dir)\n",
220
+ "trainer.save_model(output_dir)\n",
221
+ "tokenizer.save_pretrained(output_dir)\n",
222
+ "\n",
223
+ "with open(os.path.join(output_dir, \"mode.txt\"), \"w\") as f:\n",
224
+ " f.write(cfg.mode)\n",
225
+ "\n",
226
+ "print(f\"Model saved to '{output_dir}'\")"
227
+ ]
228
+ },
229
+ {
230
+ "cell_type": "markdown",
231
+ "id": "c2bd2384",
232
+ "source": "### Validation Metrics per Epoch",
233
+ "metadata": {}
234
+ },
235
+ {
236
+ "cell_type": "code",
237
+ "id": "dc800309",
238
+ "source": "import pandas as pd\n\neval_logs = [l for l in trainer.state.log_history if \"eval_loss\" in l]\n\nrows = []\nfor l in eval_logs:\n rows.append({\n \"epoch\": int(l[\"epoch\"]),\n \"eval_loss\": round(l[\"eval_loss\"], 4),\n \"macro_f1\": round(l[\"eval_macro_f1\"], 4),\n \"f1_negative\": round(l.get(\"eval_f1_negative\", 0), 4),\n \"f1_neutral\": round(l.get(\"eval_f1_neutral\", 0), 4),\n \"f1_positive\": round(l.get(\"eval_f1_positive\", 0), 4),\n })\n\ndf_metrics = pd.DataFrame(rows)\ndf_metrics",
239
+ "metadata": {},
240
+ "execution_count": null,
241
+ "outputs": []
242
+ },
243
+ {
244
+ "cell_type": "markdown",
245
+ "id": "b13",
246
+ "metadata": {},
247
+ "source": [
248
+ "## 4. Evaluation"
249
+ ]
250
+ },
251
+ {
252
+ "cell_type": "code",
253
+ "execution_count": null,
254
+ "id": "b14",
255
+ "metadata": {},
256
+ "outputs": [],
257
+ "source": [
258
+ "test_ds = EntitySentimentDataset(test_exs, tokenizer, cfg.max_len)\n",
259
+ "pred_output = trainer.predict(test_ds)\n",
260
+ "\n",
261
+ "test_preds = np.argmax(pred_output.predictions, axis=-1).tolist()\n",
262
+ "test_labels = pred_output.label_ids.tolist()\n",
263
+ "macro_f1 = f1_score(test_labels, test_preds, average=\"macro\")\n",
264
+ "\n",
265
+ "print(f\"Test macro-F1: {macro_f1:.4f}\")\n",
266
+ "print()\n",
267
+ "print(classification_report(\n",
268
+ " test_labels, test_preds,\n",
269
+ " target_names=list(SENTIMENT_LABELS.classes),\n",
270
+ "))"
271
+ ]
272
+ },
273
+ {
274
+ "cell_type": "markdown",
275
+ "id": "dd99b7e0",
276
+ "source": "### Entity-level Evaluation (highest-confidence aggregation)\n\nWhen an entity has multiple positions, each produces a separate prediction.\nHere we aggregate by keeping the prediction with the highest confidence per entity,\nmatching the production inference behavior.",
277
+ "metadata": {}
278
+ },
279
+ {
280
+ "cell_type": "code",
281
+ "id": "aa42bf08",
282
+ "source": "import torch.nn.functional as F\n\nprobs = F.softmax(torch.tensor(pred_output.predictions), dim=-1).numpy()\nconfidences = probs.max(axis=-1)\npreds_all = np.argmax(probs, axis=-1)\n\nsentiments = list(SENTIMENT_LABELS.classes)\nentity_best: dict[tuple, tuple[int, int, float]] = {}\nfor ex, pred_id, conf, label in zip(test_exs, preds_all, confidences, test_labels):\n key = (ex[\"sample_id\"], ex[\"entity_id\"])\n if key not in entity_best or conf > entity_best[key][2]:\n entity_best[key] = (int(pred_id), int(label), float(conf))\n\nent_preds = [v[0] for v in entity_best.values()]\nent_labels = [v[1] for v in entity_best.values()]\n\nmacro_f1_entity = f1_score(ent_labels, ent_preds, average=\"macro\")\nprint(f\"Entity-level test macro-F1: {macro_f1_entity:.4f}\")\nprint(f\"({len(entity_best)} unique entities from {len(test_exs)} position-level examples)\")\nprint()\nprint(classification_report(\n ent_labels, ent_preds,\n target_names=sentiments,\n))",
283
+ "metadata": {},
284
+ "execution_count": null,
285
+ "outputs": []
286
+ },
287
+ {
288
+ "cell_type": "markdown",
289
+ "id": "925dff05",
290
+ "source": "## 5. Training & Evaluation — Deduplicated",
291
+ "metadata": {}
292
+ },
293
+ {
294
+ "cell_type": "code",
295
+ "id": "88e587d7",
296
+ "source": "model_dedup = AutoModelForSequenceClassification.from_pretrained(\n cfg.model_name,\n num_labels=mode_cfg.labels.num_labels,\n id2label=mode_cfg.labels.id2label,\n label2id=mode_cfg.labels.label2id,\n)\n\ntrain_ds_dedup = EntitySentimentDataset(train_dedup, tokenizer, cfg.max_len)\nval_ds_dedup = EntitySentimentDataset(val_dedup, tokenizer, cfg.max_len)\n\ntraining_args_dedup = cfg.to_training_arguments()\ntraining_args_dedup.output_dir = os.path.join(\"..\", cfg.output_dir + \"_dedup\")\ntraining_args_dedup.logging_dir = os.path.join(\"..\", cfg.output_dir + \"_dedup\", \"logs\")\n\nclass_weights_dedup = compute_class_weights(train_dedup, n_classes=mode_cfg.labels.num_labels)\nprint(f\"Class weights (dedup): {class_weights_dedup.tolist()}\")",
297
+ "metadata": {},
298
+ "execution_count": null,
299
+ "outputs": []
300
+ },
301
+ {
302
+ "cell_type": "code",
303
+ "id": "a4f5bc5e",
304
+ "source": "trainer_dedup = WeightedLossTrainer(\n model=model_dedup,\n args=training_args_dedup,\n train_dataset=train_ds_dedup,\n eval_dataset=val_ds_dedup,\n compute_metrics=make_compute_metrics(cfg.mode),\n callbacks=[EarlyStoppingCallback(\n early_stopping_patience=cfg.early_stopping_patience\n )],\n class_weights=class_weights_dedup,\n)\n\ntrainer_dedup.train()",
305
+ "metadata": {},
306
+ "execution_count": null,
307
+ "outputs": []
308
+ },
309
+ {
310
+ "cell_type": "code",
311
+ "id": "20ebd649",
312
+ "source": "output_dir_dedup = os.path.join(\"..\", cfg.output_dir + \"_dedup\")\ntrainer_dedup.save_model(output_dir_dedup)\ntokenizer.save_pretrained(output_dir_dedup)\n\nwith open(os.path.join(output_dir_dedup, \"mode.txt\"), \"w\") as f:\n f.write(cfg.mode)\n\nprint(f\"Dedup model saved to '{output_dir_dedup}'\")",
313
+ "metadata": {},
314
+ "execution_count": null,
315
+ "outputs": []
316
+ },
317
+ {
318
+ "cell_type": "markdown",
319
+ "id": "4a0e255e",
320
+ "source": "### Dedup Validation Metrics per Epoch",
321
+ "metadata": {}
322
+ },
323
+ {
324
+ "cell_type": "code",
325
+ "id": "69f51f21",
326
+ "source": "eval_logs_d = [l for l in trainer_dedup.state.log_history if \"eval_loss\" in l]\n\nrows_d = []\nfor l in eval_logs_d:\n rows_d.append({\n \"epoch\": int(l[\"epoch\"]),\n \"eval_loss\": round(l[\"eval_loss\"], 4),\n \"macro_f1\": round(l[\"eval_macro_f1\"], 4),\n \"f1_negative\": round(l.get(\"eval_f1_negative\", 0), 4),\n \"f1_neutral\": round(l.get(\"eval_f1_neutral\", 0), 4),\n \"f1_positive\": round(l.get(\"eval_f1_positive\", 0), 4),\n })\n\ndf_metrics_dedup = pd.DataFrame(rows_d)\ndf_metrics_dedup",
327
+ "metadata": {},
328
+ "execution_count": null,
329
+ "outputs": []
330
+ },
331
+ {
332
+ "cell_type": "markdown",
333
+ "id": "54409311",
334
+ "source": "### Dedup Test Evaluation",
335
+ "metadata": {}
336
+ },
337
+ {
338
+ "cell_type": "code",
339
+ "id": "b9f18bc4",
340
+ "source": "test_ds_dedup = EntitySentimentDataset(test_dedup, tokenizer, cfg.max_len)\npred_output_dedup = trainer_dedup.predict(test_ds_dedup)\n\ntest_preds_dedup = np.argmax(pred_output_dedup.predictions, axis=-1).tolist()\ntest_labels_dedup = pred_output_dedup.label_ids.tolist()\nmacro_f1_dedup = f1_score(test_labels_dedup, test_preds_dedup, average=\"macro\")\n\nprint(f\"Test macro-F1 (dedup): {macro_f1_dedup:.4f}\")\nprint()\nprint(classification_report(\n test_labels_dedup, test_preds_dedup,\n target_names=list(SENTIMENT_LABELS.classes),\n))",
341
+ "metadata": {},
342
+ "execution_count": null,
343
+ "outputs": []
344
+ },
345
+ {
346
+ "cell_type": "markdown",
347
+ "id": "900bdb55",
348
+ "source": "### Comparison: All Positions vs Deduplicated",
349
+ "metadata": {}
350
+ },
351
+ {
352
+ "cell_type": "code",
353
+ "id": "111ce91e",
354
+ "source": "comparison = pd.DataFrame({\n \"variant\": [\"all positions (per-position)\", \"all positions (entity-level)\", \"deduplicated\"],\n \"train_examples\": [len(train_exs), len(train_exs), len(train_dedup)],\n \"test_examples\": [len(test_exs), f\"{len(test_exs)} -> {len(entity_best)} entities\", len(test_dedup)],\n \"test_macro_f1\": [round(macro_f1, 4), round(macro_f1_entity, 4), round(macro_f1_dedup, 4)],\n})\ncomparison",
355
+ "metadata": {},
356
+ "execution_count": null,
357
+ "outputs": []
358
+ }
359
+ ],
360
+ "metadata": {
361
+ "kernelspec": {
362
+ "display_name": "Python 3",
363
+ "language": "python",
364
+ "name": "python3"
365
+ },
366
+ "language_info": {
367
+ "name": "python",
368
+ "version": "3.11.0"
369
+ }
370
+ },
371
+ "nbformat": 4,
372
+ "nbformat_minor": 5
373
+ }