마이크로컨트롤러에서 머신러닝 실행 — embml 샘플 사용법

발행: (2026년 3월 1일 오후 07:06 GMT+9)
18 분 소요
원문: Dev.to

Source: Dev.to

마이크로컨트롤러에서 머신러닝 실행하기: embml 샘플 사용법

소개

마이크로컨트롤러와 같은 제한된 리소스를 가진 디바이스에서도 머신러닝 모델을 실행할 수 있다는 사실을 알고 계셨나요? embml은 이러한 환경을 위해 설계된 경량화된 머신러닝 프레임워크이며, C/C++ 코드로 변환된 모델을 손쉽게 마이크로컨트롤러에 올릴 수 있게 도와줍니다. 이번 포스트에서는 embml을 이용해 간단한 예제 모델을 훈련하고, 이를 마이크로컨트롤러에 배포하는 전 과정을 살펴보겠습니다.

사전 준비

다음 항목들이 설치되어 있어야 합니다.

  • Python 3.8 이상
  • pip 패키지 매니저
  • embml 라이브러리 (pip install embml)
  • TensorFlow 또는 PyTorch (예제에서는 TensorFlow 사용)
  • Arduino IDE (또는 사용 중인 마이크로컨트롤러에 맞는 IDE)

Tip: Windows 사용자라면 git bash 혹은 WSL을 이용하면 환경 설정이 더 수월합니다.

1️⃣ 모델 만들기

아래 코드는 TensorFlow를 사용해 1‑D 입력을 받아 두 클래스로 분류하는 간단한 신경망을 정의합니다.

import tensorflow as tf

def create_model():
    model = tf.keras.Sequential([
        tf.keras.layers.Input(shape=(10,)),   # 10개의 피처
        tf.keras.layers.Dense(16, activation='relu'),
        tf.keras.layers.Dense(2, activation='softmax')
    ])
    model.compile(optimizer='adam',
                  loss='sparse_categorical_crossentropy',
                  metrics=['accuracy'])
    return model

2️⃣ 데이터 준비 및 학습

임의의 데이터를 생성하고 모델을 학습시킵니다.

import numpy as np

# 임의의 데이터 (1000 샘플)
X = np.random.rand(1000, 10).astype(np.float32)
y = np.random.randint(0, 2, size=(1000,))

model = create_model()
model.fit(X, y, epochs=5, batch_size=32, verbose=1)

3️⃣ embml 로 변환

훈련이 끝난 모델을 embml 포맷으로 변환합니다.

embml convert \
    --model_path ./model.h5 \
    --output_path ./model_embml.c \
    --target microcontroller

위 명령은 TensorFlow SavedModel(model.h5)을 C 코드(model_embml.c)로 변환합니다. 변환된 파일에는 가중치와 추론 함수가 모두 포함됩니다.

4️⃣ 마이크로컨트롤러 프로젝트에 통합

Arduino IDE를 열고 새 스케치를 만든 뒤, 변환된 model_embml.c와 헤더 파일(model_embml.h)을 프로젝트 폴더에 복사합니다. 그런 다음 setup()loop() 함수에서 모델을 초기화하고 추론을 수행합니다.

#include "model_embml.h"

void setup() {
    Serial.begin(115200);
    // embml 초기화 (필요 시)
    embml_init();
}

void loop() {
    // 예시 입력 (10개의 float 값)
    float input[10] = {0.1, 0.2, 0.3, 0.4, 0.5,
                       0.6, 0.7, 0.8, 0.9, 1.0};

    // 추론 결과 저장
    float output[2];
    embml_predict(input, output);

    // 결과 출력
    Serial.print("Class 0 prob: ");
    Serial.println(output[0]);
    Serial.print("Class 1 prob: ");
    Serial.println(output[1]);

    delay(1000); // 1초 대기
}

5️⃣ 빌드 및 업로드

  • 보드 선택: Tools > Board에서 사용 중인 마이크로컨트롤러(예: Arduino Nano 33 BLE)를 선택합니다.
  • 포트 선택: Tools > Port에서 연결된 시리얼 포트를 지정합니다.
  • 업로드: Upload 버튼을 눌러 코드를 보드에 플래시합니다.

업로드가 완료되면 시리얼 모니터를 열어 모델이 출력하는 확률 값을 확인할 수 있습니다.

성능 팁

  • 정밀도 감소: embmlfloat32 외에도 int8 혹은 int16 정밀도로 변환할 수 있습니다. 메모리와 연산량을 크게 절감하려면 --quantize int8 옵션을 사용해 보세요.
  • 메모리 최적화: 사용하지 않는 레이어를 제거하거나, 모델 크기를 줄이면 플래시와 RAM 사용량을 최소화할 수 있습니다.
  • 배터리 수명: 추론 주기를 늘리거나, 저전력 슬립 모드를 활용해 전력 소비를 낮출 수 있습니다.

결론

embml을 이용하면 복잡한 머신러닝 파이프라인을 마이크로컨트롤러 수준으로 끌어내릴 수 있습니다. 이번 예제에서는 간단한 2‑클래스 분류 모델을 사용했지만, 동일한 흐름을 따라 이미지, 음성, 시계열 데이터 등 다양한 도메인에 적용할 수 있습니다. 여러분도 직접 모델을 설계하고, embml로 변환해 보세요. 마이크로컨트롤러에서 실시간 인텔리전스를 구현하는 재미를 느낄 수 있을 것입니다.


추가 자료

행복한 코딩 되세요! 🚀

개요

대부분의 임베디드 개발자는 이제 TinyML에 대한 이야기를 들어봤을 것입니다: 파이썬으로 모델을 학습하고, 양자화하고, 변환한 뒤, 고정된 블롭을 디바이스에 플래시하고 마이크로컨트롤러가 추론을 수행하도록 하는 방식입니다. 이 모델은 절대 학습하지도, 적응하지도 않으며 단순히 실행만 합니다.

이 방식은 특정 문제군에 대해서는 잘 작동하지만, 놓치는 부분이 많습니다.

  • 현장에서 6개월 정도 사용한 뒤 센서가 드리프트한다면 어떻게 할까요?
  • 장치를 특정 모터에 맞게 스스로 튜닝하도록 하고 싶다면, 훈련 데이터셋에 있는 일반적인 모터가 아니라 해당 모터에 맞춰야 하는데 어떻게 할까요?
  • 서버가 전혀 없는 상황이라면 어떻게 할까요?

embml은 순수 C로, 동적 메모리 할당 없이, 표준 라이브러리 외의 외부 의존성 없이, 파이썬 런타임도 전혀 사용하지 않고 디바이스 자체에서 머신러닝을 수행하는 방법을 탐구하는 샘플 레포지토리입니다.

📦 샘플 레포지토리:

이는 프로덕션용 프레임워크가 아닙니다. 잘 구조화되고 가독성이 높은 시작점이며, 임베디드 개발자가 클론하고, 읽고, 이해하고, 적용할 수 있는 레퍼런스입니다. 모든 알고리즘은 C99로 처음부터 구현되었으며, 호출자가 모든 버퍼를 직접 관리합니다.

레포에 포함된 내용

이 라이브러리는 src/에 있는 여덟 개의 모듈을 포함합니다:

모듈알고리즘
embml_linearSGD를 이용한 온라인 선형 회귀
embml_logisticSGD를 이용한 이진 로지스틱 회귀
embml_lmsLMS 및 정규화 LMS 적응 필터
embml_rls망각 인자를 포함한 재귀 최소 제곱 (RLS)
embml_iqrGivens 회전을 이용한 증분 QR
embml_nn피드포워드 MLP – 역전파, Xavier 초기화, 그래디언트 클리핑
embml_gru시계열 추론을 위한 최소 GRU 셀
embml_esnEcho State Network – 고정 리저버, RLS로 학습된 출력층

각 모듈은 .c/.h 쌍으로 구성되어 있어 펌웨어 프로젝트에 바로 포함시킬 수 있습니다.

Source:

샘플 사용법

아래 예제들은 실제 사용 사례를 보여줍니다. 이는 의사 코드가 아니라 실제로 ESP32, STM32F4, RP2040, Arduino‑Mega‑클래스 하드웨어에서 컴파일되고 실행됩니다.

선형 회귀 – 온‑디바이스 온도 보정

센서 읽값이 보드 온도에 따라 선형적으로 드리프트합니다. 서버 없이 샘플마다 실시간으로 보정 모델을 학습합니다.

#include "embml.h"

#define N_FEAT 2   /* [raw_reading, board_temp] → corrected_value */

float weights[N_FEAT];
LinearModel model;

void setup(void) {
    linear_init(&model, N_FEAT, 0.01f, weights);
}

void loop(void) {
    float x[N_FEAT] = { read_sensor(), read_board_temp() };
    float y_true    = read_reference();   /* 보정 기준값 */

    /* 배치를 사용할 필요 없이 각 샘플마다 학습 */
    linear_update(&model, x, y_true);

    float corrected = linear_predict(&model, x);
    log_value(corrected);
}

수백 개의 샘플 후 모델은 보정 곡선에 수렴합니다. 노트북도, 파이썬도 필요 없습니다. 디바이스가 스스로 학습합니다.

로지스틱 회귀 – 결함 감지

두 개의 진동 특성을 이용해 모터가 정상(0)인지 초기 결함 징후(1)를 보이는지를 분류합니다.

#include "embml.h"

#define N_FEAT 3   /* [rms_vibration, peak_freq, temp] */

float weights[N_FEAT];
LogisticModel model;

void setup(void) {
    logistic_init(&model, N_FEAT, 0.005f, weights);
}

void loop(void) {
    float x[N_FEAT] = { rms(), peak_freq(), motor_temp() };

    /* 알려진 정상 가동 단계에서는 라벨 = 0 */
    logistic_update(&model, x, 0.0f);

    /* 운영 중: */
    uint8_t fault = logistic_classify(&model, x);
    float   prob  = logistic_predict(&model, x);

    if (prob > 0.75f)
        trigger_alert();
}

LMS – 배경 잡음 제거

최소 평균 제곱(LMS) 필터는 신호에서 주기적인 잡음원을 제거하도록 적응하며, 가중치당 한 번의 곱-누적 연산만으로 매 샘플마다 업데이트됩니다 — 가능한 가장 가벼운 온라인 학습기입니다.

#include "embml.h"

#define FILTER_LEN 16

float weights[FILTER_LEN];
LMSModel model;

void setup(void) {
    /* 정규화된 LMS: 단계 크기를 수동으로 조정할 필요 없이 안정적 */
    lms_init_nlms(&model, FILTER_LEN, 0.5f, 1e-6f, weights);
}

void loop(void) {
    float noisy_signal[FILTER_LEN] = { /* ADC 샘플의 원형 버퍼 */ };
    float desired = read_reference_mic();

    lms_update(&model, noisy_signal, desired);
    float clean = lms_predict(&model, noisy_signal);

    output_audio(clean);
}

RLS – 빠른 수렴 시스템 식별

RLS는 학습률을 튜닝할 필요 없이 SGD보다 훨씬 빠르게 수렴합니다. 여기서는 실시간으로 미지의 플랜트(예: 모터 전달 함수)의 계수를 식별합니다.

#include "embml.h"

#define N 5

float weights[N], P[N * N], k_scratch[N];
RLSModel model;

void setup(void) {
    /* lambda = 0.98 : 천천히 변하는 시스템에 대한 적당한 망각 인자 */
    /* delta = 1000  : 약한 사전 정보 — 데이터를 빠르게 신뢰 */
    rls_init(&model, N, 0.98f, 1000.0f, weights, P);
}

void loop(void) {
    float x[N] = { u_delayed(1), u_delayed(2),
                   y_delayed(1), y_delayed(2), 1.0f };
    float y_now = read_plant_output();

    rls_update(&model, x, y_now, k_scratch);

    /* weights[]는 이제 ARX 모델 계수를 근사합니다 */
    float y_pred = rls_predict(&model, x);
    float residual = y_now - y_pred;
}

Incremental QR – 수치적으로 견고한 최소 제곱

입력 데이터가 조건이 좋지 않을 때(예: 특성 간 상관관계가 높을 때), RLS는 수치적 안정성을 잃을 수 있습니다. Givens 회전을 이용한 Incremental QR은 공분산 행렬을 직접 형성하지 않음으로써 이를 방지합니다.

#include "embml.h"

#define N 6

float R[N * N], f[N];
float w[N], scratch[2 * N];
IQRModel model;

void setup(void) {
    /* ridge = 1e-4 : 충분한 샘플이 모일 때까지 작은 정규화 */
    iqr_init(&model, N, 0.99f, 1e-4f, R, f);
}

void loop(void) {
    float x[N] = { feature_1(), feature_2(), feature_3(),
                   feature_4()
, feature_5(), feature_6() };
    float y   = measurement();

    iqr_update(&model, x, y, w, scratch);
    float y_est = iqr_predict(&model, x);
}

시작하기

  1. 레포지토리 복제

    git clone https://github.com/hejhdiss/embml.git
  2. 필요한 모듈 (*.c*.h)을 프로젝트의 소스 트리에 복사합니다.

  3. API를 사용하는 모든 파일에 embml.h 포함합니다.

  4. 모델 구성 (학습률, 망각 계수 등)을 위의 예시대로 설정합니다.

  5. 빌드 및 플래시 – 코드는 모든 C99‑호환 툴체인(ARM‑GCC, ESP‑IDF, Arduino 등)에서 컴파일됩니다.

라이선스

embmlMIT 라이선스 하에 배포됩니다 – 자유롭게 사용하고, 수정하고, 상업 제품에 포함시킬 수 있습니다.

피드포워드 MLP — 소형 신경망, 온‑디바이스 학습

4개의 입력, 8개의 은닉 뉴런, 1개의 출력으로 구성된 3계층 네트워크. Xavier 초기화. 역전파와 그래디언트 클리핑으로 학습 — 모두 MCU에서 수행.

#include "embml.h"

#define L0 4
#define L1 8
#define L2 1

float W0[L1*L0], b0[L1], a1[L1], d1[L1];
float W1[L2*L1], b1_[L2], a2[L2], d2[L2];
float input_buf[L0];

NNLayer layers[2] = {
    { W0, b0,  a1, d1, L0, L1, EMBML_ACT_RELU    },
    { W1, b1_, a2, d2, L1, L2, EMBML_ACT_SIGMOID },
};
NNModel net;

void setup(void) {
    nn_init(&net, layers, 2, input_buf, L0, 0.01f, 1.0f);
}

void loop(void) {
    float x[L0]      = { s1(), s2(), s3(), s4() };
    float target[L2] = { ground_truth() };

    nn_train_sample(&net, x, target);

    /* Or just inference: */
    const embml_float_t *out = nn_forward(&net, x);
    float prediction = out[0];
}

GRU — Time‑Series Inference

게이트 순환 유닛(GRU) 셀은 순차적인 센서 데이터를 한 단계씩 처리합니다. 가중치는 플래시 메모리에서 로드되며(호스트에서 오프라인으로 학습된), 은닉 상태는 시간 단계마다 유지됩니다.

#include "embml.h"

#define X_SZ 4
#define H_SZ 8

/* Weights trained offline, stored as const arrays in flash */
#include "gru_weights.h"   /* defines Wz, Wr, Wn, Uz, Ur, Un, bz, br, bn */

float h_state[H_SZ];
float scratch[3 * H_SZ];
GRUCell cell;

void setup(void) {
    gru_init(&cell, X_SZ, H_SZ,
             Wz, Wr, Wn, Uz, Ur, Un,
             bz, br, bn, h_state, scratch);
}

void loop(void) {
    float x_t[X_SZ] = { accel_x(), accel_y(), accel_z(), gyro_z() };

    gru_step(&cell, x_t);

    /* Hidden state in cell.h[] — pass to a classifier or threshold */
    float anomaly_score = cell.h[0];
    if (anomaly_score > 0.8f)
        flag_anomaly();
}

Echo State Network — 온‑디바이스 학습, 역전파 없음

레조버(무작위 가중치)는 고정되어 플래시 메모리에 저장됩니다. 선형 읽기‑출력 레이어만 RLS를 사용해 한 번에 하나의 샘플씩 학습합니다. 이는 임베디드 시계열 학습에서 적응성 및 연산 비용의 최적 균형을 제공합니다.

#include "embml.h"
#include "esn_reservoir.h"  /* const W_in[H*X], const W_res[H*H] in flash */

#define X_SZ  4
#define H_SZ 32
#define Y_SZ  1

float W_out[Y_SZ * H_SZ];
float state[H_SZ], scratch[H_SZ];
float P[H_SZ * H_SZ], k[H_SZ];

ESNModel esn;
RLSModel rls;

void setup(void) {
    esn_init(&esn, X_SZ, H_SZ, Y_SZ,
             W_in, W_res, 0.9f,
             state, scratch, W_out);
    esn_rls_init(&esn, &rls, 0.98f, 1000.0f, P, k);
}

void loop(void) {
    float x[X_SZ] = { s1(), s2(), s3(), s4() };
    float y[Y_SZ] = { read_target() };

    /* Training mode */
    esn_update_state(&esn, x);
    esn_rls_update(&esn, y);

    /* Inference mode */
    float y_out[Y_SZ];
    esn_update_state(&esn, x);
    esn_predict(&esn, y_out);
}

이 저장소가 존재하는 이유

이것은 샘플 — 이러한 알고리즘이 임베디드 C에 깔끔하게 들어맞고, API가 머신러닝 배경이 없는 펌웨어 엔지니어도 사용할 수 있으며, 온‑디바이스 학습이 중간 사양 MCU에 대해 공상 과학이 아니라는 것을 입증하는 개념 증명입니다.

만약 여러분이 이를 사용해 무언가를 만들거나, 수정하거나, 단순히 소스를 읽어 RLS 또는 Givens 회전이 평면 C 배열에서 실제로 어떻게 동작하는지 이해하고자 한다면— 바로 그것이 이 저장소의 목적입니다.

📦 샘플 저장소:

MIT License · Author: @hejhdiss · Claude Sonnet 4.5 로 생성됨

0 조회
Back to Blog

관련 글

더 보기 »

일이 정신 건강 위험이 될 때

markdown !Ravi Mishrahttps://media2.dev.to/dynamic/image/width=50,height=50,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fu...