.. _sec_attention-scoring-functions: Функции оценки внимания ======================= В :numref:`sec_attention-pooling` мы использовали ряд различных ядер, основанных на расстоянии, включая гауссово ядро для моделирования взаимодействий между запросами и ключами. Как выяснилось, функции расстояния вычисляются несколько дороже, чем скалярные произведения. Поэтому, учитывая операцию софтмакс, обеспечивающую неотрицательность весов внимания, значительная часть усилий была направлена на разработку *функций оценки внимания* :math:`a` в :eq:`eq_softmax_attention` и :numref:`fig_attention_output`, которые проще вычислять. .. _fig_attention_output: .. figure:: ../img/attention-output.svg Вычисление результата агрегирования по вниманию как взвешенного среднего значений, где веса вычисляются с помощью функции оценки внимания :math:`\mathit{a}` и операции софтмакс. .. raw:: latex \diilbookstyleinputcell .. code:: python import math import torch from torch import nn from d2l import torch as d2l Внимание на основе скалярного произведения ------------------------------------------ Давайте на мгновение рассмотрим функцию внимания (без экспоненцирования) из гауссова ядра: .. math:: a(\mathbf{q}, \mathbf{k}_i) = -\frac{1}{2} \|\mathbf{q} - \mathbf{k}_i\|^2 = \mathbf{q}^\top \mathbf{k}_i -\frac{1}{2} \|\mathbf{k}_i\|^2 -\frac{1}{2} \|\mathbf{q}\|^2. Прежде всего отметим, что последний член зависит только от :math:`\mathbf{q}`. Следовательно, он одинаков для всех пар :math:`(\mathbf{q}, \mathbf{k}_i)`. Нормализация весов внимания до :math:`1`, как это делается в :eq:`eq_softmax_attention`, гарантирует, что этот член полностью исчезает. Во-вторых, отметим, что как пакетная, так и нормализация слоя (о которых речь пойдет позже) приводят к активациям с хорошо ограниченными и часто постоянными нормами :math:`\|\mathbf{k}_i\|`. Это имеет место, например, когда ключи :math:`\mathbf{k}_i` были получены с помощью нормализации слоя. Таким образом, мы можем опустить его из определения :math:`a` без существенного изменения результата. Наконец, необходимо контролировать порядок величины аргументов экспоненциальной функции. Предположим, что все элементы запроса :math:`\mathbf{q} \in \mathbb{R}^d` и ключа :math:`\mathbf{k}_i \in \mathbb{R}^d` являются независимыми и одинаково распределенными случайными величинами с нулевым средним и единичной дисперсией. Скалярное произведение этих векторов имеет нулевое среднее и дисперсию :math:`d`. Чтобы дисперсия скалярного произведения оставалась равной :math:`1` независимо от длины вектора, мы используем функцию оценки *масштабированного скалярного произведения внимания* (scaled dot product attention). То есть мы нормируем скалярное произведение на :math:`1/\sqrt{d}`. Таким образом, мы получаем первую широко используемую функцию внимания, которая применяется, например, в трансформерах :cite:`Vaswani.Shazeer.Parmar.ea.2017`: .. math:: a(\mathbf{q}, \mathbf{k}_i) = \mathbf{q}^\top \mathbf{k}_i / \sqrt{d}. :label: eq_dot_product_attention Обратите внимание, что веса внимания :math:`\alpha` всё ещё требуют нормализации. Мы можем упростить это ещё больше, используя операцию софтмакс :eq:`eq_softmax_attention`: .. math:: \alpha(\mathbf{q}, \mathbf{k}_i) = \mathrm{softmax}(a(\mathbf{q}, \mathbf{k}_i)) = \frac{\exp(\mathbf{q}^\top \mathbf{k}_i / \sqrt{d})}{\sum_{j=1} \exp(\mathbf{q}^\top \mathbf{k}_j / \sqrt{d})}. :label: eq_attn-scoring-alpha Как выясняется, все популярные механизмы внимания используют софтмакс, поэтому в оставшейся части этой главы мы ограничимся именно им. Вспомогательные функции ----------------------- Для эффективного развертывания механизма внимания нам потребуется несколько вспомогательных функций. В их число входят инструменты для работы со строками переменной длины (что типично для обработки естественного языка) и средства эффективного вычисления на мини-пакетах (пакетное матричное умножение). Маскированная операция софтмакс ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Одним из наиболее популярных применений механизма внимания являются модели последовательностей. Следовательно, нам необходимо уметь обрабатывать последовательности разной длины. В некоторых случаях такие последовательности могут оказаться в одном мини-батче, что потребует дополнения более коротких последовательностей фиктивными токенами (см. :numref:`sec_machine_translation` для примера). Эти специальные токены не несут смысловой нагрузки. Например, предположим, что у нас есть следующие три предложения: :: Dive into Deep Learning Learn to code Hello world Поскольку мы не хотим, чтобы в нашей модели внимания присутствовали пустые места, нам просто нужно ограничить :math:`\sum_{i=1}^n \alpha(\mathbf{q}, \mathbf{k}_i) \mathbf{v}_i` до :math:`\sum_{i=1}^l \alpha(\mathbf{q}, \mathbf{k}_i) \mathbf{v}_i` на протяжении всей длины :math:`l \leq n`, равной длине самого предложения. Поскольку это очень распространённая проблема, у неё есть своё название: *маскированная операция софтмакс*. Давайте реализуем это. На самом деле реализация немного «хитрит», устанавливая значения :math:`\mathbf{v}_i` при :math:`i > l` равными нулю. Кроме того, она присваивает весам внимания большое отрицательное число, например :math:`-10^{6}`, чтобы на практике их вклад в градиенты и значения становился пренебрежимо малым. Это делается потому, что ядра и операторы линейной алгебры сильно оптимизированы для графических процессоров, и вычислительно выгоднее допустить небольшую расточительность, чем использовать код с условными операторами (if-then-else). .. raw:: latex \diilbookstyleinputcell .. code:: python def masked_softmax(X, valid_lens): #@save """Perform softmax operation by masking elements on the last axis.""" # X: 3D tensor, valid_lens: 1D or 2D tensor def _sequence_mask(X, valid_len, value=0): maxlen = X.size(1) mask = torch.arange((maxlen), dtype=torch.float32, device=X.device)[None, :] < valid_len[:, None] X[~mask] = value return X if valid_lens is None: return nn.functional.softmax(X, dim=-1) else: shape = X.shape if valid_lens.dim() == 1: valid_lens = torch.repeat_interleave(valid_lens, shape[1]) else: valid_lens = valid_lens.reshape(-1) # On the last axis, replace masked elements with a very large negative # value, whose exponentiation outputs 0 X = _sequence_mask(X.reshape(-1, shape[-1]), valid_lens, value=-1e6) return nn.functional.softmax(X.reshape(shape), dim=-1) Чтобы проиллюстрировать, как работает эта функция, рассмотрим мини-батч из двух примеров размера :math:`2 \times 4`, у которых допустимые длины равны :math:`2` и :math:`3` соответственно. В результате применения операции маскированного софтмакса значения, выходящие за пределы допустимых длин для каждой пары векторов, заменяются нулями. .. raw:: latex \diilbookstyleinputcell .. code:: python masked_softmax(torch.rand(2, 2, 4), torch.tensor([2, 3])) Если нам требуется более тонкий контроль для указания допустимой длины каждого из двух векторов в каждом примере, мы просто используем двумерный тензор допустимых длин. Это даёт: .. raw:: latex \diilbookstyleinputcell .. code:: python masked_softmax(torch.rand(2, 2, 4), torch.tensor([[1, 3], [2, 4]])) .. _subsec_batch_dot: Пакетное матричное умножение ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Другой часто используемой операцией является поэлементное умножение батчей матриц. Это удобно, когда у нас есть мини-батчи запросов, ключей и значений. Более конкретно, предположим, что .. math:: \mathbf{Q} = [\mathbf{Q}_1, \mathbf{Q}_2, \ldots, \mathbf{Q}_n] \in \mathbb{R}^{n \times a \times b}, \\ \mathbf{K} = [\mathbf{K}_1, \mathbf{K}_2, \ldots, \mathbf{K}_n] \in \mathbb{R}^{n \times b \times c}. Тогда пакетное матричное умножение (BMM) вычисляет поэлементное произведение .. math:: \textrm{BMM}(\mathbf{Q}, \mathbf{K}) = [\mathbf{Q}_1 \mathbf{K}_1, \mathbf{Q}_2 \mathbf{K}_2, \ldots, \mathbf{Q}_n \mathbf{K}_n] \in \mathbb{R}^{n \times a \times c}. :label: eq_batch-matrix-mul Давайте посмотрим, как это работает в фреймворке глубокого обучения. .. raw:: latex \diilbookstyleinputcell .. code:: python Q = torch.ones((2, 3, 4)) K = torch.ones((2, 4, 6)) d2l.check_shape(torch.bmm(Q, K), (2, 3, 6)) Внимание с масштабированным скалярным произведением --------------------------------------------------- Вернёмся к механизму внимания на основе скалярного произведения, описанному в :eq:`eq_dot_product_attention`. Как правило, он требует, чтобы векторы запроса и ключа имели одинаковую длину, обозначим её как :math:`d`. Хотя эту проблему можно легко решить, заменив :math:`\mathbf{q}^\top \mathbf{k}` на :math:`\mathbf{q}^\top \mathbf{M} \mathbf{k}`, где :math:`\mathbf{M}` — это матрица, специально подобранная для преобразования между обоими пространствами. Пока будем считать, что размерности совпадают. На практике для повышения эффективности мы часто работаем с мини-пакетами, например, вычисляя внимание для :math:`n` запросов и :math:`m` пар ключ-значение, где запросы и ключи имеют длину :math:`d`, а значения — длину :math:`v`. Масштабированное внимание на основе скалярного произведения для запросов :math:`\mathbf Q\in\mathbb R^{n\times d}`, ключей :math:`\mathbf K\in\mathbb R^{m\times d}` и значений :math:`\mathbf V\in\mathbb R^{m\times v}` можно записать как .. math:: \mathrm{softmax}\left(\frac{\mathbf Q \mathbf K^\top }{\sqrt{d}}\right) \mathbf V \in \mathbb{R}^{n\times v}. :label: eq_softmax_QK_V Обратите внимание, что при применении этого к мини-батчу необходимо использовать матричное умножение для батчей, введенное в :eq:`eq_batch-matrix-mul`. В следующей реализации взвешенного скалярного произведения для внимания мы используем дропаут для регуляризации модели. .. raw:: latex \diilbookstyleinputcell .. code:: python class DotProductAttention(nn.Module): #@save """Scaled dot product attention.""" def __init__(self, dropout): super().__init__() self.dropout = nn.Dropout(dropout) # Shape of queries: (batch_size, no. of queries, d) # Shape of keys: (batch_size, no. of key-value pairs, d) # Shape of values: (batch_size, no. of key-value pairs, value dimension) # Shape of valid_lens: (batch_size,) or (batch_size, no. of queries) def forward(self, queries, keys, values, valid_lens=None): d = queries.shape[-1] # Swap the last two dimensions of keys with keys.transpose(1, 2) scores = torch.bmm(queries, keys.transpose(1, 2)) / math.sqrt(d) self.attention_weights = masked_softmax(scores, valid_lens) return torch.bmm(self.dropout(self.attention_weights), values) Чтобы проиллюстрировать работу класса ``DotProductAttention`` , мы используем те же ключи, значения и допустимые длины, что и в предыдущем примере с аддитивным вниманием. В рамках нашего примера предположим, что размер мини-батча равен :math:`2`, общее количество ключей и значений равно :math:`10`, а размерность значений составляет :math:`4`. Кроме того, предположим, что допустимая длина для каждого наблюдения равна :math:`2` и :math:`6` соответственно. Таким образом, мы ожидаем, что выходной тензор будет иметь размерность :math:`2 \times 1 \times 4`, то есть по одной строке на пример в мини-батче. .. raw:: latex \diilbookstyleinputcell .. code:: python queries = torch.normal(0, 1, (2, 1, 2)) keys = torch.normal(0, 1, (2, 10, 2)) values = torch.normal(0, 1, (2, 10, 4)) valid_lens = torch.tensor([2, 6]) attention = DotProductAttention(dropout=0.5) attention.eval() d2l.check_shape(attention(queries, keys, values, valid_lens), (2, 1, 4)) Проверим, действительно ли веса внимания обращаются в ноль для всех элементов, кроме второго и шестого столбцов соответственно (поскольку допустимая длина установлена равной :math:`2` и :math:`6`). .. raw:: latex \diilbookstyleinputcell .. code:: python d2l.show_heatmaps(attention.attention_weights.reshape((1, 1, 2, 10)), xlabel='Keys', ylabel='Queries') .. _subsec_additive-attention: Аддитивное внимание ------------------- Когда запросы :math:`\mathbf{q}` и ключи :math:`\mathbf{k}` имеют различную размерность, мы можем либо использовать матрицу для устранения несоответствия через :math:`\mathbf{q}^\top \mathbf{M} \mathbf{k}`, либо применить аддитивное внимание в качестве функции оценки. Ещё одно преимущество заключается в том, что, как следует из названия, внимание является аддитивным. Это может привести к некоторым незначительным вычислительным экономиям. Для заданных запроса :math:`\mathbf{q} \in \mathbb{R}^q` и ключа :math:`\mathbf{k} \in \mathbb{R}^k` функция оценки *аддитивного внимания* :cite:`Bahdanau.Cho.Bengio.2014` задаётся следующим образом: .. math:: a(\mathbf q, \mathbf k) = \mathbf w_v^\top \textrm{tanh}(\mathbf W_q\mathbf q + \mathbf W_k \mathbf k) \in \mathbb{R}, :label: eq_additive-attn где :math:`\mathbf W_q\in\mathbb R^{h\times q}`, :math:`\mathbf W_k\in\mathbb R^{h\times k}` и :math:`\mathbf w_v\in\mathbb R^{h}` — обучаемые параметры. Затем этот результат подается на софтмакс, чтобы обеспечить неотрицательность и нормировку. Эквивалентная интерпретация :eq:`eq_additive-attn` заключается в том, что запрос и ключ объединяются и подаются на многослойный перцептрон с одним скрытым слоем. Используя :math:`\tanh` в качестве функции активации и отключив смещения, мы реализуем аддитивное внимание следующим образом: .. raw:: latex \diilbookstyleinputcell .. code:: python class AdditiveAttention(nn.Module): #@save """Additive attention.""" def __init__(self, num_hiddens, dropout, **kwargs): super(AdditiveAttention, self).__init__(**kwargs) self.W_k = nn.LazyLinear(num_hiddens, bias=False) self.W_q = nn.LazyLinear(num_hiddens, bias=False) self.w_v = nn.LazyLinear(1, bias=False) self.dropout = nn.Dropout(dropout) def forward(self, queries, keys, values, valid_lens): queries, keys = self.W_q(queries), self.W_k(keys) # After dimension expansion, shape of queries: (batch_size, no. of # queries, 1, num_hiddens) and shape of keys: (batch_size, 1, no. of # key-value pairs, num_hiddens). Sum them up with broadcasting features = queries.unsqueeze(2) + keys.unsqueeze(1) features = torch.tanh(features) # There is only one output of self.w_v, so we remove the last # one-dimensional entry from the shape. Shape of scores: (batch_size, # no. of queries, no. of key-value pairs) scores = self.w_v(features).squeeze(-1) self.attention_weights = masked_softmax(scores, valid_lens) # Shape of values: (batch_size, no. of key-value pairs, value # dimension) return torch.bmm(self.dropout(self.attention_weights), values) Давайте посмотрим, как работает ``AdditiveAttention`` . В нашем учебном примере мы выбираем запросы, ключи и значения размерностей :math:`(2, 1, 20)`, :math:`(2, 10, 2)` и :math:`(2, 10, 4)` соответственно. Это совпадает с нашим выбором для ``DotProductAttention``, за исключением того, что теперь запросы имеют размерность :math:`20`. Аналогично, мы выбираем :math:`(2, 6)` в качестве допустимых длин последовательностей в мини-батче. .. raw:: latex \diilbookstyleinputcell .. code:: python queries = torch.normal(0, 1, (2, 1, 20)) attention = AdditiveAttention(num_hiddens=8, dropout=0.1) attention.eval() d2l.check_shape(attention(queries, keys, values, valid_lens), (2, 1, 4)) При рассмотрении функции внимания мы наблюдаем поведение, которое качественно весьма похоже на поведение ``DotProductAttention``. То есть ненулевыми являются только те элементы, которые находятся в пределах выбранной допустимой длины :math:`(2, 6)`. .. raw:: latex \diilbookstyleinputcell .. code:: python d2l.show_heatmaps(attention.attention_weights.reshape((1, 1, 2, 10)), xlabel='Keys', ylabel='Queries') Итоги ----- В этом разделе мы рассмотрели две основные функции оценки внимания: на основе скалярного произведения и аддитивное. Они являются эффективными инструментами для агрегации данных в последовательностях переменной длины. В частности, внимание на основе скалярного произведения составляет основу современных архитектур трансформеров. Когда запросы и ключи имеют разную длину, можно использовать функцию оценки на основе аддитивного внимания. Оптимизация этих слоёв является одним из ключевых направлений прогресса в последние годы. Например, `библиотека Transformer от NVIDIA `__ и Megatron :cite:`shoeybi2019megatron` критически зависят от эффективных вариантов механизма внимания. Мы подробно рассмотрим этот вопрос при изучении трансформеров в последующих разделах. Упражнения ---------- 1. Реализуйте внимание на основе расстояний, изменив код ``DotProductAttention``. Обратите внимание, что для эффективной реализации достаточно знать только квадратные нормы ключей :math:`\|\mathbf{k}_i\|^2`. 2. Модифицируйте внимание на основе скалярного произведения, чтобы разрешить использование запросов и ключей разной размерности, применяя матрицу для приведения размерностей. 3. Как вычислительная стоимость зависит от размерности ключей, запросов и значений, а также от их количества? Как обстоят дела с требованиями к пропускной способности памяти? `Обсуждения `__