15.3 Duomenų rinkinys žodžių įterpiniams iš anksto apmokyti
Dabar, kai žinome word2vec modelių technines detales ir apytikrius apmokymo metodus, pereikime prie jų įgyvendinimo. Konkrečiai, kaip pavyzdį imsime skip-gram modelį iš 15.1 skyrelis ir neigiamų pavyzdžių atranką iš 15.2 skyrelis. Šiame skyrelyje pradedame nuo duomenų rinkinio žodžių įterpinių modeliui iš anksto apmokyti: pradinis duomenų formatas bus paverstas minirinkiniais, kuriuos galima iteruoti apmokymo metu.
import collections
from d2l import mxnet as d2l
import math
from mxnet import gluon, np
import os
import random
import collections
from d2l import torch as d2l
import math
import torch
import os
import random
15.3.1 Duomenų rinkinio skaitymas
Čia naudojamas duomenų rinkinys yra Penn Tree Bank (PTB). Šis tekstynas atrinktas iš Wall Street Journal straipsnių ir padalytas į apmokymo, patikros ir testavimo aibes. Pradiniame formate kiekviena teksto failo eilutė yra žodžių sakinys, kurio žodžiai atskirti tarpais. Čia kiekvieną žodį laikome teksto vienetu.
#@save
d2l.DATA_HUB['ptb'] = (d2l.DATA_URL + 'ptb.zip',
'319d85e578af0cdc590547f26231e4e31cdf1e42')
#@save
def read_ptb():
"""Load the PTB dataset into a list of text lines."""
data_dir = d2l.download_extract('ptb')
# Read the training set
with open(os.path.join(data_dir, 'ptb.train.txt')) as f:
raw_text = f.read()
return [line.split() for line in raw_text.split('\n')]
sentences = read_ptb()
f'# sentences: {len(sentences)}'
Nuskaitę apmokymo aibę, sudarome tekstyno žodyną, kuriame bet kuris žodis, pasirodantis rečiau nei 10 kartų, pakeičiamas teksto vienetu „<unk>“. Atkreipkite dėmesį, kad pradiniame duomenų rinkinyje taip pat yra „<unk>“ teksto vienetų, vaizduojančių retus (nežinomus) žodžius.
vocab = d2l.Vocab(sentences, min_freq=10)
f'vocab size: {len(vocab)}'
15.3.2 Imties retinimas
Teksto duomenyse paprastai yra dažnai pasitaikančių žodžių, tokių kaip „the“, „a“ ir „in“: jie gali pasirodyti net milijardus kartų labai dideliuose tekstynuose. Tačiau šie žodžiai dažnai pasirodo kartu su daugeliu skirtingų žodžių konteksto languose ir suteikia mažai naudingų signalų. Pavyzdžiui, panagrinėkime žodį „chip“ konteksto lange: intuityviai jo bendras pasirodymas su retu žodžiu „intel“ yra naudingesnis apmokymui nei bendras pasirodymas su dažnu žodžiu „a“. Be to, apmokymas su milžiniškais (dažnų) žodžių kiekiais yra lėtas. Todėl apmokant žodžių įterpinių modelius dažnus žodžius galima išretinti (angl. subsample) (Mikolov ir kt., 2013). Konkrečiai, kiekvienas duomenų rinkinio indeksuotas žodis bus atmetamas su tikimybe
kur yra žodžių skaičiaus ir bendro duomenų rinkinio žodžių skaičiaus santykis, o konstanta yra hiperparametras (eksperimente ). Matome, kad tik tada, kai santykinis dažnis , (dažnas) žodis gali būti atmestas, ir kuo didesnis santykinis žodžio dažnis, tuo didesnė tikimybė, kad jis bus atmestas.
#@save
def subsample(sentences, vocab):
"""Subsample high-frequency words."""
# Exclude unknown tokens ('<unk>')
sentences = [[token for token in line if vocab[token] != vocab.unk]
for line in sentences]
counter = collections.Counter([
token for line in sentences for token in line])
num_tokens = sum(counter.values())
# Return True if `token` is kept during subsampling
def keep(token):
return(random.uniform(0, 1) <
math.sqrt(1e-4 / counter[token] * num_tokens))
return ([[token for token in line if keep(token)] for line in sentences],
counter)
subsampled, counter = subsample(sentences, vocab)
Šis kodo fragmentas nubraižo teksto vienetų skaičiaus viename sakinyje histogramą prieš imties retinimą ir po jo. Kaip ir tikėtasi, imties retinimas gerokai sutrumpina sakinius atmesdamas dažnus žodžius, o tai paspartins apmokymą.
d2l.show_list_len_pair_hist(['origin', 'subsampled'], '# tokens per sentence',
'count', sentences, subsampled);
Atskiriems teksto vienetams dažno žodžio „the“ atrankos dažnis yra mažesnis nei 1/20.
def compare_counts(token):
return (f'# of "{token}": '
f'before={sum([l.count(token) for l in sentences])}, '
f'after={sum([l.count(token) for l in subsampled])}')
compare_counts('the')
Priešingai, reti žodžiai, pavyzdžiui, „join“, išsaugomi visiškai.
compare_counts('join')
Po imties retinimo tekstyno teksto vienetus atvaizduojame į jų indeksus.
corpus = [vocab[line] for line in subsampled]
corpus[:3]
15.3.3 Centrinių ir konteksto žodžių išgavimas
Toliau pateikta funkcija get_centers_and_contexts
iš corpus išgauna visus
centrinius žodžius ir jų konteksto žodžius.
Ji atsitiktinai ir tolygiai atrenka sveikąjį skaičių nuo 1 iki max_window_size
kaip konteksto lango dydį.
Bet kuriam centriniam žodžiui
tie žodžiai,
kurių atstumas nuo jo
neviršija atrinkto
konteksto lango dydžio,
yra jo konteksto žodžiai.
#@save
def get_centers_and_contexts(corpus, max_window_size):
"""Return center words and context words in skip-gram."""
centers, contexts = [], []
for line in corpus:
# To form a "center word--context word" pair, each sentence needs to
# have at least 2 words
if len(line) < 2:
continue
centers += line
for i in range(len(line)): # Context window centered at `i`
window_size = random.randint(1, max_window_size)
indices = list(range(max(0, i - window_size),
min(len(line), i + 1 + window_size)))
# Exclude the center word from the context words
indices.remove(i)
contexts.append([line[idx] for idx in indices])
return centers, contexts
Toliau sukuriame dirbtinį duomenų rinkinį, sudarytą iš dviejų atitinkamai 7 ir 3 žodžių sakinių. Tegul didžiausias konteksto lango dydis yra 2, ir atspausdinkime visus centrinius žodžius bei jų konteksto žodžius.
tiny_dataset = [list(range(7)), list(range(7, 10))]
print('dataset', tiny_dataset)
for center, context in zip(*get_centers_and_contexts(tiny_dataset, 2)):
print('center', center, 'has contexts', context)
Apmokant PTB duomenų rinkinyje, didžiausią konteksto lango dydį nustatome 5. Toliau išgaunami visi duomenų rinkinio centriniai žodžiai ir jų konteksto žodžiai.
all_centers, all_contexts = get_centers_and_contexts(corpus, 5)
f'# center-context pairs: {sum([len(contexts) for contexts in all_contexts])}'
15.3.4 Neigiamų pavyzdžių atranka
Apytikriam apmokymui naudojame neigiamų pavyzdžių atranką.
Kad atrinktume triukšmo žodžius pagal
iš anksto apibrėžtą pasiskirstymą,
apibrėžiame šią klasę RandomGenerator,
kurioje (galbūt nenormuotas) atrankos pasiskirstymas perduodamas
per argumentą sampling_weights.
#@save
class RandomGenerator:
"""Randomly draw among {1, ..., n} according to n sampling weights."""
def __init__(self, sampling_weights):
# Exclude
self.population = list(range(1, len(sampling_weights) + 1))
self.sampling_weights = sampling_weights
self.candidates = []
self.i = 0
def draw(self):
if self.i == len(self.candidates):
# Cache `k` random sampling results
self.candidates = random.choices(
self.population, self.sampling_weights, k=10000)
self.i = 0
self.i += 1
return self.candidates[self.i - 1]
Pavyzdžiui, galime išgauti 10 atsitiktinių dydžių tarp indeksų 1, 2 ir 3 su atrankos tikimybėmis ir taip.
generator = RandomGenerator([2, 3, 4])
[generator.draw() for _ in range(10)]
Centrinio žodžio ir konteksto žodžio porai
atsitiktinai atrenkame K (eksperimente 5) triukšmo žodžių. Pagal word2vec straipsnio pasiūlymus,
triukšmo žodžio atrankos tikimybė
nustatoma lygi jo santykiniam dažniui
žodyne,
pakeltam
0,75 laipsniu (Mikolov ir kt., 2013).
#@save
def get_negatives(all_contexts, vocab, counter, K):
"""Return noise words in negative sampling."""
# Sampling weights for words with indices 1, 2, ... (index 0 is the
# excluded unknown token) in the vocabulary
sampling_weights = [counter[vocab.to_tokens(i)]**0.75
for i in range(1, len(vocab))]
all_negatives, generator = [], RandomGenerator(sampling_weights)
for contexts in all_contexts:
negatives = []
while len(negatives) < len(contexts) * K:
neg = generator.draw()
# Noise words cannot be context words
if neg not in contexts:
negatives.append(neg)
all_negatives.append(negatives)
return all_negatives
all_negatives = get_negatives(all_contexts, vocab, counter, 5)
15.3.5 Apmokymo pavyzdžių įkėlimas minirinkiniais
Išgavus visus centrinius žodžius kartu su jų konteksto žodžiais ir atrinktais triukšmo žodžiais, jie bus paversti pavyzdžių minirinkiniais, kuriuos galima iteratyviai įkelti apmokymo metu.
Minirinkinyje
-asis pavyzdys apima centrinį žodį
ir jo konteksto žodžių bei triukšmo žodžių.
Dėl skirtingų konteksto langų dydžių
skiriasi skirtingiems .
Todėl
kiekvienam pavyzdžiui
jo konteksto žodžius ir triukšmo žodžius sukonkatenuojame kintamajame
contexts_negatives
ir papildome nuliais, kol konkatenacijos ilgis
pasiekia (max_len).
Kad skaičiuojant nuostolį
papildymai būtų neįtraukiami,
apibrėžiame kaukės kintamąjį masks.
Tarp masks elementų ir contexts_negatives elementų
yra abipus vienareikšmė atitiktis,
kur masks nuliai (kitaip – vienetai) atitinka contexts_negatives papildymus.
Kad atskirtume teigiamus ir neigiamus pavyzdžius,
contexts_negatives konteksto žodžius nuo triukšmo žodžių atskiriame per kintamąjį labels.
Panašiai kaip masks,
taip pat yra abipus vienareikšmė atitiktis
tarp labels elementų ir contexts_negatives elementų,
kur labels vienetai (kitaip – nuliai) atitinka contexts_negatives konteksto žodžius (teigiamus pavyzdžius).
Aukščiau pateikta idėja įgyvendinta šioje funkcijoje batchify.
Jos įvestis data yra sąrašas, kurio ilgis
lygus rinkinio dydžiui,
kur kiekvienas elementas yra pavyzdys,
sudarytas iš
centrinio žodžio center, jo konteksto žodžių context ir jo triukšmo žodžių negative.
Ši funkcija grąžina
minirinkinį, kurį galima įkelti skaičiavimams
apmokymo metu,
pavyzdžiui, įskaitant kaukės kintamąjį.
#@save
def batchify(data):
"""Return a minibatch of examples for skip-gram with negative sampling."""
max_len = max(len(c) + len(n) for _, c, n in data)
centers, contexts_negatives, masks, labels = [], [], [], []
for center, context, negative in data:
cur_len = len(context) + len(negative)
centers += [center]
contexts_negatives += [context + negative + [0] * (max_len - cur_len)]
masks += [[1] * cur_len + [0] * (max_len - cur_len)]
labels += [[1] * len(context) + [0] * (max_len - len(context))]
return (d2l.reshape(d2l.tensor(centers), (-1, 1)), d2l.tensor(
contexts_negatives), d2l.tensor(masks), d2l.tensor(labels))
Išbandykime šią funkciją naudodami dviejų pavyzdžių minirinkinį.
x_1 = (1, [2, 2], [3, 3, 3, 3])
x_2 = (1, [2, 2, 2], [3, 3])
batch = batchify((x_1, x_2))
names = ['centers', 'contexts_negatives', 'masks', 'labels']
for name, data in zip(names, batch):
print(name, '=', data)
15.3.6 Viskas kartu
Galiausiai apibrėžiame funkciją load_data_ptb, kuri nuskaito PTB duomenų rinkinį ir grąžina duomenų iteratorių bei žodyną.
#@save
def load_data_ptb(batch_size, max_window_size, num_noise_words):
"""Download the PTB dataset and then load it into memory."""
sentences = read_ptb()
vocab = d2l.Vocab(sentences, min_freq=10)
subsampled, counter = subsample(sentences, vocab)
corpus = [vocab[line] for line in subsampled]
all_centers, all_contexts = get_centers_and_contexts(
corpus, max_window_size)
all_negatives = get_negatives(
all_contexts, vocab, counter, num_noise_words)
dataset = gluon.data.ArrayDataset(
all_centers, all_contexts, all_negatives)
data_iter = gluon.data.DataLoader(
dataset, batch_size, shuffle=True,batchify_fn=batchify,
num_workers=d2l.get_dataloader_workers())
return data_iter, vocab
#@save
def load_data_ptb(batch_size, max_window_size, num_noise_words):
"""Download the PTB dataset and then load it into memory."""
num_workers = d2l.get_dataloader_workers()
sentences = read_ptb()
vocab = d2l.Vocab(sentences, min_freq=10)
subsampled, counter = subsample(sentences, vocab)
corpus = [vocab[line] for line in subsampled]
all_centers, all_contexts = get_centers_and_contexts(
corpus, max_window_size)
all_negatives = get_negatives(
all_contexts, vocab, counter, num_noise_words)
class PTBDataset(torch.utils.data.Dataset):
def __init__(self, centers, contexts, negatives):
assert len(centers) == len(contexts) == len(negatives)
self.centers = centers
self.contexts = contexts
self.negatives = negatives
def __getitem__(self, index):
return (self.centers[index], self.contexts[index],
self.negatives[index])
def __len__(self):
return len(self.centers)
dataset = PTBDataset(all_centers, all_contexts, all_negatives)
data_iter = torch.utils.data.DataLoader(dataset, batch_size, shuffle=True,
collate_fn=batchify,
num_workers=num_workers)
return data_iter, vocab
Atspausdinkime pirmąjį duomenų iteratoriaus minirinkinį.
data_iter, vocab = load_data_ptb(512, 5, 5)
for batch in data_iter:
for name, data in zip(names, batch):
print(name, 'shape:', data.shape)
break
15.3.7 Santrauka
- Dažni žodžiai apmokymui gali būti ne tokie naudingi. Galime juos išretinti, kad apmokymas būtų greitesnis.
- Skaičiavimo efektyvumui pasiekti pavyzdžius įkeliame minirinkiniais. Galime apibrėžti kitus kintamuosius, kad atskirtume papildymus nuo ne papildymų ir teigiamus pavyzdžius nuo neigiamų.
15.3.8 Pratimai
- Kaip pasikeistų šiame skyrelyje pateikto kodo veikimo laikas, jei nenaudotume imties retinimo?
- Klasė
RandomGeneratorkaupiakatsitiktinės atrankos rezultatų. Nustatykitekį kitas reikšmes ir pažiūrėkite, kaip tai veikia duomenų įkėlimo greitį. - Kokie kiti šio skyrelio kodo hiperparametrai gali turėti įtakos duomenų įkėlimo greičiui?
