网络=模型
结合我们学习到的机器学习,无论是构建线性函数来实现分类或回归的线性回归、通过“物以类聚,人以群分”原理的K近邻、通过计算每个节点的分类纯度来实现分类的决策树、通过核函数来提升维度实现分类的支持向量机、以及贝叶斯分类等,都是通过特定的某种方法来实现对我们的输入,实现分类和回归。
所以我们的模型训练出来,也就是实现对输入的分类和回归。
分类:(属于)对我输入的数据进行分类,属于哪一种,例如人类照片区分男性和女性。缺点,当输入数据不为人类照片时,也会输出男性或者女性。这就是分类。
回归:(预测)对于我们输入的随机照片,它会将它们预测为它们可能的物种。如果存在不属于我们模型结果的物种,可以预测出为“其他”这一结果。
训练网络
所谓网络的训练,就是通过已知的输入和输出构建一种类函数,来实现等式尽可能的成立。
大家都知道,我们在创建一个神经网络的过程中,使用到的数据集分为训练集和测试集合。因此对于数据集内容会有以下的要求:
- 首先明确数据集用于训练出的网络用于分类还是回归。
- 分类网络
- 训练集和测试集都必须是同一类属性。比如需要训练出一个实现检测是否佩戴口罩的神经网络,我们的数据集应该是佩戴和未佩戴口罩的人脸,而不是公路上拍到的车辆数据集。
- 对于数据集和训练集进行无关性处理。所谓无关性就是在数据集中存在非这两类的情况。例如口罩并未正确佩戴的情况。
- 回归网络
- 训练集和测试集都可以不是是同一类属性。比如有汽车、动物、人等。当我们输入一张照片时,他会预测出来这张最有可能是谁。
- 无需数据集和训练集进行无关性处理。
通过训练集构建出的类函数,用测试集对模型准确性检测,使得类函数能够对输入得出一个对应的结果。
为什么说的出对应的结果,而不是正确的结果?我们在网络的使用进行说明。
保存模型
众所周知,构建一个模型包括训练和保存。训练即不断修改权值和偏置,而最终训练好的模型是依托构建的模型框架(几个输入,隐藏、输出等)。因此,对于模型的保存,我们应该保存的是:
- 模型框架:输入层数量,隐藏层数量、连接方法、激活函数,输出层数量。这可以帮助我们复现该模型框架。
- 权值和偏置:各层的输入权值和偏置。这可以帮助我们将训练好的模型导入。
由于保存模型框架的最终目的是,可以浮现模型的前行和后向传递函数。因此我们也可以保存模型的前向和后向传递函数。
模型使用
所谓模型的使用,即按要求给定模型输入,根据模型定义的输入,将会输出一个正确结果(完美模型)。
而模型的使用过程,即调用模型的框架和权值和偏置重新构建了模型的前向传递函数。
output0 = ReLU(np.dot(weight0, input) + bias0) if active_mode == "ReLU" else sigmoid(np.dot(weight0, input) + bias0)
output1 = ReLU(np.dot(weight1, output0) + bias1) if active_mode == "ReLU" else sigmoid(np.dot(weight1, output0) + bias1)
output = ReLU(np.dot(weight2, output1) + bias2) if active_mode == "ReLU" else sigmoid(np.dot(weight2, output1) + bias2)
或是直接调用保存的前向传递函数
output = forward(input, weight0, bias0, weight1, bias1, weight2, bias2, active_mode)
模型的移植
即训练好的模型在其他设备设备上使用。以单片机为例,将上节的NN网络移植上去,我们将仅完成以下步骤:
C语言复现前向传递函数
#include <math.h>
// 定义激活函数
float ReLU(float x) {
return x > 0 ? x : 0;
}
float sigmoid(float x) {
return 1 / (1 + exp(-x));
}
// 前向传播函数
void forward(float input[16], float weight0[16][16], float bias0[16],
float weight1[16][16], float bias1[16],
float weight2[16][16], float bias2[16],
char* active_mode, float output[16]) {
float output0[16], output1[16], temp[16];
// 第一层隐藏层输出
for (int i = 0; i < 16; i++) {
temp[i] = 0;
for (int j = 0; j < 16; j++) {
temp[i] += weight0[i][j] * input[j];
}
temp[i] += bias0[i];
output0[i] = (active_mode == "ReLU") ? ReLU(temp[i]) : sigmoid(temp[i]);
}
// 第二层隐藏层输出
for (int i = 0; i < 16; i++) {
temp[i] = 0;
for (int j = 0; j < 16; j++) {
temp[i] += weight1[i][j] * output0[j];
}
temp[i] += bias1[i];
output1[i] = (active_mode == "ReLU") ? ReLU(temp[i]) : sigmoid(temp[i]);
}
// 输出层
for (int i = 0; i < 16; i++) {
temp[i] = 0;
for (int j = 0; j < 16; j++) {
temp[i] += weight2[i][j] * output1[j];
}
temp[i] += bias2[i];
output[i] = (active_mode == "ReLU") ? ReLU(temp[i]) : sigmoid(temp[i]);
}
}
参数矩阵的保存
// 假设权重和偏置已初始化为某些值
float weight0[16][16] = { /* ...训练好的参数... */ };
float bias0[16] = { /* ...初始化参数... */ };
float weight1[16][16] = { /* ...初始化参数... */ };
float bias1[16] = { /* ...初始化参数... */ };
float weight2[16][16] = { /* ...初始化参数... */ };
float bias2[16] = { /* ...初始化参数... */ };
定义单片机的输入与输出
// 也可以是某个寄存器或其地址
float input[16] = { /* 从传感器或其他数据采集设备读取 */ };
float output[16]; // 输出数组,用来存储最终预测结果
以及模型预测的触发条件
// 可以是内部中断(定时器等),也可以是外部中断(外部电平触发中断等)
// 以外部按键触发中断为例。
void onButtonPress() { //这里定义的是按键响应函数,并不是触发函数,不同的单片机的触发函数不同。
forward(input, weight0, bias0, weight1, bias1, weight2, bias2, "ReLU", output);
// 输出处理,例如显示在LCD上或通过串口输出
for (int i = 0; i < 16; i++) {
printf("Output[%d] = %f\n", i, output[i]);
// 也可以通过USART触底给上位机显示等。
}
}