[DACON] 파이토치 항공사 고객 만족도 데이터로 연습하기
from google.colab import drive
drive.mount('/content/drive')
데이터가 저장되어 있는 구글 드라이브와 연동합니다.
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torch.utils.data import DataLoader, Dataset
device = 'cuda' if torch.cuda.is_available() else 'cpu'
import random
random.seed(777)
torch.manual_seed(777)
if device == 'cuda':
torch.cuda.manual_seed_all(777)
파이토치 관련 모듈들을 임포트합니다.
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from scipy import stats
import warnings
warnings.filterwarnings("ignore")
path = '/content/drive/MyDrive/airport/'
train = pd.read_csv(path + 'train.csv')
test = pd.read_csv(path + 'test.csv')
sample_submission = pd.read_csv(path + 'sample_submission.csv')
train.head()
데이터 분석에 기본적으로 필요한 패키지를 임포트하고, 데이터 csv 파일 또한 임포트 합니다.
X = train.drop(columns = ['id', 'target'], axis = 1)
X.shape
id랑 target은 분석하는데 필요하지 않거나, 라벨 값이기 때문에 제거합니다.
binary_obj_columns = ['Gender', 'Customer Type', 'Type of Travel']
numerical_columns = ['Age', 'Departure Delay in Minutes', 'Arrival Delay in Minutes', 'Flight Distance']
multical_obj_columns = list(set(X.columns) - set(binary_obj_columns) - set(numerical_columns))
데이터를 이진분류가 되는 데이터, 두 개를 넘는 항목이 존재하는 데이터, 연속형 데이터 세개로 나눠서 정리합니다.
이진분류 데이터는 그대로 유지하고 다중분류 데이터는 라벨인코딩, 연속형 데이터는 4개의 항목으로 그룹핑 하겠습니다.
X_train_num = pd.DataFrame()
X_test_num = pd.DataFrame()
for col in binary_obj_columns :
map_dict = {key : num for num,key in enumerate(train[col].unique())}
X_train_num[col] = train[col].map(map_dict)
X_test_num[col] = test[col].map(map_dict)
X_train_group = pd.DataFrame()
X_test_group = pd.DataFrame()
for col in numerical_columns :
data = train[col]
_, bins = pd.qcut(data, 4, retbins=True, labels=False, duplicates='drop')
X_train_group[col+'_group'] = train[col].apply(lambda x : sum([x >= a for a in bins]))
X_test_group[col+'_group'] = test[col].apply(lambda x : sum([x >= a for a in bins]))
for col in multical_obj_columns :
map_dict = {key : num for num, key in enumerate(sorted(train[col].unique()))}
X_train_group[col] = train[col].map(map_dict)
X_test_group[col] = test[col].map(map_dict)
num_cols = list(X_train_num.columns)
cat_cols = list(X_train_group.columns)
X_train = pd.concat([X_train_num, X_train_group], axis = 1)
X_test = pd.concat([X_test_num, X_test_group], axis = 1)
Y = train['target'].values
X_train.head()
앞서 말한 방식대로 데이터 종류별로 전처리를 진행했습니다.
문자 형식 데이터의 경우 enumerate 함수를 통해 딕셔너리 형태로 바꾼 뒤 map 함수로 매핑시켜 라벨인코딩을 진행했습니다.
연속형 데이터의 경우 판다스 내 qcut 함수를 이용해 항목 내 데이터를 4등분하고, apply 함수로 값을 넣어주었습니다.
class CustomDataset(Dataset):
def __init__(self, x, y, cat_cols, num_cols):
# 데이터 셋 정의하는 곳
self.x_cat = x[cat_cols].copy().values.astype(np.int64)
self.x_num = x[num_cols].copy().values.astype(np.float32)
self.y = y.astype(np.float32)
def __len__(self):
# 길이 출력
return len(self.y)
def __getitem__(self, idx):
# 특정 1개 샘플 가져오는 곳
return self.x_cat[idx], self.x_num[idx], self.y[idx]
torch.utils.data 내 Dataset 클래스를 상속받아 나만의 데이터 셋 클래스를 만들었습니다.
len과 getitem 함수만 정의해준다면 클래스 형식은 자유로우며, 데이터 셋을 x/y로 분류해서 저장할 수 있습니다.
이렇게 데이터 셋을 Dataset 클래스를 상속받아 정의한다면 torch.utils.data 내 DataLoader를 사용할 수 있는 것 또한 장점입니다.
from sklearn.model_selection import train_test_split
def return_dataloaders(batch_size, random_state = 0):
data_range = X_train_group.index
train_idx, valid_idx = train_test_split(data_range, shuffle = True, stratify = Y,
test_size = .5, random_state = random_state)
X_tr, y_tr = X_train.iloc[train_idx], Y[train_idx]
X_val, y_val = X_train.iloc[valid_idx], Y[valid_idx]
train_ds = CustomDataset(X_tr, y_tr, cat_cols, num_cols)
valid_ds = CustomDataset(X_val, y_val, cat_cols, num_cols)
# 데이터 셋을 로더 내에 저장합니다.
train_dl = DataLoader(train_ds, batch_size = batch_size)
valid_dl = DataLoader(valid_ds, batch_size = batch_size)
dataloaders = {}
dataloaders['train'] = train_dl
dataloaders['valid'] = valid_dl
return dataloaders
batch_size = 32
dataloaders = return_dataloaders(batch_size)
앞서 정의한 나만의 데이터 셋 CustomDataset 클래스를 이용해 파이토치에서 지원하는 DataLoader를 정의했습니다.
def weights_init(m):
classname = m.__class__.__name__
if classname.find('BatchNorm') != -1: # 클레스의 이름이 BatchNorm 이라면
nn.init.normal_(m.weight.data, 1.0, 0.02)
nn.init.constant_(m.bias.data, 0)
print('hello world!')
코드 작성자가 정의한 함수입니다. 가중치 초기값을 주는 함수인 것 같은데 조금 더 공부해야겠습니다.
class FC_Block(nn.Module):
def __init__(self, inp_dim, out_dim):
super(FC_Block, self).__init__()
self.linear = nn.Linear(inp_dim, out_dim)
self.batch = nn.BatchNorm1d(out_dim)
def forward(self, x):
x = self.linear(x)
x = self.batch(x)
return x
nn.Module 클래스를 상속받아 FC_Block 클래스를 만들었습니다.
이 클래스는 inp, out 두 입력값을 받아 히든층을 구현한 뒤 배치 정규화를 진행합니다. 한번에 사용하면 편리하겠죠?
inp_oup_dims = [[x , x//2] for x in X_train[cat_cols].nunique()]
# nunique() 함수는 데이터 프레임 내 항목 개수를 출력해줍니다.
# 범주형 변수 항목 수, 항목 수의 절반으로 구성됩니다.
class EMBNN(nn.Module):
def __init__(self, inp_oup_dims, num_continuous):
super(EMBNN, self).__init__()
# nn.ModuleList : nn.Module 형태를 리스트로 저장하는 방식.
self.embeddings = nn.ModuleList([
nn.Embedding(inp_dim + 1, out_dim) for inp_dim, out_dim, in inp_oup_dims
])
self.n_emb = sum(e.embedding_dim for e in self.embeddings)
self.emb_drop = nn.Dropout(0.3)
self.cont_norm = nn.BatchNorm1d(num_continuous)
self.n_con = num_continuous
self.FFC = nn.Sequential(
FC_Block(self.n_emb + self.n_con, 32),
nn.Dropout(0.2),
FC_Block(32, 8),
nn.Dropout(0.2),
nn.Linear(8,1)
)
def forward(self, x_cat, x_cont):
# 범주형 자료들의 입력값을 각각 임베딩 합니다.
x_cat = [e(x_cat[:, i]) for i, e in enumerate(self.embeddings)]
# 임베딩 한 값(리스트 형태)를 가로 방향으로 합쳐줍니다.
x_cat = torch.cat(x_cat, 1)
# 드롭 아웃 진행합니다.
x_cat = self.emb_drop(x_cat)
# 연속형 자료들에 배치 정규화 진행합니다.
x_cont = self.cont_norm(x_cont)
# 범주형 자료와 연속형 자료를 합칩니다.
x = torch.cat([x_cat, x_cont], 1)
# 히든 층을 진행합니다.
x = self.FFC(x)
return F.sigmoid(x)
범주형 변수들을 특정 층에 먼저 통과시켜 임베딩을 진행 한 뒤 연속형 변수들과 합쳐줍니다. 그 이후 히든층을 진행시킵니다.
부분 부분마다 각주를 달아놓았습니다.
def train_model(model, dataloader, optimizer, criterion, num_epoch, early_stop, model_path):
best_val_loss = np.float('inf')
early_stop_epoch = 0
for epoch in range(num_epoch):
for phase in ['train', 'valid']: # 트레인, 벨리드 2가지 모드로.
if phase == 'train':
model.train()
elif phase == 'valid':
model.eval()
running_loss = 0
running_corr = 0
total = 0
for x_cat, x_num, y in dataloader[phase]:
# 데이터들을 디바이스에 실음.
x_cat = x_cat.to(device)
x_num = x_num.to(device)
y = y.to(device)
optimizer.zero_grad() # 배치마다 옵티마이저 초기화.
total += x_cat.size(0)
with torch.set_grad_enabled(phase == 'train'):
# 트레인 모드 일때만 가중치 계산을 합니다.
# 이부분이 조금 신기한게 모델(입력값)을 통해 forward 함수가 어떻게 실행되는지 모르겠습니다.
output = model(x_cat, x_num)
loss = criterion(output.squeeze(), y)
# 트레인 모드일때 역전파 + 최적화
if phase == 'train':
loss.backward()
optimizer.step()
running_loss += loss.item() # loss 내 item 함수를 사용해 로스 값 출력
running_corr += (output.round() == y.unsqueeze(1)).sum().item()
epoch_loss = running_loss / total
epoch_acc = running_corr / total
if phase == 'valid' and epoch_loss < best_val_loss:
best_val_loss = epoch_loss
best_acc = epoch_acc
torch.save(model.state_dict(), model_path)
early_stop_epoch = 0
best_epoch = epoch
elif phase == 'valid':
early_stop_epoch += 1
if (early_stop_epoch >= early_stop) or (epoch == num_epoch-1) :
"Early Stop Occured on epoch" + str(epoch)
print(f'On Epoch {best_epoch}, Best Model Saved with Valid Loss {round(epoch_loss, 6)} and Acc {round(epoch_acc, 4)*100}%')
break;
model.load_state_dict(torch.load(model_path))
return model
모델을 실제로 실행시켜주는 함수를 구현했습니다. 짚고 넘어갈 건 데이터와 모델 모두 디바이스에 실어줘야 한다는 점 입니다.
상세한 코드 설명은 주석을 달았습니다.
def predict(model):
with torch.no_grad(): # 평가하는 부분이기 때문에 가중치 업데이트 기능은 꺼둡니다.
test_cat = torch.LongTensor(X_test[cat_cols].values) # int 형 데이터
test_num = torch.FloatTensor(X_test[num_cols].values) # 실수형 데이터
# squeeze는 1인 차원을 제거해줍니다.
pred = model(test_cat, test_num).squeeze()
return pred.cpu().detach().numpy()
테스트 데이터를 평가하는 부분을 함수로 구현했습니다.
def return_pred_with_random_state(random_state = 0) :
batch_size = 32
model = EMBNN(inp_oup_dims, len(num_cols)).to(device)
model.apply(weights_init)
dataloader = return_dataloaders(batch_size, random_state)
optimizer = optim.Adam(model.parameters(), lr = 0.005)
criterion = nn.BCELoss()
trained_model = train_model(model, dataloader, optimizer, criterion,
num_epoch = 300, early_stop = 10, model_path = 'EMBNN.pth')
pred = predict(trained_model)
return pred
배치 사이즈를 정하고, 모델 정의하고, 배치 정규화시 초기 가중치를 넣어주고 데이터를 로더화 시켰습니다.
다음으로 옵티마이저 아담을 정의하고, BCELoss 손실함수를 사용했습니다. 그 후 train_model 함수를 이용해 모델 학습을 시켰습니다.
마지막으로 predict 함수를 사용해 테스트 데이터의 라벨값을 추출합니다. 작성자가 함수화 한 덕에 이해가 한번에 됩니다.
pred = return_pred_with_random_state(42)
sample_submission['target'] = pred.round()
sample_submission.to_csv('airport.csv',index=False)
방금 정의한 함수를 사용하기만 하면 결과물이 바로 나옵니다.
파이토치를 연습하고자 그동안 튜토리얼 위주로 공부했는데, 실제 대회 데이터에 적용된 코드를 공부하니 느낌이 달랐습니다.
이상적으로 잘 전처리 된 튜토리얼 내 데이터와 달리 딥러닝에 적용하기 위해 쓰는 전처리 과정에서 많이 공부 했습니다.
실제 데이터 분석은 깨끗한 데이터로 하는 경우는 많이 없으니깐요.
100% 이해하지 못한 부분도 일부 존재하는데, 이번이 끝이 아니고 계속 공부할 것이기 때문에 열심히 해보겠습니다.
다음에는 자연어 처리 부분을 파이토치를 이용한 공부를 해보겠습니다.
끝으로 코드 출처를 밝힙니다. 작성자분에게 감사드립니다.
https://dacon.io/competitions/official/235871/codeshare/4517?page=2&dtype=recent