Mukul Rayana commited on
Commit
ae3aa18
Β·
1 Parent(s): 5c84477

Add Colab notebooks: emotion classifier + corpus annotation (Day 12)

Browse files
notebooks/colab_annotate_corpus.ipynb ADDED
@@ -0,0 +1,307 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "id": "e11600f1",
6
+ "metadata": {},
7
+ "source": [
8
+ "# EmpathRAG β€” Corpus Annotation (Day 10)\n",
9
+ "Annotate all 1,674,369 FAISS chunks with emotion labels using the trained RoBERTa checkpoint. \n",
10
+ "**Runtime: A100 GPU. Expected time: ~25 minutes.**\n",
11
+ "\n",
12
+ "This notebook:\n",
13
+ "1. Copies `metadata.db` from your local machine (uploaded here) to Colab\n",
14
+ "2. Loads the RoBERTa+LoRA checkpoint from Drive\n",
15
+ "3. Runs inference in batches of 512 on GPU\n",
16
+ "4. Writes emotion labels back to the SQLite db\n",
17
+ "5. Saves the annotated db back to Drive for you to download\n"
18
+ ]
19
+ },
20
+ {
21
+ "cell_type": "code",
22
+ "execution_count": null,
23
+ "id": "e85f0c1f",
24
+ "metadata": {},
25
+ "outputs": [],
26
+ "source": [
27
+ "import torch\n",
28
+ "gpu = torch.cuda.get_device_name(0) if torch.cuda.is_available() else \"NO GPU\"\n",
29
+ "print(f\"GPU : {gpu}\")\n",
30
+ "print(f\"VRAM : {torch.cuda.get_device_properties(0).total_memory/1e9:.1f} GB\")\n",
31
+ "assert torch.cuda.is_available(), \"Switch to A100 runtime.\"\n",
32
+ "print(\"βœ… GPU ready.\")\n"
33
+ ]
34
+ },
35
+ {
36
+ "cell_type": "code",
37
+ "execution_count": null,
38
+ "id": "90dcada6",
39
+ "metadata": {},
40
+ "outputs": [],
41
+ "source": [
42
+ "!pip install -q peft>=0.18.0 accelerate>=1.0.0\n",
43
+ "import peft, accelerate\n",
44
+ "print(f\"peft: {peft.__version__} | accelerate: {accelerate.__version__}\")\n",
45
+ "print(\"βœ… Packages ready.\")\n"
46
+ ]
47
+ },
48
+ {
49
+ "cell_type": "code",
50
+ "execution_count": null,
51
+ "id": "51d42310",
52
+ "metadata": {},
53
+ "outputs": [],
54
+ "source": [
55
+ "from google.colab import drive\n",
56
+ "import os, shutil\n",
57
+ "\n",
58
+ "drive.mount(\"/content/drive\")\n",
59
+ "\n",
60
+ "CHECKPOINT_DIR = \"/content/drive/MyDrive/empathrag/emotion_classifier\"\n",
61
+ "DB_DRIVE_PATH = \"/content/drive/MyDrive/empathrag/metadata.db\"\n",
62
+ "DB_LOCAL_PATH = \"/content/metadata.db\"\n",
63
+ "\n",
64
+ "# Verify checkpoint exists\n",
65
+ "required = [\"adapter_config.json\", \"adapter_model.safetensors\", \"tokenizer.json\"]\n",
66
+ "for f in required:\n",
67
+ " path = os.path.join(CHECKPOINT_DIR, f)\n",
68
+ " assert os.path.exists(path), f\"Missing: {path}\"\n",
69
+ "print(f\"βœ… Checkpoint found at: {CHECKPOINT_DIR}\")\n"
70
+ ]
71
+ },
72
+ {
73
+ "cell_type": "markdown",
74
+ "id": "f6533428",
75
+ "metadata": {},
76
+ "source": [
77
+ "## ⬆️ Upload metadata.db\n",
78
+ "\n",
79
+ "Run the cell below. It will prompt you to upload a file. \n",
80
+ "Upload `data/indexes/metadata.db` from your local machine.\n",
81
+ "\n",
82
+ "The file is ~1.26 GB β€” upload will take 2-5 minutes depending on your connection.\n"
83
+ ]
84
+ },
85
+ {
86
+ "cell_type": "code",
87
+ "execution_count": null,
88
+ "id": "8f3928e3",
89
+ "metadata": {},
90
+ "outputs": [],
91
+ "source": [
92
+ "from google.colab import files\n",
93
+ "import shutil, os\n",
94
+ "\n",
95
+ "print(\"Click 'Choose Files' and select data/indexes/metadata.db from your local machine.\")\n",
96
+ "print(\"File size: ~1.26 GB. Wait for upload to complete before proceeding.\")\n",
97
+ "\n",
98
+ "uploaded = files.upload()\n",
99
+ "\n",
100
+ "# Move to working path\n",
101
+ "for fname in uploaded:\n",
102
+ " dest = \"/content/metadata.db\"\n",
103
+ " if fname != \"metadata.db\":\n",
104
+ " os.rename(fname, dest)\n",
105
+ " else:\n",
106
+ " pass # already at /content/metadata.db if uploaded directly\n",
107
+ "\n",
108
+ "# Verify\n",
109
+ "import sqlite3\n",
110
+ "conn = sqlite3.connect(DB_LOCAL_PATH)\n",
111
+ "total = conn.execute(\"SELECT COUNT(*) FROM chunks\").fetchone()[0]\n",
112
+ "annotated = conn.execute(\"SELECT COUNT(*) FROM chunks WHERE emotion_label != -1\").fetchone()[0]\n",
113
+ "unannotated= conn.execute(\"SELECT COUNT(*) FROM chunks WHERE emotion_label = -1\").fetchone()[0]\n",
114
+ "conn.close()\n",
115
+ "\n",
116
+ "print(f\"\\nDB loaded:\")\n",
117
+ "print(f\" Total rows : {total:,}\")\n",
118
+ "print(f\" Annotated : {annotated:,} (already done β€” will skip)\")\n",
119
+ "print(f\" Unannotated : {unannotated:,} (will process these)\")\n"
120
+ ]
121
+ },
122
+ {
123
+ "cell_type": "code",
124
+ "execution_count": null,
125
+ "id": "444f0fea",
126
+ "metadata": {},
127
+ "outputs": [],
128
+ "source": [
129
+ "import torch\n",
130
+ "from transformers import AutoTokenizer, AutoModelForSequenceClassification\n",
131
+ "from peft import PeftModel\n",
132
+ "\n",
133
+ "LABEL_NAMES = [\"distress\", \"anxiety\", \"frustration\", \"neutral\", \"hopeful\"]\n",
134
+ "SAFETY_SCORE_MAP = {0: 0.0, 1: 0.0, 2: 0.3, 3: 0.7, 4: 1.0}\n",
135
+ "DEVICE = torch.device(\"cuda\")\n",
136
+ "\n",
137
+ "print(\"Loading RoBERTa + LoRA checkpoint onto GPU...\")\n",
138
+ "tokenizer = AutoTokenizer.from_pretrained(CHECKPOINT_DIR)\n",
139
+ "base = AutoModelForSequenceClassification.from_pretrained(\"roberta-base\", num_labels=5)\n",
140
+ "model = PeftModel.from_pretrained(base, CHECKPOINT_DIR).to(DEVICE).eval()\n",
141
+ "\n",
142
+ "# Quick sanity check\n",
143
+ "enc = tokenizer(\"I feel completely overwhelmed and hopeless\",\n",
144
+ " return_tensors=\"pt\", truncation=True, max_length=128).to(DEVICE)\n",
145
+ "with torch.no_grad():\n",
146
+ " pred = model(**enc).logits.argmax(-1).item()\n",
147
+ "print(f\"Sanity check: 'overwhelmed and hopeless' β†’ {LABEL_NAMES[pred]}\")\n",
148
+ "assert LABEL_NAMES[pred] == \"distress\", f\"Expected distress, got {LABEL_NAMES[pred]}\"\n",
149
+ "print(\"βœ… Model loaded and verified on GPU.\")\n"
150
+ ]
151
+ },
152
+ {
153
+ "cell_type": "code",
154
+ "execution_count": null,
155
+ "id": "9e8ef3c2",
156
+ "metadata": {},
157
+ "outputs": [],
158
+ "source": [
159
+ "import sqlite3\n",
160
+ "from tqdm import tqdm\n",
161
+ "\n",
162
+ "BATCH_SIZE = 512 # A100 can handle 512 at max_len=128 comfortably\n",
163
+ "\n",
164
+ "conn = sqlite3.connect(DB_LOCAL_PATH)\n",
165
+ "\n",
166
+ "# Only fetch rows not yet annotated β€” resumes safely if interrupted\n",
167
+ "rows = conn.execute(\n",
168
+ " \"SELECT id, text FROM chunks WHERE emotion_label = -1\"\n",
169
+ ").fetchall()\n",
170
+ "\n",
171
+ "print(f\"Rows to annotate: {len(rows):,}\")\n",
172
+ "if len(rows) == 0:\n",
173
+ " print(\"Nothing to do β€” all rows already annotated.\")\n",
174
+ "else:\n",
175
+ " updates = []\n",
176
+ "\n",
177
+ " for i in tqdm(range(0, len(rows), BATCH_SIZE), desc=\"Annotating\"):\n",
178
+ " batch = rows[i : i + BATCH_SIZE]\n",
179
+ " ids = [r[0] for r in batch]\n",
180
+ " texts = [r[1] for r in batch]\n",
181
+ "\n",
182
+ " enc = tokenizer(\n",
183
+ " texts,\n",
184
+ " truncation=True,\n",
185
+ " max_length=128,\n",
186
+ " padding=True,\n",
187
+ " return_tensors=\"pt\"\n",
188
+ " ).to(DEVICE)\n",
189
+ "\n",
190
+ " with torch.no_grad():\n",
191
+ " logits = model(**enc).logits\n",
192
+ "\n",
193
+ " labels = logits.argmax(-1).tolist()\n",
194
+ "\n",
195
+ " for rid, lbl in zip(ids, labels):\n",
196
+ " score = SAFETY_SCORE_MAP[lbl]\n",
197
+ " updates.append((lbl, score, rid))\n",
198
+ "\n",
199
+ " # Commit every 50 batches (~25,600 rows) to avoid losing work if disconnected\n",
200
+ " if (i // BATCH_SIZE) % 50 == 0 and updates:\n",
201
+ " conn.executemany(\n",
202
+ " \"UPDATE chunks SET emotion_label=?, safety_score=? WHERE id=?\",\n",
203
+ " updates\n",
204
+ " )\n",
205
+ " conn.commit()\n",
206
+ " updates = []\n",
207
+ "\n",
208
+ " # Final commit\n",
209
+ " if updates:\n",
210
+ " conn.executemany(\n",
211
+ " \"UPDATE chunks SET emotion_label=?, safety_score=? WHERE id=?\",\n",
212
+ " updates\n",
213
+ " )\n",
214
+ " conn.commit()\n",
215
+ "\n",
216
+ "print(\"\\nβœ… Annotation complete.\")\n"
217
+ ]
218
+ },
219
+ {
220
+ "cell_type": "code",
221
+ "execution_count": null,
222
+ "id": "f434d2b2",
223
+ "metadata": {},
224
+ "outputs": [],
225
+ "source": [
226
+ "from collections import Counter\n",
227
+ "\n",
228
+ "total = conn.execute(\"SELECT COUNT(*) FROM chunks\").fetchone()[0]\n",
229
+ "annotated = conn.execute(\"SELECT COUNT(*) FROM chunks WHERE emotion_label != -1\").fetchone()[0]\n",
230
+ "unannotated= conn.execute(\"SELECT COUNT(*) FROM chunks WHERE emotion_label = -1\").fetchone()[0]\n",
231
+ "\n",
232
+ "print(f\"Total rows : {total:,}\")\n",
233
+ "print(f\"Annotated : {annotated:,}\")\n",
234
+ "print(f\"Unannotated : {unannotated:,} ← must be 0\")\n",
235
+ "\n",
236
+ "rows = conn.execute(\"SELECT emotion_label FROM chunks\").fetchall()\n",
237
+ "dist = Counter(r[0] for r in rows)\n",
238
+ "print(\"\\nEmotion distribution:\")\n",
239
+ "for i, name in enumerate(LABEL_NAMES):\n",
240
+ " pct = 100 * dist[i] / total\n",
241
+ " print(f\" {i} {name:<15} {dist[i]:>9,} ({pct:.1f}%)\")\n",
242
+ "\n",
243
+ "conn.close()\n",
244
+ "assert unannotated == 0, f\"{unannotated:,} rows still unannotated!\"\n",
245
+ "print(\"\\nβœ… All rows annotated.\")\n"
246
+ ]
247
+ },
248
+ {
249
+ "cell_type": "code",
250
+ "execution_count": null,
251
+ "id": "945552e5",
252
+ "metadata": {},
253
+ "outputs": [],
254
+ "source": [
255
+ "import shutil\n",
256
+ "\n",
257
+ "print(f\"Copying annotated DB to Drive: {DB_DRIVE_PATH}\")\n",
258
+ "os.makedirs(os.path.dirname(DB_DRIVE_PATH), exist_ok=True)\n",
259
+ "shutil.copy2(DB_LOCAL_PATH, DB_DRIVE_PATH)\n",
260
+ "\n",
261
+ "size_mb = os.path.getsize(DB_DRIVE_PATH) / 1e6\n",
262
+ "print(f\"Saved: {DB_DRIVE_PATH} ({size_mb:.1f} MB)\")\n",
263
+ "print(\"\\nβœ… Done. Download metadata.db from Drive and replace data/indexes/metadata.db locally.\")\n"
264
+ ]
265
+ },
266
+ {
267
+ "cell_type": "markdown",
268
+ "id": "ae95d9cd",
269
+ "metadata": {},
270
+ "source": [
271
+ "## βœ… Annotation Complete β€” Download Steps\n",
272
+ "\n",
273
+ "1. In Google Drive, navigate to `My Drive/empathrag/metadata.db`\n",
274
+ "2. Right-click β†’ Download\n",
275
+ "3. Replace your local `data/indexes/metadata.db` with the downloaded file\n",
276
+ "4. Verify locally:\n",
277
+ "```python\n",
278
+ "import sqlite3\n",
279
+ "from collections import Counter\n",
280
+ "conn = sqlite3.connect('data/indexes/metadata.db')\n",
281
+ "unannotated = conn.execute('SELECT COUNT(*) FROM chunks WHERE emotion_label = -1').fetchone()[0]\n",
282
+ "print(f'Unannotated: {unannotated}') # must be 0\n",
283
+ "conn.close()\n",
284
+ "```\n"
285
+ ]
286
+ }
287
+ ],
288
+ "metadata": {
289
+ "accelerator": "GPU",
290
+ "colab": {
291
+ "gpuType": "A100",
292
+ "name": "colab_annotate_corpus.ipynb",
293
+ "provenance": []
294
+ },
295
+ "kernelspec": {
296
+ "display_name": "Python 3",
297
+ "language": "python",
298
+ "name": "python3"
299
+ },
300
+ "language_info": {
301
+ "name": "python",
302
+ "version": "3.12.0"
303
+ }
304
+ },
305
+ "nbformat": 4,
306
+ "nbformat_minor": 5
307
+ }
notebooks/colab_emotion_classifier.ipynb ADDED
@@ -0,0 +1,499 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "id": "588c9c40",
6
+ "metadata": {},
7
+ "source": [
8
+ "# EmpathRAG β€” RoBERTa Emotion Classifier (Day 6)\n",
9
+ "**MSML641 Β· NLP Course Project** \n",
10
+ "Fine-tune RoBERTa-base + LoRA on GoEmotions β†’ 5-class emotion taxonomy \n",
11
+ "Target: Weighted F1 > 0.55 on 5-class test set\n",
12
+ "\n",
13
+ "---\n",
14
+ "**Before running:** Runtime β†’ Change runtime type β†’ **A100** β†’ Save \n",
15
+ "Cell 1 will hard-stop if the wrong GPU is attached.\n"
16
+ ]
17
+ },
18
+ {
19
+ "cell_type": "code",
20
+ "execution_count": null,
21
+ "id": "cce46322",
22
+ "metadata": {},
23
+ "outputs": [],
24
+ "source": [
25
+ "# ── STEP 1: Verify A100 is attached ──────────────────────────────────────────\n",
26
+ "import torch\n",
27
+ "\n",
28
+ "gpu_name = torch.cuda.get_device_name(0) if torch.cuda.is_available() else \"NO GPU\"\n",
29
+ "print(f\"GPU : {gpu_name}\")\n",
30
+ "print(f\"CUDA : {torch.cuda.is_available()}\")\n",
31
+ "if torch.cuda.is_available():\n",
32
+ " vram_gb = torch.cuda.get_device_properties(0).total_memory / 1e9\n",
33
+ " print(f\"VRAM : {vram_gb:.1f} GB\")\n",
34
+ " print(f\"PyTorch : {torch.__version__}\")\n",
35
+ "\n",
36
+ "assert torch.cuda.is_available(), \"No GPU found β€” switch to A100 runtime before proceeding.\"\n",
37
+ "print(\"\\nβœ… GPU check passed.\")\n"
38
+ ]
39
+ },
40
+ {
41
+ "cell_type": "code",
42
+ "execution_count": null,
43
+ "id": "3fdcc2e1",
44
+ "metadata": {},
45
+ "outputs": [],
46
+ "source": [
47
+ "# ── STEP 2: Install missing packages ─────────────────────────────────────────\n",
48
+ "# Strategy: Colab already has numpy 2.x, scikit-learn 1.6+, transformers 4.57+,\n",
49
+ "# torch 2.8+, datasets 2.x. We install ONLY what's missing.\n",
50
+ "# DO NOT pin numpy or scikit-learn β€” that caused the previous conflicts.\n",
51
+ "\n",
52
+ "!pip install -q --upgrade \\\n",
53
+ " peft>=0.18.0 \\\n",
54
+ " evaluate==0.4.6 \\\n",
55
+ " accelerate>=1.0.0\n",
56
+ "\n",
57
+ "# Verify no numpy conflict\n",
58
+ "import numpy as np\n",
59
+ "import sklearn\n",
60
+ "print(f\"numpy : {np.__version__}\")\n",
61
+ "print(f\"scikit-learn: {sklearn.__version__}\")\n",
62
+ "\n",
63
+ "import transformers, peft, evaluate as ev, accelerate\n",
64
+ "print(f\"transformers: {transformers.__version__}\")\n",
65
+ "print(f\"peft : {peft.__version__}\")\n",
66
+ "print(f\"evaluate : {ev.__version__}\")\n",
67
+ "print(f\"accelerate : {accelerate.__version__}\")\n",
68
+ "print(\"\\nβœ… All packages ready.\")\n"
69
+ ]
70
+ },
71
+ {
72
+ "cell_type": "code",
73
+ "execution_count": null,
74
+ "id": "b0e0abe9",
75
+ "metadata": {},
76
+ "outputs": [],
77
+ "source": [
78
+ "# ── STEP 3: Mount Drive, set checkpoint dir, detect existing checkpoints ──────\n",
79
+ "from google.colab import drive\n",
80
+ "import os, glob\n",
81
+ "\n",
82
+ "drive.mount(\"/content/drive\")\n",
83
+ "\n",
84
+ "SAVE_DIR = \"/content/drive/MyDrive/empathrag/emotion_classifier\"\n",
85
+ "os.makedirs(SAVE_DIR, exist_ok=True)\n",
86
+ "print(f\"Checkpoint dir: {SAVE_DIR}\")\n",
87
+ "\n",
88
+ "existing = sorted(glob.glob(os.path.join(SAVE_DIR, \"checkpoint-*\")))\n",
89
+ "if existing:\n",
90
+ " print(f\"\\nFound {len(existing)} checkpoint(s):\")\n",
91
+ " for c in existing:\n",
92
+ " print(f\" {c}\")\n",
93
+ " RESUME_FROM = existing[-1]\n",
94
+ " print(f\"\\nWill resume from: {RESUME_FROM}\")\n",
95
+ "else:\n",
96
+ " RESUME_FROM = None\n",
97
+ " print(\"\\nNo existing checkpoints β€” starting fresh.\")\n"
98
+ ]
99
+ },
100
+ {
101
+ "cell_type": "code",
102
+ "execution_count": null,
103
+ "id": "ed2782fa",
104
+ "metadata": {},
105
+ "outputs": [],
106
+ "source": [
107
+ "# ── STEP 4: Load GoEmotions dataset, remap 27 β†’ 5 classes ────────────────────\n",
108
+ "from datasets import load_dataset\n",
109
+ "from collections import Counter\n",
110
+ "\n",
111
+ "# 5-class taxonomy:\n",
112
+ "# 0 = distress (grief, remorse, fear, sadness)\n",
113
+ "# 1 = anxiety (nervousness, confusion, embarrassment)\n",
114
+ "# 2 = frustration(anger, annoyance, disappointment, disgust)\n",
115
+ "# 3 = neutral (neutral)\n",
116
+ "# 4 = hopeful (optimism, relief, gratitude, joy, love, admiration,\n",
117
+ "# amusement, approval, caring, curiosity, desire,\n",
118
+ "# excitement, pride, realization, surprise)\n",
119
+ "\n",
120
+ "LABEL_MAP = {\n",
121
+ " \"grief\": 0, \"remorse\": 0, \"fear\": 0, \"sadness\": 0,\n",
122
+ " \"nervousness\": 1, \"confusion\": 1, \"embarrassment\": 1,\n",
123
+ " \"anger\": 2, \"annoyance\": 2, \"disappointment\": 2, \"disgust\": 2,\n",
124
+ " \"neutral\": 3,\n",
125
+ " \"optimism\": 4, \"relief\": 4, \"gratitude\": 4, \"joy\": 4,\n",
126
+ " \"love\": 4, \"admiration\": 4, \"amusement\": 4, \"approval\": 4,\n",
127
+ " \"caring\": 4, \"curiosity\": 4, \"desire\": 4, \"excitement\": 4,\n",
128
+ " \"pride\": 4, \"realization\": 4, \"surprise\": 4,\n",
129
+ "}\n",
130
+ "LABEL_NAMES = [\"distress\", \"anxiety\", \"frustration\", \"neutral\", \"hopeful\"]\n",
131
+ "\n",
132
+ "print(\"Loading GoEmotions (simplified split)...\")\n",
133
+ "raw = load_dataset(\"google-research-datasets/go_emotions\", \"simplified\")\n",
134
+ "print(f\"Train: {len(raw['train']):,} | Val: {len(raw['validation']):,} | Test: {len(raw['test']):,}\")\n",
135
+ "\n",
136
+ "# Inspect label format β€” GoEmotions simplified stores labels as list of ints\n",
137
+ "# that are indices into feature_names\n",
138
+ "feature_names = raw[\"train\"].features[\"labels\"].feature.names\n",
139
+ "print(f\"\\nDataset has {len(feature_names)} emotion classes.\")\n",
140
+ "print(f\"Sample row labels: {raw['train'][0]['labels']} β†’ \"\n",
141
+ " f\"{[feature_names[i] for i in raw['train'][0]['labels']]}\")\n",
142
+ "\n",
143
+ "def remap(example):\n",
144
+ " \"\"\"Collapse multi-hot 27-class labels to single 5-class coarse label.\n",
145
+ " Takes the first label that appears in LABEL_MAP; falls back to neutral (3).\n",
146
+ " \"\"\"\n",
147
+ " for lid in example[\"labels\"]:\n",
148
+ " name = feature_names[lid]\n",
149
+ " if name in LABEL_MAP:\n",
150
+ " return {\"label\": LABEL_MAP[name]}\n",
151
+ " return {\"label\": 3} # neutral fallback\n",
152
+ "\n",
153
+ "print(\"\\nRemapping labels...\")\n",
154
+ "dataset = raw.map(remap, desc=\"27β†’5 remap\")\n",
155
+ "\n",
156
+ "# Distribution check\n",
157
+ "dist = Counter(dataset[\"train\"][\"label\"])\n",
158
+ "print(\"\\nTrain class distribution:\")\n",
159
+ "for i, name in enumerate(LABEL_NAMES):\n",
160
+ " pct = 100 * dist[i] / len(dataset[\"train\"])\n",
161
+ " print(f\" {i} {name:<15} {dist[i]:>6,} ({pct:.1f}%)\")\n"
162
+ ]
163
+ },
164
+ {
165
+ "cell_type": "code",
166
+ "execution_count": null,
167
+ "id": "52ddb3a8",
168
+ "metadata": {},
169
+ "outputs": [],
170
+ "source": [
171
+ "# ── STEP 5: Tokenize ─────────────────────────────────────────────────────────\n",
172
+ "from transformers import AutoTokenizer\n",
173
+ "\n",
174
+ "MODEL_NAME = \"roberta-base\"\n",
175
+ "print(f\"Loading tokenizer: {MODEL_NAME}\")\n",
176
+ "tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)\n",
177
+ "\n",
178
+ "def tokenize(batch):\n",
179
+ " return tokenizer(\n",
180
+ " batch[\"text\"],\n",
181
+ " truncation=True,\n",
182
+ " max_length=128,\n",
183
+ " padding=\"max_length\",\n",
184
+ " )\n",
185
+ "\n",
186
+ "print(\"Tokenizing (this takes ~2 min)...\")\n",
187
+ "tokenized = dataset.map(tokenize, batched=True, desc=\"Tokenizing\")\n",
188
+ "tokenized = tokenized.rename_column(\"label\", \"labels\")\n",
189
+ "tokenized.set_format(type=\"torch\", columns=[\"input_ids\", \"attention_mask\", \"labels\"])\n",
190
+ "\n",
191
+ "print(f\"\\nTokenization complete.\")\n",
192
+ "print(f\" Train : {len(tokenized['train']):,}\")\n",
193
+ "print(f\" Val : {len(tokenized['validation']):,}\")\n",
194
+ "print(f\" Test : {len(tokenized['test']):,}\")\n"
195
+ ]
196
+ },
197
+ {
198
+ "cell_type": "code",
199
+ "execution_count": null,
200
+ "id": "0c985e81",
201
+ "metadata": {},
202
+ "outputs": [],
203
+ "source": [
204
+ "# ── STEP 6: RoBERTa + LoRA model ─────────────────────────────────────────────\n",
205
+ "import torch\n",
206
+ "from transformers import AutoModelForSequenceClassification\n",
207
+ "from peft import get_peft_model, LoraConfig, TaskType\n",
208
+ "\n",
209
+ "id2label = {i: n for i, n in enumerate(LABEL_NAMES)}\n",
210
+ "label2id = {n: i for i, n in enumerate(LABEL_NAMES)}\n",
211
+ "\n",
212
+ "# LoRA config: targets query + value projections in RoBERTa attention\n",
213
+ "# r=16 β†’ ~3M trainable params vs ~125M full fine-tune\n",
214
+ "lora_cfg = LoraConfig(\n",
215
+ " task_type=TaskType.SEQ_CLS,\n",
216
+ " r=16,\n",
217
+ " lora_alpha=32,\n",
218
+ " lora_dropout=0.1,\n",
219
+ " target_modules=[\"query\", \"value\"],\n",
220
+ " bias=\"none\",\n",
221
+ ")\n",
222
+ "\n",
223
+ "base_model = AutoModelForSequenceClassification.from_pretrained(\n",
224
+ " MODEL_NAME,\n",
225
+ " num_labels=5,\n",
226
+ " id2label=id2label,\n",
227
+ " label2id=label2id,\n",
228
+ ")\n",
229
+ "model = get_peft_model(base_model, lora_cfg)\n",
230
+ "model.print_trainable_parameters()\n",
231
+ "\n",
232
+ "device = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n",
233
+ "print(f\"\\nTraining on: {device}\")\n"
234
+ ]
235
+ },
236
+ {
237
+ "cell_type": "code",
238
+ "execution_count": null,
239
+ "id": "7c43de57",
240
+ "metadata": {},
241
+ "outputs": [],
242
+ "source": [
243
+ "# ── STEP 7: Evaluation metrics ───────────────────────────────────────────────\n",
244
+ "import evaluate\n",
245
+ "import numpy as np\n",
246
+ "\n",
247
+ "f1_metric = evaluate.load(\"f1\")\n",
248
+ "\n",
249
+ "def compute_metrics(eval_pred):\n",
250
+ " logits, labels = eval_pred\n",
251
+ " preds = np.argmax(logits, axis=-1)\n",
252
+ "\n",
253
+ " # Weighted F1 β€” primary metric (handles class imbalance)\n",
254
+ " weighted_f1 = f1_metric.compute(\n",
255
+ " predictions=preds, references=labels, average=\"weighted\"\n",
256
+ " )[\"f1\"]\n",
257
+ "\n",
258
+ " # Per-class F1 β€” required for paper's breakdown table\n",
259
+ " per_class = f1_metric.compute(\n",
260
+ " predictions=preds, references=labels,\n",
261
+ " average=None, labels=list(range(5))\n",
262
+ " )[\"f1\"]\n",
263
+ "\n",
264
+ " result = {\"f1_weighted\": weighted_f1}\n",
265
+ " for i, name in enumerate(LABEL_NAMES):\n",
266
+ " result[f\"f1_{name}\"] = float(per_class[i])\n",
267
+ " return result\n",
268
+ "\n",
269
+ "print(\"Metrics function ready (weighted F1 + per-class F1 for all 5 classes).\")\n"
270
+ ]
271
+ },
272
+ {
273
+ "cell_type": "code",
274
+ "execution_count": null,
275
+ "id": "70116ba5",
276
+ "metadata": {},
277
+ "outputs": [],
278
+ "source": [
279
+ "# ── STEP 8: Training configuration ───────────────────────────────────────────\n",
280
+ "from transformers import TrainingArguments\n",
281
+ "\n",
282
+ "training_args = TrainingArguments(\n",
283
+ " output_dir=SAVE_DIR,\n",
284
+ "\n",
285
+ " # Schedule\n",
286
+ " num_train_epochs=5,\n",
287
+ " per_device_train_batch_size=64, # A100 40GB handles this fine at max_len=128\n",
288
+ " per_device_eval_batch_size=128,\n",
289
+ " learning_rate=2e-4, # Higher than full fine-tune β€” correct for LoRA\n",
290
+ " warmup_ratio=0.1,\n",
291
+ " weight_decay=0.01,\n",
292
+ "\n",
293
+ " # Checkpointing\n",
294
+ " eval_strategy=\"epoch\",\n",
295
+ " save_strategy=\"epoch\",\n",
296
+ " load_best_model_at_end=True,\n",
297
+ " metric_for_best_model=\"f1_weighted\",\n",
298
+ " greater_is_better=True,\n",
299
+ " save_total_limit=3, # Keep last 3 to save Drive space\n",
300
+ "\n",
301
+ " # Speed\n",
302
+ " fp16=True,\n",
303
+ " dataloader_num_workers=4,\n",
304
+ "\n",
305
+ " # Logging\n",
306
+ " logging_dir=f\"{SAVE_DIR}/logs\",\n",
307
+ " logging_steps=200,\n",
308
+ " report_to=\"none\",\n",
309
+ ")\n",
310
+ "\n",
311
+ "print(\"TrainingArguments set:\")\n",
312
+ "print(f\" Epochs : {training_args.num_train_epochs}\")\n",
313
+ "print(f\" Train batch : {training_args.per_device_train_batch_size}\")\n",
314
+ "print(f\" Learning rate : {training_args.learning_rate}\")\n",
315
+ "print(f\" Best metric : {training_args.metric_for_best_model}\")\n",
316
+ "print(f\" Save dir : {SAVE_DIR}\")\n",
317
+ "print(f\" Resume from : {RESUME_FROM}\")\n"
318
+ ]
319
+ },
320
+ {
321
+ "cell_type": "code",
322
+ "execution_count": null,
323
+ "id": "9d9a3ee0",
324
+ "metadata": {},
325
+ "outputs": [],
326
+ "source": [
327
+ "# ── STEP 9: Train ────────────────────────────────────────────────────────────\n",
328
+ "# Expected time on A100: ~75–90 minutes\n",
329
+ "# Checkpoints saved to Drive after every epoch β€” safe to disconnect and resume.\n",
330
+ "from transformers import Trainer\n",
331
+ "\n",
332
+ "trainer = Trainer(\n",
333
+ " model=model,\n",
334
+ " args=training_args,\n",
335
+ " train_dataset=tokenized[\"train\"],\n",
336
+ " eval_dataset=tokenized[\"validation\"],\n",
337
+ " compute_metrics=compute_metrics,\n",
338
+ ")\n",
339
+ "\n",
340
+ "print(\"Starting training...\")\n",
341
+ "print(f\"Resuming from: {RESUME_FROM}\" if RESUME_FROM else \"Fresh training run.\")\n",
342
+ "print(\"-\" * 60)\n",
343
+ "\n",
344
+ "trainer.train(resume_from_checkpoint=RESUME_FROM)\n",
345
+ "\n",
346
+ "print(\"-\" * 60)\n",
347
+ "print(\"Training complete.\")\n"
348
+ ]
349
+ },
350
+ {
351
+ "cell_type": "code",
352
+ "execution_count": null,
353
+ "id": "173bf559",
354
+ "metadata": {},
355
+ "outputs": [],
356
+ "source": [
357
+ "# ── STEP 10: Save final model + tokenizer to Drive ───────────────────────────\n",
358
+ "import os\n",
359
+ "\n",
360
+ "trainer.save_model(SAVE_DIR)\n",
361
+ "tokenizer.save_pretrained(SAVE_DIR)\n",
362
+ "\n",
363
+ "print(f\"Model saved to: {SAVE_DIR}\")\n",
364
+ "print(\"\\nContents:\")\n",
365
+ "for f in sorted(os.listdir(SAVE_DIR)):\n",
366
+ " fpath = os.path.join(SAVE_DIR, f)\n",
367
+ " size = os.path.getsize(fpath) if os.path.isfile(fpath) else 0\n",
368
+ " label = f\"{size/1e6:.1f} MB\" if size > 0 else \"dir\"\n",
369
+ " print(f\" {f:<45} {label}\")\n"
370
+ ]
371
+ },
372
+ {
373
+ "cell_type": "code",
374
+ "execution_count": null,
375
+ "id": "0258fa90",
376
+ "metadata": {},
377
+ "outputs": [],
378
+ "source": [
379
+ "# ── STEP 11: Evaluate on held-out test set ───────────────────────────────────\n",
380
+ "import json\n",
381
+ "import numpy as np\n",
382
+ "\n",
383
+ "print(\"Evaluating on test set (5,427 examples)...\")\n",
384
+ "test_results = trainer.evaluate(tokenized[\"test\"])\n",
385
+ "\n",
386
+ "print(\"\\n=== TEST SET RESULTS ===\")\n",
387
+ "weighted_f1 = test_results[\"eval_f1_weighted\"]\n",
388
+ "print(f\"Weighted F1 : {weighted_f1:.4f} (target: > 0.55)\")\n",
389
+ "print()\n",
390
+ "print(\"Per-class F1:\")\n",
391
+ "for name in LABEL_NAMES:\n",
392
+ " val = test_results.get(f\"eval_f1_{name}\", float(\"nan\"))\n",
393
+ " print(f\" {name:<15} {val:.4f}\")\n",
394
+ "\n",
395
+ "print()\n",
396
+ "if weighted_f1 >= 0.55:\n",
397
+ " print(\"βœ… PASS β€” weighted F1 exceeds target of 0.55\")\n",
398
+ "else:\n",
399
+ " print(\"⚠️ BELOW TARGET\")\n",
400
+ " print(\" Fallback option: lower lr to 1e-4, increase epochs to 8, re-run from checkpoint.\")\n",
401
+ "\n",
402
+ "# Save to Drive for the paper\n",
403
+ "results_path = f\"{SAVE_DIR}/test_results.json\"\n",
404
+ "with open(results_path, \"w\") as f:\n",
405
+ " json.dump({k: float(v) for k, v in test_results.items()}, f, indent=2)\n",
406
+ "print(f\"\\nResults saved: {results_path}\")\n"
407
+ ]
408
+ },
409
+ {
410
+ "cell_type": "code",
411
+ "execution_count": null,
412
+ "id": "3c47dff9",
413
+ "metadata": {},
414
+ "outputs": [],
415
+ "source": [
416
+ "# ── STEP 12: Confusion matrix + classification report ────────────────────────\n",
417
+ "from sklearn.metrics import confusion_matrix, classification_report\n",
418
+ "import numpy as np\n",
419
+ "\n",
420
+ "print(\"Computing confusion matrix on test set...\")\n",
421
+ "pred_output = trainer.predict(tokenized[\"test\"])\n",
422
+ "preds = np.argmax(pred_output.predictions, axis=-1)\n",
423
+ "labels = pred_output.label_ids\n",
424
+ "\n",
425
+ "print(\"\\n=== CLASSIFICATION REPORT ===\")\n",
426
+ "print(classification_report(labels, preds, target_names=LABEL_NAMES))\n",
427
+ "\n",
428
+ "cm = confusion_matrix(labels, preds)\n",
429
+ "print(\"=== CONFUSION MATRIX (rows=true, cols=pred) ===\")\n",
430
+ "header = f\"{'':>15}\" + \"\".join(f\"{n:>13}\" for n in LABEL_NAMES)\n",
431
+ "print(header)\n",
432
+ "for i, row in enumerate(cm):\n",
433
+ " print(f\"{LABEL_NAMES[i]:>15}\" + \"\".join(f\"{v:>13}\" for v in row))\n",
434
+ "\n",
435
+ "np.save(f\"{SAVE_DIR}/confusion_matrix.npy\", cm)\n",
436
+ "print(f\"\\nConfusion matrix saved to Drive.\")\n"
437
+ ]
438
+ },
439
+ {
440
+ "cell_type": "markdown",
441
+ "id": "dc6f3e30",
442
+ "metadata": {},
443
+ "source": [
444
+ "## βœ… Training Complete β€” Day 10 Checklist\n",
445
+ "\n",
446
+ "### 1. Download checkpoint from Drive\n",
447
+ "Navigate in Google Drive to: \n",
448
+ "`My Drive/empathrag/emotion_classifier/`\n",
449
+ "\n",
450
+ "Download the entire folder and place it at: \n",
451
+ "`models/emotion_classifier/` in your local repo.\n",
452
+ "\n",
453
+ "Required files:\n",
454
+ "- `adapter_config.json`\n",
455
+ "- `adapter_model.safetensors` (or `.bin`) β€” ~12 MB LoRA weights\n",
456
+ "- `config.json`\n",
457
+ "- `tokenizer.json`, `tokenizer_config.json`, `vocab.json`, `merges.txt`\n",
458
+ "- `test_results.json` β€” paste weighted F1 into paper\n",
459
+ "- `confusion_matrix.npy` β€” per-class breakdown for paper table\n",
460
+ "\n",
461
+ "### 2. Run corpus annotation (Day 10, locally)\n",
462
+ "```bash\n",
463
+ "python src/models/annotate_corpus.py\n",
464
+ "```\n",
465
+ "Updates all 1,674,369 SQLite rows with predicted emotion labels (~45 min on CPU).\n",
466
+ "\n",
467
+ "### 3. Verify the checkpoint loads locally\n",
468
+ "```python\n",
469
+ "from transformers import AutoTokenizer, AutoModelForSequenceClassification\n",
470
+ "from peft import PeftModel\n",
471
+ "\n",
472
+ "tok = AutoTokenizer.from_pretrained(\"models/emotion_classifier\")\n",
473
+ "base = AutoModelForSequenceClassification.from_pretrained(\"roberta-base\", num_labels=5)\n",
474
+ "model = PeftModel.from_pretrained(base, \"models/emotion_classifier\").eval()\n",
475
+ "print(\"βœ… Checkpoint loads correctly.\")\n",
476
+ "```\n"
477
+ ]
478
+ }
479
+ ],
480
+ "metadata": {
481
+ "accelerator": "GPU",
482
+ "colab": {
483
+ "gpuType": "A100",
484
+ "name": "colab_emotion_classifier.ipynb",
485
+ "provenance": []
486
+ },
487
+ "kernelspec": {
488
+ "display_name": "Python 3",
489
+ "language": "python",
490
+ "name": "python3"
491
+ },
492
+ "language_info": {
493
+ "name": "python",
494
+ "version": "3.12.0"
495
+ }
496
+ },
497
+ "nbformat": 4,
498
+ "nbformat_minor": 5
499
+ }