用Mathematica导入大文件/数组

21

我使用 Windows7 32位平台上的 Mathematica 8.0.1.0。 我尝试使用

Import[file,”Table”]

只要文件(文件中的数组)足够小,它就可以正常运行。但是对于更大的文件(38MB)/数组(9429 x 2052),我会收到以下消息:

No more memory available. Mathematica kernel has shut down. Try quitting other applications and then retry.
在我的Windows7 64位平台上,由于内存更大,我可以导入更大的文件。但是我认为,当文件增长或数组具有更多行时,某一天我也会遇到同样的问题。
因此,我尝试找到导入大型文件的解决方案。经过一段时间的搜索,我在这里看到了一个类似的问题:如何处理Wolfram Mathematica中的大型数据文件。 但是似乎我的Mathematica知识不足以将建议的OpenRead、ReadList或类似方法适应于我的数据(请参见此处的示例文件)。 问题在于,我需要在文件中使用数组的其他信息,例如Dimensions、某些列和行中的Max/Min,并对某些列和每一行执行操作。 但是,当我使用例如ReadList时,我从未像使用Import时获得过相同的数组信息(可能是因为我使用方法不正确)。
能否有人给我一些建议?我将非常感激!

你真的是说当读取一个仅有38MB的文件时,你会遇到这个错误吗? - Nasser
@Nasser 这是一种用于表格数据的特殊紧凑格式,类似于二进制格式。当解压缩为Mathematica的符号数据结构(列表)时,需要更多的内存并不奇怪。然而,这也无法解释如此戏剧性的内存需求 - 请参见我下面的答案。 - Leonid Shifrin
我导入这个测试文件时没有任何问题。读取后的数据字节数为619,900,248,大约620 MB。这可能会引起你的问题。 - Sjoerd C. de Vries
@Sjoerd 我确实遇到了问题 - 在某个时刻,它要求超过2GB的RAM,你可以通过使用系统监视器来查看。由于我使用的是6GB RAM的64位系统,这并不致命。但我毫不怀疑,对于运行其他一些东西的32位系统来说,这可能是一个致命问题。在我看来,这是当前“Import”实现中明显的缺陷 - 请参见我的答案以获取替代方案。至于“ByteCount”,它也是具有欺骗性的,因为最终表格实际占用的内存约为“ByteCount”指示的三分之一。 - Leonid Shifrin
@Leonid 有没有一种方法可以获取某个 Mathematica 表达式所占用的真实内存量?我已经尝试过 ByteCount,发现这个函数给出了令人困惑的结果。 - Alexey Popkov
显示剩余2条评论
1个回答

34

由于某种原因,目前针对表格数据类型TableImport实现非常浪费内存。下面我尝试着在仍然重用Mathematica高级导入功能(通过ImportString)的情况下,改善这种情况。对于稀疏表格,提供了另一种解决方案,可以大大节省内存。

通用节省内存的解决方案

下面是一个更加节省内存的函数:

Clear[readTable];
readTable[file_String?FileExistsQ, chunkSize_: 100] :=
   Module[{str, stream, dataChunk, result , linkedList, add},
      SetAttributes[linkedList, HoldAllComplete];
      add[ll_, value_] := linkedList[ll, value];           
      stream  = StringToStream[Import[file, "String"]];
      Internal`WithLocalSettings[
         Null,
         (* main code *)
         result = linkedList[];
         While[dataChunk =!= {},
           dataChunk = 
              ImportString[
                 StringJoin[Riffle[ReadList[stream, "String", chunkSize], "\n"]], 
                 "Table"];
           result = add[result, dataChunk];
         ];
         result = Flatten[result, Infinity, linkedList],
         (* clean-up *)
         Close[stream]
      ];
      Join @@ result]

在这里,我将其与标准的Import进行对比,针对您的文件:

In[3]:= used = MaxMemoryUsed[]
Out[3]= 18009752

In[4]:= 
tt = readTable["C:\\Users\\Archie\\Downloads\\ExampleFile\\ExampleFile.txt"];//Timing
Out[4]= {34.367,Null}

In[5]:= used = MaxMemoryUsed[]-used
Out[5]= 228975672

In[6]:= 
t = Import["C:\\Users\\Archie\\Downloads\\ExampleFile\\ExampleFile.txt","Table"];//Timing
Out[6]= {25.615,Null}

In[7]:= used = MaxMemoryUsed[]-used
Out[7]= 2187743192

In[8]:= tt===t
Out[8]= True

您可以看到,我的代码比Import更节省大约10倍的内存,而且速度并不慢。您可以通过调整chunkSize参数来控制内存消耗。生成的表占用大约150-200 MB的RAM。
编辑
让稀疏表更加高效
我想说明如何使此函数在导入期间变得更加节省2-3倍的内存,并且在最终内存占用方面使用SparseArray可以获得更高的内存效率。我们获得内存效率增益的程度在很大程度上取决于您的表有多么稀疏。在您的示例中,该表非常稀疏。
稀疏数组的解剖
我们从一个通常有用的API开始,用于构建和拆分SparseArray对象:
ClearAll[spart, getIC, getJR, getSparseData, getDefaultElement, makeSparseArray];
HoldPattern[spart[SparseArray[s___], p_]] := {s}[[p]];
getIC[s_SparseArray] := spart[s, 4][[2, 1]];
getJR[s_SparseArray] := Flatten@spart[s, 4][[2, 2]];
getSparseData[s_SparseArray] := spart[s, 4][[3]];
getDefaultElement[s_SparseArray] := spart[s, 3];
makeSparseArray[dims : {_, _}, jc : {__Integer}, ir : {__Integer}, 
     data_List, defElem_: 0] :=
 SparseArray @@ {Automatic, dims, defElem, {1, {jc, List /@ ir}, data}};

需要说明一些简要评论。这是一个稀疏数组示例:

In[15]:= 
ToHeldExpression@ToString@FullForm[sp  = SparseArray[{{0,0,1,0,2},{3,0,0,0,4},{0,5,0,6,7}}]]

Out[15]= 
Hold[SparseArray[Automatic,{3,5},0,{1,{{0,2,4,7},{{3},{5},{1},{5},{2},{4},{5}}},
{1,2,3,4,5,6,7}}]]

(I used the ToString-ToHeldExpression cycle to convert List[...] etc. in the FullForm back to {...} for easier reading). Here, {3,5} clearly represents the dimensions of the array. Following this is a default value of 0, and then a nested list which we can denote as {1,{ic,jr}, sparseData}. The ic list gives the cumulative number of non-zero elements added for each row - starting with 0, then adding 2 after the first row, 2 more after the second, and finally 3 more after the last. The jr list gives the positions of the non-zero elements in each row, where the first row has elements at positions 3 and 5, the second row has elements at positions 1 and 5, and the last row has elements at positions 2, 4, and 5. The ordering of the elements in the sparseData list follows the same order as the elements in the jr list, and represents the non-zero values read row by row from left to right. Knowing this internal format should hopefully clarify the role of the above functions.

The code:

Clear[readSparseTable];
readSparseTable[file_String?FileExistsQ, chunkSize_: 100] :=
   Module[{stream, dataChunk, start, ic = {}, jr = {}, sparseData = {}, 
        getDataChunkCode, dims},
     stream  = StringToStream[Import[file, "String"]];
     getDataChunkCode := 
       If[# === {}, {}, SparseArray[#]] &@
         ImportString[
             StringJoin[Riffle[ReadList[stream, "String", chunkSize], "\n"]], 
             "Table"];
     Internal`WithLocalSettings[
        Null,
        (* main code *)
        start = getDataChunkCode;
        ic = getIC[start];
        jr = getJR[start];
        sparseData = getSparseData[start];
        dims = Dimensions[start];
        While[True,
           dataChunk = getDataChunkCode;
           If[dataChunk === {}, Break[]];
           ic = Join[ic, Rest@getIC[dataChunk] + Last@ic];
           jr = Join[jr, getJR[dataChunk]];
           sparseData = Join[sparseData, getSparseData[dataChunk]];
           dims[[1]] += First[Dimensions[dataChunk]];
        ],
        (* clean - up *)
        Close[stream]
     ];
     makeSparseArray[dims, ic, jr, sparseData]]

基准测试和比较

以下是使用的初始内存量(新内核):

In[10]:= used = MemoryInUse[]
Out[10]= 17910208

我们调用我们的函数:
In[11]:= 
(tsparse= readSparseTable["C:\\Users\\Archie\\Downloads\\ExampleFile\\ExampleFile.txt"]);//Timing
Out[11]= {39.874,Null}

所以,它的速度与 readTable 相同。那么内存使用情况如何?

In[12]:= used = MaxMemoryUsed[]-used
Out[12]= 80863296

我认为这非常值得注意:我们只使用了比磁盘上文件本身所占用的两倍更多的内存。但更令人惊讶的是,计算完成后的最终内存使用量大大减少:

In[13]:= MemoryInUse[]
Out[13]= 26924456

这是因为我们使用了SparseArray
In[15]:= {tsparse,ByteCount[tsparse]}
Out[15]= {SparseArray[<326766>,{9429,2052}],12103816}

因此,我们的表只占用了12 MB的内存。我们可以将其与更通用的函数进行比较:

In[18]:= 
(t = readTable["C:\\Users\\Archie\\Downloads\\ExampleFile\\ExampleFile.txt"]);//Timing
Out[18]= {38.516,Null}

我们将稀疏表转换回普通表后,结果是相同的:
In[20]:= Normal@tsparse==t
Out[20]= True

普通表格占用的空间要多得多(似乎ByteCount把占用的内存计算了3-4倍,但实际差异仍然至少是一个数量级):

In[21]:= ByteCount[t]
Out[21]= 619900248

2
实际上,“Table”既缓慢又极其浪费内存。对于同质数据(即每个表条目都具有相同的数据类型),我通常使用“ReadList”。它更快,也更节约内存。 - Szabolcs
@Leonid,这就是为什么我使用OpenAndRead的原因。它只需要额外的一行代码。 - rcollyer
1
@rcollyer 实际上,仅仅保护不会足以防止中止。还必须保护异常,然后在代码完成后重新抛出异常。因此,这更加复杂。幸运的是,这个函数(宏)已经存在:它是“Internal`WithLocalSettings”,由Daniel Lichtblau在这里描述:http://groups.google.com/group/comp.soft-sys.math.mathematica/browse_thread/thread/a39d99bc1470bb3c 。我将按照您的建议在这里使用它。 - Leonid Shifrin
2
WithLocalSettings 在这里可以使用,但在一般情况下有其局限性。例如,它无法正确处理 ThrowCatch(例如Catch[Internal`WithLocalSettings[Print["start"], Throw[23], Print["cleanup"]]])。清理会执行,但“捕获”将被打断。在 MMa 中处理堆栈展开是一个棘手的问题。请参见数学家中的可靠清理 - WReach
我也想感谢Sjoerd、Szabolcs、rcollyer和WReach的宝贵意见!感谢@Mr.Wizard纠正我的英文错误。 - partial81
显示剩余13条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接