Obsah
1. Využití knihovny scikit-learn pro zpracování a analýzu přirozeného jazyka (NLP), 3.část
2. Získání datové sady a slovníku se slopslovy
3. Pokus o načtení datové sady do datového rámce (dataframe)
4. Specifikace kódování znaků v CSV souboru při jeho načítání do datového rámce
5. Zjištění statistiky o ohodnocení textu
6. Sloupce s očekávanými výsledky i s vlastním textem SMS
7. Vektorizace dat SMSek s výpisem výsledného slovníku
8. Výpis obsahu slovníku ve více sloupcích
9. Vektorizace všech SMSek z datové sady
10. Preprocesing textů z SMSsek s vyčištěním dat
11. Vektorizace textových dat po jejich filtraci
12. Trénink a predikce modelu nad vektorizovanými daty založený na třídě CountVectorizer
13. Trénink a predikce modelu nad vektorizovanými daty založený na třídě TfidfVectorizer
14. Jak pracovat s kontextem: řešení založené na n-gramech
15. Vektorizace textových dat s použitím n-gramů o délce 1–2 slov s výpisem výsledného slovníku
16. Trénink a predikce modelu s využitím třídy CountVectorizer při vektorizaci n-gramů
17. Trénink a predikce modelu s využitím třídy TfidfVectorizer při vektorizaci n-gramů
18. Zjištění vlivu minimální a maximální délky n-gramů na kvalitu modelu
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), 3.část
Již potřetí se dnes vrátíme k problematice zpracování a analýzy přirozeného jazyka (Natural Language Processing neboli NLP) v Pythonu s využitím knihovny scikit-learn. Budeme se zabývat velmi často řešenou úlohou – a to konkrétně analýzou, zda je předložený text spam nebo se jedná o jiný typ textu (označuje se termínem ham). A aby nebyl objem zpracovávaných dat obrovský, budeme tuto analýzu provádět nad datovou sadou obsahující anglicky psané SMSky, tj. relativně krátké texty. Jedná se tedy o zadání, které se do jisté míry podobá úloze, kterou jsme již řešili, konkrétně k vytvoření modelu pro zjištění, zda jsou tweety laděny pozitivně, negativně nebo neutrálně.
Při řešení dnes použijeme novou techniku vektorizace, která není založena na vektorizaci jednotlivých slov, ale na zjištění, jaké kombinace slov se v textu vyskytují. Vektorizovat se budou právě tyto kombinace slov, tj. například „dobrý den“ by byl samostatný záznam ve slovníku. Díky tomu bude možné lépe podchytit kontext a (možná) tak zlepšit predikční schopnosti modelu (i když, jak uvidíme dále, pro krátké SMSky od mnoha autorů to nebude tak výrazné). Ovšem v oboru NLP se s n-gramy pracuje poměrně často.
2. Získání datové sady a slovníku se slopslovy
V prvním kroku si, jak je již v tomto seriálu zvykem, stáhneme datovou sadu, kterou použijeme pro trénink a validaci modelu. Tato datová sada je tvořena jediným souborem ve formátu CSV, který je dostupný na adrese https://www.kaggle.com/datasets/uciml/sms-spam-collection-dataset?resource=download (ve skutečnosti je nutné si projít potvrzovacím dialogem, na druhou stranu celá platforma Kaggle nabízí i další užitečné materiály a dokonce i celé Jupyter notebooky, takže je vhodné tento krok „přetrpět“).
Obrázek 1: Obsah staženého souboru CSV po jeho importu do spreadsheetu.
Dále budeme v některých skriptech vyžadovat slovník s takzvanými stopslovy (stopwords), o nichž jsme se již zmínili předminule a minule. Jedná se o slova, která v textu nenesou žádnou skutečně užitečnou informaci a mohou být odfiltrována. Tento slovník stáhneme jednoduchým skriptem založeným na knihovně NLTK. Vše se stáhne do adresáře ntkl_data umístěného v domovském adresáři:
# Stažení slovníku, který bude použit pro předzpracování textu v dalších # demonstračních příkladech. import nltk # tento příkaz zajistí stažení příslušných datových souborů nltk.download("stopwords")
3. Pokus o načtení datové sady do datového rámce (dataframe)
Již při analýze datové sady s tweety o dopravcích jsme celý soubor ve formátu CSV nejprve (před jeho dalším zpracováním) načetli do datového rámce (dataframe) s využitím knihovny Pandas. Toto načtení se obešlo bez problémů, takže se pokusme o provedení naprosto stejné operace, ovšem nyní nad souborem spam.csv získaným v rámci předchozí kapitoly:
# Pokus o načtení datové sady a zjištění základních údajů import pandas as pd # načtení tabulky do datového rámce spam = pd.read_csv("spam.csv") # základní informace o datovém rámci print(spam.describe())
Nyní ovšem tato operace vyvolá výjimku, která by měla vypadat zhruba následovně:
Traceback (most recent call last): File "/home/ptisnovs/xy/205_spam_read.py", line 6, in <module> spam = pd.read_csv("spam.csv") ^^^^^^^^^^^^^^^^^^^^^^^ File "/home/ptisnovs/.local/lib/python3.11/site-packages/pandas/io/parsers/readers.py", line 1026, in read_csv return _read(filepath_or_buffer, kwds) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/home/ptisnovs/.local/lib/python3.11/site-packages/pandas/io/parsers/readers.py", line 620, in _read parser = TextFileReader(filepath_or_buffer, **kwds) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/home/ptisnovs/.local/lib/python3.11/site-packages/pandas/io/parsers/readers.py", line 1620, in __init__ self._engine = self._make_engine(f, self.engine) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/home/ptisnovs/.local/lib/python3.11/site-packages/pandas/io/parsers/readers.py", line 1898, in _make_engine return mapping[engine](f, **self.options) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/home/ptisnovs/.local/lib/python3.11/site-packages/pandas/io/parsers/c_parser_wrapper.py", line 93, in __init__ self._reader = parsers.TextReader(src, **kwds) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "parsers.pyx", line 574, in pandas._libs.parsers.TextReader.__cinit__ File "parsers.pyx", line 663, in pandas._libs.parsers.TextReader._get_header File "parsers.pyx", line 874, in pandas._libs.parsers.TextReader._tokenize_rows File "parsers.pyx", line 891, in pandas._libs.parsers.TextReader._check_tokenize_status File "parsers.pyx", line 2053, in pandas._libs.parsers.raise_parser_error UnicodeDecodeError: 'utf-8' codec can't decode bytes in position 606-607: invalid continuation byte
4. Specifikace kódování znaků v CSV souboru při jeho načítání do datového rámce
Pokusme se nyní o načtení souboru ve formátu CSV do datového rámce se specifikací kódování znaků. Již víme, že soubor, který jsme stáhli, nepoužívá UTF-8 a vlastně i nepřímo víme, že se nejedná o čisté ASCII (to je totiž podmnožinou UTF-8). Pokusme se tedy použít nějaké osmibitové kódování, kdy Pandas ve skutečnosti nemůže zjistit žádné chyby – každý bajt je převeden do jednoho znaku, ať již obsahuje jakoukoli hodnotu. Případné „paznaky“ později odfiltrujeme. Vzhledem k tomu, že kódování jen odhadujeme, zkusme například zadat latin1, což je jedno ze základních osmibitových rozšíření původní sedmibitové znakové sady ASCII:
# Načtení datové sady a zjištění základních údajů o načteném datovém rámci import pandas as pd # načtení tabulky do datového rámce, specifikace kódování souboru spam = pd.read_csv("spam.csv", encoding="latin1") # základní informace o datovém rámci print(spam.describe())
Nyní se soubor načte a vytvoří se z něho kýžený datový rámec. Z jeho popisu je patrné, že obsahuje dva skutečné datové sloupce nazvané v1 a v2 a taktéž je patrné, že některé řádky (ale je jich jen 12 z celkového počtu 5572) jsou uloženy tak netypickým způsobem, že byl programový kód načítající CSV při dekódování tak zmaten, že předpokládal, že věta obsahuje oddělovač jednotlivých buněk (což je ostatně pro formát CSV typické – tento formát nabízí velkou volnost a není tak dobře přenositelný, jak bychom si přáli):
v1 v2 ... Unnamed: 3 Unnamed: 4 count 5572 5572 ... 12 6 unique 2 5169 ... 10 5 top ham Sorry, I'll call later ... MK17 92H. 450Ppw 16" GNT:-)" freq 4825 30 ... 2 2 [4 rows x 5 columns]
Pro zajímavost se na jeden z problémových řádků podívejme:
spam,"Your free ringtone is waiting to be collected. Simply text the password \MIX\"" to 85069 to verify. Get Usher and Britney. FML", PO Box 5249," MK17 92H. 450Ppw 16""",
To je skutečně pro většinou „načítačů“ CSV matoucí.
5. Zjištění statistiky o ohodnocení textu
Další postup již vlastně známe. Zjistíme, jaké údaje jsou zapsány v prvním sloupci datového rámce. Předpokladem přitom je, že tento sloupec bude obsahovat pouze dvě možné hodnoty, konkrétně „ham“ nebo „spam“. Údaje o tom, jaké hodnoty jsou v tomto sloupci uloženy a navíc i informace o počtu těchto hodnot (frekvenci) zjistíme s využitím metody value_counts() zavolané nad objektem představujícím sloupec v1 (což je objekt typu datové řady – Serie)
# Načtení datové sady do datového rámce a zjištění statistiky o ohodnocení textu import pandas as pd # načtení tabulky do datového rámce, specifikace kódování souboru spam = pd.read_csv("spam.csv", encoding="latin1") # početm spamů a hamů print(spam.v1.value_counts())
Z výsledků je patrné, že v tomto sloupci jsou skutečně (korektně) uloženy pouze dvě možné hodnoty. Navíc je patrné, že více SMS není ohodnoceno jako spam (spamu je pouze 15%), což v důsledku ovlivní i trénink modelu a vypočtené matice záměn (viz další text):
v1 ham 4825 spam 747 Name: count, dtype: int64
6. Sloupce s očekávanými výsledky i s vlastním textem SMS
Vzhledem k tomu, že sloupce datové sady (přesněji řečeno první dva sloupce) mají taková jména, která jsou v jazyce Python platnými identifikátory, můžeme snadno přečíst hodnoty uložené v těchto sloupcích – názvy těchto sloupců jsou totiž současně i názvy atributů datového rámce spam. První sloupec přitom obsahuje očekávané výsledky (ohodnocení textu) a druhý sloupec vlastní, nijak nezpracovaný text SMS:
# hodnocení (spam/ham) labels = spam.v1.values # vlastní text SMS features = spam.v2.values
Další skript zobrazí jak několik vybraných hodnot z obou sloupců, tak i počet zde uložených hodnot:
# Načtení datové sady, charakteristiky sloupců s návěstím a vlastním textem import pandas as pd # načtení tabulky do datového rámce, specifikace kódování souboru spam = pd.read_csv("spam.csv", encoding="latin1") # hodnocení (spam/ham) labels = spam.v1.values # vlastní text SMS features = spam.v2.values # hodnoty použité později pro trénink modelu print("Labels:") print(labels) print("Number of labels:", len(labels)) print() print("Features:") print(features) print("Number of features:", len(features))
První sloupec (včetně počtu hodnot):
Labels: ['ham' 'ham' 'spam' ... 'ham' 'ham' 'ham'] Number of labels: 5572
Druhý sloupec:
Features: ['Go until jurong point, crazy.. Available only in bugis n great world la e buffet... Cine there got amore wat...' 'Ok lar... Joking wif u oni...' "Free entry in 2 a wkly comp to win FA Cup final tkts 21st May 2005. Text FA to 87121 to receive entry question(std txt rate)T&C's apply 08452810075over18's" ... 'Pity, * was in mood for that. So...any other suggestions?' "The guy did some bitching but I acted like i'd be interested in buying something else next week and he gave it to us for free" 'Rofl. Its true to its name'] Number of features: 5572
7. Vektorizace dat SMSek s výpisem výsledného slovníku
Nyní provedeme vektorizaci SMSek, tj. textových hodnot uložených ve druhém sloupci nazvaném v2. Samotnou základní vektorizaci již dobře známe. Pro tento účel zpočátku použijeme třídu CountVectorizer, zkonstruuje slovník z celého korpusu (tedy ze všech textů). Tento slovník lze z výsledného objektu získat metodou get_feature_names_out. A podobně lze získat řídkou matici s frekvencemi jednotlivých slov.
Nás však nejprve bude zajímat pouze samotný slovník, tj. jaká slova jsou v něm uložena:
# Vektorizace textových dat, výpis výsledného slovníku import pandas as pd import re from nltk.corpus import stopwords from sklearn.feature_extraction.text import CountVectorizer # načtení tabulky do datového rámce, specifikace kódování souboru spam = pd.read_csv("spam.csv", encoding="latin1") # hodnocení (spam/ham) labels = spam.v1.values # vlastní text SMS features = spam.v2.values # hodnoty použité později pro trénink modelu print("Labels:") print(labels) print("Number of labels:", len(labels)) print() print("Features:") print(features) print("Number of features:", len(features)) print() # vektorizace textu vectorizer = CountVectorizer( max_features=2500, min_df=7, max_df=0.8, stop_words=stopwords.words("english") ) vectorized_features = vectorizer.fit_transform(features).toarray() # slova pro dekódování vah feature_names = vectorizer.get_feature_names_out() print("Feature names count:", len(feature_names)) print("Feature names:") for feature_name in feature_names: print(feature_name)
Z výpisu (zde notně zkráceného) je patrné, že kromě skutečných slov zde nalezneme čísla a na konci různé „paznaky“, které možná vznikly až při exportu SMSek:
Feature names count: 1267 Feature names: 00 000 02 03 04 06 0800 08000839402 08000930705 0870 08712460324 08718720201 10 100 1000 10am 10p 11 11mths 12 12hrs 1327 150 150p 150ppm ... ... ... leaves leaving lect left leh lei lemme less lesson lessons let lets liao library life lift light ... ... ... us use used user usf using usual valentine valentines valid valued via video vikky visit vl voice ... ... ... xx xxx xy ya yahoo yar yeah year years yep yes yesterday yet yijue yo yr yrs yup ì_ ìï û_
8. Výpis obsahu slovníku ve více sloupcích
Pro zajímavost si ukažme trik použitelný pro tisk obsahu slovníku ve více sloupcích. Trik spočívá ve výběru každého n-tého prvku (pro čtyři sloupce bude n=4) postupně s offsetem 0, 1 a 2:
columns = 4 c1 = feature_names[::columns], c1 = feature_names[1::columns], c1 = feature_names[2::columns], c1 = feature_names[3::columns],
Tyto struktury by teoreticky mělo být možné vytisknout s využitím standardní funkce zip:
columns = 4 for c1, c2, c3, c4 in zip( feature_names[::columns], feature_names[1::columns], feature_names[2::columns], feature_names[3::columns], ): ... ... ...
To však nebude plně korektní ve chvíli, kdy budou mít sloupce odlišný počet prvků, tj. když původní počet prvků ve slovníku nebude dělitelný čtyřmi. Oprava spočívá v tom, že namísto funkce zip použijeme funkci zip_longest naimportovanou ze standardní knihovny itertools:
columns = 4 for c1, c2, c3, c4 in zip_longest( feature_names[::columns], feature_names[1::columns], feature_names[2::columns], feature_names[3::columns], ): ... ... ...
Upravený skript pro výpočet a zobrazení slovníku bude mít tuto podobu:
# Vektorizace textových dat, výpis výsledného slovníku import pandas as pd import re from itertools import zip_longest from nltk.corpus import stopwords from sklearn.feature_extraction.text import CountVectorizer # načtení tabulky do datového rámce, specifikace kódování souboru spam = pd.read_csv("spam.csv", encoding="latin1") # hodnocení (spam/ham) labels = spam.v1.values # vlastní text SMS features = spam.v2.values # hodnoty použité později pro trénink modelu print("Labels:") print(labels) print("Number of labels:", len(labels)) print() print("Features:") print(features) print("Number of features:", len(features)) print() # vektorizace textu vectorizer = CountVectorizer( max_features=2500, min_df=7, max_df=0.8, stop_words=stopwords.words("english") ) vectorized_features = vectorizer.fit_transform(features).toarray() # slova pro dekódování vah feature_names = vectorizer.get_feature_names_out() print("Feature names count:", len(feature_names)) print("Feature names:") columns = 4 for c1, c2, c3, c4 in zip_longest( feature_names[::columns], feature_names[1::columns], feature_names[2::columns], feature_names[3::columns], ): print(f"{c1: <20}{c2: <20}{c3: <20}{c4}")
A takto vypadá výsledný slovník, resp. jeho vybraná část při tisku do čtyř sloupců:
Feature names count: 1267 Feature names: 00 000 02 03 04 06 0800 08000839402 08000930705 0870 08712460324 08718720201 10 100 1000 10am 10p 11 11mths 12 12hrs 1327 150 150p 150ppm 16 18 1st 20 200 2000 2003 2004 20p 25 250 25p 2day 2lands 2nd 2nite 30 3030 350 36504 3g 4u 50 ... ... ... dont door double download draw dream dreams drink drive driving drop drugs dude dun dunno dvd earlier early easy eat eating eg eh either else em email end ending ends energy england enjoy enough enter entered entitled entry especially etc eve even evening ever every everyone everything ex exam excellent excuse experience expires extra eyes face facebook fact family fancy fantastic far fast fat father fault feb feel feeling feels felt figure film final finally find fine fingers finish finished first fixed following fone food forever forget forgot forward forwarded found fr free freemsg freephone frens fri friday friend friends ... ... .... meeting meh member men merry message messages met mid midnight might min mind mine mins minute minutes miracle miss missed missing mistake mm mo mob mobile mobiles mobileupd8 mode model mom moment mon monday money month months mood moon moral morning mother motorola move movie movies mp3 mr mrng mrt msg msgs mu much mum murdered murderer music must muz na nah naked name national naughty near need needs net network neva never new news next ni8 nice nigeria night ... ... ... slave sleep sleeping slept slow slowly small smile smiling smoke sms smth snow sofa sol somebody someone something sometimes somewhere song sony soon sorry sort sound sounds south sp space speak special specially spend spent spree st stand start started starting starts statement station stay staying std still stop store story street ... ... ... wk wkly woke wonder wonderful wondering wont word words work workin working world worried worries worry worse worth wot would wow write wrong wun www xmas xx xxx xy ya yahoo yar yeah year years yep yes yesterday yet yijue yo yr yrs yup ì_ ìï û_ None
9. Vektorizace všech SMSek z datové sady
Pro vlastní vektorizaci SMSsek opět, stejně jako v předchozích dvou příkladech, použijeme třídu CountVectorizer, která nejprve zkonstruuje slovník z celého korpusu (tedy ze všech textů) a následně každou SMS nahradí vektorem obsahujícím v i-tém prvku počet výskytů slova z i-tého místa ve slovníku v dané SMS. Z těchto vektorů se pak vytvoří matice, přičemž výsledné rozměry matice jsou n sloupců a m řádků, kde n odpovídá velikosti slovníku a m počtu SMSsek, které se zpracovaly (což odpovídá velikosti korpusu).
# Vektorizace textových dat import pandas as pd import re from nltk.corpus import stopwords from sklearn.feature_extraction.text import CountVectorizer # načtení tabulky do datového rámce, specifikace kódování souboru spam = pd.read_csv("spam.csv", encoding="latin1") # hodnocení (spam/ham) labels = spam.v1.values # vlastní text SMS features = spam.v2.values # hodnoty použité později pro trénink modelu print("Labels:") print(labels) print("Number of labels:", len(labels)) print() print("Features:") print(features) print("Number of features:", len(features)) # vektorizace textu vectorizer = CountVectorizer( max_features=2500, min_df=7, max_df=0.8, stop_words=stopwords.words("english") ) vectorized_features = vectorizer.fit_transform(features).toarray() # slova pro dekódování vah feature_names = vectorizer.get_feature_names_out() print("Feature names count:", len(feature_names)) print() # vlastní výsledek vektorizace print("Sparse matrix of size", vectorized_features.shape, ":") print() # ukázka způsobu zakódování print("Selected tweet:") print("Original: ", features[2]) print("Vectorized: ", vectorized_features[2]) print() print("word# weight meaning") for i, f in enumerate(vectorized_features[2]): if f > 0: print(f"{i:4} {f:5} {feature_names[i]}")
Tento skript nejprve vypíše ohodnocení SMSek:
Labels: ['ham' 'ham' 'spam' ... 'ham' 'ham' 'ham'] Number of labels: 5572
Dále vypíše část samotných obsahů SMSsek:
Features: ['Go until jurong point, crazy.. Available only in bugis n great world la e buffet... Cine there got amore wat...' 'Ok lar... Joking wif u oni...' "Free entry in 2 a wkly comp to win FA Cup final tkts 21st May 2005. Text FA to 87121 to receive entry question(std txt rate)T&C's apply 08452810075over18's" ... 'Pity, * was in mood for that. So...any other suggestions?' "The guy did some bitching but I acted like i'd be interested in buying something else next week and he gave it to us for free" 'Rofl. Its true to its name'] Number of features: 5572
Důležitější je však údaj o rozměrech řídké matice. Ta má celkem 1276 sloupců (počet slov ve slovníku) a 5572 řádků (počet SMSek):
Feature names count: 1267 Sparse matrix of size (5572, 1267) :
V dalším kroku je ukázána vybraná SMSka. Nejprve je zobrazen její původní text:
Selected tweet: Original: Free entry in 2 a wkly comp to win FA Cup final tkts 21st May 2005. Text FA to 87121 to receive entry question(std txt rate)T&C's apply 08452810075over18's
Poté se zobrazí vektor odpovídající slovům v SMSce. Tento vektor obsahuje prakticky samé nuly, takže se nejedná o praktický výpis:
Vectorized: [0 0 0 ... 0 0 0]
A následně se zobrazí pouze ta slova ve vektoru, která se ve vybrané SMSce vyskytují alespoň jedenkrát:
word# weight meaning 104 1 apply 238 1 comp 271 1 cup 357 2 entry 393 1 final 412 1 free 664 1 may 867 1 question 874 1 rate 888 1 receive 1030 1 std 1073 1 text 1133 1 txt 1210 1 win 1221 1 wkly
10. Preprocesing textů z SMSsek s vyčištěním dat
Vraťme se ještě ke slovníku, který jsme získali a vypsali v příkladu uvedeném v osmé kapitole. Jak je patrné, obsahuje tento slovník velké množství částí textu, který nelze považovat za běžná slova. Bude tedy vhodné provést nějaký preprocesing dat, v jehož rámci odstraníme ze vstupních textů různé „paznaky“ (ostatně zpracováváme anglický text), čísla atd. Pro tento účel sice existují specializované knihovny, my si ovšem prozatím vystačíme se sekvencí regulárních výrazů, odstraněním znaků, které nepatří do ASCII pomocí „pipeline“ představované příkazem:
processed_feature = processed_feature.encode("ascii", errors="ignore").decode()
který je následován konverzí SMSky na malá písmena s odstraněním přebytečných bílých znaků na začátku a na konci zprávy:
processed_feature.strip().lower()
Celý skript, který tuto činnost provádí, vypadá následovně:
# Vektorizace textových dat, výpis výsledného slovníku import pandas as pd import re from itertools import zip_longest from nltk.corpus import stopwords from sklearn.feature_extraction.text import CountVectorizer # načtení tabulky do datového rámce, specifikace kódování souboru spam = pd.read_csv("spam.csv", encoding="latin1") # hodnocení (spam/ham) labels = spam.v1.values # vlastní text SMS features = spam.v2.values # hodnoty použité později pro trénink modelu print("Labels:") print(labels) print("Number of labels:", len(labels)) print() print("Features:") print(features) print("Number of features:", len(features)) print() 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í slov s číslicemi processed_feature = re.sub("\w*\d\w*", "", processed_feature) # odstranění prefixů ^b processed_feature = re.sub(r"^b\s+", "", processed_feature) # odstranění znaků, které nejsou ASCII processed_feature = processed_feature.encode("ascii", errors="ignore").decode() # konverze výsledku na malá písmena return processed_feature.strip().lower() # preprocesing všech hodnocení processed_features = [process_feature(feature) for feature in features] # vektorizace textu vectorizer = CountVectorizer( max_features=2500, min_df=7, max_df=0.8, stop_words=stopwords.words("english") ) vectorized_features = vectorizer.fit_transform(processed_features).toarray() # slova pro dekódování vah feature_names = vectorizer.get_feature_names_out() print("Feature names count:", len(feature_names)) print("Feature names:") columns = 4 for c1, c2, c3, c4 in zip_longest( feature_names[::columns], feature_names[1::columns], feature_names[2::columns], feature_names[3::columns], ): print(f"{str(c1): <20}{str(c2): <20}{str(c3): <20}{c4}")
Výsledkem bude slovník, z něhož byla skutečně odstraněna „neslova“. Slovník bude pochopitelně kratší a tím pádem bude menší i výsledná řídká matice získaná vektorizací:
Feature names: abiola able abt ac access account across actually add address admirer advance aft afternoon age ago ah aha ahead aight al alex almost alone already alright alrite also always amp angry another ans answer anymore anyone ... ... ... wrong wun www xmas xx xxx xy ya yahoo yar yeah year years yep yes yesterday yet yijue yo yr yrs yup None None
11. Vektorizace textových dat po jejich filtraci
Opět se pokusme o vektorizaci SMSsek (tj. našeho korpusu), nyní ovšem s provedením filtrace textu, který z SMSek odstraní „neslova“, přičemž samotná vektorizace bude provedena až nad takto zpracovanými SMSkami. Nejprve si uvedeme samotný skript a poté porovnáme jeho výstup s výstupem skriptu, který pracoval s nefiltrovanými SMSkami:
# Vektorizace textových dat po jejich filtraci s využitím série regulárních výrazů import pandas as pd import re from itertools import zip_longest from nltk.corpus import stopwords from sklearn.feature_extraction.text import CountVectorizer # načtení tabulky do datového rámce, specifikace kódování souboru spam = pd.read_csv("spam.csv", encoding="latin1") # hodnocení (spam/ham) labels = spam.v1.values # vlastní text SMS features = spam.v2.values # hodnoty použité později pro trénink modelu print("Labels:") print(labels) print("Number of labels:", len(labels)) print() print("Features:") print(features) print("Number of features:", len(features)) print() 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í slov s číslicemi processed_feature = re.sub("\w*\d\w*", "", processed_feature) # odstranění prefixů ^b processed_feature = re.sub(r"^b\s+", "", processed_feature) # odstranění znaků, které nejsou ASCII processed_feature = processed_feature.encode("ascii", errors="ignore").decode() # konverze výsledku na malá písmena return processed_feature.strip().lower() # preprocesing všech hodnocení processed_features = [process_feature(feature) for feature in features] # vektorizace textu vectorizer = CountVectorizer( max_features=2500, min_df=7, max_df=0.8, stop_words=stopwords.words("english") ) vectorized_features = vectorizer.fit_transform(processed_features).toarray() # slova pro dekódování vah feature_names = vectorizer.get_feature_names_out() print("Feature names count:", len(feature_names)) print() # vlastní výsledek vektorizace print("Sparse matrix of size", vectorized_features.shape, ":") print() # ukázka způsobu zakódování print("Selected tweet:") print("Original: ", features[2]) print("Processed: ", processed_features[2]) print("Vectorized: ", vectorized_features[2]) print() print("word# weight meaning") for i, f in enumerate(vectorized_features[2]): if f > 0: print(f"{i:4} {f:5} {feature_names[i]}")
Skript nyní vypíše, že ve slovníku má 1186 slov a tím pádem i řídká matice má rozměry 5572×1186 prvků:
Feature names count: 1186 Sparse matrix of size (5572, 1186) :
Připomeňme si, že bez provedení filtrace jsou výsledky odlišné:
Feature names count: 1267 Sparse matrix of size (5572, 1267) :
Zajímavější bude zjištění, jak se liší originální SMSka od SMSky přefiltrované (Original vs. Processed):
Selected tweet: Original: Free entry in 2 a wkly comp to win FA Cup final tkts 21st May 2005. Text FA to 87121 to receive entry question(std txt rate)T&C's apply 08452810075over18's Processed: free entry in wkly comp to win fa cup final tkts may text fa to to receive entry question std txt rate c apply s Vectorized: [0 0 0 ... 0 0 0]
Slova použitá ve vektorizované SMSce:
word# weight meaning 40 1 apply 171 1 comp 202 1 cup 288 2 entry 324 1 final 343 1 free 594 1 may 792 1 question 799 1 rate 813 1 receive 954 1 std 996 1 text 1056 1 txt 1132 1 win 1143 1 wkly
12. Trénink a predikce modelu nad vektorizovanými daty založený na třídě CountVectorizer
Ve chvíli, kdy již máme k dispozici jak očekávané odpovědi (první sloupec datové sady) i vektorizované SMSky, můžeme přistoupit k tréninku modelu. V prvním demonstračním příkladu pro vektorizaci použijeme třídu CountVectorizer a model bude představován třídou KNeighborsClassifier. Model natrénujeme s využitím 80% dat z datové sady, přičemž zbylých 10% bude použito pro otestování kvality předpovědi modelu. A nakonec si necháme zobrazit matici záměn, která nyní bude jednoduchá – její velikost bude 2×2 prvky, protože jsou k dispozici jen dvě možné odpovědi:
# Trénink a predikce modelu nad vektorizovanými daty, založeno na třídě CountVectorizer 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 CountVectorizer from sklearn.metrics import ConfusionMatrixDisplay from sklearn.metrics import classification_report, accuracy_score from sklearn.neighbors import KNeighborsClassifier # načtení tabulky do datového rámce, specifikace kódování souboru spam = pd.read_csv("spam.csv", encoding="latin1") # hodnocení (spam/ham) labels = spam.v1.values # vlastní text SMS features = spam.v2.values # hodnoty použité později pro trénink modelu print("Labels:") print(labels) print("Number of labels:", len(labels)) print() print("Features:") print(features) print("Number of features:", len(features)) print() 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í slov s číslicemi processed_feature = re.sub("\w*\d\w*", "", processed_feature) # odstranění prefixů ^b processed_feature = re.sub(r"^b\s+", "", processed_feature) # odstranění znaků, které nejsou ASCII processed_feature = processed_feature.encode("ascii", errors="ignore").decode() # konverze výsledku na malá písmena return processed_feature.strip().lower() # preprocesing všech hodnocení processed_features = [process_feature(feature) for feature in features] # vektorizace textu vectorizer = CountVectorizer( 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 = KNeighborsClassifier(n_neighbors=1) # 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:", accuracy_score(testY, predictions)) print() # 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) print() # uložení výsledků ve formě rastrového obrázku plt.savefig("214_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("214_2.png") # vizualizace matice plt.show()
Vyhodnocení kvality modelu:
Number of features: 5572 precision recall f1-score support ham 0.95 1.00 0.98 949 spam 0.99 0.73 0.84 166 accuracy 0.96 1115 macro avg 0.97 0.86 0.91 1115 weighted avg 0.96 0.96 0.96 1115
Zajímavější je vyjádření přesnosti odpovědí modelu, která dosahuje velmi pěkných 95–96%:
Accuracy score: 0.9587443946188341
A pochopitelně si můžeme nechat zobrazit matice záměn, a to jak v absolutní, tak i relativní podobě:
[[948 1] [ 45 121]] [[0.99894626 0.00105374] [0.27108434 0.72891566]]
Naprostá většina hodnot leží na hlavní diagonále, což odpovídá (poměrně) kvalitnímu modelu!
Obrázek 2: Matice záměn s absolutními hodnotami.
Obrázek 3: Matice záměn s relativními hodnotami.
13. Trénink a predikce modelu nad vektorizovanými daty založený na třídě TfidfVectorizer
Se třídou TfidfVectorizer, která do výsledné matice ukládá nikoli frekvence slov, ale jejich hodnoty tf-idf (tj. numericky vyjádřenou specifičnost slov vůči dokumentu i celému korpusu), jsme se již setkali. Teoreticky by měl model natrénovaný s maticí obsahující prvky tf-idf dávat lepší výsledky v porovnání s použitím matice s frekvencemi slov. Jestli tomu tak bude i při analýze spamu v SMSkách, se můžeme snadno přesvědčit:
# Trénink a predikce modelu nad vektorizovanými daty, založeno na třídě TfidfVectorizer 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, accuracy_score from sklearn.neighbors import KNeighborsClassifier # načtení tabulky do datového rámce, specifikace kódování souboru spam = pd.read_csv("spam.csv", encoding="latin1") # hodnocení (spam/ham) labels = spam.v1.values # vlastní text SMS features = spam.v2.values # hodnoty použité později pro trénink modelu print("Labels:") print(labels) print("Number of labels:", len(labels)) print() print("Features:") print(features) print("Number of features:", len(features)) print() 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í slov s číslicemi processed_feature = re.sub("\w*\d\w*", "", processed_feature) # odstranění prefixů ^b processed_feature = re.sub(r"^b\s+", "", processed_feature) # odstranění znaků, které nejsou ASCII processed_feature = processed_feature.encode("ascii", errors="ignore").decode() # konverze výsledku na malá písmena return processed_feature.strip().lower() # preprocesing všech hodnocení processed_features = [process_feature(feature) for feature in features] # 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 = KNeighborsClassifier(n_neighbors=1) # 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:", accuracy_score(testY, predictions)) print() # 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) print() # uložení výsledků ve formě rastrového obrázku plt.savefig("215_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("215_2.png") # vizualizace matice plt.show()
Ve skutečnosti bude mít tento model nepatrně horší výsledky, než model předchozí. Ovšem rozdíly se v tomto případě skutečně pohybují hluboko pod hranicí statistické odchylky (což je ovšem zajímavé – evidetně se model nenaučil mnoho slov vyloženě specifických pro spam či naopak):
precision recall f1-score support ham 0.96 1.00 0.98 949 spam 0.97 0.73 0.84 166 accuracy 0.96 1115 macro avg 0.96 0.87 0.91 1115 weighted avg 0.96 0.96 0.95 1115 Accuracy score: 0.95695067264574 [[945 4] [ 44 122]] [[0.99578504 0.00421496] [0.26506024 0.73493976]]
Z maticí záměn je patrné, že je tento model nepatrně horší, protože čísla na hlavní diagonále jsou v součtu menší, než tomu bylo u předchozího modelu:
Obrázek 4: Matice záměn s absolutními hodnotami.
Obrázek 5: Matice záměn s relativními hodnotami.
14. Jak pracovat s kontextem: řešení založené na n-gramech
Již minule jsme se zmínili o tom, že po vektorizaci vlastně ztrácíme informaci o pořadí slov v jednotlivých dokumentech. A to může znamenat, že modely nebudou natrénovány tak kvalitně, jak by to bylo možné při použití více sofistikovaných metod. Ovšem samotná vektorizace nemusí probíhat pouze nad slovníkem, který obsahuje jednotlivá slova. Namísto jednotlivých slov (či navíc k jednotlivým slovům) můžeme doplnit i dvojice, trojice atd. často používaných skupin slov. Takovým n-ticím se někdy říká n-gramy, i když je nutné poznamenat, že samotný název n-gram má více významů (může se například jednat o skupinu znaků). Ovšem my se budeme zabývat pouze n-gramy ve smyslu „n-tice slov“. Knihovna scikit-learn podporuje tvorbu slovníků, jehož prvky mohou být n-gramy, takže si můžeme vyzkoušet, zda tento alternativní způsob konstrukce slovníků povede k lepšímu nebo naopak k horšímu modelu.
15. Vektorizace textových dat s použitím n-gramů o délce 1–2 slov s výpisem výsledného slovníku
Specifikace minimálního a maximálního počtu slov v n-gramech je snadná, protože do třídy CountVectorizer nebo TfidfVectorizer je možné předat nepovinný parametr ngram_range, který musí obsahovat dvojici (minimální_délka, maximální_délka). Pokusme se tedy zkonstruovat slovník, ve kterém budou obsažena jak jednotlivá slova, tak i jejich dvojice:
# vektorizace textu vectorizer = CountVectorizer( max_features=2500, min_df=7, max_df=0.8, stop_words=stopwords.words("english"), ngram_range=(1,2) ) vectorized_features = vectorizer.fit_transform(processed_features).toarray()
Výše uvedený konstruktor třídy CountVectorizer je zařazen do dalšího skriptu, který po vektorizaci vypíše nový slovník s jednotlivými slovy i jejich dvojicemi:
# Vektorizace textových dat s použitím n-gramů o délce 1-2 slov, výpis výsledného slovníku import pandas as pd import re from itertools import zip_longest from nltk.corpus import stopwords from sklearn.feature_extraction.text import CountVectorizer # načtení tabulky do datového rámce, specifikace kódování souboru spam = pd.read_csv("spam.csv", encoding="latin1") # hodnocení (spam/ham) labels = spam.v1.values # vlastní text SMS features = spam.v2.values # hodnoty použité později pro trénink modelu print("Labels:") print(labels) print("Number of labels:", len(labels)) print() print("Features:") print(features) print("Number of features:", len(features)) print() 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í slov s číslicemi processed_feature = re.sub("\w*\d\w*", "", processed_feature) # odstranění prefixů ^b processed_feature = re.sub(r"^b\s+", "", processed_feature) # odstranění znaků, které nejsou ASCII processed_feature = processed_feature.encode("ascii", errors="ignore").decode() # konverze výsledku na malá písmena return processed_feature.strip().lower() # preprocesing všech hodnocení processed_features = [process_feature(feature) for feature in features] # vektorizace textu vectorizer = CountVectorizer( max_features=2500, min_df=7, max_df=0.8, stop_words=stopwords.words("english"), ngram_range=(1,2) ) vectorized_features = vectorizer.fit_transform(processed_features).toarray() # slova pro dekódování vah feature_names = vectorizer.get_feature_names_out() print("Feature names count:", len(feature_names)) print() print("Feature names:") for feature_name in feature_names: print(feature_name) print() # vlastní výsledek vektorizace print("Sparse matrix of size", vectorized_features.shape, ":") print() # ukázka způsobu zakódování print("Selected tweet:") print("Original: ", features[2]) print("Processed: ", processed_features[2]) print("Vectorized: ", vectorized_features[2]) print() print("word# weight meaning") for i, f in enumerate(vectorized_features[2]): if f > 0: print(f"{i:4} {f:5} {feature_names[i]}")
Nyní bude do slovníku zařazeno 1475 prvků:
Feature names count: 1475
Z výpisu obsahu slovníku (zde pochopitelně zkráceného) je patrné, že v něm skutečně nalezneme i dvojice slov. Příkladem může být častá kombinace call + další slovo:
Feature names: abiola able abt ac access account account statement across across sea actually add ... ... ... call call back call claim call customer call free call identifier call land call landline call later call min call mobile call per call reply ... ... ... worth worth discount yes yes see
Zvětší se pochopitelně i matice získaná vektorizací:
Sparse matrix of size (5572, 1475) :
Ukázka vektorizace jedné SMSky:
Selected tweet: Original: Free entry in 2 a wkly comp to win FA Cup final tkts 21st May 2005. Text FA to 87121 to receive entry question(std txt rate)T&C's apply 08452810075over18's Processed: free entry in wkly comp to win fa cup final tkts may text fa to to receive entry question std txt rate c apply s Vectorized: [0 0 0 ... 0 0 0]
Zajímavé je, že i tato SMSka obsahuje dvojici „free entry“ zařazenou do slovníku:
word# weight meaning 45 1 apply 220 1 comp 258 1 cup 360 2 entry 400 1 final 421 1 free 424 1 free entry 742 1 may 989 1 question 996 1 rate 1010 1 receive 1180 1 std 1229 1 text 1303 1 txt 1414 1 win 1427 1 wkly
16. Trénink a predikce modelu s využitím třídy CountVectorizer při vektorizaci n-gramů
Nový model nyní natrénujeme takovým způsobem, že použijeme třídu CountVectorizer, ovšem umožníme, aby se do slovníku nevkládala pouze jednotlivá slova, ale i jejich dvojice. Tím by se teoreticky měla zvýšit výsledná kvalita modelu. Praktické výsledky ovšem mohou být odlišné od předpokladů, takže si vždy musíme předpoklady ověřit měřením (natrénováním a otestováním modelu):
# Trénink a predikce modelu nad vektorizovanými daty, založeno na třídě CountVectorizer, použití ngramů 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 CountVectorizer from sklearn.metrics import ConfusionMatrixDisplay from sklearn.metrics import classification_report, accuracy_score from sklearn.neighbors import KNeighborsClassifier # načtení tabulky do datového rámce, specifikace kódování souboru spam = pd.read_csv("spam.csv", encoding="latin1") # hodnocení (spam/ham) labels = spam.v1.values # vlastní text SMS features = spam.v2.values # hodnoty použité později pro trénink modelu print("Labels:") print(labels) print("Number of labels:", len(labels)) print() print("Features:") print(features) print("Number of features:", len(features)) print() 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í slov s číslicemi processed_feature = re.sub("\w*\d\w*", "", processed_feature) # odstranění prefixů ^b processed_feature = re.sub(r"^b\s+", "", processed_feature) # odstranění znaků, které nejsou ASCII processed_feature = processed_feature.encode("ascii", errors="ignore").decode() # konverze výsledku na malá písmena return processed_feature.strip().lower() # preprocesing všech hodnocení processed_features = [process_feature(feature) for feature in features] # vektorizace textu vectorizer = CountVectorizer( max_features=2500, min_df=7, max_df=0.8, stop_words=stopwords.words("english"), ngram_range=(1, 2) ) 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 = KNeighborsClassifier(n_neighbors=1) # 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:", accuracy_score(testY, predictions)) print() # 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) print() # uložení výsledků ve formě rastrového obrázku plt.savefig("217_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("217_2.png") # vizualizace matice plt.show()
Výsledky v tomto případě kupodivu nebudou o mnoho lepší v porovnání s modelem natrénovaným pro jednotlivá slova. To je sice poněkud neintuitivní, ovšem je dobré si uvědomit, že zpracováváme jen krátké SMSky a nikoli delší dokumenty, v nichž by se daly najít různé specifičnosti (každý autor například používá podobná slovní spojení atd.):
precision recall f1-score support ham 0.95 1.00 0.98 949 spam 0.99 0.72 0.84 166 accuracy 0.96 1115 macro avg 0.97 0.86 0.91 1115 weighted avg 0.96 0.96 0.96 1115 Accuracy score: 0.957847533632287 [[948 1] [ 46 120]] [[0.99894626 0.00105374] [0.27710843 0.72289157]]
Matice záměn zobrazené formou grafu:
Obrázek 6: Matice záměn s absolutními hodnotami.
Obrázek 7: Matice záměn s relativními hodnotami.
17. Trénink a predikce modelu s využitím třídy TfidfVectorizer při vektorizaci n-gramů
Na závěr si ještě upravme demonstrační příklad z předchozí kapitoly do podoby, v níž je vektorizace provedena třídou TfidfVectorizer. Výsledkem budou prvky obsahující informaci o specifičnosti jednotlivých slov i jejich dvojic, což by teoreticky mělo znamenat, že model bude mít lepší predikce, protože dvojice slov již může poměrně přesně určovat, jestli se jedná o spam nebo ham. Pojďme si to vyzkoušet:
# Trénink a predikce modelu nad vektorizovanými daty, založeno na třídě TfidfVectorizer, použití ngramů 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, accuracy_score from sklearn.neighbors import KNeighborsClassifier # načtení tabulky do datového rámce, specifikace kódování souboru spam = pd.read_csv("spam.csv", encoding="latin1") # hodnocení (spam/ham) labels = spam.v1.values # vlastní text SMS features = spam.v2.values # hodnoty použité později pro trénink modelu print("Labels:") print(labels) print("Number of labels:", len(labels)) print() print("Features:") print(features) print("Number of features:", len(features)) print() 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í slov s číslicemi processed_feature = re.sub("\w*\d\w*", "", processed_feature) # odstranění prefixů ^b processed_feature = re.sub(r"^b\s+", "", processed_feature) # odstranění znaků, které nejsou ASCII processed_feature = processed_feature.encode("ascii", errors="ignore").decode() # konverze výsledku na malá písmena return processed_feature.strip().lower() # preprocesing všech hodnocení processed_features = [process_feature(feature) for feature in features] # vektorizace textu vectorizer = TfidfVectorizer( max_features=2500, min_df=7, max_df=0.8, stop_words=stopwords.words("english"), ngram_range=(1, 2) ) 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 = KNeighborsClassifier(n_neighbors=1) # 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:", accuracy_score(testY, predictions)) print() # 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) print() # uložení výsledků ve formě rastrového obrázku plt.savefig("218_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("218_2.png") # vizualizace matice plt.show()
Výsledky nového modelu jsou skutečně lepší, než tomu bylo v předchozím příkladu, i když se opět jedná spíše o statistickou odchylku (tedy záleží, která data jsou vybrána do trénovací a která do testovací množiny):
precision recall f1-score support ham 0.96 1.00 0.98 949 spam 0.98 0.74 0.84 166 accuracy 0.96 1115 macro avg 0.97 0.87 0.91 1115 weighted avg 0.96 0.96 0.96 1115 Accuracy score: 0.9587443946188341 [[946 3] [ 43 123]] [[0.99683878 0.00316122] [0.25903614 0.74096386]]
Matice záměn ve vizuální podobě vypadají takto:
Obrázek 8: Matice záměn s absolutními hodnotami.
Obrázek 9: Matice záměn s relativními hodnotami.
18. Zjištění vlivu minimální a maximální délky n-gramů na kvalitu modelu
V dnešním posledním demonstračním příkladu se pokusíme zjistit, jaká je (pro danou vstupní datovou sadu a pouze pro ni!) vhodná minimální a maximální délka n-gramů. Může totiž nastat situace, že bude nejvýhodnější model natrénovat pouze s využitím dvojic slov, nebo s využitím dvojic a trojic slov atd. Dopředu není možné přesně odhadnout, která kombinace (min, max) bude nejvhodnější, takže se opět uchýlíme k měření. To je realizováno v dalším skriptu, který pro každou legální kombinaci minimální a maximální délky n-gramů vypíše počet záznamů ve slovníku i celkové dosažené skóre modelu, tedy jeho předikční schopnosti:
# Trénink a predikce modelu nad vektorizovanými daty, založeno na třídě TfidfVectorizer, použití ngramů import pandas as pd import re 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, accuracy_score from sklearn.neighbors import KNeighborsClassifier # načtení tabulky do datového rámce, specifikace kódování souboru spam = pd.read_csv("spam.csv", encoding="latin1") # hodnocení (spam/ham) labels = spam.v1.values # vlastní text SMS features = spam.v2.values # hodnoty použité později pro trénink modelu print("Labels:") print(labels) print("Number of labels:", len(labels)) print() print("Features:") print(features) print("Number of features:", len(features)) print() 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í slov s číslicemi processed_feature = re.sub("\w*\d\w*", "", processed_feature) # odstranění prefixů ^b processed_feature = re.sub(r"^b\s+", "", processed_feature) # odstranění znaků, které nejsou ASCII processed_feature = processed_feature.encode("ascii", errors="ignore").decode() # konverze výsledku na malá písmena return processed_feature.strip().lower() # preprocesing všech hodnocení processed_features = [process_feature(feature) for feature in features] def model_with_ngrams(min_ngrams, max_ngrams, detailed_report): # vektorizace textu vectorizer = TfidfVectorizer( max_features=5000, min_df=7, max_df=0.8, stop_words=stopwords.words("english"), ngram_range=(min_ngrams, max_ngrams) ) vectorized_features = vectorizer.fit_transform(processed_features).toarray() columns = vectorized_features.shape[1] # 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 = KNeighborsClassifier(n_neighbors=1) # trénink modelu classifier.fit(trainX, trainY) # predikce modelu pro testovací vstupy (ne pro trénovací data) predictions = classifier.predict(testX) # vyhodnocení kvality modelu if detailed_report: print(classification_report(testY, predictions)) print() # matice záměn - absolutní hodnoty disp = ConfusionMatrixDisplay.from_estimator( classifier, testX, testY, normalize=None, ) # zobrazení matice v textové podobě print(disp.confusion_matrix) print() # matice záměn - relativní hodnoty disp = ConfusionMatrixDisplay.from_estimator( classifier, testX, testY, normalize="true", ) # zobrazení matice v textové podobě print(disp.confusion_matrix) score = accuracy_score(testY, predictions) print(f"{min_ngrams} {max_ngrams} {columns:4} {score:05.3}") for min_ngrams in range(1, 7): for max_ngrams in range(min_ngrams, 8): model_with_ngrams(min_ngrams, max_ngrams, False)
Podívejme se nejdříve na numerické výsledky, které se následně pokusíme nějakým způsobem zhodnotit:
1 1 1186 0.957 1 2 1475 0.959 1 3 1587 0.958 1 4 1652 0.957 1 5 1695 0.957 1 6 1727 0.957 1 7 1751 0.957 2 2 289 0.943 2 3 401 0.943 2 4 466 0.943 2 5 509 0.943 2 6 541 0.943 2 7 565 0.943 3 3 112 0.896 3 4 177 0.896 3 5 220 0.896 3 6 252 0.896 3 7 276 0.896 4 4 65 0.883 4 5 108 0.883 4 6 140 0.883 4 7 164 0.883 5 5 43 0.871 5 6 75 0.871 5 7 99 0.871 6 6 32 0.868 6 7 56 0.868
Nejlepších výsledků bylo dosaženo při zařazení jednotlivých slov a jejich dvojic do slovníku. Druhý nejlepší výsledek je získán při použití jednotlivých slov, dvojic a trojic. Pro další kombinace již úspěšnost modelu klesá. A důležitý (a snadno pochopitelný) je i fakt, že pokud se do slovníku nezařazují jednotlivá slova, bude slovník mnohem menší. Například čistých dvojic existuje jen 289, čistých čtveřic dokonce jen 65. I v těchto případech by bylo možné počet záznamů zvýšit, a to manipulací s hodnotami min_df a max_df, které jsme si vysvětlili minule. O další vylepšení se pokusíme v navazujícím článku, ve kterém celý popis NLP s využitím scikit-learn dokončíme.
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 - SMS Spam Collection Dataset
https://www.kaggle.com/datasets/uciml/sms-spam-collection-dataset