Osadzenia słów — word2vec i wektory, które coś znaczą
Embedding słowa to krótki wektor wyuczonych liczb. Word2vec pozwala prześledzić, jak te liczby zyskują strukturę: od okna kontekstu aż do aktualizacji gradientowej.
Znana analogia king − man + woman ≈ queen ilustruje możliwą relację wektorów. Poniższy diagram używa ręcznie rozmieszczonych punktów 2D, a nie zmierzonych wektorów word2vec. Równość przesunięć wynika z konstrukcji; rzeczywiste analogie są przybliżone i zależą od korpusu, modelu oraz metody porównania.
Klikaj przycisk step, żeby obejrzeć tę konstrukcję krok po kroku.
Schematic: hand-placed 2D points illustrate vector arithmetic.
W wytrenowanym modelu współrzędne wynikają z optymalizacji, a nie z nazwanych osi semantycznych. Niektórym relacjom mogą odpowiadać przydatne kierunki, ale nie istnieje uniwersalna współrzędna ani przesunięcie „królewskości” działające dla każdego słowa.
One-hot i TF-IDF przypisują każdemu elementowi słownika osobną współrzędną. Gęste embeddingi używają mniejszego, wspólnego zestawu wymiarów. Oba podejścia są użyteczne; embeddingi dodają wyuczony sposób porównywania słów poza ich dokładną zgodnością.
Word2vec, przedstawiony w 2013 roku, usprawnił trening wektorów słów na dużą skalę dzięki płytkim modelom predykcyjnym. Rozwijał wcześniejsze reprezentacje rozproszone; nie był początkiem idei uczonych wektorów słów.
Skip-gram przewiduje kontekst na podstawie słowa centralnego, a CBOW słowo centralne na podstawie kontekstu. Podobne użycie słów daje podobne informacje predykcyjne i może prowadzić do pokrewnych reprezentacji. To efekt statystyczny, a nie gwarancja prostego przesunięcia dla każdej relacji semantycznej.
Od hipotezy do geometrii
Hipoteza dystrybucyjna wiąże podobne konteksty z pokrewnymi znaczeniami lub rolami gramatycznymi. To użyteczna wskazówka, nie pełna definicja znaczenia: antonimy takie jak hot i cold też mogą dzielić wiele kontekstów.
Mały wymiar embeddingu ogranicza rząd macierzy wyników, sprzyjając wspólnej strukturze predykcyjnej. Nie wymusza jednak tych samych współrzędnych słów. Zamiana E na E A i E' na A⁻¹ E' dla odwracalnej macierzy A zachowuje wszystkie wyniki iloczynów skalarnych, choć może zmienić odległości i kąty między embeddingami wejściowymi.
Warto rozróżnić trzy zastosowania pojęcia „embedding”:
Statyczne embeddingi słów przypisują słowu ten sam wektor w różnych kontekstach. Embeddingi kontekstowe, jak stany ukryte BERT, zależą od otaczających tokenów. Embeddingi zdań lub fragmentów podsumowują dłuższy tekst do porównań i wyszukiwania, często po dodatkowym treningu kontrastowym. Nie muszą mieć tej samej funkcji celu.
Transformer autoregresyjny również zaczyna od tablicy embeddingów tokenów, zwykle podwyrazowych. Kolejne stany ukryte zależą od wcześniejszego kontekstu. Ich uśrednienie nie daje automatycznie dobrego embeddingu do wyszukiwania; to osobna decyzja modelowa.
Ten artykuł prowadzi od zliczeń współwystępowania do skip-gram i CBOW, a następnie wyjaśnia, co zmieniają reprezentacje kontekstowe.
Hipoteza dystrybucyjna
Już w 1957 roku językoznawca J.R. Firth napisał słynne: „Poznasz słowo po towarzystwie, w jakim przebywa”. To cała idea. Słowa pojawiające się w podobnych kontekstach mają podobne znaczenia. Żeby zobaczyć dlaczego, rozważcie trzy zdania z jednym brakującym słowem:
- ___ zaszczekał na listonosza.
- ___ mruczał mi na kolanach.
- ___ odleciał na zimę na południe.
Nie musicie znać brakujących słów, żeby wiedzieć, że odnoszą się do różnych rodzajów zwierząt. Kontekst — słowa wokół luki — zawęża to, co pasuje. To jedna połowa idei: kontekst przewiduje słowo.
W wielu zdaniach słowa dog, puppy i hound dzielą przydatne wzorce kontekstu. Model predykcyjny może ponownie wykorzystywać parametry dla tych wzorców. Wspólny kontekst może wskazywać podobieństwo, ale też związek tematyczny lub zgodność gramatyczną.
Pytanie brzmi, jak policzyć to efektywnie. Klasyczną odpowiedzią, sięgającą wczesnych lat 90., były bardzo duże rzadkie macierze współwystępowania — analiza semantyki ukrytej i jej krewni. Dla zdania „the cat sat on the mat” z oknem 2 słów (patrzymy do 2 tokenów w lewo i w prawo od każdego słowa centralnego) macierz wygląda tak:
the cat sat on mat
the [ 0, 1, 2, 1, 1 ]
cat [ 1, 0, 1, 1, 0 ]
sat [ 2, 1, 0, 1, 0 ]
on [ 1, 1, 1, 0, 1 ]
mat [ 1, 0, 0, 1, 0 ]M[i,j] liczy wystąpienia słowa j w oknie słowa i. Pomijamy centralną pozycję, a nie każde wystąpienie tego samego słowa. Tutaj przekątna jest zerowa, lecz powtarzające się słowa w oknie mogą dawać niezerowe elementy przekątnej.
Macierz buduje się przesuwając okno po korpusie i zliczając.
W każdej pozycji ze słowem centralnym i patrzymy na słowa w jego oknie ±W i dla każdego sąsiada j w tym oknie zwiększamy M[i][j] o jeden. Przewińcie widżet od początku do końca, a macierz wypełni się komórka po komórce — jedno przejście przez korpus i wszystkie zliczenia współwystępowania są zapisane.
To pierwszy krok potoku „zlicz, potem faktoryzuj”, a nad nim są jeszcze dwa. Kształt wektora słowa istotnie zmienia się na każdym etapie:
Etap 1 — surowe zliczenia. Macierz powyżej, taka jaka jest. Każdy wiersz ma długość V liczb, po jednym slocie na każde słowo w słowniku — to już jest wektor słowa, tylko szalenie przewymiarowany. Każdy wpis to dosłownie „ile razy to konkretne słowo ze słownika pojawiło się w oknie ±2 tego słowa w całym korpusie”. Wiersz dla cat to [1, 0, 1, 1, 0]. Zabawkowy korpus ma tylko sześć tokenów, więc liczby są maleńkie, ale przy skali liczy się struktura.
Etap 2 — korekta częstości. PMI porównuje prawdopodobieństwo współwystąpienia z iloczynem rozkładów brzegowych. Dla nieobserwowanych par surowe PMI jest nieokreślone lub równe minus nieskończoności, więc często używa się dodatniego PMI, PPMI = max(PMI, 0), z zerami dla nieobserwowanych wpisów. Wygładzanie i wagi wpływają na wynik.
Etap 3 — kompresja. Obcięte SVD przybliża ważoną macierz za pomocą największych wartości osobliwych i odpowiadających im wektorów. Częsta postać tablicy słów to U_d Σ_d. To przybliżenie niskiego rzędu, nie automatycznie PCA: PCA najpierw centruje dane, a ta faktoryzacja nie musi tego robić.
raw cat counts: [1, 0, 1, 1, 0]
PPMI (rounded): [0.182, 0, 0.405, 0.405, 0]
SVD representation: a row of U_d @ Sigma_dUzyskane d współrzędnych łączy informacje z wielu słów kontekstu. Poszczególne współrzędne nie muszą odpowiadać nazwanym cechom takim jak „królewskość” czy „płeć”.
Faktoryzacja zliczeń i word2vec tworzą gęste tablice, lecz optymalizują różne cele. Związek z przesuniętym PMI dotyczy skip-gram z negative sampling przy określonych założeniach; nie czyni każdego wariantu word2vec równoważnym SVD. Wyjaśniamy go w artykule o negative sampling.
Macierz słowo–kontekst ma V×V możliwych wpisów, lecz zapis rzadki nie przechowuje wszystkich zer. Zliczanie i faktoryzacja nadal mogą być kosztowne dla dużych korpusów. Word2vec aktualizuje wektory bezpośrednio z przykładów, bez budowania tej macierzy.
word2vec
Word2vec uczy tablicę embeddingów przez przewidywanie słów w lokalnych oknach. Zaczniemy od pełnego softmaxu, by pokazać przejście w przód i gradienty, a potem omówimy tańsze cele używane w praktyce.
Sama tablica przypisuje słowu jeden wektor niezależnie od zdania. Dalszy model sekwencyjny może jednak łączyć te wektory z kolejnością słów i kontekstem. Statyczne embeddingi są reprezentacją wejściową, a nie kompletnym modelem zdania.
Word2vec udostępnia dwie alternatywne architektury treningowe:
- Skip-gram. Mając słowo centralne, przewidź słowa w małym oknie wokół niego. Podajemy słowo centralne na wejściu i przewidujemy po kolei każde z otaczających słów kontekstowych — każda para
(centrum, kontekst)jest osobnym przykładem treningowym, więc to samo słowo centralne jest wykorzystywane raz na sąsiada. - CBOW (continuous bag of words). Odwrotnie: mając słowa w oknie, przewidź to centralne. Podajemy zbiór słów kontekstowych i przewidujemy jedno słowo w środku. Ten kształt „wypełnij lukę” jest tym, który później dziedziczy i skaluje maskowane modelowanie języka w BERT.
Oba robią to samo, tylko prowadzą predykcję w przeciwnych kierunkach. Przejdziemy skip-gram od początku do końca, a potem wrócimy do CBOW.
Dane treningowe i wejścia one-hot
Cały potok skip-gramu dzieli się czysto na dwa etapy z ostrą granicą: wstępne przetwarzanie zamienia surowy tekst w listę przykładów treningowych, a potem trening przepuszcza te przykłady przez sieć neuronową.
Najpierw spójrzmy na wstępne przetwarzanie — zamianę surowego tekstu w długą listę par liczb całkowitych. Żadna sieć neuronowa nie jest tu jeszcze zaangażowana:
- Tokenizuj korpus — podziel tekst na listę tokenów słownych (oddzielonych spacjami, zwykle małymi literami; word2vec używa tokenów na poziomie słów, nie podsłów).
- Zbuduj słownik — przypisz każdemu odrębnemu tokenowi liczbę całkowitą; ten identyfikator posłuży później jako indeks tokenu w wektorach one-hot i jako numer wiersza w macierzy osadzeń. Łączna liczba to
V(rozmiar słownika). - Wyodrębnij pary
(centrum, kontekst)— przesuwaj okno po strumieniu identyfikatorów i emituj po jednym przykładzie treningowym na sąsiada.
W tym przykładzie wybieramy okno po dwa tokeny z każdej strony. Jego rozmiar jest hiperparametrem; praktyczne implementacje mogą także losować mniejszy promień i pomijać część częstych słów.
Biorąc zdanie „the cat sat on the mat” jako przykład, ustawiamy okno na ±2 i przesuwamy je słowo po słowie. Gdy okno jest wyśrodkowane na sat, sąsiadami są the, cat, on, the — ta jedna pozycja emituje cztery pary: (sat, the), (sat, cat), (sat, on), (sat, the). Krok naprzód do on, a okno znajduje cat, sat, the, mat — cztery kolejne pary. Kolejny krok, kolejne cztery, i tak dalej, aż cały korpus zapada się w długą listę par (centrum, kontekst), wygenerowaną w całości z samego tekstu, bez ani jednej ludzkiej etykiety.
Pary indeksów można wygenerować krótką pętlą Pythona:
# Step 1: tokenize.
corpus = "the cat sat on the mat"
tokens = corpus.split()
# ['the', 'cat', 'sat', 'on', 'the', 'mat']
# Step 2: build vocabulary and convert tokens to integer IDs.
vocab = sorted(set(tokens)) # ['cat', 'mat', 'on', 'sat', 'the']
word2id = {w: i for i, w in enumerate(vocab)} # {'cat': 0, 'mat': 1, 'on': 2, 'sat': 3, 'the': 4}
V = len(vocab) # 5
ids = [word2id[w] for w in tokens] # [4, 0, 3, 2, 4, 1]
# Step 3: slide a ±2 window over the ID stream, emit (center, context) pairs.
window = 2
pairs = []
for i, c in enumerate(ids):
for j in range(max(0, i - window), min(len(ids), i + window + 1)):
if i != j:
pairs.append((c, ids[j]))
len(pairs) # 18 — exactly the (center_id, context_id) pairs the widget above emits.Wstępne przetwarzanie jest w zasadzie takie samo niezależnie od tego, który wariant word2vec trenujesz dalej — tokenizacja, słownik i okienkowanie są identyczne. Różni się tylko format emitowanych przykładów: skip-gram pakuje je jako pary, a CBOW emituje jeden worek kontekstu plus jego centrum na okno. Trening jest tym, gdzie faktycznie mieszka algorytm, i reszta tej sekcji omawia go szczegółowo.
Każda para to przykład treningowy
Gdy wstępne przetwarzanie jest gotowe, spójrzmy, jak algorytm treningowy wykorzystuje te pary.
Każda para dostarcza słowo wejściowe i etykietę celu. Okno wokół sat daje cztery przykłady z celami the, cat, on, the — trzy różne słowa, w tym dwa wystąpienia the. W pełnym softmaxie cel jest używany przez stratę, a nie podczas obliczania wyników przejścia w przód.
To podobne do MNIST: każdy przykład MNIST paruje obraz z etykietą cyfry, a tutaj każda para skip-gramu (c, t) paruje słowo centralne c (wejście) z jednym słowem kontekstowym t (cel). Dla (sat, cat): podaj sat, dostań przewidziany rozkład po słowniku, porównaj go z cat, zrób krok SGD. Potem kolejna para.
Zanim cokolwiek podamy do sieci, musimy przedstawić słowo jako wektor liczb.
W MNIST ten krok jest w zasadzie darmowy — obraz już jest siatką intensywności pikseli, więc po prostu spłaszczamy go w wektor 784 liczb.
Słowo nie ma wrodzonej treści liczbowej, więc wymyśliliśmy ją w kroku budowania słownika powyżej — każde słowo ma już liczbę całkowitą od 0 do V−1. Żeby podać ją do sieci, rozwijamy ten indeks w one-hot wektor długości V — wektor V liczb, same zera poza jedną 1 na indeksie tego słowa.
Dla naszego przewodniego słownika 5 słów (V=5) kodowanie wygląda tak:
"cat" → [1, 0, 0, 0, 0]
"mat" → [0, 1, 0, 0, 0]
"on" → [0, 0, 1, 0, 0]
"sat" → [0, 0, 0, 1, 0]
"the" → [0, 0, 0, 0, 1]Na szczęście jest sprytny kawałek algebry liniowej, który pozwala w ogóle nie budować wektora one-hot — pomnożenie one-hota przez macierz jest tym samym, co wybranie jednego wiersza tej macierzy (odczyt po całkowitym identyfikatorze słowa). Dla naszego 5-wyrazowego słownika, z sat na indeksie 3 i pewną macierzą M o kształcie (5, d):
one-hot for "sat" M (5 rows × 3 cols) result
[ 0 0 0 1 0 ] · [ row 0: 0.21 -0.43 0.15 ] = [ 0.33 -0.27 0.84 ]
[ row 1: 0.07 0.62 -0.31 ] (just row 3)
[ row 2: -0.55 0.18 0.40 ]
[ row 3: 0.33 -0.27 0.84 ]
[ row 4: -0.12 0.49 -0.06 ]Każdy składnik dotykający 0 z one-hota znika, zostawiając tylko wkład wiersza 3 — więc odpowiedzią jest po prostu wiersz 3 macierzy M.
# dot product, column by column:
col 0: 0·0.21 + 0·0.07 + 0·(-0.55) + 1·0.33 + 0·(-0.12) = 0.33
col 1: 0·(-0.43) + 0·0.62 + 0·0.18 + 1·(-0.27) + 0·0.49 = -0.27
col 2: 0·0.15 + 0·(-0.31) + 0·0.40 + 1·0.84 + 0·(-0.06) = 0.84Model myślowy — co próbujemy zrobić
Dla każdej pary (center, target) porównujemy wynik celu z pozostałymi słowami i dostosowujemy obie tablice embeddingów, by zmniejszyć stratę predykcji.
Trening używa nieznormalizowanego iloczynu skalarnego, a nie podobieństwa cosinusowego. Cosinus dzieli iloczyn przez normy obu wektorów i jest często używany do porównań po treningu. Softmax przekształca wyniki w rozkład, zachowując ich kolejność.
Będziemy mieć dwie trenowalne macierze wag — każda między parą warstw sieci — razem dające każdemu słowu w słowniku dwa d-wymiarowe wektory (po jednym osadzeniu na rolę):
Eo kształcie(V, d)— każdy wiersz to osadzenie wejściowe jednego słowa, używane, gdy słowo występuje jako centrum pary treningowej (słowo, na które warunkujemy).E'o kształcie(d, V)— każda kolumna to osadzenie wyjściowe jednego słowa, używane, gdy słowo występuje jako cel/kontekst, który jest przewidywany (słowo oceniane jako kandydat).
E i E' są niezależnymi macierzami parametrów, a nie swoimi transpozycjami. Układ umieszcza wektory wejściowe w wierszach, a wyjściowe w kolumnach. Każde słowo ma osobny uczony wektor dla każdej roli.
Osobne tablice pozwalają słowu pełnić różne role wejściowe i wyjściowe. Po treningu często używa się E, ale można też wykorzystać wektory wyjściowe lub połączyć obie tablice. Przydatność zależy od zadania docelowego.
Widżet poniżej czyni ten krok konkretnym — liczenie podobieństwa w poprzek wymiarów między słowem centralnym a każdym słowem słownika, interakcja E i E'. Skupiamy się na sat jako centrum (podświetlone na zielono w E). Przejdź krokami, by zobaczyć, jak mnożenie macierzy punktuje je względem każdego słowa słownika — po jednym iloczynie skalarnym na słowo, wypełniając wektor wyników wpis po wpisie. (Tylko etapy 1 i 2 — softmax i strata przyjdą parę sekcji później.)
Wynik to v_c · v'_t. Przy wektorach wyjściowych w kolumnach scores = v_c @ E' oblicza naraz wyniki całego słownika. Jeśli są w wierszach Ep, równoważny zapis to scores = Ep @ v_c; żaden układ nie wymaga pętli Pythona.
Podobnie jak w warstwie wyjściowej klasyfikatora każdy kandydat ma wektor wag oceniający reprezentację ukrytą. Wymiary embeddingu są uczone wspólnie i zwykle nie odpowiadają pojedynczym, nazwanym cechom semantycznym.
Architektura
Gdy mamy już zebrane pary, w istocie prowadzimy nadzorowane zadanie predykcji z etykietą — mając słowo centralne, przewidzieć, które słowo ze słownika pojawi się obok — więc układ jest podobny do MNIST w kluczowych aspektach: jedna warstwa ukryta, softmax po klasach wyjściowych, entropia krzyżowa względem etykiety. Różni się skala (tu rozmiar słownika V kontra 10 klas cyfr w MNIST), format wejścia (one-hot kontra gęste rzeczywiste piksele) i cel (chcemy wytrenowanych osadzeń, nie samej predykcji).
Architektonicznie word2vec (2013) to dwuwarstwowa sieć neuronowa jednokierunkowa — sieć feed-forward z dwóch w pełni połączonych warstw (dwie macierze wag E i E'), z danymi płynącymi wejście → ukryta → wyjście i bez pętli. To ta sama rodzina co klasyfikator MNIST, z jednym twistem: warstwa ukryta jest czysto liniowa, więc uczy się kodowań słów, zamiast działać jako nieliniowy ekstraktor cech.
Konkretnie: wejście to V-wymiarowy one-hot słowa centralnego; warstwa ukryta ma d neuronów (np. 300) i jest czysto liniowa (bez obciążenia, bez nieliniowości); wyjście ma V neuronów z softmaxem po wszystkich V, produkującym P(w | c) — prawdopodobieństwo każdego słowa słownika przy danym centrum. Dwie macierze E i E', które wprowadziliśmy w poprzedniej sekcji, żyją między tymi warstwami: E to macierz wag wejście → ukryta (kształt (V, d)), E' to ukryta → wyjście (kształt (d, V)). To jedyne wyuczone parametry sieci; obie startują losowo, a po treningu tylko E wychodzi jako finalna tablica osadzeń słów — E' jest wyrzucane.
Oto architektura z pełnym softmaxem w Keras, dla powyższego małego słownika:
from tensorflow import keras
from tensorflow.keras import layers
V = len(vocab) # five words from the example above
d = 3
model = keras.Sequential([
keras.Input(shape=(1,), dtype='int32'), # integer ID of the center word
layers.Embedding(input_dim=V, output_dim=d), # E: shape (V, d), the lookup
layers.Reshape((d,)), # (1, d) → (d,)
layers.Dense(V, use_bias=False), # E': shape (d, V), the linear layer
layers.Softmax(), # softmax over V vocab scores
])
model.compile(optimizer='sgd', loss='sparse_categorical_crossentropy')Embedding(V, d) pobiera wektory wejściowe, a Dense(V, use_bias=False) przechowuje wagi wyjściowe. Wejście ma kształt (batch, 1), więc wyjście embeddingu to (batch, 1, d). Reshape usuwa oś jednoelementowej sekwencji przed obliczaniem wyników. Między uczonymi warstwami nie ma nieliniowej aktywacji.
Softmax zamienia V surowych wyników w rozkład prawdopodobieństwa po słowniku, a sparse_categorical_crossentropy to standardowa strata klasyfikacyjna na wierzchu — ten sam softmax-plus-entropia-krzyżowa, którego używa MNIST, tylko z V klasami słownika zamiast 10 cyfr. Stratę i jej gradient omówimy szczegółowo w kolejnych sekcjach.
Wymiar d jest hiperparametrem wpływającym na pojemność, pamięć i obliczenia. Często używa się kilkuset wymiarów, ale właściwy rozmiar zależy od korpusu i zadania, nie tylko od wielkości słownika.
To, co właśnie opisaliśmy, to bazowy przebieg w przód — softmax po całym słowniku po stronie wyjścia.
Przebieg w przód — odczyt, punktowanie, softmax
Skoro omówiliśmy architekturę na wysokim poziomie — wejściowy one-hot, ukryty odczyt, wyjściowe wyniki — przybliżmy i prześledźmy dokładnie, co się dzieje, gdy jeden przykład treningowy płynie przez sieć. Dla słowa centralnego c przebieg w przód idzie od lewej do prawej w trzech etapach: odczyt, punktowanie i softmax.
Widżet z sekcji o modelu myślowym pokazał już pierwsze dwa etapy w izolacji — one-hot razy macierz zapadający się w odczyt wiersza, a potem wiersz razy macierz produkujący V wyników. Tutaj je nazwiemy, dołożymy softmax na wierzch i prześledzimy liczby od początku do końca.
Etap 1: wejście × E → ukryta (odczyt). Matematycznie to mnożenie macierzy one_hot(c) @ E, produkujące d-wymiarowy wektor ukryty. Wejście jest rzadkie — V−1 wpisów to zera — więc niemal każde mnożenie daje zero, a całe mnożenie (V, d) zapada się w odczyt jednego wiersza: v_c = E[c].
Etap 2: warstwa ukryta × E’ → wyniki słownika. scores = v_c @ E' oblicza jeden iloczyn skalarny na słowo wyjściowe, z kosztem O(Vd). Propagacja wsteczna może dać gradienty wszystkim kolumnom wyjściowym i wybranemu wierszowi wejściowemu.
Mnożenie nigdy nie patrzy na słowo docelowe. Używa wyłącznie v_c = E[sat] i punktuje sat względem całego słownika, produkując wszystkie V wyników naraz. Zatem te konkretne pięć wyników — cat -0.19, mat 0.26, on 0.39, sat -0.07, the -0.45 — są identyczne dla każdej pary treningowej dzielącej centrum sat: (sat, cat), (sat, on) i (sat, the) wykonują to samo mnożenie i lądują na tych samych pięciu liczbach. Słowo docelowe wchodzi dopiero później, przy stracie; przebieg w przód nigdy go nie widzi.
Etap 3: softmax → prawdopodobieństwa. V wyników — zwanych logitami — to dowolne liczby rzeczywiste: mogą być ujemne, nieograniczone, nie sumujące się do niczego szczególnego. Softmax zamienia je w P(w | c) — V nieujemnych liczb sumujących się do 1, przewidziane przez model prawdopodobieństwo, że słowo w jest w kontekście c.
Na tym samym 5-wyrazowym słowniku widżet poniżej przechodzi wszystkie trzy etapy. Wybierz parę treningową (center, context), potem naciskaj step, by wypełniać wektor wyników jeden iloczyn skalarny na raz; gdy wszystkie V wyników są na miejscu, softmax zamienia je w prawdopodobieństwa.
W tym ręcznie dobranym przykładzie liczbowym on ma najwyższy wynik, około 0.39. Liczby ilustrują obliczenia; nie dowodzą, że ten mały model nauczył się zdania.
W skip-gramie te wyniki to iloczyny skalarne v_c · E'[:, w] dla każdego słowa słownika w, a wynikające prawdopodobieństwa to P(w | c). Wyciągając sam krok softmaxu z widżetu powyżej, pięć wyników [-0.19, 0.26, 0.39, -0.07, -0.45] zamienia się w porządny rozkład prawdopodobieństwa sumujący się do 1:
Softmax potęguje wyniki funkcją wykładniczą i dzieli przez ich sumę. Implementacje najpierw odejmują największy wynik dla stabilności numerycznej; nie zmienia to prawdopodobieństw. Wyjście sumuje się do jedności, lecz sama normalizacja nie gwarantuje kalibracji predykcji.
Funkcja straty
Strata dla jednego przykładu treningowego ze słowem docelowym t to ujemny logarytm prawdopodobieństwa, jakie model przypisał temu celowi:
loss = −log P(t | c)To daje jedną liczbę na parę treningową (c, t) — małą, gdy softmax nasypał prawdopodobieństwa na prawdziwy cel, dużą, gdy nie. To entropia krzyżowa z etykietą one-hot — ta sama strata, której używa MNIST, a gradient przez softmax jest tam wyprowadzony.
Konkretnie, dla pary (sat, cat) przy wynikach i prawdopodobieństwach z góry:
cat mat on sat the
scores = [-0.19, 0.26, 0.39, -0.07, -0.45]
probabilities = [ 0.16, 0.25, 0.29, 0.18, 0.12]
target = cat
P(cat | sat) = 0.16
loss = −log(0.16) ≈ 1.83
# what-ifs — how the loss responds to different P(cat):
P(cat) = 0.90 → loss = −log(0.90) ≈ 0.11 (good prediction)
P(cat) = 0.01 → loss = −log(0.01) ≈ 4.6 (bad prediction)Widżet poniżej przenosi pięć prawdopodobieństw softmaxu z góry i nanosi je na krzywą −log. Kliknij inne słowo, by wyznaczyć je jako cel — znacznik przesuwa się po krzywej i widać wprost, jak cel, który model już faworyzuje, kosztuje prawie nic, podczas gdy cel niedoważony płaci ostrą cenę.
Strata rośnie, gdy prawdopodobieństwo celu zbliża się do zera. Jednak po zróżniczkowaniu przez softmax gradient względem logitu wynosi P(w) − 1[w=t] i mieści się między −1 a 1. Gradienty parametrów zależą też od użytych wektorów.
Rozkład jednostajny po pięciu słowach daje każdemu celowi prawdopodobieństwo 0.20 i stratę log(5) ≈ 1.61. Nasz przykład daje cat około 0.16, więc strata jest wyższa. Rozkład jednostajny to użyteczny punkt odniesienia, nie cecha każdej losowej inicjalizacji.
Gradient
Gradient jest podobny do MNIST: gradient straty względem każdego logitu to P(w) − 𝟙[w == t] — przewidziane prawdopodobieństwo minus cel one-hot. Gradient słowa docelowego to P(t) − 1 (ujemny — popchnij jego wynik w górę); każdego innego słowa to P(w) (dodatni — popchnij jego wynik w dół, proporcjonalnie do tego, ile prawdopodobieństwa aktualnie ma). Słowa, które model już poprawnie uważa za mało prawdopodobne, ledwie drgną; słowa, co do których się myli, dostają najwięcej sygnału.
Widżet poniżej czyni to odejmowanie konkretnym na tych samych pięciu prawdopodobieństwach. Kliknij inny cel, by zobaczyć, jak wiersz gradientu się przerysowuje — jeden wysoki niebieski słupek ciągnący wynik celu w górę i cztery krótkie czerwone popychające pozostałe w dół.
Propagacja wsteczna przenosi te gradienty wyników do E' i E przez regułę łańcuchową:
scores = v_c @ E' (the forward step we're differentiating)
∂loss / ∂E'[:, w] = ( P(w) − 𝟙[w == t] ) · v_c ← gradient on column w of E'
∂loss / ∂v_c = E' @ ( P − one_hot_t ) ← gradient into the hidden vector
∂loss / ∂E[c] = ∂loss / ∂v_c ← because v_c = E[c]Dwie konsekwencje warte zapamiętania, obie z wejścia one-hot:
Lookup daje gradient tylko wierszowi E[c]; pozostałe wiersze wejściowe mają dla tego przykładu gradient zerowy. Pełny softmax obejmuje każdą kolumnę E'. Gradienty dotyczą wyników iloczynów skalarnych i nie gwarantują zmian wszystkich odległości zgodnych z metaforą „przyciągania i odpychania”.
Te gradienty są następnie użyte do wykonania kroku spadku gradientu: zastosuj je do wag jako E -= lr × ∂L/∂E i E' -= lr × ∂L/∂E'. To zadanie optymalizatora, a wybór optymalizatora (zwykły SGD, momentum, Adam, RMSprop, AdaGrad…) ma znaczenie tylko na tym kroku — wszystkie konsumują te same gradienty, ale używają ich inaczej.
Mini-partie i epoki
Powyższe omówienie przetwarzało pary po jednej. Większość treningów sieci uogólnia to w SGD na mini-partiach — grupuj przykłady w partie po B i przetwarzaj całą partię w jednym przebiegu w przód + wstecz, dokładnie ten sam SGD na mini-partiach co w MNIST, tylko uśredniony po B przykładach na krok. To przepis, którego używają BERT, GPT i praktycznie każdy współczesny model, i to opisują punkty poniżej.
Oryginalny word2vec i Gensim stosują zoptymalizowany trening na CPU z asynchronicznymi aktualizacjami wątków. Poniższy opis mini-batchy z pełnym softmaxem jest ogólnym modelem odniesienia, a nie opisem wewnętrznej pętli Gensim.
Epoka to przejście przez korpus lub przygotowane przykłady. Jeśli implementacja losuje okna kontekstu lub pomija częste tokeny, kolejne epoki nie muszą zawierać identycznych par słów.
Na partię pętla wygląda tak:
- Przebieg w przód na partii B przykładów — każda para
(c, t)płynie przez sieć. Zwektoryzowany odczyt układa B wierszy zEw macierz(B, d), krok punktowania staje się jednym mnożeniem(B, d) @ (d, V) → (B, V), a softmax działa wierszami. - Strata — policz entropię krzyżową na przykład (ten sam wzór co przy B=1), potem uśrednij po partii do jednego skalara.
- Przebieg wsteczny (backprop). Policz gradienty przez regułę łańcuchową — czysta analiza od straty wstecz do każdej wagi, jeszcze bez aktualizacji wag. Wyjście: tensory gradientów
∂L/∂Ei∂L/∂E', tego samego kształtu co tensory wag. - Krok spadku gradientu — zastosuj gradienty do wag przez optymalizator, dokładnie jak w przypadku pojedynczej pary powyżej. Powtórz dla kolejnej partii.
Uruchom tę pętlę przez wiele partii na epokę i garść epok po korpusie, a wiersze E i E' osiadają w geometrii, którą strata preferuje.
CBOW — i most do BERT-a
CBOW (continuous bag of words) to drugi algorytm word2vec, prowadzony jako lustrzane odbicie skip-gramu. Tam, gdzie skip-gram bierze słowo centralne i przewiduje słowo docelowe jako jego kontekst, CBOW bierze kilka słów jako kontekst i przewiduje centrum — brakujące słowo.
CBOW i maskowane modelowanie języka w BERT przewidują słowo lub token z otaczającego kontekstu. Reprezentacje są jednak inne: CBOW uśrednia mały, nieuporządkowany worek słów, a BERT tworzy kontekstowe stany uwzględniające pozycje za pomocą transformera. BERT nie jest po prostu CBOW z większym oknem.
Mechanicznie CBOW różni się od skip-gramu dokładnie jednym krokiem uśredniania na wejściu; reszta pętli treningowej jest identyczna. Więc większość powyższych sekcji o skip-gramie przenosi się bez zmian — skupiamy się tu tylko na tym, gdzie CBOW się różni.
Wstępne przetwarzanie — worki kontekstu zamiast par
Potok wstępnego przetwarzania jest ten sam co w skip-gramie: tokenizuj, zbuduj słownik, przesuwaj okno po strumieniu tokenów. Zmienia się kształt tego, co jest emitowane na pozycję okna:
Position centered on: Skip-gram emits (per position): CBOW emits (per position):
───────────────────── ────────────────────────────── ──────────────────────────
the (pos 0) (the, cat), (the, sat) ({cat, sat}, the)
cat (pos 1) (cat, the), (cat, sat), (cat, on) ({the, sat, on}, cat)
sat (pos 2) (sat, the), (sat, cat), ({the, cat, on, the}, sat)
(sat, on), (sat, the)
on (pos 3) (on, cat), (on, sat), ({cat, sat, the, mat}, on)
(on, the), (on, mat)
the (pos 4) (the, sat), (the, on), (the, mat) ({sat, on, mat}, the)
mat (pos 5) (mat, on), (mat, the) ({on, the}, mat)
───────────────────── ────────────────
total: 18 pairs total: 6 examplesTen sam korpus, to samo okno — skip-gram produkuje 18 osobnych par treningowych (centrum, sąsiad), CBOW produkuje 6 przykładów (worek_kontekstu, centrum). Przejdźcie to krokami w widżecie:
Architektura
Architektura CBOW to architektura skip-gramu z odwróconymi końcami — wejściami są słowa kontekstowe, wyjściem słowo centralne. Te same dwie macierze wag E i E', ta sama wymiarowość d, ten sam softmax po V. Jedyna różnica architektoniczna jest po stronie wejścia: skip-gram odczytuje jeden wiersz E (centrum), CBOW odczytuje C wierszy (po jednym na słowo kontekstowe) i uśrednia je w jeden d-wymiarowy wektor ukryty h:
Skip-gram: h = E[c] (one row read)
CBOW: h = mean(E[c_1], E[c_2], ..., E[c_C]) (C rows read, then averaged)Zwizualizowane poniżej — C wejść one-hot, C odpowiadających im odczytów wierszy w E oraz krok uśredniania, który produkuje h:
shape: C × V
shape: V × d
shape: d
Uśrednianie jest tym, co nadaje „workowi słów” nazwę — kolejność słów wewnątrz okna jest odrzucana, kontekst staje się multizbiorem. Na każdym kroku treningowym odczyty odbywają się na bieżących, „w locie” wartościach E — wiersze, które czytasz w przebiegu w przód, są tymi samymi, które aktualizujesz w przebiegu wstecznym, tak jak w każdym treningu SGD sieci neuronowej. Gdy h jest policzone, wszystko dalej — scores = h @ E', softmax, strata entropii krzyżowej, backprop, SGD na E i E' oraz skrót z negatywnym próbkowaniem — jest identyczne ze skip-gramem.
Trening — trzy drobne różnice względem skip-gramu
Przebieg w przód, strata, gradient, mini-partie i skrót z negatywnym próbkowaniem przenoszą się ze skip-gramu bez zmian. Trzy różnice warte zapamiętania:
CBOW daje jedną predykcję na okno, a skip-gram jedną na wystąpienie słowa kontekstu, więc CBOW często wymaga mniej pracy warstwy wyjściowej. Przy średniej kontekstu każde wystąpienie wejściowe otrzymuje grad_h / C; powtórzone indeksy słów wymagają sumowania wkładów. Pełny softmax aktualizuje wszystkie kolumny wyjściowe, a negative sampling tylko wybrane wektory. Szybkość i jakość zależą od danych oraz ustawień.
Jak word2vec trenuje się w praktyce — Gensim
Word2Vec w Gensim obsługuje budowanie słownika, próbkowanie kontekstu, negative sampling i pobieranie wektorów. sentences to iterowalny zbiór zdań podzielonych na tokeny. Ten mały przykład pokazuje API; jest zbyt mały, by uczyć przydatnych analogii semantycznych.
from gensim.models import Word2Vec
sentences = [
"the king sat in the palace".split(),
"the queen sat in the palace".split(),
"the king and the queen ruled".split(),
]
model = Word2Vec(sentences, vector_size=50, window=2, min_count=1,
sg=1, negative=5, hs=0, workers=1, seed=42, epochs=20)
v_king = model.wv["king"]Trening na CPU i dostęp do pamięci
Przy negative samplingu jedna para dotyka tylko kilku wektorów. Wiele implementacji przechowuje obie tablice wierszami; we wcześniejszym układzie kolumnowym dostęp do wyjścia to E'[:, w]. Przybliżona praca na parę:
1. Read the input vector: E[center]
2. Read output vectors: target + k sampled negatives
3. Compute scores: k+1 dot products, each length d
4. Accumulate gradients: one input row and sampled output vectors
5. Apply the SGD update: repeated indices receive summed contributionsDwie tablice float32 zajmują 2 × V × d × 4 bajtów: około 2,4 GB dla miliona słów i 300 wymiarów, bez słownika i stanu optymalizatora. Małe, nieregularne aktualizacje mogą dobrze pasować do CPU, lecz wydajność GPU zależy od batchowania, układu pamięci i implementacji. Word2vec nie musi być szybszy na CPU na każdym sprzęcie.
Alternatywy dla pełnego softmaxu
Opisany wcześniej model oblicza wynik każdego słowa, z kosztem O(Vd) na przykład. Przykład Gensim używa zamiast tego negative samplingu. Dwa znane sposoby uniknięcia obliczeń dla całego słownika to:
Negative sampling zastępuje klasyfikację po słowniku losowanymi etykietami binarnymi, z kosztem O((k+1)d) dla pozytywu i k negatywów. Optymalizuje inny cel; nie jest nieobciążonym przybliżeniem gradientu softmaxu. Następny artykuł wyprowadza stratę i pokazuje działającą aktualizację.
Hierarchiczny softmax umieszcza słowa w liściach drzewa binarnego i przypisuje wektor każdemu węzłowi wewnętrznemu. Prawdopodobieństwo słowa jest iloczynem odpowiednich decyzji lewo/prawo na ścieżce, z jednym iloczynem skalarnym na węzeł. Zrównoważone drzewo ma ścieżki rzędu log₂V; drzewo Huffmana daje częstym słowom krótsze ścieżki. Prawdopodobieństwa liści tworzą znormalizowany rozkład.
Hierarchiczny softmax jest dostępny w Gensim, na przykład z hs=1, negative=0. Wybór zależy od kosztu treningu i potrzeb zadania; nie jest to przestarzały algorytm, który zawsze przegrywa z negative samplingiem.
Gdzie statyczne osadzenia się psują
word2vec produkuje statyczne osadzenia: jeden stały wektor na słowo, niezależnie od kontekstu. To dokładnie właściwy kształt dla hipotezy dystrybucyjnej w pierwotnym sformułowaniu, ale ma trzy tryby awarii, które stawały się coraz bardziej widoczne, gdy NLP przechodziło do trudniejszych zadań.
Polisemia
Rozważcie te dwa zdania:
- I deposited the cheque at the bank. (Wpłaciłem czek w banku.)
- We had a picnic on the river bank. (Urządziliśmy piknik na brzegu rzeki.)
Statyczny embedding przypisuje bank jeden wektor w obu zdaniach. Łączy informacje z różnych użyć, nie wskazując znaczenia w danym wystąpieniu. Nie musi to być średnia arytmetyczna osobnych wektorów znaczeń.
bank with two sense clustersglove-wiki-gigaword-300Widget porównuje bank ze słowami finansowymi i związanymi z rzeką w jednym wytrenowanym modelu. Podobieństwa zależą od modelu i korpusu. Dalszy model widzący otaczające słowa nadal może rozstrzygnąć znaczenie zdania; sam statyczny wektor tego nie zapewnia.
Brak wrażliwości na składnię
Reprezentacja typu worek wektorów wyrzuca kolejność słów. Zdania:
- Pies gryzie człowieka.
- Człowiek gryzie psa.
mają ten sam worek słów, ale inną kolejność tokenów. Uśrednienie statycznych wektorów daje tę samą reprezentację. Zachowanie ich jako sekwencji przechowuje kolejność, z której może korzystać RNN, konwolucja lub transformer.
Pokrycie słownika i adaptacja
„Statyczny” oznacza niezależny od kontekstu, nie na zawsze zamrożony. Wektory można dostrajać lub trenować ponownie. Zwykła tablica word2vec nie ma wpisu dla nieznanego słowa, więc wymaga polityki obsługi takich tokenów lub innej reprezentacji dla rosnącego słownika.
fastText częściowo rozwiązuje problem nieznanych słów, składając wektory z n-gramów znakowych. Reprezentacje pozostają niezależne od kontekstu; obsługa nowej pisowni i rozstrzyganie znaczenia w zdaniu to osobne problemy.
Co dalej
ELMo używało stanów dwukierunkowego modelu języka do reprezentowania tokenów w kontekście; BERT korzysta z enkodera transformerowego trenowanego do przewidywania zamaskowanych tokenów. Dalszy ciąg funkcji celu opisuje artykuł o negative sampling i uczeniu kontrastowym, rozróżniający stratę sigmoid word2vec i softmax CLIP.