11.09.2009

Пакет NumPy. Краткое введение

Введение. Пакет NumPy - зачем нужен, где взять

Пакет (библиотека) языка Pyton NumPy предоставляет программисту средства для высокоэффективной работы с огромными многомерными массивами данных. Как составная часть и основа, пакет NumPy входит в большинство проектов, использующих язык Python и требующих мало-мальски громоздких вычислений. В частности, с его использованием написаны популярные пакеты вычислительной математики и научной графики SciPy и mathplotLib.

Python интерпретируемый язык программирования, как следствие, математические алгоритмы, написанные на Python'е, часто работают заметно медленнее по сравнению с теми же алгоритмами, реализованными на компилируемых языках. Авторы NumPy постарались разрешить эту проблему в части обработки массивов, предложив модуль, включающий набор функций для создания многомерных массивов и работы с ними, причем исполняемый код функций реализован на языках C и Fortran. Кроме того, в пакете определены функции линейной алгебры, преобразования Фурье и генерации случайных чисел.

Сейчас я постараюсь дать самое краткое описание основ использования пакета.

Взять дистрибутив NumPy можно из репозитория SourceForge. На момент написания крайняя версия 1.3.0, выпущена 05 апреля 2009 года. Установка пакета не должна вызвать каких-либо затруднений, какой-либо особой настройки пакет так же не требует.

Модуль NumPy предназначен для работы с массивами, соответственно основной тип данных определенных в модуле - массив (nump.ndarray). Массив - контейнер, содержащий элементы одного типа (и одной длинны, если элементы вложенные массивы), организованные в упорядоченную многомерную таблицу. Доступ к элементам осуществляется по индексу - кортежу целых чисел.

Создать массив можно несколькими способами.

Способы создания массивов

Массив может быть создан на основе имеющегося списка:

>>> import numpy as np

>>> a = np.array([.1, .2, .3, .4, .5])
>>> a
array([ 0.1,  0.2,  0.3,  0.4,  0.5])
>>> type(a)
<type 'numpy.ndarray'>

Имя класса numpy.ndarray выбрано специально, чтобы не было путаницы со стандартным классом Python array.

Аналогично можно создать многомерный массив:

>>> a3 = np.array([ [ [1, 2], [3, 4], [5, 6] ], [ [7, 8], [9, 10], [11, 12] ] ])
>>> a3
array([[[ 1,  2],
        [ 3,  4],
        [ 5,  6]],

       [[ 7,  8],
        [ 9, 10],
        [11, 12]]])
>>> type(a3)
<type 'numpy.ndarray'>

Массивы NumPy во многом аналогичны стандартным спискам Python.

Доступ к элементам массива получают по индексу (разумеется, индекс первого элемента 0):

>>>print a[0]
0.1

>>>print a[3]
0.4

Для индексации многомерных массивов используют кортежи (индексы перечисляют через запятую):

>>> a3[1]
array([[ 7,  8],
       [ 9, 10],
       [11, 12]])

>>> a3[1, 1]
array([ 9, 10])

>>> a3[1, 1, 1]
10

От массивов можно брать срезы:

>>> a[2:4]
array([ 0.3,  0.4])

>>> a3[0, 1:]
array([[3, 4],
       [5, 6]])

Массивы можно использовать в различных итерациях:

>>> for i in a:
...     print i
...
0.1
0.2
0.3
0.4
0.5

>>> for i in a3:
...     for j in i:
...             print j
...
[1 2]
[3 4]
[5 6]
[7 8]
[ 9 10]
[11 12]

Каждый объект numpy.ndarray имеет атрибут shape - кортеж целых чисел, который определяет шаблон массива. Здесь под шаблоном подразумевается количество элементов каждой размерности массива, так для матрицы с n строк и m столбцов шаблон (n, m).

>>> a.shape
(5,)

>>> a3.shape
(2, 3, 2)

Изменяя атрибут shape, можно изменять размерность и структуру массива, но при этом число элементов массива должно оставаться неизменным:

>>> a3.shape = (6, 2)
>>> a3
array([[ 1,  2],
       [ 3,  4],
       [ 5,  6],
       [ 7,  8],
       [ 9, 10],
       [11, 12]])

>>> a3.shape = (12)
>>> a3
array([ 1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12])

>>> a3.shape = (3, 3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: total size of new array must be unchanged

Массив может быть создан с помощью функции numpy.arange().

Функция arange() в самом простом варианте вызова принимает в качестве аргумента целое положительное число n, возвращает массив из n элементов от 0 до n-1:

>>> b = np.arange(5)
>>> b
array([0, 1, 2, 3, 4])
>>> type(b)
<type 'numpy.ndarray'>

Полный формат вызова функции: numpy.arange([start,] stop[, step,], dtype = None),

где:

start - опциональный аргумент, начальное значение интервала, по умолчанию равен 0;

stop - обязательный аргумент, конечное значение интервала, не входящее в сам интервал, интервал замыкает значение stop - step;

step - опциональный аргумент, шаг итерации, разность между каждым последующим и предыдущим значениями интервала, по умолчанию равен 1;

dtype - тип элементов массива, по умолчанию None, в этом случае тип элементов определяется по типу переданных функции аргуметов start, stop.

Массив может быть создан с помощью функции numpy.linspace().

Функция linspace() в простейшем случае принимает три аргумента - числа: начальный элемент массива, конечный элемент массива, количество элементов массива. Возвращает массив чисел равномерно распределенных от начального до конечного значения.

>>> c = np.linspace(-3.0, 3.0, 7)
>>> c
array([-3., -2., -1.,  0.,  1.,  2.,  3.])
>>> type(c)
<type 'numpy.ndarray'>

Полный формат вызова функции: numpy.linspace(start, stop, num = 50, endpoint = True, retstep = False),

где:

start - обязательный аргумент, первый член последовательности элементов массива;

stop - обязательный аргумент, последний член последовательности элементов массива;

num - опциональный аргумент, количество элементов массива, по умолчанию равен 50;

endpoint - опциональный аргумент, логическое значение, по умолчанию True. Если передано True, stop, последний элемент массива. Если установлено в False, последовательность элементов формируется от start до stop для num + 1 элементов, при этом в возвращаемый массив последний элемент не входит;

>>> d = np.linspace(1.0, 6.0, 5, endpoint = False)
>>> d
array([ 1.,  2.,  3.,  4.,  5.])

retstep - опциональный аргумент, логическое значение, по умолчанию False. Если передано True, функция возвращает кортеж из двух членов, первый - массив, последовательность элементов, второй - число, приращение между элементами последовательности.

>>> f = np.linspace(.5, -.5, 5, retstep = True)
>>> f
(array([ 0.5 ,  0.25,  0.  , -0.25, -0.5 ]), -0.25)

Массив может быть создан с помощью функций numpy.zeros(), numpy.ones(), numpy.empty().

Функции принимают один обязательный аргумент - кортеж, размерность массива и возвращают массив затребованной структуры содержащий: нули (функция numpy.zeros()), единицы numpy.ones(), произвольный мусор (неинициализированный массив, функция numpy.empty()):

>>> z = np.zeros((3,3))
>>> z
array([[ 0.,  0.,  0.],
       [ 0.,  0.,  0.],
       [ 0.,  0.,  0.]])

>>> o = np.ones((3, 3))
>>> o
array([[ 1.,  1.,  1.],
       [ 1.,  1.,  1.],
       [ 1.,  1.,  1.]])

>>> e = np.empty((3,3))
>>> e
array([[  8.32475314e-306,   8.32468797e-306,   8.32462278e-306],
       [  8.32455759e-306,   8.32449240e-306,   5.99491582e+197],
       [  5.30481132e+180,   5.51965256e-311,   0.00000000e+000]])

Полный формат вызова функций:

numpy.zeros(shape, dtype = float, order = 'C')

numpy.ones(shape, dtype = float, order = 'C')

numpy.empty(shape, dtype = float, order = 'C')

где:

shape - обязательный аргумент, кортеж, требуемая размерность массива;

dtype - опциональный аргумент, тип элементов массива, по умолчанию float;

order - опциональный аргумент, строка, способ представления данных массива в памяти, два возможных значения 'C' и 'F', пока я не сталкивался с необходимостью изменять умолчальное значение.

Ну и последний из рассматриваемых способов с помощью функций numpy.zeros_like(), numpy.ones_like(), numpy.empty_like().

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

>>> d
array([ 1.,  2.,  3.,  4.,  5.])

>>> dz = np.zeros_like(d)
>>> dz
array([ 0.,  0.,  0.,  0.,  0.])

>>> do = np.ones_like(d)
>>> do
array([ 1.,  1.,  1.,  1.,  1.])

>>> de = np.empty_like(d)
>>> de
array([  2.24122267e+201,   6.32526950e-317,   0.00000000e+000,
         2.24686637e+201,   6.34874355e-321])

Полный формат вызова функций:

numpy.zeros_like(a)

numpy.ones_like(a)

numpy.empty_like(a)

где:

a - обязательный аргумент, массив, структуру которого необходимо повторить;

О массивах чуть подробней

Как я уже говорил массив это упорядоченная "многомерная таблица" содержащая элементы одного типа, доступ к которым осуществляется по индексу. Индекс - кортеж целых чисел, однозначно определяющий положение элемента в массиве.

Многомерную таблицу можно представить как некое расширение обычной, "двумерной" таблицы, включающей кроме строк и столбцов еще произвольное число "измерений", вместе образующих некую многомерную решетку. В документации NumPy такие измерения именуют осями (axis), а число осей у конкретного массива рангом (rank) массива (при этом термин ранг применяется и в контексте матриц, что, разумеется, не одно и тоже). Для еще большей путаницы вместо числа осей или ранга используют термин размерность (dimension). В разделе документации официального сайта проекта есть Numpy Glossary, там для слова axis приводиться одно значение, для слова rank два, а для слова dimension - три. На текущем уровне просветления моё воображение позволяет представить массив с размерностью не большей четырех, а в практической деятельности, я не сталкивался с массивами имеющими более трех осей :) Так вектор это одномерный массив, матрица, растровый черно-белый рисунок, таблица Excel двумерный, цветной растровый рисунок трехмерный.

Каждый объект класса numpy.ndarray имеет атрибут ndarray.ndim - число осей, ранг, размерность массива. По определению ранга массива значение ndarray.ndim должно быть равно числу элементов в кортеже ndarray.shape.

Общее число элементов массива можно узнать, обратившись к атрибуту ndarray.size.

>>> import numpy as np

>>> vector = np.array([1, 3, 5, 7, 9])
>>> print vector.ndim
1
>>> print vector.shape
(5,)
>>> vector.size
5

>>> bwimage = np.array([ [255, 247, 236, 12, 149], [123, 34, 147, 45, 102], [34, 69, 108, 81, 94] ])
>>> print bwimage.ndim
2
>>> print bwimage.shape
(3, 5)
>>> bwimage.size
15

>>> rgbimage = np.array([ [ [234, 126, 34, 45, 18], [123, 123, 49, 34, 56], [255, 233, 233, 233, 233] ],
... [ [12, 12, 12, 12, 12], [14, 15, 16, 17, 17], [13, 12, 14, 12, 12] ],
... [ [67, 56, 56, 57, 56], [56, 57, 56, 57, 58], [59, 58, 58, 59, 57] ] ])
>>> print rgbimage.ndim
3
>>> print rgbimage.shape
(3, 3, 5)
>>> rgbimage.size
45

В принципе с размерностями и рангами можно особо не заморачиваться, рассматривая многомерные массивы как вложенные (массивы, содержащие другие массивы) при этом следует не забывать, что длинна всех массивов находящихся на одном уровне вложения (на одной оси) обязана быть одинаковой.

>>> good = np.array([ [1, 2, 3], [4, 5, 6], [7, 8, 9] ])
>>> wrong = np.array([ [1, 2, 3], [4, 5, 6], [7, 8] ])
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: setting an array element with a sequence.

Операции над массивами

Все вышесказанное было бы абсолютно бесполезной белибердой, если бы над массивами (над данными содержащимися в массивах) невозможно было б производить каких либо содержательных действий. Разумеется это не так. Цель создания пакета NumPy - предоставить пользователю Python инструменты для "быстрой" работы с большими наборами данных.

Объекты класса numpy.ndarray можно использовать как операнды в выражениях, при этом все операции с массивами будут выполняться поэлементно, то есть итогом операции будет новый массив, содержащий результаты применения операции к каждому элементу исходного массива:

>>> import numpy as np

>>> a = np.arange(6)
>>> print a
[0 1 2 3 4 5]

>>> print a + 1
[1 2 3 4 5 6]

>>> print a * 3
[ 0  3  6  9 12 15]

>>> print a ** 3
[  0   1   8  27  64 125]

>>> print a < 3
[ True  True  True False False False]

>>> a += 2
>>> print a
[2 3 4 5 6 7]

В пакете NumPy определены основные математические функции, так же позволяющие проводить поэлементные вычисления:

>>> t = np.linspace(0.0, 2 * np.pi, 4)
>>> print t
[ 0.          2.0943951   4.1887902   6.28318531]

>>> print np.cos(t)
[ 1.  -0.5 -0.5  1. ]

>>> print np.sin(t)
[  0.00000000e+00   8.66025404e-01  -8.66025404e-01  -2.44929360e-16]

>>> print np.tan(t)
[  0.00000000e+00  -1.73205081e+00   1.73205081e+00  -2.44929360e-16]

>>> s = np.linspace(2.0, 6.0, 5)
>>> print s
[ 2.  3.  4.  5.  6.]

>>> s = np.exp(s)
>>> print s
[   7.3890561    20.08553692   54.59815003  148.4131591   403.42879349]

>>> s = np.log(s)
>>> print s
[ 2.  3.  4.  5.  6.]

>>> print np.sqrt(s ** 2)
[ 2.  3.  4.  5.  6.]

Привожу список самых полезных (на мой взгляд) математических функций пакета NumPy:

Во всех определениях ниже a массив или скаляр.

numpy.abs(a) - абсолютное значение a;

numpy.around(a, decimals = 0, out = None) - округляет a до заданного количества десятичных разрядов, по умолчанию до целого. Придерживается следующего правила округления. Если значение a находиться точно по середине между двумя целыми, округление производиться до ближайшего четного целого. Так если a равно 1.5 или 2.5 будет возвращено 2, если a равно -0.5 или 0.5 будет возвращено 0.0. Аргумент decimals - целое, десятичный разряд после запятой, до которого производиться округление, если decimals отрицательное, разряд отсчитывается влево от запятой.

>>> print np.around(np.array([0.5, 1.8, 2.5, 3.5]))
[ 0.  2.  2.  4.]

>>> print np.around(np.array([1, 5, 15, 45]), decimals = 1)
[ 1  5 15 45]

>>> print np.around(np.array([1, 5, 15, 45]), decimals = -1)
[ 0  0 20 40]

Аргумент out - массив, в который будет передан результат, структура массива out, должна совпадать со структурой возвращаемого массива. Если None (по умолчанию) будет создан новый массив.

numpy.fix(a, out = None) - отбрасывает дробную часть a;

numpy.ceil(a, out = None) - округляет a до меньшего из целых больших или равных a;

>>>  print np.ceil(np.array([-2.7, -1.2, -0.5, 1.8, 2.4, 3.6]))
[-2. -1. -0.  2.  3.  4.]

numpy.floor(a, out = None) - округляет a до большего из целых меньших или равных a;

>>> print np.floor(np.array([-2.7, -1.2, -0.5, 1.8, 2.4, 3.6]))
[-3. -2. -1.  1.  2.  3.]

numpy.sign(a) - возвращает -1 если a < 0, 0 если a == 0, 1 если a > 0;

numpy.degrees(a) - конвертирует a из радиан в градусы;

numpy.radians(a) - конвертирует a из градусов в радианы;

numpy.cos(a), numpy.sin(a), numpy.tan(a) - возвращает косинус, синус, тангенс a. a в радианах;

numpy.cosh(a), numpy.sinh(a), numpy.tanh(a) - возвращает гиперболические косинус, синус, тангенс a. a в радианах;

numpy.arccos(a), numpy.arcsin(a), numpy.arctan(a) - возвращает арккосинус, арксинус, арктангенс a в радианах. Для арккосинуса в диапазоне [0, nump.pi], для арксинуса [-numpy.pi/2, numpy.pi/2], для арктангенса [-numpy.pi/2, numpy.pi/2];

numpy.arccosh(a), numpy.arcsinh(a), numpy.arctanh(a) - возвращает гиперболические косинус, синус, тангенс a. a в радианах;

numpy.exp(a) - возвращает основание натурального логарифма (число e) в степени a;

numpy.log(a), numpy.log10(a), numpy.log2(a) - возвращает натуральный логарифм a, логарифм a по основанию 10, логарифм a по основанию 2;

numpy.log1p(a) - возвращает натуральный логарифм a + 1;

numpy.sqrt(a) - возвращает положительный квадратный корень a;

numpy.square(a) - возвращает квадрат a.

В выражения можно включать несколько массивов. В случае если атрибуты ndarray.shape этих массивов совпадают, результат очевиден - действия над массивами будут производиться поэлементно:

>>> a1 = np.array([ [1.0, 2.0], [3.0, 4.0] ])
>>> a2 = np.array([ [5.0, 6.0], [7.0, 8.0] ])
>>> a3 = np.array([ [9.0, 10.0], [11.0, 12.0] ])

>>> print a1 + a2 + a3
[[ 15.  18.]
 [ 21.  24.]]

>>> print a1 * a2
[[  5.  12.]
 [ 21.  32.]]

>>> print a3 / a1
[[ 9.          5.        ]
 [ 3.66666667  3.        ]]

>>> print (a3 - a1) * a2
[[ 40.  48.]
 [ 56.  64.]]

Если атрибуты ndarray.shape не совпадают - действия над массивами производятся в соответствии с концепцией транслирования (broadcasting).

Операция транслирования - расширение одного или обоих массивов операндов до массивов с равной размерностью.

Для начала несколько примеров:

>>> a2 = np.array([1, 2])
>>> print a2
[1 2]
>>> print a2.shape
(2,)

>>> a22 = np.array([ [1, 2], [3, 4] ])
>>> print a22
[[1 2]
 [3 4]]
>>> print a22.shape
(2, 2)

>>> a32 = np.array([ [1, 2], [3, 4], [5, 6] ])
>>> print a32
[[1 2]
 [3 4]
 [5 6]]
>>> print a32.shape
(3, 2)

>>> a222 = np.array([ [ [1, 2], [3, 4] ], [ [5, 6], [7, 8] ] ])
>>> print a222
[[[1 2]
  [3 4]]

 [[5 6]
  [7 8]]]
>>> print a222.shape
(2, 2, 2)

>>> print a22 + a2
[[2 4]
 [4 6]]
 
>>> print a32 + a2
[[2 4]
 [4 6]
 [6 8]]

>>> print a222 + a2
[[[ 2  4]
  [ 4  6]]

 [[ 6  8]
  [ 8 10]]]

>>> print a32 + a22
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: shape mismatch: objects cannot be broadcast to a single shape

>>> print a222 + a22
[[[ 2  4]
  [ 6  8]]

 [[ 6  8]
  [10 12]]]

>>> print a222 + a32
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: shape mismatch: objects cannot be broadcast to a single shape

И так правило. Если длины осей массивов, начиная с замыкающей, попарно равны или в каждой из сравниваемых пар длин хотя бы одна равна единице, то к таким массивам может быть применена операция транслирования. Вот. Длина замыкающей оси - длина массива вложенного наиболее глубоко.

>>> a = np.ones((7, 3, 4, 9, 8))
>>> b = np.ones((4, 9, 8))
>>> c = np.ones((4, 3, 8))

>>> print (a + b).shape
(7, 3, 4, 9, 8)

>>> print (a + c).shape
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: shape mismatch: objects cannot be broadcast to a single shape

>>> d = np.ones((9, 7, 1, 6, 1))
>>> e = np.ones((7, 2, 1, 4))
>>> print (d + e).shape
(9, 7, 2, 6, 4)

В случае если в массивах присутствуют оси единичной длины, расширяться могут оба массива.

>>> f = np.array([1, 2])
>>> print f
[1 2]
>>> print f.shape
(2,)

>>> g = np.array([ [3], [4], [5] ])
>>> print g
[[3]
 [4]
 [5]]
>>> print g.shape
(3, 1)

>>> h = f + g
>>> print h
[[4 5]
 [5 6]
 [6 7]]
>>> print h.shape
(3, 2)

Трансляция массивов происходит и при вызове некоторых функций.

Например, функция numpy.power(a1, a2), где a1, a2 массив или скаляр, возвращает a1 в степени a2:

>>> print np.power(np.array([-2.0, -1.0, 0.0, 2.0, 3.0, 4.0]), 4)
[  16.    1.    0.   16.   81.  256.]

>>> print np.power(np.array([-2.0, -1.0, 0.0, 2.0, 3.0, 4.0]), -4)
[ 0.0625      1.                 Inf  0.0625      0.01234568  0.00390625]

в случае если у переданных массивов не совпадает структура, проводит трансляцию.

>>> print np.power(np.array([3, 7]), np.array([ [1], [2], [3] ]))
[[  3   7]
 [  9  49]
 [ 27 343]]

Некоторые полезные функции

Здесь я приведу еще один набор полезных функций.

numpy.min(a, axis = None, out = None),

numpy.max(a, axis = None, out = None) -

возвращает минимальное, максимальное значение элементов массива соответственно:

>>> import numpy as np

>>> print np.min(np.array([ [1.0, -0.5, 3.0], [4.0, 3.0, -0.5] ]))
-0.5

axis - опциональный аргумент, индекс оси (измерения) массива по которому проводится поиск минимального, максимального значения. Под индексом оси понимается индекс в кортеже ndarray.shape.

>>> a = np.array([ [ [1.0, 2.0, 3.0], [4.0, 5.0, 6.0] ], [ [7.0, 8.0, 9.0], [11, 12, 13] ] ])
>>> print a
[[[  1.   2.   3.]
  [  4.   5.   6.]]

 [[  7.   8.   9.]
  [ 11.  12.  13.]]]
>>> print a.shape
(2, 2, 3)

>>> print np.min(a, axis = 0)
[[ 1.  2.  3.]
 [ 4.  5.  6.]]

>>> print np.min(a, axis = 1)
[[ 1.  2.  3.]
 [ 7.  8.  9.]]

>>> print np.min(a, axis = 2)
[[  1.   4.]
 [  7.  11.]]

out - опциональный аргумент, массив, в который будет помещен результат. Структура массива out должна соответствовать структуре массива результата. Если out = None (по умолчанию) будет создан новый массив.

numpy.argmin(a, axis = None),

numpy.argmax(a, axis = None) -

возвращает индекс минимального, максимального значения элементов массива соответственно:

>>> print np.argmin(np.array([ [1.0, -0.5, 3.0], [4.0, 3.0, -0.5] ]))
1

>>> print np.argmin(a, axis = 0)
[[0 0 0]
 [0 0 0]]

>>> print np.argmin(a, axis = 1)
[[0 0 0]
 [0 0 0]]

>>> print np.argmin(a, axis = 2)
[[0 0]
 [0 0]]

numpy.sum(a, axis = None, dtype = None, out = None),

numpy.prod(a, axis = None, dtype = None, out = None) -

возвращает сумму, произведение элементов массива соответственно:

>>> print np.sum(np.array([ [1.0, -0.5, 3.0], [4.0, 3.0, -0.5] ]))
10.0
>>> print np.sum(a, axis = 0)
[[  8.  10.  12.]
 [ 15.  17.  19.]]
>>> print np.sum(a, axis = 1)
[[  5.   7.   9.]
 [ 18.  20.  22.]]
>>> print np.sum(a, axis = 2)
[[  6.  15.]
 [ 24.  36.]]

Ну вот пожалуй на сегодня и все. Разговор о пакете NumPy продолжим в следующий раз.

4 комментария:

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

    a = np.array([1,2,3,4,5])
    a[a<3]

    ОтветитьУдалить
  2. Очень полезная статья. Не хотите так же написать и про SciPy? Было бы очень интересно.

    ОтветитьУдалить
  3. можно ли numpy прикрутить к ssi или mpi?

    в частности нужно обратить огромнейшую матрицу - хотелось бы другие компьютеры сети в помощь....

    ОтветитьУдалить
  4. Этот комментарий был удален автором.

    ОтветитьУдалить