À propos de cet atelier de programmation
1. Avant de commencer
Dans cet atelier de programmation, vous allez apprendre à exécuter une inférence de détection d'objets à partir d'une application Android en utilisant TensorFlow Serving avec REST et gRPC.
Prérequis
- Connaissances de base en développement Android avec Java
- Des connaissances de base en machine learning avec TensorFlow, telles que l'entraînement et le déploiement
- Connaissances de base des terminaux et de Docker
Points abordés
- Trouver des modèles de détection d'objets pré-entraînés sur TensorFlow Hub
- Découvrez comment créer une application Android simple et effectuer des prédictions à l'aide du modèle de détection d'objets téléchargé via TensorFlow Serving (REST et gRPC).
- Comment afficher le résultat de la détection dans l'interface utilisateur
Prérequis
- La dernière version d'Android Studio
- Docker
- Bash
2. Configuration
Pour télécharger le code de cet atelier de programmation:
- Accédez au dépôt GitHub de cet atelier de programmation.
- Cliquez sur Code > Télécharger le fichier ZIP pour télécharger l'ensemble du code de cet atelier de programmation.
- Décompressez le fichier ZIP téléchargé pour décompresser un dossier racine
codelabs
contenant toutes les ressources dont vous avez besoin.
Pour cet atelier de programmation, vous n'avez besoin que des fichiers du sous-répertoire TFServing/ObjectDetectionAndroid
dans le dépôt, qui contient deux dossiers:
- Le dossier
starter
contient le code de démarrage sur lequel s'appuie cet atelier de programmation. - Le dossier
finished
contient le code final de l'exemple d'application.
3. Ajouter les dépendances au projet
Importer l'application de départ dans Android Studio
- Dans Android Studio, cliquez sur File > New > Import Project (Fichier > Nouveau > Importer un projet), puis sélectionnez le dossier
starter
dans le code source que vous avez téléchargé précédemment.
Ajouter les dépendances pour OkHttp et gRPC
- Dans le fichier
app/build.gradle
de votre projet, confirmez la présence des dépendances.
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'
}
Synchroniser votre projet avec les fichiers Gradle
- Dans le menu de navigation, sélectionnez
Sync Project with Gradle Files (Synchroniser le projet avec les fichiers Gradle).
4. Exécuter l'application de démarrage
- Lancez Android Emulator,puis cliquez sur
Run 'app' dans le menu de navigation.
Exécuter et explorer l'application
L'application devrait se lancer sur votre appareil Android. L'interface utilisateur est assez simple: vous voulez détecter des objets dans une image de chat et l'utilisateur peut choisir la méthode d'envoi des données au backend, avec REST ou gRPC. Le backend effectue une détection d'objets sur l'image et renvoie les résultats de la détection à l'application cliente, qui affiche à nouveau l'interface utilisateur.
Pour le moment, si vous cliquez sur Runinférence (Exécuter l'inférence), rien ne se passe. car elle ne peut pas encore communiquer avec le backend.
5. Déployer un modèle de détection d'objets avec TensorFlow Serving
La détection d'objets est une tâche de ML très courante qui vise à détecter des objets dans des images, à savoir prédire des catégories possibles d'objets et des cadres de délimitation autour d'eux. Voici un exemple de résultat de détection:
Google a publié un certain nombre de modèles pré-entraînés dans TensorFlow Hub. Pour afficher la liste complète, accédez à la page object_detection. Vous allez utiliser le modèle SSD MobileNet V2 FPNLite 320x320, qui est relativement léger. Vous n'avez donc pas forcément besoin d'utiliser un GPU pour l'exécuter.
Pour déployer le modèle de détection d'objets avec TensorFlow Serving, procédez comme suit:
- Téléchargez le fichier de modèle.
- Décompressez le fichier
.tar.gz
téléchargé à l'aide d'un outil de décompression, tel que 7-Zip. - Créez un dossier
ssd_mobilenet_v2_2_320
, puis créez un sous-dossier123
à l'intérieur de celui-ci. - Placez le dossier
variables
et le fichiersaved_model.pb
extraits dans le sous-dossier123
.
Vous pouvez désigner le dossier ssd_mobilenet_v2_2_320
comme le dossier SavedModel
. 123
est un exemple de numéro de version. Si vous le souhaitez, vous pouvez en choisir un autre.
La structure de dossiers doit se présenter comme suit:
Lancer TensorFlow Serving
- Dans votre terminal, démarrez TensorFlow Serving avec Docker, mais remplacez l'espace réservé
PATH/TO/SAVEDMODEL
par le chemin absolu du dossierssd_mobilenet_v2_2_320
sur votre ordinateur.
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 télécharge d'abord l'image TensorFlow Serving, ce qui prend une minute. Ensuite, le service TensorFlow Serving doit commencer. Le journal doit ressembler à l'extrait de code suivant:
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. Connecter l'application Android à l'aide de TensorFlow Serving via REST
Le backend est prêt. Vous pouvez donc envoyer des requêtes client à TensorFlow Serving pour détecter des objets dans les images. Il existe deux façons d'envoyer des requêtes à TensorFlow Serving:
- REST
- gRPC
Envoyer des requêtes et recevoir des réponses via REST
Il vous suffit de suivre ces trois étapes simples:
- Créez la requête REST.
- Envoyez la requête REST à TensorFlow Serving.
- Extrayez le résultat prédit de la réponse REST et affichez l'interface utilisateur.
Vous allez atteindre cet objectif en MainActivity.java.
Créer la requête REST
Le fichier MainActivity.java
contient actuellement une fonction createRESTRequest()
vide. Vous implémentez cette fonction pour créer une requête REST.
private Request createRESTRequest() {
}
TensorFlow Serving attend une requête POST contenant le Tensor d'image pour le modèle SSD MobileNet que vous utilisez. Vous devez donc extraire les valeurs RVB de chaque pixel de l'image dans un tableau, puis encapsuler le tableau dans un fichier JSON (la charge utile). de la demande.
- Ajoutez ce code à la fonction
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;
Envoyer la requête REST à TensorFlow Serving
L'application permet à l'utilisateur de choisir REST ou gRPC pour communiquer avec TensorFlow Serving. Il existe donc deux branches dans l'écouteur onClick(View view)
.
predictButton.setOnClickListener(
new View.OnClickListener() {
@Override
public void onClick(View view) {
if (requestRadioGroup.getCheckedRadioButtonId() == R.id.rest) {
// TODO: REST request
}
else {
}
}
}
)
- Ajoutez ce code à la branche REST de l'écouteur
onClick(View view)
pour utiliser OkHttp afin d'envoyer la requête à TensorFlow Serving:
// 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;
}
Traitez la réponse REST à partir de TensorFlow Serving.
Le modèle SSD MobileNet renvoie un certain nombre de résultats, parmi lesquels:
num_detections
: nombre de détections.detection_scores
: scores de détectiondetection_classes
: index de classe de détectiondetection_boxes
: coordonnées du cadre de délimitation
Vous mettez en œuvre la fonction postprocessRESTResponse()
pour gérer la réponse.
private void postprocessRESTResponse(Predict.PredictResponse response) {
}
- Ajoutez ce code à la fonction
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);
La fonction de post-traitement extrait les valeurs prédites de la réponse, identifie la catégorie la plus probable de l'objet et les coordonnées des sommets du cadre de délimitation, puis affiche le cadre de délimitation de la détection dans l'interface utilisateur.
Exécuter le code
- Dans le menu de navigation, cliquez sur
Exécuter "app", puis attendez que l'application se charge.
- Sélectionnez REST > Exécuter une inférence.
Il faut quelques secondes avant que l'application affiche le cadre de délimitation du chat et affiche 17
comme catégorie de l'objet, qui est mappée à l'objet cat
dans l'ensemble de données COCO.
7. Connecter l'application Android à TensorFlow Serving via gRPC
Outre REST, TensorFlow Serving est également compatible avec gRPC.
gRPC est un framework d'appel de procédure à distance (RPC) moderne et Open Source qui peut être exécuté dans n'importe quel environnement. Grâce à cette solution, les services peuvent être connectés efficacement aux services de centres de données, et ils sont compatibles avec l'équilibrage de charge, le traçage, la vérification de l'état et l'authentification. En pratique, gRPC a été plus performant que REST en pratique.
Envoyer des requêtes et recevoir des réponses avec gRPC
Il existe quatre étapes simples:
- (Facultatif) Générez le code de souche de clients gRPC.
- Créez la requête gRPC.
- Envoyez la requête gRPC à TensorFlow Serving.
- Extrayez le résultat prédit de la réponse gRPC, puis affichez l'interface utilisateur.
Vous allez atteindre cet objectif en MainActivity.java.
Facultatif: Générer le code du bouchon pour le client gRPC
Pour utiliser gRPC avec TensorFlow Serving, vous devez suivre le workflow gRPC. Pour en savoir plus, consultez la documentation gRPC.
TensorFlow Serving et TensorFlow définissent les fichiers .proto
pour vous. À partir de TensorFlow et de TensorFlow Serving 2.8, ces fichiers .proto
sont nécessaires:
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
- Pour générer le bouchon, ajoutez ce code au fichier
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' }
}
}
}
}
Créer la requête gRPC
Comme pour la requête REST, vous créez la requête gRPC dans la fonction createGRPCRequest()
.
private Request createGRPCRequest() {
}
- Ajoutez ce code à la fonction
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();
Envoyer la requête gRPC à TensorFlow Serving
Vous pouvez à présent terminer l'écouteur onClick(View view)
.
predictButton.setOnClickListener(
new View.OnClickListener() {
@Override
public void onClick(View view) {
if (requestRadioGroup.getCheckedRadioButtonId() == R.id.rest) {
}
else {
// TODO: gRPC request
}
}
}
)
- Ajoutez ce code à la branche 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;
}
Traiter la réponse gRPC de TensorFlow Serving
Comme pour gRPC, vous mettez en œuvre la fonction postprocessGRPCResponse()
pour gérer la réponse.
private void postprocessGRPCResponse(Predict.PredictResponse response) {
}
- Ajoutez ce code à la fonction
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);
La fonction de post-traitement peut désormais extraire les valeurs prédites de la réponse et afficher le cadre de délimitation de la détection dans l'interface utilisateur.
Exécuter le code
- Dans le menu de navigation, cliquez sur
Exécuter "app", puis attendez que l'application se charge.
- Sélectionnez Webhook > Exécuter l'inférence.
Il faut quelques secondes pour que l'application affiche le cadre de délimitation du chat et affiche 17
comme catégorie de l'objet, qui correspond à la catégorie cat
dans l'ensemble de données COCO.
8. Félicitations
Vous avez utilisé TensorFlow Serving pour ajouter des fonctionnalités de détection d'objets à votre application !