C++ 中的反向传播人工神经网络






4.88/5 (27投票s)
本文演示了一个具有验证集和测试集的反向传播人工神经网络控制台应用程序,用于通过不均匀分布度量来估算性能。
引言
我想介绍一个基于控制台的后向传播神经网络 C++ 库实现,该库是我在医学数据分类研究和用于人脸检测的 CV 库期间开发和使用的:具有皮肤和运动分析的人脸检测 C++ 库。CodeProject 上已有几篇很好的文章,您可以参考它们来了解理论。在我的代码中,我通过 Minmax、Zscore、Sigmoidal 和 Energy 归一化等输入数据预处理来呈现输入层所需的特性。这些参数从训练集中获得,然后用于预处理每个传入的分类向量。控制台支持在反向传播训练之前将训练数据随机分成训练集、验证集和测试集。随机分割可以获得一个代表性的训练集,并比较在验证集和测试集上的性能。验证集可用于通过估算在该集上的性能来防止过拟合。在反向传播会话结束时,我将保存两种网络配置:一种在验证集上具有最佳性能的配置,以及最后一次训练的 epoch 配置。对于性能估算,我使用灵敏度、特异性、阳性预测值、阴性预测值和准确率等度量。对于在数据分布不均(例如,在我的“人脸检测”文章中,您可能会发现非人脸比人脸多得多,比例为 19:1)的情况下进行验证性能估算,我提供了几何平均值和 F-measure 度量来支持该场景。为了支持大量数据向量,我提供了基于文件映射的数据加载。这允许在 1 毫秒内将您数百兆字节的训练数据映射到内存,并立即开始您的训练会话。对于相对少量的数据,您可以使用文本文件格式。最后,控制台实现更易于使用,您可以避免 GUI 应用程序中的大量鼠标点击,并可以使用批处理文件自动化过程,以选择正确的网络拓扑、在验证集和测试集上的最佳性能等。
背景
请参阅 CodeProject 神经网络文章。我还使用了来自 generation5 网站的教程。对于数据分布不均的问题,我使用了:Evaluation of classifiers for an uneven class distribution problem。在那里您可以了解几何平均值和 F-measure 度量。
使用代码
下面显示了控制台的帮助行
argv[1] t-train
argv[2] network conf file
argv[3] cls1 files [0.9]
argv[4] cls2 files [0.1]
argv[5] epochs num
argv[6] [validation class]
argv[7] [test class]
argv[8] [validation TH 0.5]
argv[9] [vld metric mse]
argv[10] [norm]: [0-no], 1-minmax, 2-zscore, 3-softmax, 4-energy
argv[11] [error tolerance cls] +- 0.05 default
argv[1] r-run
argv[2] network conf file
argv[3] cls files
argv[4] [validation TH 0.5]
argv[5] [norm]: [0-no], 1-minmax, 2-zscore, 3-softmax, 4-energy
ann1dn.exe t net.nn cls1 cls2 3000 [tst.txt][val.txt]
[TH [0.5]][val type [mse]] [norm [0]] [err [0.05]]
ann1dn.exe r net.nn testcls [TH [0.5]] [norm [0]]
metrics: [0 - mse]
1 - AC
2 - sqrt(SE*SP)
3 - sqrt(SE*PP)
4 - sqrt(SE*SP*AC)
5 - sqrt(SE*SP*PP*NP*AC)
6 - F-measure b=1
7 - F-measure b=1.5
8 - F-measure b=3
启动训练会话的最少参数
>ann1dn.exe t network.nn data1_file data2_file 1000
它将使用 `network.nn` 文件作为神经网络,并从 `data1_file` 和 `data2_file` 加载数据,这两个文件代表正类和负类的数据向量,并训练 1000 个 epoch。
神经网络文件格式在我的人脸检测文章中有描述。要开始训练会话前进行随机初始化的权重,您需要在该文件中仅提供层数和每层的神经元数量。例如,在演示 zip 文件中,您会找到一个 `iris.nn` 文件
3
4 8 1
三层,每层有 4、8 和 1 个神经元。
它支持两种数据文件格式。文本格式是
vector_name class_mark
x1 x2 x3 ... xN
...
`vector_name` 是您的特定向量的名称;它不应该以数字开头,只能以字母开头。`class_mark` 是与类属性对应的非零数字:1、2 等。在控制台应用程序中,我只使用 1 作为正类,期望输出为 0.9,使用 2 作为负类,期望输出为 0.1。下一行包含整数或浮点格式的向量条目。
在演示 zip 文件中,IRIS 数据使用具有四维条目的文本格式进行组织
virgi1 2
64 28 56 22
virgi2 2
67 31 56 24
virgi3 2
63 28 51 15
virgi4 2
69 31 51 23
virgi5 2
65 30 52 20
virgi6 2
65 30 55 18
...
setosa1 1
50 33 14 2
setosa2 1
46 34 14 3
setosa3 1
46 36 10 2
setosa4 1
51 33 17 5
setosa5 1
55 35 13 2
...
二进制浮点文件格式在数据量很大时很方便。数据以二进制格式存储在单独的文件中,作为浮点数序列,每个浮点数占用 4 个字节。
file1.dat(2x3 矩阵)
[x11] [x12] [x13] [x21] [x22] [x23]
并且,数据矩阵的维度保存在具有相同名称但扩展名为 `.hea` 的文件中。
file1.hea
2 3
在前面的示例中,文件 `file1.dat` 包含两个三维向量。
在这种情况下,您的 `data1_file` 或 `data2_file` 可能包含带有文件完整路径和类别标记的条目
fullpath\file1.dat 1
fullpath\file2.dat 1
...
控制台应用程序进行反向传播训练的下一个参数是可选的。您可以使用它们来验证和测试您的网络,进行输入数据归一化,以及在训练过程中设置误差限制。
>ann1dn t network.nn data1_file data2_file 1000 vld_file tst_file 0.5 2 2
此命令行演示了您在 `vld_file` 和 `tst_file` 文件中使用您的验证集和测试集,这些文件为文本或二进制格式,如上所述,验证阈值为 0.5(即,网络输出大于 0.5 将数据向量归类为正类),使用灵敏度和特异性的几何平均值作为验证停止性能度量,并使用 Zscore 归一化。允许的验证度量在控制台帮助行的末尾指定。如果您的 `vld_file` 或 `tst_file` 是空文件,则相应的数据集将由训练集中随机选择的条目组成,每个类别包含 25% 的记录。
控制台的最后一个(第十一个)参数是误差容差。如果在连续 10 个 epoch 中,正类和负类的期望输出与平均网络输出之间的差小于该误差,则训练停止。在反向传播训练过程中,如果特定向量的期望输出与网络输出之间的差小于您指定的误差,则网络权重不会被调整。这允许校正尚未“记忆”的数据向量的网络权重连接。
下一个示例演示了 IRIS 数据的样本训练会话
>ann1dn t iris.nn setosa_versi.dat virgi.dat 200 void void 0.5 2 3
loading data...
cls1: 100 cls2: 50 files loaded. size: 4 samples
validaton size: 25 12
validaton size: 26 13
normalizing minmax...
training...
epoch: 1 out: 0.555723 0.478843 max acur: 0.92 (epoch 1) se:84.00 sp:100.00 ac:89.19
epoch: 2 out: 0.582674 0.400396 max acur: 0.92 (epoch 1) se:84.00 sp:100.00 ac:89.19
epoch: 3 out: 0.626480 0.359573 max acur: 0.92 (epoch 3) se:84.00 sp:100.00 ac:89.19
epoch: 4 out: 0.655483 0.326918 max acur: 0.94 (epoch 4) se:96.00 sp:91.67 ac:94.59
epoch: 5 out: 0.699125 0.323879 max acur: 0.94 (epoch 5) se:88.00 sp:100.00 ac:91.89
epoch: 6 out: 0.715539 0.299085 max acur: 0.94 (epoch 6) se:88.00 sp:100.00 ac:91.89
epoch: 7 out: 0.733927 0.292526 max acur: 0.96 (epoch 7) se:92.00 sp:100.00 ac:94.59
epoch: 8 out: 0.750638 0.278721 max acur: 0.98 (epoch 8) se:96.00 sp:100.00 ac:97.30
epoch: 9 out: 0.774599 0.277550 max acur: 0.98 (epoch 8) se:96.00 sp:100.00 ac:97.30
epoch: 10 out: 0.774196 0.256110 max acur: 0.98 (epoch 8) se:96.00 sp:100.00 ac:97.30
epoch: 11 out: 0.793877 0.260753 max acur: 0.98 (epoch 8) se:96.00 sp:100.00 ac:97.30
epoch: 12 out: 0.806802 0.245758 max acur: 0.98 (epoch 8) se:96.00 sp:100.00 ac:97.30
epoch: 13 out: 0.804381 0.228810 max acur: 0.98 (epoch 13) se:96.00 sp:100.00 ac:97.30
epoch: 14 out: 0.814079 0.218740 max acur: 0.98 (epoch 13) se:96.00 sp:100.00 ac:97.30
epoch: 15 out: 0.827635 0.223827 max acur: 0.98 (epoch 13) se:96.00 sp:100.00 ac:97.30
epoch: 16 out: 0.832102 0.210360 max acur: 0.98 (epoch 13) se:96.00 sp:100.00 ac:97.30
epoch: 17 out: 0.840352 0.213165 max acur: 0.98 (epoch 17) se:96.00 sp:100.00 ac:97.30
epoch: 18 out: 0.848957 0.201766 max acur: 0.98 (epoch 18) se:96.00 sp:100.00 ac:97.30
epoch: 19 out: 0.844319 0.188338 max acur: 0.98 (epoch 19) se:96.00 sp:100.00 ac:97.30
epoch: 20 out: 0.856258 0.184954 max acur: 0.98 (epoch 19) se:96.00 sp:100.00 ac:97.30
epoch: 21 out: 0.853244 0.178349 max acur: 0.98 (epoch 19) se:96.00 sp:100.00 ac:97.30
epoch: 22 out: 0.867145 0.185852 max acur: 0.98 (epoch 22) se:96.00 sp:100.00 ac:97.30
epoch: 23 out: 0.863079 0.171684 max acur: 0.98 (epoch 23) se:96.00 sp:100.00 ac:97.30
epoch: 24 out: 0.870108 0.170253 max acur: 0.98 (epoch 24) se:96.00 sp:100.00 ac:97.30
epoch: 25 out: 0.873538 0.164185 max acur: 0.98 (epoch 25) se:96.00 sp:100.00 ac:97.30
epoch: 26 out: 0.871584 0.150496 max acur: 1.00 (epoch 26) se:100.00 sp:100.00 ac:100.00
epoch: 27 out: 0.879310 0.161155 max acur: 1.00 (epoch 26) se:100.00 sp:100.00 ac:100.00
epoch: 28 out: 0.879986 0.154784 max acur: 1.00 (epoch 26) se:100.00 sp:100.00 ac:100.00
epoch: 29 out: 0.880308 0.139083 max acur: 1.00 (epoch 26) se:100.00 sp:100.00 ac:100.00
epoch: 30 out: 0.890360 0.149518 max acur: 1.00 (epoch 26) se:100.00 sp:100.00 ac:100.00
epoch: 31 out: 0.888561 0.145144 max acur: 1.00 (epoch 26) se:100.00 sp:100.00 ac:100.00
epoch: 32 out: 0.880072 0.129197 max acur: 1.00 (epoch 32) se:100.00 sp:100.00 ac:100.00
epoch: 33 out: 0.896553 0.139937 max acur: 1.00 (epoch 32) se:100.00 sp:100.00 ac:100.00
epoch: 34 out: 0.893467 0.137607 max acur: 1.00 (epoch 32) se:100.00 sp:100.00 ac:100.00
epoch: 35 out: 0.893400 0.125793 max acur: 1.00 (epoch 32) se:100.00 sp:100.00 ac:100.00
epoch: 36 out: 0.905036 0.139306 max acur: 1.00 (epoch 32) se:100.00 sp:100.00 ac:100.00
epoch: 37 out: 0.900872 0.118167 max acur: 1.00 (epoch 32) se:100.00 sp:100.00 ac:100.00
epoch: 38 out: 0.909384 0.134014 max acur: 1.00 (epoch 32) se:100.00 sp:100.00 ac:100.00
training done.
training time: 00:00:00:031
classification results: maxacur.nn
train set: 49 25
sensitivity: 100.00
specificity: 100.00
+predictive: 100.00
-predictive: 100.00
accuracy: 100.00
validation set: 25 12
sensitivity: 100.00
specificity: 100.00
+predictive: 100.00
-predictive: 100.00
accuracy: 100.00
test set: 26 13
sensitivity: 88.46
specificity: 92.31
+predictive: 95.83
-predictive: 80.00
accuracy: 89.74
classification results: iris.nn
train set: 49 25
sensitivity: 97.96
specificity: 100.00
+predictive: 100.00
-predictive: 96.15
accuracy: 98.65
validation set: 25 12
sensitivity: 96.00
specificity: 100.00
+predictive: 100.00
-predictive: 92.31
accuracy: 97.30
test set: 26 13
sensitivity: 88.46
specificity: 100.00
+predictive: 100.00
-predictive: 81.25
accuracy: 92.31
网络配置保存到 `maxacur.nn`,对应于验证集上的最佳性能,最后 epoch 配置保存到 `iris.nn`。最后,您可以比较它们的结果。
要使用您训练好的网络进行测试,只需使用以下参数运行控制台
>ann1dn r iris.nn test_data
`test_data` 文件为文本或二进制格式。您可以提供第四个参数作为阈值(默认为 0.5),以在您的测试集上获得 ROC 曲线,例如,将其在 0.0 和 1.0 之间变化。
神经网络类
神经网络由以下类组成
ANNetwork
ANNLayer
ANeuron
ANLink
`ANNetwork` 类包含神经网络的实现,供库用户使用。为了避免对其他类使用受保护接口编程,我使用了 `friend`。我将首先描述库结构,然后提供您需要从 `ANNetwork` 类使用的函数来维护您自己的实现。
`ANNetwork` 包含一个 `ANNLayer` 层数组。每一层包含一个 `ANeuron` 神经元对象数组,每个神经元包含 `ANLink` 输入和输出连接数组。通过这种设计,您可以排列任何所需的网络结构;但是,在我的实现中,我只提供前馈全连接结构。
神经网络的基本单元是神经元类 `ANeuron`。您可以向其添加偏置或输入连接,表示为 `ANLink` 对象。
void ANeuron::add_bias()
void ANeuron::add_input(ANeuron *poutn)
如您所知,偏置连接始终以 1.0f 作为输入值。通过 `add_input()`,您可以向神经元添加连接,为其参数提供连接到它的前一层中的神经元。
void ANeuron::add_input(ANeuron *poutn)
{
//poutn - Neuron from previous layer
ANLink *plnk = new ANLink(this, poutn);
inputs.push_back(plnk);
if (poutn)
poutn->outputs.push_back(plnk);
}
因此,每个神经元都知道下一层中的哪些神经元连接到其输出。`ANLink` 类似于“箭头”,从前一层中的神经元 `ANLink::poutput_neuron` 指向下一层中的神经元 `ANLink::pinput_neuron`。
我以这种方式组织了一个全连接的神经网络结构
void ANNetwork::init_links(const float *avec, const float *mvec, int ifunc, int hfunc)
{
ANNLayer *plr; //current layer
ANNLayer *pprevlr; //previous layer
ANeuron *pnrn; //neuron pointer
int l = 0;
/////////////////////////input layer/////////////////////////////
plr = layers[l++];
swprintf(plr->layer_name, L"input layer");
for (int n = 0; n < plr->get_neurons_number(); n++) {
pnrn = plr->neurons[n];
pnrn->function = ifunc;
pnrn->add_input();
//one input link for every "input layer" neuron
if (avec)
pnrn->inputs[0]->iadd = avec[n];
if (mvec)
pnrn->inputs[0]->w = mvec[n];
else
pnrn->inputs[0]->w = 1.0f;
}
/////////////////////////////////////////////////////////////////
////////////////////////hidden layer's/////////////////////////////////////// 1bias
for (int i = 0; i < m_layers_number - 2 ; i++) { //1input [l-2 hidden] 1output
pprevlr = plr;
plr = layers[l++];
swprintf(plr->layer_name, L"hidden layer %d", i + 1);
for (int n = 0; n < plr->get_neurons_number(); n++) {
pnrn = plr->neurons[n];
pnrn->function = hfunc;
pnrn->add_bias();
for (int m = 0; m < pprevlr->get_neurons_number(); m++)
pnrn->add_input(pprevlr->neurons[m]);
}
}
//////////////////////////////////////////////////////////////////////////////
////////////////////////output layer///////////////////////////////////////// 1bias
pprevlr = plr;
plr = layers[l++];
swprintf(plr->layer_name, L"output layer");
for (int n = 0; n < plr->get_neurons_number(); n++) {
pnrn = plr->neurons[n];
pnrn->function = hfunc;
pnrn->add_bias();
for (int m = 0; m < pprevlr->get_neurons_number(); m++)
pnrn->add_input(pprevlr->neurons[m]);
}
//////////////////////////////////////////////////////////////////////////////
}
`ANeuron` 中使神经元“激发”的函数,即从其输入获取数据并处理它们到其输出的函数是:
void ANeuron::input_fire()
void ANeuron::fire()
第一个仅用于输入层神经元。`ANLink` 包含一个附加项 `iadd`,用于归一化。最后一个用于隐藏层和输出层神经元。
inline void ANeuron::input_fire()
{
//input layer normalization
oval = (inputs[0]->ival + inputs[0]->iadd) * inputs[0]->w;
//single input for input layer neuron
switch (function) {
default:
case LINEAR:
break;
case SIGMOID:
oval = 1.0f / (1.0f + exp(float((-1.0f) * oval)));
break;
}
//transfer my output to links connected to my output
for (int i = 0; i < get_output_links_number(); i++)
outputs[i]->ival = oval;
}
inline void ANeuron::fire()
{
//oval = SUM (in[]*w[])
oval = 0.0f;
//compute output for Neuron
for (int i = 0; i < get_input_links_number(); i++)
oval += inputs[i]->ival * inputs[i]->w;
switch (function) {
default:
case LINEAR:
break;
case SIGMOID:
oval = 1.0f / (1.0f + exp(float((-1.0f) * oval)));
break;
}
//transfer my output to links connected to my output
for (int i = 0; i < get_output_links_number(); i++)
outputs[i]->ival = oval;
}
现在,您对库的内部结构有了一些了解,接下来,我将描述您可用于自己实现的 `ANNetwork` 类。
您可以从文件中加载神经网络,或通过指定层数和每层的神经元数量来安排其结构
ANNetwork::ANNetwork(const wchar_t *fname);
ANNetwork::ANNetwork(int layers_number, int *neurons_per_layer);
int nerons_per_layer[4] = {128, 64, 32, 10};
ANNetwork *ann = new ANNetwork(4, neurons_per_layer);
ann->init_links(); //feed-forward full connectionist structure
ann->randomize_weights();
如果您想要自定义神经网络配置,例如带有循环连接或其他连接,您必须提供自己的函数。
`ANNetwork::status()` 函数在构造后返回类状态。负值表示错误,0 - 成功从文件加载网络,1 - 随机权重。
要训练、分类和保存您的网络,提供了以下函数:
bool ANNetwork::train(const float *ivec, float *ovec, const float *dsrdvec, float error = 0.05);
void ANNetwork::classify(const float *ivec, float *ovec);
bool ANNetwork::save(const wchar_t *fname) const;
`ivec` 和 `ovec` 分别代表输入到神经网络的输入向量和存储其结果的输出向量。它们的维度应与网络结构中的输入和输出神经元数量匹配。`dsrdvec` 是期望输出向量,网络将调整其连接以在 `error` 容差内匹配它。`ANNetwork::train()` 函数在反向传播发生时返回 true,如果网络输出在 `error` 容差内接近期望向量,则返回 false。
反向传播函数使用此代码
bool ANNetwork::train(const float *ivec, float *ovec, const float *dsrdvec, float error)
// 0.0 - 1.0 learning
{
float dst = 0.0f;
//run network, computation of inputs to output
classify(ivec, ovec);
for (int n = 0; n < layers[m_layers_number-1]->get_neurons_number(); n++) {
dst = fabs(ovec[n] - dsrdvec[n]);
if (dst > error) break;
}
if (dst > error) {
backprop_run(dsrdvec); //it was trained
return true;
} else //it wasnt trained
return false;
}
void ANNetwork::backprop_run(const float *dsrdvec)
{
float nrule = m_nrule; //learning rule
float alpha = m_alpha; //momentum
float delta, dw, oval;
//get deltas for "output layer"
for (int n = 0; n < layers[m_layers_number-1]->get_neurons_number(); n++) {
oval = layers[m_layers_number-1]->neurons[n]->oval;
layers[m_layers_number-1]->neurons[n]->delta =
oval * (1.0f - oval) * (dsrdvec[n] - oval);
}
//get deltas for hidden layers
for (int l = m_layers_number - 2; l > 0; l--) {
for (int n = 0; n < layers[l]->get_neurons_number(); n++) {
delta = 0.0f;
for (int i = 0; i < layers[l]->neurons[n]->get_output_links_number(); i++)
delta += layers[l]->neurons[n]->outputs[i]->w *
layers[l]->neurons[n]->outputs[i]->pinput_neuron->delta;
oval = layers[l]->neurons[n]->oval;
layers[l]->neurons[n]->delta = oval * (1 - oval) * delta;
}
}
////////correct weights for every layer///////////////////////////
for (int l = 1; l < m_layers_number; l++) {
for (int n = 0; n < layers[l]->get_neurons_number(); n++) {
for (int i = 0; i < layers[l]->neurons[n]->get_input_links_number(); i++) {
//dw = rule*Xin*delta + moment*dWprv
dw = nrule * layers[l]->neurons[n]->inputs[i]->ival *
layers[l]->neurons[n]->delta;
dw += alpha * layers[l]->neurons[n]->inputs[i]->dwprv;
layers[l]->neurons[n]->inputs[i]->dwprv = dw;
//correct weight
layers[l]->neurons[n]->inputs[i]->w += dw;
}
}
}
}
现在,您可以构建自己的网络,并进行 OCR、计算机视觉等领域的典型分类任务。