Reprezentowanie słów za pomocą osadzeń
W poprzednim przykładzie działaliśmy na wektorach o wysokiej wymiarowości torba słów o długości vocab_size, i jawnie przekonwertowaliśmy wektory reprezentacji pozycyjnej o niskiej wymiarowości na rozrzedzone jednowymiarowe gorące reprezentacje. Ta jednowymiarowa reprezentacja nie jest efektywna pod względem pamięci. Ponadto każdy wyraz jest traktowany niezależnie, więc wektory kodowania one-hot nie odzwierciedlają semantycznych podobieństw między słowami.
W tej jednostce kontynuujemy eksplorowanie zestawu danych AG News. Aby rozpocząć, załadujmy dane i pobierzemy definicje z poprzedniej lekcji.
import tensorflow as tf
import keras
import tensorflow_datasets as tfds
import numpy as np
# In this tutorial, we will be training a lot of models. In order to use GPU memory cautiously,
# we will set tensorflow option to grow GPU memory allocation when required.
physical_devices = tf.config.list_physical_devices('GPU')
if len(physical_devices)>0:
tf.config.set_memory_growth(physical_devices[0], True)
dataset = tfds.load('ag_news_subset')
ds_train = dataset['train']
ds_test = dataset['test']
Co to jest osadzanie?
Ideą osadzania jest reprezentowanie wyrazów przy użyciu wektorów gęstych o niższych wymiarach, które odzwierciedlają semantyczne znaczenie słowa. Później omówimy sposób tworzenia znaczących osadzeń słów, ale na razie po prostu pomyślmy o osadzaniu jako sposób na zmniejszenie wymiarowości wektora wyrazów.
Dlatego warstwa embedding przyjmuje słowo jako dane wejściowe i generuje wektor wyjściowy określonego embedding_size. W pewnym sensie jest to podobne do warstwy Dense, ale zamiast przyjmować wektor zakodowany one-hot jako dane wejściowe, jest w stanie przyjąć numer słowa.
Używając warstwy osadzania jako pierwszej warstwy w naszej sieci, możemy przełączyć się z modelu bag-of-words na model torby osadzeń
Nasza sieć neuronowa klasyfikatora składa się z następujących warstw:
- Warstwa TextVectorization, która przyjmuje ciąg znaków jako wejście i tworzy tensor z numerami tokenów. Określimy rozsądny rozmiar
vocab_sizesłownictwa i zignorujmy rzadziej używane słowa. Kształt wejściowy wynosi 1, a kształt wyjściowy będzie $n$, ponieważ otrzymujemy tokeny $n$ w wyniku, każdy z nich zawiera liczby z zakresu od 0 dovocab_size. - Warstwa osadzania, która przyjmuje liczby $n$ i zmniejsza każdą liczbę do gęstego wektora o danej długości (100 w naszym przykładzie). W związku z tym tensor wejściowy o kształcie $n$ zostanie przekształcony w tensor $n \times 100$.
- Warstwa agregacji, która przyjmuje średnią tego tensoru wzdłuż pierwszej osi, czyli oblicza średnią wszystkich $n$ wartości tensorów wejściowych odpowiadających różnym wyrazom. Aby zaimplementować tę warstwę, użyjemy warstwy
Lambda, i przekażemy do niej funkcję w celu obliczenia średniej. Dane wyjściowe będą miały kształt 100 i jest to liczbowa reprezentacja całej sekwencji danych wejściowych. - Końcowy klasyfikator liniowy gęsty.
Możemy zaimplementować te warstwy przy użyciu następującego kodu:
vocab_size = 30000
batch_size = 128
vectorizer = keras.layers.TextVectorization(max_tokens=vocab_size)
model = keras.Sequential([
keras.Input(shape=(1,), dtype=tf.string),
vectorizer,
keras.layers.Embedding(vocab_size,100),
keras.layers.Lambda(lambda x: tf.reduce_mean(x,axis=1)),
keras.layers.Dense(4, activation='softmax')
])
model.summary()
Uruchomienie tego kodu powoduje wygenerowanie następujących danych wyjściowych:
Model: "sequential"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
┃ Layer (type) ┃ Output Shape ┃ Param # ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
│ text_vectorization │ (None, None) │ 0 │
│ (TextVectorization) │ │ │
├──────────────────────────────┼───────────────────────────┼───────────────┤
│ embedding (Embedding) │ (None, None, 100) │ 3,000,000 │
├──────────────────────────────┼───────────────────────────┼───────────────┤
│ lambda (Lambda) │ (None, 100) │ 0 │
├──────────────────────────────┼───────────────────────────┼───────────────┤
│ dense (Dense) │ (None, 4) │ 404 │
└──────────────────────────────┴───────────────────────────┴───────────────┘
Total params: 3,000,404 (11.45 MB)
Trainable params: 3,000,404 (11.45 MB)
Non-trainable params: 0 (0.00 B)
summary W wydruku w kolumnie kształtu wyjściowego pierwszy wymiar None tensora odpowiada rozmiarowi minipartii, a drugi odpowiada długości sekwencji tokenów. Wszystkie sekwencje tokenów w minibatch mają różne długości. Omówimy, jak sobie z tym poradzić w następnej sekcji.
Sieć można wytrenować przy użyciu następującego kodu:
def extract_text(x):
return x['title']+' '+x['description']
def tupelize(x):
return (extract_text(x),x['label'])
print("Training vectorizer")
vectorizer.adapt(ds_train.take(500).map(extract_text))
model.compile(loss='sparse_categorical_crossentropy',optimizer='adam',metrics=['acc'])
model.fit(ds_train.map(tupelize).batch(batch_size),validation_data=ds_test.map(tupelize).batch(batch_size))
Uwaga / Notatka
Tworzymy wektoryzator na podstawie podzestawu danych. Jest to wykonywane w celu przyspieszenia procesu i może to spowodować sytuację, gdy nie wszystkie tokeny z naszego tekstu są obecne w słownictwie. W takim przypadku tokeny te zostaną zignorowane, co może spowodować nieznaczną niższą dokładność. Jednak w prawdziwym życiu podzbiór tekstu często daje dobre oszacowanie słownictwa.
Obsługa zmiennych rozmiarów sekwencji
Zrozummy, jak odbywa się trenowanie w minipartycjach. W powyższym przykładzie tensor wejściowy ma wymiar 1, a my używamy minipartiami o długości 128, więc rzeczywisty rozmiar tensoru wynosi $128 \times 1$. Jednak liczba tokenów w każdym zdaniu jest inna. Jeśli zastosujemy warstwę TextVectorization do pojedynczego wejścia, liczba zwróconych tokenów różni się w zależności od tego, jak tekst jest tokenizowany.
print(vectorizer('Hello, world!'))
print(vectorizer('I am glad to meet you!'))
Uruchomienie tego kodu powoduje wygenerowanie następujących danych wyjściowych:
tf.Tensor([ 1 45], shape=(2,), dtype=int64)
tf.Tensor([ 112 1271 1 3 1747 158], shape=(6,), dtype=int64)
Jednak po zastosowaniu wektoryzatora do kilku sekwencji musi on utworzyć tensor prostokątnego kształtu, więc wypełnia nieużywane elementy tokenem PAD (co w naszym przypadku ma wartość zero):
vectorizer(['Hello, world!','I am glad to meet you!'])
Uruchomienie tego kodu powoduje wygenerowanie następujących danych wyjściowych:
<tf.Tensor: shape=(2, 6), dtype=int64, numpy=
array([[ 1, 45, 0, 0, 0, 0],
[ 112, 1271, 1, 3, 1747, 158]])>
Możemy tutaj zobaczyć zakodowania:
model.layers[1](vectorizer(['Hello, world!','I am glad to meet you!'])).numpy()
Uruchomienie tego kodu powoduje wygenerowanie następujących danych wyjściowych:
array([[[-0.02485236, -0.00416857, -0.06599288, ..., -0.02404598,
0.03529833, -0.02100844],
[ 0.22493948, 0.01383338, 0.12420551, ..., 0.19531338,
0.13524376, 0.04216914],
[ 0.04510409, 0.00708018, -0.0310419 , ..., -0.0188726 ,
-0.0179676 , -0.04813331],
[ 0.04510409, 0.00708018, -0.0310419 , ..., -0.0188726 ,
-0.0179676 , -0.04813331],
[ 0.04510409, 0.00708018, -0.0310419 , ..., -0.0188726 ,
-0.0179676 , -0.04813331],
[ 0.04510409, 0.00708018, -0.0310419 , ..., -0.0188726 ,
-0.0179676 , -0.04813331]],
[[-0.00226152, -0.0972852 , -0.00063103, ..., 0.00504377,
0.22460397, 0.1497297 ],
[-0.15621698, -0.13758421, -0.02889572, ..., -0.02577994,
0.03472563, 0.08767739],
[-0.02485236, -0.00416857, -0.06599288, ..., -0.02404598,
0.03529833, -0.02100844],
[-0.06490357, -0.08200071, -0.06175491, ..., -0.02477042,
-0.06802022, -0.01040947],
[ 0.03279151, 0.12563369, 0.06062867, ..., -0.04349922,
-0.12154414, -0.12533969],
[-0.14435016, -0.304014 , -0.00378676, ..., 0.05609043,
0.20370889, 0.28518862]]], dtype=float32)
Uwaga / Notatka
Aby zminimalizować ilość dopełnienia, w niektórych przypadkach warto sortować wszystkie sekwencje w zestawie danych w kolejności rosnącej długości (lub, dokładniej, liczby tokenów). Dzięki temu każdy minibatch zawiera sekwencje o podobnej długości.
Osadzanie semantyczne: Word2Vec
W poprzednim przykładzie warstwa osadzania nauczyła się mapować wyrazy na reprezentacje wektorowe, jednak te reprezentacje nie miały znaczenia semantycznego. Warto nauczyć się reprezentacji wektorów, tak aby podobne słowa lub synonimy odpowiadały wektorom zbliżonym do siebie pod względem odległości wektorów (na przykład odległości euklidesowej).
W tym celu musimy wstępnie wytrenować nasz model osadzania w dużej kolekcji tekstu przy użyciu techniki, takiej jak Word2Vec. Jest ona oparta na dwóch głównych architekturach, które są używane do tworzenia rozproszonej reprezentacji wyrazów:
- Continuous bag-of-words (CBoW), gdzie trenujemy model, aby przewidzieć słowo z otaczającego kontekstu. Biorąc pod uwagę ngram $(W_{-2},W_{-1},W_0,W_1,W_2)$, celem modelu jest przewidywanie $W_0$ z $(W_{-2},W_{-1},W_1,W_2)$.
- Ciągły skip-gram jest przeciwieństwem CBoW. Model używa słowa wejściowego ($W_0$) do przewidywania otaczającego okna wyrazów kontekstowych.
CBoW jest szybszy, chociaż skip-gram jest wolniejszy, lepiej reprezentuje rzadkie słowa.
Aby eksperymentować z osadzaniem Word2Vec wstępnie wytrenowanym na zestawie danych Google News, możemy użyć biblioteki gensim. Poniżej znajdują się słowa najbardziej podobne do "neuralnych".
Uwaga / Notatka
Podczas pierwszego tworzenia wektorów słów pobieranie ich może zająć trochę czasu!
import gensim.downloader as api
w2v = api.load('word2vec-google-news-300')
for w,p in w2v.most_similar('neural'):
print(f"{w} -> {p}")
Uruchomienie tego kodu powoduje wygenerowanie następujących danych wyjściowych:
neuronal -> 0.7804799675941467
neurons -> 0.7326500415802002
neural_circuits -> 0.7252851724624634
neuron -> 0.7174385190010071
cortical -> 0.6941086649894714
brain_circuitry -> 0.6923246383666992
synaptic -> 0.6699118614196777
neural_circuitry -> 0.6638563275337219
neurochemical -> 0.6555314064025879
neuronal_activity -> 0.6531826257705688
Możemy również wyodrębnić wektor osadzania ze słowa , który ma być używany w trenowaniu modelu klasyfikacji. Osadzanie zawiera 300 składników, ale w tym miejscu pokazano tylko pierwsze 20 składników wektora w celu uzyskania jasności:
w2v['play'][:20]
Uruchomienie tego kodu powoduje wygenerowanie następujących danych wyjściowych:
array([ 0.01226807, 0.06225586, 0.10693359, 0.05810547, 0.23828125,
0.03686523, 0.05151367, -0.20703125, 0.01989746, 0.10058594,
-0.03759766, -0.1015625 , -0.15820312, -0.08105469, -0.0390625 ,
-0.05053711, 0.16015625, 0.2578125 , 0.10058594, -0.25976562],
dtype=float32)
Świetną rzeczą na temat semantycznych osadzeń jest to, że można manipulować kodowaniem wektorów na podstawie semantyki. Na przykład możemy poprosić o znalezienie wyrazu, którego wektorowa reprezentacja jest jak najbardziej zbliżona do słowa król i kobieta, i jak najdalej od słowa człowiek:
w2v.most_similar(positive=['king','woman'],negative=['man'])[0]
Uruchomienie tego kodu powoduje wygenerowanie następujących danych wyjściowych:
('queen', 0.7118192911148071)
W powyższym przykładzie użyto wewnętrznej magii Gensim, ale podstawowa logika jest prosta. Interesujące jest to, że można wykonywać typowe operacje wektorowe na wektorach osadzania, co odzwierciedla operacje na znaczeniach słów. Powyższy przykład można wyrazić w odniesieniu do operacji wektorów: obliczamy wektor odpowiadający KING-MAN+WOMAN (operacje + i - są wykonywane na reprezentacjach wektorowych odpowiadających im wyrazów), a następnie znajduje najbliższe słowo w słowniku do tego wektora:
# get the vector corresponding to king-man+woman
qvec = w2v['king'] - w2v['man'] + w2v['woman']
# find the index of the closest embedding vector, excluding input words
d = np.sum((w2v.vectors-qvec)**2,axis=1)
exclude = {w2v.key_to_index[w] for w in ['king', 'man', 'woman']}
d[list(exclude)] = np.inf
min_idx = np.argmin(d)
# find the corresponding word
w2v.index_to_key[min_idx]
Uruchomienie tego kodu powoduje wygenerowanie następujących danych wyjściowych:
'queen'
Aby znaleźć najbliższy wektor, użyjemy biblioteki NumPy do obliczenia wektora odległości między naszym wektorem a wszystkimi wektorami słownictwa, a następnie znajdź indeks najbliższego słowa przy użyciu metody argmin. Wykluczamy z wyszukiwania wyrazy wejściowe (king, man, woman), ponieważ most_similar metoda wykonuje to automatycznie.
Chociaż Word2Vec wydaje się świetnym sposobem wyrażania semantyki słów, ma wiele wad, w tym następujące:
- Modele CBoW i skip-gram są osadzaniem predykcyjnym i uwzględniają tylko kontekst lokalny. Word2Vec nie korzysta z kontekstu globalnego.
- Word2Vec nie uwzględnia morfologii wyrazów, czyli faktu, że znaczenie słowa może zależeć od różnych części słowa, takich jak rdzeń.
FastText próbuje przezwyciężyć drugie ograniczenie i opiera się na Word2Vec, ucząc się reprezentacji wektorów dla każdego słowa i n-gramów znakowych znalezionych w każdym słowie. Wartości reprezentacji są następnie uśrednione do jednego wektora w każdym kroku trenowania. Chociaż dodaje to wiele dodatkowych obliczeń do wstępnego trenowania, umożliwia osadzanie słów w celu kodowania informacji o podsłowach.
Inna metoda GloVe używa innego podejścia do osadzania słów na podstawie faktoryzacji macierzy słów i kontekstu. Najpierw tworzy dużą macierz, która zlicza liczbę wystąpień słów w różnych kontekstach, a następnie próbuje przedstawić tę macierz w niższych wymiarach w sposób, który minimalizuje utratę rekonstrukcji.
Biblioteka gensim obsługuje osadzanie tych wyrazów i można z nimi eksperymentować, zmieniając powyższy kod ładowania modelu.
Używanie wstępnie wytrenowanych osadzania w protokole Keras
Możemy zmodyfikować powyższy przykład, aby wstępnie wypełniać macierz w warstwie osadzania za pomocą semantycznych osadzeń, takich jak Word2Vec. Słownictwo wstępnie wytrenowanej reprezentacji i korpusu tekstu prawdopodobnie nie będzie zgodne, więc musimy wybrać jedno z nich. W tym miejscu przyjrzymy się dwóm możliwym opcjom: używanie słownictwa tokenizatora i używanie słownictwa z osadzeń Word2Vec.
Używanie słownictwa tokenizatora
W przypadku używania słownictwa tokenizatora niektóre słowa ze słownictwa mają odpowiednie osadzenia Word2Vec, a niektórych będzie brakować. Biorąc pod uwagę, że nasz rozmiar słownictwa to vocab_size, a długość wektora osadzania Word2Vec to embed_size, warstwa osadzania będzie reprezentowana przez macierz wag o kształcie vocab_size$\times$embed_size. Uzupełnimy tę matrycę, przeglądając słownictwo.
embed_size = len(w2v.get_vector('hello'))
print(f'Embedding size: {embed_size}')
vocab = vectorizer.get_vocabulary()
W = np.zeros((vocab_size,embed_size))
print('Populating matrix, this will take some time...',end='')
found, not_found = 0,0
for i,w in enumerate(vocab):
try:
W[i] = w2v.get_vector(w)
found+=1
except KeyError:
# W[i] = np.random.normal(0.0,0.3,size=(embed_size,))
not_found+=1
print(f"Done, found {found} words, {not_found} words missing")
W przypadku słów, które nie są obecne w słownictwie Word2Vec, możemy pozostawić je jako zera lub wygenerować losowy wektor.
Aby zdefiniować warstwę osadzania z wstępnie wytrenowanym ciężarem, uruchomimy następujący kod:
emb = keras.layers.Embedding(vocab_size,embed_size,weights=[W],trainable=False)
model = keras.Sequential([
keras.Input(shape=(1,), dtype=tf.string),
vectorizer, emb,
keras.layers.Lambda(lambda x: tf.reduce_mean(x,axis=1)),
keras.layers.Dense(4, activation='softmax')
])
Po wykonaniu tej czynności możemy wytrenować nasz model.
model.compile(loss='sparse_categorical_crossentropy',optimizer='adam',metrics=['acc'])
model.fit(ds_train.map(tupelize).batch(batch_size),
validation_data=ds_test.map(tupelize).batch(batch_size))
Uwaga / Notatka
Zwróć uwagę, że ustawiamy trainable=False podczas tworzenia Embedding elementu, co oznacza, że nie przeprowadzamy ponownego trenowania warstwy osadzania. Może to spowodować, że dokładność będzie nieco niższa, ale przyspiesza trenowanie.
Używanie słownictwa osadzonego
Jednym z problemów z poprzednim podejściem jest to, że słownictwo używane w TextVectorization i Embedding jest inne. Aby rozwiązać ten problem, możemy użyć jednego z następujących rozwiązań:
- Ponownie przeszkolij model Word2Vec w naszym słownictwie.
- Załaduj nasz zestaw danych z użyciem słownictwa z wytrenowanego wcześniej modelu Word2Vec. Słownictwo używane do ładowania zestawu danych można określić podczas ładowania.
Drugie podejście wydaje się łatwiejsze, więc zaimplementujmy je. Przede wszystkim tworzymy warstwę TextVectorization z określonym słownictwem pobranym z osadzania Word2Vec:
vocab = list(w2v.key_to_index.keys())
vectorizer = keras.layers.TextVectorization()
vectorizer.set_vocabulary(vocab)
Teraz musimy skompilować macierz wagi osadzania z wektorów Word2Vec. Tworzymy macierz, w której każdy wiersz odpowiada słowu w słownictwie i ręcznie skonstruujemy warstwę osadzania Keras:
embed_size = w2v.vector_size
vocab_size_w2v = len(vocab) + 2 # +2 for padding and unknown tokens
W = np.zeros((vocab_size_w2v, embed_size))
for i, word in enumerate(vocab):
W[i + 2] = w2v[word] # offset by 2 for padding (0) and unknown (1) tokens
emb_w2v = keras.layers.Embedding(vocab_size_w2v, embed_size, weights=[W], trainable=False)
model = keras.Sequential([
keras.Input(shape=(1,), dtype=tf.string),
vectorizer,
emb_w2v,
keras.layers.Lambda(lambda x: tf.reduce_mean(x,axis=1)),
keras.layers.Dense(4, activation='softmax')
])
model.compile(loss='sparse_categorical_crossentropy',optimizer='adam',metrics=['acc'])
model.fit(ds_train.map(tupelize).batch(128),validation_data=ds_test.map(tupelize).batch(128),epochs=5)
Uruchomienie tego kodu powoduje wygenerowanie następujących danych wyjściowych:
Epoch 1/5
938/938 ━━━━━━━━━━━━━━━━━━━━ 7s 7ms/step - loss: 1.3381 - acc: 0.4961 - val_loss: 1.2996 - val_acc: 0.5682
Epoch 2/5
938/938 ━━━━━━━━━━━━━━━━━━━━ 7s 7ms/step - loss: 1.2591 - acc: 0.5714 - val_loss: 1.2340 - val_acc: 0.5839
Epoch 3/5
938/938 ━━━━━━━━━━━━━━━━━━━━ 7s 7ms/step - loss: 1.1983 - acc: 0.5883 - val_loss: 1.1827 - val_acc: 0.5951
Epoch 4/5
938/938 ━━━━━━━━━━━━━━━━━━━━ 7s 7ms/step - loss: 1.1505 - acc: 0.6001 - val_loss: 1.1417 - val_acc: 0.6021
Epoch 5/5
938/938 ━━━━━━━━━━━━━━━━━━━━ 7s 7ms/step - loss: 1.1122 - acc: 0.6093 - val_loss: 1.1084 - val_acc: 0.6103
Jedną z przyczyn, dla których nie widzimy większej dokładności, jest to, że brakuje niektórych słów z naszego zestawu danych w wstępnie wytrenowanym słownictwie Word2Vec, i dlatego są ignorowane. Wstępnie wytrenowany model Word2Vec został przeszkolony na korpusie Google News, który ma inny obszar i rozkład słownictwa niż zestaw danych klasyfikacji AG News. Słowa obecne w naszych danych treningowych, ale nieobecne w słownictwie Word2Vec są mapowane na wektory zerowe, zmniejszając skuteczny sygnał dostępny dla klasyfikatora. Ponadto użycie trainable=False warstwy osadzania oznacza, że model nie może dostosować wektorów słów do określonego zadania klasyfikacji, co ogranicza możliwość uczenia się funkcji specyficznych dla zadań. Aby rozwiązać ten problem, możemy wytrenować własne osadzanie na podstawie naszego zestawu danych.
Uwaga / Notatka
Oczekiwana jest niższa dokładność w porównaniu z osadzaniem trenowania od podstaw. Wstępnie wytrenowany model Word2Vec został przeszkolony na korpusie Google News, który ma inne słownictwo i dziedzinę niż zbiór danych klasyfikacji AG News. Słowa obecne w danych treningowych modułu, ale nieobecne w słownictwie Word2Vec są dyskretnie ignorowane (mapowane na zero wektorów), zmniejszając skuteczny sygnał dostępny dla klasyfikatora. Aby uzyskać najlepsze wyniki dotyczące zadań specyficznych dla domeny, rozważ dostrajanie osadzania danych w domenie.
Trenowanie własnych osadzeń
W naszych przykładach używaliśmy wstępnie wytrenowanych osadzeń semantycznych, ale warto zobaczyć, jak te osadzanie można wytrenować przy użyciu architektur CBoW lub skip-gram. To ćwiczenie wykracza poza ten moduł, ale osoby zainteresowane mogą chcieć zapoznać się z tym oficjalnym samouczkiem TensorFlow dotyczącym trenowania modelu Word2Vec. Ponadto struktura gensim może służyć do trenowania najczęściej używanych osadzania w kilku wierszach kodu, zgodnie z opisem w oficjalnej dokumentacji.
Kontekstowe osadzanie
Jednym z kluczowych ograniczeń tradycyjnych wstępnie wytrenowanych reprezentacji osadzania, takich jak Word2Vec, jest fakt, że mimo że mogą przechwytywać pewne znaczenie słowa, nie mogą rozróżniać różnych znaczeń. Może to powodować problemy w modelach podrzędnych.
Na przykład słowo "play" ma inne znaczenie w tych dwóch różnych zdaniach:
- Poszedłem do sztuki w teatrze.
- John chce grać ze swoimi przyjaciółmi.
Wstępnie wytrenowane wektory, które omawialiśmy, reprezentują oba znaczenia słowa "play" w tym samym wektorze. Aby przezwyciężyć to ograniczenie, musimy utworzyć osadzanie na podstawie modelu językowego, który jest trenowany na dużym korpusie tekstu i wie , jak wyrazy można łączyć w różnych kontekstach. Omawianie kontekstowych osadzania jest poza zakresem tego modułu, ale wrócimy do nich podczas mówienia o modelach językowych w następnej lekcji.