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

1 训练

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
else
RESIZE_HEIGHT=0
RESIZE_WIDTH=0
fi 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
fi 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
fi 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 训练部分参考文件

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

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曲线绘制有两种方法:

  1. 每种类别下,都可以得到m个测试样本为该类别的概率(矩阵P中的列)。所以,根据概率矩阵P和标签矩阵L中对应的每一列,可以计算出各个阈值下的假正例率(FPR)和真正例率(TPR),从而绘制出一条ROC曲线。这样总共可以绘制出n条ROC曲线。最后对n条ROC曲线取平均,即可得到最终的ROC曲线。
  2. 首先,对于一个测试样本: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 结果可视化部分参考文件

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 部署和量化部分参考文件

4 总结

本文只写了caffe分类模型的相关笔记,目标检测就没写了。因为用darknet的yolo比较多。有空在写caffe目标检测的笔记。

[深度学习] caffe分类模型训练、结果可视化、部署及量化笔记的更多相关文章

  1. 【转】[caffe]深度学习之图像分类模型AlexNet解读

    [caffe]深度学习之图像分类模型AlexNet解读 原文地址:http://blog.csdn.net/sunbaigui/article/details/39938097   本文章已收录于: ...

  2. [caffe]深度学习之图像分类模型VGG解读

    一.简单介绍 vgg和googlenet是2014年imagenet竞赛的双雄,这两类模型结构有一个共同特点是go deeper.跟googlenet不同的是.vgg继承了lenet以及alexnet ...

  3. 深度学习的seq2seq模型——本质是LSTM,训练过程是使得所有样本的p(y1,...,yT‘|x1,...,xT)概率之和最大

    from:https://baijiahao.baidu.com/s?id=1584177164196579663&wfr=spider&for=pc seq2seq模型是以编码(En ...

  4. 深度学习与计算机视觉教程(15) | 视觉模型可视化与可解释性(CV通关指南·完结)

    作者:韩信子@ShowMeAI 教程地址:http://www.showmeai.tech/tutorials/37 本文地址:http://www.showmeai.tech/article-det ...

  5. 人工智能深度学习Caffe框架介绍,优秀的深度学习架构

    人工智能深度学习Caffe框架介绍,优秀的深度学习架构 在深度学习领域,Caffe框架是人们无法绕过的一座山.这不仅是因为它无论在结构.性能上,还是在代码质量上,都称得上一款十分出色的开源框架.更重要 ...

  6. (zhuan) 126 篇殿堂级深度学习论文分类整理 从入门到应用

    126 篇殿堂级深度学习论文分类整理 从入门到应用 | 干货 雷锋网 作者: 三川 2017-03-02 18:40:00 查看源网址 阅读数:66 如果你有非常大的决心从事深度学习,又不想在这一行打 ...

  7. 时间序列深度学习:seq2seq 模型预测太阳黑子

    目录 时间序列深度学习:seq2seq 模型预测太阳黑子 学习路线 商业中的时间序列深度学习 商业中应用时间序列深度学习 深度学习时间序列预测:使用 keras 预测太阳黑子 递归神经网络 设置.预处 ...

  8. 深度学习 vs. 概率图模型 vs. 逻辑学

    深度学习 vs. 概率图模型 vs. 逻辑学 摘要:本文回顾过去50年人工智能(AI)领域形成的三大范式:逻辑学.概率方法和深度学习.文章按时间顺序展开,先回顾逻辑学和概率图方法,然后就人工智能和机器 ...

  9. Windows下mnist数据集caffemodel分类模型训练及测试

    1. MNIST数据集介绍 MNIST是一个手写数字数据库,样本收集的是美国中学生手写样本,比较符合实际情况,大体上样本是这样的: MNIST数据库有以下特性: 包含了60000个训练样本集和1000 ...

随机推荐

  1. 洛谷P2517 HAOI2010 订货 (费用流)

    标准的费用流问题,关键在于巧妙地建模 一共有n个月份,源点设为0,汇点设为n+1 1.源点向所有月份连边,容量为正无穷,费用为该月进货的费用 2.每个月向下一个月连边,容量为仓库容量,费用为存货费用 ...

  2. C++编程范式(函数)

    1 // 2 // main.cpp 3 // test 4 // 5 // Created by Shaojun on 30/5/2020. 6 // Copyright 2020 Shaojun. ...

  3. 挑战海量数据:基于Apache DolphinScheduler对千亿级数据应用实践

    点亮 ️ Star · 照亮开源之路 GitHub:https://github.com/apache/dolphinscheduler 精彩回顾 近期,初灵科技的大数据开发工程师钟霈合在社区活动的线 ...

  4. Node.js躬行记(24)——低代码

    低代码开发平台(LCDP)是无需编码(0代码)或通过少量代码就可以快速生成应用程序的开发平台.让具有不同经验水平的开发人员可以通过图形化的用户界面,通过拖拽组件和模型驱动的逻辑来创建网页和移动应用程序 ...

  5. springboot+thymeleaf中前台页面展示中、将不同的数字替换成不同的字符串。使用条件运算符

    主要用到的知识就是thyme leaf中的条件运算符 表达式:(condition)?:then:else 当条件condition成立时返回then.否则返回else 具体代码:<td th: ...

  6. C语言爱心表白程序

    #include <stdio.h> #include <math.h> #include <windows.h> #include <tchar.h> ...

  7. 【JavaSE】JDK 环境配置

    下载 JDK 网站网址:oracle 安装 JDK 双击运行安装包 配置环境变量

  8. Ubuntu编译安装php7.4

    Ubuntu编译安装php7.4  [root@ubuntu2004 php-7.4.30]#apt install gcc libssl-dev libxml2-dev libsqlite3-dev ...

  9. CSS 动画一站式指南

    CSS 动画一站式指南 目录 CSS 动画一站式指南 1. CSS 动画 1.1 变换 1.1.1 变换属性介绍 1.1.2 变换动画实践 1.2 过渡 1.2.1 过渡属性介绍 1.2.2 过渡动画 ...

  10. JavaWeb3

    1. 会话技术 会话:一次会话中包含多次请求和响应 一次会话:浏览器第一次给服务器资源发送请求,会话建立,直到有一方断开为止 功能:在一次会话的范围内的多次请求间共享数据 方式: 客户端会话技术:Co ...