我一直在研究log-sum-exp问题。我有一个以对数形式存储的数字列表,我希望将它们相加并以对数形式存储。
天真算法是
def naive(listOfLogs):
return math.log10(sum(10**x for x in listOfLogs))
许多网站包括: logsumexp implementation in C? 和 http://machineintelligence.tumblr.com/post/4998477107/ 建议使用
def recommend(listOfLogs):
maxLog = max(listOfLogs)
return maxLog + math.log10(sum(10**(x-maxLog) for x in listOfLogs))
又名
def recommend(listOfLogs):
maxLog = max(listOfLogs)
return maxLog + naive((x-maxLog) for x in listOfLogs)
我不明白的是,如果建议的算法更好,为什么我们应该递归调用它? 那会提供更多的好处吗?
def recursive(listOfLogs):
maxLog = max(listOfLogs)
return maxLog + recursive((x-maxLog) for x in listOfLogs)
虽然我在问是否有其他技巧可以使这个计算在数值上更稳定?
答案 0 :(得分:9)
其他人的一些背景:当你直接计算以下类型的表达时
ln( exp(x_1) + exp(x_2) + ... )
你可能遇到两种问题:
exp(x_i)
可能会溢出(x_i
太大),导致数字无法合并exp(x_i)
可能下溢(x_i
太小),导致一堆零如果所有值都很大,或者所有值都很小,我们可以除以exp(const)
并将const
添加到ln
的外部以获得相同的值。因此,如果我们可以选择正确的const
,我们可以将值移到某个范围内以防止溢出/下溢。
OP的问题是,为什么我们为这个const而不是任何其他值选择max(x_i)
?为什么我们不递归地进行这种计算,选择每个子集的最大值并重复计算对数?
答案:因为无关紧要。
原因?假设x_1 = 10
很大,x_2 = -10
很小。 (这些数字在数量上甚至不是很大,对吗?)表达式
ln( exp(10) + exp(-10) )
会给你一个非常接近10的值。如果你不相信我,那试试吧。事实上,如果某些特定ln( exp(x_1) + exp(x_2) + ... )
比其他所有max(x_i)
大得多,x_i
通常会ln
非常接近exp(max(x_i) - some_constant)
。 (顺便说一句,这个函数形式,渐近地,实际上允许你从数学集合中数学选择最大值。)
因此,我们选择max而不是任何其他值的原因是因为较小的值几乎不会影响结果。如果它们下溢,它们本来就太小而无法影响总和,因为它将由最大数量和接近它的任何东西支配。在计算术语中,在计算{{1}}之后,小数字的贡献将小于ulp。因此,没有理由浪费时间递归计算较小值的表达式,如果它们将在最终结果中丢失。
如果你真的想要实现这个,你可以将{{1}}左右除以'中心'得到的值大约为1,以避免溢出和下溢,这可能会给你一些额外的结果中的精度数字。但是避免溢出对于避免下溢更为重要,因为前者决定了结果而后者没有,所以这样做就简单得多了。
答案 1 :(得分:2)
递归执行它并不是更好。问题只是你要确保你的有限精度算术不会在噪声中淹没答案。通过自己处理max,可以确保在最终答案中保留任何垃圾,因为它中最重要的部分可以保证通过。
道歉的解释。自己尝试一些数字(一个明智的列表,可能是[1E-5,1E25,1E-5])并看看会有什么感觉。
答案 2 :(得分:2)
正如您所定义的那样,您的recursive
函数永远不会终止。这是因为((x-maxlog) for x in listOfLogs)
仍然具有与listOfLogs
相同数量的元素。
我认为这也不容易解决,不会显着影响性能或精度(与非递归版本相比)。