提问人:Menish 提问时间:6/16/2023 最后编辑:Menish 更新时间:6/17/2023 访问量:137
行列式、秩和逆矩阵计算的 Haskell 实现 - 输入矩阵大小限制
Haskell implementation of Determinant, Rank and Inverse Matrix calculation- input matrix size limitation
问:
我是 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]]
代替从磁盘加载的矩阵。
我将不胜感激任何建议。
答:
似乎存在空间泄漏/懒惰问题。我不怪你:我发现获得我想要的评估行为出奇地挑剔,即使我已经非常确定问题出在哪里!
你要确保的是,在创建新矩阵时,你不会保留对旧矩阵的任何引用。如果这样做,则 GC 无法收集旧矩阵。特别是,引用包括类似或或类似的潜在计算。所以!让我们讨论一下我必须做出哪些改变才能实现这一点。take i oldMatrix
oldMatrix !! 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)
最后,在 中,我建议从 切换到 。在我的测试中,在这种情况下,它似乎没有区别,但这是一个养成的好习惯;提供的额外懒惰几乎从来都不是必需的,而且往往是有害的。eliminateRow
foldl
foldl'
foldl
eliminateRow row (mat, invMat, detAccum, rank) = foldl' eliminateEntry (mat, invMat, detAccum, rank) [0..length mat-1]
通过这些更改,我现在看到内存使用量要低得多:600x600 矩阵为 ~50 秒,96MiB,800x800 矩阵为 ~128 秒,167MiB。
如果需要的话,可能有机会进行重大的进一步优化;例如,我希望为您的矩阵切换到可变数组表示将是另一个很大的推动力。
评论
countingMatrix :: Int -> Matrix; countingMatrix n = [[fromIntegral (j*n+i) | j <- [1..n]] | i <- [1..n]]
!!
不是你的朋友。它在索引参数中是线性的,因此当算法幼稚地从数组转换为列表时,性能通常很差。试着想象它不存在,并避免一般的指数。列表旨在映射、折叠、压缩......但未编入索引。!!