【问题标题】:How can I repeatedly read in shuffled lines of a large data file in Haskell?如何在 Haskell 中反复读取大数据文件的洗牌行?
【发布时间】:2019-11-04 23:54:31
【问题描述】:

我有一个 60k 行的数据文件,其中每行有 ~1k 逗号分隔的 Ints(我想立即变成 Doubles)。

我想遍历 32 行的随机“批次”序列,其中批次是所有行的随机子集,并且没有一个批次共享行。由于每批次有 60k 行和 32 行,所以应该有 1875 个批次。

如有必要,我愿意进行更改,但我希望它们采用延迟评估的(批次)列表的形式。需要这个的代码是foldM,我在其中使用它:

resulting_struct <- foldM fold_fn my_struct batch_list

以便它在当前累加器my_structbatch_list 的下一个元素的结果上重复调用fold_fn

我很困惑。当我不需要洗牌时,这很容易;我只是将它们读入并分块,它们被懒惰地评估,所以我没有问题。现在我完全被卡住了,感觉我一定错过了一些简单的东西。

我尝试了以下方法:

  1. 将文件读入行列表并天真地打乱输入。这不起作用,因为 readFile 被延迟评估,但它需要将整个文件读入内存以随机打乱它,它很快就会耗尽我所有的 ~8 GB RAM。

  2. 获取文件的长度,然后创建一个从 0 到 60k 的混洗索引批次列表,这些列表对应于将被选择形成批次的行号。然后,当我想实际获取数据批次时,我会这样做:

ind_batches <- get_shuffled_ind_batches_from_file fname
batch_list <- mapM (get_data_batch_from_ind_batch fname) ind_batches

地点:

get_shuffled_ind_batches_from_file :: String -> IO [[Int]]
get_shuffled_ind_batches_from_file fname = do
  contents <- get_contents_from_file fname -- uses readFile, returns [[Double]]
  let n_samps = length contents
      ind = [0..(n_samps-1)]
  shuffled_indices <- shuffle_list ind
  let shuffled_ind_chunks = take 1800 $ chunksOf 32 shuffled_indices
  return shuffled_ind_chunks

get_data_batch_from_ind_batch :: String -> [Int] -> IO [[Double]]
get_data_batch_from_ind_batch fname ind_chunk = do
  contents <- get_contents_from_file fname
  let data_batch = get_elems_at_indices contents ind_chunk
  return data_batch

shuffle_list :: [a] -> IO [a]
shuffle_list xs = do
        ar <- newArray n xs
        forM [1..n] $ \i -> do
            j <- randomRIO (i,n)
            vi <- readArray ar i
            vj <- readArray ar j
            writeArray ar j vi
            return vj
  where
    n = length xs
    newArray :: Int -> [a] -> IO (IOArray Int a)
    newArray n xs =  newListArray (1,n) xs

get_elems_at_indices :: [a] -> [Int] -> [a]
get_elems_at_indices my_list ind_list = (map . (!!)) my_list ind_list

然而,mapM 似乎立即评估,然后尝试重复读取文件内容(我认为,RAM 无论如何都会爆炸)。

  1. 多一点搜索告诉我,我可以尝试使用unsafeInterleaveIO 来制作它,以便它懒惰地评估一个动作,所以我尝试像这样坚持它:
get_data_batch_from_ind_batch :: String -> [Int] -> IO [[Double]]
get_data_batch_from_ind_batch fname ind_chunk = unsafeInterleaveIO $ do
  contents <- get_contents_from_file fname
  let data_batch = get_elems_at_indices contents ind_chunk
  return data_batch

但没有运气,和上面一样的问题。

我觉得我一直在这里撞墙,一定错过了一些非常简单的事情。有人建议改用流或管道,但是当我查看它们的文档时,我并不清楚如何使用它们来解决这个问题。

如何在不耗尽所有内存的情况下读取大型数据文件并随机播放它?

【问题讨论】:

    标签: file haskell input lazy-evaluation


    【解决方案1】:

    hGetContents 将懒惰地返回文件的内容,但如果你对结果做很多事情,你会立即实现整个文件。我建议读取该文件一次,然后扫描它以查找换行符,以便您可以建立一个索引,其中哪个块从哪个字节偏移开始。该索引将非常小,因此您可以轻松地对其进行洗牌。然后你可以遍历索引,每次打开文件并只读取它定义的子范围,并且只解析那个块。

    【讨论】:

    • 嗨,我应该更清楚一点:这些块不是连续的。一个块由文件中随机(非连续)位置的 32 行随机行组成。
    • 另外,也许我不明白什么,但是:readFile 也很懒,对吧?但似乎mapM 无论如何都会评估整个地图?所以如果我理解正确,使用hGetContents 仍然无济于事,因为它会被评估?
    • 我认为mapM 会保留返回文件的惰性,如果给定一个足够惰性的函数。你的问题是 get_data_batch_from_ind_batch 写的非常不懒惰。它将文件读入一个字符串,然后重用该字符串按索引查找多次。如果您想避免将文件保存在内存中,则永远不能重用 getContents 生成的字符串。每次要开始新扫描时都必须重新读取文件。
    • 另外,一次扫描一个字节是毫无意义的慢:我会先用hSeek之类的东西将句柄指向所需的字节偏移量,然后只读取你想要的字节。
    • 请注意,hGetContents 没有什么特别之处:Haskell 中的任何惰性操作都只是“第一次”惰性操作。如果你想使用一个值两次,你必须要么把它保存在内存中,要么重新计算它。当您保存hGetContents 生成的字符串并多次使用它时,您会强制 Haskell 将其全部保存在内存中。
    猜你喜欢
    • 1970-01-01
    • 2019-05-25
    • 2013-11-26
    • 2011-07-21
    • 2017-06-05
    • 1970-01-01
    • 2013-01-15
    • 1970-01-01
    • 2023-03-27
    相关资源
    最近更新 更多