Contents
前言
选的题目为第二个汉字识别。原先计划实现的神经网络为卷积神经网络(打算采用卷积神经网络的原因是提取很多的局部特征,因为图片分类任务是一个局部信息很有用的任务,做个极端假设,假设局部特征很大,刚好是想识别的那个汉字,那无论这个汉字出现在图片的哪个位置,卷积核都能把它提取出来,卷积核对与图片的各个局部来说,它的参数是被共享的)。但是失败了,卷积神经网络的反向传播实现出了很大的问题,后面代码实现发现,想法错误,没有真正推导出卷积层的反向传播的公式。只实现了传统的神经网络(卷积神经网络的全连接层)。这篇报告包含了我对算法的一些理解,及一些具体的实现细节。
由于没有显卡,一开始就放弃了识别完所有汉字,只尝试了去识别5个汉字。效果非常不好,算是一个失败的尝试。
刚开始做的时候想着造轮子,所以没有采用框架,(实际证明效果非常差),因为比较熟悉C++,所以用了opencv,主要是读取图片以及使用它的Mat(矩阵)类,因为图片是png的,如果用数组得解码,图片大小也不规整,还得写插值,而且矩阵运算是并行度非常高的运算,自己写的话,难以利用并行性(确实应该避免显示得写for循环)。真实的逻辑工作量应该和用numpy差不多,只是不太熟悉python。(其实我认为实际最好用框架,正确的做法应该是用框架得到参数,神经网络非常难并且最有价值的地方在于如何得到的一些效果好的参数,然后根据需要自己实现正向传播,反向传播实际实现起来非常困难,特别是卷积网络卷积层那种映射关系复杂的层,而且实际也没太有必要实现。)
神经网络算法理解
整体思想
神经网络整体是个仿生学。里面跟生物学其实可能并不那么对应,现阶段人脑远远比计算机强,神经网络用的更多的是数学思想。它的整个算法思想是个特征归纳过程。比如一幅60X60X3的图片,每一个像素都看作一个特征。而神经网络算法做的就是把这60X60X3的特征,一步一步归纳为多个特征。为什么说多个特征呢,如果只判断是或者不是,那就归纳为一个特征就行了(输出层激活函数用sigmod)。但是分类任务,一般采用的是(softmax),输出层应该归纳为多个特征。比如3700个汉字,应该就是3700个特征,然后取值最大的那个特征,对应的就是所分类的汉字。(我刚开始在想为什么不归纳为一个特征,然后那个特征比如取值0到3700,0到1对应一个汉字,1到2对应一个汉字,后来想了想可能激活函数那些设计比较困难)。
特征向量
前面说到神经网络归纳特征,那什么是特征呢,其实只要是变量就是特征。无论是二维的图像,还是一维的声音都是特征,甚至即使是一个一行一列的变量都可以是特征。那么就有个问题,图像二维的,声音一维的,可能还有其他三维的,四维的特征,或许叫特征空间比较好,这些特征空间并不整齐。在神经网络里,为了方便表示,统统把他们重新排列为列向量。所以CNN全连接层是非常有必要的。
激活单元
单个激活单元核心的功能就是把输入的多个特征(1个列向量)归纳为一个特征。
激活单元包括两部分,线性单元(行向量+b),(这里要注意的是这个行向量同一层的激活单元一定是不一样的,同一层w不要初始化为一样的),激活函数。这是模拟人的神经元。其中激活函数,主要是用于破坏线性的。没有激活函数,多层神经网络也没有意义了。(因为两个线性运算,可以合并为一个线性运算)。
具体点线性单元就是wx+b,x是输入特征单元。w是个向量,一定一定要注意的是b,对于单个激活单元来说,他其实是一个数字,但是实现的时候要采用广播的方式,python默认是有的,但是如果用其他语言就应该自己实现。
值得注意是激活函数不是对单个激活单元而言的。为了方便矩阵化表示,每一层的激活单元用同一个激活函数。
激活函数就比较自由了,其实不一定局限于sigmoid,Relu,tanh,softmax。我认为最重要的标准就是要求导方便,所以要么你的激活函数带有e^x(如sigmoid,tanh,softmax),e^x有优良的特性就是求导等于它本身。要么就像Relu,为正就得1,为负数就为0,或者带泄漏得到一个很小很小的数字。经验主义做多图片分类,中间层用Relu,输出层用softmax。对了输出层一定要和损失函数挂钩。因为直接从损失函数开始求偏导数,如果不挂钩,求导会很困难。(这里还有个重点是softmax,实现的时候要偏移一个最大值,比如有一列(5,4,2,1),实现起来第二个对应应该求e^(4-5)/(e^(5-5)+e^(4-5)+e^(2-5)+e^(1-5)),不然会很容易溢出。)
行向量与列向量的意义
为什么特征用列向量表示,激活单元的w用行向量表示呢。其实是有讲究的,考虑我们有俩激活单元,三个输入特征向量。输出将会是两行三列。输出中,每一列对应的是每一个特征向量,每一行对应的是每一个激活单元的输出。这是有严格的对应关系的,也许在纸上演算一下会更价直观。一个简单的说法是每一个输入特征向量(列向量)在向量乘法中共享激活单元的线性单元(行向量)。
我觉得这最大的意义就是非常方便我们把每一层矩阵化表示。
损失函数与梯度
刚才已经提及了神经网络就是一个特征归纳器。里面有很多很多的激活单元,每个激活单元的作用把一个特征向量归纳为一个特征。那么有一些问题,激活单元的参数w和b到底是多少才能有个好的归纳效果?怎么衡量神经网络的归纳效果?
为此人们设计了损失函数与成本函数。损失函数衡量的是单个样本的效果(用预测值和已经提前知道的先验值根据一定的规则去算)。Softmax函数采用的是交叉熵。
衡量全体样本训练效果的就是成本函数。我记得好像是损失函数加起来取平均得到成本函数,输出层采用不同的激活单元那成本函数是不同的,一个考虑是希望求偏导能够简单,方便反向传播实现。
神经网络的目标就是要使成本函数尽可能小。要使成本函数尽可能小,就得必须调整w和b,(从实现的角度激活函数刚开始就已经是给定死了的,激活函数是中途不能调整的),那w和b怎么调整呢?答案其实非常朴素,就是往梯度方向调整。(梯度其实就是自然流到最小值的方向,举个例子此时函数变量正处在一座高山的一滴水,最小值是山底,那么函数变量这个水滴自然运动的方向就是梯度,所以因为要用梯度下降法,成本函数最好是只有一个最小值点的凹函数,那样朝着梯度方向就一定会达到最小值。)
还有一点细小的问题是w和b每次调整多少,调整多少次。设置学习率可以控制w和b的调整幅度,学习率设置的很小的话要训练很久很久才收敛,学习率设置太大的话,容易出现溢出问题,在训练会出现inf和nan,从数学角度来讲应该是你不仅移动到了最低点,还移动过度了,导致了难以预测的输出发生。
神经网络的整个工作过程其实就是正向传播,求得成本函数(其实也不用求,实际要求的是反向传播需要的dz=a-y,(其中y为实际值,a为当前迭代输出层的值,这个a-y其实是求偏导数求出来的,与输出层所选用的激活函数还有定义的成本函数有关。))然后不断的反向偏导求各层的dw和db,然后更新w和b,开始下一轮迭代,这里没有提及cnn的卷积核,后文会提及。
激活单元的缺陷
神经网络中的激活单元其实是有不足的,就是对位置信息不敏感,感觉更多的是依靠变量值的统计特性。举个简单的例子是有俩列向量z0(x1,x2,x3),和z1(x3,x1,x2)。如果有个激活单元w(w1,w2,w3),并且他的系数是随机初始化的。那么这个激活单元会把z0和z1当作是一样的特征向量。(个人理解)
卷积层算法理解
为什么要引入卷积
卷积个人认为最重要的就是提取局部特征。可以联系一下图像处理当中的滤波器,高斯算子,拉普拉斯算子等。不过在卷积神经网络中算子是可以变化的。或许说归纳局部的特征向量。比如一个3X3X3的卷积核。卷积一个32*32*3的图片,得到一个30*30的矩阵。这个30*30的矩阵的每一个元素,对应的是原始图片对应位置局部3*3*3=27个元素的归纳值。如果是5*5*3的卷积核同理得到的就是26*26的矩阵。那这个26*26的矩阵的每一个元素,对应的是原始图片对应位置局部5*5*3=75个元素的归纳值。(卷积核信道数不一定是3,如果卷积图片是3,如果卷积的不是图片,而是卷积层当中的中间层那么信道数就是上一个卷积层卷积核的个数,因为有多少个卷积核输出就有多少层)
一些超级参数
卷积层还有三个参数,一个是填充,一个是卷积步长,一个是池化类型。
填充的主要目的就是为了防止损失边缘信息。因为卷积运算是会损失边缘信息的。如果卷积核心是3x3x3,则会损失最外面一层的信息。如果是5X5X3则会损失最外面两层的信息。那为了防止损失新建层,可以填充0,或者把原来的最外面层复制填充。
卷积步长和池化都是用来减少处理的特征数的。
卷积歩长,就是控制卷积核局部特征的间隔。如果步长为2的话(就每卷积一次,就跳一步的话)卷积核尺寸却是3的话,那么刚好卷积核提取局部特征的时候可以不重合。
池化有两种一个是最大池化,一个是平均池化。我个人的理解其实就是一个采样。对卷积核提取的特征进行采样,每4个里面采样1个,最大池化是用最大值替代,平均池化就是用平均值替代。
接入神经网络
传统神经网络处理每个样本的方法的是列向量。但卷积层得到的却是个有行有列的二维向量。所以要有个展平函数,就是把这个有行有列的向量展平成一个列向量。其实卷积神经网络中的全连接层其实就是传统的神经网络。
部分具体实现
大体步骤
其实理解了神经网络算法之后实现一个传统的神经网络真的不难,实现一个卷积神经网络正向传播也不难。(当然要充分利用矩阵运算的并行性,并充分掌握具体硬件,使算法高效非常有难度)。抛开运行效率,真正的难点在卷积层反向传播的实现。卷积层的反向传播,映射关系比较难处理。我尝试推导了但失败了,全链接层比较整齐,资料也比较丰富,实际实现反向传播就是很简单的几个公式。假设有一层z = wx+b, a=activation(z)。先求得这一层的da,然后求这一层的dz,然后根据这一层的dz,求得dw和db,输出层还可以直接求dz。当然这里用求偏导都是向量的求导,并不是单个变量的求导,一定不要搞错了,但是它的求偏导结果和单个变量非常像,所以很容易记,当然这里面有个转置,总的来说难度不大。
关于卷积运层反向传播的的一些探讨
化卷积运算为矩阵乘法
卷积层的反向传播,主要是调整卷积核,控制局部特征的提取老实说,我真的不知道卷积层的反向传播怎么推导。我上网查了一些资料,是把卷积运算拆成乘法。假如没有填充,步长为1,并且没有池化,通道数为1,最简单卷积大概可以等效为下图。如果能正确的推导出一些位置映射关系,卷积层的反向传播或许可以实现。
对b的处理
什么是广播?举个例子,有输入x它包含两个特征向量,有三个激活单元。那么与之对应的b是一个三行一列的向量。假设执行z=w*x+b,这是会报错的。必须先把b展开成三行两列。这就是广播。
广播会带来一些小小的问题,其中一个问题是训练集b的列数不是1并且和测试集b的列数不一样的问题。我们知道db=learing_rate*dz(上一层传入)。但其实理论上一个激单元的b应该是只有一个。不同的输入特征向量(每一列)应该都加上同一个b。这个问题怎么理解呢?我的理解方法是不要把b看成一个,允许不同的输入特征向量,偏移不同的b,最后在测试集和采用b的均值作为偏移量。
具体网络模型
我设计的网络模型大体上就是直接把图片展开,相当于直接连全连接层FC。然后经过4层激活函数为ReLU的层,一层输出为softmax的层。然后每一层的激活单元数可以自己设定。
附录
#include <opencv2/opencv.hpp>
#include <string>
#include <iostream>
#include <vector>
#include <cmath>
/*这是一份实现了nn神经网络的代码,基于nn的小数据拟合是没问题的,内存占用会比较大。但像图片分类这样的任务是不行的
因为没有卷积层提取局部特征,如果强行用nn,那可能nn网络参数会多的吓人,毕竟图片局部信息真的有用
比如【-1,0,1
-1,0,1
-1,0,1】提取横向梯度,当然具体怎么提取局部特征可以训练得到,我是真推不出来卷积层怎么求偏导的
所以很勉强,直接将图片(是经过插值处理后的固定尺寸的)展成一个列向量(相当于只有全连接层的cnn),在一个小数据范围内,试验了下nn的实现正确
具体可参见附带的演示视频
*/
//特别说明看到0.00001f的话,我没用严格用0.0f,因为我怕某个步骤出现除数过大的情况
cv::Mat forward_propagation(const cv::Mat &last_a, const cv::Mat &w, const cv::Mat &b,
const std::string activation_type)
{
/*检测数据有没有溢出*/
for (int i = 0; i < last_a.rows; i++)
for (int j = 0; j < last_a.cols; j++)
if (isnan(last_a.at<float>(i, j)) || isinf(last_a.at<float>(i, j)))
{
std::cout << "数据已经溢出,请调小学习率" << '\n';
exit(1);
}
cv::Mat z = w * last_a + b;
cv::Mat a(z.rows, z.cols, CV_32FC1);//这里我把softmax和其他前向转播分开了,因为只有softmax要先算当前列总和,我的实现还有点不一样。
//我做了偏移,就偏移一个最大值,不这样的话e^的次方很容易就溢出了,我刚开始实现的时候数据量一大就溢出
if (activation_type == "softmax")
{
for (int j = 0; j < z.cols; ++j)
{
float offset = z.at<float>(0, j);
for (int i = 1; i < z.rows; ++i)//找到当前列的最大值为偏移量
offset = std::max(z.at<float>(i, j), offset);
float softmax_sum = 0.00001f;
for (int i = 0; i < z.rows; ++i)
softmax_sum += expf(z.at<float>(i, j) - offset);
for (int i = 0; i < z.rows; ++i)
{
a.at<float>(i, j) = expf(z.at<float>(i, j) - offset) / softmax_sum;
if (isnan(a.at<float>(i, j)))
std::cout << ' ' << a.at<float>(i, j);
}
}
}
else
{
for (int i = 0; i < z.rows; ++i)
{
float *ptr_z = z.ptr<float>(i);
float *ptr_a = a.ptr<float>(i);
for (int j = 0; j < z.cols; ++j)
{
if (activation_type == "tanh")
ptr_a[j] = tanh(ptr_z[j]);
else if (activation_type == "sigmoid")
{
ptr_a[j] = 1.0f / (1.0f + expf(-1.0f * ptr_z[j]));
}
else if (activation_type == "ReLU")
ptr_a[j] = ptr_z[j] > 0.00001f ? ptr_z[j] : 0.00001f;
else
{
std::cout << "不支持的激活函数类型" << '\n';
exit(1);
}
}
}
}
return a;
}
cv::Mat activation_derivation(const cv::Mat &a, const std::string activation_type) //根据激活函数对a反向求导的函数
{
cv::Mat a_derivation;
a.copyTo(a_derivation);
a_derivation.forEach<float>(//这个foreach循环是我看API的时候看到的就用了,其实和展开for最终效果一样,但opencv,API简介说利用了并行性
[&activation_type](float &element, const int *position) -> void {
if (activation_type == "tanh")
element = 1 - element * element;
else if (activation_type == "sigmoid")
element = element * (1 - element);
else if (activation_type == "ReLU")
element = element > 0.0f ? 1.0f : 0.0f;
else
{
std::cout << "不支持的激活函数类型" << '\n';
exit(1);
}
});
return a_derivation;
}
void classification()
{
/*读取训练集*/
struct object//这是读取训练集我创的结构体,后面发现其实不用但没改,有点代码冗余
{
cv::Mat image;
cv::Mat label;
int pos;
object() {}
object(cv::Mat &image, int pos)
{
label = cv::Mat(1, 5, CV_32FC1, cv::Scalar(0.00001f));
label.at<float>(0, pos) = 1.0f;
this->pos = pos;
this->image = image;
}
object(cv::Mat &&image, int pos)
{
label = cv::Mat(1, 5, CV_32FC1, cv::Scalar(0.00001f));
label.resize(5, 0.00001f);
label.at<float>(0, pos) = 1.0f;
this->pos = pos;
this->image = image;
}
};
const std::string path = "artificial_intelligence//chinese_base//";//路径
const int type_num = 5;//类型数,这里附录带的是5,我测过大的,数据一大nn真不行
const int per_chinese_training_image_num = 2;//每一类的图片数
const int training_num = per_chinese_training_image_num * type_num;
std::vector<object> training(training_num);//训练集
const int image_row = 50;//图片尺寸的行数
const int image_col = 50;//图片尺寸的列数
for (int i = 0; i < training_num; ++i)
{
std::string file_name;
int pos;
if (i < 1 * per_chinese_training_image_num)
{
pos = 0;
file_name = path + "物//" + std::to_string(i % per_chinese_training_image_num) + ".png";
}
else if (1 * per_chinese_training_image_num <= i && i < 2 * per_chinese_training_image_num)
{
pos = 1;
file_name = path + "联//" + std::to_string(i % per_chinese_training_image_num) + ".png";
}
else if (2 * per_chinese_training_image_num <= i && i < 3 * per_chinese_training_image_num)
{
pos = 2;
file_name = path + "网//" + std::to_string(i % per_chinese_training_image_num) + ".png";
}
else if (3 * per_chinese_training_image_num <= i && i < 4 * per_chinese_training_image_num)
{
pos = 3;
file_name = path + "黄//" + std::to_string(i % per_chinese_training_image_num) + ".png";
}
else if (4 * per_chinese_training_image_num <= i && i < 5 * per_chinese_training_image_num)
{
pos = 4;
file_name = path + "剑//" + std::to_string(i % per_chinese_training_image_num) + ".png";
}
cv::Mat image = cv::imread(file_name);
if (image.empty())
{
std::cout << file_name << "图片不存在" << '\n';
}
cv::Size option_size(image_row, image_col);
cv::Mat temp_8u(option_size, CV_8UC3);
cv::resize(image, temp_8u, option_size);//调整图片大小
cv::Mat temp_32f(image_row, image_col, CV_32FC3);//因为图片都是3通道的,RGB吧好像是,每一个5位,如果不是,Opencv应该也转换了
temp_8u.convertTo(temp_32f, CV_32FC3);//但是训练都是浮点数,要转一下
training[i] = object(temp_32f, pos);
}
/*将二维图片训练集展开成列向量*/
cv::Mat x;
x.reserve(training_num);
for (int i = 0; i < training_num; ++i)
{
cv::Mat temp(training[i].image.rows * training[i].image.cols * training[i].image.channels(), 1, CV_32FC1);
for (int j = 0; j < training[i].image.rows; ++j)
for (int k = 0; k < training[i].image.cols; ++k)
{
float *ptr = training[i].image.ptr<float>(j, k);
for (int w = 0; w < training[i].image.channels(); ++w)
temp.at<float>(j * (training[i].image.cols * training[i].image.channels()) + k * training[i].image.channels() + w, 0) = ptr[w];
}
x.push_back(std::move(temp));
}
x = x.reshape(0, image_row * image_row * 3);
/*初始化各层参数*/
cv::RNG rng(0); //设置随机数种子
//注意每一层的行数就是激活单元的数量
const int w_1_row = 500;
cv::Mat w_1 = cv::Mat(w_1_row, image_row * image_row * 3, CV_32FC1);
rng.fill(w_1, cv::RNG::UNIFORM, -1.0f / w_1.cols, 1.0f / w_1.cols);
cv::Mat b_1 = cv::Mat(w_1_row, training_num, CV_32FC1, cv::Scalar(0.00001f));
const std::string a_1_activation_type = "ReLU";
const int w_2_row = 400;
cv::Mat w_2 = cv::Mat(w_2_row, w_1_row, CV_32FC1);
rng.fill(w_2, cv::RNG::UNIFORM, -1.0f / w_2.cols, 1.0f / w_2.cols);
cv::Mat b_2 = cv::Mat(w_2_row, training_num, CV_32FC1, cv::Scalar(0.00001f));
const std::string a_2_activation_type = "ReLU";
const int w_3_row = 300;
cv::Mat w_3 = cv::Mat(w_3_row, w_2_row, CV_32FC1);
rng.fill(w_3, cv::RNG::UNIFORM, -1.0f / w_3.cols, 1.0f / w_3.cols);
cv::Mat b_3 = cv::Mat(w_3_row, training_num, CV_32FC1, cv::Scalar(0.00001f));
const std::string a_3_activation_type = "ReLU";
const int w_4_row = 200;
cv::Mat w_4 = cv::Mat(w_4_row, w_3_row, CV_32FC1);
rng.fill(w_4, cv::RNG::UNIFORM, -1.0f / w_4.cols, 1.0f / w_4.cols);
cv::Mat b_4 = cv::Mat(w_4_row, training_num, CV_32FC1, cv::Scalar(0.00001f));
const std::string a_4_activation_type = "ReLU";
const int w_5_row = type_num;
cv::Mat w_5 = cv::Mat(w_5_row, w_4_row, CV_32FC1);
rng.fill(w_5, cv::RNG::UNIFORM, -1.0f / w_5.cols, 1.0f / w_5.cols);
cv::Mat b_5 = cv::Mat(type_num, training_num, CV_32FC1, cv::Scalar(0.00001f));
const std::string a_5_activation_type = "softmax";
/*读取标签y*/
cv::Mat y;
y.reserve(training_num);
for (auto &t : training)
y.push_back(t.label);
y = y.t();
/*迭代训练*/
//注意的是在测试集上b会有点特殊,要用平均的b,不然列数都不匹配
//还有得有个标准softmax输出大于零点多少,你认为是分到了这一类
const float learning_rate = 0.01f;
const int iteration_times = 10000;
for (int i = 0; i < iteration_times; ++i)
{
std::cout << "第" << i + 1 << "次迭代:" << '\n';
/*正向传播*/
cv::Mat a_1 = forward_propagation(x, w_1, b_1, a_1_activation_type);
cv::Mat a_2 = forward_propagation(a_1, w_2, b_2, a_2_activation_type);
cv::Mat a_3 = forward_propagation(a_2, w_3, b_3, a_3_activation_type);
cv::Mat a_4 = forward_propagation(a_3, w_4, b_4, a_4_activation_type);
cv::Mat a_5 = forward_propagation(a_4, w_5, b_5, a_5_activation_type);
/*反向传播*/
//所有层都是先计算dz然后计算dw和db 其中输出层的dz可以直接a-y,因为dz先根据成本函数求的da然后由da求得dz,省略中间步骤就是a-y
//非输出层求dz是根据这一层的a,是a不是da,求它的偏导获得dz,对应activation_derivation
//(1.0f / training_num),每一层都有这个系数是因为,a_5-y这一项,是成本函数所有样本的偏导,我们希望求得的是一个平均值,所以每一层dw和db都有这一项
//然后注意每个dz和dw求导是都有一项是转置,是因为是向量的求导,不是单个变量求导,只是长得很像单变量的求导,可以这么记,但是具体怎么加转置,可以上笔推推行和列要对应嘛
cv::Mat dz_5 = a_5 - y;
cv::Mat dw_5 = (1.0f / training_num) * dz_5 * (a_4.t());
cv::Mat db_5 = (1.0f / training_num) * dz_5;
cv::Mat dz_4 = ((w_5.t()) * dz_5).mul(activation_derivation(a_4, a_4_activation_type));
cv::Mat dw_4 = (1.0f / training_num) * dz_4 * (a_3.t());
cv::Mat db_4 = (1.0f / training_num) * dz_4;
cv::Mat dz_3 = ((w_4.t()) * dz_4).mul(activation_derivation(a_3, a_3_activation_type));
cv::Mat dw_3 = (1.0f / training_num) * dz_3 * (a_2.t());
cv::Mat db_3 = (1.0f / training_num) * dz_3;
cv::Mat dz_2 = ((w_3.t()) * dz_3).mul(activation_derivation(a_2, a_2_activation_type));
cv::Mat dw_2 = (1.0f / training_num) * dz_2 * (a_1.t());
cv::Mat db_2 = (1.0f / training_num) * dz_2;
cv::Mat dz_1 = ((w_2.t()) * dz_2).mul(activation_derivation(a_1, a_1_activation_type));
cv::Mat dw_1 = (1.0f / training_num) * dz_1 * (x.t());
cv::Mat db_1 = (1.0f / training_num) * dz_1;
/*更新系数*/
//学习率就是控制每一次迭代的步长,太大的话,由于计算机都是数字的会出现nan(not a number)包括inf(无穷小,无穷大)还有一出等
//但是太小就会训练很久,调参一直是个问题
w_5 -= learning_rate * dw_5;
b_5 -= learning_rate * db_5;
w_4 -= learning_rate * dw_4;
b_4 -= learning_rate * db_4;
w_3 -= learning_rate * dw_3;
b_3 -= learning_rate * db_3;
w_2 -= learning_rate * dw_2;
b_2 -= learning_rate * db_2;
w_1 = learning_rate * dw_1;
b_1 = learning_rate * db_1;
/*输出每层的迭代结果*/
//这里我是一列一列的输出的,看到我第一层循环是j,因为softmax的输出行和列数和标签是一样的
for (int j = 0; j < a_5.cols; j++)
{
std::cout << "第" << j << "张图片对应softmax:";
for (int i = 0; i < a_5.rows; i++)
std::cout << std::fixed << ' ' << std::setprecision(5) << a_5.at<float>(i, j);
std::cout << '\n';
}
}
/* 为了方便演示我给注释掉了,而且你会发现有cov_w1这个参数,这是我尝试加入卷积层之后的测试函数。而且这是一段代码算是废弃了的代码,测试集就是不用迭代循环,
直接用已经得到的w和b,卷积核其实应该也已经用得到的,但由于我没有很好的实现卷积层的反向传播,所有也没办法用 */
/*测试集*/
/* auto data_test = [&cov_w_1, &w_1, &b_1, &w_2, &b_2, &w_3, &b_3, &w_4, &b_4, &w_5, &b_5,
&a_1_activation_type, &a_2_activation_type, &a_3_activation_type, &a_4_activation_type, &a_5_activation_type]() -> void {
const std::string path = "artificial_intelligence//chinese_base//";
const int per_test_chinese_image_num = 2;
const int test_num = per_test_chinese_image_num * type_num;
std::vector<object> test(test_num);
for (int i = 0; i < test_num; ++i)
{
std::string file_name;
int pos;
if (i < 1 * per_test_chinese_image_num)
{
pos = 0;
file_name = path + "物//" + std::to_string(i % per_test_chinese_image_num + per_chinese_training_image_num) + ".png";
}
else if (1 * per_test_chinese_image_num <= i && i < 2 * per_test_chinese_image_num)
{
pos = 1;
file_name = path + "联//" + std::to_string(i % per_test_chinese_image_num + per_chinese_training_image_num) + ".png";
}
else if (2 * per_test_chinese_image_num <= i && i < 3 * per_test_chinese_image_num)
{
pos = 2;
file_name = path + "网//" + std::to_string(i % per_test_chinese_image_num + per_chinese_training_image_num) + ".png";
}
else if (3 * per_test_chinese_image_num <= i && i < 4 * per_test_chinese_image_num)
{
pos = 3;
file_name = path + "黄//" + std::to_string(i % per_test_chinese_image_num + per_chinese_training_image_num) + ".png";
}
else if (4 * per_test_chinese_image_num <= i && i < 5 * per_test_chinese_image_num)
{
pos = 4;
file_name = path + "剑//" + std::to_string(i % per_test_chinese_image_num + per_chinese_training_image_num) + ".png";
}
cv::Mat image = cv::imread(file_name);
cv::Size option_size(image_row, image_col);
cv::Mat temp_8u(option_size, CV_8UC3);
cv::resize(image, temp_8u, option_size);
cv::Mat temp_32f(image_row, image_col, CV_32FC3);
temp_8u.convertTo(temp_32f, CV_32FC3);
test[i] = object(temp_32f, pos);
}
cv::Mat test_x;
test_x.reserve(test_num);
for (int i = 0; i < test_num; ++i)
{
cv::Mat temp((image_row - 2) * (image_col - 2) * image_chanel *
kernel_size * kernel_size,
1, CV_32FC1);
int base = 0;
for (int j = 0; j < test[i].image.rows + 1 - kernel_size; ++j)
for (int k = 0; k < test[i].image.cols + 1 - kernel_size; ++k)
{
for (int cur_i = 0; cur_i < kernel_size; ++cur_i)
for (int cur_j = 0; cur_j < kernel_size; ++cur_j)
{
float *ptr = test[i].image.ptr<float>(j + cur_i, k + cur_j);
for (int cur_chanle = 0; cur_chanle < test[i].image.channels(); ++cur_chanle)
{
if (isnan(ptr[cur_chanle]) || isinf(ptr[cur_chanle]))
std::cout << ' ' << ptr[cur_chanle];
temp.at<float>(base + cur_i * (kernel_size * test[i].image.channels()) +
cur_j * test[i].image.channels() +
cur_chanle,
0) = ptr[cur_chanle];
}
}
base += kernel_size * kernel_size * test[i].image.channels();
}
test_x.push_back(std::move(temp));
}
test_x = test_x.reshape(0, (image_row - 2) * (image_col - 2) * image_chanel *
kernel_size * kernel_size);
cv::Mat test_y;
test_y.reserve(test_num);
for (auto &t : test)
test_y.push_back(t.label);
test_y = test_y.t();
cv::Mat test_cov_a_1((test_x.rows / (kernel_size * kernel_size * kernel_chanel)) * conv_1_row, test_x.cols, CV_32FC1);
for (int k = 0; k < test_x.cols; ++k)
{
int test_cov_a_1_j = 0;
for (int cur_cov = 0; cur_cov < conv_1_row; ++cur_cov)
{
int base = -1;
for (int j = 0; j < test_x.rows; ++j)
{
float temp;
if (j % (kernel_size * kernel_size * kernel_chanel) == 0)
{
if (base >= 0)
test_cov_a_1.at<float>(test_cov_a_1_j * (test_x.rows / (kernel_size * kernel_size * kernel_chanel)) + base, k) = temp;
++base;
temp = 0.0f;
}
temp += test_x.at<float>(j, k) * cov_w_1.at<float>(cur_cov, j % (kernel_size * kernel_size * kernel_chanel));
}
++test_cov_a_1_j;
}
}
cv::Mat test_a_1 = forward_propagation(test_cov_a_1, w_1, b_1, a_1_activation_type);
cv::Mat test_a_2 = forward_propagation(test_a_1, w_2, b_2, a_2_activation_type);
cv::Mat test_a_3 = forward_propagation(test_a_2, w_3, b_3, a_3_activation_type);
cv::Mat test_a_4 = forward_propagation(test_a_3, w_4, b_4, a_4_activation_type);
cv::Mat test_a_5 = forward_propagation(test_a_4, w_5, b_5, a_5_activation_type);
int fault_num = 0;
for (int j = 0; j < test_a_2.cols; ++j)
{
int max_pos = 0;
int real_pos = 0;
for (int i = 0; i < test_a_2.rows; ++i)
{
if (test_a_2.at<float>(i, j) > test_a_2.at<float>(max_pos, j))
max_pos = i;
if (static_cast<int>(test_y.at<float>(i, j)) == 1)
real_pos = i;
}
if (max_pos != real_pos)
++fault_num;
}
std::cout << '\n'
<< "测试集:" << '\n';
for (int j = 0; j < test_a_5.cols; ++j)
{
for (int i = 0; i < test_a_5.rows; ++i)
std::cout << std::fixed << ' ' << std::setprecision(5) << test_a_5.at<float>(i, j);
std::cout << '\n';
}
float fault_rate = static_cast<float>(fault_num) / test_num;
std::cout << "错误率为:" << fault_rate << '\n';
};
data_test(); */
}
int main(int argc, char **argv)
{
classification();
return 0;
}