647 lines
33 KiB
Plaintext
647 lines
33 KiB
Plaintext
{
|
||
"cells": [
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"# Задача за класификация на текст\n",
|
||
"\n",
|
||
"В този модул ще започнем с проста задача за класификация на текст, базирана на **[AG_NEWS](http://www.di.unipi.it/~gulli/AG_corpus_of_news_articles.html)** набора от данни: ще класифицираме заглавия на новини в една от 4 категории: Свят, Спорт, Бизнес и Наука/Технологии.\n",
|
||
"\n",
|
||
"## Наборът от данни\n",
|
||
"\n",
|
||
"За да заредим набора от данни, ще използваме **[TensorFlow Datasets](https://www.tensorflow.org/datasets)** API.\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 1,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"import tensorflow as tf\n",
|
||
"from tensorflow import keras\n",
|
||
"import tensorflow_datasets as tfds\n",
|
||
"\n",
|
||
"# In this tutorial, we will be training a lot of models. In order to use GPU memory cautiously,\n",
|
||
"# we will set tensorflow option to grow GPU memory allocation when required.\n",
|
||
"physical_devices = tf.config.list_physical_devices('GPU') \n",
|
||
"if len(physical_devices)>0:\n",
|
||
" tf.config.experimental.set_memory_growth(physical_devices[0], True)\n",
|
||
"\n",
|
||
"dataset = tfds.load('ag_news_subset')"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"Сега можем да достъпим тренировъчната и тестовата част на набора от данни, като използваме `dataset['train']` и `dataset['test']` съответно:\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 3,
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"Length of train dataset = 120000\n",
|
||
"Length of test dataset = 7600\n"
|
||
]
|
||
}
|
||
],
|
||
"source": [
|
||
"ds_train = dataset['train']\n",
|
||
"ds_test = dataset['test']\n",
|
||
"\n",
|
||
"print(f\"Length of train dataset = {len(ds_train)}\")\n",
|
||
"print(f\"Length of test dataset = {len(ds_test)}\")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"Нека отпечатаме първите 10 нови заглавия от нашия набор от данни:\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 4,
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"3 (Sci/Tech) -> b'AMD Debuts Dual-Core Opteron Processor' b'AMD #39;s new dual-core Opteron chip is designed mainly for corporate computing applications, including databases, Web services, and financial transactions.'\n",
|
||
"1 (Sports) -> b\"Wood's Suspension Upheld (Reuters)\" b'Reuters - Major League Baseball\\\\Monday announced a decision on the appeal filed by Chicago Cubs\\\\pitcher Kerry Wood regarding a suspension stemming from an\\\\incident earlier this season.'\n",
|
||
"2 (Business) -> b'Bush reform may have blue states seeing red' b'President Bush #39;s quot;revenue-neutral quot; tax reform needs losers to balance its winners, and people claiming the federal deduction for state and local taxes may be in administration planners #39; sights, news reports say.'\n",
|
||
"3 (Sci/Tech) -> b\"'Halt science decline in schools'\" b'Britain will run out of leading scientists unless science education is improved, says Professor Colin Pillinger.'\n",
|
||
"1 (Sports) -> b'Gerrard leaves practice' b'London, England (Sports Network) - England midfielder Steven Gerrard injured his groin late in Thursday #39;s training session, but is hopeful he will be ready for Saturday #39;s World Cup qualifier against Austria.'\n"
|
||
]
|
||
}
|
||
],
|
||
"source": [
|
||
"classes = ['World', 'Sports', 'Business', 'Sci/Tech']\n",
|
||
"\n",
|
||
"for i,x in zip(range(5),ds_train):\n",
|
||
" print(f\"{x['label']} ({classes[x['label']]}) -> {x['title']} {x['description']}\")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"## Векторизация на текст\n",
|
||
"\n",
|
||
"Сега трябва да преобразуваме текста в **числа**, които могат да бъдат представени като тензори. Ако искаме представяне на ниво думи, трябва да направим две неща:\n",
|
||
"\n",
|
||
"* Използване на **токенизатор**, за да разделим текста на **токени**.\n",
|
||
"* Създаване на **речник** от тези токени.\n",
|
||
"\n",
|
||
"### Ограничаване на размера на речника\n",
|
||
"\n",
|
||
"В примера с набора от данни AG News, размерът на речника е доста голям — повече от 100 хиляди думи. По принцип, не ни трябват думи, които рядко се срещат в текста — само няколко изречения ще ги съдържат, и моделът няма да се научи от тях. Затова има логика да ограничим размера на речника до по-малко число, като подадем аргумент на конструктора на векторизатора:\n",
|
||
"\n",
|
||
"И двата тези етапа могат да бъдат обработени с помощта на слоя **TextVectorization**. Нека създадем обекта на векторизатора и след това извикаме метода `adapt`, за да преминем през целия текст и да изградим речник:\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 5,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"vocab_size = 50000\n",
|
||
"vectorizer = keras.layers.experimental.preprocessing.TextVectorization(max_tokens=vocab_size)\n",
|
||
"vectorizer.adapt(ds_train.take(500).map(lambda x: x['title']+' '+x['description']))"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"> **Забележка**: Използваме само подмножество от целия набор от данни, за да изградим речник. Правим това, за да ускорим времето за изпълнение и да не ви караме да чакате. Въпреки това, поемаме риска, че някои от думите от целия набор от данни няма да бъдат включени в речника и ще бъдат игнорирани по време на обучението. Следователно, използването на целия размер на речника и преминаването през целия набор от данни по време на `adapt` би трябвало да увеличи крайната точност, но не значително.\n",
|
||
"\n",
|
||
"Сега можем да достъпим действителния речник:\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 6,
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"['', '[UNK]', 'the', 'to', 'a', 'in', 'of', 'and', 'on', 'for']\n",
|
||
"Length of vocabulary: 5335\n"
|
||
]
|
||
}
|
||
],
|
||
"source": [
|
||
"vocab = vectorizer.get_vocabulary()\n",
|
||
"vocab_size = len(vocab)\n",
|
||
"print(vocab[:10])\n",
|
||
"print(f\"Length of vocabulary: {vocab_size}\")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"Използвайки векторизатора, можем лесно да кодираме всеки текст в набор от числа:\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 7,
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"data": {
|
||
"text/plain": [
|
||
"<tf.Tensor: shape=(7,), dtype=int64, numpy=array([ 112, 3695, 3, 304, 11, 1041, 1], dtype=int64)>"
|
||
]
|
||
},
|
||
"execution_count": 7,
|
||
"metadata": {},
|
||
"output_type": "execute_result"
|
||
}
|
||
],
|
||
"source": [
|
||
"vectorizer('I love to play with my words')"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"## Представяне на текст чрез \"чанта с думи\"\n",
|
||
"\n",
|
||
"Тъй като думите носят значение, понякога можем да разберем смисъла на даден текст, като просто разгледаме отделните думи, без да обръщаме внимание на техния ред в изречението. Например, при класифициране на новини, думи като *време* и *сняг* вероятно ще насочат към *прогноза за времето*, докато думи като *акции* и *долар* биха се отнасяли към *финансови новини*.\n",
|
||
"\n",
|
||
"**Чанта с думи** (BoW) е най-простото за разбиране традиционно представяне на вектор. Всяка дума е свързана с индекс във вектора, а елементът на вектора съдържа броя на срещанията на всяка дума в даден документ.\n",
|
||
"\n",
|
||
" \n",
|
||
"\n",
|
||
"> **Note**: Можете също да мислите за BoW като сума от всички едно-горещо-кодирани вектори за отделните думи в текста.\n",
|
||
"\n",
|
||
"По-долу е даден пример за това как да генерирате представяне чрез чанта с думи, използвайки библиотеката Scikit Learn на Python:\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 8,
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"data": {
|
||
"text/plain": [
|
||
"array([[1, 1, 0, 2, 0, 0, 0, 0, 0]], dtype=int64)"
|
||
]
|
||
},
|
||
"execution_count": 8,
|
||
"metadata": {},
|
||
"output_type": "execute_result"
|
||
}
|
||
],
|
||
"source": [
|
||
"from sklearn.feature_extraction.text import CountVectorizer\n",
|
||
"sc_vectorizer = CountVectorizer()\n",
|
||
"corpus = [\n",
|
||
" 'I like hot dogs.',\n",
|
||
" 'The dog ran fast.',\n",
|
||
" 'Its hot outside.',\n",
|
||
" ]\n",
|
||
"sc_vectorizer.fit_transform(corpus)\n",
|
||
"sc_vectorizer.transform(['My dog likes hot dogs on a hot day.']).toarray()"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"Можем също да използваме Keras векторизатора, който дефинирахме по-горе, като преобразуваме всяко число на дума в one-hot кодиране и събираме всички тези вектори.\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 9,
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"data": {
|
||
"text/plain": [
|
||
"array([0., 5., 0., ..., 0., 0., 0.], dtype=float32)"
|
||
]
|
||
},
|
||
"execution_count": 9,
|
||
"metadata": {},
|
||
"output_type": "execute_result"
|
||
}
|
||
],
|
||
"source": [
|
||
"def to_bow(text):\n",
|
||
" return tf.reduce_sum(tf.one_hot(vectorizer(text),vocab_size),axis=0)\n",
|
||
"\n",
|
||
"to_bow('My dog likes hot dogs on a hot day.').numpy()"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"> **Забележка**: Може да се изненадате, че резултатът се различава от предишния пример. Причината е, че в примера с Keras дължината на вектора съответства на размера на речника, който е създаден от целия набор данни AG News, докато в примера със Scikit Learn създадохме речника от примерния текст в движение.\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"## Обучение на класификатора BoW\n",
|
||
"\n",
|
||
"Сега, след като научихме как да създадем представянето на текста като \"чанта с думи\", нека обучим класификатор, който го използва. Първо, трябва да преобразуваме нашия набор от данни в представяне като \"чанта с думи\". Това може да се постигне чрез използване на функцията `map` по следния начин:\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 11,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"batch_size = 128\n",
|
||
"\n",
|
||
"ds_train_bow = ds_train.map(lambda x: (to_bow(x['title']+x['description']),x['label'])).batch(batch_size)\n",
|
||
"ds_test_bow = ds_test.map(lambda x: (to_bow(x['title']+x['description']),x['label'])).batch(batch_size)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"Сега нека дефинираме прост класификаторен невронен модел, който съдържа един линеен слой. Размерът на входа е `vocab_size`, а размерът на изхода съответства на броя на класовете (4). Тъй като решаваме задача за класификация, крайната активационна функция е **softmax**:\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 12,
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"938/938 [==============================] - 66s 70ms/step - loss: 0.6144 - acc: 0.8427 - val_loss: 0.4416 - val_acc: 0.8697\n"
|
||
]
|
||
},
|
||
{
|
||
"data": {
|
||
"text/plain": [
|
||
"<keras.callbacks.History at 0x20c70a947f0>"
|
||
]
|
||
},
|
||
"execution_count": 12,
|
||
"metadata": {},
|
||
"output_type": "execute_result"
|
||
}
|
||
],
|
||
"source": [
|
||
"model = keras.models.Sequential([\n",
|
||
" keras.layers.Dense(4,activation='softmax',input_shape=(vocab_size,))\n",
|
||
"])\n",
|
||
"model.compile(loss='sparse_categorical_crossentropy',optimizer='adam',metrics=['acc'])\n",
|
||
"model.fit(ds_train_bow,validation_data=ds_test_bow)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"Тъй като имаме 4 класа, точност над 80% е добър резултат.\n",
|
||
"\n",
|
||
"## Обучение на класификатор като една мрежа\n",
|
||
"\n",
|
||
"Тъй като векторизаторът също е слой на Keras, можем да дефинираме мрежа, която го включва, и да я обучим от край до край. По този начин не е необходимо да векторизираме набора от данни с помощта на `map`, а можем просто да подадем оригиналния набор от данни към входа на мрежата.\n",
|
||
"\n",
|
||
"> **Note**: Все пак ще трябва да приложим `map` към нашия набор от данни, за да преобразуваме полетата от речници (като `title`, `description` и `label`) в кортежи. Въпреки това, когато зареждаме данни от диска, можем да изградим набор от данни с необходимата структура още от самото начало.\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 13,
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"Model: \"model\"\n",
|
||
"_________________________________________________________________\n",
|
||
" Layer (type) Output Shape Param # \n",
|
||
"=================================================================\n",
|
||
" input_1 (InputLayer) [(None, 1)] 0 \n",
|
||
" \n",
|
||
" text_vectorization (TextVec (None, None) 0 \n",
|
||
" torization) \n",
|
||
" \n",
|
||
" tf.one_hot (TFOpLambda) (None, None, 5335) 0 \n",
|
||
" \n",
|
||
" tf.math.reduce_sum (TFOpLam (None, 5335) 0 \n",
|
||
" bda) \n",
|
||
" \n",
|
||
" dense_2 (Dense) (None, 4) 21344 \n",
|
||
" \n",
|
||
"=================================================================\n",
|
||
"Total params: 21,344\n",
|
||
"Trainable params: 21,344\n",
|
||
"Non-trainable params: 0\n",
|
||
"_________________________________________________________________\n",
|
||
"938/938 [==============================] - 73s 77ms/step - loss: 0.6057 - acc: 0.8414 - val_loss: 0.4202 - val_acc: 0.8736\n"
|
||
]
|
||
},
|
||
{
|
||
"data": {
|
||
"text/plain": [
|
||
"<keras.callbacks.History at 0x20c721521f0>"
|
||
]
|
||
},
|
||
"execution_count": 13,
|
||
"metadata": {},
|
||
"output_type": "execute_result"
|
||
}
|
||
],
|
||
"source": [
|
||
"def extract_text(x):\n",
|
||
" return x['title']+' '+x['description']\n",
|
||
"\n",
|
||
"def tupelize(x):\n",
|
||
" return (extract_text(x),x['label'])\n",
|
||
"\n",
|
||
"inp = keras.Input(shape=(1,),dtype=tf.string)\n",
|
||
"x = vectorizer(inp)\n",
|
||
"x = tf.reduce_sum(tf.one_hot(x,vocab_size),axis=1)\n",
|
||
"out = keras.layers.Dense(4,activation='softmax')(x)\n",
|
||
"model = keras.models.Model(inp,out)\n",
|
||
"model.summary()\n",
|
||
"\n",
|
||
"model.compile(loss='sparse_categorical_crossentropy',optimizer='adam',metrics=['acc'])\n",
|
||
"model.fit(ds_train.map(tupelize).batch(batch_size),validation_data=ds_test.map(tupelize).batch(batch_size))\n"
|
||
]
|
||
},
|
||
{
|
||
"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": 14,
|
||
"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": 14,
|
||
"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-грам с техника за намаляване на размерността, като например *вграждания* (embeddings), които ще обсъдим в следващия модул.\n",
|
||
"\n",
|
||
"За да използваме представяне с n-грам в нашия **AG News** набор от данни, трябва да подадем параметъра `ngrams` на конструктора на `TextVectorization`. Дължината на речника за биграми е **значително по-голяма** – в нашия случай тя е над 1.3 милиона токена! Затова има смисъл да ограничим и токените за биграми до някакъв разумен брой.\n",
|
||
"\n",
|
||
"Можем да използваме същия код като по-горе, за да обучим класификатора, но това би било много неефективно по отношение на паметта. В следващия модул ще обучим класификатор за биграми, използвайки вграждания. Междувременно можете да експериментирате с обучението на класификатор за биграми в този ноутбук и да видите дали можете да постигнете по-висока точност.\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"## Автоматично изчисляване на BoW вектори\n",
|
||
"\n",
|
||
"В примера по-горе изчислихме BoW вектори ръчно, като събрахме едноразрядните кодировки на отделните думи. Въпреки това, последната версия на TensorFlow ни позволява автоматично да изчисляваме BoW вектори, като предадем параметъра `output_mode='count` на конструктора на векторизатора. Това значително улеснява дефинирането и обучението на нашия модел:\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 15,
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"Training vectorizer\n",
|
||
"938/938 [==============================] - 7s 7ms/step - loss: 0.5929 - acc: 0.8486 - val_loss: 0.4168 - val_acc: 0.8772\n"
|
||
]
|
||
},
|
||
{
|
||
"data": {
|
||
"text/plain": [
|
||
"<keras.callbacks.History at 0x20c725217c0>"
|
||
]
|
||
},
|
||
"execution_count": 15,
|
||
"metadata": {},
|
||
"output_type": "execute_result"
|
||
}
|
||
],
|
||
"source": [
|
||
"model = keras.models.Sequential([\n",
|
||
" keras.layers.experimental.preprocessing.TextVectorization(max_tokens=vocab_size,output_mode='count'),\n",
|
||
" keras.layers.Dense(4,input_shape=(vocab_size,), activation='softmax')\n",
|
||
"])\n",
|
||
"print(\"Training vectorizer\")\n",
|
||
"model.layers[0].adapt(ds_train.take(500).map(extract_text))\n",
|
||
"model.compile(loss='sparse_categorical_crossentropy',optimizer='adam',metrics=['acc'])\n",
|
||
"model.fit(ds_train.map(tupelize).batch(batch_size),validation_data=ds_test.map(tupelize).batch(batch_size))"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"## Честота на термина - обратна честота на документа (TF-IDF)\n",
|
||
"\n",
|
||
"В представянето на BoW, появите на думите се претеглят с една и съща техника, независимо от самата дума. Въпреки това е ясно, че често срещани думи като *a* и *in* са много по-малко важни за класификацията в сравнение със специализирани термини. В повечето задачи в обработката на естествен език някои думи са по-релевантни от други.\n",
|
||
"\n",
|
||
"**TF-IDF** означава **честота на термина - обратна честота на документа**. Това е вариация на bag-of-words, при която вместо бинарна стойност 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": 16,
|
||
"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": 16,
|
||
"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": [
|
||
"В Keras слоят `TextVectorization` може автоматично да изчислява TF-IDF честоти, като се зададе параметърът `output_mode='tf-idf'`. Нека повторим кода, който използвахме по-горе, за да видим дали използването на TF-IDF увеличава точността:\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 17,
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"Training vectorizer\n",
|
||
"938/938 [==============================] - 12s 12ms/step - loss: 0.4197 - acc: 0.8662 - val_loss: 0.3432 - val_acc: 0.8849\n"
|
||
]
|
||
},
|
||
{
|
||
"data": {
|
||
"text/plain": [
|
||
"<keras.callbacks.History at 0x20c729dfd30>"
|
||
]
|
||
},
|
||
"execution_count": 17,
|
||
"metadata": {},
|
||
"output_type": "execute_result"
|
||
}
|
||
],
|
||
"source": [
|
||
"model = keras.models.Sequential([\n",
|
||
" keras.layers.experimental.preprocessing.TextVectorization(max_tokens=vocab_size,output_mode='tf-idf'),\n",
|
||
" keras.layers.Dense(4,input_shape=(vocab_size,), activation='softmax')\n",
|
||
"])\n",
|
||
"print(\"Training vectorizer\")\n",
|
||
"model.layers[0].adapt(ds_train.take(500).map(extract_text))\n",
|
||
"model.compile(loss='sparse_categorical_crossentropy',optimizer='adam',metrics=['acc'])\n",
|
||
"model.fit(ds_train.map(tupelize).batch(batch_size),validation_data=ds_test.map(tupelize).batch(batch_size))"
|
||
]
|
||
},
|
||
{
|
||
"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": "0cb620c6d4b9f7a635928804c26cf22403d89d98d79684e4529119355ee6d5a5"
|
||
},
|
||
"kernel_info": {
|
||
"name": "conda-env-py37_tensorflow-py"
|
||
},
|
||
"kernelspec": {
|
||
"display_name": "py37_tensorflow",
|
||
"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"
|
||
},
|
||
"nteract": {
|
||
"version": "nteract-front-end@1.0.0"
|
||
},
|
||
"coopTranslator": {
|
||
"original_hash": "19b43951d55b377a76209c24c1f017e4",
|
||
"translation_date": "2025-08-30T01:16:45+00:00",
|
||
"source_file": "lessons/5-NLP/13-TextRep/TextRepresentationTF.ipynb",
|
||
"language_code": "bg"
|
||
}
|
||
},
|
||
"nbformat": 4,
|
||
"nbformat_minor": 4
|
||
} |