반응형

1년만에 큰 대회에서 한번 더 수상을 하게 되었습니다. 국가정보원, 국가보안기술연구소에서 주최하고, Dacon 에서 운영한 HAICon2021, 산업제어시스템 보안위협 탐지 AI 경진대회에서 4위와 특별상을 수상하게 되었습니다. 

4위!

데이콘 대회 바로가기:

 

HAICon2021 산업제어시스템 보안위협 탐지 AI 경진대회 - DACON

좋아요는 1분 내에 한 번만 클릭 할 수 있습니다.

dacon.io

 

시드 고정과 Threshold 설정하는 부분의 코드가 좀 부족해서 점수 재현이 잘 안되지 않을까 걱정되어 잠을 잘 못잤는데, 다행히 잘 봐주신 것 같습니다. 시상식을 갔다 왔는데, "4등에 특별상까지 타고, 정말 축하한다. 설명 문서 제출하는것도 제일 열심히 쓴 것 같고 정말 열심히 논문도 많이 본 것 같고, 관련 전공자도 아닌 뜬금없는 설비, 소방 학과 학생이 이렇게까지 해줘서 놀랐다. 열심히 참가해줘서 고맙다." 라는 엄청난 칭찬을 들었습니다. 몸둘 바를 모르겠습니다. 제가 더 감사합니다...

이번 학기는 학교에서 전공 과목들을 15주(16주) 수업을 12주로 압축하여 수업을 진행하는 3학년 2학기였는데요, 본전공 과목들, 복수전공 과목들, 졸업 필수 교양 과목들이 15주, 12주 수업이 섞여버렸고, 매주 나오는 과제, 팀플 일정과 중간고사 일정이 모두 겹쳐버리는 등의.... 힘든 학기였습니다. 공부하다 힘들어서 코피를 흘린다는게 다 거짓말인 줄 알았는데, 이번 학기 12주 수업이 진행되는 와중에만 코피를 7번을 흘렸네요. (세수하고보니 주르륵. 주르륵. 주르륵.) 그런 와중에 같이 진행하던 대회였어서 정말 힘들었습니다. 

하지만 12주 수업을 했던 과목들의 성적도 만족스럽게 나왔고, 큰 대회에서 좋은 성적을 거뒀고, 상을 한개도 아니고 두개! 나 타서 정말 좋습니다. 4위 수상으로 상금을 받고, 특별상으로는 M1 아이패드 프로(11인치) 를 받았는데, 아이패드 8세대를 쓰고 있던 저에게는 정말 만족스러웠습니다. True Tone 최고... ProMotion 최고... M1 짱짱...

 


 

1. 데이터 소개

 

HAICon2021 산업제어시스템 보안위협 탐지 AI 경진대회 - DACON

좋아요는 1분 내에 한 번만 클릭 할 수 있습니다.

dacon.io

데이터는 여러 파일로 나누어져 있지만 크게 보아 총 3개입니다. 학습용, 검증용, 그리고 테스트, 3개입니다. 데이터는 여러 가지의 센서, 액추에이터의 값인데, 이러한 센서값들을 이용해서, 해당 시스템이 공격을 받는지, 이상이 발생하였는지를 맞춰야 하는 문제입니다. 하지만 무턱대고 진행하면 되는 것이 아니고, 학습 데이터에는 '이상 여부' 가 없이 모두 '정상 상태'의 데이터셋만 존재합니다. 정상 상태의 데이터만을 가지고 학습을 진행하여, 시스템의 이상 여부를 판단해야 하는 문제입니다. 아무래도 이상 상태의 데이터는 정상 상태의 센서와 액추에이터 값이 다를 것이므로, AutoEncoder 를 통한 Anomaly Detection 을 진행하는 것 처럼, 데이터의 복원이 잘 안되면 이상 상태인 것으로 간주할 수 있고, 그 기준선을 정하는 것 역시 필요합니다. 

다음 중요한 특징은, 시계열 데이터라는 점입니다. 개인적으로 시계열을 매우 어려워하지만, 돌아보니 시계열에 대한 좋은 기억들이 많아 열심히 해봐야겠다 라고 다짐하였습니다. 무작정 표 데이터여서 MLP 를 만들고 시작하면, 시간 순서를 잘 고려하는 것 보다 낮은 성능을 보일 가능성이 매우 높습니다. 실제로 이 데이터로 한 첫번째 실험이 MLP AutoEncoder 였는데, 아주 엉망이었습니다. 그래서 바로 시계열 모델을 만들어야겠다고 생각했습니다. 

 

2. 모델링

최종적으로 사용한 모델은 MLP-Mixer 를 구현하여 사용하였습니다. 구현한 코드는 여기 에 있는 MLP Mixer 구현을 모델 저장하기 편하게, 사용하기 편하게 개인적으로 수정을 하였습니다. 

모델은 일단 '시계열'을 바로 다룰 수 있는 모델이어야 하기 때문에, 우선적으로는 RNN, 1D CNN, CNN+RNN, RNN+CNN, Transformer 들의 구조가 생각이 났습니다. Input 으로 time N 까지의 시계열 시퀀스를 받아, N+1 스텝의 센서 값을 예측하는 것이 목표였습니다. 아예 Seq2Seq 구조 처럼 출력 시퀀스를 뱉어내도록 만들어 볼까 하는 생각도 있었지만, 여러 실험을 하면서 코드를 새로 뜯어 고치기는 너무 바빴습니다. ㅠㅠ

첫번째 시도는, Dacon 에서 제공한 Baseline 코드 처럼 Bidirectional RNN 모델입니다. GRU와 LSTM 모두 실험 해 보았는데, 결과는 영 좋지 않았습니다. 검증 데이터셋에서는 아주 낮은 점수를 보였고, 그에 비해서 테스트 셋에서는 상대적으로 높은 점수를 보였습니다. 여기서 검증데이터와 테스트 데이터가 꽤 차이가 있을 수 있겠구나 라고 생각했습니다. 

두번째 시도는 CNN을 같이 이용한 모델을 시험해 보았습니다. 1D CNN만을 이용하여 만든 VGG style 의 모델은 전혀 학습을 하지 못했고, RNN과 CNN이 결합된 모델 역시 마찬가지였습니다. 

그 다음 시도한 것은 Transformer 모델입니다. Transformer 는 인코더만 만들어서 사용하였습니다. 예측하고자 하는 Timestep이 1개였기에 굳이 디코더까지 만들지 않아도 될 것이라 생각하였습니다. 그렇게 트랜스포머 인코더를 간단히 구현해서 예측을 하였는데, 점수가 눈에 띄게 상승한 것을 확인했습니다. RNN, CNN과 Transformer 가 다른 가장 큰 포인트, '전체 시퀀스를 한번에 볼 수 있느냐' 의 차이가 큰 효과가 있었다고 생각했고, Transformer 모델을 만들기 시작했었습니다. 하지만 Transformer 모델은 입력 시퀀스 길이를 길게 하여 실험을 하다 보니까, 시간이 너무 오래 걸렸고, 제 컴퓨터로 하기에는 답답해졌습니다. 

그래서 최종 결정한 모델이 MLP-Mixer 입니다. MLP Mixer 는 Transformer 처럼 한번에 전체 시퀀스를 다룰 수 있지만, 더 가볍고, 빠르고, VRAM이 모자란 제 컴퓨터에서 더 잘 돌아갈 것 같았고, MultiHeadAttention 과정보다 'Function Approximator' 에 가깝다고 볼 수 있다고 생각했습니다.  이러한 시계열 문제에서 Self Attention이 작동하는 과정은 (수학적으로 정확한 설명은 아니지만), Sequence 내의 각  Timestep간의 연관성, 중요도를 계산하는 것이라 생각했습니다. 하지만 MLP Mixer 는 각 Timestep 간의 연관성, 혹은 중요도, 관계를 파악하는 것이 아니라 함수로 표현하는 것이라 생각해서, 이것도 충분히 가능성 있는 모델이라고 생각했습니다. 제가 이해하고 있는 두 모델의 차이를 키노트로 그려 보았는데, 아래 그림과 같습니다. 

Sequence in Self Attention

 

Sequence in Mixer

 

하지만 MLP-Mixer 를 구현하여 학습을 진행해보니, 너무 빠르게 Overfit 되는 현상이 있었습니다. 그래서 MLP Mixer 에다가 Sharpenss Aware Minimization 을 추가해서 학습하기로 생각했습니다. 이 논문(When Vision Transformers Outperform ResNets without Pre-training or Strong Data Augmentations) 에서 보면, ViT와 Mixer 의 학습을 SAM 을 이용해서 도울 수 있다고 나와있습니다. 그래서 Mixer 와 SAM을 같이 써서 Overfitting 을 줄이고, Transformer + SAM 모델보다 더 빠르게 많은 모델을 만들어서 결과물을 앙상블 하는 것이 더 효율적으로 좋은 결과를 낼 것이라 생각하였습니다. 하지만 가장 큰 이유는, MLP-Mixer 논문 마지막 부분에, '다른 도메인에서도 잘 먹히는지 봐도 좋을 것 같다' 고 쓰여있어서 시도해본 것이 결정적 이유였습니다. 

MLP-Mixer 를 구현하고 나서, 어느 정도 성능이 잘 나오는 것을 확인하였습니다. 1D CNN, RNN보다 Transformer 와 Mixer 의 성능이 높게 나오는 것을 보아, 전체 Sequence 를 한번에 보고 처리하는 모델이 더 잘 작동하고 있고, 두 모델 모두 컴퓨터에서 돌아가는 선에서는 성능이 유사하게 나온다면, 복잡한 모델보다 단순한 모델이 낫다는 생각으로 MLP-Mixer 를 여러 타임스텝에 맞춰 Scale Up, Scale Down 한 16개의 모델을 만들고, 그 앙상블 모델을 최종 모델로 선택했습니다. MLP AutoEncoder 를 만들 때, 무작정 층 수를 늘리거나 뉴런 수를 많게 한다고 반드시 좋은 모델이 되지 않는다는 생각과 맥락을 같이 합니다. 

모델만큼 성능에 중요한 영향을 끼친 부분은 Threshold 를 정하는 일이었습니다. Threshold 는 optuna 를 사용하여 2000회의 반복을 통해서 결정하였습니다. 무작정 RandomSearch 를 하는 것 보다는 Bayesian Optimization 을 하는 것이 좋다고 판단했고, 평소 하이퍼파라미터 튜닝에 optuna 를 많이 사용해서 익숙한 함수를 만들듯이 적용할 수 있었습니다. 먼저 16개 예측값에 대하여, 이동 평균을 이용해 예측 결과물을 smoothing 시키고, threshold 를 결정해 [0, 1, 0, 0, ...] 과 같은 예측 결과물을 만들었습니다. 적용할 이동평균 값과 threshold 를 아래 그림과 같이 반복을 통하여 구했습니다. 

Decide MA, Threshold

다음으로는 16개 예측 결과를 조합하는 과정 역시 optuna 를 이용해서 만들었습니다. 0과 1로 이루어진 예측 결과물들을 softmax 함수를 거친 weight vector 를 통해 Soft Voting 하도록 하였습니다. 

Soft Voting Ensemble of 16 predictions

이러한 파이프라인을 만들었는데, 이 과정에서 가장 큰 문제는 검증 데이터에 대한 Overfitting 이 매우 잘 일어난다는 점이었습니다. 2020년 대회에 적용했을 때는, 먼저 예측값을 만들고 Voting 하는 것이 그닥 좋은 결과를 내지 못했는데, 2021년 대회에는 잘 적용되는 것을 확인했습니다. 제가 내년 대회에도 참여하게 될지는 잘 모르겠는데, 이 부분은 실험을 통해서 대회마다 다르게 적용되어야 할 것 같습니다. 

 

3. 결론, 느낀점

검증데이터셋의 활용이 정말 힘들었던 대회였습니다. 학습에는 절대 사용하면 안되지만, Early Stopping 을 걸거나, 검증 데이터셋 일부를 학습 데이터셋에 포함시켜서 Scaling 하는 것은 허용되었고, 검증 데이터셋 점수와 테스트 데이터 점수가 일관성이 별로 없게 나와서 혼란이 많았던 대회라고 생각합니다. 

2020년에도 HAICon이 진행되었는데, 1년 전에 저는 이 과정을 이해를 못하고, 1D CNN으로 만든 모델 3번 만들어보고, 대회가 이해가 되지 않아 포기했었던 기억이 납니다. 비슷한 시기에 올해도 비슷한 대회가 진행되어서, 1년 사이에 제가 조금은 성장했구나 라는 생각이 들어서 기뻤고, 좋은 결과까지 얻어서 더욱 기쁩니다. Public LB에서 7위였기 때문에 3등 안으로 들어가는 것은 상상도 하지 못했었고, Private LB도 뚜껑을 열어 보니 3등팀과의 점수 차이는 어마어마해서, 4등에 매우 만족하고있습니다. 더 열심히, 정진하도록 하겠습니다. 

반응형
Posted by Jamm_
반응형

 

 

cuijamm/CompetitionReview

Review of Competitions. My solutions, winner's code, or trials with new algorithms are uploaded. - cuijamm/CompetitionReview

github.com

 

 

운동 동작 분류 AI 경진대회

출처 : DACON - Data Science Competition

dacon.io

Github Repository & Dacon Codshare Post. 

 


 

오랜만의 대회 관련 포스팅입니다. 

2020년은 개인적으로 최고의 상승장이었지만, 2021년에는 다시 하락장이 시작되고있네요. 하락장 와중에 있었던 반등 같은 대회였습니다. 

월간 데이콘 13으로 진행되었던 운동 동작 분류 AI 경진대회에서 최종 3위를 기록하게 되었습니다. 무야호~

 

그만큼 신나시다는 거지!

 

전체 파이프라인 코드는 깃헙과 데이콘 코드공유 (맨 위의 링크 두개)에 올려져 있으니, 코드 자체를 블로그에 다시 적는건 의미가 없을 것 같고, 대회 중에 들었던 생각들과 과정들만 정리해보도록 하겠습니다. 

 


 

1. 데이터

총 600개의 timestep 을 가진 시계열 센서 데이터가 주어졌습니다. 해당 센서는 오른쪽 팔에 자이로스코프, 가속도계가 달린 센서를 착용하고, 특정 운동 동작을 수행했을 때, 그 동작이 61개 동작 중에서 어떤 class 에 해당하는지를 맞추는 Classification 문제였습니다. 데이터는 csv 파일로 주어지지만, 시계열 데이터에 맞춰 numpy array 로 reshape 하면 총 3125개의 센서 값이 기록되어 있음을 알 수 있습니다. 데이터가 아주 많지는 않네요. (Original Shape : (3125, 600, 6))

때마침 애플워치를 구입한지 얼마 되지 않았던 시기였기 때문에, 워치를 생각하며 애플워치를 착용하고 운동을 하는구나 라고 생각하고 대회에 재밌게 참여할 수 있었습니다. 

 

1.1. 라벨 불균형

대회 초반에 모델을 무작정 만들고 있을때도 어느 정도의 점수는 나왔었지만, 특정 점수 이상으로 잘 올라가지 않는 느낌을 받았습니다. 그래서 혹시나 해서 타겟변수를 살펴보니

 

총 학습데이터 3125개 중 절반이 26번, 나머지 절반 데이터를 60개 동작이 나눠먹고 있는 모습

 

상당히 imbalance 가 심한 것을 확인했습니다. 3000여 개의 데이터중에서 한 클래스의 갯수가 12개라니... 이거 너무한거 아니냐고? 응아니야

점수를 더 올리려면 이걸 해결해야겠다고 생각했습니다. 

 

1.2. Feature Engineering

feature_names = ['acc_x','acc_y','acc_z','gy_x','gy_y','gy_z']

grad_cols=[]
for col in feature_names:
    grad_cols.append(f"grad_{col}")

integ_cols = []
for col in feature_names:
    integ_cols.append(f"integ_{col}")
    
#position_cols = ['pos_x','pos_y','pos_z']
    
total_feature_names = feature_names + grad_cols + integ_cols #+ position_cols

고등학교때 수학시간에 들었던 말이 생각났습니다. 미적분 문제에 접근하는 것을 유독 힘들어했었는데, 선생님께서 '일단 속도가 보인다? 미분 할 생각부터 해라. 가속도를 구해야 풀리는 문제들이다' 이런 뉘앙스의 말을 하셨습니다. 주어진 데이터는 가속도 x, y, z 와 각속도 x, y, z 이므로 이들을 미분해서 가가속도, 각가속도를 만들고, 적분도 해서 속도, 각도 변수도 만들었습니다. 이렇게 적분했던 속도를 한번 더 적분하여 변위를 만들어서 사용했었는데, 이렇게 연속으로 적분을 하니까 오차가 점점 누적되어서 그런가, 의미가 없는 결과값을 얻었습니다. 

예전에 캐글의 Ion Switching 대회에서도 이렇게 gradient 를 만들어서 접근을 했던게 생각났습니다. 그때는 lag feature, delta features, moving average features 역시 만들어서 추가했었는데, 대회 중에는 생각이 안나서 시도해보지 못했던 것이 아쉽습니다. 

이렇게 해서 사용한 변수는 총 6 * 3 = 18개의 변수를 사용하였습니다. 

 


 

2. 모델

 

2.1. Augmentation

이번 대회에서 가장 아쉬움이 남았던 부분입니다. 1위 솔루션을 보았는데 정말 여러가지 Augmentation 기법들을 시도하고 사용해 보셨더라고요. 심지어 라벨에서 'left arm', 'right arm' 이라고 쓰여진 부분도 있었는데, 전부 다 오른팔에 착용했다고 생각하고 다른 augmentation 을 생각조차 하지 않았다는 점이 좀 아쉬웠습니다. 

처음에는 도저히 감이 잡히지 않았지만, Dobby님 께서 올려주신 코드 공유를 보고, 이런 방식으로 접근하면 되겠다고 생각했습니다. 

 

운동 동작 분류 AI 경진대회

출처 : DACON - Data Science Competition

dacon.io

numpy의 roll 을 이용하여 augmentation을 하면, 머릿속으로 동영상을 만들어 봤을 때 해당 센서 데이터가 Loop 처럼 반복된다고 볼 수 있다고 생각이 들었습니다. 킹도비 아이디어 갓... 직접적으로 저 코드처럼 구현을 하지는 않았지만, tf.roll 을 사용하여 커스텀 레이어를 만들어서, 학습시에는 랜덤한 값으로 Augmentation 을 수행하고, test 시에는 적용되지 않도록 구현하였습니다. 

# 모델의 인풋 바로 다음에 랜덤한 값으로 Rolling 을 하는 커스텀 레이어. 
class Rolling(Layer):
    def __init__(self, roll_max=599, roll_min=0):
        super(Rolling, self).__init__()
        self.random_roll = random.randint(roll_min, roll_max)   
        
    #def build(self, input_shape):  # Create the state of the layer (weights)
    #    pass
    
    def call(self, inputs, training=None):# Defines the computation from inputs to outputs
        if training:
            return tf.roll(inputs, shift=self.random_roll, axis=1)
        else:
            return inputs
        
    def get_config(self):
        return {'random_roll': self.random_roll}

 

2.2. Minority Oversampling

# 데이터를 하나하나마다 다른 Rolling 과 다른 노이즈를 추가하여 오버샘플링 하는 용도의 함수
def aug_data(w, noise=True, roll_max=550, roll_min=50, noise_std=0.02):
    assert w.ndim == 3
    auged=[]

    for i in range(w.shape[0]):
        roll_amount = np.random.randint(roll_min, roll_max)
        data = np.roll(w[i:i+1], shift=roll_amount, axis=1)
        if noise:
            gaussian_noise = np.random.normal(0, noise_std, data.shape)
            data += gaussian_noise
        auged.append(data)
    
    auged = np.concatenate(auged)
    return auged

위에서 확인했듯, Imbalance 가 매우 심합니다. 3125개중에 12개를 정확히 맞추는 것은 아무리 생각해 보아도 선을 넘은 것 같습니다. 그래서 Oversampling을 해 주었습니다. 

학습을 Stratified 10 Fold CV 를 하였는데, 매 Fold 마다 train과 valid를 쪼갠 이후, train데이터의 26번(Non-Exercise)항목이 아닌 데이터들만 뽑아서 위 함수를 이용하여 적용시켜 주었습니다. 원본 데이터를 그대로 복사하는것은 아니고, 데이터 전체가 아니라 각각의 데이터마다 랜덤하게 roll을 해주고, 약간의 가우시안 노이즈를 추가하여 train 데이터에 concat 하였습니다. 1번 정도만 적용하니 성능이 향상되었고, 2번 이상부터는 overfit이 쉽게 일어나는 것 같았습니다. 

 

2.3. Modeling

모델 구조는 여러 가지를 생각해 보았는데, 

  • Conv1D 이후 Dense (VGG-like)
  • RNN (LSTM / GRU) 이후 Dense (Stacked LSTM)
  • RNN 과 Conv1D 를 섞어서 Skip Connection을 골고루 넣는 (떡칠하는) 모델
  • RNN Path 와 Conv1D Path 를 따로 두고 Concat하여 Timestep 과 Local feature들을 동시에 고려하는 모델

들이 생각이 났었는데, 최종 모델로 선택한 것은 1번 이었습니다. 레이어를 아무리 넣고 빼고 자시고를 반복해도 RNN계열 층이 섞여있을 때는 성능이 생각보다 잘 나오지 않았습니다. 개인적으로 시계열 문제를 굉장히 싫어하는데, (잘하고싶은데, 잘 안돼요..) 아직까지는 한번도 RNN 계열 층을 써서 CNN보다 잘 나오는 경우를 못겪어봤습니다...

 

# Convolution, Dense 레이어 여러번 적기 번거로워서 만든 함수
def ConvBlock3(w, kernel_size, filter_size, activation):
    x_res = Conv1D(filter_size, kernel_size, kernel_initializer='he_uniform', padding='same')(w)
    x = BatchNormalization()(x_res)
    x = Activation(activation)(x)
    x = Conv1D(filter_size, kernel_size, kernel_initializer='he_uniform', padding='same')(x)
    x = BatchNormalization()(x)
    x = Activation(activation)(x)
    x = Conv1D(filter_size, kernel_size, kernel_initializer='he_uniform', padding='same')(x)
    x = Add()([x, x_res])
    x = BatchNormalization()(x)
    x = Activation(activation)(x)
    return x
    
def DenseBNAct(w, dense_units, activation):
    x = Dense(dense_units, kernel_initializer='he_uniform')(w)
    x = BatchNormalization()(x)
    x = Activation(activation)(x)
    return x



def build_fn(lr = 0.001):
    activation='elu'
    kernel_size=9
    
    
    model_in = Input(shape=Xtrain_scaled.shape[1:])
    x = Rolling(roll_max=599, roll_min=0)(model_in)
    x = SpatialDropout1D(0.1)(x)
    
    x = ConvBlock3(x, kernel_size=kernel_size, filter_size=128, activation=activation)
    x = MaxPooling1D(3)(x)
    x = SpatialDropout1D(0.1)(x)
    
    x = ConvBlock3(x, kernel_size=kernel_size, filter_size=128, activation=activation)
    x = GlobalAveragePooling1D()(x)
    
    x = DenseBNAct(x, dense_units=64, activation=activation)
    x = Dropout(0.4)(x)
    
    
    model_out = Dense(units=61, activation='softmax')(x)
    model = Model(model_in, model_out)
    model.compile(loss='sparse_categorical_crossentropy', optimizer=Nadam(learning_rate=lr), metrics='accuracy')
    
    return model


build_fn().summary()

VGG 스타일의 심플한 Conv1D 모델입니다. Conv1D는 커널사이즈를 꽤나 크게 잡아도 파라미터 수가 엄청 뻥튀기 되지 않고, 오히려 충분한 커널사이즈가 있어야 Timeseries 의 컨텍스트를 잡아낼 수 있을거라 생각해서 커널 사이즈를 흔히 Conv2D에서 사용하는 3이 아니라 9로 정했습니다. 

이후 학습은 Stratified 10Fold CV를 사용하여 10개 모델의 평균을 내어 제출하였습니다. 

 


 

3. 기타 다른 아이디어

  • 캐글의 ion switching 대회에서 나왔던 Kalman Filter 를 이용한 noise smoothing - 데이터가 상당히 깔끔하게 잘 나와있었어서 굳이 할 필요가 없었다고 생각이 듭니다. 
  • 데이터들의 statistics 들을 통한 aggregation, 및 Tree 기반 모델 접근 - 대회 초반에 가만히 생각해 보았지만, '굳이 데이터를 요약?까지 해야 하나? Conv1D나 LSTM, GRU 쓰면 바로도 충분히 접근할 수 있을 것 같은데.' 라는 생각에 시도해보지는 않았습니다. 
  • Stacking(meta-modeling) - 스태킹을 할때 test 셋을 bagging 해서 만들면 oof로 만들어진 meta training set과 bagging으로 만들어진 meta test set이 차이가 나서 그런가, 점수가 잘 오르지 않는 모습을 예전부터 보고 있었습니다. 스태킹 잘하시는 분들 혹시 이 글을 보신다면... 꿀팁 알려주시면 감사하겠습니다.  개인적으로 앞으로도 평균 앙상블은 정말 많이 사용할 것 같은데, 스태킹은 거의 안하게 될 것 같습니다. 좀 많이 양보하면.. 단순평균 아니라 가중평균정도...?

 


 

4. 결론 및 아쉬운 점

다른 대회에서도 저는 Augmentation을 잘 안하는 편인데, 역시나 이번에도 마찬가지였습니다. 항상 적절한 augmentation 방법을 찾아 적용하는데 실패해서 매번 버리는 경우가 많았는데, 이 대회에서는 Augmentation 에 더 노력을 덜 기울였던 점이 끝나고 보니까 아쉬움으로 남는 것 같습니다. 충분한 Augmentation으로 성능이 잘 나오는 데이터였는데, 위에 생각했던 것들을 하나씩 하고 나니까 리더보드 수상권으로 들어오기도 했고, 너무 안일하게 슬슬 마무리 짓자 라는 생각을 했던 것 같습니다. 기회가 된다면 다른 유저분들이 사용했던 Augmentation 방법론들을 또 추가해보고, (특히 왼손 오른손 Augmentation이 제일 인상깊었습니다...) 한번 더 해보고 싶은 대회네요. 데이터도 작아서 데스크탑 정도로 부담 없이 재밌게 진행할 수 있었고, CV-LB 점수가 상당히 정직하게 나와서 접근하기 좋았던 대회였던 것 같습니다. 

반응형
Posted by Jamm_
반응형

기말고사가 드디어 끝났습니다. 얼마 전 시험 시작 전에 한국수력원자력에서 진행했던 데이콘 대회의 최종 순위가 발표되었고, 정말 운이 좋게도 우승이라는 좋은 결과를 얻을 수 있었습니다. 개인적으로는 친구들이 양주사라, 코로나만 잠잠해지면 회식 가자 하는 중이고.. 양가 친척 모두에게 소문이 나서 '아이고 장하다 고놈' 그런 이야기를 듣고 있었습니다. 신문 기사에서 너무 대문짝만하게 나오기도 해서 너무 부끄럽고, 학교 홈페이지에도 올라가서 수업듣는 교수님이 '너가 그 우승한 걔 맞냐' 물어보시기도 했습니다... 아싸는 부담스러워요...

지금까지 공부하면서 얻었던 가장 좋은 성적이었기에 얼른 공유를 하고 싶었는데, 코드의 저작권과 데이콘과 작성했던 양수양도 계약서, 상금 지급 여부 등의 문제 때문에 바로 올리지는 못하고 약간 뒷북이 되긴 하였지만 이제서야 올릴 수 있게 되었습니다. 개인적으로 너무 힘들었던 대회였고, 마지막까지 예측이 안되던 대회였어서 얼떨떨하고 합니다...

 

공공데이터 활용 수력 댐 강우예측 AI 경진대회

출처 : DACON - Data Science Competition

dacon.io

입상자 코드가 공개된다고 하니, 저는 코드를 직접 쓰지는 않고 생각만 적어보겠습니다. 

사실 코드를 안올리는 진짜 이유는...코드 내에 오타가 있었어서...아래에 썼던 '생각했던 모델' 과 연결구조가 약간 다릅니다... 층을 3개 쓰려고 했는데 두개만 들어갔다던가... 이걸 대회 끝날때까지 모르고 있다가, 코드 제출하면서 찾았다니 어이가 없을 뿐입니다...

 

1. 데이터 소개

데이터셋에 대해서 이야기를 하자면, 학습데이터로는 (120, 120) 의 레이더 사진이 4장, 타겟 데이터로는 (120, 120)의 한장이 주어졌습니다. 샘플의 수는 약 6만여개가 주어졌고, npy 파일로 데이터를 받을 수 있습니다. 4장의 사진은 (30분 전, 20분 전, 10분 전, 현재) 의 4장의 사진을 가지고 10분 후의 사진 1장을 예측하는것이 목표입니다. 이렇게 레이더 사진을 예측하여 제출하면 레이더 사진을 강수량으로 변환하여, 강수 여부(정확도, 정확히는 CSI)와 강수량(MAE) 를 계산하여 점수를 얻게 됩니다. 

이미지의 시계열 데이터라고 생각을 했습니다. '이미지의 시계열이면 동영상 아니야?' 하는 생각에, 대회 초반에는 케라스의 ConvLSTM2D 층을 잔뜩 쌓은 모델을 만들려고 했습니다. 또 어떻게 보면 구름이 있어야만 비가 올 수 있으니, 미래 이미지에 대한 Segmentation으로도 접근할 수 있겠다고 생각하고 있었습니다. (CSI만 생각하면 어느정도 비슷한 접근이라고 생각합니다.) 하지만 ConvLSTM2D의 결과는 점수상으로 엉망이었고, 아 이게 아닌가 어떻게 하지 하며 고민하는 와중에 데이콘에서 Baseline 코드가 올라왔고, UNet이라는 것을 알게 되었습니다. 

UNet 의 구조 (논문 내 사진)

 

U-Net: Convolutional Networks for Biomedical Image Segmentation

There is large consent that successful training of deep networks requires many thousand annotated training samples. In this paper, we present a network and training strategy that relies on the strong use of data augmentation to use the available annotated

arxiv.org

UNet 의 논문은 위 링크에서 확인할 수 있습니다. 

원래는 Biomedical 분야에서의 의료 영상 Segmentation 을 위해 만들어진 모델이라고 하는데, 데이콘의 베이스라인 코드가 간소화된 UNet 구조를 사용하고 있었습니다. 

놀랍게도 이 UNet구조를 본따서 모델을 만들기 시작했는데, ConvLSTM2D와는 차원이 다른(?) 점수 상승을 불러왔습니다. 아, 이거구나! 유레카를 외치며 UNet 구조의 제 모델을 더 개선시키기 위해 코드를 계속 사용했습니다. 

그리고 또 검색을 해보니 RainNet 이라는 것도 있더군요. 

 

hydrogo/rainnet

RainNet: a convolutional neural network for radar-based precipitation nowcasting - hydrogo/rainnet

github.com

RainNet의 구조 역시 UNet과 거의 같았습니다. 깃헙 안에 있는 모델 코드를 보면 두군데 Dropout이 추가되었습니다. 이 RainNet에 따르면 데이터는 레이더 영상이 아니라 강수량으로 변환한 데이터였고, Loss Function은 LogCosh라고 검색을 하다가 봤던 것 같습니다. 아무튼 이 UNet Style Model이 효과가 있다는 것은 확인했습니다. 

 

 

2. 모델링

최종적으로 작성한 모델링 아이디어

키노트로 모델 구조를 그려보았습니다. 기본적인 UNet 구조로 흘러가는 Path 1 과, ConvLSTM2D를 거치는 Path 2 가 있고, 마지막에는 둘을 합쳐 마지막 Convolution을 진행하여 출력하고, 출력 Convolution을 3 * 3 으로 두었습니다. 이렇게 하면 구름의 주변 값도 고려하지 않을까 라고 생각했는데, 1 * 1 Convolution 과의 비교를 할 시간이 없었어가지고, 약간 아쉽습니다. Upsampling 층 대신에 Conv2DTranspose를 사용했고, RainNet에 있던 Dropout 위치에다가 그림에는 지금 Dropout 이라고 표시되어 있지만 SpatialDropout2D를 사용했고, 활성화함수는 ELU를 사용했습니다.  개인적으로 mish 를 정말 좋아하는데 이번에는 mish를 못써서 속상했습니다(?) 왜 좋냐고요? 그냥 이름이 이뻐서...

그림에는 그냥 나와있지만, 필터수 256, 512에서는 제가 '월간데이콘 6, 음성 중첩 분류 대회' 에서 사용했던 선형 Bottleneck 구조를 다시 사용했고,(파라미터 수를 줄이고 오래 걸리는 모델을 얻었다...) 모든 3번의 Convolution 블럭? 의 첫번째와 세번째를 잇는 Additive Skip Connection이 들어있습니다. 개인적으로 스킵커넥션이 Add 인 경우는 뭔가 '야 이게 맞으니까 당장 반영해.' 느낌이 있고, Concat인 경우는, '자 이렇게 될 수도 있는데 같이 생각해봐~' 이런 느낌이었습니다.

그냥 UNet만 사용하다가 ConvLSTM2D를 추가하게 된 이유는 다음과 같습니다. 

  • Conv 층의 필터끼리는 Dense Layer 와 비슷하게 연산된다. -> 이렇게 되면 시계열 순서가 담고있던 의미가 섞이게 될 것입니다.
  • (예전에 어떤 블로그에서 읽었던 글인데, 기억이 안나요 죄송합니다.) 신경망은 Sparse 할 수록 성능이 좋다, 인셉션이 성능이 좋았던 이유. 라고 설명을 해 주셨었는데, 지금도 Sparse가 어떤 의미인지 완벽히 이해하지 못하고 있지만, 인셉션 구조의 컨셉? 그림? 이 생각이 나서 이런식의 디자인이 나름의 인셉션 역할을 하지 않을까 라고 생각했습니다. 
  • ConvLSTM2D층 한개짜리 모델의 결과를 시각화 해 보았더니, 생각보다 성능이 좋아 보였다. -> 아주 개인적으로는 구름이 대충 '어디에 있는지' 정도는 층 하나로도 잘 잡아내는 듯 했습니다. 하지만 구름 모양의 세부적인 모양 디테일이나 그 값을 잘 잡아내지 못했기 때문에 점수가 잘 나오지 않았다고 생각했습니다. 그래서 ConvLSTM 구조로 모양을 잡고, UNet 구조로 디테일을 잡아서 합치면 어떨까? 하는 생각에 이렇게 만들게 되었습니다. 

학습, 예측 과정은 특별한 과정 없이 10 Fold Cross Validation을 하였고, Early Stopping을 걸어서 Fold 별로 베스트 모델을 저장해두었다가, 10개 모델들의 결과물의 단순평균을 구해서 제출했습니다. 오랫동안 1등을 유지하고있었던 0.50033(?) 점의 모델도 이 구조의 단일 모델 결과였습니다. 그래서 평균 앙상블 모델의 Public 점수는 매우 별로였지만 그래도 선택 할 수 있었다고 생각합니다. 

 

 

3. 기타 시도했던 것들 / 대충 시도 또는 생각만 해본....

  1. Metric 을 AUC 로 둔 것. 픽셀별로 0~1 값이니까 뭔가 측정이 될 수 있지 않을까 하는데 의미 없었습니다.
  2. Grouped Conv2D. Timestep이 4니까 UNet의 Conv에서 group을 4로 주고 해보았습니다. 지금 봐도 나쁘지 않은 생각인 것 같은데, 할 수 있다면 다시 해보고 싶습니다. 
  3. TimeDistributed(Conv2D). 이거 할 바에는 그냥 ConvLSTM2D를 해야겠다 생각했습니다.
  4. UNet Style ConvLSTM2D. ConvLSTM을 64로 시작해서, UNet의 모든 Conv 층을 ConvLSTM으로 대신할 수 있을까 생각했지만 컴퓨터가 못버텨서 빠른 포기.
  5. Input -> ConvLSTM2D -> UNet 구조 / UNet에서 가운데 층만 ConvLSTM2D. 전자의 경우는 좀 더 생각해볼 수 있지 않을까 싶은데, 후자의 경우는 Conv2D 통과하는 순간 시계열이 섞이기 때문에 틀린 생각이었다고 생각되네요. 시간 압박때문에 ConvLSTM2D 층을 두개 이상 쓰기는 정말 힘들었습니다. 
  6. Input -> DownSample -> ConvLSTM2D -> Upsample. 모델 크기는 작아지긴했지만, 대책없이 DownSampling을 해서 그런가 정보 손실이 있을 것 같습니다. 
  7. DownSampling 시에 CBAM 어텐션, UpSampling 시에는 'Attention UNet' 의 Attention Gate 추가. 둘다 썼을 때나, 둘중에 하나만 썼을 때나 둘다 결과가 그닥 좋지 않았습니다. 논문에 있는 그림 보고 무작정 만들어 봤었는데, 잘못 만들었을 것 같은 느낌이 확 드네요. 이것도 다시 해보고 싶은 아이디어 입니다. 

 

4. 결론

정말 너무 힘든 대회였습니다. 제출할 때마다 Public LB 점수는 오락가락 하고 있고, 모델 하나 만들어서 제출하는 시간은 너무 오래 걸리고, 컴퓨터는 이러다가 불날 것 같았고... 하지만 운이 잘 따라주었다고 생각합니다. 저는 여전히 부족한게 너무 많네요... 아직은 실력에 비해서 Over Prized(?) 된 것 같습니다. 그래도 상은 감사히 받고, 상값은 앞으로 더 열심히 해서 갚도록 하겠습니다. 

 

데이콘 토론 탭에도 요약본을 올렸습니다 :

 

공공데이터 활용 수력 댐 강우예측 AI 경진대회

출처 : DACON - Data Science Competition

dacon.io

 

반응형
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_