9

假设我有两个非常大的列表 {a1, a2, ...} 和 {b1, b2, ...},其中所有 ai 和 bj 都是大型稀疏数组。为了内存效率,我将每个列表存储为一个全面的稀疏数组。

现在我想在所有可能的 ai 和 bj 对上计算一些函数 f,其中每个结果 f[ai, bj] 又是一个稀疏数组。顺便说一下,所有这些稀疏数组都具有相同的维度。

尽管

Flatten[Outer[f, {a1, a2, ...}, {b1, b2, ...}, 1], 1]

返回所需的结果(原则上)它似乎消耗了过多的内存。尤其是因为返回值是稀疏数组的列表,而在我感兴趣的情况下,一个全面的稀疏数组效率更高。

是否有上述使用的有效替代方法Outer

更具体的例子:

{SparseArray[{{1, 1, 1, 1} -> 1, {2, 2, 2, 2} -> 1}],
 SparseArray[{{1, 1, 1, 2} -> 1, {2, 2, 2, 1} -> 1}],
 SparseArray[{{1, 1, 2, 1} -> 1, {2, 2, 1, 2} -> 1}],
 SparseArray[{{1, 1, 2, 2} -> -1, {2, 2, 1, 1} -> 1}],
 SparseArray[{{1, 2, 1, 1} -> 1, {2, 1, 2, 2} -> 1}],
 SparseArray[{{1, 2, 1, 2} -> 1, {2, 1, 2, 1} -> 1}],
 SparseArray[{{1, 2, 2, 1} -> -1, {2, 1, 1, 2} -> 1}],
 SparseArray[{{1, 2, 2, 2} -> 1, {2, 1, 1, 1} -> 1}]};
ByteCount[%]

list = SparseArray[%%]
ByteCount[%]

Flatten[Outer[Dot, list, list, 1], 1];
ByteCount[%]
list1x2 = SparseArray[%%]
ByteCount[%]

Flatten[Outer[Dot, list1x2, list, 1], 1];
ByteCount[%]
list1x3 = SparseArray[%%]
ByteCount[%]

Outer等等。 (稀疏数组列表)的原始中间结果不仅效率极低,Outer而且在计算本身期间似乎也消耗了太多内存。

4

3 回答 3

5

我将提出一个相当复杂的解决方案,但只允许在计算期间使用大约两倍的内存,将最终结果存储为SparseArray. 为此付出的代价将是执行速度要慢得多。

编码

稀疏数组构造/解构 API

这是代码。首先,稍作修改(以解决高维稀疏数组)稀疏数组构造 - 解构 API,取自此答案

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] := spart[s, 4][[2, 2]];
getSparseData[s_SparseArray] := spart[s, 4][[3]];
getDefaultElement[s_SparseArray] := spart[s, 3];
makeSparseArray[dims_List, jc_List, ir_List, data_List, defElem_: 0] :=
   SparseArray @@ {Automatic, dims, defElem, {1, {jc, ir}, data}};

迭代器

以下函数产生迭代器。迭代器是封装迭代过程的好方法。

ClearAll[makeTwoListIterator];
makeTwoListIterator[fname_Symbol, a_List, b_List] :=
  With[{indices = Flatten[Outer[List, a, b, 1], 1]},
   With[{len  = Length[indices]},
    Module[{i = 0},
      ClearAll[fname];
      fname[] := With[{ind = ++i}, indices[[ind]] /; ind <= len];
      fname[] := Null;
      fname[n_] := 
        With[{ind = i + 1}, i += n; 
           indices[[ind ;; Min[len, ind + n - 1]]] /; ind <= len];
      fname[n_] := Null;
 ]]];

请注意,我本可以使用更多内存来实现上述功能 - 高效而不是Outer在其中使用,但出于我们的目的,这不是主要问题。

这是一个更专业的版本,它为成对的二维索引生成插入器。

ClearAll[make2DIndexInterator];
make2DIndexInterator[fname_Symbol, i : {iStart_, iEnd_}, j : {jStart_, jEnd_}] :=
   makeTwoListIterator[fname, Range @@ i, Range @@ j];
 make2DIndexInterator[fname_Symbol, ilen_Integer, jlen_Integer] :=
   make2DIndexInterator[fname, {1, ilen}, {1, jlen}];

这是它的工作原理:

In[14]:= 
makeTwoListIterator[next,{a,b,c},{d,e}];
next[]
next[]
next[]

Out[15]= {a,d}
Out[16]= {a,e}
Out[17]= {b,d}

我们还可以使用它来获取批处理结果:

In[18]:= 
makeTwoListIterator[next,{a,b,c},{d,e}];
next[2]
next[2]

Out[19]= {{a,d},{a,e}}
Out[20]= {{b,d},{b,e}}

,我们将使用第二种形式。

SparseArray - 构建函数

这个函数将迭代地构建一个SparseArray对象,通过获取数据块(也是SparseArray形式)并将它们粘合在一起。它基本上是这个答案中使用的代码,打包成一个函数。它接受用于生成下一个数据块的代码片段,包裹在Hold(我也可以让它HoldAll

Clear[accumulateSparseArray];
accumulateSparseArray[Hold[getDataChunkCode_]] :=
  Module[{start, ic, jr, sparseData, dims, dataChunk},
   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]];
   ];
   makeSparseArray[dims, ic, jr, sparseData]];

把它们放在一起

这个功能是主要的,把它们放在一起:

ClearAll[sparseArrayOuter];
sparseArrayOuter[f_, a_SparseArray, b_SparseArray, chunkSize_: 100] := 
  Module[{next, wrapperF, getDataChunkCode},
    make2DIndexInterator[next, Length@a, Length@b];
    wrapperF[x_List, y_List] := SparseArray[f @@@ Transpose[{x, y}]];
    getDataChunkCode :=
      With[{inds = next[chunkSize]},
         If[inds === Null, Return[{}]];
         wrapperF[a[[#]] & /@ inds[[All, 1]], b[[#]] & /@ inds[[All, -1]]]
      ];
    accumulateSparseArray[Hold[getDataChunkCode]]
  ];

在这里,我们首先生成迭代器,它将按需为我们提供索引对列表的部分,用于提取元素(也SparseArrays)。请注意,我们通常会一次从两个大输入中提取一对以上的元素SparseArray,以加快代码速度。我们一次处理多少对由可选chunkSize参数控制,默认为100. 然后我们构造代码来处理这些元素并将结果放回SparseArray其中,我们使用辅助函数wrapperF。迭代器的使用不是绝对必要的(可以使用Reap-Sow相反,与其他答案一样),但允许我将迭代逻辑与稀疏数组的通用累积逻辑分离。

基准

首先,我们准备大型稀疏数组并测试我们的功能:

In[49]:= 
arr = {SparseArray[{{1,1,1,1}->1,{2,2,2,2}->1}],SparseArray[{{1,1,1,2}->1,{2,2,2,1}->1}],
SparseArray[{{1,1,2,1}->1,{2,2,1,2}->1}],SparseArray[{{1,1,2,2}->-1,{2,2,1,1}->1}],
SparseArray[{{1,2,1,1}->1,{2,1,2,2}->1}],SparseArray[{{1,2,1,2}->1,{2,1,2,1}->1}]};

In[50]:= list=SparseArray[arr]
Out[50]= SparseArray[<12>,{6,2,2,2,2}]

In[51]:= larger = sparseArrayOuter[Dot,list,list]
Out[51]= SparseArray[<72>,{36,2,2,2,2,2,2}]

In[52]:= (large= sparseArrayOuter[Dot,larger,larger])//Timing
Out[52]= {0.047,SparseArray[<2592>,{1296,2,2,2,2,2,2,2,2,2,2}]}

In[53]:= SparseArray[Flatten[Outer[Dot,larger,larger,1],1]]==large
Out[53]= True

In[54]:= MaxMemoryUsed[]
Out[54]= 21347336

现在我们进行功率测试

In[55]:= (huge= sparseArrayOuter[Dot,large,large,2000])//Timing
Out[55]= {114.344,SparseArray[<3359232>,{1679616,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2}]}

In[56]:= MaxMemoryUsed[]
Out[56]= 536941120

In[57]:= ByteCount[huge]
Out[57]= 262021120

In[58]:= (huge1 = Flatten[Outer[Dot,large,large,1],1]);//Timing
Out[58]= {8.687,Null}

In[59]:= MaxMemoryUsed[]
Out[59]= 2527281392

对于这个特定示例,建议的方法比直接使用 的内存效率高 5 倍Outer,但速度慢约 15 倍。我不得不调整chunksize参数(默认为100,但对于上面我使用的2000,以获得最佳速度/内存使用组合)。我的方法仅用作存储最终结果所需内存的两倍的峰值。与基于方法相比,内存节省的程度Outer将取决于所讨论的稀疏数组。

于 2011-12-22T19:58:25.830 回答
0

使用您的示例list数据,我相信您会发现该功能对您Append很有帮助SparseArray

acc = SparseArray[{}, {1, 2, 2, 2, 2, 2, 2}]

Do[AppendTo[acc, i.j], {i, list}, {j, list}]

Rest[acc]

我需要Rest删除结果中的第一个零填充张量。种子的第二个参数SparseArray必须是带有前缀的每个元素的尺寸1。您可能需要明确指定种子的背景SparseArray以优化性能。

于 2011-12-22T02:19:34.563 回答
0

如果lst1并且lst2是您的清单,

Reap[
   Do[Sow[f[#1[[i]], #2[[j]]]],
       {i, 1, Length@#1},
       {j, 1, Length@#2}
       ] &[lst1, lst2];
   ] // Last // Last

完成这项工作,并且可能更节省内存。另一方面,也许不是。纳赛尔是对的,一个明确的例子会很有用。

编辑:使用 Nasser 的随机生成的数组和 forlen=200表示MaxMemoryUsed[]此表单需要 170MB,而Outer问题中的表单需要 435MB。

于 2011-12-21T21:21:59.127 回答