{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[{"sourceType":"competition","sourceId":41875,"databundleVersionId":5521661},{"sourceType":"datasetVersion","sourceId":16048445,"datasetId":10292050,"databundleVersionId":17015471},{"sourceType":"kernelVersion","sourceId":134063325},{"sourceType":"kernelVersion","sourceId":281424068},{"sourceType":"kernelVersion","sourceId":286397083},{"sourceType":"kernelVersion","sourceId":294261447},{"sourceType":"kernelVersion","sourceId":312548759},{"sourceType":"kernelVersion","sourceId":312596935},{"sourceType":"kernelVersion","sourceId":312860814},{"sourceType":"kernelVersion","sourceId":313134450},{"sourceType":"kernelVersion","sourceId":314249659},{"sourceType":"kernelVersion","sourceId":314988882},{"sourceType":"kernelVersion","sourceId":315396838}],"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# This Python 3 environment comes with many helpful analytics libraries installed\n# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python\n# For example, here's several helpful packages to load\n\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\n\n# Input data files are available in the read-only \"../input/\" directory\n# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory\n\nimport os\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    for filename in filenames:\n        print(os.path.join(dirname, filename))\n\n# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using \"Save & Run All\" \n# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-05-02T03:37:02.122082Z","iopub.execute_input":"2026-05-02T03:37:02.122355Z","iopub.status.idle":"2026-05-02T03:37:03.162573Z","shell.execute_reply.started":"2026-05-02T03:37:02.122325Z","shell.execute_reply":"2026-05-02T03:37:03.161656Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!pip install biopython\n!pip install obonet","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-02T03:37:03.164276Z","iopub.execute_input":"2026-05-02T03:37:03.165007Z","iopub.status.idle":"2026-05-02T03:37:12.124302Z","shell.execute_reply.started":"2026-05-02T03:37:03.164982Z","shell.execute_reply":"2026-05-02T03:37:12.123308Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"'''\ndef prepare_foldseek_matrix(foldseek_df, train_ids, train_terms_df, mlb):\n    \"\"\"\n    foldseek_df: 欄位 ['query', 'target', 'bits']\n    train_ids: 蛋白質 ID 列表 \n    train_terms_df: 訓練集的 GO 標籤\n    mlb: 預先 fit 好的 MultiLabelBinarizer (1500維)\n    \"\"\"\n    # 1. 排除 Self-match\n    foldseek_df = foldseek_df[foldseek_df['query'] != foldseek_df['target']]\n    \n    # 2. 取得 Target 的 GO 向量\n    target_go_dict = train_terms_df.groupby('EntryID')['term'].apply(list).to_dict()\n    \n    # 3. 計算加權標籤向量\n    num_proteins = len(train_ids)\n    num_labels = len(mlb.classes_)\n    fs_go_matrix = np.zeros((num_proteins, num_labels), dtype=np.float32)\n    fs_score_stats = np.zeros((num_proteins, 2), dtype=np.float32) # [mean_score, max_score]\n\n    # 建立 ID 到索引的映射\n    id_to_idx = {id: i for i, id in enumerate(train_ids['EntryID'])}\n\n    for query, group in foldseek_df.groupby('query'):\n        if query not in id_to_idx: continue\n        idx = id_to_idx[query]\n        \n        weights = group['bits'].values\n        targets = group['target'].values\n        \n        weighted_vector = np.zeros(num_labels)\n        for t, w in zip(targets, weights):\n            if t in target_go_dict:\n                # 將該 target 的標籤轉為向量並加權\n                target_vec = mlb.transform([target_go_dict[t]])[0]\n                weighted_vector += target_vec * w\n        \n        # 歸一化或取平均\n        if weights.sum() > 0:\n            fs_go_matrix[idx] = weighted_vector / weights.sum()\n            fs_score_stats[idx] = [weights.mean(), weights.max()]\n\n    # 拼接成最終 FoldSeek 特徵 (1500維標籤先驗 + 2維分數統計)\n    return np.concatenate([fs_go_matrix, fs_score_stats], axis=1)\n'''","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-16T11:43:49.609033Z","iopub.status.idle":"2026-04-16T11:43:49.609360Z","shell.execute_reply.started":"2026-04-16T11:43:49.609210Z","shell.execute_reply":"2026-04-16T11:43:49.609230Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"'''\n# 1. 調整 FoldSeek 的維度，讓它變成 (N, Channels, 1500)\n# 假設目前只有 FoldSeek 一種先驗，所以 Channel 數為 1。如果有 IEA，Channel 就變 2。\ndef prepare_prior_features(foldseek_matrix_1500):\n    \"\"\"將原本 (N, 1500) 的矩陣，增加一個 Channel 維度變成 (N, 1, 1500)\"\"\"\n    # 捨棄那 2 維的分數統計，只保留 1500 維的 GO prior\n    prior_matrix = foldseek_matrix_1500[:, :1500]\n    return np.expand_dims(prior_matrix, axis=1).astype(np.float32)\n\nclass CAFAFullDataset(torch.utils.data.Dataset):\n    def __init__(self, esm_path, t5_path, taxon_arr, prior_arr, labels_arr=None, mask_arr=None):\n        self.esm = np.load(esm_path, mmap_mode='r')\n        self.t5 = np.load(t5_path, mmap_mode='r')\n        self.taxon = taxon_arr\n        self.prior = prior_arr # 形狀應為 (N, Channels, 1500)\n        self.labels = labels_arr\n        self.mask = mask_arr\n\n    def __len__(self): \n        return len(self.esm)\n\n    def __getitem__(self, idx):\n        # 序列特徵\n        x_esm = torch.from_numpy(self.esm[idx].copy())\n        x_t5 = torch.from_numpy(self.t5[idx].copy())\n        \n        # 輔助特徵\n        x_tax = torch.from_numpy(self.taxon[idx])\n        \n        # 先驗特徵 (給 1D-CNN 用的)\n        x_prior = torch.from_numpy(self.prior[idx])\n        \n        if self.labels is not None and self.mask is not None:\n            y = torch.tensor(self.labels[idx], dtype=torch.float32)\n            m = torch.tensor(self.mask[idx], dtype=torch.float32)\n            # 注意這裡回傳了 6 個變數，完美對接後面的 Training Loop\n            return x_esm, x_t5, x_tax, x_prior, y, m\n            \n        return x_esm, x_t5, x_tax, x_prior\n'''","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-16T11:43:49.611738Z","iopub.status.idle":"2026-04-16T11:43:49.612222Z","shell.execute_reply.started":"2026-04-16T11:43:49.612006Z","shell.execute_reply":"2026-04-16T11:43:49.612029Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport torch.nn as nn\nimport numpy as np\nimport pandas as pd\nimport json\nimport scipy.sparse as sp\nimport collections\nfrom torch.cuda.amp import autocast, GradScaler\nfrom torch.optim.lr_scheduler import OneCycleLR\nfrom torch.utils.data import Dataset, DataLoader, Subset\nfrom sklearn.model_selection import StratifiedKFold\nimport gc\nfrom tqdm import tqdm","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-25T06:21:13.646101Z","iopub.execute_input":"2026-04-25T06:21:13.646477Z","iopub.status.idle":"2026-04-25T06:21:17.791303Z","shell.execute_reply.started":"2026-04-25T06:21:13.646442Z","shell.execute_reply":"2026-04-25T06:21:17.790314Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import numpy as np\nimport pandas as pd\nfrom Bio import SeqIO\n\nprint(\"=== 特徵對齊檢查 ===\")\n\n# 檢查 FASTA 原始檔\nfasta_path = \"/kaggle/input/competitions/cafa-5-protein-function-prediction/Train/train_sequences.fasta\"\nfasta_ids = [seq.id for seq in SeqIO.parse(fasta_path, \"fasta\")]\nprint(f\"1️⃣ 原始 FASTA 序列總數: {len(fasta_ids)}\")\n\ntry:\n    train_ids_df = pd.read_csv(\"/kaggle/input/notebooks/t8101349/cafa-5-protein-function-prediction-embeddings/train_ids.csv\")\n    print(f\"2️⃣ 實際提取的 ID 總數 (train_ids.csv): {len(train_ids_df)}\")\nexcept FileNotFoundError:\n    print(\"⚠️ 找不到 train_ids.csv，請確認你當初提取特徵時是否有存下 ID 列表！\")\n\ntry:\n    # mmap_mode='r' 超級重要：不載入記憶體，只讀形狀！\n    esm_matrix = np.load(\"/kaggle/input/notebooks/t8101349/cafa-5-protein-function-prediction-embeddings/esm_embeddings_full.npy\", mmap_mode='r')\n    print(f\"3️⃣ ESM-2 特徵矩陣形狀: {esm_matrix.shape}\")\nexcept FileNotFoundError:\n    print(\"⚠️ 找不到 esm_embeddings_full.npy\")\n\ntry:\n    bert_matrix = np.load(\"/kaggle/input/notebooks/t8101349/cafa-5-protein-function-prediction-embeddings1-2/protbert_embeddings_full.npy\", mmap_mode='r')\n    print(f\"4️⃣ ProtBERT 特徵矩陣形狀: {bert_matrix.shape}\")\nexcept FileNotFoundError:\n    print(\"⚠️ 找不到 protbert_embeddings_full.npy\")\n\ntry:\n    if len(train_ids_df) == esm_matrix.shape[0] == bert_matrix.shape[0]:\n        print(\"完美對齊！你的 ESM-2 和 ProtBERT 矩陣列數與 train_ids.csv 完全一致。\")\n    else:\n        print(\"❌ 警告：數量不一致！請勿直接訓練，會發生特徵錯位！\")\n        print(f\"差異 -> IDs: {len(train_ids_df)}, ESM: {esm_matrix.shape[0]}, BERT: {bert_matrix.shape[0]}\")\nexcept Exception as e:\n    print(\"診斷未完成，請先確保上述檔案皆存在。\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\n\ndef align_all_features_to_anchor(base_path, custom_labels_path):\n    print(\"align_all_features...\")\n    \n    # ==========================================\n    # 1. 確立絕對座標系 (The Anchor)\n    # ==========================================\n    # 讀取 train_ids.csv\n    train_ids_df = pd.read_csv(f\"/kaggle/input/notebooks/t8101349/cafa-5-protein-function-prediction-embeddings/train_ids.csv\")\n    anchor_ids = train_ids_df['EntryID'].tolist()\n    N_train = len(anchor_ids)\n\n    print(\"掃描 Train FASTA，驗證特徵排序絕對一致性...\")\n    fasta_path = f\"/kaggle/input/competitions/cafa-5-protein-function-prediction/Train/train_sequences.fasta\"\n    \n    with open(fasta_path, \"r\") as f:\n        idx = 0\n        for line in f:\n            if line.startswith(\">\"):\n                # 訓練集的 header 通常只有 ID\n                pid = line.strip()[1:].split()[0] \n                \n                # 防呆確認\n                if pid != anchor_ids[idx]:\n                    raise ValueError(f\"❌ 致命錯位！FASTA 讀到 {pid}，但 CSV 第 {idx} 筆是 {anchor_ids[idx]}\")\n                \n                idx += 1\n                \n    print(f\"   ✅ 完美過關！FASTA 與 CSV 的 {idx} 筆資料 1 對 1 絕對吻合。\")\n    \n    \n    # 建立 {ProteinID: 絕對行號} 的查表\n    anchor_dict = {pid: i for i, pid in enumerate(anchor_ids)}\n    print(f\"基準座標確立：共 {N_train} 筆蛋白質 (與 ESM/PB Embedding 完美對齊)\")\n\n    # ==========================================\n    # 2. 對齊 Ground Truth (Y_true)\n    # ==========================================\n    print(\"正在對齊 Ground Truth (train_terms)...\")\n    with open(f\"/kaggle/input/notebooks/mafiosoquasar/ems-protbert-model-ensemble/outputs_labels/maps.json\", \"r\") as f:\n        go_maps = json.load(f)\n    \n    # 計算總標籤數與對應的欄位索引 (MF -> BP -> CC)\n    ontologies = ['MF', 'BP', 'CC']\n    term_to_col = {}\n    current_col = 0\n    for ont in ontologies:\n        for col_idx_str, term in go_maps[ont].items():\n            term_to_col[term] = current_col + int(col_idx_str)\n        current_col += len(go_maps[ont])\n    total_labels = current_col\n    \n    # 建立空的稀疏矩陣\n    y_true_aligned = sp.lil_matrix((N_train, total_labels), dtype=np.float32)\n    train_terms = pd.read_csv(f\"/kaggle/input/competitions/cafa-5-protein-function-prediction/Train/train_terms.tsv\", sep=\"\\t\")\n    \n    hit_count = 0\n    for _, row in tqdm(train_terms.iterrows(), total=len(train_terms), desc=\"Y_true\"):\n        pid, term = row['EntryID'], row['term']\n        # 只有在 anchor (train_ids) 裡面，且是我們要預測的 term 才填入 1\n        if pid in anchor_dict and term in term_to_col:\n            y_true_aligned[anchor_dict[pid], term_to_col[term]] = 1.0\n            hit_count += 1\n            \n    y_true_aligned = y_true_aligned.tocsr()\n    print(f\"   ✅ Y_true 填入完成！形狀: {y_true_aligned.shape}, 命中有效標籤數: {hit_count}\")\n\n    # ==========================================\n    # 3. 對齊 Taxonomy 特徵\n    # ==========================================\n    print(\"正在對齊 Taxonomy 特徵...\")\n    tax_df = pd.read_csv(f\"/kaggle/input/competitions/cafa-5-protein-function-prediction/Train/train_taxonomy.tsv\", sep=\"\\t\")\n    test_tax_df = pd.read_csv(f\"/kaggle/input/competitions/cafa-5-protein-function-prediction/Test (Targets)/testsuperset-taxon-list.tsv\", sep=\"\\t\", encoding=\"ISO-8859-1\")\n    \n    # 建立 1~90 的 Index 對應表，0 給 Others\n    taxon_to_idx = {tax_id: i + 1 for i, tax_id in enumerate(test_tax_df['ID'])}\n    \n    taxon_aligned = np.zeros(N_train, dtype=np.int64)\n    tax_lookup = dict(zip(tax_df['EntryID'], tax_df['taxonomyID']))\n    \n    for pid, row_idx in anchor_dict.items():\n        tax_id = tax_lookup.get(pid, -1)\n        taxon_aligned[row_idx] = taxon_to_idx.get(tax_id, 0)\n        \n    print(f\" ✅ Taxon Array 對齊完成！形狀: {taxon_aligned.shape}\")\n\n    # ==========================================\n    # 4. 對齊 Non-Experimental 特徵 (最混亂的那個)\n    # ==========================================\n    print(\"正在對齊 Non-Exp 先驗特徵 (擷取交集)...\")\n    with open(f\"/kaggle/input/notebooks/t8101349/cafa-5-protein-function-prediction-embeddings2/protein_mapping.json\", \"r\") as f:\n        non_exp_mapping = json.load(f) # {ProteinID: 巨型NPZ裡的行號}\n        \n    # 讀取原本龐大且包含 train/test 的矩陣\n    pos_combined = sp.hstack([\n        sp.load_npz(f\"{custom_labels_path}/labels_MF.npz\"),\n        sp.load_npz(f\"{custom_labels_path}/labels_BP.npz\"),\n        sp.load_npz(f\"{custom_labels_path}/labels_CC.npz\")\n    ]).tocsr()\n    \n    neg_combined = sp.hstack([\n        sp.load_npz(f\"{custom_labels_path}/labels_MF_neg.npz\"),\n        sp.load_npz(f\"{custom_labels_path}/labels_BP_neg.npz\"),\n        sp.load_npz(f\"{custom_labels_path}/labels_CC_neg.npz\")\n    ]).tocsr()\n\n    non_exp_pos_aligned = sp.lil_matrix((N_train, total_labels), dtype=np.float32)\n    non_exp_neg_aligned = sp.lil_matrix((N_train, total_labels), dtype=np.float32)\n    \n    found_non_exp_count = 0\n    for pid, anchor_idx in tqdm(anchor_dict.items(), desc=\"Non-Exp\"):\n        if pid in non_exp_mapping:\n            old_row_idx = non_exp_mapping[pid]\n            \n            non_exp_pos_aligned[anchor_idx] = pos_combined[old_row_idx]\n            non_exp_neg_aligned[anchor_idx] = neg_combined[old_row_idx]\n            found_non_exp_count += 1\n        \n\n    print(f\"   ✅ Non-Exp 對齊完成！形狀: {non_exp_pos_aligned.shape},{non_exp_neg_aligned.shape}\")\n    print(f\"   ℹ️ 共有 {found_non_exp_count} / {N_train} 個蛋白質具備 Non-Exp 先驗特徵。\")\n\n    # ==========================================\n    # 5. 儲存大一統檔案\n    # ==========================================\n    print(\"正在儲存對齊後的標準化資料...\")\n    sp.save_npz(\"aligned_Y_true.npz\", y_true_aligned)\n    np.save(\"aligned_taxon.npy\", taxon_aligned)\n    \n    sp.save_npz(\"aligned_non_exp_pos.npz\", non_exp_pos_aligned.tocsr())\n    sp.save_npz(\"aligned_non_exp_neg.npz\", non_exp_neg_aligned.tocsr())\n    \n    print(\"所有矩陣的第 i 列都完美對應 train_ids.csv 的第 i 個蛋白質！\")\n\n\n\nalign_all_features_to_anchor(\n    base_path=\"/kaggle/input/notebooks/t8101349/cafa-5-protein-function-prediction-embeddings\",\n    custom_labels_path=\"/kaggle/input/notebooks/t8101349/cafa-5-protein-function-prediction-embeddings2\"\n)\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ==========================================\n# 資源與權重準備 (Data & Weights Preparation)\n# ==========================================\n\n# 模型是 BP, CC, MF 全維度輸出，IA 權重也必須是「三合一」且加入 Offset 對齊。\ndef get_combined_ia_weights_tensor(ia_df, go_maps, ontologies=['MF', 'BP', 'CC']):\n    \"\"\"\n    讀取 IA.txt 與 maps.json，建構與模型輸出維度完美對齊的 IA 權重 Tensor。\n    \n    Args:\n        ia_file_path (str): 官方 IA.txt 的路徑\n        maps_file_path (str): 神級對照表 maps.json 的路徑\n        ontology (str): 要轉換的本體論分類，可選 \"MF\", \"BP\", \"CC\"\n        \n    Returns:\n        torch.Tensor: 形狀為 (num_labels,) 的 1D 權重 Tensor\n    \"\"\"\n    \"\"\"建構嚴格對齊全維度 (MF+BP+CC) 的 IA 權重 Tensor\"\"\"\n    ia_dict = dict(zip(ia_df['term'], ia_df['ia']))\n    total_labels = sum(len(go_maps[ont]) for ont in ontologies)\n    ia_tensor_list = [0.0] * total_labels\n    \n    current_offset = 0\n    missing_count = 0\n    zero_ia_count = 0\n    for ont in ontologies:\n        target_map = go_maps[ont]\n        term_to_col = {v: int(k) for k, v in target_map.items()}\n        \n        for term, col_idx in term_to_col.items():\n            absolute_col_idx = current_offset + col_idx\n            \n            if term in ia_dict:\n                \n                ia_tensor_list[absolute_col_idx] = ia_dict[term]\n                if ia_dict[term] == 0.0:\n                    zero_ia_count += 1\n                    \n            else:\n                missing_count += 1\n        current_offset += len(target_map)\n        \n    ia_weights_tensor = torch.tensor(ia_tensor_list, dtype=torch.float32)\n    \n    print(f\"✅ 全維度 IA 權重對齊完成！Shape: {ia_weights_tensor.shape}\")\n    print(f\" 統計情報：\")\n    print(f\"   ├─ 官方指定 IA 為 0 的節點數: {zero_ia_count}\")\n    print(f\"   └─ 官方檔案缺失的節點數: {missing_count} (已一併棄用)\")\n\n    return ia_weights_tensor\n\n\n\nIA_TXT_PATH = \"/kaggle/input/competitions/cafa-5-protein-function-prediction/IA.txt\"\n\nia_df = pd.read_csv(IA_TXT_PATH, sep='\\t', header=None, names=['term', 'ia'])\nprint(f\"成功載入 IA 權重表，共 {len(ia_df)} 筆資料\")\n\nwith open(f\"/kaggle/input/notebooks/mafiosoquasar/ems-protbert-model-ensemble/outputs_labels/maps.json\", 'r') as f:\n    go_maps = json.load(f) \nia_tensor = get_combined_ia_weights_tensor(ia_df, go_maps, ontologies=['MF', 'BP', 'CC'])\n\ntorch.save(ia_tensor.clone().detach().float(), \"ia_weights_combined.pt\")\nprint(\"✅ IA 權重已儲存為 ia_weights_combined.pt\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ==========================================\n# 資源與權重準備 (Data Preparation)\n# ==========================================\ndef load_aligned_cafa_resources():\n    print(\"啟動 CAFA 5 資源對齊載入...\")\n    \n    # ---------------------------------------------------------\n    # 1. 載入對照表與圖論結構 (維持原樣)\n    # ---------------------------------------------------------\n    print(\"載入 JSON 對照表...\")\n    with open(\"/kaggle/input/notebooks/mafiosoquasar/ems-protbert-model-ensemble/outputs_labels/maps.json\", 'r') as f:\n        go_maps = json.load(f)      \n    with open(\"/kaggle/input/notebooks/t8101349/cafa-5-protein-function-prediction-embeddings2/ancestors.json\", 'r') as f:\n        ancestors = json.load(f)    \n        \n    # ---------------------------------------------------------\n    # 2. 載入 序列特徵 (The Anchor 絕對座標基準)\n    # ---------------------------------------------------------\n    print(\"載入序列特徵 (ESM & ProtBERT)...\")\n    esm_emb = np.load(\"/kaggle/input/notebooks/t8101349/cafa-5-protein-function-prediction-embeddings/esm_embeddings_full.npy\", mmap_mode='r')\n    pb_emb = np.load(\"/kaggle/input/notebooks/t8101349/cafa-5-protein-function-prediction-embeddings1-2/protbert_embeddings_full.npy\", mmap_mode='r')\n    \n    # ---------------------------------------------------------\n    # 3. 載入 對齊後的 Ground Truth 與 Taxonomy\n    # ---------------------------------------------------------\n    print(\"載入對齊後的 Y_true 與 Taxonomy...\")\n    ALIGNED_DIR = \"./\" \n    \n    y_true_aligned = sp.load_npz(f\"{ALIGNED_DIR}aligned_Y_true.npz\")\n    taxon_aligned = np.load(f\"{ALIGNED_DIR}aligned_taxon.npy\", mmap_mode='r')\n    \n    # ---------------------------------------------------------\n    # 4. 載入 對齊後的 Non-Exp 先驗特徵 (雙通道稀疏矩陣)\n    # ---------------------------------------------------------\n    print(\"載入對齊後的 Non-Exp 雙通道特徵...\")\n    non_exp_pos = sp.load_npz(f\"{ALIGNED_DIR}aligned_non_exp_pos.npz\")\n    non_exp_neg = sp.load_npz(f\"{ALIGNED_DIR}aligned_non_exp_neg.npz\")\n    \n    # ---------------------------------------------------------\n    # 5. 載入 IA 權重 (直接讀取之前存好的 .pt 檔最快)\n    # ---------------------------------------------------------\n    print(\"載入 IA 權重 Tensor...\")\n    ia_weights_tensor = torch.load(f\"{ALIGNED_DIR}ia_weights_combined.pt\")\n    \n    \n    # ---------------------------------------------------------\n    # 6. 終極防呆斷言 (Assertions)\n    # ---------------------------------------------------------\n    print(\"執行維度安全檢查...\")\n    N_proteins = esm_emb.shape[0]\n    total_labels = sum(len(go_maps[ont]) for ont in ['MF', 'BP', 'CC'])\n    \n    # 檢查 Row (蛋白質數量) 是否完全一致\n    assert pb_emb.shape[0] == N_proteins, \"❌ ESM 與 PB 數量不一致！\"\n    assert y_true_aligned.shape[0] == N_proteins, \"❌ Y_true 數量不一致！\"\n    assert taxon_aligned.shape[0] == N_proteins, \"❌ Taxon 數量不一致！\"\n    assert non_exp_pos.shape[0] == N_proteins, \"❌ Non-Exp 數量不一致！\"\n    \n    # 檢查 Col (標籤數量) 是否與 IA 權重一致\n    assert y_true_aligned.shape[1] == ia_weights_tensor.shape[0], \"❌ 標籤矩陣與 IA 維度不一致！\"\n    assert y_true_aligned.shape[1] == total_labels, \"❌ 標籤矩陣總長度不等於 MF+BP+CC 的總和！\"\n\n    print(\"✅ 載入與對齊檢查完美通過！\")\n    \n    return go_maps, ancestors, esm_emb, pb_emb, y_true_aligned, taxon_aligned, non_exp_pos, non_exp_neg, ia_weights_tensor","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ==========================================\n# 資料前處理模組 (Data Preprocessing)\n# ==========================================\ndef prepare_taxon_array_top90(base_path, prot_mapping):\n    \"\"\"嚴格對齊測試集的 90 個 Taxonomy ID\"\"\"\n    print(\"執行 Taxonomy 截斷 (Top 90)...\")\n    tax_df = pd.read_csv(f\"/kaggle/input/competitions/cafa-5-protein-function-prediction/Train/train_taxonomy.tsv\", sep=\"\\t\")\n    test_taxon_df = pd.read_csv(f\"/kaggle/input/competitions/cafa-5-protein-function-prediction/Test (Targets)/testsuperset-taxon-list.tsv\", sep=\"\\t\", encoding=\"ISO-8859-1\")\n        \n    valid_taxons = test_taxon_df['ID'].tolist()\n    taxon_to_idx = {tax_id: i + 1 for i, tax_id in enumerate(valid_taxons)}\n    num_taxons = len(valid_taxons) + 1  # 總共 91 維\n    \n    taxon_arr = np.zeros(len(prot_mapping), dtype=np.int64)\n    tax_dict = dict(zip(tax_df['EntryID'], tax_df['taxonomyID']))\n    \n    for pid, row_idx in prot_mapping.items():\n        tax_id = tax_dict.get(pid, -1)\n        taxon_arr[row_idx] = taxon_to_idx.get(tax_id, 0)\n\n    print(f\"✅ Taxonomy Array 建立完成！)\")\n    return taxon_arr, num_taxons\n\n'''\ndef build_dual_mask_optimized(bp_mat, cc_mat, mf_mat, ancestors_dict, term_list):\n    \"\"\"建立實驗證據 Mask + 條件機率 NaN Mask \"\"\"\n    print(\"建立雙重遮罩 (Dual Mask)...\")\n    \n    # 保持 CSR 稀疏矩陣，絕對不要在這裡用 .toarray()！\n    full_labels_mat = sp.hstack([mf_mat, bp_mat, cc_mat]).tocsr()\n    num_proteins = full_labels_mat.shape[0]\n    total_labels = full_labels_mat.shape[1]\n    \n    # --- 1. 實驗證據 Mask ---\n    has_bp = np.array((bp_mat.sum(axis=1) > 0).astype(np.float32)).flatten()\n    has_cc = np.array((cc_mat.sum(axis=1) > 0).astype(np.float32)).flatten()\n    has_mf = np.array((mf_mat.sum(axis=1) > 0).astype(np.float32)).flatten()\n    \n    mask_mf = np.repeat(has_mf[:, np.newaxis], mf_mat.shape[1], axis=1)\n    mask_bp = np.repeat(has_bp[:, np.newaxis], bp_mat.shape[1], axis=1)\n    mask_cc = np.repeat(has_cc[:, np.newaxis], cc_mat.shape[1], axis=1)\n    exp_mask = np.concatenate([mask_mf, mask_bp, mask_cc], axis=1)\n\n    # --- 2. 條件機率 NaN Mask ---\n    print(\"計算條件機率 Mask (使用 CSC 稀疏切片防爆)...\")\n    \n    # 直接初始化一個形狀相同的 ones 矩陣，不依賴 toarray()\n    cond_mask = np.ones((num_proteins, total_labels), dtype=np.float32)\n    term_to_idx = {term: i for i, term in enumerate(term_list)}\n    \n    # 轉成 CSC 格式。這對「直行 (Column)」的切片讀取速度最快！\n    labels_csc = full_labels_mat.tocsc()\n    \n    for child_term, idx in tqdm(term_to_idx.items(), desc=\"Cond Mask\"):\n        if child_term in ancestors_dict:\n            # 排除自己 (Self-loop)\n            parent_indices = [term_to_idx[p] for p in ancestors_dict[child_term] if p in term_to_idx and p != child_term]\n            \n            if parent_indices:\n                # 直接在稀疏矩陣上抽出父母的欄位並加總\n                # .sum(axis=1) 會回傳一個 (N, 1) 的 numpy matrix，再 flatten 變成 1D 陣列\n                parents_sum = np.array(labels_csc[:, parent_indices].sum(axis=1)).flatten()\n                \n                # 將父母總和為 0 的位置設為 0\n                cond_mask[parents_sum == 0, idx] = 0.0\n                \n    # 最終 Mask: 必須有實驗證據 且 不能是條件孤兒\n    final_mask = exp_mask * cond_mask\n    \n    print(f\"✅ 雙重遮罩建立完成！形狀: {final_mask.shape}\")\n    return full_labels_mat, final_mask\n'''","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def build_dual_mask_optimized(y_true_aligned_csr, ancestors_dict, go_maps, save_path=\"aligned_final_mask.npy\"):\n    print(\"\\n[開始] 建構雙重遮罩 ...\")\n    \n    num_proteins, total_labels = y_true_aligned_csr.shape\n    num_mf = len(go_maps['MF'])\n    num_bp = len(go_maps['BP'])\n    num_cc = len(go_maps['CC'])\n    \n    # 刪除舊檔案避免 memmap 殘留問題\n    if os.path.exists(save_path):\n        os.remove(save_path)\n    \n    # ==========================================\n    # 1. 直接建立硬碟映射 (完全不佔 RAM)\n    # ==========================================\n    print(f\"   ├─ 建立硬碟映射檔案 (Memmap): {save_path}\")\n    final_mask = np.memmap(save_path, dtype='float16', mode='w+', shape=(num_proteins, total_labels))\n    \n    # ==========================================\n    # 2. 實驗證據 Mask (分塊寫入硬碟)\n    # ==========================================\n    print(\"   ├─ 計算 Exp Mask (分塊寫入，無 RAM 尖峰)...\")\n    \n    # MF 區塊\n    has_mf = np.array((y_true_aligned_csr[:, 0:num_mf].sum(axis=1) > 0), dtype=np.float16).flatten()\n    final_mask[:, 0:num_mf] = np.repeat(has_mf[:, np.newaxis], num_mf, axis=1)\n    \n    # BP 區塊\n    has_bp = np.array((y_true_aligned_csr[:, num_mf:num_mf+num_bp].sum(axis=1) > 0), dtype=np.float16).flatten()\n    final_mask[:, num_mf:num_mf+num_bp] = np.repeat(has_bp[:, np.newaxis], num_bp, axis=1)\n    \n    # CC 區塊\n    has_cc = np.array((y_true_aligned_csr[:, num_mf+num_bp:total_labels].sum(axis=1) > 0), dtype=np.float16).flatten()\n    final_mask[:, num_mf+num_bp:total_labels] = np.repeat(has_cc[:, np.newaxis], num_cc, axis=1)\n    \n    final_mask.flush() # 強制存入硬碟\n    print(\"   ✅ 實驗證據 Mask 已安全寫入硬碟。\")\n\n    # ==========================================\n    # 3. 條件機率 Mask (使用 CSC 稀疏查詢)\n    # ==========================================\n    print(\"   ├─ 準備稀疏矩陣 (CSC 格式加速查詢)...\")\n    # 轉為 CSC 格式，這種格式對 Column 的切片查詢極度優化！\n    y_csc = y_true_aligned_csr.tocsc() \n    \n    term_to_col = {}\n    current_col = 0\n    for ont in ['MF', 'BP', 'CC']:\n        for col_idx_str, term in go_maps[ont].items():\n            term_to_col[term] = current_col + int(col_idx_str)\n        current_col += len(go_maps[ont])\n        \n    for child_term, col_idx in tqdm(term_to_col.items(), desc=\"   ├─ 計算 Cond Mask\"):\n        if child_term in ancestors_dict:\n            parent_cols = [term_to_col[p] for p in ancestors_dict[child_term] if p in term_to_col and p != child_term]\n            \n            if parent_cols:\n                # 直接對稀疏矩陣加總\n                parents_sum = np.array(y_csc[:, parent_cols].sum(axis=1)).flatten()\n                \n                # 找出孤立節點的行號\n                isolated_idx = np.where(parents_sum == 0)[0]\n                if len(isolated_idx) > 0:\n                    # 直接修改硬碟裡的 memmap\n                    final_mask[isolated_idx, col_idx] = 0.0\n                    \n    final_mask.flush() # 最後再確保所有修改都寫入硬碟\n    \n    print(f\"\\n   預期維度: ({num_proteins}, {total_labels})\")\n    print(f\"   實際維度: {final_mask.shape}\")\n    assert final_mask.shape == (num_proteins, total_labels), \"❌ 致命錯誤：Final Mask 維度不正確！\"\n    \n    # ==========================================\n    # 4. 安全轉移 (釋放寫入權，改為唯讀)\n    # ==========================================\n    del final_mask\n    del y_csc\n    gc.collect()\n    \n    # 重新以唯讀模式 (mmap_mode='r') 載入\n    readonly_mask = np.load(save_path, mmap_mode='r')\n    print(f\"   ✅ 雙重遮罩建構完畢，已無縫轉為 mmap 唯讀模式交接給 DataLoader！\")\n    \n    return readonly_mask","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def get_valid_training_indices(prot_mapping_dict, seq_ids_csv_path, train_terms_tsv_path):\n    print(\"\\n[資料清洗] 正在過濾無效的蛋白質 (取特徵與標籤的交集)...\")\n    \n    seq_df = pd.read_csv(seq_ids_csv_path)\n    seq_ids = set(seq_df['EntryID'])\n    \n    labels_df = pd.read_csv(train_terms_tsv_path, sep=\"\\t\")\n    label_ids = set(labels_df['EntryID'])\n    \n    mapping_ids = set(prot_mapping_dict.keys())\n    \n    # 核心：取三個集合的交集！\n    valid_ids = seq_ids.intersection(label_ids).intersection(mapping_ids)\n    \n    # 將這些 ID 轉換為神經網路對應的絕對 row index\n    valid_indices = [prot_mapping_dict[pid] for pid in valid_ids]\n    \n    print(f\"   - 總 Mapping 數量: {len(mapping_ids)}\")\n    print(f\"   - 具備序列的數量: {len(seq_ids)}\")\n    print(f\"   - 具備標籤的數量: {len(label_ids)}\")\n    print(f\"   ✅ 最終獲准進入訓練的有效蛋白質數量: {len(valid_indices)}\")\n    \n    return np.array(valid_indices), list(valid_ids)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ==========================================\n# 資料分割模組 \n# ==========================================\nfrom sklearn.model_selection import StratifiedKFold\n\n#確認Annotation_Date\n\ndef prepare_strict_validation_splits(train_ids_df, test_taxons_list, n_splits=5):\n    # 建立絕對座標\n    train_ids_df['original_idx'] = np.arange(len(train_ids_df))\n    \n    # 1. 定義終極驗證集 (The Vault)\n    # 條件：日期 >= 2023-01-01 且 物種在 90 類測試名單中\n    vault_mask = (train_ids_df['Annotation_Date'] >= '2023-01-01') & \\\n                 (train_ids_df['TaxonID'].isin(test_taxons_list))\n    \n    vault_df = train_ids_df[vault_mask].copy()\n    forge_pool_df = train_ids_df[~vault_mask].copy()\n    \n    print(f\"終極保險箱 (Vault) 規模: {len(vault_df)} 筆\")\n    print(f\"訓練熔爐 (Forge Pool) 規模: {len(forge_pool_df)} 筆\")\n\n    # 2. 在 Forge Pool 內部進行物種分層 K-Fold\n    # 我們只針對 Forge Pool 裡也屬於測試物種的資料進行分層，其餘隨機分配\n    forge_pool_df['is_test_species'] = forge_pool_df['TaxonID'].isin(test_taxons_list)\n    \n    # 建立一個用於 Stratify 的標籤：若是測試物種則用 TaxonID，否則設為 -1\n    forge_pool_df['stratify_col'] = forge_pool_df.apply(\n        lambda x: x['TaxonID'] if x['is_test_species'] else -1, axis=1\n    )\n\n    skf = StratifiedKFold(n_splits=n_splits, shuffle=True, random_state=42)\n    folds = []\n    \n    # 在熔爐內切分\n    for f_train_idx, f_val_idx in skf.split(forge_pool_df, forge_pool_df['stratify_col']):\n        train_indices = forge_pool_df.iloc[f_train_idx]['original_idx'].values\n        val_indices = forge_pool_df.iloc[f_val_idx]['original_idx'].values\n        folds.append((train_indices, val_indices))\n        \n    return vault_df['original_idx'].values, folds\n\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ==========================================\n# PyTorch Dataset\n# ==========================================\nclass CAFA_Ultimate_Dataset(Dataset):\n    def __init__(self, esm_matrix, bert_matrix, taxon_arr, non_exp_pos, non_exp_neg, labels_matrix, mask_matrix, subset_indices):\n        \"\"\"\n        參數說明：\n        esm_matrix, bert_matrix: numpy.memmap (mmap_mode='r')\n        taxon_arr: numpy 1D array\n        non_exp_pos, non_exp_neg: scipy.sparse.csr_matrix\n        labels_matrix: scipy.sparse.csr_matrix\n        mask_matrix: numpy 2D array \n        subset_indices: 該 fold 被選中的 valid_indices (list or 1D array)\n        \"\"\"\n        self.esm = esm_matrix\n        self.bert = bert_matrix\n        self.taxon_arr = taxon_arr\n        self.indices = subset_indices\n        \n        # ==========================================\n        # 提前切片 (Pre-slicing)\n        # ==========================================\n        # 與其在 __getitem__ 裡面每次提取一行，\n        # 不如在 __init__ 的時候，就把整個 Fold 的稀疏矩陣先切出來。\n        # CSR 矩陣支援直接用 index list 切片，這樣切出來的子矩陣會小很多，\n        # 甚至可以直接轉成 Dense Tensor 常駐在 RAM/VRAM 中，速度提升百倍！\n        \n        print(\"[Dataset] 正在將稀疏矩陣預取 (Pre-fetch) 為連續記憶體區塊...\")\n        \n        # 1. 處理 Non-Exp 特徵\n        pos_subset = non_exp_pos[self.indices].toarray()\n        neg_subset = non_exp_neg[self.indices].toarray()\n        \n        # 將 (N, 26125) 和 (N, 26125) 疊加成 (N, 2, 26125) 的 Tensor\n        self.non_exp_tensor = torch.from_numpy(np.stack([pos_subset, neg_subset], axis=1)).float()\n        \n        # 2. 處理 Ground Truth\n        self.y_true_tensor = torch.from_numpy(labels_matrix[self.indices].toarray()).float()\n        \n        # 3. 處理 Mask\n        if isinstance(mask_matrix, np.ndarray):\n            self.mask_tensor = torch.from_numpy(mask_matrix[self.indices]).float()\n        else:\n            self.mask_tensor = torch.from_numpy(mask_matrix[self.indices].toarray()).float()\n\n    def __len__(self):\n        return len(self.indices)\n\n    def __getitem__(self, i):\n        # i 是從 0 到 len(subset_indices)-1 的連續整數\n        # real_idx 才是對應到 mmap 大矩陣的絕對行號\n        real_idx = self.indices[i]\n        \n        # Memmap 矩陣必須使用 .copy() 防止記憶體洩漏\n        x_esm = torch.from_numpy(self.esm[real_idx].copy()).float()\n        x_bert = torch.from_numpy(self.bert[real_idx].copy()).float()\n        x_tax = torch.tensor(self.taxon_arr[real_idx], dtype=torch.long)\n        \n        # 已經是 Tensor 了，直接索引\n        x_non_exp = self.non_exp_tensor[i]\n        y_true = self.y_true_tensor[i]\n        y_mask = self.mask_tensor[i]\n        \n        return x_esm, x_bert, x_tax, x_non_exp, y_true, y_mask","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class CAFA_Ultimate_Dataset_LowRAM(Dataset):\n    def __init__(self, esm_matrix, bert_matrix, taxon_arr, non_exp_pos, non_exp_neg, labels_matrix, mask_matrix, subset_indices):\n        self.esm = esm_matrix\n        self.bert = bert_matrix\n        self.taxon_arr = taxon_arr\n        \n        # 保持為 CSR 稀疏矩陣 (極省 RAM)\n        self.non_exp_pos = non_exp_pos\n        self.non_exp_neg = non_exp_neg\n        self.labels_mat = labels_matrix\n        self.mask_mat = mask_matrix\n        \n        self.indices = subset_indices\n        self.is_mask_sparse = not isinstance(mask_matrix, np.ndarray)\n\n    def __len__(self):\n        return len(self.indices)\n\n    def __getitem__(self, i):\n        real_idx = self.indices[i]\n        \n        x_esm = torch.from_numpy(self.esm[real_idx].copy()).float()\n        x_bert = torch.from_numpy(self.bert[real_idx].copy()).float()\n        x_tax = torch.tensor(self.taxon_arr[real_idx], dtype=torch.long)\n        \n        # 使用 CSR 矩陣的特定方法 getrow() 來提取，會比直接索引稍微快一點\n        # 然後再 toarray()\n        pos_row = self.non_exp_pos.getrow(real_idx).toarray()[0]\n        neg_row = self.non_exp_neg.getrow(real_idx).toarray()[0]\n        x_non_exp = torch.from_numpy(np.stack([pos_row, neg_row])).float()\n        \n        y_true_row = self.labels_mat.getrow(real_idx).toarray()[0]\n        y_true = torch.from_numpy(y_true_row).float()\n        \n        if self.is_mask_sparse:\n            y_mask = torch.from_numpy(self.mask_mat.getrow(real_idx).toarray()[0]).float()\n        else:\n            y_mask = torch.from_numpy(self.mask_mat[real_idx].copy()).float()\n            \n        return x_esm, x_bert, x_tax, x_non_exp, y_true, y_mask","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ==========================================\n# [模型] 融合 ESM + T5 + Taxon Emb + Non-Exp CNN\n# ==========================================\nclass CAFA_Ultimate_Model_MultiHead(nn.Module):\n    def __init__(self, esm_dim=1280, bert_dim=1024, num_taxons=91, taxon_emb_dim=64, \n                 num_bp=1000, num_cc=300, num_mf=200): \n        super().__init__()\n        self.taxon_embedding = nn.Embedding(num_embeddings=num_taxons, embedding_dim=taxon_emb_dim)\n        \n        # 2 通道 CNN \n        self.non_exp_cnn = nn.Sequential(\n            nn.Conv1d(in_channels=2, out_channels=1, kernel_size=1),\n            nn.Sigmoid() \n        )\n        \n        total_labels = num_bp + num_cc + num_mf\n        fusion_dim = esm_dim + bert_dim + taxon_emb_dim + total_labels \n        \n        self.shared_mlp = nn.Sequential(\n            nn.Linear(fusion_dim, 2048), nn.BatchNorm1d(2048), nn.GELU(), nn.Dropout(0.3),\n            nn.Linear(2048, 1024), nn.BatchNorm1d(1024), nn.GELU(), nn.Dropout(0.2)\n        )\n        \n        self.head_bp = nn.Sequential(nn.Linear(1024, 512), nn.GELU(), nn.Linear(512, num_bp))\n        self.head_cc = nn.Sequential(nn.Linear(1024, 256), nn.GELU(), nn.Linear(256, num_cc))\n        self.head_mf = nn.Sequential(nn.Linear(1024, 256), nn.GELU(), nn.Linear(256, num_mf))\n\n    # 參數名改為 x_bert，讓內外呼叫一致\n    def forward(self, x_esm, x_bert, x_tax, x_non_exp):\n        tax_emb = self.taxon_embedding(x_tax)\n        \n        # x_non_exp shape 為 (Batch, 2, total_labels)\n        cnn_prior = self.non_exp_cnn(x_non_exp).squeeze(1) \n        \n        merged = torch.cat([x_esm, x_bert, tax_emb, cnn_prior], dim=1)\n        shared_features = self.shared_mlp(merged)\n        \n        logits_bp = self.head_bp(shared_features)\n        logits_cc = self.head_cc(shared_features)\n        logits_mf = self.head_mf(shared_features)\n        \n        # MF -> BP -> CC ！\n        return torch.cat([logits_mf, logits_bp, logits_cc], dim=1)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ==========================================\n# [Loss] 支援雙重 Mask 的 IA Asymmetric Loss\n# ==========================================\n\nclass IA_AsymmetricLossMasked(nn.Module):\n    def __init__(self, ia_weights_tensor, gamma_neg=4, gamma_pos=1, clip=0.05):\n        \"\"\"\n        ia_weights_tensor: shape 為 (num_labels,) 的 1D Tensor，必須與模型輸出維度完全對齊\n        \"\"\"\n        super().__init__()\n        self.gamma_neg = gamma_neg\n        self.gamma_pos = gamma_pos\n        self.clip = clip\n        \n        # 註冊為 buffer，它會自動跟隨模型送到 GPU，且不會被 Optimizer 更新\n        self.register_buffer('ia_weights', ia_weights_tensor)\n\n    def forward(self, inputs, targets, valid_mask):\n        xs_pos = torch.sigmoid(inputs)\n        xs_neg = 1 - xs_pos\n\n        if self.clip is not None and self.clip > 0:\n            xs_neg = (xs_neg + self.clip).clamp(max=1)\n\n        loss_pos = targets * torch.log(xs_pos.clamp(min=1e-8))\n        loss_neg = (1 - targets) * torch.log(xs_neg.clamp(min=1e-8))\n        \n        # 1. 基礎 ASL 計算\n        loss = -(loss_pos * (1 - xs_pos)**self.gamma_pos + loss_neg * (xs_neg)**self.gamma_neg)\n        \n        # 2. 乘上 Mask (濾除無實驗證據與條件機率 NaN)\n        masked_loss = loss * valid_mask\n        \n        # 3. 乘上 IA 權重\n        # masked_loss shape: [Batch_size, num_labels]\n        # ia_weights shape:  [num_labels]\n        # PyTorch 會自動將 ia_weights 複製 Batch_size 次進行相乘 (Broadcasting)\n        ia_masked_loss = masked_loss * self.ia_weights\n        \n        # 4. 最終平均 (依照有效 Mask 的數量平均，保持梯度穩定)\n        return ia_masked_loss.sum() / (valid_mask.sum() + 1e-8)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ==========================================\n# 4. 訓練引擎 (Training and validation Loop)\n# ==========================================\nfrom torch.cuda.amp import autocast, GradScaler\nfrom tqdm import tqdm\n\ndef train_ultimate_stage(model, train_loader, val_loader, criterion, epochs=15, lr=1e-4, stage=1, save_path=\"best_model.pth\"):\n    print(f\" 啟動模型訓練管線 (Stage {stage})... | Epochs: {epochs}, LR: {lr}\")\n    \n    optimizer = torch.optim.AdamW(model.parameters(), lr=lr, weight_decay=1e-4)\n    \n    if stage == 1:\n        # Stage 1: OneCycleLR warm up\n        scheduler = torch.optim.lr_scheduler.OneCycleLR(\n            optimizer, max_lr=lr, steps_per_epoch=len(train_loader), epochs=epochs\n        )\n    else:\n        # Stage 2: CosineAnnealingLR \n        # 讓學習率在整個 stage 期間緩慢降到接近 0 (1e-6)\n        total_steps = epochs * len(train_loader)\n        scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(\n            optimizer, T_max=total_steps, eta_min=1e-6\n        )\n        \n    scaler = GradScaler()\n    best_val_loss = float('inf') \n    \n    for epoch in range(epochs):\n        # ==========================================\n        #  Training Phase\n        # ==========================================\n        model.train()\n        total_train_loss = 0\n        train_loop = tqdm(train_loader, leave=False, desc=f\"Stage {stage} | Epoch [{epoch+1}/{epochs}] Train\")\n        \n        for batch in train_loop:\n            x_esm, x_pb, x_tax, x_non_exp, y_true, y_mask = [b.cuda() for b in batch]\n            \n            # Label Smoothing\n            y_smoothed = torch.where(y_true == 1.0, 0.95, 0.0)\n            \n            optimizer.zero_grad()\n            with torch.cuda.amp.autocast():\n                logits = model(x_esm, x_pb, x_tax, x_non_exp)\n                loss = criterion(logits, y_smoothed, y_mask)\n            \n            scaler.scale(loss).backward()\n            scaler.unscale_(optimizer) \n            torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)\n            \n            scaler.step(optimizer)\n            scaler.update()\n            \n            # Step per batch 適用 OneCycle 和 CosineAnnealing\n            scheduler.step() \n            \n            total_train_loss += loss.item()\n            train_loop.set_postfix(loss=f\"{loss.item():.4f}\", lr=f\"{scheduler.get_last_lr()[0]:.6f}\")\n            \n        avg_train_loss = total_train_loss / len(train_loader)\n\n        # ==========================================\n        #  Validation Phase\n        # ==========================================\n        model.eval() \n        total_val_loss = 0\n        val_loop = tqdm(val_loader, leave=False, desc=f\"Stage {stage} | Epoch [{epoch+1}/{epochs}] Valid\")\n        \n        with torch.no_grad():\n            for batch in val_loop:\n                x_esm, x_pb, x_tax, x_non_exp, y_true, y_mask = [b.cuda() for b in batch]\n                \n                with torch.cuda.amp.autocast():\n                    logits = model(x_esm, x_pb, x_tax, x_non_exp)\n                    loss = criterion(logits, y_true, y_mask)\n                \n                total_val_loss += loss.item()\n                val_loop.set_postfix(val_loss=f\"{loss.item():.4f}\")\n                \n        avg_val_loss = total_val_loss / len(val_loader)\n        \n        # ==========================================\n        # Model Checkpointing\n        # ==========================================\n        print(f\"Stage {stage} | Epoch [{epoch+1}/{epochs}] 結算 | Train Loss: {avg_train_loss:.5f} | Val Loss: {avg_val_loss:.5f}\")\n        \n        if avg_val_loss < best_val_loss:\n            best_val_loss = avg_val_loss\n            if isinstance(model, torch.nn.DataParallel):\n                torch.save(model.module.state_dict(), save_path)\n            else:\n                torch.save(model.state_dict(), save_path)\n            print(f\"   🏆 驗證集 Loss 破紀錄！已儲存模型至 {save_path}\")\n\n    print(f\" Stage {stage} 訓練全數完成！\")\n    \n    # 自動載回最佳權重交接給下一個階段或推論\n    if isinstance(model, torch.nn.DataParallel):\n        model.module.load_state_dict(torch.load(save_path))\n    else:\n        model.load_state_dict(torch.load(save_path))\n        \n    print(f\"🔄 已重新載入最佳權重 (Val Loss: {best_val_loss:.5f})\")\n    \n    return model","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-01T10:46:43.542240Z","iopub.execute_input":"2026-05-01T10:46:43.542709Z","iopub.status.idle":"2026-05-01T10:46:46.760279Z","shell.execute_reply.started":"2026-05-01T10:46:43.542675Z","shell.execute_reply":"2026-05-01T10:46:46.759519Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ==========================================\n# [後處理] 公式：[Raw + Max(Children) + Min(Parents)] / 3\n# ==========================================\n\nimport collections\nimport numpy as np\nfrom tqdm import tqdm\n\ndef apply_3way_average_formula_fixed(preds_matrix, ancestors_dict, term_list):\n    \"\"\"\n    根據 ancestors.json 執行第二名三向平均後處理 (已修正 Self-Loop)。\n    \n    參數:\n        preds_matrix (np.ndarray): 模型輸出的預測機率矩陣，形狀為 (N_samples, 1500)\n        ancestors_dict (dict): 讀取自 ancestors.json，格式如 {\"GO:子\": [\"GO:父1\", \"GO:父2\", \"GO:子\"]}\n        term_list (list): 1500 個標籤的順序列表，必須與 preds_matrix 的欄位對齊\n        \n    回傳:\n        final_preds (np.ndarray): 修正後的機率矩陣\n    \"\"\"\n    print(\"執行三向平均後處理 (修正 Self-Loop 迴避)...\")\n    num_samples, num_terms = preds_matrix.shape\n    final_preds = np.copy(preds_matrix)\n    term_to_idx = {term: i for i, term in enumerate(term_list)}\n    \n    # ---------------------------------------------------------\n    # 步驟 1: 從 Ancestors 反推建立 Children Dictionary\n    # ---------------------------------------------------------\n    children_dict = collections.defaultdict(list)\n    for child, parents in ancestors_dict.items():\n        for p in parents:\n            if p != child: \n                children_dict[p].append(child)\n            \n    # ---------------------------------------------------------\n    # 步驟 2: 對每個 GO Term 獨立進行推播計算\n    # ---------------------------------------------------------\n    for term, idx in tqdm(term_to_idx.items(), desc=\"Graph Formula Post-Processing\"):\n        \n        # [A] 獲取該 Term 的原始預測機率 (Shape: N_samples,)\n        p_raw = preds_matrix[:, idx] \n        \n        # [B] 計算 Max(Children)\n        p_child_max = np.zeros(num_samples) # 如果沒子節點，預設給 0\n        if term in children_dict:\n            valid_children = [term_to_idx[c] for c in children_dict[term] if c in term_to_idx and c != term]\n            if valid_children:\n                p_child_max = np.max(preds_matrix[:, valid_children], axis=1)\n                \n        # [C] 計算 Min(Parents)\n        p_parent_min = np.ones(num_samples) # 如果沒父節點，預設給 1 (避免拉低平均)\n        if term in ancestors_dict:\n            valid_parents = [term_to_idx[p] for p in ancestors_dict[term] if p in term_to_idx and p != term]\n            if valid_parents:\n                p_parent_min = np.min(preds_matrix[:, valid_parents], axis=1)\n        \n        # ---------------------------------------------------------\n        # 步驟 3: 套用最終公式 \n        # [ 原始預測 + Max(所有子節點預測) + Min(所有父節點預測) ] / 3\n        # ---------------------------------------------------------\n        final_preds[:, idx] = (p_raw + p_child_max + p_parent_min) / 3.0\n        \n    return final_preds","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-01T10:46:40.332467Z","iopub.execute_input":"2026-05-01T10:46:40.332834Z","iopub.status.idle":"2026-05-01T10:46:40.351728Z","shell.execute_reply.started":"2026-05-01T10:46:40.332803Z","shell.execute_reply":"2026-05-01T10:46:40.350544Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def run_kfold_pipeline(\n    model_class, \n    model_params, \n    esm_matrix,       \n    bert_matrix, \n    taxon_arr, \n    non_exp_pos, \n    non_exp_neg, \n    labels_matrix, \n    mask_matrix, \n    kfolds, \n    ultimate_val_idx, \n    criterion):\n\n    \n    fold_val_scores = []\n    \n    # ==========================================\n    # 建立驗證 DataLoader \n    # ==========================================\n    print(\"準備 Vault Dataset...\")\n    ultimate_ds = CAFA_Ultimate_Dataset(\n        esm_matrix, bert_matrix, taxon_arr, non_exp_pos, non_exp_neg, \n        labels_matrix, mask_matrix, subset_indices=ultimate_val_idx\n    )\n    ultimate_loader = DataLoader(ultimate_ds, batch_size=32, shuffle=False, num_workers=2, pin_memory=True)\n\n    # ---------------------------------------------------------\n    # 宣告雙階段 Criterion\n    # ---------------------------------------------------------\n    # Stage 1: 平等看待所有節點 (IA 權重全為 1.0)\n    flat_ia_tensor = torch.ones(26125, dtype=torch.float32)\n    criterion_stage1 = IA_AsymmetricLossMasked(flat_ia_tensor, gamma_neg=4, gamma_pos=1, clip=0.05).cuda()\n    \n    # Stage 2: 真實 IA 競賽計分權重\n    real_ia_tensor = torch.load(\"ia_weights_combined.pt\")\n    real_ia_tensor = real_ia_tensor / (real_ia_tensor.mean() + 1e-8)\n    criterion_stage2 = IA_AsymmetricLossMasked(real_ia_tensor, gamma_neg=4, gamma_pos=1, clip=0.05).cuda()\n\n    # ==========================================\n    # 啟動 K-Fold 引擎\n    # ==========================================\n    for fold, (t_idx, v_idx) in enumerate(kfolds):\n        print(f\"\\n\" + \"=\"*40 + f\"\\n       🚩 啟動第 {fold+1} 摺疊訓練\\n\" + \"=\"*40)\n        \n        model = model_class(**model_params)\n        model = nn.DataParallel(model).cuda()\n        \n        # 動態實例化當前 Fold 的 Dataset \n        train_ds = CAFA_Ultimate_Dataset(\n            esm_matrix, bert_matrix, taxon_arr, non_exp_pos, non_exp_neg, \n            labels_matrix, mask_matrix, subset_indices=t_idx\n        )\n        val_ds = CAFA_Ultimate_Dataset(\n            esm_matrix, bert_matrix, taxon_arr, non_exp_pos, non_exp_neg, \n            labels_matrix, mask_matrix, subset_indices=v_idx\n        )\n        \n        train_loader = DataLoader(train_ds, batch_size=32, shuffle=True, drop_last=True, num_workers=2, pin_memory=True)\n        val_loader = DataLoader(val_ds, batch_size=32, shuffle=False, num_workers=2, pin_memory=True)\n        \n\n        # 第一階段\n        model = train_ultimate_stage(\n            model=model,\n            train_loader=train_loader,\n            val_loader=val_loader,\n            criterion=criterion_stage1, \n            epochs=10, # 8\n            lr=1e-3, \n            stage=1,\n            save_path=f\"model_fold_{fold+1}_stage1.pth\"\n        )\n    \n        # 第二階段：IA 權重\n        best_fold_model = train_ultimate_stage(\n            model=model, \n            train_loader=train_loader,\n            val_loader=val_loader,\n            criterion=criterion_stage2, \n            epochs=5, # 7\n            lr=5e-5, \n            stage=2,\n            save_path=f\"model_fold_{fold+1}_final.pth\"\n        )\n        \n        # ==========================================\n        # 4. 終極保險箱 (Vault) 真實壓力測試\n        # ==========================================\n        print(f\"✅ 第 {fold+1} 摺疊對『Vault』進行真實壓力測試...\")\n        \n        best_fold_model.eval() \n        vault_total_loss = 0.0\n        \n        all_vault_preds = []\n        all_vault_labels = []\n        \n        with torch.no_grad():\n            for batch in ultimate_loader:\n                x_esm, x_bert, x_tax, x_non_exp, y_true_batch, y_mask = [b.cuda() for b in batch]\n                \n                with torch.cuda.amp.autocast():\n                    logits = best_fold_model(x_esm, x_bert, x_tax, x_non_exp)\n                    \n                    # 1. 紀錄純粹的 Loss (不加後處理)\n                    loss = criterion(logits, y_true_batch, y_mask)\n                    vault_total_loss += loss.item()\n                    \n                    # 2. 儲存機率與答案，準備進行真實模擬\n                    all_vault_preds.append(torch.sigmoid(logits).cpu().numpy())\n                    all_vault_labels.append(y_true_batch.cpu().numpy())\n                    \n        avg_vault_loss = vault_total_loss / len(ultimate_loader)\n        \n        # --- 啟動真實模擬 (模擬 Submission) ---\n        print(\"   正在套用圖論後處理與計算 F_max...\")\n        full_preds = np.vstack(all_vault_preds)\n        full_labels = np.vstack(all_vault_labels)\n        \n        # 3. 套用三向圖論平均！\n        full_preds_post = batch_3way_average_fixed(full_preds, ancestors, term_list)\n        \n        # 4. 算出真實的 F_max 分數！\n        vault_fmax = calculate_protein_centric_fmax(full_labels, full_preds_post)\n        \n        print(f\"   神經網路 Loss: {avg_vault_loss:.5f}\")\n        print(f\"   Vault F_max: {vault_fmax:.4f}\")\n        \n        # 你可以選擇把 f_max 存起來，這比 val_loss 更有參考價值\n        fold_val_scores.append(vault_fmax)\n        \n        del model, best_fold_model, train_ds, val_ds, train_loader, val_loader\n        gc.collect()\n        torch.cuda.empty_cache()\n\n    print(f\"所有摺疊訓練完成，5 個最佳模型已就位。\")\n    print(f\"各摺疊壓力測試分數 (Vault Scores):\")\n    for i, score in enumerate(fold_val_scores):\n        print(f\"   🚩 Fold {i+1}: {score:.5f}\")\n        \n    print(f\"\\n平均分數 (Mean Score): {np.mean(fold_val_scores):.5f}\")\n    return fold_val_scores","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-01T10:46:35.781149Z","iopub.execute_input":"2026-05-01T10:46:35.781484Z","iopub.status.idle":"2026-05-01T10:46:35.799261Z","shell.execute_reply.started":"2026-05-01T10:46:35.781458Z","shell.execute_reply":"2026-05-01T10:46:35.798290Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"trainning","metadata":{}},{"cell_type":"code","source":"# ==========================================\n# 6. 主程式執行區塊 (Main Execution)\n# ==========================================\nif __name__ == \"__main__\":\n    print(\"=== 啟動 CAFA 5 訓練 ===\")\n\n    # ---------------------------------------------------------\n    # [步驟 A] 全域設定與測試目標載入\n    # ---------------------------------------------------------\n    BASE_PATH = \"/kaggle/input/competitions/cafa-5-protein-function-prediction\"\n    TRAIN_IDS_CSV_PATH = \"/kaggle/input/notebooks/t8101349/cafa-5-protein-function-prediction-embeddings/train_ids.csv\"\n    IA_WEIGHTS_PATH = \"ia_weights_combined.pt\" \n    \n    print(\"[階段 1] 讀取測試集 Taxonomy 名單...\")\n    test_taxon_path = f\"{BASE_PATH}/Test (Targets)/testsuperset-taxon-list.tsv\"\n    test_taxon_df = pd.read_csv(test_taxon_path, sep=\"\\t\", encoding=\"ISO-8859-1\")\n    TEST_TAXONS = test_taxon_df['ID'].astype(int).tolist()\n    print(f\"✅ 成功讀取，共 {len(TEST_TAXONS)} 個目標物種。\")\n\n    # ---------------------------------------------------------\n    # [步驟 B] 載入已對齊的「大一統資源」\n    # ---------------------------------------------------------\n    print(\"\\n[階段 2] 載入全域大一統資源...\")\n    # 這裡只負責「讀取」，所有矩陣的 Row 0~142245 皆已對齊 train_ids.csv\n    go_maps, ancestors, esm_emb, pb_emb, y_true, taxon_arr, non_exp_pos, non_exp_neg, _ = load_aligned_cafa_resources()\n    \n    num_mf = len(go_maps['MF'])\n    num_bp = len(go_maps['BP'])\n    num_cc = len(go_maps['CC'])\n    num_taxons = int(taxon_arr.max()) + 1 # 動態抓取 Taxon 總類別數 (包含 0 類)\n\n    print(f\"  檢查本體維度總和: {num_mf} + {num_bp} + {num_cc} = {num_mf + num_bp + num_cc}\")\n\n    # 順序必須是 MF -> BP -> CC，這與 y_true 和 model 的輸出頭 100% 對齊\n    term_list = (\n        list(go_maps['MF'].values()) + \n        list(go_maps['BP'].values()) + \n        list(go_maps['CC'].values())\n    )\n    \n    # 驗證維度\n    print(f\"✅ term_list 建立完成，總共包含 {len(term_list)} 個 GO Terms。\")\n    # 根據你的筆記，這裡應該會印出 26125\n\n    # ---------------------------------------------------------\n    # [步驟 C] 秒殺級：獲取有效訓練索引 (Valid Indices)\n    # ---------------------------------------------------------\n    print(\"\\n[階段 3] 篩選有效訓練樣本...\")\n    # 算出每個蛋白質擁有的標籤數量\n    row_sums = np.array(y_true.sum(axis=1)).flatten() \n    \n    # 抓出大於 0 的絕對行號 (只保留有實驗標籤的蛋白質)\n    valid_indices = np.where(row_sums > 0)[0] \n    \n    train_ids_df = pd.read_csv(TRAIN_IDS_CSV_PATH)\n    print(f\"   - 總庫存: {len(train_ids_df)} 筆\")\n    print(f\"   ✅ 獲准進入訓練的有效蛋白質: {len(valid_indices)} 筆\")\n\n    # ---------------------------------------------------------\n    # [步驟 D] 建立雙重遮罩 (Dual Mask)\n    # ---------------------------------------------------------\n    print(\"\\n[階段 4] 建立雙重遮罩 (Dual Mask)...\")\n    # 直接使用我們寫好的優化版函數 (它會回傳建構好的 final_mask 矩陣)\n    final_mask = build_dual_mask_optimized(y_true, ancestors, go_maps)\n\n    # ---------------------------------------------------------\n    # [步驟 E] 戰術切分 (K-Fold & Vault)\n    # ---------------------------------------------------------\n    print(\"\\n[階段 5] 準備 Vault 與 K-Fold 嚴格切分...\")\n    # 1. 針對 142246 全宇宙進行時間與物種切分\n    vault_idx_raw, kfolds_raw = prepare_strict_validation_splits(train_ids_df, TEST_TAXONS)\n    \n    # 2. 用 Valid Indices 取交集，剔除沒有標籤的廢資料\n    vault_idx = np.intersect1d(vault_idx_raw, valid_indices)\n    \n    kfolds = []\n    for t_idx_raw, v_idx_raw in kfolds_raw:\n        t_idx_clean = np.intersect1d(t_idx_raw, valid_indices)\n        v_idx_clean = np.intersect1d(v_idx_raw, valid_indices)\n        kfolds.append((t_idx_clean, v_idx_clean))\n        \n    print(f\"   ✅ 切分洗牌完成！乾淨的 Vault 數量: {len(vault_idx)}\")\n\n    # ---------------------------------------------------------\n    # [步驟 F] 實例化終極 Loss 函數\n    # ---------------------------------------------------------\n    print(\"\\n[階段 6] 載入 IA 權重與 Loss Function...\")\n    ia_tensor = torch.load(IA_WEIGHTS_PATH)\n    \n    # IA 權重正規化 (穩定梯度 Scale)\n    ia_tensor = ia_tensor / (ia_tensor.mean() + 1e-8) \n    print(f\"   IA Tensor 載入: Shape {ia_tensor.shape}, Max: {ia_tensor.max():.4f}, Mean: {ia_tensor.mean():.4f}\")\n    \n    criterion = IA_AsymmetricLossMasked(\n        ia_weights_tensor=ia_tensor, \n        gamma_neg=4, \n        gamma_pos=1, \n        clip=0.05\n    ).cuda()\n\n    # ---------------------------------------------------------\n    # [步驟 G] 啟動 5-Fold 引擎\n    # ---------------------------------------------------------\n    print(\"\\n[階段 7]  啟動 5-Fold 交叉驗證...\")\n\n    # 打包神經網路架構參數\n    model_params = {\n        'num_taxons': num_taxons,\n        'num_bp': num_bp,\n        'num_cc': num_cc,\n        'num_mf': num_mf,\n        'esm_dim': 1280,\n        'bert_dim': 1024\n    }\n    \n    # 點火！\n    run_kfold_pipeline(\n        model_class=CAFA_Ultimate_Model_MultiHead,\n        model_params=model_params,\n        esm_matrix=esm_emb,         # 傳入 memmap\n        bert_matrix=pb_emb,         # 傳入 memmap\n        taxon_arr=taxon_arr, \n        non_exp_pos=non_exp_pos,    # 傳入稀疏矩陣\n        non_exp_neg=non_exp_neg,    # 傳入稀疏矩陣\n        labels_matrix=y_true,      # 傳入稀疏矩陣\n        mask_matrix=final_mask,       # 傳入 float16 矩陣\n        kfolds=kfolds,\n        ultimate_val_idx=vault_idx,\n        criterion=criterion\n    )\n\n    print(\"\\n🏆 所有摺疊訓練完成，5 個最佳模型已完成！\")\n    print(\"下個階段：請載入這 5 個 .pth 權重，對 Vault 與 Test Set 進行預測與融合。\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"test prediction","metadata":{}},{"cell_type":"code","source":"import pandas as pd\nimport numpy as np\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\nimport scipy.sparse as sp\nimport json\nfrom tqdm import tqdm\nimport collections\n\n# ==========================================\n# 1. 測試集專用 Dataset\n# ==========================================\nclass CAFA_Test_Dataset(Dataset):\n    def __init__(self, esm_matrix, bert_matrix, taxon_arr, non_exp_pos, non_exp_neg):\n        self.esm = esm_matrix\n        self.bert = bert_matrix\n        self.taxon_arr = taxon_arr\n        \n        # 預先轉為連續記憶體以加速，因為 Test Set 只過一次\n        print(\"[Test Dataset] 預載 Non-Exp 矩陣...\")\n        pos_subset = non_exp_pos.toarray()\n        neg_subset = non_exp_neg.toarray()\n        self.non_exp_tensor = torch.from_numpy(np.stack([pos_subset, neg_subset], axis=1)).float()\n        self.length = len(self.taxon_arr)\n\n    def __len__(self):\n        return self.length\n\n    def __getitem__(self, i):\n        x_esm = torch.from_numpy(self.esm[i].copy()).float()\n        x_bert = torch.from_numpy(self.bert[i].copy()).float()\n        x_tax = torch.tensor(self.taxon_arr[i], dtype=torch.long)\n        x_non_exp = self.non_exp_tensor[i]\n        \n        return x_esm, x_bert, x_tax, x_non_exp\n\n# ==========================================\n# 2. Test 資料對齊器 (解析 FASTA)\n# ==========================================\ndef prepare_test_data(base_path, custom_path, prot_mapping_dict, test_taxon_list, \n                             test_ids_csv_path, test_esm_path, test_pb_path):\n    print(\"啟動 Test 對齊工程...\")\n    \n    # 1. 讀取 Test IDs (基準列表)\n    test_ids_df = pd.read_csv(test_ids_csv_path)\n    test_ids = test_ids_df['EntryID'].tolist()\n    N_test = len(test_ids)\n    \n    # 2. 解析 FASTA 並進行「循序防呆確認」\n    print(\"解析 FASTA 獲取 Taxon，並驗證排序一致性...\")\n    taxon_to_idx = {tax_id: i + 1 for i, tax_id in enumerate(test_taxon_list)}\n    test_taxons = np.zeros(N_test, dtype=np.int64)\n    \n    with open(f\"{base_path}/Test (Targets)/testsuperset.fasta\", \"r\") as f:\n        idx = 0\n        for line in f:\n            if line.startswith(\">\"):\n                parts = line.strip()[1:].split() \n                pid = parts[0]\n                \n                # 防呆確認：如果順序跟 CSV 對不上，直接報錯中斷！\n                if pid != test_ids[idx]:\n                    raise ValueError(f\"❌ 致命錯位！FASTA 讀到 {pid}，但 CSV 第 {idx} 筆是 {test_ids[idx]}\")\n                \n                tax_id = int(parts[1]) if len(parts) > 1 else 0\n                test_taxons[idx] = taxon_to_idx.get(tax_id, 0)\n                idx += 1\n                \n    print(f\"   ✅ 順序確認無誤！FASTA 與 CSV 的 {idx} 筆資料完美 1 對 1 吻合。\")\n\n    # 3. 對齊 Non-Exp 特徵\n    # (因為 Non-Exp 是從全域的大矩陣來的，所以這個還是得用 prot_mapping_dict 抓)\n    print(\"擷取 Non-Exp 先驗特徵...\")\n    pos_combined = sp.hstack([\n        sp.load_npz(f\"{custom_path}/labels_MF.npz\"),\n        sp.load_npz(f\"{custom_path}/labels_BP.npz\"),\n        sp.load_npz(f\"{custom_path}/labels_CC.npz\")\n    ]).tocsr()\n    \n    neg_combined = sp.hstack([\n        sp.load_npz(f\"{custom_path}/labels_MF_neg.npz\"),\n        sp.load_npz(f\"{custom_path}/labels_BP_neg.npz\"),\n        sp.load_npz(f\"{custom_path}/labels_CC_neg.npz\")\n    ]).tocsr()\n    \n    total_labels = pos_combined.shape[1]\n    aligned_pos = sp.lil_matrix((N_test, total_labels), dtype=np.float32)\n    aligned_neg = sp.lil_matrix((N_test, total_labels), dtype=np.float32)\n    \n    found_count = 0\n    for i, pid in enumerate(tqdm(test_ids, desc=\"Test Non-Exp\")):\n        # 如果這個測試蛋白質在我們原本的全域 mapping 裡有找到先驗特徵\n        if pid in prot_mapping_dict:\n            old_idx = prot_mapping_dict[pid]\n            aligned_pos[i] = pos_combined[old_idx]\n            aligned_neg[i] = neg_combined[old_idx]\n            found_count += 1\n            \n    print(f\"   共有 {found_count} / {N_test} 筆蛋白質擁有先驗特徵。\")\n    \n    # 4. 直接載入 Embeddings (既然順序一致，直接讀進來即可)\n    print(\"載入 Embeddings...\")\n    test_esm_emb = np.load(test_esm_path, mmap_mode='r')\n    test_pb_emb = np.load(test_pb_path, mmap_mode='r')\n\n    return test_ids, test_taxons, aligned_pos.tocsr(), aligned_neg.tocsr(), test_esm_emb, test_pb_emb","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def batch_3way_average_fixed(batch_preds, ancestors_dict, term_list):\n    \"\"\"\n    對一個 Batch 的預測結果 (batch_size, 26125) 進行三向圖論修正。\n    \"\"\"\n    term_to_idx = {term: i for i, term in enumerate(term_list)}\n    final_preds = np.copy(batch_preds)\n    \n    children_dict = collections.defaultdict(list)\n    for child, parents in ancestors_dict.items():\n        for p in parents:\n            if p != child: \n                children_dict[p].append(child)\n                \n    for term, idx in term_to_idx.items():\n        p_raw = batch_preds[:, idx]\n        \n        p_child_max = np.zeros(batch_preds.shape[0])\n        if term in children_dict:\n            valid_children = [term_to_idx[c] for c in children_dict[term] if c in term_to_idx and c != term]\n            if valid_children:\n                p_child_max = np.max(batch_preds[:, valid_children], axis=1)\n                \n        p_parent_min = np.ones(batch_preds.shape[0])\n        if term in ancestors_dict:\n            valid_parents = [term_to_idx[p] for p in ancestors_dict[term] if p in term_to_idx and p != term]\n            if valid_parents:\n                p_parent_min = np.min(batch_preds[:, valid_parents], axis=1)\n                \n        final_preds[:, idx] = (p_raw + p_child_max + p_parent_min) / 3.0\n        \n    return final_preds","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def generate_cafa5_submission(\n    models, test_loader, test_ids, term_list, ancestors_dict, \n    output_path=\"submission.tsv\", threshold=0.01, top_k=500\n):\n    print(f\"\\n 啟動 5-Fold 融合推論與提交生成 (閾值: {threshold}, 最高保留: {top_k})...\")\n    \n    # 確保所有模型都在 eval 模式\n    for model in models:\n        model.eval()\n        \n    # 開啟檔案準備以 Append 模式串流寫入\n    with open(output_path, \"w\") as f:\n        # 寫入 CAFA 5 官方要求的標頭 (如果有要求)\n        # f.write(\"Protein Id\\tGO Term Id\\tPrediction\\n\")\n        \n        test_loop = tqdm(test_loader, desc=\"Inference & Post-processing\")\n        current_idx = 0\n        \n        with torch.no_grad():\n            for batch in test_loop:\n                x_esm, x_bert, x_tax, x_non_exp = [b.cuda() for b in batch]\n                \n                batch_size = x_esm.size(0)\n                \n                # 1. 計算 5-Fold 平均機率\n                ensemble_preds = torch.zeros((batch_size, len(term_list)), device='cuda')\n                \n                with torch.cuda.amp.autocast():\n                    for model in models:\n                        logits = model(x_esm, x_bert, x_tax, x_non_exp)\n                        ensemble_preds += torch.sigmoid(logits)\n                        \n                ensemble_preds = ensemble_preds / len(models)\n                \n                # 將張量拉回 CPU 並轉 numpy 準備後處理\n                ensemble_preds_np = ensemble_preds.cpu().numpy()\n                \n                # 2. 執行 Batch-Level 圖論後處理 (讓機率符合 GO DAG 結構)\n                final_preds_np = batch_3way_average_fixed(ensemble_preds_np, ancestors_dict, term_list)\n                \n                # 3. 雙重過濾與寫入 TSV (節省檔案大小與計分時間)\n                for b_idx in range(batch_size):\n                    pid = test_ids[current_idx]\n                    p_scores = final_preds_np[b_idx]\n                    \n                    # 找出大於閾值的索引\n                    valid_indices = np.where(p_scores > threshold)[0]\n                    valid_scores = p_scores[valid_indices]\n                    \n                    # 如果超過 Top K，則進行排序並截斷 (保留最高分的 K 個)\n                    if len(valid_scores) > top_k:\n                        # argsort 是從小到大，[::-1] 反轉成從大到小\n                        sort_idx = np.argsort(valid_scores)[::-1][:top_k]\n                        valid_indices = valid_indices[sort_idx]\n                        valid_scores = valid_scores[sort_idx]\n                        \n                    # 逐行寫入檔案\n                    for v_idx, score in zip(valid_indices, valid_scores):\n                        go_term = term_list[v_idx]\n                        # 格式化輸出到小數點後 3 位即可 (節省空間)\n                        f.write(f\"{pid}\\t{go_term}\\t{score:.3f}\\n\")\n                        \n                    current_idx += 1\n                    \n    print(f\"\\n Submission 已成功生成至：{output_path}\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ==========================================\n# 終極預測與提交階段 (The Final Submission)\n# ==========================================\nprint(\"\\n\" + \"=\"*50)\nprint(\"🏆 啟動預測與 Submission 生成程序\")\nprint(\"=\"*50)\n\n# 1. 資料路徑\nTEST_IDS_PATH = \"/kaggle/input/notebooks/t8101349/cafa-5-protein-function-prediction-test-embed/test_ids.csv\"\nTEST_ESM_PATH = \"/kaggle/input/notebooks/t8101349/cafa-5-protein-function-prediction-test-embed/test_esm_emb_aligned.npy\"\nTEST_PB_PATH = \"/kaggle/input/notebooks/t8101349/cafa-5-protein-function-prediction-test-embed/test_pb_emb_aligned.npy\"\nCUSTOM_LABELS_PATH = \"/kaggle/input/notebooks/t8101349/cafa-5-protein-function-prediction-embeddings2\"\n\n# 2. 嚴格對齊的 Test \ntest_ids, test_taxons, test_pos, test_neg, test_esm_emb, test_pb_emb = prepare_test_data(\n    base_path=BASE_PATH,\n    custom_path=CUSTOM_LABELS_PATH,\n    prot_mapping_dict=prot_mapping_dict, # 訓練時讀取的那個\n    test_taxon_list=TEST_TAXONS,\n    test_ids_csv_path=TEST_IDS_PATH,\n    test_esm_path=TEST_ESM_PATH,\n    test_pb_path=TEST_PB_PATH\n)\n\n# 3. 封裝進 Dataset 與 DataLoader ( CAFA_Test_Dataset)\ntest_ds = CAFA_Test_Dataset(test_esm_emb, test_pb_emb, test_taxons, test_pos, test_neg)\ntest_loader = DataLoader(test_ds, batch_size=64, shuffle=False, num_workers=2, pin_memory=True)\n\n# 4. 準備推論工具\nterm_list = (\n    list(go_maps['MF'].values()) + \n    list(go_maps['BP'].values()) + \n    list(go_maps['CC'].values())\n)\n\n# 驗證維度\nprint(f\"✅ term_list 建立完成，總共包含 {len(term_list)} 個 GO Terms。\")\n\nmodels = []\nfor i in range(1, 6):\n    model = CAFA_Ultimate_Model_MultiHead(**model_params)\n    model.load_state_dict(torch.load(f\"model_fold_{i}_final.pth\"))\n    model = nn.DataParallel(model).cuda()\n    models.append(model)\n\n# 5. 推論\ngenerate_cafa5_submission(\n    models=models,             # 裝有 5 個最佳 fold 權重的 model list\n    test_loader=test_loader,\n    test_ids=test_ids,         # CSV 的絕對 ID 順序\n    term_list=term_list,\n    ancestors_dict=ancestors,\n    output_path=\"submission.tsv\",\n    threshold=0.01,            # 濾除 1% 以下的微弱訊號，保護檔案大小\n    top_k=500                  # 避免超出官方上傳限制 每個蛋白質最多上傳 500 個標籤\n)","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}