Làm quen với bộ vi điều khiển

Tài liệu này giải thích cách huấn luyện mô hình và chạy suy luận bằng cách sử dụng vi điều khiển.

Ví dụ về Hello World

Chiến lược phát hành đĩa đơn Xin chào mọi người được thiết kế để thể hiện kiến thức cơ bản tuyệt đối về việc sử dụng LiteRT cho Vi điều khiển. Chúng tôi huấn luyện và chạy một mô hình sao chép hàm sin, tức là hàm lấy một số duy nhất làm đầu vào và cho ra kết quả của số đó sin. Khi được triển khai cho vi điều khiển, các dự đoán của bộ vi điều khiển được dùng để nhấp nháy đèn LED hoặc điều khiển ảnh động.

Quy trình làm việc toàn diện bao gồm các bước sau:

  1. Đào tạo một mô hình (bằng Python): Tệp python để huấn luyện, chuyển đổi và tối ưu hoá một mô hình để sử dụng trên thiết bị.
  2. Chạy suy luận (trong C++ 17): Kiểm thử đơn vị toàn diện chạy suy luận trên mô hình bằng cách sử dụng thư viện C++.

Mua một thiết bị được hỗ trợ

Ứng dụng mẫu chúng ta sẽ sử dụng đã được thử nghiệm trên các thiết bị sau:

Tìm hiểu thêm về các nền tảng được hỗ trợ trong LiteRT cho vi điều khiển.

Huấn luyện người mẫu

Sử dụng train.py để huấn luyện mô hình Hello world (mô hình xin chào thế giới) về nhận dạng sinwave

Chạy: bazel build tensorflow/lite/micro/examples/hello_world:train bazel-bin/tensorflow/lite/micro/examples/hello_world/train --save_tf_model --save_dir=/tmp/model_created/

Chạy dự đoán

Để chạy mô hình trên thiết bị của bạn, chúng tôi sẽ xem xét hướng dẫn trong README.md:

Chào bạn README.md trên thế giới

Các phần sau đây sẽ hướng dẫn bạn evaluate_test.cc! phương thức kiểm thử đơn vị minh hoạ cách chạy dự đoán bằng LiteRT cho Vi điều khiển. Trình này tải mô hình và chạy dự đoán nhiều lần.

1. Thêm tiêu đề thư viện

Để sử dụng thư viện LiteRT cho Vi điều khiển, chúng ta phải đưa vào các tệp tiêu đề sau:

#include "tensorflow/lite/micro/micro_mutable_op_resolver.h"
#include "tensorflow/lite/micro/micro_error_reporter.h"
#include "tensorflow/lite/micro/micro_interpreter.h"
#include "tensorflow/lite/schema/schema_generated.h"
#include "tensorflow/lite/version.h"

2. Thêm tiêu đề mô hình

Trình thông dịch LiteRT cho bộ vi điều khiển dự kiến rằng mô hình sẽ được cung cấp dưới dạng mảng C++. Mô hình này được xác định trong các tệp model.hmodel.cc. Tiêu đề này được bao gồm với dòng sau:

#include "tensorflow/lite/micro/examples/hello_world/model.h"

3. Thêm tiêu đề khung kiểm thử đơn vị

Để tạo một kiểm thử đơn vị, chúng tôi đưa LiteRT vào cho Khung kiểm thử đơn vị bộ vi điều khiển bằng cách thêm dòng sau:

#include "tensorflow/lite/micro/testing/micro_test.h"

Chương trình kiểm thử này được xác định bằng các macro sau:

TF_LITE_MICRO_TESTS_BEGIN

TF_LITE_MICRO_TEST(LoadModelAndPerformInference) {
  . // add code here
  .
}

TF_LITE_MICRO_TESTS_END

Bây giờ, chúng ta sẽ thảo luận về mã được đưa vào macro ở trên.

4. Thiết lập tính năng ghi nhật ký

Để thiết lập tính năng ghi nhật ký, con trỏ tflite::ErrorReporter sẽ được tạo bằng con trỏ đến một thực thể tflite::MicroErrorReporter:

tflite::MicroErrorReporter micro_error_reporter;
tflite::ErrorReporter* error_reporter = &micro_error_reporter;

Biến này sẽ được chuyển vào trình phiên dịch, cho phép biến viết nhật ký. Vì bộ vi điều khiển thường có nhiều cơ chế để ghi nhật ký, Việc triển khai tflite::MicroErrorReporter được thiết kế để tùy chỉnh cho thiết bị cụ thể của bạn.

5. Tải mô hình

Trong mã sau, mô hình được tạo thực thể bằng cách sử dụng dữ liệu từ mảng char, g_model được khai báo trong model.h. Sau đó, chúng tôi sẽ kiểm tra mô hình để đảm bảo phiên bản giản đồ tương thích với phiên bản chúng tôi đang sử dụng:

const tflite::Model* model = ::tflite::GetModel(g_model);
if (model->version() != TFLITE_SCHEMA_VERSION) {
  TF_LITE_REPORT_ERROR(error_reporter,
      "Model provided is schema version %d not equal "
      "to supported version %d.\n",
      model->version(), TFLITE_SCHEMA_VERSION);
}

6. Tạo thực thể trình phân giải hoạt động

Đáp MicroMutableOpResolver thực thể được khai báo. Thông tin này sẽ được phiên dịch viên sử dụng để đăng ký và truy cập vào các toán tử mà mô hình sử dụng:

using HelloWorldOpResolver = tflite::MicroMutableOpResolver<1>;

TfLiteStatus RegisterOps(HelloWorldOpResolver& op_resolver) {
  TF_LITE_ENSURE_STATUS(op_resolver.AddFullyConnected());
  return kTfLiteOk;

MicroMutableOpResolver cần có một tham số mẫu cho biết số hoạt động sẽ được đăng ký. Hàm RegisterOps đăng ký hoạt động với trình phân giải.

HelloWorldOpResolver op_resolver;
TF_LITE_ENSURE_STATUS(RegisterOps(op_resolver));

7. Phân bổ bộ nhớ

Chúng ta cần phân bổ trước một mức bộ nhớ nhất định cho dữ liệu đầu vào, đầu ra và mảng trung gian. Giá trị này được cung cấp dưới dạng mảng kích thước uint8_t tensor_arena_size:

const int tensor_arena_size = 2 * 1024;
uint8_t tensor_arena[tensor_arena_size];

Kích thước bắt buộc sẽ phụ thuộc vào mô hình bạn đang sử dụng và có thể cần phải được xác định bằng thử nghiệm.

8. Tạo thực thể phiên dịch

Chúng ta tạo một thực thể tflite::MicroInterpreter, truyền các biến vào được tạo trước đó:

tflite::MicroInterpreter interpreter(model, resolver, tensor_arena,
                                     tensor_arena_size, error_reporter);

9. Phân bổ tensor

Chúng ta yêu cầu trình thông dịch phân bổ bộ nhớ từ tensor_arena cho tensor của mô hình:

interpreter.AllocateTensors();

10. Xác thực hình dạng nhập

Thực thể MicroInterpreter có thể cung cấp cho chúng ta con trỏ đến đầu vào tensor bằng cách gọi .input(0), trong đó 0 đại diện cho tensor đầu tiên (và duy nhất) tensor đầu vào:

  // Obtain a pointer to the model's input tensor
  TfLiteTensor* input = interpreter.input(0);

Sau đó, chúng ta kiểm tra tensor này để xác nhận rằng hình dạng và kiểu của tensor này đúng như bản chất của chúng ta đang mong đợi:

// Make sure the input has the properties we expect
TF_LITE_MICRO_EXPECT_NE(nullptr, input);
// The property "dims" tells us the tensor's shape. It has one element for
// each dimension. Our input is a 2D tensor containing 1 element, so "dims"
// should have size 2.
TF_LITE_MICRO_EXPECT_EQ(2, input->dims->size);
// The value of each element gives the length of the corresponding tensor.
// We should expect two single element tensors (one is contained within the
// other).
TF_LITE_MICRO_EXPECT_EQ(1, input->dims->data[0]);
TF_LITE_MICRO_EXPECT_EQ(1, input->dims->data[1]);
// The input is a 32 bit floating point value
TF_LITE_MICRO_EXPECT_EQ(kTfLiteFloat32, input->type);

Giá trị enum kTfLiteFloat32 là tham chiếu đến một trong các LiteRT và được xác định trong common.h.

11. Cung cấp giá trị nhập

Để cung cấp đầu vào cho mô hình, chúng ta đặt nội dung của tensor đầu vào như sau:

input->data.f[0] = 0.;

Trong trường hợp này, chúng ta sẽ nhập một giá trị dấu phẩy động đại diện cho 0.

12. Chạy mô hình

Để chạy mô hình này, chúng ta có thể gọi Invoke() trên tflite::MicroInterpreter thực thể:

TfLiteStatus invoke_status = interpreter.Invoke();
if (invoke_status != kTfLiteOk) {
  TF_LITE_REPORT_ERROR(error_reporter, "Invoke failed\n");
}

Chúng ta có thể kiểm tra giá trị trả về (TfLiteStatus) để xác định xem lần chạy có thành công. Các giá trị có thể có của TfLiteStatus, được xác định trong common.h, là kTfLiteOkkTfLiteError.

Mã sau đây khẳng định rằng giá trị là kTfLiteOk, nghĩa là suy luận đã chạy thành công.

TF_LITE_MICRO_EXPECT_EQ(kTfLiteOk, invoke_status);

13. Nhận kết quả

Bạn có thể thu được tensor đầu ra của mô hình bằng cách gọi output(0) trên tflite::MicroInterpreter, trong đó 0 đại diện cho đầu ra đầu tiên (và duy nhất) tensor.

Trong ví dụ này, đầu ra của mô hình là một giá trị dấu phẩy động duy nhất được chứa trong tensor 2D:

TfLiteTensor* output = interpreter.output(0);
TF_LITE_MICRO_EXPECT_EQ(2, output->dims->size);
TF_LITE_MICRO_EXPECT_EQ(1, input->dims->data[0]);
TF_LITE_MICRO_EXPECT_EQ(1, input->dims->data[1]);
TF_LITE_MICRO_EXPECT_EQ(kTfLiteFloat32, output->type);

Chúng ta có thể đọc trực tiếp giá trị từ tensor đầu ra và xác nhận rằng đó là giá trị chúng tôi mong đợi:

// Obtain the output value from the tensor
float value = output->data.f[0];
// Check that the output value is within 0.05 of the expected value
TF_LITE_MICRO_EXPECT_NEAR(0., value, 0.05);

14. Chạy lại suy luận

Phần còn lại của mã sẽ chạy suy luận thêm vài lần nữa. Trong mỗi trường hợp, chúng ta gán một giá trị cho tensor đầu vào, gọi trình thông dịch và đọc kết quả của tensor đầu ra:

input->data.f[0] = 1.;
interpreter.Invoke();
value = output->data.f[0];
TF_LITE_MICRO_EXPECT_NEAR(0.841, value, 0.05);

input->data.f[0] = 3.;
interpreter.Invoke();
value = output->data.f[0];
TF_LITE_MICRO_EXPECT_NEAR(0.141, value, 0.05);

input->data.f[0] = 5.;
interpreter.Invoke();
value = output->data.f[0];
TF_LITE_MICRO_EXPECT_NEAR(-0.959, value, 0.05);