Mukul Rayana commited on
Commit
dad49b3
·
1 Parent(s): 257c78b

Day 3: proper Colab ipynb notebook for FAISS build

Browse files
notebooks/colab_build_faiss_index.ipynb ADDED
@@ -0,0 +1,274 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "id": "93b5a3cf",
6
+ "metadata": {},
7
+ "source": [
8
+ "# EmpathRAG — FAISS Index Builder\n",
9
+ "**Run on Colab Pro with A100 GPU**\n",
10
+ "\n",
11
+ "Encodes Reddit Mental Health corpus into 768-dim vectors and builds a FAISS index + SQLite sidecar.\n",
12
+ "\n",
13
+ "- Estimated time on A100: 30–60 minutes\n",
14
+ "- Output: `faiss_flat.index` + `metadata.db` saved to Google Drive\n",
15
+ "- **Do not run on local RTX 3060 laptop** — would take 6–12 hours\n",
16
+ "\n",
17
+ "### Before running:\n",
18
+ "1. Upload `data/raw/reddit_mental_health/` (108 CSVs, ~3.1GB) to Google Drive at `My Drive/empathrag/data/raw/reddit_mental_health/`\n",
19
+ "2. Set Runtime → Change runtime type → **A100 GPU**\n",
20
+ "3. Run all cells in order"
21
+ ]
22
+ },
23
+ {
24
+ "cell_type": "code",
25
+ "execution_count": null,
26
+ "id": "b9ab42f1",
27
+ "metadata": {},
28
+ "outputs": [],
29
+ "source": [
30
+ "# Cell 1: Install dependencies\n",
31
+ "!pip install sentence-transformers faiss-cpu tqdm transformers -q"
32
+ ]
33
+ },
34
+ {
35
+ "cell_type": "code",
36
+ "execution_count": null,
37
+ "id": "9129b2f3",
38
+ "metadata": {},
39
+ "outputs": [],
40
+ "source": [
41
+ "# Cell 2: Mount Google Drive\n",
42
+ "from google.colab import drive\n",
43
+ "drive.mount(\"/content/drive\")\n",
44
+ "\n",
45
+ "BASE = \"/content/drive/MyDrive/empathrag\"\n",
46
+ "REDDIT_DIR = f\"{BASE}/data/raw/reddit_mental_health\"\n",
47
+ "INDEX_PATH = f\"{BASE}/data/indexes/faiss_flat.index\"\n",
48
+ "DB_PATH = f\"{BASE}/data/indexes/metadata.db\"\n",
49
+ "CKPT_PATH = f\"{BASE}/data/indexes/embeddings_checkpoint.npy\"\n",
50
+ "\n",
51
+ "import os\n",
52
+ "os.makedirs(f\"{BASE}/data/indexes\", exist_ok=True)\n",
53
+ "print(\"Drive mounted. Paths configured.\")\n",
54
+ "print(f\"Reddit dir exists: {os.path.exists(REDDIT_DIR)}\")\n",
55
+ "files = [f for f in os.listdir(REDDIT_DIR) if f.endswith('.csv')] if os.path.exists(REDDIT_DIR) else []\n",
56
+ "print(f\"CSV files found: {len(files)}\")"
57
+ ]
58
+ },
59
+ {
60
+ "cell_type": "code",
61
+ "execution_count": null,
62
+ "id": "54ccc2ba",
63
+ "metadata": {},
64
+ "outputs": [],
65
+ "source": [
66
+ "# Cell 3: Helper functions\n",
67
+ "import re\n",
68
+ "\n",
69
+ "def clean_text(text: str) -> str:\n",
70
+ " text = re.sub(r\"u/\\w+\", \"\", text)\n",
71
+ " text = re.sub(r\"r/\\w+\", \"\", text)\n",
72
+ " text = re.sub(r\"http\\S+\", \"\", text)\n",
73
+ " text = re.sub(r\"\\[deleted\\]|\\[removed\\]\", \"\", text)\n",
74
+ " text = re.sub(r\"[^\\x00-\\x7F]+\", \" \", text)\n",
75
+ " text = re.sub(r\"\\s+\", \" \", text).strip()\n",
76
+ " return text\n",
77
+ "\n",
78
+ "def chunk_text(text, tokenizer, chunk_size=256, stride=32, max_chunks=8):\n",
79
+ " tokens = tokenizer.encode(text)\n",
80
+ " if len(tokens) < 64:\n",
81
+ " return [text]\n",
82
+ " chunks = []\n",
83
+ " start = 0\n",
84
+ " while start < len(tokens) and len(chunks) < max_chunks:\n",
85
+ " end = min(start + chunk_size, len(tokens))\n",
86
+ " chunks.append(tokenizer.decode(tokens[start:end], skip_special_tokens=True))\n",
87
+ " start += chunk_size - stride\n",
88
+ " return chunks\n",
89
+ "\n",
90
+ "print(\"Helper functions defined.\")"
91
+ ]
92
+ },
93
+ {
94
+ "cell_type": "code",
95
+ "execution_count": null,
96
+ "id": "68b83384",
97
+ "metadata": {},
98
+ "outputs": [],
99
+ "source": [
100
+ "# Cell 4: Load and chunk Reddit posts\n",
101
+ "# This reads all 108 CSVs and chunks the post text.\n",
102
+ "# Expected: 1-5 million chunks depending on corpus size.\n",
103
+ "import pandas as pd\n",
104
+ "from transformers import AutoTokenizer\n",
105
+ "from tqdm import tqdm\n",
106
+ "\n",
107
+ "tok = AutoTokenizer.from_pretrained(\"roberta-base\")\n",
108
+ "\n",
109
+ "all_posts = []\n",
110
+ "csv_files = [f for f in os.listdir(REDDIT_DIR) if f.endswith('.csv')]\n",
111
+ "print(f\"Reading {len(csv_files)} CSV files...\")\n",
112
+ "\n",
113
+ "for fname in tqdm(csv_files, desc=\"Reading CSVs\"):\n",
114
+ " fpath = os.path.join(REDDIT_DIR, fname)\n",
115
+ " try:\n",
116
+ " df = pd.read_csv(fpath, on_bad_lines=\"skip\",\n",
117
+ " usecols=lambda c: c in [\"post\", \"body\", \"selftext\"])\n",
118
+ " for col in [\"post\", \"body\", \"selftext\"]:\n",
119
+ " if col in df.columns:\n",
120
+ " all_posts.extend(df[col].dropna().tolist())\n",
121
+ " break\n",
122
+ " except Exception as e:\n",
123
+ " print(f\" Skipping {fname}: {e}\")\n",
124
+ "\n",
125
+ "print(f\"Raw posts loaded: {len(all_posts):,}\")\n",
126
+ "\n",
127
+ "chunks = []\n",
128
+ "for post in tqdm(all_posts, desc=\"Chunking\"):\n",
129
+ " cleaned = clean_text(str(post))\n",
130
+ " if not cleaned:\n",
131
+ " continue\n",
132
+ " chunks.extend(chunk_text(cleaned, tok))\n",
133
+ "\n",
134
+ "print(f\"Total chunks: {len(chunks):,}\")\n",
135
+ "\n",
136
+ "# Save chunks list to Drive as checkpoint\n",
137
+ "import pickle\n",
138
+ "with open(f\"{BASE}/data/indexes/chunks_checkpoint.pkl\", \"wb\") as f:\n",
139
+ " pickle.dump(chunks, f)\n",
140
+ "print(\"Chunks saved to Drive checkpoint.\")"
141
+ ]
142
+ },
143
+ {
144
+ "cell_type": "code",
145
+ "execution_count": null,
146
+ "id": "1ee62270",
147
+ "metadata": {},
148
+ "outputs": [],
149
+ "source": [
150
+ "# Cell 5: Encode embeddings\n",
151
+ "# Uses A100 GPU. Saves progress every 100K chunks so Colab disconnects are recoverable.\n",
152
+ "import numpy as np\n",
153
+ "from sentence_transformers import SentenceTransformer\n",
154
+ "import torch\n",
155
+ "\n",
156
+ "BATCH_SIZE = 256 # Safe for A100 40GB. Reduce to 64 if OOM.\n",
157
+ "SAVE_EVERY = 100_000 # Save checkpoint every N chunks\n",
158
+ "\n",
159
+ "encoder = SentenceTransformer(\"sentence-transformers/all-mpnet-base-v2\")\n",
160
+ "print(f\"Encoding on: {encoder.device}\")\n",
161
+ "print(f\"Total chunks to encode: {len(chunks):,}\")\n",
162
+ "print(f\"Estimated time at batch_size={BATCH_SIZE}: ~{len(chunks) // (BATCH_SIZE * 50)} minutes\")\n",
163
+ "\n",
164
+ "# Check if partial checkpoint exists\n",
165
+ "all_embeddings = []\n",
166
+ "start_idx = 0\n",
167
+ "\n",
168
+ "if os.path.exists(CKPT_PATH):\n",
169
+ " print(f\"Resuming from checkpoint: {CKPT_PATH}\")\n",
170
+ " all_embeddings = list(np.load(CKPT_PATH))\n",
171
+ " start_idx = len(all_embeddings)\n",
172
+ " print(f\"Resuming from chunk {start_idx:,}\")\n",
173
+ "\n",
174
+ "# Encode in segments\n",
175
+ "for seg_start in tqdm(range(start_idx, len(chunks), SAVE_EVERY), desc=\"Encoding segments\"):\n",
176
+ " seg_end = min(seg_start + SAVE_EVERY, len(chunks))\n",
177
+ " seg = chunks[seg_start:seg_end]\n",
178
+ " emb = encoder.encode(seg, batch_size=BATCH_SIZE, show_progress_bar=False,\n",
179
+ " normalize_embeddings=True, convert_to_numpy=True)\n",
180
+ " all_embeddings.extend(emb)\n",
181
+ " # Save checkpoint\n",
182
+ " np.save(CKPT_PATH, np.array(all_embeddings, dtype=np.float32))\n",
183
+ " print(f\" Checkpoint saved at {seg_end:,}/{len(chunks):,} chunks\")\n",
184
+ "\n",
185
+ "embeddings = np.array(all_embeddings, dtype=np.float32)\n",
186
+ "print(f\"Embeddings shape: {embeddings.shape}\")"
187
+ ]
188
+ },
189
+ {
190
+ "cell_type": "code",
191
+ "execution_count": null,
192
+ "id": "a761c6e2",
193
+ "metadata": {},
194
+ "outputs": [],
195
+ "source": [
196
+ "# Cell 6: Build FAISS index and SQLite sidecar\n",
197
+ "import faiss\n",
198
+ "import sqlite3\n",
199
+ "\n",
200
+ "dim = embeddings.shape[1] # 768\n",
201
+ "n = embeddings.shape[0]\n",
202
+ "\n",
203
+ "print(f\"Building FAISS index for {n:,} vectors of dim {dim}...\")\n",
204
+ "\n",
205
+ "if n > 100_000:\n",
206
+ " quantizer = faiss.IndexFlatL2(dim)\n",
207
+ " index = faiss.IndexIVFFlat(quantizer, dim, 100)\n",
208
+ " print(\"Training IVFFlat index (this takes a few minutes)...\")\n",
209
+ " index.train(embeddings)\n",
210
+ "else:\n",
211
+ " index = faiss.IndexFlatL2(dim)\n",
212
+ "\n",
213
+ "index.add(embeddings)\n",
214
+ "faiss.write_index(index, INDEX_PATH)\n",
215
+ "print(f\"FAISS index saved: {index.ntotal:,} vectors → {INDEX_PATH}\")\n",
216
+ "\n",
217
+ "# SQLite sidecar\n",
218
+ "conn = sqlite3.connect(DB_PATH)\n",
219
+ "c = conn.cursor()\n",
220
+ "c.execute(\"\"\"CREATE TABLE IF NOT EXISTS chunks (\n",
221
+ " id INTEGER PRIMARY KEY,\n",
222
+ " text TEXT,\n",
223
+ " emotion_label INTEGER DEFAULT -1,\n",
224
+ " safety_score REAL DEFAULT 0.7,\n",
225
+ " source TEXT\n",
226
+ ")\"\"\")\n",
227
+ "\n",
228
+ "BATCH = 10_000\n",
229
+ "for i in tqdm(range(0, len(chunks), BATCH), desc=\"Writing SQLite\"):\n",
230
+ " batch = chunks[i:i+BATCH]\n",
231
+ " c.executemany(\n",
232
+ " \"INSERT OR REPLACE INTO chunks VALUES (?,?,?,?,?)\",\n",
233
+ " [(i+j, text, -1, 0.7, \"reddit\") for j, text in enumerate(batch)]\n",
234
+ " )\n",
235
+ " conn.commit()\n",
236
+ "conn.close()\n",
237
+ "print(f\"SQLite DB saved: {len(chunks):,} rows → {DB_PATH}\")"
238
+ ]
239
+ },
240
+ {
241
+ "cell_type": "code",
242
+ "execution_count": null,
243
+ "id": "431f2949",
244
+ "metadata": {},
245
+ "outputs": [],
246
+ "source": [
247
+ "# Cell 7: Verify outputs\n",
248
+ "import faiss, sqlite3\n",
249
+ "\n",
250
+ "idx = faiss.read_index(INDEX_PATH)\n",
251
+ "print(f\"FAISS index: {idx.ntotal:,} vectors\")\n",
252
+ "\n",
253
+ "conn = sqlite3.connect(DB_PATH)\n",
254
+ "row_count = conn.execute(\"SELECT COUNT(*) FROM chunks\").fetchone()[0]\n",
255
+ "sample = conn.execute(\"SELECT text FROM chunks LIMIT 3\").fetchall()\n",
256
+ "conn.close()\n",
257
+ "print(f\"SQLite rows: {row_count:,}\")\n",
258
+ "print(\"Sample chunks:\")\n",
259
+ "for i, (t,) in enumerate(sample):\n",
260
+ " print(f\" [{i}] {t[:100]}...\")\n",
261
+ "\n",
262
+ "print()\n",
263
+ "print(\"=== BUILD COMPLETE ===\")\n",
264
+ "print(f\"Download these two files from Drive:\")\n",
265
+ "print(f\" {INDEX_PATH}\")\n",
266
+ "print(f\" {DB_PATH}\")\n",
267
+ "print(\"Place them in your local data/indexes/ folder.\")"
268
+ ]
269
+ }
270
+ ],
271
+ "metadata": {},
272
+ "nbformat": 4,
273
+ "nbformat_minor": 5
274
+ }
notebooks/colab_build_faiss_index.py DELETED
@@ -1,164 +0,0 @@
1
- # EmpathRAG — FAISS Index Builder
2
- # Run on Google Colab Pro (A100).
3
- # Estimated time: 30-60 minutes for full Reddit Mental Health corpus.
4
- #
5
- # SETUP INSTRUCTIONS:
6
- # 1. Upload the entire data/raw/reddit_mental_health/ folder to Google Drive
7
- # at: My Drive/empathrag/data/raw/reddit_mental_health/
8
- # 2. Set Colab runtime to A100 GPU
9
- # 3. Run all cells in order
10
- # 4. Download faiss_flat.index and metadata.db from Drive when done
11
-
12
- # ── Cell 1: Install ──────────────────────────────────────────────────────────
13
- # !pip install sentence-transformers faiss-cpu tqdm -q
14
- # !pip install transformers -q
15
-
16
- # ── Cell 2: Mount Drive ──────────────────────────────────────────────────────
17
- # from google.colab import drive
18
- # drive.mount("/content/drive")
19
-
20
- # BASE = "/content/drive/MyDrive/empathrag"
21
- # REDDIT_DIR = f"{BASE}/data/raw/reddit_mental_health"
22
- # INDEX_PATH = f"{BASE}/data/indexes/faiss_flat.index"
23
- # DB_PATH = f"{BASE}/data/indexes/metadata.db"
24
- # import os
25
- # os.makedirs(f"{BASE}/data/indexes", exist_ok=True)
26
-
27
- # ── Cell 3: Build index ──────────────────────────────────────────────────────
28
- import os
29
- import re
30
- import sqlite3
31
- import faiss
32
- import numpy as np
33
- import pandas as pd
34
- from sentence_transformers import SentenceTransformer
35
- from transformers import AutoTokenizer
36
- from tqdm import tqdm
37
-
38
- # ── Config ───────────────────────────────────────────────────────────────────
39
- # When running locally for testing, override these paths:
40
- REDDIT_DIR = "data/raw/reddit_mental_health"
41
- INDEX_PATH = "data/indexes/faiss_flat.index"
42
- DB_PATH = "data/indexes/metadata.db"
43
-
44
- MODEL_NAME = "sentence-transformers/all-mpnet-base-v2"
45
- CHUNK_SIZE = 256
46
- STRIDE = 32
47
- MAX_CHUNKS = 8
48
- # A100: batch_size=128 is safe. RTX 3060 6GB laptop: use 16.
49
- BATCH_SIZE = 128
50
-
51
-
52
- # ── Text cleaning ─────────────────────────────────────────────────────────────
53
- def clean_text(text: str) -> str:
54
- text = re.sub(r"u/\w+", "", text)
55
- text = re.sub(r"r/\w+", "", text)
56
- text = re.sub(r"http\S+", "", text)
57
- text = re.sub(r"\[deleted\]|\[removed\]", "", text)
58
- text = re.sub(r"[^\x00-\x7F]+", " ", text)
59
- text = re.sub(r"\s+", " ", text).strip()
60
- return text
61
-
62
-
63
- # ── Chunking ──────────────────────────────────────────────────────────────────
64
- def chunk_text(text, tokenizer, chunk_size=CHUNK_SIZE, stride=STRIDE, max_chunks=MAX_CHUNKS):
65
- tokens = tokenizer.encode(text)
66
- if len(tokens) < 64:
67
- return [text]
68
- chunks = []
69
- start = 0
70
- while start < len(tokens) and len(chunks) < max_chunks:
71
- end = min(start + chunk_size, len(tokens))
72
- chunks.append(tokenizer.decode(tokens[start:end], skip_special_tokens=True))
73
- start += chunk_size - stride
74
- return chunks
75
-
76
-
77
- # ── Load posts ────────────────────────────────────────────────────────────────
78
- def load_reddit_posts(data_dir):
79
- all_posts = []
80
- files = [f for f in os.listdir(data_dir) if f.endswith(".csv")]
81
- print(f"Loading from {len(files)} CSV files...")
82
- for fname in tqdm(files, desc="Reading CSVs"):
83
- fpath = os.path.join(data_dir, fname)
84
- try:
85
- df = pd.read_csv(fpath, on_bad_lines="skip", usecols=lambda c: c in ["post", "body", "selftext"])
86
- for col in ["post", "body", "selftext"]:
87
- if col in df.columns:
88
- all_posts.extend(df[col].dropna().tolist())
89
- break
90
- except Exception as e:
91
- print(f" Skipping {fname}: {e}")
92
- print(f"Total raw posts loaded: {len(all_posts):,}")
93
- return all_posts
94
-
95
-
96
- # ── Main ──────────────────────────────────────────────────────────────────────
97
- def build_index():
98
- os.makedirs(os.path.dirname(INDEX_PATH), exist_ok=True)
99
-
100
- # Load and chunk
101
- tok = AutoTokenizer.from_pretrained("roberta-base")
102
- all_posts = load_reddit_posts(REDDIT_DIR)
103
-
104
- chunks = []
105
- for post in tqdm(all_posts, desc="Chunking"):
106
- cleaned = clean_text(str(post))
107
- if not cleaned:
108
- continue
109
- chunks.extend(chunk_text(cleaned, tok))
110
-
111
- print(f"Total chunks to encode: {len(chunks):,}")
112
-
113
- # Encode — use GPU automatically if available
114
- encoder = SentenceTransformer(MODEL_NAME)
115
- print(f"Encoding on: {encoder.device}")
116
- embeddings = encoder.encode(
117
- chunks,
118
- batch_size=BATCH_SIZE,
119
- show_progress_bar=True,
120
- normalize_embeddings=True,
121
- convert_to_numpy=True,
122
- )
123
- embeddings = np.array(embeddings, dtype=np.float32)
124
- print(f"Embeddings shape: {embeddings.shape}")
125
-
126
- # Build FAISS index
127
- dim = embeddings.shape[1] # 768
128
- if len(chunks) > 100_000:
129
- quantizer = faiss.IndexFlatL2(dim)
130
- index = faiss.IndexIVFFlat(quantizer, dim, 100)
131
- print("Training IVFFlat index...")
132
- index.train(embeddings)
133
- else:
134
- index = faiss.IndexFlatL2(dim)
135
- index.add(embeddings)
136
- faiss.write_index(index, INDEX_PATH)
137
- print(f"FAISS index saved: {index.ntotal:,} vectors → {INDEX_PATH}")
138
-
139
- # SQLite sidecar
140
- conn = sqlite3.connect(DB_PATH)
141
- c = conn.cursor()
142
- c.execute("""CREATE TABLE IF NOT EXISTS chunks (
143
- id INTEGER PRIMARY KEY,
144
- text TEXT,
145
- emotion_label INTEGER DEFAULT -1,
146
- safety_score REAL DEFAULT 0.7,
147
- source TEXT
148
- )""")
149
- # Insert in batches to avoid memory spike
150
- ISERT_BATCH = 10_000
151
- for i in range(0, len(chunks), ISERT_BATCH):
152
- batch = chunks[i:i+ISERT_BATCH]
153
- c.executemany(
154
- "INSERT OR REPLACE INTO chunks VALUES (?,?,?,?,?)",
155
- [(i+j, text, -1, 0.7, "reddit") for j, text in enumerate(batch)]
156
- )
157
- conn.commit()
158
- conn.close()
159
- print(f"SQLite DB saved: {len(chunks):,} rows → {DB_PATH}")
160
- print("Done. Download faiss_flat.index and metadata.db from Drive.")
161
-
162
-
163
- if __name__ == "__main__":
164
- build_index()