[밑딥] 4장. 신경망 학습

Speedwell🍀·2022년 5월 4일
0

학습
: 훈련 데이터로부터 가중치 매개변수의 최적값을 자동으로 획득하는 것

  • 이번 장에서는 신경망이 학습할 수 있도록 해주는 '지표'인 손실 함수를 소개한다.
    ❗이 손실 함수의 결과값을 가장 작게 만드는 가중치 매개변수를 찾는 것이 학습의 목표!

  • 이번 장에서는 손실 함수의 값을 가급적 작게 만드는 기법으로 '함수의 기울기를 활용하는 경사법'을 소개한다.

  • 이번 장에서는 신경망 학습(데이터로부터 매개변수의 값을 정하는 방법)에 대해 설명하고, 파이썬으로 MNIST 데이터셋의 손글씨 숫자를 학습하는 코드를 구현해볼 것이다.


1. 데이터 주도 학습

신경망의 특징은 데이터를 보고 학습할 수 있다는 것이다.
➡️ 데이터에서 학습한다는 것은 가중치 매개변수 값을 데이터로 보고 자동으로 결정한다는 뜻!


알고리즘을 밑바닥부터 '설계'하는 대신, 주어진 데이터를 잘 활용해서 해결할 수 있다.

이미지에서 특징(feature)을 추출하고 그 특징의 패턴을 기계학습 기술로 학습!

특징: 입력 데이터에서 본질적인/중요한 데이터를 정확하게 추출할 수 있도록 설계된 변환기

  1. 이미지 데이터의 특징을 벡터로 변환한다.

    • CV 분야에서는 SIFT, SURF, HOG 등의 특징을 많이 사용
  2. 변환된 벡터를 가지고 학습

    • 지도 학습 방식의 대표 분류 기법인 SVM, KNN 등으로 학습

(회색 블록은 사람이 개입하지 않음을 뜻함)

위의 그림에서 볼 수 있듯이 신경망은 이미지를 있는 그대로 학습한다.
두 번째 접근 방식 (특징과 기계학습 방식)과 달리 이미지에 포함된 중요한 특징까지 '기계'가 스스로 학습한다.


👍 신경망은 모든 문제를 같은 맥락에서 풀 수 있다는 이점
➡️ 신경망은 모든 문제를 주어진 데이터 그대로를 입력 데이터로 활용해 'end-to-end'로 학습할 수 있다.


train/test data

기계학습 문제는 데이터를 훈련 데이터(train data)시험 데이터(test data)로 나눠 학습과 실험을 수행한다.

  1. 훈련 데이터만 사용하여 학습하면서 최적의 매개변수를 찾는다.
  2. 시험 데이터를 사용하여 앞서 훈련한 모델의 실력을 평가한다.

🤔 왜 훈련 데이터와 시험 데이터를 분리할까?
❗ 범용적으로 사용할 수 있는 모델을 원하므로 이 범용 능력을 제대로 평가하기 위해!

범용 능력: 아직 보지 못한 데이터(훈련 데이터에 포함되지 않는 데이터)로도 문제를 올바르게 풀어내는 능력

만약 한 데이터셋에만 지나치게 최적화되면 이 상태를 오버피팅(overfitting)이라고 부른다.



2. 손실 함수 (Loss Function)

신경망 학습에서는 현재의 상태를 '하나의 지표'로 표현한다. 그리고 그 지표를 가장 좋게 만들어주는 가중치 매개변수의 값을 탐색하는 것이다.

'하나의 지표'를 기준으로 최적의 매개변수 값을 탐색하는데, 신경망 학습에서 사용하는 지표가 손실 함수
(주의) 손실 함수는 신경망 성능의 '나쁨'을 나타내는 지표

손실 함수는 일반적으로 오차제곱합교차 엔트로피 오차를 사용한다.


2-1) 오차제곱합 (SSE; sum of squares for error)

가장 많이 쓰이는 손실 함수

y_k: 신경망의 출력(신경망이 추정한 값), t_k: 정답 레이블(원-핫 인코딩), k: 데이터의 차원 수

각 원소의 출력(추정 값)과 정답 레이블(참 값)의 차를 제곱한 후, 그 총합


def sum_squares_error(y, t):
	return 0.5 * np.sum((y-t)**2) # y와 t는 넘파이 배열

이 함수를 실제로 사용해보자.

>>> # 정답은 '2'
>>> t = [0, 0, 1, 0, 0, 0, 0, 0, 0, 0] # 원-핫 인코딩
>>>
>>> # 예1: '2'일 확률이 가장 높다고 추정함(0.6)
>>> y = [0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0]
>>> sum_squares_error(np.array(y), np.array(t))
0.09750000000000031
>>> 
>>> # 예2: '7'일 확률이 가장 높다고 추정함(0.6)
>>> y = [0.1, 0.05, 0.1, 0.0, 0.05, 0.1, 0.0, 0.6, 0.0, 0.0]
>>> sum_squares_error(np.array(y), np.array(t))
0.59750000000000003

➡️ 오차제곱합 기준으로 첫 번째 추정 결과가 (오차가 더 작으니) 정답에 더 가까운 것으로 판단!


2-2) 교차 엔트로피 오차 (cross entropy error)

log: 밑이 e인 자연로그, y_k: 신경망의 출력, t_k: 정답 레이블(원-핫 인코딩)

실질적으로 정답일 때의 추정(t_k가 1일 때의 y_k)의 자연로그를 계산하는 식
즉, 교차 엔트로피 오차는 정답일 때의 출력이 전체 값을 정한다.

위는 자연로그 y=logx의 그래프이다. x가 1일 때는 y는 0이 되고, x가 0에 가까워질수록 y의 값은 점점 작아지는 것을 볼 수 있다.
➡️ 교차 엔트로피 오차도 마찬가지로 정답에 해당하는 출력이 커질수록 0에 다가가다가, 그 출력이 1일 때 0이 된다. 반대로 정답일 때의 출력이 작아질수록 오차는 커진다.


def cross_entropy_error(y, t): # y와 t는 넘파이 배열
	delta = 1e-7
    return -np.sum(t * np.log(y + delta))

🤔 np.log를 계산할 때 왜 delta를 더했을까?

np.log() 함수에 0을 입력하면 마이너스 무한대인 -inf가 되어 더 이상 계산을 진행할 수 없게 되기 때문이다.
➡️ 아주 작은 값을 더해서 절대 0이 되지 않도록, 즉 마이너스 무한대가 발생하지 않도록 한 것이다.


2-3) 미니배치 학습

기계 학습은 훈련 데이터에 대한 손실 함수의 값을 구하고, 그 값을 최대한 줄여주는 매개변수를 찾아낸다.
➡️ 👎이렇게 하려면 모든 훈련 데이터를 대상으로 손실 함수 값을 구해야 한다.


훈련 데이터 모두에 대한 손실 함수의 합을 구하는 방법을 생각해보자!
앞에서 본 교차 엔트로피 오차의 경우를 예로 들어보자! 데이터의 개수는 N개이다.

우리는 앞에서(2-2) 데이터 하나에 대한 손실 함수를 구했었다.
위의 식은 N개의 데이터로 확장한 후, 마지막에 N으로 나누어 정규화를 함으로써 '평균 손실 함수'를 구한 것이다.
➡️ 이렇게 평균을 구해 사용하면 훈련 데이터 개수와 관계없이 언제든 통일된 지표를 얻을 수 있다.


🤔 하지만 모든 데이터를 대상으로 손실 함수의 합을 구하려면 시간이 오래 걸리고 현실적이지 않다!
➡️ 이런 경우 데이터 일부를 추려 전체의 '근사치'로 이용할 수 있다.

📌 신경망 학습에서도 훈련 데이터로부터 일부만 골라 학습을 수행한다.
이 일부를 미니배치(mini-batch)라고 한다. 이렇게 학습하는 방법을 미니배치 학습이라고 한다.


np.random.choice()를 통해 지정한 범위의 수 중에서 무작위로 원하는 개수만 꺼낼 수 있다.
➡️ 무작위로 선택한 이 인덱스를 사용해 미니배치를 뽑아내기만 하면 된다. 손실 함수도 이 매니배치로 계산한다.


2-4) (배치용) 교차 엔트로피 오차 구현하기

미니배치 같은 배치 데이터를 지원하는 교차 엔트로피 오차를 구현해보자!
데이터가 하나인 경우와 데이터가 배치로 묶여 입력될 경우 모두를 처리할 수 있도록 구현해보자!

정답 레이블이 원-핫 인코딩인 경우

def cross_entropy_error(y, t):
	if y.dim == 1:
    	t = t.reshape(1, t.size)
        y = y.reshape(1, y.size)
	
    batch_size = y.shape[0]
    return -np.sum(t * np.log(y + 1e-7)) / batch_size

y: 신경망의 출력, t: 정답 레이블

y가 1차원이라면, 즉 데이터 하나당 교차 엔트로피 오차를 구하는 경우는 reshape 함수로 데이터의 형상을 바꿔준다. 배치의 크기로 나눠 정규화하고 이미지 1장당 평균의 교차 엔트로피 오차를 계산한다.


정답 레이블이 원-핫 인코딩이 아니라 숫자 레이블로 주어진 경우

def cross_entropy_error(y, t):
	if y.dim == 1:
    	t = t.reshape(1, t.size)
        y = y.reshape(1, y.size)
	
    batch_size = y.shape[0]
    return -np.sum(np.log(y[np.arange(batch_size), t] + 1e-7)) / batch_size

📌 이 구현에서는 원-핫 인코딩일 때 t가 0인 원소는 교차 엔트로피 오차도 0이므로, 그 계산은 무시해도 좋다는 것이 핵심!
즉, 정답에 해당하는 신경망의 출력만으로 교차 엔트로피 오차를 계산할 수 있다.

따라서 원-핫 인코딩 시 t * np.log(y)였던 부분을 레이블 표현일 때는
np.log(y[np.arange(batch_size), t])로 구현한다.

식이 바뀌는 것이 이해가 안 갔는데 아래의 슬라이드를 통해 이해했다.

np.log(y[np.arange(batch_size), t]) 설명

  • np.arange(batch_size) → [0, 1, ..., batch_size-1] 넘파이 배열 생성
  • t에는 레이블이 [2, 7, 0, 9, 4]와 같이 저장되어 있음
    ➡️ y[np.arange(batch_size), t]는 각 데이터의 정답 레이블에 해당하는 신경망의 출력을 추출한다.
    (이 예에서는 [y[0,2], y[1,7], y[2,0], y[3,9], y[4,4]]인 넘파이 배열을 생성한다.)

2-5) 왜 손실 함수를 설정하는가?

🤔 우리의 궁극적인 목적은 높은 '정확도'를 끌어내는 매개변수를 찾는 것인데, 왜 '정확도'라는 지표를 놔두고 '손실 함수의 값'이라는 우회적인 방법을 택하는 것일까?

❗신경망 학습에서의 미분의 역할에 주목하면 알 수 있다! (자세한 것은 이 글의 3번)


신경망 학습에서는 최적의 매개변수(가중치와 편향)을 탐색할 때 손실 함수의 값을 가능한 한 작게 하는 매개변수 값을 찾는다.
➡️ 이때 매개변수의 미분(정확히는 기울기)을 계산하고, 그 미분 값을 단서로 매개변수의 값을 서서히 갱신하는 과정을 반복한다.


가중치 매개변수의 손실 함수의 미분이란 가중치 매개변수의 값을 아주 조금 변화시켰을 때, 손실 함수가 어떻게 변하는가라는 의미!

  • 미분 값이 음수이면?

    • 그 가중치 매개변수를 양의 방향으로 변화시켜 손실 함수의 값을 줄일 수 있다.
  • 미분 값이 양수라면?

    • 가중치 매개변수를 음의 방향으로 변화시켜 손실 함수의 값을 줄일 수 있다.
  • 미분 값이 0이면?

    • 가중치 매개변수를 어느 쪽으로 움직여도 손실 함수의 값은 줄어들지 않는다.
      ➡️ 가중치 매개변수의 갱신이 멈춘다.

신경망을 학습할 때 정확도를 지표로 삼으면 안된다!
정확도를 지표로 하면 매개변수의 미분 값이 대부분의 장소에서 0이 되기 때문이다.


🤔 왜 정확도를 지표로 삼으면 매개변수의 미분이 대부분의 장소에서 0이 될까??

❗ 매개변수를 약간만 조정해서는 정확도가 개선되지 않고 일정하게 유지되기 때문이다.


정리하자면, 정확도는 매개변수의 미소한 변화에는 거의 반응을 보이지 않고, 반응이 있더라도 그 값이 불연속적으로 갑자기 변화한다.
→ 이는 계단 함수를 활성화 함수로 사용하지 않는 이유와도 들어맞는다.


활성화 함수로 계단 함수를 사용하면 신경망 학습이 잘 이뤄지지 않는다. 계단 함수의 미분은 대부분의 장소(0 이외의 곳)에서 0이기 때문이다.
➡️ 그 결과, 계단 함수를 이용하면 손실 함수를 지표로 삼는 게 아무 의미가 없게 된다. 매개변수의 작은 변화가 주는 파장을 계단 함수가 말살하여 손실 함수의 값에는 아무런 변화가 나타나지 않기 때문이다.


반면에 시그모이드 함수의 미분(접선)은 출력(세로축의 값)이 연속적으로 변하고, 곡선의 기울기도 연속적으로 변한다.

📌 즉, 시그모이드 함수의 미분은 어느 장소라도 0이 되지는 않는다.
이는 신경망 학습에서 중요한 성질로, 기울기가 0이 되지 않는 덕분에 신경망이 올바르게 학습할 수 있는 것이다.


3. 수치 미분

경사법에서는 기울기(경사)의 값을 기준으로 나아갈 방향을 정한다.

기울기와 기울기의 성질을 공부하기에 앞서 미분부터 복습해보자!

3-1) 미분

미분은 특정 순간의 변화량을 뜻한다.
즉, x의 '작은 변화'가 함수 f(x)를 얼마나 변화시키냐를 의미한다.

이때 시간의 작은 변화, 즉 시간을 뜻하는 h를 한없이 0에 가깝게 한다는 의미로 lim_h->0로 나타낸다.


함수를 미분하는 계산을 파이썬으로 구현해보자!

# 나쁜 구현 예
def numerical_diff(f, x):
	h = 1e-50
    return (f(x + h) - f(x)) / h

함수의 이름은 수치 미분(numerical differentiation)이라는 뜻이다. '함수 f'와 '함수 f에 넘길 인수 x'라는 두 인수를 받는다.
이 함수에서는 두 가지를 개선해야 한다!

  1. 반올림 오차(rounding error) 문제

    • 반올림 오차는 작은 값(가령 소수점 8자리 이하)이 생략되어 최종 계산 결과에 오차가 생기게 한다.
  2. 함수 f의 차분(임의의 두 점에서의 함수 값들의 차이) 오차

    • '진정한 미분'은 x 위치의 함수의 기울기(접선)지만, 이번 구현에서의 미분은 (x+h)와 x 사이의 기울기
    • 이 차이는 h를 무한히 0으로 좁히는 것이 불가능해 생기는 한계

그림과 같이 수치 미분에는 오차가 포함된다.
➡ 오차를 줄이기 위해 (x+h)와 (x-h)일 때의 함수 f의 차분을 계산하는 방법을 쓰기도 한다.
중심 차분/중앙 차분
(x+h)와 x의 차분은 전방 차분


두 개선점을 적용해 수치 미분을 다시 구현해보자!

def numerical_diff(f, x):
	h = 1e-4 # 0.0001
    return (f(x+h) - f(x-h)) / (2*h)

수치 미분: 아주 작은 차분으로 미분하는 것

한편, 수식을 전개해 미분하는 것은 해석적(analytic)이라는 말을 이용해 '해석적 해' 혹은 '해석적으로 미분'
➡ 해석적 미분은 오차를 포함하지 않는 '진정한 미분' 값을 구해준다.


3-2) 편미분

: 변수가 여럿인 함수에 대한 미분

편미분은 변수가 하나인 미분과 마찬가지로 특정 장소의 기울기를 구한다.
단, 여러 변수 중 목표 변수 하나에 초점을 맞추고 다른 변수는 값을 고정한다.



4. 기울기(Gradient)

: 모든 변수의 편미분을 벡터로 정리한 것

기울기를 구현해보자!

def numerical_gradient(f, x):
	h = 1e-4 # 0.0001
    grad = np.zeros_like(x) # x와 형상이 같고 그 원소가 모두 0인 배열 생성
    
    for idx in range(x.size):
    	tmp_val = x[idx]
        
        # f(x+h) 계산
        x[idx] = tmp_val + h
        fxh1 = f(x)
        
        # f(x-h) 계산
        x[idx] = tmp_val - h
        fxh2 = f(x)
        
        grad[idx] = (fxh1 - fxh2) / (2*h)
        x[idx] = tmp_val # 값 복원
	
    return grad

이 함수 numerical_gradient(f, x)는 넘파이 배열 x의 각 원소에 대해서 수치 미분을 구하는 함수이다.


🤔 기울기가 의미하는 게 무엇일까?

기울기의 결과에 마이너스를 붙인 벡터를 그려보자.

위의 그림을 보면 기울기는 함수의 가장 낮은 장소(최솟값)을 가리키는 것처럼 보인다.
또한 가장 낮은 곳에서 멀어질수록 화살표의 크기가 커지고 있다.

사실 기울기는 각 지점에서 낮아지는 방향을 가리킨다.

더 정확히 말하면 기울기가 가리키는 쪽은 각 장소에서 함수의 출력 값을 가장 크게 줄이는 방향이다.
중요하니까 꼭 기억해두자🔥🔥


4-1) 경사법(경사 하강법)


기계학습 문제 대부분은 학습 단계에서 최적의 매개변수를 찾아낸다. 신경망 역시 최적의 매개변수(가중치와 편향)를 학습 시에 찾아야 한다.
여기에서 최적이란 손실 함수가 최솟값이 될 때의 매개변수 값!
➡️ 경사법(gradient method): 기울기를 잘 이용해 함수의 최솟값(또는 가능한 한 작은 값)을 찾으려는 것

주의) 함수가 극솟값, 최솟값, 안장점이 되는 장소에서는 기울기가 0
안장점(saddle point): 어느 방향에서 보면 극댓값이고, 다른 방향에서 보면 극솟값이 되는 점

  • 경사법은 기울기가 0인 장소를 찾지만 그것이 반드시 최솟값이라고 할 수 없다. (극솟값이나 안장점일 가능성)
  • 또한 복잡하고 찌그러진 모양의 함수라면 (대부분) 평평한 곳으로 파고들면서 고원(plateau)이라 하는, 학습이 진행되지 않는 정체기에 빠질 수 있다.

기울어진 방향이 꼭 최솟값을 가리키는 것은 아니나, 그 방향으로 가야 함수의 값을 줄일 수 있다.
➡️ 최솟값이 되는 장소를 찾는 문제에서는 기울기 정보를 단서로 나아갈 방향을 정해야 한다.


경사법은 현 위치에서 기울어진 방향으로 일정 거리만큼 이동하고, 이동한 곳에서 기울기를 구해 그 기울어진 방향으로 이동하는 과정을 반복한다.
이렇게 함수의 값을 점차 줄이는 것이 경사법


📌 경사법은 기계학습을 최적화하는 데 흔히 쓰는 방법이다.
특히 신경망 학습에는 경사법을 많이 사용한다.

경사 하강법(gradient descent method): 최솟값을 찾음
경사 상승법(gradient ascent method): 최댓값을 찾음

손실 함수의 부호를 반전시키면 되므로 하강/상승은 본질적으로는 중요하지 않다.
일반적으로 신경망(딥러닝) 분야에서의 경사법은 경사 하강법


경사법을 수식으로 나타내보자!

ŋ(eta, 에타): 갱신하는 양 ➡️ 이를 신경망 학습에서는 학습률(learning rate)

학습률: 한 번의 학습으로 얼마만큼 학습해야 할지, 즉 매개변수 값을 얼마나 갱신하느냐를 정하는 것


위의 식은 1회에 해당하는 갱신이고, 이 단계를 반복한다.
➡️ 변수의 값을 갱신하는 단계를 여러 번 반복하면서 서서히 함수의 값을 줄이는 것!

위의 식은 변수가 2개인 경우이지만, 변수가 늘어도 같은 식(각 변수의 편미분 값)으로 갱신한다.


학습률 값은 미리 특정 값(0.01, 0.001,...)으로 정해두어야 한다.
이 값이 너무 크거나 작으면 '좋은 장소'를 찾아갈 수 없다.
➡️ 신경망 학습에서는 보통 이 학습률 값을 변경하면서 올바르게 학습하고 있는지를 확인하면서 진행한다.


경사 하강법을 구현해보자!

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: 최적화하려는 함수
init_x: 초깃값
lr: 학습률
step_num: 경사법에 따른 반복 횟수

함수의 기울기는 numerical_gradient(f, x)로 구하고, 그 기울기에 학습률을 곱한 값으로 갱신하는 처리를 step_num번 반복한다.

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

학습률 같은 매개변수를 하이퍼파라미터(hyper parameter)라고 한다.
사람이 직접 설정해야 하는 매개변수. 일반적으로 이 하이퍼파라미터들은 여러 후보 값 중에서 시험을 통해 가장 잘 학습하는 값을 찾는 과정을 거쳐야 한다.

하이퍼파라미터는 가중치와 편향 같은 신경망의 매개변수와는 성질이 다른 매개변수이다.
신경망의 가중치 매개변수는 훈련 데이터와 학습 알고리즘에 의해서 자동으로 획득되는 매개변수


4-2) 신경망에서의 기울기


여기서 말하는 기울기는 가중치 매개변수에 대한 손실 함수의 기울기


예를 들어 형상이 2x3, 가중치가 W, 손실 함수가 L인 신경망이 있을 때, 경사는 ∂L / ∂W
∂L / ∂W의 각 원소는 각각의 원소에 관한 편미분

∂L / ∂WW의 형상이 같다!⭐


간단한 신경망을 예로 들어 실제로 기울기를 구하는 코드를 구현해보자!

import sys, os
sys.path.append(os.pardir)
import numpy as np
from common.functions import softmax, cross_entropy_error
from common.gradient import numerical_gradient

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

simpleNet 클래스는 형상이 2x3인 가중치 매개변수 하나를 매개변수로 갖는다.
x는 입력 데이터, t는 정답 레이블


simpleNet을 사용해 몇 가지 시험을 해보자!

>>> net = simpleNet()
>>> print(net.W) # 가중치 매개변수
[[ 0.47355232  0.9977393   0.84668094]
 [ 0.85557411  0.03563661  0.69422093]]
>>>
>>> x = np.array([0.6, 0.9])
>>> p = net.predict(x)
>>> print(p)
[ 1.05414809 0.63071653 1.1328074]
>>> np.argmax(p) # 최댓값의 인덱스
2
>>>
>>> t = np.array([0, 0, 1]) # 정답 레이블
>>> net.loss(x, t)
0.92806853663411326

이어서 기울기를 구해보자!

지금까지처럼 numerical_gradient(f, x)를 사용하면 된다.
(여기에서 정의한 f(W) 함수는 더미(dummy)로 만든 것. numerical_gradient(f, x) 내부에서 f(x)를 실행하는데, 그와의 일관성을 위해)

>>> def f(W):
... 	return net.loss(x, t)
...
>>> dW = numerical_gradient(f, net.W)
>>> print(dW)
[[ 0.21924763  0.14356247 -0.36281009]
 [ 0.32887144  0.2153437  -0.54421514]]

net.W를 인수로 받아 손실 함수를 계산하는 새로운 함수 f를 정의하고, 이 함수를 numerical_gradient(f, x)에 넘긴다.

dW을 보면,

  • 손실 함수를 줄인다는 관점에서 ∂L / ∂W₂₃은 양의 방향으로 갱신하고, w₁₁은 음의 방향으로 갱신해야 함
  • 한 번에 갱신되는 양에는 ∂L / ∂W₂₃w₁₁보다 크게 기여

참고로 이 구현에서는 새로운 함수를 정의하는데 "def f(x):..." 문법을 썼는데, 파이썬에서는 간단한 함수라면 람다(lambda) 기법을 쓰면 더 편하다.

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

신경망의 기울기를 구한 다음에는 경사법에 따라 가중치 매개변수를 갱신하기만 하면 된다.

여기에서 사용한 numerical_gradient()는 가중치 매개변수 W가 다차원 배열을 처리할 수 있도록 앞의 구현에서 조금 수정했다.
common/gradient.py 참고



5. 학습 알고리즘 구현하기

정리) 신경망 학습의 절차


전제

  • 신경망에는 적응 가능한 가중치와 편향이 있고, 이 가중치와 편향을 훈련 데이터에 적응하도록 조정하는 과정을 '학습'이라 한다. 신경망 학습은 다음과 같이 4단계로 수행한다.

1단계 - 미니배치

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

2단계 - 기울기 산출

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

3단계 - 매개변수 갱신

  • 가중치 매개변수를 기울기 방향으로 아주 조금 갱신한다.

4단계 - 반복

  • 1~3단계를 반복한다.

➡️ 이것이 신경망 학습이 이뤄지는 순서!

이는 경사 하강법으로 매개변수를 갱신하는 방법이며, 이때 데이터를 미니배치로 무작위로 선정하기 때문에 확률적 경사 하강법(stochastic gradient descent, SGD)라고 부른다.
'확률적으로 무작위로 골라낸 데이터'에 대해 수행하는 경사 하강법이라는 의미!

📌 대부분 딥러닝 프레임워크는 확률적 경사 하강법을 딴 SGD라는 함수로 이 기능을 구현하고 있다.


5-1) 2층 신경망 클래스(TwoLayerNet) 구현하기

실제로 손글씨 숫자를 학습하는 신경망을 구현해보자!
여기에서는 2층 신경망(은닉층이 1개인 네트워크)을 대상으로 MNIST 데이터셋을 사용하여 학습을 수행한다.


먼저 2층 신경망을 하나의 클래스로 구현해보자! (클래스 이름은 TwoLayerNet)

import sys, os
sys.path.append(os.pardir)
from common.functions import *
from common.gradient import numerical_gradient

class TwoLayerNet:
	def __init__(self, input_size, hidden_size, output_size, weight_init_std=0.01):
    	# 가중치 초기화
        self.params = {}
        self.params['W1'] = weight_init_std * \
        					np.random.randn(input_size, hidden_size)
		self.params['b1'] = np.zeros(hidden_size)
        self.params['W2'] = weight_init_std * \
        					np.random.randn(hidden_size, output_size)
		self.params['b2'] = np.zeros(output_size)

	def predict(self, x):
    	W1, W2 = self.params['W1'], self.params['W2']
        b1, b2 = self.params['b1'], self.params['b2']
        
        a1 = np.dot(x, W1) + b1
        z1 = sigmoid(a1)
        a2 = np.dot(z1, W2) + b2
        y = softmax(a2)
        
        return y
	
    # x: 입력 데이터, t: 정답 레이블
    def loss(self, x, t):
    	y = self.predict(x)
        
        return cross_entropy_error(y, t)
	
    def accuracy(self, x, t):
    	y = self.predict(x)
        y = np.argmax(y, axis=1)
        t = np.argmax(t, axis=1)
        
        accuracy = np.sum(y == t) / float(x.shape[0])
        return accuracy

	# x: 입력 데이터, t: 정답 레이블
    def numerical_gradient(self, x, t):
    	loss_W = lambda W: self.loss(x, t)
        
        grads = {}
        grads['W1'] = numerical_gradient(loss_W, self.params['W1'])
        grads['b1'] = numerical_gradient(loss_W, self.params['b1'])
        grads['W2'] = numerical_gradient(loss_W, self.params['W2'])
        grads['b2'] = numerical_gradient(loss_W, self.params['b2'])
        
        return grads


TwoLayerNet 클래스의 변수


변수설명
params신경망의 매개변수를 보관하는 딕셔너리 변수(인스턴스 변수)
params['W1']은 1번째 층의 가중치, params['b1']은 1번째 층의 편향
params['W2']는 2번째 층의 가중치, params['b1']는 2번째 층의 편향
grads기울기 보관하는 딕셔너리 변수(numerical_gradient() 메서드의 반환 값)
grads['W1']은 1번째 층의 가중치의 기울기, grads['b1']은 1번째 층의 편향의 기울기
grads['W2']는 2번째 층의 가중치의 기울기, grads['b2']는 2번째 층의 편향의 기울기

TwoLayerNet 클래스는 딕셔너리인 params와 grads를 인스턴스 변수로 갖는다.

1. params 변수

  • 가중치 매개변수가 저장됨 (신경망에 필요한 매개변수가 모두 저장됨)

    • 1번째 층의 가중치 매개변수는 params['W1'] 키에 넘파이 배열로 저장됨
    • 1번째 층의 편향은 params['b1'] 키로 접근
  • params 변수에 저장된 가중치 매개변수가 예측 처리(순방향 처리)에서 사용됨

net = TwoLayerNet(input_size=784, hidden_size=100, output_size=10)
net.params['W1'].shape # (784, 100)
net.params['b1'].shape # (100,)
net.params['W2'].shape # (100, 10)
net.params['b2'].shape # (10,)
# 예측 처리
x = np.random.rand(100, 784) # 더미 입력 데이터(100장 분량)
y = net.predict(x)

2. grads 변수

  • params 변수에 대응하는 각 매개변수의 기울기가 저장됨
    • 예를 들어 아래와 같이 numerical_gradient() 메서드를 사용해 기울기를 계산하면 grads 변수에 기울기 정보가 저장됨
x = np.random.rand(100, 784) # 더미 입력 데이터(100장 분량)
t = np.random.rand(100, 10)  # 더미 입력 레이블(100장 분량)

grads = net.numerical_gradient(x, t) # 기울기 계산

grads['W1'].shape # (784, 100)
grads['b1'].shape # (100,)
grads['W2'].shape # (100, 10)
grads['b2'].shape # (10,)


TwoLayerNet 클래스의 메서드


메서드설명
__init__(self, input_size, hidden_size, output_size)초기화를 수행한다.
인수는 순서대로 입력층의 뉴런 수, 은닉층의 뉴런 수, 출력층의 뉴런 수
predict(self, x)예측(추론)을 수행한다.
인수 x는 이미지 데이터
loss(self, x, t)손실 함수의 값을 구한다.
인수 x는 이미지 데이터, t는 정답 레이블(아래 칸의 세 메서드의 인수들도 마찬가지)
accuracy(self, x, t)정확도를 구한다.
numerical_gradient(self, x, t)가중치 매개변수의 기울기를 구한다.
gradient(self, x, t)가중치 매개변수의 기울기를 구한다.
numerical_gradient()의 성능 개선판! (구현은 다음 장에서)

1. __init__(self, input_size, hidden_size, output_size)

  • 클래스 초기화

    • 초기화 메서드는 TwoLayerNet을 생성할 때 불리는 메서드
  • 인수는 순서대로 입력층의 뉴런 수, 은닉층의 뉴런 수, 출력층의 뉴런 수

    • 예를 들어 손글씨 숫자 인식에서는 크기가 28x28인 입력 이미지가 총 784개이고 출력은 10개
      ➡ input_size=784, output_size=10, 은닉층의 개수인 hidden_size는 적당한 값으로 설정
  • 가중치 매개변수도 초기화

    • 가중치 매개변수의 초깃값을 무엇으로 설정하냐가 신경망 학습의 성공을 좌우하기도 함
    • 당장은 정규분포를 따르는 난수로, 편향은 0으로 초기화한다고 생각하고 넘어가기 (자세한 내용은 추후에)

2. predict(self, x)

3. accuracy(self, x, t)

위의 두 메서드는 앞에서 본 신경망의 추론 처리와 거의 같으므로 넘어간다.


4. loss(self, x, t)

  • 손실 함수의 값을 계산하는 메서드

  • predict()의 결과와 정답 레이블을 바탕으로 교차 엔트로피 오차를 구하도록 구현함


5. numerical_gradient(self, x, t)

  • 각 매개변수의 기울기를 계산하는 메서드
    • 수치 미분 방식으로 각 매개변수의 손실 함수에 대한 기울기 계산

다음 장에서는 numerical_gradient()의 기울기 계산을 고속으로 수행하는 기법을 설명한다. ➡ 오차역전파법
오차역전파법을 쓰면 수치 미분을 사용할 때와 거의 같은 결과를 훨씬 빠르게!


6. gradient(self, x, t)

다음 장에서 구현할 메서드!

오차역전파법을 사용하여 기울기를 효율적이고 빠르게 계산

신경망 학습은 시간이 오래 걸리니, 시간을 절약하려면 numerical_gradient() 대신 gradient()를 쓰는 것이 좋다.



5-2) 미니배치 학습 구현하기

신경망 학습 구현에 미니배치 학습을 활용해보자!

미니배치 학습: 훈련 데이터 중 일부를 무작위로 꺼내고(미니배치), 그 미니배치에 대해서 경사법으로 매개변수를 갱신


TwoLayerNet 클래스와 MNIST 데이터셋을 사용해 학습을 수행해보자!

import numpy as np
from dataset.mnist import load_mnist
from two_layer_net import TwoLayerNet

(x_train, t_train), (x_test, t_test) = \
	load_mnist(normalize=True, one_hot_label=True)
    
train_loss_list = []

# 하이퍼파라미터
iters_num = 10000 # 반복 횟수
train_size = x_train.shape[0]
batch_size = 100 # 미니배치 크기
learning_rate = 0.1

network = TwoLayerNet(input_size=784, hidden_size=50, output_size=10)

for i in range(iters_num):
	# 미니배치 획득
    batch_mask = np.random.choice(train_size, batch_size)
    x_batch = x_train[batch_mask]
    t_batch = t_train[batch_mask]
    
    # 기울기 계산
    grad = network.numerical_gradient(x_batch, t_batch)
    # grad = network.gradient(x_batch, t_batch) # 성능 개선판!
    
    # 매개변수 갱신
    for key in ('W1', 'b1', 'W2', 'b2'):
    	network.params[key] -= learning_rate * grad[key]
	
    # 학습 경과 기록
    loss = network.loss(x_batch, t_batch)
    train_loss_list.append(loss)

미니배치 크기 = 100
➡️ 매번 600,000개의 훈련 데이터에서 임의로 100개의 데이터(이미지 데이터와 정답 레이블 데이터)를 추려낸다.
➡️ 그 100개의 미니배치를 대상으로 확률적 경사 하강법을 수행해 매개변수를 갱신


경사법에 의한 갱신 횟수(반복 횟수) = 10,000
➡️ 갱신할 때마다 훈련 데이터에 대한 손실 함수를 계산하고, 그 값을 배열에 추가

반복 횟수가 늘어가면서 손실 함수의 값이 줄어드는 것은 학습이 잘 되고 있다는 뜻!

  • 신경망이 가중치 매개변수가 서서히 데이터에 적응하고 있음을 의미
  • 데이터를 반복해서 학습함으로써 최적 가중치 매개변수로 서서히 다가서고 있다는 의미

5-3) 시험 데이터로 평가하기

import numpy as np
from dataset.mnist import load_mnist
from two_layer_net import TwoLayerNet

(x_train, t_train), (x_test, t_test) = \
	load_mnist(normalize=True, one_hot_label=True)
    
network = TwoLayerNet(input_size=784, hidden_size=50, output_size=10)

# 하이퍼파라미터
iters_num = 10000 # 반복 횟수
train_size = x_train.shape[0]
batch_size = 100 # 미니배치 크기
learning_rate = 0.1

train_loss_list = []
train_acc_list = []
test_acc_list = []

# 1에폭당 반복 수
iter_per_epoch = max(train_size / batch_size, 1)

for i in range(iters_num):
	# 미니배치 획득
    batch_mask = np.random.choice(train_size, batch_size)
    x_batch = x_train[batch_mask]
    t_batch = t_train[batch_mask]
    
    # 기울기 계산
    grad = network.numerical_gradient(x_batch, t_batch)
    # grad = network.gradient(x_batch, t_batch) # 성능 개선판!
    
    # 매개변수 갱신
    for key in ('W1', 'b1', 'W2', 'b2'):
    	network.params[key] -= learning_rate * grad[key]
	
    # 학습 경과 기록
    loss = network.loss(x_batch, t_batch)
    train_loss_list.append(loss)
    
	# 1에폭당 정확도 계산
    if i % iter_per_epoch == 0:
    	train_acc = network.accuracy(x_train, t_train)
        test_acc = network.accuracy(x_test, t_test)
        train_acc_list.append(train_acc)
        test_acc_list.append(test_acc)
        print("train acc, test acc | "
        	+ str(train_acc) + ", " + str(test_acc))

6. 정리

  • 신경망이 학습을 수행할 수 있도록 손실 함수라는 '지표'를 도입
    ➡️ 이 손실 함수를 기준으로 그 값이 가장 작아지는 가중치 매개변수 값을 찾아내는 것이 신경망 학습의 목표

  • 가능한 한 작은 손실 함수의 값을 찾는 수법으로 경사법을 소개
    ➡️ 경사법은 함수의 기울기를 이용하는 방법

이번 장에서 배운 내용

  • 기계학습에서 사용하는 데이터셋은 훈련 데이터시험 데이터로 나눠 사용한다.
  • 훈련 데이터로 학습한 모델의 범용 능력을 시험 데이터로 평가한다.
  • 가중치 매개변수를 갱신할 때는 가중치 매개변수의 기울기를 이용하고, 기울어진 방향으로 가중치의 값을 갱신하는 작업을 반복한다.
  • 아주 작은 값을 주었을 때의 차분으로 미분하는 것을 수치 미분이라고 한다.
  • 수치 미분을 이용해 가중치 매개변수의 기울기를 구할 수 있다.
  • 수치 미분을 이용한 계산에는 시간이 걸리지만 구현은 간단하다.
    ➡️ 다음 장에서 구현하는 오차역전파법은 기울기를 고속으로 구할 수 있다.

0개의 댓글