Deep Learning through deep learning

#1 ANN (Artificial neural network) - AND/OR/XOR 직접 구현 본문

ML&DL/ANN

#1 ANN (Artificial neural network) - AND/OR/XOR 직접 구현

NeuroN 2023. 8. 7. 15:38

딥러닝의 가장 근간이 되는 인공신경망 ANN 에 대해 다루고자 한다.

ANN 의 경우,

  1. Perceptron 이라는 것에 대해 시각적으로 이해하고,
  2. Input 이 Output으로 나가는 과정을 적어보고,
  3. 직접 구현해봄으로써 전체적인 틀을 이해할 수 있다고 생각한다.

ANN 은 이후의 모든 딥러닝 모델의 근간이 되므로 그 구조나 틀에 대해 어느정도 감을 잡고 들어간다면, 이후 오픈소스 활용에 있어서도 대강적인 구조 변경이 가능할것이라 생각한다.

물론 대중적으로 널린 세부적인 이론을 제외한 필수 이론만 보고 넘어가며 구현만 해볼 예정이니 코드를 참고하여 활용하길 바란다.


Theory

딥러닝이라는 용어 이전에 우리는 Perceptron이라는 용어를 알고 들어가야 한다.

Single Layer Perceptron

  • 간단히 말해 y = ax + b 의 수식을 따르는 1차식으로 구성되어 있다.
  • 일상생확 속 예로 들어 우리가 몸무게(y) 를 예측하고 싶을 때, 키(x)를 보고 대강 몸무게를 예측할 수 있다. 이때 키가 입력으로 들어가게되고, 키를 보고 몸무게를 유추할 수 있다는 아이디어이다.
  • 본격적으로 다루게 될 AND / OR 문제에서, 아래의 규칙에 따라 AND, OR gate를 퍼셉트론으로 구현이 가능하다. 단지 x가 하나 더 추가되어 집값을 예측하는 것처럼 주변 상거리(x1), 범죄율(x2)... 으로 집값(y)를 예측하는 것과 같다.
    • AND 는 x1, x2 가 둘다 1인 경우에 y = 1
    • OR 는 x1, x2 가 둘 중 하나라도 1이 있는 경우에 y = 1
  • AND / OR 문제와 같이 우리는 y = ax + b 라는 수식에서, x가 무수히 많아져도 y 하나에 대해 예측이 가능하다.

Multi Layer Perceptron

  • 문제는 y가 여러개인 경우에 발생한다. y1,y2... 처럼 많은 경우, 예시를 들어 3가지 종류의 꽃 종류를 판별한다고 할 때, 우리는 해바라기(y1),튤립(y2),장미(y3) 로 둘 수 있다. 하지만 이 경우, 위의 Single Layer Perceptron 은 사용할 수 없다.
  • 우리는 더 복잡한 수식을 다뤄야 하고, Multi Layer Perceptron 에선 입력 x가 여러개이면서 출력 y가 여러개 임에도 이를 가능하게 한다.
  • 대표적으로 XOR 문제를 들 수 있다.
    • XOR 는 x1, x2 가 둘 중 하나만 1인 경우 y = 1
    • 왜 XOR와 AND/OR 는 다른가 하고 의문을 가지는 분들께 간단하게 설명해드리고자 한다. AND/OR 는 x,y 평면상에 직선 하나로 1,0이 구분이 가능하다. XOR는 불가능하다.

즉, SLP / MLP 라는 용어가 여기서 나온 것이다. SLP 로는 단순하게 한가지 값을 예측하기 위해, MLP 로는 여러가지의 값을 예측하기 위해 사용한다고 이해하고 y의 개수 유무로 생각하면 된다.

 

Deep Learning

  • 딥러닝 구현을 위한 필수 내부 정보들을 살펴보고자 한다. 위의 퍼셉트론 구조만으로 우리는 값을 예측하는데 딥러닝이 사용될 수 있다는걸 알 수 있다.
  • Foward Propagation (순전파)
    • y = wx+b 라는 수식에서 (x,y가 여러개면 행렬), y 는 출력, x 는 입력, w는 가중치 (a대신), b는 편향이라고 칭하며 w,b 값을 조정해 x에 대한 입력에서 y에 대한 출력을 조정한다.
    • x가 입력되어 y로 출력되기 까지의 전 과정이 들어간다. (순전파에서 사용된 값으로 손실함수를 구할 수 있음)
  • Loss Function (손실 함수)
    • 학습을 위해 우리는 정답 y 를 가지고 있어야 하고, 학습을 통해 우리는 예측된 y (이하 y_hat)을 가지게 된다.
    • y와 y_hat의 차이를 수식으로 표현한 것이 손실함수이며, 이 차이가 적을수록 정답과 유사하다는 의미이다.
    • 즉, 손실함수가 작을수록, 정답과 유사하고, 정확도가 높아진다는 이야기가 된다.
    • 우리는 손실함수를 줄이는 것을 목표로 한다.
  • Back Propagation (역전파)
    • 위의 손실함수를 최저로 낮추기 위해 우리는 역전파를 이용한다.
    • 미분이라는 개념을 이용해 순전파와 반대방향으로 거슬러 올라가 w,b 의 값을 계속해서 업데이트 해주면서 출력 y를 조정한다. 조정된 y로 인해 손실 함수도 바뀌게 되면서 점점 최적화된 모델이 만들어진다.

이하의 내용은 더 많은 소스와 함께 구글에서 찾아볼 수 있지만, 이 정도만으로 우리는 딥러닝의 과정을 유추해볼 수 있다.

  1. 순전파로 우리는 값을 예측한다.
  2. 하지만 이 과정에서 오차가 많이 발생할 것이고 손실함수를 이용해 오차를 계산한다.
  3. 이 오차를 줄이기 위해 우리는 역전파를 이용해 오차를 줄여나간다.
  4. 그리고 순전파를 통해 다시한번 예측을 하면서 이 과정을 반복하는 것이다.

추가적으로 활성화함수, 학습률에 대해서는 꼭 알아보길 바란다.

서론이 길었다. 우리만의 SLP, MLP를 만들어 보자. 그에 앞서 이 글에서는 AND/OR/XOR에 대해서만 다룰 예정이다.


Coding AND / OR (SLP)

더보기
import tensorflow as tf
T = 1.0
F = 0.0
bias = 1.0

def get_AND_data():
    X = [[F, F, bias],[F, T, bias],[T, F, bias],[T, T, bias]]
    y = [[F],[F],[F],[T]]
    return X, y

def get_OR_data():
    X = [[F, F, bias],[F, T, bias],[T, F, bias],[T, T, bias]]
    y = [[F],[T],[T],[T]]
    return X, y

def get_XOR_data():
    X = [[F, F, bias],[F, T, bias],[T, F, bias],[T, T, bias]]
    y = [[F],[T],[T],[F]]   
    return X, y

class LinearRegression:
    def __init__(self):
        self.W = tf.Variable(tf.random.normal([3, 1]))
    
    def train(self,X):
        err = 1
        epoch, max_epochs = 0, 20
        while err > 0.0 and epoch < max_epochs:
            epoch += 1
            self.optimize(X)
            err = self.mse(y, self.pred(X)).numpy()
            print('epoch:', epoch, 'mse:', err)
    
    @tf.function
    
    def pred(self, X):
        return self.step(tf.matmul(X, self.W))
       
    def mse(self, y, y_hat):
        return tf.reduce_mean(tf.square(tf.subtract(y, y_hat)))
    
    def step(self,x):
        # step(x) = { 1 if x > 0; 0 otherwise }
        return tf.dtypes.cast(tf.math.greater(x, 0), tf.float32)

    def optimize(self, X):
        delta = tf.matmul(X, tf.subtract(y, self.step(tf.matmul(X, self.W))), transpose_a=True)
        self.W.assign(self.W+delta)

X, y = get_AND_data()
# X, y = get_OR_data()
# X, y = get_XOR_data()
        
perceptron = LinearRegression()
perceptron.train(X)

print(perceptron.pred(X).numpy())

먼저, AND, OR, XOR 에 대한 데이터셋을 함수로 정의한다. 일일이 만들어서 정의해도 되나 이편이 AND,OR,XOR 에 대한 결과를 빠르게 파악하기 쉽다.

이후 LinearRegression 클래스를 정의한다. init, train, pred, mse, step, optimizer 등의 함수를 정의한다.

init 함수를 정의한다.  w 가중치에 대한 값을 랜덤하게 초기화해준다. (b없음)

train 함수를 정의한다. 학습 수를 최대치랑 최소치를 만들어 오차가 0이 되면 종료한다. (오차가 0이면 정확도가 100퍼란 뜻)

pred 함수를 정의한다. y = wx 수식 계산과 같다.

mse 함수를 정의한다. 손실함수이며 수식은 찾아보길 바란다.

step 및 optimize 함수를 정의한다. 이를 통해 w를 갱신한다.

 

이제 본격적으로 데이터 X,y를 불러온다.

클래스를 변수에 할당하고, train 함수를 불러와 학습을 시킨다.

pred 함수를 불러와 결과를 확인한다.

결과를 확인하면 재밌는 사실을 알 수 있다.

AND & OR 데이터에 대해서는 오차가 0에 수렴하고, 결과도 예상했던 결과를 보여준다.

하지만, XOR 데이터에 대해서는 오차가 0.5에서 줄어들지 않고 결과또한 이상하다. 이를 통해 우리는 XOR 문제를 다루기 위해 MLP로 넘어가보자.

Coding XOR (MLP)

더보기
import numpy as np

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


def numerical_derivative(f, input_data):
    delta_x = 1e-4

    ret = np.zeros_like(input_data)
    it = np.nditer(input_data, flags=['multi_index'])

    while not it.finished:
        idx = it.multi_index

        tmp = input_data[idx]
        input_data[idx] = float(tmp) + delta_x
        fx1 = f(input_data)

        input_data[idx] = float(tmp) - delta_x
        fx2 = f(input_data)

        ret[idx] = (fx1 - fx2) / (2 * delta_x)
        input_data[idx] = tmp
        it.iternext()

    return ret

class NonLinearRegression:

    def __init__(self, gate_name, x_data, t_data):
        self.name = gate_name

        # 입력 데이터
        self.__x_data = x_data
        self.__t_data = t_data

        # 임의의 W2, b2, W3, b3 준비
        self.__W2 = np.random.randn(2,6)
        self.__b2 = np.random.randn(1,6)
        self.__W3 = np.random.randn(6,1)
        self.__b3 = np.random.randn(1,1)

        self.__learning_rate = 1e-1
        self.loss_func = self.__feed_forward
    
    def __feed_forward(self):
        z2 = np.dot(self.__x_data, self.__W2) + self.__b2        # 입력층 -> 은닉층
        a2 = sigmoid(z2)
        z3 = np.dot(a2, self.__W3) + self.__b3
        y = sigmoid(z3)

        return -np.sum(self.__t_data * np.log(y + delta) + (1 - self.__t_data) * np.log(1 - y + delta))


    def train(self,epoch):
        f = lambda x: self.__feed_forward()
        
        for step in range(epoch):
            self.__W2 -= self.__learning_rate * numerical_derivative(f, self.__W2)
            self.__b2 -= self.__learning_rate * numerical_derivative(f, self.__b2)
            self.__W3 -= self.__learning_rate * numerical_derivative(f, self.__W3)
            self.__b3 -= self.__learning_rate * numerical_derivative(f, self.__b3)
            
            if step % 10 == 0:
                print("step = ", step,"error_val = ", self.loss_func())
                

    def predict(self, x_data):
        z2 = np.dot(x_data, self.__W2) + self.__b2
        a2 = sigmoid(z2)

        z3 = np.dot(a2, self.__W3) + self.__b3
        pro = sigmoid(z3)
        
        if pro < 0.5:
            return 0, pro
        return 1, pro

x_data = np.array([[0, 0], [0, 1], [1, 0], [1, 1]]).reshape([4, 2])
y_data = np.array([0, 0, 0, 1]).reshape([4, 1])

XOR_Gate = NonLinearRegression("XOR_Gate", x_data, y_data)
XOR_Gate.train(50)

print(XOR_Gate.predict([0, 0]))
print(XOR_Gate.predict([1, 0]))
print(XOR_Gate.predict([0, 1]))
print(XOR_Gate.predict([1, 1]))

> XOR (MLP) 를 numpy 로 구현한 전체 코드이다.

sigmoid 활성화 함수를 정의한다. 출력을 1개이므로 0~1사이의 값으로 출력값을 0또는 1로 한정할 수 있다.

 

numerical_derivative 함수를 정의한다. 이는 미분을 이용하기 때문에 이를 쉽게하고자 하는 수치미분 함수이다.

 

NonLinearRegression 클래스 및 함수들을 정의한다. 우리는 이 클래스 안에서 init을 통한 초기값, feed forward를 통한 순전파, train을 통한 훈련 과정, predict를 통한 예측 함수를 정의하고자 한다.

 

init 함수를 먼저 정의한다. x,t 가 바로 입력과 출력이다. w2,w3,b2,b3 는 각각 가중치, 편향에 대한 값이며 만약 층이 더 깊어지면 더 많은 w,b를 정의해주거나 행렬로 처리 가능하다. (아이디어만 제시) 이후 학습률과 손실함수를 정의한다.

 

forward 함수를 정의한다. y = wx + b 의 구조를 띄며 numpy 행렬곱을 이용해준다. 추가적으로 각 층마다 활성화함수 sigmoid를 씌워준다. forward 함수를 통해 loss function을 구하여 return 해준다.

 

train 함수를 정의한다. 학습 수 (epoch)를 입력받고, 그만큼 학습을 진행한다. 학습마다 w,b를 업데이트해주고, 10회 학습마다 loss랑 학습횟수를 출력해준다.

 

predict 함수를 정의한다. 입력된 x 데이터에 대해 학습된 모델을 가지고 y를 출력하는 구조이다. sigmoid 활성화함수 특성상 0.5를 기준으로 1에 가까우면 1을, 0에 가까우면 0을 출력한다.

 

본격적으로 학습을 시작하기에 앞서 x_data, t_data를 정의해준다. x는 0,0 / 0,1 / 1,0 / 1,1 4가지 경우의 수가 있다. t는 이에 따른 AND의 경우 0,0,0,1 / OR 의 경우 0,1,1,1 / XOR 의 경우 0,1,1,0 으로 정의한다.

이후 새로운 변수를 통해 class를 할당한다.

train 함수를 불러와 50번의 학습을 진행해본다.

학습 이후 마지막에는 predict 함수를 불러와 결과를 보여준다.

 

결과를 통해 XOR문제를 다룰 수 있게 되었다.

하지만 여기서 그치지 않고 우리는 이 코드를 가지고 우리의 입맛에 맞게 다룰 수 있다.

  1. w,b 를 행렬로 만들거나 여러개를 추가하여 층을 추가하거나, 노드를 추가하여 더 깊은 신경망 모델을 구축이 가능하다.
  2. activation function 을 바꾸어 sigmoid뿐 아닌 softmax 같은 함수로 여러개의 클래스 예측도 가능하다.
  3. 또한 predict 함수에서 우리가 얻은 결과를 출력하고 싶은 형태로 변형이 가능하다.

일일이 구현하는 과정에서 우리는 더 많은 자유도를 얻었다. 우리만의 데이터셋을 우리만의 모델 구조로 변형하여 학습이 가능하며 최종적으로 원하는 결과를 출력해낼 수 있다는 것이다.


else

이는 앞으로의 더 복잡한 모델에서 모델의 구조를 파악하고 활용하는데 있어 중요한 첫걸음이 될 것이라고 생각한다.

그러나 아쉽게도 층을 추가하거나 노드는 추가하는 방식이 까다로우니, 이 코드를 이해했으면 pytorch 를 사용해 모델을 구축해보아도 좋다. (굳이 pytorch를 추천하는 이유는 여러분이 원하는대로 모델을 변형하여 입맛에 맞게 다룰 수 있기 때문이다.)