Массивы NumPy
Краткий справочник для читателей, не знакомых с Python. Полностью тема рассматривается в основных главах книги; в печатное издание приложение не вошло.
NumPy является основной библиотекой для научных вычислений на Python: она предоставляет высокопроизводительный объект многомерного массива и инструменты для работы с такими массивами.
Прежде всего необходимо импортировать пакет numpy.
import numpy as np
Массивы
Массив NumPy представляет собой сетку значений одного типа, индексируемую кортежем неотрицательных целых чисел. Число измерений массива хранится в поле ndim, а форма (shape) представляет собой кортеж целых чисел с размерами вдоль каждого измерения.
Массивы NumPy могут быть созданы из вложенных списков Python, а обращение к элементам осуществляется через квадратные скобки:
a = np.array([1, 2, 3]) # Создаём одномерный массив
print(type(a), a.shape, a[0], a[1], a[2])
a[0] = 5 # Меняем элемент массива
print(a)
<class 'numpy.ndarray'> (3,) 1 2 3
[5 2 3]
b = np.array([[1,2,3],[4,5,6]]) # Создаём двумерный массив
print(b)
[[1 2 3]
[4 5 6]]
print(b.shape)
print(b[0, 0], b[0, 1], b[1, 0])
(2, 3)
1 2 4
NumPy также предоставляет большое число функций для создания массивов:
a = np.zeros((2,2)) # Создаём массив из одних нулей
print(a)
[[0. 0.]
[0. 0.]]
b = np.ones((1,2)) # Создаём массив из одних единиц
print(b)
[[1. 1.]]
c = np.full((2,2), 7) # Создаём массив, заполненный константой
print(c)
[[7 7]
[7 7]]
d = np.eye(2) # Создаём единичную матрицу 2x2
print(d)
[[1. 0.]
[0. 1.]]
e = np.random.random((2,2)) # Создаём массив, заполненный случайными значениями
print(e)
[[0.57584699 0.0757792 ]
[0.18793454 0.78004389]]
Индексация массивов
NumPy предоставляет несколько способов индексации массивов.
Срезы. Как и к спискам Python, к массивам NumPy применимы срезы; поскольку массивы многомерны, срез указывается для каждого измерения.
import numpy as np
# Создаём следующий двумерный массив с формой (3, 4)
# [[ 1 2 3 4]
# [ 5 6 7 8]
# [ 9 10 11 12]]
a = np.array([[1,2,3,4], [5,6,7,8], [9,10,11,12]])
# С помощью среза вытаскиваем подмассив из первых двух строк
# и столбцов 1 и 2; b — следующий массив с формой (2, 2):
# [[2 3]
# [6 7]]
b = a[:2, 1:3]
print(b)
[[2 3]
[6 7]]
Срез массива является представлением (view) тех же самых данных, поэтому изменение среза приводит к изменению исходного массива.
print(a[0, 1])
b[0, 0] = 77 # b[0, 0] — те же самые данные, что и a[0, 1]
print(a[0, 1])
2
77
Целочисленную индексацию можно сочетать со срезами, однако в результате получается массив меньшей размерности, чем исходный.
# Создаём следующий двумерный массив с формой (3, 4)
a = np.array([[1,2,3,4], [5,6,7,8], [9,10,11,12]])
print(a)
[[ 1 2 3 4]
[ 5 6 7 8]
[ 9 10 11 12]]
Существует два способа обращения к средней строке массива: сочетание целочисленной индексации со срезами даёт массив меньшей размерности, тогда как одни только срезы сохраняют число измерений исходного массива.
row_r1 = a[1, :] # Одномерное представление второй строки a
row_r2 = a[1:2, :] # Двумерное представление второй строки a
row_r3 = a[[1], :] # Двумерное представление второй строки a
print(row_r1, row_r1.shape)
print(row_r2, row_r2.shape)
print(row_r3, row_r3.shape)
[5 6 7 8] (4,)
[[5 6 7 8]] (1, 4)
[[5 6 7 8]] (1, 4)
# То же различие видно и при обращении к столбцам массива:
col_r1 = a[:, 1]
col_r2 = a[:, 1:2]
print(col_r1, col_r1.shape)
print(col_r2, col_r2.shape)
[ 2 6 10] (3,)
[[ 2]
[ 6]
[10]] (3, 1)
Индексация целочисленными массивами. Срез всегда является подмассивом исходного массива, тогда как индексация целочисленными массивами позволяет формировать произвольные массивы из данных другого массива.
a = np.array([[1,2], [3, 4], [5, 6]])
# Пример индексации целочисленными массивами.
# Возвращаемый массив будет иметь форму (3,)
print(a[[0, 1, 2], [0, 1, 0]])
# Пример выше эквивалентен вот этому:
print(np.array([a[0, 0], a[1, 1], a[2, 0]]))
[1 4 5]
[1 4 5]
# При индексации целочисленными массивами можно повторно
# использовать один и тот же элемент исходного массива:
print(a[[0, 0], [1, 1]])
# Эквивалентно предыдущему примеру с целочисленной индексацией
print(np.array([a[0, 1], a[0, 1]]))
[2 2]
[2 2]
Индексация целочисленными массивами позволяет выбрать или изменить один элемент в каждой строке матрицы.
# Создаём новый массив, из которого будем выбирать элементы
a = np.array([[1,2,3], [4,5,6], [7,8,9], [10, 11, 12]])
print(a)
[[ 1 2 3]
[ 4 5 6]
[ 7 8 9]
[10 11 12]]
# Создаём массив индексов
b = np.array([0, 2, 0, 1])
# Выбираем по одному элементу из каждой строки a, используя индексы из b
print(a[np.arange(4), b]) # Напечатает "[ 1 6 7 11]"
[ 1 6 7 11]
# Изменяем по одному элементу в каждой строке a, используя индексы из b
a[np.arange(4), b] += 10
print(a)
[[11 2 3]
[ 4 5 16]
[17 8 9]
[10 21 12]]
Булева индексация. Этот способ позволяет выбирать произвольные элементы массива; чаще всего он применяется для отбора элементов, удовлетворяющих заданному условию.
import numpy as np
a = np.array([[1,2], [3, 4], [5, 6]])
bool_idx = (a > 2) # Находим элементы a, которые больше 2;
# получаем numpy-массив булевых значений той же
# формы, что и a, где каждая ячейка bool_idx
# сообщает, выполняется ли для элемента a условие > 2.
print(bool_idx)
[[False False]
[ True True]
[ True True]]
# Используем булеву индексацию, чтобы построить одномерный массив,
# состоящий из элементов a, которым соответствуют значения True
# в bool_idx
print(a[bool_idx])
# Всё это можно записать одним коротким выражением:
print(a[a > 2])
[3 4 5 6]
[3 4 5 6]
Многие детали индексации здесь опущены; полное описание приведено в документации.
Типы данных
Каждый массив NumPy состоит из элементов одного типа; библиотека предоставляет большой набор числовых типов. При создании массива NumPy определяет тип автоматически, однако функции создания массивов, как правило, принимают необязательный аргумент, задающий тип явно.
x = np.array([1, 2]) # Пусть numpy сам выберет тип данных
y = np.array([1.0, 2.0]) # Пусть numpy сам выберет тип данных
z = np.array([1, 2], dtype=np.int64) # Явно задаём конкретный тип данных
print(x.dtype, y.dtype, z.dtype)
int64 float64 int64
Полное описание типов данных NumPy приведено в документации.
Математические операции над массивами
Базовые математические функции применяются к массивам поэлементно и доступны как в виде перегруженных операторов, так и в виде функций модуля numpy:
x = np.array([[1,2],[3,4]], dtype=np.float64)
y = np.array([[5,6],[7,8]], dtype=np.float64)
# Поэлементная сумма; оба способа дают массив
print(x + y)
print(np.add(x, y))
[[ 6. 8.]
[10. 12.]]
[[ 6. 8.]
[10. 12.]]
# Поэлементная разность; оба способа дают массив
print(x - y)
print(np.subtract(x, y))
[[-4. -4.]
[-4. -4.]]
[[-4. -4.]
[-4. -4.]]
# Поэлементное произведение; оба способа дают массив
print(x * y)
print(np.multiply(x, y))
[[ 5. 12.]
[21. 32.]]
[[ 5. 12.]
[21. 32.]]
# Поэлементное деление; оба способа дают массив
# [[ 0.2 0.33333333]
# [ 0.42857143 0.5 ]]
print(x / y)
print(np.divide(x, y))
[[0.2 0.33333333]
[0.42857143 0.5 ]]
[[0.2 0.33333333]
[0.42857143 0.5 ]]
# Поэлементный квадратный корень; получается массив
# [[ 1. 1.41421356]
# [ 1.73205081 2. ]]
print(np.sqrt(x))
[[1. 1.41421356]
[1.73205081 2. ]]
В отличие от MATLAB, оператор * выполняет поэлементное, а не матричное умножение. Для скалярного произведения векторов, умножения вектора на матрицу и перемножения матриц служит функция dot, доступная как в виде функции модуля numpy, так и в виде метода объекта-массива.
x = np.array([[1,2],[3,4]])
y = np.array([[5,6],[7,8]])
v = np.array([9,10])
w = np.array([11, 12])
# Скалярное произведение векторов; оба способа дают 219
print(v.dot(w))
print(np.dot(v, w))
219
219
# Произведение матрицы на вектор; оба способа дают одномерный массив [29 67]
print(x.dot(v))
print(np.dot(x, v))
[29 67]
[29 67]
# Произведение матриц; оба способа дают двумерный массив
# [[19 22]
# [43 50]]
print(x.dot(y))
print(np.dot(x, y))
[[19 22]
[43 50]]
[[19 22]
[43 50]]
Одной из наиболее востребованных функций для вычислений над массивами является sum.
x = np.array([[1,2],[3,4]])
print(np.sum(x)) # Считаем сумму всех элементов; напечатает "10"
print(np.sum(x, axis=0)) # Считаем сумму каждого столбца; напечатает "[4 6]"
print(np.sum(x, axis=1)) # Считаем сумму каждой строки; напечатает "[3 7]"
10
[4 6]
[3 7]
Полный список математических функций NumPy приведён в документации.
Помимо вычислений часто требуется изменять форму данных в массивах. Простейшим примером является транспонирование матрицы с помощью атрибута T объекта-массива.
print(x)
print(x.T)
[[1 2]
[3 4]]
[[1 3]
[2 4]]
v = np.array([[1,2,3]])
print(v)
print(v.T)
[[1 2 3]]
[[1]
[2]
[3]]
Broadcasting
Broadcasting (транслирование) представляет собой механизм, позволяющий NumPy выполнять арифметические операции над массивами разной формы. Типичный случай: имеются малый и большой массивы, и малый необходимо многократно использовать в операции над большим.
Предположим, что требуется прибавить постоянный вектор к каждой строке матрицы:
# Прибавим вектор v к каждой строке матрицы x,
# результат запишем в матрицу y
x = np.array([[1,2,3], [4,5,6], [7,8,9], [10, 11, 12]])
v = np.array([1, 0, 1])
y = np.empty_like(x) # Создаём пустую матрицу той же формы, что и x
# Прибавляем вектор v к каждой строке матрицы x явным циклом
for i in range(4):
y[i, :] = x[i, :] + v
print(y)
[[ 2 2 4]
[ 5 5 7]
[ 8 8 10]
[11 11 13]]
Такой подход работает, однако при большой матрице x явный цикл на Python выполняется медленно. Прибавление вектора v к каждой строке x равносильно формированию матрицы vv из вертикально уложенных копий v с последующим поэлементным сложением x и vv.
vv = np.tile(v, (4, 1)) # Укладываем 4 копии v друг на друга
print(vv) # Напечатает "[[1 0 1]
# [1 0 1]
# [1 0 1]
# [1 0 1]]"
[[1 0 1]
[1 0 1]
[1 0 1]
[1 0 1]]
y = x + vv # Поэлементно складываем x и vv
print(y)
[[ 2 2 4]
[ 5 5 7]
[ 8 8 10]
[11 11 13]]
Broadcasting позволяет выполнить это вычисление, не создавая копий v.
# Прибавим вектор v к каждой строке матрицы x,
# результат запишем в матрицу y
x = np.array([[1,2,3], [4,5,6], [7,8,9], [10, 11, 12]])
v = np.array([1, 0, 1])
y = x + v # Прибавляем v к каждой строке x с помощью broadcasting
print(y)
[[ 2 2 4]
[ 5 5 7]
[ 8 8 10]
[11 11 13]]
Строка y = x + v выполняется корректно, хотя x имеет форму (4, 3), а v — форму (3,): благодаря broadcasting она выполняется так, как если бы v имел форму (4, 3), каждая его строка являлась копией v, а сложение осуществлялось поэлементно.
Broadcasting двух массивов подчиняется следующим правилам:
- Если массивы имеют разное число измерений, форма массива меньшей размерности дополняется единицами слева, пока обе формы не станут одной длины.
- Два массива называются совместимыми по измерению, если их размеры в этом измерении совпадают или если размер одного из массивов в этом измерении равен 1.
- Broadcasting массивов возможен, если они совместимы по всем измерениям.
- После broadcasting каждый массив ведёт себя так, как будто его форма равна поэлементному максимуму форм двух входных массивов.
- В каждом измерении, где размер одного массива равен 1, а размер другого больше 1, первый массив ведёт себя так, как будто его скопировали вдоль этого измерения.
Иное изложение тех же правил приведено в документации.
Функции, поддерживающие broadcasting, называются универсальными; полный список приведён в документации.
Ниже приведены несколько применений broadcasting:
# Вычисляем внешнее произведение векторов
v = np.array([1,2,3]) # v имеет форму (3,)
w = np.array([4,5]) # w имеет форму (2,)
# Чтобы вычислить внешнее произведение, сначала превращаем v в вектор-столбец
# формы (3, 1); затем с помощью broadcasting совмещаем его с w и получаем
# результат формы (3, 2) — внешнее произведение v и w:
print(np.reshape(v, (3, 1)) * w)
[[ 4 5]
[ 8 10]
[12 15]]
# Прибавляем вектор к каждой строке матрицы
x = np.array([[1,2,3], [4,5,6]])
# x имеет форму (2, 3), а v — форму (3,), поэтому broadcasting сводит их
# к форме (2, 3) и даёт следующую матрицу:
print(x + v)
[[2 4 6]
[5 7 9]]
# Прибавляем вектор к каждому столбцу матрицы
# x имеет форму (2, 3), а w — форму (2,).
# Если транспонировать x, он получит форму (3, 2), и его можно совместить
# с w через broadcasting, получив результат формы (3, 2);
# транспонировав этот результат, получаем итог формы (2, 3) — матрицу x,
# к каждому столбцу которой прибавлен вектор w. Получается следующая матрица:
print((x.T + w).T)
[[ 5 6 7]
[ 9 10 11]]
# Другое решение — преобразовать w в вектор формы (2, 1);
# тогда его можно совместить с x через broadcasting напрямую
# и получить тот же результат.
print(x + np.reshape(w, (2, 1)))
[[ 5 6 7]
[ 9 10 11]]
# Умножаем матрицу на константу:
# x имеет форму (2, 3). NumPy рассматривает скаляры как массивы формы ();
# broadcasting сводит их к форме (2, 3), и получается
# следующий массив:
print(x * 2)
[[ 2 4 6]
[ 8 10 12]]
Broadcasting, как правило, делает код более лаконичным и быстрым; его целесообразно использовать везде, где это возможно.
Настоящий обзор далеко не полон; остальное приведено в справочнике NumPy.