Всем привет, кто зашел почитать этот пост. Сегодня я попытаюсь объяснить вам свою точку зрения, с которой я смотрю на N-мерные пространства.
Этим вопросом я задался, когда наконец решил для себя: пора понять: "если числа в векторе - это координаты в len(вектор)-мерном пространстве, то как нам интерпретировать векторы, у которых длина больше 3х?". Терпеть не могу ждать, поэтому сел разбираться прямо сейчас — боюсь, что в восьмом классе учитель алгебры не будет тратить время на объяснения. Что такое вектор поверхностно — я знаю: это набор чисел, который указывает на какую-то координату в n-мерном пространстве (nD пространстве). Но как их интерпретировать?
Сразу хотел бы сказать - пожалуйста, не судите строго. Я просто рассказываю свою точку зрения, которая может кому-то пригодится.
Начало. Вспоминаем, что такое векторы.
Если говорить совсем просто: вектор - это массив чисел, который указывает куда и на сколько единиц надо двигаться от начала координат по n-мерному пространству.
Ладно, давайте разберем это на примере. Например, у нас есть начало координат — (0; 0; 0) в трёхмерном пространстве и вектор [2; 5; -1]. Если придерживаться определения выше, то мы должны "сдвинуть" начальные координаты на такие-то определённые числа из вектора. Важно помнить, что позиции тут очень важны — число на позиции 0 (X) нельзя прибавить к позиции 2 (Z) из начальных координат. То есть надо строго придерживаться правила: "Каждый сдвиг оси из вектора должен складываться или вычитаться строго из координат этой же оси из начальных координат". Если же мы все правильно сдвинем, то мы получим координату: ((0+2); (0+5); (0+(-1))) = (2;5;-1).
Так как мы разбираем в основном нейросети, то дальше я буду иметь ввиду координаты самой точки под определением "вектор". В нейросетях чаще всего вектор - координаты какой-то точки, или сущности.
Еще бы хотел добавить - на практике большая часть не превращает вектор в нейросетях в какую-то точку и вовсе не воспринимает этот вектор как координаты точки. В основном, это просто результат выполнения математической операции либо с участием веса, либо без. Точка - это что-то на научном.
N-мерное пространство.
Мы живем в трех мерном пространстве (XYZ). А какая ось добавляется в 4-мерном пространстве?
Я немного почитал и пришел к выводу, что в четвертом пространстве добавляется новая ось - T (Time). Если в трехмерном пространстве мы двигаемся по времени только вперед, то в четырехмерном мы можем двигаться по этой оси вперед-назад.
Далее, я почитал про пятимерное пространство и пришел к выводу, что в пятимерном пространстве мы поверх оси T (Time) получаем ось M (Moment). То-есть, мы получаем возможность и двигать время вперед-назад и создавать сразу несколько вариаций этого момента параллельно в это же время (по оси M).
Логически решил продолжить эту цепочку, и получил следующее предположение: А вдруг дальше (6D, 7D, ..., ND) мы просто создаем новые оси T и M внутри прошлых осей M? Звучит очень запутанно, но я попробую объяснить. Давайте представим 4-мерное пространство, где мы можем управлять временем (двигать назад-вперед) благодаря оси T, но не можем управлять судьбой. А в пятимерном пространстве мы это делать можем благодаря оси M. А вдруг, в этой оси M еще есть своя ось T, в которой есть еще одна своя ось M и так n раз? А вдруг сейчас вы существуете сразу в нескольких версиях себя?
Еще у меня есть предположение, что единой "точки" в 4+-мерных пространствах просто нет. Например, какая-нибудь кривая.
На эти вопросы очень трудно ответить, так как недостаточно информации. Но просто, как частичная гипотеза - сойдет.
Итоги.
В этом посте я попытался поднести вам свою точку зрения и идею того, как устроена N-мерность. Я понимаю, что это все не доказанно. Но это просто точка зрения.
Введение и обсуждение идеи. Выявление проблем обычного внимания.
Недавно решил попробовать обучить MultiheadAttention способом стохастического градиентного спуска на последовательностях длинной 512 с целью убедиться, что я прав в утверждении, что обычное мультиголовое внимание не сможет обучиться из-за градиентов и некоторых конфликтов. В итоге, после примерно двух часов работы за компьютером, точность (accuracy) выросла с 0,0 до 0,002. По привычке я первым делом полез проверять градиенты и обнаружил, что их значения находятся в диапазоне 0.0002 - 0,000008.
Однако с архитектурой всё в порядке: она полностью соответствует стандарту - Pre-Norm и Skip Connection на месте. Тогда я решил поискать проблему в самом MultiheadAttention. После недолгих размышлений я решил, что корень проблемы — в SoftMax: а именно, в способе распределения вероятностей по токенам и в расчёте градиента.
Во-первых, давайте разберёмся, как обычный MultiheadAttention нормализует входные данные перед SoftMax и помогает ли это в принципе. В стандартном MultiheadAttention нормализация - это просто деление на квадратный корень от размера головы. Это делается именно так, потому что при вычислении произведения QK.T (веса которого распределены в диапазоне [−1;1]) каждое результирующее значение по статистике будет примерно равно корню из размерности головы. Это аналогично тому, что при 60 подбрасываниях монетки выпадет примерно по 30 орлов и решек - чистая статистика. Однако деление на √d_k - это лишь "костыль", поскольку на практике значения QK.T могут быть как больше, так и меньше этой величины. А так как в процессе обучения веса постоянно меняются, то вместе с ними масштабируются и эти значения. Здесь и кроется проблема: если одни элементы произведения становятся больше, а другие - меньше, то SoftMax распределяет основную вероятность лишь на несколько "больших" элементов, оставляя для остальных исчезающе малые доли. Когда SoftMax выдаёт ничтожно малые значения для тех или иных токенов, градиент для них стремится к нулю (по формуле p⋅(1−p)), и при последующем умножении на другие градиенты это полностью обнуляет градиент для данного токена. Когда градиент, на который мы обновляем веса, оказывается значительно меньше самих весов, обучение практически останавливается - мы «стоим на месте». Более того, когда модель опирается лишь на несколько токенов, присваивая остальным исчезающе малые вероятности, она фактически теряет контекст, а вместе с ним и потенциально важные детали. Это серьёзно усугубляет проблему обучения: другие значимые токены вносят слишком мелкий вклад, который попросту подавляется доминирующими. В итоге, такое внимание неспособно эффективно обучаться стандартным SGD - ограничением выступает сам градиент.
Но что же тогда делать? И почему на практике все работает?
На практике всё работает именно так, потому что никто не использует одну голову для длинных последовательностей. Например, в DeepSeek V4 PRO используется 128 голов и множество слоёв внимания. Да, такая модель тоже игнорирует много токенов. Да, у неё тоже при обучении градиенты с большой вероятностью где-то затухали. Но никто не использует SGD. А несколько слоёв внимания компенсируют потерю, создавая дополнительные параметры и, грубо говоря, дополнительно выявляют признаки из предыдущего слоя внимания. То есть все используют Adam / AdamW, который подходит к обучению "с умом", а не просто умножает градиент на learning rate. И именно благодаря ему все трансформеры работают отлично. Я же использую SGD только в рамках эксперимента.
А если хочется стабильных градиентов? А если компьютер не тянет Adam / AdamW, но хочется такое внимание, которое по качеству будет похоже на MultiheadAttention или даже превзойдёт его, но при этом будет стабильно обучатся и с SGD? Для начала давайте выявим самую главную проблему, почему обычное внимание не обучается методом SGD. А вот и причина: как я говорил ранее, SoftMax делает градиенты для токенов с низкой вероятностью очень маленькими. И сеть просто не способна учиться стабильно из-за этих мелких градиентов. А что если на других токенах внимание просто смотрит на два этих токена, а на остальные плюет? Это будет ужасным конфликтом для обучения генеративных моделей, классификаторов и для многих других задач.
Немного подумав, я сначала пришёл к выводу поставить Sigmoid вместо SoftMax, а вместо деления произведения QK.T на √d_k делить на std(QK.T) отдельно для каждого вектора, независимо от других. Как раз Sigmoid не зависит от всех чисел в векторе, а работает локально с каждым элементом отдельно, что как раз и исправляет проблему затухания градиента, свойственную Softmax. Но сразу же откинул эту идею по следующим причинам:
Sigmoid (по моим расчётам) имеет рабочий диапазон [−0,5; 0,5], в котором градиент почти максимален, а выходы не крутятся вокруг 0.5. Но деление произведения QK.T на std(QK.T) приводит к тому, что среднее отклонение становится равным единице, что вдвое превышает рабочий диапазон Sigmoid. В результате градиенты Sigmoid оказываются далеки от своего максимума.
После того, как Sigmoid выдал свои выходы, мы их умножаем на V. Вот тут и кроется вторая проблема. Так как Sigmoid выдает значения в диапазоне (0; 1) и никогда не равен ровно 0 или 1, то V умножается строго на положительные числа. Но главная беда в том, что сумма выходов Sigmoid не ограничена единицей - в отличие от Softmax. И эти значения будут расти с длиной последовательности. Чем длиннее вход, тем больше токенов вносят свой вклад, и итоговые значения в выходной матрице V становятся огромными. Конечно, с градиентами для самого внимания проблем не будет, но для следующего слоя - будут. Это исправляется тем, что мы ставим LayerNorm или RMSNorm перед следующим слоем, но все же - как проблема, это проблема.
И пожалуй, самая главная проблема - максимально возможный градиент функции. Sigmoid имеет максимальный градиент = 0,25. А по моим расчётам лучший диапазон для градиента очень маленький - [-0.5; 0.5].
Немного подумав и перебрав несколько вариантов замены Sigmoid я остановился на Tanh, чему и посвящена эта мини-статья.
Взял я именно его по следующим причинам:
Tanh в отличие от Sigmoid по моим расчётам имеет больший диапазон - [-1; 1]. В этом диапазоне градиент почти в два раза больше максимально возможного градиента Sigmoid (0,25). За счёт большего в два раза диапазона он полностью подходит под нашу нормализацию, которую я поставил вместо QK.T.
Также Tanh в отличие от Sigmoid имеет максимальный градиент в 4 раза больше, чем максимальный градиент Sigmoid. Это очень хорошее дополнение. Если брать рабочий диапазон Tanh (по моим расчётам) [-1; 1], то минимальный градиент будет примерно равен 0,5, а максимальный - 1. А у Sigmoid в рабочем диапазоне (по моим расчётам) [-0,5; 0,5] минимальный градиент примерно равен 0,23, а максимальный - 0,25. Лучше брать тот вариант, где градиент больше. Особенно для SGD.
Больших значений после произведения выходов Tanh на V будет намного меньше, чем с Sigmoid. Это можно объяснить тем, что у Tanh выход имеет диапазон [-1; 1] - там есть отрицательные числа, а у Sigmoid [0; 1] - отрицательных чисел быть не может. То есть теперь мы умножаем на V не только положительные числа, но и отрицательные, которые смогут добавить дополнительные варианты для подбора весов и помогут при сложении помешать огромному выбросу, либо сделать его не таким огромным. Но, это не отменяет важность LayerNorm / RMSNorm на выходе из внимания.
Была мысль взять ReLU или SiLU, но сразу же я ее откинул, так как ReLU и SiLU хоть и имеют хорошие градиенты на положительных входах, но это гарантированно приведет к огромным значениям в V. Также это может понизить качество модели в плане того, что мы просто обнуляем или "гасим" мелкие значения, тем самым уменьшая варианты для подбора весов градиентом.
Разобравшись, почему именно Tanh - давайте окончательно соберем весь этот пазл.
Как я уже говорил, рабочий диапазон Tanh — [-1; 1]. В этом диапазоне градиент находится в идеальном положении. А так как деление на √d_k - это просто костыль, который может не работать (как я уже объяснял), мы будем делить на std(QK.T), где std вычисляется отдельно для каждого вектора в матрице QK.T. Далее - просто матрично умножаем выходы Tanh на V. И после - конкатенируем результаты всех голов. Следующее действие очень важно для стабильности градиентов: после конкатенации голов необходимо поставить LayerNorm или RMSNorm, так как даже при использовании Tanh, хоть вероятность взрывов и меньше, они всё ещё возможны.
Итак, формула для одной головы принимает следующий вид:
А формула выхода из Tanh Attention примет такой вид:
Тестовое обучение Tanh Attention методом SGD на последовательности длинной 512. Подводим итоги.
После того как я реализовал Tanh Attention, я собрал две модели:
Модель с Tanh Attention (1 слой внимания, 4 головы)
Архитектуры, датасет, данные для обучения и код абсолютно одинаковы в обоих экспериментах. Обучение проводилось методом SGD с одинаковым learning rate = 0.001. Ниже представлены результаты, а также графики распределения градиентов для Tanh Attention и стандартного MultiheadAttention.
Tanh Attention-21.22 %-2 часа 37 минут-6.58-0.00604-0.029
MultiheadAttention-0.002 %-2 часа 39 минут-9.84-0.000008-0.0002
Фото:
Градиенты Tanh Attention
Градиенты MultiheadAttention
Итог: в этой статье мы разобрали проблемы стандартного механизма внимания и предложили способ их решения. Эксперименты показали, что обычное внимание вообще не обучается с помощью SGD, тогда как Tanh Attention стабильно сходится.
Дополнительно я провёл эксперимент с оптимизатором Adam. На Tanh Attention он показал такую же скорость сходимости, как и SGD. Обычное внимание с Adam обучалось почти так же хорошо, как и Tanh Attention, хотя и немного медленнее — но это некритично.
Общий вывод: замена Softmax на Tanh улучшает градиенты и добавляет вариативность модели. Кроме того, Tanh Attention с высокой вероятностью лучше удерживает контекст за счёт более равномерного распределения весов.
P.S. статью так же написал на хабре, но с модерацией не известно.