×

使用Wio Terminal和Tensorflow Lite创建智能气象站

消耗积分:0 | 格式:zip | 大小:0.00 MB | 2023-06-25

王利祥

分享资料个

描述

Dmitry Maslov 在 Seeed 工作室博客上的原创文章

在今天的文章中,我们将使用用于微控制器的 Wio Terminal 和 Tensorflow Lite 创建一个智能气象站,能够根据 BME280 环境传感器的本地数据预测未来 24 小时的天气和降水。

poYBAGNYkeOANrBRAABoZSn6Yys537.png
 

 

我将告诉你如何应用模型优化技术,这不仅可以运行中型卷积神经网络,还可以让这个时尚的 GUI 和 WiFi 连接在同一时间同时运行数天和数月!

2022 年 3 月 29 日更新我尽我所能定期更新我的文章,并根据您在 YouTube/Hackster 评论部分的反馈。如果您想表达对这些努力的支持和赞赏,请考虑给我买杯咖啡(或披萨):)

 

这是最终结果,您可以看到屏幕上显示当前温度、湿度和大气压力值,以及城市名称、预测天气类型和预测降水机会——屏幕底部有一个日志输出字段,您可以轻松地将其重新用于显示极端天气信息、人工智能笑话或来自我的推文。 虽然它看起来不错且有用,但您可以自己添加很多东西 - 例如上面提到的屏幕上的新闻/推文输出或使用深度睡眠模式来节省能源并使其由电池供电等等。

这个项目扩展了我的同事Jonathan Tan的一篇关于天气预报的文章。最值得注意的是,我们将从该文章中的基本实现中改进一些内容:

  • 我们将使用 BME280 传感器,它可以用来获取大气压力信息,以及温度和湿度。
  • 原始项目中的神经网络模型经过训练,可以根据前 3 小时的数据点预测下半小时的天气,每半小时测量一次。所以,它更像是一个天气描述符,而不是一个真正的天气预报。我们将利用更先进的数据处理和模型架构,根据之前 24 小时的测量结果预测未来 24 小时的天气类型和降水机会。
  • 我们还将利用模型优化,这将使我们能够获得更小的模型并在 Wio Terminal 内存中容纳更多的东西,例如,Web 服务器和带有暗/亮材质主题的漂亮 LVGL 界面。

数据处理和模型训练

那么,我们从哪里开始呢?当然,这一切都始于数据。在本教程中,我们将使用来自 Kaggle 的现成天气数据集,历史每小时天气数据 2012-2017。 我住在深圳,中国南方的一个城市——数据集中没有那个城市,所以我选择了一个纬度相近,也属于亚热带气候的城市——迈阿密。

poYBAGNYkeWAIL8QAADJp3rOda8542.png
 

你需要选择一个至少与你居住的气候相似的城市——不用说,这个模型在迈阿密的数据上进行了训练,然后在冬天部署到芝加哥,这将是 Confused Beyond All Reason。

pYYBAGNYkeiAXzcKAAFhWO_wMco034.png
新验证码的好主意
 

对于数据处理和模型训练步骤,让我们打开我为这个项目在 Github 存储库中准备和共享的 Colab Notebook 。

准备环境

获得训练好的模型后,就可以将其部署到 Wio Terminal。

 

如果你在 Windows 上制作这个项目,你需要做的第一件事是下载 Arduino IDE 的夜间版本,因为当前稳定版本 1.18.3 不会编译具有大量库依赖项的草图(问题是链接器命令编译期间超过 Windows 上的最大长度)。

编辑 2021 年 10 月:如果您为 Wio 终端使用 1.8.2 板定义,则无需替换 cmsis_gcc.h。这也是 TC3 定时器库正常运行所必需的。

其次,您需要将 cmsis_gcc.h 文件的内容替换为较新的版本,以避免`__SXTB16_RORn`未定义。您将在此项目的 Github 存储库中找到该文件的较新版本。然后只需将其复制到C:\Users\[your_user_name]\AppData\Local\Arduino15\packages\Seeeduino\tools\CMSIS\5.4.0\CMSIS\Core\IncludeWindows 和/home/[your_user_name]/.arduino15/packages/Seeeduino/tools/CMSIS/5.4.0/CMSIS/Core/IncludeLinux 上。

最后,由于我们使用卷积神经网络并使用 Keras API 构建它,它包含当前稳定版本的 Tensorflow Micro 不支持的操作。浏览 Github 上的 Tensorflow 问题,我发现有一个拉取请求将此操作(EXPAND_DIMS)添加到可用操作列表中,但在撰写本文时它没有合并到 master 中。因此,您需要做的(2021 年 10 月编辑)是执行

git clone https://github.com/tensorflow/tflite-micro-arduino-examples Arduino_TensorFlowLite

在您的 Arduino 草图/库文件夹中。您可以在TensorFlow Lite Micro Library for Arduino 存储库中找到有关安装最新开发版本库的更多详细信息

使用虚拟数据进行测试

完成后,创建一个空草图并保存。然后将您训练的模型复制到草图文件夹并重新打开草图。将模型和模型长度的变量名称更改为更短的名称。然后使用 wio_terminal_tfmicro_weather_prediction_static.ino 中的代码进行测试:

让我们回顾一下我们在 C++ 代码中的主要步骤

我们包含了 Tensorflow 库的头文件和带有模型 flatbuffer 的文件

#include 
//#include "tensorflow/lite/micro/micro_mutable_op_resolver.h"
#include "tensorflow/lite/micro/all_ops_resolver.h"
#include "tensorflow/lite/micro/micro_error_reporter.h"
#include "tensorflow/lite/micro/system_setup.h"
#include "tensorflow/lite/micro/micro_interpreter.h"
#include "tensorflow/lite/schema/schema_generated.h"

#include "model_Conv1D.h"

请注意我如何注释掉 micro_mutable_op_resolver.h 并启用 all_ops_resolver.h – all_ops_resolver.h 标头编译了 Tensorflow Micro 中当前存在的所有操作并且便于测试,但是一旦完成测试,最好切换到 micro_mutable_op_resolver.h 以保存设备内存——它确实有很大的不同。

接下来我们定义错误报告器、模型、输入和输出张量和解释器的指针。注意我们的模型有两个输出——一个是降水量,另一个是天气类型。我们还定义了 tensor arena,您可以将其视为一个草稿板,用于保存输入、输出和中间数组——所需的大小取决于您使用的模型,并且可能需要通过实验来确定。

// Globals, used for compatibility with Arduino-style sketches.
namespace {
tflite::ErrorReporter* error_reporter = nullptr;
const tflite::Model* model = nullptr;
tflite::MicroInterpreter* interpreter = nullptr;
TfLiteTensor* input = nullptr;
TfLiteTensor* output_type = nullptr;
TfLiteTensor* output_precip = nullptr;

constexpr int kTensorArenaSize = 1024*25;
uint8_t tensor_arena[kTensorArenaSize];
}  // namespace

然后在 setup 函数中,有更多样板文件,例如实例化错误报告器、操作解析器、解释器、映射模型、分配张量以及最后检查分配后的张量形状。如果当前版本的 Tensorflow Micro 库不支持某些模型操作,则代码可能会在运行时抛出错误。如果您有不受支持的操作,您可以更改模型架构或自己添加对操作员的支持,通常是从 Tensorflow Lite 移植它。

void setup() {
  Serial.begin(115200);
  while (!Serial) {delay(10);}
  
  // Set up logging. Google style is to avoid globals or statics because of
  // lifetime uncertainty, but since this has a trivial destructor it's okay.
  // NOLINTNEXTLINE(runtime-global-variables)
  static tflite::MicroErrorReporter micro_error_reporter;
  error_reporter = µ_error_reporter;

  // Map the model into a usable data structure. This doesn't involve any
  // copying or parsing, it's a very lightweight operation.
  model = tflite::GetModel(Conv1D_tflite);
  if (model->version() != TFLITE_SCHEMA_VERSION) {
    TF_LITE_REPORT_ERROR(error_reporter,
                         "Model provided is schema version %d not equal "
                         "to supported version %d.",
                         model->version(), TFLITE_SCHEMA_VERSION);
    return;
  }

  // This pulls in all the operation implementations we need.
  // NOLINTNEXTLINE(runtime-global-variables)
  //static tflite::MicroMutableOpResolver<1> resolver;
  static tflite::AllOpsResolver resolver;
  // Build an interpreter to run the model with.
  static tflite::MicroInterpreter static_interpreter(model, resolver, tensor_arena, kTensorArenaSize, error_reporter);
  interpreter = &static_interpreter;

  // Allocate memory from the tensor_arena for the model's tensors.
  TfLiteStatus allocate_status = interpreter->AllocateTensors();
  if (allocate_status != kTfLiteOk) {
    TF_LITE_REPORT_ERROR(error_reporter, "AllocateTensors() failed");
    return;
  }

  // Obtain pointers to the model's input and output tensors.
  input = interpreter->input(0);
  output_type = interpreter->output(1);
  output_precip = interpreter->output(0);
  
  Serial.println(input->dims->size);
  Serial.println(input->dims->data[1]);
  Serial.println(input->dims->data[2]);
  Serial.println(input->type);

  Serial.println(output_type->dims->size);
  Serial.println(output_type->dims->data[1]);
  Serial.println(output_type->type);

  Serial.println(output_precip->dims->size);
  Serial.println(output_precip->dims->data[1]);
  Serial.println(output_precip->type);
}

最后,在循环函数中,我们为量化的 INT8 值和浮点值数组定义了一个占位符,您可以从 Colab 笔记本复制粘贴,以比较设备上的模型推理与 Colab 中的模型推理。

void loop() {

  int8_t x_quantized[72];
  float x[72] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
       0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
       0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
       0, 0, 0, 0, 0, 0};

我们在 for 循环中将浮点值量化为 INT8,并将它们一一放入输入张量中:

for (byte i = 0; i < 72; i = i + 1) {
        input->data.int8[i] = x[i] / input->params.scale + input->params.zero_point;
  }

然后由 Tensorflow Micro 解释器执行推理,如果没有报告错误,则将值放置在输出张量中。

// Run inference, and report any error
  TfLiteStatus invoke_status = interpreter->Invoke();
  
  if (invoke_status != kTfLiteOk) {
    TF_LITE_REPORT_ERROR(error_reporter, "Invoke failed");
    return;
  }

与输入类似,模型的输出也是量化的,所以我们需要进行逆运算,将其从INT8转换为float。

// Obtain the quantized output from model's output tensor
  float y_type[4];

  // Dequantize the output from integer to floating-point
  int8_t y_precip_q = output_precip->data.int8[0];
  Serial.println(y_precip_q);
  float y_precip = (y_precip_q - output_precip->params.zero_point) * output_precip->params.scale;  
  Serial.print("Precip: ");
  Serial.print(y_precip);
  Serial.print("\t");
  Serial.print("Type: ");
  for (byte i = 0; i < 4; i = i + 1) {
    y_type[i] = (output_type->data.int8[i] - output_type->params.zero_point) * output_type->params.scale;
    Serial.print(y_type[i]);
    Serial.print(" ");
  }
  Serial.print("\n");
}

检查并比较同一数据点的值,对于 Colab 笔记本中的量化 Tensorflow Lite 模型和在 Wio 终端上运行的 Tensorflow Micro 模型,它们应该相同。

 

探索并尝试完整版的草图

凉爽的!所以它确实有效,现在下一步是将其从演示变成实际有用的项目。从 Seeed Arduino 速写本存储库打开速写并查看其内容。

我将代码分为主草图、get_historical_data 和 GUI 部分。由于我们的模型需要过去 24 小时的数据,我们需要等待 24 小时才能执行第一次推理,这需要很多时间——为了解决这个问题,我们从 openweathermap.com API 获取过去 24 小时的天气,并且可以执行第一次推理设备启动后立即推断,然后用连接到 Wio 终端 I2C Grove 插座的 BME280 传感器的温度、湿度和压力替换循环缓冲区中的值。对于 GUI,我使用了 LVGL,一个小巧而多功能的图形库——它也是一个快速发展的项目,使用它并不容易,但它的功能非常值得!

poYBAGNYkeqAXKF7AADNTCxz30Q419.png
 

按照 Github 存储库中的说明安装必要的库并配置 LVGL 以运行演示。

直到下一次!


声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

评论(0)
发评论

下载排行榜

全部0条评论

快来发表一下你的评论吧 !

'+ '

'+ '

'+ ''+ '
'+ ''+ ''+ '
'+ ''+ '' ); $.get('/article/vipdownload/aid/'+webid,function(data){ if(data.code ==5){ $(pop_this).attr('href',"/login/index.html"); return false } if(data.code == 2){ //跳转到VIP升级页面 window.location.href="//m.obk20.com/vip/index?aid=" + webid return false } //是会员 if (data.code > 0) { $('body').append(htmlSetNormalDownload); var getWidth=$("#poplayer").width(); $("#poplayer").css("margin-left","-"+getWidth/2+"px"); $('#tips').html(data.msg) $('.download_confirm').click(function(){ $('#dialog').remove(); }) } else { var down_url = $('#vipdownload').attr('data-url'); isBindAnalysisForm(pop_this, down_url, 1) } }); }); //是否开通VIP $.get('/article/vipdownload/aid/'+webid,function(data){ if(data.code == 2 || data.code ==5){ //跳转到VIP升级页面 $('#vipdownload>span').text("开通VIP 免费下载") return false }else{ // 待续费 if(data.code == 3) { vipExpiredInfo.ifVipExpired = true vipExpiredInfo.vipExpiredDate = data.data.endoftime } $('#vipdownload .icon-vip-tips').remove() $('#vipdownload>span').text("VIP免积分下载") } }); }).on("click",".download_cancel",function(){ $('#dialog').remove(); }) var setWeixinShare={};//定义默认的微信分享信息,页面如果要自定义分享,直接更改此变量即可 if(window.navigator.userAgent.toLowerCase().match(/MicroMessenger/i) == 'micromessenger'){ var d={ title:'使用Wio Terminal和Tensorflow Lite创建智能气象站',//标题 desc:$('[name=description]').attr("content"), //描述 imgUrl:'https://'+location.host+'/static/images/ele-logo.png',// 分享图标,默认是logo link:'',//链接 type:'',// 分享类型,music、video或link,不填默认为link dataUrl:'',//如果type是music或video,则要提供数据链接,默认为空 success:'', // 用户确认分享后执行的回调函数 cancel:''// 用户取消分享后执行的回调函数 } setWeixinShare=$.extend(d,setWeixinShare); $.ajax({ url:"//www.obk20.com/app/wechat/index.php?s=Home/ShareConfig/index", data:"share_url="+encodeURIComponent(location.href)+"&format=jsonp&domain=m", type:'get', dataType:'jsonp', success:function(res){ if(res.status!="successed"){ return false; } $.getScript('https://res.wx.qq.com/open/js/jweixin-1.0.0.js',function(result,status){ if(status!="success"){ return false; } var getWxCfg=res.data; wx.config({ //debug: true, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。 appId:getWxCfg.appId, // 必填,公众号的唯一标识 timestamp:getWxCfg.timestamp, // 必填,生成签名的时间戳 nonceStr:getWxCfg.nonceStr, // 必填,生成签名的随机串 signature:getWxCfg.signature,// 必填,签名,见附录1 jsApiList:['onMenuShareTimeline','onMenuShareAppMessage','onMenuShareQQ','onMenuShareWeibo','onMenuShareQZone'] // 必填,需要使用的JS接口列表,所有JS接口列表见附录2 }); wx.ready(function(){ //获取“分享到朋友圈”按钮点击状态及自定义分享内容接口 wx.onMenuShareTimeline({ title: setWeixinShare.title, // 分享标题 link: setWeixinShare.link, // 分享链接 imgUrl: setWeixinShare.imgUrl, // 分享图标 success: function () { setWeixinShare.success; // 用户确认分享后执行的回调函数 }, cancel: function () { setWeixinShare.cancel; // 用户取消分享后执行的回调函数 } }); //获取“分享给朋友”按钮点击状态及自定义分享内容接口 wx.onMenuShareAppMessage({ title: setWeixinShare.title, // 分享标题 desc: setWeixinShare.desc, // 分享描述 link: setWeixinShare.link, // 分享链接 imgUrl: setWeixinShare.imgUrl, // 分享图标 type: setWeixinShare.type, // 分享类型,music、video或link,不填默认为link dataUrl: setWeixinShare.dataUrl, // 如果type是music或video,则要提供数据链接,默认为空 success: function () { setWeixinShare.success; // 用户确认分享后执行的回调函数 }, cancel: function () { setWeixinShare.cancel; // 用户取消分享后执行的回调函数 } }); //获取“分享到QQ”按钮点击状态及自定义分享内容接口 wx.onMenuShareQQ({ title: setWeixinShare.title, // 分享标题 desc: setWeixinShare.desc, // 分享描述 link: setWeixinShare.link, // 分享链接 imgUrl: setWeixinShare.imgUrl, // 分享图标 success: function () { setWeixinShare.success; // 用户确认分享后执行的回调函数 }, cancel: function () { setWeixinShare.cancel; // 用户取消分享后执行的回调函数 } }); //获取“分享到腾讯微博”按钮点击状态及自定义分享内容接口 wx.onMenuShareWeibo({ title: setWeixinShare.title, // 分享标题 desc: setWeixinShare.desc, // 分享描述 link: setWeixinShare.link, // 分享链接 imgUrl: setWeixinShare.imgUrl, // 分享图标 success: function () { setWeixinShare.success; // 用户确认分享后执行的回调函数 }, cancel: function () { setWeixinShare.cancel; // 用户取消分享后执行的回调函数 } }); //获取“分享到QQ空间”按钮点击状态及自定义分享内容接口 wx.onMenuShareQZone({ title: setWeixinShare.title, // 分享标题 desc: setWeixinShare.desc, // 分享描述 link: setWeixinShare.link, // 分享链接 imgUrl: setWeixinShare.imgUrl, // 分享图标 success: function () { setWeixinShare.success; // 用户确认分享后执行的回调函数 }, cancel: function () { setWeixinShare.cancel; // 用户取消分享后执行的回调函数 } }); }); }); } }); } function openX_ad(posterid, htmlid, width, height) { if ($(htmlid).length > 0) { var randomnumber = Math.random(); var now_url = encodeURIComponent(window.location.href); var ga = document.createElement('iframe'); ga.src = 'https://www1.elecfans.com/www/delivery/myafr.php?target=_blank&cb=' + randomnumber + '&zoneid=' + posterid+'&prefer='+now_url; ga.width = width; ga.height = height; ga.frameBorder = 0; ga.scrolling = 'no'; var s = $(htmlid).append(ga); } } openX_ad(828, '#berry-300', 300, 250);