当前位置:首页 > 技术 > 正文内容

基于 NumPy 实现 LSTM 图像分类:将 PyTorch 模型迁移至嵌入式环境

访客 技术 2026年11月7日 3

概述

在资源受限的嵌入式设备(如树莓派)上运行深度学习模型时,直接部署重量级的框架(如 PyTorch 或 TensorFlow)往往会面临内存不足或安装困难的问题。通过将模型参数导出为通用的 NumPy 数组,并在推理端手动实现网络逻辑,可以实现轻量化的模型部署。本文将介绍如何使用 NumPy 实现一个双层 LSTM 网络,加载预训练的 PyTorch 模型参数,完成对 MNIST 手写数字集的识别。

第一阶段:模型训练与参数导出

首先在 PC 端使用 PyTorch 定义并训练一个双层 LSTM 网络。训练完成后,需要提取 state_dict 中的权重信息,并将其保存为 .npz 格式以便后续读取。

import torch
import torch.nn as nn
import torchvision
import numpy as np

# 定义网络结构
class DigitsLSTM(nn.Module):
    def __init__(self, input_dim, hidden_dim, layer_num, class_num):
        super(DigitsLSTM, self).__init__()
        self.hidden_dim = hidden_dim
        self.layer_num = layer_num
        self.lstm_core = nn.LSTM(input_dim, hidden_dim, layer_num, batch_first=True)
        self.classifier = nn.Linear(hidden_dim, class_num)

    def forward(self, x):
        h_init = torch.zeros(self.layer_num, x.size(0), self.hidden_dim).to(x.device)
        c_init = torch.zeros(self.layer_num, x.size(0), self.hidden_dim).to(x.device)
        out, _ = self.lstm_core(x, (h_init, c_init))
        return self.classifier(out[:, -1, :])

# 配置超参数
IMG_SIZE = 28
HIDDEN_SIZE = 128
LAYERS = 2
CLASSES = 10
LR = 0.001
EPOCHS = 1

# 数据准备
transform = torchvision.transforms.ToTensor()
train_data = torchvision.datasets.MNIST(root='./data', train=True, transform=transform, download=True)
loader = torch.utils.data.DataLoader(dataset=train_data, batch_size=64, shuffle=True)

model = DigitsLSTM(IMG_SIZE, HIDDEN_SIZE, LAYERS, CLASSES)
loss_fn = nn.CrossEntropyLoss()
opt = torch.optim.Adam(model.parameters(), lr=LR)

# 简易训练流程
for epoch in range(EPOCHS):
    for step, (imgs, labels) in enumerate(loader):
        imgs = imgs.view(-1, IMG_SIZE, IMG_SIZE)
        preds = model(imgs)
        loss = loss_fn(preds, labels)
        
        opt.zero_grad()
        loss.backward()
        opt.step()
        
        if step % 100 == 0:
            print(f"Epoch {epoch}, Step {step}, Loss: {loss.item():.4f}")

# 导出参数为 NumPy 格式
params_dict = {}
for key, val in model.named_parameters():
    params_dict[key] = val.detach().cpu().numpy()
np.savez('lstm_weights.npz', **params_dict)

在导出数据前,建议准备一些测试图片。图片可以按"索引_标签.png"的方式命名,方便在嵌入式端验证识别准确率。

数据文件准备示例

第二阶段:NumPy 推理端实现

在树莓派上,由于无法安装完整的深度学习框架,我们需要使用 NumPy 手动构建 LSTM 的前向计算。LSTM 的核心在于对输入门、遗忘门、细胞状态和输出门的精确模拟。需要注意的是,PyTorch 的 LSTM 权重矩阵是合并存储的,格式为 $(W_{ii}|W_{if}|W_{ig}|W_{io})$,在计算时需要对其进行切片处理。

import numpy as np
import os
from PIL import Image

def sigmoid(x):
    return 1 / (1 + np.exp(-x))

class NumpyLSTMInference:
    def __init__(self, weight_path):
        data = np.load(weight_path)
        # 获取第一层权重
        self.w_ih_l0 = data['lstm_core.weight_ih_l0']
        self.w_hh_l0 = data['lstm_core.weight_hh_l0']
        self.b_ih_l0 = data['lstm_core.bias_ih_l0']
        self.b_hh_l0 = data['lstm_core.bias_hh_l0']
        # 获取第二层权重
        self.w_ih_l1 = data['lstm_core.weight_ih_l1']
        self.w_hh_l1 = data['lstm_core.weight_hh_l1']
        self.b_ih_l1 = data['lstm_core.bias_ih_l1']
        self.b_hh_l1 = data['lstm_core.bias_hh_l1']
        # 全连接层
        self.fc_w = data['classifier.weight']
        self.fc_b = data['classifier.bias']
        self.h_dim = 128

    def _compute_cell(self, x, h_prev, c_prev, w_ih, w_hh, b_ih, b_hh):
        # 拆分四个门的权重
        gates = np.dot(x, w_ih.T) + b_ih + np.dot(h_prev, w_hh.T) + b_hh
        
        i = sigmoid(gates[:self.h_dim])
        f = sigmoid(gates[self.h_dim : 2*self.h_dim])
        g = np.tanh(gates[2*self.h_dim : 3*self.h_dim])
        o = sigmoid(gates[3*self.h_dim:])
        
        c_next = f * c_prev + i * g
        h_next = o * np.tanh(c_next)
        return h_next, c_next

    def predict(self, input_seq):
        # input_seq: (seq_len, feature_dim)
        seq_len, _ = input_seq.shape
        
        # 第一层 LSTM 遍历时间步
        h_l0, c_l0 = np.zeros(self.h_dim), np.zeros(self.h_dim)
        layer0_outputs = []
        for t in range(seq_len):
            h_l0, c_l0 = self._compute_cell(input_seq[t], h_l0, c_l0, 
                                           self.w_ih_l0, self.w_hh_l0, 
                                           self.b_ih_l0, self.b_hh_l0)
            layer0_outputs.append(h_l0)
            
        # 第二层 LSTM 接收第一层的序列输出
        h_l1, c_l1 = np.zeros(self.h_dim), np.zeros(self.h_dim)
        for t in range(seq_len):
            h_l1, c_l1 = self._compute_cell(layer0_outputs[t], h_l1, c_l1, 
                                           self.w_ih_l1, self.w_hh_l1, 
                                           self.b_ih_l1, self.b_hh_l1)
        
        # 全连接输出
        logits = np.dot(h_l1, self.fc_w.T) + self.fc_b
        return np.argmax(logits)

# 执行推理
engine = NumpyLSTMInference('lstm_weights.npz')
img_dir = './mnist_pi'

for fname in os.listdir(img_dir):
    if fname.endswith(('.png', '.jpg')):
        path = os.path.join(img_dir, fname)
        raw_img = Image.open(path)
        # 归一化并转换为 numpy
        img_arr = np.array(raw_img) / 255.0
        res = engine.predict(img_arr)
        print(f"File: {fname}, Predicted: {res}")

性能与结果分析

手动实现的 LSTM 推理在 CPU 上运行良好,能够正确识别出手写数字。虽然由于使用了 Python 循环遍历时间步,推理速度相对于优化的算子库(如 MKL 或 OpenBLAS)较慢,但在非实时场景下完全可以接受。通过这种方法,我们成功将 PyTorch 的强大训练能力与 NumPy 的轻量级部署方案相结合,为嵌入式 AI 应用提供了一种可行思路。

推理识别结果

相关文章

Linux crontab 详解

1) crontab 是什么cron 是 Linux 的定时任务守护进程;crontab 是用来编辑/查看“按时间周期执行命令”的表(cron table)。常见两类:用户 crontab:每个用户一份(crontab -e 编辑)系统级 crontab / cron.d:可指定执行用户(/etc/crontab、/etc/cron.d/*)2) crontab 时间...

富文本里可以允许的 HTML 属性

一、所有标签默认允许的安全属性(极少)class        (可选)id           (通常建议禁用)title️ 注意:id 容易被滥用做锚点注入,很多系统直接禁用class 允许的话最好只允许固定前缀(如 editor-*)二、a 标签允许属性<a href="" t...

Mac 安装 Node.js 指南

方法一:通过官网安装包(最简单,适合初学者)如果你只是想快速安装并开始使用,这是最直接的方法。访问 Node.js 官网。页面会显示两个版本:LTS (Recommended For Most Users):长期支持版,最稳定。建议选这个。Current:最新特性版,包含最新功能但可能不够稳定。下载 .pkg 安装包并运行。按照安装向导点击“下一步”即可完成。方法二:使用 Homebrew 安装(...

Dom\HTML_NO_DEFAULT_NS 的副作用:自动加闭合标签

在使用Dom\HTMLDocument时,Dom\HTML_NO_DEFAULT_NS 将禁止在解析过程中设置元素的命名空间, 此设置是为了与DOMDocument向后兼容而存在的。当使用它时,已知的一个副作用就是:自动加闭合标签例如 </img> 为什么会这样?当你使用:Dom\HTML_NO_DEFAULT_NS文档会变成 无命名空间模式,此时内部更接近 XML...

Laravel 事件和监听器创建

在 Laravel 中,使用 Artisan 命令创建 Events(事件) 和 Listeners(监听器) 是非常高效的。你可以通过以下几种方式来实现:1. 手动创建单个 Event如果你只想创建一个事件类,可以使用 make:event 命令:Bashphp artisan make:event UserRegistered执行后,文件将生成在 app/Even...

自定义域名解析神器 dnsmasq

什么是 dnsmasq?dnsmasq 是一个轻量级、功能强大的网络服务工具,专为小型和中等规模网络设计。它是一个综合的网络基础设施解决方案[1]。dnsmasq 能做什么?功能说明应用场景DNS 转发与缓存将 DNS 查询转发到上游服务器(ISP、Google DNS 等),并在本地缓存结果加快 DNS 查询速度,减少外部 DNS 流量本地 DNS解析本地网络设备的主机名,无需编辑&n...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。