반응형

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

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

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

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