עיבוד נתוני קלט ופלט באמצעות ספריית התמיכה של TensorFlow Lite

בדרך כלל, מפתחי אפליקציות לנייד יוצרים אינטראקציה עם אובייקטים מוקלדים, כמו מפות סיביות או פרימיטיבים, כמו מספרים שלמים. עם זאת, ב-API של התרגום ב-TensorFlow Lite, שמפעיל את המודל של למידת מכונה במכשיר, נעשה שימוש במתקנים מסוג ByteBuffer, שקשה לנפות באגים ולבצע בהם שינויים. ספריית התמיכה ל-Android ב-TensorFlow Lite נועדה לעבד את הקלט והפלט של דגמי TensorFlow Lite, ולהקל על השימוש במתרגם ב-TensorFlow Lite.

תחילת העבודה

ייבוא תלות של Gradle והגדרות אחרות

מעתיקים את קובץ המודל .tflite לספריית הנכסים של מודול Android, שבה המודל יפעל. מציינים שלא לדחוס את הקובץ, ומוסיפים את ספריית TensorFlow Lite לקובץ build.gradle של המודול:

android {
    // Other settings

    // Specify tflite file should not be compressed for the app apk
    aaptOptions {
        noCompress "tflite"
    }

}

dependencies {
    // Other dependencies

    // Import tflite dependencies
    implementation 'org.tensorflow:tensorflow-lite:0.0.0-nightly-SNAPSHOT'
    // The GPU delegate library is optional. Depend on it as needed.
    implementation 'org.tensorflow:tensorflow-lite-gpu:0.0.0-nightly-SNAPSHOT'
    implementation 'org.tensorflow:tensorflow-lite-support:0.0.0-nightly-SNAPSHOT'
}

עיון בגרסאות השונות של ספריית התמיכה של TensorFlow Lite AAR שמתארח ב-MavenCentral

מניפולציה והמרה של תמונות בסיסיות

בספריית התמיכה של TensorFlow Lite יש חבילה של שיטות בסיסיות לשינוי תמונות, כמו חיתוך ושינוי גודל. כדי להשתמש בו, צריך ליצור ImagePreprocessor ולהוסיף את הפעולות הנדרשות. כדי להמיר את התמונה לפורמט tensor שנדרש על ידי התרגום של TensorFlow Lite, צריך ליצור TensorImage שישמש כקלט:

import org.tensorflow.lite.DataType;
import org.tensorflow.lite.support.image.ImageProcessor;
import org.tensorflow.lite.support.image.TensorImage;
import org.tensorflow.lite.support.image.ops.ResizeOp;

// Initialization code
// Create an ImageProcessor with all ops required. For more ops, please
// refer to the ImageProcessor Architecture section in this README.
ImageProcessor imageProcessor =
    new ImageProcessor.Builder()
        .add(new ResizeOp(224, 224, ResizeOp.ResizeMethod.BILINEAR))
        .build();

// Create a TensorImage object. This creates the tensor of the corresponding
// tensor type (uint8 in this case) that the TensorFlow Lite interpreter needs.
TensorImage tensorImage = new TensorImage(DataType.UINT8);

// Analysis code for every frame
// Preprocess the image
tensorImage.load(bitmap);
tensorImage = imageProcessor.process(tensorImage);

אפשר לקרוא את DataType של tensor בספריית המטא-נתונים לחילוץ נתונים, וגם במידע אחר של המודל.

עיבוד בסיסי של נתוני אודיו

בספריית התמיכה של TensorFlow Lite מוגדרת גם אריזה של TensorAudio של כמה שיטות בסיסיות לעיבוד נתוני אודיו. משתמשים בו בדרך כלל בשילוב עם AudioRecord ומתעדים דגימות אודיו במאגר נתונים זמני של הצלצול.

import android.media.AudioRecord;
import org.tensorflow.lite.support.audio.TensorAudio;

// Create an `AudioRecord` instance.
AudioRecord record = AudioRecord(...)

// Create a `TensorAudio` object from Android AudioFormat.
TensorAudio tensorAudio = new TensorAudio(record.getFormat(), size)

// Load all audio samples available in the AudioRecord without blocking.
tensorAudio.load(record)

// Get the `TensorBuffer` for inference.
TensorBuffer buffer = tensorAudio.getTensorBuffer()

יצירת אובייקטי פלט והרצת המודל

לפני הרצת המודל, אנחנו צריכים ליצור את האובייקטים של הקונטיינר שיאחסנו את התוצאה:

import org.tensorflow.lite.DataType;
import org.tensorflow.lite.support.tensorbuffer.TensorBuffer;

// Create a container for the result and specify that this is a quantized model.
// Hence, the 'DataType' is defined as UINT8 (8-bit unsigned integer)
TensorBuffer probabilityBuffer =
    TensorBuffer.createFixedSize(new int[]{1, 1001}, DataType.UINT8);

טעינת המודל וההסקה שפועלת:

import java.nio.MappedByteBuffer;
import org.tensorflow.lite.InterpreterFactory;
import org.tensorflow.lite.InterpreterApi;

// Initialise the model
try{
    MappedByteBuffer tfliteModel
        = FileUtil.loadMappedFile(activity,
            "mobilenet_v1_1.0_224_quant.tflite");
    InterpreterApi tflite = new InterpreterFactory().create(
        tfliteModel, new InterpreterApi.Options());
} catch (IOException e){
    Log.e("tfliteSupport", "Error reading model", e);
}

// Running inference
if(null != tflite) {
    tflite.run(tImage.getBuffer(), probabilityBuffer.getBuffer());
}

איך ניגשים לתוצאה

המפתחים יכולים לגשת לפלט ישירות דרך probabilityBuffer.getFloatArray(). אם המודל מפיק פלט כמותית, זכרו להמיר את התוצאה. במודל הכמותי של MobileNet, המפתח צריך לחלק כל ערך פלט ב-255 כדי לקבל את ההסתברות שנעה בין 0 (הסבירות הנמוכה ביותר) עד 1 (הסבירות הגבוהה ביותר) לכל קטגוריה.

אופציונלי: מיפוי תוצאות לתוויות

מפתחים יכולים גם למפות את התוצאות לתוויות. קודם כל, מעתיקים את קובץ הטקסט שמכיל תוויות לספריית הנכסים של המודול. לאחר מכן, טוענים את קובץ התווית באמצעות הקוד הבא:

import org.tensorflow.lite.support.common.FileUtil;

final String ASSOCIATED_AXIS_LABELS = "labels.txt";
List<String> associatedAxisLabels = null;

try {
    associatedAxisLabels = FileUtil.loadLabels(this, ASSOCIATED_AXIS_LABELS);
} catch (IOException e) {
    Log.e("tfliteSupport", "Error reading label file", e);
}

קטע הקוד הבא מדגים איך לשייך את ההסתברויות לתוויות של קטגוריות:

import java.util.Map;
import org.tensorflow.lite.support.common.TensorProcessor;
import org.tensorflow.lite.support.common.ops.NormalizeOp;
import org.tensorflow.lite.support.label.TensorLabel;

// Post-processor which dequantize the result
TensorProcessor probabilityProcessor =
    new TensorProcessor.Builder().add(new NormalizeOp(0, 255)).build();

if (null != associatedAxisLabels) {
    // Map of labels and their corresponding probability
    TensorLabel labels = new TensorLabel(associatedAxisLabels,
        probabilityProcessor.process(probabilityBuffer));

    // Create a map to access the result based on label
    Map<String, Float> floatMap = labels.getMapWithFloatValue();
}

כיסוי התרחיש לדוגמה הנוכחי

הגרסה הנוכחית של ספריית התמיכה ב-TensorFlow Lite מכסה:

  • סוגי נתונים נפוצים (float, uint8, תמונות, אודיו ומערך של האובייקטים האלה) כקלט ופלט של מודלים של tflite.
  • פעולות בסיסיות של התמונה (חיתוך תמונה, שינוי גודל וסיבוב).
  • נירמול וקוונטיזציה
  • רכיבי utils של קבצים

גרסאות עתידיות ישפרו את התמיכה באפליקציות הקשורות לטקסט.

ארכיטקטורת ImageProcessor

העיצוב של ImageProcessor אפשר לבצע מראש את הפעולות של מניפולציית התמונות ולבצע אופטימיזציה שלהן במהלך תהליך ה-build. בשלב זה ImageProcessor תומך בשלוש פעולות בסיסיות של עיבוד מראש, כפי שמתואר בשלוש התגובות בקטע הקוד הבא:

import org.tensorflow.lite.support.common.ops.NormalizeOp;
import org.tensorflow.lite.support.common.ops.QuantizeOp;
import org.tensorflow.lite.support.image.ops.ResizeOp;
import org.tensorflow.lite.support.image.ops.ResizeWithCropOrPadOp;
import org.tensorflow.lite.support.image.ops.Rot90Op;

int width = bitmap.getWidth();
int height = bitmap.getHeight();

int size = height > width ? width : height;

ImageProcessor imageProcessor =
    new ImageProcessor.Builder()
        // Center crop the image to the largest square possible
        .add(new ResizeWithCropOrPadOp(size, size))
        // Resize using Bilinear or Nearest neighbour
        .add(new ResizeOp(224, 224, ResizeOp.ResizeMethod.BILINEAR));
        // Rotation counter-clockwise in 90 degree increments
        .add(new Rot90Op(rotateDegrees / 90))
        .add(new NormalizeOp(127.5, 127.5))
        .add(new QuantizeOp(128.0, 1/128.0))
        .build();

כאן יש פרטים נוספים על נירמול וקוונטיזציה.

המטרה הסופית של ספריית התמיכה היא לתמוך בכל הטרנספורמציות של tf.image. המשמעות היא שהטרנספורמציה תהיה זהה ל-TensorFlow, וההטמעה לא תהיה תלויה במערכת ההפעלה.

בנוסף, המפתחים מוזמנים ליצור מעבדים בהתאמה אישית. במקרים כאלה חשוב להתאים לתהליך האימון, כלומר, אותו עיבוד מראש צריך לחול גם על האימון וגם על ההסקה, כדי להגביר את יכולת השחזור.

כמות

כשמפעילים אובייקטים של קלט או פלט כמו TensorImage או TensorBuffer, צריך לציין את הסוגים שלהם כך DataType.UINT8 או DataType.FLOAT32.

TensorImage tensorImage = new TensorImage(DataType.UINT8);
TensorBuffer probabilityBuffer =
    TensorBuffer.createFixedSize(new int[]{1, 1001}, DataType.UINT8);

אפשר להשתמש ב-TensorProcessor כדי לכמת את גורמי הקלט (tensor) של הקלט או כדי להפחית את הקונפיגורציה של הפלט. לדוגמה, כשמעבד פלט כמותית TensorBuffer, המפתח יכול להשתמש ב-DequantizeOp כדי להפחית את הקו של התוצאה להסתברות של נקודה צפה (floating-point) בין 0 ל-1:

import org.tensorflow.lite.support.common.TensorProcessor;

// Post-processor which dequantize the result
TensorProcessor probabilityProcessor =
    new TensorProcessor.Builder().add(new DequantizeOp(0, 1/255.0)).build();
TensorBuffer dequantizedBuffer = probabilityProcessor.process(probabilityBuffer);

אפשר לקרוא את הפרמטרים של הקוונטיזציה של tensor בספריית המטא-נתונים לחילוץ נתונים.