인공지능/딥러닝 직접 구현하기 프로젝트

* 모든 코드는 제 깃허브 (cdjs1432/DeepLearningBasic: Deep Learning from scratch)에서 확인할 수 있습니다.

 

이번 시간에는 Softmax Classification을 구현해 보고, 이를 MNIST 데이터셋으로 테스트 해보겠습니다.

혹시라도 MNIST 데이터셋이 조금 부담스럽다 하시는 분들은, 제 깃허브 (https://github.com/cdjs1432/DeepLearningBasic)에 작은 데이터인 Iris로 훈련하는 코드가 있으니, 확인해 보셔도 좋을 것 같습니다. (IrisTest.py)

 

 

들어가기에 앞서, 간단하게 Softmax 함수와 Cross-Entropy Loss가 무엇인지 먼저 알아봅시다.

 

우선, Softmax 함수는 다음과 같이 정의됩니다.

 

$$ y_i(z) = \frac{e^{z_i}}{\sum_{i=1}^N e^{z_i}} $$

 

여기서 z는 (적어도 이번 차시에서는) x.dot(w) + b입니다.

이 Softmax 함수를 지나온 값들의 합은 1이 되는데, 그렇기에 위의 값들을 "확률"로 생각해도 좋습니다.

이렇게만 말하면 조금 이해가 안될 테니, 그냥 코드로 알아보도록 합시다.

 

 

import numpy as np
import pandas as pd
import ComputeGrad

일단은 필요한 모듈을 모두 import합시다.

지금 당장은 안쓰더라도, 이번 시간에 모두 쓸 것들입니다.

 

def softmax(a):
    C = np.max(a)
    exp_a = np.exp(a - C)
    if a.ndim == 1:
        sum_exp_a = np.sum(exp_a)
        y = exp_a / sum_exp_a
    else:
        sum_exp_a = np.sum(exp_a, 1)
        sum_exp_a = sum_exp_a.reshape(sum_exp_a.shape[0], 1)
        y = exp_a / sum_exp_a
    return y


a = np.array([1.3, 2.4, 3.7])
print(softmax(a))

위 코드를 실행하면, [0.06654537 0.19991333 0.73354131] 라는 값이 출력될 것입니다.

사실 계산 부분은 위의 softmax의 수식과 동일하지만, C값이 눈에 들어옵니다.

 

이 C 값을 놓는 이유는, 만약 이 값 없이 입력인 a값이 너무 큰 값으로 들어오게 되면 오버플로우가 날 확률이 높기 때문입니다.

그러나 이렇게 a를 C로 빼 준 뒤 exp 연산을 하게 되면, 그렇게 오버플로우가 날 확률이 현저히 낮아집니다.

 

참고로, softmax 함수는 저런 식으로 연산 내부 값을 빼 준다고 하더라도 결과값이 동일하게 나오게 됩니다.

(왜 그런지는 직접 계산해 보시면서 해보셔도 재밌을 것 같습니다.)

 

그리고 직접 보시면 아시겠지만, 위 실행 결과를 더해보면 1이 나오게 됩니다. (물론 출력값은 소수점이 살짝은 잘리기에 완벽히 1이 되진 않습니다.)

그리고, 위의 값들을 "0번째 인덱스가 정답일 확률은 0.06, 2번째 인덱스가 정답일 확률은 0.73이겠군" 이라고 생각할 수 있습니다.

바로 이런 성질을 사용하여 Classification을 할 수 있는 것이죠.

 

그리고 a가 행렬로 입력될 경우, 각 행에서의 합이 1이어야 하므로, a.ndim==... 코드를 넣어서 경우를 나눠주었습니다.

(이 구현이 좀 예쁜 구현은 아닐수는 있겠지만, 그래도 시간복잡도는 비슷하니 뭐...)

 

 

 

다음은 Cross-Entropy Loss입니다.

이 오차 함수를 식으로 쓰면,

$$ E=-\sum_{i} t_i \ln y_i $$

가 됩니다.

 

여기서 t는 정답 label, y는 우리의 예측의 출력값이 됩니다.

 

이제 이를 직접 구현해 봅시다.

(위의 코드와 이어집니다.)

def cross_entropy_loss(y, t):
    return -np.sum(t * np.log(y))

y = softmax(a)
print(y)
t = np.array([1, 0, 0])
print(cross_entropy_loss(y, t))
t = np.array([0, 1, 0])
print(cross_entropy_loss(y, t))
t = np.array([0, 0, 1])
print(cross_entropy_loss(y, t))

위 코드를 실행시키면,

2.7098713687418052
1.6098713687418051
0.309871368741805

 

라는 값이 나옵니다. 아까 전의 softmax(a), 즉 y값이 대략 [0.06, 0.19, 0.73] 이었던 것을 고려한다면, y가 가장 높은 인덱스인 0.73에 해당하는 loss값이 가장 낮으므로, 성공적으로 구현됐다고 생각할 수 있겠습니다.

 

그러나, 우리는 Cross Entropy Loss를 미니배치에 적용시켜서 학습을 할 것이므로, 위 함수를 다음과 같이 바꿔줍시다.

def cross_entropy_loss(y, t):
    c = 1e-7
    if y.ndim == 1:
        t = t.reshape(1, t.size)
        y = y.reshape(1, y.size)

    batch_size = y.shape[0]
    return -np.sum(t * np.log(y + c)) / batch_size

위의 y.ndim==1은 y(혹은 t, 어차피 같은 shape로 들어와야 하므로 상관없음)가 2차원 배열이 아니라 1차원 배열일 때도 사용할 수 있게 하기 위한 코드입니다.

 

여기서의 c값은 np.log 연산을 할 때 오버플로우가 나오는 것을 방지해 주는 역할입니다.

log의 진수로 0이 들어오면 분명 오버플로우가 발생할테니까요.

 

 

자, 이제 데이터를 불러와 볼까요?

# load data
train = pd.read_csv("./Data/MNIST_data/mnist_train.csv")
y_train = train["label"]
x_train = train.drop("label", 1)
x_train = x_train.values / x_train.values.max()
y_train = y_train.values

# y to one-hot
one_hot = np.zeros((y_train.shape[0], y_train.max() + 1))
one_hot[np.arange(y_train.shape[0]), y_train] = 1
y_train = one_hot


# initialize parameters and hyperparameters
num_classes = y_train.shape[1]
w = np.random.uniform(-1, 1, (x_train.shape[1], num_classes))
b = np.zeros(num_classes)

learning_rate = 0.01
epoch = 10000
batch_size = 128

w, b = ComputeGrad.SoftmaxGD(x_train, y_train, w, b, learning_rate, epoch, batch_size)

아, 참고로 해당 데이터는 제 깃허브에 올려놓았으니 사용하셔도 되고, 아니면 다른 방식을 사용해서 MNIST를 불러와도 무방합니다.

 

간단히 코드를 설명하자면, load data에서는 당연히 데이터를 불러오고,

 

그리고, 그 아래에는 y_train을 one-hot vector로 만드는 코드가 있습니다.

 

이 글을 볼 사람들이라면 웬만하면 다 알겠지만 그래도 설명하자면, one-hot vector란 정답 클래스를 숫자로 4, 8, ... 이런 식으로 두는 것이 아니라, [0, 0, 0, 1, 0, 0, 0, 0, 0, 0]과 같은 방식으로 두는 것입니다.

 

 

그렇다면 이제 SoftmaxGD를 구현해 봅시다.

 

 

 

우선, 수식을 먼저 다시 확인해 보겠습니다.

 

$$ y_i(z) = \frac{e^{z_i}}{\sum_{i=1}^N e^{z_i}} $$

$$ E=-\sum_{i} t_i \ln y_i $$

 

이 식을 바탕으로 Cross-Entropy Loss E를 w, b에 대해 미분한 값을 구해 봅시다.

 

우선, Cross-Entropy Loss E를 $z_j$에 미분한다 하면, 다음과 같은 식이 얻어집니다.

$$\frac{\partial E}{\partial z_j} = -\frac{\partial {\sum_{i} t_i \ln y_i} }{\partial z_j} = -\sum_{i} t_i \frac{\partial \ln y_i}{\partial z_j}$$

 

이 때, chain rule에 의하여, 위 식은 다음과 같이 변형이 가능합니다.

$$-\sum_{i} t_i \frac{\partial \ln y_i}{\partial z_j} = -\sum_{i} t_i \frac{\partial \ln y_i}{\partial y_i} \frac{\partial y_i}{\partial z_j} = -\sum_{i} \frac{t_i}{y_i} \frac{\partial y_i}{\partial z_j}$$

 

 

자, 이제 $\frac{\partial y_i}{\partial z_j}$를 구해 봅시다.

그런데 이 때, 조심해야 할 점이 있습니다.

그것은 바로 $\frac{\partial y_i}{\partial z_j}$를 계산할 때 j==i인 경우와 j!=i인 경우를 나누어서 생각해 주어야 한다는 것입니다.

일단, j와 i가 둘 다 k로 같은 경우로 계산을 해보도록 하겠습니다.

$$\frac{\partial y_k}{\partial z_k} = \frac{\partial \frac{e^{z_k}}{\sum_{i=1}^N e^{z_i}}}{\partial z_k} $$

여기서, $\sum_{i=1}^N e^{z_i}$를 S라고 치환한다면, $\frac{\partial S}{\partial z_k} = e^{z_k}$ 이므로,

$$\frac{\partial y_k}{\partial z_k} = \frac{\partial \frac{e^{z_k}}{S}}{\partial z_k} = \frac{e^{z_k}(S - e^{z_k})}{S^2}$$

가 됩니다.

그런데, 이 때 $\frac{e^{z_k}}{S} = y_k$이므로, 식을 다음과 같이 정리할 수 있습니다.

$$\frac{\partial y_k}{\partial z_k} = \frac{e^{z_k}(S - e^{z_k})}{S^2} = y_k(1 - y_k)$$

 

그러면, $\frac{\partial E}{\partial z_k}$는 다음과 같이 나오게 됩니다.

$$ \frac{\partial E}{\partial z_k} = \frac{\partial E}{\partial y_k} * \frac{\partial y_k}{\partial z_k} = -\frac{t_k}{y_k} * y_k(1 - y_k) = t_k(y_k - 1) $$

 

그리고, 만약 i!=j인 상황이라면, 계산은 다음과 같이 이루어집니다.

$$\frac{\partial y_i}{\partial z_j} = \frac{\partial \frac{e^{z_j}}{\sum_{i=1}^N e^{z_i}}}{\partial z_j} $$

$$\sum_{i=1}^N e^{z_i} = S, \frac{\partial S}{\partial z_j} = e^{z_j}$$

$$\frac{\partial y_i}{\partial z_j} = \frac{\partial \frac{e^{z_i}}{S}}{\partial z_j} = \frac{0 - e^{z_i}e^{z_j}}{S^2} = {y_i}{y_j}$$

 

아주 훌륭합니다!

이제 위 두 식을 하나로 합치게 되면 다음과 같은 식이 완성됩니다.

 

 

$$\frac{\partial y_i}{\partial z_j} = y_i(1 \lbrace i==j \rbrace - y_j)$$

이 때, $1 \lbrace i==j \rbrace$ 는 i==j일때는 1, 아니면 0이라는 뜻입니다.

 

 

자, 이제 원래 구하고 있던 $\frac{\partial E}{\partial z_j}$을 구해보자면,

$$ \frac{\partial E}{\partial z_j} = -\sum_{i} \frac{t_i}{y_i} \frac{\partial y_i}{\partial z_j} = -\sum_{i} \frac{t_i}{y_i} y_i(1 \lbrace i==j \rbrace - y_j) = -\sum_{i} t_i(1 \lbrace i==j \rbrace - y_j) = -\sum_{i} t_i(1 \lbrace i==j \rbrace) + y_j\sum_{i} t_i$$

라는 식이 나옵니다!

 

 

이 때, 식 $-\sum_{i} t_i(1 \lbrace i==j \rbrace)$는 i==j 일 때  $-t_j$, i!=j 일 때 0으로 정의되므로 시그마가 사라지며 값 자체를 $-t_j$로 바꿀 수 있습니다.

 

또한,  $t_i$는 단 하나의 정답 인덱스에만 1의 값을 가지고 다른 모든 인덱스에 대해선 0의 값을 가지기 때문에 $\sum_{i} t_i=1$입니다.

 

따라서, 위 식을 다시 정리해서 쓰면,

 

$$\frac{\partial E}{\partial z_j} = y_j - t_j$$

로 아름답게 정리가 됩니다!

 

 

드디어 끝이 보이네요! 

마지막으로, 정말 원래 구하려 했던 식인 $\frac{\partial E}{\partial w}$ 와 $\frac{\partial E}{\partial b}$를 구해 보면,

$$\frac{\partial E}{\partial w} = \frac{\partial E}{\partial z_j}\frac{\partial z_j}{\partial w} = (y_j - t_j)x$$

$$\frac{\partial E}{\partial b} = \frac{\partial E}{\partial z_j}\frac{\partial z_j}{\partial b} = (y_j - t_j)$$

 

*(2021-06-04 수정: 설명과 index값 오류를 수정했습니다. 제보해 주셔서 감사드립니다.)

이제 위 식을 코드로 구현해 봅시다!!

 

 

def SoftmaxGD(x, y, w, b, learning_rate=0.01, epoch=100000, batch_size=128):

    for epochs in range(epoch):
        batch_mask = np.random.choice(x.shape[0], batch_size)
        x_batch = x[batch_mask]
        y_batch = y[batch_mask]

        z = x_batch.dot(w) + b
        pred = softmax(z)
        dz = (pred - y_batch) / batch_size
        dw = np.dot(x_batch.T, dz)
        db = dz * 1.0
        w -= dw * learning_rate
        b -= (db * learning_rate).mean(0)

        if epochs % (epoch / 10) == 0:
            pred = softmax(x.dot(w) + b)
            print("ACC : ", (pred.argmax(1) == y.argmax(1)).mean())
            err = cross_entropy_loss(pred, y)
            print("ERR : ", err)


    return w, b

 

 

우선 코드를 보시면 batch가 눈에 띄실 건데요, MNIST는 60000개의 데이터로 이루어진 데이터셋이기에 SGD가 아닌 그냥 GD를 쓰면 너무 훈련이 오래 걸리기에, SGD를 적용하였습니다.

그 외의 나머지 부분은 딱히 설명할 부분이 없는 것 같네요. 수식을 코드에 직접 적용시켰을 뿐입니다.

 

 

위에서 hyperparameter를 바꾸지 않고 학습시켰다면, 아마 마지막에는 ACC가 대략 0.8이상 (80% 이상)이 나올 것입니다.

계층 하나만으로도 손글씨 인식률이 80%나 되다니!

 

 

그렇다면, 이제 test 데이터에서도 잘 작동하나 확인해 봅시다.

# load data
test = pd.read_csv("./Data/MNIST_data/mnist_test.csv")
y_test = test["label"]
x_test = test.drop("label", 1)
x_test = x_test.values / x_test.values.max()
y_test = y_test.values

# y to one-hot
one_hot = np.zeros((y_test.shape[0], y_test.max() + 1))
one_hot[np.arange(y_test.shape[0]), y_test] = 1
y_test = one_hot

pred = x_test.dot(w) + b
pred = softmax(pred)
print("ACC : ", (pred.argmax(1) == y_test.argmax(1)).mean())

 

 

이 부분의 코드는 그냥 윗부분 그대로 복사 붙여넣기 한 부분이니, 그냥 그대로 넣으시면 될 것 같습니다.

 

그리고, 결과를 확인해 보니 test 데이터에서도 좋은 결과가 나오는 것을 확인할 수 있습니다!

 

 

그러면 이제 이것으로 Softmax Classification의 구현을 마치도록 하겠습니다.

* 모든 코드는 제 깃허브 (cdjs1432/DeepLearningBasic: Deep Learning from scratch)에서 확인할 수 있습니다.

 

이번 시간엔 Logistic Regression을 구현하고, 이를 breast cancer 데이터셋으로 테스트까지 해보도록 하겠습니다.

(30가지 데이터를 토대로, 해당 유방암이 악성인지 양성인지 확인하는 데이터셋)

 

import numpy as np
from sklearn import datasets
import ComputeGrad

일단 필요한 모듈 먼저 import해 줍시다.

원래는 직접 데이터셋을 다운받아서 해도 되겠지만, 편의를 위해 sklearn 모듈을 import하겠습니다.

 

 

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

그리고, Logistic Regression에 필수적인, 시그모이드 함수를 정의해 줍시다.

 

 

cancer = datasets.load_breast_cancer()
x = cancer.data
x /= x.mean()
y = cancer.target
w = np.random.uniform(-1, 1, x.shape[1])
b = 0
learning_rate = 0.0001
epoch = 10000

w, b = ComputeGrad.LogisticGD(x, y, w, b, learning_rate, epoch)

그리고, 이제 필요한 변수들을 모두 불러와 봅시다.

 

아까 import 해왔던 datasets에서 breast_cancer을 불러오고, 각각의 data와 target을 x, y에 집어넣어 줍시다.

(이 때, 당연히 data는 원래 x값에 들어갈 데이터를, target은 y값에 들어갈 데이터를 가지고 있습니다.)

 

그런데 이번 코드에는 저번과는 다르게 x값을 x의 평균으로 나누는 코드까지 같이 들어있습니다.

왜 이렇게 하는걸까요?

 

이유를 잠깐 간단히 설명해 보도록 하겠습니다.

만약 x값의 범위가 위아래로 너무 커버리면, w값과 곱할 때도 더욱 범위가 커지게 됩니다.

그러면, 한번에 너무 크게 dw값이 진동하게 되고, 이는 결국 learning_rate를 아주 큰 값으로 둔 것과 비슷한 결과를 낳게 됩니다.

(참고로, 사실 x를 그냥 평균으로 나눠버리는 것 보다 x값을 표준화 시키는 더욱 좋은 방법들이 많이 있지만, 그것은 나중에 언급하도록 하고 우선 지금은 이정도로 둡시다. 이정도도 충분히 잘 작동하거든요.)

 

 

아무튼, 그 뒤 w값과 b값, learning_rate와 epoch값을 초기화 해 줍니다.

 

이제, LogisticGD 함수를 확인하러 갑시다.

 

 

def LogisticGD(x, y, w, b=0, learning_rate=0.001, epoch=10000):
    for epochs in range(epoch):
        z = x.dot(w) + b
        pred = sigmoid(z)
        err = -np.log(pred) * y - np.log(1 - pred) * (1 - y)
        err = err.mean()
        """
        to minimize err --> differentiate pred
        err = -ln(pred)*y - ln(1-pred)*(1-y)
        --> -y/pred + (1-y) / (1-pred)

        differentiate pred by z
        pred = 1 / (1+e^-z)
        --> e^z / (1+e^-z)^2
        --> e^z / (1+e^-z) * 1 / (1+e^-z)
        --> pred * (1 - pred)

        chain rule : dl/dpred * dpred/dz = dl/dz

        as the chain rule, derivative of the loss function respect of z
        --> (-y/pred + (1-y) / (1-pred)) * (pred * 1-pred)
        --> (1-pred) * -y + pred * (1-y)
        --> -y +y*pred +pred -y*pred
        --> pred - y

         now, let's find dl/dw
         dl/dw = dl/dz * dz/dw
               = (pred - y) * dz/dw
         let's find dz/dw...

         z = wT.x + b
         --> dz/dw = x

         so, dl/dw = (pred - y) * dz/dw
                   = (pred - y) * x


        similarly, find dl/db...
        dl/db = dl/dz * dz/db
              =  (pred - y) * dz/db
        z = wT.x + b
        --> dz/db = 1

        so, dl/db = (pred - y)

        """
        dw = (pred - y).dot(x)
        db = (pred - y).mean()
        w -= dw * learning_rate
        b -= db * learning_rate
        if epochs % 1000 == 0:
            print(err)
    return w, b

 

 

 

주석이 더럽게 깁니다. 계산 과정을 보고싶은 것이 아니라면, 그냥 무시하고 지워버리셔도 됩니다.

 

일단, logistic regression에서의 prediction은 사실 Gradient Descent에다가 sigmoid만 집어넣은 것입니다.

그리고, Logistic Regression의 err도 아래의 loss function을 사용하도록 하겠습니다.

 

$ err = -ln(pred) * y - ln(1 - pred) * (1-y) $

 

이제 저희가 생각해야 할 부분은, 이 err 함수를 각각 w와 b에 대해서 미분하는 것입니다.

 

 

우리는 여기서 chain rule(연쇄 법칙)이라는 것을 사용할 것입니다.

chain rule이란, $ \frac{\partial z}{\partial x} = \frac{\partial z}{\partial y} \frac{\partial y}{\partial x} $인 것을 말합니다.

저희는 이제부터 $ \frac{\partial err}{\partial w} $의 값을 찾을 겁니다.

 

우선, 가장 간단하게 err을 pred에 대해서 미분해 볼까요?

$ err = -ln(pred) * y - ln(1 - pred) * (1-y) $ 이었고, 이를 pred에 미분해보면...

$$ \frac{\partial err}{\partial pred} = \frac{-y}{pred} + \frac{1-y}{1-pred} $$

가 됩니다.

 

또, pred는 $ sigmoid(z) = \frac{1}{1+e^{-z}} $ 였으므로, 이 pred를 z에 대해서 미분하면,

$$ \frac{\partial pred}{\partial z} = \frac{e^{-z}}{(1+e^{-z})^2}$$

이 된다는 것을 알 수 있습니다.

 

그리고, z=wx+b이므로 z를 w에 대해 미분하면 그냥 x, z를 b에 대해 미분하면 그냥 1이 나옵니다.

그리고 지금까지 구한 것들을 chain rule을 사용하여 다시 정리해 봅시다.

 

$$ \frac{\partial err}{\partial w} = \frac{\partial err}{\partial pred} * \frac{\partial pred}{\partial z} * \frac{\partial z}{\partial w} $$

$$ \frac{\partial err}{\partial w} = (\frac{-y}{pred} + \frac{1-y}{1-pred}) * \frac{e^{-z}}{(1+e^{-z})^2} * x$$

 

자! 이제 이 위의 pred와 z에 한번 식을 대입해서 계산해 봅시다!!

 

 

 

는 사실 좀;;;; 너무하죠 그쵸?

 

 

사실 아까 전에, $ \frac{\partial pred}{\partial z} = \frac{e^{-z}}{(1+e^{-z})^2} $ 라고 했던 걸 조금 정리를 해 보면,

$pred = \frac{1}{1+e^{-z}}$라고 했으므로,

 

$$ \frac{e^{-z}}{(1+e^{-z})^2} = \frac{1}{1+e^{-z}} * \frac{e^{-z}}{1+e^{-z}} = pred * (1-pred)$$

로 정리가 됩니다!!

 

 

자, 그럼 이제 다시 위 식으로 돌아가서 하던 계산을 마저 해 봅시다!

 

 

$$ \frac{\partial err}{\partial w} = \frac{\partial err}{\partial pred} * \frac{\partial pred}{\partial z} * \frac{\partial z}{\partial w} $$

$$ = (\frac{-y}{pred} + \frac{1-y}{1-pred}) * pred * (1 - pred) * x$$

$$ = ((pred - 1) * y + pred * (1 - y)) * x$$

$$ = (pred - y) * x$$

 

 

이제 이렇게 두니, 아주 예쁜 식이 나왔군요!

또한,$ \frac{\partial err}{\partial b}$의 값도 구해 보자면, $ \frac{\partial z}{\partial b} = 1 $이었으므로,

$$ \frac{\partial err}{\partial b} = \frac{\partial err}{\partial pred} * \frac{\partial pred}{\partial z} * \frac{\partial z}{\partial b} $$

$$ = pred - y$$

 

라는 식이 도출됩니다!!

 

따라서, dw, db를 위의 코드와 같이 update시켜줄 수 있습니다.

 

 

이제, 제대로 훈련이 되었나 확인해야겠죠?

원래 쓰던 코드에, 다음과 같은 코드를 넣어서 확인해 봅시다.

z = x.dot(w) + b
pred = sigmoid(z)

pred[pred > 0.5] = 1
pred[pred <= 0.5] = 0
print("ACC : ", (pred == y).mean())

pred 값을 직접 계산해서, 0.5보다 크면 1(양성), 0.5보다 작거나 같으면 0(악성)인 종양으로 판별하게 하는 것입니다.

 

그리고, (pred ==y).mean()의 코드로 정확도를 보면, 대충 0.92, 즉 92%의 정확도를 보이네요!

(참고 - (pred == y)를 하면 pred와 y가 같으면 1, 다르면 0을 각각의 인덱스에 따라 출력하니, 그 평균을 맞추면 정확도가 됩니다.)

 

이렇게, Logistic Regression의 구현까지 마쳤습니다. 다음 시간에는 Softmax Classification의 구현을 해보도록 하겠습니다.

* 모든 코드는 제 깃허브 (cdjs1432/DeepLearningBasic: Deep Learning from scratch)에서 확인할 수 있습니다.

 

이번 차시에서는 Stochastic Gradient Descent, 줄여서 SGD를 구현해 보도록 하겠습니다.

 

 

일단 SGD가 뭔지 아주 짤막하게만 설명하자면, 기존 Gradient Descent에서 약간의 mini-batch만을 뽑아서 학습시키는 방법입니다.

이 방법을 사용하면 조금 학습이 불안정해지지만, 더욱 빠른 시간에 꽤 괜찮은 값으로 수렴하게 됩니다.

 

import numpy as np
import ComputeGrad
train_x = np.random.uniform(-5, 5, (100, 10))
ans_w = np.random.uniform(-5, 5, (10, 1))
ans_b = 3
train_y = train_x.dot(ans_w) + ans_b
w = np.random.uniform(-5, 5, (10, 1))
b = np.random.uniform(-5, 5, (1, 1))


w, b = ComputeGrad.SGD(train_x, train_y, 3, w)

print(ans_w)
print(w)

print(ans_b)
print(b)

일단 테스트를 돌리는 코드는 저번 시간과 크게 다르지 않습니다.

그렇다면, SGD는 어떻게 생겼을지 확인해 봅시다.

 

 

def SGD(x, y, b, w, learning_rate=0.01, epoch=1000, batch_size=16):
    if type(w) != np.ndarray:
        w = float(w)
        w = np.reshape(w, (1, 1))
    if x.size == x.shape[0]:
        x = x.reshape(x.shape[0], 1)
    if y.size == y.shape[0]:
        y = y.reshape(y.shape[0], 1)

    for epochs in range(epoch):
        batch_mask = np.random.choice(x.shape[0], batch_size)
        x_batch = x[batch_mask]
        y_batch = y[batch_mask]

        pred = x_batch.dot(w) + b
        dw = ((pred - y_batch) * x_batch).mean(0)
        dw = dw.reshape(dw.shape[0], 1)
        db = (pred - y_batch).mean()
        w -= dw * learning_rate
        b -= db * learning_rate
        if epochs % (epoch / 10) == 0:
            pred = x.dot(w) + b
            err = np.mean(np.square(pred - y))
            print("error : ", err)
    return w, b

...라고는 해도, 저번 Gradient Descent와 크게 달라지지 않았습니다.

어떤 부분이 달라졌는지 확인해 봅시다.

 

 

 

우선, 함수에 batch_size와 batch_mask라는 변수가 생겼습니다.

batch_size는 말 그대로 mini-batch의 크기를 정해 주고, batch_mask는 mini-batch를 만드는 역할을 해 주는데요,

 

위에서 np.random.choice 함수는 0부터 x.shape[0]-1까지의 랜덤한 숫자를 batch_size만큼 뽑아주는 역할을 합니다.

그러므로, x[batch_mask]를 하게 되면, 랜덤하게 골라진 batch_size개의 인덱스를 참조하여 mini-batch를 만들게 됩니다.

 

 

그리고, 이 과정이 전부입니다.

저번 Gradient Descent 코드에서 크게 달라진 부분은 없으니, 쉽게 이해가 되실 거라 믿습니다.

* 모든 코드는 제 깃허브 (cdjs1432/DeepLearningBasic: Deep Learning from scratch)에서 확인할 수 있습니다.

import numpy as np
import ComputeGrad
data = np.array([[1, 2, 3], [4, 6, 8]])  # y=2x+2 --> w=2, b=2
train_x = data[0]
train_y = data[1]

w = 1
b = 1

learning_rate = 0.1

epoch = 10000
w, b = ComputeGrad.GradientDescent(train_x, train_y, b, w, learning_rate, epoch)
print(w)
print(b)

 

 

 

 

일단 가장 먼저 할 것은, 간단한 Linear Regression 및 Gradient Descent의 구현입니다.

어차피 설명할 부분이 적으니, 코드로 바로 들어가도록 하겠습니다.

 

 

data = np.array([[1, 2, 3], [4, 6, 8]])  # y=2x+2 --> w=2, b=2
train_x = data[0]
train_y = data[1]

w = 1
b = 1

learning_rate = 0.1
epoch = 10000

우선 처음 시작은 이렇게 간단한 데이터로 하겠습니다.

이정도로 간단한 데이터라면 분명 Gradient Descent를 통해 w=2, b=2라는 정답을 찾아낼 수 있을 겁니다.

위 코드를 통해 train_x에는 [1, 2, 3]이, train_y에는 [4, 6, 8]이 들어가게 됩니다.

또, w값과 b값은 초기 값을 1로 설정하고, learning rate와 epoch까지 설정했습니다.

 

 

 

def GradientDescent(x, y, b, w, learning_rate=0.01, epoch=10000):
    if type(w) != np.ndarray:
        w = float(w)
        w = np.reshape(w, (1, 1))
    if x.size == x.shape[0]:
        x = x.reshape(x.shape[0], 1)
    if y.size == y.shape[0]:
        y = y.reshape(y.shape[0], 1)
    for epochs in range(epoch):
        pred = x.dot(w) + b  # y = w1x1 + w2x2 + ... + wmxm + b
        """
        err = 1/m * ((Wx1 + b - y1)^2 + (Wx2 + b - y2)^2 + ... + (Wxm + b - ym)^2)
        to minimize err --> differentiate w and b
        dw = 2/m * ((Wx1 + b - y1) * x1 + (Wx2 + b - y2) * x2 + ... + (Wxm + b - ym) * xm) 
        db = 2/m * ((Wx1 + b - y1) * 1 + (Wx2 + b - y2) * 1 + ... + (Wxm + b - ym))  
        """
        dw = ((pred - y) * x).mean(0)
        db = (pred - y).mean()
        dw = dw.reshape(dw.shape[0], 1)
        w -= dw * learning_rate
        b -= db * learning_rate


        if epochs % 1000 == 0:
            err = np.mean(np.square(pred - y))  # err = 1/m * (w1x1 + w2x2 + ... + wmxm + b - y)^2
            print("error : ", err)
    return w, b

다음은 Gradient Descent 부분 코드입니다.

 

    if type(w) != np.ndarray:
        w = float(w)
        w = np.reshape(w, (1, 1))
    if x.size == x.shape[0]:
        x = x.reshape(x.shape[0], 1)
    if y.size == y.shape[0]:
        y = y.reshape(y.shape[0], 1)

원래 Multi Variable Linear Regression을 먼저 만들 생각이었기에, w와 x의 값을 행렬곱에 적합한 형태로 바꾸어 주어야 합니다.

 

    for epochs in range(epoch):
        pred = x.dot(w) + b  # y = w1x1 + w2x2 + ... + wmxm + b
        """
        err = 1/m * ((Wx1 + b - y1)^2 + (Wx2 + b - y2)^2 + ... + (Wxm + b - ym)^2)
        to minimize err --> differentiate w and b
        dw = 2/m * ((Wx1 + b - y1) * x1 + (Wx2 + b - y2) * x2 + ... + (Wxm + b - ym) * xm) 
        db = 2/m * ((Wx1 + b - y1) * 1 + (Wx2 + b - y2) * 1 + ... + (Wxm + b - ym))  
        """
        dw = ((pred - y) * x).mean(0)
        db = (pred - y).mean()
        dw = dw.reshape(dw.shape[0], 1)
        w -= dw * learning_rate
        b -= db * learning_rate


        if epochs % 1000 == 0:
            err = np.mean(np.square(pred - y))  # err = 1/m * (w1x1 + w2x2 + ... + wmxm + b - y)^2
            print("error : ", err)
    return w, b

 

다음은 훈련 과정입니다.

 

우선, Linear Regression에서의 예측값을 pred, loss값을 err로 설정해 두었습니다.

 

계산 과정은 주석으로 달아놨지만, 조금 더 자세히 써보도록 하겠습니다.

 

 

우선 err값은 1/m * ((Wx1 + b - y1)^2 + (Wx2 + b - y2)^2 + ... + (Wxm + b - ym)^2)으로 설정해 두었습니다.

하지만 지금은 multi variable gradient descent가 아니므로, w와 x는 값을 각 한개씩을 가지게 됩니다.

따라서 err값은 $ err = 1/m * (wx + b - y)^2 $ 로 설정이 됩니다.

 

이제 이 err 값을 w와 b에 대하여 미분해 보면,

$$ \frac{\partial{err}}{{\partial{w}}} = 2/m * (wx + b - y) * x$$

$$ \frac{\partial{err}}{{\partial{b}}} = 2/m * (wx + b - y)$$

가 됩니다.

 

따라서, 이를 코드로 구현하면, 

        dw = ((pred - y) * x).mean(0)

        db = (pred - y).mean()

가 됩니다.

(2를 곱하지 않은 이유는, 딱히 중요한 이유가 있는게 아니라 코드에 2 곱하는 코드 있으면 더러워 보여서 그런겁니다.)

 

 

 

 

그리고, 아까 위에서 잠깐 언급했듯, 위 코드는 Multi Variable일때도 작동합니다.

import numpy as np
import ComputeGrad
train_x = np.random.uniform(-5, 5, (100, 10))
ans_w = np.random.uniform(-5, 5, (10, 1))
ans_b = 3
train_y = train_x.dot(ans_w) + ans_b
w = np.random.uniform(-5, 5, (10, 1))
b = np.random.uniform(-5, 5, (1, 1))

w, b = ComputeGrad.GradientDescent(train_x, train_y, 3, w)

print(ans_w)
print(w)

print(ans_b)
print(b)

 

위의 코드가 바로 그 Multi Variable일 경우의 코드입니다.

 

train_x와 ans_w, ans_b를 각각 만들어 주어 ans_w * train_x + b의 값으로 y를 만들어 주었고,

w와 b의 초기값을 랜덤으로 설정해 두며 Gradient Descent를 작동시켰습니다.

 

마지막의 결과값을 보면 ans_w와 w, ans_b와 b가 서로 비슷해져 있는 모습을 볼 수 있습니다.

 

 

지금부터 별로 쓸데는 없지만 재미있는 프로젝트를 하나 할까 합니다.

 

지금까지는 계속 인공지능에 관한 새로운 내용, 새로운 알고리즘 등을 배우면서 텐서플로우 및 파이토치로 시연하기만 했었는데, 이렇게 하니 알고리즘에 대한 확실한 이해가 부족해지는 것 같았습니다. 응용 능력도 많이 떨어지는것 같구요.

 

따라서, 이 프로젝트에서는 기본적인 데이터셋 관련 함수나 numpy, pandas와 같은 모듈만을 사용하여 딥러닝 및 머신 러닝 기술을 구현할 예정입니다.

역전파법과 순전파의 계산과 같은 수학적인 부분들도 직접 계산해서 코드로 구현할 예정입니다.

또한, 이 프로젝트의 게시글은 다른 게시글처럼 인공지능을 모르는 사람들을 위한 게시글이 아니라, 인공지능이 뭔지는 잘 아는 사람들을 위한 게시글입니다.

따라서, 기본적인 인공지능과 관련된 내용은 버리고, 특정 함수의 미분법이나 구현한 방식 등에 대해서만 다룰 예정입니다.

(인공지능의 기본이 부족하다면 모두를 위한 딥러닝 게시글 및 영상을 참조하시면 되겠습니다.)

 

일단 이 프로젝트의 첫 목표는 "Policy Gradient로 오목/지뢰찾기 인공지능 만들기"가 될 것 같습니다. 매주 주말마다 열심히 코드를 짜면서 1학기가 끝나기 전까지 완성이 되기를 바랍니다.

참고로, "밑바닥부터 시작하는 딥 러닝" 서적을 참고하여 제작하기에, 코드가 일부 비슷한 부분이 있을 수 있습니다.

 

 

https://github.com/cdjs1432/DeepLearningBasic

 

cdjs1432/DeepLearningBasic

Contribute to cdjs1432/DeepLearningBasic development by creating an account on GitHub.

github.com

그리고, 이번에 처음으로 깃허브를 좀 활용할까 합니다.

코딩하다 뻘짓해서 코드 날리는 경우가 꽤 흔했던지라 ㅠ

글 올라오면 바로 해당 코드를 깃허브에 업로드 할 예정이니, 전체 코드가 필요하시다면 깃허브에서 가져가시면 되겠습니다.

 

 

일단 지금 구상하고 있는 프로젝트의 목차는 다음과 같습니다.

 

 

1. 기본 머신 러닝

 1-1. Linear Regression / Gradient Descent 구현하기

 1-2. Stochastic Gradient Descent 구현하기

 1-3. Logistic Regression 구현하기 (Iris dataset)

 1-4. Softmax Classification 구현하기 (MNIST dataset)

 

2. 딥 러닝

 2-1. Single-layer Gradient Descent 구현하기

 2-2. Multi-layer Softmax Classification 구현하기 (MNIST dataset)

 2-3. 다양한 활성화 함수 (activation function) 구현하기 (ReLU, Leaky ReLU, maxout, ...)

 2-4. 부록) 코드 리팩토링

 2-5. 다양한 optimization 기법 구현하기 (RMSprop, Adam, ...)

 2-6. Xavier Initialization, Regularization, Dropout 구현하기

 

3. CNN(Convolutional Neural Network) 구현하기

 3-1. Convolution Layer 구현하기 

 3-2. Pooling / Padding 구현하기

 

4. 강화 학습 (원래는 CNN구현을 먼저 하려 했는데, 우선 오목/지뢰찾기 인공지능이 급해서..)

 4-1. 기본적인 Q-Learning 구현하기 (gym 모듈 활용)

 4-2. Policy Gradient 구현하기 (직접 만든 지뢰찾기 활용)

 4-...

 

 

위의 목차는 그냥 단순한 구상일 뿐이고, 앞으로 계속해서 수정해 나가겠습니다.

 

한번 시작해 보겠습니다!

+ Recent posts