본문 바로가기

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

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

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 로직

  1. x.data 에서 x 값 추출
  2. forward에서 $y = f(x)$ 시전
  3. y 값을 Variable 로 감싸 output에 저장
  4. 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)
  1. 출력 변수인 outputs에 담겨있는 있는 미분갑들을 리스트에 담습니다.
  2. 에서 함수 f의 역전파를 호출합니다. 이때 f.backward(*gys) 처럼
    인수에 별표를 붙여 호출하여 리스트를 풀어줍니다. (리스트 언팩)
  3. gxs가 튜플이 아니라면 튜플로 변환합니다.
  4. 각각의 입력 변수 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