GRU는 어떻게 동작하는가
— 게이트 하나를 덜어 낸 값
GRU(Gated Recurrent Unit)는 LSTM을 단순화한 순환 셀입니다. 게이트는 셋에서 둘로, 상태는 둘에서 하나로 줄었고 파라미터는 정확히 4분의 3입니다. 그런데 성능은 대부분의 과제에서 LSTM과 비슷합니다. 무엇을 빼도 괜찮았고, 무엇을 합쳤고, 그 대가로 무엇을 잃었는지를 한 스텝씩 계산하고, 마지막에는 브라우저에서 RNN·GRU·LSTM을 나란히 학습시켜 봅니다.
§1 LSTM에서 GRU로 — 세 번의 수술
먼저 식 네 줄입니다. 이 글 전체에서 이 표기를 씁니다.
rt = σ(Wrxt + Urht−1 + br) 리셋 게이트
h̃t = tanh(Whxt + Uh(rt ⊙ ht−1) + bh) 후보
ht = (1 − zt) ⊙ ht−1 + zt ⊙ h̃t 섞기 Chung 외(2014)의 표기 — z가 "새 것을 받는 몫". 원 논문과 PyTorch는 z를 반대로 씁니다(§8)
GRU는 LSTM을 깎아서 만든 것처럼 보이지만, Cho 외는 LSTM에서 "영감을 받아" 새로 설계했다고 씁니다. 그래도 LSTM에서 출발해 세 번 수술하면 GRU에 도착한다고 보면 각 부품이 왜 있는지가 가장 잘 보입니다. 아래에서 한 단계씩 넘겨 보세요. 파라미터 수는 입력·은닉 차원을 바꾸면 다시 계산됩니다.
세 수술을 말로 정리하면 이렇습니다.
- 망각과 입력을 한 손잡이로. LSTM은 "얼마나 지울지(f)"와 "얼마나 쓸지(i)"를 따로 정합니다. 이것을
f = 1 − i로 묶으면 지운 만큼만 쓸 수 있습니다. Greff 외(2017)가 CIFG(coupled input-forget gate)라 부른 변형이고, 성능을 크게 해치지 않았습니다. 손잡이 하나가 곧 GRU의 업데이트 게이트z입니다. - 셀 상태와 은닉 상태를 하나로. LSTM은 긴 기억
c를 안쪽에 두고 출력 게이트로 일부만 꺼내h로 보여 줍니다. GRU는 둘을 합쳐 기억하는 것 = 내보내는 것으로 만들고, 출력 게이트를 없앱니다. 이 단계에서 가중치는 두 벌까지 줄어듭니다. - 리셋 게이트를 더하기. 두 벌짜리 셀은 후보
h̃를 만들 때 항상 과거ht−1를 통째로 봅니다. 문장이 바뀌는 지점처럼 과거를 무시하고 새로 시작해야 할 때가 있어서, 후보를 만들 때만 과거를 가리는 게이트r을 하나 더 둡니다(§4). 그래서 GRU는 두 벌이 아니라 세 벌입니다.
§2 한 스텝 따라가기 — 섞기는 선분 위의 한 점이다
GRU 한 스텝의 마지막 줄을 다시 봅시다.
계수의 합이 1이라는 것은 새 상태가 이전 상태와 후보를 잇는 선분 위 어딘가에 놓인다는 뜻입니다.
z는 그 선분 위의 위치입니다. 0이면 이전 상태 그대로, 1이면 후보로 완전히 갈아타고, 0.3이면 30%만큼 후보 쪽으로 이동합니다.
한 차원짜리 GRU로 직접 움직여 보세요.
두 게이트가 하는 일이 확실히 다르다는 게 보입니다.
z는 점을 선분 위에서 움직입니다. 선분의 양 끝(이전 상태, 후보)은 그대로 두고, 둘 중 어디에 설지만 정합니다.r은 선분의 한쪽 끝(후보)을 옮깁니다.r을 0으로 내리면 후보가tanh(Whxt), 즉 현재 입력만으로 만든 값으로 바뀝니다. 이전 상태 쪽 끝은 꿈쩍하지 않습니다.- 그리고
ht−1과h̃t가 둘 다 (−1, 1) 안에 있으니 그 사이의 점ht도 절대 이 구간을 벗어나지 못합니다. 이 한 줄이 §5의 이야기 전부입니다.
f = 1 − z, i = z, o = 1이고 c = h인 셀입니다.
LSTM의 갱신 ct = f ⊙ ct−1 + i ⊙ C̃t에서 f + i는 0부터 2까지 아무 값이나 될 수 있는데,
GRU에서는 항상 정확히 1입니다.
§3 업데이트 게이트 — 기울기가 지나가는 길
바닐라 RNN의 문제는 기울기가 한 스텝 거슬러 갈 때마다 U와 tanh의 미분을 곱해야 한다는 것이었습니다.
그 곱이 1보다 작으면 k스텝 뒤에는 λk로 사라집니다. GRU에서 한 스텝의 야코비안을 쓰면 이렇습니다.
첫 항이 핵심입니다. z ≈ 0인 구간에서는 상태가 거의 그대로 복사되고, 기울기도 거의 그대로 거꾸로 흐릅니다.
LSTM에서 f ≈ 1일 때 셀 상태 레일이 하는 일과 같습니다. 여기에 결정적인 차이가 하나 더 있는데,
z는 상수가 아니라 입력을 보고 스텝마다 정해진다는 것입니다.
h̃t = xt로 둡니다.
z를 고정하면 GRU는 지수 이동 평균(EMA)입니다. 반감기는 ln 0.5 / ln(1 − z)스텝이라
z를 작게 하면 오래 기억하지만 사건에 느리게 반응하고, 크게 하면 빨리 반응하지만 잡음에 휩쓸립니다.
어떤 상수를 골라도 둘 다 잡을 수는 없습니다.
z가 입력을 보게 하면 이 맞교환이 사라집니다. 사건이 오면 z ≈ 1로 갈아타고, 잡음이 오면 z ≈ 0으로 버팁니다.
아래 차트가 그 결과입니다 — 사건과 사건 사이에서 ∏(1 − z)가 거의 1로 유지되어,
마지막 스텝의 기울기가 수십 칸 전 사건까지 줄지 않고 도착합니다. 바닐라 RNN(λ = 0.9)의 회색 선과 비교해 보세요.
실제 GRU에는 은닉 유닛마다 자기 z가 있습니다. 어떤 유닛은 z를 늘 작게 유지해 느린 시간 척도를 맡고,
어떤 유닛은 z를 크게 두어 방금 들어온 입력을 따라갑니다. 시간 척도를 유닛마다, 스텝마다 학습하는 셈입니다.
§4 리셋 게이트 — 과거를 끊으려면 스위치가 둘 필요하다
업데이트 게이트만으로 "완전히 잊기"가 될까요? z = 1이면 ht = h̃t로 이전 상태를 버리니 될 것 같지만,
후보 h̃t = tanh(Whxt + Uhht−1) 안에 과거가 또 들어 있습니다.
문을 닫았는데 새로 들이는 손님이 과거를 업고 들어오는 격입니다.
확인하는 방법이 있습니다. 서로 다른 과거를 가진 두 시퀀스 A와 B를 준비하고, 경계 이후로는 완전히 같은 입력을 넣습니다. 과거가 완전히 잊혔다면 경계 직후부터 두 상태가 정확히 같아야 합니다.
z = 1만으로는 두 선이 경계에서 가까워질 뿐 붙지 않습니다. r = 0만으로도 붙지 않습니다(이전 상태가 1 − z만큼 남으니까요).
둘을 함께 끊어야 차이가 정확히 0이 되고, 그 뒤로는 영원히 같은 궤적을 갑니다.
LSTM에서는 망각 게이트 f = 0 하나로 되는 일이, GRU에서는 두 게이트의 합작입니다.
§5 볼록 결합의 대가 — GRU는 세지 못한다
§2에서 본 성질로 돌아갑시다. GRU의 상태는 이전 상태와 후보 사이의 점이라 항상 (−1, 1) 안에 있습니다.
LSTM은 다릅니다. f = 1, i = 1이면 ct = ct−1 + C̃t — 지우지 않고 계속 더할 수 있어서 셀 상태가 경계 없이 자랍니다.
이 차이가 드러나는 대표 과제가 anbn입니다. a를 n개 본 뒤 b를 정확히 n개 봤는지 판단하려면 개수를 세어야 합니다.
LSTM은 a에 +1, b에 −1을 더하는 카운터가 되고, 끝에서 c = 0인지만 보면 됩니다.
GRU에게도 최선을 다할 기회를 줍니다 — a면 후보 +1 쪽으로, b면 −1 쪽으로 z만큼 이동하는 셀입니다.
C̃ = +1(a) / −1(b). GRU: h̃ = +1(a) / −1(b), z는 상수.
위 차트는 anbm을 읽는 동안의 상태, 아래 차트는 m = n과 m = n + 1을 가르는 끝 상태의 차이를 n에 따라 그린 것입니다.
LSTM은 n이 얼마든 m = n이면 끝 상태가 정확히 c = 0이고, 한 개라도 어긋나면 c = ±1입니다. 차이가 n과 무관하게 늘 1입니다.
GRU는 m = n일 때의 끝 상태가 n마다 다르고(고정된 "정답 위치"가 없음), m = n과 m = n + 1의 차이가 z(2 − qn)qn(q = 1 − z)로
n에 대해 지수적으로 작아집니다. 그 차이가 float32로 구분할 수 있는 한계보다 작아지면, 가중치를 어떻게 정해도 두 경우를 가를 수 없습니다.
z를 작게 하면 한계 n이 늘어나지만 대신 차이 자체가 z배로 작아집니다 — 어떤 상수 z도 한계를 없애지는 못합니다.
anbn을 학습시키면 LSTM은 학습 때 본 것보다 훨씬 긴 n으로 일반화했고, GRU는 그러지 못했습니다.
표현력만 보면 LSTM이 GRU보다 넓다는 뜻입니다. 다만 실제 언어 과제에서 이 차이가 드러나는 경우는 드뭅니다(§9).
§6 직접 학습시켜 보기 — 80칸 전의 한 비트
지금까지는 게이트 값을 손으로 정했습니다. 이번에는 브라우저에서 역전파로 실제로 학습시킵니다. 과제는 단순합니다 — 시퀀스 첫 칸에 ±1이 오고, 나머지 T − 1칸은 [−1, 1] 균등 잡음입니다. 마지막 칸에서 첫 칸의 부호를 맞히면 됩니다. 은닉 8차원, 배치 32, Adam(학습률 0.02), 기울기 노름 클리핑 1로 세 셀을 같은 조건에서 돌립니다.
기본 설정(T = 80, b = 3, 시드 1)에서 돌리면 이렇게 됩니다.
- 바닐라 RNN은 600스텝 내내 0.5 근처를 맴돕니다. 80칸을 거슬러 가는 동안 기울기가 사라져, 첫 칸이 결과에 영향을 준다는 신호 자체를 받지 못합니다.
- GRU는 200스텝이 안 되어 정확도 0.97을 넘습니다.
- LSTM도 올라오지만 이 시드에서는 GRU보다 한참 늦습니다. 시드를 바꾸면 순서가 바뀌기도 합니다 — 게이트 있는 두 셀 사이의 차이는 대개 이 정도이고, 바닐라 RNN과의 차이와는 급이 다릅니다.
이제 게이트 편향을 0으로 내리고 T = 20으로 돌려 보세요. GRU와 LSTM이 T = 20에서도 실패하고, 오히려 바닐라 RNN이 풉니다.
편향 0이면 시작할 때 z ≈ 0.5라 한 스텝마다 상태의 절반이 잡음으로 덮이고, 복사 경로 ∏(1 − z)가 0.5k로 바닐라 RNN보다 빨리 사라지기 때문입니다.
게이트는 "닫힌 상태"에서 출발해야 기울기 길이 열린 채로 학습이 시작됩니다.
LSTM의 망각 편향을 1로 두라는 오래된 조언(Gers 외 2000, Jozefowicz 외 2015)과 같은 이야기이고, GRU에서는 업데이트 편향을 음수로 두는 것이 짝입니다.
nn.GRU와 nn.LSTM은 모든 편향을 같은 균등 분포로 초기화합니다 — 이 트릭을 자동으로 해 주지 않습니다.
(Keras LSTM은 unit_forget_bias=True가 기본이지만, GRU에는 해당 옵션이 없습니다.) 긴 의존성이 중요한 과제라면 직접 편향을 채워 넣을 가치가 있습니다.
§7 파라미터와 행렬 곱 — DS2가 곱셈 순서를 바꾼 이유
GRU는 가중치가 세 벌이니 파라미터는 LSTM의 정확히 3/4입니다. 계산기로 확인해 봅시다.
FLOP도 같은 비율로 줄어듭니다. 그런데 GPU에서 순환층의 속도를 정하는 것은 FLOP보다 ht−1에 걸리는 행렬 곱을 몇 번, 어떤 순서로 부르는가입니다.
순환층은 스텝 t가 끝나야 t + 1을 시작할 수 있어서, 한 스텝 안의 행렬 곱들이 순서대로 이어지면 그대로 지연이 됩니다.
원래 GRU에는 걸림돌이 하나 있습니다. Uh(rt ⊙ ht−1)는 rt가 나와야 시작할 수 있습니다.
그래서 Uzh와 Urh를 한 번에 묶더라도 Uh 곱은 따로, 뒤에 해야 합니다.
Deep Speech 2(Amodei 외 2015, 3.4절)는 곱셈 순서를 바꿨습니다.
리셋 나중: h̃t = tanh(Whxt + rt ⊙ (Uhht−1 + bhn) + bh) Uh를 먼저 곱하면 Uz·Ur·Uh를 쌓은 (3dh×dh) 행렬 하나로 끝난다
FLOP은 한 개도 다르지 않습니다. 달라지는 것은 순서대로 이어지는 행렬 곱이 2번에서 1번으로 준다는 것,
작은 행렬 곱 여러 번이 큰 행렬 곱 한 번이 된다는 것입니다(따로 부르면 3번 → 1번). GPU는 큰 행렬 곱 하나를 훨씬 효율적으로 처리합니다.
그래서 cuDNN이 이 변형을 구현했고, PyTorch nn.GRU와 Keras(reset_after=True가 TF 2의 기본값)도 이쪽입니다.
대신 7-B의 아래 수치가 보여 주듯 같은 가중치를 넣어도 출력이 다릅니다. 두 변형은 다른 모델이라, 원 논문식으로 학습한 가중치를 PyTorch에 그대로 옮기면 조용히 틀린 값이 나옵니다.
§8 실무 메모 — 프레임워크마다 다른 GRU
수식으로는 한 줄 차이지만, 가중치를 옮기거나 게이트를 시각화할 때 실제로 부딪히는 차이들입니다.
| 이 글 (Chung 외 표기) | Cho 외 원 논문 | PyTorch nn.GRU | Keras GRU (TF 2) | |
|---|---|---|---|---|
| 섞기 식 | (1−z)h + z·h̃ | z·h + (1−z)·h̃ | z·h + (1−z)·n | z·h + (1−z)·h̃ |
| z = 1의 뜻 | 새 것으로 교체 | 이전 상태 유지 | ||
| 리셋 위치 | U 곱 앞 | U 곱 앞 | U 곱 뒤 | U 곱 뒤 (기본값) |
| 편향 | 게이트당 1개 | 게이트당 1개 | 2개 (bih, bhh) | 2개 (reset_after일 때) |
| 가중치 쌓는 순서 | — | — | r, z, n | z, r, h |
| LSTM의 순서 (참고) | — | — | i, f, g, o | i, f, c, o |
- z의 방향이 반대입니다. 이 글과 Chung 외는
z가 "새 것을 받는 몫", 원 논문·PyTorch·Keras는 "유지하는 몫"입니다. 학습된 게이트 값을 그려 놓고 "이 유닛은 기억을 오래 한다"고 해석하기 전에 어느 쪽인지부터 확인하세요. - 게이트 순서가 프레임워크마다 다릅니다. PyTorch는
weight_ih_l0에 r, z, n 순서로, Keras는 z, r, h 순서로 쌓습니다. 가중치 변환 스크립트에서 가장 흔한 버그가 이 순서입니다. - PyTorch의
b_hn은 리셋 게이트 안쪽에 있습니다. 편향이 두 개라서bih + bhh로 합칠 수 있을 것 같지만,n에 쓰이는bhn은r ⊙ (…)안에 곱해지므로 합치면 안 됩니다. z와 r의 편향은 합쳐도 됩니다. - 업데이트 편향 초기화. §6에서 본 것처럼 기본 초기화는 게이트를 반쯤 열린 상태로 시작합니다. PyTorch에서 z 편향은
bias_ih_l0[H:2H](r 다음)이고, PyTorch 쪽 z는 "유지하는 몫"이므로 양수로 채워야 합니다.
§9 그래서 LSTM이냐 GRU냐
여러 대규모 비교가 비슷한 결론을 냅니다 — 대체로 비슷하고, 과제와 규모에 따라 갈린다.
| 연구 | 무엇을 비교했나 | 결론 |
|---|---|---|
| Chung 외 (2014) | 다성 음악·음성 신호 모델링에서 tanh RNN / LSTM / GRU | 게이트 있는 두 셀이 tanh RNN보다 확실히 낫고, LSTM과 GRU 사이는 데이터셋마다 승자가 갈려 우열을 가리지 못함 |
| Jozefowicz 외 (ICML 2015) | 1만 개 넘는 구조를 진화 탐색 | GRU가 LSTM보다 나은 과제가 있었지만, LSTM의 망각 편향을 1로 초기화하면 격차가 대부분 사라짐 |
| Greff 외 (2017) | LSTM 변형 8종 × 음성·필기·음악 | 입력·망각 결합(CIFG, GRU의 핵심)은 성능을 크게 해치지 않음. 망각 게이트와 출력 활성화는 빼면 안 되는 부품 |
| Weiss 외 (ACL 2018) | anbn 등 세기가 필요한 형식 언어 | LSTM은 세기를 배워 일반화, GRU는 실패 — 표현력 차이(§5) |
| Deep Speech 2 (2015) | 음성 인식, 3,800만 파라미터(표 3)와 1억 파라미터(표 11) | 7층 기준 WER은 GRU 7.79 vs RNN 9.78로 GRU 승. 그런데 1억 규모에서는 RNN 7.73 vs GRU 7.78로 뒤집힘 — 이후 실험 대부분은 바닐라 RNN |
실무에서의 선택 기준은 대개 이렇게 정리됩니다.
- 파라미터·속도·메모리가 빠듯하면 GRU. 같은 은닉 차원에서 3/4이고, 상태도 하나라 들고 다닐 것이 적습니다.
- 안전한 기본값이 필요하면 LSTM(망각 편향 1). 세기처럼 값을 누적해야 하는 과제에서는 LSTM만 가능한 경우가 있습니다.
- 파라미터 수를 맞춰서 비교하세요. 같은 dh로 비교하면 GRU가 25% 작은 모델입니다. 같은 예산이면 GRU의 dh를 더 키울 수 있습니다 — 층을 쌓아 입력 차원도 같이 커지는 경우 √(4/3) ≈ 1.15배, 입력 차원이 dh와 같은 값으로 고정이면 약 1.21배입니다.
§10 정리
| 바닐라 RNN | GRU | LSTM | |
|---|---|---|---|
| 가중치 묶음 | 1 | 3 | 4 |
| 상태 | h | h | c, h |
| 갱신 | tanh(Wx + Uh) | (1−z)h + z·h̃ | f·c + i·C̃ |
| 계수의 합 | — | 항상 1 (볼록 결합) | 0 ~ 2 (자유) |
| 상태의 범위 | (−1, 1) | (−1, 1) | c는 제한 없음 |
| 기울기 복사 경로 | 없음 | ∏(1 − z) | ∏ f |
| 완전히 잊기 | — | z = 1 이고 r = 0 | f = 0 |
| 세기 | 불가 | 불가 (지수적으로 흐려짐) | 가능 |
한 문장으로 줄이면 이렇습니다.
GRU는 LSTM의 "지울 양"과 "쓸 양"을 한 손잡이로 묶고 기억과 출력을 하나로 합친 셀입니다. 그 덕에 가중치가 한 벌 줄었고 기울기의 복사 경로는 그대로 남았습니다. 대신 상태가 늘 두 점 사이에 갇혀서, 끝없이 더해 가는 일은 할 수 없습니다.
헷갈리기 쉬운 것들:
- GRU는 LSTM의 개선판이 아니라 단순화판입니다. 표현력은 오히려 LSTM이 조금 넓습니다.
- z가 1이면 유지인지 교체인지는 구현마다 반대입니다 (§8).
- 업데이트 게이트만으로는 과거를 완전히 끊지 못합니다 — 후보가 과거를 업고 들어옵니다 (§4).
- PyTorch의 GRU는 원 논문의 GRU와 같은 모델이 아닙니다 — 리셋 위치가 다릅니다 (§7).
- 게이트의 이득은 편향 초기화가 열어 줘야 쓸 수 있습니다 (§6).