SAE 入门(二)——基于 tiny_dnn 的手写数字重建

SAE 入门(二)——基于 tiny_dnn 的手写数字重建

前言

上一篇文章中,我们使用 Python 使用 SAE 网络实现了手写数字的重建。在本文中,我们将尝试使用 tiny_dnn 库实现手写数字重建。

tiny_dnn 简介

tiny-dnn 项目地址:https://github.com/tiny-dnn/tiny-dnn,这是深度学习的一个 C ++ 14 实现。它适合在有限的计算资源,嵌入式系统和 IoT 设备上进行深度学习。整个项目仅由头文件构成,使用时无需编译,直接引用即可。

搭建环境

版本要求

需要一个 C++ 14 编译器,例如 gcc 4.9+,clang 3.6+ 或者 VS 2015+。本文中使用 Visual Studio 2019 为例进行配置。

创建项目

打开 VS,创建一个名为 testTinyDNN 的控制台应用。将 tiny_dnn 下载解压之后,放置到如下图所示的位置,与 testTinyDNN.cpp 属于同一层级。

项目文件结构示意图

编辑配置

  1. 编辑 config.h 文件第 61 行,将其取消注释;这样我们才可以将栈式自编码器预测的图片保存到本地。涉及内容如下:
1
2
3
4
5
/**
 * Enable Image API support.
 * Currently we use stb by default.
 **/
#define DNN_USE_IMAGE_API
  1. 编辑 image.h 文件第 378 行,将 border_width 值设置为 0,这样保存的图片每个像素周围就不会存在白色边框。涉及内容如下:
1
const size_t border_width = 0;

编写代码

打开 testTinyDNN.cpp 文件,将下列代码粘贴进去。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
#include <iostream>
#include <string>
#include "tiny_dnn/tiny_dnn.h"
using namespace tiny_dnn;
using namespace tiny_dnn::activation;
using namespace tiny_dnn::layers;
using namespace std;

#define EPOCHS 50
#define BATCH_SIZE 256

void sae() {
    // define network, optimizer and engine
    network<sequential> net;
    adam optimizer;
    core::backend_t backend_type = core::default_engine();

    // construct network layers, include 3 encoder layers and 3 decoder layers
    net << fully_connected_layer(784, 128, true, backend_type) << relu()
        << fully_connected_layer(128, 64, true, backend_type) << relu()
        << fully_connected_layer(64, 32, true, backend_type) << relu()
        << fully_connected_layer(32, 64, true, backend_type) << relu()
        << fully_connected_layer(64, 128, true, backend_type) << sigmoid()
        << fully_connected_layer(128, 784, true, backend_type);

    // load MNIST dataset
    vector<vec_t> train_images, test_images;
    string data_dir_path = "tiny_dnn/data";
    parse_mnist_images(data_dir_path + "/train-images.idx3-ubyte",
        &train_images, -1.0, 1.0, 0, 0);
    parse_mnist_images(data_dir_path + "/t10k-images.idx3-ubyte",
        &test_images, -1.0, 1.0, 0, 0);

    cout << "start training" << endl;

    // define learning rate (0.05)
    optimizer.alpha *= static_cast<tiny_dnn::float_t>(0.05);

    // display training progress bar, and show training duration
    progress_display disp(static_cast<unsigned long>(train_images.size()));
    timer t;

    // create callback
    int epoch = 0;
    auto on_enumerate_epoch = [&]() {
        epoch++;
        cout << "\n" << t.elapsed() << "s elapsed." << endl;
        cout << "epoch=" << epoch << "/" << EPOCHS << endl;
        disp.restart(static_cast<unsigned long>(train_images.size()));
        t.restart();
    };

    auto on_enumerate_minibatch = [&]() {
        disp += BATCH_SIZE;
    };

    // training
    net.fit<mse>(optimizer, train_images, train_images, BATCH_SIZE, EPOCHS,
        on_enumerate_minibatch, on_enumerate_epoch);

    // save model
    net.save("sae-net");
    cout << "end training." << endl;

    // if the model already exists, you can read it directly
    //net.load("sae-net");

    // save layers to image
    //for (size_t i = 0; i < net.depth(); i++) {
    //    auto out_img = net[i]->output_to_image();
    //    auto filename = "layer_" + to_string(i) + ".bmp";
    //    out_img.save(filename);
    //}

    // test and show results
    for (int i = 0; i < 10; i++) {
        // get predicted result image
        auto predict = net.predict(test_images[i]);

        // save predicted result image to file
        auto image = vec2image<float>(predict, 10, 28);
        auto filename = "image_predicted_" + to_string(i) + ".bmp";
        image.save(filename);

        // save the origin test image to file
        image = vec2image<float>(test_images[i], 10, 28);
        filename = "image_test_" + to_string(i) + ".bmp";
        image.save(filename);
    }
}

int main() {
    sae();
}

在代码中,我们定义了每批次训练数据量为 256 条,总共训练 50 个批次。

网络结构为 3 个编码层 + 3 个解码层。编码层将数据从 784(28 * 28) 维分别编码(降维)到 128、64、32 维,解码器再将 32 维的编码结果解码(升维)到 64、128、784 维,完成手写数字重建。各层之间的激活函数选用 relu()sigmoid()

结果展示

手写数字重建效果对比图

从上到下,第一行为测试图像,第二行为 keras 搭建的 SAE 网络重建图像,第三行为 tiny_dnn 搭建的 SAE 网络重建图像。下面展示数字 2 和 5 重建的详细效果,左侧为 Python 平台重建结果,右侧为 C++ 平台重建结果。

数字 2

数字 2 的重建效果对比

数字 5

数字 5 的重建效果对比

性能对比

测试使用的 CPU 型号为 Intel i5-4200H,基准频率为 2.80GHz。

基于 tiny_dnn 的 C++ 平台训练时长为 2624.95 秒,基于 keras 的 Python 平台训练时长为 135.70 秒。在 50 个 epoch 测试中,Python 平台比 C++ 平台快了大约 19 倍,Python 平台 loss 大约为 0.08。由重建图片结果不难看出,Python 平台效果明显优于 C++ 平台。

存在的不足

  1. C++ 平台目前无法计算每个 epoch 的 loss;
  2. 将在 C++ 平台测试更多的 epoch,观察图像重建效果是否会有改善。

参考文献

  1. A simple and basic tutorial of tiny-dnn
  2. A quick introduction to tiny-dnn
  3. Details about tiny-dnn’s API and short examples
Licensed under CC BY-NC-SA 4.0
Built with Hugo
Theme Stack designed by Jimmy