hyo-min
2024. 6. 16. 20:41
1. 수치 미분(numerical differentiation)
- 수치 미분이란 해석적 미분을 근사치로 계산하는 방법이다.
- 경사법에서는 기울기(경사) 값을 기준으로 나아갈 방향을 정한다.
- 기울기란 무엇인지, 어떤 성질이 있는지 알아보기에 앞서, 미분부터 복습해보자
1.1. 미분
- 미분은 한순간의 변화량을 표시한 것이다.
$$
\frac{df(x)}{dx} = \lim_{{h \to 0}} \frac{f(x + h) - f(x)}{h}
$$
- 좌변은 $f(x)$의 x에 대한 미분을 나타내는 기호이다.
- 결국, x의 '작은 변화'가 함수 $f(x)$를 얼마나 변화시키느냐를 의미한다.
- 이때 시간의 작은 변화, 즉 시간을 뜻하는 h를 한없이 0에 가깝게 한다는 의미를 $\lim_{{h \to 0}}$로 나타낸다.
def numerical_diff(f, x):
h = 1e-50
return (f(x + h) - f(x)) / h
- 위 수식을 그대로 구현한 파이썬 코드이다.
- 얼핏 보면 문제가 없어 보이지만, 실제로는 개선해야 할 점이 2가지 있다.
- 첫 번째는 반올림 오차(rounding error) 문제다.
- 위 코드는 h에 가급적 작은 값을 대입하고 싶었기에 le-50이라는 값을 사용했다.
- 그런데 컴퓨터 계산에서는 너무 작은 값은 생략되어 0으로 계산하기 때문에 최종 계산 결과에 오차가 생기게 된다.
- h값으로 $10^{-4}$ 정도로 사용하면 좋은 결과를 얻을 수 있다.
- 두 번째는 함수 f의 차분과 관련된 문제다.
- 차분은 임의 두 점에서의 함수 값들의 차이를 말한다.
- 위 코드는 x + h 와 x 사이의 함수 f의 차분을 계산하고 있지만
- h를 무한히 0으로 좁히는 것이 불가능해 생기는 오차가 생긴다.
- 이 오차를 줄이기 위해 (x + h)와 (x - h)일 때의 함수 f의 차분을 계산하는 방법을 쓰기도 한다.
- 첫 번째는 반올림 오차(rounding error) 문제다.
- 그럼 두 개선점을 적용해 수치 미분을 다시 구현해보자.
def numerical_diff(f, x):
h = 1e-4
return (f(x + h) - f(x - h)) / (2 * h)
1.2. 편미분
$$
f(x_{0}, x_{1}) = x_{0}^{2} + x_{1}^{2}
$$
- 변수가 여럿인 함수에 대한 미분
- 이 식은 파이썬 코드로 다음과 같이 구현할 수 있다.
def function(x):
return x[0]**2 + x[1]**2
- 이 때 인수 x는 넘파이 배열이라고 가정한다.
- 이 코드는 넘파이 배열의 각 원소를 제곱하고 그 합을 구할뿐인 간단한 구현이다.
- 이제 편미분을 구해보자
# x0 = 3, x1= 4일 때 x0에 대한 편미분
def function_tmp1(x0):
return x0*x0 + 4.0**2.0
print(numerical_diff(function_tmp1, 3.0))
# 출력 : 6.00000000000378
# x0 = 3, x1= 4일 때 x1에 대한 편미분
def function_tmp2(x1):
return 3.0**2.0 + x1*x1
print(numerical_diff(function_tmp2, 4.0))
# 출력 : 7.999999999999119
- 위 코드에서는 $x_{0}$과 $x_{1}$의 편미분을 변수별로 따로 계산했다.
- 이번에는 $x_{0}$과 $x_{1}$의 편미분을 동시에 계산해보자.
def numerical_gradient(f, x):
h = 1e-4 # 0.0001
grad = np.zeros_like(x) # x와 형상이 같은 배열을 생성
for idx in range(x.size):
tmp_val = x[idx]
x[idx] = tmp_val + h
fxh1 = f(x)
x[idx] = tmp_val - h
fxh2 = f(x)
grad[idx] = (fxh1 - fxh2) / (2*h)
x[idx] = tmp_val # 값 복원
return grad
print(numerical_gradient(function, np.array([3.0, 4.0])))
print(numerical_gradient(function, np.array([0.0, 2.0])))
print(numerical_gradient(function, np.array([3.0, 2.0])))
"""
출력
[6. 8.]
[0. 4.]
[6. 4.]
"""
2. 기울기
- $f(x_{0}, x_{1}) = x_{0}^{2} + x_{1}^{2}$의 기울기 결과에 마이너스를 붙인 벡터를 그래프로 표현하면 이렇게 된다.
- 위 그래프를 보면 기울기는 함수의 최솟값을 가르키고 있다. 또한 최솟값에서 멀어질수록 화살표의 크기가 커진다.
- 즉, 기울기가 가르키는 쪽은 각 장소에서 함수의 출력 값을 가장 크게 줄이는 방향이다.
- 이건 중요한 포인트이니 확실히 기억하자.
2.1. 경사법(경사 하강법)
- 신경망은 최적의 매개변수(가중치와 편향)를 학습 시에 찾아야 한다.
- 여기에서 최적이란 손실 함수가 최솟값이 될 때의 매개변수 값이다.
- 이런 상황에서 기울기를 잘 이용해 함수의 최솟값을 찾으려는 것이 바로 경사법이다.
- 경사법은 형 위치에서 기울어진 방향으로 일정 거리만큼 이동한다.
- 그런 다음 이동한 곳에서도 마찬가지로 기울기를 구하고, 또 그 기울어진 방향으로 나아가기를 반복한다.
- 이렇게 해서 함수의 값을 점차 줄이는 것이 경사법이다.
$$
x_{0} = x_{0} - \eta \frac{\partial{f}}{\partial{x_{0}}}
$$
$$
x_{1} = x_{1} - \eta \frac{\partial{f}}{\partial{x_{1}}}
$$
- 위 수식의 $\eta$(eta)는 갱신하는 양을 나타낸다. 이를 신경망 학습에서는 학습률(learning rate)이라고 한다.
- 한 번의 학습으로 얼마만큼 학습해야 할지, 즉 매개변수 값을 얼마나 갱신하냐를 정하는 것이 학습률이다.
- 위 수식은 1회에 해당하는 갱신이고, 이 단계를 반복하며 서서히 함수의 값을 줄이는 것
def gradient_descent(f, init_x, lr=0.01, step_num=100):
x = init_x
for i in range(step_num):
grad = numerical_gradient(f, x)
x -= lr * grad
return x
f = lambda x: x[0]**2 + x[1]**2
init_x = np.array([-3.0, 4.0])
print(gradient_descent(f, init_x=init_x, lr=0.1, step_num=100))
# 출력 : [-6.11110793e-10 8.14814391e-10]
- 코드로 구현한 경사하강법이다.
- f : 최적화 하려는 함수, init_x : 초깃값, lr : 학습률, step_num : 경사법에 따른 반복 횟수
- 함수의 기울기는 위에서 정의한 numerical_gradient(f, x)로 구하고, 그 기울기에 학습률을 곱한 값으로 갱신하는 처리를 step_num 만큼 반복한다.
- 초깃값을 (-3.0, 4,0)으로 설정한 후 경사법을 사용한 최종 결과는 (-6.1e-10 8.1e-10)으로, 거의 (0, 0)에 가까운 결과가 나온다.
- 실제로 최솟값은 (0, 0)이므로 경사법으로 거의 정확한 결과를 얻었다.
2.2. 신경망에서의 기울기
- 신경망 학습에서도 가중치 매개변수에 대한 손실 함수의 기울기를 구해야 한다.
- 예를 들어 형상 : 2 x 3, 가중치 : W, 손실 함수 : L인 신경망의 경사는 $\frac{\partial L}{\partial W}$ 로 나타낼 수 있다.
- 수식으로는 다음과 같다.
$$
W = \begin{pmatrix}
W_{11} & W_{12} & W_{13} \
W_{21} & W_{22} & W_{23}
\end{pmatrix}
$$
$$
\frac{\partial L}{\partial W} = \begin{pmatrix}
\frac{\partial L}{\partial W_{11}} & \frac{\partial L}{\partial W_{12}} & \frac{\partial L}{\partial W_{13}} \
\frac{\partial L}{\partial W_{21}} & \frac{\partial L}{\partial W_{22}} & \frac{\partial L}{\partial W_{23}}
\end{pmatrix}
$$
- $\frac{\partial L}{\partial W}$의 각 원소는 각각의 원소에 관한 편미분이다.
- 여기서 중요한 점은 $\frac{\partial L}{\partial W}$의 형상이 W와 같다는 것이다.
- 그럼 간단한 신경망을 예로 들어 실제로 기울기를 구하는 코드를 구현해보자
class SimpleNet:
def __init__(self):
self.W = np.random.randn(2,3) # 정규분포로 초기화
def predict(self, x):
return np.dot(x, self.W)
def loss(self, x, t):
z = self.predict(x)
y = softmax(z)
loss = cross_entropy_error(y, t)
return loss
x = np.array([0.6, 0.9])
t = np.array([0, 0, 1])
net = SimpleNet()
f = lambda w: net.loss(x, t)
dW = numerical_gradient(f, net.W)
print(dW)
"""
출력
[[ 0.48671341 0.07941803 -0.56613144]
[ 0.73007011 0.11912705 -0.84919716]]
"""
- SimpleNet 클래스는 형상이 2 x 3인 가중치 매개변수 하나를 인스턴스 변수로 갖는다.
- predict(x)는 예측을 수행하는 메서드
- loss(x, t)는 손실 함수의 값을 구하는 메서드이다.
- x는 인수, t는 정답 레이블이다.
- 기울기는 numerical_gradient(f, x)를 써서 구하고, f의 인수 W는 더미로 만든 것이다.
- numerical_gradient(f, x)는 내부에서 f(x)를 실행하는데, 그와의 일관성을 위해 f(W)를 lambda로 정의한 것
- dW는 numerical_gradient(f, net.W)의 결과로 그 형상은 2 x 3의 2차원 배열이다.
- dW의 $ \frac{\partial L}{\partial W} $의 $ \frac{\partial L}{\partial W_{11}} $은 대략 0.48이다.
- 이는 w11을 h만큼 늘리면 손실 함수의 값은 0.48h만큼 증가한다는 의미
- 마찬가지로 $ \frac{\partial L}{\partial W_{23}} $은 대략 -0.84이니 w23을 h만큼 늘리면 손실 함수의 값은 0.84h만큼 감소한다는 것이다.
- 이러한 기울기 정보는 신경망 학습 과정에서 매우 중요한 역할을 한다.
- 기울기를 이용해 가중치를 업데이트하면 손실 함수의 값을 최소화할 수 있기 때문
- 기울기가 양수라면 가중치를 감소시키고, 음수라면 가중치를 증가시키는 방향으로 조정한다.