Dacon 전복 나이 예측 경진대회

48099 단어 dacondacon

이번에 처음으로 데이콘 경진대회에 참여해보았다. 3월 21일부터 4월 1일까지 진행된 전복 나이 예측 경진대회로, 비교적 간단한 데이콘 베이직 대회임에도 불구하고 EDA부터 모델링까지 해보며 많은 것을 배울 수 있었다. 특히 스터디원 분들께서 좋은 내용을 많이 공유해주셔서 너무 감사했던 😭 (저도 더 분발하겠습니다..)

🦪 전복 나이 예측 경진대회

주어진 전복 데이터를 통해 전복의 나이를 예측하는 경진대회

데이터

1. train.csv : 학습 데이터

  • id : 샘플 아이디
  • Gender : 전복 성별
  • Lenght : 전복 길이
  • Diameter : 전복 둘레
  • Height : 전복 키
  • Whole Weight : 전복 전체 무게
  • Shucked Weight : 껍질을 제외한 무게
  • Viscra Weight : 내장 무게
  • Shell Weight : 껍질 무게
  • Target : 전복 나이

2. test.csv : 테스트 데이터
학습 데이터와 동일

3. sample_submissoin.csv

  • id : 샘플 아이디
  • Target : 전복 나이

코드

EDA

train.info()


Dtype이 objectGender 변수가 눈에 띈다. 추후 전처리 과정에서 인코딩이 필요할 것으로 보인다.

print(f'train: {train.shape}')
print(f'test: {test.shape}')

# output
# train: (1253, 10)
# test: (2924, 9)
train.isnull().sum().to_frame('nan_count')


결측치가 존재하지 않으므로 결측치 전처리 과정은 불필요하다.

train = train.drop(['id'], axis = 1) # id 열 제거

id는 고유번호(일련번호)와 같으므로 제거해준다.

위와 같이 이상치 판별도 해보았다. 처음에는 z-method를 이용하였으나, 모든 행이 이상치로 판별되는 상황이 발생해 IQR method을 이용했다. 하지만 결론적으로 모델링 과정에서 이상치를 제거한 경우에 성능이 떨어지는 것을 확인하여, 원 데이터 그대로 모델링을 진행하였다.

Target(나이)

temp = train['Target'].unique()
print(np.sort(temp))

plt.figure(figsize=(20,10))
sns.countplot('Target', data=train)
plt.title("Abalone age by count", fontsize = 30)
plt.xlabel("target(age)")
plt.ylabel("count")
plt.show()

Gender

plt.figure(figsize=(10,5))
sns.countplot('Gender', data=train)
plt.title("Abalone gender by count", fontsize = 20)
plt.show()

# kdeplot은 확률밀도가 추정되어 범주형 변수를 연속적으로 만들어줌

plt.figure(figsize=(10,5))
sns.kdeplot('Target', hue='Gender', data=train)
plt.title("Abalone age by gender", fontsize=30)
plt.show()

Target and Features

train_corr = train.drop(['Gender'], axis=1)
scaler = MinMaxScaler()
train_corr[train_corr.columns] = scaler.fit_transform(train_corr[train_corr.columns])
corr28 = train_corr.corr('pearson')
s28 = corr28.unstack()
df_temp28 = pd.DataFrame(s28['Target'].sort_values(ascending=False), columns=['Target'])
df_temp28.style.background_gradient()


Shell Weight가 가장 높은 양의 상관관계를 보인다.
이는 나이가 많을수록 껍질의 무게가 무거워진다는 것을 의미한다.
또한 피어슨 상관계수가 0.3 이상은 상관관계가 유의미하다는 것을 의미하므로, 전복의 무게, 키, 지름, 둘레가 나이가 많을수록 늘어난다는 것을 알 수 있다.

Features

plt.figure(figsize=(10,10))

features = train.drop(['Gender','Target'], axis=1)
heat_table = features.corr()
mask = np.zeros_like(heat_table)
mask[np.triu_indices_from(mask)] = True
heatmap_ax = sns.heatmap(heat_table, annot=True, mask = mask, cmap='coolwarm')
heatmap_ax.set_xticklabels(heatmap_ax.get_xticklabels(), fontsize=20, rotation=45)
heatmap_ax.set_yticklabels(heatmap_ax.get_yticklabels(), fontsize=20, rotation=45)
plt.title('correlation between features', fontsize=20)
plt.show()

%%time

f1 = features

scaler = MinMaxScaler()
f1[f1.columns] = scaler.fit_transform(f1[f1.columns])

vif_data = pd.DataFrame()
vif_data["features"] = f1.columns
vif_data["VIF"] = [variance_inflation_factor(f1.values.astype("float32"), i) for i in range(f1.shape[1])]

vif_data.sort_values("VIF")

feature들 간의 상관관계가 높다. 정규화 후에도 vif 값이 높게 나오며, feature들 간의 높은 상관관계는 다중공선성 문제를 발생시킨다.
따라서 회귀 모델을 사용하려면 다른 방법으로 정규화를 해주거나, 특정 피처들만 이용해서 모델링을 해야할 것으로 보인다.

전처리

범주형 변수 처리

train = pd.get_dummies(data = train, columns = ['Gender'], prefix = 'Gender')
test = pd.get_dummies(data = test, columns = ['Gender'], prefix = 'Gender')


Gender는 범주형 변수이기에 get_dummies를 이용해서 인코딩해주었다.

데이터 분할

train_X = train.drop(['id', 'Target'], axis=1)
train_y = train.Target

test.drop(['id'], axis=1, inplace=True)

새로운 변수 추가

변수의 선형 결합을 이용해 새로운 변수를 추가
다중공선성이 높아지지만 딥러닝 모델을 사용할 예정이기에 이는 고려하지 않음
(회귀 모델 사용시 고려해야 함)

모델링 과정에서 변수를 포함시키기도, 제외시키기도 했습니다. 결론적으로 val_mae 값이 가장 낮게 나온 모델의 결과를 제출하였는데 그 때 사용한 변수가 정확히 기억이 나지 않네요..ㅎㅎ 대략 6~7개 정도의 새로운 변수를 사용했던 거 같습니다.

  1. ratio = 껍질을 제외한 무게 / 전체 무게
train_X['ratio'] = train_X['Shucked Weight']/train_X['Whole Weight']
test['ratio'] = test['Shucked Weight']/test['Whole Weight']
  1. w_origin = 내장 무게 + 껍질을 제외한 무게
train_X['w_origin'] = train_X['Viscra Weight'] + train_X['Shucked Weight']
test['w_origin'] = test['Viscra Weight'] + test['Shucked Weight']
  1. foreign body = 전체 무게 - (껍질 제외 무게 + 내장 무게 + 껍질 무게)
    전복 전체 무게 대비 불필요한 물질들의 값 (이물질, 핏물 등)
train_X['foreign body'] = train_X['Whole Weight'] - (train_X['Shucked Weight'] + train_X['Viscra Weight'] + train_X['Shell Weight'])
test['foreign body'] = test['Whole Weight'] - (test['Shucked Weight'] + test['Viscra Weight'] + test['Shell Weight'])

# 음수 값은 0으로 처리했을 때보다, min 값으로 처리했을 때 높은 성능을 보임
train_X.loc[train_X['foreign body'] < 0 , 'foreign body'] = np.min(train_X[train_X['foreign body'] > 0])['foreign body']
test.loc[test['foreign body'] < 0 , 'foreign body'] = np.min(test[test['foreign body'] > 0])['foreign body']
  1. water = 전체 무게 - (껍질 무게 + 껍질 제외한 무게)
train_X['water'] = train_X['Whole Weight'] - (train_X['Shell Weight'] + train_X['Shucked Weight'])
test['water'] = test['Whole Weight'] - (test['Shell Weight'] + test['Shucked Weight'])

# 음수 값은 0으로 처리했을 때보다, min 값으로 처리했을 때 높은 성능을 보임
train_X.loc[train_X['water'] < 0 , 'water'] = np.min(train_X[train_X['water'] > 0])['water']
test.loc[test['water'] < 0 , 'water'] = np.min(test[test['water'] > 0])['water']
  1. area = 지름/2 키/2 pi
train_X['area'] = train_X['Diameter']/2 * train_X['Height']/2 * np.pi
test['area'] = test['Diameter']/2 * test['Height']/2 * np.pi
  1. volume = (키 x 길이 x 둘레/pi) / 무게
train_X['volume'] = (train_X['Lenght']/2) * (train_X['Height']/2) * (train_X['Diameter']/(2*np.pi)) * 3/4 * np.pi
test['volume'] = (test['Lenght']/2) * (test['Height']/2) * (test['Diameter']/(2*np.pi)) * 3/4 * np.pi
  1. n_ratio = 이물질 / 둘레
train_X['n_ratio'] = train_X['foreign body'] / (train_X['Shucked Weight']/train_X['Whole Weight'])
test['n_ratio'] = test['foreign body'] / (test['Shucked Weight']/test['Whole Weight'])
  1. new = 무게 ratio * 길이 ratio
train_X['new'] = train_X['Viscra Weight'] / train_X['Shucked Weight'] * train_X['Height'] / train_X['Lenght']
test['new'] = test['Viscra Weight'] / test['Shucked Weight'] * test['Height'] / test['Lenght']

모델링

# 딥러닝 모델 선언
model.add(Dense(16, input_dim=15, activation='LeakyReLU'))
model.add(Dense(32, activation='elu'))    
model.add(Dense(64, activation='LeakyReLU'))
model.add(Dropout(0.3))
model.add(Dense(64, activation='LeakyReLU'))
model.add(Dense(32, activation='elu'))
model.add(Dense(16, activation='LeakyReLU'))
model.add(Dense(1))

model.compile(loss='mean_absolute_error',
              optimizer='Nadam',
              metrics=['mae'])
# 모델 저장 폴더 만들기
MODEL_DIR = './model/'
if not os.path.exists(MODEL_DIR):
    os.mkdir(MODEL_DIR)

modelpath = "./model/{epoch:02d}-{val_loss:.4f}.hdf5"

# 모델 업데이트 및 저장
cp = ModelCheckpoint(filepath=modelpath, monitor='val_mae', verbose=0, save_best_only=True, mode = 'min')

# 학습 자동 중단 설정
es = EarlyStopping(monitor='val_mae', patience=100, mode='min')

# 모델의 개선이 없을 경우 learning rate를 조절해 모델의 개선 유도
rlrp = ReduceLROnPlateau(monitor='val_mae', factor=0.8, patience=100, mode='min')
model.fit(train_X, train_y, validation_split=0.3, epochs=1000, batch_size=32, verbose=1, callbacks=[es, cp, rlrp])

다양한 모델을 시도해보다가, 데이터 양이 적어 성능이 좋지 않을 것이라 생각했던 딥러닝 모델이 예상 외로 선방했다는 사실을 알게 되었다. 전반적인 코드는 즈믄님께서 올려주신 코드를 참고하였다.

활성화 함수로 사용한 eluLeakyReLU는 기존 ReLU의 Dying ReLU 문제를 보완한 활성화 함수이다. eluLeakyReLU 중 하나의 함수를 사용했을 때보다 두 개를 혼합하여 층을 쌓았을 때 높은 성능을 보임을 알 수 있었고, 데이터 양이 적은 편이다보니 dropout 비율을 0.5로 설정했을 때보다, 0.3으로 설정했을 때 미세한 성능 향상을 보였다.

중간에 층을 하나 더 쌓아보기도 했지만 train set에서만 에러 값이 낮아질 뿐, validation set에서의 에러 값은 높아졌다. 데이터가 작을 경우에는, DNN 모델을 쌓을 때 층이 많을수록 과적합되기 쉽다는 점을 유의해야겠다.

dropout 외에도 earlystopping, ReduceLROnPlateau 등을 통해 모델이 overfitting(과적합)되는 것을 방지하였다. 또한, validation_split의 비율도 여러번 조정해보았으니 30%로 설정했을 때의 성능이 가장 높았다.

초반에는 val_mae 값이 0.157 정도에서 머무르다가, 새로운 feature들을 정의해주고, 활성화 함수를 조정해준 뒤에 val_mae 값이 0.152까지 낮아졌다. 모델을 여러번 돌려보고, validation set의 에러 값이 가장 낮게 나온 모델의 예측 값을 제출하였다.

예측

Y_prediction = model.predict(test)

결과

결론적으로 Public 64위, Private 53위의 결과를 얻었다.

이 또한 내 힘이 아닌 코드 및 팁들을 공유해주신 다른 참가자 분들께서 만들어주신 결과라고 생각한다. 언젠가는 코드 공유나 토크 페이지를 보지 않고 온전한 내 실력으로 순위권 안에 드는 날이 왔으면 좋겠다.

대회 기간 중에 하필 전공 시험이 있는 바람에..ㅎㅎ 대회 마감 전 하루 전부터 모델링을 시작했는데, 하루 3번 제출 제한이 생각보다 정말 타이트했다 😭

그래서 이번주부터 시작된 뉴스 그룹 분류 경진대회에서는 정말 간단한 모델이라도 하루에 세번씩 꼭 제출해보려 한다. 이번 대회 주제가 교육 세션 이후로 처음 접하는 NLP라 난항을 겪을 듯 싶지만, 일주일 동안 열심히 공부하며 다양한 시도를 해봐야겠다.

전복 나이 예측 경진대회를 주제로 스터디를 진행하며, 스터디원 분들께 AutoML, pandas-profiling 등 여러가지 머신러닝 툴과 데이터를 바라보는 다각적인 시각을 배울 수 있었다. 스터디를 통해 얻어가는 게 많은 만큼 나도 더 노력해서 도움이 되는 스터디원이 되어보자.. 파이팅..!

마지막으로 private score 상위권 분들은 주로 신경망 모델을 앙상블하신 거 같았다. 캐글 프로젝트를 하면서도 느꼈는데 앙상블은 성능 향상에 확실히 도움이 되는 거 같다. 반면에, 성별을 M/FI 두가지로 분류해, 성별에 따른 두가지의 DNN 모델을 만드신 분도 있었다.

확실히 다른 성별에 비해 모든 feature에서 낮은 값을 보이는 I 성별은 따로 모델링을 해주는 게 효과적이었던 것 같다. (I = Infant : 아직 성장 중인 유아 전복을 의미)

모델 자체는 단순했지만, 다른 분포를 띄는 feature를 분류하여 모델링한 것이 높은 성능 향상을 보였다. 이를 통해 데이터 탐색 및 전처리 과정의 중요성을 다시 한번 깨달을 수 있었다.

좋은 웹페이지 즐겨찾기