45. 마지막 정리, 새로운 시작 (종강)

머신러닝을 준비하면서 마치 롤러코스터를 타는 듯한 감정의 기복에 여러 번 놀랐다.

누구나 할 수 있는 머신러닝! 통계나 수학을 전공하지 않아도, 프로그래머가 아니어도 할 수 있다고 했다. 지금 시작하지 않으면 비전공이라고 부르는 사람들에게까지 나의 자리를 내주어야 할 것 같았다.

나는 파이썬 강사이며, 여러 개의 프로젝트를 수행한 프로그래머다. 머신러닝이 너무 어렵다. 파이썬을 잘 해도 TensorFlow에 대해서 모르기 때문에 어렵다. 잠깐 등장하는 미분이 너무 어렵다. 미분 때문에 내부 구조를 유추할 수 없어서 어렵다. 머신러닝 구동환경이 너무 어렵다. 우분투에 TensorFlow는 좋은 조합일까?

너무 어려워서 마음이 놓인다. 내 자리를 쉽게 내어줄 것 같아서 마음 고생이 심했다. 프로그래밍을 하면 차이를 낼 수 있는 부분이 있을 거라고 위로하면서 견뎠다.

프로그래밍을 하는 사람은 많지만, 제대로 하는 사람은 많지 않다. 머신러닝을 하는 사람은 많겠지만, 제대로 하는 사람은 많지 않을 것이다. 유행 때문에, 옆 사람이 하기 때문에, 달라 보이기 위해서. 여러 가지 이유로 시작을 하겠지만, 목표가 분명하지 않다면 도태될 것이다.

나의 머신러닝 시즌1은 끝났다. 나는 시즌2를 준비하고 있지만, 내 글을 읽은 사람들은 무엇을 해야할지 모를게 틀림없다.


1. 파이썬 정리
프로그래밍을 몰라도 머신러닝을 할 수 있다고 믿는 사람이 있다? 그런데, 왜 머신러닝을 활용해서 결과를 내지 못하는 것일까? 프로그래밍 외에 미분 등의 중차대한 요소가 발목을 잡고 있어서 그런 것일까?

프로그래밍에 대한 정리가 확실하지 않으면, 머신러닝은 불가능하다. 머신러닝 이전 시대에 비해 진입 장벽이 낮아진 것은 사실이지만, 없어지지는 않았다. 먼, 어쩌면 가까운 미래에는 사라질 수도 있겠지만, 아직 아니다.

파이썬을 포함한 모든 프로그래밍 언어의 핵심은 반복문이다. 1차원과 2차원의 기본적인 반복문 형태에 익숙해지는 것이 우선되어야 한다. 내가 본 머신러닝 코드는 기본적인 반복문을 조금 확장한 정도여서 코드 때문에 어렵다는 말은 나오지 않을 거라고 확신한다.


2. TensorFlow 정리
머신러닝의 도구로 Theano 혹은 caffe 등의 도구를 선택해도 상황은 동일하다. TensorFlow 라이브러리의 코드에 익숙해져야 한다. Theano 라이브러리에 익숙해져야 한다.

개인적으로는 TensorFlow에 익숙하지 않아서 너무 힘들었다. 머신러닝 자체에 대해서도 공부할 것이 많아서 TensorFlow를 볼 시간이 없었다, 라고 생각했다. TensorFlow에 대한 최소한의 정리를 먼저 하는 것이 유리하다.

TensorFlow 홈페이지에 가면 개요부터 시작해서 설명이 잘 되어 있다. 이들 문서를 읽어보지도 않고 시작을 했다. 전체 문서를 읽어보고 TensorFlow를 구성하는 요소들에 대해 파악할 시간이 됐다.


3. 얕은 수학적 지식
수학적 지식을 필요로 하지 않는다는 것은 맞을 수도 있고, 틀릴 수도 있다. 어디나 그렇지만, 만드는 사람과 사용하는 사람의 두 종류가 있다. 나는, 우리는 만드는 사람이 될 수 없다. 충분히 사용한 이후라면 가능할 수도 있지만, 기대하지 말자.

실적이란 것을 내기 전까지는 철저하게 사용자로 살기로 했다. 만드는 사람에게는 고난이도의 수학이 필요하다. 사용하는 사람에게는 저난이도의 수학이 필요하다. 제대로 사용하기 위해서는 최소한의 수학적 지식이 필요하다. 그것이 김성훈 교수님 동영상에서 소개한 정도라고 믿고 있다. Gradient Descent 알고리듬이 제대로 동작하는 것을 이해하기 위해서는 미분을 알아야 한다.

운전하는 도중에 기어를 바꿀 수 있을까, 없을까? 오버드라이브라고 부르는 것이 있어서 뭔가를 한다고 하는데, 그게 뭘까? 나는 훌륭한 운전자가 아니다. 이들에 대해서 전혀 모른다. 심지어 엔진 오일이나 타이어를 언제 갈아야 하는지도 모른다. 그래도 운전을 한다.

미분과 지수 함수에 대한 좋은 동영상 강좌를 찾아보자. 1타 수능강사들이 올려 놓은 동영상도 있을 것 같다. 무료로 볼 수 있지 않을까? 머신러닝이 인생을 바꿀 거라고 믿기 때문에 머신러닝 고수가 될 것이다. 운전은 인생을 바꾸어 주지 못하기 때문에 자세히 알고 싶지 않았다.


4. 핵심 알고리즘
머신러닝을 관통하는 가장 중요한 알고리즘이 두 개 있다. 극히 개인적인 생각이다. 가장 먼저 배운 Linear Regression과 이를 응용한 Binary Classification. 이들은 지금까지의 지식으로는 이후에 나오는 모든 알고리듬에 적극적으로 개입한다. 뭐가 됐든 이들로부터 시작한다.

교수님의 동영상을 5번 이상은 꼭 봤다. 이해가 안 되서. 그런데, 스터디를 진행한 이후에 보는 그 다음 주 동영상이 이해가 됐다. 4번, 5번 볼 때는 이해가 안 됐는데, 앞의 부분을 이해하니까 전에 놓쳤던 부분들까지 모두 보였다. 순차적으로 진행해야 하는 것을 나중에 알았다. 강사라서 알고 있긴 했는데, 믿고 싶지 않았다.


5. 실습
지금까지 배운 코드를 응용해서 자신만의 코드를 만들어 보자. 이게 될리가 없다. 이게 된다면, 천재?

흥미 있는 부분부터 자신만의 예제를 만들자. 함께 했던 분들은 대부분 RNN에 많은 관심을 보였다. 자신만의 예제를 만들어 발표하기로 했다.

개인적으로도 RNN부터 해보는 것을 추천한다. RNN 이전의 알고리듬과 코드는 어디에 적용해야 할지 막연한 느낌이다. 반면, RNN은 교수님께서 알려주시기만 했고 소개하지 않은 코드도 많이 있었다. 특히, 텍스트를 기반으로 구현할 수 있기 때문에 데이터를 모으는 것도 어렵지 않다.


6. 추가 강좌
다른 강좌를 듣는 것도 좋다. 그렇지만 지금은 아니다. 여유를 갖고 교수님의 동영상을 다시 한번 정주행 하자. 교수님 강의가 좋은 것도 있지만, 강사로써 복습을 통한 이해에 한 표를 던질 수밖에 없다.

머신러닝 알고리듬을 이해하고 싶다!
나한테 익숙한 설명과 코드로 시작하는 것이 가장 좋은 방법이다.


7. 추천 사이트
구글링을 통해 몇 개 사이트로 정리할 수 있었다. 도움이 되는 사이트가 정말 많겠지만, 머신러닝을 어떻게, 무엇을 공부할지에 대해서는 많이 없다. 중요도 순이 아니라 구글링에 나온 순서대로 나열했다.

1. 머신러닝, 제대로 배우는 법
2. 수학을 포기한 직업 프로그래머가 머신러닝 학습을 시작하기위한 학습법 소개
3. 딥러닝을 처음 시작하는 분들을 위해
4. 딥러닝 공부 가이드 (HW / SW 준비편)
5. 빅데이터와 머신러닝에 대한 즐겨찾기 모음


8. 실전(캐글)
지금까지 배운 것을 실제로 접목해야 한다. 그렇지 않으면 머신러닝과 금방 멀어지게 된다. 이때 캐글(www.kaggle.com) 사이트를 이용하면 좋다.

이전에 스터디를 했던 분들과 함께 캐글에 올라온 몇 개의 예제를 분석했다. 많이 배웠다. 텐서플로우 외에도 많은 도구들이 있고, 특히 데이터 선처리에 대한 이해를 높일 수 있었다. 프로그래머로써 "이렇게 하면 될 것이다"라는 막연한 생각만 있었는데, 그런 생각들에 대해 확신을 갖게 된 것도 있었고 잘못 생각한 부분도 많았다.

캐글에는 기업에서 제안한 모든 문제들에 대해 먼저 다녀간 사람들이 남겨놓은 자신만의 소스코드가 있다. 이걸 통해 텐서플로우로부터 어느 정도는 벗어나야 한다는 것을 배우는 중이다. 텐서플로우가 가장 좋다고 믿지만, 모든 것을 대신할 수 없다는 겸손함 정도.

나는 여기까지 배웠고, 얘기할 수 있는 것도 여기까지이다. 더 배우게 된다면.. 뒷부분을 더 채울 수 있을텐데.. 아쉽다!

44. 4주차 스터디 정리

너무 금세 처음 계획했던 4주의 시간이 흘렀다. 무지하게 힘들었지만, 재미있었으니까 고생 제로.


1. convolutional의 뜻
영어 사전에 보면 "소용돌이 치는, 나선형의, 주름 모양의" 뜻으로 정의되어 있다. 그런데, convolutional network의 구성을 보면 단어의 뜻과는 차이가 있다. 다른 분이 정의한 내용 중에는 이런 것이 있었다. [컨볼루션 네트워크]이곳에는 다른 좋은 내용도 많아서 살펴보기만 해도 도움이 될 수 있으니 꼭 들러보자.
어원은 라틴어 convolvere이고 그 뜻은 두 가지를 같이 돌돌 마는 행동을 의미합니다. 수학에서 컨볼루션은 두 가지 함수가 얼마나 겹치는지를 적분을 이용해 측정하는 것입니다. 두 함수를 하나로 합치는 것이라고 이해해도 됩니다.

2. 고양이가 반응하는 방식과 convolutional하고 무슨 관계가 있는걸까?
두 가지의 공통점은 매우 작은 일부 영역을 통해서 전체를 찾아낼 수 있다는 것. 고양이가 사물을 볼 때 뇌의 일부만 활성화되지만, 결국에는 사물 전체가 무엇인지 식별하게 된다. ConvNet은 이미지를 조그맣게 잘라서 이들 영역을 통해 결국에는 무슨 그림인지 식별하게 된다.

3. 마지막에 반드시 pooling 하는 이유
동영상에서의 pooling은 이미지 크기를 절반으로 줄이는 역할을 한다. pooling layer를 호출할 때마다 크기가 줄기 때문에 원하는 정도의 크기가 될 때까지 반복해서 호출하는 것이 맞다. FC에 전달할 데이터의 갯수를 조절해야 한다면, FC 앞에 위치하는 것이 좋다.

4. 마지막에 FC하는 이유
fully connected network은 쉽게 얘기하면 softmax를 의미한다. softmax는 여러 개의 label 중에서 하나를 선택하는 모델이기 때문에 그림의 label이 무엇인지 판단하는 CNN과 매우 잘 어울린다. label을 선택하기 위해 있을 뿐이다.

5. network이란?
network은 여러 층의 layer가 연결된 형태의 모델을 말하고, layer는 여러 개의 노드(node)라는 것으로 구성된다.

6. 나누어 떨어지지 않으면 필터와 spride를 구성할 수 없는가?
구성할 수 없다. 나누어 떨어지지 않아도 계산을 진행할 수는 있겠지만, 나누어 떨어진다면 쉽게 처리할 수 있다. 데이터를 가공하는 과정에서 여러 가지 경우로 쉽게 나누어 떨어뜨리도록 만드는 것도 기술이 될 수 있다.

7. 이미지가 작아지는 것이 문제일까? 패딩을 추가하면 크기를 유지할 수 있다.
convolutional layer를 거칠 때마다 그림 크기가 작아진다. 1998년에 만든 LeNet이 동작한 방식이다. 그러나, 그 이후에 나온 모델에서는 패딩을 사용해서 원본 크기가 줄어들지 않도록 강제한다. 대표적으로 구글의 알파고가 패딩을 사용했다. 패딩을 사용하면 크기는 줄어들지 않겠지만, 테두리는 희석될 수밖에 없다. 가운데 영역은 희석되지 않고 테두리만 희석된다. 그런데도 이게 의미가 있을까?
교수님께서 앞에서 이 부분에 대해 잠깐 말씀하신 적이 있다. 패딩을 추가하는 목적은 두 가지로 크기 유지와 경계선 추가. 프로그래밍의 관점에서 본다면 경계선을 추가해서 계산의 성능을 높이는 것에 한 표. 희석되기 때문에 결과만 놓고 본다면 크기가 줄어드는 것과 차이가 없지 않을 것 같다.
최근에 크기가 줄어들지 않기 때문에 테두리 근방의 값들이 보존될 수 있다는 생각이 들었다. 테두리 자체는 희석될 수도 있겠지만, 줄어들지 않기 때문에 새로운 테두리가 생기고 없어지는 현상은 막을 수 있게 된다.

8. 계속 같은 크기로 만들려면 매번 패딩을 추가해야 하는데..
정답. 크기가 줄어든다면 항상 패딩을 먼저 추가한다. 원본 크기를 유지하기 위해 패딩의 크기도 제각각. 1, 2, 3픽셀 등등 원하는 크기로 추가한다.

9. 왜 여러 개의 필터를 둘까(activation maps)?
필터를 정확하게 말하면 weight이 된다. 이걸 이해하지 못하면.. 좀 어렵다. 6장의 필터를 사용하면 6장의 출력이 만들어진다. 6장의 필터는 모두 조금씩 다른 weight을 표현한다. 결국 여러 개의 필터를 사용해서 동일 데이터(필터와 계산할 영역)에 대해 6가지의 결과를 얻게 된다. 이 말은 6명의 전문가를 투입하는 것과 같고, 정확도 향상을 위해 여러 장의 필터를 사용하는 것이 좋다.

10. 5x5x3에서 3은 무엇을 의미하나?
채널(channel)을 의미한다. 똑같은 크기의 그림이 여러 장 겹쳐 있다고 보면 된다.

11. 필터에 포함된 변수(피처)의 갯수는?
필터는 weight의 다른 말. 필터 크기가 5x5x3이라면 75개의 피처를 갖는 셈이 된다. 여기에 필터가 6개라면 6을 곱해서 450개의 피처가 된다.

12. Wx에 사용된 곱셈
원본 그림과 필터를 곱해서 one number를 구하는데, 이때 사용된 곱셈은 행렬 곱셈이 아니다. 필터가 덮은 영역의 값과 곱셈을 하기 때문에 element-wise 곱셈이 되어야 한다.

13. 왜 Max Pooling을 사용할까?
pooling에는 여러 가지 방법이 있다. 가장 먼저 생각나는 방법은 평균을 사용하는 것이다. 여러 개 중에서 하나를 선택해야 한다면, 중앙값과 평균 중에서 하나를 고르는 것이 맞다.
평균은 필터 영역의 특징을 표현하지 못하기 때문으로 보인다. 평균을 사용하면 여러 번의 pooling을 거치면 전체 값들이 비슷해지는 증상을 보일 수 있을 것 같다. 평균의 평균의 평균을 하는 셈이 되므로 비슷해지는 것이 당연하다. 그럼.. 모든 그림이 다 똑같은 그림이라는 결론이 나올 수도 있겠다.
특징을 표현할 수 있는 값을 선택해야 한다면, max pooling이 맞는 것 같다. Min Pooling은 사용할 수 없다. LeRU를 거치기 때문에 음수는 0이 되어 버려서 최소값의 의미가 없다.
우리가 사람의 얼굴을 기억한다는 것은, 얼굴의 모든 것을 기억하는 것이 아니라 두드러진 특징 몇 가지를 기억하는 것이다. CNN에서 두드러진 특징을 찾기 위한 방법이 max pooling이라고 보면 된다.

14. pooling에도 stride가 있다?
convolutional과 pooling은 모두 필터와 stride를 사용한다. 필터는 영역의 크기를 지정하기 위해, stride는 움직일 거리를 지정하기 위해 필요하다. pooling도 몇 개 중에서 뽑을 것인지, 다음 번에 어디서 뽑을 것인지 결정해야 한다.

15. 마지막 fc는 구체적으로 어떻게 구성되어지나?
softmax라고 설명했다. LeNet 그림을 보면 첫 번째 layer는 120개, 두 번째는 84개, 마지막에는 10개로 줄어든다. 줄어드는 규칙은 모르겠지만, 10개를 고를 때 가장 이상적인 조합이 아닐까, 생각한다.

16. 파라미터의 갯수가 35k라는데..?
AlexNet에서는 227x227x3 형식의 그림을 사용하고 있다. 필터 크기는 11x11x3, stride는 4x4. 공식에 넣어보면 출력 크기는 55x55가 된다. 필터 갯수는 96개였으니까, 최종적으로는 55x55x96.


  (227 - 11) / 4 + 1 = 216/4 + 1 = 54+1 = 55

  출력크기 : 55x55x96 = 290,400 byte
  파라미터 : 11x11x3x96 = 34,848 (35k)

필터는 weight을 가리키고 11x11x3이 weight의 갯수가 된다. 이런 게 96개 있다.

43. TensorFlow에서 RNN 구현하기 (lab 12) 소스코드

lab 12 동영상의 내용이 너무 길어서 소스코드를 이곳에 별도로 정리했다.

먼저 동영상에 나온 코드를 있는 그대로 작성한 코드이다. 교수님께서 알려주신 사이트에 있는 코드는 동영상과 많이 달라서 싣지 않았다. 이번 코드는 이전 글에서 부분적으로 충분히 설명했기 때문에 설명은 생략한다.


import tensorflow as tf
import numpy as np

char_rdic = ['h', 'e', 'l', 'o'] # id -> char
char_dic = {w : i for i, w in enumerate(char_rdic)} # char -> id

sample = [char_dic[c] for c in 'hello']

x_data = np.array([[1,0,0,0], # h
[0,1,0,0], # e
[0,0,1,0], # l
[0,0,1,0]], # l
dtype = 'f')

# Configuration
char_vocab_size = len(char_dic)
rnn_size = char_vocab_size # 1 hot coding (one of 4)
time_step_size = 4 # 'hell' -> predict 'ello'
batch_size = 1 # one sample

# RNN Model
rnn_cell = tf.nn.rnn_cell.BasicRNNCell(rnn_size)
state = tf.zeros([batch_size, rnn_cell.state_size])
X_split = tf.split(0, time_step_size, x_data)

outputs, state = tf.nn.rnn(rnn_cell, X_split, state)

logits = tf.reshape(tf.concat(1, outputs), [-1, rnn_size])
targets = tf.reshape(sample[1:], [-1])
weights = tf.ones([len(char_dic) * batch_size])

loss = tf.nn.seq2seq.sequence_loss_by_example([logits], [targets], [weights])
cost = tf.reduce_sum(loss) / batch_size
train_op = tf.train.RMSPropOptimizer(0.01, 0.9).minimize(cost)

# Launch the graph in a session
with tf.Session() as sess:
tf.initialize_all_variables().run()
for i in range(100):
sess.run(train_op)
result = sess.run(tf.argmax(logits, 1))
print(result, [char_rdic[t] for t in result])


두 번째는 생각보다 복잡한 구석이 많아서 변수를 상수로 대체해서 다시 작성해 봤다. 이름까지 정리하고 나니까 코드를 읽기가 많이 편해져서 소개한다.

import tensorflow as tf
import numpy as np

unique = 'helo'
y_data = [1, 2, 2, 3] # 'ello'. index from 'helo'
x_data = np.array([[1,0,0,0], [0,1,0,0], [0,0,1,0], [0,0,1,0]], dtype='f') # 'hell'

cells = tf.nn.rnn_cell.BasicRNNCell(4) # 출력 결과(4가지 중에서 선택)
state = tf.zeros([1, cells.state_size]) # shape(1, 4), [[0, 0, 0, 0]]
x_data = tf.split(0, 4, x_data) # layer에 포함될 cell 갯수(4). time_step_size

# outputs = [shape(1, 4), shape(1, 4), shape(1, 4), shape(1, 4)]
# state = shape(1, 4)
outputs, state = tf.nn.rnn(cells, x_data, state)

# tf.reshape(tensor, shape, name=None)
# tf.concat(1, outputs) --> shape(1, 16)
logits = tf.reshape(tf.concat(1, outputs), [-1, 4]) # shape(4, 4)
targets = tf.reshape(y_data, [-1]) # shape(4), [1, 2, 2, 3]
weights = tf.ones([4]) # shape(4), [1, 1, 1, 1]

loss = tf.nn.seq2seq.sequence_loss_by_example([logits], [targets], [weights])
cost = tf.reduce_sum(loss)
train_op = tf.train.RMSPropOptimizer(0.01, 0.9).minimize(cost)

with tf.Session() as sess:
tf.initialize_all_variables().run()
for i in range(100):
sess.run(train_op)
r0, r1, r2, r3 = sess.run(tf.argmax(logits, 1))
print(r0, r1, r2, r3, ':', unique[r0], unique[r1], unique[r2], unique[r3])

코드를 볼 때, 중간에 생성되는 데이터가 어떤 형태인지 아는 것이 매우 중요하다. shape(4)는 1차원 배열로 4개의 요소를 갖고 있고, shape(4, 3)은 2차원 배열로 4개의 행과 3개의 열을 요소로 갖고 있다. [1, 2, 3]은 1차원 배열이고, [[1, 2, 3]]은 2차원 해열로 1x3의 크기를 갖는다.

'hello'를 다룬다고 했는데, 실제로는 마지막 글자를 제외한 'hell'만 사용된다. language model은 다음에 올 글자나 단어를 예측하는 모델이어서, 마지막 글자가 입력으로 들어와도 예측할 수가 없다. 정답에 해당하는 label이 없기 때문에 전달하지 않는 것이 맞다.

tf.split 함수는 데이터를 지정한 갯수로 나눈 다음, 나누어진 요소를 Tensor 객체로 만들어 리스트에 넣어서 반환한다. x_data 변수는 Tensor가 4개 들어간 리스트라는 뜻이다. 처음 만들 때는 np.array로 만들었다가 Tensor 리스트로 변환했다. 첫 번째 매개변수인 0은 행에 대해 나누라는 뜻이다. 1을 전달하면 열을 기준으로 나눈다.

tf.concat 함수는 입력을 연결해서 새로운 Tensor를 만든다. 0을 전달하면 행 기준, 1을 전달하면 열 기준이 된다. 1을 전달했으므로 0번째부터 3번째까지 순서대로 사용해서 1x16 크기의 Tensor를 만든다. 매개변수로 넘어온 outputs는 4개짜리 리스트인데, 3차원으로 해석한다면 4x1x4로 볼 수 있다. reshape 함수를 사용해서 4x4 크기의 Tensor 객체로 바꾸고 있다.

reshape 함수에 [-1]이라고 전달하면 1차원으로 만들겠다는 뜻이고, [-1, 4]로 전달하면 2차원으로 만드는데, 행은 4로 나눈 값을 쓰겠다는 뜻이 된다. 1차원에서는 나눌 값이 없기 때문에 전체를 1차원으로 늘어 놓는 형태가 된다. flat이라고 한다.

tf.argmax 함수는 one-hot encoding을 처리해 주고, 이 값을 run 함수에 전달해 결과를 계산했다. 이번 모델은 입력도 4개, 출력도 4개였다. run 함수의 결과를 리스트 1개로 받을 수도 있지만, 변수 4개로 받았다. r0, r1, r2, r3은 예측한 값의 인덱스로, 순서대로 h, e, l, l 다음에 오는 예측 값을 가리킨다. 원본 문자열은 unique 변수에 넣었고, unique로부터 인덱스 번째의 문자를 출력하면 그것이 예측한 문자가 된다.

42. TensorFlow에서 RNN 구현하기 (lab 12)


김성훈 교수님 동영상 정리는 이번이 마지막이다. 아마존 웹서비스를 사용하는 동영상이 더 있지만, 시기적으로 적절치 않아서 생략했다.


이번 동영상에서 구현할 RNN 모델이다. character 기반의 language model로 "hello"가 입력일 때의 결과를 예측하는 초간단, 초단순 RNN이다.


RNN에서 거쳐야 할 작업 중의 첫 번째다. 기본 cell을 비롯해서 이전 동영상에서 언급됐던 LSTM과 GRU cell을 사용할 수도 있다. 그림에 있는 rnn_size에 들어가는 값은 output layer의 크기가 된다.


이전 그림에 언급했던 것처럼 output layer의 크기인 4를 전달하고 있다. 동영상에 나오는 코드는 최신 버전에서 살짝 동작하지 않는다. tensorflow 사용법이 조금 바뀌었다. 그림에 나오는 사이트에 가면 동작하는 코드를 볼 수 있다. (소스코드 보기)


전체 코드에서 가장 중요하다고 말씀하신 X_split 변수에 대해 설명한다. 그림 아래에 위치한 값이 X_split을 의미한다. 1행 4열의 Tensor 객체를 4개 갖고 있는 리스트이다. 입력으로 들어오는 값이 h, e, l, o 중의 하나를 가리키고, 가리키는 방식으로 one-hot encoding을 사용하기 때문에 1행 4열이 된다.

그림 위쪽은 출력 결과이다. 결과 또한 one-hot encoding으로 나오기 때문에 1행 4열이다. 입력과 출력 모두 time-step size는 4개로 동일하다. 글자 4개를 한 번에 전달해서 결과로 4개를 돌려 받는다는 뜻이다. 입력과 출력의 갯수는 당연히 다를 수 있다. 1대N, N대N, N대1 등의 모델에 들어간 숫자를 보면 알 수 있다.


vocabulary는 데이터에 포함된 unique한 문자를 의미한다. word 기반이 된다면 unique한 단어가 될 것이다. 4개의 글자 중에서 하나를 가리키는 방식으로 특정 인덱스의 숫자만 1로 만든다(one-hot encoding). 1행 4열을 하나로 관리해야 하니까, x_data는 4행 4열의 2차원 배열이 된다. np는 numpy의 약자.


코드 순서와 그림에 배치된 layer 순서가 일치한다. rnn_cell은 output layer, state는 hidden layer, X_split은 input layer에 해당한다.

rnn.rnn은 0.9 버전에서 tf.nn.rnn으로 바뀌었다. RNN에서 가장 중요한 것은 자신의 상태를 매번 바꾸어 나가면서 매번 예측을 하는 것이었다. 여기서 보면 rnn 함수 호출로 예측에 해당하는 outputs와 새로운 상태인 state가 반환되는 것을 알 수 있다.


코드만 놓고 본다면, 전체 소스코드에서 이 부분이 가장 어렵다. logits는 y에 해당하는 예측값, targets는 정답을 갖고 있는 label, weights는 말 그대로 weight.

logits는 1x4로 구성된 배열이기 때문에 2차원 배열이어야 하고, targets는 각각에 대한 정답을 담고 있기 때문에 1차원 배열, weights 또한 입력에 대해 계산되는 값이기 때문에 1차원 배열이 된다.

sequence_loss_by_example 함수는 이들 배열을 한 번에 받아서 loss(erorr)를 한 번에 계산해 준다. 4개의 입력이 있었으니까, 이들을 모아서 최종 cost를 계산하고 optimizer가 가장 작은 cost를 찾는다. 이전 동영상에 나왔던 RMSPropOptimizer 함수를 사용하고 있다. sequence_loss_by_example 함수는 파이썬의 리스트를 매개변수로 받기 때문에 logits 등을 []로 감쌌다.


학습해서 중간 결과를 보여주는 코드는 이전과 유사하다. 다만 예측 결과로 나온 result는 문자에 대한 인덱스를 갖고 있는 리스트라서 해당 인덱스가 어떤 문자인지 검사하기 위해서 리스트 컴프리헨션(comprehension)을 사용하고 있다. 이 코드는 char_rdic[result[0]]과 같은 형태로 바꿀 수 있지만, 바꾸면 코드가 훨씬 길어진다. 출력이 4개니까.


전체 소스코드. 주석을 제거하면 얼마 안 된다고 교수님께서 강조하셨다. 말씀하신 것처럼 직접 입력해 보면 얼마 걸리지 않을 정도로 짧다.


이전 코드에서 100번 반복하기 때문에 출력 결과는 많이 생략되어 있다. 어찌 됐든 스스로 정답을 찾아가는 모습이 신기하다. 다만 착각하면 안 되는 것이, 이번 모델은 "hello"에 최적화되어 있어서 다른 문자열에 대해서는 동작하지 않는다.


RNN을 여러 층으로 쌓을 수 있고, 쌓는 방법 또한 무지하게 간단하다. 훨씬 복잡한 모델이 나타난다면 사용해야 할 방법이다.


셰익스피어를 학습하면 셰익스피어가 될 수 있다. 실제 만들어진 글을 보면 셰익스피어의 향기가 물씬 풍긴다. 이 부분에 대한 소스 코드는 없지만, 관련 글이 있는 곳은 찾았다. (셰익스피어 보기)


리눅스 소스코드를 학습하면, 프로그래밍도 가능하다고 한다. 이번 내용은 셰익스피어와 같은 사이트에 있다.


N대N 모델의 소스코드가 있는 곳이다. sherjilozair이 만든 코드를 교수님께서 수정하셨다고 했다.

원본 코드  https://github.com/sherjilozair/char-rnn-tensorflow
수정 코드  https://github.com/hunkim/word-rnn-tensorflow


교수님께서 시를 쓰는 모델을 만드셨다. 페이지에 들어가면 몇 가지 문장을 선택하라고 한다. 말이 되거나 좋아보이는 것을 선택하면, 그 내용으로 학습해서 신춘문예에 내신다고 한다. 직접 좋아하는 시를 입력해서 학습시킬 수도 있다. (신춘문예 2017 후보 시봇)

시봇 알고리즘에 기여하고 싶거나 소스코드가 보고 싶다면 아래 링크로 간다.

  https://github.com/DeepLearningProjects/poem-bot


N대1 모델의 소스코드가 있는 곳의 주소다. (07_lstm.py) mnist 손글씨 이미지를 LSTM 모델을 사용해서 분류하는 코드를 볼 수 있다. 그런데, 이곳은 이전 동영상에서 다운 받아야 한다고 알려준 적이 있는 곳이다.


RNN으로 할 수 있는 다양한 영역을 보여준다. 프로그래머가 아닌 일반인들이 훨씬 좋아할 만한 주제들이다.


다음 동영상에 대해 간단하게 설명하셨다. 아마존 웹서비스를 사용해서 머신러닝을 할 수 있다고.

41. NN의 꽃 RNN 이야기 (lec 12)


Neural Network의 꽃이라고 불리는 RNN(Recurrent Neural Network)에 대해서 소개하는 동영상이다.


RNN은 sequence data를 처리하는 모델이다. sequence는 순서대로 처리해야 하는 것을 뜻하고, 이런 데이터에는 음성인식, 자연어처리 등이 포함된다. 자연어의 경우 단어 하나만 안다고 해서 처리될 수 없고, 앞뒤 문맥을 함께 이해해야 해석이 가능하다.

얼마나 배웠는지는 모르지만, 지금까지 열심히 공부한 neural network이나 convolutional neural network으로는 할 수 없다고 하신다.


순차적으로 들어오는 입력을 그림으로 표현하면 위와 같이 된다. 똑같은 모양이 오른쪽으로 무한하게 반복된다. 이걸 간단하게 표현하려고 하니까, 그림 왼쪽과 같은 형태가 된다. A에서 나온 화살표가 다시 A로 들어간다. 프로그래밍에서는 이런 걸 재진입(re-enterence) 또는 재귀(recursion)라고 부른다. 이쪽 세계에서는 recurrent라는 용어를 쓰고, "다시 현재"라는 뜻이고 정확하게는 "되풀이"라고 해석한다.

RNN에서 가장 중요한 것은 상태(state)를 갖는다는 것이고, 여기서는 ht로 표현하고 있다. 반면 매번 들어오는 입력은 Xt라고 표현한다. h는 상태를 가리키지만, 동시에 hidden layer를 가리키는 뜻도 있기 때문에 약자로 h를 사용한다.


RNN(Recurrent Neural Network)은 일반적으로 step을 거칠 때마다 어떤 결과를 예측하게 된다. 그리고, 이런 예측 값을 앞에서 배웠던 것처럼 y라고 부른다. y = Wx + b라고 생각하면 된다. 이 그림에서는 예측 값인 y를 표현하는 대신 상태를 의미하는 h는 표시하지 않고 있다. 뒤쪽에 가면 y와 h가 함께 표시된 그림을 볼 수 있다.


공식으로 표현하면 위와 같이 된다. 이전 단계에서의 상태 값과 입력 벡터(x)로 계산하면 새로운 상태 값이 만들어진다. 코드로 표현하면 아래와 같이 된다. 여기서 노드 갯수는 layer에 포함된 노드(그림에서는 초록색으로 표시된 RNN) 갯수를 말한다.

  for _ in range(노드 갯수):
      현재 상태 = W에 대한 함수(이전 상태, 입력 벡터)


바닐라(vanilla)는 아무 것도 첨가하지 않은 처음 상태의 아이스크림을 의미한다. 여기에 초코나 딸기 시럽을 얹고 땅콩 가루를 뿌리는 등의 옵션을 추가하면 맛이 더 좋아진다. 바닐라는 아무 것도 가공하지 않은 처음 형태로, 바닐라 RNN은 가장 단순한 형태의 RNN 모델을 뜻한다.

t는 sequence data에 대한 특정 시점의 데이터를 가리키고, 여기서는 t에 대해 두 가지를 계산한다. 첫 번째 줄의 공식은 W의 이전 상태와 입력을 갖고 fw에 해당하는 tanh 함수를 호출하는 것이다.

ht는 현재 상태를 의미하고 h의 t-1번째는 이전(old) 상태와 입력 값(x)을 사용해서 계산한다. yt는 예측 값을 의미하고, W와 현재 상태(ht)를 곱해서 계산한다.


같은 함수에 대해 같은 입력이 매번 똑같이 사용되고 있다고 강조한다. step마다 다른 값이 적용되는 것이 아니다.


글자를 다루는 RNN을 문자 기반의 언어 모델(Character-level Language Model)이라고 부른다. 일반적으로 language model은 출력 결과로 단어와 같은 글자를 예측하는 모델이라고 얘기한다. 여기서는 4가지 종류의 글자 h, e, l, o가 있고, 연속된 sequence인 "hello"에 대해 이후에 나올 값을 예측하려고 한다.


4가지 종류의 글자가 있기 때문에 크기가 4인 벡터(리스트)로 처리한다. multi-nomial classification에서 봤던 것처럼 4가지 중에서 하나를 선택하게 하려고 한다. 몇 번째 값이 켜졌느냐에 따라 순서대로 h, e, l, o가 된다. 이번 그림에는 'o'가 없다.

공식에서 보여주는 것처럼 h의 값과 x의 값을 W와 계산한 다음 tanh 함수에 전달하면 hidden layer에서 보여주는 값이 차례대로 만들어 진다. 매번 계산이 끝날 때마다 새로운 상태를 가리키는 hidden layer의 값이 바뀌는 것을 볼 수 있다.

tanh 함수는 sigmoid 함수 중의 하나로 처음 나왔던 sigmoid를 개량한 버전이다. 기존의 sigmoid가 0에서 1 사이의 값을 반환하는데, tanh 함수는 -1에서 1 사이의 값을 반환하도록 개량했다. 현재 상태를 가리키는 ht는 tanh 함수의 반환값이므로 -1과 1 사이의 값이 된다. 그래서, hidden layer에 있는 값들도 해당 범위에 존재하게 된다.


최종적으로는 모든 단계에서 값을 예측하고 실제 값과 맞는지 비교할 수 있다. "hello"가 들어왔다면, "hell"에 대한 예측은 "ello"가 되어야 하지만, h가 입력된 첫 번째 예측에서는 o가 나왔기 때문에 틀렸다. [1.0, 2.2, -3.0, 4.1]은 네 번째가 가장 크기 때문에 one-hot encoding을 거치면 o가 된다. e를 예측했어야 했다. 나머지는 정확하게 예측을 하고 있다.

마지막으로 정리해 본다. RNN의 핵심은 상태와 예측에 있다. 상태를 가리키는 값은 hidden layer에 있고 매번 바뀐다. 예측을 가리키는 값은 output layer에 있고 매번 예측한다.

위의 그림은 학습 중의 상황을 보여주기 때문에 75%의 정확도를 보여주고 있다. 학습이 종료됐다면, 잘못 예측한 첫 번째 노드까지 정확하게 예측해야 한다.


RNN을 통해서 할 수 있는 것들을 정리해 주셨다.


첫 번째 그림으로 바닐라 RNN을 가리킨다. 가장 단순한 형태로 1대1(one-to-one) 기반의 모델이다.


1대다(one-to-many) 기반의 모델로 이미지에 대해 설명을 붙일 때 사용한다. 한 장의 그림에 대해 "소년이 사과를 고르고 있다"처럼 여러 개의 단어 형태로 표현될 수 있다.


다대1(many-to-one) 형태의 모델로 여러 개의 입력에 대해 하나의 결과를 만들어 준다. 우리가 하는 말을 통해 우리의 심리 상태를 "안정", "불안", "공포" 등의 한 단어로 결과를 예측할 때 사용된다. sentiment는 감정을 의미한다.


다대다(many-to-many) 형태의 모델로 기계 번역에서 사용된다. 여러 개의 단어로 구성된 문장을 입력으로 받아서 여러 개의 단어로 구성된 문장을 반환한다. 구글 번역기 등이 이에 해당한다.


다대다(many-to-many) 모델의 또 다른 형태다. 동영상같은 경우는 여러 개의 이미지 프레임에 대해 여러 개의 설명이나 번역 형태로 결과를 반환하게 된다.


RNN도 여러 개의 layer를 두고 복잡한 형태로 구성할 수 있다. 위의 여러 가지 그림들을 다단계로 배치한 형태라고 보면 된다.


RNN 또한 layer가 많아지면서 복잡해지기 때문에 이를 극복할 수 있는 다양한 방법들이 소개되고 있다. 현재는 RNN이라고 하면 많은 경우 LSTM을 의미한다. 그리고, LSTM처럼 많이 사용되는 방법으로 대한민국의 조교수님께서 만든 GRU도 있다.

40. ConvNet을 TensorFlow로 구현하자 (MNIST 99%) (lab 11)


앞서 설명했던 3개 동영상에 대한 실습 동영상이다.


교수님께서 올려 놓으신 소스코드가 있는 웹사이트 주소. 오른쪽 하단의 주소를 입력해서 다운로드하면 된다. 이번 동영상에 나온 코드 외에도 많이 있다.

  https://github.com/nlintz/TensorFlow-Tutorials


CNN(Convolutional Neural Network) 구성도. conv 2개, pooling 2개, fc의 형태로 만들어 졌다.


이전 동영상에 나왔던 그림을 코드로 어떻게 구현하는지 보여준다. conv2d 함수의 설명은 아래와 같다.

  tf.nn.conv2d(input, filter, strides, padding, use_cudnn_on_gpu=None, data_format=None, name=None)

  input : [batch, in_height, in_width, in_channels] 형식. 28x28x1 형식의 손글씨 이미지.
  filter : [filter_height, filter_width, in_channels, out_channels] 형식. 3, 3, 1, 32의 w.
  strides : 크기 4인 1차원 리스트. [0], [3]은 반드시 1. 일반적으로 [1], [2]는 같은 값 사용.
  padding : 'SAME' 또는 'VALID'. 패딩을 추가하는 공식의 차이. SAME은 출력 크기를 입력과 같게 유지.

3x3x1 필터를 32개 만드는 것을 코드로 표현하면 [3, 3, 1, 32]가 된다. 순서대로 너비(3), 높이(3), 입력 채널(1), 출력 채널(32)을 뜻한다. 32개의 출력이 만들어진다.


출력 채널을 부르는 용어는 activation map이다. 물음표로 처리되어 크기를 알 수 없는데, 원본 이미지와 똑같은 28이 된다. SAME 옵션을 사용해서 패딩을 줬으니까.


convolutional layer를 적용한 결과를 ReLU에 전달하고 있다. 그냥 매개변수로 전달하면 끝이다.


여러 개 중에서 하나를 선택하는 것을 pooling이라고 한다. 다른 말로는 sampling이라고도 한다.

  tf.nn.max_pool(value, ksize, strides, padding, data_format='NHWC', name=None)

  value : [batch, height, width, channels] 형식의 입력 데이터. ReLU를 통과한 출력 결과가 된다.
  ksize : 4개 이상의 크기를 갖는 리스트로 입력 데이터의 각 차원의 윈도우 크기.
  data_format : NHWC 또는 NCHW. n-count, height, width, channel의 약자 사용.

ksize가 [1,2,2,1]이라는 뜻은 2칸씩 이동하면서 출력 결과를 1개 만들어 낸다는 것이다. 다시 말해 4개의 데이터 중에서 가장 큰 1개를 반환하는 역할을 한다.


Max Pooling에 대한 그림이 다시 나왔다. 4개의 칸에서 1개를 추출하고 있다.


여러 개의 변수를 사용하는 과정에서 헷갈리는 것이 당연하다. 이럴 경우 tensorflow에게 물어보는 것이 좋다고 말씀하셨다. print 함수로 출력하면 알려준다.


reshape 함수에 대해 설명하셨다. trX라는 변수의 형태를 4차원으로 변환하고 있다. -1은 갯수가 정해지지 않아서 모를 경우에 사용한다.

max_pool 함수에도 padding 옵션을 사용하고 있다. 앞의 설명에서 필터와 stride만으로 크기가 결정되는 줄 알았는데, 실제로는 크기가 바뀔 수 있다. l3a에서 결과는 [?, 7, 7, 128]인데, max pooling에서 나누어 떨어지지 않기 때문에 처리할 수 없다. 이때 padding 옵션이 들어가서 나누어 떨어지도록 패딩을 추가하게 된다.


앞의 코드에 dropout을 살짝 추가했다. 기존 코드를 수정하지 않고, 중간에 넣으면 끝.


이전 코드에서 마지막 max pooling의 결과는 [?, 4, 4, 128]이었다. 이것을 FC로 구현하려면 Wx + b에 맞게 W를 구성해야 한다. 5x5x3의 필터가 75개의 feature를 갖는 것처럼, 4x4x128만큼의 feature가 필요하다. w4의 출력 결과는 625개로 설정했고, 다음 번 layer에서는 625를 10개로 축소했다. 10개는 0부터 9까지의 숫자 label을 뜻한다.

이번 코드에서는 layer가 3개 사용됐다. l3와 l4는 hidden layer, pyx는 output layer. l3는 [?, 2048]이고, l4는 [2048, 625], pyx는 [625, 10]이 된다. FC에서 이전 출력 크기와 다음 입력 크기가 같아야 행렬 곱셈을 할 수 있기 때문에 숫자가 중복되고 있다.


cost를 계산하는 것은 이전 동영상에서 봤던 그 함수다. 달라진 점은 RMSOptimizer 함수 사용에 있다. RMSProp 알고리듬을 구현하고 있는 함수다. 0.001은 learning rate, 0.9는 learning rate을 감소시킬 매개변수(decay)를 뜻한다.


tensorflow에서 제공하는 다양한 optimizer에 대해 보여준다. 시간이 허락하면 모두 사용해 보고 언제 어떤 성능을 내는지 이해해야 한다.


128개씩 끊어서 전달하는 코드를 보여준다. 전체적으로 100번 반복하고 있다.


결과는 놀랍다. 무려 99.2%의 정확도를 자랑한다. 더욱이 왼쪽이 반복 횟수를 의미한다면 5번만에 찾았다는 것인데.. 실제 코드를 돌려본 결과는 여기 출력과는 차이가 있다. 아래에서 다시 설명한다.

교수님께서 올려놓으신 코드. 전혀 수정하지 않았다. 소스 코드에 대한 설명은 앞에서 했으므로 생략한다. 참, 이 코드를 구동하려면 구글에서 배포한 input_data.py 파일도 필요하다. 앞에서 언급한 사이트에 있다. 아니면 모듈 import 위치에서도 확인할 수 있다. tensorflow 모듈의 examples 폴더 아래에 tutorials/mnist 폴더가 실제로 존재한다.


import tensorflow as tf
import numpy as np
from tensorflow.examples.tutorials.mnist import input_data

batch_size = 128
test_size = 256

def init_weights(shape):
return tf.Variable(tf.random_normal(shape, stddev=0.01))

def model(X, w, w2, w3, w4, w_o, p_keep_conv, p_keep_hidden):
l1a = tf.nn.relu(tf.nn.conv2d(X, w, # l1a shape=(?, 28, 28, 32)
strides=[1, 1, 1, 1], padding='SAME'))
l1 = tf.nn.max_pool(l1a, ksize=[1, 2, 2, 1], # l1 shape=(?, 14, 14, 32)
strides=[1, 2, 2, 1], padding='SAME')
l1 = tf.nn.dropout(l1, p_keep_conv)

l2a = tf.nn.relu(tf.nn.conv2d(l1, w2, # l2a shape=(?, 14, 14, 64)
strides=[1, 1, 1, 1], padding='SAME'))
l2 = tf.nn.max_pool(l2a, ksize=[1, 2, 2, 1], # l2 shape=(?, 7, 7, 64)
strides=[1, 2, 2, 1], padding='SAME')
l2 = tf.nn.dropout(l2, p_keep_conv)

l3a = tf.nn.relu(tf.nn.conv2d(l2, w3, # l3a shape=(?, 7, 7, 128)
strides=[1, 1, 1, 1], padding='SAME'))
l3 = tf.nn.max_pool(l3a, ksize=[1, 2, 2, 1], # l3 shape=(?, 4, 4, 128)
strides=[1, 2, 2, 1], padding='SAME')
l3 = tf.reshape(l3, [-1, w4.get_shape().as_list()[0]]) # reshape to (?, 2048)
l3 = tf.nn.dropout(l3, p_keep_conv)

l4 = tf.nn.relu(tf.matmul(l3, w4))
l4 = tf.nn.dropout(l4, p_keep_hidden)

pyx = tf.matmul(l4, w_o)
return pyx

mnist = input_data.read_data_sets("MNIST_data/", one_hot=True)
trX, trY, teX, teY = mnist.train.images, mnist.train.labels, mnist.test.images, mnist.test.labels
trX = trX.reshape(-1, 28, 28, 1) # 28x28x1 input img
teX = teX.reshape(-1, 28, 28, 1) # 28x28x1 input img

X = tf.placeholder("float", [None, 28, 28, 1])
Y = tf.placeholder("float", [None, 10])

w = init_weights([3, 3, 1, 32]) # 3x3x1 conv, 32 outputs
w2 = init_weights([3, 3, 32, 64]) # 3x3x32 conv, 64 outputs
w3 = init_weights([3, 3, 64, 128]) # 3x3x32 conv, 128 outputs
w4 = init_weights([128 * 4 * 4, 625]) # FC 128 * 4 * 4 inputs, 625 outputs
w_o = init_weights([625, 10]) # FC 625 inputs, 10 outputs (labels)

p_keep_conv = tf.placeholder("float")
p_keep_hidden = tf.placeholder("float")
py_x = model(X, w, w2, w3, w4, w_o, p_keep_conv, p_keep_hidden)

cost = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(py_x, Y))
train_op = tf.train.RMSPropOptimizer(0.001, 0.9).minimize(cost)
predict_op = tf.argmax(py_x, 1)

# Launch the graph in a session
with tf.Session() as sess:
# you need to initialize all variables
tf.initialize_all_variables().run()

for i in range(100):
training_batch = zip(range(0, len(trX), batch_size),
range(batch_size, len(trX)+1, batch_size))
for start, end in training_batch:
sess.run(train_op, feed_dict={X: trX[start:end], Y: trY[start:end],
p_keep_conv: 0.8, p_keep_hidden: 0.5})

test_indices = np.arange(len(teX)) # Get A Test Batch
np.random.shuffle(test_indices)
test_indices = test_indices[0:test_size]

print(i, np.mean(np.argmax(teY[test_indices], axis=1) ==
sess.run(predict_op, feed_dict={X: teX[test_indices],
Y: teY[test_indices],
p_keep_conv: 1.0,
p_keep_hidden: 1.0})))
[출력 결과]
0 0.94140625
1 0.97265625
2 0.98828125
3 0.98828125
4 0.99609375
5 0.99609375
6 0.99609375
7 0.98828125
8 1.0
9 0.98828125
...
95 0.9921875
96 1.0
97 0.99609375
98 0.9921875
99 0.9921875

출력 결과를 보면 1.0 부근에서 왔다갔다 한다. 이런 상황이 100번 반복할 때까지 계속된다. 어떻든지간에 결과는 99.2% 이상 나온다. 소요 시간은 너무 길어서 측정할 생각도 하지 않았는데, 1시간 이상 걸리는 걸로 판단. 이전에 수행해 봤던 비슷한 코드가 그만큼 나왔으니까.

얼마 전 구매한 GTX1060 그래픽카드를 사용하면 대략 2분에서 3분 정도 걸린다. CPU로는 50분 걸릴 때도 있고 1시간 30분 걸릴 때도 있었다. 보급형 GPU만으로도 너무 잘 나와서 기분이 좋았다. 다만 GPU를 지원하는 텐서플로우 설치는 좀 피곤하다.

39. ConvNet의 활용예 (lec 11-3)


이번 동영상에서는 CNN이 발전해 온 과정에 대해 구체적인 사례를 들어 설명한다.


CNN의 고전이라고 부를 수 있는 LeNet-5이다. LeCun 교수님께서 1998년에 만드신 모델로, 이후에 나오는 모델에 비해 굉장히 단순한 형태이다. 6개의 hidden layer를 사용하고 있다.

1. Input - 크기 32x32x1. 흑백 이미지. (필터 5x5, stride 1)
2. C1 - 크기 28x28x6. feature maps 6개. (필터 2x2, subsampling)
3. S2 - 크기 14x14x6. feature maps 6개. (필터 5x5, stride 1)
4. C3 - 크기 10x10x16. feature maps 16개
5. S4 - 크기 5x5x16. feature maps 16개.
6. C5 - FC(Full Connection )120개
7. F6 - FC(Full Connection) 84개
8. Output - GC(Gaussian Connections) 10개. (최종 분류)


softmax에서 좋은 결과를 내기 위한 두 가지 방법이 있었다. Deep & Wide. layer를 여러 개 만드는 deep과 하나의 layer에 노드를 많이 만드는 wide.


Deep Learning계의 전설이 된 AlexNet이다. 불과 몇 년 전인데, 벌써 전설이라 불러도 어색하지 않다. 이쪽 세계에서는 시간이 빨리 흐른다.

원본 그림의 크기가 227x227의 정사각형 형태이고, RGB의 3가지 색상으로 구성된다. 앞에 나왔던 32x32의 흑백 이미지에 비해 대략 150배 용량이 증가했다. 필터의 크기 또한 11x11로 엄청나게 커졌다. 그렇다면 W의 갯수는 11x11x3이니까 363개나 된다. 와우.. 이런 필터를 96개나 만들었다. 그럼 전체 입력의 갯수는 363x96으로 34,848(35k)개가 된다. 이러니 며칠씩 걸리면서 계산을 하는 것이 맞다.

  (227 - 11) / 4 + 1 = 216/4 + 1 = 54+1 = 55

출력 결과의 크기는 위의 공식으로 계산할 수 있다. 필터의 갯수는 출력 layer 갯수와 동일해야 하니까, 전체 크기는 55x55x96이 된다.

갑자기 96이 뭐지? 라는 생각이 들었다면, 이전 글을 보고 오자. 필터의 갯수는 색상처럼 생각하면 된다고 매우 강조했다. 픽셀을 구분하기 위해 96가지 특징이 있는 셈이다.


두 번째 layer에 대해 설명한다. convolutional layer가 아니라 pooling layer를 적용했다. pooling layer도 stride를 지정할 수 있다. (55 - 3) / 2 + 1은 27이 된다. 단순하게 가장 큰 값을 찾는 Max Pooling을 적용했으므로 필터 갯수였던 96은 바뀌지 않고 입력 또한 없다. Max Pooling을 사용했다는 표시는 네트워크 구성도에 쓰여 있다.


전체 네트워크를 어떤 방식으로 구성했는지 보여준다. 이 정도면 할만 하겠다는 생각이 든다.

그림 오른쪽 설명에서 ReLU를 사용한 첫 번째 모델이라고 되어 있다. dropout도 사용했고 앙상블(ensemble)까지 적용하고 있다. 앞에서 배운 모든 기술을 적용했다는 것을 알 수 있다. 그래서, 좋은 성능이 나왔고, 사람들이 놀랬다.

그런데, ImageNet 대회가 처음 있었던 것도 아닌데, 15.4%의 에러에 사람들이 반응했다는 것이 어색할 수도 있다. 그 이전에는 27%의 에러였고, 12% 정도의 성능 향상이 있었는데, 기존 기술로도 충분히 가능하다고 생각한다. 개인적으로는 AlexNet의 놀라운 점은 hinton 교수님의 학생이었고, 발전할 가능성이 많은 모델이었기 때문이라고 생각한다. 기존의 머신러닝 기술은 사실상 한계에 도달했었고 새로운 돌파구가 필요했을 수도 있다. 마치 deep learning에 찾아왔던 겨울(winter)처럼. 어찌 됐건 그 이후로 AlexNet을 바탕으로 한 새로운 network이 매년 출시되고 있고, 계속적인 성능 향상이 일어나고 있는 중이다.


2013년의 자료는 없다. 2014년에는 놀랍게도 구글에서 출전했고, 이름 값을 했다. GoogleNet이 아니라 GoogLeNet이다. 1998년에 만들어진 LeNet을 기념하기 위해 붙인 이름인 듯.

inception module이라는 새로운 이론을 적용했다. inception의 뜻은 처음 또는 시초, 발단이라고 되어 있다. 아무도 만들어 본 적이 없기 때문일까?

앞에서 본 AlexNet에 비해 많이 깊어졌다. 그러나, 많이 넓어지지는 않았다. layer에 포함된 파란 사각형의 갯수가 많아야 5개에 불과하다. 특이한 점은 모델의 처음과 끝에 FC layer가 적용됐다는 점이다. CNN에서는 보통 마지막에만 둔다. convolutional layer와 pooling layer를 이상한 조합으로 연결한 점이 특별하고, 목표까지 가기 위한 layer가 많지 않은 점도 특별하다. 그림 아래에 있는 inception module에는 hidden layer에 포함된 노드(or layer)가 7개 있는데, 최대 2개만 거치면 목표까지 갈 수 있다. 거쳐야 하는 1개 또는 2개는 반복할 때마다 달라질 것이고, 역시 균형 잡힌 결과를 얻게 된다.

인터넷에서 관련 논문을 잠시 찾아봤다. 놀랍게도 저자가 10명이 넘었다. 그냥 참가한 사람은 없을테니, 모델을 만드는 것이 얼마나 어려운 것인지.. 짐작할 수 있다. 교수님께서 우리도 할 수 있다고 용기를 주시고 있긴 한데.. 진짜 가능한지 모르겠다.


2015년에 나온 따끈따끈한 사례다. 2015년에 개최된 여러 대회에서 놀라운 성적으로 우승했음을 보여주고 있다. 2등과의 격차가 장난 아니다. 그런데, 이런 정도의 격차라면 2014년의 GoogLeNet보다 못한 2등이다. 왜 그랬을까?


해를 거듭할수록 layer의 갯수가 기하급수적으로 늘어나고 있다. 무려 152개의 layer를 사용했다. 그림 오른쪽에 2~3주 동안 8개의 GPU로 학습했다고 설명하고 있다. 이번에 큰 맘 먹고 장만한 내 컴퓨터는 GPU가 1개밖에 없다.

그런데, 정말 놀라운 것이 8배 이상의 layer로 인해 학습은 오래 걸렸지만, 실전 적용에서는 VGGNet보다 빠르다고 말하고 있다. VGGNet은 2014년에 ImageNet 대회에 출전했던 모델로 네트웍의 깊이가 성능과 중요한 관련이 있음을 보여줬다. 당시 16개의 CONV/FC layer만을 사용해서 놀라운 성적을 거두었던 모델이다.

어떻게 152개의 layer를 갖는 모델이 16 layer 모델보다 빠를 수 있을까?


좋은 성능을 위한 방법을 보여준다. 이상하게 보일 수 있지만, 모든 layer를 거치는 것이 아니라 일부 layer만 거치는 것이 핵심이다.

ResNet은 Residual Network의 약자로 residual은 '남아 있는' 정도의 뜻으로 쓰이는데, '계산이 설명되지 않는' 등의 뜻 또한 갖고 있다. 교수님께서 설명하실 때, '왜 그런지는 모르지만'이라고 말씀하셨는데, 그런 의미를 준 것일지도 모르겠다.


교수님께서는 이것을 fast net이라고 말씀하셨다. 여러 layer를 건너 뛰어서 목표에 빨리 도착할 수 있기 때문에 fast라고 설명하셨다. 건너뛰는 갯수를 난수로 처리하면, 반복할 때마다 다른 결과를 얻을 수 있고, 역시 균형 잡힌 결과를 구할 수 있게 된다.


ResNet과 GoogLeNet의 공통점을 보여주고 있다. 모양은 다르지만, 목표까지 가기 위해 거치는 layer의 갯수가 적다. ResNet은 일렬로 배치해서 건너 뛰도록 설계했고, GoogLeNet은 처음부터 길을 여러 개 만들었다.

GoogLeNet의 단점은 경로가 여러 개이긴 하지만, 특정 경로로 진입하면 경로에 포함된 모든 layer를 거쳐야 한다는 점이 아닐까 싶다. ResNet이 매번 통과할 때마다 거치는 layer의 구성이 달라지는 반면 GoogLeNet은 구성이 같기 때문에 다양한 결과를 만들지 못하는 것이 단점으로 보인다. ResNet이 GoogLeNet보다 더욱 균형 잡힌 결과를 만들어 낼 수 있는 구조라고 보여진다.


2014년에 발표한 문장 분석을 위한 CNN. 한국인 윤김 교수님께서 만드셨다. 많이 사용하는 모델이라고 말씀도 하셨다.


설명할 필요가 없는 사진, 알파고. 그림 오른쪽에는 알파고가 국면을 해석하는 단계를 순서대로 보여주고 있다. 여기에도 CNN이 사용됐다고 한다.


국제적인 학술지 Paper에 실린 알파고에 대한 설명.

19x19x48 이미지 스택을 사용하고 있다. 바둑은 19x19개의 칸으로 구성되어 있다. 즉, 바둑판 이미지를 읽을 지는 모르지만, 19픽셀로 재구성하고 있음을 알 수 있다. 이후 이미지 크기를 패딩을 적용해서 23x23으로 바꾸었다고 설명하고 있다. 48개의 feature planes(채널)를 사용했다는 것은 바둑돌이 하나 놓여질 때마다 해당 돌의 특징을 48가지로 판단했음을 뜻한다. 가령, 흑돌인지 백돌인지, 주변에 흑돌이 있는지 백돌이 있는지, 현재 정세가 어떠한지 등등. 바둑을 조금밖에 모르기 때문에 48가지의 특징을 짐작조차 할 수 없지만, 아무리 생각해도 48가지라는 것은 너무 엄청나다. 그랬기 때문에 이세돌을 이길 수 있었던 것 같다.

앞에서 배운 지식을 사용하면 대략이나마 어떻게 구성했는지 알 수 있다. 어렵긴 하지만, 한 발 나아간 것 같아 다행이다.

38. ConvNet Max pooling 과 Full Network (lec 11-2)


이번 동영상에서는 CNN을 구성하는 핵심 요소인 pooling과 fully connected에 대해 설명한다.


보기만 해도 머리 아픈 CNN 구성도. 개인적으로 이 그림에서 중요하게 볼 것은 자동차 그림이 뒤로 갈수록 애매해 진다는 사실. 작긴 하지만, 앞쪽에 위치한 자동차는 선명한 반면, 뒤쪽의 자동차는 알아볼 수 없다.

이와 같이 선명한 이미지를 애매하게 만들어 그룹으로 구분하는 것이 CNN의 핵심이 아닐까 생각한다. 그래야 처음에는 달랐지만, 유사했던 이미지를 '같다'라고 판단할 수 있게 된다.


pooling의 다른 말은 sampling 또는 resizing이다. pooling은 모은다는 뜻이고, sampling은 고른다는 뜻이고, resizing은 크기를 바꾼다는 뜻이다. 어떤 작업을 하건 pooling을 거치면 크기가 작아진다는 것이 핵심이다. 이번 그림에서는 convolutional layer에 사용된 그림이 1/2 정도로 작아졌다는 것을 보여준다. activation map이라고 불렀던 채널(channel)의 두께는 바뀌지 않는다.


여러 가지 pooling 기법 중에서 가장 많이 사용하는 Max Pooling에 대해 설명한다. 여러 개의 값 중에서 가장 큰 값을 꺼내서 모아 놓는 것을 뜻한다.

convolutional layer에서는 필터(W)를 곱해서 새로운 출력 결과를 만들어 낸다. 그러나, pooling에서는 단순하게 존재하는 값 중에서 하나를 선택한다. 여기서도 필터를 사용한다고 하는데, 앞에 나온 필터와 헷갈리면 안 된다. pooling에는 최소값을 선택하거나 평균이나 표준편차를 계산하는 등의 다양한 방법들이 있고, CNN에서는 가장 큰 값을 선택하는 Max Pooling을 주로 사용한다.

4x4로 이루어진 출력 결과에 대해 2x2 필터를 사용해서 4개 영역으로 구분한다. 필터가 이동하는 크기인 stride는 필터 크기와 동일하게 2로 설정했다. 그래서, 결과는 2x2가 된다. 필터가 가리키는 영역에서 가장 큰 값을 선택했다.


CNN 이전까지 배운 형태의 layer를 FC(Fully Connected) layer라고 부른다. 말 그대로 처음부터 끝까지 하나로 연결되어 있다는 뜻이다. 이번 그림에서는 최종 결과물이 5개 중의 하나이므로 5개 중에서 하나를 선택하는 softmax가 들어가게 된다.


CNN 코드가 동작하는 것을 시각적으로 보여주고 있는 사이트를 알려 주셨다. 정말 눈여겨 봐야 할 것이 위에 있는 그림처럼 convolutional layer를 거칠 때마다 그림이 거칠어 지는 것이다. 실제 거칠어지는 모습을 시각적으로 확인할 수 있다. 그림에 나온 웹사이트는 그림이어서 클릭할 수 없다. 속지 말자!

  http://cs.stanford.edu/people/karpathy/convnetjs/demo/cifar10.html


37. ConvNet의 Conv 레이어 만들기 (lec 11-1)


여기부터 Deep Learning의 후반부라고 볼 수 있다. 전체 동영상을 공부한 이후에도 할게 많지만, 이전까지는 기초, 여기부터는 중급이 된다.


이렇게 어려운 내용을 1959년부터 누가 연구했다. 고양이가 사물을 볼 때의 뇌를 연구해 보니까, 뇌 전체가 아닌 일부만 활성화된다는 사실을 발견했다.


CNN(Convolutional Neural Network)이 얼마나 복잡한지 보여주셨다. CONV와 RELU가 한 쌍으로 구성되고 중간중간 POOL이 들어가 있다. 마지막에는 FC가 있다. POOL은 pooling(sampling, resizing)을 말하고, FC는 fully connected network을 말한다. 이렇게 여러 장의 layer를 연결하고 나니까, deep learning처럼 보인다.


몇 장의 비슷한 그림이 계속 등장한다. 그림 크기는 32x32이고 색상을 갖고 있기 때문에 3(red, green, blue)을 곱했다. 픽셀(pixel, 화소) 1개는 흑백의 경우 1바이트를 사용하기 때문에 256단계의 gray scale을 표현할 수 있고, 컬러의 경우 RGB에 1바이트씩 할당하기 때문에 256x256x256만큼의 색상을 표현할 수 있다.


그림 일부를 덮는 필터가 있다. 여기서 중요한 것은 색상에 해당하는 3은 항상 같아야 한다는 점이다. 색상을 1로 지정하는 것은 RGB 중에서 빨강에 해당하는 요소만 처리하겠다는 뜻이다. 특별한 경우에는 필요하겠지만, 일반적으로는 달라야 할 이유가 없으니까, 항상 똑같은 값으로 지정한다. 뒤에서 나오는데, input layer의 색상은 hidden layer의 filter와 같은 개념이다. 그래서, "filter가 3개 있다"라고 얘기해도 된다.


필터가 차지하는 영역으로부터 읽어온 값을 공식을 통해 하나의 값으로 변환한다. Wx+b는 지속적으로 등장하는 공식인데, 여기서 W와 x는 누구를 가리키는 것일까?

W는 weight에 해당하는 값으로 여기서는 필터(filter)에 해당한다. W를 찾는 것이 deep learning의 목표라고 한다면, 올바른 필터를 찾는 것이 CNN의 목표라고 할 수 있다. x는 외부에서 읽어온 값으로 이 값은 바뀌지 않는다. 여기서는 이미지에 해당한다.

one number에 해당하는 값을 얻기 위해 Wx+b의 결과를 ReLU에 전달한다. sigmoid가 아니니까, 음수가 아닌 값을 1개 얻게 된다. 그렇다면, 5x5x3에 대해서 1개의 값을 얻었다면, 전체에 대해서는 몇 개의 값이 나올지 궁금할 것이다. 조금만 참자!


동일한 필터를 사용해서 다른 영역도 조사한다. 5x5x3 크기의 필터를 수평과 수직으로 움직인다. Wx에서 W는 동일한 layer에 대해 같은 값으로 계속 사용하고, x의 값을 모두 읽게 되면 layer 하나가 끝나게 된다. 그래서, 제목에 same filter(W)라고 되어 있다.


앞에 나온 그림과 똑같은데, 필터가 가리키는 곳이 바뀌었다.


수평과 수직에 대해 모두 반복해서 처리한다. 필터 영역에 대해 하나의 값으로 변환하는 작업. 그렇다면, 모두 반복했을 때 얻게 되는 값은 몇 개일까? 수평과 수직으로 움직이는 규칙을 알아야 계산할 수 있다. 지금은 모른다.

수평으로 4번, 수직으로 7번 이동했다면 4x7만큼의 숫자를 얻게 된다. 이 때, 32x32x3에 들어가 있는 3은 반영되지 않는다. 4x7x3이 되어야 한다고 생각할 수도 있다. 5x5x3으로 계산해서 one number를 얻기 때문에 3이 나올 수가 없다. 그리고, 색상(filter)의 갯수는 생략하면 계산이 쉽기 때문에 생략할 때도 있다.

5x5로 계산한다면 3번 계산해야 하므로 4x7x3이 되는 것이 맞지만, 우리는 5x5x3으로 계산했다. 계산에 사용한 4와 7은 몇 개가 나오는지 알려주기 위해 고른 숫자일 뿐이다. 의미를 부여하지 말자.


필터가 움직이는 규칙을 살펴보려고 한다. 필터의 크기는 그림에 있는 것처럼 3x3이다. 필터를 적용할 그림의 크기는 7x7이다. 색상 갯수는 무시한다.


만약에 필터를 한 칸씩 이동시킨다면 출력의 결과는 5x5가 된다. 왼쪽 끝에서 5번 이동하면 오른쪽 끝에 닿기 때문에 5번까지만 이동할 수 있다.


그러나, 굳이 한 칸씩 움직일 필요는 없다. 두 칸씩 움직여보자. 출력 크기는 3x3이 된다. 필터가 이동하는 크기를 stride라고 부른다. 잘 이해가 안 되면, 잠시 읽는 것을 멈추고 손으로 그려보길 바란다. 그러면 된다.

3x3의 출력이라는 것은 말그대로 가로세로 3픽셀 크기의 결과를 얻었다는 뜻이다. 필터의 크기 때문에 원본보다 작아질 수밖에 없는 구조다. 1x1 크기의 필터를 사용하면 크기는 줄지 않겠지만, 이미지의 특성을 추출하기 어려울 수 있다. 뒤에서 1x1 크기의 필터를 사용하는 것과 비슷한 효과를 주는 방법이 나온다.


input, output, stride 사이에는 쉬운 규칙이 있다.

  출력 크기 = 1 + (입력 크기 - 필터 크기) / stride 크기

여기서 중요한 것은 나누어 떨어져야 한다는 점. 그림에서 stride 3을 적용한 공식은 2.33이 나오기 때문에 사용할 수 없다. 왼쪽 경계에서 시작해서 오른쪽 경계로 끝나야 사용할 수 있다. 자투리가 조금 있어도 될 수 있을 것 같은데, 허용하지 않는다. 미세한 차이지만, 정확도를 높일 수 있는 확실한 방법이라고 생각한다.


실전에서는 convolutinal layer를 거칠 때마다 크기가 작아지는 문제가 발생한다. stride 크기에 상관없이 최소한 (필터 크기-1)만큼 줄어들 수밖에 없다. 이번 그림에서는 원본 이미지의 크기가 줄어들지 않도록 padding을 얼마나 넣을지 설명한다. padding을 넣는 이유는 원본 이미지 크기를 유지하기 위함이다.

보통은 stride를 1로 하기 때문에 padding의 크기 또한 1이 된다. 그런데, 테두리 전체에 대해 추가되기 때문에 크기는 2만큼 증가한다.

padding의 두께는 정해져 있지 않다. 출력 결과가 원본과 같은 크기를 만들 수 있다면 얼마든지 가능하다. 그림을 보면 필터 크기가 3, 5, 7일 때 각각 1, 2, 3픽셀의 padding을 넣는다고 되어 있다. 3픽셀 padding을 넣으면, 양쪽으로 추가되기 때문에 결과적으로는 6픽셀이 추가된다.

필터 크기가 7이고 원본 크기가 7인 경우라면, padding을 포함한 전체 크기는 13이 된다. 7픽셀의 필터가 움직일 수 있는 횟수는 7번이 되고, 출력 결과의 크기는 7x7이 되어서 원본 크기와 같다. padding은 한쪽만을 얘기하므로 2로 나누었고, 양쪽으로 2개 들어가니까 2를 곱했다. 그래도 2로 나누었다 2로 다시 곱하니까 어색한 부분이 있다.

  출력 크기 = 원본 + (필터-1)/2 * 2 = 7 + (7-1)/2 * 2 = 13


32x32 그림 전체에 대해 출력을 계산한다. 그런데, 출력 결과를 하나가 아니라 여러 개로 중첩시킬 수 있다. 출력 결과는 필터를 거친 결과를 의미하므로, 결국 여러 개의 필터를 사용한다는 말이 된다.

처음에 이 부분이 매우 어려웠다. 5x5x3 크기의 필터를 수평과 수직으로 이동시켜서 전체를 순회하면 필터 1개가 만들어진다. 그런데, 필터라는 단어가 너무 무분별하게 쓰이는 것처럼 보인다. 5x5x3에서도 나오고 출력 결과에서도 나온다. 뒤에서 잠깐 언급하고 있는데, 5x5x3은 필터라고 하고 결과물은 채널(channel)이라고 부른다. 필터 1개에 채널 1개가 만들어진다. 여기서는 2개의 채널이 있으니까, 필터를 2개 사용했다. 필터는 weight의 역할을 하고 있으니까, 이 말은 여러 가지 가중치를 사용해서 여러 번의 결과를 만든다는 것과 같은 뜻이 된다. 계속적으로 등장하는 "균형 잡힌 결과"와 같은 맥락이다.


그림이 분명하지 않은데, 여기서는 6개의 필터를 사용하고 있다. 필터를 적용해서 만들어진 새로운 출력 결과를 activation map이라고 한다. 교수님이 28x28x6이라고 써놓으셨는데, 이해할 수 있어야 한다.

이전 그림에서 stride가 1이라고 했고, 필터 크기는 5x5이다. 그렇다면 activation map의 크기는 (32-5)/1 + 1의 공식에 따라서 28이 된다. 여기에 필터의 갯수 6이 들어간다.


convolutional layer를 거칠 때마다 조금씩 작아지고 있다. 앞에서 얘기한 padding을 추가했다면 크기가 작아지지 않았을 것이다. stride가 1이라고 가정하고 결과물에 대해 크기를 계산해 보자.

  (32 - 5)/1 + 1 = 28  (red layer)
  (28 - 5)/1 + 1 = 24  (yellow layer)

이번 동영상 마치기 전에 중요한 거 하나 얘기한다. 그림 상자의 두께가 점점 두꺼워지고 있다. convolutional layer를 거칠수록 크기는 작아지지만 두께는 두꺼워지는 것이 일반적이다. 작아진 크기를 두께로 보강한다고 볼 수도 있을 것 같다.

그런데, 두께는 무엇을 의미할까? 그냥 필터의 갯수라고 말하면 될까? 내 생각에는 색상으로 보면 쉬울 것 같다. TensorFlow 도움말에서는 채널(channel)로 설명한다.

첫 번째 상자에서 32x32x3인데, 3은 Red, Green, Blue의 값을 뜻한다. 같은 관점에서 색상은 아니지만, 두 번째 상자의 28x28x6에서 6은 색상과 같은 개념이다. 6개의 값이 모여서 1개의 픽셀을 구성하는 것이다.

이것이 색상이 아니어도 되는 것이 다른 필터에 있는 값들과 비교할 수만 있으면 된다. 중요한 것은 비용(cost)을 계산해서 얼마나 가까운지 판단하기만 하면 된다. 꼭 기억하자!

35. 딥러닝으로 MNIST 98%이상 해보기 (lab 10) 소스 코드

이번 동영상에서 교수님께서 여러 가지 코드를 말씀하셨다. 종류도 많고 코드도 길고 해서 따로 정리했다.

모든 코드의 learning_rate은 0.001로 고정시켰고, training_epochs 또한 15회로 고정시켰다. 교수님께서 말씀하신 부분만 수정했다. 다만 XavierForMNIST.py에서 동영상에 나오지 않은 코드가 일부 있다. bias를 초기화하는 부분인데, 동영상에는 없었다.

교수님 동영상을 보고 나처럼 정리하는 사람들이 많다. 도움을 받은 사이트를 소개한다. 아래 코드에 대한 설명은 이미 이전 글에서 했다. 그래도 부족한 사람은 아래 링크를 참고하기 바란다.

  MNIST 데이터 셋을 이용한 손글씨 인식 Deep Neural Network 구현


# NeuralNetworkForMnist.py : 94.57%

import tensorflow as tf

# Import MINST data
from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets("/tmp/data/", one_hot=True)

# Parameters. 반복문에서 사용하는데, 미리 만들어 놓았다.
learning_rate = 0.001
training_epochs = 15
batch_size = 100
display_step = 1

# tf Graph Input
X = tf.placeholder(tf.float32, [None, 784]) # mnist data image of shape 28*28=784
Y = tf.placeholder(tf.float32, [None, 10]) # 0-9 digits recognition => 10 classes

# --------------------------- 수정한 부분 ------------------------------ #
# Set model weights
W1 = tf.Variable(tf.random_normal([784, 256]))
W2 = tf.Variable(tf.random_normal([256, 256]))
W3 = tf.Variable(tf.random_normal([256, 10]))

B1 = tf.Variable(tf.random_normal([256]))
B2 = tf.Variable(tf.random_normal([256]))
B3 = tf.Variable(tf.random_normal([ 10]))

# Construct model
L1 = tf.nn.relu(tf.add(tf.matmul(X, W1), B1))
L2 = tf.nn.relu(tf.add(tf.matmul(L1, W2), B2)) # Hidden layer with ReLU activation
hypothesis = tf.add(tf.matmul(L2, W3), B3) # No need to use softmax here
# ---------------------------- 여기까지 ------------------------------- #

# Minimize error using cross entropy
cost = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(hypothesis, Y)) # softmax loss
# Gradient Descent
optimizer = tf.train.AdamOptimizer(learning_rate=learning_rate).minimize(cost)

# Initializing the variables
init = tf.initialize_all_variables()

# Launch the graph
with tf.Session() as sess:
sess.run(init)

# Training cycle
for epoch in range(training_epochs):
avg_cost = 0.
# 나누어 떨어지지 않으면, 뒤쪽 이미지 일부는 사용하지 않는다.
total_batch = int(mnist.train.num_examples/batch_size)
# Loop over all batches
for i in range(total_batch):
batch_xs, batch_ys = mnist.train.next_batch(batch_size)
# Run optimization op (backprop) and cost op (to get loss value)
_, c = sess.run([optimizer, cost], feed_dict={X: batch_xs, Y: batch_ys})

# 분할해서 구동하기 때문에 cost를 계속해서 누적시킨다. 전체 중의 일부에 대한 비용.
avg_cost += c / total_batch
# Display logs per epoch step. display_step이 1이기 때문에 if는 필요없다.
if (epoch+1) % display_step == 0:
print("Epoch:", '%04d' % (epoch+1), "cost=", "{:.9f}".format(avg_cost))

print("Optimization Finished!")

# Test model
correct_prediction = tf.equal(tf.argmax(hypothesis, 1), tf.argmax(Y, 1))
# Calculate accuracy
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
print("Accuracy:", accuracy.eval({X: mnist.test.images, Y: mnist.test.labels}))
[출력 결과]
Epoch: 0001 cost= 230.355805381
Epoch: 0002 cost= 44.542764827
Epoch: 0003 cost= 27.731916585
Epoch: 0004 cost= 19.474284099
Epoch: 0005 cost= 13.878197979
Epoch: 0006 cost= 10.265536548
Epoch: 0007 cost= 7.547600131
Epoch: 0008 cost= 5.628515447
Epoch: 0009 cost= 4.225564419
Epoch: 0010 cost= 3.117088286
Epoch: 0011 cost= 2.374554315
Epoch: 0012 cost= 1.842727151
Epoch: 0013 cost= 1.413149142
Epoch: 0014 cost= 1.093248269
Epoch: 0015 cost= 0.855855221
Optimization Finished!
Accuracy: 0.9457


정말로 아무 것도 하지 않고, 앞의 코드에 대해 초기값만 xavier 알고리듬을 사용했다. 전체 코드 대신 수정이 발생한 부분에 대해서만 코드를 표시했다.

# XavierForMnist.py : 97.78%

import tensorflow as tf

# --------------------------- 추가한 부분 ------------------------------ #
# http://stackoverflow.com/questions/33640581/how-to-do-xavier-initialization-on-tensorflow
def xavier_init(n_inputs, n_outputs, uniform=True):

if uniform:
# 6 was used in the paper.
init_range = tf.sqrt(6.0 / (n_inputs + n_outputs))
return tf.random_uniform_initializer(-init_range, init_range)
else:
# 3 gives us approximately the same limits as above since this repicks
# values greater than 2 standard deviations from the mean.
stddev = tf.sqrt(3.0 / (n_inputs + n_outputs))
return tf.truncated_normal_initializer(stddev=stddev)
# ---------------------------- 여기까지 ------------------------------- #

# 수정하지 않은 부분의 마지막 줄
X = tf.placeholder(tf.float32, [None, 784])
Y = tf.placeholder(tf.float32, [None, 10])

# --------------------------- 수정한 부분 ------------------------------ #
# Store layers weight & bias
W1 = tf.get_variable("W1", shape=[784, 256], initializer=xavier_init(784, 256))
W2 = tf.get_variable("W2", shape=[256, 256], initializer=xavier_init(256, 256))
W3 = tf.get_variable("W3", shape=[256, 10], initializer=xavier_init(256, 10))

B1 = tf.Variable(tf.zeros([256])) # 동영상에 없는 코드
B2 = tf.Variable(tf.zeros([256]))
B3 = tf.Variable(tf.zeros([ 10]))
# ---------------------------- 여기까지 ------------------------------- #

# 수정한 내용없음
[출력 결과]
Epoch: 0001 cost= 0.283199429
Epoch: 0002 cost= 0.100252399
Epoch: 0003 cost= 0.062173021
Epoch: 0004 cost= 0.046147975
Epoch: 0005 cost= 0.034034847
Epoch: 0006 cost= 0.024862914
Epoch: 0007 cost= 0.022365937
Epoch: 0008 cost= 0.018858417
Epoch: 0009 cost= 0.015144211
Epoch: 0010 cost= 0.011575518
Epoch: 0011 cost= 0.015414950
Epoch: 0012 cost= 0.011545146
Epoch: 0013 cost= 0.012465422
Epoch: 0014 cost= 0.010137170
Epoch: 0015 cost= 0.012283421
Optimization Finished!
Accuracy: 0.9778

xavier 초기화에서 눈여겨봐야 할 것이 있다면, [출력 결과]이다. 첫 번째로 출력된 cost가 0.283으로 시작한다. 반면 첫 번째 코드에서는 마지막의 cost가 0.855이다. hinton 교수님께서 말씀하셨던 "좋은 초기화"가 무엇인지 제대로 보여주고 있다.


바로 앞에 있는 xavier 초기화 코드에 dropout 알고리듬을 다시 추가했다. 마찬가지로 수정이 발생한 부분에 대해서만 표시했다.

# DropoutForMnist.py : 98.19%

# 수정하지 않은 부분의 마지막 줄
X = tf.placeholder(tf.float32, [None, 784])
Y = tf.placeholder(tf.float32, [None, 10])

# --------------------------- 수정한 부분 ------------------------------ #
# set dropout rate
dropout_rate = tf.placeholder("float")

# set model weights
W1 = tf.get_variable("W1", shape=[784, 256], initializer=xavier_init(784, 256))
W2 = tf.get_variable("W2", shape=[256, 256], initializer=xavier_init(256, 256))
W3 = tf.get_variable("W3", shape=[256, 256], initializer=xavier_init(256, 256))
W4 = tf.get_variable("W4", shape=[256, 256], initializer=xavier_init(256, 256))
W5 = tf.get_variable("W5", shape=[256, 10], initializer=xavier_init(256, 10))

B1 = tf.Variable(tf.random_normal([256]))
B2 = tf.Variable(tf.random_normal([256]))
B3 = tf.Variable(tf.random_normal([256]))
B4 = tf.Variable(tf.random_normal([256]))
B5 = tf.Variable(tf.random_normal([ 10]))

# Construct model
_L1 = tf.nn.relu(tf.add(tf.matmul(X,W1),B1))
L1 = tf.nn.dropout(_L1, dropout_rate)
_L2 = tf.nn.relu(tf.add(tf.matmul(L1, W2),B2)) # Hidden layer with ReLU activation
L2 = tf.nn.dropout(_L2, dropout_rate)
_L3 = tf.nn.relu(tf.add(tf.matmul(L2, W3),B3)) # Hidden layer with ReLU activation
L3 = tf.nn.dropout(_L3, dropout_rate)
_L4 = tf.nn.relu(tf.add(tf.matmul(L3, W4),B4)) # Hidden layer with ReLU activation
L4 = tf.nn.dropout(_L4, dropout_rate)

hypothesis = tf.add(tf.matmul(L4, W5), B5) # No need to use softmax here
# ---------------------------- 여기까지 ------------------------------- #

# 여기서부터는 run 호출에 dropout_rate 추가한 부분만 수정
_, c = sess.run([optimizer, cost],
feed_dict={X: batch_xs, Y: batch_ys, dropout_rate: 0.7})
print("Accuracy:", accuracy.eval({X: mnist.test.images,
Y: mnist.test.labels, dropout_rate: 1}))
[출력 결과]
Epoch: 0001 cost= 0.285693568
Epoch: 0002 cost= 0.102228592
Epoch: 0003 cost= 0.065145561
Epoch: 0004 cost= 0.047662693
Epoch: 0005 cost= 0.034276855
Epoch: 0006 cost= 0.027074164
Epoch: 0007 cost= 0.021216354
Epoch: 0008 cost= 0.019675067
Epoch: 0009 cost= 0.015092999
Epoch: 0010 cost= 0.014085908
Epoch: 0011 cost= 0.013787527
Epoch: 0012 cost= 0.010379254
Epoch: 0013 cost= 0.010147506
Epoch: 0014 cost= 0.013407918
Epoch: 0015 cost= 0.009291326
Optimization Finished!
Accuracy: 0.9819

레이어 갯수가 많지 않아서 dropout이 좋은 효과를 내지 못하는 것처럼 보인다. CPU 버전에서는 dropout 코드가 그래도 가장 좋은 성능을 보여줬는데, GPU 버전에서 돌려보니까 의외로 dropout 알고리듬을 적용하지 않은 xavier 초기화 코드가 가장 좋은 성능을 냈다. CPU와 GPU의 버전 차이가 있는지는 모르겠지만, "성능 차이는 없겠다"라는 생각이 들었다.

횟수를 150으로 늘려봤다. cost는 0.002까지 떨어졌으니까, 마지막의 0.009에 비하면 많이 떨어지긴 했는데 정확도는 조금 올라간 것으로 나왔다. 텐서플로우 샘플에 포함된 코드처럼 99%를 찍거나 하지는 않았다. xavier와 dropout 모두 95.25%까지 나오고 더 이상 발전이 없었다. 

이런.. 마지막으로 다시 돌렸는데, 98.56%가 나왔다. 100번 반복 이후로는 cost가 0으로 나왔다. 소숫점 9자리에서 0이니까, 그 아래로 미세한 숫자가 있을 수 있지만, 더 이상 줄어들 것이 없기 때문에 이 정도가 최선의 결과일 수밖에 없겠다.

이번 동영상의 코드는 교수님께서 여러 가지 이론들이 어떻게 반영되는지, 성능이 얼마나 좋아지는지 보여주기 위해서 만드셨다. 나중에 반드시 텐서플로우 샘플에 포함된 코드도 봐야 한다. 코드가 쉽진 않지만, 99.2%의 결과를 어떻게 만들 수 있는지는 꼭 확인해야 한다.