2017년 6월 4일 일요일

텐서플로우 기반 머신러닝 개념 및 사용 방법

이 글은 텐서플로우 학습 방법을 요약한다. 이 글은 텐서플로우(버전 1.1)를 이미 설치하였다고 가정하고 진행한다. 여기서 실행된 샘플은 아래 e-book 레퍼런스에서 제공된 것이다. 이 레퍼런스들은 구글 텐서플로우 설치때 함께 설치되는 예제들을 기반으로 작성되어, 테스트하기 편리하다.
텐서플로우 학습과정(텐서보드)

1. 학습 동작 개념
1. 연산
텐서플로우는 학습에 필요한 연산 과정과 데이터를 그래프에 추가한 후에, 병렬로 학습을 run 하는 방식으로 동작한다. 그래프 기반 프로그래밍 방식이다. 이 과정을 간략히 정리해 본다.

다음 그림은 텐서에서 사용하는 그래프 구조를 간략화한 것이다. 그래프는 연산자(op)와 데이터로 구성된다. 그림에서 예로 주어진 수식은 연산자 +, -, *로 구성되며, 변수값은 말단 노드에서 정의되어 있다.
그래프 기반 프로그래밍 방식

텐서는 배열 차원을 가지는 랭크(rank)를 가진다. 0차원은 스칼라, 1차원은 벡터, 2차원은 행렬이다. 다음은 이와 관련된 함수이다.

tf.shape            텐서 구조 확인
tf.size               텐서 크기 확인
tf.rank              텐서 랭크 확인
tf.expand_dims   텐서 차원을 추가
tf.reverse           텐서 차원 역전
tf.transpose        텐서 차원 전치

예를 들어, 텐서 2000 x 2 배열을 3차원 배열로 확장하고 싶다면, 다음과 같이 처리한다.

vectors = tf.constaint(points)
expanded_vectors = tf.expand_dims(vectros, 0)

텐서(행렬) 간 연산이 기본이므로, 차원을 추가 및 삭제해 동일한 차원을 만든 후 계산하고, 계산된 결과를 축소된 차원으로 표현하는 등의 연산이 많다.

상수 및 텐서 관련 함수는 다음과 같다.

tf.zeros_like       모든 원소를 0으로 초기화
tf.ones_like        모든 원소를 1로 초기화
tf.constant         주어진 값으로 상수 텐서 생성

tf.random_normal   정규분포 난수로 텐서 생성
tf.random_uniform  균등분포 난수로 텐서 생성

2. 변수
변수를 사용하려면, 데이터 그래프 구성 후 run() 함수 실행 전에 tf.global_variables_initializer()로 초기화해야 한다. 변수는 모델을 훈련하는 중에 tf.train.Saver() 클래스를 이용해 디스크에 저장할 수 있다.

실행 중 데이터 변경을 위해서는 심벌릭 변수인 플레이스홀더(placeholder)를 사용한다. 다음은 관련 예이다.

import tensorflow as tf

a = tf.placeholder("float")
b = tf.placeholder("float")

y = tf.mul(a, b)

sess = tf.Session()
print sess.run(y, feed_dic={a: 3, b: 3})

텐서 플로우 변수는 생성할 때 다음과 같이 상수값이나 램덤값으로 초기화할 수 있다.
# Create two variables.
weights = tf.Variable(tf.random_normal([784, 200], stddev=0.35),
                      name="weights")
biases = tf.Variable(tf.zeros([200]), name="biases")
tf.Variables()를 호출할 때, 몇몇 연산자들(ops)를 텐서 데이터 그래프에 추가한다. 위 코드의 경우, weight는 random_normal 연산자가 정의되었고, biases 는 zeros 상수 연산자가 정의되었다. 이는 그래프에 추가만 된 상태이고, 실제 변수값이 옵션에 따라 초기화된 상태는 아니다. 그러므로, tf.global_variables_initializer() 리턴된 initializer로 초기화해줘야 한다.

텐서플로우는 철저히 그래프 기반 연산으로 처리된다. 그러므로, initializer와 같은 연산자는 그래프 형태의 데이터를 참조할 수 있으며, 연산자를 실행할 수 있는 별도의 Session.run() 함수로 실행된다.

텐서플로우는 실행 상태에서 데이터를 전달하는 3가지 방법이 있다.

  • Feeding: 각 단계에서 실행될 때 데이터를 전달한다.
  • Reading from files: 입력 파이프라인(input pipeline)이 텐서플로우 그래프의 시작에 파일로부터 데이터를 읽는다. 
  • Preloaded data: 텐서플로우 그래프의상수와 변수가 모든 데이터를 보관한다.

텐서플로우의 feed 메커니즘은 개발자가 데이터를 계산되고 있는 그래프의 특정 텐서에 데이터를 주입하는 것을 가능하게 해 준다. feed data 는 feed_dict를 통해 전달해 주며, run(), eval() 함수를 사용해 처리한다.
with tf.Session():
  input = tf.placeholder(tf.float32)
  classifier = ...
  print(classifier.eval(feed_dict={input: my_python_preprocessing_fn()}))
feed data를 이용해 어떤 텐서값을 교환하고 싶을 때는 tf.placeholder 그래프 노드를 사용하라. placeholder는 초기에 데이터를 포함하지 않는다. placeholder는 feed 가 없이 실행되면 에러를 발생한다.

Reading from files.


3. 병렬처리
텐서플로우는 병렬처리를 위해, 다중 CPU, GPU를 지원한다. 텐서플로우에서 다음처럼 사용한다.

"/cpu:0": 첫번째 CPU지정
"/gpu:0": 첫번째 GPU지정
"/gpu:1": 두번째 GPU지정

세선 셍성 때, log_device_placement 를 True로 하면, 연산 및 텐서가 어떤 디바이스에 할당되었는 지 알 수 있다. 
sess = tf.Session(config=tf.ConfigProto(log_device_placement=True))

서로 다른 GPU에서 연산을 실행하려면 다음과 같이 한다.

import tensorflow as tf

with tf.device('/gpu:2'):
   a = tf.constant([1, 2, 3, 4, 5, 6], shape=[2, 3], name='a')
   b = tf.constant([1, 2, 3, 4, 5, 6], shape=[3, 2], name='b')
   c = tf.matmul(a, b)

sess = tf.Session(config=tf.ConfigProto(log_device_placement=True))
print sess.run()

다음은 병렬처리 이전, 이후 시간을 측정한다. 

import numpy as np
import tensorflow as tf
import datetime

A = np.random.rand(1e4, 1e4).astype('float32')
B = np.random.rand(1e4, 1e4).astype('float32')

n = 10

c1 = []
c2 = []

defmatpow(M, n):
    if n < 1: #Abstract cases where n < 1
       return M
    else:
       return tf.matmul(M, matpow(M, n-1))

with tf.device('/gpu:0'):
    a = tf.constant(A)
    b = tf.constant(B)
    c1.append(matpow(a, n))
    c1.append(matpow(b, n))

with tf.device('/cpu:0'):
sum = tf.add_n(c1)

t1_1 = datetime.datetime.now()

with tf.Session(config=tf.ConfigProto(log_device_placement=True)) as sess:
sess.run(sum)
t2_1 = datetime.datetime.now()

with tf.device('/gpu:0'):
    #compute A^n and store result in c2
    a = tf.constant(A)
    c2.append(matpow(a, n))
with tf.device('/gpu:1'):
    #compute B^n and store result in c2
    b = tf.constant(B)
    c2.append(matpow(b, n))

with tf.device('/cpu:0'):
    sum = tf.add_n(c2) #Addition of all elements in c2, i.e. A^n + B^n
    t1_2 = datetime.datetime.now()

with tf.Session(config=tf.ConfigProto(log_device_placement=True)) as sess:
    # Runs the op.
    sess.run(sum)
t2_2 = datetime.datetime.now()

print "Single GPU computation time: " + str(t2_1-t1_1)
print "Multi GPU computation time: " + str(t2_2-t1_2)


2. 선형회귀분석
텐서플로우로 선형회귀를 이용한 학습모델을 만들어 본다.

# 테스트할 샘플 데이터를 생성함
import numpy as np

num_points = 1000
vectors_set = []

for i in range(num_points):
   x1 = np.random.normal(0, 0.55)
   y1 = x1 * 0.1 + 0.3 + np.random.normal(0, 0.03)
   vectors_set.append([x1, y1])

x_data = [v[0] for v in vectors_set]
y_data = [v[1] for v in vectors_set]

# 그래픽을 표시함
import matplotlib.pyplot as plt

plt.plot(x_data, y_data, 'ro')
plt.show()

# 텐서 플로우 학습 모델 정의
import tensorflow as tf

W = tf.Variable(tf.random_uniform([1], -1.0, 1.0))
b = tf.Variable(tf.zeros([1]))
y = W * x_data + b

loss = tf.reduce_mean(tf.square(y - y_data))          # 대상값과 오차를 최소로하는 loss

optimizer = tf.train.GradientDescentOptimizer(0.5)  # 경사하강법. 학습속도를 0.5로 함
train = optimizer.minimize(loss)

init = tf.global_variables_initializer()
sess = tf.Session()
sess.run(init)

# 학습
for step in range(8):
   sess.run(train)
   print(step, sess.run(loss))
   # print(step, sess.run(W), sess.run(b))

# 학습 결과 확인
plt.plot(x_data, y_data, 'ro')
plt.plot(x_data, sess.run(W) * x_data + sess.run(b))
plt.xlabel('x')
plt.ylabel('y')
plt.show()

실행결과는 다음과 같다. 학습에 따라 loss가 줄어드는 것을 확인할 수 있다.
훈련 결과 해에 수렴하는 모습

3. 군집화(clustering)
앞의 선형회귀분석은 입력 데이터와 출력값(레이블. label)을 학습모델에 제시해 주었다는 점에서 지도 학습(supervised learning)이다. 하지만, 모든 데이터에 레이블이 있는 것은 아니다. 이런 경우, 군집화라는 자율 학습(unsupervised learning)을 사용할 수 있다.
여기서는 K-평균 군집화 방법을 소개한다.

K-군집화는 다음 순서로 진행된다.
1. 초기: K개 중심 초기 집합 결정
2. 할당: 각 데이터를 가장 가까운 군집 할당
3. 업데이트: 각 그룹의 새로운 중심점 계산

코드는 다음과 같다.
# 2000개의 점을 랜덤하게 생성
import numpy as np
num_points = 2000
vectors_set = []
for i in range(num_points):
   if np.random.random() > 0.5:
      vectors_set.append([np.random.normal(0, 0.9), np.random.normal(0, 0.9)])
   else:
      vectors_set.append([np.random.normal(3, 0.5), np.random.normal(1, 0.5)])
# 그림 출력
import matplotlib.pyplot as plt
import pandas as pd
import seaborn as sns
df = pd.DataFrame({"x": [v[0] for v in vectors_set], "y": [v[1] for v in vectors_set]})
sns.lmplot("x", "y", data=df, fit_reg=False, size=6)
plt.show()
# 군집화
import tensorflow as tf
vectors = tf.constant(vectors_set)
k = 4
centroids = tf.Variable(tf.slice(tf.random_shuffle(vectors), [0, 0], [k, -1]))  # k개 중심선택
expanded_vectors = tf.expand_dims(vectors, 0)       # vectors 차원 일치
expanded_centroids = tf.expand_dims(centroids, 1)  # centroids 차원 일치
diff = tf.subtract(expanded_vectors, expanded_centroids)   # 유클리드 거리 계산
sqr = tf.square(diff)                        # square(Vx - Cx), square(Vy - Cy). 행렬(4 x 2000 x 2)
distances = tf.reduce_sum(sqr, 2)       # 두번째 지정된 차원 축소. 거리 행렬(4 x 2000)
assignments = tf.argmin(distances, 0) # D0값 인덱스. 예) array([2, 1, 1, ..., 3, 0, 2], int64)

이 결과로 다음과 같이, sqr, distances, assignments 값이 계산된다.

argmin은 두번째 인수로 지정된 차원에서 가장 작은 값의 인덱스를 리턴한다.
argmin함수 계산 결과, 2000개 각 좌표값이 어느 K군집 중심값과 가장 가까운지 인덱스가 assginments에 저장된다. 아래의 경우, 첫번째 좌표값은 2번 군집 중심점과 제일 가깝다.

다음 코드를 실행한다.
means = tf.concat([tf.reduce_mean(tf.gather(vectors, tf.reshape(tf.where(tf.equal(assignments, c)), [1, -1])), reduction_indices = [1]) for c in range(k)], 0)   # c군집에 속한 모든 점 평균값을 가진 텐서 생성
update_centroids = tf.assign(centroids, means)  # means를 centroids에 할당
means의 결과는 다음과 같이 c군집의 거리 평균값이 된다.

코드 설명은 다음과 같다.
1. equal() 을 이용해, 한 군집과 일치하는 assignments 각 원소 위치를 True로 표시하는 불리언 텐서(Dim 2000)을 만든다.

2. where()을 이용해, True인 원소의 인덱스 행렬(? x 1)을 리턴한다. ?는 c군집의 갯수에 따라 다르다. c군집에 대해 equal() 결과 얻어진 True 값 원소 갯수라고 생각하면 된다.

3. reshape()을 이용해, vectors의 shape과 동일한 모양인, 1 x ? 행렬을 만든다.

이제, vectors에서 해당 인덱스의 좌표값들만 gather()을 이용해 얻으면 된다. 참고로, vectors의 shape은 다음과 같다.

4. gather()을 이용해, 좌표를 모든 텐서 1 x ? x 2 를 만든다.

5. reduce_mean() 을 이용해, c 군집의 모든 점 평균값을 가진 텐서 1 x 2를 만든다.

6. concat()을 이용해, K개 군집 평균값을 가진 4 x 2 텐서를 만든다.


다음 코드를 실행해, 앞에서 생성한 그래프를 실행한다.
init_op = tf.global_variables_initializer()   # 모든 변수 초기화
sess = tf.Session()
sess.run(init_op)
for step in range(100):
   _, centroid_values, assignment_values = sess.run([update_centroids, centroids, assignments])   # 탐색해야 할 세개 값들에 대한 훈련 실행
   print(centroid_values)
# assignment_values 텐서 결과 확인
data = {"x": [], "y": [], "cluster": []}
for i in range(len(assignment_values)):
   data["x"].append(vectors_set[i][0])
   data["y"].append(vectors_set[i][1])
   data["cluster"].append(assignment_values[i])
df = pd.DataFrame(data)
sns.lmplot("x", "y", data=df, fit_reg=False, size=6, hue="cluster", legend=False)
plt.show()
요약해 보면, 정의된 그래프를 100번 실행하면서, random_shuffle()로 k군집을 뒷섞어, argmin()을 이용해 k군집 중심과 최소 거리를 가진 점들을 클러스터링한다. 결과는 다음과 같다.

4. 신경망 학습
앞 포스팅에서 작업해 보았던 MNIST 손글씨 이미지를 이용해 신경망을 만든 후, 학습을 해 본다. 이 예제는 텐서플로우 기본 예제에 포함되어 있다.
여기서 사용하는 신경망 학습에는 오차 측정을 교차 엔트로피 에러(cross entropy error), 학습은 경사 하강법 알고리즘을 사용해 교차 엔트로피를 최소화하는 역전파 알고리즘(GradientDescentOptimizer 함수), 출력값은 softmax 함수를 사용한다.

상세한 내용은 아래 글을 참고한다.
아래 코드를 입력해 본다.

from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets("MNIST_data/", one_hot=True)

import tensorflow as tf
x = tf.placeholder(tf.float32, [None, 784])
W = tf.Variable(tf.zeros([784, 10]))
b = tf.Variable(tf.zeros([10]))

y = tf.nn.softmax(tf.matmul(x, W) + b)         # softmax 함수
y_ = tf.placeholder(tf.float32, [None, 10])    # 교차 엔트로피 변수

cross_entropy = tf.reduce_mean(-tf.reduce_sum(y_ * tf.log(y), reduction_indices=[1]))  # 교차 엔트로피 함수
train_step = tf.train.GradientDescentOptimizer(0.5).minimize(cross_entropy)  # 경사 하강법(미분)

sess = tf.InteractiveSession()
tf.global_variables_initializer().run()

for _ in range(1000):
  batch_xs, batch_ys = mnist.train.next_batch(100)
  sess.run(train_step, feed_dict={x: batch_xs, y_: batch_ys})

correct_prediction = tf.equal(tf.argmax(y,1), tf.argmax(y_,1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))  #

print(sess.run(accuracy, feed_dict={x: mnist.test.images, y_: mnist.test.labels}))

이 코드의 실행결과는 다음과 같다. 대략 92% 정확도를 가진 훈련모델을 얻을 수 있다.


5. CNN 다층 신경망 학습
이 챕터는 CNN(Convolutional Neural Networks. ConvNet. 컨볼루션 뉴럴 네트워크) 다층 신경망을 이용해, MNIST 필기체 이미지를 학습해 본다. 이 역시, 텐서플로우 기본 예제에 포함되어 있다. 자세한 내용은 아래 글을 참고한다.
아래 코드를 입력하고 실행한다.

from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets("MNIST_data/", one_hot=True)

import tensorflow as tf
x = tf.placeholder(tf.float32, [None, 784])
W = tf.Variable(tf.zeros([784, 10]))
b = tf.Variable(tf.zeros([10]))
y = tf.nn.softmax(tf.matmul(x, W) + b)
y_ = tf.placeholder(tf.float32, [None, 10])

def weight_variable(shape):   # 가중치 초기화
  initial = tf.truncated_normal(shape, stddev=0.1)
  return tf.Variable(initial)

def bias_variable(shape):       # b값 초기화
  initial = tf.constant(0.1, shape=shape)
  return tf.Variable(initial)

def conv2d(x, W):
  return tf.nn.conv2d(x, W, strides=[1, 1, 1, 1], padding='SAME')   # stride만큼 픽셀 이동하며, conv 적용. 이미지 테두리 크기 패딩(padding) 지정

def max_pool_2x2(x):
  return tf.nn.max_pool(x, ksize=[1, 2, 2, 1],
                        strides=[1, 2, 2, 1], padding='SAME')  # 

W_conv1 = weight_variable([5, 5, 1, 32])   # 특징맵 생성을 위한 커널(kernel. 필터 filter) 정의
b_conv1 = bias_variable([32])                    # b값 정의. 출력벡터 크기는 32

x_image = tf.reshape(x, [-1,28,28,1])

h_conv1 = tf.nn.relu(conv2d(x_image, W_conv1) + b_conv1)    # 첫번째 conv 적용
h_pool1 = max_pool_2x2(h_conv1)          # 정보 일반화(압축)를 위한 max pooling 적용 후 14x14크기

W_conv2 = weight_variable([5, 5, 32, 64])     # 이전 출력벡터 크기 32를 채널 수로 넘김
b_conv2 = bias_variable([64])                        # b값 정의. 출력벡터 크기는 64

h_conv2 = tf.nn.relu(conv2d(h_pool1, W_conv2) + b_conv2)    # 두번째 conv 적용
h_pool2 = max_pool_2x2(h_conv2)          # 정보 일반화(압축)를 위한 max pooling 적용후 7x7크기

W_fc1 = weight_variable([7 * 7 * 64, 1024])    # 7x7x64(이전 출력벡터 크기). 출력벡터 크기 1024
b_fc1 = bias_variable([1024])

h_pool2_flat = tf.reshape(h_pool2, [-1, 7*7*64])   # softmax 함수 전달을 위한 행렬 직렬화 처리
h_fc1 = tf.nn.relu(tf.matmul(h_pool2_flat, W_fc1) + b_fc1)

keep_prob = tf.placeholder(tf.float32)
h_fc1_drop = tf.nn.dropout(h_fc1, keep_prob)    # 오버 피팅 방지를 위한 드롭아웃 처리

W_fc2 = weight_variable([1024, 10])
b_fc2 = bias_variable([10])

y_conv = tf.matmul(h_fc1_drop, W_fc2) + b_fc2

cross_entropy = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(labels=y_, logits=y_conv)) # softmax 함수 처리. 엔트로피 변수 y_ 및 y_conv 출력값을 입력함.

train_step = tf.train.AdamOptimizer(1e-4).minimize(cross_entropy)  # 경사하강법 대신 ADAM최적화 알고리즘 적용
correct_prediction = tf.equal(tf.argmax(y_conv,1), tf.argmax(y_,1))  

accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
sess.run(tf.global_variables_initializer())
for i in range(20000):
  batch = mnist.train.next_batch(50)     # 배치 처리
  if i%100 == 0:
    train_accuracy = accuracy.eval(feed_dict={x:batch[0], y_: batch[1], keep_prob: 1.0})
    print("step %d, training accuracy %g"%(i, train_accuracy))
  train_step.run(feed_dict={x: batch[0], y_: batch[1], keep_prob: 0.5})

print("test accuracy %g"%accuracy.eval(feed_dict={
    x: mnist.test.images, y_: mnist.test.labels, keep_prob: 1.0}))

실행 결과는 다음과 같다. 98% 이상의 정확도를 얻을 수 있다.

6. 순환 신경망 학습
RNN(Recurrent Neural Network. 순환신경망)을 이용한 단어 예측 문제를 구현해 본다. 순환신경망은 뉴럴의 출력정보가 다시 재사용되어, 앞으로 입력된다. 이런 특성으로 인해, 일부 누락된 패턴의 향후 변화를 예측하는 것에 자주 사용된다. 예를 들어, 학습된 모델을 바탕으로 일부 누락된 문자들에 대한 자동 글 완성 기능, 기사 쓰기, 시계열 예측, 챗봇, 음악 작곡, 주가 예측 등에 적용되고 있다.

다음 그림과 같이, 어떤 시간 t의 뉴런에는 그 이전 시간 (t - 1)에 생성된 뉴런 상태가 입력되어 새로운 상태가 만들어지고, 그 데이터는 이후 시간 (t + 1)에 입력된다. 순환 신경망에서 이런 뉴런을 메모리 셀(memory cell. cell)이라 한다. 셀에서 만들어지는 상태 데이터는 은닉 상태(hidden state)라 한다.
순환 신경망 뉴런 구조(Understanding LSTM)
기본적인 순환 신경망 은닉 상태 계산 방법은 이전 은닉 상태와 현재 입력값을 어떤 가중치 W로 곱하고 편향값 b를 더하는 것이다. 활성화 함수는 보통 hyperbolic tangent 함수를 사용한다.
다만, 기본적인 순환 신경망은 짧은 거리에 있는 문맥 정보만 보관하므로, 먼 거리 데이터 간 연관관계는 파악하기 힘들다. 즉, 단기기억(short term memory)를 저장한다. 
RNN 계산 원리

다음은 RNN을 이용해 주가를 예측하는 파이토치 기반 코드이다. 이 코드는 넥플릭스의 2019년 주가 데이터를 기반으로 학습한다. 시계열 데이터 입력 shape은 30 x 3 개(개장가, 최고가, 최저가), 라벨로 예측되는 값은 주가 종가 1개이다. 배치크기는 32개 이며, 전체 967개 데이터이다. 데이터 파일은 케글(Netflix Stock Price)에서 다운로드 한다.
import numpy as np
from torch.utils.data.dataset import Dataset

class Netflix(Dataset): 
    def __init__(self):
        self.csv = pd.read_csv("NFLX.csv")       

        # normalize data
        self.data = self.csv.iloc[:, 1:4].values   
        self.data = self.data / np.max(self.data)

        self.label = data["Close"].values         
        self.label = self.label / np.max(self.label)
    def __len__(self):
        return len(self.data) - 30 # return batch size

    def __getitem__(self, i):
        data = self.data[i:i+30] # data
        label = self.label[i+30] # label

        return data, label
    
import torch
import torch.nn as nn

class RNN(nn.Module):
   def __init__(self):
       super(RNN, self).__init__()

       self.rnn = nn.RNN(input_size=3, hidden_size=8, num_layers=5,
                         batch_first=True) # input data size = 3, hidden size = 8, layer = 5
       
       self.fc1 = nn.Linear(in_features=240, out_features=64)
       self.fc2 = nn.Linear(in_features=64, out_features=1)
       self.relu = nn.ReLU() 
        
   def forward(self, x, h0):
       x, hn = self.rnn(x, h0)  # h0 = zero vector
       x = torch.reshape(x, (x.shape[0], -1))
       x = self.fc1(x)
       x = self.relu(x)
       x = self.fc2(x)
      
       x = torch.flatten(x)
       return x
   
import tqdm
from torch.optim.adam import Adam
from torch.utils.data.dataloader import DataLoader

device = "cuda" if torch.cuda.is_available() else "cpu"

model = RNN().to(device)  
dataset = Netflix() 
loader = DataLoader(dataset, batch_size=32)  
optim = Adam(params=model.parameters(), lr=0.0001) 
for epoch in range(200):
   iterator = tqdm.tqdm(loader)
   for data, label in iterator:
       optim.zero_grad()

       h0 = torch.zeros(5, data.shape[0], 8).to(device)
       pred = model(data.type(torch.FloatTensor).to(device), h0)

       loss = nn.MSELoss()(pred, label.type(torch.FloatTensor).to(device))
       loss.backward() 
       optim.step()  

       iterator.set_description(f"epoch{epoch} loss:{loss.item()}")

torch.save(model.state_dict(), "./rnn.pth")  

import matplotlib.pyplot as plt
loader = DataLoader(dataset, batch_size=1)  

preds = []  
total_loss = 0
with torch.no_grad():
   model.load_state_dict(torch.load("rnn.pth", map_location=device))

   for data, label in loader:
       h0 = torch.zeros(5, data.shape[0], 8).to(device) 

       pred = model(data.type(torch.FloatTensor).to(device), h0)
       preds.append(pred.item())  
       loss = nn.MSELoss()(pred,
                           label.type(torch.FloatTensor).to(device))  
       total_loss += loss/len(loader) 

total_loss.item()

plt.plot(preds, label="prediction")
plt.plot(dataset.label[30:], label="actual")
plt.legend()
plt.show()

코드를 실행해 보면, 결과는 다음과 같이 주가를 잘 예측하는 것을 확인할 수 있다.

LSTM은 RNN의 단점을 개선하기 위해 LSTM(long short-term memory) 순환 신경망 알고리즘이 개발되었다. LSTM 순환 신경망은 은닉 상태와 셀 상태 두 가지를 계산한다. 은닉상태(h)는 상위 계층 입력값으로 전달되고, 다음번 계산을 위해 전달되지만, 셀 상태(c)는 상위 계층으로 전달되지 않는다.
계산식은 이전 셀의 은식 상태(ht-1)와 현재 입력값 (xt)에 가중치를 곱한 선형 계산 결과를 p로 정의한다. LSTM 순환 신경망은 셀 상태 (ct)를 계산하기 위해, 삭제해야 할 정보 학습 시 사용하는 삭제 게이트(forget gate)와 새롭게 추가해야할 정보 학습을 도와주는 입력 게이트(input gate) 두가지를 이용한다.
LSTM은 기존 드롭아웃이 효과가 없다. 이런 이유로 아래 그림에서 신경망의 수직 방향에 대해서, 드롭아웃을 적용하는 방법이 개발되었다.
만약, 입력데이터 Xt가 드롭아웃되어 셀로 전달되지 못하였다고 하면, 그 정보는 ht와 Ct에도 포함되지 않는다. Xt데이터가 드롭아웃되지 않았더라도 D(ht)에서 드롭아웃되었다면, 그 셀의 은닉상태는 상위계층에 전달되지 못한다. 다만, 드롭아웃을 수평 방향으로 적용하지 않아, Ht와 Ct는 다음번 은닉상태와 셀상태 계산시 사용될 수 있다. 

LSTM에서 유명한 예제는 Penn Treebank 데이터셋을 사용해, 언어 모델을 학습하는 예이다. 이 데이터셋은 10000개의 단어로 이루어진 글이다. 이 글을 이용해, 어떤 단어의 열 뒤에 오는 단어의 패턴을 학습하고, 일부 단어들만 주어졌을 때, 뒤에 오는 단어를 예측하는 학습모델을 만들어 본다.

텐서플로우는 이를 구현해 놓은 코드를 예제로 제공한다. 상세한 텐서플로우 LSTM 코드 구현 방법은 다음을 참고한다.
학습에 필요한 실행 소스코드와 데이터셋을 다운한다.

$ wget http://www.fit.uvtbr.cz/~imikolov/rnnlm/simple-examples.tgz
$ tar xvf simple-examples.tgz

이 파일을 풀면, ptb_word_lm.py 학습 수행 파일이 있다. 예제 데이터 파일은 학습(train), 검증(valid), 테스트 성능 평가(test) 데이터로 구성되어 있다.

학습 수행 파일을 실행하면, 학습 후 혼잡도가 출력된다

7. 학습모델 배포 및 서비스
지금까지 텐서플로우를 이용해 학습 모델을 만들고, 실행하는 방법을 소개해 보았다. 이 장에서는 지금까지 만든 학습모델을 배포하기 위해, 저장(save), 복구(restore), 서비스를 개발하는 방법을 소개한다.

모델을 훈련할 때, 개발자는 variables 를 사용해 파라메터를 보관하거나 갱신한다. Variables(변수)는 텐서를 보관하는 메모리 버퍼이다. 훈련 후에는 변수를 디스크에 저장할 수 있다. 이후, 그 모델을 사용하거나 분석할 때 저장된 변수값을 복구할 수 있다.

훈련된 모델을 저장 혹은 복구(save and restore)하기 위해서는 tf.train.Saver 객체를 사용할 수 있다. Saver 객체의 생성자는 그래프의 모든 변수 노드에 save, restore 연산자를 추가한다.

Variables는 바이너리(binary) 포맷으로 저장된다. 저장은 변수 이름과 텐서 값을 가진 맵 형식이다. Saver 객체를 생성할 때, 체크포인트(checkpoint) 파일 이름을 지정할 수 있다. 다음은 훈련 모델 저장 예이다.
# Create some variables.
v1 = tf.Variable(..., name="v1")
v2 = tf.Variable(..., name="v2")
...
# Add an op to initialize the variables.
init_op = tf.global_variables_initializer()
# Add ops to save and restore all the variables.
saver = tf.train.Saver()
# Later, launch the model, initialize the variables, do some work, save the
# variables to disk.
with tf.Session() as sess:
  sess.run(init_op)
  # Do some work with the model.
  ..
  # Save the variables to disk.
  save_path = saver.save(sess, "/tmp/model.ckpt")
  print("Model saved in file: %s" % save_path)
훈련된 모델을 복구하는 코드는 다음과 같다.
# Create some variables.
v1 = tf.Variable(..., name="v1")
v2 = tf.Variable(..., name="v2")
...
# Add ops to save and restore all the variables.
saver = tf.train.Saver()
# Later, launch the model, use the saver to restore variables from disk, and
# do some work with the model.
with tf.Session() as sess:
  # Restore variables from disk.
  saver.restore(sess, "/tmp/model.ckpt")
  print("Model restored.")
  # Do some work with the model
  ...
만약. tf.train.Saver()에 파라메터를 전달하지 않는다면, saver는 tf가 관리하는 그래프의 모든 변수를 다루게 된다. 체크포인트에서 변수들에 대한 특별한 이름을 지정하는 것이 유용할 수 있다. tf에서 관리되는 특정 변수들만 저장할 수 있다. 예를 들어, 연구자가 복구할 변수명이 param이라 하더라도, weights라는 변수명인 훈련 모델을 가질 수 있다. 만약, 연구자가 5개 레이어 뉴럴 네트워을 훈련했을 때, 새롭게 한개 개층을 추가하여 6개 레이어를 훈련하고자 하면, 이전 훈련 모델을 새로운 모델에 추가하기도 쉬워진다. 다음은 이와 관련된 예이다.
# Create some variables.
v1 = tf.Variable(..., name="v1")
v2 = tf.Variable(..., name="v2")
...
# Add ops to save and restore only 'v2' using the name "my_v2"
saver = tf.train.Saver({"my_v2": v2})
# Use the saver object normally after that.
...
저장된 체크포인트 파일은 학습 모델 그래프를 저장한 .meta 파일, 학습 모델 데이터를 저장한 파일로 구성된다. 이 파일들을 이용하면, 모델을 학습할 때 걸리는 시간을 생략할 수 있다.

다음은 메타파일을 이용해, 학습 모델 그래프를 재구성하는 코드이다.
new_saver = tf.train.import_meta_graph("./minist_softmax.ckpt.meta")
new_saver.restore(sess, "./minist_softmax.ckpt")
학습된 모델 데이터는 saver.restore() 함수를 이용해 로딩할 수 있다.
    save_path = "./minist_softmax.ckpt"
    saver.restore(sess, save_path)
로딩된 학습 모델 데이터를 이용해, 필요한 서비스를 아이폰이나 안드로이드 플랫폼에서 개발하면 된다. 이와 관련된 예제는 여기를 참고한다.

8. 마무리
텐서플로우의 핵심적인 부분만 실행해보고, 간략히 정리해 보았다. 텐서플로우에 관련된 매우 많은 예제들이 있어, 동작을 이해한다면, 사용이 그리 어렵지는 않다. PyTorch 등 다른 머신러닝 프레임웍도 딥러닝과 같은 기본 개념은 비슷하므로, 텐서플로우를 먼저 연습하고, 다른 프레임웍을 사용하는 것이 그리 어렵지는 않을 것이다.

기타 레퍼런스
  1. Online T academy
  2. DL4J, Long Short-Term Memory Units (LSTM)
  3. The Unreasonable Effectiveness of Recurrent Neural Networks
  4. Understanding LSTM Networks
  5. RNN과 LSTM 이해
  6. Tensorflow Tutorials
  7. Tensorflow slim


2017년 6월 3일 토요일

오픈소스 기반 실시간 실내 위치 측정 시스템

오늘은 오픈소스 기반 실시간 실내 측위 시스템(RTLS. Real time Location System)을 간략히 소개해 보도록 하겠습니다. 앞으로 측위 정보는 IoT(Internet of Things. 사물인터넷) 데이터를 사용하는 서비스 등에 필수적인 요소가 될 것입니다.

1.  개요
RTLS의 가장 보편적 인 예는 GPS입니다. GPS는 실내에서 위성 신호를 수신할 수 없으므로, 작동하지 않습니다. 이 경우, GPS가 제공하는 모든 추적 및 위치 기능이 갑자기 사라집니다. 창고 자재 관리, 공장 자동화, 건축물 실내 위치 기반 서비스, 건설 터널 시공 관리 등 광범위한 산업 분야에서 정확한 위치 정보를 사용하면, 많은 경우 해당 비즈니스를 변화 시키거나 비용을 크게 개선 할 수 있습니다. 이런 이유로, 실내 측위 기술의 필요성이 높아지고 있습니다.

2. OpenRTLS
OpenRTLS는 말 그대로, 오픈소스 기반으로 개발된 실시간으로 현재 위치를 측정하는 시스템입니다. 누구나 소스를 변경할 수 있고, 라이센스 사용료도 무료입니다.ㅎ

OpenRTLS 클라이언트

지금까지 RTLS 솔루션의 문제점은 다음과 같았습니다. 

  • 위치 정밀도 
  • 전력 소비 가격 
  • 간섭 및 다중 경로 환경 
  • 의존 맞춤형 시스템 

OpenRTLS는 저가격, 고품질의 DW1000을 사용합니다. Fabless 반도체 업체인 Decawave의 DW1000은 IEEE 802.15.4 UWB(초광대역. Ultra-wideband) 표준을 기반으로하는 싱글칩 CMOS Ultra-Wideband IC를 제공하여, 이런 문제를 해결하고 있습니다.

DW1000-ORTLS 보드(OpenRTLS shop)

OpenRTLS는 다음과 같은 실내 측위 기준을 만족합니다.

  • 정확성 - OpenRTLS UWB 솔루션은 10 ~ 30 cm의 정확도를 제공합니다. EU commision research에 따르면 UWB 시스템 만이 실내에서 안정적인 위치 정확도를 제공합니다. 많은 비UWB 시스템은 고정된 로컬 조건과 기준점에 근접한 범위에서 동작합니다. 실험실에서 블루투스 저에너지 및 Wi-Fi는 sub meter 급 정확도를 제공합니다. 실용적인 응용 프로그램에서는 참조점이 수 미터 떨어져 있고, 사람과 물체의 양이 변하면 시스템 성능이 몇 미터의 정확도로 떨어집니다. UWB 기반 RTLS 시스템은 시뮬레이션 상황에서도 센티미터 단위정확도로만 저하됩니다. 
  • 범위 - 실내는 30 미터 및 옥외는 300 미터 (Line of Sight. LOS = 시선, 110 kbps) 측정을 지원합니다. 추적 할 수 있는 모바일 태그의 한계는 200평방미터 당 10,000 노드입니다. 
  • 하드웨어 및 설치 비용 - Decawave openRTLS의 경우, 건물 유형에 따른 설치를 포함해 평방미터 당 2 ~ 6 US 달러 수준입니다. 정말 싸죠.
  • 전력 소비 - UWB 태그의 전력 소모는 최저 한 번의 충전으로 9년 이상 지속될 수 있습니다. Idle / sleep 시 전류는 65 nA이며, 이는 8년의 수명을 제공합니다. 일반적인 600 mAh 배터리를 사용하여 10 초당 1번의 위치 업데이트 시 태그는 5 년 간 동작될 수 있습니다.

OpenRTLS는 다양한 곳에 활용되고 있습니다. 병원, 건설, 제조, 접근 통제, 창고 관리 등 다양한곳에 사용되고 있습니다.

UWB,  WiFi case study for a large hospital

Warehouses and DC's

3. PolyPoint
PolyPoint는 오픈소스 기반 실내 측위 시스템입니다. 미시간대학의 Embedded Systems Research인 Lab11에서 개발하였습니다. 
이 시스템은 UWB(초광대역. Ultra-wideband) RF time-of-flight를 사용합니다. DecaWave DW1000을 UWB로 사용하여, 패킷 전송 및 수신 시간의 정확한 타임 스탬프를 제공합니다. DW1000칩은 가격이 매우 저렴한 Indoor Precision Location & Communication 칩입니다. 

PolyPoint (Accurate RF indoor localization, Lab11)
이 칩은 UWB호환, 무선 통신 기능을 내장하고, 기본적으로 10cm 실내 측위 정밀도를 지원합니다. 아두이노 등 라이브러리를 제공하고 있어, 개발에 편리합니다. 

PolyPoint는 소스를 GitHub에서 제공하고 있습니다(미시간대 Lab11에서 수행하는 대부분의 프로젝트는 오픈소스로 제공되고 있다.존경스러움ㅎ). 


다음 명령으로 PolyPoint 소스를 모두 다운로드 받을 수 있습니다.
git clone --recursive https://github.com/lab11/polypoint.git
PolyPoint 시스템은 여러 가지 하드웨어로 구성됩니다. 핵심 하드웨어 및 소프트웨어를 모두 포함하는 측면 삼각형에 TriPoint 모듈이 있습니다. 보드는 UWB 안테나와 블루투스 저에너지 라디오 및 배터리 충전 회로를 포함합니다. PolyPoint는 DW1000 칩보다 정밀한 측위정보를 얻을 수 있습니다. PolyPoint는 모바일폰 애플리케이션에 거리 측위정보를 제공 할 수 있습니다.

Lab11은 PolyPoint외에도 Harmonia와 같은 다양한 프로젝트가 오픈소스로 제공되고 있습니다. 
Lab11의 Harmonia 프로젝트 (GitHub)
4. 마무리
요즘에는 오픈소스를 통한 공헌과 공유문화가 활성화된 곳이 기술 선진국이라는 생각이 드네요. 엔지니어링 분야도 오픈소스 문화가 거셉니다. 저는 오픈소스라는 것이 아이디어를 보호하는 문화속에 한곳에 집중할 수 있는 여유와 잉여시간이 보장된 곳에서만 꽃 필수 있다고 생각합니다. ㅎ가져다 사용하기만 하는 우리나라는 언제 오픈소스 선진국이 될까요. 


레퍼런스
  1. Arduino DW1000 Library
  2. OpenRTLS

2017년 5월 24일 수요일

Docker 기반 우분투, 텐서플로우, PyTorch 설치, 개발 및 관련 명령 정리

노트북에서 윈도우와 우분투 듀얼 부팅을 사용하다 보면, 오피스 작업하다가 재부팅해야 하는 등 불편할 때가 많다. 이럴 경우, 윈도우에서 도커를 설치하고, 그 위에 우분투 이미지를 설치해 사용하는 방법이 좋은 대안이 될 수 있다.

이 글은 도커에 대해 간략히 살펴보고, 도커 컨테이너에 우분투, 텐서플로우, PyTorch(파이 토치)를 설치해 본다.

이 내용은 다음 링크를 참고하였다.
1. 개념
도커는 2013년 3월 Pycon 컨퍼런스(산타클라라)에서 dotCloud 창업자인 Solomon Hykes가 리눅스 컨테이너의 미래라는 세션을 발표하면서 처음 알려졌다. 이후 도커는 급성장해 2015년 4월 1,100억원 투자유치를 받았으며, 마이크로소프트가 2016년 6월 4 billion dollar에 도커를 인수하려 했다.

도커는 컨테이너 기반 오픈소스 가상화 플랫폼이다. 실행 중인 컨테이너에 접속해 명령을 실행하고, apt-get 으로 패키지를 설치할 수 있다. 여러 프로세스를 백그라운드로 실행할 수 있다.

이미지는 컨테이너 실행에 필요한 파일과 설정값을 포함하고 있다. 컨테이너는 이미지를 실행한 상태이다. 예를 들어, 우분투 이미지는 우분투 실행을 위한 모든 파일을 가지고 있다. 

도커는 레이어 구조로 설치 패키지를 관리하기 때문에, 이미 빌드된 레이어를 재활용해, 도커 빌드 속도를 크게 개선할 수 있다. 아울러, 빌드된 레이어를 재활용해, 빌드될 이미지를 조합할 수 있다.
도커 레이어 구조

도커 이미지는 Docker hub에 등록되거나 Docker Registry 저장소를 만들어 관리할 수 있다. 현재 공개된 도커 이미지는 50만개 이상이며, 도커 기반 오픈소스 프로젝트는 10만개가 넘는다.

이미지는 url 방식으로 관리하고, 태그를 붙일 수 있다. 도커는 이미지를 만들기 위해 Dockerfile 이란 DSL(domain specific language)를 이용해 이미지 생성과정을 기술한다.

도커 명령어는 직관적이며, http기반의 REST API도 지원해 확장성이 좋다.

도커는 가상머신에 비해 사용이 쉽고 성능이 좋으나 한계가 있다. 운영체제의 라이브러리 및 시스템을 재활용하므로, 이에 의존되어 동작된다. 만약, 운영체제의 드라이버 설정에 GPU가 설치안되어 있다면, 도커 이미지도 이와 관련된 라이브러리를 사용하지 못할 것이다. 이는 성능은 느리나 운영체제 독립적인 가상머신과는 크게 다른 점이다.

2. 도커 설치
도커를 설치하는 방법은 간단하다. 아래 링크를 클릭해 적당한 버전을 설치하면 된다. 맥, 윈도우, 리눅스 버전 등이 제공된다. 
만약, 윈도우 10 버전에서 하이퍼-V(Hyper-V) 가상머신을 사용해 도커를 실행하려면, 윈도우 10 프로페셔널 이상 운영체제가 설치되어야 함에 주의한다. 하위 버전에서는 오라클 VirtualBox가 설치된다(성능이 하이퍼-V보다 낮다고 알려져 있다). 
설치 후에 Docker Quickstart Terminal, Kitematic, Oracle VM VirtualBox(혹은 Hyper-V)가 설치된 것을 확인할 수 있다.

참고 - 2019년 4월 도커 버전과 Kitematic 버전이 서로 문제를 일으켜서, 도커 이미지 다운로드 안되는 현상있음. 이 경우, https://github.com/docker/kitematic/releases 에서 Kitematic 0.17.3 버전을 다운로드 받아 실행해 볼것.

3. 실행
도커 버전을 확인하기 위해, Docker Quickstart Terminal을 실행해 다음과 같은 명령을 내려 본다. 
$ docker version
도커는 서버와 클라이언트로 분리되어 있고, 명령을 입력하면, 클라이언트가 서버로 명령을 전송하고 결과를 받아 터미널에 출력해 준다. 
도커 클라이언트 서버 구조
Kitematic 를 실행해 보면, 도커 가상머신에서 올릴 수 있는 수많은 이미지를 검색할 수 있다(2019년 4월 현재 도커 최신 버전과 Kitematic 버전이 서로 문제를 일으켜서, 다운로드가 안됨. 이 경우, https://github.com/docker/kitematic/releases 에서 Kitematic 0.17.3 버전을 다운로드 받아 실행해 볼것). 검색한 이미지는 도커 위에서 생성해 사용할 수 있다. 

4. 우분투 이미지 생성 및 실행하기 
우분투 이미지를 올려보자. 터미널을 실행하고, 아래 명령을 실행해 본다. 
$ docker run ubuntu:16.04

만약, 이미지가 로컬에 없다면, 다운로드(pull 명령) 후 컨테이너를 생성(create 명령)하고, 시작(start)한다. 우분투 이미지에 명령은 전달하지 않았기 때문에 이미지는 시작하자 마자 종료된다. 

이제, /bin/bash 명령어를 이용해, 컨테이너를 실행한다.
$ docker run --rm -it ubuntu:16.04 /bin/bash


도커의 각 명령은 docker 명령해 뒤에 아래 명령을 추가하면 실행된다. 각 명령의 의미는 다음과 같다. 
옵션설명
-ddetached mode 흔히 말하는 백그라운드 모드
-p호스트와 컨테이너의 포트를 연결 (포워딩)
-v호스트와 컨테이너의 디렉토리를 연결 (마운트)
-e컨테이너 내에서 사용할 환경변수 설정
–name컨테이너 이름 설정
–rm프로세스 종료시 컨테이너 자동 제거
-it-i와 -t를 동시에 사용한 것으로 터미널 입력을 위한 옵션
–link컨테이너 연결 [컨테이너명:별칭]
exit 를 실행해 bash 쉘을 종료하면, 우분투 이미지 컨테이너도 자동 종료된다. 

5. 텐서플로우 이미지 실행
이제 구글의 머신러닝 플랫폼인 텐서플로우 이미지를 실행해 보자. 다음 명령어를 도커 입력하면, 이미지를 다운로드 받은 후 실행된다. 
$ docker run -d -p 8888:8888 -p 6006:6006 teamlab/pydata-tensorflow:0.1


이미지가 실행된 후, 웹브라우저에서 jupyter를 접속해, 파이썬 코드를 입력해 본다. 제대로 실행되면, 성공한 것이다.

6. PyTorch 이미지 실행
PyTorch는 텐서플로우와 비슷한 머신러닝 플랫폼이다.

다음 명령을 입력해, PyTorch 이미지를 컨테이너에서 실행해 본다. 참고로 이미지는 DockerHub의 StepanKuzmin의 pytorch-notebook을 사용한다.
$ docker run -it --rm -p 8888:8888 stepankuzmin/pytorch-notebook

7. 간단한 도커 우분투, Ngnix 서버 이미지 만들기
이 장은 도커 이미지 개념을 이해하기 쉽도록, 간단하게 도커 우분투, node.js 서버 이미지 만드는 방법을 설명한다. 도커가 설치되었다는 가정하에 다음 명령을 입력한다(우분투 기준). 참고로, NVIDIA GPU 도커 이미지는 현재 우분투에서 밖에 지원되지 않는다.
mkdir -p nginx-image
cd nginx-image/
touch Dockerfile
gedit Dockerfile

다음과 같이 우분투 20.04 버전을 기반으로 NGNIX 서버를 설치하고, 환경을 설정하는 명령을 Dockerfile에 입력한 후 저장한다.
# Download base image ubuntu 20.04
FROM ubuntu:20.04

# LABEL about the custom image
LABEL maintainer="admin@sysadminjournal.com"
LABEL version="0.1"
LABEL description="This is custom Docker Image for \
the PHP-FPM and Nginx Services."

# Disable Prompt During Packages Installation
ARG DEBIAN_FRONTEND=noninteractive

# Update Ubuntu Software repository
RUN apt update

# Install nginx, php-fpm and supervisord from ubuntu repository
RUN apt install -y nginx php-fpm supervisor && \
    rm -rf /var/lib/apt/lists/* && \
    apt clean
 
# Define the ENV variable
ENV nginx_vhost /etc/nginx/sites-available/default
ENV php_conf /etc/php/7.4/fpm/php.ini
ENV nginx_conf /etc/nginx/nginx.conf
ENV supervisor_conf /etc/supervisor/supervisord.conf

# Enable PHP-fpm on nginx virtualhost configuration
COPY default ${nginx_vhost}
RUN sed -i -e 's/;cgi.fix_pathinfo=1/cgi.fix_pathinfo=0/g' ${php_conf} && \
    echo "\ndaemon off;" >> ${nginx_conf}
 
# Copy supervisor configuration
COPY supervisord.conf ${supervisor_conf}

RUN mkdir -p /run/php && \
    chown -R www-data:www-data /var/www/html && \
    chown -R www-data:www-data /run/php
 
# Volume configuration
VOLUME ["/etc/nginx/sites-enabled", "/etc/nginx/certs", "/etc/nginx/conf.d", "/var/log/nginx", "/var/www/html"]

# Copy start.sh script and define default command for the container
COPY start.sh /start.sh
CMD ["./start.sh"]

# Expose Port for the Application
EXPOSE 80 443

이제 앞서 추가 설정해야 할 Nginx 웹서버 설정 파일을 다음과 같이 수정한다. 
gedit default

server {
    listen 80 default_server;
    root /var/www/html;
    index index.html index.htm index.nginx-debian.html;
    server_name _;
    location / {
        try_files $uri $uri/ =404;
    }
    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php7.4-fpm.sock;
    }
}

다음과 같이 supervisrod.conf 파일을 생성한다. 
[unix_http_server]
file=/dev/shm/supervisor.sock   ; (the path to the socket file)

[supervisord]
logfile=/var/log/supervisord.log ; (main log file;default $CWD/supervisord.log)
logfile_maxbytes=50MB        ; (max main logfile bytes b4 rotation;default 50MB)
logfile_backups=10           ; (num of main logfile rotation backups;default 10)
loglevel=info                ; (log level;default info; others: debug,warn,trace)
pidfile=/tmp/supervisord.pid ; (supervisord pidfile;default supervisord.pid)
nodaemon=false               ; (start in foreground if true;default false)
minfds=1024                  ; (min. avail startup file descriptors;default 1024)
minprocs=200                 ; (min. avail process descriptors;default 200)
user=root             ;

[rpcinterface:supervisor]
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface

[supervisorctl]
serverurl=unix:///dev/shm/supervisor.sock ; use a unix:// URL  for a unix socket

[include]
files = /etc/supervisor/conf.d/*.conf

[program:php-fpm7.4]
command=/usr/sbin/php-fpm7.4 -F
numprocs=1
autostart=true
autorestart=true

[program:nginx]
command=/usr/sbin/nginx
numprocs=1
autostart=true
autorestart=true

다음과 같이 start.sh 파일을 생성한다.
#!/bin/sh
/usr/bin/supervisord -n -c /etc/supervisor/supervisord.conf

다음과 같이 실행권한으로 변경한다.
chmod +x start.sh

다음 명령을 실행해 앞서 생성한 도커 파일들 의존성을 확인한다. 
tree .

다음과 같이 도커를 빌드한다.
docker build -t nginx-image .

빌드 후 이미지 리스트를 확인한다.
docker image ls

다음과 같이 index.html 파일을 생성한다. 
echo '<h1>Nginx and PHP-FPM 7.4 inside Docker Container with Ubuntu 20.04 Base Image</h1>' > /webroot/index.html

웹 루트 폴더를 생성한다. 그리고, 다음과 같이 도커 이미지를 통해 웹서버를 실행한다.
mkdir -p /webroot
docker run -d -v /webroot:/var/www/html -p 8080:80 --name test-container nginx-image

크롬으로 로컬호스트 8080 포트를 입력하면, 설정된 도커 nginx 서버가 실행될 것이다. 

도커 이미지 내부 파일 저장하기
도커 이미지 실행 후 데이터베이스같은 파일을 저장하고, 종료하면, 파일도 함께 사라진다. 하지만, 도커 마운트를 이용하면, 도커를 실행하는 호스트 파일 시스템에 도커에서 생성된 파일들을 영구 보관할 수 있다.

도커는 이를 위해 볼륨(volume), 바인드 마운트(bind mount) 두 가지 방법을 제공한다.

볼륨은 다음과 같은 명령으로 생성한다. 이를 통해 다중 도커 이미지 간 폴더 파일들을 함께 공유할 수 있다.
docker volume create

예를 들어 다음과 같이 볼륨을 생성하고, 도커를 실행하면, myvol이 도커 /app와 연결된다.
docker volume create my-vol
docker run -d --name devtest --mount source=myvol, target=/app nginx:latest

볼륨 저장 위치는 다음 명령으로 알 수 있다.
docker inspect my-vol

바인드 마운트는 호스트 파일 시스템의 중요 시스템 변경이 가능하다. 바인드를 위해서, 다음 $(pwd)부분을 현재 바인드할 폴더명령 입력한다. 다음 예는 ngnix 서버를 실행하며, 영구 저장할 파일이 보관될 곳으로 $(pwd)와 /app를 서로 바인딩하는 예이다.
docker run -d -it --name devtest --mount type=bind, source="$(pwd)"/target, target=/app nginx:latest

이를 통해, 도커 이미지를 종료하더라도 이미지에서 생성, 수정한 파일을 영구 보관할 수 있다. 다음 링크는 mysql database file을 마운트를 통해 영구 보관하는 방법을 보여준다.
NVIDIA GPU 도커 사용하기

지금까지 도커 개발 방법을 간단히 살펴보았다. 도커는 운영체계 라이브러리에 의존하므로, 윈도우에서는 아직 NVIDIA GPU 도커가 동작하지 않는다. 즉, GPU 모드 딥러닝 프레임웍을 사용할 수가 없다. 여기서는 NVIDIA GPU 우분투 도커 버전을 간략히 설명한다.
이 도커는 아래 구조로 동작한다.
NVIDIA GPU 도커 이미지는 우분투 16.04, 18.04, 20.04에서 동작한다.

다음 명령을 통해 NVIDIA GPU 도커를 설치한다.
distribution=$(. /etc/os-release;echo $ID$VERSION_ID)
curl -s -L https://nvidia.github.io/nvidia-docker/gpgkey | sudo apt-key add -
curl -s -L https://nvidia.github.io/nvidia-docker/$distribution/nvidia-docker.list | sudo tee /etc/apt/sources.list.d/nvidia-docker.list

sudo apt-get update && sudo apt-get install -y nvidia-container-toolkit
sudo systemctl restart docker

# Add the package repositories
distribution=$(. /etc/os-release;echo $ID$VERSION_ID)
zypper ar https://nvidia.github.io/nvidia-docker/$distribution/nvidia-docker.repo

sudo zypper install -y nvidia-docker2  # accept the overwrite of /etc/docker/daemon.json
sudo systemctl restart docker

다음과 같이 사용한다.
#### Test nvidia-smi with the latest official CUDA image
docker run --gpus all nvidia/cuda:10.0-base nvidia-smi

# Start a GPU enabled container on two GPUs
docker run --gpus 2 nvidia/cuda:10.0-base nvidia-smi

# Starting a GPU enabled container on specific GPUs
docker run --gpus '"device=1,2"' nvidia/cuda:10.0-base nvidia-smi
docker run --gpus '"device=UUID-ABCDEF,1"' nvidia/cuda:10.0-base nvidia-smi

# Specifying a capability (graphics, compute, ...) for my container
# Note this is rarely if ever used this way
docker run --gpus all,capabilities=utility nvidia/cuda:10.0-base nvidia-smi

도커 명령어
이 장에서는 도커 명령을 간단히 요약해 본다.

1. 컨테이너 목록 확인하기
$ docker ps -a

2. 컨테이너 중지하기
$ docker stop ${container ID}

3. 컨테이너 제거하기
$ docker rm ${container ID#1} ${container ID#2}

4. 이미지 목록 확인하기
$ docker images

5. 이미지 다운로드 하기
$ docker pull

6. 이미지 삭제하기
$ docker rmi ${image ID}

7. 컨테이너 로그 보기
$ docker logs --tail 10 ${container ID}

8. 실시간 컨테이너 로그 보기
$ docker logs -f ${container ID}

9. 실행중인 컨테이너에 명령 실행하기
$ docker exec [option] container command [arg ...]
run은 새로 컨테이너를 만들어 실행하고, exec는 실행 중인 컨테이너에 명령을 실행하는 차이이다.
다음은 MySQL 컨테이너의 /bin/bash 쉘을 실행하는 예이다.
$ docker exec -it mysql /bin/bash

10. 도커 컨테이너 업데이트
도커에서 컨테이너를 업데이트할 때 컨테이너에 생성된 파일은 모두 삭제될 수 있다. 그러므로, 유지해야 할 데이터가 있는 경우, 컨테이너 내부가 아닌 외부 스토리지에 저장해야 한다.

11. 도커 컴포즈
도커 컴포즈 툴을 이용해, 도커 명령을 하나의 설정으로 간편하게 처리할 수 있다. 자세한 내용은 다음 링크를 참고한다.
기타 도커에서 생성된 데이터를 보관하려면 호스트 드라이버와 연결해 데이터를 저장하거나, 도커 볼륨을 생성해 그 위에 도커를 생성해 사용해야 한다. 그냥 도커를 실행한 후 종료하면 그 동안 작업된 데이터는 삭제된다는 것을 주의해야 한다. 관련 내용은 아래를 참고한다.

도커 이미지 완전 삭제 방법
도커 이미지를 완전히 삭제하기 위해서는 이미지를 사용하는 컨테이너를 모두 제거한 후 삭제해야 한다. 아래와 같이 명령을 실행한다. 지금 실행된 이미지들을 리스트한 후, 해당 이미지를 삭제해보자. 명령어에 있는 숫자들은 삭제할 IMAGE ID 이다. 

sudo docker images

sudo docker rm -f $(sudo docker ps -a --filter ancestor=8b9d78381e5d)

sudo docker stop $(sudo docker ps -a --filter ancestor=8b9d78381e5d)

sudo docker rmi 8b9d78381e5d

이래도 안되면, 다음 명령을 입력하면 완전 삭제될 것이다. 
docker system prune --all --volumes --force && docker volume prune --all --force

마무리

2017년 5월 23일 화요일

텐서플로우 MNIST 딥러닝 및 텐서보드 그래프 가시화

이 글은 TensorFlow를 설치하면, 사용할 수 있는 필기체 이미지인 MNIST 를 훈련해 보고, Tensorboard(텐서보드)로 훈련된 모델을 가시화해본다. 참고로, 이 글에서 사용하는 Tensorflow 버전은 1.1이다.

TensorFlow 설치는 앞에 글을 참고한다. 이 글은 아래  TensorFlow Tutorial 문서를 참고하였다.
MNIST는 손글씨로 쓴 0에서 9사이 숫자 이미지이다. 이 이미지는 20 x 20 픽셀 정규화된 흑백 이미지이다. 이를 안티알리아싱(ansialiasing)처리된 28 x 28 그레이(gray) 이미지가 포함되어 있다. mnist.train(훈련), mnist.test(테스트) 데이터가 포함되어 있고, 각 데이터 원소는 이미지(xs), 레이블(ys)로 구성되어 있다. 이미지는 mnist.train.image로 참조가 가능하다.

그레이 이미지 행렬이 28 x 28 이미로, 784차원 벡터라 볼 수 있다. 다음 그림과 같이, 각 펙셀의 그레이값은 0 ~ 1.0 사이값으로 표현된다.
그레이 이미지는 다음 그림과 같이 55000개가 있다.
from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets("MNIST_data/", one_hot=True)
from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets("MNIST_data/", one_hot=True)
from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets("MNIST_data/", one_hot=True)
from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets("MNIST_data/", one_hot=True)
from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets("MNIST_data/", one_hot=True)
이에 대응하는 라벨 벡터도 55000개 있으며, 구조는 10개 라벨에 대한 참값을 가지는 1차원 벡터로 구성된다. 예를 들어, 숫자 2에 대응하는 라벨 벡터는 [0, 0, 1, ..., 0] 이다.

이 이미지를 텍스트 라벨로 분류할 수 있도록 TensorFlow로 훈련시켜본다. 훈련방법은 softmax를 이용한 신경망 모델, CNN(convolutional neural network) 모델 두가지 방법이다.

1. softmax 함수 신경망 기반 MNIST 훈련
MNIST 훈련을 위해, 다음과 같이 신경망을 구성하고, MNIST 데이터를 훈련해 본다. 학습 모델의 텐서 구조는 다음과 같다.
A simple MNIST model 구조

활성함수 φ는 다음과 같은 softmax 함수를 이용한다. 
   y = softmax(v)

신경망 계산 및 softmax 함수 동작 방식

TensorFlow에서 다음 코드를 입력한다.

from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets("MNIST_data/", one_hot=True)

import tensorflow as tf
x = tf.placeholder(tf.float32, [None, 784])
W = tf.Variable(tf.zeros([784, 10]))
b = tf.Variable(tf.zeros([10]))
y = tf.nn.softmax(tf.matmul(x, W) + b)
y_ = tf.placeholder(tf.float32, [None, 10])

cross_entropy = tf.reduce_mean(-tf.reduce_sum(y_ * tf.log(y), reduction_indices=[1]))
train_step = tf.train.GradientDescentOptimizer(0.5).minimize(cross_entropy)

sess = tf.InteractiveSession()
tf.global_variables_initializer().run()

for _ in range(1000):
  batch_xs, batch_ys = mnist.train.next_batch(100)
  sess.run(train_step, feed_dict={x: batch_xs, y_: batch_ys})

correct_prediction = tf.equal(tf.argmax(y,1), tf.argmax(y_,1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))

print(sess.run(accuracy, feed_dict={x: mnist.test.images, y_: mnist.test.labels}))

이 코드의 실행결과는 다음과 같다. 대략 92% 정확도를 가진 훈련모델을 얻을 수 있다.

2. CNN 기반 MNIST 훈련
92% 정확도도 나쁘지는 않지만, 99.2% 수준으로 훈련모델을 개선할 수 있는 더 좋은 방법이 있다. CNN(convolutional neural network) 딥러닝을 이용해 다음과 같이 개선한다.
코드는 다음과 같다.

def weight_variable(shape):
  initial = tf.truncated_normal(shape, stddev=0.1)
  return tf.Variable(initial)

def bias_variable(shape):
  initial = tf.constant(0.1, shape=shape)
  return tf.Variable(initial)

def conv2d(x, W):
  return tf.nn.conv2d(x, W, strides=[1, 1, 1, 1], padding='SAME')

def max_pool_2x2(x):
  return tf.nn.max_pool(x, ksize=[1, 2, 2, 1],
                        strides=[1, 2, 2, 1], padding='SAME')

W_conv1 = weight_variable([5, 5, 1, 32])
b_conv1 = bias_variable([32])

x_image = tf.reshape(x, [-1,28,28,1])

h_conv1 = tf.nn.relu(conv2d(x_image, W_conv1) + b_conv1)
h_pool1 = max_pool_2x2(h_conv1)

W_conv2 = weight_variable([5, 5, 32, 64])
b_conv2 = bias_variable([64])

h_conv2 = tf.nn.relu(conv2d(h_pool1, W_conv2) + b_conv2)
h_pool2 = max_pool_2x2(h_conv2)

W_fc1 = weight_variable([7 * 7 * 64, 1024])
b_fc1 = bias_variable([1024])

h_pool2_flat = tf.reshape(h_pool2, [-1, 7*7*64])
h_fc1 = tf.nn.relu(tf.matmul(h_pool2_flat, W_fc1) + b_fc1)

keep_prob = tf.placeholder(tf.float32)
h_fc1_drop = tf.nn.dropout(h_fc1, keep_prob)

W_fc2 = weight_variable([1024, 10])
b_fc2 = bias_variable([10])

y_conv = tf.matmul(h_fc1_drop, W_fc2) + b_fc2

cross_entropy = tf.reduce_mean(
    tf.nn.softmax_cross_entropy_with_logits(labels=y_, logits=y_conv))
train_step = tf.train.AdamOptimizer(1e-4).minimize(cross_entropy)
correct_prediction = tf.equal(tf.argmax(y_conv,1), tf.argmax(y_,1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
sess.run(tf.global_variables_initializer())
for i in range(20000):
  batch = mnist.train.next_batch(50)
  if i%100 == 0:
    train_accuracy = accuracy.eval(feed_dict={
        x:batch[0], y_: batch[1], keep_prob: 1.0})
    print("step %d, training accuracy %g"%(i, train_accuracy))
  train_step.run(feed_dict={x: batch[0], y_: batch[1], keep_prob: 0.5})

print("test accuracy %g"%accuracy.eval(feed_dict={
    x: mnist.test.images, y_: mnist.test.labels, keep_prob: 1.0}))

실행 결과는 다음과 같다. 98% 이상의 정확도를 얻을 수 있다.

3. Fully Connected Feed
MNIST는 머신러닝의 전통적인 문제를 보여준다. 이 챕터에서는 tensorflow sample에 포함되어 있는 fully conntected feed 예제를 실행해 본다. 아나콘다를 설치한 경우, 이 예제는 [아나콘다 설치 폴더]\envs\tensorflow\Lib\site-packages\tensorflow\examples\tutorials에 포함되어 있다.

python fully_connected_feed.py

그럼, 다음과 같은 결과를 얻을 수 있다.

4. TensorBoard 활용 그래프 가시화
텐서보드(TensorBoard)를 이용하면, 텐서플로우 학습모델링 그래프를 시각적으로 확인할 수 있다. 텐서보드를 이용하려면, 텐서보드 그래프 가시화에 필요한 로그 데이터를 저장할 폴더를 지정해야 한다. 다음과 같이 아나콘다에서 텐서보드 logdir를 지정한다. 

tensorboard --logdir=[Tensorboard log directory] 
예) tensorboard --logdir=d:\program\anaconda3\envs\tensorflow\tensorboard_log

여기에서는 앞의 fully_connected_feed.py 에서 실행된 logdir 폴더에 저장된 데이터를 다음과 같이 텐서보드로 확인해 보겠다. 

tensorboard --logdir=/tmp/tensorflow/mnist/logs/fully_connected_feed


실행 후에 다음과 같이, http://localhost:6006/에서 학습된 결과들을 확인할 수 있다. 
텐서보드의 그래프를 살펴보면, 학습모델을 어떻게 구성했는 지 직관적으로 파악 수 있다.
fully_connected_feed 텐서플로우 그래프
텐서보드의 임베딩 벡터 가시화
텐서보드의 임베딩 벡터 가중치 T-SINE 가시화 영상(iteration)

5. 마무리
지금까지 텐서플로우를 이용해, MNIST를 딥러닝해보고, 학습모델링된 그래프를 가시화해 보는 텐서보드를 살펴보았다. 앞으로, 텐서플로우와 같은 머신러닝 플랫폼을 이용해, 많은 문제들이 지능화되리라 생각한다.


기타 레퍼런스