728x90
반응형
SMALL

앞서, 경사하강법을 위한 미분과 편미분을 알아보았습니다.

2024.01.29 - [Programming/Deep Learning] - [Python/DeepLearning] #9. 수치 미분과 경사하강법(상)

이에 이어, 경사법(경사하강법)의 원리와 신경망에서의 기울기에 대한 신경망 학습을 알아보도록 하겠습니다.

 

경사법(경사하강법)

머신러닝 문제 대부분은 학습 단계 "fit"에서 최적의 매개변수를 찾아냅니다.

신경망 역시 최적의 매개변수를 학습 시에 찾아야 하는데요, 이때의 매개변수는 가중치와 편향을 이야기합니다.

여기서 최적이란 손실 함수가 최솟값이 될 때의 매개변수 값을 의미하는데요, 일반적인 문제의 손실함수는 매우 복잡합니다.

왜냐하면 매개변수 공간(범위)이 광대하여 어디가 최솟값이 되는 곳인지를 알아내기가 힘들기 때문이죠.

이런 상황에서 손실 함수의 기울기를 잘 이용해 함수의 최솟값(또는 가능한 한 작은 값)을 찾으려는 방법이 경사 하강법 입니다.

여기서 미분이 활용됩니다! 각 지점에서 함수의 결괏값을 낮추는 방안을 제시하는 지표가 기울기이기 때문이죠. 하지만 어떤 특정 지점의 기울기가 가리키는 곳에 정말 함수의 최솟값이 있는지, 즉 그쪽으로 정말로 나아갈 방향인지는 보장할 수 없습니다.

실제로 복잡한 함수에서는 기울기가 가리키는 방향에 최솟값이 없는 경우가 대부분입니다.

 

주의할 점은 경사법은 기울기가 0인 장소를 찾지만, 그것이 반드시 최솟값이라고 할 수는 없습니다.

극솟값이나 안장점이 될 수도 있는데요, 찌그러진 모양의 함수라면 (사실 대부분이 찌그러진 함수) 함수의 그래프가 평평한 곳으로 파고들면서 고원이라고 하는, 학습이 진행되지 않는 정체기에 빠질 수가 있습니다.

즉, 기울어진 방향이 꼭 최솟값을 가리키는 것은 아니지만 그 방향으로 가야 함수의 값을 줄일 수 있습니다.

그래서 최솟값이 되는 장소를 찾는 문제에서는 기울기 정보를 단서로 나아가야 할 방향을 정해야 합니다.

 

경사법 원리

경사법은 현 위치에서 기울어진 방향으로 일정 거리만큼 이동합니다.

그런 다음 이동한 곳에서도 마찬가지로 기울기를 구하고, 또 그 기울어진 방향으로 계속 나아가기를 반복합니다.

이렇게 해서 함수의 결괏값을 줄이는 것이 경사법(gradient method) 입니다. 경사법은 머신러닝 모델을 최적화하는데 흔히 쓰이는 방법입니다. 특히나 저희가 해볼 신경망 학습에는 경사법을 많이 사용합니다. 경사법을 수식으로 나타내면 다음과 같습니다.

위의 식에서 𝜂(eta. 에타)는 갱신하는 양을 나타냅니다.

이를 신경망에서는 학습률(learning rate)라고 볼 수 있는데요. 한 번의 학습으로 얼마큼 학습할지, 즉 다음 지점을 얼마만큼 이동할지에 대한 거리가 됩니다. 위 식은 1회에 해당하는 갱신이고, 이 단계를 계속 반복합니다.

즉, 위 식 처럼 변수의 값을 갱신하는 단계를 여러 번 반복하면서 서서히 함수의 값을 줄이는 것입니다. 또 여기서는 변수가 2개인 경우를 이야기했지만, 변수가 늘어도 늘 같은 편미분 값으로 갱신하게 됩니다.

또한 학습률 𝜂 값은 미리 특정 값으로 고정시켜 놓아야 합니다. 0.01이나 0.001 같이 정해두게 되는데, 일반적으로 이 값이 너무 크거나 작으면 '좋은 장소'를 찾아갈 수 없습니다.

신경망 학습에서는 보통 이 학습률 값을 변경하면서 올바르게 학습하고 있는지를 확인하면서 진행합니다. 파이썬 코드로 위의 수식을 나타내 보겠습니다.

# 경사하강법 구현
# 편미분 대상 함수, 초기 x 좌표, lr, step_num, 
def gradient_descent(f, init_x, lr=0.01, step_num=100):
  x = init_x

  for _ in range(step_num):
    # step_num 만큼 반복 하면서 기울기 배열( 기울기 벡터 )를 구해서 x를 업데이트

    # 1. 기울기 배열을 구하자
    grad = numerical_gradient(f, x)
    
    x -= lr * grad
    print("기울기 : {} \t x 좌표 : {}".format(grad, x))
  return x

첫 번째 인수 f는 최적화할 함수, init_x는 초깃값, lr은 learning_rate로써 학습률, step_num은 경사법에 다른 반복 횟수를 뜻합니다.

함수의 기울기는 미분 함수 numerical_gradient로 구하고, 그 기울기에 학습률을 곱한 값으로 갱신하는 처리를 step_num번 for문을 통해 반복합니다.

이 함수를 사용하면 함수의 극솟값을 구할 수 있고 잘하면 최솟값을 구해볼 수도 있습니다.

gradient_descent(function_2, np.array([3.0, 4.0]), lr=0.1, step_num=100)

 

거의 0에 가까운 최솟값이 확인되었습니다.

실제로 진정한 최솟값은 (0, 0) 이므로 경사법으로 거의 정확한 결과를 얻었다고 볼 수 있겠습니다.

갱신 과정을 그림으로 나타내면 다음과 같습니다. 검사할 때마다 거의 원점에 가까워지는 것이 확인됩니다.

 

# coding: utf-8
import numpy as np
import matplotlib.pylab as plt

def gradient_descent(f, init_x, lr=0.01, step_num=100):
    x = init_x
    x_history = []

    for i in range(step_num):
        x_history.append( x.copy() )

        grad = numerical_gradient(f, x)
        x -= lr * grad

    return x, np.array(x_history)

init_x = np.array([-3.0, 4.0])    

lr = 0.1
step_num = 20
x, x_history = gradient_descent(function_2, init_x, lr=lr, step_num=step_num)

plt.plot( [-5, 5], [0,0], '--b')
plt.plot( [0,0], [-5, 5], '--b')
plt.plot(x_history[:,0], x_history[:,1], 'o')

plt.xlim(-3.5, 3.5)
plt.ylim(-4.5, 4.5)
plt.xlabel("X0")
plt.ylabel("X1")
plt.show()

학습률이 크거나 너무 작으면 좋은 결과를 얻을 수 없습니다... 두 경우를 실험해 보죠!

# 학습률이 너무 큰 예. lr = 10.0
init_x = np.array([-3.0, 4.0])
result, _ = gradient_descent(function_2, init_x=init_x, lr=10.0, step_num=100)
print("Learning Rate 10.0 : {}".format(result))


# 학습률이 너무 작은 예. lr = 1e-10
init_x = np.array([-3.0, 4.0])
result, _ = gradient_descent(function_2, init_x=init_x, lr=1e-10, step_num=100)
print("Learning Rate 1e-10 : {}".format(result))

하이퍼 파라미터인 learning rate를 적절하게 조절하는 것이 좋습니다! adam 같은 알고리즘은 이러한 learning rate도 적절하게 조절해 줍니다. (물리의 가속도 개념)

 

학습률이 너무 크면 큰 값으로 발산해 버리고, 반대로 너무 작으면 최솟값에 도달하지 않은 채 갱신이 거의 안되고 끝나버립니다.

이것이 학습률을 잘 조절해야 하는 이유입니다!

참고로 이런 학습률 같은 사람이 직접 지정해야 하는 매개변수를 하이퍼 파라미터 라고 합니다.

 

신경망에서의 기울기

신경망 학습에서의 기울기도 위와 마찬가지로 구할 수 있습니다. 여기서 말하는 기울기는 가중치 매개변수 W에 대한 손실함수의 기울기입니다.

  • 신경망의 학습이란? 손실함수 (Loss)의 값을 최소화시키는 가중치 W와 편향 B를 구하는 것

따라서 가중치의 기울기 배열을 구할 수 있지 않을까?

  • Loss에 대한 W의 미분값을 구하는 것
  • W가 변했을 때 Loss는 얼마만큼 변할 것인가?

예를 들어 2 X 3 형태의 가중치를 만들고, 손실함수를 L로 정의한 신경망을 생각해 볼 수 있습니다. 이때 경사도는 ∂𝐿 / ∂𝑊로 나타 낼 수 있습니다. 수식으로 자세히 살펴봅시다!

∂𝐿 / ∂𝑊의 각 원소는 각각의 원소에 대한 편미분입니다. 

∂𝐿 / ∂𝑤11은 𝑤11을 조금 변경했을 때 손실함수 𝐿이 얼마나 변화하느냐를 나타낼 수 있습니다. 여기서 중요한 점은 ∂𝐿 / ∂𝑊의 형상(shape)이 𝑊와 같다는 것입니다. 둘 다 2 X 3 형태의 행렬인 것을 확인할 수 있습니다. 그럼 간단한 신경망을 예로 들어 실제로 기울기는 구하는 코드를 구해보도록 하겠습니다.

# 클래스로 하는 이유
# tf, keras, pytorch 등을 사용하면 보통 개발자가 직접 커스터마이징 하는 경우가 많습니다.
# 아주 기본적인 CNN, RNN (LSTM) 같은 것들은 그냥 쓰면 되는데, 실제 실무에서는 이런일이 많이 없습니다.
# 특정한 비즈니스에 맞춰서 레이어를 여러분들이 커스터마이징 하는 것이 필요합니다

class SimpleNet:
  
  # 신경망에서의 초기화는 필요한 매개변수를 준비하는 과정
  def __init__(self):
    self.W = np.random.randn(2, 3) # 정규분포로 초기화
    # 실제 신경망은 np.random.randn(2, 3) * 0.01 로 만드는 경우가 많습니다. (표준편차를 0.01로 설정)
  
  def predict(self, x):
    return np.dot(x, self.W)

  def loss(self, x, t):
    # loss를 구하는 과정

    # 1. 예측
    z = self.predict(x)

    # 2. softmax 통과
    y = softmax(z)

    # 3. loss함수를 이용해 실제 loss값 구하기
    loss = cross_entropy_error(y, t)

    return loss

 

SimpleNet 클래스의 객체를 생성하면 자동으로 2 X 3 형태의 가중치 배열을 랜덤으로 생성하게 됩니다.

그다음 예측을 수행하는 predict는 각 뉴런에서 객체가 만들어질 때 생성된 가중치 행렬 W와 데이터인 x의 내적을 구하고, loss 메소드 에서는 예측된 결과(predict)에 softmax 활성화 함수를 적용시키고 나서 해당 결과에 손실 함수인 cross_entropy_error를 이용해 손실값을 찾습니다. x는 입력 데이터, t는 정답 레이블입니다.

net = SimpleNet()

print("가중치 확인 : \n{}".format(net.W))

x = np.array([0.6, 0.9])
p = net.predict(x)

# 예측값은 몇개가 나오지?
print("단순 예측 값 : \n{}".format(p))

print("최댓값의 인덱스 : {}".format(np.argmax(p)))

# 임의의 정답을 마련하자
t = np.array([0, 1, 0]) # 정답은 1

net.loss(x, t)

 

Loss에 대한 기울기를 구하는 함수를 따로 만들어보도록 하겠습니다! numerical_gradient(f, x)를 써서 구하면 됩니다.

# Loss의 대한 W의 기울기를 구하기 위한 함수
def f(W):
  return net.loss(x, t)
 
# 기울기에 대한 Loss의 기울기 배열을 구할 수 있어요
dW = numerical_gradient(f, net.W)
dW

numerical_gradient(f, x)의 인수 f는 함수, x는 함수 f의 인수입니다. 그래서 가중치를 의미하는 net.W를 인수로 받아 손실 함수를 계산하는 새로운 함수 f를 정의하였습니다.

numerical_gradient에서는 함수 f와 net.W를 입력받아 가중치들에 대해 한꺼번에 기울기를 구합니다. dW는 numericalgradient의 결과로써, 2 X 3차원의 배열입니다. dW를 내용을 보겠습니다.

예를 들어 𝑤11에 위치한 0.12 정도의 값은 𝑤11의 값을  만큼 늘리면 손실 함수의 값은 0.12ℎ 만큼 증가한다는 뜻이 됩니다.

마찬가지로 𝑤32는 대략 -0.47이니, 𝑤32를 만큼 늘리면 손실 함수의 값은 0.5ℎ만큼 감소한다고 볼 수 있습니다. 그래서 손실 함숫값을 줄인다는 관점에서 𝑤11은 음의 방향으로 갱신해야 하고, 𝑤32는 양의 방향으로 갱신해야 함을 알 수 있습니다.

또한 한 번에 갱신되는 양에는 𝑤32가 𝑤11보다 크게 바뀐다는 사실도 알 수 있죠 위 코드의 f(W)를 lambda 형태로 바꿀 수도 있습니다.

dW = numerical_gradient(lambda w : net.loss(x, t), net.W)
dW

Summary

신경망 학습에 대한 이야기는 여기까지면 충분합니다.

몇 가지 키워드로 정리해 보고, 다음 글에서 최종적으로 MNIST를 학습할 수 있는 신경망을 직접 구현하도록 해봅시다!

전제

신경망에는 적응 가능한 가중치와 편향이 있고, 이 가중치와 편향을 훈련 데이터에 적응하도록 조정하는 과정을 '학습'이라 합니다.

4 단계로 정리해 보겠습니다!

1단계 - 미니배치

훈련 데이터 중 일부를 무작위로 가져옵니다. 이렇게 무작위로 가져온 데이터들을 미니 배치라 하며, 그 미니배치의 손실 함숫값을 줄이는 것이 목표입니다.

2단계 - 기울기 산출

미니 배치의 손실 함숫값을 줄이기 위해 각 가중치 매개변수의 기울기를 구합니다. 기울기는 손실 함수의 값을 가장 작게 하는 방향을 제시합니다.

3단계 - 매개변수 갱신

가중치 매개변수를 기울기 방향으로 아주 조금씩 갱신시킵니다.

4단계 - 반복

1 ~ 3 단계를 계속 반복합니다.

 

정리한 내용은 경사 하강법으로 매개변수를 갱신하는 방법이라고 볼 수 있으며, 데이터를 미니 배치로 무작위로 가져오기 때문에 이를 확률적 경사 하강법(stochastic gradient descent, SGD)라고 합니다.

확률적으로 무작위로 골라낸 데이터에 대해 수행하는 경사하강법이라는 의미가 됩니다. 대부분의 딥러닝 프레임워크에서는 SGD라는 함수로 해당 기능을 구현하고 있습니다.

다음 글에서 본격적으로 MNIST 데이터셋을 이용한 2층 신경망(은닉층이 1개인 네트워크)을 구현해 보겠습니다!

728x90
반응형
LIST

+ Recent posts