Векторизация
Contents
Векторизация¶
Про векторизацию¶
NumPy
оптимизирован для работы с многомерными массивами, но циклы python
нет. В связи с этим распространен подход, называемый векторизацией, при котором устраняются циклы, а вместо них вызываются встроенные в NumPy
методы. Таким образом эти циклы осуществляются внутри C
кода.
Продемонстрируем это на примере.
import numpy as np
N = 10 ** 7
x = np.random.random(N)
%%time
s1 = 0
for i in range(N):
s1 += x[i]
CPU times: total: 3.36 s
Wall time: 3.36 s
%%timeit
s2 = x.sum()
11.6 ms ± 23.5 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
Note
%%time
— команда jupyter notebook измеряющая затраченное на исполнение кода в ячейке время.
%%timeit
усредняет это время по нескольким запускам.
У векторизации есть три основных плюса:
Скорость: векторизованный код исполняется гораздо быстрее, чем его аналог в циклах;
Выразительность: векторизованный код больше похож на математическое выражение, что упрощает его чтение;
Количество кода: векторизованный код без циклов как правило короче, а значит в нем сложнее ошибиться.
Простые математические функции¶
В NumPy
есть большое количество математических функций, полный список которых можно посмотреть здесь. Применять их можно сразу к всему массиву. Тогда выбранная функция будет применена к всем элементам массива. Все циклы выполнятся в C
коде.
%%time
y = np.zeros(N)
for i in range(N):
y[i] = np.sin(x[i])
CPU times: total: 27.3 s
Wall time: 27.9 s
%%timeit
y = np.sin(x)
111 ms ± 1.12 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
Суммы, произведения, максимум, минимум и т.п.¶
Так же в NumPy
реализован ряд агрегирующих методов, таких как сумма элементов массива (ndarray.sum), их произведение (ndarray.prod) и т.п. Часть из них перечислена здесь. Методы ndarray.min
и ndarray.max
ссылаются на функции np.amin и np.amax соответственно.
Каждая из них позволяет не только вычислять значение по всему массиву, но и вдоль обозначенной оси. Рассмотрим на примере функций min
и sum
.
x = np.random.randint(0, 100, size=(3, 3))
print(x)
[[97 31 91]
[56 86 8]
[15 83 56]]
print(x.min()) # минимум всего массива
8
print(x.min(axis=0)) # минимум по столбцам
[15 31 8]
print(x.min(axis=1)) # минимум по строкам
[31 8 15]
print(x.sum())
523
print(x.sum(axis=0))
[168 200 155]
print(x.sum(axis=1))
[219 150 154]
Арифметика.¶
Аналогично, всегда предпочтительнее использовать операторы +
, -
, *
, /
, //
, %
, **
, @
, |
, &
сразу к массивам, а не к их элементам внутри циклов.
x = np.zeros(N)
y = np.ones(N)
%%time
z = np.zeros(N)
for i in range(N):
z[i] = x[i] + y[i]
CPU times: total: 4.8 s
Wall time: 4.81 s
%%timeit
z = x + y
41.7 ms ± 447 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
Broadcasting¶
В ряде случаев допускается применять арифметические операторы к массивам разной формы. Строго говоря, NumPy
может оперировать с двумя массивами, если их формы совместимы (broadcastable
). Подробнее почитать о broadcasting
в документации можно здесь и здесь.
Пример 1. Скаляр и массив.
Начнем с самого простого примера. Если в арифметическом выражении участвует массив любой формы и скаляр, то можно представить, что скаляр расширяется до массива такой же формы, у которого все элементы равны значению скаляра.
На картинке выше массив a
из трех элементов умножается на скаляр b
(который можно представить в виде массива из одного элемента).
a = np.array([1, 2, 3])
b = np.array([2])
result = a * b
print(f"{a} * {b} = {result}")
[1 2 3] * [2] = [2 4 6]
Note
В реальности никакого промежуточного массива из двоек в примере выше не создается. Это лишь удобный способ представить себе, что происходит.
Общий случай. В более общем случае, два массива совместимы, если размеры вдоль всех их осей совместимы. Размеры вдоль двух осей совместимы, только если
они равны;
один из них равен единицы; При этом проверка этих размеров происходит слева направо.
Рассмотрим примеры совместимых и не совместимых форм.
Совместимые формы.
|
|
|
Комментарий |
---|---|---|---|
(3,) |
(1, ) |
(3, ) |
Размер |
(1, 3) |
(3, 1) |
(3, 3) |
Размеры вдоль последней оси совместимы, т.к. размер |
(8, 1, 6, 1) |
(7, 1, 5) |
(8, 7, 5, 6) |
Массив |
(15, 4, 3) |
(15, 1, 3) |
(15, 4, 3) |
Размеры вдоль всех осей совместимы |
Несовместимые формы.
|
|
Комментарий |
---|---|---|
(3, ) |
(4, ) |
Размеры вдоль единственных осей не равны между собой, ни один из них не равен 1 |
(2, 1) |
(2, 4, 3) |
Сначала сравниваются 1 и 3: всё ок. Затем сравниваются 2 и 4: несовместимы. Можно представить форму (2, 1) в виде (1, 2, 1) |
Пример 2. Матрица и вектор-строка.
Если складываются (умножаются и т.п.) двумерный и одномерный массивы, то операция возможна, если количество столбцов в матрице соответствует количеству элементов в одномерном массиве.
В примере выше складываются массивы с формами (4, 3)
и (3, )
, что эквивалентно записи (4, 3)
и (1, 3)
. Формы совместимы и результат выходит такой, будто одномерный массив расширяется до двумерного массива с формой (4, 3)
“по столбцам”.
a = np.array([[0, 0, 0], [10, 10, 10], [20, 20, 20], [30, 30, 30]])
b = np.array([0, 1, 2])
result = a + b
print(f"{a} \n + \n {b} \n = \n {result} ")
[[ 0 0 0]
[10 10 10]
[20 20 20]
[30 30 30]]
+
[0 1 2]
=
[[ 0 1 2]
[10 11 12]
[20 21 22]
[30 31 32]]
А в этом примере формы не совместимы.
a = np.array([[0, 0, 0], [10, 10, 10], [20, 20, 20], [30, 30, 30]])
b = np.array([1, 2, 3, 4])
a + b
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
Input In [18], in <cell line: 3>()
1 a = np.array([[0, 0, 0], [10, 10, 10], [20, 20, 20], [30, 30, 30]])
2 b = np.array([1, 2, 3, 4])
----> 3 a + b
ValueError: operands could not be broadcast together with shapes (4,3) (4,)
Пример 3. Матрица и вектор столбец.
Аналогично можно сложить матрицу с вектором столбцом, но для этого этот массив нужно транспонировать, т.е. привести вектор формы (n, )
(одномерный массив из n
элементов) к форме (n, 1)
(матрица из n
строк длинны 1).
К сожалению, ndarray.T
(np.transpose
) не приведёт к желаемому результату, если массив одномерный.
print(b, b.T)
[1 2 3 4] [1 2 3 4]
Сделать это можно изменив форму или “вставив ось”.
n = b.shape[0]
print(b.reshape((n, 1)))
print()
print(b[:, np.newaxis])
[[1]
[2]
[3]
[4]]
[[1]
[2]
[3]
[4]]
Теперь можно сложить матрицу со столбцом.
result = a + b[:, np.newaxis]
print(result)
[[ 1 1 1]
[12 12 12]
[23 23 23]
[34 34 34]]
Пример 3. Вектор столбец и вектор строка
Если складывать вектор столбец и вектор строку, то каждая из них как бы расширится до матрицы по строкам и по столбцам.
a = np.array([0, 10, 20, 30])[:, np.newaxis]
b = np.array([1, 2, 3])
result = a + b
print(f"{a} \n + \n {b} \n = \n {result}")
[[ 0]
[10]
[20]
[30]]
+
[1 2 3]
=
[[ 1 2 3]
[11 12 13]
[21 22 23]
[31 32 33]]