AI-For-Beginners/translations/uk/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.uk.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": [
"Щоб обчислити вектор \"мішок слів\" з векторного представлення нашого набору даних 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": [
"## Навчання класифікатора BoW\n",
"\n",
"Тепер, коли ми навчилися створювати представлення тексту у вигляді \"Мішка слів\" (Bag-of-Words), давайте навчимо класифікатор на його основі. Спершу нам потрібно перетворити наш набір даних для навчання таким чином, щоб усі позиційні векторні представлення були перетворені на представлення \"Мішка слів\". Це можна зробити, передавши функцію `bowify` як параметр `collate_fn` до стандартного `DataLoader` з бібліотеки torch:\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-грам є те, що розмір словника починає зростати надзвичайно швидко. На практиці нам потрібно поєднувати представлення Н-грам із деякими методами зменшення розмірності, такими як *вбудовування* (embeddings), про які ми поговоримо в наступному розділі.\n",
"\n",
"Щоб використовувати представлення Н-грам у нашому наборі даних **AG News**, нам потрібно створити спеціальний словник Н-грам:\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",
"> **Примітка:** Ви можете залишити лише ті n-грами, які зустрічаються в тексті більше заданої кількості разів. Це забезпечить виключення рідкісних біграм і значно зменшить розмірність. Для цього встановіть параметр `min_freq` на вищу величину і спостерігайте, як змінюється довжина словника.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Частота терміну та обернена частота документа (TF-IDF)\n",
"\n",
"У представленні BoW (Bag of Words) всі слова мають однакову вагу, незалежно від їх значення. Однак очевидно, що частовживані слова, такі як *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-30T10:41:22+00:00",
"source_file": "lessons/5-NLP/13-TextRep/TextRepresentationPyTorch.ipynb",
"language_code": "uk"
}
},
"nbformat": 4,
"nbformat_minor": 2
}