이미지를 분류하는 간단한 웹사이트 만들기

1. 시작하기 전에

이 Codelab에서는 REST 및 gRPC와 함께 TensorFlow Serving을 사용하여 웹사이트에서 이미지 분류 추론을 실행하는 방법을 알아봅니다.

기본 요건

  • HTML 및 자바스크립트와 같은 웹 개발에 대한 기본 지식
  • 학습 및 배포와 같은 TensorFlow를 활용한 머신러닝에 관한 기본 지식
  • 터미널 및 Docker에 관한 기본 지식

실습 내용

  • TensorFlow Hub에서 선행 학습된 이미지 분류 모델을 찾는 방법
  • TensorFlow Serving (REST 및 gRPC)을 통해 간단한 웹사이트를 빌드하고 다운로드한 이미지 분류 모델을 사용하여 예측하는 방법
  • UI에서 감지 결과를 렌더링하는 방법

준비물

2 설정

이 Codelab의 코드를 다운로드하려면 다음 안내를 따르세요.

  1. 이 GitHub 저장소로 이동합니다.
  2. Code > 다운로드 zip을 클릭하여 이 Codelab의 모든 코드를 다운로드합니다.

A72f2bb4caa9a96.png

  1. 다운로드한 ZIP 파일의 압축을 풀고 필요한 모든 리소스로 codelabs 루트 폴더를 압축 해제합니다.

이 Codelab에서는 저장소의 TFServing/ImageClassificationWeb 하위 디렉터리에 있는 다음 두 폴더만 있는 파일이 필요합니다.

  • starter 폴더에는 이 Codelab을 위해 빌드하는 시작 코드가 포함되어 있습니다.
  • finished 폴더에는 완료된 샘플 앱의 완성된 코드가 포함되어 있습니다.

3. 종속성 설치

종속 항목을 설치하려면 다음 단계를 따르세요.

  • 터미널에서 starter 폴더로 이동한 다음 필요한 NPM 패키지를 설치합니다.
npm install

4. 시작 웹사이트 실행

Chrome용 웹 서버를 사용하여 TFServing/ImageClassificationWeb/starter/dist/index.html 파일을 로드합니다.

  1. Chrome의 주소 표시줄에 Chrome://apps/를 입력하고 앱 목록에서 Chrome의 웹 서버를 찾습니다.
  2. Chrome용 웹 서버를 실행한 다음 TFServing/ImageClassificationWeb/starter/dist/ 폴더를 선택합니다.
  3. 웹 서버 전환 버튼을 클릭하여 사용 설정한 다음 브라우저에서 http://localhost:8887/로 이동합니다.

f7b43cd44ebf1f1b.png

웹사이트 실행 및 탐색

이제 웹사이트가 표시됩니다. UI는 매우 간단합니다. 사용자가 분류하려는 고양이 이미지가 있으며 사용자는 REST 또는 gRPC를 사용하여 백엔드로 데이터를 전송할 수 있습니다. 백엔드가 이미지에서 이미지 분류를 수행하고 분류 결과를 웹사이트에 반환합니다.

837d97a27c59a0b3.png

분류를 클릭하면 아직 백엔드와 통신할 수 없으므로 아무 일도 일어나지 않습니다.

5 TensorFlow Serving으로 이미지 분류 모델 배포하기

이미지 분류는 이미지의 기본 콘텐츠를 기반으로 이미지를 사전 정의된 카테고리로 분류하는 매우 일반적인 ML 작업입니다. 다음은 꽃 분류 예입니다.

A6da16b4a7665db0.png

TensorFlow Hub에는 선행 학습된 이미지 분류 모델이 많이 있습니다. 이 Codelab에서는 널리 사용되는 Inception v3 모델을 사용합니다.

TensorFlow Serving으로 이미지 분류 모델을 배포하려면 다음 안내를 따르세요.

  1. Inception v3 모델 파일을 다운로드합니다.
  2. 7-Zip과 같은 압축 해제 도구로 다운로드한 .tar.gz 파일의 압축을 풉니다.
  3. inception_v3 폴더를 만든 후 그 안에 123 하위 폴더를 만듭니다.
  4. 추출된 variables 폴더와 saved_model.pb 파일을 123 하위 폴더에 넣습니다.

inception_v3 폴더를 SavedModel 폴더로 참조할 수 있습니다. 123는 버전 번호의 예입니다. 원하는 경우 다른 번호를 선택할 수 있습니다.

폴더 구조는 다음과 같습니다.

21a8675ac8d31907.png

TensorFlow Serving 시작

  • 터미널에서 TensorFlow Serving을 Docker로 시작하되, PATH/TO/SAVEDMODEL를 컴퓨터에 있는 inception_v3 폴더의 절대 경로로 바꿉니다.
docker pull tensorflow/serving

docker run -it --rm -p 8500:8500 -p 8501:8501 -v "PATH/TO/SAVEDMODEL:/models/inception" -e MODEL_NAME=inception 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/inception/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/inception/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: inception 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. Envoy 프록시 설정

현재 TensorFlow Serving은 Access-Control-Allow-Origin 헤더를 설정하지 않으므로 브라우저에서 보안상의 이유로 프런트엔드 자바스크립트에서 TensorFlow Serving으로의 요청을 차단합니다. 이 문제를 해결하려면 Envoy 같은 프록시를 사용하여 자바스크립트에서 TensorFlow Serving 백엔드로 요청을 프록시해야 합니다.

엔보이 시작

  • 터미널에서 Envoy 이미지를 다운로드하고 Docker로 Envoy를 시작하지만 PATH/TO/ENVOY-CUSTOM.YAML 자리표시자를 starter 폴더에 있는 envoy-custom.yaml 파일의 절대 경로로 바꿉니다.
docker pull envoyproxy/envoy-dev:fd3e8370ddb7a96634c192d1461516e6de1d1797

docker run --add-host host.docker.internal:host-gateway --rm -it -p 9901:9901 -p 8000:8000 -p 8080:8080 -v PATH/TO/ENVOY-CUSTOM.YAML:/envoy-custom.yaml envoyproxy/envoy-dev:fd3e8370ddb7a96634c192d1461516e6de1d1797 -c /envoy-custom.yaml

Docker가 먼저 Envoy 이미지를 자동으로 다운로드합니다. 일정 시간이 지나면 Envoy가 시작됩니다. 로그는 다음 코드 스니펫과 같아야 합니다.

[2022-03-02 07:51:48.563][1][info][main] [source/server/server.cc:436]   response trailer map: 152 bytes: grpc-message,grpc-status
[2022-03-02 07:51:48.681][1][info][main] [source/server/server.cc:772] runtime: {}
[2022-03-02 07:51:48.682][1][info][admin] [source/server/admin/admin.cc:134] admin address: 0.0.0.0:9901
[2022-03-02 07:51:48.683][1][info][config] [source/server/configuration_impl.cc:127] loading tracing configuration
[2022-03-02 07:51:48.683][1][info][config] [source/server/configuration_impl.cc:87] loading 0 static secret(s)
[2022-03-02 07:51:48.683][1][info][config] [source/server/configuration_impl.cc:93] loading 2 cluster(s)
[2022-03-02 07:51:48.687][1][info][config] [source/server/configuration_impl.cc:97] loading 2 listener(s)
[2022-03-02 07:51:48.694][1][info][config] [source/server/configuration_impl.cc:109] loading stats configuration
[2022-03-02 07:51:48.696][1][info][main] [source/server/server.cc:868] starting main dispatch loop
[2022-03-02 07:51:48.881][1][info][runtime] [source/common/runtime/runtime_impl.cc:446] RTDS has finished initialization
[2022-03-02 07:51:48.881][1][info][upstream] [source/common/upstream/cluster_manager_impl.cc:207] cm init: all clusters initialized
[2022-03-02 07:51:48.881][1][info][main] [source/server/server.cc:849] all clusters initialized. initializing init manager
[2022-03-02 07:51:48.881][1][info][config] [source/server/listener_manager_impl.cc:784] all dependencies initialized. starting workers
[2022-03-02 07:51:48.902][1][warning][main] [source/server/server.cc:747] there is no configured limit to the number of allowed active connections. Set a limit via the runtime key overload.global_downstream_max_connections

7 REST를 통해 웹사이트를 TensorFlow에 연결

이제 TensorFlow가 클라이언트 요청을 TensorFlow Serving으로 전송하여 이미지를 분류할 수 있으므로 백엔드가 준비되었습니다. TensorFlow Serving에 요청을 보내는 방법에는 두 가지가 있습니다.

  • REST
  • gRPC

REST를 통해 요청 보내기 및 응답 수신

REST를 통해 요청을 주고받는 간단한 세 가지 단계는 다음과 같습니다.

  1. REST 요청을 만듭니다.
  2. TensorFlow Serving에 REST 요청을 보냅니다.
  3. REST 응답에서 예측 결과를 추출하고 결과를 표시합니다.

이 단계는 src/index.js 파일에서 진행합니다.

REST 요청 만들기

현재 classify_img() 함수는 REST 요청을 TensorFlow Serving에 전송하지 않습니다. 먼저 REST 요청을 만들려면 REST 분기를 구현해야 합니다.

if (radioButtons[0].checked) {
    console.log('Using REST');
    // TODO: Add code to send a REST request to TensorFlow Serving.

}

TensorFlow Serving은 사용 중인 Inception v3 모델의 이미지 텐서가 포함된 POST 요청을 예상하므로 이미지의 각 픽셀에서 RGB 값을 추출한 다음 해당 배열을 페이로드인 JSON으로 래핑해야 합니다. 요청할 수 있습니다

  • REST 분기에 다음 코드를 추가합니다.
//Create the REST request.
let imgTensor = new Array();
let pixelArray = new Array();
context.drawImage(img, 0, 0);
for(let i=0; i<inputImgHeight; i++) {
    pixelArray[i] = new Array();
    for (let j=0; j<inputImgWidth; j++) {
        pixelArray[i][j] = new Array();
        pixelArray[i][j].push(context.getImageData(i, j, 1, 1).data[0]/255);
        pixelArray[i][j].push(context.getImageData(i, j, 1, 1).data[1]/255);
        pixelArray[i][j].push(context.getImageData(i, j, 1, 1).data[2]/255);
    }
}
imgTensor.push(pixelArray);

const RESTURL = 'http://localhost:8000/v1/models/inception:predict';
let xhr = new XMLHttpRequest();
xhr.open('POST', RESTURL);
xhr.setRequestHeader('Content-Type', 'application/json;charset=utf-8;');
let data = JSON.stringify({
    instances: imgTensor
});
xhr.onload = () => {

}
xhr.onerror = () => {
    console.log('REST request error');
}

TensorFlow Serving으로 REST 요청 보내기

이제 요청을 보낼 수 있습니다.

  • REST 분기에서 위의 코드 바로 뒤에 다음 코드를 추가합니다.
// Send the REST request.
xhr.send(data);

TensorFlow Serving의 REST 응답 처리

Inception v3 모델은 이미지가 사전 정의된 카테고리에 속할 확률을 반환합니다. 예측이 성공하면 UI에 가장 가능성이 높은 카테고리를 출력해야 합니다.

onload() 리스너를 구현하여 응답을 처리합니다.

xhr.onload = () => {

}
  • 다음 코드를 onload() 리스너에 추가합니다.
// Process the REST response.
const response = JSON.parse(xhr.responseText);
const maxIndex = argmax(response['predictions'][0])
document.getElementById('category').textContent = 'Predicted category: ' + maxIndex;

이제 리스너가 응답에서 예측 가능성을 추출하고 객체에서 가장 가능성이 높은 카테고리를 식별하며 UI에 결과를 표시합니다.

실행

  1. 터미널에서 starter 폴더로 이동하여 webpack을 사용하여 모든 자바스크립트 파일을 dist/index.html 파일에 삽입할 수 있는 단일 파일로 묶습니다.
npm install -g npx
npm install --save-dev webpack
npx webpack
  1. 브라우저에서 http://localhost:8887/을 새로고침한 다음 REST > Classify를 클릭합니다.

웹사이트에서 286를 예측 카테고리로 표시하고 ImageNet 데이터 세트Egyptian Cat 라벨에 매핑합니다.

C865a93b9b58335d.png

8 gRPC를 통해 웹사이트를 TensorFlow Serving과 연결

TensorFlow Serving은 REST 외에 gRPC도 지원합니다.

b6f4449c2c850b0e.png

gRPC는 어느 환경에서나 실행할 수 있는 최신 오픈소스 RPC (Remote Procedure Call) 프레임워크입니다. 부하 분산, 추적, 상태 확인, 인증을 지원하며 데이터 센터 안팎의 서비스를 효율적으로 연결할 수 있습니다. gRPC가 실제로 REST보다 성능이 더 좋은 것으로 관찰되었습니다.

gRPC로 요청 보내기 및 응답 수신

다음의 간단한 4단계가 있습니다.

  1. 선택사항: gRPC 클라이언트 스텁 코드를 생성합니다.
  2. gRPC 요청을 만듭니다.
  3. TensorFlow Serving에 gRPC 요청을 전송합니다.
  4. gRPC 응답에서 예측 결과를 추출하여 UI에 표시합니다.

이러한 단계는 src/index.js 파일에서 완료합니다.

선택사항: gRPC 클라이언트 스텁 코드 생성

TensorFlow Serving과 함께 gRPC를 사용하려면 gRPC 워크플로를 따라야 합니다. 자세한 내용은 gRPC 문서를 참고하세요.

A9d0e5cb543467b4.png

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
  • 터미널에서 starter/src/proto/ 폴더로 이동하여 스텁을 생성합니다.
bash generate_grpc_stub_js.sh

gRPC 요청 만들기

REST 요청과 마찬가지로 gRPC 분기에서 gRPC 요청을 생성합니다..

if (connectionMode[picker.selectedRow(inComponent: 0)] == "REST") {

}
else {
    print("Using gRPC")
    // TODO: Add code to send a gRPC request to TensorFlow Serving.

}
  • gRPC 분기에 다음 코드를 추가합니다.
// Create the gRPC request.
const PredictModule = require('./proto/generated/tensorflow_serving/apis/predict_pb.js');
const PredictionServiceClientModule = require('./proto/generated/tensorflow_serving/apis/prediction_service_grpc_web_pb.js');
const ModelModule = require('./proto/generated/tensorflow_serving/apis/model_pb.js');
const TensorModule = require('./proto/generated/tensorflow/core/framework/tensor_pb.js');

const GPRCURL = 'http://localhost:8080';
const stub = new PredictionServiceClientModule.PredictionServiceClient(GPRCURL);

const modelSpec = new ModelModule.ModelSpec();
modelSpec.setName('inception');

const tensorProto = new TensorModule.TensorProto();
const tensorShapeProto = new TensorModule.TensorShapeProto();

const batchDim = (new TensorModule.TensorShapeProto.Dim()).setSize(1);
const heightDim = (new TensorModule.TensorShapeProto.Dim()).setSize(inputImgHeight);
const widthDim = (new TensorModule.TensorShapeProto.Dim()).setSize(inputImgWidth);
const channelDim = (new TensorModule.TensorShapeProto.Dim()).setSize(3);

tensorShapeProto.setDimList([batchDim, heightDim, widthDim, channelDim]);

tensorProto.setDtype(proto.tensorflow.DataType.DT_FLOAT);
tensorProto.setTensorShape(tensorShapeProto);
context.drawImage(img, 0, 0);
for(let i=0; i<inputImgHeight; i++) {
    for (let j=0; j<inputImgWidth; j++) {
        tensorProto.addFloatVal(context.getImageData(i, j, 1, 1).data[0]/255);
        tensorProto.addFloatVal(context.getImageData(i, j, 1, 1).data[1]/255);
        tensorProto.addFloatVal(context.getImageData(i, j, 1, 1).data[2]/255);
    }
}

const predictionServiceRequest = new PredictModule.PredictRequest();
predictionServiceRequest.setModelSpec(modelSpec);
predictionServiceRequest.getInputsMap().set('inputs', tensorProto);

TensorFlow Serving에 gRPC 요청 보내기

이제 요청을 보낼 수 있습니다.

  • 이전 코드 스니펫에서 gRPC 브랜치의 코드 바로 뒤에 다음 코드를 추가합니다.
// Send the gRPC request.
stub.predict(predictionServiceRequest, {}, function(err, response) {
    // TODO: Add code to process the response.
});

TensorFlow Serving의 gRPC 응답 처리

마지막으로 위의 콜백 함수를 구현하여 응답을 처리합니다.

  • 다음 코드를 이전 코드 스니펫의 함수 본문에 추가합니다.
// Process the gRPC response.
if (err) {
    console.log(err.code);
    console.log(err.message);
}
else {
    const maxIndex = argmax(response.getOutputsMap().get('logits').getFloatValList());
    document.getElementById('category').textContent = 'Predicted category: ' + maxIndex;
}

이제 리스너가 응답에서 예측 가능성을 추출하고 객체에서 가장 가능성이 높은 카테고리를 식별하며 UI에 결과를 표시합니다.

실행

  1. 터미널에서 webpack을 사용하여 모든 자바스크립트 파일을 index.html 파일에 삽입할 수 있는 단일 파일로 묶습니다.
npx webpack
  1. 브라우저에서 http://localhost:8887/을 새로고침합니다.
  2. gRPC > 분류를 클릭합니다.

웹사이트에서 ImageNet 데이터 세트Egyptian Cat 라벨에 매핑되는 286의 예측 카테고리를 표시합니다.

9. 축하합니다

여러분은 TensorFlow Serving을 사용하여 웹사이트에 이미지 분류 기능을 추가했습니다!

자세히 알아보기