Загружаем каталог…
Загружаем каталог…
4.2 LSTM과 GRU — 코드 리뷰 노트 노트북: 07-02. lstm_and_gru.ipynb (셀 9개, 코드 셀 7개) 예상 시간: 약 12분 (노트북 3분 + 추가 셀 9분) · 짝꿍 글: 4.2_LSTM과_GRU.md 발표 전 체크 노트북이 아주 짧다. 층을 만들어서 출력(repr)만 보는 셀뿐이고, 입력을 넣어 실행하는 셀이 없다. 그래서 이 노트에 세션에서 같이 돌려 볼 추가 셀 A~E 를 붙였다. 미리 코랩에 붙여 두자. ⚠️ 셀 5의 출력이 코드와 안 맞는다. 코드는 nn.LSTM(input_dim, hidden_size, ...) 이고 input_dim = 5 인데, 저장된 출력은 LSTM(8, 8, batch_first=True) 다. 작성자가 예전에 input_dim = 8 로 돌린 출력이 그대로 남은 것으로 보인다. 다시 실행하면 LSTM(5, 8, batch_first=True) 가 나온다. 4.1 노트북의 버그와 같은 교훈이라 짧게 짚고 가면 좋다. 셀 4와 셀 7은 똑같은 nn.RNN(...) 코드다. 비교용으로 일부러 둔 것이다. 흐름 한눈에 파트 셀 한 줄 메시지 시간 1. 노트북 0~8 만드는 법은 RNN과 똑같다. 이름만 바뀐다 3분 2. 추가 A — LSTM은 반환값이 (h_n, c_n) 튜플 2분 3. 추가 B — 파라미터 4배, 3배 2분 4. 추가 C — 수식 6줄 = nn.LSTM 3분 5. 추가 D — 기울기 실험 (시간 남으면) 2분 읽는 법: 🗣 = 그대로 말해도 되는 문장, ❓ = 나올 만한 질문과 답 Part 1. 노트북 (셀 0~8) 셀 0~1 — 임포트와 버전 import torch import torch.nn as nn torch.__version__ # '2.4.0+cu121' (노트북에 저장된 값) 버전 출력은 작성자 환경 값이다. 코랩에서 돌리면 다른 버전이 나와도 상관없다. 셀 2 — 크기 정하기 input_dim = 5 # 입력의 크기 hidden_size = 8 # 은닉 상태의 크기 4.1의 파이토치 파트와 같은 숫자다(입력 5, 은닉 8). 4.1의 결과와 바로 비교할 수 있다. 셀 4~5 — RNN과 LSTM 나란히 만들기 nn.RNN(input_dim, hidden_size, batch_first=True) # → RNN(5, 8, batch_first=True) nn.LSTM(input_dim, hidden_size, batch_first=True) # → LSTM(8, 8, batch_first=True) ← 저장된 출력이 이상하다 두 줄이 클래스 이름만 다르고 인자는 완전히 같다. 바깥에서 쓰는 법은 RNN과 같고, 셀 안쪽만 다르다는 게 이 셀의 메시지다. 변수에 담지 않았으니 층을 만들기만 하고 버린다. 주피터가 마지막 줄의 값을 보여주니까 층의 요약(repr) 이 출력된다. 출력은 (입력 크기, 은닉 크기, 옵션) 순서다. 셀 5의 저장된 출력 LSTM(8, 8, ...) 은 지금 코드로 나올 수 없는 값 이다. 다시 실행하면 LSTM(5, 8, ...) 이 나온다. 🗣 "RNN을 LSTM으로 바꾸는 건 이름 한 단어만 바꾸면 돼요. 4.5에서도 모델 코드에서 nn.LSTM 한 줄이 이 자리에 들어갑니다." 🗣 "셀 5 출력을 보면 LSTM(8, 8)이라고 찍혀 있는데, 코드는 input_dim이 5예요. 예전에 돌린 출력이 남아 있는 거고, 다시 돌리면 5가 나옵니다. 4.1에서도 봤지만, 저장된 출력은 '언젠가 돌린 결과'일 뿐이라 그대로 믿으면 안 돼요." 셀 7~8 — RNN과 GRU 나란히 만들기 nn.RNN(input_dim, hidden_size, batch_first=True) # → RNN(5, 8, batch_first=True) nn.GRU(input_dim, hidden_size, batch_first=True) # → GRU(5, 8, batch_first=True) 이번에도 이름만 다르다. GRU는 출력이 정상( 5, 8 )이다. ❓ 만드는 법이 다 같으면 뭐가 달라? 안에 든 가중치 수와 계산 순서가 다르다. 그리고 LSTM만 반환값 모양이 다르다. 추가 셀 A, B에서 바로 확인한다. Part 2. 추가 셀 A — 실행해서 반환값 보기 x = torch.randn(1, 10, 5) # (배치, 시점, 입력 차원) rnn = nn.RNN(5, 8, batch_first=True) lstm = nn.LSTM(5, 8, batch_first=True) gru = nn.GRU(5, 8, batch_first=True) out_r, h_r = rnn(x) out_l, (h_l, c_l) = lstm(x) # ← LSTM만 튜플로 받는다 out_g, h_g = gru(x) print(out_r.shape, h_r.shape) # [1, 10, 8] [1, 1, 8] print(out_l.shape, h_l.shape, c_l.shape) # [1, 10, 8] [1, 1, 8] [1, 1, 8] print(out_g.shape, h_g.shape) # [1, 10, 8] [1, 1, 8] outputs 모양은 셋 다 (1, 10, 8) 로 같다. LSTM은 두 번째 반환값이 (h_n, c_n) 튜플 이다. c_n 이 마지막 시점의 셀 상태 다. outputs 에는 $h_t$만 담긴다. 셀 상태는 셀 안에서만 흐르고, 마지막 값만 c_n 으로 꺼내 준다. 일부러 틀려 보기: outputs, _status = lstm(x) # 4.1처럼 받으면 print(_status.shape) # AttributeError: 'tuple' object has no attribute 'shape' 🗣 "4.1 습관대로 outputs, _status 로 받으면 _status가 튜플이라 shape에서 에러가 나요. LSTM은 꼭 괄호로 풀어서 받습니다. 4.5 모델 코드도 lstm_out, (hidden, cell) = self.lstm(embedded) 로 되어 있어요." ❓ 4.5에서는 뭘 써? hidden.squeeze(0) . (1, 배치, 128) 에서 층 차원 1을 없애 (배치, 128) 로 만들고 fc층에 넣는다. 다 대 일이라 마지막 은닉 상태 하나만 쓴다. Part 3. 추가 셀 B — 파라미터 세어 보기 for m in [rnn, lstm, gru]: print(type(m).__name__, sum(p.numel() for p in m.parameters())) for name, p in m.named_parameters(): print(' ', name, tuple(p.shape)) RNN 120 weight_ih_l0 (8, 5) weight_hh_l0 (8, 8) ... LSTM 480 weight_ih_l0 (32, 5) weight_hh_l0 (32, 8) ... GRU 360 weight_ih_l0 (24, 5) weight_hh_l0 (24, 8) ... RNN의 가중치 행 수는 8, LSTM은 32 = 8 × 4 , GRU는 24 = 8 × 3 이다. LSTM은 $f, i, g, o$ 네 개의 가중치를 한 행렬에 세로로 쌓아 둔다. 순서는 i, f, g, o 다. 행렬곱 한 번으로 넷을 같이 계산하고, 나중에 8칸씩 자른다. GRU는 r, z, g 세 벌이다. 🗣 "파라미터가 정확히 4배, 3배인 건 게이트마다 4.1의 RNN 셀이 하나씩 들어 있다고 보면 돼요. 게이트 하나가 RNN 셀 하나랑 같은 모양의 식이거든요." ❓ LSTM의 첫 8행만 꺼내면 뭐야? lstm.weight_ih_l0[:8] 이 입력 게이트 $i$의 $W_{xi}$ (8, 5) 다. [8:16] 은 삭제 게이트 $f$다. Part 4. 추가 셀 C — 수식 6줄로 nn.LSTM 재현하기 W_ih, W_hh = lstm.weight_ih_l0, lstm.weight_hh_l0 b_ih, b_hh = lstm.bias_ih_l0, lstm.bias_hh_l0 h = torch.zeros(1, 8) # h_0 c = torch.zeros(1, 8) # C_0 outs = [] for t in range(10): z = x[:, t] @ W_ih.T + b_ih + h @ W_hh.T + b_hh # (1, 32) 네 게이트를 한 번에 i, f, g, o = z.chunk(4, dim=1) # 8칸씩 넷으로 (순서 i, f, g, o) i, f, g, o = torch.sigmoid(i), torch.sigmoid(f), torch.tanh(g), torch.sigmoid(o) c = f * c + i * g # 셀 상태 h = o * torch.tanh(c) # 은닉 상태 outs.append(h) print(torch.allclose(torch.stack(outs, 1), out_l, atol=1e-6)) # True print(torch.allclose(c, c_l[0], atol=1e-6)) # True 한 줄씩: z = ... → 4.1의 RNN 셀 계산과 똑같은 모양이다. 다만 결과가 8칸이 아니라 32칸 이다(게이트 넷 몫). z.chunk(4, dim=1) → 32칸을 8칸씩 넷으로 자른다. 순서가 i, f, g, o인 걸 꼭 짚자. 활성화 → 게이트 셋(i, f, o)은 시그모이드 (0 1 밸브), 후보 g는 tanh (-1 1 내용). c = f * c + i * g → 셀 상태 갱신. 이번 편의 핵심 줄. * 는 원소별 곱이다. h = o * torch.tanh(c) → 셀 상태를 다듬어서 필요한 만큼 꺼낸다. 🗣 "4.1에서 RNN은 for문 안에 한 줄이었죠. LSTM은 이 여섯 줄이고, 그중 진짜 새로운 건 c = f * c + i * g 한 줄이에요. 곱셈 한 번, 덧셈 한 번으로만 기억이 바뀌니까 기울기가 멀리까지 갈 수 있습니다." ❓ GRU도 이렇게 재현돼? 된다. 다만 파이토치 GRU는 리셋 게이트를 곱하는 위치가 위키독스 수식과 다르다. 위키독스는 $W_{hg}(r \odot h)$이고, 파이토치는 $r \odot (W_{hg}h + b)$다. 위키독스 수식 그대로 짜면 nn.GRU 와 값이 다르게 나온다(직접 확인함). 학습 성능 차이는 거의 없다. Part 5. 추가 셀 D — 기울기가 얼마나 멀리 가나 (시간 남으면) def grad_by_distance(model, T=60): x = torch.randn(1, T, 5, requires_grad=True) out = model(x)[0] out[:, -1].sum().backward() # 마지막 출력에서 역전파 return x.grad[0].norm(dim=1).flip(0) # [0칸 앞, 1칸 앞, ...] torch.manual_seed(0) rnn, lstm = nn.RNN(5, 8, batch_first=True), nn.LSTM(5, 8, batch_first=True) lstm_open = nn.LSTM(5, 8, batch_first=True) with torch.no_grad(): lstm_open.bias_ih_l0[8:16] = 3.0 # 삭제 게이트 f의 편향만 3으로 → f ≈ 0.95 for name, m in [('RNN', rnn), ('LSTM', lstm), ('LSTM f≈0.95', lstm_open)]: g = grad_by_distance(m) print(name, [f'{g[k]:.0e}' for k in (0, 10, 30, 59)]) 결과 경향(시드 20개 평균, 글의 그래프와 같음): 0칸 10칸 30칸 59칸 RNN 1e+00 1e-03 2e-07 2e-11 LSTM (초기값) 3e-01 1e-03 6e-07 9e-11 LSTM (f ≈ 0.95) 2e-01 6e-02 3e-02 2e-02 bias_ih_l0[8:16] → 32칸 중 두 번째 8칸, 즉 삭제 게이트 $f$ 의 편향이다(순서 i, f , g, o). 시드 하나로 돌리면 숫자는 꽤 달라진다(예: 시드 1에서 RNN 59칸은 3e-21). 하지만 "RNN과 초기 LSTM은 계속 작아지고, f ≈ 0.95인 LSTM만 버틴다"는 경향은 같다. 🗣 "초기값 그대로의 LSTM은 RNN이랑 똑같이 기울기가 사라져요. 학습 전에는 f가 0.5 근처라서 매 시점 기억이 반씩 줄거든요. f를 0.95로 열어 주면 60칸 앞까지 기울기가 살아 있습니다." 🗣 "그러니까 LSTM이 기울기 소실을 공짜로 없애 주는 게 아니에요. 기울기가 지나갈 길을 만들어 두고, 그 길을 열지 말지를 학습하게 한 겁니다." ❓ 그럼 4.5에서도 편향을 바꿔야 해? 안 바꾼다. 학습하면서 필요한 칸의 $f$가 알아서 올라간다. 처음부터 1 정도로 잡아 두는 경우도 있지만 필수는 아니다. 마무리 (1분) 🗣 "LSTM과 GRU는 바깥에서 쓰는 법이 RNN과 거의 같아요. 다른 점은 셋입니다. 첫째, LSTM은 반환값이 outputs, (h_n, c_n) 튜플이에요. 둘째, 파라미터가 4배, 3배예요. 셋째, 안에서 게이트가 기억을 얼마나 지우고 넣을지 조절합니다. 4.5 모델 코드에서 nn.LSTM 한 줄과 (hidden, cell)로 받는 부분이 오늘 본 그대로 나옵니다." 치트시트 (입력 (1, 10, 5) , hidden_size=8 ) 받는 법 outputs 마지막 상태 파라미터 가중치 순서 nn.RNN out, h_n (1, 10, 8) h_n (1, 1, 8) 120 — nn.LSTM out, (h_n, c_n) (1, 10, 8) h_n , c_n 각 (1, 1, 8) 480 i, f, g, o nn.GRU out, h_n (1, 10, 8) h_n (1, 1, 8) 360 r, z, g
То, что RADAR обнаружил и классифицировал для этой возможности. Это опубликованный источником текст, а не подтверждение, что предложение ещё действует.
4.2 LSTM과 GRU 코드 리뷰. 4.2 LSTM과 GRU — 코드 리뷰 노트 노트북: 07-02. lstm_and_gru.ipynb (셀 9개, 코드 셀 7개) 예상 시간: 약 12분 (노트북 3분 + 추가 셀 9분) · 짝꿍 글: 4.2_LSTM과_GRU.md 발표 전 체크 노트북이 아주 짧다. 층을 만들어서 출력(repr)만 보는 셀뿐이고, 입력을 넣어 실행하는 셀이 없다. 그래서 이 노트에 세션에서 같이 돌려 볼 추가 셀 A~E 를 붙였다. 미리 코랩에 붙여 두자. ⚠️ 셀 5의 출력이 코드와 안 맞는다. 코드는 nn.LSTM(input_dim, hidden_size, ...) 이고 input_dim = 5 인데, 저장된 출력은 LSTM(8, 8, batch_first=True) 다. 작성자가…
Открыть источник