컴퓨터공학/인공지능

개인 프로젝트 - 모델 도구를 만들어보자(1) : 뉴런

dori3220 2024. 12. 21. 01:51

계기

 

 처음엔 '정말 정말 작고 단순한 프로젝트 하나를 진행해 볼까?' 하고 생각했습니다. 근래에 선형 회귀에 대해서 공부했기도 하고 관련된 무언가가 좋을 것 같아서 다층 퍼셉트론을 만들어보면 좋을 것 같았죠. 특히 2개 이상의 레이어로만 구현 가능한 XOR 게이트를 만들어 보는 것이 좋을 것 같았죠. 그렇게 GateGenerator 클래스를 하나 만들고 그 안에 모든 것을 구현하려고 욱여넣다 보니 잘 풀리지 않았습니다. 정말 하루를 꼬박 새워서 머리를 싸매니까 미칠 것 같았습니다. 이렇게 간단한 신경망도 나 스스로 구현할 능력이 없다니! 무언가를 부정당한 느낌이었습니다. 다시 처음부터 차근차근 시작해 보기로 마음먹고 처음 생긴 목표는 "뉴런 하나라도 제대로 구현해 보자"였습니다. 그렇게 한 단계씩 발전시켜 가며 저의 지식도 점점 발전하고 구현할 수 있는 기능도 점차 넓어졌습니다. 그 첫 단추에 있는 뉴런을 만든 과정! 함께 보실까요?

 


 

인공지능에 대한 고찰

 

 ChatGPT 출시 이후 인공지능이 폭발적인 관심을 받고 있습니다. 인공지능에 대한 다양한 정의들이 있지만 저는 '사람의 이해, 학습, 추론을 모방하여 문제를 해결하는 능력 혹은 기능'이라고 하겠습니다.  지금부터는 오로지 저의 생각입니다. (나중에 기회가 된다면 철학 카테고리를 만들어서 철학에 대해서도 다루겠습니다.) 지식은 데이터라고 생각합니다. 데이터는 그 자체로 의미를 가지지 못합니다. 데이터 과학자들은 데이터를 가지고 우리에게 쓸모 있는 정보를 만듭니다. 혹은 한 개인이 데이터 간의 관계를 따져보고 정보를 만들기도 합니다. 정보에는 의미가 담겨있습니다. 지능은 지식을 활용하는 능력이라고 생각합니다. 즉, 정보를 만드는 능력이라고 할 수 있겠네요. 따라서 인공지능은 정보를 만드는 능력 혹은 기능이라고 생각합니다.

 

인공지능 다이어그램

 

 인공지능은 밑에 다양한 서브셋을 가지고 있습니다. 몇 십 년간 수많은 전문가들이 연구한 끝에 딥러닝 분야가 탄생했습니다. AlphaGo와 ChatGPT도 딥러닝을 통해서 만들어졌죠. 딥러닝의 가장 큰 특징은 인공 신경망을 구현한다는 점입니다. 우리의 뇌를 모방한 인공 신경망은 뇌의 구조 및 기능과 매우 흡사합니다. 뉴런을 가지고 있고 뉴런끼리 신호를 전달하죠. 여러 개의 뉴런들이 데이터를 전달하고 결론을 도출해서 정보를 만듭니다. 인간의 지능을 모방한 것이죠. 그러면 어떻게 컴퓨터로 이것을 구현한다는 걸까요?

 


 

뉴런을 어떻게 만들지?

 

 처음에는 정말 많은 질문들이 생겼습니다. 어떻게 컴퓨터로 인간의 뇌를 모방한 다는 거지? 뉴런은 어떻게 구현하지? 저는 복잡한 것을 정말 싫어해서 단순하게 요약하는 것을 즐기는데요. 이 프로젝트를 진행하면서 알게 된 정말 강력한 문장 하나가 있습니다.

뉴런은 함수다.

 

이 말 하나로 뉴런에 대한 설명은 끝입니다. 뉴런은 입력을 받고 어떠한 계산을 거친 뒤 출력을 뱉는 함수로 구현됩니다. 이러한 뉴런들이 모여서 층(Layer)을 이루고 층들이 모여서 모형(model)을 이룹니다. 이 모델은 어떠한 정보를 제공하는 기능을 합니다. 즉, 지능이 되는 것이죠. 딥러닝에 대해서 공부할 때마다 모델이란 단어를 많이 접하는데 저는 하나의 지능이라고 생각하고 있습니다.

 

 

 조금 더 자세히 들여다보겠습니다. 하나의 뉴런은 하나의 함수입니다. 그렇다는 것은 변수가 있고 곱하고 더해지는 매개 변수(parameters)가 있다는 것이죠. 어떠한 매개 변수들이 있는지 예시를 통해 알아볼까요. 하나의 입력(x)이 들어오는 하나의 뉴런을 생각해 봅시다. 뉴런은 들어오는 입력에 대해서 곱하는 값이 있습니다. 그것을 가중치(weights)라고 합니다. 하나의 입력 x에 대해서 가중치 w를 곱해줍니다. 그리고 편향(bias)이라는 값도 있습니다. 편향은 뉴런이 가지고 있는 말 그대로 편향입니다. 뉴런이 어느 쪽으로 편향되어 있는지 이 값을 통해서 나타낸다고 이해하고 넘어갑시다.

z(x) = w*x + b

 

이렇게 매개 변수들을 곱하고 더한 함수를 z라고 표기합니다. 입력이 늘어나면 가중치도 따라서 늘어납니다. 이때 편향은 뉴런 당 하나이므로 헷갈리시면 안 됩니다. z는 바로 출력으로 가지 않고 최적화 함수(optimizer)를 거치게 됩니다. 최적화 함수는 모델의 성능을 향상시키기 위해서 사용하는 함수입니다. 자세한 내용은 선형 회귀에 대해서 다룰 때 하도록 하고 일단 넘어가겠습니다. 대표적으로 sigmoid함수, ReLU함수가 있습니다. 최적화 함수는 h라고 표기합니다. sigmoid 함수가 가장 대표적이기 때문에 예시로 들겠습니다.

h(z) = sigmoid(z) = 1/(1+e^-z)

 

이렇게 최적화 함수까지 거치면 비로소 출력으로 내보냅니다. 두 개의 함수를 거쳐서 출력이 됐습니다. 정말 중요한 사실입니다. 이후 나올 역전파 알고리즘에서 미분의 연쇄 법칙이 나오거든요. 

 


 

뉴런을 어떻게 학습시키지?

 

 프로젝트 관련해서 글을 쓰려고 했는데 점점 강의처럼 변해가고 있는 기분이 드네요 ㅎㅎ;; 나중에 선형 회귀에 대해서 글을 쓸 예정이기 때문에 자세한 설명은 넘어가겠습니다. 크게 골자만 살펴볼게요. 결국 뉴런을 학습시킨다는 건 파라미터 값을 정답에 맞게 조정한다는 의미입니다. 즉, 가중치(w)와 편향(b)의 값을 계속해서 조금씩 더하고 빼주면서 정답에 가깝도록 만들어주는 것이 학습입니다.

 

 순전파(Feed forwarding)는 앞선 예시처럼 입력을 두 개의 함수를 통과시켜서 출력을 얻는 것을 말합니다. 순서대로 정방향으로 계산을 한다는 의미이죠. 순전파를 통해서 나온 출력값은 해당 모델의 예측값입니다. 예측이 정말 맞는지 정답과의 차이를 계산하는 함수를 손실 함수라고 합니다. 역전파(Back propagation)는 손실 함수의 도함수 값(정답에서 멀어진 정도)에 비례해서 매개 변숫값을 더하거나 빼는 것을 말합니다. 학습은 순전파와 역전파의 반복으로 이루어져 있습니다.

 


 

구현

 

 우선 뉴런의 구조를 기술할 Neuron 클래스를 정의하겠습니다. 패키지는 numpy, matplotlib, random, time을 사용했습니다.

import numpy as np
import matplotlib.pyplot as plt
import random
import time

class Neuron:

    def __init__(self, act='sigmoid', lr=0.1):
        self.act = act.lower()
        self.lr = lr
        random.seed(time.time())
        self.w = random.random()
        self.b = random.random()

클래스 객체의 생성과 동시에 act(활성화 함수)를 정해주도록 했습니다. 그리고 학습률도 선언과 동시에 정해집니다. 생성자에서 임의의 w(가중치)와 b(편향) 값을 만들어 줍니다.

opt : 어떤 활성화 함수를 쓸지 입력받는 문자열 형 변수
lr : 학습률을 받는 실수형 변수
w : 가중치를 저장할 실수형 변수
b : 편향을 저장할 실수형 변수

 

 활성화 함수와 그 도함수들도 미리 정의해두겠습니다. 일단 기본적인 sigmoid와 ReLu만 넣어봤습니다.

    def sigmoid(self,x):
        return 1/(1+np.exp(-x))
    
    def dsigmoid(self,x):
        return x*(1-x)
    
    def relu(self,x):
        return np.maximum(0, x)
    
    def drelu(self,x):
        return np.where(x > 0, 1, 0)

 

순전파 함수도 만들었습니다.

    def forward(self,x):
        z = self.w * x + self.b
        if self.act == 'sigmoid':
            h = self.sigmoid(z)
            return h
        elif self.act == 'relu':
            h = self.relu(z)
            return h
        else:
            return z

위에서 설명한 뉴런의 구조대로 두 개의 함수를 통과하고 h 값을 반환합니다.

 

 최적화 함수는 경사 하강법, 비용 함수는 mse를 사용했습니다. 역전파 함수도 만들었습니다.

    def mse(self,y,pred_y):
        return (y-pred_y)**2
    
    def backward(self,x,y,y_pred):
        dL_dh = -2*(y - y_pred)
        dz_dw = x
        if self.act == 'sigmoid':
            dh_dz = self.dsigmoid(y_pred)
            dL_dw = dL_dh * dh_dz * dz_dw
            self.b -= self.lr * dL_dh * dh_dz
        elif self.act == 'relu':
            dh_dz = self.drelu(y_pred)
            dL_dw = dL_dh * dh_dz * dz_dw
            self.b -= self.lr * dL_dh * dh_dz
        else:
            dL_dw = dL_dh * dz_dw
            self.b -= self.lr * dL_dh
        
        self.w -= self.lr*dL_dw

역전파에 대해서 공부하면서 제가 역전파를 완전히 잘못 알고 있었다는 사실을 알았습니다. (그냥 mse를 학습률과 곱해서 빼주면 되는줄 알고 있었습니다ㅠㅠ) 미분의 연쇄 법칙을 이용해서 z를 w에 대해서 미분한 값과 h를 z에 대해서 미분한 값과 손실 함수를 h에 대해서 미분한 값을 모두 곱해서 학습률과 함께 가중치에서 빼주었습니다. h는 sigmoid와 ReLU인 경우 두 경우에 대해서 if문으로 분기해 줬습니다. 

 

 그리고 객체가 자체적으로 학습할 수 있는 train 메서드를 만들었습니다.

    def train(self,x,y,epochs=10000,target_loss=0.0,show=False):
        if show:
            print("**********train start**********")
        errs = []
        for epoch in range(epochs):
            y_pred = self.forward(x)
            err = self.mse(y, y_pred)
            errs.append(err)
            self.backward(x, y, y_pred)
            if err <= target_loss:
                break
            if show and epoch%100==0:
                print(f"epoch: {epoch} >> err: {err:.4f}")
        if show:
            print(f"epoch: {epoch} >> err: {err}")
            print("**********train done**********")
            plt.plot(errs)
            plt.show()
x : 입력값
y : 출력값
epochs : 학습을 반복할 횟수 (디폴트 1만 회)
target_loss : 학습을 중간에 중지시키기 위한 최소 손실값
show : 시각화할지 여부

 

 이렇게 학습시킨 모델을 평가할 수 있도록 eval 메서드를 만들었습니다.

    def eval(self,x,y):
        y_pred = self.forward(x)
        acc = abs(y_pred/y)*100 if abs(y)>abs(y_pred) else abs(y/y_pred)*100
        print(f"input: {x} -> output: {y_pred}")
        print(f"acc: {acc}%")
        return acc

 


 

전체 코드입니다.

import numpy as np
import matplotlib.pyplot as plt
import random
import time

class Neuron:
    
    def __init__(self, act='sigmoid', lr=0.1):
        self.act = act.lower()
        self.lr = lr
        random.seed(time.time())
        self.w = random.random()
        self.b = random.random()
    
    def sigmoid(self,x):
        return 1/(1+np.exp(-x))
    
    def dsigmoid(self,x):
        return x*(1-x)
    
    def relu(self,x):
        return np.maximum(0, x)
    
    def drelu(self,x):
        return np.where(x > 0, 1, 0)
    
    def forward(self,x):
        z = self.w * x + self.b
        if self.act == 'sigmoid':
            h = self.sigmoid(z)
            return h
        elif self.act == 'relu':
            h = self.relu(z)
            return h
        else:
            return z
    
    def mse(self,y,pred_y):
        return (y-pred_y)**2
    
    def backward(self,x,y,y_pred):
        dL_dh = -2*(y - y_pred)
        dz_dw = x
        if self.act == 'sigmoid':
            dh_dz = self.dsigmoid(y_pred)
            dL_dw = dL_dh * dh_dz * dz_dw
            self.b -= self.lr * dL_dh * dh_dz
        elif self.act == 'relu':
            dh_dz = self.drelu(y_pred)
            dL_dw = dL_dh * dh_dz * dz_dw
            self.b -= self.lr * dL_dh * dh_dz
        else:
            dL_dw = dL_dh * dz_dw
            self.b -= self.lr * dL_dh
        
        self.w -= self.lr*dL_dw
        
    def train(self,x,y,epochs=10000,target_loss=0.0,show=False):
        if show:
            print("**********train start**********")
        errs = []
        for epoch in range(epochs):
            y_pred = self.forward(x)
            err = self.mse(y, y_pred)
            errs.append(err)
            self.backward(x, y, y_pred)
            if err <= target_loss:
                break
            if show and epoch%100==0:
                print(f"epoch: {epoch} >> err: {err:.4f}")
        if show:
            print(f"epoch: {epoch} >> err: {err}")
            print("**********train done**********")
            plt.plot(errs)
            plt.show()
    
    def eval(self,x,y):
        y_pred = self.forward(x)
        acc = abs(y_pred/y)*100 if abs(y)>abs(y_pred) else abs(y/y_pred)*100
        print(f"input: {x} -> output: {y_pred}")
        print(f"acc: {acc}%")
        return acc

 


 

테스트

 

 자 그럼 잘 작동이 되는지 시험해 볼까요? 입력으로 3을 주면 0.2를 출력하도록 뉴런을 학습시켜 보겠습니다. (최적화 함수를 sigmoid로 할 것이기 때문에 출력은 [0,1] 실수에서 임의로 0.2라고 설정했습니다.) 뉴런 객체 n1을 만들고 train 메서드로 학습시켜 보겠습니다. 이때 학습의 경과와 결과를 확인하기 위해 show 값을 True로 주겠습니다.

n1 = Neuron()
n1.train(3,0.7,show=True)
**********train start**********
epoch: 0 >> err: 0.0015
epoch: 100 >> err: 0.0000
epoch: 200 >> err: 0.0000
epoch: 300 >> err: 0.0000
epoch: 376 >> err: 0.0
**********train done**********

 

학습이 매우 잘되네요! 무려 377회에서 학습이 끝났습니다. plot을 통해 loss값의 변화도 살펴볼 수 있습니다.

loss값 변화

 

그렇다면 accuracy가 얼마나 나오는지 검증도 같이 해보도록 하겠습니다.

n1 = Neuron()
n1.train(3,0.7,show=True)
n1.eval(3,0.7)
**********train start**********
epoch: 0 >> err: 0.0647       
epoch: 100 >> err: 0.0000     
epoch: 200 >> err: 0.0000     
epoch: 300 >> err: 0.0000     
epoch: 400 >> err: 0.0000     
epoch: 431 >> err: 0.0        
**********train done**********
input: 3 -> output: 0.7
acc: 100.0%

정확도 100퍼센트를 자랑하는 뉴런이네요 ㅎㅎ


 

 사실 forward와 backward 메서드에 입력값, 출력값, 예측값을 매개 변수로 넣은 이유가 있습니다. forward와 backward 메서드가 독자적으로 사용되어야 하기 때문인데요. 이유는 2편에서 설명하도록 하겠습니다. 우선 어떻게 독자적으로 사용되는지 예시를 보겠습니다.

n2 = Neuron()
for _ in range(10000):
    h = n2.forward(2)
    n2.backward(2,0.5,h)
n2.eval(2,0.5)
input: 2 -> output: 0.5
acc: 100.0%

 

우선 train 메서드를 사용하지 않고도 이 두 함수만을 이용해서 뉴런을 학습시킬 수 있습니다. 앞서 "학습은 순전파와 역전파의 반복"이라고 말씀드린 바가 있습니다. 이를 가장 잘 나타낸 코드라고 생각이 드네요. 이 "원리를 가장 직관적이고 가시성 좋게 표현하기 위해서"가 첫 번째 이유가 되겠습니다. 그리고 다음 코드를 한 번 보겠습니다.

 

n3 = Neuron()
n3.train(1,0.32)
n3.eval(1,0.32)

print(n3.forward(1))
print(n3.forward(2))
input: 1 -> output: 0.3200000000000001
acc: 99.99999999999997%
0.3200000000000001
0.30556984674691506

 

정말 원하는 입출력을 학습시킨 뉴런에게 다른 입력값을 준 결과를 확인할 수 있습니다. "다양한 입력에 대한 결과를 확인할 수 있기 때문에"가 두 번째 이유입니다.

 


 

 이것저것 열심히 담고 설명하려다 보니 뭔가 짬뽕이 된 느낌입니다ㅠㅠ 그래도 어찌저찌 글을 마무리했네요! 정말 뿌듯합니다. 하루빨리 다음 글을 쓰고 싶네요! 다음은 뉴런들이 모여서 만들어진 Layer를 구현한 것에 대해서 써보도록 하겠습니다. 긴 글 읽어주셔서 감사합니다~