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 안드로이드 앱