Эффективная альтернатива Outer для разреженных массивов в Mathematica?

Предположим, у меня есть два очень больших списка {a1, a2,...} и {b1, b2,...}, где все ai и bj являются большими разреженными массивами. Ради экономии памяти я храню каждый список как один всеобъемлющий разреженный массив.

Теперь я хотел бы вычислить некоторую функцию f для всех возможных пар ai и bj, где каждый результат 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, похоже, также потребляет слишком много памяти во время самого вычисления.


person groovybaby    schedule 21.12.2011    source источник
comment
Вы могли бы сделать небольшой автономный пример, чтобы проиллюстрировать вашу проблему. Потому что любой, кто хотел бы ответить вам, должен был это сделать. см. sscce.org. Это тот тип примера, который у вас есть? длина = 1000; f[x_] := SparseArray[Table[x^2, {len}]]; list1 = SparseArray[Table[RandomReal[], {len}]]; list2 = SparseArray[Table[RandomReal[], {len}]]; Время[Внешний[f, список1, список2]][[1]]   -  person Nasser    schedule 22.12.2011
comment
Я не уверен, понимаю ли я, что вы имеете в виду под фразой «всеобъемлющий разреженный массив». Не могли бы вы немного уточнить это?   -  person Sjoerd C. de Vries    schedule 22.12.2011
comment
@Nasser Разве функция f в вашем примере не должна быть одной с двумя аргументами?   -  person Sjoerd C. de Vries    schedule 22.12.2011
comment
@ SjoerdC.deVries, я уверен, что вы можете быть правы, но я спрашиваю об этом. Я не был уверен в вопросе. Если пользователь публикует небольшой конкретный пример, то тем, кто отвечает, не приходится что-то придумывать и можно что-то упустить в процессе.   -  person Nasser    schedule 22.12.2011
comment
Вот несколько мыслей: Outer, кажется, имеет специальную поддержку для Times: он сохраняет разреженную структуру и эффективен. Как правило, Outer[f, ...] можно было бы сделать гораздо более эффективным для разреженных массивов, если бы можно было гарантировать, что функция f не имеет побочных эффектов (и в результате f[a,b] всегда возвращает один и тот же результат для одних и тех же a и b) . Тогда неопределенный элемент (обычно 0) разреженного массива можно было передать f только один раз, а результат можно было использовать в качестве нового неопределенного элемента в новом разреженном массиве. Хотя автоматически определить, что некоторые...   -  person Szabolcs    schedule 22.12.2011
comment
... функция f не имеет побочных эффектов, Outer может иметь опцию, в которой мы явно указываем ей, что она может сделать это предположение. Примите этот небольшой комментарий как предложенное мною улучшение Outer :-)   -  person Szabolcs    schedule 22.12.2011
comment
Не уверен, имеет ли это прямое отношение к вашему предложению, но функция f[a, b] на самом деле будет иметь в качестве аргументов не листья разреженного массива list, а элементы на первом уровне, то есть разреженные подмассивы. Я просто собираю эти подмассивы в общий разреженный массив (а не в список), чтобы уменьшить объем памяти.   -  person groovybaby    schedule 22.12.2011


Ответы (3)


Я предложу решение, которое довольно сложное, но позволяет использовать только вдвое больше памяти во время вычислений, чем необходимо для хранения конечного результата в виде 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 будет зависеть от рассматриваемых разреженных массивов.

person Leonid Shifrin    schedule 22.12.2011
comment
Я не буду голосовать, пока у меня не будет времени проверить и понять это, но я надеялся, что вы ответите. - person Mr.Wizard; 23.12.2011

Если lst1 и lst2 ваши списки,

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

выполняет свою работу и может быть более эффективным с точки зрения использования памяти. С другой стороны, может быть, и нет. Насер прав, явный пример был бы полезен.

РЕДАКТИРОВАТЬ: Используя случайно сгенерированные массивы Нассера, и для len=200, MaxMemoryUsed[] указывает, что для этой формы требуется 170 МБ, а форма Outer в вопросе занимает 435 МБ.

person acl    schedule 21.12.2011
comment
Как насчет: Reap[Do[Sow[Dot[i, j]], {i, r}, {j, r}];] // Last // Last с r={SparseArray[],..} или Flatten [Таблица[Точка[i, j], {i, r}, {j, r}], 1] - person ; 22.12.2011

Используя данные вашего примера 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, чтобы оптимизировать производительность.

person Mr.Wizard    schedule 22.12.2011
comment
Вы можете использовать материалы Internal`Bag, в зависимости от того, сколько у вас таких массивов. - person ; 22.12.2011
comment
@ruebenko, как это соотносится с добавлением непосредственно к SparseArray? - person Mr.Wizard; 22.12.2011
comment
на самом деле это не так; Suffing Bag с SparseArray делает Нормальную из SA. Я не знал этого. Это, по крайней мере, объясняет, почему Internal`Bag является только внутренним. Он не полностью реализован для произвольных выражений. - person ; 22.12.2011
comment
Использование acc = SparseArray[{}, {1, 2, 2, 2, 2, 2, 2}]; Do[AppendTo[acc, Dot[i, j]], {i, list}, {j, list}]; list1x2 = Rest[acc]; Clear[acc] и т. д. На самом деле я получаю гораздо худшую производительность. Во-первых, разреженные массивы list1x2, list1x3 и т. д. занимают больше памяти, чем их аналоги, созданные с помощью Outer выше, хотя тест показывает, что они равны. Кроме того, к моменту вычисления list1x6 у MathKernel заканчивается память, чего не происходит с подходом Outer. Есть идеи, почему это так? - person groovybaby; 22.12.2011
comment
@groovybaby, пожалуйста, попробуйте это и сообщите: Do[AppendTo[acc, list[[i]].list[[j]]], {i, Length@list}, {j, Length@list}] Я думаю, что Do не разрежает элементы. - person Mr.Wizard; 23.12.2011
comment
@Mr.Wizard Попробовал это, и это кажется смехотворно эффективным в отношении памяти ... хотя за счет очень медленного времени выполнения. Ну, я думаю, всегда есть компромисс в ту или иную сторону. Спасибо за подсказку! - person groovybaby; 25.12.2011