2025, Oct 22 12:01

Векторизация скользящего окна в PyTorch: unfold вместо цикла

Как векторизовать скользящее окно для 1D тензоров в PyTorch: используем tensor.unfold и broadcasting вместо torch.roll и циклов. Пример кода и прирост скорости.

Скользящие окна по одномерным тензорам — типичный прием в анализе временных рядов, обработке сигналов и моделировании последовательностей. Простой цикл с torch.roll работает, но теряет в производительности и плохо масштабируется. Цель — посчитать результаты для всех окон за один векторизованный проход и собрать их в 2D‑тензор.

Базовая реализация через цикл

Следующий код обрабатывает 1D‑тензор, каждый раз беря четыре соседних значения, передавая их функции и сдвигая стартовую позицию на один элемент на каждой итерации. Промежуточные результаты складываются в тензор 5×4.

import torch


def apply_block(win: torch.tensor, scale: float) -> torch.tensor:
    return win * scale

win_width = 4
series = torch.tensor([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0])
idx_pick = torch.tensor(list(range(win_width)))

chunks = []
for _ in range(5):
    slice_vals = torch.index_select(series, 0, idx_pick.view(-1))
    out_vec = apply_block(slice_vals, series[0])
    chunks.append(out_vec)
    series = torch.roll(series, -1)

stacked_loop = torch.stack(chunks)

В чем узкое место на самом деле

Сама задача — это классическое «скользящее окно»: на каждом шаге берутся четыре подряд идущих элемента. Если реализовать это на Python‑цикле с повторяющимися индексными операциями, работа уходит из быстрых векторизованных операций с тензорами. Лучше сразу построить двумерное представление всех окон и подать его в ту же функцию. Такое представление можно получить без копирования данных с помощью tensor.unfold — окна будут отображаться как строки матрицы.

Векторизованный подход с видом скользящего окна

Ниже та же операция выполняется за один вызов: строим двумерный «вид скользящих окон» и подаем его в функцию. Масштаб берется из первого элемента каждого окна и растягивается (broadcast) до нужных размеров.

import torch


def apply_block(win: torch.tensor, scale: float) -> torch.tensor:
    return win * scale

win_width = 4
base = torch.tensor([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0])

windows = base.unfold(0, win_width, 1)
vectorized_out = apply_block(windows, windows[:, 0:1])

# Дополнительная сверка с версией на цикле
check_series = torch.tensor([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0])
idx_pick = torch.tensor(list(range(win_width)))
accum = []
for _ in range(5):
    sl = torch.index_select(check_series, 0, idx_pick.view(-1))
    accum.append(apply_block(sl, check_series[0]))
    check_series = torch.roll(check_series, -1)
stacked_loop = torch.stack(accum)

assert torch.allclose(stacked_loop, vectorized_out)

Почему это важно

Заменив циклы на стороне Python одной тензорной операцией, вы сохраняете вычисления внутри оптимизированного конвейера PyTorch. Код становится нагляднее: двумерная матрица окон явно выражает задумку, а распространение масштаба по второй размерности естественно соответствует структуре данных. Это особенно полезно при большом числе окон, когда накладные расходы циклов и повторное индексирование начинают заметно влиять на скорость.

Выводы

Когда нужны последовательные группы элементов тензора, создавайте вид скользящего окна через unfold и применяйте вычисление сразу ко всем окнам. Сохраняйте входы тензорными, чтобы при необходимости использовать torch.index_select для индексирования, а для согласования форм полагайтесь на broadcasting — без лишних копий. Такой подход сохраняет исходную логику и одновременно повышает читаемость и масштабируемость.

Статья основана на вопросе со StackOverflow от FlumeRS и ответе simon.