{ "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/sr/bag-of-words-example.606fc1738f1d7ba9.webp) \n", "\n", "> **Напомена**: Можете такође размишљати о 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": [ "Да бисмо израчунали вектор торбе речи из векторске репрезентације нашег 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 **Напомена:** Овде користимо глобалну променљиву `vocab_size` да бисмо одредили подразумевану величину речника. Пошто је величина речника често прилично велика, можемо ограничити величину речника на најчешће речи. Покушајте да смањите вредност `vocab_size` и покренете код испод, и видите како то утиче на тачност. Требало би да очекујете одређени пад тачности, али не драматичан, у замену за боље перформансе.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Тренирање BoW класификатора\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", "\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-gram приступа је што величина речника почиње да расте изузетно брзо. У пракси, потребно је комбиновати N-gram репрезентацију са неким техникама за смањење димензионалности, као што су *уграђивања* (*embeddings*), о којима ћемо говорити у наредној јединици.\n", "\n", "Да бисмо користили N-gram репрезентацију у нашем **AG News** скупу података, потребно је да изградимо посебан 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": [ "Могли бисмо користити исти код као горе за тренирање класификатора, међутим, то би било веома неефикасно у погледу меморије. У наредној јединици, тренираћемо класификатор са биграмима користећи ембедингсе.\n", "\n", "> **Напомена:** Можете оставити само оне нграме који се у тексту појављују више од одређеног броја пута. Ово ће осигурати да се ретки биграми изоставе и значајно смањити димензионалност. Да бисте то урадили, подесите параметар `min_freq` на већу вредност и посматрајте промену дужине речника.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Учестаност термина и обрнута учестаност докумената TF-IDF\n", "\n", "У представљању BoW, појаве речи се равномерно вреднују, без обзира на саму реч. Међутим, јасно је да су учестале речи, као што су *a*, *in*, итд., много мање важне за класификацију у поређењу са специјализованим терминима. У ствари, у већини NLP задатака неке речи су значајније од других.\n", "\n", "**TF-IDF** означава **учестаност термина–обрнута учестаност докумената**. То је варијација модела торбе речи, где се уместо бинарне вредности 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Овај документ је преведен коришћењем услуге за превођење помоћу вештачке интелигенције [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:53+00:00", "source_file": "lessons/5-NLP/13-TextRep/TextRepresentationPyTorch.ipynb", "language_code": "sr" } }, "nbformat": 4, "nbformat_minor": 2 }