إنشاء واجهة برمجة تطبيقات خاصة بخدمة Task API

توفّر TensorFlow Lite Task Library الذي تم إنشاؤه مسبقًا واجهات برمجة تطبيقات C++ وAndroid وiOS أعلى البنية الأساسية نفسها التي تستخلص منها TensorFlow. يمكنك توسيع البنية الأساسية لواجهة برمجة التطبيقات Task API لإنشاء واجهات برمجة تطبيقات مخصَّصة. إذا لم يكن نموذجك متوافقًا مع مكتبات المهام الحالية.

نظرة عامة

تتمتع البنية الأساسية لواجهة برمجة تطبيقات المهام بهيكل من طبقتين: طبقة C++ السفلية تغليف بيئة تشغيل TFLite وطبقة أعلى من Java/ObjC يتواصل مع طبقة C++ من خلال JNI أو برنامج تضمين.

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

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

يوفّر تطبيق TensorFlow Lite بعض واجهات برمجة التطبيقات المنشأة مسبقًا للتطبيقات الأكثر رواجًا. مهام الرؤية وNLP: يمكنك إنشاء واجهات برمجة التطبيقات الخاصة بك لمهام أخرى باستخدام البنية الأساسية لواجهة برمجة تطبيقات المهام.

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

إنشاء واجهة برمجة تطبيقات باستخدام Task API أدناه

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

يتم تنفيذ جميع تفاصيل TFLite في واجهة برمجة التطبيقات C++ 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

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

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

لإنشاء عنصر واجهة برمجة التطبيقات، يجب تقديم المعلومات التالية من خلال توسيع BaseTaskApi

  • تحديد واجهة برمجة التطبيقات I/O: من المفترض أن تعرض واجهة برمجة التطبيقات بيانات إدخال وإخراج مماثلة. عبر منصات مختلفة. مثلاً: تأخذ BertQuestionAnswerer سلسلتين (std::string& context, std::string& question) كإدخال وإخراج خط متجه للإجابة المحتملة والاحتمالات على النحو التالي std::vector<QaAnswer>. هذا النمط يتم من خلال تحديد الأنواع المطابقة في دالة BaseTaskApi مَعلمة النموذج. مع تحديد معلمات النموذج، BaseTaskApi::Infer أنواع الإدخال/الإخراج الصحيحة في الدالة. يمكن أن تكون هذه الدالة يتم استدعاؤها مباشرة عن طريق برامج واجهة برمجة التطبيقات، ولكن من الجيد إحاطتها داخل دالة محددة لنموذج، وهي 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();
      }
    }
    
  • توفير منطق الإحالة الناجحة بين إدخالات/إخراج واجهة برمجة التطبيقات وموتر الإدخال/الإخراج الخاص model - مع تحديد أنواع المدخلات والمخرجات، تحتاج الفئات الفرعية أيضًا إلى تنفيذ الدوال المكتوبة BaseTaskApi::Preprocess أو BaseTaskApi::Postprocess توفر الدالتان الإدخالات أو المخرجات من TFLite FlatBuffer. الفئة الفرعية مسؤولة عن تعيين من واجهة برمجة التطبيقات I/O إلى موتّرات وحدات الإدخال والإخراج. الاطّلاع على عملية التنفيذ الكاملة مثال في 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;
      }
    }
    
  • إنشاء وظائف المصنع لواجهة برمجة التطبيقات: ملف نموذج 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 API

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

مثال

فيما يلي مثال على استخدام 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"

على غرار واجهات برمجة التطبيقات الأصلية، يجب توفير واجهة برمجة التطبيقات للعميل المعلومات التالية من خلال تمديد BaseTaskApi, توفِّر عمليات معالجة JNI لجميع واجهات برمجة تطبيقات مهام Java.

  • تحديد إدخال/إخراج واجهة برمجة التطبيقات: يؤدي هذا عادةً إلى النسخ المطابق للواجهات الأصلية. مثلاً: يتم استخدام (String context, String question) كإدخال في BertQuestionAnswerer والنتائج 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
                                           );
    
    }
    
  • إنشاء وظائف المصنع لواجهة برمجة التطبيقات: كما أنه يتماشى مع المصنع الأصلي بخلاف ذلك، باستثناء أن وظائف Android المصنع تحتاج أيضًا إلى اتخاذ Context للوصول إلى الملفات. يستدعي التنفيذ إحدى الأدوات المساعدة في TaskJniUtils لإنشاء كائن واجهة برمجة تطبيقات C++ المقابل وتمرير مؤشر الماوس إلى الدالة الإنشائية 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 واحدة. ستعمل دوال المصنع على إنشاء كائن واجهة برمجة تطبيقات أصلي ثم إرجاع المؤشر الخاص بها كنوع طويل لـ Java. في الاتصالات اللاحقة بواجهة برمجة تطبيقات Java، يتم تمرير مؤشر type مرة أخرى إلى JNI وإعادته إلى كائن واجهة برمجة التطبيقات الأصلي. يتم بعد ذلك تحويل نتائج واجهة برمجة التطبيقات الأصلية إلى نتائج 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 من خلال دمج عنصر واجهة برمجة تطبيقات أصلي في كائن واجهة برمجة تطبيقات ObjC. تشير رسالة الأشكال البيانية يمكن استخدام كائن واجهة برمجة التطبيقات الذي تم إنشاؤه إما في ObjC أو Swift. تتطلب واجهة برمجة تطبيقات iOS الأصلية التي سيتم إنشاؤها أولاً.

مثال

فيما يلي مثال باستخدام 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 هي برنامج تضمين بسيط من نوع ObjC أعلى واجهة برمجة التطبيقات الأصلية. إنشاء واجهة برمجة التطبيقات من خلال باتباع الخطوات أدناه:

  • تحديد برنامج تضمين ObjC - تحديد فئة ObjC وتفويض عمليات التنفيذ لكائن واجهة برمجة التطبيقات الأصلي المقابل. ملاحظة المحتوى الأصلي يمكن أن تظهر التبعيات فقط في ملف .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];
      }
    }