Android에 머신러닝을 붙이기 위해 겪은 고통

1) Android 에 올릴 수 있는 모델 파일 생성

이 부분은 시행착오이므로 정확하지 않을 수 있습니다.

(1) tflite

Keras모델은 주로 JSON(모델파일) + h5(HDF5/weight파일)로 저장합니다.
공식문서에서 tflite로 바꾸는 예제를 보고 코드를 추가했습니다.

Tensorflow Lite Converter

import tensorflow as tf
converter = tf.lite.TFLiteConverter.from_keras_model_file('model.h5')
tflite_model = converter.convert()
open("model.tflite", "wb") .write(tflite_model)

다음과 같은 오류가 떠서
'TFLiteConverterV2' has no attribute 'from_keras_model_file'

tensorflow nightly를 설치를 해줬습니다. 진짜 오래걸림(약 2시간) 하지만 tf nightly를 설치해도 TFLiteConverter을 이용할 수 없었습니다.

model = load_model('model.h5')
converter = tf.lite.TFLiteConverter.from_keras_model_file(model)

왜냐하면 모델이 불러와지지 않았습니다.

load_model()이 안 되는 원인을 분석한 결과,

  • h5파일에 model, weights둘 다 들어가 있으면 문제가 없습니다. model.save('model.h5')
    위의 경우가 아니라면, json을 통해 해결가능 → tf nightly에선 안 됩니다..
  • model = Sequential() ...(중략) 로 모델을 만들어준 다음 model.load_weights('model.h5') 하면 학습한 모델과 같이 불러올 수 있습니다.
    하지만 불러올 model과 레이어 등이 일치하게 만들어야 합니다.
    그래서 '준원님의 학습코드에서 에포크를 확 줄여서 학습하고 weight를 받아오면 되지 않을까?' 라는 생각을 했는데 AttributeError: module 'tensorflow.python.framework.ops' has no attribute '_TensorLike' 이 에러 등장! 이게 뭐냐 하면 방금 전 json에서 발생한 에러와 같습니다. → tf nighly에선...

  • 현재 학습 코드도 tf nightly에서 돌아가지 않습니다.
  • 따라서 텐서플로우를 1.5로 다운그레이드 했습니다.
    나중에 알게 되었지만, 처음엔 tf 버전2가 문제인 줄 알았고 버전1로 갔는데 그냥 nightly가 문제였습니다.
    테스트를 위해 colab을 tf-nightly환경으로 만들어 놓았습니다. 그렇게 1.5와 tf-nightly 두 가지로 테스트해보기 시작했습니다.

tf 1.5에서는 학습은 잘 돌아갔지만, tflite로 변환할 순 없었습니다. 왜냐면 LSTM을 이용한 keras모델이었는데, tf 버전1에서는 TOCO converter만 지원하고, 이 컨버터는 LSTM을 지원하지 않습니다!

스크린샷 2020-05-18 오전 4 58 42

다행히 2020년인 지금 TFLiteConver을 통해 LSTM을 지원해줍니다.
스크린샷 2020-05-18 오전 5 01 13
하지만 tf 버전2에서만 지원한다는 점!!!
이 때만 하더라도 tf 버전2가 tf-nightly인 줄 알았습니다. 무지한 게 죄입니다 ㅠㅠ
(tf-nightly는 개발 중인 버전이므로 불안정하다고 합니다.)
nightly에선 학습이 안 되고, TFLiteConverter는 2.0 이상에서 가능하니까 둘은 함께할 수 없다는 굉장히 아쉬운 판단을 내려버립니다.

그렇게 시작된 삽질..!

일단 모델을 불러오기 위해 다양한 시도를 했습니다.
경로를 통해 모델을 불러오고, json을 통해 불러오고, h5파일로 불러오고, model을 간단하게 만든 뒤 weight를 불러오고, 애초에 model.save('model.h5')로 h5에 전부 저장한 뒤 불러와보고...
이렇게 하다가 pb 파일로 변환시키는 것은 성공합니다! (당시엔 로컬이 1.5버전이라 TFLiteConverter 불가)
스크린샷 2020-05-18 오후 7 00 30
하지만 pb파일을 tflite로 변환하는 것도 쉽지 않았습니다.

스크린샷 2020-05-18 오후 11 39 04
와중에 'keras에서 h5모델을 로드하는 방법' 보다가 '넌 할 수있어'보고 힘을 얻고^^..
원문은 'You can use' 인데 번역이 이렇게 된 것 같습니다.

그러다가 DL4J에 눈독을 들였고 잠시 갔다가 슬쩍 돌아왔습니다. ((2번) 참고)

준원님께서 tflite로 변환하는 것 까지 있는 학습 코드를 올려주셔서 돌려보았습니다. 테스트가 목적이었기 때문에 체크포인트 관련 코드는 주석처리했습니다.
스크린샷 2020-05-19 오전 12 34 55
이런 에러가 발생했고, 준원님도 이 에러를 계속 만나셨다고...!

에러로 인한 분노를 애써 삼키며 코드를 다시 돌려보았습니다.

Using TensorFlow backend.
Traceback (most recent call last):
File "/Users/euzl/PycharmProjects/please/STT_Model.py", line 110, in <module>
model = Sequential()
...(중략)
AttributeError: module 'tensorflow' has no attribute 'get_default_graph'

위와 같은 에러가 발생했는데, 검색해보니 from keras를 from tensorflow.keras로 바꾸라고 했습니다. 그랬더니 갑자기 tflite가 만들어 집니다.... ?!

tensorflow.keras로 바꾼 뒤 정확도 issue가 있었지만, 나중에 준원님께서 해결해 주셨습니다!
(해결법 요약 : 다시 from keras로 바꿈 -> h5모델로 저장 -> tflite로 변환)

(2) DL4J

간단히 넘어가겠습니다.

  1. dl4j에서 사용할 수 있는 모델 파일로 변환하는 것은 tflite로 변환하는 것보다는 수월해 보였다. (위에서 한창 삽질하고 있을 때)
  2. 변환에 성공하면 android studio에서 모델을 불러오고, 사용한다는 점에서 어렵지 않으리라 생각했다.
  3. JAVA코드를 통해 변환해야 했다.
  4. android studio에서 변환하려고 했는데.. 기본 셋팅이 엄청 어려웠다.
  5. 시간은 없는데 새로 알아야 하는 정보가 너무 많았다. 안다고 해서 되리라는 보장도 없었다. 레퍼런스도 tflite도 적었는데 dl4j는 더 적었다.
  6. JAVA에서 변환하려고 하고 있었다. 그러려면 DL4J도 설치해야 했다.
  7. 멘토님의 말을 듣고 확신 없는 것은 tflite와 같기 때문에 tflite에 집중하기로 결정했다.

2) 안드로이드에 올리기

역할 분담을 통해, 만들어진 tflite파일을 이용해 안드로이드 연동을 시작했습니다.

tensorflow에서 공개한 'Text classification'예제 코드와 'Image classification'예제 코드를 분석하면서 interface를 이용해서 모델을 읽고 추론한다는 것을 알게 되었습니다.

tensorflow-lite 인터프리터에 대한 설명 (링크)

(1) 모델 파일 안드로이드 프로젝트에 추가하기

  1. Android를 Project로 바꾼다.
  2. main폴더assets 폴더를 생성
  3. model.tflite, labels.txt, vodab.txt 를 넣어준다.
    스크린샷 2020-05-19 오후 5 34 08

(2) gradle 파일 수정

  1. build.gradle (Module :app) 에 다음 내용 추가
dependencies {
		...
		implementation 'org.tensorflow:tensorflow-lite:+'
}

2) build.gradle (Module :app) 의 android부분에 `aaptOptions` 추가
→ 모델 파일이 압축되는 것을 방지함 (이걸 하지 않으면 메모리를 절약하기 위해 리소스를 압축한다고 하네요)
android {
		...
    aaptOptions {
        noCompress "tflite"
    }
}
  1. Sync now 동기화 진행

(3) 에러 발생👾

우리 앱에 모델을 읽는 코드를 작성하기도 하고, 예제 프로젝트에 모델을 올려보는 시도를 했는데 둘 다 에러가 발생했다. 굉장히 답답한 것이... 분명 어딘가 잘 못 되었는데 logcat에는 아무 것도 뜨지 않았다. 디버깅을 해보면 run함수에서 멈추고 loop함수를 돌 뿐..
스크린샷 2020-05-19 오후 8 06 21

2020-05-20 01:20:52.272 16860-16860/com.euzl.plztflite E/AndroidRuntime: FATAL EXCEPTION: main
    java.lang.IllegalArgumentException: Cannot convert between a TensorFlowLite tensor with type FLOAT32 and a Java object of type [Ljava.lang.String; (which is compatible with the TensorFlowLite type STRING).

input타입을 float로 바꿔 보았지만 효과는 없었다.
아는 분께 tflite.run(input, output) 에서 안 된다고 했더니 '모델의 input, output이 뭐냐'고 물어 보셨다. 그렇다. 나는 모델의 input과 output도 모른채 연동하고 있었다.

모델 학습 코드를 찬찬히 다시 살펴봤다. 학습 전 데이터와 라벨에 정수 인코딩을 해준 뒤 라벨만 원-핫 인코딩을 하는 부분이 있었다. 따라서 인풋은 정수이고, 라벨은 정수로 된 이중 벡터일 것이라 생각했다. [[0, 0, 1], [0, 1, 0], [1, 0, 0]] 이런 느낌!

이렇게 수정해서 넣었는데 실패 ~!

(4) 예제 모델부터 다시

핵데이 마감일이 이틀 남았기에 멘토님, 팀원과 긴급 행아웃 회의를 했다. 혼날 줄 알았는데 허심탄회한 대화로 이어졌다. 나는 koalanlp를 사용하는 방안을 얘기했는데, 멘토님께서는 '그렇게 하면 핵데이에서 얻어갈 수 있는 것이 없다.'며 딱 3시간만 준원님은 정확도를 높이는 연구를 하고 (다른 프레임워크도 고려), 나는 예제 모델을 우리 애플리케이션에 올리는 것을 시도하라고 하셨다. 또, 다른 변수는 배제하고 주어진 예제부터 하라고 하셨다.

예제 모델을 우리 앱에 올리던 중 이런 에러가 발생!!

2020-05-20 05:06:19.451 4343-6887/? E/Auth: [GoogleAccountDataServiceImpl] getToken() -> BAD_AUTHENTICATION. Account: [ELLIDED:-323920191](ellided:-323920191), App: com.google.android.gms, Service: oauth2:[https://www.googleapis.com/auth/login_manager](https://www.googleapis.com/auth/login_manager)
sgx: Long live credential not available.

google_services.json 를 확인해보라고 해서 팀원분께 요청 뒤 다시 넣었는데 변화가 없었다. 당연하다. 이 파일 문제가 아니기 때문이다. 우리 앱은 login에서 시작되는데, 테스트용 액티비티부터 시작하게 바꿔서 발생한 에러였다.

시작을 다시 login으로 바꾸고, 테스트용 액티비티는 버튼을 통해 시작되도록 바꿨더니 해결되었다.

아무튼 예제모델 올리기 성공!

(5) 우리의 모델을 올려보자

사실 (3)번에서 발생한 문제만 해결하면 거의 끝났다.
하지만 (3)번을 할 때와 다른 점이 있다면, (4)번 과정이 있었기 때문에 (3)번 문제만 해결되면 거의 끝이라는 것! 이 말은 input, output만 찾으면 끝난다!!!!!!!

1. input 전처리

input을 전처리하는 부분 코드를 보니 사용자가 입력한 텍스트를 어절 단위로 쪼갠 뒤, vocab를 통해서 정수로 바꿔주었다. 그런데 이 방식이 모델 학습시 정수인코딩할 때 하는 Tokenizer와 같았다. 따라서 tokenizer의 내장함수를 이용해서 vocab파일을 만들었다.
그리고 input데이터에 padding이 포함되어있어서 (input+padding)전체 길이를 받아 맞춰주었다.

2. output 찾기

output의 형태를 미리 알아야 하는 이유는 tflite.run(input, output); 로 파라미터를 넣어서 호출한다. 따라서 input을 넣어 추론한 결과가 output에 저장되어 나온다.

제일 먼저 파이썬에서 tflite로 변환한 뒤, output detail을 출력을 시도했다.
그런데 contrib가 없다는 에러 발생!

contrib is not inside TF 2.0. object_detection should use TF 1.15 for now

2.0 부터는 포함 안 된다는 뜻. 간단하다. tf.contrib.litetf.lite 로 바꿔주면 끝!

output detail 프린트결과

[{'name': 'Identity', 'index': 126, 'shape': array([], dtype=int32), 'shape_signature': array([], dtype=int32), 'dtype': <class 'numpy.float32'>, 'quantization': (0.0, 0), 'quantization_parameters': {'scales': array([], dtype=float32), 'zero_points': array([], dtype=int32), 'quantized_dimension': 0}, 'sparsity_parameters': {}}]

흠.. dtype을 봤을 때 float로 된 배열이라는 것만 알겠다..

그렇게 삽질만 하다가 오프라인 미팅을 정하기 위한 마지막 전체 행아웃 회의를 했다. 끝나고 준원님과 행아웃에 남아서 input, output을 찾기 시작했다.

예제 프로젝트를 분석하다가 알게 되었는데, 안드로이드에서 인터프리터를 통해 나오는 output은 각 라벨에 대한 정확도이다. 예제 같은 경우 라벨이 2개인데, [[0.35 0.65] [0.53 0.47] ...] 이런 형식으로 output에 저장되었다. (ex. 라벨1정확도 = 0.35, 라벨2정확도 = 0.65)이때 두번째 데이터부터 패딩이라면 output[0]만이 필요한 부분이다. output[1]부터는 패딩에 대한 결과이기 때문이다. (input으로 넣어주는 데이터의 갯수마다 다르다. 만약 input이 다섯개까지 의미 있는 데이터라면, output[5]부터 패딩에 대한 결과다. 우리 모델은 첫번째 데이터 빼고 다 패딩값이다. 그냥 input과 매칭된다 생각하면 됨! LSTM의 many-to-many를 사용해서 그렇다.)

암튼 파이썬 코드에서 예측값, 실제값을 출력하는 코드를 보다가 준원님과 출력했다.

y_predicted = model.predict(np.array([X_test[i]]))

print(y_predicted)의 결과 ㅠㅠ

[[[0.11467472 0.11449986 0.11310288 0.11348396 0.11331331 0.10842343 0.10740846 0.10839051 0.1067029 ] [0.11396125 0.11315132 0.11211114 0.1129631 0.11216038 0.10940797 0.10892657 0.10931582 0.10800253] ... ]]

애자일이 해냈다 ㅠㅠㅠ 준원님 화면을 공유해서 같이 봤는데, 출력 되는 순간 뇌에 소름이 돋았다. 아직까진 생생하다. 우뇌였다.

X_test[i]도 출력해서 1)할 때 반영했다.

3. 실행 그리고 성공

또 또 또 실행이 안 됐다. (3)번 에러 발생.. 무엇이 문제일까 하다가 android에 있는 labels에는 라벨이 7개인데 파이썬에선 8개인게 발견되었다. 'type'도 들어갔기 때문..! 라벨파일에 type도 추가해주었다.

또한, 학습 코드에서 tag_size = len(tar_tokenizer.word_index) + 1 이 부분이 있기 때문에 labels.size() + 1 해줬다. ('00V'가 추가돼서 tag개수에 +1 해준 것) 성공적⭐️

최종 input, output이다. (MAX_LEN == 패딩을 포함한 인풋길이)
스크린샷 2020-05-23 오전 2 16 13
그랬더니 드디어!! run함수를 지나갔다!!!
스크린샷 2020-05-23 오전 2 19 21
이 순간을 담은 slack 캡쳐

그렇게 애자일 신봉자가 되었습니다.

3) 문장에서 라벨 추출

프로젝트에서는 필요한 부분이지만, 모델을 안드로이드에 올린 이후라 생략하겠습니다.

끝낸 당시 적은 글

우와... 됐다...해냈다...
해낼 줄 몰랐는데........
이게 되네....?


4) 마치며

(1) 중간 후기 편지

끝까지 끈기 있게 모델을 만들어 준 파이썬 장인 준원님께 고맙습니다. 스스로가 자신감이 많이 떨어졌는데 덕분에 힘내서 마지막까지 할 수 있었습니다. 멘탈이 터졌을지도 몰라요. 멘토님께도 감사합니다. 초반에 많이 질문을 안 해서 아쉽습니다. '1시간'으로 제한도 걸어 주셨는데 '조금만 조금만' 하다가 시간을 많이 날리기도 했습니다. 그리고 순서의 중요성을 느꼈습니다. (멘토님은 코딩계의 백종원...) version 2 회고를 못했을 때 해주신 말씀에서 많이 느꼈고 이후 좀 더 원활한 커뮤니케이션이 됐던 것 같아요. 그리고 목표를 이루게 되어서 지난 10일간의 공부와 행동들이 굉장히 의미있는 시간이 되었습니다. 멘토님은 코알라nlp로 전향하지 않게 방향을 잡아주시고, 준원님은 모델개발에 성공해주셔서 감사합니다🔥

(2) 기억할 것

  • 차근차근
  • 알고 쓰자
  • 질문을 많이하자 (모르면 물어봐라)
  • 고민사항도 얘기하자

🔜모든 내용은 다음 게시물에 계속

© 2020 euzl. from JunhoBaik's, Built with Gatsby