相关文章推荐

本文为本人caffe分类网络训练、结果可视化、部署及量化具体过程的心得笔记。caffe目前官方已经停止支持了,但是caffe是目前工业落地最常用的深度学习框架,用的人挺多。其实主要怕自己忘了,弄个备份,弄caffe很久了,怕不用东西都忘了,但是本文主要是讲述caffe下的分类网络。caffe默认已经配置好了,而且尽可能是linux系统,本文基于ubuntu18系统。如果有错误,希望积极指正。

1.1 数据准备

首先在caffe/data路径建立example_data文件夹,在example_data里建立三个文件夹。
train文件为训练文件数据,val为验证文件数据,dataSet为最后生成caffe所用数据存放文件夹。这里准备五类数据,分别放在文件夹0-4。标号必须为0到4。val文件夹也是一样的。
0到4文件夹存放各类图像,各类图像编号类似如下: 在这里插入图片描述
然后调用label.py创建数据集标签名,分别在train目录和val目录生成数据集标签文件,创建数据集的txt文件,如果是训练集命名为train.txt,如果是验证集命名为val.txt。label.py代码如下:

import os 
# 各类分类文件夹名
dealPaths=['0','1','2','3','4']
# 创建数据集的txt文件,如果是训练集命名为train.txt如果是验证集命名为val.txt
imageData=open('imageData.txt','w')
for dealPath in dealPaths:
    for filename in os.listdir(dealPath):
        imageData.write(filename+' '+dealPath+"\n")
imageData.close()

生成的数据标签文件如下,这个数据标签文件包含所有训练集图像数据标签,图像必须是jpg文件。
然后将所有单个分类也就是0到4文件夹里面的数据放入一个文件夹。这里通过imageMove.py将所有图像移动到自己设定的set文件夹。但是如果是训练数据集,将set文件夹重命名为imageTrain,然后上一步得到的train.txt文件移动到imageTrain里面,然后将验证集数据集得到的set文件夹重命名为imageVal,把上一部得到的val.txt移动到imageVal文件夹里面。

import os import shutil movePaths=['0','1','2','3','4'] # 保存的文件夹 dstPath='set' os.makedirs(dstPath,exist_ok=True) imageCount=[] for movePath in movePaths: imageCount.append(len(os.listdir(movePath))) print('current path is {}'.format(movePath)) for filename in os.listdir(movePath): srcFile=os.path.join(movePath,filename) dstFile=os.path.join(dstPath,filename) shutil.copy(srcFile,dstFile) if len(os.listdir(dstPath)) == sum(imageCount): print('move sucess!') else: print('error')

对于建立图像训练集可能会出现jpg文件损坏以及其他问题,一些小工具可以见:

https://blog.csdn.net/LuohenYJ/article/details/86574451

1.2 创建lmdb文件

将上一节得到的imageTrain文件夹和imageVal文件夹移动到dataSet目录。如下所示:
其中create_data.sh用于创建lmbdb文件,make_data_mean.sh用于创建均值文件。首先在caffe根目录下运行create_data.sh文件,命令如下:
create_data.sh脚本具体内容如下,具体要改的参数都有标明,都是在caffe目录下使用相对路径名,绝对路径名容易出错。

#!/usr/bin/env sh
# Create the imagenet lmdb inputs
# N.B. set the path to the imagenet train + val data dirs
set -e
# 数据文件根目录
EXAMPLE=data/example_data
# 数据文件目录
DATA=data/example_data/dataSet
# 生成lmdb文件所用到目录
TOOLS=build/tools
# 生成的train.val的lmdb文件保存目录
TRAIN_DATA_ROOT=data/example_data/dataSet/imageTrain/
VAL_DATA_ROOT=data/example_data/dataSet/imageVal/
# Set RESIZE=true to resize the images to 256x256. Leave as false if images have
# already been resized using another tool.
RESIZE=true
# 改一改深度学习模型要求输入图像的大小
if $RESIZE; then
  RESIZE_HEIGHT=227
  RESIZE_WIDTH=227
  RESIZE_HEIGHT=0
  RESIZE_WIDTH=0
if [ ! -d "$TRAIN_DATA_ROOT" ]; then
  echo "Error: TRAIN_DATA_ROOT is not a path to a directory: $TRAIN_DATA_ROOT"
  echo "Set the TRAIN_DATA_ROOT variable in create_imagenet.sh to the path" \
       "where the ImageNet training data is stored."
  exit 1
if [ ! -d "$VAL_DATA_ROOT" ]; then
  echo "Error: VAL_DATA_ROOT is not a path to a directory: $VAL_DATA_ROOT"
  echo "Set the VAL_DATA_ROOT variable in create_imagenet.sh to the path" \
       "where the ImageNet validation data is stored."
  exit 1
echo "Creating train lmdb..."
GLOG_logtostderr=1 $TOOLS/convert_imageset \
    --resize_height=$RESIZE_HEIGHT \
    --resize_width=$RESIZE_WIDTH \
    # 打乱图像
    --shuffle=true \
    $TRAIN_DATA_ROOT \
    # 训练集文件标签
    $DATA/imageTrain/train.txt \
    $EXAMPLE/example_data_train_lmdb
echo "Creating val lmdb..."
GLOG_logtostderr=1 $TOOLS/convert_imageset \
    --resize_height=$RESIZE_HEIGHT \
    --resize_width=$RESIZE_WIDTH \
    --shuffle \
    $VAL_DATA_ROOT \
    # 测试集文件标签
    $DATA/imageVal/val.txt \
    $EXAMPLE/example_data_val_lmdb
echo "Done."

生成lmdb文件后,然后生成均值文件也就是example_data_mean.binaryproto文件

#!/usr/bin/env sh
# Compute the mean image from the imagenet training lmdb
# N.B. this is available in data/ilsvrc12
# 和前面一样
EXAMPLE=data/example_data
DATA=data/example_data
TOOLS=build/tools
$TOOLS/compute_image_mean $EXAMPLE/example_data_train_lmdb \
  $DATA/example_data_mean.binaryproto
echo "Done."

最后我们会得到均值数据文件,分别是BGR通道训练样本均值。记得把这三个值记一下以后要用到。
所得到的文件lmdb和均值文件可以在example_data根目录下找到。训练模型用这三个文件就行了。

1.3 模型训练

训练时还需要solver文件来对参数进行更新, 同时还需要网络结构文件。在caffe/examples下建立example_data文件夹下进行训练。将前面的lmdb文件和均值文件复制到改文件夹下,如下图所示。橙色框选的文件是上步所获得的,红色框选的文件是网络结构参数文件和调参文件。backup是生成用于保存训练模型的文件夹,caffe.log是训练时生成的日志文件。
solver文件就用示例的配置alexnet文件。solver文件主要参数搜索就用,其中net值得是模型结构文件路径,训练习惯以caffe为根目录,snapshot_prefix为模型保存目录。

# 网络结构地址
net: "examples/example_data/alexnet_train_val.prototxt"
test_iter: 200
test_interval: 200
base_lr: 0.0001
lr_policy: "step"
gamma: 0.1
stepsize: 1000
display: 100
max_iter: 3000
momentum: 0.9
weight_decay: 0.0005
snapshot: 1000
# 模型保存文件
snapshot_prefix: "examples/example_data/backup/"
solver_mode: GPU

至于train_val模型文件,有两部分需要改,第一部分是开头的data文件,需要配置lmdb文件,train和val都配置。其次是均值部分,一种方法是类似配置lmdb文件一样,设定mean_file路径,另外一种是直接给出均值,把前面BGR均值用mean_value:给出。batch_size的修改具体见搜索。lmdb图像输入大小需要和crop_size对应。如下所示:

name: "AlexNet"
layer {
  name: "data"
  type: "Data"
  top: "data"
  top: "label"
  include {
    phase: TRAIN
  transform_param {
    mirror: true
    crop_size: 227
    mean_file: "examples/example_data/example_data_mean.binaryproto"
    #mean_value: 85.0205
    #mean_value: 85.0205
    #mean_value: 85.0205
  data_param {
    source: "examples/example_data/example_data_train_lmdb"
    batch_size: 4
    backend: LMDB
layer {
  name: "data"
  type: "Data"
  top: "data"
  top: "label"
  include {
    phase: TEST
  transform_param {
    mirror: false
    crop_size: 227
    mean_file: "examples/example_data/example_data_mean.binaryproto"
    #mean_value: 85.0205
    #mean_value: 85.0205
    #mean_value: 85.0205
  data_param {
    source: "examples/example_data/example_data_val_lmdb"
    batch_size: 8
    backend: LMDB

另外一个要改的就是结尾的num_output,将最后一个num_output改为分类个数,本文有5类所以num_output:5

layer {
  name: "conv10"
  type: "Convolution"
  bottom: "fire9/concat"
  top: "conv10"
  convolution_param {
  	# 输出,按照分类个数确定
    num_output: 5
    kernel_size: 1
    weight_filler {
      type: "gaussian"
      mean: 0.0
      std: 0.01
layer {
  name: "relu_conv10"
  type: "ReLU"
  bottom: "conv10"
  top: "conv10"
layer {
  name: "pool10"
  type: "Pooling"
  bottom: "conv10"
  top: "pool10"
  pooling_param {
    pool: AVE
    global_pooling: true
layer {
  name: "loss"
  type: "SoftmaxWithLoss"
  bottom: "pool10"
  bottom: "label"
  top: "loss"
  #include {
  #  phase: TRAIN
layer {
  name: "accuracy"
  type: "Accuracy"
  bottom: "pool10"
  bottom: "label"
  top: "accuracy"
  #include {
  #  phase: TEST

最后进行训练时还是在caffe根目录下输入以下指令。第一个训练指令就是直接训练文件,输出结果打印在命令行。推荐用第二个训练命令,在结果打印在命令行同时,也将结果保存为log日志文件方便以后分析使用。其中-solver表示训练参数,后面跟训练参数文件。如果用到gpu设置 -gpu=0选择用哪个gpu。

./build/tools/caffe train -solver examples/example_data/alexnet_solver.prototxt 
./build/tools/caffe train -solver examples/example_data/alexnet_solver.prototxt 2>&1| tee examples/example_data/caffe.log

还有一种训练方式,就是用已有模型进行微调 finetune。如果你不是自己设计模型,这种方式比直接训练要好得多。比如获得squeezenet的solver.prototxt,train_val.prototxt以及模型文件。类似前面直接训练修改solver.protoxt,train_val.prototxt。对于train_val.prototxt修改,只需要修改source处data文件,由于是用别人的模型微调不要改mean值,直接用人家的mean值不要用自己的mean值。

layer {
  name: "data"
  type: "Data"
  top: "data"
  top: "label"
  include {
    phase: TRAIN
  transform_param {
    crop_size: 227
    mean_value: 104
    mean_value: 117
    mean_value: 123
  data_param {
    source: "examples/example_data/example_data_train_lmdb"
    batch_size: 32
    backend: LMDB
layer {
  name: "data"
  type: "Data"
  top: "data"
  top: "label"
  include {
    phase: TEST
  transform_param {
    crop_size: 227
    mean_value: 104
    mean_value: 117
    mean_value: 123
  data_param {
    source: "examples/example_data/example_data_train_lmdb"
    batch_size: 25 #not *iter_size
    backend: LMDB

对于train_val最后的输出,如果想微调的层就把名字改了,一般都是从后往前改。名字不改的层就不会训练。loss和accuray层不需要改,由于不需要top5输出。accuracy_top5层就被删除了。

layer {
  #原来是conv10
  name: "conv10_example"
  type: "Convolution"
  bottom: "fire9/concat"
  #原来是conv10
  top: "conv10_example"
  convolution_param {
    num_output: 5
    kernel_size: 1
    weight_filler {
      type: "gaussian"
      mean: 0.0
      std: 0.01
layer {
  #原来是relu_conv10
  name: "relu_conv10_example"
  type: "ReLU"
  bottom: "conv10_example"
  top: "conv10_example"
layer {
  #原来是pool10
  name: "pool10_example"
  type: "Pooling"
  bottom: "conv10_example"
  top: "pool10_example"
  pooling_param {
    pool: AVE
    global_pooling: true
layer {
  name: "loss"
  type: "SoftmaxWithLoss"
  bottom: "pool10_example"
  bottom: "label"
  top: "loss"
  #include {
  #  phase: TRAIN
layer {
  name: "accuracy"
  type: "Accuracy"
  bottom: "pool10_example"
  bottom: "label"
  top: "accuracy"
  #include {
  #  phase: TEST

调用参数如下,只是加了weight命令,指向微调网络的模型。

./build/tools/caffe train -solver examples/example_data/solver.prototxt -weights examples/example_data/squeezenet_v1.1.caffemodel 2>&1| tee examples/example_data/caffe.log

从训练日志可以看到,训练时会忽视名字改动的层,进行微调。
训练结束后,可以对训练好的模型进行Testing。还是在caffe根目录下输入以下命令进行测试。model表示模型结构参数文件,weights表示模型权重文件路径。会输出当前模型在test数据下准确率和loss。

./build/tools/caffe test -model examples/example_data/train_val.prototxt -weights examples/example_data/backup/solver_iter_100.caffemodel

1.4 训练部分参考文件

除了官方文件外,参考的内容有:

  • 训练自己的数据集
  • caffe入门
  • caffe官方教程
  • solver参数设置
  • solver参数说明
  • 模型微调1
  • 模型微调2
  • 模型微调3
  • 模型微调4
  • 经典网络总结
  • 经典网络模型
  • caffe猫狗大战训练
  • 2 结果可视化

    2.1 训练数据展示

    在caffe的训练过程中,需要图形化训练数据结果。caffe中自带了工具显示结果。在examples/example_data下建立analyze文件,然后把训练过程生成的caffe.log移动到该文件夹。然后将caffe根目录tools/extra文件夹下的extract_seconds.py、parse_log.py、parse_log.sh和plot_training_log.py.example文件移动到该文件夹下。
    在analyze当前目录下输入以下指令就可以可视化训练日志

    ./plot_training_log.py.example 0  save.png caffe.log
    

    上面0表示可视化的类型,save.png表示可视化保存的图像名,caffe.log表示日志文件。
    可视化类型具体参数如下。0、1、2等序号表示可视化类型,vs为横坐标参数,vs右边为纵坐标参数

        0: Test accuracy  vs. Iters
        1: Test accuracy  vs. Seconds
        2: Test loss  vs. Iters
        3: Test loss  vs. Seconds
        4: Train learning rate  vs. Iters
        5: Train learning rate  vs. Seconds
        6: Train loss  vs. Iters
        7: Train loss  vs. Seconds
    

    绘图结果如下:
    这种方法所画出来的图每次都是随机样式且无法定制参数。如果要定制可视化图像,在analyze当前目录下,输入以下参数提取log文件。

    ./parse_log.py caffe.log ./
    

    这样将得到以下两个文件,分别表示训练数据和测试数据

    caffe.log.train
    caffe.log.test
    

    这种方法和前面可视化命令都会得到这两个文件,但是实质内容有所区别。通过parse_log.py得到的文件内容更规则。如caffe.log.test具体内容如下:
    然后在当前文件夹下建立drawCaffe.py文件可视化训练log。drawCaffe.py内容如下:

    import pandas as pd
    import matplotlib.pyplot as plt
    #训练文件
    train_log = pd.read_csv("caffe.log.train")
    #测试文件
    test_log = pd.read_csv("caffe.log.test")
    _, ax1 = plt.subplots()
    # 可可视化参数NumIters,Seconds,LearningRate,accuracy,loss
    ax1.set_title("train loss and test loss")
    ax1.plot(train_log["NumIters"], train_log["loss"], alpha=0.5)
    ax1.plot(test_log["NumIters"], test_log["loss"], 'g')
    ax1.plot(train_log["NumIters"], train_log["accuracy"], alpha=0.5)
    ax1.plot(test_log["NumIters"], test_log["accuracy"], 'g')
    ax1.set_xlabel('NumIters')
    plt.legend(loc='best')
    # 保存图像
    plt.savefig("save.png", dpi=300)
    plt.show()
    dfTrain= pd.DataFrame(data=train_log, columns=['NumIters', 'loss', 'accuracy'])
    dfTest = pd.DataFrame(data=test_log, columns=['NumIters', 'loss', 'accuracy'])
    dfTrain.to_csv('train.csv')
    dfTest.to_csv('val.csv')
    print("done")
    

    通过以上代码能够分析log文件,同时把训练参数结果保存为train.csv和val.csv。其他就是matplotlib美化图像。绘图结果如下:

    2.2 网络模型可视化

    通过netscope可以可视化caffe模型。打开下面链接的网页,打开Editor,将网络结构的prototxt文件复制到网页左侧编辑框后,shift+enter,就可以直接显示网络结构。非常的简单和方便。同时将鼠标选中某层 将可视化其参数。

    http://ethereon.github.io/netscope/quickstart.html
    如下图所示:

    当然这种方式可视化很简单。但是目前深度学习网络框架很多,但不同框架之间可视化网络层方法差别。一个深度学习模型结构可视化神器Netron,可以直接可视化不同框架下网络的模型。Netron支持tf, caffe, keras,mxnet等多种框架模型的可视化,具体地址如下:

    https://github.com/lutzroeder/Netron

    Netron安装很简单,具体看官方例子。Netron使用也非常简单,View设置显示内容,点击具体某个层可以看到该层具体参数。通过File-export可是导出网络结构为png图像或者svg图像。Netron具体界面如下:

    2.3 ROC曲线绘制

    2.3.1 二分类ROC曲线绘制

    ROC曲线中的主要两个指标就是真正率和假正率,。其中横坐标为假正率(FPR),纵坐标为真正率(TPR)。ROC用于评价模型的预测能力,基于混淆矩阵得出的。TPR越高,同时FPR越低(即ROC曲线越陡),那么模型的性能就越好。曲线下面积AOC(Area Under Curve)被定义为ROC曲线下的面积,使用AUC值作为评价标准是因为很多时候ROC曲线并不能清晰的说明哪个分类器的效果更好,而作为一个数值,对应AUC更大的分类器效果更好。
    AUC的一般判断标准:
    0.5 - 0.7:效果较低,但用于预测股票已经很不错了;
    0.7 - 0.85:效果一般;
    0.85 - 0.95:效果很好;
    0.95 - 1:效果非常好,但一般不太可能。
    本文主要通过sklearn.metrics中的roc_curve, auc函数,并通过opencv中的DNN模块调用caffe模型实现分类。主要计算ROC时,输入的是真实样本标签和判断为正样本的概率。二分类ROC曲线绘制python代码(caffe_roc.py)如下:

    opencv调用caffe并计算roc import numpy as np opencv调用caffe并计算roc import numpy as np import matplotlib.pyplot as plt import cv2 import os from sklearn import metrics # 真实图像标签为0的图像路径 imagePath_0 = ['0'] # 真实图像标签为1的图像路径 imagePath_1 = ['1'] # 正类标签 poslabel = 1 # 模型路径 prototxtFile = 'deploy_227.prototxt' modelFile = 'model_227.caffemodel' # 真实分类结果 trueResult = [] # 检测结果 detectProbs = [] # 图像检测 def detectCaffe(srcImg): detectImg = srcImg.copy() blob = cv2.dnn.blobFromImage( detectImg, 1, (227, 227), (92.713, 106.446, 118.115), swapRB=False) net = cv2.dnn.readNetFromCaffe(prototxtFile, modelFile) net.setInput(blob) detections = net.forward() # 分类结果 order = detections[0].argmax() prob = detections[0].max() #print('the predict class is:',order) #print('the positive class prob is: ', prob) # 返回分类结果和概率 return order, prob # 图像检测 def imageDetect(detectImagePath, trueLabel): for imageFileName in os.listdir(detectImagePath): imageFilePath = os.path.join(detectImagePath, imageFileName) print("current detect image is: ", imageFileName) srcImg = cv2.imread(imageFilePath) if srcImg is None: print("error image is: ", imageFilePath) continue detectOrder, detectProb = detectCaffe(srcImg) trueResult.append(trueLabel) # 如果正样本编号和检测结果标签一致直接保存分类概率 if detectOrder == poslabel: detectProbs.append(detectProb) # 如果不一致保存正样本的分类概率 else: detectProbs.append(1-detectProb) # 画ROC图,输入真实标签,正样本模型分类概率,正样本编号 def drawROC(trueResult, detectProbs, poslabel): fpr, tpr, thresholds = metrics.roc_curve( trueResult, detectProbs, pos_label=poslabel) #auc = metrics.roc_auc_score(y, scores) roc_auc = metrics.auc(fpr, tpr) # 计算约登指数Youden Index(TPR-FPR或者TPR+TNR-1) tpr_fpr = list(tpr-fpr) bestIndex = tpr_fpr.index(max(tpr_fpr)) print("约登指数为{}".format(max(tpr_fpr))) tprBest = tpr[bestIndex] fprBest = fpr[bestIndex] thresholdsBest = thresholds[bestIndex] print("最佳约登指数阈值为:", thresholdsBest) # 假正率为横坐标,真正率为纵坐标做曲线 plt.plot(fpr, tpr, color='darkorange', label='ROC curve (area = %0.2f)' % roc_auc) plt.plot([0, 1], [0, 1], color='navy', linestyle='--') #plt.xlim([0.0, 1.0]) #plt.ylim([0.0, 1.05]) plt.xlabel('False Positive Rate') plt.ylabel('True Positive Rate') plt.title('Receiver operating characteristic example') plt.legend(loc="lower right") # 画出约登指数最大值 plt.plot(fprBest, tprBest, "ro") plt.savefig("roc.png", dpi=300) plt.show() return fpr, tpr, thresholds, bestIndex def main(): # 0标签图像遍历 for imagePath in imagePath_0: imageDetect(imagePath, 0) for imagePath in imagePath_1: imageDetect(imagePath, 1) # poslabel正例标签 fpr, tpr, thresholds, bestIndex = drawROC( trueResult, detectProbs, poslabel) np.save('fpr.npy', fpr) np.save('tpr.npy', tpr) np.save('thresholds', thresholds) return fpr, tpr, thresholds if __name__ == '__main__': fpr, tpr, thresholds = main()

    同时计算约登指数Youden Index(TPR-FPR或者TPR+TNR-1),取使得约登指数最大的阈值为最佳阈值。二分类ROC曲线绘制结果如下图所示,area为AUC值。另外二分类ROC曲线detectProbs 用的是分类概率和真实标签对比绘制ROC曲线。

    2.3.2 多分类ROC曲线绘制

    由于ROC曲线是针对二分类的情况,对于多分类问题,先将分类标签转换为独热编码。比如n=3时标签转换为:

    0->100
    1->010
    2->001
    

    多分类ROC曲线绘制有两种方法:

  • 每种类别下,都可以得到m个测试样本为该类别的概率(矩阵P中的列)。所以,根据概率矩阵P和标签矩阵L中对应的每一列,可以计算出各个阈值下的假正例率(FPR)和真正例率(TPR),从而绘制出一条ROC曲线。这样总共可以绘制出n条ROC曲线。最后对n条ROC曲线取平均,即可得到最终的ROC曲线。
  • 首先,对于一个测试样本:1)标签只由0和1组成,1的位置表明了它的类别(可对应二分类问题中的‘’正’’),0就表示其他类别(‘’负‘’);2)要是分类器对该测试样本分类正确,则该样本标签中1对应的位置在概率矩阵P中的值是大于0对应的位置的概率值的。基于这两点,将标签矩阵L和概率矩阵P分别按行展开,转置后形成两列,这就得到了一个二分类的结果。所以,此方法经过计算后可以直接得到最终的ROC曲线。
  • 上面的两个方法得到的ROC曲线是不同的,当然曲线下的面积AUC也是不一样的。 在python中,方法1和方法2分别对应sklearn.metrics.roc_auc_score函数中参数average值为'macro'和'micro'的情况。本文主要是应用方法2,方法1太麻烦。方法1可以见:

    https://scikit-learn.org/stable/auto_examples/model_selection/plot_roc.html

    caffe下绘制多分类ROC曲线代码(caffe_roc_multi.cpp)如下所示:

    opencv调用caffe并计算多分类roc import numpy as np import matplotlib.pyplot as plt import cv2 import os from sklearn import metrics from sklearn.preprocessing import label_binarize # 真实图像标签为0的图像路径 imagePath_0 = ['0'] # 真实图像标签为1的图像路径 imagePath_1 = ['1'] # 真实图像标签2的图像路径 imagePath_2 = ['2'] # 图像分类标签 imageClass = [0, 1, 2] # 图像分类颜色 classColor = ['aqua', 'darkorange', 'cornflowerblue'] # 模型路径 prototxtFile = 'deploy.prototxt' modelFile = 'model.caffemodel' # 真实分类结果 trueResult = [] # 检测分类结果 detectResult = [] # 最佳阈值结果 thresholdsBest = [1]*len(imageClass) # 图像检测 def detectCaffe(srcImg): detectImg = srcImg.copy() # 自己输入均值 blob = cv2.dnn.blobFromImage( detectImg, 1, (227, 227), (101.897, 111.704, 121.366), swapRB=False) net = cv2.dnn.readNetFromCaffe(prototxtFile, modelFile) net.setInput(blob) detections = net.forward() # 分类结果 order = detections[0].argmax() prob = detections[0].max() #print('the predict class is:',order) #print('the predict class prob is: ', prob) # 返回分类结果和概率 return order, prob # 图像检测 def imageDetect(detectImagePath, trueLabel): for imageFileName in os.listdir(detectImagePath): imageFilePath = os.path.join(detectImagePath, imageFileName) print("current detect image is: ", imageFileName) srcImg = cv2.imread(imageFilePath) if srcImg is None: print("error image is: ", imageFilePath) continue detectOrder, detectProb = detectCaffe(srcImg) trueResult.append(trueLabel) # 如果真实标签和检测结果标签一直直接保存分类概率 detectResult.append(detectOrder) # 画ROC图,输入真实标签,模型分类标签 def drawROC(trueResult, detectResult): # 将图像标签二值化 trueResultBinary = label_binarize(trueResult, classes=imageClass) detectResultBinary = label_binarize(detectResult, classes=imageClass) # 计算每一类的ROC fpr = dict() tpr = dict() roc_auc = dict() thresholds = dict() # 单独计算每一类ROC值 for i in range(len(imageClass)): # 提取第i类预测数据 fpr[i], tpr[i], thresholds[i] = metrics.roc_curve( trueResultBinary[:, i], detectResultBinary[:, i]) roc_auc[i] = metrics.auc(fpr[i], tpr[i]) # Compute micro-average ROC curve and ROC area # micro法计算总的roc fprMicro, tprMicro, _ = metrics.roc_curve( trueResultBinary.ravel(), detectResultBinary.ravel()) # 计算auc值 roc_aucMircro = metrics.auc(fprMicro, tprMicro) plt.figure() # 假正率为横坐标,真正率为纵坐标做曲线 plt.plot(fprMicro, tprMicro, color='deeppink', label='ROC curve (area = {:0.2f})'.format(roc_aucMircro), linestyle=':', linewidth=4) # 画出每一类的ROC曲线 for i in range(len(imageClass)): plt.plot(fpr[i], tpr[i], color=classColor[i], label='ROC curve of class{} (area = {:0.2f})'.format( i, roc_auc[i]), # 计算约登指数Youden Index(TPR-FPR或者TPR+TNR-1) tpr_fpr = list(tpr[i]-fpr[i]) bestIndex = tpr_fpr.index(max(tpr_fpr)) tprBest = tpr[i][bestIndex] fprBest = fpr[i][bestIndex] thresholdsBest[i] = thresholds[i][bestIndex] # 画出约登指数最大值 plt.plot(fprBest, tprBest, "ro", color=classColor[i]) plt.plot([0, 1], [0, 1], 'k--') plt.xlabel('False Positive Rate') plt.ylabel('True Positive Rate') plt.title('Some extension of Receiver operating characteristic to multi-class') plt.legend(loc="best") # 保存图像 plt.savefig("multi_roc.png", dpi=300) return fpr, tpr,trueResultBinary,detectResultBinary def main(): # 0标签图像遍历 for imagePath in imagePath_0: if not os.path.isdir(imagePath): continue imageDetect(imagePath, 0) for imagePath in imagePath_1: if not os.path.isdir(imagePath): continue imageDetect(imagePath, 1) for imagePath in imagePath_2: if not os.path.isdir(imagePath): continue imageDetect(imagePath, 2) fpr, tpr,trueResultBinary,detectResultBinary = drawROC(trueResult, detectResult) np.save('fpr.npy', fpr) np.save('tpr.npy', tpr) np.save('thresholdsBest.npy', thresholdsBest) return fpr, tpr,trueResultBinary,detectResultBinary if __name__ == '__main__': fpr, tpr,trueResultBinary,detectResultBinary = main()

    对于单个分类曲线计算约登指数Youden Index(TPR-FPR或者TPR+TNR-1),取使得约登指数最大的阈值为最佳阈值。多分类ROC曲线绘制结果如下图所示,area为AUC值。另外多分类ROC曲线detectResult 用的是预测标签和真实标签对比绘制ROC曲线。

    2.4 结果可视化部分参考文件

  • caffe训练日志可视化1
  • caffe训练日志可视化2
  • Netscope
  • Netron
  • ROC理论1
  • ROC理论2
  • ROC曲线绘制1
  • ROC曲线绘制2
  • 3 部署和量化

    3.1 部署

    部署有三种推荐方式,OpenCV DNN模块调用caffe模型/mini-caffe/ncnn。本文主要介绍DNN调用caffe模型和ncnn。
    OpenCV调用caffe模型代码(dnn_test.cpp)如下:

    #include "pch.h"
    #include <iostream>
    #include <fstream>
    #include <sstream>
    #include <opencv2/opencv.hpp>
    #include <opencv2/dnn.hpp> //dnn模块
    #include <time.h>
    using namespace std;
    using namespace cv;
    using namespace ::dnn; //调用DNN命名空间
    clock_t start, finish;
    String model_file = "model/model.caffemodel"; //模型结构文件
    String model_text = "model/deploy.prototxt";  //模型数据
    //图像深度学习检测
    double detect_NN(Mat detectImg, Net net)
    	if (net.empty())
    		cout << "no model!" << endl;
    		return -1;
    	//initialize images(输入图像初始化)
    	Mat src = detectImg.clone();
    	if (src.empty())
    		return -1;
    	//图像识别转换
    	//第一个参数输入图像,第二个参数图像放缩大小,第三个参数输入图像尺寸,第四个参数模型训练图像三个通道RGB的均值(均值文件)
    	start = clock();
    	Mat inputBlob;
    	//resize(src, src, Size(227, 227));
    	// 参数分别为输入图像,归一化参数,模型大小,BGR均值
    	inputBlob = blobFromImage(src, 1.0, Size(227, 227), Scalar(92.71, 106.44, 118.11));
    	Mat prob; //输出结果
    	for (int i = 0; i < 1; i++)
    		net.setInput(inputBlob, "data");
    		prob = net.forward("prob"); //输出层2
    	Mat probMat = prob.reshape(1, 1); //转化为1行2列
    	Point classNumber;				  //最大值的位置
    	double classProb;
    	//最大值多少
    	//最大最小值查找,忽略最小值
    	minMaxLoc(probMat, NULL, &classProb, NULL, &classNumber);
    	int classidx = classNumber.x;
    	printf("classidx is:%d\n", classidx);
    	printf("prob is %f\n", classProb);
    	finish = clock();
    	double duration = (double)(finish - start);
    	printf("run time is %f ms\n", duration);
    	return duration;
    int main()
    	Net net = readNetFromCaffe(model_text, model_file);
    	Mat detectImg = imread("image/cat.jpg");
    	double runTime = detect_NN(detectImg, net);
    	return 0;
    

    以上只是caffe简单调用分类模型。其他网络调用可以参见以下链接:

    https://docs.opencv.org/4.1.1/d6/d0f/group__dnn.html
    https://blog.csdn.net/LuohenYJ/column/info/34751

    另外一种就是通过ncnn,ncnn调用已经写过博客,可以参见以下链接:

    https://github.com/Tencent/ncnn/wiki/ncnn-组件使用指北-alexnet
    https://blog.csdn.net/LuohenYJ/article/details/97031156

    实际ncnn使用参考ncnn在github/examples目录文件调用。地址如下:
    https://github.com/Tencent/ncnn/tree/master/examples
    但是不管ncnn都需要输入模型均值,输入图像后要减去训练均值。如果不知道均值,只有binaryproto文件,可以建立mean.py读取binaryproto文件里面的信息,获得BGR均值。代码如下:

    #coding=utf-8
    import caffe
    import numpy as np
    # 待转换的pb格式图像均值文件路径
    MEAN_PROTO_PATH = 'mean.binaryproto'
    # 转换后的numpy格式图像均值文件路径
    MEAN_NPY_PATH = 'mean.npy'
    # 创建protobuf blob
    blob = caffe.proto.caffe_pb2.BlobProto()           
    # 读入mean.binaryproto文件内容
    data = open(MEAN_PROTO_PATH, 'rb' ).read()         
    # 解析文件内容到blob
    blob.ParseFromString(data)
    # 将blob中的均值转换成numpy格式,array的shape (mean_number,channel, hight, width)
    array = np.array(caffe.io.blobproto_to_array(blob))
    # 一个array中可以有多组均值存在,故需要通过下标选择其中一组均值
    mean_npy = array[0]
    print(mean_npy.mean(1).mean(1))
    np.save(MEAN_NPY_PATH ,mean_npy)
    

    3.2 量化

    caffe模型太大,速度太慢。可通过float32转int8进行模型压缩和速度提升。量化的意思就是一般而言,神经网络模型的参数都是用的32bit长度的浮点型数表示,实际上不需要保留那么高的精度,可以通过量化,比如用0~255表示原来32个bit所表示的精度,通过牺牲精度来降低每一个权值所需要占用的空间。常用量化方法为int8量化,float 32进行int 8量化,能够使模型尺寸更小、推断更快、耗电更低。唯一的缺点,模型精度会下降。
    int8量化主要方法可以见下图(图来自网上):
    本文主要在ncnn下实现caffe的int8量化。caffe的int8量化项目地址为:

    https://github.com/BUG1989/caffe-int8-convert-tools

    量化方法非常简单,首先

    git clone https://github.com/BUG1989/caffe-int8-convert-tools
    

    然后进入caffe-int8-convert-tools,把验证集和caffe模型参数放入该目录,输入以下命令实现量化:

    python caffe-int8-convert-tool-dev-weight.py --proto=model/deploy.prototxt --model=model/deploy.caffemodel --mean 92.713 106.446 118.15 --images=val/ --output=model.table
    

    官方教程命令为:

    python caffe-int8-convert-tool-dev-weight.py --proto=test/models/mobilenet_v1.prototxt --model=test/models/mobilenet_v1.caffemodel --mean 103.94 116.78 123.68 --norm=0.017 --images=test/images/ output=mobilenet_v1.table --group=1 --gpu=1
    

    使用教程如下:

    $ python caffe-int8-convert-tool.py --help
    usage: caffe-int8-convert-tool.py [-h] [--proto PROTO] [--model MODEL]
                                      [--mean MEAN MEAN MEAN] [--norm NORM]
                                      [--images IMAGES] [--output OUTPUT]
                                      [--gpu GPU]
    find the pretrained caffe models int8 quantize scale value
    optional arguments:
      -h, --help            show this help message and exit
      --proto PROTO         path to deploy prototxt.
      --model MODEL         path to pretrained weights
      --mean MEAN           value of mean
      --norm NORM           value of normalize
      --images IMAGES       path to calibration images
      --output OUTPUT       path to output calibration table file
      --gpu GPU             use gpu to forward
    

    proto/model是caffe模型参数文件,mean为BGR均值,norm是caffe模型归一化参数,有些模型没有归一化就不要填,image是用于校准图像目录(jpg图像)。量化时需要校准,具体原理见:

    https://arleyzhang.github.io/articles/923e2c40/

    这样量化后获得输出文件model.table,在将caff模型转换为ncnn模型时,加入model.table,如下所示,就可以得到量化的模型。

    ./caffe2ncnn deploy.prototxt model.caffemodel model_int8.param model_int8.bin 256 model.table

    上面256指的是quantizelevel量化级别,如果为0就不进行量化。如果某些层不想量化,打开model.table,输出那一层的量化参数。
    调用int量化后的模型,和原来ncnn调用不量化的模型一样,ncnn会自动调用量化模型。
    如果不行量化模型或者模型量化后精度较低,可以在ncnn下对模型优化ncnnoptimize。也就是合并模型某些层,比如conv层和relu层,具体如下:
    https://github.com/Tencent/ncnn/wiki/model-optimize
    调用命令很简单如下所示,其中65536表示2的16次方,即FP16,这表示模型优化还会将模型从float32转为float16。

    ncnnoptimize model.param model.bin model-opt.param model-opt.bin 65536
    

    对于squeezenet,就精度而言model和model-opt精度差不多,model_int8精度越降5%。就速度而言model-opt为model的95%,model_int8为model的90%。正常使用情况推荐:
    model>model-opt>model_int8

    3.4 部署和量化部分参考文件

  • OpenCV调用Caffe1
  • OpenCV调用Caffe2
  • OpenCV深度学习调用实例
  • ncnn入门1
  • ncnn入门2
  • caffe量化1
  • caffe量化2
  • ncnn量化详解
  • 本文只写了caffe分类模型的相关笔记,目标检测就没写了。因为用darknet的yolo比较多。有空在写caffe目标检测的笔记。

    本文来自博客园,作者:落痕的寒假,转载请注明原文链接:https://www.cnblogs.com/luohenyueji/p/16993315.html

     
    推荐文章