안녕, 세상!

4. 신경망 학습 본문

It공부/Deep learning

4. 신경망 학습

dev_Lumin 2020. 6. 17. 21:35

(1) 신경망의 특징

신경망의 특징은 데이터를 보고 학습할 수 있다는 점입니다.

가중치 매개변수의 값을 데이터를 보고 자동으로 결정할 수 있습니다.

사람의 개입을 최소화하고 수집한 데이터로부터 패턴을 찾습니다.

신경망은 데이터를 '있는 그대로' 학습합니다. 

따라서 기계를 학습시킬려면 데이터가 반드시 필요합니다.

 

(2) 훈련데이터와 시험데이터

기계학습 문제는 데이터를 훈련데이터와 시험 데이터로 나눠 학습과 실험을 수행합니다.

훈련데이터와 시험 데이터를 나누는 이유는 범용적을 할 수 있는 모델을 얻기 위해서 입니다.

범용 능력은 아직 보지 못한 데이터로도 문제를 올바르게 풀어내는 능력입니다.

만들고자 하는 신경망은 특정 데이터만을 판단하는 것이 아니라 임의의 데이터를 판단하고 분류하는 것입니다.

그래서 데이터셋을 훈련 데이터만으로 학습과 평가를 수행하면 올바른 평가가 될 수 없습니다.

( 오버피팅(overfitting) - 한 쪽의 데이터셋에만 지나치게 최적화된 상태 )

 

(3) 손실 함수

신경망은 '하나의 지표'를 기준으로 최적의 매개변수(가중치) 값을 탐색하는데 여기서 그 지표가 손실함수입니다.

쉽게 표현하자면 신경망에서 손실함수는 가중치 값의 오차를 표현해줍니다.

손실함수를 기준으로 가중치를 조절하는데 일반적으로는 평균 제곱 오차교차 엔트로피 오차를 사용합니다.

 

① 평균 제곱 오차

평균 제곱 오차의 수식은 다음과 같습니다.

평균 제곱 오차 수식

y는 신경망의 출력(신경망이 추정한 값), t는 정답 레이블, k는 데이터의 차원수를 나타냅니다.

평균 제곱 오차를 코드로 나타내면 다음과 같습니다.

 

평균 제곱 오차를 적용한 예시를 손글씨 분류로 아래 코드를 통해 보여드리겠습니다.

y 와 t 배열의 원소는 첫번째 인덱스 부터 순서대로 숫자 '0', '1', '2' ... 일 때의 값을 말합니다.

y는 출력값으로 소프트맥스 함수를 적용시켜서 확률로 표현된 것 입니다.

첫번째 y배열의 인덱스 [4] 부분이 0.6으로 숫자 '4'일 확률이 0.6퍼센트라는 뜻으로 제일 높습니다.

두번째 y배열의 인덱스 [0] 부분이 0.6이므로 숫자 '0'일 확률이 제일 높습니다.

t는 정답 레이블로 원-핫 인코딩으로 표현되어서 인덱스 [4] 부분이 1 이므로 정답은 숫자 '4' 라는 의미입니다.

 

첫번째 상황은 신경망이 예측한 숫자의 확률이 제일 높은 숫자가 정답과 같은 경우입니다.

첫번째 상황에서 평균 제곱 오차를 적용시켜 나온 값은 0.0975... 입니다.

 

두번째 상황은 신경망이 예측한 숫자의 확률이 제일 높은 숫자가 정답이 아니고 정답인 숫자인 '4'를 신경망이 예측한 확률이 0.1인 경우입니다.

두번째 상황에서 평균 제곱 오차를 적용시켜 나온 값은 0.5975 입니다

 

이를 통해 평균 제곱 오차는 오차를 표현하며 값이 적을수록 정확하다는 것을 알 수 있습니다.

 

② 교차 엔트로피 오차

교차 엔트로피 오차 수식은 다음과 같습니다.

교차 엔트로피 오차 수식

y는 신경망의 출력, t는 정답 레이블이고 원-핫 인코딩으로 원소들이 이뤄져 있습니다.

코드로 표현하면 다음과 같습니다.

여기서 delta(아주 작은 수)가 추가 되었는데 추가된 이유는 np.log() 함수에 0을 입력하면 마니어스 무한대를 뜻하는 

-inf 가 되어 더 이상 계산을 없기 때문입니다.

따라서 아주 작은 수를 넣어 0이 되지 않도록 더해준 것입니다.

 

교차 엔트로피 오차 예시도 손글씨 분류를 통해 아래 코드로 표현하겠습니다.

평균 제곱 오차 때와 수치를 같게 해서 표현했습니다.

마찬가지로 오차의 수치가 적을수록 정답일 가능성이 높다는 것을 알 수 있습니다.

 

 

미니배치 학습

배치(batch)란 앞서 설명했듯이 데이터의 묶음입니다.

교차 엔트로피 오차를 배치로 학습시키면 수식이 다음과 같아집니다.

여기서 N(n)은 묶여진 데이터의 수입니다.

마지막에 N으로 나눔으로써 평균 손실 함수를 구할 수 있습니다.

 

이번 예제도 손글씨 분류로 표현할 것입니다.

다음 예제에는 배치의 수는 10개로 할 것 입니다.

미니 배치로 학습 시키기 위해 먼저 학습데이터와 정답레이블을 불러온다음에 훈련데이터에서 무작위로 10장을 빼는 코드는 다음과 같습니다.

여기서 정답레이블인 t_train.shape 의 형상이 (60000,10) 으로 나오는 이유는 load_mnist함수의 매개변수인 one_hot_label을 True로 설정해 놓아서 원_핫 인코딩이 되어서 정답레이블의 형상이 숫자 '0'~'9' 까지를 각각의 인덱스에 대조 했을 경우 해당 숫자를 1로 표현하고 아닌 숫자는 0으로 표현되므로 형상의 열의 개수가 10이 되는 것입니다.

 

.shape[0] - 다차원 배열의 행의 갯수를 반환 함

.shape[1] - 다차원 배열의 열의 갯수를 반환 함

 

np.random.choice() 함수 - 지정한 범위의 수중에서 무작위로 원하는 개수만 꺼낼수 있는 함수

                                    형식: np.random.choice(범위수, 뽑을 개수)

 

배치용 교차 엔트로피 오차를 구현한 코드는 아래와 같습니다.

배치 교차 엔트로피 오차 y가 원-핫 인코딩일 경우
배치 교차 엔트로피 오차 원-핫코딩일 경우 아닐 경우 모두 고려한 코드

.ndim  -  넘파이 배열의 차원을 뜻함

 

.reshape()  -  배열의 형상을 바꾸는 메소드

                  형식: 넘파이배열변수.reshape(행,열)

                  주의해야 될 부분이 1차원 배열을 (1, 1차원배열원소수 ) 로 하면 달라짐

                  위의 코드에서 1차원 배열인데 reshape를 하는 이유를 다음 코드를 통해 알 수 있음

shape[0]은 그냥 일반 1차원 배열일 경우엔 위와 같이 카운팅이 된다는 점 조심하기

 

 

넘파이 배열y[0,2] 는 y[0][2] 랑 같은 의미 입니다.

위의 y[np.arange(batch_size),t]는 예시로 batch_size가 5이고 t는 [7,3,5,4,1]이라고 나온 경우 np.arange(batch_size)가 [0,1,2,3,4]를 뜻하고 t는 정답레이블 인덱스 숫자, 즉 정답숫자 그 자체를 의미하므로 [y[0,7], y[1,3], y[2,5], y[3,4], y[4,1]]인 넘파이 배열을 뜻하는 것입니다.

원-핫 인코딩일 경우 t는 결국 정답레이블 원소만 1이고 나머지 원소는 0이므로 y의 배열과 t의 배열을 곱하면 결국 y배열의 정답 원소만 값을 가지고 나머지는 0이 되버린 상태를 어차피 np.sum으로 합칠거니 위와 같이 (y[0,7], y[1,3]...) y의 배열에서만 값을 뽑아내도 문제가 없습니다.

 

 

np.sum() - 넘파이 배열의 원소들의 합을 나타내는 함수

               다차원 함수여도 모든 원소들의 합을 함

 

 

③ 손실 함수를 설정하는 이유

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

손실함수의 가중치에 대한 미분이란 '가중치 매개변수의 값을 아주 조금 변화 시켰을 때, 손실함수가 어떻게 변하냐'라는 의미입니다. 

예시로 이 미분값이 음수면 그 가중치의 매개변수를 양의방향으로 변화시켜 손실 함수의 값을 줄이고, 미분값이 양수면 매개변수를 음의방향으로 변화시켜 손실함수의 값을 줄일 수 있습니다.

매개변수(가중치와 편향)를 조절하여 손실함수의 값을 줄임으로써 신경망의 매개변수 오차를 줄여서 정확도가 높아집니다.

 

만약 지표를 손실함수가 아닌 정확도를 이용한다면 매개변수의 미분(변화량)이 대부분의 장소에서 0이 되기 때문에 가중치를 조절하면서 정확도를 높일 수 없습니다.

예를 들어 한 신경망이 100장의 훈련데이타중 41장을 올바로 인식한다고 하면 정확도는 41%입니다.

만약 정확도가 지표라면 매개변수(가중치와 편향)를 약간만 조정해도 정확도가 개선되지 않고 그대로 41%일 겁니다.

하지만 손실함수를 지표로 삼으면 손실함수의 값은 0.9325..와 같은 수치로 나타나므로 매개변수의 값이 조금만 바뀌어도 그에 반응하여 손실함수의 값도 연속적으로 변화할 것입니다.

그러므로 지표로 손실함수를 사용해야 합니다.

 

 

(4) 수치 미분

미분의 기본 수식은 다음과 같습니다.

미분 수식

다음 수식을 기준으로 코드를 작성하면 다음과 같습니다.

h는 0으로 수렴하는것을 표현하기 위해 아주 작은 값을 넣었습니다.

다음과 같이 미분을 코드로 작성할 경우엔 두 가지 개선할 점이 있습니다.

첫번 째

h에 최대한 작은 값을 대입하려고 0.00....1을 넣었는데 0이 50개라서 반올림 오차가 발생하여 h가 그냥 0이 되버립니다.

반올림 오차는 작은값이 생략되어서 최종 계산 결과에 오차가 생기게 합니다.

그래서 h를 10e-4 정도의 값을 사용하면 적당한 결과를 얻을 수 있다고 알려져 있습니다.

두번 째

위의 코드에서 나온 값 즉, 기울기와 실제 기울기의 오차가 납니다.

물론 애당초 오차가 나는것은 당연하지만 조금이라도 오차를 줄이기 위해서 중심 차분 미분 식으로 코드를 구현합니다.

 

 

편미분

편미분은 앞의 미분식과 달리 변수가 2개 이상입니다.

편미분을 통해 두 개의 기울기를 세트로 하여 벡터로 나타낼 수 있으며, f의 x에 대한 경사라고 부릅니다.

경사는 기울기가 가장 큰 방향과 그 크기를 나타냅니다.

 

이 함수식을 그래프로 표현하면 다음과 같습니다.

보통 편미분을 할 경우 위의 식 기준으로 설명할 때 X1에대한 편미분, X2에 대한 편미분을 나눠서 합니다.

하지만 신경망에서는 기울기를 알아야하기 때문에 편미분의 기울기를 알기 위해서는 변수별로 미분을 따로 하지 않고 묶어서 기울기를 알아야 합니다.

그 기울기는 다음과 같이 코드로 작성 할 수 있습니다.

편미분의 기울기 코드

function_2 함수는 위의 편미분 수식을 함수로 표현한 것이고 numerical_gradient함수는 편미분의 기울기를 구하는 함수입니다.

위의 예시에서 x1=3.0, x2=4.0 일경우 X1**2+X2**2의 기울기가 (6.0, 8,0) 으로 나왔습니다.

편미분의 기울기의 의미는 나온 결과의 기울기에 마이너스를 붙인 벡터를 그려보면 알 수 있습니다. 

그 그림은 다음과 같습니다.

그림을 보면 기울기에 마이너스를 붙인 벡터는 함수의 가장 낮은 장소(최솟값)를 가리킵니다.

(실제로 복잡한 함수에서는 기울기가 가리키는 방향에 최솟값이 없는 경우가 대부분임)

즉, 기울기(에 마이너스를 붙인 벡터)가 가리키는 쪽은 각 장소에서 함수의 출력 값을 가장 줄이는 방향입니다.

 

위의 그래프는 최솟값을 가는 방향으로 표현을 하였다면 원래 편미분 기울기에 대한 등고선과 화살표 방향은 다음과 같이 확인할 수 있습니다.

import numpy as np
import matplotlib.pyplot as plt

def f(w0, w1): # f의 정의
    return w0**2 + w1**2
def df_dw0(w0, w1): # f의 w0에 관한 편미분
    return 2 * w0
def df_dw1(w0, w1): # f의 w1에 관한 편미분
    return 2 * w1


w_range = 2
dw = 0.25
w0 = np.arange(-w_range, w_range + dw, dw)
w1 = np.arange(-w_range, w_range + dw, dw)
wn = w0.shape[0]
ww0, ww1 = np.meshgrid(w0, w1)
ff = np.zeros((len(w0), len(w1)))
dff_dw0 = np.zeros((len(w0), len(w1)))
dff_dw1 = np.zeros((len(w0), len(w1)))
for i0 in range(wn):
    for i1 in range(wn):
        ff[i1, i0] = f(w0[i0], w1[i1])
        dff_dw0[i1, i0] = df_dw0(w0[i0], w1[i1])
        dff_dw1[i1, i0] = df_dw1(w0[i0], w1[i1])
        
plt.figure(figsize=(9, 4))
plt.subplots_adjust(wspace=0.3)
plt.subplot(1, 2, 1)
cont = plt.contour(ww0, ww1, ff, 10, colors='k') # f의 등고선 표시
cont.clabel(fmt='%2.0f', fontsize=8)
plt.xticks(range(-w_range, w_range + 1, 1))
plt.yticks(range(-w_range, w_range + 1, 1))
plt.xlim(-w_range - 0.5, w_range + .5)
plt.ylim(-w_range - .5, w_range + .5)
plt.xlabel('$w_0$', fontsize=14)
plt.ylabel('$w_1$', fontsize=14)


plt.subplot(1, 2, 2)
plt.quiver(ww0, ww1, dff_dw0, dff_dw1) # f의 경사 벡터 표시
plt.xlabel('$w_0$', fontsize=14)
plt.ylabel('$w_1$', fontsize=14)
plt.xticks(range(-w_range, w_range + 1, 1))
plt.yticks(range(-w_range, w_range + 1, 1))
plt.xlim(-w_range - 0.5, w_range + .5)
plt.ylim(-w_range - .5, w_range + .5)
plt.show()

 

경사 하강법

경사 하강법은 현 위치에서 기울어진 방향으로 일정 거리 만큼 이동하고, 이동한 곳에서도 마찬가지로 기울기를 구하고, 또 기울어진 방향으로 나아가는 일을 반복하면서 함수의 값을 줄이는 것입니다.

경사법을 수식으로 나타내면 다음과 같습니다.

경사법 코드용 수식

여기서 에타(n처럼 생긴 문자)는 신경망 학습에서 학습률을 의미합니다.

한 번의 학습으로 매개변수 값을 얼마나 갱신하느냐를 정하는 것이 학습률입니다.

학습률 값은 0.01이나 0.001 등 미리 특정 값으로 정해두어야 합니다.

만약 학습률의 값이 너무 크거나 너무 작으면 경사법이 제역할을 하지 못합니다.

 

우선 경사 하강법은 다음과 같이 코드로 구현할 수 있습니다.

f는 함수, init_x는 초깃값(입력값), lr은 학습률, step_num은 반복횟수 입니다.

 

이 gradient_descent 함수를 사용하여 특정 f(X1,X2)=X1**2+X2**2의 최솟값을 아래 코드로 구할 수 있습니다.

경사 하강법을 100번 반복시켜서 최소값에 가까운 값을 얻었습니다.

실제 f(X1,X2)=X1**2+X2**2의 최솟값은 (0,0) 인데 출력 결과가 (0,0)에 매우 가깝습니다.

 

 

위에서 학습률은 0.01, 0.001 등 적당한 값이 아닌 너무 큰 값이나 작은 값을 설정할 경우 제 역할을 못하는 것을 아래의 코드에서 확인할 수 있습니다.

학습률이 너무 큰 경우 큰 음수 값으로 발산해 버리고 너무 작으면 입력 값이 갱신되지 않은 채 나옵니다.

따라서 적당한 학습률을 정해야 합니다.

 

 

 

 

(5) 신경망에서의 기울기

위에서 설명했듯이 매개변수(가중치)를 정확도 높게 설정하려면 손실함수의 값을 작게 만들어야 됩니다.

가중치 값을 얼마나 수정했을 때 손실함수의 값의 변화를 알기 위해선 가중치에 대한 손실함수의 미분 값을 알아야 합니다.

가중치를 W 손실함수가 L이라고 할 때 다음과 같이 수식을 표현할 수 있습니다.

 

다음은 가중치 값에 대한 손실함수의 미분(기울기) 값을 구하는 코드입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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
 
net=simpleNet()
x=np.array([0.6,0.9]) # 입력 데이터
t=np.array([0,0,1]) #정답레이블 원_핫 인코딩 한 상태처럼 초기화시킴
def f(W):   #가중치인 W에 대해서 미분을 한다는 의미로 W, W는 아무값 넣어도 됨
    return net.loss(x,t)
print(net.W)    #출력1
print(sep='\n')
p=net.predict(x)
print(p)        #출력2
print(sep='\n')
print(net.loss(x,t))  #3
print(sep='n')
dw=numerical_gradient(f,net.W) #가중치 W에 대해 미분하기 위해서 net.W넣음
print(dw)  #출력4
 
#common파일의 functions.py와 gradient.py 함께 열어 놓기
cs

 결과는 아래와 같습니다.

 common이라는 폴더에서 functions.py 코드의 softmax, cross_entropy_error 함수를 부르고 gradient.py 코드의 numerical_gradient함수(미분함수)를 불렀습니다. (이 함수는 전에 이미 설명해서 생략)

 

① __init__ (생성자)

자동으로 실행되며 객체변수 W에 가중치 값을 랜덤으로 설정합니다.

np.random() 함수

(1) np.random.randint() - 0부터 설정 범위 수만큼 랜덤으로 생성, 설정한 범위 만큼 랜덤으로 생성

                                형식: np.random.randint(범위수) or np.random.randint(시작범위수, 끝범위수)

(2) np.random.rand(m,n) - 0~1사이 난수를 설정 형상크기로 랜덤으로 생성

                             

(3) np.random.randn(m,n) - 평균0, 표준편차1인 난수를 형상크기로 랜덤으로 생성

 

② predict()

넘파이 행렬의 곱을 수행시키는 함수

 

③ loss()

입력값과 정답레이블을 받아서 predict()로 신경망 계산시키고 활성화 함수인 softmax()로 정리시킨 다음 손실함수인 cross_entropy_error 의 결과를 반환하는 함수

결국 손실함수의 값을 반환하니 손실함수라고 취급해도 됨

 

 

#출력4 부분이 가중치에 대한 손실함수의 미분을 출력하는 코드 줄입니다.

가중치에 대한 손실함수의 미분을 시키긴다는 의미로 26줄의 f(W) 함수의 매개변수를 W로 설정했습니다.

사실 위의 코드에서 f(W)의 W값을 아무 숫자나 변수로 설정해도 상관이 없습니다.

이 매개변수 W는 위의 코드에서 의미가 없습니다.

위의 코드에서 가중치에 대한 손실함수의 미분을 할 때 '가중치에 대한' 의 역할을 하는 매개변수는 결국 numerical_gradient메소드의 x매개변수입니다.

그 이유는 numerical_gradient 메소드의 매개변수가 f와 x인데 위의 코드에서 35번째 줄에 numerical_gradient의 매개변수에 넣는 값이 net.W입니다.

net.W는 net 객체가 따로 가지고 있는 W라는 변수라 따로 메모리가 있습니다.

이러한 new.W자체가 numerical_gradient메소드에 들어가서 numerical_gradient가 new.W를 직접적으로 수정(h를 더함)하고 f(W)의 리턴값인 net.loss(x,t)안에 net.W식이 포함되 있으므로 리턴값인 net.loss(x,t)안의 net.W의 값을 수정하는건 f(W)의 W가 아니라 numerical_gradient메소드의 x 매개변수에 들어간 net.W입니다.

그러므로 f(W)의 W값은 아무값이 넣어도 상관 없습니다.

 

가중치에 대한 손실함수 미분 부분만 생각해서 순서를 설명하겠습니다.

 

설명하기 전에 numerical_gradient코드는 다음과 같습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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
cs

 

1. 35줄의 numerical_gradient를 호출합니다. 

매개변수는 loss함수인 f와 가중치에 대해 미분하기 위해서 net.W를 넣습니다.

 

2. numerical_gradient 코드의 9번째 줄에서 net.W의 값에 h를 더합니다.

net객체의 해당 W변수는 기존 값보다 h만큼 증가했습니다.

 

3. numerical_gradient 코드의 10번째 줄에서 f(x)의 역할은 바뀐 net.W(h만큼 증가됨)를 손실함수인 f(W)에 적용하기 위해서 쓰입니다.

이 f(x)의 x 값도 사실 아무 값이나 넣어줘도 상관 없습니다.

예를들어 x대신에 1을 넣었다고 가정합니다.  fxh1 = f(1

그럼 26번째 줄의 f(W)로 갈텐데 이 W값은 리턴값인 net.loss(x,t)에 존재하지 않은 값입니다.

net.loss(x,t)는 W값과 상관 없이 2. 번 과정의 net.W가 h만큼 증가된 값으로 계산되어서 리턴할 것입니다.

결국 fxh1변수에는 해당 가중치값에 h만큼 더해진 값이 손실함수에 적용되어 나온 값이 될 것입니다.

위에서 주석으로 fxh1에 f(x+h)가 들어간다고 표현했는데 정확하게 설명하자면 net.loss(x,t) 자체를 L(x)라고 비유할 때 fxh1에 들어가는 값이 L(x+h)가 되는 것입니다.   (      L(net.W의 특정값+h)      )

 

 

 

 

 

 

 

 

 

 

(6) 학습 알고리즘 구현

가중치에 대한 미분값을 구하는 방법을 알았으니 이를 응용하여 가중치를 조절하는 학습 알고리즘을 구현해 보겠습니다.

 

우선 2층 신경망 클래스를 다음과 같이 코드로 구현하겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
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) #def f(w): 이것과 같음
        
        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
cs

① __init__ (생성자)

자동으로 실행되며 1층과 2층의 가중치와 편향값을 랜덤으로 설정합니다.

랜덤으로 생성할 때는 입력층, 은닉층, 출력층의 갯수를 고려해서 행렬의 형상을 정합니다.

 

② predict()

행렬을 이용해서 입력층의 값들을 가중치와 편향으로 계산을 한 후 활성화 함수과정을 거치고 앞의 과정을 한 번 더 반복 후 소프트맥스 함수로 출력층 값을 반환합니다.

 

③ loss()

출력층의 값을 교차 엔트로피 오차 함수에 넣어서 손실함수의 값을 반환합니다.

 

④accuracy()

소프트맥스 함수를 거친 출력층들의 값에서 각 행마다 가장 큰 원소의 인덱스숫자를 담은 배열을 y에 넣고, 정답레이블인 t 배열의 각 행마다 가장 큰 원소의 인덱스 숫자를 담은 배열을 t에 넣고 y와 t가 같을 때 true가 되서 그 true값들의 합에 출력행렬의 행의 갯수를 나눠서 정확도를 구하는 함수입니다.

(이 함수는 t가 원-핫 인코딩형식일 경우만 되는 함수임)

 

⑤numerical_gradient()

grad라는 딕셔너리 변수를 정의하고 이전에 설명한 기울기를 구하는 함수인 numerical_gradient() 함수를 이용해서 각 계층의 가중치와 편향에 대한 기울기를 grad 딕셔너리 변수에 저장시킵니다.

여기서 큰 틀인 numerical_gradient()함수와 이 함수안에 있는 numerical_gradient()함수는 매개변수 갯수가 다르므로 서로 다른 함수입니다.

 

 

 

위의 2층 신경망 클래스으로 '손글씨 분류' 학습 알고리즘을 구현한 코드는 아래와 같습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
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)
 
network = TwoLayerNet(input_size=784, hidden_size=50, output_size=10)
 
# 하이퍼파라미터
iters_num = 10000  # 반복 횟수를 적절히 설정한다.
train_size = x_train.shape[0]  #60000
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#적어도 1을 반환시키기위해 ',1' 넣은것임
 
for i in range(iters_num):
    # 미니배치 획득
    batch_mask = np.random.choice(train_size, batch_size)# 0부터59999중 랜덤으로 100개 뽑기
    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))
cs

28~30 줄은 x_batch라는 넘파이 배열에 x_train넘파이 배열중 100개의 행을 뽑아서 넣은 것입니다.

해당 문법은 다음 예제를 통해 numpy특징을 알 수 있습니다.

 

37~38 줄이 학습알고리즘으로 가중치와 편향들을 조정하는 줄입니다.

조정된 값(가중치 혹은 편향) = 조정할값(기존 가중치 혹은 편향) - 학습률 * 손실함수에 대한 가중치(혹은 편향) 미분 값

기울기가 음수면 손실함수는 줄어드는 방향으로 나아가고 있다는 의미로 가중치를 양의 방향으로 이동시키면 손실함수가 더 줄어드니까 '- 학습률 * 음수기울기' 는 곧 양수이므로 가중치의 값을 기존보다 늘리는 것입니다.

반대로 기울기 양수면 손실함수가 증가하는 방향으로 나아가고 있다는 의미로 가중치를 음의 방향으로 이동시키면 손실함수가 더 줄어드니까 ' - 학습률 * 양수기울기' 는 곧 음수이므로 가중치의 값을 기존보다 줄이는 것입니다.

 

에폭은 하나의 단위입니다. 

1에폭은 학습에서 훈련 데이터를 모두 소진했을 때의 횟수에 해당합니다.

 

45줄에 다음과 같은 조건코드가 나온 이유는 위 코드의 학습 데이터양은 60000장이고 배치는 100장이므로 총 600번을 학습시킬 때 비로소 60000장을 학습시키게 됩니다.

따라서 iter_per_epoch는 600 이므로 600번 할때마다 다음과 같이 정확도를 표시해주는 코드가 구현된 것입니다.

 

'It공부 > Deep learning' 카테고리의 다른 글

6. 학습 관련 기술들  (0) 2020.06.21
5. 오차역전파법  (0) 2020.06.19
3-(1) 손글씨 숫자 인식 분석  (1) 2020.06.16
3. 신경망  (0) 2020.06.16
2. 퍼셉트론  (0) 2020.06.15
Comments