카테고리 없음

MNIST Lab 7편 - NeuralNetwork로 계층 조립하기

cedis 2026. 5. 29. 00:23

MNIST Lab 기본 구현 7편

이제 개별 부품을 하나의 모델로 묶는다. NeuralNetwork는 params를 만들고, 계층을 순서대로 쌓고, forward와 backward를 통해 전체 신경망을 통과시킨다.

1. 모델 구조

베이스 모델은 784개 입력을 512, 256 은닉층을 거쳐 10개 숫자 클래스로 보낸다.

1
입력

MNIST 28x28 이미지를 펼친 784차원 벡터

2
은닉층 1

Affine1 - BatchNorm1 - ReLU1 - Dropout1

3
은닉층 2

Affine2 - BatchNorm2 - ReLU2 - Dropout2

4
출력층

Affine3 - Softmax

2. params, layers, grads가 어떻게 연결되는가

NeuralNetwork 구현에서 헷갈리는 지점은 계층을 쌓는 코드보다 이름의 연결이다. 같은 가중치가 세 장소에서 서로 다른 역할로 등장한다.

저장 위치 예시 하는 일
params params['W1'] 학습해야 할 실제 파라미터 값을 가진다.
layers Affine1(W1, b1) forward/backward 계산에 params의 값을 사용한다.
grads grads['W1'] backward가 계산한 수정 방향을 담는다.
optimizer params['W1'] -= ... params와 grads의 같은 key를 찾아 값을 갱신한다.
1
초기화

params에 W1, b1, gamma1, beta1 같은 이름으로 값을 만든다.

2
계층 조립

Affine1과 BatchNorm1은 params의 값을 참조해서 계산 계층이 된다.

3
역전파

각 계층이 dW, db, dgamma, dbeta를 만들고 grads에 같은 번호로 모인다.

4
갱신

optimizer는 params와 grads의 key를 맞춰 실제 값을 바꾼다.

이름이 설계다

W1을 쓰는 계층이 Affine1인데 backward에서 grads['W_1']처럼 다른 이름으로 저장하면 optimizer는 그 gradient를 찾지 못한다. 이 글에서 params와 grads key를 반복해서 확인하는 이유가 여기에 있다.

3. 핵심 구현 코드

class NeuralNetwork:
    def __init__(self, use_batchnorm=True, use_dropout=True, dropout_ratio=0.5):
        layer_sizes = [784, 512, 256, 10]
        self.params = {}

        for i in range(1, len(layer_sizes)):
            fan_in = layer_sizes[i - 1]
            fan_out = layer_sizes[i]
            scale = np.sqrt(2.0 / fan_in)
            self.params[f"W{i}"] = scale * np.random.randn(fan_in, fan_out)
            self.params[f"b{i}"] = np.zeros(fan_out)

            if i < len(layer_sizes) - 1 and use_batchnorm:
                self.params[f"gamma{i}"] = np.ones(fan_out)
                self.params[f"beta{i}"] = np.zeros(fan_out)

        self.layers = OrderedDict()

        for i in range(1, len(layer_sizes)):
            self.layers[f"Affine{i}"] = Affine(self.params[f"W{i}"], self.params[f"b{i}"])

            if i < len(layer_sizes) - 1:
                if use_batchnorm:
                    self.layers[f"BatchNorm{i}"] = BatchNorm(
                        self.params[f"gamma{i}"],
                        self.params[f"beta{i}"]
                    )
                self.layers[f"ReLU{i}"] = ReLU()
                if use_dropout:
                    self.layers[f"Dropout{i}"] = Dropout(dropout_ratio)

        self.softmax = Softmax()
왜 OrderedDict를 쓰는가

forward는 계층을 넣은 순서대로 지나가야 하고, backward는 그 순서를 정확히 뒤집어야 한다. 순서가 모델 구조 그 자체이기 때문에 OrderedDict로 계층 순서를 분명하게 둔다.

4. forward와 backward

def forward(self, x, train=True):
    out = x
    for layer in self.layers.values():
        if isinstance(layer, BatchNorm) or isinstance(layer, Dropout):
            out = layer.forward(out, train=train)
        else:
            out = layer.forward(out)
    out = self.softmax.forward(out)
    return out

def backward(self, dout):
    self.grads = {}
    dout = self.softmax.backward(dout)

    for name, layer in reversed(list(self.layers.items())):
        dout = layer.backward(dout)

        if isinstance(layer, Affine):
            idx = name.replace("Affine", "")
            self.grads[f"W{idx}"] = layer.dW
            self.grads[f"b{idx}"] = layer.db

        if isinstance(layer, BatchNorm):
            idx = name.replace("BatchNorm", "")
            self.grads[f"gamma{idx}"] = layer.dgamma
            self.grads[f"beta{idx}"] = layer.dbeta
흐름 순서 왜 그런가
forward Affine1 -> BatchNorm1 -> ReLU1 -> Dropout1 -> ... -> Softmax 입력이 예측 확률로 바뀌는 방향이다.
backward Softmax -> ... -> Dropout1 -> ReLU1 -> BatchNorm1 -> Affine1 최종 손실의 gradient를 원래 파라미터 방향으로 되돌리는 방향이다.
grads 수집 Affine과 BatchNorm을 만날 때만 grads에 저장 ReLU와 Dropout은 학습 파라미터가 없으므로 optimizer가 갱신할 값이 없다.

5. 테스트가 묻는 것

테스트 확인 조건
test_neural_network_forward_shape 입력 batch가 들어오면 출력 shape가 batch x 10인가
test_neural_network_params_exist optimizer가 수정할 params가 존재하는가
test_neural_network_backward_produces_grads params와 같은 이름의 grads가 만들어지는가
가장 많이 터지는 지점

params에는 W1, b1, gamma1처럼 저장되어 있는데 grads 이름이 다르면 optimizer가 짝을 맞출 수 없다. backward에서 이름을 맞추는 부분이 중요하다.

이번 글에서 기억할 것

NeuralNetwork는 계층을 순서대로 조립하고, params와 grads의 이름을 맞춰 optimizer가 갱신할 수 있게 만드는 설계도다.

스스로 점검

  1. 왜 마지막 출력층에는 BatchNorm과 Dropout을 붙이지 않았는가?
  2. forward와 backward의 순서는 어떻게 반대가 되는가?
  3. params와 grads의 key가 맞아야 하는 이유는 무엇인가?

다음 글 예고

다음 글에서는 BatchNorm을 구현한다. 평균과 분산으로 값을 정규화하고, gamma와 beta gradient까지 계산한다.

한 줄 정리

NeuralNetwork는 계층을 순서대로 조립하고, params와 grads의 이름을 맞춰 optimizer가 갱신할 수 있게 만드는 설계도다.