텐서플로 라이트를 활용한 안드로이드 딥러닝-4장

텐서플로 라이트 모델 개발

Aiden
47 min readOct 23, 2021

이 포스팅에 사용되는 책은 임태규 저, 한빛미디어에서 출판된 ‘텐서플로 라이트를 활용한 안드로이드 딥러닝’ 이다.

지난 포스팅은 책의 1장을 다루었으며, 주된 내용은 텐서플로 라이트에 대한 설명과 특징, 장단점, 개발 환경 세팅에 대한 내용이었다.

책의 2, 3장은 안드로이드의 기본 개발 세팅 및 UI에 관한 기본적인 지식을 알려주는 파트라 넘어가게 되었다. 안드로이드 개발 파트에 대한 부분은 텐서플로 라이트의 활용과 관련이 없다면 거의 넘어갈 것이다. 따라서, 이 포스팅은 안드로이드 개발에 대한 지식이 어느정도 있는 사람이 보면 좋을 것 같다.

이번 포스팅은 책의 4장을 다룰 것이며, 주된 내용은 텐서플로 라이트 모델 개발 워크 플로, 모델 선택 / 개발 / 변환, 기기 배포의 내용이 될 것이다. 모델 개발 파트는 머신러닝에 대한 기본 지식(용어)이 없으면 이해하기 어려울 수 있다. 간단한 설명을 추가하겠지만, 글을 읽으면서 검색을 하거나, 머신러닝 기본 서적 혹은 인터넷 강의 등을 듣고 오기를 추천한다

아래는 포스팅에 사용된 코드이다. 모든 코드는 포스팅에 있지만 혹시라도 필요할 경우가 있을지 모르니 첨부해 둔다.

텐서플로 라이트 모델 개발 워크 플로

텐서플로 라이트 모델 개발은 모델 선택 -> 모델 변환 -> 기기 배포 -> 모델 최적화 순으로 진행

모델 선택

모델 선택은 안드로이드 앱에서 이용할 딥러닝 모델을 선택하는 프로세스이다. 모델은 텐서플로를 이용하여 직접 개발할 수도 있고 이미 개발된 모델(텐서플로 허브에서 확인 가능)을 사용할 수도 있다.

모델 변환

개발한(혹은 이미 개발된) 모델을 텐서플로 라이트 모델로 변환하는 프로세스이다. 텐서플로 라이트는 모델을 저장할 때 ‘.tflite’라는 별도의 포맷을 사용한다. 텐서플로 모델을 tflite로 변환하는 과정에서 자동으로 최적화를 하는데, 이때 정확도 손실을 최소한으로 하면서 모델의 크기를 줄인다.

  1. 파이썬 환경 — 텐서플로 모델을 tflite 파일로 변환
  2. 안드로이드 — tflite 파일을 배포

기기 배포

tflite파일을 안드로이드 스튜디오의 프로젝트에 배포하고, 이를 이용하여 안드로이드 앱을 만들어 기기에 배포하는 프로세스이다.

모델 최적화

모델이 안드로이드 기기에서 최적의 성능을 발휘하도록 튜닝하는 프로세스이다. 최적화를 거치면 모델의 정확도 손실을 최소화하면서 모델의 크기가 줄어든다. 모델 변환 단계에서 텐서플로 라이트가 자동으로 최적화를 수행하지만, 실행 속도나 정확도를 더욱 개선하기 위해 직접 최적화를 할 수 있다. 모델의 정확도와 크기는 서로 트레이드오프 관게이므로 최적화의 목표는 정확도와 크기 사이에서 이상적으로 균형을 맞추는 것이다.

모델 선택

모델 선택의 방법에 따라

  1. 모델 설계
  2. 모델 학습
  3. 모델 변환

위 3가지 사항의 필요/불필요가 나뉘게 된다.

모델 직접 개발

텐서플로를 이용하여 딥러닝 모델을 직접 개발하고 이를 변환하여 안드로이드 앱에 배포한다. 모델 설계와 학습, 변환까지 모든 과정을 직접할 수도 있고, 모델 설계만 텐서플로에서 제공하는 모델 아키텍처를 이용하는 방법으로 대체할 수도 있다.

사전 학습 모델 이용

이미 훈련이 완료된 모델로, 복잡하고 오래 걸리는 학습 절차 없이 바로 이 모델을 이용하여 추론할 수 있다. 텐서플로는 이미 학습이 완료된 몇 가지 모델을 제공하므로 학습 데이터가 필요 없고, 많은 컴퓨팅 자원을 이용하여 오랜 시간 모델을 학습시킬 필요도 없다. 그러나 아직은 널리 사용되는 몇 가지 모델만 사전 학습 모델로 제공하기 때문에 해결해야 할 문제에 딱 맞는 모델을 찾기 어려울 수도 있다.

사전 학습 모델은 텐서플로 라이트 모델로 제공되기도 하고 텐서플로 모델로 제공되기도 한다.

  1. 텐서플로 라이트 — 바로 앱에서 사용할 수 있는 tflite 파일로 제공
  2. 텐서플로 모델은 — 케라스(Keras) 애플리케이션 모듈에서 제공하는 모델을 이용하거나 텐서플로 허브에서 제공하는 모델을 이용할 수 있다.

케라스(Keras)는 파이썬으로 작성된 오픈 소스 신경망 라이브러리이다. MXNet, Deeplearning4j, 텐서플로, Microsoft Cognitive Toolkit 또는 Theano 위에서 수행할 수 있다.
출처-https://ko.wikipedia.org/wiki/%EC%BC%80%EB%9D%BC%EC%8A%A4

전이 학습(transfer learning)

직접 모델을 개발하는 방법과 사전 학습 모델을 이용하는 방법의 장점을 결합한 방법이다. 학습이 완료된 모델을 다른 문제에 다시 학습시키는 방식으로 모델을 개발. 예를 들어, 개와 고양이를 분류하는 모델을 사자와 호랑이 데이터로 다시 학습시켜 사자와 호랑이를 분류하는 모델을 만드는 것이다.

텐서플로 허브

머신러닝 모델을 업로드하고 공유하는 저장소이다. 텐서플로 허브를 이용하면 더욱 다양한 최신 사전 학습 모델을 이용할 수 있다.

텐서플로 허브 외에도 다양한 사이트에서 모델과 데이터셋을 구할 수 있다.

모델 관련

데이터셋 관련

모델 개발

모델을 직접 개발하기로 결정했다면 선택한 모델을 먼저 개발해야 한다. 딥러닝 모델 개발에 대해 설명하려면 책 학 권으로도 부족하기 때문에 여기서는 모델 선택 방법별로 모델 개발을 이해할 수 있도록 간단한 모델을 만들어 보겠다.

모델 직접 개발

MNIST 데이터셋을 이용하여 손글씨 분류 모델을 만들어 보자. 직접 설계한 간단한 다층 퍼셉트론(Multi-Layer Perceptron, MLP)과 합성곱 신경망(Convolutional Neural Network, CNN)을 구현하고, 텐서플로에서 제공하는 ResNet 아키텍처를 이용한 모델을 구현하겠다.

MNIST 데이터베이스 (Modified National Institute of Standards and Technology database)는 손으로 쓴 숫자들로 이루어진 대형 데이터베이스이며, 다양한 화상 처리 시스템을 트레이닝하기 위해 일반적으로 사용된다. 손으로 쓴 글씨들이 모여있는 이미지 데이터 셋이라고 생각하면 된다.
출처 — https://ko.wikipedia.org/wiki/MNIST_%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4

퍼셉트론(perceptron)은 인공신경망의 한 종류로서, 1957년에 코넬 항공 연구소(Cornell Aeronautical Lab)의 프랑크 로젠블라트 (Frank Rosenblatt)에 의해 고안되었다. 이것은 가장 간단한 형태의 피드포워드(Feedforward) 네트워크 — 선형분류기- 로도 볼 수 있다.
출처 — https://ko.wikipedia.org/wiki/%ED%8D%BC%EC%85%89%ED%8A%B8%EB%A1%A0

합성곱 신경망(Convolutional neural network, CNN)은 시각적 영상을 분석하는 데 사용되는 다층의 피드-포워드적인 인공신경망의 한 종류이다. 딥 러닝에서 심층 신경망으로 분류되며, 시각적 영상 분석에 주로 적용된다. 또한 공유 가중치 구조와 변환 불변성 특성에 기초하여 변이 불변 또는 공간 불변 인공 신경망 (SIANN)으로도 알려져 있다. 영상 및 동영상 인식, 추천 시스템, 영상 분류, 의료 영상 분석 및 자연어 처리 등에 응용된다.
출처 — https://ko.wikipedia.org/wiki/%ED%95%A9%EC%84%B1%EA%B3%B1_%EC%8B%A0%EA%B2%BD%EB%A7%9D

텐서플로 2.0부터는 공식 문서와 튜토리얼에서 케라스를 사용한다. 케라스는 파이썬으로 구현된 고수준 딥러닝 API 이다. 저수준 API로 텐서플로, 테아노(Theano), CNTK 중 하나를 선택할 수 있지만, 여기서는 텐서플로에 포함된 케라스를 이용하기 때문에 저수준 API로 텐서플로를 사용한다.

케라스는 인터페이스가 직관적이고 모듈화가 잘 되어 있어 여러 모듈을 조합하여 모델을 쉽게 만들 수 있다.

데이터셋 준비

손글씨 분류 모델을 학습시키기 위해 MNIST 데이터가 필요하며, 이는 텐서플로 프레임워크에서 다운로드할 수 있다. 총 7만개의 28 *28의 손글씨 이미지와 레이블이 제공되는데 6만개는 학습 데이터로, 1만개는 검증 데이터로 사용한다.

앞으로 사용되는 코드는 구글 코랩에서 파일을 만들어서 실행하면 된다

아래와 같은 코드를 사용해서 MNIST 데이터를 가져오자

# 코드 4–1

import tensorflow as tf

mnist = tf.keras.datasets.mnist

(x_train, y_train), (x_test, y_test) = mnist.load_data()

위의 코드에서 할당된 변수들의 값을 살펴보자

  1. x_train: 28x28 크기의 이미지 6만개로 구성된 학습 데이터
  2. y_train: 값이 0~9인 레이블 6만개로 구성된 학습 데이터
  3. x_test: 28x28 크기의 이미지 1만개로 구성된 검증 데이터
  4. y_test: 값이 0~9인 레이블 1만개로 구성된 검증 데이터

이제 입력 데이터인 x_train과 x_test의 값을 정규화(nomalization) 시켜주는 작업이 필요하다. 정규화는 입력 데이터를 0~1의 범위로 변경시켜 주는 과정이며, 정규화를 하게되면 모델의 정확도가 증가한다.

정규화의 공식은 다음과 같다

(대상 값 - 입력 값의 최솟값) / (입력 값의 최댓 값 -입력 값의 최솟값)

이를 우리가 정규화할 대상에 적용해보면, 입력 데이터는 28*28 이미지이고, 각 픽셀 값의 범위는 0~255이다. 따라서 입력값의 최솟값은 0, 최대값은 255이다. 이를 위 공식에 대입해보면 우리의 정규화 공식은 아래와 같이 도출된다

(대상 값 / 255)

정규화 과정이 이해가 되었으면, 아래와 같은 코드를 추가해준다

# 코드 4–2

x_train, x_test = x_train / 255.0, x_test / 255.0

모델 설계 및 학습

데이터셋을 준비했으면 딥러닝 모델을 설계한다. 가장 단순한 인공 신경망인 다층 퍼셉트로 모델을 구현할 것이다. 다층 퍼셉트론은 입력층(input layer), 은닉층(hidden layer), 출력층(output layer)으로 구성되어 있다. 각 층 간의 모든 노드가 서로 연결되어 있기 때문에 다층 퍼셉트론을 완전 연결 신경망(Fully Connected Neural Network, FCN)이라고도 부른다.

모델에서 사용하는 입력 데이터는 28x28 크기의 이미지이고 레이블은 숫자 0~9이다. 따라서 모델의 출력은 크기가 10인 리스트 형태인데, 첫 번째 값은 입력 데이터가 0일 확률을 나타낸다. 두번째 값은 입력 데이터이터가 1일 확률, 열 번째 값은 입력 데이터가 9일 확률을 나타낸다.

우리의 목표는 손글씨 숫자 이미지를 보고 0~9까지의 숫자 중 어떤 것인지 파악하는 모델을 만드는 것이다.

손글씨 분류는 다중 클래스 분류 문제이므로 출력층의 활성화 함수로 softmax를 사용한다. 은닉층의 개수는 임의로 128개로 설정했으며, 입력층의 활성화 함수로 Relu를 사용한다.

입력층은 28 x 28 = 784이고 각 픽셀 값이 입력값이 되므로 총 784개 노드로 나타낼 수 있다. 또한 은닉층은 128개 노드로 구성되고 출력층은 0~9까지 9개 노드로 구성된다.

이를 코드로 나타내면,

# 코드 4–3

mlp_model = tf.keras.models.Sequential([

tf.keras.layers.Flatten(input_shape=(28,28)),

tf.keras.layers.Dense(128, activation=’relu’),

tf.keras.layers.Dense(10, activation=’softmax’)

])

tf.kears.models.Sequential 클래스를 이용했는데, Sequential 클래스는 모델 및 모델 내 모든 레이어에 입력 텐서와 출력 텐서가 하나씩만 있을 때 사용하며, 각 레이어를 순차적으로 쌓아서 모델을 만든다.

위 코드는 Flatten 레이어 1개와 Dense 레이어 2개로 구성되어 있다. Flatten 레이어는 28 x 28 크기의 2차원 입력을 784개의 1차원 배열로 바꿔준다. 다음 Dense 레이어는 128개의 뉴런으로 이루어진 은닉층이고, 마지막 Dense 레이어는 10개의 클래스에 대응되도록 10개의 뉴런으로 이루어진 출력층이다.

모델 생성이 완료되면 모델을 컴파일하기 위해 하단 코드와 같이 옵티마이저(optimizer), 손실 함수(loss function), 평가 지표(metric)를 인자로 전달하고 compile() 함수를 호출한다.

# 코드 4–4

mlp_model.compile(optimizer=’adam’, loss=’sparse_categorical_crossentropy’, metrics=[‘accuracy’])

자주 사용되는 손실 함수로 평균 제곱 오차(Mean Squre Error, MSE)와 교차 엔트로피 오차(Cross Entropy Error, CEE)가 있다. 평균 제곱 오차는 예측 값과 실제 값의 오차를 제곱한 값의 평균이다. 따라서 평균 제곱 오차가 작을수록 예측 값과 실제 값의 차이가 작다. 오차의 제곱 대신 오차의 절댓값을 사용하는 평균 절대 오차(Mean Absolute Error, MAE)나 오차의 제곱에 루트를 적용하는 루트 평균 제곱 오차(Root Mean Square Error, RMSE) 등의 함수도 많이 사용된다. 교차 엔트로피 오차는 예측 값과 실제 값의 확률 분포 차이를 계산할 때 사용한다. 교차 엔트로피 오차가 작을수록 예측 값과 실제 값의 확률 분포가 유사하다. 일반적으로 회귀 문제에는 평균 제곱 오차를 사용하고 분류 문제에는 교차 엔트로피 오차를 사용한다.

교차 엔트로피 오차는 문제 유형 및 데이터에 따라 세가지 유형이 있는데 이진 분류(binary classification) 문제는 binary_crossentropy를, 다중 클래스 분류(multi-class classification) 문제에는 categorical_crossentropy 또는 sparse_categorical_crossentropy를 사용한다. 다중 클래스 분류 문제에는 원 핫 인코딩(one-hot-encoding) 기법을 적용할 수 있으며, 이를 적용한 경우 categorical_crossentropy를 사용하고 적용하지 않은 경우 sparse_categorical_crossentropy를 사용한다.

원 핫 인코딩(one-hot-encoding)

범주형 데이터를 실수로 표현하지 않고 행렬로 표현하는 기법. 예를 들어 손글씨 분류에서 원 핫 인코딩을 적용하지 않으면 0~9 클래스를 그대로 0~9로 표현하지만, 원 핫 인코딩을 적용하면 0~9 숫자 클래스를 각각 10개의 값을 가진 리스트로 표현한다. 즉 0은 [1,0,0 …0], 1은 [0,1,0…0], 9는 [0,0…1]로 표현된다.

위 코드에서는 원 핫 인코딩을 적용하지 않았기 때문에 손실함수로 sparse_categorical_crossentropy값을 사용했다.

원 핫 인코딩을 적용하고 싶다면 하단과 같은 코드를 사용하면 된다.

# 코드 4–5

y_train = tf.keras.utils.to_categorical(y_train)

y_test = tf.keras.utils.to_categorical(y_test)

mlp_model.compile(optimizer=’adam’, loss=’categorical_crossentropy’, metrics=[‘accuracy’])

모델의 구조는 summary() 함수로 확인할 수 있다.

# 코드 4–6

mlp_model.summary()

summary() 함수를 호출하면 위와같은 표를 확인 할 수 있는데, 레이어의 구성과 레이어별 출력 형태, 파라미터 수를 보여준다.

첫 번째 Dense 레이어는 입력 784개, 편향 1개가 128개의 출력과 완전 연결되어 있으므로 파라미터가 785*128 = 100,480개이고, 두 번째 Dense 레이어는 입력 128개, 평향 1개, 출력 10개이므로 파라미터가 1,290개이다. 그러므로 총 파라미터는 100,480 + 1,290 = 101,770이다.

모델 설계가 끝났으면 하단과 같은 코드를 실행하여 학습을 시작한다. 학습 데이터와 레이블을 각각 전달하고 epochs를 5로 설정했다.

# 코드 4–7

mlp_model.fit(x_train, y_train, epochs=5)
* 혹시 에러가 발생하는 경우, 원핫인코딩을 하지 않은채로 진행해야한다.

epochs를 5로 설정했으므로 다섯번 반복해 학습하면서 점점 loss가 줄어들고 accuracy가 증가하는 것을 볼 수 있다.

모델 정확도 평가

학습이 완료되면 evaluate() 함수를 호출하여 테스트 데이터를 가지고 모델의 정확도를 확인할 수 있다.

# 코드 4–8

mlp_model.evaluate(x_test, y_test, verbose=2)
# 필자는 97.5% 정도 나왔는데, 아마 대부분의 경우에 97.x% 정도로 나올 것 같다

Functional API와 Model 클래스 상속을 통한 모델 개발

Functional API의 경우 Sequential 클래스보다 더 자유롭게 모델을 만들 수 있다. 모델이나 레이어의 다중 입력 또는 다중 출력을 구현할 수 있고 잔차 연결(residual connection), 다중 분기(multi-branch) 등 비선형 토폴로지(non-linear topology) 모델을 구현할 수도 있다.

Functional API를 이용하거나 Model 클래스를 상속하여 만든 모델은 동일하므로 성능에 차이가 없다.

Functional API를 사용하는 경우

# 코드 4–9

inputs = tf.keras.Input(shape=(28,28))

x = tf.keras.layers.Flatten()(inputs)

x = tf.keras.layers.Dense(128, activation=’relu’)(x)

outputs = tf.keras.layers.Dense(10, activation=’softmax’)(x)

mlp_model = tf.keras.Model(inputs=inputs, outputs=outputs)

Model 클래스 상속을 한 경우

# 코드 4–10

class MLP_Model(tf.keras.Model):

def __init__(self):

super(MLP_Model, self).__init__()

self.flatten = tf.keras.layers.Flatten()

self.dense = tf.keras.layers.Dense(128, activation=’relu’)

self.softmax = tf.keras.layers.Dense(10, activation=’softmax’)

def call(self, inputs):

x = self.flatten(inputs)

x = self.dense(x)

return self.softmax(x)

mlp_model = MLP_Model()

합성곱 신경망

다층 퍼셉트론 모델의 평가 결과는 97.x% 정도로 매우 높은 수준이지만, 합성곱 신경망을 이용하면 이미지 분류 모델의 정확도를 더욱 향상할 수 있다. 합성곱 신경망은 합성곱 연산을 적용하여 지역성(locality)에 기반한 특징(feature)을 학습한다. 또한 필터 반복 적용으로 가중치가 공유되어 완전 연결 네트워크에 비해 파라미터가 훨씬 적다. 이러한 특징 때문에 합성곱 신경망은 이미지 데이터에 매우 적합한 모델이다.

합성곱 신경망은 계층 간에 완전 연결되지 않는다. 또한 인접 노드 간에 가중치를 공유한다.

합성곱 신경망을 사용하기 위해 입력 데이터의 형태를 바꿔야한다. 다층 퍼셉트론의 입력 데이터는 ‘높이, 너비'의 2차원 텐서로 28 x 28형태이지만, 합성곱 신경망은 입력 이미지로 ‘높이, 너비, 채널'의 3차원 텐서를 사용하기 때문이다. MNIST 데이터는 1채널 이미지이므로 (28,28) 형태의 데이터를 (28, 28, 1) 형태로 변환해야 한다.

# 책의 코드가 잘 작동하지 않아, tensorflow 합성곱 신경망 예제의 코드를 빌려사용하였다

# 코드 4–11

(train_images, train_labels), (test_images, test_labels) = mnist.load_data()

# 입력 값의 형태를 4차원으로 변경하는 코드

train_images = train_images.reshape((-1, 28, 28, 1))

test_images = test_images.reshape((-1, 28, 28, 1))

# 픽셀 값을 0~1 사이로 다시 정규화

train_images, test_images = train_images / 255.0, test_images / 255.0

model = tf.keras.models.Sequential()

model.add(tf.keras.layers.Conv2D(32, (3, 3), activation=’relu’, input_shape=(28, 28, 1)))

model.add(tf.keras.layers.MaxPooling2D((2, 2)))

model.add(tf.keras.layers.Conv2D(64, (3, 3), activation=’relu’))

model.add(tf.keras.layers.MaxPooling2D((2, 2)))

model.add(tf.keras.layers.Conv2D(64, (3, 3), activation=’relu’))

model.summary()

model.add(tf.keras.layers.Flatten())

model.add(tf.keras.layers.Dense(64, activation=’relu’))

model.add(tf.keras.layers.Dense(10, activation=’softmax’))

model.summary()

model.compile(optimizer=’adam’,

loss=’sparse_categorical_crossentropy’,

metrics=[‘accuracy’])

model.fit(train_images, train_labels, epochs=5)

필자의 모델 정확도는 99.3%로 합성곱 신경망의 높은 성능을 확인할 수 있다.

케라스 애플리케이션 모델

지금까지 다층 퍼셉트론과 합성곱 신경망을 구현해보았다. 다층 퍼셉트론과 합성곱 신경망은 비교적 간단한 모델이므로 구현이 어렵지 않았지만, ResNet, MobileNet, EfficientNet 등 최근 많이 사용하고 있는 모델은 훨씬 깊고 복잡한 구조를 가지고 있다. 텐서플로는 이러한 모델을 직접 구현하지 않아도 편리하게 이용할 수 있도록 케라스 애플리케이션 모듈에서 몇 가지 모델을 제공하는데, 그중에서도 ResNet 모델을 이용하여 MNIST 데이터로 훈련시킨 손글씨 분류를 구현하는 방법을 알아보겠다.

인공 신경망은 네트워크 깊이가 깊어질수록 더 복잡한 문제를 해결할 수 있지만, 깊이가 지나치게 깊으면 기울기 소실(vanishing gradient) 문제가 발생하고 성능이 급격히 떨어진다. ResNet은 잔차 학습(residual learning)을 이용하여 이를 개선한 모델로, 잔차 블록(residual block)을 여러 층 쌓은 구조이다. 잔차 블록은 입력을 그대로 출력으로 연결하는 숏컷 연결(shortcut connection)을 가지고 있다. 숏컷 연결 덕분에 네트워크가 깊어도 신호가 소실되지 않고 네트워크 전체에 영향을 줄 수 잇다.

# GPU를 사용하지 않으면, 속도가 상당히 느리니 꼭 런타임 -> 런타임 유형변경 -> 하드웨어 가속기(GPU)로 변경후에 하단 코드를 실행하도록 하자

# 코드 4–13

(train_images, train_labels), (test_images, test_labels) = mnist.load_data()

train_images = train_images.reshape((-1, 28, 28, 1))

test_images = test_images.reshape((-1, 28, 28, 1))

train_images, test_images = train_images / 255.0, test_images / 255.0

resized_x_train = tf.image.resize(train_images, [32, 32])

resized_x_test = tf.image.resize(test_images, [32, 32])

resnet_model = tf.keras.applications.ResNet50V2(

input_shape=(32,32,1),

classes=10,

weights=None

)

resnet_model.compile(optimizer=’adam’,

loss=’sparse_categorical_crossentropy’,

metrics=[‘accuracy’])

resnet_model.fit(resized_x_train, train_labels, epochs=5)

resnet_model.evaluate(resized_x_test, test_labels, verbose=2)

먼저 합성곱 신경망과 마찬가지로 입력 뎅디터에 채널을 포함하여 4차원 형태로 변환했다. ResNet이 지원하는 최소 이미지 크기가 32 x 32이므로 tf.image.resize() 함수를 이용하여 이미지 크기를 28 x 28에서 32 x 32로 확대했다. 그런 다음 모델을 생성하기 위해 tf.keras.applications.ResNet50V2 클래스에 입력 데이터의 형태(input_shape), 분류할 클래스 수(classes), 초기 가중치(weights)를 파라미터로 전달한다.

사전 학습 모델 이용

사전 학습 모델은 이미 학습이 완료된 모델로 tflite 파일 또는 텐서플로 모델로 제공된다. tflite 파일은 모델 개발의 최종 산출물이므로 바로 안드로이드 스튜디오에 배포하여 앱을 개발하면 된다. 그러나 텐서플로 모델은 tflite파일로 변환하는 과정을 거쳐야 한다. 여기서는 텐서플로 모델을 이용하는 방법만 다루겠다. ImageNet 데이터로 학습된 MobileNet V2를 사용할 것이다.

MobileNet은 기존 합성곱 신경망의 합성곱 연산을 깊이 분할 합성곱(depthwise separable convolution) 연산으로 변경하여 기존 합성곱 신경망 모델 대비 계산량을 낮춘 모델로 약 8~9배의 높은 효율을 보인다.

이번에는 직접 학습하지 않기 때문에 학습 데이터가 필요 없지만 테스트 데이터가 있어야 모델이 잘 동작하는지 확인할 수 있다. 그러나 ImageNet 데이터는 용량이 매우 커서 테스트하기 적합하지 않으므로 ImageNet 데이터 대신 임의의 이미지 5개를 사용하여 모델이 이 이미지를 잘 분류하는지 테스트 하겠다.

테스트 데이터 준비

# 책에서는 이미지를 미리 다운로드 받고 시작하라고 했으나, 불편하기 때문에 코드에 이미지 다운로드 받는 것까지 추가하였다

#코드 4–14

from PIL import Image

import os

import numpy as np

!mkdir “./images”

!wget -O ./images/image1.png https://static.turbosquid.com/Preview/2019/08/01__08_55_55/Samsung_Notebook7_2019_13inch_00.jpgA0E07B83-D533-4243-A5AB-9E9394E895D7Large.jpg

!wget -O ./images/image2.png https://upload.wikimedia.org/wikipedia/commons/1/18/Freightliner_truck_in_Vietnam.JPG

!wget -O ./images/image3.png https://cf.ltkcdn.net/kids/images/orig/239664-1604x988-snail.jpg

!wget -O ./images/image4.png https://upload.wikimedia.org/wikipedia/commons/7/7b/Orange-Whole-%26-Split.jpg

!wget -O ./images/image5.png https://hips.hearstapps.com/hmg-prod.s3.amazonaws.com/images/bojnice-castle-1603142898.jpg?crop=1.00xw:0.752xh;0,0.0240xh&resize=640:*

data_dir = “./images/”

files = os.listdir(data_dir)

images = []

for file in files:

path = os.path.join(data_dir, file)

images.append(np.array(Image.open(path)))

데이터 전처리

#코드 4–15

import tensorflow as tf

resized_images = np.array(np.zeros((len(images), 224, 224, 3)))

for i in range(len(images)):

resized_images[i] = tf.image.resize(images[i], [224, 224])

preprocessed_images = tf.keras.applications.mobilenet_v2.preprocess_input(resized_images)

MobileNet V2의 기본 입력 텐서 형태는 (224, 224, 3)이므로 이미지 형태를 (224, 224, 3)으로 변환한다. 변환된 이미지를 array에 담아 mobilenet_v2 모듈에 포함된 preprocess_input() 함수를 적용하여 입력 값을 전처리 한다.

모델 생성 및 추론

#코드 4–16

mobilenet_imagenet_model = tf.keras.applications.MobileNetV2(weights=”imagenet”)

y_pred = mobilenet_imagenet_model.predict(preprocessed_images)

topK = 1

y_pred_top = tf.keras.applications.mobilenet_v2.decode_predictions(y_pred, top = topK)

위 코드는 모델을 생성하고 이를 이용하여 모델의 분류 결과를 확인하는 코드이다. tf.keras.applications.MobiileNetV2 클래스를 통해 모델을 불러올 수 있다. 파라미터로 weights를 “imagenet”으로 지정하면 ImageNet 데이터로 학습된 모델을 얻을 수 있다. “imagenet”이 아니더라도 weights가 저장된 path를 지정하면 해당 weights값이 적용된 모델을 얻을 수 있다.

predict() 함수로 테스트 데이터를 모델에 입력하고 예측 결과를 y_pred에 담았다. y_pred는 5개의 이미지가 1000개의 클래스에 각각 속할 확률이 얼마인지를 담고 있으므로 형태가 (5, 1000)이다.

결과값 해석을 위해 mobilenet_v2 모듈에 포함된 decode_predictions() 함수를 적용한다. top 인자로 topK인 1을 주었기 때문에 확률이 가장 높은 클래스 1개만 반환한다.

마지막으로 테스트에 사용한 이미지와 모델의 추론 결과를 비교하여 살펴보겠다.

#코드 4–17

from matplotlib import pyplot as plt

import numpy as np

for i in range(len(images)):

plt.imshow(images[i])

plt.show()

for k in range(topK):

print(f’{y_pred_top[i][k][1]} ({round(y_pred_top[i][k][2] * 100, 1)}%’)

plt.imshow(), plt.show() 함수를 이용하여 이미지를 출력하고, y_pred_top 변수에 담긴 추론한 클래스와 확률을 출력했다. 콘솔창에 결과값이 나올테고, 5개의 이미지를 모두 성공적으로 분류했을 것이다. ImageNet 데이터로 학습한 모델을 일반적인 이미지의 분류에 사용해도 높은 정확도로 분류해낼 수 있다는 것을 알 수 있다.

전이학습

전이 학습은 학습된 모델을 원하는 데이터로 다시 학습시키는 방법이다. 사전 학습 모델 이용과 마찬가지로 이미 학습이 완료된 모델을 활용하지만 그대로 활용하지 않고 원하는 데이터로 다시 학습시킨다. 여기서는 ImageNet 데이터를 학습한 MobileNet V2를 사용하겠다. 이 모델을 개와 고양이 이미지로 학습시켜 개와 고양이 이미지를 구분하는 모델을 얻을 것이다.

import tensorflow_datasets as tfds

tfds.disable_progress_bar()

raw_train, raw_test = tfds.load(

‘cats_vs_dogs’,

split=[‘train[:80%]’, ‘train[20%:]’],

as_supervised=True

)

위 코드는 tensorflow-dataset 라이브러리를 통해 개와 고양이 이미지 데이터를 다운로드 하는 코드이다.

# 코드 4–19

import numpy as np

import tensorflow as tf

from tensorflow.image import ResizeMethod

def preprocess(image, label):

out_image = tf.image.resize(image, [224,224], method=ResizeMethod.BICUBIC)

out_image = tf.keras.applications.mobilenet_v2.preprocess_input(out_image)

return out_image, label

batch_size = 32

train_batch = raw_train.map(preprocess).batch(batch_size)

test_batch = raw_test.map(preprocess).batch(batch_size)

위 코드는 입력 이미지를 전처리하는 코드이다. 케라스 애플리케이션은 입력 이미지의 크기가 96, 128, 160, 192, 224인 mobileNet 모델만 지원하기 때문에 preprocess() 함수 안에서 바이큐빅 보간법을 이용하여 입력 이미지의 크기를 224 x 224로 변환했다.

# 코드 4–20

mobilenet_base = tf.keras.applications.MobileNetV2(

input_shape=(224, 224, 3),

weights=”imagenet”,

include_top=False

)

위 코드는 tf.keras.applications.MobileNetV2 클래스를 통해 모델을 생성하고 파라미터로 입력 데이터의 크기와 weights, include_top을 지정했다. include_top을 False로 설정하면 모델의 마지막 풀링 레이어와 Dense 레이어를 제외한 모델을 얻을 수 있다. 마지막 레이어를 제외하는 이유는 모델의 출력 결과가 문제에 의존적이기 때문이다. 우리는 개와 고양이를 나누는 두개의 결과값만 필요한데, 지금 모델은 1000개의 결과으로 출력하기 때문이다.

# 코드 4–21

mobilenet_base.trainable = False

mobilenet_model = tf.keras.Sequential()

mobilenet_model.add(mobilenet_base)

mobilenet_model.add(tf.keras.layers.GlobalAveragePooling2D())

mobilenet_model.add(tf.keras.layers.Dense(1))

mobilenet_model.compile(optimizer=’adam’, loss=’binary_crossentropy’, metrics=[‘accuracy’])

mobilenet_model.fit(train_batch, epochs=5)

mobilenet_model.evaluate(test_batch,verbose=2)

위 코드는 이미 학습이 완료된 mobilenet_base의 가중치가 더 이상 학습되지 않도록 막기 위해 mobilenet_base.trainable 값을 False로 설정했다. 일반적으로 전이 학습은 이미 학습된 가중치가 더 이상 학습되지 않도록 동결시키고 훈련을 진행한다. 나중에 성능 향상을 위해 모델을 다시 학습 가능하도록 되돌리고 학습률(learning rate)를 낮추어 전체를 다시 한번 학습하며 미세 튜닝을 할 수 있다.

개와 고양이 이미지를 분류하는 문제는 이진 분류이므로 손글씨 분류와 달리 손실 함수도 binary_crossentropy로 바꿔야 한다.

텐서플로 허브

텐서플로 허브도 케라스 애플리케이션 모듈처럼 학습된 모델을 제공한다. 앞의 전의학습에서 진행했던 모델을 케라스 애플리케이션이 아닌 텐서플로 허브에서 가져와 개와 고양이 이미지를 분류할 것이다.

위의 모델을 사용할 것이다.

데이터셋 준비

# 코드 4–22

import tensorflow_datasets as tfds

tfds.disable_progress_bar()

raw_train, raw_test = tfds.load(

‘cats_vs_dogs’,

split=[‘train[:80%]’, ‘train[20%:]’],

as_supervised=True

)

코드 4–22는 코드 4–18과 동일하다

# 코드 4–23

import numpy as np

import tensorflow as tf

def preprocess(image, label):

out_image = tf.image.resize(image/255, [224,224])

return out_image, label

batch_size = 32

train_batch = raw_train.map(preprocess).batch(batch_size)

test_batch = raw_test.map(preprocess).batch(batch_size)

텐서플로 허브는 케라스 애플리케이션처럼 전처리 함수를 제공하지 않는다. 따라서 모델에서 요구하는 스펙에 맞게 직접 데이터를 전처리 해야한다. 모델 설명 페이지를 보면 데이터의 범위가 [0,1]인데 우리가 사용할 이미지의 각 픽셀 범위는 [0,255]이다. 따라서 입력 이미지를 255로 나누어 [0,1] 범위로 맞추고, tf.image.resize() 함수를 이용하여 이미지 크기를 224 x 224로 변환한다.

모델 생성 및 평가

# 코드 4–25

import tensorflow_hub as hub

url = “https://tfhub.dev/google/imagenet/mobilenet_v2_100_224/feature_vector/5"

hub_model_transfer = tf.keras.Sequential([

hub.KerasLayer(url, input_shape=[224,224,3], trainable=False),

tf.keras.layers.Dense(1)

])

URL은 텐서플로 허브에서 복사했던 URL이다. tf.keras.Sequential 클래스를 이용하여 모델을 만든다.

# 코드 4–26

hub_model_transfer.compile(optimizer=’adam’, loss=’binary_crossentropy’, metrics=[‘accuracy’])

hub_model_transfer.fit(train_batch, epochs=5)

학습이 완료되면 테스트 데이터를 이용하여 모델의 추론 결과를 평가한다.

# 코드 4–27

hub_model_transfer.evaluate(test_batch, verbose=2)

모델 변환

사전 학습 모델, 전이 학습 모델, 직접 개발한 모델 등 텐서플로에서 개발한 모델을 안드로이드에서 사용하려면 텐서플로 라이트 모델로 변환해야 한다. 텐서플로 라이트는 케라스 모델, SavedModel, Concrete 함수를 각각 TFLite 모델로 변환할 수 있고, 각 포맷의 변환 함수가 tf.lite.TFLiteConverter에 작성되어 있다.

케라스 모델 변환

케라스 모델은 딥러닝 모델을 개발하기 위한 고수준 라이브러리인 케라스를 이용하여 만든 모델이다. 텐서플로의 tf.keras 모듈을 통해 케라스 모델을 바로 만들거나 SavedModel, HDF5포맷으로 저장된 모델을 케라스 모델로 불러와서 텐서플로 라이트 모델로 변환할 수 있다. 여기서는 앞에서 개발한 다층 퍼셉트론 모델을 이용하여 변환하는 방법을 알아보겠다.

# 코드 4–28

import tensorflow as tf

mnist = tf.keras.datasets.mnist

(x_train, y_train), (x_test, y_test) = mnist.load_data()

x_train, x_test = x_train / 255.0, x_test / 255.0

mlp_model = tf.keras.models.Sequential([

tf.keras.layers.Flatten(input_shape=(28,28)),

tf.keras.layers.Dense(128, activation=’relu’),

tf.keras.layers.Dense(10, activation=’softmax’)

])

mlp_model.compile(optimizer=’adam’, loss=’sparse_categorical_crossentropy’, metrics=[‘accuracy’])

mlp_model.fit(x_train, y_train, epochs=5)

앞에서 만든 다층 퍼셉트론 모델을 동일하게 생성하여 학습까지 완료했다

# 코드 4–29

converter = tf.lite.TFLiteConverter.from_keras_model(mlp_model)

tflite_model = converter.convert()

변환기(converter)를 만들고 변환기의 convert()함수로 모델을 변환했다.

# 코드 4–30

with open(‘./keras_model.tflite’, ‘wb’) as f:

f.write(tflite_model)

변환한 모델을 파일로 저장했다.

SavedModel 변환

텐서플로 모델 저장 및 불러오기

텐서플로 모델을 저장하는 방법으로는 학습된 파라미터만 저장하는 방법과 모델 전체를 저장하는 방법이 있다. 파라미터만 저장하려면 체크포인트(checkpoint)를 사용하는데, 모델이 사용한 모든 파라미터 값을 저장하고 모델의 아키텍처는 저장하지 않는다. 모델 전체를 저장하려면 모델 아키텍처, 가중치, 컴파일 관련 설정 값, 옵티마이저를 모두 저장해야 하므로 SavedModel이나 HDF5 포맷을 사용한다. HDF5는 텐서플로 v2에서 주로 사용되던 방식이고 텐서플로 v2는 SavedModel 방식을 권장한다.

케라스 모델을 SavedModel로 저장

# 코드 4–31

tf.saved_model.save(mlp_model, “./mlp_model/”)

# 또는

mlp_model.save(“./mlp_model/”)

케라스 모델을 HDF5 포맷으로 저장

# 코드 4–32

mlp_model.save(“./mlp_model.h5”)

이미 저장된 모델을 케라스 모델로 불러오는 코드는 다음과 같다

# 코드 4–33

# SavedModel을 케라스 모델로 불러오기

saved_model = tf.keras.models.load_model(“./mlp_model/”)

# 코드 4–34

# HDF5로 저장된 모델을 케라스 모델로 불러오기

h5_model = tf.keras.models.load_model(“./mlp_model.h5”)

이렇게 불러온 모델을 코드 4–29처럼 변환하면 tflite 파일로 변환된다. 하지만 SavedModel은 케라스 모델로 불러오지 않고도 바로 tflite 파일로 변환이 가능하다. 텐서플로 라이트는 모델 변환에 이 방식을 가장 추천하고 있다.

SavedModel 바로 변환

# 코드 4–35

converter = tf.lite.TFLiteConverter.from_saved_model(“./mlp_model/”)

tflite_model = converter.convert()

위 코드가 SavedModel을 텐서플로 라이트 모델로 변환하는 코드이다. 변환 후에 아래 코드를 이용하면 tflite 파일이 생성된다

# 코드 4–36

with open(‘./saved_model.tflite’, ‘wb’) as f:

f.write(tflite_model)

Concrete 함수 변환

Concrete 함수

텐서플로 v2에는 즉시 실행 모드가 추가되었으며 기본으로 활성화되어 있다. 한편 모델을 고도화하기 위해 즉시 실행 모드보다 성능과 이식성이 뛰어난 그래프 모드를 사용하기도 한다. 즉시 실행 모드로 동작하는 파이썬 함수에 @tf.function 데코레이션을 붙이거나 모델과 함수를 tf.function() 함수에 인자로 전달하면 자동으로 그래프 모드로 변환된다. 아래는 이해를 돕기 위한 예제 코드이다. 입력받은 데이터에 1을 더하는 간단한 케라스 레이어다

# 코드 4–37

class Inc(tf.keras.layers.Layer):

def call(self, inputs):

return inputs + 1

inc = Inc()

위 Inc 레이어의 call() 함수에 @tf.function 데코레이터를 추가하여 아래와 같이 그래프 모드로 바꿀 수 있다.

# 코드 4–38

class Inc_Graph(tf.keras.layers.Layer):

@tf.function

def call(self, inputs):

return inputs + 1

inc_g = Inc_Graph()

또는 아래 코드와 같이 클래스의 인스턴스를 tf.function() 함수에 전달하여 그래프모드로 바꿀 수도 있다.

# 코드 4–39

inc_g2 = tf.function(inc)

파이썬은 기본적으로 다형성(polymorph)을 가지고 있어 함수를 만들면 다양한 자료형의 파라미터에 맞추어 동작한다. 앞의 Inc 클래스나 Inc_Graph 클래스의 call() 함수도 다형성을 가지고 있다. 따라서 아래 코드와 같이 어떤 데이터 타입이나 형태가 전달되어도 모두 처리가 가능하다

# 코드 4–40

print(inc_g(tf.constant(3)))

print(inc_g(tf.constant([3,2])))

print(inc_g(tf.constant([[3,2],[1.0,5.0]])))

print(inc_g2(tf.constant(3)))

print(inc_g2(tf.constant([3,2])))

print(inc_g2(tf.constant([[3,2],[1.0,5.0]])))

텐서플로 그래프는 일반적인 파이썬 함수와 달리 정적인 데이터 타입과 형태가 필요하기 때문에 호출 시 전달받은 파라미터의 타입과 형태에 맞는 Concrete 함수를 만든다. 타입과 형태를 묶어서 시그니처(Signature)라고 한다. 위 코드와 같이 int32 스칼라 텐서, 형태가 [2]인 int32 텐서, 형태가 [2,2]인 float32 텐서가 입력되면 각 입력 시그니처에 맞는 Concrete 함수를 만든다. 위 코드의 경우 총 3개의 Concrete 함수가 생성될 것이다. Concrete 함수는 시그니처별로 하나만 생성되어 재사용된다.

# 코드 4–41

print(inc_g(tf.constant(4)))

print(inc_g2(tf.constant(4)))

위 코드는 이미 호출된 시그니처와 동일한 시그니처를 가진 파라미터를 호출한 경우이다.

시그니처별 Concrete 함수는 아래 코드와 같이 get_concrete_function() 함수에 시그니처를 입력하여 얻을 수 있다.

데코레이터를 이용하여 그래프 모드로 변환한 함수의 Concrete 함수 획득

# 코드 4–42

concrete_fun = inc_g.call.get_concrete_function(tf.TensorSpec(shape=(1,3), dtype=tf.float32))

print(concrete_fun(tf.constant([[1.0,2.0,3.0]])))

그래프 모드로 변환한 함수의 Concrete 함수 획득

# 코드 4–43

concrete_fun = inc_g2.get_concrete_function(tf.TensorSpec(shape=(1,3), dtype=tf.float32))

print(concrete_fun(tf.constant([[1.0,2.0,3.0]])))

케라스 모델에서도 Concrete 함수를 얻을 수 있다.

tf.function() 함수를 이용한 그래프 모드 적용

# 코드 4–44

mlp_model = tf.keras.models.Sequential([

tf.keras.layers.Flatten(input_shape=(28,28)),

tf.keras.layers.Dense(128, activation=’relu’),

tf.keras.layers.Dense(10, activation=’softmax’)

])

graph_model = tf.function(mlp_model)

concrete_func = graph_model.get_concrete_function(tf.TensorSpec(shape=mlp_model.inputs[0].shape, dtype=mlp_model.inputs[0].dtype))

@tf.function() 데코레이터를 이용한 그래프 모드 적용

# 코드 4–45

class MLP_Model(tf.keras.Model):

def __init__(self):

super(MLP_Model, self).__init__()

self.flatten = tf.keras.layers.Flatten()

self.dense = tf.keras.layers.Dense(128, activation=’relu’)

self.softmax = tf.keras.layers.Dense(10, activation=’softmax’)

@tf.function

def call(self, inputs):

x = self.flatten(inputs)

x = self.dense(x)

return self.softmax(x)

mlp_model = MLP_Model()

concrete_func = mlp_model.call.get_concrete_function(

tf.TensorSpec(shape=(None, 28, 28), dtype=tf.float32))

Concrete 함수 변환

Concrete 함수를 업었다면 아래 코드와 같이 TFLite 모델로 변환하고 저장할 수 있다.

# 코드 4–46

converter = tf.lite.TFLiteConverter.from_concrete_functions([concrete_func])

tflite_model = converter.convert()

with open(‘./concrete_cunc_model.tflite’,’wb’) as f:

f.write(tflite_model)

텐서플로 허브의 TFLite 모델

텐서플로 허브에서 모델 포맷에 TFLite로 필터링 한 뒤에, 필요한 모델을 tflite 포맷으로 바로 다운로드하여 활용할 수 있다.

기기 배포

모델을 개발하고 tflite 파일로 변환했다면 앱에서 활용할 수 있도록 안드로이드 스튜디오에 배포해야한다. 안드로이드 스튜디오에서 프로젝트에 assets 폴더를 생성하고 그 안에 tflite 파일을 복사하면 배포가 완료된다. 먼저 [File]-[New]-[Folder]-[Assets Folder]를 선택한다

asset folder를 만들 수 있는 창이 나타나면 finish를 누른다

이후 왼편에 창을 보면 assets라고 폴더가 생긴것을 확인할 수 있다. 이 위치에 구글colab에서 만든 tflite 파일을 다운로드 받은 뒤에, 드래그 앤 드롭으로 tflite 파일을 이동시켜 보자

참고로 드래그앤 드롭이 안되는 경우 asset 폴더를 우클릭 후에 Open In -finder를 클릭하여 폴더를 열어놓고 그 안에다가 파일을 직접 옮겨주자 이후에도 asset 폴더에 안나온다면 안드로이드 스튜디오를 재시작해보자(시간이 조금 지난뒤에 반영이 되는것처럼 보인다)

마무리

이 장에서는 딥러닝 모델을 개발하고 이를 텐서플로 라이트 모델로 변환하는 방법을 살펴보았다. 모델을 선택하여 이를 텐서플로 라이트로 변환하는 방법을 설명하고 텐서플로 허브를 이용하여 이미 훈련된 모델을 활용하는 방법도 다루었다. 모델을 구현하는 방법은 이 책에 소개된 것보다 훨씬 더 많은 지식이 필요하기 때문에 깊이 공부하고 싶다면 ‘핸즈온 머신러닝’ 등 모델 개발을 전문적으로 다루는책을 참고하기 바란다.

다음 포스팅은 5장 ‘텐서플로 라이트 모델을 이용한 안드로이드 앱 개발’이며, 오늘 만든 모델을 활용하여 안드로이드 앱 내부에서 손글씨의 결과를 추론해보는 예제앱을 만들 예정이다.

--

--

Aiden

안드로이드 개발자(개인 공부용도의 블로그)