코딩뚠뚠

[머신러닝 공부] Tiny ML -8 / 음성인식 어플리케이션-1 본문

공부/ML&DL

[머신러닝 공부] Tiny ML -8 / 음성인식 어플리케이션-1

로디네로 2022. 1. 13. 23:53
반응형

 

Chapter8. 음성인식(호출어 감지) 어플리케이션 만들기

 

"
이번 챕터에서는 TinyML을 좀더 생활에 밀접한 부분에 사용해본다.

"

 

 

목차 :

  • TinyML을 이용한 호출어감지는 왜 필요할까?
  • 만들고자 하는 시스템
  • 어플리케이션 아키텍처
  • 테스트코드 분석
  • 호출어 듣기 -> 다음장에서 계속
  • MCU에 배포하기 -> 다음장에서 계속

 


 

1. TinyML을 이용한 호출어감지는 왜 필요할까?

 

 

우리는 음성인식이 되는 기기를 사용할 때 'OK google', '시리야' 와 같이 호출어를 사용한 후,

'오늘 네이버 주가 검색해줘' 와 같이 음성 명령을 내리는 순서에 익숙하다.

 

음성명령은 서버에서 추론하여 결과를 내는 편이 정확성면에서 좋을것이다.

하지만 이는 많은 에너지(데이터,전력 등)를 소모한다.

 

따라서 언제 호출될지 모르는 호출어를 위해 항상 서버에 음성을 보내는 짓은 무모한 짓이다.

 

저전력 시스템을 사용해 호출어만 항상 감지하도록 한 후 감지된다면 이후 문장을 서버에서 추론하는건 어떨까?

TinyML로 위와 같은 호출어 감지 어플리케이션을 만들 수 있다.

 

필요할때만 더 큰 자원을 깨우는 접근법을 "Cascading" 이라고 한다.

 

 


 

2. 만들고자 하는 시스템

 

훈련된 모델을 사용하여 음성 오디오를 분류하는 어플리케이션을 구축하고자 한다.

 

모델 훈련은 다다음장에서 계속된다.

 

모델은 Yes, No, 무음, 배경소음 을 구분할 수 있다.

 


 

3. 어플리케이션 아키텍처

 

기본적인 머신러닝 아키텍처는 아래와 같다.

Input -> Feature Extraction -> Inference -> Post processing -> Run

(입력 -> 특징추출 -> 추론 -> 후처리 -> 실행)

 

이번에는 아래와 같다.

입력(마이크) -> 특징추출(스펙트로그램) -> 인터프리터(모델실행) -> 명령인식(확인) -> 실행

중간에 어떤 세부적인 과정들이 있을진 모르겠지만 큰 흐름은 같다.


우리가 쓸 모델은 Speech Commands 라는 데이터셋으로 훈련되었으며

30개의 짧은단어묶음 6만5000개로 이루어져있다.

그 중 분류되는 class는 yes / no / 알수없음 / 무음 단 네 가지이다.

 

Speech_commands  |  TensorFlow Datasets

도움말 Kaggle에 TensorFlow과 그레이트 배리어 리프 (Great Barrier Reef)를 보호하기 도전에 참여 이 페이지는 Cloud Translation API를 통해 번역되었습니다. Switch to English Speech_commands 키워드 탐지 시스템을

www.tensorflow.org


입력된 음성을 스펙트로그램으로 만드는 작업(전처리)을 거쳐

이를 CNN모델의 input 으로 넣어준다.

https://www.kaggle.com/christianlillelund/classify-mnist-audio-using-spectrograms-keras-cnn

 

CNN은 이미지 분석에 많이 쓰이지만 스펙트로그램과 같은 다차원 벡터 입력의 분류에도 사용할 수 있다.

 

 


 

4. 테스트코드 분석

 

아래 저장소의 코드를 분석한다. (tflite-micro 7/29 update ver.)

- 이전 저장소 코드를 가져오는 내용은 Tiny ML -7 포스팅에..

 

GitHub - tensorflow/tflite-micro: TensorFlow Lite for Microcontrollers

TensorFlow Lite for Microcontrollers. Contribute to tensorflow/tflite-micro development by creating an account on GitHub.

github.com

 

위 저장소의 코드 중 몇몇 중요 코드를 분석해본다.

  • micro_speech_test.cc
  • audio_provicer_test.cc
  • feature_provider_mock_test.cc
  • recognize_commands_test.cc
  • command_responder_test.cc

micro_speech_test.cc : 기본 흐름

 

> 로깅을 설정하고 모델을 로드한다.

> 모델에 필요한 Op(작업,자원)를 정의한다.

> MicroMutableOpResolver를 선언하고 Op를 추가한다. (특정모델에 어떤Op를 매칭할지)

  tflite::MicroMutableOpResolver<4> micro_op_resolver;
  micro_op_resolver.AddDepthwiseConv2D();
  micro_op_resolver.AddFullyConnected();
  micro_op_resolver.AddReshape();
  micro_op_resolver.AddSoftmax();

 

> 작업 메모리를 설정, 모델을 실행할 인터프리터 빌드

  // 입출력 및 중간 계산에 사용할 메모리 영역을 정의한다. 시행착오가 필요하다..
#if defined(XTENSA)
  constexpr int tensor_arena_size = 15 * 1024;
#else
  constexpr int tensor_arena_size = 10 * 1024;
#endif
  uint8_t tensor_arena[tensor_arena_size];
  // 모델을 실행할 인터프리터를 빌드한다.
  tflite::MicroInterpreter interpreter(model, micro_op_resolver, tensor_arena,
                                       tensor_arena_size,
                                       &micro_error_reporter);
  interpreter.AllocateTensors();

 

> 입력 텐서 크기 확인

  // 메모리 영역의 정보를 획득한다.
  TfLiteTensor* input = interpreter.input(0);
  // 예상하는 속성이 그대로 존재하는지 확인한다.
  TF_LITE_MICRO_EXPECT_NE(nullptr, input);
  TF_LITE_MICRO_EXPECT_EQ(2, input->dims->size); //입력텐서의 크기는 2
  TF_LITE_MICRO_EXPECT_EQ(1, input->dims->data[0]); //단일 차원 나타냄
  TF_LITE_MICRO_EXPECT_EQ(1960, input->dims->data[1]); //단일 차원 나타냄 
  TF_LITE_MICRO_EXPECT_EQ(kTfLiteInt8, input->type);

 

> 'yes' 음성에 대한 샘플 스펙트로그램을 가져온다.

// g_yes_micro_f2e59fea_nohash_1_data에 저장되어있다.
// yes의 스펙트로그램을 메모리 영역으로 복사한다. (입력위해)
const int8_t* yes_features_data = g_yes_micro_f2e59fea_nohash_1_data;
  // 1D 배열로 존재하는 스펙트로그램을 반복문으로 읽어온다.
  for (size_t i = 0; i < input->bytes; ++i) {
    input->data.int8[i] = yes_features_data[i];
  }

 

> 입력 모델 실행하고 성공했는지 확인한다.

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

 

> 출력결과를 모델에서 가져와 예상한 크기와 유형인지 확인한다.

> 예상한 yes의 점수가 다른 클래스보다 높은지 확인한다.

// yes_score 변수의 값이 다른 변수들보다 높아야 될 것이다. 
// (yes 스펙트로그램을 입력으로 줬기 때문에)
  uint8_t silence_score = output->data.int8[kSilenceIndex] + 128;
  uint8_t unknown_score = output->data.int8[kUnknownIndex] + 128;
  uint8_t yes_score = output->data.int8[kYesIndex] + 128;
  uint8_t no_score = output->data.int8[kNoIndex] + 128;
  TF_LITE_MICRO_EXPECT_GT(yes_score, silence_score);
  TF_LITE_MICRO_EXPECT_GT(yes_score, unknown_score);
  TF_LITE_MICRO_EXPECT_GT(yes_score, no_score);

 

> no 스펙트로그램으로도 실행해본다.

 

위와 같이 코드를 하나씩 짚어봤다.

테스트를 실행하려면 아래와 같은 명령을 실행하면 된다.

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

 


 

audio_provicer_test.cc : 오디오 추출

마이크H/W 를 코드와 연결하는 역할

 

> GetAudioSamples 함수 사용

TfLiteStatus GetAudioSamples(tflite::ErrorReporter* error_reporter,
                             int start_ms, int duration_ms,
                             int* audio_samples_size, int16_t** audio_samples) {
// 인수는 순서대로 ErrorReporter인스턴스, 시작시간, 기간, 두개의 포인터
  for (int i = 0; i < kMaxAudioSampleSize; ++i) {
    g_dummy_audio_data[i] = 0;
  }
  *audio_samples_size = kMaxAudioSampleSize;
  *audio_samples = g_dummy_audio_data;
  return kTfLiteOk;
}

결과적으로 이 함수는 16비트 펄스 코드 변조 오디오 데이터의 배열을 반환한다.

첫번째 포인터인 audio_sample_size는 오디오데이터에서 16비트 샘플의 총 개수를 받아온다.

audio_samples는 오디오 데이터 자체를 포함하는 배열을 받아온다.

 

> 함수가 호출된 후 포인터가 올바르게 할당되었는지 테스트한다.

TF_LITE_MICRO_TEST(TestAudioProvider) {
  tflite::MicroErrorReporter micro_error_reporter;

  int audio_samples_size = 0;
  int16_t* audio_samples = nullptr;
  TfLiteStatus get_status =
      GetAudioSamples(&micro_error_reporter, 0, kFeatureSliceDurationMs,
                      &audio_samples_size, &audio_samples);
  TF_LITE_MICRO_EXPECT_EQ(kTfLiteOk, get_status);
  TF_LITE_MICRO_EXPECT_LE(audio_samples_size, kMaxAudioSampleSize);
  TF_LITE_MICRO_EXPECT_NE(audio_samples, nullptr);

//올바른 크기인지 보기 위해 예상되는 샘플 수 만큼 반복문을 돌린다.
  int total = 0;
  for (int i = 0; i < audio_samples_size; ++i) {
    total += audio_samples[i];
  }
}

 

 


 

feature_provider_mock_test.cc  : 특징 추출

오디오 추출기에서 받은 1초짜리 wav를 스펙트로그램으로 변환한다. (배열을 채우는것)

 

> 우선 feature_provider.h의 FeatureProvider 인터페이스를 먼저 보자.

class FeatureProvider {
 public:
  // FeatureProvider를 생성하고 메모리 영역에 바인딩한다.
  // 후속 호출에 데이터가 필요하기 때문에 수명 주기 동안 접근가능한 상태로 유지
  FeatureProvider(int feature_size, int8_t* feature_data);
  ~FeatureProvider();

  // 오디오 input으로 데이터를 채우고 업데이트 된 특징 수를 반환한다.
  TfLiteStatus PopulateFeatureData(tflite::ErrorReporter* error_reporter,
                                   int32_t last_time_in_ms, int32_t time_in_ms,
                                   int* how_many_new_slices);

 private:
  int feature_size_;
  int8_t* feature_data_;
  // 캐시된 정보가 추출기의 첫 호출인 경우 그 정보를 사용하지 않는다.
  bool is_first_run_;
};

 

> 이후 테스트 코드를 확인하자.

TF_LITE_MICRO_TEST(TestFeatureProviderMockYes) {
  tflite::MicroErrorReporter micro_error_reporter;

  int8_t feature_data[kFeatureElementCount];
  // FeatureProvider를 생성하기 위해 인수를 전달한다.
  // kFeatureElementCount 는 스펙트로그램 에 있어야되는 데이터 원소의 전체 수
  // feature_data는 스펙트로그램 데이터로 채울 배열에 대한 포인터
  FeatureProvider feature_provider(kFeatureElementCount, feature_data);

  int how_many_new_slices = 0;
  // 1초동안의 오디오 feature를 얻기 위해 아래 함수를 호출한다.
  TfLiteStatus populate_status = feature_provider.PopulateFeatureData(
      &micro_error_reporter, /* last_time_in_ms= */ 0, /* time_in_ms= */ 970,
      &how_many_new_slices);
  TF_LITE_MICRO_EXPECT_EQ(kTfLiteOk, populate_status);
  TF_LITE_MICRO_EXPECT_EQ(kFeatureSliceCount, how_many_new_slices);

  for (int i = 0; i < kFeatureElementCount; ++i) {
    TF_LITE_MICRO_EXPECT_EQ(g_yes_micro_f2e59fea_nohash_1_data[i],
                            feature_data[i]);
  }
}

특징 추출기는 last_time_in_ms를 time_in_ms와 비교하여 그 사이 시간 오디오에 대한 스펙트로그램 데이터를 생성하고 feature_data 배열을 업데이트한 뒤 1초보다 오래된것은 삭제한다.

 

테스트 코드는 아래 명령으로 실행하며, yes 또는 no를 나타내는 오디오 테스트를 수행한다.

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

 


recognize_commands_test.cc : 명령 인식

 

앞에서는 모델이 확률 세트를 출력했다.

이번엔 이 확률이 임계값이상이면 과연 성공적인 호출어 감지인가 를 판별한다.

 

예를 들면 noted 라는 단어를 no 까지만 듣고 실제론 no가 아닌데도 no 일 확률을 올렸을 수도 있기 때문이다. 

 

> 해당 인터페이스는 recognize_commands.h에 있으며 이는 아래와 같다

class RecognizeCommands {
 public:
  explicit RecognizeCommands(tflite::ErrorReporter* error_reporter,
                             int32_t average_window_duration_ms = 1000,
                             uint8_t detection_threshold = 200,
                             int32_t suppression_ms = 1500,
                             int32_t minimum_count = 3);
  TfLiteStatus ProcessLatestResults(const TfLiteTensor* latest_results,
                                    const int32_t current_time_ms,
                                    const char** found_command, uint8_t* score,
                                    bool* is_new_command);

RecognizeCommands 클래스의 생성자는 아래 사항에 대한 기본값을 정의한다.

average_window_duration_ms : 창의 길이

detection_threshold : 명령 탐지의 기준이 되는 최소 평균 점수

suppression_ms : 명령을 인식한 후 두 번째 명령을 인식하기 전에 기다리는 시간

minimum_count : 결과를 세는 데 필요한 최소 추론 횟수

 

> recognize_command.cc 

> 입력 텐서가 올바른 모양과 유형인지 확인

> current_time_ms를 검사해서 가장 최근결과 이후인지 확인

> 최신 결과를 평균화 할 목록에 추가한다.

> 평균화 값 내에 minimun_count 보다 적은 결과가 있는 경우 유효한 평균이 제공되지 않는다.

> 평균을 낼 수 있다면 평균을 낸다.

> 최고 점수인 카테고리를 찾는다.

  int current_top_index = 0;
  int32_t current_top_score = 0;
  for (int i = 0; i < kCategoryCount; ++i) {
    if (average_scores[i] > current_top_score) {
      current_top_score = average_scores[i];
      current_top_index = i;
    }
  }
  const char* current_top_label = kCategoryLabels[current_top_index];

 

> 탐지 임계값보다 높고 유효 탐지 후 너무 빨리 발생하지 않았는지 확인한다.

> 유효하다면 is_new_command가 true로 설정된다.

 

아래 명령어로 테스트를 실행해볼 수 있다.

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

 


command_responder_test.cc : 명령 응답

호출어가 감지됐음을 알려주는 출력을 생성하는 코드

 

> command_responder.cc

> 탐지 결과를 텍스트로 기록하는 구문이다.

void RespondToCommand(tflite::ErrorReporter* error_reporter,
                      int32_t current_time, const char* found_command,
                      uint8_t score, bool is_new_command) {
  if (is_new_command) {
    TF_LITE_REPORT_ERROR(error_reporter, "Heard %s (%d) @%dms", found_command,
                         score, current_time);
  }
}

 

> command_responder_test.cc

> 함수를 호출해서 올바른 출력을 내는지 테스트한다.

TF_LITE_MICRO_TEST(TestCallability) {
  tflite::MicroErrorReporter micro_error_reporter;
  RespondToCommand(&micro_error_reporter, 0, "foo", 0, true);
}

 

> 터미널에 입력해서 출력을 확인해보자.

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

 


 

호출어를 듣는 부분과 MCU에 직접 이식하는 파트는 다음 포스팅에서 이어나갈 예정이다.

 

 

끝.

 

 

 

 

 

반응형