핸즈 온 머신러닝 (Hands-On Machine Learning with Scikit-Learn & TensorFlow) / 오렐리앙 제론 지음 , 박해선 옮김
을 읽고, 추후 기억을 되살릴 수 있게끔 나만의 방법으로 내용을 리뷰한다. 따라서 리뷰의 내용 별 비중이 주관적일 수 있다.
챕터 14. 순환 신경망
순환 신경망 (recurrent neural networks)은 미래를 예측할 수 있는 신경망이다. 주식가격 예측, 자율 주행 시스템에서 차의 이동경로 예측 등을 도운다. 입력은 임의 길이를 가진 sequence 일 수 있다. 또한 RNN의 창의성을 활용하여 멜로디 제작, 문장 생성, 이미지 캡션 생성 등을 할 수 있다.
14.1 순환 뉴런
지금까지는 신호가 입력층에서 출력층 한 방향으로만 흐르는 피드포워드 신경망을 살펴보았다면, RNN에서는 출력이 뒤쪽으로 향하여 다시 입력이 되는 경우가 있다. 가장 간단한 RNN은 본인의 출력을 본인의 입력으로 다시 사용하는 뉴런이다. 각 time step 혹은 frame 마다 recurrent neuron의 출력이 다시 입력이 되므로 시간을 축으로하여 펼칠 수 있다.
하나의 순환뉴런 층에 neuron 개의 뉴런이 있다면, 출력층 Y(t)의 크기는 (m, neuron). 입력층 X(t)의 크기는 (m, inputs)
가중치 W_x의 크기는 (inputs, neuron), 가중치 W_y의 크기는 (neuron, neuron).
Y(t) = f( X(t) W_x + Y(t-1) W_y + b)
14.1.1 메모리 셀
순환 뉴런의 출력은 이전 타임 스텝의 모든 입력에 대한 함수이기 떄문에 일종의 메모리 형태로 표현할 수 있다. 가장 기본적인 셀은 출력이 입력이 되는 것이지만, 더 복잡한 경우 다른 종류의 메모리가 등장한다.
14.1.2 입력과 출력 시퀀스
sequence-to-sequence : 주식가격 등의 시계열 데이터 예측 (1 ~ N일 입력 후 2 ~ N+1일 예측)
sequence-to-vector : 마지막을 제외한 모든 출력 무시. 영화 리뷰를 읽고 점수 출력
vector-to-sequence : 첫 번째 타임 스텝만 입력하고 나머지는 0 입력. 이미지에 대한 캡션 출력
delayed sequence-to-sequence : 인코더(s-to-v)와 디코더(v-to-s) 존재. 번역기에서 문장을 다 읽고 번역해야 하기 때문
14.2 텐서플로로 기본 RNN 구성하기
텐서플로의 RNN 연산을 사용하지 않고 RNN 모델을 구현하려면,
Y0 = tf.tanh(tf.matmul(X0, Wx) + b)
Y1 = tf.tanh(tf.matmul(Y0, Wy) + tf.matmul(X1, Wx) + b)
와 같이 구현할 수 있다. (placeholder 와 Variable 따로 정의) 하지만 100개의 타임 스텝에서 RNN을 위와 같은 방식으로 구현하면 매우 큰 그래프가 만들어지므로 RNN 연산을 사용해보자.
14.2.1 정적으로 타임 스텝 펼치기
셀을 생성하는 factory인 BasicRNNCell 객체를 만들고, static_rnn 함수를 호출하여 factory를 전달하면 각 스텝에서의 출력 텐서를 담고 있는 파이썬 리스트와 최종 상태를 담고 있는 텐서를 반환한다.
https://www.tensorflow.org/api_docs/python/tf/compat/v1/nn/rnn_cell/BasicRNNCell
https://www.tensorflow.org/api_docs/python/tf/compat/v1/nn/static_rnn
static_rnn은 타입 스텝마다 하나의 셀을 그래프에 추가하므로 타임 스텝이 많다면 그래프가 복잡해지고 메모리 에러가 발생할 가능성이 크다. 따라서 dynamic_rnn()을 쓰는 것이 좋다.
14.2.2 동적으로 타임 스텝 펼치기
dynamic_rnn() 함수는 타임 스텝마다 실제로 그래프를 만드는 것이 아니라 while_loop() 연산을 사용하여 matmul 연산을 생성한다. swap_memory = True로 설정하면 GPU 메모리 대신 CPU 메모리를 사용한다. 또한 static_rnn과 다르게 데이터 형태를 전처리 해 줄 필요가 없다.
https://www.tensorflow.org/api_docs/python/tf/compat/v1/nn/dynamic_rnn
14.2.3 가변 길이 입력 시퀀스 다루기
입력 시퀀스의 길이가 가변이면 dynamic_rnn() 함수를 호출할 때 sequence_lenth 매개변수를 설정하면 된다. 이 경우 입력시에는 제로패딩을 해주어야 하며 출력시에는 길이 초과 값에 대해서 0을 출력한다.
14.2.4 가변 길이 출력 시퀀스 다루기
번역된 문장의 경우 출력 길이를 알 수 없다. 이 때 EOS (end-of-sequence) 토큰을 출력하여 토큰 뒤에는 더 이상 출력이 없음을 알린다.
14.3 RNN 훈련하기
타임 스텝으로 네트워크를 펼치고 역전파를 사용하는 아이디어로 훈련한다. 이를 BPTT (backpropagation through time) 이라고 부른다.
14.3.1 시퀀스 분류기 훈련하기
MNIST 이미지를 RNN을 통해 훈련할 필요는 없지만, RNN을 통해 훈련하는 상황을 보자. 이미지의 각 행을 각 타임 스텝에 입력하고 마지막 출력에 10개 뉴런으로된 완전 연결 신경망을 연결하고 소프트맥스 층을 연결하는 방식으로 그래프를 생성할 수 있다. 그 자체로 좋은 결과가 나오지만 하이퍼파라미터 튜닝, He초기화, 규제 적용 등의 방법으로 결과를 개선할 수 있다.
14.3.2 시계열 예측을 위해 훈련하기
20 step의 시계열 데이터를 100개의 neuron을 통해 훈련한다면 출력이 20개의 타임 스텝에 대하여 길이 100의 벡터 형태로 주어진다. 실제로 원하는 것은 타임 스텝마다 하나의 출력이므로, OutputProjectionWrapper로 셀을 감싸서 100개의 출력에 선형 완전 연결층을 추가하여 하나의 출력이 되도록 훈련할 수 있다. 이후 비용함수와 Optimizer를 정의하여 훈련한다. 하지만 이는 아주 효율적이지는 않다. 타임 스텝마다 다른 완전 연결층을 훈련하기 때문이다. 따라서 RNN의 출력을 [batch_size, n_steps, n_neurons] 에서 [batch_size * n_steps, n_neurons]로 바꾸고 학습한 후 [batch_size * n_steps, n_outputs] 를 [batch_size, n_steps, n_outputs] 로 바꾸어 주면 step 마다 같은 모델을 학습하게 되어 효율적이다.
14.3.3 RNN의 창조성
초기 seed 시퀀스 (예를 들면 전부 0 혹은 예제 데이터)를 입력한 후, 예측값을 시퀀스의 끝에 추가하여 예측을 반복해 나가면 새로운 시퀀스를 창조할 수 있다. 하지만 더 좋은 예측을 위하여 많은 뉴런과 층으로 이루어진 심층 RNN이 필요할 것이다.
14.4 심층 RNN
셀을 여러 층으로 쌓는 것은 일반적이며, 이를 deep RNN 이라고 한다. 텐서플로에서 구현하기 위하여 셀을 MultiRNNCell 으로 쌓아올린 후 dynamic_rnn 등 함수에 전달하면 된다. MultiRNNCell을 만들 때 state_is_tuple = False로 지정하면 출력값이 하나의 텐서에 합쳐진다. 그렇지 않으면 여러 텐서를 튜플로 반환한다.
https://www.tensorflow.org/api_docs/python/tf/compat/v1/nn/rnn_cell/MultiRNNCell
14.4.1 여러 GPU에 심층 RNN 분산하기
BasicRNNCell을 서로 다른 device 블록에서 만드는 것은 아무 소용이 없는데, factory를 실행하는 dynamic_rnn() 함수가 호출되는 곳에 셀이 만들어지기 때문이다. 따라서 DeviceWrapper를 정의하고 이를 BasicRNNCell에 감싸서 factory들이 cell을 생성하는 위치를 지정해주어야한다.
https://www.tensorflow.org/api_docs/python/tf/compat/v1/nn/rnn_cell/DeviceWrapper?hl=th
14.4.2 드롭아웃 적용하기
BasicRNNCell을 만들 때에 DropoutWrapper를 적용하면 Dropout 층을 추가할 수 있다. 테스트할 때에는 확률을 1이 되도록 두어 Dropout을 꺼야한다. output_keep_prob이나 state_keep_prob을 사용해 출력이나 셀 상태에도 dropout을 적용할 수 있다.
14.4.3 많은 타임 스텝에서 훈련의 어려움
sequence가 길면 매우 깊은 RNN 네트워크가 된다. 따라서 그래디언트 소실과 폭주 문제를 가질 수 있으며 오랜 시간이 걸린다. 파라미터 초기화, 수렴하지 않는 호라성화 함수, 배치 정규화, 그래디언트 클리핑, 빠른 옵티마이저 등의 기법이 Deep RNN에 쓰일 수 있다. 제한된 타임 스텝만큼만 RNN을 펼치는 해결책도 있다. (이 경우 장기 학습이 필요하면 시퀀스에 과거에 대한 데이터를 압축해서 포함)
긴 시간 RNN의 다른 문제점은 초기 입력의 기억이 사라져간다는 사실이다. 따라서 장기 메모리를 가진 여러 종류의 셀이 연구되었다.
14.5 LSTM 셀
LSTM (long short-term memory) 셀은 BasicRNNCell 대신 BasicLSTMCell을 사용하면 된다.
https://www.tensorflow.org/api_docs/python/tf/compat/v1/nn/rnn_cell/BasicLSTMCell
우선 출력은 y(t)이고 메모리는 h(t), c(t)이다. h는 단기기억, c는 장기기억이라고 알면 된다.
c(t-1)은 삭제 게이트 f(t)와의 곱연산을 통해 기억을 잃고, 입력 게이트에서 선택한 기억과 합연산을 통해 기억을 추가하고 c(t)가 된다.
단기기억 h(t-1)과 입력값 x(t)로부터 삭제 게이트 f(t)를 학습하고, 입력게이트 g(t)와 i(t)를 학습하며, 출력게이트 o(t)를 학습한다. 이 때, f, i, o는 로지스틱 활성화함수를 거쳐 0에서 1사이의 값을 가지며 g는 기존의 메모리셀과 같이 tanh 함수를 사용한다. g에서 어떤 값을 학습할 지 정해주는 i와 곱연산을 통해 입력 게이트가 된다.
마지막으로 c(t)에 출력 게이트에서 o(t)와 곱연산을 수행하여 단기기억을 업데이트하고, 출력한다. 즉 h(t)와 y(t)가 된다.
14.5.1 핍홀 연결
LSTM에서 게이트들은 h(t-1)과 x(t)만을 보고 결정되는데, c(t-1)도 보고싶다면 peephole connection을 사용하면 된다. c(t-1)이 f(t), i(t)에 추가되며 c(t)가 o(t)에 입력으로 사용된다.
텐서플로에서 BasicLSTMCell 대신 LSTMCell을 사용하고 use_peepholes=True로 지정하면 된다.
14.6 GRU 셀
GRU 셀 (Gated Recurrent Unit)은 LSTM셀의 간소화된 버전이다. 상태 벡터가 h(t)로 합쳐지고 입력게이트가 z(t)로 합쳐졌다. 출력 게이트 대신 게이트 제어기 r(t)가 존재한다. r(t)는 기존 기억 중 어느 부분을 g(t) 학습에 사용할 지 정한다. 입력 x(t)와 h(t-1)로 부터 게이트 제어기 z(t), r(t)를 학습한다. g(t)의 입력은 입력값 x(t)와, h(t-1) * r(t)이다.
h(t-1)은 z(t)와 곱연산하여 기억을 잃고, (1-z(t))와 곱연산 된 g(t)를 새로 기억하여 h(t) 및 y(t)가 된다. 즉 z(t)가 1에 가까우면 기억이 유지되고 0에 가까우면 g(t)를 학습한다.
LSTM셀과 GRU셀 덕분에 자연어 처리 분야가 성공하였다.
14.7 자연어 처리
Word2Vec과 Seq2Seq 튜토리얼에 자연어 처리에 대한 내용이 잘 정리되어있다.
14.7.1 워드 임베딩
물과 우유는 가까운 단어지만 물과 신발은 비교적 먼 단어이다. 단어 각각을 벡터에 표시하면 희박하며 효율적이지 않으므로 단어를 더 작은 차원에 축소되고 밀집된 벡터로 표현하는 것을 embedding 이라고 한다. embedding 초기값은 랜덤이지만 신경망이 embedding 벡터를 학습하게 된다. embedding 결과는 각 단어를 남성/여성, 단수/복수, 형용사/명사 등으로 나열하며 결과물이 매우 놀랍다. embedding_lookup() 함수를 사용하여 벡터를 임베딩화 할 수 있다. word embedding은 다른 NLP 모델에서 잘 학습된 결과를 가져와도 효율적으로 재사용할 수 있다.
14.7.2 기계 번역을 위한 인코더-디코더 네트워크
문장을 번역하는 모델을 살펴보자. 이 모델은 인코더와 디코더로 구성되어있다. 입력 문장은 단어의 순서를 역으로 입력하는 것이 더 좋다. 따라서 인코더에는 단어가 역순으로 임베딩되어 입력된다. 디코더의 입력값은 번역 된 문장이며, 가장 첫 입력은 비어져 있다. 또한 시퀀스의 끝에 EOS를 나타내는 토큰이 있다. 각 스텝마다 디코더는 어휘의 점수를 출력한다. 그러므로 softmax를 적용하여 확률로 변환하며, softmax_cross_entropy_with_logits() 함수로 훈련할 수 있다.
inference 할 때에는 디코더에 주입할 타겟 문장이 없으므로 이전 스텝에서 출력한 단어를 디코더에 임베딩 조회 후 입력한다.
- 인코더와 디코더로 입력되는 시퀀스의 길이를 가변으로 설정하려면 앞에서 언급한 sequence_length 매개변수를 사용하거나, 비슷한 길이의 문장을 bucket에 모아서 padding 하는 방법이 있다. (예를 들어, 6개 단어 단위로 bucket을 만든 뒤 6의 배수가 될 때 까지 padding)
- 어휘 목록이 클 때에 모든 어휘에 대해 확률을 출력하는 것이 느리므로, 작은 벡터를 출력하도록 하고 sampled softmax 기법을 사용할 수 있다.
- 디코더가 입력 시퀀스를 들여다보도록 하는 attention mechanism 이라는 것이 있다.
- 워드 임베딩을 처리해서 인코더-디코더 모델을 만들어주는 embedding_rnn_seq2seq() 함수가 있다.
'개발 인생 > ML' 카테고리의 다른 글
핸즈 온 머신러닝 :: 16. 강화 학습 (0) | 2020.02.02 |
---|---|
핸즈 온 머신러닝 :: 15. 오토인코더 (0) | 2020.01.31 |
핸즈 온 머신러닝 :: 13. 합성곱 신경망 (0) | 2020.01.27 |
핸즈 온 머신러닝 :: 12. 다중 머신과 장치를 위한 분산 텐서플로 (0) | 2020.01.24 |
핸즈 온 머신러닝 :: 11. 심층 신경망 훈련 (0) | 2020.01.22 |