Open In Colab

필요한 패키지와 데이터 불러오기

from google.colab import drive
drive.mount('/content/drive')
Drive already mounted at /content/drive; to attempt to forcibly remount, call drive.mount("/content/drive", force_remount=True).

데이터가 저장되어 있는 구글 드라이브와 연동합니다.

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()
id Gender Customer Type Age Type of Travel Class Flight Distance Seat comfort Departure/Arrival time convenient Food and drink Gate location Inflight wifi service Inflight entertainment Online support Ease of Online booking On-board service Leg room service Baggage handling Checkin service Cleanliness Online boarding Departure Delay in Minutes Arrival Delay in Minutes target
0 1 Female disloyal Customer 22 Business travel Eco 1599 3 0 3 3 4 3 4 4 5 4 4 4 5 4 0 0.0 0
1 2 Female Loyal Customer 37 Business travel Business 2810 2 4 4 4 1 4 3 5 5 4 2 1 5 2 18 18.0 0
2 3 Male Loyal Customer 46 Business travel Business 2622 1 1 1 1 4 5 5 4 4 4 4 5 4 3 0 0.0 1
3 4 Female disloyal Customer 24 Business travel Eco 2348 3 3 3 3 3 3 3 3 2 4 5 3 4 3 10 2.0 0
4 5 Female Loyal Customer 58 Business travel Business 105 3 3 3 3 4 4 5 4 4 4 4 4 4 5 0 0.0 1

데이터 분석에 기본적으로 필요한 패키지를 임포트하고, 데이터 csv 파일 또한 임포트 합니다.

X = train.drop(columns = ['id', 'target'], axis = 1)
X.shape
(3000, 22)

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()
Gender Customer Type Type of Travel Age_group Departure Delay in Minutes_group Arrival Delay in Minutes_group Flight Distance_group Leg room service Inflight entertainment Seat comfort On-board service Class Baggage handling Ease of Online booking Cleanliness Checkin service Online support Online boarding Gate location Food and drink Departure/Arrival time convenient Inflight wifi service
0 0 0 0 1 1 1 2 4 3 3 4 1 3 4 4 3 3 4 2 3 0 4
1 0 1 0 2 2 2 4 4 4 2 4 0 1 5 4 0 2 2 3 4 4 1
2 1 1 0 3 1 1 4 4 5 1 3 0 3 4 3 4 4 3 0 1 1 4
3 0 0 0 1 1 1 3 4 3 3 1 1 4 3 3 2 2 3 2 3 3 3
4 0 1 0 4 1 1 1 4 4 3 3 0 3 4 3 3 4 5 2 3 3 4

앞서 말한 방식대로 데이터 종류별로 전처리를 진행했습니다.

문자 형식 데이터의 경우 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)
hello world!
hello world!
hello world!
On Epoch 29, Best Model Saved with Valid Loss 0.007403 and Acc 90.53%

방금 정의한 함수를 사용하기만 하면 결과물이 바로 나옵니다.

느낀점

파이토치를 연습하고자 그동안 튜토리얼 위주로 공부했는데, 실제 대회 데이터에 적용된 코드를 공부하니 느낌이 달랐습니다.

이상적으로 잘 전처리 된 튜토리얼 내 데이터와 달리 딥러닝에 적용하기 위해 쓰는 전처리 과정에서 많이 공부 했습니다.

실제 데이터 분석은 깨끗한 데이터로 하는 경우는 많이 없으니깐요.

100% 이해하지 못한 부분도 일부 존재하는데, 이번이 끝이 아니고 계속 공부할 것이기 때문에 열심히 해보겠습니다.

다음에는 자연어 처리 부분을 파이토치를 이용한 공부를 해보겠습니다.

끝으로 코드 출처를 밝힙니다. 작성자분에게 감사드립니다.

https://dacon.io/competitions/official/235871/codeshare/4517?page=2&dtype=recent