Study/MachineLearning

[Deep Learning with Python] 4-4. 합성곱 신경망을 사용한 시퀀스 처리

soohwan_justin 2021. 9. 12. 11:58

이번 포스트는 케라스 창시자에게 배우는 딥러닝(Deep Learning with Python)의 ch.6을 참고하였습니다.

 

이번 포스트를 읽기 전에, [Do it!] 시리즈를 먼저 읽는것을 권장합니다. 이 포스트에서는 [Do it!] 시리즈에 중복해서 나온 내용들을 많이 생략하였습니다.

 

이 책은 딥 러닝의 입문서로서는 적절하지 않다고 생각됩니다. 딥 러닝에 대한 어느정도 지식이 있고, 케라스의 사용방법에 대해 좀 더 자세히 알고 싶은 분께 권장합니다.

 

 

 

 

 

 

합성곱 신경망은 입력의 부분 패치에서 특성을 뽑아내어 구조적인 표현을 만들고, 데이터를 효율적으로 사용하기 때문에 컴퓨터 비전 문제에 잘 맞습니다. 이는 시퀀스 처리와도 깊게 관련되어 있으며, 시간을 2D이미지의 높이와 너비처럼 공간의 차원으로 다룰 수 있습니다. 1차원 합성곱은 시퀀스 처리 문제에서 RNN과 견줄 만하며, 계산 비용이 매우 싼 특징이 있습니다.

 

 

1차원 합성곱

 

1차원 합성곱은 시퀀스에 있는 지역 패턴을 인식할 수 있습니다. 특정 위치에서 인식한 패턴은 다른 위치에서도 인식할 수 있는데, 이는 1차원 합성곱 신경망에 (시간의 이동에 대한) 이동 불변성을 제공합니다. 예를 들어, 크기가 5인 윈도우를 사용하여 문자 시퀀스를 처리하는 1차원 합성곱 신경망은 5개 이하의 단어를 학습할 수 있습니다. 이 합성곱 신경망은 단어가 입력 시퀀스의 어느 문장에 있더라도 인식할 수 있습니다.

 

 

1차원 풀링

 

이미지에 사용하는 합성곱 신경망처럼, 다운샘플링을 위한 풀링은 1차원 합성곱 신경망에도 동일하게 적용되며, 1차원 입력의 길이를 줄이기 위해 사용합니다.

 

 

1차원 합성곱 네트워크 구현

 

케라스에서 1차원 합성곱 신경망은 Conv1D를 사용하여 구현합니다. Conv1D는 (samples, time, features)크기의 3D 텐서를 입력으로 받고, 비슷한 형태의 3D 텐서를 리턴합니다. 합성곱의 윈도우는 시간 축의 1차원 윈도우입니다. 즉, 입력 텐서의 두 번째 축입니다.

 

1차원 합성곱 신경망을 사용한 모델을 만들어 IMDB 감성 분류 문제에 적용해보겠습니다.

 

먼저, 데이터를 전처리합니다.

from keras.datasets import imdb
from keras.preprocessing import sequence
 
max_features = 10000
max_len = 500
 
print('loading data...')
(x_train, y_train), (x_test, y_test) = imdb.load_data(num_words=max_features)
print(len(x_train), 'training sequence')
print(len(x_test), 'test sequence')
 
print('sequence padding (samples x time)')
x_train = sequence.pad_sequences(x_train, maxlen=max_len)
x_test = sequence.pad_sequences(x_test, maxlen=max_len)
print('size of x_train : ', x_train.shape)
print('size of x_test : ', x_test.shape)

 

 

 

다음으로, 1차원 합성곱 네트워크를 구현합니다. 1차원 합성곱에 사용할 윈도우의 크기는 7입니다.

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)

 

 

그래프를 그려서 훈련 결과를 확인해봅니다.

%matplotlib inline
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()

 

약 83%의 검증 정확도로, LSTM보다 조금 성능이 낮지만 학습 속도가 훨씬 빠릅니다.

 

 

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

 

1D 컨브넷은 입력 패치를 독립적으로 처리하므로, RNN과 달리 합성곱 윈도우 크기를 넘어서는 타임스텝의 순서에 민감하지 않습니다. 장기간의 패턴을 인식하기 위해서는 많은 합성곱 풀링 층을 쌓을 수도 있습니다. 상위 층은 원본 입력에서 긴 범위를 볼 수 있지만, 이런 방법은 순서를 감지하기에는 부족합니다. 온도 예측 문제에 1차원 합성곱 신경망을 적용하여 확인해보겠습니다. 이 문제는 순서를 감지해야 좋은 예측값을 만들 수 있습니다.

 

이전에 사용했던 데이터 제너레이터를 사용합니다.

import os
import numpy as np
 
data_dir = './datasets/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:]
 
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
 
mean = float_data[:200000].mean(axis=0)
float_data -= mean
std = float_data[:200000].std(axis=0)
float_data /= std
 
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
 
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_steps = (300000 - 200001 - lookback) // batch_size
 
test_steps = (len(float_data) - 300001 - lookback) // batch_size
 
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가 약 0.45 정도로 보입니다. 상식 수준의 기준점이 0.335정도였던 것을 감안하면 별로 좋은 성능을 보이지 않습니다. 이는 합성곱 신경망이 입력 시계열에 있는 패턴을 보고, 이 패턴의 시간축 위치를 고려하지 않기 때문입니다. 최근의 데이터와 오래된 데이터는 각기 다르게 해석해야하는데, 이는 합성곱 신경망만 사용해서는 불가능합니다. IMDB 데이터 감성 분류 문제에서는 단어의 위치가 별로 중요하지 않으므로, 합성곱 신경망만 사용해도 괜찮은 결과를 보일 수 있었습니다.

 

합성곱 신경망의 속도를 RNN의 순서 감지 능력과 결합하는 방법은, 1차원 합성곱 신경망을 RNN 이전에 전처리 단계로 사용하는 것입니다. 이는 수천 개의 스텝을 가진 시퀀스 같이 RNN으로 처리하기에는 너무 긴 시퀀스를 다룰 때 특히 더 도움이 됩니다. 합성곱 신경망이 긴 입력 시퀀스를 더 짧은 고수준 특성의 시퀀스로 변환하고, 이 시퀀스는 RNN의 입력이 됩니다.

 

 

이 방법은 훨씬 긴 시퀀스를 다룰 수 있으므로 더 오래전의 데이터를 사용할 수 있고, 시계열 데이터를 더 촘촘히 사용할 수도 있습니다.

 

 

# 이전에는 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개의 합성곱층을 쌓고, 그 후 GRU 층을 놓은 모델을 만듭니다.

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.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보다 좋지는 않지만, 훈련 속도가 훨씬 빠른 것을 확인할 수 있습니다.