أنشئ واجهة برمجة تطبيقات المهام الخاصة بك

توفر مكتبة المهام TensorFlow Lite واجهات برمجة التطبيقات الأصلية/Android/iOS المعدة مسبقًا بالإضافة إلى البنية التحتية نفسها التي تلخص TensorFlow. يمكنك توسيع البنية الأساسية لـ Task API لإنشاء واجهات برمجة تطبيقات مخصصة إذا كان النموذج الخاص بك غير مدعوم من مكتبات المهام الموجودة.

ملخص

تحتوي البنية الأساسية لـ Task API على بنية مكونة من طبقتين: طبقة C++ السفلية التي تغلف وقت تشغيل TFLite الأصلي وطبقة Java/ObjC العليا التي تتواصل مع طبقة C++ من خلال JNI أو الغلاف الأصلي.

يؤدي تنفيذ كل منطق TensorFlow في C++ فقط إلى تقليل التكلفة وزيادة أداء الاستدلال إلى الحد الأقصى وتبسيط سير العمل الإجمالي عبر الأنظمة الأساسية.

لإنشاء فئة مهمة، قم بتوسيع BaseTaskApi لتوفير منطق التحويل بين واجهة نموذج TFLite وواجهة Task API، ثم استخدم أدوات Java/ObjC المساعدة لإنشاء واجهات برمجة التطبيقات المقابلة. مع إخفاء جميع تفاصيل TensorFlow، يمكنك نشر نموذج TFLite في تطبيقاتك دون أي معرفة بالتعلم الآلي.

يوفر TensorFlow Lite بعض واجهات برمجة التطبيقات المعدة مسبقًا لمهام Vision وNLP الأكثر شيوعًا. يمكنك إنشاء واجهات برمجة التطبيقات (API) الخاصة بك لمهام أخرى باستخدام البنية الأساسية لـ Task API.

prebuilt_task_apis
الشكل 1. واجهات برمجة التطبيقات للمهام المعدة مسبقًا

أنشئ واجهة برمجة التطبيقات (API) الخاصة بك باستخدام Task API infra

واجهة برمجة تطبيقات C++

يتم تنفيذ جميع تفاصيل TFLite في واجهة برمجة التطبيقات الأصلية. قم بإنشاء كائن API باستخدام إحدى وظائف المصنع واحصل على نتائج النموذج عن طريق استدعاء الوظائف المحددة في الواجهة.

استخدام العينة

فيما يلي مثال باستخدام C++ BertQuestionAnswerer لـ MobileBert .

  char kBertModelPath[] = "path/to/model.tflite";
  // Create the API from a model file
  std::unique_ptr<BertQuestionAnswerer> question_answerer =
      BertQuestionAnswerer::CreateFromFile(kBertModelPath);

  char kContext[] = ...; // context of a question to be answered
  char kQuestion[] = ...; // question to be answered
  // ask a question
  std::vector<QaAnswer> answers = question_answerer.Answer(kContext, kQuestion);
  // answers[0].text is the best answer

بناء واجهة برمجة التطبيقات

original_task_api
الشكل 2. واجهة برمجة التطبيقات للمهام الأصلية

لإنشاء كائن API، يجب عليك توفير المعلومات التالية عن طريق توسيع BaseTaskApi

  • تحديد الإدخال/الإخراج لواجهة برمجة التطبيقات (API) - يجب أن تعرض واجهة برمجة التطبيقات (API) لديك مدخلات/مخرجات مماثلة عبر منصات مختلفة. على سبيل المثال، يأخذ BertQuestionAnswerer سلسلتين (std::string& context, std::string& question) كمدخلات ويخرج متجهًا للإجابة والاحتمالات المحتملة مثل std::vector<QaAnswer> . ويتم ذلك عن طريق تحديد الأنواع المقابلة في معلمة قالب BaseTaskApi . مع تحديد معلمات القالب، ستحتوي الدالة BaseTaskApi::Infer على أنواع الإدخال/الإخراج الصحيحة. يمكن لعملاء واجهة برمجة التطبيقات (API) استدعاء هذه الوظيفة مباشرةً، لكن من الممارسات الجيدة تغليفها داخل دالة خاصة بنموذج معين، في هذه الحالة، BertQuestionAnswerer::Answer .

    class BertQuestionAnswerer : public BaseTaskApi<
                                  std::vector<QaAnswer>, // OutputType
                                  const std::string&, const std::string& // InputTypes
                                  > {
      // Model specific function delegating calls to BaseTaskApi::Infer
      std::vector<QaAnswer> Answer(const std::string& context, const std::string& question) {
        return Infer(context, question).value();
      }
    }
    
  • توفير منطق التحويل بين الإدخال/الإخراج لواجهة برمجة التطبيقات (API) وموتر الإدخال/الإخراج للنموذج - مع تحديد أنواع الإدخال والإخراج، تحتاج الفئات الفرعية أيضًا إلى تنفيذ الوظائف المكتوبة BaseTaskApi::Preprocess و BaseTaskApi::Postprocess . توفر الوظيفتان المدخلات والمخرجات من TFLite FlatBuffer . الفئة الفرعية مسؤولة عن تعيين القيم من موترات الإدخال/الإخراج لواجهة برمجة التطبيقات (API) إلى موترات الإدخال/الإخراج. راجع مثال التنفيذ الكامل في BertQuestionAnswerer .

    class BertQuestionAnswerer : public BaseTaskApi<
                                  std::vector<QaAnswer>, // OutputType
                                  const std::string&, const std::string& // InputTypes
                                  > {
      // Convert API input into tensors
      absl::Status BertQuestionAnswerer::Preprocess(
        const std::vector<TfLiteTensor*>& input_tensors, // input tensors of the model
        const std::string& context, const std::string& query // InputType of the API
      ) {
        // Perform tokenization on input strings
        ...
        // Populate IDs, Masks and SegmentIDs to corresponding input tensors
        PopulateTensor(input_ids, input_tensors[0]);
        PopulateTensor(input_mask, input_tensors[1]);
        PopulateTensor(segment_ids, input_tensors[2]);
        return absl::OkStatus();
      }
    
      // Convert output tensors into API output
      StatusOr<std::vector<QaAnswer>> // OutputType
      BertQuestionAnswerer::Postprocess(
        const std::vector<const TfLiteTensor*>& output_tensors, // output tensors of the model
      ) {
        // Get start/end logits of prediction result from output tensors
        std::vector<float> end_logits;
        std::vector<float> start_logits;
        // output_tensors[0]: end_logits FLOAT[1, 384]
        PopulateVector(output_tensors[0], &end_logits);
        // output_tensors[1]: start_logits FLOAT[1, 384]
        PopulateVector(output_tensors[1], &start_logits);
        ...
        std::vector<QaAnswer::Pos> orig_results;
        // Look up the indices from vocabulary file and build results
        ...
        return orig_results;
      }
    }
    
  • إنشاء وظائف المصنع لواجهة برمجة التطبيقات (API) - يلزم وجود ملف نموذجي و OpResolver لتهيئة tflite::Interpreter . يوفر TaskAPIFactory وظائف مساعدة لإنشاء مثيلات BaseTaskApi.

    يجب عليك أيضًا تقديم أي ملفات مرتبطة بالنموذج. على سبيل المثال، يمكن أن يحتوي BertQuestionAnswerer أيضًا على ملف إضافي لمفردات أداة الرمز المميزة الخاصة به.

    class BertQuestionAnswerer : public BaseTaskApi<
                                  std::vector<QaAnswer>, // OutputType
                                  const std::string&, const std::string& // InputTypes
                                  > {
      // Factory function to create the API instance
      StatusOr<std::unique_ptr<QuestionAnswerer>>
      BertQuestionAnswerer::CreateBertQuestionAnswerer(
          const std::string& path_to_model, // model to passed to TaskApiFactory
          const std::string& path_to_vocab  // additional model specific files
      ) {
        // Creates an API object by calling one of the utils from TaskAPIFactory
        std::unique_ptr<BertQuestionAnswerer> api_to_init;
        ASSIGN_OR_RETURN(
            api_to_init,
            core::TaskAPIFactory::CreateFromFile<BertQuestionAnswerer>(
                path_to_model,
                absl::make_unique<tflite::ops::builtin::BuiltinOpResolver>(),
                kNumLiteThreads));
    
        // Perform additional model specific initializations
        // In this case building a vocabulary vector from the vocab file.
        api_to_init->InitializeVocab(path_to_vocab);
        return api_to_init;
      }
    }
    

واجهة برمجة تطبيقات أندرويد

قم بإنشاء واجهات برمجة تطبيقات Android عن طريق تحديد واجهة Java/Kotlin وتفويض المنطق إلى طبقة C++ من خلال JNI. يتطلب Android API إنشاء واجهة برمجة التطبيقات الأصلية أولاً.

استخدام العينة

فيما يلي مثال لاستخدام Java BertQuestionAnswerer لـ MobileBert .

  String BERT_MODEL_FILE = "path/to/model.tflite";
  String VOCAB_FILE = "path/to/vocab.txt";
  // Create the API from a model file and vocabulary file
    BertQuestionAnswerer bertQuestionAnswerer =
        BertQuestionAnswerer.createBertQuestionAnswerer(
            ApplicationProvider.getApplicationContext(), BERT_MODEL_FILE, VOCAB_FILE);

  String CONTEXT = ...; // context of a question to be answered
  String QUESTION = ...; // question to be answered
  // ask a question
  List<QaAnswer> answers = bertQuestionAnswerer.answer(CONTEXT, QUESTION);
  // answers.get(0).text is the best answer

بناء واجهة برمجة التطبيقات

android_task_api
الشكل 3. واجهة برمجة تطبيقات مهام Android

كما هو الحال مع واجهات برمجة التطبيقات الأصلية، لإنشاء كائن API، يحتاج العميل إلى توفير المعلومات التالية عن طريق توسيع BaseTaskApi ، الذي يوفر معالجات JNI لجميع واجهات برمجة تطبيقات مهام Java.

  • تحديد واجهة الإدخال/الإخراج لواجهة برمجة التطبيقات (API) - وهذا عادةً ما يعكس الواجهات الأصلية. على سبيل المثال، BertQuestionAnswerer يأخذ (String context, String question) كمدخلات ومخرجات List<QaAnswer> . يستدعي التنفيذ دالة أصلية خاصة بتوقيع مماثل، إلا أنه يحتوي على معلمة إضافية long nativeHandle ، وهو المؤشر الذي تم إرجاعه من C++.

    class BertQuestionAnswerer extends BaseTaskApi {
      public List<QaAnswer> answer(String context, String question) {
        return answerNative(getNativeHandle(), context, question);
      }
    
      private static native List<QaAnswer> answerNative(
                                            long nativeHandle, // C++ pointer
                                            String context, String question // API I/O
                                           );
    
    }
    
  • إنشاء وظائف المصنع لواجهة برمجة التطبيقات (API) - يعكس هذا أيضًا وظائف المصنع الأصلية، باستثناء أن وظائف مصنع Android تحتاج أيضًا إلى اتخاذ Context للوصول إلى الملفات. يستدعي التنفيذ إحدى الأدوات المساعدة في TaskJniUtils لإنشاء كائن C++ API المقابل وتمرير المؤشر الخاص به إلى مُنشئ BaseTaskApi .

      class BertQuestionAnswerer extends BaseTaskApi {
        private static final String BERT_QUESTION_ANSWERER_NATIVE_LIBNAME =
                                                  "bert_question_answerer_jni";
    
        // Extending super constructor by providing the
        // native handle(pointer of corresponding C++ API object)
        private BertQuestionAnswerer(long nativeHandle) {
          super(nativeHandle);
        }
    
        public static BertQuestionAnswerer createBertQuestionAnswerer(
                                            Context context, // Accessing Android files
                                            String pathToModel, String pathToVocab) {
          return new BertQuestionAnswerer(
              // The util first try loads the JNI module with name
              // BERT_QUESTION_ANSWERER_NATIVE_LIBNAME, then opens two files,
              // converts them into ByteBuffer, finally ::initJniWithBertByteBuffers
              // is called with the buffer for a C++ API object pointer
              TaskJniUtils.createHandleWithMultipleAssetFilesFromLibrary(
                  context,
                  BertQuestionAnswerer::initJniWithBertByteBuffers,
                  BERT_QUESTION_ANSWERER_NATIVE_LIBNAME,
                  pathToModel,
                  pathToVocab));
        }
    
        // modelBuffers[0] is tflite model file buffer, and modelBuffers[1] is vocab file buffer.
        // returns C++ API object pointer casted to long
        private static native long initJniWithBertByteBuffers(ByteBuffer... modelBuffers);
    
      }
    
  • تنفيذ وحدة JNI للوظائف الأصلية - يتم تنفيذ جميع أساليب Java الأصلية عن طريق استدعاء وظيفة أصلية مقابلة من وحدة JNI. ستقوم وظائف المصنع بإنشاء كائن API أصلي وإرجاع المؤشر الخاص به كنوع طويل إلى Java. في الاستدعاءات اللاحقة لـ Java API، يتم تمرير مؤشر النوع الطويل مرة أخرى إلى JNI وإعادته إلى كائن API الأصلي. يتم بعد ذلك تحويل نتائج API الأصلية مرة أخرى إلى نتائج Java.

    على سبيل المثال، هذه هي الطريقة التي يتم بها تنفيذ bert_question_answerer_jni .

      // Implements BertQuestionAnswerer::initJniWithBertByteBuffers
      extern "C" JNIEXPORT jlong JNICALL
      Java_org_tensorflow_lite_task_text_qa_BertQuestionAnswerer_initJniWithBertByteBuffers(
          JNIEnv* env, jclass thiz, jobjectArray model_buffers) {
        // Convert Java ByteBuffer object into a buffer that can be read by native factory functions
        absl::string_view model =
            GetMappedFileBuffer(env, env->GetObjectArrayElement(model_buffers, 0));
    
        // Creates the native API object
        absl::StatusOr<std::unique_ptr<QuestionAnswerer>> status =
            BertQuestionAnswerer::CreateFromBuffer(
                model.data(), model.size());
        if (status.ok()) {
          // converts the object pointer to jlong and return to Java.
          return reinterpret_cast<jlong>(status->release());
        } else {
          return kInvalidPointer;
        }
      }
    
      // Implements BertQuestionAnswerer::answerNative
      extern "C" JNIEXPORT jobject JNICALL
      Java_org_tensorflow_lite_task_text_qa_BertQuestionAnswerer_answerNative(
      JNIEnv* env, jclass thiz, jlong native_handle, jstring context, jstring question) {
      // Convert long to native API object pointer
      QuestionAnswerer* question_answerer = reinterpret_cast<QuestionAnswerer*>(native_handle);
    
      // Calls the native API
      std::vector<QaAnswer> results = question_answerer->Answer(JStringToString(env, context),
                                             JStringToString(env, question));
    
      // Converts native result(std::vector<QaAnswer>) to Java result(List<QaAnswerer>)
      jclass qa_answer_class =
        env->FindClass("org/tensorflow/lite/task/text/qa/QaAnswer");
      jmethodID qa_answer_ctor =
        env->GetMethodID(qa_answer_class, "<init>", "(Ljava/lang/String;IIF)V");
      return ConvertVectorToArrayList<QaAnswer>(
        env, results,
        [env, qa_answer_class, qa_answer_ctor](const QaAnswer& ans) {
          jstring text = env->NewStringUTF(ans.text.data());
          jobject qa_answer =
              env->NewObject(qa_answer_class, qa_answer_ctor, text, ans.pos.start,
                             ans.pos.end, ans.pos.logit);
          env->DeleteLocalRef(text);
          return qa_answer;
        });
      }
    
      // Implements BaseTaskApi::deinitJni by delete the native object
      extern "C" JNIEXPORT void JNICALL Java_task_core_BaseTaskApi_deinitJni(
          JNIEnv* env, jobject thiz, jlong native_handle) {
        delete reinterpret_cast<QuestionAnswerer*>(native_handle);
      }
    

واجهة برمجة تطبيقات iOS

قم بإنشاء واجهات برمجة تطبيقات iOS عن طريق التفاف كائن API الأصلي في كائن ObjC API. يمكن استخدام كائن API الذي تم إنشاؤه في ObjC أو Swift. يتطلب iOS API إنشاء واجهة برمجة التطبيقات الأصلية أولاً.

استخدام العينة

فيما يلي مثال لاستخدام ObjC TFLBertQuestionAnswerer لـ MobileBert في Swift.

  static let mobileBertModelPath = "path/to/model.tflite";
  // Create the API from a model file and vocabulary file
  let mobileBertAnswerer = TFLBertQuestionAnswerer.mobilebertQuestionAnswerer(
      modelPath: mobileBertModelPath)

  static let context = ...; // context of a question to be answered
  static let question = ...; // question to be answered
  // ask a question
  let answers = mobileBertAnswerer.answer(
      context: TFLBertQuestionAnswererTest.context, question: TFLBertQuestionAnswererTest.question)
  // answers.[0].text is the best answer

بناء واجهة برمجة التطبيقات

ios_task_api
الشكل 4. واجهة برمجة تطبيقات مهام iOS

iOS API عبارة عن غلاف ObjC بسيط أعلى واجهة برمجة التطبيقات الأصلية. أنشئ واجهة برمجة التطبيقات (API) باتباع الخطوات التالية:

  • تحديد غلاف ObjC - تحديد فئة ObjC وتفويض التطبيقات إلى كائن API الأصلي المقابل. لاحظ أن التبعيات الأصلية يمكن أن تظهر فقط في ملف .mm بسبب عدم قدرة Swift على التفاعل مع C++.

    • ملف .h
      @interface TFLBertQuestionAnswerer : NSObject
    
      // Delegate calls to the native BertQuestionAnswerer::CreateBertQuestionAnswerer
      + (instancetype)mobilebertQuestionAnswererWithModelPath:(NSString*)modelPath
                                                    vocabPath:(NSString*)vocabPath
          NS_SWIFT_NAME(mobilebertQuestionAnswerer(modelPath:vocabPath:));
    
      // Delegate calls to the native BertQuestionAnswerer::Answer
      - (NSArray<TFLQAAnswer*>*)answerWithContext:(NSString*)context
                                         question:(NSString*)question
          NS_SWIFT_NAME(answer(context:question:));
    }
    
    • ملف .mm
      using BertQuestionAnswererCPP = ::tflite::task::text::BertQuestionAnswerer;
    
      @implementation TFLBertQuestionAnswerer {
        // define an iVar for the native API object
        std::unique_ptr<QuestionAnswererCPP> _bertQuestionAnswerwer;
      }
    
      // Initialize the native API object
      + (instancetype)mobilebertQuestionAnswererWithModelPath:(NSString *)modelPath
                                              vocabPath:(NSString *)vocabPath {
        absl::StatusOr<std::unique_ptr<QuestionAnswererCPP>> cQuestionAnswerer =
            BertQuestionAnswererCPP::CreateBertQuestionAnswerer(MakeString(modelPath),
                                                                MakeString(vocabPath));
        _GTMDevAssert(cQuestionAnswerer.ok(), @"Failed to create BertQuestionAnswerer");
        return [[TFLBertQuestionAnswerer alloc]
            initWithQuestionAnswerer:std::move(cQuestionAnswerer.value())];
      }
    
      // Calls the native API and converts C++ results into ObjC results
      - (NSArray<TFLQAAnswer *> *)answerWithContext:(NSString *)context question:(NSString *)question {
        std::vector<QaAnswerCPP> results =
          _bertQuestionAnswerwer->Answer(MakeString(context), MakeString(question));
        return [self arrayFromVector:results];
      }
    }