.. _sec_attention-pooling: Агрегирование по вниманию на основе сходства ============================================ Теперь, когда мы рассмотрели основные компоненты механизма внимания, применим их в классическом контексте, а именно в регрессии и классификации с помощью оценки плотности на основе ядер :cite:`Nadaraya.1964,Watson.1964`. Это отступление служит лишь для предоставления дополнительной информации: оно полностью необязательно и при необходимости может быть пропущено. В основе оценок Надарая—Ватсона лежит ядро сходства :math:`\alpha(\mathbf{q}, \mathbf{k})`, связывающее запросы :math:`\mathbf{q}` с ключами :math:`\mathbf{k}`. Некоторые распространённые ядра включают .. math:: \begin{aligned} \alpha(\mathbf{q}, \mathbf{k}) & = \exp\left(-\frac{1}{2} \|\mathbf{q} - \mathbf{k}\|^2 \right) && \textrm{Gaussian;} \\ \alpha(\mathbf{q}, \mathbf{k}) & = 1 \textrm{ if } \|\mathbf{q} - \mathbf{k}\| \leq 1 && \textrm{Boxcar;} \\ \alpha(\mathbf{q}, \mathbf{k}) & = \mathop{\mathrm{max}}\left(0, 1 - \|\mathbf{q} - \mathbf{k}\|\right) && \textrm{Epanechikov.} \end{aligned} Существует множество других вариантов. Более подробный обзор и связь выбора ядер с оценкой плотности по методу ядер (иногда называемой *окнами Парзена*) :cite:`parzen1957consistent` можно найти в [статье в Википедии](https://en.wikipedia.org/wiki/Kernel\_(statistics). Все ядра являются эвристическими и могут быть настроены. Например, мы можем регулировать ширину не только глобально, но и для каждой координаты в отдельности. Тем не менее, все они приводят к следующему уравнению, которое применимо как для регрессии, так и для классификации: .. math:: f(\mathbf{q}) = \sum_i \mathbf{v}_i \frac{\alpha(\mathbf{q}, \mathbf{k}_i)}{\sum_j \alpha(\mathbf{q}, \mathbf{k}_j)}. В случае (скалярной) регрессии с наблюдениями :math:`(\mathbf{x}_i, y_i)` для признаков и меток соответственно, :math:`\mathbf{v}_i = y_i` являются скалярами, :math:`\mathbf{k}_i = \mathbf{x}_i` — векторами, а запрос :math:`\mathbf{q}` обозначает новую точку, в которой следует вычислить значение :math:`f`. В случае (многоклассовой) классификации мы используем кодирование меток :math:`y_i` в формате one-hot, чтобы получить :math:`\mathbf{v}_i`. Одним из удобных свойств этого оценщика является то, что он не требует обучения. Более того, если мы соответствующим образом сужаем ядро по мере увеличения объёма данных, подход является состоятельным :cite:`mack1982weak`, то есть он сходится к некоторому статистически оптимальному решению. Начнем с рассмотрения некоторых ядер. .. raw:: latex \diilbookstyleinputcell .. code:: python import numpy as np import torch from torch import nn from torch.nn import functional as F from d2l import torch as d2l d2l.use_svg_display() Ядра и данные ------------- Все ядра :math:`\alpha(\mathbf{k}, \mathbf{q})`, определённые в этом разделе, инвариантны относительно сдвига и поворота; то есть, если мы сдвигаем и поворачиваем :math:`\mathbf{k}` и :math:`\mathbf{q}` одинаковым образом, значение :math:`\alpha` остаётся неизменным. Для простоты мы выбираем скалярные аргументы :math:`k, q \in \mathbb{R}` и принимаем ключ :math:`k = 0` за начало координат. Это даёт: .. raw:: latex \diilbookstyleinputcell .. code:: python # Define some kernels def gaussian(x): return torch.exp(-x**2 / 2) def boxcar(x): return torch.abs(x) < 1.0 def constant(x): return 1.0 + 0 * x def epanechikov(x): return torch.max(1 - torch.abs(x), torch.zeros_like(x)) .. raw:: latex \diilbookstyleinputcell .. code:: python fig, axes = d2l.plt.subplots(1, 4, sharey=True, figsize=(12, 3)) kernels = (gaussian, boxcar, constant, epanechikov) names = ('Gaussian', 'Boxcar', 'Constant', 'Epanechikov') x = torch.arange(-2.5, 2.5, 0.1) for kernel, name, ax in zip(kernels, names, axes): ax.plot(x.detach().numpy(), kernel(x).detach().numpy()) ax.set_xlabel(name) d2l.plt.show() Различные ядра соответствуют разным представлениям о диапазоне и гладкости. Например, прямоугольное ядро учитывает только наблюдения, находящиеся на расстоянии :math:`1` (или некоторого другого заданного гиперпараметра), и делает это без различий. Чтобы увидеть оценку Надарая–Ватсона в действии, определим некоторые обучающие данные. В дальнейшем мы будем использовать зависимость .. math:: y_i = 2\sin(x_i) + x_i + \epsilon, где :math:`\epsilon` берётся из нормального распределения с нулевым средним и единичной дисперсией. Мы генерируем 40 обучающих примеров. .. raw:: latex \diilbookstyleinputcell .. code:: python def f(x): return 2 * torch.sin(x) + x n = 40 x_train, _ = torch.sort(torch.rand(n) * 5) y_train = f(x_train) + torch.randn(n) x_val = torch.arange(0, 5, 0.1) y_val = f(x_val) Агрегирование по вниманию через регрессию Надарая–Ватсона --------------------------------------------------------- Теперь, когда у нас есть данные и ядра, нам нужна лишь функция, вычисляющая оценки регрессии по методу Надарая–Ватсона. Обратите внимание, что мы также хотим получить относительные веса ядра для проведения некоторых дополнительных проверок. Следовательно, сначала мы вычисляем ядро между всеми обучающими признаками (ковариатами) ``x_train`` и всеми валидационными признаками ``x_val``. Это даёт матрицу, которую мы затем нормируем. При умножении на обучающие метки ``y_train`` мы получаем оценки. Вспомним агрегирование по вниманию из :eq:`eq_attention_pooling`. Пусть каждый валидационный признак будет запросом, а каждая пара «признак–метка» обучающей выборки — парой «ключ–значение». В результате нормализованные относительные веса ядра (``attention_w`` ниже) становятся *весами внимания*. .. raw:: latex \diilbookstyleinputcell .. code:: python def nadaraya_watson(x_train, y_train, x_val, kernel): dists = x_train.reshape((-1, 1)) - x_val.reshape((1, -1)) # Each column/row corresponds to each query/key k = kernel(dists).type(torch.float32) # Normalization over keys for each query attention_w = k / k.sum(0) y_hat = y_train@attention_w return y_hat, attention_w Рассмотрим оценки, которые дают различные ядра. .. raw:: latex \diilbookstyleinputcell .. code:: python def plot(x_train, y_train, x_val, y_val, kernels, names, attention=False): fig, axes = d2l.plt.subplots(1, 4, sharey=True, figsize=(12, 3)) for kernel, name, ax in zip(kernels, names, axes): y_hat, attention_w = nadaraya_watson(x_train, y_train, x_val, kernel) if attention: pcm = ax.imshow(attention_w.detach().numpy(), cmap='Reds') else: ax.plot(x_val, y_hat) ax.plot(x_val, y_val, 'm--') ax.plot(x_train, y_train, 'o', alpha=0.5); ax.set_xlabel(name) if not attention: ax.legend(['y_hat', 'y']) if attention: fig.colorbar(pcm, ax=axes, shrink=0.7) .. raw:: latex \diilbookstyleinputcell .. code:: python plot(x_train, y_train, x_val, y_val, kernels, names) Прежде всего бросается в глаза, что все три нетривиальных ядра (Гауссово, прямоугольное и Епанечникова) дают вполне приёмлемые оценки, не слишком сильно отклоняющиеся от истинной функции. Лишь постоянное ядро, приводящее к тривиальной оценке :math:`f(x) = \frac{1}{n} \sum_i y_i`, даёт весьма нереалистичный результат. Рассмотрим веса внимания чуть подробнее: .. raw:: latex \diilbookstyleinputcell .. code:: python plot(x_train, y_train, x_val, y_val, kernels, names, attention=True) Наглядная визуализация чётко показывает, почему оценки для гауссова, прямоугольного и эпанечникова ядер столь похожи: ведь они получены на основе весьма схожих весов внимания, несмотря на различия в функциональной форме ядра. Это наводит на вопрос, всегда ли это так. Адаптация агрегирования по вниманию ----------------------------------- Мы можем заменить гауссово ядро на ядро другой ширины. То есть мы можем использовать :math:`\alpha(\mathbf{q}, \mathbf{k}) = \exp\left(-\frac{1}{2 \sigma^2} \|\mathbf{q} - \mathbf{k}\|^2 \right)`, где :math:`\sigma^2` определяет ширину ядра. Давайте посмотрим, влияет ли это на результаты. .. raw:: latex \diilbookstyleinputcell .. code:: python sigmas = (0.1, 0.2, 0.5, 1) names = ['Sigma ' + str(sigma) for sigma in sigmas] def gaussian_with_width(sigma): return (lambda x: torch.exp(-x**2 / (2*sigma**2))) kernels = [gaussian_with_width(sigma) for sigma in sigmas] plot(x_train, y_train, x_val, y_val, kernels, names) Очевидно, чем уже ядро, тем менее гладкой получается оценка. В то же время она лучше адаптируется к локальным вариациям. Рассмотрим соответствующие веса внимания. .. raw:: latex \diilbookstyleinputcell .. code:: python plot(x_train, y_train, x_val, y_val, kernels, names, attention=True) Как и следовало ожидать, чем уже ядро, тем уже диапазон весов внимания, принимающих большие значения. Также очевидно, что выбор одинаковой ширины может быть неидеальным. На самом деле, :cite:t:`Silverman86` предложили эвристический метод, зависящий от локальной плотности. Было предложено гораздо больше подобных «трюков». Например, :cite:t:`norelli2022asif` использовали похожую технику интерполяции на основе ближайших соседей для проектирования кросс-модальных представлений изображений и текста. Внимательный читатель может задаться вопросом, зачем мы посвящаем этот углубленный разбор методу, которому уже более полувека. Во-первых, это один из самых ранних прообразов современных механизмов внимания. Во-вторых, он отлично подходит для визуализации. В-третьих, и это не менее важно, он демонстрирует ограничения механизмов внимания, сконструированных вручную. Гораздо более эффективная стратегия — *научиться* этому механизму, обучая представления для запросов и ключей. Именно этим мы займемся в следующих разделах. Итоги ----- Регрессия Надарая–Ватсона с использованием ядра является ранним прообразом современных механизмов внимания. Её можно применять непосредственно, практически без этапа обучения или тонкой настройки, как для задач классификации, так и для регрессии. Вес внимания присваивается в зависимости от сходства (или расстояния) между запросом и ключом, а также от количества доступных похожих наблюдений. Упражнения ---------- 1. Оценки плотности методом окон Парзена задаются формулой :math:`\hat{p}(\mathbf{x}) = \frac{1}{n} \sum_i k(\mathbf{x}, \mathbf{x}_i)`. Докажите, что для задачи бинарной классификации функция :math:`\hat{p}(\mathbf{x}, y=1) - \hat{p}(\mathbf{x}, y=-1)`, полученная методом окон Парзена, эквивалентна классификации Надарая—Ватсона. 2. Реализуйте стохастический градиентный спуск для подбора оптимального значения ширины ядра в регрессии Надарая–Ватсона. 1. Что произойдет, если вы просто используете приведённые выше оценки для прямой минимизации :math:`(f(\mathbf{x_i}) - y_i)^2`? Подсказка: :math:`y_i` входит в состав выражений, используемых для вычисления :math:`f`. 2. Удалите :math:`(\mathbf{x}_i, y_i)` из оценки для :math:`f(\mathbf{x}_i)` и выполните оптимизацию по ширинам ядер. Наблюдается ли при этом переобучение? 3. Предположим, что все :math:`\mathbf{x}` лежат на единичной сфере, то есть удовлетворяют условию :math:`\|\mathbf{x}\| = 1`. Можете ли вы упростить член :math:`\|\mathbf{x} - \mathbf{x}_i\|^2` в экспоненте? Подсказка: позже мы увидим, что это тесно связано с вниманием на основе скалярного произведения. 4. Напомним, что в :cite:t:`mack1982weak` было доказано, что оценка Надарая — Ватсона является состоятельной. Как быстро следует уменьшать масштаб механизма внимания по мере увеличения объёма данных? Приведите интуитивное обоснование вашего ответа. Зависит ли это от размерности данных? Если да, то как именно? `Обсуждения `__