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

지금까지 이 책에서 소개한 모든 신경망은 Sequential 모델을 사용하여 만들었습니다. Sequential 모델은 네트워크 입력과 출력이 하나라고 가정합니다. 이 모델은 층을 차례대로 쌓아 구성합니다(그림 7-1 참고).

나타낼 수 없음
그림 7-1. Sequential 모델: 차례대로 쌓은 층

많은 경우에 이런 가정이 적절합니다. 하지만 일부 네트워크는 개별 입력이 여러 개 필요하거나 출력이 여러 개 필요합니다. 또한 층을 차례대로 쌓지 않고 층 사이를 연결하여 그래프처럼 만드는 네트워크도 있습니다.

입력이 여러 개인 예로 중고 의류의 시장 가격을 예측하는 딥러닝 모델을 상상해 보죠. 이 모델은 사용자가 제공한 메타데이터(의류 브랜드, 연도 등), 사용자가 제공한 텍스트 설명, 제품 사진을 입력으로 사용합니다. 각각의 경우 아래와 같이 모델을 사용할 수 있습니다.

  • 메타데이터: 원-핫 인코딩으로 바꾸고 완전 연결 네트워크를 사용하여 가격을 예측
  • 텍스트: RNN이나 1D 컨브넷
  • 사진 이미지: 2D 컨브넷

이 세 모델을 동시에 모두 사용할 수 있을까요? 간단한 방법은 3개의 모델을 따로 훈련하고 각 예측을 가중 평균(weighted average)하는 것입니다. 각 모델에서 추출한 정보가 중복된다면 이 방식은 최적이 아닐 것입니다. 가능한 모든 종류의 입력 데이터를 동시에 사용해서 정확한 하나의 모델을 학습하는 것이 더 나은 방법입니다. 이 모델은 3개의 입력 가지(branch)가 필요합니다(그림 7-2 참고).

나타낼 수 없음
그림 7-2. 다중 입력 모델

출력이 여러 개인 예로 소설이나 짧은 글이 있을 때 자동으로 장르별로 분류(로맨스나 스릴러 등)하고, 글을 쓴 대략의 시대를 예측하는 모델을 보겠습니다. 물론 장르를 위한 모델과 시대를 위한 모델로 따로 훈련할 수 있습니다. 하지만 이 속성들은 통계적으로 독립적이지 않기 때문에 동시에 장르와 시대를 함께 예측하도록 학습해야 더 좋은 모델을 만들 수 있습니다. 이 모델은 2개의 출력 또는 머리(head)를 가집니다(그림 7-3 참고). 장르와 시대 사이의 상관관계 때문에 소설 시대를 알면 장르의 공간에서 정확하고 풍부한 표현을 학습하는 데 도움이 됩니다. 그 반대도 마찬가지입니다.

나타낼 수 없음
그림 7-3. 다중 출력 모델

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

나타낼 수 없음
그림 7-4. 인셉션 모듈: 나란히 놓인 합성곱 층으로 구성된 서브그래프

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

나타낼 수 없음
그림 7-5. 잔차 연결: 하위 층의 출력을 상위 층의 특성 맵에 더한다

이러한 경우에 다중 입력 모델, 다중 출력 모델, 그래프 구조를 띤 모델이 필요하지만 케라스의 Sequential 클래스를 사용해서는 만들지 못합니다. 케라스에는 훨씬 더 일반적이고 유연한 다른 방법인 함수형 API가 있습니다. 이 절에서 함수형 API가 무엇인지 소개하고, 함수형 API를 사용하는 방법과 이를 사용하여 할 수 있는 것을 자세히 설명하겠습니다.

7.1.1 함수형 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)

3

간단한 예를 통해 Sequential 모델과 함수형 API로 만든 동일한 모델을 나란히 비교해 보겠습니다.

from keras.models import Sequential, Model
from keras import layers
from keras import Input

# 익숙한 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 = Model(input_tensor, output_tensor)

모델 구조를 확인해 보죠!

>>> model.summary()
Model: "model_1"
_________________________________________________________________
Layer (type)                 Output Shape              Param #
=================================================================
input_1 (InputLayer)         (None, 64)                0
_________________________________________________________________
dense_4 (Dense)              (None, 32)                2080
_________________________________________________________________
dense_5 (Dense)              (None, 32)                1056
_________________________________________________________________
dense_6 (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가 발생합니다.

>>> unrelated_input = Input(shape=(64,))
>>> bad_model = Model(unrelated_input, output_tensor)
RuntimeError: Graph disconnected: cannot obtain value for tensor
Tensor("input_1:0", shape=(?, 64), dtype=float32) at layer "input_1".

이 에러는 케라스가 출력 텐서에서 input_1 텐서로 다다를 수 없다는 뜻입니다. Model 객체를 사용한 컴파일, 훈련, 평가 API는 Sequential 클래스와 같습니다.

(책에서는 입력 크기를 달리했지만 같은 크기로 해도 역시 같은 에러를 볼 수 있습니다. 여기서 결론은 크기가 중요한게 아니라 모델을 정의할 때 사용했던 input_tensor를 사용하여 모델을 정의해야만 한다는 것입니다.)

# 모델을 컴파일합니다.
model.compile(optimizer='rmsprop', loss='categorical_crossentropy')

# 훈련을 위해 랜덤한 넘파이 데이터를 생성합니다.
import numpy as np
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)

7.1.2 다중 입력 모델

일반적으로 이런 모델은 서로 다른 입력 가지를 합치기 위해 여러 텐서를 연결할 수 있는 텐서를 더하거나 이어 붙이는 식의 층을 사용합니다. 이와 관련된 케라스의 함수는 keras.layers.add, keras.layers.concatenate 등입니다.4 아주 간단한 다중 입력 모델을 살펴보겠습니다. 질문-응답(question-answering) 모델입니다.

전형적인 질문-응답 모델은 2개의 입력을 가집니다. 하나는 자연어 질문이고, 또 하나는 답변에 필요한 정보가 담겨 있는 텍스트(예를 들어 뉴스 기사)입니다. 그러면 모델은 답을 출력해야 합니다. 가장 간단한 구조는 미리 정의한 어휘 사전에서 소프트맥스 함수를 통해 한 단어로 된 답을 출력하는 것입니다(그림 7-6 참고).

나타낼 수 없음
그림 7-6. 질문-응답 모델

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

# 코드 7-1 2개의 입력을 가진 질문-응답 모델의 함수형 API 구현하기
from keras.models import Model
from keras import layers
from keras import Input

text_vocabulary_size = 10000
question_vocabulary_size = 10000
answer_vocabulary_size = 500

# 텍스트 입력은 길이가 정해지지 않은 정수 시퀀스입니다. 입력 이름을 지정할 수 있습니다.
text_input = Input(shape=(None,), dtype='int32', name='text')
# 입력을 크기가 64인 벡터의 시퀀스로 임베딩합니다.
embedded_text = layers.Embedding(text_vocabulary_size, 64)(text_input)
# LSTM을 사용하여 이 벡터들을 하나의 벡터로 인코딩합니다.
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)

# 모델 객체를 만들고 2개의 입력과 출력을 주입합니다.
model = Model([text_input, question_input], answer)
model.compile(optimizer='rmsprop', loss='categorical_crossentropy', metrics=['acc'])

5 6

그럼 이렇게 입력이 2개인 모델은 어떻게 훈련할까요? 두 가지 방식이 있습니다. 넘파이 배열의 리스트를 주입하거나 입력 이름과 넘파이 배열로 이루어진 딕셔너리를 모델의 입력으로 주입할 수 있습니다. 당연하게 두 번째 방식은 입력 이름을 설정했을 때 사용할 수 있습니다.

# 코드 7-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)

7.1.3 다중 출력 모델

같은 식으로 함수형 API를 사용하여 다중 출력(또는 다중 머리) 모델을 만들 수 있습니다. 예를 들면 소셜 미디어에서 익명 사용자의 포스트를 입력으로 받아 그 사람의 나이, 소득 수준, 성별 등을 예측하는 모델입니다.(그림 7-7 참고).

# 코드 7-3 3개의 출력을 가진 함수형 API 구현하기
from keras import layers
from keras import Input
from keras.models import Model
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.GlobalMaxPooling1D()(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])

나타낼 수 없음
그림 7-7. 3개의 출력을 가진 소셜 미디어 모델

이런 모델을 훈련하려면 네트워크 출력마다 다른 손실 함수를 지정해야 합니다. 예를 들어 나이 예측은 스칼라 회귀 문제이지만 성별 예측은 이진 클래스 문제라 훈련 방식이 다릅니다. 경사 하강법은 하나의 스칼라 값을 최소화하기 때문에 모델을 훈련하려면 이 손실들을 하나의 값으로 합쳐야 합니다. 손실 값을 합치는 가장 간단한 방법은 모두 더하는 것입니다. 케라스에서는 compile 메서드에 리스트나 딕셔너리를 사용하여 출력마다 다른 손실을 지정할 수 있습니다. 계산된 손실 값은 전체 손실 하나로 더해지고 훈련 과정을 통해 최소화됩니다.

# 코드 7-4 다중 출력 모델의 컴파일 옵션: 다중 손실
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를 줄 수 있습니다.

# 코드 7-5 다중 출력 모델의 컴파일 옵션: 손실 가중치
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.])

다중 입력 모델과 마찬가지로 넘파이 배열의 리스트나 딕셔너리를 모델에 전달하여 훈련합니다.

# 코드 7-6 다중 출력 모델에 데이터 주입하기
# age_targets, income_targets, gender_targets가 넘파이 배열이라고 가정합니다.
model.fit(posts, [age_targets, income_targets, gender_targets], epochs=10, batch_size=65)

# 위와 동일합니다(추력 층에 이름을 지정했을 때만 사용할 수 있습니다).
model.fit(posts, {'age': age_targets, 'income': income_targets, 'gender': gender_targets},
          epochs=10, batch_size=65)

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

함수형 API를 사용하면 다중 입력이나 다중 출력 모델뿐만 아니라 내부 토폴로지가 복잡한 네트워크도 만들 수 있습니다. 케라스의 신경망은 층으로 구성된 어떤 비순환 유향 그래프(directed acyclic graph)도 만들 수 있습니다. 비순환이라는 것이 중요합니다. 다시 말해 이 그래프는 원형을 띨 수 없습니다. 텐서 x가 자기 자신을 출력하는 층의 입력이 될 수 없습니다. 만들 수 있는 루프(즉 순환 연결)는 순환 층의 내부에 있는 것뿐입니다.

그래프로 구현된 몇 개의 신경망 컴포넌트가 널리 사용됩니다. 가장 유명한 2개는 인셉션 모듈과 잔차 연결입니다. 케라스에서 이 2개의 컴포넌트를 어떻게 구현하는지 살펴보겠습니다. 함수형 API를 사용하여 층의 그래프를 만드는 방법을 이해하는 데 도움이 될 것입니다.

인셉션 모듈

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

나타낼 수 없음
그림 7-8. 인셉션 모듈


Note 1×1 합성곱의 목적

이미 알고 있듯이 합성곱은 입력 텐서에서 타일 주변의 패치를 추출하고 각 패치에 동일한 연산을 수행합니다. 이 경우는 추출된 패치가 하나의 타일로 이루어졌을 때입니다. 이 합성곱 연산은 모든 타일 벡터를 하나의 Dense 층에 통과시키는 것과 동일합니다. 즉 입력 텐서의 채널 정보를 혼합한 특성을 계산합니다. 공간 방향으로는 정보를 섞지 않습니다(한 번에 하나의 타일만 처리하기 때문입니다). 이런 1×1 합성곱[또는 점별 합성곱(pointwise convolution)]은 인셉션 모듈의 특징입니다. 채널 방향의 특성 학습과 공간 방향의 특성 학습을 분리하는 데 도움을 줍니다. 채널이 공간 방향으로 상관관계가 크고 채널 간에는 독립적이라고 가정하면 납득할 만한 전략입니다.


다음은 함수형 API를 사용하여 그림 7-8의 모듈을 구현하는 예입니다.12 이 예에서 입력 x는 4D 텐서라고 가정합니다.

from keras import layers
# 모든 가지는 동일한 스트라이드(2)를 사용합니다. 출력 크기를 동일하게 만들어 하나로 합치기 위해서입니다.
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', stride=2)(branch_b)

# 이 가지에서는 평균 풀링 층에서 스트라이드를 적용합니다.
branch_c = layers.AveragePooling2D(3, stride=2)(x)
branch_c = layers.Conv2D(128, 3, activation='relu', stride=2)(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', stride=2)(branch_d)

# 모든 가지의 출력을 연결하여 모듈의 출력을 만듭니다.
output = layers.concatenate([branch_a, branch_b, branch_c, branch_d], axis=1)

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

잔차 연결

잔차 연결(residual connection)은 엑셉션을 포함하여 2015년 이후 등장한 많은 네트워크 구조에 있는 그래프 형태의 네트워크 컴포넌트입니다. 2015년 후반 ILSVRC ImageNet 경연 대회 우승 팀인 마이크로소프트의 허 등이 소개했습니다.16 대규모 딥러닝 모델에서 흔히 나타나는 두 가지 문제인 그래디언트 소실과 표현 병목(representational bottleneck)을 해결했습니다. 일반적으로 10개 층 이상을 가진 모델에 잔차 연결을 추가하면 도움이 됩니다.

잔차 연결은 하위 층의 출력을 상위 층의 입력으로 사용합니다. 순서대로 놓인 네트워크를 질러 가는 연결이 만들어집니다. 하위 층의 출력이 상위 층의 활성화 출력에 연결되는 것이 아니고 더해집니다. 따라서 두 출력의 크기가 동일해야 합니다. 크기가 다르면 선형 변환을 사용하여 하위 층의 활성화 출력을 목표 크기로 변환합니다(예를 들어 활성화 함수를 사용하지 않는 Dense 층이나 합성곱의 특성 맵이라면 활성화 함수가 없는 1×1 합성곱).

다음 코드는 케라스에서 특성 맵의 크기가 같을 때 원본을 그대로 사용하는 잔차 연결을 구현한 예입니다. 여기서는 입력 x가 4D 텐서라고 가정합니다.

from keras import layers

x = ...
# x에 어떤 변환을 적용합니다.
y = layers.Conv2D(128, 3, activation='relu', padding='same')(x)
y = layers.Conv2D(128, 3, activation='relu', padding='same')(y)
y = layers.Conv2D(128, 3, activation='relu', padding='same')(y)

# 원본 x를 출력 특성에 더합니다.
y = layers.add([y, x])

17

다음은 특성 맵의 크기가 다를 때 선형 변환을 사용하여 잔차 연결을 구현한 예입니다(여기에서도 입력 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')(y)
y = layers.MaxPooling2D(2, strides=2)(y)

# y와 크기를 맞추기 위해 1×1 합성곱을 사용하여 원본 텐서 x를 다운샘플링합니다.
residual = layers.Conv2D(128, 1, strides=2, padding='same')(x)
# 다운샘플링된 x를 출력 특성에 더합니다.
y = layers.add([y, residual])

Note

딥러닝의 표현 병목

Sequential 모델에서 표현 학습을 위한 층은 다른 층위에 연달아 놓입니다. 다시 말해 층은 이전 층의 활성화 출력 정보만 사용합니다. 어떤 층이 너무 작으면(예를 들어 특성이 너무 저차원이면) 이 활성화 출력에 얼마나 많은 정보를 채울 수 있느냐에 모델 성능이 좌우됩니다.

이 개념을 신호 처리에 비유할 수 있습니다. 일련의 연산으로 구성된 오디오 처리 파이프라인이 있다고 가정해 보죠. 각 단계는 이전 연산의 출력을 입력으로 사용합니다. 한 연산이 신호를 저주파 영역(예를 들어 0~15kHz)으로 잘라 냈다면 이후 연산이 줄어든 주파수를 복원할 수 없을 것입니다. 손실된 정보는 영구 불변입니다. 하위 층의 정보를 다시 주입하는 잔차 연결은 딥러닝 모델에서 이 이슈를 어느 정도 해결합니다.

딥러닝의 그래디언트 소실 문제

심층 신경망을 훈련하는 데 사용되는 핵심 알고리즘인 역전파는 출력 손실에서 얻은 피드백 신호를 하위 층에 전파합니다. 피드백 신호가 깊이 쌓인 층을 통과하여 전파되면 신호가 아주 작아지거나 완전히 사라질 수도 있습니다. 이렇게 되면 네트워크가 훈련되지 않습니다. 이를 그래디언트 소실(vanishing gradient) 문제라고 합니다.

이 문제는 심층 신경망과 긴 시퀀스를 처리하는 순환 신경망에서 모두 나타납니다. 양쪽 모두 피드백 신호가 일련의 긴 연산을 통과하여 전파되기 때문입니다. 순환 신경망에서 LSTM 층이 이 문제를 해결하기 위해 사용하는 방식을 보았습니다. 이동 트랙이 주요 처리 트랙에 나란히 정보를 실어 날랐습니다. 잔차 연결은 피드포워드 심층 신경망에서 비슷한 역할을 합니다. 하지만 좀 더 단순합니다. 주 네트워크 층에 나란히 단순한 선형 정보를 실어 나릅니다. 이는 그래디언트가 깊게 쌓은 층을 통과하여 전파하도록 도와줍니다.18


7.1.5 층 가중치 공유

함수형 API의 중요한 또 하나의 기능은 층 객체를 여러 번 재사용할 수 있다는 것입니다. 층 객체를 두 번 호출하면 새로운 층 객체를 만들지 않고 각 호출에 동일한 가중치를 재사용합니다. 이런 기능 때문에 공유 가지를 가진 모델을 만들 수 있습니다.

예를 들어 두 문장 사이의 의미가 비슷한지 판단하는 모델을 가정해 보죠. 이 모델은 2개의 입력(비교할 2개의 문장)을 받고 0과 1 사이의 점수를 출력합니다. 0은 관련 없는 문장을 의미하고 1은 두 문장이 동일하거나 재구성되었다는 것을 의미합니다.19

이런 문제에서는 두 입력 시퀀스가 바뀔 수 있습니다. 즉, A에서 B에 대한 유사도는 B에서 A에 대한 유사도와 같습니다. 이런 이유 때문에 각 입력 문장을 처리하는 2개의 독립된 모델을 학습하는 것은 이치에 맞지 않습니다. 그 대신 하나의 LSTM 층으로 양쪽을 모두 처리하는 것이 좋습니다. 이 LSTM 층의 표현(가중치)은 두 입력에 대해 함께 학습됩니다. 이를 샴 LSTM(Siamese LSTM) 모델 또는 공유 LSTM이라고 부릅니다.

다음은 케라스의 함수형 API로 공유 층(재사용 층)을 사용하는 모델을 구현하는 예입니다.

from keras import layers
from keras import Input
from keras import Model

# LSTM 층 객체 하나를 만듭니다.
lstm = layer.LSTM(32)

# 모델의 왼쪽 가지를 구성합니다. 입력은 크기가 128인 벡터의 가변 길이 시퀀스입니다.
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')(merge)

# 모델 객체를 만들고 훈련합니다. 이런 모델을 훈련하면 LSTM층의 가중치는 양쪽 입력을 바탕으로 업데이트됩니다.
model = Model([left_input, right_input], predictions)
model.fit([left_input, right_input], targets)

당연하게 층 객체는 한 번 이상 사용할 수 있습니다. 같은 가중치를 재사용하면서 얼마든지 여러 번 호출할 수 있습니다.

7.1.6 층과 모델

함수형 API에서는 모델을 층처럼 사용할 수 있습니다. 모델을 ‘커다란 층’으로 생각해도 됩니다.20 Sequential 클래스와 Model 클래스에서 모두 동일합니다. 이 말은 입력 텐서로 모델을 호출해서 출력 텐서를 얻을 수 있다는 뜻입니다.

y = model(x)

모델에서 입력 텐서와 출력 텐서가 여러 개이면 텐서의 리스트로 호출합니다.

y1, y2 = model([x1, x2])

모델 객체를 호출할 때 모델의 가중치가 재사용됩니다. 층 객체를 호출할 때와 정확히 같습니다. 층 객체나 모델 객체나 객체를 호출하는 것은 항상 그 객체가 가진 학습된 표현을 재사용합니다.

모델 객체를 재사용하는 간단한 실전 예는 듀얼 카메라에서 입력을 받는 비전 모델입니다. 두 카메라가 몇 센티미터(1인치) 간격을 두고 나란히 있습니다. 이런 모델은 깊이를 감지할 수 있습니다. 많은 애플리케이션에서 유용한 기능입니다. 왼쪽 카메라와 오른쪽 카메라에서 시각적 특징을 추출하여 합치기 위해 2개의 독립된 모델을 사용할 필요가 없습니다. 두 입력에 저수준 처리 과정이 공유될 수 있습니다. 다시 말해 가중치가 같고 동일한 표현을 공유하는 층을 사용합니다. 다음은 케라스에서 샴 비전 모델(공유 합성곱 기반 층)을 구현하는 예입니다.

from keras import layers
from keras import applications
from keras import Input
# 이미지 처리 기본 모델은 엑셉션 네트워크입니다(합성곱 기반 층만 사용합니다).
xception_base = applications.Xception(weights=None,
                                      include_top=False)

# 입력은 250x250 RGB 이미지입니다.
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)

21

7.1.7 정리

이것으로 케라스의 함수형 API 소개를 마칩니다. 이는 고급 심층 신경망 구조를 구축하기 위해 필수적인 도구입니다. 여기에서 다음 내용을 배웠습니다.

  • 차례대로 층을 쌓는 것 이상이 필요할 때는 Sequential API 사용 불가
  • 함수형 API를 사용하여 다중 입력, 다중 출력, 복잡한 네트워크 토폴로지를 갖는 케라스 모델을 만드는 방법
  • 다른 네트워크 가지에서 같은 층이나 모델 객체를 여러 번 호출하여 가중치를 재사용하는 방법
  1. Christian Szegedy et al., “Going Deeper with Convolutions,” Conference on Computer Vision and Pattern Recognition (2014), https://arxiv.org/abs/1409.4842

  2. Kaiming He et al., “Deep Residual Learning for Image Recognition,” Conference on Computer Vision and Pattern Recognition (2015), https://arxiv.org/abs/1512.03385

  3. 역주 파이썬에는 클래스 객체를 함수처럼 호출할 수 있는 특수한 __call__() 메서드가 있습니다. 케라스는 이 메서드를 즐겨 사용합니다. dense(input_tensor)를 다르게 쓰면 dense.__call__(input_tensor)와 같습니다. 어떤 객체가 함수 호출이 가능한지 알려면 callable() 내장 함수의 반환값을 확인하세요. 

  4. 이외에도 layers.average(), layers.maximum(), layers.minimum(), layers.multiply(), layers.subtract(), layers.dot()이 있습니다. 이 함수들은 keras.layers.merge 모듈 아래 정의되어 있습니다. 

  5. 이 코드에 나오는 텐서의 크기는 다음과 같습니다. text_input(None, None), embedded_text(None, None, 64), encoded_text(None, 32), question_input(None, None), embedded_question(None, None, 32), encoded_question(None, 16)입니다. encoded_textencoded_question을 합친 concatenated의 크기는 (None, 48)입니다. 500개의 유닛을 가진 Dense 층의 출력인 answer의 크기는 (None, 500)입니다. 모든 텐서의 첫 번째 차원은 배치 차원입니다. 

  6. layers.concatenate()layers.Concatenate 클래스를 단순히 감싼 함수입니다. 이 코드는 concatenated = layers.Concatenate(axis=-1)([encoded_text, encoded_question])과 동일합니다. axis 매개변수는 기본값이 -1이므로 생략해도 됩니다. 

  7. https://arxiv.org/abs/1409.4842

  8. 이 네트워크를 GoogLeNet이라고도 부릅니다. 

  9. Min Lin, Qiang Chen, and Shuicheng Yan, “Network in Network,” International Conference on Learning Representations (2013), https://arxiv.org/abs/1312.4400

  10. 이 논문에서 1×1 합성곱의 아이디어를 소개합니다. 

  11. https://arxiv.org/abs/1512.00567 논문에서 인셉션 V2, V3 모델을 소개합니다. 인셉션 V2와 V3의 인셉션 모듈 구조는 같습니다. 인셉션 V4는 https://arxiv.org/abs/1602.07261에서 소개되었습니다. 

  12. 사실 이 구현 예에서 4개의 가지가 출력하는 텐서의 크기가 조금씩 다릅니다. 실전에서는 padding='same' 옵션을 주어 출력 크기를 맞춥니다. 

  13. François Chollet, “Xception: Deep Learning with Depthwise Separable Convolutions,” Conference on Computer Vision and Pattern Recognition (2017), https://arxiv.org/abs/1610.02357

  14. 엑셉션 모듈은 3×3 커널을 사용한 layers.SeparableConv2D 합성곱을 사용합니다. 입력 채널에 대해 따로따로 3×3 합성곱을 수행하고(이 합성곱에서는 채널을 늘리지 않습니다), 그다음 1×1 합성곱을 적용합니다(출력 채널을 늘립니다). SeparableConv2D의 출력 채널은 128개에서 2,048개까지 네트워크가 깊어질수록 늘어납니다. 엑셉션 네트워크는 keras.applications.xception.Xception에 포함되어 있습니다. 

  15. 인셉션의 모델 파라미터 개수는 약 2,380만 개고 엑셉션의 모델 파라미터 개수는 약 2,280만개로 대략 100만 개 정도 차이가 납니다. 

  16. He et al., “Deep Residual Learning for Image Recognition,” https://arxiv.org/abs/1512.03385

  17. layers.add()layers.Add 클래스를 단순히 감싼 함수입니다. 이 코드는 y = layers.Add()([y, x])와 동일합니다. 

  18. 잔차 연결에서 하위 층의 출력과 상위 층의 출력을 단순히 더했으므로 그래디언트가 그대로 잔차 연결을 따라 하위 층으로도 전달됩니다. 잔차 연결을 따라 내려온 그래디언트는 주 네트워크 층을 거쳐 줄어든 그래디언트와 더해집니다. 

  19. 이런 모델은 대화 시스템(dialog system)에서 자연어 질의에 대한 중복 제거를 포함하여 많은 애플리케이션에서 유용하게 사용될 수 있습니다. 

  20. 이와 같은 개념이 코드 5-20에서도 나옵니다. 여기에서는 한 모델을 다른 모델의 층으로 사용했습니다. 

  21. include_top=False로 지정하여 최상단에 놓인 분류기(전역 평균 풀링과 완전 연결 층)를 제외한 모델을 사용합니다. 

댓글남기기