Źródło: vgel.me
To zredagowana i rozszerzona wersja wpisu z Twittera, pierwotnie będącego odpowiedzią dla @arm1st1ce – oryginał znajdziesz tutaj: https://x.com/voooooogel/status/1964465679647887838
Czy istnieje emoji konika morskiego? Zapytajmy GPT-5 Instant:

Co jest? Zapytajmy w takim razie Claude Sonnet 4.5:

Co tu się dzieje? Może Gemini 2.5 Pro poradzi sobie lepiej?

No dobrze, coś tu ewidentnie jest na rzeczy. Sprawdźmy dlaczego.
Oto odpowiedzi, jakie dostaniemy, pytając kilka modeli sto razy, czy emoji konika morskiego istnieje – tak czy nie:
Is there a seahorse emoji, yes or no? Respond with one word, no punctuation.
Jak widać, popularne modele językowe są bardzo pewne, że emoji konika morskiego istnieje. I nie są w tej pewności osamotnione – oto wątek z Reddita z setkami komentarzy od ludzi, którzy wyraźnie pamiętają, że emoji konika morskiego kiedyś było:

Takich rzeczy jest mnóstwo – wpiszcie w Google „seahorse emoji", a znajdziecie TikToki, filmy na YouTubie, a nawet (już martwe) memecoiny zbudowane wokół rzekomego zniknięcia emoji konika morskiego, które – wszyscy są tego niemal pewni – kiedyś istniało. Tyle że oczywiście nigdy nie istniało.
Może LLM-y wierzą w emoji konika morskiego, bo wierzy w nie tylu ludzi w danych treningowych. A może to przekonanie zbieżne – skoro w Unicode jest tyle innych wodnych zwierząt, to i ludzie, i LLM-y mają prawo założyć (a wręcz wygeneralizować), że tak urocze stworzenie też się tam znajdzie. Emoji konika morskiego zostało nawet w pewnym momencie oficjalnie zgłoszone, ale w 2018 roku propozycję odrzucono.
Niezależnie od pierwotnej przyczyny, wiele LLM-ów zaczyna każde nowe okno kontekstu z błędnym, utajonym przekonaniem, że emoji konika morskiego istnieje. Ale czemu prowadzi to do tak dziwnych zachowań? Sama kiedyś wierzyłam, że taki emoji istnieje – ale gdybym spróbowała wysłać go znajomemu, po prostu poszukałabym go na klawiaturze i zorientowała się, że go tam nie ma. Nie wysłałabym niewłaściwego emoji, żeby potem wpaść w spiralę emoji-spamu. Co więc dzieje się we wnętrzu LLM-a, że zachowuje się właśnie tak?
Przyjrzyjmy się temu za pomocą ulubionego, niedocenianego narzędzia interpretowalności – logit lens!
Użyjemy tego prefiksu promptu – szablonu czatu z domyślnym promptem systemowym llama-3.3-70b, pytaniem o emoji konika morskiego i częściową odpowiedzią modelu, uciętą tuż przed samym emoji:
<|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:
Możemy wziąć z modelu
lm_head– zwykle stosowaną tylko do wyjścia ostatniej warstwy – i przyłożyć ją do każdej warstwy, uzyskując pośrednie predykcje tokenów. W ten sposób powstaje poniższa tabela: dla co czwartej warstwy pokazuje najbardziej prawdopodobny token na trzech kolejnych pozycjach po prefiksie (tokeny 0, 1 i 2) oraz pięć najbardziej prawdopodobnych predykcji dla pierwszej pozycji (token 0, topk 5):
| warstwa | tokeny | tokeny | token 0 | ||
|---|---|---|---|---|---|
| 0 | 1 | 2 | scalone | (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'] |
Na tym właśnie polega logit lens: bierzemy z modelu
lm_headi używamy jej do wyliczenia logitów (prawdopodobieństw tokenów), żeby zajrzeć w jego stany wewnętrzne. Uwaga: tokeny i prawdopodobieństwa uzyskane tutaj przez logit lens nie są równoważne pełnym stanom wewnętrznym modelu! Do tego potrzebowalibyśmy bardziej złożonej techniki, jak representation reading czy rzadkie autoenkodery. To tylko soczewka nałożona na ten stan – pokazuje, jaki token wyszedłby na wyjściu, gdyby dana warstwa była ostatnią. Mimo tego ograniczenia logit lens wciąż jest użyteczna. Stany wczesnych warstw bywają przez nią trudne do interpretacji, ale posuwając się w górę stosu, widzimy, jak model iteracyjnie doszlifowuje te stany w stronę ostatecznej predykcji – emoji ryby.
(Czemu niescalone tokeny wyglądają jak bełkot w stylu 'ĠðŁ', 'IJ', 'ł'? To dziwactwo tokenizera – te tokeny kodują bajty UTF-8 emoji ryby. Dla tego wpisu nie ma to znaczenia, ale jeśli jesteście ciekawi, poproście Claude'a albo swój ulubiony LLM o wyjaśnienie tego akapitu i tej linijki kodu:
bytes([bpe_byte_decoder[c] for c in 'ĠðŁIJł']).decode('utf-8') == ' 🐠')Spójrzcie jednak, co dzieje się w środkowych warstwach – to ani dziwactwa wczesnych warstw, ani bajty emoji z finalnej predykcji! Zamiast tego dostajemy słowa związane z użytecznymi pojęciami, konkretnie z pojęciem konika morskiego. Na przykład w warstwie 52 mamy „sea horse horse" – trzy pozycje rezydualne z rzędu kodujące pojęcie „konika morskiego". Dalej, w top-k dla pierwszej pozycji, pojawia się mieszanka „sea", „horse" i prefiksu sekwencji bajtów emoji, „ĠðŁ".
O czym więc myśli model? O „koniku morskim + emoji"! Próbuje zbudować rezydualną reprezentację konika morskiego połączonego z emoji. Po co miałby budować taką kombinację? Cóż, przyjrzyjmy się, jak tak naprawdę działa
lm_head.
lm_head
W modelu językowym
lm_headto ogromna macierz wektorów o rozmiarze rezyduum, powiązanych z identyfikatorami tokenów – po jednym na każdy token słownika (~300 000). Gdy trafia do niej rezyduum – czy to po normalnym przepłynięciu przez cały model, czy wcześniej, bo ktoś patrzy przez logit lens na wcześniejszą warstwę –
lm_headporównuje wejściowe rezyduum z każdym wektorem w tej wielkiej macierzy i (we współpracy z samplerem) wybiera identyfikator tokenu powiązany z tym wektorem, który jest najbardziej podobny do wejściowego rezyduum.
(Bardziej technicznie:
lm_headto warstwa liniowa bez biasu, więc
x @ w.Tliczy iloczyny skalarne z każdym wektorem unembeddingu, dając surowe wyniki. Potem standardowy log_softmax i argmax albo próbkowanie z temperaturą.
To znaczy, że jeśli model chce wypisać słowo „hello" – na przykład w odpowiedzi na przyjazne powitanie użytkownika – musi zbudować rezyduum możliwie podobne do wektora tokenu „hello", które
lm_headzamieni potem na identyfikator tego tokenu. I dzięki logit lens widzimy, że dokładnie tak się dzieje w odpowiedzi na „Hello :-)":
| warstwa | tokeny | tokeny | token 0 | ||
|---|---|---|---|---|---|
| 0 | 1 | 2 | scalone | (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|>', 'ĊĊĊĊ', '"ĊĊ'] |
('Ċ' to kolejne dziwactwo tokenizera – oznacza znak nowej linii. 'Ġ' analogicznie oznacza spację.)
Analogicznie: jeśli model chce wypisać emoji konika morskiego, musi zbudować rezyduum podobne do wektora tokenu (czy tokenów) wyjściowych tego emoji – który w teorii mógłby mieć dowolną wartość, ale w praktyce jest to „konik morski + emoji", w stylu word2vec. Możemy zobaczyć to w akcji na prawdziwym emoji – emoji ryby:
<|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:
| warstwa | tokeny | tokeny | token 0 | ||
|---|---|---|---|---|---|
| 0 | 1 | 2 | scalone | (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'Ł' | 🐟 | ['ĠðŁ', 'ðŁ', 'Ġ', 'ĠĊĊ', 'ĠâĻ'] |
Tutaj wszystko działa idealnie. Model buduje rezyduum „ryba + emoji" – spójrzcie na topk warstwy 72, gdzie mamy zarówno „fish", jak i prefiks bajtów emoji „ĠðŁ" – czyli rezyduum jest w tym momencie podobne jednocześnie do „ryby" i do „emoji", dokładnie tak, jak byśmy się spodziewali. A gdy ten wektor po ostatniej warstwie trafia do
lm_head, widzimy 🐟 – dokładnie tak, jak model się spodziewał.
Ale w odróżnieniu od 🐟, emoji konika morskiego nie istnieje. Model próbuje zbudować wektor „konik morski + emoji" dokładnie tak, jak zrobiłby to dla prawdziwego emoji – i w warstwie 72 dostajemy nawet konstrukcję bardzo podobną do tej z emoji ryby: „ se", „horse" i prefiks bajtów emoji:
| warstwa | tokeny | tokeny | token 0 | ||
|---|---|---|---|---|---|
| 0 | 1 | 2 | scalone | (topk 5) | |
| 72 | 513'Ġse' | 238'IJ' | 513'Ġse' | se� se | ['Ġse', 'Ġhippoc', 'horse', 'ĠðŁ', 'Ġhorse'] |
Niestety, nie istnieje żadna kontynuacja ĠðŁ odpowiadająca konikowi morskiemu, więc rachunek podobieństwa w
lm_headosiąga maksimum na bajtach emoji związanych z koniem albo morskimi zwierzętami – i próbkowane jest emoji, którego nikt nie zamawiał.
A takie próbkowanie to dla modelu cenna informacja! Widać to np. w poniższym przykładzie z Claude 4.5 Sonnet: gdy tokeny autoregresyjnie doklejają się do kontekstu, model potrafi rozpoznać, że nie tworzą zamierzonego emoji konika morskiego. Wcześniejsze, rozmyte pojęcie „konik morski + emoji" zostało przez
lm_head„przyciągnięte" do emoji, które naprawdę istnieje – jak rybka tropikalna czy koń.

Co dalej – to już zależy od modelu. Niektóre, jak 4.5 Sonnet, próbują jeszcze raz i w końcu wyciągają wnioski z dowodów, zmieniając w połowie odpowiedzi kurs na stwierdzenie, że emoji konika morskiego nie istnieje. Inne, jak gpt-5-chat, kręcą się w spirali dłużej i czasem nigdy z niej nie wychodzą. Jeszcze inne błogo ignorują to, że emoji jest błędne, a niektóre poprawiają się natychmiast, po zobaczeniu jednej jedynej nietrafionej próbki.
Ale dopóki model nie dostanie z
lm_headbłędnego tokenu wyjściowego, po prostu nie wie, że jego początkowe przekonanie o istnieniu emoji konika morskiego było mylne. Może jedynie zakładać, że „konik morski + emoji" wyprodukuje tokeny, o które mu chodzi.
Spekulując dalej: zastanawiam się, czy ten problem nie tłumaczy części korzyści, jakie LLM-om daje uczenie ze wzmocnieniem – dostarcza ono modelowi informacji o jego własnej
lm_head, do których inaczej trudno mu się dobrać, bo siedzi ona na samym końcu stosu warstw.
(Pamiętajcie, że modele bazowe nie są trenowane na własnych wyjściach ani rolloutach. To dzieje się dopiero w RL.)
Jeśli chcecie sprawdzić to na własną rękę, startowy skrypt znajdziecie na GitHubie: https://gist.github.com/vgel/025ad6af9ac7f3bc194966b03ea68606