带有repa的haskell双精度下溢

haskell double precision underflow with repa

我已经编写了一些代码,使用 repa 计算距离矩阵:

1
2
3
4
5
6
7
8
9
10
11
12
distance :: Int -> Int -> Mat -> Double
distance aindx bindx arr = let a = slice arr (Any :. aindx :. All)
                               b = slice arr (Any :. bindx :. All)-
                               sqdiff = R.map (\\x -> x*x) $ R.zipWith (-) a b
                            in sqrt $ sumAllS sqdiff

buildDistanceMatrix :: Mat -> Mat
buildDistanceMatrix m = let (Z :. height :. width) = R.extent m
                            cords = fromListUnboxed (Z :. (height * height) )  [ (x,y) | x <- [0..height-1], y <- [0..height-1]]
                            dist = R.smap (\\(a,b) -> distance a b m) cords
                            dmat = R.reshape (Z :. height  :. height ) dist
                         in R.computeS dmat

它似乎工作。但后来我添加了一个 QuickCheck:

1
2
3
4
5
prop_distmat :: Double -> Bool
prop_distmat d = let dvec = [d,0,0,0]
                     dmat = R.fromListUnboxed (Z :. (2::Int) :. (2::Int)) dvec
                     dist = buildDistanceMatrix dmat
                  in (R.toList dist) == [0.0, d, d, 0.0 ]

换句话说,被距离 D 分开的两点应该产生一个看起来像 [0,D,D,0] 的距离矩阵。在我的临时手动测试中,确实如此。但 QuickCheck 很快发现 5.0e-324 的距离会产生 [0,0,0,0]

的距离矩阵

1
2
distance matrix  *** Failed! Falsifiable (after 2 tests and 1074 shrinks):    
5.0e-324

这仅仅是因为 Doubles 的精度吗?我是否需要限制 QuickCheck 将发送的可能值?或者这是一个真正的错误?


您正在测试浮点数是否相等,这通常应该避免(在任何语言中,这不是 Haskell 特定的)。你也会得到一个无限大的双打。并且 sqrt (x*x) == x 通常不成立,即使对于那些你没有上溢或下溢的双打。因此,您需要同时替换 == 并检查差异最多是一些合理的 epsilon 并限制可能的值(或检查属性中的溢出)。


天真地计算向量的 L2 范数可能会在应用平方根函数之前很久就给出下溢或溢流。我引用知道的人的话:"Fortran 中对两个范数最稳健的计算有超过 200 行代码,但那是 25 年前的事了"。我建议搜索 Fortran 实现,然后使用可能出错的知识将其应用于您的 Haskell 实现。数字很??棘手;好消息是,大约 50 年前,大多数问题可能都在 Fortran 中得到了解决。