4. 신경망 학습

[4.1] 데이터에서 학습한다!

인공 신경망 개념의 확장
2장 퍼셉트론: 뉴런 + 가중치 + 편향
예제: 기본적인 논리 회로 구현(AND, OR, NAND, XOR)
3장 신경망: 은닉층 + 활성화 함수 + 순전파
신경망에 여러 은닉층을 쌓아서 매개변수 수를 늘리고, 선형 과정을 비선형 과정으로 변환하는 활성화 함수를 도입하면 훨씬 어려운(비선형) 문제를 신경망으로 다룰 수 있다.
예제: 숫자 필기 인식 및 분류 모델(MNIST)
4장 신경망 학습: 손실 함수 + 경사 하강법
두 개념의 추가를 통해 기계학습 분야에서 말하는 “학습”의 개념을 구현
학습: 훈련 데이터로부터 매개변수 최적값자동으로 결정하는 것
손실 함수: 신경망의 출력 값(예측)과 정답 사이의 오차를 계산하는 함수. 이를 통해, 학습 단계에서 신경망의 성능(모의고사 점수)을 평가
경사 하강법: 미분을 이용하여 손실 함수 값을 가장 작게 만드는 기법매개변수의 최적화
데이터 주도 학습
데이터 패턴 탐색 방법
사람에 의한 조사(inspection): 사람의 경험과 직관에 의존적. 큰 데이터에는 적합하지 않음
통계 기법: 큰 데이터를 요약하는 통계량들을 통해 데이터를 이해
기계 학습: 큰 데이터로부터 패턴 자동 탐색
손글씨 숫자’5’을 분류하는 프로그램을 직접 밑바닥부터 만든다면?
기계학습에서는 기계가 데이터의 규칙성을 효율적으로 찾아내지만 문제에 적합한 특징을 설계하는 것은 여전히 사람에 의존 (예: 안면 인식 문제)
반면, 딥러닝의 경우 기계가 스스로 학습하기 때문에 특징을 설계할 수 있음
딥러닝을 입력 데이터로부터 목표한 결과를 사람의 개입 없이 얻는다는 의미로 end-to-end machine learning 이라고도 한다.
AutoML(Automated Machine Learning): 특징 설계를 포함한 데이터 전처리, 모델 학습 및 선택을 포함하는 기계학습의 전 과정을 자동화하는 기술
기계학습 패러다임의 변화. 회색 블록은 사람이 개입하지 않음을 뜻한다.

[4.2] 손실함수

훈련 데이터와 시험 데이터
기계학습에서는 데이터를 훈련 데이터시험 데이터로 나눠 학습과 평가를 수행
훈련 데이터(training data): 학습 단계에서 사용되는 데이터(문제집)
시험 데이터(test data): 훈련된 모델의 성능을 평가할 때 사용되는 데이터(최종 시험)
훈련 데이터, 검증(validation) 데이터, 시험 데이터로 나눠서 수행하기도 함
우리가 앞서 살펴본 MNIST는 출력벡터의 인덱스와 우리가 원하는 값인 숫자가 동일해서 인덱스로 접근해서 해결할 수 있었다.
그러나 이러한 상황에서도 인덱스로 접근해서 해결할 수 있을까?
이처럼 수식에서는 입력이든 출력이든 모두 숫자만 들어갈 수 있기 때문에,
표현하고 싶은 단어나 범주형 데이터인 경우에는 우리가 이전까지 사용했던 모델에 바로 사용할 수 없다.
<one - hot encoding>
따라서 표현하고 싶은 단어의 인덱스에 1의 값을 부여하고, 다른 인덱스에 0을 부여하는 단어의 벡터 표현이 one-hot encoding이라고 부른다.
숫자가 아닌 데이터는 one-hot encoding을 통해 숫자 형태로 바꾸어주는 과정을 거쳐야 뉴런 네트워크에서 학습이 가능하다.

손실함수

뉴런 네트워크에게 단순히 데이터들만 input 시킨다고 해서 스스로 정답률이 높은 머신으로 학습을 할 수 있을까?
시험준비를 위해 문제를 많이 풀기만 하고, 채점을 안하면 실력이 늘지 않을 것이다. 실력을 향상시키려면 정답과 오답을 체크하고, 왜 틀렸는지 학습을 할 필요가 있음
뉴런 네트워크를 학습시키기 위해서 벌점이라는 점수를 주고, 예측한 정답과 라벨(실제 정답)간의 차이(오차)를 학습해서 라벨에 가까워져야함
위의 예시에서의 벌점은 손실 함수로, 뉴런 네트워크(인공신경망)의 성능이 좋을수록 손실함수 값이 낮고, 성능이 나쁠수록 손실함수 값이 높음
일반적으로 손실함수는 오차제곱합(sum of squares for error SSE)교차 엔트로피 오차(cross entropy error CEE)를 사용함
★★★ 손실함수를 알아보기 전에 명심해야 할 사실은 ★★★
1) 머신의 출력값은 확률벡터(softmax 과정을 거치므로)
2) 확률의 합은 항상 1
3) 확률은 음수가 존재하지 않음

오차제곱합

예를 들어,
x축 : 비가 올 확률
y축 : 비가 오지 않을 확률
★ x + y =1이므로
우리는 one-hot encoding을 통해
비가 올 확률이 100%인 경우를 → (1,0)
비가 오지 않을 확률이 100%인 경우를 → (0,1)로 정의하면,
이 두 점으로부터 거리를 측정했을 때, 거리가 가까울수록 예측을 더 잘했다고 직관적으로 이해할 수 있다.
여기서는 손실 함수의 값은 0 ~ √2 사이에 존재하게 된다.
이를 통해 손실 함수의 값을 더 줄여가는 방향으로 학습하면 된다.
이 개념을 일반화시킨 것이 오차제곱합이다.
MNIST를 예를 들어보면,
‘0’ 이라는 데이터를 뉴런 네트워크에 넣었더니, 머신은
(0.8,0.05,0.1,0.05….) ⇒ y 이라는 확률 벡터를 알려주었다.
0의 one-hot encoding의 값 ⇒ t 는 (1,0,0,0,0….) 이므로
이에 대해 평균 제곱 오차를 구하면
[ (1-0.8)^2 + (0-0.5)^2 + (0 - 0.1)^2 + ….] 의 값이 나오게 된다.
Q: 그렇다면 왜 두 점 사이의 거리를 측정하는 것이라면서 √를 사용하지 않을까?
Q: 그리고 왜 마지막에 1/2을 곱해주는 것일까?
A: 결국에 우리는 오차를 계산하는 것이 목표이지, 두 점 사이의 정확한 거리를 측정하는 것이 아니다.
또한 머신러닝은 미분을 통해서 학습을 하는데 √가 존재하면 계산하기 매우 번거롭기 때문에 √를 제외하고, 평균제곱의 오차로 정의한다고 한다.
또한,
미분을 했을 경우 2라는 숫자가 모두 생기기 때문에, 이를 계산의 편의성을 위해 미리 1/2을 곱하는 것이라고 한다.
※ 현재는 하나의 이미지를 학습한 경우의 평균제곱의 오차값이고, 배치처리처럼 다양한 이미지를 학습할 때는 평균제곱의 오차값들의 평균값으로 도출한다고 한다.

교차 엔트로피 오차

우선 여기서도 위와 동일하게 t와 y는 확률을 나타내므로 log에 들어가는 값 yk는 0부터 1사이의 값을 갖게 된다. 따라서 우리는 로그 함수 전체를 참고할 필요가 없고, x가 0부터 1까지의 범위만 생각해보면 된다.
이런 모양이 된다.
결국 각 이미지에 대해서 교차 엔트로피 오차값은 -log yk 값만 나오므로,
배치처리를 하면 batch_size 만큼의 log 값을 더해준 후, 평균을 내주면 된다.
#MNIST 데이터셋 읽어오기 import sys,os sys.path.append(os.pardir) # error 발생시 참고 import numpy as np from dataset.mnist import load_mnist #error 발생 지점 -> dataset.mnist를 찾지 못함 (x_train, t_train), (x_test, t_test) = load_mnist(normalize =True, one_hot_label= True) def sum_squared_error(y,t) : return 0.5 * np.sum( (y-t)**2) def cross_entropy_error(y,t) : if y.ndim == 1: t=t.reshape(1,t.size) y=y.reshape(1,y.size) if t.size == y.size: t= t.argmax(axis =1) batch_size = y.shape[0] return -np.sum(np.log(y[np.arange(batch_size), t ] ) ) / batch_size #return -np.sum(np.sum( t* np.log( y + 1e-7) ) / batch_size # def cross_entropy_error(y,t): #return -np.sum(t * np.log(y+ 1e-7) ) -> 1e-7를 곱하는 이유는 y가 0이 되어 -inf가 되는 것을 방지하기 위해서
Python
복사
<sys.path>
다른 파일에 있는 파일을 불러오고 싶을 때, python이 하드디스크에 있는 모든 파일을 읽고 불러오려면 많은 시간이 필요할 것.
따라서 디렉토리에 경로들을 list로 path에 저장하면, python이 path에 저장된 경로만 검색하도록 하는 역할을 함
<sys.path.append(os.pardir)>
x=[1,2]일 때, x.append(3)을 하면, x= [1,2,3]이 되듯이, sys.path.append(os.pardir)를 하여 os.pardir를 추가시키는 명령어이다. -> os.pardir => os.parent_directory이므로 현재 작업하는 파일(os)의 parent의 directory를 추가한다.
하지만, MNIST 데이터셋을 불러와 코드를 실행해야 하는데, 현재 작업하고 있는 파일의 디렉토리와 parent director에 데이터셋이 존재하지 않으면, 이 코드는 error가 발생한다.
따라서, 만약에 error가 발생한다면, mnist.py를 포함하고 있는 dataset이 있는 디렉토리의 경로를 append해주면, error가 해결된다.
<if y.ndim == 1>
y는 머신이 예측한 확률벡터를 말하는데, 이미지 한 개를 넣으면 10차원 벡터를,
n개를 넣으면 n by 10 행렬로 나타낼 수 있다.
즉 y.ndim 이 1이라는 것은 배치처리를 안해줬을 경우를 의미하는데,
cross_entropy_error 함수를 일반화하기 위해서 행렬 간의 연산으로 구현되어 있다.
python에게는 벡터와 행렬을 명확하게 알려줘야 하기 때문에, 벡터를 1 by t.size(y.size) 행렬로 reshape 해줄 필요가 있다.
예를 들어보자.
A가 5차원 벡터라고 하고, 이에 대해 A.shape[0]을 출력하면 5가 출력된다.
마찬가지로 y가 10차원 벡터이면, batch_size = y.shape[0] 에서 batch_size가 10이 되는데,
이는 이미지 1개를 cross_entropy_error를 구하는 과정에서 1개의 cross_entropy_error를 구하고, batch_size(10)개로 나누어버리는 논리적 오류가 발생한다.
따라서 reshape을 통해 이러한 오류를 방지해야 한다.
<if t.size == y.size >
배치처리를 n개로 했다면, 라벨(t)는 n by 1 행렬이 되고, 라벨의 one-hot encoding을 했다면 n by 10 행렬이 될 것이다.
따라서 one-hot encoding을 했다면, t와 y의 size가 n*10이 될테니 결국에 one-hot encoding 여부를 물어보는 조건문
<t= t.argmax(axis =1 ) >
one-hot encoding을 했던 t를 one-hot encoding 하기 전의 t로 되돌려주는 코드
<batch_size = y.shape[0]>
y가 n by 10 즉, y.shape[0]은 n, 즉 배치 사이즈를 의미함
<return –np.sum(np.log(y[np.arrage(batch_size), t] ) ) / batch_size>
np.arrage(batch_size) = [0, 1, 2, 3, ... , n-1] t(라벨) = [ 0, 2, 1, ...... ] => n개 단 0부터 9까지의 수로 이루어짐.
y는 n by 10이므로 y의 행렬의 각 원소 (0,0), (1,2), (3,1) .... 의 원소로 이루어진 n차원 벡터가 도출된다. => y[np.arrage(batch_size), t ] 의 결과
<보충학습 필요>
목적: 높은 정확도를 끌어내는 매개변수 값을 찾기
Q: 그렇다면 왜 정확도라는 지표를 사용하지 않고, 손실 함수의 값이라는 지표로 우회적인 방법을 택할까?
<가중치 매개변수의 손실 함수의 미분>
가중치 매개변수의 값을 아주 조금 변화시켰을 때, 손실 함수가 어떻게 변화는가?
음의 미분값 → 가중치 매개변수를 양의 방향으로 변화시켜 손실 함수의 값 줄이기
양의 미분값 → 가중치 매개변수를 음의 방향으로 변화시켜 손실 함수의 값 줄이기
그러나, 미분 값이 0이면 가중치 매개변수를 어느 쪽으로 움직여도 손실 함수의 값은 줄어들지 않는다.
Q: 정확도를 지표로 삼으면 매개변수의 미분이 대부분의 장소에서 0이 된다?
정확도는 매개변수의 미소한 변화에는 거의 반응을 보이지 않고, 반응이 있더라도 그 값이 불연속적으로 갑자기 변화함
이는 우리가 신경망에서 계단 함수를 사용하지 않고, 시그모이드 함수를 사용하는 이유가 유사함.

[4.3] 수치 미분

미분
미분(differentiation) : 한 순간의 변화량
Δx\Delta {x} (xx의 변화량)을 hh로 치환하여 사용하기도 함
hh→0 : 서로의 차이가 점점 줄어든다 (hh를 0으로 줄이며 계산)
df(x)dx=limh0f(x+h)f(x)h{df(x) \over dx} = \lim_{h \to 0} {f(x+h)-f(x)\over h}
수치 미분
수치 미분(numerical differentiation) : 함수의 값을 이용하여 미분 값을 근사화하는 방법
근사화(approximation) : 어떤 값을 정확하게 구하기 어려운 경우에 해당 값을 근접한 값으로 추정하는 것
함수의 미분을 해석적으로 구하기 어려운 경우 사용 (수치 미분은 근사값으로 계산)
해석적(analytic) : 수학적으로 정확하고 명확한 방법으로 계산하는 것
함수의 입력값을 약간씩 조정하며 함숫값을 변화시킴
ex) 0.3333…→ 0.33 또는 0.34와 같은 근사값으로 표현
미분의 정확성을 일부 포기하지만 계산적으로 간단히 미분값을 구할 수 있음.
수치 미분의 종류
차분(difference) : 임의의 두 점에서의 함숫값 차이
함숫값의 차를 이용해 미분계수(순간변화율)를 근사하는 방법
전방 차분(Forward Difference) : 함수의 어떤 지점 xx에서 f(x)f(x)f(x+h)f(x+h)의 차분 계산
주로 함수가 증가하는 영역의 기울기를 근사할 때 사용 가능
f(x)f(x+h)f(x)hf'(x) \approx {{f(x+h)-f(x)} \over h}
후방 차분(Backward Difference) : 함수의 어떤 지점 xx에서 f(x)f(x)f(xh)f(x-h)의 차분 계산
주로 함수가 감소하는 영역의 기울기를 근사할 때 사용 가능
f(x)f(x)f(xh)hf'(x) \approx {{f(x)-f(x-h)} \over h}
중심 차분(Central Difference) : 함수의 어떤 지점 xx에서 f(x+h)f(x+h)f(xh)f(x-h)의 차분 계산
f(x)f(x+h)f(xh)2hf'(x) \approx {{f(x+h)-f(x-h)} \over 2h}
수치 미분 시 발생하는 수치 문제
문제 1. (디지털 시스템 환경일 경우) 반올림 오차 → 해결: hh를 매우 작은 양수값으로 설정
반올림 오차(rounding error) : 디지털 시스템에서 실수를 부동소수점 방식으로 표현하기 위해 근사화하는 과정에서 발생하는 실제 값과 근삿값 간의 오차
hh 를 적당히 작은 값으로 설정해야 함 → 104(=0.0001)10^{-4}(=0.0001)
# 함수 미분 (나쁜 구현 예시) def numerical_diff(f, x): h = 1e-50 # 0.00000000...1 return (f(x+h) - f(x)) / h
Python
복사
np.float32(1e-50) # 0.0
Python
복사
# 개선된 함수 미분(h 값을 키움) def numerical_diff(f, x): h = 1e-4 # 0.0001 return (f(x+h) - f(x-h)) / (2*h)
Python
복사
문제 2. 실제 값과의 오차 → 해결: 중심 차분 사용
두 개의 함숫값(지점)을 사용하기 때문 오차를 상쇄시키고 더 정확한 근사값을 얻을 수 있음
(a) : 전방 차분, (b) 후방 차분, (c) 중심 차분
4.3.2 수치 미분의 예
y=0.01x2+0.1x\mathrm {y} = 0.01x^2 + 0.1x
# 함수 구현 def function_1(x): return 0.01*x**2 + 0.1*x
Python
복사
# 개선된 수치 미분 구현 (중심 차분) def numerical_diff(f, x): h = 1e-4 # 0.0001 return (f(x+h) - f(x-h)) / (2*h)
Python
복사
numerical_diff(function_1, 5) # 0.1999999999990898 numerical_diff(function_1, 10) # 0.2999999999986347
Python
복사
# 함수 시각화 x = np.arange(0.0, 20.0, 0.1) y = function_1(x) plt.xlabel("x") plt.ylabel("f(x)") plt.plot(x, y) plt.show()
Python
복사
f(x)=0.01x2+0.1xf(x) = 0.01x^2 + 0.1x의 해석적 해는 df(x)dx=0.02x+0.1{df(x) \over dx} = 0.02x + 0.1로, xx가 5 와 10일 때 ‘진정한 미분’은 0.2와 0.3 → 오차가 매우 작음
x=5, x=10에서의 접선(수치 미분 계산으로 구한 기울기 적용)
편미분이란?
편미분(partial derivative) : 다변수 함수의 특정 변수를 제외한 나머지 변수를 상수로 간주하여 미분하는 것
일변수 함수는 그래프 위 하나의 점에 1개의 접선만 존재하지만 다변수 함수는 무수히 많은 접선이 존재
변수가 2개 이상인 함수에서 수많은 접선 중 변수 1개의 변화에 따른 변화율(기울기, 순간변화율)을 구할 때 사용
ex 1) 두 개의 변수 x0,x1x_0, x_1를 가지는 함수
f(x0, x1)=x02+x12f(x_0,\ x_1) = x_0^2 + x_1^2
f(x0,x1)=x02+x12f(x_0, x_1) = x_0^2 + x_1^2 의 함수 시각화
x0x_0에 대한 편미분 : fx0\frac{∂f}{∂x_0}, x1x_1에 대한 편미분: fx1\frac{∂f}{∂x_1}
ex 2) (x, y, z)라는 3개의 시험 과목에 대해, x과목의 점수가 변화했을 때 전체 평균이 얼마나 변할까? → x에 대해 편미분(1점 떨어질 때 평균은 0.3씩 떨어짐)
전미분
편미분 예제
편미분 구현
def numerical_diff(f, x): h = 1e-4 # 0.0001 return (f(x+h) - f(x-h)) / (2*h)
Python
복사
x0=3,x1=4x_0 = 3, x_1 = 4일 때, x0x_0에 대한 편미분 fx0\partial f \over \partial x_0 구하기
def function_tmp1(x0): return x0*x0 + 4.0**2.0 numerical_diff(function_tmp1, 3.0) # 6.00000000000378
Python
복사
x0=3,x1=4x_0 = 3, x_1 = 4일 때, x1x_1에 대한 편미분 fx1\partial f \over \partial x_1 구하기
def function_tmp2(x1): return 3.0**2.0 + x1*x1 numerical_diff(function_tmp2, 4.0) # 7.999999999999119
Python
복사

[4.4] 기울기

다변수 함수의 편미분을 동시에 계산하고 싶을 때는? → 벡터 형태로 계산
기울기(gradient) : 모든 변수의 편미분을 벡터로 정리한 것
ex) x0x_0x1x_1의 편미분 벡터(기울기) = (fx0,fx1)({\partial f \over \partial x_0}, {\partial f \over \partial 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] # 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 # 함수 정의 def function_2(x): return x[0]**2 + x[1]**2 # return np.sum(x**2)도 가능 -> x는 2차원 벡터
Python
복사
numerical_gradient(function_2, np.array([3.0, 4.0])) # array([6., 8.]) numerical_gradient(function_2, np.array([0.0, 2.0])) # array([0., 4.]) numerical_gradient(function_2, np.array([3.0, 0.0])) # array([6., 0.])
Python
복사
⇒ 기울기는 각 장소에서 함수의 출력값을 가장 크게 줄이는 방향을 가리킴
f(x0,x1)=x02+x12f(x_0, x_1) = x_0^2 + x_1^2 의 기울기
최적화
모델의 성능을 높이기 위해서는
손실 함수의 값을 최소화하여 최적의 매개변수(가중치와 평균)를 찾아야함
기울기의 방향과 크기를 통해 함수가 증가 또는 감소하는 방향을 파악하고 학습률을 조정하며 최적의 매개변수를 찾음
경사하강법
눈을 감고 산을 내려가는 것 - 현재 서 있는 곳에서 가장 낮은 방향으로 한 발씩 내딛으며 내려간다
경사법(gradient method) : 손실 함수의 기울기를 이용해 매개변수를 최적화 하는 알고리즘
경사 하강법(gradient descent method) : 손실 함수를 최소화하는 매개변수를 찾을 때 사용
경사 상승법(gradient ascent method) : 손실 함수를 최대화하는 매개변수를 찾을 때 사용
⇒ 손실 함수가 최솟값이 될 때, 매개변수 값은 최적
경사하강법을 사용하는 이유
‘미분계수가 0인 지점을 찾으면 되지 않나?’
다양한 함수에 범용적으로 사용 가능 : 실제 분석에 사용되는 함수는 닫힌 형태(closed form)가 아니거나 복잡(비선형 등)하기 때문에 미분식을 구하기 어려운데 경사 하강법은 미분식을 필요로 하지 않기 때문에 복잡한 모델에도 적용이 가능함
로컬 최적화 방지 : 미분계수가 0인 지점이 최솟값이 아닌 극솟값이나 안장점일 수 있음 → 로컬 최적화를 방지하는 방법 적용
극솟값(local minima) : 국소 범위에서의 최솟값 (극댓값 : local maxima)
안장점(saddle point) : 방향에 따라 극댓값 또는 극솟값이 되는 점
고원 상태 방지 : 함수의 값이 거의 변하지 않는 지점인 고원(plateau, 플레토)이 많을 경우 더 이상 최적화가 진행되지 않고 고원 상태에 빠질 수 있음 → 경사하강법을 통해 함수의 모양에 더 민감하게 반응하거나 학습률을 조정해 고원에서도 최적화되도록 전역 최솟값 탐색
고차원(다변수) 데이터의 손실함수 그래프
saddle point, 말 안장과 닮은 생김새
로컬 최적화를 방지하는 방법
경사하강법 수식
x0 next=x0ηfx0x1 next=x1ηfx1x_0^{\mathrm {\ next }} = x_0 - \eta {\partial f \over \partial x_0} \qquad \quad x_1^{\mathrm {\ next }} = x_1 - \eta {\partial f \over \partial x_1}
좌변 x0,x1x_0, x_1 : 업데이트된 매개변수를 의미
\partial (round, 라운드) : 델타의 변형된 모습으로 fx{\partial f \over \partial x}는 ‘xx에 대한 ff의 편미분’이라고 읽음
η\eta (eta, 에타) : 신경망 학습에서의 학습률(learning rate)
학습률
학습률이 너무 작으면 학습 시간이 많이 소요되고, 학습률이 너무 크면 시간이 적게 소요되지만 최저점을 지나칠 수 있음 ⇒ 학습률을 점차적으로 줄여나가는 방식으로 문제 개선
하이퍼 파라미터(Hyper parameter, 초매개변수) : 데이터로부터 자동 학습되지 않고 사람이 직접 설정해야 하는 매개변수
모델 내부 학습에 의해 자동으로 얻어지는 매개변수와 달리 데이터로부터 얻어지지 않음
모델의 학습 과정과 구조를 결정하는 데에 사용
여러 값들의 실험을 통한 적절한 설정이 필요함
학습률(learning rate), 배치 크기(batch size), 에폭(epoch) 수 , 신경망의 은닉층 수, 은닉층의 뉴런 수 등이 초매개변수에 해당
경사하강법 과정
(1) 시작점 선택: 최적화 할 파라미터(매개변수) 초기화
(2) 시작점에서 손실 함수의 기울기(gradient) 계산: 파라미터를 업데이트 할 방향 결정
(3) 매개변수 업데이트 : 학습률(learning rate)을 통해 강도 설정
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 # 학습률X기울기 return x
Python
복사
⇒ 현 지점(매개변수) init_x 에서 step_num만큼 경사하강법을 적용하여 손실 함수 f를 최소화하는 매개변수 x의 값을 찾는 함수로 → 최종 계산된 x 를 최적화된 매개변수 값으로 반환
신경망 학습
신경망에서의 기울기 = 가중치 매개변수에 대한 손실 함수의 기울기
손실 함수에서 가중치가 얼마나 영향을 미치는지, 즉 가중치를 어떤 방향으로 조절해야 손실함수를 줄일 수 있는지를 확인 가능
W={w11 w12 w13w21 w22 w23\mathrm W = \begin{cases} \mathrm{w_{11}\ w_{12}\ w_{13}} \\ \mathrm{w_{21}\ w_{22}\ w_{23}} \end{cases}
LW={Lw11 Lw12 Lw13Lw21 Lw22 Lw23{\partial L \over \partial\mathrm W} = \begin{cases} {\partial L \over \partial\mathrm w_{11}}\ {\partial L \over \partial\mathrm w_{12}}\ {\partial L \over \partial\mathrm w_{13}} \\ {\partial L \over \partial\mathrm w_{21}}\ {\partial L \over \partial\mathrm w_{22}}\ {\partial L \over \partial\mathrm w_{23}}\end{cases}
W\mathrm W : 가중치
LL : 손실함수
LW\partial L \over \partial \mathrm W : 기울기
LW\partial L \over \partial \mathrm W의 각 원소는 각각에 대한 편미분에 해당 (w\mathrm w를 조정했을 떄 LL의 변화)
W\mathrm WLW\partial L \over \partial \mathrm W의 크기는 서로 같다
신경망에서 기울기를 구하는 코드 구현
simpleNet
2x3 크기의 가중치 매개변수 하나를 인스턴스 변수로 가짐
predict(x) : 예측 수행
loss(x, t) : 손실 함수의 함숫값 구하기
(x: 입력 데이터, t: 정답 레이블)
class simpleNet: def __init__(self): self.W = np.random.randn(2,3) # 정규분포를 따르는 랜덤 수 생성 -> 2X3 행렬 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()
Python
복사
f = lambda w: net.loss(x, t) # net클래스의 loss 메서드를 호출해서 손실값 계산 dW = numerical_gradient(f, net.W) # net.W를 인수로 받아 기울기 계산 print(dW) # 2차원 배열
Python
복사
경사하강법의 종류
배치 경사하강법(Batch Gradient Descent, BGD)
모든 데이터 포인트들에 대해 파라미터 업데이트
비효율적(속도가 느림)
확률적 경사하강법(Stochastic Gradient Descent, SGD)
랜덤 샘플링된 각 데이터 포인트마다 파라미터 업데이트
무작위성으로 local minima에 빠질 가능성이 낮아짐
미니 배치 경사하강법(Mini Batch Gradient Descent, Mini batch SGD)
랜덤 샘플링된 데이터 포인트들을 미니 배치(부분 집합)로 묶어 파라미터 업데이트
SGD의 노이즈를 줄이면서 BGD보다 효율적임
보편적으로 많이 사용

[4.5] 학습 알고리즘 구현하기

Q: 4.4에서 경사 하강법(gradient descent)에 대해 설명하다가 갑자기 확률적 경사 하강법(stochastic gradient descent)에 대해 이야기하는데 둘이 어떤 차이점이 있는 것일까?
A: ⇒ chat gpt의 도움을 받아 작성했습니다
Gradient Descent computes the gradient using the entire training dataset to make a single update to the model parameters.
Stochastic Gradient Descent updates the model parameters using only a single training example (or a small batch of training examples) at a time.
chat GPT4에게 Gradient Descent와 Stochastic Gradient Descent에 대한 차이점에 대해 물어본 답변
정리하면, 뉴런 네트워크에 모든 데이터에 대해 손실함수를 구하고, 이들의 손실함수의 평균을 구해서 경사하강법을 구하면 학습 비용이 너무 크다는 문제가 존재하므로,
모든 데이터를 다 적용하는 것이 아닌 대표적으로 랜덤으로 선택해서 학습시키면, 전체 데이터로 학습한 것과 결과는 다르지만, 표본을 크게 잡으면 실제와의 오차는 적을 것이다.
따라서 정확도를 줄이더라도 학습비용을 줄이는 방향으로 접근하는 방법(Stochastic Gradient Descent)
cf) Q: 경사하강법을 이해하기 쉬운 방법이 있을까?
A:
Chat GPT4에게 경사하강법을 이해하기 쉽게 설명해달라고 부탁했을 때의 답변
그럼 하나의 의문점이 든다.
모든 데이터를 적용하는 방법이 학습 비용이 너무 커서 일부를 무작위로 선별해서 학습시키는 것은 이해가 된다(이를 미니배치라고 책에서 서술)
그러나,
Q: 왜 Gradient Descent와 다르게 Stochastic Gradient Descent의 학습 과정을 보면, 지그재그로 이동을 할까?
Gradient Descent
Stochastic Gradient Descent
A:
정리하면, 결국에 데이터는 함수에서 계수( ay+bx+c에서 a,b,c)역할을 하게 되며, 각 단계마다 랜덤으로 샘플링을 하여 경사하강법을 적용한다.
다시 말해서, 이는 매 단계마다 데이터가 달라짐에 따라 함수의 그래프가 달라지고, 이에 따라 경사 하강도의 목표 지점도 달라지기 때문에 지그재그로 나아가는 것이다.

4.5.1 2층 신경망 클래스 구현하기

ch04/two_layer_net.py 소스코드 이해하기
이를 이해하기 전에 우선 4.4에서 보았던 ch04/gradient_simplenet.py를 살펴보는 것이 좋을 듯하다.
#ch04 / gradient_simplenet.py # coding: utf-8 import numpy as np import matplotlib.pylab as plt from gradient_2d import numerical_gradient 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) def function_2(x): return x[0]**2 + x[1]**2 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()
Python
복사
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) # 학습이 끝나고 성능을 측정할 때 미분이 필요X -> 정확도로 측정함 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 # 이 코드는 역전파를 구현한 코드이므로 6장 학습 후 다시 확인해보기!! def gradient(self, x, t): W1, W2 = self.params['W1'], self.params['W2'] b1, b2 = self.params['b1'], self.params['b2'] grads = {} batch_num = x.shape[0] # forward a1 = np.dot(x, W1) + b1 z1 = sigmoid(a1) a2 = np.dot(z1, W2) + b2 y = softmax(a2) # backward dy = (y - t) / batch_num grads['W2'] = np.dot(z1.T, dy) grads['b2'] = np.sum(dy, axis=0) da1 = np.dot(dy, W2.T) dz1 = sigmoid_grad(a1) * da1 grads['W1'] = np.dot(x.T, dz1) grads['b1'] = np.sum(dz1, axis=0) return grads
Python
복사

4.5.2 미니배치 학습 구현하기

4.5.1의 TwoLayerNet 클래스와 MNIST 데이터셋을 사용하여 학습 수행하기
→ ch04/train_neuralnet.py 소스코드 이해하기
import sys, os sys.path.append(os.pardir) # 부모 디렉터리의 파일을 가져올 수 있도록 설정 import numpy as np import matplotlib.pyplot as plt 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, flatten =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) #epoch이란 => 데이터를 다 소진할 때까지 학습하는 것을 의미함 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)) # 그래프 그리기 markers = {'train': 'o', 'test': 's'} x = np.arange(len(train_acc_list)) plt.plot(x, train_acc_list, label='train acc') plt.plot(x, test_acc_list, label='test acc', linestyle='--') plt.xlabel("epochs") plt.ylabel("accuracy") plt.ylim(0, 1.0) plt.legend(loc='lower right') plt.show()
Python
복사
<learning_rate를 1.0으로 수정 후 결과>
<learning_rate를 10.0으로 수정 후 결과>
cf) 위에서 gradient를 구할 때, 5장에서 배울 역전파를 구현한 gradient 함수를 사용했다.
그래서 역전파 개념 대신 numerical_gradient로 구현해보면, 결과를 확인할 수 없을 정도로 시간이 엄청나게 걸린다
이 내용은 수원대학교 한경훈 교수님께서 언급해주시고, 설명해주신 내용을 바탕으로
왜 numerical_gradient로 구현하면 연산이 오래 걸리는지 정리해 보았다.
우선 손실함수 변수 개수는
784 * 50 + 50 + 50*10 + 10 = 39,740개이다.
단지 network.gradient(x_batch, t_batch) ——> network.numerical_gradient(x_batch, t_batch)로만 수정했으니 역추적을 해보자.
MNIST 데이터는 1개의 이미지를 flatten 하면, 784개의 데이터를 갖게 된다.
우선 network가 two_layer_net.py에서 시작된 것이므로, two_layer_net.py로 가서 확인해 볼 필요성이 있다.
two_layer_net.py에서 numerical_gradient를 찾아보면, 이 함수에서 또 동일한 이름의 numerical_gradient가 존재한다.
이는 common.gradient에서의 numerical_gradient이므로 또 다시 common.gradient.py에 가서 찾아봐야 한다.
(왜 헷갈리게 동일한 함수의 이름을 여러 py에 걸쳐 사용하게 했는지는 의문이다.)
결국 이 곳에서 수치미분계수를 정의하고 있는데,
손실함수 변수마다(즉, 39740개마다) fxh1 - fxh2 / 2*h를 다 계산해야 한다.
그런데 이것이 겨우 학습을 한 번 하는데 이 정도이므로,
iters_num = 10000 이면 최종적으로 계산 횟수는
39,740 * 10,000 번의 계산을 해야 하므로, 시간이 엄청 오래 걸리게 된다.

4.6 정리

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

Reference

경사하강법(gradient descent), github blog (link)
수알못의 머신러닝 공부 : 경사하강법, tistory (link)
[미분의 연쇄법칙, 체인룰(Chain Rule)] 양파를 까는 일과 같다, naver blog (link)
머신러닝, 딥러닝 학습, 최적화(Optimization)에 대해서 (SGD, Momentum, RMSProp, Adam), tistory (link)
[Deep Learning] 경사하강법 (Gradient Descent) 이란, tistory (link)
밑바닥부터 시작하는 딥러닝 #3-4 경사법, tistory (link)
손실 함수 (Loss Function)와 경사 하강법 (Gradient Descent), github blog (link)
경사하강법(gradient descent), tistory (link)
데이터과학 유망주의 매일 글쓰기 — 65일차, medium (link)
최적화(Optimizer): (1) Momentum, tistory (link)
Local Minimum, Local Maximum, tistory (link)
[머신러닝] 선형 회귀와 경사 하강법, tistory (link)
학습률 (Learning rate), tistory (link)
경사하강법의 세 종류(BGD, SGD, MGD), tistory (link)
Local Minima 문제에도 불구하고 딥러닝이 잘 되는 이유는?, tistory (link)
경사하강법 종류, tistory (link)
수치 미분(Numericla differentiation)이란?, tistory (link)
수치미분(Numerical Differentiation) 3 - 중앙차분법, Centered Divided Difference, tistory (link)