Расширение массивов в Numpy
Давайте рассмотрим более продвинутую концепцию в numpy, называемую расширением массивов. Термин «расширение массивов» описывает, как numpy обрабатывает массивы с разными форматами во время арифметических операций. При определенных ограничениях меньший массив «расширяется» на больший массив, чтобы они имели совместимые форматы. Расширение массивов обеспечивает способ векторизации операций над массивами, так что циклы выполняются на C, а не на Python. Это делается без необходимости создания лишних копий данных и обычно приводит к эффективной реализации алгоритмов. Также есть случаи, когда расширение массивов – плохая идея, поскольку оно приводит к неэффективному использованию памяти, что замедляет вычисления. Эта статья предоставляет краткое введение в расширение массивов с многочисленными примерами, начиная от простых и заканчивая сложными. Она также предоставляет подсказки о том, когда использовать расширение массивов, а когда – нет.
Операции numpy обычно выполняются поэлементно, что требует, чтобы два массива имели точно одинаковую форму:
>>> from numpy import array >>> a = array([1.0, 2.0, 3.0]) >>> b = array([2.0, 2.0, 2.0]) >>> a * b array([ 2., 4., 6.])
Правило расширения массивов numpy ослабляет это ограничение, когда форматы массивов удовлетворяют определенным условиям. Простейший пример расширения массивов возникает, когда массив и скалярное значение объединяются в операции:
>>> from numpy import array >>> a = array([1.0,2.0,3.0]) >>> b = 2.0 >>> a * b array([ 2., 4., 6.])
Результат эквивалентен предыдущему примеру, где b был массивом. Мы можем представить скаляр b как растягиваемый во время арифметической операции в массив с такой же формой, что и a. Новые элементы в b, как показано на рисунке Рисунок 1, просто являются копиями исходного скаляра. Аналогия с растяжением только концептуальна. Numpy достаточно умен, чтобы использовать исходное скалярное значение без фактического создания копий, чтобы операции расширения массивов были максимально эффективными с точки зрения памяти и вычислений. Поскольку Пример 2 перемещает меньше памяти (b — скаляр, а не массив) во время умножения, он примерно на 10% быстрее, чем Пример 1 с использованием стандартного numpy в Windows 2000 с массивами из одного миллиона элементов.
Рисунок 1
В простейшем примере расширения массивов скаляр ``b`` растягивается, чтобы стать массивом с такой же формой, что и ``a``, чтобы формы были совместимы для поэлементного умножения.
Правило, определяющее, имеют ли два массива совместимые формы для расширения массивов, можно сформулировать в одном предложении.
Правило расширения массивов
Для расширения массивов размер последующих осей для обоих массивов в операции должен быть одинаковым или один из них должен быть равен единице.
Если это условие не выполняется, выбрасывается исключение ValueError('frames are not aligned'), указывающее, что формы массивов несовместимы. Размер результирующего массива, созданного операциями расширения массивов, является максимальным размером по каждой размерности от входных массивов. Обратите внимание, что правило ничего не говорит о необходимости у массивов одинакового числа измерений. Например, если у вас есть массив 256 x 256 x 3 значений RGB, и вы хотите масштабировать каждый цвет изображения с помощью разного значения, вы можете умножить изображение на одномерный массив с 3 значениями. Выравнивание размеров последующих осей этих массивов в соответствии с правилом расширения показывает, что они совместимы
Изображение | (3d массив) | 256 x | 256 x | 3 |
Масштаб | (1d массив) | 3 | ||
Результат | (3d массив) | 256 x | 256 x | 3 |
В следующем примере и массивы A и B имеют оси с длиной один, которые расширяются до большего размера в операции расширения массивов.
A | (4d массив) | 8 x | 1 x | 6 x | 1 |
B | (3d массив) | 7 x | 1 x | 5 | |
Результат | (4d массив) | 8 x | 7 x | 6 x | 5 |
Ниже приведены несколько примеров кода и графических представлений, которые помогают наглядно продемонстрировать правило расширения массивов. Пример 3 добавляет одномерный массив к двумерному массиву:
>>> from numpy import array
>>> a = array([[ 0.0, 0.0, 0.0],
... [10.0, 10.0, 10.0],
... [20.0, 20.0, 20.0],
... [30.0, 30.0, 30.0]])
>>> b = array([1.0, 2.0, 3.0])
>>> a + b
array([[ 1., 2., 3.],
[ 11., 12., 13.],
[ 21., 22., 23.],
[ 31., 32., 33.]])
Как показано на рисунке Рисунок 2, b добавляется к каждой строке a. Если b длиннее строк a, как показано на рисунке Рисунок 3, возникает исключение из-за несовместимых форм.
Рисунок 2
Двумерный массив, умноженный на одномерный массив, приводит к расширению массивов, если количество элементов одномерного массива соответствует количеству столбцов двумерного массива.
Рисунок 3
Когда последующие размерности массивов не равны, расширение массивов не выполняется, потому что невозможно выровнять значения в строках 1-го массива с элементами 2-го массива для поэлементного сложения.
Расширение массивов обеспечивает удобный способ вычисления внешнего произведения (или любой другой внешней операции) двух массивов. Следующий пример демонстрирует внешнюю операцию сложения двух одномерных массивов, которая дает тот же результат, что и Пример 3
>>> from numpy import array, newaxis
>>> a = array([0.0, 10.0, 20.0, 30.0])
>>> b = array([1.0, 2.0, 3.0])
>>> a[:,newaxis] + b
array([[ 1., 2., 3.],
[ 11., 12., 13.],
[ 21., 22., 23.],
[ 31., 32., 33.]])
Здесь оператор newaxis вставляет новую ось в a, превращая его в двумерный массив 4x1. Рисунок Рисунок 4 иллюстрирует растяжение обоих массивов для получения желаемого выходного массива 4x3.
Рисунок 4
В некоторых случаях расширение массивов растягивает оба массива, образуя выходной массив, больший, чем любой из начальных массивов.*
Практический пример: векторное квантование.
Расширение массивов довольно часто встречается в реальных задачах. Типичный пример возникает в алгоритме векторного квантования (VQ), используемом в теории информации, классификации и других смежных областях. Основная операция в VQ [#f0] находит ближайшую точку в наборе точек, называемых кодами в жаргоне VQ, к заданной точке, называемой наблюдением. В очень простом двумерном случае, показанном на рисунке Рисунок 5, значения в наблюдении описывают вес и рост спортсмена для классификации. Коды представляют собой различные классы спортсменов. 2 Нахождение ближайшей точки требует вычисления расстояния между наблюдением и каждым из кодов. Наименьшее расстояние обеспечивает наилучшее соответствие. В этом примере codes[0] является ближайшим классом, указывая на то, что спортсмен, скорее всего, баскетболист.
Рисунок 5
Основная операция векторного квантования вычисляет расстояние между объектом, подлежащим классификации, тёмным квадратом, и несколькими известными кодами, серыми кружками. В этом простом случае коды представляют отдельные классы. Более сложные случаи используют несколько кодов на класс.
Примечания
-
1 -
Векторное квантование Дж. Махоул, С. Рукос и Х. Гиш, «Векторное квантование в кодировании речи», Proc. IEEE, том 73, с. 1551-1587, ноябрь 1985 г.
-
2 -
В этом примере вес оказывает большее влияние на вычисление расстояния, чем рост, из-за больших значений. На практике важно нормализовать рост и вес, часто по их стандартному отклонению по всему набору данных, чтобы оба оказывали одинаковое влияние на вычисление расстояния.
Примечание
Код для создания рисунков является частью книги AstroML
© 2005–2020 NumPy Developers
Licensed under the 3-clause BSD License.
https://numpy.org/doc/1.18/user/theory.broadcasting.html