面向windows的支持plugins的caffe

https://github.com/wadefelix/caffe/tree/pwin

prototxt文件整合是在CMake脚本里完成,所以修改proto文件的话要用CMake再生成。

另外protocal buffer版本换成了最新的3.5.2,预编译的protobuf对扩展的支持没搞定可能不太好搞。编译protobuf时注意修改其运行库是和caffe一致的多线程DLL

已携带了和Linux下pCaffe分支一样有的 fast_rcnnSSDyolo

另外,因为电脑所限,我只编译了纯CPU版本,所以GPU版本不确定,也没有支持CUDA的Dynamic Parallelism动态并行特性。

VS2017编译caffe

caffe的windows分支支持的VS版本是2013和2015,而我安装的是Visual Studio Community 2017, 直接使用cmake是不行的。

根据报错信息,凡是不行的地方让VS2017直接使用VS2015的配置,就可以使用caffe工程提供的预编译的二进制的依赖库了。

CMake脚本的修改

cmake/WindowsDownloadPrebuiltDependencies.cmake需要增加

set(DEPENDENCIES_URL_1912_27 "${DEPENDENCIES_URL_BASE}/v${DEPENDENCIES_VERSION}/${DEPENDENCIES_NAME_1900_27}${DEPENDENCIES_FILE_EXT}")
set(DEPENDENCIES_SHA_1912_27 "17eecb095bd3b0774a87a38624a77ce35e497cd2")

1912就是VS2017的,1900是2015的。

依赖包中OpenCV的配置

预编译的OpenCV中也缺少VS2017的信息,.caffe\dependencies\libraries\OpenCVConfig.cmake中增加VS2017的信息

  elseif(MSVC_VERSION EQUAL 1900)
    set(OpenCV_RUNTIME vc14)
  elseif(MSVC_VERSION EQUAL 1912)
    set(OpenCV_RUNTIME vc14)
  endif()

BLAS选择OpenBLAS,我这苏菲也没有N记卡,不用CUDA。选择VS2017 amd64编译起,cmake搞起,成功编译出工程, VS打开sln,编译也成功。Debug配置下
C:\Users\renwe\Projects\caffe\build>.\tools\Debug\caffe-d.exe --help,Release配置下C:\Users\renwe\Projects\caffe\build>.\tools\Release\caffe.exe --help成功运行。

YEAH!对了,

python版本选择

我看了caffe仅支持2.7和3.5版本的python,那VS2016里安装的python3.6我就放弃吧,有miniconda这种神器我何必自己折腾呢。选择了安装了miniconda2版本(想着Caffe2还不支持py3)。

mnist走一波

caffe工程目录下(我的C:\Users\renwe\Projects\caffe> )开PowerShell:

.\data\mnist\get_mnist.ps1
.\examples\mnist\create_mnist.ps1
.\examples\mnist\train_lenet.ps1

注意1:PowerShell运行脚本的权限,默认可能是不能执行的,修改命令Set-ExecutionPolicy RemoteSigned -Scope CurrentUser
注意2:VS工程配置的问题,可执行文件都在各自的Debug和Release字目录中。适当修改下ps1脚本就OK了。
最后训练完测试完

I0331 02:00:46.701665 15772 solver.cpp:310] Iteration 10000, loss = 0.00280334
I0331 02:00:46.701665 15772 solver.cpp:330] Iteration 10000, Testing net (#0)
I0331 02:00:56.045547 19868 data_layer.cpp:73] Restarting data prefetching from start.
I0331 02:00:56.420552 15772 solver.cpp:397]     Test net output #0: accuracy = 0.9901
I0331 02:00:56.420552 15772 solver.cpp:397]     Test net output #1: loss = 0.0309957 (* 1 = 0.0309957 loss)
I0331 02:00:56.420552 15772 solver.cpp:315] Optimization Done.
I0331 02:00:56.420552 15772 caffe.cpp:260] Optimization Done.

caffe输入层怎么编写?

caffe的输入层总结

前面已经总结了下caffe已内置的输入层,但是除了python layer外,其余的局限性都比较大,比如大家常用的BoundingBox数据就很不统一,py-faster-rcnn里面使用python layer作输入层直接读取从PASCAL VOC格式数据(其实是从它构建的roidb并缓存了),而SSD自己构建了AnnotatedDataLayer来处理保存在lmdb中的数据,caffe-yolo也自己构建了BoxDataLayer供自己使用,大家都很不统一,caffe官方也迟迟不统一一个BBoxDataLayer出来,局面很分裂。本文就算是总结下阅读这些代码的经验吧。

那输入层应该怎么写?先来搞清除这几个概念。

一些基础概念

Datum

Datum是LMDB/LevelDB每个条目的值。但是Datum的内容是在caffe.proto里规定好了的,原版只有bytesdatafloatfloat_data两个字段可供保存数据。如果想保存多种数据,没办法,只能先组装到一块再放到datum中。

DataReader

yolo和ssd的DataReader完全一样啊。本对象还实例化了一个继承自InternalThread的类负责启动一个线程单独的读取磁盘数据。

如果输入层直接继承InternalThread类,则可以不实现DataReader类,相当于自己就是读入类,线程入口是InternalThreadEntry

Caffe原装的DataLayer就已继承InternalThread,只需要实现load_batch即可。

Batch

Bath就是每次让DataReader读到的数据,这个数据在输入层一般直接就直接想下一层传递了。这个caffe默认的只有data_label_两个Blob成员,如果我们有多种类型数据,caffe默认的恐怕也不够用。

Prefetch队列

  virtual void load_batch(Batch<Dtype>* batch);
  vector<shared_ptr<Batch<Dtype> > > prefetch_;
  BlockingQueue<Batch<Dtype>*> prefetch_free_;
  BlockingQueue<Batch<Dtype>*> prefetch_full_;

load_batch函数的用途:batch_size次从数据库中读取出共计batch_sizedatum数据,将这些datum转成Batch对象。

InternalThreadEntry每次从prefetch_free_pop一个Batch对象出来,然后调用load_batch装满这个Batch对象,再把这个Batch对象pushprefetch_full_中去。

XXXDataLayer::Forward_cpu的作用就是将当前的prefetch_current_指向的Batch对象pushprefetch_free_中去,然后再用从prefetch_full_pop取出一个对象到prefetch_current_上来。

总结下方法

简单标签

如果你的数据只是一些图,每张图对应一个int型可以表示的label,那你直接把图文件名和label组成文本文件用ImageDataLayer读就可以了,想用LMDB的就把数据存到LMDB中也可以。

bbox标签

因为原版datum字段有限,特别是label只有一个,大家只好各显神通。

caffe-yolo的做法是,datumdata存放图片,float_data存放bbox六元组(标签,难易标识,四个坐标)。ssd的做法则是对datum做了更大的更改以支持bbox数据。我为了方便,直接给原版datum定义了扩展字段,允许自行扩展若干自定义字段。

也可以将datum视为最简单的容器,我们在其内的data字段内另建容器,把数据结构先序列化存到datum的data字段,使用时再反序列化出来。

protobuf extension的使用

在proto中扩展Datum

// message Datum 需要先声明扩展字段区间,比如 {
//   extensions 11 to 20;
// }
// 然后想扩展datum时,只需要另外如此声明即可
extend Datum {
  optional string imgfilename = 15;
}

python代码中对消息扩展字段的使用

datum.Extensions[caffe.proto.caffe_pb2.imgfilename] = "/path/to/imgfile"

CPP对对消息扩展字段的使用

// get
datum.GetExtension(imgfilename)
// set
datum.SetExtension(imgfilename, "/path/to/imgfile");

 

方法1:直接继承BasePrefetchingDataLayer

需要实现load_batch函数,这个非常简单,队列和线程方法都已定义,仅需自己实现load_bactchLayerSetup相关的函数即可。

有些继承自此方法的层还定义了DataReader另外起个线程来读数据,其实没什么必要,因为load_batch就已经是在单独的线程里的。

方法2:直接继承Layer和InternalThread来实现

需要实现InternalThread里的InternalThreadEntry方法,并定义数据缓冲队列供使用。这个线程里的方法来调用一些方法实现文件输入并放在队列里供Layer的Forward方法取用。这个方法等于自己实现了一遍BasePrefetchingDataLayerLayerSetup函数最后启动这个线程即可。

附:各输入层的继承关系



caffe的输入层总结

来自图片:ImageDataLayer

最简单的输入方式,适用于分类任务,输入就是一个文件文件,而文本文件每行就是一个图片保存路径名称和它的数值标签即可。如果图片都已以文件夹或文件名保存了,可以非常容易地使用lsawk sed等命令建立其要求的文件文件。

来自LMDB或LevelDB数据库:DataLayper

每个条目记录值就是一个Datum,具体代码中就是使用datum.ParseFromString(cursor_->value())获取的。

但是原始的Datum只有一个data项和一个label项和可供使用的float_data项(直接保存的浮点数数据替代bytes表示的data)。如果您的输入数据除了图片还有别的,想自己新写输入层,除了使用这个float_data项外,您可以使用我pCaffe,对Datum定义了扩展条目,可以通过设置扩展字段的方式增加别的data条目。

来自HDF5数据文件:HDF5DataLayer

HDF5数据层参数中不能包含transform参数,不支持数据的图像预处理,直接拷贝数据使用。如果数据确实需要预处理,可以在其后增加reshape crop等层。

来自内存:MemoryDataLayer

每个batch开始前,数据必须使用MemoryDataLayer::Reset方法将内存地址传给网络,caffe会免拷贝地使用它。

自动切图:WindowData

主要是根据窗口定义文件自动地切图贴标签,与基准窗口overlap大于阈值的设为前景窗,否则设为背景窗。 窗口文件里可根据每张图手工控制这些指标,比如可以通过增大小样本类的窗口数以平衡样本不均衡现象。

DummyDataLayer

DummyData有Filler参数,设置来可以自动根据需要填充数据,在构建测试代码时相当好用。

InputLayer

这层参数只有shape,一般仅用于deploy网络中。替代旧式的位于网络参数中的input_shape参数。

PythonLayer

python层用来书写自定义数据的输入层是相当方便的。比如py-faster-rcnn就使用python层来搞定 GroundTruth BoundingBox 的输入。

支持动态并行和源码级plugins的Caffe——pCaffe

支持动态并行和源码级plugins的Caffe——pCaffe

https://github.com/wadefelix/caffe

特性:

  1. 开启了 CUDA Dynamic Parallelism 特性,在 Makefile.config 定义 CUDA_DYNAMIC_PARALLEL := 1 即可。
  2. 支持源码级别的代码 plugins 以支持开发新层等代码,每个新插入的层放置在 src/caffe/plugins 目录下单独的子目录中,方便交流新代码。
  3. plugins 目录已放置了 fast_rcnnSSD 的代码可供使用和参考。
    该 fast_rcnn 所有层均是caffe移植,无需python,且每个minibatch支持多张图,并且实践应用了 CUDA Dynamic Parallelism 特性。

继续阅读支持动态并行和源码级plugins的Caffe——pCaffe

手动通知caffe快照操作等小技巧

caffe支持接受SIGHUP信号进行快照操作,所以命令就是:

# PID是caffe的进程号
kill -SIGHUP PID

# 查看PID的命令
ps -ef | grep caffe

# 或者使用pidof命令,这次一步完成
kill -SIGHUP $(pidof caffe)

使用caffe的solverstate继续训练的命令格式

./build/tools/caffe train --solver=solver.prototxt --snapshot=caffenet_train_iter_10000.solverstate

最简单caffe模型测试命令

import caffe
import numpy as np
import cv2
 
# 把图读进来解码
im = cv2.imread('000001.jpg',cv2.IMREAD_COLOR)
# 把图resize了 并转成网络input的shape大小
imblob=np.transpose(cv2.resize(im,(224,224)).reshape((1,224,224,3)),(0,3,1,2))
 
n = caffe.Net('deploy.prototxt','vgg16_iter_20000.caffemodel',caffe.TEST)
n.forward(data=imblob.astype(np.float32))

# 在直接取出输出层的结果即可,如
print n['cls_prob']

如何更容易的开发和共享caffe新层

caffe项目中早已有不少人在尝试解决这个问题:
Allow self-contained development of Caffe layers · Issue #1896 · BVLC/caffe
Layer modules by hgaiser · Pull Request #5294 · BVLC/caffe
还有动态加载的讨论:
Decentralize development with layer parameter registration mechanism · Issue #3408 · BVLC/caffe

为什么要讨论这个问题,虽然Making a Caffe Layer Development等链接已给出了指导,但是因为caffe版本或者这样那样的原因,别人开发的新层要插入到你自己的caffe代码里也并不是容易的事情,开发新层要在不同的目录添加文件和修改文件,扒起来不易。较好的方法就是直接将新层的代码git clone下来与caffe一起编译最好了。
我们有交流model的 Model Zoo,但caffe的这个缺陷导致迟迟产生不了 Layer Zoo。各位在开发过程中添加的风格也很不统一,甚至部分人对caffe原有代码还有改动,这些代码合并起来颇为麻烦。

我的设想是在在caffe的src目录中再放一个plugins子目录,这个plugins目录中新加层就新加个子目录,一个新层一个子目录,绝不与caffe原有代码混淆。
但这里有个问题就是proto文件,虽然protobuf支持Extension特性,但是caffe网络初始化就要知道参数信息,无法基于 protobuf Extension 开发。
PR #5294 的方法是像python层一样使用一个param_str的通用参数,新层的特性参数就编码到这个字符串中。
我自己的方案就是新层也提供一个 .proto 文件,然后我编译前把所有层的 .proto 文件全部插入到 caffe.proto 文件中再编译。这个方案里,新层所有参数的使用完全和原有层一样,而不需要去解析param_str字符串拿参数。

修改caffemodel文件

1 修改某层名称

先把prototxt新修改出一份,然后分别用新旧prototxt去加载caffemodel文件,再把旧参数付给新网络的相应层。

#! python
#coding=utf-8
import sys
from caffe import Net
n_orig = Net('test_o.prototxt', 'orig.caffemodel', 0)
n_new = Net('test_n.prototxt', 'orig.caffemodel', 0)
n_new.params['newlayername'] = n_orig.params['origlayername']
n_new.save('new.caffemodel')

2 修改层shape

如添加一个新类,直接修改classification全连接层,修改后继续训练就可以了。

#! python
#coding=utf-8
import sys,caffe
import numpy as np

n = caffe.Net('train.prototxt','orig.caffemodel',0)

# 这里直接用原有数值的均值初始化新数据空间。
cls_score0=n.params['cls_score'][0].data.copy()
cls_score0_n = np.append(cls_score0, np.mean(cls_score0)*np.ones((1,4096), dtype=cls_score0.dtype),axis=0)

cls_score1 = n.params['cls_score'][1].data.copy()
cls_score1_n = np.append(cls_score1, np.mean(cls_score1))

n.params['cls_score'][0].reshape(22,4096)
n.params['cls_score'][0].data[...] = cls_score0_n
n.params['cls_score'][1].reshape (*cls_score1_n.shape)
n.params['cls_score'][1].data[...] = cls_score1_n
 
n.save('newclass22.caffemodel')

3 从caffemodel中恢复出prototxt结构

可以从caffemodel反推出网络结构,但不会完全一致,主要是原来隐含层会被显式表达出来。

#! python
#coding=utf-8
 
from caffe.proto import caffe_pb2
 
def Caffemodel2Prototxt(modelName,deployName):
  with open(modelName, 'rb') as f:
    caffemodel = caffe_pb2.NetParameter()
    caffemodel.ParseFromString(f.read())
  for item in caffemodel.layers:
    item.ClearField('blobs')
  for item in caffemodel.layer:
    item.ClearField('blobs')
 
  with open(deployName, 'w') as f:
    f.write(str(caffemodel))
 
if __name__ == '__main__':
  modelName = 'VGG16_iter_28000.caffemodel'
  deployName = 'VGG16_deploy.prototxt'
  Caffemodel2Prototxt(modelName,deployName)

caffe 支持 CUDA Dynamic Parallelism

CUDA从3.5开始就支持了动态并行,就是支持GPU线程再直接启动一组子计算线程,而在这个之前GPU计算线程都是由CPU线程启动的。
为了支持这个动态并行特性,需要链接 cudadevrt 库。

不过,caffe 始终没有应用这个CUDA特性。 我自己fork了一个分支启用了该特性 支持动态并行的caffe实现
为了启用该特性,还更改了cuda代码的编译链接过程,原来都是每个cu文件直出可链接的目标文件。这个分支时每个cu文件都先 device-c 到一个目标文件,再把所有的cu的目标文件 dlink 到一个可链接的目标文件。
启用这个特性的所有修改都在 Makefile 中,我定义了一个 CUDA_DYNAMIC_PARALLEL 标识控制是否启用, 在 Makefile.config 中定义 CUDA_DYNAMIC_PARALLEL := 1 即可启用。启用后也传递了一个 CUDA_DYNAMIC_PARALLEL 预定义变量到编译器以方便代码中据此控制区分下代码。

启用后即可在cuda代码中享用动态并行特性了。

至于能不能显著提高计算速度?对原有的层没有影响,这是要修改代码的啊,可能原有的部分层会受益于此功能,我没检查。对于新增层的话,如果层计算逻辑可以包含用GPU计算线程新起计算线程的话,就可受益,毕竟不必在绕回到CPU线程由CPU线程来启动下一轮GPU线程。