[tensorflow] dacon 운동 동작 분류 AI 경진대회
dacon에서 주최한 월간 데이콘 11 인 운동 동작 분류 AI 경진대회에 참가하였다.
(dacon.io/competitions/official/235689/overview/description/)
결과적으로 public 22등 / private 27 등으로 아쉽지만 동메달을 땄다.
한동안 미루던 코드를 정리해서 올리려고 한다.
일단 사용한 환경은 jupyterlab이고 실험 관리는 neptune.ai 를 사용하였다.
neptune.ai 가 매우 편하니 읽어보시는 분은 neptune.ai 사용해보시는 걸 추천
(mlflow도 있지만 이건 직접 설치, 관리해야하기 때문에 대회에서는 약간 비추)
코드
아래 코드들은 모두 하나의 .py 안에 들어있으니 필요하다면 각 블럭을 모두 복붙해서 하나의 파일에 순서대로 때려넣으면 돌아간다.
(중간에 key 부분은 직접 설정해야함)
import
import neptune
import pandas as pd
import numpy as np
import neptune
import tensorflow as tf
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, LSTM, Bidirectional, LeakyReLU, Conv1D, BatchNormalization
from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint
from tensorflow.keras.optimizers import Adam
from neptunecontrib.monitoring.keras import NeptuneMonitor
from tqdm import tqdm
import random
import os
from attrdict import AttrDict
from sklearn.model_selection import KFold
from statsmodels.tsa.seasonal import seasonal_decompose
seed 설정
SEED = 42
os.environ['PYTHONHASHSEED'] = str(SEED)
os.environ['TF_DETERMINISTIC_OPS'] = '1'
tf.random.set_seed(SEED)
np.random.seed(SEED)
random.seed(SEED)
baseline이 Tensorflow라서 그대로 진행을 하였고, 이에 맞는 seed를 설정하였다.
gpu 설정
gpus = tf.config.experimental.list_physical_devices('GPU')
if gpus:
try:
tf.config.experimental.set_memory_growth(gpus[0], True)
except RuntimeError as e:
# 프로그램 시작시에 메모리 증가가 설정되어야만 합니다
print(e)
gpu는 1개만 사용하기 때문에 gpus[0] 으로 첫번째 gpu 에 대해서만 메모리 증가 옵션을 적용해주었다.
neptune.ai 설정
def initNeptune(params):
neptune.init('<workspace>', api_token='<neptune api key>')
neptune.create_experiment(name=f'ml_modeling_v{params.version}', params=params)
# neptune.append_tag('test_tag')
neptune.append_tag(f'version_{params.version}')
neptune.ai 에 대한 설정을 지정해준다. api token 은 neptune.ai 페이지에서 발급받으면 된다.
CSV 읽어오기
def readData(train_data, train_label, test_data, submission):
train = pd.read_csv(train_data)
train_labels = pd.read_csv(train_label)
test = pd.read_csv(test_data)
submission = pd.read_csv(submission)
return train, train_labels, test, submission
csv 파일들을 전부 읽어온다.
preprocess
def preprocess(data_df):
data_df['acc_x'] /= 8
data_df['acc_y'] /= 8
data_df['acc_z'] /= 8
data_df['gy_x'] /= 200
data_df['gy_y'] /= 200
data_df['gy_z'] /= 200
X = np.reshape(np.array(data_df.iloc[:,2:]),[-1, 600, 6])
def abs_norm(abs_np):
return abs_np ** (1/2)
def angle_norm(angle_np):
return angle_np / (np.pi * 2) + 0.5
def makeDecompose(X):
neptune.append_tag('decompose')
decompose_list = []
print('make decompose')
for _x in tqdm(X):
_decompose_list = []
for _idx in range(_x.shape[-1]):
season_result = seasonal_decompose(_x[:, _idx], model='additive', freq=60)
_observed = season_result.observed
_trend = season_result.trend
_seasonal = season_result.seasonal
_resid = season_result.resid
_observed[np.isnan(_observed)] = 0
_trend[np.isnan(_trend)] = 0
_seasonal[np.isnan(_seasonal)] = 0
_resid[np.isnan(_resid)] = 0
_observed = np.fft.fft(_observed)
_trend = np.fft.fft(_trend)
_seasonal = np.fft.fft(_seasonal)
_resid = np.fft.fft(_resid)
_observed_abs = abs_norm(np.abs(_observed))
_observed_angle = angle_norm(np.angle(_observed))
_trend_abs = abs_norm(np.abs(_trend))
_trend_angle = angle_norm(np.angle(_trend))
_seasonal_abs = abs_norm(np.abs(_seasonal))
_seasonal_angle = angle_norm(np.angle(_seasonal))
_resid_abs = abs_norm(np.abs(_resid))
_resid_angle = angle_norm(np.angle(_resid))
_decompose_list.append([_observed_abs, _observed_angle,
_trend_abs, _trend_angle,
_seasonal_abs, _seasonal_angle,
_resid_abs, _resid_angle])
decompose_list.append(_decompose_list)
return np.array(decompose_list)
def makeFFT(X):
neptune.append_tag('fft')
abs_list, angle_list = [], []
print('make FFT')
for _x in tqdm(X):
_abs_list, _angle_list = [], []
for _idx in range(_x.shape[-1]):
_fft = np.fft.fft(_x[:, _idx])
_abs_list.append(np.abs(_fft))
_angle_list.append(np.angle(_fft))
abs_list.append(_abs_list)
angle_list.append(_angle_list)
abs_list = abs_norm(np.array(abs_list).transpose(0, 2, 1))
angle_list = angle_norm(np.array(angle_list).transpose(0, 2, 1))
return abs_list, angle_list
abs_list, angle_list = makeFFT(X)
decompose_list = makeDecompose(X).transpose(0,3,1,2)
decompose_list = np.reshape(decompose_list, [-1, decompose_list.shape[1], 48])
X = np.concatenate((X, abs_list, angle_list, decompose_list), axis=2)
X = np.reshape(X, [-1, 600, 18+48])
return X
3축 가속도계와 3축 자이로스코프에 대해서 normalize를 진행하였다. 3축 가속도계는 8, 자이로스코프는 200으로 나누어 주었다.
minmax도 진행해보고, outlier들에 대해 예외처리도 해보았지만 8과 200으로만 나누었을 때가 가장 성능이 좋았다.
8과 200은 quantile을 적용하여 0.9~0.95 에 해당하는 정수로 나누어주었다.
normalize를 진행한 후 각 데이터 별로 fft를 적용하였다. 그리고 각 데이터 별로 seasonal decompose를 적용하여 fft에 대한 시계열 분석을 진행하였다.
label 변환
def preprocessLabel(label_df):
return tf.keras.utils.to_categorical(label_df['label'])
categorical 예측이라 tensorflow 에 내장되어 있는 함수를 사용하였다.
Model 생성
def makeModel(params):
#가벼운 모델 생성
model = Sequential()
model.add(Conv1D(256, 5, activation='relu', padding='same', input_shape=(600, 18+48)))
model.add(Bidirectional(LSTM(256)))
model.add(Dense(61, activation='softmax'))
lr_rate = params.learning_rate if 'learning_rate' in params else 0.001
optim = Adam(learning_rate=lr_rate)
model.compile(optimizer=optim, loss='categorical_crossentropy', metrics=['accuracy'])
return model
모델의 경우, 시작할 때는 Conv1D 3층, Bi-LSTM 2층을 쌓고 hidden unit도 784, 512 등으로 더욱 컸다.
또한, Conv1D의 kernel size는 3으로 시작하였다.
preprocess에서 어느 정도 성능이 나오고 나서, 모델을 바꿔가면서 시도해봤는데 layer를 늘리는 것보다 줄이는 것이 성능이 좋게 나와서 점점 줄이다 위 모델이 제일 성능이 좋았다.
위 모델에서 Conv1D, Bi-LSTM 모두 hidden unit을 128, 256, 512, 784로 테스트해봤는데 256이 제일 성능이 좋았다.
학습
def train(data_x, data_y, params):
os.makedirs(params.weights_dir+f'_v{params.version}', exist_ok=True)
k_fold = KFold(n_splits=params.n_splits)
model_list = []
for idx, (train_idx, valid_idx) in enumerate(k_fold.split(data_x)):
_train_x, _train_y = data_x[train_idx], data_y[train_idx]
_valid_x, _valid_y = data_x[valid_idx], data_y[valid_idx]
early_stopping = EarlyStopping(monitor='val_loss', patience=10, restore_best_weights=True)
weight_save_path = os.path.join(params.weights_dir+f'_v{params.version}', 'checkpoint-kfold'+str(idx)+'.hdf5')
check_point = ModelCheckpoint(filepath=weight_save_path, monitor='val_loss', save_best_only=True, save_weights_only=True)
model = makeModel(params)
model.fit(_train_x, _train_y, validation_data=(_valid_x, _valid_y), epochs=params.epochs, batch_size=params.batch_size, callbacks=[early_stopping, check_point, NeptuneMonitor()])
neptune.log_artifact(weight_save_path)
model_list.append(model)
return model_list
학습은 k_fold로 진행하였다.
k_fold는 20까지 성능이 좋았지만 이후는 의미가 없었다.
예측
def predict(test_x, model_list, submission_df, params):
pred_list = []
_test_x = preprocess(test_x)
for _model in model_list:
_pred = _model.predict(_test_x)
pred_list.append(_pred)
pred_np = np.array(pred_list)
pred_np = pred_np.mean(axis=0)
submission_df.iloc[:,1:] = pred_np
submission_save_path = os.path.join(params.weights_dir + f'_v{params.version}', 'result.csv')
submission_df.to_csv(submission_save_path, index=False)
neptune.log_artifact(submission_save_path)
main
if __name__ == "__main__":
params = {
'n_splits': 20,
'learning_rate': 0.001,
'weights_dir': 'weights',
'version': '36',
'epochs': 100,
'batch_size': 128
}
params = AttrDict(params)
initNeptune(params)
train_df, label_df, test_df, submission_df = readData('../data/train_features.csv',
'../data/train_labels.csv',
'../data/test_features.csv',
'../data/sample_submission.csv')
train_x, train_y = preprocess(train_df), preprocessLabel(label_df)
model_list = train(train_x, train_y, params)
predict(test_df, model_list, submission_df, params)
다른 팀들의 경우 데이터에 rotation, permutation augmentation 기법을 사용하고, 모델은 transformer, 전처리는 운동 에너지 공식을 사용하여 성능을 높였었다.
1등 팀은 모델을 GRU를 썼는데 LSTM보다 성능이 높은건지, 아니면 rotation, permutation augmetation을 사용해서 성능이 높은 건지는 확인이 필요하다.
해당 경진대회에서 생각보다 더 많은 인사이트를 얻을 수 있었다.
일단 참여 목적은 등수도 등수지만 workflow를 사용해보는 것, 데이터에 대한 인사이트를 늘리는 것이 목적이었다.
시계열 데이터 + classification 문제로 구성된건 처음봐서 다른 때보다 더 넓은 인사이트를 얻을 수 있었다.