ML,DL

[cs231n] 최적화 : 확률 그라디언트 하강(Stochastic Gradient Descent)

yeeunnnn 2026. 2. 1. 20:40

🚨cs231n 2017 강의를 듣고 작성하였습니다.

🚨해당 게시글에 포함된 이미지 중 출처가 쓰여있지 않은 이미지는 모두 직접 그렸습니다.

 

3.1. 손실함수(Loss Function)의 시각화(Visualization)

손실함수들은 대체로 고차원 공간에서 정의되므로 시각화하기가 어렵다. 따라서, 고차원 공간을 1차원 직선이나 2차원 평면으로 잘라서 생각하면 된다.

https://cs231n.github.io/optimization-1/

  • 파란색은 낮은 loss, 빨간색은 높은 loss를
  • 손실함수가 부분적으로 선형이며, 가운데 그림과 같은 그래프들에서 손실을 평균낸 것이 마지막 그림이 된다.
  • 부분적으로 선형인 이유는 수식을 통해 알 수 있다. (i는 각 데이터의 인덱스, 손실함수는 SVM으로 가정)


3.2. 최적화(Optimization)

손실함수는 파라미터 W행렬이 얼마나 나쁜지를 측정한다.

최적화를 통해 이 손실함수를 최소화시키는 W를 찾아낼 수 있다.

3.2.1. 매우 나쁜 방법 : Random search

무작위로 파라미터를 골라서 넣어보고 넣어 본 값들 중 제일 좋은 값을 기록한다.

# X_train의 각 열(column)이 예제 하나에 해당하는 행렬이라고 생각하자. (예를 들어, 3073 x 50,000짜리)
# Y_train 은 레이블값이 저장된 array이라고 하자. (즉, 길이 50,000짜리 1차원 어레이)
# 그리고 함수 L이 손실함수라고 하자.

bestloss = float("inf") # Python assigns the highest possible float value
for num in xrange(1000):
  W = np.random.randn(10, 3073) * 0.0001 # generate random parameters
  loss = L(X_train, Y_train, W) # get the loss over the entire training set
  if loss < bestloss: # keep track of the best solution
    bestloss = loss
    bestW = W
  print 'in attempt %d the loss was %f, best %f' % (num, loss, bestloss)

# prints:
# in attempt 0 the loss was 9.401632, best 9.401632
# in attempt 1 the loss was 8.959668, best 8.959668
# in attempt 2 the loss was 9.044034, best 8.959668
# in attempt 3 the loss was 9.278948, best 8.959668
# in attempt 4 the loss was 8.857370, best 8.857370
# in attempt 5 the loss was 8.943151, best 8.857370
# in attempt 6 the loss was 8.605604, best 8.605604
# ... (trunctated: continues for 1000 lines)
# X_test은 크기가 [3073 x 10000]인 행렬, Y_test는 크기가 [10000 x 1]인 어레이라고 하자.
scores = Wbest.dot(Xte_cols) # 모든 테스트데이터 예제(1만개)에 대한 각 클라스(10개)별 점수를 모아놓은 크기 10 x 10000짜리인 행렬
# 각 열(column)에서 가장 높은 점수에 해당하는 클래스를 찾자. (즉, 예측 클래스)
Yte_predict = np.argmax(scores, axis = 0)
# 그리고 정확도를 계산하자. (예측 성공률)
np.mean(Yte_predict == Yte)
# 정확도 값이 0.1555라고 한다.

 

직관적으로는, 경사진 지형에서 눈가리개를 하고 점점 아래로 내려오는 자기 자신을 생각해보면 된다.

여기서 각 지점에서의 고도는 손실함수의 손실값을 의미하게 된다.

우리는 눈이 보이지 않는 상황이지만, 발바닥의 감각(기울기)을 이용해 가장 가파른 내리막을 찾을 수 있을 것이다.

3.2.2. Random Local search

W = np.random.randn(10, 3073) * 0.001 # 임의의 시작 파라미터를 랜덤하게 고른다.
bestloss = float("inf")
for i in xrange(1000):
  step_size = 0.0001
  Wtry = W + np.random.randn(10, 3073) * step_size
  loss = L(Xtr_cols, Ytr, Wtry)
  if loss < bestloss:
    W = Wtry
    bestloss = loss
  print 'iter %d loss is %f' % (i, bestloss)

 

시작점에서 무작위로 방향을 정해서 발을 살짝 뻗어서 더듬어보고 그게 내리막길일때만 한발짝 내딛는다고 생각하면 된다.

3.2.3. 그라디언트(gradient) 따라가기

1차원 함수의 경우, 기울기는 함수값의 순간증가율을 나타낸다.

그라디언트(gradient)는 이 기울기를 변수 하나가 아니라 여러 개인 경우로 일반화시킨 것이다.

즉, 그라디언트는 입력데이터공간의 각 차원에 해당하는 기울기들의 벡터이다.

함수가 숫자 하나가 아닌 벡터를 입력으로 받는 경우 우리는 미분을 편미분이라고 부르고, 그라디언트는 단순히 각 차원으로의 편미분들을 모아놓은 벡터이다.

 

발 밑 지형을 잘 더듬어보고 가장 가파르다는 느낌을 주는 방향으로 내려가는 것이다.


3.3. 그라디언트(Gradient) 계산

그라디언트 계산법은 크게 2가지가 있다.

: 느리고 근사값이지만 쉬운 방법인 수치 그라디언트

: 빠르고 정확하지만 미분이 필요하고 실수하기 쉬운 방법인 해석적 그라디언트

3.3.1. 수치 그라디언트

유한한 차이를 이용하여 수치적으로 그라디언트를 계산할 수 있다.

def eval_numerical_gradient(f, x):
  """
  함수 f의 x에서의 그라디언트를 매우 단순하게 구현하기.
  - f 는 입력값 1개를 받는 함수여야한다.
  - x는 numpy 어레이(array)로서그라디언트를 계산할 지점
  """

  fx = f(x) # 원래 지점 x에서 함수값 구하기.
  grad = np.zeros(x.shape)
  h = 0.00001

  # x의 모든 인덱스를 다 돌면서 계산하기.
  it = np.nditer(x, flags=['multi_index'], op_flags=['readwrite'])
  while not it.finished:

    # 함수 값을  x+h에서 계산하기.
    ix = it.multi_index
    old_value = x[ix]
    x[ix] = old_value + h # 변화랑 h
    fxh = f(x) # evalute f(x + h)
    x[ix] = old_value # 이전 값을 다시 가져온다. (매우 중요!)

    # 편미분 계산
    grad[ix] = (fxh - fx) / h # 기울기
    it.iternext() # 다음 단계로 가서 반복.

  return grad

 

W가 아래와 같이 주어졌을 때 유한한 차이 h=0.0001를 이용하여 수식에 대입하면 그라디언트를 구할 수 있다.

첫 번째 요소에 이 과정을 시행하여 첫 번째 요소의 그라디언트를 구하고, 첫 번째 요소를 돌려놓고 두 번째 요소에 이 과정을 시행하여 두 번째 요소의 그라디언트를 구한다.

계속해서 마지막 요소까지 이 과정을 반복하면 된다.

  • 스텝 크기가 미치는 영향
    • 그라디언트를 통해 알 수 있는 것은 함수값이 가장 빠르게 증가하는 방향이다.
    • 스텝 크기가 작다면 일관되지만 느리게 최적화를 진행시킨다.
    • 스텝 크기가 너무 크다면 최소값을 지나쳐서 손실값이 더 커지는 지점까지 갈 수 있다.
    • 얼만큼 가야하는지를 의미하는 스텝 크기는 신경망을 학습시킬 때 있어 가장 중요한 하이퍼파라미터가 될 것이다.

눈을 가리고 산을 내려가는 비유에서, 우리는 우리 발 밑으로 어느 방향이 가장 가파른지 느끼지만, 얼마나 발을 뻗어야 할지는 불확실하다.

발을 살살 휘저으면 꾸준하지만 매우 조금씩밖에 못 내려갈 것이다

반대로, 욕심껏 빨리 내려가려고 크고 과감하게 발을 내딛을 수도 있는데, 오히려 손실값을 증가시킬 수 있다.

3.3.2. 해석적 그라디언트

수치적으로 계산하는 그라디언트는 값의 차이를 이용해서 매우 단순하다. 하지만, 근사값이고 계산이 비효율적이다.

따라서, 미적분을 이용해서 해석적으로 그라디언트를 계산할 수 있다. (정확하고 빠르지만 실수하기 쉽다.)

과거에는 그라디언트체크(gradient check : 해석적으로 구한 뒤 수치적으로 구한 것과 비교하고, 틀린 경우 고치는 것) 과정을 거쳤다.

https://youtu.be/h7iBpEHGVNc?si=gJyqZu_PqluqbXoy

위 과정을 SVM 손실함수로 예를 들어 설명해보자.

파라미터로 이 함수를 미분할 수 있으므로 w_{y_i}로 미분해보자.

(여기서 1은 정의함수로, 괄호 안의 조건이 참이면 1, 거짓이면 0)

손실함수의 증가에 일조하는 클래스의 개수를 세고, 이 숫자를 데이터벡터 x_i에 곱하면 그라디언트가 나온다.

j≠y_i인 항에 대한 그라디언트는 다음과 같다.


3.4. 그라디언트 하강(gradient descent)

: 그라디언트를 반복적으로 평가하고 파라미터 업데이트를 수행하는 절차

 

# 단순한 경사하강(gradient descent)

while True: # while문의 조건식을 항상 참인 True로 지정하면 무한 루프
  weights_grad = evaluate_gradient(loss_fun, data, weights)
  weights += - step_size * weights_grad # 파라미터 업데이트(parameter update)
  1. W를 임의의 값으로 초기화한다.
  2. Loss와 gradient를 계산한 뒤에 가중치를 gradient의 반대방향으로 업데이트한다.
    → gradient는 함수에서 증가하는 방향이므로 ‘-’를 해야 내려가는 방향이 된다.
    → -gradient는 손실함수의 값을 최소화하는 방향을 가리킨다.
  3. 계속 반복하면 결국 수렴한다.

https://youtu.be/h7iBpEHGVNc?si=gJyqZu_PqluqbXoy

위의 그릇처럼 보이는 것이 손실함수이다.

가운데 빨간 부분이 낮은 LOSS, 테두리의 파란영역과 초록영역은 높은 LOSS이다.

임의의 점에 W를 설정해두고 -gradient를 계산할 것이고, LOSS가 가장 낮은 지점에 도달할 것이다.

https://youtu.be/h7iBpEHGVNc?si=gJyqZu_PqluqbXoy

  • 왼쪽 그림 : 임의의 점에서 시작해서 매번 조금씩 계속 이동 파라미터가 최저점으로 휘어져 들어가는 것을 볼 수 있다.
  • 오른쪽 그림 : 검정색은 왼쪽 그림과 같은 방법, 나머지 두 개는 Update rules(다음 스텝에 현재의 gradient 정보를 이용)

3.4.1. 미니배치 그라디언트 하강(Mini-batch gradient descent(MGD)

어떤 경우에서는 학습데이터가 수백만개 주어질 수 있고, 파라미터를 한 번 업데이트하려고 학습데이터 전체를 계산에 사용하는 것은 비효율적이다.

학습데이터의 배치를 이용해서 그라디언트를 구할 수 있다.

전체 Loss를 계산할 때 전체 트레이닝 셋 Loss의 평균을 사용했기 때문에 전체 Loss를 계산하는 것은 정말 오래걸린다.

→ N이 매우 크면 전체 데이터셋의 gradient와 loss를 계산하기보다는 Minibatch라는 작은 트레이닝 샘플 집합으로 나눠서 학습하는 것

Minibatch는 보통 2의 승수로 정하며 주로 32, 64, 128을 사용한다.

따라서 이 작은 minibatch를 이용해서 Loss의 전체 합의 “추정치”와 실제 gradient의 “추정치”를 계산하는 것이다.

# 단순한 미니배치 (minibatch) 그라디언트(gradient) 업데이트

while True:
  data_batch = sample_training_data(data, 256) # 예제 256개짜리 미니배치(mini-batch)
  weights_grad = evaluate_gradient(loss_fun, data_batch, weights)
  weights += - step_size * weights_grad # 파라미터 업데이트(parameter update)

이 방법이 잘 수행되는 이유는 학습데이터들의 예시들끼리 상관관계가 있기 때문이다.

120만개의 이미지를 사용하는 ILSVRC 훈련 데이터 셋을 예시로 들어보자.

1000개의 클래스로 이루어져있고, 하나의 클래스는 그 클래스를 의미하는 이미지를 1200개 복사한 것으로 이루어졌다고 하자.

이런 경우는 같은 클래스에 해당하는 이미지의 그라디언트가 동일하고, 따라서 1000개씩 묶어서 계산한다고 하여도 큰 무리가 없을 것이다.

3.4.2. Stochastic Gradient Descent(SGD)

미니배치 그라디언트 하강의 가장 극단적인 경우인 각 배치가 데이터 단 하나로 이루어졌을 때이다.

이건 상대적으로 덜 보편적인데, 그 이유는 프로그램을 짤 때 계산을 벡터/행렬로 만들어서 하기 때문에 한 예제에서 100번 계산하는 것보다 100개의 예제에서 1번 계산하는게 더 빠르기 때문이다.

SGD가 엄밀한 의미에서는 예제 하나짜리 미니배치(mini-batch)에서 그라디언트(gradient)를 계산하는 것이지만, 많은 사람들이 그냥 MGD를 의미하면서 SGD라고 부르기도 한다.

혹은 드물게나마 배치 그라디언트 하강 (Batch gradient descent, BGD)이라고도 부른다.

미니배치(mini-batch)의 크기도 하이퍼파라미터(hyperparameter)이지만, 이것을 교차검증하는 일은 흔치 않다.

이건 대체로 컴퓨터 메모리 크기의 한계에 따라 결정되거나, 몇몇 특정값 (예를 들어, 32, 64 or 128 같은 것)을 이용한다. 2의 제곱수를 이용하는 이유는 많은 벡터 계산이 2의 제곱수가 입력될 때 더 빠르기 때문이다.


3.5. Image Features(이미지 특징)

DNN이 유행하기 전에 주로 쓰는 방법은 두 가지 스테이지를 거치는 방법이 있었다.

https://youtu.be/h7iBpEHGVNc?si=gJyqZu_PqluqbXoy

이미지가 있으면 여러가지 특징 표현을 계산하는 것

  • 이러한 특징 표현은 이미지의 모양새와 관련된 것일 수 있다.
  • 여러 특징 표현들을 연결시켜 하나의 특징 벡터로 만든다.
  • 그러면 이 특징 벡터가 Linear classifier의 입력으로 들어간다.

https://youtu.be/h7iBpEHGVNc?si=gJyqZu_PqluqbXoy

왼쪽 그림의 빨간 점과 파란 점이 트레이닝 셋이라고 한다면? Linear한 결정 경계를 그릴 방법이 없다.

따라서 적절하게 특징 변환을 해보자.(여기서는 극좌표계로 변환함.)

복잡하던 데이터가 선형으로 분리가 가능하게 바뀌었다.

3.5.1. 특징 변환의 예

  • 컬러 히스토그램 : 각 픽셀을 해당하는 색의 양동이에 넣고 각 양동이에 들어있는 픽셀의 개수를 세는 것
    • 이미지에서 Hue 값만 뽑아서 모든 픽셀을 각 양동이에 넣는다.
    • 이를 통해 이미지가 전체적으로 어떤 색인지를 알 수 있다.

https://youtu.be/h7iBpEHGVNc?si=gJyqZu_PqluqbXoy

위 개구리의 예를 보면 초록색이 많은 것을 알 수 있다.

  • HOG(Histogram of oriented gradients) : Local orientation edges를 측정
    • 이미지를 8x8 픽셀로 나누고, 픽셀 내에서 가장 지배적인 edge의 방향을 계산한다.
    • edge directions를 양자화해서 양동이에 넣는다.
    • 다양한 edge orientations에 대한 히스토그램을 계산
    • 그러면 전체 특징 벡터는 각각의 모든 8x8 지역들이 가진 “edge orientation”에 대한 히스토그램”이 된다.

https://youtu.be/h7iBpEHGVNc?si=gJyqZu_PqluqbXoy

  • bag of words (자연어처리에서 영감을 받음)
    • BOW을 이미지에 적용하는 것이 쉽지 않아 “시각 단어(visual words)”라는 용어를 정의 
    • 시각 단어(visual words)
      • 엄청 많은 이미지들을 임의대로 조각내고 조각들을 K-means와 같은 알고리즘으로 군집화
      • 빨강색, 파랑색, 노랑색과 같은 다양한 색을 포착하고, 다양한 방향의 oriented edges 또한 포착
      • → 이러한 시각 단어 집합인 codebook을 만들고 나면 이 이미지의 시각 단어들의 발생빈도를 총합해서 이미지 인코딩 가능
      • → 이미지가 어떻게 생겼는지에 대한 다양한 정보를 제공

https://youtu.be/h7iBpEHGVNc?si=gJyqZu_PqluqbXoy


요약

  • 함수의 그라디언트는 그 함수값이 감소하는 가장 빠른 방향이다.
  • 그라디언트를 계산할 때 수치적인 방법과 해석적인 방법의 장단점을 알았다. 수치적인 그라디언트는 단순하지만, 근사값이고 비효율적이다. 해석적인 그라디언트는 정확하고 빠르지만 실수를 할 수 있다. 따라서 실제에서는 그라디언트 체크과정을 거친다.
  • 그라디언트 하강은 반복적으로 그라디언트를 계산하고 파라미터를 업그레이드 하는 것이다.
  •