我目前正尝试在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
存储分类器的分数,还是只有true
或false
?
如果有人可以快速查看我的代码并帮助我找到错误,那就太棒了。 THX!
答案 0 :(得分:1)
这个答案是不正确的,因为它从OP评论中假设算法需要每项评估误报和真正的正分配。事实上,变量tp
和fp
是整个数据集的跟踪总计,只是在假设循环中的当前预测变为正数时进行调整。看到我的其他答案。
在此代码块中:
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
答案 1 :(得分: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
(因此fp
和tp
逻辑必须为0
因为根本没有允许p
。但这并不重要,因为你没有使用该值。
在循环内部,正在进行的是代码正在跟踪如果阈值设置在当前项目之上,则总误报率和真实积极数将是多少。当您将此栏放低到具有相同分数的项目组时,它们将预测正值(无需对threshold
变量进行测试,这会使您感到困惑)。您所要做的就是将这些正值排序为tp
或fp
计数。对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]
除了改变测试之外,我删除了一个不准确的评论(“估算器也是正确的”) - 我们不会在此代码中判断估算器是否对于单个值是“正确的”,我们只是看到在特定截止点得分fp
与tp
的得分有多好。排序列表上的单次传递过程依赖于以下事实:根据对fp
和tp
计数的更改,这将是从最后一个点开始的小增量更改。
现在应该从[0.0,0.0]
转到[1.0,1.0]
r.each do |point|
puts "(#{point[0].round(3)},#{point[1].round(3)})"
end