텍스트 데이터 + 순환 신경망 + 시퀀스 데이터의 처리

본 문서는 [케라스 창시자에게 배우는 딥러닝] 책을 기반으로 하고 있으며, subinium(본인)이 정리하고 추가한 내용입니다. 생략된 부분과 추가된 부분이 있으니 추가/수정하면 좋을 것 같은 부분은 댓글로 이야기해주시면 감사하겠습니다.

  • 본 정리는 6장. 텍스트와 시퀀스를 위한 딥러닝 (1)과 연결된 내용입니다.
  • 예제들은 Colab 에서 진행했습니다.
  • Colab에서 진행하는데 돌리면 멈춰서, 이번에도 중반까지 시도했습니다.
  • 기본적으로 뒤의 예제는 1시간 이상 필요합니다.

6.3 순환 신경망의 고급 사용법

순환 신경망의 성능과 일반화 능력을 향상시키기 위한 세 가지 고급 기술을 살펴보겠습니다. 온도 예측 문제와 함께 각 개념을 이해해보도록 하겠습니다. 이 문제는 다음과 같은 특성을 알 수 있습니다.

  • 시계열 데이터
  • 건물 옥상에 설치된 센서에서 취득한 온도, 기압, 습도 데이터
  • 마지막 데이터 포인트에서 24시간 이후의 온도를 예측
  • 전형적이고 도전적인 문제

기법들은 다음과 같습니다.

  • 순환 드롭아웃(recurrent dropout) : 순환 층에서 과대적합을 방지하기 위해 케라스에 내장되어 있는 드롭아웃을 사용
  • 스태킹 순환 층(stacking recurrent layer) : 네트워크의 표현 능력(representation power)을 증가 - 대신 계산 비용이 많이듬
  • 양방향 순환 층(bidirectional recurrent layer) : 순환 네트워크에 같은 정보를 다른 방향으로 주입하여 정확도를 높이고 기억을 좀 더 오래 유지시킵니다.

6.3.1 기온 예측 문제

이 절에서 사용하는 예제는 날씨 시계열 데이터샛을 사용합니다. 이 데이터는 독일 예나시에 있는 막스 플랑크 생물지구화학 연구소의 지상 관측소에서 수집한 것입니다.

  • 수년간의 데이터
  • 10분 단위의 데이터
  • 14개의 관측치(특징)

본 문제에서는 2009~2016년 사이의 데이터만 사용합니다. 데이터는 다음과 같이 내려받을 수 있습니다.

wget https://s3.amazonaws.com/keras-datasets/jena_climate_2009_2016.csv.zip
unzip jena_climate_2009_2016.csv.zip

다운을 받고 데이터를 살펴봅시다.

# 코드 6-28 예나의 날씨 데이터셋 조사하기

import os

data_dir = './jena_climate'
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:]

print(header)
print(len(lines))
['"Date Time"', '"p (mbar)"', '"T (degC)"', '"Tpot (K)"', '"Tdew (degC)"', '"rh (%)"', '"VPmax (mbar)"', '"VPact (mbar)"', '"VPdef (mbar)"', '"sh (g/kg)"', '"H2OC (mmol/mol)"', '"rho (g/m**3)"', '"wv (m/s)"', '"max. wv (m/s)"', '"wd (deg)"']

420551

총 42만 551개의 데이터가 있습니다 420만분 정도의 데이터입니다. 이제 이를 넘파이 배열로 바꿉니다.

# 코드 6-29 데이터 파싱하기
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

온도의 경향성을 보기위해 가볍게 그래프를 그려봅니다.

# 코드 6-30 식ㅖ열 데이터 온도 그래프 그리기
from matplotlib import pyplot as plt

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

온도데이터

최고 기온과 최저 기온을 보니 저기도 헬인건 분명한 것을 알 수 있습니다! 이건 중요한게 아니고 처음 10일간의 온도 데이터를 따로 보면 다음과 같습니다. 24*60 / 10 * 10 = 1440개의 데이터 포인트로 그린 그래프입니다. 책에선 10일치만 했으니 전 20일치를 보겠습니다. 이유는 없습니다.

# 코드 6-31 처음 20일간 온도 그래프 그리기
plt.plot(range(144*20), temp[:144*20])

온도데이터2

데이터를 관찰하면 매우 추운 날이 점점 따뜻해지는 것을 알 수 있습니다. 한파가 왔다가 점점 날이 풀리는 것을 알 수 있습니다.

분명 하루의 주기성이 있으며 몇 달 그리고 몇 년간의 데이터가 있으니 주기를 통해 예측을 할 수 있을 것 같습니다.

6.3.2 데이터 준비

이 문제의 정확한 정의는 다음과 같습니다.

lookback 타임스텝만큼 이전으로 돌아가서 매 steps 타임스텝마다 샘플링합니다. 이 데이터를 바탕으로 delay 타임스텝 이후 온도를 예측할 수 있을까요?

  • lookback = 1440: 10일 전 데이터로 돌아갑니다.
  • steps = 6: 1시간마다 데이터 포인트 하나를 샘플링합니다.
  • delay = 144: 24시간이 지난 데이터가 타깃이 됩니다.

시작하기 전에 두가지 작업을 처리해야 합니다.

  • 신경망에 주입할 수 있는 형태로 데이터를 전처리합니다. 데이터가 이미 수치형이므로 추가적인 벡터화가 필요하지 않습니다., 하지만 데이터에 있는 각 시계열 특성은 범위가 서로 다르므로 정규화해야합니다.
  • float_data 배열을 받아 과거 데이터의 배치와 미래 타깃 온도를 추출하는 파이썬 제너레이터를 만듭니다. 이 데이터셋에 있는 샘플은 중복이 많습니다. 모든 샘플을 각기 메모리에 적재하는 것은 낭비가 심하므로 원본 데이터를 사용하여 그때그때 배치를 만들어야합니다.

처음 20만 개의 타임스펩을 훈련 데이터로 사용할 것이니 이로 정규화를 진행합니다.

# 코드 6-32 데이터 정규화하기
mean = float_data[:200000].mean(axis=0)
float_data -= mean
std = float_data[:200000].std(axis=0)
float_data /= std

아래 코드는 여기서 사용할 제너레이터입니다. 이 제너레이터 함수는 (samples, targets) 튜플을 반복적으로 반환합니다. 각각은 입력 데이터로 사용할 배치와 이에 대응되는 타깃의 온도의 배열입니다. 다음과 같은 매개변수를 가집니다.

  • data : 정규화를 진행한 부동 소수 데이터로 이루어진 원본 배열
  • lookback : 입력으로 사용하기 위해 거슬러 올라갈 타임스텝
  • delay : 타깃으로 사용할 미래의 타임스텝
  • min_index와 max_index : 추출할 타임스텝의 범위를 지정하기 위한 data 배열의 인덱스, 검증 데이터와 테스트 데이터를 분리하는 데 사용
  • shuffle : 샘플을 섞을지, 시간 순서대로 추출할지 결정
  • batch_size : 배치의 샘플 수
  • step : 테이터를 샘플링할 타임스텝 간격. 1시간에 하나의 데이터 포인트를 추출하기 위해 6으로 지정
# 코드 6-33 시계열 데이터와 타깃을 반환하는 제너레이터 함수

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만 개를 사용하고, 테스트 제너레이터는 나머지를 사용합니다.

# 코드 6-34 후련, 검증, 테스트 제너레이터 준비하기
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)
val_gen = generator(float_data,
                    lookback=lookback,
                    delay=delay,
                    min_index=200001,
                    max_index=300000,
                    step=step,
                    batch_size=batch_size)
test_gen = generator(float_data,
                     lookback=lookback,
                     delay=delay,
                     min_index=300001,
                     max_index=None,
                     step=step,
                     batch_size=batch_size)

# 전체 검증 세트를 순회하기 위해 val_gen에서 추출할 횟수
val_steps = (300000 - 200001 - lookback) // batch_size

# 전체 테스트 세트를 순회하기 위해 test_gen에서 추출할 횟수
test_steps = (len(float_data) - 300001 - lookback) // batch_size

6.3.3 상식 수준의 기준점

복잡한 딥러닝에 들어가기 전 상식 수준에서 해법을 시도해보겠습니다. 정상 여부 확인을 위한 용도이자 딥러닝이 넘어야 할 정도에 대한 기준점을 만드는 것입니다.

이런 방법은 해결책이 없는 새로운 문제를 다루어야 할 때 유용합니다. 강수확률이 70%인 국가에서 매일 비가 온다고 하는 경우 매일 비가 내린다고 한다면 정확도는 70%가 됩니다. 딥러닝은 이보다는 높은 성적을 내야합니다.

이 온도 시계열 데이터는 연속성이 있고 일자별로 주기성을 가지므로 24시간 후 온도는 지금과 동일하다고 예측할 수 있습니다. 이 방법을 평균 절댓값 오차(MAE)로 평가해 보겠습니다.

# 코드 6-35 상식적인 기준 모델의 MAE 계산하기

def evaluate_naive_method():
    batch_maes = []
    for step in range(val_steps):
        samples, targets = next(val_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.29 입니다. 이 온도 데이터는 평균이 0이고 표준 편차가 1이므로 결과값의 의미를 파악하기는 어렵습니다. 평균 절댓값 오차에 표준 편차를 곱하면 알 수 있습니다.

# 코드 6-36 MAE 섭씨 단위로 변환하기

0.29 * std[1]

섭씨 2.57도 임을 알 수 있습니다. 표준 편차가 큰 편입니다. 여기서 std배열은 14개의 특성에 대한 표준 편차가 모두 기록된 넘파이 배열입니다. 코드 6-32에서 계산한 값입니다. 온도의 인덱스는 1이기에 std[1]로 데이터를 가져올 수 있습니다.

6.3.4 기본적인 머신 러닝 방법

상식 수준의 기준점이 있으니 머신 러닝으로 보다 좋은 결과를 만들어봅시다.

RNN전에 소규모의 완전 연결 네트워크를 먼저 만들어 평가합니다. 데이터를 펼치고, 회귀 문제이므로 활성화 함수를 두지 않았습니다. 손실함수는 MAE입니다.

# 코드 6-37 완전 연결 모델을 훈련하고 평가하기

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=val_gen,
                              validation_steps=val_steps)

훈련 손실과 검증 손실 그래프를 그립니다.

# 코드 6-38 결과 그래프 그리기
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('Training and validation loss')
plt.legend()

plt.show()

11

일부 검증 손실은 학습을 사용하지 않은 기준점에 가깝지만 안정적이지는 못합니다. 이 문제는 기준점을 넘기 쉽지 않습니다. 이미 적용한 상식은 기계에게는 상식이 아닌 핵심 정보가 많이 포함되어 있기 때문입니다.

왜 상식 수준보다 훈련된 모델이 성능이 낮을까요? 그 이유는 훈련 과정이 찾는 것은 간단한 모델이 아니기 때문입니다. 현재 훈련으로 학습된 모델은 매개변수로 설정한 2개의 층을 가진 네트워크의 모든 가능한 가중치 조합입니다. 복잡한 모델 공간에서 해결책을 탐색할 때 간단한고 괜찮은 성능을 내는 모델은 찾지 못할 수 있는 것입니다. 이것이 머신 러닝이 가진 심각한 제약 사항입니다.

6.3.5 첫 번째 순환 신경망

첫 번째 완전 연결 네트워크는 실패입니다. 앞선 모델은 시계열 데이터를 펼쳐 시간 개념을 활용할 수 없었습니다. 이제 순서의 의미가 있는 시퀀스 데이터를 그대로 사용해보겠습니다.

이전 절에서 소개한 LSTM이 아닌 GRU 층을 사용합니다. GRU(Gated Recurrent Unit) 층은 LSTM과 같은 원리로 작동하지만 조금 간결하고, 계산 비용이 덜 듭니다. 대신 학습 능력이 비교적 부족할 수는 있습니다.

# 코드 6-39 GRU를 사용한 모델을 훈련하고 평가하기
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=val_gen,
                              validation_steps=val_steps)

여기서 부터 한 에포크당 200초이상 소모되어 저는 직접돌리는 것은 포기했습니다. 이에 대한 그래프는 다음과 같습니다.

12

완전 연결 네트워크에 비해 순환 네트워크가 훨씬 좋은 성능을 내는 것을 알 수 있습니다. 새로운 검증 MAE는 0.265이고 섭씨로 복원하면 2.35도 입니다. 기준보다 좋은 결과를 얻은 것을 알 수 있습니다.

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

훈련 손실과 검증 손실 곡선을 보면 모델이 과대적합인지 알 수 있습니다. 이를 개선하기 위해 드롭아웃을 사용합니다. 하지만 순환 신경망에 드롭아웃을 올바르게 적용하는 방법은 간단하지 않습니다. 순환 층 이전에 드롭아웃을 적용하면 규제에 도움되는 것보다 학습에 더 방해되는 것으로 알려져있습니다.

하지만 2015년 Yarin Gal의 베이지안 딥러닝에 관한 박사 논문에서 순환 신경망에서 드롭아웃을 사용하는 방법을 알아냈습니다. 타임스텝마다 랜덤하게 드롭아웃 마스크를 바꾸는 것이 아니라 동일한 드롭아웃 마스크를 모든 타임스텝에 적용해야합니다.

케라스에서는recurrent_dropout을 이용하여 순환 드롭아웃을 적용할 수 있습니다. 드롭아웃을 하게되면 완전히 수렴하는데 더 오래걸리므로 에포크 수를 2배로 늘립니다.

# 코드 6-40 드롭아웃 규제된 GRU를 사용한 모델을 훈련하고 평가하기
from keras.models import Sequential
from keras import layers
from keras.optimizers import RMSprop

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=val_gen,
                              validation_steps=val_steps)

그래프는 다음과 같습니다.

13

평가점수는 안정적이나 이전보다는 나아지지 않았습니다.

6.3.7 스태킹 순환 층

과대 적합은 더 이상 없지만 성능상 병목이 있는 것 같으므로 네트워크의 용량을 늘리는 방법을 택합니다. 네트워크 용량을 늘리려면 일반적으로 층에 있는 유닛의 수를 늘리거나 층을 더 많이 추가합니다.

케라스에서 순환 층을 쌓으려면 모든 중간층은 마지막 타임스텝 출력만 아니고 전체 시퀀스(3D)를 출력해야 합니다 return_sequences=True로 지정하면 됩니다.

# 코드 6-41 드롭아웃으로 규제하고 스태킹한 GRU 모델을 훈련하고 평가하기
from keras.models import Sequential
from keras import layers
from keras.optimizers import RMSprop

model = Sequential()
model.add(layers.GRU(32,
                     dropout=0.1,
                     recurrent_dropout=0.5,
                     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=val_gen,
                              validation_steps=val_steps)

결과는 다음과 같습니다.

14

조금 나아지긴 했지만 거의 향상된 점이 없습니다. 여기서 두 가지를 알 수 있습니다.

  • 충분히 과대적합을 만들지 못했기 때문에 검증 손실을 향상하기 위해서 층의 크기를 늘릴 수 있습니다. 하지만 시간이..
  • 층을 추가한 만큼 도움이 되지 않았습니다.(시간 약 2.5배) 여기서 네트워크 용량을 늘리는 것은 도움이 되지 않습니다.

6.3.8 양방향 RNN 사용하기

마지막은 양방향 RNN 입니다. 이는 RNN의 변종이며 특정 작업에서 RNN보다 좋은 성능을 냅니다. 자연어 처리에서 자주 사용됩니다.

RNN은 순서 또는 시간에 민감합니다. 타임스텝을 섞거나 거꾸로 하면 RNN이 시퀀스에서 학습하는 표현을 완전히 바꾸어버립니다. 양방향 RNN은 RNN이 순서에 민감하다는 성질을 사용합니다. GRU나 LSTM 같은 RNN을 2개 사용합니다. 각 RNN은 각 입력을 한 방향(시간 순서나 반대 순서)으로 처리한 후 각 표현을 합칩니다. 시퀀스를 양쪽 방향으로 처리하기 때문에 RNN이 놓치기 쉬운 패턴을 감지할 수 있습니다.

하지만 오래된 타임스텝이 먼저 나오도록 시퀀스를 처리하는 것은 근거 없는 결정입니다. 하지만 최근 타임스텝이 먼저 나오도록 입력 시퀀스를 처리하면 더 나은 결과를 만들까요?

결론부터 말하면 그렇지 않습니다. 기본적인 GRU층은 먼 과거보다 최근 내용을 더 잘 기억하기 때문입니다. 시간 순서대로 처리하는 네트워크가 거꾸로 처리하는 것보다 성능이 높아야만 합니다.

하지만 자연어 처리를 포함한 다양한 문제에서는 그렇지 않습니다. 문장을 이해하는데 단어의 중요성은 위치와 크게 상관도가 없기 때문입니다.

다음은 IMDB를 LSTM으로 거꾸로 훈련하고 평가하는 코드입니다.

# 코드 6-42 거꾸로 된 시퀀스를 사용한 LSTM을 훈련하고 평가하기
def reverse_order_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[:, ::-1, :], targets

train_gen_reverse = reverse_order_generator(
    float_data,
    lookback=lookback,
    delay=delay,
    min_index=0,
    max_index=200000,
    shuffle=True,
    step=step,
    batch_size=batch_size)
val_gen_reverse = reverse_order_generator(
    float_data,
    lookback=lookback,
    delay=delay,
    min_index=200001,
    max_index=300000,
    step=step,
    batch_size=batch_size)

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_reverse,
                              steps_per_epoch=500,
                              epochs=20,
                              validation_data=val_gen_reverse,                               validation_steps=val_steps)

시간 순서로 훈련한 LSTM과 거의 동일한 성능을 얻을 수 있습니다. 이는 언어를 이해하는 데 단어의 순서가 중요하지만 결정적이지 않다는 가정을 뒷받침합니다.

쨋든 양방향은 이렇게 각 방향에 대한 정보를 합쳐 더 나은 정보를 만든다는 점이 포인트입니다. 케라스에서는 Bidirectional층을 사용해 양방향 RNN을 만듭니다. 이 클래스는 첫 번째 매개 변수로 순환 층의 객체를 전달받습니다. 그 다음 전달받은 순환 층으로 새로운 두 번째 객체를 만듭니다.

# 코드 6-43 양방향 LSTM을 훈련하고 평가하기
model = Sequential()
model.add(layers.Embedding(max_features, 32))
model.add(layers.Bidirectional(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보다 성능이 조금 더 높습니다. 하지만 모델 파라미터가 많아 보다 빨리 과대적합됩니다. 이제 이를 이용해 원 문제인 온도 예측 문제에 적용합니다.

# 코드 6-44 양방향 GRU 훈련하기
from keras.models import Sequential
from keras import layers
from keras.optimizers import RMSprop

model = Sequential()
model.add(layers.Bidirectional(
    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=val_gen,
                              validation_steps=val_steps)

비슷한 수준의 결과를 내는 것을 알 수 있습니다. 왜냐면 여기서는 역방향 GRU가 좋은 성능을 내지 못하기 때문입니다.

6.3.9 더 나아가서

온도 예측 문제의 성능을 향상하기 위해 시도해 볼 수 있는 것은 많습니다.

  • 스태킹한 각 순환 층의 유닛 수 조정
  • RMSprop 옵티마이저가 사용한 학습률을 조정합니다.
  • GRU 대신 LSTM
  • 순환 층 위에 용량이 큰 완전 연결된 회귀 층을 사용
  • 최종적으로 최선의 모델을 테스트 세트에서 확인

6.3.10 정리

생략

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

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

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

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

이런 1D 합성곱 층은 시퀀스에 있는 지역 패턴을 인식할 수 있습니다. 동일한 변환이 시퀀스에 있는 모든 패치에 적용되기 때문에 특정 위치에서 학습한 패턴을 나중에 다른 위치에서 인식할 수 있습니다. 이는 1D 컨브넷에 시간의 이동에 대한 이동 불변성을 제공합니다. 문자 수준의 1D 컨브넷의 경우에는 단어 형태학에 관해 학습할 수 있습니다.

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

컨브넷에서 이미지 텐서의 크기를 다운 샘플링하기 위해 사용하는 평균 풀링이나 맥스 풀링 같은 2D 풀링 연산을 배웠습니다. 1D 풀링 연산은 2D 풀링 연산과 동일합니다. 입력에서 1D 패치를 추출하고 풀링을 통해 출력합니다. 결국에는 입력의 길이를 줄이기 위해 사용합니다.

6.4.3 1D 컨브넷 구현

케라스에서는 Conv1D를 이용하여 구현합니다. (samples, time, features) 크기의 3D 텐서를 입력받고 비슷한 형태의 3D 텐서를 반환합니다.

간단한 2개의 층으로 된 1D 컨브넷을 만들어 IMDB 감성 분류 문제에 적용해봅니다. 데이터 로드 및 전처리는 이전에 사용한 코드를 그대로 쓰면 됩니다.

# 코드 6-45 IMDB 데이터 전처리하기
from keras.datasets import imdb
from keras.preprocessing import sequence

max_features = 10000  # 특성으로 사용할 단어의 수
max_len = 500  # 사용할 텍스트의 길이(가장 빈번한 max_features 개의 단어만 사용합니다)

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=max_len)
x_test = sequence.pad_sequences(x_test, maxlen=max_len)
print('x_train 크기:', x_train.shape)
print('x_test 크기:', x_test.shape)

이제 2D 컨브넷과 유사하나 차이점이 있다면 윈도우 크기를 7 또는 9로 한다는 점입니다.

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=max_len))
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)

훈련 결과를 출력하면 다음과 같습니다.

15

16

LSTM보다 부족할 수 있지만 더 빠르게 실행됩니다. 특정 문제에 대해서는 경제적인 방법이 될 수 있다는 것입니다.

6.4.4 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)을 확인하면 다음과 같습니다.

17

0.40 부근에 머물러 있는 것을 알 수 있습니다. 컨브넷은 시간에 따른 중요성을 알 수 없기에 의미있는 결과를 만들기는 어렵습니다.

컨브넷의 속도와 경량함을 RNN의 순서 감지 능력과 결합하는 한 가지 전략은 1D 컨브넷을 RNN 이전의 전처리 단계로 사용하는 것입니다. 수천 개의 스텝을 가진 시퀀스 같이 RNN으로 처리하기에는 긴 시퀀스를 다룰 때 유용합니다.

이 기법이 연구 논문이나 실전 애플리케이션에 자주 등장하지는 않습니다. 아마도 널리 알려지지 않았기 때문일 것입니다.(책의 의견은 이러하지만 또 찾아보면 안좋은 점이 있지 않을까요?)

이 방법을 사용하면 훨씬 긴 시퀀스를 다룰 수 있으므로 더 오래전 데이터를 바라보거나, 시계열 데이터를 더 촘촘히 바라볼 수 있습니다.

# 코드 6-48 고밀도 데이터 제너레이터로 예나 데이터셋 준비하기

# 이전에는 6이었습니다(시간마다 1 포인트); 이제는 3 입니다(30분마다 1 포인트)
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)
val_gen = generator(float_data,
                    lookback=lookback,
                    delay=delay,
                    min_index=200001,
                    max_index=300000,
                    step=step)
test_gen = generator(float_data,
                     lookback=lookback,
                     delay=delay,
                     min_index=300001,
                     max_index=None,
                     step=step)
val_steps = (300000 - 200001 - lookback) // 128
test_steps = (len(float_data) - 300001 - lookback) // 128

이 모델은 2개의 Conv1D 층 다음에 GRU 층을 놓았습니다.

# 코드 6-49 1D 합성곱과 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)

다음은 이에 대한 훈련 손실과 검증 손실그래프 입니다.

18

검증 손실로 보면 규제가 있는 GRU 모델만큼 좋지는 않습니다. 하지만 훨씬 빠르기에 데이터를 많이 처리할 수 있고, 다른 문제에는 유용할 수 있습니다.

6.4.5 정리

생략

6.5 요약

생략

Leave a Comment