AI/AI 기초

[AI 기초] 3. 신경망

kk_______yy 2024. 1. 17. 15:04

퍼셉트론 관련해서는 좋은 소식과 나쁜 소식

  • good : 퍼셉트론으로 복잡한 함수도 표현할 수 있다는 것
  • bad : 가중치를 설정하는 작업(원하는 결과를 출력하도록 가중치 값을 적절히 정하는 작업)은 여전히 사람이 수동

신경망은 이 나쁜 소식을 해결

가중치 매개변수의 적절한 값을 데이터로부터 자동으로 학습

 

3.1 퍼셉트론에서 신경망으로

3.1.1 신경망의 예

그림 3-1

  • 은닉층 뉴런 : (입력층, 출력층과 달리) 사람 눈에 보이지 않음. '은닉'

* 구현 편의성을 위해 순서대로 0, 1, 2층으로 명명

* 신경망은 총 3층이지만, 가중치 갖는 층이 2개 이므로 '2층 신경망'

 

신경망은 퍼셉트론과 달리 신호를 어떻게 전달할까?

 

3.1.2 퍼셉트론 복습

그림 3-2 퍼셉트론 복습

x1과 x2라는 두 신호를 입력받아 y 출력

식 3.1

b : 편향 매개변수, 뉴런이 얼마나 쉽게 활성화되느냐 제어

w1, w2 : 각 신호 가중치 매개변수, 각 신호의 영향력 제어

 

 

그림 3-3 편향을 명시한 퍼셉트론

  • 가중치 b, 입력 1인 뉴런 추가
  • 퍼셉트론의 동작은 x1, x2, 1이라는 3개의 신호가 뉴런에 입력 - 각 신호에 가중치 곱한 후, 다음 뉴런 전달
  • 합이 0을 넘으면 1 출력, 그렇지 않으면 0 출력

 

 

 

식 3.2 입력 신호 총합이 h(x)라는 함수를 거쳐 변환, y의 출력이 됨
식 3.3 입력 0 넘으면 1, 그렇지 않으면 0

  • [식 3.1], [3.2], [3.3]이 하는 일은 동일

 

3.1.3 활성화 함수의 등장

활성화 함수(activation function)

  • 입력 신호의 총합을 출력 신호로 변환하는 함수
  • '활성화' 입력 신호의 총합이 활성화를 일으키는지 정하는 역할

 

[식 3.2] 두 단계

  • 가중치가 곱해진 입력 신호의 총합 계산
  • 활성화 함수에 입력해 결과

이를 다음과 같은 2개의 식으로 나눌 수 있음

식 3.4 가중치가 달린 입력 신호와 편향의 총합 계산(a)
식 3.5 a를 함수 h( )에 넣어 y를 출력하는 흐름

 

 

지금까지와 같이 뉴런을 큰 원으로 그리면 [그림 3-4]처럼 나타낼 수 있음

그림 3-4 활성화 함수의 처리 과정

  • 가중치 신호를 조합한 결과가 a라는 노드가 되고, 활성화 함수 h( )를 통과하여 y라는 노드로 변환되는 과정
    * 뉴런 = 노드
    * 보통 뉴런을 하나의 원으로 그리나, 신경망 동작을 더 명확히 드러낼 땐 활성화 처리 과정 명시

 

[그림 3-5] 왼 : 일반적인 뉴런, 오: 활성화 처리 과정 명시(a 입력신호총합, h() 활성화함수, y 출력)

* WARNING

  • 단순 퍼셉트론 : 단층 네트워크에서 계단 함수(임계값을 경계로 출력이 바뀌는 함수)를 활성화 함수로 사용한 모델
  • 다층 퍼셉트론 : 신경망(여러 층 구성, 시그모이드 함수 등 매끈한 활성화 함수 사용)

 

3.2 활성화 함수

계단 함수(step function) : [식 3.3] 활성화 함수는 임계값 경계로 출력 바뀜

  • '퍼셉트론에서는 활성화 함수로 계단 함수 이용' (임곗값 이하, 초과 기준)

 

활성화 함수를 다른 함수로 바꿔보자!

 

3.2.1 시그모이드 함수

[식 3.6] 시그모이드 함수

  • 얼핏 복잡해 보이지만, 단순한 '함수'
    * 함수 : 입력을 주면 출력을 돌려주는 변환기
  • 퍼셉트론과 신경망의 주된 차이 : 활성화 함수

 

3.2.2 계단 함수 구현하기

# 파이썬 계단 함수 (x 실수만 받아들임)
def step_functio(x):
    if x > 0:
        return 1
    else:
        return 0
        
# 파이썬 계단 함수 (넘파이 배열 지원)
def step_function(x):
    y = x > 0    # True or False
    return y.astype(np.int)    # True: 1, False: 0 반환
  • y = x > 0    # True or False
  • return y.astype(np.int)    # True: 1, False: 0 반환
    * 넘파이 배열의 자료형 변환 : astype( ) 메서드

 

3.2.3 계단 함수의 그래프

import numpy as np
import matplotlib.pylab as plt

def step_function(x):
    return np.array(x > 0, dtype=np.int)
    
x = np.arange(-5.0, 5.0, 0.1)    # -0.5 ~ 5.0 전까지 0.1 간격의 넘파이 배열
y = step_function(x)
plt.plot(x, y)
plt.ylim(-0.1, 1.1)    # y축의 범위 지정
pt.show()

[그림 3-6] 계단 함수의 그래프

 

3.2.4 시그모이드 함수 구현하기

def sigmoid(x):
    return 1 / (1 + np.exp(-x))
    
x = np.array([-1.0, 1.0, 2.0])
sigmoid(x)
t = np.array([1.0, 2.0, 3.0])
1.0 + t
>>> array([2., 3., 4.])
1.0 / t
>>> array([1., 0.5, 0.33333333])

* 넘파이 브로드캐스트

  : 넘파이 배열과 스칼라값의 연산을 넘파이 배열의 원소 각각과 스칼라값의 연산으로 바꿔 수행

 

x = np.arange(-5.0, 5.0, 0.1)
y = sigmoid(x)
plt.plot(x, y)
plt.ylim(-0.1, 1.1)    # y축 범위 지정
plt.show()

[그림 3-7 시그모이드 함수의 그래프]

* 시그모이드 : 'S자 모양'이라는 뜻

 

3.2.5 시그모이드 함수와 계단 함수 비교

[그림 3-8]

차이점

계단 함수 시그모이드 함수
0을 경계로 출력 바뀜 부드러운 곡선, 연속적 출력
0과 1 출력 실수 출력

 

공통점

  • 입력이 커지면 출력 1에 가까움
  • 입력이 작으면 출력 0에 가까움
  • 비선형 함수

 

3.2.6 비선형 함수

계단함수와 시그모이드 함수의 중요한 공통점 '비선형 함수'

 

NOTE

  • 함수 : 어떤 값을 입력하면 그에 따른 값을 돌려주는 '변환기'
  • 선형 함수 : 변환기에 무언가 입력했을 때 출력이 입력의 상수배만큼 변하는 함수. f(x) = ax + b (a, b 상수)
  • 비선형 함수 : '선형이 아닌' 함수. 직선 1개로는 그릴 수 없는 함수.

 

신경망에서는 비선형 함수를 사용!

  • 선형 함수를 이용하면 신경망의 층을 깊게 하는 의미가 없어지기 때문
  • 층을 아무리 깊게 해도 '은닉층이 없는 네트워크'로도 똑같은 기능

ex) h(x) = cx 를 활성화 함수로 사용한 3층 네트워크 y(x) = h(h(h(x))) 는 결국 y(x) = ax 와 똑같은 식

 

활성화 함수는 꼭 비선형 함수!

 

3.2.7 ReLU 함수

ReLU(Rectified Linear Unit) 함수 : 입력이 0을 넘으면 입력대로 출력, 0 이하면 0을 출력

[그림 3-9]
[식 3.7]

 

def relu(x):
    return np.maximum(0, x)

 

* 넘파이 maximum 함수 : 두 입력 중 큰 값 선택하여 반환

 

3.3 다차원 배열의 계산

3.3.1 다차원 배열

다차원 배열도 그 기본은 '숫자의 집합'

# 넘파이 다차원 배열
import numpy as np
A = np.array([1, 2, 3, 4])
print(A)
>>> [1 2 3 4]
np.ndim(A)    # 배열의 차원 수
>>> 1
A.shape       # 배열의 형상 -> 튜플 반환(다차원 배열에서 동일한 출력 내고자)
>>> (4,)
A.shape[0]
>>> 4

[그림 3-10]

2차원 배열은 특히 행렬(matrix)

  • 가로는 행(row) 세로는 열(column)

 

3.3.2 행렬의 곱

[그림 3-11] 행렬의 곱 계산 방법

  • 왼쪽 행렬의 행(가로) X 오른쪽 행렬의 열(세로)
  • 원소별로 곱하고 그 값들을 더해서 계산
  • 계산 결과가 새로운 다차열 배열의 원소

* 책에서 행렬 A처럼 표기

# 2 X 2 행렬 A, B
A = np.array([[1, 2], [3, 4]])
A.shape
>>> (2, 2)

B = np.array([[5, 6], [7, 8]])
B.shape
>>> (2, 2)

np.dot(A, B)    # 두 행렬의 곱은 넘파이 함수 np.dot()으로 계산
>>> array ([[19, 22], 
             43, 50]])
  • np.dot(A, B)와 np.dot(B, A)는 다른 값이 될 수 있음.
  • 행렬의 형상(shape)에 주의 : 행렬 A의 열 수와 행렬 B의 행 수가 같아야 함. 그렇지 않으면 에러 발생.

[그림 3-12]

  • 행렬 A와 B의 대응하는 차원의 원소 수가 같아야 함
  • 계산 결과인 행렬 C의 형상은 행렬 A의 행 수와 행렬 B의 열 수가 됨

[그림3-13] A가 2차원 행렬, B가 1차원 배열일 때도 대응하는 원소 수를 일치시켜라

 

# 3-13의 예
A = np.array([1, 2], [3, 4], [5, 6]])
A.shape
>>> (3, 2)
B = np.array([7, 8])
B.shape
>>> (2, )
np.dot(A, B)
>>> array([23, 53, 83])

 

3.3.3 신경망에서의 행렬 곱

[그림 3-14] 행렬의 곱으로 신경망의 계산 수행

넘파이 행렬을 이용한 신경망 구현

X = np.array([1, 2])
X.shape
>>> (2, )
W = np.array([1, 3, 5], [2, 4, 6]])
pritn(W)
>>> [[1 3 5]
     [2 4 6]]
W.shape
>>> (2, 3)
Y = np.dot(X, W)    # 다차원 배열의 스칼라곱 구해주는 함수
print(Y)
>>> [5 11 17]

 

3.4 3층 신경망 구현하기

 

[그림 3-15] 3층 신경망

  • 3층 신경망에서 수행되는, 입력부터 출력까지의 처리(순방향 처리) 구현
    * 넘파이 배열을 잘 사용하면 아주 적은 코드만으로 신경망의 순방향 처리를 완성

 

3.4.1 표기법 설명

* WARNING : 이 절의 핵심은 신경망에서의 계산을 행렬 계산으로 정리 가능하다는 것

[그림 3-16] ~층에 속한 뉴런, 다음층 뉴런 인덱스와 앞층의 인덱스

 

3.4.2 각 층의 신호 전달 구현하기

[그림 3-17]

 

  • '1층의 첫 번째 뉴런'으로 가는 신호를 살펴보자
  • 편향을 뜻하는 뉴런 '1'이 추가됨     # 편향은 오른쪽 아래 인덱스 하나뿐 (앞 층의 편향 뉴런이 하나라서)

 

  • a1(1)은 가중치를 곱한 신호 두 개와 편향을 합하여 계산

[식 3.8]

  • 행렬의 곱을 이용하면 1층의 '가중치 부분'을 다음 식처럼 간소화할 수 있음

[식 3.9]

위 식에서 각각의 행렬은 다음과 같음

A(1) = (a1(1)  a2(1)  a3(1))

X = (x1 x2)

B(1) = (b1(1)  b2(1)  b3(1))

W(1) = ( w11(1) w21(1) w31(1)

              w12(1) w22(1) w32(1) )

 

넘파이 다차원 배열을 사용한 [식 3.9] 구현

# [식 3.9] 구현
X = np.array([1.0, 0.5])
W1 = np.array([[0.1, 0.3, 0.5], [0.2, 0.4, 0.6]])
B1 = np.array([0.1, 0.2, 0.3])

print(W1.shape)    # (2, 3)
print(X.shape)     # (2, )
print(B1.shape)    # (3, )

A1 = np.dot(X, W1) + B1
  • W1은 2X3 행렬, X는 원소가 2개인 1차원 배열 → W1과 X의 대응하는 차원의 원소 수 일치

 

1층의 활성화 함수에서의 처리를 살펴보자.

[그림 3-18] 입력층에서 1층으로의 신호 전달

  • a : 은닉층에서의 가중치 합(가중 신호와 편향의 총합)
  • z : 활성화 함수 h()로 변환된 신호
    * 활성화 함수로 시그모이드 함수 사용 가정

 

Z1 = sigmoid(A1)    # 앞서 정의한 함수, 넘파이 배열 받아 같은 수의 원소로 구성된 넘파이 배열 반환

print(A1)    # [0.3, 0.7, 1.1]
print(Z1)    # [0. 57444252, 0.66818777, 0.75026011]

 

1층에서 2층으로 가는 과정과 그 구현

[그림 3-19] 1층에서 2층으로의 신호 전달

# 1층에서 2층으로 가는 과정 [그림 3-19] 구현
W2 = np.array([[0.1, 0.4], [0.2, 0.5], [0.3, 0.6]])
B2 = np.array([0.1, 0.2])

print(Z1.shape)    # (3, )
print(W2.shape)    # (3, 2)
print(B2.shape)    # (2, )

A2 = np.dot(Z1, W2) + B2
Z2 = sigmoid(A2)

 

1층의 출력 Z1이 2층의 입력이 됨

→ 넘파이 배열을 사용하면서 층 사이의 신호 전달을 쉽게 구현

 

2층에서 출력층을의 신호 전달(그림 3-20)

활성화 함수만 지금까지의 은닉층과 다름 

[그림 3-20] 2층에서 출력층으로의 신호 전달

# [그림 3-20] 2층에서 출력층으로의 신호 전달
def identity_function(x):
    return x
    
W3 = np.array([[0.1, 0.3], [0.2, [0.4]])
B3 = np.array([0.1, 0.2])

A3 = np.dot(Z2, W3) + B3
Y = identity_function(A3)    # 혹은 Y = A3

 

  • 항등 함수 identity_function() 출력층의 활성화 함수로 이용
    → 굳이 정의 필요 없지만, 그동안의 흐름과 통일하기 위해 사용
  • [그림 3-20]에서는 출력층의 활성화 함수를 σ()로 표시하여 은닉층의 활성화 함수 h()와는 다름을 명시

* 항등 함수 : 입력을 그대로 출력

 

* NOTE

출력층의 활성화 함수는 풀고자 하는 문제 성질에 맞게 정의

  • 회귀 → 항등 함수
  • 클래스 분류 → 시그모이드 함수
  • 다중 클래스 분류 → 소프트맥스 함수

 

3.4.3 구현 정리

* 신경망 구현 관례 : 가중치만 W1 대문자, 그 외 편향과 중간 결과 등은 모두 소문자

def init_network():    # 가중치와 편향 초기화, 딕셔너리 변수 network에 저장
    network = {}
    network['W1'] = np.array([[0.1, 0.3, 0.5], [0.2, 0.4, 0.6]])
    network['b1'] = np.array([0.1, 0.2, 0.3])
    network['W2'] = np.array([[0.1, 0.4], [0.2, 0.5], [0.3, 0.6]])
    network['b2'] = np.array([0.1, 0.2])
    network['W3'] = np.array([[0.1, 0.3], [0.2, 0.4]])
    networt['b3'] = np.array([0.1, 0.2])
    
    return network
    
def forward(network, x):    # 입력 신홀 출력으로 변환하는 처리 과정
    W1, W2, W3 = network['W1'], network['W2'], network['W3']
    b1, b2, b3 = network['b1'], network['b2'], network['b3']
    
    a1 = np.dot(x, W1) + b1
    z1 = sigmoid(a1)
    a2 = np.dot(z1, W2) + b2
    z2 = sigmoid(a2)
    a3 = np.dot(z2, W3) + b3
    y = identity_function(a3)
    
    return y

network = init_network()
x = np.array([1.0, 0.5])
y = forward(network, x)
print(y)    # [0.31682708   0.69627909]

* forward : 신호가 순방향(입력에서 출력 방향)으로 전달됨(순전파)

 

3.5 출력층 설계하기

신경망은 분류와 회귀 모두에 이용

다만 둘 중 어떤 문제냐에 따라 출력층에서 사용하는 활성화 함수 달라짐

  • 회귀(입력 데이터에서 연속적인 수치 예측) - 항등 함수
  • 분류(데이터가 어느 클래스에 속하느냐 문제) - 소프트맥스 함수

 

3.5.1 항등 함수와 소프트맥스 함수 구현하기

항등 함수(identify function) : 입력 그대로 출력, '입력 = 출력'의 항등

  • 출력층에서 항등 함수를 사용하면 입력 신호가 그대로 출력 신호 됨.

[그림 3-21] 항등 함수

 

소프트맥스 함수(softmax function) : 분자는 입력 신호의 지수 함수, 분모는 모든 입력 신호의 지수 함수 합으로 구성

  • 분류에 사용
  • 소프트맥스의 출력은 모든 입력 신호로부터 화살표 받음.
    ([식 3.10]의 분모에서 보듯, 출력층 각 뉴런이 모든 입력 신호에서 영상 받음)

[식 3.10] 소프트맥스 함수
[그림 3-22] 소프트맥스 함수

# 소프트맥스 함수 구현
a = np.array([0.3, 2.9, 4.0])

exp_a = np.exp(a)    # 지수 함수
print(exp_a)
>>> [1.34985881   18.17414537   54.59815003]

sum_exp_a = np.sum(exp_a)    # 지수 함수의 합
print(sum_exp_a)
>>> 74.1221542102

y = exp_a / sum_exp_a
print(y)
>>> [0.01821127   0.24519181   0.73659691]
# 파이썬 함수화
def softmax(a):    # np.array 형태로 입력 들어옴
    exp_a = np.exp(a)
    sum_exp_a = np.sum(exp_a)
    y = exp_a / sum_exp_a
    
    return y

 

3.5.2 소프트맥스 함수 구현 시 주의점

앞의 softmax() 함수는 오버플로 문제 발생

 

* WARNING

  • 오버플로 : 표현할 수 있는 수의 범위가 한정되어 너무 큰 값은 표현할 수 없는 문제

 

이를 해결하기 위해 수식을 개선해보자.

[식 3.11] 개선한 소프트맥스 함수

  • [식 3.11]이 말하는 것은 소프트맥스의 지수 함수를 계산할 때 어떤 정수를 더해도(빼도) 결과는 바뀌지 않는다는 것
  • C'에는 뭘 넣어도 상관없지만, 오버플로를 막을 목적으로는 입력 신호 중 최댓값 이용이 일반적
# 예시 코드
a = np.array([1010,. 1000, 990])
np.exp(a) / np.sum(np.wxp(a))    # 소프트맥스 함수의 계산
>>> array([nan, nan, nan)        # 제대로 계산되지 않음

c = np.max(a)    # c = 1010 (최댓값)
a - c
>>> array([0, -10, -20])

np.exp(a - c) / np.sum(np.ecp(a - c))
>> array([9.99954600e-01,   4.53978686e-05,    2.06106005e-09])
  • 위의 예시에서 아무런 조치 없이 그냥 계산하면 nan 출력
  • 하지만, 입력 신호 중 최댓값(c)를 빼주면 올바르게 계산

 

# 개선된 소프트맥스 함수
def softmax(a)
    c = np.max(a)
    exp_a = np.exp(a - c)    # 오버플로 대책
    sum_exp_a = np.sum(exp_a)
    y = exp_a / sum_exp_a
    
    return y

 

3.5.3 소프트맥스 함수의 특징

# softmax() 함수 사용한 신경망 출력
a = np.array([0.3, 2.9, 4.0])
y = softmax(a)
print(y)
>>> [0.01821127   0.24519181   0.73659691]
np.sum(y)
>>> 1.0

 

소프트맥스 함수

  • 출력은 0 ~ 1.0 사이의 실수
  • 함수 출력의 총합은 1 (중요) → 소프트맥스 함수 출력을 '확률'로 해석 가능
    ex) y[0]의 확률은 0.018(1.8%) / y[1]의 확률은 0.245(24.5%) / y[2]의 확률은 0.737(73.7%)로 해석 가능
          - "2번째 원소의 확률이 가장 높으니, 답은 2번째 클래스다!"라는 해석
          - "74%의 확률로 2번째 클래스, 25%의 확률로 1번째 클래스, 1%의 확률로 0번째 클래스다"와 같은 확률적인 결론
  • 즉, 소프트맥스 함수를 이용하여 문제를 확률적(통계적)으로 대응할 수 있게 됨

 

주의 : 소프트맥스 함수를 적용해도 각 원소의 대소 관계는 변하지 않음
→ 지수 함수 y = exp(x)가 단조 증가 함수라서

 

  • 신경망을 이용한 분류에서는 일반적으로 가장 큰 출력을 내는 뉴런에 해당하는 클래스로만 인식
  • 소프트맥스 함수를 적용해도 출력이 가장 큰 뉴런의 위치 달라지지 않음
  • 결과적으로, 신경망으로 분류할 때는 출력층의 소프트맥스 함수를 생략해도 됨
    (현업에서도 지수 함수 계산에 드는 자원 낭비를 줄이고자 출력층의 소프트맥스 함수는 생략이 일반적)

 

* NOTE

  • 기계학습의 문제 풀이는 학습 + 추론
  • 추론 단계에서는 출력층의 소프트맥스 함수 생략이 일반적
  • 한편, 신경망을 학습시킬 때는 출력층에서 소프트맥스 함수 사용

 

3.5.4 출력층의 뉴런 수 정하기

  • 출력층의 뉴런 수는 문제에 적절히 설정
  • 분류에서는 분류하고 싶은 클래스 수로 설정하는 것이 일반적
  • ex) 입력 숫자 이미지 0~9 중 하나로 분류하는 문제 → 출력층의 뉴런을 10개로 설정

  • 뉴런의 회색 농도가 해당 뉴런의 출력 값의 크기 의미 → y2 뉴런이 가장 큰 값을 출력, 숫자 '2'로 판단

 

3.6 손글씨 숫자 인식

 

* NOTE

  • 기계학습과 마찬가지로 신경망도 두 단계를 거쳐 문제 해결
  • 학습 : 훈련 데이터를 사용해 가중치 매개변수 학습
  • 추론 : 앞서 학습한 매개변수를 사용하여 입력 데이터 분류

 

3.6.1 MNIST 데이터셋

0 ~ 9까지의 숫자 이미지로 구성

훈련 이미지 60,000장, 시험 이미지 10,000장

28 x 28 크기의 회색조 이미지91채널)

각 픽셀은 0 ~ 255 값

실제 의미하는 숫자가 레이블로 붙어있음

[그림 3-24]

# MNIST 데이터를 쉽게 가져오는 법
import sys, os
sys.path.append(os.pardir)    # 부모 디렉터리의 파일을 가져올 수 있도록 설정
from dataset.mnist import load_mnist

# 처음 한 번은 몇 분 정도 걸립니다.
(x_train, t_train), (x_test, t_test) = load_mnist(flatten=True, normalize=False)

# 각 데이터의 형상 출력
print(x_train.shape)    # (60000, 784)
print(t_train.shape)    # (60000, )
print(x_test.shape)     # (10000, 784)
print(t_test.shape)     # (10000, )

 

* load_mnist 함수

  • MNIST 데이터를 (훈련 이미지, 훈련 레이블), (시험 이미지, 시험 레이블) 형식으로 반환

인수

  • normalize
    - True(입력 이미지의 픽셀값을 0.0~1.0 사이의 값으로 정규화 설정)
    - False(입력 이미지의 픽셀은 원래 값 그대로 0~255 사이의 값을 유지)
  • flatten : 입력이미지를 평탄하게, 즉 1차원 배열로 만들지 정함
    - True(784개의 원소로 이뤄진 1차원 배열로 저장)
    - False(입력 이미지를 1 x 28 x 28의 3차원 배열로 저장)
  • one_hot_label : [0,0,1,0,0,0,0,0,0]처럼 정답을 뜻하는 원소만 1이고 나머지는 모두 0인 배열
    - True(원-핫 인코딩하여 저장)
    - False('7', '2'와 같은 숫자 형태로 저장)

 

PIL(python image library) 모듈로 이미지 표시

import sys, os
sys.path.append(os.pardir)
import numpy as np
from dataset.mnist import load_mnist
from PIL import Image

def img_show(img):
    pil_img = Image.fromarray(np.unit8(img))
    pil_img.show()
    
(x_train, t_train), (x_test, t_test) = load_mnist(flatten=True, normalize=False)

img = x_train[0]
label = t_train[0]
print(label)     # 5

print(img.shape)             # (784, )
img = img.reshape(28, 28)    # 원래 이미지의 모양으로 변형
print(img.shape)             # (28, 28)

img_show(img)

[그림 3-25]

  • flatten=True로 읽어들인 이미지는 1차원 배열이므로, 28 x 28 크기로 다시 변형해야 함
  • reshape() 메서드에 원하는 형상으로 인수를 지정하면 넘파이 배열의 형상 바꿀 수 있음
  • 넘파이로 저장된 이미지 데이터를 PIL용 데이터 객체로 변환해야 함 → Image.fromarray()가 수행

 

3.6.2 신경망의 추론 처리

MNIST 데이터셋으로 추론 수행 신경망을 구현하자

  • 입력층 뉴런 784개(이미지 크기 28x28=784), 출력층 뉴런 10개(0~9까지 숫자 구분)로 구성
  • 은닉층은 총 두 개, 첫 번째 은닉층은 50개의 뉴런, 두 번째 은닉층에는 100개의 뉴런 (50, 100은 임의로 정함)
 def get_data():
     (x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, flatten=True, one_hot_label=False)
     return x_test, t_test
     
 def init_networt():
     with open("sample_weight.pkl", 'rb') as f:
         network = pickle.load(f)     
     return network
     
 def predict(network, x):
     W1, W2, W3 = network['W1'], network['W2'], network['W3']
     b1, b2, b3 = network['b1'], network['b2'], network['b3']
     
     a1 = np.dot(x, W1) + b1
     z1 = sigmoid(a1)
     a2 = np.dot(z1, W2) + b2
     z2 = sigmoid(a2)
     a3 = np.dot(z2, W3) + b3
     y = softmax(a3)
     
     return y
  • init_network()에서는 pickle 파일인 sample_weight.pkl에 저장된 '학습된 가중치 매개변수'를 읽음
    → 가중치와 편향 매개변수가 딕션너리 변수로 저장

 

이 세 함수를 이용한 신경망 추론을 수행하고, 정확도(accuracy)를 평가하자

x, t = get_data()
network = init_network()

accuracy_cnt = 0
for i in range(len(x)):
    y = predict(network, x[i])
    p = np.argmax(y)    # 확률이 가장 높은 원소의 인덱스를 얻는다.
    if p == t[i]:
        accuracy_cont += 1
        
print("Accuracy:" + str(float(accuracy_cnt) / len(x)))
>>> Accuracy:0.9352
  1. MNIST 데이터셋을 얻고 네트워크 생성
  2. for 문을 돌며 x에 저장된 이미지 데이터를 1장씩 꺼내 predict( ) 함수로 분류
  3. predict( ) 함수는 각 레이블의 확률을 넘파이 배열로 반환
    ex) [0.1, 0.3, 0.2, ..., 0.04]와 같은 배열 반환, 숫자 0일 확률 0.1, ... 식으로 해석
  4. np.argmax() 함수로 배열에서 값이 가장 큰 원소의 인덱스 구함 → 예측 결과
  5. 신경망이 예측한 답변과 정답 레이블을 비교하여 맞힌 숫자(accuaracy_cnt)를 세고, 이를 전체 이미지 숫자로 나눠 정확도 구함

출력 : Accuracy:0.9352    # 올바르게 분류한 비율 93.52% 라는 의미

(앞으로 순차적으로 공부하며 정확도 높여가보자)

 

 

* 전처리(pre-processing) : 신경망의 입력 데이터에 특정 변환

  • ex) load_mnist 인수로 normalize=True → 0~255 범위를 0~1.0 범위로(단순히 픽셀을 255로 나눔)

 

* 정규화(normalization) : 데이터를 특정 범위로 변환하는 처리

  • 입력 이미지 데이터에 대하 전처리 작업으로 정규화 수행

 

* NOTE : 전처리

  • 현업에서도 활발히 사용
  • 전처리를 통해 식별 능력 개선, 학습 속도 높이는 등의 사례
  • 현업에서는 데이터 전체의 분포를 고려해 전처리하는 경우 많음
    ex) 데이터 전체 평균과 표준편차를 써서 데이터들이 0을 중심으로 분포하도록 이동하거나 데이터의 확산범위 제한
  • 백색화 : 전체 데이터를 균일하게 분포

 

3.6.3 배치 처리

# 앞서 구현한 신경망 각 층의 가중치 형상
x, _ = get_data()
network = init_network()
W1, W2, W3 = network['W1'], network['W2'], network['W3']

x.shape
>>> (10000, 784)
W1.shape
>>> (784, 50)
W2.shape
>>> (50, 100)
W3.shape
>>> (100, 10)
  • 다차원 배열에 대응하는 차원의 원소 수가 일치함(편향 생략)
  • 최종 결과로는 원소가 10개인 1차원 배열 y가 출력

 

[그림 3-26]

  • 원소 784개로 구성된 1차원 배열(28x28 2차원 배열)이 입력
  • 마지막에는 원소가 10개인 1차원 배열 출력

 

여러 장의 입력에 대한 처리

ex) 100장의 데이터

[그림 3-27]

  • 입력 데이터의 형상 : 100x784
  • 출력 데이터의 형상 : 100x10 → 100장 분량 입력 데이터의 결과가 한 번에 출력됨
  • ex) x[0]와 y[0]에는 0번째 이미지와 추론 결과, x[1]와 y[1]에는 1번째 이미지와 추론 결과 저장

 

배치(batch) : 하나로 묶은 입력 데이터, 즉 묶음

 

* NOTE : 배치의 이점

  • 배치 처리는 컴퓨터로 계산할 때 큰 이점
  • 이미지 1장당 처리 시간 대폭 절약
    이유 1) 수치 계산 라이브러리 대부분이 큰 배열을 효율적으로 처리할 수 있도록 고도로 최적화 되어 있음
    이유 2) 커다란 신경망에서 데이터 전송이 병목으로 작용하는 경우. 배치 처리로 버스에 주는 부하 줄임
                (느린 I/O를 통해 데이터 읽는 횟수 줄어, 빠른 CPU나 GPU로 순수 계산 수행 비율 높아짐)
  • 즉, 배치 처리를 통해 큰 배열로 이뤄진 계산
          → 컴퓨터에서는 큰 배열을 한꺼번에 계산하는 것이 작은 배열 여러 번 계산보다 빠름
x, t = get_data()
network = init_network()

batch_size = 100    # 배치 크기
accuracy_cnt = 0

for i in range(0, len(x), batch_size):
    x_batch = x[i:i+batch_size]
    y_batch = predict(network, x_batch)
    p = np.argmax(y_batch, axis=1)
    accuracy_cnt += np.sum(p == t[i:i+batch_size])
    
print("Accuracy:" + str(float(accuracy_cnt) / len(x)))
  • range(start, end) : 인수 2개 지정하여 호출하면, start에서 end-1 정수 리스트 반환
  • range(start, end, step) : step 간격으로 증가하는 리스트 반환
  • x[i:i+batch_size] : 입력 데이터의 i번째부터 i+batch_size번째까지의 데이터 묶음
  • argmax() : 최댓값의 인덱스   
    # axis=1 인수 100x10 배열 중 1번째 차원을 구성하는 각 원소에서 최댓값의 인덱스 찾도록
    ex)
x = np.array([[0.1, 0.8, 0.1], [0.3, 0.1, 0.6], [0.2, 0.5, 0.3], [0.8, 0.1, 0.1]])
y = np.argmax(x, axis=1)
print(y)
>>> [1  2  1  0]

 

  • 마지막으로 배치 단위로 분류한 결과를 실제 답과 비교
  • ==연산자로 넘파이 배열끼리 비교하여 True/False로 구성된 bool 배열 생성
  • 배열에서 True 몇 개인지 개수
y = np.array([1, 2, 1, 0])
t = np.array([1, 2, 0, 0])
print(y==t)
>>> [True  True  False  True]
np.sum(y==t)
>>> 3

 

3.7 정리

  • 신경망에서는 활성화 함수로 시그모이드 함수와 ReLU 함수 같은 매끄럽게 변화하는 함수를 이용한다.
  • 넘파이의 다차원 배열을 잘 사용하면 신경망을 효율적으로 구현할 수 있다.
  • 기계학습 문제는 크게 회귀와 분류로 나눌 수 있다.
  • 출력층의 활성화 함수로는 회귀에서는 주로 항등 함수를, 분류에서는 주로 소프트맥스 함수를 이용한다.
  • 분류에서는 출력층의 뉴런 수를 분류하려는 클래스 수와 같게 설정한다.
  • 입력 데이터를 묶은 것을 배치라 하며, 추론 처리를 이 배치 단위로 진행하면 결과를 훨씬 빠르게 얻을 수 있다.