Есть ли функция, которая может применять правила вещания NumPy к списку фигур и возвращать окончательную форму?

Это не вопрос о том, как работает вещание (т. е. это не дубликат эти вопросы).

Я просто хотел бы найти функцию, которая может применять правила вещания NumPy к списку фигур и возвращать окончательную форму, например:

>>> broadcast_shapes([6], [4, 2, 3, 1], [2, 1, 1])
[4, 2, 3, 6]

Спасибо!


person MiniQuark    schedule 25.02.2019    source источник
comment
np.broadcast_arrays будет транслировать массивы друг против друга. Вы можете принять форму любого из возвращаемых массивов. Он возвращает фактические массивы, но, поскольку они передаются, это не должно быть особенно дорого; все они будут представлениями оригиналов.   -  person hpaulj    schedule 25.02.2019
comment
broadcast_arrays использует np.lib.stride_tricks._broadcast_shape(*args) для получения целевой формы. Использование этого напрямую может быть немного быстрее, но оно помечено как приватное. Он, в свою очередь, использует np.broadcast. Я не использовал ни один из них много, но я подозреваю, что broadcast_arrays является самым простым/безопасным в использовании.   -  person hpaulj    schedule 25.02.2019


Ответы (5)


Вот еще одна прямая реализация, которая превосходит другие на примере. Почетное упоминание принадлежит @hpaulj's с хаком @Warren Weckesser, который почти такой же быстрый и гораздо более лаконичный:

def bs_pp(*shapes):
    ml = max(shapes, key=len)
    out = list(ml)
    for l in shapes:
        if l is ml:
            continue
        for i, x in enumerate(l, -len(l)):
            if x != 1 and x != out[i]:
                if out[i] != 1:
                    raise ValueError
                out[i] = x
    return (*out,)

def bs_mq1(*shapes):
    max_rank = max([len(shape) for shape in shapes])
    shapes = [[1] * (max_rank - len(shape)) + shape for shape in shapes]
    final_shape = [1] * max_rank
    for shape in shapes:
        for dim, size in enumerate(shape):
            if size != 1:
                final_size = final_shape[dim]
                if final_size == 1:
                    final_shape[dim] = size
                elif final_size != size:
                    raise ValueError("Cannot broadcast these shapes")
    return (*final_shape,)

import numpy as np

def bs_mq2(*shapes):
    max_rank = max([len(shape) for shape in shapes])
    shapes = np.array([[1] * (max_rank - len(shape)) + shape
                      for shape in shapes])
    shapes[shapes==1] = -1
    final_shape = shapes.max(axis=0)
    final_shape[final_shape==-1] = 1
    return (*final_shape,)

def bs_hp_ww(*shapes):
    return np.broadcast(*[np.empty(shape + [0,], int) for shape in shapes]).shape[:-1]

L = [6], [4, 2, 3, 1], [2, 1, 1]

from timeit import timeit

print('pp:       ', timeit(lambda: bs_pp(*L), number=10_000)/10)
print('mq 1:     ', timeit(lambda: bs_mq1(*L), number=10_000)/10)
print('mq 2:     ', timeit(lambda: bs_mq2(*L), number=10_000)/10)
print('hpaulj/ww:', timeit(lambda: bs_hp_ww(*L), number=10_000)/10)

assert bs_pp(*L) == bs_mq1(*L) and bs_pp(*L) == bs_mq2(*L) and bs_pp(*L) == bs_hp_ww(*L)

Пример запуска:

pp:        0.0021552839782088993
mq 1:      0.00398325570859015
mq 2:      0.01497043427079916
hpaulj/ww: 0.003267909213900566
person Paul Panzer    schedule 25.02.2019

Я не вижу ничего для этого в документах NumPy. Вы можете передать один 0-мерный массив каждой целевой форме, а затем передать все результаты друг другу:

def broadcast_shapes(*shapes):
    base = numpy.array(0)
    broadcast1 = [numpy.broadcast_to(base, shape) for shape in shapes]
    return numpy.broadcast(*broadcast1).shape

Это позволяет избежать выделения больших объемов памяти для больших фигур. Однако необходимость создавать массивы вообще кажется глупой.

person user2357112 supports Monica    schedule 25.02.2019
comment
Интересно, спасибо @user2357112. Вы уверены, что broadcast_to() и broadcast() на самом деле не выделяют память? Они просто возвращают просмотры? - person MiniQuark; 25.02.2019
comment
@MiniQuark: broadcast_to делает вид. broadcast возвращает широковещательный объект, а не массив, и широковещательный объект также не связан с проблемными выделениями. - person user2357112 supports Monica; 25.02.2019
comment
По крайней мере, на моей машине они просто возвращают просмотры. a = np.broadcast_to(base,[4,2,3,6]) -> a.strides = (0, 0, 0, 0). - person user545424; 25.02.2019
comment
«Хак» @Warren (комментарий к моему ответу) работает быстрее. - person hpaulj; 25.02.2019

Вот простая реализация, на всякий случай, если она кому-то понадобится (это может помочь понять трансляцию). Однако я бы предпочел использовать функцию NumPy.

def broadcast_shapes(*shapes):
    max_rank = max([len(shape) for shape in shapes])
    shapes = [[1] * (max_rank - len(shape)) + shape for shape in shapes]
    final_shape = [1] * max_rank
    for shape in shapes:
        for dim, size in enumerate(shape):
            if size != 1:
                final_size = final_shape[dim]
                if final_size == 1:
                    final_shape[dim] = size
                elif final_size != size:
                    raise ValueError("Cannot broadcast these shapes")
    return final_shape

Изменить

Я сопоставил эту функцию с несколькими другими ответами, и она оказалась самой быстрой (edit, Paul Panzer написал еще более быструю функцию, см. его ответ, я добавил ее в список ниже):

%timeit bs_pp(*shapes) # Peter Panzer's answer
2.33 µs ± 10.2 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

%timeit broadcast_shapes1(*shapes)  # this answer
4.21 µs ± 11.2 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

%timeit broadcast_shapes2(*shapes) # my other answer with shapes.max(axis=0)
12.8 µs ± 67.3 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

%timeit broadcast_shapes3(*shapes) # user2357112's answer
18 µs ± 26.9 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

%timeit broadcast_shapes4(*shapes) # hpaulj's answer
18.1 µs ± 263 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
person MiniQuark    schedule 25.02.2019

In [120]: shapes = [6], [4, 2, 3, 1], [2, 1, 1]                                 
In [121]: arrs = np.broadcast_arrays(*[np.empty(shape,int) for shape in shapes])
     ...:                                                                       
In [122]: [a.shape for a in arrs]                                               
Out[122]: [(4, 2, 3, 6), (4, 2, 3, 6), (4, 2, 3, 6)]

In [124]: np.lib.stride_tricks._broadcast_shape(*[np.empty(shape,int) for shape 
     ...: in shapes])                                                           
Out[124]: (4, 2, 3, 6)

In [131]: np.broadcast(*[np.empty(shape,int) for shape in shapes]).shape        
Out[131]: (4, 2, 3, 6)

Второй раз немного быстрее, 4,79 мкс против 42,4 мкс. Третий немного быстрее.

Когда я впервые прокомментировал, я начал с broadcast_arrays и посмотрел код. То мне до _broadcast_shape, то до np.broadcast.

person hpaulj    schedule 25.02.2019
comment
Спасибо @hpaulj. Разве эти решения не будут выделять оперативную память для массивов empty()? Если я захочу использовать огромные выходные фигуры, это может сломаться, верно? - person MiniQuark; 25.02.2019
comment
Ленивое выделение памяти на уровне ОС, вероятно, смягчает некоторые проблемы с выделением памяти, связанные с этим, но по-прежнему кажется плохой идеей запрашивать всю эту память. - person user2357112 supports Monica; 25.02.2019
comment
Вот хак, чтобы избежать выделения памяти последней опции: np.broadcast(*[np.empty(shape + [0,], int) for shape in shapes]).shape[:-1] - person Warren Weckesser; 25.02.2019
comment
@WarrenWeckesser Умница! И на самом деле довольно быстро кажется. - person Paul Panzer; 25.02.2019

Предполагая, что формы действительно могут транслироваться, это работает:

def broadcast_shapes(*shapes):
    max_rank = max([len(shape) for shape in shapes])
    shapes = np.array([[1] * (max_rank - len(shape)) + shape
                      for shape in shapes])
    shapes[shapes==1] = -1
    final_shape = shapes.max(axis=0)
    final_shape[final_shape==-1] = 1
    return final_shape

Если вы предполагаете, что пустого измерения нет, то -1 хак не нужен:

def broadcast_shapes(*shapes):
    max_rank = max([len(shape) for shape in shapes])
    shapes = np.array([[1] * (max_rank - len(shape)) + shape
                      for shape in shapes])
    return shapes.max(axis=0)
person MiniQuark    schedule 25.02.2019