자연어 처리에서 크롤링 등으로 얻어낸 corpus data가 필요에 맞게 전처리되지 않은 상태라면, 용도에 맞게 토큰화(tokenization) & 정제(cleaning) & 정규화(normalization)하는 일을 하게 된다. 

이중 토큰화에 대해 알아보자.

 

토큰화(tokenization): 주어진 코퍼스(corpus)에서 토큰(token)이라 불리는 단위로 나뉘는 작업(보통 의미있는 단위로 토큰을 정의한다)

 

1. 단어 토큰화(Word Tokenization)

토큰의 기준을 단어(word)로 하는 경우, 단어 토큰화(word tokenization)라고 한다. 

*단어(word)는 단어 단위 외에도 단어구, 의미를 갖는 문자열로도 간주되기도 한다.

 

input) Time is an illusion. Lunchtime double so!

 

위 입력으로부터 토큰화 작업을 시키면 다음과 같다.

 

output) 'Time', 'is', 'an', 'illusion', 'Lunchtime', 'double', 'so'

 

위 출력을 나타내기 위해, 구두점을 지운 뒤에 띄어쓰기(whitespace)를 기준으로 잘라냈다.(가장 기초)

보통 토큰화 작업은 단순히 구두점이나 특수문자를 전부 제거하는 정제(cleaning) 작업을 수행하는 것만으로 해결되지 않는다. 구두점이나 특수문자를 전부 제거하면 토큰의 의미를 잃어버리는 경우가 발생하기도 한다. 심지어 띄어쓰기 단위로 자르면 사실상 단어 토큰이 구분되는 영어와 달리, 한국어는 띄어쓰기만으로는 단어 토큰을  구분하기 어렵다.

 

2. 토큰화 중 생기는 선택의 순간

토큰화를 하다보면, 예상하지 못한 경우가 있어 토큰화의 기준을 생각해봐야 하는 경우가 발생한다. 물론, 이러한 선택은 해당 데이터를 가지고 어떤 용도로 사용할 것인가에 따라 그 용도에 영향이 없는 기준으로 정하면 된다.

 

ex) Don't be fooled by the dark sounding name, Mr. Jone's Orphanage is as cheery as cheery goes for a pastry shop.

 

" ' " 가 들어간 상황에서 Dont't 와 Jone's는 어떻게 토큰화할 수 있을까?

 

[Dont't, Dont t, Dont, Do n't, Jone's, Jone s, Jone, Jones]

 

원하는 결과가 나오도록 토큰화 도구를 직접 설계할 수 있겠지만, 기존에 공개된 도구들을 사용하였을 때의 결과가 사용자의 목적과 일치한다면 해당 도구를 사용할 수 있을 것이다. NLTK는 영어 코퍼스를 토큰화하기 위한 도구들을 제공한다.

 

word_tokenize를 사용한 예를 살펴보자.

 

from nltk.tokenize import word_tokenize

print(word_tokenize("Don't be fooled by the dark sounding name, Mr. Jone's Orpanage is as cheery as cheery goes for a pastry shop."))

['Do', "n't", 'be', 'fooled', 'by', 'the', 'dark', 'sounding', 'name', ',', 'Mr.', 'Jone', "'s", 'Orpanage', 'is', 'as', 'cheery', 'as', 'cheery', 'goes', 'for', 'a', 'pastry', 'shop', '.']

word_tokenize는 Dont't 를 Do 와 n't로 불리하였으며, 반면 Jone's는 Jone과 's로 분리한 것을 확인할 수 있다.

 

wordPunctTokenizer를 사용한 예를 살펴보자

 

from nltk.tokenize import WordPunctTokenizer
print(WordPunctTokenizer().tokenize("Don't be fooled by the dark sounding name, Mr. Jone's Orpanage is as cheery as cheery goes for a pastry shop."))

['Don', "'", 't', 'be', 'fooled', 'by', 'the', 'dark', 'sounding', 'name', ',', 'Mr', '.', 'Jone', "'", 's', 'Orpanage', 'is', 'as', 'cheery', 'as', 'cheery', 'goes', 'for', 'a', 'pastry', 'shop', '.']

WordPunctTokenizer는 구두점을 별도로 분류하는 특징을 갖고 있기 때문에, word_tokenize와 달리 Don't를 Don과 '와 t로 분리하였으며, 이와 마찬가지로 Jone's를 Jone과 '와 s로 분리한 것을 확인할 수 있다.

 

케라스 또한 토큰화 도구로서 text_to_word_sequence를 지원한다. 

text_to_word_sequence를 사용한 예를 살펴보자

 

from tensorflow.keras.preprocessing.text import text_to_word_sequence
print(text_to_word_sequence("Don't be fooled by the dark sounding name, Mr. Jone's Orphanage is as cheery as cheery goes for a pastry shop."))

["don't", 'be', 'fooled', 'by', 'the', 'dark', 'sounding', 'name', 'mr', "jone's", 'orphanage', 'is', 'as', 'cheery', 'as', 'cheery', 'goes', 'for', 'a', 'pastry', 'shop']

text_to_word_sequence는 기본적으로 모든 알파벳을 소문자로 바꾸면서 마침표나 컴마, 느낌표 등의 구두점을 제거한다. 하지만 어퍼스트로피는 보존하는 것을 볼 수 있다.

 

 

토큰화에서 고려해야할 사항

토큰화작업은 단순하게 코퍼스에서 구두점을 제외하고 공백 기준으로 잘라내는 작업이라 생각하면 안된다. 

이유는 다음과 같다.

1) 구두점이나 특수 문자를 단순 제외해서는 안된다.

코퍼스에 대한 정제 작업을 진행하다보면, 구두점 조차도 하나의 토큰으로 분류하기도 한다. 

이러한 점에 있어 해결법은 단어의 빈도수 확인 및 직접 확인을 통해 사전을 만들어주는 경우, 혹은 오픈소스로 되어있는 단어집을 사용하는 방법이 있다.

2) 줄임말과 단어 내에 띄어쓰기가 있는 경우.

토큰화 작업에서 종종 영어권 언어의 "'"는 압축된 단어를 다시 펼치는 역할을 하기도 한다.

사용 용도에 따라, 하나의 단어 사이에 띄어쓰기가 있는 경우도 하나의 토큰으로 봐야하는 경우가 있을 수 있다. 

 

3) 표준 토큰화 예제

표준으로 쓰이는 토큰화 방법 중 하나인 Peen Treebank Tokenization의 규칙을 알아보자.

Rule 1) 하이푼으로 구성된 단어는 하나로 유지.

Rule 2) dont't 와 같이 아포스토로피가 함께하는 단어는 분리해준다.

 

예를 살펴보자.

input) "Starting a home-based restaurant may be an ideal. it doesn't have a food chain or restaurant of their own."

 

from nltk.tokenize import TreebankWordTokenizer
tokenizer=TreebankWordTokenizer()
text="Starting a home-based restaurant may be an ideal. it doesn't have a food chain or restaurant of their own"
print(tokenizer.tokenize(text))

['Starting', 'a', 'home-based', 'restaurant', 'may', 'be', 'an', 'ideal.', 'it', 'does', "n't", 'have', 'a', 'food', 'chain', 'or', 'restaurant', 'of', 'their', 'own']

결과는, 각 Rule1)과 Rule2)에 따라 home-based는 하나의 토큰으로 취급하고 있으며, doesn't의 경우 does와 n't로 분리되었음을 볼 수 있다.

 

4. 문장 토큰화(Sentence Tokenization)

대부분 갖고있는 코퍼스가 정제되지 않은 상태라면, 코퍼스는 문장 단위로 구분되어있지 않을 가능성이 높다.

이를 사용하고자 하는 용도에 맞게 하기 위해서는 문장 토큰화가 필요할 수 있다.

 

어떤 기준으로 문장을 나누어야할까? 코퍼스가 어떤 국적의 언어인지, 또는 해당 코퍼스 내에서 특수문자들이 어떻게 사용되고 있는지에 따라서 직접 규칙을 정의해야 한다. 이는 또한 100%의 정확성을 얻는 것은 아니다. 

NLTK에서는 영어 문장의 토큰화를 수행하는 sent_tokenize를 지원한다.

 

코드로 살펴보자.

 

from nltk.tokenize import sent_tokenize
text="His barber kept his word. But keeping such a huge secret to himself was driving him crazy. Finally, the barber went up a mountain and almost to the edge of a cliff. He dug a hole in the midst of some reeds. He looked about, to make sure no one was near."
print(sent_tokenize(text))

['His barber kept his word.', 'But keeping such a huge secret to himself was driving him crazy.', 'Finally, the barber went up a mountain and almost to the edge of a cliff.', 'He dug a hole in the midst of some reeds.', 'He looked about, to make sure no one was near.']

위 코드는 text에 저장된 여러 개의 문장들로부터 문장을 구분하는 코드이다. 

이번에는 마침표가 여러번 등장하는 경우를 살펴보자.

 

from nltk.tokenize import sent_tokenize
text="I am actively looking for Ph.D. students. and you are a Ph.D student."
print(sent_tokenize(text))

['I am actively looking for Ph.D. students.', 'and you are a Ph.D student.']

 

NLTK는 단순히 마침표를 구분자로 하여 문장을 구분하지 않았기 때문에, ph.D.를 문장 내의 단어로 인식하여 성공적으로 인식하는 것을 볼 수 있다.

 

한국어 문장 토큰화 도구 KSS(Korean Sentence Splitter)

import kss

text="대부분 갖고있는 코퍼스가 정제되지 않은 상태라면, 코퍼스는 문장 단위로 구분되어있지 않을 가능성이 높다.이를 사용하고자 하는 용도에 맞게 하기 위해서는 문장 토큰화가 필요할 수 있다."
print(kss.split_sentences(text))

['대부분 갖고있는 코퍼스가 정제되지 않은 상태라면, 코퍼스는 문장 단위로 구분되어있지 않을 가능성이 높다.', '이를 사용하고자 하는 용도에 맞게 하기 위해서는 문장 토큰화가 필요할 수 있다.']

 

5. 토큰화가 어려운 한국어

한국어는 띄어쓰기가 되어있는 단어를 '어절'이라 한다. 어절 토큰화는 NLP에서 지양되고 있다. 어절 토큰화와 단어 토큰화가 같지 않기 때문인데, 그 근본적인 이유는 한국어가 영어와는 다르게 다른 형태를 가지는 언어인 교착어라는 점에서 기인한다.

 

교착어: 언어의 형태적 유형의 하나. 실질적인 의미를 가진 단어 또는 어간에 문법적인 기능을 가진 요소가 차례로 결합함으로써 문장 속에서의 문법적인 역할이나 관계의 차이를 나타내는 언어로, 한국어ㆍ터키어ㆍ일본어ㆍ핀란드어 따위가 여기에 속한다.

 

자연어 처리를 하다보면 같은 단어여도 서로 다른 조사가 붙어 다른 단어로 인식이 되는 경우가 많다. 대부분 한국어 NLP에서 조사는 분리해줄 필요가 있다.

 

한국어 토큰화에서는 형태소(morpheme)란 개념을 반드시 알아야 한다.

 

형태소: 단어를 분석한 단위로, 뜻을 가진 가장 작은 말의 단위

 

또한 한국어는 띄어쓰기가 영어보다 잘 지켜지지 않는다.

대부분의 한국어 경우 띄어쓰기가 틀렸거나, 잘 지켜지지 않은 코퍼스가 많다. 한국어는 영어권 언어와 달리 띄어쓰기가 어렵고 잘 지켜지지 않는 경향이 있다.

하지만, 실제로 잘 이루어지지 않더라도 이해가 가능한것이 또 한국어이다.

 

[한국어]

지금이렇게띄어쓰기를안한다해도이해못하는사람없겠죠?

 

[영어]

Itisdifficulttounderstandenglishbecauseitdidn'tsplitsentence

 

이 차이는 한국어(모아쓰기 방식)와 영어(풀어쓰기 방식)라는 언어적 특성의 차이 때문이다.

 

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

트랜스포머(Transformer)(2)  (0) 2021.03.04
트랜스포머(Transformer)(1)  (0) 2021.03.04
Transfer Learning(전이 학습)  (0) 2021.03.02

합성곱/풀링 계층 구현하기

4차원 배열

앞선 포스팅에서 설명한대로 CNN에서 계층 사이를 흐르는 데이터는 4차원이다. 파이썬으로 구현해보자.

import numpy as np

x=np.random.rand(10,1,28,28)
x.shape

(10, 1, 28, 28)

*첫번째 데이터에 접근하려면 단순히 x[0]이라고 쓴다(파이썬의 인덱스는 0부터 시작한다).

 

x[0].shape #(1,28,28)
x[1].shape #(1,28,28)

 

*또, 첫 번째 데이터의 첫 채널의 공간 데이터에 접근하려면 다음과 같이 구현한다.

x[0,0].shape #또는 x[0][0]

 

im2col로 데이터 전개하기

합성곱 연산을 곧이곧대로 구현하려면 for 문을 겹겹이 써야한다. 하지만 넘파이에 for문을 사용하면 성능이 떨어지는 단점이 있다.(넘파이에서는 원소에 접근할 때 for문을 사용하지 않는 것이 바람직하다).

그렇기에 for 문 대신 im2col이라는 편의 함수를 사용해 간단하게 구현해보자.

(대략적인) im2col의 동작

im2col은 입력 데이터를 필터링(가중치 계산)하기 좋게 전개하는 함수이다. 위 그림과 같이 3차원 입력 데이터에 im2col 을 적용하면 2차원 행렬로 바뀐다. 

정확히는 배치 안의 데이터 수까지 포함한 4차원 데이터를 2차원으로 변환한다.

필터 적용 영역을 앞에서부터 순서대로 1줄로 펼친다.

im2col은 필터링하기 좋게 입력 데이터를 전개한다. 구체적으로는 위 그림과 같이 입력 데이터에서 필터를 적용하는 영역(3차원 블록)을 한줄로 늘어놓는다. 이 전개를 필터를 적용하는 모든 영역에서 수행하는게 im2col이다.

위 그림에서는 보기에 좋게끔 스트라이드를 크게 잡아 필터의 적용 영역이 겹치지 않도록 했지만, 실제 상황에서는 영역이 겹치는 경우가 대부분이다. 

필터 적용 영역이 겹치면 im2col로 전개한 후의 원소 수가 블록의 원소 수보다 많아진다.(이에 메모리를 더 소비하는 단점도 있다)

 

[Note]

im2col은 'image to column' 즉 '이미지에서 행렬로'라는 뜻이다. 카페와 체이너등의 딥러닝 프레임워크는 im2col이라는 이름의 함수를 만들어 합성곱 계층을 구현할 때 이용하고 있다.

 

합성곱 연산의 필터 처리 상세 과정: 필터를 세로로 1열로 전개하고, im2col이 전개한 데이터와 행렬 내적을 계산한다. 마지막으로 출력데이터를 reshape 한다.

  im2col 함수

def im2col(input_data, filter_h, filter_w, stride=1, pad=0):
    N, C, H, W = input_data.shape
    out_h = (H + 2 * pad - filter_h) // stride + 1
    out_w = (W + 2 * pad - filter_w) // stride + 1

    img = np.pad(input_data, [(0, 0), (0, 0), (pad, pad), (pad, pad)], 'constant')
    col = np.zeros((N, C, filter_h, filter_w, out_h, out_w))

    for y in range(filter_h):
        y_max = y + stride * out_h
        for x in range(filter_w):
            x_max = x + stride * out_w
            col[:, :, y, x, :, :] = img[:, :, y:y_max:stride, x:x_max:stride]

    col = col.transpose(0, 4, 5, 1, 2, 3).reshape(N * out_h * out_w, -1)
    return col
x1=np.random.rand(1,3,7,7) #데이터의 수,채널 수, 높이, 너비
col1=im2col(x1,5,5,stride=1,pad=0)
print(col1.shape) 

(9, 75)

위 예는 batchsize=1(data=1), channel=3, height*width=7*7 

x2=np.random.rand(10,3,7,7)
col2=im2col(x2,5,5,stride=1,pad=0)
print(col2.shape)

(90, 75)

위 예는 batchsize=10(data=10),channel=3,height*width=7*7

 

위 두 예 모두 2번째 차원의 원소는 75개이다. 이 값은 필터의 원소 수와 같다(channel=3, 5*5 data).

 

또한, batchsize=1일 때는 im2col의 결과의 크기가 (9,75)이고, 10일 때는 그 10배인 (90,75) 크기의 데이터가 저장된다.

 

다음은 Convolution class를 구현해보자.

class Convolution:
    def __init__(self,W,b,stride=1,pad=0):
        self.W=W
        self.b=b
        self.stride=stride
        self.pad=pad
    def forward(self,x):
        FN,C,FH,FW=self.W.shape
        N,C,H,W=x.shape
        out_h=int(1+(H+2*self.pad-FH)/self.stride)
        out.w=int(1+(W+2*self.pad-FW)/self.stride)
        
        col=im2col(x,FH,FW,self.stride,self.pad)
        col_W=self.W.reshape(FN,-1).T #필터 전개
        out=np.dot(col,col_W)+self.b
        
        out=out.reshape(N,out_h,out_w,-1).transpose(0,3,1,2)
        
        return out

 

 

합성곱 계층은 필터(가중치), 편향, 스트라이드, 패딩을 인수로 받아 초기화한다.

필터는 (FN,C,FH,FW)의 4차원 형상이다. 여기서 FN은 필터 개수, C는 채널, FH는 필터 높이, FW는 필터 너비이다.

 

col=im2col(x,FH,FW,self.stride,self.pad)
col_W=self.W.reshape(FN,-1).T #필터 전개
out=np.dot(col,col_W)+self.b

위 코드 3줄이 가장 중요하다. 위 코드는 input_data를 im2col로 전개하고 필터도 reshape하여 2차원 배열로 전개한다. 그 후 내적을 진행한다.

 

forward 구현의 마지막에는 넘파이의 transpose 함수를 사용하는데, 이는 다차원 배열의 축 순서를 바꿔주는 함수이다.

np.transpose() 원리

풀링 계층 구현하기

풀링 계층 구현도 함성곱 계층과 마찬가지로 im2col을 사용해 입력 데이터를 전개한다. 단, 풀링의 경우엔 채널 쪽이 독립적이라는 점이 합성곱 계층 때와 다르다. 즉, 풀링 적용 영역을 채널마다 독립적으로 전개한다.

입력 데이터에 풀링 적용 영역을 전개(2*2 풀링의 예)

우선 이렇게 전개한 후, 전개한 행렬에서 행별 최댓값을 구하고 적절한 형상으로 성형하면된다.

풀링 계층 구현의 흐름: 풀링 적용 영역에서 가장 큰 원소는 회색을 표시

 

풀링계층을 코딩으로 구현해보자.

class Pooling:
    def __init__(self,pool_h,pool_w,stride=1,pad=0):
        self.pool_h=pool_h
        self.pool_w=pool_w
        self.stride=stride
        self.pad=pad
        
    def forward(self,x):
        N,C,H,W=x.shape
        out_h=int(1+(H-self.pool_h)/self.stride)
        out_w=int(1+(W-self.pool_w)/self.stride)
        
        #전개(1)
        col=im2col(x,self.pool_h,self.pool_w,self.stride,self.pad)
        col=col.reshape(-1,self.pool*self.pool_w)
        
        #최댓값(2)
        out=np.max(col,axis=1)
        
        #성형(3)
        out=out.reshape(N,out_h,out_w,C).transpose(0,3,1,2)
        return out

[Note]

최댓값 계산에는 넘파이의 np.max 메서드를 사용할 수 있다. np.max는 인수로 축(axis)을 지정할 수 있는데, 이 인수로 지정한 축마다 최댓값을 구할 수 있다. 가령 np.max(x,axis=1)과 같이 쓰면 입력 x의 1번째 축마다 최댓값을 구한다.

 

CNN 구현하기

합성곱 계층과 풀링 계층을 구현했으니, 조합하여 손글자 인식 CNN을 조립해보자.

단순한 CNN의 네트워크 구성

초기화 때 받는 인수

-input_dim :입력 데이터(채널 수, 높이,너비)의 차원

-conv_param: 함성곱 계층의 하이퍼파라미터(딕셔너리). 딕셔너리 키는 다음과 같다.

   filter_num: 필터의 수

   filter_size: 필터 크기

   stride: 스트라이드

   pad: 패딩

   hidden_size: 은닉층(fully-connected)의 뉴런 수

   output_size: 출력층(fully_connected)의 뉴런 수

   weight_init_std: 초기화 때의 가중치 표준편차

 

여기서 CNN 계층의 하이퍼파라미터는 딕셔너리 형태로 주어진다(conv_param).

 

class SimpleConvNet:
    def __init__(self,input_dim=(1,28,28),
                conv_param={'filter_num':30,'filter_size':5,'pad':0,'stride':1},
                hidden_size=100,output_size=10,weight_init_std=0.01):
        filter_num=conv_param['filter_num']
        filter_size=conv_param['filter_size']
        filter_pad=conv_param['pad']
        filter_stride=conv_param['stride']
        input_size=input_dim[1]
        conv_output_size=(input_size-filter_size+2*filter_pad)/filter_stride+1
        pool_output_size=int(filter_num*(conv_output_size/2)*(conv_output_size/2))
        
        
        self.params={}
        self.params['W1']=weight_init_std*np.random.randn(filter_num,input_dim[0],filter_size,filter_size)
        self.params['b1']=np.zeros(filter_num)
        self.params['W2']=weight_init_std*np.random.randn(pool_output_size,hidden_size)
        self.params['b2']=np.zeros(hidden_size)
        self.parmas['W3']=weight_init_std*np.random.randn(hidden_size,output_size)
        self.params['b3']=np.zeros(output_size)
        
        self.layers=OrderedDict()
        self.layers['Conv1']=Convolution(self.params['W1'],self.params['b1'],conv_param['stride'],conv_param['pad'])
        self.layers['Relu1']=Relu()
        self.layers['Pool1']=Pooling(pool_h=2,pool_w=2,stride=2)
        self.layers['Affine1']=Affine(self.params['W2'],self.params['b2'])
        
        self.layers['Relu2']=Relu()
        self.layers['Affine2']=Affine(self.params['W3'],self.params['b3'])
        
        self.last_layer=SoftmaxWithLoss()
    def predict(self,x):
        for layer in self.layers.values():
            x=layer.forward(x)
        return x
    
    def loss(self,x,t):
        y=self.predict(x)
        return self.last_layer.forward(y,t)
    
    def gradient(self,x,t):
        #순전파
        self.loss(x,t)
        
        #역전파
        dout=1
        dout=self.last_layer.backward(dout)
        
        layers=list(self.layers.values())
        layers.reverse()
        for layer in layers:
            dout=layer.backward(dout)
        
        #결과 저장
        grads={}
        grads['W1']=self.layers['Conv1'].dW
        grads['b1']=self.layers['Conv1'].db
        grads['W2']=self.layers['Affine1'].dW
        grads['b2']=self.layers['Affine1'].db
        grads['W3']=self.layers['Affine2'].dW
        grads['b3']=self.layers['Affine2'].db
        
        return grads
    
    
    

 

이상으로 DeepLearning from Scratch 밑바닥부터 시작하는 딥러닝1을 전부 공부하며 리뷰했다. 다음은 

딥러닝 2를 리뷰하겠다.

CNN(Convolutional neural network)은 이미지 인식과 음성 인식 등 다양한 곳에서 사용된다.

특히, 이미지 인식 분야에서 딥러닝을 활용한 기법은 거의 CNN을 기초로 한다.

 

전체 구조

CNN도 지금까지 본 신경망과 같이 레고 블록처럼 계층을 조합하여 만들 수 있다. 다만, 합성곱 계층(Convolutional layer)과 풀링 계층(pooling layer)이 등장한다.

 

복기하자.

완전연결(fully-connected): 인접하는 계층의 모든 뉴런과 결합되어 있는 신경망.

 

위 계층을 Affine 계층이라는 이름으로 구현했다.

완전연결 계층(Affine 계층)으로 이뤄진 네트워크의 예

위 그림과 같이 완전연결 신경망은 Affine 계층 뒤에 활성화 함수를 갖는 ReLU 계층(or Sigmoid 계층)이 이어진다.

이 그림에서는 Affine-ReLU 조합이 4개가 쌓였고, 마지막 5번째 층은 Affine 계층에 이어 Softmax 계층에서 최종 결과(확률)를 출력한다.

CNN으로 이뤄진 네트워크의 예: 합성곱 계층과 풀링 계층이 새로 추가(회색)

위 그림은 새로운 '합성곱 계층'과 '풀링 계층'이 추가된다. 

CNN의 계층은 'Conv-ReLU-(Pooling)'흐름으로 연결된다.(Pooling 계층은 필요에 따라 생략하기도 한다)

 

여기서 주목할 점은 출력에 가까운 층에서는 지금까지의 완전연결 신경망의 'Affine-ReLU'구성을 사용할 수 있다는 것이다. 또한, 마지막 출력 계층에서는 'Affine-Softmax' 조합을 그대로 사용한다.

이상이 일반적인 CNN 구성이다.

 

합성곱 계층

CNN에서는 패딩(padding), 스트라이드(stride) 등 CNN 고유의 용어가 등장한다. 또한, 각 계층 사이에는 3차원 데이터같이 입체적인 데이터가 흐른다는 점에서 fully-connected와 다르다. 

 

완전연결 계층의 문제점

간단하게 말하면 fully-connected 계층은 '데이터의 형상이 무시'된다는 점이다.

예를 들어 데이터가 이미지인 경우를 생각해보자. 이미지는 통상 세로 채널(색상)으로 구성된 3차원 데이터이다.

그러나 완전연결 계층에 입력할 때는 3차원 데이터를 평평한 1차원데이터로 평탄화해줘야 한다. 

이전까지의 MNIST 데이터셋을 사용한 사례에서는 형상이 (1,28,28)인 이미지(1채널, 세로 28픽셀, 가로 28픽셀)를 1줄로 세운 784개의 데이터를 첫 Affine 계층에 입력했다.

 

이미지는 3차원 형상이며, 이 형상에는 공간적 정보가 담겨있다. 예를 들어 공간적으로 가까운 픽셀은 값이 비슷하거나, RGB의 각 채널은 서로 밀접하게 관련되어 있거나, 거리가 먼 픽셀끼리는 별 연관이 없는 등, 3차원 속에서 의미를 갖는 본질적인 패턴이 숨어 있을 것이다. 

 

이에 완전연결 계층은 형상(공간적 정보)은 무시하고 모든 입력 데이터를 동등한 뉴런(같은 차원의 뉴런)으로 취급하여 형상에 담긴 정보를 살릴 수 없다.

 

하지만 합성곱 계층은 형상을 유지한다. 이미지도 3차원 데이터로 입력받으며, 마찬가지로 다음 계층에도 3차원 데이터로 전달한다. 

그래서 CNN에서는 이미지처럼 형상을 가진 데이터를 제대로 이해할 (가능성이 있는) 것이다.

 

CNN에서는 합성곡 계층의 입출력 데이터를 특징 맵(feature map)이라고 한다. 합성곱 계층의 입력 데이터를 입력 특징 맵(input feature map), 출력 데이터를 출력 특징맵(output feature map)이라고 한다.

 

합성곱 연산

합성곱 계층에서의 합성곱 연산을 처리한다. 합성곱 연산은 이미치 처리에서는 필터연산에 해당한다.

합성곱  연산의 예: 합성곱 연산을 *기호로 표기

위 그림과 같이 합성곱 연산은 입력 데이터에 필터를 적용한다. 입력 데이터는 세로*가로 방향의 형상을 가졌고, 필터 역시 세로*가로 방향의 차원을 갖는다. 데이터와 필터의 형상을 (height,width)로 표기하며, 이 예에서는 입력은 (4,4),필터는 (3,3), 출력은 (2,2)가 된다. 문헌에 따라 필터를 커널이라고 칭하기도 한다.

 

합성곱 연산은 필터의 윈도우를 일정 간격으로 이동해가며 입력 데이터에 적용한다. 

합성곱 연산의 계산 순서

여기서 말하는 윈도우는 위 그림에서 회색 3*3 부분을 가리킨다. 그림과 같이 입력 필토에서 대응하는 원소끼리 곱한 후 그 총합을 구한다.(이 계산을 단일 곱셈-누산 fused multiply-add,FMA라 한다).

그 후 결과를 출력의 해당 장소에 저장한다. 이 과정을 모든 장소에서 수행하면 합성곱 연산의 출력이 완성된다.

 

*fully-connected neural network에는 가중치 매개변수와 편향이 존재한다. 

*CNN에서는 필터의 매개변수가 그동안의 '가중치'에 해당한다. 또한 편향도 존재한다.

 

합성곱 연산의 편향: 필터를 적용한 원소에 고정값(편향)을 더한다.

 

패딩(Padding)

패딩이란 합성곱 연산을 수행하기 전에 입력 데이터 주변을 특정 값 으로 채우는 것을 말한다.

합성곱 연산에서는 자주 이용하는 기법이다. 

합성곱 연산의 패딩 처리: 입력 데이터 주위에 0을 채운다(패딩은 점선으로 표시했으며 그 안의 값'0'은 생략)

위 그림과 같이 처음 크기가 (4,4)인 입력 데이터에 패딩이 추가되어 (6,6)이 된다. 이 입력에 (3,3)크기의 필터를 걸면 (4,4) 크기의 출력 데이터가 생성된다. 

위 예는 패딩을 1로 설정했지만, 2로 설정하면 입력 데이터의 크기는 (8,8)이 되고 3으로 설정하면 (10,10)이 된다.

 

[Note]

패딩은 주로 출력 크기를 조정할 목적으로 사용한다. 예를 들어 (4,4) 입력 데이터에 (3,3)필터를 적용하면 출력은 (2,2)가되어 입력보다 2만큼 줄어든다. 이는 합성곱 연산을 몇 번이나 되플이하는 심층 신경망에서는 문제가 될 수 있다. 합성곱 연산을 거칠 때마다 크기가 작아지면 어느 시점에서는 출력 크기가 1이 되버린다. 즉, 더 이상 합성곱 연산을 적용할 수 없다는 뜻이다. 이를 막기 위해 패딩을 사용한다. 앞의 예에서는 패딩의 폭을 1로 설정하니 (4,4) 입력에 대한 출력이 같은 크기인 (4,4)로 유지되었다. 

 

스트라이드(Stride)

스트라이드란 필터를 적용하는 위치의 간격을 말한다. 지금까지 본 예는 스트라이드가 1이었지만, 스트라이드를 2로 하면 필터를 적용하는 윈도우가 두 칸씩 이동한다.

스트라이드가 2인 합성곱 연상

스트라이드를 2로 하면 출력은 (3,3)이 된다. 이처럼 스트라이드를 키우면 출력 크기는 작아진다. 

이와 반대로 패딩을 크게 하면 출력 크기는 커졌다. 수식으로 확인해보자.

입력 크기를 (H,W), 필터 크기를(FH,FW), 출력 크기를 (OH,OW), 패딩을 P, 스트라이드를 S라 하면, 출력크기는 다음과 같다.

 

위 예들처럼 단순히 값을 대입하기만 하면 출력 크기를 구할 수 있다.

단, OH와 OW가 정수로 나눠떨어지는 값이어야 한다는 점에 주의하자.

출력 크기가 정수가 아니면 오류를 내는 등의 대응을 해줘야 한다. 덧붙여, 딥러닝 프레임워크 중에는 값이 딱 나눠떨어지지 않을 때는 가장 가까운 정수로 반올림하는 등, 특별히 에러를 내지 않고 진행하도록 구현하는 경우도 있다.

 

3차원 데이터의 합성곱 연산

지금까지 2차원 형상을 다루는 합성곱 연산을 보았다. 그러나 이미지는 3차원 데이터라고 앞서 언급했다. 3차원 합성곱 연산에 대해 알아보자.

합성곱 연산의 예
3차원 데이터 합성곱 연산의 계산 순서

위 두 그림을 보면 길이 방향(채널 방향)으로 특징 맵이 늘어났다. 채널 쪽으로 특징 맵이 여러 개 있다면 입력 데이터와 필터의 합성곱 연산을 채널마다 수행하고, 그 결과를 더해서 하나의 출력을 얻는다.

 

주의할 점은 입력 데이터의 채널 수와 필터의 채널 수가 같아야 한다.

한편, 필터 자체의 크기는 원하는 값으로 설정할 수 있다.(단, 모든 채널의 필터가 같은 크기여야 한다).

위 예에서는 필터의 크기가 (3,3)이지만, 원한다면 (2,2)나 (1,1) 혹은 (5,5)등으로 설정해도 된다.

 

블록으로 생각하기

3차원의 합성곱 연산은 데이터와 필터를 직육면체 블록이라고 생각하면 쉽다.

합성곱 연산을 직육면체 블록으로 생각한다. 블록의 형상에 주의할 것!

예를 들어 채널 수 C,높이 H, 너비 W인 데이터의 형상은 (C,H,W)로 쓴다. 필터도 같은 순서로 쓴다. 

채널 수 C, 필터 높이(FH filter height), 필터 너비(FW filter width)로 쓴다.

 

합성곱 연산의 출력으로 다수의 채널을 내보내려면 어떻게 해야 할까? 답은 필터(가중치)를 다수 사용하는 것이다.

여러 필터를 사용한 합성곱 연산의 예

위 그림과 같이 필터를 FN개 적용하면 출력 맵도 FN개가 생성된다. 그 FN개의 맵을 모으면 형상이 (FN,OH,OW)인 블록이 완성된다. 

이 완성된 블록을 다음 계층으로 넘기겠다는 것이 CNN의 처리 흐름이다.

 

합성곱 연산에서는 필터의 수도 고려해야 한다. 그런 이유로 필터의 가중치 데이터는 4차원 데이터이다. 예를 들어 채널 수 3, 크기 5*5 인 필터가 20개 있다면 (20,3,5,5)로 쓴다.

 

합성곱 연산에도 (완전연결 계층과 마찬가지로) 편향이 쓰인다. 

 

합성곱 연산의 처리 흐름(편향 추가)

위 그림과 같이 편향은 채널 하나에 값 하나씩으로 구성된다. 위 예에서 편향의 형상은 (FN,1,1)이고, 필터의 출력 결과의 형상은 (FN,OH,OW)이다. 이들 두 블록을 더하면 편향의 각 값이 필터의 출력인 (FN,OH,OW) 블록의 대응 채널의 원소 모두에 더해진다. 

 

배치 처리

신경망 처리에서는 입력 데이터를 한 덩어리로 묶어 배치로 처리했다. 완전연결 신경망을 구현하면서는 이 방식을 지원하여 처리 효율을 높이고, 미니배치 방식의 학습도 지원하도록 했다. 합성곱 연산도 마찬가지로 배치 처리를 지원하고자 한다. 각 계층을 흐르는 데이터의 차원을 하나 늘려 4차원 데이터로 저장한다. 구체적으로는 데이터를 (데이터 수, 채널 수, 높이, 너비) 순으로 저장한다. 

합성곱 연산의 처리 흐름(배치 처리)

위 그림을 보면 각 데이터의 선두에 배치용 차원을 추가했다. 이처럼 데이터는 4차원 형상을 가진 채 각 계층을 타고 흐른다. 주의할 점으로는 신경망에 4차원 데이터가 하나 흐를 때마다 데이터 N개에 대한 합성곱 연산이 이뤄진다는 것이다. 즉, N회 분의 처리를 한번에 수행하는 것이다.

 

 

풀링(Pooling) 계층

풀링은 세로 가로 방향의 공간을 줄이는 연산이다. 

최대 풀링의 처리 순서

위 그림은 2*2 최대 풀링(max pooling)을 스트라이드 2로 처리하는 순서이다. 최대 풀링은 최댓값을 구하는 연산으로, '2*2'는 대상 영역의 크기를 뜻한다. 즉 2*2 최대 풀링은 그림과 같이 2*2 크기의 영역에서 가장 큰 원소 하나를 꺼낸다. 

그 후 윈도우가 2칸 간격으로 이동한다. 

참고로 풀링의 윈도우 크기와 스트라이드는 같은 값으로 설정하는 것이 보통이다.

 

[Note]

최대 풀링(max pooling): 대상 영역(윈도우 영역) 에서 최댓값을 취하는 연산

평균 풀링(average poling): 대상 영역(윈도우 영역)에서의 평균을 계산한다. 

*이미지 인식 분야에서는 주로 최대 풀링을 사용한다. 

 

풀링 계층의 특징

학습해야 할 매개변수가 없다.

풀링은 대상 영역에서 최댓값이나 평균을 취하는 명확한 처리이므로 특별히 학습할 것이 없다.

 

채널 수가 변하지 않는다.

풀링 연산은 입력 데이터의 채널 수 그대로 출력 데이터로 내보낸다. 독립적으로 계산하기 때문.

풀링은 채널 수를 바꾸지 않는다.

입력의 변화에 영향을 적게 받는다(강건하다)

입력 데이터가 조금 변해도 풀링의 결과는 잘 변하지 않는다. 

입력 데이터가 가로로 1원소만큼 어긋나도 출력은 같다(데이터에 따라 다를 수도 있다).

 

에러가 나오는 원인

get_config라는 함수가 없기때문.

callback 함수를 사용해 모델을 저장하려하는데 위 에러 발생

callback 함수를 사용하지 않고 model.fit을 하는 경우에는 위 에러가 발생하지 않는다.

 

해결 방법

model class 내에 get_config라는 함수를 만들어 주면 된다.

def get_config(self):
    config=super().get_config().copy()
    return config

 

참고URL:stackoverflow.com/questions/58678836/notimplementederror-layers-with-arguments-in-init-must-override-get-conf

챗봇을 만들기 위해서는 우선 앞선 포스팅인 트랜스포머에 대해서 이해하고 다음을 알아보자.

https://undeadkwandoll.tistory.com/26

 

트랜스포머(Transformer)(1)

Transformer는 2017년 구글이 발표한 논문인 'Attention is all you need'에서 나온 모델이다. 기존의 seq2seq 구조인 encoder-decoder를 따르면서도 논문의 이름처럼 어텐션 기법만으로 구현한 모델이다. 이 모델..

undeadkwandoll.tistory.com

undeadkwandoll.tistory.com/27

 

트랜스포머(Transformer)(2)

2021/03/04 - [Deep Learning/Deep Leaning inside] - 트랜스포머(Transformer)(1) 8)패딩 마스크(Padding Mask) 앞서 포스팅한 Scaled dot-product attention함수 내부를 보면 mask라는 값을 인자로 받아서, 이 m..

undeadkwandoll.tistory.com

 

데이터 로드

 

시작하기전 필요한 라이브러리를 불러오자.

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import re
import urllib.request
import time
import tensorflow_datasets as tfds
import tensorflow as tf

챗봇 데이터를 로드하여 상위 5개 샘플 출력해보자.

urllib.request.urlretrieve("https://raw.githubusercontent.com/songys/Chatbot_data/master/ChatbotData%20.csv", filename="ChatBotData.csv")
train_data = pd.read_csv('ChatBotData.csv')
train_data.head()

상위 5개 샘플

위 데이터를 보면 Q(question), A(answer) 의 쌍으로 이루어진 데이터이다.

#총 샘플 개수 확인
print('챗봇 샘플의 개수 :',len(train_data))

챗봇 샘플의 개수 : 11823

총 샘플 갯수는 11823개이며, 불필요한 NULL값이 있는지 확인해보자.

 

print(train_data.isnull().sum())

Null값은 별도로 존재하지 않는다. 기본적으로 사용하는 토큰화를 위한 형태소 분석툴을 사용하지 않고, 다른 방법인 학습 기반의 토크나이저를 사용할 것이다. 그러므로, row data에서 구두점을 미리 처리해야한다. 

구두점들은 단순히 제거해버릴수 있지만, 구두점 앞에 띄어쓰기를 추가하여 다른 문자들과 구분해보자.

 

questions=[]
for sentence in train_data['Q']:
    #구두점에 대해서 띄어쓰기
    #ex) 12시 땡! ->12시 땡 !
    sentence=re.sub(r"([?.!,])", r" \1",sentence)
    sentence=sentence.strip()
    questions.append(sentence)
answers=[]
for sentence in train_data['A']:
    #구두점에 대해서 띄어쓰기
    #ex) 12시 땡! ->12시 땡 !
    sentence=re.sub(r"([?.!,])",r" \1",sentence)
    sentence=sentence.strip()
    answers.append(sentence)

이후 잘 처리가 되었는지 확인해보자.

 

print(questions[:5])
print(answers[:5])



['12시 땡 !', '1지망 학교 떨어졌어', '3박4일 놀러가고 싶다', '3박4일 정도 놀러가고 싶다', 'PPL 심하네']
['하루가 또 가네요 .', '위로해 드립니다 .', '여행은 언제나 좋죠 .', '여행은 언제나 좋죠 .', '눈살이 찌푸려지죠 .']

다음과 같이 잘 처리되었다.

 

단어 집합 생성

서브워드텍스트인코더를 사용해 자주 사용되는 서브워드 단위로 토큰을 분리하는 토크나이저로 학습 데이터로부터 학습하여 서브워드로 구성된 단어 집합을 생성해보자.

#서브워드텍스트인코더를 사용하여 질문, 답변 데이터로부터 단어 집합(Vocabulary) 생성
tokenizer=tfds.deprecated.text.SubwordTextEncoder.build_from_corpus(questions+answers,target_vocab_size=2**13)

seq2seq에서의 인코더-디코더 모델 계열에는 디코더의 입력으로 사용할 시작을 의미하는 시작 토큰 SOS와 종료 토큰 EOS 또한 존재한다. 해당 토큰들도 단어 집합에 포함시킬 필요가 있으므로 이 두 토큰에 정수를 부여하자.

 

#시작 토큰과 종료 토큰에 대한 정수 부여.
START_TOKEN,END_TOKEN=[tokenizer.vocab_size],[tokenizer.vocab_size+1]

#시작 토큰과 종료 토큰을 고려햐여 단어 집합의 크기를 +2
VOCAB_SIZE=tokenizer.vocab_size+2

print('시작 토큰 번호: ',START_TOKEN)
print('종료 토큰 번호: ',END_TOKEN)
print('단어 집합의 크기: ',VOCAB_SIZE)


시작 토큰 번호:  [8170]
종료 토큰 번호:  [8171]
단어 집합의 크기:  8172

padding에 사용될 0번 토큰부터 마지막 토큰인 8171번 토큰까지의 개수를 카운트하면 단어 집합의 크기는 8172개이다.

 

정수 인코딩과 패딩

단어 집합 생성 후, 서브워드텍스트인코더의 토크나이저로 정수 인코딩을 진행할 수 있다. 이는 토크나이저의 .encode()를 사용하여 가능하다. 랜덤샘플 20번 질문 샘플, 즉 ,questions[20]을 갖고 인코딩 해보자.

 

#서브워드텍스트인코더 토크나이저의 .encode()를 사용하여 텍스트 시퀀스를 정수 시퀀스로 변환.
print('임의의 질문 샘플을 정수 인코딩 : {}'.format(tokenizer.encode(questions[20])))

임의의 질문 샘플을 정수 인코딩 : [5759, 607, 3502, 138, 681, 3740, 846]

임의의 질문 문장이 정수 시퀀스로 변환되었다. 반대로 정수 인코딩 된 결과는 다시 decode()를 사용해 기존의 텍스트 시퀀스로 복원할 수 있다. 

 

#서브워드텍스트인코더 토크나이저의 .encode()와 .decode() 테스트해보기
#임의의 입력 문장을 sample_string에 저장
sample_string=questions[20]

#encode() : 텍스트 시퀀스 -->정수 시퀀스
tokenized_string=tokenizer.encode(sample_string)
print('정수 인코딩 후의 문장 {}'.format(tokenized_string))

#decode(): 정수 시퀀스-->텍스트 시퀀스
original_string=tokenizer.decode(tokenized_string)
print('기존 문장: {}'.format(original_string))


정수 인코딩 후의 문장 [5759, 607, 3502, 138, 681, 3740, 846]
기존 문장: 가스비 비싼데 감기 걸리겠어

정수 인코딩 된 문장을 .decode()를 하면 자동으로 서브워드들까지 다시 붙여서 기존 단어로 복원해준다. 위 결과를 보면 정수가 7개인데 기존 문장의 띄어쓰기 단위인 어절은 4개밖에 존재하지 않는다. 이는 결국 '가스비','비싼데'라는 한 어절이 정수 인코딩 후에는 두 개 이상의 정수일 수 있다는 것이다.

확인해보자.

 

#각 정수는 각 단어와 어떻게 mapping되는지 병렬로 출력
#서브워드텍스트인코더는 의미있는 단위의 서브워드로 토크나이징한다. 띄어쓰기 단위 x형태소 분석 단위 x
for ts in tokenized_string:
    print('{}----->{}'.format(ts,tokenizer.decode([ts])))
    
    
5759----->가스
607----->비 
3502----->비싼
138----->데 
681----->감기 
3740----->걸리
846----->겠어

 

샘플1개를 갖고 인코딩 디코딩을 해보았다. 이제 전체 데이터에 대해 정수 인코딩과 패딩을 하자. 

 

#최대 길이를 40으로 정의
MAX_LENGTH=40

#토큰화/정수 인코딩/ 시작 토큰과 종료 토큰 추가/ 패딩
def tokenize_and_filter(inputs,outputs):
    tokenized_inputs,tokenized_outputs=[],[]
    
    for (sentence1,sentence2) in zip(inputs,outputs):
        #encode(토큰화 +정수 인코딩), 시작 토큰과 종료 토큰 추가
        sentence1=START_TOKEN+tokenizer.encode(sentence1)+END_TOKEN
        sentence2=START_TOKEN+tokenizer.encode(sentence2)+END_TOKEN
        
        tokenized_inputs.append(sentence1)
        tokenized_outputs.append(sentence2)
        
    #패딩
    tokenized_inputs=tf.keras.preprocessing.sequence.pad_sequences(tokenized_inputs,maxlen=MAX_LENGTH,padding='post')
    tokenized_outputs=tf.keras.preprocessing.sequence.pad_sequences(tokenized_outputs,maxlen=MAX_LENGTH,padding='post')

    return tokenized_inputs,tokenized_outputs
questions,answers=tokenize_and_filter(questions,answers)

데이터의 크기를 확인해보자.

print('질문 데이터의 크기(shape): ',questions.shape)
print('답변 데이터의 크기(shape): ',answers.shape)

질문 데이터의 크기(shape):  (11823, 40)
답변 데이터의 크기(shape):  (11823, 40)

임의로 3번 샘플을 출력해보자.

 

print(questions[3])
print(answers[3])

3번째 샘플

길이 40을 맞추기 위해 뒤에 0이 임의로 패딩되어있는 것을 확인할 수 있다.

 

인코더와 디코더의 입력, 레이블 만들기.

데이터를 배치 단위로 불러오기 위해 tf.data.Dataset을 사용한다.

 

#텐서플로우 dataset을 이용하여 셔플(shuffle)을 수행하되, 배치 크기로 데이터를 묶는다.
#또한 이 과정에서 교사 강요(teacher forcing)을 사용하기 위해서 디코더의 입력과 실제 값 시퀀스를 구성한다.
BATCH_SIZE=64
BUFFER_SIZE=20000

#디코더의 실제값 시퀀스에서는 시작 토큰을 제거해야 한다.
dataset=tf.data.Dataset.from_tensor_slices((
    {
        'inputs':questions,
        'dec_inputs':answers[:,:-1] #디코더의 입력. 마지막 패딩 토큰이 제거된다.
    },
    {
      'outputs':answers[:,1:]  
    },
))

dataset=dataset.cache()
dataset=dataset.shuffle(BUFFER_SIZE)
dataset=dataset.batch(BATCH_SIZE)
dataset=dataset.prefetch(tf.data.experimental.AUTOTUNE)
#임의의 샘플에 대해서 [:,:-1] 과 [:,1:]이 어떤 의미를 가지는지 테스트해본다.
#기존 샘플
print(answers[0]) 
#마지막 패딩 토큰 제거하면서 길이가 39가 된다
print(answers[:1][:,:-1]) 
#맨 처음 토큰이 제거된다. 다시 말해 시작 토큰이 제거된다. 길이는 역시 39가 된다
print(answers[:1][:,1:]) 

 

트랜스포머 만들기

하이퍼파라미터를 조정하여 실제 논문의 트랜스포머보다는 작은 모델을 만든다.

주요 하이퍼파라미터는 다음과 같다.

tf.keras.backend.clear_session()

#Hyper-parameters
D_MODEL=256
NUM_LAYERS=2
NUM_HEADS=8
DFF=512
DROPOUT=0.1

model=transformer(
    vocab_size=VOCAB_SIZE,
    num_layers=NUM_LAYERS,
    dff=DFF,
    d_model=D_MODEL,
    num_heads=NUM_HEADS,
    dropout=DROPOUT)

learning rate, optimizer을 정의하고 compile 해보자

learning_rate=CustomSchedule(D_MODEL)
optimizer=tf.keras.optimizers.Adam(learning_rate,beta_1=0.9,beta_2=0.98,epsilon=1e-9)

def accuracy(y_true,y_pred):
    #레이블의 크기는 (batch_size, MAX_LENGTH-1)
    y_true=tf.reshape(y_true,shape=(-1,MAX_LENGTH-1))
    return tf.keras.metrics.sparse_categorical_accuracy(y_true,y_pred)

model.compile(optimizer=optimizer,loss=loss_function,metrics=[accuracy])
EPOCHS=50
model.fit(dataset,epochs=EPOCHS)
Epoch 1/50
185/185 [==============================] - 272s 1s/step - loss: 1.4499 - accuracy: 0.0304
Epoch 2/50
185/185 [==============================] - 256s 1s/step - loss: 1.1818 - accuracy: 0.0495
Epoch 3/50
185/185 [==============================] - 273s 1s/step - loss: 1.0029 - accuracy: 0.0507
Epoch 4/50
185/185 [==============================] - 276s 1s/step - loss: 0.9276 - accuracy: 0.0547
Epoch 5/50
185/185 [==============================] - 263s 1s/step - loss: 0.8703 - accuracy: 0.0575
										.
                                        .
                                        .
Epoch 45/50
185/185 [==============================] - 204s 1s/step - loss: 0.0068 - accuracy: 0.1734
Epoch 46/50
185/185 [==============================] - 206s 1s/step - loss: 0.0068 - accuracy: 0.1735
Epoch 47/50
185/185 [==============================] - 206s 1s/step - loss: 0.0062 - accuracy: 0.1736
Epoch 48/50
185/185 [==============================] - 203s 1s/step - loss: 0.0063 - accuracy: 0.1736
Epoch 49/50
185/185 [==============================] - 204s 1s/step - loss: 0.0058 - accuracy: 0.1737
Epoch 50/50
185/185 [==============================] - 204s 1s/step - loss: 0.0058 - accuracy: 0.1737
<tensorflow.python.keras.callbacks.History at 0x2076df63ca0>

챗봇 평가

def evaluate(sentence):
    sentence = preprocess_sentence(sentence)

    sentence = tf.expand_dims(
      START_TOKEN + tokenizer.encode(sentence) + END_TOKEN, axis=0)

    output = tf.expand_dims(START_TOKEN, 0)

  # 디코더의 예측 시작
    for i in range(MAX_LENGTH):
        predictions = model(inputs=[sentence, output], training=False)

    # 현재(마지막) 시점의 예측 단어를 받아온다.
        predictions = predictions[:, -1:, :]
        predicted_id = tf.cast(tf.argmax(predictions, axis=-1), tf.int32)

    # 만약 마지막 시점의 예측 단어가 종료 토큰이라면 예측을 중단
        if tf.equal(predicted_id, END_TOKEN[0]):
            break

    # 마지막 시점의 예측 단어를 출력에 연결한다.
    # 이는 for문을 통해서 디코더의 입력으로 사용될 예정이다.
        output = tf.concat([output, predicted_id], axis=-1)

    return tf.squeeze(output, axis=0)
def predict(sentence):
    prediction = evaluate(sentence)

    predicted_sentence = tokenizer.decode(
      [i for i in prediction if i < tokenizer.vocab_size])

    print('Input: {}'.format(sentence))
    print('Output: {}'.format(predicted_sentence))

    return predicted_sentence 
def preprocess_sentence(sentence):
    sentence = re.sub(r"([?.!,])", r" \1 ", sentence)
    sentence = sentence.strip()
    return sentence
output = predict("영화 볼래?")
Input: 영화 볼래?
Output: 최신 영화가 좋을 것 같아요.

output=predict("머신러닝이란..")
Input: 머신러닝이란..
Output: 축하할 일이죠.

output=predict("너 불굴의관돌이 알아?")
Input: 너 불굴의관돌이 알아?
Output: 저도 쉬고 놀고 하고 싶어요.

output=predict("힘들구나")
Input: 힘들구나
Output: 건강에 유의하세요.

대략 4개정도 확인해보았지만 어느정도는 대답을 해주는 것을 볼 수 있다. 위 과정은 간단하게 만든 것이기에 이정도이지만, 실제 데이터를 더 많이 늘려서 시행해본다면 더 좋은 결과를 나을 수 있다고 생각해본다.

 

 

 

 

 

[출처]https://wikidocs.net/89786

 

 

Batch Normalization

앞전 포스팅에서 각 층의 활성화 값 분포를 관찰하며, 가중치의 초깃값을 적절히 설정하면 각 층의 활성화값 분포가 적당히 퍼지며, 학습이 원만하게 수행됨을 배웠다.

그렇다면,

각 층이 활성화를 적당히 퍼뜨리도록 '강제' 해보면 어떨까?

 

Batch Normalization Algorithm

배치 정규화가 주목받는 이유는 3가지가 있다.

1) 학습을 빨리 진행할 수 있다(학습 속도 개선)

2) 초깃값에 크게 의존하지 않는다(골치 아픈 초깃값 선택 장애 해소)

3) 오버피팅을 억제한다(드롭아웃 등의 필요성 감소)

 

배치 정규화를 사용한 신경망의 예

배치 정규화는 그 이름과 같이 학습 시 미니배치를 단위로 정규화한다. 데이터 분포가 평균이 0, 분산이 1이 되도록 정규화 하는 것이다.

수식은 다음과 같다.

미니배치 B={x1,x2,x....,xm}이라는 M개의 입력 데이터의 집합에 대해 평균 수식 1과 분산 수식 2를 구한다.

이 처리를 활성화 함수의 앞(혹은 뒤)에 삽입함으로써 데이터 분포가 덜 치우치게 할 수 있다.

또, 배치 정규화 계층마다 이 정규화된 데이터에 고유한 확대와 이동변환을 수행한다.

수식은 다음과 같다.

 

이 식에서 람다가 확대(scale)를 베타가 이동(shift)를 담당한다. 두 값은 처음에는 각 1,0부터 시작하고 학습하면서 적합한 값으로 조정해간다.

배치 정규화 계산 그래프

Batch Normalization 효과

실선이 배치 정규화를 사용한 경우, 점선이 사용하지 않은 경우: 가중치 초깃값의 표준편차는 각 그래프 위에 표기

MNIST데이터셋을 이용해 배치정규화 진도율을 확인해보았다. 거의 모든 경우에서 배치 정규화를 사용할 때의 학습 진도가 빠른 것을 나타난다. 실제로 이용하지 않는 경우, 초깃값이 잘 분포되어 있지 않으면 학습이 전혀 진행되지 않는 모습도 확인된다.

 

바른 학습을 위해

기계학습에서는 오버피팅이 문제가 되는 일이 많다. 

오버피팅이란 신경망이 훈련데이터에만 지나치게 적응되어 그 외의 데이터에는 제대로 대응하지 못하는 상태를 말한다.

우리의 목표는 기계학습의 범용 성능이다. 훈련 데이터에는 포함되지 않는, 아직 보지 못한 데이터가 주어져도 바르게 식별해내는 모델이 바람직하다. 

오버피팅

오버피팅이 일어날 가장 주된 경우는 2가지가 있다.

1)매개변수가 많고 표현력이 높은 모델

2)훈련 데이터가 적을때

 

다음 코드는 일부로 오버피팅을 일으킨 코딩이다.

(x_train,t_train),(x_test,t_test)=load_mnist(normalize=True)
#오버피팅을 재현하기 위해 학습 데이터 수를 줄임
x_train=x_train[:300]
t_train=t_train[:300]

network=MultiLayerNet(input_size=784,hidden_size_list=[100,100,100,100,100,100],output_size=10)
#학습률이 0.01인 SGD로 매개변수 갱신
optimizer=SGD(lr=0.01)
max_epochs=201
train_size=x_train.shape[0]
batch_size=100

train_loss_list=[]
train_acc_list=[]
test_acc_list=[]

iter_per_epoch=max(train_size/batch_size,1)
epoch_cnt=0

for i in range(100000000):
    batch_mask=np.random.choice(train_size,batch_size)
    x_batch=x_train[batch_mask]
    t_batch=t_train[batch_mask]
    
    grads=network.gradient(x_batch,t_batch)
    optimizer.update(network.params,grads)
    
    if i%iter_per_epoch==0:
        train_acc=network.accuracy(x_train,t_train)
        test_acc=network.accuracy(x_test,t_test)
        train_acc_list.append(train_acc)
        test_acc_list.append(test_acc)
        
        epoch_cnt+=1
        if epoch_cnt>=max_epochs:
            break

훈련 데이터를 사용하여 측정환 정확도는 100 에폭을 지나는 무렵부터 거의 100%이다. 

하지만, 시험 데이터에 대해서는 큰 차이를 보인다. 이처럼 정확도가 크게 벌어지는 것은  훈련데이터에만 적응해버린 것이다.

 

가중치 감소

오버피팅 억제용으로 예로부터 많이 이용해온 방법 중 가중치 감소(weight decay)가 있다.

이는 큰 가중치에 대해서는 그에 상응하는 큰 페널티를 부과하여 오버피팅을 억제하는 방법이다. 원래 오버피팅은 가중치 매개변수의 값이 커서 발생하는 경우가 많기 때문이다. 

이와 비슷한 것이 통계학에서는 회귀분석시 수치적 오류가 나는 것을 피하기위해 표준상관계수분석등을 추가로 하는 이유가 이런 것이다.(?)라고 한번 생각해본다.

신경망 학습의 목적은 손실 함수의 값을 줄이는 것이다. 이때 예를 들어 가중치의 제곱 norm을 손실함수에 더해주는 방법도 있다.

 

드롭아웃

가중치 감소는 간단하게 구현할 수 있고 어느 정도 지나친 학습을 억제할 수 있다. 그러나 신경망 모델이 복잡해지면 가중치 감소만으로는 대응하기 어려워진다. 이를 흔히 드롭아웃(drop out)이라는 기법을 이용한다.

 

드롭아웃이란 뉴런을 임의로 삭제하면서 학습하는 방법이다.

훈련 때 은닉층의 뉴런을 무작위로 골라 삭제한다. 삭제된 뉴런은 아래 그림과 같이 신호를 전달하지 않게 된다. 훈련때는 데이터를 흘릴 때마다 삭제할 뉴런을 무작위로 선택하고, 시험 떄는 모든 뉴런에 신호를 전달한다. 

단, 시험 때는 각 뉴런의 출력에 훈련 때 삭제한 비율을 곱하여 출력한다.

 

드롭아웃의 개념: 왼쪽이 일반적인 신경망, 오른쪽이 드롭아웃을 적용한 신경망, 드롭아웃은 뉴런을 무작위로 선택해 삭제하여 신호 전달을 차단한다.

코딩을 살펴보자.

class Dropout:
    def __init__(self,dropout_ratio=0.5):
        self.dropout_ratio=dropout_ratio
        self.mask=None
    def forward(self,x,train_flg=True):
        if train_flg:
            self.mask=np.random.rand(*x.shape)>self.dropout_ratio
            return x*self.mask
    def backward(self,dout):
        return dout*self.mask

핵심은 훈련 시에는 순전파 때마다 self.mask에 삭제할 뉴런을 False로 표시한다는 것이다. 

self.mask는 x와 형상이 같은 배열을 무작위로 생성하고, 그 값이 dropout_ratio보다 큰 원소만 True로 설정한다.

역전파 때의 동작은 ReLU와 같다.

즉, 순전파때 신호를 통과시키는 뉴런은 역전파 때도 신호를 그대로 통과시키고, 순전파 때 통과시키지 않은 뉴런은 역전파 때도 신호를 차단한다.

 

왼쪽은 드롭아웃 없이, 오른쪽은 드롭아웃을 적용한 결과(dropout_ratio=0.15)

위 그림과 같이 드롭아웃을 적용하니 훈련 데이터와 시험 데이터에 대한 정확도 차이가 줄었음을 볼 수있다.

또한, 훈련 데이터에 대한 정확도가 100%에 도달하지도 않게 되었다.

 

[Note]

기계학습에서는 앙상블 학습(ensemble learning)을 애용한다.

앙상블 학습은 개별적으로 학습시킨 여러 모델의 출력을 평균내어 추론하는 방식이다.
신경망의 맥락에서 얘기하면, 가령 같은 구조의 네크워크를 5개 준비하여 따로따로 학습시키고, 시험 때는 그 5개의 출력을 평균 내어 답하는 것이다.
앙상블 학습을 수행하면 신경망의 정확도가 몇% 정도 개선된다는 것이 실험적으로 알려져 있다.

앙상블 학습은 드롭아웃과 밀접하다.
드롭아웃이 학습 때 뉴런을 무작위로 삭제하는 행위를 매번 다른 모델을 학습시키는 것으로 해석할 수 있기 때문이다.
그리고 추론 때는 뉴런의 출력에 삭제한 비율(이를테면 0.5 등)을 곱함으로써 앙상블 학습에서 여러 모델의 평균을 내는 것과 같은 효과를 얻는 것이다.

즉, 드롭아웃은 앙상블 학습과 같은 효과를 (대략) 하나의 네트워크로 구현했다고 생각할 수 있다.

 

적절한 하이퍼파라미터 값 찾기

신경망에는 하이퍼파라미터가 다수 등장한다. 여기서 말하는 하이퍼파라미터는, 예를 들면 각 층의 뉴런 수, 배치 크기, 매개변수 갱신 시의 학습률과 가중치 감소 등이다. 이러한 하이퍼파라미터의 값을 적절히 설정하지 않으면 모델의 성능이 크게 떨어지기도 한다. 하이퍼라라미터의 값은 매우 중요하지만 그 값을 결정하기까지는 일반적으로 많은 시행착오를 겪는다.

 

 

검증 데이터

앞으로 하이퍼파라미터를 다양한 값으로 설정하고 검증할 텐데, 여기서 주의할 점은 하이퍼파라미터의 성능을 평가할 때는 시험 데이터를 사용해서는 안된다.

 

같은 성능 평가인데 안되는 이유가 무엇일까?

대답은 시험 데이터를 사용하여 하이퍼파라미터를 조정하면 하이퍼파라미터 값이 시험 데이터에 오버피팅되기 때문이다. 다시 말해, 하이퍼파라미터 값의 '좋음'을 시험 데이터로 확인하게 되므로 하이퍼파라미터 값이 시험 데이터에만 적합하도록 조정되어 버린다.

 

그래서 하이퍼파라미터를 조정할 떄는 하이퍼파라미터 전용 확인 데이터가 필요하다. 하이퍼파라미터 조정용 데이터를 일반적으로 검증데이터(validation data)라고 부른다.

 

[Note]

훈련 데이터는 매개변수(가중치와 편향)의 학습에 이용하고, 검증 데이터는 하이퍼파라미터의 성능을 평가하는 데 이용한다. 

시험 데이터는 범용 성능을 확인하기 위해서 마지막에 (이상적으로는 한번만) 이용한다.

-훈련데이터: 매개변수 학습

-검증데이터: 하이퍼파라미터 성능 평가

-시험데이터: 신경망의 범용 성능 평가

 

코드로 살펴보자.

(x_train,t_train),(x_test,t_test)=load_mnist()

#훈련 데이터를 뒤섞는다.
x_train,t_train=shuffle_dataset(x_train,t_train)

#20%를 검증 데이터로 분할
validation_rate=0.2
validation_num=int(x_train.shape[0]*validation_rate)

x_val=x_train[:validation_num]
t_val=t_train[:validation_num]
x_train=x_train[validation_num:]
t_train=t_train[validation_num:]

위 코드는 훈련 데이터를 분리하기 전에 입력 데이터와 정답 레이블을 섞는다. 이어서 검증 데이터를 사용하여 하이퍼파라미터를 최적화하는 기법을 살펴보자.

 

하이퍼파라미터 최적화

하이퍼파라미터를 최적화할 때의 핵심은 하이퍼파라미터의 '최적 값'이 존재하는 범위를 줄여나간다는 것이다. 

범위를 조금씩 줄이려면 우선 대략적인 범위를 설정하고 그 범위에서 무작위로 하이퍼파라미터 값을 골라낸(샘플링) 후, 그 값으로 정확도를 평가한다.

 

[Note] 

신경망의 하이퍼파라미터 최적화에서는 그리드 서치(grid search)같은 규칙적인 탐색보다는 무작위로 샘플링해 탐색하는 편이 좋은 결과를 낸다고 알려져 있다. 이는 최종 정확도에 미치는 영향력이 하이퍼파라미터마다 다르기 때문이다.

 

 

하이퍼파라미터의 범위는 '대략적' 지정이 가장 효과적이다. 실제로도 0.001에서 1000사이와 같이 10의 거듭제곱 단위로 범위를 지정한다. 이를 log scale이라고 부른다.

 

정리해보면,

1)하이퍼파라미터 값의 범위를 설정한다.

2)설정된 범위에서 하이퍼파라미터의 값을 무작위로 추출한다.

3)2단계에서 샘플링한 하이퍼파라미터 값을 사용하여 학습하고 검증 데이터로 정확도를 평가한다. (단, epoch은 작게 설정)

4)2단계와 3단계를 특정 횟수(log scale)반복하여, 그 정확도의 결과를 보고 하이퍼파라미터의 범위를 좁힌다.

 

MNIST 예제를 통해 결과를 살펴보면,

 

학습이 잘 진행될 때의 학습률은 0.001~0.01, 가중치 감소 계수는 10^-8~10^-6정도라는 것을 알 수 있다.

이처럼 잘될 것 같은 값의 범위를 관찰하고 범위를 좁혀가면된다.

+ Recent posts