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에 이미지 파일 넣는 방법

넷플릭스

이제 한 달 조금 넘었다.
무료로 볼 수 있는 기간이 지났고
4명 동시 재생할 수 있는 상품으로 14,500원 결제가 발생했다.
가족들과 동생까지 해서 2개 갖고는 부족할 것 같아서 2,500원을 추가했다.

사업을 다시 할 것인지 말 것인지 고민을 했던 것 같다.
노는 걸 너무 좋아하는데
일을 할 수 있을까,하는 부분에서 걱정이 되지 않을 수 없었다.

올해 처음으로 서핑을 왔다.
죽도 해변의 망고서프.
도미토리에 나 말고 아무도 없다.
바다에도 나 말고 간혹 한 명 정도가 눈에 띄다가 없다가 그런다.

놀면서 일을 할 수 있을까?
어제 오늘 이틀째 지내고 있는데.. 가능하다는 판단이 섰다.
나의 경우에 한해서.

오전에 2시간, 오후에 2시간 서핑하고
중간에 2시간, 저녁 먹고 3~4시간 정도 공부를 했다.
보통의 근무 시간인 8시간을 맞추는 것은 어렵지만
놀러와서까지 8시간을 한다면 사람이기를 포기하는 것은 아닐지..

8시간을 채우는 것 또한 가능해 보이긴 하는데
따뜻한 바람도 맞아야 하고
멍하니 바다 위의 구름도 보아야 하고
오른편에 위치한 죽도정에도 올라야 하고
서늘해지면 동네도 한바퀴 돌아야 하는데..

다만 도시처럼 노트북으로 일하기에 편한 장소가 많지 않고
서핑 후에 몸이 너무 힘들다는 점은 아쉽고 해결할 과제이기도 하다.
몸이 힘든 점은 익숙함의 문제일 수도 있을 것 같다.
이 부분은 지속적으로 관찰이 필요하다.

일과 놀이를 병행함에 있어 가장 중요한 점은
책임이 아닐까 싶다.
새벽 두 시까지 공부를 할 수 있었는데
어느 정도는 해놓아야 다음 날 마무리를 지을 수 있기 때문이었다.

제목을 넷플릭스라고 해놓고 엉뚱한 얘기만 늘어놓았다.
실리콘밸리에 전설처럼 내려오는 문서가 있다.

"넷플릭스의 문화 : 자유와 책임"
이번에 하는 사업에서는 꼭 행복하고 싶었고
내가 행복한 만큼 함께 일하는 사람에게도 행복을 보장할 생각이었다.
막연히 알고 있던 넷플릭스의 문서를 정독했고
우재와 서진이한테 공동 발표를 시켜서 애들도 알 수 있도록 했다.

내가 생각했던 행복은 "자유와 책임"에 있었다.
자유를 가지는 것은 당연하고
그에 따른 책임을 지는 것도 당연하다.
어른이 됐고 그걸 모를 사람이 어디 있겠는가.

회사가 성공하는 것에 목표를 두고
누릴 수 있는 만큼 자유로운 회사를 만들고 싶다.
오전 서핑 후에 공부하는 도중에
바깥 풍경이 너무 좋아 뭐라도 써야할 것 같아서..

나무 사이로 살짝 바다가 보이고,
왼쪽 승용차 위에 있는 것이 나의 첫 번째 서핑보드다.
오늘은 구름 없이 하늘이 유난히 파란 날이다.

망고서프에서

 

'스타트업' 카테고리의 다른 글

넷플릭스  (0) 2019.04.17
출사표  (0) 2019.04.12

29. 플러터 : 앨범 + 카메라

수업도 하고 창업 준비도 하고..
그리고 놀기도 해야 하고..
하루가 길지 않음을 새삼 느끼게 된다.

플러터를 사용하는 기본적인 코드조차 머리 속에 남아있지 않음을 느끼며
이건 빨리 우재 시켜야겠다는 생각이 든다.
딥러닝 모델을 플러터에 얹으려니
카메라 사용을 먼저 처리하지 않으면 아무 것도 할 수 없어서
어쩔 수 없이 시간을 내야 했다.
머리가 너무 아프다.

앱을 구동하면 나타나는 화면.
사진도 카메라도 없기 때문에 'EMPTY' 문자열을 표시했다.
갤러리에서 사진을 선택해서 가져오면 오른쪽과 같은 모양이 된다.
이때 안드로이드 에뮬레이터 또는 아이폰 시뮬레이터에서 가져와야 하기 때문에
갤러리 또는 앨범에 사진이 들어있어야 한다.
에뮬레이터에 사진을 추가하는 방법은 다른 사이트를 참고하도록 한다.

카메라 버튼을 선택하면 촬영 모드로 들어가고
촬영한 이후에는 갤러리에서 가져오는 것과 동일하다.
실물이 훨씬 낫다.

잘 봤겠지?
이제부터 앞에서 본 것처럼 훌륭한 앱을 직접 만들어 보자.
약간 업된 것 같은 이유는
앱을 코딩했을 때는 도서관이었지만
이 글을 쓰는 곳은 바닷가이기 때문이다.
서핑하러 인구해변에 왔는데
역스웰이래나 뭐래나.. 그냥 파도가 없을 뿐이다.

프로젝트를 생성하고
pubspec.yaml 파일을 열어서 image_picker 모듈을 가져온다.
dependencies 영역 마지막에 'image_picker:'라고 쓴다.
버전을 생략하면 최신 버전을 가져온다는 뜻이다.

dependencies:
  flutter:
    sdk: flutter

  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^0.1.2

  # 최신 버전 사용하도록 버전 명시하지 않음
  image_picker:

 

main.dart 파일을 작성한다.
앞서 봤던 것처럼 처음에 문자열을 표시했다가
사진을 선택하면 해당 사진으로 바꾼다.

우리가 할건 없고 ImagePicker 라이브러리에
갤러리(ImageSource.gallery) 또는 카메라(ImageSource.camera)를 전달하기만 하면 된다.

import 'dart:io';

import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';    // 앨범과 카메라 모두에 대해 작동


class ImageAndCamera extends StatefulWidget {
    @override
    ImageAndCameraState createState() => ImageAndCameraState();
}


class ImageAndCameraState extends State<ImageAndCamera> {
    // 파일 경로 문자열은 카메라에서는 에러 발생했다. image_picker 모듈에서 File 객체 반환.
    File mPhoto;
    
    @override
    Widget build(BuildContext context) {
        Widget photo = (mPhoto != null) ? Image.file(mPhoto) : Text('EMPTY');

        return Container(
            child: Column(
                children: <Widget>[
                    // 버튼을 제외한 영역의 가운데 출력
                    Expanded(
                        child: Center(child: photo),
                    ),
                    Row(
                        children: <Widget>[
                            RaisedButton(
                                child: Text('앨범'),
                                onPressed: () => onPhoto(ImageSource.gallery),  // 앨범에서 선택
                            ),
                            RaisedButton(
                                child: Text('카메라'),
                                onPressed: () => onPhoto(ImageSource.camera),   // 사진 찍기
                            ),
                        ],
                        mainAxisAlignment: MainAxisAlignment.center,
                    ),
                ],
                // 화면 하단에 배치
                mainAxisAlignment: MainAxisAlignment.end,
            ),
        );
    }

    // 앨범과 카메라 양쪽에서 호출. ImageSource.gallery와 ImageSource.camera 두 가지밖에 없다.
    void onPhoto(ImageSource source) async {
        // await 키워드 때문에 setState 안에서 호출할 수 없다.
        // pickImage 함수 외에 pickVideo 함수가 더 있다.
        File f = await ImagePicker.pickImage(source: source);
        setState(() => mPhoto = f);
    }
}

void main() {
    runApp(MaterialApp(
        home: MaterialApp(
            title: '앨범 + 카메라',
            home: Scaffold(
                appBar: AppBar(title: Text('앨범 + 카메라'),),
                body: ImageAndCamera(),
            )
        )
    ));
}

출사표

기지개를 켠다.

IT 10년 주기설을 믿고
그에 맞게 사고하며 행동하며 살았다.
딥러닝 세상이 열릴 것을 벌써 알았지만
배드민턴 치며 노느라
애써 세상의 흐름을 무시하며 살았다.

이제 일어날 때도 되지 않았겠는가?
몇 년 놀았고
부족하지만 대략적인 준비도 했고.

세상 무서운 것을 알고 있다.
무섭다고 나서지 못할 소심함이 내겐 없다.
평이하고 나른한 즐거움은
나에게는 행복으로 다가오지 못한다.

인생이란
올라가는 것이 있으면 내려가는 것도 있어야 하고
올라가지 못할 것처럼 느껴지는 시기도
내려가지 않을 것 같은 시기도 있어야 한다.

강사로서 최고의 시기를 보내면서
금전적으로 넘쳐나고
시간적으로도 넘쳐나는 일을 뒤로 한다는 것이
주변에 미안한 마음과 더해
부담이 된다.

그러나 이런 정도의 부담보다는
배드민턴, 서핑, 스노보드, 패러글라이딩까지..
새로 시작한 행복과
멀어지면 어쩌나 하는 두려움이 가장 크다.

나를 힘들게 하는 것이 두려운 것이 아니라
누리던 것을 못 누리는 것도 아니고
덜 누릴 수밖에 없는 것에 두려움을 느낀다.
애나 어른이나 놀 때 가장 행복하다.

첫 번째 사업은 실패했고
빚과 그래선 안 된다는 교훈을 얻었다.
두 번째 사업을 시작하려 하고
세 번째 기회가 있을까,라는 걱정을 조금 한다.

스타트업이 아니라 스타트다운이 되는 것은 아닐까?
난 이런 말장난이 너무 좋다.
덕분에 심각한 얘기를 어떻게 끝내야 할지 놓쳤다.

그럼에도
스타트업이라는 형태의 사업을 다시 한다는 사실은 달라지지 않는다.
전에 했던 회사,
아직까지 갖고 있는 법인의 이름은 글루소프트이다.
아주 끈끈한 회사를 만들고 싶었었다.

'스타트업' 카테고리의 다른 글

넷플릭스  (0) 2019.04.17
출사표  (0) 2019.04.12

윈도우10 + 텐서플로 GPU 버전 설치

얼마만인지 모르겠다.
블로그에 올린 많은 글들 중에
나름 봐줬으면 하는 글들이 꽤 있는데
통계를 보면 우분투에 텐서플로 GPU 버전을 설치하는 글이 가장 인기가 많다.
왜 그런지는 모르겠다.
아직 그 부분이 해결되지 않았나?

엔비디아 1060에 GPU 버전을 설치해서 쓰고 있었는데
애들 핑계로 1070을 구매했고 지나가는 길에 설치를 시도했는데.. 잘 되지 않았다.
큰 아들 우재한테 시켰더니
며칠째 끙끙대기만 하고 결과가 나오지 않는다.

첫 번째 설치는 당연히 실패하고
여기저기 찾아다닌 끝에 3시간만에 성공했다.
그 후에 이 방식이 맞는지 검증하기 위해 다시 3시간을 보냈다.

설치 환경을 요약하면 아래와 같다.

- 운영체제 : 윈도우 10
- 그래픽카드 : 엔비디아 1070 TI
- 드라이버 : 419.67
- CUDA : 10.0
- cudnn : 10.0
- 파이썬 : 3.6.8
- 텐서플로 GPU : 1.13.1

 

2019년 3월 31일 현재 최신 버전은 CUDA 10.1 버전이다.
그런데.. 설치가 되질 않는다.
10.0을 설치하는 방법과 똑같이 진행을 했는데.. 실패다.
대여섯번 하다가 안 되면 그만할 줄도 알아야 한다.
컴퓨터 2대에서 시도했고, 아쉽지만 이번에는 10.0에서 만족하기로 했다.

파이썬은 미리 설치되어 있다고 가정한다.
엔비디아 그래픽 카드 드라이버는 
기본 설치 버전이 있는 상태에서 최신 버전인 419.67 버전을 설치했더니
자연스럽게 기본 버전은 제거가 됐다.

 

윈도우10에 설치된 엔비디아 그래픽 카드 프로그램 목록

 

얼마나 실패를 할지 몰라
이번에도 단단히 준비를 하고 시작했다.
GPU 버전 설치는 어찌 보면 다양한 버전들로부터 궁합이 잘 맞는 버전을 찾는 문제일 수 있다.

다운로드 받아 놓은 설치 파일들

 

설치 파일을 다운로드할 수 있는 사이트를 정리했다.
맨 앞의 텐서플로 공식 사이트에서 말하는 내용 정도는 보는 것이 좋겠다.
최신 버전을 설치하려면
시도한 사람이 많지 않은 상태에서는 부족하긴 하지만 공식 문서가 최신일 수도 있다.

 

 

설치 파일을 다운로드하는 화면은 넣지 않았다.
운영체제 버전 선택하는 그런 것들이 필요해 보이지는 않는데..
다만 설치 환경이 모두 다를 수 있기 때문에
자신의 환경에 맞는 버전을 정확하게 선택해야 할 것이다.

 

그래픽 카드 드라이버 다운로드

 

GPU 버전 설치는 CUDA와 cuDNN 설치가 전부일 수 있다.
가장 최신의 버전인 CUDA 10.1은 설치할 수 없어서 10.0 버전을 설치했다고 앞에서 얘기했다.

 

1. CUDA 10.0 설치
그림 상단에 버전 번호가 표시되어 있다. 
별도 옵션을 선택하는 것은 필요 없었다. 기본값인 [빠른 설치] 선택하고 [다음] 버튼으로 진행한다.
설치가 완료되고 나면 창을 닫는다.

 

CUDA 10.0 설치

 

2. cuDNN 복사
cuDNN 라이브러리는 압축 파일로 되어 있고
압축을 풀면 3개의 폴더가 나오고 그 안에는 각각 1개씩의 파일이 들어 있다.

일반적으로는 이들 파일은 CUDA 라이브러리가 설치된 폴더에 복사해서 덮어써야 하는데
어떤 블로그에서 덮어쓰는 것보다 복사해서 사용하는 것을 추천해서 나도 그렇게 했다.
덮어써도 될 수 있기는 한데
잘못될 경우를 대비해서 원본 파일을 보관해야 하는데
그것보다 cuDNN 폴더를 복사하는 것이 훨씬 쉽다.

cuDNN 라이브러리는 CUDA 라이브러리와 똑같은 버전을 사용해야 한다.
CUDA가 10.0이니까 cuDNN도 10.0을 사용한다.
압축을 푼 폴더에 가면 그 안에 cuda 폴더가 있고 그 안에 bin, include, lib 폴더가 있다.
cuda 폴더를 바탕화면에 복사한다.
이 시점에서 특별한 폴더를 찾을 생각하지 말고
설치에 성공하고 나면 원하는 폴더로 옮기는 것은 어렵지 않다.

 

3. 환경변수에 경로 추가
cuda 폴더를 덮어쓰지 않았기 때문에
cuDNN 파일들에 대한 경로를 환경에 추가해야 한다.

[내 PC] - [속성] - [고급 시스템 설정]으로 들어간다.

 

[환경 변수] - [사용자 변수] - [Path] - [편집]으로 들어가서
바탕화면에 추가된 bin, include, lib 폴더의 경로를 추가한다.

앞의 그림 중에 가운데 그림에 보면
[시스템 변수]에 CUDA_PATH 항목이 보이는데, CUDA 라이브러리 설치가 잘 되었다는 뜻이다.

 

4. 가상환경 생성
파이썬 가상환경을 반드시 만들어야 하는 것은 아니지만
잘 안될 수도 있고 CPU와 GPU 버전을 함께 사용해야 할 수도 있으니까.

커맨드 창을 연다. 관리자 모드로 열지 않아도 된다.
가상환경을 여러 번 구축해야 할 수도 있으니까 폴더 이름 뒤에 숫자를 붙였다.

가상환경 생성 및 진입

 

순서대로 아래 명령어를 입력한다.
python -m venv tf_1
cd tf_1
Scripts\activate.bat

 

5. 텐서플로 설치
activate 명령을 통해 가상환경으로 들어갔다.
여기서부터는 나만의 파이썬을 사용한다고 생각하면 된다.

텐서플로 GPU 버전을 설치하는 명령을 입력한다.
버전을 명시하지 않았기 때문에 현재 시점에서 최신 버전인 1.13.1을 설치한다.


pip install tensorflow-gpu

텐서플로 설치 중..

 

설치를 마치고 나면 노란색 글자가 보여 걱정이 된다.
pip 모듈을 업데이트하라는 말인데.. 텐서플로 GPU 버전하고는 상관없다. 하지 않아도 된다.

텐서플로 설치 완료

 

6. 설치 확인
python이라고 입력해서 파이썬 인터프리터를 구동한다.
갈매기(>>>) 프롬프트가 보이면 딱 한 줄만 입력하면 된다.

>>> import tensorflow

이 코드 한 줄에 대해 에러가 나지 않으면 성공이다.
그래픽 카드 버전을 뜻하는 빨간색의 메시지는 나타나지 않는다.
나중에 코드를 더 넣고 구동하게 되면 그때는 볼 수 있다.

이 한 줄의 코드로 안심이 되지 않는다면
파이참 등의 에디터를 통해 텐서플로 코드를 구동해 본다.
나는 mnist를 비롯해서 고양이와 강아지도 돌려봤다. 
너무 감격적이다.


많은 글을 읽었는데
성공의 핵심은 cuDNN 라이브러리를 바탕화면에 복사해서 사용한 것이 아닐까 생각한다.
별별정보님께서 올려놓은 [tensorflow gpu install - NVIDIA CUDA 설치하기]를 참고했다.

추가로 될거라고 생각했던 것도 있는데 CUPTI와 관련된 것이다.
텐서플로 공식 페이지에서 이 부분에 대해 언급했기 때문에 어떤 식으로든 설치에 관여할 것이라 생각했다.
[Windows 10에서 tensorflow-gpu 설치]에 설명되어 있고
혹시 앞의 과정에서 에러가 난다면 시도해 볼 수 있는 방법이다.

하나 더.
1.13 버전이 나온지 얼마 안되었기 때문에
설치 관련 글을 읽어보면 CUDA 9.0 또는 9.2 버전에 대한 얘기가 많다.
10.0 설치가 잘 되지 않았다면
굳이 10.0을 고집할 필요는 없어 보인다.

처음에 CUDA 9.0 버전 설치로 성공했는데
10.0 버전도 되는지 확인하고 싶었고 성공했다.
다만 9.0 버전을 설치할 때는 반드시 텐서플로 1.12 버전을 사용해야 한다.
텐서플로를 설치할 때 버전 번호를 명시하면 된다.

pip install tensorflow-gpu==1.12

수업을 해야 해서
내가 가장 많이 볼 거라고 생각하고 만들었다.
새벽 1시 30분.
졸려서 혹시 빠진 내용이 있을지도 모르겠다.
확인은 내일 하는 걸로.

 

5. 텐서플로 라이트 : 안드로이드 + AND 모델 (2)

어제 저녁부터 해서 밤 늦게까지 했고
오늘 아침부터 했는데.. 벌써 점심 먹을 때가 됐다.
하고 나면 별거 아닌데..
왜 이렇게 많은 시간이 걸리는 건지..

안드로이드 프로젝트의 이름은 LogicalAnd로 했다.
프로젝트 생성 및 환경 설정은 이전 글을 참고하자.

파이썬에서 만든 모델 파일 2개를 assets 폴더로 옮겼을려나..?
그리고 해야 할 것이
텐서플로 라이트 라이브러리 관련 코드를 build.gradle 파일에 추가하는 것을 잊어먹으면 안된다.
내가 만든 문서가 생각이 안나서
다시 보면서 프로젝트를 하고 있는데
build.gradle 파일에 코드 추가하는 것을 잊어먹어서 30분 날려먹었다.
애초 지금처럼 써놨어야 했는데..

xml 파일의 내용을 아래 코드로 대체한다.
이전 예제하고 비슷한데, 텍스트뷰의 높이를 높였고 버튼을 1개 줄였다.

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:orientation="vertical">

<TextView
android:id="@+id/tv_output"
android:text="결과 출력"
android:textSize="23dp"
android:gravity="center"
android:layout_width="match_parent"
android:layout_height="200dp" />

<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">

<Button
android:id="@+id/button_1"
android:text="hx only"
android:textSize="19dp"
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="100dp" />

<Button
android:id="@+id/button_2"
android:text="hx multi"
android:textSize="19dp"
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="100dp" />

<Button
android:id="@+id/button_3"
android:text="hx + logic"
android:textSize="19dp"
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="100dp" />

</LinearLayout>
</LinearLayout>
</FrameLayout>


다음으로 MainActivity.java 파일을 아래 코드로 대체하자.
코드에 대한 설명은 주석에 충분히 했으니까.. 생략한다.

주의 깊게 봐야 할 부분은 입력과 출력 배열 구성이다.
무엇을 생각했건.. 그보다 복잡해서 고생했다.

package com.example.logicaland;

import android.app.Activity;
import android.content.res.AssetFileDescriptor;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;

import org.tensorflow.lite.Interpreter;

import java.io.FileInputStream;
import java.io.IOException;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;

public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

// hx 연산 1개만 처리 (and_model_for_hx_only.tflite 파일)
findViewById(R.id.button_1).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
try {
// {{0}, {0}}는 파이썬에서의 [0, 0]과 같다.
// 이전 예제에서 보면 [0, 0]를 처리하기 위해 {{0}, {0}}를 사용했었다.
// 그래서 복잡하긴 하지만 3차원의 형태가 되어야 한다.
float[][][] input = new float[][][]{{{0}, {0}}, {{0}, {1}}, {{1}, {0}}, {{1}, {1}}};

// hx 연산은 0과 1 사이의 실수를 반환하기 때문에 float 타입으로 선언
float[][] output = new float[][]{{0}, {0}, {0}, {0}};

Interpreter tflite = getTfliteInterpreter("and_model_for_hx_only.tflite");
tflite.run(input, output);

// 마지막 매개변수는 button_3에서만 사용.
TextView tv_output = findViewById(R.id.tv_output);
tv_output.setText(makeOutputText("hx single", output, null));
} catch (Exception e) {
e.printStackTrace();
}
}
});

// hx 연산 1개를 다중 출력으로 처리 (and_model_for_hx_only.tflite 파일)
findViewById(R.id.button_2).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
try {
float[][][] input = new float[][][]{{{0}, {0}}, {{0}, {1}}, {{1}, {0}}, {{1}, {1}}};
float[][] output = new float[][]{{0}, {0}, {0}, {0}};

// 3차원이 여러 개 있어야 하니까 4차원
float[][][][] inputs = new float[][][][]{input};

// 출력이 1개라서 요소도 1개만 넣는다.
java.util.Map<Integer, Object> outputs = new java.util.HashMap();
outputs.put(0, output);

Interpreter tflite = getTfliteInterpreter("and_model_for_hx_only.tflite");
tflite.runForMultipleInputsOutputs(inputs, outputs);

TextView tv_output = findViewById(R.id.tv_output);
tv_output.setText(makeOutputText("hx multi", output, null));
} catch (Exception e) {
e.printStackTrace();
}
}
});

// hx와 logics 연산 2개 처리 (and_model_for_hx_logics.tflite 파일)
findViewById(R.id.button_3).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
try {
// 자료형을 Object로 바꾸면 좋은데 makeOutputText 함수를 구성하기 어려워져서 생략.
float[][][] input = new float[][][]{{{0}, {0}}, {{0}, {1}}, {{1}, {0}}, {{1}, {1}}};
float[][] output_1 = new float[][]{{0}, {0}, {0}, {0}};
int[][] output_2 = new int[][]{{-1}, {-1}, {-1}, {-1}};

// 4차원은 복잡하니까 float[][][]를 Object 타입으로 처리하면 간편.
Object[] inputs = new Object[]{input};

// 출력 연산 개수에 맞게 출력 배열 추가 (hx와 logics 2개)
java.util.Map<Integer, Object> outputs = new java.util.HashMap();
outputs.put(0, output_1);
outputs.put(1, output_2);

// 이전 함수와 모델 파일이 달라졌다.
Interpreter tflite = getTfliteInterpreter("and_model_for_hx_logics.tflite");
tflite.runForMultipleInputsOutputs(inputs, outputs);

// makeOutputText 함수의 마지막 매개변수는 여기서만 사용. 이전 함수는 출력 결과가 하나뿐.
TextView tv_output = findViewById(R.id.tv_output);
tv_output.setText(makeOutputText("hx + logics", output_1, output_2));
} catch (Exception e) {
e.printStackTrace();
}
}
});
}

// output은 4행 1열. 4행이라는 것은 4개의 데이터에 대해 예측했다는 뜻.
private String makeOutputText(String title, float[][] output_1, int[][] output_2) {
String result = title + "\n";
for (int i = 0; i < output_1.length; i++)
result += String.valueOf(output_1[i][0]) + " : ";

// button_3에서만 사용하는 코드. 출력 2개는 button_3에만 있다.
if(output_2 != null) {
result += "\n";
for (int i = 0; i < output_2.length; i++)
result += String.valueOf(output_2[i][0]) + " : ";
}

return result;
}

private Interpreter getTfliteInterpreter(String modelPath) {
try {
return new Interpreter(loadModelFile(MainActivity.this, modelPath));
}
catch (Exception e) {
e.printStackTrace();
}
return null;
}

private MappedByteBuffer loadModelFile(Activity activity, String modelPath) throws IOException {
AssetFileDescriptor fileDescriptor = activity.getAssets().openFd(modelPath);
FileInputStream inputStream = new FileInputStream(fileDescriptor.getFileDescriptor());
FileChannel fileChannel = inputStream.getChannel();
long startOffset = fileDescriptor.getStartOffset();
long declaredLength = fileDescriptor.getDeclaredLength();
return fileChannel.map(FileChannel.MapMode.READ_ONLY, startOffset, declaredLength);
}
}


텐서플로 라이트는 왜 여러 개의 연산을 처리할 수 있는 쉬운 코드를 제공하지 않았을까?
내가 내린 결론은 의미가 없어서.

모델이라고 하는 것은
특정한 기능 하나를 수행하는 것을 전제로 한다.
회귀도 하고, 분류도 할 수 있는 모델이 의미가 있을까?
정답은 연속된 숫자 아니면 범주형 데이터이지.. 두 가지 모두가 될 수는 없다.

하나의 모델에 회귀와 분류를 같이 넣을 수 있을까?
있다!
학습을 각각 시키고 각각의 기능을 제공하는 연산을 출력으로 전달하면 된다.
그러나, 이렇게 했을 때
회귀 연산만을 추출해서 사용할 수 있는 방법은 없다.
안드로이드 API를 모두 살펴봤지만 그런 함수는 존재하지 않았다.
결국은 출력에 전달된 모든 연산을 수행하고 나서
필요한 출력 결과만 사용해야 하는데.. 너무 바보같은 짓이 되고 만다.

그렇지만 안드로이드 앱에서는 두 가지 기능을 모두 제공할 필요가 있다.
이걸 쉽게 처리하는 방법은
이번 예제에서처럼 회귀 모델과 분류 모델을 각각 제공하고
눌린 버튼에 따라 다른 모델을 사용하면 된다.
인터페이스도 깔끔해지고 사용하는 개발자도 쉽다.

에고.. 힘들었다.
논리 연산에 대한 원본 소스는 아래 사이트에서 가져왔다.
케라스 버전으로 되어 있는데, 함께 보면 좋겠다.

XOR 논리값 모델

XOR 안드로이드 앱

4. 텐서플로 라이트 : 안드로이드 + AND 모델 (1)

앞에서 안드로이드와 연동하는 기초를 꼼꼼하게 챙겼다고 생각했다.
너무 쉽게 생각한 것 아닌가 하는 생각이 들었다.

여기서는 AND 논리 연산을 모델로 꾸며서 안드로이드와 연동한다.
FC 레이어를 사용하는 진짜 딥러닝 모델이기 때문에
이번 예제를 처리할 수 있다면
그 어떤 모델이라도 연동할 수 있다고 생각한다.
그렇지만..
이번에도 나만의 착각일 수 있다.

이번 예제를 통해 공부하려고 했던 것은
모델에 여러 개의 연산이 존재할 때 안드로이드에서 개별적인 호출이 가능한지, 였다.
결론부터 말하자면
할 수는 있지만, 그래야 할 이유가 전혀 없다.

최종적인 출력은 다음과 같다.
얼핏 보면 같아보일 수 있는데.. 각각 해당 버튼을 누른 결과이다.
차이를 주기 위해 상단 제목을 모두 다르게 처리했다.


출력된 실수가 모두 같은 것은
학습이 끝난 모델에 동일한 데이터로 결과를 예측하기 때문이다.
마지막 화면은 시그모이드 연산과 시그모이드를 논리값으로 바꾸는 연산의 결과를 보여준다.

파이썬에서 모델 파일을 2개 만든다.
첫 번째는 출력이 하나일 때 사용하고, 두 번째는 출력이 두 개일 때 사용한다.
이번 코드에서 중요한 것은 입력이 복잡해졌고, 두 번째 출력을 담당하는 logics 연산이다.

import tensorflow as tf
import numpy as np


def make_model(model_path, sess, inputs, outputs):
# inputs와 outputs가 여러 요소를 갖는다면 다중 입출력이 된다.
converter = tf.lite.TFLiteConverter.from_session(sess, inputs, outputs)
flat_data = converter.convert()

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


# and 논리값.
# [0, 0] => [0]
# [1, 1] => [1]
x = [[0, 0], [0, 1], [1, 0], [1, 1]] # np로 변환하지 않아도 됨
y = np.float32([[0], [0], [0], [1]]) # 변환하지 않으면 z와 타입 불일치 에러

# 2와 1을 변수로 대체하면 지저분해지기 때문에 생략
# n_features, n_classes = len(x[0]), len(y[0]) # 2, 1

holder_x = tf.placeholder(tf.float32, shape=[None, 2])

w = tf.Variable(np.random.rand(2, 1), dtype=tf.float32)
b = tf.Variable(np.random.rand(1), dtype=tf.float32)

# (4, 1) = (4, 2) @ (2, 1)
z = tf.matmul(holder_x, w) + b # fully connected 레이어
hx = tf.nn.sigmoid(z) # 활성 함수

# 정수 형변환. 텐서플로 연산이기 떄문에 모델에 포함되고, 안드로이드에서 사용할 수 있다.
# hx는 실수 배열, logics는 정수 배열. 일부러 출력 결과를 다르게 만들었다.
logics = tf.cast(hx >= 0.5, dtype=tf.int32)

loss = tf.nn.sigmoid_cross_entropy_with_logits(labels=y, logits=z)
train = tf.train.GradientDescentOptimizer(0.1).minimize(loss)

with tf.Session() as sess:
sess.run(tf.global_variables_initializer())

for i in range(100):
sess.run(train, {holder_x: x})

# print(sess.run([hx, logics], {holder_x: x}))
# hx : [[0.04933239], [0.22989444], [0.24103162], [0.64626104]],
# logics : [[0], [0], [0], [1]]

# 출력 1개인 모델과 2개인 모델을 별도 생성
make_model('and_model_for_hx_only.tflite', sess, [holder_x], [hx])
make_model('and_model_for_hx_logics.tflite', sess, [holder_x], [hx, logics])


make_model 함수를 만들어서
첫 번째는 [hx], 두 번째는 [hx, logics]를 전달했고 파일 이름을 다르게 처리했다.

이번 글은 여기까지 하고
안드로이드 프로젝트는 다음 글에서 만들어 본다.

3. 텐서플로 라이트 : 안드로이드 기초(3)

안드로이드 기초의 마지막 부분이다.
시뮬레이터에서 앱을 구동해서 모델과 연동하는 마지막 코드까지 간다.


8. tflite 모델 파일을 로딩하고 run 함수를 호출해서 결과를 가져온다.
MainActivity.java 파일을 연다.
안드로이드가 익숙하지 않다면 아래 화면이 도움이 될 것이다. ^^


별 내용 없다.
파일 전체를 아래 코드로 대체한다.
붙여넣기를 할 때, 꼭대기에 패키지 이름이 있는데
여기서 설명하는 이름과 같지 않다면 첫 번째 줄은 복사하지 않는다.

package com.example.simplelite;

import android.app.Activity;
import android.content.res.AssetFileDescriptor;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;

import org.tensorflow.lite.Interpreter; // 핵심 모듈

import java.io.FileInputStream;
import java.io.IOException;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;

public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

// xml 파일에 정의된 TextView 객체 얻기
final TextView tv_output = findViewById(R.id.tv_output);

// R.id.button_1 : 첫 번째 버튼을 가리키는 id
// setOnClickListener : 버튼이 눌렸을 때 호출될 함수 설정
findViewById(R.id.button_1).setOnClickListener(new View.OnClickListener() {
// 리스너의 기능 중에서 클릭(single touch) 사용
@Override
public void onClick(View v) {
// input : 텐서플로 모델의 placeholder에 전달할 데이터(3)
// output: 텐서플로 모델로부터 결과를 넘겨받을 배열. 덮어쓰기 때문에 초기값은 의미없다.
int[] input = new int[]{3};
int[] output = new int[]{0}; // 15 = 3 * 5, out = x * 5

// 1번 모델을 해석할 인터프리터 생성
Interpreter tflite = getTfliteInterpreter("simple_1.tflite");

// 모델 구동.
// 정확하게는 from_session 함수의 output_tensors 매개변수에 전달된 연산 호출
tflite.run(input, output);

// 출력을 배열에 저장하기 때문에 0번째 요소를 가져와서 문자열로 변환
tv_output.setText(String.valueOf(output[0]));
}
});

findViewById(R.id.button_2).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 입력 데이터 2개 사용. [][]는 2차원 배열을 의미한다.
int[][] input = new int[][]{{3}, {7}};
int[] output = new int[]{0}; // 58 = (3 * 3) + (7 * 7), out = sum(x * x)

Interpreter tflite = getTfliteInterpreter("simple_2.tflite");
tflite.run(input, output);

tv_output.setText(String.valueOf(output[0]));

// 아래 코드는 에러.
// 텐서플로의 벡터 연산을 자바쪽에서 풀어서 계산해야 하는데,
// 구성 요소가 객체 형태로 존재하지 않을 경우 shape이 일치하지 않아서 발생하는 에러
// int[] input = new int[]{3, 7};
//
// 모델을 구성할 때 사용한 코드. x * x는 배열간의 연산이다.
// x = tf.placeholder(tf.int32, shape=[2])
// out = tf.reduce_sum(x * x)
}
});

findViewById(R.id.button_3).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 입력 변수를 별도로 2개 구성
int[] input_1 = new int[]{3};
int[] input_2 = new int[]{7};
int[][] inputs = new int[][]{input_1, input_2};

// 출력은 하나지만, 함수 매개변수를 맞추기 위해 맵 생성
java.util.Map<Integer, Object> outputs = new java.util.HashMap();

// 출력을 받아올 변수 1개 추가
int[] output_1 = new int[]{0}; // 10 = 3 + 7, out = x + y
outputs.put(0, output_1);

Interpreter tflite = getTfliteInterpreter("simple_3.tflite");

// 구동 함수는 run과 지금 이 함수밖에 없다.
// runForMultipleInputsOutputs 함수는 입력도 여럿, 출력도 여럿이다.
// 입력은 입력들의 배열, 출력은 <Integer, Object> 형태의 Map.
// key와 value에 해당하는 Integer와 Object 자료형은 변경할 수 없다.
tflite.runForMultipleInputsOutputs(inputs, outputs);

tv_output.setText(String.valueOf(output_1[0]));
}
});

findViewById(R.id.button_4).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 변수 2개를 전달하는 방법으로 앞에서처럼 해도 되지만 이번 코드가 간결하다.
int[][] inputs = new int[][]{{3}, {7}};

java.util.Map<Integer, Object> outputs = new java.util.HashMap();

// 별도 변수없이 직접 put 함수에 전달하면서 배열 생성
outputs.put(0, new int[]{0}); // 10, 21 = 3 + 7, 3 * 7 : out1, out2 = x + x, y * y
outputs.put(1, new int[]{0});

Interpreter tflite = getTfliteInterpreter("simple_4.tflite");
tflite.runForMultipleInputsOutputs(inputs, outputs);

// 별도로 출력 변수를 정의하지 않았기 때문에 Map 클래스의 get 함수를 통해 가져온다.
// Object 자료형을 배열로 변환해서 사용
int[] output_1 = (int[]) outputs.get(0);
int[] output_2 = (int[]) outputs.get(1);
tv_output.setText(String.valueOf(output_1[0]) + " : " + String.valueOf(output_2[0]));
}
});
}

// 모델 파일 인터프리터를 생성하는 공통 함수
// loadModelFile 함수에 예외가 포함되어 있기 때문에 반드시 try, catch 블록이 필요하다.
private Interpreter getTfliteInterpreter(String modelPath) {
try {
return new Interpreter(loadModelFile(MainActivity.this, modelPath));
}
catch (Exception e) {
e.printStackTrace();
}
return null;
}

// 모델을 읽어오는 함수로, 텐서플로 라이트 홈페이지에 있다.
// MappedByteBuffer 바이트 버퍼를 Interpreter 객체에 전달하면 모델 해석을 할 수 있다.
private MappedByteBuffer loadModelFile(Activity activity, String modelPath) throws IOException {
AssetFileDescriptor fileDescriptor = activity.getAssets().openFd(modelPath);
FileInputStream inputStream = new FileInputStream(fileDescriptor.getFileDescriptor());
FileChannel fileChannel = inputStream.getChannel();
long startOffset = fileDescriptor.getStartOffset();
long declaredLength = fileDescriptor.getDeclaredLength();
return fileChannel.map(FileChannel.MapMode.READ_ONLY, startOffset, declaredLength);
}
}


9. 안드로이드 화면을 구성하고 코드를 추가해서 결과를 표시한다.
화면 구성은 xml 파일에서 한다.
layout 폴더에서 activity_main.xml 파일을 연다.
아래와 같이 "Hello World"가 출력되는 단순한 미리보기가 나타난다.


아래 코드 전체를 activity_main.xml 파일에 덮어쓴다.
레이아웃과 컴포넌트는 안드로이드를 몰라도 대충 이해할 수 있기 때문에 설명은 생략한다.

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:orientation="vertical">

<TextView
android:id="@+id/tv_output"
android:text="결과 출력"
android:textSize="23dp"
android:gravity="center"
android:layout_width="match_parent"
android:layout_height="100dp" />

<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">

<Button
android:id="@+id/button_1"
android:text="입력 1\n출력 1"
android:textSize="19dp"
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="100dp" />

<Button
android:id="@+id/button_2"
android:text="입력 2\n출력 1"
android:textSize="19dp"
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="100dp" />

<Button
android:id="@+id/button_3"
android:text="입력 1+1\n출력 1"
android:textSize="19dp"
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="100dp" />

<Button
android:id="@+id/button_4"
android:text="입력 1+1\n출력 1+1"
android:textSize="19dp"
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="100dp" />

</LinearLayout>
</LinearLayout>
</FrameLayout>


마지막으로 구동하고 나면
아래와 같은 최종 화면을 얻게 되고, 버튼을 누를 때마다 학습한 결과를 볼 수 있을 것이다.


2. 텐서플로 라이트 : 안드로이드 기초(2)

파이썬으로 해야 할 작업은 끝났다.
원래는 시행착오를 거쳐야 하기 때문에
파이썬과 안드로이드를 왔다갔다 하면서 짜증이 엄청 나야 한다.

이번에 할 작업은 안드로이트 프로젝트 구현 전반부이다.
안드로이드 프로젝트 생성부터 gradle 파일 수정까지 진행한다.

3. 안드로이드 프로젝트를 생성한다.
비어있는 프로젝트를 하나 만든다.


프로젝트 이름은 SimpleLite으로 한다.
프로젝트 폴더에 한글이 포함되면 경고가 뜬다. 이름만 입력하고 나머지는 그대로 둔다.

4. assets 폴더를 만들고 모델 파일을 붙여넣는다.
프로젝트를 만들면 assets 폴더가 존재하지 않는다.
폴더가 너무 많아서 위치를 정확하게 잘 찾아야 한다.
왼쪽 상단에 Project가 열려있는 것을 볼 수 있는데, 처음에는 Android라고 되어 있다.
Project로 변경하면 아래 화면을 볼 수 있고 main 폴더까지 가서 만들어야 한다.

이전 글에서 만든 4개의 모델 파일을 assets 폴더로 복사한다.


5. 텐서플로 라이트 모듈을 사용할 수 있도록 gradle 파일에 내용을 추가한다.

gradle 파일을 수정해야 하는데
그림에서 보는 것처럼 build.gradle 파일이 두 가지가 있다.
아래에 있는 Module 버전을 수정한다.


파일을 열어보면 dependencies 영역이 나오는데
텐서플로 라이트 모듈을 마지막에 있는 것처럼 추가한다.

dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.android.support:appcompat-v7:28.0.0'
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'

implementation 'org.tensorflow:tensorflow-lite:+'
}


6. gradle 파일을 수정해서 모델 파일이 압축되지 않도록 한다.
android 영역을 찾아서 마지막에 aaptOptions를 추가한다.
제목에 있는 것처럼 압축을 방지한다.
메모리를 절약하기 위해 리소스를 압축하는데 그럴 경우 모델을 올바로 읽어들일 수가 없다.

android {
... 생략
aaptOptions {
noCompress "tflite"
}
}


7. gradle 파일을 수정했으니까 동기화를 진행한다.
파일을 수정하게 되면 파일 오른쪽 상단에 [Sync Now] 메뉴가 나타난다.
눌러주면 동기화를 진행하고 문제없으면 에러가 표시되지 않는다.

이번 글은 여기까지.

1. 텐서플로 라이트 : 안드로이드 기초(1)

첫 번째로 모델이라고 부를 수도 없는 모델을 안드로이드와 연동한다.
모델이 됐건 연산이 됐건
텐서플로 라이트 입장에선 다를 것이 없다.
너무 간단한 덧셈과 곱셈 연산을 통해
안드로이드로부터 플레이스 홀더 입력을 받아오는 것부터 해보자.

내용이 길어서 여러 개로 나누어서 작업한다.
여기서는 텐서플로 모델을 생성하고 tflite 파일로 변환하는 것까지 진행한다.

  1. PC에서 모델을 학습한다.
  2. 학습한 모델을 텐서플로 라이트 버전으로 변환한다.

안드로이드 앱을 구성하고 나면
최종적으로 아래와 같은 화면이 뜬다.
다양한 입력을 보여주기 위해 4가지 형태로 구성했다.
4개의 버튼 중에서 하나를 누르면 상단에 있는 텍스트뷰에 결과를 보여준다.


코드가 조금 긴데..
4가지 코드를 한번에 보여주기 때문에 그런 것뿐.. 하나하나는 가볍기 그지 없다.
model_common 함수를 중점적으로 봐야 하고
나머지 함수들에서는 입력과 출력이 어떻게 달라지는지를 봐야 한다.

import tensorflow as tf


# 이번 파일에서 공통으로 사용하는 함수.
# 컨버터 생성해서 파일로 저장. 다시 말해 모바일에서 사용할 수 있는 형태로 변환해서 저장.
def model_common(inputs, outputs, model_path):
# 텐서플로 API만을 사용해서 저장할 수 있음을 보여준다.
# 4가지 방법 중에서 가장 기본.
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())

# input_tensors: List of input tensors. Type and shape are computed using get_shape() and dtype.
# output_tensors: List of output tensors (only .name is used from this).
converter = tf.lite.TFLiteConverter.from_session(sess,
input_tensors=inputs,
output_tensors=outputs)
# 세션에 들어있는 모든 연산, 즉 모델 전체를 변환
# 반환값은 TFLite 형식의 Flatbuffer 또는 Graphviz 그래프
flat_data = converter.convert()

# 텍스트가 아니기 때문에 바이너리 형태로 저장. w(write), b(binary)
with open(model_path, 'wb') as f:
f.write(flat_data)


# 입력 1개, 출력 1개
def simple_model_1(model_path):
# 에러. 반드시 shape을 지정해야 함.
# x = tf.placeholder(tf.int32)

# 안드로이드에서 전달한 입력과 출력 변수가 플레이스 홀더와 연동
x = tf.placeholder(tf.int32, shape=[1])
out = x * 5

model_common([x], [out], model_path)

# 에러. 반드시 [] 형태로 전달해야 함.
# model_common(x, out, model_path)


# 입력 2개짜리 1개, 출력 1개
def simple_model_2(model_path):
x = tf.placeholder(tf.int32, shape=[2])
out = tf.reduce_sum(x * x)

model_common([x], [out], model_path)


# 입력 1개짜리 2개, 출력 1개
def simple_model_3(model_path):
# 에러. 반드시 shape을 지정해야 함.
# x1 = tf.placeholder(tf.int32, shape=[0])
# x2 = tf.placeholder(tf.int32, shape=[0])

x1 = tf.placeholder(tf.int32, shape=[1])
x2 = tf.placeholder(tf.int32, shape=[1])
out = tf.add(x1, x2)

# 입력에 2개 전달
model_common([x1, x2], [out], model_path)


# 입력 1개짜리 2개, 출력 1개짜리 2개
def simple_model_4(model_path):
x1 = tf.placeholder(tf.int32, shape=[1])
x2 = tf.placeholder(tf.int32, shape=[1])
out_1 = x1 + x2
out_2 = x1 * x2

# 입력에 2개, 출력에 2개 전달
model_common([x1, x2], [out_1, out_2], model_path)


simple_model_1('simple_1.tflite')
simple_model_2('simple_2.tflite')
simple_model_3('simple_3.tflite')
simple_model_4('simple_4.tflite')


tflite 파일로 변환하는 방법에는 4가지가 있는데
나는 쉬운 것만 쓰고 좀더 나을 수도 있겠지만 복잡해 보이는 방법은 사용하지 않을 것이다.

여기서는 가장 기본이 되는 텐서플로 코드를 직접 변환하는 방법을 사용했다.
세션에 들어있는 모든 데이터와 연산을 변환하게 되고 함수 이름은 from_session이다.
다른 방법이 궁금하면
도트(.)를 찍고 나면 어떤 방법이 있는지 이름을 통해 확인할 수 있다.

위의 코드를 실행하고 나면
현재 폴더에 tflite 파일 4개가 만들어진다.
생성된 모든 파일은 뒤에서 안드로이드 프로젝트로 복사해서 붙여넣는다.

PC 버전의 모델을 tflite 모델로 변환한다는 것은
모바일 버전에서 효율적으로 동작할 수 있도록 재구성하는 것이다.
PC 버전을 살짝 바꿔서 모바일 버전을 구현할 수도 있겠지만
그럴 경우 하드웨어 사양이 낮은 모바일에서는 결과 보기가 매우 힘들어진다.

convert 함수를 호출하면 모델을 FlatBuffer로 변환한다.
구글에서 게임 개발에 사용하기 위해 만들었는데
크로스 플랫폼에서 데이터를 효율적으로 처리하기 위한 라이브러리로 사용된다.