밑바닥 부터 시작하는 딥러닝3 정리 2
저번내용
2025.04.12 - [AI/밑바닥부터 시작하는 딥러닝3] - 밑바닥 부터 시작하는 딥러닝 3 정리 1~4단계
밑바닥 부터 시작하는 딥러닝 3 정리 1~4단계
import numpy as npclass Variable: def __init__(self,data): self.data = data설명: class는 객체(인스턴스)를 만들기위한 설계도 data(속성)과 def(메소드)동작으로 구성되어있음인스턴스: class 를 기반으로 만든 객체ini
bbakgosu.tistory.com
역전파 설명 까지
import numpy as np
import matplotlib.pyplot as plt
class Variable:
def __init__(self,data):
self.data = data
class Function:
def __call__(self, x):
x = x.data
y = self.forward(x) # 구체적인 계산은 forward 메서드에서 한다.
output = Variable(y)
return output
def forward(self, x):
raise NotImplementedError()
def numerical_diff(f, x, eps = 1e-4):
x0 = Variable(x.data - eps)
x1 = Variable(x.data + eps)
y0 = f(x0)
y1 = f(x1)
return (y1.data - y0.data) / (2*eps)
수동 역전파
Variable 에 미분값을 저장하도록 변경
class Variable:
def __init__(self,data):
self.data = data
self.grad = None
Function 클래스 변경
미분을 계산하는 backward 메서드 추가
forward 메서드 호출시 건내받은 Varaiable 인스턴스 저장
class Function:
def __call__(self, x):
self.x = x # backward 계산할때 필요함
x = x.data
y = self.forward(x) # 구체적인 계산은 forward 메서드에서 한다.
output = Variable(y)
self.output = output
return output
def forward(self, x):
raise NotImplementedError()
def backward(self, gy):
raise NotImplementedError()
def numerical_diff(f, x, eps = 1e-4):
x0 = Variable(x.data - eps)
x1 = Variable(x.data + eps)
y0 = f(x0)
y1 = f(x1)
return (y1.data - y0.data) / (2*eps)
왜 x = x.data 인가? self.x 로 사용하지 않는 이유는
차이점은 self.x 로 하면 인스턴스로 저장되지만 여기서는 필요하지 않음
이후 input이란 인스턴스로 저장함.
Square, Exp 추가구현
backward 에 맟추어 미분을 구현한다.
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
$ \frac{dy}{dx} = x \cdot 2 $
이렇기 때문에(코드상으로) ${gx} = 2 \cdot x \cdot {gy}$
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
$ \frac{dy}{dx} = \exp(x) $
따라서(코드상으로) ${gx} = \exp(x) \cdot {gy}$
A = Square()
B = Exp()
C = Square()
x = Variable(np.array(0.5))
a = A(x)
b = B(a)
y = C(b)
print(y.data)
1.648721270700128
y.grad = np.array(1.0) # 또는 float64
b.grad = C.backward(y.grad)
a.grad = B.backward(b.grad)
x.grad = A.backward(a.grad)
print(x.grad)
3.297442541400256
역전파 자동화
Define - by - Run
딥러닝에서 수행하는 계산들을 계산시점에 연결하는 방식 - 동적 계산 그래프
변수와 함수의 관계
변수 관점에서 함수는 Creator
함수 입장에서 입력 변수는 input 출력변수는 output
이를 코드로 녹일것.
class Variable:
def __init__(self, data):
self.data = data
self.grad = None
self.creator = None
def set_creator(self, func):
self.creator = func # ← 이 줄이 함수 내부에 잘 들어가 있어야 해
class Function:
def __call__(self, x):
self.x = x # backward 계산할때 필요함
x = x.data
y = self.forward(x) # 구체적인 계산은 forward 메서드에서 한다.
output = Variable(y)
output.set_creator(self)
self.output = output
return output
def forward(self, x):
raise NotImplementedError()
def backward(self, gy):
raise NotImplementedError()
output.set_creator(self) 에 self 가 들어가는 이유?
self는 지금 실행중인 Function인스턴스를 가리킨다.
f = add()
z = f(x) 이럴때 f가 바로 self이다.
연산을 수행한 주체가(메서드, 함수 객체)를 self로 전달.
x = Variable(np.array(2.0))
f = Square() # Square는 Function을 상속받은 클래스
y = f(x) # f.call 호출됨
따라서
y.set_creator(f) 가 실행되고
그리고 f.input을 통해 x를 알수 있어
그걸 기반으로f.backword()를 호출해서 x로 미분값 전달
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
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
A = Square()
B = Exp()
C = Square()
x = Variable(0.5)
a = A(x)
b = B(a)
y = C(b)
print(y.data)
# y는 C에 의해 생성됨
assert y.creator == C
# C에 입력된 값은 b
assert y.creator.x == b
# b는 B에 의해 생성됨
assert b.creator == B
# B에 입력된 값은 a
assert b.creator.x == a
# a는 A에 의해 생성됨
assert a.creator == A
# A에 입력된 값은 x
assert a.creator.x == x
1.648721270700128
y.grad = np.array(1.0)
C = y.creator
b = C.x
b.grad = C.backward(y.grad)
- 함수 가져오기
- 함수의 입력 가져오기
- 함수의 backward 메서드 호출해서 기울기 넣기
이 과정을 자동화를 시키면 된다.
class Variable:
def __init__(self, data):
self.data = data
self.grad = None
self.creator = None
def set_creator(self, func):
self.creator = func # ← 이 줄이 함수 내부에 잘 들어가 있어야 해
def backward(self):
f = self.creator
if f is not None:
x = f.x
x.grad = f.backward(self.grad)
x.backward()
A = Square()
B = Exp()
C = Square()
x = Variable(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
재귀에서 반복문 구조로
재귀로 바꿔야 하는 이유?
나중에 식이 복잡해지면 메모리가 터질 수 있기 때문에
class Variable:
def __init__(self, data):
self.data = data
self.grad = None
self.creator = None
def set_creator(self, func):
self.creator = func # ← 이 줄이 함수 내부에 잘 들어가 있어야 해
def backward(self):
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)
if x.creator is not None:
funs.append(x.creator)
이 코드가 존재하는 이유?
변수 X가 어떤 연산 Function 에서 나왔으면 그 Function도 역전파 대상으로
큐에 넣자 <- 이게 없으면 안돌아 가겠넹
A = Square()
B = Exp()
C = Square()
x = Variable(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
편의성 추가
파이썬 함수로 이용하기
Squaer 이란 함수를 사용하기 위해선
x = Variable(np.array(3.0))
f= Square()
y = f(x)
이와 같이 사용해야 하는데 번거롭다 따라서 파이썬 함수처럼 사용하기 위해서
def square(x):
f= Square()
return f(x)
def exp(x):
f = Exp()
return f(x)
이런식으로 선한하면 파이썬 함수처럼 사용이 가능합니다.
x = Variable(0.5)
a = square(x)
b = exp(a)
y = square(b)
y.grad = np.array(1.0)
y.backward()
print(x.grad)
3.297442541400256
x = Variable(0.5)
y = square(exp(square(x)))
y.grad = np.array(1.0)
y.backward()
print(x.grad)
3.297442541400256
backward 메서드 간소화
위에 생성한 코드에서 y.grad = np.array(1.0)을안해도 되도록 변경
class Variable:
def __init__(self, data):
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)
if self.grad is None:
self.grad = np.ones_like(self.data)
의미는 self.grad가 설정되지 않았을때.
self.grad에 self.data 와 타입이 똑같게 1로 채워준다.
self.grad에 self.data와 동일한 shape과 dtype을 가지는 배열을 생성하고,
값은 모두 1로 채운다.
x = Variable(0.5)
y = square(exp(square(x)))
y.backward()
print(x.grad)
3.297442541400256
ndarray 만 취급하기
사용자가 의도하지 않게 다른 데이터 타입을 사용할 수 도 있으니
Variable 에 ndarray 이외의 데이터 타입이 오면 오류가 나도록 설정
class Variable:
def __init__(self, data):
if data is not None:
if not isinstance(data,np.ndarray):
raise TypeError("{}은(는) 지원하지 않습니다.".format(type(data)))
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)
x = Variable(1)
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[31], line 1
----> 1 x = Variable(1)
Cell In[30], line 5, in Variable.__init__(self, data)
3 if data is not None:
4 if not isinstance(data,np.ndarray):
----> 5 raise TypeError("{}은(는) 지원하지 않습니다.".format(type(data)))
8 self.data = data
9 self.grad = None
TypeError: <class 'int'>은(는) 지원하지 않습니다.
추가적으로 데이터가 항상 ndarray 인스턴스가 아닐 수 있습니다.
다음과 같은 상황이라 가정해 보겠습니다.
x = np.array(1.0)
y = x**2
print(type(x),x.ndim)
print(type(y))
<class 'numpy.ndarray'> 0
<class 'numpy.float64'>
x 는 0차원의 ndarray이지만 제곱했더니 (스칼라로판단하는듯) float64,32로 변환이 됩니다.
따라서 다음과 같은 함수를 준비해서 Function 에 넣어야 합니다.
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()
다음에 할거
테스트
편의성 증가