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

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

BBakGoSu 2025. 4. 12. 23:40
import numpy as np
class Variable:
    def __init__(self,data):
        self.data = data

설명: class는 객체(인스턴스)를 만들기위한 설계도 data(속성)과 def(메소드)동작으로 구성되어있음
인스턴스: class 를 기반으로 만든 객체

init의 역할: 인스턴스를 만들때 호출되는 초기화 메서드(예를들어 생산년도 같은 느낌)

def: 메서드: class 안에서 정의되어있는 동작

data: 매개변수 외부의 값을 전달받는 값을 저장하는 변수

self: 인스턴스 자기 자신
.data: 속성 : 인스턴스가 가지고 있는 데이터/상태를 표현하는 변수 쉽게 말해 객체가 "가지고 있는 정보"
dict이라는 내부 딕셔너리에 저장되어있음

따라서 Variable 는 새로운 변수를 만드는 클래스

data = np.array(1.0)
x = Variable(data)
print(x.data)
1.0

넘파이의 다차원 배열 클래스 numpy.ndarray

Vasiable 클래스는 넘파이의 다차원 배열 클래스만 취급합니다.

x.data = np.array(2.0)
print(x.data)
2.0

넘파이의 다차원 배열

다차원 배열에서 원소의 순서에는 방향이 있고 이 방향을 차원 혹은 축이라고 합니다.

스칼라 백터 행렬

0 차원 1차원 2차원


차원 과 축의 차이

차원 (dimension):
데이터가 몇 단계로 중첩되어 있는지를 말한다.
축(axis):
각 차원에서의 방향을 뜻한다. axis = 0 은 행 방향(새로) axis = 1은 열방향(가로)

백터의 차원(선형대수의 관점):
[1,2,3,] 은 3차원 백터(3개의 성분 -> 수학적 의미의 3차원)

배열의 차원(프로그래밍의 관점)
[1,2,3] -> 1차원"배열"
[[1].[2],[3]] -> 2차원 배열/ 3차원 백터

함수

class Function:
    def __call__(self, input):
        x = input.data #데이터를 꺼낸다.
        y = x**2 #데이터를 계산한다.
        output = Variable(y) # 다시 상자 안에 넣는다.
        return output

call 메서드는 파이썬의 특수 메서드
f = Function() 이런식으로 변수의 함수의 인스턴스를 대입해놓고 나중에 f(...)의 형태로 호출이 가능

x = Variable(np.array(10))
f = Function()
y = f(x)

print(type(y)) #객체의 class를 알려준다.
print(y.data)
<class '__main__.Variable'>
100

구현한 함수는 입력값의 제곱으로 고정되어 있는 함수입니다. 따라서 Square이란 이름이 더 알맞는다
따라서

  • Function 클래스는 기반 클래스로서, 모든 함수에 공통되는 기능을 구현
  • 구체적인 함수는 Function 클래스를 상속한 클래스에서 구현합니다.
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()

call 에서 하는것은
매개변수input 에서 데이터 속성을 x에 저장 (Variable에서 데이터 찾기)
계산하고 다시 Variable에 포장하기 두가지일
그리고계산으 forward 메서드를 호출

forward 메서드의 구체적인 로직은 하위클래스에서 구현합니다.

class Square(Function):
    def forward(self, x):
        return x**2

Square 은 Function을 상속하기 때문에 call 메서드는 계승된다.

상속이란?
Square는 Function이란부모 클래스(슈퍼클래스)의 특성과 기능을 물려받은
자식 클래스(서브 클래스)이다.

기존 클래스의 기능을 재사용하거나 확장할 수 있게 해주는 oop의 개념이다.
따라서 call이 없어도
f = Square()
result = f(2.0) # 내부적으로 f.forward(2.0) 호출됨
이런식으로 사용이 가능


따라서 Square()의 하는 일은
forward메서드를 오버라이딩(재정의) 한것이다.

x = Variable(np.array(10))
f = Square()
y = f(x)

print(y.data)
print(type(y))
100
<class '__main__.Variable'>

함수의 연결

class Exp(Function):
    def forward(self, x):
        return np.exp(x)

Function 을 상속받은 함수들은 전부 call 기능을 상속받았기 때문에

연이어 사용이 가능하다.
따라서 다음과 같은 연산이 가능하다.
$$
y = \left( e^{x^2} \right)^2
$$

A = Square()
B = Exp()

x = Variable(np.array(0.5))
y = A(B(A(x)))
print(y.data)
1.648721270700128

수치 미분

📘 미분이란?

  • 어떤 함수 ( f(x) )에서, ( f(x+h) ) 와 ( f(x) ) 사이의 변화율을 측정하는 것
  • 즉, y가 얼마나 변했는지에 비해 x가 얼마나 변했는지를 보는 것

💡 평균 변화율:

[
\frac{f(x+h) - f(x)}{h}
]


⏱ 순간 변화율 (h를 0으로 보낸다!)

[
\lim_{h \to 0} \frac{f(x+h) - f(x)}{h} = f'(x)
]


이게 바로 미분의 직관적 의미야!
변화율을 구하는 도구이자, 그래프의 기울기를 측정하는 도구!

import numpy as np
import matplotlib.pyplot as plt

# ✅ 함수 정의
def f(x):
    return x**2

# ✅ 도함수 (미분값) 정의
def df(x):
    return 2*x  # f(x) = x^2 의 도함수는 f'(x) = 2x

# ✅ 기준이 되는 지점
x0 = 1.5                  # 접점을 x = 1.5로 설정
y0 = f(x0)                # 해당 지점의 함수값
slope = df(x0)            # 해당 지점에서의 기울기

# ✅ 접선의 함수 정의 (기울기와 점 이용한 직선의 방정식)
def tangent_line(x):
    return slope * (x - x0) + y0

# ✅ x값 범위 설정
x_vals = np.linspace(0, 3, 300)
y_vals = f(x_vals)
tangent_vals = tangent_line(x_vals)

# ✅ 그래프 그리기
plt.figure(figsize=(8, 6))
plt.plot(x_vals, y_vals, label="f(x) = x²", color="blue")  # 원 함수
plt.plot(x_vals, tangent_vals, label=f"Tangent at x = {x0}", linestyle='--', color="orange")  # 접선
plt.scatter([x0], [y0], color="red", label="Point of Tangency")  # 접점
plt.title("Function and Tangent Line")
plt.xlabel("x")
plt.ylabel("y")
plt.axhline(0, color='gray', lw=0.5)
plt.axvline(0, color='gray', lw=0.5)
plt.legend()
plt.grid(True)
plt.tight_layout()
plt.show()

png

수치미분

컴퓨터는 극한을 처리할 수 없기 때문에 h(변화량)을 엄청나게 작은 값으로 설정하고
순간변화율을 근사합니다. 이때 오차가 발생하는데 가장 적은 오차는 "중앙차분 입니다."

import numpy as np
import matplotlib.pyplot as plt

# 함수 정의: f(x) = x^2
def f(x):
    return x**2

# 도함수 정의: f'(x) = 2x
def df(x):
    return 2*x

# 중심점과 h 설정
x0 = 1.5
h = 1.0  # 차이가 크게 보이도록 설정

# 샘플 포인트 계산
x_minus = x0 - h
x_plus = x0 + h
y_minus = f(x_minus)
y_plus = f(x_plus)
y0 = f(x0)

# 전진 차분 근사 기울기
fwd_slope = (f(x0 + h) - f(x0)) / h

# 중앙 차분 근사 기울기
central_slope = (f(x0 + h) - f(x0 - h)) / (2 * h)

# 실제 미분 값
true_slope = df(x0)

# 직선 함수 정의 (기준점과 기울기로 직선 생성)
def line(x, x0, y0, slope):
    return slope * (x - x0) + y0

# 그래프 x 값 범위
x_vals = np.linspace(0, 3, 300)
y_vals = f(x_vals)

# 각 선 그리기
true_line = line(x_vals, x0, y0, true_slope)
fwd_line = line(x_vals, x0, y0, fwd_slope)
central_line = line(x_vals, x0, y0, central_slope)

# 그래프 그리기
plt.figure(figsize=(10, 6))
plt.plot(x_vals, y_vals, label="f(x) = x²", color="black")
plt.plot(x_vals, true_line, label="True Derivative", color="green", linewidth=2)
plt.plot(x_vals, fwd_line, label="Forward Difference", color="red", linestyle='--')
plt.plot(x_vals, central_line, label="Central Difference", color="blue", linestyle='--')

# 점 찍기
plt.scatter([x0], [y0], color="green", label="x₀")
plt.scatter([x_minus], [y_minus], color="purple", label="x₀ - h")
plt.scatter([x_plus], [y_plus], color="orange", label="x₀ + h")

# 시각화 설정
plt.title("Forward vs Central Difference vs True Derivative (h = 1.0)")
plt.xlabel("x")
plt.ylabel("y")
plt.axhline(0, color='gray', lw=0.5)
plt.axvline(0, color='gray', lw=0.5)
plt.legend()
plt.grid(True)
plt.tight_layout()
plt.show()

png

📘 중앙차분법 (Central Difference Method)

중앙차분법(Central Difference Method)은 함수 ( f(x) )의 도함수를 근사하는 수치 해석 기법 중 하나입니다.
이 방법은 ( x )의 좌우에서 동일한 간격으로 떨어진 두 점 ( x+h ), ( x-h )에서의 함수 값을 이용해
기울기를 계산합니다.

전진 차분(forward difference)이나 후진 차분(backward difference)보다 오차가 더 작고,
함수의 대칭성을 이용하여 더 정확한 근사값을 제공합니다.

📐 공식

$$
f'(x) \approx \frac{f(x + h) - f(x - h)}{\boldsymbol{2h}}
$$

  • 분모가 ( \boldsymbol{2h} )인 것이 핵심 포인트입니다.
  • 함수의 기울기를 중심점 ( x ) 기준으로 양쪽에서 접근하므로, 대칭성과 정확성이 향상됩니다.

✅ 특징

  • 정확도: 2차 정확도 (오차: ( O(h^2) ))
  • 전진/후진 차분보다 정확함
  • 테일러 급수를 양쪽에서 사용하여 유도됨
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)

numerical_diff 매개변수: f , x, eps
x0 x의 데이터에서 아주작은수를 빼고 다시 포장
x1 x의 데이터에서 아주작은수를 더하고 다시포장
이후 미분 진행

f= Square()
x = Variable(np.array(2.0))
dy = numerical_diff(f,x)

print(dy)
4.000000000004

합성함수의 미분
$
y = \left( e^{x^2} \right)^2
$
아까 이 식을 미분하는법

def f(x):

    A = Square()
    B = Exp()
    y = A(B(A(x)))
    return y


x = Variable(np.array(0.5))
dy = numerical_diff(f,x)

print(dy)
3.2974426293330694

함수 또한 객체이기때문에 다른 함수에 인수로 전달이 가능합니다.
하지만 수치미분은 문제가 있습니다.

수치미분의 문제

  1. 계산량
    변수가 여러개인 계산을 미분할 경우 변수 각각을 미분해야 하기 때문입니다.
    이를 해결하기위해 등장한 개념이 역전파 입니다.
  2. 작은 오차가 있음
    오차가 있는 이유는 자리수 누락때문이다.

역전파 이론

📘 체인 룰 (Chain Rule)

합성함수 $y = f(g(x))$의 도함수는 다음과 같이 계산합니다:

$$
\frac{dy}{dx} = \frac{dy}{du} \cdot \frac{du}{dx}
$$

여기서,

  • $u = g(x)$
  • $y = f(u)$

즉, 바깥 함수의 미분안쪽 함수의 미분을 곱하는 방식입니다.


📌 예제

다음 함수를 미분해보자:

$$
y = \sin(x^2)
$$

  1. 합성함수로 표현:
    • 바깥 함수: $f(u) = \sin(u)$
    • 안쪽 함수: $u = x^2$
  2. 각각 미분:
    • $\frac{df}{du} = \cos(u)$
    • $\frac{du}{dx} = 2x$
  3. 체인 룰 적용:

$$
\frac{dy}{dx} = \frac{df}{du} \cdot \frac{du}{dx} = \cos(x^2) \cdot 2x
$$


✅ 정리

최종적으로,

$$
\boxed{\frac{d}{dx}\sin(x^2) = 2x \cos(x^2)}
$$

🎯 계산 그래프와 역전파 예제

✅ 순전파 (Forward)

함수:
$$
y = (x_1 + x_2) \cdot x_3
$$

계산 그래프:

   x1     x2     x3
    │      │      │
    └──┬───┘      │
      Add       │
       │         │
       └───┬─────┘
          Mul
           │
           y

Add = $x_1 + x_2$
Mul = $(x_1 + x_2) \cdot x_3$
즉, $y = \text{Add} \cdot x_3$


🔁 역전파 (Backward)

역전파는 출력 $y$에 대한 각 변수의 미분 $\frac{\partial y}{\partial (\cdot)}$를 계산하는 과정이야.

  1. 시작점:
    $$
    \frac{\partial y}{\partial y} = 1
    $$
  2. Mul 노드 역전파:
    $$
    \frac{\partial y}{\partial \text{Add}} = \frac{\partial y}{\partial y} \cdot \frac{\partial y}{\partial \text{Add}} = 1 \cdot x_3
    $$
    $$
    \frac{\partial y}{\partial x_3} = \frac{\partial y}{\partial y} \cdot \frac{\partial y}{\partial x_3} = 1 \cdot \text{Add}
    $$
  3. Add 노드 역전파:
    $$
    \frac{\partial y}{\partial x_1} = \frac{\partial y}{\partial \text{Add}} \cdot \frac{\partial \text{Add}}{\partial x_1} = x_3 \cdot 1 = x_3
    $$
    $$
    \frac{\partial y}{\partial x_2} = \frac{\partial y}{\partial \text{Add}} \cdot \frac{\partial \text{Add}}{\partial x_2} = x_3 \cdot 1 = x_3
    $$

📌 요약

각 변수에 대한 역전파 결과:

  • $\frac{\partial y}{\partial x_1} = x_3$
  • $\frac{\partial y}{\partial x_2} = x_3$
  • $\frac{\partial y}{\partial x_3} = x_1 + x_2$

즉, 왼쪽으로 흐르는 모든 값은 $y$에 대한 각 입력의 미분값이다.

 


다음 할것 자동 역전파 코드 구현