'딥러닝_김성훈교수님'에 해당되는 글 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에서 제공하는 함수를 써서 처리하고 실제 구현은 보여주지 않으셨다.

14. Softmax Regression- 기본 개념 소개 (lec 06-1)

Logistic Regression을 부르는 다른 이름은 binary classification이다. 데이터를 1과 0의 두 가지 그룹으로 나누기 위해 사용하는 모델이다. softmax는 데이터를 2개 이상의 그룹으로 나누기 위해 binary classification을 확장한 모델이다.

softmax라는 용어 때문에 많이 헷갈렸었다. 통계에서 가장 큰 값을 찾는 개념을 hardmax라고 부른다. softmax는 새로운 조건으로 가장 큰 값을 찾는 개념을 말한다. 일반적으로는 큰 숫자를 찾는 것이 hardmax에 해당하고, 숫자를 거꾸로 뒤집었을 경우에 대해 가장 큰 숫자를 찾는다면 softmax에 해당한다. 여기서는 우리가 알고 있는 큰 숫자를 찾는 것이 아니라는 뜻으로 쓰인다.


앞에서 배운 logistic regression을 보여주는 그림이다. 좌표상에 표현된 데이터를 2개의 그룹으로 나누는 decision boundary 직선이 인상적이다. 여기서 중요한 것은 Wx의 결과로 z가 나오고, S로 표현되는 sigmoid에 전달되고 최종적으로 Y를 예측한다. Y에 모자를 씌운 Y hat은 Y를 예측한 값을 의미한다.


여러 개의 label을 갖는 multinomial classification을 어떻게 구현할 수 있는지 보여주는 그림이다. 좌표상에 A, B, C의 3개의 그룹이 있고, 오른쪽 그림에서 binary classification에서 사용한 decision boundary를 여러 개 그려 놓았다.

이 원리는 프로그래밍에서 사용하는 if문과 완전히 동일하다. if문으로는 2개 중에 하나만 선택할 수 있는데, 이걸 여러 번 늘어 놓아서 다중 선택을 구현하게 된다. if문과 똑같이 A이거나 아니거나, B이거나 아니거나, C이거나 아니거나와 같은 방식으로 걸러내게 된다.


여러 개로 분류를 하기 위해서는 그림 오른쪽에 있는 것처럼 여러 개의 binary classification이 필요하다는 말씀을 하셨다.


binary classification을 구현하려면, 그림 왼쪽에 있는 것처럼 각각 행렬 1개씩을 요구한다. 3개의 binary classification이 필요하니까, 이걸 3x3 행렬로 결합하는 그림이 오른쪽에 있다. 복잡해졌다고 생각할 수 있지만, 프로그래밍에서는 변수 1개로 처리할 수 있기 때문에 당연히 결합해야 한다.

이번 그림에서 나중까지 잘못 알고 있었던 사실이 하나 있다. 왜 그랬는지 여러 번 반복해야 하는 것은 W가 아니라 x라고 생각했었다. 3x3 행렬에는 x가 들어가야 한다고 생각했다.

softmax는 binary classification을 여러 번 결합한 결과다. 예측 결과가 A, B, C 중의 하나가 되어야 한다면, 동일한 x에 대해 A가 될 확률, B가 될 확률, C가 될 확률을 모두 구해야 한다. x는 3번 사용되지만, W는 A, B, C에 대해서 한 번씩 필요하니까 3번 반복된다. 그래서, W는 예측해야 하는 숫자만큼 필요하게 된다. 10개 중에서 예측한다면 10개의 W가 나와야 한다.

반면 x는 위의 그림만 놓고 보면, 1개밖에 없는 것처럼 보일 수 있다. 나중에는 100개 또는 1,000개 정도로 데이터의 갯수가 커지면, x 또한 행렬이 된다. 물론 지금도 행렬이긴 하지만, 3행 1열이라서 행렬같은 느낌은 없다.


행렬 곱셈을 보여주고 있다. 행렬 A와 행렬 B를 곱하면, A의 행과 B의 열을 곱하게 되어 있다. 그래서, 이번 행렬 곱셈의 결과는 3행 1열의 행렬이 만들어 진다.


binary classification을 여러 번 적용해서 구한 행렬에 대해 sigmoid를 적용하는 곳은 어디인지 묻고 있다. 이 부분은 다음 글에서 살펴본다.

13. 파이썬으로 Logistic Regression 직접 구현

제목을 붙일 게 없어서 직접 구현한다고는 했지만, 거짓말이다. 직접 구현하려면 cost 함수와 gradient descent 알고리듬의 2개를 모두 만들어야 하는데, 여기서는 cost 함수만 구현했다. 왜 그랬는지는 차차 알아보자.


import math
import numpy as np

# z는 값(scalar)일 수도 있고, vector 또는 matrix일 수도 있다.
def sigmoid(z):
return 1 / (1 + math.e ** -z)

print(sigmoid(100))
print(sigmoid( 0))
print(sigmoid(-10))
print(sigmoid(np.array([100, 0, -10])))
[출력 결과]
1.0
0.5
4.539786870243442e-05
[ 1.00000000e+00 5.00000000e-01 4.53978687e-05]

정말 중요한 함수인 sigmoid는 쉽게 구현할 수 있었다. 어떤 숫자가 들어와도 0과 1 사이의 값으로 변환해주는 함수로, 여기서는 100, 0, -10의 세 가지를 사용했다. 0을 넣었을 때, 가운데 값이므로 정확하게 0.5가 나와야 하는 것도 중요하다.

math 모듈에 자연상수 e가 들어있는 것을 찾았다. 지수 연산자인 **를 사용해서 아주 쉽게 분모를 구성했고, 분자로는 1을 주었다. 파이썬3에서는 //는 정수 나눗셈, /는 실수 나눗셈이다. sigmoid는 나중에 행렬까지 처리할 수 있어야 하기 때문에 numpy의 배열을 사용해서 검증까지 했다. 리스트는 행렬 연산을 지원하지 않는다.

sigmoid 함수를 아래처럼 만들 수도 있다.

  def sigmoid(z):
      return 1 / (1 + math.exp(-z))


ex2data1.txt - 소스코드에서 사용하는 데이터 파일.

import math
import numpy as np
import matplotlib.pyplot as plt

# z는 값(scalar)일 수도 있고, vector 또는 matrix일 수도 있다.
def sigmoid(z):
return 1 / (1 + math.e ** -z)

def costFunction(W, X, y):

m = y.size # 100

# 최초 실행시 값 : [[ 0.5] [ 0.5] [ 0.5] ... [ 0.5]]
h = sigmoid(np.dot(W, X)) # 1행 m열

# 값 1개. 곱셈(*)은 element-wise 곱셈
cost = -(1/m) * sum(y*np.log(h) + (1-y)*np.log(1-h))

# (h-y)는 1행 m열
grad = (1/m) * np.dot(X, h-y)

return cost, grad

# ex2data1.txt 파일에는 아래와 같은 줄이 100개 있다. 쉼표로 구분할 수 있는 csv 파일.
# 34.62365962451697,78.0246928153624,0
# 30.28671076822607,43.89499752400101,0
# 35.84740876993872,72.90219802708364,0
# 60.18259938620976,86.30855209546826,1
# 79.0327360507101,75.3443764369103,1

xy = np.loadtxt('ex2data1.txt', unpack=True, dtype='float32', delimiter=',')

print(xy.shape) # (3, 100). 행과 열을 바꿔서 읽어온다.
print(xy[:,:5]) # numpy 문법. 리스트는 안됨

# [[ 34.62366104 30.28671074 35.84740829 60.18259811 79.03273773]
# [ 78.02469635 43.89499664 72.90219879 86.3085556 75.34437561]
# [ 0. 0. 0. 1. 1. ]]

x_data = xy[:-1] # 2행 100열. 정확하게는 2차원 배열
y_data = xy[-1] # 1행 100열. 정확하게는 1차원 배열

# y_data가 1 또는 0인 값의 인덱스 배열 생성
pos = np.where(y_data==1)
neg = np.where(y_data==0)

# 옥타브와 비슷한 형태로 그래프 출력
# x_data[0,pos]에서 0은 행, pos는 열을 가리킨다. 쉼표 양쪽에 범위 또는 인덱스 배열 지정 가능.
t1 = plt.plot(x_data[0,pos], x_data[1,pos], color='black', marker='+', markersize=7)
t2 = plt.plot(x_data[0,neg], x_data[1,neg], markerfacecolor='yellow', marker='o', markersize=7)

plt.xlabel('exam 1 score')
plt.ylabel('exam 2 score')
plt.legend([t1[0], t2[0]], ['Admitted', 'Not admitted']) # 범례

plt.show()

# ---------------------------------------------------------------------- #

n, m = x_data.shape # [2, 100]. 행과 열의 크기
print('m, n :', m, n)

# 1로 구성된 배열을 맨 앞에 추가
x_data = np.vstack((np.ones(m), x_data))
print(x_data.shape) # 100
print(x_data[:,:5])

# [[ 1. 1. 1. 1. 1. ]
# [ 34.62366104 30.28671074 35.84740829 60.18259811 79.03273773]
# [ 78.02469635 43.89499664 72.90219879 86.3085556 75.34437561]]

W = np.zeros(n+1) # [ 0. 0. 0.]. 1행 3열

cost, grad = costFunction(W, x_data, y_data)
print('------------------------------')
print('cost :', cost) # cost : 0.69314718056
print('grad :', *grad) # grad : -0.1 -12.0092164707 -11.2628421021

앤드류 교수님 수업에 나오는 과제를 파이썬으로 옮겨 보았다. 코딩은 좀 하는 편인데, 여기 와선 도대체 뭘 하라는 건지 감을 못잡겠다. 어찌어찌 컨닝도 하고 해가면서 간신히 만들었다. 출력 결과는 주석에 실은 관계로 따로 표시하지는 않았다.


# 최초 실행시 값 : [[ 0.5] [ 0.5] [ 0.5] ... [ 0.5]]
h = sigmoid(np.dot(W, X)) # 1행 m열

sigmoid 함수는 앞에서 이미 봤다. costFunction 함수야말로 이번 코드의 핵심이다. costFunction 함수에서 sigmoid를 호출하고 있다.

  H(X) = Wx + b

이 공식은 linear regression에서 hypothesis에 해당하는 핵심 공식으로, cost를 계산하는 기초가 된다. Logistic Regression이 나온 이유가, 이 공식의 결과가 1보다 크거나 0보다 작을 수 있기 때문이었다. 다시 말해, 이 공식의 결과를 sigmoid에 전달해야 한다는 것이다.

W와 X는 모두 행렬이기 때문에 행렬 곱셈을 수행한 결과를 전달해야 한다. sigmoid 호출 결과는 행렬이 들어왔다면, 그와 똑같은 형태의 행렬을 반환한다. numpy에서의 행렬 곱셈은 dot 함수가 수행한다. W는 1행 3열, X는 3행 100열이므로 결과인 h는 1행 100열이 된다. 여기서는 이 함수를 1회 호출하므로, h에 들어가는 결과는 주석에 있는 것처럼 전체가 0.5이다.


# 값 1개. 곱셈(*)은 element-wise 곱셈
cost = -(1/m) * sum(y*np.log(h) + (1-y)*np.log(1-h) )

-(1/m) ∑ylog(H(x)) + (1-y)log(1-H(x))을 구현하고 있다. h는 앞에서 sigmoid에 적용한 결과로 모든 값은 0과 1 사이에 있다는 것을 보장한다. y와 log(h)를 곱하고, (1-y)와 log(1-h)를 곱한다. 이때 행렬 곱셈이 아니라 같은 자리끼리 곱하는 element-wise 곱셈이다. 3행 5열끼리의 곱셈처럼, 곱셈에 참여하는 행렬의 크기가 서로 같아야 한다. cost 변수에 들어가는 결과는 비용이라고 부르는 값 1개이다.


# (h-y)는 1행 m열
grad = (1/m) * np.dot(X, h-y)

h와 y는 모두 1차원 배열이고 크기는 똑같이 m개를 갖고 있다. element-wise 뺄셈을 적용한다. X와 (h-y)를 행렬 곱셈한다. X는 3행 100열, (h-y)는 1행 100이다. 일반적인 행렬 연산에서는 에러가 발생해야 한다. 뒤에 오는 (h-y)는 100행 1열이어야 어울리기 때문이다. 그러나, 이 경우 numpy는 행렬 연산을 100행 1열인 것처럼 적용해서 결과는 1행 100열로 변환까지 해준다. np.dot(X, h-y)의 결과는 크기가 m인 1차원 배열이 된다. 여기에 element-wise 곱셈인 1/m을 한다.


matplotlib 모듈을 사용해서 x_data를 그래프로 출력했다. 앤드류 교수님 과제 제출에 이것도 포함되어 있어서 비슷하게 만들어 봤다. 파이썬 그래프는 핵심이 아니니까, 설명은 코드에 있는 주석으로 대신한다.


# 1로 구성된 배열을 맨 앞에 추가
x_data = np.vstack((np.ones(m), x_data))

numpy에서 새로운 행을 맨 앞에 추가하기 위한 코드다. 정확하게는 2개의 배열을 연동해서 새로운 배열을 만든다. y 절편에 해당하는 b를 행렬의 맨 앞에 추가해야 Wx + b의 코드가 완성된다. ones 함수는 원하는 형태의 배열을 만들고 1로 채워준다. 뒤에는 0으로 채워주는 zero 함수도 나온다.


W = np.zeros(n+1)           # [ 0.  0.  0.]. 1행 3열
cost, grad = costFunction(W, x_data, y_data)

장황하게 설명했던 costFunction 함수를 호출하고 있다. W는 초기값으로 1차원 배열로 만들고 모두 0을 넣었다. 0에 대한 sigmoid 값은 0.5다. 김성훈 교수님 동영상 그래프에 그렇게 그려져 있다.

앤드류 교수님 과제에서 costFunction 함수를 사용하는데, 일단 이것 또한 정답이다. 아쉬운 점은 앞서 말했듯이 decision boundary와 같은 직선을 그어서 시각적으로 그룹을 나누려면 log로 되어 있는 cost 함수를 미분해야 하는 엄청난 문제에 봉착한다. 그래서, 앤드류 교수님도 이 부분에 대해서는 코드를 공개해서 그냥 확인할 수 있도록 했다.

미분을 다룰 수 있는 사람이라면 앞의 코드에 살을 붙여서 decision boundary 직선까지 출력할 수 있도록 하면 좋겠다.


# decision boundary 직선. 앤드류 교수님. 
# 안에 들어있는 값은 gradient descent 알고리듬을 구현한 이후에 발생한 값
# x값은 x_data에서의 최소값과 최대값. y값은 W값을 이용해서 계산된 값.
plt.plot([28.059, 101.828], [96.166, 20.653], 'b')

이 코드를 plt.show() 함수 앞에 추가하면 decision boundary 직선을 볼 수 있다. decision boundary는 직선일 수도 있고 타원일 수도 있고, 상황에 따라 달라진다.


왼쪽 그림은 앞의 코드에서 출력한 그래프이고, 오른쪽 그림은 앤드류 교수님 수업에서 출력한 그래프다. 데이터를 정확히 2개의 그룹으로 나눌 수도 있는데, 머신러닝의 최대 적인 오버피팅(overfitting)을 만난 것이므로 굉장히 잘못된 decision boundary가 된다.

12. TensorFlow로 Logistic Classification의 구현하기 (lab 05)

개인적으로는 코딩이 훨씬 쉽고 개념 정리하는 것도 쉽게 느껴진다. 힘들게 힘들게 Logistic Regression에 대해 이론적인 내용을 정리했으니, 텐서플로우로 결과를 볼 때가 됐다.


앞의 글에서 엄청난 설명을 했던 공식들이다. 복잡하긴 한데, 앞의 글을 읽었다면 그래도 좀 알 것 같은 느낌이 들어야 하지 않을까?


아래는 이번 글에서 사용한 소스코드 전체이다.  04train.txt

import tensorflow as tf
import numpy as np

# 04train.txt
# #x0 x1 x2 y
# 1 2 1 0
# 1 3 2 0
# 1 3 5 0
# 1 5 5 1
# 1 7 5 1
# 1 2 5 1

# 원본 파일은 6행 4열이지만, 열 우선이라서 4행 6열로 가져옴
xy = np.loadtxt('04train.txt', unpack=True, dtype='float32')

# print(xy[0], xy[-1]) # [ 1. 1. 1. 1. 1. 1.] [ 0. 0. 0. 1. 1. 1.]

x_data = xy[:-1] # 3행 6열
y_data = xy[-1] # 1행 6열

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

# feature별 가중치를 난수로 초기화. feature는 bias 포함해서 3개. 1행 3열.
W = tf.Variable(tf.random_uniform([1, len(x_data)], -1.0, 1.0))

# 행렬 곱셈. (1x3) * (3x6)
h = tf.matmul(W, X)
hypothesis = tf.div(1., 1. + tf.exp(-h)) # exp(-h) = e ** -h. e는 자연상수

# exp()에는 실수만 전달
# print(tf.exp([1., 2., 3.]).eval()) # [2.71828175 7.38905621 20.08553696]
# print(tf.exp([-1., -2., -3.]).eval()) # [0.36787945 0.13533528 0.04978707]

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()

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

for step in range(2001):
sess.run(train, feed_dict={X: x_data, Y: y_data})
if step % 20 == 0:
print(step, sess.run(cost, feed_dict={X: x_data, Y: y_data}), sess.run(W))

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

# 결과가 0 또는 1로 계산되는 것이 아니라 0과 1 사이의 값으로 나오기 때문에 True/False는 직접 판단
print('[1, 2, 2] :', sess.run(hypothesis, feed_dict={X: [[1], [2], [2]]}) > 0.5)
print('[1, 5, 5] :', sess.run(hypothesis, feed_dict={X: [[1], [5], [5]]}) > 0.5)
print('[1, 4, 2] [1, 0, 10] :', end=' ')
print(sess.run(hypothesis, feed_dict={X: [[1, 1], [4, 0], [2, 10]]}) > 0.5)
sess.close()
[출력 결과]
...
1920 0.340956 [[-5.85679626  0.47247875  1.018206  ]]
1940 0.340727 [[-5.87786055  0.47344807  1.02187181]]
1960 0.340503 [[-5.8986907   0.47439972  1.02550077]]
1980 0.340284 [[-5.91929054  0.4753342   1.02909374]]
2000 0.340069 [[-5.93966627  0.47625202  1.03265131]]
-----------------------------------------
[1, 2, 2] : [[False]]
[1, 5, 5] : [[ True]]
[1, 4, 2] [1, 0, 10] : [[False  True]]

파일에 있는 데이터를 읽어서 x_data와 y_data에 치환해서 사용하는 코드다. 음수 인덱스와 슬라이싱은 이전 글에서 설명했으니 여기서는 건너뛴다.


# feature별 가중치를 난수로 초기화. feature는 bias 포함해서 3개. 1행 3열.
W = tf.Variable(tf.random_uniform([1, len(x_data)], -1.0, 1.0))

가중치 배열을 생성하는 코드로 1행 3열의 2차원 배열이다. 


# 행렬 곱셈. (1x3) * (3x6)
h = tf.matmul(W, X)
hypothesis = tf.div(1., 1. + tf.exp(-h)) # exp(-h) = e ** -h. e는 자연상수

X는 3행 6열의 배열이다. 현재는 placeholder로 되어 있고, 맨 밑의 반복문에서 데이터와 연결된다. matmul 함수는 행렬 곱셈을 지원하는 함수로, W와 X의 행렬 크기가 어울리지 않으면 에러를 발생시킨다. 여기서 사용된 2개의 변수에는 오해가 있을 수 있다. 정확하게 말하면 h가 hypothesis에 해당하고, hypothesis는 sigmoid에 해당한다. 그러나, 처음에 나온 그림에서 첫 번째 공식을 표현할 때 sigmoid를 H(X)로 표현했기 때문에, 김성훈 교수님의 동영상에서는 적절한 변수 이름이라고 할 수 있다. 그러나, sigmoid의 역할이 자연상수 e의 지수를 사용해서 hypothesis의 결과를 0과 1 사이의 값으로 변환하는 것임을 상기하면 수긍 가능할 것이다.


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

앞의 그림에서 두 번째 공식을 표현한다. reduce_mean 함수의 파라미터는 정확하게 시그마(∑)의 오른쪽에 있는 공식을 있는 그대로 표현하고 있다. 글자 그대로 log 함수를 호출했고, 약속했던 매개변수를 전달한다. 코드 맨 앞에는 음수 기호(-)도 붙어 있다.


print('[1, 2, 2] :', sess.run(hypothesis, feed_dict={X: [[1], [2], [2]]}) > 0.5)

학습 결과를 사용해서 성공과 실패를 예측하고 있다. 여기서 착각하면 안 되는 것이 hypothesis가 성공 또는 실패를 참과 거짓으로 직접 알려주는 것이 아니라 sigmoid 함수를 통해 0과 1 사이의 값으로 변환한 결과를 알려준다는 사실이다. 그래서, 참과 거짓에 대한 결과는 직접 0.5와 비교해서 사용해야 한다. 60%의 성공 확률이라고 표현할 수도 있기 때문에, 이렇게 반환하는 것이 맞다고 생각한다.

placeholder에 전달되는 X의 값이 조금 어렵다. 그런데, 앞에서 '04train.txt' 파일을 읽어올 때의 x_data는 6개의 데이터를 갖고 있어서 3행 6열이었다. 이번에는 데이터를 1개만 전달하기 때문에 3행 1열이 되어야 하니까, [[1], [2], [2]]와 같은 []가 두 번 중첩되어서 나오는 것이 맞다.


동영상에서 코드가 있는 부분을 캡쳐했다. 위의 그림을 보면, 코드와 어울리는 공식이 어떤 것인지 쉽게 알 수 있다.


96000 0.321994 [[-12.81583881   0.57090575   2.35556102]]
97000 0.321991 [[-12.83300495 0.57092708 2.35898232]]
98000 0.321988 [[-12.85017109 0.570948 2.36240411]]
99000 0.321985 [[-12.86729622 0.57096756 2.36581874]]
100000 0.321983 [[-12.88358688 0.57098198 2.36907005]]

최종적으로 두 번째 열에 있는 cost는 0이 되지 않았다. 앞에서 보여준 간단하면서도 feature가 하나밖에 없는 예제에서는 cost가 0이 될 수도 있겠지만, 실제 상황에서는 0이 될 수 없는 것이 맞다. 10만번을 구동시켜서 확인했는데, 위와 같은 결과가 나왔다. 줄어들기는 하지만, 0이 될 수는 없다.

결국  classification이라고 하는 것은 2차원 좌표상에 흩어진 데이터를 직선을 그어서 구분하겠다는 뜻인데, 그 직선을 계산하는 비용이 0이 될 수는 없다. 어떻게 계산해도 상당한 거리일 수밖에 없다. Logistic Regression을 포함한 모든 예측에서 100% 만족한 결과란 존재하지 않는다. 100%는 신의 영역이다. 대통령 선거와 같은 투표 결과 또한 위아래 5% 정도의 오차를 허용하고 있지 않은가?

11. Logistic Regression의 cost 함수 설명 (lec 05-2)

동영상이 2개로 되어 있어서 글도 두 개로 정리한다. 이번 동영상에서는 cost 함수와 gradient descent 알고리듬에 대해서 공부한다.


Linear Regression에서 배운 hypothesis와 이번에 배운 hypothesis를 비교해서 보여주고 있다. hypothesis는 cost 함수를 구성하는 핵심이기 때문에, 여기서는 cost 함수 또한 이전과 달라져야 한다고 얘기하고 있다.

그림의 왼쪽 부분은 매끈한 밥그릇이고, 오른쪽 부분은 울퉁불퉁하다. 김성훈 교수님은 이 부분에 대해서 왼쪽은 직선을 살짝 구부려서 연결을 한 모양이고, 오른쪽은 sigmoid를 구부려서 연결을 했기 때문이라고 설명하셨다.

sigmoid 모양에 대해서 추가 설명을 한다면, e의 지수 형태의 그래프를 사용했기 때문에 구부러진 곡선이 연결된 느낌이다. 그러나, 실제로는 log 함수를 사용하기 때문에 매끄러운 밥그릇이 만들어진다.

오른쪽 그림에 교수님께서 Local이라고 쓰셨는데 이 부분을 매끄럽게 만들지 않으면 local minimum을 최저점으로 잘못 발견할 수 있는 문제가 발생한다. 그래서, 반드시 구불구불한 형태의 밥그릇을 매끈하게 펴야 한다. 그래야 global minimum에 도착할 수 있다.


새로운 cost 함수다. cost 함수의 목적은 비용(cost)을 판단해서 올바른 W와 b를 찾는 것이다. 다시 말해, 목표로 하는 W와 b를 찾을 수 있다면, 어떤 형태가 됐건 cost 함수라고 부를 수 있다는 뜻이다.

자연상수(e)의 지수를 사용하는 hypothesis의 비용을 판단하기 위해, 드디어 log가 등장했다. 나는 지수도 잘 모르고 로그도 잘 모르는데, 찾아보니 지수의 반대를 로그로 부르고 있었다. 로그를 사용하는 이유는 앞에서 잠깐 언급했던 것처럼, 구불구불한 cost 함수를 매끈하게 펴기 위함이다. 당연히 첫 번째 목표는 hypothesis가 반영된 비용을 올바르게 판단하기 위함이다.

조금 아쉬운 점은 새로운 cost 함수인 C(H(x), y)의 공식이 2가지라는 사실이다. y가 1일 때 사용하는 공식과 0일 때 사용하는 공식이 따로 있어서, 괜히 부담스럽게 느껴진다.


그림이 조금 희미한데, 아래쪽에 그래프가 2개 있다. 매끈한 밥그릇의 왼쪽을 담당하는 그래프와 오른쪽을 담당하는 그래프. 두 개를 합쳐서 하나의 밥그릇을 만든다. log 함수가 매끈하기 때문에 gradient descent 또한 잘 동작한다. 왼쪽은 -log(z)의 그래프이고, 오른쪽은 -log(1-z)의 그래프다. 이 부분은 수학적인 내용이 필요하므로 다시 정리하도록 하겠다.

교수님께서 y 값이 1일 때와 0일 때에 대해 손수 적어놓으셨다. 한번 따라가 보자.

  y가 1일 때
  H(X) = 1일 때는 왼쪽 그래프에서 y는 0이 된다. (cost=0)
  H(X) = 0일 때는 왼쪽 그래프에서 y는 무한대(∞)가 된다. (cost=무한대)

  y가 0일 때
  H(X) = 0일 때는 오른쪽 그래프에서 y는 0이 된다. (cost=0)
  H(X) = 1일 때는 오른쪽 그래프에서 y는 무한대가 된다. (cost=무한대)

이번 설명에서 조금 헷갈리는 것이 y와 H(X)의 정의였다. 이전 글들에서 당연히 설명하면서 이해했다고 생각했음에도, 얕은 지식이라 새로운 게 나타나면 헷갈려 버린다. y는 파일 등에서 가져온 실제 데이터(label)이고, H(X)는 y를 예측한 값(y hat)이다. 그래서, H(X)는 기존의 hypothesis처럼 틀릴 수 있는 가능성이 있다.

  y가 1일 때 1을 예측(H(X)=1)했다는 것은 맞았다는 뜻이고, 이때의 비용은 0이다.
  y가 1일 때 0을 예측
(H(X)=0)했다는 것은 틀렸다는 뜻이고, 이때의 비용은 무한대이다.
  y가 0일 때 0을 예측(H(X)=0)했다는 것은 맞았다는 뜻이고, 이때의 비용은 0이다.
  y가 0일 때 1을 예측(H(X)=1)했다는 것은 틀렸다는 뜻이고, 이때의 비용은 무한대이다.

log로 시작하는 cost 함수가 어렵긴 해도, 맞는 예측을 했을 때의 비용을 0으로 만들어 줌으로써 cost 함수의 역할을 제대로 함을 알 수 있다.

착각하면 안 되는 것이 있다. 예측의 결과는 0과 1이 나올 수도 있지만, 대부분은 0과 1 사이의 어떤 값이 된다. 가령, 0.7이 나왔으면 0.7에서 label에 해당하는 1 또는 0을 뺀 결과를 활용하는 것은 여전히 동일하다. 다만 Linear Regression에서 했던 것처럼 페널티를 주기 위한 제곱을 하진 않는다. log 자체에 페널티와 동일한 무한대로 수렴하는 값이 있기 때문에 제곱을 할 필요가 없기 때문이다. 결론적으로 분류(classification)에서도 지금까지와 동일하게 최소 cost가 되도록 W를 조절하는 것이 핵심이다.

앞에 나온 2개의 공식은 아래에 다시 나오니까 생략하고. 마지막 공식은 가운데 있는 공식을 하나로 합친 공식이다. 하나로 만들지 않으면, 매번 코딩할 때마다 if문이 들어가기 때문에 불편하다.

두 개의 값 A와 B가 존재한다. y의 값에 따라 A와 B 중에서 하나만 사용하고 싶다. 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


아주 간단한 공식이다. y에 대해 1 또는 0을 넣어보면 원하는 값만 사용할 수 있음을 쉽게 알 수 있다. A는 -log(H(x))이고, B는 -log(1-H(x))이다. 이렇게 해서 마지막 세 번째에 있는 공식이 만들어졌다.

  y*A + (1-y)*B  ==>  y * -log(H(x))  +  (1-y) * -log(1-H(x))  ==>  -ylog(H(x)) - (1-y)log(1-H(x))

앞에서 만들었던 공식을 cost 함수와 합쳤다. 일단 아래처럼 음수 기호(-)를 앞으로 빼서 안쪽을 +로 만든다.

  -ylog(H(x)) - (1-y)log(1-H(x))   ==>   -(ylog(H(x)) + (1-y)log(1-H(x)))

cost 함수인 cost(W) = (1/m) ∑ c(H(x), y)를 재구성한다.

  (1/m) ∑  -(ylog(H(x)) + (1-y)log(1-H(x)))   ==>   -(1/m) ∑ylog(H(x)) + (1-y)log(1-H(x))

이렇게 해서 최종적인 cost 함수가 탄생했다. 이 함수는 나중에 파이썬으로 직접 구현해서 보여줄거라서, 지금 이해 못했어도 아직 기회가 있다. 나 또한 이 식을 보고는 이해하지 못했다.

두 번째 있는 공식은 cost(W)에 대해 미분을 적용해서 W의 다음 번 위치를 계산하는 공식이다. Linear Regression과 공식에서는 달라진 게 없지만, 김성훈 교수님께서 말씀하신 것처럼 cost(W)가 달라져서 매우 복잡한 미분이 되어 버렸다. 동영상에서도 이 부분을 생략하셨기 때문에, 우리 또한 "미분을 하는구나!" 정도로 넘어가야 하는 부분이다.

Linear Regression 알고리듬에서 cost 함수와 gradient descent를 구하는 공식이 각각 있었던 것처럼 Logistic Regression에서도 두 개의 공식이 있어야 하고, 위의 그림에서 확인할 수 있었다.

다만 미분에 대한 공식은 달라졌지만, W를 일정 크기만큼 이동시키는 부분은 여전히 동일하기 때문에 앞서 배운 gradient descent 알고리듬을 여기서도 사용할 수 있고, 다음 글에서 텐서플로우를 통해 보여줄 것이다.

10. Logistic Classification의 가설 함수 정의 (lec 05-1)

이번 글부터 2주차에 해당한다. 1주차에 너무 고생을 해서 2주차는 쉽겠거니 했는데, 여전히 새로운 것들은 이해가 되지 않는다. 그래도 아는 만큼 정리해 본다.

이전에 다룬 내용은 좌표상에 위치한 데이터를 가로지르는 직선을 그어서 새로운 데이터의 위치를 예측하는 Linear Regression 모델이었다. 물론 feature가 여러 개인 multi-variables인 경우에는 당연히 직선으로 표시되지 않겠지만, 나에게는 이 정도의 설명이 딱 좋은 것 같다.

이번에 공부한 내용은 Linear Regression을 활용해서 데이터를 분류하는 모델이다. 이름은 Logistic Classification이라고 부른다. 분류에서 가장 단순한 모델로 2가지 중에 하나를 찾는 모델이다.


두 가지 분류를 활용할 수 있는 몇 가지 예제를 설명하고 있다. 스팸 메일 탐지, 페이스북 피드 표시, 신용카드 부정 사용은 두 가지 값 중의 하나를 선택하게 된다. 프로그래밍에서는 이 값을 boolean이라고 부르지만, 여기서는 쉽게 1과 0으로 구분한다. 1은 spam, show, fraud에 해당한다. 1과 0에 특별한 값을 할당하도록 정해진 것은 아니다. 다만 찾고자 하는 것에 1을 붙이는 것이 일반적이다.

이번 그래프는 학생들의 성적을 선처리(preprocessing)해서 모든 점수를 0 또는 1로 변환했단는 것을 전제로 한다. linear regression은 Wx+b 공식을 통해서 직선을 긋고 이걸 토대로 결과를 예측하는 방식이다. 교수님께서는 기존 그래프에 공부를 많이 한 학생이 등장하면 기울기가 달라지기 때문에 문제가 발생한다고 말씀하셨다. 이걸 풀어보자.

예측을 한다는 것은 학습된 모델에 새로운 데이터를 전달한 결과를 가져온다는 뜻이다. 한 번 학습된 모델을 지속적으로 사용하면서 활용하는 것이 일반적이다. 그럴려면 학습된 모델을 수정하면 안 되는데, 너무 많이 공부한 학생이 발견되어서, 즉 기존 모델로 예측에 실패해서 모델을 수정하게 되었다는 것을 의미한다.

1. 학습된 모델을 수정하는 것은 옳지 않다.
  예측이 틀릴 수는 있지만, 누구나 알 수 있는 사실에 대해 틀린 예측을 하면 올바른 모델이라고 할 수가 없다.
2. 어떤 방식의 선처리가 됐건 데이터를 2개로 분리하는 것은 편협한 느낌일 수밖에 없다.
  0에서 100점, 0에서 1.0 등의 구간을 통해서 성공과 실패를 판정할 수 있는 모델이 필요하다.
3. 직선에 대해 x에 대한 y를 예측하는 것이 아니라 직선의 아래쪽과 위쪽이라는 새로운 방식의 판정 기준을 도입해야 한다.
  이 부분은 분류이기 때문에 어쩔 수 없을 수도 있다.
4. 점수가 아주 작거나 매우 큰 경우에 대해서도 모델을 수정하지 않고 사용할 수 있는 방법이 필요하다.
  이 부분은 1번과 같은 말이 될 수도 있다.



Linear Regression을 분류에 사용할 때 발생할 수 있는 문제에 대해서는 앞에서 설명했다. Wx+b라는 공식을 있는 그대로 사용하면 W를 1/2이라고 했을 때, x의 값이 100인 경우 50이라는 엄청난 값이 만들어 질 수 있다. 0과 1만을 사용해야 하는데, 범위를 벗어나는 값이 나오게 된다. 50보다 작으면 0, 크면 1이라고 표현하거나 1/2보다 작으면 0, 크면 1이라고 표현할 수 있는 추가 코드가 반드시 있어야 한다. 아래 그림에서는 이러한 표현식을 sigmoid라고 설명하고 있다.


시그모이드(sigmoid) 함수는 앞에서 배운 공식(Wx + b )이 만들어 내는 값을 0과 1 사이의 값으로 변환한다. 어떤 값이든지 sigmoid  함수를 통과하기만 하면 0과 1 사이의 값이 되는 놀라운 기적을 보여준다. 그림과 공식을 보면 그냥 겁먹게 되는데, 그럴 필요 없다. 소스 코드를 보면 진짜 간단하다. 왼쪽 그림의 공식을 보자.

  e로 시작하는 계산식이 0일 때, 1/1이 되어서 최대값인 1이 된다.
  e로 시작하는 계산식이 매우 클 때, (1/큰수)이 되어서 최소값인 0이 된다.
  WX가 0일 때, 지수가 0이 되어, 분모는 2가 되고, 이때 중간값인 1/2이 된다.

e로 시작하는 공식이 복잡해 보이지만, 실제로는 전혀 복잡하지 않다.

  e는 자연상수(mathematical constant) 또는 오일러 상수라고 부르고, 2.718281828459로 시작하는 무한 소수.
  e를 사용하는 이유는 이걸 사용하면 공식이 매우 자연스러워지면서 짧게 표현할 수 있기 때문이다. (어느 수학자)

결국 이 식은 e의 지수(exponent)일 뿐이다. Wx+b를 그래프에서 표현하는 것처럼 z라고 부르자. z가 음수가 되면 e의 z승은 엄청나게 작아지고, 양수가 되면 엄청나게 커진다. z 앞에 음수 기호가 있기 때문에 z가 음수일 때 오히려 양수가 되어 e의 z승은 큰 값이 되고, 분모로 사용되었기 때문에 전체 값을 0에 가깝게 만들어 버린다. 어찌 됐든 이 공식은 z가 무한히 작거나 무한히 커도 잘 동작한다는 것은 기억하자.

아주 중요하니까, 다시 한번 강조한다.
sigmoid는 linear regression에서 가져온 값을 0과 1 사이의 값으로 변환한다. z가 0일 때, 0.5가 된다.

09. 1주차 스터디 정리

여기까지가 1주차에 진행한 내용이다.
이후에 나오는 내용에 비해 많은 내용인데다 처음으로 진행하는 스터디라 어려운 점이 많았다.
스터디를 준비하는 과정에서 궁금한 내용이 많아서 적어둔 것이 있었다.
지나고 보니, 별거 아니었고 대부분은 설명할 수 있게 되었다.
말도 안 되는 궁금증일 수 있지만, 나와 같은 사람들이 있을 수 있어서 아는 범위에서 답을 남긴다.

1. Variable
우리 말로는 변수라고 하는데, 이쪽 분야에서는 입력이라고도 한다. 중요한 점은 이게 변한다는 거다.
x축의 데이터가 변함에 따라 결과로 나오는 y도 변하게 된다.
feature와 같은 뜻이고, 앤드류 교수님 수업에서는 feature를 주로 사용한다.
입력 변수는 앤드류 교수님은 몇 천 개도 얘기하시던데.. 나는 감이 안 잡힌다.

2. label
답을 갖고 있는 변수를 가리킨다. 여기서는 김성훈 교수님 설명에서는 y_data에 해당한다.
답을 알고 있으면 supervised, 모르면 unspervised가 된다.

3. cost 함수 vs. 분산
통계 수업을 잠깐 들었을 때, 분산과 표준편차에 대해 배웠다.
이게 머신러닝에 쓰이는 것은 아직 모르겠지만,
비용을 계산한다는 것이 결국 분산이나 표준편차와 같은 느낌이다.
데이터가 분포한 모습을 보는 것이나 그 모습에서 비용을 계산하는 것이나 정도의 차이처럼 보였다.

4. linear gradient 알고리듬에서 learning rate를 처음엔 크게, 나중엔 작게 할 수 없을까?
목표에 도달하기 위해 처음엔 빨리 가다가 어느 시점부터는 미세하게 다가가면 좋겠다는 생각이 들었다.
당연히 learning rate에 해당하는 rate 변수의 값을 바꾸면 할 수 있다. 얼마든지.
그런데, 할 필요가 없다.
아래 그림을 보면, 동일한 rate를 사용함에도 불구하고 최소 비용에 가까워질수록 작게 이동하는 것을 알 수 있다.
W가 1일 때의 주변 빨강 점이 오밀조밀하다.

 

5. 데이터가 몇 개 없는데, 너무 오래 걸린다.
텐서플로우가 무겁기 때문인지 기다리는 것이 느껴진다.
데이터가 100개 정도 되고 변수가 5개 정도 되면, 화장실에 갔다올 정도가 될 수도 있다.
변수가 많아진다는 것은 반복문을 여러 개 사용하는 것과 같다는 것을 알 수 있다.
그래도 반복문을 여러 번 중첩시켜도 데이터가 없는데.. 너무 느리다.
그냥 텐서플로우가 일단은 무거운 것 같다.

6. random_uniform 호출 결과를 직접 보려면?
"02. TensorFlow의 설치 및 기본적인 operations (lab 01)" 글에서 설명했다. showTensor 함수를 봐라.
데이터가 어떻게 생긴지 알아야 코드를 이해할 수 있는데, 이걸 몰라서 많이 답답했었다.

7. gradient descent 알고리듬에서 어떻게 최소값을 찾는지 모르겠다.
이게 제일 문제였었다. 아무리 봐도 이해가 가지 않았었다.
현재 비용(cost)에서 기울기를 구해 다음 번 비용으로 이동하는 게 전부였다. 
이때 내부적으로 기울기에 해당하는 W를 미분으로 계산한 값을 빼서 다음 번 W를 계산하는 방식이다.
이 설명에서는 현재 값이 얼마인지는 필요치 않으므로, 우리는 어디에 있건 최소 비용을 찾아갈 수 있게 된다.
앞의 W와 cost 그래프를 다시 한번 보도록 하자.
그래도 안되면 파이썬으로 구현해 놓은 "06. cost 함수를 파이썬만으로 직접 구현"을 읽어 본다.

8. cost 함수에서 W가 1일 때의 뜻은?

김성훈 교수님께서 잘 정리해 주셨다. 그래도 다시 정리해 보면,

  hypothesis = w * x_data + b
  mean(square(hypothesis - y_data))

x를 hypothesis에 집어넣고 y를 뺀 결과의 제곱을 리스트 전체에 대해 합계 계산, 그리고 평균 계산

  ((1*1 - 1)**2 + (1*2 - 2)**2 + (1*3 - 3)**2) / 3 = (0**2 + 0**2 + 0**2) / 3 = 0
  ((0*1 - 1)**2 + (0*2 - 2)**2 + (0*3 - 3)**2) / 3 = (1**2 + 2**2 + 3**2) / 3 = (1+4+9) / 3 = 14/3 = 4.67
  ((2*1 - 1)**2 + (2*2 - 2)**2 + (2*3 - 3)**2) / 3 = (1**2 + 2**2 + 3**2) / 3 = (1+4+9) / 3 = 14/3 = 4.67

9. tf.Variable은 모델 안에서 계속 변하기 때문에 변수가 아닐까?
tf.constant는 연산 과정에서 바뀌지 않는 것을 뜻하고, tf.Variable은 바뀔 수 있음을 뜻한다.
바뀔 일이 없을 것 같은 learning rate을 tf.Variable로 주는 이유가 내부적으로 바뀔 가능성이 있기 때문이다.
실제로도 바뀐 것을 본 적이 있다.
W와 b가 바뀌는 이유는 tf.Variable로 주었기 때문이고, 바뀌어야 하기 때문에 반드시 tf.Variable이어야 한다.

10. Session.run()
공식 도움말에 보면 runs one "step" of TensorFlow computation라고 되어 있다.
코드에서는 update 또는 cost라는 이름의 변수로 표현되는데,
run(update)는 update에 연결된 모든 변수가 1회 실행한 만큼 변화한다는 뜻을 담고 있다.
다른 말로는 다음 값으로 넘어간다라고 얘기할 수도 있겠다. 우리 코드에서 변화한 것은 기울기(W)와 y 절편(b)이었다.

11. one-variable과 multi-variables의 hypothesis 공식이 왜 똑같을까?
값을 하나씩 처리하는 방식이라면 당연히 달라야 한다.
그러나, 데이터 갯수에 상관없이 행렬로 처리하기 때문에 같을 수밖에 없다.
one-variable 방식에서는 변수가 하나였지만, 실제로는 1x1 행렬을 사용한 것이었다.
multi-variables 방식에서는 1xn 크기의 행렬을 사용했기 때문에 코드는 같다.

12. gradient descent 알고리듬에서 local optimum에 있을 때, 왜 값이 더 아래로 내려가지 않을까?
local optimum 아래에 global optimum에 해당하는 최저점이 위치할 수도 있지만,
현재 cost가 지속적으로 내려갈 수 있는 기울기를 제공하지 않는다면, 진행할 수가 없다.
local optimum이라는 것은 나름 조그마한 밥그릇을 형성했다는 뜻이므로 내려갈 수 없는 것이 당연하다.

13. 기울기가 0이 되는 시점에서 텐서플로우는 무엇을 할까?
텐서플로우를 구동해 보면, 최소 비용이 되어서 같은 결과를 계속해서 보여주게 된다.
이때 텐서플로우는 내부적으로 step에 따른 결과를 매번 계산할까?
오픈소스이긴 하지만, 코드를 살펴볼 생각이 없는 나에겐 영원히 풀 수 없는 미스테리!

14. minimize cost(W, b)의 뜻은?
가장 작은 기울기(W)와 y 절편(b)을 구하는 것이 아니라 최소 비용에 따른 W와 b를 찾으라는 뜻이다.
최소 비용(minimize cost)에 W와 b를 파라미터로 전달해서 이름 그대로 최소 비용을 찾겠다는 뜻이다.

15. 텐서플로우의 노드(node)와 간선(edge)
텐서플로우는 Data Flow Graph, 즉 데이터의 흐름을 그래프로 표현한 것.
node와 edge로 구성되어 있고, node는 operation, edge는 n-dim data array(tensors) 표현.
일반적으로는 node를 데이터, edge를 연산으로 생각한다.

16. y 절편 b는 몇 번 더해지는가?
이 부분에 대한 답은 "08. multi-variable linear regression을 TensorFlow에서 구현하기(lab 04)" 글에 있다.
x_data는 여러 개의 feature가 있어 복잡하니까, y_data의 갯수만큼이라고 얘기하면 쉽다.
y_data = [1, 2, 3, 4, 5]라면 b는 다섯 번 더해진다.
y 절편이라고 하는 것이 원점(0, 0)에서 시작하는 직선을 y 절편만큼 이동시켰다는 뜻이고,
이것은 직선에 포함된 모든 데이터가 y 절편만큼 움직였다는 뜻이다.

17. rate 또는 alpha라고 부르는 learning rate의 뜻은?
현재 cost에 대한 기울기(W)는 이미 정해져 있다.
이 값은 외부에서 변경할 수 없고, 내부적으로 gradient descent 알고리듬을 통해 스스로 수정해 나간다.
이렇게 자동으로 구한 값은 일반적으로 너무 커서 overshooting이라는 문제를 일으킬 확률이 매우 높다.
경사를 타고 내려가는 것이 아니라 거꾸로 올라가는 현상을 말한다.
그래서, 보통은 1보다 작은 값을 곱해서 오버슈팅이 일어나지 않고 경사를 타고 내려가도록 조정한다.
이 값은 정해져 있는 것이 아니라 데이터에 따라 달라지므로, 여러 번에 걸쳐 테스트를 통해 결정하게 된다.
꽤 빠른 속도로 내려가서 최저 비용을 계산한다면 이상적인 learning rate라고 부를 수 있다.
이 부분은 뒤의 동영상에 나오는데, 이상적인 learning rate은 여러 번 구동시켜서 찾는 방법 외에는 없다.

18, Wx + b에서 W를 찾는 것은 알겠다. 그런데, 어떻게 b까지 찾을 수 있는 것일까?
결국엔 W와 b를 모두 바꾸면서 비용을 측정하는 수밖에 없다.
그래서, 아래쪽에 답을 찾지 못한 질문에 있는 것처럼  b가 추가되면 multi-variables와 같은 상황이 된다.
기울기를 바꾸나 y 절편을 바꾸나 계속 바꿔야 하는 것은 같다.
기울기 바꾸는 방법은 알고 있으니, 오히려 y 절편을 바꾸는 것이 더욱 어려워 보인다.

19. 학습한 결과를 저장했다가 내일 사용하는 방법은?
Saver 클래스에 포함된 save와 restore 함수를 통해 쉽게 구현할 수 있다. save 함수를 호출하면 checkpoint 파일이 생성되는데, 이 파일을 restore하면 모델을 재사용할 수 있게 된다. 확장자는 ckpt.

20. 저장할 수 있다면, 크기는 어느 정도일까?
모델만 놓고 판단한다면 ckpt 파일의 크기면 충분하다.
지금까지의 설명을 적용하면, W와 b만 알고 있으면 되기 때문에 20바이트만 있어도 충분할 수 있다.
다만 실제 상황에서는 feature 개수나 모델을 구성하기 위한 필터 등의 환경이 필요하기 때문에 좀더 많이 필요할 수 있다.
그렇지만 학습이 끝났다면 연산과 메모리 모두 거의 필요없는 수준이라고 볼 수 있다. 학습과 비교한다면.

21. convex 형태인지 확인할 수 있는 방법이 있을까?
동셩상에서는 확인해야 한다고만 하셨고, 방법에 대한 말씀은 없으셨다.
내 생각에는 "있다"라기 보다는 이미 만들어진 방법을 사용하고 있다고 보는 것이 맞는 것 같다.
딥러닝의 핵심은 gradient descent 알고리듬에 있고, 이 방법은 cost에 대해 convex하다는 것을 항상 보장한다.
gradient descent 알고리즘의 여러 변형이 존재하지만, 
모두 cost에 대한 미분을 적용하기 때문에 convex하다는 사실은 여전히 달라지지 않는다.

다만 기존에 알려진 방법이 아닌 새로운 방법으로 모델을 구성하고 있다면
반복 횟수를 늘리거나 learning rate을 조절해서 convex 형태를 직접 확인해볼 수도 있을 것 같다.
만약 그런 일이 생긴다면 무척이나 괴로울 것 같다. ^^

22. multi-variables linear regression에서 hypothesis는 등고선 모양에서 중심을 향해 가는 직선일까?

왼쪽 그림은 동영상에 나왔던 그림이고, 오른쪽 그림은 앤드류 교수님 수업에 나오는 그림이다.
양쪽 모두 feature가 2개인 경우를 표현하고 있는데, 결국은 가운데 있는 global optimum을 찾아가야 한다.
이때 최저점을 찾기 위해 임의의 위치에서 직선으로 내려가는 방법이 존재할까?

프로그램적으로는 가능하지 않지만, 왠지 수학적으로는 가능해 보인다.
혹은 직선에 가까운 형태 정도는 존재하지 않을까, 생각한다.
천천히 생각해 보면 직선으로 내려간다는 것이 불가능해 보일 것이다.

뒤쪽 동영상에 김성훈 교수님께서 특정 사이트를 통해
여러 가지 방법들의 성능에 대해서 비교해 주는 부분이 있는데, 거기에 힌트가 있긴 한듯 하다.
모든 방법들이 직선으로 향하지 않고 떨듯이 움직이면서 둥글게 선회하기도 하고 다양한 모습을 보여준다.


08. multi-variable linear regression을 TensorFlow에서 구현하기(lab 04)

여러 개의 입력(variable, feature)을 가진 linear regression을 텐서플로우로 직접 구현해 본다.


정확한 결과를 보여주기 위해 매우 확실한 데이터를 사용하고 있다. 우리의 목적은 최소 비용을 갖는 W1과 W2, b를 찾는 것이다. 그래야 정확한 직선을 그래프에 그릴 수 있고, 그걸 토대로 다른 값의 feature에 대해 예측할 수 있게 된다. 데이터에서 요구하는 정답은 W1과 W2는 모두 1이고, b는 0인 결과이다.

  1*1 + 0*1 + 0 = 1
  0*1 + 2*1 + 0 = 2
  3*1 + 0*1 + 0 = 3
  0*1 + 4*1 + 0 = 4
  5*1 + 0*1 + 0 = 5

그림에 있는 코드를 잠시 보자. 이 코드가 뒤에 나올 코드의 핵심적인 부분이다. W1과 W2는 cost 함수에서 매번 변경된다는 것을 기억할 것이다. 시작은 -1과 1 사이의 어떤 난수로 했다. y 절편에 해당하는 b 또한 -1과 1 사이의 난수이다.

hypothesis를 보면 "1*1 + 0*1 + 0"처럼 정직하게 공식을 구성하고 있다. 이렇게 hypothesis를 구성하고 나면, 우리가 추가로 할 것은 없다. 텐서플로우에 전달하면 알아서 기울기(W)를 계산해 준다.


import tensorflow as tf

x1_data = [1, 0, 3, 0, 5]
x2_data = [0, 2, 0, 4, 0]
y_data = [1, 2, 3, 4, 5]

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

b = tf.Variable(tf.random_uniform([1], -1.0, 1.0))

# feature 갯수만큼 곱하는 이 부분을 제외하면 one-variable과 다른 곳이 없다
hypothesis = W1*x1_data + W2*x2_data + b

cost = tf.reduce_mean(tf.square(hypothesis - y_data))

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

init = tf.initialize_all_variables()

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

for step in range(2001):

sess.run(train)

if step%20 == 0:
print(step, sess.run(cost), sess.run(W1), sess.run(W2), sess.run(b)) sess.close()

[출력 결과] ... 1920 1.42109e-14 [ 1.] [ 0.99999994] [ 1.74593097e-07] 1940 1.42109e-14 [ 1.] [ 0.99999994] [ 1.74593097e-07] 1960 1.42109e-14 [ 1.] [ 0.99999994] [ 1.74593097e-07] 1980 1.42109e-14 [ 1.] [ 0.99999994] [ 1.74593097e-07] 2000 1.42109e-14 [ 1.] [ 0.99999994] [ 1.74593097e-07]

one-variable에서 작성했던 코드와 다른 점이 없어 코드에 대한 설명은 생략한다.

출력 결과에서 3번째가 W1, 4번째가 W2, 마지막이 b에 해당한다. 어느 시점 이후로는 cost가 바뀌지 않기 때문에 W1, W2, b의 결과 또한 바뀌지 않음을 알 수 있다. 이미 충분히 작은 값이기 때문에 더이상 줄어들 여지가 없을 수도 있다. 결과에 나온 cost는 1.42109를 0(zero)이 14개인 숫자로 나눈 값이다.

동영상과 다른 결과가 나오는 것은 난수로 시작했기 때문이고, 어떤 값에서 시작을 해도 같은 결과가 나온다는 것을 보여주기 위해 난수를 사용하고 있다고 이전 글에서 설명했었다.


앞에서 보여준 코드는 정직하다. 이제 행렬(matrix)로 변환한 코드를 볼 차례다. x에 해당하는 데이터를 2개가 아닌 1개의 데이터로 만들었다. x_data는 2차원 리스트이고, 0번째에는 x1, 1번째에는 x2가 들어있다.

기울기인 W 또한 행렬로 만들어야 한다. x_data가 2차원이니까 값을 2개 갖는 행렬이 되어야 한다. 1x2가 맞을까, 2x1이 맞을까? 아직도 이게 딱 생각나지 않는다. 잠시 차분하게 생각을 해야 답을 알 수 있다. 내 자신이 답답하다.

행렬 곱셈을 할 때, W가 앞에 온다는 것을 기억할 것이다. (WX + b) 그래서, W가 앞에 와야 한다면 x_data가 2행 5열이기 때문에 W는 1행 2열이어야 한다. 2행 1열 x 2행 5열은 에러니까.

이제 진짜 중요하고 어려운 것을 설명한다. x_data가 진짜 2행 5열의 리스트라는 생각이 드는가? 정말? 이게 말이 안 되는 이유는 이 글의 첫 번째 그림에 나와 있다.

그림에서는 x1과 x2에 대해 5행 2열로 표현하고 있다. 행이 5개 열이 2개란 말이다. 그런데, 어떻게 여기 와서 2행 5열이라고 얘기할 수가 있단 말인가?

문법적으로는 2행 5열이 맞다. 그러나, 여기서는 각각을 feature에 관련된 데이터 1개로 취급을 해야 한다. 2행 5열이 아닌 2행 1열로 보는 것이 좋다. 결론적으로 1행 2열 x 2행 1열이 되어서 행렬 곱셈이 일어난다. W1과 x1 feature에 포함된 5개를 곱해서 더하고, W2와 x2 feature에 포함된 5개를 곱해서 더하고.

그러나, 헷갈리면 안 된다. 2행 1열이라고 얘기한 것은 이렇게 곱셈하게 된다는 것을 보여주기 위한 것이지 진짜는 아니다. 1행 2열 x 2행 5열이 진짜가 되고, 결과 행렬을 1행 5열이 된다. 실제 코드에서는 1행 5열의 결과에서 y_data(1행 5열)를 뺀 결과에 제곱을 하고 덧셈을 하는 추가 연산이 일어난다. 이 부분은 진짜 중요하니까, 반드시 이해해야 한다. 내 경험담이다.


import tensorflow as tf

# 앞의 코드에서는 정수였는데, 실수로 바뀌었다. 정수로 처리하면 에러.
x_data = [[1., 0., 3., 0., 5.], [0., 2., 0., 4., 0.]] # W의 값은 [[-0.49763036 0.79181528]]
y_data = [1, 2, 3, 4, 5]

W = tf.Variable(tf.random_uniform([1, 2], -1.0, 1.0)) # [2, 1]은 안 된다. 행렬 곱셈이니까.
b = tf.Variable(tf.random_uniform([1], -1.0, 1.0))

# W와 곱해야 하기 때문에 x_data를 실수로 변경
hypothesis = tf.matmul(W, x_data) + b # (1x2) * (2x5) = (1x5)

cost = tf.reduce_mean(tf.square(hypothesis - y_data))

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

init = tf.initialize_all_variables()

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

# 테스트 출력
print(sess.run(W))
print(sess.run(tf.matmul(W, x_data)))

for step in range(2001):

sess.run(train)

if step%20 == 0:
print(step, sess.run(cost), sess.run(W), sess.run(b)) sess.close()

[출력 결과] [[ 0.23744965 -0.22514296]] [[ 0.23744965 -0.45028591 0.71234894 -0.90057182 1.18724823]] ... 1920 1.42109e-14 [[ 1. 0.99999994]] [ 1.75134133e-07] 1940 1.42109e-14 [[ 1. 0.99999994]] [ 1.75134133e-07] 1960 1.42109e-14 [[ 1. 0.99999994]] [ 1.75134133e-07] 1980 1.42109e-14 [[ 1. 0.99999994]] [ 1.75134133e-07] 2000 1.42109e-14 [[ 1. 0.99999994]] [ 1.75134133e-07]

x_data는 2행 5열, W는 1행 2열이다. 1행 2열 x 2행 5열의 결과는 주석에 있는 것처럼 1행 5열이 된다.텐서플로우에서 행렬 곱셈을 수행하는 함수는 matmul이다. cost를 구하기 위해 hypothesis - y_data는 행렬 뺄셈이다. 곱셈과 달리 같은 행과 열에 있는 요소를 빼면 된다. 다만 그러기 위해서는 같은 크기의 행렬이어야 하고, hypothesis와 y_data 크기가 같아야 한다. 모두 1행 5열이다. square 함수 또한 행렬 함수로 각 요소를 제곱하고, 최종적으로 reduce_mean 함수로 평균을 계산한다. W와 x_data의 순서를 바꿔서 tf.matmul(x_data, W)라고 쓰는 것은 에러다. 2행 5열 x 1행 2열은 행렬 곱셈을 수행할 수 없으니까.

테스트 출력을 통해 헷갈릴 수 있는 행렬 연산의 결과를 표시했다. W는 [0.23744965 -0.22514296]을 요소로 갖고 있는 1행 2열의 리스트이다. W와 x_data를 곱한 결과는 [0.23744965 -0.45028591 0.71234894 -0.90057182 1.18724823] 요소를 갖는 1행 5열의 리스트이다. 1개만 갖고 있기 때문에 1행이고, 그 안에 2개 또는 5개가 있어서 2열 또는 5열이다.

출력 결과는 앞의 코드와 똑같이 W1, W2에 해당하는 W 행렬은 결과가 모두 1이고, b는 0과 다름없다. feature(x 변수)가 많아질 때, 코드의 어떤 부분에 영향을 주는지 이해하는 것이 중요하다. 텐서플로우와 관련된 코드는 거의 달라지지 않는다. hypothesis를 구성하는 공식과 W의 결과를 출력하는 정도만 달라지고 있다.


마지막 코드다. 그동안 무시 아닌 무시를 당했던 y 절편에 해당하는 b를 행렬에 포함시켰다. 1행 4열 x 4행 1열의 행렬 곱셈으로 크기가 살짝 늘어났다. 그럼에도 행렬 곱셈이기 때문에 코드가 달라지진 않는다.

위의 공식은 y를 1개만 예측한다는 것을 정확히 이해해야 한다. X의 feature가 3개 있기 때문에 가중치인 W 또한 3개가 되고, 이들 전체를 계산해서 1개의 y 예측을 만들어 낸다. 여러 번에 걸친 데이터가 있다면, 초록색으로 표시된 W는 바뀌지 않지만, 빨간색으로 표시한 X는 오른쪽으로 길게 늘어지는 행렬이 되어야 한다. 5개의 데이터가 있다면, 3행 5열. 여기에 b라고 하는 feature 아닌 feature까지 추가되니까, 최종적으로는 4행 5열의 행렬이 만들어 진다.


import tensorflow as tf

# 앞의 코드에서 bias(b)를 행렬에 추가
x_data = [[1, 1, 1, 1, 1], [1., 0., 3., 0., 5.], [0., 2., 0., 4., 0.]] # 갯수가 같아야 하므로 b를 리스트로 처리
y_data = [1, 2, 3, 4, 5]

W = tf.Variable(tf.random_uniform([1, 3], -1.0, 1.0)) # [1, 3]으로 변경하고, b 삭제

hypothesis = tf.matmul(W, x_data) # b가 사라짐

# ------------------------------------------------------------------------- #

cost = tf.reduce_mean(tf.square(hypothesis - y_data))

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

init = tf.initialize_all_variables()

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

for step in range(2001):

sess.run(train)

if step%20 == 0:
print(step, sess.run(cost), sess.run(W)) # b 사라짐. W에서 3가지 항목 출력 sess.close()
[출력 결과]
...
1920 4.83169e-14 [[  1.60856530e-07   9.99999940e-01   9.99999940e-01]]
1940 4.83169e-14 [[  1.60856530e-07   9.99999940e-01   9.99999940e-01]]
1960 4.83169e-14 [[  1.60856530e-07   9.99999940e-01   9.99999940e-01]]
1980 4.83169e-14 [[  1.60856530e-07   9.99999940e-01   9.99999940e-01]]
2000 4.83169e-14 [[  1.60856530e-07   9.99999940e-01   9.99999940e-01]]

y 절편 b를 추가해서 맨 앞의 코드 일부만 달라졌다. x_data는 3행 5열이 되었고, W는 1행 3열이 되었다. hypothesis를 구하는 코드에서 b가 사라진 것을 볼 수 있다. 이번 코드에서 가장 중요한 것은 b가 행렬에 포함되면서 여러 번 반복된 사실이다. 정확하게는 열의 갯수만큼으로 확장되었다.

  H(x) = Wx + b

이 코드는 b를 한 번만 더하는 코드같지만, 실제로는 X의 데이터 갯수만큼 더한다.

  x가 1일 때에 기울기를 곱한 결과에 b를 더한다.
  x가 2일 때에 기울기를 곱한 결과에 b를 더한다.
  x가 3일 때에 기울기를 곱한 결과에 b를 더한다.
  x가 4일 때에 기울기를 곱한 결과에 b를 더한다.
  x가 5일 때에 기울기를 곱한 결과에 b를 더한다.

이게 당연한 것이 W는 기울기일 뿐이고 말 그대로 기울어진 정도만을 표시한다. x가 1일 때의 기울기를 곱한 결과는 원점(0,0)에서의 y값이므로 여기에 y 절편 b(bias)를 더해서 이동을 시켜야 한다. 설명이 이해가 안 되면, y = 2x + 3의 그래프를 그려보면 된다.

  x = [1, 2, 3, 4, 5]
  y = [5, 7, 9, 11, 13]

여기서 W*x에 해당하는 y = 2x의 그래프를 그리면, 왜 매번 3을 더해야 하는지를 알 수 있을 것이다.


예제에서 사용할 데이터 파일 03train.txt

import tensorflow as tf
import numpy as np

# 03train.txt
# #x0 x1 x2 y
# 1 1 0 1
# 1 0 2 2
# 1 3 0 3
# 1 0 4 4
# 1 5 0 5

# #으로 시작하는 첫 번째 줄은 주석으로 판단하고 읽지 않음
xy = np.loadtxt('03train.txt', unpack=True, dtype='float32')
x_data = xy[:-1]
y_data = xy[-1]

# 5행 4열로 구성된 파일이지만, numpy에서 읽어오면 4행 5열이 된다.
# 그래야 열 단위로 행렬 연산을 수행할 수 있다.
print(type(xy)) # <class 'numpy.ndarray'>
print(xy.shape) # (4, 5)
print(len(x_data)) # 3

# W는 1x(x_data 크기) 매트릭스
W = tf.Variable(tf.random_uniform([1, len(x_data)], -1, 1))

hypothesis = tf.matmul(W, x_data)

cost = tf.reduce_mean(tf.square(hypothesis - y_data))

rate = tf.Variable(0.1) # learning rate, alpha
optimizer = tf.train.GradientDescentOptimizer(rate)
train = optimizer.minimize(cost) # goal is minimize cost

init = tf.initialize_all_variables()

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

for step in range(2001):

sess.run(train)

if step % 20 == 0:
print(step, sess.run(cost), sess.run(W)) sess.close()

이전 코드의 파일 버전이다. 출력 결과는 이전과 똑같기 때문에 생략한다.앞에서는 x_data와 y_data의 데이터를 직접 값을 넣는 방식으로 채웠지만,앞으로는 파일에서 직접 가져오는 방식을 사용하게 된다. 데이터가 너무 많으니까.

데이터를 읽어오는 부분만 보면 된다. numpy에 포함된 loadtxt 함수를 호출해서 파일을 읽어왔다. 정상적으로 읽으면 5행 4열이 되고, 왼쪽 3열과 오른쪽 1열을 분리해서 x_data와 y_data로 사용해야 한다. 그런데, 이 방식은 데이터를 분리하기가 조금 피곤하다. transpose시켜서 읽으면 4행 5열이 되고, 분리하기가 쉽다. unpack 파라미터를 사용하지 않으면 원본 그대로, True를 전달하면 transpose해서 읽는다.


x_data = [[1, 1, 1, 1, 1], [1., 0., 3., 0., 5.], [0., 2., 0., 4., 0.]] 

앞에서는 이렇게 행렬을 구성했다. 이 데이터는 원래 5행 3열로 생성되어야 하지만, 실제로는 3행 5열이다. 행렬 연산을 열 기준으로 적용해야 하기 때문에 발생하는 현상이다.

파이썬에는 정말 괜찮은 음수 인덱스 기능이 있어서 마지막을 가리킬 때는 -1번째, 마지막에서 두 번째는 -2번째가 된다. 여기에 슬라이싱(slicing)을 사용해서 range 함수와 동일한 기능을 수행할 수도 있다.

  xy[1]      1번째 요소
  xy[1:3]   1번째와 2번째 요소. 3은 end 인덱스로 범위에 포함되지 않는다.
  xy[1::2]  1번째부터 2칸씩 건너뛴 모든 요소. 비워둔 칸은 전체의 뜻을 가짐.

여기서는 xy[:-1]을 했으므로 처음부터 마지막 이전까지의 모든 요소가 된다. end 인덱스는 포함하지 않으니까, -1번째는 포함되지 않는다. xy[-1]은 말 그대로 마지막 요소, 여기서는 마지막 열이 된다.


데이터를 읽어올 때, transpose시키지 않았을 때의 달라지는 부분을 적어 봤다. loadtxt 함수 호출에 unpack 파라미터가 빠졌다. 마지막 열을 제거하기 위한 코드도 복잡하고, matmul 함수 호출에서 x_data를 transpose시켜야 하는 것도 불편하다. 나머지는 달라지는 곳이 없다.

data = np.loadtxt('03rain.txt', dtype=np.float32)

x_data = data[:,:-1]
y_data = data[:,-1]

W = tf.Variable(tf.random_uniform([1, 3], -1, 1))

hypothesis = tf.matmul(W, tf.transpose(x_data))


07. multi-variable linear regression (lec 04)

여기까지가 머신러닝에서 가장 기본이 되는 Linear Regression이다. 앞에서는 문제를 쉽게 하기 위해 입력 변수(feature)가 하나밖에 없는 단순 모델을 보여줬지만, 여기서는 여러 개의 입력 변수를 다루기 위한 전초전으로 2개의 입력을 처리하고 있다. 굿럭.


시험 성적을 예측하기 위해 공부한 시간만으로 판단하는 것에는 무리가 있다. 가령, 텔레비전을 켜고 공부했는지, 친구들하고 함께 공부했는지, 졸린 상태로 했는지 등등 성적에 영향을 줄 수 있는 다양한 요인을 살펴봐야 한다. 여기서는 수업 참석 여부를 갖고 판단하고 있다.

왼쪽 그림에는 시간을 뜻하는 변수(x, hours)가 있고, 오른쪽 그림에는 시간(x1, hours)과 수업 참석(x2, attendance)의 두 가지 변수가 있다. 그런데, 두 가지가 됐다고 해서 겁낼 필요 없다고 교수님께서 말씀하셨다. 우리에게는 거의 차이가 없다.


왼쪽 그림은 hypothesis, 오른쪽 그림은 cost 함수이다. 앞에서 지겹도록 보아온 내용이다. hypothesis에서는 공식이 조금 달라졌다. feature가 1개일 때는 Wx+b로 처리할 수 있었는데, 이제는 복잡해져서 w₁x₁ + w₂x₂ + b라고 해야 한다. 다시 말해, w와 x를 feature 갯수만큼 곱하고 b를 더하면 된다.

그런데, 중요한 것은 이게 아니다. 오른쪽 그림에서 hypothesis가 달라졌음에도 불구하고 cost 함수의 정의는 달라지지 않았다는 사실이다. 이 뜻은 나중에 나오는데, 기존 코드를 그대로 사용하면 multi-variable도 처리할 수 있음을 의미한다.



이번 그림에서는 앞서 설명한 feature가 여러 개일 경우에 대해, 2개일 때와 n개일 때에 대해 친절하게 나열해서 보여주고 있다. 갯수가 많아졌기 때문에 위와 같은 방식으로는 복잡해 보일 수밖에 없지만, 축약하면 같은 공식이 나오므로 걱정하지 않아도 된다.


동영상 중간에 행렬에 대해 짧게 얘기하는 곳이 나온다. 두 가지만 이해하면 된다. 왼쪽 그림은 행렬의 곱셈. 행렬은 앞쪽의 열 전체와 뒤쪽의 행 전체가 연산에 참여한다. 참여한 셀 각각을 곱해서 더하는 것이 기본이다. 곱셈이 성립하기 위해서는 앞 행렬의 열과 뒤 행렬의 행 크기가 같아야 한다. 이번 코딩을 진행하면서 가장 많이 실수했던 부분이다. 에러가 발생하는데, 코드상으로는 문제가 없어 보였다. 오랫동안 살펴보고 나서야 행과 열의 크기가 맞지 않는다는 사실을 발견할 수 있었다.

  2행 3열 x 3행 2열 = 2행 2열
  1행 2열 x 2행 1열 = 1행 1열
  5행 3열 x 3행 5열 = 5행 5열

간단하다. 곱셈에 인접한 2개의 숫자(왼쪽 열, 오른쪽 행)만 같으면 된다.

오른쪽 그림은 전치 행렬(transposed matrix)을 보여준다. 우리가 최종적으로 원하는 것은 행렬의 특정 열과 특정 행을 곱셈해서 더한 결과인데, 이때 행 또는 열의 순서가 맞지 않는 경우가 있을 수 있다. 앞서 말했듯이 행렬 곱셈은 열과 행이 참가해야 하기 때문인데, 이때 전치 행렬을 만들어서 곱셈을 한다. 당연히 행과 열을 맞추기 위해서 행과 열을 바꾸는 것이 아니라, 그렇게 곱셈해야 하기 때문이다.

나는 전치 행렬의 깊은 의미는 모르지만, 김성훈 교수님께서 말씀하신 내용에 대해서는 어렵지 않게 이해를 했다. 바로 뒤에 나오니까, 아래 그림을 참고하자.


변수가 여러 개인 cost 함수를 계산하기 편하도록 행렬(matrix)로 표현했다. w는 1x3 행렬, x는 3x1 행렬, 결과는 1x1 행렬이다. 1행 3열 x 3행 1열은 바깥쪽 행과 열이 결과 행렬의 크기를 좌우하므로 1행 1열이 나와야 한다. cost 함수는 기울기(w)에 따른 비용을 계산하는 함수이므로, 결과는 1개의 어떤 값이 되어야 한다.

그런데, 왜 w와 x의 행렬 크기가 다른 것일까? 다른 이유는 없고 행렬 곱셈을 적용해야 하기 때문이다. 2개의 행렬을 같은 크기인 1x3으로 만들면 곱할 수가 없기 때문에 전치(transpose)시킨 후에 곱셈을 해야 한다. 행렬은 우리가 직접 만들기 때문에 조금이라도 계산이 편하도록 미리 transpose시켰다고 보면 된다.

행렬을 hypothesis로 표현하게 되면 오른쪽 그림이 나온다. 결국은 행렬 곱셈이라는 1개의 표현으로 정리되기 때문에 지금까지 배운 hypothesis와 같을 수밖에 없다.



수학이 제일 어려웠던 것처럼 수학하는 사람들이 참, 똑똑하다. hypothesis를 처리할 때, 계산을 쉽게 하기 위해 y 절편에 해당하는 b를 넣지 않았었다. 계산의 편리함도 사실이긴 하지만, 진실은 필요없기 때문이었다는 것을 위의 그림이 보여주고 있다.

변수가 여러 개일 때, 결국은 행렬 곱셈을 해야 하고, y 절편도 넣을 수 있기 때문에 결국 b는 공식에 나타나지 않게 된다. 다만 b는 상수이고, 최종적으로 1회 더해지기만 하면 되므로 x쪽 행렬에 1을 넣는다. 이 부분이 굉장히 중요한데 그냥 1이다. 그래야 b와 1을 곱해서 최종적으로 b를 더하게 된다.

지금까지 보여준 왼쪽 그림은 수학적으로는 어색할 수밖에 없다. 2개의 행렬을 곱하는데, 한쪽은 수평으로 길고, 한쪽은 수직으로 길다. 어색하다. 수학을 전공했던 분들은 이런 상황을 인정하지 않고, 2개의 행렬이 똑같은 크기를 갖는다고 가정한다.

가령, 1x3 행렬 2개라던가 3x1 행렬 2개라던가.. 그런데, 행렬 곱셈에서 크기가 같은 행렬은 곱할 수가 없다. 행과 열의 크기가 같은 정방 행렬(square matrix)을 제외하면 말이다. 오른쪽 그림의 공식에서는 W의 위에 T가 붙어 있다. 수학에서는 T가 붙은 행렬을 전치 행렬이라고 부른다. 다시 말해, W 행렬을 transpose해서 사용한다는 뜻이다.

여기서 잠깐! 행렬 X는 1x3이 맞을까, 3x1이 맞을까? 둘 다 맞다고 생각한다면.. 그럴 수도 있긴 하겠지만..

최종 결과는 1개의 값으로 나와야 하기 때문에, 가능한 행렬 조합은 1x3과 3x1을 곱하는 것 뿐이다. 그래서, W 행렬을 transpose시켜야 하기 때문에 정답은 3x1 행렬이 된다.

06. cost 함수를 파이썬만으로 직접 구현

텐서플로우가 동작하는 것은 그렇다 치고 직접 앞에서 설명한 공식들을 코드로 만들고 싶어졌다. 구글링을 엄청 하면서 어렵게 만든 어색한 코드가 여기 있다.


def cost(W, X, y):
s = 0
for i in range(len(X)):
s += (W * X[i] - y[i]) ** 2

return s / len(X)

X = [1., 2., 3.]
Y = [1., 2., 3.]

W_val, cost_val = [], []
for i in range(-30, 51):
W = i*0.1
c = cost(W, X, Y)

print('{:.1f}, {:.1f}'.format(W, c))

W_val.append(W)
cost_val.append(c)

# ------------------------------------------ #

import matplotlib.pyplot as plt

plt.plot(W_val, cost_val, 'ro')
plt.ylabel('Cost')
plt.xlabel('W')
plt.show()
[출력 결과]
...
4.6, 60.5
4.7, 63.9
4.8, 67.4
4.9, 71.0
5.0, 74.7


05. Linear Regression의 cost 최소화의 TensorFlow 구현 (lab 03)에서 보여준 텐서플로우 코드와 동일한 결과를 보여준다. 기울기에 해당하는 W가 바뀌는 과정에서 cost가 어떻게 바뀌는지를 보여주고 있다. 원본 코드와 다른 점이 있다면 cost에 해당하는 값인데, 이건 중요하지 않다. 비용을 계산하는 cost 함수가 convex 형태로 진행되는지가 중요한 것이니까.


def cost(W, X, y):
s = 0
for i in range(len(X)):
s += (W * X[i] - y[i]) ** 2

return s / len(X)

cost를 계산하는 함수로 이번 코드에서 가장 중요하다. 1x3 행렬에 들어있는 X는 행렬 연산을 할 수 없어서 for문 안에서 매번 X[i]와 W를 곱한 다음에 y[i]를 빼고 있다. 여기에 제곱을 취해서 벌점을 부여해야 해서, 이 값에 ** 연산자를 사용해서 제곱을 했다. 합계는 변수 s에 저장했고, 반환할 때 갯수 m에 해당하는 len(X)로 나누었으니까 return문의 오른쪽에서 평균을 구하고 있다.

이게 간단해 보이지만, 이거 구현하기 위해서 cost 함수에 대해 연구하고 구글링한 시간이 얼마인지.. 만들고 나니까, 어이없을 정도로 간단하고 잘 동작했다.


# ------------------------------------------------------------------ #


이번에는 경사타고 내려가기(Gradient Descent) 알고리듬에서 다음 번 위치로 이동하기 위한 경사값을 직접 계산해서 앞에서 보여준 것처럼 원하는 기울기(W)가 1에 수렴하는지 확인하려고 만든 코드다.

def gradients(W, X, y):
nX = []
for i in range(len(X)):
nX.append(W * X[i] - y[i])

s = 0
for i in range(len(X)):
s += nX[i] * X[i]

return s / len(X)

X = [1., 2., 3.]
Y = [1., 2., 3.]

# 양수로 시작하면 값이 줄어들고
# 음수로 시작하면 값이 늘어난다.
W = 100 # -100으로도 테스트해 볼 것.
for i in range(1000):
g = gradients(W, X, Y)
W = W - g*0.01

if i%20 == 19:
print('{:4} : {:12.8f} {:12.8f}'.format(i+1, g, W))
[출력 결과]
 ...
 920 :   0.00000000   1.00000000
 940 :   0.00000000   1.00000000
 960 :   0.00000000   1.00000000
 980 :   0.00000000   1.00000000
1000 :   0.00000000   1.00000000

중간에 달아놓은 주석처럼 최소 비용이 1이라면 초기값 100은 경사면의 오른쪽에 있고 gradients 함수가 호출된 결과를 반영할 때마다 최소 비용을 향해 진행한다. 만약 -100으로 시작하면 경사면 왼쪽에서부터 오른쪽으로 진행하는 값을 확인할 수 있다.

gradients 함수는 cost 함수를 미분한 결과, 즉 변화량을 값으로 만들어 내는 함수이다. 이렇게 반환된 값에 learning rate에 해당하는 α(알파)를 곱해서 실제 진행량을 결정하는데, 여기서는 0.01을 사용했다. 즉, 변화량의 1/100만큼 적용하고 있다.

 α(알파) 오른쪽에 있는 식을 코드로 구현했다. 첫 번째 반복문에서 W(x) - y 를 계산해서 nX 리스트에 저장했다. 두 번째 반복문에서 이 값을 공식 오른쪽 끝의 x(i)와 곱한 결과를 s에 저장했다. return문에서 데이터 갯수로 나누어서 앞선 코드처럼 평균을 반환했다.

아래는 gradients 함수를 축약 버전으로 구성한 코드다. 반복문을 하나로 줄이고, 중간 결과를 리스트에 저장했던 코드 대신 반복문 하나로 처리했다. 머신러닝이 익숙하지 않을 때는 앞의 코드가 쉽게 느껴졌는데, 지금은 아래 코드가 훨씬 편하다. 이번 코드의 좋은 점은 수식에 나타난 것과 같은 방식으로 구성했다는 점이다. 이번 코드는 이번 글의 맨 앞에 나왔던 cost 함수와 매우 유사하다. 제곱(**) 대신 X[i]를 곱하는 코드만 다를 뿐이다. 꼭! cost 함수와 비교해서 봐야 한다.

def newGradients(W, X, y):
s = 0
for i in range(len(X)):
s += (W * X[i] - y[i]) * X[i]

return s / len(X)


텐서플로우가 한 줄도 없지만, 제대로 동작한다. 공식을 제대로 반영하는 것이 얼마나 중요한지 새삼 깨달을 수 있었다. 여기서 보여준 두 개의 함수를 구현하기 위해 1주일은 꼬박 고생했다. 공식 없이 마구잡이로 코딩하다가 벌어진 결과로 이 글을 보는 사람은 나처럼 미련하지 않기를 바라고, 계속적으로 중요하다고 하는 공식을 한번 더 보았으면 한다.


def cost(W, X, y):
s = 0
for i in range(len(X)):
s += (W * X[i] - y[i]) ** 2

return s / len(X)

def gradients(W, X, y):
nX = []
for i in range(len(X)):
nX.append(W * X[i] - y[i])

s = 0
for i in range(len(X)):
s += nX[i] * X[i]

return s / len(X)

X = [1., 2., 3.]
Y = [1., 2., 3.]

W = 100
for i in range(1000):
c = cost(W, X, Y)
g = gradients(W, X, Y)
W = W - g*0.01

# cost는 거리의 제곱을 취하기 때문에 W가 음수이건 양수이건 상관없다.
if c < 1.0e-15:
break

if i%20 == 19:
print('{:4} : {:17.12f} {:12.8f} {:12.8f}'.format(i+1, c, g, W))
[출력 결과]
  20 : 7440.099604469872 186.33428246  39.06543199
  40 : 1099.942255029881  71.64540360  15.63613245
 ...
 400 :    0.000000000001   0.00000242   1.00000049
 420 :    0.000000000000   0.00000093   1.00000019
 440 :    0.000000000000   0.00000036   1.00000007
 460 :    0.000000000000   0.00000014   1.00000003

앞에 설명했던 코드를 묶어서 cost 함수와 gradient descent 알고리듬으로 동작하는 예제를 만들었다. 이렇게 되어야 텐서플로우에서 보여주던 예제와 똑같은 예제가 된다.

반복문 안에서 cost와 gradients가 함께 사용되는 것이 핵심이다. 이 부분은 앤드류 교수님께 배웠다. 텐서플로우 내부가 어떨지는 모르겠지만, 여기서는 c(비용)가 일정 수준 이하로 떨어지면 더 이상 계산하지 않도록 처리했다. 전체 횟수만큼 반복하지 않고 미리 탈출할 수 있기 때문에 early stopping이라고 부른다. 출력 결과에서 콜론(:) 바로 오른쪽에 있는 숫자가 비용이다.

05. Linear Regression의 cost 최소화의 TensorFlow 구현 (lab 03)

텐서플로우를 사용해서 비용(cost)과 기울기(W)가 어떻게 변화하는지 보여주는 코드를 설명한다.

import tensorflow as tf

X = [1., 2., 3.]
Y = [1., 2., 3.]
m = len(X)

W = tf.placeholder(tf.float32)

hypothesis = tf.mul(W, X)
cost = tf.reduce_sum(tf.pow(hypothesis-Y, 2)) / m

init = tf.initialize_all_variables()

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

# 그래프로 표시하기 위해 데이터를 누적할 리스트
W_val, cost_val = [], []

# 0.1 단위로 증가할 수 없어서 -30부터 시작. 그래프에는 -3에서 5까지 표시됨.
for i in range(-30, 51):
xPos = i*0.1 # x 좌표. -3에서 5까지 0.1씩 증가
yPos = sess.run(cost, feed_dict={W: xPos}) # x 좌표에 따른 y 값

print('{:3.1f}, {:3.1f}'.format(xPos, yPos))

# 그래프에 표시할 데이터 누적. 단순히 리스트에 갯수를 늘려나감
W_val.append(xPos)
cost_val.append(yPos)

sess.close()
# ------------------------------------------ #

import matplotlib.pyplot as plt

plt.plot(W_val, cost_val, 'ro')
plt.ylabel('Cost')
plt.xlabel('W')
plt.show()
[출력 결과]
...
4.6, 60.5
4.7, 63.9
4.8, 67.4
4.9, 71.0
5.0, 74.7

[출력 결과]는 그래프에 표시된 좌표를 보여준다. -3에서 5까지 81개의 좌표가 나왔다. 출력된 숫자는 왼쪽 열이 W, 오른쪽 열이 Cost가 된다.

이 코드에서는 최소 비용을 찾는 것이 목적이 아니라 최소 비용을 포함하고 있는 W의 값까지 포함해서 일정 범위의 W에 대한 Cost를 계산해서 Cost가 어떻게 변화하는지 보여주는 것이 목적이다.

그래프는 W가 변화할 때의 Cost를 보여준다는 것을 명심하자. 앞선 글에서 빨강 점 단위로 이동한다고 가정할 때, 해당 위치(Cost)에서 미분한 결과를 빼면 가능하다는 것을 설명했었다.


X = [1., 2., 3.]
Y = [1., 2., 3.]
m = len(X)

W = tf.placeholder(tf.float32)

X와 Y 데이터를 1x3 매트릭스로 초기화했고, m은 데이터의 갯수를 뜻한다. 기울기(W)를 바꾸면서 비용을 계산해서 보여주려는 것이 목적이기 때문에 W를 placeholder로 처리했다. 여기서는 y 절편에 해당하는 b는 생략하고 있다. 다음 번 동영상에서 절편 b를 처리하는 쉬운 방법을 배운다. 여기서는 생략했고, 결과를 명확하게 보여주기 위한 것이라고 생각하자.


hypothesis = tf.mul(W, X)
cost = tf.reduce_sum(tf.pow(hypothesis-Y, 2)) / m

H(x) = Wx 로 식을 간단하게 정리했으므로 hypothesis에서도 b를 더하는 코드는 보이지 않는다. cost를 계산하는 코드에서는 square 함수 대신 pow 함수를 사용하고 있다. square 함수는 제곱을 처리하고, pow 함수는 지수를 처리한다. pow 함수이기 때문에 두 번째 매개변수로 2가 전달되었다. 평균을 구하기 위해 reduce_mean 함수 대신 m으로 직접 나누고 있다.

위의 코드를 이전 코드에서는 아래처럼 처리했었다.

hypothesis = W * x_data + b
cost = tf.reduce_mean(tf.square(hypothesis - y_data))


# 그래프로 표시하기 위해 데이터를 누적할 리스트
W_val, cost_val = [], []

# 0.1 단위로 증가할 수 없어서 -30부터 시작. 그래프에는 -3에서 5까지 표시됨.
for i in range(-30, 51):
xPos = i*0.1 # x 좌표. -3에서 5까지 0.1씩 증가
yPos = sess.run(cost, feed_dict={W: xPos}) # x 좌표에 따른 y 값

print('{:3.1f}, {:3.1f}'.format(xPos, yPos))

# 그래프에 표시할 데이터 누적. 단순히 리스트에 갯수를 늘려나감
W_val.append(xPos)
cost_val.append(yPos)

그래프에 출력하기 위해서 W_val과 cost_val 변수를 리스트로 만들었다. 81번의 호출 결과를 저장해야 하니까. 원본 코드에서는 50인데, 나는 51을 사용해서 왼쪽과 오른쪽의 균형을 맞추었다.

원본에는 중복되는 코드가 있어서 여기서는 xPos와 yPos로 대체했다. 코드는 몇 줄 길어졌다.


import matplotlib.pyplot as plt

plt.plot(W_val, cost_val, 'ro')
plt.ylabel('Cost')
plt.xlabel('W')
plt.show()

matplotlib 모듈은 파이썬에서 가장 많이 사용하는 그래프 출력 모듈이다. 빨강 점이 찍힌  밥그릇 그래프가 여기서 출력됐다.

plot 함수의 첫 번째는 x축 데이터, 두 번째는 y축 데이터, 세 번째는 그래프 타입. r은 빨강의 red, o는 동그라미를 의미한다. o 대신 x를 사용하면 x 표시로 바뀐다.  그래프는 show 함수를 호출하기 전까지는 출력되지 않는다.


# --------------------------------------------------------------------- #


이번 소스코드는 placeholder를 사용하는 것과 동시에 앞의 글에서 설명했던 현재 cost에 대한 미분 결과를 어떻게 계산하는지 보여준다. 여기서 한발 더 나아가서 텐서플로우가 계산한 값과 직접 계산한 값이 똑같다는 것까지 보여준다.

음.. 이번 코드는 동영상과 많은 부분에서 다르다. 동영상에서는 수동으로 계산해도 최소 비용을 잘 찾는다는 것을 보여주는 반면, 여기서는 앞에 설명한 내용들을 보여준다.

import tensorflow as tf

x_data = [1., 2., 3., 4.]
y_data = [1., 3., 5., 7.] # x와 y의 관계가 모호하다. cost가 내려가지 않는 것이 맞을 수도 있다.

# 동영상에 나온 데이터셋. 40번째 위치에서 best fit을 찾는다. 이번에는 사용하지 않음.
# x_data = [1., 2., 3.]
# y_data = [1., 2., 3.]

W = tf.Variable(tf.random_uniform([1], -10000., 10000.)) # tensor 객체 반환

X = tf.placeholder(tf.float32) # 반복문에서 x_data, y_data로 치환됨
Y = tf.placeholder(tf.float32)

hypothesis = W * X
cost = tf.reduce_mean(tf.square(hypothesis - Y))

# 동영상에서 미분을 적용해서 구한 새로운 공식. cost를 계산하는 공식
mean = tf.reduce_mean(tf.mul(tf.mul(W, X) - Y, X)) # 변경된 W가 mean에도 영향을 준다
descent = W - tf.mul(0.01, mean)
# W 업데이트. tf.assign(W, descent). 호출할 때마다 변경된 W의 값이 반영되기 때문에 업데이트된다.
update = W.assign(descent)

init = tf.initialize_all_variables()

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

for step in range(50):
uResult = sess.run(update, feed_dict={X: x_data, Y: y_data}) # 이 코드를 호출하지 않으면 W가 바뀌지 않는다.
cResult = sess.run( cost, feed_dict={X: x_data, Y: y_data}) # update에서 바꾼 W가 영향을 주기 때문에 같은 값이 나온다.
wResult = sess.run(W)
mResult = sess.run(mean, feed_dict={X: x_data, Y: y_data})

# 결과가 오른쪽과 왼쪽 경사를 번갈아 이동하면서 내려온다. 기존에 한 쪽 경계만 타고 내려오는 것과 차이가 있다.
# 최종적으로 오른쪽과 왼쪽 경사의 중앙에서 최소 비용을 얻게 된다. (생성된 난수값에 따라 한쪽 경사만 타기도 한다.)
# descent 계산에서 0.1 대신 0.01을 사용하면 오른쪽 경사만 타고 내려오는 것을 확인할 수 있다. 결국 step이 너무 커서 발생한 현상
print('{} {} {} [{}, {}]'.format(step, mResult, cResult, wResult, uResult))

print('-'*50)
print('[] 안에 들어간 2개의 결과가 동일하다. 즉, update와 cost 계산값이 동일하다.')

print(sess.run(hypothesis, feed_dict={X: 5.0}))
print(sess.run(hypothesis, feed_dict={X: 2.5})) sess.close()
[출력 결과]
...
45 952.0852661132812 120862.359375 [[ 128.6113739], [ 128.6113739]]
46 880.678955078125 103412.8828125 [[ 119.09052277], [ 119.09052277]]
47 814.6279907226562 88482.671875 [[ 110.28373718], [ 110.28373718]]
48 753.5309448242188 75708.015625 [[ 102.1374588], [ 102.1374588]]
49 697.01611328125 64777.6953125 [[ 94.60214996], [ 94.60214996]]
--------------------------------------------------
[] 안에 들어간 2개의 결과가 동일하다. 즉, update와 cost 계산값이 동일하다.
[ 473.01074219]
[ 236.50537109]

최종 결과만 확인해 보자. 오른쪽 끝에 있는 두 개의 열을 보면 된다. 여기서는 기울기(W)가 1에 수렴하지 않는다. [1, 2, 3, 4]와 [1, 3, 5, 7]은 모든 좌표를 지나는 직선이 없기 때문이다. 더욱이 50번 돌려서는 제대로 된 결과 또한 기대할 수 없다는 것을 보여준다.

이렇게 얻은 잘못된 기울기(W)에 대해 궁금증(5, 2.5)을 적용하면 말도 안되는 473과 236이 나오는 것은 당연하다. 얼마나 반복해야 적절한 기울기가 됐다는 것을 알 수 있을까? 정답은 충분히 반복해야 한다는 모호함 정도.

이번 코드는 해석이 어렵다. 일단 GradientDescentOptimizer 함수를 호출하지 않고 있다. update 계산을 통해 얻은 W를 cost에 전달하고 있다. 그럼에도 불구하고 cost는 정상적으로 최저점을 찾아서 진행한다. 이 말은 update 계산에 포함된 공식이 올바르게 GradientDescentOptimizer 함수의 역할을 하고 있다는 뜻이 된다. 즉, GradientDescentOptimizer 함수 없이 직접 만들어서 구동할 수도 있다는 것을 보여주는 예제이다.


mean    = tf.reduce_mean(tf.mul(tf.mul(W, X) - Y, X))   # 변경된 W가 mean에도 영향을 준다
descent = W - tf.mul(0.01, mean)

코드와 공식을 함께 보자.

              W(x)              -->                                                         tf.mul(W, X)
              W(x) - y         -->                                                        tf.mul(W, X) - Y
             (W(x) - y) * x   -->                                             tf.mul(tf.mul(W, X) - Y, X)
   1/m * ∑(W(x) - y) * x)  -->                    tf.reduce_mean(tf.mul(tf.mul(W, X) - Y, X))
α(1/m * ∑(W(x) - y) * x)  --> tf.mul(0.01, tf.reduce_mean(tf.mul(tf.mul(W, X) - Y, X)))

복잡하기는 하지만, 하나씩 순서대로 확장해 가니 조금 쉬워 보인다. 혹시 브라우저에 따라 수직으로 줄을 맞춘 부분이 어긋날 수도 있을 것 같다. 맥 크롬에서는 얼추 맞았다.

04. Linear Regression의 cost 최소화 알고리즘의 원리 설명 (lec 03)

드디어 정말 중요한 gradient descent 알고리듬에 대한 설명이 나온다. 정말 중요하니까, 여러 번에 걸쳐 보는 것은 필수!


왼쪽 그림은 지금까지 봐왔던 hypothesis와 cost 함수에 대한 공식이고, 오른쪽 그림은 기존 공식을 단순화하기 위해 y 절편에 해당하는 b를 없앤 공식이다. b를 없애고, cost 함수에 H(x) 대신 Wx를 직접 입력했다.

공식을 가볍게 만들기 위해 b를 없애는 것이 이상해 보일 수도 있지만, 뒤쪽 동영상에서는 실제로 b를 없앤다. W의 갯수가 많아지고, 행렬로 처리해야 하는 시점이 되면 b를 행렬에 넣어서 공식을 간단하게 만든다. 어차피 공식에서 사라질 값을, 우리의 이해를 돕기 위해 미리 없애주신 자상한 배려가 눈에 띈다.


b를 없앤 단순 버전의 공식에 대해 실제 데이터인 x, y를 직접 대입해서 cost를 계산해 본다. 오른쪽 그림은 왼쪽에서 계산한 공식이 반복된 경우를 가정한 그래프이다. 그래프의 수평은 W, 수직은 Cost라고 되어 있다. 중요하다. W의 값에 따라 Cost가 변하는 정도를 그래프로 보여주고 있다는 것을 명심하자. 밥그릇 모양이라고 교수님께서 말씀하셨다. 영어로는 Convex(볼록)하다고 말한다. 그림이 작긴 하지만, W가 0일 때, 1일 때, 2일 때만 보면 왼쪽 그림의 결과와 같음을 알 수 있다.

x와 y의 현재 데이터에서는 W가 1일 때 가장 작은 비용이 든다. W가 1이라는 것은 H(x) = X라는 뜻이고, x에 대한 y 좌표가 정확하게 일치하기 때문에 cost는 0이 된다. 다시 말해, 이것보다 작은 비용이 발생할 수는 없으므로, 현재 구한 기울기(W)가 가장 적합하다는 뜻이 된다.

x가 세 개이기 때문에 세 번 계산해야 하고, 매번 y 값을 빼서 제곱을 한 합계에 대해 평균을 내야 한다. 개인적으로는 수식을 풀어쓴 것이 더 쉽다고 느끼지만, 요소가 100개쯤 되면 (시그마)를 사용한 수식이 필요할 수밖에 없다.

W가 2일 때의 결과는 베일에 쌓여 있다. 풀어보자. W가 2라는 뜻은 H(x) = 2X 이므로 x의 모든 값에 2를 곱한 후에 계산하면 된다.

(1/3) *((2*1-1)^2 +(2*2-2)^2 +(2*3-3)^2) = (1^2 + 2^2 + 3^2)/3 = (1+4+9)/3 = 4.67

실제 그래프를 lab 03에서 출력해서 보여주고 있으니, 정말 저렇게 나오는지 궁금하겠지만, 일단 나머지를 읽고 다음 번 글을 읽어보기 바란다.


Gradient Descent 알고리듬은 왼쪽 그림에 있는 설명처럼 다음과 같은 역할을 수행한다.

1. 최저 비용을 계산하는 함수
2. 다양한 최저 계산에 사용
3. Cost 함수에 대해 최소가 되는 기울기(W)와 y 절편(b) 탐색
4. 피처(feature, 변수)가 여러 개인 버전에도 적용 가능

위의 설명을 다시 정리해 보면, 다음과 같다. 

  • 어떤 위치(왼쪽 또는 오른쪽 경사)에서 시작하더라도 최소 비용 계산
  • 기울기(W)와 y 절편( b)을 계속적으로 변경하면서 최소 비용 계산
  • 반복할 때마다 다음 번 gradients(기울기의 정도) 계산
  • 최소 비용에 수렴(converge)할 때까지 반복


공식이 변해가는 과정을 추적해 보자. 먼저 왼쪽 그림에서 데이터의 갯수가 m일 때, m과 2m 중에서 2m을 사용하겠다는 뜻이다. 리스트에 [3, 6, 9]가 들어있다고 했을 때 이걸 데이터 갯수인 m으로 나누면 [1, 2, 3]이 된다. 그런데, 2m으로 나누게 되면 [3, 6, 9]/6이 되니까 최종 결과는 [0.5, 1, 1.5]가 된다.

제곱의 평균을 구하기 위해 사용하는 m은 W와 b를 사용해서 계산이 끝난 이후에 적용하기 때문에, m으로 나누건 2m으로 나누건 W와 b에는 영향을 주지 않는다는 것을 먼저 이해해야 한다.

비용 계산의 목적은 최소 비용이 되었을 때의 W와 b가 필요하기 때문이다. 최소 비용이 얼마인지는 전혀 중요하지 않다. 여러 개의 비용이 있을 때, 이 중에서 가장 작은 비용을 찾고, 그 때의 W와 b를 사용하면 된다. 여러 개의 비용에 대해 모두 2를 곱하거나 2로 나누면, 비용은 늘어나거나 줄어들지만 상대적인 크기는 달라지지 않는다.

그렇다면, 왜 2m을 사용할까? 오른쪽 그림에 답이 있다. 제곱을 미분하게 되면 2가 앞으로 오는데, 이때 분자에 있는 2와 분모에 있는 2를 곱해서 1로 만들면 공식이 단순해지기 때문에 일부러 넣는 것이다.

가운데 그림에 이는 두 개의 공식. 위의 공식은 최소 비용을 계산하는 cost 함수, 아래는 기울기(W)의 다음 번 위치를 판단하기 위한 변량(gradients)을 계산하기 위한 공식.

  W = W - 변화량

변화량을 구하는 가장 쉽고 일반적인 수학공식은 미분(deravative)이다. 나 또한 미분을 잘 모른다. 스터디 회원의 도움을 받아 작성 중임을 고백한다. 여기서 중요한 것은 기울기(W)를 얼마나, 어떤 방향으로 변화시킬 것인지이다.  현재 위치에서 변화량을 계산해서 현재의 W에서 빼면 다음 번 W의 위치가 나오게 된다. 앞에서 나왔던 빨간 점이 찍힌 밥그릇 그래프를 보면 밥그릇의 왼쪽 경사에 있을 때는 변화량이 음수가 되어서 W의 값이 증가하게 되고 밥그릇의 오른쪽 경사에 있을 때는 변화량이 양수가 되어서 W의 값이 감소하게 된다. 결국 어느 위치에 있건 중앙에 있는 cost가 가장 적게 발생하는 최소 비용에 수렴하게 된다.

밥그릇의 오른쪽에 있을 때 기울기가 양수가 된다고 얘기했다. 미분(기울기)은 수평(x축)으로 이동할 때 수직(y축)으로 이동한 만큼의 크기를 말한다. 밥그릇 오른쪽에서는 x축도 증가하고 y축도 증가하기 때문에 크기의 차이는 있을 수 있지만, 결과는 양수가 된다. 밥그릇 왼쪽에서는 x축이 오른쪽으로 이동(증가)할 때 y축이 아래로 이동(감소)하기 때문에 결과는 음수가 된다.

그렇다면 '변화량'은 어떻게 구할 것인가? 현재 위치에서 발생한 cost에 대해 미분을 하면 된다. 그래프에서 수평은 W, 수직은 Cost라고 강조했는데, 정말 중요하다. 밥그릇에서의 미분은 W가 변하는 크기에 따른 Cost의 변화량을 측정하는 것이다. 그래서, Cost에 대해서 W로 미분을 하게 된다. 학교에서 배운 그래프에서는 x가 변할 때의 y 변화량을 계산하기 때문에 항상 x에 대해 미분을 했었다.


              


미분을 설명하려니까.. 너무 힘이 드는데.. 앞에서 공식 일부를 가져왔다. 왼쪽은 우리가 구하고자 하는 변화량이고, 가운데는 cost(W)를 가리키는 공식이다. 공식의 앞에 붙어 있는 α(알파)는 중요하지 않다. 우리가 구한 변화량을 어느 정도로 반영할 것인지에 대한 상수일 뿐이다. 왼쪽과 가운데 공식을 더하면, 오른쪽에 있는 복잡한 공식이 만들어진다. 너무 어렵다고만 생각해서인지 두 개를 더해서 오른쪽을 만들었다는 것을 꽤 오랫동안 몰랐다.

이때 분자와 분모에 있는 δ 기호는 미분에서 나타나는 도함수라고 하는 개념에 잘 설명되어 있다. 난 너무 부족해서 이 부분에 대한 설명은 넘어간다. 왼쪽 그림은 이미 W로 미분을 한 상태이고, 이제 cost(W)에 대해서 미분을 적용할 차례다.


설명이 너무 길어져서 앞의 그림을 다시 가져왔다. 첫 번째 공식에서 제곱은 두 번째 공식의 ∑(시그마) 오른쪽에 2로 변환된다. 여기에 (Wx - y)에 대해서 미분을 적용해야 하고 결과는 Wx는 x가 되고 y는 W가 없으니까 상수 처리되어서 0이 된다. 결국 (Wx - y)에 대해 W로 미분을 하면 x가 나오게 되고, 두 번째 공식의 오른쪽 끝에 추가되었다.

공통되는 것들을 정리하면 세 번째 공식이 만들어진다. 세 번째 공식의 앞에 있는 α(알파)는 우리가 임의로 추가하는 상수로 미분에는 참가하지 않는다. α(알파)가 너무 크면 오버슈팅(overshooting)이 될 수 있고, 너무 작으면 최소 비용에 수렴할 때까지 너무 오래 걸리게 된다. 이 값은 여러 번에 걸쳐 테스트하면서 결정해야 하는 중요한 값이지만, 역시 미분에는 참가하지 않는다. 뒤쪽 동영상에서 이 값을 어떻게 활용하는지 배우게 된다.


두 장의 그림은 모두 3차원으로 그려져 있고, 가로와 세로는 W와 b, 높이는 cost를 가리킨다. W와 b의 값에 따른 cost를 표현하고 있다. cost를 계산했을 때의 결과를 보여주기 때문에, 쉽게 말해 cost 함수라고 얘기할 수 있다. 오른쪽 그림의 윗부분에 cost 함수의 계산식이 나와 있다.

우리가 만든 모델의 cost 함수가 왼쪽 그래프와 같은 형태로 표현된다면, 어디서 시작하는지에 따라 지역 최저점(local minimum)에 도착할 수 있고, 우리가 원하는 최저점(global minimum)에 도착하지 못할 수 있다. 지역 최저점들이 여러 개 있을 수 있으니까, 이들을 합쳐서 local minima라고 부른다(minima는 minimum의 복수). cost 함수는 왼쪽처럼 울퉁불퉁한 형태로 동작하지 않게 만들어야 한다. 반드시 오른쪽처럼 오목한 형태가 되도록 만들어야 하는데, 이를 convex 함수라고 부른다. gradient descent 알고리듬은 convex(오목, 밥그릇) 형태에 대해서만 적용할 수 있기 때문에 매우 중요하다.

최근에 공부한 내용을 통해 모멘텀(momentum) 등의 알고리듬을 사용하면 어느 정도의 로컬 최저점을 넘어설 수 있다는 것을 알았다. 최저점을 향해 내려갈 때 관성이나 가속도같은 운동량(momentum, 기세)을 줘서 반대편으로 살짝 올라갈 수 있게 처리하는 알고리듬이 모멘텀이다.

03. Tensorflow로 간단한 linear regression 구현 (lab 02)

김성훈 교수님의 lab 두 번째 동영상에 대해서 풀어본다.

이론을 설명하는 동영상에 등장했던 공식이 처음에 나온다. hypothesis와 cost 함수를 구현하는데 필요한 공식이고, 아래 코드에서 이들 공식을 텐서플로우로 구현하고 있다.


import tensorflow as tf

x_data = [1., 2., 3.]
y_data = [1., 2., 3.]

# try to find values for w and b that compute y_data = W * x_data + b
W = tf.Variable(tf.random_uniform([1], -1.0, 1.0))
b = tf.Variable(tf.random_uniform([1], -1.0, 1.0))

# my hypothesis
hypothesis = W * x_data + b

# Simplified cost function
cost = tf.reduce_mean(tf.square(hypothesis - y_data))

# minimize
rate = tf.Variable(0.1) # learning rate, alpha
optimizer = tf.train.GradientDescentOptimizer(rate)
train = optimizer.minimize(cost)

# before starting, initialize the variables. We will 'run' this first.
init = tf.initialize_all_variables()

# launch the graph
sess = tf.Session()
sess.run(init)

# fit the line
for step in range(2001):
sess.run(train)
if step % 20 == 0:
print('{:4} {} {} {}'.format(step, sess.run(cost), sess.run(W), sess.run(b)))

# learns best fit is W: [1] b: [0]
[출력 결과]
...
1920 0.0 [ 1.] [  5.25413171e-08]
1940 0.0 [ 1.] [  5.25413171e-08]
1960 0.0 [ 1.] [  5.25413171e-08]
1980 0.0 [ 1.] [  5.25413171e-08]
2000 0.0 [ 1.] [  5.25413171e-08]
동영상에 나왔던 코드를 3.x 버전으로만 수정했고, 어떤 주석도 추가하지 않았다. 출력 결과를 보면 best fit에 해당하는 W는 1.으로, b는 5.25413171e-08으로 표시됐다. 마지막에 똑같은 결과가 계속해서 나오는 이유는 두 번째 열에 출력된 cost가 0.0이 되어서 더 이상 비용을 줄일 수 없기 때문에 계산이 필요 없어서이다.

x_data = [1., 2., 3.]
y_data = [1., 2., 3.]

x와 y 데이터가 모두 1행 3열의 리스트이다. 2차원 좌표로 표현하면 (1,1), (2,2), (3,3)이 되고, 원점에서 시작하는 y = x라는 직선을 그릴 수 있다. x와 y의 갯수가 다르다는 것은 있을 수 없다. 2차원 좌표로 표현해야 되니까.


# try to find values for w and b that compute y_data = W * x_data + b
W = tf.Variable(tf.random_uniform([1], -1.0, 1.0))
b = tf.Variable(tf.random_uniform([1], -1.0, 1.0))

# 함수 프로토타입
# tf.random_uniform(shape, minval=0, maxval=None, dtype=tf.float32, seed=None, name=None)

# print(tf.random_uniform([1], 0, 32))
# 결과 : Tensor("random_uniform_2:0", shape=(1,), dtype=float32)

cost 함수에서 궁극적으로 찾고자 하는 기울기(W)와 y 절편(b)의 초기값을 설정한다.

tf.random_uniform 함수는 정규분포 난수를 생성하는 함수로, 배열의 shape, 최소값, 최대값을 파라미터로 사용한다. 여기서는 [1], -1.0, 1.0을 전달했기 때문에 -1에서 1 사이의 난수를 1개 만든다. 결과는 1행 1열의 행렬이 된다. 나중에 여러 개의 데이터를 생성하는 코드가 나오는데, 5개일 경우 첫 번째 파라미터로 [5]라고 전달하면 된다.

여기서 난수를 사용하는 중요한 의도가 있다. 최저 비용을 스스로 찾아가야 하는데, 시작 위치가 매번 달라짐에도 불구하고 항상 최저 비용을 찾는다는 것을 보여주기 위해서. 머신러닝은 초기값으로 무엇이 주어지건, 비용이 줄어드는 방향으로 스스로 진행하는 놀라운 능력을 갖고 있다.

정확하게 위의 사실을 확인하고 싶다면, 난수 대신 상수를 전달해도 된다. 나는 각각 5, 15, 115의 세 번에 걸쳐 검사했고, 정상적으로 동작함을 확인했다.

W = tf.Variable(115.)     # 5, 15, 115로 테스트
b = tf.Variable(115.)


# 실수 대신 정수 데이터를 사용하기 위해서는 데이터와 변수를 함께 바꾸어야 한다.
# 그럼에도 여전히 난수의 내부 타입은 float32를 사용해야 한다. int32 등의 자료형은 에러.
x_data = [1, 2, 3]
y_data = [1, 2, 3]

# 정수 생성의 경우 2**n 규칙을 따르는 것이 좋다. 구글 문서.
W = tf.Variable(tf.random_uniform([1], 0, 32, dtype=tf.float32))
b = tf.Variable(tf.random_uniform([1], 0, 32))

지금까지 설명했던 코드를 바로 위의 코드처럼 바꿀 수 있다. 


# my hypothesis
hypothesis = W * x_data + b

H(x) = Wx + b라는 공식을 충실하게 구현한 코드.
먼저 W와 x_data를 곱한 결과는 무엇일까? int 또는 float으로 표현할 수 있는 1개의 값일까? 땡!

데이터를 다루는 곳에서는 이것을 벡터(vector) 연산이라고 부른다. W는 1x1, x_data는 1x3 행렬이기 때문에, 행렬 연산에 따라 결과는 1x3의 행렬이 나온다. 이해가 안 가면 행렬의 곱셈을 구글링 해보기 바란다. 여기에 b를 더하면 b는 1회 더해지는 것일까? 땡! 행렬과 행렬이 아닌 값을 더하거나 곱할 때는 행렬의 모든 요소에 영향을 주기 때문에 덧셈 또한 3회 발생한다. 그러나, 아직 run 함수를 호출하지 않았기 때문에 계산이 일어난 상태는 아니다.


# Simplified cost function
cost = tf.reduce_mean(tf.square(hypothesis - y_data))

기울기(W)와 y 절편(b)에 대한 적합성을 판단하는 정말, 정말, 정말 중요한 코드다. 머신러닝을 좀 한다고 얘기하려면, 텐서플로우 없이 파이썬 만으로 이 코드를 구성할 수 있어야 한다. 스탠포트 대학교의 앤드류 응 교수님 말씀.

코드가 아니라 공식을 풀어보면,

1. hypothesis 방정식에서 y 좌표의 값을 빼면, 단순 거리가 나온다.
   hypothesis - y_data가 여기에 해당하고 hypothesis와 y_data 모두 1x3 매트릭스. 즉, 행렬(벡터) 연산.

2. 단순 거리는 음수 또는 양수이기 때문에 제곱을 해서 멀리 있는 데이터에 벌점을 부여한다.
   tf.square() - 매트릭스에 포함된 요소에 대해 각각 제곱하는 행렬 연산

3. 합계에 대해 평균을 계산한다.
   tf.reduce_mean() - 합계 코드가 보이지 않아도 평균을 위해 내부적으로 합계 계산. 결과값은 실수 1개.


# minimize
rate = tf.Variable(0.1) # learning rate, alpha
optimizer = tf.train.GradientDescentOptimizer(rate)
train = optimizer.minimize(cost)

learning rate라고 부르는 값이 있는데, 완전, 완전, 완전 중요하다. 이 값을 자동으로 알아낼 수는 없고, 여러 번에 걸쳐 테스트하면서 적절한 값을 찾아야 한다. 여기서는 0.1을 사용하고 있다. 이때 0.1이 적용되는 대상은 기울기에 해당하는 W이다. 나중에 그래프가 나올 때 확인할 수 있다.

김성훈 교수님께서 엄청나게 강조하시는 gradient descent 알고리듬을 구현한 코드가 tf.train.GradientDescentOptimizer 함수이다. 단어 뜻 그대로 "경사타고 내려가기"라는 미분을 통해 최저 비용을 향해 진행하도록 만드는 핵심 함수이다. 이때 rate를 전달했기 때문에 매번 0.1 만큼씩 내려가게 된다. W축에 대해서.

minimize 함수는 글자 그대로 최소 비용을 찾아주는 함수라고 생각하면 된다. 그러나, 정확하게는 gradient descent 알고리듬에서 gradients를 계산해서 변수에 적용하는 일을 동시에 하는 함수다. W와 b를 적절하게 계산해서 변경하는 역할을 하는데, 그 진행 방향이 cost가 작아지는 쪽이라는 뜻이다.

이 코드에서 정말 중요한 것은 train 텐서에 연결된 것이 정말 많다는 것이다. optimizer는 직접 연결되었고, optimizer에는 cost와 rate가 연결되었으니까 이들은 한 다리 걸쳐 연결되었고, cost에는 reduce_mean과 square 함수를 통해 (hypothesis - y_data)의 결과가 두 다리 걸쳐 연결되었고, hypothesis는 W, x_data, b와 연결되었으므로 세 다리 걸쳐 연결된 상태라는 것이다. 그래서, train을 구한다는 것은 이 모든 연결된 객체들을 계산한다는 것과 같은 뜻이 된다.


# before starting, initialize the variables. We will 'run' this first.
init = tf.initialize_all_variables()

# launch the graph
sess = tf.Session()
sess.run(init)

텐서플로우를 구동하기 위해서는 그래프에 연결된 모든 변수를 초기화해야 한다. 이 코드는 run 함수를 호출하기 전에 나와야 한다. tf.initialize_all_variables()는 initialize_variables(all_variables())의 축약된 표현으로 전달된 변수들을 초기화하는 연산(op)을 반환하는 함수로, 여기서는 init 변수가 초기화 operations에 해당한다. 세션을 만들고, 앞에서 초기화한 변수들을 run 함수에 넣고 구동했다. init에 포함된 모든 텐서를 평가한다. (0.12버전에서 initialize_all_variables 함수 대신 global_variables_initializer 함수로 바뀌었다.)


# fit the line
for step in range(2001):
sess.run(train)
if step % 20 == 0:
print('{:4} {} {} {}'.format(step, sess.run(cost), sess.run(W), sess.run(b)))

range 함수는 매개변수가 생략될 경우 0부터 시작하고 2001은 포함하지 않으므로, 0에서 2000까지 2001번 반복하는 코드이다. 출력이 너무 많이 발생하는 관계로 20번에 한 번씩만 출력하도록 조절하고 있다.

반복문 바로 밑에서 sess.run(train) 함수를 호출해서, 앞서 설명한 모든 관련된 텐서들을 계산한다. 이렇게 계산한 결과는 print 함수를 통해 화면에 출력한다. 이미 계산된 결과임에도 불구하고 run 함수를 통해 확인해야 하는 것은 불편한 일일 뿐더러 다시 한번 run 함수를 호출해야 하므로 성능저하의 원인처럼 보여질 수도 있다. 여기서는 진행 결과를 보여줘야 하기 때문에 호출하는 것이고, 출력 횟수 자체를 100 또는 1000으로 지정하면 추가 계산을 최소한으로 유지할 수 있기 때문에 성능저하라고 볼 수는 없다. 특히, 이 부분은 학습을 하는 과정이기 때문에 모델을 구축한 이후에는 예측(prediction)에 관여하지 않기 때문에 문제가 되지 않는다. 참.. W와 b는 학습 과정에서 전혀 출력할 이유가 없고, cost가 줄어드는 것만으로 충분하다.


동영상에서 두 번째로 소개된 placeholder를 사용한 버전의 코드다.

import tensorflow as tf

x_data = [1., 2., 3., 4.]
y_data = [2., 4., 6., 8.]

# range is -100 ~ 100
W = tf.Variable(tf.random_uniform([1], -100., 100.))
b = tf.Variable(tf.random_uniform([1], -100., 100.))

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

hypothesis = W * X + b

cost = tf.reduce_mean(tf.square(hypothesis - Y))

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

init = tf.initialize_all_variables()

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

for step in range(2001):
sess.run(train, feed_dict={X: x_data, Y: y_data})
if step % 20 == 0:
print(step, sess.run(cost, feed_dict={X: x_data, Y: y_data}), sess.run(W), sess.run(b))

print(sess.run(hypothesis, feed_dict={X: 5})) # [ 10.]
print(sess.run(hypothesis, feed_dict={X: 2.5})) # [5.]
print(sess.run(hypothesis, feed_dict={X: [2.5, 5]})) # [ 5. 10.], 원하는 X의 값만큼 전달.

오렌지 색이 그냥 좋다.
오렌지 색으로 표시한 코드가 중요하다. 많이 바꾼 것 같지만, 실제로는 그다지 바뀐게 없다.

x_data와 y_data를 각가 4개씩으로 하고 y_data의 값이 x_data의 두 배가 되도록 했다.  기울기(W)와 y 절편(b)의 값은 -10에서 10까지의 숫자로 조금 크게 했고, 이들을 받을 hypothesis를 구성했다. hypothesis에서 예전에는 x_data라는 상수 데이터(바뀌지 않기 때문에)를 사용했다면 이제는 언제든지 다른 데이터를 전달할 수 있도록 placeholder X를 사용하고 있다. cost를 계산하는 코드에서도 y_data 대신 placeholder Y를 전달했다.

for문 안쪽에서 X, Y에 대해 x_data, y_data를 전달하는 코드가 보인다. 여기까지만 보면 X, Y를 만든 이유가 없다. 그러나, for문 바깥에서 X에 새로운 데이터를 넣어서 구동하는 코드가 중요하다. 우리의 목적은 비용을 최소로 만드는 기울기(W)와 y 절편( b)를 구하는 것이었고, 이 값들은 hypothesis에 공식과 함께 저장되어 있다. X를 전달하면 Y를 결과값으로 알려준다. 5나 2.5처럼 한 개를 전달해도 되고 [2.5, 5]처럼 여러 개를 전달해도 된다. 5는 1x1, [2.5, 5]는 1x2 크기의 행렬이다.

01. TensorFlow의 설치 및 기본적인 operations (lab 01)

김성훈 교수님께서 올려놓은 lab으로 시작하는 텐서플로우 코딩 동영상에 관해 풀어본다. 참고로 교수님께서 사용하신 파이썬은 2.x여서, 이곳에 올라가는 모든 코드는 3.x 버전으로 수정을 해서 올릴 생각이다.


텐서플로우는 그래프라고 하는 자료구조(Data Structure)처럼 동작한다는 것을 알려주는 그림이다.  그래프는 자료구조에서 리스트, 트리 등을 배운 다음에 배우게 되는 고급 자료구조의 영역에 속하는 내용이고, 정거장처럼 보이는 노드(node)가 간선(edge)으로 연결된 다른 노드로 이동할 수 있다는 것을 전제로 한다. 연결되어 있기만 하면 어디든 가기 때문에, 이론적으로는 이동할 수 있는 노드 갯수에 제한이 없다. 그래서, 일반적으로는 노드만 중요하고 간선은 상대적으로 덜 중요하다. 단순하게 이동할 수 있는 방법만 제안하면 되기 때문에.

텐서플로우에서는 노드에 연산(계산, operation)을 담고, 에찌(간선)에 데이터를 담고 있다고 설명한다.  직역을 하면, 에찌는 노드간에 전달되는 다차원 데이터 배열(텐서)이라고 되어 있다. 처음에는 이 말이 이해가 가지 않았다. 어떻게 에찌에 데이터가 있지? 노드가 중요하기 때문에, 노드에 데이터를 담고 데이터와 데이터 사이에 연산자를 넣는 것이 맞는데..라는 생각이 끊이질 않았다.

스터디를 하는 과정에서 에찌에 대한 얘기가 나왔었고, 그 와중에 퍼뜩 떠올랐다. 데이터가 있고, 데이터를 연산에 전달하면 수정된 데이터가 나오고. 연산에는 입력과 출력이 있는데, 입력 데이터는 결국 에찌가 되고, 출력 데이터 또한 에찌가 될 수 있겠다는. 이 부분은 그다지 중요하지는 않다. 그냥 궁금해서 고민해 본 내용일 뿐.


텐서플로우 설치에서 회원들 사이에 말이 많았다. 맥으로 작업하기 때문에 설치에는 전혀 어려움이 없었고, 동영상에 소개된 위의 그림대로 진행했고 바로 설치됐다. 그런데, 자세히 안 봐서 몰랐는데, 윈도우에서는 직접 설치가 안 되고 도커를 사용해야 한다는 설명이 있었다. 윈도우 관련 설치는 다른 문서를 참고하기 바란다. (2016년 12월부터는 도커 없이 윈도우에 직접 설치가 가능하다. 윈도우7과 10에서 검증했다. 덕분에 우분투를 켜지 않게 됐다.)
구글 텐서플로우 pip 설치 페이지는 여기.


def hello():
a = tf.constant('hello, tensorflow!')
print(a) # Tensor("Const:0", shape=(), dtype=string)

sess = tf.Session()
result = sess.run(a)

# 2.x 버전에서는 문자열로 출력되지만, 3.x 버전에서는 byte 자료형
# 문자열로 변환하기 위해 decode 함수로 변환
print(result) # b'hello, tensorflow!'
print(type(result)) # <class 'bytes'>
print(result.decode(encoding='utf-8')) # hello, tensorflow!
print(type(result.decode(encoding='utf-8'))) # <class 'str'>

# 세션 닫기
sess.close()

일단 코드는 함수로 작성을 했다. 

교수님 코드에서는 파일 단위로 되어 있지만, 코드가 간단해서 여러 개를 하나의 파일에 넣다 보니까 함수로 만들어야 했다. print 함수는 오른쪽에 ()가 있기 때문에 3.x 버전인 것을 쉽게 알 수 있다. 교수님 코드에서 이것과 xrange 함수 호출만 수정하면 대부분 변환이 된다. 대단한 작업을 한 것이 전혀 아니다. print 함수 오른쪽에는 실행했을 때의 결과를 주석으로 넣어 놓았다.

변수 a는 문자열 상수를 저장하고 있다. 그렇다면, 앞에서 설명한 바에 따르면 내부적으로 에찌에 저장된다는 말일까? 이 부분은 확인하기는 어려워 보인다. 어찌 됐든 print(a)에서 데이터가 직접 출력되지 않는 것은 당연하다. 머신러닝에서는 현재 데이터가 무엇인지 판단할 수 없는 상황이 매우 많다. 구동시켜 보기 전에는, 즉 run 함수를 호출하기 전에는 값을 알 수 없기 때문에 일관되게 처리하기 위해서는 모든 텐서 객체에 대해 자신이 누구인지만 알려주는 요약본을 출력하는 것이 맞다.

구동하기 위해서는 세션이 필요하다. 아직 세션이 무엇인지 모르기 때문에, 이 부분에 대해서는 알게 되는 시점에 다시 설명한다. 텐서플로우 구동은 세션에 포함된 run 함수를 호출하면 된다. 

  sess.run(a)

run 호출의 결과를 type  함수로 확인했다. 디코딩을 하기 전에는 bytes라고 하는 일종의 바이트 배열이다. 어떤 데이터인지 정확하게 알고 있기 때문에 utf-8 인코딩을 적용했고, 파이썬 문자열 타입인 str을 얻을 수 있었다.

세션 사용이 끝나면 닫는 것이 좋다. 코드가 종료되기 때문에 닫지 않아도 괜찮긴 하겠지만, 이런 것은 습관의 영역으로 보인다.


def constant():
a = tf.constant(2)
b = tf.constant(3)

# with 구문을 벗어날 때, 종료 코드가 있다면 대신 호출해 줌
# 예외가 발생한 경우에도 보장
with tf.Session() as sess:
result = sess.run(a+b)
print(type(result)) # <class 'numpy.int32'>
print(result) # 5

# int 자료형과 연산 가능
print(result + 7) # 12
print(type(result + 7)) # <class 'numpy.int64'>

숫자 상수를 텐서플로우로 만들었다. with 구문을 사용하면 리소스를 정리하는 close 비슷한 함수를 호출하지 않아도 된다. with 구문에서 알아서 호출해 준다. sess.close()를 호출하지 않았다. 파일 열기와 닫기 같은, 쌍을 이루는 코드에 주로 사용한다. as 연산자는 별칭을 주는 기능이고, with 구문에서는 변수를 만들 수 없기 때문에 as를 사용해서 이름을 주어야 한다.

정수를 덧셈한 결과를 저장한 자료형은 numpy 모듈에 있는 int32 자료형이다. 파이썬에서 기본적으로 사용하고 있는 int 자료형이 아닌 것이 중요하다. 파이썬은 인터프리터 방식의 엄청나게 느린 언어이기 때문에 굳이 성능을 올리겠다고 생각한다면, 파이썬 코드를 최소한으로 유지하고 모듈에 포함된 기능을 사용하면 된다.

numpy는 C 언어에 있는 배열과 같은 형태로 움직이는 다차원 배열을 기반으로 하는 모듈이다. 빅데이터, 머신러닝, 과학산술 등의 수치연산이 필요한 모든 경우에 최적의 성능을 보장해 준다. 그래서, 텐서플로우는 내부적으로 numpy를 사용할 수밖에 없다. 여기서는 numpy 코드가 나오면 함께 설명을 할 생각이다. 참, numpy에 대한 발음은 '넘피'와 '넘파이' 둘 중의 하나를 사용하면 되는데, 외국 동영상 등에 자주 등장하는 발음은 '넘파이'. 처음에는 '넘피'로 발음하다가 지금은 '넘파이'를 쓰고 있다.

마지막 줄의 코드에서 7을 더하면 자료형이 int64로 바뀐다. 이것은 numpy의 고유한 기능이다. 32비트 숫자 2개를 더하면 오버플로우(overflow)라는 데이터 넘침 현상이 일어날 수 있기 때문에 수치연산을 많이 하는 numpy에서는 이러한 오버플로우를 막기 위한 당연한 조치이다.


def placeHolder():
a = tf.placeholder(tf.int16)
b = tf.placeholder(tf.int16)

add = tf.add(a, b)
mul = tf.mul(a, b)

with tf.Session() as sess:
# {a: 2, b: 3}는 딕셔너리
# key로 'a'와 'b'를 사용하고, value로 2와 3 사용
# free_dict를 사용하지 않을 경우 None 기본값 적용
r1 = sess.run(add, feed_dict={a: 2, b: 3})
r2 = sess.run(mul, feed_dict={a: 2, b: 3})

print(type(r1)) # <class 'numpy.int16'>
print(r1, r2) # 5, 6

placeholder는 자리만 차지하고 있는 물건이나 사람을 뜻하는 영어 단어다. 그런데, 텐서플로우에 와서 엄청나게 중요한 역할이 주어졌다. 머신러닝에 전달되는 데이터를 변경하기 위한 수단이 되었다. 머신에게 공부를 시킨 이유는 내가 궁금한 무엇을 물어보기 위해서다. 그렇다면 궁금한 것을 전달해야 하고, 전달할 수 있는 문법이 있어야 하는데, 그것이 placehoder이다.

placeholder를 만들 때는 우리가 궁금해 하는 데이터의 자료형에 대해 알려줘야 한다. 여기서는 매우 작은 정수를 다루기 때문에 tf.int16이라고 지정했고, 출력 결과에서는 numpy.int16이라고 표시됐다. 사용자에게, 가능하면 numpy라고 하는 생소한 이름을 언급하지 않으려는 배려라고 보여지는 부분이다.

add()와 mul()이라는 덧셈과 곱셈 연산(노드)을 만들었고, 어떤 데이터를 전달할지는 나중에 결정할 수 있도록 placeholder로 처리했다. with 블록 안에서 add에 대해 결과를 요청하면서 2, 3을 파라미터로 전달했다. 지금은 별거 아닌 것처럼 보이지만, 그래프 기반이라서 add()와 mul() 등을 수십 개 연결할 수 있다고 생각해 보면 엄청나게 복잡한 연산을 매우 쉽게 처리할 수 있는 효과적인 방법이라는 것을 알 수 있다. 딕셔너리(사전) 자료형을 사용하기 때문에 파라미터의 갯수에는 제한이 없다. 100개를 전달해도 괜찮고 실전에서는 이런 일이 빈번하게 일어난다고 들었다. 배우는 중이라서 직접 넣어본 적이 없다.


def showTensor():
sess = tf.InteractiveSession()

x = tf.Variable([1.0, 2.0])
a = tf.constant([3.0, 3.0])

# x에 대해서 연산을 수행해서 결과를 먼저 만든다.
x.initializer.run() # Initialize 'x' using the run() method of its initializer op.

sub = tf.sub(x, a) # Add an op to subtract 'a' from 'x'. Run it and print the result
print(sub.eval()) # [-2. -1.]

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

# 결과를 내장하고 있다면 eval() 사용 가능. initializer 없이 x에 대해서 호출하면 비정상 종료
print(a.eval()) # [ 3. 3.]
print(x.eval()) # [ 1. 2.]

# -1에서 1 사이의 정규분포 난수 3개 생성. b는 1행 3열의 텐서 객체
b = tf.random_uniform([3], -1.0, 1.0)
print(type(b)) # <class 'tensorflow.python.framework.ops.Tensor'>
print(b.eval()) # [-0.16271138 -0.33350062 0.51194 ]

# tensor라면 initializer 사용
w = tf.Variable(tf.random_uniform([5, 3], 0, 32, dtype=tf.int32))
w.initializer.run()
print(w.eval()) # [[15 1 21] [14 16 27] [13 30 28] [23 21 26] [15 19 16]]

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

x = [[1., 1.], [10., 2.]]
print(tf.reduce_mean(x).eval()) # 3.5, 전체 평균
print(tf.reduce_mean(x, 0).eval()) # [ 5.5 1.5], 0은 column
print(tf.reduce_mean(x, 1).eval()) # [ 1. 6.], 1은 row

sess.close()

정말 세션을 만들고, run 함수를 정상적으로 구동해야 하는지 궁금해서 구글링을 열심히 했다. 어쨌건 세션은 반드시 필요하긴 한데, 미리 만들어 놓은 세션에 연결하기 위한 InteractiveSession 함수를 찾을 수 있었다. 세션을 만들 수 있는 방법이 두 가지 있는데, 대부분 Session 클래스를 사용한다. InteractiveSession은 주피터 등에서 코드와 설명을 함께 구성할 때만 사용한다.

코드에 대한 설명은 달아놓은 주석으로 대신한다.

02. Linear Regression의 Hypothesis와 cost 설명 (lec 02)

김성훈 교수님 동영상에서 lec 02에 해당하는 부분. 먼저 몇 가지 용어에 대한 정리가 필요하다.

1. 회귀분석 (http://math7.tistory.com/118 에서 발췌)

점들이 퍼져있는 형태에서 패턴을 찾아내고, 이 패턴을 활용해서 무언가를 예측하는 분석. 새로운 표본을 뽑았을 때 평균으로 돌아가려는 특징이 있기 때문에 붙은 이름. 회귀(回歸 돌 회, 돌아갈 귀)라는 용어는 일반적으로 '돌아간다'는 정도로만 사용하기 때문에 회귀로부터 '예측'이라는 단어를 떠올리기는 쉽지 않다.

2. Linear Regression

2차원 좌표에 분포된 데이터를 1차원 직선 방정식을 통해 표현되지 않은 데이터를 예측하기 위한 분석 모델. 머신러닝 입문에서는 기본적으로 2차원이나 3차원까지만 정리하면 된다. 여기서는 편의상 1차원 직선으로 정리하고 있다. xy축 좌표계에서 직선을 그렸다고 생각하면 된다.

3. Hypothesis

Linear Regression에서 사용하는 1차원 방정식을 가리키는 용어로, 우리말로는 가설이라고 한다. 수식에서는 h(x) 또는 H(x)로 표현한다.

H(x) = Wx + b에서 Wx + b는 x에 대한 1차 방적식으로 직선을 표현한다는 것은 모두 알 것이고, 기울기에 해당하는 W(Weight)와 절편에 해당하는 b(bias)가 반복되는 과정에서 계속 바뀌고, 마지막 루프에서 바뀐 최종 값을 사용해서 데이터 예측(prediction)에 사용하게 된다. 최종 결과로 나온 가설을 모델(model)이라고 부르고, "학습되었다"라고 한다. 학습된 모델은 배포되어서 새로운 학습을 통해 수정되기 전까지 지속적으로 활용된다.

4. Cost (비용)

앞에서 설명한 Hypothesis 방정식에 대한 비용(cost)으로 방정식의 결과가 크게 나오면 좋지 않다고 얘기하고, 루프를 돌 때마다 W와 b를 비용이 적게 발생하는 방향으로 수정하게 된다.

놀랍게도 미분이라는 수학 공식을 통해 스스로 최저 비용을 찾아가는 마술같은 경험을 하게 될 것이다. 프로그래밍을 하는 사람의 입장에서 어떻게 이런 일이 생길 수 있을까.. 라고 생각할 정도로 놀라운 경험이었다. 그런데, 여전히 신기하다!

5. Cost 함수

Hypothesis 방정식을 포함하는 계산식으로, 현재의 기울기(W)와 절편(b)에 대해 비용을 계산해 주는 함수다. 매번 호출할 때마다 반환값으로 표현되는 비용이 줄어들도록 코딩되어야 한다. 여기서는 Linear Regression에서 최소 비용을 검색하기 위한 역할을 담당한다.


# ----------------------------------------------------------- #


첨부한 이미지는 lec_02 동영상과 함께 배포되는 pdf 파일에서 가져왔다. 먼저 그림을 통해 hypothesis가 무엇인지 알아보자.

왼쪽 그림을 보면, 노랑, 파랑, 빨강의 직선이 3개 그려져 있다. xy 좌표에서 데이터는 (1,1), (2,2), (3,3)에 3개가 있는 상태이고, 가장 좋은 직선을 찾으라고 한다면 당연히 파랑색 직선이다.

그런데, 눈으로 보면 파랑색이 가장 좋다는 것을 알지만, 어떤 데이터가 있는지도 모르는 상태에서 어떻게 파랑색이라고 단정지을 수 있겠는가? 이 때, 오른쪽 그림이 사용된다. 다만 왼쪽 그림과 달리 직선의 기울기와 절편이 조금 달라졌다. 그러니 여기서의 파랑색 직선은 왼쪽 그림에서 노랑이거나 빨강일 수도 있고, 실제로는 모든 직선이라고 보면 된다.

우리가 찾으려는 직선은 모든 데이터를 관통하면 최상이겠지만, 들쭉날쭉한 데이터에 대해 그런 상황은 거의 불가능하다. 그래서, 데이터에 가장 가깝게 그려지는 직선을 찾는 것이 목표가 되고, 직선으로부터 각각의 데이터(좌표)까지의 거리 합계를 계산한 것을 cost라고 부르고, 이 값이 가장 작은 직선을 찾으면 목표 달성이다.

위의 그림 2장이 어떻게 직선으로부터 데이터가 위치한 좌표까지의 거리가 가까운지 판단하는 방법을 보여준다. 결국은 직선으로 표현하고 있지만, 이것은 x축에 대한 y값을 갖는 3개의 좌표라고 보는 것이 좋다. 특정 데이터까지의 거리를 계산할 때 삐딱하게 재는 것은 이상하지 않겠는가? 수직선을 내리거나 올려서 직선에 닿는 y 좌표를 계산하면 공정한 거리를 얻을 수 있다.

왼쪽 그림에서, y를 빼는 행위에는 H(x)에 포함된 모든 x만큼의 계산이 포함되어 있다. 즉, 뺄셈 1회가 아니라 3회를 하게 된다. 오른쪽 그림을 보면, 상당히 복잡한 형태로 빼는 것에 대해 나열하고 있다. H(x(1)) - y(1) 이라고 하는 표현식은 x의 1번째 데이터를 입력했을 때의 방정식(hypothesis) 결과에서 y의 1번째 데이터를 뺐다는 뜻이다. 위의 그림만으로 판단한다면 x의 1번째는 1이고, y의 1번째도 1이다. 그러나, W와 b가 모호하기 때문에 정확한 결과값을 볼 수는 없다.

그렇다면, 왜 hypothesis의 결과에서 y를 뺀 다음에 제곱을 하는 것일까? 이 부분은 통계에서 표준편차와 분산이라고 하는 것을 스치듯이 읽기만 해도 알 수 있다. 나는 머신러닝 공부하려고 스치듯이 동영상을 시청했다.

1. 뺄셈을 하게 되면 직선 위치에 따라 음수와 양수가 섞여서 나오게 된다. 계산이 피곤해진다.
2. 절대값을 취하는 것이 가장 쉽지만, 제곱을 하는 것도 방법이다. (음수x음수)와 (양수x양수)에 대해 항상 양수가 나오니까
3. 제곱을 하면 가까운 데이터는 작은 값이 나오고, 멀리 있는 데이터는 큰 값이 나오기 때문에 멀리 있는 데이터에 벌점(penalty)을 부과할 수 있다.

최종적으로는 좌표가 3개 있으니까, 3으로 나눈다. 왜 나누냐고 할 수도 있지만, 나누지 않으면 데이터가 100개만 되도 값이 엄청나게 커지게 되고 이후 계산이 복잡해진다. 평균을 내기 위해 합계를 구한 다음에 갯수로 나누는 것과 같다고 보면 된다.

공식 중에 마징가제트처럼 생긴 ∑(시그마, sigma)가 있는데, 이건 오른쪽에 있는 식에 대해 1부터 m번째 까지를 적용한 합계를 뜻한다. 이게 없으면 위쪽에 표현된 복잡한 계산식을 다 써야 하고, 데이터가 100개쯤 되면 쓸 방법도 없게 되니까, 무조건 익숙해져야 한다. 당연한 얘기로 데이터 개수는 m개 이므로  ∑(시그마)에서도 1에서 m개라고 얘기하고, 전체 갯수로 나누기 위해 1/m에서도 사용된다. 즉, m이 2회 나온다.

앞에서 거리의 제곱을 취한다고 얘기했는데, 이 방법을 LSM(Least Square Method)이라고 부른다. 통계학 관련서적에 보면, 절대값을 이용한 처리보다 튼튼(robust)하다고 알려져 있다. 이 부분에 대해서는 통계에 대한 지식이 약해 설명을 생략하지만, 최소 제곱을 사용한 방법이 더 좋은 결과를 낸다고만 이해하고 있다. 그리고, 표준편차를 이용한 방법도 가능할수 있는데, LSM에 비해 미분하기가 어렵다는 단점이 있다고 한다.

왼쪽 그림을 보자. 앞에서 설명했던 공식을 다시 정리하고 있다. 데이터까지의 합계를 m으로 나눈 결과는 해당 hypothesis에 대한 비용(cost)이다. 이 비용을 최소로 만드는 W와 b를 찾는 것이 목적이다. 오른쪽 그림이 말하려고 하는 것이다. 다만 cost(W,b)라고 표현할 때, 나한테는 W와 b를 찾는 것처럼 보였다.

목표는 cost를 최소로 만드는 W(기울기)와 b(절편)를 찾는 것이다. 

말장난처럼 들릴 수도 있지만, 이 개념을 이해하지 못해서 뒷 과정을 공부하는데 엄청나게 고생했다는 것만 알아주기 바란다. cost(W,b)로 시작하는 공식을 보면, 실제로는 W와 b가 보이지 않는다. H(x) 안에 숨어있기 때문이다.

f(x) = 3x+2라는 식에서 f(x) 안에 포함된 x가 무엇이냐에 따라 오른쪽 식의 결과가 달라지는 것처럼 cost(W,b)를 이해할 때 W와 b가 바뀔 때마다 오른쪽 식의 결과가 달라진다고 생각해도 좋다.  즉, W와 b를 공식에 적용하는 과정에서 cost가 줄어들도록 W와  b를 변경하는 것이 진짜 중요하다. 이 부분은 뒤에서 자세히 설명한다.

00. 머신러닝 입문기

파이썬을 가르치면서
의도하지 않았던 방향으로 흘러가는 인생을 느낍니다.
결국 머신러닝까지 왔고
늦은 건 아닌지 걱정하며 공부합니다.

현재,
초보자로써 머신러닝에 대해 느끼는 궁금함을 정리하고 싶었습니다.
금방 뭘 궁금해했는지 모를까봐 걱정입니다.

여기에 기록되는 내용은
파이썬을 배웠던 분들과 진행한 머신러닝 스터디를 준비하면서 정리했던,
홍콩과기대 김성훈 교수님의 동영상을 토대로 작성했습니다.

잘된 점은 모두 교수님 덕분이고
바라는 것이 있다면
교수님께서 하신 말씀을 보시는 분들이 이해할 수 있도록 
나름 정리해 놓는 것입니다.
나중에 뵙게 된다면
정리 잘 했다라는 칭찬 정도는 받아보고 싶습니다.

참.. 댓글은 허용하지 않습니다.
댓글에 답을 달 수 없다는 점을 알기 때문에
해당 댓글에 대해 책임질 수 없다면 
댓글을 막아두는 것이 좋다고 생각합니다.
아쉬운 점은
피드백을 받지 못해서 잘못된 내용에 대해 수정하지 못하는 점일텐데..

그래도
책임질 수 없으니까, 댓글은 없도록 하겠습니다.

감사합니다. 꾸벅.