参考链接:AI基础教程 / 神经网络原理简明教程(来自github)
C++课程设计中有用到神经网络预测模型,网上参考代码很多,但是大部分无法运行或与我的目标不一致。所以我重新写了一个能用的,下面提供完整的C++工程。
可以进入顶部参考链接了解详细原理和公式推导过程。参考链接使用Python语言,由于课程设计要求,我按照源码思路用C++重写了一遍,包括一个前馈神经网络和一个带时间步长的神经网络(简单的RNN),如下图。
实验发现,普通的前馈网络预测效果较差,RNN更适合用于与时间相关的数据。
普通前馈:
简单的RNN:
已知25天的销售量数据,预测未来几天的销售量。
数据如下:
X特征(第n天):
double x[25] = {1, 2, 3, …, 24, 25}
Y标签(第n天的销售量):
double y[25] = {300, 320, 340, 360, 380, 415, 423, 430, 394, 350, 340, 320,320, 340, 380, 360, 380, 410, 420, 430, 394, 350, 340, 300, 320};
请按照规定创建文件和类,在vs2019中可以直接运行。
提示:训练过程中出现“特别大的loss值”是偶然现象,请多次训练以获取最优值。
提供两种模型,请仔细查看注释
首先得保证你的ide能运行C++工程,然后按标题名创建3个文件并复制代码,编译运行即可。
#include
#include
#include "Network.h"
using namespace std;
int main()
{
/**
* 可以直接运行程序
* 训练举例,偶尔会训练失败出现nan等错误,再次训练即可
* 两种网络使用方式请根据[1] [2]提示进行选择
* 默认为RNN训练
*/
double y[25] = {
300, 320, 340, 360, 380, 415, 423, 430, 394, 350, 340, 320, 320, 340, 380,
360, 380, 410, 420, 430, 394, 350, 340, 300, 320};
double x[25];
for (int i = 0; i < 25; i++)
{
x[i] = double(i) + 1.0;
}
Network n(5, 25); /*< 神经元数量=5, 训练数据量=25*/
n.readTrainData(x, y); /*< 读取x, y*/
//[1]
/*用RNN训练*/
n.RNNTrain(10000); /*< 用RNN训练10000次*/
n.RNNShowResult(); /*< 输出训练结果,将原数据和训练结果复制到excel中绘图进行对比*/
/*end 用RNN训练*/
//[2]
/*用普通网络训练,请注释上面的RNN训练部分*/
// n.train(5000); /*< 自动输出结果*/
/*end 用普通网络训练*/
}
}
#pragma once
class Network
{
private:
double* w1; /*< 权重w1*/
double* w2; /*< 权重w2*/
double* b1; /*< 偏移量b1*/
double b2; /*< 偏移量b2*/
double* gw1; /*< 权重w1梯度*/
double* gw2; /*< 权重w2梯度*/
double* gb1; /*< 偏移量b1梯度*/
double gb2; /*< 偏移量b2梯度*/
double* trainX; /*< 待训练的数据*/
double* trainY;
double output; /*< 输出值*/
double* z1;
double* a1;
double eta; /*< 更新效率*/
int lenData; /*< 数据长度*/
int lenWeight; /*< 权重数量*/
double* saveZ;
double* SAOutput;
double* normalParamX; /*< 归一化参数,用于恢复数据*/
double* normalParamY;
/*RNN-Param*/
double* W;
double* U;
double* V;
double* bH;
double bZ;
double* h1;
double* s1;
double x1;
double gz1;
double* gh1;
double* gbh1;
double* gU1;
double* h2;
double* s2;
double x2;
double z2;
double gz2;
double gbZ;
double* gh2;
double* gbh2;
double* gV2;
double* gU2;
double* gW;
public:
Network(int numOfWeights, int numOfData);
~Network();
void forward(double x); /*< 向前计算*/
void backward(double x, double y); /*< 向后计算*/
void readTrainData(double* dataX, double* dataY); /*< 读取数据,并做归一化处理*/
void normalize(double* normalParam, double* data);
void train(int iterTime);
void update();
void randomParam();
void shuffle(double* a, double* b);
void showResult();
/*-----------------RNN-----------------*/
void forward1(double x1, double* U, double* V, double* W, double* bh );
void forward2(double x2, double* U, double* V, double* W, double* bh, double bz, double* s1);
void backward2(double y, double* s1);
void backward1(double y, double* gh2);
void RNNTrain(int maxEpoch);
void RNNUpdate();
void RNNInitParam();
void RNNShowResult();
bool RNNCheckLoss(double x1, double x2, double y, int totalIterTime);
/*--------------end RNN-----------------*/
double test(double x);
double loss(double* z, double* y); /*< 损失函数*/
double singleLoss(double z, double y);
double sigmoid(double x);
double dSigmoid(double x);
double resData(double normalData, double differMaxMin, double avgRes); /*< 复原数据*/
double dTanh(double y); /*< tanh求导*/
bool checkErrorAndLoss(double x, double y, int totalIterTime);
};
#include "Network.h"
#include
#include
#include
#include
/**
* @brief 初始化神经网络
* @param 随机产生权重的数量
* @detail 随机产生初始权重
*/
using namespace std;
Network::Network( int numOfWeights, int numOfData)
{
//b1 = b2 = 0.0;
eta = 0.01;
lenData = numOfData;
lenWeight = numOfWeights; /*< 权重数量等于神经元数量*/
w1 = new double[lenWeight];
w2 = new double[lenWeight];
b1 = new double[lenWeight];
output = 0.0;
trainX = new double[lenData];
trainY = new double[lenData];
z1 = new double[lenWeight];
a1 = new double[lenWeight];
gw1 = new double[lenWeight];
gw2 = new double[lenWeight];
gb1 = new double[lenWeight];
gb2 = 0.0;
SAOutput = new double[lenWeight];
saveZ = new double[lenData];
srand((unsigned)time(NULL));
randomParam();
normalParamX = new double[2];
normalParamY = new double[2];
/*----------------RNN Init---------------*/
W = new double[lenWeight];
U = new double[lenWeight];
V = new double[lenWeight];
bH = new double[lenWeight];
bZ = 0.0;
h1 = new double[lenWeight];
s1 = new double[lenWeight];
x1 = 0.0;
gz1 = 0.0;
gh1 = new double[lenWeight];
gbh1 = new double[lenWeight];
gU1 = new double[lenWeight];
h2 = new double[lenWeight];
s2 = new double[lenWeight];
x2 = 0.0;
z2 = 0.0;
gz2 = 0.0;
gbZ = 0.0;
gh2 = new double[lenWeight];
gbh2 = new double[lenWeight];
gV2 = new double[lenWeight];
gU2 = new double[lenWeight];
gW = new double[lenWeight];
RNNInitParam();
/*-------------end RNN Init-------------*/
cout << "初始化完成;" << endl;
}
Network::~Network()
{
delete[]w1;
delete[]w2;
delete[]b1;
delete[]trainX;
delete[]trainY;
delete[]z1;
delete[]a1;
delete[]gw1;
delete[]gw2;
delete[]gb1;
delete[]SAOutput;
delete[]saveZ;
delete normalParamX;
delete normalParamY;
/*------RNN Delete---------*/
delete[]W;
delete[]U;
delete[]V;
delete[]bH;
delete[]h1;
delete[]s1;
delete[]gh1;
delete[]gbh1;
delete[]gU1;
delete[]h2;
delete[]s2;
delete[]gh2;
delete[]gbh2;
delete[]gV2;
delete[]gU2;
delete[]gW;
/*------ end RNN Delete----*/
cout << "end" << endl;
}
/**
* 向前计算
*/
void Network::forward(double x)
{
//const int m = lenWeight;
/*向前计算*/
//std::cout << "开始向前计算。。。output:" << std::endl;
double z2;
z2 = 0.0;
for (int i = 0; i < lenWeight; i++)
{
z1[i] = x * w1[i] + b1[i];
a1[i] = sigmoid(z1[i]);
z2 += a1[i] * w2[i];
}
z2 += b2;
output = z2;
//std::cout << output << std::endl;
//cout << "向前计算完毕;" << endl;
}
/**
* 向后计算
*/
void Network::backward(double x, double y)
{
//cout << "开始向后计算。。。gb1:" << endl;
double* gz1 = new double[lenWeight];
double* ga1 = new double[lenWeight];
double gz2 = output - y; /*< 第二层梯度输入*/
//cout << y << endl;
for (int i = 0; i < lenWeight; i++)
{
gw2[i] = gz2 * a1[i]; /*< w2梯度*/
ga1[i] = gz2 * w2[i]; /*< a1偏导*/
gz1[i] = ga1[i] * dSigmoid(a1[i]); /*< 第一层梯度输入*/
gw1[i] = x * gz1[i]; /*< w1梯度*/
gb1[i] = gz1[i]; /*< gb1梯度*/
}
gb2 = gz2; /*< b2梯度*/
delete[]gz1;
delete[]ga1;
//cout << "向后计算完毕;" << endl;
}
/**
* 读取数据,并做归一化处理
*/
void Network::readTrainData(double* dataX, double* dataY)
{
/*归一化数据*/
normalize(normalParamX, dataX);
normalize(normalParamY, dataY);
for (int i = 0; i < lenData; i++)
{
trainX[i] = dataX[i];
trainY[i] = dataY[i];
//cout << trainX[i] << endl;
//cout << trainY[i] << endl;
}
}
void Network::normalize(double* normalParam, double* data)
{
double maximum;
double minimum;
double avg;
maximum = *std::max_element(data, data + lenData);
minimum = *std::min_element(data, data + lenData);
avg = data[0];
for (int i = 1; i < lenData; i++)
{
avg += data[i];
}
avg = avg / lenData;
for (int i = 0; i < lenData; i++)
{
data[i] = (data[i] - avg) / (maximum - minimum);
}
normalParam[0] = avg;
normalParam[1] = (maximum - minimum);
}
/**
* 开始训练
*/
void Network::train(int iterTime)
{
double x;
double y;
int totalIterTime;
bool needStop;
for (int i = 0; i < iterTime; i++)
{
shuffle(trainX, trainY);
for (int m = 0; m < lenData; m++)
{
x = trainX[m];
y = trainY[m];
forward(x);
backward(x, y);
update(); /*< 更新权重和偏移值*/
totalIterTime = i * lenData + m;
if (totalIterTime % 50 == 0)
{
needStop = checkErrorAndLoss(x, y, totalIterTime);
if (needStop)
{
break;
} /*< end if*/
} /*< end if*/
} /*< end for*/
if (needStop)
{
break;
} /*< end if*/
} /*< end for*/
for (int i = 0; i < lenData; i++)
{
forward(trainX[i]);
cout << trainX[i] << "===" << trainY[i] << "=====" << output << endl;
}
cout << "w1--" << w1[0] << endl;
cout << "w2--" << w2[0] << endl;
cout << "b1--" << b1[0] << endl;
cout << "b2--" << b2 << endl;
checkErrorAndLoss(0.45, 0.45, totalIterTime);
}
void Network::update()
{
for (int i = 0; i < lenWeight; i++)
{
w1[i] = w1[i] - gw1[i] * eta;
w2[i] = w2[i] - gw2[i] * eta;
b1[i] = b1[i] - gb1[i] * eta;
}
b2 = b2 - gb2 * eta;
}
/*产生随机参数*/
void Network::randomParam()
{
for (int i = 0; i < lenWeight; i++)
{
w1[i] = double((rand() % 100)) / 100;
w2[i] = double((rand() % 100)) / 100;
b1[i] = double((rand() % 100)) / 100;
}
b2 = double((rand() % 100)) / 100;
}
void Network::shuffle(double* a, double* b)
{
int len = lenData; // 全集元素数量
srand(unsigned(time(NULL))); // 摇号机准备
for (int i = len; i > 1; i--) // 从全集开始摇号,直至只剩一个,已排在最后
{
int cur = len - i + (rand() % i); // 在剩余集合中摇号
double tmpa = a[len - i]; // 当前序号位置挪空
double tmpb = b[len - i];
a[len - i] = a[cur]; // 摇出的彩球放入当前序号
b[len - i] = b[cur];
a[cur] = tmpa; // 放好剩余的彩球
b[cur] = tmpb;
}
}
void Network::showResult()
{
//cout << "gb2==" << gb2 << endl;
//cout << "output==" << output << endl;
/*cout << "w1==" << w1[0] << endl;
cout << "w2==" << w2[0] << endl;
cout << "b1==" << b1[0] << endl;
cout << "b2==" << b2 << endl;*/
//cout << "gw2==" << gw2[0] << endl;
}
void Network::forward1(double x1, double* u, double* v, double* w, double* bh)
{
for (int i = 0; i < lenWeight; i++)
{
this->W[i] = w[i];
this->h1[i] = x1 * u[i] + bh[i];
this->s1[i] = tanh(this->h1[i]);
}
this->x1 = x1;
}
void Network::forward2(double x2, double* u, double* v, double* w, double* bh, double bz, double* s1)
{
this->x2 = x2;
this->z2 = 0.0;
for (int i = 0; i < lenWeight; i++)
{
this->V[i] = v[i];
this->h2[i] = x2 * u[i] + s1[i] * w[i] + bh[i];
this->s2[i] = tanh(this->h2[i]);
this->z2+= this->s2[i] * this->V[i];
}
this->z2 += bz;
}
void Network::backward2(double y, double* s1)
{
this->gz2 = this->z2 - y;
this->gbZ = this->gz2;
for (int i = 0; i < lenWeight; i++)
{
this->gh2[i] = (this->gz2) * (this->V[i]) * dTanh(this->s2[i]);
this->gbh2[i] = this->gh2[i];
this->gV2[i] = this->s2[i] * this->gz2;
this->gU2[i] = this->x2 * this->gh2[i];
this->gW[i] = s1[i] * this->gh2[i];
}
}
void Network::backward1(double y, double* gh2)
{
for (int i = 0; i < lenWeight; i++)
{
this->gh1[i] = gh2[i] * this->W[i] * dTanh(this->s1[i]);
this->gbh1[i] = this->gh1[i];
this->gU1[i] = this->x1 * this->gh1[i];
}
}
void Network::RNNTrain(int maxEpoch)
{
double x1;
double y1;
double x2;
double y2;
int totalIterTime;
bool needStop = false;
for (int i = 0; i < maxEpoch; i++)
{
for (int j = 0; j < lenData; j++)
{
x1 = trainX[j];
y1 = 0.0;
x2 = trainX[j + 1];
y2 = trainY[j + 1];
forward1(x1, U, V, W, bH);
forward2(x2, U, V, W, bH, bZ, s1);
backward2(y2, s1);
backward1(y1, gh2);
RNNUpdate();
//cout << gbh1[0] << endl;
totalIterTime = i * lenData + j;
if (totalIterTime % 500 == 0)
{
needStop = RNNCheckLoss(x1, x2, trainY[j], totalIterTime);
if (needStop)
{
break;
} /*< end if*/
} /*< end if*/
}
}
}
void Network::RNNUpdate()
{
for (int i = 0; i < lenWeight; i++)
{
U[i] = U[i] - (gU1[i] + gU2[i]) * eta;
V[i] = V[i] - (gV2[i]) * eta;
W[i] = W[i] - gW[i] * eta;
bH[i] = bH[i] - (gbh1[i] + gbh2[i]) * eta;
}
bZ = bZ - gbZ * eta;
}
void Network::RNNInitParam()
{
for (int i = 0; i < lenWeight; i++)
{
U[i] = double((rand() % 100)) / 100;
V[i] = double((rand() % 100)) / 100;
W[i] = double((rand() % 100)) / 100;
bH[i] = double((rand() % 100)) / 100;
}
bZ = double((rand() % 100)) / 100;
}
void Network::RNNShowResult()
{
cout << "---------- RNN - Param -----------" << endl;
for (int i = 0; i < lenWeight; i++)
{
cout << "U" << i << ": " << U[i] << endl;
cout << "V" << i << ": " << V[i] << endl;
cout << "W" << i << ": " << W[i] << endl;
cout << "bh" << i << ": " << bH[i] << endl;
}
cout << "bz" << ": " << bZ << endl;
cout << "-------- END RNN - Param --------" << endl;
cout << endl;
cout << endl;
cout << "---------- RNN - Test -----------" << endl;
double* vldOutput = new double[lenData];
/*验证*/
vldOutput[0] = trainY[0];
for (int i = 0; i < lenData - 1; i++)
{
forward1(trainX[i], U, V, W, bH);
forward2(trainX[i + 1], U, V, W, bH, bZ, s1);
vldOutput[i + 1] = z2;
//cout << "testX: " << trainX[i + 1] << "testY: " << trainY[i + 1] << "--------outputY: " << vldOutput[i + 1] << endl;
cout << resData(vldOutput[i + 1], normalParamY[1], normalParamY[0]) << endl;
}
cout << "-------- END RNN - Test --------" << endl;
}
bool Network::RNNCheckLoss(double X1, double X2, double y, int totalIterTime)
{
double lossTrain;
double lossVld;
bool needStop;
double* vldOutput = new double[lenData];
cout << "iterTime= " << totalIterTime << endl;
forward1(X1, U, V, W, bH);
forward2(X2, U, V, W, bH, bZ, s1);
lossTrain = singleLoss(z2, y);
cout << "lossTrain= " << lossTrain << endl;
/*验证*/
vldOutput[0] = trainY[0];
for (int i = 0; i < lenData-1; i++)
{
forward1(trainX[i], U, V, W, bH);
forward2(trainX[i+1], U, V, W, bH, bZ, s1);
vldOutput[i+1] = z2;
}
lossVld = loss(vldOutput, trainY);
cout << "lossValid= " << lossVld << endl;
if (lossVld <= 0.000001)
{
needStop = true;
}
else
{
needStop = false;
}
return needStop;
return false;
}
double Network::test(double x)
{
double z = 0.0;
double data;
double avgX = normalParamX[0];
double differMaxMinX = normalParamX[1];
double avgY = normalParamY[0];
double differMaxMinY = normalParamY[1];
data = (x - avgX) / differMaxMinX;
forward(data);
z = resData(output, differMaxMinY, avgY);
return z;
}
double Network::loss(double* z, double* y)
{
double ls;
int n;
ls = 0;
n = lenData;
for (int i = 0; i < n; i++)
{
ls += (z[i] - y[i]) * (z[i] - y[i]);
}
ls = (0.5 / n) * ls;
return ls;
}
double Network::singleLoss(double z, double y)
{
double ls;
ls = (z - y) * (z - y) * 0.5;
return ls;
}
double Network::sigmoid(double x)
{
double s;
s = 1.0/(exp(-x)+1.0);
return s;
}
/*sigmoid导数*/
double Network::dSigmoid(double x)
{
double s;
s = x * (1.0 - x);
return s;
}
double Network::resData(double normalData, double differMaxMin, double avgRes)
{
double data;
data = normalData * differMaxMin + avgRes;
return data;
}
double Network::dTanh(double y)
{
return (1 - y * y);
}
bool Network::checkErrorAndLoss(double x, double y, int totalIterTime)
{
double lossTrain;
double lossVld;
bool needStop;
double* vldOutput = new double[lenData];
cout << "iterTime= " << totalIterTime << endl;
forward(x);
lossTrain = singleLoss(output, y);
cout << "lossTrain= " << lossTrain << endl;
/*验证*/
for (int i = 0; i < lenData; i++)
{
forward(trainX[i]);
vldOutput[i] = output;
}
lossVld = loss(vldOutput, trainY);
cout << "lossValid= " << lossVld << endl;
if (lossVld <= 0.000001)
{
needStop = true;
}
else
{
needStop = false;
}
return needStop;
}
VS部分,完。
建议保存为json格式,原项目是用Qt做的,所以下面提供有关参数保存和读取的Qt源码,理解思路就好
#include
#include
#include
/**
* @brief 从已有的json文件中读取训练完成的参数
*/
void RNN::readParam()
{
//打开文件
QFile file("rnn_param.json");
if(!file.open(QIODevice::ReadOnly)) {
qDebug() << "File open failed!";
} else {
qDebug() <<"File open successfully!";
}
QJsonParseError *error=new QJsonParseError;
QJsonDocument jdc=QJsonDocument::fromJson(file.readAll(),error);
//判断文件是否完整
if(error->error!=QJsonParseError::NoError)
{
qDebug()<<"parseJson:"<<error->errorString();
}
QJsonObject obj = jdc.object(); //获取对象
// qDebug() <<"object size:"<
lenData = obj["lenData"].toInt();
lenWeight = obj["lenWeight"].toInt();
trainY[0] = obj["trainY0"].toDouble();
QJsonArray arr = obj["WeightsBias"].toArray();
for (int i=0; i<lenWeight; i++)
{
this->U[i] = arr[i].toObject()["U"].toDouble();
this->V[i] = arr[i].toObject()["V"].toDouble();
this->W[i] = arr[i].toObject()["W"].toDouble();
this->bH[i] = arr[i].toObject()["bH"].toDouble();
}
this->bZ = arr[0].toObject()["bZ"].toDouble();
// qDebug() << bZ;
normalParamX[0] = obj["AverageX"].toDouble();
normalParamX[1] = obj["differMaxMinX"].toDouble();
normalParamY[0] = obj["AverageY"].toDouble();
normalParamY[1] = obj["differMaxMinY"].toDouble();
file.close();
}
/**
* @brief 将训练完成的参数保存为json格式的文件
*/
void RNN::saveParam()
{
//打开文件
QFile file("rnn_param.json");
if(!file.open(QIODevice::WriteOnly)) {
qDebug() << "File open failed!";
} else {
qDebug() <<"File open successfully!";
}
file.resize(0);
QJsonDocument jdoc;
QJsonObject obj;
QJsonArray WeightsBias;
for(int i=0;i<lenWeight;i++)
{
QJsonObject trainParam; //定义数组成员
trainParam["U"] = U[i];
trainParam["V"] = V[i];
trainParam["W"] = W[i];
trainParam["bH"] = bH[i];
trainParam["bZ"] = bZ;
WeightsBias.append(trainParam);
}
obj["WeightsBias"] = WeightsBias;
obj["AverageX"] = normalParamX[0];
obj["differMaxMinX"] = normalParamX[1];
obj["AverageY"] = normalParamY[0];
obj["differMaxMinY"] = normalParamY[1];
obj["lenData"] = lenData;
obj["lenWeight"] = lenWeight;
obj["trainY0"] = trainY[0];
jdoc.setObject(obj);
file.write(jdoc.toJson(QJsonDocument::Indented)); //Indented:表示自动添加/n回车符
file.close();
}
提示:完整项目已经开源:
C++ QT Creater 车票信息管理及预测系统