Obsah
1. Využití knihovny scikit-learn pro zpracování a analýzu přirozeného jazyka (NLP), 2.část
2. Vektorizace sady textů (korpusu) s využitím třídy CountVectorizer
3. Zkonstruovaná tabulka (slovník) pro zpětný převod indexů na slova
4. Význam parametru cut off pro vektorizaci
5. Význam parametru max_df pro vektorizaci
6. Zpětný převod matice s frekvencí slov na slova
7. Odstranění stopslov v průběhu vektorizace
8. Korektně provedený zpětný převod vektorů na dokumenty
10. Základní vlastnosti výpočtu relevance slov
11. Další příklady výpočtu tf-idf
12. Realizace výpočtu matice s relevancí jednotlivých slov
13. Vliv většího množství dokumentů se stejnými slovy v korpusu na vypočtenou relevanci slov
14. Přímé využití třídy TfidfVectorizer
15. Nalezení textu, který se nejvíce blíží hledanému vzorku
16. Realizace algoritmu pro nalezení nejpodobnější věty
17. Úskalí algoritmu pro hledání podobných textů
18. Vektorizovaný text a neuronové sítě
19. Repositář s demonstračními příklady
1. Využití knihovny scikit-learn pro zpracování a analýzu přirozeného jazyka (NLP), 2.část
Na úvodní článek o využití knihovny scikit-learn pro zpracování přirozeného jazyka (Natural Language Processing – NLP) dnes navážeme. Vysvětlíme si algoritmy pro vektorizaci jazykového korpusu, což je proces, při němž se z jednotlivých textových dokumentů (každý dokument je představován řetězcem) konstruuje matice s numerickými hodnotami. Tyto hodnoty nějakým způsobem reprezentují původní korpus, resp. přesněji řečeno jeho vybrané typické znaky. Navíc tato matice může v sobě mít zakódovány i další vlastnosti, například specifičnost jednotlivých slov v kontextu celého korpusu atd. Tato matice se používá jak pro trénink a validaci modelů (včetně modelů založených na neuronové síti), tak i pro jednodušší operace – nalezení nejbližšího textu (text similarity). I s tímto algoritmem se dnes ve stručnosti seznámíme.
2. Vektorizace sady textů (korpusu) s využitím třídy CountVectorizer
Prvním úkolem, který je nutné provést ještě předtím, než (nějakým způsobem) natrénujeme a následně zvalidujeme model, je převod sady textů (ty označujeme jako korpus) do numerické podoby, konkrétně do 2D matice. Nejjednodušším postupem, který se v této oblasti používá, je konstrukce 2D matice, která má počet řádků odpovídající počtu vstupních dokumentů a počet sloupců odpovídá počtu všech unikátních slov ze všech vstupních dokumentů (tedy z celého korpusu). Do takové matice můžeme zapsat počty jednotlivých slov ve vstupních dokumentech – což ovšem znamená, že ztratíme kontextovou informaci o tom, jaká je sekvence slov v původních dokumentech. Výše uvedený algoritmus je v knihovně scikit-learn realizován třídou CountVectorizer. Ukažme si její použití při vektorizaci korpusu, v němž je pouze šest vět:
# Vektorizace sady textů (korpusu) from sklearn.feature_extraction.text import CountVectorizer # sada textů (korpus) corpus = [ "I'd like an apple or an apple pie", "An apple a day keeps the doctor away", "Never compare an apple to an orange", "I prefer scikit-learn to orange", "The scikit-learn docs are orange and blue", "New apple logo will be orange and blue", ] # provedení vektorizace korpusu vectorizer = CountVectorizer(min_df=1) vectorized = vectorizer.fit_transform(corpus) # výsledek vektorizace - řídká matice print(vectorized) print() # převod na běžnou matici as_array = vectorized.toarray() # zobrazení výsledku v novém formátu print(as_array) # získat vektor z 2D pole flattened = as_array.flatten() # unikátní prvky a jejich frekvence unique, counts = np.unique(flattened, return_counts=True) print("Word count statistic:") print(np.asarray((unique, counts)).T)
Samotná vektorizace je realizována následovně:
# provedení vektorizace korpusu vectorizer = CountVectorizer(min_df=1) vectorized = vectorizer.fit_transform(corpus) # výsledek vektorizace - řídká matice print(vectorized)
Výsledkem vektorizace je řídká matice (sparse matrix), která by měla vypadat takto:
(0, 13) 1 (0, 0) 2 (0, 2) 2 (0, 17) 1 (0, 19) 1 (1, 0) 1 (1, 2) 1 (1, 8) 1 (1, 11) 1 (1, 22) 1 (1, 10) 1 (1, 4) 1 (2, 0) 2 (2, 2) 1 (2, 15) 1 (2, 7) 1 (2, 23) 1 (2, 18) 1 (3, 23) 1 (3, 18) 1 (3, 20) 1 (3, 21) 1 (3, 12) 1 (4, 22) 1 (4, 18) 1 (4, 21) 1 (4, 12) 1 (4, 9) 1 (4, 3) 1 (4, 1) 1 (4, 6) 1 (5, 2) 1 (5, 18) 1 (5, 1) 1 (5, 6) 1 (5, 16) 1 (5, 14) 1 (5, 24) 1 (5, 5) 1
Řídkou matici si můžeme nechat převést na běžnou matici pomocí metody toarray:
# převod na běžnou matici as_array = vectorized.toarray() # zobrazení výsledku v novém formátu print(as_array)
Výsledná matice:
[[2 0 2 0 0 0 0 0 0 0 0 0 0 1 0 0 0 1 0 1 0 0 0 0 0] [1 0 1 0 1 0 0 0 1 0 1 1 0 0 0 0 0 0 0 0 0 0 1 0 0] [2 0 1 0 0 0 0 1 0 0 0 0 0 0 0 1 0 0 1 0 0 0 0 1 0] [0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 1 0 1 1 0 1 0] [0 1 0 1 0 0 1 0 0 1 0 0 1 0 0 0 0 0 1 0 0 1 1 0 0] [0 1 1 0 0 1 1 0 0 0 0 0 0 0 1 0 1 0 1 0 0 0 0 0 1]]
Taktéž si můžeme nechat zobrazit statistické informace – frekvence jednotlivých prvků v celé matici, která obsahuje 25×6=150 prvků:
# získat vektor z 2D pole flattened = as_array.flatten() # unikátní prvky a jejich frekvence unique, counts = np.unique(flattened, return_counts=True) print("Word count statistic:") print(np.asarray((unique, counts)).T)
Ze statistiky je patrné, že matice skutečně obsahuje mnoho nulových prvků a proto je její reprezentace řídkou maticí užitečná (první sloupec zobrazuje hodnotu prvku, druhý počet prvků s touto hodnotou):
Word count statistic: [[ 0 111] [ 1 36] [ 2 3]]
3. Zkonstruovaná tabulka (slovník) pro zpětný převod indexů na slova
Ve skutečnosti není výsledkem vektorizace pouze tabulka s četnostmi jednotlivých slov, protože pouze z této informace bychom nezjistili, o jaká konkrétní slova se jedná. Musíme mít k dispozici i slovník, z něhož bude patrné, že slovo na první pozici (první sloupec matice) je například „ahoj“ a druhé slovo (druhý sloupec matice) je „zeď“. I k těmto údajům ve skutečnosti máme přístup, protože z objektu představujícího výsledek vektorizace lze metodou get_feature_names_out přečíst jména takzvaných atributů (features), což je onen požadovaný slovník:
# Tabulka pro zpětný převod indexů na slova from sklearn.feature_extraction.text import CountVectorizer # sada textů (korpus) corpus = [ "I'd like an apple or an apple pie", "An apple a day keeps the doctor away", "Never compare an apple to an orange", "I prefer scikit-learn to orange", "The scikit-learn docs are orange and blue", "New apple logo will be orange and blue", ] # provedení vektorizace korpusu vectorizer = CountVectorizer(min_df=1) vectorized = vectorizer.fit_transform(corpus) # výsledek vektorizace - řídká matice print(vectorized) print() # slova pro dekódování vah feature_names = vectorizer.get_feature_names_out() print("Feature names count:", len(feature_names)) print() # tabulka indexů slov a vlastních slov print("Index Feature") for i, feature_name in enumerate(feature_names): print(f"{i:2} {feature_name}") print() # převod na běžnou matici as_array = vectorized.toarray() # zobrazení výsledku v novém formátu print(as_array)
V našem konkrétním případě bude slovník obsahovat 25 slov získaných ze všech šesti dokumentů (vět) korpusu:
Feature names count: 25 Index Feature 0 an 1 and 2 apple 3 are 4 away 5 be 6 blue 7 compare 8 day 9 docs 10 doctor 11 keeps 12 learn 13 like 14 logo 15 never 16 new 17 or 18 orange 19 pie 20 prefer 21 scikit 22 the 23 to 24 will
4. Význam parametru cut off pro vektorizaci
Připomeňme si, že při vektorizaci se konstruktoru CountVectorizer předával i parametr min_df, které je ovšem znám spíše pod označením cut off:
# provedení vektorizace korpusu vectorizer = CountVectorizer(min_df=1) vectorized = vectorizer.fit_transform(corpus)
Tímto parametrem se určuje, jaký minimální počet výskytů musí nějaké slovo mít, aby se uložilo do slovníku. Výchozí hodnotou je 1, tj. do slovníku je uloženo jakékoli nalezené slovo. Ovšem pokud například nastavíme tento parametr na 3, bude slovník menší – bude totiž obsahovat pouze slova, která byla nalezena minimálně třikrát atd. Ukažme si vliv tohoto parametru jak na velikost slovníku, tak i na rozměry matice vytvořené vektorizací:
# Tabulka pro zpětný převod indexů na slova, role parametru cut off. from sklearn.feature_extraction.text import CountVectorizer # sada textů (korpus) corpus = [ "I'd like an apple or an apple pie", "An apple a day keeps the doctor away", "Never compare an apple to an orange", "I prefer scikit-learn to orange", "The scikit-learn docs are orange and blue", "New apple logo will be orange and blue", ] for cut_off in range(1, 7): print("Cut off set to:", cut_off) print() # provedení vektorizace korpusu vectorizer = CountVectorizer(min_df=cut_off) vectorized = vectorizer.fit_transform(corpus) # slova pro dekódování vah feature_names = vectorizer.get_feature_names_out() print("Feature names count:", len(feature_names)) print() # tabulka indexů slov a vlastních slov print("Index Feature") for i, feature_name in enumerate(feature_names): print(f"{i:2} {feature_name}") print() # převod na běžnou matici as_array = vectorized.toarray() # zobrazení výsledku v novém formátu print(as_array) print("-" * 100)
Povšimněte si, jak se slovník i matice zmenšuje společně s rostoucí hodnotou cut off:
Cut off set to: 1 Feature names count: 25 Index Feature 0 an 1 and 2 apple 3 are 4 away 5 be 6 blue 7 compare 8 day 9 docs 10 doctor 11 keeps 12 learn 13 like 14 logo 15 never 16 new 17 or 18 orange 19 pie 20 prefer 21 scikit 22 the 23 to 24 will [[2 0 2 0 0 0 0 0 0 0 0 0 0 1 0 0 0 1 0 1 0 0 0 0 0] [1 0 1 0 1 0 0 0 1 0 1 1 0 0 0 0 0 0 0 0 0 0 1 0 0] [2 0 1 0 0 0 0 1 0 0 0 0 0 0 0 1 0 0 1 0 0 0 0 1 0] [0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 1 0 1 1 0 1 0] [0 1 0 1 0 0 1 0 0 1 0 0 1 0 0 0 0 0 1 0 0 1 1 0 0] [0 1 1 0 0 1 1 0 0 0 0 0 0 0 1 0 1 0 1 0 0 0 0 0 1]] ---------------------------------------------------------------------------------------------------- Cut off set to: 2 Feature names count: 9 Index Feature 0 an 1 and 2 apple 3 blue 4 learn 5 orange 6 scikit 7 the 8 to [[2 0 2 0 0 0 0 0 0] [1 0 1 0 0 0 0 1 0] [2 0 1 0 0 1 0 0 1] [0 0 0 0 1 1 1 0 1] [0 1 0 1 1 1 1 1 0] [0 1 1 1 0 1 0 0 0]] ---------------------------------------------------------------------------------------------------- Cut off set to: 3 Feature names count: 3 Index Feature 0 an 1 apple 2 orange [[2 2 0] [1 1 0] [2 1 1] [0 0 1] [0 0 1] [0 1 1]] ---------------------------------------------------------------------------------------------------- Cut off set to: 4 Feature names count: 2 Index Feature 0 apple 1 orange [[2 0] [1 0] [1 1] [0 1] [0 1] [1 1]]
Pro větší hodnoty cut off se již vyhodí výjimka, protože by slovník byl prázdný a matice tudíž měla 0 sloupců:
---------------------------------------------------------------------------------------------------- Cut off set to: 5 Traceback (most recent call last): File "/home/ptisnovs/xy/199_cut_off.py", line 20, in <module> vectorized = vectorizer.fit_transform(corpus) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/home/ptisnovs/.local/lib/python3.11/site-packages/sklearn/base.py", line 1473, in wrapper return fit_method(estimator, *args, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/home/ptisnovs/.local/lib/python3.11/site-packages/sklearn/feature_extraction/text.py", line 1385, in fit_transform X = self._limit_features( ^^^^^^^^^^^^^^^^^^^^^ File "/home/ptisnovs/.local/lib/python3.11/site-packages/sklearn/feature_extraction/text.py", line 1237, in _limit_features raise ValueError( ValueError: After pruning, no terms remain. Try a lower min_df or a higher max_df.
5. Význam parametru max_df pro vektorizaci
Průběh vektorizace lze ovlivnit i parametrem max df, který má opačný význam, než cut off. Umožňuje nám totiž odstranit ze slovníku ta slova, která mají příliš vysoký výskyt. Taková slova (pravděpodobně) nebudou mít vliv na přesnost naučeného modelu a naopak jejich odstraněním můžeme významně zmenšit velikost výsledné matice. Ukažme si nejdříve, jak se tento parametr předává konstruktoru CountVectorizer:
# Tabulka pro zpětný převod indexů na slova role parametru max_df from sklearn.feature_extraction.text import CountVectorizer # sada textů (korpus) corpus = [ "I'd like an apple or an apple pie", "An apple a day keeps the doctor away", "Never compare an apple to an orange", "I prefer scikit-learn to orange", "The scikit-learn docs are orange and blue", "New apple logo will be orange and blue", ] for max_df in range(1, 4): print("max_df set to:", max_df) print() # provedení vektorizace korpusu vectorizer = CountVectorizer(min_df=1, max_df=max_df) vectorized = vectorizer.fit_transform(corpus) # slova pro dekódování vah feature_names = vectorizer.get_feature_names_out() print("Feature names count:", len(feature_names)) print() # tabulka indexů slov a vlastních slov print("Index Feature") for i, feature_name in enumerate(feature_names): print(f"{i:2} {feature_name}") print() # převod na běžnou matici as_array = vectorized.toarray() # zobrazení výsledku v novém formátu print(as_array) print("-" * 100)
První slovník bude obsahovat pouze šestnáct slov:
---------------------------------------------------------------------------------------------------- max_df set to: 1 Feature names count: 16 Index Feature 0 are 1 away 2 be 3 compare 4 day 5 docs 6 doctor 7 keeps 8 like 9 logo 10 never 11 new 12 or 13 pie 14 prefer 15 will [[0 0 0 0 0 0 0 0 1 0 0 0 1 1 0 0] [0 1 0 0 1 0 1 1 0 0 0 0 0 0 0 0] [0 0 0 1 0 0 0 0 0 0 1 0 0 0 0 0] [0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0] [1 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0] [0 0 1 0 0 0 0 0 0 1 0 1 0 0 0 1]]
Druhý slovník již bude mít 22 slov (přidáno bylo například frekventované slovo „and“ a taktéž „the“ atd.):
---------------------------------------------------------------------------------------------------- max_df set to: 2 Feature names count: 22 Index Feature 0 and 1 are 2 away 3 be 4 blue 5 compare 6 day 7 docs 8 doctor 9 keeps 10 learn 11 like 12 logo 13 never 14 new 15 or 16 pie 17 prefer 18 scikit 19 the 20 to 21 will [[0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 1 1 0 0 0 0 0] [0 0 1 0 0 0 1 0 1 1 0 0 0 0 0 0 0 0 0 1 0 0] [0 0 0 0 0 1 0 0 0 0 0 0 0 1 0 0 0 0 0 0 1 0] [0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 1 1 0 1 0] [1 1 0 0 1 0 0 1 0 0 1 0 0 0 0 0 0 0 1 1 0 0] [1 0 0 1 1 0 0 0 0 0 0 0 1 0 1 0 0 0 0 0 0 1]]
A konečně třetí slovník bude mít 23 slov, a to včetně členu „an“:
---------------------------------------------------------------------------------------------------- max_df set to: 3 Feature names count: 23 Index Feature 0 an 1 and 2 are 3 away 4 be 5 blue 6 compare 7 day 8 docs 9 doctor 10 keeps 11 learn 12 like 13 logo 14 never 15 new 16 or 17 pie 18 prefer 19 scikit 20 the 21 to 22 will [[2 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 1 1 0 0 0 0 0] [1 0 0 1 0 0 0 1 0 1 1 0 0 0 0 0 0 0 0 0 1 0 0] [2 0 0 0 0 0 1 0 0 0 0 0 0 0 1 0 0 0 0 0 0 1 0] [0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 1 1 0 1 0] [0 1 1 0 0 1 0 0 1 0 0 1 0 0 0 0 0 0 0 1 1 0 0] [0 1 0 0 1 1 0 0 0 0 0 0 0 1 0 1 0 0 0 0 0 0 1]] ----------------------------------------------------------------------------------------------------
6. Zpětný převod matice s frekvencí slov na slova
V případě, že máme k dispozici matici s frekvencí slov (řádky přitom odpovídají původním dokumentům, sloupce pak odpovídají pořadí slova ve slovníku), můžeme se pokusit o realizaci zpětného převodu matice na původní dokumenty. Již nyní ovšem víme, že nezískáme původní věty (dokumenty), ale pouze skupinu slov tvořících původní větu. To by však mohlo pro mnoho účelů dostačovat. Samotný převod je jednoduchý a je založen na detekci těch prvků v matici, které mají nenulovou hodnotu:
# Realizace zpětného převodu na slova from sklearn.feature_extraction.text import CountVectorizer # sada textů (korpus) corpus = [ "I'd like an apple or an apple pie", "An apple a day keeps the doctor away", "Never compare an apple to an orange", "I prefer scikit-learn to orange", "The scikit-learn docs are orange and blue", "New apple logo will be orange and blue", ] # provedení vektorizace korpusu vectorizer = CountVectorizer(min_df=1) vectorized = vectorizer.fit_transform(corpus) # výsledek vektorizace - řídká matice print(vectorized) print() # slova pro dekódování vah feature_names = vectorizer.get_feature_names_out() print("Feature names count:", len(feature_names)) print() # tabulka indexů slov a vlastních slov print("Index Feature") for i, feature_name in enumerate(feature_names): print(f"{i:2} {feature_name}") print() # převod na běžnou matici as_array = vectorized.toarray() # zobrazení výsledku v novém formátu print(as_array) print() def array_to_words(feature_names, as_array): """Převod matice s frekvencemi slov zpět na slova.""" return list( feature_names[index] for index, included in enumerate(as_array[i]) if included == 1 ) # zpětný převod vět for i in range(len(corpus)): print("Original: ", corpus[i]) print("Vectors: ", as_array[i]) print("As words: ", array_to_words(feature_names, as_array)) print()
Výše uvedená funkce array_to_words sice není napsána korektně (a pokuste se sami zjistit, proč tomu tak je – vysvětlení podáme později), ovšem dokáže z vektoru 25 hodnot, tedy z jednoho řádku tabulky, částečně „zrekonstruovat“ původní věty, resp. slova použitá v těchto větách:
Original: I'd like an apple or an apple pie Vectors: [2 0 2 0 0 0 0 0 0 0 0 0 0 1 0 0 0 1 0 1 0 0 0 0 0] As words: ['like', 'or', 'pie'] Original: An apple a day keeps the doctor away Vectors: [1 0 1 0 1 0 0 0 1 0 1 1 0 0 0 0 0 0 0 0 0 0 1 0 0] As words: ['an', 'apple', 'away', 'day', 'doctor', 'keeps', 'the'] Original: Never compare an apple to an orange Vectors: [2 0 1 0 0 0 0 1 0 0 0 0 0 0 0 1 0 0 1 0 0 0 0 1 0] As words: ['apple', 'compare', 'never', 'orange', 'to'] Original: I prefer scikit-learn to orange Vectors: [0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 1 0 1 1 0 1 0] As words: ['learn', 'orange', 'prefer', 'scikit', 'to'] Original: The scikit-learn docs are orange and blue Vectors: [0 1 0 1 0 0 1 0 0 1 0 0 1 0 0 0 0 0 1 0 0 1 1 0 0] As words: ['and', 'are', 'blue', 'docs', 'learn', 'orange', 'scikit', 'the'] Original: New apple logo will be orange and blue Vectors: [0 1 1 0 0 1 1 0 0 0 0 0 0 0 1 0 1 0 1 0 0 0 0 0 1] As words: ['and', 'apple', 'be', 'blue', 'logo', 'new', 'orange', 'will']
7. Odstranění stopslov v průběhu vektorizace
Již minule jsme se v rychlosti zmínili o takzvaných stopslovech neboli anglicky stopwords. Jedná se většinou o slova, která ve větách mají spíše jen gramatický význam, ovšem nenesou příliš mnoho dalších informací. V angličtině se v první řadě jedná o členy, ale může se jednat i o předložky atd. Taková slova většinou můžeme ze slovníku odstranit (resp. je vůbec do slovníku nepřidávat), aniž by to změnilo kvalitu výsledného modelu. Pokusme se tedy tuto operaci provést při vektorizaci našeho malého korpusu složeného ze šesti vět:
# provedení vektorizace korpusu vectorizer = CountVectorizer(min_df=1, stop_words="english") vectorized = vectorizer.fit_transform(corpus)
Upravený skript bude vypadat následovně:
# Realizace zpětného převodu na slova, odstranění stopslov from sklearn.feature_extraction.text import CountVectorizer # sada textů (korpus) corpus = [ "I'd like an apple or an apple pie", "An apple a day keeps the doctor away", "Never compare an apple to an orange", "I prefer scikit-learn to orange", "The scikit-learn docs are orange and blue", "New apple logo will be orange and blue", ] # provedení vektorizace korpusu vectorizer = CountVectorizer(min_df=1, stop_words="english") vectorized = vectorizer.fit_transform(corpus) # výsledek vektorizace - řídká matice print(vectorized) print() # slova pro dekódování vah feature_names = vectorizer.get_feature_names_out() print("Feature names count:", len(feature_names)) print() # tabulka indexů slov a vlastních slov print("Index Feature") for i, feature_name in enumerate(feature_names): print(f"{i:2} {feature_name}") print() # převod na běžnou matici as_array = vectorized.toarray() # zobrazení výsledku v novém formátu print(as_array) print() def array_to_words(feature_names, as_array): """Převod matice s frekvencemi slov zpět na slova.""" return list( feature_names[index] for index, included in enumerate(as_array[i]) if included == 1 ) # zpětný převod vět for i in range(len(corpus)): print("Original: ", corpus[i]) print("Vectors: ", as_array[i]) print("As words: ", array_to_words(feature_names, as_array)) print()
Nyní bude slovník menší, protože namísto 25 slov bude obsahovat jen slov šestnáct:
Feature names count: 16 Index Feature 0 apple 1 away 2 blue 3 compare 4 day 5 docs 6 doctor 7 keeps 8 learn 9 like 10 logo 11 new 12 orange 13 pie 14 prefer 15 scikit
Stejně tak se zmenší matice získaná vektorizací:
[[2 0 0 0 0 0 0 0 0 1 0 0 0 1 0 0] [1 1 0 0 1 0 1 1 0 0 0 0 0 0 0 0] [1 0 0 1 0 0 0 0 0 0 0 0 1 0 0 0] [0 0 0 0 0 0 0 0 1 0 0 0 1 0 1 1] [0 0 1 0 0 1 0 0 1 0 0 0 1 0 0 1] [1 0 1 0 0 0 0 0 0 0 1 1 1 0 0 0]]
Při obnově původních vět nyní budou pochopitelně všechna stopslova chybět – tuto informaci jsme ztratili při vektorizaci:
Original: I'd like an apple or an apple pie Vectors: [2 0 0 0 0 0 0 0 0 1 0 0 0 1 0 0] As words: ['like', 'pie'] Original: An apple a day keeps the doctor away Vectors: [1 1 0 0 1 0 1 1 0 0 0 0 0 0 0 0] As words: ['apple', 'away', 'day', 'doctor', 'keeps'] Original: Never compare an apple to an orange Vectors: [1 0 0 1 0 0 0 0 0 0 0 0 1 0 0 0] As words: ['apple', 'compare', 'orange'] Original: I prefer scikit-learn to orange Vectors: [0 0 0 0 0 0 0 0 1 0 0 0 1 0 1 1] As words: ['learn', 'orange', 'prefer', 'scikit'] Original: The scikit-learn docs are orange and blue Vectors: [0 0 1 0 0 1 0 0 1 0 0 0 1 0 0 1] As words: ['blue', 'docs', 'learn', 'orange', 'scikit'] Original: New apple logo will be orange and blue Vectors: [1 0 1 0 0 0 0 0 0 0 1 1 1 0 0 0] As words: ['apple', 'blue', 'logo', 'new', 'orange']
8. Korektně provedený zpětný převod vektorů na dokumenty
Předchozí dva demonstrační příklady vypisovaly pouze ta slova, která se v daném dokumentu (v našem případě v jedné větě) vyskytovaly pouze (přesně) jedenkrát. Ovšem pochopitelně není možné ignorovat opakující se slova, takže funkci array_to_words je nutné nepatrně modifikovat do následující podoby:
def array_to_words(feature_names, as_array): """Převod matice s frekvencemi slov zpět na slova.""" return list( feature_names[index] for index, included in enumerate(as_array[i]) if included >= 1 )
Upravený skript:
# Realizace zpětného převodu na slova (korektní práce s opakujícími se slovy) from sklearn.feature_extraction.text import CountVectorizer # sada textů (korpus) corpus = [ "I'd like an apple or an apple pie", "An apple a day keeps the doctor away", "Never compare an apple to an orange", "I prefer scikit-learn to orange", "The scikit-learn docs are orange and blue", "New apple logo will be orange and blue", ] # provedení vektorizace korpusu vectorizer = CountVectorizer(min_df=1, stop_words="english") vectorized = vectorizer.fit_transform(corpus) # výsledek vektorizace - řídká matice print(vectorized) print() # slova pro dekódování vah feature_names = vectorizer.get_feature_names_out() print("Feature names count:", len(feature_names)) print() # tabulka indexů slov a vlastních slov print("Index Feature") for i, feature_name in enumerate(feature_names): print(f"{i:2} {feature_name}") print() # převod na běžnou matici as_array = vectorized.toarray() # zobrazení výsledku v novém formátu print(as_array) print() def array_to_words(feature_names, as_array): """Převod matice s frekvencemi slov zpět na slova.""" return list( feature_names[index] for index, included in enumerate(as_array[i]) if included >= 1 ) # zpětný převod vět for i in range(len(corpus)): print("Original: ", corpus[i]) print("Vectors: ", as_array[i]) print("As words: ", array_to_words(feature_names, as_array)) print()
Nyní jsou již výsledky korektní – zpětně zrekonstruované věty bez stopslov a bez správného pořadí slov:
Original: I'd like an apple or an apple pie Vectors: [2 0 0 0 0 0 0 0 0 1 0 0 0 1 0 0] As words: ['apple', 'like', 'pie'] Original: An apple a day keeps the doctor away Vectors: [1 1 0 0 1 0 1 1 0 0 0 0 0 0 0 0] As words: ['apple', 'away', 'day', 'doctor', 'keeps'] Original: Never compare an apple to an orange Vectors: [1 0 0 1 0 0 0 0 0 0 0 0 1 0 0 0] As words: ['apple', 'compare', 'orange'] Original: I prefer scikit-learn to orange Vectors: [0 0 0 0 0 0 0 0 1 0 0 0 1 0 1 1] As words: ['learn', 'orange', 'prefer', 'scikit'] Original: The scikit-learn docs are orange and blue Vectors: [0 0 1 0 0 1 0 0 1 0 0 0 1 0 0 1] As words: ['blue', 'docs', 'learn', 'orange', 'scikit'] Original: New apple logo will be orange and blue Vectors: [1 0 1 0 0 0 0 0 0 0 1 1 1 0 0 0] As words: ['apple', 'blue', 'logo', 'new', 'orange']
9. Tf-idf: relevance a specifičnost jednotlivých slov vztažená k aktuálnímu dokumentu i k celému korpusu
Již minule jsme se zmínili o algoritmu pro výpočet relevance jednotlivých slov. Zjednodušeně řečeno se jedná o numerickou hodnotu určující, do jaké míry je nějaké slovo specifické pro určitý dokument. Slova s nízkým ohodnocením jsou příliš obecná a vyskytují se v mnoha dokumentech. A naopak slova s vysokým ohodnocením jsou specifická pro menší počet dokumentů (či dokonce pro jediný dokument). A právě taková slova mají význam jak pro hledání nejbližšího dokumentu, tak i pro trénink modelů.
Můžeme si to ukázat na příkladu. Mějme jazykový korpus, ve kterém budou uloženy texty 1000 knih v angličtině. Slovo „the“ bude mít velmi nízkou specifičnost, protože se vyskytuje v každém dokumentu (knize). Naopak slovo „newspeak“ bude mít vysokou specifičnost v knize „1984“ (vyskytuje se zde mnohokrát) a naopak nízkou nebo nulovou specifičnost v dalších knihách. To například znamená, že model může rozpoznat část textu a říci „toto patří do knihy 1984“ apod. Podobně to může fungovat u algoritmu pro vyhledání podobných textů (vět).
10. Základní vlastnosti výpočtu relevance slov
V následujícím skriptu vektorizujeme korpus obsahující pouze trojici dokumentů (řetězců), přičemž každý řetězec obsahuje pouze tři slova. Všechna slova jsou v tomto případě striktně unikátní – neopakují se:
import numpy as np from sklearn.feature_extraction.text import TfidfVectorizer # sada textů (korpus) corpus = [ "foo bar baz", "apple pear orange", "Go Python Clojure", ] # provedení vektorizace korpusu vectorizer = TfidfVectorizer(min_df=1) vectorized = vectorizer.fit_transform(corpus) # slova pro dekódování vah feature_names = vectorizer.get_feature_names_out() for index, feature in enumerate(feature_names): print(index, feature) print() # výsledek vektorizace - řídká matice: převod na běžnou matici as_array = vectorized.toarray() # zobrazení výsledku v novém formátu np.set_printoptions(precision=2) print(as_array)
Skript nejprve zobrazí vytvořený slovník:
0 apple 1 bar 2 baz 3 clojure 4 foo 5 go 6 orange 7 pear 8 python
Následně se zobrazí vektorizovaná podoba matice:
[[0. 0.58 0.58 0. 0.58 0. 0. 0. 0. ] [0.58 0. 0. 0. 0. 0. 0.58 0.58 0. ] [0. 0. 0. 0.58 0. 0.58 0. 0. 0.58]]
Otázkou je, jakým způsobem se došlo k hodnotám 0,58 (naproti tomu nula je zřejmá – slovo se v daném dokumentu vůbec nevyskytuje).
Provádí se tento výpočet:
idf = log((1+n) / (1 + df(t))) + 1
Dosadíme tedy n=3 (počet dokumentů) a df(t)=1 (počet dokumentů s výskytem slova – to je v našem případě vždy rovno jedné):
idf = log((1+3) / (1 + 1)) + 1 = log(4/2) + 1 = 1,301
Vektory obsahující tyto hodnoty se ovšem ještě normalizují a vzhledem k tomu, že každý vektor obsahuje trojici stejných hodnot, je výpočet poměrně triviální (uvádím ho pro jeden prvek, ale rozšířit se musí na všech šest prvků):
v[1] = 1,301 ÷ sqrt(1,3^2+1,3^2+1,3^2) = 0,577
tedy po zaokrouhlení získáme vektory s prvky 0.58.
11. Další příklady výpočtu tf-idf
Podívejme se nyní na další příklady výpočtu tf-idf. Začneme příkladem, který opět obsahuje tři dokumenty v korpusu, ovšem druhý dokument má jen dvě slova a nikoli slova tři:
import numpy as np from sklearn.feature_extraction.text import TfidfVectorizer # sada textů (korpus) corpus = [ "foo bar baz", "apple orange", "Go Python Clojure", ] # provedení vektorizace korpusu vectorizer = TfidfVectorizer(min_df=1) vectorized = vectorizer.fit_transform(corpus) # slova pro dekódování vah feature_names = vectorizer.get_feature_names_out() for index, feature in enumerate(feature_names): print(index, feature) print() # výsledek vektorizace - řídká matice: převod na běžnou matici as_array = vectorized.toarray() # zobrazení výsledku v novém formátu np.set_printoptions(precision=2) print(as_array)
Kvůli odlišně provedené normalizaci budou ve druhém řádku odlišné hodnoty:
v[1] = 1,301 ÷ sqrt(1,3^2+1,3^2) = 0,707 ≈ 0,71
Což si lze snadno ověřit:
0 apple 1 bar 2 baz 3 clojure 4 foo 5 go 6 orange 7 python [[0. 0.58 0.58 0. 0.58 0. 0. 0. ] [0.71 0. 0. 0. 0. 0. 0.71 0. ] [0. 0. 0. 0.58 0. 0.58 0. 0.58]]
Pojďme ještě dále a upravme poslední dokument tak, že bude obsahovat jen jediné slovo:
import numpy as np from sklearn.feature_extraction.text import TfidfVectorizer # sada textů (korpus) corpus = [ "foo bar baz", "apple orange", "Go", ] # provedení vektorizace korpusu vectorizer = TfidfVectorizer(min_df=1) vectorized = vectorizer.fit_transform(corpus) # slova pro dekódování vah feature_names = vectorizer.get_feature_names_out() for index, feature in enumerate(feature_names): print(index, feature) print() # výsledek vektorizace - řídká matice: převod na běžnou matici as_array = vectorized.toarray() # zobrazení výsledku v novém formátu np.set_printoptions(precision=2) print(as_array)
Zde normalizace výsledek nijak „nespraví“:
v[1] = 1,301 ÷ sqrt(1,3^2) = 1,301/1,301 = 1
To je ostatně patrné i na posledním řádku matice, který obsahuje jedinou hodnotu rovnou jedničce:
0 apple 1 bar 2 baz 3 foo 4 go 5 orange [[0. 0.58 0.58 0.58 0. 0. ] [0.71 0. 0. 0. 0. 0.71] [0. 0. 0. 0. 1. 0. ]]
A konečně si ukažme případ, v němž se jedno slovo „foo“ opakuje ve dvou dokumentech – v dokumentu prvním a třetím:
import numpy as np from sklearn.feature_extraction.text import TfidfVectorizer # sada textů (korpus) corpus = [ "foo bar baz", "apple orange", "Go foo", ] # provedení vektorizace korpusu vectorizer = TfidfVectorizer(min_df=1) vectorized = vectorizer.fit_transform(corpus) # slova pro dekódování vah feature_names = vectorizer.get_feature_names_out() for index, feature in enumerate(feature_names): print(index, feature) print() # výsledek vektorizace - řídká matice: převod na běžnou matici as_array = vectorized.toarray() # zobrazení výsledku v novém formátu np.set_printoptions(precision=2) print(as_array)
Výsledky nyní budou odlišné:
0 apple 1 bar 2 baz 3 foo 4 go 5 orange [[0. 0.62 0.62 0.47 0. 0. ] [0.71 0. 0. 0. 0. 0.71] [0. 0. 0. 0.61 0.8 0. ]]
Proč tomu tak je? Změní se výpočet idf pro slovo „foo“. Zatímco u slov, které se neopakovaly, stále platí:
idf = log((1+3) / (1 + 1)) + 1 = log(4/2) + 1 = 1,301
tak u slova „foo“ jsou hodnoty odlišné:
idf = log((1 + 3) / (2 + 1)) + 1 = log(4/3) + 1 = 1,12
Tato menší hodnota (pochopitelně po normalizaci) ovlivní právě čtvrtý sloupec v tabulce.
12. Realizace výpočtu matice s relevancí jednotlivých slov
V dalším kroku se pokusme o realizaci jednoduché „pipeline“, ve které se nejprve vypočtou frekvence slov s využitím nám již známé třídy CountVectorizer a následně se provede transformace na hodnoty odpovídající tf-idf. Tato druhá transformace je založena na třídě TfidfTransformer. Postup je stále totožný a odpovídá ustálenému vzoru – konstrukce instance objektu následovaná voláním metody fit, přičemž instanci třídy TfidfTransformer se předává výsledek vektorizace:
vectorizer = CountVectorizer(min_df=1, stop_words="english") vectorized = vectorizer.fit_transform(corpus) # výpočet IDF - převrácené četnosti slov transformer = TfidfTransformer(smooth_idf=True, use_idf=True) transformer.fit(vectorized)
Tento postup je ukázán v následujícím skriptu:
# Výpočet relevance jednotlivých slov from sklearn.feature_extraction.text import CountVectorizer, TfidfTransformer # sada textů (korpus) corpus = [ "I'd like an apple or an apple pie", "An apple a day keeps the doctor away", "Never compare an apple to an orange", "I prefer scikit-learn to orange", "The scikit-learn docs are orange and blue", "New apple logo will be orange and blue", ] # provedení vektorizace korpusu vectorizer = CountVectorizer(min_df=1, stop_words="english") vectorized = vectorizer.fit_transform(corpus) # slova pro dekódování vah feature_names = vectorizer.get_feature_names_out() # převod na běžnou matici as_array = vectorized.toarray() # výpočet IDF - převrácené četnosti slov transformer = TfidfTransformer(smooth_idf=True, use_idf=True) transformer.fit(vectorized) # zobrazení tabulky s převrácenými četnostmi jednotlivých slov for feature_name, idf in zip(feature_names, transformer.idf_): print(f"{feature_name:9} {idf:5.2}")
Tento skript nejdříve zobrazí matici s frekvencemi slov:
[[2 0 0 0 0 0 0 0 0 1 0 0 0 1 0 0] [1 1 0 0 1 0 1 1 0 0 0 0 0 0 0 0] [1 0 0 1 0 0 0 0 0 0 0 0 1 0 0 0] [0 0 0 0 0 0 0 0 1 0 0 0 1 0 1 1] [0 0 1 0 0 1 0 0 1 0 0 0 1 0 0 1] [1 0 1 0 0 0 0 0 0 0 1 1 1 0 0 0]]
Následně se zobrazí slovník i s vypočtenými hodnotami tf-idf:
apple 1.3 away 2.3 blue 1.8 compare 2.3 day 2.3 docs 2.3 doctor 2.3 keeps 2.3 learn 1.8 like 2.3 logo 2.3 new 2.3 orange 1.3 pie 2.3 prefer 2.3 scikit 1.8
13. Vliv většího množství dokumentů se stejnými slovy v korpusu na vypočtenou relevanci slov
Pokusme se nyní do zpracovávaného korpusu přidat větší množství dalších „dokumentů“, které budou obsahovat stejná slova. To provedeme docela snadno:
# věty pro vyšší četnosti vybraných slov for i in range(20): corpus.append("blue day")
Skript by po svém spuštění měl zobrazit odlišné hodnoty tf-idf u slov „blue“ a „day“, konkrétně by se měly hodnoty u těchto slov snížit. A kvůli způsobu výpočtu se naopak zvýší hodnoty u ostatních slov:
# Výpočet relevance jednotlivých slov from sklearn.feature_extraction.text import CountVectorizer, TfidfTransformer # sada textů (korpus) corpus = [ "I'd like an apple or an apple pie", "An apple a day keeps the doctor away", "Never compare an apple to an orange", "I prefer scikit-learn to orange", "The scikit-learn docs are orange and blue", "New apple logo will be orange and blue", ] # věty pro vyšší četnosti vybraných slov for i in range(20): corpus.append("blue day") # provedení vektorizace korpusu vectorizer = CountVectorizer(min_df=1, stop_words="english") vectorized = vectorizer.fit_transform(corpus) # slova pro dekódování vah feature_names = vectorizer.get_feature_names_out() # převod na běžnou matici as_array = vectorized.toarray() # výpočet IDF - převrácené četnosti slov transformer = TfidfTransformer(smooth_idf=True, use_idf=True) transformer.fit(vectorized) # zobrazení tabulky s převrácenými četnostmi jednotlivých slov for feature_name, idf in zip(feature_names, transformer.idf_): print(f"{feature_name:9} {idf:5.2}")
Původní vypočtená matice s frekvencemi slov:
[[2 0 0 0 0 0 0 0 0 1 0 0 0 1 0 0] [1 1 0 0 1 0 1 1 0 0 0 0 0 0 0 0] [1 0 0 1 0 0 0 0 0 0 0 0 1 0 0 0] [0 0 0 0 0 0 0 0 1 0 0 0 1 0 1 1] [0 0 1 0 0 1 0 0 1 0 0 0 1 0 0 1] [1 0 1 0 0 0 0 0 0 0 1 1 1 0 0 0] [0 0 1 0 1 0 0 0 0 0 0 0 0 0 0 0] [0 0 1 0 1 0 0 0 0 0 0 0 0 0 0 0] [0 0 1 0 1 0 0 0 0 0 0 0 0 0 0 0] [0 0 1 0 1 0 0 0 0 0 0 0 0 0 0 0] [0 0 1 0 1 0 0 0 0 0 0 0 0 0 0 0] [0 0 1 0 1 0 0 0 0 0 0 0 0 0 0 0] [0 0 1 0 1 0 0 0 0 0 0 0 0 0 0 0] [0 0 1 0 1 0 0 0 0 0 0 0 0 0 0 0] [0 0 1 0 1 0 0 0 0 0 0 0 0 0 0 0] [0 0 1 0 1 0 0 0 0 0 0 0 0 0 0 0] [0 0 1 0 1 0 0 0 0 0 0 0 0 0 0 0] [0 0 1 0 1 0 0 0 0 0 0 0 0 0 0 0] [0 0 1 0 1 0 0 0 0 0 0 0 0 0 0 0] [0 0 1 0 1 0 0 0 0 0 0 0 0 0 0 0] [0 0 1 0 1 0 0 0 0 0 0 0 0 0 0 0] [0 0 1 0 1 0 0 0 0 0 0 0 0 0 0 0] [0 0 1 0 1 0 0 0 0 0 0 0 0 0 0 0] [0 0 1 0 1 0 0 0 0 0 0 0 0 0 0 0] [0 0 1 0 1 0 0 0 0 0 0 0 0 0 0 0] [0 0 1 0 1 0 0 0 0 0 0 0 0 0 0 0]]
Hodnoty tf-idf vypočtené u jednotlivých slov:
apple 2.7 away 3.6 blue 1.2 compare 3.6 day 1.2 docs 3.6 doctor 3.6 keeps 3.6 learn 3.2 like 3.6 logo 3.6 new 3.6 orange 2.7 pie 3.6 prefer 3.6 scikit 3.2
Zajímavé bude porovnání hodnot z předchozí kapitoly a této kapitoly:
apple 1.3 2.7 away 2.3 3.6 blue 1.8 1.2 (menší) compare 2.3 3.6 day 2.3 1.2 (menší) docs 2.3 3.6 doctor 2.3 3.6 keeps 2.3 3.6 learn 1.8 3.2 like 2.3 3.6 logo 2.3 3.6 new 2.3 3.6 orange 1.3 2.7 pie 2.3 3.6 prefer 2.3 3.6 scikit 1.8 3.2
14. Přímé využití třídy TfidfVectorizer
V předchozím textu jsme si ukázali, jakým způsobem je možné zkombinovat třídu CountVectorizer společně se třídou TfidfTransformer do jednoduché „pipeline“, jejímž výsledkem bude ohodnocení slov v jazykovém korpusu. Ovšem v praxi se setkáme spíše se třídou TfidfVectorizer, která provádí (poněkud zjednodušeně řečeno) obě operace současně. Výsledkem takového výpočtu je matice s váhami slov podle jejich specifičnosti v kontextu jednoho dokumentu (řádek matice) i celého korpusu (sloupec matice). Vlastnosti takových matic i způsob výpočtu jsme si již ukázali v desáté a jedenácté kapitole.
Pokusme se tedy upravit demonstrační příklad z předchozí kapitoly tak, aby se při vektorizaci přímo používala třída TfidfVectorizer. Je to ve skutečnosti velmi snadné:
# Přímý výpočet relevance jednotlivých slov from sklearn.feature_extraction.text import TfidfVectorizer # sada textů (korpus) corpus = [ "I'd like an apple or an apple pie", "An apple a day keeps the doctor away", "Never compare an apple to an orange", "I prefer scikit-learn to orange", "The scikit-learn docs are orange and blue", "New apple logo will be orange and blue", ] # provedení vektorizace korpusu vectorizer = TfidfVectorizer(min_df=1, stop_words="english") vectorized = vectorizer.fit_transform(corpus) # výsledek vektorizace - řídká matice print(vectorized) print() # převod na běžnou matici as_array = vectorized.toarray() # zobrazení výsledku v novém formátu print(as_array)
Výsledek ve formě řídké matice (zobrazují se tedy prvky s nenulovou hodnotou):
(0, 9) 0.5417030933272916 (0, 0) 0.642740629927257 (0, 13) 0.5417030933272916 (1, 0) 0.2843820458449716 (1, 4) 0.47935551837885115 (1, 7) 0.47935551837885115 (1, 6) 0.47935551837885115 (1, 1) 0.47935551837885115 (2, 0) 0.45448625796567665 (2, 3) 0.7660838613629144 (2, 12) 0.45448625796567665 (3, 12) 0.3612599391251273 (3, 14) 0.6089412039859956 (3, 15) 0.49934049829319643 (3, 8) 0.49934049829319643 (4, 12) 0.32320592952117017 (4, 15) 0.44674150776102867 (4, 8) 0.44674150776102867 (4, 5) 0.5447972125961776 (4, 2) 0.44674150776102867 (5, 0) 0.32286546559023394 (5, 12) 0.32286546559023394 (5, 2) 0.4462709119088114 (5, 11) 0.5442233252270988 (5, 10) 0.5442233252270988
Následuje výsledek ve formě běžné matice. Čím vyšší je nějaká hodnota, tím specifičtější slovo bylo použito (a současně i tím kratší je dokument):
[[0.64 0. 0. 0. 0. 0. 0. 0. 0. 0.54 0. 0. 0. 0.54 0. 0. ] [0.28 0.48 0. 0. 0.48 0. 0.48 0.48 0. 0. 0. 0. 0. 0. 0. 0. ] [0.45 0. 0. 0.77 0. 0. 0. 0. 0. 0. 0. 0. 0.45 0. 0. 0. ] [0. 0. 0. 0. 0. 0. 0. 0. 0.5 0. 0. 0. 0.36 0. 0.61 0.5 ] [0. 0. 0.45 0. 0. 0.54 0. 0. 0.45 0. 0. 0. 0.32 0. 0. 0.45] [0.32 0. 0.45 0. 0. 0. 0. 0. 0. 0. 0.54 0.54 0.32 0. 0. 0. ]]
15. Nalezení textu, který se nejvíce blíží hledanému vzorku
Ukažme si nyní jedno praktické použití matic s hodnotami tf-idf. Naimplementujeme si algoritmus, která se v korpusu pokusí najít větu (dokument), který se nejvíce blíží hledanému vzorku. Existuje hned několik metod, jak toho dosáhnout, ovšem nejjednodušší a vlastně i nejrychlejší metoda je založena právě na použití matice s hodnotami tf-idf.
Vstupem bude matice získaná vektorizací korpusu. Ta bude obsahovat například následujících sedm vektorů pro sedm dokumentů (řetězců):
[[0.6427 0. 0. 0. 0. 0. 0. 0. 0. 0.5417 0. 0. 0. 0.5417 0. 0. ] [0.2844 0.4794 0. 0. 0.4794 0. 0.4794 0.4794 0. 0. 0. 0. 0. 0. 0. 0. ] [0.4545 0. 0. 0.7661 0. 0. 0. 0. 0. 0. 0. 0. 0.4545 0. 0. 0. ] [0. 0. 0. 0. 0. 0. 0. 0. 0.4993 0. 0. 0. 0.3613 0. 0.6089 0.4993] [0. 0. 0.4467 0. 0. 0.5448 0. 0. 0.4467 0. 0. 0. 0.3232 0. 0. 0.4467] [0.3229 0. 0.4463 0. 0. 0. 0. 0. 0. 0. 0.5442 0.5442 0.3229 0. 0. 0. ]]
Jak však zjistit, které dokumenty se nejvíce podobají? Můžeme využít toho, že podobné dokumenty budou pravděpodobně obsahovat podobná slova a největší váhu mají mít slova s vyšší hodnotou v matici, protože tato slova jsou více specifická. Takže vlastně můžeme využít skalárních součinů vektorů získaných z jazykového korpusu, který bude obsahovat jak hledanou větu, tak i vzorek. Skalární součin dvou vektorů můžeme vypočítat snadno, ovšem všechny skalární součiny jsou de facto realizovány maticovým součinem, konkrétně součinem výše uvedené matice se stejnou maticí, která je však nejdříve transponována. Výsledkem by měla být matice 7×7 prvků:
[[1. 0.1828 0.2921 0. 0. 0.2075] [0.1828 1. 0.1292 0. 0. 0.0918] [0.2921 0.1292 1. 0.1642 0.1469 0.2935] [0. 0. 0.1642 1. 0.5629 0.1166] [0. 0. 0.1469 0.5629 1. 0.3037] [0.2075 0.0918 0.2935 0.1166 0.3037 1. ]]
V této nové matici prvek A[i,j] říká, jak moc se podobají dokumenty s indexy i a j, přičemž pochopitelně platí, že dokument se nejvíce podobá sobě samotnému (hlavní diagonála). Z tohoto důvodu nejdříve z hlavní diagonály hodnoty vymažeme (nahradíme je za „nečísla“). Dostaneme následující matici:
[[ nan 0.1828 0.2921 0. 0. 0.2075] [0.1828 nan 0.1292 0. 0. 0.0918] [0.2921 0.1292 nan 0.1642 0.1469 0.2935] [0. 0. 0.1642 nan 0.5629 0.1166] [0. 0. 0.1469 0.5629 nan 0.3037] [0.2075 0.0918 0.2935 0.1166 0.3037 nan]]
Lze tedy relativně snadno zjistit, že nejvíce se podobají dokumenty číslo 3 a 4 (číslováno od nuly) a nejméně například dokument 0 a 3. A poslední dokument je nejvíce podobný dokumentu číslo 4.
16. Realizace algoritmu pro nalezení nejpodobnější věty
Vyzkoušejme si nyní konkrétní realizaci algoritmu, s nímž jsme se právě seznámili. Je zde provedena transpozice matice s výpočtem součinu matice představující původní vektorizovaný korpus s transponovanou maticí. Navíc jsou z výsledné matice odstraněny prvky na hlavní diagonále a následně je tato nová matice použita pro nalezení, která další věta z korpusu je nejbližší větě „New apple logo will be orange and blue“:
# Nalezení textu, který se nejvíce blíží hledanému vzorku import numpy as np from sklearn.feature_extraction.text import TfidfVectorizer # sada textů (korpus) corpus = [ "I'd like an apple or an apple pie", "An apple a day keeps the doctor away", "Never compare an apple to an orange", "I prefer scikit-learn to orange", "The scikit-learn docs are orange and blue", "New apple logo will be orange and blue", ] # provedení vektorizace korpusu vectorizer = TfidfVectorizer(min_df=1, stop_words="english") vectorized = vectorizer.fit_transform(corpus) # výsledek vektorizace - řídká matice print(vectorized) print() # matice s převrácenými četnostmi pairwise_similarity = vectorized * vectorized.T print(pairwise_similarity) print() print(pairwise_similarity.toarray()) print() arr = pairwise_similarity.toarray() # odstranění jedniček na hlavní diagonále np.fill_diagonal(arr, np.nan) print(arr) print() # hledaná věta input_doc = "New apple logo will be orange and blue" input_idx = corpus.index(input_doc) print(input_idx, input_doc) result_idx = np.nanargmax(arr[input_idx]) print(result_idx, corpus[result_idx])
Tento skript nejdříve vypíše použité matice (ty jsme si však již uvedli v předchozím textu, takže je zde nebudu opakovat) a nakonec určí, která věta je nejbližší větě zadané. Je to věta číslo 4 z našeho jazykového korpusu:
4 The scikit-learn docs are orange and blue
17. Úskalí algoritmu pro hledání podobných textů
Algoritmus určený pro vyhledávání podobných textů v realizaci, v jaké byl právě představen, trpí jednou zásadní vadou – nedokáže totiž korektně rozlišit pořadí slov a navíc (většinou) jsou z vektorizovaného textu odstraněna stopslova, která v některých případech mohou měnit smysl textu. To například může znamenat, že dvě věty (nebo delší sekvence textu), které mají zcela opačný význam kvůli použití slov jako „no“, „not“ atd., budou ve skutečnosti považovány za věty velmi si podobné.
Oba zmíněné nedostatky je možné do značné míry odstranit takovým způsobem, že při vektorizaci jazykového korpusu budeme do slovníku ukládat nikoli samostatná slova, ale například dvojice slov. Celý slovník se pochopitelně zvětší a tím pádem se zvětší i matice s vektorizovaným textem, ovšem v některých situacích je to jedno z mála praktických řešení, které je navíc možné velmi snadno implementovat. Základy tohoto postupu si vysvětlíme v navazujícím článku.
18. Vektorizovaný text a neuronové sítě
Vraťme se ještě na chvíli k minulému článku, v němž jsme se pokusili natrénovat model určený pro rozeznávání pozitivně, negativně nebo neutrálně naladěných tweetů o dopravcích. Samozřejmě nám nic nebrání v tom, abychom pro model využili neuronovou síť a (možná?) tak zlepšili jeho predikční schopnosti. Ukažme si, jak by celý postup mohl vypadat. Neuronovou síť prozatím ponecháme v původní podobě – nebudeme ji tedy nijak „dolaďovat“:
# Trénink a predikce modelu nad vektorizovanými daty import pandas as pd import re import matplotlib.pyplot as plt from nltk.corpus import stopwords from sklearn.model_selection import train_test_split from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.metrics import ConfusionMatrixDisplay from sklearn.metrics import classification_report, confusion_matrix, accuracy_score # model zalozeny na neuronove siti from sklearn.neural_network import MLPClassifier # načtení tabulky do datového rámce s předzpracováním numerických dat airline_tweets = pd.read_csv("Tweets_airlines.csv") # hodnocení (pozitivní, negativní, neutrální) labels = airline_tweets["airline_sentiment"].values # vlastní text hodnocení features = airline_tweets["text"].values def process_feature(feature): """Preprocesing textových dat.""" # odstranění speciálních znaků a dalšího smetí processed_feature = re.sub(r"\W", " ", feature) # odstranění samostatných znaků (oddělených bílými znaky) processed_feature = re.sub(r"\s+[a-zA-Z]\s+", " ", processed_feature) # odstranění samostatných znaků na začátku vět processed_feature = re.sub(r"\^[a-zA-Z]\s+", " ", processed_feature) # náhrada více mezer (nebo jiných bílých znaků) za jedinou mezeru processed_feature = re.sub(r"\s+", " ", processed_feature, flags=re.I) # odstranění prefixů ^b processed_feature = re.sub(r"^b\s+", "", processed_feature) # konverze výsledku na malá písmena return processed_feature.lower() # preprocesing všech hodnocení processed_features = [process_feature(feature) for feature in features] # hodnoty použité později pro trénink modelu print("Labels:") print(labels) print("Number of labels:", len(labels)) print() print("Features:") print(processed_features) print("Number of features:", len(processed_features)) print() # vektorizace textu vectorizer = TfidfVectorizer( max_features=2500, min_df=7, max_df=0.8, stop_words=stopwords.words("english") ) vectorized_features = vectorizer.fit_transform(processed_features).toarray() # klasické rozdělení datové sady na trénovací a testovací část trainX, testX, trainY, testY = train_test_split( vectorized_features, labels, test_size=0.2, random_state=0 ) # konstrukce vybraného modelu s předáním hyperparametrů classifier = MLPClassifier(max_iter=5000) # trénink modelu classifier.fit(trainX, trainY) # predikce modelu pro testovací vstupy (ne pro trénovací data) predictions = classifier.predict(testX) # vyhodnocení kvality modelu print(classification_report(testY, predictions)) print(accuracy_score(testY, predictions)) # matice záměn - absolutní hodnoty disp = ConfusionMatrixDisplay.from_estimator( classifier, testX, testY, cmap=plt.cm.Blues, normalize=None, ) # zobrazení matice v textové podobě print(disp.confusion_matrix) # uložení výsledků ve formě rastrového obrázku plt.savefig("189_1.png") # vizualizace matice plt.show() # matice záměn - relativní hodnoty disp = ConfusionMatrixDisplay.from_estimator( classifier, testX, testY, cmap=plt.cm.Blues, normalize="true", ) # zobrazení matice v textové podobě print(disp.confusion_matrix) # uložení výsledků ve formě rastrového obrázku plt.savefig("189_2.png") # vizualizace matice plt.show()
Matice záměn budou vypadat takto:
Obrázek 1: Matice záměn s absolutními hodnotami.
Obrázek 2: Matice záměn s relativními hodnotami.
19. Repositář s demonstračními příklady
Všechny demonstrační příklady využívající knihovnu Scikit-learn lze nalézt v repositáři https://github.com/tisnik/most-popular-python-libs. Následují odkazy na jednotlivé příklady i na (Jupyter) diáře s postupem výpočtů a analýz:
V repositáři nalezneme taktéž projektový soubor a Jupyter Notebook s vysvětlením, jak lze modely využít pro rozpoznávání obsahu rastrových obrázků:
# | Příklad | Stručný popis | Adresa příkladu |
---|---|---|---|
1 | pyproject.toml | projektový soubor (pro PDM) se všemi závislostmi | https://github.com/tisnik/most-popular-python-libs/blob/master/sklearn/pyproject.toml |
2 | pdm.lock | lock soubor s konkrétními verzemi všech přímých i tranzitivních závislostí | https://github.com/tisnik/most-popular-python-libs/blob/master/sklearn/pdm.lock |
3 | Rozpoznání_obrazu_scikit-learn.ipynb | Jupyter notebook s celým postupem | https://github.com/tisnik/most-popular-python-libs/blob/master/sklearn/Rozpoznání_obrazu_scikit-learn.ipynb |
4 | particle_life.py | emergence: příklad vzniku struktury | https://github.com/tisnik/most-popular-python-libs/blob/master/particles/particle_life.py |
20. Odkazy na Internetu
- Python for NLP: Sentiment Analysis with Scikit-Learn
https://stackabuse.com/python-for-nlp-sentiment-analysis-with-scikit-learn/ - Datová sada – hodnocení leteckých dopravců
https://raw.githubusercontent.com/satyajeetkrjha/kaggle-Twitter-US-Airline-Sentiment-/refs/heads/master/Tweets.csv - Twitter_US_Airline_Sentiment_Analysis
https://github.com/rustagijanvi/Twitter_US_Airline_Sentiment_Analysis/tree/main - Shluková analýza (clustering) a knihovna Scikit-learn
https://www.root.cz/clanky/shlukova-analyza-clustering-a-knihovna-scikit-learn/ - Shluková analýza (clustering) a knihovna Scikit-learn (2)
https://www.root.cz/clanky/shlukova-analyza-clustering-a-knihovna-scikit-learn-2/ - Shluková analýza (clustering) a knihovna Scikit-learn (z plochy do 3D prostoru)
https://www.root.cz/clanky/shlukova-analyza-clustering-a-knihovna-scikit-learn-z-plochy-do-3d-prostoru/ - Rozpoznávání obrázků knihovnou Scikit-learn: první kroky
https://www.root.cz/clanky/rozpoznavani-obrazku-knihovnou-scikit-learn-prvni-kroky/ - scikit-learn: Machine Learning in Python
https://scikit-learn.org/stable/index.html - Sklearn-pandas
https://github.com/scikit-learn-contrib/sklearn-pandas - sklearn-xarray
https://github.com/phausamann/sklearn-xarray/ - Clustering
https://scikit-learn.org/stable/modules/clustering.html - Cluster analysis (Wikipedia)
https://en.wikipedia.org/wiki/Cluster_analysis - Shluková analýza (Wikipedia)
https://cs.wikipedia.org/wiki/Shlukov%C3%A1_anal%C3%BDza - K-means
https://cs.wikipedia.org/wiki/K-means - k-means clustering
https://en.wikipedia.org/wiki/K-means_clustering - Spectral clustering
https://en.wikipedia.org/wiki/Spectral_clustering - Emergence
https://cs.wikipedia.org/wiki/Emergence - Particle Life: Vivid structures from rudimentary rules
https://particle-life.com/ - Hertzsprungův–Russellův diagram
https://cs.wikipedia.org/wiki/Hertzsprung%C5%AFv%E2%80%93Russell%C5%AFv_diagram - Using Machine Learning in an HR Diagram
https://cocalc.com/share/public_paths/08b6e03583cbdef3cdb9813a54ec68ff773c747f - Gaia H-R diagrams: Querying Gaia data for one million nearby stars
https://vlas.dev/post/gaia-dr2-hrd/ - The Hertzsprung–Russell diagram
https://scipython.com/book2/chapter-9-data-analysis-with-pandas/problems/p92/the-hertzsprung-russell-diagram/ - Animated Hertzsprung-Russell Diagram with 119,614 datapoints
https://github.com/zonination/h-r-diagram - Neuraxle Pipelines
https://github.com/Neuraxio/Neuraxle - scikit-learn: Getting Started
https://scikit-learn.org/stable/getting_started.html - Support Vector Machines
https://scikit-learn.org/stable/modules/svm.html - Use Deep Learning to Detect Programming Languages
http://searene.me/2017/11/26/use-neural-networks-to-detect-programming-languages/ - Natural-language processing
https://en.wikipedia.org/wiki/Natural-language_processing - THE MNIST DATABASE of handwritten digits
http://yann.lecun.com/exdb/mnist/ - MNIST database (Wikipedia)
https://en.wikipedia.org/wiki/MNIST_database - MNIST For ML Beginners
https://www.tensorflow.org/get_started/mnist/beginners - Stránka projektu Torch
http://torch.ch/ - Torch: Serialization
https://github.com/torch/torch7/blob/master/doc/serialization.md - Torch: modul image
https://github.com/torch/image/blob/master/README.md - Data pro neuronové sítě
http://archive.ics.uci.edu/ml/index.php - Torch na GitHubu (několik repositářů)
https://github.com/torch - Torch (machine learning), Wikipedia
https://en.wikipedia.org/wiki/Torch_%28machine_learning%29 - Torch Package Reference Manual
https://github.com/torch/torch7/blob/master/README.md - Torch Cheatsheet
https://github.com/torch/torch7/wiki/Cheatsheet - Neural network containres (Torch)
https://github.com/torch/nn/blob/master/doc/containers.md - Simple layers
https://github.com/torch/nn/blob/master/doc/simple.md#nn.Linear - Transfer Function Layers
https://github.com/torch/nn/blob/master/doc/transfer.md#nn.transfer.dok - Feedforward neural network
https://en.wikipedia.org/wiki/Feedforward_neural_network - Biologické algoritmy (4) – Neuronové sítě
https://www.root.cz/clanky/biologicke-algoritmy-4-neuronove-site/ - Biologické algoritmy (5) – Neuronové sítě
https://www.root.cz/clanky/biologicke-algoritmy-5-neuronove-site/ - Umělá neuronová síť (Wikipedia)
https://cs.wikipedia.org/wiki/Um%C4%9Bl%C3%A1_neuronov%C3%A1_s%C3%AD%C5%A5 - PyTorch
http://pytorch.org/ - JupyterLite na PyPi
https://pypi.org/project/jupyterlite/ - JupyterLite na GitHubu
https://github.com/jupyterlite/jupyterlite - Dokumentace k projektu JupyterLite
https://github.com/jupyterlite/jupyterlite - Matplotlib Home Page
http://matplotlib.org/ - Matplotlib (Wikipedia)
https://en.wikipedia.org/wiki/Matplotlib - Popis barvových map modulu matplotlib.cm
https://gist.github.com/endolith/2719900#id7 - Ukázky (palety) barvových map modulu matplotlib.cm
http://matplotlib.org/examples/color/colormaps_reference.html - Galerie grafů vytvořených v Matplotlibu
https://matplotlib.org/3.2.1/gallery/ - 3D rendering
https://en.wikipedia.org/wiki/3D_rendering - 3D computer graphics
https://en.wikipedia.org/wiki/3D_computer_graphics - Primary 3D view planes
https://matplotlib.org/stable/gallery/mplot3d/view_planes_3d.html - Getting started in scikit-learn with the famous iris dataset
https://www.youtube.com/watch?v=hd1W4CyPX58 - Training a machine learning model with scikit-learn
https://www.youtube.com/watch?v=RlQuVL6-qe8 - Iris (plant)
https://en.wikipedia.org/wiki/Iris_(plant) - Kosatec
https://cs.wikipedia.org/wiki/Kosatec - Iris setosa
https://en.wikipedia.org/wiki/Iris_setosa - Iris versicolor
https://en.wikipedia.org/wiki/Iris_versicolor - Iris virginica
https://en.wikipedia.org/wiki/Iris_virginica - Druh
https://cs.wikipedia.org/wiki/Druh - Iris subg. Limniris
https://en.wikipedia.org/wiki/Iris_subg._Limniris - Iris Dataset Classification with Python: A Tutorial
https://www.pycodemates.com/2022/05/iris-dataset-classification-with-python.html - Iris flower data set
https://en.wikipedia.org/wiki/Iris_flower_data_set - List of datasets for machine-learning research
https://en.wikipedia.org/wiki/List_of_datasets_for_machine-learning_research - Analýza hlavních komponent
https://cs.wikipedia.org/wiki/Anal%C3%BDza_hlavn%C3%ADch_komponent - Principal component analysis
https://en.wikipedia.org/wiki/Principal_component_analysis - Scikit-learn Crash Course – Machine Learning Library for Python
https://www.youtube.com/watch?v=0B5eIE_1vpU - calm-notebooks
https://github.com/koaning/calm-notebooks - Should you teach Python or R for data science?
https://www.dataschool.io/python-or-r-for-data-science/ - nbviewer: A simple way to share Jupyter Notebooks
https://nbviewer.org/ - AI vs Machine Learning (Youtube)
https://www.youtube.com/watch?v=4RixMPF4×is - Machine Learning | What Is Machine Learning? | Introduction To Machine Learning | 2024 | Simplilearn (Youtube)
https://www.youtube.com/watch?v=ukzFI9rgwfU - A Gentle Introduction to Machine Learning (Youtube)
https://www.youtube.com/watch?v=Gv9_4yMHFhI - Machine Learning vs Deep Learning
https://www.youtube.com/watch?v=q6kJ71tEYqM - Umělá inteligence (slajdy)
https://slideplayer.cz/slide/12119218/ - Úvod do umělé inteligence
https://slideplayer.cz/slide/2505525/ - Umělá inteligence I / Artificial Intelligence I
https://ktiml.mff.cuni.cz/~bartak/ui/ - Matplotlib vs. seaborn vs. Plotly vs. MATLAB vs. ggplot2 vs. pandas
https://ritza.co/articles/matplotlib-vs-seaborn-vs-plotly-vs-MATLAB-vs-ggplot2-vs-pandas/ - Matplotlib, Seaborn or Plotnine?
https://www.reddit.com/r/datascience/comments/jvrqxt/matplotlib_seaborn_or_plotnine/ - @Rabeez: Rabeez/plotting_comparison.ipynb
https://gist.github.com/Rabeez/ffc0b59d4a41e20fa8d944c44a96adbc - Matplotlib, Seaborn, Plotly and Plotnine Comparison
https://python.plainenglish.io/matplotlib-seaborn-plotly-and-plotnine-comparison-baf2db5a9c40 - Data Visualization 101: How to Choose a Python Plotting Library
https://towardsdatascience.com/data-visualization-101-how-to-choose-a-python-plotting-library-853460a08a8a - Data science in Python: pandas, seaborn, scikit-learn
https://www.youtube.com/watch?v=3ZWuPVWq7p4 - 7.2. Real world datasets
https://scikit-learn.org/stable/datasets/real_world.html#california-housing-dataset - 7.2.7. California Housing dataset
https://scikit-learn.org/stable/datasets/real_world.html#california-housing-dataset - Comprehensive Guide to Classification Models in Scikit-Learn
https://www.geeksforgeeks.org/comprehensive-guide-to-classification-models-in-scikit-learn/ - Tidy Data Visualization: ggplot2 vs seaborn
https://blog.tidy-intelligence.com/posts/ggplot2-vs-seaborn/ - seaborn: statistical data visualization
https://seaborn.pydata.org/ - Linear regression (Wikipedia)
https://en.wikipedia.org/wiki/Linear_regression - Lineární regrese (Wikipedia)
https://cs.wikipedia.org/wiki/Line%C3%A1rn%C3%AD_regrese - Iris Flower Classification with MLP Classifier
https://www.metriccoders.com/post/iris-flower-classification-with-mlp-classifier