#!/usr/bin/env python # coding: utf-8 # # 밑바닥부터 시작하는 딥러닝 # # # Deep Learning from Scratch # # ## Github # # https://github.com/WegraLee/deep-learning-from-scratch # # ## 목차 # # http://nbviewer.jupyter.org/github/SDRLurker/deep-learning/blob/master/%EB%AA%A9%EC%B0%A8.ipynb # # 4 신경망 학습 # 학습: 훈련 데이터로부터 가중치 매개변수의 최적값을 자동으로 획득하는 것 # # 손실 함수의 결과값을 가장 작게 만드는 가중치 매개변수를 찾는 것이 학습 목표 # # 함수의 기울기를 활용하는 경사법 소개 # ## 4.1 데이터에서 학습한다! # # 데이터를 보고 학습: 가중치 매개변수의 값을 데이터를 보고 자동으로 결정 # # 신경망 학습(데이터로부터 매개변수의 값을 정하는 방법) 설명 # # 손글씨 숫자를 학습하는 코드 구현 # ### 4.1.1 데이터 주도 학습 # 기계학습의 중심에는 데이터가 존재 # # 수집한 데이터로부터 패턴을 찾으려 시도 # In[1]: # 그림 4-1 손글씨 숫자 '5'의 예: 사람마다 자신만의 필체가 있다. # 출처 https://www.researchgate.net/figure/265798034_fig2_Figure-5-Images-obtained-from-the-hand-written-data-base-MNIST from IPython.display import Image, display display(Image(filename='5.png', embed=True)) # 숫자(5)를 인식하는 명확한 로직을 사람이 만들기 어려움 # # 기계학습의 2 가지 접근법 # # (1) 사람이 생각한 특징(SIFT, HOG 등) -> 기계학습(SVM, KNN등) # # * 이미지에서 특징(feature)을 추출. SIFG, HOG 등의 특징 사용. 벡터로 기술. # # * 기계학습: 데이터로부터 규칙을 찾아내는 역할 # # (2) 신경망(딥러닝): 이미지를 '있는 그대로' 학습 # # 종단간 기계학습(end-to-end machine learning) : 처음부터 끝까지, 데이터(입력)에서 목표한 결과(출력)을 얻음 # ### 4.1.2 훈련 데이터와 시험 데이터 # 기계학습 문제는 훈련 데이터(training data)와 시험 데이터(test data)로 나눠 학습과 실험을 수행하는 것이 일반적 # # 범용 능력을 제대로 평가하기 위해 훈련 데이터와 시험 데이터를 분리 # # 범용 능력: 아직 보지 못한 데이터(훈련 데이터에 포함되지 않는 데이터)로도 문제를 올바르게 풀어내는 능력. 기계학습의 최종목표. # # 오버피팅(overfitting): 한 데이터셋(예시: 훈련 데이터만)에만 지나치게 최적화된 상태. 다른 데이터셋을 엉망으로 평가할 수 있음 # ## 4.2 손실 함수 # 신경망 학습에서는 현재의 상태를 '하나의 지표'로 표현. 그 지표를 가장 좋게 만들어주는 가중치 매개변수의 값을 탐색. # # 손실함수(loss function): 신경망 성능의 '나쁨'을 나타내는 지표 # # * 평균 제곱 오차 # * 교차 엔트로피 오차 # ### 4.2.1 평균 제곱 오차 # 평균 제곱 오차(mean squared error, MSE) 수식 # # \begin{equation*} # E = \frac{1}{2} \sum_{k} (y_{k}-t_{k})^{2} # \end{equation*} # # * yk 신경망의 출력(신경망이 추정한 값) # * tk 정답 레이블 # * k 데이터의 차원 수 # In[2]: y = [0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0] t = [0, 0, 1, 0, 0, 0, 0, 0, 0, 0] # 배열들의 원소는 첫 번째 인덱스부터 순서대로 숫자 '0', '1', '2', ...일 때 값 # # y 소프트맥스 함수의 출력 # # t 정답을 의미하는 위치의 원소는 1로, 그 외에는 0으로 표기 # # 숫자 '2'에 해당하는 원소의 값이 1이므로 정답이 '2' # # 원-핫 인코딩: 한 원소만 1로 하고 그 외는 0으로 나타내는 표기법 # # 평균 제곱 오차 파이썬 구현 # In[3]: def mean_squared_error(y, t): return 0.5 * np.sum((y-t)**2) # In[4]: import numpy as np # 예1: '2'일 확률이 가장 높다고 추정함 (0.6) mean_squared_error(np.array(y), np.array(t)) # In[5]: # 예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] mean_squared_error(np.array(y), np.array(t)) # 첫 번째 예: 정답이 '2', 신경망의 출력도 '2' # # 두 번째 예: 정답이 '2', 신경망의 출력은 '7' # # 평균 제곱 오차를 기준으로 첫 번째 추정 결과가 오차가 더 작으니 정답에 더 가까울 것이라 판단 가능 # ### 4.2.2 교차 엔트로피 오차 # 교차 엔트로피 오차(cross entropy error, CEE) 수식 # # \begin{equation*} # E = - \sum_{k} t_{k} log y_{k} # \end{equation*} # # * log는 밑이 e인 자연로그 # * yk 신경망의 출력(신경망이 추정한 값) # * tk 정답 레이블 : 정답에 해당하는 인덱스의 원소만 1, 나머지는 0(원-핫 인코딩) # # 정답 레이블은 '2'가 정답. 신경망 출력이 0.6이라면 교차 엔트로피 오차는 -log0.6 = 0.51 # # 같은 조건에서 신경망 출력이 0.1이라면 -log0.1 = 2.3 # # 그림 4-3 자연로그 y = logx 그래프 # In[6]: get_ipython().run_line_magic('matplotlib', 'inline') import matplotlib.pylab as plt x = np.arange(0.001, 1.0, 0.001) y = np.log(x) plt.plot(x, y) plt.ylim(-5.0, 0.0) # y축의 범위 지정 plt.show() # x가 1일 때 y는 0이 되고 x가 0에 가까워질 수록 y의 값은 점점 작아짐 # # 교차 엔트로피 구현 # In[7]: def cross_entropy_error(y, t): delta = 1e-7 return -np.sum(t * np.log(y + delta)) # y와 t는 넘파이 배열. # # 아주 작은 delta 값을 더하는 이유 # # np.log 함수에 0을 입력하면 마이너스 무한대를 뜻하는 -inf가 되어 계산을 진행할 수 없음 # # y가 0이 되지 않도록 delta를 더하였음 # # cross_entropy_error 계산 예시 # In[8]: t = [0, 0, 1, 0, 0, 0, 0, 0, 0, 0] y = [0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0] cross_entropy_error(np.array(y), np.array(t)) # In[9]: y = [0.1, 0.05, 0.1, 0.0, 0.05, 0.1, 0.0, 0.6, 0.0, 0.0] cross_entropy_error(np.array(y), np.array(t)) # 첫 번째 예: 정답일 때의 출력 0.6인 경우 교차 엔트로피 오차는 약 0.51 # # 두 번째 예: 출력이 0.1이라면 교차 엔트로피 오차는 약 2.3 # # 첫 번째 추정이 정답일 가능성이 높다고 판단 # ### 4.2.3 미니배치 학습 # 훈련 데이터 모두에 대한 손실함수의 평균을 구함 # # 교차 엔트로피 오차(훈련 데이터 모두에 대한) 수식 # # \begin{equation*} # E = - \frac{1}{N} \sum_{n} \sum_{k} t_{nk} log y_{nk} # \end{equation*} # 데이터가 N개. tnk: n번째 데이터의 k차원 째 값을 의미. # # ynk는 신경망의 출력. tnk는 정답 레이블 # # N으로 나눔으로써 '평균 손실 함수'를 구함 # # 미니배치: 훈련 데이터로부터 일부만 골라 학습을 수행 # # 미니배치 학습: 60,000장의 훈련 데이터 중 100장을 무작위로 뽑아 그 100장을 사용하여 학습 # # MNIST 데이터셋을 읽어오는 코드 # In[10]: import sys, os sys.path.append(os.pardir) import numpy as np from dataset.mnist import load_mnist (x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, one_hot_label=True) print(x_train.shape) # (60000, 784) print(t_train.shape) # (60000, 10) # load_mnist 함수로 MNIST (훈련 데이터와 시험 데이터) 데이터셋을 읽음. # # one_hot_label=True로 지정하여, 원-핫 인코딩. 정답 위치의 원소만 1이고 나머지가 0인 배열을 얻음. # # 훈련 데이터에서 무작위로 10장만 빼내려면? np.random.choice() 함수 사용. # In[11]: train_size = x_train.shape[0] batch_size = 10 batch_mask = np.random.choice(train_size, batch_size) x_batch = x_train[batch_mask] t_batch = t_train[batch_mask] # np.random.choice(60000, 10)은 0에서 60000 미만의 수 중에서 무작위로 10개를 골라냄 # # 이 함수가 출력한 배열을 미니배치로 뽑아낼 데이터의 인덱스로 사용가능 # In[12]: np.random.choice(60000, 10) # 미니배치의 손실 함수도 일부 표본 데이터로 전체를 비슷하게 계측. # # 전체 훈련 데이터의 대표로서 무작위로 선택한 작은 덩어리(미니배치)를 사용하는 것. # ### 4.2.4 (배치용) 교차 엔트로피 오차 구현하기 # 배치 데이터를 처리할 수 있는 교차 엔트로피 오차 구하기 # # (데이터가 하나인 경우와 배치로 묶여 입력될 경우 모두 처리할 수 있음) # In[13]: def cross_entropy_error(y, t): if y.ndim == 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)) / batch_size # y는 신경망의 출력, t는 정답 레이블. # # y가 1차원이라면, 데이터 하나당 교차 엔트로피 오차를 구하는 경우는 reshape로 형상을 변경. # # 배치의 크기로 나눠 정규화하고 이미지 1장당 평균의 교차 엔트로피 오차를 계산 # # 정답 레이블이 원-핫 인코딩이 아닌 숫자 레이블('2', '7')로 주어졌을 때 교차 엔트로피 오차 # In[14]: def cross_entropy_error(y, t): if y.ndim == 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])) / 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] 설명 # # batch_size가 5면 np.arange(batch_size)는 [0, 1, 2, 3, 4] # # t에는 레이블이 [2, 7, 0, 9, 4]와 같이 저장됨 # # y[np.arange(batch_size),t]는 각 데이터의 정답 레이블에 해당하는 신경망의 출력을 추출 # # y[np.arange(batch_size),t] = [y[0,2], y[1,7], y[2,0], y[3,9], y[4,4]] 인 넘파이 배열 생성 # ### 4.2.5 왜 손실 함수를 설정하는가? # 신경망 학습에서는 최적의 매개변수(가중치와 편향)를 탐색할 때 손실 함수의 값을 가능한 작게 하는 매개변수 값을 찾음 # # 매개변수의 미분을 계산하고, 그 미분 값을 단서로 매개변수 값을 서서히 갱신하는 과정을 반복 # # 신경망을 학습할 때 정확도를 지표로 삼아서는 안 된다. 정확도를 지표로 하면 매개변수의 미분이 대부분의 장소에서 0이 되기 때문이다. # # 매개변수를 약간만 조정해서는 정확도가 개선되지 않고 일정하게 유지. 개선되도 불연속적인 값으로 바뀜. # # 손실함수를 지표로 삼으면 매개변수 값이 조금 변하면 그에 반응하여 연속적으로 변화. # # 계산함수의 미분은 대부분의 장소(0 이외의 곳)에서 0. # In[15]: # 그림 4-4 계단함수와 시그모이드 함수: 계단 함수는 대부분의 장소에서 기울기가 0이지만, 시그모이드 함수의 기울기(접선)는 0이 아니다. import numpy as np import matplotlib.pylab as plt def step_function(x): return np.array(x > 0, dtype=np.int) x = np.arange(-6.0, 6.0, 0.1) y1 = step_function(x) y2 = np.array([1 for _ in range(x.size)]) y3 = np.array([0 for _ in range(x.size)]) plt.plot(x, y1) plt.plot(x, y2, color='green') plt.plot(x, y3, color='green') plt.scatter([4,-4],[1,0],color='red') plt.ylim(-0.1, 1.1) # y축의 범위 지정 plt.show() # 시그모이드 함수의 기울기(접선)은 0이 아니다. # In[16]: def sigmoid(x): return 1 / (1 + np.exp(-x)) def sigmoid_diff(x): return sigmoid(x) * (1 - sigmoid(x)) def 시그모이드_접선(x): # 접선 ax+b에서 a,b 값을 리턴 return sigmoid_diff(x), sigmoid(x) - sigmoid_diff(x) * x x = np.arange(-6.0, 6.0, 0.1) y1 = sigmoid(x) a2, b2 = 시그모이드_접선(4) y2 = a2 * x + b2 a3, b3 = 시그모이드_접선(-4) y3 = a3 * x + b3 plt.plot(x, y1) plt.plot(x, y2, color='green') plt.plot(x, y3, color='green') plt.scatter([4,-4],[a2*4+b2,a3*-4+b3],color='red') plt.ylim(-0.1, 1.1) # y축의 범위 지정 plt.show() # 계단 함수는 한 순간만 변화를 일으킴. # # 시그모이드 함수는 출력이 연속적으로 변하고 곡선의 기울기(미분)도 연속적으로 변함. # # 기울기가 0이 되지 않는 덕분에 신경망이 올바르게 학습됨. # ## 4.3 수치미분 # 경사법에서는 기울기(경사) 값을 기준으로 나아갈 방향을 결정 # # '미분' 복습 # ### 4.3.1 미분 # '특정 순간'의 변화량 # # 미분은 한순간의 변화량을 표시한 것. 수식 # # \begin{equation*} # \frac{df(x)}{dx} = lim_{h->0} \frac{f(x+h)-f(x)}{h} # \end{equation*} # # 좌변은 f(x)의 x에 대한 미분(x에 대한 f(x)의 변화량)을 나타내는 기호 # # 작은 변화, 시간을 뜻하는 h를 한없이 0에 가깝게 한다는 의미 # # \begin{equation*} # lim_{h->0} # \end{equation*} # # 함수의 미분을 구하는 계산 파이썬 구현 # In[17]: # 나쁜 구현 예 def numerical_diff(): h = 10e-50 return (f(x + h) - f(x)) / h # 함수 이름은 수치 미분(numerical differentiation)에서 따온 numerical_diff(f,x)로 함 # # 2개의 인수: '함수 f'와 함수 f에 넘길 '인수 x' # # 이 함수의 문제점 # # * 반올림 오차(rounding error): 작은 값(가령 소수점 8자리 이하)이 생략되어 최종 계산 결과에 오차가 발생 # In[18]: np.float32(1e-50) # 미세한 값 h를 10 \*\* -4를 이용. # # 10 \*\* -4 정도의 값을 사용하면 좋은 결과를 얻는다고 알려져 있음 # * f의 차분(임의 두 점에서 함수 값들의 차이) 계산에 오차 # # x+h와 x 사이의 함수 f의 차분을 계산 # # 진정한 미분은 x 위치의 함수의 기울기(접선) # # 이번 구현에서 구한 미분은 (x+h)와 x 사이의 기울기에 해당 # # h를 무한히 0으로 좁히는 것이 불가능해 생기는 한계 # In[19]: # 그림 4-5 진정한 미분(진정한 접선)과 수치 미분(근사로 구한 접선)의 값은 다르다. # -*- coding: utf-8 -*- from matplotlib import rc import matplotlib.font_manager as fm import matplotlib #matplotlib.rc('figure', figsize=(16, 4)) # 우분투에 나눔글꼴 설치한 경우 (c9.io) #fp = fm.FontProperties(fname="/usr/share/fonts/truetype/nanum/NanumGothic.ttf") # Windows 경우 #fp = fm.FontProperties(fname="c:/Windows/Fonts/NanumGothic.ttf") fp = fm.FontProperties(fname="c:/Windows/Fonts/NGULIM.ttf") # MacOS 경우 #fp = fm.FontProperties(fname="/Users/plusjune/Library/Fonts/NanumGothic.ttf") rc('font', family=fp.get_name()) x = np.arange(-6.0, 6.0, 0.1) y1 = sigmoid(x) a2, b2 = 시그모이드_접선(0) y2 = a2 * x + b2 a3 = (sigmoid(2.5) - sigmoid(0)) / 2.5 y3 = a3 * x + b2 plt.plot(x, y1, label='y=f(x)') plt.plot(x, y2, color='black', label='진정한 접선') plt.plot(x, y3, color='green', label='근사로 구한 접선') xv = np.arange(-0.1, 0.5, 0.01) plt.text(-0.2,0,"x") plt.plot(np.array([0 for _ in range(xv.size)]), xv, 'k--') xhv = np.arange(-0.1, sigmoid(2.5), 0.01) plt.text(2,0,"x+h") plt.plot(np.array([2.5 for _ in range(xhv.size)]), xhv, 'k--') plt.scatter([0],[b2],color='red') plt.ylim(-0.1,1.1) plt.xlim(-4,4) plt.legend(loc='upper center') plt.show() # 위 그래프처럼 수치 미분에는 오차가 포함 # # 중심차분, 중앙차분: 이 오차를 줄이기 위해 (x+h)와 (x-h)일 때의 함수 f의 차분을 계산하는 방법을 사용 # # 전방차분: (x+h)와 x의 차분 # # 위의 2가지 개선점을 적용한 수치 미분 구현 # In[20]: def numerical_diff(f, x): h = 1e-4 # 0.0001 return (f(x+h) - f(x-h)) / (2*h) # 수치 미분: 아주 작은 차분으로 미분을 구하는 것 # # 해석적(analytic) 해: 수식을 전개해 미분을 구하는 것 # # 해석적 미분은 오차를 포함하지 않는 '진정한 미분' 값을 구함 # ### 4.3.2 수치 미분의 예 # 간단한 함수 미분 예시 # # 2차 함수 # # \begin{equation*} # y = 0.01x^{2} + 0.1x # \end{equation*} # In[21]: def function_1(x): return 0.01*x**2 + 0.1*x # 이 함수 그래프로 그리기 # In[22]: # 그림 4-6 식 f(x)=0.01x**2 + 0.1x 그래프 import numpy as np import matplotlib.pylab as plt x = np.arange(0.0, 20.0, 0.1) # 0에서 20까지 0.1 간격의 배열 x를 만든다. y = function_1(x) plt.xlabel("x") plt.ylabel("f(x)") plt.plot(x,y) plt.show() # x = 5, 10일때 미분 계산 # In[23]: numerical_diff(function_1, 5) # In[24]: numerical_diff(function_1, 10) # 위에서 계산한 미분 값이 x에 대한 f(x)의 변화량 # # \begin{equation*} # y = 0.01x^{2} + 0.1x # \end{equation*} # # 의 해석적 해법은 # # \begin{equation*} # \frac{df(x)}{dx} = 0.02x + 0.1 # \end{equation*} # # x가 5, 10일 때 '진정한 미분'은 0.2, 0.3이 됨. # # 앞의 수치 미분과 결과를 비교하면 오차가 매우 작음. # # x=5, x=10에서의 접선: 직선의 기울기는 수치 미분에서 구한 값을 사용하였다. # In[25]: # 그림 4-7 x=5, x=10에서의 접선: 직선의 기울기는 수치 미분에서 구한 값을 사용하였다. # https://github.com/WegraLee/deep-learning-from-scratch/blob/master/ch04/gradient_1d.py 소스 참고 def tangent_line(f, x): d = numerical_diff(f, x) y = f(x) - d*x return lambda t: d*t + y def draw(ax, x, y, line, tox, toy): ax.set_xlabel("x") ax.set_ylabel("f(x)") ax.plot(x, y) ax.plot(x, line) h = np.arange(0, tox, 0.01) v = np.arange(-1, toy, 0.01) ax.plot(np.array([tox for _ in range(v.size)]), v, 'k--') ax.plot(h, np.array([toy for _ in range(h.size)]), 'k--') ax.scatter(tox,toy,color='red') ax.set_xlim([0,20]) x = np.arange(0.0, 20.0, 0.1) y = function_1(x) tf = tangent_line(function_1, 5) y2 = tf(x) tf = tangent_line(function_1, 10) y3 = tf(x) f, (ax1, ax2) = plt.subplots(2, 1) draw(ax1, x, y, y2, 5, function_1(5)) draw(ax2, x, y, y3, 10, function_1(10)) # ### 4.3.3 편미분 # 변수가 다음처럼 2개 이상인 식 # # \begin{equation*} # f(x_{0}, x_{1}) = x_{0}^{2} + x_{1}^{2} # \end{equation*} # # 파이썬으로 다음과 같이 구현가능 # In[26]: def function_2(x): return x[0]**2 + x[1]**2 # 또는 return np.sum(x**2) # 위 함수를 그래프로 그리기 # In[27]: # 그림 4-8 f(x0, x1) = x0**2 + x1**2 그래프 # 3차원 참고주소: https://www.datascienceschool.net/view-notebook/6e71dbff254542d9b0a054a7c98b34ec/ from mpl_toolkits.mplot3d import Axes3D X = np.arange(-3, 3, 0.25) Y = np.arange(-3, 3, 0.25) XX, YY = np.meshgrid(X, Y) ZZ = XX**2 + YY**2 fig = plt.figure() ax = Axes3D(fig) ax.plot_surface(XX, YY, ZZ, rstride=1, cstride=1, cmap='hot'); # 편미분: 변수가 여럿인 함수에 대한 미분. 어느 변수에 대한 미분이냐를 구별해야 함 # # 수식으로는 다음처럼 씀 # # \begin{equation*} # \frac{\partial f}{\partial x_{0}}, \frac{\partial f}{\partial x_{1}} # \end{equation*} # # 문제 1: x0=3, x1=4일 때 x0에 대한 편미분을 구하라. # In[28]: def function_tmp1(x0): return x0*x0 + 4.0**2.0 numerical_diff(function_tmp1, 3.0) # 문제 2: x0=3, x1=4일 때 x1에 대한 편미분을 구하라. # In[29]: def function_tmp2(x1): return 3.0**2.0 + x1*x1 numerical_diff(function_tmp2, 4.0) # 문제 1에서는 x1=4로 고정된 새로운 함수를 정의, 변수가 x0 하나뿐인 함수에 대해 수치 미분함수 적용. # # 편미분은 변수가 하나인 미분과 마찬가지로 특정 장소의 기울기를 구함 # # 목표 변수 하나에 초점을 맞추고 다른 변수는 값을 고정 # ## 4.4 기울기 # 기울기(gradient): 모든 변수의 편미분을 벡터로 정리한 것. # # \begin{equation*} # (\frac{\partial f}{\partial x_{0}}, \frac{\partial f}{\partial x_{1}}) # \end{equation*} # # 기울기 구현예시 # In[30]: def _numerical_gradient_no_batch(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] = float(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) 동작 방식은 변수가 하나일 때의 수치 미문과 거의 같음 # # np.zeros_like(x): x와 형상이 같고 그 원소가 모두 0인 배열을 만듬 # # 이 함수를 이용하여 세 점 (3,4), (0.2), (3,0)에서 기울기 구하기 # In[31]: _numerical_gradient_no_batch(function_2, np.array([3.0, 4.0])) # In[32]: _numerical_gradient_no_batch(function_2, np.array([0.0, 2.0])) # In[33]: _numerical_gradient_no_batch(function_2, np.array([3.0, 0.0])) # 기울기의 결과에 마이너스를 붙인 벡터 그리기 # In[34]: # 그림 4-9 f(x0, x1) = x0**2 + x1**2 의 기울기 # https://github.com/WegraLee/deep-learning-from-scratch/blob/master/ch04/gradient_2d.py 소스 참고 from mpl_toolkits.mplot3d import Axes3D def numerical_gradient(f, X): if X.ndim == 1: return _numerical_gradient_no_batch(f, X) else: grad = np.zeros_like(X) for idx, x in enumerate(X): grad[idx] = _numerical_gradient_no_batch(f, x) return grad def function_2(x): if x.ndim == 1: return np.sum(x**2) else: return np.sum(x**2, axis=1) def tangent_line(f, x): d = numerical_gradient(f, x) print(d) y = f(x) - d*x return lambda t: d*t + y x0 = np.arange(-2, 2.5, 0.25) x1 = np.arange(-2, 2.5, 0.25) X, Y = np.meshgrid(x0, x1) X = X.flatten() Y = Y.flatten() grad = numerical_gradient(function_2, np.array([X, Y]) ) plt.figure() plt.quiver(X, Y, -grad[0], -grad[1], angles="xy",color="#666666")#,headwidth=10,scale=40,color="#444444") plt.xlim([-2, 2]) plt.ylim([-2, 2]) plt.xlabel('x0') plt.ylabel('x1') plt.grid() plt.legend() plt.draw() plt.show() # 기울기는 방향을 가진 벡터(화살표)로 그려짐 # # '가장 낮은 곳'에서 멀어질 수록 화살표가 커짐을 알 수 있음. # # 기울기가 가리키는 쪽은 각 장소에서 함수의 출력 값을 가장 줄이는 방향 # ### 4.4.1 경사법(경사 하강법) # 최적의 매개변수(가중치와 편향)을 학습 시에 찾음. 최적은 손실함수가 최소값이 될 때 매개변수 값. # # 경사법: 기울기를 잘 이용해 함수의 최소값을 찾으려는 방법 # # 함수가 극소값, 최소값, 안장점(saddle point)이 되는 장소에서는 기울기가 0 # # 극소값: 한정된 범위에서의 최소값인 점 # # 안장점: 어느 방향에서 보면 극대값 다른 방향에서 보면 극소값이 되는 점 # # 복잡하고 찌그러진 모양의 함수라면 (대부분) 평평한 곳으로 파고들면서 고원(plateau, 플래토)이라 하는, # # 학습이 진행되지 않는 정체기에 빠질 수 있음. # # 경사법(gradient method) # # 현 위치에서 기울어진 방향으로 일정 거리만큼 이동. 다음 위치에서도 또 기울어진 방향으로 나아가는 일을 반복. # # 경사 하강법(gradient descent method): 최소값을 찾음. 신경망(딥러닝) 분야에서 주로 사용. # # 경사 상승법(gradient ascent method): 최대값을 찾음 # # 경사법 수식 # # \begin{equation*} # x_{0} = x_{0} - \eta \frac{\partial f}{\partial x_{0}} # \end{equation*} # \begin{equation*} # x_{1} = x_{1} - \eta \frac{\partial f}{\partial x_{1}} # \end{equation*} # # 학습률(learning rate): 한 번에 얼만큼 학습해야 할 지, 매개변수 값을 얼마나 갱신하느냐를 정함 # # 위의 식은 1회에 해당하는 갱신이고, 이 단계를 반복 # # 변수의 값을 여러번 갱신하면서 서서히 함수의 값을 줄임 # # 학습률 값을 변경하면서 올바르게 학습하는 지 확인하면서 진행. 너무 크거나 작으면 '좋은 장소'를 찾을 수 없음. # # 경사 하강법 구현코드 # In[35]: 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은 learning rate를 의미하는 학습률, step_num은 경사법 반복 회수 # # numerical_gradient(f, x)로 함수의 기울기를 구함. 그 기울기에 학습률을 곱한 값으로 갱신하는 처리를 step_num번 반복 # # 문제: 경사법으로 f(x0,x1) = x0 \*\* 2 + x1 \*\* 2의 최소값을 구하라. # In[36]: init_x = np.array([-3.0, 4.0]) gradient_descent(function_2, init_x=init_x, lr=0.1, step_num=100) # 초기값을 (-3.0, 4.0)으로 설정한 후 경사법을 사용해 최소값을 구함 # # 경사법을 이용한 갱신 과정 그래프 # In[37]: # 그림 4-10 f(x0, x1) = x0**2 + x1**2 의 갱신 과정 : 점선은 함수의 등고선을 나타낸다. # https://github.com/WegraLee/deep-learning-from-scratch/blob/master/ch04/gradient_method.py 소스 참고 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() # 학습률이 너무 클 때와 작을 때 문제 # In[38]: 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 # In[39]: # 학습률이 너무 큰 예 : lr=10 init_x = np.array([-3.0, 4.0]) gradient_descent(function_2, init_x=init_x, lr=10.0, step_num=100) # In[40]: # 학습률이 너무 작은 예 : lr=1e-10 init_x = np.array([-3.0, 4.0]) gradient_descent(function_2, init_x=init_x, lr=1e-10, step_num=100) # 학습률이 너무 크면 큰 값으로 발산함 # # 학습률이 너무 작으면 거의 갱신되지 않은 채 끝남 # # 하이퍼파라미터(hyper parameter, 초매개변수) # # 사람이 직접 설정해야 하는 매개변수. 시험을 통해 가장 잘 학습하는 값을 찾는 과정이 필요. # ### 4.4.2 신경망에서의 기울기 # 신경망 기울기: 가중치 매개변수에 관한 손실 함수의 기울기 # # 형상 2X3, 가중치 W, 손실함수 L인 신경망 수식 # # \begin{equation*} # W = \begin{vmatrix} # w_{11} w_{21} w_{31}\\ # w_{12} w_{22} w_{32}\ # \end{vmatrix} # \end{equation*} # # \begin{equation*} # \frac{\partial{L}}{\partial{W}} = \begin{vmatrix} # \frac{\partial{L}}{\partial{W_{11}}} \frac{\partial{L}}{\partial{W_{21}}} \frac{\partial{L}}{\partial{W_{31}}}\\ # \frac{\partial{L}}{\partial{W_{12}}} \frac{\partial{L}}{\partial{W_{22}}} \frac{\partial{L}}{\partial{W_{32}}}\ # \end{vmatrix} # \end{equation*} # # 아래 행렬의 각 원소는 각각 원소에 대한 편미분 # # 간단한 신경망에 대한 기울기를 구하는 코드 # In[41]: # https://github.com/WegraLee/deep-learning-from-scratch/blob/master/common/gradient.py 소스 참고 import numpy as np def softmax(x): if x.ndim == 2: x = x.T x = x - np.max(x, axis=0) y = np.exp(x) / np.sum(np.exp(x), axis=0) return y.T x = x - np.max(x) # 오버플로 대책 return np.exp(x) / np.sum(np.exp(x)) 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 def numerical_gradient(f, x): h = 1e-4 # 0.0001 grad = np.zeros_like(x) it = np.nditer(x, flags=['multi_index'], op_flags=['readwrite']) while not it.finished: idx = it.multi_index tmp_val = x[idx] x[idx] = float(tmp_val) + h fxh1 = f(x) # f(x+h) x[idx] = tmp_val - h fxh2 = f(x) # f(x-h) grad[idx] = (fxh1 - fxh2) / (2*h) x[idx] = tmp_val # 값 복원 it.iternext() return grad 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인 가중치 매개변수 하나를 인스턴스 변수로 가짐 # # predict(x) 메소드, loss(x,t) 메소드 # # 인수 x는 입력 데이터, t는 정답 레이블 # # simpleNet 몇 가지 시험 # In[42]: net = simpleNet() print(net.W) # 가중치 매개변수 # In[43]: x = np.array([0.6, 0.9]) p = net.predict(x) print(p) # In[44]: np.argmax(p) # 최대값의 인덱스 # In[45]: t = np.array([1, 0, 0]) # 정답 레이블 net.loss(x,t) # numerical_gradient 함수를 사용하여 기울기 구하기 # In[46]: def f(W): return net.loss(x, t) dW = numerical_gradient(f, net.W) print(dW) # w11은 대략 -0.11, w11을 h만큼 늘리면 손실함수는 -0.11h만큼 감소. 손실함수를 줄이려면 '양의 방향' 으로 갱신해야 함. # # w23은 대략 0.06, w23을 h만큼 늘리면 손실함수는 0.06h만큼 증가. 손실함수를 줄이려면 '음의 방향' 으로 갱신해야 함. # 파이썬에서는 간단한 함수는 lambda 기법을 쓰면 더 편리함 # In[47]: f = lambda w: net.loss(x, t) dW = numerical_gradient(f, net.W) print(dW) # ## 4.5 학습 알고리즘 구현하기 # 신경망 학습의 절차 # # 학습: 가중치와 편향을 훈련 데이터에 적응하도록 조정하는 과정 # # 1단계 - 미니배치 : 훈련 데이터 중 일부를 무작위로 가져옴. 손실 함수 값을 줄이는 것이 목표. # # 2단계 - 기울기 산출 : 각 가중치 매개변수의 기울기를 구함. 기울기는 손실 함수의 값을 가장 작게 하는 방향을 제시 # # 3단계 - 매개변수 갱신 : 가중치 매개변수를 기울기 방향으로 아주 조금 갱신 # # 4단계 - 반복 : 1~3단계를 반복 # # 확률적 경사 하강법(stochastic gradient descent, SGD) : 미니배치로 무작위로 선택하여 골라낸 데이터로 경사 하강법 실시 # ### 4.5.1 2층 신경망 클래스 구현하기 # 2층 신경망을 하나의 클래스로 구현. 클래스명은 TwoLayerNet # In[48]: # https://github.com/WegraLee/deep-learning-from-scratch/blob/master/ch04/two_layer_net.py 소스 참고 import numpy as np def sigmoid_grad(x): return (1.0 - sigmoid(x)) * sigmoid(x) 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 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 # TwoLayerNet 클래스가 사용하는 변수 # # params : 신경망의 매개변수를 보관하는 딕셔너리 # # params['W1'] : 1번째 층의 가중치, params['b1'] : 1번째 층의 편향 # # params['W2'] : 2번째 층의 가중치, params['b2'] : 2번째 층의 편향 # # grads : 기울기를 보관하는 딕셔너리 # # grads['W1'] : 1번째 층의 가중치, grads['b1'] : 1번째 층의 편향 # # grads['W2'] : 2번째 층의 가중치, grads['b2'] : 2번째 층의 편향 # # TwoLayerNet 클래스의 메서드 # # \__init\__(self, input_size, hidden_size, output_size) : 초기화를 수행 # # 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번째 층의 매개변수 예시 # In[49]: net = TwoLayerNet(input_size=784, hidden_size=100, output_size=10) print(net.params['W1'].shape) # (784, 100) print(net.params['b1'].shape) # (100,) print(net.params['W2'].shape) # (100, 10) print(net.params['b2'].shape) # (10,) # params 변수에는 신경망에 필요한 매개변수가 모두 저장됨 # # 예측 처리는 다음과 같이 실행 # In[50]: x = np.random.rand(100, 784) # 더미 입력 데이터(100장 분량) y = net.predict(x) # numerical_gradient() 메서드를 사용해 기울기를 계산하면 grads 변수에 기울기 정보가 저장됨 # In[51]: x = np.random.rand(100, 784) # 더미 입력 데이터(100장 분량) t = np.random.rand(100, 10) # 더미 정답 레이블(100장 분량) grads = net.numerical_gradient(x, t) # 기울기 계산 print(grads['W1'].shape) # (784, 100) print(grads['b1'].shape) # (100,) print(grads['W2'].shape) # (100, 10) print(grads['b2'].shape) # (10,) # 초기화 메서드: 가중치 매개변수도 초기화 # # 초기값이 신경망 학습의 성공을 좌우 # # 가중치를 정규분포를 따르는 난수, 편향을 0으로 초기화 # # numerical_gradient(self, x, t) 메서드는 각 매개변수의 기울기를 계산함 # # 수치 미분 방식으로 각 매개변수의 손실 함수에 대한 기울기를 계산 # # gradient(self, x, t) 메서드는 오차역전차법을 사용하여 기울기를 효율적이고 빠르게 계산 # ### 4.5.2 미니배치 학습 구현하기 # 미니배치에 대해 경사법으로 매개변수를 갱신. TwoLayerNet 클래스와 MNIST 데이터셋을 사용하여 학습을 수행. # In[52]: # 데이터 읽기 (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)) # 미니배치 크기를 100으로 설정. 확률적 경사 하강법을 수행해 매개변수를 갱신. # # 경사법에 의한 갱신 횟수는 10,000번으로 설정. 갱신할 때마다 손실 함수를 계산하고 그 값을 배열에 추가 # In[53]: # 그림 4-11 손실 함수의 추이: 위쪽은 10,000회 반복까지의 추이, 아래쪽은 1,000회 반복까지의 추이 f, (ax1, ax2) = plt.subplots(2, 1) x = np.array(range(iters_num)) ax1.plot(x, train_loss_list, label='loss') ax1.set_xlabel("iteration") ax1.set_ylabel("loss") ax1.set_ylim(0, 3.0) ax2.plot(x[:1000], train_loss_list[:1000], label='loss') ax2.set_xlabel("iteration") ax2.set_ylabel("loss") ax2.set_ylim(0, 3.0) # 학습 회수가 늘어나면서 손실 함수의 값이 줄어듬 # # 학습이 잘 되고 있다는 뜻 # # 데이터를 반복해서 학습함으로써 최적 가중치 매개변수로 서서히 다가서고 있다! # ### 4.5.3 시험 데이터로 평가하기 # 훈련 데이터 외의 데이터를 올바르게 인식하는지 확인 필요. '오버피팅'을 일으키지 않는지 확인. # # 오버피팅: 훈련 데이터에 포함된 이미지만 제대로 구분. 그렇지 않은 이미지는 식별할 수 없다는 의미 # # 범용 능력을 평가하기 위해 학습 도중 정기적으로 훈련 데이터와 시험 데이터를 대상으로 정확도를 기록. # # 1에폭별로 훈련 데이터와 시험 데이터에 대한 정확도를 기록. # # 에폭(epoch): 1에폭은 학습에서 훈련 데이터를 모두 소진했을 때의 회수에 해당. # # 훈련 데이터 10,000개를 100개의 미니배치로 학습할 경우, 100회가 1에폭이 됨. # # 앞의 코드로 얻은 결과를 그래프로 그리기 # In[54]: # 그림 4-12 훈련 데이터와 시험 데이터에 대한 정확도 추이 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() # 훈련 데이터에 대한 정확도는 실선. 시험 데이터에 대한 정확도는 점선 # # 두 정확도에는 차이가 없음. 오버피팅이 발생하지 않음을 확인 # # 오버피팅이 발생하면 훈련 데이터와 시험 데이터를 보면서 잘못된 판단을 함 # # 이 순간을 포착해 학습을 중단하면 오버피팅을 예방 가능. => 조기 종료(early stopping) # # '6.4 바른 학습을 위해'에서 살펴볼 '가중치 감소', '드롭아웃'과 함께 대표적은 오버피팅 예방법 # ### 4.6 정리 # 신경망 학습의 목표: 손실 함수 값이 가장 작아지는 가중치 매개변수 값을 찾아내는 것 # # 경사법: 가능한 작은 손실 함수의 값을 찾는 수법. 함수의 기울기를 이용하는 방법 # # 이번 장에서 배운 것 # # 기계학습에서 사용하는 데이터 셋은 훈련 데이터와 시험 데이터로 나눠 사용 # # 학습한 데이터의 범용 능력을 시험 데이터로 평가 # # 신경망 학습은 손실 함수를 지표로, 손실 함수의 값이 작아지는 방향으로 가중치 매개변수를 갱신 # # 이를 갱신할 때 가중치 매개변수의 기울기를 이용. 기울어진 방향으로 가중치의 값을 갱신하는 작업 반복 # # 수치 미분 : 아주 작은 값을 주었을 때의 차분으로 미분을 구하는 것 # # 수치 미분으로 가중치 매개변수의 기울기를 구할 수 있음 # # 수히 미분을 이용한 계산은 시간이 걸림. 그 구현이 간단. # # 오차역전파법은 기울기를 고속으로 구할 수 있음.