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

Day 3: FAISS index builder notebook for Colab A100

Browse files
notebooks/COLAB_FAISS_INSTRUCTIONS.md ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # FAISS Index Build β€” Colab Instructions
2
+
3
+ ## What this does
4
+ Encodes all Reddit Mental Health posts into 768-dim vectors and builds a FAISS index.
5
+ Expected time on A100: 30-60 minutes. Do NOT run locally on RTX 3060 β€” it would take 6-12 hours.
6
+
7
+ ## Steps
8
+
9
+ 1. Upload `data/raw/reddit_mental_health/` (108 CSV files, ~3.1GB) to Google Drive at:
10
+ `My Drive/empathrag/data/raw/reddit_mental_health/`
11
+
12
+ 2. In `colab_build_faiss_index.py`, uncomment the Cell 2 block (Drive mount + path config)
13
+ and comment out the local path config block at the top of Cell 3.
14
+
15
+ 3. Open Colab β†’ Runtime β†’ Change runtime type β†’ A100 GPU
16
+
17
+ 4. Run all cells in order.
18
+
19
+ 5. When complete, download from Drive:
20
+ - `My Drive/empathrag/data/indexes/faiss_flat.index`
21
+ - `My Drive/empathrag/data/indexes/metadata.db`
22
+ Place them in your local `data/indexes/` folder.
23
+
24
+ ## Expected output
25
+ - Total chunks: ~1-5 million (depends on corpus)
26
+ - FAISS index: IndexIVFFlat if >100K chunks, IndexFlatL2 if smaller
27
+ - SQLite DB: same number of rows as chunks, emotion_label=-1 (filled on Day 10)
notebooks/colab_build_faiss_index.py ADDED
@@ -0,0 +1,164 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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()