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

577 lines
27 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": [
"# Задача за класификация на текст\n",
"\n",
"Както споменахме, ще се фокусираме върху проста задача за класификация на текст, базирана на набора от данни **AG_NEWS**, която включва класифициране на заглавия на новини в една от 4 категории: Свят, Спорт, Бизнес и Наука/Технологии.\n",
"\n",
"## Наборът от данни\n",
"\n",
"Този набор от данни е вграден в модула [`torchtext`](https://github.com/pytorch/text), така че можем лесно да го достъпим.\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": [
"Тук `train_dataset` и `test_dataset` съдържат колекции, които връщат двойки от етикет (номер на класа) и текст съответно, например:\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": [
"И така, нека отпечатаме първите 10 нови заглавия от нашия набор от данни:\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": [
"Тъй като наборите от данни са итератори, ако искаме да използваме данните многократно, трябва да ги преобразуваме в списък:\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": [
"## Токенизация\n",
"\n",
"Сега трябва да преобразуваме текста в **числа**, които могат да бъдат представени като тензори. Ако искаме представяне на ниво думи, трябва да направим две неща:\n",
"* използваме **токенизатор**, за да разделим текста на **токени**\n",
"* изградим **речник** от тези токени.\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": [
"Използвайки речник, можем лесно да кодираме нашия токенизиран низ в набор от числа:\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": [
"## Представяне на текст чрез \"Чанта с думи\"\n",
"\n",
"Тъй като думите носят значение, понякога можем да разберем смисъла на текста, като просто разгледаме отделните думи, без да обръщаме внимание на техния ред в изречението. Например, при класифициране на новини, думи като *време*, *сняг* вероятно ще насочат към *прогноза за времето*, докато думи като *акции*, *долар* биха се отнесли към *финансови новини*.\n",
"\n",
"**Чанта с думи** (BoW) е най-често използваното традиционно векторно представяне. Всяка дума е свързана с индекс във вектора, а елементът на вектора съдържа броя на срещанията на дадена дума в конкретен документ.\n",
"\n",
"![Изображение, показващо как представянето чрез \"чанта с думи\" се съхранява в паметта.](../../../../../translated_images/bag-of-words-example.606fc1738f1d7ba98a9d693e3bcd706c6e83fa7bf8221e6e90d1a206d82f2ea4.bg.png) \n",
"\n",
"> **Note**: Можете също да мислите за BoW като за сума от всички едноразрядно кодирани вектори за отделните думи в текста.\n",
"\n",
"По-долу е даден пример как да генерирате представяне чрез \"чанта с думи\", използвайки библиотеката Scikit Learn на Python:\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": [
"За да изчислим bag-of-words вектор от векторното представяне на нашия AG_NEWS набор от данни, можем да използваме следната функция:\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": [
"> **Забележка:** Тук използваме глобалната променлива `vocab_size`, за да зададем стандартния размер на речника. Тъй като често размерът на речника е доста голям, можем да го ограничим до най-често срещаните думи. Опитайте да намалите стойността на `vocab_size` и да изпълните кода по-долу, за да видите как това влияе на точността. Трябва да очаквате известно намаление на точността, но не драстично, за сметка на по-висока производителност.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Обучение на класификатор с метод Bag-of-Words\n",
"\n",
"Сега, след като научихме как да изградим представяне на текста чрез Bag-of-Words, нека обучим класификатор върху него. Първо, трябва да преобразуваме нашия набор от данни за обучение така, че всички позиционни векторни представяния да бъдат преобразувани в представяне чрез Bag-of-Words. Това може да се постигне, като предадем функцията `bowify` като параметър `collate_fn` на стандартния 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": [
"Сега нека дефинираме прост класификаторен невронен модел, който съдържа един линеен слой. Размерът на входния вектор е равен на `vocab_size`, а размерът на изхода съответства на броя на класовете (4). Тъй като решаваме задача за класификация, крайната активационна функция е `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": [
"Сега ще дефинираме стандартен цикъл за обучение в PyTorch. Тъй като нашият набор от данни е доста голям, за целите на обучението ще тренираме само за една епоха, а понякога дори за по-малко от една епоха (параметърът `epoch_size` ни позволява да ограничим обучението). Също така ще докладваме натрупаната точност на обучението по време на тренировката; честотата на докладване се определя чрез параметъра `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": [
"## Биграми, триграми и N-грами\n",
"\n",
"Едно от ограниченията на подхода с чанта от думи е, че някои думи са част от изрази, състоящи се от няколко думи. Например, думата „hot dog“ има напълно различно значение от думите „hot“ и „dog“ в други контексти. Ако винаги представяме думите „hot“ и „dog“ със същите вектори, това може да обърка модела ни.\n",
"\n",
"За да се справим с това, често се използват **представяния с N-грам** в методите за класификация на документи, където честотата на всяка дума, двойка думи или тройка думи е полезна характеристика за обучение на класификатори. При представяне с биграми, например, ще добавим всички двойки думи към речника, в допълнение към оригиналните думи.\n",
"\n",
"По-долу е даден пример за това как да генерираме представяне с чанта от думи с биграми, използвайки 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": [
"Основният недостатък на подхода с N-грам е, че размерът на речника започва да нараства изключително бързо. На практика е необходимо да комбинираме представянето с N-грам с някои техники за намаляване на размерността, като например *вграждания*, които ще обсъдим в следващия модул.\n",
"\n",
"За да използваме представянето с N-грам в нашия **AG News** набор от данни, трябва да изградим специален речник за n-грам:\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": [
"Можем да използваме същия код като горния, за да обучим класификатора, но това би било много неефективно по отношение на паметта. В следващия модул ще обучим биграм класификатор, използвайки вграждания.\n",
"\n",
"> **Note:** Можете да оставите само онези n-грамове, които се срещат в текста повече от определен брой пъти. Това ще гарантира, че рядко срещаните биграми ще бъдат пропуснати и значително ще намали размерността. За да направите това, задайте параметъра `min_freq` на по-висока стойност и наблюдавайте как се променя дължината на речника.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Честота на термина и обратна честота на документа (TF-IDF)\n",
"\n",
"В представянето на BoW (Bag of Words), срещанията на думите се претеглят равномерно, независимо от самата дума. Въпреки това е очевидно, че често срещани думи като *а*, *в* и т.н. са много по-малко важни за класификацията в сравнение със специализираните термини. Всъщност, в повечето задачи по обработка на естествен език (NLP) някои думи са по-релевантни от други.\n",
"\n",
"**TF-IDF** означава **честота на термина обратна честота на документа**. Това е вариация на модела \"чанта с думи\" (BoW), при която вместо бинарна стойност 0/1, указваща наличието на дума в даден документ, се използва стойност с плаваща запетая, която е свързана с честотата на срещане на думата в корпуса.\n",
"\n",
"По-формално, теглото $w_{ij}$ на дума $i$ в документ $j$ се дефинира като:\n",
"$$\n",
"w_{ij} = tf_{ij}\\times\\log({N\\over df_i})\n",
"$$\n",
"където:\n",
"* $tf_{ij}$ е броят на срещанията на $i$ в $j$, т.е. стойността на BoW, която видяхме по-рано\n",
"* $N$ е броят на документите в колекцията\n",
"* $df_i$ е броят на документите, съдържащи думата $i$ в цялата колекция\n",
"\n",
"Стойността на TF-IDF $w_{ij}$ нараства пропорционално на броя пъти, в които една дума се появява в даден документ, и се коригира спрямо броя на документите в корпуса, които съдържат тази дума. Това помага да се компенсира фактът, че някои думи се срещат по-често от други. Например, ако дадена дума се появява *във всеки* документ от колекцията, тогава $df_i=N$, и $w_{ij}=0$, като тези термини ще бъдат напълно игнорирани.\n",
"\n",
"Можете лесно да създадете векторизация на текст с TF-IDF, използвайки 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": [
"## Заключение\n",
"\n",
"Въпреки че представянията чрез TF-IDF предоставят тегло на честотата за различни думи, те не могат да представят значението или реда. Както известният лингвист Дж. Р. Фърт каза през 1935 г., „Пълното значение на една дума винаги е контекстуално, и никакво изследване на значението извън контекста не може да бъде взето на сериозно.“. По-късно в курса ще научим как да улавяме контекстуална информация от текст чрез моделиране на езика.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n---\n\n**Отказ от отговорност**: \nТози документ е преведен с помощта на AI услуга за превод [Co-op Translator](https://github.com/Azure/co-op-translator). Въпреки че се стремим към точност, моля, имайте предвид, че автоматизираните преводи може да съдържат грешки или неточности. Оригиналният документ на неговия роден език трябва да се счита за авторитетен източник. За критична информация се препоръчва професионален човешки превод. Ние не носим отговорност за недоразумения или погрешни интерпретации, произтичащи от използването на този превод.\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-30T01:13:03+00:00",
"source_file": "lessons/5-NLP/13-TextRep/TextRepresentationPyTorch.ipynb",
"language_code": "bg"
}
},
"nbformat": 4,
"nbformat_minor": 2
}