{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Zadanie klasyfikacji tekstu\n", "\n", "Jak już wspomnieliśmy, skupimy się na prostym zadaniu klasyfikacji tekstu opartym na zbiorze danych **AG_NEWS**, które polega na klasyfikacji nagłówków wiadomości do jednej z 4 kategorii: Świat, Sport, Biznes i Nauka/Technologia.\n", "\n", "## Zbiór danych\n", "\n", "Ten zbiór danych jest wbudowany w moduł [`torchtext`](https://github.com/pytorch/text), dzięki czemu mamy do niego łatwy dostęp.\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": [ "Tutaj `train_dataset` i `test_dataset` zawierają kolekcje, które zwracają pary etykiety (numer klasy) i tekstu odpowiednio, na przykład:\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": [ "Więc wydrukujmy pierwsze 10 nowych nagłówków z naszego zestawu danych:\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": [ "Ponieważ zestawy danych są iteratorami, jeśli chcemy używać danych wielokrotnie, musimy je przekonwertować na listę:\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": [ "## Tokenizacja\n", "\n", "Teraz musimy przekształcić tekst w **liczby**, które mogą być reprezentowane jako tensory. Jeśli chcemy reprezentację na poziomie słów, musimy zrobić dwie rzeczy:\n", "* użyć **tokenizatora**, aby podzielić tekst na **tokeny**\n", "* zbudować **słownik** tych tokenów.\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": [ "Używając słownictwa, możemy łatwo zakodować ztokenizowany ciąg w zestaw liczb:\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": [ "## Reprezentacja tekstu za pomocą Bag of Words\n", "\n", "Ponieważ słowa niosą znaczenie, czasami możemy zrozumieć sens tekstu, patrząc jedynie na pojedyncze słowa, niezależnie od ich kolejności w zdaniu. Na przykład, przy klasyfikacji wiadomości, słowa takie jak *pogoda*, *śnieg* prawdopodobnie wskazują na *prognozę pogody*, podczas gdy słowa takie jak *akcje*, *dolar* będą sugerować *wiadomości finansowe*.\n", "\n", "Reprezentacja wektorowa **Bag of Words** (BoW) jest najczęściej używaną tradycyjną metodą reprezentacji wektorowej. Każde słowo jest powiązane z indeksem wektora, a element wektora zawiera liczbę wystąpień danego słowa w określonym dokumencie.\n", "\n", "![Obraz pokazujący, jak reprezentacja wektorowa Bag of Words jest przechowywana w pamięci.](../../../../../lessons/5-NLP/13-TextRep/images/bag-of-words-example.png) \n", "\n", "> **Note**: Możesz również myśleć o BoW jako o sumie wszystkich wektorów zakodowanych w formie one-hot dla poszczególnych słów w tekście.\n", "\n", "Poniżej znajduje się przykład generowania reprezentacji Bag of Words przy użyciu biblioteki Scikit Learn w Pythonie:\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": [ "Aby obliczyć wektor bag-of-words z reprezentacji wektorowej naszego zbioru danych AG_NEWS, możemy użyć następującej funkcji:\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 **Uwaga:** Tutaj używamy globalnej zmiennej `vocab_size`, aby określić domyślny rozmiar słownika. Ponieważ często rozmiar słownika jest dość duży, możemy ograniczyć jego rozmiar do najczęściej występujących słów. Spróbuj zmniejszyć wartość `vocab_size` i uruchomić poniższy kod, aby zobaczyć, jak wpływa to na dokładność. Powinieneś spodziewać się pewnego spadku dokładności, ale nie dramatycznego, w zamian za wyższą wydajność.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Trenowanie klasyfikatora BoW\n", "\n", "Teraz, gdy nauczyliśmy się tworzyć reprezentację Bag-of-Words dla naszego tekstu, przejdźmy do trenowania klasyfikatora na jej podstawie. Najpierw musimy przekształcić nasz zbiór danych do trenowania w taki sposób, aby wszystkie wektorowe reprezentacje pozycji zostały zamienione na reprezentację Bag-of-Words. Można to osiągnąć, przekazując funkcję `bowify` jako parametr `collate_fn` do standardowego 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": [ "Teraz zdefiniujmy prostą sieć neuronową klasyfikatora, która zawiera jedną warstwę liniową. Rozmiar wektora wejściowego jest równy `vocab_size`, a rozmiar wyjściowy odpowiada liczbie klas (4). Ponieważ rozwiązujemy zadanie klasyfikacji, końcową funkcją aktywacji jest `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": [ "Teraz zdefiniujemy standardową pętlę treningową PyTorch. Ponieważ nasz zbiór danych jest dość duży, na potrzeby nauczania będziemy trenować tylko przez jedną epokę, a czasami nawet krócej niż jedną epokę (określenie parametru `epoch_size` pozwala nam ograniczyć trening). Będziemy również raportować skumulowaną dokładność treningu podczas treningu; częstotliwość raportowania jest określana za 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 i N-Gramy\n", "\n", "Jednym z ograniczeń podejścia typu worek słów jest to, że niektóre słowa są częścią wyrażeń wielowyrazowych. Na przykład słowo „hot dog” ma zupełnie inne znaczenie niż słowa „hot” i „dog” w innych kontekstach. Jeśli zawsze reprezentujemy słowa „hot” i „dog” za pomocą tych samych wektorów, może to wprowadzać zamieszanie w naszym modelu.\n", "\n", "Aby rozwiązać ten problem, w metodach klasyfikacji dokumentów często stosuje się **reprezentacje N-gramów**, gdzie częstotliwość każdego słowa, pary słów (bigramu) lub trójki słów (trigramu) jest przydatną cechą do trenowania klasyfikatorów. W reprezentacji bigramowej, na przykład, dodajemy wszystkie pary słów do słownika, oprócz oryginalnych słów.\n", "\n", "Poniżej znajduje się przykład, jak wygenerować reprezentację bigramową w podejściu worek słów za 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": [ "Główną wadą podejścia N-gram jest to, że rozmiar słownika zaczyna rosnąć niezwykle szybko. W praktyce musimy połączyć reprezentację N-gram z technikami redukcji wymiarów, takimi jak *osadzenia* (*embeddings*), które omówimy w następnej jednostce.\n", "\n", "Aby użyć reprezentacji N-gram w naszym zbiorze danych **AG News**, musimy zbudować specjalny słownik ngramów:\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": [ "Możemy użyć tego samego kodu, co powyżej, aby wytrenować klasyfikator, jednak byłoby to bardzo nieefektywne pod względem pamięci. W następnej jednostce wytrenujemy klasyfikator bigramowy, korzystając z osadzeń.\n", "\n", "> **Uwaga:** Możesz pozostawić tylko te ngramy, które występują w tekście więcej niż określoną liczbę razy. Dzięki temu rzadkie bigramy zostaną pominięte, co znacząco zmniejszy wymiarowość. Aby to zrobić, ustaw parametr `min_freq` na wyższą wartość i obserwuj zmianę długości słownika.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Term Frequency Inverse Document Frequency TF-IDF\n", "\n", "W reprezentacji BoW wystąpienia słów są równomiernie ważone, niezależnie od samego słowa. Jednak oczywiste jest, że częste słowa, takie jak *a*, *w* itd., są znacznie mniej istotne dla klasyfikacji niż terminy specjalistyczne. W rzeczywistości, w większości zadań NLP, niektóre słowa są bardziej istotne niż inne.\n", "\n", "**TF-IDF** oznacza **częstość terminu–odwrotną częstość dokumentu**. Jest to wariacja torby słów, gdzie zamiast binarnej wartości 0/1 wskazującej na obecność słowa w dokumencie, używana jest wartość zmiennoprzecinkowa, która jest związana z częstością występowania słowa w korpusie.\n", "\n", "Bardziej formalnie, waga $w_{ij}$ słowa $i$ w dokumencie $j$ jest definiowana jako:\n", "$$\n", "w_{ij} = tf_{ij}\\times\\log({N\\over df_i})\n", "$$\n", "gdzie:\n", "* $tf_{ij}$ to liczba wystąpień $i$ w $j$, czyli wartość BoW, którą widzieliśmy wcześniej\n", "* $N$ to liczba dokumentów w kolekcji\n", "* $df_i$ to liczba dokumentów zawierających słowo $i$ w całej kolekcji\n", "\n", "Wartość TF-IDF $w_{ij}$ rośnie proporcjonalnie do liczby wystąpień słowa w dokumencie i jest korygowana o liczbę dokumentów w korpusie, które zawierają dane słowo, co pomaga uwzględnić fakt, że niektóre słowa pojawiają się częściej niż inne. Na przykład, jeśli słowo pojawia się *w każdym* dokumencie w kolekcji, $df_i=N$, a $w_{ij}=0$, i takie terminy byłyby całkowicie pomijane.\n", "\n", "Możesz łatwo stworzyć wektoryzację TF-IDF tekstu za 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": [ "## Podsumowanie\n", "\n", "Mimo że reprezentacje TF-IDF nadają wagę częstotliwości różnym słowom, nie są w stanie oddać znaczenia ani kolejności. Jak powiedział słynny językoznawca J. R. Firth w 1935 roku: „Pełne znaczenie słowa zawsze zależy od kontekstu, a żadnego badania znaczenia poza kontekstem nie można traktować poważnie.”. W dalszej części kursu dowiemy się, jak uchwycić informacje kontekstowe z tekstu za pomocą modelowania języka.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n---\n\n**Zastrzeżenie**: \nTen dokument został przetłumaczony za pomocą usługi tłumaczenia AI [Co-op Translator](https://github.com/Azure/co-op-translator). Chociaż staramy się zapewnić dokładność, prosimy mieć na uwadze, że automatyczne tłumaczenia mogą zawierać błędy lub nieścisłości. Oryginalny dokument w jego rodzimym języku powinien być uznawany za wiarygodne źródło. W przypadku informacji krytycznych zaleca się skorzystanie z profesjonalnego tłumaczenia wykonanego przez człowieka. Nie ponosimy odpowiedzialności za jakiekolwiek nieporozumienia lub błędne interpretacje wynikające z użycia tego tłumaczenia.\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-31T14:10:44+00:00", "source_file": "lessons/5-NLP/13-TextRep/TextRepresentationPyTorch.ipynb", "language_code": "pl" } }, "nbformat": 4, "nbformat_minor": 2 }