6. 텐서플로 라이트 : 안드로이드 + mnist 모델(케라스) (1)

딥러닝 모델을 스마트폰과 연동하고 있는데
왜 이리 할게 많은 것일까?
기본 단계에서는 사진까지만 처리할 수 있으면 되는데..

케라스 모델을 텐서플로 라이트 버전으로 수정하는 것은 또 달랐다.
입력과 출력을 지정하지 않는 것은 좋은데
스마트폰에서는 어떻게 해야 하는 것일까?

사진을 다루기 때문에 코드가 꽤 길다.
파이썬으로 모델을 만들고
해당 모델을 안드로이드에서 가져다 사용하는 방식으로 진행한다.

ㅎㅎ
이전 글에서 새로 바뀐 편집기가 코드 색상을 지원하지 않아서 엄청 욕했다.
확인해 보니 예전 편집기를 사용할 수 있는 방법이 있었다.
불편하긴 하지만, 코드를 여러 색상으로 보여주는 건 타협할 수 없다.

먼저 mnist 데이터셋을 소프트맥스 알고리즘으로 분류하는 모델을 저장한다.
케라스를 사용하기 때문에 코드가 참 짧고 보기 좋다.
에포크는 15번 반복한다. 정확도는 92%를 조금 넘고, 파일 이미지에 대해서는 30장 기준으로 98%를 넘는다.

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

np.set_printoptions(linewidth=1000)


def save_model(h5_path, model_path):
model = tf.keras.Sequential()
model.add(tf.keras.layers.Dense(10, activation='softmax', input_shape=[784]))

model.compile(optimizer=tf.keras.optimizers.Adam(lr=0.001),
loss=tf.keras.losses.sparse_categorical_crossentropy,
metrics=['acc'])

mnist = input_data.read_data_sets('mnist')
model.fit(mnist.train.images, mnist.train.labels,
validation_data=[mnist.validation.images, mnist.validation.labels],
epochs=15, batch_size=128, verbose=0)

# 케라스 모델과 변수 모두 저장
model.save(h5_path)

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

# 저장한 파일로부터 모델 변환 후 다시 저장
converter = tf.lite.TFLiteConverter.from_keras_model_file(h5_path)
flat_data = converter.convert()

with open(model_path, 'wb') as f:
f.write(flat_data)


save_model('./model/mnist.h5', './model/mnist.tflite')


이전에 만든 글에서 mnist 데이터셋을 파일로 저장하는 내용을 다뤘다.

mnist 숫자를 파일로 저장

왜 직접 손으로 써서 카메라로 촬영하면 안 되는 것인지에 대해 설명했다.
그렇다면 저장한 파일을 읽어와야 해당 숫자에 대해 예측할 수 있다는 뜻이다.
특정 폴더에 포함된 모든 파일을 읽어오는 함수를 만들었다.
주의할 점은 이 폴더의 내용은 반드시 mnist 숫자 이미지를 변환한 파일이어야 한다.
나는 이전 글의 코드를 사용해서 숫자별로 5장씩 50장을 저장했다.
그러나, 안드로이드 에뮬레이터에는 3장만 저장해서 사용했다.

다시 얘기하겠지만 중요한 점이 있다.
반드시 PC와 에뮬레이터의 파일은 같은 내용이어야 한다.
물론 여기서 만든 예제는 잘 동작하기 때문에 비교하지 않아도 되지만
이 부분 때문에 1주일이나 걸렸다.
잘 안되니까 하기 싫고.. 중간에 서핑도 조금 하고.

def read_testset(dir_path):
filenames = os.listdir(dir_path)
# filenames = ['0_003.png', '1_005.png', '2_013.png']

# images를 배열이 들어있는 리스트로 생성하면 에러
images = np.zeros([len(filenames), 784], dtype=np.float32)
labels = np.zeros([len(filenames)], dtype=np.int32)

for i, filename in enumerate(filenames):
img = PIL.Image.open(os.path.join(dir_path, filename))
# gray 스케일 변환
img = img.convert("L")
# 원본이 mnist와 맞지 않는다면 필요
img = img.resize([28, 28])
# PIL로 읽어오면 0~255까지의 정수. 0~1 사이의 실수로 변환.
# 생략하면 소프트맥스 결과가 하나가 1이고 나머지는 모두 0. 확률 개념이 사라짐.
# 255로 나누면 원본과 비교해서 오차가 있긴 하지만, 정말 의미 없는 수준이라서 무시해도 된다.
img = np.uint8(img) / 255
# 2차원을 mnist와 동일한 1차원으로 변환. np로 변환 후에 reshape 호출
images[i] = img.reshape(-1)

# 레이블. 이름의 맨 앞에 정답 있다.
finds = filename.split('_') # 0_003.png
labels[i] = int(finds[0]) # 0

return images, labels


read_testset 함수를 사용해서 특정 폴더의 내용을 모두 가져와서 예측해 보자.
나는 new_data라는 이름의 폴더를 사용했다.

def load_model(h5_path, dir_path):
model = tf.keras.models.load_model(h5_path)

mnist = input_data.read_data_sets('mnist')
print('mnist :', model.evaluate(mnist.test.images, mnist.test.labels))

# 파일로 변환한 mnist 숫자 이미지 파일 읽
images, labels = read_testset(dir_path)
print('files :', model.evaluate(images, labels))

# 에뮬레이터 결과와 비교 목적
preds = model.predict(images)
print(preds)


load_model('./model/mnist.h5', './mnist/new_data')

# [출력 결과]
# 10000/10000 [==============================] - 0s 21us/sample - loss: 0.2636 - acc: 0.9262
# mnist : [0.26363739772737027, 0.9262]
# 50/50 [==============================] - 0s 64us/sample - loss: 0.1373 - acc: 0.9800
# files : [0.13729526340961457, 0.98]
# [[9.81748290e-03 9.09259825e-07 2.68990714e-02 1.55930538e-02 2.32001161e-03 3.85744683e-02 8.65096133e-03 1.09144221e-05 8.93410563e-01 4.72251419e-03]
# [9.44603598e-05 1.64911069e-06 2.58899899e-03 4.38177539e-03 4.03641279e-05 6.74966127e-02 2.20050606e-06 3.07728811e-08 9.24911320e-01 4.82621283e-04]
# [5.07208024e-05 1.35197956e-06 3.72361648e-03 3.30108742e-04 9.75759685e-01 8.81002983e-04 8.18982720e-04 5.65604598e-04 9.81913181e-04 1.68870017e-02]
# 이하 생략


추가로 모델과 상관없지만
결과가 너무 이상하게 나왔었기 때문에
에뮬레이터와 계속해서 비교하지 않을 수 없었다.
가장 중요했던 것은 원본 이미지와 에뮬레이터에서 사용하는 이미지의 픽셀 값이 같은 것인지,였다.
그래서 PC에서는 아래 함수를 만들어서 사용했다.

# 파일 이미지 출력
def show_image_values(file_path):
img = PIL.Image.open(file_path)

img = img.convert("L")
img = img.resize([28, 28])
img = np.uint8(img) / 255

print(img)


# pc와 에뮬레이터에 같은 파일을 넣고 실제 값 출력해서 비교.
# 똑같이 나왔다. 변환이 잘 되었다는 뜻.
show_image_values('./mnist/new_data/2_013.png')

# [출력 결과] 너무 길어서 앞뒤의 0에 대해 공백을 제거했다.
# 생략..
# [0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.09411765 0.9254902 0.44313725 0. 0. 0. 0. 0.5254902 0.99215686 0.99215686 0.25490196 0. 0. 0. 0. 0. 0.]
# [0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.00392157 0.03529412 0. 0. 0. 0. 0.28627451 0.97647059 0.99215686 0.52156863 0.01176471 0. 0. 0. 0. 0. 0.]
# [0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.08627451 0.97647059 0.99215686 0.84705882 0.12156863 0. 0. 0. 0. 0. 0. 0.]
# [0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.23529412 0.96078431 0.99215686 0.92156863 0.1254902 0. 0. 0. 0. 0. 0. 0. 0.]
# [0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.21568627 0.80784314 0.99215686 0.98431373 0.45882353 0. 0. 0. 0. 0. 0. 0. 0. 0.]
# 생략..


이번 글에서 필요한 것은
텐서플로 라이트 모델로 변환한 "mnist.tflite" 모델 파일이다.
나머지는 중간 과정.