본문 바로가기
공부/ML&DL

[머신러닝 공부] Tiny ML -17 / 제스처인식 어플리케이션 -1

by 로디네로 2022. 4. 2.
반응형

 

Chapter17. 제스처인식 어플리케이션

 

"

자이로센서를 이용해 간단한 제스처를 인식하는 어플리케이션을 만들어보자

"

목차 :

  • 개요
  • 만들고자 하는 시스템
  • 기본흐름 코드
  • 제스처 감지 -> 다음장
  • MCU에 배포 -> 다음장
  • 마치며 -> 다음장


이전장 링크

 

[머신러닝 공부] Tiny ML -16 / 인체감지 모델 훈련하기

Chapter16. 인체감지 모델 훈련하기 " 이전장까지 썼던 모델 을 훈련시켜보자 " 목차 : 연산 환경 선택 Google Cloud Platform 설정 프레임워크선택 데이터셋 구축 모델 훈련과 평가 텐서플로 라이트 기타

dbstndi6316.tistory.com

 


참고 repo

 

GitHub - tensorflow/tensorflow: An Open Source Machine Learning Framework for Everyone

An Open Source Machine Learning Framework for Everyone - GitHub - tensorflow/tensorflow: An Open Source Machine Learning Framework for Everyone

github.com

 


 

개요

 

이전까지 음성인식, 이미지인식 프로젝트 인간이 더 잘 이해할 수 있는 데이터를 사용했다.

 

하지만 이제는 제스처,추세,패턴 과 같이 인간의 감각으로 판단하기 어려운 데이터를 다뤄보려한다.

 

이러한 데이터를 함수로 나타내는 유형의 기능을 Heuristic 이라고 하며 산업자동화, 의료 기기등의 분야에 널리 쓰인다.

 

아래는 Staying Walking Jogging 과 Magnititude에 대한 Heuristic 이라고 볼 수 있다.

그래프로 그려놨으니 보이지만.. 연속적인 상황에 놓여있다면 직감적으로 인간은 무엇을 느낄수 있을까요?

https://www.researchgate.net/figure/Comparison-of-staying-walking-and-jogging-using-accelerometer-magnitude-signals_fig2_316444286

 

Heuristic은 단지 데이터를 그려놓는 것 뿐 아닌 해석하는것 까지 포함한다.

 

책에서는 이를 쉬운 예시를 들어 설명하는데

 

1. 체온을 기준으로 감기에 걸렸는지 여부를 결정하려면 감기와 체온의 상관관계에 대한 도메인지식이 필요하다.

 

2. 자동화시스템에서 상태를 식별하려면 통계분석 또는 신호처리와 같은 수학적 기법에 대한 지식이 필요하다.

 

딥러닝을 이용하면 위의 내용들은 분명 도움은 되겠지만, 복잡한 Heuristic 알고리즘 설계없이 보다 정확한 해석을 해낼 수 있다.

 

 


 

만들고자 하는 시스템

 

'W' 'O' 'V' 의 동작을 학습시켜 해당 제스처를 했을 때 각기 다른 LED를 켜도록 한다.

 

Sparkfun Edge의 3축 IMU를 이용해 데이터를 얻고, 이 출력을 딥러닝모델에 제공해 분류를 수행하게 한다.

 

모델은 크기가 약 20KB인 CNN으로 자이로센서 값을 입력으로 받는다.

25Hz 속도로 샘플링한 128개의 x,y,z 값

 

필터링 또한 필요하다.

 

초당 여러 개의 추론을 수행하기 때문에 한 번의 잘못된 추론이 전체를 나타내서는 안되기 때문이다.

 

메인루프는 아래와 같이 이루어진다.

 

가속도계 핸들
> 데이터 캡쳐
TF lite 인터프리터
> 모델 실행
감지 응답기
> 제스처 감지했는지 결정
출력 핸들
> 다음 동작을 결정

 


 

기본 흐름 코드

 

 magic_wand_test.cc

: 가속도계 데이터에서 추론을 실행

 

▽ 모델에서 사용할 Op 를 나열

▽ 추론에 필요한 것을 설정 및 모델의 입력 텐서에 대한 포인터를 가져오기

TF_LITE_MICRO_TEST(LoadModelAndPerformInference) {
  
  // 로깅 설정
  tflite::MicroErrorReporter micro_error_reporter;
  tflite::ErrorReporter* error_reporter = &micro_error_reporter;

  // 모델을 사용 가능한 데이터 구조에 매핑
  const tflite::Model* model = ::tflite::GetModel(g_magic_wand_model_data);
  if (model->version() != TFLITE_SCHEMA_VERSION) {
    error_reporter->Report(
        "Model provided is schema version %d not equal "
        "to supported version %d.\n",
        model->version(), TFLITE_SCHEMA_VERSION);
  }

  // 작업 구현에 필요한 모듈 가져오기
  static tflite::MicroMutableOpResolver micro_mutable_op_resolver;  // NOLINT
  micro_mutable_op_resolver.AddBuiltin(
      tflite::BuiltinOperator_DEPTHWISE_CONV_2D,
      tflite::ops::micro::Register_DEPTHWISE_CONV_2D());
  micro_mutable_op_resolver.AddBuiltin(
      tflite::BuiltinOperator_MAX_POOL_2D,
      tflite::ops::micro::Register_MAX_POOL_2D());
  micro_mutable_op_resolver.AddBuiltin(tflite::BuiltinOperator_CONV_2D,
                                       tflite::ops::micro::Register_CONV_2D());
  micro_mutable_op_resolver.AddBuiltin(
      tflite::BuiltinOperator_FULLY_CONNECTED,
      tflite::ops::micro::Register_FULLY_CONNECTED());
  micro_mutable_op_resolver.AddBuiltin(tflite::BuiltinOperator_SOFTMAX,
                                       tflite::ops::micro::Register_SOFTMAX());

  // 입출력 배열에 사용할 메모리 영역을 생성한다.
  const int tensor_arena_size = 60 * 1024;
  uint8_t tensor_arena[tensor_arena_size];

  // 모델 실행위한 인터프리터 빌드
  tflite::MicroInterpreter interpreter(model, micro_mutable_op_resolver,
                                       tensor_arena, tensor_arena_size,
                                       error_reporter);

  // 텐서에 대한 메모리를 할당
  interpreter.AllocateTensors();

  // 모델 입력텐서에 대한 포인터를 획득
  TfLiteTensor* input = interpreter.input(0);

 

▽ 입력 텐서 형태 확인

▽ 입력 텐서에 데이터를 제공한다.

  const float* ring_features_data = g_ring_micro_f9643d42_nohash_4_data;
  error_reporter->Report("%d", input->bytes);
  for (int i = 0; i < (input->bytes / sizeof(float)); ++i) {
    input->data.f[i] = ring_features_data[i];
  }

 

▽ 추론을 실행한다.

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

 

▽ 출력텐서 형태를 확인한다.

▽ 데이터를 테스트 해서 예상결과인지 확인

  // 클래스 4개가 존재한다.
  const int kWingIndex = 0;
  const int kRingIndex = 1;
  const int kSlopeIndex = 2;
  const int kNegativeIndex = 3;

  // 점수로 평가한다.
  float wing_score = output->data.f[kWingIndex];
  float ring_score = output->data.f[kRingIndex];
  float slope_score = output->data.f[kSlopeIndex];
  float negative_score = output->data.f[kNegativeIndex];
  TF_LITE_MICRO_EXPECT_GT(ring_score, wing_score);
  TF_LITE_MICRO_EXPECT_GT(ring_score, slope_score);
  TF_LITE_MICRO_EXPECT_GT(ring_score, negative_score);

 

▽ 테스트를 실행해본다.

make -f tensorflow/lite/micro/tools/make/Makefile test_magic_wand_test

 

예상된 추론 결과가 맞게 나오는것을 볼 수 있다.

 


 

 

 accelerometer_handler_test.cc 

: 가속도계 핸들러를 사용해 새로운 데이터를 얻음

 

▽ accelerometer_handler_test.cc에 있는 테스트에서 핸들러를 호출한다.

▽ SetupAccelerometer() 함수 - 설정을 수행하고 성공을 나타내는 값을 반환한다.

TF_LITE_MICRO_TEST(TestSetup) {
  tflite::MicroErrorReporter micro_error_reporter;
  TfLiteStatus setup_status = SetupAccelerometer(&micro_error_reporter);
  TF_LITE_MICRO_EXPECT_EQ(kTfLiteOk, setup_status);
}

 

▽ 입력 텐서를 데이터로 채운다.

TF_LITE_MICRO_TEST(TestAccelerometer) {
  // 3축 센서이기 때문에 배열의 크기는 128*3 이며, 값을 0.0으로 초기화
  float input[384] = {0.0};
  tflite::MicroErrorReporter micro_error_reporter;
  
  // 데이터가 불충분하면 함수가 false 반환하는지 여부
  bool inference_flag = ReadAccelerometer(&micro_error_reporter, input, 384, false);
  TF_LITE_MICRO_EXPECT_EQ(inference_flag, false);

  // 데이터가 충분하면 true 반환하는지 여부
  for (int i = 1; i <= 128; i++) {
    inference_flag = ReadAccelerometer(&micro_error_reporter, input, 384, false);
  }
  TF_LITE_MICRO_EXPECT_EQ(inference_flag, true);
}

 

▽ 테스트를 실행해본다.

make -f tensorflow/lite/micro/tools/make/Makefile test_gesture_accelerometer_handler_test

데이터가 충분하게 들어왔는지 여부 - PASS로 통과

 

 


 

 

gesture_predictor.cc

: 제스처 예측기를 사용해 추론의 결과가 유효한지 확인한다.

 

▽ 변수를 정의한다.

// 가장 최근 제스처가 연속 몇 번 감지됐는지
int continuous_count = 0;
// 마지막 예측결과
int last_predict = -1;

 

▽ PredictGesture() 함수는 제스처 확률이 임곗값을 넘는지, 일관되게 감지됐는지 확인한다.

// 0: wing, 1: ring, 2: slope, 3: unknown
int PredictGesture(float* output) {
  // 확률이 0.8보다 큰 출력을 탐색한다.
  int this_predict = -1;
  for (int i = 0; i < 3; i++) {
    if (output[i] > 0.8) this_predict = i;
  }

 

▽ 임계값을 초과하는 동작이 감지되지 않을경우

  if (this_predict == -1) {
    continuous_count = 0;
    last_predict = 3;
    return 3;
  }

 

▽ 최근의 예측이 이전예측과 동일하다면

  if (last_predict == this_predict) {
    continuous_count += 1;
  } else {
    continuous_count = 0;
  }
  last_predict = this_predict;

 

▽ 현재 제스처가 아직 임계값을 충족하는지 확인한다. 이후 리셋

  if (continuous_count < kConsecutiveInferenceThresholds[this_predict]) {
    return 3; // 인식 횟수가 충분하지않다면 3을 반환
  }
  
    continuous_count = 0;
  last_predict = -1;
  return this_predict;
}

 

▽ gesture_predictor_test.cc 에서의 테스트 동작

1. 카테고리0 의 확률을 PredictGesture()에 전달 -> 임계값 횟수 넘어서면 카테고리 0을 나타낼것

2. 한 카테고리에 대해 높은확률로 연속 호출하다가 다른 카테고리의 대해 높은 확률로 호출

3. 임계값에 도달하지 못하는 확률을 무시

 

▽ 테스트를 실행해본다.

make -f tensorflow/lite/micro/tools/make/Makefile test_gesture_predictor_test

1,2,3 테스트에서 예상과 같은 결과가 도출되어 PASS

 

 


 

 

 output_handler_test.cc

: 출력 핸들러를 사용해 추론 결과를표시

 

▽ PredictGesture()에서 반환한 클래스 인덱스만 가져와서 결과를 표시한다.

TF_LITE_MICRO_TEST(TestCallability) {
  tflite::MicroErrorReporter micro_error_reporter;
  tflite::ErrorReporter* error_reporter = &micro_error_reporter;
  HandleOutput(error_reporter, 0);
  HandleOutput(error_reporter, 1);
  HandleOutput(error_reporter, 2);
  HandleOutput(error_reporter, 3);
}

 

▽ 테스트를 실행한다.

make -f tensorflow/lite/micro/tools/make/Makefile test_gesture_output_handler_test

 


 

이렇게 제스처감지 각 부분의 test 코드들을 돌려보면서 동작을 확인했다.

 

다음 포스팅에서는 main function부터 MCU에 배포까지 진행해보겠다.

 

 

반응형

댓글