Масштабирование массивов в Numpy
Давайте рассмотрим более продвинутую концепцию в numpy под названием масштабирование (broadcasting). Термин масштабирование описывает, как 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
Когда хвостовые измерения массивов не равны, масштабирование не выполняется, потому что невозможно согласовать значения строк первого массива с элементами второго массива для поэлементного сложения.
Масштабирование предоставляет удобный способ вычисления внешнего произведения (или любой другой внешней операции) двух массивов. Следующий пример демонстрирует внешнюю операцию сложения двух одномерных массивов, которая даёт тот же результат, что и пример 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–2022 NumPy Developers
Licensed under the 3-clause BSD License.
https://numpy.org/doc/1.21/user/theory.broadcasting.html