2026년 5월 12일 화요일

내 신경망 제작하기

내 신경망 제작하기

이글의 제목은 다분히 "내 칩 제작 서비스"에서 따왔다.

바야흐로 "인공 지능"의 시대다. 일상 다반사가 된 "인공지능"의 기초는 "신경망"이라는데 도데체 그게 뭔가 싶어 찾아보면 알듯 모를듯 하다. 수많은 AI를 내세운 동영상 강좌가 넘쳐난다. 기초라고 하지만 끝까지 시청하기 어렵다. 수식 자랑을 늘어 놓는 탓이다. 그러다가 이 책을 발견 했다.

    Make Your Own Neural Network

마침 한글 번역판도 있다.

    신경망 첫걸음

이 책의 부제 "수포자도 이해하는 신경망 동작 원리와 딥러닝 기초"라는 문구에 동의한다. 다만 책 분량이 만만치 않다. 그래서 요약글을 준비했다. 원서의 제목대로 "내 신경망(My Own Neural Network)"을 제작하기가 목표다. 이 책은 1부에서 신경망의 작동 원리를 곱셈과 덧셈 만으로 설명한다. 약간의 고등 수학처럼 보이는 부분이 가미되어 있지만 1차 방정식과 인수분해 만으로도 충분히 이해할만 한 수준이다. 2부는 '파이썬(Python)'으로 내 신경망을 제작한다. DIY with Python 라니 제목부터 남다르다. 파이썬이라는 컴퓨팅 언어를 모르는 입문자를 배려하여  "아주 부드럽게 시작(A Very Gentle Start with Python)"한다. 1부가 '수포자' 였다면 2부는 '컴포자(컴퓨팅 언어를 포기한자)'를 대상으로 쓰였다고 해줄 만 하다. 텐서플로우니 파이토치니 하는 따위를 사용하지 않고도 MNIST 라는 손글씨 숫자 영상 인식을 수행하는 "내 신경망"을 충분히 코딩하고 실행할 수 있음을 보여준다. 가장 기본적인 numpy, matplot 만 사용한다. 사설이 길었다. 시작해 보자.

신경망이라고 하면서 무슨 생물학(뇌과학)을 들이밀 필요는 없을 것이다. 너무나 많이 들었을테니까!

비례식은 들어봤을 것이다. 예를 들어 거리의 단위로 마일(mile)과 키로미터(km)가 있다. 이 둘 사이의 환산 공식을 아는가? 모른다고 치자. 다행히 두 거리 단위의 관계는 비례한다.

    Mile = A * Kilometer

마일에 숫자(비례 상수 A)를 곱하면 킬로미터가 된다는 의미다. 두 단위의 관계를 그래프로 표현하면 다음과 같다. 가로축에 킬로미터 세로축이 마일이다. '무지렁이' 기계한테 '비례 관계'라는 정보만 주고 그 상수 값 A 를 구하라고 시켜보자. 기계는 아무렇게나 직선을 주욱 긋는다. 이 직선의 기울기가 상수 W 다.

<그림>

제대로 그었는지 확인하기 위해 한 측정 치를 제시한다.

    37mile -> 59.6Km

똑똑한 인간 이라면 단번에 계산할 수 있겠지만 기계는 아무것도 모른다. 자기가 그은 직선과 제시된 측정치와 맞춰보고 오차로부터 기울기를 보정할 줄은 안다.

<그림>

기계가 아무렇게나 그어놓은 직선으로부터 얻은 값을 y 라고 하자. 가로축의 동일한 지점 x 에서 제대로 나와야 할 값은 t 라고 하자.

    y = W*x

    t = (W + delta_W)*x

오차 Err은 t와 y의 차분이다.

    Err = (t - y) = delta_W*x

이로부터 수정해야 할 직선의 기울기의 량을 구했다.

    delta_W = Err / x

이 "보정치 구하기"가 바로 기계학습이다. 예로 돌아가 보자. 기계가 아무렇게 그은 직선의 기울기 W = 0.9 였다고 하자. 제시된 정보에 따르면 x = 37에서 목표치는 59.6 이라고 한다. 기계가 그은 직선에 의하면 x = 37에서 y=0.9*37=33.3 이다. 오차 Err = 59.6 - 33.3 = 26.3이다. 보정해야 할 기울기 delta_W = 26.3/37 = 0.7108 다. 따라서 기계는 그만큼 보정하여 기울기를 수정한다. 

    W_new = W + delta_W = 0.9 + 0.7108 = 1.6181

제시했던 정보를 검산 해보면 약간의 계산 오차가 있지만 마일에서 킬로미터로 변환하는 비례상수 A를 구했다.

    37 * W_new = 37 * 1.6108 = 59.5996

요약해보면,

- 기계는 아무렇게나 기울기를 정해서 직선을 그었다.
- 학습할 정보를 가지고 아무렇게 그은 직선의 기울기를 보정한다.

"아무렇게 그은 직선"이 신경망 학습의 시발점이자 핵심이다. "신경망" 별 것 아닐것 같은가?

직선을 사용하여 세상사를 구분하려고 한다. 세상일이 이렇게 단순하면 좋으련만 수많은 변수가 서로 얼키고 설켜있다. 직선 하나만으로 구분하기 어렵다는 것을 알게됐다. 당장 단 2개의 변수를 가진 XOR 라는 논리함수를 보자. 이 함수의 출력을 두축의 좌표계상에 배치하고 직선 한개로 구분할 방법이 없다.

<그림>

그렇다면 여러개의 직선을 동원하자. 동물의 뇌에서 일어나는 작동을 알아보니 단순하다. 신경 세포들 사이의 작용이 비례관계에 있다. 수많은 신경 세포들이 서로 얽혀있다. 인간의 뇌는 약 8천억개의 신경 세포들이 있다. 초파리는 장애물을 피해 비행하는데 10만개의 신경세포들을 동원한다. 엄청난 수의 직선들이 얽혀 신통한 결정을 하는 셈이다. 신경 세포는 별 것아닐지 몰라도 떼로 모아놓은 "신경망"은 상상을 초월한다. 신경 세포들이 층을 이뤄 연결되어 더욱 위력을 발휘한다. 게다가 이 연결은 유연하다.

<그림>

신경 세포들의 연결망을 다음과 같이 모사한다. 다수의 신경 세포들은 층으로 나눠져 분포되었다. 신경 세포들 사이의 연결을 묘사하는 수식은 1차 함수다. 1차 함수의 기울기가 연결 유연성을 나타내며 이를 가중치라 한다. 연결된 두 신경 세포의 관계는 이전 신경 세포의 출력과 가중치의 곱이다. 한 신경 세포에 다수의 신경 세포들이 연결되어 있으므로 이전 신경 세포의 출력과 연결강도(가중치) 곱의 합이 현재 신경 세포의 입력이다.

<그림>

한 신경 세포는 다수의 입력을 받아 자신의 출력을 결정하는 함수를 가진다. 이 함수를 활성함수라 한다. 연결받을 신경세포의 갯수(함수의 정의 구역의 범위)가 특정되지 않았더라도 출력을 제한할 필요가 있다. 시그모이드 함수는 입력이 무한히 증가 하더라도 그 출력을 수렴 시킬 수 있다.

<그림>

모사한 뇌의 신경 세포 연결망의 규모는 유한할 수 밖에 없다. 연결 강도의 범위를 제한 하는 방법도 있다. 따라서 시그모이드 함수를 적용하지 않더라도 신경 세포의 출력은 예측 가능하다. 계산이 복잡한 시그모이드 함수 대신 단순한 직선식을 적용하기도 한다.

두 층 사이 신경 세포의 연결을 수식으로 표현하려면 매우 많은 1차 식이 동원되어야 한다. 수학의 시그마 기호는 이를 간략하게 표현하는 방법이다. 층 1에서 층 2로 다수의 신경망들이 연결되었다고 하자.

<그림>

층2 신경세포의 입력은,

Input_0 = W_00*Out_0 + W_10*Out_1 +  W_20*Out_2

Input_1 = W_01*Out_0 + W_11*Out_1 +  W_21*Out_2

Input_2 = W_02*Out_0 + W_12*Out_1 +  W_22*Out_2

이를 일반화 시켜보자.

Input_0 = {sigma{i=0,1,2)} W_i0*Out_i

Input_1 = {sigma{i=0,1,2)} W_i1*Out_i

Input_2 = {sigma{i=0,1,2)} W_i2*Out_i

좀더 일반화 하면 다음과 같다.

Input_j = {sigma{i=0,2)} W_ij*Out_i, j=0,1,2

W_ij 는 층 1의 i번째 신경세포의 출력과 층 2의 j번째 신경세포의 입력 사이에 연결 강도를 나타낸다. 









2026년 4월 22일 수요일

[내 뉴럴 네트워크 만들기/2부] 3. 손글씨 인식 신경망(MNIST)

[내 뉴럴 네트워크 만들기/2부] 3. 손글씨 인식 신경망(MNIST)

1. 개요

2. 파이썬 신경망 기본틀
    2-1. 크래스 초기화 함수,  __init__()
    2-2. 초기 가중치
    2-3. 조회 함수 , query()
    2-4. 훈련 함수, train()

3. 신경망 

----------------------------------------------------------------------------------------------
[참고서] Make Your Own Neural Networks, Tariq Rashid [book][검색링크]
----------------------------------------------------------------------------------------------












3Blue1Brown-neural networks
https://www.3blue1brown.com/?topic=neural-networks

3Blue1Brown 한국어 - ML/DL
https://www.youtube.com/playlist?list=PLkoaXOTFHiqhM4MeCMrS016jOWKfIXTjK

이미지 분류: 다층 퍼셉트론(MLP)으로 손글씨 숫자(MNIST) 인식
https://youtu.be/gh8UR3nw2uk

딥러닝의 핵심 활성화 함수(1): Sigmoid의 특징과 한계

딥러닝의 핵심 활성화 함수(2):Tanh, ReLU, Leaky ReLU


예제로 배우는 역전파(backpropagation)
https://youtu.be/Ku1xUFK9I3Y

합성곱 신경망(CNN) (기초이론)
https://youtu.be/h1Io450Igrg
 

합성곱 신경망(CNN) (MNIST 실습)
https://youtu.be/IHbsSmRbcrw

https://en.wikipedia.org/wiki/Edge_detection

https://en.wikipedia.org/wiki/Canny_edge_detector






2026년 4월 18일 토요일

[내 뉴럴 네트워크 만들기/2부] 2. 파이썬으로 작성하는 신경망

[내 뉴럴 네트워크 만들기/2부] 2. 파이썬으로 작성하는 신경망

1. 개요

2. 파이썬 신경망 기본틀
    2-1. 크래스 초기화 함수,  __init__()
    2-2. 초기 가중치
    2-3. 조회 함수 , query()
    2-4. 훈련 함수, train()

3. 소규모 신경망 

----------------------------------------------------------------------------------------------
[참고서] Make Your Own Neural Networks, Tariq Rashid [book][검색링크]
----------------------------------------------------------------------------------------------

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():
        pass

2-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

각층의 노드 갯수를 주고 크래스를 사례화 하여 신경망을 구성한다.

>>> input_nodes = 3
>>> hidden_nodes = 3
>>> output_nodes = 3
>>> learning_rate = 0.3
>>> n = neuralNetwork(input_nodes, hidden_nodes, output_nodes, learning_rate)

2-2. 초기 가중치

각층의 노드들 사이의 연결강도는 신경망의 핵심이다. 학습은 이 연결강도의 조정과정이다. 연결강도는 행렬로 표현한다. 연결의 시작을 행으로, 종착을 열로 나타낸다. 예를들어,

- wih은 입력층에서 은닉층으로 연결되는 가중치 행렬이다. wih[1][3] 은 입력층 1번째 노드에서 은닉층 3번째 노드의 연결 강도다.
- who 는 은닉층에서 출력층으로 연결되는 가중치 행렬이다. who[3][2] 는 은닉층 3번째 노드에서 출력층 2번째 노드의 연결 강도다.

초기 가중치는 numpy 모듈의 난수 발생 함수를 사용하여 줄 수 있다. 난수 발생 함수는 다음과 같다.

>>> import numpy
>>> numpy.random.rand()
0.6175598212712633

난수를 갖는 3행 3렬의 행렬을 단 한문장으로 쉽게 만들 수 있다. 과학 함수 모듈 SciPy의 난수 발생 함수는 [링크]를 참조한다.

>>> wih = numpy.random.rand(3, 3)
>>> print(wih)
[[0.69180601 0.49551435 0.60070353]
 [0.78342214 0.53015784 0.73122591]
 [0.04353726 0.62959035 0.7916583 ]]

numpy의 난수는 0.0 과 1.0 사이의 값을 갖는다. 범위를 -0.5와 +0.5 사이 값으로 변경해 주어야 한다. 한 문장으로 모든 행렬 값을 쉽게 병경 할 수 있다.  파이썬의 코드 작성에 효율적인 면을 보여준다.

>>> wih = numpy.random.rand(3, 3) - 0.5
>>> print(wih)
[[ 0.30124198 -0.23037785  0.18063384]
 [-0.24784492  0.35512545 -0.44099057]
 [ 0.44976501 -0.40828219  0.44500375]]

초기 가중치를 무작위 난수보다 정규 확률 분포를 따르는 난수가 효과적이다. 정규 확률 분포 난수 발생 함수 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]]

신경망의 연결강도를 임의의 난수 대신 정규 확률 분포를 가지는 난수로  초기화 해준다.

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 에 대하여 연결 강도 행렬 W의 내적은 다음과 같다.

    X_hidden = W_input_hidden . transpose(I_input)

입력층의 노드 갯수가 3, 은닉층의 노드 갯수는 4일 경우,

    |X[0]| = |W[0][0] W[1][0] W[2][0]| . |I[0]|
    |X[1]|   |W[0][1] W[1][1] W[2][1]|   |
I[1]|
    |X[2]|   |W[0][2] W[1][2] W[2][2]|   |I[3]|
    |X[3]|   |W[0][3] W[1][3] W[2][3]|

파이썬의 numpy 모듈은 행렬과 벡터의 내적을 처리하는 함수를 가지고 있다.

    X_hidden = numpy.dot(self.wih, I)

높은 추상화 수준의 언어(객체 선언과 할용이 매우 유연하다)인 파이썬은 라이브러리 구축과 활용에 매우 유리하다. 많은 사용자들에 의해 방대한 라이브러리(모듈)들을 손쉽게 공유할 수 있다. 비교적 현대적인 언어로서 과학기술 계산, 자료처리(인공지능), 데이터 시각화 등 다양한 라이브러리들이 있다. 은닉층 노드의 최종 값은 발화함수의 출력이다.

    |X[0]| = |sigmoid(X[0])|
    |X[1]|   |
sigmoid(X[1])|
    |X[2]|   |sigmoid(X[2])|
    |X[3]|   |sigmoid(X[3])|

시그모이드 함수를 거친 은닉층의 출력은 다음과 같다.

    O_hidden = sigmoid(X_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)

 '익명 함수(anoymous)'라고 부르는 '람다(lambda)' 수식(expression)으로 '함수'를 기술하는 기법이다. 한 문장으로 함수를 간결하게 기술할 수 있다.

    lambda 인자: 표현식

람다 식으로 기술한 활성함수를 사용하는 방법은 일반 함수와 같다.

    #calculate the signal emerging from hidden layer
    hidden_output = self.activation_function(O_hidden)
 

신경망을 구성하는 두 층 사이의 노드 연결과 출력은 다음과 같다. 입력 벡터와 연결 강도(가중치) 행렬의 내적과 활성함수 적용후 출력이다.

    # calculate signals into hidden layer
    hidden_inputs = numpy.dot(self.wih, inputs)

    # calculate the signals emerging from hidden layer
    hidden_outputs = self.activation_function(hidden_inputs)

동일한 방식으로 은닉층과 출력층을 연결하여 신경망 최종 출력을 얻는다. 

    # calculate signals into final output layer
    final_inputs = numpy.dot(self.who, hidden_outputs)

    # calculate the signals emerging from final output layer
    final_outputs = self.activation_function(final_inputs)

초기화와 조회 함수가 포함된 파이썬 코드는 다음과 같다. 학습 함수 train() 은 아직 작성 전이다.

# Filename: Code_init_query.py
# neural network class definition
class neuralNetwork :
    # initialise 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

        # link weight matrices, wih and who
        # weights inside the arrays are w_i_j,
        # where link is from node i to node j in the next layer
        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))

        # learning rate
        self.lr = learningrate

          # activation function is the sigmoid function
        self.activation_function = lambda x: scipy.special.expit(x)
        pass

    # train the neural network
    def train() :
                pass

    # query the neural network
    def query(self, inputs_list) :
        # convert inputs list to 2d array
        inputs = numpy.array(inputs_list, ndmin=2).T
        print(inputs)

        # calculate signals into hidden layer
        hidden_inputs = numpy.dot(self.wih, inputs)

        # calculate the signals emerging from hidden layer
        hidden_outputs = self.activation_function(hidden_inputs)

        # calculate signals into final output layer
        final_inputs = numpy.dot(self.who, hidden_outputs)

        # calculate the signals emerging from final output
        final_outputs = self.activation_function(final_inputs)

        return final_outputs

    pass # End of class, neuralNetwork

실행,

$  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)행렬로 바꿔 주어야 한다.

>>> 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()

이제 신경망을 훈련시켜보자. 훈련은 먼저 query()로 순방향 계산을 수행하고 이를 토대로 목표와 차분을 역전파하여 가중치를 갱신한다. 학습 함수 train()은 두개의 인자(시험입력과 목표)를 갖는다. 순방향 입력에 대하여 순방향 계산을 수행 한 후,

    # train the neural network
    def train(self, inputs_list, targets_list):
        # convert inputs list to 2d array
        inputs = numpy.array(inputs_list, ndmin=2).T
        targets = numpy.array(targets_list, ndmin=2).T

        # calculate signals into hidden layer
        hidden_inputs = numpy.dot(self.wih, inputs)
        # calculate the signals emerging from hidden layer
        hidden_outputs = self.activation_function(hidden_inputs) 

        # calculate signals into final output layer
        final_inputs = numpy.dot(self.who, hidden_outputs)
        # calculate the signals emerging from final output layer
        final_outputs = self.activation_function(final_inputs)
        pass

출력과 목표의 차를 구한다.

    # error is the (target - actual)
    output_errors = target - final_output

출력층의 오차는 직전의 은닉층과 결합 가중치 갱신을 위해 역전파된다. 은닉층 오차 계산은 다음과 같다.

     errors_hidden = transpose(Weight_hidden_output) . errors_output

1부에서 다뤘던 가중치 갱신량은 다음과 같다. 두 층 사이의 각 노드들이 연결되는 가중치의 갱신량이다.

 

위의 식에서 은닉층 노드는 색인 j로  출력층 노드는 색인 k라면,

       self.lr  -> (alpha)
    
       Ek  -> output_errors
   
sigmoid(Ok) -> ​final_outputs
            
Oj  -> hidden_outputs

은닉층과 출력층의 연결 가중치 갱신을 파이썬 코드로 옮기면 다음과 같다.

# update the weights for the links between the hidden and output layers
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))

학습 함수까지 작성된 신경망 파이썬 코드는 아래 링크에서 받을 수 있다. 

https://github.com/makeyourownneuralnetwork/makeyourownneuralnetwork/blob/master/part2_neural_network.ipynb

3. 소규모 신경망

소규모 신경망의 내용은 다음과 같다.

# python notebook for Make Your Own Neural Network
# (c) Tariq Rashid, 2016
# license is GPLv2

import numpy
# scipy.special for the sigmoid function expit()
import scipy.special

# neural network class definition
class neuralNetwork:
    # initialise 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

        # link weight matrices, wih and who
        # weights inside the arrays are w_i_j,
        # where link is from node i to node j in the next layer
        #     w11 w21
        #     w12 w22 etc
        self.wih = numpy.random.normal(0.0, pow(self.inodes, -0.5),
                                            (self.hnodes, self.inodes))
        self.who = numpy.random.normal(0.0, pow(self.hnodes, -0.5),
                                            (self.onodes, self.hnodes))

        # learning rate
        self.lr = learningrate

        # activation function is the sigmoid function
        self.activation_function = lambda x: scipy.special.expit(x)

        pass

    # train the neural network
    def train(self, inputs_list, targets_list):

        # convert inputs list to 2d array for transpose operation
        inputs = numpy.array(inputs_list, ndmin=2).T
        targets = numpy.array(targets_list, ndmin=2).T

        # calculate signals into hidden layer
        hidden_inputs = numpy.dot(self.wih, inputs)
        # calculate the signals emerging from hidden layer
        hidden_outputs = self.activation_function(hidden_inputs)

        # calculate signals into final output layer
        final_inputs = numpy.dot(self.who, hidden_outputs)
        # calculate the signals emerging from final output layer
        final_outputs = self.activation_function(final_inputs)

        # output layer error is the (target - actual)
        output_errors = targets - final_outputs

        # hidden layer error is the output_errors,
        #     split by weights, recombined at hidden nodes(Transpose!)
        hidden_errors = numpy.dot(self.who.T, output_errors)

        # update the weights for the links
        #    between the hidden and output layers
        self.who += self.lr *
                        numpy.dot(
                            (output_errors * final_outputs *
                                                (1.0 - final_outputs)),
                            numpy.transpose(hidden_outputs))

        # update the weights for the links
        #    between the input and hidden layers
        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):

        # convert inputs list to 2d array
        inputs = numpy.array(inputs_list, ndmin=2).T

        # calculate signals into hidden layer
        hidden_inputs = numpy.dot(self.wih, inputs)
        # calculate the signals emerging from hidden layer
        hidden_outputs = self.activation_function(hidden_inputs)

        # calculate signals into final output layer
        final_inputs = numpy.dot(self.who, hidden_outputs)
        # calculate the signals emerging from final output layer
        final_outputs = self.activation_function(final_inputs)

        return final_outputs

간단한 신경망 이지만 여러 실험을 해볼 수 있다. 아래의 테스트벤치 코드를 가지고 실험해보자.

if __name__=="__main__":
    # number of input, hidden and 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)

    input_list  = [0.0, 1.0, 0.0]
    target_list = [0.0, 0.0, 1.0]

    for k in range(100):
        n.train(input_list, target_list)
        pass
    print('Trained with test', input_list, ', target', target_list)

    # test query
    rand = lambda : abs(numpy.random.rand())
    print('Query Test:')
    for k in range(10):
        query_list = [rand()/2.0, 1.0, rand()/2.0]
        output_list = numpy.transpose(n.query(query_list))
        print(f"{query_list}", '->', output_list, end="")

        if (output_list[0,2] < 0.9):
            print('<----------Oops!')
        else :
            print()

        pass

----

입력을 [0 1 0] , 목표를 [0 0 1]로 주고 100회 반복 학습을 시킨 후 시험 입력을 넣어 보면 결과가 별로 신통치 않다.

----- 

$ python3 part2_neural_network.py
Trained with test [0.0, 1.0, 0.0] , target [0.0, 0.0, 1.0]
Query Test:
[0.15031, 1.0, 0.12314] -> [[0.16026 0.15989 0.87123]]<----------Oops!
[0.04647, 1.0, 0.42817] -> [[0.15789 0.15930 0.87107]]<----------Oops!
[0.07979, 1.0, 0.49753] -> [[0.15724 0.15932 0.87088]]<----------Oops!
[0.26465, 1.0, 0.07751] -> [[0.16033 0.16023 0.87109]]<----------Oops!
[0.43167, 1.0, 0.45617] -> [[0.15662 0.16007 0.87016]]<----------Oops!
[0.36737, 1.0, 0.19353] -> [[0.15895 0.16027 0.87081]]<----------Oops!
[0.00176, 1.0, 0.07209] -> [[0.16126 0.15959 0.87140]]<----------Oops!
[0.10080, 1.0, 0.01487] -> [[0.10591 0.12179 0.92038]]
[0.01896, 1.0, 0.26416] -> [[0.10763 0.12263 0.91782]]
[0.03633, 1.0, 0.37903] -> [[0.10798 0.12269 0.91724]]
 

----------------------

소규모 신경망으로 할 수 있는 것은 별로 없다는 것을 알 수 있다. 파이썬으로 작성한 코드가 작동 한다는 점만 확인하는데 만족하자. 대규모 네트워크를 구성하여 방대한 자료를 기반으로 학습해야 한다. 최소한의  파이썬을 익혔다. 다음에는 본격적으로 손글씨 이미지를 받아 훈련시키고 인식하는 신경망을 작성해 보기로 하자.

 


[내 뉴럴 네트워크 만들기/2부] 1. 최소한의 파이썬(찬조출연: C++)

[내 뉴럴 네트워크 만들기/2부] 1. 최소한의 파이썬(찬조출연: C++)

1. 개요

2. 준비 및 실행환경

    2-1. 필요한 모듈 설치
    2-2. 파이썬 실행
    2-3. 파이썬 프롬프트에서 리눅스 명령 실행

3. 최소한의 파이썬

    3-1. 파이썬 인터프리터 환경에서 문장 실행
    3-2. for 반복문
    3-3. 함수
    3-4. 배열 다루기 모듈 numpy
    3-5. 배열 시각화 모듈 matplotlib
    3-6. 객체 크래스

4. 실습

    4-1: 파이썬 예제
    4-2: C++과 비교 예제

----------------------------------------------------------------------------------------------
[참고서] Make Your Own Neural Networks, Tariq Rashid [book][검색링크]
----------------------------------------------------------------------------------------------

1. 개요

파이썬(python)은 매우 높은 추상화 수준의 컴퓨팅 언어다. 세상에서 가장 '시작하기 쉬운' 컴퓨팅 언어라는 말도 있다. 수많은 라이브러리(모듈, 패키지)들이 준비되어 있어서 적은 양의 코딩으로도 멋진 응용 소프트웨어를 제작할 수 있다. 파이썬(python) 프로그래밍 언어가 '인공지능'에 적합하다고 들 한다. 이는 남이 만들어 놓은 '인공지능' 용 모듈 자원이 많이 축적되어 있기 때문이지 이 언어가 인공지능에 특화된 프로그래밍 언어라는 뜻은 아니다. 하지만 '시작하기' 쉽다고 '잘하기' 쉽다는 말은 아니다. 어쨌든 다들 파이썬을 부르짖으니 이참에 알아보기로 한다. 프로그래밍 언어는 '거기서 거기다.' 실습을 통해 파이썬과 C++ 언어를 비교해보자.

2. 준비 및 실행환경

2-1. 필요한 모듈 설치

리눅스(WSL 가상 머신을 포함해서)환경에서 파이썬 언어를 사용하려면 다음과 같은 소프트웨어들을 설치해야 한다. 리눅스 명령줄에서 파이썬 언어 실행환경(인터프리터)을 설치한다.

sudo apt install python3
sudo apt install python3-dev
sudo apt install python3-pip

과학 및 수학, 통계 그리고 자료 시각화 처리에 유용한 기본 모듈을 설치한다.

sudo apt install python3-numpy
sudo apt install python3-scipy
sudo apt install python3-matplotlib

아래 모듈들은 명령줄 환경을 좀더 편리하게 운영할 수 있도록 해준다. 웹 브라우져에서 쥬피터 노트북(Jupyter Notebook)을 실행 시킬 수 있다. 필수 사항은 아니다.

sudo apt install python3-ipython
sudo apt install python3-ipykernel

2-2. 파이썬 실행

파이썬은 '인터프리터' 언어다. 리눅스 쉘 명령줄에서 python3 을 실행 하면 프롬프트가 나타날 것이다.

$ 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.

>>>

2-3. 파이썬 프롬프트에서 리눅스 명령 실행

파이썬 프롬프트에서 리눅스 명령 실행하려면 os 모듈을 들여와야 한다. 운영체제에서 제공하는 표준 입출력 <stdio> 함수들을 사용할 수 있다.

>>> import os
>>> os.system("ls -l")

total 24
-rw-r--r-- 1 mychip mychip 135 Apr  7 11:37 arrays.py
-rw-r--r-- 1 mychip mychip 594 Apr  7 12:09 class.py
-rw-r--r-- 1 mychip mychip  51 Apr  7 11:20 for_loop.py
-rw-r--r-- 1 mychip mychip  85 Apr  7 11:22 for_sqr.py
-rw-r--r-- 1 mychip mychip 259 Apr  7 11:44 plotting_arrays.py
-rw-r--r-- 1 mychip mychip 193 Apr  7 12:13 Skeleton_Code.py

현재 디렉토리에 파이썬 파일 'array.py'을 일어 프린터 해보자.

>>> print(open('./arrays.py').read())
import numpy
a = numpy.zeros([3,2])
print(a)
a[0,0] = 1
a[0,1] = 2
a[1,0] = 9
a[2,1] =12
print(a)
print(a[0,1])
v = a[1,0]
print(v)

파이썬 프롬프트에서 미리 작성한 스크립트 파일을 읽어 실행 하려면 exec() 함수를 사용한다.

>>> exec(open('./arrays.py').read())
[[0. 0.]
 [0. 0.]
 [0. 0.]]
[[ 1.  2.]
 [ 9.  0.]
 [ 0. 12.]]
2.0
9.0

또는, 파이썬 스크립트 파일을 모듈로 들여와 실행 시킬 수 있다.

>>> import arrays

3. 최소한의 파이썬

Jupyter Notebook, MS Code 등이 편리한 파이썬 실행 환경을 제공한다. 파이썬 명령 줄 환경도 괜찮다. 파이썬 명령 줄 프롬프트는 '>>>' 다.

3-1. 파이썬 인터프리터 환경에서 문장 실행

파이썬의 문장의 기초,

- 파이썬은 문장의 마침표가 따로 없이 '엔터'로 끝난다.
- 제어문의 구역 시작은 콜론(:) 이다.
- 제어문이 영향을 미치는 구역의 표시는 들여쓰기다. 문장 앞에 공백문자(스페이스 또는 탭)넣는다.
- pass 는 아무런 실행내용이 없는 '자리채움'일 뿐이다. 제어 영역의 마침을 표시한다.
- 샵(#) 문자 뒤는 주석(comment)이다.

>>> 2+3
5

>>> print("Hello World!")
Hello World!

>>> x=10
>>> print(x)
10

>>> print(x+5)
15

>>> y=x+7
>>> print(y)
17

>>> print(z)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'z' is not defined

>>> list(range(10))
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

>>> 

3-2. for 반복문

>>> for n in range(10):
...  print(n)
...  pass
...

0
1
2
3
4
5
6
7
8
9

파일 for_loop.py 에 작성한 for 반목문을 읽어서 실행,

>> print(open('./for_loop.py').read())
# For 반복문
for n in range(10):
    print(n)
    pass
print("done")

>>> exec(open('./for_loop.py').read())
0
1
2
3
4
5
6
7
8
9
done

3-3. 함수

모든 컴퓨팅 언어가 그렇듯 함수는 이름과 인수 그리고 되돌림 값을 가진다. 함수를 정의하는 def, 되돌림 return은 파이썬의 키워드다.

>>> import os

>>> os.system("cat func_avg.py")
# Function that takes 2 numbers as input
# and outputs their average
def avg(x,y):
    print("first input is: ", x)
    print("second input is: ", y)
    a = (x+y)/2.0
    print("average is ", a)
    return a

0

>>> import func_avg
>>> func_avg.avg(4,7)
first input is:  4
second input is:  7
average is  5.5

5.5

>>>

변수의 선언과 운영은 컴퓨팅 언어를 대할 때 입문자에게 가장 큰 장벽이다. C++ 같은 언어는 프로그래머에게 컴퓨터의 메모리 운영방식, 예를들어 다양한 자료형과 포인터 같은 이해를 요구하며 매우 까다롭다. 이에 비해 파이썬은 변수형을 따로 선언 할 필요도 없으며 할당 되는 자료형식에 의해 '알아서' 메모리를 운영해준다. 이는 파이썬이 입문하기 '쉬운' 인상은 주는 요인중 하나다.

3-4. 배열 다루기 모듈 numpy

numpy는 통계, 수치해석 등의 응용에서 다차원 배열 객체를 수월하게 다룰 수 있도록 해주는 파이썬 모듈(라이브러리)이다. 배열 값을 0으로 채운 5행 6열 배열 객체를 생성하는 예는 다음과 같다.

>>> import numpy
>>> a = numpy.zeros([5,6])
>>> print(a)
[[0. 0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0. 0.]]

대괄호([])는 배열에 접근할 때 사용된다.

>>> a[0,0] = 1
>>> a[3,4] = 2
>>> a[5,5] = 3

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IndexError: index 5 is out of bounds for axis 0 with size 5

>>> a[4,5] = 3
>>> print(a)
[[1. 0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 2. 0.]
 [0. 0. 0. 0. 0. 3.]]

3-5. 배열 시각화 모듈 matplotlib

파이썬 모듈 matplotlib는 행렬 데이터의 시각화에 유용하다. 5행 6열 2차원 배열 객체를 만들어 임의의 값을 저장한 후 값에 해당하는 가장 가까운 색으로 그림을 그리는 파이썬 스크립트 plotting_arrays.py는 다음과 같다.

# Plotting Array Example
# Filename: plotting_arrays.py

import numpy

arr = numpy.zeros([5,6])

(nRows,nCols)=arr.shape
print('Array has ',nRows, 'x', nCols)

for i in range(nRows):
  for j in range(nCols):
    arr[i][j] = int(numpy.random.rand()*100)
    pass
  pass

print(arr)

import matplotlib.pyplot

matplotlib.pyplot.imshow(arr, interpolation="nearest")

matplotlib.pyplot.show()

위의 파이썬을 실행 시켜보자.

>>> import plotting_arrays
Array has  5 x 6
[[81. 44. 98. 82. 13. 77.]
 [63. 84. 42. 48. 50. 54.]
 [31. 86. 60. 32. 91. 58.]
 [49. 81.  3. 53. 22. 67.]
 [34. 97. 13. 45. 50. 26.]]

3-6. 객체 크래스

파이썬에서 복합 객체를 사용할 수 있다.

# Filename: ex_class.py
# Class for Dog object

class Dog:

    # Initializer or constructor of the class
    def __init__(self, petname, temp):
        self.name = petname
        self.temperature = temp
        pass

    # Member function status()
    def status(self):
        print("Name: ", self.name)
        print("Temp: ", self.temperature)
        pass

    def setTemperature(self, temp):
        self.temperature = temp
        pass

    # This Dog can bark()
    def bark(self):
        print("woof!")
        pass

    pass

크래스 사례화 방법은 크래스를 함수처럼 호출한다. 이는 크래스의 초기화 함수 __init()__ 를 실행하게 되는데 C++ 언어에서 구성자(constructor)와 같다. 크래스의 소속 함수를 통해 객체에 접근 한다.

>>> exec(open('dog_class.py').read())

>>> sizzles=Dog("sizzles", 37)

>>> sizzles.status()
Name:  sizzles
Temp:  37

>>> sizzles.bark()
woof!

----------------------------------------------

[실습1] 아래의 파이썬 소스 파일들을 읽고 실행시켜보자.

1. for_loop.py

2. for_sqr.py

3. func_avg.py

4. arrays.py

5. plotting_arrays.py

6. dog_class.py

이정도 파이썬을 이해했다면 신경망을 시작해 보기에 충분한하다고 한다. 파이썬이 시작하기 쉽기는 한 모양이다. 

[실습2] 아래의 C++ 소스 파일들을 파이썬과 비교해보자.

1. arrays.cpp

2. for_sqrt.cpp

3. func_avg.cpp

4. plotting_arrays.cpp

5. dog_class.cpp

파이썬에 비하면 C++ 는 좀더 신경쓸 일이 많다. 이는 파이썬이 C++에 비해 '직관적(척! 보고 인간의 의도를 알아서 처리해준다)'이라는 뜻이며 추상성이 높다는 뜻이기도 하다.