CS231n Lecture 7 - Recurrent Neural Networks

LECTURE 글 목록
목차

핵심 한 줄 정리

RNN은 이전 hidden state와 현재 입력을 같은 가중치로 반복 처리해 가변 길이 sequence를 모델링하며, BPTT로 시간축을 따라 학습하지만 순차 계산과 vanishing gradient라는 한계가 있어 LSTM과 현대적인 state space model로 발전했다.

반드시 기억할 개념

Sequence modeling의 형태

일반적인 image classifier는 하나의 고정된 크기 입력을 받아 하나의 고정된 크기 출력을 만든다. Sequence modeling에서는 입력이나 출력이 가변 길이 sequence가 될 수 있다.

형태입력과 출력예시
one-to-one고정 입력 하나, 고정 출력 하나image classification
one-to-many고정 입력 하나, 가변 길이 출력image captioning
many-to-one가변 길이 입력, 출력 하나여러 video frame을 이용한 video classification
many-to-many가변 길이 입력과 가변 길이 출력video captioning
synchronized many-to-many각 입력마다 출력 하나frame마다 수행하는 video classification

Many-to-many에서는 입력 sequence와 출력 sequence의 길이가 반드시 같을 필요는 없다. RNN의 기본 구조는 각 time step의 입력마다 hidden state를 갱신하고 출력을 만드는 synchronized many-to-many 형태에서 가장 쉽게 볼 수 있으며, 입출력 위치를 바꾸면 다른 형태에도 적용할 수 있다.

RNN의 hidden state

RNN은 이전까지 처리한 정보를 hidden state에 저장한다. 현재 hidden state hth_t는 현재 입력 xtx_t와 직전 hidden state ht1h_{t-1}로 계산한다.

ht=fW(ht1,xt)h_t = f_W(h_{t-1},x_t)

여기서 tt는 time step이고 fWf_W는 parameter WW를 사용하는 state update function이다. 중요한 점은 time step마다 새로운 WW를 사용하는 것이 아니라 sequence 전체에서 같은 WW를 공유한다는 것이다.

RNN을 시간축으로 펼치면 다음과 같은 unrolled computational graph가 된다.

h0h1h2hTh_0 \rightarrow h_1 \rightarrow h_2 \rightarrow \cdots \rightarrow h_T

hth_t에는 해당 time step의 xtx_t도 함께 들어온다. 따라서 hth_t를 계산하려면 ht1h_{t-1}이 먼저 필요하다.

출력 yty_t는 hidden state를 task의 output dimension으로 바꾸는 별도의 함수로 계산한다.

yt=fy(Whyht)y_t = f_y(W_{hy}h_t)

WhyW_{hy}는 hidden state를 실제 출력으로 변환하는 학습 가능한 weight이다. 입력 차원을 DD, hidden dimension을 HH, 출력 차원을 CC라고 하면 shape는 다음과 같다.

xtRD,htRH,ytRCx_t \in \mathbb{R}^{D}, \qquad h_t \in \mathbb{R}^{H}, \qquad y_t \in \mathbb{R}^{C} WxhRH×D,WhhRH×H,WhyRC×HW_{xh} \in \mathbb{R}^{H \times D}, \qquad W_{hh} \in \mathbb{R}^{H \times H}, \qquad W_{hy} \in \mathbb{R}^{C \times H}

h0h_0는 sequence 처리를 시작하기 위한 initial hidden state이다. Zero vector를 사용할 수도 있고, 특정 값으로 직접 정하거나 학습 가능한 vector로 둘 수도 있다.

Vanilla RNN

Vanilla RNN은 보통 다음 식으로 hidden state를 갱신한다.

ht=tanh(Whhht1+Wxhxt+bh)h_t = \tanh(W_{hh}h_{t-1}+W_{xh}x_t+b_h)

WxhW_{xh}는 현재 입력을 hidden dimension으로 옮기고, WhhW_{hh}는 이전 hidden state를 현재 hidden state로 변환한다. 두 결과를 더한 뒤 activation function을 적용한다.

두 weight를 하나로 묶어 쓰기도 한다.

ht=tanh(W[ht1xt]+bh)h_t = \tanh \left( W \begin{bmatrix} h_{t-1} \\ x_t \end{bmatrix} +b_h \right) W=[WhhWxh]W = \begin{bmatrix} W_{hh} & W_{xh} \end{bmatrix}

가장 단순한 output layer는 다음과 같다.

yt=Whyht+byy_t = W_{hy}h_t+b_y

Vanilla RNN에서 tanh\tanh를 자주 사용하는 이유는 출력 범위가 1-111 사이이고 zero-centered이기 때문이다.

1<tanh(z)<1-1 < \tanh(z) < 1

같은 변환을 반복해도 activation이 무한히 커지는 것을 어느 정도 막을 수 있다. 다만 tanh\tanh의 derivative는 대부분 11보다 작으므로 긴 시간축을 역전파할 때 vanishing gradient의 원인이 된다.

%% title: Vanilla RNN을 시간축으로 펼친 구조
%% caption: 각 time step은 같은 parameter W를 공유하며, 이전 hidden state와 현재 입력으로 다음 hidden state를 만든다.
flowchart LR
    h0["h₀"] --> cell1["RNN Cell<br/>shared W"]
    x1["x₁"] --> cell1
    cell1 --> h1["h₁"]
    h1 --> cell2["RNN Cell<br/>shared W"]
    x2["x₂"] --> cell2
    cell2 --> h2["h₂"]
    h2 --> cell3["RNN Cell<br/>shared W"]
    x3["x₃"] --> cell3
    cell3 --> h3["h₃"]
    h1 --> y1["y₁"]
    h2 --> y2["y₂"]
    h3 --> y3["y₃"]

예시: 연속된 두 개의 1 찾기

RNN의 행렬 계산을 직접 보기 위해 다음과 같은 synchronized many-to-many 문제를 생각할 수 있다.

  • 입력은 0011로 이루어진 sequence이다.
  • 현재 값과 직전 값이 모두 11이면 11을 출력한다.
  • 그 외에는 00을 출력한다.
  • 계산을 단순하게 보기 위해 tanh\tanh 대신 ReLU를 사용한다.

입력 sequence를 다음과 같이 두자.

(x1,x2,x3,x4,x5,x6)=(0,1,0,1,1,1)(x_1,x_2,x_3,x_4,x_5,x_6) = (0,1,0,1,1,1)

원하는 출력은 다음과 같다.

(y1,y2,y3,y4,y5,y6)=(0,0,0,0,1,1)(y_1,y_2,y_3,y_4,y_5,y_6) = (0,0,0,0,1,1)

출력을 결정하려면 현재 입력과 직전 입력을 알아야 한다. 계산을 간단하게 만들기 위해 항상 11인 항까지 붙인 3차원 hidden state를 사용한다.

ht=[xtxt11]h_t = \begin{bmatrix} x_t \\ x_{t-1} \\ 1 \end{bmatrix}

초기 상태는 앞에서 두 개의 00을 본 것으로 간주해 다음처럼 둔다.

h0=[001]h_0 = \begin{bmatrix} 0 \\ 0 \\ 1 \end{bmatrix}

Hidden state는 다음 recurrence로 계산한다.

ht=ReLU(Whhht1+Wxhxt)h_t = \operatorname{ReLU} \left( W_{hh}h_{t-1}+W_{xh}x_t \right)

현재 입력을 hidden state의 첫 번째 성분에 넣는 WxhW_{xh}3×13 \times 1 column vector이다.

Wxh=[100]W_{xh} = \begin{bmatrix} 1 \\ 0 \\ 0 \end{bmatrix}

이전 hidden state의 첫 번째 성분을 두 번째 성분으로 옮기고 마지막 상수 11을 유지하는 WhhW_{hh}는 다음과 같다.

Whh=[000100001]W_{hh} = \begin{bmatrix} 0 & 0 & 0 \\ 1 & 0 & 0 \\ 0 & 0 & 1 \end{bmatrix}

따라서 두 항을 더하면 원하는 hidden state가 만들어진다.

Whhht1+Wxhxt=[xtxt11]W_{hh}h_{t-1}+W_{xh}x_t = \begin{bmatrix} x_t \\ x_{t-1} \\ 1 \end{bmatrix}

현재 값과 직전 값을 더하고 11을 빼면 두 값이 모두 11일 때만 양수가 된다. 이를 위한 output weight는 다음과 같다.

Why=[111]W_{hy} = \begin{bmatrix} 1 & 1 & -1 \end{bmatrix} yt=ReLU(Whyht)=ReLU(xt+xt11)y_t = \operatorname{ReLU}(W_{hy}h_t) = \operatorname{ReLU}(x_t+x_{t-1}-1)
xt1x_{t-1}xtx_txt+xt11x_t+x_{t-1}-1yty_t
00-10
0100
1000
1111

전체 sequence의 hidden state와 출력은 다음과 같이 변한다.

ttxtx_tht=[xt,xt1,1]Th_t=[x_t,x_{t-1},1]^Tyty_t
10[0,0,1]T[0,0,1]^T0
21[1,0,1]T[1,0,1]^T0
30[0,1,1]T[0,1,1]^T0
41[1,0,1]T[1,0,1]^T0
51[1,1,1]T[1,1,1]^T1
61[1,1,1]T[1,1,1]^T1

이 예시에서는 hidden state의 의미와 weight를 손으로 정했지만, 실제 RNN에서는 더 큰 hidden state를 사용하고 WxhW_{xh}, WhhW_{hh}, WhyW_{hy}를 gradient descent로 학습한다.

Backpropagation Through Time

RNN을 unroll하면 하나의 weight가 시간축의 여러 위치에서 반복 사용되는 computational graph가 된다. 각 time step에 loss가 있는 many-to-many task라면 전체 loss는 다음과 같다.

L=t=1TLtL = \sum_{t=1}^{T}L_t

같은 WW가 모든 time step에서 사용되므로 각 loss에서 shared weight로 흐르는 gradient도 모두 합쳐야 한다.

LW=t=1TLtW\frac{\partial L}{\partial W} = \sum_{t=1}^{T} \frac{\partial L_t}{\partial W}

Computational graph에서는 time step마다 별도 weight가 있는 것처럼 local gradient를 계산할 수 있지만, 실제로 모두 같은 parameter를 가리키므로 최종 gradient는 하나로 누적된다. 이렇게 시간축으로 펼친 graph를 따라 수행하는 역전파가 Backpropagation Through Time, BPTT이다.

Many-to-one task에서는 마지막 hidden state만 이용해 하나의 loss를 만들 수 있다. Video 전체에 정보가 퍼져 있다면 모든 hidden state를 average pooling이나 max pooling한 뒤 하나의 출력을 만드는 방법도 있다.

One-to-many task에서는 첫 입력 이후 xtx_t 자리에 zero vector를 넣거나, 직전에 생성한 yt1y_{t-1}을 다음 입력으로 다시 넣을 수 있다. Autoregressive language model과 image captioning에서는 이전 output을 다음 입력으로 사용하는 방식이 흔하다.

Truncated BPTT

Sequence가 길어지면 forward activation과 gradient를 모든 time step에 대해 memory에 보관해야 한다. 이를 줄이기 위해 sequence를 고정 길이 window로 나누는 truncated BPTT를 사용한다.

첫 번째 chunk의 마지막 hidden state 값은 다음 chunk의 initial hidden state로 넘긴다.

h0(2)=hK(1)h_0^{(2)} = h_K^{(1)}

다만 chunk 경계에서 computational graph는 detach한다.

h0(2)=stop_gradient(hK(1))h_0^{(2)} = \operatorname{stop\_gradient}(h_K^{(1)})

Hidden state의 값은 이어지지만 다음 chunk의 gradient가 이전 chunk 전체로 역전파되지는 않는다. 이 방식은 memory를 크게 줄이는 대신 window보다 먼 dependency를 직접 학습하기 어려워진다. 전체 graph가 memory에 들어간다면 full BPTT가 더 정확하지만, 긴 sequence를 처리하려면 truncated BPTT가 필요한 경우가 많다.

%% title: Truncated BPTT의 Chunk 경계
%% caption: Hidden state는 다음 chunk로 전달하지만, 역전파 그래프는 경계에서 detach해 gradient가 이전 chunk로 흐르지 않게 한다.
flowchart LR
    h0["h₀"] --> w1["Chunk 1<br/>t = 1 … K"]
    w1 --> hk["hₖ"]
    hk --> detach1["detach"]
    detach1 --> w2["Chunk 2<br/>t = K+1 … 2K"]
    w2 --> h2k["h₂ₖ"]
    h2k --> detach2["detach"]
    detach2 --> w3["Chunk 3<br/>t = 2K+1 … 3K"]
    w1 -. "gradient" .-> h0
    w2 -. "gradient" .-> detach1
    w3 -. "gradient" .-> detach2

Character-level language model

Character-level language model은 현재까지의 문자를 보고 다음 문자를 예측한다. 입력 문자는 one-hot vector로 표현할 수 있다.

xt=one_hot(ct)x_t = \operatorname{one\_hot}(c_t)

RNN은 xtx_tht1h_{t-1}hth_t를 만들고, output layer에서 다음 문자에 대한 logit과 probability를 계산한다.

st=Whyht+bys_t = W_{hy}h_t+b_y pt=softmax(st)p_t = \operatorname{softmax}(s_t)

학습 target은 현재 문자 다음에 실제로 등장한 문자 ct+1c_{t+1}이다. 각 time step의 cross-entropy loss를 더하면 된다.

L=t=1Tlogpt(ct+1)L = -\sum_{t=1}^{T} \log p_t(c_{t+1})

한 time step에서 정답 문자의 logit이 2.22.2인데 다른 문자의 logit이 4.14.1이라면 model은 잘못된 문자에 더 높은 확률을 준다. 이 경우 정답 확률이 낮아 해당 time step의 loss가 커진다.

Embedding layer

실제 구현에서는 one-hot vector를 그대로 넣기보다 embedding matrix에서 해당 문자의 row를 가져온다.

ERV×dE \in \mathbb{R}^{V \times d} et=E[ct]e_t = E[c_t]

VV는 vocabulary에 있는 문자 또는 token 수이고 dd는 embedding dimension이다. One-hot vector와의 matrix multiplication으로도 같은 연산을 표현할 수 있다.

et=xtTEe_t = x_t^T E

입력 종류와 embedding dimension을 모두 DD로 잡으면 EED×DD \times D가 되지만, 일반적으로 두 값이 같을 필요는 없다. Embedding은 discrete index를 dense vector로 바꾸며 다른 weight와 함께 학습된다.

Autoregressive decoding

Test time에는 이전에 생성한 문자를 다음 time step의 입력으로 다시 넣는다.

c^t+1pt\hat{c}_{t+1} \sim p_t

매번 가장 확률이 높은 문자를 고르는 방법은 greedy decoding이다.

c^t+1=argmaxcpt(c)\hat{c}_{t+1} = \arg\max_c p_t(c)

Greedy decoding은 같은 initial input과 model이면 항상 같은 sequence를 만든다. 더 다양한 결과를 만들려면 softmax distribution에 따라 sampling할 수 있다. 예를 들어 두 문자의 확률이 각각 0.840.840.130.13이면 그 비율을 반영해 다음 문자를 선택한다.

여러 candidate sequence를 유지하며 이후 probability까지 비교하는 beam search도 있다. Model이 probability를 계산하는 것과 그 distribution에서 실제 sequence를 선택하는 것은 서로 다른 단계이다.

Character-level RNN은 매우 작은 구현으로도 Shakespeare의 sonnet이나 Linux source code의 pattern을 학습할 수 있다. Training 초반에는 의미 없는 문자열을 만들지만 점차 word 형태, 문체, 괄호, indentation, keyword 같은 구조를 생성한다. 현대 language model은 character 대신 여러 character를 묶은 token을 주로 사용하지만, 이전 token으로 다음 token을 예측하는 autoregressive objective는 같다.

Hidden state의 해석

RNN의 hidden state 각 성분을 문자 위치와 함께 시각화하면 일부 cell이 무엇을 추적하는지 볼 수 있다. tanh\tanh activation을 사용하는 경우 값은 1-1부터 11 사이이며, activation 변화를 문자마다 색으로 표시할 수 있다.

학습된 character-level RNN에서는 다음과 같은 cell이 관찰되었다.

  • quote가 시작되면 켜지고 닫는 quote가 나오면 꺼지는 cell
  • line 길이와 newline에 가까워지는 정도를 추적하는 cell
  • 현재 위치가 if statement 안인지 추적하는 cell
  • comment의 시작과 끝을 추적하는 cell
  • code의 indentation과 nesting depth를 추적하는 cell

모든 hidden unit이 이렇게 명확하게 해석되는 것은 아니다. 그래도 RNN이 필요한 과거 정보를 hidden state의 특정 방향에 저장할 수 있음을 보여주는 예이다.

Multi-layer RNN

RNN도 여러 layer로 쌓을 수 있다. Layer ll의 hidden state는 같은 layer의 이전 time step과 아래 layer의 현재 time step 출력에 의존한다.

ht(l)=f(l)(ht1(l),ht(l1))h_t^{(l)} = f^{(l)} \left( h_{t-1}^{(l)}, h_t^{(l-1)} \right)

첫 번째 layer에서는 실제 입력을 사용한다.

ht(0)=xth_t^{(0)}=x_t

같은 layer 안에서는 time step마다 weight를 공유하지만 서로 다른 layer는 각각 별도의 weight를 갖는다. 시간축과 depth축을 함께 펼치면 grid 형태의 computational graph가 되며, 오른쪽 위의 state를 계산하려면 왼쪽과 아래쪽 state가 먼저 계산되어야 한다.

Vanishing gradient와 exploding gradient

Vanilla RNN의 pre-activation을 다음과 같이 두자.

at=Whhht1+Wxhxt+bha_t = W_{hh}h_{t-1}+W_{xh}x_t+b_h ht=tanh(at)h_t = \tanh(a_t)

한 time step의 hidden state Jacobian은 다음과 같다.

htht1=diag(1tanh2(at))Whh\frac{\partial h_t}{\partial h_{t-1}} = \operatorname{diag} \left( 1-\tanh^2(a_t) \right) W_{hh}

먼 과거의 hidden state로 gradient를 보내려면 이 Jacobian을 반복해서 곱해야 한다.

hTht=k=t+1Thkhk1\frac{\partial h_T}{\partial h_t} = \prod_{k=t+1}^{T} \frac{\partial h_k}{\partial h_{k-1}}

tanh\tanh의 derivative는 최대가 11이고 대부분의 영역에서 11보다 작다.

tanh(z)=1tanh2(z)\tanh'(z) = 1-\tanh^2(z) 0<tanh(z)10 < \tanh'(z) \le 1

11보다 작은 값이 시간축을 따라 반복해서 곱해지면 gradient가 0에 가까워진다. 여기에 WhhW_{hh}도 매번 곱해진다. WhhW_{hh}의 큰 singular value가 11보다 훨씬 크면 특정 방향의 vector가 계속 커져 exploding gradient가 생길 수 있고, singular value가 작으면 계속 줄어들어 vanishing gradient가 심해진다.

Exploding gradient는 gradient clipping으로 완화할 수 있다.

ggmin(1,τg)g \leftarrow g\cdot \min\left(1,\frac{\tau}{\lVert g\rVert}\right)

τ\tau는 허용할 gradient norm의 threshold이다. Gradient가 너무 크면 크기만 줄이고 방향은 유지한다. 반면 이미 거의 0이 된 gradient에서 장기 dependency 정보를 복구하는 것은 어렵기 때문에 vanishing gradient가 더 근본적인 문제이다.

LSTM

LSTM은 Vanilla RNN의 vanishing gradient와 장기 정보 손실을 줄이기 위한 recurrent architecture이다. Hidden state hth_t 외에 cell state ctc_t를 두고, gate를 통해 어떤 정보를 저장하고 잊고 출력할지 정한다.

입력 xtx_t와 이전 hidden state ht1h_{t-1}을 붙인 vector를 다음과 같이 둔다.

zt=[ht1xt]z_t = \begin{bmatrix} h_{t-1} \\ x_t \end{bmatrix}

각 gate와 candidate value는 다음과 같다.

it=σ(Wizt+bi)i_t=\sigma(W_i z_t+b_i) ft=σ(Wfzt+bf)f_t=\sigma(W_f z_t+b_f) ot=σ(Wozt+bo)o_t=\sigma(W_o z_t+b_o) gt=tanh(Wgzt+bg)g_t=\tanh(W_g z_t+b_g)

iti_t는 input gate, ftf_t는 forget gate, oto_t는 output gate이고 gtg_t는 새로 기록할 candidate value이다.

ct=ftct1+itgtc_t = f_t\odot c_{t-1} + i_t\odot g_t ht=ottanh(ct)h_t = o_t\odot\tanh(c_t)

Cell state는 매 time step마다 반드시 새로운 tanh\tanh 변환을 거치지 않고 additive update로 이어진다. Forget gate가 정보를 완전히 지우지만 않는다면 gradient와 장기 정보를 Vanilla RNN보다 쉽게 전달할 수 있다.

LSTM이 vanishing gradient를 완전히 없애는 것은 아니다. 다만 필요한 정보를 얼마나 유지하고 버릴지 gate가 학습하고, 긴 dependency를 위한 비교적 직접적인 경로를 제공하므로 Vanilla RNN보다 안정적으로 학습된다.

이 구조는 ResNet의 skip connection과 비슷한 직관을 가진다. ResNet은 depth 방향으로 layer를 건너뛰는 경로를 만들고, LSTM은 time 방향으로 정보를 전달하는 cell state 경로를 만든다.

%% title: LSTM Cell의 Gate와 State 흐름
%% caption: Cell state cₜ는 forget gate로 유지할 정보를 결정하고 input gate로 새 정보를 더한다. Output gate는 현재 hidden state로 노출할 부분을 결정한다.
flowchart LR
    z["zₜ = [hₜ₋₁, xₜ]"] --> fg["Forget gate<br/>fₜ"]
    z --> ig["Input gate<br/>iₜ"]
    z --> cand["Candidate<br/>gₜ"]
    z --> og["Output gate<br/>oₜ"]
    cprev["cₜ₋₁"] --> keep["⊙"]
    fg --> keep
    ig --> write["⊙"]
    cand --> write
    keep --> add(("+"))
    write --> add
    add --> ct["cₜ"]
    ct --> tanh["tanh"]
    tanh --> expose["⊙"]
    og --> expose
    expose --> ht["hₜ"]

RNN의 장점과 한계

RNN의 장점은 다음과 같다.

  • architecture 자체에는 고정된 maximum context length가 없다.
  • 과거 정보가 hidden state에 남아 있다면 오래전 time step의 정보를 사용할 수 있다.
  • sequence가 길어져도 parameter 수는 증가하지 않는다.
  • 모든 time step에서 같은 weight와 update rule을 사용한다.
  • recurrent update의 계산량은 sequence length에 대해 선형으로 증가한다.

반면 다음 한계가 있다.

  • hth_t를 계산하려면 ht1h_{t-1}이 필요하므로 time step을 병렬 계산하기 어렵다.
  • 긴 sequence를 학습하면 activation 저장에 필요한 memory와 계산 시간이 커진다.
  • fixed-size hidden state에 과거 정보를 계속 압축하므로 sequence가 길어질수록 정보가 사라질 수 있다.
  • 긴 시간축을 따라 backpropagation하면 gradient가 vanish하거나 explode할 수 있다.

RNN에 명시적인 context window가 없다는 것이 모든 과거 정보를 완벽히 유지한다는 뜻은 아니다. 임의 길이 sequence를 입력할 수 있는 것과 아주 먼 dependency를 정확히 기억하는 것은 다른 문제이다.

Computer vision에서의 활용

Image captioning

Image captioning은 image 하나에서 가변 길이 word sequence를 만드는 one-to-many task이다. 기본 구조는 visual encoder와 recurrent decoder의 결합이다.

ICNNvRNN(w1,w2,,wT)I \xrightarrow{\text{CNN}} v \xrightarrow{\text{RNN}} (w_1,w_2,\ldots,w_T)

ImageNet으로 pretrain한 CNN의 class label 대신 second-to-last layer의 feature vv를 사용한다. 이 visual feature를 별도의 WihW_{ih}로 hidden state에 넣고, 이후에는 이전에 생성한 text와 hidden state로 다음 word를 생성한다.

Sequence는 start token에서 시작하고 end token이 sampling되면 끝난다. 당시 모델은 그럴듯한 caption을 만들었지만 training data의 co-occurrence에 의존한 오류도 보였다.

  • 손을 오므려 물체를 든 모습 때문에 실제 phone을 mouse라고 판단했다.
  • fur 소재 옷을 보고 cat을 들고 있다고 판단했다.
  • beach가 보인다는 이유로 실제로 없는 surfboard를 생성했다.
  • 공이 glove로 들어가는 장면을 catching이 아니라 throwing이라고 설명했다.

이런 오류는 특정 scene과 object 또는 action이 dataset에서 자주 함께 등장하기 때문에 생긴다. 현재 vision-language model에서도 비슷한 hallucination이 나타날 수 있다.

Visual question answering과 navigation

Visual question answering은 image와 question을 보고 answer를 만드는 task이다. Answer를 text sequence로 생성한다면 전체 answer probability는 다음과 같다.

P(a1,,aTI,q)=t=1TP(ata<t,I,q)P(a_1,\ldots,a_T\mid I,q) = \prod_{t=1}^{T} P(a_t\mid a_{<t},I,q)

여러 answer candidate 중 하나를 선택한다면 candidate를 class로 보는 classifier로 구성할 수도 있다. Answer가 네 개라면 answer 1부터 answer 4까지를 출력하는 four-way classification problem이 된다.

Visual dialogue는 image에 관한 여러 turn의 대화를 sequence로 처리한다. Visual navigation에서는 연속된 image observation을 입력받아 2차원 floor plan에서 목적지로 이동하기 위한 direction sequence를 출력한다.

RNN과 state space model

Transformer의 self-attention은 모든 token pair를 비교하므로 기본적으로 sequence length TT에 대해 quadratic compute를 요구한다.

O(T2)O(T^2)

Recurrent model은 같은 state update를 time step마다 한 번씩 수행하므로 sequence length에 대해 linear하게 계산량이 증가한다.

O(T)O(T)

RWKV와 Mamba 같은 현대 model은 recurrent model 또는 state space model의 linear scaling을 활용하면서 Transformer에 가까운 modeling 성능을 얻는 것을 목표로 한다.

RNN의 핵심인 state update는 사라진 개념이 아니다. Vanilla RNN의 정보 압축과 학습 불안정 문제를 개선한 형태로 긴 context를 효율적으로 처리하는 현대 sequence model에 다시 사용되고 있다.

과제에서 확인할 것

RNN forward pass의 shape

각 time step에서 다음 shape가 맞는지 확인한다.

WxhxtRHW_{xh}x_t \in \mathbb{R}^{H} Whhht1RHW_{hh}h_{t-1} \in \mathbb{R}^{H} WhyhtRCW_{hy}h_t \in \mathbb{R}^{C}

모든 time step에서 같은 WxhW_{xh}, WhhW_{hh}, WhyW_{hy}를 공유하는지도 확인해야 한다.

BPTT의 gradient 누적

Backward pass에서는 time step마다 shared weight에 대한 gradient가 생긴다. 이를 덮어쓰지 않고 모두 더해야 한다.

dW=t=1TdWtdW = \sum_{t=1}^{T}dW_t

Numerical gradient check로 dxdx, dh0dh_0, dWxhdW_{xh}, dWhhdW_{hh}, dWhydW_{hy}가 올바른지 확인하는 것이 좋다.

Sequence target과 loss

Character-level language model에서는 입력 sequence와 target sequence가 한 칸 어긋나 있어야 한다.

(c1,c2,,cT)(c2,c3,,cT+1)(c_1,c_2,\ldots,c_T) \rightarrow (c_2,c_3,\ldots,c_{T+1})

각 time step의 softmax loss를 합하거나 평균 내어 sequence loss를 만드는지 확인한다.

Embedding lookup

One-hot vector와 embedding matrix의 곱은 해당 row를 선택하는 것과 같다.

xtTE=E[ct]x_t^TE = E[c_t]

실제 구현에서는 큰 one-hot matrix를 만들지 않고 integer index로 row를 바로 가져오는 편이 효율적이다.

긴 sequence의 학습

긴 sequence에서는 gradient norm을 기록해 vanishing gradient와 exploding gradient가 어떻게 나타나는지 확인한다. Gradient clipping은 norm이 threshold를 넘을 때만 gradient 크기를 줄여야 한다.

Truncated BPTT를 사용한다면 chunk 사이에서 hidden state 값은 넘기되 computational graph는 detach되는지 확인한다.