Трудности перевода: почему языковые модели генерируют случайные иероглифы - Академия Selectel

Трудности перевода: почему языковые модели генерируют случайные иероглифы

Тирекс
Тирекс Самый зубастый автор
10 июня 2026

Рассказали, как локальные и нелокальные языковые модели из-за системных сбоев внезапно начинают сыпать иероглифами, и подробно объяснили, какая именно техническая ошибка скрывается за этим странным поведением ИИ.

Изображение записи

Статью поделили на два уровня. Первая часть (без которой сложно понять вторую) — для тех, кто слышал слово «эмбеддинг», но не трогал его руками: разберем на пальцах и со стрелочками, что модель держит внутри своего цифрового серого вещества, в общем объясню простые вещи простыми словами. Вторая часть — для тех, кому интересно копнуть чуть дальше базы: туда я поместил grokking, фурье-частоты и суперпозицию, и там мы вытащим реальное пространство обученной модели и посмотрим, как оно устроено. 

Часть 1. Для тех, кто слышал слово «эмбеддинг»

Модель не видит букв

Первое, что стоит обозначить: модель не работает с текстом в привычном нам с вами смысле (даже если название «большие языковые модели» заставляет вас думать обратное). Сначала текст режется на токены — на куски из символов. Иногда токен это целое слово, а иногда и полслова. Каждому токену присвоен номер, и дальше внутрь модели идет только последовательность номеров. Даже слова «токен» для нее не существует, зато есть условный токен номер 8421.

Но с номерами тоже не выйдет нормально работать, ведь номер — это просто бессмысленный ярлык: токен 8421 не «больше» и не «лучше» токена 8425. Поэтому каждому токену сопоставляют вектор — список из нескольких десятков или сотен чисел. Вот этот список и называется эмбеддингом.

Вектор — это просто стрелка

Не пугайтесь слова «вектор». Вектор из двух чисел, например (3, 5) — это стрелка из начала координат в точку (3, 5): три базисных (единичных) вектора вправо, и пять вверх. 

Вектор из трех чисел — стрелка в пространстве. Вектор из 128 чисел — стрелка в 128-мерном пространстве, вот только его невозможно представить (углеродным формам жизни уж точно, зато кремниевым…), но проводить расчеты с ним в его пространстве все равно можно: есть длина стрелки, есть направление, есть угол между двумя стрелками.

Вот так выглядят векторы чисел в нашей будущей модели, спроецированные на плоскость:

Визуализация векторов чисел, расходящихся по кругу на шаге 400 обучения или симуляции.

Каждая стрелка соответствует одному числу. Смысл числа теперь в том, куда смотрит стрелка. И вот это «куда смотрит» модель определяет сама.

Откуда у стрелок берется направление

Тут самое важное место первой части. В начале обучения все эти векторы случайные. Каждому токену выдали стрелку, смотрящую в произвольную сторону (чистый шум и  никакого смысла). Если в этот момент посмотреть на пространство, это просто облако точек без структуры.

Смысл появляется во время обучения. Модель решает свою задачу, ошибается, и после каждой ошибки чуть-чуть подкручивает векторы, чтобы в следующий раз ошибаться меньше. Миллионы таких подкручиваний, и стрелки (которые были случайными, напоминаю) разворачиваются в осмысленные — для нас и для модели — стороны. Похожие по роли токены начинают смотреть в близкие стороны, а разные — соответственно в разные.

Это главная идея, на которой держится вся тема: модель расставляет точки в пространстве так, чтобы геометрия этого пространства отражала смысл. Близко = похоже, а направление = свойство.

Дальше я покажу это на работающей модели. Если первая часть зашла и хочется глубже, то добро пожаловать в кроличью нору.

Часть 2. Для тех, кто хочет копнуть поглубже

Эксперимент: учим модель складывать по модулю

Давайте возьмем задачу попроще, чтобы мы смогли препарировать нашу модель. Берем числа от 0 до 52 и учим модель складывать их по кругу. То есть у нас будет круг из 53 делений: дошли до 52, и следующий шаг возвращает в 0. Математики пишут это как (a + b) mod 53, но за значком mod прячется та самая идея циферблата.

Математическая иллюстрация модульной арифметики и сложения по модулю 53 на числовом круге.

Почему именно 53, а не круглые 50 или 100? Потому что 53 — простое число, оно не делится ни на что, кроме себя и единицы. Из-за этого у модели не будет соблазна схитрить и разбить круг на аккуратные половинки или четвертинки. 

Сам опыт, о котором идет речь, много раз показывали в англоязычном интернете. Но по-русски, чтобы с кодом и анимацией и разжевано я ничего толкового не нашел, поэтому собрал свое.

Модель я нарочно сделал примитивной: только таблица стрелок из первой части плюс пара слоев, которые перемалывают числа. Никакого внимания и никаких трансформеров для большей наглядности работы модельки. И половину всех пар чисел я показываю модели на обучении, а вторую половину прячу и держу для экзамена.


      import numpy as np, torch, torch.nn as nn
import matplotlib.pyplot as plt
from matplotlib import cm
torch.manual_seed(0); np.random.seed(0)
N, DIM, STEPS = 53, 128, 4000
a = np.repeat(np.arange(N), N); b = np.tile(np.arange(N), N)
A = torch.tensor(a); B = torch.tensor(b); Y = torch.tensor((a + b) % N)
idx = np.random.permutation(len(a)); cut = len(a) // 2
tr = torch.tensor(idx[:cut])
class Model(nn.Module):
    def __init__(s):
        super().__init__()
        s.emb = nn.Embedding(N, DIM)
        s.mlp = nn.Sequential(nn.Linear(2*DIM,256), nn.ReLU(),
                              nn.Linear(256,256), nn.ReLU(), nn.Linear(256,N))
    def forward(s, a, b):
        return s.mlp(torch.cat([s.emb(a), s.emb(b)], -1))
m = Model()
opt = torch.optim.AdamW(m.parameters(), lr=1e-3, weight_decay=1.0)
lossf = nn.CrossEntropyLoss()
for step in range(STEPS+1):
    loss = lossf(m(A[tr], B[tr]), Y[tr])
    opt.zero_grad(); loss.backward(); opt.step()
E = m.emb.weight.detach().numpy(); X = E - E.mean(0); ns = np.arange(N)
k = max(range(1, N//2+1),
        key=lambda k: np.sum((np.cos(2*np.pi*k*ns/N) @ X)**2) +
                      np.sum((np.sin(2*np.pi*k*ns/N) @ X)**2))
c, s = np.cos(2*np.pi*k*ns/N), np.sin(2*np.pi*k*ns/N)
coef = np.linalg.lstsq(X, np.stack([c, s], 1), rcond=None)[0]
P = X @ coef
plt.style.use("dark_background")
fig, ax = plt.subplots(figsize=(7,7))
ax.plot(P[:,0], P[:,1], color="#3a3a55", lw=.8)
ax.scatter(P[:,0], P[:,1], c=cm.twilight(np.linspace(0,1,N)), s=130)
for i in range(N):
    ax.annotate(str(i), P[i], color="white", fontsize=7, ha="center", va="center")
ax.set_aspect("equal"); ax.set_xticks([]); ax.set_yticks([])
plt.tight_layout(); plt.savefig("ring.png", dpi=130); print("freq k =", k)

Вес weight_decay не для красоты. Без сильной регуляризации модель просто выучит обучающие пары и не станет строить никакую структуру — ей это будет не нужно.  Под регуляризацией здесь понимается искусственный штраф за сложность, который притягивает значения внутренних параметров модели к нулю и не дает им бесконтрольно расти.

Регуляризация давит на зубрежку и заставляет искать обобщающее решение. Дело в том, что для запоминания тысяч отдельных фактов модели требуются огромные рандомные веса. 

Штраф же принудительно сжимает их, буквально вынуждая систему искать самое компактное и красивое математическое правило. Именно поэтому здесь и проявляется grokking — озарение.

Зубрежка, плато, озарение

Давайте теперь следить за двумя цифрами по ходу обучения: 

  • первая — насколько хорошо модель отвечает на тех парах, что ей показывали; 
  • вторая — насколько хорошо она отвечает на спрятанных парах.

И вот что мы увидим. На показанных парах модель почти сразу отвечает отлично — ну еще бы, она их просто запомнила. А на спрятанных долго не может ничего, тычет наугад, цифра лежит на нуле. Логично: заученное знает, а правил не понимает, поэтому и валится.  

А потом идет озарение. Где-то после полутора-двух тысяч шагов цифра на спрятанных парах вдруг резко идет вверх и догоняет показанные. 

Вот как все это выглядит:

Визуализация процесса обучения модели: распределение эмбеддингов на шаге 480 и график точности с эффектом «озарения» (grokking).

Никто модели ничего не подсказывал в эту секунду. Она просто достаточно долго крутилась под прессом регуляризации, и в какой-то момент зубрежка надломилась и превратилась в понимание (или просто в grokking). 

А что в эту же секунду происходит с эмбеддингом?

Числа складываются в кольцо

Помните, в начале все стрелки смотрели куда попало? Так вот, к моменту озарения числа перестают быть стохастически направлены и встают аккуратным кольцом:

Граф на темном фоне, где пронумерованные вершины выстроились в идеальное кольцо с шагом по частоте.

Посмотрите внимательно на порядок. Числа по кругу идут не подряд, а через постоянный шаг, и если соединить их линией, рисуется многоконечная звезда. Модель сама поняла, что числа замкнуты в круг, и разложила их по окружности, потому что на круге складывать проще всего. Сложить два числа теперь значит просто повернуться на два угла и посмотреть, куда мы попали.

Никто не давал модели окружность. Ей вообще не давали геометрии, только пары чисел и правильные ответы. Круг она построила сама, потому что для модульного сложения это вычислительно самое дешевое представление.

Оговорка про частоту

Теперь надо прояснить одну вещь, иначе картинка выйдет приукрашенной. Стрелки у нас обитают не на плоскости, а в пространстве из 128 направлений. А я показываю вам плоскую картинку. Все потому что я взял это многомерное облако и спроецировал его на плоскость. И проекцию я выбрал не случайную, а ту, на которой кольцо видно отчетливее всего.

И тут, прошу внимания — главная мысль второй части. Кольцо не одно. Если аккуратно разобрать на сколько разных кругов модель разложила свои числа, окажется, что кругов сразу несколько, и они вложены друг в друга в одном и том же пространстве. 

Один круг с крупным шагом, другой с мелким, третий с еще каким-то. Если уж и спускаться в кроличью нору с часами, то представьте циферблат, на который наложили второй циферблат с другим числом делений, а сверху третий, и все они работают одновременно. Модель считает сложение сразу на всех этих часах параллельно, а потом сводит ответы в один.

У этого тоже есть имя в науке. Разложить сигнал на набор круговых волн разной частоты — это и есть преобразование Фурье, та самая штука, которой раскладывают звук на ноты. Модель, по сути, переоткрыла Фурье сама, никто ее этому не учил. Именно это Nanda и соавторы разобрали в своей работе про grokking.

А на плоской картинке мы видим только один круг, самый громкий. Остальные просто лежат в других проекциях того же пространства, и на одной плоскости их сразу не показать.

Зачем это все, если модель игрушечная

Справедливый вопрос. У нашей крохи 128 направлений на 53 числа — вроде бы вагон места, можно было бы каждому числу выделить отдельную полку и не мучиться. А она все равно не стала, а упаковала числа внахлест в несколько кругов. Запомните этот факт, он сейчас выстрелит.

Теперь давайте подумаем о настоящих, больших LLM-ках. Сколько всего им надо удержать в голове: каждое слово языка, каждый оттенок смысла, грамматику, факты про мир, имена, даты. Этого добра в миллионы раз больше, чем у модели есть направлений в пространстве. Выделить каждому понятию по личной полке физически невозможно. 

И что делает модель? Ровно то же, что и наша: кладет несколько смыслов внахлест. Это называют суперпозицией, когда на одном месте лежит сразу несколько вещей, и нужную достаешь по точному адресу.

Отсюда же вытекает и другой факт. Хочется ведь ткнуть пальцем в один нейрон модели и сказать: вот этот отвечает за белки, а вот тот за глаголы — а не выйдет. Тот же самый нейрон у вас будет отвечать и за белки, и заодно за что-нибудь про юридические тексты, и еще за пару вещей, между собой никак не связанных, которые модель просто сложила на одну полку из экономии. Распутать этот клубок обратно — тема для отдельной статьи.

А вывод простой и он переносится с нашей игрушки на любую серьезную модель. У нее (в смысле, у серьезной модели) смыслов всегда больше, чем измерений в пространстве, поэтому она держит их внахлест, а различает по тому, куда смотрит вектор.

Возвращаемся к иероглифам

Вот теперь, со всем этим багажом, понятно, чем опасен недоученный токен. Это кусочек, который в словаре модели есть, но в текстах, на которых ее учили, почти не попадался. Так бывает с редкими иероглифами и обрывками юникода, когда корпус собран из кучи языков вперемешку. Раз примеров почти не было, модель не получила сигнала, куда его двигать, и его стрелка так и осталась торчать там, где ее бросили в самом начале.

Пока этот токен не всплывает, все тихо. Но вспомните, как модель вообще выбирает следующий кусочек текста: она прикидывает вероятности сразу для всего словаря и тянет жребий. 

Обычно правильный кусочек забирает себе почти всю вероятность, и жребий честно выпадает на него. А вот на стыке языков или в каком-нибудь редком, кривом контексте модель плывет, вероятности размазываются тонким слоем по множеству вариантов — и в этом хвосте оказывается наш иероглиф. Достаточно одного неудачного броска жребия, чтобы он вылез наружу прямо посреди текста. А дальше хуже: модель видит его уже как часть написанного и может с этого места поехать, и дальше.