深度学习模型部署与特征工程全解析

1. 2D CNN 的部署

要运行 2D CNN,需要 2D 输入,如图片、随时间变化的传感器值或多点传感器读数。不过,TensorFlow 运行时期望的是 1D 输入数据,所以必须先将输入展平,以数组而非矩阵的形式提供。以下是常见数据类型的展平技术示例:
- 图像 :灰度图像的预期输入格式是按行优先(第一行像素、第二行像素等)。对于 RGB 图像,格式是 R1, G1, B1, R2, G2, B2 等。灰度情况适用于所有其他类似图像的输入。
- 随时间变化的传感器数据 :预期输入格式是按时间优先(SensorAt=1, SensorBt=1, SensorAt=2, SensorBt=2)。

1.1 在 Arduino 上运行 CNN2D 的示例代码

#include "./PetsCNN2D.h"
// ARENA 是为模型预留的内存量
// 较大的模型需要更多内存,但没有计算最优值的公式
// 这是一个反复试验的过程
#define ARENA 20000
tinyml4all::CNN2D<ARENA> cnn;
uint8_t dog[48*48] = {...};
uint8_t cat[48*48] = {...};

void setup() {
    Serial.begin(115200);
    while (!Serial);
    Serial.println("TensorFlow 2D CNN demo");
    cnn.begin();
}

void loop() {
    // 对狗图像进行分类
    if (!cnn.predict(dog)) {
        Serial.println(nn.error());
        return;
    }
    // .label 保存预测类别的名称
    // .output 保存数值输出(用于回归)或类别 ID(用于分类)
    // .outputs 是包含所有输出的数组(分类时每个类别一个)
    // .runtime_ms 保存预测的持续时间
    // outputsAsString 返回每个类别的得分,范围从 0 到 1(仅适用于分类)
    Serial.print("Expected dog, predicted ");
    Serial.print(cnn.label);
    Serial.print(" with confidence ");
    Serial.println(cnn.confidence);
    Serial.print(" > Scores: ");
    Serial.println(cnn.outputsAsString());

    // 对猫图像进行分类
    if (!cnn.predict(cat)) {
        Serial.println(cnn.error());
        return;
    }
    Serial.print("Expected cat, predicted ");
    Serial.print(cnn.label);
    Serial.print(" with confidence ");
    Serial.println(cnn.confidence);
    Serial.print(" > Scores: ");
    Serial.println(nn.outputsAsString());
    delay(1000);
}

1.2 猫狗分类的混淆矩阵

True vs Predicted dog cat
dog 8 3
cat 4 5

从这个混淆矩阵中,我们可以分析模型的分类性能。例如,有 8 次正确将狗预测为狗,但有 3 次将狗错误预测为猫;有 5 次正确将猫预测为猫,但有 4 次将猫错误预测为狗。

2. 特征工程操作符

特征工程对于提高机器学习模型的性能至关重要。以下介绍几种特征工程操作符。

2.1 特征缩放

特征缩放用于改变输入数据的范围,可以通过线性变换(如最小 - 最大、z - 分数、鲁棒操作符)、非线性变换(如 Box - Cox 和 Yeo - Johnson)或基于实例的变换(如范数)。

2.1.1 Z - 分数归一化

也称为标准化,z - 分数归一化是一种特征缩放方法,通过以下公式将每列重新缩放为均值为 0、方差为 1。
[z = \frac{x - \text{mean}(x)}{\text{std}(x)}]

需要注意的是,z - 分数归一化对异常值敏感。以下是应用该归一化到表格数据集的示例代码:

standard = Scale(method="zscore")
table2 = standard(table)
print(table2.describe())

归一化后的表格统计信息如下:
| | r | g | b |
| — | — | — | — |
| count | 150 | 150 | 150 |
| mean | 0 | 0 | 0 |
| std | 1.0034 | 1.0034 | 1.0034 |
| min | -1.7455 | -2.1319 | -2.3148 |
| 25% | -0.926 | -0.6539 | -0.4955 |
| 50% | -0.0246 | -0.1612 | 0.0243 |
| 75% | 0.7539 | 0.5554 | 0.5441 |
| max | 2.5978 | 7.0049 | 7.5613 |

2.1.2 鲁棒归一化

当数据包含异常值时,鲁棒归一化就派上用场了。它将均值替换为中位数,标准差替换为四分位距(IQR),公式如下:
[x’ = \frac{x - \text{median}(x)}{\text{IQR}}]

以下是应用鲁棒缩放的示例代码:

robust = Scale(method="robust")
table2 = robust(table)
print(table2.describe())

鲁棒归一化后的表格统计信息如下:
| | r | g | b |
| — | — | — | — |
| count | 150 | 150 | 150 |
| mean | 0.0146 | 0.1333 | -0.0233 |
| std | 0.5972 | 0.8297 | 0.9651 |
| min | -1.0244 | -1.6296 | -2.25 |
| 25% | -0.5366 | -0.4074 | -0.5 |
| 50% | 0 | 0 | 0 |
| 75% | 0.4634 | 0.5926 | 0.5 |
| max | 1.561 | 5.9259 | 7.25 |

2.1.3 单位范数

这是一种基于实例的特征缩放策略,不计算全局统计信息。它通过计算样本的范数并将每个分量除以该范数来实现。有不同阶的范数可供选择,如 L1 范数、L2 范数和 L - max 范数。

  • L1 范数(曼哈顿范数) :计算为向量分量绝对值的总和。
    [||x|| 1 = \sum {i = 1}^{N} |x_i|]
  • L2 范数(欧几里得范数) :定义为向量每个分量平方和的平方根。
    [||x|| 2 = \sqrt{\sum {i = 1}^{N} x_i^2}]
  • L - max 范数 :是向量分量绝对值的最大值。
    [||x||_{\max} = \max(|x_i|)]

以下是应用不同范数的示例代码:

# L1 范数
L1 = Scale(method="L1")
table2 = L1(table)
print(table2.describe())

# L2 范数
L2 = Scale(method="L2")
table2 = L2(table)
print(table2.describe())

# L - max 范数
Lmax = Scale(method="Lmax")
table2 = Lmax(table)
print(table2.describe())

2.2 幂变换

2.2.1 Box - Cox 幂变换

Box - Cox 变换属于幂变换家族,通过非线性映射来稳定方差并使数据更接近正态分布。公式如下:
[y^{(\lambda)} =
\begin{cases}
\frac{y^{\lambda} - 1}{\lambda}, & \lambda \neq 0 \
\ln(y), & \lambda = 0
\end{cases}
]

需要注意的是,Box - Cox 变换仅适用于严格正的数据。以下是应用该变换的示例代码:

from tinyml4all.tabular.features import BoxCox
boxcox = BoxCox()
table2 = boxcox(table)

变换后的表格统计信息如下:
| | r | g | b |
| — | — | — | — |
| count | 150 | 150 | 150 |
| mean | 6.4087 | 2.2936 | 2.0929 |
| std | 1.1713 | 0.1512 | 0.1115 |
| min | 3.83 | 1.7369 | 1.6699 |
| 25% | 5.3378 | 2.1957 | 2.0421 |
| 50% | 6.5263 | 2.2897 | 2.1103 |
| 75% | 7.3512 | 2.4006 | 2.1693 |
| max | 8.9006 | 2.8958 | 2.5908 |

2.2.2 Yeo - Johnson 幂变换

Yeo - Johnson 变换与 Box - Cox 变换属于同一家族,但它适用于任何数据,包括负数。以下是应用该变换的示例代码:

from tinyml4all.tabular.features import YeoJohnson
yeojohnson = YeoJohnson()
table2 = yeojohnson(table)

变换后的表格统计信息如下:
| | r | g | b |
| — | — | — | — |
| count | 150 | 150 | 150 |
| mean | 6.276 | 2.1292 | 1.9302 |
| std | 1.0779 | 0.1168 | 0.0831 |
| min | 3.9118 | 1.7014 | 1.6159 |
| 25% | 5.2894 | 2.0534 | 1.8922 |
| 50% | 6.3834 | 2.1262 | 1.9432 |
| 75% | 7.1437 | 2.2122 | 1.9873 |
| max | 8.5706 | 2.5908 | 2.2987 |

2.3 离散化

特征离散化是将连续变量转换为离散变量的过程,许多机器学习算法可以从中受益。

2.3.1 二值化

有时,数值包含的信息过多,只需要一个二进制特征。例如,在气象站中,对于降雨量,可能只需要知道是否下雨,而不需要具体的降雨量。这一过程称为二值化,需要定义一个阈值,低于阈值的值转换为 False,高于阈值的值转换为 True。

以下是仅对“rain”列进行二值化的示例代码:

from tinyml4all.tabular.features import Discretize
bin_rain = Discretize(column="rain", threshold=0.1)
table2 = bin_rain(table)

二值化后的表格如下:
| temperature | humidity | rain |
| — | — | — |
| 22.1 | 40 | False |
| 23.3 | 41 | False |
| 9.6 | 60 | True |
| 9.7 | 61 | True |
| 9.5 | 65 | True |

如果要对多个列进行二值化,并且每个列有不同的阈值,可以链式调用。以下是示例代码:

from tinyml4all.tabular.features import Discretize
from tinyml4all.tabular.classification import Chain

# 1. 对温度高于 20° 进行二值化并创建 "hot" 列
# 2. 对湿度高于 80% 进行二值化并创建 "wet" 列
# 3. 对湿度低于 20% 进行二值化并创建 "dry" 列
# 4. 对降雨量高于 0.1 进行二值化
binarize_many = Chain(
    Discretize("temperature", threshold=20, append="hot"),
    Discretize("humidity", threshold=80, append="wet"),
    Discretize("humidity", threshold=20, append="dry", flip=True),
    Discretize(column="rain", threshold=0.1)
)
table2 = binarize_many(table)
2.3.2 分箱

分箱(也称为桶化)是将连续数据拟合到有限范围的可能值(“箱”或“桶”)的过程。这可以平滑数据的小波动,使模型更不易过拟合。例如,在气象站中,对于湿度,可能不需要精确到 1 的分辨率,以 10 为增量的粗略值可能就足够了。以下是对湿度列进行分箱的示例代码:

bin = Discretize("humidity", bins=10, append="humidity_bin")
table2 = bin(table)

分箱后的表格如下:
| humidity | humidity_bin |
| — | — |
| 42 | 4 |
| 41 | 4 |
| 60 | 6 |
| 61 | 6 |
| 62 | 6 |

2.3.3 独热编码

独热编码将分类列(只能取有限值的列,如星期几)转换为二进制值列表,其中只有第 i 个位置的值为 1(i 表示该值在可用值列表中的位置)。这有助于那些为每个特征分配重要性得分的模型。需要注意的是,独热编码仅适用于分类、离散数据,不能对实值、连续列进行编码。以下是应用独热编码的示例代码:

from tinyml4all.tabular.features import OneHot
onehot = OneHot(column="fruit")
table2 = onehot(fruit)

独热编码后的表格如下:
| fruit | fruit=orange | fruit=tomato | fruit=zucchini |
| — | — | — | — |
| orange | TRUE | FALSE | FALSE |
| orange | TRUE | FALSE | FALSE |
| tomato | FALSE | TRUE | FALSE |
| tomato | FALSE | TRUE | FALSE |
| zucchini | FALSE | FALSE | TRUE |

下面是特征工程操作的流程图:

graph LR
    A[特征工程] --> B[特征缩放]
    A --> C[幂变换]
    A --> D[离散化]
    B --> B1[Z - 分数归一化]
    B --> B2[鲁棒归一化]
    B --> B3[单位范数]
    C --> C1[Box - Cox 幂变换]
    C --> C2[Yeo - Johnson 幂变换]
    D --> D1[二值化]
    D --> D2[分箱]
    D --> D3[独热编码]

3. 机器学习中的数据类型与处理

3.1 数据类型概述

机器学习中常见的数据类型有以下几种:
- 音频数据 :可用于音频分类、语音识别等任务。例如,在音频唤醒词检测中,通过对音频数据的处理来判断是否检测到特定的唤醒词。
- 图像和空间数据 :适用于图像分类、目标检测等场景。像使用卷积神经网络(CNN)对图像中的物体进行识别。
- 表格数据 :具有固定的结构,列代表特征,行代表样本。常用于分类和回归任务,如根据水果的特征进行分类。
- 时间序列数据 :按时间顺序排列的数据,可用于预测、趋势分析等。例如,根据历史的股票价格数据预测未来的价格走势。

3.2 不同数据类型的处理流程

3.2.1 表格数据分类处理流程
步骤 操作内容
数据采集 可以通过传感器采集数据,如使用颜色传感器采集水果的 RGB 数据。数据采集时要注意数据的质量,确保数据的准确性和完整性。
数据加载与检查 使用 Python 脚本加载数据,可使用 tinyml4all 包进行操作。可以对数据进行可视化检查,如绘制散点图、矩阵对图等,以了解数据的分布情况。
特征工程 包括特征缩放、特征选择等操作。特征缩放可以使用多种方法,如 z - 分数归一化、鲁棒归一化等;特征选择可以采用递归特征消除(RFE)、基于分数的选择等方法。
模型训练与评估 选择合适的分类模型,如决策树、逻辑回归、支持向量机等,并对模型进行训练和评估。可以使用混淆矩阵、准确率、召回率等指标来评估模型的性能。
模型部署 将训练好的模型部署到目标设备上,如 Arduino 板。
3.2.2 时间序列数据分类处理流程
graph LR
    A[数据采集] --> B[特征工程]
    B --> C[模型训练]
    C --> D[模型部署]
    A --> E[数据标注]
    E --> C
  • 数据采集 :可以使用惯性测量单元(IMU)传感器采集数据,如加速度计、磁力计的数据。采集的数据可以是连续的运动数据或间歇性的时间序列数据。
  • 特征工程 :对时间序列数据进行特征提取,如频域特征、时域特征等。可以使用 FIFO 数据结构、窗口逻辑等方法进行特征工程。
  • 模型训练 :选择合适的模型,如 1D CNN、LSTM 等,并对模型进行训练。
  • 模型部署 :将训练好的模型部署到 Arduino 板上,需要注意编译时间、推理串行输出等问题。

4. 机器学习模型类型与特点

4.1 分类模型

4.1.1 常见分类模型
模型名称 特点 优点 缺点
决策树 基于树结构进行决策,易于理解和解释。 可以处理非线性数据,不需要特征缩放。 容易过拟合,尤其是在树的深度较大时。
逻辑回归 使用逻辑函数进行分类,适用于二分类问题。 计算效率高,可解释性强。 对非线性数据的处理能力有限。
支持向量机(SVM) 通过寻找最优的超平面进行分类。 在处理高维数据和非线性数据时表现较好。 计算复杂度较高,对参数的选择比较敏感。
随机森林 由多个决策树组成的集成模型。 具有较高的准确率和稳定性,能够处理高维数据。 模型解释性相对较差,训练时间较长。
极端梯度提升(XGBoost) 一种高效的梯度提升算法。 具有较高的准确率和效率,能够处理大规模数据。 对内存要求较高,容易过拟合。
4.1.2 分类模型的评估指标
  • 准确率 :预测正确的样本数占总样本数的比例。
  • 精确率 :预测为正类的样本中实际为正类的比例。
  • 召回率 :实际为正类的样本中被预测为正类的比例。
  • 混淆矩阵 :用于展示模型在不同类别上的分类情况,包括真正类、假正类、真负类、假负类。

4.2 回归模型

4.2.1 常见回归模型
  • 普通最小二乘法(OLS) :通过最小化误差平方和来拟合数据。适用于线性回归问题。
  • 多项式回归 :通过对特征进行多项式扩展来处理非线性关系。
  • 决策树回归 :基于决策树的结构进行回归预测。
4.2.2 回归模型的评估指标
  • 均绝对误差(MAE) :预测值与真实值之间绝对误差的平均值。
  • 均方根误差(RMSE) :预测值与真实值之间误差平方的平均值的平方根。
  • 决定系数 :衡量回归模型对数据的拟合程度。

5. 模型部署与优化

5.1 模型部署到 Arduino

在将模型部署到 Arduino 时,需要注意以下几点:
- 内存管理 :为模型预留足够的内存空间,如在运行 2D CNN 时,需要定义合适的 ARENA 值。
- 编译时间 :不同的模型和代码可能会导致不同的编译时间,需要进行优化。
- 推理串行输出 :确保模型在 Arduino 上的推理结果能够正确输出,以便进行后续的处理和分析。

5.2 模型优化

  • 特征工程优化 :选择合适的特征工程方法,如特征缩放、特征选择等,以提高模型的性能。
  • 模型选择优化 :根据数据的特点和任务的需求,选择合适的模型。例如,对于图像分类任务,选择 CNN 模型;对于时间序列数据,选择 LSTM 模型。
  • 参数调优 :对模型的参数进行调优,以提高模型的准确率和稳定性。可以使用网格搜索、随机搜索等方法进行参数调优。

总之,机器学习涉及到数据处理、模型选择、特征工程、模型训练和部署等多个环节。通过合理地运用各种技术和方法,可以提高模型的性能,实现更准确的预测和分类任务。在实际应用中,需要根据具体的问题和数据特点,选择合适的方法和模型,并进行不断的优化和改进。

Logo

码道开发者社区,聚焦华为云码道 CodeArts 代码智能体,沉淀 Agent、Skill、鸿蒙开发实战内容,供开发者查阅资料、交流技术、分享工程实践

更多推荐