AI-For-Beginners/translations/pl/lessons/5-NLP/13-TextRep/TextRepresentationPyTorch.i...

577 lines
22 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{
"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<bow_vocab_size:\n",
" res[i] += 1\n",
" return res\n",
"\n",
"print(to_bow(train_dataset[0][1]))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"> **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ść terminuodwrotną 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
}