Существует ли эмодзи морского конька? Давайте спросим об этом у GPT-5 Instant:
«Да, эмодзи морского конька существует» [Далее следует долгий перебор эмодзи с попыткой найти нужный.]
Чего? Тогда спросим у Claude Sonnet 4.5:
«Да! 🐎 Хотя нужно уточнить, что это эмодзи лошади. Вот эмодзи морского конька: 🦈. Хотя нет, это акула!»
Что здесь происходит? Возможно, Gemini 2.5 Pro справится лучше?
«Да, эмодзи морского конька есть! Seahorse»
Хм, что-то непонятное творится. Будем разбираться, в чём причина.
Вот ответы, которые можно получить, если сто раз спросить у множества разных моделей, существует ли эмодзи морского конька:
Существует ли эмодзи морского конька, да или нет? Ответь одним словом, без пунктуации.
gpt-5-chat
100% «Да»
gpt-5
100% «Да»
claude-4.5-sonnet
100% «Да»
llama-3.3-70b
83% «да»
17% «Да»
Не стоит и говорить, что популярные языковые модели крайне уверены в том, что эмодзи морского конька есть. И в этой уверенности они не одиноки: вот пост на Reddit с сотнями комментариев от людей, чётко помнящих о существовании такого эмодзи:
Таких источников целая куча — загуглите «seahorse emoji», и вы найдёте кучу тиктоков, видео на Youtube и даже мемкойны (уже не действующие), связанные с исчезновением эмодзи морского конька, в существовании которого уверены люди. Но, разумеется, его никогда не было.
Возможно, LLM считают, что этот эмодзи существует, потому что так считают многие люди в обучающих данных. Или, возможно, эта вера возникла из-за схождения — в Unicode есть много других морских животных, поэтому и люди, и LLM вполне логично могут предполагать (и даже обобщать) наличие такого удивительного животного. Эмодзи морского конька даже было предложено формально, но в 2018 году это предложение отклонили.
Какой бы ни была первопричина, многие LLM начинают каждое новое окно контекста со свежей ошибочной верой в существование такого эмодзи. Но почему возникает такое странное поведение? На самом деле, я и сам раньше думал, что эмодзи морского конька есть, но если бы я захотел отправить его другу, то просто посмотрел бы на клавиатуру и осознал, что его там нет, не отправил бы неправильное эмодзи. Какие внутренние механизмы заставляют LLM вести себя подобным образом?
Давайте посмотрим на этот вопрос через любимый, но недооценённый инструмент интерпретируемости — логитный объектив!
Используем префикс промпта — шаблон чата со стандартным системным промптом llama-3.3-70b, вопрос о эмодзи морского конька и частичный ответ модели перед тем, как она выводит сам эмодзи:
<|begin_of_text|><|begin_of_text|><|start_header_id|>system<|end_header_id>
Cutting Knowledge Date: December 2023
Today Date: 26 Jul 2024
<|eot_id|><|start_header_id|>user<|end_header_id|>
Is there a seahorse emoji?<|eot_id|><|start_header_id|>assistant<|end_header_id|>
Yes, there is a seahorse emoji:
Мы можем взять lm_head
модели, который обычно используется только в выводе последнего слоя, и применить его к каждому слою для генерации промежуточных прогнозов токенов. Этот процесс даст нам следующую таблицу, демонстрирующую для каждого четвёртого слоя наиболее вероятный токен для следующих трёх позиций после префикса (токены 0, 1 и 2) и пять наиболее вероятных прогнозов для первой позиции (токен 0 topk 5):
слой | токены | токены | токен 0 | ||
---|---|---|---|---|---|
0 | 1 | 2 | объединённые | (topk 5) | |
0 | 83244'ĠBail' | 15591'ĠHarr' | 5309'Ġvert' | Bail Harr vert | ['ĠBail', 'ĠPeanut', 'ĠãĢ', 'orr', 'ĠâĢĭâĢĭ'] |
4 | 111484'emez' | 26140'abi' | 25727'avery' | emezabiavery | ['emez', 'Ġunm', 'ĠOswald', 'Ġrem', 'rix'] |
8 | 122029'chyb' | 44465'ĠCaps' | 15610'iller' | chyb Capsiller | ['chyb', 'ĠSund', 'ترÛĮ', 'resse', 'Ġsod'] |
12 | 1131'...' | 48952'ĠCliff' | 51965'ĠJackie' | ... Cliff Jackie | ['...', 'ages', 'dump', 'qing', 'Ġexp'] |
16 | 1131'...' | 12676'365' | 31447'ĠAld' | ...365 Ald | ['...', '...Ċ', 'Ġindeed', 'Ġboth', 'ĠYes'] |
20 | 1131'...' | 109596'éļĨ' | 51965'ĠJackie' | ...隆 Jackie | ['...', '...Ċ', 'Z', 'Ġboth', 'ĠHust'] |
24 | 12'-' | 31643'ï¸ı' | 287'ing' | -️ing | ['-', '...', 'â̦', '...Ċ', 'em'] |
28 | 1131'...' | 96154'ĠGaut' | 51965'ĠJackie' | ... Gaut Jackie | ['...', '-', '...Ċ', '-Ċ', 'Ġ'] |
32 | 1131'...' | 96154'ĠGaut' | 6892'Ġing' | ... Gaut ing | ['...', 'â̦', '...Ċ', 'O', 'zer'] |
36 | 1131'...' | 12'-' | 88'y' | ...-y | ['...', 'â̦', '...Ċ', 'Ġ', 'u'] |
40 | 1131'...' | 31643'ï¸ı' | 88'y' | ...️y | ['...', 'u', 'â̦', 'Âł', '...Ċ'] |
44 | 80435'ĠScor' | 15580'Ġhorse' | 15580'Ġhorse' | Scor horse horse | ['ĠScor', 'u', 'ĠPan', 'in', 'Ġhttps'] |
48 | 15580'Ġhorse' | 15580'Ġhorse' | 15580'Ġhorse' | horse horse horse | ['Ġhorse', 'Âł', 'ĠPan', 'ĠHomes', 'ĠHorse'] |
52 | 9581'Ġsea' | 15580'Ġhorse' | 15580'Ġhorse' | sea horse horse | ['Ġsea', 'Ġhorse', 'ĠHorse', 'ĠSea', 'âĢij'] |
56 | 9581'Ġsea' | 43269'ĠSeah' | 15580'Ġhorse' | sea Seah horse | ['Ġsea', 'ĠSea', 'ĠSeah', 'Ġhippoc', 'Ġhorse'] |
60 | 15580'Ġhorse' | 15580'Ġhorse' | 15580'Ġhorse' | horse horse horse | ['Ġhorse', 'Ġsea', 'ĠSeah', 'Ġse', 'horse'] |
64 | 15580'Ġhorse' | 15580'Ġhorse' | 15580'Ġhorse' | horse horse horse | ['Ġhorse', 'Ġse', 'ĠHorse', 'horse', 'Ġhors'] |
68 | 60775'horse' | 238'IJ' | 15580'Ġhorse' | horse� horse | ['horse', 'Ġse', 'Ġhorse', 'Ġhippoc', 'ĠSeah'] |
72 | 513'Ġse' | 238'IJ' | 513'Ġse' | se� se | ['Ġse', 'Ġhippoc', 'horse', 'ĠðŁ', 'Ġhorse'] |
76 | 513'Ġse' | 238'IJ' | 513'Ġse' | se� se | ['Ġse', 'Ġhippoc', 'hip', 'Ġhorse', 'ĠHipp'] |
80 | 11410'ĠðŁ' | 238'IJ' | 254'ł' | 🐠 | ['ĠðŁ', 'ðŁ', 'ĠðŁĴ', 'Ġ', 'ĠðŁij'] |
Это и есть логитный объектив: мы используем lm_head
модели для создания логитов (вероятностей токенов) для изучения внутренних состояний. Стоит отметить, что токены и вероятности, получаемые из логитного объектива, не эквивалентны полным внутренним состояниям модели! Для их получения нам был понадобилась более сложная методика наподобие representation reading или sparse autoencoders. Мы получили только объектив состояния — он показывает, каким был бы выходной токен, если бы этот слой оказался последним. Но несмотря на это ограничение, логитный объектив всё равно полезен. С его помощью может быть сложно интерпретировать ранние слои, но двигаясь по стеку, мы сможем наблюдать, как модель итеративно совершенствует эти состояния в направлении окончательного прогноза — эмодзи рыбы.
(Почему необъединённые токены выглядят, как символы «ĠðŁ», «IJ», «ł»? Это особенность токенизатора — такие токены кодируют байты UTF-8 эмодзи рыбы. Это не относится к теме статьи, но если вам любопытно, то попросите Claude или другую LLM объяснить этот параграф и эту строку кода: bytes([bpe_byte_decoder[c] for c in 'ĠðŁIJł']).decode('utf-8') == ' 🐠'
)
Однако посмотрите, что происходит в средних слоях — это не просто странности ранних слоёв и не байты эмодзи окончательного прогноза! Вместо них мы получаем слова, относящиеся к полезным концепциям; в частности, к концепции морского конька. Например, в слое 52 мы получаем «sea horse horse» — три позиции скрытого состояния подряд, кодирующие концепцию «seahorse». Позже, в top-k для первой позиции, мы получаем смесь «sea», «horse» и префикса последовательности байтов эмодзи «ĠðŁ».
О чём же думает модель? «seahorse + emoji»! Она пытается сконструировать представление скрытого состояния морского конька в сочетании с эмодзи. Почему модель пытается создать эту комбинацию? Давайте разберёмся, как работает lm_head
.
Слой lm_head
языковой модели — это огромная матрица, состоящая из векторов размерностью скрытого состояния. Каждому токену в словаре (их примерно 300 тысяч) соответствует один такой вектор. Когда ему передаётся скрытое состояние (или при обычной передаче по модели, или на ранних этапах, потому что кто-то использовал логитный объектив), lm_head
сравнивает это скрытое состояние ввода с каждым вектором размерностью скрытого состояния в этой большой матрице и (согласованно с сэмплером) выбирает идентификатор токена, связанного с вектором матрицы, наиболее близким к скрытому состоянию ввода.
(Выражаясь более технически, lm_head
— это линейный слой без смещения, поэтому x @ w.T
выполняет скалярное произведение с каждым вектором анэмбеддинга для получения сырых оценок. Затем выполняется обычный log_softmax и сэмплирование argmax/temperature.)
Это значит, что если модель хочет вывести «hello», например, в ответ на приветствие со стороны пользователя, ей нужно создать скрытое состояние, как можно более схожее с вектором токена «hello», которое lm_head
затем может превратить в идентификатор токена hello. И при помощи логитного объектива мы можем увидеть, что именно это и происходит в ответ на «Hello :-)»:
слой | токены | токены | токен 0 | ||
---|---|---|---|---|---|
0 | 1 | 2 | объединённые | (topk 5) | |
0 | 0'!' | 0'!' | 40952'opa' | !!opa | ['"', '!', '#', '%', '$'] |
8 | 121495'ÅĻiv' | 16'1' | 73078'iae' | řiv1iae | ['ÅĻiv', '-', '(', '.', ','] |
16 | 34935'Ġconsect' | 7341'arks' | 13118'Ġindeed' | consectarks indeed | ['Ġobscure', 'Ġconsect', 'äºķ', 'ĠпÑĢоÑĦеÑģÑģионалÑĮ', 'Îŀ'] |
24 | 67846'<[' | 24748'Ġhello' | 15960'Ġhi' | <[ hello hi | ['<[', 'arks', 'outh', 'ĠHam', 'la'] |
32 | 15825'-back' | 2312'ln' | 14451'UBL' | -backlnUBL | ['ÂŃi', '-back', 'Ġquestion', 'ln', 'ant'] |
40 | 15648'Ġsmile' | 14262'Welcome' | 1203'Ġback' | smileWelcome back | ['Ġsmile', 'ĠÑĥлÑĭб', 'Ġsmiled', 'ĠSmile', 'etwork'] |
48 | 15648'Ġsmile' | 21694'ĠHi' | 1203'Ġback' | smile Hi back | ['Ġsmile', 'Ġsmiled', 'ĠHello', 'Ġsmiling', 'Ġhello'] |
56 | 22691'ĠHello' | 15960'Ġhi' | 1203'Ġback' | Hello hi back | ['ĠHello', 'Ġhi', 'Ġsmile', 'Ġhello', 'Hello'] |
64 | 4773'-sm' | 24748'Ġhello' | 1203'Ġback' | -sm hello back | ['-sm', 'ĠHello', 'ĠSm', 'sm', 'Hello'] |
72 | 22691'ĠHello' | 22691'ĠHello' | 1203'Ġback' | Hello Hello back | ['ĠHello', 'Ġhello', 'Hello', 'ĠHEL', 'Ġhel'] |
80 | 271'ĊĊ' | 9906'Hello' | 0'!' | Hello! | ['ĊĊ', 'ĊĊĊ', '<|end_of_text|>', 'ĊĊĊĊ', '"ĊĊ'] |
(«Ċ» — это ещё одна особенность токенизатора, обозначающая разрыв строки. «Ġ» — это пробел.)
Аналогично, если модель хочет вывести эмодзи морского конька, то ей нужно создать скрытое состояние, схожее с вектором выходных токенов эмодзи морского конька, который, теоретически, может быть любым произвольным значением, но на практике это «seahorse + emoji» в стиле word2vec. Мы можем проверить это на примере реального эмодзи рыбы:
<|begin_of_text|><|begin_of_text|><|start_header_id|>system<|end_header_id|>
Cutting Knowledge Date: December 2023
Today Date: 26 Jul 2024
<|eot_id|><|start_header_id|>user<|end_header_id|>
Is there a fish emoji?<|eot_id|><|start_header_id|>assistant<|end_header_id|>
Yes, there is a fish emoji:
слой | токены | токены | токен 0 | ||
---|---|---|---|---|---|
0 | 1 | 2 | объединённые | (topk 5) | |
0 | 83244'ĠBail' | 15591'ĠHarr' | 5309'Ġvert' | Bail Harr vert | ['ĠBail', 'ĠPeanut', 'ĠãĢ', 'orr', 'ĠâĢĭâĢĭ'] |
8 | 122029'chyb' | 44465'ĠCaps' | 15610'iller' | chyb Capsiller | ['chyb', '...', 'ترÛĮ', 'ĠSund', 'resse'] |
16 | 1131'...' | 12676'365' | 65615'ĠSole' | ...365 Sole | ['...', '...Ċ', 'Ġboth', 'Ġindeed', 'ĠYes'] |
24 | 12'-' | 31643'ï¸ı' | 51965'ĠJackie' | -️ Jackie | ['-', '...', 'â̦', 'em', '...Ċ'] |
32 | 1131'...' | 96154'ĠGaut' | 88'y' | ... Gauty | ['...', 'â̦', '...Ċ', 'O', 'u'] |
40 | 220'Ġ' | 6"'" | 7795'Ġfish' | 'fish | ['Ġ', '...', 'â̦', 'Âł', 'u'] |
48 | 7795'Ġfish' | 7795'Ġfish' | 7795'Ġfish' | fish fish fish | ['Ġfish', 'ĠFish', 'ĠBerk', 'â̦', 'Âł'] |
56 | 7795'Ġfish' | 7795'Ġfish' | 7795'Ġfish' | fish fish fish | ['Ġfish', 'ĠFish', 'fish', 'Fish', 'é±¼'] |
64 | 7795'Ġfish' | 238'IJ' | 7795'Ġfish' | fish� fish | ['Ġfish', 'ĠFish', 'ĠPis', 'Fish', 'ĠÙħاÙĩ'] |
72 | 7795'Ġfish' | 238'IJ' | 253'Ł' | fish�� | ['Ġfish', 'ĠFish', 'ĠðŁ', 'Ġ', 'ÂŁ'] |
80 | 11410'ĠðŁ' | 238'IJ' | 253'Ł' | 🐟 | ['ĠðŁ', 'ðŁ', 'Ġ', 'ĠĊĊ', 'ĠâĻ'] |
В этом случае всё работает идеально. Модель создаёт скрытое состояние «fish + emoji» — взгляните на topk слоя 72, где есть и «fish», и префикс байтов эмодзи «ĠðŁ»; это означает, что на этом этапе скрытое состояние схоже и с «fish», и с «emoji», как и можно было ожидать. Когда этот вектор передаётся в lm_head
после последнего слоя, мы видим 🐟, как и ожидала модель.
Но в отличие от 🐟, эмодзи морского конька не существует. Модель пытается сконструировать вектор «seahorse + emoji», как делала бы это для реального эмодзи, и в слое 72 мы даже видим очень похожую с эмодзи рыбы конструкцию: « se», «horse» и префикс байтов эмодзи:
слой | токены | токены | токен 0 | ||
---|---|---|---|---|---|
0 | 1 | 2 | объединённые | (topk 5) | |
72 | 513'Ġse' | 238'IJ' | 513'Ġse' | se� se | ['Ġse', 'Ġhippoc', 'horse', 'ĠðŁ', 'Ġhorse'] |
Но, увы, у ĠðŁ нет продолжения, соответствующего seahorse, поэтому оценка схожести lm_head
вместо него выбирает максимум в виде байтов эмодзи лошади или эмодзи, связанного с морским животным, из-за чего сэмплируется не тот эмодзи, который ожидала модель.
Это сэмплирование — важная информация для модели! Это можно увидеть в примере ниже с Claude 4.5 Sonnet, где токены авторегрессивно добавляются к контексту и модель понимает, что они не образуют требуемый эмодзи морского конька. lm_head
привязывает предыдущую нечёткую концепцию «seahorse + emoji» к реально существующему эмодзи, например, к тропической рыбе или лошади.
После этого модель сама решает, что делать дальше. Некоторые модели, например, 4.5 Sonnet, пробуют снова, и рано или поздно обновляют свидетельство меняя промежуточный ответ на утверждение о том, что эмодзи морского конька не существует. Другие модели, например, gpt-5-chat, ходят кругами дольше, иногда так и не восстанавливаясь. Прочие модели остаются в блаженном неведении о некорректности эмодзи, а некоторые даже мгновенно исправляют себя, увидев лишь единственный некорректный сэмпл.
Но пока модель не получит ошибочный токен вывода от lm_head
, она просто не знает об ошибочности своего исходного убеждения о существовании эмодзи морского конька. Она может лишь предполагать, что «seahorse + emoji» создаст нужные ей токены.
Можно задаться вопросом, а не является ли эта проблема частью преимуществ обучения с подкреплением LLM — она даёт модели информацию о её lm_head
, которую в противном случае было бы сложно получить, потому что он находится в конце стека слоёв.
(Надо помнить о том, что базовые модели не обучаются на своих собственных выходных данных и пробных прогонах (rollout), это происходит только при обучении с подкреплением.)
Если вы хотите попробовать самостоятельно, то начальный скрипт можно найти на Github: https://gist.github.com/vgel/025ad6af9ac7f3bc194966b03ea68606