In [27]:
# Doinštalujeme balíčky, ktoré nie
# sú defaultne k dispozícii
import sys
!{sys.executable} -m pip install lapjv
Requirement already satisfied: lapjv in /usr/local/lib/python3.6/dist-packages (1.3.1)
Collecting hdbscan
  Downloading https://files.pythonhosted.org/packages/10/7c/1401ec61b0e7392287e045b6913206cfff050b65d869c19f7ec0f5626487/hdbscan-0.8.22.tar.gz (4.0MB)
     |████████████████████████████████| 4.0MB 4.8MB/s 
  Installing build dependencies ... done
  Getting requirements to build wheel ... done
    Preparing wheel metadata ... done
Requirement already satisfied: numpy in /usr/local/lib/python3.6/dist-packages (from lapjv) (1.16.4)
Requirement already satisfied: cython>=0.27 in /usr/local/lib/python3.6/dist-packages (from hdbscan) (0.29.13)
Requirement already satisfied: scikit-learn>=0.17 in /usr/local/lib/python3.6/dist-packages (from hdbscan) (0.21.3)
Requirement already satisfied: joblib in /usr/local/lib/python3.6/dist-packages (from hdbscan) (0.13.2)
Requirement already satisfied: scipy>=0.9 in /usr/local/lib/python3.6/dist-packages (from hdbscan) (1.3.1)
Building wheels for collected packages: hdbscan
  Building wheel for hdbscan (PEP 517) ... done
  Created wheel for hdbscan: filename=hdbscan-0.8.22-cp36-cp36m-linux_x86_64.whl size=2336942 sha256=00f19f6127e8b22629784c95aeb701120dcb01ab20e58d1af847441627bd0bdb
  Stored in directory: /root/.cache/pip/wheels/6d/f9/db/f2e5e704427932f5b05c91fc520effbb0bd10ba8d73fd3bfc7
Successfully built hdbscan
Installing collected packages: hdbscan
Successfully installed hdbscan-0.8.22
In [1]:
# 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
In [49]:
# 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
File ‘data/sk.vec’ already there; not retrieving.
File ‘data/words_sk’ already there; not retrieving.

Embedovanie slov

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é.

word2vec

FastText vektory

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.

Načítanie vektorov a pomocné funkcie

Najprv načítame samotné vektory:

In [2]:
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.

In [3]:
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.

In [4]:
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á).

In [5]:
vecs = []

for w in words:
    try:
        vecs.append(vec(w))
    except IndexError:
        print("Word '{}' not found in the index.".format(w))

Znižovanie rozmeru pomocou UMAP

Aby sme mohli vektory jednotlivých slov vizualizovať, redukujeme ich do 2-rozmerného priestoru pomocou metódy UMAP.

In [38]:
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).

In [39]:
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é.

In [40]:
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ť.

In [41]:
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)

Zobrazenie v mriežke pomocou algoritmu Jonker-Volgenant

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.

In [10]:
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.

In [42]:
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.

Aritmetické operácie s vektormi

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ť:

In [0]:
nbrs = NearestNeighbors(n_neighbors=7, algorithm='brute',
        metric='cosine').fit(df.iloc[:, 1:])

Teraz môžeme otestovať operáciu "kráľ" - "muž" + "žena":

In [143]:
diff = vec("kráľ") - vec("muž") + vec("žena")

dist, ind = nbrs.kneighbors(diff.values.reshape((1, -1)))
print(df.iloc[ind[0], 0])
1042            kráľ
5582        kráľovná
84926     panovníčka
181198         saská
171675      vládkyňa
19972       Kráľovná
20880      kráľovská
Name: 0, dtype: object

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é:


Úloha 1: Krajina a hlavné mesto

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.


In [0]:
 

Úloha 2: Stupňovanie prídavných mien

Otestujte vektorovú aritmetiku na prípade stupňovania prídavných mien (vysoký, vyšší, ...).


In [0]: