# ========================= # 0. REPRODUCIBILITY # ========================= import torch, numpy as np, random torch.manual_seed(42) np.random.seed(42) random.seed(42) # ========================= # 1. SAFE DOWNLOAD FUNCTION (FIXED) # ========================= import os, requests, zipfile, time def safe_download(url, path): if os.path.exists(path): print(f"{path} already exists, skipping download.") return for i in range(5): try: print(f"Downloading (attempt {i+1})...") r = requests.get(url, stream=True, timeout=60) with open(path, "wb") as f: for chunk in r.iter_content(8192): if chunk: f.write(chunk) print("Download complete") return except Exception as e: print("Retrying due to:", e) time.sleep(5) raise Exception("Download failed after retries") # ========================= # 2. LOAD LABELS # ========================= import pandas as pd def load_split(csv_file): df = pd.read_csv(csv_file) df.columns = df.columns.str.strip() split = {} for _, row in df.iterrows(): try: pid = str(int(row["Participant_ID"])) split[pid] = int(row["PHQ8_Binary"]) except: continue return split train_labels = load_split("train_split_Depression_AVEC2017.csv") dev_labels = load_split("dev_split_Depression_AVEC2017.csv") ALL_REQUIRED_IDS = set(list(train_labels.keys()) + list(dev_labels.keys())) print("Total required participants:", len(ALL_REQUIRED_IDS)) # ========================= # 3. SELECTIVE EXTRACTION # ========================= def extract_needed(zip_path): with zipfile.ZipFile(zip_path, "r") as zip_ref: for file in zip_ref.namelist(): parts = file.split("/") if len(parts) < 2: continue folder = parts[1] if parts[0].startswith("DAIC") else parts[0] if any(folder.startswith(pid + "_") for pid in ALL_REQUIRED_IDS): zip_ref.extract(file, "data") # ========================= # 4. DOWNLOAD + EXTRACT (SAFE) # ========================= urls = [ "https://huggingface.co/datasets/ananyakarn/DAIC_WOZ_Data/resolve/main/DAIC_WOZ_Data.zip", "https://huggingface.co/datasets/ananyakarn/DAIC_WOZ_Data/resolve/main/DAIC_WOZ_Data-2.zip" ] os.makedirs("data", exist_ok=True) # 👉 skip extraction if already done if len(os.listdir("data")) == 0: for i, url in enumerate(urls): zip_path = f"temp_{i}.zip" safe_download(url, zip_path) print(f"Extracting dataset {i+1}...") extract_needed(zip_path) os.remove(zip_path) else: print("Dataset already extracted. Skipping download.") # ========================= # 5. GET PARTICIPANTS # ========================= def get_all_paths(): paths = [] for root, dirs, _ in os.walk("data"): for d in dirs: if "_P" in d or "_C" in d: paths.append(os.path.join(root, d)) return paths ALL_PATHS = get_all_paths() print("Extracted participants:", len(ALL_PATHS)) # ========================= # 6. LIBRARIES # ========================= import librosa import torch.nn as nn from tqdm import tqdm from transformers import AutoTokenizer, AutoModel from sklearn.metrics import accuracy_score, f1_score from sklearn.preprocessing import StandardScaler device = torch.device("cuda" if torch.cuda.is_available() else "cpu") # ========================= # 7. TEXT MODEL # ========================= tokenizer = AutoTokenizer.from_pretrained("distilbert-base-uncased") bert = AutoModel.from_pretrained("distilbert-base-uncased").to(device) bert.eval() # ========================= # 8. FEATURE FUNCTIONS # ========================= def load_text(folder): try: file = [f for f in os.listdir(folder) if "TRANSCRIPT" in f][0] df = pd.read_csv(os.path.join(folder, file)) return " ".join(df.iloc[:, -1].astype(str)) except: return "" def get_text_embedding(text): if text == "": return np.zeros(768) inputs = tokenizer(text, return_tensors="pt", truncation=True, padding=True, max_length=256).to(device) with torch.no_grad(): out = bert(**inputs) return out.last_hidden_state.mean(dim=1).squeeze().cpu().numpy() def get_audio_features(folder): try: file = [f for f in os.listdir(folder) if f.endswith("_AUDIO.wav")][0] y, sr = librosa.load(os.path.join(folder, file), sr=16000) mfcc = librosa.feature.mfcc(y=y, sr=sr, n_mfcc=40) return np.mean(mfcc.T, axis=0) except: return np.zeros(40) def get_visual_features(folder): feats = [] for key in ["AUs", "pose", "gaze"]: try: file = [f for f in os.listdir(folder) if key in f][0] df = pd.read_csv(os.path.join(folder, file)) df = df.select_dtypes(include=[np.number]) df.replace(-100, np.nan, inplace=True) df.fillna(0, inplace=True) if df.shape[1] == 0: feats.append(np.zeros(20)) continue feats.append(np.concatenate([df.mean().values, df.std().values])) except: feats.append(np.zeros(20)) return np.concatenate(feats) # ========================= # 9. BUILD DATA # ========================= def build(label_dict): Xt, Xa, Xv, y = [], [], [], [] for path in tqdm(ALL_PATHS): pid = os.path.basename(path).split("_")[0] if pid not in label_dict: continue Xt.append(get_text_embedding(load_text(path))) Xa.append(get_audio_features(path)) Xv.append(get_visual_features(path)) y.append(label_dict[pid]) return np.array(Xt), np.array(Xa), np.array(Xv), np.array(y) Xt, Xa, Xv, y_train = build(train_labels) Xt_test, Xa_test, Xv_test, y_test = build(dev_labels) if len(y_train) == 0: raise Exception("No training data found!") print("Train size:", len(y_train)) print("Test size:", len(y_test)) # ========================= # 10. NORMALIZE # ========================= sc_t, sc_a, sc_v = StandardScaler(), StandardScaler(), StandardScaler() Xt = sc_t.fit_transform(Xt) Xa = sc_a.fit_transform(Xa) Xv = sc_v.fit_transform(Xv) Xt_test = sc_t.transform(Xt_test) Xa_test = sc_a.transform(Xa_test) Xv_test = sc_v.transform(Xv_test) # ========================= # 🔥 RANDOM FOREST (SAFE ADD) # ========================= from sklearn.ensemble import RandomForestClassifier # IMPORTANT: use numpy arrays BEFORE tensor conversion # (at this point Xt, Xa, Xv are still numpy) # Optional: reduce dimensionality for stability Xt_rf = Xt[:, :128] # reduce text features Xv_rf = Xv[:, :50] # reduce visual features Xt_test_rf = Xt_test[:, :128] Xv_test_rf = Xv_test[:, :50] # Combine features X_train_rf = np.concatenate([Xt_rf, Xa, Xv_rf], axis=1) X_test_rf = np.concatenate([Xt_test_rf, Xa_test, Xv_test_rf], axis=1) # Model rf = RandomForestClassifier( n_estimators=200, max_depth=5, random_state=42 ) print("\nTraining Random Forest...") rf.fit(X_train_rf, y_train) # Predictions rf_preds = rf.predict(X_test_rf) rf_acc = accuracy_score(y_test, rf_preds) rf_f1 = f1_score(y_test, rf_preds) # ========================= # 11. MODELS # ========================= import torch.nn as nn class Model(nn.Module): def __init__(self, vdim): super().__init__() self.t = nn.Sequential(nn.Linear(768,128), nn.ReLU()) self.a = nn.Sequential(nn.Linear(40,32), nn.ReLU()) self.v = nn.Sequential(nn.Linear(vdim,64), nn.ReLU()) self.f = nn.Sequential( nn.Linear(224,64), nn.ReLU(), nn.Dropout(0.3), nn.Linear(64,1) ) def forward(self, t, a, v): return self.f(torch.cat([self.t(t), self.a(a), self.v(v)], dim=1)) class AttentionFusionModel(nn.Module): def __init__(self, vdim): super().__init__() self.t = nn.Sequential(nn.Linear(768,128), nn.ReLU()) self.a = nn.Sequential(nn.Linear(40,32), nn.ReLU()) self.v = nn.Sequential(nn.Linear(vdim,64), nn.ReLU()) self.attn = nn.Sequential( nn.Linear(224,64), nn.Tanh(), nn.Linear(64,3) ) self.f = nn.Sequential( nn.Linear(224,64), nn.ReLU(), nn.Dropout(0.3), nn.Linear(64,1) ) def forward(self, t, a, v): t_feat = self.t(t) a_feat = self.a(a) v_feat = self.v(v) combined = torch.cat([t_feat, a_feat, v_feat], dim=1) weights = torch.softmax(self.attn(combined), dim=1) fused = torch.cat([ weights[:,0:1] * t_feat, weights[:,1:2] * a_feat, weights[:,2:3] * v_feat ], dim=1) return self.f(fused) # ========================= # 12. CONVERT TO TENSORS # ========================= Xt = torch.tensor(Xt, dtype=torch.float32).to(device) Xa = torch.tensor(Xa, dtype=torch.float32).to(device) Xv = torch.tensor(Xv, dtype=torch.float32).to(device) yt = torch.tensor(y_train, dtype=torch.float32).to(device) Xt_test = torch.tensor(Xt_test, dtype=torch.float32).to(device) Xa_test = torch.tensor(Xa_test, dtype=torch.float32).to(device) Xv_test = torch.tensor(Xv_test, dtype=torch.float32).to(device) # ========================= # 13. TRAIN BASELINE MODEL # ========================= baseline_model = Model(Xv.shape[1]).to(device) opt1 = torch.optim.Adam(baseline_model.parameters(), lr=1e-4) loss_fn = nn.BCEWithLogitsLoss() print("\nTraining Baseline Model...") for e in range(5): # 🔥 reduced epochs (important) baseline_model.train() opt1.zero_grad() outputs = baseline_model(Xt, Xa, Xv).squeeze() loss = loss_fn(outputs, yt) loss.backward() opt1.step() print(f"Epoch {e+1}, Loss: {loss.item():.4f}") # ========================= # 14. TRAIN ATTENTION MODEL # ========================= attention_model = AttentionFusionModel(Xv.shape[1]).to(device) opt2 = torch.optim.AdamW(attention_model.parameters(), lr=1e-4) loss_fn_attn = nn.BCEWithLogitsLoss() print("\nTraining Attention Model...") for e in range(5): # 🔥 reduced epochs attention_model.train() opt2.zero_grad() outputs = attention_model(Xt, Xa, Xv).squeeze() loss = loss_fn_attn(outputs, yt) loss.backward() opt2.step() print(f"Epoch {e+1}, Loss: {loss.item():.4f}") # ========================= # 15. EVALUATION # ========================= from sklearn.metrics import accuracy_score, f1_score baseline_model.eval() attention_model.eval() with torch.no_grad(): pred_baseline = (torch.sigmoid( baseline_model(Xt_test, Xa_test, Xv_test).squeeze() ) > 0.5).int().cpu().numpy() pred_attention = (torch.sigmoid( attention_model(Xt_test, Xa_test, Xv_test).squeeze() ) > 0.5).int().cpu().numpy() print("\n===== MODEL COMPARISON =====") print("\nRandom Forest:") print("Accuracy:", rf_acc) print("F1 Score:", rf_f1) print("\nBaseline Model:") print("Accuracy:", accuracy_score(y_test, pred_baseline)) print("F1 Score:", f1_score(y_test, pred_baseline)) print("\nAttention Model:") print("Accuracy:", accuracy_score(y_test, pred_attention)) print("F1 Score:", f1_score(y_test, pred_attention))