먼저, 순전파 시 세대를 설정하는 부분부터 시작하겠습니다.
그 다음, 역전파 시 최근 세대의 함수 부터 꺼내도록 합니다.
16.1 세대 추가(순전파)
Variable 클래스, Function 클래스의 인스턴스 변수인 generation을 추가합니다.
Variable 클래스에 인스턴스 변수 generation 추가
class Variable:
def __init__(self, data):
if data is not None:
if not isinstance(data, np.ndarray):
raise TypeError('{} is not supported'.format(type(data)))
self.data = data
self.grad = None
self.creator = None
self.generation = 0 # Variable 클래스의 인스턴스 변수 추가하여 세대 수를 기록함
def set_creator(self, func):
self.creator = func
# set_creator 메서드가 실행되면 func(부모 함수)의 세대에 1을 더해 세대 수를 기록함
self.generation = func.generation + 1
위의 코드를 설명하자면 Variable 클래스는 처음 genration을 0으로 초기화 해줍니다.
그리고 함수의 set_creator의 메서드가 호출되면 부모 함수의 세대보다 1만큼 큰 값을 self.generation변수에 저장해줍니다. 그러면 Variable 클래스의 def __init__ 부분의 self.generation = 0에서 0 부분이 func.generation+1의 값으로 변경됩니다.

위의 이미지를 예시로 보면 D 함수를 거쳐 Y로 값을 내보낼때 D의 함수에서 set_creator가 호출되고
self.generation은 2(=D.generation) + 1이 되서 3이 됩니다.
이 self.generation은 Y라는 이름의 Variable 상자니까 Y.generation이고 결국엔 Y.generation은 3이 되는겁니다.
Function 클래스에 generation 추가
1. Function 클래스의 generation은 입력 변수와 같은 값으로 설정해줍니다.(같은 세대로 설정해줍니다.)

입력 변수가 아래 이미지처럼 2개 이상이라면 가장 큰(max) generation의 수를 선택합니다.

class Function:
def __call__(self, *inputs):
xs = [x.data for x in inputs]
ys = self.forward(*xs)
if not isinstance(ys, tuple):
ys = (ys,)
outputs = [Variable(as_array(y)) for y in ys]
self.generation = max([x.generation for x in inputs])
print([x.generation for x in inputs])
for output in outputs:
output.set_creator(self)
self.inputs = inputs
self.outputs = outputs
return outputs if len(outputs) > 1 else outputs[0]
self.generation = max([x.generation for x in inputs])
여기 부분이 여러개의 inputs을 반복문을 돌려 각각의 inputs의 세대를 리스트에 담아 max로 최대값을 뽑아 Function 클래스의 self.generation에 저장하는 부분입니다.
지금까지 Variable, Function 클래스의 코드를 변경하였는데 이는 순전파 시 모든 변수와 함수의 세대가 설정되는 것으로 이해하면됩니다.
16.2 세대 순으로 꺼내기(역전파)

위의 그림을 보면 A, B, C, D 함수 순서대로 세대가 0, 1, 1, 2로 되어있어
역전파 때 함수를 높은 세대 순서대로 꺼낼 수 있습니다.
* Variable 클래스의 backward 메서드 안에서는 처리할 함수의 후보들을 funcs 리스트에 보관합니다.
funcs에서 세대가 큰 함수부터 꺼내면 올바른 순서로 역전파를 진행할 수 있습니다.
generations = [2, 0, 1, 4, 2]
funcs = []
for g in generations:
f = Function() # f 인스턴스 생성
f.generation = g # f.generation을 for문을 통해 들어오는 g값을 넣어줌
funcs.append(f) # f를 funcs에 어펜드 해줌
funcs.sort(key=lambda x: x.generation) # .sort()를 사용하여 x.generatin을 정렬
[f.generation for f in funcs] # 확인하기
f = funcs.pop() # f변수는 정렬된 funcs에 마지막(=가장큰) 원소를 뺀 것
f.generation # f의 세대 확인
위의 코드를 보면 sort 메서드를 이용하여 generation을 오름차순으로 정렬합니다.
이 메서드의 인수인 key=lambda x: x.generation은 리스트의 원소를 x라고 했을 때 x.generation을 사용해 정렬하는 겁니다.
왜 lambda를 사용할까요?
현재, funcs의 리스트 안의 값을 보면 아래 처럼 들어 있어요. [<__main__.Function object at 0x000001ABFF91DF60>, <__main__.Function object at 0x000001ABFF91E050>, <__main__.Function object at 0x000001ABFF91DBA0>, <__main__.Function object at 0x000001ABFF91E110>]
우리는 이걸 정렬을 해줘야하는데 정렬의 기준을 각 Function.generation의 숫자로 하자는 겁니다.
정렬 후 [f.generation for f in funcs]로 확인해보면 [0, 1, 2, 2, 4] 이렇게 잘 정렬 된 것을 알 수 있습니다.
정렬되면 .pop 메서드를 써서 리스트의 끝 원소를 꺼내면 자연스럽게 가장 큰 함수를 얻을 수 있습니다.
(추가) sort()와 sorted()
1. sort()
- sort() 메서드는 리스트 객체만 사용되며 해당 리스트 자체를 정렬한 다음 변경합니다.(원본 리스트를 변경)
my_list = [3, 1, 2]
my_list.sort()
print(my_list)
- sorted(lambda)는 복잡한 정렬 기준을 간단하게 지정할 수 있습니다.
- 예를들어 튜플로 구성된 리스트를 두 번째 요소에 따라 정렬기준을 지정한다고 했을때
아래처럼 x[1]로 기준을 정해 정렬할 수 있습니다.
my_list = [(1, 'one'), (4, 'four'), (3, 'three'), (2, 'two')]
my_list.sort(key=lambda x: x[1])
print(my_list)
# [(4, 'four'), (1, 'one'), (3, 'three'), (2, 'two')]
2. sorted()
- sorted() 함수는 iterable 객체(리스트, 튜플, 딕셔너리 등)를 입력으로 받아 정렬된 새로운 리스트를 반환합니다.
- 원본 데이터를 변경하지 않습니다.
my_list = [3, 1, 2]
new_list = sorted(my_list)
print(new_list) # [1, 2, 3]
print(my_list) # [3, 1, 2] (원본은 변경되지 않음)
16.3 Variable 클래스의 backward
이제, Variable 클래스의 backward 메서드를 구현하겠습니다.

def add_func(f): 를 설명하자면, add_func()에 함수가 들어올때마다 seen_set에 없으면 funcs에 f를 넣어준다음
funcs의 x.generation을 정렬해줘서 아래 while funcs:를 마지막 세대부터 들어갈 수 있게 해줍니다.
그럼, 중간에 add_func(self.creator)가 중첩함수로써 왜 있냐면
x = Variable(np.array(2.0))
a = square(x)
y = add(square(a), square(a))
y.backward()
위의 코드의 기준으로 보면 add함수를 가장 먼저 역전파를 진행할텐데 add_func(add함수)를 넣어서 동작할 수 있게 해주는 겁니다.
중첩함수로 쓰인 add_func(self.creator)는 두 조건을 충족할때 적합합니다.
1) 감싸는 메서드(backward 메서드) 안에서만 이용한다.
2) 감싸는 메서드(backward 메서드)에 정의된 변수 (funcs과 seen_set)를 사용해야한다.
* seen_set은 set이라는 집합 자료형을 사용하여 같은 함수를 중복 추가하지 않아 함수의 backward 메서드가 잘못되어 여러 번 불리는 일이 발생하지 않습니다.
16.4 동작 확인
순전파 시 함수의 진행 방향에 따라 세대 수를 추가해 줬고 역전파 시 가장 마지막 세대 부터 호출되며 함수가 중복되어 진행되지 않고 가장 처음 세대까지 순차적으로 진행됩니다.

x = Variable(np.array(2.0))
a = square(x)
y = add(square(a), square(a))
y.backward()
print(y.data)
print(x.grad)
a = x**2
y = x**4 + x**4 = 2x**4
y' = 4 * 2x**(4-1) = 4 * 2x**3 = 8x**3 이고 x는 2니까 8 * 8(=2**3)= 64
아래의 값을 확인했을 때 x.grad의 값이 64니까 역전파까지 잘 계산이 된 것을 알 수 있습니다.

'Study 공부할레나' 카테고리의 다른 글
| [스터디/밑시딥3] 제 2고지 자연스러운 코드로_18. 메모리 절약 모드 (0) | 2023.08.26 |
|---|---|
| [스터디/밑시딥3] 제 2고지 자연스러운 코드로_17. 메모리 관리와 순환 참조 (0) | 2023.08.26 |
| [스터디/밑시딥3] 제 2고지 자연스러운 코드로_15. 복잡한 계산 그래프(이론 편) (0) | 2023.08.22 |
| [스터디/밑시딥3] 제 2고지 자연스러운 코드로_14. 같은 변수 반복 사용 (0) | 2023.08.22 |
| [스터디/밑시딥3] 제 2고지 자연스러운 코드로_13. 가변 길이 인수(역전파 편) (0) | 2023.08.21 |