{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Úloha klasifikace textu\n", "\n", "Jak jsme již zmínili, zaměříme se na jednoduchou úlohu klasifikace textu založenou na datasetu **AG_NEWS**, kde je cílem zařadit nadpisy zpráv do jedné ze 4 kategorií: Svět, Sport, Byznys a Věda/Technika.\n", "\n", "## Dataset\n", "\n", "Tento dataset je součástí modulu [`torchtext`](https://github.com/pytorch/text), takže k němu máme snadný přístup.\n" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "import torch\n", "import torchtext\n", "import os\n", "import collections\n", "os.makedirs('./data',exist_ok=True)\n", "train_dataset, test_dataset = torchtext.datasets.AG_NEWS(root='./data')\n", "classes = ['World', 'Sports', 'Business', 'Sci/Tech']" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Zde `train_dataset` a `test_dataset` obsahují kolekce, které vracejí dvojice štítku (číslo třídy) a textu, například:\n" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "(3,\n", " \"Wall St. Bears Claw Back Into the Black (Reuters) Reuters - Short-sellers, Wall Street's dwindling\\\\band of ultra-cynics, are seeing green again.\")" ] }, "execution_count": 2, "metadata": {}, "output_type": "execute_result" } ], "source": [ "list(train_dataset)[0]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Takže, pojďme vytisknout prvních 10 nových titulků z našeho datasetu:\n" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "**Sci/Tech** -> Wall St. Bears Claw Back Into the Black (Reuters) Reuters - Short-sellers, Wall Street's dwindling\\band of ultra-cynics, are seeing green again.\n", "**Sci/Tech** -> Carlyle Looks Toward Commercial Aerospace (Reuters) Reuters - Private investment firm Carlyle Group,\\which has a reputation for making well-timed and occasionally\\controversial plays in the defense industry, has quietly placed\\its bets on another part of the market.\n", "**Sci/Tech** -> Oil and Economy Cloud Stocks' Outlook (Reuters) Reuters - Soaring crude prices plus worries\\about the economy and the outlook for earnings are expected to\\hang over the stock market next week during the depth of the\\summer doldrums.\n", "**Sci/Tech** -> Iraq Halts Oil Exports from Main Southern Pipeline (Reuters) Reuters - Authorities have halted oil export\\flows from the main pipeline in southern Iraq after\\intelligence showed a rebel militia could strike\\infrastructure, an oil official said on Saturday.\n", "**Sci/Tech** -> Oil prices soar to all-time record, posing new menace to US economy (AFP) AFP - Tearaway world oil prices, toppling records and straining wallets, present a new economic menace barely three months before the US presidential elections.\n" ] } ], "source": [ "for i,x in zip(range(5),train_dataset):\n", " print(f\"**{classes[x[0]]}** -> {x[1]}\")\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Protože datové sady jsou iterátory, pokud chceme data použít vícekrát, musíme je převést na seznam:\n" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [], "source": [ "train_dataset, test_dataset = torchtext.datasets.AG_NEWS(root='./data')\n", "train_dataset = list(train_dataset)\n", "test_dataset = list(test_dataset)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Tokenizace\n", "\n", "Nyní musíme převést text na **čísla**, která mohou být reprezentována jako tensory. Pokud chceme reprezentaci na úrovni slov, musíme udělat dvě věci:\n", "* použít **tokenizér** k rozdělení textu na **tokeny**\n", "* vytvořit **slovník** těchto tokenů.\n" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "['he', 'said', 'hello']" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "tokenizer = torchtext.data.utils.get_tokenizer('basic_english')\n", "tokenizer('He said: hello')" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [], "source": [ "counter = collections.Counter()\n", "for (label, line) in train_dataset:\n", " counter.update(tokenizer(line))\n", "vocab = torchtext.vocab.vocab(counter, min_freq=1)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Pomocí slovníku můžeme snadno zakódovat náš tokenizovaný řetězec do sady čísel:\n" ] }, { "cell_type": "code", "execution_count": 19, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Vocab size if 95810\n" ] }, { "data": { "text/plain": [ "[599, 3279, 97, 1220, 329, 225, 7368]" ] }, "execution_count": 19, "metadata": {}, "output_type": "execute_result" } ], "source": [ "vocab_size = len(vocab)\n", "print(f\"Vocab size if {vocab_size}\")\n", "\n", "stoi = vocab.get_stoi() # dict to convert tokens to indices\n", "\n", "def encode(x):\n", " return [stoi[s] for s in tokenizer(x)]\n", "\n", "encode('I love to play with my words')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Reprezentace textu pomocí Bag of Words\n", "\n", "Protože slova nesou význam, někdy můžeme pochopit smysl textu jen tím, že se podíváme na jednotlivá slova, bez ohledu na jejich pořadí ve větě. Například při klasifikaci zpráv slova jako *počasí*, *sníh* pravděpodobně naznačují *předpověď počasí*, zatímco slova jako *akcie*, *dolar* by se počítala jako *finanční zprávy*.\n", "\n", "**Bag of Words** (BoW) je nejčastěji používaná tradiční vektorová reprezentace. Každé slovo je spojeno s indexem vektoru, prvek vektoru obsahuje počet výskytů daného slova v konkrétním dokumentu.\n", "\n", "![Obrázek ukazující, jak je v paměti reprezentována vektorová reprezentace Bag of Words.](../../../../../translated_images/cs/bag-of-words-example.606fc1738f1d7ba9.webp) \n", "\n", "> **Note**: Na BoW můžete také nahlížet jako na součet všech vektorů zakódovaných metodou one-hot pro jednotlivá slova v textu.\n", "\n", "Níže je příklad, jak vytvořit reprezentaci Bag of Words pomocí python knihovny Scikit Learn:\n" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([[1, 1, 0, 2, 0, 0, 0, 0, 0]], dtype=int64)" ] }, "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ "from sklearn.feature_extraction.text import CountVectorizer\n", "vectorizer = CountVectorizer()\n", "corpus = [\n", " 'I like hot dogs.',\n", " 'The dog ran fast.',\n", " 'Its hot outside.',\n", " ]\n", "vectorizer.fit_transform(corpus)\n", "vectorizer.transform(['My dog likes hot dogs on a hot day.']).toarray()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "K výpočtu vektoru bag-of-words z vektorové reprezentace našeho datasetu AG_NEWS můžeme použít následující funkci:\n" ] }, { "cell_type": "code", "execution_count": 20, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "tensor([2., 1., 2., ..., 0., 0., 0.])\n" ] } ], "source": [ "vocab_size = len(vocab)\n", "\n", "def to_bow(text,bow_vocab_size=vocab_size):\n", " res = torch.zeros(bow_vocab_size,dtype=torch.float32)\n", " for i in encode(text):\n", " if i **Poznámka:** Zde používáme globální proměnnou `vocab_size` k určení výchozí velikosti slovníku. Protože velikost slovníku je často poměrně velká, můžeme omezit jeho velikost na nejčastější slova. Zkuste snížit hodnotu `vocab_size` a spustit níže uvedený kód, abyste viděli, jak to ovlivní přesnost. Měli byste očekávat určitý pokles přesnosti, ale ne dramatický, výměnou za vyšší výkon.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Trénování klasifikátoru BoW\n", "\n", "Nyní, když jsme se naučili, jak vytvořit reprezentaci Bag-of-Words pro náš text, pojďme na ní natrénovat klasifikátor. Nejprve musíme převést náš dataset pro trénování tak, aby všechny poziční vektorové reprezentace byly převedeny na reprezentaci bag-of-words. Toho lze dosáhnout předáním funkce `bowify` jako parametru `collate_fn` do standardního torch `DataLoader`:\n" ] }, { "cell_type": "code", "execution_count": 21, "metadata": {}, "outputs": [], "source": [ "from torch.utils.data import DataLoader\n", "import numpy as np \n", "\n", "# this collate function gets list of batch_size tuples, and needs to \n", "# return a pair of label-feature tensors for the whole minibatch\n", "def bowify(b):\n", " return (\n", " torch.LongTensor([t[0]-1 for t in b]),\n", " torch.stack([to_bow(t[1]) for t in b])\n", " )\n", "\n", "train_loader = DataLoader(train_dataset, batch_size=16, collate_fn=bowify, shuffle=True)\n", "test_loader = DataLoader(test_dataset, batch_size=16, collate_fn=bowify, shuffle=True)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Nyní definujme jednoduchou klasifikační neuronovou síť, která obsahuje jednu lineární vrstvu. Velikost vstupního vektoru se rovná `vocab_size` a velikost výstupu odpovídá počtu tříd (4). Protože řešíme klasifikační úlohu, konečná aktivační funkce je `LogSoftmax()`.\n" ] }, { "cell_type": "code", "execution_count": 22, "metadata": {}, "outputs": [], "source": [ "net = torch.nn.Sequential(torch.nn.Linear(vocab_size,4),torch.nn.LogSoftmax(dim=1))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Nyní definujeme standardní trénovací smyčku PyTorch. Protože náš dataset je poměrně velký, pro účely výuky budeme trénovat pouze jednu epochu, a někdy dokonce méně než jednu epochu (parametr `epoch_size` nám umožňuje omezit trénování). Během trénování také budeme hlásit kumulovanou přesnost trénování; frekvence hlášení je určena pomocí parametru `report_freq`.\n" ] }, { "cell_type": "code", "execution_count": 24, "metadata": {}, "outputs": [], "source": [ "def train_epoch(net,dataloader,lr=0.01,optimizer=None,loss_fn = torch.nn.NLLLoss(),epoch_size=None, report_freq=200):\n", " optimizer = optimizer or torch.optim.Adam(net.parameters(),lr=lr)\n", " net.train()\n", " total_loss,acc,count,i = 0,0,0,0\n", " for labels,features in dataloader:\n", " optimizer.zero_grad()\n", " out = net(features)\n", " loss = loss_fn(out,labels) #cross_entropy(out,labels)\n", " loss.backward()\n", " optimizer.step()\n", " total_loss+=loss\n", " _,predicted = torch.max(out,1)\n", " acc+=(predicted==labels).sum()\n", " count+=len(labels)\n", " i+=1\n", " if i%report_freq==0:\n", " print(f\"{count}: acc={acc.item()/count}\")\n", " if epoch_size and count>epoch_size:\n", " break\n", " return total_loss.item()/count, acc.item()/count" ] }, { "cell_type": "code", "execution_count": 25, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "3200: acc=0.8028125\n", "6400: acc=0.8371875\n", "9600: acc=0.8534375\n", "12800: acc=0.85765625\n" ] }, { "data": { "text/plain": [ "(0.026090790722161722, 0.8620069296375267)" ] }, "execution_count": 25, "metadata": {}, "output_type": "execute_result" } ], "source": [ "train_epoch(net,train_loader,epoch_size=15000)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## BiGramy, TriGramy a N-Gramy\n", "\n", "Jedním z omezení přístupu \"bag of words\" je, že některá slova jsou součástí víceslovných výrazů. Například slovo 'hot dog' má úplně jiný význam než slova 'hot' a 'dog' v jiných kontextech. Pokud bychom slova 'hot' a 'dog' vždy reprezentovali stejnými vektory, mohlo by to zmást náš model.\n", "\n", "Abychom tento problém vyřešili, často se používají **reprezentace N-gramů** v metodách klasifikace dokumentů, kde frekvence každého slova, dvojice slov nebo trojice slov představuje užitečný prvek pro trénování klasifikátorů. Například v reprezentaci bigramů přidáme do slovníku všechny dvojice slov, kromě původních slov.\n", "\n", "Níže je příklad, jak vytvořit reprezentaci bigramů pomocí Scikit Learn:\n" ] }, { "cell_type": "code", "execution_count": 26, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Vocabulary:\n", " {'i': 7, 'like': 11, 'hot': 4, 'dogs': 2, 'i like': 8, 'like hot': 12, 'hot dogs': 5, 'the': 16, 'dog': 0, 'ran': 14, 'fast': 3, 'the dog': 17, 'dog ran': 1, 'ran fast': 15, 'its': 9, 'outside': 13, 'its hot': 10, 'hot outside': 6}\n" ] }, { "data": { "text/plain": [ "array([[1, 0, 1, 0, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]],\n", " dtype=int64)" ] }, "execution_count": 26, "metadata": {}, "output_type": "execute_result" } ], "source": [ "bigram_vectorizer = CountVectorizer(ngram_range=(1, 2), token_pattern=r'\\b\\w+\\b', min_df=1)\n", "corpus = [\n", " 'I like hot dogs.',\n", " 'The dog ran fast.',\n", " 'Its hot outside.',\n", " ]\n", "bigram_vectorizer.fit_transform(corpus)\n", "print(\"Vocabulary:\\n\",bigram_vectorizer.vocabulary_)\n", "bigram_vectorizer.transform(['My dog likes hot dogs on a hot day.']).toarray()\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Hlavní nevýhodou přístupu N-gram je, že velikost slovníku začíná růst extrémně rychle. V praxi je potřeba kombinovat reprezentaci N-gram s některými technikami redukce dimenzionality, jako jsou *embeddingy*, o kterých budeme diskutovat v další jednotce.\n", "\n", "Abychom mohli použít reprezentaci N-gram v našem datasetu **AG News**, musíme vytvořit speciální slovník ngramů:\n" ] }, { "cell_type": "code", "execution_count": 27, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Bigram vocabulary length = 1308842\n" ] } ], "source": [ "counter = collections.Counter()\n", "for (label, line) in train_dataset:\n", " l = tokenizer(line)\n", " counter.update(torchtext.data.utils.ngrams_iterator(l,ngrams=2))\n", " \n", "bi_vocab = torchtext.vocab.vocab(counter, min_freq=1)\n", "\n", "print(\"Bigram vocabulary length = \",len(bi_vocab))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Mohli bychom použít stejný kód jako výše k natrénování klasifikátoru, avšak to by bylo velmi neefektivní z hlediska paměti. V další části natrénujeme bigramový klasifikátor pomocí embeddingů.\n", "\n", "> **Poznámka:** Můžete ponechat pouze ty ngramy, které se v textu vyskytují vícekrát, než je stanovený počet. Tím zajistíte, že méně časté bigramy budou vynechány, a výrazně se sníží dimenzionalita. K tomu nastavte parametr `min_freq` na vyšší hodnotu a sledujte, jak se mění délka slovníku.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Term Frequency Inverse Document Frequency TF-IDF\n", "\n", "V reprezentaci BoW jsou výskyty slov rovnoměrně váženy, bez ohledu na samotné slovo. Je však zřejmé, že častá slova, jako například *a*, *v* atd., jsou pro klasifikaci mnohem méně důležitá než specializované termíny. Ve skutečnosti jsou v většině úloh NLP některá slova relevantnější než jiná.\n", "\n", "**TF-IDF** znamená **termínová frekvence–inverzní dokumentová frekvence**. Jedná se o variaci bag of words, kde místo binární hodnoty 0/1, která označuje výskyt slova v dokumentu, se používá hodnota s plovoucí desetinnou čárkou, která souvisí s frekvencí výskytu slova v korpusu.\n", "\n", "Formálně je váha $w_{ij}$ slova $i$ v dokumentu $j$ definována jako:\n", "$$\n", "w_{ij} = tf_{ij}\\times\\log({N\\over df_i})\n", "$$\n", "kde\n", "* $tf_{ij}$ je počet výskytů $i$ v $j$, tj. hodnota BoW, kterou jsme viděli dříve\n", "* $N$ je počet dokumentů v kolekci\n", "* $df_i$ je počet dokumentů obsahujících slovo $i$ v celé kolekci\n", "\n", "Hodnota TF-IDF $w_{ij}$ roste úměrně počtu výskytů slova v dokumentu a je upravena podle počtu dokumentů v korpusu, které obsahují dané slovo, což pomáhá zohlednit skutečnost, že některá slova se objevují častěji než jiná. Například pokud se slovo objeví *v každém* dokumentu v kolekci, $df_i=N$, a $w_{ij}=0$, a tyto termíny by byly zcela ignorovány.\n", "\n", "TF-IDF vektorizaci textu můžete snadno vytvořit pomocí Scikit Learn:\n" ] }, { "cell_type": "code", "execution_count": 28, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([[0.43381609, 0. , 0.43381609, 0. , 0.65985664,\n", " 0.43381609, 0. , 0. , 0. , 0. ,\n", " 0. , 0. , 0. , 0. , 0. ,\n", " 0. ]])" ] }, "execution_count": 28, "metadata": {}, "output_type": "execute_result" } ], "source": [ "from sklearn.feature_extraction.text import TfidfVectorizer\n", "vectorizer = TfidfVectorizer(ngram_range=(1,2))\n", "vectorizer.fit_transform(corpus)\n", "vectorizer.transform(['My dog likes hot dogs on a hot day.']).toarray()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Závěr\n", "\n", "I když TF-IDF reprezentace poskytují váhování frekvence různým slovům, nejsou schopny reprezentovat význam nebo pořadí. Jak řekl slavný lingvista J. R. Firth v roce 1935: „Úplný význam slova je vždy kontextový a žádné studium významu mimo kontext nelze brát vážně.“ Později v tomto kurzu se naučíme, jak zachytit kontextové informace z textu pomocí jazykového modelování.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n---\n\n**Prohlášení**: \nTento dokument byl přeložen pomocí služby pro automatický překlad [Co-op Translator](https://github.com/Azure/co-op-translator). Ačkoli se snažíme o přesnost, mějte na paměti, že automatické překlady mohou obsahovat chyby nebo nepřesnosti. Původní dokument v jeho původním jazyce by měl být považován za autoritativní zdroj. Pro důležité informace doporučujeme profesionální lidský překlad. Neodpovídáme za žádná nedorozumění nebo nesprávné interpretace vyplývající z použití tohoto překladu.\n" ] } ], "metadata": { "interpreter": { "hash": "16af2a8bbb083ea23e5e41c7f5787656b2ce26968575d8763f2c4b17f9cd711f" }, "kernelspec": { "display_name": "Python 3.8.12 ('py38')", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.8.12" }, "coopTranslator": { "original_hash": "7b9040985e748e4e2d4c689892456ad7", "translation_date": "2025-08-29T16:41:00+00:00", "source_file": "lessons/5-NLP/13-TextRep/TextRepresentationPyTorch.ipynb", "language_code": "cs" } }, "nbformat": 4, "nbformat_minor": 2 }