行列式、秩和逆矩阵计算的 Haskell 实现 - 输入矩阵大小限制

Haskell implementation of Determinant, Rank and Inverse Matrix calculation- input matrix size limitation

提问人:Menish 提问时间:6/16/2023 最后编辑:Menish 更新时间:6/17/2023 访问量:137

问:

我是 Haskell 的新手。 作为一门学术课程的一部分,我被要求在 Haskell 中实现一个函数,用于计算给定矩阵的行列式、秩和逆矩阵。

我使用高斯消元法(对原始矩阵和另一个初始化为单位矩阵的矩阵执行相同的行运算)。我使用一次性方法“即时”累积到排名和决定因素。

对于高达 600x600 的输入矩阵,该函数的行为符合预期(在本例中,执行时间为 63 秒)。 任何超过该大小的输入都会导致过多的内存使用和不切实际的计算时间。

输入是 3 个不同的矩阵,大小为 800x800、800x800 和 1000x1000。

值得一提的是,我不允许使用任何外部库(例如 HMatrix 或 Data.Matrix)

我尽了最大努力来优化代码,但由于我是新手,我没有成功地使我的代码适用于大于 600x600 的大小。

import Data.Time.Clock
import System.IO

type Matrix = [[Double]]

-- Row operations

swapRows :: Int -> Int -> Matrix -> Matrix
swapRows i j mat = take i mat ++ [mat !! j] ++ (drop (i+1) . take j $ mat) ++ [mat !! i] ++ drop (j+1) mat

scaleRow :: Int -> Double -> Matrix -> Matrix
scaleRow i k mat = take i mat ++ [map (* k) (mat !! i)] ++ drop (i+1) mat

addRows :: Int -> Int -> Double -> Matrix -> Matrix
addRows i j k mat = take i mat ++ [zipWith (+) (mat !! i) (map (* k) (mat !! j))] ++ drop (i+1) mat

-- Gaussian elimination

eliminate :: Matrix -> (Matrix, Matrix, Double, Int)
eliminate mat = foldr eliminateRow (mat, identityMatrix (length mat), 1.0, rank) [0..length mat-1]
  where
    rank = length mat  -- Initialize rank as the number of rows

    eliminateRow row (mat, invMat, detAccum, rank) = foldl eliminateEntry (mat, invMat, detAccum, rank) [0..length mat-1]
      where
        eliminateEntry (mat, invMat, detAccum, rank) col
          | row == col = (scaleRow row (1 / pivot) mat, scaleRow row (1 / pivot) invMat, detAccum * pivot, rank)
          | mat !! col !! row /= 0 = (addRows col row (-ratio) mat, addRows col row (-ratio) invMat, detAccum, rank)
          | otherwise = (mat, invMat, detAccum, rank - 1)
          where
            pivot = mat !! row !! row
            ratio = (mat !! col !! row) / pivot

-- Matrix operations

identityMatrix :: Int -> Matrix
identityMatrix n = [[fromIntegral (fromEnum (i == j)) | j <- [1..n]] | i <- [1..n]]

-- create sub matrix n x n from matrix mat
subMatrix :: [[Double]] -> Int -> [[Double]]
subMatrix mat n = take n (map (take n) mat) 

-- Testing

main :: IO ()
main = do
  --let submat = [[1, 2], [3, 4]]  -- 3x3 invertible matrix
  
  contenm_headMulScal <- readFile "det_matrix(800 x 800).txt"
  let mat = map (map read . words) (lines contenm_headMulScal) :: [[Double]]
  
  let piece_dim = 600
  let submat = subMatrix mat piece_dim
  
  tt_start <- getCurrentTime
  let (refMat, inverse, determinant, rank) = eliminate submat  -- Calculate the ref matrix, determinant, and rank
  t_end <- getCurrentTime
  --putStrLn "Original Matrix:"
  --printMatrix submat
  putStrLn "\nDeterminant:"
  print determinant
  putStrLn "\nInverse Matrix:"
  --printMatrix inverse
  putStrLn $ "First element in the inverse matrix: " ++ show (head (head inverse))
  putStrLn "\nRank:"
  print rank
  tt_end <- getCurrentTime
  
  print "Runtime:"
  print (diffUTCTime tt_end tt_start)

printMatrix :: Matrix -> IO ()
printMatrix mat = mapM_ (putStrLn . unwords . map show) mat

例如,如您所见,我从 800x800 输入中取出 600x600 的“一块”。它有效。拿一个更大的部分(700x700,或所有输入),它变得不切实际 - 大约一个小时的计算,由于内存使用过多,计算机完全卡住了。

感谢丹尼尔·瓦格纳(Daniel Wagner)指出: 对于想在家玩的人,请尝试:

countingMatrix :: Int -> Matrix
countingMatrix n = [[fromIntegral (j*n+i) | j <- [1..n]] | i <- 
[1..n]] 

代替从磁盘加载的矩阵。

我将不胜感激任何建议。

Haskell GHC GHCI 矩阵逆 行列式

评论

1赞 willeM_ Van Onsem 6/16/2023
你能展示一下你尝试过什么,什么不起作用吗?
0赞 Menish 6/16/2023
是的,对不起。我也是这个论坛的新手。善良的模组编辑了我的帖子,因此代码现在出现在问题正文中。谢谢,对不起。
2赞 Daniel Wagner 6/16/2023
对于想在家玩的人,请尝试代替从磁盘加载的矩阵。countingMatrix :: Int -> Matrix; countingMatrix n = [[fromIntegral (j*n+i) | j <- [1..n]] | i <- [1..n]]
1赞 willeM_ Van Onsem 6/16/2023
通常,人们使用 LU 分解将矩阵转换为允许确定行列式、逆等的形式:en.wikipedia.org/wiki/LU_decomposition 这可能看起来像是在做一些不相关的工作,但事实证明,在 LU 形式中,大多数矩阵属性可以非常快速地确定。
1赞 n. m. could be an AI 6/17/2023
!!不是你的朋友。它在索引参数中是线性的,因此当算法幼稚地从数组转换为列表时,性能通常很差。试着想象它不存在,并避免一般的指数。列表旨在映射、折叠、压缩......但未编入索引。!!

答:

3赞 Daniel Wagner 6/16/2023 #1

似乎存在空间泄漏/懒惰问题。我不怪你:我发现获得我想要的评估行为出奇地挑剔,即使我已经非常确定问题出在哪里!

你要确保的是,在创建新矩阵时,你不会保留对旧矩阵的任何引用。如果这样做,则 GC 无法收集旧矩阵。特别是,引用包括类似或或类似的潜在计算。所以!让我们讨论一下我必须做出哪些改变才能实现这一点。take i oldMatrixoldMatrix !! i

首先:当我们创建一个新矩阵时,它主要是旧矩阵中行的副本,但有一两行新矩阵,我们需要一种方法来表示“遍历整个矩阵,并强制其计算足够多,以便您将指针复制到特定行,而不是稍后在旧矩阵中查找该行的计算”。为了实现这一点,我们将创建一个函数来强制列表的每个元素,但仅限于 WHNF。请注意,由于我们传递的是矩阵,因此这不是完整的评估!我们不会一直强制到矩阵元素,而只强制到矩阵行的级别。在我第一次尝试时,我弄错了,一直强制到元素级别,无意中引入了一个非常严重的时间性能回归。

seqElements :: [a] -> [a]
seqElements [] = []
seqElements as@(a:at) = a `seq` seqElements at `seq` as

我们将在每个行操作的开头调用它。

swapRows i j mat = seqElements $ {- old implementation -}
scaleRows i k mat = seqElements $ {- old implementation -}
addRows i j k mat = seqElements $ {- old implementatiotn -}

这构成了我们手动强制注释的“基本情况”。不幸的是,我们现在需要将其一直传播到表达式树中——每个调用者都需要确保它在字段中使用其中一个数据结构创建的任何数据结构也将该数据结构的评估与字段的评估联系起来。在 中,这意味着我们需要一个更严格的四元组版本。eliminateEntry

quadruple a b c d = a `seq` b `seq` c `seq` d `seq` (a, b, c, d)
-- then, later, we replace (,,,) with quadruple:
        eliminateEntry (mat, invMat, detAccum, rank) col
          | row == col = quadruple (scaleRow row (1 / pivot) mat) (scaleRow row (1 / pivot) invMat) (detAccum * pivot) rank
          | mat !! col !! row /= 0 = quadruple (addRows col row (-ratio) mat) (addRows col row (-ratio) invMat) detAccum rank
          | otherwise = quadruple mat invMat detAccum (rank - 1)

最后,在 中,我建议从 切换到 。在我的测试中,在这种情况下,它似乎没有区别,但这是一个养成的好习惯;提供的额外懒惰几乎从来都不是必需的,而且往往是有害的。eliminateRowfoldlfoldl'foldl

    eliminateRow row (mat, invMat, detAccum, rank) = foldl' eliminateEntry (mat, invMat, detAccum, rank) [0..length mat-1]

通过这些更改,我现在看到内存使用量要低得多:600x600 矩阵为 ~50 秒,96MiB,800x800 矩阵为 ~128 秒,167MiB。

如果需要的话,可能有机会进行重大的进一步优化;例如,我希望为您的矩阵切换到可变数组表示将是另一个很大的推动力。

评论

0赞 Menish 6/16/2023
非常感谢丹尼尔!我非常感谢你花时间帮助我解决这个问题。我已经纳入了你所指出的所有变化。代码现在以更好的时间运行,即 40 分钟。不过还是不实用。我忘了提到我的输入矩阵包含非常小的浮点数。这可能是使我的代码在机器上运行速度变慢的另一个因素。再次感谢!
1赞 Daniel Wagner 6/17/2023
@Menish 对于 800x800 矩阵来说,这是 40 分钟的数字吗?如果是这样,您如何运行代码?我在编译时没有看到这种行为(有或没有优化),但我还没有测试解释的行为,所以如果你使用的是 ghci 或 runhaskell,你可能会通过切换到使用 ghc 来得到很好的服务。
0赞 Menish 6/17/2023
嗨,丹尼尔。再次感谢您抽出时间接受采访。事实上,800x800 矩阵的运行时间为 40 分钟,它基本上是一个充满范围 (-1,1) 的随机浮点数的矩阵。不过,在另一个 800x800 矩阵上,它运行 5 分钟(这很棒!基本上有 3 个文本文件,800x800、800x800 和 1000x1000,每个矩阵都有特定的特征来测试一次秩、一次行列式和一次逆矩阵。我使用 ghc -O2 filename.hs 进行编译,然后运行可执行文件。还有其他选择吗?如果需要,我还可以共享输入矩阵。
1赞 Daniel Wagner 6/17/2023
@Menish好的。那么在这一点上,似乎最有可能取得成果的改进是使用比高斯消元更好的算法。这是一个起点,人们已经思考了很长时间,并成功地思考了如何做得更好。