퍼셉트론의 한계점은 가중치를 설정하는 작업 (원하는 결과를 출력하도록 가중치 값을 적절히 정하는 작업)은 여전히 사람이 수동으로 한다는 것
신경망은 위 문제를 해결해준다.
자동으로 학습하는 능력이 있다.
💡 이 책에서는 입력층에서부터 출력층 방향으로 차례로 0층, 1층, 2층이라고 함.
- 그림에서 0층이 입력층, 1층이 은닉층, 2층이 출력층이 됨.
- 은닉층의 뉴런은 (입력층과 출력층과 달리) 사람 눈에 보이지 않음
- "은닉"
💡 위 그림의 신경망은 모두 3층으로 구성되지만, 가중치를 갖는 층은 2개이기 때문에 '2층 신경망'이라고도 함
- 책에서는 실제로 가중치를 갖는 층의 개수 (입력층, 은닉층, 출력층의 합계에서 1을 뺀 값)을 기준
편향을 명시한 퍼셉트론의 입력 신호를 수식을 하나의 함수로 표현하면 다음과 같다.
결과적으로 위 2개의 식이 하는 일은 동일하다.
입력 신호의 총합을 출력 신호로 변환하는 함수를 일반적으로 활성화 함수이라고 함.
입력 신호의 총합이 활성화를 일으키는지를 정하는 역할을 함.가중치가 곱해진 입력 신호의 총합을 계산하고, 그 합을 활성화 함수에 입력해 결과를 내는 2개의 식을 적으면 다음과 같다.

💡
- 일반적으로 단순 퍼셉트론은 단층 네트워크에서
계단 함수(임계값을 경계로 출력이 바뀌는 함수)를 활성화 함수로 사용한 모델을 가리킴- 다층 퍼셉트론은 신경망(여러 층으로 구성되고 시그모이드 함수 등의 매끈한 활성화 함수를 사용하는 네트워크)를 가리킴
계단 함수
퍼셉트론에서는 활성화 함수로 계단 함수를 이용한다. 라고 할 수 있음활성화 함수를 계단 함수에서 다른 함수로 변경하는 것이 신경망의 세계로 나아가는 열쇠
신경망에서 자주 이용하는 활성화 함수인 시그모이드 함수의 식은 다음과 같다.
exp(-x): 를 의미e: 자연상수로 2.7182..의 값을 갖는 실수시그모이드 함수에 1.0과 2.0을 입력하면 (1.0) = 0.731 ... (2.0) = 0.880..처럼 특정 값으을 출력함
신경망에서는 활성화 함수로 시그모이드 함수를 이용하여 신호를 변환하고, 그 변환된 신호를 다음 뉴런에 전달
주된 차이는 이 활성화 함수 차이def step_function(x):
y = x > 0
# numpy 배열을 인수로 넣을 수 있게 하는 방법
return y.astype(np.int)
# x = np.array([-1.0, 1.0, 2.0])
# y = x > 0
# y를 출력하면 0보다 큰 x값은 True로, 0보다 작거나 같은 값은 False로 나온다.
# boolean값을 int형으로 변환시키면 True는 0, False는 1이다.
import numpy as np
x = np.array([-1.0, 1.0, 2.0])
x
>>> array([-1., 1., 2.])
y = x > 0
y
>>> array([False, True, True], dtype=bool)
y = y.astype(np.int)
y
>>> array([0, 1, 1])
## 계단 함수 그래프
import numpy as np
import matploblib.pylab as plt
def step_function(x):
return np.array(x > 0, dtype=np.int)
x = np.arange(-5.0, 5.0, 0.1)
y = step_function(x)
plt.plot(x, y)
plt.ylim(-0.1, 1.1) # y축의 범위 지정
plt.show()

0에서 1 (또는 1에서 0)로 바뀜def sigmoid(x):
return 1 / (1 = np.exp(-x))
x = np.array([-1.0, 1.0, 2.0])
sigmoid(x)
>>> array([ 0.26894142, 0.73105858, 0.88079708])
'''
넘파이의 브로드캐스트 기능
- 넘파이 배열과 스칼라값의 연산을
넘파이 배열의 원소 각각과 스칼라값의 연산으로 바꿔 수행
'''
시그모이드 함수를 그래프로 그리면 다음과 같다.
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()

차이점
공통점
0에 가깝고 (혹은 0이고), 입력이 커지면 출력이 1에 가까워지는(혹은 1이 되는) 구조0에서 1 사이중요한 공통점으로 두 함수 모두 비선형 함수이다.
💡
활성화 함수를 설명할 때 비선형 함수와 선형 함수라는 용어가 자주 등장
- 선형 함수: 함수에 무언가 입력했을 때 출력이 입력의 상수배만큼 변하는 함수
- , 이때 와 는 상수
- 곧은 1개의 직선이 됨
- 비선형 함수
- '선형이 아닌' 함수
- 직선 1개로는 그릴 수 없는 함수
신경망에서는 활성화 함수로 비선형 함수를 사용해야 함.
신경망에서는 선형 함수를 사용해서는 안된다.
그 이유는 선형 함수를 이용하면 신경망의 층을 깊게 하는 의미가 없어지기 때문
은닉층이 없는 네트워크로도 같은 기능을 할 수 있는 것이 문제선형 함수인 = 를 3층 네트워크로 쌓는다고 가정
- 식으로 나타내면 가 됨
- = 와 같은 식임. ()
- 즉, 은닉층이 없는 네트워크로 표현할 수 있음
- 그렇게 되면, 여러 층으로 구성하는 이점을 살릴 수 없음
- 그래서 층을 쌓는 혜택을 얻고 싶다면 활성화 함수로는 반드시 비선형 함수를 사용해야 함.
ReLU는 입력이 0을 넘으면 그 입력을 그대로 출력하고, 0 이하이면 0을 출력하는 함수

식은 다음과 같다
# relu 구현
def relu(x):
return np.maximum(0, x)
넘파이의 다차원 배열을 사용한 계산법을 숙달하면 신경망을 효율적으로 구현할 수 있다.
'''1차원 배열'''
import numpy as np
A = np.array([1, 2, 3, 4])
np.ndim(A) # 배열의 차원 수
>>> 1
A.shape # 배열의 형상 (1차원이라도 다차원처럼 tuple로 반환-> 통일)
>>> (4,)
A.shape[0]
>>> 4
'''2차원 배열'''
B = np.array([[1,2], [3,4], [5,6]])
np.ndim(B)
>>> 2
B.shape
>>> (3,2)
2차원 배열은 특히 행렬 이라 부르고, 배열의 가로 방향을 행 , 세로 방향을 열이라고 한다.


행렬 곱은 왼쪽 행렬의 행(가로)과 오른쪽 행렬의 열(세로)을 원소별로 곱하고, 그 값들을 더해서 계산
'''위 행렬곱 파이썬 구현'''
import numpy as np
A = np.array([[1,2], [3,4]]) # shape: 2x2 행렬
B = np.array([[5,6], [7,8]]) # shape: 2x2 행렬
np.dot(A, B)
>>> array([[19, 22],
[43, 50]])
np.dot() : 입력이 1차원 배열이면 벡터를, 2차원 배열이면 행렬 곱을 계산한다.
np.dot(A, B)와 np.dot(B, A)는 다른 값이 될 수 있다.

행렬 곱을 할 때는, 행렬 A의 1번째 차원의 원소 수(열 수)와 행렬 B의 0번째 차원의 원소 수 (행 수)가 동일해야 함.

X와 W의 대응하는 차원의 원소 수가 같아야 함.
import numpy as np
X = np.array([1, 2]) #x1, x2
X.shape
>>> (2,)
W = np.array([[1, 3, 5], [2, 4, 6]]) #x1: 1,3,5 | x2:2,4,6
W.shape
>>> (2, 3)
Y = np.dot(X, W)
>>> [5 11 17]
행렬 곱으로 한꺼번에 계산해주는 기능은 신경망을 구현할 때 매우 중요하다.

입력층의 뉴런 에서 다음 층의 뉴런 으로 향하는 선 위에 가중치를 표시하고 있음
가중치와 은닉 층 뉴런의 오른쪽 위의 의 의미는 1층의 가중치, 1층의 뉴런임을 뜻함.
입력층에서 1층으로 신호 전달

은 가중치를 곱한 신호 두 개와 편향을 합해서 다음과 같이 계산한다.
여기에서 행렬의 곱을 이용하면 1층의 '가중치 부분'을 간소화 가능
이때 행렬 은 각각 다음과 같다.
은닉층에서의 가중치 합(가중 신호와 편향의 총합)을 로 표기하고 활성화 함수 로 변환된 신호를 로 표현한다.
## 입력층에서 1층으로의 신호 전달 ##
import numpy as np
X = np.array([1.0], [0.5]) # shape: (2,)
W1 = np.array([[0.1, 0.3, 0.5], [0.2, 0.4, 0.6]]) # shape: (3,2)
B1 = np.array([0.1, 0.2, 0.3]) # shape: (3,)
A1 = np.dot(X, W1) + B1 # shape: (1,3)
Z1 = sigmoid(A1) # shape: (3,)
A1
>>> [0.3, 0.7, 1.1]
'''넘파이 배열을 받아 같은 수의 원소로 구성된 넘파이 배열 반환'''
Z1
>>> [0.57444252, 0.66818777, 0.75026011]
1층에서 2층으로의 신호 전달

## 1층에서 2층으로의 신호 전달 ##
W2 = np.array([[0.1, 0.4], [0.2, 0.5], [0.3, 0.6]]) #shape: (3,2)
B2 = np.array([0.1, 0.2]) #shape: (2,)
A2 = np.dot(Z1, W2) + B2 #shape: (1,2)
Z2 = sigmoid(A2) #shape: (1,2)
2층에서 출력층으로의 신호 전달

출력층의 활성화 함수로 항등 함수인 identity_function()을 정의
출력층의 활성화 함수를 로 표시하여 은닉층의 활성화 함수 와는 다름을 명시
def identity_function(x):
return x
W3 = np.array([[0.1, 0.3],[0.2, 0.4]]) #shape: (2,2)
B3 = np.array([0.1, 0.2]) #shape: (2,) 1x2
A3 = np.dot(Z2, W3) + B3 #shape: (1,2)
Y = identity_function(A3) # 혹은 Y = A3
💡
출력층의 활성화 함수는 풀고자 하는 문제의 성질에 맞게 정함
- Regression: 항등함수
- Binary Classfication: 시그모이드
- Multi class Classification: 소프트맥스
- Multi label Classification: 시그모이드
def init_network():
network = {}
# 1층으로 가는 가중치 + 편향
network['W1'] = np.array([[0.1, 0.3, 0.5], [0.2, 0.4, 0.6]]) # 2x3
network['b1'] = np.array([0.1, 0.2, 0.3]) # 1x3
# 2층으로 가는 가중치 + 편향
network['W2'] = np.array([[0.1, 0.4], [0.2, 0.5], [0.3, 0.6]]) # 3x2
network['b2'] = np.array([0.1, 0.2]) # 1x2
# 3층으로 가는 가중치 + 편향
network['W3'] = np.array([[0.1, 0.3], [0.2, 0.4]]) # 2x2
network['b3'] = np.array([0.1, 0.2]) # 1x2
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]) #1*2
y = forward(network, x)
print(y)
>>> [0.31682708 0.69627909]
기계학습 문제는 분류(classification)와 회귀(regression)로 나뉜다.
활성화 함수가 달라진다.일반적으로 회귀에는 항등함수를 분류에는 소프트맥스 함수를 출력층의 활성화 함수로 사용한다.
항등 함수은 입력을 그대로 출력한다.

소프트맥스 함수의 식은 다음과 같다.
exp(x) : 를 뜻하는 지수 함수 (는 자연상수)n : 출력층의 뉴런 수 y_k : 그중 번째 출력소프트맥스 함수의 분자는 입력 신호의 지수함수, 분모는 모든 입력 신호의 지수 함수의 합으로 구성된다.

def softmax(a):
exp_a = np.exp(a)
sum_exp_a = np.sum(exp_a)
y = exp_a / sum_exp_a
return y
지수함수를 사용하는 소프트맥스 함수는 오버플로의 문제가 발생해 수치가 '불안정'해질 수 있는 문제점이 있다.
오버플로(overflow) : 표현할 수 있는 수의 범위가 한정되어 너무 큰 값은 표현할 수 없다.
소프트맥스 함수 구현 개선

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
소프트맥스 함수의 출력은 0에서 1.0사이의 실수이다.
1확률로 해석할 수 있음소프트맥스 함수를 적용해도 각 원소의 대소 관계는 불변함
결과적으로 신경망으로 뷴류할 때는 출력층의 소프트맥스 함수를 생략해도 된다.
출력층의 뉴런 수는 풀려는 문제에 맞게 적절히 정해야 함
분류에서는 분류하고 싶은 클래스 수로 설정하는 것이 일반적💡
기계학습과 마찬가지로 신경망도 두 단계를 거쳐서 문제를 해결함
- 훈련 데이터(학습 데이터)를 사용해 가중치 매개변수를 학습
- 추론 단계에서는 앞서 학습한 매개변수를 사용하여 입력 데이터를 분류
신경망의 순전파(forward propagation): 이미 학습된 매개변수를 사용하여 입력 데이터를 분류하는 추론 과정
Train: 60,000 / Test: 10,00028 x 28 크기의 회색조 이미지(1채널)0에서 255까지의 값을 취함import sys, os
#sys.path.append(os.pardir)
#from dataset.mnist import load_mnist
from mnist import load_mnist #mnist.py만을 다운로드 받았다면
(x_train, t_train), (x_test, t_test) = load_mnist(flatten=True, normalize=False)
'''
normalize: 입력 이미지의 픽셀값을 0.0~1.0 사이의 값으로 정규화할지 정함
- 이처럼 데이터를 특정 범위로 변환하는 처리를 정규화라고 함.
flatten: 입력 이미지를 1차원 배열로 만들지를 정함
- (False: 1*28*28 3차원 배열로 True: 784개의 원소로 이루어진 1차원 배열로)
one-hot-label: 원-핫 인코딩 형태로 저장할지를 정함
- (False: 숫자 형태의 레이블을 저장 True: 레이블을 원-핫 인코딩하여 저장)
'''
# 각 데이터의 형상
'''
1x28x28 3차원 배열의 픽셀의 한 이미지가 flatten된 경우,
784개의 픽셀이 각 컬럼이 됨. (1차원 배열)
'''
print(x_train.shape) # (60000, 784)
print(t_train.shape) # (60000,)
print(x_test.shape) # (10000, 784)
print(t_test.shape) # (10000,)
시각화
import sys, os
sys.path.append(os.pardir)
from dataset.mnist import load_mnist
import numpy as np
from PIL import Image
def img_show(img):
pil_img = Image.fromarray(np.uint8(img))
#Image.fromarray: 넘파이로 저장된 이미지 데이터를 PIL용 데이터 객체로 변환해야
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)
print(img.shape) # (784,)
img = img.reshape(28, 28) # 원래 이미지의 모양으로 변형
#flatten=True는 1차원 넘파이 배열로 저장되어 있다
#이미지를 표시할 때 원래 형상인 28*28크기로 다시 변형
print(img.shape)
img_show(img)
784개, 출력층 뉴런을 10개로 구성784개인 이유는 이미지 크기가 28x28=78410개인 이유는 이 문제가 0에서 9까지의 숫자를 구분하기 문제이기 때문import numpy as np
import pickle
from dataset.mnist import load_mnist
from common.functions import sigmoid, softmax
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_network():
#sample_weight.pkl파일에 저장된 학습된 가중치 매개변수를 읽는다
#가중치와 편향 매개변수가 딕셔너리 변수로 저장되어 있다.
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
#신경망의 정확도(분류가 얼마나 올바른가)를 평가
x, t = get_data()
network = init_network()
accuracy_cnt = 0
for i in range(len(x)):
y = predict(network, x[i]) #각 레이블의 확률을 넘파이 배열로 반환 e.g. [0.1, 0.2, ... ,0.04]
p = np.argmax(y) #배열에서 값이 가장 큰(확률이 가장 높은) 원소의 인덱스를 구함
if p == t[i]:
accuracy_cnt += 1
print("Accuracy: "+str(float(accuracy_cnt) / len(x)))
하나로 묶은 입력 데이터를 배치라고 한다.
(이미지 100개를 묶어 predict() 함수에 한 번에 넘긴 경우)

1층과 2층에 뉴런 수가 각각 50, 100인 은닉층이 있음.x[0]와 y[0]에는 0번째 이미지와 그 추론 결과가, x[1]과 y[1]에는 1번째의 이미지와 그 결과가 저장된다.💡
배치 처리의 장점
1. 이미지 1장당 처리 시간을 대폭 줄여준다.
- 수치 계산 라이브러리 대부분이 큰 배열을 효율적으로 처리할 수 있도록 고도로 최적화되어 있기 때문
- 커다란 신경망에서는 데이터 전송이 병목으로 작용하는 경우가 자주 있는데, 배치 처리를 함으로써 버스에 주는 부하를 줄임
- 느린 I/O를 통해 데이터를 읽는 횟수가 줄어, 빠른 CPU나 GPU로 순수 계산을 수행하는 비율이 높아짐.
# Batch 처리
# 위 코드에서 달라진 부분
x, t = get_data()
network = init_network()
batch_size = 100 #배치 크기
accuracy_cnt = 0
#range: 0부터 len(x)까지 batch_size간격으로 증가하는 리스트 반환
for i in range(0, len(x), batch_size):
x_batch = x[i:i+batch_size] #0~100, 100~200, ...
y_batch = predict(network, x_batch) #각 레이블의 확률을 넘파이 배열로 반환
p = np.argmax(y_batch, axis=1) #배열에서 값이 가장 큰(확률이 가장 높은) 원소의 인덱스를 구함
'''
"axis=1": 1번째 차원을 구성하는 각 원소에서 최댓값의 인덱스를 찾도록
x = np.array([[0.1, 0.8, 0.1], [0.3, 0.1, 0.6]])
y = np.argmax(x, axis=1)
>>> [1 2]
'''
accuracy_cnt += np.sum(p == t[i:i+batch_size])
print(f"Accuracy: {str(float(accuracy_cnt) / len(x)))}"
- 신경망에서는 활성화 함수로 시그모이드 함수와 ReLU 함수 같은 매끄럽게 변화하는 함수를 이용
- 넘파이의 다차원 배열을 잘 사용하면 신경망을 효율적으로 구현할 수 있음
- 기계학습 문제는 크게 회귀와 분류로 나눌 수 있음
- 출력층의 활성화 함수로는 회귀에서는 주로 항등 함수를, 분류에서는 주로 소프트맥스 함수를 이용
- 분류에서는 출력층의 뉴런 수를 분류하려는 클래스 수와 같게 설정
- 입력 데이터를 묶은 것을 배치라 하며, 추론 처리를 이 배치 단위로 진행하면 결과를 훨씬 빠르게 얻을 수 있다.