描述

dlib针对卷积神经网络的有很好的支持,实现的卷积层、池化层、BN层等关键网络层。

  • 卷积层:dlib::con(add_layer<con_<num_filters,nr,nc,stride_y,stride_x>, SUBNET>)
  • 最大池化层:dlib::max_pool(add_layer<max_pool_<nr,nc,stride_y,stride_x>, SUBNET>)
  • 平均池化层:dlib::avg_pool(add_layer<avg_pool_<nr,nc,stride_y,stride_x>, SUBNET>)
  • 批量规范化:dlib::bn_conv(add_layer<bn_<CONV_MODE>,SUBNET>)

下面复现lenet5的训练

lenet5

lenet5是用来识别手写数字的模型,训练数据源于mnist(可以在网上下载)

加载数据

这里直接加载图片

bool loadMnistImages(const string &fpath, vector<dlib::matrix<unsigned char>> &vec_imgs, vector<unsigned long> &vec_labs)
{
    dlib::directory dir(fpath);
    vector<dlib::file> vec_files = dir.get_files();
    for (dlib::file f : vec_files)
    {
        string full_name = f.full_name();
        string fname = f.name();
        if (fname.length() > 4 && fname.find(".jpg") != fname.length() - 4)
            continue;
        // 加载数据
        dlib::matrix<dlib::rgb_pixel> img;
        unsigned char gray_img[28 * 28];
        dlib::load_image(img, full_name);
        int i = 0;
        for (dlib::rgb_pixel rgb : img)
        {
            gray_img[i] = rgb.red; // 因为是灰度图,r/g/b取任意一个就行
            i++;
        }
        dlib::matrix<unsigned char> mtx_img;
        mtx_img.set_size(28, 28);
        memcpy((char *)&mtx_img(0, 0), gray_img, 28 * 28);
        vec_imgs.push_back(mtx_img);
        vec_labs.push_back(getFileLable(fname));
    }
}
int getFileLable(const string &fname)
{
    // train_0_5.jpg
    string labstr = fname.substr(fname.find_last_of("_") + 1, 1);
    return atoi(labstr.c_str());
}

也可以加载mnist数据集,这种方式更方便些,可以参考https://www.dlib.net/dnn_introduction_ex.cpp.html

定义模型

这里对模型进行改造一下,因为用于训练的图片大小为28x28(原模型训练使用的图片大小为32x32),这里要把卷积核调整为3x3。

// 定义模型结构
using lenet5_type = dlib::loss_multiclass_log<dlib::fc<10,
                            dlib::relu<dlib::fc<84,
                            dlib::relu<dlib::fc<120,
                            dlib::max_pool<2, 2, 2, 2, dlib::relu<dlib::con<16, 3, 3, 1, 1, 
                            dlib::max_pool<2, 2, 2, 2, dlib::relu<dlib::con<6, 3, 3, 1, 1, 
                            dlib::input<dlib::matrix<unsigned char>>>>>>>>>>>>>>;

训练模型

设置完相关参数就可以训练了(由于没有GPU加速,训练过程相对漫长);这里没有设置最大训练轮数,而是设置最小learning_rate当训练过程中learning_rate小于min_learning_rate时,结束训练。

lenet5_type net;
dlib::dnn_trainer<lenet5_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();
trainer.set_synchronization_file("mnist_sync", std::chrono::seconds(20));// 每过20秒保存一下训练状态
trainer.train(train_datas, train_labels);
net.clean();
dlib::serialize("lenet5_network.dat") << net;
dlib::net_to_xml(net,"lenet5.xml"); // 另外一种保存模型的方式

trainer.set_synchronization_file(“mnist_sync”, std::chrono::seconds(20))的目的是定时保存训练状态,防止训练过程异常中断导致已有的训练数据丢失。

可以通过get_synchronization_file加载最后保存的训练状态,继续完成训练目的。

完整代码

void cnnLeNet5(const string &train_path, const string &test_path)
{
    vector<dlib::matrix<unsigned char>> train_datas;
    vector<unsigned long> train_labels;
    vector<dlib::matrix<unsigned char>> test_datas;
    vector<unsigned long> test_labels;
    if (!(loadMnistImages(train_path, train_datas, train_labels) &&
          loadMnistImages(test_path, test_datas, test_labels)))
    {
        cout << "load mnist images failed!"<<endl;
        return;
    }
    cout << "load mnist images successful!"<<endl;
    // 定义模型结构
    using lenet5_type = dlib::loss_multiclass_log<dlib::fc<10,
                                                           dlib::relu<dlib::fc<84,
                                                           dlib::relu<dlib::fc<120,
                                                           dlib::max_pool<2, 2, 2, 2, dlib::relu<dlib::con<16, 3, 3, 1, 1, 
                                                           dlib::max_pool<2, 2, 2, 2, dlib::relu<dlib::con<6, 3, 3, 1, 1, 
                                                           dlib::input<dlib::matrix<unsigned char>>>>>>>>>>>>>>;
    lenet5_type net;
    dlib::dnn_trainer<lenet5_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();
    trainer.set_synchronization_file("mnist_sync", std::chrono::seconds(20));
    trainer.train(train_datas, train_labels);
    net.clean();
    dlib::serialize("lenet5_network.dat") << net;
    dlib::net_to_xml(net,"lenet5.xml"); // 另外一种保存模型的方式
    vector<unsigned long> predict_train = net(train_datas);
    int ok_count = 0;
    int error_count = 0;
    for (int i = 0; i < train_labels.size(); i++)
    {
        if (train_labels.at(i) == predict_train.at(i))
            ok_count += 1;
        else
            error_count += 1;
    }
    cout<<"train accurary :"<<ok_count / static_cast<double>(ok_count + error_count)<<endl;
    vector<unsigned long> predict_test = net(test_datas);
    ok_count = 0;
    error_count = 0;
    for (int i = 0; i < test_labels.size(); i++)
    {
        if (test_labels.at(i) == predict_test.at(i))
            ok_count += 1;
        else
            error_count += 1;
    }
    cout<<"test accurary :"<<ok_count / static_cast<double>(ok_count + error_count)<<endl;
}

最终训练结果

train accurary :0.999717
test accurary :0.9913

ResNet

深度残差网络是 Kaiming He et al. 提出的,一种运用了短路连接的神经网络形式。深度残差网络本身并没有一个固定的结构与参数,这使得深度残差网络非常灵活,可以有效的插入其他模型而提高模型表现。

常见的几种ResNet的具体结构:

在这里插入图片描述

根据Block类型,可以将ResNet分为两类:

  • 一种基于BasicBlock,浅层网络ResNet18, 34都由BasicBlock搭成;
  • 一种基于Bottleneck,深层网络ResNet50, 101, 152乃至更深的网络,都由Bottleneck搭成。

BasicBlock

BasicBlock相对简单些

这里实现ResNet18(采用BasicBlock)

// 定义block
template <int N, template <typename> class BN, int stride, typename SUBNET>
using block = BN<dlib::con<N,3, 3, 1, 1, dlib::relu<BN<dlib::con <N, 3, 3, stride, stride, SUBNET >>>>> ;
template <template <int,template<typename>class,int,typename> class block,  int N, template<typename>class BN, typename SUBNET>
using residual_down = dlib::add_prev2<dlib::avg_pool<2,2,2,2,dlib::skip1<dlib::tag2<block<N,BN,2,dlib::tag1<SUBNET>>>>>>;
// 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
*/

// 定义模型
using resnet_18 = dlib::loss_multiclass_log<dlib::fc<10,
                                    dlib::avg_pool_everything<
                                    residual_down<block,512,dlib::bn_con,
                                    residual_down<block,256,dlib::bn_con,
                                    residual_down<block,128,dlib::bn_con,
                                    block<64, dlib::bn_con,1,
                                    dlib::max_pool<2,2,1,1,dlib::relu<dlib::bn_con<
                                    dlib::con<64,3,3,1,1,dlib::input<dlib::matrix<dlib::rgb_pixel>>>>>>>>>>>>>;
// 可以打印网络详细信息
resnet_18 net;
cout<<'layers num:'<<net.num_layers<<endl;
cout<<net<<endl;

// 指定优化器
dlib::dnn_trainer<resnet_18,dlib::adam> trainer(net,dlib::adam()); // 可以根据需要调节adam参数

//....

// 训练后的模型可以保存为xml格式
net.clean()
dlib::net_to_xml(net,"resnet18_network.xml");

如果没有N卡加速,上面的模型就不需要尝试训练了(太慢了)

Bottleneck

和Basicblock不同的一点是,每一个Bottleneck都会在输入和输出之间加上一个卷积层。

这里不再举例了,可以参考一下官方例子:https://www.dlib.net/dnn_introduction2_ex.cpp.html

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐