독학 연구소
공부한 내용을 정리합니다.
머신러닝 (33)
소프트맥스 회귀(Softmax Regression) (1)

 

2020/11/06 - [머신러닝/딥러닝] - 로지스틱 회귀(Logistic Regression)

 

스프트맥스 회귀(Softmax Regression)

3개 이상의 범주 혹은 클래스를 갖는 다중 클래스 분류(multi-class classification)를 위한 알고리즘입니다.

 

이진 분류 문제에서 로지스틱 회귀는 시그모이드 함수를 통과한 확률에 해당하는 출력값에 0.5와 같은 임계값(threshold)을 기준으로 분류합니다.

 

다중 분류 문제에서도 시그모이드 함수를 이용할 수 있을까요?

 

이진 분류의 경우 하나의 출력으로 두 클래스에 대한 확률을 모두 알 수 있습니다. ($A$ 일 확률 $p$, $B$ 일 확률 $1 - p$) 하지만 소프트맥스 회귀에서는 입력된 데이터에 대해 하나의 출력으로 바로 특정 클래스를 예측하는 것이 아니라 각 클래스에 대한 확률을 출력하여 가장 높은 확률을 갖는 클래스로 예측하는 것입니다. 따라서 출력의 합이 1이 되어야 하기 때문에 시그모이드 함수를 사용할 수 없는 것입니다.

 

 

소프트맥스 함수(Softmax Function)

소프트맥스 함수는 활성화 함수(activation function)의 한 종류로 출력의 합을 1로 만드는데 이것을 출력 강도를 정규화한다고 합니다.

 

3개의 출력에 대한 식으로 나타내면 다음과 같습니다.($e$ 는 자연 상수, $z$ 는 입력)

 

$z_1 \rightarrow {e^{z_1} \over e^{z_1}+e^{z_2}+e^{z_3}}$

 

$z_2 \rightarrow {e^{z_1} \over e^{z_1}+e^{z_2}+e^{z_3}}$

 

$z_3 \rightarrow {e^{z_1} \over e^{z_1}+e^{z_2}+e^{z_3}}$

 

$z_1+z_2+z_3=1$

 

import numpy as np

def softmax(array):
    r = []
    for z in sigmoid(array):
        r.append(np.exp(z) / sum(np.exp(sigmoid(array))))
    return r

a = np.array([0.3, 2.6, 4.1])

print(softmax(a))
print(sum(softmax(a)))
[0.25420056710998096, 0.36305075758862626, 0.38274867530139284]
1.0

 

 

원-핫 인코딩(One-Hot Encoding)

사용할 데이터는 붓꽃 데이터셋으로 타겟의 구성값을 확인합니다.

import numpy as np
from sklearn.datasets import load_iris

iris = load_iris()
data = iris.data
target = iris.target

np.unique(target, return_counts=1)
(array([0, 1, 2]), array([50, 50, 50], dtype=int64))

 

3개의 클래스를 가지며 값은 0,1,2  정수 인코딩되어있습니다. 신경망에 입력될 때는 정수 인코딩된 값을 원-핫 인코딩하여 각 클래스간의 관계를 균등하게 합니다.

 

넘파이에서는 단위 행렬을 생성하는 np.eye 를 이용할 수 있습니다. 

np.eye(3, 7)
array([[1., 0., 0., 0., 0., 0., 0.],
       [0., 1., 0., 0., 0., 0., 0.],
       [0., 0., 1., 0., 0., 0., 0.]])

 

다음과 같이 사용하면 입력 데이터에 대해 원-핫 인코딩할 수 있습니다.

label_size = np.unique(target).size

np.eye(label_size)[target]
array([[1., 0., 0.],
       [1., 0., 0.],
       [1., 0., 0.],

...

       [0., 1., 0.],
       [0., 1., 0.],
       [0., 1., 0.],


...

       [0., 0., 1.],
       [0., 0., 1.],
       [0., 0., 1.]])

 

텐서플로에서 제공하는 tf.keras.utils.to_categorical 을 이용하면 더욱 간단합니다.

import tensorflow as tf

tf.keras.utils.to_categorical(target)
array([[1., 0., 0.],
       [1., 0., 0.],
       [1., 0., 0.],

...

       [0., 1., 0.],
       [0., 1., 0.],
       [0., 1., 0.],


...

       [0., 0., 1.],
       [0., 0., 1.],
       [0., 0., 1.]])

 

또한 신경망 내부적으로 처리할 때는 텐서를 반환하는 tf.one_hot 을 이용할 수 있습니다.

import numpy as np
import tensorflow as tf

label_size = 5

x = tf.placeholder(tf.int32)
onehot = tf.one_hot(x, label_size)

with tf.Session() as sess:
    sess.run(tf.global_variables_initializer())
    
    print(sess.run(onehot, {x: [1, 4, 0, 2, 3]}))
[[0. 1. 0. 0. 0.]
 [0. 0. 0. 0. 1.]
 [1. 0. 0. 0. 0.]
 [0. 0. 1. 0. 0.]
 [0. 0. 0. 1. 0.]]

 

 

손실 함수(Loss Function)

다중 클래스 분류 문제에서는 손실 함수로 크로스 엔트로피(cross-entropy)가 사용됩니다. 크로스 엔트로피는 정답 분포 $q$ 와 예측 분포 $p$ 사이의 평균 정보량을 의미합니다. 정보량은 해당 사건이 일어날 확률이 낮으면 증가하고 높으면 감소하게 되는데, 경사 하강법 최적화 알고리즘을 통해 그 정보량을 감소시켜 확률을 높이게 되는 것입니다.

 

크로스 엔트로피 손실 함수와 로지스틱 회귀에서의 손실 함수는 매우 유사합니다.

 

$L=-{1 \over n}\displaystyle \sum_{i=1}^n[y_i \text{log}(p_i)+(1-y_i) \text{log}(1-p_i)]$

 

이는 2개의 클래스를 갖는 이진 분류 문제에 대한 식으로 정답값이 0인 경우와 1인 경우를 고려한 것입니다. 다중 클래스 분류 문제에 대한 식도 이와 비슷합니다. 신경망에 입력되는 타겟 벡터는 원-핫 인코딩된 것으로 정답 클래스의 값은 1이고 나머지는 0입니다.

 

다음과 같이 클래스의 개수만큼 더해주어 나타낼 수 있습니다. ($k$는 클래스의 개수, $p$는 해당 클래스의 확률값, $y$는 원-핫 인코딩된 타겟 벡터)

 

$L=-\displaystyle \sum_{j=1}^ky_j \text{log}(p_j)$

 

최종적으로 n개의 데이터에 대한 식으로 나타내면 다음과 같습니다.

 

$L=-{1 \over n}\displaystyle \sum_{i=1}^n \sum_{j=1}^k y_{ij} \text{log}(p_{ij})$

 

텐서플로에서 제공하는 tf.nn.softmax_cross_entropy_logits_v2 를 이용하면 간단합니다.

import tensorflow as tf

x = tf.placeholder(tf.float32, [None, 1])
y = tf.placeholder(tf.float32, [None, 3])

output = tf.layers.dense(x, 3)

cross_entropy = -tf.reduce_sum(y * tf.log(tf.nn.softmax(output)), 1)
cross_entropy2 = tf.nn.softmax_cross_entropy_with_logits_v2(logits=output, labels=y)

with tf.Session() as sess:
    sess.run(tf.global_variables_initializer())
    
    _cross_entropy, _cross_entropy2 = sess.run([cross_entropy, cross_entropy2], {x: [[0.1], [0.2]], y: [[1., 0., 0.], [0., 1., 0.]]})
    
    print(_cross_entropy)
    print(_cross_entropy2)
[1.0372838 1.0633986]
[1.0372838 1.0633986]

 

 

구현

소프트맥스 회귀 모델을 구현합니다.

 

붓꽃 데이터셋을 불러옵니다.

from sklearn.datasets import load_iris

iris = load_iris()
data = iris.data
target = iris.target
feature_names = iris.feature_names
target_names = iris.target_names

print('data shape:', data.shape)
print('data sample:', data[0])
print('feature names:', feature_names)

print('target shape:', target.shape)
print('target sample:', target[0], target[1], target[2])
print('target label:', np.unique(target, return_counts=True))
print('target names:', target_names)
data shape: (150, 4)
data sample: [5.1 3.5 1.4 0.2]
feature names: ['sepal length (cm)', 'sepal width (cm)', 'petal length (cm)', 'petal width (cm)']
target shape: (150,)
target sample: 0 0 0
target label: (array([0, 1, 2]), array([50, 50, 50], dtype=int64))
target names: ['setosa' 'versicolor' 'virginica']

 

레이블은 3개의 클래스를 가지며 고르게 분포되어 있습니다.

target label: (array([0, 1, 2]), array([50, 50, 50], dtype=int64))

 

8:2 비율로 학습 데이터와 테스트 데이터로 분할합니다.

from sklearn.model_selection import train_test_split

x_train, x_test, y_train, y_test = train_test_split(data, target, test_size=0.2)

 

신경망을 정의합니다.

import numpy as np
import tensorflow as tf

class Model:
    def __init__(self, lr=1e-2):
        tf.reset_default_graph()
        
        with tf.name_scope('input'):
            self.x = tf.placeholder(tf.float32, [None, 4])
            self.y = tf.placeholder(tf.int64)

        with tf.name_scope('y_onehot'):
            y_onehot = tf.one_hot(self.y, 3)
            
        with tf.name_scope('layer'):
            fc = tf.layers.dense(self.x, 32, tf.nn.relu)
            logits = tf.layers.dense(fc, 3)
            
        with tf.name_scope('output'):
            h = tf.nn.softmax(logits)
            self.predict = tf.argmax(h, 1)

        with tf.name_scope('accuracy'):
            self.accuracy = tf.reduce_mean(tf.cast(tf.equal(tf.to_int64(self.predict), self.y), dtype=tf.float32))    
        
        with tf.name_scope('loss'):
            cross_entropy = tf.nn.softmax_cross_entropy_with_logits_v2(logits=logits, labels=y_onehot)
            self.loss = tf.reduce_mean(cross_entropy)
        
        with tf.name_scope('optimizer'):
            self.train_op = tf.train.GradientDescentOptimizer(lr).minimize(self.loss)

        with tf.name_scope('summary'):
            tf.summary.scalar('loss', self.loss)
            tf.summary.scalar('accuracy', self.accuracy)
            
            self.merge = tf.summary.merge_all()

        self.writer = tf.summary.FileWriter('./tmp/softmax-regression_iris', tf.get_default_graph())
        
        self.sess = tf.Session()
        
        self.sess.run(tf.global_variables_initializer())
    
    def train(self, x_train, y_train, epochs):
        for e in range(epochs):
            summary, loss, accuracy, _ = self.sess.run([self.merge, self.loss, self.accuracy, self.train_op], {self.x: x_train, self.y: y_train})
            self.writer.add_summary(summary, e)
            print('epoch:', e + 1, ' / loss:', loss, '/ accuracy:', accuracy)
    
    def score(self, x, y):
        return self.sess.run(self.accuracy, {self.x: x, self.y: y})

 

배치 경사 하강법으로 학습을 수행합니다.

def train(self, x_train, y_train, epochs):
    for e in range(epochs):
        summary, loss, accuracy, _ = self.sess.run([self.merge, self.loss, self.accuracy, self.train_op], {self.x: x_train, self.y: y_train})
        self.writer.add_summary(summary, e)
        print('epoch:', e + 1, ' / loss:', loss, '/ accuracy:', accuracy)

 

모델을 학습하고 테스트합니다.

model = Model()
model.train(x_train, y_train, epochs=500)
model.score(x_test, y_test)
...

epoch: 490  / loss: 0.25155848 / accuracy: 0.9583333
epoch: 491  / loss: 0.25125748 / accuracy: 0.9583333
epoch: 492  / loss: 0.2509577 / accuracy: 0.9583333
epoch: 493  / loss: 0.2506586 / accuracy: 0.9583333
epoch: 494  / loss: 0.25035974 / accuracy: 0.9583333
epoch: 495  / loss: 0.25006163 / accuracy: 0.9583333
epoch: 496  / loss: 0.24976416 / accuracy: 0.9583333
epoch: 497  / loss: 0.24946807 / accuracy: 0.9583333
epoch: 498  / loss: 0.24917193 / accuracy: 0.9583333
epoch: 499  / loss: 0.24887681 / accuracy: 0.9583333
epoch: 500  / loss: 0.24858236 / accuracy: 0.9583333

0.96666664

 

에포크에 대한 정확도와 손실 함수의 그래프는 다음과 같습니다.

 

 

 

  Comments,     Trackbacks
Model-Free Prediction

모델-프리(Model-Free)

동적 프로그래밍(Dynamic Programming, DP)은 MDP 모델을 아는 상황에서의 계획입니다. 이와 반대로 보상 함수와 전이 확률과 같은 정보를 모르는 상황을 model-free 라고 하며 이는 경험(experience)을 통해야하며 에이전트와 환경의 상호작용을 통해 학습해야 하는 것입니다.

 

이러한 model-free 에서 정책 평가(policy evaluation) 혹은 예측(prediction) 방법으로는 몬테카를로 예측(MC prediction)과 시간차 예측(TD prediction)이 있습니다. 또한 여기서는 다루는 정책은 고정된 임의의 정책 $\pi$ 이라고 가정합니다.

 

 

몬테카를로 방법(Monte Carlo Method)

무작위로 샘플링을 반복하면 실제값에 근사한다는 방법론으로 계산하려는 값이 닫힌 형식으로 표현되지 않거나 복잡한 경우에 근사적으로 계산할 때 사용합니다. 몬테카를로 방법은 샘플이 많을 수록 대수의 법칙(law of large numbers)에 의해 실제 가치에 더 가까워진다는 것입니다.

 

아래 예제는 100번 던진 주사위와 1000번 던진 주사위의 차이를 보여줍니다.

import matplotlib.pyplot as plt
import numpy as np

dice = [1,2,3,4,5,6]

def roll_dice():
    return np.random.choice(dice)

num_episode = 100
r = [roll_dice() for _ in range(num_episode)]
r = np.unique(r, return_counts=True)[1]
plt.bar(dice, r)
plt.title(str(num_episode) + ' episode')
plt.show()

num_episode = 1000
r = [roll_dice() for _ in range(num_episode)]
r = np.unique(r, return_counts=True)[1]
plt.bar(dice, r, color='red')
plt.title(str(num_episode) + ' episode')
plt.show()

 

 

MC 예측(Monte Carlo Prediction)

MC 예측은 샘플 리턴의 평균값을 기반으로 가치 함수를 추정하는 것을 말합니다.

 

상태 가치 함수의 정의는 다음과 같습니다.

 

$v_\pi(s) \doteq \mathbb{E}_\pi[G_t|S_t=s]$

 

상태 $s$ 의 가치는 리턴의 기대값입니다. 따라서 $G_t$ 를 계속 샘플링하면 그 평균은 $v_\pi(s)$ 의 기댓값인 실제 가치(true value)에 수렴하게 된다는 것을 의미합니다. 이를 $G_t$ 는 $v_\pi(s)$ 의 불평 추정량(unbiased estimate)이라고 합니다.

 

각 상태에 대한 리턴을 구하면 다음과 같습니다.

 

 

각 상태마다 이러한 리턴 샘플을 모아서 평균을 구하면 각 상태의 가치는 실제 가치에 근사한다는 것입니다.

 

이를 식으로 나타내면 다음과 같습니다.

 

$v_\pi(s)={1 \over N} \displaystyle \sum_{i=1}^{N} G_i(s)$ 

 

여기서 문제는 1000번의 에피소드를 진행한다면 에피소드가 1000번 진행 후에 계산될 것이며 이는 매우 비효율적일 것입니다.

 

위의 식과 본질적으로 같으며 한 에피소드가 끝날 때마다 업데이트할 수 있는 식은 다음과 같습니다.

 

$V(S_t) \leftarrow (1-\alpha) * V(S_t) + \alpha * G_t$

 

$\alpha$ 의 값을 기준으로 기존의 값과 새로운 값을 섞어주는 것입니다. $\alpha$ 값을 작게 한다면 기존의 값을 크게 새로운 값을 작게하여 변화에 천천히 적용하는 것이라고 할 수 있습니다. 

 

위 식을 정리하면 다음과 같습니다.

 

$V(S_t) \leftarrow V(S_t) + \alpha(G_t - V(S_t))$

 

 

또한 이러한 업데이트 방식을 샘플 업데이트(sample update)라고 합니다. 이는 샘플을 기반으로 한 것으로 DP 의 기댓값 업데이트(expected update)와는 다른 것입니다

 

 

구현

MC prediction 을 구현합니다.

# mc prediction(policy evaluation)

import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np
import gym

env = gym.make('FrozenLake-v0', is_slippery=False)

gamma = 0.99
alpha = 0.1

v_table = np.zeros(env.observation_space.n)

num_episode = 10000
for i in range(num_episode):
    memory = []
    
    state = env.reset()
    done = False
    while not done:
        action = env.action_space.sample()
        next_state, reward, done, _ = env.step(action)
        memory.append((state, reward))
        state = next_state
    
    g = 0
    for s, r in reversed(memory):
        g = r + gamma*g
        v_table[s] = v_table[s] + alpha*(g-v_table[s])
        
# print(v_table.reshape(4,4))

sns.heatmap(np.around(v_table.reshape(4, 4), 3), annot=True)
plt.title('MC Prediction', fontsize=18)
plt.show()

 

 

시간차 방법(Temporal Difference Method)

MC 는 단점이 있는데 에피소드가 끝날 때까지 기다려야 한다는 점입니다. 업데이트하기 위해서는 리턴이 필요한데 리턴은 에피소드가 끝나기 전까지는 알 수 없습니다. 즉 MC 는 종료되는 환경에서만 사용할 수 있는 것입니다. 이번에는 종료되지 않는 환경에도 온라인 학습이 가능한 TD 방법을 알아보겠습니다.

 

 

TD 예측(Temporal Difference Prediction)

TD 도 model-free 환경에서 경험을 통해 학습하는 방법이며 MC 와 DP 방법을 결합한 것입니다. 

 

상태 가치 함수의 정의로부터 각 유도된 식은 다음과 같습니다.

 

$\begin{aligned} 
v_{\pi}(s) & \doteq \mathbb{E}_{\pi}[G_t|S_t=s] & \scriptstyle{\text{MC}} \\ 
&=\mathbb{E}_{\pi}[R_{t+1} + \gamma v_{\pi}(S_{t+1})|S_t = s] & \scriptstyle{\text{TD}} \\
&=\displaystyle \sum_{a \in A} \pi(a|s) \displaystyle \sum_{s' \in S} P_{ss'}^a \left[ r + \gamma v_{\pi}(s') \right] & \scriptstyle{\text{DP}}
\end{aligned}$

 

MC 방법은 실제 리턴의 기댓값 대신에 샘플 리턴의 평균으로 기댓값에 근사하는 것입니다. DP 는 기댓값이 MDP 모델로부터 완전히 제공받는다고 볼 수 있으며 $v_{\pi}(S_{t+1})$ 대신 현재 추정된 가치 함수 $V(S_{t+1})$ 로부터 계산합니다. TD 는 두 방법을 모두 포함하는 것으로 샘플을 이용하며 실제 $v_{\pi}$ 대신 추정 $V$ 로 계산하는 것이라고 할 수 있으며 이를 부트스트랩(bootstrap)이라고 합니다. 

 

업데이트 식은 다음과 같습니다.

 

$V(S_t) \leftarrow V(S_t) + \alpha(r_{t+1}+\gamma V(S_{t+1}) - V(S_t))$

 

 

 

구현

TD prediction 을 구현합니다.

# td prediction(policy evaluation)

import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np
import gym

env = gym.make('FrozenLake-v0', is_slippery=False)

gamma = 0.99
alpha = 0.1

v_table = np.zeros(env.observation_space.n)

num_episode = 10000
for i in range(num_episode):
    state = env.reset()
    done = False
    while not done:
        action = env.action_space.sample()
        next_state, reward, done, _ = env.step(action)
        
        td_target = reward + gamma * v_table[next_state]
        v_table[state] = v_table[state] + alpha * (td_target - v_table[state])
        
        state = next_state
    
# print(v_table.reshape(4,4))

sns.heatmap(np.around(v_table.reshape(4, 4), 3), annot=True)
plt.title('TD Prediction', fontsize=18)
plt.show()

 

 

편향(Bias)

MC 와 TD 에서 사용하는 식을 비교합니다.

 

$\begin{aligned}   
v_{\pi}(s) & \doteq \mathbb{E}_{\pi}[G_t|S_t=s] & \scriptstyle{\text{MC}} \\   
&=\mathbb{E}_{\pi}[R_{t+1} + \gamma v_{\pi}(S_{t+1})|S_t = s] & \scriptstyle{\text{TD}} \\   
\end{aligned}$

 

MC 방법은 리턴의 평균을 기반으로 리턴의 기댓값에 근사하고자하는 방법론으로 모든 상태를 방문하고 충분히 반복한다면 실제 가치에 수렴하게 될 것입니다. 즉 $G_t$ 는 $v_{\pi}(s)$ 의 불평 추정량으로 편향되지 않는 것입니다.

 

TD 방법의 경우에는 추정을 추정으로 계산하여 식 $r_{t+1} + \gamma v(S_{t+1})$ 에서 $v(S_{t+1})$ 에 대한 계산은 추정된 $V$ 를 이용합니다. $r_{t+1} + \gamma v_\pi(s_{t+1})$ 는 $v_{\pi}(s)$ 의 불편 추정량이지만 $r_{t+1} + \gamma V(S_{t+1})$ 은 편향되었다는 것입니다.

 

 

분산(Variance)

MC 방법은 한 에피소드동안 발생된 샘플의 리턴을 이용해 업데이트가 이루어집니다.

 

 

방문한 상태의 많은 샘플에 대해 계산하기 때문에 분산 혹은 변동성이 크다는 것입니다. 즉 현재 상태에서 다음 상태로 가는데 한 번에 갈 수도 있지만 다른 상태를 거쳐서 갈 수도 있기 때문입니다.

 

반면 TD 방법은 현재 상태 $S_t$ 는 단일 샘플 $r_{t+1}$ 과 $V(S_{t+1})$ 로 업데이트가 가능하기 때문에 분산이 적다고 할 수 있습니다. 또한 단일 샘플을 이용하는 계산을 TD(0) 혹은 1-스텝 TD 라고합니다.

 

 

 

n-스텝 예측(n-Step Prediction)

n-스텝 TD 는 1-스텝 TD 와 MC 양극단의 중간쯤 되는 방법으로 n-스텝 리턴을 통해 업데이트합니다.

 

 

상태 $S_t$ 의 가치 추정값 $V(S_t)$ 에 대해 한 에피소드의 궤적 $S_t, R_{t+1}, S_{t+1}, R_{t+2}, \dots, R_T, S_T$ 을 이용해서 업데이트한다고 가정합니다.

 

이에 대한 리턴은 다음과 같습니다.

 

$G_t \doteq R_{t+1} + \gamma R_{t+2} + \gamma^2 R_{t+3} + \cdots + \gamma ^{T-t-1} R_T$

 

n-스텝 리턴을 나타내면 다음과 같습니다.

 

$G_{t:t+1} \doteq R_{t+1} + \gamma V_t(S_{t+1})$

 

$G_{t:t+2} \doteq R_{t+1} + \gamma R_{t+2} + \gamma^2 V_{t+1}(S_{t+2})$

 

$\dots$

 

$G_{t:t+n} \doteq R_{t+1} + \gamma R_{t+2} + \cdots + \gamma^{n-1} R_{t+n} + \gamma^n V_{t+n-1}(S_{t+n})$

 

스텝 n 값을 1로하면 1-스텝 TD이며 무한히 크게하면 그냥 리턴이 됩니다. 즉 n 값에 따라 미래가 과거에 영향을 주는 정도를 조절할 수 있으며 n 값을 크게하면 MC 방법의 성질과 가까워지므로 분산이 커지게 됩니다. 적당한 n 값을 사용하면 MC 와 TD 의 장점을 모두 살려 편향과 분산을 조절할 수 있습니다.

 

다음은 어떤 환경의 10번의 에피소드에 대하며 100번 반복한 실험의 결과로 실제값과 예측값의 평균제곱오차를 나타냅니다.

 

n 의 중간 값을 사용한 방법이 좋은 결과를 나타내고 있습니다.

 

 

n-스텝 리턴을 이용한 업데이트식은 다음과 같습니다.

 

$V(S_t) \leftarrow V(S_t) + \alpha(G_{t:t+n} - V(S_t))$

 

 

참고로 A3C 알고리즘에서 사용된 n-스텝 TD 입니다.

 

에피소드 종료 혹은 n 값에 해당하는 $t_{max}$ 에 도달하면 그 이전까지 모았던 샘플 궤적 $s_t$, $a_t$, $r_t$, ..., $s_n$, $a_n$, $r_n$ 을 역방향으로 계산하여 각 상태의 리턴과 상태의 추정 가치의 오차를 줄여나가는 과정입니다.

 

 

구현

n-스텝 TD prediction 을 구현합니다.

# n-step td prediction(policy evaluation)

import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np
import gym

env = gym.make('FrozenLake-v0', is_slippery=False)

n = 8

gamma = 0.99
alpha = 0.1

v_table = np.zeros(env.observation_space.n)

num_episode = 10000
for i in range(num_episode):
    memory = []
    
    state = env.reset()
    done = False
    while not done:
        action = env.action_space.sample()
        next_state, reward, done, _ = env.step(action)
        
        memory.append((state, reward))
        
        if done or len(memory) == n:
            if done:
                g = 0
            else: 
                next_action = get_action(e, next_state)
                g = v_table[next_state]
                
            for s, r in reversed(memory):
                g = r + gamma * g
                v_table[s] += alpha * (g - v_table[s])
                
            memory.clear()
        
        state = next_state
    
# print(v_table.reshape(4,4))

sns.heatmap(np.around(v_table.reshape(4, 4), 3), annot=True)
plt.title('n-Step TD Prediction', fontsize=18)
plt.show()

 

 

'머신러닝 > 강화학습' 카테고리의 다른 글

Model-Free Control  (0) 2020.11.13
소프트맥스 회귀(Softmax Regression) (2)  (0) 2020.11.12
OpenAI Gym  (0) 2020.11.09
동적 프로그래밍(Dynamic Programming)  (0) 2020.11.06
벨만 방정식(Bellman Equation)  (0) 2020.11.04
  Comments,     Trackbacks
OpenAI Gym

OpenAI Gym 

강화학습 알고리즘 테스트를 위한 환경을 제공해주는 라이브러리입니다.

 

 

 

 

설치

파이썬 환경은 pip 를 이용하면 간단하게 설치할 수 있습니다. 

pip install gym

 

 

예제

환경을 불러올 때는 gym.make() 함수를 이용합니다.

 

그리드월드 환경FrozenLake 불러와 상태와 행동 크기를 확인합니다.

import gym

env = gym.make('FrozenLake-v0', is_slippery=False)

print(env.observation_space.n)
print(env.action_space.n)

 

16개의 상태를 가지고 4개의 행동을 취할 수 있습니다.

16
4

 

다음과 같이 에피소드를 반복할 수 있습니다.

for i in range(5):
    state = env.reset()
    done = False
    while not done:
        env.render()
        action = env.action_space.sample()
        state, reward, done, info = env.step(action)
        print(state, reward, done, info)

 

매 에피소드마다 env.reset() 함수를 통해 환경을 초기화하고 해당 에피소드가 종료될 때까지 타임 스텝을 반복합니다. env.action_space.sample() 함수는 해당 환경에서 취할 수 있는 랜덤한 행동을 반환합니다. env.step() 함수는 행동을 받아 다음 스텝의 정보들을 반환합니다.

 

환경으로부터 받는 상태, 보상, 종료 여부, 기타 정보는 다음과 같습니다.

0 0.0 False {'prob': 1.0} 
1 0.0 False {'prob': 1.0} 
2 0.0 False {'prob': 1.0} 
1 0.0 False {'prob': 1.0} 
2 0.0 False {'prob': 1.0} 
2 0.0 False {'prob': 1.0} 
2 0.0 False {'prob': 1.0} 
3 0.0 False {'prob': 1.0} 
7 0.0 True {'prob': 1.0}

 

env.render() 함수를 호출하면 현재 상태를 렌더링합니다.

 

 

 

다음은 CarRacing 이라는 환경입니다.

import gym

env = gym.make("CarRacing-v0")
state = env.reset()

done = False
while not done:
    env.render()
    action = env.action_space.sample()
    state, reward, done, _ = env.step(action)
    print(action, state.shape, reward, done)

env.close()

 

다음과 같은 형태의 연속적인 행동 공간과 이미지에 해당하는 상태 정보를 반환합니다.

[0.81338143 0.07590234 0.50560546] (96, 96, 3) 7.1463768115942035 False 
[-0.46199107 0.23878533 0.3978563 ] (96, 96, 3) -0.09999999999999964 False 
[-0.1199649 0.10941219 0.68902004] (96, 96, 3) -0.09999999999999964 False

 

 

 

  Comments,     Trackbacks
교차 검증(Cross Validation)

 

2020/11/06 - [머신러닝/딥러닝] - 로지스틱 회귀(Logistic Regression)

 

검증 데이터(Validation Data)

데이터는 학습 데이터와 테스트 나누어지는데, 학습 데이터로 학습을 하고 테스트 데이터를 이용해 범용 성능을 평가합니다. 여기서 문제는 과대적합(Overfitting)이 나타날 수 있다는 것인데, 이는 학습 데이터에만 적합한 모델이 될 수 있다는 것입니다. 따라서 학습 데이터의 일부를 검증 데이터로 분할하여 튜닝에 사용할 수 있습니다.

 

 

튜닝한다는 것은 모델의 하이퍼파라미터를 조정하는 것으로 성능 지표가 필요합니다. 이에 대해 테스트 데이터를 사용하여 튜닝하면 테스트 데이터에만 적합하도록 조정되어 과대적합이 발생하며 테스트 데이터의 정보가 알려져 범용 성능이 떨어지게 됩니다.

 

 

Holdout Cross Validation

학습 데이터의 일부를 고정적으로 분할하는 방식으로 전체 데이터가 많을 경우에 사용할 수 있는 방법입니다.

 

전체 데이터를 8:2 비율로 학습 데이터와 테스트 데이터를 분할하고 학습 데이터를 또다시 8:2 비율로 학습 데이터와 검증 데이터로 분할합니다.

from sklearn.model_selection import train_test_split

x_train, x_test, y_train, y_test = train_test_split(data, target, test_size=0.2)
x_train, x_val, y_train, y_val = train_test_split(x_train, y_train, test_size=0.2)

 

주의해야할 점은 전체 데이터에서 6:2:2과 같이 한 번에 나누는게 아니라는 것입니다.

 

 

K-Fold Cross Validation

전체 데이터가 적을 경우에 사용할 수 있는 방법입니다.

 

k는 학습 데이터를 분할하는 수를 의미하며 분할된 데이터를 폴드(fold)라고 합니다. 모든 폴드는 1개의 검증 폴드와 나머지 학습 폴드로 구성되는데, 모든 폴드가 최소 1번의 검증 폴드가 되도록 반복합니다. 이 과정에서 검증 폴드를 이용하여 성능 평가를 수행하며, 이에 대한 평균을 지표로 이용하는 것입니다.

 

사이킷런에서 제공하는 KFold 를 이용할 수 있습니다.

from sklearn.model_selection import KFold

data = np.arange(20).reshape(10, 2)
print(data)

kf = KFold(n_splits=4)

for train_idx, val_idx in kf.split(data):
    print('\ntrain index:', train_idx)
    print('val index:', val_idx)
    print('val data')
    print(data[val_idx])
[[ 0  1]
 [ 2  3]
 [ 4  5]
 [ 6  7]
 [ 8  9]
 [10 11]
 [12 13]
 [14 15]
 [16 17]
 [18 19]]

train index: [3 4 5 6 7 8 9]
val index: [0 1 2]
val data
[[0 1]
 [2 3]
 [4 5]]

train index: [0 1 2 6 7 8 9]
val index: [3 4 5]
val data
[[ 6  7]
 [ 8  9]
 [10 11]]

train index: [0 1 2 3 4 5 8 9]
val index: [6 7]
val data
[[12 13]
 [14 15]]

train index: [0 1 2 3 4 5 6 7]
val index: [8 9]
val data
[[16 17]
 [18 19]]

 

입력 데이터에 대해 k번 반복하여 학습 인덱스와 검증 인덱스를 반환합니다.

 

 

구현

K폴드 교차 검증을 하는 로지스틱 회귀 모델을 구현합니다.

 

유방암 데이터셋을 불러옵니다.

from sklearn.datasets import load_breast_cancer

breast_cancer = load_breast_cancer()

data = breast_cancer.data
target = breast_cancer.target
feature_names = breast_cancer.feature_names

print('data shape:', data.shape)
print('data sample:', data[0])
print('feature_names:', feature_names)

print('target shape:', target.shape)
print('target sample:', target[0], target[1], target[2])

 

타겟 형태를 변형합니다.

target = target.reshape(-1, 1)
target.shape

 

8:2 비율로 학습 데이터와 테스트 데이터를 분할합니다.

from sklearn.model_selection import train_test_split

x_train, x_test, y_train, y_test = train_test_split(data, target, test_size=0.2)

 

테스트 데이터를 전처리합니다.

x_test = (x_test - x_train.mean(axis=0)) / x_train.std(axis=0)

 

신경망을 정의합니다.

import numpy as np
import tensorflow as tf
from sklearn.model_selection import KFold

class Model:
    def __init__(self, lr=1e-3):
        with tf.name_scope('input'):
            self.x = tf.placeholder(tf.float32, [None, 30])
            self.y = tf.placeholder(tf.float32, [None, 1])

        with tf.name_scope('layer'):
            fc = tf.layers.dense(self.x, 64, tf.nn.relu)
            fc2 = tf.layers.dense(fc, 64, tf.nn.relu)
            fc3 = tf.layers.dense(fc2, 1)
            
        with tf.name_scope('output'):
            self.output = tf.nn.sigmoid(fc3)

        with tf.name_scope('accuracy'):
            self.predict = tf.cast(tf.greater(self.output, tf.constant([0.5])), dtype=tf.float32)
            self.accuracy = tf.reduce_mean(tf.cast(tf.equal(self.y, self.predict), dtype=tf.float32))    
        
        with tf.name_scope('loss'):
            self.loss = -tf.reduce_mean(self.y * tf.log(self.output) + (1-self.y) * tf.log(1-self.output))

        with tf.name_scope('optimizer'):
            self.train_op = tf.train.GradientDescentOptimizer(lr).minimize(self.loss)
      
        self.sess = tf.Session()
        self.sess.run(tf.global_variables_initializer())
   
    def train(self, x_train, y_train, epochs=100):
        train_loss, train_acc = [], []
        for e in range(epochs):
            loss, acc, _ = self.sess.run([self.loss, self.accuracy, self.train_op], {self.x: x_train, self.y: y_train})
            train_loss.append(loss)
            train_acc.append(acc)
        print('train loss:', np.mean(train_loss), '/ train accuracy:', np.mean(train_acc))
    
    def score(self, x, y):
        return self.sess.run(self.accuracy, {self.x: x, self.y: y})

 

배치 경사 하강법으로 학습을 수행합니다.

def train(self, x_train, y_train, epochs=100):
    train_loss, train_acc = [], []
    for e in range(epochs):
        loss, acc, _ = self.sess.run([self.loss, self.accuracy, self.train_op], {self.x: x_train, self.y: y_train})
        train_loss.append(loss)
        train_acc.append(acc)
    print('train loss:', np.mean(train_loss), '/ train accuracy:', np.mean(train_acc))

 

K폴드 교차 검증을 위한 반복을 수행합니다.

model = Model()

k = 5
kf = KFold(n_splits=k, shuffle=True)

val_scores = []

for train_idx, val_idx in kf.split(x_train):
    train_fold, train_target = x_train[train_idx], y_train[train_idx]
    val_fold, val_target = x_train[val_idx], y_train[val_idx]
    
    train_mean = train_fold.mean()
    train_std = train_fold.std()
    
    train_fold = (train_fold - train_mean) / train_std
    val_fold = (val_fold - train_mean) / train_std
    
    model.train(train_fold, train_target, epochs=500)
    acc = model.score(val_fold, val_target)
    val_scores.append(acc)

print('\nk-fold validation accuracy:', np.mean(val_scores))

print('\ntest accuracy:', model.score(x_test, y_test))
train loss: 0.615141 / train accuracy: 0.6809341
train loss: 0.5062132 / train accuracy: 0.91860986
train loss: 0.43708414 / train accuracy: 0.91754395
train loss: 0.37843737 / train accuracy: 0.92094517
train loss: 0.33940402 / train accuracy: 0.9114397

k-fold validation accuracy: 0.9076923

test accuracy: 0.85087717

 

전체 학습 데이터를 5분할합니다.

k = 5
kf = KFold(n_splits=k, shuffle=True)

 

각 반복의 학습 폴드를 이용하여 검증 폴드를 전처리합니다.

_x_train, _y_train = x_train[train_idx], y_train[train_idx]
x_val, y_val = x_train[val_idx], y_train[val_idx]

train_mean = _x_train.mean()
train_std = _x_train.std()

_x_train = (_x_train - train_mean) / train_std
x_val = (x_val - train_mean) / train_std

 

각 반복의 검증 폴드를 이용한 성능 평가에 대한 평균을 구합니다.

val_scores = []

...

acc = model.score(x_val, y_val)
val_scores.append(acc)

...

print('\nk-fold validation accuracy:', np.mean(val_scores))

 

 

  Comments,     Trackbacks
로지스틱 회귀(Logistic Regression)

로지스틱 회귀(Logistic Regression)

로지스틱 회귀는 0 또는 1과 같은 범주를 갖는 이진 분류(binary classification) 문제를 위한 알고리즘으로 해당 범주에 대한 확률을 예측합니다. 

 

 

 

시그모이드 함수(Sigmoid Function)

시그모이드 함수는 출력으로 $0<y<1$ 의 범위를 갖는 활성화 함수(activation function)의 한 종류입니다. 

 

 

시그모이드 함수의 유도되는 과정은 다음과 같습니다.

 

오즈 비(odds ratio) > 로짓 함수(logit function) > 시그모이드 함수(sigmoid function)

 

 

오즈 비는 성공 확률 $p$ 과 실패 확률 $1-p$ 의 비율을 나타냅니다.

 

$OR = {p \over 1-p}$

 

import numpy as np
import matplotlib.pyplot as plt

def odds_ratio(x):
    return x / (1-x)

x = np.arange(0, 1, 0.01)
y = odds_ratio(x)

plt.plot(x, y)
plt.title('Odds ratio')
plt.xlabel('p')
plt.show()

 

$p$ 의 값은 1에 가까워지면 급격히 증가하는 특징을 가집니다.

 

 

로짓 함수는 오즈 비에 로그를 취한 함수입니다.

 

$logit(p)=\text{log}({p \over 1-p})$

 

import numpy as np
import matplotlib.pyplot as plt

def odds_ratio(x):
    return x / (1-x)

def logit(x):
    return np.log(odds_ratio(x))

x = np.arange(0.001, 1, 0.001)
y = logit(x)

plt.plot(x, y)
plt.title('Logit function')
plt.xlabel('p')
plt.show()

 

$p$ 가 0.5일 때 0이 되고 0과 1일 때 각각 음의 무한, 양의 무한이 되는 특징을 가집니다.

 

 

로짓 함수를 출력을 $z$ 로 놓고 $z$ 대하여 다시 정리하면 로지스틱 함수가 됩니다.

 

$\text{log}({p \over 1-p})=z$ 

 

$sigmoid(z)=\frac{1}{ 1+e^{-z} }$

 

import numpy as np
import matplotlib.pyplot as plt

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

x = np.arange(-10, 10, 0.1)
y = sigmoid(x)

plt.plot(x, y)
plt.title('Logistic function')
plt.xlabel('z')
plt.show()

 

로짓 함수를 반대로 뒤집어 놓은 모양으로 S자를 나타내어 로지스틱 함수 시그모이드 함수라고 합니다.

 

이러한 시그모이드 함수를 통해 출력된 값을 0.5와 같은 임계값을 기준으로 나누면 이진 분류할 수 있는 것입니다.

 

 

손실 함수

이진 분류 문제에서 손실 함수는 어떻게 정의할 수 있을까요?

 

이진 분류 문제에서 정답 데이터는 0과 1의 범주를 갖으며 신경망의 예측에 해당하는 시그모이드 함수의 출력은 0과 1 사이의 범위를 갖습니다. 이에 대해 로그 함수의 특징을 이용하여 손실 함수를 정의할 수 있습니다.

 

로그 함수는 입력이 0에 가까지면 출력의 절대값이 커지고, 입력이 1에 가까워지면 출력의 절대값이 작아집니다.

 

 

이 같은 특징을 이용하여 다음과 같이 나타낼 수 있습니다.

 

$\hat y = sigmoid(x)$

 

$\text{if } y=1 \rightarrow -\text{log}(\hat y)$

 

$\text{if } y=0 \rightarrow -\text{log}(1 - \hat y)$

 

정답 $ y $ 가 1인 경우에는 예측 $ \hat y $ 이 0에 가까워진다면 오차는 커지고 1에 가까워진다면 오차는 작아지게 됩니다. $ y $ 가 0인 경우에는 $ \hat y$ 가 0에 가까워진다면 오차는 작아지고 1에 가까워진다면 오차는 커지게 됩니다.

 

이를 통해 손실 함수를 정의하면 다음과 같습니다.

 

$L=-{1 \over n}\displaystyle \sum_{i=1}^n \Big[ y_i \text{log}(\hat y_i))+(1-y_i) \text{log}(1-\hat y_i) \Big]$

 

 

구현

로지스틱 회귀 모델을 구현합니다.

 

유방암 데이터셋을 불러옵니다.

from sklearn.datasets import load_breast_cancer

breast_cancer = load_breast_cancer()

data = breast_cancer.data
target = breast_cancer.target
feature_names = breast_cancer.feature_names

print('data shape:', data.shape)
print('data sample:', data[0])
print('feature_names:', feature_names)

print('target shape:', target.shape)
print('target sample:', target[0], target[1], target[2])
data shape: (569, 30)
data sample: [1.799e+01 1.038e+01 1.228e+02 1.001e+03 1.184e-01 2.776e-01 3.001e-01
 1.471e-01 2.419e-01 7.871e-02 1.095e+00 9.053e-01 8.589e+00 1.534e+02
 6.399e-03 4.904e-02 5.373e-02 1.587e-02 3.003e-02 6.193e-03 2.538e+01
 1.733e+01 1.846e+02 2.019e+03 1.622e-01 6.656e-01 7.119e-01 2.654e-01
 4.601e-01 1.189e-01]
feature_names: ['mean radius' 'mean texture' 'mean perimeter' 'mean area'
 'mean smoothness' 'mean compactness' 'mean concavity'
 'mean concave points' 'mean symmetry' 'mean fractal dimension'
 'radius error' 'texture error' 'perimeter error' 'area error'
 'smoothness error' 'compactness error' 'concavity error'
 'concave points error' 'symmetry error' 'fractal dimension error'
 'worst radius' 'worst texture' 'worst perimeter' 'worst area'
 'worst smoothness' 'worst compactness' 'worst concavity'
 'worst concave points' 'worst symmetry' 'worst fractal dimension']
target shape: (569,)
target sample: 0 0 0

 

8:2의 비율로 학습 데이터와 테스트 데이터로 분할합니다.

from sklearn.model_selection import train_test_split

x_train, x_test, y_train, y_test = train_test_split(data, target, test_size=0.2)

 

데이터 스케일을 확인합니다.

import matplotlib.pyplot as plt

fig = plt.figure(figsize=(12, 6))
ax = fig.add_subplot(111)

for i in range(x_train.shape[1]): # all features
    ax.scatter(x_train[:, i], y_train, s=10)
    
plt.title('Raw')
plt.xlabel('x')
plt.ylabel('y')
plt.show()

 

학습 데이터와 테스트 데이터를 표준화합니다.

train_mean = x_train.mean(axis=0)
train_std = x_train.std(axis=0)

x_train = (x_train - train_mean) / train_std
x_test = (x_test - train_mean) / train_std

 

산점도로 나타나는 x축의 스케일이 조정되었습니다.

import matplotlib.pyplot as plt

fig = plt.figure(figsize=(12, 6))
ax = fig.add_subplot(111)

for i in range(x_train.shape[1]): # all features
    ax.scatter(x_train[:, i], y_train, s=10)
    
plt.title('Standardization')
plt.xlabel('x')
plt.ylabel('y')
plt.show()

 

신경망을 정의합니다.

import numpy as np
import tensorflow as tf

class Model:
    def __init__(self, lr=1e-3):
        tf.reset_default_graph()
        
        with tf.name_scope('input'):
            self.x = tf.placeholder(tf.float32, [None, 30])
            self.y = tf.placeholder(tf.float32, [None, 1])

        with tf.name_scope('layer'):
            fc = tf.layers.dense(self.x, 128, tf.nn.relu)
            fc2 = tf.layers.dense(fc, 128, tf.nn.relu)
            fc3 = tf.layers.dense(fc2, 1)
            
        with tf.name_scope('output'):
            self.output = tf.nn.sigmoid(fc3)

        with tf.name_scope('accuracy'):
            self.predict = tf.cast(tf.greater(self.output, tf.constant([0.5])), dtype=tf.float32)
            self.accuracy = tf.reduce_mean(tf.cast(tf.equal(self.y, self.predict), dtype=tf.float32))    
        
        with tf.name_scope('loss'):
            self.loss = -tf.reduce_mean(self.y * tf.log(self.output) + (1-self.y) * tf.log(1-self.output))

        with tf.name_scope('optimizer'):
            self.train_op = tf.train.GradientDescentOptimizer(lr).minimize(self.loss)

        with tf.name_scope('summary'):
            self.summary_loss = tf.placeholder(tf.float32)
            self.summary_accuracy = tf.placeholder(tf.float32)
            
            tf.summary.scalar('loss', self.summary_loss)
            tf.summary.scalar('accuracy', self.summary_accuracy)
            
            self.merge = tf.summary.merge_all()

        self.writer = tf.summary.FileWriter('./tmp/logistic-regression_breast_cancer_1', tf.get_default_graph())
        
        self.sess = tf.Session()
        
        self.sess.run(tf.global_variables_initializer())
                               
    def train(self, x, y, epochs, batch_size=32):
        data_size = len(x)
        for e in range(epochs):
            epoch_loss, epoch_acc = [], []
            
            idx = np.random.permutation(np.arange(data_size))
            x_train, y_train = x[idx], y[idx]
            for i in range(0, data_size, batch_size):
                si, ei = i, i + batch_size
                if ei > data_size:
                    ei = data_size

                x_batch, y_batch = x_train[si:ei, :], y_train[si:ei]
                
                loss, acc, _ = self.sess.run([self.loss, self.accuracy, self.train_op], {self.x: x_batch, self.y: y_batch})
                
                epoch_loss.append(loss)
                epoch_acc.append(acc)
            
            summary = self.sess.run(self.merge, {self.summary_loss: np.mean(epoch_loss), self.summary_accuracy: np.mean(epoch_acc)})
            self.writer.add_summary(summary, e)
            
            print('epoch:', e + 1, ' / loss:', np.mean(epoch_loss), '/ accuracy:', np.mean(epoch_acc))
    
    def score(self, x, y):
        return self.sess.run(self.accuracy, {self.x: x, self.y: y})

 

미니 배치 경사 하강법으로 학습을 수행합니다.

def train(self, x, y, epochs, batch_size=32):
    data_size = len(x)
    for e in range(epochs):
        epoch_loss, epoch_acc = [], []

        idx = np.random.permutation(np.arange(data_size))
        x_train, y_train = x[idx], y[idx]
        for i in range(0, data_size, batch_size):
            si, ei = i, i + batch_size
            if ei > data_size:
                ei = data_size

            x_batch, y_batch = x_train[si:ei, :], y_train[si:ei]

            loss, acc, _ = self.sess.run([self.loss, self.accuracy, self.train_op], {self.x: x_batch, self.y: y_batch})

            epoch_loss.append(loss)
            epoch_acc.append(acc)

        summary = self.sess.run(self.merge, {self.summary_loss: np.mean(epoch_loss), self.summary_accuracy: np.mean(epoch_acc)})
        self.writer.add_summary(summary, e)

        print('epoch:', e + 1, ' / loss:', np.mean(epoch_loss), '/ accuracy:', np.mean(epoch_acc))

 

모델을 학습하고 테스트합니다.

model = Model()
model.train(x_train, y_train, epochs=200)
model.score(x_test, y_test)
...

epoch: 190  / loss: 0.1256168 / accuracy: 0.96875
epoch: 191  / loss: 0.12216289 / accuracy: 0.96875
epoch: 192  / loss: 0.1224106 / accuracy: 0.96875
epoch: 193  / loss: 0.12211617 / accuracy: 0.96875
epoch: 194  / loss: 0.12102139 / accuracy: 0.96875
epoch: 195  / loss: 0.12139161 / accuracy: 0.96875
epoch: 196  / loss: 0.12274469 / accuracy: 0.96875
epoch: 197  / loss: 0.12596376 / accuracy: 0.96339285
epoch: 198  / loss: 0.12571438 / accuracy: 0.97083336
epoch: 199  / loss: 0.1223996 / accuracy: 0.97291666
epoch: 200  / loss: 0.12401686 / accuracy: 0.97291666

0.98245615

 

에포크에 대한 정확도와 손실 함수의 그래프는 다음과 같습니다.

 

 

 

'머신러닝 > 딥러닝' 카테고리의 다른 글

소프트맥스 회귀(Softmax Regression) (1)  (0) 2020.11.10
교차 검증(Cross Validation)  (0) 2020.11.09
선형 회귀(Linear Regression)  (0) 2020.11.06
순전파와 역전파  (0) 2020.11.06
신경망 학습  (0) 2020.11.04
  Comments,     Trackbacks
선형 회귀(Linear Regression)

선형 회귀(Linear Regression)

종속 변수 $y$ 와 하나 이상의 독립 변수 $x$ 와의 선형 관계를 모델링하는 통계학 기법입니다. 변수 $x$ 의 값은 독립적으로 변할 수 있으나 $y$ 의 값은 $x$ 의 값에 의해 종속적으로 결정됩니다.

 

1개의 독립 변수를 갖는 단순 선형 회귀(simple linear regression)의 수식은 다음과 같습니다.

 

$y=Wx+b$

 

이는 직선 방정식으로 $W$ 는 기울기, $b$ 는 절편이며 신경망의 파라미터(parameter)로 각각 가중치(weight)편향(bias)을 의미합니다.

 

 

즉 입력 데이터에 대한 예측으로 직선을 구하는 문제라고 할 수 있습니다.

 

 

2개 이상의 독립 변수를 다루면 이를 다중 선형 회귀(multiple linear regression)라고 합니다.

 

$y=W_0x_0+W_1x_1+...W_nx_n+b$

 

이는 데이터가 갖는 다수의 특성(feature) $x_0, ..., x_n$ 을 고려하여 $y$ 를 예측하는 것입니다. 입력 데이터는 n차원을 갖는 벡터로 데이터의 특성의 개수와 가중치의 개수는 같아야 합니다.

 

 

행렬의 곱

다음과 같은 다중 선형 회귀의 수식은 벡터의 내적 또는 행렬의 곱으로 나타낼 수 있습니다.

 

$y=W_0x_0+W_1x_1+...W_nx_n+b$

 

이는 데이터의 개수와 관련이 있는데 1개의 입력 데이터 $ [ x_0\ x_1\ x_2\ \cdots\ x_n  ] $ 에 대한 연산은 벡터의 내적으로 표현할 수 있습니다.

 

$
y =   
\left[  
    \begin{array}{c}  
      x_{0}\ x_{1}\ x_{2}\ \cdots\ x_{n}  
    \end{array}  
  \right]  
\left[  
    \begin{array}{c}  
      W_{0} \\  
      W_{1} \\  
      W_{2} \\  
      \cdot\cdot\cdot \\  
      W_{n}  
    \end{array}  
  \right]  
+  
b
= x_0W_0 + x_1W_1 + x_2W_2 + ... + x_nW_n + b 
$

입력 데이터가 여러 개인 경우 행렬의 곱으로 표현할 수 있습니다.

$\left[   
    \begin{array}{c}   
      x_{11}\ x_{12}\ x_{13}\ x_{14} \\   
      x_{21}\ x_{22}\ x_{23}\ x_{24} \\   
      x_{31}\ x_{32}\ x_{33}\ x_{34} \\   
      x_{41}\ x_{42}\ x_{43}\ x_{44} \\   
      x_{51}\ x_{52}\ x_{53}\ x_{54} \\   
    \end{array}   
  \right]   
\left[   
    \begin{array}{c}   
      w_{1} \\   
      w_{2} \\   
      w_{3} \\   
      w_{4} \\   
    \end{array}   
  \right]  

  +  

\left[   
    \begin{array}{c}   
      b \\   
      b \\   
      b \\   
      b \\

      b \\   
    \end{array}   
  \right]  
  =   
\left[   
    \begin{array}{c}   
      x_{11}w_{1}+ x_{12}w_{2}+ x_{13}w_{3}+ x_{14}w_{4} + b \\   
      x_{21}w_{1}+ x_{22}w_{2}+ x_{23}w_{3}+ x_{24}w_{4} + b \\   
      x_{31}w_{1}+ x_{32}w_{2}+ x_{33}w_{3}+ x_{34}w_{4} + b \\   
      x_{41}w_{1}+ x_{42}w_{2}+ x_{43}w_{3}+ x_{44}w_{4} + b \\   
      x_{51}w_{1}+ x_{52}w_{2}+ x_{53}w_{3}+ x_{54}w_{4} + b \\   
    \end{array}   
  \right]$

 

넘파이의 np.dot 을 이용하면 두 행렬간의 곱 연산을 수행할 수 있습니다.

import numpy as np

a = np.arange(20).reshape(5, 4)
b = np.array([1, -1, 1, -1, 0, 0, 0, 0]).reshape(-1, 2)

r = np.dot(a, b)

print(a)
print(b)
print(r)
[[ 0  1  2  3]
 [ 4  5  6  7]
 [ 8  9 10 11]
 [12 13 14 15]
 [16 17 18 19]]
[[ 1 -1]
 [ 1 -1]
 [ 0  0]
 [ 0  0]]
[[  1  -1]
 [  9  -9]
 [ 17 -17]
 [ 25 -25]
 [ 33 -33]]

 

 

데이터 전처리

신경망에 입력되는 데이터가 여러 특성을 갖을 경우에는 각 특성의 데이터 분포를 확인해야 합니다. 주어지는 데이터는 항상 신경망 학습을 위해 특화되어 있지 않으며 특성에 따라 구성된 값의 단위가 다르다는 것입니다.

 

데이터의 스케일을 조정하는 방법에는 표준화(standardization)정규화(normalization)가 있습니다.

 

표준화는 데이터를 평균 $ \mu $ 이 0이고 표준 편차 $ \sigma $ 가 1인 분포로 만들어주는 것입니다.

 

$ X = { X - \mu \over \sigma} $ 

 

정규화는 스케일의 범위를 $ [0, 1] $ 로 만들어 주는 것입니다. 

 

$ X = { X - X_{min} \over X_{max} - X_{min} } $

 

특성의 영향을 균등하게 하는 것이라고 할 수 있습니다.

 

테스트 데이터를 산점도를 통해 비교해보면 기존 분포의 모양을 유지하며 y축의 스케일이 조정된 것을 확인할 수 있습니다.(실제 데이터는 y축이 아닌 x축 데이터를 전처리합니다.)

import numpy as np
import matplotlib.pyplot as plt

x = 500
y = np.random.normal(0, 10, x)

fig = plt.figure(figsize=(16, 4))

plt.subplot(131)
plt.scatter(range(x), y)
plt.title('Raw')

plt.subplot(132)
y_std = (y - y.mean()) / y.std()
plt.scatter(range(x), y_std, color='red')
plt.title('Standardization')

plt.subplot(133)
y_norm = (y - y.min()) / (y.max() - y.min())
plt.scatter(range(x), y_norm, color='green')
plt.title('Normalization')

plt.show()

 

 

손실 함수(Loss Function)

회귀 문제에서 손실 함수는 주로 평균 제곱 오차(mean squared error, MSE)가 사용되며 정답 $y$ 과 예측 $\hat y$ 의 오차 또는 손실을 최소화(minimize)하는 방향으로 파라미터를 학습합니다.

 

$MSE={1 \over n}\displaystyle \sum_{i=1}^n (y_i-\hat y_i)^2$

 

 

구현

다중 선형 회귀 모델을 구현합니다.

 

보스턴 주택 가격 데이터셋을 불러옵니다.

from sklearn.datasets import load_boston

boston = load_boston()

data = boston.data
target = boston.target
feature_names = boston.feature_names

print('data shape:', data.shape)
print('data sample:', data[0])
print('feature_names:', feature_names)

print('target shape:', target.shape)
print('target sample:', target[0], target[1], target[2])
data shape: (506, 13)
data sample: [6.320e-03 1.800e+01 2.310e+00 0.000e+00 5.380e-01 6.575e+00 6.520e+01
 4.090e+00 1.000e+00 2.960e+02 1.530e+01 3.969e+02 4.980e+00]
feature_names: ['CRIM' 'ZN' 'INDUS' 'CHAS' 'NOX' 'RM' 'AGE' 'DIS' 'RAD' 'TAX' 'PTRATIO'
 'B' 'LSTAT']
target shape: (506,)
target sample: 24.0 21.6 34.7

 

신경망에 정의한 입력 타겟의 형태는 다음과 같습니다.

self.y = tf.placeholder(tf.float32, [None, 1])

 

따라서 기존 타겟 데이터의 형태 변형이 필요합니다.

target = target.reshape(-1, 1)
target.shape
(506, 1)

 

8:2의 비율로 학습 데이터와 테스트 데이터로 분할합니다.

from sklearn.model_selection import train_test_split

x_train, x_test, y_train, y_test = train_test_split(data, target, test_size=0.2)

 

데이터 분포를 확인합니다.

import matplotlib.pyplot as plt

fig = plt.figure(figsize=(12, 6))
ax = fig.add_subplot(111)

for i in range(x_train.shape[1]): # all features
    ax.scatter(x_train[:, i], y_train, s=10)
    
plt.title('Raw')
plt.xlabel('x')
plt.ylabel('y')
plt.show()

 

산점도 상에 나타난 각 특성의 스케일은 0에서 700정도로 넓게 분포합니다. 안정적인 학습을 위해서는 전처리가 필요합니다.

 

표준화를 위해 학습 데이터의 평균과 표준 편차를 구합니다.

train_mean = x_train.mean(axis=0)
train_std = x_train.std(axis=0)

 

평균과 표준 편차를 이용해 학습 데이터와 테스트 데이터를 표준화합니다. 

x_train = (x_train - train_mean) / train_std
x_test = (x_test - train_mean) / train_std

 

주의할 점은 테스트 데이터를 전처리할 때도 학습 데이터의 통계를 이용한다는 것입니다. 테스트 데이터는 최종 성능 평가를 위한 것으로 어떠한 정보도 누출되어서는 안됩니다.

 

산점도로 나타나는 x축의 스케일이 조정되었습니다.

import matplotlib.pyplot as plt

fig = plt.figure(figsize=(12, 6))
ax = fig.add_subplot(111)

for i in range(x_train.shape[1]): # all features
    ax.scatter(x_train[:, i], y_train, s=10)
    
plt.title('Standardization')
plt.xlabel('x')
plt.ylabel('y')
plt.show()

 

신경망을 정의합니다.

import numpy as np
import tensorflow as tf

class Model:
    def __init__(self, lr=1e-3):
        with tf.name_scope('input'):
            self.x = tf.placeholder(tf.float32, [None, 13])
            self.y = tf.placeholder(tf.float32, [None, 1])

        with tf.name_scope('layer'):
            w = tf.Variable(tf.random_normal([13, 64]))
            b = tf.Variable(tf.random_normal([1]))

            z = tf.matmul(self.x, w) + b
            h = tf.nn.relu(z)

            w2 = tf.Variable(tf.random_normal([64, 1]))
            b2 = tf.Variable(tf.random_normal([1]))

        with tf.name_scope('output'):
            pred = tf.matmul(h, w2) + b2

        with tf.name_scope('loss'):
            self.loss = tf.losses.mean_squared_error(self.y, pred)

        with tf.name_scope('optimizer'):
            self.train_op = tf.train.GradientDescentOptimizer(lr).minimize(self.loss)

        with tf.name_scope('summary'):
            tf.summary.scalar('loss', self.loss)
            self.merge = tf.summary.merge_all()

        self.writer = tf.summary.FileWriter('./tmp/linear-regression', tf.get_default_graph())
        
        self.sess = tf.Session()
        
        self.sess.run(tf.global_variables_initializer())
                               
    def train(self, x, y, epochs):
        for e in range(epochs):
            summary, loss, _ = self.sess.run([self.merge, self.loss, self.train_op], {self.x: x, self.y: y})
            self.writer.add_summary(summary, e)
            print('epoch:', e + 1, ' / loss:', loss)
    
    def test(self, x, y):
        loss = self.sess.run(self.loss, {self.x: x, self.y: y})
        return loss

 

배치 경사 하강법으로 학습을 수행합니다.

def train(self, x, y, epochs):
    for e in range(epochs):
        summary, loss, _ = self.sess.run([self.merge, self.loss, self.train_op], {self.x: x, self.y: y})
        self.writer.add_summary(summary, e)
        print('epoch:', e + 1, ' / loss:', loss)

 

모델을 학습하고 테스트합니다.

model = Model()
model.train(x_train, y_train, epochs=1000)
model.test(x_test, y_test)
...

epoch: 990  / loss: 9.514389
epoch: 991  / loss: 9.510468
epoch: 992  / loss: 9.506571
epoch: 993  / loss: 9.502658
epoch: 994  / loss: 9.49877
epoch: 995  / loss: 9.494896
epoch: 996  / loss: 9.490974
epoch: 997  / loss: 9.4871645
epoch: 998  / loss: 9.483273
epoch: 999  / loss: 9.479406
epoch: 1000  / loss: 9.475529

14.222591

 

에포크에 대한 손실 함수의 그래프는 다음과 같습니다.

 

 

 

'머신러닝 > 딥러닝' 카테고리의 다른 글

교차 검증(Cross Validation)  (0) 2020.11.09
로지스틱 회귀(Logistic Regression)  (0) 2020.11.06
순전파와 역전파  (0) 2020.11.06
신경망 학습  (0) 2020.11.04
퍼셉트론(Perceptron)  (0) 2020.11.04
  Comments,     Trackbacks
순전파와 역전파

신경망(Neural Network)

신경망의 내부 동작은 추론 과정에 해당하는 순전파(forward-propagation)학습 과정에 해당하는 역전파(back-propagation)로 나누어집니다.

 

특히 역전파는 다층 퍼셉트론과 같은 은닉층(hidden layer)을 포함하는 신경망에서 경사 하강법(gradient descent)을 이용한 학습 과정으로 미분의 체인 룰(chain rule)을 통해 기울기가 역으로 전파되어 파라미터가 업데이트되는 원리입니다.

 

예제를 통해 순전파와 역전파의 계산 과정을 알아보겠습니다.

 

다음 신경망은 입력층, 은닉층, 출력층으로 구성되며 각 층은 2개의 노드를 가지고 있습니다.

신경망에서 학습 가능한 파라미터는 가중치 $w_1$~$w_8$ 와 편향 $b_1, b_2$ 이며, 은닉층 노드 $h_1, h_2$ 와 출력층 노드 $o_1, o_2$ 에는 시그모이드 활성화 함수가 사용됩니다. 또한 손실 함수는 평균 제곱 오차를 사용하며 경사 하강법의 학습률은 0.5로 지정합니다.

 

초기 파라미터 값은 다음과 같습니다.

 

 

순전파(Forward-Propagation)

먼저 노드 $h_1$ 의 입력 $net_{h_1}$ 에 대해 계산합니다.

 

$ net_{h_1} = w_1 * i_1 + w_2 * i_2 + b_1 = 0.15 * 0.05 + 0.2 * 0.1 + 0.35 = 0.3775 $

 

$ net_{h_1} $ 을 시그모이드 함수를 통한 출력 $ out_{h_1} $ 은 다음과 같습니다.

 

$ out_{h_1} = {1 \over {1 + e^{net_{h_1}}} } = {1 \over {1 + e^{ -0.3775 }} } = 0.593269992 $

 

$ h_2 $ 도 같은 방식으로 계산합니다.

 

$ net_{h_2} = w_3 * i_1 + w_4 * i_2 + b_1 = 0.25 * 0.05 + 0.3 * 0.1 + 0.35 = 0.3925 $

 

$ out_{h_2} = {1 \over {1 + e^{net_{h_2}}} } = {1 \over {1 + e^{ -0.3925 }} } = 0.596884378 $

 

$ h_1, h_2 $ 에 대해 정리하면 다음과 같습니다.

 

$ net_{h_1} = 0.3775 $
$ net_{h_2} = 0.3925 $

$ out_{h_1} = 0.593269992 $

$ out_{h_2} = 0.596884378 $

 

 

위와 같은 방식으로 노드 $ o_1, o_2 $ 를 계산합니다.

 

$\begin{aligned}  
net_{o_1} & = w_5 * out_{h_1} + w_6 * out_{h_2} + b_2 \\\\  
& = 0.4 * 0.593269992 + 0.45 * 0.596884378 + 0.6 \\\\ 
& = 1.1059059669
\end{aligned}$

 

$ out_{o_1} = {1 \over {1 + e^{net_{o_1}}} } = {1 \over {1 + e^{ -1.1059059669 }} } = 0.7513650695224682 $

 

$\begin{aligned}   
net_{o_2} & = w_7 * out_{h_1} + w_8 * out_{h_2} + b_2 \\\\   
& = 0.5 * 0.593269992 + 0.55 * 0.596884378 + 0.6 \\\\  
& = 1.2249214039
\end{aligned}$ 

$ out_{o_2} = {1 \over {1 + e^{net_{o_2}}} } = {1 \over {1 + e^{ -1.2249214039 }} } = 0.772928465286981 $a

 

$o_1, o_2$ 에 대해 정리하면 다음과 같습니다.

 

$net_{o_1} = 1.1059059669$ 
$net_{o_2} = 1.2249214039 $
$out_{o_1} = 0.7513650695224682$
$out_{o_2} = 0.772928465286981 $

 

 

신경망의 출력과 타겟의 평균 제곱 오차를 계산합니다. 계산에 편의를 위해 $ 1 \over 2 $ 를 곱하며 1개의 입력에 대한 것이므로 다음과 같이 나타낼 수 있습니다.

 

$E = { 1\over 2 } (y - \hat y)^2 $

 

 

출력 $out_{o_1}$ 에 대한 타겟값은 $0.01$ 이며, $out_{o_2}$ 에 대한 타겟값은 $0.99$ 입니다.

 

$ target_{o_1} = 0.01 $

$ target_{o_2} = 0.99 $

 

$ E_{o_1} = { 1 \over 2 } ( target_{o_1} - out_{o_1} )^2 = { 1 \over 2 } ( 0.01 - 0.7513650695224682 )^2 = 0.274811083 $ 

 

$ E_{o_2} = { 1 \over 2 } ( target_{o_2} - out_{o_2} )^2 = { 1 \over 2 } ( 0.99 - 0.772928465286981 )^2 = 0.023560026 $ 

 

오차의 합을 계산합니다.

 

$ E_\text{total} = E_{o_1} + E_{o_2} = 0.274811083 + 0.023560026 = 0.298371109 $

 

역전파(Back-Propagation)

신경망에서 학습 가능한 파라미터는 가중치 $w_1$~$w_8$ 와 편향 $b_1, b_2$ 입니다. 이에 대해 경사 하강법을 적용하기 위해서는 먼저 각 파라미터에 대해 기울기를 계산해야 합니다.

 

우선 출력층에 가까운 $w_5$~$w_8$, $b_2$ 에 대해 업데이트를 진행합니다.

$w_5$ 에 대한 오차의 기울기 $\partial E_\text{total} \over \partial w_5$ 는 체인 룰에 의해 다음과 같이 나타낼 수 있습니다.

 

${\partial E_{total} \over \partial w_5} = {\partial E_\text{total} \over \partial out_{o_1}} {\partial out_{o_1} \over \partial net_{o_1}} {\partial net_{o_1} \over \partial w_5 }$

 

다음 항을 계산합니다.

 

${\partial E_\text{total} \over \partial w_5} = \color{red}{\partial E_\text{total} \over \partial out_{o_1}} {\partial out_{o_1} \over \partial net_{o_1}} {\partial net_{o_1} \over \partial w_5 }$

 

$\begin{aligned} 
{\partial E_\text{total} \over \partial out_{o_1}} & = { \partial \over \partial out_{o_1} } \left[ {1 \over 2}(target_{o_1} - out_{o_1} )^2 + {1 \over 2} ( target_{o_2} - out_{o_2} )^2 \right] \\\\
& = 2 * {1 \over 2}(target_{o_1} - out_{o_1}) * -1 + 0 \\\\ 
& = -(target_{o_1} - out_{o_1}) \\\\ 
& = -(0.01 - 0.7513650695224682) \\\\ 
& = 0.741365069 
\end{aligned}$

 

다음 항을 계산합니다.

 

${\partial E_\text{total} \over \partial w_5} = {\partial E_\text{total} \over \partial out_{o_1}} \color{red}{\partial out_{o_1} \over \partial net_{o_1}} {\partial net_{o_1} \over \partial w_5 }$

 

$ out_{o_1} $ 는 시그모이드 함수를 통한 출력입니다.

 

$ out_{o_1} = {1 \over { 1 + e^{-net_{o_1}} }} $

 

시그모이드 함수의 미분은 다음과 같습니다.

 

$ {\partial \over \partial x} \sigma (x) = \sigma (x) (1 - \sigma(x)) $

 

따라서 다음과 같이 계산할 수 있습니다.

 

$\begin{aligned} 
\partial out_{o_1} \over \partial net_{o_1} & = out_{o_1} ( 1 - out_{o_1} ) \\\\ 
& = 0.7513650695224682 ( 1 - 0.7513650695224682 ) \\\\ 
& = 0.186815602 
\end{aligned}$

 

다음 항을 계산합니다.

 

${\partial E_\text{total} \over \partial w_5} = {\partial E_\text{total} \over \partial out_{o_1}} {\partial out_{o_1} \over \partial net_{o_1}} \color{red}{\partial net_{o_1} \over \partial w_5 }$

 

$\begin{aligned} 
\partial net_{o_1} \over \partial w_5 & = { \partial \over \partial w_5 } (w_5 * out_{h_1} + w_6 * out_{h_2} + b_2) \\\\
& = 1 * out_{h_1} \\\\ 
& = 1 * 0.593269992 \\\\ 
& = 0.593269992 
\end{aligned}$

 

최종적으로 $ {\partial E_\text{total} \over \partial w_5} $ 은 다음과 같습니다.

 

$\begin{aligned} 
{\partial E_\text{total} \over \partial w_5} & = {\partial E_\text{total} \over \partial out_{o_1}} {\partial out_{o_1} \over \partial net_{o_1}} {\partial net_{o_1} \over \partial w_5 } \\\\  
& = 0.741365069 * 0.186815602 * 0.593269992 \\\\ 
& = 0.082167041
\end{aligned}$

 

이어서 경사 하강법을 적용하여 $w_5$ 를 업데이트합니다.

 

$\begin{aligned}
w_5^+ & = w_5 - \eta \, { \partial E_\text{total} \over \partial w_5 } \\\\ 
& = 0.4 - 0.5 * 0.082167041 \\\\ 
& = 0.35891648 
\end{aligned}$

 

이렇게 오차가 역방향으로 전파되어 파라미터를 업데이트하는 과정이 바로 오차 역전파인 것입니다.

 

같은 방식으로 나머지 $w_6$ 을 업데이트합니다.

 

$\begin{aligned}    
{\partial E_{total} \over \partial w_6} & = {\partial E_\text{total} \over \partial out_{o_1}} {\partial out_{o_1} \over \partial net_{o_1}} {\partial net_{o_1} \over \partial w_6 } \\\\     
& = 0.741365069 * 0.186815602 * 0.596884378 \\\\   
& = 0.082667628
\end{aligned}$

 

$\begin{aligned}  
w_6^+ & = w_6 - \eta \, { \partial E_\text{total} \over \partial w_6 } \\\\   
& = 0.45 - 0.5 * 0.082667628 \\\\  
& = 0.408666186
\end{aligned}$

 

$w_7, w_8$ 은 출력 노드 $o_2$ 에 영향을 주는 파라미터입니다.

 

${\partial E_\text{total} \over \partial w_7} = {\partial E_\text{total} \over \partial out_{o_2}} {\partial out_{o_2} \over \partial net_{o_2}} {\partial net_{o_2} \over \partial w_7 }$

 

$\begin{aligned}   
{\partial E_\text{total} \over \partial out_{o_2}} & = { \partial \over \partial out_{o_2} } \left[ {1 \over 2}(target_{o_1} - out_{o_1} )^2 + {1 \over 2} ( target_{o_2} - out_{o_2} )^2 \right] \\\\
& = 0 + 2 * {1 \over 2}(target_{o_2} - out_{o_2}) * -1 \\\\  
& = -(target_{o_2} - out_{o_2}) \\\\   
& = -(0.99 - 0.772928465286981) \\\\   
& = -0.217071535 
\end{aligned}$

 

$\begin{aligned}  
\partial out_{o_2} \over \partial net_{o_2} & = out_{o_2} ( 1 - out_{o_2} ) \\\\   
& = 0.772928465286981 ( 1 - 0.772928465286981 ) \\\\   
& = 0.175510053
\end{aligned}$

 

$\begin{aligned}   
\partial net_{o_2} \over \partial w_7 & = { \partial \over \partial w_7 } (w_7 * out_{h_1} + w_8 * out_{h_2} + b_2) \\\\
& = 1 * out_{h_1} \\\\   
& = 1 * 0.593269992 \\\\   
& = 0.593269992   
\end{aligned}$

 

$\begin{aligned}       
{\partial E_\text{total} \over \partial w_7} & = {\partial E_\text{total} \over \partial out_{o_2}} {\partial out_{o_2} \over \partial net_{o_2}} {\partial net_{o_2} \over \partial w_7 } \\\\        
& = -0.217071535 * 0.175510053 * 0.593269992 \\\\      
& = -0.022602541
\end{aligned}$

 

$\begin{aligned} 
w_7^+ & = w_7 - \eta \, { \partial E_\text{total} \over \partial w_7 } \\\\  
& = 0.5 - 0.5 * -0.022602541 \\\\ 
& = 0.511301271
\end{aligned}$

 

 

$\begin{aligned}      
{\partial E_\text{total} \over \partial w_8} & = {\partial E_\text{total} \over \partial out_{o_2}} {\partial out_{o_2} \over \partial net_{o_2}} {\partial net_{o_2} \over \partial w_8 } \\\\       
& = -0.217071535 * 0.175510053 * 0.596884378 \\\\     
& = -0.022740242
\end{aligned}$

 

$\begin{aligned}   
w_8^+ & = w_8 - \eta \, { \partial E_\text{total} \over \partial w_8 } \\\\    
& = 0.55 - 0.5 * -0.022740242 \\\\   
& = 0.561370121
\end{aligned}$

 

 

편향 $b_2$ 은 2개의 출력 노드 $o_1, o_2$ 에 영향을 주는 파라미터입니다.

따라서 $b_2$ 에 대한 전체 오차 $E_\text{total}$ 의 미분을 다음과 같이 나타낼 수 있습니다.

 

${ \partial E_\text{total} \over \partial b_2} = { \partial E_{o_1} \over \partial b_2} + { \partial E_{o_2} \over \partial b_2}$

 

각각 체인 룰을 적용합니다.

 

$\begin{aligned}
{ \partial E_{o_1} \over \partial b_2} & = { \partial E_{o_1} \over \partial out_{o_1} } { \partial out_{o_1} \over \partial net_{o_1} } { \partial net_{o_1} \over \partial b_2 } \\\\ 
& = 0.741365069 * 0.186815602 * 1 \\\\ 
& = 0.138498562
\end{aligned}$

 

$\begin{aligned}
{ \partial E_{o_2} \over \partial b_2} & = { \partial E_{o_2} \over \partial out_{o_2} } { \partial out_{o_2} \over \partial net_{o_2} } { \partial net_{o_2} \over \partial b_2 } \\\\ 
& = -0.217071535 * 0.175510053 * 1 \\\\ 
& = -0.038098237
\end{aligned}$

 

$\begin{aligned}
{ \partial E_\text{total} \over \partial b_2} & = { \partial E_{o_1} \over \partial b_2} + { \partial E_{o_2} \over \partial b_2} \\\\ 
& = 0.138498562 - 0.038098237 \\\\ 
& = 0.100400325
\end{aligned}$ 

 

경사 하강법을 적용해 $b_2$ 를 업데이트합니다.

 

$\begin{aligned}    
b_2^+ & = b_2 - \eta \, { \partial E_\text{total} \over \partial b_2 } \\\\     
& = 0.6 - 0.5 * 0.100400325 \\\\    
& = 0.549799838
\end{aligned}$

 

 

다음은 $w_1$~$w_4$, $b_1$ 에 대한 업데이트입니다.

$w_1$ 에 대한 기울기는 다음과 같이 나타낼 수 있습니다.

 

$ { \partial E_\text{total} \over \partial w_1 } = { \partial E_\text{total} \over \partial out_{h1} } { \partial out_{h_1} \over \partial net_{h_1} } { \partial net_{h_1} \over \partial w_1 } $

 

여기서 은닉층의 노드 $h_1$ 의 출력 $out_{h_1}$ 은 2개의 출력 노드 $o_1, o_2$ 에 영향을 줍니다.

따라서 다음과 같이 나누어 줄 수 있습니다.

 

$ { \partial E_\text{total} \over \partial w_1 } = \color{red}{ \partial E_\text{total} \over \partial out_{h1} } { \partial out_{h_1} \over \partial net_{h_1} } { \partial net_{h_1} \over \partial w_1 } $

 

${ \partial E_\text{total} \over \partial out_{h_1} } = { \partial E_{o_1} \over \partial out_{h_1} } + { \partial E_{o_2} \over \partial out_{h_1} }$

 

각각 체인 룰을 적용하면 다음과 같습니다. 

 

${ \partial E_{o_1} \over \partial out_{h1} } = { \partial E_{o_1} \over \partial out_{o_1} } { \partial out_{o_1} \over \partial net_{o_1} } { \partial net_{o_1} \over \partial out_{h_1} } $

 

${ \partial E_{o_2} \over \partial out_{h_1} } = { \partial E_{o_2} \over \partial out_{o_2} } { \partial out_{o_2} \over \partial net_{o_2} } { \partial net_{o_2} \over \partial out_{h_1} } $

 

 

위에서 계산한 내용은 다음과 같습니다.

 

${\partial E_{o_1} \over \partial out_{o_1}} = 0.741365069$

${\partial out_{o_1} \over \partial net_{o_1}} = 0.186815602$

${\partial E_{o_2} \over \partial out_{o_2}} = -0.217071535$

${\partial out_{o_2} \over \partial net_{o_2}} = 0.175510053$

 

 

다음 항을 계산합니다.

 

${ \partial E_{o_1} \over \partial out_{h_1} } = { \partial E_{o_1} \over \partial out_{o_1} } { \partial out_{o_1} \over \partial net_{o_1} } \color{red}{ \partial net_{o_1} \over \partial out_{h_1} } $

 

$ { \partial net_{o_1} \over \partial out_{h_1} } = {\partial \over \partial out_{h_1}} (w_5 * out_{h_1} + w_6 * out_{h_2} + b_2) = w_5 = 0.4 $

 

$ { \partial E_{o_1} \over \partial out_{h_1} } $ 을 계산합니다.

 

$\begin{aligned}
{ \partial E_{o_1} \over \partial out_{h_1} } & = { \partial E_{o_1} \over \partial out_{o_1} } { \partial out_{o_1} \over \partial net_{o_1} } { \partial net_{o_1} \over \partial out_{h_1} } \\\\
& = 0.741365069 * 0.186815602 * 0.4 \\\\
& = 0.055399425
\end{aligned}$

 

동일한 방식으로 ${ \partial E_{o_2} \over \partial out_{h_1} }$ 도 계산합니다.

 

$\begin{aligned}  
{ \partial E_{o_2} \over \partial out_{h_1} } & = { \partial E_{o_2} \over \partial out_{o_2} } { \partial out_{o_2} \over \partial net_{o_2} } { \partial net_{o_2} \over \partial out_{h_1} } \\\\  
& = -0.217071535 * 0.175510053 * 0.5 \\\\  
& = -0.019049118
\end{aligned}$

 

최종적으로 ${ \partial E_\text{total} \over \partial out_{h_1} }$ 은 다음과 같습니다.

 

$\begin{aligned}
{ \partial E_\text{total} \over \partial out_{h_1} } & = { \partial E_{o_1} \over \partial out_{h_1} } + { \partial E_{o_2} \over \partial out_{h_1} } \\\\
& = 0.055399425 - 0.019049118 \\\\
& = 0.036350307 
\end{aligned}$

 

다음 항을 계산합니다.

 

$ { \partial E_\text{total} \over \partial w_1 } = { \partial E_\text{total} \over \partial out_{h1} } \color{red}{ \partial out_{h_1} \over \partial net_{h_1} } { \partial net_{h_1} \over \partial w_1 } $

 

$ { \partial out_{h_1} \over \partial net_{h_1} } = out_{h_1} ( 1 - out_{h_1} ) = 0.593269992 ( 1 - 0.593269992 ) = 0.241300709 $

 

다음 항을 계산합니다.

 

$ { \partial E_\text{total} \over \partial w_1 } = { \partial E_\text{total} \over \partial out_{h1} } { \partial out_{h_1} \over \partial net_{h_1} } \color{red}{ \partial net_{h_1} \over \partial w_1 } $

 

$ { \partial net_{h_1} \over \partial w_1 } = { \partial \over \partial w_1 } ( w_1 * i_1 + w_2 * i_2 + b_1 ) = i_1 = 0.05 $

 

최종적으로 $ { \partial E_\text{total} \over \partial w_1 } $ 을 계산합니다.

 

$\begin{aligned} 
{ \partial E_\text{total} \over \partial w_1 } & = { \partial E_\text{total} \over \partial out_{h1} } { \partial out_{h_1} \over \partial net_{h_1} } { \partial net_{h_1} \over \partial w_1 } \\\\ 
& = 0.036350307 * 0.241300709 * 0.05 \\\\ 
& = 0.000438568
\end{aligned}$

 

경사 하강법을 적용하여 $w_1$ 을 업데이트합니다.

 

$\begin{aligned} 
w_1^+ & = w_1 - \eta \, { \partial E_\text{total} \over \partial w_1 } \\\\  
& = 0.15 - 0.5 * 0.000438568 \\\\ 
& = 0.149780716
\end{aligned}$

 

동일하게 가중치 $w_2, w_3, w_4$ 도 업데이트합니다.

 

$\begin{aligned}  
{ \partial E_\text{total} \over \partial w_2 } & = { \partial E_\text{total} \over \partial out_{h1} } { \partial out_{h_1} \over \partial net_{h_1} } { \partial net_{h_1} \over \partial w_2 } \\\\  
& = 0.036350307 * 0.241300709 * 0.1 \\\\  
& = 0.000877135
\end{aligned}$

 

$\begin{aligned}   
w_2^+ & = w_2 - \eta \, { \partial E_\text{total} \over \partial w_2 } \\\\   
& = 0.2 - 0.5 * 0.000877135 \\\\   
& = 0.199561432
\end{aligned}$

 

$\begin{aligned}    
{ \partial E_\text{total} \over \partial w_3 } & = { \partial E_\text{total} \over \partial out_{h2} } { \partial out_{h_2} \over \partial net_{h_2} } { \partial net_{h_2} \over \partial w_3 } \\\\    
& = 0.041370323 * 0.240613417 * 0.05 \\\\    
& = 0.000497713
\end{aligned}$

 

$\begin{aligned}     
w_3^+ & = w_3 - \eta \, { \partial E_\text{total} \over \partial w_3 } \\\\     
& = 0.25 - 0.5 * 0.000497713 \\\\     
& = 0.249751144
\end{aligned}$

 

$\begin{aligned}    
{ \partial E_\text{total} \over \partial w_4 } & = { \partial E_\text{total} \over \partial out_{h1} } { \partial out_{h_1} \over \partial net_{h_1} } { \partial net_{h_1} \over \partial w_4 } \\\\    
& = 0.041370323 * 0.240613417 * 0.1 \\\\    
& = 0.000995426
\end{aligned}$

 

$\begin{aligned}     
w_4^+ & = w_4 - \eta \, { \partial E_\text{total} \over \partial w_4 } \\\\     
& = 0.3 - 0.5 * 0.000995426 \\\\     
& = 0.299502287
\end{aligned}$

 

 

편향 $b_1$ 은 2개의 은닉층 노드 $h1, h2$ 에 영향을 주는 파라미터입니다.

따라서 $b_1$ 에 대한 기울기는 다음과 같이 구할 수 있습니다.

 

$\begin{aligned} 
{ \partial E_\text{total} \over \partial b_1 } & = { \partial E_\text{total} \over \partial out_{h_1} } { \partial out_{h_1} \over \partial net_{h_1} } { \partial net_{h_1} \over \partial b_1 } + { \partial E_\text{total} \over \partial out_{h_2} } { \partial out_{h_2} \over \partial net_{h_2} } { \partial net_{h_2} \over \partial b_1 } \\\\
& = 0.000438568 * 0.241300709 * 1 + 0.041370323 * 0.240613417 * 1 \\\\ 
& = 0.010060082 
\end{aligned}$

 

$\begin{aligned}       
b_1^+ & = b_1 - \eta \, { \partial E_\text{total} \over \partial b_1 } \\\\       
& = 0.35 - 0.5 * 0.010060082 \\\\       
& = 0.344969959
\end{aligned}$

 

 

여기까지 모든 파라미터에 대한 업데이트를 완료했습니다.

 

$\begin{aligned}
w_1^+ = 0.149780716 \\
w_2^+ = 0.199561432 \\
w_3^+ = 0.249751144 \\
w_4^+ = 0.299502287 \\
w_5^+ = 0.358916480 \\
w_6^+ = 0.408666186 \\
w_7^+ = 0.511301271 \\
w_8^+ = 0.561370121 \\
b_1^+ = 0.344969959 \\
b_2^+ = 0.549799838
\end{aligned}$

 

이제 업데이트된 파라미터를 이용해 다시 순전파를 통해 출력값을 계산합니다.

 

$\begin{aligned}
net_{h_1} & = w_1 * i_1 + w_2 * i_2 + b_1 \\\\
& = 0.149780716 * 0.05 + 0.199561432 * 0.1 + 0.344969959 \\\\
& = 0.372415138
\end{aligned}$

 

$ out_{h_1} = {1 \over {1 + e^{net_{h_1}}} } = {1 \over {1 + e^{ -0.372415138 }} } = 0.592042432$

 

$\begin{aligned}
net_{h_2} & = w_3 * i_1 + w_4 * i_2 + b_1 \\\\
& = 0.249751144 * 0.05 + 0.299502287 * 0.1 + 0.344969959 \\\\
& = 0.387407745 
\end{aligned}$

 

$ out_{h_2} = {1 \over {1 + e^{net_{h_2}}} } = {1 \over {1 + e^{ -0.3874077449 }} } = 0.595658511 $

 

$\begin{aligned}     
net_{o_1} & = w_5 * out_{h_1} + w_6 * out_{h_2} + b_2 \\\\     
& = 0.358916480 * 0.592042432+ 0.408666186 * 0.595658511 + 0.549799838 \\\\    
& = 1.005719116
\end{aligned} $

 

$ out_{o_1} = {1 \over {1 + e^{net_{o_1}}} } = {1 \over {1 + e^{ -1.005719116 }} } = 0.732181538 $

 

$ \begin{aligned}     
net_{o_2} & = w_7 * out_{h_1} + w_8 * out_{h_2} + b_2 \\\\     
& = 0.511301271 * 0.592042432 + 0.561370121 * 0.595658511 + 0.549799838 \\\\    
& = 1.186896776
\end{aligned} $

 

$ out_{o_2} = {1 \over {1 + e^{net_{o_2}}} } = {1 \over {1 + e^{ -1.186896776 }} } = 0.766185596 $

 

$ E_{o_1} = { 1 \over 2 } ( target_{o_1} - out_{o_1} )^2 = { 1 \over 2 } ( 0.01 - 0.732181538 )^2 = 0.260773087$

 

$ E_{o_2} = { 1 \over 2 } ( target_{o_2} - out_{o_2} )^2 = { 1 \over 2 } ( 0.99 - 0.766185596 )^2 = 0.025046444$

 

$ E_\text{total} = E_{o_1} + E_{o_2} = 0.260773087 + 0.025046444 = 0.285819531 $

 

 

기존 오차와 비교해 감소되었습니다.

 

$ E_\text{old total} - E_\text{new total} = 0.298371109 - 0.285819531 = 0.012551578$

 

 

 

'머신러닝 > 딥러닝' 카테고리의 다른 글

교차 검증(Cross Validation)  (0) 2020.11.09
로지스틱 회귀(Logistic Regression)  (0) 2020.11.06
선형 회귀(Linear Regression)  (0) 2020.11.06
신경망 학습  (0) 2020.11.04
퍼셉트론(Perceptron)  (0) 2020.11.04
  Comments,     Trackbacks
동적 프로그래밍(Dynamic Programming)

동적 프로그래밍(Dynamic Programming, DP)

동적 프로그래밍은 MDP 와 같은 환경의 모델이 완벽히 주어졌을 때 푸는 방법론입니다. 즉 보상 함수와 전이 확률과 같은 정보를 알 때 시뮬레이션을 통해 계획(planning) 을 세우는 것이라고 할 수 있습니다.

 

MDP 를 푼다는 것은 크게 예측(prediction)제어(control) 문제로 나누어집니다.

 

prediction 은 주어진 정책 $\pi$ 에 대해 가치 함수를 계산하여 정책을 평가하는 것이라고 할 수 있으며 control 은 정책 평가를 통해 얻은 가치 함수를 이용해 정책을 개선하는 것이라고 할 수 있습니다.

 

 

정책 평가(Policy Evaluation)

정책이 좋은 정책인지 나쁜 정책인지 평가하는 것은 정책 $\pi$ 에 대한 가치 함수 $v_{\pi}$ 를 구하는 것과 같습니다. (아래에서 다루는 policy iteration 을 통해 직관적으로 이해할 수 있습니다.)

 

정책 $\pi$ 에 대한 $v_{\pi}$ 계산은 벨만 기대 방정식을 이용합니다.

 

$v_{\pi}(s) = \displaystyle \sum_{a \in A} \pi(a|s) \displaystyle \sum_{s' \in S} P_{ss'}^a \left[ r + \gamma v_{\pi}(s') \right]$

 

 

다음과 같은 episodic MDP 로 정의된 4x4 그리드월드 환경이 있습니다.

 

양쪽 끝의 종료 상태가 두 개있고 이를 제외한 나머지 상태는 4개의 행동을 취할 수 있습니다. 4개의 행동에 대해 동일한 확률을 가지는 정책이며 행동에 대한 보상은 모두 -1입니다. 가장자리 상태에서 주어진 환경을 벗어나는 행동을 하면 제자리로 돌아오며 상태 전이는 확정적으로 100% 확률로 전이됩니다. 또한 에피소딕 문제이므로 $\gamma=1$ 로 합니다.

 

초기에는 종료 상태 포함 모든 상태의 가치는 0으로 초기화했다고 가정합니다.

 

 

첫 번째 스텝에서 1번 상태의 가치를 계산하면 다음과 같습니다.

 

$4 * 0.25(-1 + 1 * 0) = -1$

 

2번에서 14번 또한 동일하게 계산되며 업데이트된 모든 상태는 다음과 같습니다.

 

 

이처럼 새로운 가치는 이전 가치와 즉각적인 보상의 기댓값을 이용하여 구하는데 이것을 기댓값 업데이트(expected update)라고 합니다. 이 업데이트는 샘플이 아닌 가능한 전체 상태에 대한 기댓값에 기반하여 이루어 지는 것입니다.

 

두 번째 스텝에서는 1번 상태의 가치를 계산하면 다음과 같습니다.

 

$0.25(-1 + 1 * 0) + 3 * 0.25(-1 + 1 * -1) = -1.75$

 

왼쪽으로 가는 행동에 대한 계산은 종료 상태의 가치는 0이므로 $0.25(-1 + 1 * 0)$ 으로 계산한 것이고 위로 가는 행동은 제자리로 돌아오기 때문에 자기 상태의 가치를 이용합니다. 따라서 위, 아래, 오른쪽 모두 동일하게 $0.25(-1 + 1 * -1)$ 으로 계산한 것입니다.

 

2번 상태의 가치의 계산은 다음과 같습니다.

 

$4 * 0.25(-1 + 1 * -1) = -2$

 

계산된 전체 가치는 다음과 같습니다.

 

 

이 과정을 반복하다 보면 각 상태의 값은 수렴하게 되는데 이를 실제 가치(true value) 라고 합니다.

 

 

 

정책 개선(Policy Improvement)

정책 개선 단계는 정책 평가에서 구한 가치 함수를 이용해 그리디 정책(greedy policy)을 생성합니다. 이는 각 상태에서 다음 상태가 최대 가치를 가지는 행동을 선택하게 정책을 수정하는 과정인 것입니다.

 

$\pi'=\text{greedy}(v_\pi)$

 

 

정책 평가의 첫 번째 스텝의 가치 함수를 이용해서 그리디 정책을 생성하면 다음과 같습니다.

 

모든 상태가 동일한 가치이기 때문에 여전히 랜덤한 정책을 갖게 됩니다.

 

 

두 번째 스텝의 가치 함수를 이용해보면 다음과 같습니다.

 

종료 상태와 근접한 상태의 행동 선택이 수정되었습니다.

 

 

실제 가치 함수에 수렴하면 최종적으로 다음과 같은 정책으로 수정됩니다.

 

 

 

정책 반복(Policy Iteration)

정책 평가와 정책 개선을 번갈아 수행하여 최적 정책 $\pi_*$ 및 최적 가치 함수 $v_*$ 로 수렴할 때까지 반복하는 것을 말합니다.

 

 

policy iteration 의 사이클은 다음과 같이 나타낼 수 있습니다.

 

$\pi_0 \overset {E} \longrightarrow v_{\pi_0} \overset {I} \longrightarrow \pi_1 \overset {E} \longrightarrow v_{\pi_1} \overset {I} \longrightarrow \pi_2 \overset {E} \longrightarrow \cdots \overset {I} \longrightarrow \pi_* \overset {E} \longrightarrow v_*$

 

정책 $\pi_0$ 으로 시작해서 정책 평가를 통해 $v_{\pi_0}$ 를 계산하고 이를 이용해서 정책 개선을 하면  $\pi_0$ 보다 향상된 $\pi_1$ 가 될 것입니다. 이 과정을 반복하면 최적 정책 $\pi_*$ 에 수렴할 것이고 이를 통해 계산된 가치 함수는 최적 가치 함수 $v_*$ 가 될 것입니다.

 

 

policy iteration 알고리즘은 다음과 같습니다.

 

 

 

구현

policy iteration 을 구현합니다.

import numpy as np

SIZE = 4
# SIZE = 3

T = [(0, 0), (3, 3)]
# T = [(3, 2), (1, 1)]

K = 3
R = -1
P = 0.25
GAMMA = 1

ACTION = ['left', 'up', 'down', 'right']

def get_value(table, row, col, action):
    if action == 'up':
        if row != 0:
            row -= 1
    elif action == 'down':
        if row != SIZE-1:
            row += 1
    
    if action == 'left':
        if col != 0:
            col -= 1
    elif action == 'right':
        if col != SIZE-1:
            col += 1
        
    return table[row][col]

def view_policy(policy):
    for row in range(SIZE):
        line = []
        for col in range(SIZE):
            if (row, col) in T:
                line.append('T')
            else:
                idx = policy[row][col]
                line.append(ACTION[idx])
        print(' | '.join(line))

def policy_evaluation(policy):
    table = np.zeros((SIZE, SIZE))
    for _ in range(100):
        pre_table = table.copy()
        for row in range(SIZE):
            for col in range(SIZE):
                if (row, col) in T:
                    continue
                update_value = 0
                for idx, prob in enumerate(policy[row][col]):
                    update_value += prob * (R + GAMMA*get_value(pre_table, row, col, ACTION[idx]))
                table[row][col] = update_value
    return table

def policy_improvement(table):
    policy = np.zeros((SIZE, SIZE, len(ACTION)))
    
    for row in range(SIZE):
        for col in range(SIZE):
            q = []
            for action in ACTION:
                q.append(R + GAMMA*get_value(table, row, col, action))
            idx = np.argmax(q)
            policy[row, col, idx] = 1.
            
    return policy

def policy_iteration():
    policy = np.ones((SIZE, SIZE, len(ACTION))) * P

    for k in range(K):
        print('\n\nk=',k)
        print('-'*50)
        table = policy_evaluation(policy)
        print(table)
        print('-'*50)
        policy = policy_improvement(table)
        view_policy(np.argmax(policy, axis=2))

print('Gridworld size : {} x {}'.format(SIZE, SIZE))
print('Terminate state:', *T)

policy_iteration()
Gridworld size : 4 x 4
Terminate state: (0, 0) (3, 3)


k= 0
--------------------------------------------------
[[  0.         -13.94260509 -19.91495107 -21.90482522]
 [-13.94260509 -17.92507693 -19.91551999 -19.91495107]
 [-19.91495107 -19.91551999 -17.92507693 -13.94260509]
 [-21.90482522 -19.91495107 -13.94260509   0.        ]]
--------------------------------------------------
T | left | left | left
up | left | left | down
up | up | right | down
up | right | right | T


k= 1
--------------------------------------------------
[[ 0. -1. -2. -3.]
 [-1. -2. -3. -2.]
 [-2. -3. -2. -1.]
 [-3. -2. -1.  0.]]
--------------------------------------------------
T | left | left | left
up | left | left | down
up | left | down | down
up | right | right | T


k= 2
--------------------------------------------------
[[ 0. -1. -2. -3.]
 [-1. -2. -3. -2.]
 [-2. -3. -2. -1.]
 [-3. -2. -1.  0.]]
--------------------------------------------------
T | left | left | left
up | left | left | down
up | left | down | down
up | right | right | T

 

 

가치 반복(Value Iteration)

policy iteration 은 최적 정책을 찾기 위해 정책 평가와 개선의 과정을 반복하는 것이었습니다. 하지만 정책 평가는 많은 비용이 발생하는 계산으로 $v_{\pi}$ 가 수렴하기 위해서는 많은 반복을 통해야만 하며 정책 이터레이션의 각 단계마다 계산해야하는 과정입니다. 이와 같은 정책 평가의 반복되는 과정은 중간에 중단된다 하더라도 정책 이터레이션의 수렴성이 보장되는데 이 같은 성질을 이용하여 적은 수의 반복으로 최적의 정책을 찾고자하는 것이 value iteration 입니다.

 

value iteration 은 벨만 최적 방정식을 이용합니다.

 

$\begin{aligned}
v_{k+1}(s) & \doteq {\underset {a} {\text{max}} } \, \mathbb{E}[R_{t+1} + \gamma v_k(S_{t+1}) \, | \, S_t=s, A_t=a] \\  
& = {\underset {a} {\text{max}} } \displaystyle \sum_{s',r} p(s',r \, | \, s,a)\Big[ r + \gamma v_k(s') \Big]  
\end{aligned}$

 

모든 상태 $s \in \mathcal{S}$ 에 대해 성립하며 임의의 $v_0$ 에 대해 $v_*$ 를 보장하는 조건하에 수열 ${v_k}$ 는 $v_*$ 로 수렴하는 것입니다.

 

벨만 최적 방정식은 $\underset {a} {\text{max}}$ 이 붙어 최적의 행동만을 고려한 계산입니다. 이것은 정책 개선과 유사하다고 할 수 있습니다. 따라서 value iteration 은 중단된 정책 평가와 정책 개선을 결합하는 것으로 적은 수의 반복으로도 policy iteration 과 같은 기능을 하게 되는 것입니다.

 

 

value iteration 알고리즘은 다음과 같습니다.

 

 

 

구현

value iteration 을 구현합니다.

# value iteration

import numpy as np

SIZE = 4

T = [(0, 0), (3, 3)]

K = 4
R = -1
GAMMA = 1

ACTION = ['left', 'up', 'down', 'right']

def get_value(table, row, col, action):
    if action == 'up':
        if row != 0:
            row -= 1
    elif action == 'down':
        if row != SIZE-1:
            row += 1
    
    if action == 'left':
        if col != 0:
            col -= 1
    elif action == 'right':
        if col != SIZE-1:
            col += 1
        
    return table[row][col]

def get_policy(table):
    policy = np.zeros((SIZE, SIZE, len(ACTION)))
    for row in range(SIZE):
        for col in range(SIZE):
            q = []
            for action in ACTION:
                q.append(R + GAMMA*get_value(table, row, col, action))
            idx = np.argmax(q)
            policy[row, col, idx] = 1.
            
    return policy

def view_policy(policy):
    for row in range(SIZE):
        line = []
        for col in range(SIZE):
            if (row, col) in T:
                line.append('T')
            else:
                idx = policy[row][col]
                line.append(ACTION[idx])
        print(' | '.join(line))


def value_iteration():
    table = np.zeros((SIZE, SIZE))
    for k in range(K):
        pre_table = table.copy()
        for row in range(SIZE):
            for col in range(SIZE):
                if (row, col) in T:
                    continue
                next_values = []
                for action in ACTION:
                    value = R + GAMMA*get_value(pre_table, row, col, action)
                    next_values.append(value)
                table[row][col] = np.max(next_values)
        print('\n\nk=', k)
        print('-'*20)
        print(table)
    print('\n\n')
    policy = get_policy(table)
    view_policy(np.argmax(policy, axis=2))
        
value_iteration()
k= 0
--------------------
[[ 0. -1. -1. -1.]
 [-1. -1. -1. -1.]
 [-1. -1. -1. -1.]
 [-1. -1. -1.  0.]]


k= 1
--------------------
[[ 0. -1. -2. -2.]
 [-1. -2. -2. -2.]
 [-2. -2. -2. -1.]
 [-2. -2. -1.  0.]]


k= 2
--------------------
[[ 0. -1. -2. -3.]
 [-1. -2. -3. -2.]
 [-2. -3. -2. -1.]
 [-3. -2. -1.  0.]]


k= 3
--------------------
[[ 0. -1. -2. -3.]
 [-1. -2. -3. -2.]
 [-2. -3. -2. -1.]
 [-3. -2. -1.  0.]]



T | left | left | left
up | left | left | down
up | left | down | down
up | right | right | T

 

 

 

'머신러닝 > 강화학습' 카테고리의 다른 글

Model-Free Prediction  (0) 2020.11.09
OpenAI Gym  (0) 2020.11.09
벨만 방정식(Bellman Equation)  (0) 2020.11.04
마르코프 결정 프로세스(Markov Decision Process)  (0) 2020.11.04
강화 학습(Reinforcement Learning)  (0) 2020.11.04
  Comments,     Trackbacks
신경망 학습

퍼셉트론(Perceptron)

다음은 입력 $x_1, x_2$ 에 대해 $y$ 를 출력하는 퍼셉트론입니다.

 

 

이와 같은 신경망을 학습하는 것은 입력으로 받는 $x_1, x_2$ 에 대해 적절한 가중치(weight) $w_1, w_2$ 와 편향(bias) $b$ 을 구하는 것이라고 할 수 있으며 이러한 가중치와 편향을 신경망에서는 파라미터(parameter)라고 합니다.

 

 

활성화 함수(Activation Function)

다음은 퍼셉트론의 동작을 나타냅니다.

 

$
y=   
\begin{cases}   0 \ ( \, b + w_1 x_1 + w_2 x_2 \le 0 \ ) \\\\  
1 \ ( \, b + w_1 x_1 + w_2 x_2 > 0 \ )  
\end{cases}
$

$b + w_1 x_1 + w_2 x_2$ 의 값에 임계값 0을 기준으로 0과 1을 분류하는 것인데 이 같은 임계값을 경계로 출력이 바뀌는 함수를 계단 함수(step function)이라고 합니다. 

 

def step_function(x):
    if x > 0:
        return 1
    else:
        return 0

 

또한 계단 함수와 같이 입력 신호에 대해 출력 신호로 변환하는 함수를 활성화 함수(activation function)라고 합니다. 보통 신경망에서는 활성화 함수로 비선형(nonlinear) 함수를 사용하는데, 이는 선형(linear) 함수를 사용하면 층을 깊게 하는 의미가 없기 때문입니다. 또한 활성화 함수로 사용하기 위해서는 미분 가능해야 하는데 이는 파라미터 업데이트 과정에서 미분을 이용하기 때문입니다.

 

 

데이터(Data)

학습의 목적은 주어진 데이터에 대해 적절한 파라미터를 구하는 것입니다. 그 과정에 있어 여러 데이터의 특성(feature)으로부터 패턴과 같은 규칙성을 찾아내고 파라미터를 조정하는 것입니다. 이 같은 데이터는 신경망 학습에서 없어서는 안되는 중요한 요소입니다.

 

다음은 Iris 라는 데이터의 구조입니다.

 

 

행은 샘플의 개수를 나타내며 열은 데이터가 갖는 특성을 의미합니다. 또한 해당 행에 대해 분류하고자하는 어떤 범주 또는 클래스의 값을 가지는 특성을 레이블(label)이라고 하며, 이 값은 신경망의 출력인 예측과 일치하고자하는 타겟(target)인 것입니다.

 

또한 데이터는 학습에 사용하는 학습 데이터(train data)와 테스트에 사용하는 테스트 데이터(test data)로 나누어지는데 이는 모델을 범용적으로 사용할 수 있게 일반화하는 것입니다. 

 

다음과 같이 데이터가 분리되어 있지 않은 경우도 있습니다.

from sklearn.datasets import load_boston

boston = load_boston()

data = boston.data
target = boston.target

print(dir(boston))
['DESCR', 'data', 'feature_names', 'filename', 'target']

 

사이킷런에서 제공하는 train_test_split 을 이용하면 학습 데이터와 테스트 데이터를 나눌 수 있습니다.

from sklearn.model_selection import train_test_split

x_train, x_test, y_train, y_test = train_test_split(data, target, test_size=0.2)

 

여기서 test_size=0.2 의 의미는 데이터의 20% 를 테스트 데이터로 분리하는 것이며 기본적으로 무작위로 섞여서 분리됩니다.

 

 

손실 함수(Loss Function)

신경망 학습을 위한 방향 또는 성능 지표를 의미하며 이를 통해 신경망의 파라미터를 조정하는 것입니다. 이 같은 손실 함수는 예측의 목표가 되는 타겟과의 관계로 정의할 수 있습니다.

 

회귀 문제에서 주로 사용되는 손실 함수인 평균 제곱 오차(mean squared error, MSE)는 다음과 같습니다.

 

$MSE={1 \over n}\displaystyle \sum_{i=1}^n (y_i-\hat y_i)^2$

 

데이터 $n$ 개에 대해 타겟 $y$ 과 예측 $\hat y$ 의 오차(error) 또는 손실(loss)의 평균을 나타내는 것으로 예측이 타겟에 가까울수록 손실값은 0에 가까워지게 됩니다. 

 

이러한 손실 함수는 신경망의 파라미터에 대해 미분이 가능해야 하며 이를 지표로 파라미터를 업데이트할 수 있게 됩니다.

 

 

경사 하강법(Gradient Descent)

경사 하강법은 파라미터를 학습하기 위한 최적화 알고리즘(optimizer)으로 손실 함수에서 계산된 손실값을 최소화(minimize)하는 방향으로 파라미터를 업데이트합니다.

 

손실 함수의 최솟값을 구하기 위해서는 기울기(gradient)를 따라 손실값이 감소하는 방향으로 이동해야 하는데, 위와 같이 기울기가 음수인 지점이라면 $w$ 값이 커져야 손실값이 작아지게 되고 기울기가 양수라면 $w$ 값이 작아져야 손실값이 작아져 최종적으로 최솟값에 수렴하게 되는 것입니다.

 

이러한 기울기는 경사 하강법에서 손실값을 낮추는 방안을 제시하는 지표로서 이용되는 것이며 신경망의 파라미터인 가중치나 편향에 대한 손실 함수의 미분으로 구할 수 있습니다.

 

파라미터에 대한 평균 제곱 오차 손실 함수의 기울기는 다음과 같습니다.

 

$MSE={1 \over n}\displaystyle \sum_{i=1}^n (y_i-\hat y_i)^2$

 

${\partial \over \partial w} MSE(w, b)$

 

${\partial \over \partial b} MSE(w, b)$

 

또한 신경망의 파라미터는 벡터이며 각 파라미터가 손실값에 미치는 영향력을 알아내기 위해 편미분을 이용하는데 이는 대상 변수 외에 다른 변수는 상수로 취급하는 것을 의미합니다. 

 

신경망의 파라미터를 업데이트하는 경사 하강법의 수식은 다음과 같습니다.

 

$\theta= \theta - \eta \, \nabla_\theta L(\theta)$

 

파라미터 $\theta$ 에 대한 손실 함수 $L(\theta)$ 의 기울기를 이용하여 파라미터를 업데이트하는 것으로  $\eta$ 는 학습률(step size, learning rate)로 업데이트하는 양을 의미합니다. 이러한 학습률은 하이퍼파라미터(hyperparameter)로 적절한 크기로 설정해야 안정적인 학습이 가능합니다.

 

신경망의 학습은 이러한 경사 하강법을 이용하여 반복적으로 파라미터를 업데이트하여 최적화(optimization)하는 것입니다.

 

 

추가적으로 배치(batch)와 관련하여 경사 하강법 종류는 다음과 같습니다.

 

- 확률적 경사 하강법(stochastic gradient descent, SGD)

- 배치 경사 하강법(batch gradient descent)

- 미니배치 경사 하강법(minibatch gradient descent)

 

배치란 경사 하강법의 계산에 사용되는 데이터의 단위를 의미합니다. 예를 들어 배치의 크기가 32이면 한 번에 계산되는 데이터의 개수가 32개라는 것입니다. 배치의 크기는 학습에 영향을 미치는 요소로 이와 관련하여 경사 하강법의 종류가 나뉘어 진 것입니다.

 

확률적 경사 하강법은 하나의 데이터(배치 크기 1)마다 계산하는 것으로 학습 전에 데이터를 무작위로 추출하기 때문에 확률적인 것입니다. 특징은 계산 비용이 적은 대신 수렴이 불안정합니다. 이에 반해 배치 경사 하강법은 모든 학습 데이터에 대해 한 번에 계산하는 것입니다. 특징은 계산 비용이 큰 대신 수렴 과정이 안정적입니다. 또한 두 방식을 절충한 것이 미니 배치 경사 하강법입니다.

 

 

또한 경사 하강법에는 단점이 있는데 신경망 파라미터의 무작위 초기화(random initialization) 방식으로 인해 전역 극소값(global minimum)이 아닌 지역 극소값(local minimum)에 수렴할 수 있다는 것입니다.

 

 

 

구현

경사 하강법을 이용해 학습하는 선형 회귀 모델을 구현합니다.

 

학습 데이터를 생성합니다.

import numpy as np
import matplotlib.pyplot as plt

n = 50

x_data = np.random.rand(n, 1)
b_noise = np.random.normal(0, 0.5, size=(n, 1))

w = 5
b = 1

y_data = w * x_data + b + b_noise

plt.scatter(x_data, y_data)
plt.show()

 

모델을 정의합니다.

class Model:
    def __init__(self, lr=0.01, epochs=100):
        self.w = np.random.rand()
        self.b = np.random.rand()
        self.lr = lr
        self.epochs = epochs
    
    def predict(self, x):
        return self.w * x + self.b
    
    def train(self, x_data, y_data):
        for epoch in range(self.epochs):
            for x, y in zip(x_data, y_data):
                y_hat = self.predict(x)
                err = -(y - y_hat)
                w_grad = self.lr * err * x
                b_grad = self.lr * err
                self.w -= w_grad
                self.b -= b_grad

    def draw_graph(self, x_data, y_data):
        plt.scatter(x_data, y_data)
        pt1 = (0.01, self.predict(0.01))
        pt2 = (0.99, self.predict(0.99))
        plt.plot([pt1[0], pt2[0]], [pt1[1], pt2[1]], 'red')
        plt.title('Linear Regression')
        plt.xlabel('x')
        plt.ylabel('y')
        plt.show()

 

학습을 수행합니다.

def train(self, x_data, y_data):
    for epoch in range(self.epochs):
        for x, y in zip(x_data, y_data):
            y_hat = self.predict(x)
            err = -(y - y_hat)
            w_grad = self.lr * err * x
            b_grad = self.lr * err
            self.w -= w_grad
            self.b -= b_grad

 

선형 회귀의 예측 $\hat y$ 을 수식으로 나타내면 다음과 같습니다.

 

$\hat y = wx + b$

 

1개의 데이터로 업데이트를 수행하여 손실 함수는 제곱 오차(squared error)를 이용합니다.

 

$L = {1 \over 2}(y - \hat y)^2$

 

가중치 $w$ 에 대한 손실 함수 $L$ 의 기울기는 다음과 같이 구할 수 있습니다.

 

$\begin{aligned}  
{\partial L \over \partial w} & = {\partial \over \partial w} {1 \over 2}(y - \hat y)^2 \\  
& = 2 * {1 \over 2}(y - \hat y) * - {\partial \over \partial w} \hat y \\ 
& = (y - \hat y) * -x  \\
& = -(y - \hat y) x
\end{aligned}$

 

경사 하강법을 통한 파라미터 업데이트 식으로 나타내면 다음과 같습니다.

 

$\begin{aligned}
w & = w - \eta {\partial L \over \partial w} \\
& = w + \eta (y - \hat y) x
\end{aligned}$

 

 

학습을 진행하고 그래프를 확인합니다.

model = Model()
model.train(x_data, y_data)
model.draw_graph(x_data, y_data)

 

 

'머신러닝 > 딥러닝' 카테고리의 다른 글

교차 검증(Cross Validation)  (0) 2020.11.09
로지스틱 회귀(Logistic Regression)  (0) 2020.11.06
선형 회귀(Linear Regression)  (0) 2020.11.06
순전파와 역전파  (0) 2020.11.06
퍼셉트론(Perceptron)  (0) 2020.11.04
  Comments,     Trackbacks
퍼셉트론(Perceptron)

퍼셉트론(Perceptron)

1957년 Frank Rosenblatt 가 고안한 알고리즘으로 인공 신경망의 기원이 됩니다.

 

뇌의 신경 세포인 뉴런(neuron)의 구조에서 수상돌기(dendrites)는 다른 신경 세포로부터 받은 신호를 축삭돌기(axon)을 통해 다른 뉴런이나 세포에 전달합니다. 이와 비슷한 기능을 하는 퍼셉트론은 다수의 입력 신호로부터 하나의 신호(0 또는 1)를 출력합니다.

 

 

다음은 2개의 입력 신호 $x_1, x_2$ 를 받아 신호 $y$ 를 출력하는 퍼셉트론입니다. 

 

 

여기서 $w_1, w_2$ 는 가중치(weight)를 의미하는데 이는 출력에 대해 각 신호에 주는 영향을 조절하는 요소입니다.

 

 

계산은 다음과 같으며 임계값 $\theta$ 을 기준으로 0과 1을 출력하는데 이를 이진 분류(binary classification)라고 합니다.

 

$y=
\begin{cases}
0 \ ( \, w_1 x_1 + w_2 x_2 \le \theta \ ) \\\\
1 \ ( \, w_1 x_1 + w_2 x_2 > \theta \ )
\end{cases}
$

 

또한 1을 출력하는 것을 뉴런을 활성화(activation)한다고 합니다. 

 

 

이어서 편향 $b$ 를 도입하기 위해 $\theta$ 를 $-b$ 로 치환하고 이항하면 다음과 같습니다. 

 

$y= 
\begin{cases} 
0 \ ( \, b + w_1 x_1 + w_2 x_2 \le 0 \ ) \\\\
1 \ ( \, b + w_1 x_1 + w_2 x_2 > 0 \ )
\end{cases} 
$

 

편향은 뉴런의 활성화를 조절하는 요소입니다. 예를 들어 $b$ 가 -1인 경우, $w_1 x_1 + w_2 x_2$ 의 값이 1을 초과할 때 뉴런이 활성화하고 $b$ 가 -10인 경우, $w_1 x_1 + w_2 x_2$ 의 값이 10을 초과해야 뉴런이 활성화합니다. 즉 얼마나 쉽게 활성화를 하는지를 조절하는 것입니다.

 

 

구현

퍼셉트론으로 논리 회로를 구현합니다.

 

 

AND Gate

 

모든 입력이 참인 경우에만 참을 출력합니다.

 

AND 게이트에 대한 진리표를 참고하여 임의의 가중치와 편향을 설정합니다.

 

import numpy as np

def AND(x1, x2):
    # inputs
    x = np.array([x1, x2])
    
    # weights
    w = np.array([0.4, 0.4])
    
    # bias
    b = -0.5
    
    z = np.sum(x*w) + b
    
    if z <= 0:
        return 0
    else:
        return 1
print(AND(0, 0))
print(AND(0, 1))
print(AND(1, 0))
print(AND(1, 1))
0
0
0
1

 

 

NAND Gate

 

AND 게이트와 반대로 모든 입력이 참인 경우에만 거짓을 출력합니다.

 

NAND 게이트에 대한 진리표를 참고하여 임의의 가중치와 편향을 설정합니다.

 

import numpy as np

def NAND(x1, x2):
    # inputs
    x = np.array([x1, x2])
    
    # weights
    w = np.array([-0.4, -0.4])
    
    # bias
    b = 0.5
    
    z = np.sum(x*w) + b
    
    if z <= 0:
        return 0
    else:
        return 1
print(NAND(0, 0))
print(NAND(0, 1))
print(NAND(1, 0))
print(NAND(1, 1))
1
1
1
0

 

 

OR Gate

 

하나 이상의 입력이 참인 경우에 참을 출력합니다.

 

OR 게이트에 대한 진리표를 참고하여 임의의 가중치와 편향을 설정합니다.

 

import numpy as np

def OR(x1, x2):
    # inputs
    x = np.array([x1, x2])
    
    # weights
    w = np.array([0.4, 0.4])
    
    # bias
    b = -0.3
    
    z = np.sum(x*w) + b
    
    if z <= 0:
        return 0
    else:
        return 1
print(OR(0, 0))
print(OR(0, 1))
print(OR(1, 0))
print(OR(1, 1))
0
1
1
1

 

 

선형과 비선형

계속해서 퍼셉트론으로 XOR 게이트를 구현할 수 있을까요?

 

XOR 게이트는 다음과 같은 진리표를 가지며 두 입력 중 하나의 입력이 참일 경우에만 참을 출력합니다.

 

 

퍼셉트론은 분류하고자 하는 두 영역을 직선으로 나누는 것으로 AND, NAN, OR 의 진리표에 만족합니다.

 

 

하지만 XOR 에서는 선형(linear)적인 직선으로는 입력에 대해 참과 거짓을 분류하지 못하는데 이러한 문제를 비선형(nonlinear) 문제라고 합니다.

 

 

 

 

다층 퍼셉트론(Multilayer Perceptron, MLP)

앞서 다룬 하나의 입력층과 출력층을 갖는 단층의 구조로는 비선형 문제에 해당하는 XOR 게이트를 구현하지 못했습니다. 

 

 

이를 해결하는 것이 다층 퍼셉트론으로 입력층과 출력층 사이에 은닉층(hidden layer)을 갖는 구조입니다. 

 

 

이 같은 구조로 기본 논리 게이트를 조합하여 XOR 을 구현할 수 있게 됩니다.

 

def XOR(x1, x2):
    s1 = NAND(x1, x2)
    s2 = OR(x1, x2)
    y = AND(s1, s2)
    return y
print(XOR(0, 0))
print(XOR(0, 1))
print(XOR(1, 0))
print(XOR(1, 1))
0
1
1
0

 

 

'머신러닝 > 딥러닝' 카테고리의 다른 글

교차 검증(Cross Validation)  (0) 2020.11.09
로지스틱 회귀(Logistic Regression)  (0) 2020.11.06
선형 회귀(Linear Regression)  (0) 2020.11.06
순전파와 역전파  (0) 2020.11.06
신경망 학습  (0) 2020.11.04
  Comments,     Trackbacks