'딥러닝_김성훈교수님'에 해당되는 글 45건

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%의 결과를 어떻게 만들 수 있는지는 꼭 확인해야 한다.

34. 딥러닝으로 MNIST 98%이상 해보기 (lab 10)


여기까지만 잘 해도 전문가 소리를 듣는다고 교수님께서 말씀하셨다. 그래서, 나는 전문가가 아니다. 따라하긴 하는데, 내껀 하나도 없는 것처럼 느껴진다.


19. 학습 rate, training-test 셋으로 성능평가 (lab 07)에서 설명했던 코드가 다시 나왔다. 이번 동영상에서 설명하는 모든 코드는 이 코드를 기반으로 한다. 이 코드를 이해하면 이번 동영상도 이해할 수 있다. 이전 글에서 충분히 설명했으니까, 생략한다.


새로운 기술을 적용하고 성능 측정한 결과를 보여주신다. 첫 번째로 볼 것은 neural network의 본질에 충실하게 hidden layer를 두었다. layer가 늘어났으니까, 그에 맞게 W와 b도 바뀌었다.

참.. sigmoid 대신 ReLU activation을 사용했고, 코드 마지막 줄에서 GradientDescentOptimizer 대신에 AdamOptimizer 함수로 바꿨다. 이게 현존하는 알고리듬 중에서 가장 좋다고 말씀하셨다.


지난 번 코드에서는 91%를 기록했는데, 이번에는 94%를 가볍게 넘겼다. 더욱이 반복 횟수도 15회로 훨씬 적게 반복했다. cost가 급격하게 떨어지는 것이 충격적이다. GradientDescentOptimizer로도 해봐야겠다.


hinton 교수님이 말씀하신 좋은 초기화를 하면 어떻게 달라질까? xavier 초기화를 했다. he 초기화가 아니다. 여기서는 bias를 사용하지 않았다.


놀랍다. 초기화 하나 했을 뿐인데, 97%를 넘겼다. 초기화를 잘 했기 때문에 첫 번째 cost부터 인상적인 값이 출력되었다. 그림 왼쪽의 마지막 cost보다 작은 값이다.


layer의 갯수를 늘렸다. 이럴 경우 overfitting 문제가 발생할 수 있으므로 dropout을 적용했다. 중간에 layer가 있을 때마다 하나씩 추가되었다. for문에서 0.7을 전달했으니까, 30%는 쉬라고 얘기한 셈이다. 마지막 줄에서는 dropout_rate로 1을 전달했다. 즉, 100% 참가해야 한다고 말했다. dropout에서 주의할 점의 하나가 마지막에는 100% 참가해야 한다는 점이다.


드디어 동영상 처음에 말씀하신 것처럼 98%를 넘었다. 이건 참, 놀라운 것이다. 어찌 보면 코드상에서는 변화가 많이 없는데, 결과에서는 변화가 많다.


여러 개의 optimizer 성능을 비교하는 사이트를 소개해 주셨다. 이 중에서 가장 좋은 것은 파란색, adagrad라는 방식이다. 아래 링크를 꼭 확인해 보자. 여러 가지 optimizer에 대한 링크도 함께 있다.

  Alec Radford's animations for optimization algorithms


앞의 그림처럼 여러 가지 optimizer를 비교하는 그림이다. "ADAM: a method for stochastic optimization"는 2015년에 발표된 논문인데, 엄청 유명하다. 구글링해서 보니까, 논문 인용도 많이 됐고, 이 그림 말고도 다양하게 비교하는 그림이 많이 실려 있다. 아래 링크는 해당 논문의 pdf 파일과 연결되어 있다.

  ADAM: a method for stochastic optimization


AdamOptimizer를 사용하는 것이 GradientDescentOptimizer 사용과 다르지 않다고 설명하셨다. 단지 함수 이름만 바꾸면 된다. 성능 비교를 보면, 너무 차이가 많이 나서 AdamOptimizer 사용하지 않기는 어려워 보인다.


극적인 결과를 이루어낸 성능 측정을 요약했다. 다시 한번 물어보자. 나는 전문가가 되었는가?

33. 레고처럼 넷트웍 모듈을 마음껏 쌓아 보자 (lec 10-4)


Deep Learning 기초를 마무리하는 시간. 지금까지 많은 것을 배웠고, 이번 동영상에서는 교수님께서 누구나 잘 할 수 있다고 희망을 북돋워 주신다.


Neural Network은 마치 레고 놀이와 같다. 필요한 만큼 블록을 가져다 꽂기만 하면 된다. 이런 방식을 feedforward neural network이라고 부른다. 다만 많이 꽂으면 시간이 오래 걸린다. 좋은 GPU가 필수적인 요소가 된다.


2015년. 불과 1년밖에 되지 않았다. 홍콩 중문대 he가 ImageNet에 제출한 모델. 3% 이하의 error를 보인 놀라운 작품.

fast forward라는 이름으로 부르는데, 음악을 빨리 돌리는 것과 유사하다. 앞쪽으로 이동할 때 한 칸이 아니라 여러 칸씩 건너뛴다. 그림에서는 1칸씩 가야 하는데, 2칸씩 가고 있다. 헐.. 이라는 생각밖에 들지 않지만, 생각의 관점에서 다양한 방법들이 많이 남아있을 것 같은 희망이 들기도 한다. 나도 새로운 모델을 하나쯤 만들어 볼 수 있지 않을까? 김성훈 교수님께서 바라는 것이 이걸까?

fast forward 또한 dropout이나 ensemble처럼 다양한 결과를 통합하는 느낌을 주고 있다. 이동하는 과정에서 난수를 사용하면, 반복할 때마다 다른 결과를 얻게 되고, 전체적으로 균형이 잡히게 된다.


layer를 구축할 때 꼭 한 줄로 늘어놔야 할까? 중간에 벌어졌다가 나중에 다시 모이거나 처음부터 여러 개로 출발해서 나중에 하나로 합쳐지는 것도 가능할까?

이것이 엄청나게 유명한 Convolutional Neural Network(CNN)이다. 생각은 단순하지만, 이런 단순한 생각들이 너무 잘 동작한다는 사실이 감동적이다. TensorFlow에 포함된 mnist 모델이 CNN으로 구축되었고, 99.2%의 정확도를 보여줬다. 깜놀.


여러 방향으로 진행하는 것도 가능할까? 앞으로도 가고 옆으로도 가고..

이것도 엄청나게 유명한 모델로 Recurrent Neural Network(RNN)이라고 부른다. 연속된 방식으로 입력되는 음성인식 등의 분야에서 활발하게 연구 중인 주제이다.


누구나 모델을 만들 수 있다. 상상할 수 있다면, 어렵지 않게 만들어서 확인할 수 있다. TensorFlow라는 이미 만들어진 도구를 통해 누구나 가능한 세상이 되었다.

그림에는 현실에 존재할 수 없는 모양이 레고로 만들어져 있다. 그리기 전에는 누구도 이런 그림이 존재할 거라고 생각하지 못했다. 나도, 너도, 그녀도 할 수 있다.

32. Dropout 과 앙상블 (lec 10-3)


Deep Learning 잘 하기, 세 번째 시간.


layer가 많아질 때 발생하는 대표적인 문제점은 overfitting. 그림 왼쪽처럼 정확하게 분류하려 하지 말고 어느 정도의 오차를 허용해야 하는데, 그림 오른쪽처럼 선을 지나치게 구불려서 지나치게 잘 분류가 되는 것처럼 보일 때, overfitting이라 한다.


overfitting을 확인할 수 있는 방법이 있을까? 파란색으로 그려진 선은 training set에 대해 동작하는 오차(error)를 나타내고 빨간색으로 그려진 선은 실제 데이터에 대해 동작하는 오차를 나타낸다. 이번 그래프에서 x축은 weight(layer), y축은 error다.

overfitting된 모델은 layer의 갯수가 어느 정도일 때까지는 잘 동작하지만, 그 선을 넘어가는 순간부터는 오히려 오차가 증가하는 모습을 보인다. training에서는 99%의 예측을 하지만, test에서는 85% 정도의 빈약한 모습을 보여주고 있다.


overfitting을 방지하는 3가지 방법.

첫 번째 : training data를 많이 모으는 것이다. 데이터가 많으면 training set, validation set, test set으로 나누어서 진행할 수 있고, 영역별 데이터의 크기도 커지기 때문에 overfitting 확률이 낮아진다. 달리 생각하면 overfitting을 판단하기가 쉬워지기 때문에 정확한 모델을 만들 수 있다고 볼 수도 있다.

두 번째 : feature의 갯수를 줄이는 것이다. 이들 문제는 multinomial classification에서도 다뤘었는데, 서로 비중이 다른 feature가 섞여서 weight에 대해 경합을 하면 오히려 좋지 않은 결과가 나왔었다. 그래서, feature 갯수를 줄이는 것이 중요한데, 이 부분은 deep learning에서는 중요하지 않다고 말씀하셨다. deep learning은 sigmoid 대신 LeRU 함수 계열을 사용하는 것도 있고 바로 뒤에 나오는 dropout을 통해 feature를 스스로 줄일 수 있는 방법도 있기 때문에.

세 번째 : regularization. 앞에서는 weight이 너무 큰 값을 갖지 못하도록 제한하기 위한 방법으로 설명했다. 이 방법을 사용하면 weight이 커지지 않기 때문에 선이 구부러지는 형태를 피할 수 있다.


regularization에서 가장 많이 사용하는 l2reg(엘투 regularization)이라고 부르는 방법을 보여준다. 앞에 있는 람다(λ)의 값을 보통 0.001로 주는데, 중요하게 생각한다면 0.01도 가능하고, 진짜 너무너무 중요하다면 0.1로 줄 수도 있다고 설명하셨다. deep learning에서도 overfitting을 막기 위한 좋은 방법이라고 하셨다.

그런데, 람다의 값을 0.001로 준다면 1/1000만 반영한다는 말인데, weight의 제곱 합계이기 때문에 이 정도만으로도 충분한 영향력을 행사할 수 있다. 0.1을 준다면 얼마나 중요하게 생각하는지 상상할 수도 없다.


deep learning에서 overfitting을 줄이는 두 번째 방법으로 dropout이 있다. dropout을 사전에서 찾아보면 탈락, 낙오라고 되어 있다. 전체 weight을 계산에 참여시키는 것이 아니라 layer에 포함된 weight 중에서 일부만 참여시키는 것이다. 전혀 동작하지 않을 것 같지만, 굉장히 좋은 성능을 낸다.


forward pass로 데이터를 전달할 때, 난수를 사용해서 일부 neuron을 0으로 만드는 방법이라고 설명하고 있다. 특정 neuron을 제외하는 것보다 0으로 만들면 제외한 것과 같은 효과가 난다.


제목을 보면.. 글쓴 이도 놀라고 있다. 어떻게 이게 좋은 생각일 수 있을까?

전문가들이 너무 많다고 가정하자. 귀만 판단하는 전문가, 꼬리만 판단하는 전문가 등등 너무 많은 weight이 있다면, 이들 중 일부만 사용해도 충분히 결과를 낼 수 있다. 오히려 이들 중에서 충분할 만큼의 전문가만 선출해서 반복적으로 결과를 낸다면, 오히려 균형 잡힌 훌륭한 결과가 나올 수도 있다.

한국 속담에 딱 맞는 말이 있다. "사공이 많으면 배가 산으로 간다". 머신러닝을 공부하는 과정에서 많이 듣게 되는 용어가 균형(balance)일 수 있다. 여러 가지 방법 또는 시도를 통한 균형을 잡을 때, 좋은 성능이 난다고 알려져 있다.


tensorflow는 dropout 구현을 이미 해놓았다. relu를 호출한 후에 다음 layer에 전달하기 전에 dropout 함수를 호출하기만 하면 된다.

이번 그림에서 약간 착오가 있는 것이 dropout에 전달되는 값은 0부터 1 사이의 값으로, 사용하려고 하는 비율을 말한다. 전체 weight을 사용할 때 1을 사용한다. 이 말은 dropout을 사용하지 않겠다는 말과 같다. TRAIN 항목에 사용된 0.7은 70%의 weight을 사용하겠다는 뜻이다.


overfitting을 피한다는 말은 무엇을 뜻하는 것일까? 실제 데이터에 대해 좋은 결과를 낸다는 말이다.

데이터가 많고, 컴퓨터도 많다면 앙상블(ensemble) 방법도 가능하다. 데이터를 여러 개의 training set으로 나누어서 동시에 학습을 진행해서, 모든 training set에 대한 학습이 끝나면 결과를 통합하는 방법이다.

ensemble은 여러 전문가들에게 동시에 자문을 받기에 충분히 타당한 방법으로 보인다. 이 방법을 사용하면 최소 2%에서 4~5%까지의 성능이 향상된다고 말씀하셨다. 여러 번의 시도를 거쳐 균형 잡힌 결과를 만들어 낸다는 점에서 dropout과 비슷한 부분이 있다.

31. Weight 초기화 잘해보자 (lec 10-2)

이번 동영상은 Deep Learning을 잘 하는 두 번째 방법인 "초기화를 잘 해보자"에 대해 얘기한다.


backpropagation에서 layer를 따라 진행할수록 값이 사라지는 현상은 ReLU를 통해 해결할 수 있었다. 더욱이 그 이후에 ReLU를 응용한 다양한 방법을 비롯해서 창의적인 여러 가지 방법이 존재하는 것도 보았다.


hinton 교수님이 정리한 4가지 중에서 세 번째. 지금까지 우리는 잘못된 방법으로 weight을 초기화했다.


똑같은 코드에 대해서 난수로 초기화를 하기 때문에 ReLU에 대해서도 2가지 결과가 나온다. 어떤 초기값을 선택하느냐에 따라 매번 달라지고, 성능이 좋기도 하고 나쁘기도 하다.


weight에 대한 초기값을 모두 0으로 설정한다면? Deep Learning 알고리듬은 전혀 동작하지 않을 것이다.

그림 왼쪽에 있는 weight에 해당하는 W가 0이 된다면 어떤 일이 벌어지겠는가? W와 x를 곱셈으로 연결하고 있는데, W가 0이 된다면 x의 값은 아무런 의미가 없다. 즉, x의 값이 무엇이건 0이 되고, 지금까지 거쳐온 layer의 값들 또한 모두 무효가 된다.


전체 초기값을 0으로 주는 것은 안 된다고 말씀하시면서, 이 분야는 아직 해야할 것이 많다고 하신다. 2006년 hinton 교수님이 발표한 Restricted Boltzmann Machine(RBM)이 이 문제를 해결했다. 현재는 RBM보다 쉽고 좋은 방법들도 존재한다. 그러나, hinton 교수님께서 좋은 초기화를 할 수 있다는 길을 제시한 것은 틀림없다.


RBM의 구조를 보여주는 그림이라고 하는데, 처음 봐서는 잘 모른다. Restriction이란 단어를 붙인 것은 같은 layer 안에서 어떠한 연결도 존재하지 않기 때문이다. 단지 2개의 layer에 대해서만 x 값을 사용해서 동작하고 y 값도 사용하지 않는 초기화 방법이다.


RBM은 현재 layer와 다음 layer에 대해서만 동작한다. (forward) 현재 layer에 들어온 x값에 대해 weight을 계산한 값을 다음 layer에 전달한다. (backward) 이렇게 전달 받은 값을 이번에는 거꾸로 이전(현재) layer에 weight 값을 계산해서 전달한다.

forward와 backward 계산을 반복해서 진행하면, 최초 전달된 x와 예측한 값(x hat)의 차이가 최소가 되는 weight을 발견하게 된다.


RBM은 앞에 설명한 내용을 2개 layer에 대해 초기값을 결정할 때까지 반복하고, 다음 번 2개에 대해 다시 반복하고. 이것을 마지막 layer까지 반복하는 방식이다. 이렇게 초기화된 모델을 Deep Belief Network라고 부른다. 신뢰할 수 있는 초기값을 사용하기 때문에 belief라는 단어가 들어갔나 보다.


이러한 과정을 pre-training이라고 부른다. 첫 번째 그림에서 첫 번째 layer의 weight을 초기화하고, 두 번째 그림에서 두 번째 weight을 초기화하고, 세 번째 그림까지 진행하면 전체 layer에서 사용하는 모든 weight이 초기화된다. (앗, 3장의 그림인데, 마치 한 장처럼 붙어보일 수도 있겠다.)


이렇게 해서 네트워크 전체의 weight이 초기화 되었다.


제대로 초기화해서 학습을 시작하면 학습 시간이 얼마 걸리지 않는다. 그래서, 학습(learning)이라고 부르지 않고 Fine Tuning이라고 부른다. 그만큼 초기화가 중요하다는 뜻이다.


그런데, 설명이 긴 만큼 구현하는 것도 쉽지 않을 건 틀림없다. 좋은 소식이 있다. 굳이 복잡한 형태의 초기화를 하지 않아도 비슷한 결과를 낼 수 있다고 한다.

2010년에 발표된 xavier 초기화와 2015년에 xavier 초기화를 응용한 He 초기화가 있다. 이들 초기화 방법은 놀랄 정도로 단순하고, 놀랄 정도로 결과가 좋다.

  xavier(재비어) - 다음 영어사전에 보면 사비에, 자비에라는 남자의 이름으로 나온다.


아..! 코드가 놀랄 정도로 단순하다. 그냥 한 줄로 정리된다. 입력값을 fan-in, 출력값을 fan-out이라고 부른다. 홍콩 중문대 박사과정에 있던 he가 2015년에 이 방식을 사용해서 ImageNet에서 3% 에러를 달성했다.

  xavier - 입력값과 출력값 사이의 난수를 선택해서 입력값의 제곱근으로 나눈다.
  he - 입력값을 반으로 나눈 제곱근 사용. 분모가 작아지기 때문에 xavier보다 넓은 범위의 난수 생성.


스택 오버플로우에 있는 코드로, 구글링을 해서 쉽게 코드를 확인할 수 있다. 왠지 간단한 코드지만, 직접 만들 경우 조금 틀릴 수 있지 않을까, 하는 고민이 들었다. (아쉽지만, 언제인지 모르게 텐서플로우에 xavier 초기화 코드가 포함되었다.)

  http://stackoverflow.com/questions/33640581/how-to-do-xavier-initialization-on-tensorflow


성능 비교. xavier 초기화가 좋다고 해서 자랑해야 하는데, 더 좋은 초기화 방법들이 수두룩하다. 그래도 간단한 초기화만으로 성능을 획기적으로 끌어올릴 수 있다는 사실은 여전히 놀랍다.


이 분야는 아직도 연구 중이다. 잠깐 찾아봤는데, 앞에 언급된 초기화를 사용해서 다양한 초기화 방법이 여러 논문으로 발표되고 있다. 모두 비교한 방법보다 좋았기 때문에 발표하는 것일텐데.. 이 부분은 최신 연구동향에 신경을 써야할 것 같다. 최신의 발표되는 논문을 가끔 살펴보는 정도로.


hinton 교수님께서 제안한 좋은 초기화에 대해 이번 동영상에서 설명했다. sigmoid를 개선한 ReLU 사용 및 RBM 방식을 사용한 좋은 초기화. 여기에 xavier와 he 초기화도 포함됐다.

30. Sigmoid 보다 ReLU가 더 좋아 (lec 10-1)



신앙처럼 여겨왔던 sigmoid를 능가하는 존재가 나타났다. 여기서는 Deep Learning의 성능을 향상시키는 다양한 방법들에 대해 알려준다.


sigmoid는 logistic classification에서 어디에 속하는지 분류를 하기 위해 사용했다. 일정 값을 넘어야 성공내지는 참(True)이 될 수 있기 때문에 Activation function이라고도 불렀다.


업계 최고라고 부르는 9단이 나타났다. 9개의 hidden layer에 맞게 W와 b 또한 그 만큼의 갯수로 늘어났다. 그러나, 코드가 어렵지는 않다. 지루한 반복같은 느낌이다.


TensorBoard로 결과를 보기 위한 코드도 추가했다. (그림 오른쪽 코드)


각각의 layer가 그래프의 노드가 되어 표시되었다. 보기 좋다.


그런데, 결과는 좋지 않다. hidden layer를 무려 9개나 두었음에도 불구하고 정확도는 겨우 50%!


결과가 좋지 않은 상황을 그래프로 보니, 어떻게 된건지 한 눈에 들어온다. cost는 처음에 뚝 떨어진 이후로 변화가 없지만, accuracy는 뒤쪽에 가서 이상하게 변하고 있다.

layer가 많아지면 overfitting 문제가 발생한다고 했던 것을 기억하는가? 너무 잘 하려다 보니 지나쳐 버린 것이다. 그러나, 여기서는 더 중요한 문제가 있다.


backpropagation을 그린 그림이다. 지금처럼 layer가 많을 때는 미분 결과를 최초 layer까지 전달하는 것이 어렵다. 아니 불가능하다! 계산이 쉬운 것이지 올바른 결과 전달까지 쉬운 것은 아니다. 보통 2~3개의 layer에 대해서는 잘 동작하지만, 그 이상이 되면 문제가 발생한다.


backpropagation에서 결과를 전달할 때 sigmoid를 사용한다. 그런데, sigmoid는 전달된 값을 0과 1 사이로 심하게 변형을 한다. 일정 비율로 줄어들기 때문에 왜곡은 아니지만, 값이 현저하게 작아지는 현상이 벌어진다. 3개의 layer를 거치면서 계속해서 1/10로 줄어들었다면, 현재 사용 중인 값은 처음 값의 1/1000이 된다. 이렇게 작아진 값을 갖고 원래의 영향력을 그대로 복원한다는 것은 불가능하다.


이 문제를 vanishing gradient라고 부르고 1986년부터 2006년까지 아무도 해결하지 못했다. Neural Network에 있어 두 번째로 찾아온 위기였다. backpropagation에서 전달하는 값은 똑같아 보이지만, layer를 지날 때마다 최초 값보다 현저하게 작아지기 때문에 값을 전달해도 의미를 가질 수가 없었다.


CIFAR에서 지원했던 hinton 교수가 2006년에 방법을 찾아냈다. Neural Network가 발전하지 못했던 것들에 대해 4가지로 요약을 했는데, 여기서는 마지막이 중요하다.

non-linearity에 대해 잘못된 방법을 사용했다는 것이었다. non-linearity는 sigmoid 함수를 말한다. vanishing gradient 문제의 발생 원인은 sigmoid 함수에 있었다. sigmoid 함수가 값을 변형하면서 이런 문제가 생긴 것이었다. 이걸 알아내는데, 10년이 넘게 걸렸다.


hinton 교수님은 sigmoid 함수 대신 ReLU 함수를 제안했다. ReLU 함수는 그림에 있는 것처럼 0보다 작을 때는 0을 사용하고, 0보다 큰 값에 대해서는 해당 값을 그대로 사용하는 방법이다. 음수에 대해서는 값이 바뀌지만, 양수에 대해서는 값을 바꾸지 않는다.

ReLU를 함수로 구현하면 다음과 같다. 너무 간단해서 설명하기도 그렇다. 0과 현재 값(x) 중에서 큰 값을 선택하면 끝이다. 코드로 구현하면 max(0, x)이 된다.


TensorFlow에서 ReLU를 적용하는 방법은 매우 간단하다. sigmoid 함수를 relu 함수로 대신한다. 전달된 값을 처리하는 방식이 조금 달라졌기 때문에 나머지는 전혀 수정하지 않아도 된다. ReLU 함수는 Rectified Linear Unit의 약자로 rectified는 "수정된"이란 뜻을 담고 있다. 즉, 기존의 linear 함수인 sigmoid를 개선했다는 뜻이다.


sigmoid를 ReLU로 대체했다. layer의 갯수가 바뀌거나 하지 않는다. 나머지 코드는 전혀 수정하지 않는다.


잘 동작한다. 감동적이다. Accuracy는 100%를 찍었다. 다만 왼쪽에 있는 step이 198,000이어서 눈에 거슬린다.


그래프를 통해서 봐도 제대로 동작한다. accuracy는 처음에 100%가 된 이후에 계속 100%를 유지했다. cost는 처음에 급격히 떨어진 이후로는 아주 미세하게 값이 줄어들었다. 198,000번까지 반복할 필요가 없었다는 것도 알 수 있다.


sigmoid와 ReLU 함수를 비교하고 있다. 비교 자체가 무의미할 정도다. 정확히 말하면, sigmoid를 사용해선 안될 곳에 사용했기 때문에 올바른 비교라고 할 수도 없다.

ReLU 함수를 두 번 표시한 것은 나중에 설명할 초기값 선택 때문이다. Weight의 초기값을 어떤 걸로 하느냐에 따라 결과가 많이 달라진다. 엄청나게 반복하면 결국엔 같아지겠지만, 좋은 초기값을 선택하면 덜 반복해도 좋은 결과가 나온다.


다양한 activation 함수들을 보여주고 있다. 뒤에 결과가 나오는데, 이 중에서 가장 좋은 성능을 내는 것은 maxout이다.

tanh - sigmoid 함수를 재활용하기 위한 함수. sigmoid의 범위를 -1에서 1로 넓혔다.
ReLU - max(0, x)처럼 음수에 대해서만 0으로 처리하는 함수
Leaky ReLU - ReLU 함수의 변형으로 음수에 대해 1/10로 값을 줄여서 사용하는 함수
ELU - ReLU를 0이 아닌 다른 값을 기준으로 사용하는 함수
maxout - 두 개의 W와 b 중에서 큰 값이 나온 것을 사용하는 함수


Activation 함수의 성능을 비교하고 있다. sigmoid 함수가 n/c라고 나온 것은 결과를 낼 수 없기 때문이다. 사용하지 않아야 할 함수를 사용했으니까.

1%를 약간의 차이라고 해야 할까? 나에게는 약간인데, 김성훈 교수님께서는 상당하다고 말씀하신다. 실제 상황에서 1% 끌어올리는 것이 그만큼 어렵기 때문이 아닐까,하는 생각이 든다.

29. Tensor Board로 딥네트웍 들여다보기 (lab 09-2)


Tensorboard를 활용한 TensorFlow 시각화를 공부할 차례다. 어찌 된게 뭐 하나 쉬운게 없고.. 계속해서 벽에다 머리 들이박는 상상만 하고..


TensorBoard는 TensorFlow에서 발생한 로그를 표시하거나 디버깅(debugging)을 하기 위한 도구이다. 그래프를 그려서 통계를 시각화하는 것이 주요 기능이다.


예전에는 이렇게 텍스트로 출력을 해서 결과를 보았다. 여전히 그런 사람이 있다. TensorBoard를 사용하기 위해서는 분명 별도의 코드를 넣어야 하니까. 그래도 위와 같은 방식으로 100개 정도만 출력되도 데이터의 변화를 눈으로 추적하기는 어려워진다.


이제는 그래프를 사용해서 결과를 보고 잘못된 부분을 찾아야 할 때이다. 그래프로 검증하는 새로운 방법인 TensorBoard를 사용해 보자.


교수님께서 말씀하셨다. 순서대로 5가지만 하면 된다. 주석(annotate, not comment)으로 처리하고 싶은 노드를 선택하고, 한 군데로 모아서, 특정 파일에 기록한다. 즉, 로그를 예약한다. run 함수로 구동한 결과를 add_summary로 추가하고, tensorboard 명령으로 결과를 확인한다.

쉬워보일 수 있지만, 생각보다 어렵다. 아마도 tensorflow 자체에 익숙하지 않기 때문인 것 같다. 텍스트로 결과 보기도 어려운데, 어떻게 시각화까지 할 수 있겠는가? 잘 보려면 그래프가 좋지만, 쉽게 보려면 텍스트가 낫다는 사실은 바뀌지 않는다. 앞에 언급했던 "여전히 그런 사람"에 나 또한 포함된다.

Create writer 항목에서 sess.graph_def는 sess.graph로 바뀌었다. Launch Tensorboard 항목에서 /tmp/mnist_logs는 루트(/) 폴더 밑의 tmp 폴더 밑의 mnist_logs 폴더를 가리킨다.


tensorboard는 터미널(콘솔)에서 실행시킨다. 소스 코드가 있는 폴더로 이동해서 작업을 하면 더 쉽다. tmp 폴더는 리눅스나 맥에서 임시 데이터를 저장하는 폴더이다. 삭제해도 되는 데이터를 보관한다.

현재 폴더에 로그를 기록하려면 "./logs/mnist_logs"라고 한다. 현재 폴더(.) 밑의 logs 폴더 밑의 mnist_logs 폴더라는 뜻이다.

tensorboard를 실행한 후에 웹 브라우저를 열고 주소창에 "0.0.0.0:6006"이라고 입력한다. 마지막에 실행한 tensorboard 명령에 맞는 결과를 보여준다. 폴더를 잘못 지정하는 등의 오타가 있으면 결과가 표시되지 않는다. 이 부분에서 많이 실수했다. 잘못되면 문제가 발생해야 하는데, 내 생각에는 문제로 보이지 않아서 문제였다.


히스토그램을 보여주는 tensorboard. 다른 코드를 사용해서 돌려보니까, 이 모양이 나오지 않았다. 문제는 그 모양이 맞는지 틀리는지 모르는 상황. 그냥 저 그림을 봤을 때 올바르게 나왔다,라는 생각이 들까?


1개짜리 벡터인 스칼라를 추가한 그림이다. accuracy와 cost 두 가지를 보여준다. cost가 줄어드는 방식이 확실히 보인다.



여기 있는 3장의 그림은 교수님께서 따로 배포하신 코드를 실행했을 때의 모습이다. 동영상에서 사용한 코드와는 조금 다르긴 하지만, 모양은 잘 나오는 것 같다. mnist를 구현한 코드이고, 정확도는 100회 반복에 98.64% 나왔다.


import tensorflow as tf
import input_data

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

# This network is the same as the previous one except with an extra hidden layer + dropout
def model(X, w_h, w_h2, w_o, p_keep_input, p_keep_hidden):
# Add layer name scopes for better graph visualization
with tf.name_scope("layer1"):
X = tf.nn.dropout(X, p_keep_input)
h = tf.nn.relu(tf.matmul(X, w_h))
with tf.name_scope("layer2"):
h = tf.nn.dropout(h, p_keep_hidden)
h2 = tf.nn.relu(tf.matmul(h, w_h2))
with tf.name_scope("layer3"):
h2 = tf.nn.dropout(h2, p_keep_hidden)
return tf.matmul(h2, w_o)

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

X = tf.placeholder("float", [None, 784], name="X")
Y = tf.placeholder("float", [None, 10], name="Y")

w_h = init_weights([784, 625], "w_h")
w_h2 = init_weights([625, 625], "w_h2")
w_o = init_weights([625, 10], "w_o")

# Add histogram summaries for weights
tf.histogram_summary("w_h_summ", w_h)
tf.histogram_summary("w_h2_summ", w_h2)
tf.histogram_summary("w_o_summ", w_o)

p_keep_input = tf.placeholder("float", name="p_keep_input")
p_keep_hidden = tf.placeholder("float", name="p_keep_hidden")
py_x = model(X, w_h, w_h2, w_o, p_keep_input, p_keep_hidden)

with tf.name_scope("cost"):
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)
# Add scalar summary for cost
tf.scalar_summary("cost", cost)

with tf.name_scope("accuracy"):
correct_pred = tf.equal(tf.argmax(Y, 1), tf.argmax(py_x, 1)) # Count correct predictions
acc_op = tf.reduce_mean(tf.cast(correct_pred, "float")) # Cast boolean to float to average
# Add scalar summary for accuracy
tf.scalar_summary("accuracy", acc_op)

with tf.Session() as sess:
# create a log writer. run 'tensorboard --logdir=./logs/nn_logs'
writer = tf.train.SummaryWriter("./logs/nn_logs", sess.graph) # for 0.8
merged = tf.merge_all_summaries()

# you need to initialize all variables
tf.initialize_all_variables().run()

for i in range(100):
for start, end in zip(range(0, len(trX), 128), range(128, len(trX), 128)):
sess.run(train_op, feed_dict={X: trX[start:end], Y: trY[start:end],
p_keep_input: 0.8, p_keep_hidden: 0.5})
summary, acc = sess.run([merged, acc_op], feed_dict={X: teX, Y: teY,
p_keep_input: 1.0, p_keep_hidden: 1.0})
writer.add_summary(summary, i) # Write summary
print(i, acc) # Report the accuracy

이 코드를 돌리기 위해서는 input_data.py 파일이 필요하다. 구글에서 배포한 파일로 처음 배포하신 코드에 포함되어 있다. 아니면 텐서플로우 샘플 폴더에서도 찾을 수 있다.

여기서는 tensorboard에 결과를 보여주는 것이 목표이므로, 설명은 생략한다. 이 코드에는 drop out, relu 등의 다양한 기법들을 사용하고 있기 때문에 분석하면 많은 도움이 될 것이다.

28. XOR을 위한 텐서플로우 딥네트웍 (lab 09-1)


Neural Network 코드를 작성해 볼 시간이다.


logistic regression을 사용해서 실행시킨 결과이다. 그림에 있는 것처럼 XOR 문제를 logistic regression으로 푸는 것은 불가능하다. Accuracy에 0.5라고 출력됐으니까, 50%의 정확도라는 뜻이다. 앤드류 교수님 수업에도 자주 나오는데, 여러 개의 logistic regression을 연결하지 않고, 단 한 개만으로 XOR 문제를 풀 수 없다고 말씀하셨다.


아래 코드에서 사용할 파일   07train.txt

import tensorflow as tf
import numpy as np

# 07train.txt
# # x1 x2 y
# 0 0 0
# 0 1 1
# 1 0 1
# 1 1 0

xy = np.loadtxt('07train.txt', unpack=True)

x_data = xy[:-1]
y_data = xy[-1]

X = tf.placeholder(tf.float32)
Y = tf.placeholder(tf.float32)

W = tf.Variable(tf.random_uniform([1, len(x_data)], -1.0, 1.0))

h = tf.matmul(W, X)
hypothesis = tf.div(1., 1. + tf.exp(-h))

cost = -tf.reduce_mean(Y * tf.log(hypothesis) + (1 - Y) * tf.log(1 - hypothesis))

rate = tf.Variable(0.1)
optimizer = tf.train.GradientDescentOptimizer(rate)
train = optimizer.minimize(cost)

init = tf.initialize_all_variables()

with tf.Session() as sess:
sess.run(init)

for step in range(1000):
sess.run(train, feed_dict={X: x_data, Y: y_data})
if step % 200 == 0:
print(step, sess.run(cost, feed_dict={X: x_data, Y: y_data}), sess.run(W))
print('-'*50)

# Test model
correct_prediction = tf.equal(tf.floor(hypothesis+0.5), Y)

#Calculate accuraty
accuracy = tf.reduce_mean(tf.cast(correct_prediction, 'float'))
param = [hypothesis, tf.floor(hypothesis+0.5), correct_prediction, accuracy]
result = sess.run(param, feed_dict={X:x_data, Y:y_data})

for i in result:
print(i)
print('Accuracy :', accuracy.eval({X:x_data, Y:y_data}))

소스 코드가 없어서 직접 입력해서 만들었다.

음.. 거짓말. 앞에 나온 logistic regression 코드를 그대로 가져다가 사용했다. 달라진 점은 사용할 데이터 파일과 출력하는 형식 정도. 데이터 파일은 0과 1로 구성된 4개의 행을 갖고 있고, 결과는 XOR에 맞게 0, 1, 1, 0으로 되어 있다. 출력은 한 줄로 너무 길게 나와서 줄 단위로 보여줄 수 있도록 교수님의 코드를 조금 수정했다.


[출력 결과]
0 0.722337 [[-0.34294498 -0.4513675 ]]
200 0.693178 [[ 0.0063774 -0.02477646]]
400 0.693148 [[ 0.00423667 -0.00465424]]
600 0.693147 [[ 0.00126391 -0.0012734 ]]
800 0.693147 [[ 0.00036196 -0.00036215]]
--------------------------------------------------
[[0.5 0.49997401 0.50002599 0.5]] # hypothesis
[[1. 0. 1. 1.]] # tf.floor(hypothesis+0.5)
[[False False True False]] # correct_prediction
0.25 # accuracy
Accuracy : 0.25

hypothesis를 보면 결과가 0.5 부근에 몰려있는 것을 볼 수 있다. 동영상 캡쳐에서도 동일하다. 교수님은 50% 확률로 맞았지만, 나는 25%밖에 안 된다.

tf.floor 함수를 호출해서 결과를 0 또는 1로 만들었다. 0.5를 더한 다음에 소숫점 이하를 버렸다. 이걸 [0, 1, 1, 0]과 비교하면 첫 번째, 두 번째, 네 번째를 틀렸고, 세 번째에서 하나 맞았다. 결과는 [False, False, True, False]. XOR에 대해서 직선을 그어서 구분한다는 것 자체가 그냥 봐도 불가능하다.


앞의 코드를 살짝 바꾸니까 neural network 코드가 됐다. 그림에서는 198,000번 반복한 것 같은데.. 어찌 됐든 Accuracy가 1.0으로 잘 나왔다.


그림에서 보여지는 만큼만 코드가 바뀌었다. W1과 W2, b1과 b2를 초기화시키는 부분하고 logistic regression을 연결시키는 부분.

W1을 만들 때, 2x2 행렬이 들어간다. 이 값은 L2를 만들 때, matmul(X, W1)에서 사용된다. 그런데, 우리는 여전히 2x4 크기의 x_data를 갖고 있어서 에러가 발생한다. 동영상에서 이 부분이 누락됐다.

  x_data = np.transpose(xy[:-1])
  y_data = np.reshape(xy[-1], (4,1))

두 번의 행렬 계산이 일어난다. 첫 번째는 앞에서 설명한 matmul(X, W1). 이 부분은 (4행 2열) x (2행 2열)의 행렬 계산으로 처리되고 결과는 4행 2열로 나온다. 두 번째는 matmul(L2, W2)인데, (4행 2열) x (2행 1열)의 행렬 계산이 일어나고 최종 결과는 4행 1열로 나온다. 마지막에는 y_data와 비교해야 하니까 y_data도 4행 1열로 만들었다.


import tensorflow as tf
import numpy as np

xy = np.loadtxt('07train.txt', unpack=True)

x_data = np.transpose(xy[:-1])
y_data = np.reshape(xy[-1], (4, 1))

X = tf.placeholder(tf.float32)
Y = tf.placeholder(tf.float32)

W1 = tf.Variable(tf.random_uniform([2, 2], -1.0, 1.0))
W2 = tf.Variable(tf.random_uniform([2, 1], -1.0, 1.0))

b1 = tf.Variable(tf.zeros([2]))
b2 = tf.Variable(tf.zeros([1]))

L2 = tf.sigmoid(tf.matmul(X, W1) + b1)
hypothesis = tf.sigmoid(tf.matmul(L2, W2) + b2)

cost = -tf.reduce_mean(Y * tf.log(hypothesis) + (1 - Y) * tf.log(1 - hypothesis))

rate = tf.Variable(0.1)
optimizer = tf.train.GradientDescentOptimizer(rate)
train = optimizer.minimize(cost)

init = tf.initialize_all_variables()

with tf.Session() as sess:
sess.run(init)

for step in range(10000):
sess.run(train, feed_dict={X: x_data, Y: y_data})
if step % 1000 == 999:
# b1과 b2는 출력 생략. 한 줄에 출력하기 위해 reshape 사용
r1, (r2, r3) = sess.run(cost, feed_dict={X: x_data, Y: y_data}), sess.run([W1, W2])
print('{:5} {:10.8f} {} {}'.format(step+1, r1, np.reshape(r2, (1,4)), np.reshape(r3, (1,2))))
print('-'*50)

# Test model
correct_prediction = tf.equal(tf.floor(hypothesis+0.5), Y)

#Calculate accuraty
accuracy = tf.reduce_mean(tf.cast(correct_prediction, 'float'))
param = [hypothesis, tf.floor(hypothesis+0.5), correct_prediction, accuracy]
result = sess.run(param, feed_dict={X:x_data, Y:y_data})

print(*result[0])
print(*result[1])
print(*result[2])
print( result[-1])
print('Accuracy :', accuracy.eval({X:x_data, Y:y_data}))

역시 코드를 직접 입력해서 만들어 보았다. logistic regression처럼 1,000번만 돌렸더니, 결과가 안 나왔다. 2,000번을 돌렸는데도, 결과가 좋지 않게 나왔다. 그래서, 10,000번을 반복하고 있다. 앞에 설명했던 것처럼 코드를 일부만 수정했고, 추가로 print 함수의 형식을 조금 바꿨다.


[출력 결과]
1000 0.68227279 [[-1.24171805 0.06442181 -0.91778374 0.46263656]] [[-0.98959821 -0.52660322]]
2000 0.59741580 [[-2.62564516 0.24180347 -2.59751081 0.63329911]] [[-2.69694042 -0.66693091]]
3000 0.46362704 [[-4.15857601 0.92896783 -4.20671129 1.22253466]] [[-4.52800512 -1.86550784]]
4000 0.19515765 [[-5.1121006 2.5385437 -5.1532464 2.54854703]] [[-6.11614418 -4.89787388]]
5000 0.08121696 [[-5.61055708 3.47669911 -5.63592672 3.47990155]] [[-7.34246731 -6.90866327]]
6000 0.04758544 [[-5.89690733 3.94383311 -5.91585779 3.94633341]] [[-8.16183186 -8.0021019 ]]
7000 0.03300261 [[-6.09076691 4.23373413 -6.1062789 4.23588562]] [[-8.75467396 -8.72057724]]
8000 0.02506309 [[-6.23520756 4.43864489 -6.24855947 4.4405632 ]] [[-9.21470547 -9.24997044]]
9000 0.02012194 [[-6.34940481 4.59504986 -6.3612566 4.59679461]] [[-9.58913803 -9.66739845]]
10000 0.01676891 [[-6.44336367 4.72052336 -6.45410633 4.72213221]] [[ -9.90425777 -10.01130104]]
--------------------------------------------------
[ 0.01334288] [ 0.981884] [ 0.9818663] [ 0.01691606]
[ 0.] [ 1.] [ 1.] [ 0.]
[ True] [ True] [ True] [ True]
1.0
Accuracy : 1.0

출력 결과를 보니까, Accuracy에 1.0이 나왔고, cost 또한 제대로 감소하고 있다는 것을 볼 수 있다. []로 감싸여 있는 W1과 W2 또한 값이 잘 바뀌는 것 같다. 그러나, 여전히 cost가 감소하고 있기 때문에 반복 횟수를 늘려도 된다. 원하는 결과가 나오지 않았다면, 당연히 20,000번으로 수정해야 한다.



Neural Network 모델에서 layer를 추가했다. 첫 번째 layer를 input layer, 가운데 layer를 hidden layer, 마지막 layer를 output layer라고 부른다. hidden layer의 갯수에는 제한이 없다. 다만 너무 많으면 overfitting 문제가 발생할 수 있다.

W1, W2, W3에서 중요한 점은 행렬 곱셈이 적용되기 때문에 행렬 곱셈을 할 수 있는 형태로 layer가 추가되어야 한다는 점이다. 첫 번째가 2행 5열이라면 두 번째는 5행으로 시작해서 5행 4열, 세 번째는 4행으로 시작해서 4행 1열. 입력과 출력 데이터는 갯수가 정해져 있기 때문에 바꿀 수 없다. 파일에서 가져왔을 때, 열의 갯수가 이미 정해져 있기 때문에 바꿀 수 없다. 다만 열 전체를 사용하지 않거나 조합해서 새로운 열을 만드는 방식으로 feature 갯수를 바꿀 수는 있다. 이 코드에서는 2행 5열이므로 x열은 2개, 4행 1열이니까 y열은 1개가 된다.

hypothesis를 계산하는 것은 이전과 달라지지 않았다. L2를 L3에 전달하고, L3를 hypothesis에 전달하면 된다.그냥 갯수만 많아질 뿐, 복잡하지는 않다.


이번 그림에서 출력된 W1은 10행 2열의 결과를 보여준다. W1 출력에 포함된 갯수를 세면 된다. 이번 코드는 텐서보드와 관련이 있어서 여기서 설명하지 않고, 다음 글에서 텐서보드와 함께 보도록 한다.

27. 딥넷트웍 학습 시키기 (backpropagation) (lec 09-3)


헐.. 캡쳐하고 보니까 제목이 9-2라고 되어 있네. 교수님께서 미분 특별편을 나중에 넣어서 생긴 현상. 이것도 추억이니까, 수정하지 않고 그대로 두겠음.

이전 동영상에 나온 Neural Network 복습. 여러 개의 입력이 발생할 수 있는데, 이들 입력을 행렬을 사용해서 하나로 통합한 것까지 진행하고, 어떻게 W1, W2, b1, b2를 학습시킬 수 있을까, 고민하는 것까지 진행했다.


결론은 gradient descent 알고리듬. 모양이 어찌 되었든 미분을 사용해서 cost가 줄어드는 방향으로 진행해서 최저점에 도착하면 된다.



Neural Network은 여러 개의 layer를 두어서 복잡한 문제까지 해결할 수 있도록 구성된다. 첫 번째 layer를 input layer, 마지막 layer를 output layer, 중간에 있는 모두를 hidden layer라고 부른다. 말 그대로 눈에 보이지 않기 때문에 붙은 이름이다. 눈에 보이지 않기 때문에 내부를 파악하기 어려운 단점이 존재한다. 심한 경우 동작하는 방식을 이해하지 못할 수도 있다.

그림 제목이 Derivation인 것처럼 이 문제를 해결한 방식은 미분이다. 앞쪽 layer가 뒤쪽 layer에 어떤 영향을 끼쳤는지 알 수 있으면, 그 반대 또한 알 수 있다.


앞에서부터 뒤로 진행하면서 W와 b를 바꾸어 나갈 때 forward propagation이라고 하고, 뒤에서부터 앞으로 거꾸로 진행하면서 바꾸어 나갈 때 backward propagation이라고 한다.

Neural Network에 포함된 여러 개의 layer 중에서 결과에 가장 큰 영향을 주는 것은 뒤쪽 layer가 되고, 이들 layer부터 수정해 나가는 것이 어찌 보면 타당하다. 결론적으로 많은 layer 중에서 뒤쪽에 있는 일부만 수정을 하는 것이 훨씬 더 좋은 예측을 할 수 있게 된다.


덧셈이 됐건 곱셈이 됐건 숫자들에 대한 연산을 하는 것일 뿐이다. 덧셈이라는 것을 알고 있기 때문에 그 반대로 미분을 해서 추적하는 것이 가능하다.

이번 그림에서 사용되는 값은 w, x, b, g, f의 다섯 가지다. w, x, b, g가 변화할 때, f가 어느 정도 변화하는지 구하려는 것이 이번 그림의 목표다. 이제부터 설명하는 공식이 쉽건 어렵건 중요한 것이 아니다. backpropagation이라고 하는 알고리듬이 동작한다는 것을 이해하기만 하면 된다.

  f = wx + b,  g = wx,  f = g + b

g는 w와 x를 곱해서 만들어 진다고 그림에 있으니까, g = wx라고 할 수 있다. 그렇다면 f = wx + b는 f = g + b라고 얘기해도 된다.

먼저 g를 미분해 보자. g = wx이다. x로 미분하는 것은 x가 1 변화할 때, g의 변화량을 의미한다. w로 미분하는 것은 w가 1 변화할 때, g의 변화량을 의미한다.

  w = -2, x = 5  -->  g = -2 * 5 = -10
  w = -1, x = 5  -->  g = -1 * 5 = -5      # w가 1 변할 때 x(5)만큼 변화
  w = -2, x = 6  -->  g = -2 * 6 = -12    # x가 1 변할 때 w(-2)만큼 변화

  g를 x로 미분하면 w가 되고, w로 미분하면 x가 된다.


f를 미분할 수 있는 값은 인접한 g와 b, 두 가지가 있다. g로 미분하는 것은 g가 1 변화할 때, f의 변화량을 의미한다. b로 미분하는 것은 b가 1 변화할 때, f의 변화량을 의미한다.

  g = -10, b = 3  -->  f = -10 + 3 = -7
  g =   -9, b = 3  -->  f =   -9 + 3 = -6     # g가 1 변할 때, 1만큼 변화
  g = -10, b = 4  -->  f = -10 + 4 = -6     # b가 1 변할 때, 1만큼 변화

  f를 g나 b로 미분하면 1이 된다.


이번에는 편미분에 도전하자. w가 1 변화할 때, f가 어떻게 바뀌는지 알고 싶다. 그럴려면 w가 바뀔 때 인접한 g가 바뀌는 값과 g가 바뀔 때 인접한 f가 바뀌는 값을 알면 된다.

  δf/δw(f를 w로 미분) = δf/δg(f를 g로 미분) * δg/δw(g를 w로 미분) = 1 * x = x(5)

  w = -2, x = 5, b = 3  -->  f = -2*5 + 3 = -7
  w = -1, x = 5, b = 3  -->  f = -1*5 + 3 = -2   # w가 1 변할 때 5만큼 변화


x가 변화할 때 f가 어떻게 바뀌는지도 알아보자.

  δf/δx(f를 x로 미분) = δf/δg(f를 g로 미분) * δg/δx(g를 x로 미분) = 1 * w = w(-2)

  w = -2, x = 5, b = 3  -->  f = -2*5 + 3 = -7
  w = -2, x = 6, b = 3  -->  f = -2*6 + 3 = -9   # x가 1 변할 때 -2만큼 변화


이번 그림만 갖고 판단한다면, 맨 앞에 있는 w와 x는 input layer, 마지막에 있는 f는 output layer, 중간에 있는 g는 hidden layer에 해당한다. 놀랍게도 input layer에 있는 값을 1만큼 수정할 때 output layer의 값이 어떻게 바뀌는지 계산할 수 있었다. 간단하긴 하지만, backpropagation 알고리듬이 동작하는 원리를 확인할 수 있었다.


Neural Network은 이번 그림처럼 계산이 길게 늘어서 있는 형태일 뿐이다. 2개 layer에 대해 가능했다면, 그림처럼 엄청 많아도 미분을 통해 첫 번째 layer까지 진행할 수 있다. 김성훈 교수님께서는 이 부분에 대해 간단한 미분이라고 여러 번 강조하셨다.


앞에 나온 그림을 깔끔하게 정리.


sigmoid를 예로 들어 설명하셨다. 결국 sigmoid 또한 여러 개의 계산으로 나누어질 수 있고, 역으로 미분을 통해 어느 정도의 영향을 줬는지 판단할 수 있다고 하셨다. 영향력을 판단할 수 있으면, 얼마를 변경할 때 어떤 결과가 나오는지도 예측할 수 있게 된다.

g(z)를 풀어보면 위의 그림이 나오고, 말로 하면 아래처럼 된다.

  1. z에 -1을 곱해서 음수로 만든다.
  2. exp(지수)를 계산한다.
  3. 1을 더한다.
  4. 앞의 결과를 분모로 취한다. (1/x)


텐서보드를 통해 본 hypothesis의 모습. Sigmoid도 있고 행렬 곱셈(MatMul)도 있다.


텐서보드를 통해 본 cost 함수의 모습. 많이 복잡해 보이지만, 이게 내부를 살펴볼 수 있는 가장 좋은 방법일 수 있다. 텍스트로 출력을 해서 본다고 상상해 보자. 끔찍하다.


똑같은 그림이 다시 나왔는데, 제목만 Back propagation으로 바뀌었다. 지금까지 설명한 내용이 back propagation이었고, 그림처럼 많은 layer에 대해서도 잘 동작한다는 것을 배웠다.


Minsky 교수로 인해 발생했던 암울한 침체기를 backpropagation을 통해 비로소 지날 수 있었다. 우리는 minsky 교수님께서 하지 못했던 일을 하게 됐다,라고 김성훈 교수님께서 말씀하셨다.

26. 특별편- 10분안에 미분 정리하기 (lec 09-2)

필요한 만큼만 알려주시려고 만든 미분 동영상. 개인적으로는 이걸로 미분 이해하기는 어려웠다. 다만 미분이 뭔지 대충 이해는 할 수 있었다.


함수가 f(x)니까, 이런 경우 x에 대해서 미분을 한다. 음.. 내가 미분을 설명한다는 것은 굉장한 문제가 있긴 하지만, 내가 아는 만큼 설명을 해 본다. 미분의 기본은 순간 변화량이다. x 변화량에 대한 y 변화량을 미분이라고 한다.


f(x) = 3이라면 x 값에 상관없이 항상 같은 값을 갖는다. x의 값이 바뀌어도 y는 항상 3이다.
f(1), f(2), f(3) 모두에 대해서 y는 3이다.
x에 대해서 y의 변화량이 존재하지 않기 때문에,
다시 말해 분모가 항상 0이기 때문에 분자(x 변화량)에 상관없이 결과는 0이 된다.
f(x) = 3에 대해 미분 결과는 0.

f(x) = x라면 x가 1 변할 때, y도 1 변한다는 뜻이다.
f(1) = 1, f(2) = 2, f(3) = 3이 된다.
x가 1씩 증가할 때마다 y 또한 1 증가한다는 것을 알 수 있다.
f(x) = x에 대한 미분 결과는 1이 된다.

f(x) = 2x라면 x가 1 변할 때, y는 2만큼 변한다.
f(1) = 2, f(2) = 4, f(3) = 6이 된다.
x가 1씩 증가할 때마다 y는 2씩 변하므로, 미분 결과는 2가 된다.


이전 그림에서 x에 대해 미분을 한다고 하면,
x가 없을 때는 미분 값이 존재할 수 없고, x가 있을 때는 x를 제거하면 되는 것처럼 보였다.

f(x,y) = xy라면 x가 1 증가할 때마다 y만큼 증가하게 된다.
f(1,y) = y, f(2,y) = 2y, f(3,y) = 3y이기 때문에 미분 결과는 y가 된다.
0이나 1 같은 상수가 아니어서 당황될 수도 있지만, y 단위로 변하는 것은 이해할 수 있을 것이다.

f(x,y) = xy에서 y로 미분을 한다면,
f(x,1) = x, f(x,2) = 2x, f(x,3) = 3x이기 때문에 미분 결과는 x가 된다.


f(x) = 2x = x + x는 f(x) = x가 두 번 나온 것과 같으므로 1+1이 되고 결과는 2가 된다.
f(x) = x + 3에서 x를 미분하면 1, 3을 미분하면 0이 되어서 결과는 1이다.
f(1) = 1+3, f(2) = 2+3, f(3) = 3+3이므로 x가 1 증가할 때마다 y 또한 1 증가한다. 미분 결과는 1이다.

f(x,y) = x+y를 x로 미분하면 x는 1이 되고, y는 x의 영향을 받지 않기 때문에 0이 된다.
그래서, 미분 전체 결과는 1이 된다. x+y는 x+3과 다를 것이 없다.

그림 오른쪽의 편미분은 잘 모르지만, 느낌만 이해를 했다. f(g(x)에 대해서 미분을 직접 하면 어렵기 때문에 순서대로 나눠서 미분을 한다. 먼저 안에 있는 g(x)로 미분을 하고, g(x)를 다시 x로 미분을 하는 방식이다. 이렇게 되면 δf/δg * δg/δx가 되기 때문에 δg를 약분하면 결과는 δf/δx가 된다. δ는 delta(델타)라고 읽고, 편미분에서의 변화량을 의미한다.

--------------------------------------------------------------------

수학천재의 도움을 받아 편미분을 조금 더 이해했다. 남자인 내가 아들을 낳으면 나를 얼마나 닮게 될까? 절반 정도 닮는다고 하자. 내 아들이 아들(손자)을 낳으면 나(할아버지)를 얼마나 닮게 될까? 아들의 반만 닮는다고 하자.

  아들이 나를 닮을 확률 - 50/100(절반)
  손자가 아들을 닮을 확률 - 50/100(절반)

그런데, 손자가 아들을 닮을 확률 50%를 아들이 나를 닮을 확률과 연결해서 계산하면, 25/50이 된다. 아들이 나를 닮을 확률이 50%니까. 그렇다면, 손자가 나를 닮을 확률은 얼마일까?

  손자가 아들을 닮을 확률 * 아들이 나를 닮을 확률 = 25/50 * 50/100 = 25/100 = 1/4 = 0.25 = 25%

25/100에서 25는 손자의 확률, 100은 나 자신.
아들과 관련된 값이 약분되어 사라진다. 위의 공식에서도 g(x)가 사라지게 되고 결과는 δf/δx.

미분은 x가 바뀔 때의 y에 대한 변화량을 계산하는 공식. 할아버진인 내가 1만큼 바뀔 때, 손자는 어느 정도 바뀔 것인지와 같은 개념이다. 위의 사례로 보면 내가 1 바뀌면 손자는 0.25만큼 바뀐다.

25. 파이썬으로 XOR 조합 찾아보기

이번 코드는 주석을 간단하게만 붙였다. 머신러닝과 어찌 보면 관련이 없을 수 있으니까.. 그럼에도 이곳에 있지 않으면, 그닥 쓸모없는 것처럼 보일 수 있긴 하니까.


import time

# cell은 W와 b를 가리키고, x는 0과 1의 조합
# x는 (0, 0), (0, 1), (1, 0), (1, 1)의 4가지 가능.
def check(cell, x):
# 첫 번째 logistic 계산할 때 결과가 None이 되는 경우 존재
if None in x:
return None

v = cell[0]*x[0] + cell[1]*x[1] + cell[2]
# print(cell, x, v)

# 0을 어떻게 처리할지 몰라 이번 코드에서는 제외
if v == 0:
return None

if v < 0:
return 0

return 1


# xor 연산의 결과와 같은지 검사
def xor(cell, s1, s2):
return [check(cell, (s1[i], s2[i])) for i in range(4)] == [0,1,1,0]


# 같은 패턴만 찾아내기 위한 함수. 패턴 구분은 0과 1의 조합으로 처리
def include(results, new):
for _, _, av, bv, _ in results:
if av == new[-2] and bv == new[-1]:
return True

return False


start = time.time()

# 리스트 컴프리헨션(comprehension)
a1 = [(i, j , k) for i in range(-10, 10, 3) for j in range(-10, 10, 3) for k in range(-10, 11, 4)]
a2 = [[check(i, x) for x in [(0,0), (0,1), (1,0), (1,1)]] for i in a1]

b1 = [(i, j , k) for i in range(-10, 10, 3) for j in range(-10, 10, 3) for k in range(-10, 11, 4)]
b2 = [[check(i, x) for x in [(0,0), (0,1), (1,0), (1,1)]] for i in b1]

c1 = [(i, j , k) for i in range(-10, 10, 3) for j in range(-10, 10, 3) for k in range(-10, 11, 4)]

# 생각없이 코딩한 3차원 반복문. 오랜만에 써본다.
results = []
for i, av in enumerate(a2):
for j, bv in enumerate(b2):
for k in c1:
if xor(k, av, bv) == True:
new = [a1[i], b1[j], av, bv]

# 다른 패턴인 경우에만 추가. 패턴 결과는 출력을 보도록 한다.
if include(results, new) == False:
new.append(k)
results.append(new)

print('elapsed :', time.time()-start)
for i in results:
print(i)

출력 결과에서 중요한 것은 세 번째와 네 번째 있는 패턴이다. 여기서는 16가지만 나와서 이상한 생각이 든다. 나름 규칙이 있는 것 같은데, 찾아보지는 않았다. 머리 쓰는 게 싫다. 출력 결과는 패턴이 잘 보일 수 있도록 조금 수정했다.


아래 출력 결과에서 첫 번째 줄의 (2, 5, -6)을 보면, 2와 5는 W, -6은 b에 해당한다. a와 b가 input layer, c가 output layer를 담당한다. elapsed는 소요된 실행 시간을 의미하는데, 155초로 생각보다 오래 걸렸다. 3차원 반복문은 역시 대단하다.

[출력 결과]
elapsed : 155.85134291648865
[(  2,   5, -6), (  5,   5, -2), [0, 0, 0, 1], [0, 1, 1, 1], (-10,   5, -2)]
[(  2,   5, -6), (-10, -10,  2), [0, 0, 0, 1], [1, 0, 0, 0], (-10, -10,  2)]
[(  5, -10, -2), (-10,   5, -2), [0, 0, 1, 0], [0, 1, 0, 0], (  5,   5, -2)]
[(  5, -10, -2), (  2,  -7,  6), [0, 0, 1, 0], [1, 0, 1, 1], (  2,  -7,  6)]
[(-10,   5, -2), (  5, -10, -2), [0, 1, 0, 0], [0, 0, 1, 0], (  5,   5, -2)]
[(-10,   5, -2), (-10,   5,  6), [0, 1, 0, 0], [1, 1, 0, 1], (  2,  -7,  6)]
[(  5,   5, -2), (  2,   5, -6), [0, 1, 1, 1], [0, 0, 0, 1], (  5, -10, -2)]
[(  5,   5, -2), ( -7,  -7, 10), [0, 1, 1, 1], [1, 1, 1, 0], (  2,   5, -6)]
[( -7,  -7, 10), (  5,   5, -2), [1, 1, 1, 0], [0, 1, 1, 1], (  2,   5, -6)]
[( -7,  -7, 10), (-10, -10,  2), [1, 1, 1, 0], [1, 0, 0, 0], (  5, -10, -2)]
[(-10,   5,  6), (-10,   5, -2), [1, 1, 0, 1], [0, 1, 0, 0], (-10,   5,  6)]
[(-10,   5,  6), (  2,  -7,  6), [1, 1, 0, 1], [1, 0, 1, 1], ( -7,  -7, 10)]
[(  2,  -7,  6), (  5, -10, -2), [1, 0, 1, 1], [0, 0, 1, 0], (-10,   5,  6)]
[(  2,  -7,  6), (-10,   5,  6), [1, 0, 1, 1], [1, 1, 0, 1], ( -7,  -7, 10)]
[(-10, -10,  2), (  2,   5, -6), [1, 0, 0, 0], [0, 0, 0, 1], (-10, -10,  2)]
[(-10, -10,  2), ( -7,  -7, 10), [1, 0, 0, 0], [1, 1, 1, 0], (-10,   5, -2)]

24. XOR 문제 딥러닝으로 풀기 (lec 09-1)


여기서부터는 딥러닝(Deep Learning)이라 부르는 뉴럴 네트워크(Neural Network)의 세계. 앞에 있는 내용을 이해하면, 이번 내용을 이해하기에는 전혀 무리가 없을 거라고 생각한다. 놀랍게도 이전에 배운 multinomial regression을 확장해서 구현하고 있다.


각각은 logistic regression으로 두 개 중에서 하나만 선택할 수 있다. logistic regression 한 개만 사용해서는 XOR 문제를 해결할 수 없지만, 그림처럼 3개를 연결해서 사용한다면 가능하다고 설명하고 있다. 어떻게 보면 이 부분이 Neural Network의 핵심일 수 있다. 기존의 방식을 연결해서 사용하는 거.


Minsky 교수가 말했다. "XOR 문제를 풀 수는 있지만, W와 b를 학습시킬 수 있는 방법(a viable way to train)은 없어!"


두 개의 feature x1과 x2가 있고, 이들은 0 또는 1의 값을 갖는 boolean 데이터다. 이때 XOR 연산은 두 개의 값이 같은 경우에는 False, 다른 경우에는 True가 된다는 것을 보여준다. 그래프를 통해서도 볼 수 있다. 다만 여기서는 데이터를 한 개씩만 표시했는데, 앤드류 교수님은 4개의 영역에 대해 각각 10개 이상의 많은 데이터를 표시했다. 이 문제가 단순한 XOR의 문제만을 해결하려는 것이 아니라는 뜻이다.


3개의 logistic regression을 그렸다. 마지막에는 S로 표현되는 sigmoid가 자리하고 있다. 이 말은 hypothesis의 결과가 항상 0과 1 사이의 값으로 조정된다는 것을 뜻한다. 이 그림에서 중요한 것은 앞에 나온 2개의 결과가 마지막 logistic regression의 입력이 되는 부분이다. 앞에 나온 2개의 결과는 y1과 y2이고, 마지막의 입력은 y1과 y2이다.

그림 왼쪽은 x1과 x2가 갖는 값에 대해서 차례대로 계산한 결과를 나열하셨다. 여기서는 마지막 계산인 x1과 x2가 모두 1인 경우에 대해서만 캡쳐를 했다. 최종적으로 Ŷ(Y hat)열에 보면 올바른 값이 계산되는 것을 볼 수 있다.

x1과 x2는 (0, 0), (0, 1), (1, 0), (1, 1)

W = (5, 5),   b = -8
(0*5 + 0*5) + -8 =   0 - 8 = -8   ==>   sigmoid(-8) = 0
(0*5 + 1*5) + -8 =   5 - 8 = -3   ==>   sigmoid(-3) = 0
(1*5 + 0*5) + -8 =   5 - 8 = -3   ==>   sigmoid(-3) = 0
(1*5 + 1*5) + -8 = 10 - 8 =   2   ==>   sigmoid(  2) = 1

W = (-7, -7),   b = 3
(0*-7 + 0*-7) + 3 =     0 + 3 =     3   ==>   sigmoid(3) = 1
(0*-7 + 1*-7) + 3 =   -7 + 3 =   -4   ==>   sigmoid(3) = 0
(1*-7 + 0*-7) + 3 =   -7 + 3 =   -4   ==>   sigmoid(3) = 0
(1*-7 + 1*-7) + 3 = -14 + 3 = -11   ==>   sigmoid(3) = 0


마지막 logistic regression에 전달될 x1과 x2는 이전 결과의 조합이므로 (0, 1), (0, 0), (0, 0), (1, 0)

W = (-11, -11),   b = 6
(0*-11 + 1*-11) + 6 = -11 + 6 = -5   ==>   sigmoid(-5) = 0
(0*-11 + 0*-11) + 6 =     0 + 6 =   6   ==>   sigmoid(  6) = 1
(0*-11 + 0*-11) + 6 =     0 + 6 =   6   ==>   sigmoid(  6) = 1
(1*-11 + 0*-11) + 6 = -11 + 6 = -5   ==>   sigmoid(-5) = 0


이전 그림에 나온 그림을 정리하면, 이번 그림처럼 하나로 연결해서 정리할 수가 있다. 최초의 x1과 x2는 두 개의 logistic regression에 전달되고, 이들의 계산 결과를 마지막 logistic regression의 입력으로 전달한다.


# (  2,   5, -6), (  5,   5, -2), (-10,   5, -2)
# ( 2, 5, -6), (-10, -10, 2), (-10, -10, 2)
# ( 5, -10, -2), (-10, 5, -2), ( 5, 5, -2)
# ( 5, -10, -2), ( 2, -7, 6), ( 2, -7, 6)
# (-10, 5, -2), ( 5, -10, -2), ( 5, 5, -2)
# (-10, 5, -2), (-10, 5, 6), ( 2, -7, 6)
# ( 5, 5, -2), ( 2, 5, -6), ( 5, -10, -2)
# ( 5, 5, -2), ( -7, -7, 10), ( 2, 5, -6)
# ( -7, -7, 10), ( 5, 5, -2), ( 2, 5, -6)
# ( -7, -7, 10), (-10, -10, 2), ( 5, -10, -2)
# (-10, 5, 6), (-10, 5, -2), (-10, 5, 6)
# (-10, 5, 6), ( 2, -7, 6), ( -7, -7, 10)
# ( 2, -7, 6), ( 5, -10, -2), (-10, 5, 6)
# ( 2, -7, 6), (-10, 5, 6), ( -7, -7, 10)
# (-10, -10, 2), ( 2, 5, -6), (-10, -10, 2)
# (-10, -10, 2), ( -7, -7, 10), (-10, 5, -2)

XOR 조건을 만족시키는 또 다른 조합이 있는지 찾아보라고 말씀하셨다. 찾아봤다. 파이썬으로 만들어서 돌려보니까, 16개의 결과가 나왔다. 더욱 다양한 값들도 가능하겠지만, 이 정도만 해도 충분해 보인다. 맨 위의 하나를 계산했는데, 답이 맞았다. 코드는 다음 글에 올리도록 하겠다.


그림 왼쪽을 오른쪽처럼 처리할 수 있다. logistic regression을 multinomial classification으로 변환할 때, 행렬을 사용해서 처리했었다. 오른쪽은 왼쪽 그림에 있는 첫 번째 logistic regression들을 하나로 결합할 수 있다는 것을 보여준다. 뒤에 가면 여러 개의 logistic regression들을 묶어서 layer라고 부른다.


기억을 되살리기 위해 추가한 그림. 3가지 중에서 한 가지를 선택하기 위한 multinomial classification에 대한 내용이다.


Neural Network는 여러 개의 logistic regression을 순차적으로 연결하는 구조이기 때문에 코드에서도 동일한 방식으로 나타난다. 그림 오른쪽 아래에 있는 텐서플로우 코드에서 K는 첫 번째 logistic regression들의 결과이고 이것을 행렬 곱셈(matmul)에 다시 전달하고 있다.

그렇다면, 전달된 데이터로부터 W1, W2, b1, b2를 어떻게 학습시킬 수 있을까?

23. 딥러닝의 기본 개념2- Back-propagation 과 2006-2007 '딥'의 출현 (lec 08-2)


머신러닝의 발전에 엄청난 공을 세운 캐나다의 단체. 이 단체 덕분에 머신러닝을 주도하는 연구소나 학자들은 캐나다의 몬트리올, 토론토 출신. 1987년 Hinton 교수가 이 단체의 지원을 받기 위해 캐나다로 이주하면서 발생한 현상.


가장 어려웠던 시기에 도박과도 같은 베팅을 Hinton에게 했던 CIFAR.


Hinton은 2006년과 2007년 머신러닝을 부활시킬 수 있는 논문 발표. 1987년 이주 이후 20년만에 거둔 성과.


  • 여러 개의 layer가 있어도 초기값을 잘 선택하면 학습이 가능하다.
  • 신경망을 잘 구성하면 복잡한 문제를 효율적으로 풀 수 있다.
  • Neural Network 대신 사람들의 주의를 끌 수 있는 Deep Learning으로 rebranding.

이미지가 무엇인지 맞추는 ImageNet 경진대회에서 Hinton 교수님 밑에 있는 대학원생이 AlexNet으로 획기적인 성능 향상. 사람들의 관심을 끌어올린 계기가 됨.


ImageNet은 2015년 사람보다 더 잘 구별할 수 있는 수준인 97%까지 발전. 스탠포드 대학생의 수준은 95%.


현재는 사진의 상황까지 설명 가능.


김성훈 교수님 연구실에서 하는 연구 중의 하나. 사람이 음성으로 내린 명령을 분석해서 실행해 줌. 공상과학 영화에 나오는 한 장면.


중국 바이두에서 개발한 음성 인식 기술. 소음이 심한 곳에서도 사람의 음성을 90%까지 인식 가능.


사람보다 게임을 더 잘 한다는 딥러닝


알파고에도 일부 포함되어 있다는 딥러닝.


Hinton 교수님께서 지금까지 발견한 것들에 대한 요약.

  • labeled 데이터셋이 너무 작았고
  • 컴퓨터는 수백만 배 느렸다.
  • 초기화를 잘 하지 못했고
  • non-linearity(sigmoid)를 잘못 사용했다.


왜 머신러닝을 공부해야하는지 묻는다면?
데이터를 갖고 있거나 무언가를 팔고 있거나 비지니스를 하고 있다면, 누구라도 머신러닝을 활용할 수 있다!
머신러닝은 이미 우리 옆에 있고, 세상을 이롭게 할 준비가 된 상태이다. 주인공이 우리가 되면 더 좋지 않겠는가?


음성을 인식해서 자동으로 표시되는 유튜브 자막. 사람이 입력한 것이 아님.


친구들이 올린 내용 중에서 내가 좋아할 만한 것만 추천하는 페이스북 뉴스피드 시스템.


구글 검색 시스템. 내가 찾고 싶은 것을 상위에 표시해 줌.


올 여름에 내가 볼 영화를 넷플릭스는 알고 있다!


아마존의 제품추천 시스템. 머신러닝을 사용하지 않고 비즈니스는 가능한 것일까?


왜 지금이어야 할까?

  • 세계적인 전문가가 되기에 늦지 않았고, 그렇게 복잡하지도 않다.
  • 실제 사용할 수 정도로 정확하고, 공개된 도구 또한 많고, 파이썬과 같은 쉬운 언어가 있다.
  • 그럼에도 불구하고 가장 중요한 것은, 재밌다!!


22. 딥러닝의 기본 개념- 시작과 XOR 문제 (lec 08-1)


인류의 궁극적인 꿈은 생각하는 기계!


사람의 뇌(brain)를 흉내내면 가능하지 않을까?


사람의 뇌와 비슷하게 동작하도록 구성. 일정 크기 이하라면 활성화(activation)되지 않도록 구성. 지금까지 우리가 배워온 것과 다르지 않다.


Logistic Regression을 여러 번 적용하면 오른쪽과 같은 형태로 구성할 수 있음.


XOR 문제 대두. OR과 AND에 대해서는 잘 동작하는데, XOR 문제는 linear 방식으로 풀 수가 없었음.


급기야 당대 최고의 학자인 Minsky 교수가 불가능하다고 선언.


layer가 여러 개 있을 때, 각각의 layer에서 사용한 W와 b를 조절할 수 없다고 수식으로 증명. 이 사건으로 머신러닝은 최소 10년에서 20년 정도의 침체기를 겪게 됨.


1986년에 Hinton 교수가 Backpropagation 논문 발표. 1974년과 1982년에 Werbos가 발표했던 논문의 독자적인 재발견. Output layer부터 Input layer까지 반대로 에러를 보정하는 기술.


고양이가 인지하는 감각을 흉내내는 네트워크. 사물을 볼 때, 뇌의 일부만 활성화되는 것에서 착안.


LeCun 교수가 만든 Convolutional 네트워크. 텐서플로우에서 배포하는 mnist 예제 코드가 이걸로 되어 있음. 여러 가지를 동시에 구성해서 99.2% 달성.


미국 해군에서 초창기에 개발했던 무인 주행 자동차.


영화 터미네이터에 나오는 인간형 로봇. My CPU is a neural-net processor.


Backpropagation에 존재하는 엄청난 문제점 대두. layer가 많을 경우 뒤에서부터 멀리 떨어진 layer의 W와 b를 변경할 수 없음. 머신러닝 분야의 두 번째 침체기.

21. 로그 함수 정리

이번 주 스터디에서 cost 함수에 들어가는 log 함수 때문에 고생을 많이 했다. 수학 사이트를 뒤져서 로그만 다시 봐야 하는건가, 하는 고민도 하고..


이 글은 함께 스터디하시는 수학 천재의 도움을 받아 작성할 수 있었음을 밝힌다. 지금 어느 정도 이해됐을 때 재빠르게 정리하지 않으면 다시 처음으로 돌아갈 것 같다.


import math

for i in range(1, 21):
print('{:6.3f} ({})'.format(math.log10(i/10), i/10), end=' ')

if i%5 == 0:
print()

# 출력 결과
# -1.000 (0.1) -0.699 (0.2) -0.523 (0.3) -0.398 (0.4) -0.301 (0.5)
# -0.222 (0.6) -0.155 (0.7) -0.097 (0.8) -0.046 (0.9) 0.000 (1.0)
# 0.041 (1.1) 0.079 (1.2) 0.114 (1.3) 0.146 (1.4) 0.176 (1.5)
# 0.204 (1.6) 0.230 (1.7) 0.255 (1.8) 0.279 (1.9) 0.301 (2.0)

도대체 로그 결과가 어떻게 나오는지 확인하고 싶었다. 이 별거 아닌 코드를 만들면서 에러도 많이 만들었다.

log는 0을 포함해서 음수를 전달하면 계산할 수 없다는 것도 이번에 알았다. 왜 안되는지는 알고 싶지 않다. 그건 수학 천재들의 영역이다. 그래서, 코드는 0.1부터 2.0까지 진행한 결과다. 여기서 중요한 것이 0.0이 나오는 부분인데, x가 1일 때 로그에서 결과가 0이 된다. 이건 수학 천재에게 들은 건데, 정말 그렇게 나왔다.

위의 결과를 그래프에 그려봤다. 유명한 사이트가 있어서 결과를 쉽게 비교할 수 있었다. 동영상을 다시 보니, 이 사이트가 교수님께서 사용하셨던 사이트였다.

https://www.desmos.com/calculator


첫 번째는 파이썬 코드로 확인했던 log(x)에 대한 그래프이다. x가 0일 때 음수 무한, 1일 때 0이 되는 것을 알 수 있다. 화면상에서는 x가 0에 가까울 때 그래프가 y축에 붙은 것처럼 보이지만, 붙은 게 아니라 0에 무한히 커지긴 하지만 y축에 달라붙을 수는 없다. 앞에서 말했듯이 x는 0이 될 수 없고, 음수도 될 수 없다.

log(x)의 결과에 - 기호를 붙여서 결과를 음수로 만들었다. 이게 생각보다 헷갈리긴 한데, 전체 결과에 -를 붙였으니까, 결론적으로 y가 뒤집힌다. log 함수에 전달되는 x는 그대로인데, 결과는 반대가 됐다.

cost 함수에 등장하는 -log(x) 그래프의 실제 모습이다. 교수님께서는 x가 1보다 큰 부분에 대해서는 필요없기 때문에 보여주지 않으셨다. cost 함수에 전달되는 결과는 sigmoid를 거치기 때문에 log 함수에 전달될 수 있는 값의 범위는 0~1 사이가 되므로 1보다 큰 부분은 없어도 된다.

이번 그래프는 x축에 대해서 대칭이다. log 함수에 전달되는 값이 음수인 경우에 이와 같이 그려진다.

cost 함수에 등장하는 두 번째 그래프다. 이 그래프가 밥그릇에서 오른쪽 부분을 담당한다.

처음 그렸던 log(x)에 대해서 x축과 y축 모두에 대해 대칭인 그래프이다. 다만 cost 함수에서 사용하는 그래프는 0부터 시작하기 때문에 이번 그래프와는 조금 차이가 있다.

바로 앞의 그래프를 오른쪽으로 1만큼 이동시켰다. 이동시키는 방법은 1에서 x를 빼면 된다. 아니 기존 값에 1을 더하는게 좋겠다. y = -log(-x+1)이라는 표현이 더 쉽다.

아무 기대하지 않고 그려봤다. -log(x)와 -log(1-x)를 더했다. cost 함수에서는 실제로 이와 비슷하게 사용한다.

비슷해 보여도 말도 안 되게 다르다. cost 함수는 2개의 그래프 중에서 하나를 선택해서 사용한다. 두 개를 동시에 사용하는 일은 없다.

cost 함수는 이번 그래프의 식에 y를 곱하는 부분이 추가되어 있기 때문에 이번 그래프의 식과는 완전 다르다.


cost 함수 그림을 가져왔다. 다음에 나오는 그래프와 비교해서 보기 바란다.



아주 큼지막하게 그려봤다. cost 함수에서 사용되는 2개의 log 함수이다. 우리는 이 그래프에서 0과 1 사이의 구간만 사용한다. 0보다 작거나 1보다 큰 영역의 그래프는 필요없다. 더욱이 진짜 밥그릇처럼 2개를 연결해서 그린다는 것은 말이 되지 않는다. 하나는 0에서 시작하고, 하나는 1에서 시작하는데 어떻게 연결할 수 있겠는가?


아래는 log 함수 4개를 하나로 모은 그래프이다. 앞에서 충분한 설명이 되었지만, 한 눈에 비교를 해 보면 더 좋을 수도 있을 것 같다.

20. 2주차 스터디 정리

이번 주에 궁금했던 내용들을 정리해 본다. 다행스럽게 이번 주에도 대부분의 결과에 대해 나름대로의 답을 얘기할 수 있었다.


# ------------------- sigmoid ----------------- #

1. sigmoid를 영어 사전에서 찾아보면 S 또는 C 자 형태의 모양을 말함.
sigmoid는 기존의 hypothesis 함수의 범위를 0과 1 사이로 조정하기 위해 추가한 함수

2. sigmoid는 그 자체로 굴곡이 있기 때문에 cost 함수가 밥그릇처럼 매끈하게 나오지 않는다.
시작점이 어딘가에 따라서 최저점을 찾지 못할 수도 있다. global minimum을 찾아야 하는데 local minimum을 찾고 멈출 수 있다. 그래서, 기존의 cost 함수를 log 함수가 포함된 공식으로 수정해서 사용한다.

3. 새로운 cost 함수
y가 0일 때와 1일 때에 대해 다른 공식 적용. H(x)와 1-H(x). 두 개의 log 함수를 묶어서 마치 하나의 공식인 것처럼 사용한다. 두 개의 공식이 동시에 사용되는 경우는 없다.

4. "sigmoid 결과가 참, 거짓으로 나온다"라고 착각할 수 있다.
전혀 그렇지 않고 0과 1 사이의 어떤 값으로 나올 뿐이다. 참과 거짓이 필요하다면 0.5와 직접 비교해야 한다.

5. linear regression에서는 x축에 대해 y를 결정하기 때문에 one feature 그래프에 대해 2차원으로 표현. logistic regression에서는 x1과 x2에 대해 분류를 하고 y가 없기 때문에 two feature 그래프가 2차원. logistic regression은 그룹을 구분하는 선을 찾는 작업. 여러 차원이 있기 때문에 hyper plain이라고도 함.

6. decision boundary는 그룹을 나누어줄 수 있는 경계선을 말함.
1차원 직선일 수도 있지만, 대부분은 타원이나 비정형의 아메바 같은 형태가 된다. deep learning 알고리듬으로 처리할 수도 있겠지만, SVC(Support Vector Machine)과 같은 다른 머신러닝 알고리듬에 나오는 이론을 적용하면 더 쉽게 그릴 수 있다.


# ------------------- cost 함수 ----------------- #

1. cost 함수의 목적
구현 방법에 상관없이 맞는 예측일 때 적은 비용을, 틀린 예측일 때 많은 비용을 부과하는 것이다. 그러면 미분을 통해 비용이 적은 방향으로 진행할 수 있다.

2. 두 개의 값 A와 B가 존재. y 값에 따라 A나 B만 사용하고 싶은 게 목적.
앞에서도 설명했는데, 이 공식을 사용해서 두 개의 log 함수를 연결해서 하나로 작성할수 있다. y는 언제나 1 또는 0이고, 1일 때 A를 사용한다.

  공식  ==>  y*A + (1-y)*B
  y=1  ==>  1*A + (1-1)*B = A
  y=0  ==>  0*A + (1-0)*B = B

3. Y vs. Ŷ(y hat)
Y : real. 데이터에 포함되어 있는 실제 값으로 label이라고도 부른다.
Ŷ : prediction, H(x). 모델이 예측한 값으로 이 값과 Y를 비교해서 비용을 계산함


# ------------------- softmax ----------------- #

1. softmax는 점수(score)와 같은 데이터를 확률(probability)로 변경
one-hot encoding은 행렬에 포함된 값들 중에서 가장 큰 값만 1로, 나머지는 0으로 변경

2. softmax는 binary classfication으로 나온 여러 개의 결과를 계산하기 위한 방법
결과값을 확률로 변환하면서, 전체 합계가 1이 되도록 만드는 함수. one-hot encoding을 사용해서 확률이 가장 높은 것은 1로, 나머지는 0으로 처리. 결국 A, B, C 중에서 고르는 상황이고 A의 확률이 가장 높다면 A라고 예측

3. softmax에서 W가 왜 2차원 매트릭스 3x3이 될까? 수평으로 해석해야 할까, 수직으로 해석해야 할까?
행렬 곱셈을 하게 될 X가 곱셈을 할 수 있는 형태이면 된다. 다만 W는 x로 표현되는 각 행에 포함되는 열과 곱해야 한다. W가 앞에 오면 W의 행과 X의 열을 곱하게 되고, W가 뒤에 오면 X의 행과 W의 열을 곱하게 된다. 계산 순서는 편의에 따라 바꿀 수 있다.

4. multinomial classification에서 W는 3x3 행렬이 맞고, X는 3x1 행렬이 맞다.
X는 여러 개의 binary classification에서 재사용하니까. 데이터가 1개일 때는 3x1이지만, 많아져서 8개 되었다면 3x8로 바뀌게 된다. 앞서 나온 설명처럼 3x8로 사용할 것인지 8x3으로 사용할 것인지는 코드에 따라 선택해서 사용해야 한다.

5. hypothesis -> cost -> softmax -> one-hot encoding(argmax)
4단계의 순서를 거치고 나면 값을 예측하는 모델이 완성됨. 다음 순서는 예측한 값을 실제 값과 비교해서 cost 함수 완성.(cross entropy 함수)

6. softmax가 하는 두 가지 일
3개 중에서 하나를 예측한다고 가정했을 때, 첫 번째는 3가지를 예측하는 결과가 0과 1사이에 오도록 하는 sigmoid 역할이고, 두 번째는 이들 합계가 1이 되도록 하는 확률적인 역할이다. 


# ------------------- cross entropy 함수 ----------------- #

1. cross entropy
통계에서 현재 값과 평균까지의 정보량을 계산하는 방법으로, softmax를 통해 예측한 값(y hat)과 실제 값(y) 차이를 계산한다.

2. -∑Li log(Si) = -∑Li log(Ŷi) = ∑(Li) * (-log(Ŷi))
곱셈은 element-wise 곱셈. 행렬 곱셈이 아니라 같은 자릿수끼리의 곱셈. 이 식에서 -log(Ŷi)는 5장에서 나왔다. binary classification에서 왼쪽 그래프에 사용. 오른쪽 그래프는 1-h로 표현. Ŷ(y hat)은 softmax를 통과했기 때문에 항상 0과 1 사이.

3. y에 해당하는 L을 표시할 때 2행 1열의 행렬로 그린다. y는 그냥 0 또는 1이 되어야 하는거 아닌가? 왜 값이 2개가 있는 걸까?
이 부분은 정말 이해가 안 갔었다. binary classification이 2개 있다고 가정하기 때문. 3개 있다면 3행 1열로 표현해야 한다. A를 표현할 때는 [[0], [1]], B를 표현할 때는 [[1], [0]]이 된다. 즉, 선택할 label에 맞는 위치의 값이 1로 표시되는 방식을 사용하기 때문에. 그냥 0이나 1로 A와 B를 표현할 수 있지만, 이 방식은 성능은 좋아질 수 있겠지만 70%의 정확성 등에 비해 확장성이 떨어진다고 할 수 있다.
one-hot encoding에서 이와 같은 형태로 만들어 버린다. 그렇기 때문에 이와 같은 형태로 답 또한 존재해야 한다. 데이터 파일을 보면 y에 해당하는 열이 3개 있는데, [0 0 1]처럼 되어 있다. [0 0 1]은 A는 틀리고, B도 틀리고, C는 맞았다는 뜻이므로 C가 된다.

4. 왜 cross entropy에서 2개 짜리 행렬만 갖고 설명을 할까? feature가 3개 라면 1x3 행렬을 사용하는 것인가?
feature 몇 개가 아니라 multinomial(label) 갯수에 따라 정해진다. 3개로 분류한다면 1x3 행렬을 사용한다.

5. logistic cost와 cross entropy가 똑같다고 교수님께서 과제로 내주셨다.
cost 함수의 목적은 틀렸을 때 벌을 주어서 비용을 크게 만들어야 하는데, 양쪽 모두 무한대라는 벌칙을 적용한다. 다만 logistic regression에서는 2개의 log식을 연결해서 사용하지만, cross-entropy에서는 행렬로 한 번에 계산하는 방식을 취할 뿐이다. 즉, logistoic regression을 cross-entropy로 처리할 수 있다. 바로 앞에 나온 2개 중에 하나를 선택하는 그림이 logistoic regression의 cross-entropy 버전이다. 같다라기 보다는 cross entropy가 logistic cost를 포함하고 있는 느낌이다.

6. 이번에 배운 cost 함수도 미분을 할 수 있을까?
김성훈 교수님도 그렇고 앤드류 교수님까지 이번 미분은 보여주지 않으셨다. 이게 되어야 텐서플로우 코드를 파이썬으로 변환할 수 있다.


# ------------------- 기타 ----------------- #

1. 자연상수 mathematical constant
2.71828182845904523536
오일러가 이름을 붙여서 오일러 상수라고도 한다. 비순환소수이며 무리수이다. 초월수로 증명된 첫 번째 상수이고, 원주율도 초월수의 하나. 수학 공식에 e를 넣으면 표기법이 놀랍도록 간단해져서 '자연'이라는 단어를 붙임. 그러나, 영어를 직역하면 '수학적인 상수' 정도가 됨. 파이썬에서는 math.e, 옥타브에서는 e라고 하는 상수 또는 변수가 있음.

2. binary classification을 행렬로 묶으면 multinomial classification이 됨.
앤드류 교수님은 polynomial classification으로 부름

3. tf.matmul 함수
tf.matmul(a, b, transpose_a=False, transpose_b=False, a_is_sparse=False, b_is_sparse=False, name=None)
매개변수를 통해 transpose를 지정할 수 있다. 기본값은 False. 매개변수를 2개만 전달했다는 것은 행렬 곱셈이 transpose 없이 된다는 뜻.

4. WX vs. XW
행렬 곱셈은 앞쪽 행과 뒤쪽 열을 곱한다. 이미 W에서 어떤 것을, X에서 어떤 것을 곱해야 하는지 알기 때문에 순서를 바꿀 수 있다. W가 앞에 와도 되고 뒤에 와도 된다. 앤드류 교수님은 항상 W를 앞쪽에 두는 코드를 보여주었다.

5. linear와 logistic regression 용어 차이
linear regression은 값을 예측. logistic regression은 classification으로도 부르며 한 개를 선택. 비슷해 보이지만 완전히 다른 개념. linear는 숫자로 된 값을 예측하는 것이고, logistic은 label로 된 항목을 선택하는 것이다. 전문 용어로는 regression과 classification. linear regression을 사용해서 logistic classification을 구현할 수 있기 때문에 logistic regression이라고 부르는 듯 하다.

6. 분류
binary classfication 2개 그룹, 시험 pass/non pass
multinomial classfication 다중 그룹, 시험 성적 A/B/C/D/F.
multinomial은 binary를 여러 번 사용해서 구현 가능

7. 데이터 구분
original set = training set + test set
original set = training set + validation set + test set
mnist 예제에서는 validation set까지 지정해서 구현하고 있다.

8. online learning
한 번에 학습하는 것이 아니라 일부만 갖고 학습하고, 또 일부를 추가하는 방식. mnist 예제에서 batch 크기를 정해서 일부씩 잘라서 처리하고 있다. 두 가지 중의 한 가지를 사용하면 된다.

9. mnist
The MNIST(Mixed National Institute of Standards and Technology) database is a large database of handwritten digits that is commonly used for training various image processing systems.
이미지를 처리하기 위해 opencv 모듈을 사용하는데 맥에서는 설치가 잘 되지 않음. opencv 2버전은 쉽게 되는데, 새로 나온 3버전은 잘 안됨. 성능 차이가 꽤 있음. 그런데, mnist 예제에 opencv 없어도 잘 동작함.

0. reduce_sum(a, reduction_indices=1)
첫 번째 매개변수에 대한 합계 계산. 두 번째가 없으면 전체 합계, 0을 입력하면 열 기준, 1을 입력하면 행 기준 합계 적용. Y*tf.log(hypothesis)의 결과가 어떤 매트릭스인지 파악되면, reduce_sum 함수도 쉽게 이해할 수 있음

x = [[1, 1, 1] [1, 1, 1]]

tf.reduce_sum(x)

# 6

tf.reduce_sum(x, 0)

# [2, 2, 2]

tf.reduce_sum(x, 1)

# [3, 3]

tf.reduce_sum(x, 1, keep_dims=True)

# [[3], [3]]

tf.reduce_sum(x, [0, 1])

# 6

19. 학습 rate, training-test 셋으로 성능평가 (lab 07)


이번 동영상에서는 learning rate의 중요성과 mnist 모델에 대한 성능을 측정해 본다. 드디어 조작된 데이터를 벗어나 실제 데이터를 사용한다.


동영상 안에서 알려주신 사이트. 이번 동영상에 사용할 소스 코드인 LogisticRegression.py 파일이 포함되어 있다. 이 사이트에는 이번 예제 말고도 좋은 내용이 많이 있다. https://github.com/aymericdamien로 접속하면 더욱 많은 코드가 기다리고 있다.


그림 위쪽에 learning rate 변수가 있다. learning rate을 10으로 바꿨는데, 이렇게 되면 gradient descent에서 성큼성큼 이동하게 된다.


결과는 처참하다. 빨리 가는 것도 좋지만, 목표를 지나친건지 어쩐건지 숫자가 아닌 것들이 나왔다. 그냥 잘못됐다는 뜻이다. 이걸 overshooting이라고 하는데, learning rate이 클 때 발생하는 대표적인 현상이다. nan은 "Not A Number"의 약자다.


이번에는 굉장히 작은 learning rate으로 0.0001을 줬다. 결과를 보면 진행되긴 하는데, 값이 소숫점 세 번째 자리에서 바뀌고 있다. 1800 오른쪽에 있는 것이 cost로, cost의 변화량이 너무 작다. 이렇게 되면 최저점에 도착할 때까지 몇 번을 더 반복해야 할지 알 수 없다. 도착한다고 해도 얼마나 걸릴지 장담하기가 어렵다.


처음에는 0.01로 learning rate을 주셨다. 잘 동작했다. 이어서 지금 그림처럼 0.1로 좀더 과감하게 learning rate을 주었고, 결과 또한 성공적이다. 가장 좋은 learning rate은 적절하게 이동해야 한다고 하는데, 여기에 대한 좋은 숫자는 없다. 여러 번에 걸쳐 실행한 결과를 보고 직접 판단해야 한다. 그것이 learning rate을 결정해야 할 때 가장 어려운 점이다.


mnist 데이터셋이 왜 유명한지는 모르겠지만, tensorflow에서 기본 예제로 사용하기 때문에 tensorflow 계열에서는 가장 유명하다. 기초부터 전문가 수준까지 다양한 예제 코드가 공개되어 있다. (나중에 보니까, 딥러닝을 비롯한 다양한 머신러닝에서 가장 신뢰할 수 있는 데이터로 어디서나 mnist 데이터셋을 사용하고 있었다.)

mnist는 손으로 쓴 우편번호를 기계가 인식해서 자동 분류할 수 있도록 하기 위해 만든 데이터셋이다. 글자 하나의 크기는 28x28이고, 모두 흑백으로 되어 있다. 데이터셋은 image와 label로 구분되어 있고, 각각은 다시 train과 test로 나누어져 있다. validation set은 보이지 않는데, tensorflow 예제를 분석하다 보면 만날 수 있다. (이미지를 분류할 수 있는 샘플이면서, 흑백이어서 GPU가 없어도 돌려볼 수 있는 정도의 가벼움을 지니고 있는 것이 많이 사용하는 이유 중의 하나일 것이다.)


mnist 데이터셋을 읽어오는 소스 코드이다. 이 코드는 tensorflow 배포판에 포함된 예제에 들어있고, 파일 이름은 input_data.py이다. 이 코드 때문에 현재 폴더에 mnist 데이터셋이 없어도 인터넷을 통해 데이터를 가져올 수 있다. mnist 예제를 전문가 수준까지 진행하면서 공부하려면 반드시 이 파일을 분석해야 한다.


이미지 1개는 28x28로 되어 있고, 1차원으로 늘어 놓으면 784가 되고 이것을 feature라고 부른다. 이미지 갯수는 정해져 있긴 하지만 모르는 경우에도 처리할 수 있어야 하니까 갯수를 의미하는 행(row) 크기를 None으로 줬다. feature를 의미하는 열(column) 크기는 앞에서 말한대로 784가 된다. 784픽셀로 이루어진 이미지가 여러 장 있다는 뜻이다. y는 label의 갯수, 즉 0에서 9까지의 숫자를 판단하기 때문에 10개가 되어야 하고, 이미지 갯수는 모르므로 역시 None으로 처리한다.

갑자기 784라는 어마어마한 갯수의 feature를 만나서 믿고 싶지 않을 것이다. 그런데, 사실이다. 공부 시간(x1)과 수업 참석 횟수(x2)와 같은 단순한 데이터일 때는 feature가 2개밖에 없을 수 있다. 여기에 bias를 더한다고 해도 3개에 불과하다. 이미지는 픽셀(pixel, 화소) 단위로 분석해야 하기 때문에 픽셀만큼의 feature가 생길 수밖에 없다.

W(weight)는 x의 feature 갯수와 같아야 한다. x가 갖고 있는 feature 각각에 대한 가중치니까. 또한, 10개의 classification을 구성해야 하니까, binary classification이 10개 있어야 한다. 그래서, W의 크기는 784x10이 된다. b(bias)는 binary classification마다 한 개씩 있으므로 10개가 된다.


이전 동영상에서 multinomial classification을 떠올리면 된다. 이 파일에서 feature는 x0, x1, x2의 3개이고, label에 해당하는 y 또한 3개이다. y가 3개인 이유는 x 때문이 아니라 A, B, C 중에서 하나를 선택해야 하기 때문이다. 그래서, x 데이터가 8개라면 y 데이터 또한 8개가 되어야 한다. 이 코드에서 x는 8행 3열, y도 8행 3열이 된다. 이게 mnist에서 [None, 784], [None, 10]으로 x와 y를 설정하는 이유이다.


hypothesis를 예전에는 sigmoid로도 불렀었고, softmax라고도 불렀었다. hypothesis 자체가 가설을 말하기 때문에 어떤 함수가 됐건 가설이라고 얘기할 수 있었다. Deep Learning에 와서는 activation 함수라고 부른다. 일정 기준을 만족시킬 때 활성화(activation)되기 때문에 붙은 이름이다. 사람의 뇌(brain)가 동작하는 방식과 같다.

이 부분은 이전과 달라진 것이 없다. 행과 열의 갯수가 784와 10으로 커지긴 했지만, 행렬 연산을 하기 때문에 코드에서 변화가 발생하진 않는다. 28x28 크기의 이미지가 갖고 있는 픽셀(pixel) 하나하나를 W와 곱한 다음에 b를 더할 뿐이다. 그러면 어떤 픽셀은 색이 칠해져 있고, 어떤 픽셀은 색이 없을 것이다. 흑백이니까, 있거나 없거나 둘 중의 하나다.

색깔을 갖는 숫자 이미지였다면 색상을 표현하는 RGB(Red, Green, Blue) 각각이 별도의 픽셀이 되어야 하므로 feature 갯수는 768x3만큼이 된다. 텐서플로우에 포함된 cifar10 예제를 보면 나온다.

결국 이 말은 784칸 중에서 픽셀들이 칠해진 방식에 따라 숫자를 구분하겠다는 것과 같다. 이렇게 칠해져 있으면 0, 저렇게 칠해져 있으면 1이라고 판단하는 방식이다. 그래서, 고양이도 찾을 수 있고 자동차도 찾을 수 있는 것이다. 칠해진 방식을 갖고서 판단하니까.


데이터가 너무 커서 한 번에 읽어들이는 것은 무리다. 소스 코드에 보면 training_epochs는 25, batch_size는 100으로 되어 있다. 안쪽 for문에서는 num_examples 갯수만큼의 이미지를 가져와서 학습한다.

num_examples가 반환하는 값이 55,000이니까, 바깥쪽 반복문은 25회, 안쪽 반복문은 550회 반복한다. 한 번에 100개씩의 이미지를 처리하고 있다. next_batch 함수는 지정한 갯수만큼 image와 label을 순차적으로 반환하는 함수이다. 이 코드에서는 55,000개의 이미지를 모두 사용하는 대신, 13,750(25x550)개만 사용하고 있다. 그러나, mnist 전문가 예제에서는 10만개의 이미지를 사용한다.

여기서 중요한 코드는 mnist.train이다. mnist에는 3개의 데이터셋이 있는데, 그 중에서 train을 사용하고 있다. 지금은 학습을 해야 하므로 train을 사용하고, 학습이 끝나면 test를 사용한다.


이 코드는 교수님께서 알려주신 사이트에 없는 부분이다. 아래쪽에 첨부한 소스 코드에는 이 부분을 채워 넣었다. 정말 신기하게도 예측을 하는데, 몇 번 안해 봤지만 잘 맞는다.

여기서는 train이 아니라 test를 사용하고 있다. 전체 test 갯수 중에서 하나를 난수로 선택해서 test set으로부터 image와 label을 1개만 가져온다. [r:r+1]은 슬라이싱(slicing)에 해당하는 문법으로 r에서부터 r+1 이전까지의 범위를 나타낸다. 즉, r번째의 image와 label 하나를 가리킨다.

마지막에 있는 코드는 matplotlib 모듈을 사용해서 숫자 이미지를 출력해 준다. 원래는 그래프를 그리는 모듈인데, 이미지를 출력하기 위한 이미지 모듈까지 같이 갖고 있다.

정확도를 예측했는데, 91.46%가 나왔다. 가장 기본적인 것만 사용해서 91%니까, 굉장히 잘 나온 것이다. 그러나, deep learning을 사용하면 95% 정도는 쉽게 만들 수 있고, mnist 전문가 예제에서는 99.2%의 정확도를 보여준다.

argmax 함수는 가장 큰 값을 찾아서 1로 변환하는 one-hot encoding 알고리듬을 구현한다. argmax에 전달된 두 번째 매개변수 1은 차원을 의미하는데, 이 함수는 별도의 글에 따로 정리를 해두었다. ([텐서플로우 정리] 09. argmax 함수) argmax 함수를 거치면 가장 큰 확률을 갖는 요소만 1로 바뀌고 나머지는 0이 된다. 3번째 요소가 1이 되었다면, 이미지가 0부터 시작하므로 숫자 2를 가리킨다는 뜻이 된다. 이 값을 label과 비교하면 맞았는지 틀렸는지를 알 수 있다. equal 함수의 결과는 True 아니면 False다.

boolean 타입을 float 자료형으로 변환해서 평균을 구한다. True는 1로, False는 0으로 바뀐다. 맞는 것도 있고, 틀린 것도 있겠지만, 전체를 더해서 평균을 내면 0과 1 사이의 값이 나온다. 이 값이 얼마나 맞았는지를 알려주는 accuracy가 된다. 전체가 0이면 하나도 맞은 것이 없으므로 평균은 0이 된다.

accuracy에서 사용할 데이터로 mnist.test.images와 mnist.test.labels를 주었다. images는 예측하기 위해, labels는 예측한 것과 비교하기 위해.


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.1
training_epochs = 25
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
W = tf.Variable(tf.zeros([784, 10]))
b = tf.Variable(tf.zeros([10]))

# Construct model
activation = tf.nn.softmax(tf.matmul(x, W) + b) # Softmax

# Minimize error using cross entropy
cost = tf.reduce_mean(-tf.reduce_sum(y*tf.log(activation), reduction_indices=1))
# Gradient Descent
optimizer = tf.train.GradientDescentOptimizer(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))

# 추가한 코드. Label과 Prediction이 같은 값을 출력하면 맞는 것이다.
import random
r = random.randrange(mnist.test.num_examples)
print('Label : ', sess.run(tf.argmax(mnist.test.labels[r:r+1], 1)))
print('Prediction :', sess.run(tf.argmax(activation, 1), {x: mnist.test.images[r:r+1]}))

# 1줄로 된 것을 28x28로 변환
import matplotlib.pyplot as plt
plt.imshow(mnist.test.images[r:r+1].reshape(28, 28), cmap='Greys', interpolation='nearest')
plt.show()

print("Optimization Finished!")

# Test model
correct_prediction = tf.equal(tf.argmax(activation, 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}))

앞에서 충분히 설명했기 때문에 추가 설명은 없다. 필요한 부분만 짧게 주석을 달았다. 인터넷에서 가져온 데이터셋은 "/tmp/data/" 폴더에 저장한다고 되어 있다.


[출력 결과]
...
Epoch: 0021 cost= 0.269660726
Epoch: 0022 cost= 0.268667803
Epoch: 0023 cost= 0.267811905
Epoch: 0024 cost= 0.266890075
Epoch: 0025 cost= 0.266161013
label : [8]
Prediction : [8]
Optimization Finished!
Accuracy: 0.9216

출력 결과도 너무 길어서 뒤쪽 일부만 실었다. 난수가 8(label)을 전달했고, 8(Prediction)을 예측했으므로 맞았다. 정확도는 92.16% 나왔다.

18. Training-Testing 데이타 셋 (lec 07-2)


이전 글에서 overfitting을 피할 수 있는 방법으로 제안한 것 중에 "training dataset이 많으면 좋다"라고 얘기한 부분이 있었다. 이번 동영상은 데이터가 충분하지 않으면 사용할 수 없다는 것을 전제로 한다.


모델을 만들었다면 성능이 어떤지 평가해야 한다. 모델을 만들 때도 데이터가 필요하지만, 평가할 때도 데이터가 필요하다.


전체 데이터를 training data로 사용했다면, 현재 데이터에 대해 모두 기억을 하고 있다면, 현재 데이터에 대해서 100%의 정확도를 가질 수 있다. 그런데, 이게 의미가 있을까?

우리의 목표는 새로 들어오는 입력에 대해 결과를 예측하는 것인데, 그런 상황에서도 100% 정확도로 예측할 수 있을까? 의미는 조금 다르지만, 이런 경우 overfitting 되었다고 얘기할 수 있을 것이다.


전체 데이터를 2개 영역으로 나누어서 적용하는 것이 중요하다. 70% 정도는 학습을 시키기 위한 training set으로, 30% 정도는 학습 결과를 확인하기 위한 test set으로 구성하고 있다. 학습 과정에서 잘 나왔다고 test set에 대해서도 잘 동작할지는 알 수 없다. 여러 번에 걸쳐 학습(train)하고, 검증(test)하는 작업을 반복해야 한다.

학습한 결과를 토대로 test set에 대해 예측하고, test set의 실제 값과 결과를 비교해서 "목표로 하는 95% 수준에 도달했는지", "사용할 수 있는 수준이 되었는지"에 대해서 판단하게 된다.


데이터 영역별 구성도다. 데이터가 충분하다면 validation set을 구성하지 않을 이유가 없다. 쉽게 보면 test를 한번 더 한다고 생각해도 좋을 것이다. 머신러닝의 교과서에 해당하는 mnist 예제는 train 55,000개, validation 5,000개, test 10,000개의 dataset으로 구성되어 있다.


데이터가 너무 많아도 문제다. 이럴 경우에는 전체를 한번에 처리하지 않고 조금씩 나누어서 처리할 수도 있다. 이것을 online learning이라고 한다. 데이터가 너무 많거나 신규 데이터가 지속적으로 유입되는 상황에서 사용하는 모델이다.


Online Learning은 머신러닝의 대표 예제인 mnist dataset을 통해서 확인할 수 있다. 이 경우는 데이터가 너무 크다고 판단해서 10개의 구간으로 나누어서 처리를 하는 방식을 택할 수 있다. 다음 동영상에서 김성훈 교수님께서 이 부분을 코드로 구현해서 보여준다.

mnist 예제는 내부적으로는 앞서 얘기한 것처럼 3개의 dataset으로 구분되어 있지만, 코드 상에서는 training과 testing의 두 가지 dataset만 사용한다. 그리고, 그림을 구분하기 위한 label 파일을 별도로 제공하고 있다.

17. 학습 rate, Overfitting, 그리고 일반화 (Regularization) (lec 07-1)


cost를 계산하고 gradient descent 알고리듬을 구현하는 과정에서 고민했던 몇 가지 문제점들에서 대해서 이번 글에서 해소할 수 있었다.


gradient descent 그래프와 알고리듬을 텐서플로우로 구현했을 때의 그림이다. 여기서 learning rate은 변수 이름 그대로 learning_rate이 담당하고, 여기서의 값은 0.001이다. 다음 그림들에서는 이 값에 따라 달라지는 상황들에 대해 말씀하신다.


learning rate은 단순하게 보면 숫자에 불과하고 그 숫자는 프로그래밍에서처럼 "크다, 작다, 같다"의 3가지로 나누어질 수 있다.

왼쪽 그림은 learning rate이 너무 클 때의 그림으로, overshooting 현상이 발생하는 것을 보여준다. 보폭이 너무 크면 최저점을 지나 반대편 경사면으로 이동할 수 있게 되고 심할 경우 경사면을 타고 내려가는 것이 아니라 올라가는 현상까지 발생할 수 있다. overshooting은 경사면을 타고 올라가다가 결국에는 밥그릇을 탈출하는 현상을 말한다.

오른쪽 그림은 너무 작을 때를 보여준다. 한참을 내려갔음에도 불구하고 최저점까지는 아직 멀었다. 일반적으로는 조금씩 이동하는 것이 단점이 되지 않는데, 머신러닝처럼 몇만 혹은 몇십만 번을 반복해야 하는 상황에서는 치명적인 단점이 된다. 최저점까지 갈려면 한 달 이상 걸릴 수도 있으니까.

learning rate을 너무 크지 않게, 그러면서도 작지 않게 조절하는 것은 무척 어렵고, 많은 경험이 필요한 영역이다. 그림에서는 overshooting이 더 안 좋은 것처럼 설명되지만, 실제로는 overshooting이 발생하면 바로 알아챌 수 있기 때문에 문제가 되지 않는다. 오히려 learning rate이 작은 경우에 늦게 알아챌 수가 있다. 마치 정상적으로 내려가는 것처럼 보이니까. 두 가지 모두에 있어 여러 번의 경험을 통해 적절한지 혹은 적절하지 않은지에 대한 안목을 키우는 것이 최선이다.


적당한 learning rate을 찾는 것이 얼마나 중요한지 알았다. 그렇다면 learning rate을 찾기 위한 방법들에는 어떤 것들이 있을까? 나도 굉장히 놀란 부분인데, 특별한 방법은 전혀 없다. 교수님께서도 답답한 부분일거라고 생각한다. 몇 가지 일반론을 정리해 주셨다.

  다양한 learning rate을 사용해서 여러 번에 걸쳐 실행하는 것이 최선
  cost가 거꾸로 증가하는 overshooting 현상과 너무 조금씩 감소하는 현상 확인

그림에 있는 reasonable이라는 단어는 얼마나 모호한지.. 합리적 또한 합당한 learning rate이라는 것이 얼마나 주관적인 것인지 모두 알 것이다. 이 말은 머신러닝에 대해 많은 경험을 쌓아야 한다는 말과 같다. 교수님께서는 누구나 할 수 있다고 하셨는데, 나에게는 누구나 할 수 있는 것은 아닌 것처럼 보인다.


이번 그림부터는 데이터 선처리(preprocessing)에 대해서 설명하신다. preprocessing이라고 하는 것은 어떤 일을 본격적으로 하기 전에 진행되는 준비 작업을 가리킨다. 요리를 한다고 하면, 요리하기 전에 가스불을 점검하고 칼을 갈아놓고 빠진 양념을 확인하는 것들을 말한다.

multi-variables 모델에서, 변수라고 부르는 feature가 여러 개 있는 경우의 그래프는 위의 그림처럼 등고선 내지는 3차원 입체 이상으로 표현되어질 수밖에 없다. 등고선의 한 점에서 gradient descent 알고리듬으로 등고선의 중심인 최저점을 향해 간다고 할 때 매끄럽게 보이는 직선처럼 이동하는 것은 매우 어려울 수밖에 없다.


x1 변수는 10보다 작은 숫자, x2 변수는 -5000에서 9000까지의 숫자라면 진짜 동그랗게 생긴 원의 모양이 아니라 한쪽으로 길게 늘어진 타원 모양이 된다. 이렇게 된다면 수평으로 이동할 때와 수직으로 이동할 때 엄청난 불균형이 발생하게 되어 gradient descent 알고리듬을 적용하기 어려운 상황이 될 수 있다.

등고선으로 표현할 때, 가장 좋은 형태는 완변하게 둥근 원(circle)이다. 수평과 수직으로 동일한 범위를 갖게 만들면 가장 이상적인 원이 된다. gradient descent 알고리듬을 적용하기 전에 preprocessing 작업으로 데이터의 범위를 제한할 수 있다.


데이터를 preprocessing하는 두 가지를 보여준다. 최초 데이터는 중심에서 빗겨있는 상태이고, 한쪽 방향으로 길게 늘어진 형태이다. 데이터 전체가 중심에 올 수 있도록 이동시킬 수도 있고, 원에 가까운 형태로 만들 수도 있다.


Feature Scaling은 변수의 범위를 일정하게 혹은 비교할 수 있도록 만드는 것을 말한다. Normalization(Re-scaling)과 Standardization의 두 가지가 있는데, feature scaling 자체를 Data-Normalization으로 부르기도 하기 때문에, standardization을 normalization으로 호칭할 수도 있다.

아래 그림의 출처(http://gentlej90.tistory.com/26)


중요하니까, 설명 하나 더 추가. 설명을 빌려온 사이트(https://brunch.co.kr/@rapaellee/4). 길지 않게 잘 설명하고 있으니 꼭 가서 볼 것.

  Normalization
  수식 : (요소값 - 최소값) / (최대값 - 최소값)
  설명 : 전체 구간을 0~100으로 설정하여 데이터를 관찰하는 방법으로, 특정 데이터의 위치를 확인할 수 있게 해줌

  Standardization
  수식 : (요소값 - 평균) / 표준편차
  설명 : 평균까지의 거리로, 2개 이상의 대상이 단위가 다를 때, 대상 데이터를 같은 기준으로 볼 수 있게 해줌

김성훈 교수님은 standardization에 대해 설명하셨고, 앤드류 교수님은 normalization에 대해 설명하셨다. 그래서, 두 가지 모두 중요한 방법이다.


이번 그림부터는 머신러닝의 가장 큰 문제점인 overfitting에 대해서 보여준다. 우리가 만든 모델이 training dataset에 너무 잘 맞는 경우를 말한다. 너무 잘 맞을 때 문제가 되는 이유는, 정말 잘 맞추기 위해 과도하게 복잡해지기 때문이다. 그래서, 실제로 사용할 때는 오히려 맞지 않는 현상이 벌어지는데, 이것을 overfitting이라고 부른다.


많은 수의 데이터가 정확하게 그룹지어서 있을 수 있을까? 그림에서 보는 것처럼 어느 정도는 섞여있을 수밖에 없고, 완벽하게 예측한다는 것이 불가능함을 보여준다. 이런 상황에서 완벽하게 맞추기 위해 그림 오른쪽처럼 선을 구부리는 것처럼 모델을 구성할 수도 있다. 이럴 경우, 실제 사용에서 예측하게 되는 데이터에 대해서는 오히려 그림 왼쪽에 있는 단순한 직선보다도 예측을 잘 못하게 된다.


overfitting에는 해결책이 있다. learning rate에서는 꽤나 암담했었다.

  . training data가 많을 수록 좋다
  . 입력으로 들어오는 변수(feature, x)의 갯수를 줄여라
  . Regularization을 사용해라


Regularization은 W(weight)가 너무 큰 값들을 갖지 않도록 하는 것을 말한다. 값이 커지면, 그림에서 보는 것처럼 구불구불한 형태의 cost 함수가 만들어지고 예측에 실패하게 된다. 머신러닝에서는 "데이터보다 모델의 복잡도(complexity)가 크다"라고 설명한다. 과도하게 복잡하기 때문에 발생하는 문제라고 보는 것이다. 다시 말하면, Regularization은 모델의 복잡도를 낮추기 위한 방법을 말한다.


모델을 구축했을 때의 3가지 경우를 보여준다. underfitting은 너무 대충 맞춰서 error가 많이 발생하는 현상을 말하는데, 일부러 그럴려는 사람은 없기 때문에 underfitting이 발생하는 것은 머신러닝에 대한 지식 부족일 확률이 높다. 반면, overfitting은 너무 잘 맞춰서 error가 거의 발생하지 않는 현상으로, train dataset에 대해서는 완벽하지만 test dataset을 비롯한 나머지 데이터에 대해서는 underfitting처럼 많은 error가 발생하게 된다.

W가 크면 예측하려는 값(y hat)이 정상적인 규칙으로부터 벗어나 있는 경우에도 예측이 가능하다. 그러나, 비정상적이거나 애매한 위치에 있는 데이터를 올바르게 예측하는 것을 '맞았다'라고 얘기할 수는 없다. 오히려 '틀렸다'라고 얘기하는 것이 더욱 좋을 수 있다. 이럴 경우 training dataset에 특화된 overfitting 현상이 발생한다. 가장 이상적인 경우는 최소한의 에러를 인정하는 'Just right'이다.


Regularization을 구현하는 것은 매우 쉽다. cost 함수가 틀렸을 때 높은 비용이 발생할 수 있도록 벌점(penalty)을 부과하는 것처럼 W에 대한 값이 클 경우에 penalty를 부여하면 된다.

W에 대해 제곱을 한 합계를 cost 함수에 더하는 것이 전부다. 다만 합계를 어느 정도로 반영할지 결정할 수 있어야, 사용하는 시점에서 다양한 적용이 가능하다. 람다(λ)라고 부르는 값을 사용해서 얼마나 penalty를 부여할 것인지 결정할 수 있다.

16. TensorFlow로 Softmax Classification의 구현하기 (lab 06)

이번 글에서는 텐서플로우에서 제공하는 softmax 알고리듬과 이를 적용한 결과를 살펴본다.


아래는 동영상에서 나온 코드에 주석을 붙인 전체 소스코드이다. 05train.txt

import tensorflow as tf
import numpy as np

# softmax이기 때문에 y를 표현할 때, 벡터로 표현한다.
# 1개의 값으로 표현한다고 할 때, 뭐라고 쓸지도 사실 애매하다.

# 05train.txt
# #x0 x1 x2 y[A B C]
# 1 2 1 0 0 1 # C
# 1 3 2 0 0 1
# 1 3 4 0 0 1
# 1 5 5 0 1 0 # B
# 1 7 5 0 1 0
# 1 2 5 0 1 0
# 1 6 6 1 0 0 # A
# 1 7 7 1 0 0

xy = np.loadtxt('05train.txt', unpack=True, dtype='float32')

# xy는 6x8. xy[:3]은 3x8. 행렬 곱셈을 하기 위해 미리 transpose.
x_data = np.transpose(xy[:3])
y_data = np.transpose(xy[3:])

print('x_data :', x_data.shape) # x_data : (8, 3)
print('y_data :', y_data.shape) # y_data : (8, 3)

X = tf.placeholder("float", [None, 3]) # x_data와 같은 크기의 열 가짐. 행 크기는 모름.
Y = tf.placeholder("float", [None, 3]) # tf.float32라고 써도 됨

W = tf.Variable(tf.zeros([3, 3])) # 3x3 행렬. 전체 0.

# softmax 알고리듬 적용. X*W = (8x3) * (3x3) = (8x3)
hypothesis = tf.nn.softmax(tf.matmul(X, W))

# cross-entropy cost 함수
cost = tf.reduce_mean(-tf.reduce_sum(Y * tf.log(hypothesis), reduction_indices=1))

learning_rate = 0.01
train = tf.train.GradientDescentOptimizer(learning_rate).minimize(cost)

init = tf.initialize_all_variables()

with tf.Session() as sess:
sess.run(init)

for step in range(2001):
sess.run(train, feed_dict={X: x_data, Y: y_data})
if step % 200 == 0:
feed = {X: x_data, Y: y_data}
print('{:4} {:8.6}'.format(step, sess.run(cost, feed_dict=feed)), *sess.run(W))

print('-------------------------------')

# 1은 bias로 항상 1. (11, 7)은 x 입력
a = sess.run(hypothesis, feed_dict={X: [[1, 11, 7]]})
print("a :", a, sess.run(tf.argmax(a, 1))) # a : [[ 0.68849683 0.26731509 0.04418806]] [0]

b = sess.run(hypothesis, feed_dict={X: [[1, 3, 4]]})
print("b :", b, sess.run(tf.argmax(b, 1))) # b : [[ 0.2432227 0.44183081 0.3149465 ]] [1]

c = sess.run(hypothesis, feed_dict={X: [[1, 1, 0]]})
print("c :", c, sess.run(tf.argmax(c, 1))) # c : [[ 0.02974809 0.08208466 0.8881672 ]] [2]

# 한번에 여러 개 판단 가능
d = sess.run(hypothesis, feed_dict={X: [[1, 11, 7], [1, 3, 4], [1, 1, 0]]})
print("d : ", *d, end=' ')
print(sess.run(tf.argmax(d, 1))) # d : ... [0 1 2]
[출력 결과]
...
1200 0.780959 [-1.06231141 -0.26727256 1.32958329] [ 0.06808002 -0.11823837 0.05015869] [ 0.17550457 0.23514736 -0.41065109]
1400 0.756943 [-1.19854832 -0.29670811 1.49525583] [ 0.0759144 -0.11214781 0.0362338 ] [ 0.19498998 0.23733102 -0.43232021]
1600 0.735893 [-1.32743549 -0.32218221 1.64961684] [ 0.08333751 -0.10558002 0.02224298] [ 0.21336637 0.23823628 -0.45160189]
1800 0.717269 [-1.44994986 -0.34407791 1.79402602] [ 0.09020084 -0.09902247 0.00882212] [ 0.23099622 0.2384187 -0.46941417]
2000 0.700649 [-1.56689751 -0.36275655 1.92965221] [ 0.09643653 -0.09271803 -0.00371794] [ 0.248116 0.23818409 -0.48629922]
-------------------------------
a : [[ 0.68849683 0.26731509 0.04418806]] [0]
b : [[ 0.2432227 0.44183081 0.3149465 ]] [1]
c : [[ 0.02974809 0.08208466 0.8881672 ]] [2]
d : [ 0.68849683 0.26731509 0.04418806] [ 0.2432227 0.44183081 0.3149465 ] [ 0.02974809 0.08208466 0.8881672 ] [0 1 2]

핵심적인 부분만 설명하고, 나머지는 주석으로 대신한다. 이전 글들의 소스코드에서 설명된 내용 또한 생략한다. 출력 결과에서 W는 3행 3열이기 때문에 총 9개의 결과로 나타나고 있다. 많다고 생각할 수도 있지만, 실전에서는 수천 개의 weight을 다루게 되고, 지금처럼 출력할 생각은 하지 않게 된다. 몇 개가 됐든 가중치는 변경되어서 최저 cost를 찾아가야 한다.


# xy는 6x8. xy[:3]은 3x8. 행렬 곱셈을 하기 위해 미리 transpose.
x_data = np.transpose(xy[:3])
y_data = np.transpose(xy[3:])

loadtxt 함수는 파일을 읽을 때 행과 열을 바꿔서 읽어오기 때문에, 행렬 곱셈을 위해 먼저 transpose 시켰다. y_data에 들어가는 값이 1차원이 아니라 2차원이 됐다. y_data는 3번째부터 전체라고 표현했으니까, 6행 8열의 데이터에서 뒤쪽에 오는 3행 8열을 transpose 시켜서 8행 3열이 되었다.


X = tf.placeholder("float", [None, 3])  # x_data와 같은 크기의 열 가짐. 행 크기는 모름.
Y = tf.placeholder("float", [None, 3]) # tf.float32라고 써도 됨

W = tf.Variable(tf.zeros([3, 3])) # 3x3 행렬. 전체 0.

2차원 배열을 placeholder로 지정하는 문법으로 여기서 처음 나왔다. 앞에 오는 None은 행의 갯수를 알 수 없기 때문이고, 열의 갯수는 언제나 3이어야 한다. 행은 한두 개 없어도 될만큼 가볍지만, 열은 feature로 나타나는 핵심 요소라는 것을 기억하자. 설마 행이 8개라고 생각하는 것은.. 아니겠지? W는 binary classification이 3개 들어가야 하니까, 3행 3열로 표현했다.

만약에 feature가 하나 늘어난다면, 다시 말해 x_data가 8x3이 아니라 8x4가 된다면 W는 행이 증가할까, 열이 증가할까?

W에서 행은 binary classification을, 열은 feature 갯수를 의미한다. 4가지 중에서 골라야 한다면 행이 증가하고, x의 특징을 가리키는 열, 즉 feature가 추가됐다면 열이 증가한다.


# softmax 알고리듬 적용. X*W = (8x3) * (3x3) = (8x3)
hypothesis = tf.nn.softmax(tf.matmul(X, W))

softmax 알고리듬이다. W와 X를 곱하는 게 아니라, X와 W를 곱하고 있다. X는 8행 3열, W는 3행 3열. 행렬 곱셈으로 손색이 없다. W와 X의 위치를 바꾸면 당연히 에러다. X의 각 행에는 x0(bias)와 x1, x2가 들어 있고, 이 값을 토대로 y를 예측해야 한다. 그러니 X의 행과 W의 열을 곱하는 것이 맞다.

x_data를 transpose 시키지 않고 읽어온 그대로 사용한다면 matmul(W, X)라고 해야 한다. W를 앞에 두는 것이 편하기 때문에 이게 좋아보일 수도 있다. 이럴 경우 W는 행으로 동작하고, X는 열로 동작하게 된다. 3행 3열 x 3행 8열의 행렬 곱셈이 되어서 결과는 3행 8열이 된다. 그런데, 결과를 추출할 때 불편하다. 값이 열 단위로 되어 있기 때문에 각각의 행에서 하나씩 가져와서 조합해야 판단이 가능해진다. 별건 아니지만, 이런 불편한 점 때문에 W와 X를 바꿔서 계산을 한다.

또 하나, W를 뒤에 곱할 때는 열 단위로 읽게 되는데, 지금은 앞에 나왔기 때문에 행 단위로 계산을 해야 한다. transpose할 필요까지는 없겠지만, 이 사실을 알고 처음 값을 줄 때 열 단위로 주어야 한다는 점은 이해해야 한다.


# cross-entropy cost 함수
cost = tf.reduce_mean(-tf.reduce_sum(Y * tf.log(hypothesis), reduction_indices=1))

cross-entropy cost 함수의 텐서플로우 버전이다. log 함수를 호출해서 hypothesis를 처리하고 있다. hypothesis는 softmax를 거쳤으므로 0과 1 사이의 값들만 갖게 된다.

여기서 Y는 공식에서는 L(label)로 표현되었던 값으로 실제 y 값을 의미한다. 즉, L * log(S)를 처리하는 부분이 reduce_sum에 Y * tf.log(hypothesis)이다. 곱셈(*)은 행렬 곱셈이 아닌 element-wise 곱셈이다. 텐서플로우의 행렬 곱셈은 matmul 함수이다.


Y * tf.log(hypothesis) 결과는 행 단위로 더해야 한다. 그림에서 보면, 최종 cost를 계산하기 전에 행 단위로 결과를 더하고 있다. 이것을 가능하게 하는 옵션이 reduction_indices 매개변수다. 0을 전달하면 열 합계, 1을 전달하면 행 합계, 아무 것도 전달하지 않으면 전체 합계. 이렇게 행 단위로 더한 결과에 대해 전체 합계를 내서 평균을 구하기 위해 reduce_mean 함수가 사용됐다.


# 1은 bias로 항상 1. (11, 7)은 x 입력
a = sess.run(hypothesis, feed_dict={X: [[1, 11, 7]]})
print("a :", a, sess.run(tf.argmax(a, 1))) # a : [[ 0.68849683 0.26731509 0.04418806]] [0]

학습한 결과를 토대로 학점을 예측하고 있다. 이 학생은 A를 맞았다고 나오는 것이 아니라 A를 가리키는 인덱스인 0을 반환하고 있다. argmax 함수는 one-hot encoding을 구현하는 텐서플로우 함수다.

15. Softmax classifier 의 cost함수 (lec 06-2)

동영상이 2개로 되어 있고, 이전 글에서 sigmoid는 어디에 있는가,까지 진행했다.


그림 중간에 빨강색으로 표시된 2.0, 1.0, 0.1이 예측된 Y의 값이다. 이것을 Y hat이라고 부른다고 했다. 이 값은 W에 X를 곱하기 때문에 굉장히 크거나 작은 값일 수 있다. 그래서, 이 부분 뒤쪽에 sigmoid가 들어가서 값을 0과 1 사이로 조정하게 된다.


그림에서 예측한 결과 y가 1개가 아니라 3개라는 점은 진짜 중요하다. 선택 가능한 옵션이 a, b, c의 3개가 있어서 binary classification을 3개 사용했고 각각의 결과를 저장해야 하므로 3개가 된다. binary classification을 세 번에 걸쳐 적용하고 있다는 것을 기억하자.

이들 값을 0과 1 사이의 값으로 바꾸니까, 각각 0.7, 0.2, 0.1이 됐다. 이들을 모두 더하면 1이 된다. a, b, c 중에서 하나를 고르라면 a를 선택하게 된다.


softmax는 점수로 나온 결과를 전체 합계가 1이 되는 0과 1 사이의 값으로 변경해 준다. 전체를 더하면 1이 되기 때문에 확률(probabilites)이라고 부르면 의미가 더욱 분명해진다. 0.7이라는 뜻은 70%의 확률로 a가 될 수 있다는 뜻이다. 검정 상자 안에는 softmax를 구현하는 공식이 표시되어 있는데, 텐서플로우에서는 softmax 함수가 있어서 그냥 호출하면 끝난다.

교수님께서는 이 부분을 가볍게 말씀하셨는데, 공식이 어렵지 않고, 코드로 구현하는 것도 어렵지 않다. 확률이기 때문에 전체 합계는 1이 되어야 한다. 다시 말해, 나의 크기가 전체 크기 중에서 어느 정도인지를 판단하면 되기 때문에 (내 크기/전체 합계) 공식으로 표현할 수 있다. 분모에는 시그마(∑)가 있고, 분자에는 없는 것을 볼 수 있다.

softmax는 두 가지 역할을 수행한다.
1. 입력을 sigmoid와 마찬가지로 0과 1 사이의 값으로 변환한다.
2. 변환된 결과에 대한 합계가 1이 되도록 만들어 준다.

y를 예측한 이후부터의 과정을 알려주는 그림이다. one-hot encoding은 softmax로 구한 값 중에서 가장 큰 값을 1로, 나머지를 0으로 만든다. 어떤 것을 선택할지를 확실하게 정리해 준다. one-hot encoding은 설명한 것처럼 매우 간단하기 때문에 직접 구현할 수도 있지만, 텐서플로우에서는 argmax 함수라는 이름으로 제공하고 있다.


지금까지 글을 작성하면서 가장 어려웠던 부분이 cost 함수였던 것 같다. 이번에도 여지없이 비용을 측정하는 cost 함수가 나왔고, 색깔이 그다지 좋지 않다.

entropy는 열역학에서 사용하는 전문 용어로 복잡도 내지는 무질서량을 의미한다. 엔트로피가 크다는 것은 복잡하다는 뜻이다. cross-entropy는 통계학 용어로, 두 확률 분포 p와 q 사이에 존재하는 정보량을 계산하는 방법을 말한다. 다행스럽게 cross-entropy라는 용어에 엄청나게 심오한 이론이 숨어있는 것 같지는 않다.

S(Y)는 softmax가 예측한 값이고, L(Y)는 실제 Y의 값으로 L은 label을 의미한다. cost 함수는 예측한 값과 실제 값의 거리(distance, D)를 계산하는 함수로, 이 값이 줄어드는 방향으로, 즉 entropy가 감소하는 방향으로 진행하다 보면 최저점을 만나게 된다.


cross-entropy cost 함수가 제대로 동작한다는 것을 풀어서 설명하고 있다. 공식의 오른쪽에 나타난 log는 logistic regression에서 이미 봤다. 그림 오른쪽에 있는 것처럼 1을 전달하면 y는 0이 되고, 0을 전달하면 y는 무한대가 된다. 이때, 비용이 최소로 나오는 것을 선택해야 하므로, 전체 결과가 무한대가 나온다면 선택할 수 없다는 것을 뜻한다.

지금 그림은 두 가지 중에서 한 가지를 선택하는 것을 보여주고 있다. 이전 그림은 a, b, c 중에서 선택을 하는 것이기 때문에 이번 그림과는 일부 맞지 않는다. 세 가지라면 label에 들어가는 값이 [0, 1]이 아니라 a(1,0,0)나 b(0,1,0)처럼 세 개의 값이 들어가는 부분이 다르다. 추가적으로 y hat 또한 3개가 될 것이고, 계산은 조금 더 복잡해질 것이다.

그런데, 처음 공식의 log에는 S가 들어가 있었는데, 실제 계산은 y hat으로 하고 계신다. 0과 1로 log를 취하면 결과가 분명하기 때문에 그렇게 하신 것이지만, 의미가 분명하지 않을 수도 있다. 가령, S는 sigmoid를 가리키고 0부터 1 사이의 실수이므로 실제 결과는 0 또는 무한대가 아니라 작은 값이나 큰 값이 나오게 된다. 이들 값을 취합해서 cost를 계산하고 그에 따라 weight를 계산하면서 조절해서 최저점을 찾게 된다.


교수님께서 보여주신 식 전개를 다시 썼다. L은 label의 약자로 Y의 실제 값이다. Y hat은 Y를 예측한 값으로, 맞게 예측했을 때와 틀리게 예측했을 때를 보여주고 있다. 여기서 왜 L이 1개의 값이 아니라 2개의 값을 갖는지 이해를 못했었다. 처음 들었을 때는 그냥 그러려니 했는데, 생각할수록 이해가 되지 않았던 부분이다. 다음 글에 나오는 텐서플로우 예제를 보면, 실제 L의 값은 3개짜리 배열로 표현된다. A, B, C 중에서 고르니까.

결론부터 말하면 binary classification을 두 번 진행한다는 뜻이다. 이전 그림처럼 3개 중에서 하나를 고른다면, L의 크기는 3이 되어야 한다. 각각의 요소는 binary classification의 결과다. 공식에서 보면 L의 i번째라고 표현하는 것은, 여기서는 두 개 있으니까 두 번 반복하게 된다. Y를 예측한 값이 0 또는 1일 수밖에 없는 이유는 one-hot encoding을 거쳤기 때문이다.

위의 공식이 A와 B 중에서 하나를 선택해야 한다면, 현재 L에 들어있는 값은 B에 해당하는 요소가 1이므로 B를 가리킨다. 첫 번째 Y hat은 B를 가리키니까 맞게 예측했고, 두 번째 Y hat은 A를 가리키므로 잘못 예측했다. element-wise 곱셈을 적용한 최종 결과를 보면, 맞게 예측했을 때는 0이 나오고, 잘못 예측했을 때는 무한대가 나왔다. 잘 동작하는 cost 함수임을 알 수 있다.

동영상에서는 L의 값이 (1,0)일 때, 즉 A일 때에 대해서도 보여주지만, 똑같은 설명의 반복이므로 생략한다. 그러나, 이 글을 보는 사람은 직접 이 부분에 대해 손으로 직접 써봐야 한다. 나는 그렇게 이해했다.


여기서 교수님께서 과제를 내주셨다. logistic regression에서 사용했던 cost 함수와 multinomial classification의 cross-entropy cost 함수가 똑같다고 말씀하셨다. 같은 이유에 대해 살짝 설명하셨는데, 나에게는 정말 살짝이었다.

왜 두 개의 cost 함수가 같은 것일까? 동영상을 다섯 번쯤 보고 앤드류 교수님 수업도 듣고 하면서 답을 찾은 것 같다.

cost 함수의 목적은 틀렸을 때 벌을 주어서 비용을 크게 만들어야 하는데, 양쪽 모두 무한대라는 벌칙을 적용한다. 다만 logistic regression에서는 2개의 log식을 연결해서 사용하지만 cross-entropy에서는 행렬로 한 번에 계산하는 방식을 취할 뿐이다. 즉, logistoic regression을 cross-entropy로 처리할 수 있다. 바로 앞에 나온 2개 중에 하나를 선택하는 그림이 logistoic regression의 cross-entropy 버전이다. cross entropy 하나로 logistic regression까지 처리할 수 있으므로, 포함 관계로 생각할 수도 있을 것 같다. 어찌 됐든 cross entropy 하나만 하면 된다는 뜻이다.


cost 함수를 다시 한번 강조하셨다. WX + b는 지겹도록 본 공식으로 y를 예측한다. 왼쪽에 있는 L은 loss의 약자로 다른 말로는 cost 또는 error라고 한다. 비용이라고 하는 것은 결국 잘못 예측했을 때의 값, 즉 에러(error)라고 볼 수 있다. training set을 갖고 작업해야 한다고도 얘기한다. training set으로 학습하고 validation set으로 검증하고, test set으로 최종 확인까지 한다고 나중에 나온다.


cross-entropy cost 함수를 만들었다면, gradient descent 알고리듬에 적용해서 최소 비용을 찾아야 한다. 역시 이때 중요한 것은 그림에 표현된 w1, w2의 값이다. 알파(a)는 learning rate로 어느 정도로 이동할 것인지를 알려주고, 알파 오른쪽의 삼각형은 미분을 한다는 뜻이다.

예전 글에서 gradient descent 알고리듬을 구현하기 위해서는 cost 함수와 gradient를 계산하는 함수가 모두 필요하다고 얘기했었다. 여기서 보여주려는 것이 learning rate에 미분 결과를 곱한 값을 빼야 한다는 점이다. 앞에 음수 기호(-)가 있다.

김성훈 교수님께서는 이걸 미분하는 것은 너무 복잡하기 때문에 생략한다고 말씀하셨다. 앤드류 교수님도 이 부분에서 octave에서 제공하는 함수를 써서 처리하고 실제 구현은 보여주지 않으셨다.