2025.04.15 - [AI/밑바닥부터 시작하는 딥러닝3] - 밑바닥 부터 시작하는 딥러닝 정리 2
밑바닥 부터 시작하는 딥러닝 정리 2
저번내용2025.04.12 - [AI/밑바닥부터 시작하는 딥러닝3] - 밑바닥 부터 시작하는 딥러닝 3 정리 1~4단계 밑바닥 부터 시작하는 딥러닝 3 정리 1~4단계import numpy as npclass Variable: def __init__(self,data): self.dat
bbakgosu.tistory.com
이전 내용
import numpy as np
import matplotlib.pyplot as plt
class Variable:
def __init__(self, data):
if data is not None:
if not isinstance(data,np.ndarray):
print("{} type인 {}를 np.array type으로 변환하였습니다.".format(type(data),data))
data = np.array(data) # ⭐ 자동으로 ndarray로 변환!
self.data = data
self.grad = None
self.creator = None
def set_creator(self, func):
self.creator = func # ← 이 줄이 함수 내부에 잘 들어가 있어야 해
def backward(self):
if self.grad is None:
self.grad = np.ones_like(self.data)
funs = [self.creator]
while funs:
f = funs.pop()# 함수 하나 가져온다.
x , y = f.x, f.output #함수의 입력괴 출력 가져온다.
x.grad = f.backward(y.grad)
if x.creator is not None:
funs.append(x.creator)
def as_array(x):
if np.isscalar(x): #스칼라면 True
return np.array(x)
return x
class Function:
def __call__(self, x):
self.x = x # backward 계산할때 필요함
x = x.data
y = self.forward(x) # 구체적인 계산은 forward 메서드에서 한다.
output = Variable(as_array(y)) #만약 y 값이 스칼라면 ndarray로 변환
output.set_creator(self)
self.output = output
return output
def forward(self, x):
raise NotImplementedError()
def backward(self, gy):
raise NotImplementedError()
class Exp(Function):
def forward(self,x):
y = np.exp(x)
return y
def backward(self, gy):
x = self.x.data
gx = np.exp(x) * gy
return gx
class Square(Function):
def forward(self, x):
y = x**2
return y
def backward(self, gy):
x = self.x.data
gx = 2*x*gy
return gx
def square(x):
f= Square()
return f(x)
def exp(x):
f = Exp()
return f(x)
A = Square()
B = Exp()
C = Square()
x = Variable(np.array(0.5))
a = A(x)
b = B(a)
y = C(b)
print(y.data)
#역전파
y.grad = np.array(1.0)
y.backward()
print(x.grad)
1.648721270700128
3.297442541400256
파이썬 유닛 테스트
테스트를 해야 일어날 버그를 미리 확인할 수 있습니다.
import unittest
class SquareTest(unittest.TestCase):
def test_forward(self):
x = Variable(np.array(2.0))
y = square(x)
expected = np.array(4.0)
self.assertEqual(y.data, expected)
if __name__ == '__main__':
unittest.main(argv=['first-arg-is-ignored'], exit=False)
.
----------------------------------------------------------------------
Ran 1 test in 0.001s
OK
import unittest
class SquareTest(unittest.TestCase):
def test_backward(self):
x = Variable(np.array(3.0))
y = square(x)
y.backward()
expected = np.array(6.0)
self.assertEqual(x.grad,expected)
if __name__ == '__main__':
unittest.main(argv=['first-arg-is-ignored'], exit=False)
.
----------------------------------------------------------------------
Ran 1 test in 0.001s
OK
테스트 자동화
자동 미분 함수의 결과와 실제 backward 차이가 크지 않으면 테스트 통과
def numerical_diff(f,x,eps = 1e-4):
x1 = Variable((x.data - eps))
x2 = Variable((x.data + eps))
y1 = f(x1)
y2 = f(x2)
output =(y2.data-y1.data) / (2*eps)
return output
import unittest
class SquareTest(unittest.TestCase):
def test_forward(self):
x = Variable(np.array(2.0))
y = square(x)
y.backward()
num_grad = numerical_diff(square,x)
flg = np.allclose(x.grad,num_grad)
self.assertTrue(flg)
if __name__ == '__main__':
unittest.main(argv=['first-arg-is-ignored'], exit=False)
.
----------------------------------------------------------------------
Ran 1 test in 0.001s
OK
<class 'numpy.float64'> type인 1.9999를 np.array type으로 변환하였습니다.
<class 'numpy.float64'> type인 2.0001를 np.array type으로 변환하였습니다.
두 메서드의 값이 비슷한지 확인하는 방법
np.allclose(a,b) <- ndarray 인스턴스인 a 와 b가 값이 가까운지 확인합니다.
abs(a - b) <= (atol + rtol * abs(b))
• a, b: 비교할 두 배열 (또는 스칼라)
• abs(): 절댓값
• atol: 절대 오차 허용치 (default: 1e-08)
• rtol: 상대 오차 허용치 (default: 1e-05)
이 말은?
• 값이 커질수록 상대 오차 기준이 중요해지고
• 값이 작을수록 절대 오차 기준이 중요해짐
위 조건을 통과하면 True 를 반환한다.
이상 1고지 내용
지금부터 2고지 시작
자연스러운 코드로
가변 길이 인수(순전파)
지금까지는 함수에 입출력 변수가 하나씩인 경우만 생각해 왔습니다.
하지만 함수에따라 여러개의 변수를 입력받기도 합니다.
Ex) 덧샘 , 곱샘등
x0 x1
| |
| |
| |
└──┬────┘
▼
add
|
▼
y
이런식으로 가변길이 입출력을 처리할 수 있도록 확장을 하려고 합니다.
Function 코드 수정
변수들을 리스트에 넣어 처리하려고 한다.
인수와 반환값의 타입을 리스트로 바꾸고 필요한 변수들을 리스트에 넣는 방식으로 바꿀에정
기존 Function 로직
- x.data 에서 x 값 추출
- forward에서 $y = f(x)$ 시전
- y 값을 Variable 로 감싸 output에 저장
- output.set_creator를 통해 어떤 함수에서 값이 생성되었는지 저장
class Function:
def __call__(self, xs):
xs = [k.data for k in xs]
ys = self.forward(xs)
outputs = [Variable(as_array(y)) for y in ys]
for output in outputs:
output.set_creator(self)
self.xs = xs # backward 계산할때 필요함
self.outputs = outputs
return outputs
def forward(self, xs):
raise NotImplementedError()
def backward(self, gys):
raise NotImplementedError()
xs = [k.data for k in xs]
리스트 내포
각각의 구성된 리스트의 K 의 데이터를 꺼내고
꺼낸 원소들로 구성된 새로운 리스트 만들기
add class 구현
중요한것은 인수와 반환값이 리스트여야 한다는 것이다.
왜?
아마 다음에 있는 add를 고려하던가 아님 역전파를 고려해서 설계한거 같다.
class Add(Function):
def forward(self, xs):
x0,x1 = xs
y = x0+x1
return (y,)
xs = [Variable(np.array(2)),Variable(np.array(1))]
f = Add()
ys = f(xs)
y = ys[0]
print(y.data)
3
바로 개선
사용하는 사람 입장에서의 개선과
개발자의 입장에서 개선
class Function:
def __call__(self, *xs):
xs = [k.data for k in xs]
ys = self.forward(xs)
outputs = [Variable(as_array(y)) for y in ys]
for output in outputs:
output.set_creator(self)
self.xs = xs # backward 계산할때 필요함
self.outputs = outputs
return outputs if len(outputs)>1 else outputs[0]
def forward(self, xs):
raise NotImplementedError()
def backward(self, gys):
raise NotImplementedError()
1) 인자에 * 추가
- 은 여러개의 인자를 튜플로 받아서 args에 패킹(paking) 해주는 역할이야.
여러 인자를 받아서 하나의 튜플로 모으는것
즉, f(x0,x1) 으로 호출되면 xs = (x0,x1)이 되는거야
2) return 부분 수정
return outputs if len(outputs)>1 else outputs[0]
반환값이 1개 이상이면 첫번째 원소를 반환한다.
x0 = Variable(np.array(2))
x1 = Variable(np.array(5))
f = Add()
y = f(x0,x1)
print(y.data)
7
함수를 구현하기 쉽도록 바꾸기
class Function:
def __call__(self, *xs):
self.inputs = xs # backward 계산할때 필요함
xs_data = [k.data for k in xs]
ys = self.forward(*xs_data)
if not isinstance(ys, tuple):
ys = (ys,)
outputs = [Variable(as_array(y)) for y in ys]
for output in outputs:
output.set_creator(self)
#self.xs = xs # backward 계산할때 필요함
self.outputs = outputs
return outputs if len(outputs)>1 else outputs[0]
def forward(self, inputs):
raise NotImplementedError()
def backward(self, gys):
raise NotImplementedError()
ys 에서 리스트를 언팩(*) 해서 받고
만약 튜플이 아니라면 튜플로 변환까지 추가
따라서 add 함수를 다음과 같이 변환가능
class Add(Function):
def forward(self, x0,x1):
y = x0+x1
return y
# 함수로 구현
def add(x,y):
return Add()(x,y)
x0 = Variable(np.array(10))
x1 = Variable(np.array(10))
y = add(x0,x1)
print(y.data)
20
가변 길이 인수 (역전파)
역전파 구현
덧샘의 역전파는 입력을 그대로 흘려보내야 한다.
class Add(Function):
def forward(self, x0,x1):
y = x0+x1
return y
def backward(self, gy):
return gy, gy
# 함수로 구현
def add(x,y):
return Add()(x,y)
Variabel 클래스 수정
backward 메서드에서 두개를 흘려보내기 때문에 수정해야 된다.
class Variable:
def __init__(self, data):
if data is not None:
if not isinstance(data,np.ndarray):
print("{} type인 {}를 np.array type으로 변환하였습니다.".format(type(data),data))
data = np.array(data) # ⭐ 자동으로 ndarray로 변환!
self.data = data
self.grad = None
self.creator = None
def set_creator(self, func):
self.creator = func # ← 이 줄이 함수 내부에 잘 들어가 있어야 해
def backward(self):
if self.grad is None:
self.grad = np.ones_like(self.data)
funs = [self.creator]
while funs:
f = funs.pop()# 함수 하나 가져온다.
#x , y = f.x, f.output #함수의 입력과 출력 가져온다.
#x.grad = f.backward(y.grad)
gys = [output.grad for output in f.outputs]
gxs = f.backward(*gys)
if not isinstance(gxs, tuple):
gxs = (gxs,)
for x, gx in zip(f.inputs , gxs):
x.grad = gx
if x.creator is not None:
funs.append(x.creator)
바뀐부분 설명
#x , y = f.x, f.output #함수의 입력과 출력 가져온다.
#x.grad = f.backward(y.grad)
gys = [output.grad for output in f.output] #1
gxs = f.backward(*gys) #2
if not isinstance(gxs, tuple): #3
gxs = (gxs,)
for x, gx in zip(f.xs , gxs): #4
x.grad = gx
if x.creator is not None:
funs.append(x.creator)
- 출력 변수인 outputs에 담겨있는 있는 미분갑들을 리스트에 담습니다.
- 에서 함수 f의 역전파를 호출합니다. 이때 f.backward(*gys) 처럼
인수에 별표를 붙여 호출하여 리스트를 풀어줍니다. (리스트 언팩) - gxs가 튜플이 아니라면 튜플로 변환합니다.
- 각각의 입력 변수 x에 대해 기울기 gx를 저장하고,
만약 그 변수 x가 또 다른 함수의 출력이었다면,
그 함수도 나중에 역전파를 위해 처리 리스트에 넣는다.
Square 클래스 구현
class Square(Function):
def forward(self, x):
y = x**2
return y
def backward(self, gy):
x = self.inputs[0].data
gx = 2*x*gy
return gx
def square(x):
return Square()(x)
x = Variable(np.array(2.0))
y = Variable(np.array(2.0))
f = add(square(x),square(y))
f.backward()
print(f.data)
print(x.grad)
print(y.grad)
8.0
4.0
4.0
'AI > 밑바닥부터 시작하는 딥러닝3' 카테고리의 다른 글
밑바닥 부터 시작하는 딥러닝3 정리 2 (0) | 2025.04.15 |
---|---|
밑바닥 부터 시작하는 딥러닝 3 정리 1 (0) | 2025.04.12 |