Arduino Makine Öğrenimi ile Hareket Tanımlama

Bu Arduino Makine Öğrenimi projesinde, yaptığınız hareketleri tanımlamak için bir ivme ölçer sensörü kullanacağız. Bu içerik aslında Tensorflow blogunda yapılan bir projenin yeniden yapımıdır. Tensorflow bloğunda ki yazının aksine Arduino Nano 33 BLE yerine eski nesil ve daha güçsüz olan Arduino Nano kullanacağız. Arduino Nano, 32kb flash ve sadece2 kb RAM ile donatılmış bir geliştirme kartıdır.

arduino makine öğrenimi
Kara sınırı(decision), %99 doğruluk

Özelliklerin Tanımı

Hangi hareketi yaptığımızı anlamak için bir IMU’dan yani ivme ölçer sensöründen gelen 3 eksen (X, Y, Z) boyunca ivmeleri kullanacağız. NUM_SAMPLES ile ilk hareket algılamasından başlayarak sabit bir sayıda hareketleri kaydedeceğiz.

Bu, özellik vektörlerimizin 3 * NUM_SAMPLESArduino Nano’nun hafızasına sığmayacak kadar büyük olabilecek boyutta olacağı anlamına gelir. NUM_SAMPLESmümkün olduğu kadar yalın tutmak için düşük bir değerle başlayacağız. Sınıflandırmalarınız yetersiz doğruluktan muzdaripse, bu sayıyı artırabilirsiniz.

Örnek Verileri Kaydetmek

Sensörden(IMU) Gelen Verileri Okumak

Her şeyden önce, sensörden ham verileri okumamız gerekiyor. Bu kod parçası, kullandığınız belirli sensöre göre farklı olacaktır. Biz 9 eksenli “MPU9250” sensörünü kullanacağız alternatif olarak farklı bir ivme sensörünü ya da aynı aile üyesi olan MPU6050 sensörünü kullanabilirsiniz. İşleri basit ve anlaşılır tutmak için sensör kurulumu ve okuma işlemini 2 fonksiyonda yapacağız: imu_setupve imu_read.

MPU6050 ve MPU 9250 için birkaç örnek uygulamayı görebilirsiniz. Hangi kodu kullanırsanız kullanın projenin ana kodunda çağırmak için kullandığınız sensör kodunu imu.h isminde bir dosyaya kaydetmelisiniz. Ayrıca tüm kodları ve dosyaları tek bir klasörde tutmanız işlerinizi kolaylaştıracaktır.

MPU6050 İçin

#include "Wire.h"
// kütüphane https://github.com/jrowberg/i2cdevlib/tree/master/Arduino/MPU6050
#include "MPU6050.h"
#define OUTPUT_READABLE_ACCELGYRO

MPU6050 imu;

void imu_setup() {
    Wire.begin();
    imu.initialize();
}

void imu_read(float *ax, float *ay, float *az) {
    int16_t _ax, _ay, _az, _gx, _gy, _gz;

    imu.getMotion6(&_ax, &_ay, &_az, &_gx, &_gy, &_gz);

    *ax = _ax;
    *ay = _ay;
    *az = _az;
}

MPU9250 İçin

#include "Wire.h"
// kütüphane https://github.com/bolderflight/MPU9250
#include "MPU9250.h"

MPU9250 imu(Wire, 0x68);

void imu_setup() {
    Wire.begin();
    imu.begin();
}

void imu_read(float *ax, float *ay, float *az) {
    imu.readSensor();

    *ax = imu.getAccelX_mss();
    *ay = imu.getAccelY_mss();
    *az = imu.getAccelZ_mss();
}

Ana .ino dosyasında, sensör değerlerini seri monitör / çiziciye döküyoruz:

#include "imu.h"

#define NUM_SAMPLES 30
#define NUM_AXES 3
// bazen okumalarda "ani artışlar" alabilirsiniz
// çok büyük değerleri kesmek için mantıklı bir değer ayarlayabilirsiniz
#define TRUNCATE_AT 20

double features[NUM_SAMPLES * NUM_AXES];

void setup() {
    Serial.begin(115200);
    imu_setup();
}

void loop() {
    float ax, ay, az;

    imu_read(&ax, &ay, &az);

    ax = constrain(ax, -TRUNCATE_AT, TRUNCATE_AT);
    ay = constrain(ay, -TRUNCATE_AT, TRUNCATE_AT);
    az = constrain(az, -TRUNCATE_AT, TRUNCATE_AT);

    Serial.print(ax);
    Serial.print('\t');
    Serial.print(ay);
    Serial.print('\t');
    Serial.println(az);
}

Seri çiziciyi açıp, okumalarınızın aralığı hakkında fikir sahibi olmak için biraz hareket ettirin:

arduino makine öğrenimi

Kalibrasyon

Yerçekimi nedeniyle, durağan Z ekseninde -9.8’lik sabit bir değer alıyoruz (bunu üstteki hareketli görselden görebilirsiniz). Bu sabit değeri ortadan kaldırmak için 9.8’lik bir ofset oluşturmamız gerekiyor, bu sayede Z ekseninde yapılan hareketli yerçekiminden etkilenmemesini sağlıyoruz.

double baseline[NUM_AXES];
double features[NUM_SAMPLES * NUM_AXES];

void setup() {
    Serial.begin(115200);
    imu_setup();
    calibrate();
}

void loop() {
    float ax, ay, az;

    imu_read(&ax, &ay, &az);

    ax = constrain(ax - baseline[0], -TRUNCATE, TRUNCATE);
    ay = constrain(ay - baseline[1], -TRUNCATE, TRUNCATE);
    az = constrain(az - baseline[2], -TRUNCATE, TRUNCATE);
}

void calibrate() {
    float ax, ay, az;

    for (int i = 0; i < 10; i++) {
        imu_read(&ax, &ay, &az);
        delay(100);
    }

    baseline[0] = ax;
    baseline[1] = ay;
    baseline[2] = az;
}

Z ekseni kalibrasyonundan sonra seri çizici böyle görünecek:

arduino makine öğrenimi

İlk Hareketi Algılama

Şimdi hareket olup olmadığını kontrol etmemiz gerekiyor. Basit tutmak için, hızlanmada yüksek bir değer arayacak saf bir yaklaşım kullanacağız: bir eşik aşılırsa, bir hareket başlıyor anlamına gelecek.

Kalibrasyon adımını yaptıysanız, 5’lik bir eşik iyi çalışmalıdır. Kalibrasyon yapmadıysanız, ihtiyaçlarınıza uygun bir değer bulmanız gerekir.

#include imu.h

#define ACCEL_THRESHOLD 5

void loop() {
    float ax, ay, az;

    imu_read(&ax, &ay, &az);

    ax = constrain(ax - baseline[0], -TRUNCATE, TRUNCATE);
    ay = constrain(ay - baseline[1], -TRUNCATE, TRUNCATE);
    az = constrain(az - baseline[2], -TRUNCATE, TRUNCATE);

    if (!motionDetected(ax, ay, az)) {
        delay(10);
        return;
    }
}

bool motionDetected(float ax, float ay, float az) {
    return (abs(ax) + abs(ay) + abs(az)) > ACCEL_THRESHOLD;
}

Kayıt Optimizasyonu

Herhangi bir hareket olmazsa herhangi bir işlem yapmıyoruz ve izlemeye devam ediyoruz. Hareket oluyorsa, sonraki NUM_SAMPLESokumalarını seri monitöre yazdırıyoruz.

void loop() {
    float ax, ay, az;

    imu_read(&ax, &ay, &az);

    ax = constrain(ax - baseline[0], -TRUNCATE, TRUNCATE);
    ay = constrain(ay - baseline[1], -TRUNCATE, TRUNCATE);
    az = constrain(az - baseline[2], -TRUNCATE, TRUNCATE);

    if (!motionDetected(ax, ay, az)) {
        delay(10);
        return;
    }

    recordIMU();
    printFeatures();
    delay(2000);
}

void recordIMU() {
    float ax, ay, az;

    for (int i = 0; i < NUM_SAMPLES; i++) {
        imu_read(&ax, &ay, &az);

        ax = constrain(ax - baseline[0], -TRUNCATE, TRUNCATE);
        ay = constrain(ay - baseline[1], -TRUNCATE, TRUNCATE);
        az = constrain(az - baseline[2], -TRUNCATE, TRUNCATE);

        features[i * NUM_AXES + 0] = ax;
        features[i * NUM_AXES + 1] = ay;
        features[i * NUM_AXES + 2] = az;

        delay(INTERVAL);
    }
}
void printFeatures() {
    const uint16_t numFeatures = sizeof(features) / sizeof(float);
    
    for (int i = 0; i < numFeatures; i++) {
        Serial.print(features[i]);
        Serial.print(i == numFeatures - 1 ? 'n' : ',');
    }
}

Her hareket için 15-20 örneği kaydedelim ve her hareket için bir tane olmak üzere tek bir dosyaya kaydedelim. Çok boyutlu verilerle uğraştığımız için gürültünün ortalamasını almak için mümkün olduğunca çok örnek toplamanız gerekir.

Sınıflandırıcıyı Eğitip, Dışa Aktarmak

Eğer aşağıda ki kod sizin için bir anlam ifade etmiyorsa; Python ya da C++ ile eğittiğiniz bir modeli Arduino veya diğer geliştirme kartlarında kullanmak istiyorsanız, bu yazımızdan detayları öğrenebilirsiniz.

from sklearn.ensemble import RandomForestClassifier
from micromlgen import port

# örneklerinizi veri kümesi klasörüne koyun
# dosya başına bir sınıf
# CSV formatında satır başına bir özellik vektörü
features, classmap = load_features('dataset/')
X, y = features[:, :-1], features[:, -1]
classifier = RandomForestClassifier(n_estimators=30, max_depth=10).fit(X, y)
c_code = port(classifier, classmap=classmap)
print(c_code)

Bu noktada yazdırılan kodu kopyalamanız ve Arduino proje klasörünüze model.h olarak kaydetmeniz gerekiyor.

Makine öğrenimi ile ilgili bu projede, önceki ve daha basit olanlardan farklı olarak, %100 doğruluğa kolayca ulaşamıyoruz. Hareket oldukça gürültülüdür, bu nedenle sınıflandırıcı için birkaç parametre denemeli ve en iyi performansı gösterenleri seçmelisiniz. Birkaç örnek gösterelim:

arduino makine öğrenimi

Sensör özelliklerinin 2 PCA bileşeninin karar sınırları, doğrusal çekirdek(Linear kernel)

arduino makine öğrenimi

Sensör özelliklerinin 2 PCA bileşeninin karar sınırları, Polinom çekirdeği(Polynomial kernel)

arduino makine öğrenimi

Sensör özelliklerinin 2 PCA bileşeninin karar sınırları, RBF çekirdeği(kernel), 0.01 gama

arduino makine öğrenimi

Sensör özelliklerinin 2 PCA bileşeninin karar sınırları, RBF çekirdeği(kernel), 0.001 gama

Uygun Modeli Seçmek

Şimdi en iyi modeli seçtiğimize göre onu C koduna aktarmalıyız. İşte sorun geliyor: tüm modeller geliştirme kartına sığmayacak.

SVM’nin (Destek Vektör Makineleri) çekirdeği, destek vektörleridir: her eğitilmiş sınıflandırıcı, belirli bir sayı ile karakterize edilecektir. Sorun şu ki: çok fazla varsa, oluşturulan kod mikroişlemcinin flaşına sığmayacak kadar büyük olacaktır.

Bu nedenle doğruluk konusunda en iyi modeli seçmek yerine en iyi performans gösterenden en kötüye doğru bir sıralama yapmalısınız. Her model için, en baştan başlayarak, onu Arduino projenize aktarmalı ve derlemeye çalışmalısınız: eğer uyuyorsa sorunsuz kullanabilirsiniz. Aksi takdirde, bir sonrakini seçmeli ve tekrar denemelisiniz.

Sıkıcı bir süreç gibi görünebilir, ancak 2 Kb RAM ve 32 Kb flash’ta 90 özellikten bir sınıf çıkarmaya çalıştığımızı unutmayın.

Test ettiğimiz farklı kombinasyonlar için birkaç rakam raporu:

ÇekirdekCGammaDereceVektörlerFlaş boyutuRAM (b)Ort. doğruluk
RBF100.0013753 Kb1228%99
Poly1000.00121225 Kb1228%99
Poly1000.00132540 Kb1228%97
Doğrusal5014055 Kb1228%95
RBF1000.016180 Kb1228%95

Gördüğünüz gibi, tüm sınıflandırıcılar için test setinde çok yüksek bir doğruluk elde ettik: Arduino Nano’da sadece bir tanesidi kullandık. Tabii ki, daha büyük bir mikroişlemci kullanırsanız, diğerlerini de kullanabilirsiniz.

Hatırlatma

Bir yan not olarak, RAM sütuna bir göz atın, tüm değerler eşittir. Bunun nedeni uygulamada destek vektörlerinin sayısından bağımsız olması ve yalnızca özellik sayısına bağlı olmasıdır.

Çıkarımı Çalıştırma

#include "model.h"

void loop() {
    float ax, ay, az;

    imu_read(&ax, &ay, &az);

    ax = constrain(ax - baseline[0], -TRUNCATE, TRUNCATE);
    ay = constrain(ay - baseline[1], -TRUNCATE, TRUNCATE);
    az = constrain(az - baseline[2], -TRUNCATE, TRUNCATE);

    if (!motionDetected(ax, ay, az)) {
        delay(10);
        return;
    }

    recordIMU();
    classify();
    delay(2000);
}

void classify() {
    Serial.print("Algılanan hareket: ");
    Serial.println(classIdxToName(predict(features)));
}

Tüm çalışmaları ve işleri bitirdik. Artık Arduino Nano ve 2 Kb RAM ile hareketleri sınıflandırabilirsiniz. Uzun sinir ağları, Tensorflow, 32-bit ARM işlemcileri olmadan, 8-bit bir mikroişlemci ile SVM tabanlı %97 doğrulukta bir makine öğrenimi gerçekleştirdik.

Arduino Nano’yu (eski nesil) hedef alan program, 25310 bayt (%82) program alanı ve 1228 bayt (%59) RAM gerektiriyor. Bu, Arduino Nano’nun sağladığından bile daha az alanda makine öğrenimini çalıştırabileceğiniz anlamına gelir. Peki Arduino üzerinde makine öğrenimi çalıştırabilir miyim sorusunun cevabının net bir şekilde EVET olduğunu göstermiş olduk.