대학원 2학기가 종강했지만, 기계학습 수업에서의 MLP 과제를 다시 해보기 위해 '신경망 첫걸음(타리트 라시드)'란 책을 읽고 있다. 이 책의 Chapter 20부터는 Python 실습 내용인데 이를 블로그에 정리해보며 내재화하려 한다. 그렇다면 인공 신경망의 뼈대 코드부터 단계별로 코드를 쌓아보자.
뼈대 코드 만들기
신경망은 적어도 다음 세 가지 기능을 가져야 한다. 아래 코드에 살을 붙여 나갈 것이다.
- 초기화: 입력, 은닉, 출력 노드의 수 설정
- 학습: 학습 데이터들을 통해 학습하고 잉 따라 가중치를 업데이트
- 질의: 입력을 받아 연산한 후 출력 노드에서 답을 전달
#신경망 클래스의 정의
class neuralNetwork:
#신경망 초기화하기
def __init__():
pass
#신경망 학습시키기
def train():
pass
#신경망에 질의하기
def query():
pass
신경망 초기화하기
신경망의 형태와 크기를 정의하기 위해 입력 계층의 노드(input_nodes), 은닉 계층의 노드(hidden_nodes), 출력 계층의 노드(output_nodes)의 수를 정해야 한다. 신경망 내 직접 정의하기보다 '매개변수(parameter)'를 사용하면 하나의 클래스로 다양한 규모의 신경망을 사용할 수 있다.
#신경망 초기화하기
def __init__(self, inputnodes, hiddennodes, outputnodes, learningrate):
#set number of nodes in each input, hidden, output layer
self.inodes = inputnodes
self.hnodes = hiddennodes
self.onodes = outputnodes
#학습률
self.lr = learningrate
pass
그리고 0.3의 학습률을 가지는 작은 신경망 객채를 만들어보자.
#입력, 은닉, 출력 노드의 수
input_nodes = 3
hidden_nodes = 3
output_nodes = 3
#학습률은 0.3
learning_rate = 0.3
#인경망의 인스턴스(instance)를 생성
n = neuralNetwork(input_nodes, hidden_nodes, output_nodes, learning_rate)
신경망의 핵심인 가중치
신경망에서 가장 중요한 부분은 연결 노드의 가중치이며 노드와의 연결 노드를 생성해보자. 가중치는 전파(forward propagation)에서는 전달되는 신호를 계산하며, 역전파(back propagation)에서는 오차를 계산하는 데 쓰인다. 가중치는 다음과 같이 행렬로 표현된다.
-
(은닉 노드 X 입력 노드)의 크기를 가지는 입력 계층과 은닉 계층 사이의 가중치의 행렬: W_input_hidden
-
관행적으로 (입력 노드 X 은닉 노드)가 아니라 (은닉 노드 X 입력 노드) 표기
-
-
(출력 노드 X 은닉 노드)의 크기를 가지는 은닉 계층과 출력 계층 사이의 가중치의 행렬: W_hidden_output
가중치는 임의의 작은 값으로 초기화하는데 numpy 라이브러리를 불러온 뒤(import numpy) 후 numpy.random.rand() 함수를 사용해 0과 1사이의 임의의 값을 원소로 가지는 행렬을 생성한다.
numpy.random.rand(rows, columns)
잘 작동하는지 확인하기 위해 3X3 크기의 행렬을 생성해보자.
import numpy
numpy.random.rand(3, 3)
위 결과에서는 0~1사이의 값이나 가중치는 음수일 수 있기 때문에 -0.5 ~ 0.5 사이의 값을 가지도록 0.5씩 빼줌으로써 음수의 값도 포함하도록 변경해보자.
numpy.random.rand(3,3) - 0.5
가중치는 학습과 질의 등 다른 함수들로부터 접근이 가능해야 한다. [신경망 초기화하기]에서 정의한 self.inodes, self.hnodes, self.onodes를 이용해 2개의 가중치 행렬을 생성해보자.
#신경망 초기화하기
def __init__(self, inputnodes, hiddennodes, outputnodes, learningrate):
#set number of nodes in each input, hidden, output layer
self.inodes = inputnodes
self.hnodes = hiddennodes
self.onodes = outputnodes
wih는 입력 계층과 은닉 계층 사이의 가중치의 행렬(W_input_hidden)이고, who는 은닉 계층과 출력 계층 사이의 가중치의 행렬이다.
#가중치 행렬 wih(input_hidden)와 who(hidden_output) 정의
#배열 내 가중치는 w_i_j로 표기. 노드 i에서 다음 계층의 노드 j로 연결됨을 의미
#w11 w21
#w12 w22
self.wih = (numpy.random.rand(self.hnodes, self.inodes)- 0.5)
self.wih = (numpy.random.rand(self.onodes, self.hnodes)- 0.5)
더 정교한 가중치
가중치를 더 정교하게 초기화하기 위해 0을 중심으로 1/√(들어오는 연결 노드의 개수)의 표준편차를 가지는 정규분포(normal)에 따라 가중치의 임의의 값을 구하는 방법이 있다. numpy.random.normal(정규분포의 중심, 표준편차, numpy 행렬) 함수를 활용하면 된다.
-
정규 분포의 중심: 0.0
- 표준편차: pow(self.hnodes, -0.5), pow(self.onodes, -0.5),
-
노드로 들어오는 연결 노드의 개수에 루트를 씌우고 역수를 취한 표준편차
-
-
numpy 행렬: (self.hnodes, self.inodes), (self.onodes, self.hnodes)
self.wih = numpy.random.normal(0.0, pow(self.hnodes, - 0.5),(self.hnodes, self.inodes))
self.wih = numpy.random.normal(0.0, pow(self.onodes, - 0.5),(self.onodes, self.hnodes))
신경망에 질의하기
query() 함수는 신경망으로 들어오는 입력을 받아 출력을 반환하는 작업을 입력 계층, 은닉 계층, 출력 계층를 거쳐 수행한다. 각 노드의 가중치를 연산하고 활성화 함수를 적용하기 위해서는 행렬을 이용하면 되는데, 은닉 계층으로 들어오는 신호를 아래와 같이 표기할 수 있다.
- X_hidden = W_input_hidden ⋅ I
- X_hidden : 은닉 계층으로 들어오는 신호
- W_input_hidden : 입력 계층과 은닉 계층 사이의 가중치 행렬
- I : 입력 행렬
이를 Python으로 구현하기 위해서는 numpy 라이브러리의 dot 함수를 사용한다.
hidden_inputs = numpy.dot(self.wih, inputs)
은닉 계층으로부터 나오는 신호를 구하기 위해 시그모이드 함수를 적용한다.
- O_hidden = sigmoid(X_hidden)
시그모이드 함수는 scipy 라이브러리의 expit() 사용한다.
import scipy.special
scipy.special를 import한 뒤 신경망 초기화 함수(def __init__)에서 아래와 같이 먼저 정의한 뒤 query() 함수를 수정한다. 신경망 클래스의 신경망 에서는 lamda라는 익명함수를 사용해 x를 매개변수로 전달 받아 scipy.special.expit(x)를 반환한다.
self.activation_function = lambda x: scipy.special.expit(x)
class neuralNetwork:
def __init__(self, inputnodes, hiddennodes, outputnodes, learningrate):
self.inodes = inputnodes
self.hnodes = hiddennodes
self.onodes = outputnodes
self.wih = numpy.random.normal(0.0, pow(self.hnodes, - 0.5),(self.hnodes, self.inodes))
self.wih = numpy.random.normal(0.0, pow(self.onodes, - 0.5),(self.onodes, self.hnodes))
self.lr = learningrate
self.activation_function = lambda x: scipy.special.expit(x)
위와 같이 초기화함으로써 이후 활성화 함수 적용이 필요할 땐 self.activation_function()을 호출하면 된다. query() 함수에서 은닉 노드로 들어오는 신호에 아래와 같이 활성화 함수를 적용하면 은닉 계층으로부터 나가는 신호를 정의 할 수 있다.
#은닉 계층에서 나가는 신호를 계산
hidden_ouputs = self.activation_function(hidden_inputs)
이를 확장해서 은닉 계층에서 최종 출력 계층으로 나가는 신호를 계산하는 코드를 작성하면 아래와 같다.
#은닉 계층으로 들어오는 신호를 계산
hidden_inputs = numpy.dot(self.wih, inputs)
#은닉 계층에서 나가는 신호를 계산
hidden_outputs = self.activation_function(hidden_inputs)
#최종 출력 계층으로 들어오는 신호 계산
final_inputs = numpy.dot(self.wih, hidden_outputs)
#최종 출력 계층에서 나가는 신호 계산
final_outputs = self.activation_function(final_inputs)
query() 함수의 경우, input_list만 받으며, 은닉 계층으로 들어오는 신호를 계산하기 전에 입력 리스트를 2차원의 행렬로 변환한다. 이때 numpy.array() 함수를 사용한다.
#신경망에 질의하기
def query(self, inputs_list):
#입력 리스트를 2차원 행렬로 변환
inputs = numpy.array(inputs_list, ndmin=2).T
지금까지의 코드
import numpy
import scipy.special
#neural network class definition
class neuralNetwork:
#initialize the neural network
def __init__(self, inputnodes, hiddennodes, outputnodes, learningrate):
#set number of nodes in each input, hidden, output layer
self.inodes = inputnodes
self.hnodes = hiddennodes
self.onodes = outputnodes
#가중치 행렬 wih(input_hidden)와 who(hidden_output)
#배열 내 가중치는 w_i_j로 표기. 노드 i에서 다음 계층의 노드 j로 연결됨을 의미
#w11 w21
#w12 w22
self.wih = numpy.random.normal(0.0, pow(self.hnodes, - 0.5),(self.hnodes, self.inodes))
self.wih = numpy.random.normal(0.0, pow(self.onodes, - 0.5),(self.onodes, self.hnodes))
#learning rate
self.lr = learningrate
#expit()는 시그모이드 함수
self.activation_function = lambda x: scipy.special.expit(x)
pass
#신경망 학습시키기
def train():
pass
#신경망에 질의하기
def query(self, inputs_list):
#입력 리스트를 2차원 행렬로 변환
inputs = numpy.array(inputs_list, ndmin=2).T
#은닉 계층으로 들어오는 신호를 계산
hidden_inputs = numpy.dot(self.wih, inputs)
#은닉 계층에서 나가는 신호를 계산
hidden_outputs = self.activation_function(hidden_inputs)
#최종 출력 계층으로 들어오는 신호 계산
final_inputs = numpy.dot(self.wih, hidden_outputs)
#최종 출력 계층에서 나가는 신호 계산
final_outputs = self.activation_function(final_inputs)
return final_outputs
train()함수 작성 전에 작은 신경망으로 잘 작성하는지 확인하기 위해 학습률은 0.3과 임의의 inputs list인 [1.0, 0.5, -1.5]로 질의를 해보자.
#number of input, hidden output nodes
input_nodes = 3
hidden_nodes = 3
output_nodes = 3
#learning rate is 0.3
learning_rate = 0.3
#create instance of neural network
n = neuralNetwork(input_nodes, hidden_nodes, output_nodes, learning_rate)
n.query([1.0, 0.5, -1.5])
신경망 학습시키기
학습에는 두 가지 단계가 있다.
- 1단계: 주어진 학습 데이터에 대해 결과 값을 계산해내는 단계
- query() 함수에서 했던 작업과 다르지 않으며 출력 값을 계산해내는 단계
- 2단계: 계산한 결과 값을 실제의 값과 비교하고 이 차이를 이용해 가중치를 업데이트하는 단계
- 가중치가 어떻게 업데이트되어야 하는지 알려주기 위해 오차를 역전파하는 단계
train() 함수에는 query() 함수와 다르게 계산 값과 실제 값 간의 오차에 기반하기 때문에 target_list라는 매개 변수가 존재한다.
#신경망 학습시키기
def train(self, inputs_list, targets_list):
#입력 리스트를 2차원의 행렬로 변환
inputs = numpy.array(inputs_list, ndmin=2).T
targets = numpy.array(target_list, ndmin=2).T
이제 가중치를 업데이트하는 코드 작성을 위해 단계별로 나누어 살펴보겠다.
STEP1. 출력 계층의 오차 계산
오차는 (실제 값 행렬 - 계산 값의 행렬)이며, 출력 계층의 오차는 아래와 같다.
#출력 계층의 오차는 (실제 값 - 계산 값)
output_errors = targets - final_outputs
은닉 계층과 최종 계층 간의 가중치는 이 output_errors 이용한다.
STEP2. 은닉 계층의 역전파된 오차 계산
- error_hidden = W_T_hidden_output ⋅ error_ouput
역전파(back-propagration)를 위해서는 연결 노드의 가중치에 따라 오차를 나눠서 전달하고 각각의 은닉 계층의 노드에 대해 이를 재조합한다. 이를 numpy.dot을 활용해 행렬곱 연산을 표현하면 아래와 같다.
#은닉 계층의 오차는 가중치에 의해 나뉜 출력 계층의 오차들을 재조합해 계산
hidden_errors = numpy.dot(self.who.T, output_errors)
입력 계층과 은닉 계층 간의 가중치는 이 hidden_errors 이용한다.
STEP3. 가중치 업데이트
현재(j)와 다음(k) 표시를 활용해 노드j와 다음 계층의 노드k 간의 가중치 업데이트를 행렬로 표현할 수 있다.
- △W_jk = α * E_k * sigmoid(O_k) * (1 - sigmoid(O_k)) ⋅ O_j_T
STEP1에서 정의한 output_errors 이용해 은닉 계층과 최종 계층 간의 가중치를 업데이트하는 코드를 작성한다.
#은닉 계층과 출력 간의 업데이트
self.who += self.lr*numpy.dot((output_errors*final_outputs*(1.0-final_outputs)), numpy.transpose(hidden_outputs))
- 행렬곱 연산에 들어가는 2개의 원소 : final_outputs*(1.0-final_outputs)와 numpy.transpose(hidden_outputs)
- 학습률(α): self.lr
- E_k: output_errors
- sigmoid(O_k): final_outputs*(1.0-final_outputs)
- 1 - sigmoid(O_k): 1.0-final_outputs
- O_j_T: numpy.transpose(hidden_outputs):
STEP2에서 정의한 hidden_errors이용해 입력 계층과 은닉 계층 간의 가중치를 업데이트하는 코드를 작성한다.
#입력 계층과 은닉 계층 간의 가중치 업데이트
self.wih += self.lr*numpy.dot((hidden_errors*hidden_outputs*(1.0-hidden_outputs)), numpy.transpose(inputs))
완성된 신경망 코드
#시그모이드 함수 expit() 사용을 위해 scipy.special 불러오기
import numpy
import scipy.special
#neural network class definition
class neuralNetwork:
#initialize the neural network
def __init__(self, inputnodes, hiddennodes, outputnodes, learningrate):
#set number of nodes in each input, hidden, output layer
self.inodes = inputnodes
self.hnodes = hiddennodes
self.onodes = outputnodes
#가중치 행렬 wih(input_hidden)와 who(hidden_output)
#배열 내 가중치는 w_i_j로 표기. 노드 i에서 다음 계층의 노드 j로 연결됨을 의미
#w11 w21
#w12 w22
self.wih = numpy.random.normal(0.0, pow(self.hnodes, - 0.5),(self.hnodes, self.inodes))
self.wih = numpy.random.normal(0.0, pow(self.onodes, - 0.5),(self.onodes, self.hnodes))
#learning rate
self.lr = learningrate
#expit()는 시그모이드 함수
self.activation_function = lambda x: scipy.special.expit(x)
pass
#신경망 학습시키기
def train(self, inputs_list, targets_list):
#입력 리스트를 2차원의 행렬로 변환
inputs = numpy.array(inputs_list, ndmin=2).T
targets = numpy.array(targets_list, ndmin=2).T
#은닉 계층으로 들어오는 신호를 계산
hidden_inputs = numpy.dot(self.wih, inputs)
#은닉 계층에서 나가는 신호를 계산
hidden_outputs = self.activation_function(hidden_inputs)
#최종 출력 계층으로 들어오는 신호를 계산
final_inputs = numpy.dot(delf.who, hidden_outputs)
#최종 출력 계층에서 나가는 신호를 계산
final_outputs = self.activation_function(final_inputs)
#출력 계층의 오차는 (실제 값 - 계산 값)
output_errors = targets - final_outputs
#은닉 계층의 오차는 가중치에 의해 나뉜 계층의 오차들을 재조합해 계산
hidden_errors = numpy.dot(self.who.T, output_errors)
#은닉 계층과 출력 계층 간의 가중치 업데이트
self.who += self.lr*numpy.dot((output_errors*final_outputs*(1.0-final_outputs)), numpy.transpose(hidden_outputs))
#입력 계층과 은닉 계층 간의 가중치 업데이트
self.wih += self.lr*numpy.dot((hidden_errors*hidden_outputs*(1.0-hidden_outputs)), numpy.transpose(inputs))
pass
#query the neural network
def query(self, inputs_list):
#입력 리스트를 2차원 행렬로 변환
inputs = numpy.array(inputs_list, ndmin=2).T
#은닉 계층으로 들어오는 신호를 계산
#x_hidden = w_hidden x I (x는 가중치 행렬, I는 입력 값들의 행렬)
#입력 계층과 은닉 계층 사이의 가중치 행렬은 입력 핼렬과 조합됨
hidden_inputs = numpy.dot(self.wih, inputs)
#은닉 계층에서 나가는 신호를 계산
hidden_outputs = self.activation_function(hidden_inputs)
#최종 출력 계층으로 들어오는 신호 계산
final_inputs = numpy.dot(self.wih, hidden_outputs)
#최종 출력 계층에서 나가는 신호 계산
final_outputs = self.activation_function(final_inputs)
return final_outputs
완성된 신경망 코드는 위와 같으며 이 코드를 활용해 'MNIST 손글씨 데이터 인식하기' 실습을 진행하겠다.
'Studies & Courses > Machine Learning & Vision' 카테고리의 다른 글
[ML/DL] 수포자가 이해한 Cross Entropy와 KL-divergence (0) | 2021.01.08 |
---|---|
[ML/DL] 수포자가 이해한 2-Layer Neural Net의 vector form (0) | 2021.01.05 |
[ML/DL] MNIST 손글씨 데이터 인식하기 (0) | 2020.12.31 |
[ML 기초] 수포자가 이해한 미분과 편미분 (feat. 경사하강법) (0) | 2020.12.05 |
[머신러닝 기초] NumPy Tutorial - Python (0) | 2020.10.24 |
댓글