[CS294] Lecture 3 : TensorFlow and Neural Nets Review Session (notebook)

UC Berkeley의 강화학습 강의 CS294(2018 Fall semester)를 정리한 포스트입니다.

코스 전체에서 다루는 주제입니다. (자세한 항목은 syllabus 참조하시기 바랍니다.)

  1. From supervised learning to decision making
  2. Model-free algorithms: Q-learning, policy gradients, actor-critic
  3. Advanced model learning and prediction
  4. Exploration
  5. Transfer and multi-task learning, meta-learning
  6. Open problems, research talks, invited lectures


포스트 목차


Overview

이번 강의에서는 앞부분은 지난 강의에서 못한부분 보강을 하고, TensorFlow를 사용하는 것에 대한 전반적인 내용을 다뤘다. 지난 강의에 대한 보강은 지난 포스트 뒷부분에 정리하였다. 여기는 TensorFlow에 대한 내용을 정리한다.

CS294-03-01

강화학습에서 데이터를 통해 model을 학습시키고, agent가 행동을 취하면 또 다른 data가 쌓이고, 그 데이터를 통해 다시 model을 학습시키는 일련의 과정이 반복된다. 이번 강의에서는 모델을 학습시키는 부분을 다룬다.

CS294-03-02

모델을 학습시키는 여러 머신러닝 방법 중 하나를 설명하는 그림이다. 여기서 $f_{\theta} $ 는 우리의 모델이고($\theta$는 모델 파라미터), $f_{\theta}(x) $ 는 모델에 데이터를 넣은 출력이다. 입력 데이터 $x$에 맞는 정답 label은 $y$이다. 모든 학습 데이터셋 $(x,y)$ 에 대해 모델의 출력과 정답 label의 차이의 합은 $ \underset{(x,y) \in \mathcal{D}}{\sum} \lVert f_{\theta}(x) - y \rVert $ 이다. 이 차이를 가장 적게 만드는 모델 파라미터 $arg \underset{\theta}{min}$ 를 찾는 것이 목표다.

여기서 모델 $f_{\theta}$는 neural networks을 사용하고, $arg \underset{\theta}{min}$ 의 방법으로는 gradient descent 를 사용한다.

CS294-03-03

간단한 신경망 모델의 예시이다. 여기서 우리가 레이어 한층씩 쌓아가면서 모델을 정의하면, TensorFlow는 chain rule대로 gradient를 계산해준다. 학습을 진행할때는 loss값을 gradient를 곱해서 모델 파라미터 $\theta$를 업데이트 한다.


Implementation

*[TensorFlow] Basic tutorial 포스트와 일부 겹치는 내용이 있다.

이 부분은 강의에서는 jupyter notebook을 통해 진행되었다. jupyter notebook은 한 블록씩 코드를 실행시키면서 결과를 볼 수 있기때문에 실험적으로 구현해볼 때 쓰기 좋은 툴이다.

1
2
3
4
5
import tensorflow as tf
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.cm as cm
import matplotlib.patches as mpatches

구현에 필요한 라이브러리를 import 한다. numpy는 행렬연산을 위한 파이썬 라이브러리다. matplotlib는 시각화를 위한 파이썬 라이브러리다.

1
2
3
4
5
6
7
def tf_reset():
  try:
    sess.close()
  except:
    pass
  tf.reset_default_graph()
  return tf.Session()

기존에 열려있는 session이 있다면 닫고 초기화해서 반환하는 함수이다.

How to input data

1
2
3
4
5
6
7
8
sess = tf_reset()

a = tf.constant(1.0)
b = tf.constant(2.0)

c = a+b
c_run = sess.run(c)
print('c = {0}'.format(c_run))
1
c = 3.0

간단한 덧셈 1.0 + 2.0을 수행하는 코드이다. 이 코드에서는 ab를 각각 1.02.0으로 고정해놓고 수행하는 코드이고, 아래 코드는 입력 ab에 원하는 데이터를 넣어줄 수 있는 코드다.

1
2
3
4
5
6
7
8
9
10
11
12
sess = tf_reset()

a = tf.placeholder(dtype=tf.float32, shape=[1], name='a_placeholder')
b = tf.placeholder(dtype=tf.float32, shape=[1], name='b_placeholder')

c = a+ b

c0_run = sess.run(c, feed_dict={a: [1.0], b: [2.0]})
c1_run = sess.run(c, feed_dict={a: [2.0], b: [4.0]})

print('c0 = {0}'.format(c0_run))
print('c1 = {0}'.format(c1_run))

여기서 ab에 어떤 데이터가 들어갈지 모르니 처음 선언할때 tf.placeholder로 선언을 해두고, 나중에 sess.run을 할 때 feed_dictab값을 정해준다. 각각 다른 값을 넣고 c0_runc1_run이라는 변수로 결과값을 뽑아낸 코드다.

1
2
c0 = [3.]
c1 = [6.]

이번엔 a와 b의 값뿐만 아니라 shape까지 처음 선언할 때 고정하지 않는 코드이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
sess = tf_reset()

a = tf.placeholder(dtype=tf.float32, shape=[None], name='a_placeholder')
b = tf.placeholder(dtype=tf.float32, shape=[None], name='b_placeholder')

c = a+ b

c0_run = sess.run(c, feed_dict={a: [1.0], b: [2.0]})
c1_run = sess.run(c, feed_dict={a: [1.0, 2.0], b: [2.0, 4.0]})

print(a)
print('a shape: {0}'.format(a.get_shape()))
print(b)
print('b shape: {0}'.format(b.get_shape()))

print('c0 = {0}'.format(c0_run))
print('c1 = {0}'.format(c1_run))

tf.placeholder을 선언할 때 shape을 [None]으로 선언하고, sess.run할 때 다양한 차원의 값을 넣어 줄 수 있다.

1
2
3
4
5
6
Tensor("a_placeholder:0", shape=(?,), dtype=float32)
a shape: (?,)
Tensor("b_placeholder:0", shape=(?,), dtype=float32)
b shape: (?,)
c0 = [3.]
c1 = [3. 6.]

ab의 shape을 보면 지정해주지 않았기 때문에 둘다 (?,)으로 나오는 것을 확인할 수 있다. 또한 c0_runc1_run에서 각기 다른 shape의 데이터를 넣어줘도 잘 작동하는 것을 확인할 수 있다.

How to perform computations

1
2
3
4
5
6
7
8
9
sess = tf_reset()

a = tf.constant([[-1.],[-2.],[-3.]],dtype=tf.float32)
b = tf.constant([[1., 2., 3.]],dtype=tf.float32)

a_run, b_run = sess.run([a,b])
print('a:\n{0}'.format(a_run))
print('b:\n{0}'.format(b_run))

1
2
3
4
5
6
a:
[[-1.]
[-2.]
[-3.]]
b:
[[1. 2. 3.]]

a는 수직방향으로 정의된 벡터고, b는 수평방향으로 정의된 벡터다.

1
2
3
4
5
6
c = b + b

c_run = sess.run(c)

print('b:\n{0}'.format(b_run))
print('c:\n{0}'.format(c_run))
1
2
3
4
b:
[[1. 2. 3.]]
c:
[[2. 4. 6.]]

b + b처럼 같은 방향의 벡터끼리는 무리없이 잘 수행된다.

1
2
3
4
5
6
c = a + b
c_run = sess.run(c)

print('a:\n{0}'.format(a_run))
print('b:\n{0}'.format(b_run))
print('c:\n{0}'.format(c_run))
1
2
3
4
5
6
7
8
9
10
a:
[[-1.]
[-2.]
[-3.]]
b:
[[1. 2. 3.]]
c:
[[0. 1. 2.]
[-1. 0. 1.]
[-2. -1. 0.]]

그러나 수직벡터a와 수평벡터b를 합산하는 연산을 하니 결과가 다소 이상하다. 그 이유는 TensorFlow에서 차원이 맞지 않는 텐서에 대해서는 브로드캐스팅을 수행하여 차원을 맞춰주기 때문이다. 따라서 a를 수평으로 복제하여 3 by 3 행렬로 만들고, b를 수직으로 복제하여 3 by 3 행렬로 만든 후 더하게 된다. 이를 잘 이용한다면 편리한 기능이지만, 실수로 수행된다면 결과를 망칠 수 있으니 유의하여야 한다.

1
2
3
4
5
6
7
8
9
10
c_elementwise = a * b
c_matmul = tf.matmul(b,a)

c_elementwise_run, c_matmul_run = sess.run([c_elementwise, c_matmul])


print('a:\n{0}'.format(a_run))
print('b:\n{0}'.format(b_run))
print('c_elementwise:\n{0}'.format(c_elementwise_run))
print('c_matmul:\n{0}'.format(c_matmul_run))

곱셈에서도 마찬가지다. 일반적 * 연산은 같은 위치 원소끼리의 곱을 의미하는데, ab가 브로드캐스팅되어 3 by 3 행렬로 맞춰진 후에 원소끼리의 곱이 수행된다. 반면 행렬곱 tf.matmul은 1 by 3 행렬 b와 3 by 1 행렬 a 가 행렬곱으로 곱해져서 1 by 1 의 결과가 나온다.

1
2
3
4
5
6
7
8
9
10
11
12
13
a:
[[-1.]
[-2.]
[-3.]]
b:
[[1. 2. 3.]]
c_elementwise_run:
[[-1. -2. -3.]
[-2. -4. -6.]
[-3. -6. -9.]]

c_matmul:
[[-14.]]

다음은 연쇄적으로 이어지는 연산이다.

1
2
3
4
5
6
7
8
c0 = b + b
c1 = c0 + 1

c0_run, c1_run = sess.run([c0,c1])

print('b:\n{0}'.format(b_run))
print('c0:\n{0}'.format(c0_run))
print('c1:\n{0}'.format(c1_run))
1
2
3
4
5
6
b:
[[1. 2. 3.]]
c0:
[[2. 4. 6.]]
c1:
[[3. 5. 7.]]

다음은 TensorFlow에 내장된 연산함수를 사용하는 것이다.

1
2
3
4
5
c = tf.reduce_mean(b)

c_run = sess.run(c)
print('b:\n{0}'.format(b_run))
print('c:\n{0}'.format(c_run))
1
2
3
4
b:
[[1. 2. 3.]]
c:
2.0

tf.reduce_mean은 평균을 내고 잔여 차원을 없애준다. 다른 유용한 TensorFlow 함수들도 여러 있으니 홈페이지를 참조하면 구현에 필요한 함수를 찾을 수 있을 것이다.

How to create variables

위에서 데이터의 입력과 연산에 대해 다뤘으니, 학습 가능한 모델파라미터인 variable을 입력과의 연산에 넣을 것이다.

1
2
3
4
5
6
7
sess = tf_reset()

b = tf.constant([[1., 2., 3.]],dtype=tf.float32)

b_run = sess.run(b)

print('b:\n{0}'.format(b_run))
1
2
b:
[[1. 2. 3.]]

우선 입력 데이터 b를 만들어준다. 나중에는 tf.placeholder로 선언하여 학습 데이터를 넣어주지만, 일단 tf.constant로 선언한다.

1
2
3
4
5
6
7
var_init_value = [[2.0, 4.0, 6.0]]
var = tf.get_variable(name='myvar',
                      shape=[1,2],
                      dtype=tf.float32,
                      initializer=tf.constant_initializer(var_init_value))

print(var)
1
<tf.Variable 'myvar:0' shape=(1,3) dtype=float32_ref>

tf.get_variable은 variable을 만들어주는 함수이고, tf.constant_initializer은 입력한 값으로 variable을 초기화 시키는 함수다.

1
print(tf.global_variables())
1
[<tf.Variable 'myvar:0' shape=(1,3) dtype=float32_ref>]

tf.global_variables은 생성되는 variable을 추적한다. 앞서 선언한 var을 확인할 수 있다. 이제 variable을 이용해 연산을 해보자.

1
2
3
4
c = b + var
print(b)
print(var)
print(c)
1
2
3
Tensor("Const:0", shape=(1,3), dtype=float32)
<tf.Variable 'myvar:0' shape=(1,3) dtype=float32_ref>
Tensor("add:0", shape=(1,3), dtype=float32)

Variable 이 Tensor로써 표현된 것을 확인 할 수 있다.

1
2
init_op = tf.global_variables_initializer()
sess.run(init_op)

tf.global_variables에 등록된 variable을 초기화해준다.

1
2
3
4
c_run = sess.run(c)
print('b:\n{0}'.format(b_run))
print('var:\n{0}'.format(var_init_value))
print('c:\n{0}'.format(c_run))
1
2
3
4
5
6
b:
[[1. 2. 3.]]
var:
[[2.0 4.0 6.0]]
c:
[[3. 6. 9.]]

sess.run을 통해 입력 데이터 b와 variable var의 연산결과를 확인한다.

How to train a neural network for a simple regression problem

데이터 입력, variable선언, 이들의 연산까지 다루었으니 이제 어떻게 학습을 시킬지에 대해 다룰 것이다.

1
2
3
4
inputs = np.linspace(-2*np.pi, 2*np.pi, 10000)[:, None]
outputs = np.sin(inputs) + 0.05 * np.random.normal(size=[len(inputs),1])

plt.scatter(inputs[:,0], outputs[:,0], s=0.1, color='k', marker='o')

CS294-03-04

inputsnp.linspace를 이용하여 구간 [-2*np.pi, 2*np.pi]을 10000개로 쪼개어 데이터를 만들어주었고, outputsinputs을 입력으로 sine함수를 만들되, 약간의 정규분포를 따르는 노이즈를 더했다. 그래프는 inputs에 따른 outputs를 표현한 것이다.

아래 코드는 뉴럴넷 연산을 정의하고 mean square error loss를 최소화 시키는 gradient descent optimizer을 이용해서 학습시키는 코드다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
sess = tf_reset()

def create_model():
    input_ph = tf.placeholder(dtype=tf.float32, shape=[None,1])
    output_ph = tf.placeholder(dtype=tf.float32, shape=[None,1])

    # ceate variables
    W0 = tf.get_variable(name='W0', shape=[1,20],initializer=tf.contrib.layers.xavier_initializer())
    W1 = tf.get_variable(name='W1', shape=[20,20],initializer=tf.contrib.layers.xavier_initializer())
    W2 = tf.get_variable(name='W2', shape=[20,1],initializer=tf.contrib.layers.xavier_initializer())

    b0 = tf.get_variable(name='b0', shape=[20], initializer=tf.constant_initializer(tf.zeros[20]))
    b1 = tf.get_variable(name='b1', shape=[20], initializer=tf.constant_initializer(tf.zeros[20]))
    b2 = tf.get_variable(name='b2', shape=[1], initializer=tf.constant_initializer(tf.zeros[1]))

    weights = [W0, W1, W2]
    biases = [b0, b1, b2]
    activations = [tf.nn.relu, tf.nn.relu, None]

    # creat computation graph 
    layer = input_ph
    for W, b, activation in zip(weights, biases, activations):
        layer = tf.matmul(layer,W) + b
        if activation is not None:
            layer = activation(layer)
    output_pred = layer

    return input_ph, output_ph, output_pred

input_ph, output_ph, output_pred = create_model()

뉴럴넷의 노드 구조는 1(input) -> 20 -> 20 -> 1(output) 이고, weight와 bias를 이용하고, activation function은 ReLU function을 사용하였다. input_ph는 데이터 입력자리, output_ph는 데이터 정답 레이블 자리, output_pred는 모델에 데이터를 넣었을때 결과물을 출력하는 Tensor이다. model instance를 만든다기 보다는, input_ph로부터 output_pred까지의 연산 그래프를 만들어주는 방식의 코드이다.

1
2
3
4
5
6
7
8
9
10
# create loss
mse = tf.reduce_mean(0.5 * tf.square(output_pred - output_ph))

# create optimizer
opt = tf.train.AdamOptimizer().minimize(mse)

# initialize variables
sess.run(tf.global_variables_initializer())
# create saver to save model variables
saver = tf.train.Saver()

loss는 mean square error로 정의해준다. batch 단위로 발생하는 output_predoutput_ph의 loss를 합쳐주기 위해 tf.reduce_mean을 사용한다. mse를 최소화 하는 optimizer 은 tf.train.AdamOptimizer을 사용한다. 다른 optimizer들도 있다. 모델을 저장하고 재사용하기 위해 tf.train.Saver을 사용한다.

1
2
3
4
5
6
7
8
9
10
11
batch_size = 32
for training_step in range(10000):
    indices = np.random.randint(low=0,high=len(inputs), size=batch_size)
    input_batch = inputs[indices]
    output_batch = outputs[indices]

    _, mse_run = sess.run([opt, mse], feed_dict={input_ph: input_batch, output_ph: output_batch})

    if training_step %1000 == 0:
        print('{0:04d} mse: {1:.3f}'.format(training_step, mse_run))
        saver.save(sess, 'tmp/model/ckpt')
1
2
3
4
5
6
7
8
9
10
0000 mse: 0.310
1000 mse: 0.063
2000 mse: 0.026
3000 mse: 0.017
4000 mse: 0.010
5000 mse: 0.003
6000 mse: 0.003
7000 mse: 0.002
8000 mse: 0.002
9000 mse: 0.002

batch_size를 지정해준 만큼 랜덤으로 데이터를 뽑는다. indices에는 랜덤으로 데이터의 인덱스를 뽑고, input_batchoutput_batch에는 랜덤으로 뽑힌 데이터와 정답 레이블이 각각 들어간다. opt는 최적화를 하는 연산이므로 출력값이 없다. feed_dict로 입력과 정답값을 넣어주고 opt를 호출하면 모델이 학습된다. training_step이 1000일때마다 loss를 출력하고 모델을 저장한다.

1
2
3
4
5
6
7
8
9
10
11
sess = tf_reset()

input_ph, output_ph, output_pred = create_model()

saver = tf.train.Saver()
saver.restore(sess,"tmp/model.ckpt")

output_pred_run = sess.run(output_pred, feed_dict={input_ph: inputs})

plt.scatter(inputs[:,0], outputs[:,0], c='k', marker='o', s=0.1)
plt.scatter(inputs[:,0], output_pred_run[:,0], c='r', marker='o', s=0.1)

CS294-03-05

저장된 모델을 불러와서 결과값을 시각화 하는 코드다. 정답 데이터는 검정색으로 뿌려지고 모델 출력값은 빨간색으로 뿌려진다.

Tips and tricks

1
2
3
4
5
a = tf.constant(np.random.random(4,1))
b = tf.constant(np.random.random(1,4))
c = a * b

assert c.get_shape() ==(4,4)

위에서 다루었던 것과 같은 의도치않은 브로드캐스팅을 체크하기 위해 이러한 코드를 사용할 수 있다.

1
2
3
4
5
6
sess = tf_reset()
a = tf.get_variable('var1', shape=[4,6])
b = tf.get_variable('var2', shape=[2,7])

for var in tf.global_variables():
    print(var.name)
1
2
var1:0
var2:0

tf.global_variables를 이용하여 variable들을 순회할 수 있다.

1
help(tf.reduce_mean)

TensorFlow API에 대한 설명은 help()로도 조회할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
sess = tf_reset()

with tf.variable_scope('layer_0'):
    W0 = tf.get_variable(name='W0', shape=[1,20],initializer=tf.contrib.layers.xavier_initializer())
    b0 = tf.get_variable(name='b0', shape=[20], initializer=tf.constant_initializer(tf.zeros[20]))

with tf.variable_scope('layer_1'):
    W1 = tf.get_variable(name='W1', shape=[20,20],initializer=tf.contrib.layers.xavier_initializer())
    b1 = tf.get_variable(name='b1', shape=[20], initializer=tf.constant_initializer(tf.zeros[20]))

with tf.variable_scope('layer_2'):
    W2 = tf.get_variable(name='W2', shape=[20,1],initializer=tf.contrib.layers.xavier_initializer())
    b2 = tf.get_variable(name='b2', shape=[1], initializer=tf.constant_initializer(tf.zeros[1]))

var_names = sorted([v.name for v in tf.global_variables()])
print('\n'.join(var_names))
1
2
3
4
5
6
layer_0/W0:0
layer_0/b0:0
layer_1/W1:0
layer_1/b1:0
layer_2/W2:0
layer_2/b2:0

variable들을 scope로 묶어서 관리할 수 있다.

1
2
3
4
5
6
7
8
9
10
gpu_device = 0
gpu_frac = 0.5

import os
os.environ["CUDA_VISIBLE_DEVICES"] = str(gpu_device)

gpu_options = tf.GPUOptions(per_process_gpu_memory_fraction=gpu_frac)
config = tf.ConfigProto(gpu_options=gpu_options)

tf_sess = tf.Session(graph=tf.Graph(), config=config)

GPU 인덱스와 사용 메모리 비율을 지정할 수 있다. 위 코드는 0번 GPU의 메모리 50%를 사용하는 설정의 코드다.