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ột mô hình và tiến hành dự đoán bằng bộ vi điều khiển.

Ví dụ về Hello World

Ví dụ Hello World được thiết kế để minh hoạ các kiến thức cơ bản tuyệt đối về cách sử dụng TensorFlow Lite cho Bộ vi điều khiển. Chúng tôi huấn luyện và chạy một mô hình tái tạo một hàm sin, tức là lấy một số duy nhất làm dữ liệu đầu vào và cho ra giá trị sin của số đó. Khi được triển khai cho bộ vi điều khiển, các thông tin dự đoán của bộ vi điều khiển sẽ đượ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ô hình (bằng Python): Một tệp python để huấn luyện, chuyển đổi và tối ưu hoá mô hình để sử dụng trên thiết bị.
  2. Chạy dự đoán (trong C++ 17): Một chương trình kiểm thử đơn vị toàn diện chạy dự đoán trên mô hình bằng cách sử dụng thư viện C++.

Tải một thiết bị được hỗ trợ

Ứng dụng mẫu chúng ta sẽ sử dụng đã được kiểm thử trên các điện thoại sau:

Tìm hiểu thêm về các nền tảng được hỗ trợ trong TensorFlow Lite 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 nhằm 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 suy luận

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

Xin chào README.md

Các phần sau đây trình bày về phương thức kiểm thử đơn vị evaluate_test.cc trong ví dụ này, minh hoạ cách chạy dự đoán bằng TensorFlow Lite cho Bộ vi điều khiển. Chuỗi này sẽ 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 TensorFlow Lite cho thư viện 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 của TensorFlow Lite cho Vi điều khiển dự kiến sẽ cung cấp mô hình dưới dạng một mảng C++. Mô hình được xác định trong các tệp model.hmodel.cc. Tiêu đề này có kèm theo 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 khung kiểm thử đơn vị TensorFlow Lite cho Vi điều khiển vào bằng cách thêm dòng sau:

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

Kiểm thử đượ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ó trong 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 cách sử dụng con trỏ đến thực thể tflite::MicroErrorReporter:

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

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

5. Tải một mô hình

Trong mã sau, mô hình được tạo thực thể bằng dữ liệu của mảng char (g_model), được khai báo trong model.h. Sau đó, chúng tôi kiểm tra mô hình để đảm bảo phiên bản giản đồ của mô hình đó tương thích với phiên bản mà 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. Trình phân giải hoạt động tạo thực thể

Đã khai báo một thực thể MicroMutableOpResolver. Trình thông dịch sẽ sử dụng dữ liệu này để đăng ký và truy cập vào các thao tác 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 yêu cầu một tham số mẫu cho biết số lượng 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 lượng bộ nhớ nhất định cho các mảng đầu vào, đầu ra và trung gian. Mảng này được cung cấp dưới dạng một mảng uint8_t có kích thước tensor_arena_size:

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

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

8. Tạo phiên dịch

Chúng ta tạo một thực thể tflite::MicroInterpreter, truyền các biến đã 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 một con trỏ đến tensor đầu vào của mô hình bằng cách gọi .input(0), trong đó 0 biểu thị tensor đầu vào đầu tiên (và duy nhất):

  // 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à loại của tensor này đúng như chúng ta 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 loại dữ liệu TensorFlow Lite và được xác định trong common.h.

11. Cung cấp giá trị đầu vào

Để cung cấp dữ liệu đầ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 thực thể tflite::MicroInterpreter:

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 hay không. Các giá trị TfLiteStatus có thể có, được xác định trong common.h, là kTfLiteOkkTfLiteError.

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

TF_LITE_MICRO_EXPECT_EQ(kTfLiteOk, invoke_status);

13. Nhận kết quả

Bạn có thể lấy 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 tensor đầu ra đầu tiên (và duy nhất).

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 chứa trong một 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 giá trị trực tiếp từ tensor đầu ra và xác nhận rằng đó là điều chúng ta 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 thực thể, 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ả từ 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);