케라스 창시자에게 배우는 딥러닝/ 딥러닝을 위한 고급 도구

Sequential 모델을 넘어서: 케라스의 함수형 API

  • 지금까지 소개한 모든 신경망은 Sequential 모델을 사용하여 만들었다. Sequential 모델은 네트워크 입력과 출력이 하나라고 가정한다. 이 모델은 층을 차례로 쌓아 구성한다.

  • 많은 경우에 이런 가정이 적절하다. 하지만 이런 가정이 맞지 않는 경우도 많은데 일부 네트워크는 개별 입력이 여러 개 필요하거나 출력이 여러 개 필요하다.
    • 층을 차례로 쌓지 않고 층 사이를 연결하여 그래프처럼 만드는 네트워크도 있다.
  • 예컨대 어떤 작업은 다양한 종류의 입력이 필요하다. 다양한 입력 소스(source)에서 전달된 데이터를 다른 종류의 신경망 층을 사용하여 처리하고 합친다.
  • 중고 의류의 시장 가격을 예측하는 모델을 상상해 보자. 이 모델은 사용자가 제공한 메타데이터(브랜드, 연도 등), 사용자가 제공한 텍스트 설명, 제품 사진을 입력으로 사용한다.
    • 메타데이터는 원-핫 인코딩으로 바꾸고 완전 연결 네트워크를 사용하여 가격을 예측할 수 있다. 텍스트 설명은 RNN이나 1D 컨브넷을 사용할 수 있고, 사진 이미지는 2D 컨브넷을 사용할 수 있다.
    • 만일 3가지 모델을 동시에 사용하려면 각각의 모델을 따로 훈련하고 각 예측을 가중 평균(weighted average) 하면 된다.
    • 각 모델에서 추출한 정보가 중복된다면 이 방식은 최적이 아닐 것이다. 가능한 모든 종류의 입력 데이터를 동시에 사용해서 정확한 하나의 모델을 학습하는 것이 더 나은 방법이다. 이 모델은 3가지 입려 가지(branch)가 필요하다.

  • 이와 비슷하게 어떤 작업은 입력 데이터에서 여러 개의 타깃 속성을 예측해야 한다. 예컨대 소설이나 짧은 글이 있을 때 자동으로 장르별로 분류하고, 또 글을 쓴 대략의 시대를 예측하려고 한다.
    • 2개의 모델을 따로 훈련할 수도 있지만 이 속성들은 통계적으로 독립적이지 않기 때문에 동시에 장르와 시대를 함께 예측하도록 학습해야 더 좋은 모델을 만들 수 있다.
    • 이 모델은 2개의 출력 또는 머리(head)를 가진다. 장르와 시대 사이의 상관관계 때문에 소설 시대를 알면 장르의 공간에서 정확하고 풍부한 표현을 학습하는데 도움이 된다. 그 반대도 마찬가지다.

  • 더불어 최근에 개발된 많은 신경망 구조는 선형적이지 않은 네트워크 토폴로지(topology)가 필요하다. 비순환 유향 그래프 같은 네트워크 구조이다.
    • 예컨대 (구글의 세게디 등이 개발한) 인셉션 모듈을 사용하는 인셉션 계열의 네트워크들이다. 이 모듈에서 입력은 나란히 놓인 여러 개의 합성곱 층을 거쳐 하나의 텐서로 출력이 합쳐진다.

  • 최근에는 모델에 잔차 연결을 추가하는 경향도 있다. (마이크로소프트의 허(He) 등이 개발한) ResNet 계열의 네트워크들이 이런 방식을 사용하기 시작했다.
    • 잔차 연결은 하위 층에 출력 텐서를 상위 층의 출력 텐서에 더해서 아래 층의 표현이 네트워크 위쪽으로 흘러갈 수 있도록 한다.
    • 하위 층에서 학습된 정보가 데이터 처리하는 과정에서 손실되는 것을 방지한다. 이렇게 그래프 구조를 띈 네트워크 종류가 많다.

  • 여러 경우에 다중 입력 모델, 다중 출력 모델, 그래프 구조를 띈 모델이 필요하지만 케라스의 Sequential 클래스를 사용해서는 만들지 못한다.
    • 케라스에는 훨씬 더 일반적이고 유연한 다른 방법인 함수형 API가 있다.

함수형 API 소개

  • 함수형 API(functional API)에서는 직접 텐서들의 입출력을 다룬다. 함수처럼 층을 사용하여 텐서를 입력받고 출력한다. (그래서 함수형 API라고 부른다)
from keras import Input, layers

input_tensor = Input(shape=(32,))
dense = layers.Dense(32, activation='relu')
output_tensor = dense(input_tensor)
  • 간단한 예를 통해 Sequential 모델과 함수형 API로 만든 동일한 모델을 나란히 비교해 보자.
from keras.models import Sequential, Model

# Sequential 모델
seq_model = Sequential()
seq_model.add(layers.Dense(32, activation='relu', input_shape=(64,)))
seq_model.add(layers.Dense(32, activation='relu'))
seq_model.add(layers.Dense(10, activation='softmax'))

# 함수형 API로 만든 모델
input_tensor = Input(shape=(64,))
x = layers.Dense(32, activation='relu')(input_tensor)
x = layers.Dense(32, activation='relu')(x)
output_tensor = layers.Dense(10, activation='softmax')(x)

model = Model(input_tensor, output_tensor)
model.summary()
---
Layer (type) Output Shape Param # ================================================================= input_2 (InputLayer) (None, 64) 0 _________________________________________________________________ dense_5 (Dense) (None, 32) 2080 _________________________________________________________________ dense_6 (Dense) (None, 32) 1056 _________________________________________________________________ dense_7 (Dense) (None, 10) 330 ================================================================= Total params: 3,466 Trainable params: 3,466 Non-trainable params: 0
  • 입력 텐서와 출력 텐서만 가지고 Model 객체를 만드는 부분이 마술처럼 보인다. 
    • 무대 뒤에서 케라스는 input_tensor에서 output_tensor로 가는데 필요한 모든 층을 추출한다. 그다음 이들을 모아 그래프 데이터 구조인 Model을 만든다.
    • 물론 input_tensor를 반복 변환해서 output_tensor를 만들 수 있어야 한다. 관련되지 않은 입력과 출력으로 모델을 만들면 RuntimeError가 발생한다.
  •  Model 객체를 사용한 컴파일, 훈련, 평가 API는 Sequential 클래스와 같다.
import numpy as np

model.compile(optimizer='rmsprop', loss='categorical_crossentropy')

x_train = np.random.random((1000, 64))
y_train = np.random.random((1000, 10))

model.fit(x_train, y_train, epochs=10, batch_size=128)

score = model.evaluate(x_train, y_train)

다중 입력 모델

  • 함수형 API는 다중 입력 모델을 만드는데 사용할 수 있다. 일반적으로 이런 모델은 서로 다른 입력 가지를 합치기 위해 여러 텐서를 연결할 수 있는 층을 사용한다. 텐서를 더하거나 이어 붙이는 식이다.
    • 이와 관련된 케라스 함수는 keras.layers.add, keras.layers.concatenate 등이다.
  • 아주 간단한 다중 입력 모델을 살펴보자. 질문-응답(QA) 모델이다. 전형적인 질문-응답 모델은 2개의 입력을 가진다. 
    • 하나느 자연어 질문이고 다른 하나는 답변에 필요한 정보가 담겨 있는 텍스트(예컨대 뉴스 기사)이다. 그러면 모델은 답을 출력해야 한다. 
    • 가장 간단한 구조는 미리 정의한 어휘 사전에서 소프트맥스 함수를 통해 한 단어로 된 답을 출력하는 것이다.

  • 다음은 함수형 API를 사용하여 이런 모델을 만드는 예이다.
    • 텍스트와 질문을 벡터로 인코딩하여 독립된 입력 2개를 정의한다. 그 다음 이 벡터를 연결하고 그 위에 소프트맥스 분류기를 추가한다.
from keras.models import Model
from keras import layers, Input

text_vocabulary_size = 10000
question_vocabulary_size = 10000
answer_vocabulary_size = 500

text_input = Input(shape=(None,), dtype='int32', name='text')
embedded_text = layers.Embedding(text_vocabulary_size, 64)(text_input)
encoded_text = layers.LSTM(32)(embedded_text)

question_input = Input(shape=(None,), dtype='int32', name='question')
embedded_question = layers.Embedding(question_vocabulary_size, 32)(question_input)
encoded_question = layers.LSTM(16)(embedded_question)

concatenated = layers.concatenate([encoded_text, encoded_question], axis=-1)

answer = layers.Dense(answer_vocabulary_size, activation='softmax')(concatenated)

model = Model([text_input, question_input], answer)
model.compile(optimizer='rmsprop', loss='categorical_crossentropy', metrics=['acc'])
  • 그렇다면 입력이 2개인 모델은 어떻게 훈련할까? 두 가지 방식이 있다. 넘파이 배열의 리스트를 주입하거나 입력 이름과 넘파일 배열로 이루어진 딕셔너리를 모델의 입력으로 주입할 수 있다.
    • 물론 두 번째 방식은 입력 이름을 설정했을 때 사용할 수 있다.

import numpy as np
from keras.utils import to_categorical

num_samples = 1000
max_length = 100

text = np.random.randint(1, text_vocabulary_size, size=(num_samples, max_length))
question = np.random.randint(1, question_vocabulary_size, size=(num_samples, max_length))
answers = np.random.randint(0, answer_vocabulary_size, size=num_samples)
answers = to_categorical(answers)

# 리스트를 이용하여 학습
model.fit([text, question], answers, epochs=10, batch_size=128)

# 딕셔너리를 이용하여 학습 - 입력 이름을 지정했을 때만 사용할 수 있다.
model.fit({'text': text, 'question':question}, answers, epochs=10, batch_size=128)

다중 출력 모델

  • 같은 식으로 함수형 API를 사용하여 다중 출력(또는 다중 머리) 모델을 만들 수 있다. 간단한 예는 데이터에 있는 여러 속성을 동시에 예측하는 네트워크이다. 예컨대 소셜 미디어에서 익명의 사용자의 포스트를 입력으로 받아 그 사람의 나이, 성별, 소득 수준을 예측한다.
vocabulary_size = 50000
num_income_groups = 10

posts_input = Input(shape=(None,), dtype='int32', name='posts')
embedded_posts = layers.Embedding(vocabulary_size, 256)(posts_input)
x = layers.Conv1D(128, 5, activation='relu')(embedded_posts)
x = layers.MaxPooling1D(5)(x)
x = layers.Conv1D(256, 5, activation='relu')(x)
x = layers.Conv1D(256, 5, activation='relu')(x)
x = layers.MaxPooling1D(5)(x)
x = layers.Conv1D(256, 5, activation='relu')(x)
x = layers.Conv1D(256, 5, activation='relu')(x)
x = layers.MaxPooling1D()(x)
x = layers.Dense(128, activation='relu')(x)

age_prediction = layers.Dense(1, name='age')(x)
income_prediction = layers.Dense(num_income_groups, activation='softmax', name='income')(x)
gender_prediction = layers.Dense(1, activation='sigmoid', name='gender')(x)

model = Model(posts_input, [age_prediction, income_prediction, gender_prediction])

  • 이런 모델을 훈련하려면 네트워크 출력마다 다른 손실 함수를 지정해야 한다.
    • 예컨대 나이 예측은 스칼라 회귀 문제지만 성별 예측은 이진 클래스 문제라 훈련 방식이 다르다.
    • 경사 하강법은 하나의 스칼라 값을 최소화하기 때문에 모델을 훈련하려면 이 손실들을 하나의 값으로 합쳐야 한다.
    • 손실 값을 합치는 가장 간단한 방법은 모두 더하는 것이다. 케라스에서는 compile 메서드에 리스트나 딕셔너리를 사용하여 출력마다 다른 손실을 지정할 수 있다. 계산된 손실 값은 전체 손실 하나로 더해지고 훈련 과정을 통해 최소화 된다.
# 리스트를 이용한 방식
model.compile(optimizer='rmsprop', loss=['mse', 'categorical_crossentropy', 'binary_crossentropy'])

# 딕셔너리를 이용한 방식 - 출력 층에 이름을 지정했을 때만 사용할 수 있다.
model.compile(optimizer='rmsprop', loss={'age':'mse', 'income':'categorical_crossentropy', 'gender':'binary_crossentropy'})
  • 손실 값이 많이 불균형하면 모델이 개별 손실이 가장 큰 작업에 치우쳐 표현을 최적화할 것이다. 그 결과 다른 작업들은 손해를 입는다.
    • 이를 해결하기 위해 손실 값이 최종 손실에 기여하는 수준을 지정할 수 있다. 특히 손실 값의 스케일이 다를 때 유용하다.
    • 예컨대 다음과 같이 가정해 보자. 나이 회귀 작업에 사용되는 평균 제곱 오차(MSE) 손실은 일반적으로 3-5 사이의 값을 갖는 반면 성별 분류 작업에 사용되는 크로스엔트로피 손실은 0.1 정도로 낮다.
    • 이런 환경에서 손실에 균형을 맞추려면 크로스엔트로피 손실에 가중치 10을 주고 MSE 손실에 가중치 0.25를 줄 수 있다.
# 리스트를 이용한 방식
model.compile(optimizer='rmsprop', loss=['mse', 'categorical_crossentropy', 'binary_crossentropy'], loss_weights=[0.25, 1., 10.])

# 딕셔너리를 이용한 방식
model.compile(optimizer='rmsprop', loss={'age':'mse', 'income':'categorical_crossentropy', 'gender':'binary_crossentropy'}, loss_weights={'age':0.25, 'income':1., 'gender':10.})
  • 다중 입력 모델과 마찬가지로 넘파이 배열의 리스트나 딕셔너리를 모델에 전달하여 훈련한다.
# 리스트를 이용한 방식
model.fit(posts_input, [age_prediction, income_prediction, gender_prediction], epochs=10, batch_size=64)

# 딕셔너리를 이용한 방식
model.fit(posts_input, {'age':age_prediction, 'income':income_prediction, 'gender':gender_prediction}, epochs=10, batch_size=64)

층으로 구성된 비순향 유향 그래프

  • 함수형 API를 사용하면 다중 입력이나 다중 출력 모델 뿐만 아니라 내부 토폴로지가 복잡한 네트워크도 만들 수 있다. 케라스의 신경망은 층으로 구성된 어떤 비순환 유향 그래프(directed acyclic graph)도 만들 수 있다.
    • 비순환이라는 부분이 중요한데 이 그래프는 원형을 띌 수 없다. 텐서 x가 자기 자신을 출력하는 층의 입력이 될 수 없다. 만들 수 있는 루프는 순환 층의 내부에 있는 것 뿐이다.
  • 그래프로 구현된 몇 개의 신경망 컴포넌트가 널리 사용된다. 가장 유명한 2개는 인셉션 모듈과 잔차 연결이다. 케라스에서 이 2개의 컴포넌트를 어떻게 구현하는지 살펴보자.

인셉션 모듈

  • 인셉션(Inception)은 합성곱 신경망에서 인기 있는 네트워크 구조이다. 일찍이 네트워크 안의 네트워크(network-in-network) 구조에서 영감을 받아 2013-2014년에 크리스티안 세게디(Christian Szegedy)와 그의 구글 동료들이 만들었다.
    • 나란히 분리된 가지를 따라 모듈을 쌓아 독립된 작은 네트워크처럼 구성한다.
    • 가장 기본적인 인셉션 모듈 형태는 3-4개의 가지를 가진다. 1 x 1 합성곱으로 시작해서 3 x 3 합성곱이 뒤따르고 마지막에 전체 출력 특성이 합쳐진다.
    • 이런 구성은 네트워크가 따로따로 공간 특성과 채널 방향의 특성을 학습하도록 돕는다. 한번에 학습하는 것보다 효과가 더 높다.
    • 더 복잡한 인셉션 모듈은 풀링 연산, 여러 가지 합성곱 사이즈(예컨대 일부 가지에서는 3 x 3 대신 5 x 5 를 사용한다), 공간 합성곱이 없는 가지(1 x 1 합성곱만 존재)로 구성될 수 있다.
    • 인셉션 V3에 있는 이런 모듈의 예가 아래 그림과 같다.

  • Note) 1 x 1 합성곱의 목적
    • 합성곱은 입력 텐서에서 타일 주변의 패치를 추출하고 각 패치에 동일한 연산을 수행한다. 이 경우 추출된 패치가 하나의 타일로 이루어졌을 때이다.
    • 이 합성곱 연산은 모든 타일 벡터를 하나의 Dense 층에 통과시키는 것과 동일하다. 즉, 입력 텐서의 채널 정보를 혼합한 특성을 계산한다. 공간 방향으로는 정보를 섞지 않는다. (한 번에 하나의 타일만 처리하기 때문) 
    • 이런 1 x 1 합성곱(또는 점별 합성곱(pointwise convolution)은 인셉션 모듈의 특징이다. 채널 방향의 특성 학습과 공간 방향의 특성 학습을 분리하는데 도움을 준다. 
    • 채널이 공간 방향으로 상관관계가 크고 채널 간에는 독립적이라고 가정하면 납득할 만한 전략이다.
  • 다음은 함수형 API를 이용해 위 그림의 모듈을 구현하는 예이다. 이 예에서 입력 x는 4D 텐서라고 가정한다.
from keras import layers

branch_a = layers.Conv2D(128, 1, activation='relu', strides=2)(x)

branch_b = layers.Conv2D(128, 1, activation='relu')(x)
branch_b = layers.Conv2D(128, 3, activation='relu', strides=2)(branch_b)

branch_c = layers.AveragePooling2D(3, strides=2)(x)
branch_c = layers.Conv2D(128, 3, activation='relu')(branch_c)

branch_d = layers.Conv2D(128, 1, activation='relu')(x)
branch_d = layers.Conv2D(128, 3, activation='relu')(branch_d)
branch_d = layers.Conv2D(128, 3, activation='relu', strides=2)(branch_d)

output = layers.concatenate([branch_a, branch_b, branch_c, branch_d], axis=1)
  • 인셉션 V3 전체 구조는 케라스의 keras.applications.inception_v3.InceptionV3에 준비되어 있으며, ImageNet 데이터셋에서 사전 훈련된 가중치를 포함하고 있다.
    • 이와 아주 비슷한 모델인 엑셉션(Xception)도 케라스의 애플리케이션 모듈에 포함되어 있다. 엑셉션은 극단적인 인셉션(extreme inception)을 말한다.
    • 이 합성곱 구조는 인셉션에서 일부 영감을 얻었다. 채널 방향의 학습과 공간 방향의 학습을 극단적으로 분리한다는 아이디어에 착안하여 인셉션 모듈을 깊이별 분리 합성곱으로 바꾼다.
    • 이 합성곱은 깊이별 합성곱(depthwise convolution) (각 입력 채널에 따로따로 적용되는 공간 방향 합성곱) 다음에 점별 합성곱(1 x 1 합성곱)이 뒤따른다.
    • 인셉션 모듈의 극한 형태로 공간 특성과 채널 방향 특성을 완전히 분리한다.
    • 엑셉션은 인셉션 V3와 거의 동일한 개수의 모델 파라미터를 가지지만 실행 속도가 더 빠르고 ImageNet이나 다른 대규모 데이터셋에서 정확도가 더 높다. 이는 모델 파라미터를 더 효율적으로 사용하기 때문이다.

잔차 연결

  • 잔차 연결(residual connection)은 엑셉션을 포함하여 2015년 이후 등장한 많은 네트워크 구조에 있는 그래프 형태의 네트워크 컴포넌트이다. 2015년 후반 ILSVRC ImageNet 경연 대회 우승 팀인 마이크로소프트의 허 등이 소개했다.
    • 대규모 딥러닝 모델에서 흔히 나타나는 두 가지 문제인 그래디언트 소실과 표현 병목(representational bottleneck)을 해결했다. 일반적으로 10개 층 이상을 가진 모델에 잔차 연결을 추가하면 도움이 된다.
  • 잔차 연결은 하위 층의 출력을 상위 층의 입력으로 사용한다. 순서대로 놓인 네트워크를 질러가는 연결이 만들어진다.
    • 하위 층의 출력이 상위 층의 활성화 출력에 연결되는 것이 아니고 더 해진다. 따라서 두 출력의 크기가 동일해야 한다.
    • 크기가 다르면 선형 변환을 사용하여 하위 층의 활성화 출력을 목표 크기로 변환한다 (예컨대 활성화 함수를 사용하지 않는 Dense 층이나 합성곱의 특성 맵이라면 활성화 함수가 없는 1 x 1 합성곱)
  • 다음 코드는 케라스에서 특성 맵의 크기가 같을 때 원본을 그대로 사용하는 잔차 연결을 구현한 예이다. 여기서는 입력 x가 4D 텐서라고 가정한다.
from keras import layers

x = ...

y = layers.Conv2D(128, 3, activation='relu', padding='same')(x)
y = layers.Conv2D(128, 3, activation='relu', padding='same')(x)
y = layers.Conv2D(128, 3, activation='relu', padding='same')(x)

y = layers.add([y, x])
  • 다음은 특성 맵의 크기가 다를 때 선형 변환을 사용하여 잔차 연결을 구현한 예이다. (여기서도 x가 4D 텐서라고 가정한다)
from keras import layers

x = ...

y = layers.Conv2D(128, 3, activation='relu', padding='same')(x)
y = layers.Conv2D(128, 3, activation='relu', padding='same')(x)
y = layers.MaxPooling2D(2, strides=2)(y)

residual = layers.Conv2D(128, 1, strides=2, padding='same')(x)

y = layers.add([y, residual])
  • Note) 딥러닝의 표현 병목
    • Sequential 모델에서 표현 학습을 위한 층은 다른 층위에 연달아 놓인다. 다시 말해 층은 이전 층의 활성화 출력 정보만 사용한다. 어떤 층이 너무 작으면 (예컨대 특성이 너무 저차원이면) 이 활성화 출력에 얼마나 많은 정보를 채울 수 있느냐에 모델 성능이 좌우된다.
    • 이 개념을 신호 처리에 비유할 수 있다. 일련의 연산으로 구성된 오디어 처리 파이프라인이 있다고 가정해 보자. 각 단계는 이전 연산의 출력으로 입력을 사용한다. 한 연산이 신호를 저주파 영역(예컨대 0-15kHz)으로 잘라냈다면 이후 연산이 줄어든 주파수를 복원할 수 없을 것이다. 손실된 정보는 영구 불변이다. 하위 층의 정보를 다시 주입하는 잔차 연결은 딥러닝 모델에서 이 이슈를 어느 정도 해결해 준다.
  • Note) 딥러닝의 그래디언트 소실 문제
    • 심층 신경망을 훈련하는데 사용되는 핵심 알고리즘인 역전파는 출력 손실에서 얻은 피드백 신호를 하위 층에 전파한다. 피드백 신호가 깊이 쌓인 층을 통과하여 전파되면 신호가 아주 작아지거나 완전히 사라질 수도 있다. 이렇게 되면 네트워크가 훈련되지 않는데, 이를 그래디언트 소실(vanishing gradient) 문제라고 한다.
    • 이 문제는 심층 신경망과 긴 시퀀스를 처리하는 순환 신경망에서 모두 나타난다. 양쪽 모두 피드백 신호가 일련의 긴 연산을 통과하여 전파되기 때문이다. 순환 신경망에서 LSTM 층이 이 문제를 해결하기 위해 사용하는 방식을 보았다. 이동 트랙이 주요 처리 트랙에 나란히 정보를 실어 날랐다. 잔차 연결은 피드포워드 심층 신경망에서 비슷한 역할을 하는데, 좀 더 단순하다. 주 네트워크 층에 나란히 단순한 선형 정보를 실어 나른다. 이는 그래디언트가 깊게 쌓은 층을 통과하여 전파하도록 도와준다.

층 가중치 공유

  • 함수형 API의 중요한 또 하나의 기능은 층 객체를 여러 번 재사용할 수 있다는 것이다. 층 객체를 두 번 호출하면 새로운 층 객체를 만들지 않고 각 호출에 동일한 가중치를 재사용한다.
    • 이런 기능 때문에 공유 가지를 가진 모델을 만들 수 있다. 이런 가지는 같은 가중치를 공유하고 같은 연산을 수행한다. 다시 말해 같은 표현을 공유하고 이런 표현을 다른 입력에서 함께 학습한다.
  • 예컨대 두 문장 사이의 의미가 비슷한지 측정하는 모델을 가정해 보자. 이 모델은 2개의 입력(비교할 2개의 문장)을 받고 0과 1사이의 점수를 출력한다.
    • 0은 관련 없는 문장, 1은 두 문장이 동일하거나 재구성된 것.
    • 이런 모델은 대화 시스템(dialog system)에서 자연어 질의에 대한 중복 제거를 포함하여 많은 애플리케이션에 유용하게 사용될 수 있다.
  • 이런 문제에서 두 입력 시퀀스가 바뀔 수 있다. 의미가 비슷하다는 것은 대칭적인 관계이기 때문이다.
    • A에서 B에 대한 유사도는 B에서 A에 대한 유사도와 같다. 이런 이유로 각 입력 문장을 처리하는 2개의 독립된 모델을 학습하는 것은 이치에 맞지 않다.
    • 대신 하나의 LSTM 층으로 양쪽을 모두 처리하는 것이 좋다. 이 LSTM 층의 표현(가중치)은 두 입력에 대해 함께 학습된다.
    • 이를 샴 LSTM(Siamese LSTM) 모델 또는 공유 LSTM 이라고 부른다.
  • 다음은 케라스의 함수형 API로 공유 층(재사용 층)을 사용하는 모델을 구현하는 예이다)
from keras import layers
from keras import Input
from keras.models import Model

lstm = layers.LSTM(32)
left_input = Input(shape=(None, 128))
left_output = lstm(left_input)

right_input = Input(shape=(None, 128))
right_output = lstm(right_input)

merged = layers.concatenate([left_output, right_output], axis=-1)
predictions = layers.Dense(1, activation='sigmoid')(merged)

model = Model([left_input, right_input], predictions)
model.fit([left_data, right_data], targets)
  • 당연하게 층 객체는 한 번 이상 사용할 수 있다. 같은 가중치를 재사용하면서 얼마든지 여러 번 호출 할 수 있다.

층과 모델

  • 함수형 API에서는 모델을 층처럼 생각할 수 있다. 모델을 ‘커다란 층’으로 생각해도 된다. Sequential 클래스와 Model 클래스에서 모두 동일하다. 
    • 이 말은 입력 텐서로 모델을 호출해서 출력 텐서를 얻을 수 있다는 뜻이다.
y = model(x)
  • 모델에서 입력 텐서와 출력 텐서가 여러 개이면 텐서의 리스트로 호출한다.
y1, y2 = model([x1, x2])
  • 모델 객체를 호출할 때 모델의 가중치가 재사용된다. 층 객체를 호출할 때와 정확히 같다. 층 객체나 모델 객체나 객체를 호출하는 것은 항상 그 객체가 가진 학습된 표현을 재사용한다.
  • 모델 객체를 재사용하는 간단한 실전 예는 듀얼 카메라에서 입력을 받는 비전 모델이다.
    • 두 카메라가 몇 센티미터(1인치) 간격을 두고 나란히 있다. 이런 모델을 깊이를 감지할 수 있다. 이는 많은 애플리케이션에서 유용한 기능이다.
    • 왼쪽 카메라와 오른쪽 카메라에서 시각적 특징을 추출하여 합치기 위해 2개의 독립된 모델을 사용할 필요가 없다. 두 입력에 저수준 처리 과정이 공유될 수 있다. 다시 말해 가중치가 같고 동일한 표현을 공유하는 층을 사용한다. 
    • 다음은 케라스에서 샴 비전 모델(공유 합성곱 기반 층)을 구현하는 예이다.
from keras import layers, applications, Input

xception_base = applications.Xception(weights=None, include_top=False)

left_input = Input(shape=(250, 250, 3))
right_input = Input(shape=(250, 250, 3))

left_features = xception_base(left_input)
right_features = xception_base(right_input)

merged_features = layers.concatenate([left_features, right_features], axis=-1)

정리

  • 차례대로 층을 쌓는 것 이상이 필요할 때는 Sequential API를 사용하지 않는다.
  • 함수형 API를 사용하여 다중 입력, 다중 출력, 복잡한 네트워크 토폴로지를 갖는 케라스 모델을 만드는 방법
  • 다른 네트워크 가지에서 같은 층이나 모델 객체를 여러 번 호출하여 가중치를 재사용하는 방법

케라스 콜백과 텐서보드를 사용한 딥러닝 모델 검사와 모니터링

  • 대규모 데이터셋에서 model.fit()이나 model.fit_generator()로 수십 번 에포크를 실행하는 것은 종이 비행기를 날리는 것과 비슷하다. 일단 손을 떠나면 종이 비행기 경로와 착률 지점을 제어할 방법이 없다.
    • 나쁜 결과를 피하려면 종이 비행기 대신 다른 것을 사용하는 것이 좋다. 
    • 드론은 주변 환경을 감지한 데이터를 조작부에 전달하여 현재 상태를 바탕으로 자동으로 운전한다. 
    • 앞으로 소개할 기법은 model.fit() 호출을 종이 비행기에서 스스로 판단하고 동적으로 결정하는 똑똑한 자동 드론으로 바꾸어 줄 것이다.

콜백을 사용하여 모델의 훈련 과정 제어하기

  • 모델을 훈련할 때 미리 예상할 수 없는 것들이 많다. 특히 최적의 검증 손실을 얻기 위해 얼마나 많은 에포크가 필요한지 알지 못한다. 
    • 지금까지는 적절한 훈련 에포크를 알아내기 위해 첫 번째 실행에서 과대적합이 시작될 때까지 충분한 에포크로 훈련하고 최적의 에포크 횟수를 찾아 처음부터 새로 훈련을 시작했다. 당연히 이런 방식은 낭비가 많다.
  • 더 좋은 처리 방법은 검증 손실이 더 향상되지 않을 때 훈련을 멈추는 것이다. 이는 케라스의 콜백을 사용하여 구현할 수 있다.
    • 콜백은 모델의 fit() 메서드가 호출될 때 전달되는 객체로, 훈련하는 동안 모델은 여러 지점에서 콜백을 호출한다.
    • 콜백은 모델의 상태와 성능에 대한 모든 정보에 접근하고 훈련 중지, 모델 저장, 가중치 적재 또는 모델 상태 변경 등을 처리할 수 있다.
  • 다음은 콜백을 사용하는 몇 가지 예이다.
    • 모델 체크포인트 저장: 훈련하는 동안 어떤 지점에서 모델의 현재 가중치를 저장한다.
    • 조기 종료(early stopping): 검증 손실이 더 향상되지 않을 때 훈련을 중지한다.
    • 훈련하는 동안 하이퍼파라미터 값을 동적으로 조정: 옵티마이저의 학습률 같은 경우이다.
    • 훈련과 검증 지표를 로그에 기록하거나 모델이 학습한 표현이 업데이트될 때마다 시각화: 앞서 보았던 케라스의 진행 표시줄(progress bar)이 하나의 콜백이다.
  • keras.callback 모듈은 많은 내장 콜백을 포함하고 있다. 다음은 대표적인 예이다.
keras.callbacks.ModelCheckpoint
keras.callbacks.EarlyStopping
keras.callbacks.LearningRateScheduler
keras.callbacks.ReduceLROnPlateau
keras.callbacks.CSVLogger

ModelCheckpoint와 EarlyStopping 콜백

  • EarlyStopping 콜백을 사용하면 정해진 에포크 동안 모니터링 지표가 향상되지 않을 때 훈련을 중지할 수 있다.
    • 예컨대 과대적합이 시작되자마자 훈련을 중지할 수 있다. 따라서 에포크 횟수를 줄여 다시 모델을 훈련할 필요가 없다.
    • 일반적으로 이 콜백은 훈련하는 동안 모델을 계속 저장해주는 ModelCheckpoint와 함께 사용한다. (선택적으로 지금까지 가장 좋은 모델만 저장할 수 있다. 에포크 마지막에 다다랐을 때 최고 성능을 달성한 모델이다.
import keras

callbacks_list = [
keras.callbacks.EarlyStopping(
monitor='val_acc',
patience=1,
),
keras.callbacks.ModelCheckpoint(
filepath='my_model.h5',
monitor='val_loss',
save_best_only=True,
)
]

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

model.fit(x, y, epochs=10, batch_size=32, callbacks=callbacks_list, validation_data=(x_val, y_val))

RecudeLROnPlateau 콜백

  • 이 콜백을 사용하면 검증 손실이 향상되지 않을 때 학습률을 작게 할 수 있다. 손실 곡선이 평탄할 때 학습률을 작게 하거나 크게 하면 훈련 도중 지역 최솟값에서 효과적으로 빠져나올 수 있다.
callback_list = [
keras.callbacks.ReduceLROnPlateau(
monitor='val_loss',
factor=0.1,
patience=10,
)
]

model.fit(x, y, epochs=10, batch_size=32, callbacks=callbacks_list, validation_data=(x_val, y_val))

자신만의 콜백 만들기

  • 내장 콜백에서 제공하지 않는 특수한 행도잉 훈련 도중 필요하다면 자신만의 콜백을 만들 수 있다. 콜백은 keras.callbacks.Callback 클래스를 상속받아 구현하면 된다.
    • 그 다음 훈련하는 동안 호출될 여러 지점을 나타내기 위해 약속된 다음 메서드를 구현한다.
on_epoch_begin -- 각 에포크가 시작될 때 호출한다.
on_epoch_end -- 각 에포크가 끝날 때 호출한다.

on_batch_begin -- 각 배치 처리가 시작될 때 호출한다.
on_batch_end -- 각 배치 처리가 끝날 때 호출한다.

on_train_begin -- 훈련이 시작될 때 호출한다.
on_train_end -- 훈련이 끝날 때 호출한다.
  • 이 메서드들은 모두 logs 매개변수화 함께 호출된다. 이 매개변수에는 이전 배치, 에포크에 대한 훈련과 검증 측정값이 담겨 있는 딕셔너리가 전달된다. 또 콜백은 다음 속성을 참조할 수 있다.
    • self.model: 콜백을 호출하는 모델 객체
    • self.validation_data: fit() 메서드에 전달될 검증 데이터
  • 다음은 매 에포크의 끝에서 검증 세트의 첫 번째 샘플로 모델에 있는 모든 츠으이 활성화 출력을 계산하여 (넘파이 배열로) 디스크에 저장하는 자작 콜백의 예이다.
import keras
import numpy as np

class ActivationLogger(keras.callbacks.Callback):
def set_model(self, model):
self.model = model
layer_outputs = [layer.output for layer in model.layers]
self.activations_model = keras.models.Model(model.input, layer_outputs)

def on_epoch_end(self, epoch, logs=None):
if self.validation_data is None:
raise RuntimeError('Requires validation_data')

validation_sample = self.validation_data[0][0:1]
activations = self.activations_model.predict(validation_sample)
f = open('activation_at_epoch_' + str(epoch) + '.npz', 'wb')
np.savez(f, activations)
f.close()

텐서보드 소개: 텐서플로의 시각화 프레임워크

  • 텐서보드의 핵심 목적은 훈련 모델의 내부에서 일어나는 모든 것을 시각적으로 모니터링 할 수 있도록 돕는 것이다. 모델의 최종 손실 외에 더 많은 정보를 모니터링하면 모델 작동에 대한 명확한 그림을 그릴 수 있다. 결국 모델을 더 빠르게 개선할 수 있다.
  • 텐서 보드는 다음과 같은 기능을 제공하고 모든 브라우저에서 작동한다.
    • 훈련하는 동안 측정 지표를 시각적으로 모니터링 한다.
    • 모델 구조를 시각화 한다.
    • 활성화 출력과 그래디언트의 히스토그램을 그린다.
    • 3D로 임베딩을 표현한다.

  • 6장에서 했던 IMDB 감성 분석 문제를 이용해서 예시를 들어보자.
import keras
from keras import layers
from keras.datasets import imdb
from keras.preprocessing import sequence

max_features = 2000
max_len = 500
batch_size = 32

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

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

model = keras.models.Sequential()
model.add(layers.Embedding(max_features, 128, input_length=max_len, name='embed'))
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', loss='binary_crossentropy', metrics=['acc'])
  • 텐서보드를 시작하기 전에 로그 파일이 저장될 디렉터리를 만들어야 한다.
  • 로그 디렉터리를 만들었다면 TensorBoard 콜백 객체와 함꼐 훈련을 시작해 보자. 이 콜백은 지정된 디스크 위치에 로그 이벤트를 기록할 것이다.
callbacks = [
keras.callbacks.TensorBoard(
log_dir='my_log_dir',
histogram_freq = 1,
embeddings_freq = 1,
)
]

history = model.fit(x_train, y_train, epochs=20, batch_size=128, validation_split=0.2, callbacks=callbacks)
  • 이제 명령행에서 콜백이 사용하는 로그 디렉터리를 지정하여 텐서보드 서버를 실행한다.
    • 텐서플로를 설치했다면 텐서보드 유틸리티는 자동으로 설치되었을 것이다.
tensorboard --logdir=my_log_dir
  • 그 다음 브라우저에서 http://localhost:6006 주소에 접속하면 모델의 훈련 결과를 확인할 수 있다.
    • 훈련 지표와 검증 지표를 실시간 그래프로 볼 수 있고 Histograms 탭에서 층의 활성화 출력을 히스토그램 그래프로 볼 수 있다.

  • Embeddings 탭에서 입력 어휘 사전에 있는 2,000개의 임베딩 위치와 공간상 관계를 조사할 수 있다.
    • 이 임베딩은 첫 번째 Embedding 층이 학습한 것이다. 임베딩 공간이 128차원이기 때문에 텐서보드는 우리가 선택한 차원 축소 알고리즘을 사용하여 자동으로 2D 또는 3D로 축소한다.
    • 주성분 분석(Principle Component Analysis, PCA) 또는 t-SNE(t-distributed Stochastic Neighbor Embedding)이다.
    • 아래 그림에서 점 구름(point cloud) 그래프에서 2개의 클러스터가 뚜렷히 구분된다. 긍정적인 의미의 단어와 부정적인 의미의 단어이다.
    • 이 그래프는 주어진 문제에 완전히 특화된 모델의 목적에 맞게 임베딩이 훈련되었다는 것을 확실히 보여준다. 이것이 사전 훈련된 보편적인 단어 임베딩이 드물게 사용되는 이유이다.

  • Graphs 탭은 케라스 모델을 구성하는 저수준 텐서플로 연산의 그래프를 시각화한다. 여기서 볼 수 있듯 예상보다 훨씬 복잡하다. 
    • 케라스에서 만든 모델은 간단해 보이지만 무대 위에서는 상당히 복잡한 그래프 구조가 만들어진다. 그래프의 많은 부분은 경사하강법과 관련이 있다.
    • 보이는 것과 만들어야 하는 것 사이의 이런 복잡도 차이 때문에 모든 것을 밑바닥부터 텐서플로를 사용하여 만드는 대신 케라스를 사용하여 모델을 만든다.
    • 케라스는 작업 흐름을 극적으로 간단하게 만들어준다.

  • 텐서플로 연산의 그래프 외에 카리스의 keras.utils.plot_model 유틸리티는 모델의 층 그래프를 깔끔하게 그려 주는 기능을 제공한다. 이를 사용하려면 pydot과 pydot-ng, graphviz 라이브러리가 필요하다.
from keras.utils import plot_model

plot_model(model, to_file='model.png')
  • 위 코드는 아래 그림의 png 이미지를 만든다.

  • 층 그래프에 크기 정보를 추가할 수 있다.
    • 다음은 plot_model 함수와 show_shapes 매개변수를 사용하여 모델의 그래프를 그린다.
from keras.utils import plot_model

plot_model(model, show_shapes=True, to_file='model.png')

정리

  • 케라스 콜백은 훈련하는 동안 모델을 모니터링하고 모델 상태를 바탕으로 자동으로 작업을 수행하는 손쉬운 방법이다.
  • 텐서플로를 사용하면 텐서보드를 이용하여 모델 상황을 브라우저에서 시각화할 수 있다. 케라스 모델에서는 TensorBoard 콜백을 통해 사용한다.

모델의 성능을 최대로 끌어올리기

고급 구조 패턴

  • 이전 절에서 중요한 디자인 패턴 중 하나인 잔차 연결에 대해 소개했는데, 이 외에도 꼭 알아야 할 디자인 패턴이 2개 더 있다. 정규화와 깊이별 분리 합성곱이 그것이다. 이 패턴은 특히 고성능 심층 컨브넷을 만들 때 유용하다. 하지만 보통 다른 종류의 구조에서도 많이 등장한다.

배치 정규화

  • 정규화(normalization)는 머신 러닝 모델에 주입되는 샘플들을 균일하게 만드는 광범위한 방법이다. 이 방법은 모델이 학습하고 새로운 데이터에 잘 일반화 되도록 돕는다.
    • 데이터 정규화의 일반적인 형태는 이미 이 책에서 여러 번 나왔다. 데이터에서 평균을 빼서 데이터를 원점에 맞추고 표준 편차로 나누어 데이터의 분산을 1로 만든다.
    • 데이터가 정규 분포를 따른다고 가정하고 이 분포를 원점에 맞추고 분산이 1이 되도록 조정한 것이다.
normalized_data = (data - np.mean(data, axis=...)) / np.std(data, axis=...)
  • 이전 예제는 모델에 데이터를 주입하기 전에 정규화 했다. 하지만 데이터 정규화는 네트워크에서 일어나는 모든 변환 후에도 고려되어야 한다.
    • Dense나 Conv2D 층에 들어가는 데이터의 평균이 0이고 분산이 1이더라도 출력되는 데이터가 동일한 분포를 가질 것이라고 기대하기는 어렵다.
  • 배치 정규화(batch normalization)은 2015년 아이오페와 세게디가 제안한 층의 한 종류이다. (케라스는 BatchNormalization 클래스로 제공한다)
    • 훈련하는 동안 평균과 분산이 바뀌더라도 이에 적응하여 데이터를 정규화한다. 훈련 과정에 사용된 배치 데이터의 평균과 분산에 대한 지수 이동 평균(exponential moving average)을 내부에 유지한다.
    • 배치 정규화의 주요 효과는 잔차 연결과 매우 흡사하게 그래디언트의 전파를 도와주는 것이다. 결국 더 깊은 네트워크를 구성할 수 있다.
    • 매우 깊은 네트워크라면 여러 개의 BatchNormalization 층을 포함해야 훈련할 수 있다.
    • 예컨대 케라스에 포함된 고급 컨브넷 구조는 BatchNormalization 층을 많이 사용한다. 여기에는 ResNet50, 인셉션 V3, 엑셉션 등이 있다.
  • BatchNormalization 층은 일반적으로 합성곱이나 완전 연결 층 다음에 사용한다.
conv_model.add(layers.Conv2D(32, 3, activation='relu'))
conv_model.add(layers.BatchNormalization())

dense_model.add(layers.Dense(32, 3, activation='relu'))
dense_model.add(layers.BatchNormalization())
  • BatchNormalization 클래스에는 정규화할 특성 축을 지정하는 axis 매개변수가 있다. 이 매개변수의 기본값은 입력 텐서의 마지막 축을 나타내는 -1이다.
    • data_format을 “channels_last”로 하여 Dense, Conv1D, RNN, Conv2D 층을 사용할 때는 맞는 값이다.
    • 하지만 data_format을 “channels_first”로 사용하는 경우에는 특성 축이 1이다. 이때는 BatchNormalization의 axis 매개변수는 1이 되어야 한다.
  • Note) 배치 재정규화
    • 배치 정규화의 최근 발전은 2017년 아이오페가 소개한 배치 재정규화(batch renormalization)이다. 이 방법은 추가적인 비용을 들이지 않고 배치 정규화보다 이득이 많다. 이 책을 쓸 시점에는 배치 정규화를 대체할 가능성이 높다.
    • 좀 더 최근에는 클람바우어(Klambauer) 등이 자기 정규화 신경망(self-normalizing neural networks)을 발표했다. 특정 활성화 함수(selu)와 초기화 방법(lecun_normal)을 사용하여 Dense 층의 출력을 정규화 한다. 이 방법이 흥미롭기는 하지만 지금은 완전 연결 네트워크에만 제한되어 있어 널리 사용되지 못하고 있다.

깊이별 분리 합성곱

  • Conv2D를 대체하면서 더 가볍고(훈련할 모델 파라미터가 더 적고) 더 빨라(부동 소수 연산이 더 적고) 모델의 성능을 몇 퍼센트 포인트 높일 수 있는 층이 있다면 어떨까? 이것이 깊이별 분리 합성곱(depthwise separable convolution) 층이 하는 일이다. (SeparableConv2D)
    • 이 층은 입력 채널별로 따로따로 공간 방향의 합성곱을 수행한다. 그 다음 아래 그림과 같이 점별 합성곱 (1×1 합성곱)을 통해 출력 채널을 합친다.
    • 이는 공간 특성의 학습과 채널 방향 특성의 학습을 분리하는 효과를 낸다. 입력에서 공간상 위치는 상관관계가 크지만 채널별로는 매우 독립적이라고 가정한다면 타당하다.
    • 이 방법은 모델 파라미터와 연산의 수를 크게 줄여주기 때문에 작고 더 빠른 모델을 만든다. 합성곱을 통해 더 효율적으로 학습하기 때문에 적은 데이터로도 더 좋은 표현을 학습하고 결국 성능이 더 높은 모델을 만든다.

  • 이 장점은 제한된 데이터로 작은 모델을 처음부터 훈련시킬 때 특히 더 중요하다. 다음은 작은 데이터셋에서 이미지 분류 문제(소프트맥스 분류)를 위한 가벼운 깊이별 분리 컨브넷을 만드는 예이다.
from keras.models import Sequential, Model
from keras import layers

height = 64
width = 64
channels = 3
num_classes = 10

model = Sequential()
model.add(layers.SeparableConv2D(32, 3, activation='relu', input_shape=(height, width, channels,)))
model.add(layers.SeparableConv2D(32, 3, activation='relu'))
model.add(layers.MaxPooling2D(2))

model.add(layers.SeparableConv2D(64, 3, activation='relu'))
model.add(layers.SeparableConv2D(128, 3, activation='relu'))
model.add(layers.MaxPooling2D(2))

model.add(layers.SeparableConv2D(64, 3, activation='relu'))
model.add(layers.SeparableConv2D(128, 3, activation='relu'))
model.add(layers.GlobalAveragePooling2D())

model.add(layers.Dense(32, activation='relu'))
model.add(layers.Dense(num_classes, activation='softmax'))

model.compile(optimizer='rmsprop', loss='categorical_crossentropy')

하이퍼파라미터 최적화

  • 딥러닝 모델을 만들 때 무작위로 보이는 결정을 많이 해야 한다.
    • 얼마나 많은 층을 쌓아야 할지, 얼마나 많은 유닛이나 필터를 두어야 할지, relu 활성화 함수를 사용해야 할지 아니면 다른 함수를 사용해야 할지, 어떤 층 뒤에 BatchNormalization을 사용해야 할지, 드롭아웃은 얼마나 해야 할지 등이다.
    • 이런 구조에 관련된 파라미터를 역전파로 훈련되는 모델 파라미터와 구분하여 하이퍼파라미터(hyperparameter)라고 부른다.
  • 실제로 경험 많은 머신 러닝 엔지니어와 연구자는 하이퍼파라미터에 따라 작동하는 것과 작동하지 않는 것에 대한 직관을 가지고 있다. 하이퍼파라미터 튜닝에 관한 기술을 갖고 있는 셈이다.
    • 하지만 공식적인 규칙은 없다. 주어진 문제에서 최대의 성능을 얻고 싶다면 사람이 임의로 선택한 결정에 만족해서는 안된다.
    • 직감이 좋다라고 하더라도 첫 번째 선택은 항상 최적치가 아니다. 옵션을 수정하고 모델을 반복적으로 다시 훈련하여 선택 사항을 개선해야 한다. 이것이 머신 러닝 엔지니어와 연구자들이 대부분의 시간을 쓰는 일이다.
    • 하지만 하루 종일 하이퍼파라미터를 수정하는 것은 사람이 할 일이 아니고, 기계에 위임하는 것이 더 낫다.
  • 가능한 결정 공간을 자동적, 조직적, 규칙적 방법으로 탐색해야 한다. 가능성 있는 구조를 탐색해서 실제 가장 높은 성능을 내는 것을 찾아야 한다. 하이퍼파라미터 자동 최적화가 이에 관련된 분야이다. 이는 하나의 연구 분야이며 중요한 분야이다.
  • 전형적인 하이퍼파라미터 최적화 과정은 다음과 같다.
    • 일련의 하이퍼파라미터를 (자동으로) 선택한다.
    • 선택된 하이퍼파라미터로 모델을 만든다.
    • 훈련 데이터에 학습하고 검증 데이터에서 최종 성능을 측정한다.
    • 다음으로 시도할 하이퍼파라미터를 (자동으로) 선택한다.
    • 이 과정을 반복한다.
    • 마지막으로 테스트 데이터에서 성능을 측정한다.
  • 주어진 하이퍼파라미터에서 얻은 검증 성능을 사용하여 다음 번에 시도할 하이퍼파라미터를 선택하는 알고리즘이 이 과정의 핵심이다.
    • 여러 가지 기법을 사용할 수 있는데, 베이지안 최적화(bayesian optimization), 유전 알고리즘(genetic algorithms), 간단한 랜덤 탐색(random search) 등이다.
  • 모델의 가중치를 훈련하는 것은 비교적 쉽다. 미니 배치 데이터에 대한 손실 함수 값을 계산하고 역전파 알고리즘을 사용하여 올바른 방향으로 가중치를 이동하면 된다. 반면 하이퍼파라미터를 업데이트하는 것은 매우 어려운 일이다. 다음을 생각해 보자.
    • 피드백 신호를 계산하는 것은 매우 비용이 많이 든다(이 하이퍼파라미터가 성능이 높은 모델을 만들어 낼까?) 새로운 모델을 만들고 데이터셋을 사용하여 처음부터 다시 훈련해야 한다.
    • 하이퍼파라미터 공간은 일반적으로 분리되어 있는 결정들로 채워진다. 즉 연속적이지 않고 미분 가능하지 않다. 그러므로 하이퍼파라미터 공간에 경사 하강법을 사용할 수 없다. 대신 경사 하강법보다 훨씬 비효율적인 그래디언트-프리(gradient-free) 최적화 기법을 사용해야 한다.
  • 이 분야가 어렵고 아직 초창기이기 때문에 모델 최적화에 사용할 도구가 매우 적다. 가장 단순하지만 종종 랜덤 탐색(반복적으로 랜덤하게 하이퍼파라미터를 선택)이 제일 좋은 방법일 때가 많다.
    • 랜덤 탐색보다 더 나은 도구는 하이퍼파라미터 최적화를 위한 파이썬 라이브러리인 Hyperopt(https://github.com/hyperopt/hyperopt)이다.
    • 이 라이브러리는 잘 작동할 것 같은 하이퍼파라미터 조합을 예측하기 위해 내부적으로 Parzen 트리 추정기를 사용한다.
    • 다른 라이브러리로 Hyperas(http://github.com/maxpumperla/hyperas)는 Hyperopt와 연동하여 케라스 모델에 사용할 수 있다.
  • 전체적으로 봤을 때 하이퍼파라미터 최적화는 어느 작업에서 최고의 모델을 얻거나 머신 러닝 경연 대회에서 우승하기 위한 강력한 도구이다.
    • 오래 전에는 사람들이 얕은 머신 러닝 모델에 넣을 특성을 직접 만들었다. 이는 매우 최적화되지 않은 방법이다.
    • 요즘에는 딥러닝이 계층적인 특성 엔지니어링 작업을 자동화한다. 수작업이 아니라 피드백 신호를 사용하여 특성이 학습된다.
    • 어찌보면 당연한데, 같은 식으로 수작업으로 모델 구조를 만드는 것이 아니라 이론에 근거하여 모델 구조를 최적화해야 한다.
    • 이 글을 쓰는 시점에는 하이퍼파라미터 자동 최적화 분야는 매우 초기 단계이고 미성숙하다. 딥러닝도 수년 전에는 그랬다. 하지만 다음 몇 년 동안 크게 성장할 것으로 기대한다.

모델 앙상블

  • 모델 앙상블(model ensemble)은 가장 좋은 결과를 얻을 수 있는 또 다른 강력한 기법이다. 앙상블은 여러 개의 다른 모델의 예측을 합쳐서 더 좋은 예측을 만든다.
    • 캐글 같은 머신 러닝 경연 대회에서는 우승자들이 대규모 모델 앙상블을 사용한다. 이런 앙상블은 아주 뛰어난 단일 모델보다도 성능이 좋다.
  • 앙상블은 독립적으로 훈련된 다른 종류의 좋은 모델이 각기 다른 장점을 가지고 있다는 가정을 바탕으로 한다.
    • 각 모델은 예측을 만들기 위해 조금씩 다른 측면을 바라본다. 데이터의 모든 면이 아니고 부분 특징이다.
    • (장님과 코끼리 예시)
  • 이 장님들이 훈련 데이터의 매니폴드를 이해하려는 머신 러닝 모델이다. 
    • 각자의 가정(고유한 모델 구조와 랜덤 가중치 초기화)을 이용하고 각자의 관점으로 이해한다. 각 모델은 데이터의 일부분에 맞는 정답을 찾지만 완전한 정답은 아니다.
    • 이들의 관점을 모으면 데이터를 훨씬 더 정확하게 묘사할 수 있다. 코끼리의 신체 부위는 다양하다. 장님 한 사람이 정확히 알아맞히지 못하지만 모두 모여 이야기하면 매우 정확하게 묘사할 수 있다.
  • 분류 예를 들어보자. 분류기 예측을 (앙상블 하기 위해) 합치는 가장 쉬운 방법은 추론할 때 나온 예측을 평균 내는 것이다.
preds_a = model_a.predict(x_val)
preds_b = model_b.predict(x_val)
preds_c = model_c.predict(x_val)
preds_d = model_d.predict(x_val)

final_preds = 0.25 * (preds_a + preds_b + preds_c + preds_c)
  • 이 방식은 분류기들이 어느 정도 비슷하게 좋을 때 잘 작동한다. 분류기 중 하나가 다른 모델보다 월등히 나쁘면 최종 예측은 앙상블에 있는 가장 좋은 분류기만큼 좋지 않을 수 있다.
  • 분류기를 앙상블하는 좋은 방법은 검증 데이터에서 학습된 가중치를 사용하여 가중 평균하는 것이다. 전형적으로 분류기가 좋을수록 높은 가중치를 가지고 나쁜 분류기일 수록 낮은 가중치를 갖는다.
    • 좋은 앙상블 가중치를 찾기 위해 랜덤 서치나 넬더-미드(Nelder-Mead) 방법 같은 간단한 최적화 알고리즘을 사용할 수 있다.
preds_a = model_a.predict(x_val)
preds_b = model_b.predict(x_val)
preds_c = model_c.predict(x_val)
preds_d = model_d.predict(x_val)

final_preds = 0.5 * preds_a + 0.25 * preds_b + 0.1 * preds_c + 0.15 * preds_c
  • 이외에도 여러 변종이 있다. 예컨대 예측의 지수 값을 평균할 수 있다. 일반적으로 검증 데이터에서 찾은 최적의 가중치로 단순하게 가중 평균하는 방법이 좋은 기본값이다.
  • 앙상블이 잘 작동하게 만드는 핵심은 분류기의 다양성이다. 다양성이 앙상블의 힘이다. 장님들이 코끼리의 코만 만졌다면 모두 코끼리가 뱀 같다고 동의했을 것이다.
    • 이런 이유로 가능한 최대한 다르면서 좋은 모델을 앙상블해야 한다. 일반적으로 매우 다른 구조를 가지거나 다른 종류의 머신 러닝 방법을 말한다.
    • 같은 네트워크를 랜덤 초기화를 다르게 하여 따로따로 여러 번 훈련해서 앙상블하는 것은 거의 해볼 가치가 없다. 모델 사이 차이점이 랜덤 초기화와 모델에 주입되는 훈련 데이터의 순서라면 이 앙상블은 다양성이 낮고 하나의 모델보다 아주 조금만 성능이 향상될 것이다.
  • 모든 문제에 적용하지는 못하지만 실전에 잘 동작하는 한 가지 방법은 트리 기반 모델(랜덤 포레스트나 그래디언트 부스팅 트리)이나 심층 신경망을 앙상블하는 것이다.
  • 2014년 안드레이 콜레프(Andrei Kolev)와 필자는 캐글의 힉스 보손 붕괴(Higgs Boson decay) 감지 대회에서 여러 트리 모델과 심층 신경망을 사용하여 4위를 했다.
    • 특별하게 앙상블 모델 중 하나는 다른 방법을 사용해서 만들었다. (RGF(Regularized Greedy Forest) 모델이었다) 이 모델은 다른 모델보다 눈에 띄게 나빴다.
    • 당연히 앙상블에서 낮은 가중치를 할당했다. 하지만 놀랍게도 이 모델이 전체 앙상블의 성능을 크게 향상시켰다. 이 모델이 다른 모델과 매우 달랐기 때문이다. 다른 모델이 가지지 못한 정보를 제공한 것이다.
    • 이것이 앙상블의 핵심이다. 최상의 모델이 얼마나 좋은지보다 앙상블의 후보 모델이 얼마나 다양한지가 중요하다.
  • 최근 실전에서 매우 성공적으로 사용되는 기본 앙상블 스타일은 딥러닝과 얕은 모델을 섞은 넓고 깊은(wide and deep) 모델이다.
    • 이런 모델은 심층 신경망과 많은 선형 모델을 함께 훈련한다. 다양한 종류의 모델들을 함께 훈련하는 것은 모델 앙상블을 만드는 또 다른 방법이다.

정리

  • 고성능 심층 컨브넷을 만들려면 잔차 연결, 배치 정규화, 깊이별 분리 합성곱을 사용해야 한다. 미래에는 깊이별 분리 합성곱이 일반적인 합성곱을 완전히 대체할 것이다. 애플리케이션이 1D나 2D 또는 3D인지와 상관없이 아주 효율적으로 표현을 학습하기 때문이다.
  • 심층 네트워크를 만들 때 많은 하이퍼파라미터와 네트워크 구조를 선택해야 한다. 이 값들이 모여 모델의 성능을 결정한다. 이런 선택을 직관이나 랜덤한 선택에 의존하지 않고 최적의 선택을 찾기 위해 하이퍼파라미터 공간을 조직적으로 탐색하는 것이 좋다.
    • 현재는 이 과정에 비용이 많이 들고 좋은 도구도 없다. 하지만 Hyperopt와 Hyperas 라이브러리가 도움이 될 수 있다. 하이퍼파라미터를 최적화할 때 검증 세트에 과대적합된다는 것을 잊지 말라
  • 머신 러닝 경연 대회에서 우승하거나 어떤 문제에서 최상의 결과를 얻으려면 대규모로 모델을 앙상블하게 된다. 최적화가 잘 된 가중 평균으로 만든 앙상블은 보통 충분히 좋은 결과를 만든다.
    • 다양성이 중요하다는 것을 기억하라. 비슷한 모델을 앙상블하는 것은 거의 쓸모가 없다. 가장 좋은 앙상블은 가능한 서로 다른 모델로 만드는 것이다. (당연히 가능한 예측 성능이 높은 것 중에서 고른다)

요약

  • 이 장에서 배운 것들
    • 임의의 층 그래프를 구성하는 모델을 만드는 방법, 층을 재사용하는 방법(가중치 공유), 파이썬 함수 방식으로 모델을 사용하는 방법(모델 템플릿)
    • 케라스 콜백을 사용하여 훈련하는 동안 모델을 모니터링 하고 모델 상태를 바탕으로 작업을 수행한다.
    • 텐서보드를 사용하여 측정 지표, 활성화 출력의 히스토그램, 임베딩 공간을 시각화한다.
    • 배치 정규화, 깊이별 분리 합성곱, 잔차 연결
    • 하이퍼파라미터 최적화와 모델 앙상블을 사용하는 이유
[ssba]

The author

Player가 아니라 Creator가 되려는 자/ suyeongpark@abyne.com

댓글 남기기

이 사이트는 스팸을 줄이는 아키스밋을 사용합니다. 댓글이 어떻게 처리되는지 알아보십시오.