| """ |
| dataset.py - ๋ฐ์ดํฐ์
ํด๋์ค์ ๋ฐ์ดํฐ ์ฒ๋ฆฌ ๋ก์ง |
| """ |
| import os |
| import sys |
| import numpy as np |
| import pandas as pd |
| from tqdm.auto import tqdm |
| from typing import Dict, List, Optional, Tuple, Union |
|
|
| import torch |
| from torch.utils.data import Dataset, DataLoader |
| from transformers import AutoTokenizer |
|
|
| |
| _current_dir = os.path.dirname(os.path.abspath(__file__)) |
| _project_root = os.path.dirname(os.path.dirname(_current_dir)) |
| if _project_root not in sys.path: |
| sys.path.insert(0, _project_root) |
| |
| from admet_ft._modules.utils import ( |
| get_task_list, load_vocabs, |
| DIDB_FILTER_COLS, LOAD_DATA_PATH, LOAD_PREPARED_DATA_PATH |
| ) |
| from admet_ft._modules.scaler import ( |
| save_scaler, apply_scaler, save_scaler_power, apply_scaler_power, |
| save_scaler_minmax, apply_scaler_minmax, |
| save_scaler_adaptive, apply_scaler_adaptive |
| ) |
|
|
|
|
| class ChemMultiTaskDataset(Dataset): |
| """ChemBERTa ๊ธฐ๋ฐ Multi-Task Learning์ ์ํ ๋ฐ์ดํฐ์
""" |
| def __init__( |
| self, |
| df_dataset: pd.DataFrame, |
| all_vocabs: Dict[str, Dict[str, int]], |
| model_name: Optional[str] = None, |
| task_type: str = 'cls', |
| ): |
| """ |
| Args: |
| df_dataset: ์
๋ ฅ ๋ฐ์ดํฐํ๋ ์ |
| all_vocabs: ๋ถ๋ฅ ํ์คํฌ๋ฅผ ์ํ ๋ ์ด๋ธ ๋งคํ ์ฌ์ |
| model_name: ์ฌ์ฉํ transformer ๋ชจ๋ธ ์ด๋ฆ (ChemBERTa) |
| task_type: 'cls' ๋๋ 'reg' (๋ถ๋ฅ ๋๋ ํ๊ท) |
| """ |
| self.model_name = model_name |
| self.task_type = task_type |
| |
| |
| self.y_cols = get_task_list(task_type) |
| self.all_vocabs = all_vocabs |
|
|
| |
| self.df = df_dataset.copy() |
|
|
| |
| self.tokenizer = AutoTokenizer.from_pretrained(self.model_name) |
|
|
| def __len__(self): |
| """๋ฐ์ดํฐ์
๊ธธ์ด ๋ฐํ""" |
| return len(self.df) |
|
|
| def __getitem__(self, idx): |
| """๋ฐ์ดํฐ์
์์ idx๋ฒ์งธ ํญ๋ชฉ ๋ฐํ""" |
| row = self.df.iloc[idx] |
|
|
| |
| objs = self.tokenizer(row['SMILES'], padding='max_length', max_length=510, |
| truncation=True, return_tensors='pt') |
|
|
| |
| if self.task_type == 'cls': |
| labels = [] |
| for target in self.y_cols: |
| cls_value = str(row[target]).lower() |
| if target in self.all_vocabs and cls_value in self.all_vocabs[target]: |
| label_id = self.all_vocabs[target][cls_value] |
| else: |
| |
| label_id = 0 |
| labels.append(label_id) |
| else: |
| labels = [row[target] for target in self.y_cols] |
|
|
| labels = torch.tensor(labels, dtype=torch.float32) |
|
|
| |
| return {'input_ids': objs['input_ids'].squeeze(0), |
| 'attention_mask': objs['attention_mask'].squeeze(0), |
| 'labels': labels, |
| 'smiles': row['SMILES']} |
|
|
|
|
| class ChemMultiTaskDataModule: |
| def __init__( |
| self, |
| batch_size: int = 32, |
| scaling: bool = True, |
| scaler_path: str = "./results/scaler", |
| task_type: str = 'cls', |
| model_name: Optional[str] = None, |
| missing_label_strategy: str = 'any', |
| data_type: str = 'didb', |
| load_type: str = 'default', |
| num_workers: int = 2, |
| output_dir: str = "./results", |
| scaler_type: str = 'power' |
| ): |
| """ |
| Args: |
| data_folder: ๋ฐ์ดํฐ ํด๋ ๊ฒฝ๋ก |
| batch_size: ๋ฐฐ์น ํฌ๊ธฐ |
| scaling: ์ค์ผ์ผ๋ง ์ ์ฉ ์ฌ๋ถ |
| task_type: 'cls' ๋๋ 'reg' (๋ถ๋ฅ ๋๋ ํ๊ท) |
| model_name: ์ฌ์ฉํ transformer ๋ชจ๋ธ ์ด๋ฆ (ChemBERTa) |
| missing_label_strategy: |
| 'any' - (default) ํ ๊ฐ ์ด์์ ํด๋์ค์ ๋ผ๋ฒจ์ด ์์ผ๋ฉด ํฌํจ |
| 'all' - ๋ชจ๋ ํด๋์ค ๋ผ๋ฒจ์ด ์์ด์ผ ํฌํจ |
| """ |
| super().__init__() |
| self.batch_size = batch_size |
| self.model_name = model_name |
| self.task_type = task_type |
| self.missing_label_strategy = missing_label_strategy |
| self.data_type = data_type |
| self.load_type = load_type |
|
|
| self.scaling = scaling |
| self.scaler_path = scaler_path |
| self.output_dir = output_dir |
| self.scaler_type = scaler_type |
| self.scaler_file: Optional[str] = None |
|
|
| |
| self.num_workers = max(0, int(num_workers)) |
| world_size_env = os.environ.get("WORLD_SIZE", "1") |
| try: |
| world_size = int(world_size_env) |
| except ValueError: |
| world_size = 1 |
| world_size = max(1, world_size) |
| self.workers_per_rank = max(1, self.num_workers // world_size) if self.num_workers > 0 else 0 |
|
|
| |
| self.filter_cols = DIDB_FILTER_COLS |
|
|
| |
| if task_type == 'cls': |
| self.task_list = [f'{x}.cls' for x in self.filter_cols] |
| else: |
| self.task_list = self.filter_cols |
|
|
| |
| self.all_vocabs = None |
| self.train_dataset, self.valid_dataset, self.test_dataset = None, None, None |
|
|
| self.all_df = None |
| self.train_df, self.valid_df, self.test_df = None, None, None |
|
|
| |
| if load_type == 'default': |
| self.all_df = self._load_and_merge_data() |
| self.all_df = self._nan_column_filter(self.all_df, self.filter_cols, self.missing_label_strategy) |
|
|
| elif load_type == 'prepared': |
| self.train_df, self.valid_df, self.test_df = self._load_and_merge_prepared_data() |
|
|
| self.train_df = self._nan_column_filter(self.train_df, self.filter_cols, self.missing_label_strategy) |
| self.valid_df = self._nan_column_filter(self.valid_df, self.filter_cols, self.missing_label_strategy) |
| self.test_df = self._nan_column_filter(self.test_df, self.filter_cols, self.missing_label_strategy) |
|
|
| else: |
| raise ValueError(f"Unknown load_type: {load_type}") |
|
|
| |
| def _nan_column_filter(self, df_input: pd.DataFrame, filter_cols: List[str], filter_strategy: str = 'any') -> pd.DataFrame: |
| df_input = df_input[['SMILES'] + filter_cols].reset_index(drop=True) |
|
|
| if filter_strategy == 'all': |
| valid_rows = df_input[filter_cols].notna().all(axis=1) |
| elif filter_strategy == 'any': |
| valid_rows = df_input[filter_cols].notna().any(axis=1) |
| else: |
| raise ValueError(f"์ ์ ์๋ missing_label_strategy: {filter_strategy}") |
| |
| df_input = df_input[valid_rows].reset_index(drop=True) |
| return df_input |
|
|
|
|
| def _check_numeric_columns(self, df: pd.DataFrame) -> pd.DataFrame: |
| |
| if "SMILES" not in df.columns: |
| df["SMILES"] = "" |
| df["SMILES"] = df["SMILES"].fillna("").astype(str) |
|
|
| for col in DIDB_FILTER_COLS: |
| if col not in df.columns: |
| df[col] = np.nan |
| df[col] = pd.to_numeric(df[col], errors='coerce') |
|
|
| return df |
|
|
| def _load_and_merge_data(self): |
| if self.data_type == "didb": |
| path = LOAD_DATA_PATH["didb"] |
| elif self.data_type == "portal": |
| path = LOAD_DATA_PATH["portal"] |
| elif self.data_type == "half": |
| path = LOAD_DATA_PATH["all"] |
| elif self.data_type == "all": |
| path = LOAD_DATA_PATH["all"] |
| else: |
| raise ValueError(f"Unknown data_type: {self.data_type}") |
|
|
| |
| df = pd.read_csv(path) |
| df = self._check_numeric_columns(df) |
|
|
| if self.data_type == "half": |
| return df.sample(n=int(len(df) * 0.5), random_state=42) |
| else: |
| return df |
|
|
| def _load_and_merge_prepared_data(self): |
| if self.data_type == "didb": |
| path = LOAD_PREPARED_DATA_PATH["didb"] |
| elif self.data_type == "portal": |
| path = LOAD_PREPARED_DATA_PATH["portal"] |
| elif self.data_type == "half" or self.data_type == "all": |
| path = LOAD_PREPARED_DATA_PATH["all"] |
| elif self.data_type == "all_rating": |
| path = LOAD_PREPARED_DATA_PATH["all_rating"] |
| else: |
| raise ValueError(f"Unknown data_type: {self.data_type}") |
|
|
| |
| train_df, valid_df, test_df = pd.read_csv(os.path.join(path, "train.csv")), pd.read_csv(os.path.join(path, "valid.csv")), pd.read_csv(os.path.join(path, "test.csv")) |
| train_df, valid_df, test_df = self._check_numeric_columns(train_df), self._check_numeric_columns(valid_df), self._check_numeric_columns(test_df) |
|
|
| if self.data_type == "half": |
| train_df = train_df.sample(n=int(len(train_df) * 0.5), random_state=42) |
| valid_df = valid_df.sample(n=int(len(valid_df) * 0.5), random_state=42) |
|
|
| return train_df, valid_df, test_df |
|
|
| def setup(self): |
| """๋ฐ์ดํฐ์
์ค๋น ๋ฐ ๋ถํ """ |
| self.scaler_file = None |
|
|
| |
| if self.task_type == 'cls': |
| source_df = self.all_df if self.load_type == 'default' else self.train_df |
| cls_targets = [x for x in source_df.columns.tolist() if x.endswith('.cls')] |
| self.all_vocabs = load_vocabs(self.output_dir, cls_targets) |
| else: |
| self.all_vocabs = {} |
|
|
| |
| if self.scaler_type == 'power': |
| save_fn = save_scaler_power |
| apply_fn = apply_scaler_power |
| scaler_filename = 'scaler_power_config.csv' |
| elif self.scaler_type == 'minmax': |
| save_fn = save_scaler_minmax |
| apply_fn = apply_scaler_minmax |
| scaler_filename = 'scaler_minmax_config.csv' |
| elif self.scaler_type == 'adapt': |
| save_fn = save_scaler_adaptive |
| apply_fn = apply_scaler_adaptive |
| scaler_filename = 'scaler_adapt.csv' |
| else: |
| save_fn = save_scaler |
| apply_fn = apply_scaler |
| scaler_filename = 'scaler_config.csv' |
|
|
| if self.load_type == 'default': |
| |
| data_save_path = os.path.join(self.output_dir, "valid_test_split/") |
| os.makedirs(data_save_path, exist_ok=True) |
|
|
| if self.scaling: |
| if not self.scaler_path: |
| raise ValueError("Scaler path must be provided when scaling is enabled.") |
| scaler_file_path = os.path.join(self.scaler_path, scaler_filename) |
|
|
| |
| if self.scaler_type == 'adapt': |
| self.all_df = save_fn(self.all_df, scaler_path=scaler_file_path, feature_cols=self.filter_cols) |
| else: |
| self.all_df = save_fn(self.all_df, scaler_path=scaler_file_path) |
|
|
| T = int(len(self.all_df) * 0.7) |
| train_df = self.all_df.sample(n=T, random_state=42) |
| t_indexs = self.all_df.index.isin(train_df.index) |
| other_df = self.all_df[~t_indexs] |
| |
| V = int(len(other_df) * 0.5) |
| valid_df = other_df.sample(n=V, random_state=42) |
| v_indexs = other_df.index.isin(valid_df.index) |
| test_df = other_df[~v_indexs] |
|
|
| self.train_df = train_df |
| self.valid_df = valid_df |
| self.test_df = test_df |
|
|
| self.valid_df.to_csv(os.path.join(data_save_path, "valid.csv"), index=False) |
| self.test_df.to_csv(os.path.join(data_save_path, "test.csv"), index=False) |
|
|
| |
| self.train_dataset = ChemMultiTaskDataset( |
| self.train_df, self.all_vocabs, self.model_name, self.task_type) |
| |
| self.valid_dataset = ChemMultiTaskDataset( |
| self.valid_df, self.all_vocabs, self.model_name, self.task_type) |
| |
| self.test_dataset = ChemMultiTaskDataset( |
| self.test_df, self.all_vocabs, self.model_name, self.task_type) |
|
|
| else: |
| if self.scaling: |
| if not self.scaler_path: |
| raise ValueError("Scaler path must be provided when scaling is enabled.") |
| scaler_file_path = os.path.join(self.scaler_path, scaler_filename) |
|
|
| |
| train_indices = self.train_df.index |
| valid_indices = self.valid_df.index |
|
|
| |
| merged_df = pd.concat([self.train_df, self.valid_df], axis=0) |
|
|
| |
| if self.scaler_type == 'adapt': |
| scaled_merged = save_fn(merged_df, scaler_path=scaler_file_path, feature_cols=self.filter_cols) |
| else: |
| scaled_merged = save_fn(merged_df, scaler_path=scaler_file_path) |
|
|
| |
| self.train_df = scaled_merged.loc[train_indices].copy() |
| self.valid_df = scaled_merged.loc[valid_indices].copy() |
|
|
| |
| |
|
|
| self.scaler_file = scaler_file_path |
| else: |
| self.scaler_file = None |
| |
| self.train_dataset = ChemMultiTaskDataset( |
| self.train_df, self.all_vocabs, self.model_name, self.task_type) |
| self.valid_dataset = ChemMultiTaskDataset( |
| self.valid_df, self.all_vocabs, self.model_name, self.task_type) |
| self.test_dataset = ChemMultiTaskDataset( |
| self.test_df, self.all_vocabs, self.model_name, self.task_type) |
|
|
|
|
| def get_dataloaders(self) -> Tuple[DataLoader, DataLoader, DataLoader]: |
| """ํ๋ จ, ๊ฒ์ฆ, ํ
์คํธ ๋ฐ์ดํฐ๋ก๋ ๋ฐํ""" |
| if self.train_dataset is None: |
| self.setup() |
| |
| train_size = len(self.train_dataset) if self.train_dataset else 0 |
| valid_size = len(self.valid_dataset) if self.valid_dataset else 0 |
| test_size = len(self.test_dataset) if self.test_dataset else 0 |
| print(f"[DataModule] Train samples: {train_size}, Val samples: {valid_size}, Test samples: {test_size}") |
| |
| train_loader = DataLoader( |
| self.train_dataset, |
| batch_size=self.batch_size, |
| shuffle=True, |
| collate_fn=self._collate_fn, |
| num_workers=self.workers_per_rank, |
| pin_memory=True, |
| drop_last=False, |
| persistent_workers=self.workers_per_rank > 0 |
| ) |
| |
| valid_loader = DataLoader( |
| self.valid_dataset, |
| batch_size=self.batch_size, |
| collate_fn=self._collate_fn, |
| num_workers=self.workers_per_rank, |
| pin_memory=True, |
| drop_last=False, |
| persistent_workers=self.workers_per_rank > 0 |
| ) |
| |
| test_loader = DataLoader( |
| self.test_dataset, |
| batch_size=self.batch_size, |
| collate_fn=self._collate_fn, |
| num_workers=self.workers_per_rank, |
| pin_memory=True, |
| drop_last=False, |
| persistent_workers=self.workers_per_rank > 0 |
| ) |
| |
| return train_loader, valid_loader, test_loader |
| |
| def get_prediction_dataloader(self, df_predict: pd.DataFrame) -> DataLoader: |
| """์์ธก์ฉ ๋ฐ์ดํฐ๋ก๋ ๋ฐํ""" |
| predict_dataset = ChemMultiTaskDataset( |
| df_predict, self.all_vocabs, self.model_name, self.task_type) |
| |
| predict_loader = DataLoader( |
| predict_dataset, |
| batch_size=self.batch_size, |
| collate_fn=self._collate_fn, |
| num_workers=self.workers_per_rank, |
| pin_memory=True, |
| drop_last=False, |
| persistent_workers=self.workers_per_rank > 0 |
| ) |
| |
| return predict_loader |
| |
| def _collate_fn(self, batch): |
| """๋ฐฐ์น ๋ฐ์ดํฐ ์กฐํฉ ํจ์""" |
| if not batch: |
| raise ValueError("Empty batch encountered") |
|
|
| |
| labels = torch.stack([item['labels'] for item in batch]) |
|
|
| |
| input_ids = torch.stack([item['input_ids'] for item in batch]) |
| attention_mask = torch.stack([item['attention_mask'] for item in batch]) |
|
|
| |
| smiles = [item['smiles'] for item in batch] |
|
|
| return { |
| 'input_ids': input_ids, |
| 'attention_mask': attention_mask, |
| 'labels': labels, |
| 'smiles': smiles |
| } |