1.5 첫 번째 연구 파이프라인 완주하기

실습 · 연습문제 2개

MLP 학습 한 step: forward → loss → backward → update mlp-train-step

본문의 작은 MLP를 학습하는 한 step을 직접 쓴다. 함수 train_step(params, X, y, lr)를 완성해 갱신된 파라미터와 갱신 전 파라미터에서의 loss를 반환한다.

모델은 은닉층 하나(tanh)의 MLP다.

H=tanh⁡(XW1+b1),y^=HW2+b2,L=1N∑i=1N(y^i−yi)2.\mathbf{H} = \tanh(\mathbf{X}\mathbf{W}_1 + \mathbf{b}_1), \qquad \hat{\mathbf{y}} = \mathbf{H}\mathbf{W}_2 + \mathbf{b}_2, \qquad L = \frac{1}{N}\sum_{i=1}^{N} (\hat{y}_i - y_i)^2.
이름 shape
X (N, D)
y (N, 1)
params["W1"], params["b1"] (D, H), (H,)
params["W2"], params["b2"] (H, 1), (1,)

순서는 다음과 같다.

  1. forward: H\mathbf{H}와 y^\hat{\mathbf{y}}를 계산한다.
  2. loss: 위의 MSE LL을 계산한다.
  3. backward: 연쇄법칙으로 네 파라미터의 gradient를 구한다. ∂L/∂y^=2(y^−y)/N\partial L / \partial \hat{\mathbf{y}} = 2(\hat{\mathbf{y}} - \mathbf{y})/N이고, tanh⁡′(a)=1−tanh⁡2(a)\tanh'(a) = 1 - \tanh^2(a)다.
  4. update: 각 파라미터 pp를 p−lr⋅∂L/∂pp - \text{lr} \cdot \partial L / \partial p로 바꾼다.

반환값은 (new_params, loss)이고, new_params는 같은 키를 가진 새 dict다. 입력으로 받은 params의 배열을 제자리에서 바꾸지 않는다.

테스트는 loss 값, gradient(중심 차분과 비교), 반복할 때 loss가 줄어드는지, 입력을 바꾸지 않는지를 확인한다. gradient는 (params - new_params) / lr로 복원해 비교한다.

다시 실행해도 같은 분할 reproducible-split

"다른 사람이 같은 명령으로 같은 숫자를 다시 얻는다"의 첫 조건은 같은 분할이다. 함수 split_indices(n, fractions, seed)를 완성해 학습·검증·테스트 인덱스 배열 세 개를 반환한다.

  • fractions = (f_train, f_val, f_test)이고 합은 1이다.
  • 크기는 n_train = int(f_train * n), n_val = int(f_val * n), 나머지는 모두 테스트다. 따라서 세 배열을 합치면 0, 1, …, n-1이 빠짐없이, 겹치지 않게 들어 있다.
  • 0, …, n-1을 무작위로 섞은 뒤 앞에서부터 잘라 나눈다.
  • 무작위성은 np.random.default_rng(seed)로 만든 자기만의 난수 생성기에서만 얻는다. 전역 난수 상태(np.random.seed, np.random.permutation)를 쓰지도, 바꾸지도 않는다.

테스트는 다음을 확인한다.

  1. 크기, 겹침 없음, 빠짐없음
  2. 같은 seed이면 같은 분할, 다른 seed이면 다른 분할
  3. 함수 밖에서 np.random.seed(...)를 바꾸거나 다른 난수를 먼저 뽑아도 결과가 같은지
  4. 함수를 불러도 전역 난수 상태가 바뀌지 않는지

3–4번이 중요한 이유: 전역 난수 상태에 기대는 코드는 앞에서 어떤 코드가 몇 번 난수를 뽑았는지에 따라 분할이 달라진다. 셀 실행 순서나 import 순서만 바뀌어도 숫자가 바뀐다.