1. 시작하기 전에
이 Codelab에서는 REST 및 gRPC를 사용하여 TensorFlow Serving으로 Android 앱에서 객체 감지 추론을 실행하는 방법을 알아봅니다.
기본 요건
- Java를 사용한 Android 개발에 관한 기본 지식
- 학습 및 배포와 같은 TensorFlow를 활용한 머신러닝에 관한 기본 지식
- 터미널 및 Docker에 관한 기본 지식
학습할 내용
- TensorFlow Hub에서 사전 학습된 객체 감지 모델을 찾는 방법
- 간단한 Android 앱을 빌드하고 TensorFlow Serving (REST 및 gRPC)을 통해 다운로드한 객체 감지 모델로 예측하는 방법
- UI에 감지 결과를 렌더링하는 방법
필요한 항목
- 최신 버전의 Android 스튜디오
- Docker
- Bash
2. 설정
이 Codelab의 코드를 다운로드하려면 다음 안내를 따르세요.
- 이 Codelab의 GitHub 저장소로 이동합니다.
- Code > Download zip을 클릭하여 이 Codelab의 모든 코드를 다운로드합니다.
- 다운로드한 ZIP 파일의 압축을 풀어 필요한 모든 리소스가 포함된
codelabs
루트 폴더의 압축을 풉니다.
이 Codelab에서는 저장소의 TFServing/ObjectDetectionAndroid
하위 디렉터리에 있는 파일만 필요하며, 여기에는 다음 두 폴더가 포함됩니다.
starter
폴더에는 이 Codelab에서 빌드를 시작하는 시작 코드가 포함되어 있습니다.finished
폴더에는 완료된 샘플 앱의 완성된 코드가 포함되어 있습니다.
3. 프로젝트에 종속 항목 추가
Android 스튜디오로 시작 앱 가져오기
- Android 스튜디오에서 File > New > Import project를 클릭한 다음 이전에 다운로드한 소스 코드에서
starter
폴더를 선택합니다.
OkHttp 및 gRPC의 종속 항목 추가
- 프로젝트의
app/build.gradle
파일에서 종속 항목이 있는지 확인합니다.
dependencies {
// ...
implementation 'com.squareup.okhttp3:okhttp:4.9.0'
implementation 'javax.annotation:javax.annotation-api:1.3.2'
implementation 'io.grpc:grpc-okhttp:1.29.0'
implementation 'io.grpc:grpc-protobuf-lite:1.29.0'
implementation 'io.grpc:grpc-stub:1.29.0'
}
프로젝트를 Gradle 파일과 동기화
- 탐색 메뉴에서
Sync Project with Gradle Files를 선택합니다.
4. 시작 앱 실행
- Android Emulator를 시작한 다음 탐색 메뉴에서
Run ‘app'을 클릭합니다.
앱 실행 및 탐색
Android 기기에서 앱이 실행됩니다. UI는 매우 간단합니다. 객체를 감지하려는 고양이 이미지가 있으며 사용자는 REST 또는 gRPC를 사용하여 백엔드로 데이터를 전송하는 방법을 선택할 수 있습니다. 백엔드는 이미지에서 객체 감지를 실행하고 감지 결과를 클라이언트 앱에 반환하며, 클라이언트 앱은 UI를 다시 렌더링합니다.
지금 추론 실행을 클릭하면 아무 일도 일어나지 않습니다. 아직 백엔드와 통신할 수 없기 때문입니다.
5. TensorFlow Serving으로 객체 감지 모델 배포
객체 감지는 매우 일반적인 ML 작업이며 목표는 이미지 내에서 객체를 감지하는 것입니다. 즉, 객체의 가능한 카테고리와 객체 주변의 경계 상자를 예측하는 것입니다. 다음은 감지 결과의 예입니다.
Google은 TensorFlow Hub에 여러 사전 학습된 모델을 게시했습니다. 전체 목록을 보려면 object_detection 페이지를 방문하세요. 이 Codelab에서는 비교적 가벼운 SSD MobileNet V2 FPNLite 320x320 모델을 사용하므로 GPU를 사용하여 실행할 필요가 없습니다.
TensorFlow Serving으로 객체 감지 모델을 배포하려면 다음 단계를 따르세요.
- 모델 파일을 다운로드합니다.
- 7-Zip과 같은 압축 해제 도구를 사용하여 다운로드한
.tar.gz
파일의 압축을 풉니다. ssd_mobilenet_v2_2_320
폴더를 만든 다음 그 안에123
하위 폴더를 만듭니다.- 추출된
variables
폴더와saved_model.pb
파일을123
하위 폴더에 넣습니다.
ssd_mobilenet_v2_2_320
폴더를 SavedModel
폴더로 참조할 수 있습니다. 123
은 버전 번호의 예입니다. 원하는 경우 다른 번호를 선택할 수 있습니다.
폴더 구조는 다음 이미지와 같아야 합니다.
TensorFlow Serving 시작
- 터미널에서 Docker로 TensorFlow Serving을 시작하면서
PATH/TO/SAVEDMODEL
자리표시자를 컴퓨터에서ssd_mobilenet_v2_2_320
폴더의 절대 경로로 바꿉니다.
docker pull tensorflow/serving docker run -it --rm -p 8500:8500 -p 8501:8501 -v "PATH/TO/SAVEDMODEL:/models/ssd_mobilenet_v2_2" -e MODEL_NAME=ssd_mobilenet_v2_2 tensorflow/serving
Docker는 먼저 TensorFlow Serving 이미지를 자동으로 다운로드하며, 여기에 몇 분 정도 걸립니다. 그런 다음 TensorFlow Serving이 시작됩니다. 로그는 다음 코드 스니펫과 같아야 합니다.
2022-02-25 06:01:12.513231: I external/org_tensorflow/tensorflow/cc/saved_model/loader.cc:206] Restoring SavedModel bundle. 2022-02-25 06:01:12.585012: I external/org_tensorflow/tensorflow/core/platform/profile_utils/cpu_utils.cc:114] CPU Frequency: 3000000000 Hz 2022-02-25 06:01:13.395083: I external/org_tensorflow/tensorflow/cc/saved_model/loader.cc:190] Running initialization op on SavedModel bundle at path: /models/ssd_mobilenet_v2_2/123 2022-02-25 06:01:13.837562: I external/org_tensorflow/tensorflow/cc/saved_model/loader.cc:277] SavedModel load for tags { serve }; Status: success: OK. Took 1928700 microseconds. 2022-02-25 06:01:13.877848: I tensorflow_serving/servables/tensorflow/saved_model_warmup_util.cc:59] No warmup data file found at /models/ssd_mobilenet_v2_2/123/assets.extra/tf_serving_warmup_requests 2022-02-25 06:01:13.929844: I tensorflow_serving/core/loader_harness.cc:87] Successfully loaded servable version {name: ssd_mobilenet_v2_2 version: 123} 2022-02-25 06:01:13.985848: I tensorflow_serving/model_servers/server_core.cc:486] Finished adding/updating models 2022-02-25 06:01:13.985987: I tensorflow_serving/model_servers/server.cc:367] Profiler service is enabled 2022-02-25 06:01:13.988994: I tensorflow_serving/model_servers/server.cc:393] Running gRPC ModelServer at 0.0.0.0:8500 ... [warn] getaddrinfo: address family for nodename not supported 2022-02-25 06:01:14.033872: I tensorflow_serving/model_servers/server.cc:414] Exporting HTTP/REST API at:localhost:8501 ... [evhttp_server.cc : 245] NET_LOG: Entering the event loop ...
6. REST를 통해 Android 앱과 TensorFlow Serving 연결
이제 백엔드가 준비되었으므로 TensorFlow Serving으로 클라이언트 요청을 전송하여 이미지 내 객체를 감지할 수 있습니다. TensorFlow Serving에 요청을 보내는 방법에는 두 가지가 있습니다.
- REST
- gRPC
REST를 통해 요청 보내기 및 응답 수신
다음 세 가지 간단한 단계를 따르세요.
- REST 요청을 만듭니다.
- TensorFlow Serving에 REST 요청을 보냅니다.
- REST 응답에서 예측 결과를 추출하고 UI를 렌더링합니다.
MainActivity.java.
에 이러한 목표를 달성할 수 있습니다.
REST 요청 만들기
현재 MainActivity.java
파일에는 빈 createRESTRequest()
함수가 있습니다. 이 함수를 구현하여 REST 요청을 만듭니다.
private Request createRESTRequest() {
}
TensorFlow Serving은 사용하는 SSD MobileNet 모델의 이미지 텐서가 포함된 POST 요청을 예상하므로 이미지의 각 픽셀에서 RGB 값을 배열로 추출한 다음 배열을 요청의 페이로드인 JSON으로 래핑해야 합니다.
- 다음 코드를
createRESTRequest()
함수에 추가합니다.
//Create the REST request.
int[] inputImg = new int[INPUT_IMG_HEIGHT * INPUT_IMG_WIDTH];
int[][][][] inputImgRGB = new int[1][INPUT_IMG_HEIGHT][INPUT_IMG_WIDTH][3];
inputImgBitmap.getPixels(inputImg, 0, INPUT_IMG_WIDTH, 0, 0, INPUT_IMG_WIDTH, INPUT_IMG_HEIGHT);
int pixel;
for (int i = 0; i < INPUT_IMG_HEIGHT; i++) {
for (int j = 0; j < INPUT_IMG_WIDTH; j++) {
// Extract RBG values from each pixel; alpha is ignored
pixel = inputImg[i * INPUT_IMG_WIDTH + j];
inputImgRGB[0][i][j][0] = ((pixel >> 16) & 0xff);
inputImgRGB[0][i][j][1] = ((pixel >> 8) & 0xff);
inputImgRGB[0][i][j][2] = ((pixel) & 0xff);
}
}
RequestBody requestBody =
RequestBody.create("{\"instances\": " + Arrays.deepToString(inputImgRGB) + "}", JSON);
Request request =
new Request.Builder()
.url("http://" + SERVER + ":" + REST_PORT + "/v1/models/" + MODEL_NAME + ":predict")
.post(requestBody)
.build();
return request;
TensorFlow Serving에 REST 요청 보내기
앱을 사용하면 사용자가 TensorFlow Serving과 통신할 때 REST 또는 gRPC를 선택할 수 있으므로 onClick(View view)
리스너에는 두 개의 브랜치가 있습니다.
predictButton.setOnClickListener(
new View.OnClickListener() {
@Override
public void onClick(View view) {
if (requestRadioGroup.getCheckedRadioButtonId() == R.id.rest) {
// TODO: REST request
}
else {
}
}
}
)
- OkHttp를 사용하여 TensorFlow Serving에 요청을 전송하려면
onClick(View view)
리스너의 REST 브랜치에 다음 코드를 추가하세요.
// Send the REST request.
Request request = createRESTRequest();
try {
client =
new OkHttpClient.Builder()
.connectTimeout(20, TimeUnit.SECONDS)
.writeTimeout(20, TimeUnit.SECONDS)
.readTimeout(20, TimeUnit.SECONDS)
.callTimeout(20, TimeUnit.SECONDS)
.build();
Response response = client.newCall(request).execute();
JSONObject responseObject = new JSONObject(response.body().string());
postprocessRESTResponse(responseObject);
} catch (IOException | JSONException e) {
Log.e(TAG, e.getMessage());
responseTextView.setText(e.getMessage());
return;
}
TensorFlow Serving의 REST 응답 처리
SSD MobileNet 모델은 다음을 포함한 여러 결과를 반환합니다.
num_detections
: 감지 수detection_scores
: 감지 점수detection_classes
: 감지 클래스 색인detection_boxes
: 경계 상자 좌표
postprocessRESTResponse()
함수를 구현하여 응답을 처리합니다.
private void postprocessRESTResponse(Predict.PredictResponse response) {
}
- 다음 코드를
postprocessRESTResponse()
함수에 추가합니다.
// Process the REST response.
JSONArray predictionsArray = responseObject.getJSONArray("predictions");
//You only send one image, so you directly extract the first element.
JSONObject predictions = predictionsArray.getJSONObject(0);
// Argmax
int maxIndex = 0;
JSONArray detectionScores = predictions.getJSONArray("detection_scores");
for (int j = 0; j < predictions.getInt("num_detections"); j++) {
maxIndex =
detectionScores.getDouble(j) > detectionScores.getDouble(maxIndex + 1) ? j : maxIndex;
}
int detectionClass = predictions.getJSONArray("detection_classes").getInt(maxIndex);
JSONArray boundingBox = predictions.getJSONArray("detection_boxes").getJSONArray(maxIndex);
double ymin = boundingBox.getDouble(0);
double xmin = boundingBox.getDouble(1);
double ymax = boundingBox.getDouble(2);
double xmax = boundingBox.getDouble(3);
displayResult(detectionClass, (float) ymin, (float) xmin, (float) ymax, (float) xmax);
이제 후처리 함수가 대답에서 예측 값을 추출하고, 객체의 가장 가능성이 높은 카테고리와 경계 상자 꼭짓점의 좌표를 식별하고, 마지막으로 UI에 감지 경계 상자를 렌더링합니다.
실행하기
- 탐색 메뉴에서
Run ‘app'을 클릭한 다음 앱이 로드될 때까지 기다립니다.
- REST > Run inference를 선택합니다.
앱이 고양이의 경계 상자를 렌더링하고 17
를 객체의 카테고리로 표시하는 데 몇 초가 걸립니다. 이는 COCO 데이터 세트의 cat
객체에 매핑됩니다.
7. gRPC를 통해 Android 앱과 TensorFlow Serving 연결
TensorFlow Serving은 REST 외에 gRPC도 지원합니다.
gRPC는 모든 환경에서 실행할 수 있는 최신형 오픈소스 고성능 리모트 프로시져 콜(RPC) 프레임워크입니다. 부하 분산, 추적, 상태 확인 및 인증을 위한 플러그형 지원을 통해 데이터 센터 내부 및 데이터 센터 간에 서비스를 효율적으로 연결할 수 있습니다. gRPC는 실무에서 REST보다 성능이 우수한 것으로 관찰된 바 있습니다.
gRPC로 요청 보내기 및 응답 수신
다음 네 가지 간단한 단계를 따르세요.
- [선택사항] gRPC 클라이언트 스텁 코드를 생성합니다.
- gRPC 요청을 만듭니다.
- TensorFlow Serving에 gRPC 요청을 보냅니다.
- gRPC 응답에서 예측 결과를 추출하고 UI를 렌더링합니다.
MainActivity.java.
에 이러한 목표를 달성할 수 있습니다.
선택사항: gRPC 클라이언트 스텁 코드 생성
TensorFlow Serving과 함께 gRPC를 사용하려면 gRPC 워크플로를 따라야 합니다. 자세한 내용은 gRPC 문서를 참조하세요.
TensorFlow Serving 및 TensorFlow는 개발자를 위해 .proto
파일을 정의합니다. TensorFlow 및 TensorFlow Serving 2.8부터는 다음과 같은 .proto
파일이 필요합니다.
tensorflow/core/example/example.proto
tensorflow/core/example/feature.proto
tensorflow/core/protobuf/struct.proto
tensorflow/core/protobuf/saved_object_graph.proto
tensorflow/core/protobuf/saver.proto
tensorflow/core/protobuf/trackable_object_graph.proto
tensorflow/core/protobuf/meta_graph.proto
tensorflow/core/framework/node_def.proto
tensorflow/core/framework/attr_value.proto
tensorflow/core/framework/function.proto
tensorflow/core/framework/types.proto
tensorflow/core/framework/tensor_shape.proto
tensorflow/core/framework/full_type.proto
tensorflow/core/framework/versions.proto
tensorflow/core/framework/op_def.proto
tensorflow/core/framework/graph.proto
tensorflow/core/framework/tensor.proto
tensorflow/core/framework/resource_handle.proto
tensorflow/core/framework/variable.proto
tensorflow_serving/apis/inference.proto
tensorflow_serving/apis/classification.proto
tensorflow_serving/apis/predict.proto
tensorflow_serving/apis/regression.proto
tensorflow_serving/apis/get_model_metadata.proto
tensorflow_serving/apis/input.proto
tensorflow_serving/apis/prediction_service.proto
tensorflow_serving/apis/model.proto
- 스텁을 생성하려면
app/build.gradle
파일에 다음 코드를 추가합니다.
apply plugin: 'com.google.protobuf'
protobuf {
protoc { artifact = 'com.google.protobuf:protoc:3.11.0' }
plugins {
grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.29.0'
}
}
generateProtoTasks {
all().each { task ->
task.builtins {
java { option 'lite' }
}
task.plugins {
grpc { option 'lite' }
}
}
}
}
gRPC 요청 만들기
REST 요청과 마찬가지로 createGRPCRequest()
함수에서 gRPC 요청을 생성합니다.
private Request createGRPCRequest() {
}
- 다음 코드를
createGRPCRequest()
함수에 추가합니다.
if (stub == null) {
channel = ManagedChannelBuilder.forAddress(SERVER, GRPC_PORT).usePlaintext().build();
stub = PredictionServiceGrpc.newBlockingStub(channel);
}
Model.ModelSpec.Builder modelSpecBuilder = Model.ModelSpec.newBuilder();
modelSpecBuilder.setName(MODEL_NAME);
modelSpecBuilder.setVersion(Int64Value.of(MODEL_VERSION));
modelSpecBuilder.setSignatureName(SIGNATURE_NAME);
Predict.PredictRequest.Builder builder = Predict.PredictRequest.newBuilder();
builder.setModelSpec(modelSpecBuilder);
TensorProto.Builder tensorProtoBuilder = TensorProto.newBuilder();
tensorProtoBuilder.setDtype(DataType.DT_UINT8);
TensorShapeProto.Builder tensorShapeBuilder = TensorShapeProto.newBuilder();
tensorShapeBuilder.addDim(TensorShapeProto.Dim.newBuilder().setSize(1));
tensorShapeBuilder.addDim(TensorShapeProto.Dim.newBuilder().setSize(INPUT_IMG_HEIGHT));
tensorShapeBuilder.addDim(TensorShapeProto.Dim.newBuilder().setSize(INPUT_IMG_WIDTH));
tensorShapeBuilder.addDim(TensorShapeProto.Dim.newBuilder().setSize(3));
tensorProtoBuilder.setTensorShape(tensorShapeBuilder.build());
int[] inputImg = new int[INPUT_IMG_HEIGHT * INPUT_IMG_WIDTH];
inputImgBitmap.getPixels(inputImg, 0, INPUT_IMG_WIDTH, 0, 0, INPUT_IMG_WIDTH, INPUT_IMG_HEIGHT);
int pixel;
for (int i = 0; i < INPUT_IMG_HEIGHT; i++) {
for (int j = 0; j < INPUT_IMG_WIDTH; j++) {
// Extract RBG values from each pixel; alpha is ignored.
pixel = inputImg[i * INPUT_IMG_WIDTH + j];
tensorProtoBuilder.addIntVal((pixel >> 16) & 0xff);
tensorProtoBuilder.addIntVal((pixel >> 8) & 0xff);
tensorProtoBuilder.addIntVal((pixel) & 0xff);
}
}
TensorProto tensorProto = tensorProtoBuilder.build();
builder.putInputs("input_tensor", tensorProto);
builder.addOutputFilter("num_detections");
builder.addOutputFilter("detection_boxes");
builder.addOutputFilter("detection_classes");
builder.addOutputFilter("detection_scores");
return builder.build();
TensorFlow Serving에 gRPC 요청 보내기
이제 onClick(View view)
리스너를 완료할 수 있습니다.
predictButton.setOnClickListener(
new View.OnClickListener() {
@Override
public void onClick(View view) {
if (requestRadioGroup.getCheckedRadioButtonId() == R.id.rest) {
}
else {
// TODO: gRPC request
}
}
}
)
- gRPC 브랜치에 다음 코드를 추가합니다.
try {
Predict.PredictRequest request = createGRPCRequest();
Predict.PredictResponse response = stub.predict(request);
postprocessGRPCResponse(response);
} catch (Exception e) {
Log.e(TAG, e.getMessage());
responseTextView.setText(e.getMessage());
return;
}
TensorFlow Serving의 gRPC 응답 처리
gRPC와 마찬가지로 postprocessGRPCResponse()
함수를 구현하여 응답을 처리합니다.
private void postprocessGRPCResponse(Predict.PredictResponse response) {
}
- 다음 코드를
postprocessGRPCResponse()
함수에 추가합니다.
// Process the response.
float numDetections = response.getOutputsMap().get("num_detections").getFloatValList().get(0);
List<Float> detectionScores = response.getOutputsMap().get("detection_scores").getFloatValList();
int maxIndex = 0;
for (int j = 0; j < numDetections; j++) {
maxIndex = detectionScores.get(j) > detectionScores.get(maxIndex + 1) ? j : maxIndex;
}
Float detectionClass = response.getOutputsMap().get("detection_classes").getFloatValList().get(maxIndex);
List<Float> boundingBoxValues = response.getOutputsMap().get("detection_boxes").getFloatValList();
float ymin = boundingBoxValues.get(maxIndex * 4);
float xmin = boundingBoxValues.get(maxIndex * 4 + 1);
float ymax = boundingBoxValues.get(maxIndex * 4 + 2);
float xmax = boundingBoxValues.get(maxIndex * 4 + 3);
displayResult(detectionClass.intValue(), ymin, xmin, ymax, xmax);
이제 후처리 함수가 응답에서 예측 값을 추출하고 UI에 감지 경계 상자를 렌더링할 수 있습니다.
실행하기
- 탐색 메뉴에서
Run ‘app'을 클릭한 다음 앱이 로드될 때까지 기다립니다.
- gRPC > Run inference를 선택합니다.
앱이 고양이의 경계 상자를 렌더링하고 17
을 객체의 카테고리로 표시하는 데 몇 초가 걸립니다. 이 카테고리는 COCO 데이터 세트의 cat
카테고리에 매핑됩니다.
8. 축하합니다
TensorFlow Serving을 사용하여 앱에 객체 감지 기능을 추가했습니다.