Dlib构建神经网络
-
构建简单的LeNet CNN
-
定义LeNet
-
概况的说,网络的定义包括3个部分。损失层,一堆计算层,然后是输入层,你在下面的网络定义中可以看到这些组件。
-
输入层
这里所说的输入层,是网络期望被赋予matrix<unsigned char>矩阵对象作为输入。通常,在这里你可以用dlib 图像或是矩阵类型,说是甚至是用定制输入层定义你自己的类型。
-
中间层
中间层定义了网络将做的运算来转换输入到任何我们想要的输出。这里我们通过多个卷积,Relu单元,最大池化运算来运行图像,最后是全连接层,它将整个数据转换为10个数。
-
损失层
最后,损失层定义了网路输出间的关系,我们的10个数,并且在我们的数据集中的标签。因为我们选择loss_multiclass_log它意思是我们想要用我们的网络做多级分类。此外,网络输出的数据(例如10)是可能标签的数字。无论哪一个网络输出是最大的就是预测的标签。举个例子,如果第一个网络的输出是最大的,预测的数字就是0,如果最后一个网络输出是最大的于是预测的数据就是9。
using net_type = loss_multiclass_log<
fc<10,
relu<fc<84,
relu<fc<120,
max_pool<2,2,2,2,relu<con<16,5,5,1,1,
max_pool<2,2,2,2,relu<con<6,5,5,1,1,
input<matrix<unsignedchar>>
>>>>>>>>>>>>;
这个net_type定义了整个网络架构,例如,relu<fc<84,SUBNET>>块意思是我们从子网络中的得到输出,通过一个包含84个输出的全连接层传递它,然后运用ReLU。类似的,max_pool<2,2,2,2,relu<con<16,5,5,1,1,SUBNET>>> 意思是我们运用16个5*5滤波器大小和1*1步幅的卷积到子网络的输出,然后运用ReLU,紧接着执行2*2窗口和2*2步幅的最大池化。
这样一来,我们就可以创建一个网络实例。
net_type net;
然后用MNIST数据集训练这个网络。下面的代码用小批量随机梯度下降法以初始化学习效率为0.01来完成训练。
dnn_trainer<net_type>trainer(net);
trainer.set_learning_rate(0.01);
trainer.set_min_learning_rate(0.00001);
trainer.set_mini_batch_size(128);
trainer.be_verbose();
由于DNN训练需要花费很长的时间,所以我们可以让trainer每20秒保存它的状态到一个名叫"minist_sync"的文件。这样,如果我们终止掉这个程序并且再次启动它,就会从它停止的地方开始,而不是重新开始训练。这是因为,当程序重启,调用set_synchronization_file(),如果mnist_sync文件存在,将自动从mnist_sync文件中装载设置。
trainer.set_synchronization_file("mnist_sync", std::chrono::seconds(20));
最后,这一行就开始训练了,默认的,它会以我们指定的学习效率来运行SGD直到损失停止下降。于是学习效率降低10倍并继续运行直到损失再次停止下降。持续这样做直到学习效率已经降低至上面定义的最小学习效率以下或是最大的被执行时间(默认为10000).
trainer.train(training_images, training_labels);
在这时候我们的网络对象应该已经学习到如果分类MNIST图像。但是在我们尝试之前让我们保存它到硬盘。注意,因为训练器已经通过网络运行图像,网络将会有最后它处理的最后一批图像的一组状态(例如每一层的输出)。于是我们不关心保存这一类的数据到银盘,我可以告诉网络忘记这类瞬态数据,所以我们的文件会比较小。在保存数据前我们可以通过"cleaning"网络来实现。
net.clean();
serialize("mnist_network.dat")<< net;
现在如果我们接着想要从硬盘中调用这个网络,我们可以简单的使用以下的函数实现:
// deserialize("mnist_network.dat") >> net;
现在让我们通过网络来运行训练图像。通过这个申明运行所有的图像并且让损失层来转换网络的原始输出到标签。对我们而言,这些标签是0到9之间的数据。
std::vector<unsignedlong> predicted_labels=net(training_images);
int num_right =0;
int num_wrong =0;
// And then let's see if it classified them correctly.
for(size_t i=0; i< training_images.size();++i)
{
if(predicted_labels[i]== training_labels[i])
++num_right;
else
++num_wrong;
}
cout << "training num_right:"<< num_right << endl;
cout << "training num_wrong:"<< num_wrong << endl;
cout << "training accuracy:"<< num_right/(double)(num_right+num_wrong)<< endl;
让我们在看看网络是否可以正确的将测试图像分类,因为MNIST是一个简单的数据集,所以我们应该看到至少有99%的正确率。
predicted_labels =net(testing_images);
num_right =0;
num_wrong =0;
for(size_t i=0; i< testing_images.size();++i)
{
if(predicted_labels[i]== testing_labels[i])
++num_right;
else
++num_wrong;
}
cout << "testing num_right:"<< num_right << endl;
cout << "testing num_wrong:"<< num_wrong << endl;
cout << "testing accuracy:"<< num_right/(double)(num_right+num_wrong)<< endl;
}
catch(std::exception& e)
{
cout << e.what()<< endl;
}
-
构建复杂的神经网络
让我们从展示你如何可以方便的定义大型和复杂的网络开始。做这项工作的最重要工作是是C++的别名模板。这让我们可以定义一个有一组其他层组合而成的新层类型。这将为更多复杂的网络组成构建块。
所以我们从定义一个残量网络(residual network)的构建块开始。我们将残量网络分解成一些别名声明。首先我们定义核心块。
这里我们已经将在BN层(名义上的某种批量正则化)中的"块"层参数化,滤波器输出数字N,和块运算的步幅。
template <
int N,
template <typename> classBN,
int stride,
typename SUBNET
>
using block = BN<con<N,3,3,1,1,relu<BN<con<N,3,3,stride,stride,SUBNET>>>>>;
下一步,我们需要定义的在残量网络论文中用到的跳层机制。他们用添加输入张量到每个块的输出的方式来创建他们的块。所以我们定义一个声明别名,该声明取得一个块并且用这个跳和添加结构包装它。
注意标签层,这一层不做任何的计算,它单独地存在,所以其他层可以参考它,既然这样,add_prev1层寻找tag1层并且将tag输出和添加它到add_prev1层的输入。这个结合允许我们执行跳过和残量风格网络,在这个申明中我们已经设置了块步幅为1,这样做的意义在下面解释。
template <
template <int,template<typename>class,int,typename> class block,
int N,
template<typename>classBN,
typename SUBNET
>
using residual = add_prev1<block<N,BN,1,tag1<SUBNET>>>;
一些残量块做降采样,他们用步幅2替代1来实现,然而,当做降采样时我们也需要关心降采样从原始输入到输出的网络部分或是没有意义的像素(网络仍然运行,但是结果却没那么好)。所以这里我们定义一个残量的降采样版本。在里面,我们利用skip1层,这层仅仅输出tag1层的任何输出,因此,skip1层(在dlib里也有skip2,skip3等等)允许你创建网络结构分支。
// residual_down creates a network structure like this:
/*
input from SUBNET
/ \
/ \
block downsample(using avg_pool)
\ /
\ /
add tensors (using add_prev2 which adds the output of tag2 with avg_pool's output)
|
output
*/
template<
template<int,template<typename>class,int,typename> classblock,
int N,
template<typename>classBN,
typename SUBNET
>
using residual_down = add_prev2<avg_pool<2,2,2,2,skip1<tag2<block<N,BN,2,tag1<SUBNET>>>>>>;
现在在我们例子中我们可以定义4个不同的残量块。前两个是非降采样的残量块在最后两个样本中。同时,当 res和res_down 用批量正则化的时候ares和ares_down已经用简单的仿射层代替了批量正则化。当测试我们的网络的时候我们将用这些层的仿射版本。
template<typename SUBNET>using res= relu<residual<block,8,bn_con,SUBNET>>;
template<typename SUBNET>using ares= relu<residual<block,8,affine,SUBNET>>;
template<typename SUBNET>using res_down= relu<residual_down<block,8,bn_con,SUBNET>>;
template<typename SUBNET>using ares_down= relu<residual_down<block,8,affine,SUBNET>>;
现在我们已经有了这些方便的别名,所以我们可以不用打很多代码就可以轻松定义定义一个残量网络。注意重复图层的使用,指定的层类型允许我们键入类型repeat<9,res,SUBNET> 代替res<res<res<res<res<res<res<res<res<SUBNET>>>>>>>>>.
这也避免了当创建大型网络时候编译器抱怨超级深的模板嵌套。
constunsignedlong number_of_classes=10;
using net_type = loss_multiclass_log<fc<number_of_classes,
avg_pool_everything<
res<res<res<res_down<
repeat<9,res, // repeat this layer 9 times
res_down<
res<
input<matrix<unsignedchar>>
>>>>>>>>>>;
最后,让我们用参数化的Relu单元代替普通的Relu来定义一个残量网络构建块。
template<typename SUBNET>
using pres = prelu<add_prev1<bn_con<con<8,3,3,1,1,prelu<bn_con<con<8,3,3,1,1,tag1<SUBNET>>>>>>>>;
intmain(int argc,char** argv)try
{
if(argc!=2)
{
cout << "This example needs the MNIST dataset to run!"<< endl;
cout << "You can get MNIST from http://yann.lecun.com/exdb/mnist/"<< endl;
cout << "Download the 4 files that comprise the dataset, decompress them, and"<< endl;
cout << "put them in a folder. Then give that folder as input to this program."<< endl;
return1;
}
std::vector<matrix<unsignedchar>> training_images;
std::vector<unsignedlong> training_labels;
std::vector<matrix<unsignedchar>> testing_images;
std::vector<unsignedlong> testing_labels;
load_mnist_dataset(argv[1], training_images, training_labels, testing_images, testing_labels);
// dlib uses cuDNN under the covers. One of the features of cuDNN is the
// option to use slower methods that use less RAM or faster methods that use
// a lot of RAM. If you find that you run out of RAM on your graphics card
// then you can call this function and we will request the slower but more
// RAM frugal cuDNN algorithms.
Dlib底层用的是cuDNN,cuDNN其中的一个特性是选择使用少RAM的慢方法和用多RAM的快方法。如果你发现你你耗尽了你显卡上的内存RAM,你就可以调用这个函数并且请求更多的节约RAM的cuDNN算法
set_dnn_prefer_smallest_algorithms();
创建如上面定义的一个网络。这个网络将产生10个输出因为这就我们的定义的net_type. 然而,fc 层包含在运行时他们产生改变的数量。
net_type net
所以如果你想要用同一个网络但是却不管在运行输出的数量你可以像这样做:
net_type net2(num_fc_outputs(15));
现在我们想象我们想要用prelu层替换一些relu层,我们可以这样做:
using net_type2 = loss_multiclass_log<fc<number_of_classes,
avg_pool_everything<
pres<res<res<res_down<// 2 prelu layers here
tag4<repeat<9,pres,// 9 groups, each containing 2 prelu layers
res_down<
res<
input<matrix<unsignedchar>>
>>>>>>>>>>>;