2021/03/04 - [Deep Learning/Deep Leaning inside] - 트랜스포머(Transformer)(1)

 

8)패딩 마스크(Padding Mask)

앞서 포스팅한 Scaled dot-product attention함수 내부를 보면 mask라는 값을 인자로 받아서, 이 mask값에다가 -1e9라는 아주 작은 음수값을 곱한 후 어텐션 스코어 행렬에 더해준다. 

이는 입력 문장에 <PAD>토큰이 있을 경우 어텐션에서 사실상 제외하기 위한 연산이다. 

그림을 통해 이해해보자.

 

사실 단어 <PAD>의 경우에는 실질적인 의미를 가진 단어가 아니다. 그렇기에 트랜스포머에서는 Key의 경우에 <PAD> 토큰이 존재한다면 이에 대해 유사도를 구하지 않도록 마스킹을 해주기로 했다.

*masking이란 어텐션에서 제외하기 위해 값을 가린다는 의미이다.

어텐션 스코어 행렬에서 행에 해당하는 문장은 Query, 열에 해당하는 문장은 Key이다. 그리고 Key에 <PAD>가 있는 경우에는 해당 열 전체를 masking 해준다.

 

masking하는 방법은 어텐션 스코어 행렬의 마스킹 위치에 매우 작은 음수값을 넣어주는 것이다. 현재 어텐션 스코어 함수는 소프트맥스 함수를 지나지 않은 상태이다. 현재 마스킹 위치에 매우 작은 음수 값이 들어가 있으므로 어텐션 스코어 행렬이 소프트맥스 함수를 지난 후에는 해당 위치의 값은 0에 근사한 값을 갖게 되어 단어간 유사도를 구하는 일에 <PAD> 토큰은 반영되지 않는다는 의미이다.

위 그림은 소프트맥스 함수를 지난 후의 그림이다. 각 행의 어텐션 가중치는 총합 1이 되는데, <PAD>의 경우 0이되어 어떤 값도 갖고 있지 않다.

 

패딩마스크를 구현하는 방법은 입력된 정수 시퀀스에서 패딩 토큰의 인덱스인지, 아닌지를 판별하는 함수를 구현하는 것이다. 

 

def create_padding_mask(x):
    mask=tf.cast(tf.math.equal(x,0),tf.float32)
    return mask[:,tf.newaxis,tf.newaxis,:]
    
    
print(create_padding_mask(tf.constant([[1,21,777,0,0]])))

tf.Tensor([[[[0. 0. 0. 1. 1.]]]], shape=(1, 1, 1, 5), dtype=float32)

위 함수는 정수 시퀀스에서 패딩 토큰의 인덱스인지 아닌지를 판별하는 함수이다. 

0인경우에는 1로 변환하고, 그렇지 않은경우에는 0으로 변환하는 함수이다.

 

결과를 살펴보면 위 벡터를 통해서 1의 값을 가진 위치의 열을 어텐션 스코어 행렬에서 마스킹하는 용도로 사용할 수 있다. 위 벡터를 Scaled dot-product attention 인자로 전달하면, scaled dot-product attention에는 매우 작은 음수값인 -1e9를 곱하고 이를 행렬에 더해주어 해당 열을 전부 마스킹하게 되는 것이다.

 

 

포지션-와이즈 피드 포워드 신경망(Position-wise FFNN)

포지션-와이즈 FFNN는 쉽게 말하면 완전 연결(Fully-connected FFNN)이라고 해석할 수 있다. 다음 수식을 살펴보자.

-x는 앞서 멀티 헤드 어텐션의 결과로 나온(seq_len,d(model))의 크기를 가지는 행렬을 말한다. 가중치 행렬 W1은 (d(model),d(ff))의 크기를 가지고, 가중치 행렬 W2는 (d(ff),d(model))의 크기를 가진다. 

*논문에서는 은닉층의 크기인 d(ff)는 2047의 크기를 가진다.

 

여기서 매개변수 W1,b1,W2,b2는 하나의 인코더 층 내에서는 다른 문장, 다른 단어들마다 정확하게 동일하게 사용된다. 하지만 인코더 층마다 다른 값을 가진다.

 

위 그림에서 좌측은 인코더의 입력을 벡터 단위로 봤을 떄, 각 벡터들이 멀티헤드 어텐션 층이라는 인코더 내 첫번째 서브 층을 지나, FFNN을 통과하는 과정을 보여준다.

위 그림을 코딩으로 구현해보자.

 

outputs=tf.keras.layers.Dense(units=dff,activation='relu')(attention)
outputs=tf.keras.layers.Dense(units=d_model)(outputs)

잔차 연결(Residual connection)과 층 정규화(Layer Normalization)

트랜스포머에서는 위 그림처럼 두 개의 서브층을 가진 인코더에 추가적으로 사용하는 기법이 있다. 

바로 Add&Norm이다. 더 정확히는 이를 잔차 연결(residual connection) 과 층 정규화(layer nomalization)이라 부른다.

 

1)잔차 연결(Residual connection)

다음 식을 확인해보자.

위 그림은 입력 x와 x에 대한 어떤 함수 F(x)의 값을 더한 H(x)의 구조를 보여준다. 여기서 F(x)는 트랜스포머에서 서브층에 해당된다. 다시 말해 잔차 연결은 서브층의 입력과 출력을 더하는 것을 말한다.

위에서 언급했듯이 서브층의 입력과 출력은 동일한 차원을 갖고 있으므로, 서브층의 입력과 서브층의 출력은 연산을 할 수 있다.

 

2)층 정규화(Layer Normalization)

잔차 연결을 거친 결과는 이어, 층 정규화 과정을 거친다. 잔차 연결의 입력을 x, 잔차 연결과 층 정규화 두 가지 연산을 모두 수행한 후의 결과 행렬을 LN이라 하였을 때, 잔차 연결 후 층 정규화 연산을 수식으로 표현하면 다음과 같다.

층 정규화란 텐서의 마지막 차원에 대해서 평균과 분산을 구하고, 이를 가지고 어떤 수식을 통해 값을 정규화하여 학습을 돕는다. 

*텐서의 마지막 차원이란 트랜스포머에서는 d(model)차원을 의미한다.

층 정규화를 수행한 후에는 벡터 x(i)는 ln(i) 라는 벡터로 정규화가 된다.

수식을 이해해보자. 과정은 다음과 같다.

1. 평균과 분산을 통한 정규화

2. 감마와 베타 도입

우선,평균과 분사을 통해 벡터 x(i)를 정규화 해준다. x(i)는 벡터인 반면, 평균과 분산은 스칼라이다. 벡터 xi의 각 차원을 k라고 하였을 때, x(i,k)는 다음과 같이 정규화 할 수있다.

여기서 입실론은 분모가 0이되는 것을 방지하는 값이다.

γ(감마)와 β(베타) 벡터를 준비하자. 이들의 초깃값은 각각 1,0이다.

γ β를 도입한 층 정규화의 최종수식은 다음과 같다. γ β는 학습 가능한 파라미터이다.

 

인코더 구현하기

코딩을 살펴보자

 

def encoder_layer(dff,d_model,num_heads,dropout,name='encoder_layer'):
    inputs=tf.keras.Input(shape=(None,d_model),name='inputs')
    
    #인코더는 패딩 마스크 사용
    padding_mask=tf.keras.Input(shape=(1,1,None),name='padding_mask')
    
    #멀티-헤드 어텐션(첫번째 서브층/셀프 어텐션)
    attention=MultiHeadAttention(d_model,num_heads,name='attention')({'query':inputs,'key':inputs,'value':inputs,'mask':padding_mask})
    
    #드롭아웃+잔차 연결과 층 정규화
    attention=tf.keras.layers.Dropout(rate=dropout)(attention)
    attention=tf.keras.layers.LayerNormalization(epsilon=1e-6)(inputs+attention)
    
    
    #포지션 와이즈 피드 포워드 신경망(두번째 서브층)
    outputs=tf.keras.layers.Dense(units=dff,activation='relu')(attention)
    outputs=tf.keras.layers.Dense(units=d_model)(outputs)
    
    #드롭아웃+잔차 연결과 층 정규화
    outputs=tf.keras.layers.Dropout(rate=dropout)(outputs)
    outputs=tf.keras.layers.LayerNormalization(epsilon=1e-6)(attention+outputs)
    
    return tf.keras.Model(inputs=[inputs,padding_mask],outputs=outputs,name=name)

                   

인코더의 입력으로 들어가는 문장에는 패딩이 있을 수 있으므로, 어텐션 시 패딩 토큰을 제외하도록 패딩 마스크를 사용한다. 이는 MultiHeadAttention 함수의 mask의 인자값으로 padding_mask가 들어가는 이유이다.

인코더는 총 두 개의 서브층으로 이루어지는데, 멀티 헤드 어텐션과 피드 포워드 신경망이다. 각 서브층 이후에는 드롭 아웃, 잔차 연결과 층 정규화가 수행된다.

 

인코더 쌓기

인코더 층의 내부 아키텍처에 대해서 이해해보았다. 이러한 인코더 층을 num_layers개만큼 쌓고, 마지막 인코더 층에서 얻는 (seq_len,d_model)크기의 행렬을 디코더로 보내서 트랜스포머 인코더의 인코딩 연산이 끝나게 된다.

다음 코드는 인코더 층을 num_layers개만큼 쌓는 코드이다.

 

def encoder(vocab_size,num_layers,dff,d_model,num_heads,dropout,name='encoder'):
    inputs=tf.keras.Input(shape=(None,),name='inputs')
    
    #인코더는 패딩 마스크 사용
    padding_mask=tf.keras.Input(shape=(1,1,None),name='padding_mask')
    
    #포지셔널 인코딩+드롭아웃
    embeddings=tf.keras.layers.Embedding(vocab_size,d_model)(inputs)
    embeddings*=tf.math.sqrt(tf.cast(d_model,tf.float32))
    embeddings=PositionalEncoding(vocab_size,d_model)(embeddings)
    outputs=tf.keras.layers.Dropout(rate=dropout)(embeddings)
    
    #인코더를 num_layers개 쌓기
    for i in range(num_layers):
        outputs=encoder_layer(dff=dff,d_model=d_model,num_heads=num_heads,dropout=dropout,name='encoder_layer_{}'.format(i),)([outputs,padding_mask])
    
    return tf.keras.Model(inputs=[inputs,padding_mask],outputs=outputs,name=name)

 

인코더에서 디코더로(From Encoder To Decoder)

앞서 인코더는 총 num_layers만큼의 층 연산을 순차적으로 처리한 후, 마지막 층의 인코더의 출력을 디코더에 전달해준다. 인코더 연산이 끝났으므로, 디코더 연산이 시작되어 다시 디코더 또한 num_layers만큼의 연산을 한다. 

이때마다, 인코더가 보낸 출력을 각 디코더 층 연산에 사용한다. 

 

디코더의 첫번째 서브층: 셀프 어텐션과 룩 어헤드 마스크

위 그림과 같이 디코더도 동일하게 임베딩 층과 포지셔널 인코딩을 거친 후의 문장 행렬이 입력된다. 

트랜스포머 또한 seq2seq와 마찬가지로 교사 강요(Teacher Forcing)을 사용하여 훈련되므로 학습 과정에서 디코더는 번역할 문장에 해당되는 <sos> je suis etudiant의 문장 행렬을 한 번에 입력받는다. 그 후, 디코더는 이 문장 행렬로부터 각 시점의 단어를 예측하도록 훈련된다.

 

<문제점>

seq2seq의 디코더에 사용되는 RNN 계열의 신경망은 입력 단어를 매 시점마다 순차적으로 받으므로 다음 단어 예측에 현재 시점 이전에 입력된 단어들만 참고 할 수 있다.

이에 반해, 트랜스포머는 문장 행렬로 입력을 한번에 받으므로 현재 시점의 단오를 예측하고자 할 떄, 입력 문장 행렬로부터 미래시점의 단어까지도 참고할 수 있는 현상이 발생한다.

 

이 문제점을 해결하기 위해 트랜스포머의 디코더에서는 현재 시점의 예측에서 현재 시점보다 미래에 있는 단어들을 참고하지 못하도록 룩어헤드 마스크를 도입했다.

룩어헤드 마스크(look ahead mask)는 디코더의 첫번째 서브층에서 이루어진다.

디코더의 첫번째 서브층인 멀티 헤드 셀프어텐션층은 인코더의 첫번째 서브층인 멀티 헤드 셀프 어텐션 층과 동일한 연산을 한다. 차이점은 마스킹을 적용한다는 점이다. 

위 그림과 같이 셀프 어텐션을 통해 어텐션 스코어 행렬을 얻는다.

이후 자기 자신보다 미래에 있는 단어들은 참고하지 못하도록 마스킹 처리한다.

마스킹 된 후의 어텐션 스코어 행렬의 각 행을 보면 자기 자신과 그 이전 단어들만을 참고할 수 있음을 볼 수있다.

그 외에는 인코더의 첫 번째 서브층과 같다.

룩어헤드 마스크의 구현을 알아보면 패딩마스크와 마찬가지로 앞서 구현한 스케일드 닷 프로덕트 어텐션 함수에 mask라는 인자로 전달된다. 패딩 마스크를 써야하는 경우, 스케일드 닷 프로덕트 어텐션 함수에 패딩 마스크를 전달하고, 룩어헤드 마스킹을 써야 하는 경우에는 스케일드 닷 프로덕트 어텐션 함수에 룩어헤드 마스크를 전달하게 된다.

 

 

트랜스포머에는 총 3가지 어텐션이 존재한다. 모두 멀티 헤드 어텐션을 수행하고, 멀티 헤드 어텐션 함수 내부에서 스케일드 닷 프로덕트 어텐션 함수를 호출하는데 각 어텐션 시 함수에 전달하는 마스킹은 다음과 같다.

-인코더의 셀프 어텐션: 패딩마스크 전달

-디코더의 첫번째 서브층인 마스크드 셀프 어텐션: 룩어헤드 마스크를 전달

-디코더의 두번째 서브층인 인코더-디코더 어텐션: 패딩 마스크를 전달

 

이때, 룩어헤드 마스크를 한다고 해서 패딩 마스크가 불필요한 것이 아니므로 룩어헤드 마스크는 패딩 마스크를 포함할 수 있도록 구현한다. 

 

#디코더의 첫번쨰 서브층(sublayer)에서 미래 토큰을 Mask하는 함수
def create_look_ahead_mask(x):
    seq_len=tf.shape(x)[1]
    look_ahead_mask=1-tf.linalg.band_part(tf.ones((seq_len,seq_len)),-1,0)
    padding_mask=create_padding_mask(x)
    return tf.maximum(look_ahead_mask,padding_mask)

print(create_look_ahead_mask(tf.constant([[1,2,0,4,5]])))

tf.Tensor(
[[[[0. 1. 1. 1. 1.]
   [0. 0. 1. 1. 1.]
   [0. 0. 1. 1. 1.]
   [0. 0. 1. 0. 1.]
   [0. 0. 1. 0. 0.]]]], shape=(1, 1, 5, 5), dtype=float32)

룩어헤드 마스크이므로 삼각형 모양의 마스킹이 형성되면서, 패딩 마스크가 포함되어져 있으므로 세번째 열이 마스킹 되었다.

 

디코더의 두번째 서브층: 인코더 디코더 어텐션

디코더의 두번째 서브층은 멀티 헤드 어텐션을 수행한다는 점에서는 이전의 어텐션들과 같지만, 셀프 어텐션은 아니다.

셀프 어텐션은 Query,Key,Value가 같은 경우를 말하는데, 인코더-디코더 어텐션은 Query가 디코더인 행렬인 반면, Key와 Value는 인코더 행렬이기 때문이다.

디코더의 두번째 서브층을 확대해보면, 위와 같은 그림이다.

두 개의 (빨간)화살표는 각각 Key,Value를 의미하며, 이는 인코더의 마지막 층에서 온 행렬로부터 얻는다.

반면, Query는 디코더의 첫번쨰 서브층의 결과 행렬로부터 얻는다는 점이 다르다.

Query가 디코더 행렬, Key가 인코더 행렬일 때, 어텐션 스코어 행렬을 구하는 과정은 아래 그림과 같다.

 

디코더 구현하기

우선 코딩을 살펴보자.

def decoder_layer(dff,d_model,num_heads,dropout,name='decoder_layer'):
    inputs=tf.keras.Input(shape=(None,d_model),name='inputs')
    enc_outputs=tf.keras.Input(shape=(None,d_model),name='encoder_outputs')
    
    #룩어헤드 마스크(첫번째 서브층)
    look_ahead_mask=tf.keras.Input(shape=(1,None,None),name='look_ahead_mask')
    
    #패딩 마스크(두번째 서브층)
    padding_mask=tf.keras.Input(shape=(1,1,None),name='padding_mask')
    
    #멀티-헤드 어텐션(첫번째 서브층/마스크드 셀프 어텐션)
    attention1=MultiHeadAttention(d_model,num_heads,name='attention_1')(inputs={'query':inputs,'key':inputs,'value':inputs,'mask':look_ahead_mask})
    #잔차 연결과 층 정규화
    attention1=tf.keras.layers.LayerNormalization(epsilon=1e-6)(attention1+inputs)
    
    #멀티-헤드 어텐션(두번째 서브층/디코더-인코더 어텐션)
    attention2=MultiHeadAttention(d_model,num_heads,name='attention_2')(inputs={'query':attention1,'key':enc_outputs,'value':enc_outputs,'mask':padding_mask})
    #드롭아웃+잔차 연결과 층 정규화
    attention2=tf.keras.layers.Dropout(rate=dropout)(attention2)
    attention2=tf.keras.layers.LayerNormalization(epsilon=1e-6)(attention2+attention1)
    
    #포지션 와이즈 피드 포워드 신경망(세번째 서브층)
    outputs=tf.keras.layers.Dense(units=dff,activation='relu')(attention2)
    outputs=tf.keras.layers.Dense(units=d_model)(outputs)
    
    #드롭아웃+잔차 연결과 층 정규화
    outputs=tf.keras.layers.Dropout(rate=dropout)(outputs)
    outputs=tf.keras.layers.LayerNormalization(epsilon=1e-6)(outputs+attention2)
    
    return tf.keras.Model(inputs=[inputs,enc_outputs,look_ahead_mask,padding_mask],outputs=outputs,name=name)

 

디코더는 총 세개의 서브층으로 구성된다. 첫번째와 두번째 서브층 모두 멀티 헤드 어텐션이지만, 첫번째 서브층은 mask의 인자값으로 look_ahead_mask가 들어간다.

반면, 두번째 서브층은 mask의 인자값으로 padding_mask가 들어가는 것을 확인할 수 있다.

이유는 첫번째 서브층은 마스크드 셀프 어텐션을 수행하기 때문이다. 세개의 서브층 모두 서브층 연산 후에는 드롭 아웃, 잔차 연결, 층 정규화가 수행되는 것을 알 수 있다.

 

디코더 쌓기

def decoder(vocab_size,num_layers,dff,d_model,num_heads,dropout,name='decoder'):
    inputs=tf.keras.Input(shape=(None,),name='inputs')
    enc_outputs=tf.keras.Input(shape=(None,d_model),name='encoder_outputs')
    
    #디코더는 룩어헤드 마스크(첫번째 서브층)와 패딩 마스크(두번째 서브층) 둘 다 사용.
    look_ahead_mask=tf.keras.Input(shape=(1,None,None),name='look_ahead_mask')
    padding_mask=tf.keras.Input(shape=(1,1,None),name='padding_mask')
    
    #포지셔널 인코딩 +드롭아웃
    embeddings=tf.keras.layers.Embedding(vocab_size,d_model)(inputs)
    embeddings*=tf.math.sqrt(tf.cast(d_model,tf.float32))
    embeddings=PositionalEncoding(vocab_size,d_model)(embeddings)
    outputs=tf.keras.layers.Dropout(rate=dropout)(embeddings)
    
    #디코더를 num_layers개 쌓기
    for i in range(num_layers):
        outputs=decoder_layer(dff=dff,d_model=d_model,num_heads=num_heads,dropout=dropout,name='decoder_layer_{}'.format(i),)(inputs=[outputs,enc_outputs,look_ahead_mask,padding_mask])
    return tf.keras.Model(inputs=[inputs,enc_outputs,look_ahead_mask,padding_mask],outputs=outputs,name=name)

 

이 또한 앞선 인코더 쌓기와 동일하다.

트랜스포머 전체 구현하기

def transformer(vocab_size,num_layers,dff,d_model,num_heads,dropout,name='transformer'):
    #인코더 입력
    inputs=tf.keras.Input(shape=(None,),name='inputs')
    
    #디코더 입력
    dec_inputs=tf.keras.Input(shape=(None,),name='dec_inputs')
    
    #인코더의 패딩 마스크
    enc_padding_mask=tf.keras.layers.Lambda(create_padding_mask,output_shape=(1,1,None),name='enc_padding_mask')(inputs)
    
    #디코더의 룩어헤드 마스크(첫번째 서브층)
    look_ahead_mask=tf.keras.layers.Lambda(create_look_ahead_mask,output_shape=(1,None,None),name='look_ahead_mask')(dec_inputs)
    
    #디코더의 패딩 마스크(두번째 서브층)
    dec_padding_mask=tf.keras.layers.Lambda(create_padding_mask,output_shape=(1,1,None),name='dec_padding_mask')(inputs)
    
    #인코더의 출력은 enc_outputs. 디코더로 전달하는 과정
    enc_outputs=encoder(vocab_size=vocab_size,num_layers=num_layers,dff=dff,d_model=d_model,num_heads=num_heads,dropout=dropout)(inputs=[inputs,enc_padding_mask])
    
    #디코더의 출력은 de_outputs, 출력층으로 전달하는 과정
    dec_outputs=decoder(vocab_size=vocab_size,num_layers=num_layers,dff=dff,d_model=d_model,num_heads=num_heads,dropout=dropout)(inputs=[dec_inputs,enc_outputs,look_ahead_mask,dec_padding_mask])
    
    #다음 단어 예측을 위한 출력층
    outputs=tf.keras.layers.Dense(units=vocab_size,name='outputs')(dec_outputs)
    return tf.keras.Model(inputs=[inputs,dec_inputs],outputs=outputs,name=name)

 

트랜스포머 하이퍼파라미터 정하기

앞선 과정까지는 전체 모델에 대한 이해와 이 모델을 직접 파이썬으로 구현하는 과정을 알아보았다. 

이제는 학습을 시키기 위해 하이퍼파라미터를 지정해보자.

단어 집합의 크기는 임의로 9000으로 정한다. 단어 집합의 크기로부터 룩업 테이블을 수행할 임베딩 테이블과 포지셔널 인코딩 행렬의 행의 크기를 결정할 수 있다.

 

여기서는 논문에 제시한 것과는 조금 다르게 하이퍼파라미터를 정해본다. 인코더와 디코더의 층의 개수 num_laters는 4개, 인코더와 디코더의 포지션 와이즈 피드 포워드 신경망은 은닉층 d(ff) 128, d(model) 128, 멀티-헤드 어텐션은 병렬적 사용으로 num_heads 4로 지정해본다. 

이렇게 된다면 d(v)는 128/4로 32가 되겠다

 

small_transformer=transformer(
vocab_size=9000,
num_layers=4,
dff=512,
d_model=128,
num_heads=4,
dropout=0.3,
name='small_transformer')

tf.keras.utils.plot_model(small_transformer,to_file='C:/data/small_transformer.png',show_shapes=True)

 

손실 함수 정의하기

다중 클래스 분류 issue를 해결할 것이므로, cross_entropy 를 loss_function으로 정의하겠다.

def loss_function(y_true,y_pred):
    y_true=tf.reshape(y_true,shape=(-1,MAX_LENGTH-1))
    
    loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True,reduction='none')(y_true,y_pred)
    mask=tf.cast(tf.not_equal(y_ture,0),tf.float32)
    loss=tf.multiply(loss,mask)
    
    return tf.reduce_mean(loss)

 

학습률

트랜스포머의 경우 learning rate는 고정된 값을 유지하는 것이 아닌, 학습 경과에 따라 변하도록 설계되었다. 

위 공식으로 학습률을 계산하여 사용하며, warmup_steps의 값으로는 4000을 정의하였다.

class CustomSchedule(tf.keras.optimizers.schedules.LearningRateSchedule):
    def __init__(self,d_model,warmup_steps=4000):
        super(CustomSchedule,self).__init__()
        self.d_model=d_model
        self.d_model=tf.cast(self.d_model,tf.float32)
        self.warmup_steps=warmup_steps
        
    def __call__(self,step):
        arg1=tf.math.rsqrt(step)
        arg2=step*(self.warmup_steps**-1.5)
        
        return tf.math.rsqrt(self.d_model)*tf.math.minimum(arg1,arg2)
    
sample_learning_rate=CustomSchedule(d_model=128)
plt.plot(sample_learning_rate(tf.range(200000,dtype=tf.float32)))
plt.ylabel('Learning Rate')
plt.xlabel('Train Step')

Text(0.5, 0, 'Train Step')

이후에는 이 트랜스포머 모델을 활용해 챗봇을 만들어 보겠다.

 

 

[출처]

wikidocs.net/31379

'Deep Learning > NLP' 카테고리의 다른 글

01) 토큰화(Tokenization)  (0) 2021.03.10
트랜스포머(Transformer)(1)  (0) 2021.03.04
Transfer Learning(전이 학습)  (0) 2021.03.02

Transformer는 2017년 구글이 발표한 논문인 'Attention is all you need'에서 나온 모델이다. 

기존의 seq2seq 구조인 encoder-decoder를 따르면서도 논문의 이름처럼 어텐션 기법만으로 구현한 모델이다.

이 모델은 RNN을 사용하지 않고, 단순히 encoder-decoder 구조를 설계하였음에도 RNN보다 성능이 우수하다고 나왔다.

 

원논문은 다음 링크를 타고 확인해보자.

arxiv.org/abs/1706.03762

 

Attention Is All You Need

The dominant sequence transduction models are based on complex recurrent or convolutional neural networks in an encoder-decoder configuration. The best performing models also connect the encoder and decoder through an attention mechanism. We propose a new

arxiv.org

기존의 seq2seq 모델의 한계

트래느포머에 대해 알아보기 전에 기존의 seq2seq모델을 먼저 알아보자. 기존 seq2seq모델은 인코더-디코더 구조로 구성되어 있다. 

인코더가 하는 일: input sequence를 하나의 벡터로 압축

디코더가 하는 일: 이 벡터 표현을 통해 output sequence를 만들어낸다.

 

단점: 인코더가 하나의 input sequence를 하나의 벡터로 압축하는 과정에서 input sequence의 정보가 일부 손실된다.

 

위 단점을 보완하기 위해 어텐션 기법이 사용되었다. 

 

그렇다면 RNN을 보정하기 위한 용도로 사용하지 않고, 직접 어텐션으로 인코더와 디코더를 사용한다면 어떻게 될까

 

Transformer의 Main Hyperparameter

트랜스포머의 하이퍼파라미터를 정의해보자. 각 하이퍼파라미터의 의미에 대해서는 후에 설명하고, 간단하게만 이해해보자. 아래 정의되는 수치값은 트랜스포머를 제안한 논문에 기재된 수치값이다. 그러나 사용자가 모델 설계시 임의로 변경가능한 값이다.

 

d(model) =512

트랜스포머의 인코더와 디코더에서 정해진 입력과 출력의 크기를 의미한다. 임베딩 벡터의 차원 또한 d(model)이며, 각 인코더와 디코더가 다음 층의 인코더와 디코더로 값을 보낼 때에도 이 차원을 유지한다.

 

num_layers=6

트랜스포머에서 하나의 인코더와 디코더를 층으로 생각하였을 때, 트랜스포머 모델에서 인코더와 디코더가 총 몇 층으로 구성되었는지를 의미한다.

 

num_heads=8

트랜스포머에서는 어텐션을 사용할 때, 1번 하는 것 보다 여러 개로 분할해서 병렬로 어텐션을 수행하고 결과값을 다시 하나로 합치는 방식을 선택했다. 이때의 병렬 개수를 의미한다.

d(ff)=2048

트랜스포머 내부에는 feed forward neural network가 존재한다. 이때 hiddenlayer의 크기를 의미한다.

*feed forward neural network의 input layer, output layer의 크기는 d(model)이다. 

 

트랜스포머(Transformer)

기본 구조

트랜스포머는 RNN을 사용하지 않지만 기존의 seq2seq처럼 인코더에서 입력 시퀀스를 입력받고, 디코더에서 출력 시퀀스를 출력하는 인코더-디코더 구조를 유지한다. 

하지만 트랜스포머와 seq2seq의 차이점은 encoder, decoder 단위가 N개 존재 할 수 있다는 것이다.

즉, 이전 seq2seq 구조에서는 encoder,decoder에서 각각 하나의 RNN이 t개의 시점(time_step)을 가지는 구조였다면, 이번에는 encoder,decoder단위가 N개로 구성되는 구조이다. 

논문에서는 encoder,decoder 개수를 6개씩 사용했다.

트랜스포머 6x6 구조

위 그림은 encoder,decoder가 6개씩 존재하는 트랜스포머 구조이다.  

출력결과를 나타내는 트랜스포머구조

위 그림은 encoder로부터 정보를 전달받아 출력 결과를 만들어내는 트랜스포머 구조를 보여준다. decoder는 seq2seq구조처럼 시작 심볼<sos>를 입력으로 받아 종료 심볼<eos>가 나올 때까지 연산을 진행한다. 

RNN은 사용되지 않지만 여전히 encoder-decoder구조는 유지되고 있음을 보여준다.

 

그렇다면 이제 전체적인 큰 흐름은 확인했으니 내부 구조를 확인해보자.

 

포지셔널 인코딩(Positional Encoding)

내부를 이해하기에 앞서, 트랜스포머 입력을 알아보자. RNN이 자연어 처리에 유용했던 이유는 단어 위치에 따라 순차적으로 입력받아서 처리하는 RNN의 특성으로 인해 각 단어의 위치정보를 가질 수 있었다.

 

이와는 다르게 트랜스포머는 단어 입력을 순차적으로 받는 방식이 아니므로, 단어의 위치 정보를 다른 방식으로 알려줄 필요가 있다. 이 정보를 얻기 위해 각 단어의 임베딩 벡터에 위치 정보들을 더하여 모델의 입력으로 사용하는데, 이를 포지셔널 인코딩(Positional Encoding)이라고 한다.

 

Positional Encoding 과정

위 그림은 입력으로 사용되는 임베딩 벡터들이 트랜스포머의 입력으로 사용되기 전에 포지셔널 인코딩 값이 더해지는 것을 보여준다. 

위 그림이 실제 임베딩 벡터가 입력으로 사용되기 전에 포지셔널 인코딩값이 더해지는 과정을 시각화한 그림이다.

 

포지셔널 인코딩 값들은 어떤 값이기에 위치 정보를 반영해준다는 것일까? 다음 두가지 함수를 보자.

위 sin,cos함수는 위아래로 순환하여 요동친다. 즉, 이 함수를 이용해 임베딩벡터에 더해줌으로써, 단어의 순서 정보를 더하여준다. 

pos는 입력 문장에서의 임베딩 벡터의 위치를 나타내며, i는 임베딩 벡터 내의 차원의 인덱스를 의미한다. 각 차원의 인덱스가 짝수인 경우에는 sin함수의 값을, 홀수인 경우에는 cos함수의 값을 사용한다. 

위 식에서 d(model)은 트랜스포머의 모든 층의 출력 차원을 의미하는 하이퍼파라미터이다. 위에서 단어의 정의를 언급하였듯이 계속 나오게 될것이므로 기억해두자.

 

위와 같은 포지셔널 인코딩 방법을 사용하면 순서 정보가 보존되는데, 예를 들어 각 임베딩 벡터에 포지셔널 인코딩 값을 더하면 같은 단어라 하더라도 문장 내의 위치에 따라서 트랜스포머의 입력으로 들어가는 임베딩 벡터의 값이 달라진다.

코드로 살펴보자.

 

class PositionalEncoding(tf.keras.layers.Layer):
    def __init__(self,position,d_model):
        super(PositionalEncoding,self).__init__()
        self.pos_encoding=self.positional_encoding(position,d_model)
        
    def get_angles(self,position,i,d_model):
        angles=1/tf.pow(10000,(2*(i//2))/tf.cast(d_model,tf.float32))
        return position*angles
    
    def positional_encoding(self,position,d_model):
        angle_rads=self.get_angles(position=tf.range(position,dtype=tf.float32)[:,tf.newaxis],i=tf.range(d_model,dtype=tf.float32)[tf.newaxis,:],d_model=d_model)
        #배열의 짝수 인덱스(2i) 사인함수
        sines=tf.math.sin(angle_rads[:,0::2])
        #배열의 홀수 인덱스(2i+1) 코사인함수
        cosines=tf.math.cos(angle_rads[:,1::2])
        
        angle_rads=np.zeros(angle_rads.shape)
        angle_rads[:,0::2]=sines
        angle_rads[:,1::2]=cosines
        pos_encoding=tf.constant(angle_rads)
        pos_encoding=pos_encoding[tf.newaxis,...]
        
        print(pos_encoding.shape)
        return tf.cast(pos_encoding,tf.float32)
    
    def call(self,inputs):
        return inputs+self.pos_encoding[:,:tf.shape(inputs)[1],:]

위 클래스를 이용해 포지셔널인코딩을 정리하였다. 여기서 가장 주의깊게 봐야할 부분은 짝수,홀수일때 어떤 함수를 사용하는가이다.

 

sample_pos_encoding=PositionalEncoding(50,128)
plt.pcolormesh(sample_pos_encoding.pos_encoding.numpy()[0],cmap='RdBu')
plt.xlabel('Depth')
plt.xlim((0,128))
plt.ylabel('Position')
plt.colorbar()
plt.show()

50x128의 크기를 가진 포지셔널 인코딩 행렬을 시각화하여 어떤 형태를 가지는지 확인해보자.

 

(1, 50, 128)

단어가 50개이면서, 각 단어가 128차원의 임베딩 벡터를 가질 때 사용할 수 있는 행렬이다. 좀 더 쉽게 설명하면, '관돌'이라는 단어가 가질 수 있는 고유 숫자(벡터)는 128개라고 이해해도 괜찮다고 본다.

 

어텐션(Attention)

트랜스포머에서 사용되는 3가지 어텐션에 대해 간단하게 정리하자. 

Self-Attention은 인코더에서 이루어지는 반면에 Masked Decoder Self-Attention , Encoder-Decoder Attention은 디코더에서 이루어진다. 셀프 어텐션은 본질적으로 Query,Key,Value가 동일한 경우를 말한다. 반면, 세번째 그림 인코더-디코드 어텐션에서는 Query가 디코더의 벡터인 반면에 Key,Value가 인코더의 벡터이므로 셀프 어텐션이라 불리지 않는다.

*Query,Key 등이 같다는 것은 벡터의 값이 같다는 것이 아닌, 벡터의 출처가 같다는 의미이다.

전체적인 트랜스포머의 구조이다. 여기서 Multi-head라고 추가가 되었는데, 이는 간단하게 병렬수행으로 생각해두자. 뒤에 다시 언급할 것이다.

 

인코더(Encoder)

앞서 말했듯이 트랜스포머는 하이퍼파라미터인 num_layers 개수의 인코더 층을 쌓는다. 총 6개의 인코더 층을 사용하였다. 

인코더를 하나의 층이라는 개념으로 생각해본다면, 그 층은 크게 총 2개의 sublayer로 나뉜다. 이 sublayer는 셀프 어텐션과 피드 포워드 신경망이다. 위 그림에서 FFNN과 Self-Attention이라고 적혔는데, 멀티 해드 어텐션은 셀프 어텐션을 병렬적으로 사용한다는 의미고, Position-wise FFNN은 일반적인 FFNN이다. 

우선 셀프 어텐션에 대해서 먼저 이해해보자.

 

Self-Attention

트랜스포머에서는 셀프 어텐션이라는 어텐션 기법이 등장하는데, 어텐션 기법에 대해서는 추후 다루도록 하겠다.

1) 셀프 어텐션의 의미와 이점

어텐션 함수는 주어진 query에 대해서 모든 key와의 유사도를 각각 구한다. 그리고 구해낸 이 유사도를 가중치로 하여 키와 맵핑되어있는 각각의 value에 반영해준다. 그 후, 유사도가 반영된 value를 모두 가중합하여 return 한다.

셀프 어텐션에 대한 대표적인 효과를 이해해보자.

구글 AI블로그 포스트

위 그림에서 'The animal didn't cross the street because it was too tired'라는 글을 해석해보면 '그 동물은 그것이 너무 힘들어서 길을 건너지 않았다' 이다. 여기서 it 이 의미하는 건 무엇일까? 'street' 혹은 'animal'중 하나일 것이다. 이 셀프 어텐션을 이용하면 결국 animal에 더 연관되어 있다는 즉, 확률이 높다는 것을 찾아내 준다.

 

이제 트랜스포머에서의 셀프 어텐션의 동작 메커니즘을 알아보자.

 

2) Q,K,V 벡터 얻기

셀프 어텐션은 입력 문장의 단어 벡터들을 가지고 수행한다고 하였는데, 사실 어텐션은 인코더의 초기 입력인 d(model)의 차원을 가지는 단어 벡터들을 사용하여 셀프 어텐션을 수행하는 것이 아닌, 각 단어의 벡터들로부터 Q,K,V벡터를 얻는 작업을 거친다. 

이때 Q,K,V벡터들은 초기 입력인 d(model)의 차원을 가지는 단어 벡터들보다 더 작은 차원을 가지는데, 논문에서는 d(model)=512의 차원을 가졌던 각 단어 벡터들을 64의 차원을 갖는 Q,K,V벡터로 변환하였다.

 

여기서, 64라는 값은 트랜스포머의 또 다른 하이퍼파라미터인 num_heads로 인해 결정된다. 트랜스포머는 d(model_을 num_heads로 나누 값을 각 Q,K,V벡터의 차원으로 결정한다. 논문에서는 num_heads를 8로 설정하였다.

Q,K,V벡터 변환 과정

기존 벡터로부터 더 작은 벡터는 가중치 행렬을 곱하므로써 완성된다. 

 

3) Scaled dot-product Attention

Q,K,V 벡터를 얻었다면 기존 어텐션 메커니즘과 동일하다. 각 Q벡터는 모든 K벡터에 대해서 어텐션 스코어를 구하고, 어텐션 분포를 구한 뒤에 이를 사용해, 모든 V벡터를 가중합하여 어텐션 벨류 혹은 컨텍스트 벡터를 구하게 된다. 그리고 이를 모든 Q 벡터에 대해서 반복한다.

 

그러나 어텐션 함수의 종류는 다양하다. 트랜스포머에서는 내적을 사용하는 어텐션 함수

dot-product attention

가 아닌, 특정 값으로 나눠즈는 어텐션함수를 사용한다.

Scaled dot-product Attention

그림을 통해 확인해보자.

 

우선 단어 I에 대한 Q벡터를 살펴보자. 이 과정은 am에 대한 Q벡터, a에 대한 Q벡터, student에 대한 Q벡터에 대해서도 모드 동일한 과정을 거친다. 

위 그림에서 어텐션 스코어는 각각 단어 I가 단어 I,am,a,student와 얼마나 연관되어 있는지를 보여주는 수치이다. 트랜스포머에서는 두 벡터의 내적값을 스케일링하는 값으로 K벡터의 차원을 나타내는 d(k)에 루트를 씌워 사용하였다. 논문에서 d(k)는 d(model)/num_heads라는 식에 따라서 64의 값을 가지므로 루트를 씌운 d(k)는 8의 값을 갖는다.

 

다음은 어텐션 스코어에 softmax함수를 사용하여 어텐션 분포를 구하고, 각 V벡터와 가중합하여 Attention Value를 출력한다. 이를 단어 I에 대한 어텐션 값 또는 단어 I에 대한 context vector 라고도 부른다. 과연 이 간단한 문장에서는 하나하나 따로 연산을 하는게 큰 문제는 없어보이지만, 많은 양의 문장을 사용할 시에는 과연 따로 연산하는게 효율적일까?

 

4)행렬 연산으로 일괄 처리

위 과정을 벡터 연산이 아닌 행렬 연산을 사용하면 일괄 계산이 가능하다. 위 과정을 행렬 연산으로 다시 확인해보자.

위 그림에서 Q 행렬을 K행렬로 전치한 행렬과 곱해준다고 해보자. 이렇게 된다면 각각의 단어의 Q벡터와 K벡터의 내적이 각 행렬의 원소가 되는 행렬이 결과로 나온다.

다시 말해 위 그림의 결과 행렬의 값에 전체적으로 d(k)에 루트를 씌운 값으로 나누어주면 이는 각 행과 열이 어텐션 스코어 값을 가지는 행렬이 된다. I 행과 student 열의 값은 I의 Q벡터와 student의 K벡터의 어텐션 스코어와 동일한 행렬이 된다는 의미이다. 

위 Attention score matrix를 구하였다면 남은 것은 Attention distribution을 구하고, 이를 사용해 모든 단어에 대한 Attention value를 구하면된다. 이를 위해 score matrix에 softmax함ㅅ수를 사용하고, V행렬을 곱하면 된다.

과정은 다음과 같다.

5)Scaled dot-product attention 구하기

코드를 먼저 살펴보자.

def scaled_dot_product_attention(query,key,value,mask):
	# query 크기 : (batch_size, num_heads, query의 문장 길이, d_model/num_heads)
    # key 크기 : (batch_size, num_heads, key의 문장 길이, d_model/num_heads)
    # value 크기 : (batch_size, num_heads, value의 문장 길이, d_model/num_heads)
  	# padding_mask : (batch_size, 1, 1, key의 문장 길이)

	# Q와 K의 곱. 어텐션 스코어 행렬.
    matmul_qk=tf.matmul(query,key,transpose_b=True)
    
    # 스케일링
  	# dk의 루트값으로 나눠준다.
    depth=tf.cast(tf.shape(key)[-1],tf.float32)
    logits=matmul_qk/tf.math.sqrt(depth)
    
    # 마스킹. 어텐션 스코어 행렬의 마스킹 할 위치에 매우 작은 음수값을 넣는다.
  	# 매우 작은 값이므로 소프트맥스 함수를 지나면 행렬의 해당 위치의 값은 0이 된다.
    if mask is not None:
        logits+=(mask*-1e9)
        
    # 소프트맥스 함수는 마지막 차원인 key의 문장 길이 방향으로 수행된다.
  	# attention weight : (batch_size, num_heads, query의 문장 길이, key의 문장 길이)
    attention_weights=tf.nn.softmax(logits,axis=-1)
    
    # output : (batch_size, num_heads, query의 문장 길이, d_model/num_heads)
    output=tf.matmul(attention_weights,value)
    return output,attention_weights

 

앞서 설명한것을 다 이해하였다면 위 코드를 해석하는데는 큰문제가 없을것이라본다. 이제 테스를 해보자.

 

np.set_printoptions(suppress=True)
temp_k=tf.constant([[10,0,0],
                   [0,10,0],
                   [0,0,10],
                   [0,0,10]],dtype=tf.float32)
temp_v=tf.constant([[1,0],
                   [10,0],
                   [100,5],
                   [1000,6]],dtype=tf.float32)
temp_q=tf.constant([[0,10,0]],dtype=tf.float32)

temp_out,temp_attn=scaled_dot_product_attention(temp_q,temp_k,temp_v,None)
print(temp_attn)
print(temp_out)


tf.Tensor([[0. 1. 0. 0.]], shape=(1, 4), dtype=float32)
tf.Tensor([[10.  0.]], shape=(1, 2), dtype=float32)

 

6)멀티 헤드 어텐션(Multi-head Attention)

앞서 본 어텐션은 d(model)의 차원을 가진 단어 벡터를 num_heads로 나눈 차원을 가지는 Q,K,V벡터로 바꾸고 어텐션을 수행하였다. 논문 기준 512 차원의 각 단어 벡터를 8로 나누고 64차원의 Q,K,V벡터로 바꾸어 어텐션을 수행한 것이다. 

이번에는 num_heads의 의미와 왜 d(model)의 차원을 가진 단어 벡터를 가지고 어텐션을 하지 않고 차원을 축소시킨 벡터로 어텐션을 수행했는지 살펴보자.

 

트랜스포머 연구진은 한 번의 어텐션을 하는 것보다 여러번의 어텐션을 병렬구조로 사용하는 것이 더 효과적이라 판단했다. 그래서 d(model)의 차원을 num_heads개로 나누어 d(model)/num_heads의 차원을 가지는 Q,K,V에 대해서 num_heads개의 병렬 어텐션을 수행하였다. 

 

그렇다면 병렬 어텐션으로 얻을 수 있는 효과는 무엇이 있을까? 어텐션을 병렬로 수행하여 다른 시각으로 정보들을 수집할 수 있다.

예를들어 앞서 말한 '그 동물은 길을 건너지 않았다. 왜냐하면 그것은 너무 피곤하였기 때문이다'를 살펴보면 단어인 it이 쿼리였다고 해보자. 즉 it에 대한 Q벡터로부터 다른 단어와의 연관도를 구하였을 때 첫 번째 어텐션 헤드는 'it'과 'animal'의 연관도를 높게 본다면, 두 번째 어텐션은 'it'과 'tired'를 높게 볼 수있다. 즉, 다각도로 볼 수 있다는 장점이 있다는 것이다.

병렬 어텐션을 모두 수행하였다면, 모든 어텐션 헤드를 연결(concatenate)한다. 모두 연결된 어텐션 헤드 행렬의 크기는 (seq_len,d(model))가 된다.

 

어텐션 헤드를 모두 연결한 행렬은 또 다른 가중치 행렬 W(a)를 곱하게 되는데 이렇게 나온 결과 행렬이 멀티-헤드 어텐션의 최종 결과물이다. 

인코더의 첫번째 서브픙인 멀티-헤드 어텐션 단계를 끝마쳤을 때, 인코더의 입력으로 들어왔던 행렬의 크기가 아직 유지되고 있음을 기억해두자. 첫 번째 서브층인 멀티-헤드 어텐션과 두 번째 서브층인 포지션 와이즈 피드 포워드 신경망을 지나면서 인코더의 입력으로 들어올 때의 행렬의 크기는 계속 유지되어야 한다. 트랜스포머는 다수의 인코더를 쌓은 형태인데, 인코더에서의 입력의 크기가 출력에서도 동일한 크기로 계속 유지되어야만 다음 인코더에서도 다시 입력이 될 수 있기 때문이다.

 

7)멀티 헤드 어텐션(Multi-head Attention)구현하기

코드를 살펴보자.

class MultiHeadAttention(tf.keras.layers.Layer):
    def __init__(self,d_model,num_heads,name='multi_head_attention'):
        super(MultiHeadAttention,self).__init__(name=name)
        self.num_heads=num_heads
        self.d_model=d_model
        
        assert d_model%self.num_heads==0
        
        #d_model을 num_heads로 나눈 값.
        #논문 기준 64
        self.depth=d_model//self.num_heads
        
        #WQ,WK,WV에 해당하는 밀집층 정의
        self.query_dense=tf.keras.layers.Dense(units=d_model)
        self.key_dense=tf.keras.layers.Dense(units=d_model)
        self.value_dense=tf.keras.layers.Dense(units=d_model)
        
        #WQ에 해당하는 밀집층 정의
        self.dense=tf.keras.layers.Dense(units=d_model)
    
    #num_heads의 개수만큼 q,k,v를 나누는 함수
    def split_heads(self,inputs,batch_size):
        inputs=tf.reshape(inputs,shape=(batch_size,-1,self.num_heads,self.depth))
        return tf.transpose(inputs,perm=[0,2,1,3])
    
    
    def call(self,inputs):
        query,key,value,mask=inputs['query'],inputs['key'],inputs['value'],inputs['mask']
        batch_size=tf.shape(query)[0]
        
        #1.WQ,WK,WV에 해당하는 밀집층 지나기
        #q:(batch_size,query의 문장 길이,d_model)
        #k:(batck_size,key의 문장 길이,d_model)
        #v:(batch_size,value의 문장 길이,d_model)
        #참고) 인코더(k,v)-디코더(q)어텐션에서는 query길이와 key,value의 길이는 다를 수 있다.
        query=self.query_dense(query)
        key=self.key_dense(key)
        value=self.value_dense(value)
        
        #2. 헤드 나누기
        # q : (batch_size, num_heads, query의 문장 길이, d_model/num_heads)
    	# k : (batch_size, num_heads, key의 문장 길이, d_model/num_heads)
    	# v : (batch_size, num_heads, value의 문장 길이, d_model/num_heads)
        query=self.split_heads(query,batch_size)
        key=self.split_heads(key,batch_size)
        value=self.split_heads(value,batch_size)
        
        # 3. 스케일드 닷 프로덕트 어텐션. 앞서 구현한 함수 사용.
    	# (batch_size, num_heads, query의 문장 길이, d_model/num_heads)
        scaled_attention,_=scaled_dot_product_attention(query,key,value,mask)
        scaled_attention=tf.transpose(scaled_attention,perm=[0,2,1,3])
        
        # 4. 헤드 연결(concatenate)하기
    	# (batch_size, query의 문장 길이, d_model)
        concat_attention=tf.reshape(scaled_attention,(batch_size,-1,self.d_model))
        
        # 5. WO에 해당하는 밀집층 지나기
    	# (batch_size, query의 문장 길이, d_model)
        outputs=self.dense(concat_attention)
        return outputs

 

8)패딩마스크부터는 다음 포스팅에서 이어가겠다.

 

 

[자료 출처]

wikidocs.net/31379

'Deep Learning > NLP' 카테고리의 다른 글

01) 토큰화(Tokenization)  (0) 2021.03.10
트랜스포머(Transformer)(2)  (0) 2021.03.04
Transfer Learning(전이 학습)  (0) 2021.03.02

어느 갱신 방법을 이용할 것인가?

 

앞선 포스팅에 갱신 방법을 4가지를 알아봤다.

최적화 기법 비교: SGD, 모멘텀, AdaGrad, Adam

위 그림만 본다면 AdaGrad가 가장 나은 듯 보인다. 그러나 결과는 어떤 issue를 해결해야 하는가에 따라 달라지므로 주의해야 한다.

또한, 학습률 등의 하이퍼파라미터를 어떻게 설정하느냐에 따라서도 결과가 바뀐다. 

지금도 많은 연구에서 SGD를 사용한다. 또한, Momentum 과 AdaGrad도 시도해 볼만한 가치가 충분하다.

 

MNIST 데이터셋에 대한 학습 진도 비교

위 그림은 MNIST예제에서 사용해봤을때 얼마나 다른지를 비교해 본 것이다. 이 실험은 각 층이 100개의 뉴런으로 구성된 5층 신경망에서 ReLU를 활성화 함수로는 사용해 측정한 것이다.

 

 

가중치의 초기값

신경망 학습에서 특히 중요한 것이 가중치의 초깃값이다. 가중치의 초깃값을 무엇으로 설정하느냐가 신경망 학습의 성패가 갈린다고 해도 무방하다. 

초깃값을 0으로 한다면?

오버피팅을 억제해 범용 성능을 높이는 테크닉인 weight decay(가중치 갑소) 기법을 소개해본다. 가중치 감소는 간단히 말하자면 가중치 매개변수의 값이 작아지도록 학습하는 방법이다. 즉, 가중치 값을 작게 하여 오버피팅이 일어나지 않게하는 것이다.

가중치를 작게 만들고 싶으면 초깃값도 최대한 작은 값에서 시작하는 것이 정공법이다. 그렇다면 가중치의 초깃값을 모두 0으로 설정하면 어떻게 될까? 실제로 가중치 초깃값을 0으로 하면 학습이 올바르게 이뤄지지 않는다.

이유는 오차역전파법에서 모든 가중치의 값이 똑같이 갱신되기 때문이다. 

예를 들어 2층 신경망에서 1,2번째 층의 가중치가 0이라고 가정해보자. 그럼 순전파 때는 입력층의 가중치가 0이기 때문에 두 번째 층의 모든 뉴런에 값이 입력된다는 것은 역전파 때 두 번째 층의 가중치가 모두 똑같이 갱신된다는 말이 된다. 그래서 가중치들은 같은 초깃값에서 시작하고 갱신을 거쳐도 여전히 같은 값을 유지하는 것이다. 이는 곧 가중치를 여러 개 갖는 의미를 사라지게 한다. 

이 '가중치가 고르게 되어버리는 상황'을 막으려면 초깃값을 random 하게 설정해야 한다.

 

은닉층의 활성화 값 분포

은닉층의 활성화 값(활성화 함수의 출력 데이터)의 분포를 관찰하면 중요한 정보를 얻을 수 있다.

다음 코드를 살펴보자.

 

 

import numpy as np
import matplotlib.pyplot as plt

def sigmoid(x):
    return 1/(1+np.exp(-x))

x=np.random.randn(1000,100) #1000개의 데이터
node_num=100                #각 은닉층의 노드(뉴런) 수
hidden_layer_size=5         #은닉층이 5개
activations={}              #이곳에 활성화 결과(활성화 값)를 저장

for i in range(hidden_layer_size):
    if i!=0:
        x=activations[i-1]
        
    w=np.random.randn(node_num,node_num)*1
    a=np.dot(x,w)
    z=sigmoid(a)
    activations[i]=z
    
    
for i, a in activations.items():
    plt.subplot(1,len(activations),i+1)
    plt.title(str(i+1)+'-layer')
    plt.hist(a.flatten(),30,range=(0,1))
plt.show()
    

가중치를 표준편차가 1인 정규분포로 초기화할 때의 각 층의 활성화값 분포

위 히스토그램을 보면, 활성화 값들이 0,1에 치우쳐 분포되어있다. 여기에서 사용한 sigmoid function은 그 출력이 0에 가까워지자(또는 1에 가까워지자) 그 미분은 0에 다가간다. 그래서 데이터가 0과 1에 치우쳐 분포하게 되면 역전파의 기울기 값이 점점 작아지다가 사라진다. 이것이 바로 gradient vanishing(기울기 소실)이라 알려진 문제이다. 층을 깊게 하는 딥러닝에서는 기울기 소실은 더 심각한 문제가 될 수 있다.

 

이번에는 가중치의 표준편차를 0.01로 바꿔보자.

import numpy as np
import matplotlib.pyplot as plt

def sigmoid(x):
    return 1/(1+np.exp(-x))

x=np.random.randn(1000,100) #1000개의 데이터
node_num=100                #각 은닉층의 노드(뉴런) 수
hidden_layer_size=5         #은닉층이 5개
activations={}              #이곳에 활성화 결과(활성화 값)를 저장

for i in range(hidden_layer_size):
    if i!=0:
        x=activations[i-1]
        
    w=np.random.randn(node_num,node_num)*0.01
    a=np.dot(x,w)
    z=sigmoid(a)
    activations[i]=z
    
for i, a in activations.items():
    plt.subplot(1,len(activations),i+1)
    plt.title(str(i+1)+'-layer')
    plt.hist(a.flatten(),30,range=(0,1))
plt.show()
    

가중치를 표준편차가 0.01로 한 정규분포로 초기화할 때의 각 층의 활성화값 분포.

이번에는 0.5 부근에 집중되었다. 앞선 히스토그램처럼 0과 1로 치우치친 않았으니 기울기 소실 문제는 일어나지 않았다. 여기서 중요한 점이 있다. 활성화 값들이 치우쳤다는 것은 이 상황에서 다수의 뉴런이 거의 같은 값을 출력하고 있으므로, 뉴런을 여러개 둔 의미가 없어진다는 뜻이다. 즉, 뉴런 100개가 같은 값을 출력한다면 1개짜리와 차이가 없다는 것이다.

 

Xavier 초깃값

현재 Xavier 초깃값은 일반적인 딥러닝 프레임워크들이 표준으로 사용하고 있다. 

각 층의 활성화 값들을 광법위하게 분포시킬 목적으로 가중치의 적절한 분포를 찾고자했다. 그리고 앞 계층의 노드가 n개라면 표준편차가 1/Route(n)인 분포를 사용하면 된다.

Xavier 초기화 방식

처음에는 임의로 초깃값을 선택하였다. 

위 그림처럼 uniform Xavier initialization 과 Normal Xavier initialization 두가지가 있다. 

초기화 과정

초기화 과정은 위 그림과 같다. 더 깊은 수학적 내용과 이해는 이 챕터에서 요하지 않으므로 생략하겠다.

가중치 초깃값으로 'Xavier initialization'을 이용할 때의 각 층의 활성화값 분포

Xavier 초깃값을 사용한 결과, 층이 깊어지면서 형태가 다소 일그러지지만, 앞선 방식보다 확실히 넓게 분포됨을 알 수있다. 각 층에 흐르는 데이터는 적당히 퍼져 있으므로, 시그모이드 함수의 표현력도 제한받지 않고 학습이 효율적으로 이뤄질 것이다.

 

 

He 초깃값

sigmoid함수와 tanh함수는 좌우 대칭이라 중앙 부근이 선현인 함수로 볼 수 있다. 그래서 Xavier 초깃값이 적당한 반면, ReLU함수를 이용할 때는 ReLU에 특화된 초깃값을 이용하라고 권장한다. 이 특화된 초깃값을 찾아낸 Kaiming He 의 이름을 따 He 초깃값이라고 한다. 

He초깃값은 앞 계층의 노드가 n개일 때, 표준편차가 Route(2/n)인 NormalDistribution을 사용한다. Xavier은 Route(1/n)이었는데 ReLU는 음의 영역이 0이라 더 넓게 분포시키기 위해 2배의 계수가 필요하다고 볼 수 있다.

He 초깃값을 사용한 경우

우선 간단한 이미지분류기를 하기 위해 앞서 포스팅한 전이학습에 대한 이해가 있어야 한다. 

링크를 통해 먼저 학습하고 다음 글을 이해하자.

https://undeadkwandoll.tistory.com/22

 

Transfer Learning(전이 학습)

"Transfer learning(TL) is a research problem in machine learning(ML) that focuses on storing knowledge gained while solving one problem and applying it to a different but related problem" [Definitio..

undeadkwandoll.tistory.com

 

마동석, 이병헌, 김종국 분류기를 만들어보자.

 

import matplotlib
import matplotlib.pyplot as plt
import matplotlib.font_manager as fm

fontpath='/usr/share/fonts/truetype/nanum/NanumBarunGothic.ttf'
font=fm.FontProperties(fname=fontpath, size=10)
plt.rc('font',family='NanumBarunGothic')
matplotlib.font_manager._rebuild()

import os
os.environ['KMP_DUPLICATE_LIB_OK']='True'

matplotlib는 누구나 알듯 그래픽 출력 라이브러리.

한국어 출력을 위한 font 설정.

!git clone https://github.com/ndb796/bing_image_downloader

fatal: destination path 'bing_image_downloader' already exists and is not an empty directory.

위 github 사이트에서 필요한 이미지 다운로더 라이브러리 다운.

 

import shutil
from bing_image_downloader.bing_image_downloader import downloader

그 후, 라이브러리 import.

 

 

directory_list=['C:/data/train/',
               'C:/data/test/',]

#초기 데릭토리 만들기
for directory in directory_list:
    if not os.path.isdir(directory):
        os.makedirs(directory)
        
#수집한 이미지를 학습 데이터와 평가 데이터로 구분하는 함수
def dataset_split(query,train_cnt):
    for directory in directory_list:
        if not os.path.isdir(directory+'/'+query):
            os.makedirs(directory+'/'+query)
    cnt=0
    for file_name in os.listdir(query):
        if cnt<train_cnt:
            print(f'[Train Dataset]{file_name}')
            shutil.move(query+'/'+file_name,'C:/data/train/'+query+'/'+file_name)
        else:
            print(f'[Test Dataset]{file_name}')
            shutil.move(query+'/'+file_name,'C:/data/test/'+query+'/'+file_name)
        cnt+=1
    shutil.rmtree(query)
            

 

 

 

초기 본인이 원하는 디렉토리를 지정하여, test,train data 디렉토리를 지정해준다. 그 후 우리가 받아야 할 이미지 파일의 저장경로를 지정해준다.

 

query='마동석'
downloader.download(query,limit=40,output_dir='./',adult_filter_off=True,force_replace=False,timeout=60)
dataset_split(query,30)

query = '김종국'
downloader.download(query, limit=40,  output_dir='./', adult_filter_off=True, force_replace=False, timeout=60)
dataset_split(query, 30)

query = '이병헌'
downloader.download(query, limit=40,  output_dir='./', adult_filter_off=True, force_replace=False, timeout=60)
dataset_split(query, 30)

 

위 코드를 통해, 마동석, 김종국, 이병헌에 대한 이미지 파일을 다운로드 한다.

 

import torch
import torch.nn as nn
import torch.optim as optim

import torchvision
from torchvision import datasets, models,transforms
import numpy as np
import time

device=torch.device("cuda:0" if torch.cuda.is_available() else 'cpu')

 

GPU 사용을 최소화하기 위해 , 다음과같이 torch를 import 하여 모델을 학습시키고, Flask Server를 통해 이미지를 주고받는 식으로 사용하는 방법이다.

 

# 데이터셋을 불러올 때 사용할 변형(transformation) 객체 정의
transforms_train = transforms.Compose([
    transforms.Resize((224, 224)),
    transforms.RandomHorizontalFlip(), # 데이터 증진(augmentation)
    transforms.ToTensor(),
    transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) # 정규화(normalization)
])

transforms_test = transforms.Compose([
    transforms.Resize((224, 224)),
    transforms.ToTensor(),
    transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
])

data_dir = 'C:/data'
train_datasets = datasets.ImageFolder(os.path.join(data_dir, 'train'), transforms_train)
test_datasets = datasets.ImageFolder(os.path.join(data_dir, 'test'), transforms_test)

train_dataloader = torch.utils.data.DataLoader(train_datasets, batch_size=4, shuffle=True, num_workers=4)
test_dataloader = torch.utils.data.DataLoader(test_datasets, batch_size=4, shuffle=True, num_workers=4)

print('학습 데이터셋 크기:', len(train_datasets))
print('테스트 데이터셋 크기:', len(test_datasets))

class_names = train_datasets.classes
print('클래스:', class_names)


학습 데이터셋 크기: 90
테스트 데이터셋 크기: 32
클래스: ['김종국', '마동석', '이병헌']

train data와 test data에 대해 transformation을 시킨 후 , 정규화 하여 pytorch tensor에 맞게 변형시켜준다.

 

이후, 확인해 보기 위한 코드를 진행한다.

def imshow(input, title):
    # torch.Tensor를 numpy 객체로 변환
    input = input.numpy().transpose((1, 2, 0))
    # 이미지 정규화 해제하기
    mean = np.array([0.485, 0.456, 0.406])
    std = np.array([0.229, 0.224, 0.225])
    input = std * input + mean
    input = np.clip(input, 0, 1)
    # 이미지 출력
    plt.imshow(input)
    plt.title(title)
    plt.show()


# 학습 데이터를 배치 단위로 불러오기
iterator = iter(train_dataloader)

# 현재 배치를 이용해 격자 형태의 이미지를 만들어 시각화
inputs, classes = next(iterator)
out = torchvision.utils.make_grid(inputs)
imshow(out, title=[class_names[x] for x in classes])

model = models.resnet34(pretrained=True)
num_features = model.fc.in_features
# 전이 학습(transfer learning): 모델의 출력 뉴런 수를 3개로 교체하여 마지막 레이어 다시 학습
model.fc = nn.Linear(num_features, 3)
model = model.to(device)

criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr=0.001, momentum=0.9)
num_epochs = 50
model.train()
start_time = time.time()

# 전체 반복(epoch) 수 만큼 반복하며
for epoch in range(num_epochs):
    running_loss = 0.
    running_corrects = 0

    # 배치 단위로 학습 데이터 불러오기
    for inputs, labels in train_dataloader:
        inputs = inputs.to(device)
        labels = labels.to(device)

        # 모델에 입력(forward)하고 결과 계산
        optimizer.zero_grad()
        outputs = model(inputs)
        _, preds = torch.max(outputs, 1)
        loss = criterion(outputs, labels)

        # 역전파를 통해 기울기(gradient) 계산 및 학습 진행
        loss.backward()
        optimizer.step()

        running_loss += loss.item() * inputs.size(0)
        running_corrects += torch.sum(preds == labels.data)

    epoch_loss = running_loss / len(train_datasets)
    epoch_acc = running_corrects / len(train_datasets) * 100.

    # 학습 과정 중에 결과 출력
    print('#{} Loss: {:.4f} Acc: {:.4f}% Time: {:.4f}s'.format(epoch, epoch_loss, epoch_acc, time.time() - start_time))
#0 Loss: 1.0096 Acc: 51.1111% Time: 25.6392s
#1 Loss: 0.4748 Acc: 80.0000% Time: 54.3030s
#2 Loss: 0.5483 Acc: 80.0000% Time: 84.5246s
#3 Loss: 0.5021 Acc: 81.1111% Time: 118.2342s
#4 Loss: 0.2609 Acc: 90.0000% Time: 150.8176s
#5 Loss: 0.1877 Acc: 91.1111% Time: 184.1703s
#6 Loss: 0.1991 Acc: 92.2222% Time: 214.8244s
#7 Loss: 0.1990 Acc: 95.5556% Time: 245.0987s
#8 Loss: 0.3073 Acc: 84.4444% Time: 275.1495s
#9 Loss: 0.1584 Acc: 93.3333% Time: 304.9051s
#10 Loss: 0.2887 Acc: 94.4444% Time: 335.1631s
#11 Loss: 0.2231 Acc: 92.2222% Time: 365.2182s
#12 Loss: 0.2435 Acc: 90.0000% Time: 397.8394s
#13 Loss: 0.2753 Acc: 87.7778% Time: 428.1891s
#14 Loss: 0.4128 Acc: 93.3333% Time: 458.8629s
#15 Loss: 0.4522 Acc: 86.6667% Time: 488.5716s
#16 Loss: 0.0865 Acc: 97.7778% Time: 518.9268s
#17 Loss: 0.2886 Acc: 92.2222% Time: 550.2028s
#18 Loss: 0.5119 Acc: 77.7778% Time: 580.1998s
#19 Loss: 0.4541 Acc: 88.8889% Time: 612.7564s
#20 Loss: 0.0438 Acc: 98.8889% Time: 643.0622s
#21 Loss: 0.1214 Acc: 96.6667% Time: 673.6289s
#22 Loss: 0.0683 Acc: 97.7778% Time: 703.9003s
#23 Loss: 0.0541 Acc: 98.8889% Time: 734.5212s
#24 Loss: 0.1325 Acc: 95.5556% Time: 764.8524s
#25 Loss: 0.4077 Acc: 90.0000% Time: 795.4360s
#26 Loss: 0.0673 Acc: 98.8889% Time: 826.0624s
#27 Loss: 0.0974 Acc: 95.5556% Time: 857.2442s
#28 Loss: 0.2435 Acc: 92.2222% Time: 887.8970s
#29 Loss: 0.1925 Acc: 95.5556% Time: 919.2276s
#30 Loss: 0.2115 Acc: 95.5556% Time: 950.5770s
#31 Loss: 0.0892 Acc: 96.6667% Time: 980.9778s
#32 Loss: 0.0289 Acc: 98.8889% Time: 1011.7911s
#33 Loss: 0.0399 Acc: 98.8889% Time: 1042.0252s
#34 Loss: 0.0207 Acc: 100.0000% Time: 1072.0983s
#35 Loss: 0.0575 Acc: 96.6667% Time: 1102.7425s
#36 Loss: 0.1135 Acc: 96.6667% Time: 1133.0551s
#37 Loss: 0.1599 Acc: 96.6667% Time: 1162.9883s
#38 Loss: 0.1392 Acc: 95.5556% Time: 1193.0512s
#39 Loss: 0.4575 Acc: 88.8889% Time: 1223.0743s
#40 Loss: 0.1242 Acc: 95.5556% Time: 1253.0653s
#41 Loss: 0.2291 Acc: 93.3333% Time: 1283.8887s
#42 Loss: 0.2359 Acc: 92.2222% Time: 1314.8435s
#43 Loss: 0.0434 Acc: 97.7778% Time: 1345.1724s
#44 Loss: 0.1595 Acc: 96.6667% Time: 1375.3669s
#45 Loss: 0.0501 Acc: 98.8889% Time: 1406.0365s
#46 Loss: 0.0097 Acc: 100.0000% Time: 1437.1793s
#47 Loss: 0.0804 Acc: 98.8889% Time: 1467.5719s
#48 Loss: 0.0749 Acc: 97.7778% Time: 1498.2033s
#49 Loss: 0.0100 Acc: 100.0000% Time: 1528.7953s
model.eval()
start_time = time.time()

with torch.no_grad():
    running_loss = 0.
    running_corrects = 0

    for inputs, labels in test_dataloader:
        inputs = inputs.to(device)
        labels = labels.to(device)

        outputs = model(inputs)
        _, preds = torch.max(outputs, 1)
        loss = criterion(outputs, labels)

        running_loss += loss.item() * inputs.size(0)
        running_corrects += torch.sum(preds == labels.data)

        # 한 배치의 첫 번째 이미지에 대하여 결과 시각화
        print(f'[예측 결과: {class_names[preds[0]]}] (실제 정답: {class_names[labels.data[0]]})')
        imshow(inputs.cpu().data[0], title='예측 결과: ' + class_names[preds[0]])

    epoch_loss = running_loss / len(test_datasets)
    epoch_acc = running_corrects / len(test_datasets) * 100.
    print('[Test Phase] Loss: {:.4f} Acc: {:.4f}% Time: {:.4f}s'.format(epoch_loss, epoch_acc, time.time() - start_time))

[예측 결과: 이병헌] (실제 정답: 이병헌)

[예측 결과: 김종국] (실제 정답: 김종국)

[예측 결과: 마동석] (실제 정답: 마동석)

[예측 결과: 이병헌] (실제 정답: 이병헌)

 

 

 

 

[출처]www.youtube.com/watch?v=Lu93Ah2h9XA

 

OMP: Hint This means that multiple copies of the OpenMP runtime have been linked into the program. That is dangerous, since it can degrade performance or cause incorrect results. The best thing to do is to ensure that only a single OpenMP runtime is linked into the process, e.g. by avoiding static linking of the OpenMP runtime in any library. As an unsafe, unsupported, undocumented workaround you can set the environment variable KMP_DUPLICATE_LIB_OK=TRUE to allow the program to continue to execute, but that may cause crashes or silently produce incorrect results. For more information, please see

 

파이참에서는 다음과 같은 에러코드가 나오지만 jupyternotebook에서는 오류가 나오지않는다.

단순 shutdown....

그러므로 이를 확인하기 위해서는

 

jupyter notebook --debug

위와 같이 debug옵션을 주어 실행하면 문제점 확인가능.

 

해결법은 간단하다.

 

import os

os.environ['KMP_DUPLICATE_LIB_OK']='True'

 

혹은,

>conda install nomkl

작성해주자.

 

 

 

[출처]fredriccliver.medium.com/%EC%97%90%EB%9F%AC-omp-error-15-initializing-libiomp5-dylib-but-found-libiomp5-dylib-already-initialized-2bcc0033dc83

"Transfer learning(TL) is a research problem in machine learning(ML) that focuses on storing knowledge gained while solving one problem and applying it to a different but related problem"

[Definition of Wekipedia]

 

일반적으로 CNN 기반의 딥러닝 모델을 제대로 훈련시키려면 많은 수의 데이터가 필요하다. 그러나 이러한 데이터셋을 만드는 것은 비용이나 시간면으로 쉽지 않은일이다.

 

위 문제점을 해결하기 위한 것이 바로  TL(Transfer Learning)이다. TL이란 이미지넷과 같이 아주 큰 데이터셋에 훈련된 모델의 가중치를 가지고 와서 우리가 해결하고자 하는 과제에 맞게 재보정해서 사용하는 것을 의미한다. 

좀 더 쉽게 설명하자면, 흔히 우리가 MNIST예제를 통해 만들어 저장해 놓은 h5 파일이 있을 것이다. 이 파일은 가중치를 저장해 놓은 파일인데 이를 다시 가져와 우리가 가중치를 다시 재보정해 사용하는 것과 동일하다.

 

결과적으로 비교적 적은 수의 데이터를 가지고도 우리가 원하는 과제를 해결할 수 있는 딥러닝 모델을 훈련시킬 수 있게 되는 것이다.

 

전이학습

전이 학습은 특정 환경에서 만들어진 AI 알고리즘을 다른 비슷한 분야에 적용하는 것으로, 현재 많은 연구가 이뤄지는 머신러닝의 한 분야이다. 

간략하게 설명해보면, 체스를 익힌 AI에게 장기를 두게 하는 느낌(?)이다.

 

'Deep Learning > NLP' 카테고리의 다른 글

01) 토큰화(Tokenization)  (0) 2021.03.10
트랜스포머(Transformer)(2)  (0) 2021.03.04
트랜스포머(Transformer)(1)  (0) 2021.03.04

1. Stochastic Gradient descent(SGD)

추출된 데이터 한 개에 대해서 error gradient를 계산하고, gradient descent 알고리즘을 적용하는 방법이다.

모델의 레이어 층은 하나의 행렬곱으로 생각할 수 있고, 여러개의 묶음 데이터는 행렬이라고 생각할 수 있다.

 

SGD의 장점

위 그림에서 볼수 있듯이 Shooting이 일어나기 때문에 local optimal에 빠질 리스크가 적다.

step에 걸리는 시간이 짧기 때문에 수렴속도가 상대적으로 빠르다.

 

SGD의 단점

global optimal을 찾지 못 할 가능성이 있다.

데이터를 한개씩 처리하기 때문에 GPU의 성능을 전부 활용할 수 없다.

 

코드로 살펴보면 다음과 같다.

class SGD:
    def __init__(self,lr=0.01):
        self.lr=lr
        
    def update(self,params, grads):
        for key in params.keys():
            params[key]-=self.lr*grads[key]
            

 

[출처] light-tree.tistory.com/133

 

딥러닝 용어정리, MGD(Mini-batch gradient descent), SGD(stochastic gradient descent)의 차이

제가 공부한 내용을 정리한 글입니다. 제가 나중에 다시 볼려고 작성한 글이다보니 편의상 반말로 작성했습니다. 잘못된 내용이 있다면 지적 부탁드립니다. 감사합니다. MGD(Mini-batch gradient descen

light-tree.tistory.com

2.  Momentum

모멘텀(momentum)은 '운동량'을 뜻하는 단어로, 물리와 관계가 있다. 

W는 갱신할 가중치 매개변수, dL/dW는 W에 대한 손실 함수의 기울기, η은 학습률이다. v라는 변수가 새로 나오는데, 이는 물리에서 말하는 valocity에 해당한다. 

momentum은 위 그림과 같이 공이 그릇의 바닥을 구른는 듯한 움직임을 보여준다.

코드로 살펴보면 다음과 같다.

class Momentum:
    def __init__(self, lr=0.01,momentum=0.9):
        self.lr=lr
        self.momentum=momentum
        self.v=None
    
    def update(self,params,grads):
        if self.v is None:
            self.v={}
            for key, val in params.items():
                self.v[key]=np.zeros_like(val)
            
            for key in params.keys():
                self.v[key]=self.momentum*self.v[key]-self.lr*grads[key]
                params[key]+=self.v[key]
                

모멘텀에 의한 최적화 갱신 경로

위 그림에서 볼 수있듯, 모멘텀의 갱신 경로는 공이 그릇 바닥을 구르듯 움직인다. SGD와 비교하면 '지그재그 정도'가 덜한 것을 알 수 있다. 즉, x축의 힘은 아주 작지만 방향은 변하지 않아서 한 방향으로 일정하게 가속하기 때문이다. 거꾸로 y축의 힘은 크지만 위아래로 번갈아 받아서 상충하여 y축 방향의 속도는 안정적이지 않다. 전체적으로 SGD보다 x축 방향으로 빠르게 다가가 지그재그 움직임이 줄어든다.

3. AdaGrad

신경망 학습에서는 학습률(수식에서는 η로 표기) 값이 중요하다. 

이 학습률을 정하는 효과적 기술로 learning rate decay(학습률 감소)가 있다. 이는 학습률을 점차 줄여가는 방법이다.

학습률을 서서히 낮추는 가장 간단한 방법은 매개변수 '전체'의 합습률 값을 일괄적으로 낮추는 것이다. 이를 더욱 바전시킨 것이 AdaGrad이다. AdaGrad는 '각각의' 매개변수에 '맞춤형'값을 만들어준다.

 

AdaGrad는 개별 매개변수에 적응적으로 학습률을 조정하면서 학습을 진행한다. AdaGrad의 갱신 방법은 수식으로는 다음과 같다.

[Note] AdaGrad는 과거의 기울기를 제곱하여 계속 더해간다. 그래서 학습을 진행할수록 갱신 강도가 약해진다. 실제로 무한히 계속 학습한다면 어느 순간 갱신량이 0이 되어 전혀 갱신되지 않게 된다. 이 문제를 개선한 기법으로서 RMSProp이라는 방법이 있다. RMSProp은 과거의 모든 기울기를 균일하게 더해가는 것이 아니라, 먼 과거의 기울기는 서서히 잊고 새로운 기울기 정보를 크게 반영한다. 이를 Exponential Moving average,EMA(지수이동평균)이라 하며, 과거 기울기의 반영 규모를 기하급수적으로 감소시킨다.

 

코드로 살펴보면 다음과 같다.

class AdaGrad:
    def __init__(self,lr=0.01):
        self.lr=lr
        self.h=None
        
    def update(self,params,grads):
        if self.h is None:
            self.h={}
            
            for key, val in params.items():
                self.h[key]=np.zeros_like(val)
            for key in params.keys():
                self.h[key]+=grads[key]*grads[key]
                params[key]-=self.lr*grads[key]/(np.sqrt(self.h[key])*1e-7)
                

위 코딩에서 주의해야 할 점은 마지막 줄에 1e-7이라는 작은 값을 더하는 부분이다. 이 작은 값은 self.h[key]에 0이 담겨 있다 해도 0으로 나누는 사태를 막아준다. 대부분의 딥러닝 프레임워크에서는 이 값도 인수로 설정할 수 있다.

AdaGrad에 의한 최적화 갱신 경로

위 그림을 보면 최솟값을 향해 효율적으로 움직이는 것을 알 수 있다. y축 방향은 기울기가 커서 처음에는 크게 움직이지만, 그 큰 움직임에 비례해 갱신 정도도 큰 폭으로 작아지도록 조정된다. 그래서 y축 방향으로 갱신 강도가 빠르게 약해지고, 지그재그 움직임이 줄어든다.

 

 

4. Adam

모멘텀은 공이 그릇 바닥을 구르는 듯한 움직임을 보였다. AdaGrad는 매개변수의 원소마다 적응적으로 갱신 정도를 조정했다. 그 두 기법을 융합한 아이디어에서 출발한 기법이 Adam이다. 

Adam에 의한 최적화 갱신 경로

Adam의 갱신 과정도 그릇 바닥을 구르듯 움직인다. 모멘텀과 비슷한 패턴이지만, 모멘텀보다 공의 좌우 흔들림이 적다.

 

 

[Note]

Adam은 하이퍼파라미터를 3개 설정한다. 하나는 지금까지의 학습률(논문에서는 α로 등장), 나머지 두 개는 일차 모멘텀용 계수 β1과 이차 모멘텀용 계수 β2이다. 논문에 따르면 기본 설정값은 β1은0.9, β2는0.999이며, 이 값이면 많은 경우에 좋은 결과를 얻을 수 있다.

 

 

SGD, Momentum, AdaGrad, Adam 총 4가지의 optimizer를 알아보았다. 하지만 모든 경우에 항상 뛰어난 기법은 없다. 각자의 장단점이 있기 때문에 해당 issue에 맞는 optimizer를 선택하는게 좋다. 

앞 장에서 신경망 학습에 대해 배웠다. 가중치 매개변수에 대한 손실 함수의 기울기는 수치 미분을 통해 구했다. 하지만 수치 미분은 단순하고 구현하기도 쉽지만 계산이 오래걸린다는 단점이 있다. 이에 효율적으로 계산하는 오차역전파법(backpropagation)을 알아보자.

5.1 계산그래프

계산 그래프는 계산 과정을 그래프로 나타낸 것이다. 이는 복수의 노드와 에지로 표현된다. 

다음 문제를 확인해보자.

문제1: 현빈 군은 슈퍼에서 1개에 100원인 사과를 2개 샀다. 이때 지불 금액을 구하라. 단, 소비세가 10%부과된다.

계산 그래프로 풀어본 문제 1의 답

위 그림과 같이 처음 사과의 100원이 '*2'노드로 흐르고 200원이 되어 다음 노드로 전달된다. 이제 200원이 '*1.1' 노드를 거쳐 220원이 된다. 그러므로 최종 답은 220원이 된다.

다른 계산 그래프를 살펴보자.

계산 그래프로 풀어본 문제 1의 답: '사과의 개수'와 '소비세'를 변수로 취급해 원 밖에 표기

위 그림은 사칙기호만을 노드에 넣고 나머지는 에지를 통해 들어가는 모습이다.

 

문제2: 현빈 군은 슈퍼에서 사과를 2개, 귤을 3개 샀다. 사과는 1개에 100원, 귤은 1개 150원이다. 소비세가 10%일 때 지불 금액을 구하라.

계산 그래프로 풀어본 문제 2의 답

이 문제에는 덧셈 노드인 '+'가 새로 등장하여 사과와 귤의 금액을 합산한다.

계산 그래프를 이용한 문제 풀이는 다음 흐름으로 진행된다.

1. 계산 그래프를 구성한다.

2. 그래프에서 계산을 왼쪽에서 오른쪽으로 진행한다.

 

여기서 '계산을 왼쪽에서 오른쪽으로 진행'하는 단계를 순전파(forward propagation)

반대 방향의 전파를 역전파(backward propagation)이라 부른다.

 

5.1.2 국소적 계산

계산 그래프의 특징: '국소적 계산'을 전파함으로써 최종 결과를 얻는다는 점에 있다.

국소적 계산은 결국 전체에서 어떤 일이 벌어지든 상관없이 자신과 관계된 정보만으로 결과룰 출력할 수 있다는 것이다.

 

사과 2개를 포함해 여러 식품을 구입하는 예

위 그림의 핵심은 각 노드에서의 계싼은 국소적 계산이라는 점이다. 즉, 노드는 자신과 관련한 계산 외에는 아무것도 신경 쓸게 없다. 전체 계산이 제 아무리 복잡하더라도 각 단계에서 하는 일은 해당 노드의 '국소적 계산'이며 국소적 계산은 단순하지만, 전체를 구성하는 복잡한 계산을 해낼 수 있다.

 

5.1.3 왜 계산 그래프로 푸는가?

계산 그래프의 이점은 다음과 같다.

1. 국소적 계산

2. 계산 그래프는 중간 계산 결과를 모두 보관할 수 있다.

실제 계산 그래프르 사용하는 가장 큰 이유는 역전파를 통해 미분을 효율적으로 계산할 수 있는 점에 있다.

 

5.2 연쇄법칙

그동안 해온 계산 그래프의 순전파는 계산 결과를 왼쪽에서 오른쪽으로 전달했다. 한편 역전파는 '국소적이 미분'을 순방향과는 반대인 오른쪽에서 왼쪽으로 전달한다. 또한 이 '국소적 미분'을 전달하는 원리는 연쇄법칙(chain rule)에 따른 것이다.

5.2.1 계산 그래프의 역전파

생략)

 

5.2.2 연쇄법칙이란?

연쇄법칙은 합성 함수의 미분에 대한 성질이며 다음과 같이 정의된다.

합성 함수의 미분은 합성 함수를 구성하는 각 함수의 미분의 곱을 나타낼 수 있다.

식 5.2,5.3,5.4

5.2.3 연쇄법칙과 계산 그래프

위 식 5.4의 연쇄법칙 계산을 계산 그래프로 나타내보자. 

식5.4의 계산 그래프: 순전파와는 반대 방향으로 국소적 미분을 곱하여 전달한다.

위 그림과 같이 계산 그래프의 역전파는 오른쪽에서 왼쪽으로 신호를 전파한다. 

역전파의 계산 절차에서는 노드로 들어온 입력 신호에 그 노드의 국소적 미분(편미분)을 곱한 후 다음 노드로 전달한다.

즉, 역전파가 하는 일은 연쇄 법칙의 원리와 같다.

 

계산 그래프의 역전파 결과가 다르면 

5.3 역전파

연산을 예로 들어 역전파의 구조를 살펴보자.

 

덧셈 노드의 역전파

z=x+y라는 식을 대상으로 살펴보자.

계산 그래프를 살펴보면,

덧셈노드의 역전파: 왼쪽이 순전파, 오른쪽이 역전파다. 덧셈 노드의 역전파는 입력 값을 그대로 흘려보낸다.
최종 출력으로 가는 계산의 중간에 덧셈 노드가 존재한다. 역전파에서는 국소적 미분이 가장 오른쪽의 출력에서 시작하여 노드를 타고 역방향으로 전파된다.
덧셈 노드 역전파의 구체적인 예

곱셈 노드의 역전파

z=xy라는 식을 생각해보자.

계산 그래프로 나타내면,

곱셈 노드의 역전파: 왼쪽이 순전파, 오른쪽이 역전파다.
곱셈 노드 역전파의 구체적인 예

곱셈 또한 덧셈과 크게 다를 것이 없다. 

 

5.4 단순한 계층 구현하기

5.4.1 곱셈 계층

모든 계층은 forward() 와 backward()라는 공통의 메서드(인터페이스)를 갖도록 구현할 것이다. forward()는 순전파, backward()는 역전파를 처리한다.

다음 코드를 살펴보자.

class MulLayer:
    def __init__(self):
        self.x=None
        self.y=None
        
    def forward(self,x,y):
        self.x=x
        self.y=y
        out=x*y
        return out
    def backward(self,dout):
        dx=dout*self.y
        dy=dout*self.x
        return dx,dy

__init__에서 인스턴스 변수 x,y 초기화. 이는 순전파 시의 입력 값을 유지하기 위해서 사용한다.

forward()에선 x,y를 인수로 받아 두값을 곱해 반환.

backward()에선 미분에 순전파 때의 값을 '서로 바꿔' 곱한 후 넘긴다.

사과예를 살펴보자.

<순전파>

apple=100
apple_num=2
tax=1.1

mul_apple_layer=MulLayer()
mul_tax_layer=MulLayer()

apple_price=mul_apple_layer.forward(apple,apple_num)
price=mul_tax_layer.forward(apple_price,tax)
print(price)

220.00000000000003

<역전파>

dprice=1
dapple_price,dtax=mul_tax_layer.backward(dprice)
dapple,dapple_num=mul_apple_layer.backward(dapple_price)

print(dapple,dapple_num,dtax)

2.2 110.00000000000001 200

5.4.2 덧셈 계층

덧셈 계층 코딩은 다음과 같다.

class AddLayer:
    def __init__(self):
        pass
    def forward(self,x,y):
        out=x+y
        return out
    def backward(self,dout):
        dx=dout*1
        dy=dout*1
        return dx,dy
    

사과 2개와 귤 3개 구입

이 계산 그래프를 코딩으로 구현하면,

apple=100
apple_num=2
orange=150
orange_num=3
tax=1.1

mul_apple_layer=MulLayer()
mul_orange_layer=MulLayer()
add_apple_orange_layer=AddLayer()
mul_tax_layer=MulLayer()

apple_price=mul_apple_layer.forward(apple,apple_num)
orange_price=mul_orange_layer.forward(orange,orange_num)
all_price=add_apple_orange_layer.forward(apple_price,orange_price)
price=mul_tax_layer.forward(all_price,tax)

dprice=1
dall_price,dtax=mul_tax_layer.backward(dprice)
dapple_price,dorange_price=add_apple_orange_layer.backward(dall_price)
dorange,dorange_num=mul_orange_layer.backward(dorange_price)
dapple,dapple_num=mul_apple_layer.backward(dapple_price)

print(price)
print(dapple_num,dapple,dorange,dorange_num,dtax)

715.0000000000001
110.00000000000001 2.2 3.3000000000000003 165.0 650

5.5 활성화 함수 계층 구현하기

활성화 함수에 관련하여 앞선 포스트에 세밀하게 해놓았다. 그러므로 이번은 코딩으로 구현하는 것만 확인하겠다.

 

ReLU

class Relu:
    def __init__(self):
        self.mask=None
    def forward(self,x):
        self.mask=(x<=0)
        out=x.copy()
        out[self.mask]=0
        return out
    def backward(self,dout):
        dout[self.mask]=0
        dx=dout
        
        return dx
    
x=np.array([[1.0,-.5],[-2.0,3.0]])
print(x)

[[ 1.  -0.5]
 [-2.   3. ]]
 
mask=(x<=0)
print(mask)

[[False  True]
 [ True False]]

Sigmoid

class Sigmoid:
    def __init__(self):
        self.out=None
    def forward(self,x):
        out=1/(1+np.exp(-x))
        self.out=out
        return out
    def backward(self,dout):
        dx=dout*(1.0-self.out)*self.out
        return dx

Affine

class Affine:
    def __init__(self,W,b):
        self.W=W
        self.b=b
        self.x=None
        self.dW=None
        self.db=None
        
    def forward(self,x):
        self.x=x
        out=np.dot(x,self.W)+self.b
        return out
    def backward(self,dout):
        dx=np.dot(dout,self.W.T)
        self.dW=np.dot(self.x.T,dout)
        self.db=np.sum(dout,axis=0)
        return dx
    

Softmax-with-Loss

class SoftmaxWithLoss:
    def __init__(self):
        self.loss=None
        self.y=None
        self.t=None
    def forward(self,x,t):
        self.t=t
        self.y=softmax(x)
        self.loss=cross_entropy_error(self.y,self.t)
        return self.loss
    def backward(self,dout=1):
        batch_size=self.t.shape[0]
        dx=(self.y-self.t)/batch_size
        return dx

 

이번장은 신경망 학습이다.

 

학습: 훈련 데이터로부터 가중치 매개변수의 최적값을 자동으로 획득하는 것을 뜻한다.

 

신경망을 학습할 수 있도록 해주는 지표인 손실 함수를 알아보자.

 

4.1.0 데이터에서 학습한다

신경망의 특징은 데이터를 보고 학습할 수 있다는 것이다. 즉, 데이터에서 학습한다는 것은 가중치 매개변수의 값을 데이터를 보고 자동으로 결정한다는 의미이다. 

 

4.1.1 데이터 주도 학습

Machine Learning은 데이터가 생명이다. 데이터에서 답을 찾고 데이터에서 패턴을 발견하고 데이터로 이야기를 만드는, 그것이 바로 기계학습이다. 인간과 기계학습의 차이점을 본다면, 인간은 경험과 직관을 단서로 시행착오를 거듭하여 일을 진행한다. 이에 반해 기계학습은 사람의 개입을 최소화하고 수집한 데이터로부터 데이터를 찾으려 시도한다. 

우리가 흔히 아는 MNIST를 본다면, 이미지에서 특징을 추출하고 그 특징의 패턴을 기계학습 기술로 학습하는 방법이 있다. 이와 같이 기계학습에서는 모아진 데이터로부터 규칙을 찾아내는 역할을 '기계'가 담당한다. 하지만, 이미지를 벡터로 변환할 때 사용하는 특징은 여전히 '사람'이 설계한다.

 

4.1.2 훈련 데이터와 시험 데이터

기계학습에서는 문제를 다룰 때 데이터를 훈련데이터(Train Data)와 시험데이터(Test Data)로 나눠 학습과 실험을 수행하는 것이 일반적이다. 

1. 우선 훈련 데이터만 사용하여 학습하면서 최적의 매개변수를 찾는다.

2. 그 후, 시험 데이터를 사용하여 앞서 훈련한 모델의 실력을 평가한다.

이렇게 나누는 이유는 뭘까?

우리가 설계하는 이유는 갖고 있는 데이터를 통해서만 사용하는 것이 아닌, 범용적으로 사용하기 위한 모델을 만들기 위해서이다. 다시말해, 아직 보지 못한 데이터(훈련 데이터에 포함되지 않는 데이터)로도 문제를 올바르게 풀어내는 능력이다.

또한, 수중의 데이터셋은 제대로 맞히더라도 다른 데이터셋에는 엉망인 일도 벌어진다. 이와 관련해, 한 데이터셋에만 지나치게 최적화된 상태를 오버피팅(overfitting)이라고 한다.

오버피팅(overfitting): 특정 데이터셋에만 너무 많이 들어서 편견이 생겨버린 상태로 이해하자. 과적합, 과대적합, 과학습 등 다양하게 번역되어있다.

 

4.2 손실함수

인간이 예를 들어 행복의 지수를 수치로 파악하고, 이를 근거로 '최적의 인생'을 탐색하듯, 신경망도 '하나의 지표'를 기준으로 최적의 매개변수 값을 탐색한다. 신경망 학습에서 사용하는 지표는 손실함수(loss function)이라고 한다.

이 손실 함수는 임의의 함수를 사용할 수도 있지만, 일반적으로 평균 제곱 오차(MSE)와 교차 엔트로피 (Cross entropy error)를 사용한다.

 

4.2.1 평균 제곱 오차(Mean Squared Error)

가장 많이 쓰이는 손실 함수는 평균 제곱 오차이다. 

 

평균 제곱 오차 정의

여기서 yi는 신경망의 출력(신경망이 추정한 값), ti는 정답 레이블, i는 데이터의 차원수를 나타낸다.  코딩의 예를 통해 살펴보자. 

def mean_squared_error(y,t):
    return 0.5*np.sum((y-t)**2)
#정답은 '2'
t=[0,0,1,0,0,0,0,0,0,0]

#예1: '2'일 확률이 가장 높다고 추정함(0.6)
y1=[.1,.05,.6,.0,.05,.1,.0,.1,.0,.0]
print(mean_squared_error(np.array(y1),np.array(t)))

#예2: '7'일 확률이 가장 높다고 추정함(0.6)
y2=[.1,.05,.1,.0,.05,.1,.0,.6,.0,.0,]
print(mean_squared_error(np.array(y2),np.array(t)))

0.09750000000000003
0.5975

첫 번째 예는 정답이 '2'고 신경망의 출력도 '2'에서 가장 높은 경우다.

두 번째 예는 정답은 똑같이 '2'지만, 신경망의 출력은 '7'에서 가장 높다. 위 실험의 결과로 첫 번째 손실함수 쪽 출력이 작으며 정답레이블과의 오차도 작은 것을 알 수 있다. 즉, 평균 제곱 오차를 기준으로는 첫 번째 추정 결과가 정답에 더 가까운 것으로 판단 할 수 있다.

4.2.2 교차 엔트로피(Cross Entropy Error)

또 다른 손실 함수로서 교차 엔트로피 오차(Cross entropy error)도 자주 이용한다. 

교차 엔트로피 오차 정의

log는 밑이 e인 자연로그, yk는 신경망의 출력, tk는 정답 레이블이다. tk는 정답에 해당하는 인덱스의 원소만 1이고 나머지는 0이다(원-핫 인코딩). 코딩의 예를 통해 살펴보자.

def cross_entropy_error(y,t):
    delta=1e-7
    return -np.sum(t*np.log(y+delta))

t=[0,0,1,0,0,0,0,0,0,0]
y1=[.1,.05,.6,.0,.05,.1,.0,.1,.0,.0]
y2=[.1,.05,.1,.0,.05,.1,.0,.6,.0,.0]
print(cross_entropy_error(np.array(y1),np.array(t)))
print(cross_entropy_error(np.array(y2),np.array(t)))

0.510825457099338
2.302584092994546

첫 번째 예는 정답일 때의 출력이 .6인 경우로, 이때의 교차 엔트로피 오차는 약 .51이다. 

두 번째 예는 정답일 때의 출력이 .1인 경우로, 이때의 교차 엔트ㄹ로피 오차는 무려 2.3이다.

즉, 결과(오차)가 더 작은 첫 번째 추정이 정답일 가능성이 높다고 판단한 것으로 앞서 MSE와 일치하다.

 

4.2.3 미니배치 학습

기계학습 문제는 훈련 데이터에 대한 손실 함수의 값을 구하고, 그 값을 최대한 줄여주는 매개변수를 찾아낸다. 이렇게 하려면 모든 훈련 데이터를 대상으로 손실 함수 값을 구해야 한다. 즉, 훈련 데이터가 100개 있으면 그로부터 계산하 100개의 손실 함수 값들의 합을 지표로 삼는 것이다. 

앞선 내용에서는 데이터 하나에 대한 손실 함수만 생각했으니, 이제 훈련 데이터 모두에 대한 손실 함수의 합을 구하는 방법을 보자.

교차 엔트로피 공식은 다음과 같다.

교차엔트뢰 오차-배치 정의

이때, 데이터가 N개라면 tnk는 n번째 데이터의 k번째 값을 의미한다.(ynk는 신경망의 출력, tnk는 정답 레이블)

앞선 한 개의 대한 CEE에서 단순히 N개의 데이터로 확장했을 뿐이다. 다만, 마지막에 N으로 나누어 정규화하고 있다.

즉, N으로 나눔으로써 '평균 손실 함수'를 구하는 것이다. 이렇게 평균을 구해 사용하면 훈련 데이터의 개수와 상관없이 언제든 통일된 지표를 얻을 수 있다. 

생각해보자. 실제 빅데이터를 다루게 된다면 수천만도 넘는 거대한 데이터를 다루게 될텐데 과연 이를 일일이 손실함수를 계산하는게 현실적인가? 아니라고 본다. 그러므로 일부 데이터를 Random sampling을 통해 수집하고 학습하는게 더 효율적일 것이다. 이러한 학습 방법을 미니배치 학습이라고 한다.  코딩의 예를 통해 살펴보자.

from keras.datasets import mnist
(X_train,t_train),(X_test,t_test)=mnist.load_data()

Downloading data from https://storage.googleapis.com/tensorflow/tf-keras-datasets/mnist.npz
11493376/11490434 [==============================] - 1s 0us/step

print(X_train.shape)
print(t_train.shape)
(60000, 28, 28)
(60000,)

여기서 mnist 데이터셋을 불러오는 방법은 keras,tensorflow등 다양하니 참고하길 바란다.

train_size=X_train.shape[0]
batch_size=10
batch_mask=np.random.choice(train_size,batch_size)
x_batch=X_train[batch_mask]
t_batch=t_train[batch_mask]
np.random.choice(600000,10)
array([ 70533, 465580, 501527, 363874, 118036, 136283,  57982, 367151,
       514364, 529300])

훈련 데이터는 60000개고, 입력 데이터는 28*28인 이미지 데이터임을 알 수 있다. np.random.choice()로는 지정한 범위의 수 중에서 random sampling을 할 수 있다. 예를 들면, np.random.choice(60000,10)은 0 이상 60000미만의 수 중에서 무작위로 10개를 골라낸다는 의미이다.

 

4.2.4 (배치용) 교차 엔트로피 오차 구현하기

#정답 레이블이 원-핫 인코딩일 경우
def input_by_onehot_cross_entropy_error(y,t):
    if y.dim==1:
        t=t.reshape(1,t.size)
        y=y.reshape(1,y.size)
        
    batch_size=y.shape[0]
    return -np.sum(t*np.log(y))/batch_size

#정답 레이블이 '2'나 '7' 처럼 숫자 레이블일 경우
def input_by_shape_cross_entropy_error(y,t):
    if y.dim==1:
        t=t.reshape(1,t.size)
        y=y.reshape(1,y.size)
    batch_size=y.shape[0]
    return -np.sum(np.log(y[np.arange(batch_size),t]))/batch_size

이 구현에서 원-핫 인코딩일 때 t가 0인 원소는 교차 엔트로피 오차도 0이므로, 그 계산은 무시해도 좋다는 것이 핵심이다. 다시 말해, 정답에 해당하는 신경망의 출력만으로 교차 엔트로피의 오차를 계산할 수 있다.

 

4.2.5 왜 손실 함수를 성정하는가?

앞선 내용에서는 왜 손실 함수를 설정하는가에 대한 상세한 설명이 없었다.

그렇다면 왜 굳이 손실 함수를 사용해야할까?

이를 위해서는 우리가 기계학습을 하는 궁극적 목표를 생각해봐야 한다. 우리의 목적은 높은 '정확도'를 끌어내는 매개변수 값을 찾는 것이다.

하지만 '정확도'라는 질표를 놔두고 '손실 함수의 값'이라는 우회적인 방법을 선택하는 이유는 무엇일까?

이는 '미분'과 관련이 있다. 이는 다음장에서 설명을 하고, 간단하게 설명하자면 신경망 학습에서는 최적의 매개변수(가중치와 편향)를 탐색할 때 손실함수의 값을 가능한 작게 하는 매개변수 값을 찾는다. 이때 매개변수의 미분(정확히 기울기)을 계산하고, 그 미분 값을 단서로 매개변수의 값을 서서히 갱신하는 과정을 반복한다.

가중치 매개변수의 손실함수의 미분이란 '가중치 매개변수의 값을 아주 조금 변화시켰을 때, 손실 함수가 어떻게 변하나'라는 의미이다.

미분값이 음이면 그 가중치를 양으로, 반대로 양이면 가중치를 음으로 변화시켜 손실 함수의 값을 줄일 수 있다.

미분 값이 0이면 가중치 매개변수를 어느 쪽으로 움직여도 손실 함수의 값은 달라지지 않는다. 그래서 그 가중치 매개변수의 갱신은 거기서 멈춘다. 

즉, 이말은 미분 값이 대부분이 장소에서 0이 되어 매개변수를 갱신할 수 없기 때문이다.

정리하자면,

신경망을 학습할 때 정확도를 지표로 삼아서는 안된다. 정확도를 지표로 하면 매개변수의 미분이 대부분의 장소에서 0이되기 때문이다.

정확도는 매개변수의 미소한 변화에는 거의 반을을 보이지 않고, 반응이 있더라고 그 값이 불연속적으로 갑자기 변화한다. 이는 '계단 함수'를 활성화 함수로 사용하지 않는 이유와도 들어맞는다. 반대로 sigmoid 함수를 생각해보자. 시그모이드 함수는 출력(세로축의 값)이 연속적으로 변하고 곡선의 기울기도 연속적으로 변한다. 즉, 시그모이드 함수의 미분은 어느 장소라도 0이 되지는 않는다는 의미이다. 

시그모이드 함수 그래프

4.3,4.4 수치 미분, 기울기

미분과 관련하여는 자세하게 설명하지 않겠다. 본인은 수리통계학을 공부하기에 이는 따로 수리통계학에 정리하여 올리도록 하겠다.

대략 미분, 편미분,기울기,경사하강법,신경망에서의 기울기 등이 있는데 이는 생략한다.

 

하이퍼파라미터

학습률 같은 매개변수를 하이퍼파라미터(hyper parameter,초매개변수)라고 한다. 이는 가중치와 편향 같은 신경망의 매개변수와는 성질이 다른 매개변수이다. 신경망의 가중치 매개변수는 훈련 데이터와 학습 알고리즘에 의해서 '자동'으로 획득되는 매개변수인 반면, 학습률 같은 하이퍼파라미터는 사람이 직접 설정해야하는 매개변수인 것이다. 일반적으로 이들 하이퍼파라미터는 여러 후보 값 중에서 시험을 통해 가장 장 학습하는 값을 찾는 과정을 거쳐야 한다.

이와 관련하여 CNN Hyperparameter Optimization Based on CNN Visualization and Perception Hash Algorithm 논문을 조만간 리뷰하도록 하겠다.

 

4.5 학습 알고리즘 구현하기

Given

신경망에서 적응 가능한 가중치와 편향이 있고, 이 가중치와 편향을 훈련 데이터에 적응하도록 조정하는 과정을 '학습'이라 한다. 신경망 학습은 다음과 같이 4단계로 수행한다.

Stage1.-Mini Batch

훈련 데이터 중 일부를 무작위로 가져온다. 이렇게 선별한 데이터를 미니배치라 하며, 그 미니배치의 손실 함수 값을 줄이는 것이 목표이다.

Stage2.-Calculating Slope

미니배치의 손실 함수 값을 줄이기 위해 각 가중치 매개변수의 기울기를 구한다. 기울기는 손실 함수의 값을 가장 작게 하는 방향을 제시한다.

Stage3.-Renewing Parameter

가중치 매개변수를 기울기 방향으로 아주 조금 갱신한다.

Stage4.-Repetition

Stage1~Stage3를 반복한다.

 

이것이 신경망 학습이 이뤄지는 순서이다. 이는 경사 하강법으로 매개변수를 갱신하는 방법이며, 이때 데이터를 미니배치로 무작위로 선정하기 때문에 확률적 경사 하강법(Stochastic Gradient descent, SGD)라고 부른다. 

즉, '확률적으로 무작위로 골라낸 데이터'에 대해 수행하는 경사 하강법이라는 의미이다. 대부분의 딥러닝 프레임워크는 확률적 경사 하강법의 영어 머리글자를 딴 SGD라는 함수로 이 기능을 구현한다.

 

4.5.1,2 전체 코딩 

2-Layer Net

class TwoLayerNet:
    def __init__(self,input_size,hidden_size,output_size,weight_init_std=0.01):
        self.params={}
        self.params['W1']=weight_init_std*np.random.randn(input_size,hidden_size)
        self.params['b1']=np.zeros(hidden_size)
        self.params['W2']=weight_init_std*np.random.randn(hidden_size,output_size)
        self.params['b2']=np.zeros(output_size)
        
    def predict(self,x):
        W1,W2= self.params['W1'], self.params['W2']
        b1,b2=self.params['b1'],self.params['b2']
        
        a1=np.dot(x,W1)+b1
        z1=sigmoid(a1)
        a2=np.dot(z1,W2)+b2
        y=softmax(a2)
        
        return y
    
    def loss(self,x,t):
        y=self.predict(x)
        
        return cross_entropy_error(y,t)
    
    def accuracy(self,x,t):
        y=self.predict(x)
        y=np.argmax(y,axis=1)
        t=np.argmax(t,axis=1)
        
        accuracy=np.sum(y==t)/float(x.shape[0])
        return accuracy
    
    def numerical_gradient(self,x,t):
        loss_W=lambda W: self.loss(x,t)
        
        grads={}
        grads['W1']=numerical_gradient(loss_W,self.params['W1'])
        grads['b1']=numerical_gradient(loss_W,self.params['b1'])
        grads['W2']=numerical_gradient(loss_W,self.params['W2'])
        grads['b2']=numerical_gradient(loss_W,self.params['b1'])
        
        return grads
    
    
    

미니배치 학습 구현하기

 

(x_train,t_train),(x_test,t_test)=mnist.load_data()

train_loss_list=[]

iters_num=10000
train_size=x_train.shape[0]
batch_size=100
learning_rate=.1
network=TwoLayerNet(input_size=784,hidden_size=50,output_size=10)

for i in range(iters_num):
    batch_mask=np.random.choice(train_size,batch_size)
    x_batch=x_train[batch_mask]
    t_batch=t_train[batch_mask]
    
    grad=network.numerical_gradient(x_batch,t_batch)
    
    for key in ('W1','b1','W2','b2'):
        network.params[key]-=learning_rate*grad[key]
        
    loss=network.loss(x_batch,t_batch)
    train_loss_list.append(loss)

 

Published by the IEEE Computer Society  

[Korea University]

Kang-Min Kim Jun-Hyung Park Woo-Jong Ryu Sang-Keun Lee

위 논문은 고려대학교 논문을 발췌해 리뷰를 한것이다.

[논문 발췌]라고 적힌 문장들은 본인이 읽으며 중요하다고 생각하는 문장들을 적어 놓은 것이다.

 

Intro.

As the usage..

스마트폰 사용량이 증가함에 따라 WhatsApp,Viber,Snapchat 등 많은 채팅어플리케이션이 사용된다.

또한, 자주 사진을 문자만큼이나 많이 공유한다. 수치로는 매일 45억개의 사진을, 55억개의 문자를 보내는데 이는 Whatsapp 플랫폼에서만의 기준이다.

 

최근들어 Intelligent assistant 기술이 스마트폰 메시지 어플에 많이 접목된다. 예를들면, google allo, google assistant등.. 이것때문에 대화를 더욱 편하고 풍부하게 사용할 수 있게 되었다.

 

Figure 1. Scenario of meChat

논문 사진 발췌.

대부분의 Intelligent assistant는 대화영 인터페이서 서버쪽에 자리잡고 있다. 그 말인 즉슨, 서버는 사진을 분석하고 수집한다는 말인데 이는 정보 보안에 민감하다.

[논문 원문 발췌]

Thus, it may lead to privacy leaks.

즉, 이는 정보 유출의 가능성도 있다. 

*흔히 2020 빅데이터 컨퍼런스에서도 가장 중요한 주제 중 하나는 데이터가 많아지면 많아질수록 데이터에 대한 보안이 가장 중요하다 하였다. 이에 대한 해결법으로 블록체인을 얘기하기도...

 

이 논문에서는 스마트폰 내부의 on-device형식인 personal assistant를 이용한 meChat을 이야기한다.

meChat의 세가지 기능.

-사용자 간의 대화 문맥을 이해한다.

-사진의 의미를 수집한다.

-마지막으로 대화 문맥과 관련 있는 의미를 찾아내어 in-device형식으로 사진을 찾는다.

 

주목해야 할 점은, meChat은 conversation text를 semantic space로 프로젝트 한다 이와 동시에 photos를 semantic space로 프로젝트 한다.

 

[논문 원문 발췌]

Thus, meChat is able to model, understant and compare different modalities on top of a single unified semantic space.

 

흥미로운 점은 앞서 언급된 것처럼 meChat은 on-device text intelligence형식이므로, intelligent assistants 를 독립형 방식으로 수행한다.

 

[논문 원문 발췌]

Thus, meChat works in a privacy-protecting manner without sending out any in-device photos out-side. It does not suffer from any communication latency to the external server.

 

-meChat SCENARIO

Figure 1. 을 살펴보면, meChat이 어떤 방식으로 사용자를 도우는지 알 수 있다. 대략 논문을 간단하게 표현해 보면, Ella는 친구와 대화를 하며, 저녁을 먹자 하고, 이에 대해 @mechat을 외치며 사진을 보내달라한다. 이에 meChat은 앞서 말한 in-device형식으로 스마트폰 내부 저장소에서 안전하게 사진을 가져온다. 여러개의 매칭된 사진을 나열시켜 보여주고 이에 사용자는 원하는 사진을 선택한다.

 

[논문 원문 발췌]

Notably, meChat intelligently searches indevice photos against the semantics of conversations on smartphones, whereas most existing intelligent assistants work it on their servers. Thus, meChat does not send in-device photos outside or suffer from communication latency to the servers.

 

-METHODOLOGY

meChat을 구상하며, ODP(Open Directory Project) 기반으로 의미적 접근을 하였다. 

의미적 접근(sementic approach)는 의미적 분류(semantic classification) 과 의미적 순위법(semantic ranking)으로 구성되 어있고 훈련은 ODP를 통해서 훈련되어졌다.

ODP를 좀더 설명하자면 최대 15레벨의 웹 디렉토리, 360만 개 이상의 웹페이지가 백만개의 카테고리로 구성되어있는 

고품질 및 대규모 분류체계 사이트이다.

 

Conversation Classification

사용자를 이해하는 것이 meChat의 본질적인 일이다. 이에 진행중인 대화가 사용자를 이해하는데 아주 중요하다는것에 중점을 두어 연구했다.

 

[논문 발췌]

We interpret the classified ODP categories as conversation contexts.

 

좀더 구체적으로 보자면, vector space model을 기반으로 한 term vector로써 대화속에 텍스트들을 표현한다.

이후 Conversation Vector와 ODP categories vector들 간에 Cosine similarity를 측정한다. 분류에서, 각각의 ODP category는 vector space model에 기반한 그 각각의 category의 의미를 표현하는 vector을 갖고있다.

이후 , meChat은 몇 가지 계산된 Cosine Similarity를 기반으로한 관련성 높은 ODP categories를 얻는다.

 

Photo Classification

in-device photos의 의미를 찾기 위해 meChat은 ODP categories로 사진들을 분류함으로써 ODP semantic space에 사진을 project한다. 

*여기서 project는 같은 의미로 벡터사영이라고 해도 이해해도 괜찮을 것 같다.

결과적으로, in-device의 object와 place 정보 두가지를 활용한다. meChat은 Tensorflow Lite에 의해 제공된 MobileNets를 사용한 사진속 object를 인식한다.

 

[논문 발췌]

In particular, meChat uses venue categories as place information obtained from Foursquare APIs. Subsequently, meChat puts both the recognized objects and the obtained venue categories together into a single contextual text, which are then classified into a few highly related ODP categories. 

 

위 과정이 meChat이 ODP category로 분류하는 과정의 세심한 내용이며 본인도 중요하다 생각해 발췌한다.

 

[논문 발췌]

Notably, meChat is able to model, understand, and compare conversation text and in-device photos on top of a single ODP semantic space, by transforming both into ODP categories. This is one-of-a-kind in the literature.

주목할 점은, meChat은 모델링하고,이해하며 대화 텍스트와 기기 내 비교 단일 ODP semantic 공간위에 있는 사진 둘다 ODP 범주로 변환한다. 이는 문학에서 유일무이하다.

 

Photo Ranking

meChat은 in-device에서 대화 문맥과 사진을 match한다.

 

[논문 발췌]

To this end, it utilizes GraphScore to effectively measure the semantic similarity between the conversation and in-device photos.

 

*특히 GraphScore는 ODP taxonomy로부터 얻은 Similarity graph에 대한 wPPR(weighted Personalized Page Rank) function을 computing함으로써  sementic similarity를 얻는다.

 

[Definition of GraphScore]

논문 발췌

where) C and P are a set of top-k classified ODP categories of conversation and in-device photos, respectively.

wPPR(c,p)는 c와 p의 semantic relevance이고, 반면에 cw(c)와 cw(p)는 각 c와 p의 classification score 이다.

 

[논문 발췌]

Finally, it obtains a ranked list of semantically relevant in-device photos to the given conversation.

 

Prototype of meChat

meChat을 테스트해보기 위해 사용한 스마트폰은 

-Samsung Galaxy S7 with Android 6.0.1 Marshmallow

-LG Nexus 5X with Android 8.1 Oreo

특히, tiny text intelligence라 불리는 on-device text intelligence를 활용했다. 이를 'me' 라고 부른다.

이것의 지식 기반은 잘 구성된 전체 892개의 category를 가진 ODP taxonomy로 만들어졌다.

ODP knowledge-base의 top에서 'me'는 semantic classification과 semantic ranking을 지원한다.

SQLite를 사용함으로써, 38.9MB 되는 크기의 'me'를 smartphone에 넣는다. 

추가적으로 meChat의 실행가능성을 시험해보기 위해 두개의 스마트폰에 messaging application을 설치한다.

*실제 구현을 보고싶다면 다음 링크를 확인해보자.  https://youtu.be/ 4BzwMrhW8Ac.

논문 사진 발췌

위 Figure 2는 meChat의 architecture을 보여준다.

'me'와 context analyzer, photo analyzer, photo searcher을 포함한 세가지 software module을 포함하고 있다.

context analyzer과 photo analyzer은 on-going 대화와 in-device 사진들을 각각 분류한다. 

photo searcher는 on-going 대화와 사진들 사이의 semantic similarity을 기반으로 in-devices 사진에 순위를 매긴다.

 

[논문 발췌]

In me Chat, the photo analyzer works offline(i.e., when taking photos), while the context analyzer and photo searcher work online(i.e., when calling meChat)

 

meChat에서, photo analyzer는 offline으로 수행하는(사진을 찍을때) 반면 context analyzer과 photo searcher은 온라인으로 수행한다(meChat을 부를때).

 

합리적인 정확성 뿐만아니라 스마트폰에 어울리는 적당한 사이즈를 고려하여 모델을 선택했는데 photo analyzer에서 기기 내 사진의 GPS에 해당하는 장소 category를 얻기 위해  Place API of Foursquare을 사용했고, in-device 사진들의 objects를 인식하기 위해  'MobileNet_v1_1.0_224_quant'이라는 computer vision model을 활용했다.

*Place API of Four Square http://(https://developer.foursquare.com/places-api/)

*MobileNet_v1_1.0_224_quant (https://github.com/ tensorflow/models/blob/master/research/slim/ nets/mobilenet_v1.md/)

Performance Evaluation

[논문 발췌]

The goal of our experiments is to examine the efficacy of meChat in daily life conversations.

 

이 실험의 목표는 일상 대화에서의 meChat의 efficacy이다. 

 

*세명의 졸업생으로부터 211개의 위치가 저장되어있는 in-device 사진들을 수집.

* 이전 작업으로부터 사용한 33개의 scenario-based 기기 내 사용 데이터를 빌린다.

* 33개의 scenario 중 오직 16개의 sms text scenario를 대화로서 가져왔다 .이유는 남은 17개의 대화는 수집한 사진과 연관성이 없기 때문!

*이에 수집한 사진과 연관이 있는 다른 시나리오에 대한 17개의 대화 데이터를 추가적으로 구축하였다.

*마침내, 평균 136 term 길이의 33개 대화 데이터를 얻었다.

Conversation and Photo Classification

-Conversation classification의 성능 테스트

 

*대화가 주어지면 상위 5개 ODP 분류 카테고리와 관련하여 문장을 추론한다.

*그 다음, 세가지 다른 크기로 각각 ODP 카테고리를 label을 부여하는데 관계있고, 어느정도 관계있고, 관계없는 것으로 처리한다.

*k정확도와 관련하여 성능을 측정한다. (이 성능 metric은 추천 알고리즘 분야에서 가장 많이 쓰이는 것)

논문 사진 발췌

위 table을 보면 meChat은 conversation classification에서 준수한 성능을 보여주고 있다.

-classifuing in-device photos

*이와 관련하여 성능에 대한 세가지 접근을 비교했다. 

1) 앞서 제안된 GPS와 일치하는 장소 정보만을 분류하는 접근. (denoted as meChat pla)

2) 사진객체만을 단지 분류하는 접근 (denoted as meChat obj)

3) 위치정보와 객체 두가지를 넣음으로써 생성된 문맥 텍스트를 분류하는 접근. (denoted as meChat pla+obj)

 

*여기서 meChat(pla)가 가장 나쁜 성능을 보여줬다. 이유는 대부분 in-device photos는 더 낮은 위치 정보에 대한 정확도를 제공해주는 도심지역에서 찍혔기 때문이다.

*또한, 각 장소에서 얻어진 장소 category는 일반적으로 classification 성능을 낮게하는 결과를 만든 적어도 하나의 관련없는 category를 포함한다.

*meChat(obj)도 여전히 낮은 성능을 보여준다. 경험적으로 MobileNets의 낮은 객체 인식성능으로부터 이 결과가 나온건데 이는 5가지 정확도분석과 관련하여 거의 18퍼센트정도 낮게 측정되었다.


*주목해야할점은, meChat(pla+obj)는 가자 좋은 성능을 보여주었다. 특히, 정확성과 관련하여 meChat(pla)보다는 58.5%, meChat(obj) 보다는 13.9% 이상의 향상을 보여주었다. 

 

[논문 발췌]

The above experimental results clearly show that it is effective to project both place information and objects of photos on an ODP semantic space with respect to enriching the semantics of photos.

 

하지만, 전반적인 현실의 in-device photos분류의 저조한 성능은 다양한 관점에서 개선할 방법이 여전히 많다는걸 의미한다.

 

-Photo Ranking 성능 테스트

대화와 관련하여 정확성에 대한 k 와 MRR(mean reciprocal rank)를 이용해 in-device photo ranking을 평가하였다.

오직 top-ranked in-device photo에만 집중을 하였다. 왜냐하면 스마트폰은 사진을 보여주기에는 너무나 제한적인 화면이기 때문.

*기본적으로, syntactic 접근을 사용하였다. 이 접근은 대화와 in-device photo로부터 추출한 정보사이의 cosine similarity를 기반으로 하였다. 

논문 사진 발췌

*위 사진을 보면, meChat(pla+obj)는 k와 MRR정확성에 대한 접근에서 다른 것보다 월등한 성능을 보여줬다.

*구체적으로, meChat(pla+obj)의 성능은 세가지 추천된 사진들에 대해 적어도 하나는 공유되어진다는 것이다.

*위 깊이 분석을 통해서, 다음과 같은 observations를 만들었다.

[논문 발췌]

1) Utilizing both place information and objects of photos is much more effective than utilizing either of them alone. 2) Each semantic approach (i.e., meChat) achieves better performance than corresponding syntactic approach (i.e., Keyword).

 

1)place information과 photos의 objects 두개를 활용하는 것이 각각을 따로 사용하는것보다 더 나은 성능을 보여준다.

2)각각의 semantic approach는 일치하는 syntactic approach보다 더 나은 성능을 보여준다.

 

-System 성능 테스트

이를 시험하기에 앞서 Samsung Galaxy S7 smartphone 의 런타임과 에너지 소비를 먼저 측정하였다. 

*우선 런타임과 관련하여 클라우드 기반 text classification 과 meChat을 비교했다.

논문 사진 발췌

위 table3와 같이 meChat이 IBM보다 더 나은 성능을 보여준다. 

*런타임의 차이점은 mobile broadband 환경에 따라 증가한다. 

*Monsoon power meter을 사용한 meChat의 에너지 소비를 추가로 측정하였다.

*이는 음악을 듣는것 에너지 소비와 동일하다. 

*이후에, meChat과 Microsoft cognitive service의 image classification을 사용한 Mobile-Nets을 비교했다.

*클라우드 기반 Ms categorize는 그들의 86가지 category를 기반으로한 image categorizing을한다. 이에 대해 사진 한장에 MobileNets은 0.160s 가 걸리고 반면 클라우드 기반 MS는 7.338걸린다.

 

[논문 발췌]

The above-mentioned experimental results quantitatively indicate that privacy and latency benefit greatly from ondevice intelligence.

 

CONCLUSION

외부 서버에 의존하지 않은 스스로 작업을 할 수 있는 meChat은 사용자들에게 in-device photos와 관련하여 의미적인 공유를 도와줄 수 있다. 

외부 서버를 사용할 시에는 보안에 대해 가장 민감하기에 위 방법은 아주 참신하다.

 

[논문 발췌]

We believe meChat is oneof-a-kind personal assistant in that it works in a privacy-protecting manner with low perceived latency and energy consumption

 

meChat의 고유한 기능은 model,understand를 가능하게 해주고  통합된 ODP sematic spacec에서 in-device와 photos에 대한 대화 문맥을 비교할 수 있게 해준다.

 

2021-01-18~ 2021-01-21

meChat: In-Device Personal Assistant for Conversational Photo Sharing.

논문 해석 및 리뷰 완료.

-불굴의관돌이

3.3 다차원 배열

-다차원 배열

넘파이의 다차원 배열을 사용한 계산법을 숙달하면 신경망을 효율적으로 구현 가능하다.

다차원 배열도 기본은 '숫자의 집합'인데, N차원으로 나열하는 모든 것을 통틀어 다차원 배열이라 한다.

import numpy as np
A=np.array([1,2,3,4]) # 1차원 배열
print(A)
[1 2 3 4]

np.ndim(A)
1
A.shape
(4,)
A.shape[0]
4

B=np.array([[1,2],[3,4], [5,6]) #2차원 배열
print(B)
[[1 2]
 [3 4]
 [5 6]]
 
np.ndim(B)
2
B.shape
(3,2)

배열에 대해서는 다들 잘 알것이기에 간단하게 생략하고 넘어가겠다.

-행렬의 내적(행렬 곱)

이공계 출신이라면 당연히 내적을 할 줄 알것이라 믿는다. 그러므로 설명은 생략하고 파이썬을 통해 어떻게 내적을 구현하는지 살펴보자.

 

 

A=np.array([[1,2],[3,4]]) #2차원 배열 #2x2 행렬
B=np.array([[5,6],[7,8]]) #2차원 배열 #2x2 행렬

np.dot(A,B)

array([[19, 22],
       [43, 50]])

우리는 행렬의 내적을 할때 numpy 함수 np.dot()을 통해 계산할 수 있다. np.dot()은 넘파이 배열 2개를 인수로 받아 그 내적을 반환한다. 하지만 주의해야할 점은 np.dot(A,B)와 np.dot(B,A)는 다른 값이 될 수 있다는 점을 꼭 기억하자. 

 

또한 우리가 행렬의 곱을 할때는 중요한 점이 하나 있는데, 

행렬의 곱에서는 대응하는 차원의 원소 수를 일치시켜라.

다음과 같이 곱에서 차원의 원소수는 일치시켜야 한다. 그것이 약속이다!

 

-신경망의 내적

이번에는 신경망을 생각해보자, 

행렬의 곱으로 신경망의 계산을 수행한다

위 신경망은 편향과 활성화 함수를 생략하고 가중치만 갖는다. 위 그림을 파이썬으로 구현해보자.

X=np.array([1,2]) #1x2 행렬
W=np.array([[1,3,5],[2,4,6]]) #2x3 행렬
Y=np.dot(X,W)
print(Y)

[ 5 11 17]

 

-3층 신경망 구현하기

2층신경망까지 구현했다면, 3층 신경망은 어떨까? 

3층 신경망

위 신경망은 입력층(0층)은 2개, 첫 번째 은닉층(1층)은 3개, 두 번째 은닉층(2층)은 2개, 출력층(3층)은 2개의 뉴런으로 구성된다.

 

표기법은 다음과 같다.

 

중요한 표기

-각 층의 신호 전달 구현하기

우선 그림을 확인해보자.

입력층에서 1층으로 신호 전달

편향을 뜻하는 뉴런 (1)이 추가되었다. 편향은 오른쪽 아래 인덱스가 하나밖에 없다는 것에 주의하자.

또한, 편향의 입력 신호는 항상 1임을 기억하자.

계산은 다음과 같다. 설명은 생략하겠다.

where)

코딩으로 구현하면,

X=np.array([1.0,.5])
W1=np.array([[.1,.3,.5],[.2,.4,.6]])
B1=np.array([.1,.2,.3])

A1=np.dot(X,W1)+B1
print(A1)

[0.3 0.7 1.1]

다음은 1층의 활성화 함수에서의 처리를 살펴보자. 이 과정은 다음과 같다.

입력층에서 1층으로의 신호 전달

위 그림과 같이 은닉층에서의 가중치 합(가중 신호와 편향의 총합)을 a로 표기하고 활성화 함수h()로 변환된 신호를 z로 표기한다. 여기서 활성화 함수로 앞선 포스팅에서 배운 시그모이드를 사용한다. 

코딩으로 살펴보자

def sigmoid(x):
    return 1/(1+np.exp(-x))
X=np.array([1.0,.5])
W1=np.array([[.1,.3,.5],[.2,.4,.6]])
B1=np.array([.1,.2,.3])

A1=np.dot(X,W1)+B1
Z1=sigmoid(A1)
print(A1)
print(Z1)

[0.3 0.7 1.1]
[0.57444252 0.66818777 0.75026011]

 

그 다음은 1층에서 2층으로 가는 과정을 살펴보자. 우선 그림은 다음과 같다.

1층에서 2층으로의 신호 전달

구현 방식은 똑같다.

이제 코딩으로 구현해보자.

def sigmoid(x):
    return 1/(1+np.exp(-x))
X=np.array([1.0,.5])
W1=np.array([[.1,.3,.5],[.2,.4,.6]])
B1=np.array([.1,.2,.3])

A1=np.dot(X,W1)+B1
Z1=sigmoid(A1)
W2=np.array([[.1,.4],[.2,.5],[.3,.6]])
B2=np.array([.1,.2])

A2=np.dot(Z1,W2)+B2
Z2=sigmoid(A2)

print(A1)
print(Z1)
print(A2)
print(Z2)

[0.3 0.7 1.1]
[0.57444252 0.66818777 0.75026011]
[0.51615984 1.21402696]
[0.62624937 0.7710107 ]

마지막으로 2층에서 출력층으로의 신호 전달을 살펴보자. 우선은 그림은 다음과 같다.

2층에서 출력층으로의 신호 전달

출력층의 구현도 그동안의 구현과 거의 동일하다. 딱 한가지 다른점은, 활성화 함수만 지금까지의 은닉층과 다르다는 점이다. 

이를 코딩으로 구현해보자.

def sigmoid(x):
    return 1/(1+np.exp(-x))
X=np.array([1.0,.5])
W1=np.array([[.1,.3,.5],[.2,.4,.6]])
B1=np.array([.1,.2,.3])

A1=np.dot(X,W1)+B1
Z1=sigmoid(A1)
W2=np.array([[.1,.4],[.2,.5],[.3,.6]])
B2=np.array([.1,.2])

A2=np.dot(Z1,W2)+B2
Z2=sigmoid(A2)

def identity_function(x):
    return x

W3=np.array([[.1,.3],[.2,.4]])
B3=np.array([.1,.2])

A3=np.dot(Z2,W3)+B3
Y=identity_function(A3)
print(A1)
print(Z1)
print(A2)
print(Z2)
print(A3)
print(Y)

[0.3 0.7 1.1]
[0.57444252 0.66818777 0.75026011]
[0.51615984 1.21402696]
[0.62624937 0.7710107 ]
[0.31682708 0.69627909]
[0.31682708 0.69627909]

 

-출력층 설계하기

신경망은 분류와 회귀 모두에 이용 가능하다. 다만 둘 중 어떤 문제냐에 따라 출력층에서 사용하는 활성화 함수가 달라진다. 일반적으로 회귀에는 항등 함수를 분류에는 소프트맥스 함수를 사용한다. 

활성함수는 앞선 포스트에 열심히 다뤄놓았으니 필요하면 꼭 찾아서 보길 바란다.

 

-출력층의 뉴런 수 정하기

출력층의 뉴런 수는 주어진 문제에 맞게 적절히 정해야 한다. 일반적으로 분류하고 싶은 클래스 수로 설정한다. 책의 예를 빌리자면, 흔히 MNIST 과정에서 숫자 이미지 0~9중 하나로 분류하는 문제라면 출력층의 뉴런의 수는 10개로 설정한다.

 

뒤에 손글자 숫자 인식은 너무나 흔하며, 실제 이 과정을 더 심화로 다루는 것이 뒤에 나오니 지금은 생략하도록 하겠다. 그럼 20000

'Deep Learning > 밑바닥부터 시작하는 딥러닝(1)' 카테고리의 다른 글

Chapter 6. 학습 관련 기술들(1)  (0) 2021.03.02
Chapter 5. 오차역전파법  (0) 2021.01.25
Chapter 4. 신경망 학습  (0) 2021.01.23
Chapter 3-1 신경망  (0) 2021.01.17
2.1 퍼셉트론이란?  (0) 2021.01.17

가중치 매개 변수의 적절한 값을 데이터로부터 자동으로 학습하는 능력이 이제부터 살펴볼 신경망의 중요한 성질이다. 

3.1 퍼셉트론에서 신경망으로 

3.1.1 신경망의 예

신경망을 그림으로 나타내면 위와 같다. 여기서 왼쪽부터 입력층, 은닉층, 출력층이라고 한다. 은닉층의 뉴런은 (입력층이나 출력층과 달리) 사람 눈에는 보이지 않는다. 그래서 '은닉'이라고한다.

*위 그림의 신경망은 모두 3층으로 구성되지만, 가중치를 갖는 층은 2개뿐이기 때문에 '2층 신경망'이라고 한다. 문헌마다 다를 수도 있으니 주의하자.

 

3.1.2 퍼셉트론 복습 

퍼셉트론에 관하여는 이전 포스팅에 잘 정리해 놓았으므로 생략한다.

 

3.1.3 활성화 함수의 등장

입력 신호의 총합을 출력 신호로 변환하는 함수를 일반적으로 활성하 함수(activation function)라 한다. '활성화'라는 이름이 말해주듯 활성화 함수는 입력 신호의 총합이 활성화를 일으키는지를 정하는 역할을 한다.

a=b+w1x1+w2x2 [식 3.4]

식 3.4는 가중치가 달린 입력 신호와 편향의 총합을 계싼하고 이를 a라 한다.

y=h(a) [식 3.5]

식 3.5는 a를 함수 h()에 넣어 y를 출력하는 흐름이다.

이를 그림으로 나타내면 다음과 같다.

활성화 함수의 처리 과정

가중치 신호를 조합한 결과가 a라는 노드가 되고, 활성화 함수 h()를 통과하여 y라는 노드로 변환하는 과정이 나타나 있다.

왼쪽은 일반적인 뉴런, 오른쪽은 활성화 처리 과정을 명시한 뉴런(a는 입력 신호의 총합, h()는 활성화 함수, y는 출력)

3.2 활성화 함수

이는 개인적인 포스팅을 하겠다.

출처는 blog.naver.com/handuelly/221824080339에서 많은 자료를 공유했다.

 

1. Sigmoid 함수

신경망에서 자주 사용하는 함수이다. 이는 흔히 통계학에서 사용하는 Logistic 함수라고 불리기도 하며, input 값에 따라 0~1을 출력하는 s형 곡선 함수이다. 식과 정의는 다음과 같다.

시그모이드 함수의 정의
시그모이드 함수의 미분 과정

코딩으로 출력해보면,

import numpy as np
import matplotlib.pylab as plt

def sigmoid(x):
    return 1/(1+np.exp(-x))

x=np.linspace(-10,10,100)
y=sigmoid(x)
plt.figure(figsize=(4,4))
plt.plot(x,y,'black',linewidth=3)

plt.xlim(-10,10)
plt.grid(True)
plt.show()

 

시그모이드함수 그래프

2. Tanh 함수

Tanh함수는 쌍곡선 함수 중 하나로, 시그모이드 함수를 변형해서 얻을 수 있다. 

tanh함수의 정의와 미분

코딩으로 출력해보면,

def tanh(x,diff=False):
    if diff:
        return (1+tanh(x))*(1-tanh(x))
    else:
        return np.tanh(x)
        
x=np.linspace(-10,10,100)
y=tanh(x)
plt.figure(figsize=(4,4))
plt.plot(x,y,'black',linewidth=3)

plt.xlim(-10,10)
plt.grid(True)
plt.show()

tanh함수 그래프

3. ReLU

ReLu(Rectified Linear Unit,경사함수)는 가장 많이 사용되는 활성화 함수 중 하나이다.

Sigmoid와 tanh가 갖는 Gradient Vanishing문제를 해결하기 위한 함수이다.

ReLu함수 정의

코딩으로 출력해보면,

def relu(x):
    return np.maximum(0,x)
    
x=np.linspace(-10,10,100)
y=relu(x)
plt.figure(figsize=(4,4))
plt.plot(x,y,'black',linewidth=3)
plt.xlim(-10,10)
plt.grid(True)
plt.show()

ReLu함수 그래프

4. Leaky ReLU

Leaky ReLU는 ReLU가 갖는 Dying ReLU(뉴런이 죽는 현상)을 해결하기 위해 나온 함수이다. 

Leaky ReLU 정의

위 출처 자료에서는 0.01이 아니라 매우 작은 값이라면 무엇이든 사용 가능하다고 한다.

Leaky ReLU는 x가 음수인 영역의 값에 대해 미분값이 0이 되지 않는다는 점을 제외하면 ReLU의 특성을 동일하게 갖는다.

 

코딩으로 출력해보면,

a=0.1
def leaky_ReLU(x):
    return np.maximum(a*x,x)

x=np.arange(-5.0,5.0,.1)
y=leaky_ReLU(x)

plt.plot(x,y)
plt.plot([0,0],[5.0,0.0],':')
plt.title('Leaky ReLU Function')
plt.show()

Leaky ReLU 그래프

5.PReLU

Leaky ReLU와 거의 유사하지만 새로운 파라미터 α를 추가해 X가 음수인 영역에서도 기울기를 학습한다.

코딩으로 출력하면,

b=0.5  #새로운 파라미터 값은 임의로
def PReLU(x):
    return np.maximum(b*x,x)

x=np.arange(-5.0,5.0,.1)
y=PReLU(x)

plt.plot(x,y)
plt.plot([0,0],[5.0,0.0],':')
plt.title('PReLU Function')
plt.show()

PReLU 그래프

6. ELU

Exponential Linear Unit은 ReLU의 모든 장점을 포함하며 Dying ReLU문제를 해결했다. 출력 값이 거의 zero-centered에 가까우며, 일반적인 ReLU와 다르게 exp 함수를 계산하는 비용이 발생한다.

ELU 정의

코딩으로 표현해보면, 

def ELU(x,alpha):
    return (x>0)*x +(x<=0)*(alpha*(np.exp(x)-1))
x=np.arange(-5.0,5.0,.1)
y1=PReLU(x)
y2=ELU(x,alpha)
plt.plot(x,y2)
plt.plot([0,0],[5.0,0.0],':')
plt.title('ELU Function')
plt.show()

ELU 그래프

개형은 ReLU와 유사하며, 0보다 작은 경우는 alpha값을 이용해서 그래프를 부드럽게 만든다. 때문에 elu를 미분해도 부드럽게 이어지는 모습을 확인할 수 있다.

 

7. 소프트맥스 함수

은닉층에서 ReLU(또는 ReLU 변형) 함수들을 사용하는 것이 일반적이지만 그렇다고 해서 앞서 배운 시그모이드 함수나 소프트맥스 함수가 사용되지 않는다는 의미는 아니다. 분류 문제를 로지스틱 회귀와 소프트맥스 회귀를 출력층에 적용하여 사용한다.

소프트 맥스 함수정의

코딩으로 출력해보면, 

def Softmax(x):
    return np.exp(x)/np.sum(np.exp(x))

x=np.arange(-5.0,5.0,.1)
y1=Softmax(x)
plt.plot(x,y1)
plt.title('Softmax Function')
plt.show()

Softmax 그래프
전체비교

 

 

(이미지 출처 : http://cs231n.github.io/neural-networks-3/)

 

참고자료: blog.naver.com/handuelly/221824080339

 

딥러닝 - 활성화 함수(Activation) 종류 및 비교

# 활성화 함수딥러닝 네트워크에서 노드에 입력된 값들을 비선형 함수에 통과시킨 후 다음 레이어로 전달하...

blog.naver.com

 

퍼셉트론(perceptron) 알고리즘에 관한 내용이다. 퍼셉트론은 프랑크 로젠블라트가 1957년에 고안한 알고리즘이다. 흔히 딥러닝을 시작할 때 퍼셉트론(perceptron)의 개념에 대해서 알고 공부를 한다. 이 고대 화석같은 알고리즘을 왜 배우는가 하면 이는 퍼셉트론이 신경망(딥러닝)의 기원이기 때문이다. 

 

 

1. 퍼셉트론이란?

퍼셉트론은 다수의 신호를 입력으로 받아 하나의 신호를 출력한다. 이 책에서는 흔히 신호를 전류나 강물처럼 흐름이 있는 것으로 상상하는게 좋다고 하나, 본인은 그런 상상은 잘 안든다..

 

우선 퍼셉트론은 입력이 0과 1로 두가지 값을 갖을 수 있다.

 

 위 그림에서 

-x1 과 x2는 입력 신호, y는 출력 신호, w1과 w2는 가중치를 의미한다.

-원을 뉴런 또는 노드라고 부른다.

-입력 신호가 뉴런에 보내질 때는 각각 고유한 가중치가 곱해진다.(x1*w1,x2*w2).

-뉴런에서 전달 받은 신호의 총합이 임계값 theta를 넘을 때만 1을 출력한다.

이를 수식으로 나타내면,

 

퍼셉트론은 복수의 입력 신호 각각에 고유한 가중치를 부여한다. 가중치는 각 신호가 결과에 주는 영향력을 조절하는 요소로 작용하며, 가중치가 클수록 신호가 그만큼 더 중요함을 뜻한다.

 

 

2. 단순한 논리 회로

AND 게이트

AND Gate 진리표

이 그림은 AND게이트의 진리표로, 두 입력이 모두 1일 때만 1을 출력하고 그 외에는 0을 출력한다.

 

NAND 게이트와 OR 게이트

 

NAND 게이트는 Not AND를 의미하며, 그 동작은 AND게이트의 출력을 뒤집은 것이다. 

진리표로 확인해보면, 

NAND Gate 진리표

위 그림처럼 x1과 x2가 모두 1일 때만 0을 출력, 그 외에는 모두 1을 출력한다.

 

OR 게이트는 입력 신호 중 하나 이상이 1이면 출력이 1이 되는 논리 회로이다. 

진리표를 확인해보면,

 

OR gate 진리표

3. 퍼셉트론 구현하기

3.1 간단한 구현.

위에서 배운 논리 회로를 간단하게 파이썬으로 구현해보자.

def AND_Func(x1,x2):
    w1,w2,theta=.5,.5,.7
    tmp=x1*w1+x2*w2
    if tmp<=theta:
        return 0
    elif tmp>theta:
        return 1

위 함수는 x1과 x2를 인수로 받는 AND_Func함수이다. 매개변수 w1,w2,theta는 함수 안에서 초기화하고, 가중치를 곱한 입력의 총합이 임계값을 넘으면 1을 반환하고 그 외에는 0을 반환한다.

 

inputs=[(0,0),(1,0),(0,1),(1,1)]

for x1,x2 in inputs:
    y=AND_Func(x1,x2)
    print('({x1},{x2})->{y}'.format(x1=x1,x2=x2,y=y))
    
(0,0)->0
(1,0)->0
(0,1)->0
(1,1)->1

다음 과 같이 input튜플리스트를 만든 후, 이 값을 차례로 AND_Func에 집어넣으면 위와 같은 출력값이 나온다.

 

3.2 가중치와 편향 도입

앞에서 구현한 AND게이트는 직관적이고 알기 쉽지만, 다른 방식으로 수정해보자. 

식 2.1

위 식에서 theta를 -b로 치환하면 퍼셉트론의 동작은 다음과 같다.

식 2.2

기호 표기만 변경되었지, 그 의미는 같다. 여기서는 b를 편향이라고 하며 w1과 w2는 그대로 가중치이다.

해석해보면, 퍼셉트론은 입력 신호에 가중치를 곱한 값과 편향을 합하여, 그 값이 0을 넘으면 1을 출력, 그렇지 않으면 0

을 출력한다. 파이썬으로 확인해보자.

 

 

가중치와 편향을 도입한 AND gate

import numpy as np
def AND_Func_Bias(x1,x2):
    x=np.array([x1,x2])
    w=np.array([.5,.5])
    b=-.7
    tmp=np.sum(w*x)+b
    if tmp<=0:
        return 0
    else:
        return 1

위 함수는 theta가 -b로 치환된 함수이다. 또한, 편향은 가중치와 다르다는 사실에 주의하자. 구체적으로 설명하자면 w1과 w2는 각 입력 신호가 결과에 주는 영향력(중요도)을 조절하는 매개변수고, 편향은 뉴런이 얼마나 쉽게 활성화(결과로 1을 출력)하느냐를 조절하는 매개변수이다. 추가적으로 과녁의 예를 들 수있다. 이는 통계학 카테고리에 따로 설명을 해놓겠다.

inputs=[(0,0),(1,0),(0,1),(1,1)]

for x1,x2 in inputs:
    y=AND_Func_Bias(x1,x2)
    print('({x1},{x2}=>{y})'.format(x1=x1,x2=x2,y=y))
    
    
(0,0=>0)
(1,0=>0)
(0,1=>0)
(1,1=>1)

 

가중치와 편향을 도입한 NAND gate & OR gate

def NAND_Bias_Func(x1,x2):
    x=np.array([x1,x2])
    w=np.array([-.5,-.5])
    b=.7
    tmp=np.sum(w*x)+b
    if tmp<=0:
        return 0
    else:
        return 1
    
def OR_Bias_Func(x1,x2):
    x=np.array([x1,x2])
    w=np.array([.5,.5])
    b=-.2
    tmp=np.sum(w*x)+b
    if tmp<=0:
        return 0
    else:
        return 1
    

 

#NAND 
for x1,x2 in inputs:
    y=NAND_Bias_Func(x1,x2)
    print('({x1},{x2}=>{y})'.format(x1=x1,x2=x2,y=y))    
    
(0,0=>1)
(1,0=>1)
(0,1=>1)
(1,1=>0)

#OR
for x1,x2 in inputs:
    y=OR_Bias_Func(x1,x2)
    print('({x1},{x2}=>{y})'.format(x1=x1,x2=x2,y=y))

(0,0=>0)
(1,0=>1)
(0,1=>1)
(1,1=>1)

 

4. 퍼셉트론의 한계

XOR 게이트

XOR 게이트는 배타적 논리합이라는 논리 회로이다. x1과 x2 중 한쪽이 1일 때만 1을 출력한다. 즉 자기 외에는 거부한다는 의미이다.

XOR 게이트 진리표

이 XOR게이트로 우리는 퍼셉트론을 구현하려면 가중치 매개변수 값을 어떻게 설정할까? 사실 지금까지 본 퍼셉트론으로는 이 XOR게이트를 구현할 수 없다. 

 

즉, 단층 퍼셉트론으로 AND,NAND,OR 게이트는 구현 가능하지만, XOR게이트는 구현할 수 없다. 퍼셉트론은 위와 같이 직선으로 나뉜 두 영역을 만든다. 하지만 XOR은 직선으로 두 영역을 나눌 수 없다.

 

선형과 비선형

퍼셉트론은 직선 하나로 나눈 영역만 표현할 수 있다는 한계가 있다. 위 그림과 같은 곡선은 표현할 수 없다는 의미이다. 위 그림과 같은 곡선의 영역을 비선형 영역, 직선의 영역을 선형 영역이라고 한다. 선형, 비선형이라는 말은 기계학습 분야에서 자주 쓰이는 용어이다.

 

다층 퍼셉트론

퍼셉트론의 아름다움은 '층을 쌓아' 다층 퍼셉트론을 만들 수 있다는데 있다. 우선, XOR 게이트 문제를 다른관점에서 생각해보자.

 

-기존 게이트 조합하기

XOR 게이트를 만든느 방법은 다양하다. 그중 하나는 앞서 만든 AND,NAND,OR 게이트를 조합하는 방법이다. 

AND,NAND,OR 게이트 기호
AND,NAND,OR 게이트를 조합해 구현한 XOR게이트

위 조합이 정말 XOR를 구현하는지 살펴보자. NAND의 출력을 s1, OR의 출력을 s2로 해서 진리표를 만들면 밑에 그림처럼 된다. 

XOR게이트의 진리표

-XOR 게이트 구현하기

def XOR(x1,x2):
    s1=NAND_Bias_Func(x1,x2)
    s2=OR_Bias_Func(x1,x2)
    y=AND_Func_Bias(s1,s2)
    return y
for x1,x2 in inputs:
    y=XOR(x1,x2)
    print('({x1},{x2}=>{y})'.format(x1=x1,x2=x2,y=y))
    
(0,0=>0)
(1,0=>1)
(0,1=>1)
(1,1=>0)

이로써 XOR 게이트를 완성했다. 지금 구현한 XOR를 뉴런을 이용한 퍼셉트론으로 표현하면 다음과 같다.

XOR의 퍼셉트론

 

이전 포스팅에서 판다스 자료구조인 Series에 대해 알아보았다. 이번에는 DataFrame(데이터프레임)에 대해 알아보자.

시리즈가 엑셀의 한 열과 동일한 이해구조를 본다면, 데이터프레임은 그 열을 merge한 엑셀 시트라고 볼 수 있다.

컴퓨터 공학도에게는 DB를 알기에 데이터베이스와 동일하다고도 볼 수 있다.

 

DataFrame의 정의:

DataFrame(데이터프레임)은 numpy의 ndArray를 기반으로 한 행과 열로 이루어진 자료구조이다.

 

판다스 데이터프레임 속성은 다음과 같다.

 

https://dsbook.tistory.com/12

 

데이터프레임 생성법부터 알아보자.

생성법은 크게 3가지가 있다. list를 이용한 생성, Dictionary를 이용한 생성, Series를 이용한 생성

각각 한번 알아보자.

list를 이용한 생성법.

이 list생성도 두가지로 분류 할 수 있다. 1자원 리스트와 멀티 리스트로 볼 수 있다.

-1차원 리스트

single_list=['a','b,','c','d']
df=pd.DataFrame(single_list)
df


	0
0	a
1	b
2	c
3	d

본인도 데이터를 다루며 이런식의 데이터프레임은 잘 사용하지 않으므로 설명은 생략하겠다.

-멀티 리스트

multi_list=[['kwandoll',27],['suuuuuk',27],['heeeee',30]]
df=pd.DataFrame(multi_list)
df


0	1
0	kwandoll 27
1	suuuuuk	27
2	heeeee	30

위 리스트를 이용한 생성을 보면 칼럼들에 대한 변수명은 0 과 1이다. 만일 dimension을 늘리면 늘릴수록 칼럼명은 0,1,2,3,4~~~식으로 간다는 건데, 일반적으로 이해하는게 어렵다.

 

그렇기에, 카럼명을 지정해줘야 한다.

multi_list=[[['kwandoll',27],['suuuuuk',27]],[['heeeee',30]]]
df=pd.DataFrame(multi_list,columns=['name','age'])
df


name	age
0	kwandoll 27
1	suuuuuk	27
2	heeeee	30

이러한 작업을 피하기 위해, 본인은 파이썬에 꽃인 Dictionary자료구조를 제일 많이 사용한다. 실제 데이터 분석을 할경우 같은 의미를 뽑아야하는 경우가 있으며, 데이터프레임 내에 처리가 안될 경우에는 value들을 직접 뽑아서 처리하고 DataFrame에 다시 저장하는 경우가 비일비재하다. 그러므로 이번에 설명하는 Dictionary를 이용한 생성을 유심히 보길 바란다.

 

Dictionary를 이용한 생성

딕셔너리 자료구조로 생성할때 두가지가 있다. 단순 Dictionary로 생성하기, Dictionary로 구성된 리스트로 생성하기.

우선 단순 Dictionary로 생성하는 법을 알아보자.

 

-단순 Dictionary로 생성

Dictionary.keys()는 column명을 생성하고, Dictionary.values()는 data역할을 한다.

단, 각 딕셔너리의 길이는 동일해야한다. 

data={'lee':[1,2,3],'kwan':[2,3,4],'doll':[3,4,5]}
df=pd.DataFrame(data)
df


	lee	kwan	doll
0	1	2	3
1	2	3	4
2	3	4	5

그럼 만일 dictionary.values()의 길이가 다르다면 어떨까

data={'lee':[1,2,3],'kwan':[2,3],'doll':[3,4,5]}
df=pd.DataFrame(data)
df

ValueError: arrays must all be same length

 

다음과같은 에러가 뜬다. 즉 이말은 values에 있는 리스트의 길이는 동일해야한다는 뜻이다.

 

다음은 Dictionary로 구성된 리스트로 생성하는 법을 알아보자.

 

-Dictionary로 구성된 리스트 생성

data=[{'lee':1,'kwan':2},{'lee':3,'doll':4},{'king':5,'laa':6}]
df=pd.DataFrame(data)
df

	lee	kwan	doll	king	laa
0	1.0	2.0	NaN	NaN	NaN
1	3.0	NaN	4.0	NaN	NaN
2	NaN	NaN	NaN	5.0	6.0

data=[]에 들어있는 리스트의 순서대로 각 인덱스가 인덱스로, 즉, dict1, dict2, dict3이 각각 0,1,2가 되는것이다.

또한 Dictionary.keys()는 column명으로, dictionary.values()는 data로 저장된다. 또한, 비어있는 부분은 모두 NaN값으로 처리한다.

 

여기서 만약 인덱스와 칼럼명을 지정해준다면 다음과 같이 변형된다.

data=[{'lee':1,'kwan':2},{'lee':3,'doll':4},{'king':5,'laa':6}]
df_1=pd.DataFrame(data,index=['zero','first','second'],columns=['lee','kwan','doll','king','laa'])
df_1

	lee	kwan	doll	king	laa
zero	1.0	2.0	NaN	NaN	NaN
first	3.0	NaN	4.0	NaN	NaN
second	NaN	NaN	NaN	5.0	6.0

위 데이터에서 key인 'lee'는 value를 2개 갖고있다. 그러므로 이것이 데이터프레임으로 처리할때는 순서대로 저장되는 것이다.

 

마지막으로 Sereis를 이용한 생성을 살펴보자.

 

Series를 이용한 생성

 

앞선 포스트처럼 시리즈는 액셀의 한 열이다. 그렇다면? 병합한다는 개념을 생각해보자.

series 또한 생성할때 리스트로 생성하는 법과 딕셔너리로 생성하는 법이 있었다. 

두가지를 각각 살펴보자.

 

-리스트로 생성된 Series를 이용한 생성

 

a=pd.Series([11,22,33],['a','b','c'])
b=pd.Series([44,55,66],['d','e','f'])
c=pd.Series([77,88,99],['i','j','k'])
df=pd.DataFrame([a,b,c])
df

	a	b	c	d	e	f	i	j	k
0	11.0	22.0	33.0	NaN	NaN	NaN	NaN	NaN	NaN
1	NaN	NaN	NaN	44.0	55.0	66.0	NaN	NaN	NaN
2	NaN	NaN	NaN	NaN	NaN	NaN	77.0	88.0	99.0

어떤 형식인지 이해해보자. 각 시리즈는 이차원 리스트로 되어있으며, Data와 Column으로 구성되어있다.

즉 이것을 모두 병합하여 처리를한다면, 위 결고와 같이 각각의 Column명으로 구성된 abcdefijk와 각각의 

데이터로 구성된 11,22,33,44,55,66,77,88,99로 된다. 

만일 예를들어, a라는 칼럼이 중복되어 11,33으로 되어있다면, a칼럼의 0번째 인덱스는 11,1번째 인덱스는 33으로 되는 것이다.

 

-딕셔너리로 생성된 Series를 이용한 생성

 

example={'lee':pd.Series([85,180],['weight','height']),'park':pd.Series([52,168],['weight','height'])}
df=pd.DataFrame(example)
df


	lee	park
weight	85	52
height	180	168

위 코드를 살펴보면 'lee'라는 칼럼명에는 시리즈 첫번째 리스트에 있는 값들이 그 다음에 지정해놓은 인덱스로 각각 처리되어 저장되는 모습이다. 실제 이런 것들은 직접 코딩을 해보고 눈으로 확인을 해봐야한다.

 

지금까지 크게 3가지로 구성된 자료구조로 어떻게 데이터프레임을 만들지는 상황마다 다르며 각자 쓰는 방법마다 다르다. 그러므로 이 세가지를 잘 숙지하는 것도 데이터를 다루는 능력에 절반은 차지할것이라고 생각한다(개인적 생각).

 

 

'Data analysis > Pandas' 카테고리의 다른 글

pandas.DataFrame.apply  (0) 2021.03.16
[Pandas] Series  (0) 2021.01.15
[Pandas] DataFrame 특정 칼럼 혹은 인덱스 선택  (0) 2021.01.13
[Pandas] DataFrame(개념)  (0) 2021.01.13

판다스는 소프트웨어 라이브러리 용으로 작성된 파이썬 프로그래밍 언어 데이터 조작 및 분석을 위한 패키지라고 볼 수 있다. 특히, 수치표와 시계열을 조작하기위한 데이터 구조와 연산을 제공해준다. 

 

판다스의 Series

판다스의 시리즈를 이해하기에 앞서 우리가 자주 접하는 엑셀을 한번 생각해보자. 

엑셀은 칼럽이 있고, 열들이 있다. 그 구조를 이해한 상태에서 시리즈를 직관적으로 이해한다면, 시트의 열 하나를 떠올리면된다. 

시리즈의 속성은 index와 values가 있다. 이는 아래 예시를 통해 한번 확인해보자.

시리즈를 생성하는 방법은 list형식, dictionary형식이 있다. 

우선 list형식으로 생성해보자.

example1=pd.Series(["undead","Kwandoll","JJang"])
example1

0      undead
1    Kwandoll
2       JJang
dtype: object

그 다음은 dictionary형식으로 생성해보자.

example2=pd.Series({"undead":33,"Kwandoll":22,"JJang":11})
example2

undead      33
Kwandoll    22
JJang       11
dtype: int64

차이점은 무엇일까?

1. 리스트 형식과 딕셔너리 형식의 생성 기호는 다르다. 즉 리스트는 [] 딕셔너리는 {} 이다. 이는 파이썬 기본을 공부한다면 누구나 아는 얘기지만, 모를수도 있기에 적어본다.

2. 인덱스가 다르다. 즉, 리스트형식으로 생성한다면 인덱스라인에 있는 숫자는 리스트 원래 인덱스넘버가 그대로 생성된다. 반대로 딕셔너리로 생성을 한다면, key,value형태이기에 key가 인덱스 value가 value로 들어가는 것이다.

이 차이를 정확하게 집어가야지만 데이터프레임을 이해하고, 그 데이터프레임을 다룰때 정확하게 처리할 수 있다.

 

시리즈 인덱싱

example1['undead']
33
example2[1]
'kwandoll'

말로 설명하는 것보다 예시가 이해에 더 빠르기에, 이렇게 작성해본다. 

해석) example1이라는 Series자료구조에서 인덱스가 'undead'인 value를 출력하라. 결과) 33

해석) example2라는 Series자료구조에서 인덱스가 1인 value를 출력하라. 결과) kwandoll

 

 

 

'Data analysis > Pandas' 카테고리의 다른 글

pandas.DataFrame.apply  (0) 2021.03.16
[Pandas] DataFrame  (0) 2021.01.15
[Pandas] DataFrame 특정 칼럼 혹은 인덱스 선택  (0) 2021.01.13
[Pandas] DataFrame(개념)  (0) 2021.01.13

과연 실무에서 전공자가 아닌 비전공자가 분석 내용을 확인하는 방법중에 가장 효과적인 방법은 무엇일까?

바로 그래프이다. 그래프를 통해 알기 어려운 insight를 한눈에 알아차릴 수 있다. 하지만, 그 과정을 만들어 잘 보여지게 하는게 analysist 의 일이다.

 

시작 데이터는 항상 연습용으로 사용하는 리뷰데이터를 통해 시작하겠다. 이유는 간단하다. 전처리 하기에도 가장 훌륭한 연습용 데이터이기 때문이다.

먼저 필요한 라이브러리 함수를 불러온다.

 

import numpy as np
import pandas as pd
from konlpy.tag import Okt
from collections import Counter
import re
from konlpy.tag import Twitter
from pykospacing import spacing
import matplotlib.pyplot as plt
twitter=Twitter()
data=pd.read_csv('c:/data/thisweek_selected_classified.csv')
stopwords = ['도', '는', '다', '의', '가', '이', '은', '한', '에', '하', '고', '을', '를', '인', '듯', '과', '와', '네', '들', '듯', '지', '임', '게','요','거','로','으로',
            '것','수','할','하는','제','에서','그','데','번','해도','죠','된','건',
             '바','구','세','랑','시','저','만']

data는 작성일전으로부터 1주일 데이터를 크롤링한 데이터이다. 한번 데이터를 확인해보자.

데이터는 10개만 확인해본다.

data=pd.read_csv('c:/data/thisweek_selected_classified.csv')
data[:10]

본인이 생각하기에 실무자가 아닌 일반 비전공자들은 이 데이터르 보면 '아 1주일데이터가 이렇구나'까지이다. 우리의 임무는 어떤 방식으로 어떻게 상대방에게 결과 즉, insight를 꺼내주면 된다.

content 칼럼의 리뷰 문장들을 한번 간단하게라도 전처리 및 토큰화를 해보자.

twitter=Twitter()
data=pd.read_csv('c:/data/thisweek_selected_classified.csv')
#불용어 단어
stopwords = ['도', '는', '다', '의', '가', '이', '은', '한', '에', '하', '고', '을', '를', '인', '듯', '과', '와', '네', '들', '듯', '지', '임', '게','요','거','로','으로',
            '것','수','할','하는','제','에서','그','데','번','해도','죠','된','건',
             '바','구','세','랑','시','저','만']

#단어의 길이가 2개 이상인 경우만 선택 및 저장
data['content']=data['content'].str.replace('[^ㄱ-ㅎㅏ-ㅣ|가-힣]','') #전처리 
data['tokenized']=data['content'].apply(twitter.morphs) #명사마나ㅏ 토큰화
data['tokenized']=data['tokenized'].apply(lambda x: [item for item in x if item not in stopwords])
#불용어 처리
x=data['tokenized']
for line in x:
    for word in line:
        if len(word)<2:
            line.remove(word)
data['tokenized']=x 

*참고) Twitter 전처리 라이브러리는 Konlpy에서 만든 것인데, KoNLPy v0.4.5 버전 이후로는 Okt 전처리 라이브러리와 통합되었다.

아직까지 Konlpy에서 만든 토큰화 라이브러리는 단어가 조금만 오타가 나도 오류를 범하기에 가장 빠르게 할 수 있는 방법은 단어 길이가 1인 경우는 지워주는 것이다. 본인 기준이다.... 

다시 데이터 10개를 확인해보면 잘 처리가 되었다. 이후 우리는 numpy에서 제공하는 단어 카운트 라이브러리를 사용할 것이다. 

우선 긍정 및 부정으로 나누어 주고, 각 부분 별 단어의 빈도 최고 10개를 확인해 보자.

negative_count=np.hstack(data[data.grade<3]['tokenized'].values)
positive_count=np.hstack(data[data.grade>2]['tokenized'].values)
negative_word_count=Counter(negative_count)
positive_word_count=Counter(positive_count)

print(negative_word_count.most_common(10))
print(positive_word_count.most_common(10))

[('영상', 9), ('너무', 7), ('화면', 7), ('업데이트', 6), ('라이브', 6), ('버전', 6), ('기능', 5), ('따로', 4), ('채널', 4), ('계속', 3)]
[('영상', 12), ('브이', 8), ('너무', 7), ('기록', 5), ('삭제', 5), ('업데이트', 4), ('라이브', 4), ('까지', 4), ('설정', 4), ('좋은데', 3)]

긍정과 부정에 최빈 단어는 '영상' 이다. 수치로도 확인할 수 있지만, 본래 취지는 그래프를 통한 insight이기에 그래프로 확인해보자.

import matplotlib.font_manager as fm
def bar_plot(dict1,dict2):

    fl = fm.FontProperties(fname='c:/Windows/Fonts/malgun.ttf').get_name()
    plt.rc('font', family=fl)
    plt.bar(dict1.keys(), dict1.values(), color='b')
    plt.bar(dict2.keys(), dict2.values(), color='r')
    plt.legend()
    plt.title('Bar Chart')
    plt.show()
    
ne_data=dict(negative_word_count.most_common(10))
po_data=dict(positive_word_count.most_common(10))

bar_plot(po_data,ne_data)

위 그래프를보면 까지, 너무, 이런 단어들은 굳이 있을 필요가 없는 단어들이다. 그러므로 다시 불용어 처리를 하며 그래프를 더 정교화 시키면 된다. 연습용 데이터가 작기에 이 단어 빈도수가 작지만 데이터를 늘린다면 더 괜찮은 insight를 도출해 낼 수 있을 것이다. 

 

모델을 만든 후 학습시키는 시간 또한 데이터가 크면 클수록 오래걸린다. 짧게는 수 분에서 길게는 몇일이 걸릴 정도이다. 이를 통해 결과를 얻는다면 다음에 이걸 적용해 볼때마다 학습시키고 사용해야 하는가? 

본인도 처음 모델을 만들고 학습시킨 후, 저장 및 불러오는 방법을 몰라 30분가량을 기다리고 사용해보고 했다. 비효율 적인 상황에서 구글링을 하였고 해결책을 얻었다.

 

크게 세가지로 구분하자.

1. 모델을 저장하는 코드

model_save

2. 모델을 불러오는 코드

model_load

3. 불러온 모델을 평가하는 코드

model.compile,model.evaluate

 

1. 모델을 저장하는 코드

모델을 저장할 경우 두가지를 저장하면 된다. 

Model 을 저장하는 json 파일, Weight을 저장하는 h5 파일

 

코드는 다음과 같다.

#모델 저장
json_model=model.to_json()
with open("C:/data/model.json","w") as json_file:  
#쌍따옴표 안은 절대경로에 저장하면 파일 이름.josn
#임의 경로를 원한다면 경로 설정
	json_file.write(model_json)

#가중치 저장
model.save_weights("C:/data/weight_model.h5")
#쌍따옴표 안은 절대경로에 저장하면 파일 이름.josn
#임의 경로를 원한다면 경로 설정

2. 모델을 불러오는 코드

모데를 불러오려면 똑같이 model과 weight을 불러와야한다. 그렇다면 json 파일 및 h5를 불러오면 되는것이다.

from keras.models import model_from_json
#모델 불러오기
json_model=open("C:/data/model.json","r")
loadded_json_model=json_model.read()
json_model.close()
loadded_json_model=model_from_json(loadded_json_model)

#가중치 불러오기
model.load_weight("C:/data/weight_model.h5")

 

3. 모델을 평가하기. 

본인은 이것이 가장 중요하다 생각한다. 실제 내가 훈련시킨 데이터를 저장해 놓지 않았다면, 위 작업은 의미가 없다. pkl형식이든 csv형식이든 훈련데이터는 존재해야 한다. 꼭!

 

#평가하기
load_model.compile(optimizer='rmsprop',loss='binary_crossentropy',metrics=['acc'])
print("\n 테스트 정확도: %.4f" % (loaded_model.evaluate(X_test, Y_test)[1]))

 

평가 코드는 간단하다. 하지만 위에서 언급한 것처럼 X_test와 Y_test가 없으면 아무 소용이 없다.

 

예시를 통해 확인해보자 불러오고 컴파일시큰 것까지 정리된 것이다.

from nltk.corpus import stopwords
import pandas as pd
from konlpy.tag import Okt
okt=Okt()
from tensorflow.keras.preprocessing.text import Tokenizer
from tensorflow.keras.preprocessing.sequence import pad_sequences
from keras.models import model_from_json
stopwords = ['도', '는', '다', '의', '가', '이', '은', '한', '에', '하', '고', '을', '를', '인', '듯', '과', '와', '네', '들', '듯', '지', '임', '게','요','거','로','으로',
            '것','수','할','하는','제','에서','그','데','번','해도','죠','된','건','바','구','세']

train_data=pd.read_pickle("c:/data/Train_data.pkl")
test_data=pd.read_pickle("c:/data/Test_data.pkl")
X_Train=train_data['tokenized'].values
Y_Train=train_data['label'].values
X_Test=test_data['tokenized'].values
Y_Test=test_data['label'].values
tokenizer=Tokenizer()
tokenizer.fit_on_texts(X_Train)
vocab_size=19249
tokenizer=Tokenizer(vocab_size,oov_token='OOV')
tokenizer.fit_on_texts(X_Train)
X_Train=tokenizer.texts_to_sequences(X_Train)
X_Test=tokenizer.texts_to_sequences(X_Test)
max_len=100
X_Train=pad_sequences(X_Train,maxlen=max_len)
X_Test=pad_sequences(X_Test,maxlen=max_len)

json_file=open("c:/data/model.json","r")
loaded_model_json=json_file.read()
json_file.close()
loaded_model=model_from_json(loaded_model_json)
loaded_model.load_weights("c:/data/best_model_GRU.h5")
loaded_model.compile(optimizer='rmsprop',loss='binary_crossentropy',metrics=['acc'])

def GRU_predict(new_sentence):
    max_len=90
    new_sentence=okt.morphs(new_sentence)
    new_words=[word for word in new_sentence if not word in stopwords]
    encoded=tokenizer.texts_to_sequences([new_sentence])
    pad_new=pad_sequences(encoded,maxlen=max_len)
    score=float(loaded_model.predict(pad_new))
    percentage=score*100
    return percentage,new_words

위 작업은 GRU 기법을 활용한 감성분석 시 사용한 모델이다. 모델 저장은 크게 어렵지 않지만, 불러오는 것 또한 준비할게 있다는 걸 명심해두길 바란다.

본인이 만든 함수들은 각 ipynb로 분류해 놓았다. 그러므로 이를 따라하는 것을 추천하지는 않는다..

import import_ipynb
import send_email
import Main_1
import call_Graph
import pandas as pd


importing Jupyter notebook from send_email.ipynb
importing Jupyter notebook from Main_1.ipynb
importing Jupyter notebook from call_url.ipynb
importing Jupyter notebook from call_all_company.ipynb
importing Jupyter notebook from call_GRU_predict.ipynb
Epoch 1/15
375/375 [==============================] - ETA: 0s - loss: 0.4081 - acc: 0.8263
Epoch 00001: val_acc improved from -inf to 0.84589, saving model to best_model_GRU.h5
375/375 [==============================] - 55s 145ms/step - loss: 0.4081 - acc: 0.8263 - val_loss: 0.3822 - val_acc: 0.8459
Epoch 2/15
375/375 [==============================] - ETA: 0s - loss: 0.3341 - acc: 0.8630
Epoch 00002: val_acc improved from 0.84589 to 0.85187, saving model to best_model_GRU.h5
375/375 [==============================] - 55s 145ms/step - loss: 0.3341 - acc: 0.8630 - val_loss: 0.3605 - val_acc: 0.8519
Epoch 3/15
375/375 [==============================] - ETA: 0s - loss: 0.3128 - acc: 0.8725
Epoch 00003: val_acc did not improve from 0.85187
375/375 [==============================] - 55s 146ms/step - loss: 0.3128 - acc: 0.8725 - val_loss: 0.3565 - val_acc: 0.8499
Epoch 4/15
375/375 [==============================] - ETA: 0s - loss: 0.2961 - acc: 0.8803
Epoch 00004: val_acc did not improve from 0.85187
375/375 [==============================] - 55s 146ms/step - loss: 0.2961 - acc: 0.8803 - val_loss: 0.3612 - val_acc: 0.8492
Epoch 5/15
375/375 [==============================] - ETA: 0s - loss: 0.2810 - acc: 0.8872
Epoch 00005: val_acc did not improve from 0.85187
375/375 [==============================] - 53s 142ms/step - loss: 0.2810 - acc: 0.8872 - val_loss: 0.3755 - val_acc: 0.8447
Epoch 6/15
375/375 [==============================] - ETA: 0s - loss: 0.2659 - acc: 0.8948
Epoch 00006: val_acc did not improve from 0.85187
375/375 [==============================] - 54s 143ms/step - loss: 0.2659 - acc: 0.8948 - val_loss: 0.3756 - val_acc: 0.8431
Epoch 7/15
375/375 [==============================] - ETA: 0s - loss: 0.2504 - acc: 0.9023
Epoch 00007: val_acc did not improve from 0.85187
375/375 [==============================] - 58s 155ms/step - loss: 0.2504 - acc: 0.9023 - val_loss: 0.3964 - val_acc: 0.8342
Epoch 00007: early stopping
492/492 [==============================] - 10s 20ms/step - loss: 0.3621 - acc: 0.8509

 테스트 정확도: 0.8509
importing Jupyter notebook from call_Graph.ipynb

 

 

def kwandoll():
    check1=int(input("어떤 작업을 수행하시겠습니까?\n"+"[1]:경쟁회사 전체 데이터\n"+"[2]:경쟁회사 및 자사 데이터"))
    check2=int(input("분류된 데이터를 확인하시겠습니까?\n"+"[1]:예"+"[2]:아니오"))
    check3=int(input("이메일로 받으시겠습니까?\n"+"[1]:예"+"[2]:아니오"))
    check4=int(input("그래프로 확인해보시겠습니까?\n"+"[1]:예"+"[2]:아니오"))
    if(check1==1):
        
        if(check2==1 and check4==1):
            
            Main_1.classified_all_data()
            df=pd.read_csv("C:\\Data\\thisweek_classified.csv")
            call_Graph.wordcloud(df)
            if(check3==1):
                send_email.sending_classified_all_data()
            else:
                print("지정된 디렉토리에 저장되었습니다.")
        elif(check2==1 and check4==2):
            if(check3==1):
                send_email.sending_classified_all_data()
            else:
                print("지정된 디렉토리에 저장되었습니다.")
        elif(check2==2 and check4==1):
            Main_1.none_classified_all_data()
            df=pd.read_csv("C:\\Data\\thisweek_unclassified.csv")
            call_Graph.wordcloud(df)
            if(check3==1):
                send_email.sending_unclassified_all_data()
            else:
                print("지정된 디렉터리에 저장되었습니다.")
        elif(check2==2 and check4==2):
            Main_1.none_classified_all_data()
            if(check3==1):
                send_email.sending_unclassified_all_data()
            else:
                print("지정된 디렉토리에 저장되었습니다.")

    elif(check1==2):
        if(check2==1 and check4==1):
            Main_1.classified_selected_data()
            df=pd.read_csv("C:\\Data\\thisweek_selected_classified.csv")
            call_Graph.wordcloud(df)
            if(check3==1):
                send_email.sending_classified_selected_data()
            else:
                print("지정된 디렉토리에 저장되었습니다.")
        elif(check2==1 and check4==2):
            if(check3==1):
                send_email.sending_classified_selected_data()
            else:
                print("지정된 디렉토리에 저장되었습니다.")
        elif(check2==2 and check4==1):
            Main_1.none_classified_selected_data()
            df=pd.read_csv("C:\\Data\\thisweek_selected_unclassified.csv")
            call_Graph.wordcloud(df)
            if(check3==1):
                send_email.sending_unclassified_selected_data()
            else:
                print("지정된 디렉토리에 저장되었습니다.")
        elif(check2==2 and check4==2):
            Main_1.none_classified_selected_data()
            if(check3==1):
                send_email.sending_unclassified_selected_data()
            else:
                print("지정된 디렉토리에 저장되었습니다.")

 

 

이후, kwandoll() 함수를 호출하면

어떤 작업을 수행하시겠습니까?
[1]:경쟁회사 전체 데이터
[2]:경쟁회사 및 자사 데이터1
분류된 데이터를 확인하시겠습니까?
[1]:예[2]:아니오1
이메일로 받으시겠습니까?
[1]:예[2]:아니오1
그래프로 확인해보시겠습니까?
[1]:예[2]:아니오1
시작 년도를 적으시오: 2021
시작 월을 적으시오: 01
시작 일을 적으시오: 01
종료 년도를 적으시오: 2021
종료 월을 적으시오: 01
종료 일을 적으시오: 13
퀄리티 벨류의 최댓값을 지정하세요: 90
퀄리티 벨류의 최솟값을 지정하세요: 10

 

이름을 적으세요이관형
보낼 이메일 주소를 적으세요: asd95101@naver.com
Successfully sent the mail!!!

이로써 내가 원하는 데이터는 지정 이메일로 전송이되고, 워드클라우드를 통해 그래프로 보여진다. 

이제 시작이라 말주변도없고 이 데이터도 전처리를 더 해야하지만, 인턴을 통해 이 정도까지 왔다는 것도 만족한다. 계속 공부하며 글을 올려 실력을 올려보자.

1. 감성분석엔진을 통해 분류된 전체 회사 데이터.

def weekly_all_classified_data(dict):
    start_date,end_date=start_to_date(),end_to_date()
    maximum,minimum=score_input()
    for key,value in dict.items():
        driver = webdriver.Chrome('C:\\Users\\LeeKwanHyeong\\chromedriver_win32\\chromedriver.exe',chrome_options=options)
        driver.implicitly_wait(2)
        driver.get(value)
        # sort 선택창
        elem = driver.find_element_by_xpath("//span[@class='DPvwYc']")
        elem.click()
        time.sleep(3)
        pyautogui.press('up')
        time.sleep(0.7)
        pyautogui.press('up')
        time.sleep(0.7)
        pyautogui.press('enter')
        
        while(True):
            driver.execute_script("window.scrollTo([0],document.body.scrollHeight);")
            time.sleep(0.5)
            try:
                element=driver.find_element_by_xpath('//div[@class="U26fgb O0WRkf oG5Srb C0oVfc n9lfJ"]')
                if(element is not None):
                    element.click()
                    break
            except Exception:
                continue

        html=driver.page_source
        driver.quit()
        bsObj=BeautifulSoup(html,'lxml')
        div_reviews=bsObj.find_all("div",{"class":"d15Mdf bAhLNe"})
        
        
        company_list,grade_list,date_list,content_list=[],[],[],[]
        for div in div_reviews:
            
            

            date_=div.find('span',{"class":"p2TkOb"}).get_text()
            t=re.findall(r"\d*\.\d+|\d+",date_)
            date='{0}-{1}-{2}'.format(t[0],t[1],t[2])
            year, month, day=int(t[0]), int(t[1]), int(t[2])
            dd=datetime(year,month,day)
            if((dd-start_date>=timedelta(days=0)) and (end_date-dd>=timedelta(days=0))):
                content=div.find('span',{'jsname':'bN97Pc'}).get_text()
                content=content.replace("전체 리뷰",'')
                content=re.sub('[^가-힣0-9a-zA-Z_!?@#%^&-=:;,\"\'<>\\s]','',content)
                content.encode('utf-8')
                grade=len(div.find_all('div',{'class':'vQHuPe bUWb7c'}))
                percentage,word=call_GRU_predict.GRU_predict(content)
                if(((percentage<maximum)and (percentage>minimum))) and (len(word)>6):
                    date_list.append(dd)
                    content_list.append(content)
                    grade_list.append(grade)
                    company_list.append(key)
                else:
                    continue
        grade_Series=pd.Series(grade_list)
        date_Series=pd.Series(date_list)
        content_Series=pd.Series(content_list)
                #
        company_Series=pd.Series(company_list)
        data_frame=pd.DataFrame()
        data_frame['company']=company_Series
        data_frame['date']=date_Series
        data_frame['grade']=grade_Series
        data_frame['content']=content_Series
        
        good_data=data_frame[data_frame['grade']>2]
        bad_data=data_frame[data_frame['grade']<3]
        
        writer=pd.ExcelWriter("C:/data/thisweek_classified.xlsx")
        if not os.path.exists(writer):
            good_data.to_excel(writer, sheet_name='good',header=True)
            bad_data.to_excel(writer,sheet_name='bad',header=True)
        else:
            bad_data.to_excel(writer, sheet_name='bad',header=False)
            good_data.to_excel(writer,sheet_name='good',header=False)
        writer.save()
        if not os.path.exists('C:\\Data\\thisweek_classified.csv'):
            data_frame.to_csv('C:\\Data\\thisweek_classified.csv',index=False, mode='w',encoding='utf_8_sig')
        else:
            data_frame.to_csv('C:\\Data\\thisweek_classified.csv',index=False,mode='a',encoding='utf_8_sig',header=False)
    return data_frame
        
    #location='C:\\Data\\thisweek_classified.csv'
   
            
        
def classified_all_data():
    url_dict=call_url.call_url() #url리턴 함수
      #시작일 종료일 지정
    load=call_all_company.weekly_all_classified_data(url_dict)
    return load

 

 

2. 감성분석 엔진을 통해 분류되지 않은 전체 데이터

 

def weekly_all_unclassified_data(dict):
    start_date,end_date=start_to_date(),end_to_date()
    for key,value in dict.items():
        driver = webdriver.Chrome('C:\\Users\\LeeKwanHyeong\\chromedriver_win32\\chromedriver.exe',chrome_options=options)
        driver.implicitly_wait(2)
        driver.get(value)
        # sort 선택창
        elem = driver.find_element_by_xpath("//span[@class='DPvwYc']")
        elem.click()
        time.sleep(3)
        pyautogui.press('up')
        time.sleep(0.7)
        pyautogui.press('up')
        time.sleep(0.7)
        pyautogui.press('enter')
        
        while(True):
            driver.execute_script("window.scrollTo([0],document.body.scrollHeight);")
            time.sleep(0.5)
            try:
                element=driver.find_element_by_xpath('//div[@class="U26fgb O0WRkf oG5Srb C0oVfc n9lfJ"]')
                if(element is not None):
                    element.click()
                    break
            except Exception:
                continue

        html=driver.page_source
        driver.quit()
        bsObj=BeautifulSoup(html,'lxml')
        div_reviews=bsObj.find_all("div",{"class":"d15Mdf bAhLNe"})
        
        
        company_list,grade_list,date_list,content_list=[],[],[],[]
        for div in div_reviews:
            
            

            date_=div.find('span',{"class":"p2TkOb"}).get_text()
            t=re.findall(r"\d*\.\d+|\d+",date_)
            date='{0}-{1}-{2}'.format(t[0],t[1],t[2])
            year, month, day=int(t[0]), int(t[1]), int(t[2])
            dd=datetime(year,month,day)
            if((dd-start_date>=timedelta(days=0)) and (end_date-dd>=timedelta(days=0))):
                content=div.find('span',{'jsname':'bN97Pc'}).get_text()
                content=content.replace("전체 리뷰",'')
                content=re.sub('[^가-힣0-9a-zA-Z_!?@#%^&-=:;,\"\'<>\\s]','',content)
                content.encode('utf-8')
                grade=len(div.find_all('div',{'class':'vQHuPe bUWb7c'}))
                percentage,word=call_GRU_predict.GRU_predict(content)
               
                date_list.append(dd)
                content_list.append(content)
                grade_list.append(grade)
                company_list.append(key)
                
        grade_Series=pd.Series(grade_list)
        date_Series=pd.Series(date_list)
        content_Series=pd.Series(content_list)
                #
        company_Series=pd.Series(company_list)
        data_frame=pd.DataFrame()
        data_frame['company']=company_Series
        data_frame['date']=date_Series
        data_frame['grade']=grade_Series
        data_frame['content']=content_Series
    good_data=data_frame[data_frame['grade']>2]
    bad_data=data_frame[data_frame['grade']<3]
        
    writer=pd.ExcelWriter("C:/data/thisweek_unclassified.xlsx")
    if not os.path.exists(writer):
        good_data.to_excel(writer,sheet_name='good',header=True)
        bad_data.to_excel(writer,sheet_name='bad',header=True)
    else:
        bad_data.to_excel(writer,sheet_name='bad',header=False)
        good_data.to_excel(writer,sheet_name='good',header=False)
    writer.save()
    if not os.path.exists('C:\\Data\\thisweek_unclassified.csv'):
            data_frame.to_csv('C:\\Data\\thisweek_unclassified.csv',index=False, mode='w',encoding='utf_8_sig')
    else:
        data_frame.to_csv('C:\\Data\\thisweek_unclassified.csv',index=False,mode='a',encoding='utf_8_sig',header=False)
    #all_data=pd.read_csv("C:\\Data\\thisweek_unclassified.csv")
    return data_frame
def none_classified_all_data():
    url_dict=call_url.call_url()
    load=call_all_company.weekly_all_unclassified_data(url_dict)
    return load

 

3. 감성분류를 통해 분류된 선택된 회사 데이터

def weekly_selected_classified_data(url,company):
    
    start_date,end_date=start_to_date(),end_to_date()
    maximum,minimum=score_input()
    
    driver = webdriver.Chrome('C:\\Users\\LeeKwanHyeong\\chromedriver_win32\\chromedriver.exe',chrome_options=options)
    driver.implicitly_wait(2)
    driver.get(url)
     # sort 선택창
    elem = driver.find_element_by_xpath("//span[@class='DPvwYc']")
    elem.click()
    time.sleep(3)
    pyautogui.press('up')
    time.sleep(0.7)
    pyautogui.press('up')
    time.sleep(0.7)
    pyautogui.press('enter')
    
        
    while(True):
        driver.execute_script("window.scrollTo([0],document.body.scrollHeight);")
        time.sleep(0.5)
        try:
            element=driver.find_element_by_xpath('//div[@class="U26fgb O0WRkf oG5Srb C0oVfc n9lfJ"]')
            if(element is not None):
                element.click()
                break
        except Exception:
            continue
    html=driver.page_source
    driver.quit()
    bsObj=BeautifulSoup(html,'lxml')
    div_reviews=bsObj.find_all("div",{"class":"d15Mdf bAhLNe"})
    
    
    #
    company_list,grade_list,date_list,content_list=[],[],[],[]
    
    for div in div_reviews:
        date_=div.find('span',{"class":"p2TkOb"}).get_text()
        t=re.findall(r"\d*\.\d+|\d+",date_)
        date='{0}-{1}-{2}'.format(t[0],t[1],t[2])
        year, month, day=int(t[0]), int(t[1]), int(t[2])
        dd=datetime(year,month,day)
        if((dd-start_date>=timedelta(days=0)) and (end_date-dd>=timedelta(days=0))):
                content=div.find('span',{'jsname':'bN97Pc'}).get_text()
                content=content.replace("전체 리뷰",'')
                content=re.sub('[^가-힣0-9a-zA-Z_!?@#%^&-=:;,\"\'<>\\s]','',content)
                content.encode('utf-8')
                grade=len(div.find_all('div',{'class':'vQHuPe bUWb7c'}))
                percentage,word=call_GRU_predict.GRU_predict(content)
                if(((percentage<maximum)and (percentage>minimum))) and (len(word)>6):
                    date_list.append(dd)
                    content_list.append(content)
                    grade_list.append(grade)
                    company_list.append(company)
                else:
                    continue
    grade_Series=pd.Series(grade_list)
    date_Series=pd.Series(date_list)
    content_Series=pd.Series(content_list)
    #
    company_Series=pd.Series(company_list)
    data_frame=pd.DataFrame()
    data_frame['company']=company_Series
    data_frame['date']=date_Series
    data_frame['grade']=grade_Series
    data_frame['content']=content_Series
    #
    good_data=data_frame[data_frame['grade']>2]
    bad_data=data_frame[data_frame['grade']<3]
    if not os.path.exists('C:\\Data\\thisweek_classified.csv'):
        data_frame.to_csv('C:\\Data\\thisweek_selected_classified.csv',index=False, mode='w',encoding='utf_8_sig')
    else:
        data_frame.to_csv('C:\\Data\\thisweek_selected_classified.csv',index=False,mode='a',encoding='utf_8_sig',header=False)
    
    writer=pd.ExcelWriter('C:/data/thisweek_selected_classified.xlsx')
    if not os.path.exists(writer):
        good_data.to_excel(writer,sheet_name='good',header=True)
        bad_data.to_excel(writer,sheet_name='bad',header=True)
    else:
        good_data.to_excel(writer,sheet_name='good',header=False)
        bad_data.to_excel(writer,sheet_name='bad',header=False)
    writer.save()
    return data_frame
    
    
def classified_selected_data():
    url,company=call_url.select_url()
    load=call_all_company.weekly_selected_classified_data(url,company)
    return load

 

 

4. 감성분류를 통해 분류되지 않은 선택된 회사 데이터

def weekly_selected_unclassified_data(url,company):
    start_date,end_date=start_to_date(),end_to_date()
    driver = webdriver.Chrome('C:\\Users\\LeeKwanHyeong\\chromedriver_win32\\chromedriver.exe',chrome_options=options)
    driver.implicitly_wait(2)
    driver.get(url)
     # sort 선택창
    elem = driver.find_element_by_xpath("//span[@class='DPvwYc']")
    elem.click()
    time.sleep(3)
    pyautogui.press('up')
    time.sleep(0.7)
    pyautogui.press('up')
    time.sleep(0.7)
    pyautogui.press('enter')
    
        
    while(True):
        driver.execute_script("window.scrollTo([0],document.body.scrollHeight);")
        time.sleep(0.5)
        try:
            element=driver.find_element_by_xpath('//div[@class="U26fgb O0WRkf oG5Srb C0oVfc n9lfJ"]')
            if(element is not None):
                element.click()
                break
        except Exception:
            continue
    html=driver.page_source
    driver.quit()
    bsObj=BeautifulSoup(html,'lxml')
    div_reviews=bsObj.find_all("div",{"class":"d15Mdf bAhLNe"})
    
    
    
    #
    company_list,grade_list,date_list,content_list=[],[],[],[]
    
    for div in div_reviews:
        date_=div.find('span',{"class":"p2TkOb"}).get_text()
        t=re.findall(r"\d*\.\d+|\d+",date_)
        date='{0}-{1}-{2}'.format(t[0],t[1],t[2])
        year, month, day=int(t[0]), int(t[1]), int(t[2])
        dd=datetime(year,month,day)
        if((dd-start_date>=timedelta(days=0)) and (end_date-dd>=timedelta(days=0))):
                content=div.find('span',{'jsname':'bN97Pc'}).get_text()
                content=content.replace("전체 리뷰",'')
                content=re.sub('[^가-힣0-9a-zA-Z_!?@#%^&-=:;,\"\'<>\\s]','',content)
                content.encode('utf-8')
                grade=len(div.find_all('div',{'class':'vQHuPe bUWb7c'}))
                percentage,word=call_GRU_predict.GRU_predict(content)
               
                date_list.append(dd)
                content_list.append(content)
                grade_list.append(grade)
                company_list.append(company)
    grade_Series=pd.Series(grade_list)
    date_Series=pd.Series(date_list)
    content_Series=pd.Series(content_list)
    #
    company_Series=pd.Series(company_list)
    data_frame=pd.DataFrame()
    data_frame['company']=company_Series
    data_frame['date']=date_Series
    data_frame['grade']=grade_Series
    data_frame['content']=content_Series
    #
    good_data=data_frame[data_frame['grade']>2]
    bad_data=data_frame[data_frame['grade']>3]
    
    writer=pd.ExcelWriter("C:/data/thisweek_selected_unclassified.xlsx")
    if not os.path.exists(writer):
        good_data.to_excel(writer,sheet_name='good',header=True)
        bad_data.to_excel(writer,sheet_name='bad',header=True)
    else:
        good_data.to_excel(writer,sheet_name='good',header=False)
        bad_data.to_excel(writer,sheet_name='bad',header=False)
    writer.save()
    if not os.path.exists('C:\\Data\\thisweek_selected_unclassified.csv'):
        data_frame.to_csv('C:\\Data\\thisweek_selected_unclassified.csv',index=False, mode='w',encoding='utf_8_sig')
    else:
        data_frame.to_csv('C:\\Data\\thisweek_selected_unclassified.csv',index=False,mode='a',encoding='utf_8_sig',header=False)
    
    
    return data_frame
    
    
    
    
def none_classified_selected_data():
    url,company=call_url.select_url()
    load=call_all_campany.weekly_selected_classified_data(url,company)
    return load

 

 

 

사실 준비하는데 가장 많이 공을 들인 부분이다. 훈련된 단어들을 통해 어떻게 관련지어 워드클라우드로 표현할까이다.

본인이 행한 기법은 only 긍정 단어, only 부정 단어, 긍정&부정 단어 들로 구분시켜 전체 단어의 갯수, 각 부분별 전체 빈도수, 각 단어의 빈도수를 구하여 조합을 시킨 스코어값을 대입했다.

그렇게 되면 새로운 데이터와 비교하였을때 훈련시킨 데이터의 가중치를 통해 중요도를 구분할 수 있기 때문이다. 위 계산과정은 생략하겠다. 

 

def call_dict():
    read_ne_df=pd.read_pickle("C:\\data\\real_ne.pkl")
    read_po_df=pd.read_pickle("C:\\data\\real_po.pkl")
    read_with_df=pd.read_pickle("C:\\data\\real_with.pkl")
    po_word=list(np.array(read_po_df['word']))
    po_weight=list(np.array(read_po_df['weight']))
    ne_word=list(np.array(read_ne_df['word']))
    ne_weight=list(np.array(read_ne_df['weight']))
    with_word=list(np.array(read_with_df['word']))
    with_weight=list(np.array(read_with_df['weight']))
    all_word=po_word+ne_word+with_word
    all_weight=po_weight+ne_weight+with_weight
    all_list=[all_word,all_weight]
    all_dic=dict(zip(*all_list))
    return all_dic

#들어오는 리뷰들을 지정된 점수로 분류하여 라인별로 묶기.
#들어오는 리뷰들을 지정된 점수로 분류하여 라인별로 묶기.
def scoring(sentence):
    score,word,weight=[],[],[]
    a,b=[],[]
    dictionary=call_dict()
    summation=0
    stopwords =['도', '는', '다', '의', '가', '이', '은',  '한', '에', '하', '고', '을', '를', '인', '듯', '과', '와', '네', '들', '듯', '지', '임', '게','요','거','로','으로',
            '것','수','할','하는','제','에서','그','데','번','해도','죠','된','건','바','구','세','최신','.']
    word_tokens=okt.morphs(sentence)
    word_tokens=[x for x in word_tokens if x not in stopwords]
    
    for x in word_tokens:
        if dictionary.get(x):
            
            
            if(len(x)==1):
                continue
            elif(len(x)>1):
                word.append(x)
                score.append(dictionary[x])
        else:
            continue
    for sc in score:
        summation+=sc
    for sc in score:
        weight.append(sc/summation)
    all_list=[word,weight]
    dict_n=dict(zip(*all_list))
    s_dic=sorted(dict_n.items(),key=lambda x:x[1],reverse=True)
    best_dic=s_dic[:10]
    temp_word,temp_weight=[],[]
    for split in range(len(best_dic)):
        list_=list(best_dic[split])
        temp_word.append(list_[0])
        temp_weight.append(list_[1])
    final_list=[temp_word,temp_weight]
    final=dict(zip(*final_list))
        
    return final
## scoring function을 이용해 csv로 받아온 리뷰를 정렬 
##location 매개변수는 메인함수에서 돌릴때 찾아서 변경
def merge_all(data):
    #데이터프레임으로 된것 저장
    merged={}
    temp_list=[]
    for line in data['content']:
        x=scoring(line)
        temp_list.append(x)
    for mer in range(len(temp_list)):
        merged={**temp_list[mer],**merged}
    return merged

def wordcloud(df):
    tokens=merge_all(df)
    wordcloud=WordCloud(font_path='C:/Windows/Fonts/malgun.ttf',background_color='white',colormap='Accent_r',
                       width=1500,height=1000).generate_from_frequencies(tokens)
    plt.imshow(wordcloud)
    plt.axis('off')

    plt.show()

 

+ Recent posts