2026, Jan 05 15:04
Схожесть массивов по L1 в NumPy: векторизация без циклов
Как посчитать схожесть по минимальным L1-расстояниям между строками массивов в NumPy без циклов: бродкастинг и осевые свертки в одном векторизованном выражении.
Векторизация вычисления схожести для массивов с разным числом строк — частый прием в конвейерах обработки данных. Задача проста: для каждой строки одного массива найти минимальное L1-расстояние до любой строки другого массива и сложить эти минимумы в единый показатель. Прямолинейный способ работает, но опирается на циклы Python. Есть аккуратное, «чисто NumPy» решение, которое убирает накладные расходы Python и заметно ускоряет вычисления.
Пример: базовая реализация
Код ниже вычисляет метрику схожести между двумя NumPy-массивами разных размеров. Для каждой строки второго массива он считает L1-расстояния до всех строк первого, выбирает минимальное и в конце суммирует эти минимумы.
import numpy as np
from numpy.linalg import norm as l1norm
x_mat = np.array([(1, 2, 3), (1, 4, 9), (2, 4, 4)])
y_mat = np.array([(1, 3, 3), (1, 5, 9)])
score = sum([min(l1norm(x_mat - row_vec, ord=1, axis=1)) for row_vec in y_mat])
Что происходит под капотом
Логика прозрачна и последовательна. Вычитание одной строки из первого массива использует бродкастинг и дает построчные разности; L1-норма с axis=1 сворачивает их в расстояния, а минимум выбирает ближайшую строку. Запуск этого процесса внутри спискового включения в Python повторяет шаги для каждой строки второго массива и затем агрегирует результат через сумму. Единственный минус — явный цикл на стороне Python, который ограничивает эффективность.
Выражение только на NumPy
Цикл можно полностью убрать, построив матрицу попарных расстояний за счет бродкастинга и свернув ее по нужным осям. Математика остается той же, а вся работа переносится в векторизованные операции.
import numpy as np
from numpy.linalg import norm as l1norm
x_mat = np.array([(1, 2, 3), (1, 4, 9), (2, 4, 4)])
y_mat = np.array([(1, 3, 3), (1, 5, 9)])
score_opt = l1norm(x_mat[:, None, :] - y_mat[None], ord=1, axis=2).min(axis=0).sum()
Срезы x_mat[:, None, :] и y_mat[None] добавляют одиночные измерения, благодаря чему вычитание по бродкастингу формирует трехмерный массив попарных разностей. L1-норма с axis=2 сворачивает последнее измерение в расстояния, и получается матрица, где каждый столбец соответствует одной строке второго массива. Затем min(axis=0) выбирает минимальное расстояние по столбцам, напрямую повторяя логику поиска лучшего соответствия для каждой строки. Финальная сумма агрегирует эти минимумы в тот же скалярный показатель. Явное min(axis=0) лучше передает намерение; это эквивалентно min(0).
Почему это важно
Опираясь на API NumPy для операций над массивами целиком, мы снижаем накладные расходы Python и используем оптимизированные векторизованные ядра. На практике такой подход заметно быстрее циклической версии и даже немного опережает прямую перепись на Numba для той же задачи.
Вывод
Когда нужен показатель схожести на основе минимальных L1-расстояний между строками двух массивов разного размера, сочетание бродкастинга и осевых сверток — прямое и удобное решение. Преобразуйте массивы в тензор попарных разностей, вычислите норму по оси признаков, возьмите минимумы по столбцам и сложите. Для читаемости явно указывайте параметр axis и избегайте возвращения к циклам Python там, где NumPy позволяет выразить весь конвейер одной понятной формулой.