TinyML 정의
TinyML은 에너지 비용이 1mW 미만인 하드웨어 플랫폼으로 정의할 수 있다. 즉 TinyML을 이해하려면 임베디드 장치를 이해해야 한다.
TinyML 적용가능 하드웨어
머신러닝이란?
무언가를 만들기 위해 모든 이론을 꼭 알아야 하는 것은 아니다. 결국 머신 러닝은 시행 착오가 전부이다.
머신러닝 알고리즘은 훈련(training)이라는 과정을 통해 알고리즘에 제공한 데이터를 기반으로 시스템의 모델(model)을 구축한다. 모델은 프로그램의 한 유형이다. 모델이 완성되면 여기에 새로운 데이터를 공급하여 예측을 수행하는데 이 과정을 추론(inference)이라 부른다.
머신러닝에는 여러 접근 방식이 있다. 딥러닝은 가장 인기 있는 접근 방식이다. 딥러닝은 인간의 뇌가 작동하는 방식에 대한 간단한 개념을 기반으로 한다. 딥러닝에서 시뮬레이션한 뉴런의 네트워크는 다양한 입력과 출력 간의 관계를 모델링하도록 훈련된다. 시물레이션된 뉴런의 배열을 아키텍처(architecture)라 하며 다양한 문제에 특화된 다양한 아키텍처가 있다.
머신러닝으로 문제를 해결하는 방식은 적절한 목표를 결정하고 적절한 데이터를 수집하고 레이블을 지정하고 모델에 전달할 특징을 설계하고 모델 아키텍처를 결정하는 과정을 반복하는 것이다. 적절한 모델이 나타날 때까지 반복하게 된다.
딥러닝 워크플로
- 목표 결정
수집할 데이터와 사용할 모델 아키텍처를 결정하려면 먼저 예측할 대상을 결정해야 한다. - 데이터셋 수집
데이터 선택 : 문제 해결과 관련된 정보만 사용하여 모델을 훈련하는 것이 가장 좋다. 데이터 포함 여부를 결정할 때는 항상 도메인 전문 지식을 실험에 반영해야 한다. 특정 데이터 소스를 포함시킬지 확실하지 않다면 두 가지 모델(특정 데이터를 포함한 것과 안한 것)을 훈련시키고 그중 가장 적합한 모델을 선택하는 것도 좋은 방법이다.
데이터 수집 : 효과적인 모델을 훈련시키는 데 데이터가 얼마나 필요한지 정확히 알기는 어렵다(하지만 데이터는 많을 수록 좋다). 발생할 수 있는 모든 조건과 이벤트를 나타내는 데이터를 수집해야 한다. 다양성은 모델이 선택한 몇 가지 시나리오가 아니라 가능한 모든 시나리오를 나타내는 데 도움이 된다.
데이터 레이블링 : 수집한 데이터에 대한 클래스를 결정해야 한다. 데이터를 클래스와 연관시키는 과정을 레이블링(labeling)이라 한다. 모델이 입력을 분류하는 방법을 배울 수 있도록 훈련 과정에서 이 정보를 제공해야 한다. 데이터에 레이블을 지정하는 방법을 결정하면 이를 데이터셋에 추가할 수 있다.
최종 데이터셋 - 모델 아키텍처 설계
딥러닝 모델 아키텍처는 다양한 종류가 있으며 현존하는 아키텍처는 넓은 범위의 문제를 해결할 수 있다. 모델을 훈련할 때 고유한 아키텍처로 설계할 수도 있고 기존에 개발한 아키텍처를 기반으로 모델을 선택할 수 있다.
아키텍처를 결정할 때는 해결하려는 문제의 유형, 접근할 수 있는 데이터 유형, 데이터를 모델에 공급하기 전에 해당 데이터를 변환할 수 있는 방법을 고려해야 한다.
모델을 실행할 장치의 제약 조건도 고려해야 한다(마이크로 컨트롤러는 메모리가 제한적이고 프로세서 속도가 느리다).
TinyML에는 몇 개의 뉴련 층으로 간단한 모델을 훈련한 다음 유용한 결과를 얻을 때가지 반복적인 프로세스로 아키텍처를 개선하는 방법을 적용할 수 있다.
딥러닝 모델은 텐서 형태로 입력을 받고 출력을 생성한다.
벡터 : 배열과 비슷한 숫자 목록
행렬 : 2D 배열과 유사한 2D 텐서
고차원 텐서 : 3D 이상의 모든 형태 (그냥 텐서라고 불림)
스칼라 : 하나의 숫자 (0D 텐서)
정규화(normalization) : 네트워크에 전달하려는 값이 모두 비슷한 범위에 있도록 하는 작업 (0에 가까워지도록 축소)
일반화(regularization) : 딥러인 모델이 과적합에 빠질 가능성을 줄이기 위해 사용하는 기법 (훈련 중에 제공되는 데이터를 완벽하게 기억하지 못하도록 모델을 제한)
데이터 증식 : 훈련 데이터셋의 크기를 인위적으로 확장하는 방법 (훈련 데이터 전체에 일부 변형을 가한 여러 추가 버전을 만드는 과정) - 모델 훈련
훈련은 모델이 주어진 입력 세트로 올바른 출력을 생성하는 방법을 배우는 과정이다. 트레이닝 데이터를 모델에 제공하여 가장 정확한 예측이 가능해질 때까지 조금식 조정하는 과정이다.
모델이란 시뮬레이션된 뉴런의 네트워크이다. 여러 레이어의 숫자 배열로 표현된다(이들 숫자를 가중치, 편향 또는 네트워크 파라미터라고 함).
모델 출력의 결과는 데이터를 네트워크에 공급하여 레이어의 가중치와 편향을 포함하는 연속적인 수학적 연산에 의해 변환된 값이다.
모델이 정확한 예측을 하기 시작한 시점부터는 모델이 수렴했다고 표현한다. 모델이 수렴됐는지 확인하는 일반적인 성능 지표는 손실과 정확도이다.
모델 성능을 개선하기 위해 모델의 아키텍처를 변경하고 훈련 과정을 조정하는 데 사용하는 다양한 값을 하이퍼파라미터라고 한다.
해결하려는 문제에 대해 충분한 정확도를 달성할 수 있다는 보장은 없으며, 100% 정확하지 않더라도 유용할 수 있다.
모델이 수렴하지 못하는 가정 일반적인 이유는 과적합과 과소적합 때문이다. 모델이 적합하면 주어진 입력 세트에 대해 올바른 출력을 생성한다.
과소적합이란 충분한 패턴을 학습하지 못하여 좋은 예측을 할 수 없는 상태를 의미한다(일반적으로 아키텍처가 너무 작아 모델링해야 하는 시스템의 복잡성을 포착하기에 충분하지 않거나 충분한 데이터로 학습을 못했기 때문이다).
과적합이한 훈련 데이터를 너무 잘 배웠다는 의미이다(훈련 데이터에 대해서는 정확한 출력을 예측할 수 있지만 처음 보는 데이터까지 예측할 수 있는 일반화된 모델로 작동할 수 없다). - 모델 변환
텐서플로 모델은 출력을 생성하기 위해 인터프리터에 데이터를 변환하는 방법을 알려주는 일련의 명령어이다. 모델을 메모리에 로드하고 텐서플로 인터프리터를 사용하여 실행하면 모델을 사용할 수 있다.
텐서플로 인터프리터는 데스크톱 컴퓨터와 서버에서 모델을 실행하도록 설계되어 있다.
텐서플로 라이트는 소형 저전력 장치에서 모델을 실행하기 위한 인터프리터와 도구를 제공한다.
텐서플로 라이트에서 모델을 실행하려면 모델을 텐서플로 라이트 컨버터를 사용하여 텐서플로 라이트 형식으로 변환한 다음 디스크에 파일로 저장해야 한다. 텐서플로 라이트 컨버터는 모델의 크기를 줄이고 성능 저하 없이 빠르게 실행하기 위한 최적화 기능을 제공한다. - 추론 실행
마이크로 컨트롤러용 텐서플로 라이트 C++ 라이브러리를 사용하여 모델을 로드하고 예측을 수행한다.
모델이 애플리케이션 코드와 결합하기 때문에 원시 입력 데이터를 훈련 데이터와 동일한 형식으로 변환하는 코드를 작성해야 한다. 그런 다음 변환된 데이터를 모델로 전달하여 추론을 실행한다.
모델은 추론을 통해 예측을 포함하는 출력 데이터를 생성한다. - 평가 및 문제 해결
모델을 배포하고 실행한 후 실제 성능이 원하는 결과에 근접하는지 확인할 수 있다. 모델이 테스트 데이터에서 정확하게 예측한다는 사실은 입증했지만 실제 환경에서의 성능은 다를 수 있다.
모델이 실제 환경에서 잘 작동하지 않는다면 문제 해결을 수행해야 한다. 환경적인 문제인지 과적합 문제인지 판단하여 문제를 해결한다.
모델 구축과 훈련
머신러닝 도구
파이썬과 주피터 노트북
파이썬은 머신러닝 과학자와 엔지니어가 선호하는 프로그래밍 언어이다. 수학과 관련된 수 많은 라이브러리가 존재한다.
파이썬은 주피터 노트북과 함께 사용하면 좋다. 주피터 노트북은 클릭 한 번으로 문서 작업, 그래픽, 코드를 함께 실행할 수 있는 특수 문서 형식이다. 주피터 노트북은 머신러닝 코드와 문제를 설명하고 탐색하는 도구로 널리 사용된다.
구글 코랩
구글 코랩은 주피터 노트북을 실행하기 위한 온라인 환경을 제공하며 머신러닝 연구 개발을 장려하는 구글에서 만든 무료 도구이다(일정 부분 무료).
주피터 노트북 환경을 만들어 사용하게 되면, 파이썬 라이브러리와 같은 많은 종속성을 직접 설치하여 사용하므로 다른 버전의 종속성을 갖는 라이브러리가 있을 수 있어 다른 사람과 공유하기 어렵고 예상대로 실행되지 않기도 했다.
구글 코랩을 사용하면 구글의 하드웨어에서 무료로 노트북을 실행할 수 있고 다른 사람들과 공유하여 동일한 결과를 얻을 수 있다.
텐서플로와 케라스
텐서플로는 머신러닝 모델을 구축, 훈련, 평가, 배포하기 위해 구글에서 개발한 오픈소스 프로젝트이며 머신러닝에 가장 널리 사용되는 프레임워크이다. 개발자는 대부분 파이썬 라이브러리 형태로 텐서플로를 사용한다.
케라스는 딥러닝 네트워크를 쉽게 구축하고 훈련시킬 수 있는 텐서플로의 고급 API이다.
텐서플로 라이트는 텐서플로 모델을 모바일과 임베디드 장치에 배포하는 도구 세트이다.
모델 구축과 훈련
"모델 구축과 훈련"과 관련하여 Colab에서 검증한 노트북의 Github 링크로 대체한다. 해당 링크에서 "first_sine_model.ipynb" 파일을 참조한다. 책에 나오는 예제를 Colab에서 직접 따라해 보면서 작성한 노트북이다.
해당 노트북을 끝가지 진행하면 TensorFlow Lite Converter로 변환한 모델 소스 파일을 얻을 수 있다. 이 파일을 사용하여 마이크로 컨트롤러에서 직접 TensorFlow Lite를 구동할 것이다.
애플리케이션 구축
모델은 머신러닝 애플리케이션의 일부일 뿐이다. 모델 자체로는 아무것도 할 수 없는 정보의 덩어리이다.
모델을 사용하려면 모델을 실행하는 데 필요한 환경을 설정하고 입력을 제공하고 출력을 사용하여 동작을 생성하는 코드로 모델을 랩핑해야 한다.
기본적인 TinyML 애플리케이션 아키텍처 흐름은 다음과 같다.
- 입력 데이터
- 전처리 : 모델에 적합하게 입력을 변환
- TensorFlow Lite 인터프리터 : 데이터를 바탕으로 예측하도록 훈련된 신경망 모델을 실행
- 후처리 : 모델의 출력을 해석하고 판단
- 출력 처리 : 디바이스의 리소스를 사용하여 예측에 따른 반응 수행
실제 애플리케이션 코드 작업 전에 테스트를 먼저 작성한다. 테스트를 실행하여 예상대로 작동하는 것을 검증할 수 있다.
아래 코드는 hello_world 예제의 테스트 코드이다. 코드 설명은 주석으로 대신한다.
// 아파치 2.0 오픈소스 라이선스 정보이다.
/* Copyright 2019 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
// #include "tensorflow/lite/c/common.h"
// xxd를 사용하여 C 코드로 변환한 sine 모델
#include "tensorflow/lite/micro/examples/hello_world/sine_model_data.h"
// 인터프리터가 모델에서 사용하는
// Op(TensorFlow Operation 약자. 계산을 수행하는 핵심 노드를 의미)를
// 로드할 수 있게 하는 클래스
#include "tensorflow/lite/micro/kernels/all_ops_resolver.h"
// 디버깅을 위해 오류와 출력을 기록하는 클래스
#include "tensorflow/lite/micro/micro_error_reporter.h"
// 모델을 실행할 마이크로 컨트롤러용 TensorFlow Lite 인터프리터
#include "tensorflow/lite/micro/micro_interpreter.h"
// 테스트 작성을 위한 간단한 프레임워크. 실제 테스트를 실행
#include "tensorflow/lite/micro/testing/micro_test.h"
// sine_model_data.h의 모델 데이터를 분석하는 데 사용되는
// 텐서플로 라이트 플랫버퍼 데이터 구조를 정의하는 스키마
#include "tensorflow/lite/schema/schema_generated.h"
// 스키마의 현재 버전. 모델이 호한 가능한 버전으로 정의되어 있는지 확인
#include "tensorflow/lite/version.h"
// micro_test.h에 정의된 매크로. 테스트 시작
TF_LITE_MICRO_TESTS_BEGIN
// micro_test.h에 정의된 매크로. 테스트 수행 코드
TF_LITE_MICRO_TEST(LoadModelAndPerformInference) {
// 디버그 정보 출력 메커니즘 설정
// MicroErrorReporter 인스턴스 생성(/micro_error_reporter.h 정의).
// 추론 중에 디버그 정보를 기록하는 메커니즘을 제공.
tflite::MicroErrorReporter micro_error_reporter;
// MicroErrorReporter의 인스턴스를 ErrorReporter에 설정
tflite::ErrorReporter* error_reporter = µ_error_reporter;
// 사용 가능한 데이터 구조에 모델을 매핑한다.
// 복사나 파싱을 포함하지 않는 가벼운 작업이다.
// g_sine_model_data는 sine_model_data.h에 정의되어 있다.
// Model은 schema_generated.h에 정의되어 있고 모델 데이터를 담는 구조체이다.
const tflite::Model* model = ::tflite::GetModel(g_sine_model_data);
// 모델의 버전 번호를 TensorFlow Lite 스키마 버전과 비교한다.
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);
}
// 필요한 모든 Op(operation) 구현을 가져온다.
// all_ops_resolver.h에 정의됨.
// 마이크로 컨트롤러용 TensorFlow Lite 인터프리터가 Op에 접근할 수 있도록 한다.
tflite::ops::micro::AllOpsResolver resolver;
// 모델의 입력, 출력, 중간 텐서 배열에 사용할 메모리 영역을 생성한다.
// 이 영역을 텐서 아레나(tensor arena)라고 부른다.
// 사용하는 모델에서 최소값을 찾으려면, 시행착오가 필요하다.
const int tensor_arena_size = 2 * 1024;
uint8_t tensor_arena[tensor_arena_size];
// 모델을 실행하기 위한 인터프리터를 빌드한다.
tflite::MicroInterpreter interpreter(model, resolver, tensor_arena,
tensor_arena_size, error_reporter);
// 모델에서 사용하는 텐서에 대해 tensor_arena의 메모리를 할당
TF_LITE_MICRO_EXPECT_EQ(interpreter.AllocateTensors(), kTfLiteOk);
// 모델의 입력 텐서에 대한 포인터 획득
// 여기서는 입력이 하나이므로 인덱스는 0으로 설정
// c_api_internal.h에 정의
TfLiteTensor* input = interpreter.input(0);
// TF_LITE_MICRO_EXPECT_NE와 TF_LITE_MICRO_EXPECT_EQ는 테스트 프레임워크의 매크로이다.
// 변수 값에 대한 assertion 코드이다.
// 입력 텐서가 존재하는지 확인(NE = Not Equal)
TF_LITE_MICRO_EXPECT_NE(nullptr, input);
// 속성 "dims"는 텐서의 모양을 나타낸다. 각 차원의 원소는 하나이다.
// 입력은 한 개의 원소를 포함하는 2D 텐서이므로 "dims"의 크기는 2이다.
// (EQ = Equal)
TF_LITE_MICRO_EXPECT_EQ(2, input->dims->size);
// 각 원소의 값은 해당 텐서의 길이를 나타낸다.
// 두 개의 단일 원소 텐서(하나가 다른 하나에 포함)를 갖는지 확인한다.
TF_LITE_MICRO_EXPECT_EQ(1, input->dims->data[0]);
TF_LITE_MICRO_EXPECT_EQ(1, input->dims->data[1]);
// 입력은 32비트 부동소수점 값이다.
TF_LITE_MICRO_EXPECT_EQ(kTfLiteFloat32, input->type);
// 입력 값을 제공한다.
// data 변수는 TfLitePtrUnion(c_api_internal.h 정의)이다.
input->data.f[0] = 0.;
// 입력 값으로 모델을 실행하고 성공 여부를 검사한다.
TfLiteStatus invoke_status = interpreter.Invoke();
if (invoke_status != kTfLiteOk) {
error_reporter->Report("Invoke failed\n");
}
TF_LITE_MICRO_EXPECT_EQ(kTfLiteOk, invoke_status);
// 출력 텐서의 포인터를 획득하여 예상하는 속성을 가지고 있는지 확인한다.
// 입력 텐서와 동일한 속성이다.
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);
// 텐서의 출력 값을 가져온다.
float value = output->data.f[0];
// 출력 값과 예상 값의 차이가 0.5 범위에 있는지 확인한다.
TF_LITE_MICRO_EXPECT_NEAR(0., value, 0.05);
// 몇 가지 값의 추론을 더 실행하여 예상하는 출력을 확인한다.
// interpreter.input(0)와 interpreter.output(0)을 다시 호출할 필요가 없다.
input->data.f[0] = 1.;
invoke_status = interpreter.Invoke();
TF_LITE_MICRO_EXPECT_EQ(kTfLiteOk, invoke_status);
value = output->data.f[0];
TF_LITE_MICRO_EXPECT_NEAR(0.841, value, 0.05);
input->data.f[0] = 3.;
invoke_status = interpreter.Invoke();
TF_LITE_MICRO_EXPECT_EQ(kTfLiteOk, invoke_status);
value = output->data.f[0];
TF_LITE_MICRO_EXPECT_NEAR(0.141, value, 0.05);
input->data.f[0] = 5.;
invoke_status = interpreter.Invoke();
TF_LITE_MICRO_EXPECT_EQ(kTfLiteOk, invoke_status);
value = output->data.f[0];
TF_LITE_MICRO_EXPECT_NEAR(-0.959, value, 0.05);
}
// micro_test.h에 정의된 매크로. 테스트 종료
TF_LITE_MICRO_TESTS_END
실제 도서에서 제공되는 코드를 실행하는 절차는 아래와 같다(Ubuntu 20.04에서 실행함).
- 다운로드할 디렉터리를 생성하고, 생성한 디렉터리로 이동한다.
$ mkdir TinyML
$ cd TinyML - Github에서 소스를 다운로드하고 생성된 "tensorflow-lite" 디렉터리로 이동한다(도서에 있는 경로로 하면 실행이 안된다).
$ git clone https://github.com/yunho0130/tensorflow-lite.git
$ cd tensorflow-lite - make 커맨드를 아래와 같이 실행하여 테스트를 수행한다(처음 make 실행시 시간이 다소 소요됨).
$ make -f tensorflow/lite/micro/tools/make/Makefile test_hello_world_test
테스트를 실행하면 도서의 내용과 다르게 출력된다.
'~~~ALL TESTS PASSED~~~'
tensorflow/lite/micro/tools/make/gen/linux_x86_64/bin/hello_world_test: PASS
도서의 내용 처럼 인위적으로 실패하게 수정하면, 도서의 내용과 동일하게 출력된다.
Testing LoadModelAndPerformInference
0. (1.0*2^-127) near value (1.6142864*2^-1) failed at tensorflow/lite/micro/examples/hello_world/hello_world_test.cc:92
0/1 tests passed
~~~SOME TESTS FAILED~~~
hello_world 프로젝트 파일 구조
실제 마이크로 컨트롤러에서 사용할 hello_world 프로젝의 파일 구조를 살펴본다. 해당 프로젝트의 루트는 "tensorflow/lite/micro/examples/hello_world"에 있다.
- BUILD : 테스트를 포함하여 애플리케이션의 소스 코드를 사용하여 빌드할 수 있는 다양한 항목을 나열하는 파일.
- Makefile.inc : 테스트와 애플리케이션의 빌드 대상에 대한 정보가 포함된 Makefile(일부 소스 파일을 정의).
- README.md : 애플리케이션 빌드와 실행에 대한 절차를 포함하는 텍스트 파일.
- constants.h, constants.cc : 프로그램 동작을 정의하는 데 중요한 영향을 미치는 다양한 상수를 포함하는 파일 세트.
- create_sine_model.ipynb : 신경망 모델을 만드는 주피터 노트북.
- hello_world_test.cc : 생성한 신경망 모델을 사용하여 추론을 실행하는 테스트.
- main.cc : 애플리케이션의 실행되는 프로그램 진입점.
- main_functions.h, main_functions.cc : 프로그램에 필요한 모든 초기화를 수행하는 setup() 함수와 프로그램의 핵심 로직을 포함하고 상태 머신을 무한히 순환하게 설계된 loop() 함수를 정의하는 파일 세트. main.cc에 의해 호출.
- output_handler.h, output_handler.cc : 추론을 실행될 때마다 출력을 표시하는 데 사용할 수 있는 함수를 정의하는 파일 세트. 기본 구현은 결과를 화면에 출력하고 이 구현을 재정의하여 다른 작업을 수행할 수 있다.
- output_handler_test.cc : output_handler.h, output_handler.cc의 코드가 올바르게 작동하는지 검증하는 테스트.
- sine_model_data.h, sine_model_data.cc : 생성된 신경망 모델의 데이터 배열을 정의하는 파일 세트.
마이크로컨트롤러마다 기능과 API가 다르기 때문에 특정 디바이스용으로 빌드하는 경우 프로젝트 구조를 통해 특정 디바이스의 소스 파일을 제공할 수 있다. 아래의 디렉러리는 특정 디바이스에서 제공하는 소스 파일을 포함한다.
- /arduino
- /disco_f746ng
- /esp
- /sparkfun_edge
소스 코드 분석
main_functions.cc 파일 코드 분석 (설명은 코드 주석으로 대신한다.)
// 아파치 2.0 오픈소스 라이선스 정보이다.
/* Copyright 2019 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
// 소스에서 사용하는 헤더 파일을 정의한다.
#include "tensorflow/lite/micro/examples/hello_world/main_functions.h"
#include "tensorflow/lite/micro/examples/hello_world/constants.h"
#include "tensorflow/lite/micro/examples/hello_world/output_handler.h"
#include "tensorflow/lite/micro/examples/hello_world/sine_model_data.h"
#include "tensorflow/lite/micro/kernels/all_ops_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"
// 아두이노 스타일과 호환성을 위해 사용한다.
// 소스 파일내에서 사용할 전역 변수를 정의한다.
// 네임스페이스로 묶어서 소스내에서는 접근할 수 있지만,
// 프로젝트 내의 다른 파일에서는 접근할 수 없다.
// 동일한 변수를 서로 다른 파일에서 정의할 때 생기는 문제를 방지할 수 있다.
namespace {
// 아래 변수는 테스트 코드 작성에서 정의한 변수와 동일.
tflite::ErrorReporter* error_reporter = nullptr;
const tflite::Model* model = nullptr;
tflite::MicroInterpreter* interpreter = nullptr;
TfLiteTensor* input = nullptr;
TfLiteTensor* output = nullptr;
// 추론을 실행한 횟수를 기록
int inference_count = 0;
// 입력, 출력, 중간 배열에 사용할 메모리 영역을 생성한다.
// 신경망 모델의 최솟값을 찾으려면 시행착오가 필요하다.
constexpr int kTensorArenaSize = 2 * 1024;
uint8_t tensor_arena[kTensorArenaSize];
} // namespace
// 함수의 이름은 아두이노와 호환을 위해 중요하다.
// 프로그램이 처음 실행될 때 한번만 호출된다.
// 추론을 시작하기 전에 수행해야 할 일회성 작업을 수행한다.
void setup() {
// 로깅 설정. Google 스타일은 수명 불확실성 때문에 전역이나 정적을 피하지만,
// 사소한 소멸자가 있어 상관없다.
// NOLINTNEXTLINE(runtime-global-variables)
static tflite::MicroErrorReporter micro_error_reporter;
error_reporter = µ_error_reporter;
// 모델을 사용할 데이터 구조체에 매핑한다.
// 이는 복사나 파싱을 포함하지 않는 매우 가벼운 작업이다.
model = tflite::GetModel(g_sine_model_data);
if (model->version() != TFLITE_SCHEMA_VERSION) {
TF_LITE_REPORT_ERROR(error_reporter,
"Model provided is schema version %d not equal "
"to supported version %d.",
model->version(), TFLITE_SCHEMA_VERSION);
return;
}
// 필요한 모든 오퍼레이션 구현을 가져온다.
// NOLINTNEXTLINE(runtime-global-variables)
static tflite::ops::micro::AllOpsResolver resolver;
// 모델을 실행할 인터프리터를 빌드한다.
static tflite::MicroInterpreter static_interpreter(
model, resolver, tensor_arena, kTensorArenaSize, error_reporter);
interpreter = &static_interpreter;
// 모델의 텐서를 tensor_arena로부터 메모리 할당한다.
TfLiteStatus allocate_status = interpreter->AllocateTensors();
if (allocate_status != kTfLiteOk) {
TF_LITE_REPORT_ERROR(error_reporter, "AllocateTensors() failed");
return;
}
// 모델의 입력/출력 텐서에 대한 포인터를 가져온다.
// TfLiteTensor라는 구조체가 가진 data 멤버는 출력을 저장하기 위한
// 할당된 메모리 영역을 가리킨다. 출력이 없더라고 data 메모리 영역은 존재한다.
input = interpreter->input(0);
output = interpreter->output(0);
// 추론을 실행한 횟수를 기록하기 위한 변수 초기화
inference_count = 0;
}
// 함수의 이름은 아두이노와 호환을 중요하다.
// 계속 반복되는 실행 코드가 위치한다.
void loop() {
// 모델에 전달할 x 값을 계산한다.
// 현재 inference_count를 주기당 추론 횟수와 비교하여
// 모델이 학습된 지정 가능한 x 값 범위 내에서 위치를 결정하고
// 이를 사용하여 값을 계산한다.
// kXrange, kInferencesPerCycle 상수는 constants.h, constants.cc에 정의
float position = static_cast<float>(inference_count) /
static_cast<float>(kInferencesPerCycle);
float x_val = position * kXrange;
// 계산된 x 값을 모델의 입력 텐서에 넣는다.
input->data.f[0] = x_val;
// 추론을 실행하고 에러를 리포트한다.
TfLiteStatus invoke_status = interpreter->Invoke();
if (invoke_status != kTfLiteOk) {
TF_LITE_REPORT_ERROR(error_reporter, "Invoke failed on x_val: %f\n",
static_cast<double>(x_val));
return;
}
// 모델의 출력 텐서에서 예측된 y 값을 읽는다.
float y_val = output->data.f[0];
// 결과를 출력한다. 사용자 지정 HandleOutput 함수는
// 지원되는 하드웨어 장비에 대해 구현될 수 있다.
HandleOutput(error_reporter, x_val, y_val);
// inference_counter를 증가시키고
// 주기당 최대 추론 수에 도달하면 재설정한다.
inference_count += 1;
if (inference_count >= kInferencesPerCycle) inference_count = 0;
}
main_functions.cc 파일의 setup(), loop() 함수는 main.cc에서 호출한다.
main.cc 파일 코드 분석 (설명은 코드 주석으로 대신한다.)
// 아파치 2.0 오픈소스 라이선스 정보이다.
/* Copyright 2019 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
// setup(), loop() 함수가 정의된 헤더 파일을 포함한다.
#include "tensorflow/lite/micro/examples/hello_world/main_functions.h"
// 이 함수는 표준 C 엔트리 포인트를 가지는 시스템에서 사용되는 기본 main이다.
// 엔트리 코드에 대해 다른 요구 사항(app_main 함수와 같은)을 가지는 다른 디바이스
// (예로 FreeRTOS나 ESP32)는 대상별 하위 폴더에서 main.cc 파일을 특수화해야 한다.
int main(int argc, char* argv[]) {
setup();
while (true) {
loop();
}
}
호스트에서 애플리케이션 실행
애플리케이션에서 실행하려면 빌드를 진행해야 한다. 아래는 리눅스에서 make 명령을 사용하여 실행 가능한 바이너리를 생성하는 방법이다.
$ make -f tensorflow/lite/micro/tools/make/Makefile hello_world
빌드가 완료되면 아래와 같이 바이너리를 실행할 수 있다.
$ ./tensorflow/lite/micro/tools/make/gen/linux_x86_64/bin/hello_world
바이너리가 실행되면 아래와 같이 출력된다.
x_value: 1.2566366*2^-1, y_value: 1.1161486*2^-1
x_value: 1.8849551*2^-1, y_value: 1.5661856*2^-1
x_value: 1.2566366*2^0, y_value: 1.8288908*2^-1
x_value: 1.5707957*2^0, y_value: 1.9224764*2^-1
x_value: 1.8849551*2^0, y_value: 1.8043818*2^-1
x_value: 1.995567*2^1, y_value: 1.5224055*2^-1
x_value: 1.2566366*2^1, y_value: 1.1094988*2^-1
...
애플리케이션을 종료하려면 "Ctrl + c"를 입력한다.
마이크로 컨트롤러에 배포
여기서는 STM32H750 디스커버리 키트에 배포하는 절차를 설명한다(도서에서 설명하는 보드가 없는 관계로...).
※ STM32H750 디스커버리 키트는 내부 플래시 용량(128KBytes) 문제로 실행할 수 없다. DISCO_F746NG를 사용하는 mbed 환경 설정과 컴파일 과정만 정리한다.
마이크로 컨트롤러란?
실제 마이크로 컨트롤러는 회로 기판(PCB)에 부착된 많은 전자 부품 중 하나이다. 마이크로 컨트롤러는 핀으로 회로 기판에 연결된다. 일반적인 마이크로 컨트롤러는 수십 개의 핀이 있으며 모두 용도가 다르다. 어떤 핀은 전원을 공급하고 어떤 핀은 다른 중요한 부품과 연결된다. 마이크로 컨트롤러에서 실행되는 프로그램에 의해 디비털 신호의 입력과 출력을 수행하는 핀을 GPIO(General Purpose Input/Output) 핀이라 한다. GPIO 핀이 입력으로 동작하면 전압이 외부에서 들어오는지 검사할 수 있고, 출력으로 동작하면 전압을 외부로 공급하게 된다. GPIO 핀은 디지털이므로 출력 모드에서 스위치처럼 ON/OFF 동작을 한다. 입력 모드에서는 전압이 특정 임계값보다 높거나 낮은지 검사할 수 있다. 아날로그 입력 핀이 있는 일부 마이크로 컨트롤러는 정확한 입력 값을 측정할 수 있다.
DISCO_F746NG용 MBED 환경 설정 및 컴파일
Ubuntu 20.04에서 환경 설정 및 컴파일 방법을 설명한다. 도서에 자세히 설명되어 있으나, 몇 가지 수정 사항이 있다.
먼저 MBED CLI 환경 설정을 한다(참고 사이트).
Ubuntu에서 필요한 패키지를 설치한다.
$ sudo apt install python3 python3-pip git mercurial
Mbed CLI를 설치하고 업데이트 한다.
$ python3 -m pip install mbed-cli
$ python3 -m pip install -U mbed-cli
컴파일러를 다운 받아서 적당한 위치에 압축을 푼다. 컴파일러는 링크에서 다운 받을 수 있다.
링크에서 gcc-arm-none-eabi-10.3-2021.10-x86_64-linux.tar.bz2(2023/8/1 기준, 향후 업데이트 될 수 있음)를 다운 받아 압축을 푼다.
$ tar -xvf gcc-arm-none-eabi-10.3-2021.10-x86_64-linux.tar.bz2
Mbed CLI에서 컴파일러를 구성한다. 아래 명령어를 사용하여 "ARM_PATH"와 "GCC_ARM_PATH"를 설정한다.
$ mbed config -G ARM_PATH "<압축푼 위치>/gcc-arm-none-eabi-10.3-2021.10/bin/arm-none-eabi-gcc"
$ mbed config -G GCC_ARM_PATH "<압축푼 위치>/gcc-arm-none-eabi-10.3-2021.10/bin"
GitHub에서 소스를 다운로드한다.
$ git clone https://github.com/yunho0130/tensorflow-lite.git
다운로드한 디렉터리로 이동한다.
$ cd tensorflow-lite
이후 도서에 나오는 내용대로 진행을 한다.
$ make -f tensorflow/lite/micro/tools/make/Makefile TARGET=mbed TAGS="cmsis disco_f746ng" generate_hello_world_mbed_project
도서에는 TAGS가 "CMSIS disco_f746ng"로 되어있으나, 이렇게 수행하면 make에서 에러가 난다. "CMSIS"를 소문자 "cmsis"로 변경하면 에러 없이 진행할 수 있다. github에서 "cmsis-nn"으로 하라고 되어 있으나, 나중에 컴파일시 에러가 발생한다(에러를 해결하려다 포기).
프로젝트가 생성된 디렉터리로 이동한다.
$ cd cd tensorflow/lite/micro/tools/make/gen/mbed_cortex-m4/prj/hello_world/mbed/
Mbed 프로젝트의 루트를 설정한다.
$ mbed config root .
Mbed 종속성을 다운로드한다.
$ mbed deploy
Mbed에서 사용할 컴파일러를 설정한다. 기본으로 C++98로 컴파일을 하는데 C++11로 변경한다.
$ python -c 'import fileinput, glob;
for filename in glob.glob("mbed-os/tools/profiles/*.json"):
for line in fileinput.input(filename, inplace=True):
print(line.replace("\"-std=gnu++98\"","\"-std=c++11\"-fpermissive\""))'
아래 명령을 사용하여 컴파일을 진행한다.
$ mbed compile -m DISCO_F746NG -t GCC_ARM
컴파일이 완료되면, 아래와 같은 내용을 볼 수 있다.
Link: mbed
Elf2Bin: mbed
| Module | .text | .data | .bss |
|-------------------------------------|-----------------|-------------|---------------|
| BSP_DISCO_F746NG/Drivers | 2400(+2400) | 1(+1) | 584(+584) |
| BSP_DISCO_F746NG/Utilities | 0(+0) | 8(+8) | 0(+0) |
| LCD_DISCO_F746NG/LCD_DISCO_F746NG.o | 150(+150) | 0(+0) | 0(+0) |
| [fill] | 126(+126) | 7(+7) | 26(+26) |
| [lib]/c.a | 35640(+35640) | 2472(+2472) | 58(+58) |
| [lib]/gcc.a | 3448(+3448) | 0(+0) | 0(+0) |
| [lib]/m.a | 6216(+6216) | 0(+0) | 0(+0) |
| [lib]/misc | 188(+188) | 4(+4) | 28(+28) |
| [lib]/nosys.a | 32(+32) | 0(+0) | 0(+0) |
| [lib]/stdc++.a | 40(+40) | 0(+0) | 16(+16) |
| mbed-os/drivers | 872(+872) | 0(+0) | 0(+0) |
| mbed-os/hal | 1314(+1314) | 8(+8) | 130(+130) |
| mbed-os/platform | 4268(+4268) | 260(+260) | 240(+240) |
| mbed-os/targets | 12778(+12778) | 4(+4) | 1152(+1152) |
| tensorflow/lite | 77536(+77536) | 4(+4) | 8118(+8118) |
| Subtotals | 145008(+145008) | 2768(+2768) | 10352(+10352) |
Total Static RAM memory (data + bss): 13120(+13120) bytes
Total Flash memory (text + data): 147776(+147776) bytes
바이너리 크기가 140KBytes 정도가 나와서 DISCO_H750B에서는 돌릴 수가 없었다.
참고 사이트
사이트 | 설명 |
---|---|
초소형 머신러닝 TinyML | TinyML 도서 |
https://github.com/tensorflow/tensorflow/tree/be4f6874533d78f662d9777b66abe3cdde98f901/tensorflow/lite/experimental/micro | TinyML 도서 오픈소스 Github 저장소 |
https://github.com/yunho0130/tensorflow-lite | TinyML 도서 오픈소스 Github 저장소 |
https://www.sparkfun.com/products/retired/15420 | 스파크펀 에지2 제조사 |
https://github.com/sparkfun/SparkFun_Edge | 스파크펀 에지2 Github 저장소 |