我的第一个caffe C++程序

最近一段时间一直在考虑为浏览器添加AI过滤裸露图片的功能,但目前大多数AI相关的教程都是用python语言。如果是训练模型,使用python语言无疑是最合适的,但现在的需求是嵌入到产品中,必须要使用C++,为此特意比较了现在比较流行的深度学习框架,发现caffe比较契合需求。caffe本身使用C++语言开发,提供了丰富的C++ API,也提供了很多C++的示例。值得一提的是,雅虎提供了开源的色情图片检测模型open_nsfw,采用的正是caffe深度学习框架。因此我的目标是将open_nsfw集成到产品中。

研究了一番caffe示例和网上的一些教程,发现各有千秋,不同的模型,代码总有一些差异,真正尝试运行时,总有这样那样的问题。最后还是决定从最基本的模型入手,编写并运行一个caffe程序,并能够真正跑起来。

其实网上和书本上都有很多caffe C++的例子,但是真正自己编译运行是总会碰到这样或那样的问题。究其原因,主要是AI是一个新的领域,变化非常快,可能之前能够编译运行的代码,在新的版本上需要稍做修改才行。其次是环境的不同,比如我使用的是带GPU支持的caffe,结果编译遇到问题,网上就没有搜索到答案。所以这次记录的是我在我的环境下能够编译运行的代码,可能并不适用于你,仅供参考。

编译&运行环境

  • Host: ubuntu 16.04 64位操作系统
  • Docker: 选择的是bvlc/caffe:gpu这个镜像,这个虚拟机镜像里系统是ubunt 16.04,NVIDIA计算平台用的是CUDA 8.0版本

这篇文章主要还是说明caffe C++程序的编写,关于环境方面的问题可以搜索网上的资料。

Hello World

在很多编程教程中都会选择输出一个hello world作为第一个示例,作为我的第一个caffe程序,我也希望训练一个足够简单的AI模型,解决一个足够简单的问题。不过这个示例并不是输出hello world字符串,而是训练一个模型,能够计算布尔值的异或(XOR)值。

对于程序员来说,异或(XOR)运算并不陌生,简单说可以如下图表示:

image

嗯,就如同helloworld程序一样,这个程序并没有什么实际用途,但它足够简单,足以让我们对AI程序有个初步的印象。

使用的模型如下:

image

Caffe模型或Caffe神经网络通过连接一组blobs和层而形成。blob是一大块数据,层是应用于blob(数据)的操作。 层本身也可能有一个blob,即权重。因此,一个Caffe模型看起来像是一串交替的blobs和层,彼此相连。一个层需要blobs作为输入,并且它会生成新的blobs,成为下一层的输入。

扫描二维码关注公众号,回复: 1071329 查看本文章

使用模型文件表示如下(model.prototxt):

name: "XOR"
layer {
  name: "inputdata"
  type: "MemoryData"
  top: "fulldata"
  top: "fakelabel"
  include {
    phase: TRAIN
  }
  memory_data_param 
  {
    batch_size: 64 
    channels: 1 
    height: 1
    width: 2 
  }
}

layer {
  name: "test_inputdata"
  type: "MemoryData"
  top: "fulldata"
  top: "fakelabel"
  include {
    phase: TEST
  }
  memory_data_param 
  {
    batch_size: 4 
    channels: 1 
    height: 1
    width: 2 
  }
}

layer {
  name: "fc6"
  type: "InnerProduct"
  bottom: "fulldata"
  top: "fc6"
  inner_product_param {
    num_output: 2
    weight_filler {
      type: "xavier"
    }
    bias_filler {
      type: "constant"
    }
  }
}

layer {
  name: "fc6sig"
  bottom: "fc6"
  top: "fc6"
  type: "Sigmoid"
}

layer {
  name: "fc7"
  type: "InnerProduct"
  bottom: "fc6"
  top: "fc7"
  inner_product_param {
    num_output: 1
    weight_filler {
      type: "xavier"
    }
    bias_filler {
      type: "constant"
    }
  }
}

layer {
  name: "output"
  bottom: "fc7"
  top: "output"
  type: "Sigmoid"
  include {
    phase: TEST
  }
}

layer {
  name: "loss"
  type: "SigmoidCrossEntropyLoss"
  bottom: "fc7"
  bottom: "fakelabel"
  top: "loss"
}

模型解读

前两层是输入层,一个用于训练,另一个用于测试。这两层之间的唯一区别是batch_size。训练使用的批量大小为64,测试的批量大小为4,这是因为只需要测试这4种情况:0 xor 0,0 xor 1,1 xor 0,1 xor 1。

layer {
  name: "inputdata"
  type: "MemoryData"
  top: "fulldata"
  top: "fakelabel"
  include {
    phase: TRAIN
  }
  memory_data_param 
  {
    batch_size: 64 
    channels: 1 
    height: 1
    width: 2 
  }
}

layer {
  name: "test_inputdata"
  type: "MemoryData"
  top: "fulldata"
  top: "fakelabel"
  include {
    phase: TEST
  }
  memory_data_param 
  {
    batch_size: 4 
    channels: 1 
    height: 1
    width: 2 
  }
}

下一层是第一个隐藏层,对应于上图中的黄色神经元。根据Caffe文档,过滤器可以对初始神经网络进行随机化,否则初始权重将为零。由于该模型是完全连接的网络,因此层类型为InnerProduct。

layer {
  name: "fc6"
  type: "InnerProduct"
  bottom: "fulldata"
  top: "fc6"
  inner_product_param {
    num_output: 2
    weight_filler {
      type: "xavier"
    }
    bias_filler {
      type: "constant"
    }
  }
}

layer {
  name: "fc6sig"
  bottom: "fc6"
  top: "fc6"
  type: "Sigmoid"
}

再下来一层对应于图中的绿色输出神经元,它也是一个InnerProduct:

layer {
  name: "fc7"
  type: "InnerProduct"
  bottom: "fc6"
  top: "fc7"
  inner_product_param {
    num_output: 1
    weight_filler {
      type: "xavier"
    }
    bias_filler {
      type: "constant"
    }
  }
}

最后两个层用于激活,一个用于训练,另一个用于测试。

layer {
  name: "output"
  bottom: "fc7"
  top: "output"
  type: "Sigmoid"
  include {
    phase: TEST
  }
}

layer {
  name: "loss"
  type: "SigmoidCrossEntropyLoss"
  bottom: "fc7"
  bottom: "fakelabel"
  top: "loss"
}

solver配置文件

solver配置文件(solver.prototxt)如下:

net: "model.prototxt"
base_lr: 0.02
lr_policy: "step"
gamma: 0.5
stepsize: 500000
display: 2000
max_iter: 5000000
snapshot: 1000000
snapshot_prefix: "XOR"
solver_mode: CPU

学习率从0.02开始,每500000步减少50%,整个迭代次数是5000000。

模型训练C++代码

首先,生成400组训练数据,每个培训数据的批量大小为64。

    float *data = new float[64 * 1 * 1 * 2 * 400];
    float *label = new float[64 * 1 * 1 * 1 * 400];

    for (int i = 0; i < 64 * 1 * 1 * 1 * 400; i++){
        int a = rand() % 2;
        int b = rand() % 2;
        int c = a ^ b;
        data[i * 2 + 0] = a;
        data[i * 2 + 1] = b;
        label[i] = c;
    }

代码很简单,随机选择2个二进制数a和b并计算它们的xor值c。a和b保存在一起作为输入数据,c另存为一个单独的数组作为标签。

然后创建一个solver参数对象并加载solver.prototxt:

    SolverParameter solver_param;
    ReadSolverParamsFromTextFileOrDie("./solver.prototxt", &solver_param);

接下来,使用solver参数创建solver:

    shared_ptr<Solver<float> > // uses namespace std
    solver(SolverRegistry<float>::CreateSolver(solver_param));

然后,从solver的神经网络获取输入的MemoryData层,并输入训练数据:

    MemoryDataLayer<float> *dataLayer_trainnet =
    (MemoryDataLayer<float> *)
    (solver->net()->layer_by_name("inputdata").get());
    dataLayer_trainnet->Reset(data, label, 25600);

MemoryData的Reset函数允许您提供指向数据和标签内存的指针。每个标签的大小只能是1,而数据大小是在model.prototxt文件中指定的。25600是训练数据的计数,它必须是批量大小的64倍。

现在,调用下面的代码,开始训练网络:

solver->Solve();

模型训练完成后,生成XOR_iter_1000000.caffemodel、XOR_iter_1000000.solverstate ~ XOR_iter_5000000.caffemodel、XOR_iter_5000000.solverstate这样的模型文件,接下来我们将使用这些生成的模型文件进行测试。

模型测试C++代码

用相同的模型创建另一个网络,但传入TEST,并加载XOR_iter_5000000.caffemodel:

    shared_ptr<Net<float> > testnet;

    testnet.reset(new Net<float>("./model.prototxt", TEST));
    testnet->CopyTrainedLayersFrom("XOR_iter_5000000.caffemodel");

与训练阶段类似,需要获取输入的MemoryData层并将其输入传递给它进行测试:

    float testab[] = {0, 0, 0, 1, 1, 0, 1, 1};
    float testc[] = {0, 1, 1, 0};

    MemoryDataLayer<float> *dataLayer_testnet = (MemoryDataLayer<float> *)(testnet->layer_by_name(argv[3]).get());

    dataLayer_testnet->Reset(testab, testc, 4);

请注意,此输入层的名称是test_inputdata,而用于训练的输入层是inputdata。

然后计算神经网络输出:

    testnet->Forward();

完成之后,我们需要通过访问输出blob来获得结果:

    boost::shared_ptr<Blob<float> > output_layer = testnet->blob_by_name(argv[4]);

    const float* begin = output_layer->cpu_data();
    const float* end = begin + 4;

    vector<float> result(begin, end);

输出大小为4,并将输出保存到结果向量中。

最后打印结果:

    for (int i = 0; i < result.size(); i++) {
        cout<<"input: "<<(int)testab[i * 2 + 0]
        <<" xor "<<(int)testab[i * 2 + 1]
        <<", truth: "<<(int)testc[i]
        <<", result by NN: "<<result[i]<<endl;
    }

以下是这个简单的神经网络的测试结果:

input: 0 xor 0,  truth: 0.000000 result by nn: 0.000550
input: 0 xor 1,  truth: 1.000000 result by nn: 0.999368
input: 1 xor 0,  truth: 1.000000 result by nn: 0.999368
input: 1 xor 1,  truth: 0.000000 result by nn: 0.000626

你可能会疑惑了,怎么整这么复杂的代码,得出一个并不那么精确的结果。其实,随着机器学习的深入,你会发现概率论和统计学占据着主要的地位,一个模型能够有这么高的精度,已经非常不错了。

编译

代码编写出来,接下来肯定需要编译运行,但这部分网络上的资料相对比较少。因为caffe使用了cmake,所以这段代码我也使用cmake来构建。

这个任务其实相对比较简单,但却花费了很多时间。主要是我追求完美,希望能够让build系统更健壮,比如caffe库的头文件、库文件自动检测,相关依赖库的确定与链接等等。查了一些资料,在build caffe库的时候会生成一个FindCaffe.cmake的文件,但我费了老半天的劲也生成不出来。参照别人的FindCaffe.cmake,似乎也找不到,即使把caffe的头文件和库文件找到,还有相关的库,比如glog、openBLAS等等,还是需要手工编写。最后还是决定先手工编写,CMakeLists.txt文件如下:

cmake_minimum_required(VERSION 2.8.7)

project(XORusingCAFFE C CXX)

set(Caffe_INCLUDE_DIRS /opt/caffe/include /opt/caffe/build/include /usr/local/cuda/include)
set(Caffe_LIBRARIES caffe boost_system glog)

include_directories(${Caffe_INCLUDE_DIRS})
link_directories(/opt/caffe/build/lib)

add_executable(trainXOR  trainXOR.cpp)
target_link_libraries(trainXOR ${Caffe_LIBRARIES})

add_executable(testXOR testXOR.cpp)
target_link_libraries(testXOR ${Caffe_LIBRARIES})

这是caffe docker环境下可用的编译配置文件,如果在别的环境下,可能需要稍微做一些修改。

至此,我的第一个caffe程序编写完毕,虽然大部分的内容来自网络,但总归是我亲手敲进去,并编译运行出来的,接下来我会研究如何将这个helloworld搬到Android手机上运行。

参考

  1. A Gentle Introduction to Artificial Neural Networks
  2. Caffe c++ helloworld example with MemoryData input
  3. C# Backpropagation Tutorial (XOR)

猜你喜欢

转载自blog.csdn.net/mogoweb/article/details/79784720