beiken1201 commited on
Commit
8c86905
·
verified ·
1 Parent(s): 5e449c6

Name model-selection splits validation in drug tasks

Browse files

Use validation terminology in both drug tasks, tutorials and documentation. Add validation_label_file and validation_cell_file options with legacy aliases, and write the validation prediction field while retaining notebook support for older outputs. Keep data partitions, training behavior, dataset paths, weights and PKL files unchanged.

BioMod/README.md CHANGED
@@ -15,8 +15,8 @@ files, and plotting helpers.
15
  | Annotation | Supervised cell-type annotation workflows |
16
  | Integration | Zero-shot batch integration and UMAP/scIB-style reporting |
17
  | Perturbation | Norman gene-perturbation tutorial and smoke workflow |
18
- | Drug response | Single-drug response prediction with GDSC/CCLE data |
19
- | Drug combinations | Drug-combination synergy prediction with independent-set evaluation |
20
  | Figures | Notebook-level PDF/PNG/SVG plotting cells for paper and GitHub display |
21
 
22
  ## Repository Layout
 
15
  | Annotation | Supervised cell-type annotation workflows |
16
  | Integration | Zero-shot batch integration and UMAP/scIB-style reporting |
17
  | Perturbation | Norman gene-perturbation tutorial and smoke workflow |
18
+ | Drug response | Single-drug response prediction with GDSC/CCLE data and validation-based model selection |
19
+ | Drug combinations | Drug-combination synergy prediction with validation-based model selection |
20
  | Figures | Notebook-level PDF/PNG/SVG plotting cells for paper and GitHub display |
21
 
22
  ## Repository Layout
BioMod/drug_combination/README.md CHANGED
@@ -27,10 +27,13 @@ data_summary/
27
  |-- new_cell_features_954.csv # Training cell features (30 cell lines x 954 genes)
28
  |-- smiles_0_10.csv # Drug SMILES
29
  `-- test_data/
30
- |-- independent_input.csv # Independent test labels (668 samples)
31
- `-- independent_cell_features_954.csv # Test cell features (1020 cell lines x 954 genes)
32
  ```
33
 
 
 
 
34
  ## Dependencies
35
 
36
  - PyTorch
@@ -106,11 +109,13 @@ python train_drug_combination_performer_module.py \
106
  | `--hyper_params_path` | Required | Path to the PerformerModule hyper-parameter pkl file |
107
  | `--module_data_dir` | None | Directory containing module data files |
108
  | `--data_dir` | `./data_summary` | Data directory |
 
 
109
  | `--batch_size` | 256 | Training batch size |
110
  | `--lr` | 1e-4 | Learning rate |
111
  | `--epochs` | 100 | Number of training epochs |
112
  | `--n_folds` | 5 | Number of folds |
113
- | `--domain_gen` | True | Domain generalization mode: train on the full training set and evaluate on the independent test set |
114
  | `--eval_interval` | 5 | Evaluate every N epochs |
115
  | `--pool_method` | mean | PerformerModule pooling method (`mean`, `max`, or `concat`) |
116
  | `--cache_emb` | True | Cache cell embeddings to a `.npy` file |
@@ -120,11 +125,18 @@ python train_drug_combination_performer_module.py \
120
 
121
  Results are saved under `--output_dir` (default: `./results/`):
122
 
123
- - `best_model_fold{i}.pt` - best model weights for each fold
124
- - `fpr_tpr_fold{i}.pkl` - FPR/TPR curve data for drawing ROC curves
125
  - `cell_emb_cache_{pool_method}.npy` - cached PerformerModule cell embeddings
126
 
127
  ## Evaluation Metrics
128
 
129
  Every `--eval_interval` epochs, the script evaluates AUC, PR_AUC, ACC, BACC,
130
- PREC, TPR, and KAPPA, matching scLong.
 
 
 
 
 
 
 
 
27
  |-- new_cell_features_954.csv # Training cell features (30 cell lines x 954 genes)
28
  |-- smiles_0_10.csv # Drug SMILES
29
  `-- test_data/
30
+ |-- independent_input.csv # Validation labels (668 samples)
31
+ `-- independent_cell_features_954.csv # Validation cell features (1020 cell lines x 954 genes)
32
  ```
33
 
34
+ The directory name `test_data/` is retained for compatibility. These files are
35
+ used as the validation set for checkpoint selection.
36
+
37
  ## Dependencies
38
 
39
  - PyTorch
 
109
  | `--hyper_params_path` | Required | Path to the PerformerModule hyper-parameter pkl file |
110
  | `--module_data_dir` | None | Directory containing module data files |
111
  | `--data_dir` | `./data_summary` | Data directory |
112
+ | `--validation_label_file` | `test_data/independent_input.csv` | Validation labels for checkpoint selection; legacy alias: `--test_label_file` |
113
+ | `--validation_cell_file` | `test_data/independent_cell_features_954.csv` | Validation cell features; legacy alias: `--test_cell_file` |
114
  | `--batch_size` | 256 | Training batch size |
115
  | `--lr` | 1e-4 | Learning rate |
116
  | `--epochs` | 100 | Number of training epochs |
117
  | `--n_folds` | 5 | Number of folds |
118
+ | `--domain_gen` | True | Domain generalization mode: train on the full training set and select checkpoints using a separate validation set |
119
  | `--eval_interval` | 5 | Evaluate every N epochs |
120
  | `--pool_method` | mean | PerformerModule pooling method (`mean`, `max`, or `concat`) |
121
  | `--cache_emb` | True | Cache cell embeddings to a `.npy` file |
 
125
 
126
  Results are saved under `--output_dir` (default: `./results/`):
127
 
128
+ - `best_model_fold{i}.pt` - model weights selected by the highest validation AUC for each fold
129
+ - `fpr_tpr_fold{i}.pkl` - validation FPR/TPR curve data for drawing ROC curves
130
  - `cell_emb_cache_{pool_method}.npy` - cached PerformerModule cell embeddings
131
 
132
  ## Evaluation Metrics
133
 
134
  Every `--eval_interval` epochs, the script evaluates AUC, PR_AUC, ACC, BACC,
135
+ PREC, TPR, and KAPPA on the validation set, matching scLong's metric definitions.
136
+ The checkpoint with the highest validation AUC is saved for each fold. In
137
+ `--no_domain_gen` mode, each held-out fold serves as the validation set.
138
+
139
+ This naming update preserves the data, splits, checkpoint-selection behavior,
140
+ output filenames, metric column names, and legacy dataset cache IDs. It does not
141
+ create or evaluate an independent final test set; the reported values are
142
+ validation results used during model selection, not final test estimates.
BioMod/drug_combination/train_drug_combination_performer_module.py CHANGED
@@ -610,10 +610,14 @@ def main():
610
  default=os.path.join(SCRIPT_DIR, 'data_summary'))
611
  parser.add_argument('--label_file', type=str, default='new_labels_0_10.csv')
612
  parser.add_argument('--cell_file', type=str, default='new_cell_features_954.csv')
613
- parser.add_argument('--test_label_file', type=str,
614
- default='test_data/independent_input.csv')
615
- parser.add_argument('--test_cell_file', type=str,
616
- default='test_data/independent_cell_features_954.csv')
 
 
 
 
617
 
618
  # Training args
619
  parser.add_argument('--batch_size', type=int, default=256)
@@ -621,7 +625,7 @@ def main():
621
  parser.add_argument('--epochs', type=int, default=100)
622
  parser.add_argument('--n_folds', type=int, default=5)
623
  parser.add_argument('--domain_gen', action='store_true', default=True,
624
- help='Domain generalization: train full set, test on independent set')
625
  parser.add_argument('--no_domain_gen', dest='domain_gen', action='store_false')
626
  parser.add_argument('--eval_interval', type=int, default=5)
627
  parser.add_argument('--output_dir', type=str,
@@ -651,12 +655,12 @@ def main():
651
  train_cell_df = load_cell_features(train_cell_file)
652
  print_sys(f'Loaded train cell features: {train_cell_df.shape}')
653
 
654
- test_cell_file = os.path.join(args.data_dir, args.test_cell_file)
655
- test_cell_df = load_cell_features(test_cell_file)
656
- print_sys(f'Loaded test cell features: {test_cell_df.shape}')
657
 
658
  # Combine all cell lines and remove duplicates
659
- all_cell_df = pd.concat([train_cell_df, test_cell_df])
660
  all_cell_df = all_cell_df[~all_cell_df.index.duplicated(keep='first')]
661
  print_sys(f'Total unique cell lines: {len(all_cell_df)}')
662
 
@@ -691,13 +695,13 @@ def main():
691
  train_labels = pd.read_csv(train_label_path)
692
  print_sys(f'Train labels: {len(train_labels)} samples')
693
 
694
- test_label_path = os.path.join(args.data_dir, args.test_label_file)
695
- test_labels = pd.read_csv(test_label_path)
696
- print_sys(f'Test labels: {len(test_labels)} samples')
697
 
698
  all_smiles = set(
699
  train_labels['drug1'].tolist() + train_labels['drug2'].tolist()
700
- + test_labels['drug1'].tolist() + test_labels['drug2'].tolist()
701
  )
702
  smile_graph = {}
703
  failed = []
@@ -737,20 +741,21 @@ def main():
737
  cell_emb_dict=cell_emb_dict, smile_graph=smile_graph,
738
  )
739
 
740
- # Test datasets (independent set)
741
- drug1_test = test_labels['drug1'].tolist()
742
- drug2_test = test_labels['drug2'].tolist()
743
- cells_test = test_labels['cell'].tolist()
744
- labels_test = test_labels['label'].tolist()
 
745
 
746
- test_d1_dataset = TestbedDataset(
747
  root=args.data_dir, dataset='test_drug1_performer_module',
748
- xd=drug1_test, xt=cells_test, y=labels_test,
749
  cell_emb_dict=cell_emb_dict, smile_graph=smile_graph,
750
  )
751
- test_d2_dataset = TestbedDataset(
752
  root=args.data_dir, dataset='test_drug2_performer_module',
753
- xd=drug2_test, xt=cells_test, y=labels_test,
754
  cell_emb_dict=cell_emb_dict, smile_graph=smile_graph,
755
  )
756
 
@@ -766,33 +771,33 @@ def main():
766
  print_sys(f'{"=" * 60}')
767
 
768
  if args.domain_gen:
769
- # Domain generalization: train on full train set, test on independent set
770
  d1_train_data = train_d1_dataset
771
- d1_test_data = test_d1_dataset
772
  d2_train_data = train_d2_dataset
773
- d2_test_data = test_d2_dataset
774
  else:
775
  # Standard k-fold cross-validation
776
- test_num = random_num[pot * fold_i : pot * (fold_i + 1)]
777
  train_num = (
778
  random_num[: pot * fold_i] + random_num[pot * (fold_i + 1) :]
779
  )
780
  d1_train_data = train_d1_dataset[train_num]
781
- d1_test_data = train_d1_dataset[test_num]
782
  d2_train_data = train_d2_dataset[train_num]
783
- d2_test_data = train_d2_dataset[test_num]
784
 
785
  d1_loader_train = DataLoader(
786
  d1_train_data, batch_size=args.batch_size, shuffle=None, pin_memory=True,
787
  )
788
- d1_loader_test = DataLoader(
789
- d1_test_data, batch_size=args.batch_size, shuffle=None, pin_memory=True,
790
  )
791
  d2_loader_train = DataLoader(
792
  d2_train_data, batch_size=args.batch_size, shuffle=None, pin_memory=True,
793
  )
794
- d2_loader_test = DataLoader(
795
- d2_test_data, batch_size=args.batch_size, shuffle=None, pin_memory=True,
796
  )
797
 
798
  model = GCNNet(cell_emb_dim=cell_emb_dim).to(device)
@@ -819,7 +824,7 @@ def main():
819
  loss_history.append(avg_loss)
820
 
821
  if epoch % args.eval_interval == 0:
822
- T, S, Y = predict(model, device, d1_loader_test, d2_loader_test)
823
  m = evaluate_metrics(T, S, Y)
824
  metric_history.append({'epoch': epoch, **m})
825
 
@@ -830,10 +835,10 @@ def main():
830
  args.output_dir, f'best_model_fold{fold_i}.pt'
831
  )
832
  torch.save(model.state_dict(), save_path)
833
- print_sys(f'** New best AUROC: {best_auc:.4f} (saved)')
834
 
835
  print_sys(
836
- f'Epoch {epoch} | '
837
  f'AUROC: {m["AUC"]:.4f} PR_AUC: {m["PR_AUC"]:.4f} '
838
  f'ACC: {m["ACC"]:.4f} BACC: {m["BACC"]:.4f} '
839
  f'PREC: {m["PREC"]:.4f} TPR: {m["TPR"]:.4f} '
@@ -856,12 +861,12 @@ def main():
856
  fig.savefig(os.path.join(args.output_dir, f'loss_curve_fold{fold_i}.png'), dpi=150)
857
  plt.close(fig)
858
 
859
- # Save metrics history
860
  if metric_history:
861
  metric_df = pd.DataFrame(metric_history)
862
  metric_df.to_csv(os.path.join(args.output_dir, f'metrics_fold{fold_i}.csv'), index=False)
863
 
864
- # Plot ROC curve (using best model's predictions)
865
  fig, ax = plt.subplots(figsize=(6, 6))
866
  ax.plot(best_fpr, best_tpr, linewidth=2, label=f'AUROC = {best_auc:.4f}')
867
  ax.plot([0, 1], [0, 1], 'k--', linewidth=1, alpha=0.5)
@@ -869,7 +874,7 @@ def main():
869
  ax.set_ylim([0, 1.02])
870
  ax.set_xlabel('False Positive Rate')
871
  ax.set_ylabel('True Positive Rate')
872
- ax.set_title(f'ROC Curve - Fold {fold_i + 1}')
873
  ax.legend(loc='lower right')
874
  ax.grid(True, alpha=0.3)
875
  fig.tight_layout()
@@ -881,13 +886,13 @@ def main():
881
  with open(fpr_tpr_path, 'wb') as fp:
882
  pickle.dump((best_fpr, best_tpr), fp)
883
 
884
- print_sys(f'Fold {fold_i + 1} best AUROC: {best_auc:.4f}')
885
  all_fold_aucs.append(best_auc)
886
 
887
  # Summary
888
  print_sys(f'\n{"=" * 60}')
889
- print_sys(f'All folds AUROC: {all_fold_aucs}')
890
- print_sys(f'Mean AUROC: {np.mean(all_fold_aucs):.4f} +/- {np.std(all_fold_aucs):.4f}')
891
  print_sys('Training complete.')
892
 
893
 
 
610
  default=os.path.join(SCRIPT_DIR, 'data_summary'))
611
  parser.add_argument('--label_file', type=str, default='new_labels_0_10.csv')
612
  parser.add_argument('--cell_file', type=str, default='new_cell_features_954.csv')
613
+ parser.add_argument('--validation_label_file', '--test_label_file',
614
+ dest='validation_label_file', type=str,
615
+ default='test_data/independent_input.csv',
616
+ help='Validation labels used for checkpoint selection; --test_label_file is a legacy alias')
617
+ parser.add_argument('--validation_cell_file', '--test_cell_file',
618
+ dest='validation_cell_file', type=str,
619
+ default='test_data/independent_cell_features_954.csv',
620
+ help='Validation cell features; --test_cell_file is a legacy alias')
621
 
622
  # Training args
623
  parser.add_argument('--batch_size', type=int, default=256)
 
625
  parser.add_argument('--epochs', type=int, default=100)
626
  parser.add_argument('--n_folds', type=int, default=5)
627
  parser.add_argument('--domain_gen', action='store_true', default=True,
628
+ help='Domain generalization: train full set, validate on a separate set for checkpoint selection')
629
  parser.add_argument('--no_domain_gen', dest='domain_gen', action='store_false')
630
  parser.add_argument('--eval_interval', type=int, default=5)
631
  parser.add_argument('--output_dir', type=str,
 
655
  train_cell_df = load_cell_features(train_cell_file)
656
  print_sys(f'Loaded train cell features: {train_cell_df.shape}')
657
 
658
+ validation_cell_file = os.path.join(args.data_dir, args.validation_cell_file)
659
+ validation_cell_df = load_cell_features(validation_cell_file)
660
+ print_sys(f'Loaded validation cell features: {validation_cell_df.shape}')
661
 
662
  # Combine all cell lines and remove duplicates
663
+ all_cell_df = pd.concat([train_cell_df, validation_cell_df])
664
  all_cell_df = all_cell_df[~all_cell_df.index.duplicated(keep='first')]
665
  print_sys(f'Total unique cell lines: {len(all_cell_df)}')
666
 
 
695
  train_labels = pd.read_csv(train_label_path)
696
  print_sys(f'Train labels: {len(train_labels)} samples')
697
 
698
+ validation_label_path = os.path.join(args.data_dir, args.validation_label_file)
699
+ validation_labels = pd.read_csv(validation_label_path)
700
+ print_sys(f'Validation labels: {len(validation_labels)} samples')
701
 
702
  all_smiles = set(
703
  train_labels['drug1'].tolist() + train_labels['drug2'].tolist()
704
+ + validation_labels['drug1'].tolist() + validation_labels['drug2'].tolist()
705
  )
706
  smile_graph = {}
707
  failed = []
 
741
  cell_emb_dict=cell_emb_dict, smile_graph=smile_graph,
742
  )
743
 
744
+ # Validation datasets (separate set used for checkpoint selection)
745
+ # Preserve legacy test_* cache IDs to reuse existing preprocessed datasets.
746
+ drug1_validation = validation_labels['drug1'].tolist()
747
+ drug2_validation = validation_labels['drug2'].tolist()
748
+ cells_validation = validation_labels['cell'].tolist()
749
+ labels_validation = validation_labels['label'].tolist()
750
 
751
+ validation_d1_dataset = TestbedDataset(
752
  root=args.data_dir, dataset='test_drug1_performer_module',
753
+ xd=drug1_validation, xt=cells_validation, y=labels_validation,
754
  cell_emb_dict=cell_emb_dict, smile_graph=smile_graph,
755
  )
756
+ validation_d2_dataset = TestbedDataset(
757
  root=args.data_dir, dataset='test_drug2_performer_module',
758
+ xd=drug2_validation, xt=cells_validation, y=labels_validation,
759
  cell_emb_dict=cell_emb_dict, smile_graph=smile_graph,
760
  )
761
 
 
771
  print_sys(f'{"=" * 60}')
772
 
773
  if args.domain_gen:
774
+ # Domain generalization: train on full training set, select on validation set
775
  d1_train_data = train_d1_dataset
776
+ d1_validation_data = validation_d1_dataset
777
  d2_train_data = train_d2_dataset
778
+ d2_validation_data = validation_d2_dataset
779
  else:
780
  # Standard k-fold cross-validation
781
+ validation_num = random_num[pot * fold_i : pot * (fold_i + 1)]
782
  train_num = (
783
  random_num[: pot * fold_i] + random_num[pot * (fold_i + 1) :]
784
  )
785
  d1_train_data = train_d1_dataset[train_num]
786
+ d1_validation_data = train_d1_dataset[validation_num]
787
  d2_train_data = train_d2_dataset[train_num]
788
+ d2_validation_data = train_d2_dataset[validation_num]
789
 
790
  d1_loader_train = DataLoader(
791
  d1_train_data, batch_size=args.batch_size, shuffle=None, pin_memory=True,
792
  )
793
+ d1_loader_validation = DataLoader(
794
+ d1_validation_data, batch_size=args.batch_size, shuffle=None, pin_memory=True,
795
  )
796
  d2_loader_train = DataLoader(
797
  d2_train_data, batch_size=args.batch_size, shuffle=None, pin_memory=True,
798
  )
799
+ d2_loader_validation = DataLoader(
800
+ d2_validation_data, batch_size=args.batch_size, shuffle=None, pin_memory=True,
801
  )
802
 
803
  model = GCNNet(cell_emb_dim=cell_emb_dim).to(device)
 
824
  loss_history.append(avg_loss)
825
 
826
  if epoch % args.eval_interval == 0:
827
+ T, S, Y = predict(model, device, d1_loader_validation, d2_loader_validation)
828
  m = evaluate_metrics(T, S, Y)
829
  metric_history.append({'epoch': epoch, **m})
830
 
 
835
  args.output_dir, f'best_model_fold{fold_i}.pt'
836
  )
837
  torch.save(model.state_dict(), save_path)
838
+ print_sys(f'** New best validation AUROC: {best_auc:.4f} (saved)')
839
 
840
  print_sys(
841
+ f'Epoch {epoch} | Validation | '
842
  f'AUROC: {m["AUC"]:.4f} PR_AUC: {m["PR_AUC"]:.4f} '
843
  f'ACC: {m["ACC"]:.4f} BACC: {m["BACC"]:.4f} '
844
  f'PREC: {m["PREC"]:.4f} TPR: {m["TPR"]:.4f} '
 
861
  fig.savefig(os.path.join(args.output_dir, f'loss_curve_fold{fold_i}.png'), dpi=150)
862
  plt.close(fig)
863
 
864
+ # Save validation metrics history (retain the existing CSV schema)
865
  if metric_history:
866
  metric_df = pd.DataFrame(metric_history)
867
  metric_df.to_csv(os.path.join(args.output_dir, f'metrics_fold{fold_i}.csv'), index=False)
868
 
869
+ # Plot validation ROC curve (using the selected model's predictions)
870
  fig, ax = plt.subplots(figsize=(6, 6))
871
  ax.plot(best_fpr, best_tpr, linewidth=2, label=f'AUROC = {best_auc:.4f}')
872
  ax.plot([0, 1], [0, 1], 'k--', linewidth=1, alpha=0.5)
 
874
  ax.set_ylim([0, 1.02])
875
  ax.set_xlabel('False Positive Rate')
876
  ax.set_ylabel('True Positive Rate')
877
+ ax.set_title(f'Validation ROC Curve - Fold {fold_i + 1}')
878
  ax.legend(loc='lower right')
879
  ax.grid(True, alpha=0.3)
880
  fig.tight_layout()
 
886
  with open(fpr_tpr_path, 'wb') as fp:
887
  pickle.dump((best_fpr, best_tpr), fp)
888
 
889
+ print_sys(f'Fold {fold_i + 1} best validation AUROC: {best_auc:.4f}')
890
  all_fold_aucs.append(best_auc)
891
 
892
  # Summary
893
  print_sys(f'\n{"=" * 60}')
894
+ print_sys(f'All folds validation AUROC: {all_fold_aucs}')
895
+ print_sys(f'Mean validation AUROC: {np.mean(all_fold_aucs):.4f} +/- {np.std(all_fold_aucs):.4f}')
896
  print_sys('Training complete.')
897
 
898
 
BioMod/drug_response/train_drug_performer_module.py CHANGED
@@ -471,18 +471,18 @@ def metadata_generate(
471
 
472
 
473
  def data_split(data_idx: list, ratio: float = 0.95):
474
- """Stratified by cancer type"""
475
- train_idx, test_idx = [], []
476
  for each_type in TCGA_LABEL_SET:
477
  data_subtype_idx = [item for item in data_idx if item[-1] == each_type]
478
  if len(data_subtype_idx) == 0:
479
  continue
480
  k = max(1, int(ratio * len(data_subtype_idx)))
481
  train_list = random.sample(data_subtype_idx, k)
482
- test_list = [item for item in data_subtype_idx if item not in train_list]
483
  train_idx += train_list
484
- test_idx += test_list
485
- return train_idx, test_idx
486
 
487
 
488
  def feature_extract(
@@ -924,18 +924,18 @@ def main():
924
  seed=args.shuffle_seed,
925
  )
926
 
927
- # 3) Divide the data set
928
  print_sys("\n[Step 3/6] Splitting dataset...")
929
- train_idx, test_idx = data_split(data_idx)
930
- print_sys(f"Train samples: {len(train_idx)}, Test samples: {len(test_idx)}")
931
 
932
  # 4) Extract features
933
  print_sys("\n[Step 4/6] Extracting features...")
934
  X_drug_train, Y_train, train_cancer_info, train_index_list = feature_extract(
935
  train_idx, drug_feature, args.max_atoms
936
  )
937
- X_drug_test, Y_test, test_cancer_info, test_index_list = feature_extract(
938
- test_idx, drug_feature, args.max_atoms
939
  )
940
 
941
  # 5) Build a data set
@@ -943,8 +943,8 @@ def main():
943
  train_ds = DrugPerformerModuleDataset(
944
  X_drug_train, Y_train, train_index_list, cell_emb_dict
945
  )
946
- test_ds = DrugPerformerModuleDataset(
947
- X_drug_test, Y_test, test_index_list, cell_emb_dict
948
  )
949
 
950
  drug_dim = X_drug_train[0][0].shape[-1]
@@ -957,8 +957,8 @@ def main():
957
  num_workers=num_workers,
958
  pin_memory=torch.cuda.is_available(),
959
  )
960
- test_loader = DataLoader(
961
- test_ds,
962
  batch_size=args.batch_size,
963
  shuffle=False,
964
  num_workers=num_workers,
@@ -1002,7 +1002,7 @@ def main():
1002
 
1003
  for epoch in range(1, args.epochs + 1):
1004
  train_loss = train_epoch(model, train_loader, optimizer, criterion, device)
1005
- preds, trues, pcc = eval_epoch(model, test_loader, device)
1006
 
1007
  scheduler.step()
1008
  current_lr = optimizer.param_groups[0]["lr"]
@@ -1010,7 +1010,7 @@ def main():
1010
  print_sys(
1011
  f"Epoch {epoch:03d}/{args.epochs} | "
1012
  f"train_loss={train_loss:.4f} | "
1013
- f"val_pcc={pcc:.4f} | "
1014
  f"lr={current_lr:.6f}"
1015
  )
1016
 
@@ -1021,7 +1021,7 @@ def main():
1021
  best_epoch = epoch
1022
  patience_counter = 0
1023
 
1024
- # Save the best model
1025
  model_save_path = os.path.join(args.save_dir, "best_model.pth")
1026
  torch.save(
1027
  {
@@ -1035,7 +1035,7 @@ def main():
1035
  },
1036
  model_save_path,
1037
  )
1038
- print_sys(f" → Best model saved! PCC improved to {best_pcc:.4f}")
1039
  else:
1040
  patience_counter += 1
1041
  if patience_counter >= args.patience:
@@ -1049,12 +1049,12 @@ def main():
1049
  print_sys("Training completed!")
1050
  print_sys("=" * 70)
1051
  print_sys(f"Best epoch: {best_epoch}")
1052
- print_sys(f"Best PCC: {best_pcc:.4f}")
1053
 
1054
  if best_pred is not None:
1055
  result_path = os.path.join(args.save_dir, "predictions.npz")
1056
- np.savez(result_path, pred=best_pred, test=best_true)
1057
- print_sys(f"Predictions saved to: {result_path}")
1058
 
1059
  print_sys(f"Best model saved to: {os.path.join(args.save_dir, 'best_model.pth')}")
1060
  print_sys("=" * 70)
 
471
 
472
 
473
  def data_split(data_idx: list, ratio: float = 0.95):
474
+ """Return training and validation splits stratified by cancer type."""
475
+ train_idx, validation_idx = [], []
476
  for each_type in TCGA_LABEL_SET:
477
  data_subtype_idx = [item for item in data_idx if item[-1] == each_type]
478
  if len(data_subtype_idx) == 0:
479
  continue
480
  k = max(1, int(ratio * len(data_subtype_idx)))
481
  train_list = random.sample(data_subtype_idx, k)
482
+ validation_list = [item for item in data_subtype_idx if item not in train_list]
483
  train_idx += train_list
484
+ validation_idx += validation_list
485
+ return train_idx, validation_idx
486
 
487
 
488
  def feature_extract(
 
924
  seed=args.shuffle_seed,
925
  )
926
 
927
+ # 3) Split training and validation data; validation is used for model selection
928
  print_sys("\n[Step 3/6] Splitting dataset...")
929
+ train_idx, validation_idx = data_split(data_idx)
930
+ print_sys(f"Train samples: {len(train_idx)}, Validation samples: {len(validation_idx)}")
931
 
932
  # 4) Extract features
933
  print_sys("\n[Step 4/6] Extracting features...")
934
  X_drug_train, Y_train, train_cancer_info, train_index_list = feature_extract(
935
  train_idx, drug_feature, args.max_atoms
936
  )
937
+ X_drug_validation, Y_validation, validation_cancer_info, validation_index_list = feature_extract(
938
+ validation_idx, drug_feature, args.max_atoms
939
  )
940
 
941
  # 5) Build a data set
 
943
  train_ds = DrugPerformerModuleDataset(
944
  X_drug_train, Y_train, train_index_list, cell_emb_dict
945
  )
946
+ validation_ds = DrugPerformerModuleDataset(
947
+ X_drug_validation, Y_validation, validation_index_list, cell_emb_dict
948
  )
949
 
950
  drug_dim = X_drug_train[0][0].shape[-1]
 
957
  num_workers=num_workers,
958
  pin_memory=torch.cuda.is_available(),
959
  )
960
+ validation_loader = DataLoader(
961
+ validation_ds,
962
  batch_size=args.batch_size,
963
  shuffle=False,
964
  num_workers=num_workers,
 
1002
 
1003
  for epoch in range(1, args.epochs + 1):
1004
  train_loss = train_epoch(model, train_loader, optimizer, criterion, device)
1005
+ preds, trues, pcc = eval_epoch(model, validation_loader, device)
1006
 
1007
  scheduler.step()
1008
  current_lr = optimizer.param_groups[0]["lr"]
 
1010
  print_sys(
1011
  f"Epoch {epoch:03d}/{args.epochs} | "
1012
  f"train_loss={train_loss:.4f} | "
1013
+ f"validation_pcc={pcc:.4f} | "
1014
  f"lr={current_lr:.6f}"
1015
  )
1016
 
 
1021
  best_epoch = epoch
1022
  patience_counter = 0
1023
 
1024
+ # Save the best model selected by validation PCC
1025
  model_save_path = os.path.join(args.save_dir, "best_model.pth")
1026
  torch.save(
1027
  {
 
1035
  },
1036
  model_save_path,
1037
  )
1038
+ print_sys(f" → Best model saved! Validation PCC improved to {best_pcc:.4f}")
1039
  else:
1040
  patience_counter += 1
1041
  if patience_counter >= args.patience:
 
1049
  print_sys("Training completed!")
1050
  print_sys("=" * 70)
1051
  print_sys(f"Best epoch: {best_epoch}")
1052
+ print_sys(f"Best validation PCC: {best_pcc:.4f}")
1053
 
1054
  if best_pred is not None:
1055
  result_path = os.path.join(args.save_dir, "predictions.npz")
1056
+ np.savez(result_path, pred=best_pred, validation=best_true)
1057
+ print_sys(f"Validation predictions saved to: {result_path}")
1058
 
1059
  print_sys(f"Best model saved to: {os.path.join(args.save_dir, 'best_model.pth')}")
1060
  print_sys("=" * 70)
BioMod/toturials/drug_combination.ipynb CHANGED
@@ -8,7 +8,10 @@
8
  "\n",
9
  "This notebook prepares a runnable demo command for `drug_combination/train_drug_combination_performer_module.py` using configurable absolute paths.\n",
10
  "\n",
11
- "The drug-combination data are not present in the current project yet, so `RUN_TRAINING` is set to `False` by default. After placing `data_summary/` under the task directory, switch `RUN_TRAINING = True` in the command cell to start training."
 
 
 
12
  ]
13
  },
14
  {
@@ -124,8 +127,8 @@
124
  " 'train_labels': DATA_DIR / 'new_labels_0_10.csv',\n",
125
  " 'train_cell_features': DATA_DIR / 'new_cell_features_954.csv',\n",
126
  " 'drug_smiles': DATA_DIR / 'smiles_0_10.csv',\n",
127
- " 'independent_test_labels': DATA_DIR / 'test_data' / 'independent_input.csv',\n",
128
- " 'independent_test_cell_features': DATA_DIR / 'test_data' / 'independent_cell_features_954.csv',\n",
129
  "}\n",
130
  "\n",
131
  "def status_table(paths):\n",
@@ -378,7 +381,7 @@
378
  "\n",
379
  "\n",
380
  "data_overview = []\n",
381
- "train_labels = test_labels = None\n",
382
  "if expected_data_files['train_labels'].exists():\n",
383
  " train_labels = read_label_table(expected_data_files['train_labels'])\n",
384
  " data_overview.append({\n",
@@ -391,17 +394,17 @@
391
  " 'unique_drug2': train_labels['drug2'].nunique(),\n",
392
  " 'unique_pairs': train_labels[['drug1', 'drug2']].drop_duplicates().shape[0],\n",
393
  " })\n",
394
- "if expected_data_files['independent_test_labels'].exists():\n",
395
- " test_labels = read_label_table(expected_data_files['independent_test_labels'])\n",
396
  " data_overview.append({\n",
397
- " 'split': 'independent_test',\n",
398
- " 'rows': len(test_labels),\n",
399
- " 'positive': int((test_labels['label'] == 1).sum()),\n",
400
- " 'negative': int((test_labels['label'] == 0).sum()),\n",
401
- " 'unique_cells': test_labels['cell'].nunique(),\n",
402
- " 'unique_drug1': test_labels['drug1'].nunique(),\n",
403
- " 'unique_drug2': test_labels['drug2'].nunique(),\n",
404
- " 'unique_pairs': test_labels[['drug1', 'drug2']].drop_duplicates().shape[0],\n",
405
  " })\n",
406
  "\n",
407
  "data_overview_df = pd.DataFrame(data_overview)\n",
@@ -414,8 +417,8 @@
414
  " label_rows = []\n",
415
  " if train_labels is not None:\n",
416
  " label_rows.extend([{'split': 'train', 'label': int(k), 'count': int(v)} for k, v in train_labels['label'].value_counts().sort_index().items()])\n",
417
- " if test_labels is not None:\n",
418
- " label_rows.extend([{'split': 'independent_test', 'label': int(k), 'count': int(v)} for k, v in test_labels['label'].value_counts().sort_index().items()])\n",
419
  " label_df = pd.DataFrame(label_rows)\n",
420
  " label_df.to_csv(PLOT_DIR / 'drug_combination_label_counts.csv', index=False)\n",
421
  "\n",
@@ -748,4 +751,4 @@
748
  },
749
  "nbformat": 4,
750
  "nbformat_minor": 5
751
- }
 
8
  "\n",
9
  "This notebook prepares a runnable demo command for `drug_combination/train_drug_combination_performer_module.py` using configurable absolute paths.\n",
10
  "\n",
11
+ "The drug-combination data are not present in the current project yet, so `RUN_TRAINING` is set to `False` by default. After placing `data_summary/` under the task directory, switch `RUN_TRAINING = True` in the command cell to start training.\n",
12
+ "\n",
13
+ "The validation set is used for checkpoint selection. Reported metrics describe\n",
14
+ "validation performance; this workflow does not produce a separate final test evaluation.\n"
15
  ]
16
  },
17
  {
 
127
  " 'train_labels': DATA_DIR / 'new_labels_0_10.csv',\n",
128
  " 'train_cell_features': DATA_DIR / 'new_cell_features_954.csv',\n",
129
  " 'drug_smiles': DATA_DIR / 'smiles_0_10.csv',\n",
130
+ " 'validation_labels': DATA_DIR / 'test_data' / 'independent_input.csv',\n",
131
+ " 'validation_cell_features': DATA_DIR / 'test_data' / 'independent_cell_features_954.csv',\n",
132
  "}\n",
133
  "\n",
134
  "def status_table(paths):\n",
 
381
  "\n",
382
  "\n",
383
  "data_overview = []\n",
384
+ "train_labels = validation_labels = None\n",
385
  "if expected_data_files['train_labels'].exists():\n",
386
  " train_labels = read_label_table(expected_data_files['train_labels'])\n",
387
  " data_overview.append({\n",
 
394
  " 'unique_drug2': train_labels['drug2'].nunique(),\n",
395
  " 'unique_pairs': train_labels[['drug1', 'drug2']].drop_duplicates().shape[0],\n",
396
  " })\n",
397
+ "if expected_data_files['validation_labels'].exists():\n",
398
+ " validation_labels = read_label_table(expected_data_files['validation_labels'])\n",
399
  " data_overview.append({\n",
400
+ " 'split': 'validation',\n",
401
+ " 'rows': len(validation_labels),\n",
402
+ " 'positive': int((validation_labels['label'] == 1).sum()),\n",
403
+ " 'negative': int((validation_labels['label'] == 0).sum()),\n",
404
+ " 'unique_cells': validation_labels['cell'].nunique(),\n",
405
+ " 'unique_drug1': validation_labels['drug1'].nunique(),\n",
406
+ " 'unique_drug2': validation_labels['drug2'].nunique(),\n",
407
+ " 'unique_pairs': validation_labels[['drug1', 'drug2']].drop_duplicates().shape[0],\n",
408
  " })\n",
409
  "\n",
410
  "data_overview_df = pd.DataFrame(data_overview)\n",
 
417
  " label_rows = []\n",
418
  " if train_labels is not None:\n",
419
  " label_rows.extend([{'split': 'train', 'label': int(k), 'count': int(v)} for k, v in train_labels['label'].value_counts().sort_index().items()])\n",
420
+ " if validation_labels is not None:\n",
421
+ " label_rows.extend([{'split': 'validation', 'label': int(k), 'count': int(v)} for k, v in validation_labels['label'].value_counts().sort_index().items()])\n",
422
  " label_df = pd.DataFrame(label_rows)\n",
423
  " label_df.to_csv(PLOT_DIR / 'drug_combination_label_counts.csv', index=False)\n",
424
  "\n",
 
751
  },
752
  "nbformat": 4,
753
  "nbformat_minor": 5
754
+ }
BioMod/toturials/drug_response_prediction.ipynb CHANGED
@@ -8,7 +8,10 @@
8
  "\n",
9
  "This notebook is a tutorial version of the drug-response launcher based on `train_drug_performer_module.py`. It uses the weight0407 PerformerModule backbone, drug graph features, and CCLE/GDSC response data.\n",
10
  "\n",
11
- "By default, the notebook does not relaunch the full 300-epoch training job. It checks the inputs, builds the exact command, reads the existing `weight0407/drug` outputs, computes metrics, and writes a small tutorial scatter plot. To retrain, set `RUN_TRAINING = True` in the training cell."
 
 
 
12
  ]
13
  },
14
  {
@@ -325,13 +328,19 @@
325
  "print('prediction keys:', pred_npz.files)\n",
326
  "\n",
327
  "pred = pred_npz['pred'] if 'pred' in pred_npz.files else pred_npz['preds']\n",
328
- "true = pred_npz['test'] if 'test' in pred_npz.files else pred_npz['true']\n",
 
 
 
 
 
 
329
  "\n",
330
  "pcc = pearsonr(pred, true)[0]\n",
331
  "mae = np.mean(np.abs(pred - true))\n",
332
  "rmse = np.sqrt(np.mean((pred - true) ** 2))\n",
333
  "summary = pd.DataFrame([{\n",
334
- " 'n_test': len(pred),\n",
335
  " 'pcc': pcc,\n",
336
  " 'mae': mae,\n",
337
  " 'rmse': rmse,\n",
@@ -388,8 +397,8 @@
388
  " }\n",
389
  "\n",
390
  "\n",
391
- "def reconstruct_test_metadata(data_root: Path, n_expected: int, ratio: float = 0.95, seed: int = 0) -> pd.DataFrame:\n",
392
- " \"\"\"Rebuild the test split metadata using the same deterministic split as the training script.\"\"\"\n",
393
  " drug_info_file = data_root / 'GDSC' / '1.Drug_listMon Jun 24 09_00_55 2019.csv'\n",
394
  " cell_line_info_file = data_root / 'CCLE' / 'Cell_lines_annotations_20181226.txt'\n",
395
  " drug_feature_dir = data_root / 'GDSC' / 'drug_graph_feat'\n",
@@ -435,16 +444,16 @@
435
  " data_idx.append((each_cellline, pubchem_id, float(value), cellline2cancertype[each_cellline]))\n",
436
  "\n",
437
  " rng = random.Random(seed)\n",
438
- " test_idx = []\n",
439
  " for each_type in TCGA_LABEL_SET:\n",
440
  " subtype = [item for item in data_idx if item[-1] == each_type]\n",
441
  " if not subtype:\n",
442
  " continue\n",
443
  " k = max(1, int(ratio * len(subtype)))\n",
444
  " train_list = rng.sample(subtype, k)\n",
445
- " test_idx.extend([item for item in subtype if item not in train_list])\n",
446
  "\n",
447
- " reconstructed = pd.DataFrame(test_idx, columns=['cell_line_id', 'drug_id', 'true_from_metadata', 'cancer_type'])\n",
448
  " if len(reconstructed) != n_expected:\n",
449
  " raise ValueError(f'Reconstructed {len(reconstructed)} rows, but predictions contain {n_expected} rows.')\n",
450
  " return reconstructed[['drug_id', 'cell_line_id', 'cancer_type']]\n",
@@ -462,10 +471,10 @@
462
  "\n",
463
  "if metadata_missing:\n",
464
  " try:\n",
465
- " recovered_meta = reconstruct_test_metadata(DATA_ROOT, n_expected=len(analysis_df), ratio=0.95, seed=0)\n",
466
  " for col in metadata_cols:\n",
467
  " analysis_df[col] = recovered_meta[col].to_numpy()\n",
468
- " print('Recovered test metadata from the original GDSC/CCLE split.')\n",
469
  " except Exception as exc:\n",
470
  " print('Metadata recovery skipped:', repr(exc))\n",
471
  " for col in metadata_cols:\n",
 
8
  "\n",
9
  "This notebook is a tutorial version of the drug-response launcher based on `train_drug_performer_module.py`. It uses the weight0407 PerformerModule backbone, drug graph features, and CCLE/GDSC response data.\n",
10
  "\n",
11
+ "By default, the notebook does not relaunch the full 300-epoch training job. It checks the inputs, builds the exact command, reads the existing `weight0407/drug` outputs, computes metrics, and writes a small tutorial scatter plot. To retrain, set `RUN_TRAINING = True` in the training cell.\n",
12
+ "\n",
13
+ "The validation set is used for checkpoint selection. Reported metrics describe\n",
14
+ "validation performance; this workflow does not produce a separate final test evaluation.\n"
15
  ]
16
  },
17
  {
 
328
  "print('prediction keys:', pred_npz.files)\n",
329
  "\n",
330
  "pred = pred_npz['pred'] if 'pred' in pred_npz.files else pred_npz['preds']\n",
331
+ "# Prefer the validation key; accept historical output files as well.\n",
332
+ "if 'validation' in pred_npz.files:\n",
333
+ " true = pred_npz['validation']\n",
334
+ "elif 'test' in pred_npz.files:\n",
335
+ " true = pred_npz['test']\n",
336
+ "else:\n",
337
+ " true = pred_npz['true']\n",
338
  "\n",
339
  "pcc = pearsonr(pred, true)[0]\n",
340
  "mae = np.mean(np.abs(pred - true))\n",
341
  "rmse = np.sqrt(np.mean((pred - true) ** 2))\n",
342
  "summary = pd.DataFrame([{\n",
343
+ " 'n_validation': len(pred),\n",
344
  " 'pcc': pcc,\n",
345
  " 'mae': mae,\n",
346
  " 'rmse': rmse,\n",
 
397
  " }\n",
398
  "\n",
399
  "\n",
400
+ "def reconstruct_validation_metadata(data_root: Path, n_expected: int, ratio: float = 0.95, seed: int = 0) -> pd.DataFrame:\n",
401
+ " \"\"\"Rebuild the validation split metadata using the same deterministic split as the training script.\"\"\"\n",
402
  " drug_info_file = data_root / 'GDSC' / '1.Drug_listMon Jun 24 09_00_55 2019.csv'\n",
403
  " cell_line_info_file = data_root / 'CCLE' / 'Cell_lines_annotations_20181226.txt'\n",
404
  " drug_feature_dir = data_root / 'GDSC' / 'drug_graph_feat'\n",
 
444
  " data_idx.append((each_cellline, pubchem_id, float(value), cellline2cancertype[each_cellline]))\n",
445
  "\n",
446
  " rng = random.Random(seed)\n",
447
+ " validation_idx = []\n",
448
  " for each_type in TCGA_LABEL_SET:\n",
449
  " subtype = [item for item in data_idx if item[-1] == each_type]\n",
450
  " if not subtype:\n",
451
  " continue\n",
452
  " k = max(1, int(ratio * len(subtype)))\n",
453
  " train_list = rng.sample(subtype, k)\n",
454
+ " validation_idx.extend([item for item in subtype if item not in train_list])\n",
455
  "\n",
456
+ " reconstructed = pd.DataFrame(validation_idx, columns=['cell_line_id', 'drug_id', 'true_from_metadata', 'cancer_type'])\n",
457
  " if len(reconstructed) != n_expected:\n",
458
  " raise ValueError(f'Reconstructed {len(reconstructed)} rows, but predictions contain {n_expected} rows.')\n",
459
  " return reconstructed[['drug_id', 'cell_line_id', 'cancer_type']]\n",
 
471
  "\n",
472
  "if metadata_missing:\n",
473
  " try:\n",
474
+ " recovered_meta = reconstruct_validation_metadata(DATA_ROOT, n_expected=len(analysis_df), ratio=0.95, seed=0)\n",
475
  " for col in metadata_cols:\n",
476
  " analysis_df[col] = recovered_meta[col].to_numpy()\n",
477
+ " print('Recovered validation metadata from the original GDSC/CCLE split.')\n",
478
  " except Exception as exc:\n",
479
  " print('Metadata recovery skipped:', repr(exc))\n",
480
  " for col in metadata_cols:\n",