신경망
복습
좋은 소식
- 퍼셉트론으로 아주 복잡한 함수도 표현할 수 있다 (이론적으로 NAND 만 가지고도 컴퓨터 구현 가능)
나쁜 소식
- 가중치를 설정하는 작업은 사람이 손으로 하여야 함
이번 장에서 볼 내용
- 신경망의 개요
- 신경망이 입력을 식별하는 처리 과정 파악
퍼셉트론에서 신경망으로
![]()
- 가장 왼쪽 줄을 입력층
- 맨 오른쪽 줄을 출력층
- 중간 줄을 은닉층이라고 함
이하 0 / 1 / 2 층이라고 표현
퍼셉트론 복습

y = 0 (b + w1x1 + w2x2 <= 0)
y = 1 (b + w1x1 + w2x2 > 0)여기서 b는 편향을 타나내는 매개변수로, 뉴런이 얼마나 쉽게 활성화되는가를 제어함.
식을 더 간결한 형태로 변환하면
y = h(b + w1x1 + w2x2)
h(x) = 0 (x<= 0)
= 1 (x > 0)로 변환할 수 있음.
활성화 함수의 등장
- 위의 식에서 h(x)를 활성화 함수라고 함
- 입력 신호의 총합이 활성화를 일으키는지 정하는 역할을 한다.
- 위의 식을 다시 정리하면
a = b + w1x1 + w2x2
y = h(a)활성화 함수
- 활성화 함수는 임계값을 경계로 출력이 변경됨
- 이를 계단 함수라 함
- 활성화 함수를 계단 함수 이외의 다른 함수로 변경하는 것이 신경망의 조건
시그모이드 함수
- 신경망에서는 활성화 함수로 시그모이드 함수를 자주 사용함
- 시그모이드 함수를 사용하는 이유
- 미분불가능
- 머신러닝은 표본데이터를 가지고 Cost Function을 만들어서 원하는 예측을 해냄
- Cost Function > Classfication 하였을 때 생성되는 함수
- 이를 구분하기 위해 점진 하강 사용 > 한 지점에서 미분을 통해 x를 구하고, 그 x를 가지고 다시 c(x)를 찾아내 기울기가 0이 되는 지점까지 가는 것
- 위의 방법을 사용하려면 모든 점에서 미분 가능해야 한다. > 시그모이드 함수를 사용하는 이유
계단 함수 구현하기

- 0을 경계로 출력이 0에서 1로 변환됨
시그모이드 함수 구현하기

시그모이드 함수와 계단 함수 비교
- 시그모이드 함수는 부드러운 곡선. 입력에 따라 출력이 연속적으로 변화함
- 계단 함수는 0을 경계로 출력이 갑자기 변경
- 입력이 크던 작던, 출력은 0 < y < 1 이라는 공통점이 있음
비선형 함수
- 시그모이드와 계단 함수는 모두 비선형 함수이다.
- 출력이 입력의 상수배인 함수를 선형 함수라고 한다. (y = ax + b)
- 비선형 함수는 직선이 아닌 함수다. (직선 한 개로는 그릴 수 없다)
- 신경망에서는 활성화 함수로는 비선형 함수를 사용해야 함
- 선형 함수는 층을 깊게 하여도, 은닉층이 없는 네트워크로 같은 기능을 할 수 있다.
- h(x) = cx를 활성화 함수로 사용한 3층 네트워크
- h(h(h(x))) = c _ c _ c _ x = a _ x
- 층을 깊게 해도 혜택을 얻을 수 없다.
ReLU 함수
- 0 을 넘으면 입력을 그대로 출력, 0 이하면 0을 출력한다.
- ReLU를 사용하는 이유
backpropagation에서 결과를 전달할 때 sigmoid를 사용한다. 그런데, sigmoid는 전달된 값을 0과 1 사이로 심하게 변형을 한다. 일정 비율로 줄어들기 때문에 왜곡은 아니지만, 값이 현저하게 작아지는 현상이 벌어진다. 3개의 layer를 거치면서 계속해서 1/10로 줄어들었다면, 현재 사용 중인 값은 처음 값의 1/1000이 된다. 이렇게 작아진 값을 갖고 원래의 영향력을 그대로 복원한다는 것은 불가능하다. 이 문제를 vanishing gradient라고 부르고 1986년부터 2006년까지 아무도 해결하지 못했다. Neural Network에 있어 두 번째로 찾아온 위기였다. backpropagation에서 전달하는 값은 똑같아 보이지만, layer를 지날 때마다 최초 값보다 현저하게 작아지기 때문에 값을 전달해도 의미를 가질 수가 없었다. hinton 교수님은 sigmoid 함수 대신 ReLU 함수를 제안했다. ReLU 함수는 그림에 있는 것처럼 0보다 작을 때는 0을 사용하고, 0보다 큰 값에 대해서는 해당 값을 그대로 사용하는 방법이다. 음수에 대해서는 값이 바뀌지만, 양수에 대해서는 값을 바꾸지 않는다.
다차원 배열의 계산
- 행렬의 곱을 내적이라 함
- 행렬을 곱할 때는 첫번째 행렬의 렬과, 두번째 행렬의 행이 같아야 함 (차원이 일치해야 한다)
- (2 x 3) * (3 x 2) => (2 x 2)
- (3 x 3) * (4 x 2) => 곱할 수 없다
신경망의 내적
- 신경망이 가중치만 있다고 가정할 때
- 신경망의 내적을 이용하여 값을 편하게 계산할 수 있음
import numpy as np
X = np.array([1,2])
W = np.array([[1,3,5],[2,4,6]])
Y = np.dot(X, W) # array([ 5, 11, 17])- 1차원 백터는 shape 한 값에 따라 앞뒤로 올 때 곱의 값이 달라진다.
- 위의 식에서 np.dot(X, W) 는 (1,2) * (2,3)이므로 (1,3) 형태의 배열이 리턴됨
- 반대로 np.dot(W, X)를 하면 (2,3) * (2, 1) 이여서 차원이 맞지 않아 곱을 할 수 없다.
- ValueError: shapes (2,3) and (2,) not aligned: 3 (dim 1) != 2 (dim 0)
- 위의 에러 메시지 발생
3층 신경망 구성하기
- 3층 신경망에서 구행하는 입력부터 출력까지의 처리를 구현할 예정
- 넘파이의 다차원 배열 사용
각 층의 신호 전달 구현하기

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- 1층에서는 활성화 함수로 시그모이드 함수를 사용하기로 함
Z1 = sigmoid(A1)
print(A1) # [0.3, 0.7, 1.1]
print(Z1) # [ 0.57444252 0.66818777 0.75026011]- 1층에서 2층으로 가는 과정
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)- 2층에서 출력층으로 신호 전달
- 활성화 함수만 은닉층과 다름
def identify_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 = identify_function(A3)
print(Y) # [ 0.57444252 0.66818777 0.75026011]출력층 설계하기
- 신경망은 분류와 회귀 모두에서 사용 가능
- 어떤 문제를 푸느냐에 따라 출력층에서 사용하는 활성화 함수가 달라짐
- 일반적으로 회귀에는 항등 함수를, 분류에는 소프트맥스 함수를 사용
- 소프트맥스 함수를 이용하면 그 클래스에 속할 확률을 구할 수 있기 때문이다.
- 분류 : 데이터가 어느 클래스에 속하는가? (사진 속 인물의 성별 구분, 고양이와 강아지 구분…)
- 회귀 : 입력 데이터에서 연속적인 수치를 예측(사진 속 인물의 몸무게는?)
항등 함수와 소프트맥스 함수 구현하기
- 항등 함수는 입력을 그대로 출력한다.
- 소프트맥스 함수 구현은 다음과 같다.
def softmax(a):
exp_a = np.exp(a)
sum_exp_a = np.sum(exp_a)
y = exp_a / sum_exp_a
return y # 배열이 리턴됨소프트맥스 함수 구현시 주의점
- e의 제곱이 값이 너무 빨리 커지기 때문에 오버플로우가 발생할 수 있음
- 입력값에서 일정한 값을 + / - 하여 오버플로우가 나지 않게 값 조절 가능
a = np.array([1010, 1000, 990])
np.exp(a) / np.sum(np.exp(a)) # 오버플로우 에러 발생
c = np.max(a)
a - c # [0, -10, -20]
np.exp(a - c) / np.sum(np.exp(a - 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 # 배열이 리턴됨소프트맥스 함수의 특징
- 출력이 0 ~ 1.0 사이의 실수
- 출력의 총합이 1
- 이 두 가지 특징 때문에, 소프트맥스함수의 출력을 확률로 계산 가능
- 또한 단조함수(a <= b => f(a) <= f(b)) 이기 떄문에, 함수를 적용한 후에도 값의 대소가 변하지 않는다.
- 따라서 추론 단계에서는 소프트맥스 함수를 사용하지 않아도 무방(학습 단계에서는 사용하여야 함)
출력층의 뉴런 수 정하기
- 분류하고 싶은 클래스의 수로 설정하는 것이 일반적
- 입력 이미지를 숫자 0 ~ 9 중 하나로 분류한다면, 출력층의 뉴런을 10개로 정하면 된다.
손글씨 숫자 인식
MNIST 데이터셋
- 데이터는 MNIST 데이터셋 사용(https://github.com/WegraLee/deep-learning-from-scratch)
- 추론을 구성하는 신경망 구현할 차례
- 입력층 뉴런을 784개, 출력층 뉴런은 10개로 구성
- 입력층이 28 * 28 = 784, 출력층은 0~ 9 이므로 10개
- 은닉층은 두 개로, 첫번째 은닉층에는 50개 두번째 은닉층에는 100개의 뉴런을 배치할 것임(임의의 숫자)
- load_mnist 는 세 가지 파라미터가 있음
- normalize :
- 입력 이미지의 픽셀 값을 0 ~ 1로 정규화한다.
- False : 0 ~ 255 사이 값 유지한다.
- flatten :
- 입력 이미지를 784개의 원소를 지닌 1차원 배열로 만든다.
- False : 입력 이미지를 1 _ 28 _ 28 의 3차원 배열로 설정한다.
- one_hot_label
- 데이터를 원-핫-인코딩 형태로 저장한다. (정답을 뜻하는 원소만 1(hot). 나머지는 모두 0)
- False : ‘7’이나 ‘2’와 같이 숫자 형태의 레이블을 저장한다.
- normalize :
신경망의 추론 처리
def get_data() :
(x_train, t_train), (x_test, t_test) = load_mnist(flatten=True, normalize=True, one_hot_label=False)
return x_test, t_test
def init_network(): # 학습된 가중치 매개변수를 읽는 함수. 가중치와 편향 매개변수가 딕셔너리 변수로 저장되어 있다.
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 yx, t = get_data() # 데이터셋을 얻는다. (mnist 데이터셋을 읽어온다)
network = init_network() # 네트워크를 형성한다. (학습된 가중치 매개변수를 읽는다)
accuracy_cnt = 0
for i in range(len(x)):
y = predict(network, x[i]) # 예측한다.
p = np.argmax(y) # 가장 확률이 높은 인덱스를 얻는다. (softmax 로 리턴하기 때문에 확률 파악 가능)
if p == t[i]: # 예측 성공
accuracy_cnt += 1
print("Accuracy:" + str(float(accuracy_cnt) / len(x)))코드를 실행시켜 보면 결과는 다음과 같다.
Downloading train-images-idx3-ubyte.gz ...
Done
Downloading train-labels-idx1-ubyte.gz ...
Done
Downloading t10k-images-idx3-ubyte.gz ...
Done
Downloading t10k-labels-idx1-ubyte.gz ...
Done
Converting train-images-idx3-ubyte.gz to NumPy Array ...
Done
Converting train-labels-idx1-ubyte.gz to NumPy Array ...
Done
Converting t10k-images-idx3-ubyte.gz to NumPy Array ...
Done
Converting t10k-labels-idx1-ubyte.gz to NumPy Array ...
Done
Creating pickle file ...
Done!
Accuracy:0.9352- 지속적으로 정확도를 향상시켜 나갈 예정
- 이 예제에서는 normalize를 True로 설정했는데, 이렇게 하면 데이터를 0-1 범위로 변환하는 전처리 수행하게 됨. 이런 과정을 정규화라고 함
- 전처리를 하여 식별 능력을 개선하는 사례는 많이 제시되고 있음
- 현업에서는 데이터의 분포를 고려해 전처리하는 경우가 많다.
- 평균과 표준편차를 이용하여 데이터들이 0을 중심으로 분포하도록 이동시킨다던지…
배치 처리
- 이미지 여러 장을 한꺼번에 입력하는 경우를 고려해 보자
- 예를 들면 100장을 한꺼번에 입력했다던지?
X
W1
W2
W3
Y
input 1
784
784 * 50
50 * 100
100 * 10
10
input 100
100 * 784
784 * 50
50 * 100
100 * 10
100 * 10
- 100장 입력 데이터가 한 번에 출력됨
- x[0], y[0] 에는 0번째 이미지와 추론 결과
- x[1], y[1] 에는 1번째 이미지와 추론 결과
- 배치로 한꺼번에 처리하면 효율성이 올라감
- 이미지 한장당 처리 시간을 대폭 줄여준다.
- 디스크 I/O가 줄면서 데이터 병목되는 지점이 줄어든다.
배치가 적용된 코드는 다음과 같다.
x, t = get_data()
network = init_network()
batch_size = 100 # 배치 크기
accuracy_cnt = 0
for i in range(0, len(x), batch_size): # 0에서 len(x)까지 batch_size 의 인덱스 배열 반환
x_batch = x[i:i+batch_size] # 반환받은 곳부터 batch_size 만큼 자름
y_batch = predict(network, x_batch) # 예측한다.
p = np.argmax(y_batch, axis=1) # 각각 배열에서 최대값의 인덱스 리턴
accuracy_cnt += np.sum(p == t[t:t+batch_size]) # boolean 배열 생성하여 True 만 카운트. t는 get_data()에서 가져온 정확한 데이터 수치
print("Accuracy:" + str(float(accuracy_cnt) / len(x)))정리
- 신경망은 각 층의 뉴런이 다음 층의 뉴런으로 신호를 전달. 퍼셉트론과 다른 점은 활성화 함수가 다름.