layer 1-4 : 박막의 두께
0 ~ 255 : 반사율 스펙트럼, 빛의 파장 등이 비식별화 된 데이터
박막의 두께에 영향을 미칠 수 있는 반사율 스펙트럼이나 빛의 파장 식별 수치들
데이터에 영향을 미칠 수 있는 변수들을 추려 데이터를 만든 과정은 dacon에서 추가로 정보를 공유해주셨습니다.
https://dacon.io/competitions/official/235554/talkboard/400310
2차원 행렬 (train : 810000, 230 | test : 10000, 226) 로 이루어져 csv 파일로 제공됩니다.
------------------------------- | ---------------------|----------------------|
| 0 GeForce RTX 2080 Off | 00000000:01:00.0 On | N/A |
| 0% 45C P2 50W / 275W | 1909MiB / 7979MiB | 0% Default |
+-------------------------------+----------------------+--------------------+
| 1 GeForce RTX 2080 Off | 00000000:02:00.0 Off | N/A |
| 0% 36C P8 13W / 275W | 11MiB / 7982MiB | 0% Default |
--------------------------------|----------------------|----------------------|
import math
import time
from itertools import chain
import argparse
import numpy as np
import pandas as pd
import torch
import torch.nn as nn
# for training
from torch.utils.data import Dataset, DataLoader
from tqdm.auto import tqdm
from torch.optim import Optimizer
from torch.optim.lr_scheduler import LambdaLR
데이터의 자료형 => 수치로 구성 되어있는 것을 확인했습니다.
데이터의 컬럼을(변수) 추가하거나 제거하는 등의 노력은 딱히 기울이지 않았습니다. 모델에 맡겼습니다.
데이터를 살펴보았을 때, 정답 라벨인 layer 1-4의 두께가 오름차순으로 정리되어 있었고 그대로 학습하기에는 배치학습 특성상 모델의 편향이 발생될 것이라 판단하여 random 모듈을 사용하여 shuffle 하는 작업을 진행하였습니다.
목표 : 예측 모델 구현
128개의 변수로 4개의 변수를 예측하기 위해서는 가장 기본적인 형태의 MLP (멀티퍼셉트론) 아키텍처가 적합하다 여겼고, 베이스 형태로 잡고 아키텍처 구성을 시작했습니다. ( <> 형태 )
망은 점점 넓어지는 형태에서 줄어드는 형태로 구성하였고 ((MLP) 2개 이상)), block 마다 활성화 함수를 추가하여 backward 단계에서도 미분 및 업데이트가 가능하도록 설계했습니다. (Linear, activation function, batchnorm) 이 한 block으로 구성하였고 덕분에 여러개를 쌓아서 최적화 시키면, 결과를 예측할 수 있는 근사 함수를 만들수 있도록 신경망 알고리즘을 활용했습니다.
학습 중, 망을 늘리고 깊게 할수록 이전의 정보가 다음 정보로 가는데 추가적인 설계가 필요하다 생각되어 팀원들과 Skipconnection을 적용했습니다.
각 layer 마다 skip connection를 정의하고 다음 layer의 input에 흘러갈 수 있도록 아키텍처를 설계했습니다.
class skipConnectionModel(nn.Module):
def __init__(self):
super(skipConnectionModel, self).__init__()
self.ln = LayerNorm(10000)
self.ln1 = LayerNorm(7000)
self.ln2 = LayerNorm(4000)
self.ln3 = LayerNorm(2000)
self.upblock1 = nn.Sequential(nn.Linear(226, 2000),GELU(),nn.BatchNorm1d(2000))
self.upblock2 = nn.Sequential(nn.Linear(2000,4000),GELU(),nn.BatchNorm1d(4000))
self.upblock3 = nn.Sequential(nn.Linear(4000,7000), GELU(),nn.BatchNorm1d(7000))
self.upblock4 = nn.Sequential(nn.Linear(7000,10000),GELU(),nn.BatchNorm1d(10000))
self.downblock1 = nn.Sequential(nn.Linear(10000, 7000),GELU(),nn.BatchNorm1d(7000))
self.downblock2 = nn.Sequential(nn.Linear(7000, 4000),GELU(),nn.BatchNorm1d(4000))
self.downblock3 = nn.Sequential(nn.Linear(4000, 2000),GELU(),nn.BatchNorm1d(2000))
self.downblock4 = nn.Sequential(nn.Linear(2000, 300),GELU(),nn.BatchNorm1d(300))
self.fclayer = nn.Sequential(nn.Linear(300,4))
self.dropout = nn.Dropout(0.1)
def forward(self, x):
upblock1_out = self.upblock1(x)
upblock2_out = self.upblock2(upblock1_out)
upblock3_out = self.upblock3(upblock2_out)
upblock4_out = self.upblock4(upblock3_out)
downblock1_out = self.downblock1(self.ln(upblock4_out))
skipblock1 = downblock1_out + upblock3_out
downblock2_out = self.downblock2(self.ln1(skipblock1))
skipblock2 = downblock2_out + upblock2_out
downblock3_out = self.downblock3(self.ln2(skipblock2))
skipblock3 = downblock3_out + upblock1_out
downblock4_out = self.downblock4(self.ln3(skipblock3))
output = self.fclayer(downblock4_out)
return output
test 결과를 MAE로 판단합니다.
따라서 학습시에도 실제 정답 값들 (layer 4개)과 추정 값들의 거리를 재는 MAE loss를 사용하였습니다.
실제로 torch에서는 nn.L1loss로 구현되어 있기 때문에 이를 사용하였습니다. 예측하는 변수가 4개인데 torch에서는 자동으로 계산하여 차이를 최적화합니다.
다양한 파라미터들을 바꿔보면서 학습이 잘 되는지 실험했습니다.
def update_lr(optimizer, lr):
for param_group in optimizer.param_groups:
param_group['lr'] = lr
차후에는 torch에 구현되어있는 AdamW optimizer와 scheduler를 사용하면서 learning rate 를 1e-03로 좀 더 크게 잡고 학습했을 때, 더 학습이 잘 되었습니다.
1차적으로 처음 81000개 train set 중 10000개를 validation set으로 구성해서 학습시 overfitting이 나고 있는지 판단할 수 있도록 코드를 작성했습니다.
epoch마다 돌면서 확인할 수 있도록 해서 지속적으로 확인할 수 있었습니다.
특정 loss 감소 모니터링 후, 30 > 50 > 70 > 100 > 150 > 200 > 250 epoch을 늘려가면서 loss의 추이를 살펴보았습니다.
실질적으로 test score를 측정했을 때, val loss와 거의 흡사함을 확인할 수 있었고 이는 대회에서 성능을 내는데 큰 도움이 됐습니다.
결과를 csv 파일로 쓴 후, 추가로 기존에 잘 나왔던 부분을 고려하고자 따로 MAE 함수를 정의해서 비교할 수 있도록 했습니다.
def mae(best_path, my_path):
best = pd.read_csv(best_path)
best_value = best.iloc[:,1:].values
value = pd.read_csv(my_path)
my_value = value.iloc[:,1:].values
abs_value = abs(best_value - my_value)
size = abs_value.shape
return sum(sum(abs_value)) / (size[0]*size[1])
대회에 제출할 때는 bagging 형식의 평균 앙상블 전략을 취해서 단일 모델로는 0.41 정도의 mae를 가지는 모델을 0.32(최종기록)까지 떨어트릴 수 있었습니다.
앙상블 할 때, 모델의 hidden states를 변형을 주어 다르게 구성한 것도 있고 하이퍼 파라미터 조정을 통해서 다르게 구성한 모델도 있었습니다. 단일 모델보다는 앙상블 기법이 test 성능에서는 더 좋은 결과를 보였습니다.