본문 바로가기

Machine Learning/기본

ML Engineer를 위한 최적화 함수 정리

1. 최적화 함수(Optimizer)란 무엇인가?

 

딥러닝 모델 학습 과정은 아래와 같습니다.

  1. 딥러닝 모델이 학습 데이터에서 예측 값을 생성 합니다.
  2. 예측값과 실제 정답의 차이를 손실함수를 사용하여 계산 합니다.
  3. 역전파 알고리즘을 통해 각 가중치가 손실에 얼마나 영향을 주었는지를 나타내는 기울기(Gradient)를 계산 합니다.
  4. 계산된 기울기(Gradient)를 입력받아서, 가중치를 얼마나 업데이트 할 지를 결정하기 위해 최적화 함수를 사용합니다.

즉 최적화 함수는, 모델 학습 과정에서 손실 함수로부터 계산된 기울기(Gradient)를 입력으로 받아, 가중치를 업데이트하기 위한 최종 값을 생성하는 함수 입니다.

 

2. 최적화 함수의 종류

최적화 함수는 아래와 같은 형태로 사용됩니다.

import numpy as np

# 예시 데이터: 가상의 파라미터와 기울기
params = {'W1': np.random.randn(10, 5), 'b1': np.zeros(5)}
grads = {'W1': np.random.randn(10, 5), 'b1': np.random.randn(5)}

# # 옵티마이저 생성 및 사용법 (아래에서 각 클래스를 정의)
# optimizer = Optimizer(learning_rate=0.01) 
# updated_params = optimizer.update(params, grads)

 

 

1. Gradient  기반 경사 하강법

그래디언트를 바로 가중치에 적용하는 가장 단순한 방법 입니다.

 

배치 경사 하강법 (Batch Gradient Descent - BGD )

  • 전체 학습데이터를 사용하여 기울기를 계산 합니다.
  • 기울기가 데이터셋 전체를 반영하여 안정적이지만, 데이터셋이 큰 경우 메모리와 속도 문제로 인해 현실적으로 사용하지 않습니다.
param = param - lr * grad

 

 

확률적 경사 하강법 (Stochastic Gradient Descent - SGD)

  • 전체 데이터에서 무작위로 1개의 샘플을 추출하여 기울기를 계산하고, 업데이트 하는 방법입니다.
  • 계산 속도가 매우 빠르지만, 업데이트 방향이 불안정하고 노이즈가 존재합니다,
for x_i, y_i in dataset:
    grads = compute_gradient(model, x_i, y_i)
    params = params - learning_rate * grads

 

미니배치 경사 하강법 (Mini-batch Gradient Descent)

  • 전체 데이터를 미니배치로 나누어, 각 미니배치마다 기울기를 계산하고 업데이트 합니다.
  • 안정성과 속도의 균형을 맞춘 가장 효율적인 방법 입니다.
class SGD:
    """미니배치 경사 하강법"""
    def __init__(self, learning_rate=0.01):
        self.learning_rate = learning_rate

    def update(self, params, grads):
        updated_params = {}
        for key in params.keys():
            updated_params[key] = params[key] - self.learning_rate * grads[key]
        return updated_params

 

 

 

2. Momentum 기반 경사 하강법

 

모멘텀 (Momentum)

  • 물리 법칙에 있는 관성의 개념을 도입하였습니다.
  • 업데이트 시, 현재 기울기 뿐 아닌 과거에 이동했던 방향을 유지하여 이동합니다.
  • 지그재그 형태로 수렴하는 것이 아닌 일관된 방향으로 빠르게 수렴하도록 합니다.
  • v 라는 상태 변수를 통해 과거의 속도를 저장 합니다.
class Momentum:
    """모멘텀 SGD"""
    def __init__(self, learning_rate=0.01, momentum=0.9):
        self.learning_rate = learning_rate
        self.momentum = momentum
        self.v = {}  # 속도(velocity)를 저장할 딕셔너리

    def update(self, params, grads):
        updated_params = {}
        for key in params.keys():
            if key not in self.v:
                self.v[key] = np.zeros_like(params[key])

            # 1. 속도(v) 업데이트: v = momentum * v - learning_rate * grad
            self.v[key] = self.momentum * self.v[key] - self.learning_rate * grads[key]
            # 2. 파라미터 업데이트: param = param + v
            updated_params[key] = params[key] + self.v[key]

        return updated_params

 

 

네스테로프 가속 경사 (Nesterov Accelerated Gradient - NAG)

  • 모멘텀의 개선된 버전 입니다.
  • 현재 위치에서 기울기를 계산하는 것이 아닌, 모멘텀 방향으로 나아간 예상 위치에서 기울기를 계산하고 반영합니다.
class Nesterov:
    """네스테로프 가속 경사 (NAG)"""
    def __init__(self, learning_rate=0.01, momentum=0.9):
        self.learning_rate = learning_rate
        self.momentum = momentum
        self.v = {}

    def update(self, params, grads):
        updated_params = {}
        for key in params.keys():
            if key not in self.v:
                self.v[key] = np.zeros_like(params[key])

            # v_prev는 이전 단계의 속도를 의미
            v_prev = self.v[key] 

            # 1. 모멘텀 업데이트 (기존 모멘텀과 동일)
            self.v[key] = self.momentum * v_prev - self.learning_rate * grads[key]

            # 2. 파라미터 업데이트 (NAG의 핵심)
            # 이전 속도로 이동한 부분을 빼고, 현재 속도를 더해 'look-ahead' 효과를 냄
            update_val = -self.momentum * v_prev + (1 + self.momentum) * self.v[key]
            updated_params[key] = params[key] + update_val

        return updated_params

 

 

3. Adaptive Learning Rate 기반 경사 하강법

 

Adagrad (Adaptive Gradient)

  • 각 파라미터마다 맞춤형 학습률을 적용합니다. '변화가 적었던 파라미터는 크게, 변화가 많았던 파라미터는 작게' 업데이트합니다.
  • 이를 위해 파라미터별로 기울기 제곱 값의 합(G)을 계속 저장하고, 이 값의 제곱근으로 학습률을 나누어 업데이트 강도를 조절합니다. 
  • 학습이 길어지면 G가 너무 커져 학습이 멈추는 단점이 있습니다.
class Adagrad:
    """Adagrad"""
    def __init__(self, learning_rate=0.01, epsilon=1e-8):
        self.learning_rate = learning_rate
        self.epsilon = epsilon  # 0으로 나누는 것을 방지
        self.G = {}  # 기울기 제곱의 합을 저장할 딕셔너리

    def update(self, params, grads):
        updated_params = {}
        for key in params.keys():
            if key not in self.G:
                self.G[key] = np.zeros_like(params[key])

            self.G[key] += grads[key]**2

            # 학습률을 G의 제곱근으로 나누어 업데이트 강도 조절
            update_val = self.learning_rate * grads[key] / (np.sqrt(self.G[key]) + self.epsilon)
            updated_params[key] = params[key] - update_val

        return updated_params

 

RMSprop (Root Mean Square Propagation)

 

Adagrad의 학습 중단 문제를 해결하기 위해 등장했습니다. 

기울기 제곱의 합을 무한정 더하는 대신, 지수 이동 평균(exponential moving average)을 사용하여 최근 기울기 정보에 더 큰 가중치를 두어 학습률이 0에 수렴하는 것을 방지 합니다.

 

class RMSprop:
    """RMSprop"""
    def __init__(self, learning_rate=0.001, decay_rate=0.9, epsilon=1e-8):
        self.learning_rate = learning_rate
        self.decay_rate = decay_rate
        self.epsilon = epsilon
        self.S = {}  # 기울기 제곱의 지수 이동 평균을 저장

    def update(self, params, grads):
        updated_params = {}
        for key in params.keys():
            if key not in self.S:
                self.S[key] = np.zeros_like(params[key])

            # 기울기 제곱의 지수 이동 평균 계산
            self.S[key] = self.decay_rate * self.S[key] + (1 - self.decay_rate) * (grads[key]**2)

            update_val = self.learning_rate * grads[key] / (np.sqrt(self.S[key]) + self.epsilon)
            updated_params[key] = params[key] - update_val

        return updated_params

 

 

 

Adam (Adaptive Moment Estimation)

  • Momentum과 RMSprop의 장점을 결합한 최적화 함수입니다.
  • 기울기의 지수 이동 평균(1차 모멘텀, m)과 기울기 제곱의 지수 이동 평균(2차 모멘텀, v)을 함께 관리합니다.
  • m은 이동 방향을, v는 학습률 크기를 조절하며, 대부분의 상황에서 안정적이고 빠른 성능을 보여 가장 널리 사용되는 기본 옵티마이저입니다.
class Adam:
    """Adam"""
    def __init__(self, learning_rate=0.001, beta1=0.9, beta2=0.999, epsilon=1e-8):
        self.learning_rate = learning_rate
        self.beta1 = beta1
        self.beta2 = beta2
        self.epsilon = epsilon
        self.m = {}  # 1차 모멘텀
        self.v = {}  # 2차 모멘텀
        self.t = 0   # 타임스텝

    def update(self, params, grads):
        updated_params = {}
        self.t += 1

        for key in params.keys():
            if key not in self.m:
                self.m[key] = np.zeros_like(params[key])
                self.v[key] = np.zeros_like(params[key])

            # 1. 1차, 2차 모멘텀 업데이트
            self.m[key] = self.beta1 * self.m[key] + (1 - self.beta1) * grads[key]
            self.v[key] = self.beta2 * self.v[key] + (1 - self.beta2) * (grads[key]**2)

            # 2. 편향 보정
            m_hat = self.m[key] / (1 - self.beta1**self.t)
            v_hat = self.v[key] / (1 - self.beta2**self.t)

            # 3. 파라미터 업데이트
            update_val = self.learning_rate * m_hat / (np.sqrt(v_hat) + self.epsilon)
            updated_params[key] = params[key] - update_val

        return updated_params

 

AdamW

  • 기존 Adam에서 L2 정규화(가중치 감쇠, Weight Decay)를 적용하는 방식의 문제점을 개선한 버전입니다.
  • Adam은 Weight Decay를 모멘텀 계산에 포함시켜 효과를 왜곡시키는 반면, AdamW는 Adam의 파라미터 업데이트와 Weight Decay를 분리하여 적용합니다.
import numpy as np

class AdamW:
    """AdamW Optimizer"""
    def __init__(self, learning_rate=0.001, beta1=0.9, beta2=0.999, epsilon=1e-8, weight_decay=1e-2):
        self.learning_rate = learning_rate
        self.beta1 = beta1
        self.beta2 = beta2
        self.epsilon = epsilon
        self.weight_decay = weight_decay # AdamW에 추가된 하이퍼파라미터
        self.m = {}  # 1차 모멘텀 (momentum)
        self.v = {}  # 2차 모멘텀 (adaptive learning rate)
        self.t = 0   # 타임스텝 (업데이트 횟수)

    def update(self, params, grads):
        """
        파라미터를 업데이트합니다.

        Args:
            params (dict): 모델의 현재 파라미터
            grads (dict): 파라미터에 대한 현재 기울기

        Returns:
            dict: 업데이트된 파라미터
        """
        updated_params = {}
        self.t += 1

        for key in params.keys():
            # self.m, self.v 초기화
            if key not in self.m:
                self.m[key] = np.zeros_like(params[key])
                self.v[key] = np.zeros_like(params[key])

            # --- 이 부분까지는 Adam과 완전히 동일합니다 ---
            # 1. 1차, 2차 모멘텀 업데이트
            self.m[key] = self.beta1 * self.m[key] + (1 - self.beta1) * grads[key]
            self.v[key] = self.beta2 * self.v[key] + (1 - self.beta2) * (grads[key]**2)

            # 2. 편향 보정 (Bias Correction)
            m_hat = self.m[key] / (1 - self.beta1**self.t)
            v_hat = self.v[key] / (1 - self.beta2**self.t)

            # 3. Adam의 기본 파라미터 업데이트 값 계산
            adam_update = self.learning_rate * m_hat / (np.sqrt(v_hat) + self.epsilon)
            
            # --- AdamW의 핵심: 분리된 가중치 감쇠 (Decoupled Weight Decay) ---
            # 기존 Adam의 업데이트와 별개로, 가중치 자체에 감쇠를 직접 적용합니다.
            # 이 연산은 learning_rate와 weight_decay를 곱하여 파라미터를 원점 방향으로 조금씩 이동시킵니다.
            weight_decay_update = self.learning_rate * self.weight_decay * params[key]

            # 4. 최종 파라미터 업데이트
            # Adam 업데이트와 가중치 감쇠 업데이트를 모두 적용
            updated_params[key] = params[key] - adam_update - weight_decay_update
            
        return updated_params

 

 

Nadam (Nesterov-accelerated Adaptive Moment Estimation)

  • Adam에 네스테로프 가속 경사(NAG)의 개념을 접목한 것입니다.
import numpy as np

class Nadam:
    """Nadam (Nesterov-accelerated Adam)"""
    def __init__(self, learning_rate=0.002, beta1=0.9, beta2=0.999, epsilon=1e-8):
        self.learning_rate = learning_rate
        self.beta1 = beta1
        self.beta2 = beta2
        self.epsilon = epsilon
        self.m = {}  # 1차 모멘텀
        self.v = {}  # 2차 모멘텀
        self.t = 0   # 타임스텝

    def update(self, params, grads):
        updated_params = {}
        self.t += 1

        for key in params.keys():
            # self.m, self.v 초기화
            if key not in self.m:
                self.m[key] = np.zeros_like(params[key])
                self.v[key] = np.zeros_like(params[key])

            # --- Adam과 동일한 부분 ---
            # 1. 1차, 2차 모멘텀 계산
            self.m[key] = self.beta1 * self.m[key] + (1 - self.beta1) * grads[key]
            self.v[key] = self.beta2 * self.v[key] + (1 - self.beta2) * (grads[key]**2)

            # 2. 편향 보정
            # v에 대한 편향 보정은 Adam과 동일
            v_hat = self.v[key] / (1 - self.beta2**self.t)
            
            # --- Nadam의 핵심 차이점 ---
            # m에 대한 편향 보정을 할 때 Nesterov의 'look-ahead' 개념을 적용
            # 현재 기울기(grads)에 대한 보정과 모멘텀(m)에 대한 보정을 조합
            m_hat = (self.beta1 * self.m[key] / (1 - self.beta1**self.t)) + \
                    ((1 - self.beta1) * grads[key] / (1 - self.beta1**self.t))
            
            # --- Adam과 유사한 최종 업데이트 ---
            # 3. 파라미터 업데이트
            update_val = self.learning_rate * m_hat / (np.sqrt(v_hat) + self.epsilon)
            updated_params[key] = params[key] - update_val
            
        return updated_params

 

3. 최적화 함수 선택 방법

 

일반적으로 Adam, AdamW 를 기본으로 사용합니다.

Adam/AdamW는 대부분의 문제에서 안정적이고 빠르게 좋은 성능을 보여주기 때문에, 프로젝트 초기 단계에서 가장 먼저 시도해볼 만한 강력한 베이스라인입니다.

 

더 극한의 성능이 필요한 경우, 다른 하이퍼 파라미터와 함께 하이퍼 파라미터 튜닝을 할 수 있습니다.

 

 

'Machine Learning > 기본' 카테고리의 다른 글

ML Engineer를 위한 활성화 함수 정리  (0) 2025.08.23