05.4 단순한 계층 구현하기
이번 절에서는 지금까지 보아온 ‘사과 쇼핑’ 예를 파이썬으로 구현합니다. 여기에서는 계산 그래프의 곱셈 노드를 ‘MulLayer’, 덧셈 노드를 ‘AddLayer’라는 이름으로 구현합니다.
NOTE_ 다음 절에서는 신경망을 구성하는 '층(계층)' 각각을 하나의 클래스로 구현합니다. 여기에서 말하는 '계층'이란 신경망의 기능 단위입니다. 예를 들어 시그모이드 함수를 위한 Sigmoid, 행렬 내적을 위한 Affine 등의 기능을 계층 단위로 구현합니다. 그래서 이번 절에서도 곱셈 노드와 덧셈 노드를 '계층' 단위로 구현합니다.
5.4.1 곱셈 계층
모든 계층은 forward()와 backward()라는 공통의 메서드(인터페이스)를 갖도록 구현할 것입니다. forward()는 순전파, backward()는 역전파를 처리합니다.
그럼 먼저 곱셈 계층을 구현해보죠. 곱셈 계층은 MulLayer라는 이름의 클래스로 다음과 같이 구현할 수 있습니다.
class MulLayer:
def __init__(self):
self.x = None
self.y = None
def forward(self, x, y):
self.x = x
self.y = y
out = x * y
return out
def backward(self, dout):
dx = dout * self.y # x와 y를 바꾼다.
dy = dout * self.x
return dx, dy
도로시가 곱셈 계층 MulLayer 파이썬 구현 코드가 적힌 클립보드를 보면서 순전파 입력값 x와 y를 멤버 변수로 저장하는 이유를 면밀히 분석하고 있어요.
도로시의 코드 한 줄 분석 (MulLayer)
- 도로시: “아! 곱셈 계층 코드를 보니까,
forward에서 단순히 곱셈 연산만 하는 게 아니라self.x = x와self.y = y로 값을 보관해 놓아. 그래야만 나중에backward가 실행될 때 서로 엇갈려서 곱해줄 수 있구나!”- 지니: “맞아! 클래스의 인스턴스 변수로 값을 보관해 두는 것이 계산 그래프의 상태 메모리 역할을 하는 핵심 코딩 기법이지!”
__init__()에서는 인스턴스 변수인 x와 y를 초기화합니다. 이 두 변수는 순전파 시의 입력 값을 유지하기 위해서 사용합니다. forward()에서는 x와 y를 인수로 받고 두 값을 곱해서 반환합니다. 반면 backward()에서는 상류에서 넘어온 미분(dout)에 순전파 때의 값을 ‘서로 바꿔’ 곱한 후 하류로 흘립니다.
이상이 MulLayer의 구현입니다. 이 MulLayer를 사용해서 앞에서 본 ‘사과 쇼핑’을 구현해봅시다. 앞 절에서는 계산 그래프의 순전파와 역전파를 써서 [그림 5-16]과 같이 계산할 수 있었습니다.
그림 5-16 사과 2개 구입

MulLayer를 사용하여 [그림 5-16]의 순전파를 다음과 같이 구현할 수 있습니다.
apple = 100
apple_num = 2
tax = 1.1
# 계층들
mul_apple_layer = MulLayer()
mul_tax_layer = MulLayer()
# 순전파
apple_price = mul_apple_layer.forward(apple, apple_num)
price = mul_tax_layer.forward(apple_price, tax)
print(price) # 220
또, 각 변수에 대한 미분은 backward()에서 구할 수 있습니다.
# 역전파
dprice = 1
dapple_price, dtax = mul_tax_layer.backward(dprice)
dapple, dapple_num = mul_apple_layer.backward(dapple_price)
print(dapple, dapple_num, dtax) # 2.2 110 200
backward() 호출 순서는 forward() 때와는 반대입니다. 또, backward()가 받는 인수는 ‘순전파의 출력에 대한 미분’임에 주의하세요. 가령 mul_apple_layer라는 곱셈 계층은 순전파 때는 apple_price를 출력합니다만, 역전파 때는 apple_price의 미분 값인 dapple_price를 인수로 받습니다. 마지막으로, 이 코드를 실행한 결과는 [그림 5-16]의 결과와 일치합니다.
5.4.2 덧셈 계층
이어서 덧셈 노드인 덧셈 계층을 구현하겠습니다. 덧셈 계층은 다음과 같이 구현할 수 있습니다.
class AddLayer:
def __init__(self):
pass
def forward(self, x, y):
out = x + y
return out
def backward(self, dout):
dx = dout * 1
dy = dout * 1
return dx, dy
덧셈 계층에서는 초기화가 필요 없으니 __init__()에서는 아무 일도 하지 않습니다(pass가 ‘아무것도 하지 말라’는 명령입니다). 덧셈 계층의 forward()에서는 입력받은 두 인수 x, y를 더해서 반환합니다. backward()에서는 상류에서 내려온 미분(dout)을 그대로 하류로 흘릴 뿐입니다.
도로시의 조립 계층 조립 (MulLayer와 AddLayer)
- 도로시: “와! 곱셈 노드는
MulLayer클래스로, 덧셈 노드는AddLayer클래스로 각각의 계층(Layer) 모듈로 분리하니까 코드가 조각 레고 블록처럼 맞춰져! 각 계층은 순방향forward()와 역방향backward()메서드 한 쌍만 구현해 두면 끝이네!”- 지니: “그렇단다! 곱셈 계층은 역전파를 위해 입력값
x와y를 메모리(self.x,self.y)에 고이 보관해 둬야 하지만, 덧셈 계층은 입력값과 무관하게 상류 기울기를 그대로 토스해 주기만 하면 되니 메모리를 비워둘 수 있는 사소한 차이점도 코드를 통해 깨달을 수 있지!”- 토토: “왈왈! (레고를 조립하듯 코드를 짜며) 계층 블록을 착착 끼우면 신경망이 뚝딱이다 멍!”
도로시가 테이블 위에 ‘곱셈 계층’과 ‘덧셈 계층’ 모듈 블록을 조립하며 코드를 작성하고 있고, 지니와 토토가 그 곁에서 조립 작업을 도와주고 있어요.
이상의 덧셈 계층과 곱셈 계층을 사용하여 사과 2개와 귤 3개를 사는 [그림 5-17]의 상황을 구현해보죠.
그림 5-17 사과 2개와 귤 3개 구입

[그림 5-17]의 계산 그래프를 파이썬으로 구현하면 다음과 같습니다.
apple = 100
apple_num = 2
orange = 150
orange_num = 3
tax = 1.1
# 계층들
mul_apple_layer = MulLayer()
mul_orange_layer = MulLayer()
add_apple_orange_layer = AddLayer()
mul_tax_layer = MulLayer()
# 순전파
apple_price = mul_apple_layer.forward(apple, apple_num) # (1)
orange_price = mul_orange_layer.forward(orange, orange_num) # (2)
all_price = add_apple_orange_layer.forward(apple_price, orange_price) # (3)
price = mul_tax_layer.forward(all_price, tax) # (4)
# 역전파
dprice = 1
dall_price, dtax = mul_tax_layer.backward(dprice) # (4)
dapple_price, dorange_price = add_apple_orange_layer.backward(dall_price) # (3)
dorange, dorange_num = mul_orange_layer.backward(dorange_price) # (2)
dapple, dapple_num = mul_apple_layer.backward(dapple_price) # (1)
print(price) # 715
print(dapple_num, dapple, dorange, dorange_num, dtax) # 110 2.2 3.3 165 650
토토가 사과와 귤 바구니를 물고 서 있고, 도로시가 완성된 결제 계산 및 미분 기울기 시뮬레이션 코드가 성공적으로 실행되는 노트북 화면을 자랑스럽게 가리키고 있어요.
도로시의 최종 시뮬레이션 확인
- 도로시: “지니! 곱셈 계층 3개와 덧셈 계층 1개를 조립해서 순방향/역방향 함수들을 차례로 호출했더니, 사과 2개와 귤 3개 결제 금액이 715원으로 완벽하게 구해졌어! 각 물건 가격이 오를 때의 미분 변화율 값도 수작업으로 계산한 값과 똑같아!”
- 지니: “축하해! 우리가 직접 클래스를 연결해서 훌륭한 계산 그래프 시뮬레이터를 코드로 돌려낸 거야. 이제 아무리 큰 상점의 복잡한 물품 계산이라도 레고 블록 조립하듯이 확장할 수 있게 됐어!”
- 토토: “왈왈! (바구니를 꼬리치며) 마트 시뮬레이션 성공이다 멍!”
코드가 다소 길어졌습니다만, 하나하나의 명령은 단순합니다. 필요한 계층을 만들어 순전파 메서드인 forward()를 적절한 순서로 호출합니다. 그런 다음 순전파와 반대 순서로 역전파 메서드인 backward()를 호출하면 원하는 미분이 나옵니다.
이처럼 계산 그래프에서의 계층(여기에서는 곱셈과 덧셈)은 쉽게 구현할 수 있으며, 이를 사용해 복잡한 미분도 계산할 수 있습니다. 다음 절에서는 신경망에서 사용하는 계층을 구현하겠습니다.