신경망

Opusdeisong·2024년 4월 29일
0

밑바닥시리즈

목록 보기
2/6

신경망

그냥 다층으로 퍼셉트론을 단순히 쌓는 것이 신경망처럼 보인다. 최소한 나는 그렇게 생각하지만 이 과정에 추가로 가중치와 편향을 자동으로 개선하는 과정을 통해서 진정한 신경망이 완성되는 것 같다. 학교 수업이 나름대로 잘 짜여져 있다는 생각이 드는게 인지계 수업 시간에 다 어느정도는 해 보았던 내용이라서 그런지 좀 편하게 읽을 수 있었다.(찬양해 갓기혁!)

활성화 함수

임계값을 기준으로 출력이 바뀌는 함수를 뜻하는데 계단 함수와 관련된 내용은 과감하게 쳐내고 넘파이 배열로 받는 입력을 한 번에 처리하는 method만 간단하게 소개해보려고 한다. 우선 코드부터 보자.

def step_function(x):
	y = x > 0 
   #혹시 오류가 난다면 걍 인덴트 지웠다가 하면 됩니다.
   # T or F가 넘파이 어레이 안에 들어가고 
   # 아래는 그를 1 or 0으로 변환
   return y.astype(np.int)

결국 시그모이드 함수가 핵심이기 때문에 시그모이드 함수를 구현하면 아래와 같다.

def sigmoid(x):
	return 1 / (1 + np.exp(-x))

뭐 이거야 그냥 하면 되는 것이니 브로드캐스트고 뭐고 그런건 패스하겠다.
이 시그모이드 함수가 결국 퍼셉트론과 신경망의 차이를 보여주는 것 같다. 비슷한 성질을 갖고 있는 두 함수지만 시그모이드는 비선형 함수이고 이것이 신경망 함수만이 갖는 특징을 대변해준다고 볼 수 있다.
이후에 ReLU 함수라고도 뭐가 나오긴 하는데 가볍게 무시해주고 넘어갔다.

다차원 배열의 계산

사실 신경망 자체를 코드로 짜려면 계산 양 자체가 많을거라고 생각한다. 당장 회사에서 돌리는 단순 전처리 코드만 해도 다차원 배열의 계산양이 어마어마하기 때문에 공부한다는 느낌으로 문제를 풀어보았다. 우선 기본적인 넘파이 메쏘드를 기준으로 간단하게 정리해보았다.

A = np.array([~~])
B = np.array([~~])
np.ndim(A) # 몇 차원 배열인지 출력
A.shape #튜플 형태로 출력 (n,m)
np.dot(A,B) # 행렬 곱 자체를 진행

위의 것들을 활용하여 간단한 신경망에서의 행렬곱 연산을 정리하여 보았다.

신경망에서의 행렬 곱

우선 기본적으로 한 개의 층에서 다른 층으로 갈 때 각각의 칸에서 가려는 층으로 모두 가중치 계산을 해줘야 하므로 입력 층(X), 출력 층(Y)이 있으면 X * Y 로 되어야 한다.

import numpy as np
X = np.array([1,2])
W = np.array([[1,3,5], [2,4,6]])
Y = np.dot(X,W)
print(Y)

한 개의 출력층은 다음과 같이 된다.

3층 신경망 구현하기

위의 것을 바탕으로 3층 신경망을 간단하게 구현해보려고 한다. 우선 입력층은 0층이니 이 부분을 감안해서 코드를 구현하였다. 간단하게 내 맘대로 구현한 것은 아래와 같다.

# 0층에서 1층
X = np.array([1, 2])
W = np.array([[1,2,3],[4,5,6]])
N = np.dot(X, W)
print(N)
# 1층에서 2층
W = np.array([[1,2],[3,4],[5,6]])
NN = np.dot(N, W)
print(NN)
# 2층에서 3층
W = np.array([[1,2],[3,4]])
Y = np.dot(NN, W)
print(Y)

솔직히 이렇게 단순히 구현하는거는 지나가던 중학생도 파이썬만 가르쳐주면 할 수 있다. 그러면 책에서 나온대로 조금 더 고차원적으로 작성해보자. 우선 현재는 가중치만 들어가 있지만 B도 들어가야 하고, 활성화 함수도 사용해야 하니 이 두 부분을 포함하여 작성해보자. 코드는 89페이지의 구현 정리를 참고하였지만 작성자체는 그냥 원리만 이해하고 내가 해보았다(근데 걍 코파일럿이 대충 상상하면 바로 짜줘서 좀 싱겁긴 했다. 다들 사용하시기를 권장한다.) 시그모이드 함수는 위에서 선언했다고 가정하였다. 솔직히 귀찮아서 좀 대충 짰기 때문에 혹시 뭔가 이상하거나 틀린 점이 있다면 말해줬으면 좋겠다...

def initialize_network():
    network = {}
    network['W1'] = np.array([[0.1, 0.2, 0.3], [0.4, 0.5, 0.6]])
    network['W2'] = np.array([[0.1, 0.2], [0.3, 0.4], [0.5, 0.6]])
    network['W3'] = np.array([[0.1, 0.2], [0.3, 0.4]])
    network['b1'] = np.array([1, 1, 1])
    network['b2'] = np.array([0.5, 0.5])
    network['b3'] = np.array([0.5, 0.6])
    return network

def forward_propagate(network, X):
    A = X
    network['A1'] = A
    Z1 = np.dot(A, network['W1']) + network['b1']
    network['Z1'] = Z1
    A1 = sigmoid(Z1)
    network['A1'] = A1
    Z2 = np.dot(A1, network['W2']) + network['b2']
    network['Z2'] = Z2
    A2 = sigmoid(Z2)
    network['A2'] = A2
    Z3 = np.dot(A2, network['W3']) + network['b3']
    network['Z3'] = Z3
    return Z3

X = np.array([1, 2])
Y = forward_propagate(initialize_network(), X)
print(Y)

자 뭐 위에서도 적었지만 여기까지는 별찍기 정도의 수준 밖에 되지 않는다. 하지만 이 각각의 가중치들을 자동으로 학습 시키기 위해서는 어떤 함수를 사용해야 할까?하는 고민을 하면서 다음 챕터로 넘어갔다.

출력층 설계하기

뭐지 다 짰던게 아닌가 출력층을 또 설계해야하나? 회귀에서는 항등 함수를 사용한다고 한다. 그러면 분류에서는 뭘 사용하느냐고 하면 소프트맥스 함수를 사용한다고 한다. 소프트맥스 함수 구현은 아래와 같다.

def softmax(a):
   exp_a = np.exp(a)
   sum_exp_a = np.sum(exp_a)
   y = exp_a / sum_exp_a
   return y

다만 이 방법은 오버플로의 문제가 있다고 한다. 그래서 최댓값으로 빼주는 것을 진행한다고 한다.

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

위를 통해서 출력층들의 값을 확률론적으로 접근할 수 있다고 한다. 그 유명한 손글씨 숫자 인식으로 아래에서 연습을 해보도록 하자.

손글씨 숫자 인식

손글씨를 인식하는건 되게 유명한 문제인데 이 사이트에서 보면 좀 더 재밌게 읽었던 기억이 나서 강추한다. 이 부분은 교재 그대로 따라가지는 않았다. 그래도 진행 과정에서 순전파가 어떤 방식으로 분류하고 계산되는지에 더 집중했던 것 같다. 할 줄 아는게 별로 없어서 데이터를 토대로 TF 모델을 만들어서 이를 임포트해서 그림판 같은 느낌으로 인식하는 모델을 제작해보기로 하였다. GUI는 PyQt를 활용해서 제작해보려고 한다. 코드는 아래와 같다. 언제나 그랬듯이 주석은 위대하신 GPT 선생님의 도움을 받았다.

# TensorFlow와 tf.keras를 임포트합니다  
import tensorflow as tf  
from tensorflow.keras import layers  
  
# 데이터셋을 로드합니다  
mnist = tf.keras.datasets.mnist  
(x_train, y_train), (x_test, y_test) = mnist.load_data()  
  
# 픽셀 값을 0~1 사이로 정규화합니다.   
# 신경망 모델에서는 입력값을 작게 유지하는 것이 중요하며, 이렇게 하면 학습 과정이 더 빠르고 더 나은 성능을 달성할 수 있습니다.  
x_train, x_test = x_train / 255.0, x_test / 255.0  
  
# 층을 차례대로 쌓아 tf.keras.Sequential 모델을 만듭니다.   
# tf.keras.Sequential 모델은 층(layer)를 순서대로 쌓은 것으로, 각 층에서 이전 층의 출력을 입력으로 받아 처리합니다.  
model = tf.keras.models.Sequential([  
  tf.keras.layers.Flatten(input_shape=(28, 28)), # 28x28 픽셀의 이미지를 1차원으로 평탄화합니다.  
  tf.keras.layers.Dense(128, activation='relu'), # 128개의 노드(뉴런)을 가진 Dense 층  
  tf.keras.layers.Dropout(0.2), # 오버피팅을 막기 위한 Dropout 층. 20%의 노드를 무작위로 버립니다.  
  tf.keras.layers.Dense(10) # 10개 노드의 출력층. 이 층의 각 노드는 현재 이미지가 10개 클래스 중 하나에 속할 확률을 출력합니다.  
])  
  
# 모델이 훈련되도록 설정합니다.   
# 이 단계에서는 손실 함수(loss function), 최적화 알고리즘(optimizer), 그리고 모델의 성능을 측정할 메트릭(metrics)를 설정합니다.  
model.compile(optimizer='adam',  
              loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),  
              metrics=['accuracy'])  
  
# 모델을 훈련 데이터에 적합시킵니다.   
# 여기서 에포크(epoch)는 전체 데이터셋을 통과하는 횟수를 의미합니다.  
model.fit(x_train, y_train, epochs=5)  
  
# 모델을 테스트 데이터에 대해 평가합니다.  
model.evaluate(x_test,  y_test, verbose=2) 

# 모델을 저장합니다.
model.save('model1.h5')
 

그럼 위 방법으로 학습시킨 모델을 PyQt를 활용해서 테스트할 수 있게 제작해보았다. 깃허브에도 올려두었으니 해볼 사람들은 해보면 될 것 같다. 뭔가 코드가 이상한지 조금 오류가 나긴 한다,....ㅜㅜ

import sys  
from PyQt5.QtWidgets import *  
from PyQt5.QtGui import *  
from PyQt5.QtCore import * 
import tensorflow as tf 
import numpy as np
# 모델 로드  
model = tf.keras.models.load_model('model1.h5')


class MyApp(QMainWindow):  
  
    def __init__(self):  
        super().__init__()  
  
        # Canvas 설정  
        self.image = QImage(QSize(400, 400), QImage.Format_RGB32)  
        self.image.fill(Qt.white)  
  
        self.drawing = False  
        self.brush_size = 30  
        self.brush_color = Qt.black  
  
        self.last_point = QPoint()  
  
        # GUI 설정  
        self.initUI()  
  
    def initUI(self):  
        menubar = self.menuBar()  
        menubar.setNativeMenuBar(False)  
        filemenu = menubar.addMenu('File')  
  
        predict_action = QAction('Predict', self)  
        predict_action.triggered.connect(self.predict)  
  
        clear_action = QAction('Clear', self)  
        clear_action.triggered.connect(self.clear)  
  
        filemenu.addAction(predict_action)  
        filemenu.addAction(clear_action)  
  
        self.setWindowTitle('MNIST Recognizer')  
        self.setGeometry(300, 300, 400, 400)  
        self.show()  
  
    def paintEvent(self, e):  
        canvasPainter  = QPainter(self)  
        canvasPainter.drawImage(self.rect(),self.image, self.image.rect() )  
  
    def mousePressEvent(self, e):  
        if e.button() == Qt.LeftButton:  
            self.drawing = True  
            self.last_point = e.pos()  
  
    def mouseMoveEvent(self, e):  
        if (e.buttons() & Qt.LeftButton) & self.drawing:  
            painter = QPainter(self.image)  
            painter.setPen(QPen(self.brush_color, self.brush_size, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin))  
            painter.drawLine(self.last_point, e.pos())  
            self.last_point = e.pos()
            self.image = self.image.copy()  
            self.update()  
  
    def mouseReleaseEvent(self, e):  
        if e.button() == Qt.LeftButton:  
            self.drawing = False  
  
    def predict(self):  
    # 그림을 모델에 전달하고 예측을 반환  
        data = self.image.scaled(28, 28).convertToFormat(QImage.Format_Grayscale8)   
        ptr = data.bits()  
        ptr.setsize(data.byteCount())  
        img = np.frombuffer(ptr, np.uint8).reshape(data.height(), data.width())  
        img = img / 255.0  # Normalize to [0,1]  
        img = np.expand_dims(img, axis=0)  # Expand dimension to specify batch size  
        img = np.expand_dims(img, axis=-1)  # Expand dimension to specify number of channels  
      
        result = model.predict(img)  
        predicted_number = np.argmax(result[0])  
        QMessageBox.information(self, "Prediction", "Predicted number is {}".format(predicted_number))  
  
    def clear(self):  
        self.image.fill(Qt.white)  
        self.update()  
  
  
if __name__ == '__main__':  
    app = QApplication(sys.argv)  
    ex = MyApp()  
    sys.exit(app.exec_())  
profile
Dorsum curvatum facit informaticum.

0개의 댓글