导航栏
深度学习C++代码 (位于 Github)
深度学习C++代码配套教程(1. 总述)
深度学习C++代码配套教程(2. 基础数据操作)
深度学习C++代码配套教程(3. 数据文件读取)
深度学习C++代码配套教程(4. ANN 经典神经网络)
深度学习C++代码配套教程(5. CNN 卷积神经网络)
经典神经网络用 java 实现只需要70 行,点击访问. 我把它翻译成了 C++, 代码行数变成了若干倍.
1. 网络结构
神经网络由多层构成, 如图 1 所示. 我们先建立单层, 然后获得整个的网络.
图 1 神经网络
2. MfAnnLayer 类
每一层包括输入、输出, 因此图 1 仅有两个 MfAnnLayer.
2.1 成员变量
成员变量说明如下:
//The size of the input.
//第 1 层的 inputSize 为条件属性数.
int inputSize;
//The size of the output.
//最后 1 层的 outputSize 为类别数.
int outputSize;
//Learning rate, 一般为 0.001 之类。
double rate;
//Mobp, 控制惯性。
double mobp;
//The activator.
Activator* activator;
//The input data, only one row, 实质为一个向量.
MfDoubleMatrix* inputData;
//The output data, only one row
MfDoubleMatrix* outputData;
//The weights for edges
//这是训练网络主要改变的数据.
MfDoubleMatrix* weightMatrix;
//The weight change for edges.
//将它作为成员变量, 可以避免临时空间分配.
MfDoubleMatrix* weightDeltaMatrix;
//The offset. 偏移量, 也称 bias. 为一个向量.
//与图 1 中 +1 连接的部分即为 offset.
MfDoubleMatrix* offsetMatrix;
//The offset delta
MfDoubleMatrix* offsetDeltaMatrix;
//The layer node error.
//back propagation 时使用.
MfDoubleMatrix* errorMatrix;
几点说明:
- activator 同时管理激活 (forward) 与求导 (backPropagation). 当前使用的 sigmoid, 对iris 数据集有效, 以后用其它激活函数测试.
- weightMatrix 和 offsetMatrix 是网络需要训练的参数.
2.2 成员函数
成员函数申明如下:
//The default constructor. 没用.
MfAnnLayer();
//Constructor for input/output size
MfAnnLayer(int paraInputSize, int paraOutputSize, char paraActivation,
double paraRate, double paraMobp);
//Destructor.
virtual ~MfAnnLayer();
//Convert to string for display.
string toString();
//Show weights.
void showWeight();
//Setter.
void setActivationFunction(char paraActivation);
//Reset weight and other variables.
void reset();
//Getter.
int getInputSize();
//Getter.
int getOutputSize();
//Forward calculation
MfDoubleMatrix* forward(MfDoubleMatrix* paraData);
//Back propagation calculation
MfDoubleMatrix* backPropagation(MfDoubleMatrix* paraLabel);
//Code unit test
void unitTest();
多数函数都是常见的, 不需要特别说明.
2.2.1 forward
MfDoubleMatrix* MfAnnLayer::forward(MfDoubleMatrix* paraData)
{
//将数据拷贝到 inputData, back propagation 时还要用.
inputData->cloneToMe(paraData);
//加权和, 就是这么直接.
outputData->timesToMe(inputData, weightMatrix);
//再加上偏移量. 也是矩阵操作.
outputData->addToMe(outputData, offsetMatrix);
//激活. outputData的激活函数在构造函数中已经初始化.
outputData->activate();
return outputData;
}//Of forward
这里完成的就是 y = σ ( w x + b ) \mathbf{y} = \sigma(\mathbf{wx}+\mathbf{b}) y=σ(wx+b). 在矩阵操作的支持下, 代码简直和数学表达式一样简单! 说明如下:
- 支持不同的激活函数, 而不仅仅是sigmoid (即 σ \sigma σ).
- y \mathbf{y} y 是本层的输出, 如果本层并非输出层, 它就是下层的输入.
2.2.2 backPropagation
MfDoubleMatrix* MfAnnLayer::backPropagation(MfDoubleMatrix* paraErrors)
{
double tempValue1, tempValue2, tempValue3, tempValue4;
double tempErrorSum;
for(int i = 0; i < inputSize; i ++)
{
tempErrorSum = 0;
//Weights adjusting
for(int j = 0; j < outputSize; j ++)
{
tempErrorSum += paraErrors->getValue(0, j) * weightMatrix->getValue(i, j);
tempValue1 = mobp * weightDeltaMatrix->getValue(i, j)
+ rate * paraErrors->getValue(0, j) * inputData->getValue(0, i);
weightDeltaMatrix->setValue(i, j, tempValue1);
tempValue2 = weightMatrix->getValue(i, j);
weightMatrix->setValue(i, j, tempValue1 + tempValue2);
if (i == inputSize - 1)
{
// Offset adjusting
tempValue1 = offsetDeltaMatrix->getValue(0, j);
tempValue2 = paraErrors->getValue(0, j);
tempValue3 = mobp * tempValue1 + rate * tempValue2;
offsetDeltaMatrix->setValue(0, j, tempValue3);
tempValue4 = offsetMatrix->getValue(0, j);
offsetMatrix->setValue(0, j, tempValue4 + tempValue3);
}//Of if
}//Of for j
//For the activation function.
tempValue1 = inputData->getValue(0, i);
errorMatrix->setValue(0, i, activator->derive(tempValue1) * tempErrorSum);
}//Of for i
return errorMatrix;
}//Of backPropagation
几点说明:
- 权值都是在原有权值基础上进行小幅变动, 后者使用 mobp 与 rate 这两个超参数控制.
- offset 的处理稍的不同.
- 以求导 derive 函数结束本层 backPropagation.
3. MfFullAnn 类
把单层堆成多层.
3.1 成员变量
//Number of layers
int numLayers;
//Learning rate
double rate;
//Mobp
double mobp;
//Activation
char activation;
//The sizes of layers
MfIntArray* layerSizes;
//All layers
MfAnnLayer** layers;
//The output for current instance
MfDoubleMatrix* currentOutput;
//The current instance
MfDoubleMatrix* currentInstance;
//The decision of the current instance
MfDoubleMatrix* currentDecision;
//Number of correctly classified instances in the current round.
//It is used for statistics especially on cross-validation.
int numCorrect;
说明如下:
- rate 和 mobp 用于设置每层的相应参数, 不同的层参数相同, 这个估计不用变.
- 激活函数现在每层都相同, 以后可以扩展下支持不同设置. 这样的话, activation 将变成一个字符串, 其长度为层数.
- currentOutput 和 currentDecision 均为长度为 numClasses 的向量, 而不是整数. 如 currentOutput = [0.5, 0.7, 0.6], currentDecision = [0, 1, 0]. 这样分类是正确的, 但依然有error.
3.2 成员函数
成员函数有不少.
//The default constructor. 没啥用
MfFullAnn();
//The Ann with given sizes and activation function
MfFullAnn(MfIntArray* paraSizes, char paraActivation, double paraRate, double paraMobp);
//Destructor
virtual ~MfFullAnn();
//Convert to string for display
string toString();
//Set the activation function for the given layer
void setActivationFunction(int paraLayer, char paraFunction);
//Setter
void setRate(double paraRate);
//Setter
void setMobp(double paraMobp);
//Reset weights and other variables.
void reset();
//Forward layer by layer
MfDoubleMatrix* forward(MfDoubleMatrix* paraInput);
//Back propagation
void backPropagation(MfDoubleMatrix* paraTarget);
//Train the network with only one instance
void train(MfDoubleMatrix* paraX, int paraY);
//Train with a dataset
void train(MfDoubleMatrix* paraX, MfIntArray* paraY);
//Test with an instance
bool test(MfDoubleMatrix* paraX, int paraY);
//Test with a dataset
double test(MfDoubleMatrix* paraX, MfIntArray* paraY);
//Get the number of correctly classified instances in the current round
int getNumCorrect();
//Show weight of the network, not including the offset
void showWeight();
//Code unit test
void unitTest();
//Training/testing test
void trainingTestingTest();
//Cross validation test
void crossValidationTest();
然而, 值得一提的只有两个.
3.2.1 forward 函数
MfDoubleMatrix* MfFullAnn::forward(MfDoubleMatrix* paraInput)
{
MfDoubleMatrix* tempData = paraInput;
for (int i = 0; i < numLayers; i ++)
{
tempData = layers[i]->forward(tempData);
}//Of for i
currentOutput->cloneToMe(tempData);
return currentOutput;
}//Of forward
逐层 forward, 把结果拷贝给 currentOutput. tempData 只是一个指针, 相应空间在各层内分配, 实际上为该层的 outputData 变量.
3.2.2 backPropagation 函数
void MfFullAnn::backPropagation(MfDoubleMatrix* paraTarget)
{
//误差.
paraTarget->subtractToMe(paraTarget, currentOutput);
//求导.
currentOutput->deriveToMe(currentOutput);
//传递到激活函数左边.
currentOutput->cwiseProductToMe(currentOutput, paraTarget);
MfDoubleMatrix* tempLayerErrors = currentOutput;
for (int i = numLayers - 1; i >= 0; i --)
{
tempLayerErrors = layers[i]->backPropagation(tempLayerErrors);
}//Of for i
}//Of backPropagation
说明如下:
- 计算 Y − Y ′ Y - Y' Y−Y′;
- 计算 Y ′ Y' Y′ 对应的导数, 对于 sigmoid 为 Y ′ ( 1 − Y ′ ) Y'(1 - Y') Y′(1−Y′);
- 计算 Y ′ ( 1 − Y ′ ) ( Y − Y ′ ) Y'(1 - Y')(Y - Y') Y′(1−Y′)(Y−Y′);
- 这里的计算都是点对点的, 即涉及的矩阵 (实际上为一维向量) 大小均相同;
- 做好预处理, 就一层一层向前反馈.
4. 小结
于是经典神经网络就搭建好了.
forward 时, 激活是每层向右的结束操作; backPropagation 时, 求导是每层的向左的结束操作. 也就是说, 两个方向对层的定义稍有不同. 为什么这样设计, 我暂时还没想清楚.