CNN卷积神经网络 C++实现。这个卷积神经网络比较特殊,由1个输入层、2个卷积层、1个全连接层、1个全连接的输出层构成,无采样层、池化层。
输入是一个29*29的图像矩阵,对应着841个输入单元。输出层有9个神经元,如果应该输出3,则下标为3的单元应该输出1,其它神经元输出-1。
测试部分是我自己写的,数据集来自《机器学习实战》—Peter Harrington,中的第二章,“手写识别系统”,这样就可以用纯C++实现了 :)
《机器学习实战》中的第二章,采用了k-近邻算法识别手写,正确率为98.8%,采用了CNN后,正确率为100%,但训练时间比较长。
CNN.h
// simplified view: some members have been omitted,
// and some signatures have been altered
// helpful typedef's
#include "vector"
#include "assert.h"
#include "math.h"
using namespace std;
class NeuralNetwork;
class NNLayer;
class NNNeuron;
class NNConnection;
class NNWeight;
typedef std::vector< NNLayer* > VectorLayers;
typedef std::vector< NNWeight* > VectorWeights;
typedef std::vector< NNNeuron* > VectorNeurons;
typedef std::vector< NNConnection > VectorConnections;
#define SIGMOID(x) (tanh(x))
#define DSIGMOID(x) (1-(SIGMOID(x))*(SIGMOID(x)))
#define UNIFORM_PLUS_MINUS_ONE ( (double)(2.0 * rand())/RAND_MAX - 1.0 ) //均匀随机分布
// 神经网络
class NeuralNetwork
{
public:
NeuralNetwork();
virtual ~NeuralNetwork();
//正向传播,计算输出
void Calculate();
//反向传播,调整权值
void Backpropagate();
vector m_input; //输入向量
vector m_desiredOutput; //理应输出的向量
vector m_actualOutput; //实际输出的向量
VectorLayers m_Layers; //存储指向每一层的指针
double m_etaLearningRate; //学习速率
};
// 层
class NNLayer
{
public:
NNLayer( char* str, NNLayer* pPrev = NULL );
virtual ~NNLayer();
//正向传播,计算输出
void Calculate();
//反向传播,调整权值
void Backpropagate( std::vector< double >& dErr_wrt_dXn /* in */,
std::vector< double >& dErr_wrt_dXnm1 /* out */,
double etaLearningRate );
char *m_layerName; //该层的名称
NNLayer* m_pPrevLayer; //存储前一层的指针,以获得输入
VectorNeurons m_Neurons; //存储指向该层每个神经元的指针
VectorWeights m_Weights; //存储连向该层的每个连接的权值
};
// 神经元
class NNNeuron
{
public:
NNNeuron( char* str );
virtual ~NNNeuron();
void AddConnection( int iNeuron, int iWeight ); //添加连接,(神经元下标, 权值下标)
void AddConnection( NNConnection const & conn );
char *m_neuronName; //当前神经元的名称
double output; //当前神经元的输出
VectorConnections m_Connections; //存储连向该神经元的所有连接,以获得该神经元的输入
};
// 连接
class NNConnection
{
public:
NNConnection(int neuron = ULONG_MAX, int weight = ULONG_MAX);
//virtual ~NNConnection();
int NeuronIndex; //神经元下标
int WeightIndex; //权值下标
};
// 权值
class NNWeight
{
public:
NNWeight( double val = 0.0 );
//virtual ~NNWeight();
double value; //权值
};
CNN.cpp
// simplified code
#include "CNN.h"
#include "iostream"
using namespace std;
NeuralNetwork::NeuralNetwork()
{
m_etaLearningRate = 0.001; // 学习速率
}
NeuralNetwork::~NeuralNetwork()
{
VectorLayers::iterator it;
for( it=m_Layers.begin(); itm_Neurons.begin();
// 每个输入对应一个神经元
int count = 0;
while( nit != (*lit)->m_Neurons.end() )
{
(*nit)->output = m_input[ count ];
nit++;
count++;
}
}
// 通过Calculate()迭代剩余层
for( lit++; litCalculate();
}
// 输出向量中存储最终结果
lit = m_Layers.end();
lit--; //最后一层,输出层
nit = (*lit)->m_Neurons.begin();
while( nit != (*lit)->m_Neurons.end() )
{
m_actualOutput.push_back((*nit)->output);
nit++;
}
}
//卷积神经网络中的误差反向传播
void NeuralNetwork::Backpropagate()
{
/*
误差反向传播,从最后一层一直到迭代到第一层
Err:整个神经网络的输出误差
Xn:第n层上的输出向量
Xnm1:前一层的输出向量
Wn:第n层的权值
Yn:第n层的激活值
F:激活函数 tanh
F':F的倒数 F'(Yn) = 1 - Xn^2
*/
VectorLayers::iterator lit = m_Layers.end() - 1;
std::vector< double > dErr_wrt_dXlast( (*lit)->m_Neurons.size() ); //标准误差关于“输出值”的偏导
std::vector< std::vector< double > > differentials;
int iSize = m_Layers.size();
differentials.resize( iSize );
int ii;
// differentials 存储标准误差 0.5*sumof( (actual-target)^2 ) 的偏导关于“输出值”的偏导
for ( ii=0; ii<(*lit)->m_Neurons.size(); ++ii )
{
dErr_wrt_dXlast[ ii ] =
m_actualOutput[ ii ] - m_desiredOutput[ ii ]; //实际输出 - 目标输出
}
// 存储dErr_wrt_dXlast
// 为剩余需要存储在differentials中的vector预留空间,并初始化为0
differentials[ iSize-1 ] = dErr_wrt_dXlast; // 上一次
for ( ii=0; iim_Neurons.size(), 0.0 );
}
/*
迭代计算除了第一层之外的剩余层,每一层都要反向传播误差,
并调整自己的权值
用 differentials[ ii ] 计算 differentials[ ii - 1 ]
*/
ii = iSize - 1;
for ( lit; lit>m_Layers.begin(); lit--)
{
(*lit)->Backpropagate( differentials[ ii ],
differentials[ ii - 1 ], m_etaLearningRate );
--ii;
}
differentials.clear();
}
////////////////////////////////////////////////////////////////////////////////////////////
NNLayer::NNLayer(char* str, NNLayer* pPrev)
{
m_layerName = str;
m_pPrevLayer = pPrev;
}
NNLayer::~NNLayer()
{
VectorWeights::iterator wit;
VectorNeurons::iterator nit;
for( nit=m_Neurons.begin(); nitvalue;
for ( cit++ ; citm_Neurons.size() );
dSum += ( m_Weights[ (*cit).WeightIndex ]->value ) *
( m_pPrevLayer->m_Neurons[ (*cit).NeuronIndex ]->output );
}
n.output = SIGMOID( dSum ); //当前神经元的输出
}
}
//每一层的误差反向传播
void NNLayer::Backpropagate( std::vector< double >& dErr_wrt_dXn /* in */,
std::vector< double >& dErr_wrt_dXnm1 /* out */,
double etaLearningRate )
{
/*
Err:整个神经网络的输出误差
Xn:第n层上的输出向量
Xnm1:前一层的输出向量
Wn:第n层的权值
Yn:第n层的激活值
F:激活函数 tanh
F':F的倒数 F'(Yn) = 1 - Xn^2
*/
int ii, jj, kk;
int nIndex;
double output;
vector< double > dErr_wrt_dYn( m_Neurons.size() );
double* dErr_wrt_dWn = new double[ m_Weights.size() ];
// 计算 : dErr_wrt_dYn = F'(Yn) * dErr_wrt_Xn,标准误差关于某个单元输入加权和的偏导
for (ii=0; iioutput;
dErr_wrt_dYn[ ii ] = DSIGMOID( output ) * dErr_wrt_dXn[ ii ];
}
// 计算 : dErr_wrt_Wn = Xnm1 * dErr_wrt_Yn,标准误差关于权重的偏导
// 对于这层中的每个神经元,通过前一层中与之相连的连接更新相关权值
VectorNeurons::iterator nit;
VectorConnections::iterator cit;
ii = 0;
for ( nit=m_Neurons.begin(); nitm_Neurons[ kk ]->output;
}
dErr_wrt_dWn[ (*cit).WeightIndex ] += dErr_wrt_dYn[ ii ] * output;
}
ii++;
}
// 计算 : dErr_wrt_Xnm1 = Wn * dErr_wrt_dYn,
// 计算这一层的每个神经元的dErr_wrt_dXnm1,
// 作为下一层计算 dErr_wrt_dXn 的输入
ii = 0;
for ( nit=m_Neurons.begin(); nitvalue;
}
}
ii++;
}
// 计算 : 更新权重
// 在这层中,使用了dErr_wrt_dW 和学习速率
double oldValue, newValue;
for ( jj=0; jjvalue;
newValue = oldValue - etaLearningRate * dErr_wrt_dWn[ jj ];
m_Weights[ jj ]->value = newValue;
}
}
/////////////////////////////////////////////////////////////////////////////////
NNNeuron::NNNeuron(char* str)
{
m_neuronName = str;
output = 0.0;
m_Connections.clear();
}
NNNeuron::~NNNeuron()
{
m_Connections.clear();
}
void NNNeuron::AddConnection( int iNeuron, int iWeight )
{
m_Connections.push_back( NNConnection( iNeuron, iWeight ) );
}
void NNNeuron::AddConnection( NNConnection const & conn )
{
m_Connections.push_back( conn );
}
//////////////////////////////////////////////////////////////////////////////
NNConnection::NNConnection(int iNeuron, int iWeight)
{
NeuronIndex = iNeuron;
WeightIndex = iWeight;
}
/////////////////////////////////////////////////////////////////////////////
NNWeight::NNWeight(double val)
{
val = 0.0;
}
test.cpp
#include "cnn.h"
#include "iostream"
#include "fstream"
#include "io.h"
#include "string"
#include "time.h"
#include "stdio.h"
using namespace std;
void buildCNN(NeuralNetwork &NN)
{
NNLayer* pLayer;
int ii, jj, kk;
double initWeight;
char label[100];
int icNeurons = 0;
// 第0层,输入层
// 创建神经元,和输入的数量相等
// 装有 29x29=841 像素的 vector,没有权值
pLayer = new NNLayer( "Layer00" );
NN.m_Layers.push_back( pLayer );
for ( ii=0; ii<841; ++ii )
{
sprintf(label, "Layer00_Neuron%04d_Num%06d", ii, icNeurons);
pLayer->m_Neurons.push_back( new NNNeuron(label) );
icNeurons++;
}
// 第一层:
// 是一个卷积层,有6个特征图,每个特征图大小为13x13,
// 特征图中的每个单元是由5x5的卷积核从输入层卷积而成。
// 因此,共有13x13x6 = 1014个神经元,(5x5+1)x6 = 156个权值
pLayer = new NNLayer( "Layer01", pLayer );
NN.m_Layers.push_back( pLayer );
for ( ii=0; ii<1014; ++ii )
{
sprintf(label, "Layer00_Neuron%04d_Num%06d", ii, icNeurons);
pLayer->m_Neurons.push_back( new NNNeuron(label) );
icNeurons++;
}
for ( ii=0; ii<156; ++ii )
{
initWeight = 0.05 * UNIFORM_PLUS_MINUS_ONE; //均匀随机分布
pLayer->m_Weights.push_back( new NNWeight( initWeight ) );
}
// 和前一层相连:这是难点
// 前一层是位图,大小为29x29
// 这层中的每个神经元都和特征图中的5x5卷积核相关,
// 每次移动卷积核2个像素
int kernelTemplate[25] = {
0, 1, 2, 3, 4,
29, 30, 31, 32, 33,
58, 59, 60, 61, 62,
87, 88, 89, 90, 91,
116,117,118,119,120 };
int iNumWeight;
int fm; // "fm" 代表 "feature map"
for ( fm=0; fm<6; ++fm)
{
for ( ii=0; ii<13; ++ii )
{
for ( jj=0; jj<13; ++jj )
{
// 26 是每个特征图的权值数量
iNumWeight = fm * 26;
NNNeuron& n = *( pLayer->m_Neurons[ jj + ii*13 + fm*169 ] );
n.AddConnection( ULONG_MAX, iNumWeight++ ); // 偏移量
for ( kk=0; kk<25; ++kk )
{
// 注意:最大下标为840
// 因为前一层中的神经元数量为841
n.AddConnection( 2*jj + 58*ii + kernelTemplate[kk], iNumWeight++ );
}
}
}
}
// 第二层:
// 这层是卷积层,有50个特征图。每个特征图大小为5x5,
// 特征图中的每个单元是由5x5的卷积核卷积前一层中的所有6个特征图而得,
// 因此,有5x5x50 = 1250个神经元,(5x5+1)x6x50 = 7800个权值
pLayer = new NNLayer( "Layer02", pLayer );
NN.m_Layers.push_back( pLayer );
for ( ii=0; ii<1250; ++ii )
{
sprintf(label, "Layer00_Neuron%04d_Num%06d", ii, icNeurons);
pLayer->m_Neurons.push_back( new NNNeuron( label ) );
icNeurons++;
}
for ( ii=0; ii<7800; ++ii )
{
initWeight = 0.05 * UNIFORM_PLUS_MINUS_ONE;
pLayer->m_Weights.push_back( new NNWeight( initWeight ) );
}
// 和前一层相连:这是难点
// 前一层的每个特征图都是大小为13x13的位图,共6个特征图.
// 这层中的每个5x5特征图中的每个神经元都和6个5x5的卷积核相关。
// 这层中的每个特征图都有6个不同的卷积核
// 每次将特征图移动2个像素
int kernelTemplate2[25] = {
0, 1, 2, 3, 4,
13, 14, 15, 16, 17,
26, 27, 28, 29, 30,
39, 40, 41, 42, 43,
52, 53, 54, 55, 56 };
for ( fm=0; fm<50; ++fm)
{
for ( ii=0; ii<5; ++ii )
{
for ( jj=0; jj<5; ++jj )
{
// 26 是每个特征图的权值数
iNumWeight = fm * 26;
NNNeuron& n = *( pLayer->m_Neurons[ jj + ii*5 + fm*25 ] );
n.AddConnection( ULONG_MAX, iNumWeight++ ); // bias weight
for ( kk=0; kk<25; ++kk )
{
// 注意:最大下标为1013
// 因为前一层中有1014个神经元
n.AddConnection( 2*jj + 26*ii +
kernelTemplate2[kk], iNumWeight++ );
n.AddConnection( 169 + 2*jj + 26*ii +
kernelTemplate2[kk], iNumWeight++ );
n.AddConnection( 338 + 2*jj + 26*ii +
kernelTemplate2[kk], iNumWeight++ );
n.AddConnection( 507 + 2*jj + 26*ii +
kernelTemplate2[kk], iNumWeight++ );
n.AddConnection( 676 + 2*jj + 26*ii +
kernelTemplate2[kk], iNumWeight++ );
n.AddConnection( 845 + 2*jj + 26*ii +
kernelTemplate2[kk], iNumWeight++ );
}
}
}
}
// 第3层:
// 这是个全连接层,有100个单元
// 由于是全连接层,这层中的每个单元都和前一层中所有的1250个单元相连
// 因此,有100神经元,100*(1250+1) = 125100个权值
pLayer = new NNLayer( "Layer03", pLayer );
NN.m_Layers.push_back( pLayer );
for ( ii=0; ii<100; ++ii )
{
sprintf(label, "Layer00_Neuron%04d_Num%06d", ii, icNeurons);
pLayer->m_Neurons.push_back( new NNNeuron( label ) );
icNeurons++;
}
for ( ii=0; ii<125100; ++ii )
{
initWeight = 0.05 * UNIFORM_PLUS_MINUS_ONE;
pLayer->m_Weights.push_back( new NNWeight( initWeight ) );
}
// 和前一层相连:全连接
iNumWeight = 0; // 这层中,权值不共享
for ( fm=0; fm<100; ++fm )
{
NNNeuron& n = *( pLayer->m_Neurons[ fm ] );
n.AddConnection( ULONG_MAX, iNumWeight++ ); // 偏移量
for ( ii=0; ii<1250; ++ii )
{
n.AddConnection( ii, iNumWeight++ );
}
}
// 第4层,最后一层:
// 这是个全连接层,有10个单元。
// 由于是全连接层,每个神经元都和前一层中的所有100个神经元相连
// 因此,有10个神经元,10*(100+1)=1010个权值
pLayer = new NNLayer( "Layer04", pLayer );
NN.m_Layers.push_back( pLayer );
for ( ii=0; ii<10; ++ii )
{
sprintf(label, "Layer00_Neuron%04d_Num%06d", ii, icNeurons);
pLayer->m_Neurons.push_back( new NNNeuron( label ) );
icNeurons++;
}
for ( ii=0; ii<1010; ++ii )
{
initWeight = 0.05 * UNIFORM_PLUS_MINUS_ONE;
pLayer->m_Weights.push_back( new NNWeight( initWeight ) );
}
// 和前一层相连:全连接
iNumWeight = 0; // 这层中的权值不共享
for ( fm=0; fm<10; ++fm )
{
NNNeuron& n = *( pLayer->m_Neurons[ fm ] );
n.AddConnection( ULONG_MAX, iNumWeight++ ); // 偏移量
for ( ii=0; ii<100; ++ii )
{
n.AddConnection( ii, iNumWeight++ );
}
}
}
void img2vector(string filename, vector &input)
{
ifstream fin;
char file[100];
strcpy(file, (char*)filename.data());
fin.open(file);
char data[100];
int d;
for(int i=0; i<29; i++)
{
fin.getline(data, 100);
for(int j=0; j<29; j++)
{
d = data[j] - '0';
input.push_back((double)d);
}
}
fin.close();
}
void getFiles(string path, vector& files, vector& files1)
{
//文件句柄
long hFile = 0;
//文件信息
struct _finddata_t fileinfo;
string p;
if ((hFile = _findfirst(p.assign(path).append("/*.txt").c_str(), &fileinfo)) != -1)
{
do
{
//加入列表
files.push_back(p.assign(path).append("/").append(fileinfo.name));
files1.push_back(fileinfo.name);
} while( _findnext( hFile, &fileinfo ) == 0 );
_findclose(hFile);
}
}
int main()
{
//获取文件
int i, j, k;
vector file, file1;
getFiles("./trainingDigits", file, file1);
int n = file.size(); //训练文件夹下的文件数
vector label;
char str[10];
for(i=0; i max)
{
max = CNN.m_actualOutput[i];
maxi = i;
}
}
if(maxi == label[j]) //如果结果正确
right++;
}
cout << "正确个数:" << right << endl;
cout << "总个数:" << n << endl;
cout << "正确率:" << (double)right/(double)n << endl; //正确率
return 0;
}