[내 신경망 만들기/2부] 2. 파이썬으로 작성하는 신경망1. 개요
2. 파이썬 신경망 기본틀
2-1. 크래스 초기화 함수, __init__()
2-2. 초기 가중치
2-3. 조회 함수 , query()
2-4. 훈련 함수, train()
3. 소규모 신경망
----------------------------------------------------------------------------------------------
[참고서] Make Your Own Neural Networks, Tariq Rashid [
book][
검색링크]
----------------------------------------------------------------------------------------------
[CC-BY]
1. 개요최소한의 파이썬으로 간단한 신경망을 작성해보자.
2. 파이썬 신경망 기본틀신경망의 파이썬 크래스 기본 골격은 다음과 같은 소속함수를 두기로 한다.
- 초기화 함수(크래스 구성자): 신경망을 구성하는 입력층, 은익층 그리고 출력층의 노드 갯수를 정한다.
- 학습 함수: 각층의 노드들 사이의 연결강도(가중치)를 갱신한다. 갱신될 가중치는 학습 자료에 따라 목표 치와 비교하여 갱신될 가중치가 계산된다.
- 조회 함수: 신경망을 구성하는 각 층의 노드에 입력이 주어진 후 계산된 출력을 조회한다.
신경망 크래스 골격은 다음과 같다. 함수의 내용은 아직 비었다.
# Neural network class definition
class neuralNetwork:
# Initialise the neural network
def __init__():
pass
# Train the neural networks
def train():
pass
# query the neural network
def query():
pass2-1. 크래스 초기화 함수, __init__()신경망을 구성하는 각 층의 노드 수와 학습률 상수를 설정하기 위해 크래스 neuralNetwork의 초기화 함수를 아래와 같이 변경 추가 한다.
# Initialise the neural network
def __init__(self, inputnodes,hiddennodes,outputnodes,learningrate):
self.inodes = inputnodes # number of input nodes
self.hnodes = hiddennodes
self.onodes = outputnodes
self.lr = learningrate
pass
각층의 노드 갯수를 주고 크래스를 사례화 하여 신경망을 구성(construct)할 수 있다.
>>> input_nodes = 3
>>> hidden_nodes = 4
>>> output_nodes = 5
>>> learning_rate = 0.3
>>> n = neuralNetwork(input_nodes, hidden_nodes, output_nodes, learning_rate)2-2. 초기 가중치각층의 노드들 사이의 연결강도는 신경망의 핵심이다. 학습은 이 연결강도의 조정과정이다. 연결강도는 행렬로 표현한다. 연결의 시작 층을 열(column)로, 종착 층을 행(row)으로 나타낸다.
[주의] 행렬 표기법
가중치 행렬을 2차원 배열로 표현하는 경우 대부분 프로그래밍 언어에서 첫번째 배열 색인을 행으로 두번째 색인을 열로 나타낸다. 가중치를 표현할 때 W_ij는 i 층에서 j층으로 연결되는 노드 가중치를 의미 했었다. 이때 가중치를 행렬로 표현할 경우 i는 열, j 는 행이다.

예를들어,
- wih은 입력층에서 은닉층으로 연결되는 가중치 행렬이다. 입력층 1번째 노드에서 은닉층 3번째 노드의 연결 강도를 W_13 로 표기 한 경우 파이썬 배열 표현은 wih[3,1] 이다.
- who 는 은닉층에서 출력층으로 연결되는 가중치 행렬이다. 파이썬 배열 who[2,3]는 은닉층 3번째 노드에서 출력층 2번째 노드의 연결 강도다.
초기 가중치를 임의로 주는 방법으로 numpy 모듈의 난수 발생 함수를 사용하여 초기화 한다. 난수 발생 함수는 다음과 같다.
>>> import numpy
>>> numpy.random.rand()
0.6175598212712633난수를 갖는 4행 3렬의 행렬을 단 한문장으로 쉽게 만들 수 있다. 과학 함수 모듈 SciPy의 난수 발생 함수는 [
링크]를 참조한다.
>>> wih = numpy.random.rand(4, 3)
>>> print(wih)
[[0.83381733 0.31199744 0.74202711]
[0.72237501 0.38875002 0.23477635]
[0.39497783 0.37731733 0.20452554]
[0.33714142 0.86863996 0.02348075]]
numpy의 난수는 0.0 과 1.0 사이의 값을 갖는다. 범위를 -0.5와 +0.5 사이 값으로 변경해 주어야 한다. 한 문장으로 모든 행렬 값을 쉽게 병경 할 수 있다. 파이썬의 코드 작성에 효율적인 면을 보여준다.
>>> wih = numpy.random.rand(4, 3) - 0.5
>>> print(wih)
[[ 0.37581529 0.35526794 -0.09234779]
[ 0.16441161 0.38565857 -0.30835495]
[-0.17046742 0.36361927 0.02191034]
[ 0.34118212 0.09421848 0.36531536]]
초기 가중치를 무작위 난수 보다 정규 확률 분포를 따르는 난수가 효과적이다. 정규 확률 분포 난수 발생 함수 numpy.random.normal()는
링크를 참조한다. 이 함수를 이용하여 0.0 을 중심으로 대칭인 정규 확율(가우시안) 분포에서 난수를 생성한다. 한개의 난수값 뿐만 아니라 행렬을 쉽게 만들 수 있다. 출력층의 노드 갯수의 역수, pow(onodes, -0.5)를 확률분포의 표준 편차로 취하여 발생한 난수 행렬을 만드는 예는 다음과 같다.
>>> # normal probability distribution rando generator
>>> import numpy
>>> inodes = 3
>>> hnodes = 4
>>> wih = numpy.random.normal(0.0, pow(hnodes, -0.5), (hnodes, inodes))
>>> print(wih)
[[ 0.42839628 -0.06340692 0.56184273]
[ 0.68782127 0.26616802 -0.35102333]
[ 0.94193363 -0.23215167 -0.04476711]
[ 0.07428921 0.2611703 0.62385664]]
>>> onodes = 5
>>> who = numpy.random.normal(0.0, pow(onodes, -0.5), (onodes, hnodes))
>>> print(who)
[[-0.67086208 -0.52284949 -0.42519078 -0.18905072]
[-0.06233622 0.28244396 0.48764996 0.37380161]
[-0.39578609 0.38793941 0.13654488 -0.29288628]
[ 0.0166917 -0.25951234 0.42738579 0.54957168]
[ 0.04814966 -0.47340498 0.42078217 -0.40487517]]neuralNetwork 크래스의 초기화 함수에 신경망의 연결강도를 임의의 난수 대신 정규 확률 분포를 갖도록 아래와 같이 추가한다.
self.wih = numpy.random.normal(0.0, pow(self.hnodes, -0.5),
(self.hnodes, self.inodes))
self.who = numpy.random.normal(0.0, pow(self.onodes, -0.5), (self.onodes, self.hnodes))
2-3. 조회 함수, query()
조회 함수 query()는 노드들의 출력 계산을 수행한다. 전방향 신경망 처리다. 신경망을 구성하는 각층의 노드들 사이에 가중치를 곱한 누적 값을 활성(발화)함수 (시그모이드 함수)를 통하여 출력을 계산한다.
연결 강도 곱의 누적은 행렬과 벡터의 내적(inner product)이다. i층의 출력 벡터 O_i 에 대하여 연결 강도 행렬 W_ij의 내적은 다음과 같다. 입력층의 노드 갯수가 3, 출력(은닉)층의 노드 갯수는 4일 경우,
파이썬의 numpy 모듈은 행렬과 벡터의 내적을 처리하는 함수를 가지고 있다.
I_hidden = numpy.dot(self.wih, I)높은 추상화 수준의 언어(객체 선언과 할용이 매우 유연하다)인 파이썬은 라이브러리 구축과 활용에 매우 유리하다. 많은 사용자들에 의해 방대한 라이브러리(모듈)들을 공유하고 있다. 비교적 현대적인 언어로서 과학기술 계산, 자료처리(인공지능), 데이터 시각화 등 다양한 라이브러리들이 있다. 출력층 노드의 최종 값은 발화함수의 출력이다. 발화 함수(시그모이드 함수)를 거친 은닉층의 출력은 다음과 같다.
O_hidden = sigmoid(I_hidden)파이썬 SciPy 라이브러리에 시그모이드 함수는 expit() 다. 이 함수를 사용하기 위해 라이브러리를 들여 오자.
# scipy.special for the sigmoid function expit()
import scipy.special
노드의 출력을 결정하는 활성 함수는 시그모이드 외에 다양하게 구현된다. 굳이 복잡한 지수함수를 가진 시그모이드 보다 좀더 단순화된 함수를 사용한다. 계산과 구현의 단순화를 위해 연속함수 대신 불연속 함수가 사용되기도 한다. 대규모 계산을 요구하는 신경망을 감안하면 연산기 단순화(값의 양자화)가 실용적인 면에서 중요한 과제다. 응용에 따라 효율적인 발화함수의 구현은 나중으로 미루고 시그모이드 함수의 원형[
참고]을 발화 함수로 사용하기로 한다.
# activation function is the sigmoid function
self.activation_function = lambda x: scipy.special.expit(x)
'람다(lambda)'식(expression)은 익명으로 '함수'를 기술하는 기법이다. 한 문장으로 함수를 간결하게 기술할 수 있다.
lambda 인자: 표현식람다 식으로 기술한 활성함수를 사용하는 방법은 일반 함수와 같다.
hidden_output = self.activation_function(O_hidden)
3개층으로 구성된 신경망을 구성하는 입력층과 은닉층 사이의 노드 연결과 출력은 다음과 같이 기술할 수 있다. 입력 벡터와 연결 강도(가중치) 행렬의 내적과 활성함수 적용후 출력이다.
hidden_inputs = numpy.dot(self.wih, inputs)
hidden_outputs = self.activation_function(hidden_inputs)동일한 방식으로 은닉층과 출력층을 연결하여 신경망 최종 출력을 얻는다.
final_inputs = numpy.dot(self.who, hidden_outputs)
final_outputs = self.activation_function(final_inputs)
초기화와 조회 함수가 포함된 파이썬 코드를 실행해보자. 학습 함수 train() 은 아직 작성 전이다.
$ python3
Python 3.12.3 (main, Mar 3 2026, 12:15:18) [GCC 13.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>
>>> import numpy
>>> import scipy.special
파이썬 파일을 읽어 실행,
>>> exec(open('Code_init_query.py').read())신경망 크래스 사례화하여 소규모 신경망 만들기,
>>> input_nodes = 5
>>> hidden_nodes = 4
>>> output_nodes = 3
>>> learning_rate = 0.3
>>> n = neuralNetwork(input_nodes, hidden_nodes, output_nodes, learning_rate)신경망 크래스 객체가 사례화 하면서 신경망이 구성된다. 초기화 함수(구성자)에 의해 임의 값으로 초가화 한 입력층과 은닉층 사이의 연결 가중치 행렬 확인해보자.
입력층과 은닉층 사이의 연결 가중치 행렬 확인,
>>> print(n.wih)
[[-0.78217197 0.5808482 -0.31876802 1.64631682 -0.28874034]
[ 0.17742955 -0.26017423 -0.55911973 -0.40947595 0.41022274]
[-0.28092837 -0.04689308 -0.25862563 -0.67334743 -0.85635187]
[-0.44649879 0.31916959 0.05223414 -0.06373131 -0.46721884]]
은닉층과 출력층 사이의 연결 가중치 행렬 확인,
>>> print(n.who)
[[ 0.64205114 0.50799958 1.45701532 -1.02826693]
[ 0.56086822 0.46135494 -0.55627495 0.40996956]
[-0.63063029 -0.71400507 0.82037014 1.43052149]]
입력층은 이 1행짜리 리스트 형식이므로 행렬 내적을 수행 하려면 전치(transpose)행렬로 바꿔 주어야 한다.
inputs = numpy.array(inputs_list, ndmin=2).T
신경망(순방향)을 실행해 보자.
>>> o = n.query([1.0, 0.5, -1.5, 1.5, 2.0])
[[ 1. ]
[ 0.5]
[-1.5]
[ 1.5]
[ 2. ]]
>>> print(o)
[[0.69641302]
[0.7060551 ]
[0.32236108]]
아직 의미있는 훈련을 하지 않았지만 신경망의 순방향 작동을 확인 해봤다. 파이썬을 사용하면 최소한의 코드로 알고리즘을 기술 할 수 있다.
2-4. 훈련 함수, train()
이제 신경망을 훈련시켜보자. 신경망은 3개 층으로 구성되었으며 각 층마다 L, M, N개의 노드를 가지도록 초기화 되었다. 훈련은 먼저 순방향 계산을 수행하고 이를 토대로 목표와 차분을 역전파하여 가중치를 갱신한다.
학습 함수 train()은 두개의 인자(시험입력과 목표)를 갖는다.
# train the neural network
def train(self, inputs_list, targets_list):
시험입력 input_list는 1행 L렬이므로 연결 가중치 행렬과 내적을 수행하기 위해 전치(transpose)하여 L행 1렬로 변환 한다. 학습 폭표 target_list는 N행 1렬 벡터로 변환한다. numpy.array().T는 배열의 행과 열의 위치를 바꿔준다.
inputs = numpy.array(inputs_list, ndmin=2).T
targets = numpy.array(targets_list, ndmin=2).T
함수 numpy.dot()룰 사용하여 가중치 행렬과 입력 벡터를 내적(inner product)한다. 내적을 수행하려면 가중치 행렬 wih의 열의 갯수와 입력 벡터 input의 행의 갯수가 일치해야 한다. 입력층과 은닉층 사이의 연결강도가 저장된 행렬의 크기는 M행 L렬이다.
hidden_inputs = numpy.dot(self.wih, inputs)
hidden_outputs = self.activation_function(hidden_inputs)
이어 은닉층과 출력층의 순방향 계산이다. 연결강도 행렬은 who는 N행 M렬이다.
final_inputs = numpy.dot(self.who, hidden_outputs)
final_outputs = self.activation_function(final_inputs)
순방향 처리로 신경망의 현재 출력 final_output을 계산했다. 목표와 차로 오차 output_error를 구한 후 연결강도 행렬 who의 전치행렬과 내적으로 은닉층 오차 hidden_errors 를 구한다. 오차의 역전파(back-propagation)이다.
output_errors = targets - final_outputs
hidden_errors = numpy.dot(self.who.T, output_errors)
1부에서 다뤘던 가중치 갱신량은 다음과 같다. 활성함수는 시그모이드 함수다.
은닉층과 출력층 사이의 가중치 행렬의 갱신은 다음과 같다.
$ python3
Python 3.12.3 (main, Mar 23 2026, 19:04:32) [GCC 13.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>
미리 작성해둔 신경망을 읽어 실행,
>>> import os
>>> exec(open('./part2_neural_network.py').read())
각 층의 노드수와 학습율을 주고 신경망을 구성한다.
>>> input_nodes = 3
>>> hidden_nodes = 20
>>> output_nodes = 8
>>> learning_rate = 0.3
>>> n = neuralNetwork(input_nodes, hidden_nodes, output_nodes, learning_rate)
초기 연결 가중치 행렬을 보자.
>>> print(n.wih)
[[ 0.23480486 -0.62048203 0.48227183]
[-0.44191224 0.96688793 0.37256845]
[ 0.73100745 0.9149449 0.55167343]
. . . . . .
[-0.381088 0.11340818 -0.23275817]
[ 0.22929161 -0.65477635 0.41994934]
[-0.38261365 0.21811293 0.44382208]]
>>> print(n.who)
[[-0.05045051 -0.09326337 . . . . . -0.03752638 0.01446497]
[ 0.4941326 -0.20127579 . . . . . -0.00389773 -0.17173596]
[ 0.18257659 0.02078628 . . . . . 0.03690485 0.11091558]
. . . . . .
[ 0.1637788 -0.10455684 . . . . . -0.02724405 0.0628117 ]]
입력과 목표를 주고 훈련시킨다. 훈련 목적은 3x8 디코더다. 훈련이 용이하도록 다음과 같은 함수를 작성했다.
# Filename: train_3x8_decoder.py
def train_3x8_decoder(neuralNetwork):
for n in range(8):
target_list = numpy.zeros(8)
target_list[n] = 0.99
input_list = numpy.zeros(3)
m = n
for i in range(3):
if (m & 1): input_list[i] = 0.99
else : input_list[i] = 0.0
m >>=1
pass
pass
print("Input :", input_list)
print("Target:", target_list)
neuralNetwork.train(input_list, target_list)
pass
>>> exec(open('./train_3x8_decoder.py').read())
>>> train_3x8_decoder(n)
Input : [0. 0. 0.]
Target: [0.99 0. 0. 0. 0. 0. 0. 0. ]
Input : [0.99 0. 0. ]
Target: [0. 0.99 0. 0. 0. 0. 0. 0. ]
Input : [0. 0.99 0. ]
Target: [0. 0. 0.99 0. 0. 0. 0. 0. ]
Input : [0.99 0.99 0. ]
Target: [0. 0. 0. 0.99 0. 0. 0. 0. ]
Input : [0. 0. 0.99]
Target: [0. 0. 0. 0. 0.99 0. 0. 0. ]
Input : [0.99 0. 0.99]
Target: [0. 0. 0. 0. 0. 0.99 0. 0. ]
Input : [0. 0.99 0.99]
Target: [0. 0. 0. 0. 0. 0. 0.99 0. ]
Input : [0.99 0.99 0.99]
Target: [0. 0. 0. 0. 0. 0. 0. 0.99]
입력을 주고 신경망의 결과를 보자.
>>> input_list = [0.0, 0.99, 0.0]
>>> print(input_list)
[0.0, 0.99, 0.0]
>>> n.query(input_list)
array([[0.28647384],
[0.31572146],
[0.2357812 ],
[0.30340183],
[0.24903345],
[0.24786613],
[0.34638421],
[0.31687699]])
1번의 훈련으로는 신통치 않다. 훈련을 50회 반복하고,
>>> for k in range(50):
... train_3x8_decoder(n)
...
신경망을 시험해 본다.
>>> print(input_list)
[0.0, 0.99, 0.0]
>>> n.query(input_list)
array([[0.15615978],
[0.05668246],
[0.46297304],
[0.27559223],
[0.07328362],
[0.02519343],
[0.27244608],
[0.15757236]])
모든 경우의 입력에 대하여 신경망을 시험하기 위해 테스트 함수를 다음과 같이 작성했다.
# Filename: query_3x8_decoder.py
def query_3x8_decoder(neuralNetwork):
for n in range(8):
input_list = numpy.zeros(3)
m = n
for i in range(3):
if (m & 1): input_list[i] = 0.99
else : input_list[i] = 0.0
m >>=1
pass
pass
print("Input :", input_list)
final_outputs = neuralNetwork.query(input_list)
print("Result:\n", final_outputs)
pass
테스트 함수를 불러 실행,
>>> exec(open('./query_3x8_decoder.py').read())
>>> query_3x8_decoder(n)
Input : [0. 0. 0.]
Result:
[[0.30495533]
[0.23650692]
[0.16199468]
[0.11086559]
[0.17795601]
[0.11836501]
[0.12903878]
[0.12213821]]
Input : [0.99 0. 0. ]
Result:
[[0.20313592]
[0.53596938]
[0.05762545]
[0.25535589]
[0.08108751]
[0.27341848]
[0.04069108]
[0.14628759]]
Input : [0. 0.99 0. ]
Result:
[[0.15615978]
[0.05668246]
[0.46297304]
[0.27559223]
[0.07328362]
[0.02519343]
[0.27244608]
[0.15757236]]
Input : [0.99 0.99 0. ]
Result:
[[0.10370655]
[0.17066447]
[0.19996303]
[0.48574626]
[0.03205448]
[0.06564895]
[0.09148218]
[0.18006337]]
Input : [0. 0. 0.99]
Result:
[[0.16357412]
[0.07169463]
[0.06815046]
[0.03221097]
[0.46463536]
[0.30841241]
[0.27241288]
[0.14081779]]
Input : [0.99 0. 0.99]
Result:
[[0.10043521]
[0.20833083]
[0.02399164]
[0.08000641]
[0.22380893]
[0.50919171]
[0.09650261]
[0.16372008]]
Input : [0. 0.99 0.99]
Result:
[[0.0869016 ]
[0.02090949]
[0.23479213]
[0.08604624]
[0.19224772]
[0.0612008 ]
[0.42393379]
[0.15282409]]
Input : [0.99 0.99 0.99]
Result:
[[0.05329815]
[0.06279431]
[0.08822157]
[0.18996017]
[0.08444943]
[0.15224292]
[0.17588012]
[0.18897843]]
[과제1] 마지막 입력에 대한 결과가 신통치 않게 나왔다. 학습 횟수를 늘려 시험해 보라.
[과제2] 학습이 반복되면서 가중치의 변화를 시각적으로 표시해보라.
우리는 인코딩된 2진수를 디코딩하는 알고리즘은 이미 잘 알고있다. 이를 모르는 기계(신경망)에게 2진수 입력을 주고 디코더를 학습 시켜봤다. 결과를 얻기 위해 상당히 많은 회수의 학습이 필요하다. 소규모 신경망으로 할 수 있는 일은 아주 비효율적이다. 파이썬으로 작성한 코드가 작동 한다는 점 만 확인하는데 만족하자. 신경망이 효과를 거두려면 대규모 네트워크를 구성하여 방대한 자료를 기반으로 학습해야 한다. 다음에는 본격적으로 손글씨 이미지를 받아 훈련시키고 인식하는 신경망을 작성해 보기로 한다.
-------------------------------------------------------------------------------------
[
이전] [내 신경망 만들기/2부] 1. 최소한의 파이썬(찬조출연: C++)
[다음]