[AI 기초] 3. 신경망
퍼셉트론 관련해서는 좋은 소식과 나쁜 소식
- good : 퍼셉트론으로 복잡한 함수도 표현할 수 있다는 것
- bad : 가중치를 설정하는 작업(원하는 결과를 출력하도록 가중치 값을 적절히 정하는 작업)은 여전히 사람이 수동
신경망은 이 나쁜 소식을 해결
가중치 매개변수의 적절한 값을 데이터로부터 자동으로 학습
3.1 퍼셉트론에서 신경망으로
3.1.1 신경망의 예
- 은닉층 뉴런 : (입력층, 출력층과 달리) 사람 눈에 보이지 않음. '은닉'
* 구현 편의성을 위해 순서대로 0, 1, 2층으로 명명
* 신경망은 총 3층이지만, 가중치 갖는 층이 2개 이므로 '2층 신경망'
신경망은 퍼셉트론과 달리 신호를 어떻게 전달할까?
3.1.2 퍼셉트론 복습
x1과 x2라는 두 신호를 입력받아 y 출력
b : 편향 매개변수, 뉴런이 얼마나 쉽게 활성화되느냐 제어
w1, w2 : 각 신호 가중치 매개변수, 각 신호의 영향력 제어
- 가중치 b, 입력 1인 뉴런 추가
- 퍼셉트론의 동작은 x1, x2, 1이라는 3개의 신호가 뉴런에 입력 - 각 신호에 가중치 곱한 후, 다음 뉴런 전달
- 합이 0을 넘으면 1 출력, 그렇지 않으면 0 출력
- [식 3.1], [3.2], [3.3]이 하는 일은 동일
3.1.3 활성화 함수의 등장
활성화 함수(activation function)
- 입력 신호의 총합을 출력 신호로 변환하는 함수
- '활성화' 입력 신호의 총합이 활성화를 일으키는지 정하는 역할
[식 3.2] 두 단계
- 가중치가 곱해진 입력 신호의 총합 계산
- 활성화 함수에 입력해 결과
이를 다음과 같은 2개의 식으로 나눌 수 있음
지금까지와 같이 뉴런을 큰 원으로 그리면 [그림 3-4]처럼 나타낼 수 있음
- 가중치 신호를 조합한 결과가 a라는 노드가 되고, 활성화 함수 h( )를 통과하여 y라는 노드로 변환되는 과정
* 뉴런 = 노드
* 보통 뉴런을 하나의 원으로 그리나, 신경망 동작을 더 명확히 드러낼 땐 활성화 처리 과정 명시
* WARNING
- 단순 퍼셉트론 : 단층 네트워크에서 계단 함수(임계값을 경계로 출력이 바뀌는 함수)를 활성화 함수로 사용한 모델
- 다층 퍼셉트론 : 신경망(여러 층 구성, 시그모이드 함수 등 매끈한 활성화 함수 사용)
3.2 활성화 함수
계단 함수(step function) : [식 3.3] 활성화 함수는 임계값 경계로 출력 바뀜
- '퍼셉트론에서는 활성화 함수로 계단 함수 이용' (임곗값 이하, 초과 기준)
활성화 함수를 다른 함수로 바꿔보자!
3.2.1 시그모이드 함수
- 얼핏 복잡해 보이지만, 단순한 '함수'
* 함수 : 입력을 주면 출력을 돌려주는 변환기 - 퍼셉트론과 신경망의 주된 차이 : 활성화 함수
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.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()
* 시그모이드 : 'S자 모양'이라는 뜻
3.2.5 시그모이드 함수와 계단 함수 비교
차이점
계단 함수 | 시그모이드 함수 |
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을 출력
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
2차원 배열은 특히 행렬(matrix)
- 가로는 행(row) 세로는 열(column)
3.3.2 행렬의 곱
- 왼쪽 행렬의 행(가로) 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의 행 수가 같아야 함. 그렇지 않으면 에러 발생.
- 행렬 A와 B의 대응하는 차원의 원소 수가 같아야 함
- 계산 결과인 행렬 C의 형상은 행렬 A의 행 수와 행렬 B의 열 수가 됨
# 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 신경망에서의 행렬 곱
넘파이 행렬을 이용한 신경망 구현
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층 신경망에서 수행되는, 입력부터 출력까지의 처리(순방향 처리) 구현
* 넘파이 배열을 잘 사용하면 아주 적은 코드만으로 신경망의 순방향 처리를 완성
3.4.1 표기법 설명
* WARNING : 이 절의 핵심은 신경망에서의 계산을 행렬 계산으로 정리 가능하다는 것
3.4.2 각 층의 신호 전달 구현하기
- '1층의 첫 번째 뉴런'으로 가는 신호를 살펴보자
- 편향을 뜻하는 뉴런 '1'이 추가됨 # 편향은 오른쪽 아래 인덱스 하나뿐 (앞 층의 편향 뉴런이 하나라서)
- a1(1)은 가중치를 곱한 신호 두 개와 편향을 합하여 계산
- 행렬의 곱을 이용하면 1층의 '가중치 부분'을 다음 식처럼 간소화할 수 있음
위 식에서 각각의 행렬은 다음과 같음
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층의 활성화 함수에서의 처리를 살펴보자.
- a : 은닉층에서의 가중치 합(가중 신호와 편향의 총합)
- z : 활성화 함수 h()로 변환된 신호
* 활성화 함수로 시그모이드 함수 사용 가정
Z1 = sigmoid(A1) # 앞서 정의한 함수, 넘파이 배열 받아 같은 수의 원소로 구성된 넘파이 배열 반환
print(A1) # [0.3, 0.7, 1.1]
print(Z1) # [0. 57444252, 0.66818777, 0.75026011]
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층에서 출력층으로의 신호 전달
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) : 입력 그대로 출력, '입력 = 출력'의 항등
- 출력층에서 항등 함수를 사용하면 입력 신호가 그대로 출력 신호 됨.
소프트맥스 함수(softmax function) : 분자는 입력 신호의 지수 함수, 분모는 모든 입력 신호의 지수 함수 합으로 구성
- 분류에 사용
- 소프트맥스의 출력은 모든 입력 신호로부터 화살표 받음.
([식 3.10]의 분모에서 보듯, 출력층 각 뉴런이 모든 입력 신호에서 영상 받음)
# 소프트맥스 함수 구현
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]이 말하는 것은 소프트맥스의 지수 함수를 계산할 때 어떤 정수를 더해도(빼도) 결과는 바뀌지 않는다는 것
- 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 값
실제 의미하는 숫자가 레이블로 붙어있음
# 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)
- 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
- MNIST 데이터셋을 얻고 네트워크 생성
- for 문을 돌며 x에 저장된 이미지 데이터를 1장씩 꺼내 predict( ) 함수로 분류
- predict( ) 함수는 각 레이블의 확률을 넘파이 배열로 반환
ex) [0.1, 0.3, 0.2, ..., 0.04]와 같은 배열 반환, 숫자 0일 확률 0.1, ... 식으로 해석 - np.argmax() 함수로 배열에서 값이 가장 큰 원소의 인덱스 구함 → 예측 결과
- 신경망이 예측한 답변과 정답 레이블을 비교하여 맞힌 숫자(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가 출력
- 원소 784개로 구성된 1차원 배열(28x28 2차원 배열)이 입력
- 마지막에는 원소가 10개인 1차원 배열 출력
여러 장의 입력에 대한 처리
ex) 100장의 데이터
- 입력 데이터의 형상 : 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 함수 같은 매끄럽게 변화하는 함수를 이용한다.
- 넘파이의 다차원 배열을 잘 사용하면 신경망을 효율적으로 구현할 수 있다.
- 기계학습 문제는 크게 회귀와 분류로 나눌 수 있다.
- 출력층의 활성화 함수로는 회귀에서는 주로 항등 함수를, 분류에서는 주로 소프트맥스 함수를 이용한다.
- 분류에서는 출력층의 뉴런 수를 분류하려는 클래스 수와 같게 설정한다.
- 입력 데이터를 묶은 것을 배치라 하며, 추론 처리를 이 배치 단위로 진행하면 결과를 훨씬 빠르게 얻을 수 있다.