suyeongpark

지성을 추구하는 사람/ suyeongpark@abyne.com

케라스 창시자에게 배우는 딥러닝/ 텍스트와 시퀀스를 위한 딥러닝

  • 이 장에서는 텍스트(단어의 시퀀스 또는 문자의 시퀀스), 시계열 또는 일반적인 시퀀스(sequence) 데이터를 처리할 수 있는 딥러닝 모델을 살펴보겠다.
  • 시퀀스 데이터를 처리하는 기본적인 딥러닝 모델은 순환 신경망(recurrent neural network)과 1D 컨브넷(1D convnet) 두 가지이다.
    • 1D 컨브넷은 이전 장에서 다룬 2D 컨브넷의 1차원 버전이다.
  • 다음 애플리케이션들이 이런 알고리즘들을 사용한다.
    • 문서 분류나 시계열 분류, 예컨대 글의 주제나 책의 저자 식별하기
    • 시계열 비교, 예컨대 두 문서나 두 주식 가격이 얼마나 밀접하게 관련되어 있는지 추정하기
    • 시퀀스-투-시퀀스 학습, 예컨대 영어 문장을 프랑스어로 변환하기
    • 감성 분석, 예컨대 트윗이나 영화 리뷰가 긍정적인지 부정적인지 분류하기
    • 시계열 예측, 예컨대 어떤 지역의 최근 날씨 데이터가 주어졌을 때 향후 날씨 예측하기

텍스트 데이터 다루기

  • 텍스트는 가장 흔한 시퀀스 형태의 데이터이다. 텍스트는 단어의 시퀀스나 문자의 시퀀스로 이해할 수 있다. 보통 단어 수준으로 작업하는 경우가 많다.
    • 다음 절에 소개할 시퀀스 처리용 딥러닝 모델은 텍스트를 사용하여 기초적인 자연어 이해(natural language understanding) 문제를 처리할 수 있다.
    • 이런 모델은 문서 분류, 감성 분석, 저자 식별, (제한된 범위의) 질문 응답(Question Answering, QA) 등의 애플리케이션에 적합하다.
    • 물론 이런 딥러닝 모델이 사람처럼 진짜 텍스트를 이해하는 것은 아니다. 이런 모델은 문자 언어(written language)에 통계적 구조를 만들어 간단한 텍스트 문제를 해결한다.
    • 컴퓨터 비전이 픽셀에 적용한 패턴 인식(pattern recognition)인 것처럼 자연어 처리(natural language processing)를 위한 딥러닝은 단어, 문장, 문단에 적용한 패턴 인식이다.
  • 다른 모든 신경망과 마찬가지로 텍스트 원본을 입력으로 사용하지 못한다. 딥러닝 모델은 수치형 텐서만 다룰 수 있기 때문이다. 텍스트를 수치형 텐서로 변환하는 과정을 텍스트 벡터화(vectorizing text)라고 한다. 여기에는 여러 방식이 있다.
    • 텍스트를 단어로 나누고 각 단어를 하나의 벡터로 변환한다.
    • 텍스트를 문자로 나누고 각 문자를 하나의 벡터로 변환한다.
    • 텍스트에서 단어나 문자의 n-그램(n-gram)을 추출하여 각 n-그램을 하나의 벡터로 변환한다. n-그램은 연속된 단어나 문자의 그룹으로 텍스트에서 단어나 문자를 하나씩 이동하면서 추출한다.
  • 텍스트를 나누는 이런 단위(단어, 문자, n-그램)를 토큰(token)이라고 한다. 그리고 텍스트를 토큰으로 나누는 작업을 토큰화(tokenization)라고 한다.
    • 모든 텍스트 벡터화 과정은 어떤 종류의 토큰화를 적용하고 생성된 토큰에 수치형 벡터를 연결하는 것으로 이루어진다. 
    • 이런 벡터는 시퀀스 텐서로 묶여져서 심층 신경망에 주입된다. 
  • 토큰과 벡터를 연결하는 방법은 여러 가지가 있는데 여기서는 두 가지 주요한 방법을 소개하겠다.
    • 토큰의 원-핫 인코딩(one-hot encoding)과 토큰 임베딩(token embedding)(일반적으로 단어에 대해서만 사용되므로 단어 임베딩(word embedding)이라고도 부른다) 그것이다.

Note) n-그램과 BoW

  • 단어 n-그램은 문장에서 추출한 N개(또는 그 이하)의 연속된 단어 그룹이다. 같은 개념이 단어 대신 문자에도 적용될 수 있다.
  • “The cat sat on the mat”라는 문장을 예로 들어 2-그램 집합으로 분해하면 다음과 같다.
    • { “The”, “The cat”, “cat”, “cat sat”, “sat”, “sat on”, “on”, “on the”, “the”, “the mat”, “mat” }
  • 같은 문장을 3그램 집합으로 분해하면 다음과 같다.
    • { “The”, “The cat”, “The cat sat”, “cat”, “cat sat”, “cat sat on”, “sat”, “sat on”, “sat on the”, “on”, “on the”, “on the mat”, “the”, “the mat”, “mat” }
  • 이런 집합을 각각 2-그램 가방(bag of 2-gram) 또는 3-그램 가방(bag of 3-gram)이라고 한다.
    • 가방(bag)이란 용어는 다루고자 하는 것이 리스트나 시퀀스가 아니라 토큰의 집합이라는 사실을 의미한다. 
    • 이 토큰에는 특정한 순서가 없다. 이런 종류의 토큰화 방법을 BoW(Back-of-Words)라고 한다.
  • BoW가 순서 없는 토큰화 방법이기 때문에(생성된 토큰은 시퀀스가 아니라 집합으로 간주되고 문장의 일반적인 구조가 사라진다) 딥러닝 모델보다 얕은 학습 방법의 언어 처리 모델에 사용되는 경향이 있다.
    • n-그램을 추출하는 것은 일종의 특성 공학이다. 딥러닝은 유연하지 못하고 이런 방식을 계층적인 특성 학습으로 대체한다. 나중에 소개할 순환 신경망과 1D 컨브넷으로 단어와 문자 그룹에 대한 특성을 학습할 수 있다.
    • 이 방식들은 그룹들을 명시적으로 알려 주지 않아도 연속된 단어나 문자의 시퀀스를 봄으로써 학습한다.
  • 이런 이유 때문에 이 책에서는 n-그램을 더 다루지 않는다. 하지만 로지스틱 회귀나 랜덤 포레스트 같은 얕은 학습 방법의 텍스트 처리 모델을 사용할 때는 강력하고 아주 유용한 특성 공학 방법임을 기억해 두자.

단어와 문자의 원-핫 인코딩

  • 원-핫 인코딩은 토큰을 벡터로 변환하는 가장 일반적이고 기본적인 방법이다.
    • 3장에서 IMDB와 로이터 예제에서 살펴보았는데, 모든 단어에 고유한 정수 인덱스를 부여하고 이 정수 인덱스 i를 크기가 N인 이진 벡터로 변환한다.
    • 이 벡터는 i번째 원소만 1이고 나머지는 모두 0이다.
  • 물론 원-핫 인코딩은 문자 수준에서도 적용할 수 있다. 원-핫 인코딩이 무엇이고 어떻게 하는지 명확하기 위해 아래 단어와 문자에 대한 간단한 예를 만들었다.
### 단어 수준의 원-핫 인코딩 예
import numpy as np

samples = ['The cat sat on the mat.', 'The dog ate my homework.']

token_index = {}

for sample in samples:
for word in sample.split():
if word not in token_index:
token_index[word] = len(token_index) + 1

max_length = 10

results = np.zeros(shape=(len(samples), max_length, max(token_index.values()) + 1))

for i, sample in enumerate(samples):
for j, word in list(enumerate(sample.split()))[:max_length]:
index = token_index.get(word)
results[i, j, index] = 1.

### 문자 수준의 원-핫 인코딩 예
import string

samples = ['The cat sat on the mat.', 'The dog ate my homework.']
characters = string.printable
token_index = dict(zip(characters, range(1, len(characters) + 1)))

max_length = 50
results = np.zeros((len(samples), max_length, max(token_index.values()) + 1))

for i, sample in enumerate(samples):
for j, character in enumerate(sample):
index = token_index.get(character)
results[i, j, index] = 1.
  • 케라스에는 원본 텍스트 데이터를 단어 또는 문자 수준의 원-핫 인코딩으로 변환해 주는 유틸리티가 있다. 특수 문자를 제거하거나 빈도가 높은 N개의 단어만 선택(입력 벡터 공간이 너무 커지지 않도록 하기 위한 일반적인 제한 방법)하는 등 여러 가지 중요한 기능들이 있기 때문에 이 유틸리티를 사용하는 편이 좋다.
from keras.preprocessing.text import Tokenizer

samples = ['The cat sat on the mat.', 'The dog ate my homework.']

tokenizer = Tokenizer(num_words=1000)
tokenizer.fit_on_texts(samples)

sequences = tokenizer.texts_to_sequences(samples)

one_hot_results = tokenizer.texts_to_matrix(samples, mode='binary')

word_index = tokenizer.word_index
print('%s개의 고유한 토큰을 찾았습니다' % len(word_index))
  • 원-핫 인코딩의 변종 중 하나는 원-핫 해싱(one-hot hashing) 기법이다. 이 방식은 어휘 사전에 있는 고유한 토큰의 수가 너무 커서 모두 다루기 어려울 때 사용한다.
    • 각 단어에 명시적으로 인덱스를 할당하고 이 인덱스를 딕셔너리에 저장하는 대신 단어를 해싱하여 고정된 크기의 벡터로 변환한다. 일반적으로 간단한 해싱 함수를 사용한다.
    • 이 방식의 주요 장점은 명시적인 단어 인덱스가 필요 없기 때문에 메모리를 절약하고 온라인 방식으로 데이터를 이노딩할 수 있다.
    • 한 가지 단점은 해시 충돌(hash collision)이다. 2개의 단어가 같은 해시를 만들면 이를 바라보는 머신 러닝 모델은 단어 사이의 차이를 인식하지 못한다. 해싱 공간 차원의 해싱될 고유 토큰의 전체 개수보다 훨씬 크면 해시 충돌의 가능성은 감소한다.
samples = ['The cat sat on the mat.', 'The dog ate my homework.']

dimensionality = 1000
max_length

results = np.zeros((len(samples), max_length, dimensionality))

for i, sample in enumerate(samples):
for j, word in list(enumerate(sample.split()))[:max_length]:
index = abs(hash(word)) % dimensionality
results[i, j, index] = 1.

단어 임베딩 사용하기

  • 단어와 벡터를 연관 짓는 강력하고 인기있는 또 다른 방법은 단어 임베딩이라는 밀집 단어 벡터(word vector)를 사용하는 것이다.
    • 원-핫 인코딩으로 만든 벡터는 희소(sparse)하고 (대부분 0으로 채워진다) 고차원이다 (어휘 사전에 있는 단어의 수와 차원이 같다)
    • 반면 단어 임베딩은 저차원의 실수형 벡터(희소 벡터의 반대인 밀집 벡터)이다. 

  • 원-핫 인코딩으로 얻은 단어 벡터와 달리 단어 임베딩은 데이터로부터 학습된다.
    • 보통 256차원, 512차원 또는 큰 어휘 사전을 다룰 때는 1024차원의 단어 임베딩을 사용한다.
    • 반면 원-핫 인코딩은 2만 개의 토큰으로 이루어진 어휘 사전을 만들려면 20,000 차원 또는 그 이상의 벡터일 경우가 많다. 따라서 단어 임베디잉 더 많은 정보를 적은 차원에 저장한다.
  • 단어 임베딩을 만드는 방법은 두 가지이다.
    • (문서 분류나 감성 예측 같은) 관심 대상인 문제와 함께 단어 임베딩을 학습한다. 이런 경우에는 랜덤한 단어 벡터로 시작해서 신경망의 가중치를 학습하는 것과 같은 방식으로 단어 벡터를 학습한다.
    • 풀려는 문제가 아니고 다른 머신 러닝에서 미리 계산된 단어 임베딩을 로드한다. 이를 사전 훈련된 단어 임베딩(pretrained word embedding)이라고 한다.

Embedding 층을 사용하여 단어 임베딩 학습하기

  • 단어와 밀집 벡터를 연관 짓는 가장 간단한 방법은 랜덤하게 벡터를 선택하는 것이다.
    • 이 방식의 문제점은 임베딩 공간이 구조적이지 않다는 것이다. 예컨대 accurate와 exact 단어는 대부분 문장에서 비슷한 의미로 사용되지만 완전히 다른 임베딩을 가진다.
    • 심층 신경망이 이런 임의의 구조적이지 않은 임베딩 공간을 이해하기는 어렵다.
  • 단어 벡터 사이에 좀 더 추상적이고 기하학적인 관계를 얻으려면 단어 사이에 있는 의미 관계를 반영해야 한다. 단어 임베딩은 언어를 기하학적 공간에 매핑하는 것이다.
    • 예컨대 잘 구축된 임베딩 공간에서는 동의어가 비슷한 단어 벡터로 임베딩될 것이다. 일반적으로 두 단어 벡터 사이의 거리(L2 거리)는 이 단어 사이의 의미 거리와 관계되어 있다. (멀리 떨어진 위치에 임베딩된 단어 의미는 서로 달느 반면 비슷한 단어들은 가까이 임베딩 된다)
    • 거리 외에 임베딩 공간의 특정 방향도 의미를 가질 수 있다.
  • 예를 들어보자. 아래 그림에서 4개의 단어 cat, dog, wolf, tiger가 2D 평면에 임베딩되어 있다.
    • 예컨대 cat에서 tiger로 이동하는 것과 dog에서 wolf로 이동하는 것을 같은 벡터로 나타낼 수 있다. 이 벡터는 ‘애완동물에서 야생 동물로 이동’ 하는 것으로 해석할 수 있다.
    • 비슷하게 다른 벡터로 dog에서 cat으로 이동하는 것과 wolf에서 tiger로 이동하는 것을 나타내면 ‘개과에서 고양이과로 이동’하는 벡터로 해석할 수 있다.

  • 실제 단어 임베딩 공간에서 의미 있는 기하학적 변환의 일반적인 예는 ‘성별’ 벡터와 ‘복수(plural)’ 벡터이다. 
    • 예컨대 ‘king’ 벡터에 ‘female’ 벡터를 더하면 ‘queen’ 벡터가 되고 plural 벡터를 더하면 ‘kings’가 된다.
    • 단어 임베딩 공간은 전형적으로 이런 해석이 가능하고 잠재적으로 유용한 수천 개의 벡터를 특성으로 가진다.
  • 사람의 언어를 매핑해서 어떤 자연어 처리 작업에도 사용할 수 있는 이상적인 단어 임베딩 공간이 존재할까? 아마도 가능하겠지만 아직까지 이런 종류의 공간은 만들지 못했다. 
    • 사람의 언어에도 그런 것은 없다. 세상에는 많은 다른 언어가 있고 언어는 특정 문화와 환경을 반영하기 때문에 서로 동일하지 않다.
  • 실제로 좋은 단어 임베딩 공간을 만드는 것은 문제에 따라 크게 달라진다. 
    • 영어로 된 영화 리뷰 감성 분석 모델을 위한 완벽한 단어 임베딩 공간은 영어로 된 법률 문서 분류 모델을 위한 완벽한 임베딩 공간과 다를 것이다. 특정 의미 관계의 중요성이 작업에 따라 다르기 때문이다.
  • 따라서 새로운 작업에는 새로운 임베딩을 학습하는 것이 타당하다. 다행히 역전파를 사용하여 쉽게 만들 수 있고, 케라스를 사용하면 더 쉽다. Embedding 층의 가중치를 학습하면 된다.
from keras.layers import Embedding

embedding_layer = Embedding(1000, 64)
  • Embedding 층을 (특정 단어를 나타내는) 정수 인덱스를 밀집 벡터로 매핑하는 딕셔너리로 이해하는 것이 가장 좋습니다. 정수를 입력으로 받아 내부 딕셔너리에서 이 정수에 연관된 벡터를 찾아 반환한다. 딕셔너리 탐색은 효율적으로 수행된다.
    • 단어 인덱스 -> Embedding 층 -> 연관된 단어 벡터
  • Embedding 층은 크기가 (samples, sequence_length)인 2D 정수 텐서를 입력으로 받는다. 각 샘플은 정수의 시퀀스이다. 가변 길이의 시퀀스를 임베딩에 임베딩할 수 있다.
    • 예컨대 앞 예제의 Embedding 층에 (32, 10) 크기의 배치(길이가 10인 시퀀스 32개로 이루어진 배치)나 (64, 15) 크기의 배치를 주입할 수 있다.
    • 배치에 있는 모든 시퀀스는 길이가 같아야 하므로 (하나의 텐서에 담아야 하기 때문에) 작은 길이의 시퀀스는 0으로 패딩되고 길이가 더 긴 시퀀스는 잘린다.
  • Embedding 층은 크기가 (samples, sequence_length, embedding_dimensionality)인 3D 실수형 텐서를 반환한다.
    • 이런 3D 텐서는 RNN 층이나 1D 합성곱 층에서 처리된다.
  • Embedding 층의 객체를 생성할 때 가중치(토큰 벡터를 위한 내부 딕셔너리)는 다른 층과 마찬가지로 랜덤하게 초기화된다.
    • 훈련하면서 이 단어 벡터는 역전파를 통해 점차 조정되어 이어지는 모델이 사용할 수 있도록 임베딩 공간을 구성한다.
    • 훈련이 끝나면 임베딩 공간은 특정 문제에 특화된 구조를 많이 가지게 된다.
  • IMDB 영화 리뷰 감성 예측 문제에 적용해 보자. 우선 데이터를 준비한다.
    • 영화 리뷰에서 가장 빈도가 높은 1만 개의 단어를 추출하고 리뷰에서 20개 단어 이후는 버린다.
    • 이 네트워크는 1만 개의 단어에 대해 8차원의 임베딩을 학습하여 정수 시퀀스 입력(2D 정수 텐서)을 임베딩 시퀀스(3D 실수형 텐서)로 바꿀 것이다.
    • 그 다음 이 텐서를 2D로 펼쳐서 분류를 위한 Dense 층을 훈련하겠다.
from keras.datasets import imdb
from keras import preprocessing

max_features = 10000
maxlen = 20

(x_train, y_train), (x_test, y_test) = imdb.load_data(num_words=max_features)

x_train = preprocessing.sequence.pad_sequences(x_train, maxlen=maxlen)
x_test = preprocessing.sequence.pad_sequences(x_test, maxlen=maxlen)

from keras.models import Sequential
from keras.layers import Flatten, Dense, Embedding

model = Sequential()
model.add(Embedding(10000, 8, input_length=maxlen))

model.add(Flatten())

model.add(Dense(1, activation='sigmoid'))
model.compile(optimizer='rmsprop', loss='binary_crossentropy', metrics=['acc'])
model.summary()

history = model.fit(x_train, y_train, epochs=10, batch_size=32, validation_split=0.2)

---
Train on 20000 samples, validate on 5000 samples Epoch 1/10 20000/20000 [==============================] - 1s 37us/step - loss: 0.6748 - acc: 0.6086 - val_loss: 0.6285 - val_acc: 0.6978 Epoch 2/10 20000/20000 [==============================] - 1s 33us/step - loss: 0.5490 - acc: 0.7491 - val_loss: 0.5282 - val_acc: 0.7304 Epoch 3/10 20000/20000 [==============================] - 1s 34us/step - loss: 0.4623 - acc: 0.7865 - val_loss: 0.5007 - val_acc: 0.7430 Epoch 4/10 20000/20000 [==============================] - 1s 33us/step - loss: 0.4223 - acc: 0.8091 - val_loss: 0.4940 - val_acc: 0.7496 Epoch 5/10 20000/20000 [==============================] - 1s 34us/step - loss: 0.3960 - acc: 0.8224 - val_loss: 0.4964 - val_acc: 0.7534 Epoch 6/10 20000/20000 [==============================] - 1s 33us/step - loss: 0.3757 - acc: 0.8339 - val_loss: 0.5000 - val_acc: 0.7488 Epoch 7/10 20000/20000 [==============================] - 1s 33us/step - loss: 0.3581 - acc: 0.8445 - val_loss: 0.5059 - val_acc: 0.7502 Epoch 8/10 20000/20000 [==============================] - 1s 33us/step - loss: 0.3408 - acc: 0.8545 - val_loss: 0.5126 - val_acc: 0.7476 Epoch 9/10 20000/20000 [==============================] - 1s 32us/step - loss: 0.3243 - acc: 0.8641 - val_loss: 0.5191 - val_acc: 0.7482 Epoch 10/10 20000/20000 [==============================] - 1s 32us/step - loss: 0.3084 - acc: 0.8715 - val_loss: 0.5257 - val_acc: 0.7486
  • 약 75% 가량의 검증 정확도가 나오는데, 20개의 단어만 사용했음을 생각해 보면 괜찮은 결과이다.
    • 하지만 임베딩 시퀀스를 펼치고 하나의 Dense 층을 훈련했으므로 입력 시퀀스에 있는 각 단어를 독립적으로 다루었다. 단어 사이의 관계나 문장 구조를 고려하지 않았다.
    • 각 시퀀스 전체를 고려한 특성을 학습하도록 임베딩 층 위에 순환 층이나 1D 합성곱을 추가하는 것이 좋다. 이는 다음절에서 다루겠다.

사전 훈련된 단어 임베딩 사용하기

  • 풀려는 문제와 함께 단어 임베딩을 학습하는 대신 미리 계산된 임베딩 공간에서 임베딩 벡터를 로드할 수 있다. 이런 임베딩 공간은 뛰어난 구조와 유용한 성질을 가지고 있어서 언어 구조의 일반적인 측면을 잡아낼 수 있다.
    • 자연어 처리에서 사전 훈련된 단어 임베딩을 사용하는 이유는 이미지 분류 문제에서 사전 훈련된 컨브넷을 사용하는 이유와 거의 동일하다.
    • 충분한 데이터가 없어서 자신만의 좋은 특성을 학습하지 못하지만 꽤 일반적인 특성이 필요할 때이다. 이런 경우에는 다른 문제에서 학습한 특성을 재사용하는 것이 합리적이다.
  • 단어 임베딩은 일반적으로 (문장이나 문서에 같이 등장하는 단어를 관찰하는) 단어 출현 통계를 사용해서 계산된다.
    • 여기에는 여러 기법이 있는데 신경망을 사용하는 것도 있고 그렇지 않은 방법도 있다.
    • 단어를 위해 밀집된 저차원 임베딩 공간을 비지도 학습 방법으로 계산하는 아이디어는 요슈아 벤지오 등이 2000년대 초에 조사했다.
    • 연구나 산업 애플리케이션에 적용되기 시작한 것은 Word2Vec 알고리즘(https://code.google.com/archive/p/word2vec)이 등장한 이후이다.
    • 이 알고리즘은 2013년 구글의 토마스 미코로프(Tomas Mikolov)가 개발했으며, 가장 유명하고 성공적인 단어 임베딩 방법이다. Word2Vec의 차원은 성별처럼 구체적인 의미가 있는 속성을 잡아낸다.
  • 케라스의 Embedding 층을 위해 내려 받을 수 있는 미리 계산된 단어 임베딩 데이터베이스가 여럿 있는데, Word2Vec 외에 인기 있는 또 다른 하나는 2014년 스탠포드 대학의 연구자들이 개발한 GloVe(Global Vectors for Word Representation)(https://nlp.stanford.edu/projects/glove)이다.
    • 이 임베딩 기법은 단어의 동시 출현(co-occurrence) 통계를 기록한 행렬을 분해하는 기법을 사용한다.
    • 이 개발자들은 위키피디아 데이터와 커먼 크롤(Common Crawl) 데이터에서 가져온 수백만 개의 영어 토큰에 대해 임베딩을 미리 계산해 놓았다.
  • GloVe 임베딩을 케라스 모델에 어떻게 사용하는지 알아보자. Word2Vec 임베딩이나 다른 단어 임베딩 데이터베이스도 방법은 같다.

모든 내용을 적용하기: 원본 텍스트에서 단어 임베딩까지

  • 케라스에 포함된 IMDB 데이터는 미리 토큰화가 되어 있기 때문에, 이를 사용하는 대신 원본 텍스트 데이터를 내려받아 처음부터 시작해 보겠다.

원본 IMDB 텍스트 내려받기

  • http://mng.bz/0tIo에서 IMDB 원본 데이터셋을 내려받고 압축을 해제한다.
  • 훈련용 리뷰 하나를 문자열 하나로 만들어 훈련 데이터를 문자열의 리스트로 구성해 보자. 리뷰 레이블(긍정/부정)도 labels 리스트로 만들겠다.
import os

imdb_dir = './datasets/aclImdb/'
train_dir = os.path.join(imdb_dir, 'train')

labels = []
texts = []

for label_type in ['neg', 'pos']:
dir_name = os.path.join(train_dir, label_type)

for fname in os.listdir(dir_name):
if fname[-4:] == '.txt':
f = open(os.path.join(dir_name, fname), encoding="utf8")
texts.append(f.read())
f.close()

if label_type == 'neg':
labels.append(0)
else:
labels.append(1)

데이터 토큰화

  • 텍스트를 벡터로 만들고 훈련 세트와 검증 세트로 나누겠다. 사전 훈련된 단어 임베딩은 훈련 데이터가 부족한 문제에 특히 유용하다. (그렇지 않으면 문제에 특화된 임베딩이 훨씬 성능이 좋다)
from keras.preprocessing.text import Tokenizer
from keras.preprocessing.sequence import pad_sequences
import numpy as np

maxlen = 100
training_samples = 200
validation_samples = 10000
max_words = 10000

tokenizer = Tokenizer(num_words=max_words)
tokenizer.fit_on_texts(texts)
sequences = tokenizer.texts_to_sequences(texts)

word_index = tokenizer.word_index
print('%s개의 고유한 토큰을 찾았습니다'% len(word_index))

data = pad_sequences(sequences, maxlen=maxlen)
labels = np.asarray(labels)
print('데이터 텐서의 크기:', data.shape)
print('레이블 텐서의 크기:', labels.shape)

indices = np.arange(data.shape[0])
np.random.shuffle(indices)
data = data[indices]
labels = labels[indices]

x_train = data[:training_samples]
y_train = labels[:training_samples]
x_val = data[training_samples:training_samples + validation_samples]
y_val = labels[training_samples:training_samples + validation_samples]

GloVe 단어 임베딩 내려받기

  • https://nlp.stanford.edu/projects/glove에서 2014년 영문 위키피디아를 사용하여 사전에 계산된 임베딩을 내려 받는다.
    • 이 파일의 이름은 glove.6B.zip 이고 파일 크기는 823MB이다.
    • 40만 개의 단어(또는 단어가 아닌 토큰)에 대한 100차원의 임베딩 벡터를 포함하고 있다.

임베딩 전처리

  • 압축 해제한 파일(.txt 팡리)을 파싱하여 단어(즉 문자열)와 이에 상응하는 벡터 표현(즉 숫자 벡터)을 매핑하는 인덱스를 만든다.
glove_dir = './datasets/glove/'

embeddings_index = {}
f = open(os.path.join(glove_dir, 'glove.6B.100d.txt'), encoding="utf8")

for line in f:
values = line.split()
word = values[0]
coefs = np.asarray(values[1:], dtype='float32')
embeddings_index[word] = coefs

f.close()

print('%s개의 단어 벡터를 찾았습니다.' % len(embeddings_index))
  • 그 다음 Embedding 층에 주입할 수 있도록 임베딩 행렬을 만든다. 이 행렬의 크기는 (max_words, embedding_dim)이어야 한다.
    • 이 행렬의 i번째 원소는 (토큰화로 만든) 단어 인덱스의 i번째 단어에 상응하는 embedding_dim 차원 벡터이다.
    • 인덱스 0은 어떤 단어나 토큰도 아닐 경우를 나타낸다.
embedding_dim = 100

embedding_matrix = np.zeros((max_words, embedding_dim))

for word, i in word_index.items():
if i < max_words:
embedding_vector = embeddings_index.get(word)
if embedding_vector is not None:
embedding_matrix[i] = embedding_vector

모델 정의하기

  • 이전과 동일한 구조의 모델을 사용한다.
from keras.models import Sequential
from keras.layers import Embedding, Flatten, Dense

model = Sequential()
model.add(Embedding(max_words, embedding_dim, input_length=maxlen))
model.add(Flatten())
model.add(Dense(32, activation='relu'))
model.add(Dense(1, activation='sigmoid'))
model.summary()
---
Layer (type) Output Shape Param # ================================================================= embedding_1 (Embedding) (None, 100, 100) 1000000 _________________________________________________________________ flatten_1 (Flatten) (None, 10000) 0 _________________________________________________________________ dense_1 (Dense) (None, 32) 320032 _________________________________________________________________ dense_2 (Dense) (None, 1) 33 ================================================================= Total params: 1,320,065 Trainable params: 1,320,065 Non-trainable params: 0

모델에 GloVe 임베딩 로드하기

  • Embedding 층은 하나의 가중치 행렬을 가진다. 이 행렬은 2D 부동소수 행렬이고 각 i번째 원소는 i번째 인덱스에 상응하는 단어 벡터이다.
    • 모델의 첫 번째 층인 Embedding 층에 준비된 GloVe 행렬을 로드하자.
model.layers[0].set_weights([embedding_matrix])
model.layers[0].trainable = False
  • 추가적으로 Embedding 층을 동결한다.
    • 사전 훈련된 컨브넷 특성을 사용할 떄와 같은 이유이다. 모델의 일부는 사전 훈련되고 다른 부분은 랜덤하게 초기화 되었다면 훈련하는 동안 사전 훈련된 부분이 업데이트 되면 안 된다.
    • 이미 알고 있던 정보를 모두 잃게 된다. 랜덤하게 초기화된 층에서 대량의 그래디언트 업데이트가 발생하면 이미 학습된 특성을 오염시키기 때문이다.

모델 훈련과 평가

  • 모델을 컴파일하고 훈련한다.
model.compile(optimizer='rmsprop', loss='binary_crossentropy', metrics=['acc'])

history = model.fit(x_train, y_train, epochs=10, batch_size=32, validation_data=(x_val, y_val))

model.save_weights('pre_trained_glove_model.h5')
  • 모델의 성능을 그래프로 그려보자.
import matplotlib.pyplot as plt

acc = history.history['acc']
val_acc = history.history['val_acc']
loss = history.history['loss']
val_loss = history.history['val_loss']

epochs = range(1, len(acc) + 1)

plt.plot(epochs, acc, 'bo', label='Training acc')
plt.plot(epochs, val_acc, 'b', label='Validation acc')
plt.title('Trainging and validation accuarcy')
plt.legend()

plt.figure()

plt.plot(epochs, loss, 'bo', label='Training loss')
plt.plot(epochs, val_loss, 'b', label='Validation loss')
plt.title('Trainging and validation loss')
plt.legend()

plt.show()

  • 이 모델은 과대적합이 빠르게 시작된다.
    • 훈련 샘플 수가 작기 때문에 놀라운 일은 아니다. 같은 이유로 검증 정확도와 훈련 정확도 사이에 차이가 크다. 검증 정확도는 50% 후반을 달성한 것 같다.
    • 훈련 샘플 수가 적기 때문에 어떤 샘플 200개를 선택했는지에 따라 성능이 크게 좌우된다. 여기서는 샘플들을 랜덤하게 선택했다.
  • 사전 훈련된 단어 임베딩을 사용하지 않거나 임베딩 층을 동결하지 않고 같은 모델을 훈련할 수 있다.
    • 이런 경우 해당 작업에 특화된 입력 토큰의 임베딩을 학습할 것이다. 데이터가 풍부하게 있다면 사전 훈련된 단어 임베딩보다 훨씬 성능이 높다.
    • 훈련 샘플이 200개 뿐이지만 시도해 보자.
    • (그래프 그리는 코드는 계속 동일한 코드가 반복되므로 생략)
model = Sequential()
model.add(Embedding(max_words, embedding_dim, input_length=maxlen))
model.add(Flatten())
model.add(Dense(32, activation='relu'))
model.add(Dense(1, activation='sigmoid'))
model.summary()

model.compile(optimizer='rmsprop', loss='binary_crossentropy', metrics=['acc'])

history = model.fit(x_train, y_train, epochs=10, batch_size=32, validation_data=(x_val, y_val))

  • 검증 정확도는 50% 초반에 멈추어있다. 이 예제에서는 사전 훈련된 단어 임베딩을 사용하는 것이 더 낫다. 
  • 마지막으로 테스트 데이터에서 모델을 평가해 보자. 우선 테스트 데이터를 토큰화 해야 한다.
test_dir = os.path.join(imdb_dir, 'test')

labels = []
texts = []

for label_type in ['neg', 'pos']:
dir_name = os.path.join(train_dir, label_type)
for fname in os.listdir(dir_name):
if fname[-4:] == '.txt':
f = open(os.path.join(dir_name, fname), encoding="utf8")
texts.append(f.read())
f.close()

if label_type == 'neg':
labels.append(0)
else:
labels.append(1)

sequences = tokenizer.texts_to_sequences(texts)
x_test = pad_sequences(sequences, maxlen=maxlen)
y_test = np.asarray(labels)
  • 그 다음 첫 번째 모델을 로드하고 평가한다.
model.load_weights('pre_trained_glove_model.h5')
model.evaluate(x_test, y_test)

정리

  • 이제 다음 작업을 할 수 있다.
    • 원본 텍스트를 신경망이 처리할 수 있는 형태로 변환한다.
    • 케라스 모델에 Embedding 층을 추가하여 어떤 작업에 특화된 토큰 임베딩을 학습한다.
    • 데이터가 부족한 자연어 처리 문제에서 사전 훈련된 단어 임베딩을 사용하여 성능 향상을 꾀한다.

순환 신경망 이해하기

  • 완전 연결 네트워크가 컨브넷처럼 지금까지 본 모든 신경망의 특징은 메모리가 없다는 것. 네트워크에 주입되는 입력은 개별적으로 처리되며 입력 간에 유지되는 상태가 없다.
    • 이런 네트워크로 시퀀스나 시계열 데이터 포인트를 처리하려면 네트워크에 전체 시퀀스를 주입해야 한다. 즉 전체 시퀀스를 하나의 데이터 포인트로 변환해야 한다.
    • 예컨대 IMDB 문제에서 영화 리뷰 하나를 큰 벡터 하나로 변환해서 처리했는데, 이런 네트워크를 피드포워드 네트워크(feedforward network)라고 한다.
  • 이와 반대로 사람이 문장을 읽는 것처럼 이전에 나온 것을 기억하면서 단어별로 또는 한눈에 들어오는 만큼씩 처리할 수 있다. 이는 문장에 있는 의미를 자연스럽게 표현하도록 도와준다.
    • 생물학적 지능은 정보처리를 위한 내부 모델을 유지하면서 점진적으로 정보를 처리한다. 
    • 이 모델은 과거 정보를 사용하여 구축되며 새롭게 얻은 정보를 계속 업데이트 한다.
  • 극단적으로 단순화 시킨 버전이지만 순환 신경망(Recurrent Neural Network, RNN)은 같은 원리를 적용한 것이다. 시퀀스의 원소를 순회하면서 지금까지 처리한 정보를 상태(state)에 저장한다.
    • 사실 RNN은 내부에 루프(loop)를 가진 신경망의 한 종류이다. RNN의 상태는 2개의 다른 시퀀스(2개의 다른 IMDB 리뷰)를 처리하는 사이에 재설정된다.
    • 하나의 시퀀스가 여전히 하나의 데이터 포인트로 간주된다. 즉 네트워크에 하나의 입력을 주입한다고 가정한다. 하지만 이 데이터 포인트가 한 번에 처리 되지 않는다는 점이 다르다. 그 대신 네트워크는 시퀀스의 원소를 차례대로 방문한다.

  • 루프와 상태에 대한 개념을 명확히 하기 위해 넘파이로 RNN 정방향 계산을 구현해 보자.
    • RNN은 크기가 (timesteps, input_features)인 2D 텐서로 인코딩된 벡터의 시퀀스를 입력 받는다.
    • 이 시퀀스는 타임스텝을 따라서 반복된다. 각 타임스텝 t에서 현재 상태와 ((input_features,)크기의)입력을 연결하여 출력을 계산한다.
    • 그 다음 이 출력을 다음 스텝의 상태로 설정한다. 첫 번째 타임스텝에서는 이전 출력이 정의되지 않으므로 현재 상태가 없다. 이때는 네트워크의 초기 상태(initial state)인 0 벡터로 상태를 초기화 한다.
  • 의사코드(pseudocode)로 표현하면 RNN은 다음과 같다.
state_t = 0

for input_t in input_sequence:
output_t = f(input_t, state_t)
state_t = output_t ---출력은 다음 반복을 위한 상태가 된다.
  • f 함수는 입력과 상태를 출력으로 변환한다. 이를 2개의 행렬 W와 U 그리고 편향 벡터를 사용하는 변환으로 바꿀 수 있다. 피드포워드 네트워크의 완전 연결 층에서 수행되는 변환과 비슷하다.
state_t = 0

for input_t in input_sequence:
output_t = activation(dot(W, input_t) + dot(U, state_t) + b)
state_t = output_t
  • RNN의 정방향 계산을 넘파이로 구현해 보자
import numpy as np

timesteps = 100
input_features = 32
output_features = 64

inputs = np.random.random((timesteps, input_features))

state_t = np.zeros((output_features,))

W = np.random.random((output_features, input_features))
U = np.random.random((output_features, output_features))
b = np.random.random((output_features,))

successive_outputs = []

for input_t in inputs:
output_t = np.tanh(np.dot(W, input_t) + np.dot(U, state_t) + b)
successive_outputs.append(output_t)
state_t = output_t

final_output_sequence = np.stack(successive_outputs, axis=0)
  • 요약하면 RNN은 반복할 때 이전에 계산한 정보를 재사용하는 for 루프에 지나지 않는다.
    • 물론 이 정의에 맞는 RNN 의 종류는 많다. 이 예는 가장 간단한 RNN의 형태이다. 
    • RNN은 스텝(step) 함수에 의해 특화된다.

케라스의 순환 층

  • 넘파이로 간단하게 구현한 과정이 실제 케라스의 SimpleRNN 층에 해당한다. SimpleRNN이 한 가지 다른 점은 넘파이 예제처럼 하나의 시퀀스가 아니라 다른 케라스 층와 마찬가지로 시퀀스 배치를 처리한다는 것이다. 
    • 즉 (timesteps, input_features) 크기가 아니라 (batch_size, timesteps, input_features) 크기의 입력을 받는다.
  • 케라스에 있는 모든 순환 층과 마찬가지로 SimpleRNN은 두 가지 모드로 실행할 수 있다.
    • 각 타임스텝의 출력을 모은 전체 시퀀스를 반환하거나 (크기가 (batch_size, timesteps, output_features)인 3D 텐서), 입력 시퀀스에 대한 마지막 출력만 반환할 수 있다 (크기가 (batch_size, output_features)인 2D 텐서)
    • 이 모드는 객체를 생성할 때 return_sequences 매개변수로 선택할 수 있다. 
  • SimpleRNN을 사용하여 마지막 타임스텝의 출력만 얻는 예제를 살펴보자.
from keras.models import Sequential
from keras.layers import Embedding, SimpleRNN

model = Sequential()
model.add(Embedding(10000, 32))
model.add(SimpleRNN(32))
model.summary()
---
Layer (type) Output Shape Param # ================================================================= embedding_1 (Embedding) (None, None, 32) 320000 _________________________________________________________________ simple_rnn_1 (SimpleRNN) (None, 32) 2080 ================================================================= Total params: 322,080 Trainable params: 322,080 Non-trainable params: 0
  • 다음 예는 전체 상태 시퀀스를 반환한다.
model = Sequential()
model.add(Embedding(10000, 32))
model.add(SimpleRNN(32, return_sequences=True))
model.summary()
---
Layer (type) Output Shape Param # ================================================================= embedding_2 (Embedding) (None, None, 32) 320000 _________________________________________________________________ simple_rnn_2 (SimpleRNN) (None, None, 32) 2080 ================================================================= Total params: 322,080 Trainable params: 322,080 Non-trainable params: 0
  • 네트워크의 표현력을 증가시키기 위해 여러 개의 순환 층을 차례대로 쌓는 것이 유용할 때가 있다. 이런 설정에서는 중간층들이 전체 출력 시퀀스를 반환하도록 설정해야 한다.
model = Sequential()
model.add(Embedding(10000, 32))
model.add(SimpleRNN(32, return_sequences=True))
model.add(SimpleRNN(32, return_sequences=True))
model.add(SimpleRNN(32, return_sequences=True))
model.add(SimpleRNN(32))
model.summary()
---
Layer (type) Output Shape Param # ================================================================= embedding_3 (Embedding) (None, None, 32) 320000 _________________________________________________________________ simple_rnn_3 (SimpleRNN) (None, None, 32) 2080 _________________________________________________________________ simple_rnn_4 (SimpleRNN) (None, None, 32) 2080 _________________________________________________________________ simple_rnn_5 (SimpleRNN) (None, None, 32) 2080 _________________________________________________________________ simple_rnn_6 (SimpleRNN) (None, 32) 2080 ================================================================= Total params: 328,320 Trainable params: 328,320 Non-trainable params: 0
  • IMDB 영화 리뷰 분류 문제에 적용해 보자. 우선 데이터를 전처리한다.
from keras.datasets import imdb
from keras.preprocessing import sequence

max_features = 10000
maxlen = 500
batch_size = 32

print('데이터 로딩...')
(input_train, y_train), (input_test, y_test) = imdb.load_data(num_words=max_features)
print(len(input_train), '훈련 시퀀스')
print(len(input_test), '테스트 시퀀스')

print('시퀀스 패딩 (samples x time)')

input_train = sequence.pad_sequences(input_train, maxlen=maxlen)
input_test = sequence.pad_sequences(input_test, maxlen=maxlen)
print('input_train 크기:', input_train.shape)
print('input_test 크기:', input_test.shape)
  • Embedding 층과 SimpleRNN 층을 사용하여 간단한 순환 네트워크를 훈련시켜보자
from keras.models import Sequential
from keras.layers import Embedding, SimpleRNN, Dense

model = Sequential()
model.add(Embedding(max_features, 32))
model.add(SimpleRNN(32))
model.add(Dense(1, activation='sigmoid'))

model.compile(optimizer='rmsprop', loss='binary_crossentropy', metrics=['acc'])
history = model.fit(input_train, y_train, epochs=10, batch_size=128, validation_split=0.2)
  • 이제 훈련과 검증의 손실과 정확도를 그래프로 그린다.
    • (그래프 그리는 코드는 계속 동일한 코드가 반복되므로 생략)

  • 3장에서 이 데이터셋을 사용한 첫 번째 모델에서 얻은 테스트 정확도는 87%였다. 안타깝지만 간단한 순환 네트워크는 이 기준 모델 보다 성능이 높지 않다. (85%)
    • 이런 원인은 전체 시퀀스가 아니라 처음 500개의 단어만 입력에 사용했기 때문이다. 이 RNN은 기준 모델보다 얻은 정보가 적다.
    • 다른 이유는 SimpleRNN이 텍스트처럼 긴 시퀀스를 처리하는데 적합하지 않기 때문이다.
    • 더 잘 작동하는 다른 순환층이 있으므로 살펴보자

LSTM과 GRU 층 이해하기

  • 케라스에는 SimpleRNN 외에 다른 순환층도 있는데 LSTM과 GRU가 그것이다. 실전에서는 항상 이 둘 중 하나를 사용할 것이다. SimpleRNN은 실전에 쓰기에는 너무 단순하기 때문이다.
    • SimpleRNN은 이론적으로 시간 t에서 이전의 모든 타임스텝의 정보를 유지할 수 있지만 실제로 긴 시간에 걸친 의존성은 학습할 수 없는 것이 문제이다.
    • 층이 많은 일반 네트워크(피드포워드 네트워크)에서 나타나는 것과 비슷한 현상인 그래디언트 소실 문제(vanishing gradient problem) 때문이다. 
  • 피드포워드 네트워크에 층을 많이 추가할 수록 훈련하기 어려운 것과 같다. 1990년대 초 호크라이더(Hochreiter), 슈미트후버(Schmidhuber), 벤지오(Bengio)가 이런 현상에 대한 이론적인 원인을 연구했다. 이 문제를 해결하기 위해 고안된 것이 LSTM과 GRU 층이다.
  • LSTM 층을 살펴보자. 장, 단기 메모리(Long Short-Term Memory, LSTM) 알고리즘은 호크라이더와 슈미트후버가 1997년 개발했다. 이 알고리즘은 그래디언트 소실 문제에 대한 연구의 결정체이다.
    • 이 층은 앞서 보았던 SimpleRNN의 한 변종이다. 정보를 여러 타임스텝에 걸쳐 나르는 방법이 추가된다.
    • 처리할 시퀀스에 나란히 작동하는 컨베이어 벨트를 생각해보자. 시퀀스 어느 지점에서 추출된 정보가 컨베이어 벨트 위로 올라가 필요한 시점의 타임스텝으로 이동하여 떨군다.
    • 이것이 LSTM이 하는 일이다. 나중을 위해 정보를 저장함으로써 처리 과정에서 오래된 시그널이 점차 소실되는 것을 막아 준다.
  • 이를 자세히 이해하기 위해 SimpleRNN 셀(cell)부터 그려보자.
    • 가중치가 여러 개가 나오므로 출력(output)을 타나내는 문자 o로 셀에 있는 W와 U 행렬을 표현하겠다. (Wo와 Uo)

  • 이 그림에 타임스텝을 가로질러 정보를 나르는 데이터 흐름을 추가해 보자.
    • 타임스텝 t에서 이 값을 이동 상태 c_t라고 부르겠다. 여기서 c는 이동(carry)를 의미한다.
    • 이 정보를 사용하여 셀이 다음과 같이 바뀐다. 입력 연결과 순환 연결(상태)로부터 이 정보가 합성된다(완전 연결층과 같은 변환: 가중치 행렬과 점곱한 후 편향을 더하고 활성화 함수를 적용한다)
    • 그러고는 다음 타임스텝으로 전달될 상태를 변경시킨다(활성화 함수와 곱셈을 통해서)
    • 개념적으로 보면 데이터를 실어 나르는 이 흐름이 다음 출력과 상태를 조절한다.

  • 여기까지는 간단하다. 이제 복잡한 부분은 데이터 흐름에서 다음 이동 상태(c_t+1)이 계산되는 방식이다. 여기에는 3개의 다른 변환이 관련되어 있다. 3개 모두 SimpleRNN과 같은 형태를 가진다.
y = activation(dot(state_t, U) + dot(input_t, W) + b)
  • 3개의 변환 모두 자신만의 가중치 행렬을 가진다. 각각 i, f, k로 표시하겠다. 다음이 지금까지 설명한 내용이다.
output_t = activation(c_t) * activation(dot(input_t, Wo) + dot(state_t, Uo) + bo)

i_t = activation(dot(state_t, Ui) + dot(input_t, Wi) + bi)
f_t = activation(dot(state_t, Uf) + dot(input_t, Wf) + bf)
k_t = activation(dot(state_t, Uk) + dot(input_t, Wk) + bk)
  • i_t, f_t, k_t를 결합하여 새로운 이동 상태 (c_t+1)을 구한다.
c_t + 1 = i_t * f_t * k_t
  • 아래 그림에 이를 추가했다.
    • (SimpleRNN이 단순히 이전 출력만을 이용하는 반면, LSMT에서는 carry 되는 값을 추가로 기존 결과에 곱해준다.)
    • (문제는 이 carray 되는 값을 어떻게 구하느냐 인데, 그것은 i, f, k라는 3개의 다른 가중치 행렬을 이용해서 output을 각각 구하고, 그것들의 합하여 최종적으로 carry 되는 값을 만든다)
    • (최종적으로 그렇게 구해진 carry되는 값을 원래 그 시점에 구해야 하는 output과 곱해서 최종 output을 만든다.)

  • 이 연산들이 하는 일을 해석하면 각 의미에 대해 통찰을 얻을 수 있다.
    • 예컨대 c_t와 f_t의 곱셈은 이동을 위한 데이터 흐름에서 관련이 적은 정보를 의도적으로 삭제한다고 할 수 있다.
    • 한편 i_t와 k_t는 현재에 대한 정보를 제공하고 이동 트랙을 새로운 정보로 업데이트 한다.
    • 하지만 결국 이런 해석은 큰 의미가 없다. 이 연산들이 실제로 하는 일은 연산에 관련된 가중치 행렬에 따라 결정되기 때문이다.
    • 이 가중치는 end-to-end 방식으로 학습된다. 이 과정은 훈련 반복마다 매번 새로 시작되며 이런저런 연산들에 특정 목적을 부여하기가 불가능하다.
  • RNN 셀의 사양(specification)은 가설 공간을 결정한다. 훈련할 때 이 공간에서 좋은 모델 파라미터를 찾는다.
    • 셀의 사양이 셀이 하는 일을 결정하지 않는다. 이는 셀의 가중치에 달려 있다.  같은 셀이라도 다른 가중치를 가지는 경우 매우 다른 작업을 수행한다.
    • 따라서 RNN 셀을 구성하는 연산 조합은 엔지니어링 적인 설계가 아니라 가설 공간의 제약 조건으로 해석하는 것이 낫다.
  • 연구자에게는 RNN 셀의 구현 방법 같은 제약 조건의 선택을 엔지니어보다 (유전 알고리즘이나 강화 학습 알고리즘 같으) 최적화 알고리즘에 맡기면 더 나아보일 것이다. 미래에는 이런 식으로 네트워크를 만들게 될 것이다.
    • 요약하면 LSTM 셀의 구체적인 구조에 대해 이해할 필요가 전혀 없고 우리가 해야 할 일도 아니다. LSTM 셀의 역할만 기억하면 된다. 바로 과거 정보를 나중에 다시 주입하여 그래디언트 소실 문제를 해결하는 것이다.

케라스를 사용한 LSTM 예제

  • 이제 LSTM 층으로 모델을 구성하고 IMDB 데이터에서 훈련해 보자.
    • 이 네트워크는 SimpleRNN에서 사용했던 모델과 비슷하다. LSTM 층은 출력 차원만 지정하고 다른 매개변수는 케라스의 기본값으로 남겨두었다.
    • 케라스는 좋은 기본값을 가지고 있어서 직접 매개변수를 튜닝하는데 시간을 쓰지 않고도 거의 항상 어느 정도 작동하는 모델을 얻을 수 있다.
from keras.models import Sequential
from keras.layers import Embedding, Dense,LSTM

model = Sequential()
model.add(Embedding(max_features, 32))
model.add(LSTM(32))
model.add(Dense(1, activation='sigmoid'))

model.compile(optimizer='rmsprop', loss='binary_crossentropy', metrics=['acc'])
history = model.fit(input_train, y_train, epochs=10, batch_size=128, validation_split=0.2)

  • 이번에는 88% 정도의 검증 정확도를 달성했다. SimpleRNN 보다 확실히 더 낫다. LSTM이 그래디언트 소실 문제로부터 덜 영향을 받기 때문이다.
    • 3장의 완전 연결 네트워크보다 조금 더 낫다.
  • 하지만 많은 계산을 사용한 것치고는 획기적인 결과는 아니다. 왜 LSTM의 성능이 더 높지 않을까? 
    • 한 가지 이유는 임베딩 차원이나 LSTM 출력 차원 같은 하이퍼파라미터를 전혀 튜닝하지 않았기 때문이다. 또 하나는 규제가 없기 때문이다.
    • 솔직히 말하면 가장 큰 이유는 리뷰를 전체적으로 길게 분석하는 것(LSTM이 잘하는 일)은 감성 분류 문제에 도움이 되지 않기 때문이다.
    • 이런 간단한 문제는 각 리뷰에 어떤 단어가 나타나고 얼마나 등장하는지를 보는 것이 낫다. 바로 첫 번째 완전 연결 네트워크가 사용한 방법이다.
    • 하지만 훨씬 더 복잡한 자연어 처리 문제들에서는 LSTM 능력이 드러난다. 특히 질문-응답과 기계 번역 분야이다.

정리

  • NN이 무엇이고 동작하는 방법
  • LSTM이 무엇이고 긴 시퀀스에서 단순한 RNN보다 더 잘 작동하는 이유
  • 케라스의 RNN 층을 사용해서 시퀀스 데이터를 처리하는 방법

순환 신경망의 고급 사용법

  • 여기서는 순환 신경망의 성능과 일반화 능력을 향상시키기 위한 세 가지 고급 기술을 살펴보겠다. 온도 예측 문제로 세 가지 개념을 모두 시연해 보겠다. 이 문제는 시계열 데이터에서 일반적으로 나타나는 여러 어려운 점을 갖고 있는데, 전형적이고 꽤 도전적인 문제이다.
  • 다음 기법들을 적용하겠다.
    • 순환 드롭아웃(recurrent dropout): 순환 층에서 과대적합을 방지하기 위해 케라스에 내장되어 있는 드롭아웃을 사용한다.
    • 스태킹 순환 층(stacking recurrent layer): 네트워크의 표현 능력(representational power)를 증가시킨다. (그 대신 계산 비용이 많이 든다)
    • 양방향 순환 층(bidirectional recurrent layer): 순환 네트워크에 같은 정보를 다른 방향으로 주입하여 정확도를 높이고 기억을 좀 더 오래 유지시킨다.

기온 예측 문제

  • 지금까지 다룬 시퀀스 데이터는 IMDB 데이터셋이나 로이터 데이터셋처럼 텍스트 데이터이다. 시퀀스 데이터는 이런 언어 처리 뿐만 아니라 훨씬 많은 문제에서 등장한다.
    • 이 절에 있는 모든 예제는 날씨 시계열 데이터셋을 사용한다. 이 데이터는 독일 예나(Jena) 시에 있는 막스 플랑크 생물지구화학 연구소의 지상 관측소에서 수집한 것이다.
  • 다음 경로에서 데이터를 내려받고 압축을 불자.
import os

data_dir = 'E:/Study/Keras Deep Learning/datasets/jena/'
fname = os.path.join(data_dir, 'jena_climate_2009_2016.csv')

f = open(fname)
data = f.read()
f.close()

lines = data.split('\n')
header = lines[0].split(',')
lines = lines[1:]
  • line 수를 출력해 보면 42만 개가 나온다. 데이터 전체를 넘파이 배열로 바꾸자.
import numpy as np

float_data = np.zeros((len(lines), len(header) - 1))

for i, line in enumerate(lines):
values = [float(x) for x in line.split(',')[1:]]
float_data[i, :] = values
  • 온도를 그래프로 그려보자.
from matplotlib import pyplot as plt

temp = float_data[:, 1]
plt.plot(range(len(temp)), temp)

  • 기간을 좁혀서 처음 10일간 온도 데이터를 나타내면 다음과 같다.

데이터 준비

  • 이 문제의 정확한 정의는 다음과 같다. lookback 타임스텝만큼 이전으로 돌아가서 매 steps 타임스텝마다 샘플링한다. 이 데이터를 바탕으로 delay 타임스텝 이후의 온도를 예측할 수 있을까? 사용할 변수는 다음과 같다.
    • lookback = 1440: 10일 전 데이터로 돌아간다.
    • steps = 6: 1시간마다 데이터 포인트 하나를 샘플링한다.
    • delay = 144 : 24시간이 지난 데이터가 타깃이 된다.
  • 시작하기 전에 두 가지 작업을 해야 한다.
    • 신경망에 주입할 수 있는 형태로 데이터를 전처리한다. 데이터가 이미 수치형이므로 추가적인 벡터화는 필요하지 않다. 하지만 데이터에 있는 각 시계열 특성의 범위가 서로 다르므로 시계열 특성을 개별적으로 정규화하여 비슷한 범위를 가진 작은 값으로 바꾸자.
    • float_date 배열을 받아 과거 데이터의 배치와 미래 타깃 온도를 추출하는 파이썬 제너레이터(generator)를 만든다. 이 데이터셋에 있는 샘플은 중복이 많다. (샘플 N과 샘플 N+1은 대부분 타임스텝이 비슷하다) 모든 샘플을 각기 메모리에 적재하는 것은 낭비가 심하므로 대신 원본 데이터를 사용하여 그때그때 배치를 만들자.
  • 각 시계열 특성에 대해 평균을 빼고 표준 편차로 나누어 전처리한다. 
    • 처음 20만개만 훈련 데이터로 사용할 것이므로 전체 데이터에서 20만 개만 사용해서 평균과 표준편차를 구한다.
data = float_data[:200000]
mean = data.mean(axis=0)
float_data -= mean
std = data.std(axis=0)
float_data /= std
  • 아래 코드는 여기서 사용할 제너레이터이다. 이 제너레이터 함수는 (samples, targets) 튜플을 반복적으로 반환한다. samples는 입력 데이터로 사용할 배치고, targets은 이에 대응되는 타깃 온도의 배열이다. 이 제네레이터 함수에는 다음 매개변수가 있다.
    • data: 정규화한 부동 소수 데이터로 이루어진 원본 배열
    • lookback: 입력으로 사용하기 위해 거슬러 올라갈 타임스텝
    • delay: 타깃으로 사용할 미래의 타임스텝
    • min_index와 max_index: 추출할 타임스텝의 범위를 지정하기 위한 data 배열의 인덱스. 검증 데이터와 테스트 데이터를 분리하는데 사용한다.
    • shuffle: 샘플을 섞을지 시간 순서대로 추출할지 결정한다.
    • batch_size: 배치의 샘플 수
    • step: 데이터를 샘플링할 타임스텝 간격. 1시간에 하나의 데이터 포인트를 추출하기 위해 6으로 지정한다.
def generator(data, lookback, delay, min_index, max_index, shuffle=False, batch_size=128, step=6):
if max_index is None:
max_index = len(data) - delay - 1

i = min_index + lookback

while 1:
if shuffle:
rows = np.random.randint(min_index + lookback, max_index, size=batch_size)
else:
if i + batch_size >= max_index:
i = min_index + lookback

rows = np.arange(i, min(i + batch_size, max_index))
i += len(rows)

samples = np.zeros((len(rows), lookback // step, data.shape[-1]))
targets = np.zeros((len(rows),))

for j, row in enumerate(rows):
indices = range(rows[j] - lookback, rows[j], step)
samples[j] = data[indices]
targets[j] = data[rows[j] + delay][1]

yield samples, targets
  • 이제 generator 함수를 사용하여 훈련용, 검증용, 테스트용으로 3개의 제너레이터를 만들어 보자.
    • 각 제너레이터는 원본 데이터에서 다른 시간대를 사용한다. 훈련 제너레이터는 처음 20만 개 타임스텝을 사용하고, 검증 제너레이터는 그 다음 10만 개, 테스트 제너레이터는 나머지를 사용한다.
lookback = 1440
step = 6
delay = 144
batch_size = 128

train_gen = generator(float_data, lookback=lookback, delay=delay, min_index=0, max_index=200000, shuffle=True, step=step, batch_size=batch_size)
validation_gen = generator(float_data, lookback=lookback, delay=delay, min_index=200001, max_index=300000, shuffle=True, step=step, batch_size=batch_size)
test_gen = generator(float_data, lookback=lookback, delay=delay, min_index=300001, max_index=None, shuffle=True, step=step, batch_size=batch_size)

val_steps = (300000 - 200001 - lookback) // batch_size
test_steps = (len(float_data) - 300001 - lookback) // batch_size

상식 수준의 기준점

  • 예측 문제를 풀기 전에 상식 수준의 해법을 시도해 보자. 이는 정상 여부 확인을 위한 용도고 고수준 머신 러닝 모델이라면 뛰어넘어야 할 기준점을 만든다.
    • 이런 상식 수준의 해법은 알려진 해결책이 없는 새로운 문제를 다루어야 할 때 유용하다.
    • 일부 클래스가 월등히 많아 불균형한 분류 문제가 고전적인 예이다. 데이터셋에 클래스 A의 샘플이 90%, 클래스 B의 샘플이 10%가 있다면, 이 분류 문제에 대한 상식 수준의 접근법은 새로운 샘플을 항상 클래스 ‘A’라고 예측하는 것이다.
    • 이 분류기는 전반적으로 90%의 정확도를 낼 것이다. 이따금 이런 기본적인 기준점을 넘어서기가 아주 어려운 경우가 있다.
  • 이 경우 온도 시계열 데이터는 연속성이 있고 일자별로 주기성을 가진다고 가정할 수 있다. 그렇기 때문에 상식 수준의 해결책은 지금으로부터 24시간 후 온도는 지금과 동일하다고 예측하는 것이다.
    • 이 방법을 평균 절댓값 오차(MAE)로 평가해 보자.
def evaluate_naive_method():
batch_maes = []

for step in range(val_steps):
samples, targets = next(validation_gen)
preds = samples[:, -1, 1]
mae = np.mean(np.abs(preds - targets))
batch_maes.append(mae)

print(np.mean(batch_maes))

evaluate_naive_method()
---
0.28904772281241886
  • 출력된 MAE는 0.29이다. 이 온도 데이터는 평균이 0이고 표준 편차가 1이므로 결괏값이 바로 와닿지는 않는다. 평균 절댓값 오차 0.29에 표준 편차를 곱하면 섭씨 2.57이 된다.
  • 이제 딥러닝 모델이 더 나은지 시도해 보자

기본적인 머신 러닝 방법

  • 머신 러닝 모델을 시도하기 전에 상식 수준의 기준점을 세웠다. 비슷하게 RNN 처럼 복잡하고 연산 비용이 많이 드는 모델을 시도하기 전에 간단하고 손쉽게 만들 수 있는 머신 러닝 모델(예컨대 소규모의 완전 연결 네트워크)을 먼저 만드는 것이 좋다.
    • 이를 바탕으로 더 복잡한 방법을 도입하는 근거가 마련되고 실제적인 이득도 얻게 될 것이다.
  • 아래 코대는 데이터를 펼쳐서 2개의 Dense 층을 통과시키는 완전 연결 네트워크를 보여준다.
    • 전형적인 회귀 문제이므로 마지막 Dense 층에 활성화 함수를 두지 않았다. 손실 함수는 MAE 이다.
    • 상식 수준의 방법에서 사용한 것과 동일한 데이터와 지표를 사용했으므로 결과를 바로 비교해 볼 수 있다.
from keras.models import Sequential
from keras import layers
from keras.optimizers import RMSprop

model = Sequential()
model.add(layers.Flatten(input_shape=(lookback // step, float_data.shape[-1])))
model.add(layers.Dense(32, activation='relu'))
model.add(layers.Dense(1))

model.compile(optimizer=RMSprop(), loss='mae')

history = model.fit_generator(train_gen, steps_per_epoch=500, epochs=20, validation_data=validation_gen, validation_steps=val_steps)
  • 훈련 손실과 검증 손실의 그래프를 그려보자.
import matplotlib.pyplot as plt

loss = history.history['loss']
val_loss = history.history['val_loss']

epochs = range(1, len(loss) + 1)

plt.figure()

plt.plot(epochs, loss, 'bo', label='Training loss')
plt.plot(epochs, val_loss, 'b', label='Validation loss')
plt.title('Trainging and validation loss')
plt.legend()

plt.show()

  • 일부 검증 손실은 학습을 사용하지 않은 기준점에 가깝지만 안정적이지 못하다.
    • 앞서 기준 모델을 만든 것이 도움이 된다. 이 문제는 기준 모델의 성능을 앞지르기가 쉽지 않다. 우리가 적용한 상식에는 머신 러닝 모델이 찾지 못한 핵심 정보가 많이 포함되어 있다.

첫 번째 순환 신경망

  • 첫 번째 완전 연결 네트워크는 잘 작동하지 않았다. 그렇다고 이 문제에 머신 러닝이 적합하지 않다는 뜻은 아니다. 
    • 앞선 모델은 시계열 데이터를 펼쳤기 때문에 입력 데이터에서 시간 개념을 잃어버렸다. 그 대신 인과 관계와 순서가 의미 있는 시퀀스 데이터를 그대로 사용해 보겠다.
    • 이런 시퀀스 데이터에 아주 잘 들어맞는 순환 시퀀스 모델을 시도해 보겠다. 이 모델은 앞선 모델과 달리 데이터 포인터의 시간 순서를 사용한다.
  • 이전 절에서 소개한 LSTM 층 대신 2014년에 정준영 등이 개발한 GRU 층을 사용하겠다.
    • GRU(Gated Recurrent Unit) 층은 LSTM 층과 같은 원리로 작동하지만 좀 더 간결하고 그래서 계산 비용이 덜 든다. (LSTM 만큼 표현 학습 능력이 높지는 않을 수 있다)
    • 계산 비용과 표현 학습 능력 사이의 트레이드오프(trade-off)는 머신 러닝 어디에서나 등장한다.
from keras.models import Sequential
from keras import layers
from keras.optimizers import RMSprop

model = Sequential()
model.add(layers.GRU(32, input_shape=(None, float_data.shape[-1])))
model.add(layers.Dense(1))

model.compile(optimizer=RMSprop(), loss='mae')

history = model.fit_generator(train_gen, steps_per_epoch=500, epochs=20, validation_data=validation_gen, validation_steps=val_steps)
  • 결과는 다음과 같다.

  • 상식 수준의 모델을 크게 앞질렀다. 이 그림은 시퀀스를 펼쳐서 처리하는 완전 연결 네트워크에 비해 순환 네트워크가 이런 종류의 작업에 훨씬 뛰어나다는 것과 머신 러닝의 가치를 보여준다.
  • 새로운 검증 MAE는 0.265 이하(크게 과대적합되기 시작하는 곳)이고 정규화되기 전인 섭씨로 복원하면 MAE는 2.35이다. 초기 에러 2.57보다 확실히 낫지만 더 개선할 수 있다.

과대적합을 감소하기 위해 순환 드롭아웃 사용하기

  • 훈련 손실과 검증 손실 곡선을 보면 모델이 과대적합인지 알 수 있다. 몇 번의 에포크 이후 훈련 손실과 검증 손실이 현저히 벌어지기 시작한다.
    • 이런 문제를 해결하기 위해 잘 알려진 드롭아웃 기법을 이미 보았다. 훈련 데이터를 층에 주입할 때 데이터에 있는 우연한 상관관계를 깨뜨리기 위해 입력 층의 유닛을 랜덤하게 끄는 기법이다.
    • 순환 신경망에 드롭아웃을 올바르게 적용하는 것은 간단하지 않다. 순환 층 이전에 드롭아웃을 적용하면 규제에 도움되는 것보다 학습에 더 방해되는 것으로 오랫동안 알려졌다.
  • 2015년 야린 갈(Yarin Gal)이 베이지안 딥러닝에 관한 박사 논문에서 순환 네트워크에 적절하게 드롭아웃을 사용하는 방법을 알아냈다.
    • 타임스텝마다 랜덤하게 드롭아웃 마스크를 바꾸는 것이 아니라 동일한 드롭아웃 마스크(동일한 유닛의 드롭 패턴)를 모든 타임스텝에 적용해야 한다.
    • GRU나 LSTM 같은 순환 게이트에 의해 만들어지는 표현을 규제하려면 순환 층 내부 계산에 사용된 활성화 함수에 타임스텝마다 동일한 드롭아웃 마스크를 적용해야 한다(순환 드롭 아웃 마스크)
    • 모든 타임스텝에 동일한 드롭 아웃 마스크를 적용하면 네트워크가 학습 오차를 타임스텝에 걸쳐 적절하게 전파시킬 것이다.
    • 타임스텝마다 랜덤한 드롭아웃 마스크를 적용하면 오차 신호가 전파되는 것을 방해하고 학습 과정에 해를 끼친다.
  • 야린 갈을 케라스를 사용해서 연구했고 케라스 순환 층에 이 기능을 구현하는데 도움을 주었다. 카레스에 있는 모든 순환 층은 2개의 드롭아웃 매개변수를 가지고 있다.
    • dropout은 층의 입력에 대한 드롭아웃 비율을 정하는 부동 소수 값이다. recurrent_dropout은 순환 상태의 드롭아웃 비율을 정한다.
  • GRU 층에 드롭아웃과 순환 드롭아웃을 적용하여 과대적합에 어떤 영향을 미치는지 살펴보자. 드롭아웃으로 규제된 네트워크는 언제나 완전히 수렴하는데 더 오래 걸린다. 에포크를 2배 더 늘려 네트워크를 훈련하자.
model = Sequential()
model.add(layers.GRU(32, dropout=0.2, recurrent_dropout=0.2, input_shape=(None, float_data.shape[-1])))
model.add(layers.Dense(1))

model.compile(optimizer=RMSprop(), loss='mae')

history = model.fit_generator(train_gen, steps_per_epoch=500, epochs=40, validation_data=validation_gen, validation_steps=val_steps)
  • 결과는 다음과 같다.

  • 30번 에포크까지 과대적합이 일어나지 않았다. 평가 점수는 안정적이지만 이전보다 더 나아지지는 않았다.

스태킹 순환 층

  • 과대적합은 더는 없지만 성능상 병목이 있는 것 같으므로 네트워크의 용량을 늘려야 한다.
    • 일반적인 머신 러닝 작업 흐름을 기억하자. 과대적합이 일어날 때까지 네트워크의 용량을 늘리는 것이 좋다. 너무 많이 과대적합되지 않는 한 아직 충분한 용량에 도달한 것이 아니다.
  • 네트워크의 용량을 늘리려면 일반적으로 층에 있는 유닛의 수를 늘리거나 층을 더 많이 추가한다.
    • 순환 층 스태킹은 더 강력한 순환 네트워크를 만드는 고전적인 방법이다.
    • 예컨대 구글 번역 알고리즘의 현재 성능은 7개 대규모 LSTM 층을 쌓은 대규모 모델에서 나온 것이다.
    • 케라스에서 순환 층을 차례대로 쌓으려면 모든 중간층은 마지막 타임스텝 출력만 아니고 전체 시퀀스(3D 텐서)를 출력해야 한다. return_sequence = True로 지정하면 된다.
model = Sequential()
model.add(layers.GRU(32, dropout=0.2, recurrent_dropout=0.2, return_sequences = True, input_shape=(None, float_data.shape[-1])))
model.add(layers.GRU(64, activation='relu', dropout=0.1, recurrent_dropout=0.5))
model.add(layers.Dense(1))

model.compile(optimizer=RMSprop(), loss='mae')

history = model.fit_generator(train_gen, steps_per_epoch=500, epochs=40, validation_data=validation_gen, validation_steps=val_steps)
  • 결과는 다음과 같다.

  • 층을 추가하여 성능을 조금 향상시켰지만, 크지는 않다. 여기서 두 가지 결론을 얻을 수 있다.
    • 아직 충분히 과대적합을 만들지 못했기 때문에 검증 손실을 향상하기 위해 층의 크기를 늘릴 수 있다. 하지만 적지 않은 계산 비용이 추가된다.
    • 층을 추가한 만큼 도움이 되지 않았으므로 여기서는 네트워크의 용량을 늘리는 것이 도움이 되지 않는다고 볼 수 있다.

양방향 RNN 사용하기

  • 이 절에서 소개할 마지막 기법은 양방향 RNN(bidirectionarl RNN)이다. 양방향 RNN은 RNN의 한 변종이고 특정 작업에서 기본 RNN 보다 훨씬 좋은 성능을 낸다. 자연어 처리에서는 맥가이버 칼이라고 할 정도로 즐겨 사용된다.
  • RNN은 특히 순서 또는 시간에 민감하다. 즉 입력 시퀀스의 타임스텝 순서대로 처리한다.
    • 타임스텝을 섞거나 거꾸로 하면 RNN이 시퀀스에서 학습하는 표현을 완전히 바꾸어 버린다. 이는 온도 예측처럼 순서에 의미가 있는 문제에 잘 맞는 이유이기도 하다.
  • 양방향 RNN은 RNN이 순서에 민감하다는 성질을 사용한다. 앞서 보았던 GRU나 LSTM 같은 RNN 2개를 사용한다. 각 RNN은 입력 시퀀스를 한 방향으로 처리한 후 각 표현을 합친다.
    • 시퀀스를 양쪽 방향으로 처리하기 떄문에 양방향 RNN은 단방향 RNN이 놓치기 쉬운 패턴을 감지할 수 있다.
  • 놀랍게도 이 절에 있는 RNN 층이 시간의 순서대로 시퀀서를 처리하는 것은 근거 없는 결정이다. 시간의 반대 방향으로 입력 시퀀스를 처리하면 만족할만한 RNN 성능을 낼 수 있을까? 
    • 실제 해보고 결과를 확인해 보자. 해야 할 일은 입력 시퀀스를 시간 차원을 따라 거꾸로 생성하는 데이터 제너레이터를 만드는 것 뿐이다. (제너레이터 함수의 마지막 줄을 yield samples[:, ::-1, :], targets)로 바꾼다.
  • 결과는 다음과 같다.

  • 순서를 뒤집은 GRU는 상식 수준의 기준점보다 성능이 낮다. 이 경우에는 시간 순서대로 처리하는 것이 중요하다.
    • 같은 기법을 IMDB에 적용해 보자.
from keras.datasets import imdb
from keras.preprocessing import sequence
from keras.models import Sequential
from keras import layers

max_features = 10000
maxlen = 500

(x_train, y_train), (x_test, y_test) = imdb.load_data(num_words=max_features)

x_train = [x[::-1] for x in x_train]
x_test = [x[::-1] for x in x_test]

x_train = sequence.pad_sequences(x_train, maxlen=maxlen)
x_test = sequence.pad_sequences(x_test, maxlen=maxlen)

model = Sequential()
model.add(layers.Embedding(max_features, 128))
model.add(layers.LSTM(32))
model.add(layers.Dense(1, activation='sigmoid'))

model.compile(optimizer='rmsprop', loss='binary_crossentropy', metrics=['acc'])

history = model.fit(x_train, y_train, epochs=10, batch_size=128, validation_split=0.2)
  • 시간 순서로 훈련한 LSTM과 거의 동일한 성능을 얻을 수 있다. 놀랍게도 이런 텍스트 데이터셋에는 순서를 뒤집어 처리하는 것이 시간 순서대로 처리하는 것과 거의 동일하게 잘 작동한다.
    • 이는 언어를 이해하는데 단어의 순서가 중요하지만 결정적이지 않다는 가정을 뒷받침한다. 거꾸로 된 시퀀스에서 훈련한 RNN은 원래 시퀀스에서 훈련한 것과는 다른 표현을 학습한다.
    • 이와 비슷하게 시작할 때 죽고 마지막 날 태어나는 삶처럼 실제 세상의 시간이 거꾸로 흘러간다면 우리의 정신 세계는 달라질 것이다.
    • 머신 러닝에서 다른 표현이 유용하다면 항상 사용할 가치가 있다. 이 표현이 많이 다를수록 더 좋다. 이 표현이 데이터를 바라보는 새로운 시각을 제공하고 다른 방식에서는 놓칠 수 있는 데이터의 특징을 잡아낸다.
    • 이런 표현은 작업의 성능을 올리는데 도움을 준다. 이것이 7장에서 살펴볼 앙상블(ensemble) 개념이다.
  • 양방향 RNN은 이 아이디어를 사용해서 시간 순서대로 처리하는 RNN의 성능을 향상 시킨다. 
    • 입력 시퀀스를 양쪽 방향으로 바라보기 때문에 드러나지 않은 다양한 표현을 얻어 시간 순서대로 처리할 때 놓칠 수 있는 패턴을 잡아낸다.

  • 케라스에서는 Bidirectional 층을 사용하여 양방향 RNN을 만든다. 이 클래스는 첫 번째 매개변수로 순환 층의 객체를 전달 받는다.
    • Bidirectional 클래스는 전달받은 순환 층으로 새로운 두 번째 객체를 만든다.
    • 하나는 시간 순서대로 입력 시퀀스를 처리하고 다른 하나는 반대 순서로 입력 시퀀스를 처리한다.
  • IMDB에 이를 적용해 보자.
model = Sequential()
model.add(layers.Embedding(max_features, 32))
model.add(layers.Bidirectionarl(layers.LSTM(32)))
model.add(layers.Dense(1, activation='sigmoid'))

model.compile(optimizer='rmsprop', loss='binary_crossentropy', metrics=['acc'])

history = model.fit(x_train, y_train, epochs=10, batch_size=128, validation_split=0.2)
  • 여기서 얻은 검증 정확도는 88% 정도로 이전 절에서 사용했던 일반 LSTM 보다 성능이 조금 더 높다. 이 모델은 조금 더 일찍 과대적합되는 것 같다.
    • 양방향 순환 층이 단방향 LSTM 보다 모델 파라미터가 2배 많기 때문에 놀라운 일은 아니다.
    • 규제를 조금 추가한다면 양방향 순환층을 사용하는 것이 이 작업에 더 적합해 보인다.
  • 이제 동일한 방식을 온도 예측 문제에 적용해 보자.
from keras.optimizers import RMSprop

model = Sequential()
model.add(layers.Bidirectionarl(layers.GRU(32), input_shape=(None, float_data.shape[-1])))
model.add(layers.Dense(1))

model.compile(optimizer=RMSprop(), loss='mae')

history = model.fit_generator(train_gen, steps_per_epoch=500, epochs=40, validation_data=validation_gen, validation_steps=val_steps)
  • 이 네트워크는 GRU 층와 비슷한 성능을 낸다. 이유는 쉽게 이해할 수 있는데, 모든 예측 성능은 시간 순서대로 처리하는 네트워크의 절반에서 온다. 시간 반대 순서로 처리하는 절반은 이런 작업에 성능이 매우 좋지 않기 때문이다.

더 나아가서

  • 온도 예측 문제의 성능을 향상시키기 위해 시도해 볼 수 있는 것들이 많이 있다.
    • 스태킹한 각 순환 층의 유닛 수를 조정한다. 지금 설정은 대부분 임의로 한 것이라 최적화가 덜 되었을 것이다.
    • RMSprop 옵티마이저가 사용한 학습률을 조정한다.
    • GRU 대신 LSTM 층을 사용한다.
    • 순환 층 위에 용량이 큰 완전 연결된 회귀 층을 사용한다. 즉 유닛 수가 많은 Dense 층이나 Dense 층을 스태킹 한다.
    • 최종적으로 (검증 MAE 기준으로 보았을 때) 최선의 모델을 테스트 세트에서 확인해야 한다. 이를 잊으면 과대적합된 네트워크 구조를 만들게 될 것이다.
  • 늘 그렇듯이 딥러닝은 과학보다 예술에 가깝다. 어떤 문제에 적합하거나 그렇지 않은 가이드라인은 제시할 수 있지만 결국 모든 문제는 다르다. 경험을 바탕으로 다른 전략들을 시도해 보아야 한다. 
    • 현재는 문제를 해결하는 최선의 방법을 미리 알 수 있는 이론이 없다. 반복해서 시도해야 한다.

정리

  • 장에서 처음 배운 것처럼 새로운 문제를 해결할 때는 선택한 지표에서 상식 수준의 기준점을 설정하는 것이 좋다. 기준점을 가지고 있지 않으면 실제로 향상되었는지 알 수 없다.
  • 계산 비용을 추가할지 판단하기 위해 비용이 비싼 모델 전에 간단한 모델을 시도한다.
  • 시간 순서가 중요한 데이터가 있다면 순환 층이 적합하다. 시계열 데이터를 펼쳐서 처리하는 모델의 성능을 쉽게 앞지를 것이다.
  • 순환 네트워크에 드롭아웃을 사용하려면 타임스텝 동안 일정한 드롭아웃 마스크와 순환 드롭아웃 마스크를 사용해야 한다. 둘 다 케라스 순환 층에 포함되어 있다. 순환 층에 있는 dropout과 recurrent_dropout 매개변수를 사용하면 된다.
  • 스태킹 RNN은 단일 RNN 층보다 더 강력한 표현 능력을 제공한다. 하지만 계산 비용이 많이 들기 때문에 항상 시도할 가치가 있지는 않다. (기계 번역 같은) 복잡한 문제에서 확실히 도움이 되지만 작고 간단한 문제에서는 항상 그렇지 않다.
  • 양쪽 방향으로 시퀀스를 바라보는 양방향 RNN은 자연어 처리 문제에 유용하다. 하지만 최근 정보가 오래된 것보다 훨씬 의미 있는 시퀀스 데이터에는 잘 작동하지 않는다.

컨브넷을 사용한 시퀀스 처리

  • 1D 컨브넷은 특정 시퀀스 처리 문제에서 RNN과 견줄 만하다. 일반적으로 계산 비용이 훨씬 싸다. 1D 컨브넷은 전형적으로 팽창된 커널(dilated kernel)과 함께 사용된다.
    • 최근 오디오 생성과 기계 번역 분야에서 큰 성공을 거두었다. 이런 특정 분야의 성공 이외에도 텍스트 분류나 시계열 예측 같은 간단한 문제에서 작은 1D 컨브넷이 RNN을 대신하여 빠르게 처리할 수 있다고 알려져있다.

시퀀스 데이터를 위한 1D 합성곱 이해하기

  • 앞서 소개한 합성곱 층은 2D 합성곱이다. 이미지 텐서에 2D 패치를 추출하고 모든 패치에 동일한 변환을 적용한다. 같은 방식으로 시퀀스에서 1D 패치(부분 시퀀스)를 추출하여 1D 합성곱을 적용한다.

  • 이런 1D 합성곱 층은 시퀀스에 있는 지역 패턴을 인식할 수 있다. 동일한 변환이 시퀀스에 있는 모든 패치에 적용되기 때문에 특정 위치에서 학습한 패턴을 나중에 다른 위치에서 인식할 수 있다.
    • 이는 1D 컨브넷에 (시간의 이동에 대한) 이동 불변성(translation invariant)을 제공한다.
    • 예컨대 크기 5인 윈도우를 사용하여 문자 시퀀스를 처리하는 1D 컨브넷은 5개 이하의 단어나 단어의 부분을 학습한다. 
    • 이 컨브넷은 이 단어가 입력 시퀀스의 어느 문장에 있더라도 인식할 수 있다. 따라서 문자 수준의 1D 컨브넷은 단어 형태학(word morphology)에 관해 학습할 수 있다.

시퀀스 데이터를 위한 1D 풀링

  • 컨브넷에서 이미지 텐서의 크기를 다운샘플링하기 위해 사용하는 평균 풀링이나 맥스 풀링 같은 2D 풀링 연산을 배웠다.
    • 1D 풀링 연산은 2D 풀링 연산과 동일하다. 입력에서 1D 패치(부분 시퀀스)를 추출하고 최댓값(최대 풀링)을 출력하거나 평균값(평균 풀링)을 출력한다.
    • 2D 컨브넷과 마찬가지로 1D 입력의 길이를 줄이기 위해 사용한다(서브샘플링(subsampling))

1D 컨브넷 구현

  • 케라스에서 1D 컨브넷은 Conv1D 층을 사용해서 구현한다. Conv1D는 Conv2D와 인터페이스가 비슷하다.
    • (samples, time, features) 크기의 3D 텐서를 입력으로 받고 비슷한 형태의 3D 텐서를 반환한다.
    • 합성곱 윈도우는 시간 축의 1D 윈도우이다. 즉 입력 텐서의 두 번째 축이다.
  • 간단한 2개의 층으로 된 1D 컨브넷을 만들어 IMDB 감성 분류 문제에 적용해 보자.
from keras.datasets import imdb
from keras.preprocessing import sequence

max_features = 10000
maxlen = 500
batch_size = 32

print('데이터 로딩...')
(x_train, y_train), (x_test, y_test) = imdb.load_data(num_words=max_features)
print(len(x_train), '훈련 시퀀스')
print(len(x_test), '테스트 시퀀스')

print('시퀀스 패딩 (samples x time)')

x_train = sequence.pad_sequences(x_train, maxlen=maxlen)
x_test = sequence.pad_sequences(x_test, maxlen=maxlen)
print('input_train 크기:', x_train.shape)
print('input_test 크기:', x_test.shape)
  • 1D 컨브넷은 5장에서 사용한 2D 컨브넷과 비슷한 방식으로 구성한다.
    • Conv1D와 MaxPooling1D 층을 쌓고 전역 풀링 층이나 Flatten 층으로 마친다.
    • 이 구조는 3D 입력을 2D 출력으로 바꾸므로 분류나 회귀를 위해 모델에 하나 이상의 Dense 층을 추가할 수 있다.
  • 한 가지 다른 점은 1D 컨브넷에 큰 합성곱 윈도우를 사용할 수 있다는 것이다.
    • 2D 합성곱 층에서 3 x 3 합성곱 윈도우는 3 x 3 = 9 특성을 고려한다.
    • 1D 합성곱 층에서 크기 3인 합성곱 윈도우는 3개의 특성만 고려한다. 그래서 1D 합성곱에 크기 7이나 9의 윈도우를 사용할 수 있다.
  • 다음은 IMDB 데이터셋을 위한 1D 컨브넷의 예이다.
from keras.models import Sequential
from keras import layers
from keras.optimizers import RMSprop

model = Sequential()
model.add(layers.Embedding(max_features, 128, input_length=maxlen))
model.add(layers.Conv1D(32, 7, activation='relu'))
model.add(layers.MaxPooling1D(5))
model.add(layers.Conv1D(32, 7, activation='relu'))
model.add(layers.GlobalMaxPooling1D())
model.add(layers.Dense(1))
model.summary()

model.compile(optimizer=RMSprop(lr=1e-4), loss='binary_crossentropy', metrics=['acc'])

history = model.fit(x_train, y_train, epochs=10, batch_size=128, validation_split=0.2)
  • 결과는 다음과 같다.

  • 검증 정확도는 LSTM 보다 조금 낮지만 CPU나 GPU에서 더 빠르게 실행된다. 여기에서 적절한 에포크 수(4개)로 모델을 다시 훈련하고 테스트 세트에서 확인할 수 있다.
    • 이 예는 단어 수준의 감성 분류 작업에 순환 네트워크를 대신하여 빠르고 경제적인 1D 컨브넷을 사용할 수 있음을 보여준다.

CNN과 RNN을 연결하여 긴 시퀀스를 처리하기

  • 1D 컨브넷이 입력 패치를 독립적으로 처리하기 때문에 RNN과 달리 (합성곱 윈도우 크기의 범위를 넘어서는) 타임스텝의 순서에 민감하지 않다.
    • 물론 장기간 패턴을 인식하기 위해 많은 합성곱 층과 풀링 층을 쌓을 수 있다. 상위 층은 원본 입력에서 긴 범위를 보게 될 것이다. 이런 방법은 순서를 감지하기에 부족하다.
    • 온도 예측 문제에 1D 컨브넷을 적용해서 이를 확인해 보자. 이 문제는 순서를 감지해야 좋은 예측을 만들어 낼 수 있다.
from keras.models import Sequential
from keras import layers
from keras.optimizers import RMSprop

model = Sequential()
model.add(layers.Conv1D(32, 5, activation='relu', input_shape=(None, float_data.shape[-1])))
model.add(layers.MaxPooling1D(3))
model.add(layers.Conv1D(32, 5, activation='relu'))
model.add(layers.MaxPooling1D(3))
model.add(layers.Conv1D(32, 5, activation='relu'))
model.add(layers.GlobalMaxPooling1D())
model.add(layers.Dense(1))

model.compile(optimizer=RMSprop(), loss='mae')

history = model.fit_generator(train_gen, steps_per_epoch=500, epochs=20, validation_data=val_gen, validation_steps=val_steps)
  • 다음은 훈련 MAE와 검증 MAE를 보여준다.

  • 검증 MAE는 0.4 대에 머물러 있다. 작은 컨브넷을 사용해서 상식 수준의 기준점을 넘지 못했다. 이는 컨브넷이 입력 시계열에 있는 패턴을 보고 이 패턴의 식나 축의 위치를 고려하지 않기 때문이다.
    • 최근 데이터 포인트일수록 오래된 데이터 포인트와는 다르게 해석해야 하기 때문에 컨브넷이 의미 있는 결과를 만들지 못한다.
    • 이런 컨브넷의 한계는 IMDB 데이터에서는 문제가 되지 않는데, 긍정 또는 부정적인 감성과 연관된 키워드 패턴의 중요성은 입력 시퀀스에 나타난 위치와 무관하기 때문이다.
  • 컨브넷의 속도와 경량함을 RNN의 순서 감지 능력과 결합하는 한 가지 전략은 1D 컨브넷을 RNN 이전에 전처리 단계로 사용하는 것이다.
    • 수천 개의 스텝을 가진 시퀀스 같이 RNN으로 처리하기에는 현실적으로 너무 긴 시퀀스를 다룰 때 특별히 도움이 된다.
    • 컨브넷이 긴 입력 시퀀스를 더 짧은 고수준 특성의 (다운 샘플된) 시퀀스로 변환한다.
    • 추출된 특성의 시퀀스는 RNN 파트의 입력이 된다.

  • 이 기법이 연구 논문이나 실전 애플리케이션에 자주 등장하지는 않는데 아마도 널리 알려지지 않았기 때문일 것이다. 이 방법은 효과적이므로 많이 사용되기를 바란다.
  • 온도 에측 문제에 적용해 보자. 이 전략은 훨씬 긴 시퀀스를 다룰 수 있으므로 더 오래 전 데이터를 바라보거나 시게열 데이터를 더 촘촘히 바라볼 수 있다.
    • 여기서는 그냥 step을 절반으로 줄여서 사용하겠다. 온도 데이터가 30분마다 1포인트씩 샘플링되기 때문에 결과 시계열 데이터는 2배로 길어진다.
step = 3
lookback = 1440
delay = 144

train_gen = generator(float_data, lookback=lookback, delay=delay, min_index=0, max_index=200000, shuffle=True, step=step, batch_size=batch_size)
val_gen = generator(float_data, lookback=lookback, delay=delay, min_index=200001, max_index=300000, shuffle=True, step=step, batch_size=batch_size)
test_gen = generator(float_data, lookback=lookback, delay=delay, min_index=300001, max_index=None, shuffle=True, step=step, batch_size=batch_size)


val_steps = (300000 - 200001 - lookback) // 128
test_steps = (len(float_data) - 300001 - lookback) // 128
  • 이 모델은 2개의 Conv1D 층 다음에 GRU 층을 놓았다.
model = Sequential()
model.add(layers.Conv1D(32, 5, activation='relu', input_shape=(None, float_data.shape[-1])))
model.add(layers.MaxPooling1D(3))
model.add(layers.Conv1D(32, 5, activation='relu'))
model.add(layers.GRU(32, dropout=0.1, recurrent_dropout=0.5))
model.add(layers.Dense(1))

model.summary()

model.compile(optimizer=RMSprop(), loss='mae')

history = model.fit_generator(train_gen, steps_per_epoch=500, epochs=20, validation_data=val_gen, validation_steps=val_steps)
  • 결과는 다음과 같다.

  • 검증 손실로 비교해 보면 이 설정은 규제가 있는 GRU 모델 만큼 좋지는 않다. 하지만 훨씬 빠르기 때문에 데이터를 2배 더 많이 처리할 수 있다. 
    • 여기서는 도움이 많이 되지 않았지만 다른 데이터셋에서는 중요할 수 있다.

정리

  • 2D 컨브넷이 2D 공간의 시각적 패턴을 잘 처리하는 것처럼 1D 컨브넷은 시간에 따른 패턴을 잘 처리한다. 1D 컨브넷은 특정 자연어 처리 같은 일부 문제에 RNN을 대신할 수 있는 빠른 모델이다.
  • 전형적으로 1D 컨브넷은 컴퓨터 비전 분야의 2D 컨브넷과 비슷하게 구성한다. Conv1D 층과 Max-Pooling1D 층을 쌓고 마지막에 전역 풀링 연산이나 Flatten 층을 둔다.
  • RNN으로 아주 긴 시퀀스를 처리하려면 계산 비용이 많이 든다. 1D 컨브넷은 비용이 적게 든다. 따라서 1D 컨브넷을 RNN 이전의 전처리 단계로 사용하는 것은 좋은 생각이다. 시퀀스 길이를 줄이고 RNN이 처리할 유용한 표현을 추출해 줄 것이다.

요약

  • 이 장에서는 다음 기법들을 배웠다. 이 기법들은 텍스트에서 시계열까지 다양한 시쿠너스 데이터셋에 폭넓게 적용할 수 있다.
    • 텍스트를 토큰화 하는 방법
    • 단어 임베딩과 이를 사용하는 방법
    • 순환 네트워크와 이를 사용하는 방법
    • 더 강력한 시퀀스 처리 모델을 만들기 위해 RNN 층을 스태킹 하는 방법과 양방향 RNN을 사용하는 방법
    • 시퀀스를 처리하기 위해 1D 컨브넷을 사용하는 방법
    • 긴 시퀀스를 처리하기 위해 1D 컨브넷과 RNN을 연결하는 방법
  • 시계열 데이터를 사용한 회귀 (미래 값을 예측), 시계열 분류, 시게열에 있는 이상치 감지, 시퀀스 레이블링(문장에서 이름이나 날짜를 식별하기 등)에 RNN을 사용할 수 있다.
  • 비슷하게 기계 번역(SliceNet 같은 시퀀스-투-시퀀스 합성곱 모델), 문서 분류, 맞춤법 정정 등에 1D 컨브넷을 사용할 수 있다.
  • 시퀀스 데이터에서 전반적인 순서가 중요하다면 순환 네트워크를 사용하여 처리하는 것이 좋다. 최근의 정보가 오래된 과거보다 더 중요한 시계열 데이터가 전형적인 경우이다.
  • 전반적인 순서가 큰 의미가 없다면 1D 컨브넷이 적어도 동일한 성능을 내면서 비용도 적을 것이다. 텍스트 데이터가 종종 이에 해당한다. 문장 처음에 있는 키워드가 마지막에 있는 키워드와 같은 의미를 가진다.

현명한 투자자

현명한 투자자

워런 버핏의 스승으로 유명한 밴저민 그레이엄의 책. 가치 투자자의 대부로 꼽히기 때문에 책의 내용은 그에 대한 내용으로 가득차 있다.

기업을 소유한다는 개념으로 투자를 하라는 것이 대전제이고, 그 전제 하에 해야 하는 여러 행동들에 대한 내용과 투자를 전문적으로 하는 사람들이 볼만한 기술적인 내용도 담겨 있다. 그 유명한 안전마진이나 자산 배분(리밸런싱)에 대한 내용도 담겨 있음 –리밸런싱은 정보 이론의 아버지라 불리는 클로드 섀넌의 ‘섀넌의 도깨비’가 최초로 알고 있음

리밸런싱이라는 개념을 떠나면 주식 투자는 결국 미래를 맞추는 것이고 –정확히는 미래에 대한 다른 사람들(시장)의 예측을 예측하는 것– 본질적으로 미래는 참여자들의 상호작용에 의해 만들어지는 것인데, 그 미래를 타이밍까지 정확히 예측한다는 것은 불가능한 것이기 때문에 –지진도 스스로 얼마나 커질지 알 수 없다– 미래를 예측하는 완전한 해법을 찾는 것은 헛된일이다. –설령 일시적으로 완전한 전략이 나와도, 그 전략을 잡아 먹는 전략이 나올 것이기 때문에 완전한 해법이란 있을 수가 없다.

결국 자신의 인생은 자신이 살아가는 것이고 –그 누구도 내 삶을 대신 살아주지는 않는다– 다른 사람의 조언은 조언으로 받아들이고 자신의 길을 가야 한다. 나와 성공한 사람의 삶의 맥락이 다른데, 그 사람이 성공한 방법이 나에게도 통할 수는 없기 때문.

케라스 창시자에게 배우는 딥러닝/ 컴퓨터 비전을 위한 딥러닝

합성곱 신경망 소개

  • 이론적 배경을 알아보기 앞서 2장에서 완전 연결 네트워크로 풀었던 MNIST 숫자 이미지 분류에 컨브넷을 사용해 보자. 기본적인 컨브넷도 완전 연결 모델의 성능을 훨씬 앞지른다.
from keras import layers
from keras import models

model = models.Sequential()
model.add(layers.Conv2D(32, (3,3), activation='relu', input_shape=(28, 28, 1)))
model.add(layers.MaxPooling2D((2,2)))
model.add(layers.Conv2D(64, (3,3), activation='relu'))
model.add(layers.MaxPooling2D((2,2)))
model.add(layers.Conv2D(64, (3,3), activation='relu'))
  • 컨브넷이 (image_height, image_width, image_channels) 크기의 입력 텐서를 사용한다는 점이 중요하다 (배치 차원은 포함하지 않는다.)
    • 이 예제에서는 MNIST 이미지 포맷인 (28, 28, 1) 크기의 입력을 처리하도록 컨브넷을 설정해야 한다. 이 때문에 첫 번째 층의 매개변수로 input_shape(28, 28, 1)을 전달했다.
  • 컨브넷의 구조를 출력해 보자.
Layer (type)                 Output Shape              Param #   
=================================================================
conv2d_1 (Conv2D)            (None, 26, 26, 32)        320       
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 13, 13, 32)        0         
_________________________________________________________________
conv2d_2 (Conv2D)            (None, 11, 11, 64)        18496     
_________________________________________________________________
max_pooling2d_2 (MaxPooling2 (None, 5, 5, 64)          0         
_________________________________________________________________
conv2d_3 (Conv2D)            (None, 3, 3, 64)          36928     
=================================================================
Total params: 55,744
Trainable params: 55,744
Non-trainable params: 0
  • Conv2D와 MaxPooling2D 층의 출력은 (height, width, channels) 크기의 3D 텐서이다. 높이와 너비 차원은 네트워크가 깊어질수록 작아지는 경향이 있다. 채널의 수는 Conv2D 층에 전달된 첫 번째 매개변수에 의해 조절된다 (32개 또는 64개)
  • 다음 단계에서 마지막 층의 ((3, 3, 64) 크기인) 출력 텐서를 완전 연결 네트워크에 주입한다. 이 네트워크는 이미 익숙하게 보았던 Dense 층을 쌓은 분류기이다. 이 분류기는 1D 벡터를 처리하는데, 이전 층의 출력이 3D 텐서이다. 그래서 3D 출력을 1D 텐서로 펼쳐야 한다. 그 다음 몇 개의 Dense 층을 추가한다.
model.add(layers.Flatten())
model.add(layers.Dense(64, activation='relu'))
model.add(layers.Dense(10, activation='softmax'))
  • 10개의 클래스를 분류하기 위해 마지막 층의 출력 크기를 10으로 하고 소프트맥스 활성화 함수를 사용한다. 전체 네트워크는 다음과 같다.
Layer (type)                 Output Shape              Param #   
=================================================================
conv2d_1 (Conv2D)            (None, 26, 26, 32)        320       
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 13, 13, 32)        0         
_________________________________________________________________
conv2d_2 (Conv2D)            (None, 11, 11, 64)        18496     
_________________________________________________________________
max_pooling2d_2 (MaxPooling2 (None, 5, 5, 64)          0         
_________________________________________________________________
conv2d_3 (Conv2D)            (None, 3, 3, 64)          36928     
_________________________________________________________________
flatten_1 (Flatten)          (None, 576)               0         
_________________________________________________________________
dense_1 (Dense)              (None, 64)                36928     
_________________________________________________________________
dense_2 (Dense)              (None, 10)                650       
=================================================================
Total params: 93,322
Trainable params: 93,322
Non-trainable params: 0
  • 여기에서 볼 수 있듯이 (3, 3, 64) 출력이 (576,) 크기의 벡터로 펼쳐진 후 Dense 층으로 주입되었다.
  • 이제 MNIST 숫자 이미지에 이 컨브넷을 훈련하자.
from keras.datasets import mnist
from keras.utils import to_categorical

(train_images, train_labels), (test_images, test_labels) = mnist.load_data()

train_images = train_images.reshape((60000, 28, 28, 1))
train_images = train_images.astype('float32') / 255

test_images = test_images.reshape((10000, 28, 28, 1))
test_images = test_images.astype('float32') / 255

train_labels = to_categorical(train_labels)
test_labels = to_categorical(test_labels)

model.compile(optimizer='rmsprop', loss='categorical_crossentropy', metrics=['accuracy'])
model.fit(train_images, train_labels, epochs=5, batch_size=64)
  • 테스트 데이터에서 모델을 평가해 보자.
test_loss, test_acc = model.evaluate(test_images, test_labels)
test_acc
---
0.9927999973297119
  • 2장의 완전 연결 네트워크는 97.8%의 테스트 정확도를 얻은 반면, 기본적인 컨브넷은 99.2%의 테스트 정확도를 얻었다.
  • 완전 연결된 모델보다 왜 간단한 컨브넷이 더 잘 작동할까? 이에 대해 알아보기 위해 Conv2D와 MaxPooling2D 층이 어떤 일을 하는지 살펴보자.

합성곱 연산

  • 완전 연결 층과 합성곱 층 사이의 근본적인 차이는 다음과 같다.
    • Dense 층은 입력 특성 공간에 있는 전역 패턴(예컨대 MNIST 숫자 이미지에서는 모든 픽셀에 걸친 패턴)을 학습하지만 합성곱 층은 지역 패턴을 학습한다.
    • 이미지일 경우 작은 2D 윈도우로 입력에서 패턴을 찾는다. 앞선 예에서 윈도우는 모두 3×3 크기였다.

  • 이 핵심 특징은 컨브넷에 두 가지 흥미로운 성질을 제공한다.
    • 학습된 패턴은 평행 이동 불변성(translation invariant)을 갖는다. 
      • 컨브넷이 이미지의 오른쪽 아래 모서리에서 어떤 패턴을 학습했다면 다른 곳에서도 이 패턴을 학습할 수 있다. 완전 연결 네트워크는 새로운 위치에 나타난 것을 새로운 패턴으로 학습해야 한다.
      • 이런 성질은 컨브넷이 이미지를 효율적으로 처리하게 만들어진다. (근본적으로 우리가 보는 세상은 평행 이동으로 인해 다르게 인식되지 않는다) 적은 수의 훈련 샘플을 사용해서 일반화 능력을 가진 표현을 학습할 수 있다.
      • (회전은 안 되는 것 같다)
    • 컨브넷은 패턴의 공간적 계층 구조를 학습할 수 있다.
      • 첫 번째 합성곱 층이 에지 같은 작은 지역 패턴을 학습한다. 두 번째 합성곱 층은 첫 번째 층의 특성으로 구성되는 더 큰 패턴을 학습하는 식이다.
      • 이런 방식을 이용하여 컨브넷은 매우 복잡하고 추상적인 시각적 개념을 효과적으로 학습할 수 있다. (근본적으로 우리가 보는 세상은 공간적 계층 구조를 가진다)
  • 합성곱 연산은 특성 맵(feature map)이라고 부르는 3D 텐서에 적용된다. 이 텐서는 2개의 공간축(높이와 너비)과 깊이 축(채널 축이라고도 한다)으로 구성된다.
    • RGB 이미지는 3개의 컬러 채널을 가지므로 깊이 축의 차원이 3이 되고, MNIST 숫자처럼 흑백이미지는 깊이 축의 차원이 1이다.
    • 합성곱 연산은 입력 특성 맵에서 작은 패치들을 추출하고 이런 모든 패치에 같은 변환을 적용하여 출력 특성 맵(output feature amp)을 만든다.

  • 출력 특성 맵도 높이와 너비를 가진 3D 텐서이다. 출력 텐서의 깊이는 층의 매개변수로 결정되기 때문에 상황에 따라 다르다.
    • 이렇게 되면 깊이 축의 채널은 더 이상 RGB 입력처럼 특정 컬러를 의미하지 않는다. 그 대신 일종의 필터(filter)를 의미한다.
    • 필터는 입력 데이터의 어떤 특성을 인코딩한다. 예컨대 고수준으로 보면 하나의 필터가 ‘입력에 얼굴이 있는지’를 인코딩 할 수 있다.
  • MNIST 예제에서 첫 번째 합성곱 층이 (28, 28, 1) 크기의 특성 맵을 입력으로 받아 (26, 26, 32) 크기의 특성 맵을 출력한다. 즉 입력에 대해 32개의 필터를 적용한다.
    • 32개의 출력 채널 각각은 26×26 크기의 배열 값을 가진다. 이 값은 입력에 대한 필터의 응답 맵(response map)이다. 입력의 각 위치에서 필터 패턴에 대한 응답을 나타낸다.
    • 특성 맵이란 말이 의미하는 것은 다음과 같다. 깊이 축에 있는 각 차원은 하나의 특성(또는 필터)이고, 2D 텐서 output[:, :, n]은 입력에 대한 이 필터 응답을 나타내는 2D 공간상의 맵이다.

  • 합성곱은 핵심적인 2개의 파라미터로 정의된다.
    • 입력으로부터 뽑아낼 패치의 크기: 전형적으로 3×3 또는 5×5 크기를 사용한다. 이 예에서는 3×3을 사용했다.
    • 특성 맵의 출력 깊이: 합성곱으로 계산할 필터의 수이다. 이 예에서는 깊이 32로 시작해서 깊이 64로 끝났다.
  • 케라스의 Conv2D 층에서 이 파라미터는 Conv2D(output_depth, (window_height, window_width)) 처럼 첫 번째와 두 번째 매개변수로 전달된다.
  • 3D 입력 특성 맵 위를 3×3 또는 5×5 크기의 윈도우가 슬라이딩(sliding)하면서 모든 위치에서 3D 특성 패치(window_height, window_width, input_depth) 크기)를 추출하는 방식으로 합성곱이 작동한다.
    • 이런 3D 패치는 (output_depth, ) 크기의 1D 벡터로 변환된다. (합성곱 커널 (convolution kernel)이라고 불리는 하나의 학습된 가중치 행렬과의 텐서 곱셈을 통하여 변환된다.)
    • 변환된 모든 벡터는 (height, width, output_depth) 크기의 3D 특성 맵으로 재구성된다. 
    • 출력 특성 맵의 공간상 위치는 입력 특성 맵의 같은 위치에 대응된다. (예컨대 출력의 오른쪽 아래 모서리는 입력의 오른쪽 아래 부근에 해당하는 정보를 담고 있다)
    • 3×3 윈도우를 사용하면 3D 패치 input[i-1:i+2, j-1:j+2, :]로부터 벡터 output[i, j, :]가 만들어진다.
    • 아래 그림에 전체 과정이 표현되어 있다.
      • (입력의 깊이는 2차원인데, 출력의 깊이는 3차원이다)

  • 출력 높이와 너비는 입력의 높이, 너비와 다를 수 있는데, 여기에는 두 가지 이유가 있다.
    • 경계 문제, 입력 특성 맵에 패딩을 추가하여 대응할 수 있다.
    • 잠시 후에 설명할 스트라이드(stride)의 사용 여부에 따라 다르다.

경계 문제와 패딩 이해하기

  • 5×5 크기의 특성 맵을 생각해 보자. (총 25개의 타일이 있다고 가정)
    • 3×3 크기인 윈도우의 중앙을 맞출 수 있는 타일은 3×3 격차를 형성하는 9개 뿐이다. 따라서 출력 특성 맵은 3×3 크기가 된다.
    • 크기가 조금 줄어 들었다. 여기에서 높이와 너비 차원을 따라 정확히 2개의 타일이 줄어들었다.
    • 앞선 예에서도 이런 경계 문제를 볼 수 있는데, 첫 번째 합성곱 층에서 28×28 크기의 입력이 26×26 크기가 되었다.

  • 입력과 동일한 높이와 너비를 가진 출력 특성 맵을 얻고 싶다면 패딩(padding)을 사용할 수 있다.
    • 패딩은 입력 특성 맵의 가장자리에 적절한 개수의 행과 열을 추가한다. 그래서 모든 입력 타일에 합성곱 윈도우의 중앙을 위치시킬 수 있다.
    • 3×3 윈도우라면 위아래에 하나의 행을 추가하고 오른쪽, 왼쪽에 하나의 열을 추가한다. 5×5 윈도우라면 2개의 행과 열을 추가한다.

  • Conv2D 층에서 패딩은 padding 매개변수로 설정할 수 있다. 2개의 값이 가능한데, “valid”는 패딩을 사용하지 않는다는 뜻이고, “same”은 입력과 동일한 높이와 너비를 가진 출력을 만들기 위해 패딩한다는 뜻이다. 기본값은 “valid”이다.

합성곱 스트라이드 이해하기

  • 출력 크기에 영향을 미치는 다른 요소는 스트라이드이다.
    • 지금까지 합성곱에 대한 설명은 합성곱 윈도우의 중앙 타일이 연속적으로 지나간다고 가정한 것이다.
    • 두 번의 연속적인 윈도우 사이의 거리가 스트라이드라고 불리는 합성곱의 파라미터이다. 스트라이드의 기본값은 1이다.
    • 스트라이드가 1보다 큰 스트라이드 합성곱도 가능하다. 아래의 그림은 5×5 크기의 입력에 스트라이드 2를 사용한 3×3 크기의 윈도우로 합성곱하여 추출한 패치를 나타낸다.

  • 스트라이드를 2를 사용했다는 것은 특성 맵의 너비와 높이가 2의 배수로 다운샘플링되었다는 뜻이다. (경계 문제가 있다면 더 줄어든다)
    • 스트라이드 합성곱은 실전에서는 드물게 사용된다. 하지만 어떤 모델에서는 유용하게 사용될 수 있으므로 잘 알아둘 필요가 있다.
  • 특성 맵을 다운샘플링하기 위해 스트라이드 대신 첫 번째 컨브넷 예제에 사용된 최대 풀링(max pooling) 연산을 사용하는 경우가 많으므로 알아보자.

최대 풀링 연산

  • 앞선 컨브넷 예제에서 특성 맵의 크기가 MaxPooling2D 층마다 절반으로 줄어들었다.
    • 예컨대 MaxPooling2D 층 이전에 특성 맵의 크기는 26×26이었는데, 최대 풀링 연산으로 13×13으로 줄어들었다.
    • 스트라이드 합성곱과 매우 비슷하게 강제적으로 특성 맵을 다운샘플링하는 것이 최대 풀링의 역할이다.
  • 최대 풀링은 입력 특성 맵에서 윈도우에 맞는 패치를 추출하고 각 채널별로 최댓값을 출력한다.
    • 합성곱과 개념적으로 비슷하지만 추출한 패치에 학습된 선형 변환(합성곱 커널)을 적용하는 대신 하드코딩된 최댓값 추출 연산을 사용한다.
    • 합성곱과 가장 큰 차이점은 최대 풀링은 보통 2×2 윈도우와 스트라이드 2를 사용하여 특성 맵을 절반 크기로 다운샘플링한다는 것이다.
    • 이에 반해 합성곱은 전형적으로 3×3 윈도우와 스트라이드 1을 사용한다.
  • 왜 이런 식으로 특성 맵을 다운샘플링할까? 왜 최대 풀링 층을 빼고 큰 특성 맵을 계속 유지하지 않을까? 이런 방식을 테스트해 보자. 합성곱만으로 이루어진 모델은 다음과 같다.
model_no_max_pool = models.Sequential()
model_no_max_pool.add(layers.Conv2D(32, (3, 3), activation='relu', input_shape=(28, 28, 1)))
model_no_max_pool.add(layers.Conv2D(64, (3, 3), activation='relu'))
model_no_max_pool.add(layers.Conv2D(64, (3, 3), activation='relu'))
  • 모델의 구조는 다음과 같다.
Layer (type)                 Output Shape              Param #   
=================================================================
conv2d_4 (Conv2D)            (None, 26, 26, 32)        320       
_________________________________________________________________
conv2d_5 (Conv2D)            (None, 24, 24, 64)        18496     
_________________________________________________________________
conv2d_6 (Conv2D)            (None, 22, 22, 64)        36928     
=================================================================
Total params: 55,744
Trainable params: 55,744
Non-trainable params: 0
  • 이 설정에서 무엇이 문제일까? 두 가지가 있다.
    • 특성의 공간적 계층 구조를 학습하는데 도움이 되지 않는다. 세 번째 층의 3×3 윈도우는 초기 입력의 7×7 윈도우 영역에 대한 정보만 담고 있다. 컨브넷에 의해 학습된 고수준 패턴은 초기 입력에 관한 정보가 아주 적어 숫자 분류를 학습하기에 충분하지 않을 것이다(7×7 픽셀 크기의 창으로 숫자를 보고 분류해 보라) 마지막 학성곱 층의 특성이 전체 입력에 대한 정보를 가지고 있어야 한다.
    • 최종 특성 맵은 22 x 22 x 64 = 30,976개의 가중치를 가지는데, 아주 많다. 이 컨브넷을 펼친 후 512 크기의 Dense 층과 연결한다면 약 15.8백만 개의 가중치 파라미터가 생긴다. 작은 모델치고 너무 많은 가중치고, 심각한 과대적합이 발생할 것이다.
  • 간단히 말해서 다운샘플링을 사용하는 이유는 처리할 특성 맵의 가중치 개수를 줄이기 위해서다. 또 연속적인 합성곱 층이 (원본 입력에서 커버된느 영역 측면에서) 점점 커진 윈도우를 통해 바라보도록 만들어 필터의 공간적인 계층 구조를 구성한다.
  • 최대 풀링이 다운샘플링을 할 수 있는 유일한 방법은 아니다. 이미 알고 있듯 앞선 합성곱 층에서 스트라이드를 사용할 수 있다.
    • 최댓값을 취하는 최대 풀링 대신 입력 패치의 채널별 평균값을 계산하여 변환하는 평균 풀링(average pooling)을 사용할 수도 있다.
    • 하지만 최대 풀링이 다른 방법들보다 더 잘 작동하는 편이다. 그 이유는 특성이 특성 맵의 각 타일에서 어떤 패턴이나 개념의 존재 여부를 인코딩하는 경향이 있기 때문이다. (그래서 특성의 지도이다)
    • 따라서 특성의 평균값보다 여러 특성 중 최댓값을 사용하는 것이 더 유용하다. 가장 납득할 만한 서브샘플링(subsampling) 전략은 먼저 (스트라이드가 없는 합성곱으로) 조밀한 특성 맵을 만들고 그 다음 작은 패치에 대해 최대로 활성화된 특성을 고르는 것이다.
    • 이런 방법이 입력에 대해 (스트라이드 합성곱으로) 듬성듬성 윈도우를 슬라이드하거나 입력 패치를 평균해서 특성 정보를 놓치거나 희석시키는 것보다 낫다.

소규모 데이터셋에서 밑바닥부터 컨브넷 훈련하기

  • 적은 데이터를 이용하여 이미지 분류 모델을 훈련하는 일은 흔한 경우이다. 보통 적은 샘플이란 수백 개에서 수만 개 사이를 의미한다.
    • 실용적인 예제로 4,000개의 강아지와 고양이 사진으로 구성된 데이터셋에서 강아지와 고양이 이미지를 분류해 보자. 2,000개의 사진을 훈련 데이터로, 검증과 테스트 데이터로 1,000개씩 사용하겠다.
  • 문제를 해결하기 위해 기본적인 전략 하나를 살펴보자. 보유한 소규모 데이터셋을 사용하여 처음부터 새로운 모델을 훈련하는 것이다.
    • 2,000개의 훈련 샘플에서 작은 컨브넷을 어떤 규제 방법도 사용하지 않고 훈련하여 기준이 되는 기본 성능을 만들겠다. 이 방법은 71%의 분류 정확도를 달성할 것이다. 이 방법의 주요 이슈는 과대적합이 될 것이다.
    • 그 다음 컴퓨터 비전에서 과대적합을 줄이기 위한 강력한 방법인 데이터 증식(data augmentation)을 소개하겠다. 데이터 증식을 통해 네트워크의 성능을 82%로 향상시킬 수 있다.
  • 다음 절에서 작은 데이터셋에 딥러닝을 적용하기 위한 핵심적인 기술 두 가지를 살펴보겠다.
    • 사전 훈련된 네트워크로 특성을 추출하는 것(90%의 정확도를 얻게 된다)과 사전 훈련된 네트워크를 세밀하게 튜닝하는 것(최종 모델은 92% 정확도를 얻게 될 것이다). 
    • 이 세 가지 전략 (처음부터 작은 모델을 훈련하기, 사전 훈련도니 모델을 사용하여 특성 추출하기, 사전 훈련된 모델을 세밀하게 튜닝하기)은 작은 데이터셋에서 이미지 분류 문제를 수행할 때 여러분의 도구 상자에 포함되어 있어야 한다.

작은 데이터셋 문제에서 딥러닝의 타당성

  • 딥러닝은 데이터가 풍부할 때만 작동한다는 말을 듣는데 부분적으로만 맞는 이야기다.
    • 딥러닝의 근본적인 특징은 훈련 데이터에서 특성 공학의 수작업 없이 흥미로운 특성을 찾을 수 있는 것이다. 이는 훈련 샘플이 많아야만 가능하다. 입력 샘플이 이미지처럼 매우 고차원인 문제에서는 특히 그렇다.
  • 하지만 많은 샘플이 의미하는 것은 상대적이다. 우선 훈련하려는 네트워크의 크기와 깊이에 상대적이다. 
    • 복잡한 문제를 푸는 컨브넷을 수십 개의 샘플만 사용해서 훈련하는 것은 불가능하다. 하지만 모델이 작고 규제가 잘 되어 있으며 간단한 작업이라면 수백 개의 샘플로도 충분할 수 있다.
    • 컨브넷은 지역적이고 평행 이동으로 변하지 않는 특성을 학습하기 때문에 지각에 관한 문제에서 매우 효율적으로 데이터를 사용한다.
    • 매우 작은 데이터셋에서 어떤 종류의 특성 공학을 사용하지 않고 컨브넷을 처음부터 훈련해도 납득할만한 결과를 만들 수 있다.
  • 거기에 더해 딥러닝 모델은 태생적으로 매우 다목적이다. 말하자면 대규모 데이터셋에서 훈련시킨 이미지 분류 모델이나 스피치-투-텍스트(speech-to-text) 모델을 조금만 변경해서 완전히 다른 문제에 재사용할 수 있다.
    • 특히 컴퓨터 비전에서는 (보통 ImageNet 데이터셋에서 훈련 된) 사전 훈련된 모델들이 내려받을 수 있도록 많이 공개되어 있어서 매우 적은 데이터에서 강력한 비전 모델을 만드는데 사용할 수 있다.

데이터 내려받기

  • 여기서 사용할 강아지, 고양이 데이터셋은 케라스에 포함되어 있지 않고 캐글에 있기 때문에 캐글 계정이 필요하다. 다음 주소에서 데이터를 내려 받자.

  • 내려 받고 압축 해제 한 원본 데이터 파일을 다음 코드를 이용해서 분리한다.
import os, shutil

original_dataset_dir = './datasets/cats_and_dogs/train/' #원본 데이터셋 경로
base_dir = '.datasets/cats_and_dogs/cats_and_dogs_small/'
os.mkdir(base_dir)

train_dir = os.path.join(base_dir, 'train')
validation_dir = os.path.join(base_dir, 'validation')
test_dir = os.path.join(base_dir, 'test')

os.mkdir(train_dir)
os.mkdir(validation_dir)
os.mkdir(test_dir)

train_cats_dir = os.path.join(train_dir, 'cats')
train_dogs_dir = os.path.join(train_dir, 'dogs')

validation_cats_dir = os.path.join(validation_dir, 'cats')
validation_dogs_dir = os.path.join(validation_dir, 'dogs')

test_cats_dir = os.path.join(test_dir, 'cats')
test_dogs_dir = os.path.join(test_dir, 'dogs')

os.mkdir(train_cats_dir)
os.mkdir(train_dogs_dir)
os.mkdir(validation_cats_dir)
os.mkdir(validation_dogs_dir)
os.mkdir(test_cats_dir)
os.mkdir(test_dogs_dir)

for fname in ['cat.{}.jpg'.format(i) for i in range(1000)]:
src = os.path.join(original_dataset_dir, fname)
dst = os.path.join(train_cats_dir, fname)
shutil.copyfile(src, dst)

for fname in ['cat.{}.jpg'.format(i) for i in range(1000, 1500)]:
src = os.path.join(original_dataset_dir, fname)
dst = os.path.join(validation_cats_dir, fname)
shutil.copyfile(src, dst)

for fname in ['cat.{}.jpg'.format(i) for i in range(1500, 2000)]:
src = os.path.join(original_dataset_dir, fname)
dst = os.path.join(test_cats_dir, fname)
shutil.copyfile(src, dst)

for fname in ['dog.{}.jpg'.format(i) for i in range(1000)]:
src = os.path.join(original_dataset_dir, fname)
dst = os.path.join(train_dogs_dir, fname)
shutil.copyfile(src, dst)

for fname in ['dog.{}.jpg'.format(i) for i in range(1000, 1500)]:
src = os.path.join(original_dataset_dir, fname)
dst = os.path.join(validation_dogs_dir, fname)
shutil.copyfile(src, dst)

for fname in ['dog.{}.jpg'.format(i) for i in range(1500, 2000)]:
src = os.path.join(original_dataset_dir, fname)
dst = os.path.join(test_dogs_dir, fname)
shutil.copyfile(src, dst)

네트워크 구성하기

  • 이전보다 이미지가 크고 복잡하기 때문에 네트워크를 좀 더 만들겠다. Conv2D + MaxPooling2D 단계를 하나 더 추가한다.
    • 이렇게 하면 네트워크의 용량을 늘리고 Flatten 층의 크기가 너무 커지지 않도록 특성 맵의 크기를 줄일 수 있다.
    • 150×150 크기의 입력으로 싲가해서 Flatten 층 이전에 7×7 크기의 특성 맵으로 줄어든다.
  • 이진 분류 문제이므로 네트워크는 하나의 유닛(크기가 1인 Dense 층)과 sigmoid 활성화 함수로 끝난다. 이 유닛은 한 클래스에 대한 확률을 인코딩할 것이다.
from keras import layers
from keras import models

model = models.Sequential()
model.add(layers.Conv2D(32, (3, 3), activation='relu', input_shape=(150, 150, 3)))
model.add(layers.MaxPooling2D((2,2)))
model.add(layers.Conv2D(64, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2,2)))
model.add(layers.Conv2D(128, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2,2)))
model.add(layers.Conv2D(128, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2,2)))
model.add(layers.Flatten())
model.add(layers.Dense(512, activation='relu'))
model.add(layers.Dense(1, activation='sigmoid'))
  • 층들을 거치면서 특성 맵의 차원이 어떻게 변하는지 살펴보자
Layer (type)                 Output Shape              Param #   
=================================================================
conv2d_5 (Conv2D)            (None, 148, 148, 32)      896       
_________________________________________________________________
max_pooling2d_5 (MaxPooling2 (None, 74, 74, 32)        0         
_________________________________________________________________
conv2d_6 (Conv2D)            (None, 72, 72, 64)        18496     
_________________________________________________________________
max_pooling2d_6 (MaxPooling2 (None, 36, 36, 64)        0         
_________________________________________________________________
conv2d_7 (Conv2D)            (None, 34, 34, 128)       73856     
_________________________________________________________________
max_pooling2d_7 (MaxPooling2 (None, 17, 17, 128)       0         
_________________________________________________________________
conv2d_8 (Conv2D)            (None, 15, 15, 128)       147584    
_________________________________________________________________
max_pooling2d_8 (MaxPooling2 (None, 7, 7, 128)         0         
_________________________________________________________________
flatten_2 (Flatten)          (None, 6272)              0         
_________________________________________________________________
dense_2 (Dense)              (None, 512)               3211776   
_________________________________________________________________
dense_3 (Dense)              (None, 1)                 513       
=================================================================
Total params: 3,453,121
Trainable params: 3,453,121
Non-trainable params: 0
  • 컴파일 단계에서 이전과 같이 RMSprop 옵티마이저를 선택하겠다. 네트워크의 마지막이 하나의 시그모이드 유닛이기 때문에 이진 크로스엔트로피를 손실로 사용한다.
from keras import optimizers

model.compile(loss='binary_crossentropy', optimizer=optimizers.RMSprop(lr=1e-4), metrics=['acc'])

데이터 전처리

  • 데이터는 네트워크에 주입되기 전에 부동 소수 타입의 텐서로 전처리 되어 있어야 한다. 지금은 데이터가 JPEG 파일로 되어 있으므로 네트워크에 주입하려면 다음 과정을 따라야 한다.
    1. 사진 파일을 읽는다.
    2. JPEG 콘텐츠를 RGB 픽셀 값으로 디코딩한다.
    3. 그 다음 부동 소수 타입의 텐서로 변환한다.
    4. 픽셀 값(0-255)의 스케일을 [0, 1] 사이로 조정한다.
  • 케라스에는 이런 단계를 자동으로 처리하는 유틸리티가 있다. 또 케라스에는 keras.preprocessing.image에 이미지 처리를 위한 헬퍼 도구들도 있다. 특히 ImageDataGenerator는 디스크에 있는 이미지 파일을 전처리된 배치 텐서로 자동으로 바꾸어 주는 파이썬 제너레이터를 만들어 준다.
from keras.preprocessing.image import ImageDataGenerator

train_datagen = ImageDataGenerator(rescale=1./255)
test_datagen = ImageDataGenerator(rescale=1./255)

train_generator = train_datagen.flow_from_directory(train_dir, target_size=(150,150), batch_size=20, class_mode='binary')
validation_generator = test_datagen.flow_from_directory(validation_dir, target_size=(150,150), batch_size=20, class_mode='binary')
  • 이 제너레이터의 출력은 150×150 RGB 이미지의 배치((20, 150, 150, 3) 크기)와 이진 레이블의 배치 ((20, ) 크기)이다. 각 배치에는 20개의 샘플(배치 크기)이 있다. 
    • 제너레이터는 이 배치를 무한정 만들어낸다. 타깃 폴더에 있는 이미지를 끝없이 반복하기 때문. 따라서 반복 루프 안 어디에선가 break 문을 사용해야 한다.
  • 제너레이터를 사용한 데이터에 모델을 훈련시켜 보겠다.
    • fit_generator 메서는 fit 메서드와 동일하되 데이터 제너레이터를 사용할 수 있다. 이 메서드는 첫 번째 매개변수로 입력과 타깃의 배치를 끝없이 반환하는 파이썬 제너레이터를 기대한다.
    • 데이터가 끝없이 생성되기 때문에 케라스 모델에 하나의 에포크를 정의하기 위해 제너레이터로부터 얼마나 많은 샘플을 뽑을 것인지 알려주어야 한다. steps_per_epoch 매개변수에서 이를 설정한다.
    • steps_per_epoch개의 배치만큼 뽑은 후, 즉 steps_per_epoch 횟수만큼 경사 하강법 단계를 실행한 후에 훈련 프로세스는 다음 에포크로 넘어간다.
    • 여기서는 20개의 샘플이 하나의 배치이므로 2,000개의 샘플을 모두 처리할 때까지 100개의 배치를 뽑을 것이다.
  • fit_generator를 사용할 때 fit 메서드와 마찬가지로 validation_data 매개변수를 전달할 수 있다. 이 매개변수에는 데이터 제너레이터도 가능하지만 넘파이 배열의 튜플도 가능하다.
    • validation_data로 제너레이터를 전달하면 검증 데이터의 배치를 끝없이 반환하기 때문에 검증 데이터 제너레이터에서 얼마나 많은 배치를 추출하여 평가할지 validation_steps 매개변수에 지정해야 한다.
history = model.fit_generator(train_generator, steps_per_epoch=100, epochs=30, validation_data=validation_generator, validation_steps=50)
  • 훈련이 끝나면 항상 모델을 저장하는 습관을 갖자
model.save('cats_and_dogs_samll_1.h5')
  • 훈련 데이터와 검증 데이터에 대한 모델의 손실과 정확도를 그래프로 나타내보자
import matplotlib.pyplot as plt

acc = history.history['acc']
val_acc = history.history['val_acc']
loss = history.history['loss']
val_loss = history.history['val_loss']

epochs = range(1, len(acc) + 1)

plt.plot(epochs, acc, 'bo', label='Training acc')
plt.plot(epochs, val_acc, 'b', label='Validation acc')
plt.title('Training and validation accuracy')
plt.legend()

plt.figure()

plt.plot(epochs, loss, 'bo', label='Training loss')
plt.plot(epochs, val_loss, 'b', label='Validation loss')
plt.title('Training and validation loss')
plt.legend()

plt.show()

  • 이 그래프는 과대적합의 특성을 보여준다.
    • 훈련 정확도가 시간이 지남에 따라 선형적으로 증가해서 거의 100%에 도달하는 반면 검증 정확도는 70-72% 에서 멈추었다.
    • 검증 손실은 다섯 번의 에포크만에 최솟값에 다다른 후 더 진전되지 않았고 훈련 손실은 거의 0에 도달할 때까지 선형적으로 계속 감소한다.
  • 비교적 훈련 샘플의 수(2,000개)가 적기 때문에 과대적합이 가장 중요한 문제이다.
    • 드롭아웃이나 가중치 감소(L2 규제)처럼 과대적합을 감소시킬 수 있는 여러 기법들을 배웠다. 여기서는 컴퓨터 비전에 특화되어 있어서 딥러닝으로 이미지를 다룰 때 일반적으로 사용되는 새로운 방법인 데이터 증식을 시도해 보겠다.

데이터 증식 사용하기

  • 과대적합은 학습할 샘플이 너무 적어 새로운 데이터에 일반화할 수 있는 모델을 훈련시킬 수 없기 때문에 발생한다. 무한히 많은 데이터가 주어지면 데이터 분포의 모든 가능한 측면을 모델이 학습할 수 있을 것이다.
  • 데이터 증식은 기존 훈련 샘플로부터 더 많은 훈련 데이터를 생성하는 방법으로 이 방법은 그럴듯한 이미지를 생성하도록 여러 가지 랜덤한 변환을 적용하여 샘플을 늘린다.
    • 훈련할 때 모델이 정확히 같은 데이터를 두 번 만나지 않도록 하는 것이 목표이다. 모델이 데이터의 여러 측면을 학습하면 일반화에 도움이 될 것이다.
  • 케라스에서는 ImageDataGenerator가 읽은 이미지에 여러 종류의 랜덤 변환을 적용하도록 설정할 수 있다.
datagen = ImageDataGenerator(rotation_range=20, width_shift_range=0.1, height_shift_range=0.1, shear_range=0.1, zoom_range=0.1, horizontal_flip=True, fill_mode='nearest')
  • 추가적인 매개변수가 몇 가 더 있다. 이 코드를 살펴보면 다음과 같다.
    • rotation_range는 랜덤하게 사진을 회전시킬 각도 범위이다. (0-180 사이)
    • width_shift_range와 height_shift_range는 사진을 수평과 수직으로 랜덤하게 평행 이동 시킬 범위이다.
    • shear_range는 랜덤하게 전단 변환(shearing transformation)을 적용할 각도 범위이다.
    • zoom_range는 랜덤하게 사진을 확대할 범위이다.
    • horizontal_flip은 랜덤하게 이미지를 수평으로 뒤집는다. 수평 대칭을 가정할 수 있을 때 사용한다. (풍경/ 인물 사진 등)
    • fill_mode는 회전이나 가로/세로 이동으로 인해 새롭게 생성해야 할 픽셀을 채울 전략이다.
  • 증식된 이미지 샘플은 아래 그림과 같다.

  • 데이터 증식을 사용하여 새로운 네트워크를 훈련시킬 때 네트워크에 같은 입력 데이터가 두 번 주입되지 않는다. 하지만 적은 수의 원본 이미지에서 만들어졌기 때문에 여전히 입력 데이터들 사이에 상호 연관성이 크다.
    • 즉 새로운 정보를 만들어 낼 수 없고 단지 기존 정보의 재조합만 가능하다. 그렇기 때문에 완전히 과대적합을 제거하기에 충분하지 않을 수 있다.
    • 과대적합을 더 억제하기 위해 완전 연결 분류기 직전에 Dropout 층을 추가하겠다.
model = models.Sequential()
model.add(layers.Conv2D(32, (3, 3), activation='relu', input_shape=(150, 150, 3)))
model.add(layers.MaxPooling2D((2,2)))
model.add(layers.Conv2D(64, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2,2)))
model.add(layers.Conv2D(128, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2,2)))
model.add(layers.Conv2D(128, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2,2)))
model.add(layers.Flatten())
model.add(layers.Dropout(0.5))
model.add(layers.Dense(512, activation='relu'))
model.add(layers.Dense(1, activation='sigmoid'))

model.compile(loss='binary_crossentropy', optimizer=optimizers.RMSprop(lr=1e-4), metrics=['acc'])
  • 데이터 증식과 드롭아웃을 사용하여 이 네트워크를 훈련시켜 보자.
train_datagen = ImageDataGenerator(rescale=1./255, rotation_range=40, width_shift_range=0.2, height_shift_range=0.2, shear_range=0.2, zoom_range=0.2, horizontal_flip=True)

test_datagen = ImageDataGenerator(rescale=1./255)

train_generator = train_datagen.flow_from_directory(train_dir, target_size=(150,150), batch_size=32, class_mode='binary')
validation_generator = test_datagen.flow_from_directory(validation_dir, target_size=(150,150), batch_size=32, class_mode='binary')

history = model.fit_generator(train_generator, steps_per_epoch=100, epochs=100, validation_data=validation_generator, validation_steps=50)
  • 모델을 저장하자.
model.save('cats_and_dogs_samll_2.h5')
  • 결과 그래프를 다시 그려보자. 데이터 증식과 드롭아웃 덕분에 더는 과대적합되지 않는다. 훈련 곡선이 검증 곡선에 가깝꼐 따라가고 있다.
    • 검증 데이터에서 82% 정확도를 달성했는데, 규제하지 않은 모델과 비교했을 때 15% 정도 향상되었다.

  • 다른 규제 기법을 더 사용하고 네트워크의 파라미터를 튜닝하면 86%나 87% 정도까지 더 높은 정확도를 얻을 수 있다.
    • 하지만 데이터가 적기 때문에 컨브넷을 처음부터 훈련해서 더 높은 정확도를 달성하기는 어렵다.
    • 이런 상황에서 정확도를 높이기 위한 다음 단계는 사전 훈련된 모델을 사용하는 것이다.

사전 훈련된 컨브넷 사용하기

  • 작은 이미지 데이터셋에 딥러닝을 적용하는 일반적이고 매우 효과적인 방법은 사전 훈련된 네트워크를 사용하는 것이다.
    • 사전 훈련된 네트워크(pretrained network)는 일반적으로 대규모 이미지 분류 문제를 위해 대량의 데이터셋에서 미리 훈련되어 저장된 네트워크이다.
    • 원본 데이터셋이 충분히 크고 일반적이라면 사전 훈련된 네트워크에 의해 학습된 특성의 계층 구조는 실제 세상에 대한 일반적인 모델로 효율적인 역할을 할 수 있다. 새로운 문제가 원래 작업과 완전히 다른 클래스에 대한 것이라도 이런 특성은 많은 컴퓨터 비전 문제에 유용하다.
    • 예컨대 대부분 동물이나 생활 용품으로 이루어진 ImageNet 데이터셋에 네트워크를 훈련한다. 그 다음 이 네트워크를 이미지에서 가구 아이템을 식별하는 것 같은 다른 용도로 사용할 수 있다.
    • 학습된 특성을 다른 문제에 적용할 수 있는 이런 유연성은 이전의 많은 얕은 학습 방법과 비교했을 때 딥러닝의 핵심 장점이다. 이런 방식으로 작은 데이터셋을 가진 문제에도 딥러닝이 효율적으로 작동할 수 있다.
  • 여기서는 ImageNet 데이터셋에서 훈련된 대규모 컨브넷을 사용해 보겠다. 캐런 시몬연(Karen Simonyan)과 앤드류 지서먼(Andrew Zisserman)이 2014년 개발한 VGG16 구조를 사용하겠다.
    • VGG16은 조금 오래되었고 최고 수준의 성능에는 못 미치며 최근의 다른 모델보다는 조금 무겁다. 하지만 이 모델의 구조기 이전에 보았던 것과 비슷해서 선택했다.
    • 아마 VGG가 처음 보는 모델 애칭일지 모르겠으나 이런 이름에는 VGG, ResNet, Inception, Inception_ResNet, Xception 등이 있다. 컴퓨터 비전을 위해 딥러닝을 계속 공부하다 보면 이런 이름을 자주 만나게 될 것이다.
  • 사전 훈련된 네트워크를 사용하는 두 가지 방법이 있다. 특성 추출(feature extraction)과 미세 조정(fine tuning)이다.

특성 추출

  • 특성 추출은 사전에 학습된 네트워크의 표현을 사용하여 새로운 샘플에서 흥미로운 특성을 뽑아내는 것이다. 이런 특성을 사용하여 새로운 분류기를 처음부터 훈련한다.
  • 앞서 보앗듯이 컨브넷은 이미지 분류를 위해 두 부분으로 구성된다. 먼저 연속된 합성곱과 풀링 층으로 시작해서 완전 연결 분류기로 끝난다.
    • 첫 번째 부분을 모델의 합성곱 기반 층(convolutional base)이라 부르겠다.
    • 컨브넷의 경우 특성 추출은 사전에 훈련된 네트워크의 합성곱 기반 층을 선택하여 새로운 데이터를 통과시키고, 그 출력으로 새로운 분류기를 훈련한다.

  • 왜 합성곱 층만 재사용할까? 완전 연결 분류기도 재사용할 수 있을까?
    • 일반적으로 권장하지 않는데, 합성곱 층에 의해 학습된 표현이 더 일반적이어서 재사용이 가능하기 때문이다.
    • 컨브넷의 특성 맵은 사진에 대한 일반적인 콘셉트의 존재 여부를 기록한 맵이다. 주어진 컴퓨터 비전 문제에 상관없이 유용하게 사용할 수 있다.
    • 하지만 분류기에서 학습한 표현은 모델이 훈련된 클래스 집합에 특화되어 있다. 분류기는 전체 사진에 어떤 클래스가 존재할 확률에 관한 정보만 담고 있다.
    • 더군다나 완전 연결 층에서 찾은 표현은 더 이상 입력 이미지에 있는 객체의 위치 정보를 가지고 있지 않다. 완전 연결 층들은 공간 개념을 제거하지만 합성곱의 특성맵은 객체 위치를 고려한다. 객체 위치가 중요한 문제라면 완전 연결 층에서 만든 특성느 크게 쓸모가 없다.
  • 특정 합성곱 층에서 추출한 표현의 일반성(그리고 재사용성) 수준은 모델에 있는 층의 깊이에 달려 있다.
    • 모델의 하위 층은(에지, 색깔, 질감 등) 지역적이고 매우 일반적인 특성 맵을 추출한다. 반면 상위 층은 (강아지 눈, 고양이 귀 처럼) 좀 더 추상적인 개념을 추출한다.
    • 새로운 데이터셋이 원본 모델이 훈련한 데이터셋과 많이 다르다면 전체 합성곱 기반 층을 사용하는 것보다는 모델의 하위 층 몇 개만 특성 추출에 사용하는 것이 좋다.
  • ImageNet의 클래스 집합에는 여러 종류의 강아지와 고양이를 포함하고 있다. 이런 경우 원본 모델의 완전 연결 층에 있는 정보를 재사용하는 것이 도움이 될 것이다.
    • 하지만 새로운 문제의 클래스가 원본 모델의 클래스 집합과 겹치지 않는 좀 더 일반적인 경우를 다루기 위해 여기서는 완전 연결 층을 사용하지 않겠다.
    • ImageNet 데이터셋에 훈련된 VGG16 네트워크의 합성곱 기반 층을 사용하여 강아지와 고양이 이미지에 유용한 특성을 추출해 보겠다. 그런 다음 이 특성으로 강아지 vs 고양이 분류기를 훈련한다.
  • VGG16 모델은 케라스 패키지로 포함되어 있다. keras.applications 모듈에서 임포트 할 수 있는데, keras.applications 모듈에서 사용 가능한 이미지 분류 모델은 다음과 같다. (모두 ImageNet 데이터셋에서 훈련되었다.)
    • Xception
    • Inception V3
    • ResNet50
    • VGG16
    • VGG19
    • MobileNet
  • VGG16 모델을 만들어 보자.
from keras.application import VGG16

conv_base = VGG16(weights='imagenet', include_top=False, input_shape=(150, 150, 3))
  • VGG 함수에 3개의 매개 변수를 전달한다.
    • weights는 모델을 초기화할 가중치 체크포인트를 지정한다.
    • include_top은 네트워크의 최상위 완전 연결 분류기를 포함할지 안 할지 지정한다. 기본값은 ImageNet의 클래스 1000개에 대응되는 완전 연결 분류기를 포함한다. 별도의 완전 연결층을 추가하려 하므로 이를 포함시키지 않는다.
    • input_shape은 네트워크에 주입할 이미지 텐서의 크기이다. 이 매개변수는 선택사항인데, 이 값을 지정하지 않으면 네트워크가 어떤 크기의 입력도 처리할 수 있다.
  • 다음은 VGG16 합성곱 기반 층의 자세한 구조이다.
Layer (type)                 Output Shape              Param #   
=================================================================
input_1 (InputLayer)         (None, 150, 150, 3)       0         
_________________________________________________________________
block1_conv1 (Conv2D)        (None, 150, 150, 64)      1792      
_________________________________________________________________
block1_conv2 (Conv2D)        (None, 150, 150, 64)      36928     
_________________________________________________________________
block1_pool (MaxPooling2D)   (None, 75, 75, 64)        0         
_________________________________________________________________
block2_conv1 (Conv2D)        (None, 75, 75, 128)       73856     
_________________________________________________________________
block2_conv2 (Conv2D)        (None, 75, 75, 128)       147584    
_________________________________________________________________
block2_pool (MaxPooling2D)   (None, 37, 37, 128)       0         
_________________________________________________________________
block3_conv1 (Conv2D)        (None, 37, 37, 256)       295168    
_________________________________________________________________
block3_conv2 (Conv2D)        (None, 37, 37, 256)       590080    
_________________________________________________________________
block3_conv3 (Conv2D)        (None, 37, 37, 256)       590080    
_________________________________________________________________
block3_pool (MaxPooling2D)   (None, 18, 18, 256)       0         
_________________________________________________________________
block4_conv1 (Conv2D)        (None, 18, 18, 512)       1180160   
_________________________________________________________________
block4_conv2 (Conv2D)        (None, 18, 18, 512)       2359808   
_________________________________________________________________
block4_conv3 (Conv2D)        (None, 18, 18, 512)       2359808   
_________________________________________________________________
block4_pool (MaxPooling2D)   (None, 9, 9, 512)         0         
_________________________________________________________________
block5_conv1 (Conv2D)        (None, 9, 9, 512)         2359808   
_________________________________________________________________
block5_conv2 (Conv2D)        (None, 9, 9, 512)         2359808   
_________________________________________________________________
block5_conv3 (Conv2D)        (None, 9, 9, 512)         2359808   
_________________________________________________________________
block5_pool (MaxPooling2D)   (None, 4, 4, 512)         0         
=================================================================
Total params: 14,714,688
Trainable params: 14,714,688
Non-trainable params: 0
  • 최종 특성 맵의 크기는 (4, 4, 512)이다. 이 특성 위에 완전 연결 층을 놓을 것이다.) 이 지점에서 두 가지 새로운 방식이 가능하다.
    • 새로운 데이터셋에서 합성곱 기반 층을 실행하고 출력을 넘파이 배열로 디스크에 저장한다. 그 다음 이 데이터를 독립된 완전 연결 분류기에 입력으로 사용한다. 합성곱 연산은 전체 과정 중에서 가장 비싼 부분이다. 이 방식은 모든 입력 이미지에 대해 합성곱 기반 층을 한 번만 실행하면 되기 때문에 빠르고 비용이 적게 든다. 하지만 이런 이유로 이 기법에는 데이터 증식을 사용할 수 없다.
    • 준비한 모델(conv_base) 위에 Dense 층을 쌓아 확장한다. 그 다음 입력 데이터에서 엔드-투-엔드로 전체 모델을 실행한다. 모델에 노출된 모든 입력 이미지가 매번 합성곱 기반층을 통과하기 때문에 데이터 증식을 사용할 수 있다. 하지만 이런 이유로 첫 번째 방식보다 훨씬 많은 비용이 든다.

데이터 증식을 사용하지 않는 빠른 특성 추출

  • ImageDataGenerator를 이용해서 이미지와 레이블을 넘파이 배열로 추출하자. conv_base 모델의 predict 메서드를 호출하여 이 이미지에서 특성을 추출한다.
import os
import numpy as np
from keras.preprocessing.image import ImageDataGenerator

base_dir = 'E:/Study/Keras Deep Learning/datasets/cats_and_dogs/cats_and_dogs_small/'
train_dir = os.path.join(base_dir, 'train')
validation_dir = os.path.join(base_dir, 'validation')
test_dir = os.path.join(base_dir, 'test')

datagen = ImageDataGenerator(rescale=1./255)
batch_size = 20

def extract_features(directory, sample_count):
features = np.zeros(shape=(sample_count, 4, 4, 512))
labels = np.zeros(shape=(sample_count))
generator = datagen.flow_from_directory(directory, target_size=(150,150), batch_size=batch_size, class_mode='binary')

i = 0
for inputs_batch, labels_batch in generator:
features_batch = conv_base.predict(inputs_batch)
features[i*batch_size: (i+1) * batch_size] = features_batch
labels[i*batch_size: (i+1) * batch_size] = labels_batch
i+=1

if i * batch_size >= sample_count:
break

return features, labels

train_features, train_labels = extract_features(train_dir, 2000)
validation_features, validation_labels = extract_features(validation_dir, 1000)
test_features, test_labels = extract_features(train_dir, 1000)
  • 추출된 특성의 크기는 (samples, 4, 4, 512)이다. 완전 연결 분류기에 주입하기 위해 먼저 (samples, 8192) 크기로 펼친다.
train_features = np.reshape(train_features, (2000, 4*4*512))
validation_features = np.reshape(validation_features, (1000, 4*4*512))
test_features = np.reshape(test_features, (1000, 4*4*512))
  • 그 후에 완전 연결 분류기를 정의하고 저장된 데이터와 레이블을 사용하여 훈련한다.
from keras import models
from keras import layers
from keras import optimizers

model = models.Sequential()
model.add(layers.Dense(256, activation='relu', input_dim=4*4*512))
model.add(layers.Dropout(0.5))
model.add(layers.Dense(1, activation='sigmoid'))

model.compile(optimizer=optimizers.RMSprop(lr=2e-5), loss='binary_crossentropy', metrics=['acc'])

history = model.fit(train_features, train_labels, epochs=30, batch_size=20, validation_data=(validation_features, validation_labels))
  • 2개의 Dense 층만 처리하면 되기 때문에 훈련이 매우 빠르다. 
  • 훈련 손실과 정확도 곡선을 살펴보자.
import matplotlib.pyplot as plt

acc = history.history['acc']
val_acc = history.history['val_acc']
loss = history.history['loss']
val_loss = history.history['val_loss']

epochs = range(1, len(acc) + 1)

plt.plot(epochs, acc, 'bo', label='Training acc')
plt.plot(epochs, val_acc, 'b', label='Validation acc')
plt.title('Training and validation accuracy')
plt.legend()

plt.figure()

plt.plot(epochs, loss, 'bo', label='Training loss')
plt.plot(epochs, val_loss, 'b', label='Validation loss')
plt.title('Training and validation loss')
plt.legend()

plt.show()

  • 약 90%의 검증 정확도에 도달했다. 이전 절에서 처음부터 훈련시킨 작은 모델에서 얻은 것보다 훨씬 좋다. 
    • 하지만 이 그래프는 많은 비율로 드롭아웃을 사용했음에도 훈련을 시작하면서 거의 바로 과대적합되고 있다는 것을 보여준다. 작은 이미지 데이터셋에서는 과대적합을 막기 위해 필수적인 데이터 증식을 사용하지 않았기 때문이다.

데이터 증식을 사용한 특성 추출

  • 이 방법은 훨씬 느리고 비용이 많이 들지만 훈련하는 동안 데이터 증식 기법을 사용할 수 있다. conv_base 모델을 확장하고 입력 데이터를 사용하여 엔드-투-엔드로 실행한다.
  • 모델은 층과 동일하게 작동하므로 층을 추가하듯이 Sequential 모델에 다른 모델을 추가할 수 있다.
from keras import models
from keras import layers

model = models.Sequential()
model.add(conv_base)
model.add(layers.Flatten())
model.add(layers.Dense(256, activation='relu'))
model.add(layers.Dense(1, activation='sigmoid'))
  • 이 모델의 구조는 다음과 같다.
Layer (type)                 Output Shape              Param #   
=================================================================
vgg16 (Model)                (None, 4, 4, 512)         14714688  
_________________________________________________________________
flatten_1 (Flatten)          (None, 8192)              0         
_________________________________________________________________
dense_1 (Dense)              (None, 256)               2097408   
_________________________________________________________________
dense_2 (Dense)              (None, 1)                 257       
=================================================================
Total params: 16,812,353
Trainable params: 16,812,353
Non-trainable params: 0
  • 여기서 볼 수 있듯이 VGG16의 합성곱 기반 층은 14,714,688개의 매우 많은 파라미터를 갖고 있다. 합성곱 기반 층 위에 추가한 분류기는 200만개의 파라미터를 가진다.
  • 모델을 컴파일 하고 훈련하기 전에는 합성곱 기반 층을 동결하는 것이 아주 중요하다. 하나 이상의 층을 동결(freezing) 한다는 것은 훈련하는 동안 가중치가 업데이트되지 않도록 막는다는 뜻이다.
    • 이렇게 하지 않으면 합성곱 기반 층에 의해 사전에 학습된 표현이 훈련하는 동안 수정될 것이다.
    • 맨 위의 Dense 층은 랜덤하게 초기화되었기 때문에 매우 큰 가중치 업데이트 값이 네트워크에 전파될 것이다. 이는 사전에 학습된 표현을 크게 훼손하게 된다.
  • 케라스에서는 trainable 속성을 False로 설정하여 네트워크를 동결할 수 있다.
print('conv_base를 동결하기 전 훈련되는 가중치의 수: ', len(model.trainable_weights))
conv_base.trainable = False
print('conv_base를 동결한 후 훈련되는 가중치의 수: ', len(model.trainable_weights))
---
conv_base를 동결하기 전 훈련되는 가중치의 수: 30 conv_base를 동결한 후 훈련되는 가중치의 수: 4
  • 이렇게 설정하면 추가한 2개의 Dense 층 가중치만 훈련될 것이다. 층마다 2개씩 총 4개의 텐서가 훈련된다.
    • 변경 사항을 적용하려면 먼저 모델을 컴파일 해야 한다. 컴파일 단계 후에 trainable 속성을 변경하면 반드시 모델을 다시 컴파일해야 한다. 그렇지 않으면 변경 사항이 적용되지 않는다.
  • 앞서 사용했던 데이터 증식을 사용하여 모델 훈련을 시작할 수 있다.
from keras.preprocessing.image import ImageDataGenerator
from keras import optimizers

train_datagen = ImageDataGenerator(rescale=1./255, rotation_range=40, width_shift_range=0.2, height_shift_range=0.2, shear_range=0.2, zoom_range=0.2, horizontal_flip=True, fill_mode='nearest')

test_datagen = ImageDataGenerator(rescale=1./255)

train_generator = train_datagen.flow_from_directory(train_dir, target_size=(150,150), batch_size=20, class_mode='binary')

validation_generator = test_datagen.flow_from_directory(validation_dir, target_size=(150,150), batch_size=20, class_mode='binary')

model.compile(loss='binary_crossentropy', optimizer=optimizers.RMSprop(lr=2e-5), metrics=['acc'])

history = model.fit_generator(train_generator, steps_per_epoch=100, epochs=30, validation_data=validation_generator, validation_steps=50, verbose=2)
  • 결과 그래프는 다음과 같다. 검증 정확도가 이전과 비슷하지만 처음부터 훈련시킨 소규모 컨브넷보다 과대적합이 줄었다.

미세 조정

  • 미세 조정(fine tuning)은 특성 추출에 사용했던 동결 모델의 상위 층 몇 개를 동결에서 해제하고 모델에 새로 추가한 층(여기서는 완전 연결 분류기)과 함께 훈련하는 것이다.
    • 주어진 문제에 조금 더 밀접하게 재사용 모델의 표현을 일부 조정하기 때문에 미세 조정이라고 한다.

  • 앞서 랜덤하게 초기화된 상단 분류기를 훈련하기 위해 VGG16의 합성곱 기반 층을 동결해야 한다고 했다. 같은 이유로 맨 위에 있는 분류기가 훈련된 후에 합성곱 기반의 상위 층을 미세 조정할 수 있다.
    • 분류기가 미리 훈련되지 않으면 훈련되는 동안 너무 큰 오차 신호가 네트워크에 전파된다. 이는 미세 조정될 층들이 사전에 학습한 표현을 망가뜨리게 될 것이다.
  • 미세 조정하는 단계는 다음과 같다.
    • 사전에 훈련된 기반 네트워크 위에 새로운 네트워크를 추가한다.
    • 기반 네트워크를 동결한다.
    • 새로 추가한 네트워크를 훈련한다.
    • 기반 네트워크에서 일부 층의 동결을 해제한다.
    • 동결을 해제한 층과 새로 추가한 층을 함께 훈련한다.
  • 처음 세 단계는 특성 추출을 할 때 이미 완료했다. 네 번째 단계를 진행해 보자. conv_base의 동결을 해제하고 개별 층을 동결하겠다.
    • 앞서 보았던 합성곱 기반 층의 구조를 다시 확인해 보자.
Layer (type) Output Shape Param # ================================================================= 
input_1 (InputLayer) (None, 150, 150, 3) 0 _________________________________________________________________ block1_conv1 (Conv2D) (None, 150, 150, 64) 1792 _________________________________________________________________ block1_conv2 (Conv2D) (None, 150, 150, 64) 36928 _________________________________________________________________ block1_pool (MaxPooling2D) (None, 75, 75, 64) 0 _________________________________________________________________ block2_conv1 (Conv2D) (None, 75, 75, 128) 73856 _________________________________________________________________ block2_conv2 (Conv2D) (None, 75, 75, 128) 147584 _________________________________________________________________ block2_pool (MaxPooling2D) (None, 37, 37, 128) 0 _________________________________________________________________ block3_conv1 (Conv2D) (None, 37, 37, 256) 295168 _________________________________________________________________ block3_conv2 (Conv2D) (None, 37, 37, 256) 590080 _________________________________________________________________ block3_conv3 (Conv2D) (None, 37, 37, 256) 590080 _________________________________________________________________ block3_pool (MaxPooling2D) (None, 18, 18, 256) 0 _________________________________________________________________ block4_conv1 (Conv2D) (None, 18, 18, 512) 1180160 _________________________________________________________________ block4_conv2 (Conv2D) (None, 18, 18, 512) 2359808 _________________________________________________________________ block4_conv3 (Conv2D) (None, 18, 18, 512) 2359808 _________________________________________________________________ block4_pool (MaxPooling2D) (None, 9, 9, 512) 0 _________________________________________________________________ block5_conv1 (Conv2D) (None, 9, 9, 512) 2359808 _________________________________________________________________ block5_conv2 (Conv2D) (None, 9, 9, 512) 2359808 _________________________________________________________________ block5_conv3 (Conv2D) (None, 9, 9, 512) 2359808 _________________________________________________________________ block5_pool (MaxPooling2D) (None, 4, 4, 512) 0 =================================================================
Total params: 14,714,688
Trainable params: 14,714,688
Non-trainable params: 0
  • 마지막 3개의 합성곱 층을 미세 조정하겠다. 즉 block4_pool까지 모든 층은 동결되고 block5_conv1, block5_conv2, block5_conv3 층은 학습 대상이 된다.
  • 왜 더 많은 층을 미세 조정하지 않을까? 그렇게 할 수도 있지만 다음 사항을 고려해야 한다.
    • 합성곱 기반 층에 있는 하위 층들을 좀 더 일반적이고 재사용 가능한 특성들을 인코딩한다. 반면 상위 층은 좀 더 특화된 특성을 인코딩 한다. 새로운 문제에 재활용하도록 수정이 필요한 것은 구체적인 특성이므로 이들을 미세 조정하는 것이 유리하다. 하위 층으로 갈수록 미세 조정에 대한 효과가 감소한다.
    • 훈련해야 할 파라미터가 많을수록 과대적합의 위험이 커진다. 합성곱 기반 층은 1,500만개의 파라미터를 가지고 있으므로 작은 데이터셋으로 전부 훈련하려고 하면 매우 위험하다.
  • 앞선 예제 코드에 이어 미세 조정을 해보자.
conv_base.trainable = True

set_trainable = False

for layer in conv_base.layers:
if layer.name == 'block5_conv1':
set_trainable = True
if set_trainable:
layer.trainable = True
else:
layer.trainable = False
  • 이제 네트워크의 미세 조저을 시작해보자. 학습률을 낮춘 RMSProp 옵티마이저를 사용한다. 학습률을 낮추는 이유는 미세 조정하는 3개의 층에서 학습된 표현을 조금씩 수정하기 위해서다. 변경량이 너무 크면 학습된 표현에 나쁜 영향을 끼칠 수 있다.
model.compile(loss='binary_crossentropy', optimizer=optimizers.RMSprop(lr=1e-5), metrics=['acc'])

history = model.fit_generator(train_generator, steps_per_epoch=100, epochs=100, validation_data=validation_generator, validation_steps=50)
  • 이전과 동일한 코드로 결과를 그려보면 다음과 같다.

  • 그래프가 불규칙하게 보이는데, 그래프를 보기 쉽게 하기 위해 지수 이동 평균(exponential moving averages)으로 정확도와 손실 값을 부드럽게 표현할 수 있다.
def smooth_curve(points, factor=0.8):
smoothed_points = []
for point in points:
if smoothed_points:
previous = smoothed_points[-1]
smoothed_points.append(previous * factor + point * (1-factor))
else:
smoothed_points.append(point)
return smoothed_points

plt.plot(epochs, smooth_curve(acc), 'bo', label='Smoothed training acc')
plt.plot(epochs, smooth_curve(val_acc), 'b', label='Smoothed validation acc')
plt.title('Training and validation accuracy')
plt.legend()

plt.figure()

plt.plot(epochs, smooth_curve(loss), 'bo', label='Smoothed training loss')
plt.plot(epochs, smooth_curve(val_loss), 'b', label='Smoothed validation loss')
plt.title('Training and validation loss')
plt.legend()

plt.show()

  • 검증 정확도 곡선이 훨씬 깨끗하게 보인다. 정확도가 대략 1% 이상 향상되었다.
  • 손실 곡선은 실제 어떤 향상을 얻지 못했다(사실 악화되었다). 손실이 감소되지 않았는데 어떻게 정확도가 안정되거나 향상될 수 있었을까?
    • 답은 간단한데, 그래프는 개별적인 손실 값의 평균을 그린 것이다. 하지만 정확도에 영향을 미치는 것은 손실 값의 분포이지 평균이 아닌다.
    • 정확도는 모델이 예측한 클래스 확률이 어떤 임계 값을 넘었는지에 대한 결과이기 때문이다. 모델이 더 향상되더라도 평균 손실에 반영되지 않을 수 있다.
  • 마지막으로 테스트 데이터에서 이 모델을 평가해보자.
test_generator = test_datagen.flow_from_directory(test_dir, target_size=(150, 150), batch_size=20, class_mode='binary')

test_loss, test_acc = model.evaludate_generator(test_generator, steps=50)
print('test acc:', test_acc)
  • 92%의 테스트 정확도를 얻을 것이다. 이 데이터셋을 사용한 원래 캐글 경연 대회에서 꽤 높은 결과이다. 하지만 최신 딥러닝 기법으로 훈련 데이터의 일부분(약 10%)만 사용해서 이런 결과를 달성했다.

정리

  • 컨브넷은 컴퓨터 비전 작업에 가장 뛰어난 머신 러닝 모델이다. 아주 작은 데이터셋에서도 처음부터 훈련해서 괜찮은 성능을 낼 수 있다.
  • 작은 데이터셋에서는 과대적합이 큰 문제이다. 데이터 증식은 이미지 데이터를 다룰 때 과대적합을 막을 수 있는 강력한 방법이다.
  • 특성 추출 방식으로 새로운 데이터셋에 기존 컨브넷을 쉽게 재사용할 수 있다. 작은 이미지 데이터셋으로 작업할 때 효과적인 기법이다.
  • 특성 추출을 보완하기 위해 미세 조정을 사용할 수 있다. 미세 조정은 기존 모델에서 사전에 학습한 표현의 일부를 새로운 문제에 적응시킨다. 이 기법은 조금 더 성능을 끌어올린다.

컨브넷 학습 시각화

  • 딥러닝 모델을 ‘블랙 박스(black box)’ 같다고 자주 이야기하는데, 학습된 표현에서 사람이 이해하기 쉬운 형태를 뽑아내거나 제시하기 어렵기 때문이다.
    • 일부 딥러닝 모델에서는 이 말이 어느 정도 맞지만 컨브넷에서는 전혀 아니다. 컨브넷에서는 시각적인 개념을 학습한 것이기 때문에 시각화하기 아주 좋다.
  • 컨브넷의 표현을 시각화하고 해석하는 기법들은 다음과 같다.
    • 컨브넷 중간층의 출력(중간층에 있는 활성화)을 시각화하기: 연속된 컨브넷 층이 입력을 어떻게 변형시키는지 이해하고 개별적인 컨브넷 필터의 의미를 파악하는데 도움이 된다.
    • 컨브넷 필터를 시각화하기: 컨브넷의 필터가 찾으려는 시각적인 패턴과 개념이 무엇인지 상세하게 이해하는데 도움이 된다.
    • 클래스 활성화에 대한 히트맵(heatmap)을 이미지에 시각화하기: 이미지의 어느 부분이 주어진 클래스에 속하는데 기여했는지 이해하고 이미지에서 객체 위치를 추정(localization)하는데 도움이 된다.

중간층의 활성화 시각화하기

  • 중간층의 활성화 시각화는 어떤 입력이 주어졌을 때 네트워크에 있는 여러 합성곱과 풀링 층이 출력하는 특성 맵을 그리는 것이다. (층의 출력이 활성화 함수의 출력이라서 종종 활성화(activation)라고 부른다.)
    • 이 방법은 네트워크에 의해 학습된 필터들이 어떻게 입력을 분해하는지 보여준다.
    • 너비, 높이, 깊이(채널) 3개의 차원에 대해 특성 맵을 시각화하는 것이 좋다.
    • 각 채널은 비교적 독립적인 특성을 인코딩하므로 특성 맵의 각 채널 내용을 독립적인 2D 이미지로 그리는 것이 괜찮은 방법이다.
  • 앞서 만들었던 모델을 로드해서 시작해 보자.
from keras.models import load_model

model = load_model('cats_and_dogs_small_2.h5')
model.summary()

------
Layer (type) Output Shape Param # ================================================================= conv2d_9 (Conv2D) (None, 148, 148, 32) 896 _________________________________________________________________ max_pooling2d_9 (MaxPooling2 (None, 74, 74, 32) 0 _________________________________________________________________ conv2d_10 (Conv2D) (None, 72, 72, 64) 18496 _________________________________________________________________ max_pooling2d_10 (MaxPooling (None, 36, 36, 64) 0 _________________________________________________________________ conv2d_11 (Conv2D) (None, 34, 34, 128) 73856 _________________________________________________________________ max_pooling2d_11 (MaxPooling (None, 17, 17, 128) 0 _________________________________________________________________ conv2d_12 (Conv2D) (None, 15, 15, 128) 147584 _________________________________________________________________ max_pooling2d_12 (MaxPooling (None, 7, 7, 128) 0 _________________________________________________________________ flatten_3 (Flatten) (None, 6272) 0 _________________________________________________________________ dropout_1 (Dropout) (None, 6272) 0 _________________________________________________________________ dense_5 (Dense) (None, 512) 3211776 _________________________________________________________________ dense_6 (Dense) (None, 1) 513 ================================================================= Total params: 3,453,121 Trainable params: 3,453,121 Non-trainable params: 0
  • 그 다음 네트워크를 훈련할 때 사용했던 이미지에 포함되지 않은 고양이 사진 하나를 입력 이미지로 선택한다.
img_path = '.datasets/cats_and_dogs/cats_and_dogs_small/test/cats/cat.1700.jpg'

from keras.preprocessing import image
import numpy as np

img = image.load_img(img_path, target_size=(150,150))
img_tensor = image.img_to_array(img)
img_tensor = np.expand_dims(img_tensor, axis=0)
img_tensor /= 255.

print(img_tensor.shape)
  • 사진을 출력해 보자
import matplotlib.pyplot as plt

plt.imshow(img_tensor[0])
plt.show()

  • 확인하고 싶은 특성 맵을 추출하기 위해 이미지 배치를 입력으로 받아 모든 합성곱과 출링 층의 활성화를 출력하는 케라스 모델을 만들자. 
    • 이를 위해 케라스의 Model 클래스를 사용하겠다. 모델 객체를 만들 때 2개의 매개변수가 필요한데, 입력 텐서(또는 입력 텐서의 리스트)와 출력 텐서(또는 출력 텐서의 리스트)이다.
    • 반환되는 객체는 Sequential과 같은 케라스 모델이지만 특정 입력과 특정 출력을 매핑한다.
    • Model 클래스를 사용하면 Sequential과는 달리 여러 개의 출력을 가진 모델을 만들 수 있다.
from keras import models

layer_outputs = [layer.output for layer in model.layers[:8]] --상위 8개 층의 출력을 추출한다.
activation_model = models.Model(inputs=model.input, outputs=layer_outputs) --입력에 대해 8개 층의 출력을 반환하는 모델을 만든다.
  • 입력 이미지가 주입될 때 이 모델은 원본 모델의 활성화 값을 반환한다. 이 모델이 이 책에서는 처음 나오는 다층 출력 모델이다.
    • 지금까지 본 모델은 정확히 하나의 입력과 하나의 출력만을 가졌는는데, 일반적으로 모델은 몇 개의 입력과 출력이라도 가질 수 있다.
    • 이 모델은 입력과 층의 활성화마다 하나씩 총 8개의 출력을 가진다.
activations = activation_model.predict(img_tensor)  --층의 활성화마다 하나씩 8개의 넘파이 배열로 이루어진 리스트를 반환한다.
  • 예컨대 다음이 고양이 이미지에 대한 첫 번째 합성곱 층의 활성화 값이다.
first_layer_activation = activations[0]
print(first_layer_activation.shape)
----
(1, 148, 148, 32)
  • 32개의 채널을 가진 148×148 크기의 특성 맵이다. 원본 모델의 첫 번째 활성화 중에서 20번째 채널을 그려보자.
import matplotlib.pyplot as plt

plt.matshow(first_layer_activation[0, :, :, 19], cmap='viridis')

  • 이 채널은 대각선 에지를 감지하도록 인코딩 된 것 같다.
    • 합성곱 층이 학습한 필터는 결정적이지 않기 때문에 채널 이미지가 책과 다를 수 있다.
    • (실제 내가 돌려본 이미지는 전혀 다름)
  • 16번째 채널을 그려보자.

  • (마찬가지로 내가 돌려본 결과는 전혀 다르다)
  • 이제 네트워크의 모든 활성화를 시각화해 보자.
    • 8개의 활성화 맵에서 추출한 모든 채널을 그리기 위해 하나의 큰 이미지 텐서에 추출한 결과를 나란히 쌓겠다.
layer_names = []
for layer in model.layers[:8]:
layer_names.append(layer.name)

images_per_row = 16

for layer_name, layer_activation in zip(layer_names, activations):
n_features = layer_activation.shape[-1]
size = layer_activation.shape[1]
n_cols = n_features // images_per_row
display_grid = np.zeros((size*n_cols, images_per_row*size))

for col in range(n_cols):
for row in range(images_per_row):
channel_image = layer_activation[0, :, :, col * images_per_row + row]
channel_image -= channel_image.mean()
channel_image /= channel_image.std()
channel_image *= 64
channel_image += 128
channel_image = np.clip(channel_image, 0, 255).astype('uint8')
display_grid[col*size : (col+1) * size, row*size : (row+1)*size] = channel_image

scale = 1. / size
plt.figure(figsize=(scale * display_grid.shape[1], scale * display_grid.shape[0]))
plt.title(layer_name)
plt.grid(False)
plt.imshow(display_grid, aspect='auto', cmap='viridis')

plt.show()

 

  • 몇 가지 주목해야 할 내용이 있다.
    • 첫 번째 층은 여러 종류의 에지 감지기를 모아 놓은 것 같다. 이 단계의 활성화에는 초기 사진에 있는 거의 모든 정보가 유지된다.
    • 상위 층으로 갈수록 활성화는 점점 더 추상적으로 되고 시각적으로 이해하기 어려워진다. ‘고양이 귀’, ‘고양이 눈’ 처럼 고수준 개념을 인코딩하기 시작한다. 상위 층의 표현은 이미지의 시각적 콘텐츠에 관한 정보가 점점 줄어들고 이미지의 클래스에 관한 정보가 점점 증가한다.
    • 비어 있는 활성화가 층이 깊어짐에 따라 늘어난다. 첫 번째 층에서는 모든 필터가 입력 이미지에 활성화 되었지만 층을 올라가면서 활성화되지 않은 필터들이 생긴다. 필터에 인코딩된 패턴이 입력 이미지에 나타나지 않았다는 것을 의미한다.
  • 심층 신경망이 학습한 표현에서 일반적으로 나타나는 중요한 특징을 확인했다. 층에서 추출한 특성은 층의 깊이를 따라 점점 더 추상적이 된다.
    • 높은 층의 활성화는 특정 입력에 관한 시각적 정보가 점점 더 줄어들고 타깃에 관한 정보(이 경우에는 강아지 또는 고양이 이미지의 클래스)가 점점 더 증가한다.
    • 심층 신경망은 입력되는 원본 데이터(여기서는 RGB 포맷의 사진)에 대한 정보 정제 파이프라인처럼 작동한다.
    • 반복적인 변환을 통해 관계없는 정보(예컨대 이미지에 있는 특정 요소)를 걸러내고 유용한 정보는 강조되고 개선된다(여기서는 이미지의 클래스)
  • 사람과 동물이 세상을 인지하는 방식이 이와 비슷하다.
    • 사람은 몇 초 동안 한 장면을 보고 난 후 그 안에 있었던 추상적인 물체(자전거, 나무)를 기억할 수 있다. 하지만 물체의 구체적인 모양은 기억하지 못한다.
    • 사실 기억을 더듬어 일반적인 자전거를 그려보면 조금이라도 비슷하게 그릴 수 없다.
    • 우리 뇌는 시각적 입력에서 관련성이 적은 요소를 필터링하여 고수준 개념으로 변환한다. 이렇게 완전히 추상적으로 학습하기 때문에 눈으로 본 것을 자세히 기억하기는 어렵다.
    • (결국 인간도 특징점을 찾아 추상화 하여 기억한다는 뜻. 사실 세상의 정보는 무한한데, 그 정보를 다루려면 그런 식으로 압축해서 다루는게 맞다. 이래서 CNN이 이미지 분석을 잘 하는 듯)

컨브넷 필터 시각화하기

  • 컨브넷이 학습한 필터를 조사하는 또 다른 방법은 각 필터가 반응하는 시각적 패턴을 그려 보는 것이다.
    • 빈 입력 이미지에서 시작해서 특정 필터의 응답을 최대화하기 위해 컨브넷 입력 이미지에 경사 상승법을 적용한다.
    • 결과적으로 입력 이미지는 선택된 필터가 최대로 응답하는 이미지가 될 것이다.
  • 전체 과정은 간단하다. 특정 합성곱 츠으이 한 필터 값을 최대화하는 손실 함수를 정의한다.
    • 이 활성화 값을 최대화하기 위해 입력 이미지를 변경하도록 확률적 경사 상승법을 사용한다.
    • 예컨대 여기에서는 ImageNet에 사전 훈련된 VGG16 네트워크에서 block3_conv1 층 필터 0번의 활성화를 손실로 정의한다.
from keras.applications import VGG16
from keras import backend as K

model = VGG16(weights='imagenet', include_top=False)

layer_name = 'block3_conv1'
filter_index = 0

layer_output = model.get_layer(layer_name).output
loss = K.mean(layer_output[:, :, :, filter_index])
  • 경사 상승법을 구현하기 위해 모델의 입력에 대한 손실의 그래디언트가 필요하다. 이를 위해 케라스의 backend 모듈에 있는 gradients 함수를 사용하겠다.
grads = K.gradients(loss, model.input)[0]
  • 경사 상승법 과정을 부드럽게 하기 위해 사용하는 한 가지 기법은 그래디언트 텐서를 L2 노름(텐서에 있는 값을 제곱한 합의 제곱근)으로 나누어 나누어 정규화하는 것이다. 이렇게 하면 입력 이미지에 적용할 수정량의 크기를 항상 일정 범위 안에 놓을 수 있다.
grads /= (K.sqrt(K.mean(K.square(grads))) + 1e-5)
  • 이제 주어진 입력 이미지에 대한 손실 텐서와 그래디언트 텐서를 계산해야 한다. 케라스 백엔드 함수를 사용하여 처리하겠다.
    • iterate는 넘파이 텐서(크기가 1인 텐서의 리스트)를 입력으로 받아 손실과 그래디언트 2개의 넘파이 텐서를 반환한다.
iterate = K.function([model.input], [loss, grads])

import numpy as np
loss_value, grads_value = iterate([np.zeros((1, 150, 150, 3))])
  • 여기에서 파이썬 루프를 만들어 확률적 경사 상승법을 구성한다.
input_img_data = np.random.random((1, 150, 150, 3)) * 20 + 128. --잡음이 섞인 회색 이미지로 시작한다.

step = 1.

# 경사 상승법을 40회 실행
for i in range(40):
loss_value, grads_value = iterate([input_img_data]) --손실과 그래디언트를 계산
input_img_data += grads_value * step --손실을 최대화하는 방향으로 입력 이미지를 수정
  • 결과 이미지 텐서는 (1, 150, 150, 3) 크기의 부동 소수 텐서이다. 이 텐서 값은 [0, 255] 사이의 정수가 아니다. 따라서 출력 가능한 이미지로 변경하기 위해 후처리를 할 필요가 있다. 
    • 이를 위해 간단한 함수를 정의하여 사용하자.
def deprocess_image(x):
# 텐서의 평균이 0, 표준편차가 0.1이 되도록 정규화
x -= x.mean()
x /= (x.std() + 1e-5)
x *= 0.1

# [0, 1]로 클리핑
x += 0.5
x = np.clip(x, 0, 1)

# RGB 배열로 변환
x *= 255
x = np.clip(x, 0, 255).astype('uint8')

return x
  • 이제 모든 코드가 준비되었다. 이 코드를 모아서 층의 이름과 필터 번호를 입력으로 받는 함수를 만들겠다. 이 함수는 필터 활성화를 최대화하는 패턴을 이미지 텐서로 출력한다.
def generate_pattern(layer_name, filter_index, size=150):
layer_ouput = model.get_layer(layer_name).output
loss = K.mean(layer_output[:, :, :, filter_index])

grads = K.gradients(loss, model.input)[0]

grads /= (K.sqrt(K.mean(K.square(grads))) + 1e-5)

iterate = K.function([model.input], [loss, grads])

input_img_data = np.random.random((1, size, size, 3)) * 20 + 128.

step = 1.
for i in range(40):
loss_value, grads_value = iterate([input_img_data])
input_img_data += grads_value * step

img = input_img_data[0]
return deprecss_image(img)
  • 이 함수를 실행한 결과는 다음과 같다.
import matplotlib.pyplot as plt

plt.imshow(generate_pattern('block3_conv1', 0))

  • block3_conv1 층의 필터 0은 물방울 패턴에 반응하는 것 같다. 이제 재미있는 것을 만들어 보자. 모든 층에 있는 필터를 시각화해 보겠다.
    • 간단히 만들기 위해 각 층에서 처음 64개의 필터만 사용하겠다.
    • 또 각 합성곱 블록의 첫 번째 층만 살펴보겠다(block1_conv1, block2_conv1, block3_conv1, block4_conv1, block5_conv1)
    • 여기서 얻은 출력을 64×64 필터 패턴의 8×8 그리드로 정렬한다. 각 필터 패턴 사이에 검은색 마진을 둔다.
layer_name = 'block1_conv1'
size = 64
margin = 5

results = np.zeros((8*size + 7*margin, 8*size + 7*margin, 3), dtype='uint8')

for i in range(8):
for j in range(8):
filter_img = generate_pattern(layer_name, i + (j*8), size=size)
horizontal_start = i * size + i * margin
horizontal_end = horizontal_start + size
vertical_start = j * size + j * margin
vertical_end = vertical_start + size
results[horizontal_start: horizontal_end, vertical_start:vertical_end, :] = filter_img

plt.figure(figsize=(20,20))
plt.imshow(results)

  • 이런 필터 시각화를 통해 컨브넷 츠잉 바라보는 방식을 이해할 수 있다. 컨브넷의 각 층은 필터의 조합으로 입력을 표현할 수 있는 일련의 필터를 학습한다.
  • 이는 푸리에 변환(Fourier transform)을 사용하여 신호를 일련의 코사인 함수로 분해할 수 있는 것과 비슷하다. 이 컨브넷 필터들은 모델의 상위 층으로 갈 수록 점점 더 복잡해지고 개선된다.
    • 모델에 있는 첫 번째 층(block1_conv1)의 필터는 간단한 대각선 방향의 에지와 색깔(또는 어떤 색깔이 있는 에지)을 인코딩한다.
    • block2_conv1의 필터는 에지나 색깔의 조합으로 만들어진 간단한 질감을 인코딩한다.
    • 더 상위 층의 필터는 깃털, 눈, 나뭇잎 등 자연적인 이미지에서 찾을 수 있는 질감을 닮아가기 시작한다.

클래스 활성화의 히트맵 시각화하기

  • 한 가지 시각화 기법을 더 소개하겠다. 이 방법은 이미지의 어느 부분이 컨브넷의 최종 분류 결정에 기여하는지 이해하는데 유용하다.
    • 분류에 실수가 있는 경우 컨브넷의 결정 과정을 디버깅하는데 도움이 된다. 또 이미지에 특정 물체가 있는 위치를 파악하는데 사용할 수도 있다.
  • 이 기법의 종류를 일반적으로 클래스 활성화 맵(Class Activation Map, CAM) 시각화라고 한다.
    • 입력 이미지에 대한 클래스 활성화의 히트맵을 만든다. 클래스 활성화 히트맵은 특정 출력 클래스에 대해 입력 이미지의 모든 위치를 계산한 2D 점수 그리드이다.
    • 클래스에 대해 각 위치가 얼마나 중요한지 알려준다. 예컨대 강아지 vs 고양이 컨브넷에 한 이미지를 주입하면 CAM 시각화는 고양이 클래스에 대한 히트맵을 생성하여 이미지에서 고양이와 비슷한 부분을 알려준다.
  • 여기서 사용할 구체적인 구현은 “Grad-CAM: Visual Explanations from Deep Networks via Gradient-based Localization”에 기술되어 있는 것이다.
    • 방법은 간단하다. 입력 이미지가 주어지면 합성곱 층에 있는 특성 맵의 출력을 추출한다.
    • 그 다음 특성 맵의 모든 채널 출력에 채널에 대한 클래스의 그래디언트 평균을 곱한다.
    • 이 기법을 직관적으로 이해하는 방법은 다음과 같다. ‘입력 이미지가 각 채널을 활성화하는 정도’에 대한 공간적인 맵을 ‘클래스에 대한 각 채널의 중요도’로 가중치를 부여하여 ‘입력 이미지가 클래스를 활성화하는 정도’에 대한 공간적인 맵을 만드는 것이다.
  • 사전 훈련된 VGG16 네트워크를 다시 사용하여 시연해 보겠다.
from keras.applications.vgg16 import VGG16

model = VGG16(weights='imagenet')
  • 다음 그림의 초원을 걷는 어미와 새끼 아프리카 코끼리의 이미지를 적용해 보겠다. 이 이미지를 VGG16 모델이 인식할 수 있도록 변환하다.
    • 이 모델은 224×224 크기의 이미지에서 훈련되었고 keras.applications.vgg16.preprocess_input 함수에 있는 몇 가지 규칙에 따라 전처리 되었다.

from keras.preprocessing import image
from keras.applications.vgg16 import preprocess_input, decode_predictions
import numpy as np

img_path = '.\datasets\creative_commons_elephant.jpg'

img = image.load_img(img_path, target_size=(224, 224))

x = image.img_to_array(img)
x = np.expand_dims(x, axis=0)
x = preprocess_input(x)
  • 이제 이 이미지에서 사전 훈련된 네트워크를 실행하고 예측 벡터를 이해하기 쉽게 디코딩한다.
preds = model.predict(x)
print('Predicted:', decode_predictions(preds, top=3)[0])
---
Downloading data from https://storage.googleapis.com/download.tensorflow.org/data/imagenet_class_index.json 40960/35363 [==================================] - 0s 0us/step Predicted: [('n02504458', 'African_elephant', 0.90942144), ('n01871265', 'tusker', 0.08618243), ('n02504013', 'Indian_elephant', 0.0043545766)]
  • 이 이미지에 대한 상위 3개의 예측 클래스는 다음과 같다.
    • 아프리카 코끼리 90.9% 
    • 코끼리(tusker) 8.6%
    • 인도 코끼리 0.4%
    • (책에 있는 결과와 조금 다름)
  • 네트워크는 이 이미지가 아프리카 코끼리를 담고 있다고 인식했다. 예측 벡터에서 최대로 활성화된 항목은 ‘아프리카 코끼리’클래스에 대한 것으로 386번 인덱스이다.
np.argmax(preds[0])
---
386
  • 이 이미지에서 가장 아프리카 코끼리와 같은 부위를 시각화 하기 위해 Grad-CAM 처리 과정을 구현해 보자.
from keras import backend as K
import matplotlib.pyplot as plt

# 예측 벡터의 '아프리카 코끼리' 항목
african_elephant_output = model.output[:, 386]

# VGG16의 마지막 합성곱 층인 block5_conv3 층의 특성맵
last_conv_layer = model.get_layer('block5_conv3')

# block5_conv3의 특성 맵 출력에 대한 '아프리카 코끼리' 클래스의 그래디언트
grads = K.gradients(african_elephant_output, last_conv_layer.output)[0]

# 특성 맵 채널별 그래디언트 평균값이 담긴 (512, ) 크기의 벡터
pooled_grads = K.mean(grads, axis=(0, 1, 2))

# 샘플 이미지가 주어졌을 때 방금 전 정의한 pooled_grads와 block5_conv3의 특성맵 출력을 구한다.
iterate = K.function([model.input], [pooled_grads, last_conv_layer.output[0]])

# 두 마리 코끼리가 있는 샘플 이미지를 주입하고 2개의 넘파이 배열을 얻는다.
pooled_grads_value, conv_layer_output_value = iterate([x])

# '아프리카 코끼리' 클래스에 대한 '채널의 중요도'를 특성 맵 배열의 채널에 곱한다.
for i in range(512):
conv_layer_output_value[:, :, i] *= pooled_grads_value[i]

# 만들어진 특성 맵에서 채널 축을 따라 평균한 값이 클래스 활성화의 히트맵이다.
heatmap = np.mean(conv_layer_output_value, axis=-1)
heatmap = np.maximum(heatmap, 0)
heatmap /= np.max(heatmap)

plt.matshow(heatmap)

  • 마지막으로 OpenCV를 사용하여 히트맵에 원본 이미지를 겹친 이미지를 만들어보자.
import opencv

img = cv2.imread(img_path)

heatmap = cv2.resize(heatmap, (img.shape[1], img.shape[0]))
heatmap = np.uint8(255 * heatmap)
heatmap = cv2.applyColorMap(heatmap, cv2.COLORMAP_JET)

superimposed_img = heatmap * 0.4 + img

cv2.imwrite('.\datasets\elephant_cam.jpg', superimposed_img)

  • 이 시각화 기법은 2개의 중요한 질문에 대한 답을 준다.
    • 왜 네트워크가 이 이미지에 아프리카 코끼리가 있다고 생각하는가?
    • 아프리카 코끼리 사진은 어디 있는가?
  • 코끼리 새끼의 귀가 강하게 활성화 된 점이 흥미로운데, 아마도 이것이 네트워크가 아프리카 코끼리와 인도 코끼리의 차이를 구분하는 방법일 것이다.

요약

  • 컨브넷은 시각적인 분류 문제를 다루는데 최상의 도구이다.
  • 컨브넷은 우리가 보는 세상을 표현하기 위한 패턴의 계층 구조와 개념을 학습한다.
  • 학습된 표현은 쉽게 분석할 수 있다. 컨브넷은 블랙 박스가 아니다.
  • 이미지 분류 문제를 풀기 위해 자신만의 컨브넷을 처음부터 훈련시킬 수 있다.
  • 과대적합을 줄이기 위해 데이터 증식하는 방법을 배웠다.
  • 사전 훈련된 컨브넷을 사용하여 특성 추출과 미세 조정하는 방법을 배웠다.
  • 클래스 활성화 히트맵을 포함하여 컨브넷이 학습한 필터를 시각화 할 수 있다.

케라스 창시자에게 배우는 딥러닝/ 머신 러닝의 기본 요소

머신 러닝의 네 가지 분류

  • 앞서 다뤘던 세 종류의 머신 러닝 문제는 이진 분류, 다중 분류, 스칼라 회귀였다. 이들은 모두 지도 학습(supervised learning)의 예이다. 지도 학습의 목표는 훈련 데이터 입력과 타깃 사이의 관계를 학습하는 것이다.
  • 지도 학습은 머신 러닝 알고리즘의 일부일 뿐이고, 머신 러닝은 크게 4개의 커다란 범주로 나눈다.

지도 학습

  • 가장 흔한 경우로 (종종 사람이 레이블링한) 샘플 데이터가 주어지면 알고 있는 타깃(꼬리표(annotation))에 입력 데이터를 매핑하는 방법을 학습한다.
  • 요즘 각광받는 광학 문자 판독, 음성 인식, 이미지 분류, 언어 번역 같은 딥러닝의 거의 모든 애플리케이션이 일반적으로 이 범주에 속한다.
  • 지도 학습은 대부분 분류와 회귀로 구성되지만 다음과 같은 특이한 변종도 있다.
    • 시퀀스 생성(sequence generation): 사진이 주어지면 이를 설명하는 캡션을 생성한다. 시퀀스 생성은 이따금 일련의 분류 문제로 재구성할 수 있다.
    • 구문 트리(syntax tree) 예측: 문장이 주어지면 분해된 구문 트리를 예측한다.
    • 물체 감지(object detection) 사진이 주어지면 사진 안의 특정 물체 주위에 경계 상자(bounding box)를 그린다. 이는 분류 문제로 표현되거나 경계 상자의 좌표를 벡터 회귀로 예측하는 회귀와 분류가 결합된 문제로 표현할 수 있다.
    • 이미지 분할(image segmentation): 사진이 주어졌을 때 픽셀 단위로 특정 물체에 마스킹(masking)을 한다.

비지도 학습

  • 이 부류의 머신 러닝은 어떤 타깃도 사용하지 않고 입력 데이터에 대한 흥미로운 변환을 찾는다. 데이터 시각화, 데이터 압축, 데이터의 노이즈 제거 또는 데이터에 있는 상관관계를 더 잘 이해하기 위해 사용한다.
  • 비지도 학습(unsupervised learning)은 데이터 분석에서 빼 놓을 수 없는 요소이며, 종종 지도 학습 문제를 풀기 전에 데이터셋을 잘 이해하기 위해 필수적으로 거치는 단계이다.
  • 차원 축소(dimensionality reduction)와 군집(clustering)이 비지도 학습에서 잘 알려진 범주이다.

자기 지도 학습

  • 자기 지도 학습(self-supervised learning)은 지도 학습의 특별한 케이스지만 별도의 범주로 할 만큼 충분히 다르다.
    • 자기 지도 학습은 지도 학습이지만 사람이 만든 레이블을 사용하지 않는다. 즉 학습 과정에 사람이 개입하지 않는 지도 학습이라고 생각할 수 있다.
    • 레이블이 여전히 필요하지만 보통 경험적인 알고리즘(heuristic algorithm)을 사용해서 입력 데이터로부터 생성한다.
  • 예컨대 오토인코더(autoencoder)가 잘 알려진 자기 지도 학습의 예이다.
    • 여기서 생성된 타깃은 수정하지 않은 원본 입력이다. 같은 방식으로 지난 프레임이 주어졌을 때 비디오의 다음 프레임을 예측하는 것이나, 이전 단어가 주어졌을 때 다음 단어를 예측하는 것이 자기 지도 학습의 예이다. (이 경우 미래의 입력 데이터로부터 지도되기 때문에 시간에 따른 지도학습(temporally supervised learning)이다)
  • 지도 학습, 자기 지도 학습, 비지도 학습의 구분은 가끔 모호할 수 있다.
    • 이 범주들은 명확한 경계가 없고 연속적이다.
    • 자기 지도 학습은 학습 메커니즘과 애플리케이션 측면 중에 어디에 중점을 두는지에 따라 지도 학습 또는 비지도 학습으로 재해석 될 수 있다.

강화 학습

  • 강화 학습(reinforcement learning)은 구글 딥마인드가 아타리 게임 플레이를 학습하는데 성공적으로 적용하면서 최근 많은 관심을 받기 싲가했다.
    • 강화 학습에서 에이전트(agent)는 환경에 대한 정보를 받아 보상을 최대화하는 행동을 선택하도록 학습된다.
    • 예컨대 강화 학습으로 훈련된 신경망은 비디오 게임 화면을 입력으로 받고 게임 점수를 최대화하기 위한 게임 내 행동을 출력할 수 있다.
  • 현재 강화 학습은 대부분 연구 영역에 속해 있고 게임 이외의 실제적인 성공 사례는 아직 없다. 
    • 하지만 때가 되면 강화 학습이 실제 세상의 많은 애플리케이션을 대체할 것으로 기대하는데, 이런 애플리케이션의 예로는 자율 주행 자동차, 자원 관리, 교육 등이 있다.
  • Note) 분류와 회귀에서 사용하는 용어
    • 샘플 또는 입력: 모델에 주입될 하나의 데이터 포인트
    • 예측 또는 출력: 모델로부터 나오는 값
    • 타깃: 정답. 외부 데이터 소스에 근거하여 모델이 완벽하게 예측해야 하는 값
    • 예측 오차 또는 손실 값: 모델의 예측과 타깃 사이의 거리를 측정한 값
    • 클래스: 분류 문제에서 선택할 수 있는 가능한 레이블의 집합. 예컨대 고양이와 강아지 사진을 분류할 때 클래스는 ‘고양이’와 ‘강아지’ 2개이다.
    • 레이블: 분류 문제에서 클래스 할당의 구체적인 사례. 예컨대 사진 #1234에 ‘강아지’ 클래스가 들어 있다고 표시한다면 ‘강아지’는 사진 #1234의 레이블이 된다.
    • 참 값(ground-truth) 또는 꼬리표(annotation): 데이터셋에 대한 모든 타깃. 일반적으로 사람에 의해 수집된다.
    • 이진 분류: 각 입력 샘플이 2개의 배타적인 범주로 구분되는 분류 작업
    • 다중 분류: 각 입력 샘플이 2개 이상의 범주로 구분되는 분류 작업. 예컨대 손글씨 숫자 분류
    • 다중 레이블 분류: 각 입력 샘플이 여러 개의 레이블에 할당될 수 있는 분류 작업. 예컨대 하나의 이미지에 고양이와 강아지가 모두 들어 있을 때는 ‘고양이’ 레이블과 ‘강아지’ 레이블을 모두 할당해야 한다.
    • 스칼라 회귀: 타깃이 연속적인 스칼라 값인 작업. 주택 가격 예측이 그 예이다.
    • 벡터 회귀: 타깃이 연속적인 값의 집합인 작업. 예컨대 연속적인 값으로 이루어진 벡터이다. (이미지에 있는 경계 상자의 좌표 같은) 여러 개의 값에 대한 회귀를 한다면 벡터 회귀이다.
    • 미니 배치 또는 배치: 모델에 의해 동시에 처리되는 소량의 샘플 묶음(일반적으로 8개에서 128개 사이). 샘플 개수는 GPU의 메모리 할당이 용이하도록 2의 거듭제곱으로 하는 경우가 많다. 훈련할 때 미니 배치마다 한 번씩 모델의 가중치에 적용할 경사 하강법 업데이트 값을 계산한다.

머신 러닝 모델 평가

  • 3장에서 본 3개의 예제에서 데이터를 훈련 세트, 검증 세트, 테스트 세트로 나누었다. 훈련에 사용된 동일한 데이터로 모델을 평가하지 않은 이유는 과대적합 때문이다.
    • 훈련 데이터의 성능은 훈련이 진행될 수록 항상 증가되지만, 새로운 데이터에 대한 성능은 좋아지지 않는다. 또는 더 나빠진다.
  • 머신 러닝의 목표는 처음 본 데이터에서 잘 작동하는 일반화된 모델을 얻는 것이다. 여기서 과대적합은 주요 장애물이다.
    • 관측할 수 있는 것만 제어할 수 있으므로 모델의 일반화 성능에 대한 신뢰를 측정할 수 있는 방법이 아주 중요하다.
    • 다음 절에서 과대 적합을 완화하고 일반화를 최대화하기 위한 전략을 살펴보자.

훈련, 검증, 테스트 세트

  • 모델 평가의 핵심은 가용한 데이터를 항상 훈련, 검증, 테스트 3개의 세트로 나누는 것이다. 훈련 세트에서 모델을 훈련하고 검증 세트에서 모델을 평가한다. 모델을 출시할 준비가 되면 테스트 세트에서 최종적으로 딱 한 번 모델을 테스트한다.
  • 훈련 세트와 테스트 세트를 2개 사용하지 않는 이유는 모델을 개발할 때 항상 모델의 설정을 튜닝하기 때문이다.
    • 예컨대 층의 수나 층의 유닛 수를 선택한다. (이런 파라미터를 네트워크의 가중치와 구분하기 위해 하이퍼파라미터(hyperparameter)라고 부른다) 검증 세트에서 모델의 성능을 평가하여 이런 튜닝을 수행한다.
    • 본질적으로 이런 튜닝도 어떤 파라미터 공간에서 좋은 설정을 찾는 학습이다. 결국 검증 세트의 성능을 기반으로 모델의 설정을 튜닝하면 검증 세트로 모델을 직접 훈련하지 않더라도 빠르게 검증 세트에 과대적합될 수 있다.
  • 이 현상의 핵심은 정보 누설(information leak) 개념에 있다. 
    • 검증 세트의 모델 성능에 기반하여 모델의 하이퍼파라미터를 조정할 때마다 검증 데이터에 관한 정보가 모델로 새는 것이다.
    • 하나의 파라미터에 대해서 단 한번만 튜닝한다면 아주 적은 정보가 누설된다. 이런 검증 세트로는 모델을 평가할만하다.
    • 하지만 한 번 튜닝하고 나서 검증 세트에 평가한 결과를 가지고 다시 모델을 조정하는 과정을 여러 번 반복하면 검증 세트에 관한 정보를 모델에 아주 많이 노출시키게 된다.
    • 결국 검증 데이터에 맞추어 최적화했기 때문에 검증 데이터에 의도적으로 잘 수행되는 모델이 만들어진다.
    • 검증 데이터가 아니고 완전히 새로운 데이터에 대한 성능이 관심 대상이라면 모델을 평가하기 위해 이전에 본 적 없는 완전히 다른 데이터셋을 사용해야 한다. 바로 테스트 세트이다.
    • 모델은 간접적으로라도 테스트 세트에 대한 어떤 정보도 얻어서는 안된다. 테스트 세트 성능에 기초하여 튜닝한 모델의 모든 설정은 일반화 성능을 왜곡시킬 것이다.
  • 데이터를 훈련, 검증, 테스트 세트로 나누는 것은 간단해 보일 수 있지만, 데이터가 적을 때는 몇 가지 고급 기법을 사용하면 도움이 된다. 대표적인 세 가지 평가 방법인 단순 홀드아웃 검증(hold-out validation), K-겹 교차 검증(K-fold cross-validation), 셔플링(shuffling)을 사용한 K-겹 교차 검증(iterated K-fold cross-validation)을 살펴보겠다.

단순 홀드아웃 검증

  • 데이터의 일정량을 테스트 세트로 떼어 놓는다. 남은 데이터에서 훈련하고 테스트 세트로 평가한다.
    • 앞서 설명했듯이 정보 누설을 막기 위해 테스트 세트를 사용해서 모델을 튜닝해서는 안되기 때문에 검증 세트도 따로 떼어 놓아야 한다.
    • 홀드아웃 검증은 아래 그림과 같다.

num_validation_samples = 10000

np.random.shuffle(data)

validation_data = data[:num_validation_samples]
data = data[num_validation_samples:]

training_data = data[:]

# 훈련세트에서 모델을 훈련하고 검증 세트로 평가한다.
model = get_model()
model.train(training_data)
validation_score = model.evaluate(validation_data)

# 여기에서 모델을 튜닝하고
# 다시 훈련하고 평가하고, 다시 튜닝하고...

# 하이퍼파라미터 튜닝이 끝나면 테스트 데이터를 제외한 모든 데이터를 사용하여 모델을 다시 훈련시킨다.
model = get_model()
model.train(np.concatenate([training_data, validation_data]))
test_score = model.evaluate(test_data)
  • 이 평가 방법은 단순해서 한 가지 단점이 있는데, 데이터가 적을 때는 검증 세트와 테스트 세트의 샘플이 너무 적어 주어진 전체 데이터를 통계적으로 대표하지 못할 수 있다.
    • 이를 쉽게 확인하는 방법은 다른 난수 초깃값으로 셔플링해서 데이터를 나누었을 때 모델의 성능이 매우 달라지는지를 확인하는 것이다.
    • 이 문제는 이어서 설명할 K-겹 교차 검증과 반복 K-겹 교차 검증이 해결할 수 있다.

K-겹 교차 검증

  • 이 방식에서는 데이터를 동일한 크기를 가진 K개 분할로 나눈 후, 각 분할 i에 대해 남은 K-1개의 분할로 모델을 훈련하고 분할 i에서 모델을 평가한다. 최종 점수는 이렇게 얻은 K개의 점수를 평균한다.
    • 이 방법은 모델의 성능이 데이터 분할에 따라 편차가 클 때 도움이 된다.
    • 홀드아웃 검증처럼 모델의 튜닝에 별개의 검증 세트를 사용하게 된다.

k = 4
num_validation_samples = len(data) // k

np.random.shuffle(data)

validation_scores = []

for fold in range(k):
# 검증 데이터 부분을 선택한다.
validation_data = data[num_validation_samples * fold: num_validation_samples * (fold+1)]
# 남은 데이터를 훈련 데이터로 사용한다.
training_data = data[:num_validation_samples * fold] + data[num_validation_samples * (fold+1):]

# 훈련되지 않은 새로운 모델을 만든다.
model = get_model()
model.train(training_data)
validation_score = model.evaluate(validation_data)
validation_scores.append(validation_score)

# 검증 점수: K개 폴드의 검증 점수 평균
validation_score = np.average(validation_scores)

# 테스트 데이터를 제외한 전체 데이터로 최종 모델을 훈련한다.
model = get_model()
model.train(data)
test_score = model.evaluate(test_data)

셔플링을 사용한 반복 K-겹 교차 검증

  • 이 방법은 비교적 가용 데이터가 적고 가능한 정확하게 모델을 평가하고자 할 때 사용한다. 캐글 경연에서는 이 방법이 아주 크게 도움이 된다.
    • 이 방법은 K-겹 교차 검증을 여러 번 적용하되 K개의 분할로 나누기 전에 매번 데이터를 무작위로 섞는다.
    • 최종 점수는 모든 K-겹 교차 검증을 실행해서 얻은 점수의 평균이 된다.
    • 결국 P x K개(P는 반복 횟수)의 모델을 훈련하고 평가하므로 비용이 매우 많이 든다.

기억해야 할 것

  • 평가 방식을 선택할 때 다음 사항을 유념해야 한다.
    • 대표성 있는 데이터
      • 훈련 세트와 테스트 세트가 주어진 데이터에 대한 대표성이 있어야 한다. 숫자 이미지 분류하는 문제에서 80:20으로 훈련과 테스트 세트를 만들면 훈련 세트에는 0-7까지의 숫자가 있고 테스트 세트에는 8-9 숫자가 담기게 된다. 이런 어처구니 없는 실수는 놀랍게도 자주 일어난다.
    • 시간의 방향
      • 과거로부터 미래를 예측하고자 한다면 데이터를 분할하기 전에 무작위로 섞어서는 안된다. 이러면 미래의 정보가 누설되기 때문이다. 즉 모델이 사실상 미래 데이터에서 훈련될 것이다.
    • 데이터 중복
      • 한 데이터셋에서 어떤 데이터 포인트가 두 번 등장하면 데이터를 섞고 훈련 세트와 검증 세트로 나누었을 때 훈련 세트와 검증 세트에 데이터 포인트가 중복될 수 있다. 이로 인해 훈련 데이터의 일부로 테스트하는 최악의 경우가 발생한다.

데이터 전처리, 특성 공학, 특성 학습

  • 많은 데이터 전처리와 특성 공학 기법은 특정 도메인에 종속적이기 때문에, 여기서는 모든 종류의 데이터에 공통되는 기본 사항을 살펴보겠다.

신경망을 위한 데이터 전처리

  • 데이터 전처리 목적은 주어진 원본 데이터를 신경망에 적용하기 쉽도록 만드는 것이다. 벡터화(vectorization), 정규화(normalization), 누락된 값 다루기, 특성 추출 등이 포함된다.

벡터화

  • 신경망에서 모든 입력과 타깃은 부동 소수 데이터로 이루어진 텐서여야 한다. (때로는 정수로 된 텐서)
  • 사운드, 이미지, 텍스트 등 처리해야 할 것이 무엇이든 먼저 텐서로 변환해야 하는데, 이 단계를 데이터 벡터화(data vectorization)이라고 한다.

값 정규화

  • 숫자 이미지 분류 예에서 이미지 데이터를 그레이스케일 인코딩인 0-255 사이의 정수로 인코딩했는데, 이 데이터를 네트워크에 주입하기 전에 float32 타입으로 변경하고 255로 나누어서 0-1사이의 부동 소수 값으로 만들었다.
  • 주택 가격을 예측할 때는 특성들의 범위가 제각각이었기 때문에, 이 데이터를 네트워크에 주입하기 전에 각 특성을 독립적으로 정규화하여 평균이 0이고 표준 편차가 1이 되도록 만들었다.
  • 일반적으로 비교적 큰 값이나 균일하지 않은 데이터를 신경망에 주입하는 것은 위험한데, 업데이트할 그래디언트가 커져 네트워크가 수렴하는 것을 방해하기 때문이다.
  • 네트워크가 쉽게 학습시키려면 데이터가 다음 특징을 따라야 한다.
    • 작은 값을 취한다. 일반적으로 대부분의 값이 0-1 사이여야 한다.
      • (0-1사이의 값은 계속 곱하면 수렴하는 이유도 있을 듯. 1이 넘으면 발산할 가능성이 높다)
    • 균일해야 한다. 즉 모든 특성이 대체로 비슷한 범위를 가져야 한다.
  • 추가적으로 다음에 나오는 엄격한 정규화 방법은 꼭 필수적이지는 않지만 자주 사용되고 도움이 될 수 있다.
    • 각 특성별로 평균이 0이 되도록 정규화한다.
    • 각 특성별로 표준 편차가 1이 되도록 정규화한다.
    • (이말은 데이터가 scale-free해서 평균과 표준편차 정규화가 안되는 데이터는 다룰 수 없다는 뜻인가?)
  • 넘파이 배열에서는 다음처럼 간단히 할 수 있다.
x -= x.mean(axis=0)
x /= x.std(axis=0)

누락된 값 다루기

  • 이따금 데이터에 값이 누락된 경우가 있다.
    • 예컨대 주택 가격 예측 문제에서 첫 번째 특성은 1인당 범죄율인데, 이 특성이 모든 샘플에 들어 있지 않은 경우 어떻게 될까? 훈련 데이터나 테스트 데이터에 누락된 값이 포함된다.
    • 일반적으로 신경망에서 0이 사전에 정의된 의미 있는 값이 아니라면 누락된 값을 0으로 입력해도 괜찮다. 네트워크가 0이 누락된 데이터를 의미한다는 것을 학습하면 이 값을 무시하기 싲가할 것이다.
  • 테스트 데이터에 누락된 값이 포함될 가능성이 있다고 가정하자. 하지만 네트워크가 누락된 값이 없는 데이터에서 훈련되었다면 이 네트워크는 누락된 값을 무시하는 법을 알지 못한다.
    • 이런 경우에는 누락된 값이 있는 훈련 샘플을 고의적으로 만들어야 한다. 훈련 샘플의 일부를 여러벌 복사해서 테스트 데이터에서 빠질 것 같은 특성을 제거한다.

특성 공학

  • 특성 공학은 데이터와 머신 러닝 알고리즘(여기서는 신경망)에 관한 지식을 사용하는 단계이다.
    • 모델에 데이터를 주입하기 전에 (학습이 아닌) 하드코딩된 변환을 적용하여 알고리즘이 더 잘 수행되도록 만들어준다.
    • 많은 경우에 머신 러닝 모델이 임의의 데이터로부터 완벽한 학습을 한다고 기대하기는 어렵다. 모델이 수월하게 작업할 수 있는 어떤 방식으로 데이터가 표현될 필요가 있다.
  • 예컨대 시계 이미지를 입력으로 받고 하루의 시간을 출력하는 모델을 개발한다고 가정하자. 
    • 이미지의 원본 픽셀을 입력으로 사용한다면 어려운 머신 러닝 문제가될 것이다.

  • 고수준에서 이 문제를 이해하고 있다면(우리는 시계에서 시간을 읽는 방법을 알고 있다) 머신 러닝 알고리즘을 위해 훨씬 더 좋은 입력 특성을 만들 수 있다.
    • 예컨대 시계 바늘의 검은색 픽셀을 따라 각 바늘 끝의 (x, y) 좌표를 출력하는 간단한 파이썬 스크립트를 만든다. 그 다음 머신 러닝 알고리즘이 이 좌표와 적절한 시각을 연결하도록 쉽게 학습될 수 있다.
    • 이보다 더 좋은 특성을 만들 수도 있는데, 좌표를 바꾸어 (x, y) 포인트를 이미지 중심에 대한 극좌표로 나타낼 수 있다. 이제 각 시계 바늘의 각도가 입력이 된다.
    • 이렇게 특성을 준비하면 문제가 너무 쉬워져서 머신 러닝이 전혀 필요하지 않다. 간단한 반올림 연산과 딕셔너리 참조만으로 하루의 시간을 추정하기 충분하다.
  • 이것이 특성 공학의 핵심이다. 특성을 더 간단한 방식으로 표현하여 문제를 쉽게 만든다. 일반적으로 해당 문제를 아주 잘 이해하고 있어야 한다.
  • 딥러닝 이전에는 특성 공학이 아주 중요했다. 전통적인 얕은 학습 방법의 알고리즘들은 스스로 유용한 특성을 학습할 만큼 충분히 넓은 가설 공간을 가지고 있지 않다. 알고리즘에 데이터를 표현하는 방식에 성공 여부가 달려있다.
    • 예컨대 합성곱 신경망이 MNIST 숫자 이미지 분류 문제를 해결하기 전까지 전형적인 해결책은 하드코딩된 특성을 사용하는 것이었다.
    • 숫자 이미지에 있는 동신원의 수, 이미지에 있는 숫자의 높이, 픽셀 값의 히스토그램(histogram) 등이다.
  • 다행히 최근 딥러닝은 대부분 특성 공학이 필요하지 않다. 신경망이 자동으로 원본 데이터에서 유용한 특성을 추출할 수 있기 때문이다. 그렇다면 심층 신경망을 사용할 때 특성 공학에 대해 신경 쓰지 않아도 될것인가 하면 그것은 두 가지 이유로 그렇지 않다.
    • 좋은 특성은 적은 자원을 사용하여 문제를 더 멋지게 풀어낼 수 있다. 예컨대 시계 바늘을 읽는 문제에 합성곱 신경망을 사용하는 것은 어울리지 않다.
    • 좋은 특성은 더 적은 데이터로 문제를 풀 수 있다. 딥러닝 모델이 스스로 특성을 학습하는 능력은 가용한 훈련 데이터가 많을 때 발휘된다. 샘플의 개수가 적다면 특성에 있는 정보가 매우 중요해진다.

과대적합과 과소적합

  • 머신 러닝의 근본적인 이슈는 최적화와 일반화 사이의 줄다리기이다. 최적화(optimization)는 가능한 훈련 데이터에서 최고의 성능을 얻으려고 모델을 조정하는 과정이고, 일반화(generalization)은 훈련된 모델이 이전에 본 적 없는 데이터에서 얼마나 잘 수행되는지를 의미한다.
  • 훈련 초기에 최적화와 일반화는 상호 연관되어 있다. 훈련 데이터의 손실이 낮아질수록 테스트 데이터의 손실도 낮아진다. 이런 상황이 발생할 때 모델이 과소적합(underfitting)되었다고 말한다.
    • 이때는 모델의 성능이 계속 발전될 여지가 있다. 즉, 네트워크가 훈련 데이터에 있는 관련 특성을 모두 학습하지 못한 상태다.
  • 훈련 데이터에 여러 번 반복 학습하고 나면 어느 시점부터 일반화 성능이 더 높아지지 않고, 검증 세트의 성능이 멈추고 감소되기 시작한다. 즉 모델이 과대적합되기 시작한다.
    • 이는 훈련 데이터에 특화된 패턴을 학습하기 시작했다는 의미이다. 이 패턴은 새로운 데이터와 관련성이 적기 때문에 잘못된 판단을 하게 만든다.
  • 모델이 관련성이 없고 좋지 못한 패턴을 훈련 데이터에서 학습하지 못하도록 하려면 가장 좋은 방법은 더 많은 훈련 데이터를 모으는 것이다.
    • 더 많은 데이터에서 훈련된 모델은 자연히 일반화 성능이 더 뛰어나다.
    • 데이터를 더 모으는 것이 불가능할 때 차선책은 모델이 수용할 수 있는 정보의 양을 조절하거나 저장할 수 있는 정보에 제약을 가하는 것이다.
    • 네트워크가 적은 수의 패턴만 기억할 수 있다면 최적화 과정에서 가장 중요한 패턴에 집중하게 될 것이다. 이런 패턴은 더 나은 일반화 성능을 제공할 수 있다.
  • 이런 식으로 과대적합을 피하는 처리 과정을 규제(regularization)이라고 한다.

네트워크 크기 축소

  • 과대적합을 막는 가장 단순한 방법은 모델의 크기, 즉 모델에 있는 학습 파라미터의 수를 줄이는 것이다.
    • 파라미터의 수는 층의 수와 각 층의 유닛 수에 의해 결정된다. 딥러닝에서 모델에 있는 학습 파라미터의 수를 종종 모델의 용량(capacity)라고 한다.
    • 당연하게도 파라미터가 많은 모델이 기억 용량이 더 많다. 훈련 샘플과 타깃 사이를 딕셔너리 같은 일대일 매핑으로 완벽하게 학습할 수도 있다. 이런 매핑은 일반화 능력이 없다.
    • 항상 유념해야 할 것은 딥러닝 모델은 훈련 데이터에 잘 맞추려는 경향이 있다는 점이다. 하지만 진짜 문제는 최적화가 아니고 일반화이다.
  • 다른 한편으로 네트워크가 기억 용량에 제한이 있다면 이런 매핑을 쉽게 학습하지 못할 것이다. 따라서 손실을 최소화하기 위해 타깃에 대한 예측 성능을 가진 압축된 표현을 학습해야 한다. 정확히 이런 표현이 우리 관심 대상이다.
    • 동시에 기억해야 할 것은 과소적합되지 않도록 충분한 파라미터를 가진 모델을 사용해야 한다는 점이다. 모델의 기억 용량이 부족해서는 안 된다.
  • 너무 많은 용량과 충분하지 않은 용량 사이의 절충점을 찾아야 한다. 하지만 알맞은 층의 수나 각 층의 유닛 수를 결정할 수 있는 마법 같은 공식은 없다.
    • 데이터에 알맞은 모델 크기를 찾으려면 각기 다른 구조를 평가해 보아야 한다.
    • 적절한 모델 크기를 찾는 일반적인 작업 흐름은 비교적 적은 수의 층과 파라미터로 싲가한다. 그 다음 검증 손실이 감소되기 시작할 때까지 층이나 유닛의 수를 늘리는 것이다.
  • 영화 리뷰 분류 모델을 예로 들어보자. 원래 네트워크는 다음과 같다.
from keras import models
from keras import layers

model = models.Sequential()
model.add(layers.Dense(16, activation='relu', input_shape=(10000,)))
model.add(layers.Dense(16, activation='relu'))
model.add(layers.Dense(1, activation='sigmoid'))
  • 더 작은 네트워크로 바꾸어보자
model = models.Sequential()
model.add(layers.Dense(16, activation='relu', input_shape=(10000,)))
model.add(layers.Dense(16, activation='relu'))
model.add(layers.Dense(1, activation='sigmoid'))
  • 아래 그림은 원본 네트워크와 축소된 네트워크의 검증 손실을 비교한 것이다. 점으로 표현한 것이 작은 네트워크고 덧셈 기호가 원래 네트워크이다. (검증 손실이 작은 것이 좋은 모델이다)

  • 그림에서 볼 수 있듯이 작은 네트워크가 기본 네트워크보다 더 나중에 과대적합되기 시작했다. 과대적합이 시작되었을 때 성능이 더 천천히 감소되었다.
  • 이번에는 훨씬 더 많은 용량을 가진 네트워크를 비교해 보자.
model = models.Sequential()
model.add(layers.Dense(1024, activation='relu', input_shape=(10000,)))
model.add(layers.Dense(1024, activation='relu'))
model.add(layers.Dense(1, activation='sigmoid'))
  • 아래 그림은 더 큰 네트워크가 기본 네트워크에 비해 얼마나 차이 나는지 보여준다.

  • 욜량이 큰 네트워크는 첫 번째 에포크 이후 거의 바로 과대적합이 시작되어 갈수록 더 심해진다. 검증 손실도 매우 불안정해진다.
  • 한편 아래 그림은 두 네트워크의 훈련 손실을 보여준다. 여기서 볼 수 있듯이 용량이 큰 네트워크는 훈련 손실이 매우 빠르게 0에 가까워진다.
    • 용량이 많은 네트워크일수록 더 빠르게 훈련 데이터를 모델링할 수 있다.(결국 훈련 손실이 낮아진다)
    • 하지만 더욱 과대적합에 민감해진다. (결국 훈련과 검증 손실 사이에 큰 차이가 발생한다.)

가중치 규제 추가

  • 오캄의 면도날(Occam’s razor)에 따르면 더 적은 가정이 필요한 간단한 설명이 옳다. 이는 신경망으로 학습되는 모델에도 적용된다.
    • 어떤 훈련 데이터와 네트워크 구조가 주어졌을 때 데이터를 설명할 수 있는 가중치 값의 집합은 여러 개이다. 간단한 모델이 복잡한 모델보다 덜 과대적합될 가능성이 높다.
  • 여기에서 간단한 모델은 파라미터 값 분포의 엔트로피가 작은 모델이다. 그러므로 과대적합을 완화하기 위한 일반적인 방법은 네트워크 복잡도에 제한을 두어 가중치가 작은 값을 가지도록 강제하는 것이다.
    • 가중치 값의 분포가 더 균일하게 된다. 이를 가중치 규제(weight regularization)이라고 하며, 네트워크의 손실 함수에 큰 가중치에 연관된 비용을 추가한다. 두가지 형태의 비용이 있다.
    • L1 규제: 가중치의 절댓값에 비례하는 비용이 추가된다. (가중치의 L1 노름(norm))
    •  L2 규제: 가중치의 제곱에 비례하는 비용이 추가된다. (가중치의 L2 노름). L2 규제는 신경망에서 가중치 감쇠(weight decay)라고 한다. 가중치 감쇠는 수학적으로 L2 규제와 동일하다.
  • 케라스에서 가중치 규제 객체를 층의 키워드 매개변수로 전달해서 가중치 규제를 추가할 수 있다. 다음의 예를 보자.
from keras import regularizers

model = models.Sequential()
model.add(layers.Dense(16, kernel_regularizer=regularizers.l2(0.001), activation='relu', input_shape=(10000,)))
model.add(layers.Dense(16, kernel_regularizer=regularizers.l2(0.001), activation='relu'))
model.add(layers.Dense(1, activation='sigmoid'))
  • l2(0.001)는 가중치 행렬의 모든 원소를 제곱하고 0.001을 곱하여 네트워크 전체 손실에 더해진다는 의미이다. 이 페널티(penalty) 항은 훈련할 때만 추가된다. 이 네트워크의 손실은 테스트보다 훈련할 때 더 높을 것이다.
  • 아래 그림은 L2 규제 페널티의 효과를 보여준다. 여기서 볼 수 있듯이 두 모델이 동일한 파라미터 수를 가지고 있더라도 L2 규제를 사용한 모델이 기본 모델보다 훨씬 더 과대적합에 잘 견디고 있다.

  • 케라스에서는 L2 규제 대신 다음 가중치 규제 중 하나를 사용할 수 있다.
from keras import regularizers

regularizers.l1(0.001) -- L1 규제
regularizers.l1_l2(l1=0.001, l2=0.001) -- L1과 L2 규제 병행

드롭아웃 추가

  • 드롭아웃(dropout)은 토론토 대학의 제프리 힌튼과 그의 학생들이 개발한 것으로 신경망을 위해 사용되는 규제 기법 중 가장 효과적이고 널리 사용되는 방법 중 하나이다.
    • 네트워크 층에 드롭아웃을 적용하면 훈련하는 동안 무작위로 층의 일부 출력 특성을 제외시킨다(0으로 만든다)
  • 한 층이 정상적으로 훈련하는 동안에는 어떤 입력 샘플에 대해 [0.2, 0.5, 1.3, 0.8, 1.1] 벡터를 출력한다고 가정해보자. 드롭아웃을 적용하면 이 벡터의 일부가 무작위로 0으로 바뀐다.
    • 드롭아웃의 비율은 0이 될 특성의 비율이다. 보통 0.2-0.5 사이로 지정된다.
    • 테스트 단계에서는 어떤 유닛도 드롭아웃되지 않는다. 그 대신 층의 출력을 드롭아웃 비율에 비례해서 줄여준다. 훈련할 때보다 더 많은 유닛이 활성화 되기 때문이다.
  • 크기가 (batch_size, features)인 한 층의 출력을 담고 있는 넘파이 행렬을 생각해보자. 훈련할 때는 이 행렬 값의 일부가 랜덤하게 0이 된다.
# 훈련할 때 유닛의 출력 중 50%를 버린다.
layer_output += np.random.randint(0, high=2, size=layer_output.shape)
  • 테스트할 때는 드롭아웃 비율로 출력을 낮추어 주어야 한다. 여기서 0.5배만큼 스케일을 조정했다. (앞에서 절반의 유닛을 드롭아웃 했으므로)
layer_output *= 0.5  --테스트 단계
  • 훈련 단계에 이 두 연산을 포함시켜 테스트 단계에는 출력을 그대로 두도록 구현할 수 있다. 실제로 종종 이런 방식으로 구현한다.
layer_output += np.random.randint(0, high=2, size=layer_output.shape) --훈련 단계
layer_output /= 0.5 --여기서는 스케일을 낮추는 대신 높인다.

  • 이 기법이 이상하고 무계획적으로 보일 수 있다. 왜 드롭아웃이 과대적합을 줄이는데 도움이 될까?
    • 힌튼은 은행에서 사용하는 부정 방비 메커니즘에서 착안했다고 한다. 
    • “은행에 갔을 때 행원들이 계속 바뀌길래 왜 그런지 물었더니, ‘자신들도 이유는 모르지만 자주 업무가 바뀐다’고 했다. 나는 은행에서 부정 행위를 하려면 직원들 사이의 유대가 필요하기 때문이라고 판단했다. 각 샘플에 대해 뉴런의 일부를 무작위하게 제거하면 뉴런의 부정한 협업을 방지하고 결국 과대적합을 감소시킨다는 것을 깨달았다.”
    • 핵심 아이디어는 층의 출력 값에 노이즈를 추가하여 중요하지 않은 우연한 패턴(힌튼이 말한 부정한 협업)을 깨드리는 것이다.
    • 노이즈가 없다면 네트워크가 이 패턴을 기억하기 시작할 것이다.
  • 케라스에서는 층의 바로 뒤에 Dropout 층을 추가하여 네트워크에 드롭아웃을 적용할 수 있다.
model.add(layers.Dropout(0.5))
  • IMDB 네트워크에 2개의 Dropout 층을 추가하고 과대적합을 얼마나 줄여주는지 확인해 보자.
model = models.Sequential()
model.add(layers.Dense(1024, activation='relu', input_shape=(10000,)))
model.add(layers.Dropout(0.5))
model.add(layers.Dense(1024, activation='relu'))
model.add(layers.Dropout(0.5))
model.add(layers.Dense(1, activation='sigmoid'))
  • 아래는 결과 그래프이다. 여기서도 기본 네트워크보다 확실히 더 향상된 것을 볼 수 있다.

  • 정리하면 신경망에서 과대적합을 방지하기 위해 가장 널리 사용되는 방법은 다음과 같다.
    • 훈련 데이터를 더 모은다.
    • 네트워크의 용량을 감소시킨다.
    • 가중치 규제를 추가한다.
    • 드롭아웃을 추가한다.

보편적인 머신 러닝 작업 흐름

문제 정의와 데이터셋 수집

  • 먼저 주어진 문제를 정의해야 한다.
    • 입력데이터는 무엇이고 어떤 것을 예측하려고 하고 가용한 훈련 데이터가 있어야 하고 등
    • 당면한 문제가 어떤 종류인지, 이진분류인지 다중 분류인지 스칼라 회귀인지 벡터 회귀인지, 다중 레이블 다중 분류인지, 군집, 생성 또는 강화 학습 문제인지
  • 입력과 출력이 무엇인지와 어떤 데이터를 사용할 것인지 알기 전까지는 다음 단계로 넘어갈 수 없다. 이 단계에서 가설을 세워야 한다.
    • 주어진 입력으로 출력을 예측할 수 있다고 가설을 세운다.
    • 가용한 데이터에 입력과 출력 사이의 관계를 학습하는데 충분한 정보가 있다고 가설을 세운다.
  • 모델이 작동하기 전까지 이는 가설에 불과하기 때문에 검증될지 아닐지 기다려 보아야 한다. 모든 문제가 해결되지는 않는다.
  • 풀기 어려운 종류의 문제는 시간에 따라 변하는 문제(nonstationary problem)이다. 
    • 옷을 위한 추천 엔진을 예로 들어보자. 8월 데이터로 훈련하고 겨울에 이 엔진을 사용하려고 하면 결과가 좋게 나올 수가 없다.
  • 머신 러닝은 훈련 데이터에 있는 패턴을 기억하기 위해서만 사용한다는 것을 기억하라. 이미 보았던 것만 인식할 수 있다. 미래를 예측하기 위해 과거 데이터에서 훈련한 머신 러닝을 사용하는 것은 미래가 과거처럼 움직인다고 가정한 것이다. 사실 대부분 그렇지 않다.

성공 지표 선택

  • 어떤 것을 제어하려면 관측할 수 있어야 한다. 성공 하기 위해서는 성공은 무엇인가를 정의해야 한다. 
    • 정확도, 정밀도, 재현율, 재방문율 등 성공의 지표가 모델이 최적화할 손실 함수를 선택하는 기준이 된다. 비즈니스 성공처럼 고수준의 목표와 직접적으로 연결되어 있어야 한다.
  • 클래스 분포가 균일한 분류 문제에서는 정확도와 ROC AUC가 일반적인 지표이다.
    • 클래스 분포가 균일하지 않은 문제에서는 정밀도와 재현율을 사용할 수 있다.
    • 랭킹 문제나 다중 레이블 문제에는 평균 정밀도를 사용할 수 있다.
    • 성공을 측정하기 위해 자신만의 지표를 정의하는 일은 일반적이지 않다.

평가 방법 선택

  • 목표를 정했다면 현재의 진척 상황을 평가할 방법을 정의해야 한다. 
    • 홀드아웃 검증 세트 분리: 데이터가 풍부할 때 사용한다.
    • K-겹 교차 검증: 홀드아웃 검증을 사용하기에 샘플의 수가 너무 적을 때 사용한다.
    • 반복 K-겹 교차 검증: 데이터가 적고 매우 정확한 모델 평가가 필요할 때 사용한다.
  • 위 3가지 중 하나를 선택하면 된다. 대부분의 경우 첫 번째로 충분할 것이다.

데이터 준비

  • 무엇을 훈련할지와 무엇을 최적화할지, 그리고 어떻게 평가할지를 정했다면 거의 모델을 훈련 시킬 준비가 된 것이다. 하지만 머신 러닝 모델에 주입할 데이터를 구성해야 한다. 여기에서는 이 머신 로닝 모델을 심층 신경망이라고 가정한다.
    • 데이터는 텐서로 구성된다.
    • 이 텐서에 있는 값은 일반적으로 작은 값으로 스케일 조정되어 있다. 예컨대 [-1, 1] 이나 [0, 1] 범위이다.
    • 특성마다 범위가 다르면 (여러 종류의 값으로 이루어진 데이터라면) 정규화해야 한다.
    • 특성 공학을 수행할 수 있다. 특히 데이터가 적을 때이다.
  • 입력 데이터와 타깃 데이터의 텐서가 준비되면 모델을 훈련시킬 수 있다.

기본보다 나은 모델 훈련하기

  • 이 단계의 목표는 통계적 검정력(statistical power)을 달성하는 것이다. 즉 아주 단순한 모델보다 나은 수준의 작은 모델을 개발한다.
    • MNIST 숫자 이미지 분류 예에서는 0.1보다 높은 정확도를 내는 모델이 통계적 검정력을 가졌다고 말할 수 있다.
    • IMDB 에서는 0.5보다 높은 정확도를 갖는 것이다.
  • 통계적 검정력을 달성하는 것이 항상 가능하지는 않다. 여러 개의 타당성이 있는 네트워크 구조를 시도해 보고 무작위로 예측하는 모델보다 낫지 않다면 입력 데이터에 존재하지 않는 것을 얻으려고 한다는 신호이다. 2개의 가설이 있다는 것을 기억하라.
    • 주어진 입력으로 출력을 예측할 수 있다고 가설을 세운다.
    • 가용한 데이터에 입력과 출력 사이의 관계를 학습하는데 충분한 정보가 있다고 가설을 세운다.
  • 이 가설이 잘못된 것일 수 있다. 이때는 기획부터 다시 해야 한다.
  • 일이 잘 진행된다고 가정하면 첫 번째 모델을 만들기 위해 세 가지 중요한 선택을 해야 한다.
    • 마지막 층의 활성화 함수: 네트워크의 출력에 필요한 제한을 가한다. 예컨대 IMDB 분류 예는 마지막 층에 시그모이드 함수를 사용한다. 회귀 예에서는 마지막 층에 활성화 함수를 사용하지 않는다.
    • 손실 함수: 풀려고 하는 문제의 종류에 적합해야 한다. 예컨대 IMDB 예제는 binary_crossentropy를 사용하고, 회귀 예제는 mse를 사용하는 식이다.
    • 최적화 설정: 어떤 옵티마이저를 사용하는지? 학습률은 얼마인지? 대부분의 경우 rmsprop과 기본 학습률을 사용하는 것이 무난하다.
  • 손실 함수의 선택에 대해서 언급할 것은 주어진 문제의 성공 지표를 직접 최적화하는 것이 항상 가능하지는 않다는 점이다. 때로는 이 지표를 손실 함수로 바꿀 수 있는 방법이 없다.
    • 무엇보다도 손실 함수는 주어진 미니 배치 데이터에서 계산 가능해야 하고,(이상적으로 손실 함수는 하나의 데이터 포인트에서 계산 가능해야 한다) 미분 가능해야 한다.(그렇지 않으면 역전파 알고리즘을 사용하여 네트워크를 훈련시킬 수 없다.)
    • 예컨대 널리 사용되는 분류 지표인 ROC AUC는 직접 최적화될 수 없다. 그래서 분류 작업에는 크로스엔트로피처럼 ROC AUC를 대신할 지표를 최적화하는 것이 보통이다. 일반적으로 크로스엔트로피가 낮을수록 ROC AUC가 높다고 기대할 수 있다.
  • 다음 표는 자주 등장하는 문제 유형에 따라 선택할 수 있는 마지막 츠으이 활성화 함수와 손실 함수이다.
문제 유형 마지막 층의 활성화 함수 손실 함수
이진 분류 시그모이드 binary_crossentropy
단일 레이블 다중 분류 소프트맥스 categorical_crossentropy
다중 레이블 다중 분류 시그모이드 binary_crossentropy
임의 값에 대한 회귀 없음 mse
0과 1 사이 값에 대한 회귀 시그모이드 mse 또는 binary_crossentropy

몸집 키우기: 과대적합 모델 구축

  • 통계적 검정력을 가진 모델을 얻었다면 이제 이 모델이 충분히 성능을 내는지 질문해 보아야 한다. 주어진 문제를 적절히 모델링하기에 충분한 층과 파라미터가 있는가?
    • 예컨대 2개의 유닛을 가진 하나의 은닉 층으로 구성된 네트워크가 있다고 가정하자. 이 네트워크가 MNIST 데이터셋에서 통계적 검정력을 가질 수 있지만 문제를 잘 해결하기에는 충분하지 않을 것이다.
    • 머신러닝은 최적화와 일반화 사이의 줄다리기라는 점을 기억하라. 과소적합과 과대적합 사이, 즉 과소용량과 과대용량의 경계에 적절히 위치한 모델이 이상적이다.
    • 이 경계가 어디에 위치하는지 찾기 위해서는 먼저 지나쳐 보아야 한다.
  • 얼마나 큰 모델을 만들어야 하는지 알기 위해서는 과대적합된 모델을 만들어야 한다. 이는 아주 쉽다.
    • 층을 추가한다.
    • 층의 크기를 키운다.
    • 더 많은 에포크 동안 훈련한다.
  • 관심 대상인 훈련과 검증 지표는 물론 항상 훈련 손실과 검증 손실을 모니터링하라. 검증 데이터에서 모델 성능이 감소하기 시작했을 때 과대적합에 도달한 것이다.
  • 다음 단계에서 규제와 모델 튜닝을 싲가하여 과소적합도 아니고 과대적합도 아닌 이상적인 모델에 가능한 가깝도록 만든다.

모델 규제와 하이퍼파라미터 튜닝

  • 이 단계가 대부분의 시간을 차지한다. 반복적으로 모델을 수정하고 훈련하고 검증 데이터에서 평가한다 (이때 테스트 데이터를 사용하지 않는다). 그리고 다시 수정하고 가능한 좋은 모델을 얻을 때까지 반복한다. 적용해 볼 것은 다음과 같다.
    • 드롭아웃을 추가한다.
    • 층을 추가하거나 제거해서 다른 구조를 시도해 본다.
    • L1이나 L2 또는 두 가지 모두 추가한다.
    • 최적의 설정을 찾기 위해 하이퍼파라미터를 바꾸어 시도해 본다. (층의 유닛 수나 옵티마이저의 학습률 등)
    • 선택적으로 특성 공학을 시도해 본다. 새로운 특성을 추가하거나 유용하지 않을 것 같은 특성을 제거한다.
  • 다음 사항을 유념하라. 검증 과정에서 얻은 피드백을 사용하여 모델을 튜닝할 때마다 검증 과정에 대한 정보를 모델에 누설하고 있다는 것이다.
    • 몇 번만 반복하는 것은 큰 문제가 되지 않지만, 많이 반복하게 되면 모델이 검증 데이터에서 전혀 훈련하지 않았음에도 결국 모델이 검증 과정에 과댖거합될 것이다. 이는 검증 과정의 신뢰도를 감소시킨다.
  • 만족할 만한 모델 설정을 얻었다면 가용한 모든 데이터(훈련 데이터와 검증 데이터)를 사용해서 제품에 투입할 최종 모델을 훈련 시킨다. 그리고 마지막에 딱 한 번 테스트 세트에서 평가한다.
    • 테스트 세트의 성능이 검증 데이터에서 측정한 것보다 많이 나쁘다면, 검증 과정에 전혀 신뢰성이 없거나 모델의 하이퍼파라미터를 튜닝하는 동안 검증 데이터에 과대적합된 것이다.
    • 이런 경우에는 좀 더 신뢰할만한 평가 방법으로 바꾸는 것이 좋다. (반복 K-겹 교차 검증)

요약

  • 주어진 문제와 훈련할 데이터를 정의한다. 이 데이터를 수집하고 필요하면 레이블을 태깅한다.
  • 성공을 어떻게 측정할지 선택하라. 검증 데이터에서 모니터링할 지표는 무엇인가?
  • 평가 방법을 결정하라. 홀드아웃 검증이나 K-겹 교차 검증인가? 검증에 사용해야 할 데이터의 양은 얼마나 되는가?
  • 단순한 랜덤 선택 모델보다 나은 통계적 검정력이 있는 첫 번째 모델을 만든다.
  • 과대적합된 모델을 만든다.
  • 검증 데이터의 성능에 기초하여 모델에 규제를 적용하고 하이퍼파라미터를 튜닝한다. 머신 러닝 연구의 대부분은 이 단계에 집중된다.

시장의 기억

시장의 기억

한국의 자본 시장 역사의 주요했던 사건을 다루는 책. 대한민국 이전의 일제시대에 있었던 쌀 거래 선물 사건부터 시작해서 최근의 저금리까지를 다룬다.

기자가 쓴 책 답게 일어났던 사건에 대해 재미있게 다루고 있기는 하지만, 다루는 용어나 경제 관련 뉴스의 맥락을 이해할 수 있는 수준은 되어야 책의 내용이 이해가 되는 것은 아쉽다. 

더불어 기자들이 쓴 책의 특성이 그러하듯, 근본 구조나 원리보다는 사건의 흐름만 다루고 있어서 읽고 나면 ‘그런 일이 있었구나. 재미있네’ 만 될 뿐 ‘그런 일이 일어나게 된 근본 원인은 무엇이었을까?’ 까지는 이해하기 쉽지 않다.

경제 뉴스의 맥락을 이해할 수 있다면 재미있게 읽을 수 있는 책

책의 결말 부분에서 다뤄지는 내용에 앞으로 한국 경제를 이끌어갈 힘에 대한 우려가 있어서 근래 내가 하고 있는 생각도 추가.

2000년대 초반 IMF 이후 한국이 지금과 비슷하게 앞으로의 한국 경제를 이끌어갈 성장 동력이 안 보여서 고민했다던 내용이 있어서 상당히 흥미로웠다.

다행히도 그 이후 반도체, 조선, 자동차, 철강, 석유화학과 같은 업종이 2000년 이후 한국 경제를 이끌었는데, 2008년 글로벌 금융위기 이후 조선, 철강 업체는 예전의 기세를 회복 못했고, 2020년을 맞이하는 지금 다시 2000년대 초처럼 앞으로 한국을 이끌어갈 업종이 보이지 않아 깊은 고민에 빠져 있는 시기. 더불어 회복될 기미가 보이지 않는 고용 사정 또한 고민에 깊이를 더하는 것 같다.

하지만 2000년대 초에도 그러했듯 항상 미래는 안개 속이고 막상 닥치면 우리를 견인해 줄 새로운 업종이 나타날 수 있으리라 생각한다. –현재 주도주가 되고 있는 소프트웨어/바이오 기업들이 그것을 해줘야 됨– 다만 그것이 저절로 이루어지지는 않을테고, 지금 부지런히 투자를 해둬야 나중에 결실을 맺을 수 있을 듯.

하나 개인적으로 흥미로운 지점은 현재 고용은 악화되어 가지만 동시에 창업 비용이 대단히 낮아지는 시대가 오고 있다는 것이다. 최근에 금리가 계속 내려가고 있다는 것도 그러하고, 창업 비용을 줄일 수 있는 다양한 솔루션이 나오고 있다는 것이 그러하다. 과거에는 공장을 지어야 가능했던 창업이 이제는 각종 창업 솔루션, 자동화 설비나 소프트웨어 등을 이용하면 이전보다 대단히 적은 비용으로 창업이 가능해졌다는 것.

다시 말해 이미 존재하는 기업에 고용을 늘리기를 기대하기 보다 –기업은 항상 비용을 최적화 하는 곳이라서 비즈니스가 확대되지 않는 이상 고용을 늘리기는 어렵다. 비용이 증가해서 기업의 존립이 위태로워질 수 있음– 창업을 통한 미래 기업의 출현을 기대 해야 한다는 것. 

당연히 수많은 기업이 실패하겠지만, 그렇게 양을 늘려야 10년 후, 20년 후에 네이버, 카카오, NC 소프트 같은 기업이 다시 나올 수 있다. 그리고 그 정도 규모와 업력을 가진 기업이 해외 진출해서 국가 경쟁력을 높일 수 있는 것. 보다 많은 사람들이 창업을 할 수 있도록 비용을 낮춰주고 실패에 대한 안정망을 갖춰주는 인프라를 갖추는 것이 결국 정부가 할 일이고, 기업은 그 인프라 위에서 미래를 이끌어갈 아이템을 발굴하는 것이 선순화 구조가 아닐까 싶다.

코로나 투자전쟁

코로나 투자 전쟁

개인적으로 매일 출퇴근 길에 듣는 삼프로 TV에서 출연진들을 모아 엮어낸 코로나 이후 경제 전망에 대한 책. 아무래도 타이밍 맞춰 낸 책이다 보니 현상에 대한 설명에 치우친 느낌이 들긴 하지만, 여튼 팬심으로 읽었다.

사실 전망이라는 것은 누구도 정확히 할 수 없는 것이고, 내 인생은 결국 내가 사는 것이지 남이 내 인생 살아주지 않기 때문에 -내 인생에 피와 살을 건 것은 나 자신이다– 남들의 이야기는 조언으로 듣고 그것들을 바탕으로 나의 행동을 결정하면 되지 않을까 싶다. 책의 내용도 여러 견해가 담겨 있기 때문에 참고를 하는 정도로 보면 될 듯.

개인적으로는 현재의 모습에 대해 2001년 IT 버블과 911이 겹쳤던 시기에 대한 설명이 가장 설득력 있게 느껴지던데, 산책 나온 집주인(경기)와 개(주식시장)의 비유에서도 단기적으로 개가 앞서갈 수 있지만, 결국 집주인과 개는 집에 함께 들어간다는 점을 생각해 본다면 현시점에 어떤 판단을 내려야 하는지는 스스로 할 수 있을 듯

케라스 창시자에게 배우는 딥러닝/ 신경망 시작하기

신경망의 구조

  • 신경망 훈련에는 다음 요소들이 관련되어 있다.
    • 네트워크(또는 모델)를 구성하는 층
    • 입력 데이터와 그에 상응하는 타깃
    • 학습에 사용할 피드백 신호를 정의하는 손실 함수
    • 학습 진행 방식을 결정하는 옵티마이저
  • 이들 간의 상호 작용은 아래 그림에 나타냈다.
    • 연속된 층으로 구성된 네트워크가 입력 데이터를 예측으로 매핑한다.
    • 손실 함수는 예측과 타깃을 비교하여 네트워크의 예측이 기댓값에 얼마나 잘 맞는지를 측정하는 손실 값을 만든다.
    • 옵티마이저는 손실 값을 사용하여 네트워크 가중치를 업데이트 한다.

층: 딥러닝의 구성 단위

  • 신경망의 핵심적인 데이터 구조는 층이다. 층은 하나 이상의 텐서를 입력으로 받아 하나 이상의 텐서를 출력하는 데이터 처리 모듈이다.
    • 어떤 종류의 층은 상태가 없지만 대부분의 경우 가중치라는 층의 상태를 가진다.
    • 가중치는 확률적 경사 하강법에 의해 학습되는 하나 이상의 텐서이며 여기에 네트워크가 학습한 지식이 담겨 있다.
  • 층마다 적절한 텐서 포맷과 데이터 처리 방식이 다르다.
    • 예컨대 (samples, features) 크기의 2D 텐서가 저장된 간단한 벡터 데이터는 완전 연결 층(fully connected layer)이나 밀집 층(dense layer)라고도 불리는 밀집 연결 층(densely connected layer)에 의해 처리되는 경우가 많다. (케라스에서는 Dense 클래스이다)
    • (samples, timesteps, features) 크기의 3D 텐서로 저장된 시퀀스 데이터는 보통 LSTM 같은 순환 층(recurrent layer)에 의해 처리된다.
    • 4D 텐서로 저장되어 있는 이미지 데이터는 일반적으로 2D 합성곱 층(convolution layer)에 의해 처리 된다. (Conv2D 클래스)
  • 층을 딥러닝의 레고 블록처럼 생각할 수 있다. 이런 비유는 케라스 같은 프레임워크 때문에 생겼는데, 케라스에서는 호환 가능한 층들을 엮어 데이터 변환 파이프라인(pipeline)을 구성함으로써 딥러닝 모델을 만든다.
    • 여기에서 층 호환성(layer compatibility)은 각 층이 특정 크기의 입력 텐서만 받고 특정 크기의 출력 텐서를 반환한다는 사실을 말한다.
  • 다음 예를 살펴 보자.
from keras import layers
layer = layers.Dense(32, input_shape=(784,)) --32개의 유닛으로 밀집된 층
  • 첫 번째 차원이 784인 2D 텐서만 입력으로 받는 층을 만들었다. (배치 차원인 0번째 축은 지정하지 않기 때문에 어떤 배치 크기도 입력으로 받을 수 있다.)
    • 이 층은 첫 번째 차원 크기가 32로 변환된 텐서를 출력할 것이다.
    • 따라서 이 층에는 32차원의 벡터를 입력으로 받는 하위 층이 연결되어야 한다. 케라스에서는 모델에 추가된 층을 자동으로 상위 층의 크기에 맞추어주기 때문에 호환성을 걱정하지 않아도 된다.
  • 예컨대 다음과 같이 작성 했다고 가정하자.
from keras import models
from keras import layers

model = models.Sequential()
model.add(layers.Dense(32, input_shape=(784,)))
model.add(layers.Dense(10))
  • 두 번째 층에는 input_shape 매개변수를 지정하지 않았는데, 이러면 앞선 층의 출력 크기를 입력 크기로 자동으로 채택한다.

모델: 층의 네트워크

  • 딥러닝 모델은 층으로 만든 비순환 유향 그래프(Directed Acyclic Graph, DAG)이다.
    • 가장 일반적인 예가 하나의 입력을 하나의 출력으로 매핑하는 층을 순서대로 쌓는 것이다.
  • 앞으로 공부하다 보면 아주 다양한 네트워크 구조를 보게 될 것이다. 자주 등장하는 것들은 다음과 같다.
    • 가지(branch)가 2개인 네트워크
    • 출력이 여러 개인 네트워크
    • 인셉션(Inception) 블록
  • 네트워크 구조는 가설 공간(hypothesis space)을 정의한다.
    • 1장에서 머신 러닝을 ‘가능성 있는 공간을 사전에 정의하고 피드백 신호의 도움을 받아 입력 데이터에 대한 유용한 변환을 찾는 것’으로 정의했는데, 네트워크 구조를 선택함으로써 가능성 있는 공간(가설 공간)을 입력 데이터에서 출력 데이터로 매핑하는 일련의 특정 텐서 연산으로 제한하게 된다.
    • 우리가 찾아야 할 것은 이런 텐서 연산에 포함도니 가중치 텐서의 좋은 값이다.
  • 딱 맞는 네트워크 구조를 찾아내는 것은 과학보다 예술에 가깝다. 신뢰할 만한 모범적인 사례와 원칙이 있지만 연습을 해야만 적절한 신경망을 설계할 수 있는 기술을 갖추게 될 것이다.
    • (여기서 과학은 이론적으로 구성 가능한 것을 의미하고, 예술은 훈련을 통해 향상 시키는 기예의 의미에 가깝다. 실제 Art에는 그런 의미가 포함 되어 있음. 예술 작품이 아니라)

손실 함수와 옵티마이저: 학습 과정을 조절하는 열쇠

  • 네트워크 구조를 정의하고 나면 두 가지를 더 선택해야 한다.
    • 손실 함수(loss function) (목적 함수(objective function)): 훈련하는 동안 최소화 될 값이다. 주어진 문제에 대한 성공 지표가 된다.
    • 옵티마이저(optimizer): 손실 함수를 기반으로 네트워크가 어떻게 업데이트될지 결정한다. 특정 종류의 확률적 경사 하강법(SGD)를 구현한다.
  • 여러 개의 출력을 내는 신경망은 여러 개의 손실 함수를 가질 수 있다. (출력당 하나씩).
    • 하지만 경사 하강법 과정은 하나의 스칼라 손실 값을 기준으로 한다. 따라서 손실이 여러 개인 네트워크에서는 모든 손실이 (평균을 내서) 하나의 스칼라 양으로 합쳐진다.
  • 문제에 맞는 올바른 목적 함수를 선택하는 것이 아주 중요하다. 네트워크가 손실을 최소화하기 위해 편법을 사용할 수 있기 때문이다.
    • 목적 함수가 주어진 문제의 성공과 전혀 관련이 없다면 원하지 않은 일을 수행하는 모델이 만들어질 것이다.
    • “모든 인류의 평균 행복 지수를 최대화하기” 같은 잘못된 목적 함수에서 SGD로 훈련된 멍청하지만 전지전능한 AI가 있다고 가정하자. 이 문제를 쉽게 해결하기 위해 이 AI는 몇 사람을 남기고 모든 인류를 죽여서 남은 사람들의 행복에 초점을 맞출 수 있다.
    • 우리가 만든 모든 신경망은 단지 손실 함수를 최소화하기만 한다는 것을 기억하라. 목적 함수를 현명하게 선택하지 않으면 원하지 않는 부수 효과가 발생할 것이다.
  • 다행히 분류, 회귀와 시퀀스 예측 같은 일반적인 문제에서는 올바른 손실 함수를 선택하는 간단한 지침이 있다.
    • 예컨대 2개의 클래스가 있는 분류 문제에는 이진 크로스엔트로피(binary crossentropy), 여러 개의 클래스가 있는 분류 문제에는 범주형 크로스엔트로피(categorical croessentropy), 회귀 문제에는 평균 제곱 오차, 시퀀스 학습 문제에는 CTC(Connection Temporal Classification) 등을 사용한다.
    • 완전히 새로운 연구를 할 때만 독자적인 목적 함수를 만들게 된다.

케라스 소개

  • 이 책에서는 코드 예제를 위해 케라스(https://keras.io)를 사용한다.
    • 케라스는 거의 모든 종류의 딥러닝 모델을 간편하게 만들고 훈련시킬 수 있는 파이썬을 위한 딥러닝 프레임워크이다.
    • 케라스는 MIT 라이선스를 따르고 있으므로 상업적인 프로젝트에도 자유롭게 사용할 수 있다.

케라스, 텐서플로, 씨아노, CNTK

  • 케라스는 딥러닝 모델을 만들기 위한 고수준의 구성 요소를 제공하는 모델 수준의 라이브러리이다. 텐서 조작이나 미분 같은 저수준의 연산을 다루지 않는다. 
    • 대신 케라스의 백엔드 엔진에서 제공하는 최적화되고 특화된 텐서 라이브러리를 사용한다.
    • 케라스는 하나의 텐서 라이브러리에 국한하여 구현되어 있지 않고 모듈 구조로 구성되어 있다.
    • 여러 가지 백엔드 엔진이 케라스와 매끄럽게 연동된다. 현재는 텐서플로, 씨아노, 마이크로소프트 코그니티브 툴킷(Microsoft Cognitive Toolkit, CNTK) 3개를 백엔드 엔진으로 사용할 수 있다.

  • 텐서플로, CNTK, 씨아노는 딥러닝을 위한 주요 플랫폼 중 하나이다.
    • 씨아노는 몬트리올 대학 MILA 연구소에서 개발했고, 텐서플로는 구글에서 개발했으며, CNTK는 마이크로소프트에서 개발했다.
    • 케라스로 작성한 모든 코드는 아무런 변경 없이 이런 백엔드 중 하나를 선택해서 실행시킬 수 있다.
    • 개발하는 중간에 하나의 백엔드가 특정 작업에 더 빠르다고 판단되면 언제든 백엔드를 바꿀 수 있다.
    • 가장 널리 사용되고 확장성이 뛰어나며 상용 제품에 쓸 수 있기 때문에 딥러닝 작업에 텐서플로 백엔드가 기본으로 권장된다.
  • 텐서플로(또는 씨아노, CNTK)를 사용하기 때문에 케라스는 CPU와 GPU에서 모두 작동될 수 있다.
    • CPU에서 실행될 때 텐서플로는 Eigen이라고 불리는 저수준 텐서 연산 라이브러리를 사용하고, GPU에서는 NVIDIA CUDA 심층 신경망 라이브러리(cuDNN)라고 불리는 고도로 최적화된 딥러닝 연산 라이브러리를 이용한다.

케라스를 사용한 개발: 빠르게 둘러보기

  • 이미 케라스 모델의 예로 MNIST 예제를 살펴보았다. 전형적인 케라스 작업 흐름은 이 예제와 비슷하다.
    1. 입력 텐서와 타깃 텐서로 이루어진 훈련 데이터를 정의한다.
    2. 입력과 타깃을 매핑하는 층으로 이루어진 네트워크(또는 모델)를 정의한다.
    3. 손실 함수, 옵티마이저, 모니터링하기 위한 측정 지표를 선택하여 학습 과정을 설정한다.
    4. 훈련 데이터에 대해 모델의 fit() 메서드를 반복적으로 호출한다.
  • 모델을 정의하는 방법은 두 가지인데, Sequential 클래스(가장 자주 사용하는 구조인 층을 순서대로 쌓아 올린 네트워크) 또는 함수형 API(완전히 임의의 구조를 만들 수 있는 비순환 유향 그래프)를 사용한다.
  • Sequential 클래스를 사용하여 정의한 2개의 층으로 된 모델을 다시 살펴보자.
from keras import models
from keras import layers

model = models.Sequential()
model.add(layers.Dense(32, activation='relu', input_shape=(784,)))
model.add(layers.Dense(10, activation='softmax'))
  • 같은 모델을 함수형 API를 사용해서 만들면 다음과 같다.
input_tensor = layers.Input(shape=(784,))
x = layers.Dense(32, activation='relu')(input_tensor)
output_tensor = layers.Dense(10, activation='softmax')(x)

model = models.Model(inputs=input_tensor, outputs=output_tensor)
  • 함수형 API를 사용하면 모델이 처리할 데이터 텐서를 만들고 마치 함수처럼 이 텐서에 층을 적용한다.
  • 모델 구조가 정의된 후에는 Sequential 모델을 사용했는지 함수형 API를 사용했는지 상관없이 이후 단계가 동일하다.
  • 컴파일 단계에서 학습 과정이 설정된다. 여기에서 모델이 사용할 옵티마이저와 손실 함수, 훈련 하는 동안 모니터링하기 위해 필요한 측정 지표를 지정한다.
  • 다음이 하나의 손실 함수를 사용하는 가장 흔한 경우의 예이다.
from keras import optimizers

model.compile(optimizer=optimizers.RMSprop(lr=0.001), loss='mse', metrics=['accuracy'])
  • 마지막으로 입력 데이터의 넘파이 배열을 모델의 fit() 메서드에 전달함으로써 학습 과정이 이루어진다.
    • 이는 사이킷런(Scikit-Learn)이나 몇몇 다른 머신 러닝 라이브러리에서 하는 방식과 비슷하다.
model.fit(input_tensor, target_tensor, batch_size=128, epochs=10)

딥러닝 컴퓨터 셋팅

  • 딥러닝 애플리케이션을 실행할 때는 NVIDIA GPU에서 실행하는 것을 권장한다.
  • 운영체제는 유닉스(Unix) 운영체제를 사용하는 것이 좋다.
    • 기술적으로 윈도우에서 케라스를 사용할 수 있지만 권장하지는 않는다.
    • 번거로울 수 있지만 우분투를 사용하면 장기적으로 시간이 절약되고 문제가 발생할 가능성이 적다.

주피터 노트북: 딥러닝 실험을 위한 최적의 방법

  • 주피터 노트북(Jupyter Notebook)은 데이터 과학과 머신 러닝 커뮤니티에서 폭넓게 사용된다.
    • 노트북은 주피터 노트북 애플리케이션(https://jupyter.org)으로 만든 파일이며 웹 브라우저에서 작성할 수 있다.
    • 작업 내용을 기술하기 위해 서식 있는 텍스트 포맷을 지원하며 파이썬 코드를 실행할 수 있는 기능도 있다.
    • 필수적이지는 않지만, 케라스를 배울 때 주피터 노트북을 사용할 것을 권장한다. 하지만 파이참(PyCharm) 같은 IDE에서 코드를 실행하거나 독립된 파이썬 스크립트를 실행할 수도 있다.

케라스 시작하기: 두 가지 방법

  • 케라스를 실행하려면 다음 두 방법 중 하나를 권장한다.
    • 공식 EC2 딥러닝 AMI(https://aws.amazon.com/amazon-ai/amis)를 사용해서 EC2에서 주피터 노트북으로 케라스 예제를 실행한다. 로컬 컴퓨터에 GPU가 없을 때 이 방법을 사용한다.
  • 로컬 유닉스 컴퓨터에 처음부터 모든 것을 설치한다. 그 다음 로컬 컴퓨터에서 주피터 노트북을 실행하든지 일반 파이썬 스크립트를 실행한다.
    • 이미 고사양 GPU 카드가 있을 때 이 방법을 사용한다.

클라우드에서 딥러닝 작업을 수행했을 때 장단점

  • (생략)

어떤 GPU 카드가 딥러닝에 최적일까?

  • (생략)

영화 리뷰 분류: 이진 분류 예제

IMDB 데이터셋

  • 인터넷 영화 데이터베이스(Internet Movie Database)로부터 가져온 양극단의 리뷰 5만 개로 이루어진 IMDB 데이터셋을 사용하겠다.
    • 이 데이터셋은 훈련 데이터 25,000개와 테스트 데이터 25,000개로 나뉘어 있고 각각 50/50으로 긍정/부정 리뷰가 구성되어 있다.
  • MNIST 데이터셋처럼 IMDB 데이터셋도 케라스에 포함되어 있다. 이 데이터는 전처리되어 있어 각 리뷰(단어 시퀀스)가 숫자 시퀀스로 변환되어 있다.
    • 여기서 각 숫자는 사전에 있는 고유한 단어를 나타낸다.
  • 다음 코드는 데이터셋을 로드한다.
from keras.datasets import imdb

(train_data, train_labels), (test_data, test_labels) = imdb.load_data(num_words=10000)
  • (이하 데이터 설명 생략)

데이터 준비

  • 신경망에 숫자 리스트를 주입할 수는 없다. 리스트를 텐서로 바꾸는 두 가지 방법이 있다.
    • 같은 길이가 되도록 리스트에 패딩(padding)을 추가하고 (samples, sequence_length) 크기의 정수 텐서로 변환한다. 그 다음 이 정수 텐서를 다룰 수 있는 층을 신경망의 첫 번째 층으로 사용한다(Embedding 층)
    • 리스트를 원-핫 인코딩(one-hot encoding)하여 0과 1의 벡터로 변환한다. 예컨대 시퀀스 [3, 5]를 인덱스 3과 5의 위치는 1이고 그 외는 모두 0인 10,000차원의 벡터로 각각 변환한다. 그 다음 부동 소수 벡터 데이터를 다룰 수 있는 Dense 층을 신경망의 첫 번째 층으로 사용한다.
  • 여기서는 두 번째 방식을 사용하고 이해를 돕기 위해 직접 데이터를 원-핫 벡터로 만들겠다.
import numpy as np

def vectorize_sequences(sequences, dimension=10000):
results = np.zeros((len(sequences), dimension)) #크기가 (len(sequences), dimension)이고 모든 원소가 0인 행렬을 만든다.
for i, sequence in enumerate(sequences):
results[i, sequence] = 1. #results[i]에서 특정 인덱스의 위치를 1로 만든다.
return results

x_train = vectorize_sequences(train_data) #훈련데이터를 벡터로 변환한다
x_test = vectorize_sequences(test_data) #테스트 데이터를 벡터로 변환한다.

x_train[0]
#array([0., 1., 1., ..., 0., 0., 0.])

y_train = np.asarray(train_labels).astype('float32')
y_test = np.asarray(test_labels).astype('float32')

신경망 모델 만들기

  • 입력 데이터가 벡터이고 레이블은 스칼라(1 또는 0)이다. 이런 문제에 잘 작동하는 네트워크 종류는 relu 활성화 함수를 사용한 완전 연결 층(즉 Dense(16, activation=’relu’))을 그냥 쌓은 것이다.
  • Dense 층에 전달한 매개변수(16)은 은닉 유닛(hidden unit)의 개수이다.
    • 하나의 은닉 유닛은 층이 나타내는 표현 공간에서 하나의 차원이 된다.
    • 2장에서 relu 활성화 함수를 사용한 Dense 층을 다음 텐서 연산을 연결하여 구현했었다.
output = relu(dot(W, input) + b)
  • 16개의 은닉 유닛이 있다는 것은 가중치 행렬 W의 크기가 (input_dimension, 16)이라는 뜻이다.
    • 입력 데이터와 W를 내적하면 입력 데이터가 16차원으로 표현된 공간으로 투영된다(그리고 편향 벡터 b를 더하고 relu 연산을 적용한다)
    • 표현 공간의 차원을 ‘신경망이 내재된 표현을 학습할 때 가질 수 있는 자유도’로 이해할 수 있다.
    • 은닉 유닛을 늘리면(표현 공간을 더 고차원으로 만들면) 신경망이 더욱 복잡한 표현을 학습할 수 있지만 계산 비용이 커지고 원하지 않는 패턴을 학습할 수도 있다. (훈련 데이터에서는 성능이 향상되지만 테스트 데이터에서는 그렇지 않은 패턴)
  • Dense 층을 쌓을 때 두 가지 중요한 구조상의 결정이 필요하다.
    • 얼마나 많은 층을 사용할 것인가?
    • 각 층에 얼마나 많은 은닉 유닛을 둘 것인가?
  • 중간에 있는 은닉 층은 활성화 함수로 relu를 사용하고 마지막 층은 확률을 출력하기 위해 시그모이드 활성화 함수를 사용한다.
    • relu는 음수를 0으로 만드는 함수이다.
    • 시그모이드는 임의의 값을 [0, 1] 사이로 압축하므로 출력 값을 확률처럼 해석할 수 있다.

  • 아래 그림은 이 신경망을 보여준다.

from keras import models
from keras import layers

model = models.Sequential()
model.add(layers.Dense(16, activation='relu', input_shape=(10000,)))
model.add(layers.Dense(16, activation='relu'))
model.add(layers.Dense(1, activation='sigmoid'))
  • Note)
    • relu와 같은 활성화 함수(또는 비선형성(non-linearity)라고도 한다)가 없다면 Dense 층은 선형적인 연산인 내적과 덧셈 2개로 구성된다.
      • output = dot(W, input) + b
    • 그러므로 이 층은 입력에 대한 선형 변환(아핀 변환)만을 학습할 수 있다. 이 층의 가설 공간은 입력 데이터를 16차원의 공간으로 바꾸는 가능한 모든 선형 변환의 집합이다.
    • 이런 가설 공간은 매우 제약이 많으며, 선형 층을 깊게 쌓아도 여전히 하나의 선형 연산이기 때문에 층을 여러 개로 구성하는 장점이 없다. 즉 층을 추가해도 가설 공간이 확장되지 않는다.
    • 가설 공간을 풍부하게 만들어 층을 깊게 만드는 장점을 살리기 위해서는 비선형성 또는 활성화 함수를 추가해야 한다. relu는 딥러닝에서 가장 인기 있는 활성화 함수이다.
    • prelu, elu등 비슷한 다른 함수들도 많다.
  • 마지막으로 손실 함수와 옵티마이저를 선택해야 한다.
    • 이진 분류 문제고 신경망의 출력이 확률이기 때문에 binary_crossentropy 손실이 적합하다. 이 함수가 유일한 선택은 아니고 mean_squared_error도 사용할 수 있다. 확률을 출력하는 모델을 사용할 때는 크로스엔트로피가 최선의 선택이다.
    • 크로스엔트로피(Crossentropy)는 정보 이론(Information Theory) 분야에서 온 개념으로 확률 분포 간의 차이를 측정한다. 여기서는 원본 분포와 예측 분포 사이를 측정한다.
  • 다음은 rmsprop 옵티마이저와 binary_crossentropy 손실 함수로 모델을 설정하는 단계이다. 훈련하는 동안 정확도를 사용하여 모니터링 하겠다.
model.compile(optimizer='rmsprop', loss='binary_crossentropy', metrics=['accuracy'])
  • 케라스에 rmsprop, binary_crossentropy, accuracy가 포함되어 있기 때문에 옵티마이저, 손실 함수, 측정 지표를 문자열로 지정하는 것이 가능하다.
  • 이따금 옵티마이저의 매개변수를 바꾸거나 자신만의 손실 함수, 측정 함수를 전달해야 할 경우가 있다.
  • 전자의 경우에는 아래와 같이 간이 옵티마이저 파이썬 클래스를 사용해 객체를 직접 만들어 optimizer 매개변수에 전달하면 된다.
from keras import optimizers

model.compile(optimizer=optimizers.RMSprop(lr=0.001), loss='binary_crossentropy', metrics=['accuracy'])
  • 후자의 경우는 아래와 같이 loss와 metrics 매개변수에 함수 객체를 전달하면 된다.
from keras import losses
from keras import metrics

model.compile(optimizer=optimizers.RMSprop(lr=0.001), loss=losses.binary_crossentropy, metrics=[metrics.binary_accuracy])

훈련 검증

  • 훈련하는 동안 처음 본 데이터에 대한 모델의 정확도를 측정하기 위해 원본 훈련 데이터에서 10,000의 샘플을 떼어 검증 세트를 만들어야 한다.
x_val = x_train[:10000]
y_val = y_train[:10000]
partial_x_train = x_train[10000:]
partial_y_train = y_train[10000:]
  • 이제 모델을 512개의 샘플씩 미니 배치를 만들어 20번의 에포크 동안 훈련시킨다. 동시에 따로 떼어 놓은 1만 개의 샘플에서 손실과 정확도를 측정할 것이다.
    • 이렇게 하려면 validation_data 매개변수에 검증 데이터를 전달해야 한다.
model.compile(optimizer='rmsprop', loss='binary_crossentropy', metrics=['acc'])
history = model.fit(partial_x_train, partial_y_train, epochs=20, batch_size=512, validation_data=(x_val, y_val))
  • model.fit() 메서드는 History 객체를 반환한다. 이 객체는 훈련하는 동안 발생한 모든 정보를 담고 있는 딕셔너리인 history 속성을 갖고 있다.
history_dict = history.history
history_dict.keys()
#dict_keys(['val_loss', 'val_acc', 'loss', 'acc'])
  • 이 딕셔너리는 훈련과 검증하는 동안 모니터링할 측정 지표당 하나씩 모두 4개의 항목을 담고 있다.
    • 이어지는 두 목록에서 맷플롯립을 사용하여 훈련과 검증 데이터에 대한 손실과 정확도를 그려보자.
import matplotlib.pyplot as plt

history_dict = history.history
loss = history_dict['loss']
val_loss = history_dict['val_loss']

epochs = range(1, len(loss) + 1)

plt.plot(epochs, loss, 'bo', label='Training loss')
plt.plot(epochs, val_loss, 'b', label='Validation loss')
plt.title('Training and validation loss')
plt.xlabel('Epochs')
plt.ylabel('Loss')
plt.legend()
plt.show()

plt.clf()
acc = history_dict['acc']
val_acc = history_dict['val_acc']

plt.plot(epochs, acc, 'bo', label='Training acc')
plt.plot(epochs, val_acc, 'b', label='Validation acc')
plt.title('Trainging and validation accuracy')
plt.xlabel('Epochs')
plt.ylabel('Accuracy')
plt.legend()
plt.show()

  • 여기에서 볼 수 있듯이 훈련 손실이 에포크마다 감소하고 훈련 정확도는 에포크마다 증가한다.
    • 경사 하강법 최적화를 사용했을 때 반복마다 최소화되는 것이 손실이므로 기대했던대로이다.
    • 검증 손실과 정확도는 이와 같지 않은데, 네 번째 에포크에서 그래프가 역전되는 것을 볼 수 있다.
    • 이것이 훈련 세트에서 잘 작동하는 모델이 처음 보는 데이터에서 잘 작동하지 않을 수 있다고 한 과대적합(overfitting)이다.

훈련된 모델로 새로운 데이터에 대해 예측하기

  • 모델을 훈련 시킨 후에 이를 실전 환경에서 사용하고 싶을 것이다. predict 메서드를 사용해서 어떤 리뷰가 긍정일 확률을 예측할 수 있다.
model.predict(x_test)

추가 실험

  • (생략)

정리

  • 원본 데이터를 신경망에 텐서로 주입하기 위해서는 꽤 많은 전처리가 필요하다. 단어 시퀀스는 이진 벡터로 인코딩될 수 있고 다른 인코딩 방식도 있다.
  • relu 활성화 함수와 함께 Dense 층을 쌓은 네트워크는 여러 종류의 문제에 적용할 수 있어 앞으로 자주 사용하게 될 것이다.
  • 출력 클래스가 2개인 이진 분류 문제에서 네트워크는 하나의 유닛과 sigmoid 활성화 함수를 가진 Dense 층으로 끝나야 한다. 이 신경망의 출력은 확률을 나타내는 0과 1사이의 스칼라 값이다.
  • 이진 분류 문제에서 이런 스칼라 시그모이드 출력에 대해 사용할 손실 함수는 binary_crossentropy이다.
  • rmsprop 옵티마이저는 문제에 상관없이 일반적으로 충분히 좋은 선택이다.
  • 훈련 데이터에 대해 성능이 향상됨에 따라 신경망은 과대적합되기 시작하고 이전에 본적 없는 데이터에서는 결과가 점점 나빠진다. 항상 훈련 세트 이외의 데이터에서 성능을 모니터링 해야 한다.

뉴스 기사 분류: 다중 분류 문제

  • 여기서는 로이터 뉴스를 46개의 상호 배타적인 토픽으로 분류하는 신경망을 만들어 보겠다. 
    • 클래스가 많기 때문에 이 문제는 다중 분류(multiclass classification)의 예이다.
    • 각 데이터 포인트가 정확히 하나의 범주로 분류되기 때문에 좀 더 정확히 말하면 단일 레이블 다중 분류 문제이다.
    • 각 데이터 포인트가 여러 개의 범주에 속할 수 있다면 이것은 다중 레이블 다중 분류 문제가 된다.

로이터 데이터셋

  • IMDB, MINIST와 마찬가지로 로이터 데이터셋도 케라스에 포함되어 있다.
    • IMDB와 마찬가지로 각 샘플은 정수 리스트이다.
from keras.datasets import reuters

(train_data, train_labels), (test_data, test_labels) = reuters.load_data(num_words=10000)

데이터 준비

  • 이전 예제와 동일한 코드를 사용해서 단어를 벡터로 변환한다.
    • (코드 생략)
  • 레이블을 벡터로 바꾸는 방법은 두 가지이다. 레이블의 리스트를 정수 텐서로 변환하는 것과 원-핫 인코딩을 사용하는 것이다.
    • 원-핫 인코딩이 범주형 데이터에 널리 사용되기 때문에 범주형 인코딩(categorical encoding)이라고도 부른다.
def to_one_hot(labels, dimension=46):
results = np.zeros((len(labels), dimension))
for i, label in enumerate(labels):
results[i, label] = 1.
return results

one_hot_train_labels = to_one_hot(train_labels)
one_hot_test_labels = to_one_hot(test_labels)

  • MINIST 예제에서 보았듯이 케라스에는 이를 위한 내장 함수가 이미 있다.
from keras.utils.np_utils import to_categorical

one_hot_train_labels = to_categorical(train_labels)
one_hot_test_labels = to_categorical(test_labels)

모델 구성

  • 이 토픽 분류 문제는 영화 리뷰 분류 문제와 비슷해 보인다. 두 경우 모두 짧은 텍스트를 분류하는 것이기 때문이다.
    • 여기서는 새로운 제약 사항이 추가되었는데, 출력 클래스의 개수가 2에서 46개로 늘어났다는 점이다. 출력 공간의 차원이 훨씬 커졌다.
  • 이전에 사용했던 것처럼 Dense 층을 쌓으면 각 층은 이전 층의 출력에서 제공한 정보만 사용할 수 있다.
    • 한 층이 분류 문제에 필요한 일부 정보를 누락하면 그 다음 층에서 이를 복원할 방법이 없다.
    • 각 층은 잠재적으로 정보의 병목(information bottleneck)이 될 수 있다.
    • 이전 예제에서 16차원을 가진 중간층을 사용했지만 16차원 공간은 46개의 클래스를 구분하기에 너무 제약이 많을 것 같다. 이렇게 규모가 작은 층은 유용한 정보를 완전히 잃게 되는 정보의 병목 지점처럼 동작할 수 있다.
  • 이런 이유로 좀 더 규모가 큰 층을 사용해 보겠다. 64개의 유닛을 사용해 보자.
from keras import models
from keras import layers

model = models.Sequential()
model.add(layers.Dense(64, activation='relu', input_shape=(10000,)))
model.add(layers.Dense(64, activation='relu'))
model.add(layers.Dense(46, activation='softmax'))
  • 이 구조에서 주목해야 할 점이 두 가지 있다.
    • 마지막 Dense 층의 크기가 46이다. 각 입력 샘플에 대해 46차원의 벡터를 출력한다는 뜻이다. 이 벡터의 각 원소(각 차원)는 각기 다른 출력 클래스가 인코딩 된 것이다.
    • 마지막 층에 softmax 활성화 함수가 사용되었다.
  • 이런 문제에 사용할 최선의 손실 함수는 categorical_crossentropy이다.
    • 이 함수는 두 확률 분포 사이의 거리를 추정한다. 여기서는 네트워크가 출력한 확률 분포와 진짜 레이블의 분포 사이의 거리이다.
    • 두 분포 사이의 거리를 최소화하면 진짜 레이블에 가능한 가까운 출력을 내도록 모델을 훈련하게 된다.
model.compile(optimizer='rmsprop', loss='categorical_crossentropy', metrics=['accuracy'])

훈련 검증

  • 훈련 데이터에서 1,000개의 샘플을 떼어 검증 세트로 사용하자.
x_val = x_train[:1000]
partial_x_train = x_train[1000:]

y_val = one_hot_train_labels[:1000]
partial_y_train = one_hot_train_labels[1000:]
  • 20번의 에포크로 모델을 훈련시킨다.
import numpy as np

history = model.fit(partial_x_train, partial_y_train, epochs=20, batch_size=512, validation_data=(x_val, y_val))
  • 손실과 정확도 곡선을 그려보자.
    • (코드 생략)

새로운 데이터에 대해 예측하기

  • 모델 객체의 predict 메서드는 46개의 토픽에 대한 확률 분포를 반환한다.
model.predict(x_test)

레이블과 손실을 다루는 다른 방법

  • 레이블을 인코딩 하는 다른 방법은 다음과 같이 정수 텐서로 변환하는 것이다.
y_train = np.array(train_labels)
y_test = np.array(test_labels)
  • 이 방식을 사용하려면 손실 함수 하나만 바꾸면 된다. categorical_crossentropy는 레이블이 범주형 인코딩되어 있을 것이라고 기대한다.
    • 정수 레이블을 사용할 때는 sparse_categorical_crossentropy를 사용해야 한다.
model.compile(optimizer='rmsprop', loss='sparse_categorical_crossentropy', metrics=['acc'])
  • 이 손실 함수는 인터페이스만 다를 뿐이고 수학적으로는 categorical_crossentropy와 동일하다.

충분히 큰 중간층을 두어야 하는 이유

  • 마지막 출력이 46차원이기 때문에 중간의 히든 유닛이 46개보다 많이 적어져서는 안된다.
  • 46차원보다 훨씬 작은 중간층을 두면 정보의 병목이 어떻게 나타나는지 확인해 보겠다.
    • (생략 – 정확도가 매우 떨어짐)

추가 실험

  • (생략)

정리

  • N개의 클래스로 데이터 포인트를 분류하려면 네트워크의 마지막 Dense 층의 크기는 N이어야 한다.
  • 단일 레이블, 다중 분류 문제에서는 N개의 클래스에 대한 확률 분포를 출력하기 위해 softmax 활성화 함수를 사용해야 한다.
  • 이런 문제에는 항상 범주형 크로스엔트로피를 사용해야 한다. 이 함수는 모델이 출력한 확률 분포와 타깃 분포 사이의 거리를 최소화 한다.
  • 다중 분류에서 레이블을 다루는 두 가지 방법이 있다.
    • 레이블을 범주형 인코딩(또는 원-핫 인코딩)으로 인코딩하고 categorical_crossentropy 손실 함수를 사용한다.
    • 레이블을 정수로 인코딩하고 sparse_categorical_crossentropy 손실 함수를 사용한다.
  • 만은 수의 범주를 분류할 때 중간층의 크기가 너무 작아 네트워크에 정보의 병목이 생기지 않도록 주의해야 한다.

주택 가격 예측: 회귀 문제

  • 앞선 두 예제는 분류 문제로 입력 데이터 포인트의 개별적인 레이블 하나를 예측하는 것이 목적이었다.
  • 또 다른 종류의 머신 러닝 문제는 개별적인 레이블 대신 연속적인 값을 예측하는 회귀(regression)가 있다.

보스턴 주택 가격 데이터셋

  • 1980년 중반 보스턴 외곽 지역에 대한 데이터셋은 이전 2개의 예제와 다른데, 데이터 포인트가 506개로 비교적 적다. 404개는 훈련 샘플, 102개는 테스트 샘플로 나뉘어 있다.
    • 입력 데이터에 있는 특성(feature)은 스케일이 서로 다르다. 어떤 값은 0-1 사이의 비율이고, 어떤 것은 1-12, 1-100 사이의 값을 갖는 것도 있다.
from keras.datasets import boston_housing

(train_data, train_targets), (test_data, test_targets) = boston_housing.load_data()

데이터 준비

  • 상이한 스케일을 가진 값을 신경망에 주입하면 문제가 된다. 
    • 네트워크가 이런 다양한 데이터에 자동으로 맞추려고 할 수 있지만, 이는 확실히 학습을 더 어렵게 만든다. 이런 데이터를 다룰 때 대표적인 방법은 특성별로 정규화를 하는 것이다.
    • 입력 데이터에 있는 각 특성에 대해 특성의 평균을 내고 표준편차로 나눈다. 특성의 중앙이 0 근처에 맞추어지고 표준 편차가 1이 된다. 
    • 넘파이를 사용하면 간단하게 할 수 있다.
mean = train_data.mean(axis=0)
std = train_data.std(axis=0)

train_data -= mean
train_data /= std

test_data -= mean
test_data /= std

모델 구성

  • 샘플 개수가 적기 때문에 64개의 유닛을 가진 2개의 은닉 층으로 작은 네트워크를 구성하여 사용하겠다.
    • 일반적으로 훈련 데이터의 개수가 적을수록 과대적합이 더 쉽게 일어나므로 작은 모델을 사용하는 것이 과대적합을 피하는 방법이다.
from keras import models
from keras import layers

def build_model():
model = models.Sequential()
model.add(layers.Dense(64, activation='relu', input_shape=(train_data.shape[1],)))
model.add(layers.Dense(64, activation='relu'))
model.add(layers.Dense(1))
model.compile(optimizer='rmsprop', loss='mse', metrics=['mae'])
return model
  • 이 네트워크의 마지막 층은 하나의 유닛을 가지고 있고 활성화 함수가 없다 (선형 층이라고 부른다). 이것이 전형적인 스칼라 회귀(하나의 연속적인 값을 예측하는 회귀)를 위한 구성이다.
    • 활성화 함수를 적용하면 출력 값의 범위를 제한하게 된다. 예컨대 마지막 층에 sigmoid 활성화 함수를 적용하면 네트워크가 0-1 사이의 값을 예측하도록 학습될 것이다.
    • 여기서는 마지막 층이 순수한 선형이므로 네트워크가 어떤 범위의 값이라도 예측하도록 자유롭게 학습된다.
  • 이 모델은 mse 손실 함수를 사용하여 컴파일한다.
    • 이 함수는 평균 제곱 오차(mean squared error)의 약어로 예측과 타깃 사이 거리의 제곱이다. 회귀 문제에서 널리 사용되는 손실함수이다.
  • 훈련하는 동안 모니터링을 위해 새로운 지표인 평균 절대 오차(Mean Absolute Error, MAE)를 측정한다.
    • 이는 예측과 타깃 사이 거리의 절댓값이다. 예컨대 이 예제에서 MAE가 0.5면 예측이 평균적으로 500달러 정도 차이가 난다는 뜻이다.

K-겹 검증을 사용한 훈련 검증

  • 매개변수들을 조정하면서 모델을 평가하기 위해 이전 예제에서 했던 것처럼 데이터를 훈련 세트와 검증 세트로 나눈다.
    • 데이터 포인트가 많지 않기 때문에 검증 세트도 매우 작아진다(약 100개)
    • 결국 검증 세트와 훈련 세트로 어떤 데이터 포인트가 선택되었는지에 따라 검증 점수가 크게 달라진다. 검증 세트의 분할에 대한 검증 점수의 분산이 높다. 이렇게 되면 신뢰 있는 모델 평가를 할 수 없다.
  • 이런 상황에서 가장 좋은 방법은 K-겹 교차 검증(K-fold cross-validation)을 사용하는 것이다.
    • 데이터를 K개의 분할(즉 폴드(fold))로 나누고 (일반적으로 K = 4 또는 5), K개의 모델을 각각 만들어 K-1개의 분할에서 훈련하고 나머지 분할에서 평가하는 방법이다.
    • 모델의 검증 점수는 K개의 검증 점수 평균이 된다.

import numpy as np

k = 4

num_val_samples = len(train_data) // k
num_epochs = 100
all_scores = []

for i in range(k):
print('처리 중인 폴드 #', i)
val_data = train_data[i * num_val_samples: (i + 1) * num_val_samples]
val_targets = train_targets[i * num_val_samples: (i + 1) * num_val_samples]

partial_train_data = np.concatenate([train_data[:i*num_val_samples], train_data[(i+1)*num_val_samples:]], axis=0)
partial_train_targets = np.concatenate([train_targets[:i*num_val_samples], train_targets[(i+1)*num_val_samples:]], axis=0)

model = build_model()
model.fit(partial_train_data, partial_train_targets, epochs=num_epochs, batch_size=1, verbose=0)

val_mse, val_mae = model.evaluate(val_data, val_targets, verbose=0)
all_scores.append(val_mae)
  • num_epochs = 100으로 실행하면 다음 결과를 얻을 수 있다.
all_scores
# [2.0956787838794217, 2.220593797098292, 2.859968412040484, 2.40535704039111]

np.mean(all_scores)
# 2.3953995083523267
  • 검증 세트가 다르므로 검증 점수의 변화가 크다. 평균값이 각각의 점수보다 훨씬 신뢰할만 하다. 이것이 K-겹 교차 검증의 핵심이다.
num_epochs = 500
all_nae_histories = []

for i in range(k):
print('처리 중인 폴드 #', i)
val_data = train_data[i * num_val_samples: (i + 1) * num_val_samples]
val_targets = train_targets[i * num_val_samples: (i + 1) * num_val_samples]

partial_train_data = np.concatenate([train_data[:i*num_val_samples], train_data[(i+1)*num_val_samples:]], axis=0)
partial_train_targets = np.concatenate([train_targets[:i*num_val_samples], train_targets[(i+1)*num_val_samples:]], axis=0)

model = build_model()
history = model.fit(partial_train_data, partial_train_targets, epochs=num_epochs, batch_size=1, verbose=0)

mae_history = history.history['val_mean_absolute_error']
all_mae_histories.append(mae_history)
  • 그 다음 모든 폴드에 대해 에포크의 MAE 점수 평균을 계산한다.
average_mae_history = [np.mean([x[i] for x in all_mae_histories]) for i in range(num_epochs)]
  • 그래프로 나타내면 다음과 같다.

  • 이 그래프는 범위가 크고 변동이 심하기 때문에 보기가 어렵다. 다음과 같이 해보자.
    • 곡선의 다른 부분과 스케일이 많이 달느 첫 10개의 데이터 포인트를 제외한다.
    • 부드러운 곡선을 얻기 위해 각 포인트를 이전 포인트의 지수 이동 평균(exponential moving average)으로 대체한다.
def smooth_curve(points, factor=0.9):
smoothed_points = []
for point in points:
if smoothed_points:
previous = smoothed_points[-1]
smoothed_points.append(previous * factor + point * (1-factor))
else:
smoothed_points.append(point)
return smoothed_points

smooth_mae_history = smooth_curve(average_mae_history[10:])

  • 이 그래프를 보면 검증 MAE가 80번째 에포크 이후에 줄어드는 것이 멈추었다. 이 지점 이후로는 과대적합이 시작된다.
  • 모델의 여러 변수에 대한 튜닝이 끝나면 모든 훈련 데이터를 사용하고 최상의 매개변수로 최종 실전에 투입될 모델을 훈련시킨다. 그 다음 테스트 데이터로 성능을 확인한다.
model = build_model()
model.fit(train_data, train_targets, epochs=80, batch_size=16, verbose=0)
test_mse_score, test_mae_score = model.evaluate(test_data, test_targets)

정리

  • 회귀는 분류에서 사용했던 것과는 다른 손실 함수를 사용한다. 평균 제곱 오차(MSE)는 회귀에서 자주 사용되는 손실 함수이다.
  • 비슷하게 회귀에서 사용되는 평가 지표는 분류와 다르다. 당연히 정확도 개념은 회귀에 적용되지 않는다. 일반적인 회귀 지표는 평균 절대 오차(MAE)이다.
  • 입력 데이터의 특성이 서로 다른 범위를 가지면 전처리 단계에서 각 특성을 개별적으로 스케일 조정해야 한다.
  • 가용한 데이터가 적다면 K-겹 검증을 사용하는 것이 신뢰할 수 있는 모델 평가 방법이다.
  • 가용한 훈련 데이터가 적다면 과대적합을 피하기 위해 은닉 층의 수를 줄인 모델이 좋다. (일반적으로 1개 또는 2개)

요약

  • 이제 벡터 데이터를 사용하여 가장 일반적인 머신 러닝인 이진 분류, 다중 분류, 스칼라 회귀 작업을 다룰 수 있다.
  • 보통 원본 데이터를 신경망에 주입하기 전에 전처리해야 한다.
  • 데이터에 범위가 다른 특성이 있다면 전처리 단계에서 각 특성을 독립적으로 스케일 조정해야 한다.
  • 훈련이 진행됨에 따라 신경망의 과대적합이 시작되고 새로운 데이터에 대해 나쁜 결과를 얻게 된다.
  • 훈련 데이터가 많지 않으면 과대적합을 피하기 위해 1개 또는 2개의 은닉 층을 가진 신경망을 사용한다.
  • 데이터가 많은 범주로 나뉘어 있을 때 중간층이 너무 작으면 정보의 병목이 생길 수 있다.
  • 회귀는 분류와 다른 손실 함수와 평가 지표를 사용한다.
  • 적은 데이터를 사용할 때는 K-겹 검증이 신뢰할 수 있는 모델 평가를 도와준다.

케라스 창시자에게 배우는 딥러닝/ 시작하기 전에: 신경망의 수학적 구성 요소

신경망과의 첫 만남

  • 딥러닝의 “hello world”인 MNIST 데이터셋을 이용해서  신경망 예제를 살펴보겠다.
    • 실제 실습은 3장에서 할 것이고, 여기서는 개념만 살펴본다.
  • 머신 러닝에서 분류 문제의 범주(category)를 클래스(class)라고 한다. 데이터 포인트는 샘플(sample)이라고 한다. 특정 샘플의 클래스는 레이블(label)이라고 한다.

  • MNIST 데이터셋은 넘파이(NumPy) 배열 형태로 케라스에 이미 포함되어 있다.
from keras.datasets import mnist

(train_images, train_labels), (test_images, test_labels) = mnist.load_data()
  • train_images와 train_labels가 모델이 학습해야 할 훈련 세트(training set)를 구성한다. 모델은 test_images와 test_labels로 구성된 테스트 세트(test set)에서 테스트될 것이다.
  • 이미지는 넘파이 배열로 인코딩 되어 있고 레이블은 0-9까지의 숫자배열이다. 이미지와 레이블은 일대일 관계이다.
  • 훈련 데이터와 테스트 데이터를 살펴 보자.
train_images.shape
# (60000, 28, 28)

len(train_labels)
# 60000

train_labels
# array([5, 0, 4, ... , 5, 6, 8], dtype=unit8)

test_images.shape
# (10000, 28, 28)

len(test_labels)
# 10000

test_labels
# array([7, 2, 1, ... , 4, 5, 6], dtype=unit8)
  • 작업 순서는 다음과 같다.
    • 먼저 훈련 데이터 train_images와 train_labels를 네트워크에 주입한다.
    • 그러면 네트워크는 이미지와 레이블을 연관시킬 수 있도록 학습된다.
    • 마지막으로 test_images에 대한 예측을 네트워크에 요청한다.
    • 그리고 이 예측이 test_labels와 맞는지 확인할 것이다.
  • 신경망을 만들어 보자
from keras import models
from keras import layers

network = models.Sequential()
network.add(layers.Dense(512, activation='relu', input_shape=(28 * 28,)))
network.add(layers.Dense(10, activation='softmax'))
  • 신경망의 핵심 고숭요소는 일종의 데이터 처리 필터라고 생각할 수 있는 층(layer)이다. 어떤 데이터가 들어가면 더 유용한 형태로 출력된다.
    • 좀 더 구체적으로 층은 주어진 문제에 더 의미 있는 표현(representation)을 입력된 데이터로부터 추출한다.
    • 대부분의 딥러닝은 간단한 층을 연결하여 구성되어 있고, 점진적으로 데이터를 정제하는 형태를 띠고 있다.
    • 딥러닝 모델은 데이터 정제 필터(층)가 연속되어 있는 데이터 프로세싱을 위한 여과기와 같다.
  • 이 예에서는 조밀하게 연결된 (또는 완전 연결(fully connected)된) 신경망 층인 Dense 층 2개가 연속되어 있다.
    • 두 번째 (즉 마지막) 층은 10개의 확률 점수가 들어 있는 배열(모두 더하면 1)을 반환하는 소프트맥스(softmax) 층이다.
    • 각 점수는 현재 숫자 이미지가 10개의 숫자 클래스 중 하나에 속할 확률이 된다.
  • 신경망이 훈련 준비를 마치기 위해 컴파일 단계에 포함될 세 가지가 더 필요하다.
    • 손실 함수(loss function): 훈련 데이터에서 신경망의 성능을 측정하는 방법으로 네트워크가 옳은 방향으로 학습될 수 있도록 도와준다.
    • 옵티마이저(optimizer): 입력된 데이터와 손실 함수를 기반으로 네트워크를 업데이트하는 메커니즘
    • 훈련과 테스트 과정을 모니터링할 지표: 여기서는 정확도(정확히 분류된 이미지의 비율)만 고려하겠다.
  • 손실 함수와 옵티마이저의 정확한 목적은 이어지는 장에서 자세히 설명하겠다.
network.compile(optimizer='rmsprop', loss='categorical_crossentropy', metrics=['accuracy'])
  • 훈련을 시작하기 전에 데이터를 네트워크에 맞는 크기로 바꾸고 모든 값을 0과 1사이로 스케일 조정한다.
    • 예컨대 우리의 훈련 이미지는 [0, 255] 사이의 값인 uint8 타입의 (60000, 28 * 28) 크기를 가진 배열로 저장되어 있는데, 이 데이터를 0과 1사이의 값을 가지는 float32 타입의 (60000, 28 * 28) 크기인 배열로 바꾼다.
train_images = train_images.reshape((60000, 28 * 28))
train_images = train_images.astype('float32') / 255

test_images = test_images.reshape((10000, 28 * 28))
test_images = test_images.astype('float32') / 255
  • 또 레이블을 범주형으로 인코딩해야 하는데, 이 단계는 다음 장에서 설명하겠다.
from keras.utils import to_categorical

train_labels = to_categorical(train_labels)
test_labels = to_categorical(test_labels)
  • 이제 신경망을 훈련시킬 준비가 되었다. 케라스에서는 fit 메서드를 호출하여 데이터에 모델을 학습 시킨다.
network.fit(train_images, train_labels, epochs=5, batch_size=128)

# Epoch 1/5
# 60000/60000 [===========================================] - 1s 22us/step - loss: 0.2571 - acc: 0.9257
# Epoch 2/5
# 60000/60000 [===========================================] - 1s 12us/step - loss: 0.1027 - acc: 0.9695
# Epoch 3/5
# 60000/60000 [===========================================] - 1s 12us/step - loss: 0.0686 - acc: 0.9797
# Epoch 4/5
# 60000/60000 [===========================================] - 1s 12us/step - loss: 0.0494 - acc: 0.9856
# Epoch 5/5
# 60000/60000 [===========================================] - 1s 12us/step - loss: 0.0368 - acc: 0.9894
  • 훈련 하는 동안 2개의 정보가 출력되는데, 훈련 데이터에 대한 네트워크의 손실과 정확도이다.
  • 훈련 데이터에 대해 0.989(98.9%)의 정확도를 금방 달성했다. 테스트 세트에서도 모델이 잘 작동하는지 확인해 보자.
test_loss, test_acc = network.evaluate(test_images, test_labels)
# 10000/10000 [===========================================] - 0s 16us/step

print('test_acc:', test_acc)
# test_acc: 0.9789
  • 테스트 세트의 정확도는 97.8%로 나왔다.
    • 훈련 세트 정확도 보다 약간 낮은데, 훈련 정확도와 테스트 정확도 사이의 차이는 과대적합(overfitting) 때문이다. 이는 머신 러닝 모델이 훈련 데이터보다 새로운 데이터에서 성능이 낮아지는 경향을 말한다.
    • 과대적합에 대해서는 3장에서 자세히 논의하겠다.

신경망을 위한 데이터 표현

  • 최근의 모든 머신 러닝 시스템은 일반적으로 텐서를 기본 데이터 구조로 사용한다. 텐서는 머신 러닝의 기본 구성 요소이다.
  • 텐서는 데이터를 위한 컨테이너(container)이다.
    • 거의 항상 수치형 데이터를 다루므로 숫자를 위한 컨테이너이다.
    • 텐서는 임의의 차원 개수를 가지는 행렬의 일반화된 모습이다. (텐서에서는 차원(dimension)을 종종 축(axis)라고 부른다.)

스칼라(0D 텐서)

  • 하나의 숫자만 담고 있는 텐서를 스칼라(scalar) (또는 스칼라 텐서, 0차원 텐서, 0D 텐서)라고 부른다.
    • 넘파이에서는 float32, float64 타입의 숫자가 스칼라 텐서(또는 배열 스칼라(array scalar)) 이다.
    • ndim 속성을 사용하면 넘파이 배열의 축 개수를 확인할 수 있다.
    • 스칼라 텐서의 축 개수는 0이다(ndim==0)
    • 텐서의 축 개수를 랭크(rank)라고 부른다.

벡터(1D 텐서)

  • 숫자의 배열을 벡터(vector) 또는 1D 텐서라고 부른다.
    • 1D 텐서는 딱 하나의 축을 가진다.
    • 5개의 원소를 가진 배열을 5차원 벡터라고 부르는데, 5D 벡터와 5D 텐서를 혼동하지 말 것

행렬(2D 텐서)

  • 벡터의 배열이 행렬(matrix) 또는 2D 텐서이다.
    • 행렬에는 2개의 축이 있다. (보통 행(row)과 열(column)이라 부른다)
    • 행렬은 숫자가 채워진 사각 격자라고 생각할 수 있다.
    • 첫 번째 축에 놓여 있는 원소를 행이라 부르고, 두 번째 축에 놓여 있는 원소를 열이라 부른다.

3D 텐서와 고차원 텐서

  • 이런 행렬들을 하나의 새로운 배열로 합치면 숫자가 채워진 직육면체 형태로 해석할 수 있는 3D 텐서가 만들어진다.
  • 3D 텐서들을 하나의 배열로 합치면 4D 텐서가 만들어진다.
  • 딥러닝에서는 보통 0D에서 4D까지 텐서를 다루는데, 동영상 데이터를 다룰 때에는 5D 텐서까지 가기도 한다.

핵심 속성

  • 텐서는 4개의 핵심 속성으로 정의된다.
    • 축의 개수(rank): 예컨대 3D 텐서에는 3개의 축이 있고, 행렬에는 2개의 축이 있다. 넘파이 라이브러리에서는 ndim 속성에 저장되어 있다.
    • 크기(shape): 텐서의 각 축을 따라 얼마나 많은 차원이 있는지를 나타낸 파이썬의 튜플(tuple)이다.
      • 예컨대 행렬의 크기는 (3, 5), 3D 텐서의 크기는 (3, 3, 5)와 같이 표현된다. 벡터의 크기는 (5,)처럼 1개의 원소로 이루어진 튜플이고 배열 스칼라는 ()처럼 크기가 없다.
    • 데이터 타입(넘파이에서는 dtype에 저장된다): 텐서에 포함된 데이터의 타입이다. 예컨대 텐서의 타입은 float32, uint8, float64 등이 될 수 있다.
  • (크기 살펴보는 예시 코드 생략)

넘파이로 텐서 조작하기

  • train_images[i] 같은 형식으로 첫 번째 축을 따라 특정 숫자를 선택했는데, 배열에 있는 특정 원소들을 선택하는 것을 슬라이싱(slicing)이라고 한다. 넘파이 배열에서 할 수 있는 슬라이싱 연산을 살펴보자.
  • 다음 예는 11번째에서 101번째까지 숫자를 선택하여 (90, 28, 28) 크기의 배열을 만든다.
my_slice = train_images[10:100]
print(my_slice.shape)
# (90, 28, 28)
  • 동일하지만 조금 더 자세한 표기법은 각 배열의 축을 따라 슬라이싱의 시작 인덱스와 마지막 인덱스를 지정하는 것이다. :(콜론)은 전체 인덱스를 선택한다.
my_slice = train_images[10:100, :, :]
print(my_slice.shape)
# (90, 28, 28)

my_slice = train_images[10:100, 0:28, 0:28]
print(my_slice.shape)
# (90, 28, 28)
  • 일반적으로 각 배열의 축을 따라 어떤 인덱스 사이도 선택할 수 있다. 예컨대 이미지의 오른쪽 아래 14×14 픽셀을 선택하려면 다음과 같이 한다.
my_slice = train_images[:, 14:, 14:]
  • 음수 인덱스도 사용할 수 있다. 파이썬 리스트의 음수 인덱스와 마찬가지로 현재 축의 끝에서 상대적인 위치를 나타낸다.
    • 정중앙에 위치한 14×14 픽셀 조각을 이미지에서 잘라 내려면 다음과 같이 하면 된다.
my_slice = train_images[:, 7:-7, 7:-7]

배치 데이터

  • 일반적으로 딥러닝에서 사용하는 모든 데이터의 텐서의 첫 번째 축(인덱스가 0부터 시작하므로 0번째 축)은 샘플 축(sample axis)이다. (이따금 샘플 차원이라고도 부른다)
    • MNIST 예제에서는 숫자 이미지가 샘플이다.
  • 딥러닝 모델은 한 번에 전체 데이터셋을 처리하지 않는다. 대신 데이터를 작은 배치(batch)로 나눈다. 구체적으로 말하면 MNIST 숫자 데이터에서 크기가 128인 배치 하나는 다음과 같다.
batch = train_images[:128]
  • 그 다음 배치는 다음과 같다.
batch = train_images[128:256]
  • n번째 배치는 다음과 같다.
batch = train_images[128 * n:128 * (n + 1)]
  • 이런 배치 데이터를 다룰 때는 첫 번째 축(0번 축)을 배치 축(batch axis) 또는 배치 차원(batch dimension)이라고 부른다.

텐서의 실제 사례

  • 우리가 사용할 데이터는 대부분 다음 중 하나에 속할 것이다.
    • 벡터 데이터: (samples, features) 크기의 2D 텐서
    • 시계열 데이터 또는 시퀀스(sequence) 데이터: (samples, timesteps, features) 크기의 3D 텐서
    • 이미지: (samples, height, width, channels) 크기의 4D 텐서
    • 동영상: (samples, frames, height, width, height, channels) 크기의 5D 텐서

벡터 데이터

  • 대부분의 경우에 해당한다. 이런 데이터셋에서는 하나의 데이터 포인트가 벡터로 인코딩될 수 있으므로 배치 데이터는 2D 텐서로 인코딩될 것이다.
    • 여기서 첫 번째 축은 샘플 축이고, 두 번째 축은 특성 축(feature axis)이다.

시계열 데이터 또는 시퀀스 데이터

  • 데이터에서 시간이 (또는 연속된 순서가) 중요할 떄는 시간 축을 포함하여 3D 텐서로 저장된다.
    • 각 샘플은 벡터(2D 텐서)의 시퀀스로 인코딩 되므로 배치 데이터는 3D 텐서로 인코딩 될 것이다.
    • 관례적으로 시간 축은 항상 두 번째 축(인덱스가 1인 축)이다.

이미지 데이터

  • 이미지는 전형적으로 높이, 너비, 컬러의 3차원으로 이루어진다.
    • 흑백 이미지는 컬러 채널만 갖고 있어 2D 텐서로 저장할 수 있지만 관례상 항상 3D로 저장한다. 흑백 이미지의 경우 컬러 채널 차원의 크기는 1이다.
  • 이미지 텐서의 크기를 지정하는 방식은 두 가지인데, 텐서플로에서 사용하는 채널 마지막(channel-last) 방식과 씨아노에서 사용하는 채널 우선(channel-first) 방식이다.
    • 케라스 프레임워크는 두 형식을 모두 지원한다.

비디오 데이터

  • 비디오 데이터는 5D 텐서가 필요한 몇 안되는 데이터 중 하나로 하나의 미비도는 프레임의 연속이고 각 프레임은 하나의 컬러 이미지이다.

신경망의 톱니바퀴: 텐서 연산

  • 심층 신경망이 학습한 모든 변환을 수치 데이터 텐서에 적용하는 몇 종류의 텐서 연산(tensor operation)으로 나타낼 수 있다.
  • 첫 번째 예제에서는 Dense 층을 쌓아서 신경망을 만들었다. 케라스의 층은 다음과 같이 생성한다.
keras.layers.Dense(512, activation='relu')
  • 이 층은 2D 텐서를 입력으로 받고 입력 텐서의 새로운 표현인 또 다른 2D 텐서를 반환하는 함수처럼 해석할 수 있다.
    • 구체적으로 보면 이 함수는 다음과 같다. (W는 2D 텐서고, b는 벡터이다. 둘 모두 층의 속성이다)
output= = relu(dot(W, input) + b)
  • 좀 더 자세히 알아보자. 여기에는 3개의 텐서 연산이 있다.
    • 입력 텐서와 텐서 W 사이의 점곱(dot), 점곱의 결과인 2D 텐서와 벡터 b 사이의 덧셈, 마지막으로 relu(렐루) 연산이다.
    • relu(x)는 max(x, 0)이다.

원소별 연산

  • relu 함수와 덧셈은 원소별 연산(element-wise operation)이다. 이 연산은 텐서에 있는 각 원소에 독립적으로 적용된다.
    • 이 말은 고도의 병렬 구현(1970-1990년대 슈퍼컴퓨터의 구조인 벡터 프로세서(vector processor)에서 온 용어인 벡터화된 구현을 말한다)이 가능한 연산이라는 의미이다.
    • 파이썬에서 단순한 원소별 연산을 구현한다면 for 반복문을 사용해야 할 것이다.
    • (예시 코드 생략)
  • 사실 넘파이 배열을 다룰 때는 최적화된 넘파이 내장 함수로 이런 연산들을 처리할 수 있다.
    • 넘파이는 시스템에 설치된 BLAS(Basic Linear Algebra Subprogram) 구현에 복잡한 일들을 위임한다.
    • BLAS는 고도로 병렬화되고 효율적인 저수준의 텐서 조작 루틴이며, 전형적으로 포트란(Fortran)이나 C 언어로 구현되어 있다.
    • 넘파이는 원소별 연산을 엄청난 속도로 처리한다.

브로드캐스팅

  • Dense 층에서는 2D 텐서와 벡터를 더했는데, 크기가 다른 두 텐서가 더해질 때 무슨 일이 일어날까?
  • 모호하지 않고 실행 가능하다면 작은 텐서가 큰 텐서의 크기에 맞추어 브로드캐스팅(broadcasting) 된다. 브로드캐스팅은 두 단계로 이루어진다.
    • 큰 텐서의 ndim에 맞도록 작은 텐서에 (브로드캐스팅 축이라고 부르는) 축이 추가된다.
    • 작은 텐서가 새 축을 따라서 큰 텐서의 크기에 맞도록 반복된다.
  • 구현 입장에서 새로운 텐서가 만들어지면 매우 비효율적이므로 어떤 2D 텐서도 만들어지지 않는다. 반복된 연산은 완전히 가상적이다. 이 과정은 메모리 수준이 아니라 알고리즘 수준에서 일어난다.
    • 하지만 새로운 축을 따라 벡터가 반복된다고 생각하는 것이 이해하기 쉽다.

텐서 점곱

  • 텐서 곱셈(tensor product)이라고도 부르는 점곱 연산(dot operation)은 가장 널리 사용되고 유용한 텐서 연산이다. 원소별 연산과 반대로 입력 텐서의 원소들을 결합시킨다.
    • 넘파이, 케라스, 씨아노, 텐서플로에서 원소별 곱셈은 * 연산자를 사용한다.
    • 텐서플로에서는 dot 연산자가 다르지만 넘파이와 케라스는 점곱 연산에 보편적인 dot 연산자를 사용한다.
  • (예시 코드 생략)
  • 두 벡터의 점곱은 스칼라가 되므로 원소 개수가 같은 벡터끼리 점곱이 가능하다.
    • 행렬 x와 벡터 y 사이에서도 점곱이 가능하다. y와 x의 행 사이에서 점곱이 일어나므로 벡터가 반환된다.
    • (사실 행렬의 곱셈은 행렬의 각 벡터간 점곱(내적)의 결과이다)
  • 점곱은 임의의 축 개수를 가진 텐서에 일반화 된다. 가장 일반적인 용도는 두 행렬 간의 점곱일 것이다.

  • x, y, z는 직사각형 모양으로 그려져 있다. x의 행 벡터와 y의 열 벡터가 같은 크기여야 하므로 자동으로 x의 너비는 y의 높이와 동일해야 한다.
  • 더 일반적으로 크기를 맞추는 동일한 규칙을 따르면 다음과 같이 고차원 텐서 간의 점곱을 할 수 있다.

(a, b, c, d) \cdot (d, ) \to (a, b, c)

(a, b, c, d) \cdot (d, e) \to (a, b, c, e)

텐서 크기 변환

  • 꼭 알아두어야 할 세 번째 텐서 연산은 텐서 크기 변환(tensor reshaping)이다.
  • 텐서의 크기를 변환한다는 것은 특정 크기에 맞게 열과 행을 재배열한다는 뜻이다. 당연히 크기가 변환된 텐서는 원래 텐서와 원소 개수가 동일하다.
    • (예시 코드 생략)
  • 자주 사용하는 특별한 크기 변환은 전치(transposition)이다. 전치는 행과 열을 바꾸는 것을 의미한다.
    • 즉 x[i, :]이 x[:, i]가 된다.

텐서 연산의 기하학적 해석

  • 텐서 연산이 조작하는 텐서의 내용은 어떤 기하학적 공간에 있는 좌표 포인트로 해석될 수 있기 때문에 모든 텐서 연산은 기하학적 해석이 가능하다.
  • 다음 벡터를 보자.
A = [0.5, 1]
  • 이 포인트는 2-6 이미지와 같은 2D 공간에 있다. 일반적으로 2-7이미지와 같이 원점에서 포인트를 연결하는 화살표로 벡터를 나타낸다.

  • 새로운 포인트 B = [1, 0.25]를 이전 벡터에 더해 보자. 기하학적으로는 벡터 화살표를 연결하여 계산할 수 있다. 최종 위치는 두 벡터의 덧셈을 나타내는 벡터가 된다.

  • 일반적으로 아핀 변환(affine transformation), 회전, 스케일링(scaling) 등처럼 기본적인 기하학적 연ㅅ나은 텐서 연산으로 표현될 수 있다.
    • 예컨대 \theta 각도로 2D 벡터를 회전하는 것은 2 x 2 행렬 R = [u, v] 를 접곱하여 구할 수 있다.
    • 여기에서 u, v 는 동일 평면상의 벡터이며, u = [cos(\theta), sin(\theta)], v = [-sin(\theta), cos(\theta)] 이다.

딥러닝의 기하학적 해석

  • 신경망은 전체적으로 텐서 연산의 연결로 구성된 것이고, 모든 텐서 연산은 입력 데이터의 기하학적 변환임을 배웠다.
    • 단순한 단계들이 길게 이어져 구현된 신경망을 고차원 공간에서 매우 복잡한 기하학적 변환을 하는 것으로 해석할 수 있다.
  • 3D라면 다음 비유가 이해하는데 도움이 될 것이다.
    • 하나는 빨간색이고 다른 하나는 파란색인 2개의 색종이가 있다고 가정하자.
    • 두 장을 겹친 다음 뭉쳐서 작은 공으로 만든다.
    • 이 종이 공이 입력 데이터이고 색종이는 분류 문제의 데이터 클래스이다.
    • 신경망(또는 다른 머신 러닝 알고리즘)이 해야 할 일은 종이 공을 펼쳐서 두 클래스가 다시 깔끔하게 분리되는 변환을 찾는 것이다.
    • 손가락으로 종이 공을 조금씩 펼치는 것처럼 딥러닝을 사용하여 3D 공간에서 간단한 변환들을 연결해서 이를 구현한다.

  • 종이 공을 펼치는 것이 머신 러닝이 하는 일이다. 복잡하고 심하게 꼬여 있는 데이터의 매니폴드에 대한 깔끔한 표현을 찾는 일이다.
    • 이쯤되면 딥러닝이 왜 이런 작업에 뛰어난지 알았을 것이다.
    • 기초적인 연산을 길게 연결하여 복잡한 기하학적 변환을 조금씩 분해하는 방식이 마치 사람이 종이 공을 펼치기 위한 전략과 매우 흡사하기 때문이다.
  • 심층 네트워크의 각 층은 데이터를 조금씩 풀어 주는 변환을 적용하므로, 이런 층을 깊게 쌓으면 아주 복잡한 분해 과정을 처리할 수 있다.

신경망의 엔진: 그래디언트 기반 최적화

  • 이전 절에서 보았듯이 첫 번째 신경망 예제에 있는 각 층은 입력 데이터를 다음과 같이 변환한다.
output = relu(dot(W, intput) + b)
  • 이 식에서 텐서 W와 b는 층의 속성처럼 볼 수 있다.
    • 가중치(weight) 또는 훈련되는 파라미터(trainable parameter)라고 부른다. (각각 커널(kernel)과 편향(bias)라고 부르기도 한다) 이런 가중치에는 훈련 데이터를 신경망에 노출시켜서 학습된 정보가 담겨 있다.
  • 초기에는 가중치 행렬이 작은 난수로 채워져 있다. (무작위 초기화(random initialization) 단계라고 부른다)
    • 물론 W와 b가 난수일 때 relu(dot(W, input) + b)가 유용한 어떤 표현을 만들 것이라고 기대할 수는 없다.
    • 즉 의미없는 표현이 만들어진다. 하지만 이는 시작 단계일 뿐이다.
    • 그 다음에는 피드백 신호에 기초하여 가중치가 점진적으로 조정될 것이다.
    • 이런 점진적인 조정 또는 훈련(training)이 머신 러닝 학습의 핵심이다.
  • 훈련은 다음과 같은 훈련 반복 루프(training loop) 안에서 일어난다. 필요한 만큼 반복 루프 안에서 이런 단계가 반복된다.
    1. 훈련 샘플 x와 이에 상응하는 타깃 y의 배치를 추출한다.
    2. x를 사용하여 네트워크를 실행하고(정방향 패스(forward pass) 단계), 예측 y_pred를 구한다.
    3. y_pred와 y의 차이를 측정하여 이 배치에 대한 네트워크의 손실을 계산한다.
    4. 배치에 대한 손실이 조금 감소되도록 네트워크의 모든 가중치를 업데이트한다.
  • 결국 훈련 데이터에서 네트워크의 손실, 즉 예측 y_pred와 타깃 y의 오차가 매우 작아질 것이다.
    • 이 네트워크는 입력에 정확한 타깃을 매핑하는 것을 학습했다.
    • 전체적으로 보면 마술처럼 보이지만 개별적인 단계로 쪼개어보면 단순하다.
  • 1단계는 입출력 코드이므로 매우 쉽다. 2-3단계는 몇 개의 텐서 연산을 적용할 뿐이므로 이전 절에 배웠던 연산을 사용하여 이 단계를 구현할 수 있다.
    • 어려운 부분은 네트워크의 가중치를 업데이트하는 4단계이다. 개별적인 가중치 값이 있을 때 값이 증가해야 할지 감소해야 할지 또 얼마만큼 업데이트해야 할지 어떻게 알 수 있을까?
  • 한 가지 간단한 방법은 네트워크 가중치 행렬의 원소를 모두 고정하고 관심 있는 하나만 다른 값을 적용해 보는 것이다.
    • 이 가중치의 초깃값이 0.3이고 배치 데이터를 정방향 패스에 통과시킨 후 네트워크 손실이 0.5가 나왔다고 하자.
    • 이 가중치를 0.35로 변경하고 다시 정방향 패스를 실행했더니 손실이 0.6으로 증가했다.
    • 반대로 0.25로 줄이면 손실이 0.4로 감소했다. 이 경우에 가중치를 -0.05만틈 업데이트한 것이 손실을 줄이는데 기여한 것으로 보인다.
    • 이런 식으로 네트워크의 모든 가중치에 반복한다.
  • 이런 접근 방식은 모든 가중치 행렬의 원소마다 두 번의 (비용이 큰) 정방향 패스를 계산해야 하므로 엄청나게 비효율적이다. (보통 수천에서 경우에 따라 수백만 개의 많은 가중치가 있다.)
    • 신경망에 사용된 모든 연산이 미분가능(differentiable) 하다는 장점을 사용하여 네트워크 가중치에 대한 손실의 그래디언트(gradient)를 계산하는 것이 훨씬 더 좋은 방법이다.
    • 그래디언트의 반대 방향으로 가중치를 이동하면 손실이 감소된다.

변화율이란?

  • 실수 x를 새로운 실수 y로 매핑하는 연속적이고 매끄러운 함수 f(x) = y를 생각해 보자.
    • 이 함수가 연속적이므로 x를 조금 바꾸면 y가 조금 변경될 것이다. 이것이 연속성의 개념이다.
    • x를 작은 값 epsilon_x 만큼 증가시켰을 때 y가 epsilon_y 만큼 바뀐다고 말할 수 있다.
f(x + eplsion_x) = y + epsion_y
  • 또 이 함수가 매끈하므로(곡선의 각도가 갑자기 바뀌지 않는다) epsilon_x가 충분히 작다면 어떤 포인트 p에서 기울기 a의 선형 함수로 f를 근사할 수 있다.
    • 따라서 epsilon_y는 a * epsilon_x 가 된다.
f(x + eplsion_x) = y + a * epsion_x
  • 이 선형적인 근사는 x가 p에 충분히 가까울 때 유효하다.
  • 이 기울기를 p에서 f의 변화율(derivative)라고 한다.
    • 이는 a가 음수일 때 p에서 양수 x만큼 조금 이동하면 f(x)가 감소한다는 것을 의미한다.
    • a가 양수일 때는 음수 x만큼 조금 이동하면 f(x)가 감소된다.
    • a의 절댓값(변화율의 크기)은 이런 증가나 감소가 얼마나 빠르게 일어날지 알려준다.

  • 모든 미분 가능한(미분 가능하다는 것은 변화율을 유도할 수 있다는 의미로, 예컨대 매끄럽고 연속적인 함수이다) 함수 f(x)에 대해 x의 값을 f의 국부적인 선형 근사인 그 지점의 기울기로 매핑하는 변화율 함수 f'(x)가 존재한다.
    • 예컨대 cos(x)의 변화율은 -sin(x)이고, f(x) = a * x의 변화율은 f'(x) = a이다.
  • f(x)를 최소화하기 위해 epsilon_x 만큼 x를 업데이트하고 싶을 때 f의 변화율을 알고 있으면 해결된다.
    • 변화율 함수는 x가 바뀜에 따라 f(x)가 어떻게 바뀔지 설명해 준다.
    • f(x)의 값을 감소 시키고 싶다면 x를 변화율의 방향과 반대로 조금 이동해야 한다.

텐서 연산의 변화율: 그래디언트

  • 그래디언트는 텐서 연산의 변화율이다. 이는 다차원 입력, 즉 텐서를 입력으로 받는 함수에 변화율 개념을 확장시킨 것이다.
  • 입력 벡터 x, 행렬 W, 타깃 y와 손실 함수 loss가 있다고 가정하자.
    • W를 사용하여 타깃의 예측 y_pred를 계산하고 손실, 즉 타깃 예측 y_pred와 타깃 y사이의 오차를 계산할 수 있다.
y_pred = dot(W, x)
loss_value = loss(y_pred, y)
  • 입력 데이터 x와 y가 고정되어 있다면 이 함수는 W를 손실 값에 매핑하는 함수로 볼 수 있다.
loss_value = f(W)
  • W의 현재 값을 W0라고 하자.
    • 포인트 W0에서 f의 변화율은 W와 같은 크기의 텐서인 gradient(f)(W0)이다.
    • 이 텐서의 각 원소 gradient(f)(W0)[i, j]는 W0[i, j]를 변경했을 때 loss_value가 바뀌는 방향과 크기를 나타낸다.
    • 다시 말해 gradient(f)(W0)가 W0)에서 함수 f(W) = loss_value의 그래디언트이다.
  • 앞서 함수 f(x)의 변화율 하나는 곡선 f의 기울기로 해석할 수 있다는 것을 보았다. 비슷하게 gradient(f)(W0)는 W0에서 f(W)의 기울기를 타나내는 텐서로 해석할 수 있다.
    • 그렇기 때문에 함수 f(x)에 대해서는 변화율의 반대 방향으로 x를 조금 움직이면 f(x)의 값을 감소시킬 수 있다.
    • 동일한 방식을 적용하면 함수 f(W)의 입장에서는 그래디언트의 반대 방향으로 W를 움직이면 f(W)의 값을 줄일 수 있다.
    • 예컨대 W1 = W0 – step * gradient(f)(W0)이다. (step은 스케일을 조정하기 위한 작은 값이다)
    • 이 말은 기울기가 작아지는 곡면의 낮은 위치로 이동된다는 의미이다.
    • gradient(f)(W0)는 W0)에 아주 가까이 있을 때 기울기를 근사한 것이므로 W0에서 너무 크게 벗어나지 않기 위해 스케일링 비율 step이 필요하다.

확률적 경사 하강법

  • 미분 가능한 함수가 주어지면 이론적으로 이 함수의 최솟값을 해석적으로 구할 수 있다.
    • 함수의 최솟값은 변화율이 0인 지점이다. 따라서 우리가 할 일은 변화율이 0이 되는 지점을 모두 찾고 이 중에서 어떤 포인트의 함수 값이 가장 작은지 확인하는 것이다.
  • 신경망에 적용하면 가장 작은 손실 함수의 값을 만드는 가중치의 조합을 해석적으로 찾는 것을 의미한다.
    • 이는 식 gradient(f)(W) = 0을 풀면 해결된다. 이 식은 N개의 변수로 이루어진 다항식이다.
    • 여기서 N은 네트워크 가중치 개수이다. N=2, N=3인 식을 푸는 것은 가능하지만 실제 신경망에서는 파라미터의 개수가 수천 개보다 적은 경우가 거의 없고 종종 수천만 개가 되기 때문에 해석적으로 해결하는 것이 어렵다.
  • 그 대신 앞서 설명한 알고리즘 네 단계를 사용할 수 있다. 랜덤한 배치 데이터에서 현재 손실 값을 토대로 하여 조금씩 파라미터를 수정하는 것이다.
    • 미분 가능한 함수를 가지고 있으므로 그래디언트를 계산하여 단계 4를 효율적으로 구현할 수 있다. 그래디언트 방향으로 가중치를 업데이트 하면 손실이 매번 조금씩 감소할 것이다.
  • 그래디언트 계산을 반영하여 업데이트
    1. 훈련 샘플 x와 이에 상응하는 타깃 y의 배치를 추출한다.
    2. x를 사용하여 네트워크를 실행하고 예측 y_pred를 구한다.
    3. y_pred와 y의 차이를 측정하여 이 배치에 대한 네트워크의 손실을 계산한다.
    4. 네트워크의 파라미터에 대한 손실 함수의 그래디언트를 계산한다(역방향 패스(backward pass))
    5. 그래디언트의 반대 방향으로 파라미터를 조금 이동시킨다. 예컨대 W -= step * gradient 처럼 하면 배치에 댛나 손실이 조금 감소할 것이다.
  • 이것이 바로 미니 배치 확률적 경사 하강법(mini-batch stochastic gradient descent)(미니 배치 SGD)이다.
    • 확률적(stochastic)이란 단어는 각 배치 데이터가 무작위로 선택된다는 의미이다. (확률적이란 것은 무작위(random) 하다는 것의 과학적 표현이다.)
    • 네트워크의 파라미터와 훈련 샘플이 하나일 때 이 과정을 아래 그림에 나타냈다.

  • 그림에서 볼 수 있듯이 step 값을 적절히 고르는 것이 중요하다.
    • 이 값이 너무 작으면 곡선을 따라 내려가는데 너무 많은 반복이 필요하고 지역 최솟값(local minimum)에 갇힐 수 있다.
    • step이 너무 크면 손실 함수 곡선에서 완전히 임의의 위치로 이동시킬 수 있다.
  • 미니 배치 SGD 알고리즘의 한 가지 변종은 반복마다 하나의 샘플과 하나의 타깃을 뽑는 것이다.
    • 이것이 (미니 배치 SGD와 반대로) 진정한(true) SGD이다.
    • 다른 한편으로 극단적인 반대의 경우를 생각해 보면 가용한 모든 데이터를 사용하여 반복을 실행할 수 있다.
    • 이를 배치 SGD (batch SGD)라고 한다. 더 정확하게 업데이트 되지만 더 많은 비용이 든다.
    • 극단적인 두 가지 방법의 효율적인 절충안은 적절한 크기의 미니 배치를 사용하는 것이다.
  • 그림 2-11은 1D 파라미터 공간에서 경사 하강법을 설명하고 있지만 실제로는 매우 고차원 공간에서 경사 하강법을 사용하게 된다.
    • 신경망에 있는 각각의 가중치 값은 이 공간에서 하나의 독립된 차원이고 수만 또는 수백만 개가 될 수도 있다.
    • 손실 함수의 표면을 좀 더 쉽게 이해하기 위해 아래 그림 2-12와 같이 2D 손실 함수의 표면을 따라 진행하는 경사 하강법을 시각화해 볼 수 있다.

  • 하지만 신경망이 훈련되는 실제 과정을 시각화하기는 어렵다. 4차원 이상의 공간을 사람이 이해할 수 있도록 표현하는 것이 불가능하기 때문이다.
    • 때문에 저차원 표현으로 얻은 직관이 실전과 항상 맞지 않는다는 것을 유념해야 한다. 이는 딥러닝 연구 분야에서 오랫동안 여러 이슈를 일으키는 근원이었다.
  • 또 업데이트할 다음 가중치를 계산할 때 현재 그래디언트 값만 보지 않고 이전에 업데이트도니 가중치를 여러 다른 방식으로 고려하는 SGD 변종이 많이 있다.
    • 예컨대 모멘텀을 사용한 SGD, Adagrad, RMSProp 등이 그것이다.
    • 이런 변종들은 모두 최적화 방법(optimization method) 또는 옵티마이저라고 부른다.
  • 특히 여러 변종들에서 사용하는 모멘텀(momentum) 개념은 아주 중요하다. 모멘텀은 SGD에 이는 2개의 문제점인 수렴 속도와 지역 최솟값을 해결한다.
    • 아래 그림 2-13은 네트워크의 파라미터 하나에 대한 손실 값의 곡선을 보여준다.

  • 어떤 파라미터 값에서는 지역 최솟값에 도달한다. 그 지점 근처에서는 왼쪽으로 이동해도 손실이 증가하고 오른쪽으로 이동해도 손실이 증가하기 때문이다.
    • 대상 파라미터가 작은 학습률을 가진 SGD로 최적화되었다면 최적화 과정이 전역 최솟값으로 향하지 못하고 이 지역 최솟값에서 갇히게 될 것이다.
  • 물리학에서 영감을 얻은 모멘텀을 사용하여 이 문제를 피할 수 있다. 여기에서 최적화 과정을 손실 곡선 위로 작은 공을 굴리는 것으로 생각하면 쉽게 이해할 수 있다.
    • 모멘텀이 충분함녀 공이 골짜기에 갇히지 않고 전역 최솟값에 도달할 것이다.
    • 모멘텀은 현재 기울기 값(현재 가속도) 뿐만 아니라 (과거의 가속도로 인한) 현재 속도를 함께 고려하여 각 단계에서 공을 움직인다.
    • 실전에 적용할 때는 현재 그래디언트 값 뿐만 아니라 이전에 업데이트한 파라미터에 기초하여 파라미터 w를 업데이트 한다.
  • 다음은 단순한 구현 예이다.
past_velocity = 0
momentum = 0.1 --모멘텀 상수
while loss > 0.01: --최적화 반복 루프
w, loss, gradient = get_current_parameters()
velocity = momentum * past_velocity - learning_rate * gradient
w = w + momentum * velocity - learning_rate * gradient
past_velocity = velocity
update_parameter(w)

변화율 연결: 역전파 알고리즘

  • 앞의 알고리즘에서 함수가 미분 가능하기 때문에 변화율을 직접 계산할 수 있다고 가정했다.
    • 실제로 신경망은 많은 텐서 연산으로 구성되어 있고 이 연산들의 변화율은 간단하며 이미 잘 알려져 있다.
    • 3개의 텐서 연산 a, b, c와 가중치 행렬 W1, W2, W3으로 구성된 네트워크 f를 예로 들어보자.
f(W1, W2, W3) = a(W1, b(W2, c(W3)))
  • 이벅분에서 이렇게 연결된 함수는 연쇄 법칙(chain rule)이라 부르는 다음 항등식 f(g(x))’ = f'(g(x)) * g'(x)를 사용하여 유도될 수 있다.
    • 연쇄 법칙을 신경망의 그래디언트를 계산에 적용하여 역전파(Backpropagation) 알고리즘(후진 모드 자동 미분(reverse-mode automatic differentiation)이라고도 부른다)이 탄생되었다.
    • 역전파는 최종 손실 값에서부터 시작한다.
    • 손실 값에 각 파라미터가 기여한 정도를 계산하기 위해 연쇄 법칙을 적용하여 최상위 층에서 하위층까지 거꾸로 진행된다.
  • 요즘에는 그리고 향후 몇 년 동안은 텐서플로처럼 기호 미분(symbolic differentiation)이 가능한 최신 프레임워크를 사용하여 신경망을 구현할 것이다.
    • 이 말은 변화율이 알려진 연산들로 연결되어 있으면 (연쇄 법칙을 적용하여) 네트워크 파라미터와 그래디언트 값을 매핑하는 그래디언트 함수를 계산할 수 있다는 의미이다.
    • 이런 함수를 사용하면 역방향 패스는 그래디언트 함수를 호출하는 것으로 단순화될 수 있다.
    • 기호 미분 덕택에 역전파 알고리즘을 직접 구현할 필요가 전혀 없고 정확한 역전파 공식을 유도하느라 시간과 노력을 소모하지 않아도 된다. 그래디언트 기반의 최적화가 어떻게 작동하는지 잘 이해하는 것만으로 충분하다.

첫 번째 예제 다시 살펴보기

  • 신경망의 이면에 어떤 원리가 있는지 기초적인 내용을 이해했으므로 첫 번째 예제로 돌아가서 이전 절에서 배웠던 내용을 이용하여 코드를 리뷰해 보자.
  • 먼저 입력 데이터이다.
(train_images, train_labels), (test_images, test_labels) = mnist.load_data()

train_images = train_images.reshape((60000, 28 * 28))
train_images = train_images.astype('float32') / 255

test_images = test_images.reshape((10000, 28 * 28))
test_images = test_images.astype('float32') / 255
  • 입력 이미지의 데이터 타입은 float32, 훈련 데이터는 (60000, 784) 크기, 테스트 데이터는 (10000, 784) 크기의 넘파이 배열로 저장된다.
  • 우리가 사용할 신경망이다.
network = models.Sequential()
network.add(layers.Dense(512, activation='relu', input_shape=(28 * 28,)))
network.add(layers.Dense(10, activation='softmax'))
  • 이 네트워크는 2개의 Dense 층이 연결되어 있고 각 층은 가중치 텐서를 포함하여 입력 데이터에 대한 몇 개의 간단한 텐서 연산을 적용한다.
    • 층의 속성인 가중치 텐서는 네트워크가 정보를 저장하는 곳이다.
  • 이제 네트워크를 컴파일 하는 단계이다.
network.compile(optimizer='rmsprop', loss='categorical_crossentropy', metrics=['accuracy'])
  • categorical_crossentropy는 손실 함수이다. 가중치 텐서를 학습하기 위한 피드백 신호로 사용되며 훈련하는 동안 최소화 된다.
    • 미니 배치 확률적 경사 하강법을 통해 손실이 감소된다.
    • 경사 하강법을 적용하는 구체적인 방식은 첫 번째 매개변수로 전달된 rmsprop 옵티마이저에 의해 결정된다.
  • 마지막으로 훈련 반복이다.
network.fit(train_images, train_labels, epochs=5, batch_size=128)
  • fit 메서드를 호출했을 때 다음과 같은 일이 일어난다.
    • 네크워크가 128개 샘플씩 미니 배치로 훈련 데이터를 다섯 번 반복한다(전체 훈련 데이터에 수행되는 각 반복을 에포크(epoch)라고 한다.)
    • 각 반복마다 네트워크가 배치에서 손실에 대한 가중치의 그래디언트를 계산하고 그에 맞추어 가중치를 업데이트 한다.
    • 다섯 번의 에포크 동안 네트워크는 2,345번의 그래디언트 업데이트를 수행할 것이다 (에포크마다 469번)

요약

  • 학습(Learning)은 훈련 데이터 샘플과 그에 상응하는 타깃이 주어졌을 때 손실 함수를 최소화 하는 모델 파라미터의 조합을 찾는 것을 의미한다.
  • 데이터 샘플과 타깃의 배치를 랜덤하게 뽑고 이 배치에서 손실에 대한 파라미터의 그래디언트를 계산함으로써 학습이 진행된다. 네트워크의 파라미터는 그래디언트의 반대 방향으로 조금씩(학습률에 의해 정의된 크기만큼) 움직인다.
  • 전체 학습 과정은 신경망이 미분 가능한 텐서 연산으로 연결되어 있기 때문에 간으하다. 현재 파라미터와 배치 데이터를 그래디언트 값에 매핑해주는 그래디언트 함수를 구성하기 위해 미분의 연쇄 법칙을 사용한다.
  • 이어지는 장에서 자주 보게 될 두 가지 핵심 개념은 손실과 옵티마이저이다. 이 두 가지는 네트워크에 데이터를 주입하기 전에 정의 되어야 한다.
  • 손실은 훈련하는 동안 최소화해야 할 양이므로 해결하는 문제의 성공을 측정하는데 사용한다.
  • 옵티마이저는 손실에 대한 그래디언트가 파라미터를 업데이트하는 정확한 방식을 정의한다. 예컨대 RMSProp 옵티마이저, 모멘텀을 사용한 SGD 등이다.

케라스 창시자에게 배우는 딥러닝/ 딥러닝이란 무엇인가?

인공 지능과 머신 러닝, 딥러닝

인공 지능

  • 인공 지능은 1950년대 초기 컴퓨터 과학 선각자들이 ‘컴퓨터가 생각할 수 있는가?’라는 질문을 하면서 시작되었음
  • 이 분야에 대한 간결한 정의는 ‘보통의 사람이 수행하는 지능적인 작업을 자동화하기 위한 연구활동’이다.
    • AI는 머신 러닝과 딥러닝을 포괄하는 종합적인 분야로 학습 과정이 전혀 없는 다른 방법도 많이 포함된다.
  • 오랜 기간 많은 전문가들이 프로그래머들이 명시적인 규칙을 충분히 많이 만들어 지식을 다루면 인간 수준의 인공 지능을 만들 수 있다고 믿었는데, 이런 접근 방법을 심볼릭 AI(Symbolic AI)라고 하며, 1950-1980년대까지 AI 분야의 지배적인 패러다임이었다.
    • 1980년대 전문가 시스템(expert system)의 호황으로 그 인기가 절정에 달했다.
  • 심볼릭 AI가 체스 처럼 잘 정의된 논리적인 문제를 푸는데는 적합하다는 것이 증명되었지만, 이미지 분류, 음성 인식, 언어 번역 같은 더 복잡하고 불분명한 문제를 해결하기 위한 명확한 규칙을 찾는 것은 아주 어려운 일이었다. 이런 심볼릭 AI를 대체하기 위한 새로운 방법이 바로 머신 러닝이다.

머신 러닝

  • 최초의 기계적 범용 컴퓨터인 해석기관(Analytical Engine)을 발명한 사람은 찰스 배비지(Charles Babbage)였다.
    • 범용 컴퓨터란 개념이 정의되지 않은 때였기 때문에 해석 기관은 해석학(mathematical analysis) 분야의 계산을 자동화 하기 위해 기계적인 연산을 사용하는 방법이었을 뿐이었다. 그래서 이름이 해석 기관이다.
  • 찰스 배비지의 친구자이 동료였던 –더불어 최초의 프로그래머로 인정받는– 에이다 러브레이스(Ada Lovelace)는 이 발명에 대해 다음과 같이 언급했다.
    • “해석 기관이 무언가를 새롭게 고안해 내는 것은 아니다. 우리가 어떤 것을 작동시키기 위해 어떻게 명령할지 알고 있다면 이 장치는 무엇이든 할 수 있다. … 이런 능력은 우리가 이미 알고 있는 것을 유용하게 사용할 수 있도록 도와줄 것이다”
  • AI의 선구자인 앨런 튜링(Alan Turing)은 그의 기념비적인 논문 “Computing Machinery and Intelligence”에서 ‘러브레이스의 반론(Lady Lovelace’s objection)’으로 이 논평을 인용했다.
    • 튜링은 에이다 러브레이스의 말을 인용했지만 범용 컴퓨터가 학습과 창의력을 가질 수 있는지에 대해 가능한 일이라고 결론을 냈다.
  • 머신 러닝은 이런 질문에서 시작된다.
    • 우리가 어떤 것을 작동시키기 위해 ‘어떻게 명령할지 알고 있는 것’ 이상을 컴퓨터가 처리하는 것이 가능한가?
    • 특정 작업을 수행하는 법을 스스로 학습할 수 있는가?
    • 컴퓨터가 우리를 놀라게 할 수 있을까?
    • 프로그래머가 직접 만든 데이터 처리 규칙 대신 컴퓨터가 데이터를 보고 자동으로 이런 규칙을 학습할 수 있을까?
  • 이 질문은 새로운 프로그래밍 패러다임의 장을 열었다. 전통적인 프로그래밍인 심볼릭 AI의 패러다임에서는 규칙과 이 규칙에 따라 처리될 데이터를 입력하면 해답이 출력된다.
    • 머신 러닝에서는 데이터와 이 데이터로부터 기대되는 해답을 입력하면 규칙이 출력된다. 이 규칙을 새로운 데이터에 적용하여 창의적인 답을 만들 수 있다.

  • 머신 러닝 시스템은 명시적으로 프로그램 되는 것이 아니라 훈련(training) 된다.
    • 작업과 관련 있는 많은 샘플을 제공하면 이 데이터에서 통계적 구조를 찾아 그 작업을 자동화하기 위한 규칙을 만들어낸다.
  • 머신 러닝은 1990년대 들어서야 각광을 받기 시작했지만, 고성능 하드웨어와 대량의 데이터셋이 가능해지면서 AI에서 가장 인기 있고 성공적인 분야가 되었다.
  • 머신 러닝은 수리 통계와 밀접하게 관련되어 있지만 통계와 다른 점이 몇 가지 있다.
    • 먼저 머신 러닝은 통계와 달리 보통 대량의 복잡한 데이터셋을 다루기 때문에 베이지안 분석(Bayesian analysis) 같은 전통적인 통계 분석 방법은 현실적으로 적용하기 어렵다.
    • 이런 이유로 머신 러닝, 특히 딥러닝은 수학적 이론이 비교적 부족하고 엔지니어링 지향적이다.
    • 이런 실천적인 접근 방식 때문에 이론보다 경험을 바탕으로 아이디어가 증명되는 경우가 많다.

데이터에서 표현을 학습하기

  • 머신 러닝은 샘플과 기댓값이 주어졌을 때 데이터 처리 작업을 위한 실행 규칙을 찾는 것이다. 머신 러닝을 하기 위해서는 세 가지가 필요하다.
    • 입력 데이터 포인트 –음성 파일, 이미지 파일 등
    • 기대 출력 –강아지, 고양이 등
    • 알고리즘의 성능을 측정하는 방법 –알고리즘의 현재 출력과 기대 출력 간의 차이를 결정하기 위해 필요. 측정 값은 알고리즘의 작동 방식을 교정하기 위한 신호로 다시 피드백되는데, 이런 수정 단계를 학습(learning)이라고 한다.
  • 머신 러닝 모델은 입력 데이터를 의미 있는 출력으로 변환한다. 이것이 알고 있는 입력과 출력의 샘플로부터 학습하는 과정이다. 그렇기 때문에 머신 러닝과 딥러닝의 핵심 문제는 의미있는 데이터로의 변환이다.
    • 다시 말해 입력 데이터를 기반으로 기대 출력에 가깝게 만드는 유용한 표현(representation)을 학습하는 것이다.
    • 여기서 표현이란 데이터를 인코딩(encoding)하거나 묘사하기 위해 데이터를 바라보는 다른 방법이다. 예컨대 컬러 이미지는 RGB, HSV 포맷으로 인코딩될 수 있는데, 이들은 같은 데이터의 다른 표현이다.
    • 어떤 표현으로는 해결하기 힘든 문제가 다른 표현으로는 쉽게 해결될 수 있다. 예컨대 이미지에서 빨간색 픽셀을 선택하는 것은 RGB 포맷이 쉽고, 이미지의 채도를 낮추는 것은 HSV 포맷이 더 쉽다.
    • 머신 러닝 모델은 입력 데이터에서 적절한 표현을 찾는 것이다. 이런 데이터 변환은 분류 작업 같은 문제를 더 쉽게 해결할 수 있도록 만들어 준다.
  • 예들 들어보자. x, y축이 있고 이 x, y 좌표 시스템으로 표현된 데이터 포인트가 아래 그림과 같다고 하자.

  • 위 그림에는 흰색점과 빨간색점이 존재하는데, 포인트의 좌표 (x, y)를 입력으로 받고 그 포인트가 빨간색인지 흰색인지를 출력하는 알고리즘을 개발한다고 하자. 이 경우는 다음과 같이 요약할 수 있다.
    • 입력은 점의 좌표이다.
    • 기대 출력은 점의 색깔이다.
    • 알고리즘의 성능을 측정하는 방법은 정확히 분류한 포인트의 비율을 사용하여 알고리즘의 성능을 측정한다.
  • 여기서 우리가 원하는 것은 흰색점과 빨간색점을 완벽하게 구분하는 새로운 데이터표현이다. 사용할 수 있는 변환 방법 중 하나는 아래 그림과 같은 좌표 변환이다.

  • 포인트에 대한 새로운 좌표는 새로운 데이터 표현이라고 할 수 있다. 그리고 좋은 표현을 찾았다.
    • 이 표현을 사용하면 색깔 분류 문제를 “x > 0 인 것은 빨간색 점이다”와 같은 간단한 규칙으로 나타낼 수 있다.
    • 기본적으로 이 분류 문제를 해결한 것은 새로운 표현이다.
  • 이 경우 우리가 직접 좌표 변환을 정했는데, 만약 시스템적으로 가능한 여러 좌표 변환을 찾아서 포인트 중 몇 퍼센트가 정확히 분류되었는지를 피드백으로 사용한다면, 바로 머신 러닝을 하고 있는 것이다.
    • 이처럼 머신 러닝에서의 학습(Learning)이란 더 나은 표현을 찾는 자동화된 과정이다.
  • 모든 머신 러닝 알고리즘은 주어진 작업을 위해 데이터를 더 유용한 표현으로 바꾸는 이런 변환을 자동으로 찾는다.
    • 이 연산은 앞서 본 좌표 변환일 수도 있고 또는 선형 투영(linear projection), 이동(translation), 비선형 연산 등이 될 수 있다.
    • 머신 러닝 알고리즘은 일반적으로 이런 변환을 찾기 위한 창의력은 없다. 가설 공간(hypothesis space)이라 부르는 미리 정의된 연산의 모음들을 자세히 조사하는 것 뿐이다.
  • 기술적으로 말하면 머신 러닝은 가능성 있는 공간을 사전에 정의하고 피드백 신호의 도움을 받아 입력 데이터에 대한 유용한 변환을 찾는 것이다.
    • 이 간단한 아이디어가 음성 인식에서부터 자율 주행 자동차까지 아주 다양한 분야에서 지능에 관한 문제를 해결한다.

딥러닝에서 ‘딥’이란 무엇일까?

  • 딥러닝은 머신 러닝의 특정한 한 분야로서 연속된 층(layer)에서 점진적으로 의미 있는 표현을 배우는데 강점이 있으며, 데이터로부터 표현을 학습하는 새로운 방식이다.
    • 딥러닝의 딥(deep)이란 단어는 연속된 층으로 표현을 학습한다는 개념을 나타낸다.
    • 데이터로부터 모델을 만드는데 얼마나 많은 층을 사용했는지가 그 모델의 깊이가 된다.
    • 이 분야에 대한 적절한 다른 이름은 층 기반 표현 학습(layered representations learning) 또는 계층적 표현 학습(hierarchical representations learning)이 될 수 있다.
    • 최근 딥러닝 모델은 표현 학습을 위해 수십 개, 수백 개의 연속된 층을 갖고 있다. 이 층들을 모두 훈련 데이터에 노출해서 자동으로 학습시킨다.
    • 한편 다른 머신 러닝 접근 방법은 1-2개의 데이터 표현 층을 학습하는 경향이 있다. 그래서 이런 방식을 얕은 학습(shallow learning)이라 부르기도 한다.
  • 딥러닝에서는 기본 층을 겹겹이 쌓아 올려 구성한 신경망(neural network)이라는 모델을 사용하여 표현 층을 학습한다.
    • 신경망이란 단어는 신경 생물학의 용어로 딥러닝의 일부 핵심 개념이 뇌 구조를 이해하는 것에서 영감을 얻어 개발된 부분이 있지만, 딥러닝 모델이 뇌를 모델링한 것은 아니다.
    • 최근의 딥러닝 모델이 사용하는 학습 메커니즘과 유사한 것을 뇌가 가지고 있다는 근거는 없다. 대중 과학 저널에서 딥러닝이 뇌처럼 작동한다거나 뇌를 모방하여 만들었다고 주장하는 그은 사실이 아니다.
    • 딥러닝은 그냥 데이터로부터 표현을 학습하는 수학 모델일 뿐이다.
  • 딥러닝 알고리즘으로 학습된 표현은 어떻게 나타나는지 예를 들어보자. 몇 개의 층으로 이루어진 네트워크가 이미지 안의 숫자를 인식하기 위해 이미지를 어떻게 변환하는지 살펴보자.

  • 아래 그림에서 볼 수 있듯 최종 출력에 대해 점점 더 많은 정보를 가지지만 원본 이미지와는 점점 더 다른 표현으로 숫자 이미지가 변환된다.
    • 심층 신경망을 정보가 연속된 필터(filter)를 통과하면서 순도 높게 정제되는 다단계 정보 추출 작업으로 생각할 수 있다.

  • 바로 이것이 딥러닝이다. 기술적으로는 데이터 표현을 학습하기 위한 다단계 처리 방식을 말한다.

그림 3개로 딥러닝의 작동 원리 이해하기

  • 머신 러닝이 많은 입력과 타깃(target)의 샘플을 관찰하면서 입력을 타깃에 매핑(mapping) 하는 것임을 알았다.
    • 심층 신경망은 이런 입력-타깃 매핑을 간단한 데이터 변환기(층)를 많이 연결하여 수행한다는 것도 배웠다. 이런 데이터 변환은 샘플에 노출됨으로써 학습이 이루어진다.
  • 층에서 입력 데이터가 처리되는 상세 내용은 일련의 숫자로 이루어진 층의 가중치(weight)에 저장되어 있다.
    • 기술적으로 말하면 어떤 층에서 일어나는 변환은 그 층의 가중치를 파라미터(parameter)로 가지는 함수로 표현된다. (이따금 가중치를 그 층의 파라미터라고도 부른다)
    • 이런 맥락으로 보면 학습은 주어진 입력을 정확한 타깃에 매핑하기 위해 신경마으이 모든 층에 있는 가중치 값을 찾는 것을 의미한다.
    • 하지만 어떤 심층 신경망은 수천만 개의 파라미터를 가지기도 한다.
    • 이런 경우 모든 파라미터의 정확한 값을 찾는 것은 어려운 일로 보인다. 파라미터 하나의 값을 바꾸면 다른 모든 파라미터에 영향을 끼치기 때문이다.

  • 어떤 것을 조정하려면 먼저 관찰해야 한다. 신경망의 출력을 제어하려면 출력이 기대하는 것보다 얼마나 벗어났는지를 측정해야 한다.
    • 이는 신경망의 손실 함수(loss function) 또는 목적 함수(objective function)가 담당하는 일이다.
    • 신경망이 한 샘플에 대해 얼마나 잘 예측했는지 측정하기 위해 손실 함수가 신경망의 예측과 진짜 타깃의 차이를 점수로 계산한다. (아래 그림 참고)

  • 기본적인 딥러닝 방식은 이 점수를 피드백 신호로 사용하여 현재 샘플의 손실 점수가 감소되는 방향으로 가중치 값을 조금씩 수정하는 것이다.
    • 이런 수렴 과정은 딥러닝의 핵심 알고리즘인 역전파(Backpropagation) 알고리즘을 구현한 옵티마이저(optimizer)가 담당한다.

  • 초기에는 네트워크의 가중치가 랜덤한 값으로 할당되므로 랜덤한 변환을 연속적으로 수행한다.
    • 자연스럽게 출력은 기대한 것과 멀어지고 손실 점수가 높을 것이지만 네트워크가 모든 샘플을 처리하면서 가중치가 조금씩 올바른 방향으로 조정되고 손실 점수가 감소한다.
    • 이를 훈련 반복(training loop)이라고 하며, 충분한 횟수만큼 반복하면 손실 함수를 최소화하는 가중치 값을 산출한다.
    • 최소한의 손실을 내는 네트워크가 타깃에 가능한 가장 가까운 출력을 만든느 모델이 된다.

지금까지 딥러닝의 성과

  • 딥러닝은 머신 러닝의 오래된 하위 분야지만 2010년 초가 되서야 유명해졌다. 이때부터 몇 년 동안 이 분야에서 일어난 혁신은 결코 작지 않다.
    • 기계가 오랫동안 해결하기 어려웠던 시각과 청각 같은 지각의 문제에서 괄목할만한 성과를 냈기 때문이다.
    • (이하 성과 사례 생략)

단기간의 과대 선전을 믿지 말자

  • 딥러닝이 최근 몇 년간 놀라운 성과를 냈지만, 사람들이 향후 10년 안에 기대하는 성과는 가능한 것보다 훨씬 높다.
    • 자율 주행 같은 몇 가지 애플리케이션은 이미 가까이 다가왔지만, 신뢰할 만한 대화 시스템이나 사람 수준의 기계 번역 그리고 사람 수준의 자연어 이해처럼 더 많은 것이 오랫동안 어려운 문제로 남아 있을 것 같다.
    • 특히 사람 수준의 일반 지능(general intelligence)에 관한 이야기는 너무 심각하게 다루지 않는 것이 좋다.
  • 과거에 AI는 장미빛 전망 뒤에 이어진 실망과 회의의 사이클을 두 번이나 경험했다.
  • 첫번째는 1960년대 심볼릭 AI였는데, 심볼릭 AI 방법에 관한 가장 유명한 개척자이지 지지자 중 한 명인 마빈 민스키(Marvin Minsky)는 1967년에 “이번 세대 안에 … ‘인공 지능’을 만드는 문제는 거의 해결될 것이다”라고 주장했다.
    • 몇 년 후 이런 높은 기대가 구체화되지 못하자 연구자들과 정부 자금은 투자를 줄였고 첫 번째 AI 겨울이 시작되었다. (이는 냉전이 절정에 다다른 후에 나타난 핵 겨울(nuclear winter)에 대한 비유이다)
  • 1980년대 심볼릭 AI의 새로운 버전인 전문가 시스템(expert system)이 큰 기업들 사이에 인기를 끌기 시작해서 전 세계 회사들이 전문가 시스템을 구축하기 위해 내부에 AI 부서를 꾸리기도 했다.
    • 1990년대 초기 이 시스템은 유지 비용이 비싸고 확장하기 어려우며 제한된 범위를 가진다는 것이 증명되고 관심은 사그라들었다. 이로 인해 두 번째 AI겨울이 시작되었다.

AI에 대한 전망

  • AI에 대한 단기간의 기대는 비현실적일지 모르지만 장기간의 전망은 매우 밝다.
    • (이하 설명 생략)

딥러닝 이전: 머신 러닝의 간략한 역사

확률적 모델링

  • 확률적 모델링(probabilistic modeling)은 통계학 이론을 데이터 분석에 응요한 것으로 초기 머신 러닝 형태 중 하나고 요즘도 널리 사용된다. 가장 잘 알려진 알고리즘 중 하나는 나이브 베이즈(Naive Bayes) 알고리즘이다.
  • 나이브 베이즈는 입력 데이터의 특성이 모두 독립적이라고 가정하고 베이즈 정리를 적용하는 머신 러닝 분류 알고리즘이다.
    • 이런 형태의 데이터 분석은 컴퓨터 보다 먼저 있었기 때문에 컴퓨터가 등장하기 전에는 수작업으로 적용했다. (거의 1950년대로 거슬러 올라간다)
    • 베이즈 정리와 통계의 토대는 18세기까지 올라간다.
  • 이와 밀접하게 연관된 모델이 로지스틱 회귀(logistic regression)인데, 이 모델은 현대 머신 러닝의 “hello world”로 여겨진다.
    • 이름 때문에 헷갈리지만 로지스틱 회귀는 회귀(regression) 알고리즘이 아니라 분류(classification) 알고리즘이다.
    • 나이브 베이즈와 비슷하게 로지스틱 회귀도 컴퓨터 보다 훨씬 오래 전에 있었다.
    • 하지만 간단하고 다목적으로 활용할 수 있어서 오늘날에도 유용하다.
    • 데이터 과학자가 분류 작업에 대한 감을 빠르게 얻기 위해 데이터셋에 적용할 첫 번째 알고리즘으로 선택하는 경우가 많다.

초창기 신경망

  • 초창기 버전의 신경망은 이 책에서 다루는 최신 구조로 완전히 대체되었다.
    • 신경망의 핵심 아이디어는 1950년대 연구 되었으나 본격적으로 시작되기까지는 수십 년이 걸렸다.
    • 대규모 신경망을 훈련시킬 수 있는 효과적인 방법을 오랜 기간 동안 찾지 못했기 때문이다.
  • 1980년대 중반에 여러 사람들이 제각기 역전파 알고리즘을 재발견하고 신경망에 적용하기 시작하면서 상황이 바뀌었다.
    • 이 알고리즘은 경사 하강법 최적화를 사용하여 연쇄적으로 변수가 연결된 연산을 훈련하는 방법이다.
  • 성공적인 첫 번째 신경망 애플리케이션은 1989년 벨 연구소에서 나왔다.
    • 얀 르쿤(Yann LeCun)은 초창기 합성곱 신경망(convolution nerual network)과 역전파를 연결하여 손글씨 숫자 이미지를 분류하는 문제에 적용했다.
    • LaNet 이라 부르는 이 신경망은 우편 봉투의 우편 번호 코드를 자동으로 읽기 위해 1990년대 미국 우편 서비스에 사용 되었다.

커널 방법

  • 초기 성공에 힘입어 1990년대 신경망은 연구자들 사이에 관심을 얻기 시작했지만 머신 러닝의 새로운 접근 방법인 커널 방법이 인기를 얻자 신경망은 빠르게 잊혀졌다.
  • 커널 방법(Kernel method)는 분류 알고리즘의 한 종류이며, 그중 서포트 벡터 머신(Support Vector Machine, SVM)이 가장 유명하다.
    • 현대적인 SVM의 공식은 1990년대 초 벨 연구소의 블라드미르 바프닉(Vladmir Vapnik)과 코리나 코르테스(Corinna Cortes)에 의해 개발되었고 1995년에 공개되었다.
    • 바프닉과 알렉세이 체르보넨키스(Alexey Chervonenkis)가 만든 오래된 선형 공식은 1963년에 공개되었다.
  • SVM은 분류 문제를 해결하기 위해 2개의 다른 범주에 속한 데이터 포인트 그룹 사이에 좋은 결정 경계(decision boundary)를 찾는다.
    • 결정 경계는 훈련 데이터를 2개의 범주에 대응하는 영역으로 나누는 직선이나 표면으로 생각할 수 있다.
    • 새로운 데이터 포인트를 분류하려면 결정 경계 어느 쪽에 속하는지를 확인하기만 하면 된다.

  • SVM이 결정 경계를 찾는 과정은 두 단계이다.
    1. 결정 경계가 하나의 초평면(hyperplane)으로 표현될 수 있는 새로운 고차원 표현으로 데이터를 매핑한다. (위 그림과 같은 2차원 데이터라면 초평면은 직선이 된다)
    2. 초평면과 각 클래스의 가장 가까운 데이터 포인트 사이의 거리가 최대가 되는 최선의 결정 경계(하나의 분할 초평면)를 찾는다. 이 단계를 마진 최대화(maximizing the margin)라고 부른다. 이렇게 함으로써 결정 경계가 훈련 데이터셋 이외의 새로운 샘플에 잘 일반화 되도록 도와준다.
  • 분류 문제를 간단하게 만들어 주기 위해 데이터를 고차원 표현으로 매핑하는 기법이 이론상으로는 좋아보이지만 실제로는 컴퓨터로 구현하기 어려운 경우가 많다. 그래서 커널 기법(kernel trick)이 등장했다.
    • 요지는 다음과 같다. 새롭게 표현된 공간에서 좋은 결정 초평면을 찾기 위해 새로운 공간에 대응하는 데이터 포인트의 좌표를 실제로 구할 필요가 없다.
    • 새로운 공간에서의 두 데이터 포인트 사이의 거리를 계산할 수만 있으면 된다. 커널 함수(kernel function)를 사용하면 이를 효율적으로 계산할 수 있다.
    • 커널 함수는 원본 공간에 있는 두 데이터 포인트를 명시적으로 새로운 표현으로 변환하지 않고 타깃 표현 공간에 위치했을 떄의 거리를 매핑해 주는 계산 가능한 연산이다.
    • 커널 함수는 일반적으로 데이터로부터 학습되지 않고 직접 만들어야 한다. SVM에서 학습되는 것은 분할 초평면뿐이다.
  • SVM이 개발되었을 때 간단한 분류 문제에 대해 최고 수준의 성능을 달성했고 광범위한 이론으로 무장된 몇 안되는 머신 러닝 방법 중 하나가 되었다.
    • 또 수학적으로 깊게 분석하기 용이하여 이론을 이해하고 설명하기 쉽다.
    • 이런 유용한 특징 때문에 SVM이 오랫동안 머신 러닝 분야에서 매우 큰 인기를 끌었다.
  • 하지만 SVM은 대용량의 데이터셋에 확자오디기 어렵고 이미지 분류 같은 지각에 관련된 문제에서 좋은 성능을 내지 못했다.
    • SVM은 얕은 학습 방법이기 때문에 지각에 관련된 문제에 SVM을 적용하려면 먼저 수동으로 유용한 표현을 추출해야 하는데 (이런 단계를 특성 공학(feature engineering)이라고 한다) 이는 매우 어렵고 불안정하다.

결정 트리, 랜덤 포레스트, 그래디언트 부스팅 머신

  • 결정 트리 (decision tree)는 플로차트(flowchart) 같은 구조를 가지며 입력 데이터 포인트를 분류하거나 주어진 입력에 대해 출력 값을 예측한다.
    • 결정 트리는 시가고하하고 이해하기 쉽다.
    • 데이터에서 학습되는 결정 트리는 2000년대부터 연구자들에게 크기 관심을 받기 시작했고 2010년까지는 커널 방법보다 선호하곤 했다.

  • 특히 랜덤 포레스트(Random Forest) 알고리즘은 결정 트리 학습에 기초한 것으로 안정적이고 실전에서 유용하다.
    • 서로 다른 결정 트리를 많이 만들고 그 출력을 앙상블 하는 방법을 사용한다.
    • 랜덤 포레스트는 다양한 문제에 적용할 수 있다. 얕은 학습에 해당하는 어떤 작업에서도 거의 항상 두 번째로 가장 좋은 알고리즘이다.
    • 머신 러닝 경연 웹 사이트인 캐글(Kaggle)이 2010년에 시작되었을 때부터 랜덤 포레스트가 가장 선호하는 알고리즘이 되었다.
  • 2014년에 그래디언트 부스팅 머신(gradient boosting machine)이 그 뒤를 이어 받았다.
    • 랜덤 포레스트와 아주 비슷하게 그래디언트 부스팅 머신은 약한 예측 모델인 결정 트리를 앙상블 하는 것을 기반으로 하는 머신 러닝 기법이다.
    • 이 알고리즘은 이전 모델에서 놓친 데이터 포인트를 보완하는 새로운 모델을 반복적으로 훈련함으로써 머신 러닝 모델을 향상하는 방법인 그래디언트 부스팅(gradient boosting)을 사용한다.
  • 결정 트리에 그래디언트 부스팅 기법을 적용하면 비슷한 성질을 가지면서도 대부분의 경우에 랜덤 포레스트의 성능을 능가하는 모델을 만든다.
    • 이 알고리즘이 오늘날 지각에 관련되지 않은 데이터를 다루기 위한 알고리즘 중 최고는 아니지만 가장 뛰어나다.
    • 딥러닝을 제외하고 캐글 경연 대회에서 가장 많이 사용되는 기법이다.

다시 신경망으로

  • 2010년경 신경망은 관심을 받지 못했지만, 여전히 신경망에 대해 연구하고 있던 일부 사람들이 중요한 성과를 내기 시작했다.
    • 토론토 대학의 제프리 힌튼(Geoffrey Hinton), 몬트리올 대학의 요슈아 벤지오(Yoshua Bengio), 뉴욕 대학의 얀 르쿤, 스위스의 IDSIA이다.
  • 2011년에 IDSIA의 댄 크리슨(Dan Ciresan)이 GPU로 훈련된 심층 신경망(depp neural network)으로 학술 이미지 분류 대회에서 우승한 것이 시작이었다. 이것이 현대적인 딥러닝의 첫 번째 성공이다.
    • 이어서 2012년 대규모 이미지 분류 대회인 ImageNet에서 힌튼 팀이 등장하면서 분수령이 되었다.
    • ImageNet 대회는 당시 아주 어려운 문제로 전통적인 컴퓨터 비전 방식을 사용한 우승 모델의 상위 5개 예측이 타깃 클래스를 맞출 정확도는 74.3%였는데, 제프리 힌튼이 조언자로 참여하고 알렉스 크리체브스키(Alex Krizhevsky)가 이끄는 팀이 상위 5개 예측에 대한 정확도 83.6%의 놀라운 성과를 달성했다.
  • 이때부터 매년 이 대회는 심층 합성곱 신경망(deep convolutional nerual networ, ConvNet)이 우승을 차지했다.
    • 2015년 우승자는 96.4%의 정확도를 달성했고 ImageNet의 분류 문제는 완전히 해결된 것으로 간주되었다.
  • 2012년부터 심층 합성곱 신경망이 모든 컴퓨터 비전 작업의 주력 알고리즘이 되었다. 이 알고리즘은 조금 더 일반적이며, 지각에 관한 모든 문제에 적용할 수 있다.
    • 2015년과 2016년에 열린 주요 컴퓨터 비전 컨퍼런스에서 어떤 형태로든 컨브넷(ConvNet)을 포함하지 않은 발표를 찾는 것은 거의 불가능할 정도이다.
    • 동시에 딥러닝은 자연어 처리(natural language processing) 같은 다른 종류의 문제에도 적용되었다.
  • 다양한 애플리케이션에서 SVM과 결정 트리를 완전히 대체하고 있다.
    • 예컨대 지난 몇 년간 유럽 입자 물리 연구소(European Organization for Nuclear Research, CERN)는 대형 강입자 충돌기(Large Hadron Collider, LHC)에 있는 ATALS 감지기에서 얻은 입자 데이터를 분석하기 위해 결정 트리 기반의 알고리즘을 사용했으나 최근 연구는 케라스(Keras) 기반의 심층 신경망을 적용하기 시작했다.

딥러닝의 특징

  • 딥러닝이 이렇게 빠르게 확산된 주된 이유는 많은 문제에서 더 좋은 성능을 내고 있기 때문이다.
    • 하지만 그것뿐만이 아닌데, 딥러닝은 머신 러닝에서 가장 중요한 단계인 특성 공학을 완전히 자동화 하기 때문에 문제를 더 해결하기 쉽게 만들어준다.
  • 얕은 학습인 이전의 머신 러닝 기법은 입력 데이터를 고차원 비선형 투영(SVM)이나 결정 트리 같은 간단한 변환을 통해 하나 또는 2개의 연속된 표현 공간으로만 변환한다. 하지만 복잡한 문제에 필요한 잘 정제된 표현은 일반적으로 이런 방식으로 얻지 못한다.
    • 이런 머신 러닝 방법들로 처리하기 용이하게 사람이 초기 입력 데이터를 여러 방식으로 변환해야 한다. 즉 데이터의 좋은 표현을 수동으로 만들어야 하는데, 이를 특성 공학(feature engineering)이라고 한다.
  • 이에 반해 딥러닝은 이 단계를 완전히 자동화 한다. 딥러닝을 사용하면 특성을 직접 찾는 대신 한 번에 모든 특성을 학습할 수 있다.
    • 머신 러닝 작업 흐름을 매우 단순화시켜 주므로 고도의 다단계 작업 과정을 하나의 간단한 end-to-end 딥러닝 모델로 대체할 수 있다.
  • 이슈의 핵심이 여러 개의 연속된 표현 층을 가지는 것이라면, 얕은 학습 방법도 딥러닝의 효과를 모사하기 위해 반복적으로 적용할 수 있지 않을까 하는 의문이 들 수 있다.
    • 실제로 얕은 학습 방법을 연속적으로 적용하면 각 츠으이 효과는 빠르게 줄어든다. 3개의 층을 가진 모델에서 최정의 첫 번째 표현 층은 하나의 층이나 2개의 층을 가진 모델에서 최정의 첫 번째 층과는 달라야 한다.
  • 딥러닝의 변환 능력은 모델이 모든 표현 층을 순차적이 아니라(탐욕적(greedily) 방법이 아니라) 동시에 공동으로 학습하게 만든다.
    • 이런 공동 특성 학습 능력 덕택에 모델이 내부 특성 하나에 맞춰질 때마다 이에 의존하는 다른 모든 특성이 사람이 개입하지 않아도 자동으로 변화에 적응하게 된다.
    • 모든 학습은 하나의 피드백 신호에 의해 시작된다. 즉 모델의 모든 변화는 최종 목표를 따라가게 된다.
    • 이 방식은 모델을 많은 중간 영역(층)으로 나누어 복잡하고 추상화된 표현을 학습시킬 수 있기 때문에 얕은 학습 모델을 탐욕적으로 쌓은 것보다 훨씬 강력하다. 여기에 각 층은 이전 층의 의존하지 않는 단순한 변환을 수행한다.
  • 딥러닝이 데이터로부터 학습하는 방법에는 두 가지 중요한 특징이 있다.
    • 층을 거치면서 점진적으로 더 복잡한 표현이 만들어진다는 것과 이런 점진적인 중간 표현이 공동으로 학습된다는 사실이다.
    • 각 층은 상위 층과 하위 층의 표현이 변함에 따라 함께 바뀐다. 이 2개의 특징이 이전의 머신 러닝 접근 방법보다 딥러닝이 훨씬 성공하게 된 이유이다.

머신 러닝의 최근 동향

  • (앞선 내용 생략)
  • 오늘날 머신 러닝을 성공적으로 적용하기 위해 알아야 할 두 가지 기술은 얕은 학습 문제를 위한 그래디언트 부스팅 머신과 지각에 관한 문제를 위한 딥러닝이다.

왜 딥러닝일까? 왜 지금일까?

  • 컴퓨터 비전에 대한 딥러닝의 두 가지 핵심 아이디어인 합성곱 신경망과 역전파는 이미 1989년에 소개되었다.
    • 시계열을 위한 딥러닝의 기본인 LSTM(Long Short-Term Memory) 알고리즘은 1997년에 개발되었고 그 이후로 변화가 거의 없다.
  • 왜 2012년 이후 딥러닝이 부상하게 되었을까? 일반적으로 세 가지 기술적인 힘이 머신 러닝의 진보를 이끌었다.
    • 하드웨어
    • 데이터셋과 벤치마크
    • 알고리즘 향상
  • 이 분야는 이론보다 실험을 통해서 성장해 왔기 때문에 새로운 아이디어를 실험할 적절한 데이터와 하드웨어가 준비되어 있어야만 알고리즘이 발전할 수 있다.
    • 머신 러닝은 수학이나 물리학이 아니라 하나의 공학이다.

하드웨어

  • (GPU가 등장했다는 설명 생략)

데이터

  • (데이터가 많아졌다는 설명 생략)

알고리즘

  • 2000년대 후반까지는 매우 깊은 심층 신경망을 훈련시킬 수 있는 안정적인 방법을 찾지 못했는데, 이런 이유로 하나 또는 2개의 층만 사용하는 매우 얕은 신경망만 가능했고 SVM과 랜덤 포레스트처럼 잘 훈련된 얕은 학습 방법에 비해 크게 빛을 보지 못했다.
  • 깊게 쌓은 층을 통과해서 그래디언트(gradient)를 전파하는 것이 가장 문제였는데, 신경망을 훈련하기 위한 피드백 신호가 층이 늘어남에 따라 희미해지기 때문이다.
  • 2009-2010년경 몇 가지 간단하지만 중요한 알고리즘이 개선되면서 그래디언트를 더 잘 전파되게 만들어 주었고 상황이 바뀌었다.
    • 신경망의 층에 더 잘 맞는 활성화 함수(activation function)
    • 층별 사전 훈련(pretraining)을 불필요하게 만든 가중치 초기화(weight initialization) 방법
    • RMSProp과 Adam 같은 더 좋은 최적화 방법
  • 이런 기술의 향상으로 10개 이상의 층을 가진 모델을 훈련시킬 수 있게 되었을 때 비로소 딥러닝이 빛을 발하기 시작했다.
  • 2014-2016년 사이에 그래디언트를 더욱 잘 전파할 수 있는 배치 정규화(batch normalization), 잔차 연결(residual connection), 깊이별 분리 합성곱(depthwise separable convolution) 같은 고급 기술들이 개발된 덕에 요즘에는 층의 깊이가 수천 개인 모델을 처음부터 훈련시킬 수 있다.

새로운 투자의 바람

  • (생략)

딥러닝의 대중화

  • (생략)

지속될까?

  • (생략)

물성의 원리

물성의 원리

개인적으로 신뢰하는 최낙언씨의 물성에 관한 책. 단순히 식품 자체에 대한 것보다 식품을 구성하는 원리를 탐구한다는 컨셉으로 식품을 구성하는 핵심적인 분자인 물, 단백질, 탄수화물, 지방에 대한 특성을 담고 있다.

생각보다 우리가 분자 차원에서 알고 있는 지식이 대단히 단편적인게 많은데, 실제로 분자 차원에서는 운동성도 굉장히 높고 –그 유명한 브라운 운동도 물 분자의 운동성 때문이다–, 그 크기의 차이나 모양(구조)의 차이가 대단히 중요한데, 우리가 그런 지식을 배울 때는 그런 내용을 잘 배우지 않기 때문. 원리를 알면 영양에 대한 이상한 소리는 꽤 많이 걸러낼 수 있는데 그걸 몰라서 사람들은 잘못된 영양 정보에 현혹되는게 아닐까 싶다.

원리를 담고 있기 때문에 어쩌면 나와 같은 대중들이 이해하기는 조금 어려운 책일 수 있는데, –실제로 나도 따라가지 못한 내용이 많음– 관심 있다면 꼭 한 번 읽어볼 만한 책이라 생각함. 

개인적으로 기회가 되면 다시 읽으면서 공부를 해두고 싶을 정도로 유용한 내용이 대단히 많았는데, 내 전문 분야가 아닌 분야에 공부 시간을 쓰기가 점점 더 어려워져서 한참 나중의 일이 되지 않을까 싶다.

추가로 식품이나 영양 관련해서 정보가 필요하면 찾아보는 http://www.seehint.com 를 참조하는데, 관심 있다면 둘러 보는 것을 추천. 사이트의 내용이 책의 내용과 동일한 것을 보면, 아무래도 최낙언 씨는 본인이 궁금한 내용을 공부해서 자료로 정리하고, 그 자료들을 하나의 주제로 엮어서 책을 내시는게 아닐까 싶은데, 덕분에 저술도 엄청 많이 하셨음. 나도 나중에 이런 식으로 공부한 자료를 모으고 책을 내면 좋겠다는 생각을 했음.