인공지능

* 모든 코드는 제 깃허브 (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-...

 

 

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

 

한번 시작해 보겠습니다!

Policy Gradient - 1

- 본 포스팅은 CS234 8강의 내용을 정리합니다.

 

참고로, 8~10강까지의 내용은 비슷한 내용을 주제로 연강을 하게 됩니다.

 

목-차

이번 시간에는 Policy Search, Policy Gradient에 대해 배워보도록 하겠다.

 

Policy-Based RL

저번 시간에, 우리는 파라미터 θ에 대해 (w에 대해) value값 및 action-value (Q)을 찾는 방법을 배웠다.

그래서, 그렇게 생성된 value값을 사용하여 직접 policy를 만드는 방식을 사용했다.

(ε-greedy 방식을 사용해서 최대의 value값을 가지는, 또는 랜덤한 policy를 찾는 방법을 사용했던 것을 생각해보자.)

 

그런데 이번 시간에는, 우리는 policy πθ를 직접 파라미터로 사용할 것이다.

즉, value 값에서 policy를 찾는 것이 아니라, θ (또는 w)의 값을 토대로 policy를 직접 얻을 것이다.

또, 지금까지와 비슷하게 model에 대한 접근 없이, model-free한 RL을 진행하게 될 것이다.

 

Value-Based and Policy-Based

들어가기에 앞서, 사실 reinforcement learning도 policy를 기반으로 할지, value를 기반으로 할지 등에서 조금씩 나뉜다.

 

Value-Based라고 한다면, ε-greedy와 같은 형식으로 policy를 도출하게 되고,

Policy-Based라면 Value function 없이 직접 policy를 배우게 된다.

Actor-Critic은 그 사이에 있는 놈들인데, 나중에 다루게 될 것이니 일단 넘기도록 하자.

Advantage of Policy-Based RL

Policy-Based RL이 가지는 장점과 단점들에 대해 살펴보자.

 

우선, 장점들을 먼저 살펴보자면, Parametrization을 통해 policy를 나타내는 것이 더욱 효율적일 때가 있다. (사실 상황에 따라 다르긴 하다.)

또, high-dimensional / continuous action space의 경우 action-value pair을 사용하는 것 보다 훨씩 효율적이며,

stochastic policy, 즉 무작위적 policy를 가질 수 있게 된다.

value function을 바탕으로 policy를 짜면, 보통 최대의 value값을 갖는 policy를 선택하는데, 이는 stochastic한 policy를 얻지 못하게 막는다.

그런데 왜 stochastic policy가 필요한지는 후술하도록 하겠다.

 

 

단점들도 있는데, 우선 대부분의 policy-based RL은 global optima에 수렴하지 않고, local optima에 수렴하는 경우가 많다.

또, 일반적으로 policy를 계산하는 것은 sample inefficient 하다. (sample의 갯수가 많아야 한다는 뜻)

 

가위바위보

그런데, 아까 전에 stochastic policy를 학습할 수 있는 것이 장점이라고 했는데, 왜 stochastic policy가 필요할까?

 

가장 간단한 예시로, 가위바위보를 하는 Agent를 만든다고 생각해보도록 하자.

만약 모든 policy가 deterministic 하다면 어떨까?

예를 들어, 처음에는 바위를 내고, 전판에 상대가 보를 냈으면 다음판엔 가위를 내고, ... 하는 식으로 학습되었다고 해보자.

그렇게 학습을 끝낸 후 사람과 가위바위보를 시키면 잘 해낼 수 있을까?

가위바위보를 한판만 하는 거면 모르겠지만, 여러 판 하다 보면 결국 사람이 이게 어떤 메커니즘으로 흘러가는지 알게 되고, 그냥 처음에 보를 내고, 그 다음에 주먹을 내고, ... 하는 식으로 간파가 가능하다.

 

즉, 가장 최적의 policy는 사실, 가위바위보 셋 중 하나를 랜덤으로 내는 것이다.

아직 잘 와닿지 않는다면, 다음 예시를 통해 조금 더 와닿게 해 보겠다.

Aliased Gridworld - 1

위와 같은 grid world가 있다고 하자.

당연히 저기 있는 돈꾸러미가 goal이고, 해골 무늬에 들어가면 죽게 된다.

(해골이나 돈꾸러미에서 시작하는 일은 없다고 가정한다.)

Agent는 동서남북 네 방향만을 관찰할 수 있어서, 동서남북의 칸에 벽이 있나 없나만을 알 수 있다.

또, 자신이 하얀 블럭 위에 있는지 회색 블럭 위에 있는지도 알 수 없다.

 

자, 이에 따라 돈꾸러미에 들어가는 policy를 짠다면 어떻게 될까?

Aliased Gridworld - 2

우선, 하얀색의 블럭 세 개에서의 policy는 자명하게 구할 수 있다.

왼쪽 흰 블럭의 policy를 생각하면 : 왼쪽과 위쪽에 블럭이 있고, 아래쪽에 블럭이 없으면 오른쪽으로 간다! 가 될 것이고,

오른쪽 흰 블럭의 policy를 생각하면 : 오른쪽과 위쪽에 블럭이 있고, 아래쪽에 블럭이 없으면 왼쪽으로 간다! 가 될 것이다.

또, 가운데 흰 블럭은 : 위에만 블럭이 있으니, 아래로 간다! 가 policy가 될 것이다.

이렇게 하면 흰 블럭 위에 있을 때는 최적의 policy가 된다.

 

문제는 회색 블럭에 있을 때이다.

저 두 블럭에서는 사실 들어오는 feature가 동일하다. "아래, 위에 블럭이 있고, 왼쪽, 오른쪽은 비어 있다" 라는 feature이다.

하지만 저 feature에 대해 어떤 한 가지의 policy를 정할 수 있을까?

 

만약 "왼쪽으로 간다"라는 policy를 정한다면, 왼쪽 위 흰 블럭 또는 회색 블럭에서 시작하는 경우에는, 영원히 goal에 도달하지 못하게 될 것이다.

반대로 "오른쪽으로 간다"라는 policy를 정한다면, 오른쪽 위 흰 블럭 또는 회색 블럭에서 시작하는 경우에 goal에 영원히 도달하지 못한다.

 

하지만 Value-based learning같은 경우에는 거의 greedy한 방식으로, deterministic한 policy를 정하게 된다.

이렇게 deterministic한 policy를 정하는 경우, 이처럼 갇히게 되는 경우가 발생하여 좋지 못한 policy가 되고 만다.

Aliased Gridworld - 3

그렇다면, 위의 회색 블럭의 policy를 "50% 확률로 왼쪽으로, 50%확률로 오른쪽으로 간다" 라고 잡으면 어떻게 될까?

어디서 시작하더라도, 충분한 시간 뒤에는 결국 goal에 도착하는 이상적인 policy가 완성되게 된다.

 

이것이 바로 stochastic policy가 필요한 이유이고, 이것이 바로 Value-Based RL이 아닌 Policy-Based RL이 필요한 이유이다.

Value-Based RL같은 경우 위와 같은 Stochastic Policy를 학습하지 못하지만,

Policy-Based RL은 저런 optimal stochastic policy도 할습할 수 있기 때문이다.

policy objective functions

이제 본격적으로 policy based RL을 시작해 보도록 하자.

우리의 목표는 최고의 파라미터 θ를 찾아서 parametrized πθ를 찾는 것이다.

그런데, πθ가 얼마나 좋은지 어떻게 알 수 있을까?

 

우선 episodic environments (H steps 또는 terminal까지)의 경우, J(θ)를 start state의 Value값으로 둘 수 있다.

그리고 continuing environments 같은 경우, πθ의 평균값을 사용하거나, time-step당 reward의 평균을 사용한다.

오늘은 대부분 episodic 한 경우만 다루겠지만, 손쉽게 continuing / infinite horizon인 경우로 연장시킬 수 있다.

 

Policy Optimization

그리고 알아둘 점이 하나 있는데, 바로 policy based RL은 "optimization" 문제라는 것이다.

즉, 최대의 Vπθ를 갖는 parameter θ를 구해야 한다는 것이다.

 

물론 gradient-free한 방식도 사용할 수는 있다. (위 ppt에 나열된 방법)

하지만, 요즘에는 일반적으로 gradient를 사용하는 방식을 자주 사용한다. (강의명도 policy gradient다!)

 

Gradient-Free 예시

Gradient-Free 방식을 사용한 예시 중 하나가 바로 위 예시이다.

사람이 걷기 편하게 보조하는 기계를 학습시키는 것인데, 자세한 내용은 다음 유튜브를 참고하기 바란다.

https://www.youtube.com/watch?v=Dkp_xjbRYCo

Gradient Free PO

그리고, 사실 Gradient Free한 방식은 꽤나 잘 먹힌다. 그래서 간단한 baseline으로 자주 사용된다.

어떠한 policy parameterization을 대상으로도 잘 먹히고, 심지어는 미분 불가능한 경우에도 잘 작동하는 방식이다.

또, 병렬화 작업도 매우 쉽게 된다.

 

하지만, 치명적인 단점이 있는데, 바로 sample-efficient 하지 못하다는 것이다.

즉, 학습하는 데 매우 많은 데이터가 필요하다.

 

 

Policy Gradient

그러니까 Gradient-Free 방식은 이제 저기로 제쳐두고, policy gradient 방식에 대해 알아보자.

 

Policy Gradient

그렇다면 Policy Gradient가 그래서 어떻게 작동하는지 설명해보겠다.

우선 V(θ) = Vπθ로 둔다. 그리고 아까 말했다시피, 오늘은 episodic MDP의 경우만을 생각하자.

 

그러면 Policy Gradient 알고리즘을 사용하여 V(θ)의 local maximum을 찾을 수 있다. (Gradient Descent 사용)

그렇게 θ에 대한 V(θ)의 local maximum을 찾을 때 사용되는 식은 위 ppt에서 볼 수 있다.

이 떄, 언제나 그래왔듯 α는 learning rate이다.

 

Finite Difference를 사용한 Gradient 계산

그래서 이제 Gradient 계산을 해야 하는데...

필자는 아직 고등학생인지라 수학에 대해 아직 좀 많이 무지하다.

Finite Difference가 뭔뜻인지 (한정된 차이?는 아니지 않겠는가;;) 인터넷에 검색해 보니까, "유한 차분법" 이라는 방법이라고 한다.

그래서 이번 슬라이드는 완벽하게 이해하지 못했다.

 

그래도 이해한 것이라도 끄적여보자면,

우선 닫힌 공간 [1, n]의 있는 모든 k에 대하여, θ에 대한 k의 편미분을 계산한다.

그리고 그 편미분 값을 조금 바꿔서(약간 증가시키거나 감소시킨다는 얘기인가?) 실제 미분값에 근사시킨다.

 

그 뒤, n dimension을 따라 n번 evaluate해서 policy gradient를 계산한다.

이 방식은 간단(??)하지만, 약간 노이즈가 있고 inefficient한 방식이다. 하지만, 가끔 효율적으로 사용되는데,

미분 불가능한 policy를 포함해 어떠한 policy에도 작동할 수 있기 때문이다.

 

Training AIBO

이 방식을 사용하여, 로봇 AIBO를 걷게 시킨 실험도 있다. (2004년)

AIBO Policy Parameterization

어떻게 parameterization을 했는가 보니,

state를 두지 않고, 위 슬라이드에 나온 대로 parameterization을 진행하여, 각각의 파라미터에서 랜덤으로 -ε, 0, ε 셋 중 하나를 더해 policy를 얻었다고 한다.
 

AIBO Policy Experiments

 그런데 위에서 이거 2004년에 했다고 했는데, 이걸 어떻게 computational effecient 하게 진행했냐면,

우선 로봇 3개를 한번에 구동한다.

그리고 각각 iteration마다 policy 15개를 evaluate한다.

그리고, 각각의 policy도 3번씩 evaluate하고 (noise를 줄이려고) 그것의 평균을 구한다.

이렇게 했더니, 각 iteration이 7.5분 안에 끝났다고 한다.

Training AIBO

그렇게 traning 시켰더니, 꽤 잘 하는 모습을 보여줬다고 한다.

그리고 이 AIBO의 성능은 초기 starting policy parameter, ε (더하고 빼지던 정도), α (learning rate), 그리고 policy parameterization 등이 영향을 미쳤다고 한다.

Computing gradient analytically

그리고, 이번에는 방금의 Finite Difference 방식이 아니라 그냥 analytical 하게 gradient를 구하는 방법을 알아보자.

당연하게도, 이 방식을 사용하려면 policy πθ가 미분 가능해야 하고, gradient를 알고있다고 가정해야 한다.

(즉, 이것 자체가 연산 가능하다는 것을 알고 있어야 한다는 것이다.)

 

Likelihood Ratio Policies (직역하면 가능도 정책)

그리고 이 방식을 사용할 때는, likelihood ratio policy라는 것을 사용한다.

 

우선, 이 방식은 Episodic 한 방식에서만 사용되며, 예전부터 해왔듯이 trajectory를 설정한다.

(당연히 episodic이므로 마지막에 Terminal이 있어야 한다.)

 

그리고, Reward function R은 그냥 trajectory의 reward값의 평균이다.

또한, 이렇게 두면 Policy value는 해당 policy를 따랐을 때의 Reward값의 평균이 되는데, 이를 다시 쓰면 해당 episode에서, 즉 trajectory를 따라갈 때 얻는 Reward R과 확률 P의 곱의 평균이다.

(policy에서 확률이 왜 나오냐 할 수도 있겠지만, 이제는 이것이 deterministic하지 않는다는 것을 알아야 한다!)

 

그리고, 이제부터 나오는 대부분의 수식들에는 Reward Function이 그대로 쓰이기 보단, 위의 형태와 같이 Probability P와 R의 곱 형태로 나오게 될 것이다.

Policy Gradient under Likelihood Ratio Policy

그리고 이 상태에서, 위 V(θ)의 Policy Gradient를 구하면 (θ에 대해 Gradient한다), 위와 같게 된다.

딱히 설명할 필요는 없겠지만 대충이라도 설명을 해 보자면,

첫 번째 줄에는 그냥 양변에 gradient를 씌워 준 것이고, (이 표현이 맞는지는 모르겠지만... 뭔뜻인지 알겠지??)

어차피 θ를 인수로 갖는 것은 P(𝛾;θ) 뿐이니 저 시그마 안으로 들어가 준 다음에,

P(𝛾;θ)/P(𝛾;θ) 를 곱해준다. (1을 곱한 것과 같다.)

 

그리고, P(𝛾;θ)를 분수 아래로, ∇θP(𝛾;θ)를 분수 위로 올리면, P(𝛾;θ)R(𝛾) * ∇θP(𝛾;θ)/P(𝛾;θ)가 된다.

그런데, ∇θlog(P(𝛾;θ))1/P(𝛾;θ) * ∇θP(𝛾;θ)라고 한다.(왠지는 필자도 잘 모르겠다 ㅠㅠㅠ)

 

그래서 이를 정리하면, 맨 아래 식이 나온다.

 

 

이 식을 사용하는 이유는, policy를 사용하여 trajectory 몇 개를 sampling 한 뒤에, reward를 조금 구하면서 Gradient를 구할 수 있기 때문이다.

즉, 어떻게 Gradient를 구해야 할지에 대한 방법을 얻게 된 것이다.

 

 

Likelihood Ratio Policy Gradient

어떻게 Gradient를 구하는지 조금만 더 자세히 보자면,

우리의 목적은 policy parameters θ를 찾는 것이므로,

θ에 대해 Gradient를 아까 위에서 한 것처럼 구한 뒤에,

그 값을 그냥 policy 몇 번 돌려서 구한 값과 유사한 값으로 맞춰주면 되는 것이다.

What are we doing?

...그런데 우리가 지금 뭘 하고 있는걸까?

그냥 일반적으로 Gradient Descent를 사용하는 다른 알고리즘을 생각하면 편하다.

우리는 지금, Gradient를 구하는 방법을 알게 되었다.

그런데 우리의 trajectory는 Policy Parameter θ에 따라 달라지지 않겠는가!

그러므로, 현재 parameter가 가지는 reward값이 가장 커지도록 parameter값을 바꿔준다고 생각하면 된다.

 

조금 더 자세히 하자면, 그 sample이 얼마나 좋은지에 비례해서 그 sample이 나오게 만드는 확률을 높여주는 방향으로 진행된다.

현재 trajectory의 state에서 앞으로 가는 것이 가장 reward가 높은 행동이라면, 앞으로 가는 확률(로그가 씌워져 있긴 하지만)을 높이는 방향으로 parameter를 움직인다.

Intuition!!

조금 더 직관적으로, 위 그래프를 보자.

f(x)는 Reward Function이고, P(x)는 trajectory이다.

그래서, 좋은 reward를 갖는 trajectory를 갖게 parameter를 조정하게 된다. (위 예시에서는, 아마 가장 오른쪽의 값이 가장 reward가 높은이다.)

그냥 이게 다다. 매우 직관적이지 않은가?

 

Decomposing the Trajectories Into States and Actions

이제 우리가 할 것은, 위 Trajectory를 State와 Action에 대한 식으로 풀어 쓰는 것이다.

일단 위 식에서 Trajectory를 갖는 ∇θlogP(𝛾⁽i⁾;θ) 를 보면, parameter θ를 가질 때, i번째 trajectory의 Log Probaility의 미분값이다.

그런 일단, P(𝛾⁽i⁾;θ) 이 부분만 위의 식 μ(s₀)𝚷πθ(at|st)P(st+1|st, at) 라고 나타낼 수 있다.

왠지 대충만 설명하자면, 결국 P(𝛾⁽i⁾;θ)의 의미는 i번째 trajectory의 Prob을 의미하는데,

현재 policy에서 state가 주어졌을 때 action이 나올 확률에 dynamics model P를 곱해준 뒤에, initial state distribution까지 곱해주면, 결국 같은 것을 의미하는 식이 되는 것이다.

 

그리고 로그를 다시 안쪽으로 넣어주면, product pi 는 sigma가 되어 이와 같이 쓰일 수 있고,

그런데 이를 또다시 θ에 대해 미분해 주면, 신비로운 일이 잇어난다.

우선 왼쪽의 logμ(s₀)는 θ와 독립적이므로, θ에 대한 편미분을 하면 0이 된다.

오른쪽의 P(st+1|st, at) 또한 θ와 독립적이므로, θ에 대한 편미분값은 0이다.

그러면 결국엔 θ를 갖는 식 하나, 가운데 log(πθ(at|st))만 θ에 대해 편미분해주면 되는, 간단한 현상이 일어난다.

 

그래서 결국 마지막 식이 도출되게 된다.

근데 우리가 왜 이 짓을 했을까?

첫 번째 식과 마지막 결과식을 비교해 보자.

첫 번째 식에는 trajectory가 포함되어 있으므로, dynamics model이 무조건 필요하게 된다.

그런데, 이렇게 trajectory를 state와 action에 대한 식으로 바꿔 주니, dynamics model이 더 이상 필요하지 않다.

그냥 단순한 policy의 logprob값의 편미분값으로만 위 식이 표현이 되는 것이다!

 

Score function

그리고 우리는 위 식을, Score Function이라고 부르게 된다.

LRSFPG (ㅎㅎ;)

이제 지금까지 나온 내용을 다시 뭉쳐보자면 위와 같다.

우리는 좋은 parameter θ를 찾고자 한 것이다.

그리고, V(θ)의 편미분값은 사실 위 (1/m)Σ(i=1 ~ m)R(𝛾(i))∇θlog(P(𝛾(i); θ))이다.

또, 아까 전에 저 P(𝛾(i); θ)을 풀어 쓴 대로 써보면, (1/m)Σ(i=1 ~ m)R(𝛾(i))θlog(πθ(at|st))가 된다! 

 

위 식을 조금만 풀어서 쓰자면, trajectory를 m번씩 돌 때, 각각의 trajectory에서 나온 Reward값의 평균에다가,

θlog(πθ(at|st))를 곱한 식이 된다.

즉, θlog(πθ(at|st))를 score function으로 갖는 함수가 완성된다. (reward에다가 저 값들만 곱하면 Value값이므로)

또한, 이것도 마찬가지로 dynamics model이 필요없는, 매우 간결한 식이 된다.

 

그리고, 이 Policy Gradient Theorm은 뒤 식을 약간 일반화 시킨 식이다.

policy objective function J를 J1(episodic reward), JavR(time step당 평균 reward), 1/1-𝛾 * JavV(평균 value)라고 한다면

다음과 같은 식이 써진다. (위 ppt 참조 ㅎㅎ)

 

unbiased but noisy

그런데, 위와 같은 식은 unbiased하긴 하지만 매우 noisy하다.

대충 딱 보면 monte Carlo 방식이 떠오르지 않는가?

사실상 trajectory의 episodic reward라던지, trajectory의 평균 reward를 갖고 계산한다는 점이 말이다.

그래서, 이러한, 매우 noisy하다는 문제점을 해결하기 위해 여러 가지 방법을 사용한다.

오늘은, Temporal structure과 Baseline에 대해 알아보고, 다음 시간에 추가적인 테크닉들을 설명하겠다.

 

Temporal Structure

우선, Temporal Structure는, 말 그대로 우리 model에게 "지금 우리가 하고 있는 것은 temporal 한, 즉 시간적 구조가 나타나는 일이란다" 하고 알려주는 것이다.

 

이것을 하기 위해서, 위와 같은 공식들이 유도된다.

한번 쭉 읽어보면 딱히 필자가 유도하지 않더라도 어느 정도는 다 이해할 것이라 믿고 넘기겠다.

...사실 필자가 이해하지를 못하겠다 ㅠㅠㅠ

혹시라도 필자를 이해시켜줄 수 있는 용자가 있다면, 댓글로 조언 부탁바란다 ㅠ

(사실 이 강의 설명이 이렇게까지 늦은 이유도, 수식들 하나하나 다 이해하면서 넘어가느라 그랬는데, 여기서부턴 도저시 모르겠다 ㅠㅠ)

 

다만, 마지막 줄에서 logπθ(at, st)는 logπθ(at|st)의 오타인 것 같다.

Policy Gradient: Temporal Structure
Monte-Carlo Policy Gradient

 

 

위 내용들은 나--중에, 필자가 저 수식들을 이해할 정도의 실력이 되면 그때 다시한번 도전해 보도록 하고,

다음 시간엔 Policy search에 대한 더 많은 이야기들을 해 보도록 하겠다.

그럼... 좀 씁쓸하긴 하지만...

안녕!

- 훈련 environment : CarRacing-v1 (https://github.com/NotAnyMike/gym)

 

NotAnyMike/gym

An improvement of CarRacing-v0 from OpenAI Gym in order to make the environment complex enough for Hierarchical Reinforcement Learning - NotAnyMike/gym

github.com

https://notanymike.github.io/Solving-CarRacing/

 

Solving CarRacing with PPO - Mike.W

Solving Car Racing with Proximal Policy Optimisation I write this because I notice a significant lack of information regarding CarRacing environment. I also have expanded the environment to welcome more complex scenarios (see more). My intention is to publ

notanymike.github.io

위 링크를 참고하여 제작했습니다.

 

 

Proximal Policy Optimization (줄여서 PPO) 알고리즘을 사용하여 훈련하였습니다.

timestep 100만회 colab에서 훈련시켰고, 어느 정도 쓸만한 결과가 나왔습니다!

 

(1)

 

(2)

이렇게 좋은 모습들을 보이기도 하는 반면,

어디서 막혔는지;;; 가끔 보면 이상한 장면들도 나오곤 합니다.

 

???

너무 굴곡이 깊거나 하면 그냥 그대로 탈선해서 돌아오려고 하지도 않는 모습을 보이기도 하고,

 

??????? 너 왜 가만히 있냐?

????????

이해할수도 없이 그냥 멈춰있기도 하고;;;

 

음주운전???

갑자기 Agent가 술마셨는지 시작하자마자 어기적대면서 그대로 풀로 들어가서 주차하기도 하고;;;

 

 

한 천만번 정도는 돌린 다음에 하면 거의 완벽하게 되지 않을까 싶습니다.

Imitation Learning

- 본 포스팅은 CS234 7강의 내용을 정리합니다.

* 강의 앞부분에 DQN을 정리하는 부분이 있는데, 그 부분은 그냥 빼고 설명하겠습니다.

 

목차

오늘 배울 것들로는,

 

Behavioral Cloning, Inverse Reinforcement Learning, Apprenticeship Learning 등이 있습니다.

 

지금까지 우리는 Optimization과 Generalization에 대해서 배웠었다.

어떻게 최적의 policy를 찾아가는지, 그리고 어떻게 그것을 일반화 시킬지에 대한 이야기를 많이 했었는데,

이번에 볼 것은 바로 Efficiency, 즉 효율성이다.

컴퓨팅 파워를 많이 사용하지 않고 최적의 policy를 찾는 방법에 대해서 알아볼 것이다.

 

Efficiency

일반적인 MDP에서는, 좋은 policy를 찾기 위해서는 굉장히 많은 양의 sample들이 필요했다.

가령 DQN같은 경우, 굉장히 오랫동안 훈련시켜야, 즉 최대한 많은 양의 sample들이 있어야 좋은 성적을 낼 수 있었다.

 

그런데, 실제 강화 학습의 경우 이런 sample들을 얻기란 쉽지 않다.

만약 우리가 우주선을 발사하는 강화학습 Agent를 만들어야 한다면 어떨까?

수없이 많은 우주선 발사를 실패해야만 진짜 제대로 된 우주선 발사를 볼 수 있을 것이다.

그렇게 되면 천문학적인 비용이 깨지게 될 것이므로, 사실상 이런 방식의 RL로는 우주선 발사는 택도 없다는 것을 알 수 있다.

 

자율주행 자동차도 비슷하다. 스스로 운전하는 자동차를 만들어야 하는데, 수없이 많은 사고 이후에야 운전을 제대로 할 수 있다면, 아무도 그 비용을 감수하지 않으려고 할 것이다.

 

그렇다면, sample의 갯수를 줄일 수 있지 않을까?

그냥 아무것도 알려주지 않은 상태로 Optimal policy를 얻기를 기대하기 보다는,

이 강화 학습 과정을 도와줄 추가적인 정보나 구조들을 알려준 뒤에 훈련시키면 되지 않을까?

 

오늘은 이 아이디어를 Imitation Learning을 통해 살펴보자.

 

지금까지 배웠던 것들

지금까지 우리들은 Reward를 통해서 Agent를 학습시켰었다.

DQN, Q-learning, MC 등등 모두 다 reward function을 사용하여 최대의 reward를 얻을 수 있도록 하는 것이 주요 포인트였다.

이 방식은 매우 간단한 방식으로 훈련이 가능하다는 점에서 좋지만, 아까 위에서 언급했듯 너무 많은 sample을 요구한다는 단점이 있다.

 

이는 데이터를 얻기 쉬운 가상의 환경 (시뮬레이터 등)이라면 큰 문제가 안되겠지만, 아까 위에서 말했던 우주선이나 자율주행 자동차같은 예시의 경우에는 이런 방식은 올바르지 못할 수 있다.

 

Reward Shaping

그리고, reward를 산정하는 방식도 조금 생각을 해 보자.

가령 자율 주행 자동차의 reward를 산정하려면 어떻게 해야할까?

 

만약 이 reward를 사고날 때 -10, 안나면 +0.1 이런 식으로 두면 어떨까?

그러면 아마 이 Agent는 어떻게 해야 사고가 나고 어떻게 해야 사고가 나지 않을지 알아내느라 한참을 고생할 것이다.

 

그러면 모든 상황에 대해서 적절한 reward를 대입해주면 어떨까?

우선 이 방식은 너무 오랜 시간이 걸리기도 하고 (초마다 reward를 넣어준다고 해도 1시간동안 운전한다면...),

이렇게 reward를 정해준다고 해도 reward의 상태가 매우 불안정해질 수 있다.

 

이를 보완하기 위한 대안책으로는, 바로 reward를 demonstration, 즉 실제로 어떻게 하는지 보여주면서 reward를 implicit하게 주는 것이다.

 

Learning from Demonstration

이렇게 demonstration으로 reward를 산정하려면 어떻게 해야할까?

바로 학습시킬 일의 전문가를 데려와서 demonstration trajectory를 만들어 학습시키는 것이다.

 

가령 자율 주행 자동차를 만든다고 한다면, 운전을 매우 잘하는 어떤 사람을 데려와서 실제로 한번 운전시켜 보는 것이다.

그렇게 얻은 State/action sequence들을 강화 학습 Agent에게 줌으로써 그것을 바탕으로 학습시키면 된다.

 

이 Imitation learning 방식은 reward를 일일히 부여하거나 특정 policy를 따르도록 하게 하려는 것이 아닐 경우에 효율적이다.

 

Problem Setup

이제 Imitation learning의 기본적인 setting에 대해 설명해 보겠다.

 

우선, Input은 지금까지와 비슷하게 State space와 action space로 이루어져 있고, Transition model P가 주어진다.

다른 점은, reward function R은 주어지지 않는다. 그 대신, (s0, a0, s1, a1, ....) 과 같은 demonstration을 주어준다.

(위에서 설명했던 것과 비슷하다.)

 

이제부터 Behavioral Cloning, Inverse RL, Apprenticeship learning에 대해 배울 것인데, 각 종류에 대한 목표 과제는 다음과 같다.

 

Behavioral Cloning : supervised learning을 통해 스승(전문가)의 policy를 직접 배울 수 있게 하자!

 

Inverse RL : reward function R을 demonstration을 통해 얻을 수 있을까?

 

Apprenticeship learning : R값을 좋은 policy를 생성하는데 사용할 수 있나?

 

이제부터 Behavioral Cloning부터 차근차근 알아가 보자.

Behavioral Cloning

Behavioral Cloning 방식은 이 강화 학습 문제를 기존의 머신 러닝 문제를 푸는 방식대로 생각하는 것이다.

즉, 원래 자주 사용하던 Supervised learning 방식을 사용하자는 것이다.

 

우선 policy class를 설정해 두고, (인공 신경망, decision tree 등등을 사용할 수 있다.)

expert의 state와 action의 sequence를 supervised learning model의 input/output으로 두고 Agent를 학습시킨다.

자율 주행 자동차를 예시로 들자면, 만약 왼쪽으로 코너를 돌 때의 action이 대부분 핸들을 왼쪽으로 꺾는 것이라면,

supervised learning model은 다음부터 왼쪽으로 돌아야 한다는 state를 받을 때 action은 핸들을 왼쪽으로 꺾어야 한다고 학습할 것이다.

Behavioral Cloning Problem: Compounding Errors

하지만, 이 Behavioral Cloning 방식에는 큰 문제가 하나 있다.

Compounding Error (직역하면 합성되는 에러..?)라는 문제점이 바로 그것이다.

 

이는 Supervised learning은 모든 데이터가 iid라는 것을 전제로 한다는 것 때문에 발생한다.

여기서 iid란 각각의 데이터들이 독립적이고 동일한 확률분포를 가진다는 것을 의미하는데, 조금 더 쉽게 하자면 그냥 모든 데이터 아무거나 하나를 뽑아도 다른 데이터와 연관이 없다는 것을 전제한다는 것이다.

(그래서 Supervised learning에서는 데이터가 어떻게 주어지든 그 데이터의 순서를 마구 섞어서 input으로 집어넣어도 된다.)

그런데, 분명 우리가 주는 데이터 state, action pair은 시간의 흐름에 따라 이어지는 데이터이다.

즉, (s0, a0, s1, a1, s2, a2, ...)라는 데이터에서 분명 s0이 s1보다 먼저, s1이 s2보다 먼저 있는 state라는 것이다.

 

하지만 Supervised learning에서는 이러한 데이터의 시간적 구조를 싸그리 무시하고, 모든 데이터를 iid라고 가정하기 때문에,

언제 어떤 state였는지는 중요하지 않고, 그냥 특정 state에서 특정 action이 취해지길 바라고 있는 것이다.

 

자율 주행 자동차의 예를 다시 한번 들어보자.

사람의 경우, 고속도로를 달리는 중 휴게소가 가고 싶다면 휴게소 쪽으로 차선을 옮기게 될 것이다.

어떤 특정 state와는 관계 없이 (휴게소가 아주 가까이 있지 않더라도) 휴게소 쪽으로 차선을 옮기는 것이다.

하지만 Supervised learning 기법으로 학습한다면, 어떤 상황에 왜 차선을 변경하는지는 알지 못하고, "아, 저 state에서는 차선을 바꿔야겠구나!" 생각하고 action을 선택하게 된다.

 

이런 것들이 하나 둘씩 쌓이다 보면, error가 굉장히 커지게 된다.

 

Behavioral Cloning Problem: Compounding Errors 2

또 다른 예를 들어보자.

위 사진에서 보면, Expert가 운전한 대로 Agent가 운전을 하고 있다가, 특정 구간에서의 Error로 인해 코너링 초반에 실수를 조금 했다. (조금 더 바깥쪽으로 코너링을 시도했다.)

하지만, expert는 그런 실수를 하지 않았기에, (그리고 하지도 않을 것이기에,) 저런 상황에서 어떻게 복구해야 하는지 알 길이 없다.

그러면 time step t에서의 실수 하나로 인해 그 이후의 time step t+1, t+2에서도 계속 error가 생길 수 밖에 없고,

그러다 보면 결국 학습에 실패하게 되는 것이다.

 

DAGGER : Dataset Aggregation

이 문제를 해결하기 위해 DAGGER : Dataset Aggregation이라는 방식을 사용한다.

 

아이디어는 놀라우리만큼 간단하다.

그냥 잘못된 길을 가는 경우 expert에게 어떤 action을 취해야 할지 알려달라고 하는 것이다.

그러니까 코너링을 잘못 돌았을 때, expert에게 "이런 경우엔 어떻게 해야되요?" 라고 물어보고,

expert는 "이렇게 코너링을 돌아야 한단다 ㅎㅎ" 라고 알려주는 것이다.

 

아이디어만 들어도 알겠지만, 이 방식은 모든 상황에서 효율적으로 쓰일 수 있는 방식은 아니다.

우선, 정말 짧은 time step 간의 state에 대한 action이 필요한 경우에는 사실상 이러한 방식이 불가능하다.

자율 주행 자동차 같은 경우, 정말 짧은 시간동안 어떻게 운전할지가 중요한데, 이런 경우에는 모든 잘못된 경우마다 어떻게 해야 하는지 알려주는 것은 힘들 것이다.

 

그러나, 만약 그렇게 디테일한 정보까지는 필요 없고 간단한 정보만 필요한 경우에는 사용할 만한 가치가 있을 것이다.

가령, 예시가 간단한 Frozen lake같은 게임을 플레이 하는 것이라면, 굉장히 사용할 만 할 것이다.

오목같은 경우에도, 조금 힘들긴 하겠지만 어떻게든 사용해 먹을 수는 있을 것이다.

 

그런데 사실 일반적인 경우에는 쓰기 힘들다는 것을 알 수 있다. GPU로 훈련하는 중에 action을 어떻게 효율적으로 집어 넣어줄 것인가?

수시간, 길게는 수십시간 동안의 훈련 시간동안 expert가 컴퓨터 앞에 쭉 앉아 있을 수도 없고 말이다.

그런 단점들 때문에, 후술할 다른 방식들 보다는 이 쪽 업계에 미친 영향이 조금 적다고 한다.

 

Inverse RL

다음으로 알아볼 방식은 Inverse RL이라는 방식이다.

 

 

Feature Based Reward Function

이 방식은 (위에서 짤막하게 설명했듯이) expert의 (위 슬라이드에서는 teacher라고 명명한다.) policy를 보고, reward function을 찾아나가는 방식이다.

 

아까 전에 설명한 것이지만 다시 세팅을 알려주자면,

state space, action space, transition model P가 주어지지만, reward function R은 주어지지 않는다.

그 대신, teacher의 demonstration (s0, a0, s1, a1 ...)을 받게 된다.

 

이제 이 Inverse RL은 reward function R을 teacher의 demonstration을 통해 알아가게 된다.

 

여기서 간단하게 질문 하나를 해보자.

만약 teacher의 policy가 optimal하다는 전제가 없다면, 위 demonstration은 R에 대한 어떤 정보를 줄 수 있을까?

 

정답은, 간단하게 "줄 수 없다" 이다.

그냥 간단하게, teacher의 policy가 말짱 꽝이라고 해보자. 자율주행 자동차의 경우, 그냥 직진만 한다고 해보자.

그러면, 이 teacher가 운전하는 모습을 보고 운전을 처음 하는 사람이 뭔가를 배울 수 있을까?

뭐가 좋은 것이고 뭐가 나쁜 것인지 알 수 있을까?

당연하게도 알 수 없다.

즉, 우리가 이 Inverse RL 방식을 사용하려면 teacher의 policy는 optimal하다는 전제가 있어야 한다.

(아니, 그래도 teacher의 policy가 어느 정도 이성적으로 판단한 거 아닌가? 라고 생각할 수도 있겠지만, 그런거 베재하고 생각하는 것이다.)

 

 

그렇다면, teacher의 policy가 optimal하다고 가정했을 때 reward function은 unique할까, 아니면 여러 개가 있을 수 있을까?

(단, 데이터는 충분히 존재한다고 가정한다.)

 

 

정답은, 여러 개가 존재할 수 있다 이다.

이유는 간단하다. 그냥 모든 reward에다가 0을 던져버리면, 어떤 optimal policy가 있더라도 모두 다 동일한 reward를 가지게 될 것이다.

비단 0이 아니더라도 모든 action에 따른 reward를 1, 2, 3과 같은 동일한 상수값을 던져주게 되면, 어떤 policy라도 optimal하게 된다.

 

조금 더 쉽게 설명해 보겠다.

만약, 선생님이 당신에게 C언어에서 출력을 어떻게 하는지 알려주고 있다고 해보자.

printf("%d",a+b); 라는 코드를 짜 줄수도 있고,

printf("%c",a); 라는 코드를 짜 줄수도 있을 것이다.

그러면 일반적인 상식적으로, 우리는 저 앞의 printf("% 부분의 reward는 높을 것이고, 또 맨 뒤에 );의 reward도 높을 것이라고 예측할 수 있다.

그런데 만약 위 두 case의 각각의 철자마다 그냥 reward가 0이라고 해버린다면 어떨까?

그러니까, 선생님이 어떤 코드를 짜던지간에, "아몰라 저거 다 reward 0이라고 하면 되는거잖아? 그러면 내가 뭔 코드를 짜던지 간에 내 코드도 optimal policy로 짜지는 코드인데??" 라고 할 수 있다는 것이다.

즉, printf에 가중치가 부여되는 것이 아니라, 그냥 drqctz나 printf나 똑같은 reward를 갖게 되는 것이다.

그런데 당연히 이것을 의도하고 코딩하는 법을 가르쳐 준 것이 아니지 않겠는가!

 

이런 점에서, 이 Inverse RL은 큰 문제에 봉착하게 되었다.

 

그렇다면 이 Inverse RL의 문제를 어떻게 해결할까?

우선 저번에 배웠던 Linear value function approximation을 다시 가져와 보자.

R값은 여러 개가 존재할 수도 있다고 했지만, 그냥 일단 R값을 wT x(s)라고 둬보자.

(이 때, w는 weight vector이고 x(s)는 state의 feature이다.)

그래서, 이 weight vector w를 주어진 demonstration을 통해 찾아내는 것이 목표이다.

 

우선, policy π에 대한 Vπ값을 E[Σt=0-->∞ 𝛾^t*R(sₜ)|π] 라고 할 수 있다.

(즉, Reward의 discounted sum이다. 수식을 예쁘게 쓰고 싶은데 수식 편집기가 없어서 ㅠㅠ 위 슬라이드를 보며 이해하면 좋겠다.)

이것에 위에서 정의한 R(s) = wTx(s)를 대입하면,

E[Σt=0-->∞ 𝛾^t*wT*x(sₜ)|π]라고 할 수 있다.

 

또, 모든 π에 대하여 wT의 값은 동일하므로, wT값을 앞으로 넘겨서 

wT*E[Σt=0-->∞ 𝛾^t*x(sₜ)|π라고 할 수도 있다.

그러면 이제 거의 다 끝났다.

E[Σt=0-->∞ 𝛾^t*x(sₜ)|π]의 값을 그냥 μ(π)라는 값으로 두면,

최종적인 Vπ의 값을 wT*μ(π) 라고 할 수 있다.

 

그런데, 여기서 μ(π)(s)가 의미하는 바가 무엇인가?

(참고로, 뒤에다가 (s)붙인거 오타 아니다. μ(π)는 벡터이므로...)

각각의 time step t에서 나타나는 state feature x(s)에다가 discount factor 𝛾^t를 곱한 것이다.

그리고 거기에다가 w의 transpose를 곱하므로, 이는 각 state feature의 weighted discounted frequency를 나타내는 값과 동일해 진다.

즉, 우리가 학습시킨 weight vector w값에다가 자주 등장하는 state feature의 값을 곱해주는 것이다.

 

자, 그럼 이 값이 잘 만들어진 값인지 보자!

Inverse RL에서 우리의 목표는 무엇이었는가?

바로 (optimality가 전제된) teacher의 policy를 보고, 그 policy를 토대로 reward function이 어떻게 되어 있을지 찾아가는 것이었다!

그러면, teacher의 policy를 봤을 때 자주 보이는 state feature의 실제 reward 값은 어떨까?

당연하게도, optimality가 전제되어 있으므로, 자주 보이는 state feature를 갖는 state의 reward는 높을 것이다.

비슷하게, 거의 보이지 않았던 state feature를 갖는 state의 reward값은 일반적으로 낮게 될 것이다.

 

이것도 자율주행 자동차를 생각하면 편할 듯 하다.

왼쪽으로 코너를 돌 때, 차선을 잘 맞춰서 도는 일이 빈번할 것이므로 (optimal한 policy였으므로...) 그러한 state에서는 당연히 높은 reward값을 가질 것이다.

또, 왼쪽으로 코너를 돌 때 웬만하면 왼쪽으로 막 틀어지거나 하는 일은 없을 것이므로, 그 state의 경우는 reward가 낮게 측정되는 것이 일반적이다.

 

이렇게, 비교적 간단한 수식으로 Inverse RL을 수행할 수 있다.

그리고 이런 방식을 사용하면, 모든 경우에 reward를 0을 던질 일은 없어지므로, 제기되었던 문제를 어느 정도 해결할 수 있게 되었다!

Apprenticeship Learning

다음 방식은 Apprenticeship Learning이다.

Linear Feature Reward Inverse RL

사실 Apprenticeship Learning도 Inverse RL과 굉장히 비슷하다.

방금 전까지 한거 그대로 이어서, 조금 더 잘 만든 버전이라고 생각하면 될 듯 하다.

Vπ = wTμ(π)부분까지는 그냥 동일하다고 보면 된다.

위 ppt 참고해서 보면, V*는 언제나 Vπ보다 크거나 같다. (기억안날까봐 - *는 optimal함을 의미함.)

그러므로, expert의 demonstration이 optimal policy에서 온 것이라면, w를 찾기 위해서는

w*Tμ(π*) >= w*Tμ(π) 를 만족하는 w*를 찾을 필요가 있다.

 

수식을 해체해서 설명하자면,

μ(π*)는 expert가 주는 optimal한 policy이므로 우리가 이미 알고 있는 값이고,

μ(π)는 expert가 주는 policy를 제외한 다른 어떤 policy를 의미한다.

즉, optimal policy의 값을 정말 optimal하게 만들어주는 w*의 값을 찾아야 한다는 것이다.

(만약 위의 수식을 만족하지 않는다면 reward function R에서 V값이 optimal policy보다 높은 policy가 존재한다는 것인데, 이건 말이 안되지 않는가!)

 

**설명이 약간 부실한 것 같아서 부연설명하자면... (이미 이해했으면 걸러도 됨)

지금 문제 상황은 우리가 모르는 reward function R값이 있다는 것이다.

분명 expert는 (무의식적으로라도) reward function 값들을 알겠지만, 우리 (Agent)는 그 값을 모르니, 그 값을 찾아가겠다는 것이다.

그런데 expert가 optimal하게 움직인다는 것은, 당연히 그 reward function을 통해 얻을 수 있는 최적의 움직임을 하고 있다는 뜻이다.

그러니 저 위의 수식을 만족하는 w값을 찾을 수 있어야 한다는 것이다.

(이래도 이해 안되면 댓글로 ㅎㅎ..)

feature matching

그래서 저 수식을 만족하는, 즉 expert policy가 다른 어떤 policy보다 잘 작동하는 reward function을 찾고 싶다는 것이다.

그리고 만약 우리의 policy π와 π*에서의 Value 값이 충분히 비슷하다면, 우리는 π*, 즉 optimal policy 수준의 policy를 찾아냈다고 할 수 있을 것이다.

 

조금 더 정확히 말하자면, 어떤 ε에 대하여 ||μ(π)-μ(π*)||₁<=ε를 만족하는 π를,

그리고 ||w||∞ <=1을 만족하는 모든 w에서 |wTμ(π) - wTμ(π*)|<=ε을 만족하는 w를 구해야 한다는 것이다.

*참고 : ||X||₁은 L1 norm을, ||X||∞는 L infinity norm을 의미한다. 자세한건 구글링으로 알아보시길...

 

수식을 또 간단히 하자면 (약간 의역하자면), 모든 state에 대해서 μ(π)의 값과 μ(π*)의 값의 차이가 ε이하이고,

모든 값의 절대값이 1보다 작은 w값에 대해서 wTμ(π)와 wTμ(π)의 차이가 ε이하인 π와 w를 구해야 한다.

(당연히, 여기서 ε은 충분히 작은 값이다.)

(+ w값의 절대값이 1보다 작아야 하는 이유는 훈련 도중에 값이 explode하지 않게 하기 위함인듯 하다.)

이렇게 하면, 원래 reward function이 뭐였느냐에 관계 없이, 학습으로 도출된 reward function을 사용하더라도 충분히 optimal policy에 가까운 policy를 얻어낼 수 있다!

Apprenticeship Learning - algorithm

이 부분은 지금까지 위에서 말한 것을 알고리즘으로 나타낸 것이다.

그런데 교수님 말씀으로는 이거 이제 거의 안쓰인다고 설명을 안해주셨다.

사실 이쯤되면 위 ppt 보는것 만으로도 이해가 될 경지에 이르렀다고 생각하겠다 ㅎㅎ

 

지금까지에서 가장 중요한 것은, optimal policy와 충분히 비슷한 policy를 얻어내는 것만으로도 학습이 충분하다는 것이다.

(실제 optimal policy가 가지던 reward function과 관계없이 어떠한 reward function으로라도 저런 policy만 찾을 수 있으면 된다는 뜻이다.)

Ambiguity

하지만 아직 문제점들이 남아 있다.

아까 전에 같은 optimal policy에도 수없이 많은 reward function들이 있을 수 있다고 했는데, 사실 위의 알고리즘이 이 문제를 완벽히 해결하지는 못한다.

또한, reward function을 구했더라도 그 reward function에 최적으로 부합하는 policy도 사실 여러 개가 있을 것이다.

그 중 어떤 것을 골라야 하는가? 가 바로 그 문제점이다.

Learning from Demonstration / Imitation Learning Pointers

이런 문제들 같은 경우, 아직도 활발히 연구되고 있다.

 

주요 논문으로는, Maximum Entropy Inverse RL과 Generative adversarial imitation learning이 있다.

 

Maximum Entropy Inverse RL의 경우, 말 그대로 Entropy의 값을 최대화시키자는 것인데...

필자도 설명을 들어도 잘 모르겠어서, 구글링을 이리저리 하다가 설명을 매우 잘 해놓은 다른 블로그를 보게 되었다.

https://reinforcement-learning-kr.github.io/2019/02/10/4_maxent/

필자가 아무리 잘 설명해 봤자 이것보단 더 잘 설명할 수 없을 것 같으므로, 그냥 여기 들어가서 보는 것을 추천한다.

간단하게 설명하자면, Imitation learning의 uncertanty를 최소화 하기 위해, (즉, 최악의 선택을 피하기 위해) entropy를 최대화 시켜줘야 한다는 것이다. 최대한 일반적인 움직임만을 선택하자는 느낌이다.

 

Generative adversarial imitation learning, 줄여서 GAIL의 경우의 아이디어는 GAN과 매우 흡사하다. (이름부터가...)

주요 아이디어는, discriminator (판별자?)를 만들어서, expert policy와 우리가 찾아낸 policy를 구별하게 만드는 것이다.

그렇게 해서 최적의 이 discriminator가 expert policy와 그냥 policy를 구별할 수 없을 정도의 policy를 찾아낸다면, 그것이 바로 충분히 좋은 policy라고 하는 것이다.

이 방식을 사용함으로써, 우리는 통계적 계산의 산물인 μ(π)에서 조금 더 멀어져서, 실제 좋은 움직임을 찾을 수 있을 것이다.

Summary

자, 이제 거의 다 왔다! 이제 지금까지 배운 내용을 마무리해보자.

 

Imitation learning을 사용하면, 다른 방법들을 사용하는 것 보다 좋은 policy를 얻기 위해 필요한 데이터의 양이 매우 적어진다.

그렇기 때문에, 실제 산업 현장에서도 굉장히 practical하게 사용되고 있는 기법 중 하나이다.

특히 로봇쪽에서 굉장히 많이 보이지만, 그것 외에도 굉장히 다양한 분야에서 시도되고 있는 기법이다.

 

가장 큰 도전 중 하나는, 사실 대부분의 문제에서 우리는 optimal policy가 뭔지 모른다는 것이다.

사실 오늘 강의는 expert의 policy가 언제나 optimal하다는 것을 전제로 해 왔지만, 솔직히 누가 운전하는 optimal한 방법을 알고 있겠는가?

학생을 가장 효율적으로 교육하는 교육 시스템을 만든다고 하면, 대체 누가 가장 효율적인 학습 방법을 알고 있다는 것인가?

그리고, 아직 optimal policy를 모르는 문제들의 경우 어떻게 exploration을 진행하며 좋은 policy를 얻어가야 할까?

이러한 문제들은 Imitation learning, 그리고 강화 학습이 발전하면서 차차 해결해 나가야 할 문제이다.

다음 이 시간에는...

 

오랫만에 글을 써서 그런지 힘을 빡 주고 쓴 느낌이다.

원래는 그냥 막 넘길 부분도 조금 자세히 설명하기도 하고, 최대한 이해가 쉽게 노력했다.

(그래서 그런지 글 쓰는데 강의 시청시간 제외 총합 5시간정도 쓴것 같다;;)

(지금 보니까 지금까지 글 쓴것 중에서 가장 길게 쓴 글이다. 호달달;;)

 

모쪼록 이 정리 포스팅을 보고 강화 학습을 쉽게 배울 수 있었으면 좋겠다.

혹시라도 이해가 잘 되지 않거나, 오타나 잘못된 점들이 있으면 댓글로 남겨주면 좋겠다.

 

아무튼, 다음 시간에는 Policy Search라는 방식을 알아보도록 하겠다.

https://www.edwith.org/deepnlp/joinLectures/17363

 

딥러닝을 이용한 자연어 처리 강좌소개 : edwith

- 조경현 교수

www.edwith.org

edwith 조경현 교수님의 자연어 처리 강좌 중, C. Text Classification & Sentence Representation 부분을 정리한 글입니다.

 

사실 학교 보고서로 제출한 내용인데 그냥 그대로 놓고 있긴 좀 아까워서...ㅎㅎ

 

 

 

 

1. 텍스트 분류란?

텍스트 분류란, 단어, 문장 또는 지문 전체를 두고 카테고리 별로 구분하는 것을 의미합니다. 이미지 분류 문제와 비슷하게 지도 학습 (supervised learning)으로 작업을 하게 됩니다.

텍스트 분류에는 다양한 종류가 있습니다.

 

1.     감성 분석

말 그대로, 문장 또는 지문의 감성을 분석하는 것입니다.

대표적인 예시로, 영화 리뷰를 보고 긍정적인 리뷰인지 부정적인 리뷰인지 분석하는 것이 있습니다.

가령 이 영화는 역대 최악의 영화이다라는 문장을 분석한다면, “부정적인 리뷰로 분류하는 것이 맞겠지요.

 

2.     카테고리 분류

이것도 말 그대로 문장이나 지문을 카테고리 별로 분류하는 것입니다.

어떤 블로그의 글을 보고 그 글이 IT관련 글인지, 스포츠 관련 글인지 등등을 분류하는 것도 이 카테고리 분류로 볼 수 있겠고, 나에게 온 메일이 스팸 메일인지 그냥 메일인지 분류하는 것도 카테고리 분류로 볼 수 있겠습니다.

 

3.     의도 분류

이것도 또 말 그대로 문장의 의도를 분류하는 것입니다.

가령 챗봇한테 오늘 주말에 영업하는 안산 중앙동에 있는 식당 추천해 줘라고 말을 했을 때, 챗봇한테 이 말이 어떤 의도를 갖고 한 말인지 분류하게 하는 것입니다. 위의 경우에는 식당 추천이 의도가 될 것이고, 그러면 챗봇은 그 의도 안에서 오늘 주말에 영업”, “안산 중앙동에 있는이라는 두 구문을 알아서 잘 해석해서 답을 알려 줄 것입니다.

 

 

 

 

 

2.     문장과 토큰

문장을 그냥 그대로 곧이곧대로 자연어 처리 모델에 적용하는 것은 사실상 불가능합니다.

그렇기 때문에 문장을 자연어 처리 모델에 적용시킬 때는, 문장을 토큰이라는 단위로 나눕니다.

이 때, 토큰은 매우 추상적이고, 임의적인 성질을 지닙니다.

예를 들자면, 분명 늑대와 개는 비슷한 성질을 가졌지만 말은 아예 다르고, “수학수화는 매우 다른 의미를 가졌지만 말은 굉장히 비슷합니다.

문장을 토큰으로 나누는 방식은 굉장히 다양합니다.

자연어 처리를 공부하는 중입니다.” 라는 문장만 보더라도,

·       자연어, 처리를, 공부하는, 중입니다 (띄어쓰기로 나누기),

·       자연어, 처리, , 공부, 하는, , 입니다 (형태소로 나누기),

·       , , , (공백), , , , (공백), , , , , (공백), , , , (어절마다 나누기)

등등이 있습니다.

 

, 이렇게 나눈 토큰들을 컴퓨터에게 주려면 이 토큰들을 숫자로 변환해 주어야 합니다.

이미지를 컴퓨터에 입력할 때 픽셀 값으로 입력하는 것과 비슷한 맥락입니다.

이 토큰들을 숫자로 변환해 주기 위해, 이 토큰들로 Vocabulary(단어장)을 만들어서 인덱스 값을 토큰들에 부여합니다.

가령 위 예시에서 형태소로 나누었다고 했을 때, Vocabulary(자연어, 처리, , 공부, 하는, , 입니다) 로 나누어져 있다면 자연어0, “공부3, “입니다6이 될 것입니다.

 

 

하지만, 우리들은 비슷한 의미를 가진 토큰은 가깝게, 다른 의미를 가진 토큰은 멀리 배치하고 싶습니다. 그런 점에서 위처럼 저렇게 막 배열하는 것은 큰 의미가 없습니다.

이렇게 비슷한 의미를 가진 토큰은 가까이, 다른 의미를 가진 토큰은 멀리 배치하기 위해서 실행할 수 있는 것이 One-hot Encoding입니다.

One-hot Encoding이란, 길이가 단어장(V)인 벡터에서, 단어의 인덱스 위치에 있는 값만 1, 나머지는 모두 0으로 두는 형식입니다.

 

가령 위 예시에서 공부One-hot Encoding 하게 되면

[0, 0, 0, 1, 0, 0, 0]으로 인코딩 될 것입니다.

그러나 그냥 이렇게 인코딩만 한다고 해서 단어의 유사성이 거리에 영향을 미치게 되진 않습니다. (One-hot Encoding을 하면 모든 토큰들의 거리가 동일해 집니다.)

 

 

이를 해결하기 위해, Embedding이라는 과정을 거치게 됩니다.

Embedding이란, 각 토큰을 일종의 테이블에 집어넣는 방식입니다. 이를 하기 위해서, 인공 신경망의 Weight W에다가 One-hot Encoding된 각 토큰을 내적하게 됩니다. 이렇게 하면, 모든 토큰들은 이제 더 큰 차원의 연속적인 벡터로 변환되게 됩니다. 이렇게 변환된 토큰들은 이제 각각의 토큰의 거리가 달라지기 때문에, 여기서 의미의 유사성을 찾을 수 있게 됩니다.

 

 

 

 

3.     문장을 표현하는 방법들 (CBoW, RN, CNN, Self Attention, RNN)

이제 Vocabulary에 있는 토큰을 처리하는 것은 끝났습니다.

하지만, 이것 만으로는 충분하지 않습니다. 우리는 문장을 input값으로 집어넣는 것이지, 각각의 토큰을 input 값으로 넣어주는 것이 아니기 때문입니다.

 

하지만 각각의 문장은 모두 길이가 다릅니다. 엄청나게 긴 문장이 있는가 하면, 엄청나게 짧은 문장도 있기 마련입니다. 이 길이가 다른 문장들을 그대로 모델에 집어넣는 것은 사실상 불가능합니다. 그렇기 때문에, 이 문장을 나타내는 방법이 중요하게 됩니다. 각 토큰을 나타낸 것 같이 말입니다.

이 때 사용하는 방법 중 CBow, RN, CNN, Self Attention, RNN 방식을 알아봅시다.

 

 

·       CBoW (Continuous Bag of Words)

CBow는 그냥 문장에 담겨 있는 토큰의 평균값을 내는 매우 간단한 방식으로 문장을 나타냅니다. 이게 뭔 이상한 짓인가 생각이 들기도 하지만, 충격적이게도 이 방식은 굉장히 성능이 좋다고 합니다. 연산 속도도 매우 빠르고, 성능도 괜찮기 때문에 어떤 자연어 처리 문제를 만나도 일반적인 baseline 역할을 해 준다고 합니다.

 

 

·       RN (Relation Network)

Relation Network는 말 그대로, 단어들 사이의 관계를 보는데, 문장 안의 모든 토큰 쌍을 보고, 각 쌍에 대한 신경망을 만드는 방식으로 이루어 집니다.

가령 인덱스 값으로 본다면, (1, 2), (1, 3), …, (1, n), (2, 3), (2, 4), …. 같이 쌍을 지어 준 뒤에, 각 쌍에 대한 신경망을 만들어서 (W 곱하고 b 더하고…) 그 값들을 전부 다 평균을 내 줍니다.

물론 두 단어를 묶는 게 아니라 세 단어, 네 단어, … 끼리 묶을 수도 있지만, 이렇게 묶는 단어의 수가 늘어나면 늘어날수록 연산 횟수가 기하급수적으로 늘어나게 됩니다. (효율성이 떨어지게 됩니다.)

이 방식을 채용하면 단어들 사이의 연관성을 파악하면서 문장을 표현할 수 있다는 장점이 있지만, 아예 관계없는 단어 쌍도 같이 계산한다는 단점이 존재합니다.

 

 

·       CNN (Convolutional Neural Network)

이미지 처리에서 쓰던 CNN을 그대로 1차원 형식으로 가져와서 문장에 적용합니다.

문장 안에서 CNN을 사용해서 층을 쌓고, 그 층(layer)에서 다시 CNN을 사용해서 layer을 중첩하는 방식으로 진행됩니다.

이렇게 하면, 점층적으로 더 넓은 범위의 토큰들 사이의 관계를 파악할 수 있습니다.

필터의 크기가 3이었다고 한다면, 첫번째 layer에서는 주변 3개의 단어와의 관계를 파악하고, 두 번째 layer에서는 주변 5개의 단어와의 관계를 파악하고하는 식입니다.

이렇게 문장을 보면, 단어 -> 다중 단어 표현 -> 구문 -> 문장 순으로 문장을 분석하는 인간의 인식과도 알맞게 됩니다.

 

 

·       Self Attention

위에서 언급했던 RNCNN을 다시 봅시다.

RN에서 t번째 토큰의 표현은 f(xt,x1) + f(xt, x2) + f(xt , x3) + … + f(xt, xT)로 볼 수 있고,

CNN에서 t번째 토큰의 표현은 f(xt, xt-k) + … + f(xt,xt) + … + f(xt, xt+k)로 볼 수 있습니다.

그런데, CNN은 가중치를 xt근처 k개를 1, 나머지를 0이라고 둔 RN이라고 볼 수도 있게 됩니다.

그런데, 이렇게 가중치를 1 또는 0으로 두는 것이 아니고 0에서 1 사이의 값으로 둘 수 있을 것 같습니다.

RN에다가 가중치를 1 또는 0을 두어서 CNN 연산을 할 것이 아니라, 토큰마다의 연관성을 찾는 function을 두어서 그 function값을 가중치로 둘 수도 있는데, 이를 Self Attention 방식이라고 합니다. (function은 또 다른 인공 신경망으로 구할 수 있습니다.)

이러면 이제 RN처럼 단어들마다 가중치를 곱해줘서 연관성이 없는 단어들은 가중치를 0에 가깝게 두고, 연관성이 있는 단어들은 가중치를 1에 가깝게 둬서 연관성이 높은 토큰을 강조하고, 연관성이 낮은 토큰을 억제할 수 있습니다.

또한, 먼 거리에서도 연관성이 높은 단어들을 가져올 수 있고, 짧은 거리에서도 마찬가지로 가능하니 거리에 큰 영향을 받지 않게 됩니다.

하지만, 계산 복잡도가 굉장히 높다는 단점이 있습니다.

 

·       RNN (Recurrent Neural Network)

다른 방식으로는 RNN이 있습니다. 이 방식은 단어 처음과 끝 모두의 쌍을 맺어주며 나아가는 Self Attention 방식과는 달리 그냥 문장을 처음부터 쭉 읽어가면서, 매번 토큰 하나씩을 읽을 때 마다 메모리에 토큰을 저장해 가면서 나아갑니다. 토큰들을 메모리에 압축시키는 형식인 것입니다. 그렇게 마지막에 나온 메모리 벡터를 갖고 문장을 표현하게 됩니다.

단점으로, 토큰을 하나하나 읽어야 하는 특성 상 연산 속도가 다른 것들보다 느리게 되고, 순차적으로 읽는다는 점 때문에 나아가면 나아갈수록 예전에 읽어왔던 메모리를 잊게 되어 정보의 손실이 일어납니다. 이 점들은 나중에 나온 LSTM, GRU와 같은 RNN의 변형이 나오면서 어느 정도 해결됩니다.

 

 

 

이렇게 자연어 처리의 기본적인 부분만 가볍게(?) 살펴 보았습니다.

앞으로 시간이 더 난다면 조금 더 깊게, 조금 더 많은 내용을 살펴보고 싶습니다만,

공업 일반 1인 1프로젝트로 다시 강화 학습을 공부할 예정이라 시간이 날지 모르겠습니다.

4. Implementing DeepMind's DQN

1. [코딩] q3_nature.py의 get_q_values_op를 작성하여 [mnih2015human] 논문에 나와있는 대로 deep Q-network를 만들어보자.

나머지 코드들은 linear approximation에서 짠 코드 그대로 따라갈 것이다.

python q3_nature.py 명령어로 이를 CPU로 돌려보아라. 대략 1분에서 2분정도 걸릴 것이다. [10점]

 

 

 

sol) nature지 아래에 실린 architecture 란에 보면 : 

The first hidden layer convolves 32 filters of 8 X 8 with stride 4 with the input image and applies a rectifier nonlinearity.
The second hidden layer convolves 64 filters of 4 X 4 with stride 2, again followed by a rectifier nonlinearity
This is followed by a third convolutional layer that convolves 64 filters of 3 X 3 with stride 1 followed by a rectifier.
The final hidden layer is fully-connected and consists of 512 rectifier units.

The output layer is a fully-connected linear layer with a single output for each valid action.

라는 설명이 있습니다.

 

이를 대충 해석하면,

첫번째 convolutional layer은 32 filters, 8*8 kernel size, stride 4, ReLU

두번째 convolutional layer은 64 filters, 4*4 kernel size, stride 2, ReLU

세번째 convolutional layer은 64 filters, 3*3 kernel size, stride 1, ReLU

마지막 hidden layer은 fully-connected layer로 512개의 output에 ReLU,

output layer은 각각의 action에 대한 single output이 나오는 fully-connected layer이라고 합니다.

 

이것을 코드로 구현하면 다음과 같은 코드가 나옵니다.

class NatureQN(Linear):
    """
    Implementing DeepMind's Nature paper. Here are the relevant urls.
    https://storage.googleapis.com/deepmind-data/assets/papers/DeepMindNature14236Paper.pdf
    https://www.cs.toronto.edu/~vmnih/docs/dqn.pdf
    """

    def get_q_values_op(self, state, scope, reuse=False):
        """
        Returns Q values for all actions

        Args:
            state: (tf tensor) 
                shape = (batch_size, img height, img width, nchannels)
            scope: (string) scope name, that specifies if target network or not
            reuse: (bool) reuse of variables in the scope

        Returns:
            out: (tf tensor) of shape = (batch_size, num_actions)
        """
        # this information might be useful
        num_actions = self.env.action_space.n

        ##############################################################
        """
        TODO: implement the computation of Q values like in the paper
                https://storage.googleapis.com/deepmind-data/assets/papers/DeepMindNature14236Paper.pdf
                https://www.cs.toronto.edu/~vmnih/docs/dqn.pdf

              you may find the section "model architecture" of the appendix of the 
              nature paper particulary useful.

              store your result in out of shape = (batch_size, num_actions)

        HINT: 
            - You may find the following functions useful:
                - tf.layers.conv2d
                - tf.layers.flatten
                - tf.layers.dense

            - Make sure to also specify the scope and reuse

        """

        """
        The first hidden layer convolves 32 filters of 8 X 8 with stride 4 with the input image and applies a rectifier nonlinearity.
        The second hidden layer convolves 64 filters of 4 X 4 with stride 2, again followed by a rectifier nonlinearity
        This is followed by a third convolutional layer that convolves 64 filters of 3 X 3 with stride 1 followed by a rectifier.
        The final hidden layer is fully-connected and consists of 512 rectifier units.
        The output layer is a fully-connected linear layer with a single output for each valid action.
        """
        ##############################################################
        ################ YOUR CODE HERE - 10-15 lines ################
        input = state
        with tf.variable_scope(scope,reuse=reuse):
            conv1 = tf.layers.conv2d(input, 32, (8, 8), strides=4, activation=tf.nn.relu, name='conv1')
            conv2 = tf.layers.conv2d(conv1, 64, (4, 4), strides=2, activation=tf.nn.relu, name='conv2')
            conv3 = tf.layers.conv2d(conv2, 64, (3, 3), strides=1, activation=tf.nn.relu, name='conv3')
            flat = tf.layers.flatten(conv3, name='flatten')
            fc = tf.layers.dense(flat, 512, activation=tf.nn.relu, name='fully-connected')
            out = tf.layers.dense(fc, num_actions, name='out')


        pass

        ##############################################################
        ######################## END YOUR CODE #######################
        return out

(보기 좋으라고 Class 선언부부터 넣어두지만, 구현한 부분은 가장 아래 10줄 가량입니다. input = state 부터...)

 

 

2. (written 5pts) Attach the plot of scores, scores.png, from the directory results/q3 nature to your writeup. Compare this model with linear approximation. How do the final performances compare? How about the training time?

 

2. results/q3 nature에서 scores.png 사진을 첨부하시오. linear approximation model과 비교하시오.

성능은 어떻게 차이가 나는가? 훈련 시간은 얼마나 차이가 나는가?

Linear Approximation

일단, DQN보다 Linear Approximation이 더 빠르게 converge 하기 시작하는 모습을 볼 수 있다.

심지어는 DQN을 돌릴 때 가끔씩 reward가 4.0에서 4.1로 가지 못한 채 training이 끝나는 모습도 볼 수 있었다.

아무래도 DQN이 Linear Approximation보다 더 unstable한 방식이라 그런 것 같다.

 

또한, training 시간도 DQN쪽이 더 오래 걸린다.

 

 

이것을 통해, 이렇게 간단한 test environment 같은 경우는 Linear Approximation이 더 잘 작동할 수도 있다는 것을 알게 되었다.

즉, environment에 따라 서로 다른 최적의 모델이 존재한다는 것을 알 수 있다.

 

 

+ Recent posts