본문 바로가기
Programming/Machine Learning

1D Convolutional Neural Network 이해하기 (CNN in numpy & keras)

by a voyager 2021. 8. 27.
728x90
반응형

바다에 다다를 때까지... 

 

목차

    도입

     

    머신러닝 분야에서 예측 모델을 만드는데 가장 많이 사용되는 신경망 모델은 바로 Convolutional Neural Network(CNN)일 것이다. CNN은 특히 이미지 분류에서 높은 정확도를 보이며 많은 예측 모델의 토대를 이루었다. 

     

    반면, 1차원 CNN은 이미지가 아닌 시계열 분석 (time-series analysis)나 텍스트 분석을 하는데 주로 많이 사용된다. 여기에서 1차원이라는 것은 합성곱을 위한 커널과 적용하는 데이터의 sequence가 1차원의 모양을 가진다는 것을 의미한다. 

     

    실제 문제에 적용하기에 앞서, 이 포스팅에서는 1차원의 합성곱이 어떻게 이루어지는지 numpy와 keras.layers.Conv1D 를 이용해 알아보도록 하겠다. 

     

    우선 필요한 라이브러리를 import한다. 그리고 matplotlib의 rcParams으로 그림의 ticks의 폰트 사이즈를 설정해준다. 

     

    import numpy as np 
    import pandas as pd
    import tensorflow as tf 
    import matplotlib.pyplot as plt
    import matplotlib as mpl
    
    label_size = 14
    mpl.rcParams['xtick.labelsize'] = label_size 
    mpl.rcParams['ytick.labelsize'] = label_size

     

     

    1D Convolution 

     

    4개의 원소를 가지는 길이가 4인 배열(input data)과, 길이가 2인 filter을 만들어 본다. 

     

    data = np.array([0,3,4,5])
    conv1d_filter = np.array([1,2])

     

    합성곱은 filter를 data를 따라 이동하며 이루어지며, strides가 1인 경우 아래 그림과 같이 합성곱 진행되고, output은 길이가 3개인 배열이 나온다. 

     

     

    위 convolution의 결과를 numpy를 이용해 구해보자. 

     

    def Conv1D_Numpy(Seq, Kernel):
        kernel_size = len(Kernel)
        Length = len(Seq)
        
        output = []
        for i in range(Length-kernel_size+1): 
            conv = np.dot(Seq[i:i+kernel_size], Kernel)
            print(Seq[i:i+kernel_size], "*", Kernel, "=> ", conv)
    
            output.append(conv)
    
        output = np.array(output)
        
        return output

     

     

    위의 함수에 data와 filter를 입력한다. output은 [6,11,14]인 배열이다. 

     

    output = Conv1D_Numpy(data, conv1d_filter)
    output

     

    Using Keras

     

    이 과정을 keras의 Conv1D layer로 진행해 볼 수 있다. 1D CNN의 뉴런의 개수는 2개가 된다.

     

    이때, 문제의 정의가 도치된다. Numpy를 이용한 합성곱에서 했듯이 input 배열과 kernel filter이 주어졌을 때 output 배열을 찾는 것이 아닌, input과 output 배열을 주고 1D CNN의 kernel filter를 찾는 것이다. 이때, kernel의 원소는 CNN의 뉴런이 된다. 즉, 각 뉴런의 가중치(weights)를 찾는 것이다. 

     

    아래와 같이 1D CNN을 만들고 컴파일하고, 훈련 결과 weights을 그려보는 함수를 준비한다. 

     

    def Conv1D_compile(n_filters, SequenceLength, n_features):
        conv_model = tf.keras.Sequential([
            tf.keras.layers.Conv1D(filters=n_filters,
                                   kernel_size=2,
                                   strides=1, 
                                   padding='valid',
                                   input_shape=(SequenceLength, n_features), 
                                   use_bias=False, name='c1d')])
        
        conv_model.compile(loss=tf.losses.MeanAbsoluteError(), 
                           optimizer=tf.optimizers.Adam(learning_rate=5e-2))
    
        conv_model.summary()
    
        return conv_model     
    
    
    def Conv1D_Fit_and_PlotWeights(model, X, y, epochs, n_weights, freq=20):
        w, loss, mae = [], [], []
    
        for r in range(epochs):
            history = model.fit(X, y, verbose=0)
            if r%freq==0:
    
                w.append(np.sort(model.layers[0].get_weights()[0].reshape(n_weights)))
                loss.append(history.history['loss'][0])
    
        w = np.array(w)
                
        fig, ax = plt.subplots(figsize=(8,4))
    
        epoch = np.arange(0,len(w))*20
        
        for n in range(n_weights): 
            label = "w_{} -> {}".format(n, n+1)
            plt.plot(epoch,w[:,n], label=label, linewidth=3)
            ax.axhline(n+1, c='gray', linestyle='--')
    
        plt.xlabel("epoch", fontsize=14)
        plt.ylabel("weights", fontsize=14)
        plt.legend(loc='upper left', bbox_to_anchor=(1., 1.01), fontsize=14)
        plt.show()

     

     

    데이터를 준비하는데 주의할 점은, 비록 1차원 CNN이라고 하더라도, Conv1D layer의 입력과 출력 데이터는 3차원이어야 한다는 것이다. 공통적으로 (batch, time, feature)의 구조를 가진다. 

     

    • batch: 샘플 수 (이 예제에서는 1이다) 
    • time: 입력 데이터의 길이 (이 예제에서는 4이다)
    • feature: 데이터셋의 feature 수이다 (이 예제에서는 1이다) 

     

     

    다음과 같이 입출력 데이터를 reshape 해준다. 

     

    X = data.reshape((1, data.shape[0], 1))
    y = output.reshape((1, output.shape[0], 1))

     

     

    이제 Conv1D layer를 만들고 loss function과 optimizer를 정의하고 컴파일을 한다. 

     

    model_cnn = Conv1D_compile(n_filters=1, SequenceLength=4, n_features=1)

     

     

    summary() 매서드로 model_cnn의 구조를 볼 수 있다.

     

     

     

    2개의 변수가 있는 것을 확인한다. 이 두 변수가 Conv1D layer의 가중치이고, 훈련을 할수록 kernel 값인 1,2 값에 맞춰질 것이다. 

     

    이제 훈련을 통해 두 가중치를 업데이트 해본다. 아래 그림에서 보듯이 epoch가 증가할수록 1과 2로 빠르게 수렴하는 것을 볼 수 있다. 예상했던 대로 결과가 나왔다.  

     

    Conv1D_Fit_and_PlotWeights(model=model_cnn, X=X, y=y, 
                                         epochs=500, n_weights=2)

     

    이러한 수렴은 1D CNN에서 bias와 activation 함수를 사용하지 않았기 때문에 가능한 것이며, 이 설정은 위 Conv1D_compile 함수 내에서 확인할 수 있다. 

     

     

    1D Convolution with features 

     

    다음으로 고려해볼 변수는 feature이다. image 데이터에서 feature는 channel에 대응된다. 컬러 이미지는 RGB 3개의 채널을 가진다. 이때 이미지의 합성곱 결과는 세 채널을 따로 합성곱하여 더하는 것으로 주어진다.

     

    반면, 1차원 CNN의 경우에 feature는 input feature에 대응된다. 즉, 어떤 target value를 결정하는데 주어진 feature들이다. 온도를 예측한다고 하면 바람, 습도 등등이 바로 input feature에 해당할 것이다. 

     

    간단하게 4개의 뉴런을 가지는 CNN를 생각해보자. 이 경우 2개의 feature가지는 (n, 2)의 shape의 input 데이터를 고려해 볼 수 있다. 이때 convolution kernel도 역시, 각 feature에 다르게 적용할 수 있도록 두 개를 만든다.   

     

     

    Input = np.array([[0, 0], [3, 6], [4, 7], [5, 8]])
    kernels = np.array([[1,2],
                        [3,4]])
    Output = np.array([30, 57, 67])

     

     

    Input 데이터를 pandas dataframe으로 저장하면 feature 별 다른 시퀀스를 한눈에 볼 수 있다. 

     

    df = pd.DataFrame(data=Input, columns=["feature1", "feature2"])
    df

    two features dataset 

     

    위 테이블에서 보듯이 두 개의 feature sequence가 있고 이 두 컬럼에 다른 convolution kernel이 적용된다. 

     

     

    Using Numpy 

     

    Output = np.zeros(Output.shape)
    
    for i, col in enumerate(df.columns):
        conv = Conv1D_Numpy(df[col].values, kernels[i])
        print(conv)
        print("")
        Output +=conv
    
    print("Output: {}".format(Output))

     

    첫 번째 커널 [1,2]는 feature1에 적용되고, 두 번째 커널 [3,4]는 feature2에 적용된다. 

     

    Output은 두 convolution 배열의 합이다. 아래에서 [30, 57, 67]이 그 결과이다. 

     

     

     

    Using Keras 

     

    Input과 Output을 알았으니, 1D CNN을 이용해 kernel의 네 값을 찾도록 해보자. 

     

    위에서 했던 방법과 동일한 방법으로 데이터 차원을 변환해주고 모델을 만들고 훈련을 해준다. 

     

    X = Input.reshape(1,4,2)
    y = Output.reshape(1,3,1)
    
    model_wFeatures = Conv1D_compile(n_filters=1, SequenceLength=4, n_features=2)
    
    Conv1D_Fit_and_PlotWeights(model=model_wFeatures, 
                              X=X, y=y, 
                              epochs=1000, n_weights=4)

     

     

    summary()로 4개의 parameter가 있는 것을 확인한다.

     

    훈련이 아주 잘되었다. 예상대로 네 뉴런의 가중치 값이 kernel값으로 수렴하는 것을 확인할 수 있다. 

     

     

    1D Convolution with filters  

     

    마지막으로 살펴볼 Conv1D layer의 변수는 n_filters이다. 

     

    2차원 CNN에서 n_filters는 몇 개의 window를 이용해 합성곱을 할 것인지를 정해주는 변수이다. 이미지의 경우에는 여러 window가 잡아내는 (capturing) 특징이 다르게 된다. 즉, 어떤 window는 edge를 잡아내고 또 다른 window는 curve를 잡아낸다. 그렇기에 적당히 많은 window를 도입하면 모델의 성능을 높일 수 있다. 

     

    하지만 1차원 CNN에서 n_filters는 output의 수이다. 하나의 feature를 가지는 input 데이터에 서로 다른 n_filters개의 kernel을 적용하여 그 커널 수만큼의 output를 만드는 것이다. 

     

    다음의 예를 살펴보자. input data인 X는 하나의 시퀀스이지만, output y는 두 개의 시퀀스이다. 즉, 두 개의 다른 kernel이 적용되었다는 것이다. 

     

    X = np.array([0, 3, 4, 5]).reshape(1,4,1)
    y = np.array([[6, 12], [11, 25], [14, 32]]).reshape(1,3,2)
    
    model_wfilters = Conv1D_compile(n_filters=2, SequenceLength=4, n_features=1)
    
    Conv1D_Fit_and_PlotWeights(model=model_wfilters, 
                              X=X, y=y, 
                              epochs=200, n_weights=4)

     

    이 경우 역시 뉴런의 수는 4개이다. 

     

     

    훈련 결과 예상대로 weights의 값들이 잘 수렴한 것을 볼 수 있다. 

     

    이제 1D CNN이 어떻게 작동하는지에 대한 이해를 했으니, 다음 포스팅에서는 Conv1D가 가장 많이 사용되는 시계열 데이터 예측 모델에 적용해 보도록 하겠다. 

     

     

    참고한 글

     

    728x90
    반응형

    댓글