【问题标题】:Ruby implementation for ROC curveROC曲线的Ruby实现
【发布时间】:2013-04-11 04:36:16
【问题描述】:

我目前正在尝试在 ruby​​ 中实现 ROC 曲线的计算。我尝试将伪代码从http://people.inf.elte.hu/kiss/13dwhdm/roc.pdf(参见第 6 站点,第 5 章,算法 1“生成 ROC 点的有效方法”)转换为 Ruby 代码。

我制定了一个简单的示例,但我总是得到超过1.0 的值以供召回。我想我误解了一些东西,或者在编程时犯了一个错误。到目前为止,这是我所了解的:

# results from a classifier
# index 0: users voting
# index 1: estimate from the system
results = [[5.0,4.8],[4.6,4.2],[4.3,2.2],[3.1,4.9],[1.3,2.6],[3.9,4.3],[1.9,2.4],[2.6,2.3]]
# over a score of 2.5 an item is a positive one
threshold = 2.5
# sort by index 1, the estimate
l_sorted = results.sort { |a,b| b[1] <=> a[1] }

# count the real positives and negatives
positives, negatives = 0, 0
positives, negatives = 0, 0
l_sorted.each do |item|
  if item[0] >= threshold
    positives += 1
  else
    negatives += 1
  end
end

fp, tp = 0, 0
# the array that holds the points
r = []
f_prev = -Float::INFINITY

# iterate over all items
l_sorted.each do |item|
  # if the score of the former iteration is different,
  # add another point to r
  if item[1]!=f_prev
    r.push [fp/negatives.to_f,tp/positives.to_f]
    f_prev = item[1]
  end
  # if the current item is a real positive
  # (user likes the item indeed, and estimater was also correct)
  # add a true positive, otherwise, add a false positve
  if item[0] >= threshold && item[1] >= threshold
    tp += 1
  else
    fp += 1
  end
end

# push the last point (1,1) to the array
r.push [fp/negatives.to_f,tp/positives.to_f]

r.each do |point|
  puts "(#{point[0].round(3)},#{point[1].round(3)})"
end

基于数组的results 数组,代码尝试计算点。我不确定f_prev 是什么意思。是在f_prev 中存储的分类器的分数,还是只有在truefalse 时?

如果有人可以快速查看我的代码并帮助我找出错误,那就太棒了。谢谢!

【问题讨论】:

  • 我习惯了分类器是 0 或 1 为什么你的索引 0 是一个分数呢?您确定您的问题需要 ROC,它看起来更像是一种回归吗?编辑:我只有 ROC 下区域的简化代码,而不是曲线本身。这很简单,但可能不是您需要的。
  • 我已经运行了您编写的代码而没有修改,并且召回率低于 1.0(结果数组的索引 1)。您的意思是 fp rate 超过 1.0 吗?
  • 感谢您的 cmets!不,索引 0 是召回率(在 X 轴上),索引 1 是精度(在 Y 轴上)。还是我错了?

标签: ruby algorithm roc


【解决方案1】:

我的第二个答案是分析您的代码,并指出我认为您在哪里犯了一些错误或感到困惑。我假设您想要重现一个类似于您链接的 PDF 的第 864 页上看到的图表。

类似于 p864 上的 ROC 图是一个图表,显示了您的预测模型中假阳性率和真阳性率之间的可用折衷。要查看所有可能的折衷方案,您需要访问阈值会产生影响的所有数据点,并绘制它们的假阳性与真阳性率。

您的第一个困惑点似乎是您有一个“用户投票”浮动分数,而不是一个真/假类别。 PDF 中的示例已经确定了用于绘制 ROC 的 p/n 案例。

# results from a classifier
# index 0: users voting
# index 1: estimate from the system
results = [[5.0,4.8],[4.6,4.2],[4.3,2.2],[3.1,4.9],[1.3,2.6],[3.9,4.3],[1.9,2.4],[2.6,2.3]]

所以我觉得你最好有

results = [[true,4.8],[true,4.2],[true,2.2],[true,4.9],[false,2.6],[true,4.3],[false,2.4],[true,2.3]]

你开始绘制 ROC 之前。内联进行此转换会很好,但您需要将如何生成测试数据的问题与 ROC 图分开 - 例如,您的用户分数和机器估计分数在同一尺度上的事实是无关紧要的。

这导致threshold 变量。您可以使用例如2.5 转换您的用户数据,但这与您的 ROC 图无关。实际上,要获得完整的 ROC 图,您需要测试多个阈值以了解它们如何影响真假阳性率。

# over a score of 2.5 an item is a positive one
threshold = 2.5

这会将值按相反的顺序排序,得分最高的项目在前。无论哪种方式都可以,但对我来说,这意味着你想从一个高阈值开始(你所有的分数都预测false),并且在图表上的位置[0.0,0.0]

# sort by index 1, the estimate
l_sorted = results.sort { |a,b| b[1] <=> a[1] }

下面的代码看起来足够准确,但实际上它只是将测试的正负相加,所以不应该混淆阈值的概念:

# count the real positives and negatives
positives, negatives = 0, 0
positives, negatives = 0, 0
l_sorted.each do |item|
  if item[0] >= threshold
    positives += 1
  else
    negatives += 1
  end
end

一种更好的 Ruby 放置相同逻辑的方式,假设您将用户分数替换为 true/fasle 值在其他地方可能是

positives = l_sorted.select { |item| item[0] }.count
negatives = l_sorted.count - positives

这看起来不错,你确实从 [0.0,0.0] 开始

fp, tp = 0, 0
# the array that holds the points
r = []

但是,这看起来像是起始阈值

f_prev = -Float::INFINITY

所以在我看来,逻辑上是肯定的Float::Infinity,这样你所有的预测最初都是false(因此fptp逻辑上必须是0,因为不允许p全部)。不过没关系,因为您不使用该值。


在循环内部,代码正在跟踪如果阈值设置为刚好高于当前项目,则总误报和真阳性将是多少。当您通过具有相同分数的项目组降低此条时,它们将预测正值(无需与 threshold 变量进行测试,这会让您感到困惑)。您所要做的就是将这些正值分类为tpfp 计数。与f_prev 的检查只是帮助对相似的项目进行分组,如果 3 个预测具有相同的分数,您只会绘制一个点。

# iterate over all items
l_sorted.each do |item|
  if item[1]!=f_prev
    # Plot a point, assuming all predictions with a score equal or lower than current
    # item are thresholded out as negative.
    r.push [fp/negatives.to_f,tp/positives.to_f]
    f_prev = item[1]
  end
  # Assume the current prediction is now positive, and calculate how that affects the curve
  # if the current test item is a real positive
  # add to true positives, otherwise, it has become a false positve
  if item[0]
    tp += 1
  else
    fp += 1
  end
end

# push the last point (1,1) to the array
r.push [fp/negatives.to_f,tp/positives.to_f]

除了更改测试之外,我还删除了一个不准确的注释(“估计器也是正确的”)——我们没有在这段代码中判断估计器对于单个值是否“正确”,我们只是看到fptp 在特定截止点的得分如何。排序列表上的单次通过过程依赖于这样一个事实,即这将是基于对 fptp 计数的更改,从最后绘制的点开始的一个小的增量变化。

现在应该从 [0.0,0.0] 变为 [1.0,1.0]

r.each do |point|
  puts "(#{point[0].round(3)},#{point[1].round(3)})"
end

【讨论】:

  • 你真是太棒了!非常感谢,你真的帮了我很大的忙!
【解决方案2】:

这个答案是不正确的,因为它从 OP 评论中假设该算法需要 每个项目 评估假阳性和真阳性分配。事实上,变量tpfp 正在跟踪整个数据集的总数,并且只是在假设循环中的当前预测变为正数的情况下进行调整。请参阅我的其他答案。


在这个代码块中:

  if item[0] >= threshold && item[1] >= threshold
    tp += 1
  else
    fp += 1
  end

您似乎将“真阳性”以外的任何东西都算作“假阳性”。

这是不正确的,您忽略了结果是真或假阴性分类的可能性。试试这个:

  if item[0] >= threshold && item[1] >= threshold
    tp += 1
  elsif item[0] < threshold && item[1] >= threshold
    fp += 1
  end

或者,稍微干燥一点

  if item[1] >= threshold
    if item[0] >= threshold
      tp += 1
    else
      fp += 1
    end
  end

【讨论】:

  • 我也有同样的想法。但是不应该是elsif item[0] &lt; threshold &amp;&amp; item[1] &gt;= threshold,因为实例必须是负数并且分类器将其分类为正数吗?
  • 感谢您的回答。我玩了一段时间。但是如果item[1] &lt; thresholditem[0] &gt;= threshold 会怎样?这不会增加召回tp/(tp+fn)所需的假阴性吗?
  • 我试过了,但现在我得到的最高值是(0.5,0.667)。不应该是(1/1)吗?
  • 如果所有的预测都是肯定的,你只能从[fp/negatives.to_f,tp/positives.to_f]得到[1.0,1.0]的结果。在您的示例代码中并非如此。这是否意味着您对 PDF 的解释有误,我无法告诉您。
猜你喜欢
  • 2019-02-27
  • 2018-01-03
  • 2019-12-26
  • 2019-11-27
  • 2014-07-20
  • 2012-05-02
  • 2018-04-25
  • 2019-02-09
  • 1970-01-01
相关资源
最近更新 更多