mnist 숫자를 파일로 저장

손으로 쓴 숫자를 딥러닝으로 분류하고 싶다.
손으로 쓴 다음 카메라로 찍거나
그림판에서 동일한 크기로 마우스로 숫자를 그리거나 해보면
결과가 잘 나오지 않는다.
그렇다고 검색한 결과를 캡쳐하는 것은 바보 같다는 생각이 들고.

결과가 잘 나오지 않는 것은
이미지가 너무 작아서 축소하는 과정에서
mnist 원본과 같은 분포의 픽셀이 잘 만들어지지 않기 때문이다.
잘 할 수 있는 사람도 있겠지만
나는 이런저런 실패만 맛보았다.

이걸 하고 싶었던 이유는
스마트폰에 딥러닝 모델을 얹고 이미지로 결과를 보고 싶은데
mnist 모델이 가장 쉽고 단순하기 때문이다.
사진을 촬영하는 것 대신
다운로드한 파일을 읽어서 처리하면 같은 방식이기 때문에
숫자 이미지 파일이 필요했다.

구현하다 보니까
점점 정교한 이미지가 필요해서 결국 3가지 형태로 발전했다.
완전 정교하진 않지만 마지막 방법을 가장 선호하지 않을까 싶다, 나처럼!

먼저 공통 함수 몇 개를 정의했다.
티스토리 에디터가 업그레이드되고 나서 코드에 색상이 들어가지 않는다.
코드 블록을 넣어서 처리하도록 되었는데
편집할 때는 존재하던 색상이 외부에서 접근하게 되면 사라진다.
티스토리, 대단하다.
이런 코드를 보게 해줘서 고맙고 카카오의 미래는 참 밝아 보인다.

from tensorflow.examples.tutorials.mnist import input_data
import matplotlib.pyplot as plt
import numpy as np
import os


# mnist 데이터셋 중에서 test 데이터셋 사용
# 'mnist' 폴더에 파일 다운로드
def get_dataset():
    mnist = input_data.read_data_sets('mnist')
    return mnist.test.images, mnist.test.labels


# 파일을 저장할 폴더 확인 후 생성
def make_directory(dir_path):
    if not os.path.exists(dir_path):
        os.mkdir(dir_path)


# 파일 이름은 한 자리 숫자가 앞에 오고, 세 자리 일련번호가 뒤에 온다.
# 일련번호 최대 숫자는 999이기 때문에 1천장을 넘어가면 형식이 어긋난다.
def get_file_path(digit, order, dir_path):
    filename = '{}_{:03d}.png'.format(digit, order)
    return os.path.join(dir_path, filename)

 

첫 번째 저장 코드는 가장 단순한 형태.
test 데이터셋의 처음부터 지정한 개수만큼 저장한다.
이때 파일 이름은 앞에 정답에 해당하는 label이 오고 뒤에 몇 번째 파일인지를 가리키는 일련번호가 온다.

# mnist의 test 데이터셋으로부터 지정한 개수만큼 파일로 변환해서 저장
def make_digit_images_from_mnist(count, dir_path):
    images, labels = get_dataset()

    make_directory(dir_path)

    if count > len(images):
        count = len(images)

    for i in range(count):
        file_path = get_file_path(labels[i], i, dir_path)   # i는 일련번호 + 배열 인덱스
        plt.imsave(file_path, images[i].reshape(28, 28), cmap='gray')


make_digit_images_from_mnist(30, './mnist/new_data')

 

두 번째는 난수 개념을 추가했다.
매번 앞에서만 가져오면 항상 똑같은 순서와 똑같은 숫자만 가져오니까.
사람이라면 이런 생각이 나지 않을 수 없다.

# 난수 샘플링 적용
def make_random_images_from_mnist(count, dir_path):
    images, labels = get_dataset()

    make_directory(dir_path)

    if count > len(images):
        count = len(images)

    # 중복되지 않는 인덱스 추출
    series = np.arange(len(images))
    indices = np.random.choice(series, count, replace=False)

    for i, idx in enumerate(indices):
        # i는 일련번호, idx는 배열의 인덱스
        file_path = get_file_path(labels[idx], i, dir_path)
        plt.imsave(file_path, images[idx].reshape(28, 28), cmap='gray')


make_random_images_from_mnist(30, './mnist/new_data')

 

세 번째는 생성되는 숫자 개수를 똑같이 맞췄다.
앞의 방법들로 가져와서 보면 샘플 숫자의 개수가 일정하지 않아서
예측에 사용하기에는 많이 불편했다.
코드는 길어졌지만, 복사해서 사용할거니까 이걸 써야겠다.

# 숫자별로 지정한 개수만큼 동일하게 추출. 추출 개수는 digit_count * 10.
def make_random_images_from_mnist_by_digit(digit_count, dir_path):
    images, labels = get_dataset()

    make_directory(dir_path)

    if digit_count > len(images) // 10:
        digit_count = len(images) // 10

    # 동일 개수를 보장해야 하므로 전체 인덱스 필요
    indices = np.arange(len(images))
    np.random.shuffle(indices)

    # 0~9까지 10개 key를 사용하고, value는 0에서 시작
    extracted_digit = {0: 0, 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0, 8: 0, 9: 0}
    extracted_total = 0

    for idx in indices:
        digit = labels[idx]

        # 숫자별 최대 개수를 채웠다면, 처음으로.
        if extracted_digit[digit] >= digit_count:
            continue

        # 현재 숫자 증가
        extracted_digit[digit] += 1

        file_path = get_file_path(digit, extracted_total, dir_path)
        plt.imsave(file_path, images[idx].reshape(28, 28), cmap='gray')

        # 추출 숫자 전체와 추출해야 할 개수 비교
        extracted_total += 1
        if extracted_total >= digit_count * 10:
            break


make_random_images_from_mnist_by_digit(digit_count=2, dir_path='./mnist/new_data')

 

생성된 파일을 안드로이드 에뮬레이터에 넣는 방법은 아래 사이트를 참고하자.
아이폰 시뮬레이터는 직접 찾아보자.
요즘엔 대부분의 테스트를 안드로이드에서 하고 있다. ^^

[Cordova] android studio emulater에 이미지 파일 넣는 방법

CNN 흉내내기

5x5 크기의 문자 리스트를 사용해서 CNN 유사 모델 구현
다만 데이터셋이 숫자당 1개밖에 없기 때문에 제대로 된 학습은 불가능.
그럼에도 불구하고 상당한 수준으로 예측한다.

CNN의 후반부를 구성하는 FC에 대해 정확하게 이해할 수 있는 훌륭한 예제이다.
이전 예제에서 멀티 레이어를 구성하는 방법을 배웠다면,
여기서는 레이어에 포함된 뉴런의 의미와 갯수를 비롯해서 정답을 찾아가기 위해 어떻게 변환하는지 확인할 수 있다.

원본 코드
https://github.com/joelgrus/data-science-from-scratch/blob/master/code/neural_networks.py

원본에서는 코드가 흩어져 있거나 내 생각과 다른 부분이 있어서
새롭게 정리하고 일부 코드를 추가했다.
그 과정에서 코드가 너무 길어졌고, 읽기 편하도록 두 개로 분리했다.

아래 코드는 역전파(back-propagation) 알고리즘을 사용해서 학습하고,
마지막에는 전달된 데이터에 대해 예측까지 보여준다.
핵심 코드는 back_propagate 함수에 있고,
뉴런에 들어있는 weights를 진짜로 변경하는 것이 어렵지 않다는 것을 보여준다.
sigmoid,neuron_output,feed_forward 함수는 다른 문서에서도 함께 사용하는 공통 코드이다.
"밑바닥부터 시작하는 데이터 과학" 8장에 있는 "CAPTCHA깨기"에서 발췌한 코드를
세분화시켜서 여러 개의 문서로 만드는 과정에서 사용된 공통 코드이다.

import LinAlg                   # from linear_algebra import dot
import math, random

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

def neuron_output(weights, inputs):
return sigmoid(LinAlg.dot(weights, inputs))

def feed_forward(neural_network, input_vector):
outputs = []
for layer in neural_network:
input_with_bias = input_vector + [1]
output = [neuron_output(neuron, input_with_bias) for neuron in layer]

outputs.append(output)
input_vector = output

return outputs

def back_propagate(network, input_vector, target):
''' network에 들어있는 히든과 출력 레이어에 포함된 neuron의 weights를 변경.
히든과 출력 레이어에 포함된 뉴런의 weights를 변경할 수 있다는 것이 핵심.
변경해야 하는 올바른 방향을 제시할 수 있다면, 항상 정답을 찾아가는 것을 보장한다. '''
# unpack. network이 2개짜리 리스트라고 가정하고 있다. 히든과 출력 레이어.
hidden_layer, output_layer = network

# network이 2개짜리 레이어이므로
# feed_forward 함수 안에서 2회 반복하게 되고, 반환값 또한 2개짜리가 된다.
hidden_outputs, outputs = feed_forward(network, input_vector)

# 아래에 똑같은 코드를 2회 반복한다. 2회라서 반복문으로 구성하지 않고 나열해서 처리한다.
# 레이어가 많아진다면 반드시 반복문으로 꾸며야 한다.

# output * (1 - output)은 sigmoid의 미분. output_deltas는 1차원 10개짜리. (10,)
# output_deltas는 히든 레이어에 들어있는 뉴런과 계산할 weights.
output_deltas = [output * (1 - output) * (output - t) for output, t in zip(outputs, target)]

# 출력 레이어의 뉴런에 대해 weight 조정. output_neuron은 리스트이기 때문에 직접 수정할 수 있다.
for i, output_neuron in enumerate(output_layer):
for j, hidden_output in enumerate(hidden_outputs + [1]):
output_neuron[j] -= output_deltas[i] * hidden_output

# 오류값을 히든 레이어로 전파(뒤로 전파). output_layer는 10행 6열, hidden_outputs는 1차원 5개짜리 (5,)
# weight에 대해서만 행렬 곱셈을 수행하고 bias에 대해서는 아무 것도 하지 않는다.
# [n[i] for n in output_layer]는 행렬을 transpose시킨다는 뜻.
# 결국 10x10 inner product 수행하고 1개의 결과 리턴.
# dot 함수는 2차원에 대해서는 행렬 곱셈, 1차원에 대해서는 inner produce 수행.
hidden_deltas = [hidden_output * (1 - hidden_output) *
LinAlg.dot(output_deltas, [n[i] for n in output_layer])
for i, hidden_output in enumerate(hidden_outputs)]

# 히든 레이어의 뉴런에 대해 weight 조정. 출력 레이어 조정과 완전히 같은 코드.
for i, hidden_neuron in enumerate(hidden_layer):
for j, input in enumerate(input_vector + [1]):
hidden_neuron[j] -= hidden_deltas[i] * input

def train(network, inputs, target, loops):
# 사용하지 않는 코드. weights가 바뀌는지 확인하는 용도로 사용
hidden_layer, output_layer = network # (5, 26), (10, 6)
for _ in range(loops):
for input_vector, target_vector in zip(inputs, target):
back_propagate(network, input_vector, target_vector)
# print(hidden_layer[0][:2], output_layer[0][:2]) # 반복할 때마다 달라지는 값 표시

def show_result(network, shape):
def predict(network, input):
# _, outputs = feed_forward(network, input)
# return outputs
# -1만 사용한다는 것은 두 개 중에서 처음 것은 placeholder로 처리한다는 뜻.
return feed_forward(network, input)[-1]

for i, v in enumerate(shape):
print('@' if v else ' ', end='')
if i%5 == 4:
print()

# 10개의 숫자 중에서 가장 큰 숫자의 인덱스 출력
result = [round(x, 3) for x in predict(network, shape)]
pos = result.index(max(result))
print('predict {}, {:.1f}% : {}'.format(pos, 100*result[pos], result))
print()


inputs = [[1, 1, 1, 1, 1,
1, 0, 0, 0, 1,
1, 0, 0, 0, 1,
1, 0, 0, 0, 1,
1, 1, 1, 1, 1],
[0, 0, 1, 0, 0,
0, 0, 1, 0, 0,
0, 0, 1, 0, 0,
0, 0, 1, 0, 0,
0, 0, 1, 0, 0],
[1, 1, 1, 1, 1,
0, 0, 0, 0, 1,
1, 1, 1, 1, 1,
1, 0, 0, 0, 0,
1, 1, 1, 1, 1],
[1, 1, 1, 1, 1,
0, 0, 0, 0, 1,
1, 1, 1, 1, 1,
0, 0, 0, 0, 1,
1, 1, 1, 1, 1],
[1, 0, 0, 0, 1,
1, 0, 0, 0, 1,
1, 1, 1, 1, 1,
0, 0, 0, 0, 1,
0, 0, 0, 0, 1],
[1, 1, 1, 1, 1,
1, 0, 0, 0, 0,
1, 1, 1, 1, 1,
0, 0, 0, 0, 1,
1, 1, 1, 1, 1],
[1, 1, 1, 1, 1,
1, 0, 0, 0, 0,
1, 1, 1, 1, 1,
1, 0, 0, 0, 1,
1, 1, 1, 1, 1],
[1, 1, 1, 1, 1,
0, 0, 0, 0, 1,
0, 0, 0, 0, 1,
0, 0, 0, 0, 1,
0, 0, 0, 0, 1],
[1, 1, 1, 1, 1,
1, 0, 0, 0, 1,
1, 1, 1, 1, 1,
1, 0, 0, 0, 1,
1, 1, 1, 1, 1],
[1, 1, 1, 1, 1,
1, 0, 0, 0, 1,
1, 1, 1, 1, 1,
0, 0, 0, 0, 1,
1, 1, 1, 1, 1]]


# [[1, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 1, 0, 0, 0, 0, 0, 0, 0, 0],
# [0, 0, 1, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 1, 0, 0, 0, 0, 0, 0],
# [0, 0, 0, 0, 1, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 1, 0, 0, 0, 0],
# [0, 0, 0, 0, 0, 0, 1, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 1, 0, 0],
# [0, 0, 0, 0, 0, 0, 0, 0, 1, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 1]]
target = [[1 if i == j else 0 for i in range(10)] for j in range(10)]

random.seed(0) # seed 고정

input_size = 25 # 그림 크기 5x5
num_hidden = 5 # 히든 레이어 뉴런의 갯수
output_size = 10 # 0~9까지 10개 숫자

# +1은 bias. 초기값은 난수로 설정
# hidden_layer : 5x26, 25개의 입력, 5개의 출력
# output_layer : 10x6, 5개의 입력, 10개의 출력
hidden_layer = [[random.random() for _ in range(input_size + 1)] for _ in range(num_hidden)]
output_layer = [[random.random() for _ in range(num_hidden + 1)] for _ in range(output_size)]

network = [hidden_layer, output_layer]

# 충분히 수렴할 만큼 반복 횟수 지정.
# 10회는 모든 weight이 같고, 100회는 아주 조금 달라지고 1000회는 많이 달라진다.
train(network, inputs, target, 10)

# 입력으로 사용했던 10개의 숫자에 대해 정확하게 동작하는지 확인하는 코드.
# for input in inputs:
# show_result(network, input)

shape_1 = [0, 1, 1, 1, 0, # .@@@.
0, 0, 0, 1, 1, # ...@@
0, 0, 1, 1, 0, # ..@@.
0, 0, 0, 1, 1, # ...@@
0, 1, 1, 1, 0] # .@@@.
shape_2 = [0, 1, 1, 1, 0, # .@@@.
1, 0, 0, 1, 1, # @..@@
0, 1, 1, 1, 0, # .@@@.
1, 0, 0, 1, 1, # @..@@
0, 1, 1, 1, 0] # .@@@.

# 입력에 포함되지 않았던 데이터를 생성해서 예측하기.
show_result(network, shape_1) # 3 예측
show_result(network, shape_2) # 9 예측. 정답은 8인데 잘못 예측.

# [출력 결과]
# train 함수 10,000번 반복했을 때의 show_result 호출 결과.
# @@@
# @@
# @@
# @@
# @@@
# predict 3, 93.4% : [0.0, 0.002, 0.0, 0.934, 0.0, 0.0, 0.0, 0.007, 0.0, 0.1]
#
# @@@
# @ @@
# @@@
# @ @@
# @@@
# predict 9, 99.6% : [0.0, 0.0, 0.0, 0.0, 0.0, 0.536, 0.0, 0.0, 0.915, 0.996]

# train 함수 100,000번 반복했을 때의 show_result 호출 결과.
# 여전히 9를 예측하고 있고, 오히려 확률이 높아졌다.
# predict 3, 98.8% : [0.0, 0.003, 0.0, 0.988, 0.0, 0.0, 0.0, 0.006, 0.0, 0.025]
# predict 9, 99.9% : [0.0, 0.0, 0.0, 0.0, 0.0, 0.675, 0.0, 0.0, 0.941, 0.999]


아래 코드는 앞에서 학습한 결과를 그래프로 표시한다.
히든 레이어의 뉴런에 들어있는 weights가 어떻게 바뀌었는지 상세하게 표시하고,
최종적으로 모든 뉴런을 같은 그래프에 출력해서 비교할 수 있도록 제시한다.
이 코드는 위에 나온 코드와 같은 파일에 있어야 동작한다.

import matplotlib
import matplotlib.pyplot as plt
def patch(x, y, hatch, color):
# hatch : 빗금 사용
# color : 빗금 색상
# fill : 전체를 채울 것인지. True라면 빗금 사용할 수 없다.
return matplotlib.patches.Rectangle((x - 0.5, y - 0.5), 1, 1,
hatch=hatch, color=color, fill=False)

def show_weights_detail(weights):
''' 히든레이어 뉴런 하나를 자세하게 출력 '''
abs_weights = list(map(abs, weights))

# 25개로 구성된 1차원 리스트를 5x5 리스트로 변환
grid = [abs_weights[row:row+5] for row in range(0,25,5)]

# 그래프 6개 표시. 왼쪽 2개는 원본(빨강, 파랑) 이미지에 대해 적용. 가운데 2개는 회색 계열 colormap 적용.
# 오른쪽 2개는 interpolation(보간) 제거 및 음수 빗금 표시
plt.subplot(231)
plt.imshow(grid) # None : rc image.interpolation
plt.subplot(234)
plt.imshow(grid, interpolation='none')

plt.subplot(232)
plt.imshow(grid, cmap=matplotlib.cm.binary)
plt.subplot(235)
plt.imshow(grid, cmap='gray')
plt.subplot(233)
plt.imshow(grid, cmap=matplotlib.cm.binary, interpolation='none')

# interpolation 옵션
# Acceptable values are
# ‘none’, ‘nearest’, ‘bilinear’, ‘bicubic’, ‘spline16’, ‘spline36’, ‘hanning’, ‘hamming’,
# ‘hermite’, ‘kaiser’, ‘quadric’, ‘catrom’, ‘gaussian’, ‘bessel’, ‘mitchell’, ‘sinc’, ‘lanczos’
#
# If interpolation is None, default to rc image.interpolation.
# See also the filternorm and filterrad parameters.
# If interpolation is ‘none’, then no interpolation is performed on the Agg,
# ps and pdf backends.Other backends will fall back to ‘nearest’.

# 음수 weights에 대해 빗금 표시
plt.subplot(236)
plt.imshow(grid, cmap=matplotlib.cm.binary, interpolation='none')
ax = plt.gca()

for i in range(5):
for j in range(5):
# 음수에 대해서만 빗금 표시
if weights[5*i + j] < 0:
ax.add_patch(patch(j, i, '/', 'red'))
ax.add_patch(patch(j, i, '\\', 'red'))
plt.figure(1).suptitle('show_weights_detail', fontsize=24)
plt.show()

def show_weights_all(hidden_layer):
''' 히든레이어 뉴런 전체 출력 '''
for i, weights in enumerate(hidden_layer):
plt.subplot(1, len(hidden_layer), i+1)
abs_weights = list(map(abs, weights))

# 25개로 구성된 1차원 리스트를 5x5 리스트로 변환
grid = [abs_weights[row:row+5] for row in range(0,25,5)]

ax = plt.gca()
plt.imshow(grid, cmap=matplotlib.cm.binary, interpolation='none')

for i in range(5):
for j in range(5):
if weights[5*i + j] < 0:
ax.add_patch(patch(j, i, '/', 'red'))
ax.add_patch(patch(j, i, '\\', 'red'))
plt.figure(1).suptitle('show_weights_all', fontsize=24)
plt.show()


# 히든레이어는 5x26이기 때문에 5개까지만 출력 가능.
show_weights_detail(hidden_layer[0])
# show_weights_detail(hidden_layer[1])

show_weights_all(hidden_layer)


아래는 show_weights_detail 함수를 히든 레이어의 0번 뉴런에 대해 호출한 결과이다.
weight에는 음수와 양수가 있는데, 이 값을 색상으로 표현하고자 하는게, 이번 코드의 목적이다.
각각의 weight은 음수와 양수 범위에 대해 매우 다양하게 나타나는데,
이 값은 어떻게 해석되어야 맞는 것일까?

처음 코드에 있는 neuron_output 함수에서 답을 찾을 수 있다.
이 함수는 sigmoid에 곱해서 더한 결과(inner product 또는 행렬 곱셈)를 sigmoid 함수에 전달하는데,
sigmoid 함수의 결과는 -1에서 1 사이의 값이다.
0보다 크면 활성화되고, 0보다 작으면 비활성화되는 activation 함수 중의 하나다.
그래서, 활성화시키는 값은 좋게, 비활성화시키는 값은 나쁘게 평가할 수 있다.
즉, 양수는 좋은 쪽으로, 음수는 나쁜 쪽으로 해석하면 된다.

RGB로 표현된 그래프에서는
파랑이 절댓값이 작은 경우이고, 나머지 색상은 절댓값이 큰 경우이다.
이 값을 흑백으로 변경해서 음수에는 빗금을 표시했다.
빗금이 그려진 그림을 보면, 0번 뉴런에 대한 이해를 조금 높일 수 있다.
완전하진 않지만, weights가 어떤 숫자들로 배치되는지 이해할 수 있게 된다.
가령, 음수는 0, 2, 4번째 수직 줄에서 많이 나타난다.
양수는 0번째 수직 줄에서 큰 값을 갖고, 음수는 4번째 수직 줄에서 조금 큰 값을 갖는다.

일반적으로 weight가 0에 가까울수록 흰색이고,
양수이면서 절댓값이 크면 녹색, 음수이면서 절댓값이 크면 빨강에 가깝게 된다.
이것을 흑백으로 변경하면, 흰색은 바뀌지 않지만,
절댓값이 커질수록 짙은 색으로 표시하게 된다.
이때 양수와 음수를 구분할 수 없기 때문에 여기서는 빗금을 사용해서 처리했다.

그렇지만 딥러닝에서 히든 레이어에 대해 예측할 수 없는 문제가 있다고 말하는 것처럼
논리적으로 해석할 수 없는 경우가 더 많을 수도 있다.
사실 0번 뉴런도 해석했다고 보기는 어려운게 사실이다.
답이 나오는 것은 알지만,
어떤 과정을 거쳐서 나오는지 확신하지 못하는 것은 딥러닝의 단점 중에 하나일 수밖에 없다.
답답하니까.


아래는 show_weights_all 함수를 호출한 결과이다.
히든 레이어의 5개 뉴런에 대해 모두 그래프로 표시하고 있다.
앞에서 장황하게 설명했던 것처럼
왜 음수가 나오는지, 왜 양수가 나오는지 잘 모른다.
1번과 3번 뉴런은 놀랍게도 음수값이 하나도 없지만 왜 그런지는 알 수 없다.
우리가 아는 것은
좋은 데이터로 많은 학습을 시키면 답이 될 확률이 높다는 것뿐이다.

딥러닝에서 히든 레이어 동작 원리

머신러닝을 하면서 히든 레이어를 구현하는 방식이 너무 궁금했다.
여러 레이어들간의 입력과 출력 갯수에 규칙이 있다는 것을 당연히 생각하긴 했지만
그것이 어떤 원리로 동작하는지는 알 수 없었다.

이번 예제에서는 히든 레이어를 포함한 멀티 레이어가 동작하는 방식에 대해 살펴본다.
"밑바닥부터 시작하는 데이터 과학"을 참고로 했고
내가 이해할 수 있도록 완전 기초부터 구현하는 과정에서 원본 코드를 많이 수정, 추가했다.

코드에 앞서 이해해야 할 개념으로 뉴런(neuron)이 있다.
신경계를 구성하는 기본 단위의 세포를 말하는데, 코드에서는 레이어를 구성하는 기본 단위가 된다.
히든 레이어는 여러 개의 뉴런을 가질 수 있고, 갯수에는 제한이 없다.
뉴런은 weights와 bias로 구성되어 있고,
weights는 이전 레이어의 뉴런 갯수와 동일하고, 여기에 bias가 하나 추가된다.
첫 번째 히든 레이어의 뉴런은 입력 레이어의 갯수만큼 weights를 갖게 되고
마지막 히든 레이어의 뉴런 갯수는 출력 레이어의 갯수와 같아야 한다.

코드는 basic, multi, final의 세 가지 함수로 구성했다.
원본 코드는 멀티 레이어를 구성해서 xor 연산을 수행할 수 있다는 것을 보여주는데
여기서는 basic 함수에서만 xor 연산을 보여주고,
multi와 final에서는 멀티 레이어의 원리를 보여준다.
특히, 마지막의 final에서는 히든 레이어를 자동으로 생성할 수 있다는 것을 보여준다.
그래서, 입력과 출력 레이어만 정의되면 모든 단계를 자동으로 처리할 수 있게 된다.

아래는 첫 번째 코드로, 멀티 레이어를 사용해서 xor 연산을 처리할 수 있다는 것을 보여준다.
주석을 자세히 달았으므로 설명은 생략한다.
참, show_shape 함수는 리스트 크기를 확인하기 위한 임시 함수이다.

import numpy as np
import math

def show_shape(a):
return np.array(a).shape

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

def neuron_output(weights, inputs):
''' np.dot 함수는 1차원에 대해서는 내적(inner), 2차원에 대해서는 행렬 곱셈 수행.
어떤 곱셈이 수행되건 결과로 숫자 1개 반환. '''
# print(show_shape(weights), show_shape(inputs))
return sigmoid(np.dot(weights, inputs))

def feed_forward(neural_network, input_vector):
''' feed backward에 반대되는 말로 앞쪽부터 순서대로 진행하는 것을 의미한다.
이곳에 작성된 모든 layer 함수에서 호출된다. 코드 전체에서 가장 중요한 함수.
neural_network에는 히든과 출력 레이어가 들어 있다. '''
outputs = []
for layer in neural_network:
# 이전 레이어의 입력에 bias 추가. bias는 항상 1.
input_with_bias = input_vector + [1]

# comprehension. 레이어에 포함된 모든 뉴런에 대해 결과 계산.
# 뉴런 하나당 결과가 한 개이므로 output 크기는 뉴런 갯수와 동일하다.
# 뉴런에는 weights와 bias가 들어 있고, 크기는 입력에 해당하는 input_with_bias와 같아야 한다.
# neuron과 input_with_bias는 모두 1차원 리스트.
output = [neuron_output(neuron, input_with_bias) for neuron in layer]

# output은 계산 결과를 의미하고, 모든 결과를 outputs에 누적.
outputs.append(output)

# 이전 레이어의 출력은 다음 레이어의 입력이 된다.
input_vector = output

# 결과를 누적시켰기 때문에 출력 레이어에 대한 결과는 마지막에 있다.
return outputs

def layer_basic():
''' xor 연산 : and 연산에는 해당하지 않지만, or 연산에는 해당하는 결과.
hidden 레이어는 첫 번째 입력이 아닌 두 번째 입력에 해당한다.
첫 번째 입력을 받아서 새로운 결과를 만든다. '''
hidden = [[20, 20, -30], # and 뉴런. x1(20), x2(20), bias(-3).
[20, 20, -10]] # or 뉴런
output = [[-60, 60, -30]] # output은 hidden 레이어의 출력을 입력으로 받는다.
xor_network = [hidden, output] # output은 hidden 요소가 2개이므로 bias까지 3개 요소

# feed_forward 호출은 각각의 데이터에 대해 검증하기 때문에 4번 호출되어야 한다.
for x in ([0, 0], [0, 1], [1, 0], [1, 1]):
# logit에는 히든 레이어에 대한 결과까지 모두 포함되어 있다.
# [-1]을 사용한 이유는 마지막에 최종 결과가 들어있기 때문이다.
logit = feed_forward(xor_network, x)[-1]
print(x, logit)

# 출력 결과를 0과 1로 제한해야 하기 때문에 0.5를 기준으로 판단해야 한다.
# hidden과 output 리스트에 있는 데이터 크기에 따라 결과가 달라진다.
#
# 출력 결과 - hidden과 output에 10을 곱한 경우. 성공(0, 1, 1, 0)
# [0, 0] [9.38314668300676e-14]
# [0, 1] [0.9999999999999059]
# [1, 0] [0.9999999999999059]
# [1, 1] [9.383146683006828e-14]

# 출력 결과 - hidden과 output에 10을 곱하지 않은 경우. 실패(0, 0, 0, 0)
# [0, 0] [0.15830332818949178]
# [0, 1] [0.4434191256899033]
# [1, 0] [0.4434191256899033]
# [1, 1] [0.1583033281894919]

layer_basic()


아래는 두 번째 코드로, 멀티 레이어의 구성 원리를 보여준다.
주석에도 달았는데, 리스트에 포함된 값은 쓰레기다.
레이어들간의 입력과 출력이 동작하는 원리를 이해하는 것이 중요하다.

def layer_multi():
'''
문제
5, 4, 3, 2로 이어지는 히든 레이어를 구성해 보세요.
히든 레이어 다음에는 결과를 보여주는 출력 레이어가 와야 합니다.
결과에 상관없이 죽지 않고 동작하면 제대로 된 구성으로 간주합니다.

hidden5, hidden4, hidden3, hidden2, output은 레이어가 되고
레이어에 포함된 요소들은 뉴런이 된다. 뉴런 안에는 weights가 들어 있다. bias는 따로 추가된다.
뉴런에 포함된 weights의 값들은 전혀 의미가 없고, 뉴런과 weights 갯수만 중요하다.
죽지 않고 동작했다는 것은 레이어들간의 입력과 출력이 정확하게 맞았다는 것을 의미한다.

[hidden5]
feed_forward에 전달된 x와 연산하기 때문에 x와 데이터의 갯수가 같아야 한다. (x1, x2, bias 순서)
요소 갯수는 4개짜리 레이어와 연결되기 때문에 bias를 제외하면 데이터는 4개.

[hidden4]
hidden5를 계산한 결과에 bias 하나가 추가된 결과와 계산하기 때문에 데이터 갯수는 5개. (x1, x2, x3, x4, bias)
요소 갯수는 3개짜리 레이어와 연결되기 때문에 bias를 재외하면 데이터는 3개.

레이어에서 뉴런(요소) 갯수는 다음 레이어의 가중치(데이터) 갯수를 결정한다.
결국 이전 레이어의 요소 갯수와 현재 레이어의 가중치 갯수가 같아야 한다는 뜻이 된다.
'''
hidden5 = [[20, 20, -30], [20, 20, -10], [20, 20, -10], [20, 20, -10]]
hidden4 = [[20, 20, 20, 20, -30], [20, 20, 20, 20, -10], [20, 20, 20, 20, -10]]
hidden3 = [[20, 20, 20, -30], [20, 20, 20, -10]]
hidden2 = [[20, 20, -30], [20, 20, -30]]
output = [[-60, 60, -30]]
xor_network = [hidden5, hidden4, hidden3, hidden2, output]

for x in ([0, 0], [0, 1], [1, 0], [1, 1]):
print(x, feed_forward(xor_network, x)[-1])

# 죽지 않고 동작하지만 출력 결과는 쓰레기이므로 코드에 표시하지 않는다.

layer_multi()


마지막으로 히든 레이어를 갯수만 지정하면 자동으로 생성할 수 있다는 것을 보여준다.
딥러닝에서 히든 레이어를 직접 만들어서 전달하는 것이 아니기 때문에 항상 궁금했던 부분이다.
역시 리스트에 포함된 값과 결과는 쓰레기다.

ef layer_final(hidden_sizes, inputs):
'''
hidden_sizes의 크기는 히든 레이어의 갯수를 뜻하고,
포함된 값은 히든 레이어에 들어가는 뉴런의 갯수를 뜻한다.
마지막 요소가 1이라는 것은 입력에 대한 결과를 1개만 만든다는 것을 의미하고,
0.5를 기준으로 0 또는 1로 변환해야 함을 의미한다.
'''
def make_hidden_layer(size_out, size_in):
''' size_out : 바깥 리스트 크기, size_in : 안쪽 리스트 크기
5와 3이 전달됐다면, 뉴런은 5개, 뉴런에 포함된 weights는 3개라는 뜻이 된다.
리스트에 대한 * 연산은 부작용이 있기 때문에 copy 함수로 깊은 복사 수행. '''
# 실제 코드에서는 weights에 들어가는 값이기 때문에 1 대신 의미 있는 난수로 초기화.
return ([[1] * size_in] * size_out).copy()

xor_network = []
size = len(inputs[0])+1

# 풀어쓴 코드. 아래 나오는 반복문으로 수정.
# (5, 3) : 뉴런 5개, 가중치 3개
# layer = make_hidden_layer(hidden_sizes[0], size)
# xor_network.append(layer)
# size = len(layer)+1 # +1은 bias.
#
# (3, 6) : 뉴런 3개, 가중치 6개
# layer = make_hidden_layer(hidden_sizes[1], size)
# xor_network.append(layer)
# size = len(layer)+1
#
# (1, 4) : 뉴런 1개, 가중치 4개
# layer = make_hidden_layer(hidden_sizes[2], size)
# xor_network.append(layer)
# size = len(layer)+1 # 반복 규칙을 위해.

# 히든 레이어 구성. 마지막 레이어는 출력 레이어로 사용.
for i in hidden_sizes:
layer = make_hidden_layer(i, size)
# print(show_shape(layer)) # (5, 3), (3, 6), (1, 4)

xor_network.append(layer)
size = len(xor_network[-1])+1

# 결과 예측. 의미 없는 weights를 사용했기 때문에 결과 또한 쓰레기.
for x in inputs:
print(x, feed_forward(xor_network, x)[-1])


inputs = ([0, 0], [0, 1], [1, 0], [1, 1])
layer_final([5, 3, 1], inputs)

퍼셉트론을 사용한 and, or, xor 데이터 분류

딥러닝 역사에서 빼놓을 수 없는 첫 번째 겨울을 보내게 만든 XOR 문제.
지금 보면 어이없을 정도로 간단한 문제로 인해 10년 이상의 겨울을 보내야 했다.

아래 코드는 신경세포 하나를 의미하는 퍼셉트론(perceptron)을 사용해서
and, or, xor 문제를 해결하고 있다.
weights와 bias에 따른 결과를 보여주는 것은 너무 당연한데
놀랍게도 함수를 사용할 때 더 좋은 결과가 나올 수 있다는 사실에 놀랐다.
함수를 전달한다는 생각은 전혀 하지 못했다.

*퍼셉트론
n개의 이진수(binary)가 하나의 뉴런을 통과해서
가중합이 0보다 크면 활성화되는 가장 단순한 형태의 신경망 구조.


이번 내용은 "밑바닥부터 시작하는 데이터 과학"에 나오는 18장의 신경망을 참고했다.
사실 참고했다기 보다는 코드를 조금 내맘대로 수정했을 뿐이다.

밑바닥부터 시작하는 데이터 과학
국내도서
저자 : 조엘 그루스 / 하성주,박은정,김한결역
출판 : 인사이트 2016.06.03
상세보기


import matplotlib.pyplot as plt

def step_function(x):
''' 입력값이 양수와 0인 경우에 1, 음수라면 0 반환 '''
return 1 if x >= 0 else 0

def perceptron_output(weights, bias, x):
''' weights와 x는 요소 2개를 갖고 있는 리스트.
단순히 wx+b를 step_function에 전달한 결과 반환.
단순 곱셈과 덧셈이기 때문에 weights와 bias에 따라 결과가 달라진다는 것을 쉽게 알 수 있다. '''
return step_function(weights[0]*x[0] + weights[1]*x[1] + bias)

def logit_perceptron(weights, bias):
''' 데이터가 달라지면 결과도 달라진다는 것을 보여준다.
미리 정의된 4가지 데이터에 대해 검증. 활성화되었다면 동그라미, 비활성이라면 x 출력. '''
for x in ([0, 0], [0, 1], [1, 0], [1, 1]):
logit = 'ro' if perceptron_output(weights, bias, x) else 'rx'
plt.plot(x[0], x[1], logit, markersize=10)

# 위와 같은 코드.
# for x0 in [0, 1]:
# for x1 in [0, 1]:
# logit = 'ro' if perceptron_output(weights, bias, [x0, x1]) else 'rx'
# plt.plot(x0, x1, logit, markersize=10)

def logit_perceptron_by_func(func):
''' 전달된 코드(함수)에 따라 결과가 달라질 수 있다는 것을 보여준다.
함수 버전에서는 weights와 bias를 사용하지 않는다. '''
for x in ([0, 0], [0, 1], [1, 0], [1, 1]):
logit = 'ro' if func(x) else 'rx'
plt.plot(x[0], x[1], logit, markersize=10)

def set_common(title, plot_index):
plt.subplot(23*10 + plot_index) # 5 --> 235
plt.title(title)
plt.xlim(-0.5, 1.5) # 범위 설정이 없으면, 출력이 경계에 달라 붙는다.
plt.ylim(-0.5, 1.5)


# and, or은 가능. xor은 불가능.
set_common('and : data', 1)
logit_perceptron([2, 2], -3)
set_common('or : data', 2)
logit_perceptron([2, 2], -1)

# and, or, xor 모두 가능.
set_common('and : func', 4)
logit_perceptron_by_func(max)
set_common('or : func', 5)
logit_perceptron_by_func(min)
set_common('xor : func', 6)
logit_perceptron_by_func(lambda x: 0 if x[0] == x[1] else 1)

plt.show()

'딥러닝' 카테고리의 다른 글

CNN 흉내내기  (0) 2017.04.06
딥러닝에서 히든 레이어 동작 원리  (0) 2017.04.06
mnist 난수 종류에 따른 테스트  (0) 2017.02.28
mnist 레이어 크기에 따른 테스트  (0) 2017.02.27
mnist 레이어 갯수 테스트  (0) 2017.02.27

mnist 난수 종류에 따른 테스트

텐서플로우에서 제공하는 난수에 주로 등장하는 것으로 3가지가 있습니다.

  random_normal, random_uniform, truncated_normal.

난수에 따른 성능 차이가 있는지 궁금해서 테스트 했습니다.
이들 함수에 대한 정리는 [텐서플로우 정리] 08. 초기값과 난수에서 확인할 수 있습니다.

여러 번 검사하기 전에
전체 데이터에 대해 15회 반복하면서 cost가 어떻게 떨어지는지 비교했습니다.
random_normal과 truncated_normal 함수는 잘 동작하지만, random_uniform 함수는 올바르게 동작하지 않습니다.
random_uniform 함수가 만들어 내는 난수는 전체 구간에 대해 균일하기 때문에 발생하는 현상일텐데..
처음에 24404라는 값에서 2.30으로 떨어지고 나서 계속 그 값을 유지합니다.
이것이 더 이상 조절할 수 없을 만큼으로 w가 변경되었다라고 보기는 어려운데..
결과만 놓고 보면 조절할 수 없는 것이 맞기는 합니다.
이 부분은 나중에 좀더 정리가 필요한 부분입니다. 어찌 됐든 random_uniform 함수는 적절하지 않습니다.


# random_normal
# 1 : 60.478189058737286
# 2 : 11.734649042432965
# 3 : 7.067460532757375
# 4 : 4.989459909444504
# 5 : 3.778993105515836
# 6 : 2.991683001535285
# 7 : 2.480893753819504
# 8 : 2.027405534006062
# 9 : 1.7499642808224738
# 10 : 1.5208392191279025
# 11 : 1.333428899258666
# 12 : 1.1645081179615477
# 13 : 1.0243976788740985
# 14 : 0.9156795300565903
# 15 : 0.8343978138050405
# accuracy : 0.9163

# random_uniform
# 1 : 24404.246204987452
# 2 : 2.3088482627001676
# 3 : 2.303867313645103
# 4 : 2.3021448772603823
# 5 : 2.3015500276738954
# 6 : 2.301343701102516
# 7 : 2.3012730004570745
# 8 : 2.301249224922875
# 9 : 2.3012397917834178
# 10 : 2.3012363381819294
# 11 : 2.301236299167981
# 12 : 2.3012347472797723
# 13 : 2.3012353801727308
# 14 : 2.301232471899553
# 15 : 2.3012359909577773
# accuracy : 0.1135

# truncated_normal
# 1 : 42.671754805716574
# 2 : 9.975034481178634
# 3 : 6.153620355020877
# 4 : 4.410890528491953
# 5 : 3.343142766199837
# 6 : 2.629227198974904
# 7 : 2.118781123255989
# 8 : 1.751021036118762
# 9 : 1.4441868320022127
# 10 : 1.1963457991950714
# 11 : 1.0300166532051782
# 12 : 0.8463236458417003
# 13 : 0.7411801967909226
# 14 : 0.6237198447949415
# 15 : 0.5263266100296041
# accuracy : 0.9344


3가지 함수에 대해 동일한 코드를 사용했고, w와 b를 생성할 때 해당 함수를 사용하는 것만 다릅니다. 3회 반복하면서 결과를 출력합니다.

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

mnist = input_data.read_data_sets('mnist', one_hot=True)

learning_rate = 0.01
epoches = 15
batch_size = 100

x = tf.placeholder(tf.float32)
y = tf.placeholder(tf.float32)

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

# random_normal
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]))

# random_uniform
# w1 = tf.Variable(tf.random_uniform([784, 256]))
# w2 = tf.Variable(tf.random_uniform([256, 256]))
# w3 = tf.Variable(tf.random_uniform([256, 10]))
#
# b1 = tf.Variable(tf.random_uniform([256]))
# b2 = tf.Variable(tf.random_uniform([256]))
# b3 = tf.Variable(tf.random_uniform([ 10]))

# truncated_normal
# w1 = tf.Variable(tf.truncated_normal([784, 256]))
# w2 = tf.Variable(tf.truncated_normal([256, 256]))
# w3 = tf.Variable(tf.truncated_normal([256, 10]))
#
# b1 = tf.Variable(tf.truncated_normal([256]))
# b2 = tf.Variable(tf.truncated_normal([256]))
# b3 = tf.Variable(tf.truncated_normal([ 10]))

a1 = tf.add(tf.matmul(x, w1), b1)
r1 = tf.nn.relu(a1)
a2 = tf.add(tf.matmul(r1, w2), b2)
r2 = tf.nn.relu(a2)
a3 = tf.add(tf.matmul(r2, w3), b3)

activation = a3
cost = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=activation, labels=y))

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

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

sess = tf.Session()

for _ in range(3):
sess.run(tf.global_variables_initializer())

for epoch in range(epoches):
avg_cost = 0
total_batch = mnist.train.num_examples // batch_size # 55,000 // 100 = 550

for i in range(total_batch):
batch_xs, batch_ys = mnist.train.next_batch(batch_size)

_, c = sess.run([optimizer, cost],
feed_dict={x: batch_xs, y: batch_ys})

avg_cost += c / total_batch

print('{:2} : {}'.format(epoch+1, avg_cost))

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

pred = tf.equal(tf.argmax(activation, 1), tf.argmax(y, 1))
accuracy = tf.reduce_mean(tf.cast(pred, tf.float32))

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

sess.close()


마지막 15번째 반복과 정확도만 출력했습니다. random_uniform 함수를 사용할 수 없는 것은 여전히 당연한 사실이고, random_normal 보다는 truncated_normal 함수가 좀더 나은 성능을 보여줍니다. 설마 했는데.. 성능 좋은 난수가 있다는 것을 알게 되서.. 기분이 좋습니다. 다만 xavier 초기화 등의 난수를 현명하게 사용하는 방법이 있기 때문에 항상 그런 것은 아닐 수도 있습니다.

# random_normal
# 15 : 0.832641489035556
# accuracy : 0.9181
# 15 : 0.8100094339383366
# accuracy : 0.9207
# 15 : 0.7013166612056974
# accuracy : 0.9274

# random_uniform
# 15 : 2.3012339136817213
# accuracy : 0.1135
# 15 : 2.3012344139272516
# accuracy : 0.1135
# 15 : 2.3012313426624655
# accuracy : 0.1135

# truncated_normal
# 15 : 0.5887952694578703
# accuracy : 0.9247
# 15 : 0.5807194867451034
# accuracy : 0.9341
# 15 : 0.6066072065450834
# accuracy : 0.9277


mnist 레이어 크기에 따른 테스트

동일한 레이어 갯수에서 레이어에 포함된 노드의 갯수가 어느 정도의 영향을 줄까?

레이어 갯수가 늘어나면 성능 향상이 되는 것처럼 최종 출력까지의 과정에서 레이어 크기에 따라서 성능이 달라질 거라고 생각했다. 결과를 보면 놀랍게도 별 차이가 없다.

그럼에도 불구하고 여전히 달라져야 한다고 생각했다. 레이어가 너무 작아서 그런 것이 아닐까? 밑에 레이어를 늘린 코드를 추가했다. 결과는 차이 없었다.

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

def layerTest(mnist, test_index, loop_count):

if test_index == 1: ww = [784, 512, 512, 10]
elif test_index == 2: ww = [784, 256, 256, 10]
else: ww = [784, 512, 256, 10]

bb = ww[1:]

x = tf.placeholder(tf.float32)
y = tf.placeholder(tf.float32)

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

# mnist 기본 예제에서 3번째를 반복문 버전으로 수정한 코드.
# 굳이 반복문 안에 없어도 되기 때문에 바깥으로 뺐다.
r = x
for i in range(len(bb)):
row, col = ww[i], bb[i]

# 함수를 여러 번 호출하는 과정에서 변수 이름 충돌남. layer_count*10+i로 해결.
w = tf.get_variable(str(test_index*10+i), shape=[row, col], initializer=tf.contrib.layers.xavier_initializer())
b = tf.Variable(tf.zeros(col))
a = tf.add(tf.matmul(r, w), b)
r = a if i == len(bb) - 1 else tf.nn.relu(a)

activation = r
cost = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=activation, labels=y))

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

sess = tf.Session()

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

epoches, batch_size = 15, 100 # 15와 35 중에서 선택
for loop in range(loop_count):
# print('layer({}), loop({}) {}'.format(layer_count, loop, '-'*30))

sess.run(tf.global_variables_initializer())

for epoch in range(epoches):
avg_cost = 0
total_batch = mnist.train.num_examples // batch_size # 55,000 // 100 = 550

for i in range(total_batch):
batch_xs, batch_ys = mnist.train.next_batch(batch_size)

_, c = sess.run([optimizer, cost], feed_dict={x: batch_xs, y: batch_ys})
avg_cost += c / total_batch

# print('{:2} : {}'.format(epoch+1, avg_cost))

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

pred = tf.equal(tf.argmax(activation, 1), tf.argmax(y, 1))
accuracy = tf.reduce_mean(tf.cast(pred, tf.float32))
acc = sess.run(accuracy, feed_dict={x: mnist.test.images, y: mnist.test.labels})

print('{} : accuracy {:.4f}, cost {:.4f}'.format(ww, acc, avg_cost))

sess.close()


mnist = input_data.read_data_sets('mnist', one_hot=True)

layerTest(mnist, test_index=1, loop_count=3)
layerTest(mnist, test_index=2, loop_count=3)
layerTest(mnist, test_index=3, loop_count=3)
# [출력 결과 : 첫 번째]
# [784, 512, 512, 10] : accuracy 0.9525, cost 0.1630
# [784, 512, 512, 10] : accuracy 0.9517, cost 0.1639
# [784, 512, 512, 10] : accuracy 0.9528, cost 0.1616
#
# [784, 256, 256, 10] : accuracy 0.9511, cost 0.1711
# [784, 256, 256, 10] : accuracy 0.9525, cost 0.1738
# [784, 256, 256, 10] : accuracy 0.9501, cost 0.1736
#
# [784, 512, 256, 10] : accuracy 0.9532, cost 0.1574
# [784, 512, 256, 10] : accuracy 0.9532, cost 0.1603
# [784, 512, 256, 10] : accuracy 0.9513, cost 0.1636

# [출력 결과 : 두 번째]
# [784, 512, 512, 10] : accuracy 0.9523, cost 0.1619
# [784, 512, 512, 10] : accuracy 0.9530, cost 0.1670
# [784, 512, 512, 10] : accuracy 0.9545, cost 0.1650
#
# [784, 256, 256, 10] : accuracy 0.9496, cost 0.1730
# [784, 256, 256, 10] : accuracy 0.9519, cost 0.1713
# [784, 256, 256, 10] : accuracy 0.9511, cost 0.1729
#
# [784, 512, 256, 10] : accuracy 0.9554, cost 0.1586
# [784, 512, 256, 10] : accuracy 0.9546, cost 0.1592
# [784, 512, 256, 10] : accuracy 0.9541, cost 0.1602

# [출력 결과 : 세 번째]
# [784, 512, 512, 10] : accuracy 0.9536, cost 0.1630
# [784, 512, 512, 10] : accuracy 0.9514, cost 0.1687
# [784, 512, 512, 10] : accuracy 0.9516, cost 0.1664
#
# [784, 256, 256, 10] : accuracy 0.9519, cost 0.1719
# [784, 256, 256, 10] : accuracy 0.9506, cost 0.1756
# [784, 256, 256, 10] : accuracy 0.9490, cost 0.1752
#
# [784, 512, 256, 10] : accuracy 0.9525, cost 0.1643
# [784, 512, 256, 10] : accuracy 0.9513, cost 0.1615
# [784, 512, 256, 10] : accuracy 0.9521, cost 0.1616


결과가 생각한 대로 나오지 않아서 다시 테스트했다. 그러나, 결과는 다르지 않았다. 레이어가 많아지면 수정할 수 있는 weight가 많아지기 때문에 성능이 올라가는 것처럼 보인다. 갯수를 규칙적으로 줄어들게 하는 것은 단순하게도 기분 문제일 수도 있어 보인다. 그래도 두 가지 방식에 있어 성능 차이가 없다면, 기분상 후자를 선택하는 것은 당연하겠다.

if   test_index == 1:   ww = [784, 512, 512, 512, 512, 10]
elif test_index == 2: ww = [784, 256, 256, 256, 256, 10]
elif test_index == 3: ww = [784, 640, 512, 256, 128, 10]
else: ww = [784, 512, 256, 128, 64, 10]

# ...

layerTest(mnist, test_index=1, loop_count=1)
layerTest(mnist, test_index=2, loop_count=1)
layerTest(mnist, test_index=3, loop_count=1)
layerTest(mnist, test_index=4, loop_count=1)
# [출력 결과 : 첫 번째]
# [784, 512, 512, 512, 512, 10] : accuracy 0.9660, cost 0.0990
# [784, 256, 256, 256, 256, 10] : accuracy 0.9652, cost 0.1123
# [784, 640, 512, 256, 128, 10] : accuracy 0.9677, cost 0.0929
# [784, 512, 256, 128, 64, 10] : accuracy 0.9646, cost 0.0969

# [출력 결과 : 두 번째]
# [784, 512, 512, 512, 512, 10] : accuracy 0.9666, cost 0.1014
# [784, 256, 256, 256, 256, 10] : accuracy 0.9630, cost 0.1100
# [784, 640, 512, 256, 128, 10] : accuracy 0.9686, cost 0.0902
# [784, 512, 256, 128, 64, 10] : accuracy 0.9672, cost 0.0910

# [출력 결과 : 세 번째]
# [784, 512, 512, 512, 512, 10] : accuracy 0.9653, cost 0.1028
# [784, 256, 256, 256, 256, 10] : accuracy 0.9609, cost 0.1136
# [784, 640, 512, 256, 128, 10] : accuracy 0.9684, cost 0.0931
# [784, 512, 256, 128, 64, 10] : accuracy 0.9658, cost 0.0970


mnist 레이어 갯수 테스트

딥러닝에서 레이어가 많아지면 성능이 올라갈까?

말할 것도 없이 올라간다고 믿고 있고, 그렇게 수업했지만.. 어느 날 생각해 보니, 검증해 본 적이 없었다.

수업 중에 사용하던 예제를 반복문 버전으로 바꿔서 검증했다. 3, 5, 7, 9번 반복한 결과 레이어가 많아질수록 조금씩 성능 향상이 일어났다. 당연히 뒤로 갈수록 조금씩 증가하게 되고, 한계점에 도달하면 오히려 감소할 수도 있을 것이다.

테스트는 15회와 35회에 대해 2회 실시했다. 출력 결과를 보면 35회 반복에서는 9개의 레이어를 사용한 마지막 결과에서는 오히려 성능 저하가 일어났다.

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

def layerTest(mnist, layer_count, loop_count):

ww = [784, 10]
if layer_count == 2: ww = [784, 256, 10]
elif layer_count == 3: ww = [784, 512, 256, 10]
elif layer_count == 4: ww = [784, 512, 256, 128, 10]
elif layer_count == 5: ww = [784, 512, 384, 256, 128, 10]
elif layer_count == 6: ww = [784, 640, 512, 384, 256, 128, 10]
elif layer_count == 7: ww = [784, 640, 512, 384, 256, 128, 64, 10]
elif layer_count == 8: ww = [784, 640, 512, 384, 256, 128, 64, 32, 10]
elif layer_count == 9: ww = [784, 640, 512, 384, 256, 128, 64, 32, 20, 10]

bb = ww[1:]

x = tf.placeholder(tf.float32)
y = tf.placeholder(tf.float32)

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

# mnist 기본 예제에서 3번째를 반복문 버전으로 수정한 코드.
# 굳이 반복문 안에 없어도 되기 때문에 바깥으로 뺐다.
r = x
for i in range(len(bb)):
row, col = ww[i], bb[i]

# 함수를 여러 번 호출하는 과정에서 변수 이름 충돌남. layer_count*10+i로 해결.
w = tf.get_variable(str(layer_count*10+i), shape=[row, col], initializer=tf.contrib.layers.xavier_initializer())
b = tf.Variable(tf.zeros(col))
a = tf.add(tf.matmul(r, w), b)
r = a if i == len(bb) - 1 else tf.nn.relu(a)

activation = r
cost = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=activation, labels=y))

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

sess = tf.Session()

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

epoches, batch_size = 15, 100 # 15와 35 중에서 선택
for loop in range(loop_count):
# print('layer({}), loop({}) {}'.format(layer_count, loop, '-'*30))

sess.run(tf.global_variables_initializer())

for epoch in range(epoches):
avg_cost = 0
total_batch = mnist.train.num_examples // batch_size # 55,000 // 100 = 550

for i in range(total_batch):
batch_xs, batch_ys = mnist.train.next_batch(batch_size)

_, c = sess.run([optimizer, cost], feed_dict={x: batch_xs, y: batch_ys})
avg_cost += c / total_batch

# print('{:2} : {}'.format(epoch+1, avg_cost))

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

pred = tf.equal(tf.argmax(activation, 1), tf.argmax(y, 1))
accuracy = tf.reduce_mean(tf.cast(pred, tf.float32))
acc = sess.run(accuracy, feed_dict={x: mnist.test.images, y: mnist.test.labels})

print('{} : accuracy {:.4f}, cost {:.4f}'.format(layer_count, acc, avg_cost))

sess.close()


mnist = input_data.read_data_sets('mnist', one_hot=True)

layerTest(mnist, layer_count=3, loop_count=3)
layerTest(mnist, layer_count=5, loop_count=3)
layerTest(mnist, layer_count=7, loop_count=3)
layerTest(mnist, layer_count=9, loop_count=3)
# [출력 결과 : epoches - 15]
# 3 : accuracy 0.9557, cost 0.1588
# 3 : accuracy 0.9523, cost 0.1616
# 3 : accuracy 0.9542, cost 0.1601
#
# 5 : accuracy 0.9667, cost 0.0968
# 5 : accuracy 0.9662, cost 0.0991
# 5 : accuracy 0.9656, cost 0.0982
#
# 7 : accuracy 0.9710, cost 0.0624
# 7 : accuracy 0.9715, cost 0.0632
# 7 : accuracy 0.9667, cost 0.0650
#
# 9 : accuracy 0.9702, cost 0.0404
# 9 : accuracy 0.9722, cost 0.0643
# 9 : accuracy 0.9724, cost 0.0409
# [출력 결과 : epoches - 35]
# 3 : accuracy 0.9702, cost 0.0821
# 3 : accuracy 0.9700, cost 0.0835
# 3 : accuracy 0.9710, cost 0.0826
#
# 5 : accuracy 0.9758, cost 0.0300
# 5 : accuracy 0.9773, cost 0.0290
# 5 : accuracy 0.9749, cost 0.0287
#
# 7 : accuracy 0.9673, cost 0.0635
# 7 : accuracy 0.9781, cost 0.0065
# 7 : accuracy 0.9787, cost 0.0079
#
# 9 : accuracy 0.9772, cost 0.0020
# 9 : accuracy 0.9745, cost 0.0023
# 9 : accuracy 0.9743, cost 0.0014