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

577 lines
22 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": [
"# Tarea de clasificación de texto\n",
"\n",
"Como hemos mencionado, nos centraremos en una tarea sencilla de clasificación de texto basada en el dataset **AG_NEWS**, que consiste en clasificar titulares de noticias en una de las 4 categorías: Mundo, Deportes, Negocios y Ciencia/Tecnología.\n",
"\n",
"## El Dataset\n",
"\n",
"Este dataset está integrado en el módulo [`torchtext`](https://github.com/pytorch/text), por lo que podemos acceder a él fácilmente.\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": [
"Aquí, `train_dataset` y `test_dataset` contienen colecciones que devuelven pares de etiqueta (número de clase) y texto respectivamente, por ejemplo:\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": [
"Entonces, imprimamos los primeros 10 nuevos titulares de nuestro conjunto de datos:\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": [
"Debido a que los conjuntos de datos son iteradores, si queremos usar los datos varias veces, necesitamos convertirlos a una lista:\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": [
"## Tokenización\n",
"\n",
"Ahora necesitamos convertir el texto en **números** que puedan representarse como tensores. Si queremos una representación a nivel de palabras, debemos hacer dos cosas: \n",
"* usar un **tokenizador** para dividir el texto en **tokens** \n",
"* construir un **vocabulario** de esos tokens. \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": [
"Usando vocabulario, podemos codificar fácilmente nuestra cadena tokenizada en un conjunto de números:\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": [
"## Representación de texto con Bolsa de Palabras\n",
"\n",
"Debido a que las palabras representan significado, a veces podemos entender el significado de un texto simplemente observando las palabras individuales, sin importar su orden en la oración. Por ejemplo, al clasificar noticias, palabras como *clima*, *nieve* probablemente indiquen *pronóstico del tiempo*, mientras que palabras como *acciones*, *dólar* podrían corresponder a *noticias financieras*.\n",
"\n",
"La representación vectorial **Bolsa de Palabras** (BoW) es la representación vectorial tradicional más utilizada. Cada palabra está vinculada a un índice del vector, y el elemento del vector contiene el número de veces que una palabra aparece en un documento dado.\n",
"\n",
"![Imagen que muestra cómo se representa en memoria una representación vectorial de bolsa de palabras.](../../../../../lessons/5-NLP/13-TextRep/images/bag-of-words-example.png) \n",
"\n",
"> **Nota**: También puedes pensar en BoW como la suma de todos los vectores codificados en uno a uno para las palabras individuales en el texto.\n",
"\n",
"A continuación, se muestra un ejemplo de cómo generar una representación de bolsa de palabras utilizando la biblioteca de Python Scikit Learn:\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": [
"Para calcular el vector de bolsa de palabras a partir de la representación vectorial de nuestro conjunto de datos AG_NEWS, podemos usar la siguiente función:\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": [
"> **Nota:** Aquí estamos utilizando la variable global `vocab_size` para especificar el tamaño predeterminado del vocabulario. Dado que a menudo el tamaño del vocabulario es bastante grande, podemos limitar el tamaño del vocabulario a las palabras más frecuentes. Intenta reducir el valor de `vocab_size` y ejecutar el código a continuación, y observa cómo afecta la precisión. Deberías esperar una disminución en la precisión, pero no dramática, en lugar de un mayor rendimiento.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Entrenando el clasificador BoW\n",
"\n",
"Ahora que hemos aprendido cómo construir la representación de Bag-of-Words para nuestro texto, vamos a entrenar un clasificador sobre esta base. Primero, necesitamos convertir nuestro conjunto de datos para el entrenamiento de tal manera que todas las representaciones vectoriales posicionales se conviertan en representaciones de Bag-of-Words. Esto se puede lograr pasando la función `bowify` como el parámetro `collate_fn` al `DataLoader` estándar de 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": [
"Ahora definamos una red neuronal clasificador simple que contiene una capa lineal. El tamaño del vector de entrada es igual a `vocab_size`, y el tamaño de salida corresponde al número de clases (4). Debido a que estamos resolviendo una tarea de clasificación, la función de activación final es `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": [
"Ahora definiremos el bucle de entrenamiento estándar de PyTorch. Debido a que nuestro conjunto de datos es bastante grande, para nuestro propósito de enseñanza entrenaremos solo por una época, y a veces incluso por menos de una época (especificar el parámetro `epoch_size` nos permite limitar el entrenamiento). También informaremos la precisión acumulada del entrenamiento durante el proceso; la frecuencia de los informes se especifica utilizando el parámetro `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": [
"## BiGrams, TriGrams y N-Grams\n",
"\n",
"Una limitación del enfoque de bolsa de palabras es que algunas palabras forman expresiones de varias palabras. Por ejemplo, la palabra 'hot dog' tiene un significado completamente diferente al de las palabras 'hot' y 'dog' en otros contextos. Si representamos las palabras 'hot' y 'dog' siempre con los mismos vectores, esto puede confundir a nuestro modelo.\n",
"\n",
"Para abordar este problema, las **representaciones N-gram** se utilizan frecuentemente en métodos de clasificación de documentos, donde la frecuencia de cada palabra, bi-palabra o tri-palabra es una característica útil para entrenar clasificadores. En la representación de bigramas, por ejemplo, añadimos todos los pares de palabras al vocabulario, además de las palabras originales.\n",
"\n",
"A continuación, se muestra un ejemplo de cómo generar una representación de bolsa de palabras con bigramas utilizando 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": [
"La principal desventaja del enfoque N-gram es que el tamaño del vocabulario comienza a crecer extremadamente rápido. En la práctica, necesitamos combinar la representación N-gram con algunas técnicas de reducción de dimensionalidad, como *embeddings*, que discutiremos en la próxima unidad.\n",
"\n",
"Para usar la representación N-gram en nuestro conjunto de datos **AG News**, necesitamos construir un vocabulario especial de 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": [
"Podríamos usar el mismo código mencionado anteriormente para entrenar el clasificador, sin embargo, sería muy ineficiente en términos de memoria. En la próxima unidad, entrenaremos un clasificador de bigramas utilizando embeddings.\n",
"\n",
"> **Nota:** Solo puedes conservar aquellos ngrams que aparezcan en el texto más veces que el número especificado. Esto asegurará que los bigramas poco frecuentes sean omitidos y reducirá significativamente la dimensionalidad. Para lograr esto, ajusta el parámetro `min_freq` a un valor más alto y observa cómo cambia la longitud del vocabulario.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Frecuencia de Términos e Inversa Frecuencia de Documentos TF-IDF\n",
"\n",
"En la representación BoW, las ocurrencias de palabras tienen el mismo peso, sin importar la palabra en sí. Sin embargo, está claro que las palabras frecuentes, como *a*, *en*, etc., son mucho menos importantes para la clasificación que los términos especializados. De hecho, en la mayoría de las tareas de PLN, algunas palabras son más relevantes que otras.\n",
"\n",
"**TF-IDF** significa **frecuencia de términosinversa frecuencia de documentos**. Es una variación de bolsa de palabras, donde en lugar de un valor binario 0/1 que indica la aparición de una palabra en un documento, se utiliza un valor de punto flotante relacionado con la frecuencia de aparición de la palabra en el corpus.\n",
"\n",
"Más formalmente, el peso $w_{ij}$ de una palabra $i$ en el documento $j$ se define como:\n",
"$$\n",
"w_{ij} = tf_{ij}\\times\\log({N\\over df_i})\n",
"$$\n",
"donde\n",
"* $tf_{ij}$ es el número de ocurrencias de $i$ en $j$, es decir, el valor BoW que hemos visto antes\n",
"* $N$ es el número de documentos en la colección\n",
"* $df_i$ es el número de documentos que contienen la palabra $i$ en toda la colección\n",
"\n",
"El valor TF-IDF $w_{ij}$ aumenta proporcionalmente al número de veces que una palabra aparece en un documento y se ajusta por el número de documentos en el corpus que contienen la palabra, lo que ayuda a compensar el hecho de que algunas palabras aparecen con más frecuencia que otras. Por ejemplo, si la palabra aparece en *todos* los documentos de la colección, $df_i=N$, y $w_{ij}=0$, y esos términos serían completamente ignorados.\n",
"\n",
"Puedes crear fácilmente una vectorización TF-IDF de texto utilizando 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": [
"## Conclusión\n",
"\n",
"Sin embargo, aunque las representaciones TF-IDF asignan un peso de frecuencia a diferentes palabras, no son capaces de representar el significado ni el orden. Como dijo el famoso lingüista J. R. Firth en 1935: “El significado completo de una palabra siempre es contextual, y ningún estudio del significado fuera del contexto puede tomarse en serio”. Más adelante en el curso aprenderemos cómo capturar información contextual del texto utilizando modelos de lenguaje.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n---\n\n**Descargo de responsabilidad**: \nEste documento ha sido traducido utilizando el servicio de traducción automática [Co-op Translator](https://github.com/Azure/co-op-translator). Aunque nos esforzamos por garantizar la precisión, tenga en cuenta que las traducciones automatizadas pueden contener errores o imprecisiones. El documento original en su idioma nativo debe considerarse como la fuente autorizada. Para información crítica, se recomienda una traducción profesional realizada por humanos. No nos hacemos responsables de malentendidos o interpretaciones erróneas que puedan surgir del uso de esta traducción.\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-31T17:16:57+00:00",
"source_file": "lessons/5-NLP/13-TextRep/TextRepresentationPyTorch.ipynb",
"language_code": "es"
}
},
"nbformat": 4,
"nbformat_minor": 2
}