{ "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", "**Мешок слов** (Bag of Words, BoW) — это наиболее часто используемое традиционное векторное представление. Каждое слово связано с индексом вектора, а элемент вектора содержит количество вхождений слова в данном документе.\n", "\n", "![Изображение, показывающее, как представление \"мешка слов\" хранится в памяти.](../../../../../translated_images/ru/bag-of-words-example.606fc1738f1d7ba9.webp)\n", "\n", "> **Note**: Вы также можете представить BoW как сумму всех векторов с единичным кодированием (one-hot encoding) для отдельных слов в тексте.\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), давайте обучим на его основе классификатор. Сначала нам нужно преобразовать наш набор данных для обучения таким образом, чтобы все позиционные векторные представления были преобразованы в представление мешка слов. Это можно сделать, передав функцию `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-грамм является то, что размер словаря начинает расти чрезвычайно быстро. На практике необходимо сочетать представление Н-грамм с некоторыми методами уменьшения размерности, такими как *встраивания*, о которых мы поговорим в следующем разделе.\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 (мешок слов) все слова имеют одинаковый вес, независимо от их значимости. Однако очевидно, что часто встречающиеся слова, такие как *a*, *in* и т.д., гораздо менее важны для классификации, чем специализированные термины. На самом деле, в большинстве задач обработки естественного языка некоторые слова имеют большее значение, чем другие.\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-28T12:31:49+00:00", "source_file": "lessons/5-NLP/13-TextRep/TextRepresentationPyTorch.ipynb", "language_code": "ru" } }, "nbformat": 4, "nbformat_minor": 2 }