Hattie's picture
Add model files
91f863a
Raw
History Blame Contribute Delete
17.6 kB
"""
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()
# Tokenizer๋Š” __init__์—์„œ ์ค€๋น„๋งŒ ํ•˜๊ณ , ์‹ค์ œ ํ† ํฐํ™”๋Š” __getitem__์—์„œ ์ˆ˜ํ–‰
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]
# Tokenization์€ ์—ฌ๊ธฐ์„œ ์ˆ˜ํ–‰
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 # ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ 0 ์‚ฌ์šฉ
labels.append(label_id)
else: # reg
labels = [row[target] for target in self.y_cols]
labels = torch.tensor(labels, dtype=torch.float32)
# squeeze(0)๋กœ ๋ฐฐ์น˜ ์ฐจ์› ์ œ๊ฑฐ
return {'input_ids': objs['input_ids'].squeeze(0),
'attention_mask': objs['attention_mask'].squeeze(0),
'labels': labels,
'smiles': row['SMILES']} # 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
# multi-gpu training setting
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
# Default Data Loading
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) # [1] ๊ฒฐ์ธก ํ–‰ ์ฒ˜๋ฆฌ (์—ฌ๊ธฐ์„œ ์˜ต์…˜ ์ ์šฉ)
if filter_strategy == 'all':
valid_rows = df_input[filter_cols].notna().all(axis=1) # [2] ๋ชจ๋“  ์ปฌ๋Ÿผ์ด ๊ฒฐ์ธก ์•„๋‹Œ ํ–‰๋งŒ ์œ ์ง€
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:
# SMILES ์ปฌ๋Ÿผ ๋ณด์กด ๋ฐ ๊ฒฐ์ธก ์ฒ˜๋ฆฌ
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}")
# data load and convert string to numeric
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}")
# Load datasets
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 = {}
# Scaler ํ•จ์ˆ˜ ์„ ํƒ
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: # zscore (default)
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)
# Adaptive scaler๋Š” feature_cols ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ ํ•„์š”
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) # 70% ํ›ˆ๋ จ ๋ฐ์ดํ„ฐ
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) # ๋‚จ์€ ๋ฐ์ดํ„ฐ์˜ 50%๋ฅผ ๊ฒ€์ฆ ๋ฐ์ดํ„ฐ๋กœ
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: # [3] ์Šค์ผ€์ผ๋ง ์ ์šฉ (๊ธฐ์กด ๋ฐฉ์‹): ์ „์ฒด ๋ฐ์ดํ„ฐํ”„๋ ˆ์ž„์— ๋Œ€ํ•ด ํ‘œ์ค€ํ™” ์ˆ˜ํ–‰ ๋ฐ ์ €์žฅ
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)
# Index ๋ฐฑ์—… (์žฌ๋ถ„ํ• ์„ ์œ„ํ•ด)
train_indices = self.train_df.index
valid_indices = self.valid_df.index
# Train/Valid ๋ณ‘ํ•ฉํ•˜์—ฌ Scaler fit+transform
merged_df = pd.concat([self.train_df, self.valid_df], axis=0)
# Adaptive scaler๋Š” feature_cols ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ ํ•„์š”
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)
# Index ๊ธฐ๋ฐ˜ ์žฌ๋ถ„ํ•  (์Šค์ผ€์ผ๋ง๋œ ๋ฐ์ดํ„ฐ)
self.train_df = scaled_merged.loc[train_indices].copy()
self.valid_df = scaled_merged.loc[valid_indices].copy()
# Test ๋ฐ์ดํ„ฐ๋Š” ์›๋ณธ ์Šค์ผ€์ผ ์œ ์ง€ (์ผ๋ฐ˜ํ™” ์„ฑ๋Šฅ ํ‰๊ฐ€ ๋ชฉ์ )
# self.test_df๋Š” ๊ทธ๋Œ€๋กœ ์œ ์ง€
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])
# ์ž…๋ ฅ ID ๋ฐ ์–ดํ…์…˜ ๋งˆ์Šคํฌ ์ฒ˜๋ฆฌ
input_ids = torch.stack([item['input_ids'] for item in batch])
attention_mask = torch.stack([item['attention_mask'] for item in batch])
# SMILES ์ˆ˜์ง‘
smiles = [item['smiles'] for item in batch]
return {
'input_ids': input_ids,
'attention_mask': attention_mask,
'labels': labels,
'smiles': smiles # SMILES ์ถ”๊ฐ€
}