Обобщённый API универсальных функций#
Существует общая необходимость в циклах не только для функций над скалярами, но и для функций над векторами (или массивами). Эта концепция реализована в NumPy путём обобщения универсальных функций (ufuncs). В обычных ufuncs элементарная функция ограничена поэлементными операциями, тогда как обобщённая версия (gufuncs) поддерживает операции «подмассив» на «подмассив». Библиотека векторов Perl PDL предоставляет аналогичную функциональность, и её термины используются далее.
Каждая обобщенная ufunc имеет связанную с ней информацию, которая указывает, какова «базовая» размерность входных данных, а также соответствующая размерность выходных данных (поэлементные ufunc имеют нулевую базовую размерность). Список базовых размерностей для всех аргументов называется «сигнатурой» ufunc. Например, ufunc numpy.add имеет сигнатуру (),()->() определение двух скалярных входов и одного скалярного выхода.
Другой пример — функция inner1d(a, b) с сигнатурой
(i),(i)->(). Это применяет внутреннее произведение по последней оси каждого ввода, но сохраняет остальные индексы нетронутыми. Например, где a имеет форму (3, 5, N) и b имеет форму
(5, N), это вернет вывод формы (3,5).
Базовая элементарная функция вызывается 3 * 5 раз. В сигнатуре мы указываем одно основное измерение (i) для каждого ввода и нулевых основных измерений () для вывода, поскольку он принимает два одномерных массива и возвращает скаляр. Используя то же имя i, мы указываем, что два
соответствующих измерения должны быть одного размера.
Измерения за пределами основных измерений называются «цикловыми» измерениями. В
приведенном выше примере это соответствует (3, 5).
Сигнатура определяет, как размерности каждого входного/выходного массива разделяются на основные и циклические размерности:
Каждое измерение в сигнатуре сопоставляется с измерением соответствующего переданного массива, начиная с конца кортежа формы. Это основные измерения, и они должны присутствовать в массивах, или будет вызвана ошибка.
Основные измерения, назначенные одной и той же метке в сигнатуре (например, в
iвinner1d’s(i),(i)->()) должны иметь точно совпадающие размеры, трансляция не выполняется.Основные размерности удаляются из всех входных данных, а оставшиеся размерности транслируются вместе, определяя размерности цикла.
Форма каждого вывода определяется из измерений цикла плюс основные измерения вывода
Обычно размер всех основных измерений в выходных данных определяется размером основного измерения с той же меткой во входном массиве. Это не является требованием, и можно определить сигнатуру, где метка впервые появляется в выходных данных, хотя при вызове такой функции необходимо принять некоторые меры предосторожности. Примером может быть функция
euclidean_pdist(a), с сигнатурой (n,d)->(p), который, учитывая массив
n d-мерные векторы, вычисляет все уникальные попарные евклидовы расстояния между ними. Выходная размерность p следовательно, должно быть равно
n * (n - 1) / 2, но по умолчанию ответственность за передачу выходного массива правильного размера лежит на вызывающей стороне. Если размер основного измерения выхода не может быть определён из переданного входного или выходного массива, будет вызвана ошибка. Это можно изменить, определив PyUFunc_ProcessCoreDimsFunc функции
и присвоения ее proces_core_dims_func поле PyUFuncObject
структуру. Подробнее см. ниже.
Примечание: до NumPy 1.10.0 применялись менее строгие проверки: недостающие основные размерности создавались путём добавления единиц к форме по мере необходимости, основные размерности с одинаковыми метками транслировались вместе, а неопределённые размерности создавались с размером 1.
Определения#
- Элементарная функция
Каждый ufunc состоит из элементарной функции, которая выполняет самую базовую операцию с наименьшей частью аргументов массива (например, сложение двух чисел — это самая базовая операция при сложении двух массивов). Ufunc применяет элементарную функцию несколько раз к разным частям массивов. Входные/выходные данные элементарных функций могут быть векторами; например, элементарная функция
inner1dпринимает два вектора на вход.- Сигнатура
Сигнатура — это строка, описывающая размерности ввода/вывода элементарной функции ufunc. Подробнее см. в разделе ниже.
- Основное измерение
Размерность каждого входа/выхода элементарной функции определяется её основными размерностями (ноль основных размерностей соответствует скалярному входу/выходу). Основные размерности сопоставляются с последними размерностями массивов входа/выхода.
- Имя измерения
Имя измерения представляет собой основное измерение в сигнатуре. Разные измерения могут иметь одно имя, указывая, что они имеют одинаковый размер.
- Индекс измерения
Индекс измерения — это целое число, представляющее имя измерения. Он перечисляет имена измерений в порядке первого появления каждого имени в сигнатуре.
Подробности сигнатуры#
Сигнатура определяет «основную» размерность входных и выходных переменных и, таким образом, также определяет свертку измерений. Сигнатура представлена строкой следующего формата:
Основные размерности каждого входного или выходного массива представлены списком имен размерностей в скобках,
(i_1,...,i_N); скалярный ввод/вывод обозначается(). Вместоi_1,i_2, и т.д., можно использовать любое допустимое имя переменной Python.Списки размерностей для разных аргументов разделяются
",". Аргументы ввода/вывода разделены"->".Если используется одно и то же имя измерения в нескольких местах, это принудительно устанавливает одинаковый размер соответствующих измерений.
Формальный синтаксис сигнатур выглядит следующим образом:
<Signature> ::= <Input arguments> "->" <Output arguments>
<Input arguments> ::= <Argument list>
<Output arguments> ::= <Argument list>
<Argument list> ::= nil | <Argument> | <Argument> "," <Argument list>
<Argument> ::= "(" <Core dimension list> ")"
<Core dimension list> ::= nil | <Core dimension> |
<Core dimension> "," <Core dimension list>
<Core dimension> ::= <Dimension name> <Dimension modifier>
<Dimension name> ::= valid Python variable name | valid integer
<Dimension modifier> ::= nil | "?"
Примечания:
Все кавычки приведены для ясности.
Неизмененные основные размерности с одинаковыми именами должны иметь одинаковый размер. Каждое имя размерности обычно соответствует одному уровню цикла в реализации элементарной функции.
Пробелы игнорируются.
Целое число в качестве имени измерения фиксирует это измерение на значении.
Если имя имеет суффикс модификатора «?», измерение является основным только если оно существует во всех входах и выходах, которые его используют; в противном случае оно игнорируется (и заменяется измерением размера 1 для элементарной функции).
Вот несколько примеров сигнатур:
имя |
сигнатура |
обычное использование |
|---|---|---|
добавить |
|
бинарная универсальная функция |
sum1d |
|
reduction |
inner1d |
|
вектор-векторное умножение |
matmat |
|
умножение матриц |
vecmat |
|
умножение вектор-матрица |
matvec |
|
умножение матрицы на вектор |
matmul |
|
комбинация четырех вышеперечисленных |
outer_inner |
|
внутреннее произведение по последнему измерению, внешнее произведение по предпоследнему, и цикл/вещание по остальным. |
cross1d |
|
векторное произведение, где последнее измерение фиксировано и должно быть равно 3 |
Последний является примером фиксации основного измерения и может использоваться для повышения производительности ufunc.
C-API для реализации элементарных функций#
Текущий интерфейс остается неизменным, и PyUFunc_FromFuncAndData
всё ещё может использоваться для реализации (специализированных) универсальных функций, состоящих из
скалярных элементарных функций.
Можно использовать PyUFunc_FromFuncAndDataAndSignature для объявления более
общего ufunc. Список аргументов такой же, как
PyUFunc_FromFuncAndData, с дополнительным аргументом, указывающим
сигнатуру как строку C.
Кроме того, функция обратного вызова имеет тот же тип, что и раньше,
void (*foo)(char **args, intp *dimensions, intp *steps, void *func). При вызове, args является списком длины nargs содержащий
данные всех входных/выходных аргументов. Для скалярной элементарной
функции steps также имеет длину nargs, обозначающие шаги (strides), используемые
для аргументов. dimensions является указателем на целое число, определяющее размер оси для перебора.
Для нетривиальной сигнатуры, dimensions также будет содержать размеры
основных измерений, начиная со второй записи. Только
один размер предоставляется для каждого уникального имени измерения, и размеры
указываются в соответствии с первым появлением имени измерения в
сигнатуре.
Первый nargs элементы steps остаются такими же, как для скалярных
универсальных функций. Следующие элементы содержат шаги всех основных
измерений для всех аргументов по порядку.
Например, рассмотрим ufunc с сигнатурой (i,j),(i)->(). В этом случае, args будет содержать три указателя на данные
входных/выходных массивов a, b, c. Более того, dimensions будет
[N, I, J] для определения размера N цикла и размеров I и J
для основных размерностей i и j. Наконец, steps будет
[a_N, b_N, c_N, a_i, a_j, b_i], содержащий все необходимые шаги.
Настройка обработки размера основных измерений#
Необязательная функция типа PyUFunc_ProcessCoreDimsFunc, хранится
на process_core_dims_func атрибут ufunc, предоставляет
автору ufunc «хук» в обработку основных размерностей
массивов, переданных в ufunc. Два основных использования
этого «хука»:
Проверяет, что ограничения на основные размеры, требуемые универсальной функцией, удовлетворены (и устанавливает исключение, если нет).
Вычислить выходные формы для любых выходных основных измерений, которые не были определены входными массивами.
В качестве примера первого использования рассмотрим обобщенную универсальную функцию minmax
с сигнатурой (n)->(2) который одновременно вычисляет минимум и максимум последовательности. Он должен требовать, чтобы n > 0, потому что минимальное и максимальное значение последовательности длиной 0 не имеет смысла. В этом случае автор ufunc может определить функцию следующим образом:
int minmax_process_core_dims(PyUFuncObject *ufunc, npy_intp *core_dim_sizes) { npy_intp n = core_dim_sizes[0]; if (n == 0) { PyErr_SetString(PyExc_ValueError, "minmax requires the core dimension to " "be at least 1."); return -1; } return 0; }
В этом случае длина массива core_dim_sizes будет равно 2.
Второе значение в массиве всегда будет равно 2, поэтому функции нет необходимости проверять его. Основное измерение n хранится в первом элементе. Функция устанавливает исключение и возвращает -1, если обнаруживает, что n равен 0.
Второе использование "хука" — вычисление размера выходных массивов,
когда выходные массивы не предоставляются вызывающей стороной и одно или несколько
основных измерений выхода не являются также входными основными измерениями.
Если у универсальной функции нет определенной функции на
process_core_dims_func атрибуте, неопределённый размер основной размерности вывода приведёт к возникновению исключения. С помощью "hook", предоставленного process_core_dims_func, автор ufunc может установить размер вывода таким, какой подходит для ufunc.
В массиве, переданном в функцию "hook", основные размерности, которые не были определены входными данными, обозначаются значением -1 в core_dim_sizes массива. Функция может заменить -1 на любое подходящее значение для ufunc, основываясь на основных размерностях, которые встречаются во входных массивах.
Предупреждение
Функция никогда не должна изменять значение в core_dim_sizes который не равен -1 на входе. Изменение значения, которое не было -1, обычно приводит к некорректному выводу из ufunc и может вызвать аварийное завершение интерпретатора Python.
Например, рассмотрим обобщенную универсальную функцию conv1d для которого
элементарная функция вычисляет «полную» свертку двух
одномерных массивов x и y с длинами m и n,
соответственно. Результат этой свёртки имеет длину m + n - 1.
Для реализации этого в виде обобщенной ufunc сигнатура устанавливается в
(m),(n)->(p), и в «хук»-функции, если основное измерение
p обнаруживается как -1, он заменяется на m + n - 1. Если p
является не -1, необходимо убедиться, что заданное значение равно m + n - 1.
Если это не так, функция должна установить исключение и вернуть -1.
Для значимого результата операция также требует, чтобы m + n
составляет не менее 1, т.е. оба входа не могут иметь длину 0.
Вот как это может выглядеть в коде:
int conv1d_process_core_dims(PyUFuncObject *ufunc, npy_intp *core_dim_sizes) { // core_dim_sizes will hold the core dimensions [m, n, p]. // p will be -1 if the caller did not provide the out argument. npy_intp m = core_dim_sizes[0]; npy_intp n = core_dim_sizes[1]; npy_intp p = core_dim_sizes[2]; npy_intp required_p = m + n - 1; if (m == 0 && n == 0) { // Disallow both inputs having length 0. PyErr_SetString(PyExc_ValueError, "conv1d: both inputs have core dimension 0; the function " "requires that at least one input has size greater than 0."); return -1; } if (p == -1) { // Output array was not given in the call of the ufunc. // Set the correct output size here. core_dim_sizes[2] = required_p; return 0; } // An output array *was* given. Validate its core dimension. if (p != required_p) { PyErr_Format(PyExc_ValueError, "conv1d: the core dimension p of the out parameter " "does not equal m + n - 1, where m and n are the " "core dimensions of the inputs x and y; got m=%zd " "and n=%zd so p must be %zd, but got p=%zd.", m, n, required_p, p); return -1; } return 0; }