添加自定义层- 打印precision-recall的调和平均指标F1值

由于caffe在训练时,默认只会打印准确率和loss等相关信息,但是在特定任务中,可能还想看下训练过程,每一类的精确率和召回率是怎么变化的,所以这里添加一个自定义层,用于打印precision-recall信息,但是由于当类别多时,每个类都输出P、R的话,有点杂乱,所以这里,我们计算精确率和召回率的调和平均,F1值,这样的话,当有10个类别时,一到测试阶段,就打印出10个类各自的F1值。

按照添加自定义层的标准步骤,我们按照以下步骤:


(1)创建头文件 precision_recall_layer.hpp

因为这个功能,跟caffe里自带的Accuracy层很相似,所以,我们可以参考accuracy_layer.hpp文件的框架,编写我们需要的头文件precision_recall_layer.hpp

#ifndef CAFFE_PRECISION_RECALL_LAYER_HPP_
#define CAFFE_PRECISION_RECALL_LAYER_HPP_

#include <vector>
#include "caffe/blob.hpp"
#include "caffe/layer.hpp"
#include "caffe/proto/caffe.pb.h"
#include "caffe/layers/loss_layer.hpp"

namespace caffe {

template <typename Dtype>
class PrecisionRecallLayer : public Layer<Dtype> {
 public:

  explicit PrecisionRecallLayer(const LayerParameter& param)
      : Layer<Dtype>(param) {}

  virtual void Reshape(const vector<Blob<Dtype>*>& bottom,
      const vector<Blob<Dtype>*>& top);

  virtual inline const char* type() const { return "PrecisionRecall"; }
  virtual inline int ExactNumBottomBlobs() const { return 2; }   //需要两个输入 
  virtual inline int ExactNumTopBlobs() const { return 1; }   //产生一个输出


 protected:
  virtual void Forward_cpu(const vector<Blob<Dtype>*>& bottom,
      const vector<Blob<Dtype>*>& top);

  virtual void Backward_cpu(const vector<Blob<Dtype>*>& top,
	  const vector<bool>& propagate_down, const vector<Blob<Dtype>*>& bottom) {}
};

}  // namespace caffe

#endif  // CAFFE_PRECISIONRECALL_LAYER_HPP_

(2)创建源文件 precision_recall_layer.cpp

#include <functional>
#include <utility>
#include <vector>
#include "caffe/layers/precision_recall_layer.hpp"
#include "caffe/util/math_functions.hpp"

namespace caffe {

template <typename Dtype>
void PrecisionRecallLayer<Dtype>::LayerSetUp(
  const vector<Blob<Dtype>*>& bottom, const vector<Blob<Dtype>*>& top) {
}

template <typename Dtype>
void PrecisionRecallLayer<Dtype>::Reshape(
  const vector<Blob<Dtype>*>& bottom, const vector<Blob<Dtype>*>& top) {
	const int num_label=bottom[0]->channels();    //种类数,如 mnist 是10类
  	vector<int> top_shape(2);  //有十个类,每一类对应的F1值-精确率和召回率的调和平均
	top_shape[0]=num_label;
	top_shape[1]=1;
  	top[0]->Reshape(top_shape);   //设置top的维度是 num_label*1
}

template <typename Dtype>
void PrecisionRecallLayer<Dtype>::Forward_cpu(const vector<Blob<Dtype>*>& bottom,
    const vector<Blob<Dtype>*>& top) {

	Dtype* output=top[0]->mutable_cpu_data();  //我们需要计算的F1值  维度是num_label*1

	const int num_label=bottom[0]->channels();    //种类数,如 mnist 是10类
	const int batch_size=bottom[0]->num();            //一个batch的图片数量,比如 100张
	for(int k=0;k<num_label;++k){    //将类k作为正例,其它作为反例
		Dtype TP,FN,FP,TN;
		TP=FN=FP=TN=0;
		for(int i=0;i<batch_size;++i){
			int realLabel=static_cast<int>(bottom[1]->data_at(i,0,0,0));   //该图片的真实标签
			int predictLabel;          //预测标签
			Dtype mmax=-1000000;
			for(int j=0;j<num_label;++j){
				Dtype tmp=bottom[0]->data_at(i,j,0,0);
				if(tmp>mmax){
					mmax=tmp;
					predictLabel=j;
				}
			}
			if(realLabel==k&&predictLabel==k)
				TP++;
			else if(realLabel==k&&predictLabel!=k)
				FN++;
			else if(realLabel!=k&&predictLabel==k)
				FP++;
			else
				TN++;
		}
		int index=top[0]->offset(k,0);
		output[index]=2*TP/(batch_size+TP-TN);
	}
}

INSTANTIATE_CLASS(PrecisionRecallLayer);
REGISTER_LAYER_CLASS(PrecisionRecall);

}  // namespace caffe


(3) 修改caffe_master/src/caffe/proto/caffe.proto文件

我们需要将我们定义的层,写进注册文件里,定位到310行

// NOTE
// Update the next available ID when you add a new LayerParameter field.
//
// LayerParameter next available layer-specific ID: 148 (last added: center_loss_param)
可以看出下一个可用ID是148,所以,我们在该massage里,可以编写
optional PrecisionRecallParameter precision_recall_param = 148;
并更新上面的提示信息为:
// LayerParameter next available layer-specific ID: 149 (last added: precision_recall_param)
以便以后继续添加自定义层

接下来,虽然我们的这个自定义层,是没有参数的,但我们仍然要定义该层,可以直接在该文件末尾添加

message PrecisionRecallParameter {}
虽然里面上面都没有,但是不可缺少的一步。

(4)因为修改了caffe.proto文件,我们回到caffe根目录下

make clean

make all -j

(5)调用实例,结合mnist数据集的例子

layer {
   name: "F1"
   type: "PrecisionRecall"
   bottom: "ip2"
   bottom: "label"
   top: "F1"
   include {
     phase: TEST
   }
}

训练过程中,各阶段效果如下:

刚开始时


迭代500次后

 

迭代1500次

1W次后

其中的十个F1值,分别代表10个类的F1值(测试的时候,比如计算数字1类的F1值,则把标签为1的图片为正样本,其他9个类别的未负样本)



最后,该代码进行计算F1值,默认的上一层是全连接层,所以没有像caffe中标准层的代码那么严格检查维度信息,也不是最高效的计算方法,只是为了直观而编写的(caffe中各层的计算,都是用封装好的线性代数库的函数计算的)。

猜你喜欢

转载自blog.csdn.net/zxj942405301/article/details/72630379