tf2.0中metrics原理

2020/4/5代码更新:
之前的F1代码在分布式训练过程中有问题,但问题不是出在我的metrics没有考虑分布式上(事实上,keras.metrics.Metric这个抽象类已经替我们考虑好了分布式的问题),而是代码写错了,正确的如下:

class F1_score(keras.metrics.Metric):
  def __init__(self, thresholds=0.5, name='f1', **kwargs):
    super(F1_score, self).__init__(name=name, **kwargs)
    self.tp = self.add_weight(name='tp', initializer='zeros')
    self.fp = self.add_weight(name='fp', initializer='zeros')
    self.fn = self.add_weight(name='fn', initializer='zeros')
    self.thresholds=thresholds

  def update_state(self, y_true, y_pred, sample_weight=None):
    
    y_pred=tf.cast(tf.where(y_pred>self.thresholds,1,0),tf.int8)
    y_true=tf.cast(y_true,tf.int8)
    
    tp=tf.math.count_nonzero(y_pred*y_true,dtype=tf.float32)
    fp=tf.math.count_nonzero(y_pred*(1-y_true),dtype=tf.float32)
    fn=tf.math.count_nonzero((1-y_pred)*y_true,dtype=tf.float32)
    
    self.tp.assign_add(tp)
    self.fp.assign_add(fp)
    self.fn.assign_add(fn)
    
  def result(self):
    min_delta=1e-6
    return 2*self.tp/(2*self.tp+self.fp+self.fn+min_delta)

  def reset_states(self):
    self.tp.assign(0.)
    self.fp.assign(0.)
    self.fn.assign(0.)

感觉出错原因可能是我的f1不应该定义为状态变量,欢迎指点!

======================== 分割线 ============================

最近写的代码涉及到的metric包括precision,recall,f1,无奈的是tf2.0中没有f1的指标。看了很多人的博客说是f1=2 * p* r / ( p + r )。公式是这样没错,但是如果不结合tf.keras.metric的计算原理来写代码那就大错特错。
首先上正确的F1书写方式:

class F1_score(keras.metrics.Metric):
  def __init__(self, thresholds=0.5, name='f1', **kwargs):
    super(F1_score, self).__init__(name=name, **kwargs)
    self.f1 = self.add_weight(name='f1', initializer='zeros')
    self.tp = self.add_weight(name='tp', initializer='zeros')
    self.fp = self.add_weight(name='fp', initializer='zeros')
    self.fn = self.add_weight(name='fn', initializer='zeros')
    self.thresholds=thresholds

  def update_state(self, y_true, y_pred, sample_weight=None):
    min_delta=1e-6
    y_pred=tf.cast(tf.where(y_pred>self.thresholds,1,0),tf.int8)
    y_true=tf.cast(y_true,tf.int8)
    
    tp=tf.math.count_nonzero(y_pred*y_true,dtype=tf.float32)
    fp=tf.math.count_nonzero(y_pred*(1-y_true),dtype=tf.float32)
    fn=tf.math.count_nonzero((1-y_pred)*y_true,dtype=tf.float32)
    
    self.tp.assign_add(tp)
    self.fp.assign_add(fp)
    self.fn.assign_add(fn)

    self.f1.assign(2*self.tp/(2*self.tp+self.fp+self.fn+min_delta))
    
  def result(self):
    return self.f1

  def reset_states(self):
    # The state of the metric will be reset at the start of each epoch.
    self.f1.assign(0.)
    self.tp.assign(0.)
    self.fp.assign(0.)
    self.fn.assign(0.)

在说怎么书写正确的f1之前,先说tf.keras.metrics.Precision()的原理。代码:

pre=tf.keras.metrics.Precision()

tf2.0中metrics原理_第1张图片
第一个结果没问题,
p1=tp/(tp+fp)=2/(2+2)=0.5
但是第二次似乎有问题
p2=tp/(tp+fp)=1/(1+2)=0.667

这个问题困扰了我一天,我以为是tensorflow实现的有问题,还自己重新实现了下(错误的代码),见后面。

碰巧这两天逛StackOverflow,发现很多人都提到一个概念,average loss, average metrics,average tp,突然恍然大悟,原来上面的precision是这样计算的:
p2=(tp1+tp2)/(tp1+tp2+fp1+fp2)=3/7。
正好就是上面的结果!

其实,tensorflow里面的loss,还有metric(auc, acuracy, p,r,tp,fp,tn,fn等等)都是这种average的概念。每次计算指标的数据是到目前batch当前所有的数据,并在下个epoch开始后重置清零。

之所以这样做,是因为我们如果逐个batch的去计算这些参数意义不大,而如果按照这种平均的思想,一个epoch结束指标是整个数据集上的结果,而不是一个很小的batch的局部结果(局部结果意义不大)。这也说明,采用这种方法来计算metrics时,如果我们不是逐个epoch的去比较metrics而是逐个batch比较的话意义不大,因为前者评价的数据是整个数据集,而后者只是部分。

而实际上,tf.keras内置的loss,metrics都考虑分布式训练的影响,上述代码计算f1没有考虑分布式(后面补上),所以如果同时用gpu,cpu会报错,禁止用gpu后正常。

错误的实现Precision, Recall。

class Precision(keras.metrics.Metric):
  def __init__(self, thresholds=0.5, name='p', **kwargs):
    super(Precision, self).__init__(name=name, **kwargs)
    self.p = self.add_weight(name='p', initializer='zeros')
    self.thresholds=thresholds

  def update_state(self, y_true, y_pred, sample_weight=None):
    min_delta=1e-6
    y_pred=tf.cast(tf.where(y_pred>self.thresholds,1,0),tf.int8)
    y_true=tf.cast(y_true,tf.int8)
    tp=tf.math.count_nonzero(y_pred*y_true,dtype=tf.float32)
    fp=tf.math.count_nonzero(y_pred*(1-y_true),dtype=tf.float32)
    self.p.assign(tp/(tp+fp+min_delta))
    
  def result(self):
    return self.p

  def reset_states(self):
    # The state of the metric will be reset at the start of each epoch.
    self.p.assign(0.)

class Recall(keras.metrics.Metric):
  def __init__(self, thresholds=0.5, name='r', **kwargs):
    super(Recall, self).__init__(name=name, **kwargs)
    self.r = self.add_weight(name='r', initializer='zeros')
    self.thresholds=thresholds

  def update_state(self, y_true, y_pred, sample_weight=None):
    min_delta=1e-6
    y_pred=tf.cast(tf.where(y_pred>self.thresholds,1,0),tf.int8)
    y_true=tf.cast(y_true,tf.int8)
    tp=tf.math.count_nonzero(y_pred*y_true,dtype=tf.float32)
    fn=tf.math.count_nonzero((1-y_pred)*y_true,dtype=tf.float32)
    self.r.assign(tp/(tp+fn+min_delta))
    
  def result(self):
    return self.r

  def reset_states(self):
    # The state of the metric will be reset at the start of each epoch.
    self.r.assign(0.)

你可能感兴趣的:(细节)