本帖最后由 云天 于 2023-6-19 10:57 编辑
【项目背景】
很早之前就接触过TinyML,正如介绍中所说:微型机器学习Tiny Machine Learning (TinyML) 是机器学习的一个研究领域,专注于在超低功耗的微控制器设备上开发和部署机器学习模型。TinyML使机器学习可以在在安全、低延迟、低功耗和低带宽的边缘设备上运行。TinyML 是目前嵌入式计算领域最热门的趋势之一,甚至被称为“下一轮人工智能革命”。这主要得益于其应用潜力巨大,能够快速助力创新者和工程师使用低功耗微控制器迅速将一些想法变为现实。此外,人工智能AI正快速从“云端”走向“边缘”,进入到越来越小的物联网设备中。能够在资源最少的 MCU 上进行推理的TinyML,恰好促成了边缘工业物联网 (IIoT) 设备的兴起。
然后,我又接触到EDGE IMPULSE,适用于嵌入式领域的在线机器学习网站。Edge Impulse是一个全面的端到端解决方案,可以帮助工程师和开发者创建,部署和运行内置机器学习模型的设备。Edge Impulse提供了丰富的工具和库,可以让开发者以高效的方式处理传感器数据、构建模型等。Edge Impulse的核心目标是让设备上的机器学习变得简单、高效和专业化。这样工程师和开发者可以将注意力集中在解决有趣的问题上,而不是浪费时间处理技术上的细节。
但由于网络原因,之前访问速度有些缓慢,加之没有合适的设备,浅尝辄止,学习了一段时间就停滞了。
近期从DF商城入手了Raspberry Pi Pico微型控制板,而且EDGE IMPULSE也开始支持Pico。Pico是 Edge Impulse 完全支持的开发板。这些板带有一个特殊的固件,可以从所有传感器收集数据,允许您构建新的随时可用的二进制文件,其中包括您训练有素的冲动,并附带将您的冲动与您的自定义固件集成的示例。
但由于网上相关的教程和案例比较少,所以只能阅读官方文档不断研究摸索。
【项目设计】
打鼾常见于男性及肥胖人群,鼾声过大可导致伴侣无法入睡,严重时可影响第二天的精神状态,导致白天疲乏困倦。我有时也打打呼噜,随着加强体育运动体重下降,晚上只是偶尔因为姿势不对打呼噜,但这很影响家人休息。但只要碰我一下,我翻一下身,就好了。
为了检测我打呼噜,并提醒我,我利用Pico结合EDGE IMPULSE训练的模型,控制舵机及伸缩杆,推动气垫触碰头部提醒。因这个装置只是模型机,理想设计可在枕头中使用多个单独控制的充气馕。
【硬件连接】
压力传感用于,检测头部所在位置,当Pico检测到有打呼噜声时,同时接收压力传感器的数值,判断头部在左侧还是右侧,由此启动相应一侧的舵机和伸缩杆。
【模型训练】
1.连接传感器并采集数据
更新Pico固件:
开发板还没有附带正确的固件。要更新固件:最新的 Edge Impulse 固件,并解压文件。将ei_rp2040_firmware.uf2文件从文件夹拖到 USB 大容量存储设备。等待闪烁完成,拔下并重新插入开发板以启动新固件。
2.收集数据
通过WebUSB连接Pico,采集数据。WebUSB 和 Edge Impulse 守护程序通过将预构建的 Edge Impulse 固件闪存到您的主板,与任何完全支持的设备一起工作。
采集训练集和测试集
采集背景音和呼噜声
3.裁剪样本
背景音一次采集20秒,因训练时数据将使用4秒的窗口大小。所以要裁剪数据样本,转到要裁剪的样本并单击“⋮ ”,然后选择裁剪样本。可以指定长度4秒,或拖动手柄以调整窗口大小,然后四处移动窗口以进行选择。
4.创建 Impulse
一个完整的 Impulse 将由 3 个主要构建块组成:输入块、处理块和学习块。
输入块中设置窗口大小为4000ms(4秒),处理块选择“频谱图”——从音频或传感器数据中提取频谱图,非常适合非语音音频或具有连续频率的数据。因“呼噜声”属于“非语音音频”。学习块选择“分类”。
5.频谱图——生成特征
6.分类器——开始训练
7.EON调谐器
EON Tuner 可帮助您在目标设备的限制范围内为您的应用程序找到并选择最佳的嵌入式机器学习模型。EON Tuner 分析您的输入数据、潜在的信号处理模块和神经网络架构 - 并为您提供适合您所选设备的延迟和内存要求的可能模型架构的概览。
【部署模型】
在训练和验证您的模型后,您现在可以将其部署到任何设备。这使得模型可以在没有互联网连接的情况下运行,最大限度地减少延迟,并以最低的功耗运行。部署为可定制的库。这些部署选项让您可以将您的冲动转化为完全优化的源代码,可以进一步定制并与您的应用程序集成。可定制的库将您所有的信号处理模块、配置和机器学习模块打包到一个包含所有可用源代码的包中。
Arduino库
【安装库文件】
将下载的库文件,安装到Arduino IDE中。
【程序编写】
Arduino IDE中安装Pico主控。
1.测试推理
- #include <hulu_inferencing.h>
- //特征数据
- int num=400;
- float features[400];
- int windowsize=4;//4秒
- int HZ=1000/(num/windowsize);//采集数据频率 10赫兹
- //推理库调用的函数
- int raw_feature_get_data(size_t offset, size_t length, float *out_ptr) {
- for (int i = 0; i < num; i = i + 1)
- {
- features[i]=analogRead(A0);//从A0口(GP26)采集数据
- delay(HZ);
- }
- memcpy(out_ptr, features + offset, length * sizeof(float));//复制out_ptr中的原始要素数据
- return 0;
- }
- void inference_result(ei_impulse_result_t result);
- void setup()
- {
- pinMode(LED_BUILTIN, OUTPUT);
- }
-
- void loop()
- {
- if (sizeof(features) / sizeof(float) != EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE) {
- ei_printf("The size of your 'features' array is not correct. Expected %lu items, but had %lu\n",
- EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE, sizeof(features) / sizeof(float));
- delay(1000);
- return;
- }
- ei_impulse_result_t result = { 0 };
- // 特征数据存储在闪存
- signal_t features_signal;
- features_signal.total_length = num;
- features_signal.get_data = &raw_feature_get_data;
- // 唤起冲动
- EI_IMPULSE_ERROR res = run_classifier(&features_signal, &result, false /* debug */);
- if (res != EI_IMPULSE_OK) {
- ei_printf("ERR: Failed to run classifier (%d)\n", res);
- return;
- }
- // 推断返回代码
- inference_result(result);
- delay(1);
- }
- void inference_result(ei_impulse_result_t result) {
- // 输出预测结果(分类)
- for (uint16_t i = 0; i < EI_CLASSIFIER_LABEL_COUNT; i++) {
- if (ei_classifier_inferencing_categories[i]=="hulu" && result.classification[i].value>0.8 ) {
- digitalWrite(LED_BUILTIN, HIGH);
- }
- else
- {
- digitalWrite(LED_BUILTIN, LOW);
- }
- }
- }
-
复制代码
2.舵机测试程序-
-
- #include <Servo.h>
-
- Servo myservo; // 创建伺服对象以控制伺服
-
- int potpin = A0; // 用于连接声音传感器的模拟引脚
- int val; // 从模拟引脚读取值的变量
-
- void setup() {
- Serial.begin(9600);
- myservo.attach(3); // 将引脚3上的伺服连接到伺服对象
- }
-
- void loop() {
- val = analogRead(potpin); // 读取声音传感器的值(值在0和1023之间)
-
- val = map(val, 0, 1023, 0, 180); // 缩放以与伺服一起使用(值在0到180之间)
- myservo.write(val); // 根据缩放值设置舵机位置
- Serial.println(val);
- delay(15); //等待舵机到达那里
- }
-
复制代码
3.压力传感器测试
-
-
- int sensorPin = A1;
-
- int sensorValue = 0;
-
- void setup() {
-
- Serial.begin(9600);
- }
-
- void loop() {
- // 从A1读取压力传感器的值
- sensorValue = analogRead(sensorPin);
-
- Serial.println(sensorValue);
-
- delay(1000);
-
- }
-
复制代码
4.伸缩杆测试-
-
- void setup() {
- // initialize the LED pin as an output:
- pinMode(14, OUTPUT);
- pinMode(15, OUTPUT);
- pinMode(16, OUTPUT);
- pinMode(17, OUTPUT);
-
- }
-
- void loop() {
-
- digitalWrite(14, HIGH);
- digitalWrite(15, HIGH);
- delay(10000);
- digitalWrite(14, LOW);
- digitalWrite(15, HIGH);
- delay(10000);
- digitalWrite(16, HIGH);
- digitalWrite(17, HIGH);
- delay(10000);
- digitalWrite(16, LOW);
- digitalWrite(17, HIGH);
- delay(10000);
- }
-
复制代码
5.完整程序-
- #include <hulu_inferencing.h>
- #include <Servo.h>
-
- Servo myservo1; // 创建伺服对象以控制舵机1
- Servo myservo2; // 创建伺服对象以控制舵机2
- int val; // 从模拟引脚读取值的变量
- //特征数据
- int num=400;
- float features[400];
- int windowsize=4;//4秒
- int HZ=1000/(num/windowsize);//采集数据频率 10赫兹
- int bs=0;
- //推理库调用的函数
- int raw_feature_get_data(size_t offset, size_t length, float *out_ptr) {
-
- for (int i = 0; i < num; i = i + 1)
- {
- features[i]=analogRead(A0);//从A0口(GP26)采集数据
- //Serial.print(i);
- //Serial.print(" ");
- //Serial.println(features[i]);
- delay(HZ);
- }
- memcpy(out_ptr, features + offset, length * sizeof(float));//复制out_ptr中的原始要素数据
-
- return 0;
- }
-
- void inference_result(ei_impulse_result_t result);
-
-
- void setup()
- {
- pinMode(LED_BUILTIN, OUTPUT);
- myservo1.attach(12); // 将引脚12上的舵机1连接到伺服对象
- myservo2.attach(13); // 将引脚13上的舵机2连接到伺服对象
- pinMode(14, OUTPUT);
- pinMode(15, OUTPUT);
- pinMode(16, OUTPUT);
- pinMode(17, OUTPUT);
- Serial.begin(9600);
- //归位
- digitalWrite(16, LOW);
- digitalWrite(17, HIGH);
-
- myservo1.write(10);
- delay(2000);
- digitalWrite(14, LOW);
- digitalWrite(15, HIGH);
-
- myservo2.write(170);
- delay(2000);
- digitalWrite(15, LOW);
- digitalWrite(17, LOW);
- }
-
- void loop()
- {
- if (sizeof(features) / sizeof(float) != EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE) {
- ei_printf("The size of your 'features' array is not correct. Expected %lu items, but had %lu\n",
- EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE, sizeof(features) / sizeof(float));
- delay(1000);
- return;
- }
- ei_impulse_result_t result = { 0 };
-
- // 特征数据存储在闪存
- signal_t features_signal;
-
- features_signal.total_length = num;
-
- features_signal.get_data = &raw_feature_get_data;
-
- // 唤起冲动
- EI_IMPULSE_ERROR res = run_classifier(&features_signal, &result, false /* debug */);
-
- if (res != EI_IMPULSE_OK) {
- ei_printf("ERR: Failed to run classifier (%d)\n", res);
- return;
- }
-
-
- // 推断返回代码
-
- inference_result(result);
-
- delay(1);
-
- }
-
- void inference_result(ei_impulse_result_t result) {
-
- // 输出预测结果(分类)
-
- for (uint16_t i = 0; i < EI_CLASSIFIER_LABEL_COUNT; i++) {
-
- if (ei_classifier_inferencing_categories[i]=="hulu" && result.classification[i].value>0.8 ) {
- digitalWrite(LED_BUILTIN, HIGH);
- //左侧提醒
- if(analogRead(A1)>500){
- myservo1.write(170);//转到头部所在位置
- delay(2000);
- digitalWrite(16, HIGH);//推杆提醒
- digitalWrite(17, HIGH);
- delay(5000);
- //归位
- digitalWrite(16, LOW);
- digitalWrite(17, HIGH);
- delay(5000);
- myservo1.write(10);
- digitalWrite(17, LOW);
- delay(2000);
- }
- //右侧提醒
- if(analogRead(A2)>500){
- myservo2.write(10);
- delay(2000);
- digitalWrite(14, HIGH);
- digitalWrite(15, HIGH);
- delay(5000);
- digitalWrite(14, LOW);
- digitalWrite(15, HIGH);
- delay(5000);
- myservo2.write(170);
- digitalWrite(15, LOW);
- delay(2000);
- }
-
- digitalWrite(LED_BUILTIN, LOW);
-
- }
-
- }
-
-
- }
-
-
复制代码
【视频演示】
注:1.打鼾俗称打呼噜,主要是由于睡觉时呼吸道阻塞或狭窄导致呼吸道阻力增加,进而使得经过呼吸道的空气流动受阻,引发了咽喉部软组织振动发出响声,即鼾声。2.打鼾可能是一些疾病的症状,如阻塞性睡眠呼吸暂停低通气综合征,这类患者在睡眠中有呼吸暂停和/或低通气发生,可导致严重后果。若长期打鼾无法缓解,或影响了白天的精神状态,导致白天困倦疲劳、注意力不集中、嗜睡等问题,应及时去医院就诊。
|