AI/밑바닥부터 시작하는 딥러닝3

밑바닥 부터 시작하는 딥러닝3 정리 2

BBakGoSu 2025. 4. 15. 00:46

저번내용

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)
  1. 함수 가져오기
  2. 함수의 입력 가져오기
  3. 함수의 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()

다음에 할거

테스트

편의성 증가