글 번역2020. 9. 9. 19:43
반응형

Part 1 링크 : 

 

Kaggle Ensembling Guide(mlwave) / 캐글 앙상블 가이드 한글 번역 / Part. 1

원글은 다음 링크의 글입니다. Kaggle Ensembling Guide | MLWave Model ensembling is a very powerful technique to increase accuracy on a variety of ML tasks. In this article I will share my ensembling..

jamm-notnull.tistory.com

 

이 글은 위 문서에 대한 자의적인 한글 번역본입니다. 번역을 하면서 이해한대로 설명을 더 추가할것이고, 아무리 시도해도 이해가 되지 않은 부분은 빠진 내용이 있을 수 있습니다. 영어 실력의 부족으로 번역이 매끄럽지 않을 수 있습니다. 댓글로 번역, 내용을 지적해주시면 감사한 마음으로 수정할 수 있도록 하겠습니다. 본문이 매우, 매우, 매우 긴 관계로 몇 편의 글로 끊어서 올릴 수 있습니다. 

번역상 '나' 는 필자가 아니라, 원 글의 저자임을 밝힙니다.

 

 

 

스태킹 앙상블 & 블렌딩 (Stacked Generalization & Blending)

결과물들을 평균하는 방법은 아주 쉽고 괜찮은 방법이지만, 최고 순위의 캐글러들이 사용하는 유일한 방법은 아닙니다. 

 

Netflix 대회

Netflix 측에서는 꽤나 인기를 끌었던 데이터 사이언스 대회를 주최했습니다. 참가자들은 영화를 추천해주는 추천 시스템을 만들어야 하는데, 참가자들은 그들의 결과물들을 앙상블하는 것을 예술의 경지까지 끌어올렸습니다. 넷플릭스는 결국 우승자의 솔루션을 도입하지 않기로 결정했는데, 그 이유는 '너무 복잡하기 때문' 이었습니다. 

그럼에도 불구하고, 이 대회로 인해서 몇개의 논문들과 참신한 방법들이 나왔습니다. 

상당히 흥미롭습니다. 당신의 '캐글 게임' 능력을 향상시키고 싶다면 읽어보면 좋은 글들입니다. 

최종 결과물을 위해 수백 개의 예측모델들을 혼합(원 : blending) 하는 것은 참으로 인상깊습니다. 우리는 몇가지의 새로운 방법들을 오프라인으로 측정해 보았지만, 여기서 추가적으로 정확도를 올리는 것이 이것을 생산 환경에 적용하려는 엔지니어링에 관한 노력을 정당화하지는 못했습니다. - 넷플릭스 엔지니어들

(역주 : 여기서 정확도 몇 퍼센트 더 올리는걸 적용하는 것이 그렇게 의미 있을 정도로 효과적이지는 않았다 라는 의미 같습니다)

 

스태킹 (Stacked generalization)

스태킹 앙상블 (원 : Stacked generalization) 은 1992년 논문 에서 Wolpert 에 의해 처음 언급되었습니다. Breiman의 논문인 Bagging Predictors“ 보다 2년 먼저 나왔네요. Wolpert 는 그의 또 다른 머신러닝 이론인 “탐색과 최적화에 공짜 점심은 없다“ 로도 유명합니다.

스태킹의 기본적인 아이디어는 여러 '기본 분류기' (원 : base classifiers) 들의 pool 을 사용한다는 것입니다. 그 후, 다른 분류기로 그 예측 결과를 조합하면서 일반화 오류를 낮추는 데에 목표를 두고 있습니다. 

2-Fold Stacking 을 하는 예시를 들어보면 다음과 같습니다. 

  • train 데이터를 두 개 파트로 나눕니다 : train_a and train_b
  • 1단계 모델은 train_a 를 학습해서 train_b 를 예측합니다. 
  • 같은 모델로 train_b 를 학습해서 train_a 를 예측합니다. 
  • 마지막으로 전체 train 데이터에 대해 학습한 후, test 셋을 예측합니다.
  • 그리고 2단계 모델로 1단계 모델(들)의 예측 결과물을 학습합니다. 

이 스태킹 모델은 문제 공간에 대해 1단계 모델의 예측값을 feature 로 추가적으로 받기 때문에, 단독으로 학습하는것보다 더 많은 정보를 가지고 학습을 하게 됩니다. 

스태킹을 할 때, 0단계 모델들은 어느 하나의 변형이 아닌 가능한 한 모든 '타입' 들이어야 좋습니다. 이렇게 하면, 학습 데이터를 탐색하는 가능한 많은 방법들을 뽑아낼 수 있습니다. 이는 0단계 모델들은 '공간을 넓혀야 한다' 는 의미와 같습니다. 

[…] 스태킹은 일반화 모델들을 비선형적으로 결합해서 새로운 일반화 모델을 만드는 것입니다. 각각의 모델들을 최적의 방법으로 통합하여 원래 모델이 학습 데이터에 대해서 예측해야했던 것들을 찾아야 합니다. 각각의 모델들이 뱉는 결과값이 다양할수록, 스태킹의 결과물은 더 좋아집니다 -  Wolpert (1992) Stacked Generalization

 

블렌딩 (Blending)

블렌딩 (Blending) 이라는 말은 넷플릭스 우승자들이 처음 사용하였습니다. 이는 스태킹과 유사한 방법이지만, 비교적 더 단순하고 정보 누출의 위험이 적습니다. "Stacked ensembling" 과 "blending" 은 종종 같은 의미로 사용됩니다. 

블렌딩을 사용할 경우, out-of-fold 예측값을 사용하는 대신에, 학습 데이터의 일부를 쪼개서 holdout validation 을 사용합니다. 다음 단계 모델은 이 holdout set 만을 이용해서 학습을 하게 됩니다. 

블렌딩의 몇가지 장점은 다음과 같습니다. 

  • 스태킹보다 단순합니다. 
  • 0단계 모델과 이후 단계 모델이 다른 데이터를 사용하기 때문에 정보 누출에 더 강한 모습을 보입니다. 
  • 시드 값이나 Stratified Fold 를 팀원들과 공유할 필요가 없습니다. 누구나 모델을 만들고 'blender' 에 추가할 수 있고, 이 새로 추가한 모델을 사용할 것인가 폐기할 것인가에 대해서는 'blender' 가 결정할 뿐입니다. 

몇가지 단점은 다음과 같습니다.

  • 아무튼 사용하게 되는 데이터의 양이 적어집니다. 
  • 최종 모델이 holdout set에 대해 과적합 되었을 수 있습니다. 
  • Holdout set을 사용하는것 보다 교차 검증이 더 강인한 모습을 보여줍니다. 

성능면에서는 스태킹과 블렌딩 두 기술 모두 비슷한 결과를 주지만, 어떤 것을 사용할 지는 당신의 선호에 따라 달라질 것입니다. 나같은 경우는 스태킹을 더 선호하는 편입니다. 만약 도저히 고르지 못하겠다면 둘 다 하는것도 방법입니다. 스태킹을 하고, 다음 단계 모델에서 holdout 셋을 사용해 블렌딩하는 것도 방법이 될 수 있습니다. 

 

로지스틱 회귀분석 (Stacking with logistic regression)

로지스틱 회귀분석을 사용해서 스태킹을 하는 방법은 가장 기본적이고 전통적인 스태킹의 방법론입니다.  Emanuele Olivetti 가 적었던  덕분에 나는 이것을 이해할 수 있었습니다. 

test 세트에 대한 예측을 만들 때 한 번에 예측을 하거나 out-of-fold 예측값을 사용해서 평균을 구할 수도 있습니다.  사실 out-of-fold 로 평균을 구하는 것이 더 정확하고 깔끔한 방법이기는 하지만, 모델과 코딩의 복잡성을 낮추기 위해 나는 한번에 구하는 평균을 더 선호하는 편입니다. 

캐글 예시 : “Papirusy z Edhellond”

나는 Emanuele 가 만든 blend.py 를 사용해서 이 대회에 나갔습니다. 8개듸 다른 모델들 (ExtraTrees, RandomForest, GBM...등등) 을 로지스틱 회귀분석으로 스태킹하는 방법은 0.99409 의 정확도를 내었고, 이 점수는 우승을 하기에 충분했습니다. 

캐글 예시 : KDD-cup 2014

이 코드로 나는 Yan Xu의 모델을 더 향상시킬 수 있었습니다. 그녀의 모델은 스태킹 없이 0.605 정도의 AUC를 기록했지만, 스태킹을 하면서 0.625 정도로 향상되었습니다. 

 

비선형 모델들의 스태킹 (Stacking with non-linear algorithms)

스태킹에 주로 사용되는 유명한 비선형 모델들은 GBM, KNN, NN, RF, ET 등등이 있습니다. 다중 분류 문제에서 원래의 feature 들을 비선형 스태킹 방법으로 결합하는것은 놀라울 정도의 성능 향상을 가져왔습니다. 분명히 첫 단계의 예측들은 가장 높은 순위의 'feature importance' 를 기록하고 있었고, 도움이 되는것을 확인할 수 있습니다. 비선형 알고리즘들은 원래의 feature 들과 meta-model의 변수들의 유용한 관계를 잘 찾아냅니다. 

 

캐글 예시 : TUT Headpose Estimation Challenge

 TUT Headpose Estimation 대회는 multiclass 분류 문제이면서 multi-label 분류 문제라고 볼 수 있습니다. 

각각의 라벨 값에 대해서 다른 앙상블 모델을 학습시켰습니다. 다음 표는 각각의 모델의 결과물과, 각각의 클래스 확률을 ExtraTrees 로 스태킹하였을 때의 점수 향상을 보여줍니다. 

MODEL        |             PUBLIC MAE             |                 PRIVATE MAE

Random Forests 500 estimators 6.156 6.546
Extremely Randomized Trees 500 estimators 6.317 6.666
KNN-Classifier with 5 neighbors 6.828 7.460
Logistic Regression 6.694 6.949
Stacking with Extremely Randomized Trees 4.772 4.718

우리는 스태킹을 함으로써 각각의 모델의 결과보다 약 30%(! 씩이나) 좋은 결과를 얻을 수 있었습니다. 

이 결과에 대해서는 다음 글을 읽어보시면 됩니다 : Computer Vision for Head Pose Estimation: Review of a Competition.

 

코드 예시

이 링크에서 out-of-fold 예측값을 만드는 함수를 찾을 수 있습니다. 결과물을 numpy 의 horizontal stack (hstack) 을 사용해서 blending 데이터셋을 만들면 됩니다. 

Feature weighted linear stacking

Feature-weighted linear stacking 은 새로 만든 'meta-features' 들을 새로운 모델 예측으로 스태킹을 합니다. 이 새로운 스태킹 모델은  특정한 feature 값에 대해서 어떤 0단계 모델이 (원 : base predictior) 가장 좋은 모델인지를 학습할 것이라고 기대할 수 있습니다. 새로운 스태킹 모델(마지막 모델)로는 선형 알고리즘을 사용해서 마지막 모델의 결과가 관찰하기 단순하고 빠른 값을 낼 수 있게끔 합니다. 

Vowpal Wabbit 으로 우리는 'feature-weighted linear stacking' 의 형태를 별도의 설치 없이 구현할 수 있습니다. 우리가 가진 학습 데이터가 다음과 같이 생겼다고 가정해봅시다. 

1  |  f           f_1 : 0.55        f_2 : 0.78        f_3 : 7.9        |    s   RF : 0.95        ET :  0.97      GBM : 0.92

우리는 f-피쳐공간과 s-피쳐공간간에 -q fs(역주 : f 값 * s값 하나씩의 곱) 을 추가하면서 2차함수의 형태를 가진 변수간 상관관계를 추가할 수 있습니다.

f-피쳐공간의 이름들은 만들어진 'meta-features' 가 될 수도 있고, 원래의 'feature' 일 수도 있습니다. (역주 : 아무튼 f 는 스태킹 과정 상으로 s 보다 한단계 낮은 의미라는 뜻 같습니다.)

 

Quadratic linear stacking of models

이 이름은 이름 붙여진 것이 없어서 제가 새로 만들어낸 이름입니다. 'feature-weighted linear stacking' 과 상당히 유사하지만, 차이점은 모델들의 예측 결과들로부터 새로운 조합을 만들어낸다는 것입니다. 이것은 수많은 실험들 속에서 점수를 향상시켰고, Modeling Women’s Healthcare Decision competition 대회의 DrivenData 에서 특히 두드러졌습니다.

위에서 생각했었던 같은 예시를 다시 적어본다면 : 

1  | f        f_1 : 0.55       f_2 : 0.78       f_3 : 7.9       |     s   RF : 0.95      ET : 0.97       GBM : 0.92

우리는 2차원의 변수 상호작용인 -q ss 를 만들어서 학습을 시킬 수도 있습니다. (역주 : 예를 들면 : RF와 GBM의 예측값의 곱)

그리고 이 방법은 feature_weighted linear stacking (-q fs, -q ss) 와 쉽게 결합할 수 있고, 둘 다 성능 향상을 기대할 수 있을 것입니다.

 

Feature weighted Linear Stacking

(역주 : 번역을 하긴 했지만 아직 예시를 찾아보지 않아서 이해가 잘 되지 않네요.

이미지 출처 -(Feature-Weighted Linear Stacking(2009) arxiv.org/pdf/0911.0460.pdf#page=3))

따라서 당신은 여러 가지 기본 모델들을 만들어야합니다. 실험을 하기 전에, 이중 어떤 모델이 마지막에 사용될 meta model 에 도움이 될 지 모릅니다. 2단계 스태킹 모델의 예시를 살펴본다면, base model 들은 약한 모델(원 : weak base model)이 선호되고 있습니다. 

그런데 왜 이렇게 base model 들을 열심히 튜닝하는것입니까? 여기서 튜닝은 모델들의 다양성을 확보하는데 사용될 뿐입니다. 마지막 단계에서 어떤 모델이 유용할지는 모릅니다. 그리고 그 마지막 단계는 '선형' 단계가 될 가능성이 높습니다. 튜닝도 필요 없고, 약간의 sparsity 를 위한 파라미터 한 개 정도 가진 모델이지요.  - Mike Kim, Tuning doesn’t matter. Why are you doing it?

 

Stacking classifiers with regressors and vice versa

(역주 : Regressor 들을 모아서 마지막에만 Classification을 사용한다던가, 혹은 그 반대의 경우)

스태킹을 한다면, 당신은 Regression 문제에도 Classifier들을 사용할 수 있고, 그 반대도 가능합니다. 예를 들면 이진 분류 문제에서 base ㅡmodel 로 quantile regression 을 사용할 수 있습니다. 좋은 스태킹 모델은 Regression모델들이 최고의 분류 성능을 주지 않더라도 그 예측값으로부터 정보를 뽑아낼 수 있어야 합니다. 

하지만 반대로 Regression 문제에서 Classifier 들을 사용하는것은 약간 더 까다롭습니다. 그래서 첫 번째 단계가 'binning' 이라고 하는 방법인데, 정답값(원 : y-label) 에 binning 을 균등한 간격으로 해주면, Regression 문제는 다음과 같이 다중 분류 문제로 바뀌게 됩니다. 

  • 20000 미만 :  class 1.
  • 20000 이상, 40000 미만 : class 2.
  • 40000 이상 : class 3.

각각의 클래스에 대한 확률을 구한다면 (마지막에 사용할) 스태킹 회귀 모델이 예측을 더 잘 할 수 있도록 도와줄 것입니다. 

“나는 절대, 절대로 out-of-fold 예측값 없이는 어디도 가지 않을거라고 배웠습니다. 내가 하와이를 가던, 어디 화장실을 가도 난 이 예측값들을 가져갈 겁니다. 내가 언제 2단계 혹은 3단계 'meta classifier' 들을 학습시켜야 할지 모릅니다. ” - T. Sharf

 

비지도학습으로 만든 feature 들의 스태킹 (Stacking unsupervised learned features)

우리가 꼭 스태킹을 할때 지도학습만을 사용해야한다는 규정은 그 어디에도 없습니다. 당연히 비지도학습 방법들도 사용할 수 있습니다. 

K-Means 클러스터링은 여기서 사용될 수 있는 인기있는 좋은 방법 중 하나입니다. Sofia-ML 에서는 여기에 적합한 빠른 K-Means 알고리즘을 구현해두었습니다. 

최근에 있었던 다른 방법 중 하나는 t-SNE를 사용하는 것입니다. 데이터를 2차원 혹은 3차원으로 축소시킨 후 이 결과물들을 비선형 스태킹 모델로 학습시키는 것입니다. 이 경우에는 holdout set을 확보해 두는것이 스태킹, 블렌딩 모두에서 안전한 방법일 것 같습니다. 여기서 Mike Kim이 t-SNE 벡터들을 XGBoost 로 부스팅한 솔루션을 볼 수 있습니다. ‘0.41599 via t-SNE meta-bagging‘.

Piotr 에서 Otto Product Classification Challenge 데이터를 t-SNE 로 시각화한 좋은 결과를 볼 수 있습니다. 

 

Online Stacking

나는 online stacking 의 아이디어를 생각해내기 위해 꽤 많은 시간을 보냈습니다. 첫번째로, 해시된 이진 표현으로부터 아주 작은 임의의 tree 기반 모델을 만들었습니다. 거기에 이 모델이 정확한 예측을 한 경우 이득(원 : profit) 을 더해주고, 틀린 경우에는 빼주었습니다. 그 후, 가장 좋은 트리와 (원 : most profitable) 가장 안좋은 트리(원 : least profitable)를 가져와서 변수 (원 : feature representation) 에 더해주었습니다. 

이 방법이 먹히기는 했는데 인공적인 데이터에서 만 먹혔습니다. 예를 들면, 이런 방식으로 만든 랜덤한 트리모델들을 선형 퍼셉트론으로 학습시켜도 비선형 문제인 XOR 문제를 학습하는데 성공했습니다. 하지만 다른 실생활 문제들에는 전혀 먹히지 않았습니다. 믿어주세요. 진짜 해봤는데 안돼요. 이 이후로 나는 새로운 알고리즘을 소개하는 논문을 볼 때 인공적인 데이터들로만 실험한 것을 보면 의심을 하기 시작했습니다.

하지만 이와 유사한 아이디어가 이 논문 (random bit regression)에서 성공했습니다. 여기서 보면, 여러 feature 들로부터 생성한 임의의 선형 함수를 여러 개 만들었고, 가장 좋은 것은 강한 정규화를 통해 찾아졌습니다. 여기서 나는 다른 데이터셋에서도 적용될 수 있는 성공적인 방법을 찾을 수 있었습니다. 이는 다음에 쓸 포스트 내용이 될 것입니다. 

이 online stacking 의 예시로는 광고 클릭 여부 예측 문제를 들 수 있습니다. 각 모델들은 그때그때 잘 작동되는 최근의 데이터로 학습됩니다. 따라서 데이터가 시간적인 관계가 있으면, Vowpal Wabbit 을 사용해서 전체 데이터를 학습시키고, 더 복잡하고 강력한 XGBoost 같은 툴을 사용해서 과거의 데이터를 학습시키는 방법을 사용할 수 있습니다. 이렇게 학습된 XGBoost 모델들의 예측값과 원래의 샘플들을 같이 스태킹하면, Vowpal Wabbit이 가장 잘 하는것을 제대로 시킬 수 있습니다. 손실 함수 최적화 말이지요. 

실제 세계는 복잡합니다. 그래서 서로 다른 모델을 많이 결합하면 이러한 복잡성을 포착할 수 있습니다. - Ben Hamner ‘Machine learning best practices we’ve learned from hundreds of competitions’ (video)

 

Everything is a hyper-parameter

스태킹, 블렌딩, 메타모델링을 할 때, 내가 하는 모든 행동들이 스태킹 모델에게는 일종의 하이퍼파라미터로 적용될 수 있다고 생각해야 합니다. 예를 들면:

  • 데이터를 스케일링하지 않는것. 
  • 데이터를 Standard Scaling 하는것. 
  • 데이터를 Minmax Scaling 하는 것. 

이 모든 것들은 앙상블 학습의 성능을 끌어올리기 위해 튜닝해야하는 추가적인 하이퍼파라미터일 뿐입니다. 마찬가지로, 사용할 base model 의 수 역시도 최적화해야할 하이퍼파라미터로 볼 수도 있습니다. Feature selection 이나 결측치 처리 역시도 다른 예시가 될 수 있습니다. 

이러한 'meta-parameter' 들을 튜닝하는데는 평소 알고리즘 튜닝에 사용하는 random search 나 gridsearch가 좋은 방법이 될 수 있습니다. Li

가끔씩은 XGBoost 가 KNN-Classifier 가 보는 것들을 보게 해주는것이 효과적일 때도 있다 . – Marios Michailidis

 

Model Selection

여기서 추가적으로 여러가지 앙상블 모델들의 결과를 조합해보면서 점수를 더 최적화할 수 있습니다.

  • 접근 방법은 수동으로 선택한 좋은 앙상블 결과들에 평균내기, 투표 방식, 순위 평균을 구해 보는 것입니다. 
  • 탐욕 알고리즘적인 방법 (원 : Greedy forward model selection (Caruana et al.)) -  3개 정도의 가장 좋은 모델을 시작으로, train set의 점수를 가장 많이 올려주는 다른 모델들을 하나씩 추가합니다. 복원 추출 개념을 적용하면, 한 개의 모델이 여러번 선택되어 추가될 수 있고, 이것은 가중치를 부여하는 효과가 있습니다. (원 : weighing)
  • 유전 알고리즘적인 방법 - 유전 알고리즘과 교차 검증 점수를 사용해서 선택할 모델들을 정합니다. 예시로 inversion의 솔루션을 볼 수 있습니다. ‘Strategy for top 25 position‘.
  • 나는 Caruana 의 방법에서 아이디어를 얻은 완전히 랜덤한 방법을 사용합니다. 먼저 랜덤한 앙상블 결과를 100개 정도 만들고, 비복원추출로 고른 몇 개의 결과를 골라내고, 이 결과 중 가장 좋은 결과를 고릅니다. 

 

자동화 (Automation)

Otto product classification 대회를 하면서 스태킹을 진행할 때, 나는 top 10 등의 자리에 빠르게 올라갔습니다. 거기에 더 많은 기본 모델들을 추가하고, 스태킹 앙상블들을 bagging 했다면 점수를 더 향상시켰을 수 있을겁니다.

7개의 base model 들을 6개의 스태킹 모델로 스태킹하는 정도가 되었을때, 나는 충격과 공포를 느꼈습니다. 이 모든 과정을 자동화할 수는 없을까? 이 복잡하고 느리면서 다루기 힘든 모델들을 계속 만지는 것은 내가 좋아하는 '빠르고 단순한' 기계 학습의 영역에서 완전히 벗어나있는 일이었습니다. 

나는 대회의 남은 기간동안 이 스태킹 과정을 자동화 시키는 일에 시간을 쏟았습니다. 기본 모델로는 랜덤한 알고리즘들을 랜덤한 파라미터들로 학습을 시켰습니다. Scikit-learn의 API 를 가지는 Wrapper 들을 VW, Sofia-ML, RGF, MLP and XGBoost 등과 함께 사용했습니다. 


The first whiteboard sketch for a parallelized automated stacker with 3 buckets

스태킹 모델로는 SVM, Random Forests, ExtraTrees, GBM, XGBoost 들을 랜덤한 파라미터로, 기본 모델들의 랜덤한 집합에 학습을 시켰습니다. 마지막으로, 이 스태킹 모델들은 각각의 fold-prediction 이 더 낮은 점수를 갱신할 때 그 값을 평균내었습니다. 

이렇게 자동으로 만들어진 스태킹 모델은, 대회 마감 약 일주일 전에 57등을 기록하고 있었습니다. 이는 나의 최종 앙상블 모델에 기여하게 되었습니다. 기존에 내가 하던 방법과 다른 점이라고는 모델을 튜닝하고, 고르는 것에 전혀 시간을 쓰지 않았다는 점입니다. 코드를 실행하고, 자고 일어나면 점수가 올라 있죠. 

이렇게 만들어진 자동 스태킹 모델은 아무 튜닝이나 모델 선택을 하지 않고도, 3000여 명의 경쟁자들 중에서 상위 10%의 성적을 거두었습니다. 

이러한 자동화된 스태킹 모델은 내 가장 큰 관심 분야중 하나입니다. 여기에 관한 다음 몇 개의 글들도 기대해주세요. 이 자동 스태킹 모델은 TUT Headpose Estimation challenge 에서 가장 좋은 결과를 보여주었습니다. 이 'black-box' 솔루션이, 요즘 잘나가는 전문가들이 이 대회를 위해서 만든 특수 목적 알고리즘을 만든 것들을 모두 부숴버렸습니다.

주목할 만한 점은 : 이 대회는 다중 분류 문제입니다. 예측값들은 "yaw", "pitch" 두 가지를 모두 맞춰야합니다. (역주 : 한글로는 롤, 피치, 요 라고 하는데, 물체의 회전움직임, 혹은 방향을 3개 축으로 표현하는 방법이다 라고 보시면 됩니다.)

머리의 자세를 표현하려면, 이 "yaw" 와 "pitch" 들은 서로 관계가 있습니다. "yaw" 를  집중적으로 맞추는 모델들을 스태킹하는것이 "pitch" 값의 정확도도 올려주었고, 그 반대도 마찬가지였습니다. 흥미롭네요. 

CV 점수를 고려할 때, 이 CV 점수들의 표준편차도 고려할 수 있습니다. 아무래도 표준편차가 작은 모델이 더 안전한 모델입니다. 이 다음에는 모델의 복잡도와 메모리 사용량, 그리고 코드 실행 시간을 최적화할 수 있습니다. 마지막으로는 각 예측 결과물들에 대한 상관관계를 추가해볼 수 있습니다. 코드가 자동으로 앙상블을 수행할 때, 상관관계가 적은 예측값들을 자동으로 선호할 수 있도록 말이지요. 

이 자동화된 스태킹 파이프라인은 병렬화 및 분산화가 가능합니다. 그렇게 되면, 단일 노트북 컴퓨터로 실행해도 빠른 속도와 성능 향상을 기대할 수 있습니다. 

Contextual bandit optimization 은 gridsearch 를 대신할 수 있는 방법입니다. 알고리즘이 좋은 파라미터와 모델을 사용하기 시작하고, 전에 사용했던 SVM 이 메모리가 부족했던 경우 등등을 기억하기를 원합니다. 스태킹에 관한 이 추가 사항은 다음에 더 자세히 살펴보겠습니다.

그 동안, MLWave 의 깃헙 저장소의 미리보기를 한번 보세요. “Hodor-autoML“.

 

Otto Product Classification Challenge 의 1등, 2등 수상자들은 1000개 이상의 다른 모델들의 앙상블을 수행했습니다. 솔루션은 여기에서 확인할 수 있습니다. 1위 솔루션, 2위 솔루션

Why create these Frankenstein ensembles?

(역주 : 이런 괴물같은 앙상블을 왜 하는 것일까요?)

이렇게 천 개가 넘는 모델들을 만들고 스태킹하고 조합하는 것이 당신이 보기에는 무익하고, 미친 소리처럼 들릴 지 모릅니다. 하지만 이런 괴물같은 앙상블들은 다 쓸모가 있습니다. :

  • 캐글같은 대회를 우승할 수 있습니다.
  • 한번의 시도로 잘나가는 최신 벤치마크들을 이길 수 있습니다. 
  • 모델을 만든 후, 이 좋은 성적과 단순하고 만들기 쉬운 모델의 성적을 비교해볼 수 있습니다. 
  • 언젠가 지금 수준의 컴퓨터들과 클라우드 컴퓨팅 자원은 아주 약한 날이 올 것입니다. 그 때에 대비해서 준비할 수 있습니다
  • 이렇게 앙상블한 모델에서 얻은 지식을 다시 단순하고 얕은 모델에게 전달할 수 있습니다. (Hinton’s Dark Knowledge, Caruana’s Model Compression)
  • 모든 기본 모델들이 제 시간안에 학습이 끝나지 않아도 됩니다. 한 개의 모델이 없어도, 앙상블 모델은 좋은 예측값을 만드는 데에 큰 지장이 없습니다. 그런 점에서 앙상블은 아주 graceful 한 degradation(역주 : 도저히 어떻게 번역해야 할지 모르겠습니다.) 의 형태를 소개합니다. 
  • 자동화된 대규모 앙상블은 어떠한 튜닝이나 모델 선택을 하지 않아도 오버피팅에 강력하고, 정규화(regularization)의 성질이 있습니다. 일반인들도 충분히 사용할 수 있습니다. 
  • 현재까지 머신러닝 알고리즘의 성능을 향상시키는 가장 좋은 방법입니다. 아마도 human ensemble learning에 관해서 무언가를 알려줄 수도 있습니다. 
  • 정확도를 1% 정도 올리는 것이 돈을 투자하는 일에 있어서는 큰 손실을 볼 것을 줄여주기도 할 것입니다. 더 심각한 예시로, 헬스케어 관련해서는 목숨을 살리는 일에 도움을 줄 수도 있습니다. 

 

 


 

 

Update: Thanks a lot to Dat Le for documenting and refactoring the code accompanying this article. Thanks to Armando Segnini for adding weighted averaging. Thanks a lot everyone for the encouraging comments. My apologies if I have forgotten to link to your previous inspirational work. Further reading at “More is always better – The power of Simple Ensembles” by Carter Sibley, “Tradeshift Benchmark Tutorial with two-stage SKLearn models” by Dmitry Dryomov, “Stacking, Blending and Stacked Generalization” by Eric Chio, Ensemble Learning: The wisdom of the crowds (of machines)by Lior Rokach, and “Deep Support Vector Machines” by Marco Wiering.

Terminology: When I say ensembling I mean ‘model averaging’: combining multiple models. Algorithms like Random Forests use ensembling techniques like bagging internally. For this article we are not interested in that.

The intro image came from WikiMedia Commons and is in the public domain, courtesy of Jesse Merz.

Cite

If you use significant portions or methods from this article in a scientific paper, report, or book, please consider attributing with:

or, if you prefer a more authoritative reference:

  • Michailidis, Marios. (2017). Investigating machine learning methods in recommender systems (Thesis). University College London.

For other, less formal, material, such as blogs or educational slides, a simple link will suffice to satisfy Creative Commons 3.0 attribution.

The resource URL will remain static and the page hosted on this site for the foreseeable future.

반응형
Posted by Jamm_
글 번역2020. 9. 8. 19:16
반응형

원글은 다음 링크의 글입니다. 

 

Kaggle Ensembling Guide | MLWave

Model ensembling is a very powerful technique to increase accuracy on a variety of ML tasks. In this article I will share my ensembling approaches for Kaggle Competitions. For the first part we look at creating ensembles from submission files. The second p

mlwave.com

이 글은 위 문서에 대한 자의적인 한글 번역본입니다. 번역을 하면서 이해한대로 설명을 더 추가할것이고, 아무리 시도해도 이해가 되지 않은 부분은 빠진 내용이 있을 수 있습니다. 영어 실력의 부족으로 번역이 매끄럽지 않을 수 있습니다. 댓글로 번역, 내용을 지적해주시면 감사한 마음으로 수정할 수 있도록 하겠습니다. 본문이 매우, 매우, 매우 긴 관계로 몇 편의 글로 끊어서 올릴 수 있습니다. 

번역상 '나' 는 필자가 아니라, 원 글의 저자임을 밝힙니다.

 


 

Kaggle Ensembling Guide

모델 앙상블은 여러 가지의 머신러닝 문제의 정확도를 올릴 수 있는 아주 강력한 방법입니다. 이번 글에서는 내가 캐글 대회에서 사용했던 앙상블 방법들을 공유하고자 합니다. 

첫번째 파트에서는 우리가 만든 제출 파일들로 앙상블하는 방법을 먼저 볼 것이고, 두번째 파트로는 스태킹(stacked generalization) 과 블렌딩(blending) 방법으로 앙상블을 하는 것을 볼 것입니다. 

'왜 앙상블이 일반화 오류를 줄여주는가?' 에 대한 답이 글에 포함되어있고, 마지막으로는 여러가지 앙상블 방법들을 보여줄 것이고, 스스로 시도해 볼 수 있도록 결과물과 코드들 역시 공유할 것입니다. 

"이것이야말로 당신이 머신러닝 대회를 우승할 수 있는 방법입니다. 다른 사람들의 결과물을 가져와서 앙상블하는것." 
- Vitaly Kuznetsov (NIPS2014)

 

제출 파일들로 앙상블 만들기

앙상블을 시도하는데 가장 편리하고 기본적인 방법은 캐글에 제출하는 csv 파일들을 사용하는 것입니다. test 데이터에 대한 예측값들만 필요하고, 새로운 모델을 학습시킬 필요가 없습니다. 이건 이미 존재하는 모델들의 예측값을 앙상블하기 가장 빠른 방법이고, 팀을 만들어 앙상블 하는 과정에 적합합니다.

투표 방법 (Voting Ensemble)

처음으로 시도할 만한 것은, 단순한 투표 방식입니다. 왜 모델 앙상블 방법이 에러를 줄여주고, 낮은 상관관계를 가진 예측값들에 더 잘 작동하는지 볼 것입니다.

우주에 관한 임무를 실행할 때에는 모든 신호(signal)들이 순서대로 잘 배치되는것이 중요합니다. 우리가 가진 신호가 다음과 같은 이진 문자열이라 해봅시다.

1110110011101111011111011011

만약 신호가 다음과 같이 망가졌다면, (1 한개가 0으로 바뀐 상황):

1010110011101111011111011011

하나의 생명을 잃을 수도 있습니다. 

에러 수정 코드에서 방법을 찾을 수 있습니다. 가장 단순한 방법은 repetition-code 라는 방법입니다. 이는 신호를 같은 크기의 덩어리로 여러 번 반복하고, 다수결 투표 방식을 취합니다. 

Original signal: 1110110011

Encoded: 10,3 101011001111101100111110110011

      Decoding: 1010110011

                       1110110011

                       1110110011

Majority vote: 1110110011

신호가 망가지는 것은 흔하게 발생하는 것은 아닙니다. 그래서 이러한 다수결 투표 방식이 망가진 신호를 '망가지게' 할 가능성은 더더욱 적습니다. 

이런식으로 신호가 망가지는것을 100% 예측할 수 없지는 않습니다. 따라서 이러한 방식으로, 망가진 신호를 원래의 신호로 고쳐질 수 있습니다.  

 

머신러닝에서의 예시

우리가 10개의 샘플을 가지고 있다고 가정합시다. 이 샘플들의 정답(ground truth)은 모두 1입니다. 

1111111111

그리고 우리는 세 개의 이진 분류기 (binary classifiers) 들이 있습니다. 각각의 모델은 70%의 정확도를 가지고 있습니다. 이 세 개의 분류기를 70%확률로 1을 뱉고, 30%의 확률로 0을 뱉는 난수 생성기로 볼 수도 있습니다. 이러한 랜덤 모델들도 다수결 투표 방법으로 78%의 정확도를 가지도록 할 수 있습니다. 약간의 수학을 더하자면 : 

3명의 다수결 투표 방식으로는 4가지의 결과값을 기대할 수 있습니다.

세개 모두 정답 : 0.7 * 0.7 * 0.7 = 0.3429

두개만 정답 :  0.7 * 0.7 * 0.3 + 0.7 * 0.3 * 0.7 + 0.3 * 0.7 * 0.7 = 0.4409

한개만 정답 : 0.3 * 0.3 * 0.7 + 0.3 * 0.7 * 0.3 + 0.7 * 0.3 * 0.3 = 0.189

셋 다 틀림 : 0.3 * 0.3 * 0.3 = 0.027

우리는 여기서 약 44%라는 많은 경우에 대해서 다수결 투표가 오답을 정답으로 고칠 수 있는 것을 알 수 있습니다. 이러한 투표는 정답률을 78%까지 올려줍니다. (0.3429 + 0.4409 = 0.7838)

 

투표 구성원 수

여러 번 코드가 반복될 수록 에러 수정 능력이 증가합니다. 마찬가지로, 앙상블 방법도 많은 앙상블 구성원이 있을 때 일반적으로 더 좋은 결과를 줍니다. 

위에서의 예시를 다시 가져온다면, 3개가 아니라 5개의 70% 정확도를 가진 모델을 앙상블한다면, 약 83%정도의 정확도를 기대할 수 있습니다. 한두 개의 오답은 66% 정도의 다수결 투표로 고쳐질 수 있습니다 (0.36015 + 0.3087)

 

상관관계 (Correlation)

KDD-cup 2014에 참가하면서 팀을 꾸렸는데, Marios Michailidis (KazAnova) 가 아주 특이한 것을 제안했습니다. 그는 우리가 제출했던 모든 파일의 피어슨 상관계수를 계산해서 상관관계가 적은 괜찮은 모델들을 모았습니다. 그리고 이들의 평균을 내서 제출하는 것 만으로 우리는 50등수를 올릴 수 있었습니다. 확실히 상관관계가 적은 결과물들이 앙상블 된 것이, 상관관계가 높은것들의 앙상블보다 좋은 결과를 보였는데, 왜일까요?

위에서와 마찬가지로 3가지의 모델 결과물들을 생각해봅시다. 예측해야 하는 정답은 여전히 1 들입니다.

1111111100 = 80% accuracy

1111111100 = 80% accuracy

1011111100 = 70% accuracy.

이 세 개의 모델은 상당히 상관관계가 있어서, 다수결 투표를 적용해도 전혀 발전이 없습니다 : 

1111111100 = 80% accuracy (다수결 투표 결과)

하지만, 성능은 좀 덜하지만, 낮은 상관관계가 있는 모델들을 생각해보면 다음과 같습니다. 

1111111100 = 80% accuracy

0111011101 = 70% accuracy

1000101111 = 60% accuracy

다수결 투표를 하면:

1111111101 = 90% accuracy (다수결 투표 결과)

엄청난 발전입니다. 앙상블 구성원의 상관관계가 낮을수록, 에러 수정 능력이 더 향상되는 것 같습니다. 

 

캐글 예시 : Forest Cover Type Predictions

다수결 투표 방식은 측정공식(evaluation metrics)이 더 정확한 답을 요구할 수록  빛을 발합니다. 예를 들면 '정확도' (accuracy) 가 있습니다.

 forest cover type prediction 대회는 UCI Forest CoverType dataset 을 활용하고 있습니다. 변수의 갯수는 54개, 정답에는 6개의 클래스가 있습니다.  우리는 일단 아주 간단한 모델 을 만들었습니다. 500개의 트리가 있는 랜덤 포레스트 모델입니다. 일단 이 모델을 몇 개 만든 이후, 가장 좋은 1개의 모델을 골랐습니다. 이 상황에서는 ExtraTreesClassifier로 만든 모델이 가장 좋았네요.  

 

가중 투표 (Weighing)

그리고 우리는 가중 투표 방식을 취했습니다. 이유는, 우리는 일반적으로 '더 잘 맞추는 모델' 에 더 가중치를 주고 싶습니다. 그래서 '가장 잘 맞추는 모델' 에게 3표를 주고, 나머지 4개의 모델에는 한표씩을 주었습니다. (역주 : 총 모델은 5개지만, 투표 수는 7표겠네요.) 이유는 다음과 같습니다 : 

'좋지 않은 모델'들이 가장 좋은 모델을 이길 수 있는 방법은 그들이 가진 예측값(오답)들을 모아서 확실한 다른 답(정답)을 내는 것이기 때문입니다. 

우리는 이 앙상블이 '가장 잘 맞추는 모델' 의 몇개의 잘못된 선택을 바로잡을 수 있을 것이라 기대할 수 있습니다. 아주 약간의 성능 개선이 있겠지요. 그것이 민주주의를 잃어버리고, 플라톤의 <국가>를 만든 것에 대한 우리의 벌칙이라 할 수 있겠습니다.

(역주 : <국가> 를 읽어보지 않아서 어떤 비유인지 잘 모르겠네요. 직역하긴 했지만 이상합니다.)

“모든 도시는 서로 전쟁중에 있는 두 도시를 포함한다.” 플라톤  <국가>

아래 표는 위에서 언급한 5개의 모델을 학습시키고, 가중 투표 방식을 적용한 결과물을 보여줍니다.

GradientBoostingMachine 0.65057
RandomForest Gini 0.75107
RandomForest Entropy 0.75222
ExtraTrees Entropy 0.75524
ExtraTrees Gini (Best) 0.75571
Voting Ensemble (Democracy) 0.75337
Voting Ensemble (3*Best vs. Rest) 0.75667

 

캐글 예시 : CIFAR-10 Object detection in images

CIFAR-10 은 정확도를 보는 또 다른 다중 분류 문제입니다. 우리 팀의 리더인 Phil Culliton이 Graham 박사의 좋은 모델 구조를 채용할 수 있는 모델을 만들었습니다. 그 후 90%의 정확도가 넘는 30개의 모델에 대해 다수결 투표 방식을 적용했습니다. 이중 가장 좋은 모델의 정확도는 0.93170 이었습니다. 

다수결 투표를 한 결과물의 점수는 0.94120 이었고, 이는 인간이 분류할 수 있는 정확도를 뛰어넘었습니다. 

 

코드 예시

우리는 MLWave의 깃헙 저장소에 샘플 투표 코드를 사용할 수 있게 두었습니다. 이것은 캐글 제출 파일 디렉토리에서 실행되고, 새로 결과물을 제출합니다. Update: Armando Segnini 가 투표에 가중치를 추가하였습니다. 

10개의 신경망을 만들고, 예측 결과를 평균해라. 이것은 상당히 단순한 방법이지만 결과를 상당히 향상시킬 수 있다. 

혹자는 왜 평균을 내는 것이 이렇게 도움이 되는가 신기해 할 수도 있지만, 평균이 효과가 있는 이유는 아주 단순하다. 
두 개의 모델이 70%의 정확도를 내고 있다고 가정하자. 그 두 모델의 합의는 옳을 때도 있지만 옳지 않다면, 어느 한쪽이 옳은 경우가 종종 있다. 이때 평균을 낸 결과물은 정답에 더 많은 가중치를 둔 결과가 된다. 

이 효과는 모델이 '맞출 것' 은 확실히 맞추지만 '틀린 것' 에 대해서 애매하게 찍고있는 경우 더 잘 작동하게 된다. 
Ilya Sutskever A brief overview of Deep Learning.

 

평균 (Averaging)

평균은 분류, 회귀 문제에 상관 없이, 측정 공식(AUC, squared error, log loss) 에 따라서도 별 상관 없이 다양한 분야의 문제에서 잘 작동하는 편입니다. 

각각의 모델의 결과물을 더해서 평균을 내는 것 이상의 대단한 것도 없습니다. 캐글에서 종종 "Bagging submissions" 라고 하는 것이 이것을 의미합니다.  평균을 내는 것은 종종 오버피팅을 줄여줍니다. 당신은 몇 개의 클래스를 '부드럽게' 분리해내고 싶을겁니다. (원 : smooth seperation) 하지만 단일 모델은 그 '결정 경계' (원 : edges) 가 그렇게 부드럽지 않을 수 있지요. 

 

이 캐글 대회(Don’t Overfit!) 을 보면 (역주 : 사진이 안들어가서 문장을 약간 수정했습니다), 검정색 선이 초록색 선보다 더 좋다고 볼 수 있습니다. 초록색 선은 데이터의 노이즈를 더 학습한 상태입니다. 여러개의 초록색 선을 구해서 평균을 낸다면, 우리가 얻을 결과값은 검정 선에 더 가까워질 것입니다. 

우리가 항상 기억해야 할 것은, 절대 학습데이터를 '외워'서는 안된다는 것입니다. 새로 보게 될 데이타에서도 '일반적으로' 잘 작동하는 모델을 만들어야 합니다. (데이터를 외울거면 더 효율적인 방법들이 있습니다)

 

캐글 예시 : Bag of Words Meets Bags of Popcorn

이 대회는 영화 감정 분석 대회 입니다. 이 전 게시물에서 퍼셉트론을 이용해서 95.2 의 AUC를 얻는 것에 대해 쓴 적이 있습니다. 

퍼셉트론은 '선형 분류' 모델입니다. 이건 참 환영할만한 특성이지만, 퍼셉트론은 데이터를 분류할 수 있게 되면 더이상 학습을 하지 않는다는 사실을 알아야 합니다. 이는 새로운 데이터에 대해서 '최고의 결정 경계' (원 : best seperation) 을 찾아주지는 않을 것입니다. 

우리가 5개의 퍼셉트론 모델을 랜덤한 가중치로 초기화하고, 그 예측 결과물의 평균을 구한다면 어떨까요? 우리는 test 데이터에 대해 성능 향상을 가져왔습니다.

PUBLIC AUC 점수

Perceptron 0.95288
Random Perceptron 0.95092
Random Perceptron 0.95128
Random Perceptron 0.95118
Random Perceptron 0.95072
Bagged Perceptrons 0.95427

위 결과는 앙상블 방법이 (임시적으로) 데이터의 세부적인 내용들과 특정 머신러닝 알고리즘의 내부 작동 원리들을 연구해야 하는 당신을 구했습니다. 만약 앙상블이 잘 된다면, 좋은것이고, 잘 되지 않더라도 크게 손해나는 것은 없습니다. 

그리고 당신은 10개의 '똑같은'  선형회귀 모델을 평균내는 것에 대한 페널티도 받지 않습니다. 한개의 '올바르지 못한 교차검증 방법으로 인해 과적합된 예시를 평균내는 것 조차도 평균의 다양성을 추가하는 것이기 때문에 약간의 성능 향상을 가져올 수도 있습니다. 

코드 예시

단순 평균 예시 코드를 깃헙에 올려두었습니다. csv 파일들의 경로를 인풋으로 받고, 평균된 결과물을 출력합니다. Update: Dat Le 가 기하 평균 예시 코드를 추가했습니다. 기하평균은 단순 평균(원 : plain average, 역주 : 산술평균)보다 더 좋을 수도 있습니다.  

 

순위 평균 (Rank averaging)

서로 다른 여러개의 모델의 결과물들의 평균을 구할때 몇 가지의 문제점들이 생길 수 있습니다. 모든 예측 모델이 완벽하게 보정되지 않았다는것인데, 각각은 예측해야 할 확률값들을 지나치게 과신하거나 불신할 수 있으며, 예측값들은 특정 범위에 대충 흩어져 있는 모양새일 수 있습니다. 아주 극단적인 제출 파일의 예시를 하나 적어보자면 다음과 같습니다:

Id,Prediction 

1,0.35000056 

2,0.35000002 

3,0.35000098 

4,0.35000111

이런 예측은 측정공식이 AUC 처럼 경계값(원 : threshold) 을 기반으로 하거나, 순위를 매기는 방식이라면 리더보드에서는 결과가 좋게 나올 것입니다. 하지만 이렇게 생긴 다른 모델의 예측값과 평균을 구한다면: 

Id,Prediction 

1,0.57 

2,0.04 

3,0.96 

4,0.99

결과는 전혀 바뀌지 않을 것입니다. 

(역주 : 각 행의 값들이 거의 비슷해서, 평균을 내도 두번째 파일의 값들의 분포와 비슷해집니다. 평균을 해서 값이 변해서 오답을 고치는 능력이 적어지겠네요.)

우리의 해결책은 예측값들의 '순위' 를 매기는 것입니다. 그리고 이 순위들의 평균을 구합니다. 

Id,Rank,Prediction 

1,1,0.35000056 

2,0,0.35000002 

3,2,0.35000098 

4,3,0.35000111

이렇게 평균을 낸 순위를 0과 1 사이의 값으로 normalize 하면, 아마 예측 결과물은 균등 분포(원 : even distribution)를 이룰 것입니다. 순위 평균의 결과를 보자면 : 

Id,Prediction 

1,0.33 

2,0.0 

3,0.66 

4,1.0

(역주 : 결과물을 보시면, 1 * 0.35000056  |  0 * 0.35000002  |  2 * 0.35000098  |  3 * 0.35000111 로 되어 있습니다)

 

Historical rank.

랭킹은 test 데이터셋이 필요합니다. 하지만 새로운 1개의 샘플에 대해서만 예측하기를 원한다면 어떻게 해야할까요? 아마도 예전에 쓰던 (역주 : 모델 검증에 사용하던) test 셋에 섞어서 랭킹을 구할 수도 있겠지만, 이것은 솔루션의 복잡도를 증가시킬 것입니다. 

한 가지 방법은 'Historical rank' 를 사용하는 것입니다. 우리가 원하는 새로운 샘플의 예측값이 '0.35000110' 이라면, test 셋의 예측값들 중에서 이 예측값과 가장 가까운 값을 찾아, 그 순위를 가져옵니다. (이 경우의 순위는 '3'을 가져오겠네요. 값이 0.35000111 로 가장 비슷합니다.)

캐글 예시 : Acquire Valued Shoppers Challenge

이 순위 평균 방식은 순위를 예측하는 경우나, AUC 같은 경계값이 있는 측정공식, 그리고 Average Precision at k 와 같은 'search-engine quality metrics' 에 잘 작동하는 편입니다. 

Shopper challenge 의 목표는 해당 고객이 다음에도 다시 올 가능성이 높은지 순위를 매기는 것입니다. 

우리 팀은 첫 번째로 여러 개의 'Vowpal Wabbit' 모델들과 'R GLMNet model' 의 평균을 구했습니다. 그 후에 순위 평균 방법을 적용해서 성능을 향상시켰습니다. 

MODEL     |          PUBLIC        |          PRIVATE

Vowpal Wabbit A 0.60764 0.59962
Vowpal Wabbit B 0.60737 0.59957
Vowpal Wabbit C 0.60757 0.59954
GLMNet 0.60433 0.59665
Average Bag 0.60795 0.60031
Rank average Bag 0.61027 0.60187

나는 전에 Avito challenge 에서 순위 평균으로 엄청난 성능 향상이 있었다는 것에 대해 글을 쓴 적이 있습니다. 

 

코드 예시

간단한 순위 평균 코드 를 MLWave 의 깃헙 저장소에 추가되었습니다. 

대회가 참 좋은 이유로는, 정말 여러 가지의 모델링 방법들이 있고, 적용할 수 있는 테크닉들도 다양한데, 우리는 어떤게 효과적이었는지 절대 미리 알 수가 없다는 점입니다. - Anthony Goldbloom Data Prediction Competitions — Far More than Just a Bit of Fun

 

From ‘How Scotch Blended Whisky is Made’ on Youtube

 

 

 

 

....계속...

 

Part 2 링크 : 

 

Kaggle Ensembling Guide (MLWave) / 한글 번역 / Part. 2 - (Stacking, Blending Ensemble)

Part 1 링크 : Kaggle Ensembling Guide(mlwave) / 캐글 앙상블 가이드 한글 번역 / Part. 1 원글은 다음 링크의 글입니다. Kaggle Ensembling Guide | MLWave Model ensembling is a very powerful technique to..

jamm-notnull.tistory.com

 

반응형
Posted by Jamm_
반응형
 

House Prices: Advanced Regression Techniques

Predict sales prices and practice feature engineering, RFs, and gradient boosting

www.kaggle.com

타이타닉 대회를 마무리하고, 다음으로 도전해볼 만한 캐글 컴퍼티션이 뭐가 있을까 찾아보다가 이 집값을 예측하는 대회를 찾았다. 오호, 집값을 예측한다고? 나도 그럼 부동산으로 돈 한번 땡겨 볼 수 있을까? 하는 생각에 신나게 대회 참가 버튼을 눌렀다. 

타이타닉보다는 훨씬 데이터도 많고, (하지만 여전히 적음) 다루기도 어렵다. 그리고 타이타닉과 같은 'Classification', 분류 문제가 아니라 'Regression' 회귀 문제이다. Regression 문제는 정답이 어떤 실수 형태로 주어져서, 이 값을 예측하는 문제이다. 반면 Classification 문제는 타이타닉처럼 데이터가 어떤 종류인가 (생존인가, 사망인가) 를 맞추는 문제이다. 그럼 생존 확률을 맞추라그러는거는 뭐냐? 여기서 LogisticRegression 을 처음 들어본 사람은 멘붕이 온다. LogisticRegression은 이름에는 Regression 이라고 되어 있지만, 사실은 Classification 문제에 사용한다. 쉽게 생각하면, 이 LogisticRegression은 해당 클래스의 확률 값을 찾는 Regression이어서 이름에는 Regression 이 들어가지만, Classification 문제에 사용된다고 생각하면 된다. Classification 이어도, 문제에 따라서는 확률 값을 제출하기를 요구하는 문제들도 있다. 

 


 

 

1. 데이터 로드 (Load Data & Packages)

import pandas as pd
import numpy as np
from scipy.stats import norm
%matplotlib inline
import matplotlib.pyplot as plt
import seaborn as sns
import warnings; warnings.simplefilter('ignore')

패키지들을 로드해주고

train=pd.read_csv('train.csv', index_col='Id')
test=pd.read_csv('test.csv', index_col='Id')
submission=pd.read_csv('sammple_submission.csv', index_col='Id')
data=train
print(train.shape, test.shape, submission.shape)

마찬가지로 데이터를 불러와준다. 주피터 노트북 파일과 저 csv 파일들이 같은 폴더 안에 있을때 잘 작동한다. 여기서는 data 라는 변수에 train을 다시 저장했는데, 데이터를 보면 변수가 매우 많고, 손을 많이 댈 예정이라서 꼬일 수도 있기 때문에 data 라는 변수를 새로 만들었다. 

print 문의 결과를 보자면

(1460, 80) (1459, 79) (1459, 1)  이라고 출력될 것이다. 

총 1460 행의 train 데이터를 가지고 1459 행의 test 데이터의 집값을 예측해야 한다. 이때 변수는 79개... 로 꽤나 많이 주어진다. 손이 많이 갈 것이다. 

 

2. 데이터 분석하기

2.1. 타겟 변수 확인 (Distribution of Target)

이번 대회는 Regression 문제인 만큼 더더욱 타겟 변수의 분포를 확인해야 할 필요성이 생겼다. 코드를 보면,

figure, (ax1, ax2) = plt.subplots(nrows=1, ncols=2)
figure.set_size_inches(14,6)
sns.distplot(data['SalePrice'], fit=norm, ax=ax1)
sns.distplot(np.log(data['SalePrice']+1), fit=norm, ax=ax2)

좌 : 타겟 변수의 분포, 우 : 타겟변수에 로그를 취한 분포. 우측이 더 정규분포에 가깝게 보인다.

왼쪽 그래프를 대충 보아도 약 400000 정도 값이 중심인 것을 알 수 있다. 하지만 많은 데이터가 400000보다 왼쪽으로 치우쳐 있는데, 이렇게 데이터의 분포가 비대칭인 정도를 'Skewness' 라고 한다. 한국어로 번역하면 '왜도' 라고 한다. 이 Skewness의 꼬리가 오른쪽으로 길게 있으면 'Right Skewed' 또는 'Positive Skewed', 왼쪽으로 길게 있으면 'Left Skewed' 또는 'Negative Skewed' 라고 한다. 오른쪽이든 왼쪽이든 심한 비대칭은 머신러닝 알고리즘이 학습을 잘 하지 못하도록 방해하는 요소 중 하나이다. 대부분의 데이터가 왼쪽에 있다면, 오른쪽 꼬리 부분은 데이터가 적어서 학습이 잘 안될 가능성이 있다. 따라서 예측 결과물도 의심할 수 밖에 없다. 이런 상황에서 'Right Skewed' 를 해결하는 대표적인 방법이 바로 해당 변수에 로그를 취하는 것이다. 키야~ 로그에 취한다~

확실히 로그를 취하면 비대칭도가 줄어들고, 정규분포에 가깝게 데이터가 분포되어 있는 것을 확인할 수 있다. 우리는 이렇게, 머신러닝에게 로그를 취한 값을 타겟 변수로 주어서 예측하게끔 한 다음에, 마지막에 제출할 때만 지수 계산을 해서 제출하면 그만이다. 

그럼 조금 이따가 타겟 변수에 로그를 취하고, 다른 그래프도 띄워보자. 

 

2.2. 변수간 상관관계 확인 (Feature Correlation)

corr=data.corr()
top_corr=data[corr.nlargest(40,'SalePrice')['SalePrice'].index].corr()
figure, ax1 = plt.subplots(nrows=1, ncols=1)
figure.set_size_inches(20,15)
sns.heatmap(top_corr, annot=True, ax=ax1)

타겟 변수 'SalePrice' 와 가장 상관관계가 높은 40개 변수의 히트맵 그래프

어떤 두 개의 변수 A와 B가 있을때, 변수 A 의 값이 커질때, 변수 B의 값도 커지면 이 둘은 양의 상관관계를 갖고 있다고 한다. 반대로, 변수 A의 값이 커질때, 변수 B의 값이 작아지면 이 둘은 음의 상관관계를 갖고 있다고 말한다. 그렇다면 상관관계가 0인 경우는? 그렇다. 변수 A가 어떻게 움직이든 변수 B에 별 영향이 없는 경우를 말한다. 이 상관관계를 측정하는 값을 '상관계수' 라고 말하고, 우리는 주로 '피어슨 상관계수' 를 이용해 상관계수를 분석한다. 이 그래프는 타겟 변수인 'SalePrice' 와 가장 큰 상관관계를 가진 40개의 변수를 표시하는 그래프이다. 그래프를 뜯어보면, 변수 'OverallQual' 이라는 변수가 상관계수 0.79로 타겟변수와 가장 큰 상관관계를 가지고 있는 것으로 나타났다. 전반적으로 OverallQual 이 증가하면, 집값도 증가한다고 볼 수 있다는 뜻이다. 

sns.regplot(data['GrLivArea'], data['SalePrice'])

GrLivArea 와 SalePrice 의 관계를 표시한 그래프

두번째로 큰 상관계수를 가진 'GrLivArea' 의 그래프를 띄워보면 위와 같다. 전체적으로 'GrLivArea' 가 증가하면 집값도 증가한다는 것을 확인할 수 있다. 다만 눈에 약간 거슬리는 부분이 있는데, 오른쪽 아래에 점 두개이다. 저 점들의 분포는 그래프 상의 저 직선으로 표현이 가능한데, 오른쪽 아래에 있는 저 두 점들은 전혀 다른 결과를 보여주고 있다. 저런 데이터들을 '이상치' (Outlier)로 간주하고 삭제해주는 것도 머신러닝의 정확도를 높일 수 있는 방법 중 하나이다. 

train=train.drop(train[(train['GrLivArea']>4000) & (train['SalePrice']<300000)].index)

이제 본격적으로 데이터를 처리할 준비를 한다. 타이타닉에서 했던 것처럼 train 과 test 에 코드를 두번 쓰지 않고, train 과 test 를 묶어서 all_data 라는 변수에 저장한 후 이를 처리하고, 머신러닝에 학습시키기 전에 다시 자를것이다.

Ytrain=train['SalePrice']
train=train[list(test)]
all_data=pd.concat((train, test), axis=0)
print(all_data.shape)
Ytrain=np.log(Ytrain+1)

타겟 변수를 미리 Ytrain으로 빼서 저장하고 위에서 말한듯이 로그를 씌웠다. 로그를 씌우는데 +1 을 해주는 이유는 log0 의 값이 없기 때문이다. 공짜 집은 없겠지만 만약 Ytrain의 값중 하나가 0이라면..? log0은 마이너스 무한대로 커지기 때문에 +1씩 해주어서 이를 방지한다. 이를 'log1p' 라고 하기도 한다. 

log1p 의 그래프

 

2.3. 전체 데이터에서 결측치 확인 (Check Missing Values)

이 데이터는 사실 상당히 깔끔하지만, 막상 꺼내어 보면 결측치가 참 많이 있다. 이는 캐글에서 데이터를 다운로드 받을 때 같이 딸려오는 'data description.txt' 파일을 읽어보면, 집에 해당 시설물이 없는 경우는 결측치로 처리되어 있음을 알 수 있다. 물론 중간에는 진짜 결측지도 있겠지만, 없는 경우가 더 많다. 따라서 결측치를 처리하는 방법이 매우 간단하다. 

cols=list(all_data)
for col in list(all_data):
    if (all_data[col].isnull().sum())==0:
        cols.remove(col)
    else:
        pass
print(len(cols))

list(all_data) 를 사용하면 all_data 라는 데이터프레임의 열 이름을 리스트로 만들 수 있다. 다음 코드는 반복문을 통해서 all_data 에서 해당 열에 결측치가 없으면 리스트에서 그 열의 이름을 지운다. 그러면 남은 리스트에는 결측치가 있는 변수 이름만 남아있을 것이다. print 문으로 출력해보면 아래 코드에 있는 이름들이 나올 것이다. 

for col in ('PoolQC', 'MiscFeature', 'Alley', 'Fence', 'FireplaceQu', 'GarageType', 'GarageFinish', 'GarageQual', 'GarageCond', 'BsmtQual', 'BsmtCond', 'BsmtExposure', 'BsmtFinType1', 'BsmtFinType2', 'MasVnrType', 'MSSubClass'):
    all_data[col] = all_data[col].fillna('None')

for col in ('GarageYrBlt', 'GarageArea', 'GarageCars', 'BsmtFinSF1', 'BsmtFinSF2', 'BsmtUnfSF','TotalBsmtSF', 'BsmtFullBath', 'BsmtHalfBath', 'MasVnrArea','LotFrontage'):
    all_data[col] = all_data[col].fillna(0)
    
for col in ('MSZoning', 'Electrical', 'KitchenQual', 'Exterior1st', 'Exterior2nd', 'SaleType', 'Functional', 'Utilities'):
    all_data[col] = all_data[col].fillna(all_data[col].mode()[0])
    
print(f"Total count of missing values in all_data : {all_data.isnull().sum().sum()}")

첫번째, 집에 해당 시설물이 없는 경우 (범주형 변수), 이때는 결측치를 'None' 이라는 문자열로 채운다. (이 작은 따옴표 없는 None 과 문자열 'None' 은 다르다!)

두번째, 집에 해당 시설물이 없는 경우(수치형 변수), 이때는 결측치를 0으로 채운다. 차고면적=0 이면 차고 없음 이런식으로 생각할 수 있다. 

세번째, 해당 시설물이 없다고 보기 힘든 경우에 있는 결측치, 이때는 결측치를 해당 열의 최빈값으로 채운다. 집에 외벽 시설이 없을리는 없고, 집이 판매가 되었는데 거래 타입이 정해지지 않을리는 없다. 

 

2.4. 본격적으로 데이터 분석 (EDA)

데이터의 변수 이름을 유심히 뜯어보던 나는 몇 개의 새로운 변수를 만들기로 했다. 

 

1) 총 가용면적 (Total SF Available)

figure, ((ax1, ax2), (ax3, ax4)) = plt.subplots(nrows=2, ncols=2)
figure.set_size_inches(14,10)
sns.regplot(data['TotalBsmtSF'], data['SalePrice'], ax=ax1)
sns.regplot(data['1stFlrSF'], data['SalePrice'], ax=ax2)
sns.regplot(data['2ndFlrSF'], data['SalePrice'], ax=ax3)
sns.regplot(data['TotalBsmtSF'] + data['1stFlrSF'] + data['2ndFlrSF'], data['SalePrice'], ax=ax4)

오른쪽 아래가 총 면적 그래프이다.

지하실, 1층, 2층 면적을 모두 합한 '총 면적' 이란 변수를 추가로 만들었다. 오른쪽 아래에 있는 그래프가 나머지 3개를 합한 면적을 나타낸 그래프인데, 상당히 타겟변수를 잘 설명할 수 있다. 상관관계도 꽤 높아보인다. 이는 총 면적이 증가하면, 집값이 더 비싸진다고 볼 수 있다. 이들을 더해서 'TotalSF' 라는 이름으로 저장해야겠다. 

all_data['TotalSF']=all_data['TotalBsmtSF'] + all_data['1stFlrSF'] + all_data['2ndFlrSF']
all_data['No2ndFlr']=(all_data['2ndFlrSF']==0)
all_data['NoBsmt']=(all_data['TotalBsmtSF']==0)

추가로 'No2ndFlr' 과 'NoBsmt' 라는 변수를 만들어서 2층 없음, 지하실 없음 여부를 나타낼 수 있도록 했다. 

2층면적을 나타내는 초록 그래프를 보면, 나타나있는 직선이 점들과 별로 맞지 않는 것처럼 보인다. 나는 그 이유를 아마 저 그래프의 0들 때문이라고 생각해서, 이 0들을 따로 분리해낸다면 상당히 예쁜 직선을 그릴 수 있을 것이라고 판단했다. 

 

2) 총 욕실 수 (Bath)

figure, ((ax1, ax2), (ax3, ax4)) = plt.subplots(nrows=2, ncols=2)
figure.set_size_inches(14,10)
sns.barplot(data['BsmtFullBath'], data['SalePrice'], ax=ax1)
sns.barplot(data['FullBath'], data['SalePrice'], ax=ax2)
sns.barplot(data['BsmtHalfBath'], data['SalePrice'], ax=ax3)
sns.barplot(data['HalfBath'], data['SalePrice'], ax=ax4)

figure, (ax5) = plt.subplots(nrows=1, ncols=1)
figure.set_size_inches(14,6)
sns.barplot(data['BsmtFullBath'] + data['FullBath'] + (data['BsmtHalfBath']/2) + (data['HalfBath']/2), data['SalePrice'], ax=ax5)

욕실이 총 4개 변수로 나누어져있다.

욕실 갯수는 'FullBath' 와 'HalfBath', 그리고 지하에 있는지 여부, 총 4개 열로 이루어져있었는데, 이들을 모두 더해 하나로 만들어 보았다. FullBath 는 욕조 및 샤워 시설이 포함되어있는 욕실이고, HalfBath는 변기와 세면대 정도 있는 간단한 욕실을 말한다. 이름에 맞게 FullBath는 1개로 카운트하고, HalfBath 는 0.5개로 카운트해서 모두 더했더니 다음과 같은 그래프를 얻을 수 있었다. 

욕실 수의 총합

이를 보면 욕실 수가 많을수록 더 집값이 비싸진다고 볼 수 있다. (더 큰 집일수록 욕실수가 많을 것이라 생각한다). 하지만 여기서 특이한 점이 하나 있는데, 욕실 갯수가 5개, 6개인 집들은 막대그래프 위의 검정 세로선이 보이지 않는다. 에 이게뭐지? 할 수도 있지만, 저 검정 선은 편차를 의미한다. 그렇다면 편차가 없다는것이 무슨뜻이지? 값이 다른 데이터가 두개만 있어도 편차가 발생할텐데? 화장실 갯수가 5개, 6개인 집은 각각 하나씩밖에 없다는 뜻으로 볼 수 있다. 이들은 역시 outlier 로 판단하고 지워도 상관이 없다. 

all_data['TotalBath']=all_data['BsmtFullBath'] + all_data['FullBath'] + (all_data['BsmtHalfBath']/2) + (all_data['HalfBath']/2)

 

3) 건축연도 + 리모델링 연도 (Year Built and Remodeled)

이 둘은 상당히 비슷한 분포로 데이터가 이루어져 있다. 하지만 나는 이 둘을 모두 포함하는 데이터를 하나 추가하고 싶었다. 

figure, (ax1, ax2, ax3) = plt.subplots(nrows=1, ncols=3)
figure.set_size_inches(18,8)
sns.regplot(data['YearBuilt'], data['SalePrice'], ax=ax1)
sns.regplot(data['YearRemodAdd'], data['SalePrice'], ax=ax2)
sns.regplot((data['YearBuilt']+data['YearRemodAdd'])/2, data['SalePrice'], ax=ax3) #'/2' for graph scaling

건축연도, 리모델링 연도, 둘의 평균

건축 연도와 리모델링 연도의 평균을 구하는것이 무슨 의미가 있나 할 수도 있다. 하지만 아주 의미가 없지는 않다! 건축 연도가 오래 되었어도, 최근에 리모델링을 하면 이 값이 높게 나왔을 것이고, 건축 이후 리모델링을 하지 않았다면 이 값은 아주 낮게 나왔을 것이다. 따라서 이 값이 높은 집들은 '지어진지 얼마 되지 않은 신축 건물 + 최근에 리모델링까지함' 에 가깝고, 이 값이 낮은 집들은 '오래된 건물 + 리모델링도 안함' 에 가까울 것이다. 그리고 이는 집값에 유의미한 영향이 있다고 초록색 그래프를 보면 판단할 수 있다. 

all_data['YrBltAndRemod']=all_data['YearBuilt']+all_data['YearRemodAdd']

이를 'YrBltAndRemod' 라는 이름으로 두 개의 변수를 저장하자. 

 

2.5. 자료형 수정 (Correcting Dtypes)

all_data['MSSubClass']=all_data['MSSubClass'].astype(str)
all_data['MoSold']=all_data['MoSold'].astype(str)
all_data['YrSold']=all_data['YrSold'].astype(str)

데이터 설명을 보면 나와있지만 'MSSubClass' 는 사실 숫자로 이루어진 데이터지만, 각각의 숫자가 의미를 갖고있는 범주형 변수이다. 그리고 판매 연, 월도 역시 연산 개념을 적용하는데는 무리가 있다. 그래서 이들을 문자열로 바꿔주자. 

 

나는 사람들이 집을 볼때, 1층을 쭉 둘러보고 '괜찮네', 2층을 보고 '별로네', 지하층과 차고를 보고 '좋네' 이런식으로 판단할 거라고 생각했다. 이 평가의 기준 점수는 사람마다 약간 다르겠지만, 해당 시설물을 종합적으로 보고 판단하는 행위는 모두가 비슷할 것이라 생각했다. 나는 그래서 집의 시설물들을 묶어서 점수를 매기기로 했다. 

 

2.6. 지하실 점수 (Bsmt)

Basement = ['BsmtCond', 'BsmtExposure', 'BsmtFinSF1', 'BsmtFinSF2', 'BsmtFinType1', 'BsmtFinType2', 'BsmtQual', 'BsmtUnfSF', 'TotalBsmtSF']
Bsmt=all_data[Basement]

지하실에 관한 변수들을 묶어서 저장하고,

Bsmt=Bsmt.replace(to_replace='Po', value=1)
Bsmt=Bsmt.replace(to_replace='Fa', value=2)
Bsmt=Bsmt.replace(to_replace='TA', value=3)
Bsmt=Bsmt.replace(to_replace='Gd', value=4)
Bsmt=Bsmt.replace(to_replace='Ex', value=5)
Bsmt=Bsmt.replace(to_replace='None', value=0)

Bsmt=Bsmt.replace(to_replace='No', value=1)
Bsmt=Bsmt.replace(to_replace='Mn', value=2)
Bsmt=Bsmt.replace(to_replace='Av', value=3)
Bsmt=Bsmt.replace(to_replace='Gd', value=4)

Bsmt=Bsmt.replace(to_replace='Unf', value=1)
Bsmt=Bsmt.replace(to_replace='LwQ', value=2)
Bsmt=Bsmt.replace(to_replace='Rec', value=3)
Bsmt=Bsmt.replace(to_replace='BLQ', value=4)
Bsmt=Bsmt.replace(to_replace='ALQ', value=5)
Bsmt=Bsmt.replace(to_replace='GLQ', value=6)

이들을 인코딩해준다. 이 'Po', 'Fa', ... 등의 값은 data description 텍스트 파일에 나와있다. 따라서 순서대로 좋은 값에는 높은 숫자를, 안좋은 값에는 낮은 숫자를, 지하실이 없으면 0을 입력해주었다. 

Bsmt['BsmtScore']= Bsmt['BsmtQual']  * Bsmt['BsmtCond'] * Bsmt['TotalBsmtSF']
all_data['BsmtScore']=Bsmt['BsmtScore']

Bsmt['BsmtFin'] = (Bsmt['BsmtFinSF1'] * Bsmt['BsmtFinType1']) + (Bsmt['BsmtFinSF2'] * Bsmt['BsmtFinType2'])
all_data['BsmtFinScore']=Bsmt['BsmtFin']
all_data['BsmtDNF']=(all_data['BsmtFinScore']==0)

그리고 몇개 항목들을 곱해서 'BsmtScore' 항목을 만들어 지하실의 전반적인 상태를 복합적으로 평가할 수 있는 변수를 만들었다. 'BsmtFin' 변수는 지하실이 공사중이라면, 완성 면적과 상태를 포함하는 변수이다. 

'BsmtFinScore' 은 지하실의 완성도 점수, 'BsmtScore' 은 지하실의 종합 점수, 'BsmtDNF' 는 지하실의 미완성 여부를 나타내는 변수이다. 

 

2.7. 토지 점수 (Lot)

lot=['LotFrontage', 'LotArea','LotConfig','LotShape']
Lot=all_data[lot]

Lot['LotScore'] = np.log((Lot['LotFrontage'] * Lot['LotArea'])+1)

all_data['LotScore']=Lot['LotScore']

비슷한 상태도 토지면적과 모양, 접근성 등등을 고려할 수 있는 점수를 만들어 'LotScore' 로 저장하고

 

2.8. 차고 점수 (Garage)

garage=['GarageArea','GarageCars','GarageCond','GarageFinish','GarageQual','GarageType','GarageYrBlt']
Garage=all_data[garage]
all_data['NoGarage']=(all_data['GarageArea']==0)

차고에 관해서도 같은 방법으로 실행했다. 

Garage=Garage.replace(to_replace='Po', value=1)
Garage=Garage.replace(to_replace='Fa', value=2)
Garage=Garage.replace(to_replace='TA', value=3)
Garage=Garage.replace(to_replace='Gd', value=4)
Garage=Garage.replace(to_replace='Ex', value=5)
Garage=Garage.replace(to_replace='None', value=0)

Garage=Garage.replace(to_replace='Unf', value=1)
Garage=Garage.replace(to_replace='RFn', value=2)
Garage=Garage.replace(to_replace='Fin', value=3)

Garage=Garage.replace(to_replace='CarPort', value=1)
Garage=Garage.replace(to_replace='Basment', value=4)
Garage=Garage.replace(to_replace='Detchd', value=2)
Garage=Garage.replace(to_replace='2Types', value=3)
Garage=Garage.replace(to_replace='Basement', value=5)
Garage=Garage.replace(to_replace='Attchd', value=6)
Garage=Garage.replace(to_replace='BuiltIn', value=7)
Garage['GarageScore']=(Garage['GarageArea']) * (Garage['GarageCars']) * (Garage['GarageFinish']) * (Garage['GarageQual']) * (Garage['GarageType'])
all_data['GarageScore']=Garage['GarageScore']

'GarageScore' 변수로 차고의 종합 점수를 판단할 수 있도록 했다. 

 

2.9. 기타 변수 (Other Features)

1) 비정상적으로 하나의 값만 많은 변수들 삭제

all_data=all_data.drop(columns=['Street','Utilities','Condition2','RoofMatl','Heating'])

 

2) 비정상적으로 빈 값이 많은 변수들 삭제

집에 수영장 있는 집이 몇집이나 되겠는가...

그래프를 띄워보면 참 어이가 없다. 

figure, (ax1, ax2) = plt.subplots(nrows=1, ncols=2)
figure.set_size_inches(14,6)
sns.regplot(data=data, x='PoolArea', y='SalePrice', ax=ax1)
sns.barplot(data=data, x='PoolQC', y='SalePrice', ax=ax2)

수영장이 있는 집이라니

all_data=all_data.drop(columns=['PoolArea','PoolQC'])

.

figure, (ax1, ax2) = plt.subplots(nrows=1, ncols=2)
figure.set_size_inches(14,6)
sns.regplot(data=data, x='MiscVal', y='SalePrice', ax=ax1)
sns.barplot(data=data, x='MiscFeature', y='SalePrice', ax=ax2)

테니스코트 있는 집은 그나마 낫다

all_data=all_data.drop(columns=['MiscVal','MiscFeature'])

 

3) 위 둘보다는 낫지만, (채워진)결측치가 많은 경우

sns.regplot(data=data, x='LowQualFinSF', y='SalePrice')
sns.regplot(data=data, x='OpenPorchSF', y='SalePrice')
sns.regplot(data=data, x='WoodDeckSF', y='SalePrice')

이들은 0 값만 분리해주면 결과가 나쁘지 않을 것 같아서 0만 분리해주었다. 

all_data['NoLowQual']=(all_data['LowQualFinSF']==0)
all_data['NoOpenPorch']=(all_data['OpenPorchSF']==0)
all_data['NoWoodDeck']=(all_data['WoodDeckSF']==0)

 

 

3. 전처리 (Preprocessing)

3.1. 범주형 변수 (Categorical Feature)

범주형 변수가 상당히 많은 데이터셋이다. 범주형 변수를 다짜고짜 1, 2, 3, 4, ... 로 인코딩해주면 머신러닝이 오해할 소지가 충분하기 때문에 일단 모조리 원핫 인코딩을 한다. 

non_numeric=all_data.select_dtypes(np.object)

def onehot(col_list):
    global all_data
    while len(col_list) !=0:
        col=col_list.pop(0)
        data_encoded=pd.get_dummies(all_data[col], prefix=col)
        all_data=pd.merge(all_data, data_encoded, on='Id')
        all_data=all_data.drop(columns=col)
    print(all_data.shape)
    
onehot(list(non_numeric))

 

3.2. 수치형 변수 (Numeric Feature)

수치형 변수는 비대칭이 너무 심해지지 않게끔, Right Skewed 가 크게 되어있는 데이터들에만 로그를 씌워 적절히 변형시켜준다. 

numeric=all_data.select_dtypes(np.number)

def log_transform(col_list):
    transformed_col=[]
    while len(col_list)!=0:
        col=col_list.pop(0)
        if all_data[col].skew() > 0.5:
            all_data[col]=np.log(all_data[col]+1)
            transformed_col.append(col)
        else:
            pass
    print(f"{len(transformed_col)} features had been tranformed")
    print(all_data.shape)

log_transform(list(numeric))

 

그럼 이제 데이터를 나누는 일만 남았다. all_data 항목을 test 데이터의 갯수에 맞게끔 잘라서 따로 저장하면 된다. 

print(train.shape, test.shape)
Xtrain=all_data[:len(train)]
Xtest=all_data[len(train):]
print(Xtrain.shape, Xtest.shape)

(1458, 79) (1459, 79)

(1458, 309) (1459, 309)

원래 train 데이터의 갯수와 데이터를 가공한 Xtrain 데이터의 갯수가 1458 개로 동일하다. (위에서 'GrLivArea' 할 때 train 에서 두개를 지웠었다!)

 

 

4. 머신러닝 모델로 학습

from sklearn.linear_model import ElasticNet, Lasso
from sklearn.ensemble import GradientBoostingRegressor
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import RobustScaler
from xgboost import XGBRegressor
import time
import optuna
from sklearn.model_selection import cross_val_score
from sklearn.metrics import mean_squared_error
model_Lasso= make_pipeline(RobustScaler(), Lasso(alpha =0.000327, random_state=18))

model_ENet = make_pipeline(RobustScaler(), ElasticNet(alpha=0.00052, l1_ratio=0.70654, random_state=18))

model_GBoost = GradientBoostingRegressor(n_estimators=3000, learning_rate=0.05, max_depth=4, max_features='sqrt', min_samples_leaf=15, 
                                         min_samples_split=10, loss='huber', random_state=18)

model_XGB=XGBRegressor(colsample_bylevel=0.9229733609038979,colsample_bynode=0.21481791874780318,colsample_bytree=0.607964318297635, 
                       gamma=0.8989889254961725, learning_rate=0.009192310189734834, max_depth=3, n_estimators=3602, 
                       reg_alpha=3.185674564163364e-12,reg_lambda=4.95553539265423e-13, seed=18, subsample=0.8381904293270576,
                       tree_method='gpu_hist',verbosity=0)

Lasso, ElasticNet, sklearn의 GradientBoosting, XGBoost 4개의 모델을 불러와서 저장한다. 최종 예측 결과물은 이 4개의 모델의 예측값의 평균값을 사용해 제출할 것이다. 

model_Lasso.fit(Xtrain, Ytrain)
Lasso_predictions=model_Lasso.predict(Xtest)
train_Lasso=model_Lasso.predict(Xtrain)

model_ENet.fit(Xtrain, Ytrain)
ENet_predictions=model_ENet.predict(Xtest)
train_ENet=model_ENet.predict(Xtrain)

model_XGB.fit(Xtrain, Ytrain)
XGB_predictions=model_XGB.predict(Xtest)
train_XGB=model_XGB.predict(Xtrain)

model_GBoost.fit(Xtrain, Ytrain)
GBoost_predictions=model_GBoost.predict(Xtest)
train_GBoost=model_GBoost.predict(Xtrain)

log_train_predictions = (train_Lasso + train_ENet + train_XGB + train_GBoost)/4
train_score=np.sqrt(mean_squared_error(Ytrain, log_train_predictions))
print(f"Scoring with train data : {train_score}")

log_predictions=(Lasso_predictions + ENet_predictions + XGB_predictions + GBoost_predictions) / 4
predictions=np.exp(log_predictions)-1
submission['SalePrice']=predictions
submission.to_csv('Result.csv')

0.11657 정도의 점수가 나올 것이다. 대략 4600명 중에서 약 517등. 상위 약 12퍼센트 정도의 점수이다. (2020년 3월 4일 기준)

반응형
Posted by Jamm_
반응형
 

Titanic: Machine Learning from Disaster

Start here! Predict survival on the Titanic and get familiar with ML basics

www.kaggle.com

 

머신러닝에 입문하는 사람들이라면 누구나 들어봤을, 모두가 머신러닝 입문용으로 시작하는 바로 '그' 경진대회이다. 그 이름은 타이타닉. 나도 타이타닉으로 머신러닝에 입문했었다. 지금은 약 1년 몇 개월 전의 일이네. 내가 지금까지 참가한 대회 중에서 가장 성적이 높은 대회이기도 하다...(눈물 주르륵 광광)

 

 

정확도 약 82.2% 라는 뜻이다

 

 

데이콘이라는 사이트에서도 타이타닉 컴퍼티션이 있다. 참고로 캐글에서 0.82296의 점수는 (2020년 3월 4일 기준) 16000여 명 중에서 471등을 기록하고 있다. 캐글의 타이타닉 대회에서는 점수 측정공식이 '정확도' 인 반면, 데이콘에서는 'RMSE' 를 사용한다. RMSE를 사용한다면, 아무래도 데이터를 사망 여부 (사망시 0, 생존시 1) 로 딱딱 떨어지게 예측하기보다는, 생존 확률 (0.6인경우, 60퍼센트의 확률로 생존 가능성) 로 결과물을 제출하는 것이 더 낫기 때문에, 코드의 마지막 줄만 바꾸어서 데이콘에 제출했더니 3위이다. 

 

 

1등과 점수차이가 좀 나긴 하지만, 그래도 3등이다!

 

 

뭐 이런 튜토리얼성 경진대회 잘한다고 실력이 좋은, 뛰어난 데이터 사이언티스트의 싹이 보인다는 뜻은 당연히... 맞을 수도 있지만 아닐 가능성도 있다. 그래도 입문하면서 점수가 잘 나오면, 기분이 매우 좋다! 나는 처음 캐글을 시작하면서 랭킹이 바로바로 업데이트 되는 것을 보고 아주 환장을 하고 죽기살기로 덤볐었다. 랭킹 몇 등 올리는것이 당시 나에게는 세상 그 무엇보다도 재미있는 일이었다. 

아무튼, 나는 이 코드를 이미 캐글에 수백 번 제출했었고, 내가 하는 스터디에서도 발표했었고, 캐글 노트북(캐글 커널) 에도 업로드를 했었고, 아주 수도 없이 우려먹었었지만, 이제 마지막으로 우려먹으려고 한다. 이 블로그에 나의 머신러닝에 관한 모든 기록을 담아서 보관할 예정이라서, 여기에도 업로드를 하려고 한다.

아마 구글 검색해서 나오는 코드들중에서는 내 코드가 가장 쉬울 거라고 생각한다.... 아무리 봐도 내 코드는 너무 쉬워보인다.. 

 

머신러닝에 입문하는 사람들에게 약간이나마 도움이 되었으면 합니다.

지식은 고여있으면 썩는 법이니까.

 

21.02. - 처음에는 리더보드가 test 일부만 가지고 채점되어서 0.82296이었는데, 현재는 test 전체를 이용해서 채점하기 때문에 점수가 바뀌었습니다. 0.79186이네요. 데이콘에서도 지금은 RMSE를 사용하지 않습니다!

 


 

 

1. 패키지 로드

import pandas as pd
import numpy as np
%matplotlib inline
import seaborn as sns
import matplotlib.pyplot as plt
from scipy.stats import norm

간단히 설명을 하자면

pandas : 데이터 (주로 행렬 데이터) 를 분석하는데 쓰이는 파이썬 패키지. 특히 표 데이터를 다룰때 거의 무조건 사용한다. 

numpy : 선형대수 연산 파이썬 패키지. 그냥 수학 계산을 편하고 빠르게 할 수 있는 패키지라고 생각하면 쉽다. 

%matplotlib.inline : 그래프를 주피터 노트북 화면상에 바로 띄울 수 있게 하는 코드. 근데 없어도 seaborn 그래프 잘만 되더라.

seaborn : 쉽고 직관적인 방법으로 그래프를 띄울 수 있는 패키지. 

matplotlib.pyplot : matplotlib 라는 그래프 띄우는 패키지 중 일부. 나는 여러개의 그래프를 한 결과로 띄우고 싶을때만 주로 사용한다. 

norm : 정규분포를 의미. 데이터의 분포 그래프를 띄울때, 이 norm 을 불러와주면 정규분포랑 손쉽게 비교해 볼 수 있다. 

 

2. 데이터 로드

train=pd.read_csv('train.csv', index_col='PassengerId')
test=pd.read_csv('test.csv', index_col='PassengerId')
submission=pd.read_csv('gender_submission.csv', index_col='PassengerId')

print(train.shape, test.shape, submission.shape)

train.csv, test.csv, submission.csv 이 세 파일이 지금 작업하고있는 주피터 노트북과 같은 폴더 안에 있을 때 작동한다. 만약 다른 폴더에 있다면, '/Users/뭐시기/뭐시기/Desktop/train.csv' 처럼 경로를 지정해준다면 잘 열릴 것이다. 

pd(판다스) 의 'read_csv' 라는 메소드를 이용해서 csv 파일을 쉽게 읽어올 수 있다. 'index_col' 은 해당 열을 (여기서는 승객번호) 인덱스로 사용할 수 있게끔 해준다. 이후에 데이터를 보면 알겠지만, 승객번호는 1번, 2번, 3번, .... 800몇번 이런식으로 쭉 증가하는 고유한 값이기 때문에 나는 이 열을 인덱스로 사용했다.

마지막 print 문을 실행하면 결과는 아마

(891, 11) (418, 10) (418, 1)

이라고 출력이 될 것이다. 이 괄호 안의 숫자들이 우리가 읽어온 데이터 행렬의 크기를 나타낸다. 표시되는건, (행 숫자, 열 숫자) 로 출력이 된다. 우리는 train.csv 에 있는 891명의 승객 데이터를 가지고, test.csv 에 있는 418명의 생존 여부를 예측해서 제출해야 한다는 의미이다. 

 

3. 데이터 분석 시작

이제 남은 일은 내 입맛대로 데이터를 분석하고 요리하면 되는 것이다. 내가 행렬 데이터를 보면 항상 하는 순서가 있는데, 그 순서대로 해보겠다. 

이렇게 데이터의 열에 '이름' 이 붙어있다면, 데이터를 분석할 때 상식적인 선에서의 '가설' 을 세우고 접근하는 방법이 있다. 예를 들면, 여성이 남성보다 생존 확률이 높을 것이다. 어린이들은 생존 확률이 높을 것이다. 라는 등의 가설을 세울 수 있겠다. 이는 경우에 따라서는 상당히 좋은 방법이다. 일반적인, 그니까 상식적인 수준에서 가설을 세울 수 있다면 데이터 분석 과정을 상당히 빠르게 해낼 수 있다. 다만, 데이터가 '익명화' 되어서 데이터 열에 이름이 없는 경우, 가설을 세우기가 상당히 어려워진다. 또한, 해당 분야의 배경지식이 필요한 전문 분야의 데이터일수록 가설 세우기는 더더욱 어렵게 된다. 전문 지식이 있으면 좀 낫겠지만. 하지만 1년 몇개월 간의 짧은 경험 상으로는 없다고 해서 크게 문제되지는 않았다. '컴퍼티션 진행하는데 배경 지식이 있으면 훨씬 쉬웠겠지만, 없어도 지장은 없다.' 정도의 느낌을 받았다.

 

3.1. 타겟 변수 확인

먼저 우리가 예측해야하는 '생존 여부' 가 어떻게 이루어져 있는지 보자. 생존 여부는 train 데이터에만 있고 test 에는 없다. 우리는 train 데이터에서 주어진 승객들의 정보와 그들의 생존 여부를 머신러닝에게 학습하도록 하고, test 데이터에 존재하는 승객들의 생존 여부를 맞출 것이다. 이렇게 우리가 맞춰야 하는 데이터를 타겟 변수(target variable), 혹은 종속 변수 라고 부른다. 나는 거의 타겟 변수라고 많이 부른다. 

sns.countplot(train['Survived'])
train['Survived'].value_counts()

 

 

아마 이런 결과물이 나올 것이다. 0이 사망자, 1이 생존자를 의미한다. 그래프를 보면 사망자의 수가 생존자의 수보다 더 많은 것을 알 수 있다. 

데이터별로 이런 타겟 변수가 한쪽의 값만 지나치게 많은 경우, 우리는 이를 'Class imbalanced problem' 이라고 부른다. 카드 사기 거래 여부, 하늘에서 운석이 떨어지는 경우를 예를 들어 보면 사기가 아닌경우, 운석이 떨어지지 않을 경우가 반대의 경우보다 그 수가 압도적으로 많다. 이때 별다른 처리 없이 머신러닝에게 데이터를 학습시킨다면, 머신러닝이 모든 데이터를 0(사망, 혹은 정상거래, 혹은 운석 안떨어짐) 이라고 예측할지도 모른다. 그리고 이 정확도를 보면, 상당히 높게 나온다. 실제로 우리가 예측하려는 데이터에도 운석이 떨어지는 날은 몇일 되지 않을 것이니까. 하지만 이 녀석은 참으로 의미없는 머신러닝 모델이 될 것이다. 실제로 우리나라 일기예보도 '365일 비 안옴' 이라고 예측하면 정확도가 75% 정도 된다고 하지만, 이것이 의미있는 예측은 아니지 않은가. 이럴때는 여러 방법들을 통해 이 불균형을 해결한 후 머신러닝 알고리즘으로 학습을 시켜야 의미있는 예측을 하는 경우가 대부분이다. 그리고 타이타닉 데이터에 있는 이 불균형정도면, 상당히 양호한 편이라고 할 수 있다. 

참고로, 모든 승객을 사망 이라고 처리하고 제출해도 캐글 정확도는 약 60% 가 넘게 나온다. 

 

3.2. 결측치 처리

print(train.isnull().sum())
print(test.isnull().sum())

실행하면 아마 다음과 같은 결과가 나올 것이다. 

 

train.isnull().sum()
test.isnull().sum()

 

결측치는 말 그대로 측정하지 못한 값이다. 'Age' 와 'Cabin' 열에서 결측치가 특히 많이 발생한 것을 볼 수 있다. 생각해보면 결측치는 '없는 데이터' 이기 때문에, 머신러닝이 여기서 배울 수 있는것은 전혀 없다. 그리고 사용하는 머신러닝 알고리즘에 따라서 데이터에 결측치가 있는 경우 에러가 날 수도 있다. 결측치를 처리하는 방법으로는, 결측치가 있는 데이터를 지워버리는 방법도 있고, 주변값 또는 대표값으로 결측치를 채워 넣는 방법도 있다. 나는 일단 Cabin 을 먼저 지워버려야겠다. 

train=train.drop(columns='Cabin')
test=test.drop(columns='Cabin')

이렇게 하면 'Cabin' 열을 지울 수가 있다!

 

3.3. 성별

sns.countplot(data=train, x='Sex', hue='Survived')

 

성별에 따른 사망자수, 생존자수

 

그래프를 띄우는 코드를 설명해보자면, train 데이터를 사용하고, x 축에는 '성별', 그리고 'Survived' 항목으로 구분해서 나누어본다. 라는 뜻이다. 

결과를보면, 남자인 경우는 사망자수가 생존자수보다 더 많고, 여자인 경우는 사망자수보다 생존자 수가 더 많아, 생존률이 더 높은것을 확인할 수 있다. 이 성별에 해당하는 데이터는 머신러닝에게 유용한 정보를 제공할 수 있을 것이다. 

train.loc[train['Sex']=='male', 'Sex']=0
train.loc[train['Sex']=='female','Sex']=1
test.loc[test['Sex']=='male','Sex']=0
test.loc[test['Sex']=='female','Sex']=1

train 데이터와 test 데이터에 성별 데이터를 '인코딩' 해 준다. 머신러닝 알고리즘은 기본적으로 문자를 인식하지 못한다. 'male' 이라고 써있으면, 사람이야 바로 아 남성~ 하지만 머신러닝 알고리즘이 이걸 어떻게 알아듣겠는가? 그래서 이런 데이터를 0, 1 같은 숫자로 바꿔주는 일을 '인코딩' 이라고 한다. 

첫 줄의 코드를 말로 설명하자면, "train 에서 [ train 데이터의 [성별] 이 'male' 인 사람의, '성별' 칸에는] = 0 이라고 입력해라." 정도로 해석할 수 있다. 밑에 줄들도 마찬가지. 

이제 다음 칸에 "train['Sex']" 라고 쳐보면, 남성은 0, 여성은 1 로 데이터가 바뀌어있는것을 볼 수 있다. 

 

3.4. Pclass (객실 등급)

sns.countplot(data=train, x='Pclass', hue='Survived')

 

객실 등급에 따른 사망자, 생존자 수

 

보면, 1등석일수록 생존 확률이 높고, 3등석에는 사망률이 높아진다는 것을 알 수 있다. 역시 돈이 최고다. 아무튼 3등석보다 2등석이, 2등석보다 1등석이 생존 확률이 더 높다는 것은 머신러닝에게 좋은 정보로 작용할 것이다. 그럼 이 정보도 역시 인코딩 해주어야한다. 어? 근데 데이터가 1,  2, 3 숫자네? 아싸 개꿀~ 하려고 하는데 근데 여기서 문제가 생긴다. 

기존 0과 1에서는 생기지 않았던 문제인데, 생각해보면 (1등석) + (2등석) = (3등석) 이 성립하는가?

(2등석) ^ 2 = (1등석) + (3등석) 이것도 성립하나? 개념상으로는 맞지 않다는것을 바로 알 수 있다. 이런 형태의 데이터를 '범주형 변수', 영어로 'categorical feature' 이라고 한다. 이렇게 숫자로 넣어주면 머신러닝 알고리즘은 딱 오해하기 좋다. 혹시나 수학 계산 과정이 필요한 알고리즘이라면, 위와 같은 오해를 하기 정말 좋은 상황이다. 이럴때 필요한게 바로 '원-핫 인코딩(one-hot encoding)' 이다. 

이런 상황에서 원래대로 인코딩을 한다면, 데이터는 1개의 열에 모두 저장되어서 다음과 같이 표시될 것이다. 

객실등급 : 1, 3, 2, 3, ....

하지만 원-핫 인코딩은 여러 개의 열을 추가한다. 이런 데이터에서 예를 들어보면, 

객실 1등급 여부 : True, False, False, False, ...

객실 2등급 여부 : False, False, True, False, ...

객실 3등급 여부 : False, True, False, True, ...

이런식으로 인코딩을 진행한다. 이렇게 되면, 이 열들을 어떻게 더하고 곱하고 지지고 볶아도 중복이 나오지 않는다! 하지만 이 원핫 인코딩의 치명적인 단점은, 처리해야할 변수의 갯수가 아주아주 많아진다는 것이다. (그럴 일은 없겠지만) 만약에 객실이 60개 등급으로 이루어졌다면.... 총 60개의 변수가 생긴다. 이는 Tree 기반의 머신러닝 알고리즘에서 그닥 좋은 영향은 주지 못한다. 때에 따라서 원핫 인코딩할 변수가 너무 많다면, 그냥  1, 2, 3, ... 으로 인코딩 하기도 한다. (머신러닝 라이브러리 sklearn 에서는 이를 'LabelEncoder' 로 구현해두었다.) 상황에 따라 적절한 대처가 필요하다. 

train['Pclass_3']=(train['Pclass']==3)
train['Pclass_2']=(train['Pclass']==2)
train['Pclass_1']=(train['Pclass']==1)

test['Pclass_3']=(test['Pclass']==3)
test['Pclass_2']=(test['Pclass']==2)
test['Pclass_1']=(test['Pclass']==1)

코드를 말로 해석해보자면 "train 데이터에 있는 ['Pclass_3'] 이라는 열에 (없으면 만들고) train 데이터의 ['Pclass'] 열의 값이 3인 애들은 True, 아니면 False 로 입력해줘라" 라는 뜻이다. 밑에 줄도 마찬가지로 해석할 수 있다. 이렇게 되면 위에서 언급한 대로 원-핫 인코딩을 할 수 있다. 원-핫 인코딩을 완료했으니, 더이상 필요가 없어진 원래의 'Pclass' 열은 삭제하도록 하자.

train=train.drop(columns='Pclass')
test=test.drop(columns='Pclass')

 

3.5. 나이 (Age) 와 요금 (Fare)

그런 생각이 들었었다. 아무래도 나이가 어리면 비싼 요금은 내기 힘들 것이다. 그리고 비싼 요금은 높은 객실 등급과 더 연관이 클 것이다. 그렇다면 이것도 객실 등급처럼 생존 확률에 유의미한 영향을 끼쳤는지 한번 확인해보자. 

sns.lmplot(data=train, x='Age', y='Fare', fit_reg=False, hue='Survived')

 

나이와 요금의 상관관계 및 생존 여부를 나타낸 도표

 

못보던게 하나가 등장했는데 'fit_reg' 이다. 이 lmplot(엘 엠 플랏) 은 이런 점찍혀있는 그래프와, 점들을 대표하는 하나의 직선이 있는 그래프인데, 나는 점만 보고 싶어서 선을 없앴다. 이 선을 없애는 명령이 'fit_reg=False' 이다. 선 궁금하면 한번 'fit_reg=True' 라고 해봐도 된다.여기서만 볼때는 딱히 유의미하다고 판단하기가 어렵다. 그래프를 확대해보자.

LowFare=train[train['Fare']<80]
sns.lmplot(data=LowFare, x='Age', y='Fare', hue='Survived')

 

위 도표에서 요금을 80달러 미만으로 지불한 사람들만 모음 

 

train 데이터에서 요금을 80불 이하로 지불한 사람들을 모아서 LowFare 이라는 변수로 저장한 후, 이 LowFare만을 활용해서  그래프를 다시 띄워본다. 

이렇게 보니까 더더욱 알 수가 없다. 선을 보니 사망자와 생존자 간에 약간의 차이는 있지만, 점들의 위치를 봤을때는 유의미한 차이가 있다고 보기는 어려울 것 같다. 

위에서 결측치를 찾아볼 때, test 데이터에 'Fare' 에서 한개의 결측치를 발견했었다. 그걸 그냥 0으로 채워버려야겠다. 

test.loc[test['Fare'].isnull(),'Fare']=0

요금은 뭔가 Pclass 와 연관이 있을 것 같기도 하지만, 나이 항목은 좀처럼 무언가를 발견할 수 없었다. 따라서 'Age' 변수만 지우기로 결정했다. 

train=train.drop(columns='Age')
test=test.drop(columns='Age')

 

3.6. SibSp & Parch

영문도 모를 영어가 등장했다. 이 둘을 설명하자면, 

SibSp : Sibling(형제자매) + Spouse(배우자)

Parch : Parents(부모) + Children(자녀)

두 단어들을 줄여놓은 것이다...낚였누 이 데이터들은 '같이 동행하는 일행' 에 대한 정보를 담고 있다고 할 수 있다. 

이 둘을 더하고 + 1 (자기 자신) 을 한다면, '타이타닉에 탄 일행의 명수' 를 알수 있지 않을까? 아무래도 일행이 너무 많으면 다 찾아다니고 챙기고 하다가 탈출할 골든 타임을 놓쳤을 수도 있을 것이다. 

train['FamilySize']=train['SibSp']+train['Parch']+1
test['FamilySize']=test['SibSp']+test['Parch']+1

figure, (ax1, ax2, ax3) = plt.subplots(nrows=1, ncols=3)
figure.set_size_inches(18,6)
sns.countplot(data=train, x='SibSp', hue='Survived', ax=ax1)
sns.countplot(data=train, x='Parch', hue='Survived', ax=ax2)
sns.countplot(data=train, x='FamilySize',hue='Survived', ax=ax3)

 

 

train, test 각 데이터의 SibSp, Parch 를 더하고 1을 더해서 'FamilySize' 라는 이름의 변수를 추가로 만들었다. 여기서 보면 알 수 있는 사실은, FamilySize 가 2 ~ 4 인 경우에는 생존률이 더 높았다는 사실을 알 수 있다. 핵가족의 전형적인 인원수이다. 이들은 빠르게 뭉쳐서 다같이 빠른 판단을 해서 생존률이 높은 것일까? 가설의 진실은 알 수 없지만, 이들 데이터가 가진 사실은 알 수 있다. 이들은 생존률이 높았다는 것이다. 

과연 다른 가족 규모에서는 어떤지 보자. 

train['Single']=train['FamilySize']==1
train['Nuclear']=(2<=train['FamilySize']) & (train['FamilySize']<=4)
train['Big']=train['FamilySize']>=5

test['Single']=test['FamilySize']==1
test['Nuclear']=(2<=test['FamilySize']) & (test['FamilySize']<=4)
test['Big']=test['FamilySize']>=5

figure, (ax1, ax2, ax3) = plt.subplots(nrows=1, ncols=3)
figure.set_size_inches(18,6)
sns.countplot(data=train, x='Single', hue='Survived', ax=ax1)
sns.countplot(data=train, x='Nuclear', hue='Survived', ax=ax2)
sns.countplot(data=train, x='Big',hue='Survived', ax=ax3) 

 

1인 여행자, 핵가족 여행자, 대가족 여행자에 대한 생존률 비교

 

False 보다는 True 를 유심히 봐야하는 데이터다. 왜냐하면 가족 유형은 저 셋중 하나로 무조건 들어가게 되어있으니까. 

코드를 설명하자면, train 과 test 데이터에서 FamilySize 가 1이면 'Single'로, 2~4 명이면  'Nuclear' 로, 그 이상이면 'Big' 으로 판단하고 각각 새로운 변수로 데이터에 추가했다.

확실히 다른 가족 형태보다 핵가족인 경우에만 생존률이 높았다는 사실을 확인할 수 있었다.

여기서 지금까지 원래 데이터셋에 Single, Nuclear, FamilySize, Big 등등을 만들어서 넣었는데, 이와 관련한 데이터들중에서 Nuclear 만 남기고 모두 지우려고 한다.

train=train.drop(columns=['Single','Big','SibSp','Parch','FamilySize'])
test=test.drop(columns=['Single','Big','SibSp','Parch','FamilySize'])

 

3.7. 선착장 (Embarked)

선착장에 따라서 생존률이 차이가 날까? 의구심이 들지만, 그래도 데이터를 보고 확인해야한다. 차이가 난다면 난다, 안난다면 안난다. 한번 그래프를 띄워보자.

sns.countplot(data=train, x='Embarked', hue='Survived')

 

선착장에 따른 생존 여부

 

얼라리? 차이가 있기는 하지만, 그 상황을 겪어보지 않아서 정확한 이유는 잘 모르겠다. 아무튼 우리가 내릴 수 있는 결론은 'C' 선착장에서 탄 승객들이 생존률이 높았다는 것이다. 이 Embarked 항목도, 머신러닝에게 좋은 정보가 될 것이다. 

train['EmbarkedC']=train['Embarked']=='C'
train['EmbarkedS']=train['Embarked']=='S'
train['EmbarkedQ']=train['Embarked']=='Q'
test['EmbarkedC']=test['Embarked']=='C'
test['EmbarkedS']=test['Embarked']=='S'
test['EmbarkedQ']=test['Embarked']=='Q'

train=train.drop(columns='Embarked')
test=test.drop(columns='Embarked')

위에서 했던 것처럼 원-핫 인코딩을 하고, 원래 데이터를 지워준다.  

 

3.8. 이름 (Name)

영어 이름에는 성별 정보가 포함되어 있다. 이는 이름을 가지고도 생존 여부를 어느정도 예측할 수 있다고 볼 수도 있다. 또한 한 가족은 한 성씨(Last Name)를 따르니까, 위에서 말한 FamilySize를 남겨놓고 (LastName + FamilySize) 를 만들어서 일종의 FamilyID 를 만들 수도 있을 것이다. 예를들면, Johnson5 이런식이다. Johnson 성을 가진 5명 일행. 아마 일행중 한명이 사망했다면, 다른 구성원들도 사망했을지도 모른다. 이런식의 가설 세우기도 충분히 가능하지만, 난 귀찮으니 안하겠다. 해보고싶으면 한번 해봐도 좋을 것이다. 절대 내가 귀찮아서 그런게 아니다. 

train['Name']=train['Name'].str.split(', ').str[1].str.split('. ').str[0]
test['Name']=test['Name'].str.split(', ').str[1].str.split('. ').str[0]

train, test 의 'Name' 에다 그들의 Mr., Mrs., 등의 호칭을 가져와서 저장한다. 이름을 자세히 보면, 

'이름', Mr. '성씨' 형태로 상당히 깔끔하게 정리되어있다. 위 코드는, 이 이름 데이터를 문자열로 받아서, 콤마와 온점 기준으로 가운데 있는 문자열을 가져오는 코드이다. 

sns.countplot(data=train, x='Name', hue='Survived')

 

호칭별 생존 여부

 

Mr 은 남자의 호칭이다. Ms, Mrs 는 여자의 호칭이다. 이는 위에서 보았던 성별 차이에 따른 생존률의 차이와 일맥상통한다. 하지만 여기서 특이한점은 'Master' 호칭의 사람들도 생존 숫자가 더 많았다는 것이다. 나중에 알게 되었는데, 이 'Master' 가 옛날 영어에서 어린 남자들을 지칭하는 호칭이란다. 카더라. 아무튼 우리가 알 수 있는 정보는, Master 호칭을 가진 남자들은 생존률이 더 높다는 것을 의미한다. 

train['Master']=(train['Name']=='Master')
test['Master']=(test['Name']=='Master')

train=train.drop(columns='Name')
test=test.drop(columns='Name')

train=train.drop(columns='Ticket')
test=test.drop(columns='Ticket')

train과 test 에서 'Master' 여부 True or False 를 구분하는 변수를 만들고, 나머지 이름 데이터를 삭제한다. 

여기서 굳이 Ms, Mrs 를 따로 같이 뽑아주지 않았냐고 물어보신다면, 이미 얘네들은 여자이기 때문에, 머신러닝 알고리즘은 얘네들을 살 가능성이 높다고 판단할 것이라고 생각했다. 하지만 이 머신러닝은 남자들은 대부분 죽었다고 판단할 것이다. 우리가 데이터 분석을 하면서 정확도를 올릴 수 있는 방법은, 머신러닝이 죽었다고 판단할 남자들 중 살았을 확률이 높은 부분, 살았다고 판단했을 여자들이 죽었을 확률이 높은 부분을 찾아서 변수로 추가해주어야 정확도를 높일 수 있다. 여기서 Ms, Mrs 를 추가한다는것은 아무 의미 없이 변수만 늘리는 행위일 뿐이다. 위에서도 언급했듯이, 변수가 많아지면 결과에 좋은 영향을 끼치지 않을 가능성이 꽤 높다. 

티켓은 티켓 번호를 의미한다. 그냥지우자. 귀찮다.

 

4. 머신러닝 모델 생성 및 학습

이제 머신러닝 모델을 만들고 학습시키고 데이터를 예측시키면 모든 과정이 끝이 난다. 

머신러닝 모델은 sklearn(scikit-learn) 의 DecisionTree를 사용할 것이다.

from sklearn.tree import DecisionTreeClassifier

그럼 이제 데이터를 머신러닝 알고리즘에 넣어줄 준비를 해야한다. 

Ytrain=train['Survived']
feature_names=list(test)
Xtrain=train[feature_names]
Xtest=test[feature_names]

print(Xtrain.shape, Ytrain.shape, Xtest.shape)
Xtrain.head()

우리가 맞춰야 할 '생존여부' 를 Ytrain 이라는 이름에 변수에 저장했고, 승객 정보를 담고있는 나머지 데이터 (train 데이터에서 Survived 를 제외한 나머지 열) 들을 Xtrain, Xtest 라는 변수에 담았다.

다시 말하지만, 이 Xtrain 안에 있는 승객 정보를 가지고 Ytrain (생존여부) 를 학습한다. 그리고 머신러닝이 본 적 없는 데이터인 Xtest 에 대해서도 Ypred(예상 생존여부) 를 예측하는 것이 우리의 목표이다. 

저 print 문이 실행된다면 아마 이렇게 출력될 것이다 :

(891, 10) (891,) (418, 10)

이때 확인해야 할 것은, Xtrain 과 Ytrain의 행 수가 일치하는지, Xtrain 과 Xtest 의 열 수가 일치하는지를 확인해야 한다. 이게 안맞으면 안돌아간다. 위에 데이터 분석 과정에서 무언가 빼먹었다는 뜻이다. '.head()' 라는 메소드는 해당 데이터프레임 (여기서는 Xtrain) 의 첫 5개 줄(행) 을 보여준다. 

 

데이터 분석하고 정리한 머신러닝에 들어갈 최종 결과물

 

model=DecisionTreeClassifier(max_depth=8, random_state=18)
# random_state is an arbitrary number.
model.fit(Xtrain, Ytrain)
predictions=model.predict(Xtest)
submission['Survived']=predictions
submission.to_csv('Result.csv')
submission.head()

model 은 머신러닝 모델인 DecisionTreeClassifier을 저장했다. 여기서는 깊이 8을 설정했다. 

model.fit(Xtrain, Ytrain) : Xtrain 으로 Ytrain 을 학습해라. 

model.predict(Xtest) : .fit() 이 끝난 이후에 실행할 수 있다. Xtest를 예측해라. 

그러면 이제 submission 파일의 'Survived' 에다가 예측한 결과를 넣어서 '.to_csv' 로 저장할 수 있다. 저장된 파일은 주피터 노트북 파일과 같은 경로에 저장된다. 

이걸 이제 캐글에 제출하면 나와 같은 결과를 얻을 것이다. 

 


 

 

 

 

그짓말 안하고 타이타닉만 200번은 제출했다. 타이타닉 이제 그만하고싶다.

반응형
Posted by Jamm_
반응형

VAE로 이미지의 특성을 뽑아서, 이미지에 변형을 가하며 새로운 이미지를 생성할 수 있다. 웃지 않는 사진에 웃음을 추가한다던지, 선글라스를 씌운다던지 할 수 있다는 것을 할 수 있다. 

또 다른 생성 모델은 'GAN'이다. Generative Adversarial Network, (생성적 적대 신경망)이라고 하는데, 처음 논문이 나왔을 때 아주 혁신적인 아이디어로 세간의 이목을 집중시켰다고 한다.

GAN은 생성자와 판별자, 두 개의 신경망으로 이루어진 신경망이다. 생성자와 판별자는 서로 경쟁하는 구도를 가지는데, 각각의 역할이 존재한다. 판별자는 이미지가 Input으로 들어오면 이 이미지가 원래 있던 이미지인지(target = 1), 아니면 새로 생성된 이미지인지 (target = 0)을 예측하는 역할을 한다. 생성자는 랜덤한 노이즈가 주어지면, 이를 이용해서 이미지를 생성하는 역할을 한다. 이렇게 생성된 이미지는 판별자에게 주어져서, 판별자는 이 이미지의 진위여부를 판단하게 된다. 이 두 모델은 서로 계속해서 학습하며 판별자는 더더욱 생성된 이미지를 잘 구분할 수 있게 되고, 생성자는 더더욱 판별자가 잘 구분할 수 없는 '진짜 같은' 이미지를 만들어내게 된다. 

지금 들어도 예측 모델링만 하던 내가 이 아이디어를 듣고 와 정말 신기하다. 여기서 이렇게 할 수도 있구나 라는 생각을 했었다. 이번에는 camel numpy 파일을 이용해 직접 GAN을 만들어보고, 새로운 데이터를 생성해보자. 

camel numpy 데이터는 구글의 'Quick, Draw!' 데이터셋의 일부이다. 이 데이터셋은 온라인 게임으로부터 수집되었는데, 유저가 그림을 그린 것을 신경망이 맞추는 게임이다. 이 결과물이 주제별로 묶어져 .npy 파일로 저장되어있다. 

http://bit.lly/30HyNqg 

여기서 다운로드 받을 수 있다. 

GAN의 원본 논문에서는 Convolution층 대신에 Fully-Connected 층을 사용했다고 한다. 하지만 그 이후로 합성곱 층이 판별자의 예측 성능을 크게 높여 준다고 밝혀진 이후로, 간단한 GAN 모델들도 합성곱 층을 사용하고 있다. 이러한 모델은 사실 DCGAN (Deep Convolution GAN)이라고 부르는 게 정확하지만, 합성곱 층이 너무나 많이 쓰이고 있어서 GAN이라고 해도 DCGAN의 의미를 포함하고 있다고 한다. 

 


 

1. 패키지 및 데이터 로드

import pandas as pd
import numpy as np
import keras
import keras.backend as K
from keras.layers import Conv2D, Activation, Dropout, Flatten, Dense, BatchNormalization, Reshape, UpSampling2D, Input
from keras.models import Model
from keras.optimizers import RMSprop
from keras.preprocessing.image import array_to_img

import warnings ; warnings.filterwarnings('ignore')

import matplotlib.pyplot as plt
from tqdm import tqdm

패키지를 로드하고, 

Xtrain = np.load('full-numpy_bitmap-camel.npy')
Xtrain = np.reshape(Xtrain, (len(Xtrain), 28, 28, 1))
Xtrain = Xtrain / 255

MNIST 데이터셋처럼 0~255의 픽셀 강도가 numpy로 저장되어있다. 

책과 다른 게시물에서는 -1 ~ 1로 값을 맞춰주는데, 그냥 귀찮아서 0 ~ 1 로 맞추어주었다. 그렇다면, 마지막 단계, 생성자의 이미지 출력물을 0~1 사이로 맞춰주는 sigmoid를 사용해야겠다. 

 


 

2. 판별자 만들기

disc_input = Input(shape=(28, 28, 1))

x = Conv2D(filters=64, kernel_size=5, strides=2, padding='same')(disc_input)
x = Activation('relu')(x)
x = Dropout(rate=0.4)(x)

x = Conv2D(filters = 64, kernel_size=5, strides=2, padding='same')(x)
x = Activation('relu')(x)
x = Dropout(rate=0.4)(x)

x = Conv2D(filters=128, kernel_size=5, strides=2, padding='same')(x)
x = Activation('relu')(x)
x = Dropout(rate=0.4)(x)

x = Conv2D(filters=128, kernel_size=5, strides=1, padding='same')(x)
x = Activation('relu')(x)
x = Dropout(rate=0.4)(x)

x = Flatten()(x)
disc_output = Dense(units=1, activation='sigmoid', kernel_initializer='he_normal')(x)

discriminator = Model(disc_input, disc_output)
discriminator.summary()

discriminator의 앞 4자 disc를 사용해서 판별자를 만들었다. 

판별자를 만드는 코드를 간단히 설명하자면, 단순히 4번의 Convolution을 거쳐서, 해당 이미지가 참인지 거짓인지만을 판별한다. 마지막의 sigmoid 함수를 지나서 이 이미지가 진짜일 확률을 출력하게 된다. 

판별자 모델 요약

 

3. 생성자 만들기

gen_dense_size=(7, 7, 64)

gen_input = Input(shape = (100, ))
x = Dense(units=np.prod(gen_dense_size))(gen_input)
x = BatchNormalization()(x)
x = Activation('relu')(x)
x = Reshape(gen_dense_size)(x)

x = UpSampling2D()(x)
x = Conv2D(filters=128, kernel_size=5, padding='same', strides=1)(x)
x = BatchNormalization(momentum=0.9)(x)
x = Activation('relu')(x)

x = UpSampling2D()(x)
x = Conv2D(filters = 64, kernel_size=5, padding='same', strides=1)(x)
x = BatchNormalization(momentum=0.9)(x)
x = Activation('relu')(x)

x = Conv2D(filters=64, kernel_size=5, padding='same', strides=1)(x)
x = BatchNormalization(momentum=0.9)(x)
x = Activation('relu')(x)

x = Conv2D(filters=1, kernel_size=5, padding='same', strides=1)(x)
gen_output = Activation('sigmoid')(x)

generator = Model(gen_input, gen_output)
generator.summary()

gen_dense_size에 shape를 하나 주었는데, 이 값은 랜덤 노이즈로부터 처음 생성할 Raw 이미지의 사이즈이다. 이 Raw 이미지는 몇 번의 UpSampling2D와 Convolution2D 레이어를 거쳐서 원본 이미지와 같은 shape를 가진 이미지로 바뀐다. 여기서 Conv2DTranspose (전치 합성곱) 층 대신에 UpSampling2D를 사용했는데, 이 둘은 픽셀을 다루는 원리가 약간씩 다르다. Conv2D 같은 경우에는 합성곱 연산을 진행하기 전에 픽셀 사이에 0을 추가하는 반면, UpSampling2D는 각 행렬을 단순히 반복하여 이미지 사이즈를 두배로 만든다. Conv2DTranspose를 사용하면 이미지에 계단 모양이나 체크무늬 패턴을 만들어서 이미지 품질을 저하시킬 수 있다고 알려져 있지만, 뛰어난 GAN 논문에서 여전히 많이 사용된다. 따라서 두 방식을 모두 시도해보고 더 좋은 결과를 출력하는 방법을 사용하는 것이 바람직하다. 

생성자의 코드를 확인해보자면, 100차원의 랜덤 노이즈를 7 * 7 * 64 사이즈의 이미지로 만들고, 이를 키우면서 진행된다. 이전 레이어에서 받은 이미지를 UpSampling 하여 두 배로 크기를 늘리고, Convolution을 진행하고, 다시 UpSampling 하고, Convolution 하고를 반복한다. 마지막에는 sigmoid 함수를 적용시켜서 출력 결과물이 원본 데이터와 같은 스케일 안에 있게끔 만든다. 내가 시작할 때 Xtrain 데이터를 0 ~ 1 사이의 값으로 만들었기 때문에 다른 코드들처럼 tanh를 사용하지 않고 sigmoid 함수를 사용하는 것이 맞다고 판단했다. 

생성자 모델 요약

 


 

4. 모델 컴파일

discriminator.compile(optimizer=RMSprop(lr=0.0008), loss='binary_crossentropy', metrics=['accuracy'])

판별자를 컴파일해준다. 이미지의 진위를 판단하는 Binary 문제이므로 loss function은 binary_crossentropy를 사용하고, metrics 에는 정확도를 사용하여 모니터링한다. 

 

discriminator.trainable = False
model_input = Input(shape=(100, ))
model_output = discriminator(generator(model_input))
model = Model(model_input, model_output) 

model.compile(optimizer=RMSprop(lr=0.0004), loss='binary_crossentropy', metrics=['accuracy'])

이제 생성자를 컴파일해줘야 하는데, 생성자에서 생성한 이미지를 판별자에 넣고 그 이미지의 참 거짓을 판별하도록 하는 모델을 만들어서 컴파일을 한다. 어차피 이렇게 모델을 새로 컴파일해도, 그 안에 있는 생성자도 같이 컴파일되므로, 추후에 생성자만 이용해서 이미지를 생성하는 것이 가능하다. 

일반적으로 판별자가 더 강력해야, 생성자가 판별자의 학습을 따라가면서 점점 좋은 결과물을 생산해내기 때문에, 판별자의 learning rate의 절반으로 생성자를 컴파일해준다. 

이때 헷갈리는 것 중 하나가, 'discriminator.trainable = False'인데, 이 코드가 있어야 생성자에서 생성된 데이터를 학습하는 과정에서 판별자의 가중치가 업데이트되지 않는다. 이 trainable 속성은 모델이 컴파일할 때 작동하는데, 설명하자면 아래와 같다. 

 

1. 판별자를 컴파일할 때는 trainable=True로 되어있는 상태로 컴파일 진행. 
2. 판별자를 컴파일한 이후 trainable=False로 설정해주어도 이미 컴파일된 판별자에는 적용되지 않음. 
3. 새로 판별자를 컴파일한 모델을 만드는데, 이때는 아까 trainable=False로 해두었기 때문에 판별자의 가중치가 업데이트되지 않음. 하지만 기존에 판별자만 컴파일할 때 trainable=True 이므로, 판별자만 학습을 진행할 때는 가중치가 업데이트됨.

정도로 이해하면 된다. trainable 속성이 컴파일 시에 적용되는 건지를 모르고 있다가 이제야 알게 되었다. 

요약하자면, 새로 만든 model 객체는, 100차원의 데이터를 받아서 생성자에 입력하고, 생성자가 출력한 이미지를 판별자에 넣어서 이 이미지의 진위여부를 판별자로 하여금 예측하도록 한다. 

 


 

5. GAN 모델 훈련

GAN은 다른 것보다 훈련하는 아이디어가 핵심이다. 

1. 원본 데이터에서 랜덤하게 데이터를 추출하여 판별자 학습. (target = 1)

2. 학습되지 않은 생성자가 랜덤 노이즈를 이용해서 생성한 이미지로 판별자 학습 (target = 0)

을 해주면서 판별자는 원본 이미지와 생성된 이미지의 차이를 구분하게 된다. 생성자가 잘 훈련이 되려면, 이 이미지가 판별자의 input으로 주어졌을 때, 판별자가 1을 뱉어내기 시작한다면 학습이 잘 진행되고 있다는 의미이다. 

def train_discriminator(x_train, batch_size):
    valid = np.ones((batch_size, 1))
    fake = np.zeros((batch_size, 1))
    
    idx = np.random.randint(0, len(Xtrain), batch_size)
    true_imgs = Xtrain[idx]
    discriminator.fit(true_imgs, valid, verbose=0)
    
    noise = np.random.normal(0, 1, (batch_size, 100))
    gen_imgs = generator.predict(noise)
    
    discriminator.fit(gen_imgs, fake, verbose=0)
    
def train_generator(batch_size):
    valid = np.ones((batch_size, 1))
    noise = np.random.normal(0, 1, (batch_size, 100))
    model.fit(noise, valid, verbose=1)

코드를 요약하자면, 판별자 학습 함수, 생성자 학습 함수를 만든다. 

판별자 학습 함수에서는, 원본 데이터에서 배치 사이즈만큼 랜덤하게 뽑아와서 타겟 1 (진짜)를 주고 학습을 시키고, 생성자가 만들어낸 이미지에다가는 타겟 0 (가짜)를 주고 학습을 시킨다. 생성자를 훈련할 때 이 생성자는 점점 진짜 같은 이미지를 생성하게 되므로 이렇게 학습을 반복해주면 판별자는 원본 이미지와 만들어진 이미지를 더욱 잘 구분할 수 있게 된다. 

학습은 다음과 같은 과정의 반복으로 이루어진다. 

1. 판별자는 원본 이미지를 가져와서 1로, 생성된 이미지를 0으로 학습한다. 

2. 생성자는 랜덤 노이즈로부터 이미지를 1이라고 지정하고 학습하며, 가중치를 업데이트한다. 

3. 판별자는 다시 원본 이미지와 (업데이트된) 생성자가 생성한 이미지를 가지고 다시 원본 이미지와 생성된 이미지를 각각 1, 0으로 학습한다. 

판별자는 원본과 생성 이미지를 정확히 구분하면 loss 가 낮아진다. 반면 생성자의 목표는 판별자의 loss를 높이는(원본과 생성이미지를 구분하지 못하도록)것이 목표이다. model 객체에서 표시되는 loss는, 생성된 이미지에 대한 판별자의 loss를 의미하므로, 학습이 잘 진행될수록 이 결과는 점점 높아질 것이다. 

배치 사이즈 64로, 2000 에포크만큼 학습을 진행해보았다. 

for epoch in tqdm(range(2000)):
    train_discriminator(Xtrain, 64)
    train_generator(64)

학습 초반
학습 중반
학습 후반

학습이 진행될수록 'model' 객체에 표시되는 loss 가 점차적으로 증가하고 있다. 다시 한번 요약하자면, 여기에 표시되는 loss는 새로 생성된 이미지에 대한 판별자의 loss이기 때문에, 점점 증가해야 한다. 

 


 

6. 결과물 확인

original=array_to_img(Xtrain[0])
plt.imshow(original, cmap='gray')

원본 이미지

random_noise=np.random.normal(0, 1, (1, 100))
gen_result=generator.predict(random_noise)
gen_img=array_to_img(gen_result[0])
plt.imshow(gen_img, cmap='gray')

생성된 이미지

생성자가 어느 정도 학습이 되었다. 랜덤한 노이즈를 입력받으며 출발해서, 원본과 상당히 비슷한 낙타 모양으로 만들어냈다. 

보고 있는 책에서는 6000 에포크만큼 학습을 진행하였는데, 나도 해봐야겠다. 

반응형
Posted by Jamm_
반응형

VAE(Variational AutoEncoder)와 AE(AutoEncoder)는 구조상으로는 비슷하지만 의미적인(?) 측면에서 약간의 차이가 있다. 일반적인 오토인코더는 입력된 데이터를 저차원으로 최대한 잘 압축하는 방법에 그 초점이 맞춰져 있다면, VAE는 입력 데이터를 저차원으로 잘 압축시키고, 그 특성에 따라 새로운 데이터를 잘 생성해 내는 것에 초점이 더 맞춰져 있다. 

AE의 문제라고 한다면, AE의 결과물은 잠재 공간의 한 포인트가 되므로, 잠재 공간이 연속적으로 만들어지지 않는다는 것이다. 만약 인코딩된 결과물 (2, -2)가 숫자 4의 이미지로 완벽하게 디코딩이 되더라도, 포인트 (2.1, -2.1) 이 4와 비슷한 이미지로 디코딩될 것이라는 보장은 그 어디에도 없다. 그럴 필요도 없고. 잠재 공간이 2차원이라면 이런 문제는 상당히 덜해지지만, 잠재 공간의 차원이 늘어날수록 이 문제는 더더욱 심해지게 된다. 

VAE는 여기서 출발하는데, AE와 가장 직접적인 차이점은 VAE는 이미지를 잠재 공간의 한 점으로 직접 매핑시키는 것이 아니라, (그 포인트의 주변까지도 약간씩 포함 할 수 있도록) 그 점 주변의 다변수 정규 분포 함수에 매핑한다는 것이다. 이렇게 되면 인코딩 결과물 (2,-2)가 숫자 4의 이미지로 정확히 디코딩될 수 있다면, (2.1, -2.1) 역시 점 (2, -2)와 상당히 비슷한 위치에 있기 때문에 숫자 4와 상당히 비슷한 디코딩 결과를 낸다고 추론할 수 있다. 

요약하자면, VAE의 인코더는 입력된 이미지를 잠재 공간의 (한 포인트를 중심으로 하는) 다변수 정규 분포로 만들게 된다. 이미지 1을 일반적인 오토인코더로 인코딩한 결과가 (1, 1)이라면, VAE는 이 이미지 1을 (1, 1)을 중심으로 하는 (다변수) 정규 분포 함수를 만든다. 따라서 디코더는 이 (1, 1) 근처의 점들은 정규분포에 따라서 이미지 1 일 '확률' 이 높다고 판단하고, 이미지 1과 비슷한 형태를 띠도록 디코딩을 학습할 가능성이 높다. 하지만 점 (1, 1)과 멀리 떨어진 점 (100, 100)에 대해서는 정규분포에 따라서 이미지 1일 '확률' 이 높지 않다고 판단하고, 디코딩 결과물이 이미지 1과 비슷해지지 않도록 학습할 것이다. 

 

정규 분포를 정의하려면 두가지의 변수가 필요한데, 바로 평균과 분산이다. 따라서, 인코더는 입력된 이미지에 대하여, 평균과 분산, 두 가지 아웃풋을 낸다. 여기에다가 'epsilon' 항을 추가해서 정규 분포를 만드는데, epsilon 은 표준 정규분포에서 랜덤하게 샘플링 한 값이다. 이 epsilon의 역할은 '인코더가 만들어낸 다차원 정규분포에서 얼마나 떨어진 곳을 디코딩 시작 포인트로 잡을까'를 결정한다. 이 값이 결정되면 디코더는 인코더의 정확한 결과물 (인코더가 만들어낸 정규분포의 중심점) 이 아닌, 그 포인트 주변의 약간 떨어진 점으로부터 디코딩을 시작해야 하므로 한 번도 본 적 없는 포인트에 대해서 디코딩을 진행해야 한다. 하지만 그 시작점이 인코더의 정확한 결과물과 아주 크게 다르지는 않기 때문에 (epsilon도 정규분포를 따르므로, epsilon 값이 비정상적으로 큰 값이 나올 확률 매우 적다) 원본과 비슷한 결과물을 내야만 Loss 가 커지지 않을 것이다. 

책을 천천히 다시 읽어보고 이해한 결과를 정리했다. 머릿속으로 3차원을 넘어가는 그림이 그려지지는 않아서, (그게 됐다면 난 이런거 쓰고 있지 않았겠지?) 그리고 고등학교 수포자 이과생인 전력이 있는 나이기 때문에 정확한 수학적 이해나, 확률론(?)적으로 깔끔하게 설명할 수는 없지만, 굳이 이해를 하자면 이렇게 될 것이다. 

VAE에서는 잠재 공간의 차원 사이에는 어떤 상관관계가 없다고 가정하고 시작한다. 다시 말하면, 잠재 공간에서의 차원간의 상관계수는 0이고, 공분산 행렬은 계산할 필요가 없으며, 그러므로 인코더가 입력을 평균, 분산으로만 매핑할 수 있게끔 가정을 한다. 

VAE를 만드는 일은 생각보다 간단한데, 일반적인 오토인코더를 만든 후, 인코더가 정규분포를 만들 수 있도록 수정, 손실 함수가 정규분포를 인식할 수 있도록 수정해주면 된다. 단어가 조금 이상하다. 

대충 코드를 보자면 다음과 같다. 

 


 

1. VAE 만들기

import pandas as pd
import numpy as np
import keras
from keras.layers import Input, Conv2D, Flatten, Dense, Conv2DTranspose, Reshape, Activation, LeakyReLU, Dropout, BatchNormalization, MaxPooling2D, Lambda
from keras.models import Model
from keras import backend as K
from keras.optimizers import Adam
from keras.utils import to_categorical
from sklearn.metrics import mean_squared_error as mse

import matplotlib.pyplot as plt
%matplotlib inline
import seaborn as sns

패키지 import 하고, 

(Xtrain, Ytrain), (Xtest, Ytest) = keras.datasets.mnist.load_data()
Xtrain=Xtrain/255
Xtest=Xtest/255

Xtrain=Xtrain.reshape(len(Xtrain), 28, 28, 1)
Xtest=Xtest.reshape(len(Xtest), 28, 28, 1)

Ytrain=to_categorical(Ytrain)
Ytest=to_categorical(Ytest)

데이터 불러와서 저장, 전처리

 

1.1. 인코더 만들기

#Encoder
encoder_input=Input(shape=(28,28,1))

x=Conv2D(filters=32, kernel_size=3, strides=1, padding='same')(encoder_input)
x=LeakyReLU()(x)

x=Conv2D(filters=64, kernel_size=3, strides=2, padding='same')(x)
x=LeakyReLU()(x)

x=Conv2D(filters=64, kernel_size=3, strides=2, padding='same')(x)
x=LeakyReLU()(x)

x=Conv2D(filters=64, kernel_size=3, strides=1, padding='same')(x)
x=LeakyReLU()(x)

shape_before_flatten=K.int_shape(x)[1:]
x=Flatten()(x)

#############################################################################
mu=Dense(units=2)(x)
log_var=Dense(units=2)(x)

def sampling(args):
    mu, log_var = args
    epsilon = K.random_normal(shape=K.shape(mu), mean=0, stddev=1)
    return mu + K.exp(log_var / 2) * epsilon

encoder_output=Lambda(sampling)([mu, log_var])
model_encoder = Model(encoder_input, encoder_output)
############################################################################
print(model_encoder.summary())

코드를 자세히 뜯어보면 윗부분은 똑같다. 복사해서 갖다썼으니까 똑같다.

다만 아랫부분 ##### 으로 표시한 부분이 약간 다른데, (이래서 함수형 API 쓰라고 하는구나) 몇 번의 Convolution Layer를 지난 데이터는 Flatten 이후에 두 경로로 가게 된다. 

한쪽은 mu(뮤, 평균)를  정하고, 다른 한쪽은 log_var(로그 분산)을 정한다. (분산은 항상 양수이므로 로그를 씌워도 괜찮다)

아래 공식으로 표준과 표준편차가 정의된 정규분포에서 특정 포인트를 샘플링할 수 있다. (표준편차는 분산의 양의 제곱근이므로 분산도 오케이다.) 공식의 epsilon은 표준 정규분포(평균=0, 표준편차=1 인 정규분포)를 따르는 임의의 점이다. 

코드의 Sampling 함수에 대한 설명

그리고 그 아래에 있는 'sampling' 함수는 위에서 만들어진 평균과, (로그) 분산으로 정의된 정규 분포(분포 A라고 하자)에서 epsilon값을 추가해 분포 A에서 한 포인트를 샘플링한다. 

인코더의 아웃풋 층은 뮤, 로그분산 두 가지를 입력으로 묶어서 입력받은 후, 샘플링 함수를 실행해서 샘플링된 포인트를 반환한다.

인코더를 이렇게 변경해주고, 디코더를 똑같이 연결해주면 VAE 설계의 첫 번째 단계가 끝난다. 

#Decoder
decoder_input=Input(shape=(2,))
x=Dense(units=np.prod(shape_before_flatten))(decoder_input)
x=Reshape(shape_before_flatten)(x)

x=Conv2DTranspose(filters=64, kernel_size=3, strides=1, padding='same')(x)
x=LeakyReLU()(x)

x=Conv2DTranspose(filters=64, kernel_size=3, strides=2, padding='same')(x)
x=LeakyReLU()(x)

x=Conv2DTranspose(filters=32, kernel_size=3, strides=2, padding='same')(x)
x=LeakyReLU()(x)

x=Conv2DTranspose(filters=1, kernel_size=3, strides=1, padding='same')(x)
x=Activation('sigmoid')(x)

decoder_output=x
model_decoder=Model(decoder_input, decoder_output)
print(model_decoder.summary())


#Connect Two Models
model_input = encoder_input
model_output = model_decoder(encoder_output)

AutoEncoder=Model(model_input, model_output)

 

1.2. 손실 함수 수정

인코더가 정규분포(에서 샘플링한 한 포인트) 를 내뱉도록 설계가 완료되었으면, 이제 손실 함수를 수정하면 된다. 기존의 AE에서는 픽셀 별로 값을 비교해서 구한 mean squared error 만 사용했다면, 여기서는 KL 발산(Kullback - Leibler Divergence, 쿨백 - 라이블러 발산)을 추가로 사용한다. 

KL발산에 대한 설명을 간단히 요약하자면, 한 확률분포가 다른 확률분포와 얼마나 다른지를 측정하는 방법이라고 할 수 있다. VAE가 생성해낸 평균 mu, 분산 log_var인 정규분포가 표준 정규분포와 얼마나 다른지를 측정해야 하기 때문에, 손실 함수(loss_function) 에 KL발산 항을 추가한다. 이때 KL 발산을 계산하려면 다음과 같이 하면 된다. 

kl_loss = -0.5 * sum( 1 + log_var - mu^2 - exp(log_var)

모든 차원에서 잠재공간상의 정규분포가 mu = 0, log_var = 0 일 때 kl_loss는 최소가 된다. 이렇게 추가한 KL 발산 항은 네트워크가 샘플을 표준 정규분포에서 크게 벗어나게 인코딩할 때 큰 페널티를 가하게 된다. 

optimizer=Adam(lr=0.0005)
r_loss_factor=1000   # This is a Hyperparameter

def vae_r_loss(y_true, y_pred):    ## MSE
    r_loss = K.mean(K.square(y_true-y_pred), axis=[1,2,3])
    return r_loss_factor * r_loss

def vae_kl_loss(y_true, y_pred):   ## KL-Divergence
    kl_loss= -0.5 * K.sum(1+log_var - K.square(mu) - K.exp(log_var), axis=1)
    return kl_loss

def vae_loss(y_true, y_pred): 
    r_loss=vae_r_loss(y_true, y_pred) #Loss of Decoder
    kl_loss = vae_kl_loss(y_true, y_pred) #Loss of Encoder
    return r_loss + kl_loss #Sum of these two


AutoEncoder.compile(optimizer=optimizer, loss= vae_loss, metrics=[vae_r_loss, vae_kl_loss])

새로 만들 VAE에서의 loss function은 여기서 정의한 'vae_loss' 함수를 이용한다. vae_loss 는 기존의 MSE 손실 함수와 KL 발산의 합으로 계산된다. MSE 손실 함수는 (원본 이미지 픽셀과 생성 이미지 픽셀을 비교하여) 디코더가 원본과 비슷한 이미지를 생성해내지 못할 시 더 큰 페널티를 주고, KL발산은 인코더가 표준 정규분포와 많이 다른 분포로 데이터를 인코딩할수록 더 큰 페널티를 주게 된다. 

여기서는 또 'r_loss_factor' 이라는 것이 추가되었는데, 이는 mse에 곱해주는 가중치로 KL 발산 손실과 균형을 맞추는 용도이다. 이 'r_loss_factor'는 하이퍼파라미터로, VAE를 훈련할 때 튜닝을 잘해주어야 한다. 이 가중치가 너무 크면, KL발산(인코더의 Loss)이 역할을 제대로 못하게 되고, 너무 작으면 (디코더의 Loss에 대한 비중이 적어지기때문에) 생성된 이미지의 퀄리티가 좋지 않을 것이다. 

2. VAE 학습시키기

for i in range(10):
    print(f"Trial {i}...")
    AutoEncoder.fit(Xtrain, Xtrain, batch_size=32, shuffle=True, epochs=20)
    result=AutoEncoder.predict(Xtrain)
    
    random=np.random.randint(0, len(Xtrain))
    
    fig = plt.figure()
    rows = 1
    cols = 2
    img1 = Xtrain[random].reshape(28,28)
    img2 = result[random].reshape(28,28)
    ax1 = fig.add_subplot(rows, cols, 1)
    ax1.imshow(img1)
    ax1.set_title('Correct')
    ax1.axis("off")
    ax2 = fig.add_subplot(rows, cols, 2)
    ax2.imshow(img2)
    ax2.set_title('Generated')
    ax2.axis("off")
    plt.show()

그래프를 띄우는 코드를 똑같이 복사해서 가져왔다. 20에포크당 생성 이미지를 한 번씩 띄우는 과정을 10회 반복하여 총 200 에포크만큼의 반복 학습을 진행한다. 

 


 

다음 장으로 넘어가기 전에 이 r_loss_factor의 값을 조금씩 만져보며 수정해보고, 손실이 수렴해가는 과정을 좀 지켜보고, 결과물도 이미지로 꺼내서 한번 봐야겠다. 이 다음에는 VAE로 MNIST 데이터 말고, 얼굴 이미지를 생성하는 것을 해봐야겠다. 

반응형
Posted by Jamm_
반응형

세상에서 가장 간단한 오토인코더를 만들었었다. 이 오토인코더에 몇 개의 레이어를 추가해 성능을 조금 더 향상시켜보자. 

원래 코드를 맥북에서 작성하는게 더 편해서, 간단한 딥러닝 모델 정도는 맥북으로 그냥 돌리고 마는데, 여기부터는 컴퓨터가 힘들어하기 시작해서 게이밍 노트북으로 옮겨서 실행했다. 역시 GPU는 짱짱이다. 

그냥 뭐 설명을 적어놓을건 딱히 더 없으니 코드만 대충 약간 올려놔야겠다. 

 


 

 

import pandas as pd
import numpy as np
import keras
from keras.layers import Input, Conv2D, Flatten, Dense, Conv2DTranspose, Reshape, Activation, LeakyReLU, Dropout, BatchNormalization, MaxPooling2D, Lambda
from keras.models import Model
from keras import backend as K
from keras.optimizers import Adam
from keras.utils import to_categorical
from sklearn.metrics import mean_squared_error as mse

import matplotlib.pyplot as plt
%matplotlib inline
import seaborn as sns
(Xtrain, Ytrain), (Xtest, Ytest) = keras.datasets.mnist.load_data()
Xtrain=Xtrain/255
Xtest=Xtest/255

Xtrain=Xtrain.reshape(len(Xtrain), 28, 28, 1)
Xtest=Xtest.reshape(len(Xtest), 28, 28, 1)

Ytrain=to_categorical(Ytrain)
Ytest=to_categorical(Ytest)
#Encoder
encoder_input=Input(shape=(28,28,1))

x=Conv2D(filters=32, kernel_size=3, strides=1, padding='same')(encoder_input)
x=LeakyReLU()(x)

x=Conv2D(filters=64, kernel_size=3, strides=2, padding='same')(x)
x=LeakyReLU()(x)

x=Conv2D(filters=64, kernel_size=3, strides=2, padding='same')(x)
x=LeakyReLU()(x)

x=Conv2D(filters=64, kernel_size=3, strides=1, padding='same')(x)
x=LeakyReLU()(x)

shape_before_flatten=K.int_shape(x)[1:]
x=Flatten()(x)
encoder_output=Dense(units=2)(x)
model_encoder=Model(encoder_input, encoder_output)
print(model_encoder.summary())

#Decoder
decoder_input=Input(shape=(2,))
x=Dense(units=np.prod(shape_before_flatten))(decoder_input)
x=Reshape(shape_before_flatten)(x)

x=Conv2DTranspose(filters=64, kernel_size=3, strides=1, padding='same')(x)
x=LeakyReLU()(x)

x=Conv2DTranspose(filters=64, kernel_size=3, strides=2, padding='same')(x)
x=LeakyReLU()(x)

x=Conv2DTranspose(filters=32, kernel_size=3, strides=2, padding='same')(x)
x=LeakyReLU()(x)

x=Conv2DTranspose(filters=1, kernel_size=3, strides=1, padding='same')(x)
x=Activation('sigmoid')(x)

decoder_output=x
model_decoder=Model(decoder_input, decoder_output)
print(model_decoder.summary())


#Connect Two Models
model_input = encoder_input
model_output = model_decoder(encoder_output)

AutoEncoder=Model(model_input, model_output)

인코더 모델과 디코더 모델의 .summary() 메소드 결과물

인코더의 Input shape와, 디코더의 Output shape 가 같으므로 돌아갈 수 있는 모델이다. 

optimizer=Adam(lr=0.0005)
AutoEncoder.compile(optimizer=optimizer, loss='mean_squared_error')


for i in range(10):
    print(f"Trial {i}...")
    AutoEncoder.fit(Xtrain, Xtrain, batch_size=32, shuffle=True, epochs=20)
    result=AutoEncoder.predict(Xtrain)
    
    random=np.random.randint(0, len(Xtrain))
    
    fig = plt.figure()
    rows = 1
    cols = 2
    img1 = Xtrain[random].reshape(28,28)
    img2 = result[random].reshape(28,28)
    ax1 = fig.add_subplot(rows, cols, 1)
    ax1.imshow(img1)
    ax1.set_title('Correct')
    ax1.axis("off")
    ax2 = fig.add_subplot(rows, cols, 2)
    ax2.imshow(img2)
    ax2.set_title('Generated')
    ax2.axis("off")
    plt.show()

오토인코더 학습을 반복문으로 10회 반복 학습시키고, 학습당 20 에포크를 준다. 시간 여유가 좀 있으니까 (+GPU를 사용하면 빠르니까) 총 200 에포크만큼 반복 학습을 시킨다. 이때 20 에포크당 Xtrain에서 랜덤하게 데이터 1개를 가져와서 결과물 이미지랑 비교한다. 

얼추 그럴싸해보이는 이미지 복원 결과물
5 모양이 학습이 힘든가보다. 여전히 3이랑 더 가까워보인다. 
1처럼 단순한 형태는 쉽게 학습하는듯

총 200번의 에포크를 성공적으로 학습했다. 역시 딥러닝에는 GPU를 써야 정신건강에 도움이 되는 것 같다. 이제 VAE에 대해서 좀 더 알아봐야겠다. 

반응형
Posted by Jamm_
반응형

오토인코더는 '인코더'와 '디코더' 두 개의 모델로 이루어진 신경망이다. 이 둘의 역할은

인코더 : 고차원의 입력 데이터를 저차원의 표현 벡터로 압축.

디코더 : 주어진 표현 벡터를 고차원의 데이터로 압축 해제.

와 같은 구조를 가진 신경망이다. 

예를 들면, 인코더는 숫자 2 라는 이미지를 받아서 이를 분석한 후 2차원 좌표평면에 있는 점 (1, 1)에 점을 찍는다. 숫자 2 이미지는 위에서 설명한 고차원 데이터이고, 결과물 표현 벡터는 점 (1, 1)에 해당한다. 이때 2차원 좌표평면을 잠재 공간(latent space)라고 한다. 디코더는 이제 (1, 1)이라는 점을 보고 숫자 2의 이미지를 생성해내는 신경망이다. 따라서 디코더는 잠재 공간의 어떤 점의 좌표가 주어진다면, 그 점에 해당하는 이미지를 생성할 수 있다. 예를 들어 (9, 8)이라는 좌표를 보고 숫자 7의 이미지를 생성하는 식이다. 오토인코더는 이러한 일련의 과정을 학습하며, 점차 이미지의 특성을 잡아내면서, 이미지를 재생성할 수 있는 능력을 갖게 된다. 인코더는 더욱 효과적으로 표현 벡터를 나타내는 방법을, 디코더는 표현 벡터로부터 이미지의 특징을 더해 원본과 비슷한 이미지를 생성하는 과정을 학습하게 된다. 

 


 

1. 가장 간단한 오토인코더 만들기

1.1. 패키지 import 

import pandas as pd
import numpy as np
import keras
from keras.layers import Input, Conv2D, Flatten, Dense, Conv2DTranspose, Reshape, Activation, LeakyReLU, Dropout, BatchNormalization, MaxPooling2D
from keras.models import Model
from keras import backend as K
from keras.optimizers import Adam
from keras.utils import to_categorical
from sklearn.metrics import mean_squared_error as mse

import matplotlib.pyplot as plt
%matplotlib inline
import seaborn as sns

패키지들을 불러오자. 한번에 싹 불러오면 편할 것이다. 

1.2. 데이터 불러오기

(Xtrain, Ytrain), (Xtest, Ytest) = keras.datasets.mnist.load_data()
Xtrain=Xtrain/255
Xtest=Xtest/255

Xtrain=Xtrain.reshape(len(Xtrain), 28, 28, 1)
Xtest=Xtest.reshape(len(Xtest), 28, 28, 1)

Ytrain=to_categorical(Ytrain)
Ytest=to_categorical(Ytest)

이번에는 손글씨로 이루어진 숫자 이미지 데이터셋 (MNIST) 를 사용한다. 이 데이터셋은 28 * 28 사이즈의 손글씨로 적힌 숫자를 포함하는 이미지이고, 데이터는 0 ~ 255의 픽셀 강도로 이루어져 있다. 일반적으로 신경망은 -1 ~ 1 사이의 값에서 가장 잘 동작하므로 255로 데이터를 나누어 주면 입력 데이터를 0 ~ 1로 맞춰줄 수 있다. 

1.3. 인코더 모델

encoder_input=Input(shape=(28,28,1))

x=Conv2D(filters=4, kernel_size=3, strides=1, padding='same')(encoder_input)
x=BatchNormalization()(x)
x=LeakyReLU()(x)
shape_before_flatten=K.int_shape(x)[1:]
x=Flatten()(x)
encoder_output=Dense(units=2)(x)

model_encoder=Model(encoder_input, encoder_output)
model_encoder.summary()

model_encoder.summary() 의 결과물

입력으로 28 * 28 * 1(흑백 1채널) 의 이미지를 받아, 한 번의 Convolution, 배치 정규화, LeakyReLU 활성화 함수, 2차원 잠재 공간으로 매핑하는 신경망이다. 매우 간단하다. 

1.4. 디코더 모델

decoder_input=Input(shape=(2,))

x=Dense(units=np.prod(shape_before_flatten))(decoder_input)
x=Reshape(shape_before_flatten)(x)
x=LeakyReLU()(x)
x=BatchNormalization()(x)
x=Conv2DTranspose(filters=1, kernel_size=3, strides=1, padding='same')(x)
x=Activation('sigmoid')(x)
decoder_output=x

model_decoder=Model(decoder_input, decoder_output)
model_decoder.summary()

model_decoder.summary() 의 결과물

디코더 모델은 의외로 간단하게 만들 수 있다. 인코더 모델의 layer 들을 역순으로 해주면 쉽게 만들 수 있다. 

이때 꼭 인코더 모델과 대칭(?)을 이루도록 만들지 않아도 된다는 점이 포인트이다. 인코더에서는 Convolution을 두 번 거쳤지만, Decoder에서는 한 번만 해도 된다는 뜻이다. 인코더에 입력으로 들어오는 데이터와 같은 차원, 같은 크기이기만 하면 어떤 구조를 가져도 상관없다. 

여기서는 또 특이한 점 두가지 중 첫 번째는, Conv2DTranspose 가 들어가 있다. 이것은 일반적인 Conv2D의 역연산이라고 생각하면 쉽다. (Convolution 자체의 수학적 원리를 아직 모르기 때문에... 물어보지 마세요 엉엉)

두 번째는 마지막에 오는 활성화 함수인데, 'Sigmoid' (시그모이드 함수) 라 불린다. Logistic Regression에서 주로 봤었던 Sigmoid... 여기서는 Conv2 DTranspose의 결과물을 0 ~ 1 사이의 값으로 만들어주는 역할을 한다. 그럼 입력 데이터와 값의 스케일이 맞겠다. 단지 그뿐이다. 

1.5. 인코더, 디코더 연결하기

입력 이미지는 인코더를 거쳐 -> 잠재 공간의 한 벡터로 변환될 것이고 -> 이 벡터는 디코더를 거쳐 -> 다시 원본 이미지를 만들어내게끔 학습할 것이다. 이제, 인코더와 디코더를 연결만 해주면 되는데, 케라스에서는 이 과정은 그냥 껌이다. 

model_input = encoder_input
model_output = model_decoder(encoder_output)

AutoEncoder=Model(model_input, model_output)

 

끝. 

이렇게 만든 'AutoEncoder' 모델을 한번에 컴파일해도, 'model_encoder' 와 'model_decoder' 를 각각 사용할 수 있다. 

 


 

2. 가장 간단한 오토인코더 학습 & 새로운 이미지 생성

이제 마찬가지로 모델을 컴파일하고, 학습을 시키면 된다.

#Compile AE Model
optimizer=Adam(lr=0.001)
AutoEncoder.compile(optimizer=optimizer, loss='mean_squared_error')

#Fit AE Model
AutoEncoder.fit(Xtrain, Xtrain, batch_size=10, shuffle=True, epochs=1)

시간이 없으니, 1회만 학습시켜보자.

여기서 내가 처음에 실수했던 게, 타겟 변수에다가 자기 자신을 넣지 않고, 원래 타겟 변수를 넣었다. 자기 자신의 이미지와, 새로 생성한 자기 자신의 이미지를 비교하는 것이기 때문에, 입력 : Xtrain, 타겟 : Xtrain 이 맞다. loss 하이퍼파라미터의 'mean_squared_error'는 각각의 픽셀에 대하여 오차를 계산한다. 생성된 이미지가 원본과 비슷한 숫자라면, 비슷한 위치의 값이 같이 올라갈 것이다. 

result=AutoEncoder.predict(Xtrain)
print(result[0].shape, Xtrain[0].shape)

생성된 결과물과 원본의 shape를 확인해보면 둘다 (28, 28, 1) 임을 확인할 수 있다. 입력과 생성값의 차원이 같으므로 일단 '세상에서 가장 단순한 오토 인코더'를 성공적으로 만들었다!

그럼 이미지를 띄워볼까?

fig = plt.figure()
rows = 1
cols = 2

img1 = Xtrain[0].reshape(28,28)
img2 = result[0].reshape(28,28)
 
ax1 = fig.add_subplot(rows, cols, 1)
ax1.imshow(img1)
ax1.set_title('Correct')
ax1.axis("off")
 
ax2 = fig.add_subplot(rows, cols, 2)
ax2.imshow(img2)
ax2.set_title('Generated')
ax2.axis("off")
plt.show()

위 코드를 실행하면 Xtrain(원본) 과, result(생성 이미지)의 첫 번째 값을 이미지 파일로 볼 수 있다. 

대충 확인해보자면

원본 이미지와 새로 생성된 이미지 비교

원본은 딱 봐도 5이지만, 새로 생성한 이미지는 대충 보면 3같이 보인다. 억지로 보면 5로 이어서 볼 수는 있지만, 딱 봐도 5처럼 보여야 진짜 잘 생성한 이미지이니... 썩 좋은 결과는 아니지만, 그래도 모델이 돌아갔다는 것에 의의를 두자. Convolution과, 전결합층을 적절히 쌓고, 에포크를 충분히 주고 학습시키면 더 좋은 결과물을 얻을 수 있을지도 모른다. 

반응형
Posted by Jamm_
2020. 2. 25. 15:07

보호되어 있는 글입니다.
내용을 보시려면 비밀번호를 입력하세요.

반응형

최근에 책을 구입한 게 이 책이다. http://www.yes24.com/Product/Goods/81538614http://www.yes24.com/Product/Goods/81538614

 

미술관에 GAN 딥러닝 실전 프로젝트

창조에 다가서는 GAN의 4가지 생성 프로젝트 이 책은 케라스를 사용한 딥러닝 기초부터 AI 분야 최신 알고리즘까지 설명한다. 기계 스스로 그림을 그리고, 글을 쓰고, 음악을 작곡하고, 게임을 하는 딥러닝 생성 모델을 재현하는 과정에서 독자는 변이형 오토인코더(VAE), 생성적 적대 신경망(GAN), 인코더-디코더 모델, 월드 모델 등을...

www.yes24.com

처음에는 텐서플로우와 케라스가 뭔지도 몰랐다. "아니, 텐서플로우는 텐서플로우고, 케라스는 그럼 뭡니까? 왜 텐서플로우랑 같이 언급이 되는 거죠?"라고 묻던 게 불과 몇 주 전의 일이었다. 처음 딥러닝을 해봐야겠다 생각이 들었을 때는 텐서플로우 버전에 대한 개념이 없어서, 텐서플로우 2.0 버전을 깔고, 'tf.placeholder' 같은 코드를 따라 치다가 실행이 안 되기 시작하면서 때려치웠던 기억이 있다. (tf.placeholder... 코드는 TF 1.x 버전에서 주로 쓰던 코드라고 들었다. 지금은 쓰이지 않는다고...) 그래서 파이토치 코드를 따라 쳤는데, MNIST에서 돌아가는 데 성공했다. 단순히 실행에 성공했다는 이유로, 파이토치를 사용했었는데, 케라스를 조금 익히니까, 이거, 물건이다. 정말 간편하다. 케라스를 익힌 이유는, 이 책에서 예제 코드가 모두 케라스로 작성되어 있다... (만약 파이토치로 작성되어 있었다면, 케라스 같은 건 모르고 있었겠지.)

 


 

아무튼, 목표는 작곡하는 인공지능 만들기, 강화학습으로 핸드폰 게임 클리어하기이다.  나는 음악도 매우 좋아하는데, 신은 불행하게도 나에게 '보통 사람들보다 약간 좋은 음악적 감' 은 주셨지만, '작곡을 하며 음악을 즐길 수 있을 정도의 능력' 은 주시지 않으셨다. 대신 '코드를 읽고 쓰고 배우고 작성할 수 있는 능력' 은 주셨으니, 난 이걸로 음악을 즐겨야겠다.

반응형
Posted by Jamm_