# Doinštalujeme balíčky, ktoré nie
# sú defaultne k dispozícii
import sys
!{sys.executable} -m pip install lapjv
# Import potrebných balíčkov
import csv
import numpy as np
import pandas as pd
from umap import UMAP
from lapjv import lapjv
from scipy.spatial.distance import cdist
from sklearn.cluster import AgglomerativeClustering
from sklearn.neighbors import NearestNeighbors
import matplotlib.pyplot as plt
# Uistíme sa, že máme všetky potrebné dáta
!mkdir -p data
!wget -nc -O data/sk.vec https://www.dropbox.com/s/a05usrk3en94w2o/sk.vec?dl=1
!wget -nc -O data/words_sk https://www.dropbox.com/s/ayvc384pezg8fi4/words_sk?dl=1
V tomto notebook-u sa budeme venovať téme embedovania slov, t.j. priraďovania numerických vektorov ku jednotlivým slovám, ktoré by aspoň čiastočne vyjadrili ich vzájomné súvislosti, sémantické vlastnosti a pod.
Jednoduchý prístup ku nájdeniu takých vektorov poskytuje metóda word2vec, ako to ilustruje obrázok. Zostrojí sa jednoduchá neurónová sieť, ktorej úlohou je predikovať slovo z kontextu viacerých okolitých slov alebo naopak – predikovať zo slova jeho najpravdepodobnejší kontext. Sieť sa naučí rozbaliť identifikátor slova do takého vektora, ktorý jej v predikcii pomôže. Vektory slov, ktoré sa spolu často vyskytujú v tom istom kontexte, budú podobné.
Vektory, ktoré budeme používať v tomto notebook-u pochádzajú nie z modelu word2vec, ale boli vytvorené pomocou metódy FastText, ktorú vyvinula spoločnosť Facebook. Linky ku relevantným článkom možno nájsť na oficiálnej stránke projektu.
Predtrénované vektory sú k dispozícii pre viacero jazykov:
Keďže súbory s vektormi majú viacero GB, využijeme, že slová sú usporiadané od najčastejšie sa vyskytujúcich po najriedkavejšie a extrahujeme zo súboru len prvých $n$ riadkov. Pre slovenský jazyk bude treba extrahovať viac riadkov než pre angličtinu: kvôli skloňovaniu.
Najprv načítame samotné vektory:
df = pd.read_csv("data/sk.vec", sep=' ', skiprows=[0],
header=None, quoting=csv.QUOTE_NONE,
encoding='utf-8')
Definujeme si pomocnú funkciu, ktorá po zadaní slova vráti jeho vektor.
def vec(word):
return df[df.iloc[:, 0].str.lower() \
== word.lower()].iloc[0, 1:]
Zoznam slov, s ktorých vektormi budeme pracovať, načítame zo súboru.
with open("data/words_sk", "r") as word_file:
words = [w.strip() for w in word_file]
Extrahujeme ich vektory. Ak sa nejaké slovo nenachádza v zozname vektorov, ktoré sme načítali, preskočíme ho (iná alternatíva: keby sme si stiahli celý FastText model pre príslušný jazyk, vedeli by sme generovať vektory pre ľubovoľné slová).
vecs = []
for w in words:
try:
vecs.append(vec(w))
except IndexError:
print("Word '{}' not found in the index.".format(w))
Aby sme mohli vektory jednotlivých slov vizualizovať, redukujeme ich do 2-rozmerného priestoru pomocou metódy UMAP.
um = UMAP()
embeds = um.fit_transform(vecs)
# normalizujeme do rozsahu [0, 1]
embeds -= embeds.min(axis=0)
embeds /= embeds.max(axis=0)
Aby bol výsledný obrázok prehľadnejší, budeme chcieť zhluky slov odlíšiť farebne. Použijeme preto hierarchické zhlukovanie, aby sme zhluky identifikovali. Zhlukovanie budeme robiť nie na 2-rozmernej, ale na 10-rozmernej verzii dát: aby boli zhluky rozpoznateľnejšie. Ako vzdialenostnú metriku použijeme kosínusovú vzdialenosť (súvisí to s interpretáciou slovných vektorov).
um = UMAP(n_components=10)
clust_embeds = um.fit_transform(vecs)
clust = AgglomerativeClustering(10, affinity='cosine',
linkage='average').fit_predict(clust_embeds)
Vytvoríme si farebnú mapu a kvôli čitateľnosti zabezpečíme, aby neboli žiadne farby príliš svetlé.
cm = plt.get_cmap('jet', np.max(clust)+1)
def int2color(cl):
color = np.array(cm(cl))
if color[:-1].sum() > 1.5:
color[:-1] = np.maximum(color[:-1] - 0.2, [0, 0, 0])
return color
Následne už môžeme vektory vykresliť.
fig = plt.figure(figsize=(10, 10))
ax = plt.gca()
for iw, w in enumerate(words):
ax.annotate(w, embeds[iw], color=int2color(clust[iw]),
ha="center", va="center")
plt.xlabel("$d_1$")
plt.ylabel("$d_2$")
# fig.set_size_inches((8, 6))
# plt.savefig("vecs_umap.pdf", bbox_inches='tight', pad_inches=0)
V rámci vizualizácie vytvorenej pomocou t-SNE vidno vzdialenosti medzi vektormi jednotlivých slov a podobne. Slová sa však navzájom prekrývajú, čo robí obrázok ťažko čitateľným. Preto pozície skúsime premietnuť do pravidelnej mriežky pomocou algoritmu Jonker-Volgenant.
sqrt_size = int(np.ceil(np.sqrt(len(vecs))))
size = sqrt_size * sqrt_size
padded_embeds = np.zeros((size, embeds.shape[1]))
padded_embeds[:embeds.shape[0], :] = embeds
embeds = padded_embeds
grid = np.dstack(np.meshgrid(np.linspace(0, 1, sqrt_size), np.linspace(0, 1, sqrt_size))).reshape(-1, 2)
cost_matrix = cdist(grid, embeds, "sqeuclidean").astype(np.float32)
cost_matrix = cost_matrix * (100000 / cost_matrix.max())
row_as, col_as, _ = lapjv(cost_matrix)
grid_jv = grid[col_as]
Nové pozície slov uložené v poli grid_jv
použijeme na vykreslenie.
fig = plt.figure(figsize=(7, 6))
plt.axis('off')
ax = plt.gca()
for iw, w in enumerate(words):
color = np.array(cm(clust[iw]))
if color[:-1].sum() > 1.5:
color[:-1] = np.maximum(color[:-1] - 0.2, [0, 0, 0])
ax.annotate(w, grid_jv[iw], color=int2color(clust[iw]),
rotation=45, ha="center", va="center")
# plt.savefig("vecs_lapjv.pdf", bbox_inches="tight", pad_inches=0)
Ako z obrázkov vidno, vektory niektorých príbuzných slov sa naozaj zoskupujú blízko seba: napríklad názvy všetkých mesiacov sú pokope.
Zaujímavou vlastnosťou slovných embedovacích vektorov je, že aritmetické operácie s nimi majú niekedy sémantický zmysel.
Ak vykonáme operáciu typu "kráľ" - "muž" + "žena", dostaneme vektor, ktorý sa podobá embedovaniu slova "kráľovná". Môžeme si to overiť tak, že pomocou algoritmu nearest neighbours nájdeme najbližších susedov nového vektora.
Aplikujme teda na naše vektory algoritmus nearest neighbours. Ako vzdialenostnú metriku použijeme kosínusovú vzdialenosť:
nbrs = NearestNeighbors(n_neighbors=7, algorithm='brute',
metric='cosine').fit(df.iloc[:, 1:])
Teraz môžeme otestovať operáciu "kráľ" - "muž" + "žena":
diff = vec("kráľ") - vec("muž") + vec("žena")
dist, ind = nbrs.kneighbors(diff.values.reshape((1, -1)))
print(df.iloc[ind[0], 0])
Uvidíme, že slovo kráľovná je buď prvé alebo druhé na zozname (niekedy bude nový vektor ešte stále najbližšie pôvodnému slovu – v tomto prípade slovu "kráľ"; potom kráľovná bude až na druhom mieste).
Ďalej si môžeme vyskúšať podobnú operáciu napríklad s hlavnými mestami štátov. Ak chceme, aby aritmetika fungovala trochu robustnejšie, môžeme vypočítať priemer z viacerých rozdielových vektorov – ale aj s jedným rozdielovým vektorom sú výsledky slušné:
Otestujte obdobnú vektorovú aritmetiku na prípade krajiny a jej hlavného mesta. Ak výsledky nebudú ideálne, dá sa diff
vektor vypočítať ako priemer pre niekoľko rôznych kombinácií krajín a hlavných miest – potom môže fungovať robustnejšie.
Otestujte vektorovú aritmetiku na prípade stupňovania prídavných mien (vysoký, vyšší, ...).