diff --git "a/Analysis_code/deeplearning_model_multi.ipynb" "b/Analysis_code/deeplearning_model_multi.ipynb" deleted file mode 100644--- "a/Analysis_code/deeplearning_model_multi.ipynb" +++ /dev/null @@ -1,2676 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# 통합 적용 모델 구성" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "##### 통일 데이터셋 변환 함수 정의" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'%pip install torch torchvision scikit-learn'" - ] - }, - "execution_count": 1, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "'''%pip install torch torchvision scikit-learn'''" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "import torch\n", - "import numpy as np\n", - "import random\n", - "\n", - "# Python 및 Numpy 시드 고정\n", - "seed = 42\n", - "random.seed(seed)\n", - "np.random.seed(seed)\n", - "\n", - "# PyTorch 시드 고정\n", - "torch.manual_seed(seed)\n", - "torch.cuda.manual_seed(seed)\n", - "torch.cuda.manual_seed_all(seed) # Multi-GPU 환경에서 동일한 시드 적용\n", - "\n", - "# PyTorch 연산의 결정적 모드 설정\n", - "torch.backends.cudnn.deterministic = True # 실행마다 동일한 결과를 보장\n", - "torch.backends.cudnn.benchmark = True # 성능 최적화를 활성화 (가능한 한 빠른 연산 수행)" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "import pandas as pd\n", - "import numpy as np\n", - "from sklearn.preprocessing import StandardScaler, LabelEncoder\n", - "import torch\n", - "from torch.utils.data import DataLoader, TensorDataset\n", - "import random\n", - "\n", - "# 전처리 함수\n", - "def preprocessing(df):\n", - " df = df[df.columns].copy()\n", - " df.loc[df['wind_dir']=='정온', 'wind_dir'] = \"0\"\n", - " df['wind_dir'] = df['wind_dir'].astype('int')\n", - " df['lm_cloudcover'] = df['lm_cloudcover'].astype('int')\n", - " df['cloudcover'] = df['cloudcover'].astype('int')\n", - " return df\n", - "\n", - "# 데이터셋 준비 함수\n", - "def prepare_dataset(region, data_sample='pure', target='multi', fold=3):\n", - "\n", - " # 데이터 경로 지정\n", - " dat_path = f\"../data/data_for_modeling/{region}_train.csv\"\n", - " if data_sample == 'pure':\n", - " train_path = dat_path\n", - " else:\n", - " train_path = f'../data/data_oversampled/{data_sample}/{data_sample}_{fold}_{region}.csv'\n", - " test_path = f\"../data/data_for_modeling/{region}_test.csv\"\n", - " drop_col = ['binary_class','multi_class','visi','year']\n", - " target_col = f'{target}_class'\n", - " \n", - " # 데이터 로드\n", - " region_dat = preprocessing(pd.read_csv(dat_path, index_col=0))\n", - " if data_sample == 'pure':\n", - " region_train = region_dat.loc[~region_dat['year'].isin([2021-fold]), :]\n", - " else:\n", - " region_train = preprocessing(pd.read_csv(train_path))\n", - " region_val = region_dat.loc[region_dat['year'].isin([2021-fold]), :]\n", - " region_test = preprocessing(pd.read_csv(test_path))\n", - "\n", - " # 컬럼 정렬 (일관성 유지)\n", - " common_columns = region_train.columns.to_list()\n", - " train_data = region_train[common_columns]\n", - " val_data = region_val[common_columns]\n", - " test_data = region_test[common_columns]\n", - "\n", - " # 설명변수 & 타겟 분리\n", - " X_train = train_data.drop(columns=drop_col)\n", - " y_train = train_data[target_col]\n", - " X_val = val_data.drop(columns=drop_col)\n", - " y_val = val_data[target_col]\n", - " X_test = test_data.drop(columns=drop_col)\n", - " y_test = test_data[target_col]\n", - "\n", - " # 범주형 & 연속형 변수 분리\n", - " categorical_cols = X_train.select_dtypes(include=['object', 'category', 'int64']).columns\n", - " numerical_cols = X_train.select_dtypes(include=['float64']).columns\n", - "\n", - " # 범주형 변수 Label Encoding\n", - " label_encoders = {}\n", - " for col in categorical_cols:\n", - " le = LabelEncoder()\n", - " le.fit(X_train[col]) # Train 데이터 기준으로 학습\n", - " label_encoders[col] = le\n", - "\n", - " # 변환 적용\n", - " for col in categorical_cols:\n", - " X_train[col] = label_encoders[col].transform(X_train[col])\n", - " X_val[col] = label_encoders[col].transform(X_val[col])\n", - " X_test[col] = label_encoders[col].transform(X_test[col])\n", - "\n", - " # 연속형 변수 Standard Scaling\n", - " scaler = StandardScaler()\n", - " scaler.fit(X_train[numerical_cols]) # Train 데이터 기준으로 학습\n", - "\n", - " # 변환 적용\n", - " X_train[numerical_cols] = scaler.transform(X_train[numerical_cols])\n", - " X_val[numerical_cols] = scaler.transform(X_val[numerical_cols])\n", - " X_test[numerical_cols] = scaler.transform(X_test[numerical_cols])\n", - "\n", - " return X_train, X_val, X_test, y_train, y_val, y_test, categorical_cols, numerical_cols" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [], - "source": [ - "import pandas as pd\n", - "import numpy as np\n", - "from sklearn.preprocessing import StandardScaler, LabelEncoder\n", - "import torch\n", - "from torch.utils.data import DataLoader, TensorDataset\n", - "import random\n", - "\n", - "# 전처리 함수\n", - "def preprocessing(df):\n", - " df = df[df.columns].copy()\n", - " df.loc[df['wind_dir']=='정온', 'wind_dir'] = \"0\"\n", - " df['wind_dir'] = df['wind_dir'].astype('int')\n", - " df['lm_cloudcover'] = df['lm_cloudcover'].astype('int')\n", - " df['cloudcover'] = df['cloudcover'].astype('int')\n", - " return df\n", - "\n", - "# 데이터 변환 및 dataloader 생성 함수\n", - "def prepare_dataloader(region, data_sample='pure', target='multi', fold=3, random_state=None):\n", - "\n", - " # 데이터 경로 지정\n", - " dat_path = f\"../data/data_for_modeling/{region}_train.csv\"\n", - " if data_sample == 'pure':\n", - " train_path = dat_path\n", - " else:\n", - " train_path = f'../data/data_oversampled/{data_sample}/{data_sample}_{fold}_{region}.csv'\n", - " test_path = f\"../data/data_for_modeling/{region}_test.csv\"\n", - " drop_col = ['binary_class','multi_class','visi','year']\n", - " target_col = f'{target}_class'\n", - " \n", - " # 데이터 로드\n", - " region_dat = preprocessing(pd.read_csv(dat_path, index_col=0))\n", - " if data_sample == 'pure':\n", - " region_train = region_dat.loc[~region_dat['year'].isin([2021-fold]), :]\n", - " else:\n", - " region_train = preprocessing(pd.read_csv(train_path))\n", - " region_val = region_dat.loc[region_dat['year'].isin([2021-fold]), :]\n", - " region_test = preprocessing(pd.read_csv(test_path))\n", - "\n", - " # 컬럼 정렬 (일관성 유지)\n", - " common_columns = region_train.columns.to_list()\n", - " train_data = region_train[common_columns]\n", - " val_data = region_val[common_columns]\n", - " test_data = region_test[common_columns]\n", - "\n", - " # 설명변수 & 타겟 분리\n", - " X_train = train_data.drop(columns=drop_col)\n", - " y_train = train_data[target_col]\n", - " X_val = val_data.drop(columns=drop_col)\n", - " y_val = val_data[target_col]\n", - " X_test = test_data.drop(columns=drop_col)\n", - " y_test = test_data[target_col]\n", - "\n", - " # 범주형 & 연속형 변수 분리\n", - " categorical_cols = X_train.select_dtypes(include=['object', 'category', 'int64']).columns\n", - " numerical_cols = X_train.select_dtypes(include=['float64']).columns\n", - "\n", - " # 범주형 변수 Label Encoding\n", - " label_encoders = {}\n", - " for col in categorical_cols:\n", - " le = LabelEncoder()\n", - " le.fit(X_train[col]) # Train 데이터 기준으로 학습\n", - " label_encoders[col] = le\n", - "\n", - " # 변환 적용\n", - " for col in categorical_cols:\n", - " X_train[col] = label_encoders[col].transform(X_train[col])\n", - " X_val[col] = label_encoders[col].transform(X_val[col])\n", - " X_test[col] = label_encoders[col].transform(X_test[col])\n", - "\n", - " # 연속형 변수 Standard Scaling\n", - " scaler = StandardScaler()\n", - " scaler.fit(X_train[numerical_cols]) # Train 데이터 기준으로 학습\n", - "\n", - " # 변환 적용\n", - " X_train[numerical_cols] = scaler.transform(X_train[numerical_cols])\n", - " X_val[numerical_cols] = scaler.transform(X_val[numerical_cols])\n", - " X_test[numerical_cols] = scaler.transform(X_test[numerical_cols])\n", - "\n", - " # 연속형 변수와 범주형 변수 분리\n", - " X_train_num = torch.tensor(X_train[numerical_cols].values, dtype=torch.float32)\n", - " X_train_cat = torch.tensor(X_train[categorical_cols].values, dtype=torch.long)\n", - "\n", - " X_val_num = torch.tensor(X_val[numerical_cols].values, dtype=torch.float32)\n", - " X_val_cat = torch.tensor(X_val[categorical_cols].values, dtype=torch.long)\n", - "\n", - " X_test_num = torch.tensor(X_test[numerical_cols].values, dtype=torch.float32)\n", - " X_test_cat = torch.tensor(X_test[categorical_cols].values, dtype=torch.long)\n", - "\n", - " # 레이블 변환\n", - " if target == \"binary\":\n", - " y_train_tensor = torch.tensor(y_train.values, dtype=torch.float32) # 이진 분류 → float32\n", - " y_val_tensor = torch.tensor(y_val.values, dtype=torch.float32)\n", - " y_test_tensor = torch.tensor(y_test.values, dtype=torch.float32)\n", - " elif target == \"multi\":\n", - " y_train_tensor = torch.tensor(y_train.values, dtype=torch.long) # 다중 분류 → long\n", - " y_val_tensor = torch.tensor(y_val.values, dtype=torch.long)\n", - " y_test_tensor = torch.tensor(y_test.values, dtype=torch.long)\n", - " else:\n", - " raise ValueError(\"target must be 'binary' or 'multi'\")\n", - "\n", - " # TensorDataset 생성\n", - " train_dataset = TensorDataset(X_train_num, X_train_cat, y_train_tensor)\n", - " val_dataset = TensorDataset(X_val_num, X_val_cat, y_val_tensor)\n", - " test_dataset = TensorDataset(X_test_num, X_test_cat, y_test_tensor)\n", - "\n", - " # DataLoader 생성\n", - " if random_state == None:\n", - " train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True)\n", - " else:\n", - " train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True, generator=torch.Generator().manual_seed(random_state))\n", - " val_loader = DataLoader(val_dataset, batch_size=64, shuffle=False)\n", - " test_loader = DataLoader(test_dataset, batch_size=64, shuffle=False)\n", - " \n", - " return X_train, categorical_cols, numerical_cols, train_loader, val_loader, test_loader" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "##### 사용자 정의 성능지표 함수" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [], - "source": [ - "from sklearn.metrics import confusion_matrix\n", - "from sklearn.utils.class_weight import compute_class_weight\n", - "\n", - "def calculate_csi(Y_test, pred):\n", - "\n", - " cm = confusion_matrix(Y_test, pred) # 변수 이름을 cm으로 변경\n", - " # 혼동 행렬에서 H, F, M 추출\n", - " H = (cm[0, 0] + cm[1, 1])\n", - " \n", - " F = (cm[1, 0] + cm[2, 0] +\n", - " cm[0, 1] + cm[2, 1])\n", - " \n", - " M = (cm[0, 2] + cm[1, 2])\n", - " \n", - " # CSI 계산\n", - " CSI = H / (H + F + M + 1e-10)\n", - " return CSI\n", - "\n", - "def eval_metric_csi(y_true, pred_prob):\n", - "\n", - " pred = np.argmax(pred_prob, axis=1)\n", - " y_true = y_true\n", - " y_pred = pred\n", - " csi = calculate_csi(y_true, y_pred)\n", - " return -1*csi\n", - "\n", - "def sample_weight(y_train):\n", - " class_weights = compute_class_weight(\n", - " class_weight='balanced',\n", - " classes=np.unique(y_train), # 고유 클래스\n", - " y=y_train # 학습 데이터 레이블\n", - " )\n", - " sample_weights = np.array([class_weights[label] for label in y_train])\n", - "\n", - " return sample_weights" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "##### 통일 하이퍼파라미터 최적화 함수 정의" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'%pip install --upgrade ipywidgets'" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "'''%pip install --upgrade ipywidgets'''" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [], - "source": [ - "import optuna\n", - "from sklearn.metrics import accuracy_score, f1_score\n", - "import torch\n", - "import torch.nn as nn\n", - "import torch.optim as optim\n", - "from ft_transformer import FTTransformer\n", - "from resnet_like import ResNetLike\n", - "from deepgbm import DeepGBM\n", - "\n", - "# Optuna의 Trial 로그 숨기기 (WARNING 레벨 이상만 출력)\n", - "optuna.logging.set_verbosity(optuna.logging.WARNING)\n", - "\n", - "# 모델을 GPU로 전송\n", - "device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n", - "\n", - "# 하이퍼파라미터 최적화 함수 정의\n", - "def objective(trial, model_choose, region, data_sample='pure', target='multi', n_folds=3, random_state=None):\n", - "\n", - " val_scores = []\n", - "\n", - " # fold별로 반복\n", - " for fold in range(1, n_folds+1):\n", - " X_train, categorical_cols, numerical_cols, train_loader, val_loader, _ = prepare_dataloader(region, data_sample=data_sample, target=target, fold=fold, random_state=random_state)\n", - "\n", - " if model_choose == \"ft_transformer\":\n", - " d_token = trial.suggest_categorical(\"d_token\", [64, 128, 192, 256])\n", - " n_blocks = trial.suggest_int(\"n_blocks\", 4, 8)\n", - " attention_dropout = trial.suggest_float(\"attention_dropout\", 0.2, 0.5)\n", - " ffn_dropout = trial.suggest_float(\"ffn_dropout\", 0.2, 0.5)\n", - " lr = trial.suggest_float(\"lr\", 1e-4, 1e-3, log=True)\n", - " weight_decay = trial.suggest_float(\"weight_decay\", 1e-5, 1e-3, log=True)\n", - "\n", - " # FT-Transformer 초기화(다중분류: 3개 범주)\n", - " model = FTTransformer(\n", - " num_features=len(numerical_cols),\n", - " cat_cardinalities=[len(X_train[col].unique()) for col in categorical_cols],\n", - " d_token=d_token,\n", - " n_blocks=n_blocks,\n", - " attention_dropout=attention_dropout,\n", - " ffn_dropout=ffn_dropout,\n", - " num_classes=3\n", - " ).to(device)\n", - "\n", - " elif model_choose == 'resnet_like':\n", - " # 하이퍼파라미터 탐색 공간 정의\n", - " d_main = trial.suggest_categorical(\"d_main\", [64, 128, 192, 256])\n", - " d_hidden = trial.suggest_categorical(\"d_hidden\", [32, 64, 128])\n", - " n_blocks = trial.suggest_int(\"n_blocks\", 3, 8) # ResNet 블록 수\n", - " dropout_first = trial.suggest_float(\"dropout_first\", 0.1, 0.5) # 첫 번째 Dropout\n", - " dropout_second = trial.suggest_float(\"dropout_second\", 0.0, 0.3) # 두 번째 Dropout\n", - " lr = trial.suggest_float(\"lr\", 1e-4, 1e-2, log=True) # 학습률\n", - " weight_decay = trial.suggest_float(\"weight_decay\", 1e-6, 1e-3, log=True) # L2 정규화\n", - "\n", - " # 연속형 변수 + 범주형 변수 개수 반영하여 모델 입력 크기 설정\n", - " input_dim = len(numerical_cols) + len(categorical_cols)\n", - "\n", - " # 모델 초기화 및 GPU로 이동\n", - " model = ResNetLike(\n", - " input_dim=input_dim,\n", - " d_main=d_main, \n", - " d_hidden=d_hidden, \n", - " n_blocks=n_blocks, \n", - " dropout_first=dropout_first, \n", - " dropout_second=dropout_second,\n", - " num_classes=3\n", - " ).to(device)\n", - "\n", - " elif model_choose == 'deepgbm':\n", - " d_main = trial.suggest_categorical(\"d_main\", [64, 128, 192, 256])\n", - " d_hidden = trial.suggest_categorical(\"d_hidden\", [32, 64, 128])\n", - " n_blocks = trial.suggest_int(\"n_blocks\", 3, 8) # ResNet 블록 개수\n", - " dropout = trial.suggest_float(\"dropout\", 0.1, 0.5) # Dropout 비율\n", - " lr = trial.suggest_float(\"lr\", 1e-4, 1e-3, log=True) # 학습률\n", - " weight_decay = trial.suggest_float(\"weight_decay\", 1e-5, 1e-3, log=True) # 정규화\n", - "\n", - " # DeepGBM 모델 초기화 (x_num, x_cat을 따로 받는 구조)\n", - " model = DeepGBM(\n", - " num_features=len(numerical_cols),\n", - " cat_features=[len(X_train[col].unique()) for col in categorical_cols],\n", - " d_main=d_main,\n", - " d_hidden=d_hidden,\n", - " n_blocks=n_blocks,\n", - " dropout=dropout,\n", - " num_classes=3\n", - " ).to(device)\n", - "\n", - " # 손실 함수 및 옵티마이저 설정\n", - " if target == 'binary':\n", - " criterion = nn.BCEWithLogitsLoss() # 이진 분류용\n", - " elif target == 'multi':\n", - " criterion = nn.CrossEntropyLoss() # 다중 분류용\n", - "\n", - " # 가중치 조정\n", - " optimizer = optim.AdamW(model.parameters(), lr=lr, weight_decay=weight_decay)\n", - "\n", - " # 학습 설정\n", - " epochs = 50 # epoch 증가\n", - " patience = 8 # Early Stopping 기준 (8 epoch 동안 개선 없으면 중지)\n", - " best_val_score = 0 \n", - " counter = 0 \n", - "\n", - " for epoch in range(epochs):\n", - " model.train()\n", - " for x_num_batch, x_cat_batch, y_batch in train_loader:\n", - " x_num_batch, x_cat_batch, y_batch = (\n", - " x_num_batch.to(device),\n", - " x_cat_batch.to(device),\n", - " y_batch.to(device)\n", - " )\n", - " optimizer.zero_grad()\n", - " y_pred = model(x_num_batch, x_cat_batch)\n", - "\n", - " # 손실 계산 (이진 분류 | 다중 분류)\n", - " if target == 'binary':\n", - " loss = criterion(y_pred, y_batch.float())\n", - " elif target == 'multi':\n", - " loss = criterion(y_pred, y_batch)\n", - "\n", - " loss.backward()\n", - " optimizer.step()\n", - "\n", - " # Validation 평가\n", - " model.eval()\n", - " y_pred_val, y_true_val = [], []\n", - " with torch.no_grad():\n", - " for x_num_batch, x_cat_batch, y_batch in val_loader:\n", - " x_num_batch, x_cat_batch, y_batch = (\n", - " x_num_batch.to(device),\n", - " x_cat_batch.to(device),\n", - " y_batch.to(device)\n", - " )\n", - " output = model(x_num_batch, x_cat_batch)\n", - "\n", - " if target == 'binary':\n", - " pred = (torch.sigmoid(output) >= 0.5).long()\n", - " elif target == 'multi':\n", - " pred = output.argmax(dim=1)\n", - "\n", - " y_pred_val.extend(pred.cpu().numpy()) \n", - " y_true_val.extend(y_batch.cpu().numpy())\n", - "\n", - " # csi-score 계산 (다중클래스용)\n", - " val_csi = calculate_csi(y_true_val, y_pred_val) \n", - "\n", - " # Optuna Pruning 적용 (조기 종료)\n", - " trial.report(val_csi, epoch)\n", - " if trial.should_prune():\n", - " raise optuna.exceptions.TrialPruned()\n", - "\n", - " # Early Stopping 체크\n", - " if val_csi > best_val_score:\n", - " best_val_score = val_csi\n", - " counter = 0 # 개선되었으므로 카운터 초기화\n", - " else:\n", - " counter += 1 # 개선되지 않으면 카운터 증가\n", - "\n", - " if counter >= patience:\n", - " break # Early Stopping 발동\n", - "\n", - " val_scores.append(best_val_score)\n", - "\n", - " # 모든 fold에서 평균 성능을 반환\n", - " avg_val_score = sum(val_scores) / len(val_scores)\n", - " return avg_val_score" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "##### 통일 최적화 + soft voting 함수 정의" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [], - "source": [ - "import optuna\n", - "from sklearn.metrics import accuracy_score, f1_score\n", - "import torch\n", - "import torch.nn as nn\n", - "import torch.optim as optim\n", - "from ft_transformer import FTTransformer\n", - "from resnet_like import ResNetLike\n", - "from deepgbm import DeepGBM\n", - "import copy\n", - "import os\n", - "\n", - "# 모델을 GPU로 전송\n", - "device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n", - "\n", - "def fold_voting(model_choose, region, data_sample='pure', target='multi', n_folds=3, random_state = None):\n", - "\n", - " # Optuna 실행\n", - " sampler = optuna.samplers.TPESampler(seed=seed)\n", - " study = optuna.create_study(direction=\"maximize\", sampler=sampler)\n", - " study.optimize(lambda trial: objective(trial, model_choose=model_choose, region=region, data_sample=data_sample, target=target, n_folds=n_folds, random_state=random_state), n_trials=50, show_progress_bar=True)\n", - "\n", - " # 최적의 하이퍼파라미터 가져오기\n", - " best_params = study.best_trial.params\n", - " print(f\"### Best Params (All Folds): {best_params} ###\")\n", - "\n", - " model_paths = []\n", - "\n", - " for fold in range(1, n_folds + 1):\n", - " X_train, categorical_cols, numerical_cols, train_loader, val_loader, _ = prepare_dataloader(region=region, data_sample=data_sample, target=target, fold=fold, random_state=seed)\n", - "\n", - " # 구현모델 선택\n", - " if model_choose == 'ft_transformer':\n", - " # FT-Transformer 초기화 (최적화된 하이퍼파라미터로 설정)\n", - " model = FTTransformer(\n", - " num_features=len(numerical_cols),\n", - " cat_cardinalities=[len(X_train[col].unique()) for col in categorical_cols],\n", - " d_token=best_params[\"d_token\"],\n", - " n_blocks=best_params[\"n_blocks\"],\n", - " attention_dropout=best_params[\"attention_dropout\"],\n", - " ffn_dropout=best_params[\"ffn_dropout\"],\n", - " num_classes=3\n", - " ).to(device)\n", - " elif model_choose == 'resnet_like':\n", - " # ResNet-Like 초기화 (최적화된 하이퍼파라미터로 설정)\n", - " model = ResNetLike(\n", - " input_dim=len(numerical_cols) + len(categorical_cols), # 입력 차원\n", - " d_main=best_params[\"d_main\"],\n", - " d_hidden=best_params[\"d_hidden\"],\n", - " n_blocks=best_params[\"n_blocks\"],\n", - " dropout_first=best_params[\"dropout_first\"],\n", - " dropout_second=best_params[\"dropout_second\"],\n", - " num_classes=3\n", - " ).to(device)\n", - " elif model_choose == 'deepgbm':\n", - " # DeepGBM 초기화 (최적화된 하이퍼파라미터로 설정)\n", - " model = DeepGBM(\n", - " num_features=len(numerical_cols),\n", - " cat_features=[len(X_train[col].unique()) for col in categorical_cols],\n", - " d_main=best_params[\"d_main\"],\n", - " d_hidden=best_params[\"d_hidden\"],\n", - " n_blocks=best_params[\"n_blocks\"],\n", - " dropout=best_params[\"dropout\"],\n", - " num_classes=3\n", - " ).to(device)\n", - "\n", - " # 손실 함수 및 옵티마이저 설정\n", - " if target == 'binary':\n", - " criterion = nn.BCEWithLogitsLoss() # 이진 분류용\n", - " elif target == 'multi':\n", - " criterion = nn.CrossEntropyLoss() # 다중 분류용\n", - " optimizer_ft = optim.AdamW(model.parameters(), lr=best_params[\"lr\"], weight_decay=best_params[\"weight_decay\"])\n", - "\n", - " # Early Stopping 설정\n", - " best_csi = -float('inf') # CSI-Score는 최대화가 목표이므로 -inf로 초기화\n", - " patience = 10 # F1-Score가 개선되지 않는 Epoch 수\n", - " counter = 0 # 개선되지 않은 Epoch 수를 기록\n", - " best_model = None\n", - "\n", - " # 학습 루프\n", - " epochs = 50 # 최대 Epoch 수\n", - " for epoch in range(epochs):\n", - " # Training Phase\n", - " model.train()\n", - " for x_num_batch, x_cat_batch, y_batch in train_loader:\n", - " x_num_batch, x_cat_batch, y_batch = (\n", - " x_num_batch.to(device),\n", - " x_cat_batch.to(device),\n", - " y_batch.to(device),\n", - " )\n", - " optimizer_ft.zero_grad()\n", - " y_pred = model(x_num_batch, x_cat_batch)\n", - " \n", - " # 손실 계산 (이진 분류 | 다중 분류)\n", - " if target == 'binary':\n", - " loss = criterion(y_pred.squeeze(-1), y_batch.float())\n", - " elif target == 'multi':\n", - " loss = criterion(y_pred, y_batch)\n", - "\n", - " loss.backward()\n", - " optimizer_ft.step()\n", - "\n", - " # Validation Phase\n", - " model.eval()\n", - " y_true_val, y_pred_val = [], []\n", - " with torch.no_grad():\n", - " for x_num_batch, x_cat_batch, y_batch in val_loader:\n", - " x_num_batch, x_cat_batch, y_batch = (\n", - " x_num_batch.to(device),\n", - " x_cat_batch.to(device),\n", - " y_batch.to(device),\n", - " )\n", - " y_pred = model(x_num_batch, x_cat_batch)\n", - "\n", - " if target == 'binary':\n", - " pred = (torch.sigmoid(y_pred) >= 0.5).long()\n", - " elif target == 'multi':\n", - " pred = y_pred.argmax(dim=1)\n", - " y_true_val.extend(y_batch.cpu().numpy())\n", - " y_pred_val.extend(pred.cpu().numpy()) # 가장 높은 확률의 클래스 선택\n", - "\n", - " # CSI-Score 계산\n", - " val_csi = calculate_csi(y_true_val, y_pred_val)\n", - "\n", - " # Early Stopping 체크\n", - " if val_csi > best_csi:\n", - " best_csi = val_csi\n", - " counter = 0\n", - " best_model = copy.deepcopy(model)\n", - " else:\n", - " counter += 1\n", - " if counter >= patience:\n", - " print(f\"Early stopping at epoch {epoch+1}\")\n", - " break\n", - " \n", - " # 모델 저장 경로 설정\n", - " save_dir = f\"./save_model/{model_choose}/{data_sample}\"\n", - " os.makedirs(save_dir, exist_ok=True) # 폴더 없으면 자동 생성\n", - "\n", - " # 모델 저장\n", - " model_path = f\"./save_model/{model_choose}/{data_sample}/{region}_fold{fold}.pth\"\n", - " torch.save(best_model, model_path)\n", - " model_paths.append(model_path)\n", - " print(f\"Saving model to {model_path}\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### 사용자 soft voting 정의 함수" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [], - "source": [ - "import glob\n", - "\n", - "# 모델을 GPU로 전송\n", - "device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n", - "\n", - "# Soft Voting 앙상블\n", - "def pred_fold(region, model_choose, data_sample, fold, target='multi'):\n", - " _, _, _, _, _, y_test, _, _ = prepare_dataset(region=region, data_sample=data_sample, target=target)\n", - " _, _, _, _, _, test_loader = prepare_dataloader(region=region, data_sample=data_sample, target=target, random_state=seed)\n", - "\n", - " folder_path = f'./save_model/{model_choose}/{data_sample}'\n", - " model_paths = [path for path in glob.glob(f'{folder_path}/*.pth') if f'{region}' in path]\n", - "\n", - " model = torch.load(model_paths[fold-1], weights_only=False).to(device)\n", - " model.eval()\n", - "\n", - " test_preds = []\n", - " with torch.no_grad():\n", - " for x_num_batch, x_cat_batch, _ in test_loader:\n", - " output = model(x_num_batch.to(device), x_cat_batch.to(device))\n", - " output = torch.softmax(output, dim=1)\n", - " test_preds.extend(output.cpu().numpy())\n", - "\n", - " # 최종 예측 (Soft Voting)\n", - " final_preds = np.argmax(test_preds, axis=1)\n", - "\n", - " return y_test, final_preds" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [], - "source": [ - "import glob\n", - "\n", - "# 모델을 GPU로 전송\n", - "device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n", - "\n", - "# Soft Voting 앙상블\n", - "def soft_voting(region, model_choose, data_sample, target='multi'):\n", - " _, _, _, _, _, y_test, _, _ = prepare_dataset(region=region, data_sample=data_sample, target=target)\n", - " _, _, _, _, _, test_loader = prepare_dataloader(region=region, data_sample=data_sample, target=target, random_state=seed)\n", - "\n", - " folder_path = f'./save_model/{model_choose}/{data_sample}'\n", - " model_paths = [path for path in glob.glob(f'{folder_path}/*.pth') if f'{region}' in path]\n", - "\n", - " if target == 'multi':\n", - " test_probs = np.zeros((len(y_test), 3))\n", - " elif target == 'binary':\n", - " test_probs = np.zeros((len(y_test), 2))\n", - "\n", - " for _, path in enumerate(model_paths):\n", - " model = torch.load(path, weights_only=False).to(device)\n", - " model.eval()\n", - "\n", - " test_preds = []\n", - " with torch.no_grad():\n", - " for x_num_batch, x_cat_batch, _ in test_loader:\n", - " output = model(x_num_batch.to(device), x_cat_batch.to(device))\n", - " output = torch.softmax(output, dim=1)\n", - " test_preds.extend(output.cpu().numpy())\n", - "\n", - " test_probs += np.array(test_preds) / len(model_paths)\n", - "\n", - " # 최종 예측 (Soft Voting)\n", - " final_preds = np.argmax(test_probs, axis=1)\n", - "\n", - " return y_test, test_probs, final_preds" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# 모델 별 K-fold + Soft Voting 진행" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "for model_choose in ['ft_transformer','resnet_like','deepgbm']:\n", - " for region in ['seoul','busan','daejeon','daegu','incheon','gwangju']:\n", - " for data_sample in ['pure','smote','ctgan7000','ctgan10000','ctgan20000']:\n", - " fold_voting(model_choose=model_choose, region=region, data_sample=data_sample, random_state=seed)" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "230879b7c5e641c8b8b61492ae92c3e4", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - " 0%| | 0/50 [00:00\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
RegionModelCSI
0seoulft_transformer0.344538
1seoulresnet_like0.322176
2seouldeepgbm0.330667
3busanft_transformer0.415800
4busanresnet_like0.456522
5busandeepgbm0.398390
6daejeonft_transformer0.310212
7daejeonresnet_like0.356025
8daejeondeepgbm0.270197
9daeguft_transformer0.261818
10daeguresnet_like0.252125
11daegudeepgbm0.241611
12incheonft_transformer0.513898
13incheonresnet_like0.518868
14incheondeepgbm0.514765
15gwangjuft_transformer0.469122
16gwangjuresnet_like0.438757
17gwangjudeepgbm0.453961
\n", - "" - ], - "text/plain": [ - " Region Model CSI\n", - "0 seoul ft_transformer 0.344538\n", - "1 seoul resnet_like 0.322176\n", - "2 seoul deepgbm 0.330667\n", - "3 busan ft_transformer 0.415800\n", - "4 busan resnet_like 0.456522\n", - "5 busan deepgbm 0.398390\n", - "6 daejeon ft_transformer 0.310212\n", - "7 daejeon resnet_like 0.356025\n", - "8 daejeon deepgbm 0.270197\n", - "9 daegu ft_transformer 0.261818\n", - "10 daegu resnet_like 0.252125\n", - "11 daegu deepgbm 0.241611\n", - "12 incheon ft_transformer 0.513898\n", - "13 incheon resnet_like 0.518868\n", - "14 incheon deepgbm 0.514765\n", - "15 gwangju ft_transformer 0.469122\n", - "16 gwangju resnet_like 0.438757\n", - "17 gwangju deepgbm 0.453961" - ] - }, - "execution_count": 43, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from sklearn.metrics import accuracy_score, roc_auc_score, f1_score\n", - "\n", - "performance = []\n", - "\n", - "for region in ['seoul','busan','daejeon','daegu','incheon','gwangju']:\n", - " for model_choose in ['ft_transformer','resnet_like','deepgbm']:\n", - " y_test, test_probs, final_preds = soft_voting(region=region, model_choose=model_choose, data_sample='pure')\n", - "\n", - " # 성능 지표 계산\n", - " csi = calculate_csi(y_test, final_preds)\n", - "\n", - " # 데이터 저장\n", - " performance.append({\"Region\": region, \"Model\": model_choose, \"CSI\": csi})\n", - "\n", - "# DataFrame으로 변환\n", - "df_perf = pd.DataFrame(performance)\n", - "df_perf" - ] - }, - { - "cell_type": "code", - "execution_count": 45, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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lS9fXm3atFHPnj11584ddenSRY0aNVKtWrX4IjKB8K9HIpYvXz517txZfn5+8vHxsXc5eA0UK1ZM06ZN0zvvvKPg4GCFhISoSJEikqSUKVMqU6ZM+umnn3T//n25urrat1i80qKCkiR98sknmjlzpj799FPlypVLLVu2VK1atTR9+nQ1bNhQ+fPn1/Hjx3X06FHlyJFDNWvWlLOzMyEKzxR1urGnp6fCw8MVHh4uT09P3b9/X5kzZ9aIESNUv359DRo0SJ6enqpdu7akh2d4MLbwIiwWiyIjI3X48GHrBHBRXypGja2zZ8+qbNmyGjZsmLJmzcrkq4izx8/2yZMnj959913169dPdevWlbe3t27duqVUqVJpy5Ytunv3rtavX68rV65oxowZXNKTgPjqIxHz9PRU+fLlCeiIV4UKFdKqVavk6uqqr776Svv27bMuu3TpktKnT6979+7ZsUK8ygYMGGBzhsaaNWv0ww8/aPHixerSpYsePHig27dv69ChQ6pXr56OHz+u3Llzq1atWvrwww9Vp04d6ynuhCg8S9QXQXXr1lVISIj1tM6oLxnv3bunypUrq379+qpZs6Z1PY4sIT44OTkpS5YsCg4O1oMHD6zvXZJ06tQpzZ07V//995/KlCnDxKt4LlHvVStWrNCmTZskSUOHDlXx4sXVo0cP3bx5U97e3ho6dKiuXr2qFStWyMXFRb///rucnJysd/NB/ONfEQDxrmDBgpozZ4527dqlhg0bKiAgQJ06ddLSpUs1evRoJU2a1N4l4hV07do1bd26Vd9//71mzZol6eEZGq1atVL58uW1du1atWzZUqNHj9b27dt18eJFdezYUYcOHYq2LT5UIC7y58+vadOmafjw4erTp4927typ48ePa9KkScqXL5+GDx9uPaIEPI+o09uvXLmiS5cuWdtbtWqlmzdvKjAw0OaI5TfffKN58+bxXoYX9scff6hhw4bq0qWL/ve//0mSOnbsqBs3bujXX39VZGSkypcvr2XLlik4OFg//vijXF1d9eDBA76QTEBckw4gwRw4cEANGzZUeHi4unTpohYtWsjX19feZeEVFHXa8YULF9S1a1ddvnxZrVu3VuvWrXX58mV5eHiobt26KleunIYMGaKrV6+qevXq2rVrl1q0aMG9qhEvli5dqi5dulhnzk6fPr22b98uV1dXm7sL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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "import seaborn as sns\n", - "import matplotlib.pyplot as plt\n", - "\n", - "# 그래프 크기 설정\n", - "plt.figure(figsize=(12, 6))\n", - "\n", - "# 바 그래프 그리기\n", - "sns.barplot(data=df_perf, x=\"Region\", y=\"CSI\", hue=\"Model\")\n", - "\n", - "# 그래프 꾸미기\n", - "plt.title(\"Performance Comparison by Region and Model\")\n", - "plt.xlabel(\"Region\")\n", - "plt.ylabel(\"CSI Score\")\n", - "plt.legend(title=\"Model\")\n", - "plt.xticks(rotation=45) # 지역명이 길 경우 회전\n", - "\n", - "# 그래프 출력\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## TabNet" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'%pip install pytorch-tabnet'" - ] - }, - "execution_count": 12, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "'''%pip install pytorch-tabnet'''" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "##### 데이터셋 준비" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": {}, - "outputs": [], - "source": [ - "import pandas as pd\n", - "import numpy as np\n", - "from sklearn.preprocessing import StandardScaler, LabelEncoder\n", - "import torch\n", - "import random\n", - "\n", - "# 시드 고정\n", - "seed = 42\n", - "random.seed(seed)\n", - "np.random.seed(seed)\n", - "torch.manual_seed(seed)\n", - "torch.cuda.manual_seed_all(seed)\n", - "\n", - "# 데이터셋 불러오기\n", - "X_train, X_val, X_test, y_train, y_val, y_test, categorical_cols, numerical_cols = prepare_dataset(\"seoul\", target = \"multi\")" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "Early stopping occurred at epoch 31 with best_epoch = 23 and best_valid_csi_metric = 0.49728\n", - "\n", - "Early stopping occurred at epoch 15 with best_epoch = 7 and best_valid_csi_metric = 0.46986\n", - "\n", - "Early stopping occurred at epoch 23 with best_epoch = 15 and best_valid_csi_metric = 0.52999\n", - "\n", - "Early stopping occurred at epoch 30 with best_epoch = 22 and best_valid_csi_metric = 0.5258\n", - "\n", - "Early stopping occurred at epoch 15 with best_epoch = 7 and best_valid_csi_metric = 0.49212\n", - "\n", - "Early stopping occurred at epoch 16 with best_epoch = 8 and best_valid_csi_metric = 0.50329\n", - "\n", - "Early stopping occurred at epoch 18 with best_epoch = 10 and best_valid_csi_metric = 0.48949\n", - "\n", - "Early stopping occurred at epoch 23 with best_epoch = 15 and best_valid_csi_metric = 0.53168\n", - "Stop training because you reached max_epochs = 50 with best_epoch = 48 and best_valid_csi_metric = 0.53687\n", - "\n", - "Early stopping occurred at epoch 23 with best_epoch = 15 and best_valid_csi_metric = 0.55556\n", - "\n", - "Early stopping occurred at epoch 19 with best_epoch = 11 and best_valid_csi_metric = 0.52191\n", - "\n", - "Early stopping occurred at epoch 29 with best_epoch = 21 and best_valid_csi_metric = 0.50078\n", - "\n", - "Early stopping occurred at epoch 17 with best_epoch = 9 and best_valid_csi_metric = 0.47917\n", - "\n", - "Early stopping occurred at epoch 31 with best_epoch = 23 and best_valid_csi_metric = 0.48897\n", - "\n", - "Early stopping occurred at epoch 25 with best_epoch = 17 and best_valid_csi_metric = 0.57122\n", - "\n", - "Early stopping occurred at epoch 24 with best_epoch = 16 and best_valid_csi_metric = 0.51626\n", - "\n", - "Early stopping occurred at epoch 15 with best_epoch = 7 and best_valid_csi_metric = 0.54084\n", - "\n", - "Early stopping occurred at epoch 31 with best_epoch = 23 and best_valid_csi_metric = 0.52264\n", - "\n", - "Early stopping occurred at epoch 21 with best_epoch = 13 and best_valid_csi_metric = 0.54005\n", - "\n", - "Early stopping occurred at epoch 23 with best_epoch = 15 and best_valid_csi_metric = 0.52377\n", - "\n", - "Early stopping occurred at epoch 20 with best_epoch = 12 and best_valid_csi_metric = 0.5151\n", - "\n", - "Early stopping occurred at epoch 10 with best_epoch = 2 and best_valid_csi_metric = 0.50446\n", - "\n", - "Early stopping occurred at epoch 16 with best_epoch = 8 and best_valid_csi_metric = 0.54733\n", - "\n", - "Early stopping occurred at epoch 12 with best_epoch = 4 and best_valid_csi_metric = 0.52098\n", - "\n", - "Early stopping occurred at epoch 23 with best_epoch = 15 and best_valid_csi_metric = 0.5317\n", - "\n", - "Early stopping occurred at epoch 32 with best_epoch = 24 and best_valid_csi_metric = 0.549\n", - "\n", - "Early stopping occurred at epoch 19 with best_epoch = 11 and best_valid_csi_metric = 0.50556\n", - "\n", - "Early stopping occurred at epoch 16 with best_epoch = 8 and best_valid_csi_metric = 0.52058\n", - "\n", - "Early stopping occurred at epoch 20 with best_epoch = 12 and best_valid_csi_metric = 0.51613\n", - "\n", - "Early stopping occurred at epoch 31 with best_epoch = 23 and best_valid_csi_metric = 0.51721\n", - "\n", - "Early stopping occurred at epoch 29 with best_epoch = 21 and best_valid_csi_metric = 0.54202\n", - "\n", - "Early stopping occurred at epoch 27 with best_epoch = 19 and best_valid_csi_metric = 0.5661\n", - "\n", - "Early stopping occurred at epoch 30 with best_epoch = 22 and best_valid_csi_metric = 0.52038\n", - "\n", - "Early stopping occurred at epoch 33 with best_epoch = 25 and best_valid_csi_metric = 0.54363\n", - "\n", - "Early stopping occurred at epoch 21 with best_epoch = 13 and best_valid_csi_metric = 0.53012\n", - "\n", - "Early stopping occurred at epoch 24 with best_epoch = 16 and best_valid_csi_metric = 0.54863\n", - "\n", - "Early stopping occurred at epoch 23 with best_epoch = 15 and best_valid_csi_metric = 0.50951\n", - "\n", - "Early stopping occurred at epoch 27 with best_epoch = 19 and best_valid_csi_metric = 0.53793\n", - "\n", - "Early stopping occurred at epoch 31 with best_epoch = 23 and best_valid_csi_metric = 0.54465\n", - "\n", - "Early stopping occurred at epoch 17 with best_epoch = 9 and best_valid_csi_metric = 0.5\n", - "\n", - "Early stopping occurred at epoch 14 with best_epoch = 6 and best_valid_csi_metric = 0.56151\n", - "\n", - "Early stopping occurred at epoch 16 with best_epoch = 8 and best_valid_csi_metric = 0.51922\n", - "\n", - "Early stopping occurred at epoch 18 with best_epoch = 10 and best_valid_csi_metric = 0.51718\n", - "\n", - "Early stopping occurred at epoch 21 with best_epoch = 13 and best_valid_csi_metric = 0.51779\n", - "\n", - "Early stopping occurred at epoch 10 with best_epoch = 2 and best_valid_csi_metric = 0.52174\n", - "\n", - "Early stopping occurred at epoch 19 with best_epoch = 11 and best_valid_csi_metric = 0.50699\n", - "\n", - "Early stopping occurred at epoch 41 with best_epoch = 33 and best_valid_csi_metric = 0.54059\n", - "\n", - "Early stopping occurred at epoch 37 with best_epoch = 29 and best_valid_csi_metric = 0.56875\n", - "\n", - "Early stopping occurred at epoch 35 with best_epoch = 27 and best_valid_csi_metric = 0.53787\n", - "\n", - "Early stopping occurred at epoch 12 with best_epoch = 4 and best_valid_csi_metric = 0.51343\n", - "Best Hyperparameters: {'n_d': 48, 'n_a': 32, 'n_steps': 6, 'gamma': 1.3427992882663162, 'lambda_sparse': 0.0010676916501410128, 'momentum': 0.1377411909968035}\n" - ] - } - ], - "source": [ - "import optuna\n", - "from pytorch_tabnet.tab_model import TabNetClassifier\n", - "from pytorch_tabnet.metrics import Metric\n", - "from sklearn.metrics import f1_score\n", - "import warnings\n", - "\n", - "# 경고 메시지 숨기기\n", - "warnings.filterwarnings(\"ignore\")\n", - "\n", - "# TabNet에서 사용할 사용자 정의 Metric\n", - "class CSIMetric(Metric):\n", - " def __init__(self):\n", - " self._name = \"csi_metric\"\n", - " self._maximize = True\n", - "\n", - " def __call__(self, y_true, y_pred_prob):\n", - " y_pred = np.argmax(y_pred_prob, axis=1)\n", - " return calculate_csi(y_true, y_pred)\n", - "\n", - "# Optuna 최적화 함수\n", - "def objective(trial):\n", - " # 하이퍼파라미터 탐색 범위 설정\n", - " n_d = trial.suggest_int(\"n_d\", 8, 64, step=8)\n", - " n_a = trial.suggest_int(\"n_a\", 8, 64, step=8)\n", - " n_steps = trial.suggest_int(\"n_steps\", 3, 10)\n", - " gamma = trial.suggest_float(\"gamma\", 1.0, 2.0)\n", - " lambda_sparse = trial.suggest_float(\"lambda_sparse\", 0.0001, 0.01, log=True)\n", - " momentum = trial.suggest_float(\"momentum\", 0.01, 0.4)\n", - "\n", - " # TabNet 모델 생성\n", - " model = TabNetClassifier(\n", - " n_d=n_d, n_a=n_a, n_steps=n_steps,\n", - " gamma=gamma, lambda_sparse=lambda_sparse,\n", - " momentum=momentum,\n", - " verbose=0,\n", - " seed=seed\n", - " )\n", - "\n", - " # 모델 학습\n", - " model.fit(\n", - " X_train.to_numpy(), y_train.to_numpy(),\n", - " eval_set=[(X_val.to_numpy(), y_val.to_numpy())],\n", - " eval_name=['valid'],\n", - " eval_metric=['csi_metric'],\n", - " max_epochs=50,\n", - " patience=8,\n", - " batch_size=512,\n", - " virtual_batch_size=128\n", - " )\n", - "\n", - " # Validation 데이터로 F1-score 계산\n", - " y_pred_val = model.predict(X_val.to_numpy())\n", - " csi_score = calculate_csi(y_val.to_numpy(), y_pred_val)\n", - "\n", - " return csi_score # 최적화를 F1-score 기준으로 진행\n", - "\n", - "# Optuna 실행\n", - "sampler = optuna.samplers.TPESampler(seed=seed)\n", - "study = optuna.create_study(direction=\"maximize\", sampler=sampler)\n", - "study.optimize(objective, n_trials=50) # 50번 탐색 실행\n", - "\n", - "# 최적 하이퍼파라미터 출력\n", - "best_params = study.best_trial.params\n", - "print(\"Best Hyperparameters:\", best_params)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "##### 모델 학습 루프" - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "epoch 0 | loss: 0.65117 | valid_csi_metric: 0.2587 | 0:00:01s\n", - "epoch 1 | loss: 0.33195 | valid_csi_metric: 0.39111 | 0:00:02s\n", - "epoch 2 | loss: 0.24615 | valid_csi_metric: 0.44093 | 0:00:03s\n", - "epoch 3 | loss: 0.1948 | valid_csi_metric: 0.46779 | 0:00:04s\n", - "epoch 4 | loss: 0.18316 | valid_csi_metric: 0.50305 | 0:00:05s\n", - "epoch 5 | loss: 0.16839 | valid_csi_metric: 0.4462 | 0:00:06s\n", - "epoch 6 | loss: 0.16348 | valid_csi_metric: 0.45389 | 0:00:08s\n", - "epoch 7 | loss: 0.15119 | valid_csi_metric: 0.4276 | 0:00:09s\n", - "epoch 8 | loss: 0.15715 | valid_csi_metric: 0.50084 | 0:00:10s\n", - "epoch 9 | loss: 0.14863 | valid_csi_metric: 0.48189 | 0:00:11s\n", - "epoch 10 | loss: 0.15017 | valid_csi_metric: 0.48625 | 0:00:12s\n", - "epoch 11 | loss: 0.14889 | valid_csi_metric: 0.52342 | 0:00:13s\n", - "epoch 12 | loss: 0.14121 | valid_csi_metric: 0.52754 | 0:00:15s\n", - "epoch 13 | loss: 0.13633 | valid_csi_metric: 0.46671 | 0:00:16s\n", - "epoch 14 | loss: 0.1357 | valid_csi_metric: 0.14583 | 0:00:17s\n", - "epoch 15 | loss: 0.12973 | valid_csi_metric: 0.15389 | 0:00:18s\n", - "epoch 16 | loss: 0.13033 | valid_csi_metric: 0.39516 | 0:00:19s\n", - "epoch 17 | loss: 0.11944 | valid_csi_metric: 0.57122 | 0:00:20s\n", - "epoch 18 | loss: 0.11868 | valid_csi_metric: 0.5 | 0:00:21s\n", - "epoch 19 | loss: 0.11035 | valid_csi_metric: 0.48313 | 0:00:23s\n", - "epoch 20 | loss: 0.10546 | valid_csi_metric: 0.50574 | 0:00:24s\n", - "epoch 21 | loss: 0.14626 | valid_csi_metric: 0.37308 | 0:00:25s\n", - "epoch 22 | loss: 0.14373 | valid_csi_metric: 0.17938 | 0:00:26s\n", - "epoch 23 | loss: 0.11781 | valid_csi_metric: 0.43773 | 0:00:27s\n", - "epoch 24 | loss: 0.10846 | valid_csi_metric: 0.37634 | 0:00:28s\n", - "epoch 25 | loss: 0.10376 | valid_csi_metric: 0.1566 | 0:00:29s\n", - "epoch 26 | loss: 0.10205 | valid_csi_metric: 0.21463 | 0:00:31s\n", - "epoch 27 | loss: 0.09992 | valid_csi_metric: 0.4367 | 0:00:32s\n", - "\n", - "Early stopping occurred at epoch 27 with best_epoch = 17 and best_valid_csi_metric = 0.57122\n", - "Successfully saved model at ./models/tabnet_model.zip\n", - "Validation CSI-Score: 0.5712\n" - ] - } - ], - "source": [ - "from pytorch_tabnet.tab_model import TabNetClassifier\n", - "from sklearn.metrics import accuracy_score, f1_score, roc_auc_score\n", - "import numpy as np\n", - "\n", - "# 최적 하이퍼파라미터 적용\n", - "best_model = TabNetClassifier(\n", - " n_d=best_params[\"n_d\"],\n", - " n_a=best_params[\"n_a\"],\n", - " n_steps=best_params[\"n_steps\"],\n", - " gamma=best_params[\"gamma\"],\n", - " lambda_sparse=best_params[\"lambda_sparse\"],\n", - " momentum=best_params[\"momentum\"],\n", - " seed=seed # 시드 고정\n", - ")\n", - "\n", - "# Early Stopping 설정\n", - "best_val_f1 = -float(\"inf\") # F1-score는 최대화가 목표\n", - "patience = 10 # F1-score가 개선되지 않는 Epoch 수\n", - "counter = 0\n", - "\n", - "# 모델 학습\n", - "best_model.fit(\n", - " X_train.to_numpy(), y_train.to_numpy(),\n", - " eval_set=[(X_val.to_numpy(), y_val.to_numpy())],\n", - " eval_name=['valid'],\n", - " eval_metric=['csi_metric'],\n", - " max_epochs=50, # 최대 Epoch\n", - " patience=patience, # Early stopping 적용\n", - " batch_size=512,\n", - " virtual_batch_size=128\n", - ")\n", - "\n", - "# 학습이 끝난 후 모델 저장 (경로 명확히 지정)\n", - "best_model.save_model(\"./models/tabnet_model\")\n", - "\n", - "# 저장된 모델 불러오기\n", - "best_model.load_model(\"./models/tabnet_model.zip\")\n", - "\n", - "# Validation 데이터 평가\n", - "y_pred_val = best_model.predict(X_val.to_numpy())\n", - "val_csi = calculate_csi(y_val.to_numpy(), y_pred_val)\n", - "\n", - "print(f\"Validation CSI-Score: {val_csi:.4f}\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "##### 성능 평가" - ] - }, - { - "cell_type": "code", - "execution_count": 18, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Test Accuracy: 0.8820776255707763\n", - "Test F1-Score: 0.4866900629111339\n", - "Test CSI-Score: 0.29052197802195806\n" - ] - } - ], - "source": [ - "from sklearn.metrics import accuracy_score, f1_score\n", - "\n", - "# 저장된 모델 불러오기\n", - "loaded_model = TabNetClassifier()\n", - "loaded_model.load_model(\"./models/tabnet_model.zip\")\n", - "\n", - "# Test 데이터 평가\n", - "y_pred_test = loaded_model.predict(X_test.to_numpy())\n", - "\n", - "# 성능 평가 지표 계산\n", - "print(\"Test Accuracy:\", accuracy_score(y_test, y_pred_test))\n", - "print(\"Test F1-Score:\", f1_score(y_test, y_pred_test, average='macro'))\n", - "print(\"Test CSI-Score:\", calculate_csi(y_test, y_pred_test))" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "base", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.12.4" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -}