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 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/hand_classification/'

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 sensor_1 sensor_2 sensor_3 sensor_4 sensor_5 sensor_6 sensor_7 sensor_8 sensor_9 ... sensor_24 sensor_25 sensor_26 sensor_27 sensor_28 sensor_29 sensor_30 sensor_31 sensor_32 target
0 1 -6.149463 -0.929714 9.058368 -7.017854 -2.958471 0.179233 -0.956591 -0.972401 5.956213 ... -7.026436 -6.006282 -6.005836 7.043084 21.884650 -3.064152 -5.247552 -6.026107 -11.990822 1
1 2 -2.238836 -1.003511 5.098079 -10.880357 -0.804562 -2.992123 26.972724 -8.900861 -5.968298 ... -1.996714 -7.933806 -3.136773 8.774211 10.944759 9.858186 -0.969241 -3.935553 -15.892421 1
2 3 19.087934 -2.092514 0.946750 -21.831788 9.119235 17.853587 -21.069954 -15.933212 -9.016039 ... -6.889685 54.052330 -6.109238 12.154595 6.095989 -40.195088 -3.958124 -8.079537 -5.160090 0
3 4 -2.211629 -1.930904 21.888406 -3.067560 -0.240634 2.985056 -29.073369 0.200774 -1.043742 ... -2.126170 -1.035526 2.178769 10.032723 -1.010897 -3.912848 -2.980338 -12.983597 -3.001077 1
4 5 3.953852 2.964892 -36.044802 0.899838 26.930210 11.004409 -21.962423 -11.950189 -20.933785 ... -2.051761 10.917567 1.905335 -13.004707 17.169552 2.105194 3.967986 11.861657 -27.088846 2

5 rows × 34 columns

데이터를 판다스로 읽고 잘 읽혔는지 확인합니다. path는 데이터 파일이 저장되어있는 경로를 의미합니다.

데이터 살펴보기

print(train.shape)
print(test.shape)
(2335, 34)
(9343, 33)

데이터의 칼럼수는 타겟 열 제외하고 33개이고 트레인 데이터 2335개. 테스트 데이터 9343개 입니다.

데이터 개수가 2335개면 파라미터가 많은 모델을 사용하기 조금 적은 데이터이고, 테스트 데이터가 훨씬 많은 것도 특징입니다.

train.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 2335 entries, 0 to 2334
Data columns (total 34 columns):
 #   Column     Non-Null Count  Dtype  
---  ------     --------------  -----  
 0   id         2335 non-null   int64  
 1   sensor_1   2335 non-null   float64
 2   sensor_2   2335 non-null   float64
 3   sensor_3   2335 non-null   float64
 4   sensor_4   2335 non-null   float64
 5   sensor_5   2335 non-null   float64
 6   sensor_6   2335 non-null   float64
 7   sensor_7   2335 non-null   float64
 8   sensor_8   2335 non-null   float64
 9   sensor_9   2335 non-null   float64
 10  sensor_10  2335 non-null   float64
 11  sensor_11  2335 non-null   float64
 12  sensor_12  2335 non-null   float64
 13  sensor_13  2335 non-null   float64
 14  sensor_14  2335 non-null   float64
 15  sensor_15  2335 non-null   float64
 16  sensor_16  2335 non-null   float64
 17  sensor_17  2335 non-null   float64
 18  sensor_18  2335 non-null   float64
 19  sensor_19  2335 non-null   float64
 20  sensor_20  2335 non-null   float64
 21  sensor_21  2335 non-null   float64
 22  sensor_22  2335 non-null   float64
 23  sensor_23  2335 non-null   float64
 24  sensor_24  2335 non-null   float64
 25  sensor_25  2335 non-null   float64
 26  sensor_26  2335 non-null   float64
 27  sensor_27  2335 non-null   float64
 28  sensor_28  2335 non-null   float64
 29  sensor_29  2335 non-null   float64
 30  sensor_30  2335 non-null   float64
 31  sensor_31  2335 non-null   float64
 32  sensor_32  2335 non-null   float64
 33  target     2335 non-null   int64  
dtypes: float64(32), int64(2)
memory usage: 620.4 KB

데이터 칼럼명과 결측치를 확인하기 위한 함수입니다. 결측치는 없는 것으로 확인됩니다.

train['target'].value_counts()
3    599
2    593
1    574
0    569
Name: target, dtype: int64

우리가 맞춰야 하는 타겟 값의 분포입니다. 상당히 골고루 분포되어있는 걸 알 수 있습니다.

불균형하게 분포되어있다면 별도의 조정이 필요합니다만 그럴 필요는 없어보입니다.

plt.figure(figsize=[12,8])
plt.text(s="Target variables",x=0,y=1.3, va='bottom',ha='center',color='#189AB4',fontsize=25)
plt.pie(train['target'].value_counts(),autopct='%1.1f%%', pctdistance=1.1)
plt.legend(['3', '2', '1', '0'], loc = "upper right",title="Programming Languages", prop={'size': 15})
plt.show()

방금 살펴본 내용을 파이 그래프를 통해 시각화 하였습니다.

train.describe().T
count mean std min 25% 50% 75% max
id 2335.0 1168.000000 674.200761 1.000000 584.500000 1168.000000 1751.500000 2335.000000
sensor_1 2335.0 -1.122174 11.486353 -94.746969 -4.036597 -0.951398 2.895540 68.876142
sensor_2 2335.0 -1.024673 7.399859 -63.942094 -4.031957 -1.015582 2.140456 39.913391
sensor_3 2335.0 -0.672769 26.519159 -122.195138 -14.878500 -0.961088 13.974075 127.124171
sensor_4 2335.0 -0.147724 15.551500 -111.870691 -7.116633 -0.890469 6.110973 102.015561
sensor_5 2335.0 -0.327494 11.461970 -94.147972 -3.968687 -0.871690 2.970387 89.059852
sensor_6 2335.0 -0.423462 7.314322 -70.916786 -3.957699 -0.804810 3.006144 34.923040
sensor_7 2335.0 0.676275 26.869479 -105.956553 -13.937806 0.058910 13.934438 120.046277
sensor_8 2335.0 -0.936019 15.598104 -102.965354 -8.053214 -1.095551 4.955494 125.160611
sensor_9 2335.0 -0.797432 12.015022 -81.268085 -4.031148 -0.944613 2.235557 74.101715
sensor_10 2335.0 -0.704585 7.384626 -47.937561 -3.983620 -0.932964 2.883284 47.030119
sensor_11 2335.0 -1.099322 26.262009 -115.943693 -15.165419 -1.116522 13.022905 127.110419
sensor_12 2335.0 -0.843473 15.498328 -102.916207 -8.082508 -1.054003 6.021600 99.932331
sensor_13 2335.0 -0.491915 11.894939 -115.053373 -3.893967 -0.908079 2.992981 107.910041
sensor_14 2335.0 -0.851473 7.401702 -59.689434 -3.982224 -0.937905 2.854699 40.026878
sensor_15 2335.0 -0.344029 25.815937 -107.985386 -14.953749 -0.858820 12.965905 126.981907
sensor_16 2335.0 -1.128676 15.513633 -126.950747 -8.096568 -1.004242 5.508252 120.974880
sensor_17 2335.0 -0.959658 11.654236 -95.956853 -4.038010 -0.947597 2.895085 85.952050
sensor_18 2335.0 -0.639778 7.586333 -83.854213 -3.996916 -0.967231 2.876743 39.993408
sensor_19 2335.0 -0.559455 26.885734 -108.964270 -15.179515 -0.964579 13.978336 117.934200
sensor_20 2335.0 -0.658692 15.936823 -108.094304 -7.851749 -1.013369 5.917309 121.026042
sensor_21 2335.0 -0.611461 11.942224 -103.876936 -4.002134 -0.942706 2.948692 102.882569
sensor_22 2335.0 -0.741168 7.548507 -59.993001 -3.973502 -0.968065 2.920789 40.917741
sensor_23 2335.0 0.027448 26.671928 -93.171275 -14.102903 -1.104314 12.137937 121.959404
sensor_24 2335.0 -0.356441 16.531906 -127.797649 -7.980628 -0.926120 6.002985 127.161055
sensor_25 2335.0 -0.927744 12.021560 -99.115177 -4.004750 -0.907301 2.863184 58.113657
sensor_26 2335.0 -0.589060 7.440983 -86.193378 -4.001112 -0.897015 2.951682 59.105536
sensor_27 2335.0 -0.081374 25.923355 -105.751637 -14.096840 -0.954791 13.903783 123.179253
sensor_28 2335.0 -0.370812 15.541803 -105.890010 -8.004561 -0.989293 5.922250 111.137925
sensor_29 2335.0 -0.726941 11.636507 -74.977182 -3.981055 -0.889780 2.972719 54.098746
sensor_30 2335.0 -0.809534 7.469744 -74.006065 -3.988965 -0.928504 2.519426 35.896503
sensor_31 2335.0 -0.495062 25.291238 -121.097086 -13.998874 -0.955684 13.926128 125.974107
sensor_32 2335.0 -0.743585 16.300385 -123.876153 -7.873898 -1.019547 5.121679 104.959621
target 2335.0 1.523340 1.118221 0.000000 1.000000 2.000000 3.000000 3.000000

데이터의 분포를 보다 자세히 살펴보기 위해 describe 함수를 사용했습니다. 칼럼수가 다소 많기 때문에 보기 편하기 위해 T 함수를 사용했습니다.

변수들의 이름이 모두 sensor_ 형태로 구성되어 있습니다. 또 평균값이 모두 0 근처에 있군요.

변수들마다 조금씩 차이는 있지만 대체로 최솟값은 -130 밑으로는 안떨어지고 최댓값도 +130 위로는 올라가지 않습니다.

변수들의 분포가 상당히 유사해 보이는데요. 이미지의 하나의 픽셀값 같이 하나의 데이터를 32분할한 데이터로 추측됩니다.

이런 데이터는 변수간 일관성이 있기 때문에 딥러닝 기반 모델이 효율적일 것으로 판단됩니다.

데이터 스케일링

train_x = train.drop(['id', 'target'], axis = 1)
test_x = test.drop(['id'], axis = 1)

mins = train_x.min()
maxs = train_x.max()
mins[:5]
sensor_1    -94.746969
sensor_2    -63.942094
sensor_3   -122.195138
sensor_4   -111.870691
sensor_5    -94.147972
dtype: float64

데이터 내 칼럼별로 최솟값, 최댓값을 추출했습니다. 데이터들을 스케일링 하기 위한 목적입니다.

train_x = (train_x - mins) / (maxs - mins)
test_x = (test_x - mins) / (maxs - mins)
train_x.describe().T[['min', 'max']]
min max
sensor_1 0.0 1.0
sensor_2 0.0 1.0
sensor_3 0.0 1.0
sensor_4 0.0 1.0
sensor_5 0.0 1.0
sensor_6 0.0 1.0
sensor_7 0.0 1.0
sensor_8 0.0 1.0
sensor_9 0.0 1.0
sensor_10 0.0 1.0
sensor_11 0.0 1.0
sensor_12 0.0 1.0
sensor_13 0.0 1.0
sensor_14 0.0 1.0
sensor_15 0.0 1.0
sensor_16 0.0 1.0
sensor_17 0.0 1.0
sensor_18 0.0 1.0
sensor_19 0.0 1.0
sensor_20 0.0 1.0
sensor_21 0.0 1.0
sensor_22 0.0 1.0
sensor_23 0.0 1.0
sensor_24 0.0 1.0
sensor_25 0.0 1.0
sensor_26 0.0 1.0
sensor_27 0.0 1.0
sensor_28 0.0 1.0
sensor_29 0.0 1.0
sensor_30 0.0 1.0
sensor_31 0.0 1.0
sensor_32 0.0 1.0

(데이터 - 최솟값) / (최댓값 - 최솟값) 연산을 거치게 되면 데이터 값들이 모두 0~1 사이로 가지게 됩니다.

딥러닝에서 입력값을 표준화 시키는 것이 상당히 중요합니다.

데이터 로더 만들기

import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torch.utils.data import DataLoader, TensorDataset


import random
random.seed(42)
torch.manual_seed(42)
<torch._C.Generator at 0x7ff6406a2030>

딥러닝에 필요한 파이토치 관련 패키지들을 설치합니다.

train_x = torch.from_numpy(train_x.to_numpy()).float()
train_y = torch.tensor(train['target'].to_numpy(), dtype = torch.int64)

test_x = torch.from_numpy(test_x.to_numpy()).float()

train_x
tensor([[0.5415, 0.6067, 0.5264,  ..., 0.6256, 0.4657, 0.4889],
        [0.5654, 0.6060, 0.5106,  ..., 0.6646, 0.4742, 0.4719],
        [0.6957, 0.5955, 0.4939,  ..., 0.6374, 0.4574, 0.5188],
        ...,
        [0.5240, 0.6733, 0.4574,  ..., 0.5721, 0.4617, 0.5373],
        [0.5547, 0.6447, 0.4422,  ..., 0.6371, 0.5505, 0.4885],
        [0.5678, 0.5481, 0.3061,  ..., 0.6840, 0.4012, 0.4843]])

데이터가 판다스의 데이터 프레임 형식으로 저장되어 있습니다. 데이터 프레임을 넘파이로, 다시 넘파이를 텐서 형태로 바꾼 모습입니다.

파이토치 모델을 이용하기 위해서 데이터는 텐서형태로 변환해주어야 합니다.

train_dataset = TensorDataset(train_x, train_y)

print(train_dataset.__len__())
print(train_dataset.__getitem__(1))
2335
(tensor([0.5654, 0.6060, 0.5106, 0.4722, 0.5095, 0.6418, 0.5882, 0.4123, 0.4846,
        0.4620, 0.4892, 0.4825, 0.5209, 0.6385, 0.3529, 0.5038, 0.5435, 0.6448,
        0.5070, 0.4935, 0.5120, 0.5833, 0.5024, 0.4934, 0.5799, 0.5716, 0.5003,
        0.5383, 0.6573, 0.6646, 0.4742, 0.4719]), tensor(1))

입력한 데이터를 파이토치에서 지원하는 TensorDataset 함수를 사용해 데이터 셋 형태로 만들었습니다.

데이터가 데이터 셋 내 잘 입력 됬는지 len, getitem 함수를 통해 확인했네요.

바로 뒤에 나오는 데이터 로더를 사용하기 위해선 데이터 셋 형태로 데이터를 만들어야 합니다.

train_dataloader = DataLoader(train_dataset, batch_size = 16, shuffle = True)

for batch_idx, samples in enumerate(train_dataloader):
    if batch_idx > 0:
        break
    print(samples[0].shape)
    print(samples[1])
torch.Size([16, 32])
tensor([1, 3, 1, 1, 1, 2, 1, 0, 3, 0, 3, 0, 3, 2, 2, 0])

데이터 셋을 파이토치 내 DataLoader 함수에 넣어 데이터 로더를 만들었습니다.

데이터 로더 형식을 이용하면 배치단위로 데이터를 모델에 넣을 수 있고 shuffle 인자를 사용해 데이터를 쉽게 섞을 수도 있습니다.

모델 적합하기

class Models(nn.Module):
    def __init__(self):
        super().__init__()
        self.linear_relu_stack = nn.Sequential(
            nn.Linear(32, 64),
            nn.BatchNorm1d(64),
            nn.ReLU(),
            nn.Dropout(0.2),
            nn.Linear(64, 128),
            nn.BatchNorm1d(128),
            nn.ReLU(),
            nn.Dropout(0.2),
            nn.Linear(128, 4),
        )

    def forward(self, x):
        x = self.linear_relu_stack(x)
        return x

model = Models()

print(model)
Models(
  (linear_relu_stack): Sequential(
    (0): Linear(in_features=32, out_features=64, bias=True)
    (1): BatchNorm1d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    (2): ReLU()
    (3): Dropout(p=0.2, inplace=False)
    (4): Linear(in_features=64, out_features=128, bias=True)
    (5): BatchNorm1d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    (6): ReLU()
    (7): Dropout(p=0.2, inplace=False)
    (8): Linear(in_features=128, out_features=4, bias=True)
  )
)

간단한 딥러닝 모델을 직접 제작했습니다. 파이토치 내 nn.Module 클래스를 상속받았습니다.

32개에 입력 데이터를 받아 64개, 128개로 은닉 층 내 노드 개수를 늘리다가 예측 값이 4개이기 때문에 최종 출력 노드는 4개로 구성했습니다.

BatchNorm1d와 Dropout을 이용해 배치 정규화와 드롭아웃 기능을 사용했으며 활성화 함수로는 Relu를 사용했습니다.

데이터가 적기 때문에 파라미터수가 많으면 안될 것 같아서 층을 적게 쌓았습니다.

criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.005)

for epoch in range(20):
    
    running_loss = 0.0
    accuracy = 0
    for i, data in enumerate(train_dataloader, 0):
        inputs, labels = data

        optimizer.zero_grad() # 매개변수를 0으로 만듭니다. 매 학습시 초기화해줘야합니다.
        outputs = model(inputs) # 입력값을 넣어 순전파를 진행시킵니다.

        loss = criterion(outputs, labels) # 모델 출력값와 실제값을 손실함수에 대입합니다.
        loss.backward() # 손실함수에서 역전파 수행합니다.
        optimizer.step() # 옵티마이저를 사용해 매개변수를 최적화합니다.

        running_loss += loss.item()

        _, predictions = torch.max(outputs, 1)

        for label, prediction in zip(labels, predictions):
            if label == prediction:
                accuracy = accuracy + 1

    
    print(f'{epoch + 1} 에포크 loss: {running_loss / i:.3f}')
    print(f'{epoch + 1} 에포크 정확도: {accuracy / (i * 16):.3f}')
1 에포크 loss: 1.222
1 에포크 정확도: 0.453
2 에포크 loss: 0.986
2 에포크 정확도: 0.605
3 에포크 loss: 0.891
3 에포크 정확도: 0.631
4 에포크 loss: 0.857
4 에포크 정확도: 0.657
5 에포크 loss: 0.807
5 에포크 정확도: 0.686
6 에포크 loss: 0.790
6 에포크 정확도: 0.685
7 에포크 loss: 0.730
7 에포크 정확도: 0.719
8 에포크 loss: 0.721
8 에포크 정확도: 0.719
9 에포크 loss: 0.743
9 에포크 정확도: 0.715
10 에포크 loss: 0.695
10 에포크 정확도: 0.736
11 에포크 loss: 0.699
11 에포크 정확도: 0.727
12 에포크 loss: 0.692
12 에포크 정확도: 0.732
13 에포크 loss: 0.683
13 에포크 정확도: 0.734
14 에포크 loss: 0.672
14 에포크 정확도: 0.736
15 에포크 loss: 0.655
15 에포크 정확도: 0.745
16 에포크 loss: 0.640
16 에포크 정확도: 0.761
17 에포크 loss: 0.631
17 에포크 정확도: 0.761
18 에포크 loss: 0.647
18 에포크 정확도: 0.755
19 에포크 loss: 0.660
19 에포크 정확도: 0.750
20 에포크 loss: 0.600
20 에포크 정확도: 0.776

20 에포크를 진행했으며 에포크마다 학습이 잘 되어가는지를 평가했습니다. 옵티마이저로 무난한 Adam을 사용했습니다.

손실함수는 CrossEntropyLoss을 사용했는데, 타겟값을 원-핫 인코딩 하지 않아도 알아서 적용시켜주며 소프트맥스함수까지 알아서 적용해줘서 손실값을 구해주는 편리한 함수 입니다.

모델 평가

model.eval() # 모델을 평가모드로 바꿉니다. dropout이 일어나지 않습니다.

with torch.no_grad(): # 이 안의 코드는 가중치 업데이트가 일어나지 않습니다.
    outputs = model(test_x)
    _, pred = torch.max(outputs, 1)

pred
tensor([0, 0, 1,  ..., 2, 0, 3])

앞서 구한 모델에 테스트 데이터를 넣어서 결과를 출력합니다. torch.max 함수가 매우 편리합니다.

sample_submission['target'] = pred.numpy()
sample_submission['target'].value_counts()
1    3056
0    2239
2    2034
3    2014
Name: target, dtype: int64

테스트 데이터의 타겟 예측 값 분포 입니다. 다소 불균형이 있지만 그래도 모델이 어느정도 기능을 하는 것 같습니다.

sample_submission.to_csv('dacon_hands_4.csv',index=False)

최종 결과를 csv 형태로 저장합니다.

느낀점

역시 간간히 진행하지 않으면 실력이 금방 녹쓰는 것 같습니다. 간단한 딥러닝 코드를 쓰는 것도 쉽지 않네요.

모델을 더 최적화 시킬수 있을 것 같습니다. 층 개수 조정, 히든층 노드 수 변경 등 여러가지를 시도할 수 있겠네요.

데이터가 적어서 딥러닝 모델이 제 성능을 발휘할까 의심이 있었는데 꽤 괜찮은 모습을 보인 것 같습니다.

딥러닝 코드는 여기서 많이 참고했습니다. 감사합니다.

(http://www.gisdeveloper.co.kr/?p=8443)