第一次写机器学习的文章。学完反向传播(BP算法)后做一个小实验来巩固一下,从最基本的实现到最后的优化,实验过程中遇到很多坑,比如超参数的设定,比如每种任务适合的输出函数和相应的损失函数。一度因为选择不恰当的学习率,神经元数目和激活函数而训练出人工智障。代码采用纯C/C++完成,未采用向量运算。本文不过多讨论算法原理方面内容,主要用于记录实验过程。
一. 在实现手写数字识别之前,先练习一个小任务,用神经网络学习最简单的a+b规则。
搭建如图所示的神经网络,学习计算两个数的和,所以输入层的节点数为2,设置隐藏层的神经元数目为40,激活函数为Sigmod函数,输出层为单个神经元。因为这是一个用神经网络解决回归问题的任务,输出层节点的激活函数可以使用阶跃函数,也可以不设置。为了方便起见未设置激励函数,即y=f(
X+b)=
X+b。
当然也可以用于拟合非线性方程,理论上足够数量的神经元可以拟合任意实数范围内的方程。
当神经网络用于分类问题时,输出层可以采取补不同的激活函数。
Sigmod函数可用于二分类问题,Softmax函数可用于多分类问题。
BP算法基于链式求导法则,下面仅给出BP算法的相关公式,不予证明:
代价函数采用输出层的误差平方:
,t代表标准输出,y代表预测值 用随机梯度下降算法进行权重的更新:
计算输出层的梯度及权重的训练,
,
计算隐藏层的梯度及权重的训练,
,
,a为隐藏层的输出。
此外,对数据进行归一化的处理,将数据处理到0~1的范围内,否则可能出现收敛过慢或无法收敛的情况。
#include <bits/stdc++.h>
#define in 2
#define out 1
#define hide 40
using namespace std ;
double a[2*hide];//隐藏层输出
double y=0;//输出层输出
double w1[2*hide][2*hide],b1[2*hide];//隐藏层权重
double w2[2*hide][2*hide],b2=0;//输出层权重
double delta,delta1[2*hide];//误差项
double x[1010][3];//数据集
double label[1010];//标签
double maxx=0,maxs=0;
double f(double x)
{
return 1.0/(1.0+exp(-x));
}
void data()
{
for(int i=0;i<1000;i++)
{
x[i][0]=rand()%10000;
x[i][1]=rand()%10000;
label[i]=x[i][0]+x[i][1];
}
for(int i=0;i<1000;i++)
{
x[i][2]=1.0;
x[i][0]/=10000;
x[i][1]/=10000;
label[i]/=20000;
}
}
void Network()
{
memset(a,0,sizeof(a));
memset(b1,0,sizeof(b1));
/*freopen("out.txt","r",stdin);
for(int i=0;i<hide;i++) scanf("%lf",&w1[i][0]);
for(int i=0;i<hide;i++) scanf("%lf",&w1[i][1]);
for(int i=0;i<hide;i++) scanf("%lf",&b1[i]);
for(int i=0;i<hide;i++) scanf("%lf",&w2[0][i]);
scanf("%lf",&b2);*/
for(int i=0;i<hide;i++)
for(int j=0;j<hide;j++)
w1[i][j]=w2[i][j]=0.5;
}
void cp(int t)
{
y=0;
for(int i=0;i<hide;i++)
a[i]=f(w1[i][0]*x[t][0]+w1[i][1]*x[t][1]+b1[i]);//隐藏层前向输出
for(int i=0;i<hide;i++) y+=w2[0][i]*a[i];//输出层前向输出
y+=b2;
}
void bp(int t)
{
delta=(label[t]-y);
for(int i=0;i<hide;i++) delta1[i]=a[i]*(1.0-a[i])*w2[0][i]*delta;
for(int i=0;i<hide;i++) w2[0][i]+=0.1*delta*a[i];b2+=0.2*delta;
for(int i=0;i<hide;i++)
{
w1[i][0]+=0.1*delta1[i]*x[t][0];
w1[i][1]+=0.1*delta1[i]*x[t][1];
b1[i]+=0.2*delta1[i];
}
}
void train()
{
for(int k=1;k<=1000;k++)
{
double err=0;
for(int i=0;i<1000;i++)
{
cp(i);
err+=fabs(label[i]-y);
bp(i);
}
if(k%200==0)printf("%.5f\n",err);
}
}
void test()
{
for(int i=0;i<=9;i++)
{
x[1000][0]=1000.0*i/10000;x[1000][1]=1000.0*i/10000;
cp(1000);
printf("预测值:%.5f 实际值:%d\n",y*10000,2000*i);
}
}
int main()
{
a[hide]=1.0;
data();
Network();
train();
test();
freopen("out.txt","w",stdout);
for(int i=0;i<hide;i++) printf("%.5f ",w1[i][0]);
for(int i=0;i<hide;i++) printf("%.5f ",w1[i][1]);
for(int i=0;i<hide;i++) printf("%.5f ",b1[i]);
for(int i=0;i<hide;i++) printf("%.5f ",w2[0][i]);
printf("%.5f",b2);
return 0;
}
实验结果:
二. MNIST手写数字识别实战
MNIST数据集为大小为28*28*3的图像,输入神经元有784个,输出层为10个神经元。此任务为一个多分类任务,所以我们采用softmax函数作为输出层的激励函数,权重更新方法会在稍后给出。在不设置隐藏层的情况下,训练几分钟后的神经网络在测试集上达到88%左右的准确率。
softmax的输出为属于各个标签的概率,交叉熵损失函数基于多项式分布确定,此处不多做讨论。
softmax采用交叉熵作为损失函数
softmax层的输出:
交叉熵损失函数:
。计算梯度为:
利用梯度下降算法求解即可。
同样的将图像进行归一化处理,使每个像素点归一化到0~1的范围之内。
使用softmax函数时出现溢出情况,可以采取的方法是将所有输出值减去最大值,可同时避免上溢出和下溢出。
#include <bits/stdc++.h>
#define in 784
#define out 10
using namespace std ;
double data_out[50];//输出层输出
double w[50][1000],b[50];//输出层权重
double delta[50];//误差项
vector<double>labels;
vector<vector<double> >images;//训练集
vector<double>labels1;
vector<vector<double> >images1;//测试集
void test();
int ReverseInt(int i)
{
unsigned char ch1, ch2, ch3, ch4;
ch1 = i & 255;
ch2 = (i >> 8) & 255;
ch3 = (i >> 16) & 255;
ch4 = (i >> 24) & 255;
return((int)ch1 << 24) + ((int)ch2 << 16) + ((int)ch3 << 8) + ch4;
}
void read_Mnist_Label(string filename, vector<double>&labels)
{
ifstream file;
file.open("train-labels.idx1-ubyte", ios::binary);
if (file.is_open())
{
int magic_number = 0;
int number_of_images = 0;
file.read((char*)&magic_number, sizeof(magic_number));
file.read((char*)&number_of_images, sizeof(number_of_images));
magic_number = ReverseInt(magic_number);
number_of_images = ReverseInt(number_of_images);
cout << "magic number = " << magic_number << endl;
cout << "number of images = " << number_of_images << endl;
for (int i = 0; i < number_of_images; i++)
{
unsigned char label = 0;
file.read((char*)&label, sizeof(label));
labels.push_back((double)label);
}
}
}
void read_Mnist_Images(string filename, vector<vector<double> >&images)
{
ifstream file("train-images.idx3-ubyte", ios::binary);
if (file.is_open())
{
int magic_number = 0;
int number_of_images = 0;
int n_rows = 0;
int n_cols = 0;
unsigned char label;
file.read((char*)&magic_number, sizeof(magic_number));
file.read((char*)&number_of_images, sizeof(number_of_images));
file.read((char*)&n_rows, sizeof(n_rows));
file.read((char*)&n_cols, sizeof(n_cols));
magic_number = ReverseInt(magic_number);
number_of_images = ReverseInt(number_of_images);
n_rows = ReverseInt(n_rows);
n_cols = ReverseInt(n_cols);
cout << "magic number = " << magic_number << endl;
cout << "number of images = " << number_of_images << endl;
cout << "rows = " << n_rows << endl;
cout << "cols = " << n_cols << endl;
for (int i = 0; i < number_of_images; i++)
{
vector<double>tp;
for (int r = 0; r < n_rows; r++)
{
for (int c = 0; c < n_cols; c++)
{
unsigned char image = 0;
file.read((char*)&image, sizeof(image));
tp.push_back(image);
}
}
images.push_back(tp);
}
}
}
void read_Mnist_Label1(string filename, vector<double>&labels)
{
ifstream file;
file.open("t10k-labels.idx1-ubyte", ios::binary);
if (file.is_open())
{
int magic_number = 0;
int number_of_images = 0;
file.read((char*)&magic_number, sizeof(magic_number));
file.read((char*)&number_of_images, sizeof(number_of_images));
magic_number = ReverseInt(magic_number);
number_of_images = ReverseInt(number_of_images);
for (int i = 0; i < number_of_images; i++)
{
unsigned char label = 0;
file.read((char*)&label, sizeof(label));
labels.push_back((double)label);
}
}
}
void read_Mnist_Images1(string filename, vector<vector<double> >&images)
{
ifstream file("t10k-images.idx3-ubyte", ios::binary);
if (file.is_open())
{
int magic_number = 0;
int number_of_images = 0;
int n_rows = 0;
int n_cols = 0;
unsigned char label;
file.read((char*)&magic_number, sizeof(magic_number));
file.read((char*)&number_of_images, sizeof(number_of_images));
file.read((char*)&n_rows, sizeof(n_rows));
file.read((char*)&n_cols, sizeof(n_cols));
magic_number = ReverseInt(magic_number);
number_of_images = ReverseInt(number_of_images);
n_rows = ReverseInt(n_rows);
n_cols = ReverseInt(n_cols);
for (int i = 0; i < number_of_images; i++)
{
vector<double>tp;
for (int r = 0; r < n_rows; r++)
{
for (int c = 0; c < n_cols; c++)
{
unsigned char image = 0;
file.read((char*)&image, sizeof(image));
tp.push_back(image);
}
}
images.push_back(tp);
}
}
}
void softmax(double data_out[])
{
double sum=0.0;
for(int i=0;i<out;i++) sum+=exp(data_out[i]);
for(int i=0;i<out;i++) data_out[i]=exp(data_out[i])/sum;
}
void Network()
{
memset(data_out,0,sizeof(data_out));
freopen("out.txt","r",stdin);
for(int i=0;i<out;i++)
{
for(int j=0;j<in;j++)
{
scanf("%lf",&w[i][j]);
}
}
scanf("%lf",&b);
/*for(int i=0;i<out;i++)
for(int j=0;j<in;j++)
w[i][j]=-0.5;*/
}
void cp(int t)
{
memset(data_out,0,sizeof(data_out));
double maxx=-100000000;
for(int i=0;i<out;i++)
{
for(int j=0;j<in;j++)
{
data_out[i]+=w[i][j]*images[t][j];
}
data_out[i]+=b[i];
maxx=max(maxx,data_out[i]);
}
//printf("%.5f\n",maxx);
for(int i=0;i<out;i++)
{
data_out[i]-=maxx;
}
softmax(data_out);
/*for(int i=0;i<out;i++)
printf("%.5f ",data_out[i]);
printf("\n");*/
}
int test_out(int t)
{
memset(data_out,0,sizeof(data_out));
double maxx=-100000000;
for(int i=0;i<out;i++)
{
for(int j=0;j<in;j++)
{
data_out[i]+=w[i][j]*images1[t][j];
}
data_out[i]+=b[i];
maxx=max(maxx,data_out[i]);
}
for(int i=0;i<out;i++)
{
data_out[i]-=maxx;
}
softmax(data_out);
int ans=-1,sign=-1;
for(int i=0;i<out;i++)
{
if(data_out[i]>sign)
{
sign=data_out[i];
ans=i;
}
}
return ans;
}
void bp(int t)
{
for(int i=0;i<out;i++)
{
if(i==(int)labels[t]) delta[i]=data_out[i]-1.0;
else delta[i]=data_out[i];
//printf("%.5f ",delta[i]);
}
//printf("\n");
for(int i=0;i<in;i++)
{
for(int j=0;j<out;j++)
w[j][i]-=0.3*delta[j]*images[t][i];
}
for(int j=0;j<out;j++) b[j]-=0.3*delta[j];
}
void train()
{
for(int k=1;k<=100;k++)
{
double err=0;
for(int i=0;i<60000;i++)
{
cp(i);
err-=log(data_out[(int)labels[i]]);
bp(i);
}
printf("step: %d loss: %.5f\n",k,err/10000.0);//每次记录一遍数据集的平均误差
if(k%5==0) test();
}
}
void test()
{
int sum=0;
for(int i=0;i<10000;i++)
{
int ans=test_out(i);
int label=int(labels1[i]);
if(ans==label) sum++;
//printf("%d %d\n",ans,label);
}
//printf("\n");
//for(int i=0;i<out;i++) printf("%.5f ",data_out[i]);
//printf("\n");
printf("precision: %.6f\n",1.0*sum/10000);
}
int main()
{
read_Mnist_Label("t10k-labels.idx1-ubyte", labels);
read_Mnist_Images("t10k-images.idx3-ubyte", images);
read_Mnist_Label1("t10k-labels.idx1-ubyte", labels1);
read_Mnist_Images1("t10k-images.idx3-ubyte", images1);//读取mnist数据集
for (int i = 0; i < images1.size(); i++)
{
for (int j = 0; j < images1[0].size(); j++)
{
images1[i][j]/=255.0;
}
}
for (int i = 0; i < images.size(); i++)
{
for (int j = 0; j < images[0].size(); j++)
{
images[i][j]/=255.0;
}
}
//归一化处理图像
Network();//网络初始化
test();
train();
freopen("out.txt","w",stdout);
for(int i=0;i<out;i++)
{
for(int j=0;j<in;j++)
{
printf("%.5f ",w[i][j]);
}
}
//保存参数
printf("%.5f",b);
return 0;
}
可以进行优化的几点想法:
1.添加隐藏层可以增强神经网络的拟合能力,当然相应的会增加计算的消耗。
2.可以采用dropout,增强模型的鲁棒性,避免过拟合。