{ "cells": [ { "cell_type": "markdown", "metadata": { "id": "t9ZNGOygrFsT" }, "source": [ "Redes Neuronales para Lenguaje Natural, 2024\n", "\n", "---\n", "# **Análisis de sentimento con BERT en español (BETO)**\n", "\n", "En este notebook haremos experimentos utilizando modelos de tipo BERT:\n", "- Tokenización de oraciones.\n", "- Fine-tuning de un analizador de sentimiento. Para esto utilizaremos un corpus de comentarios sobre preoductos, películas y restaurantes traducidos al español.\n", "- Experimentos sobre uso de modelo de lenguaje enmascarado.\n", "\n", "---\n", "\n" ] }, { "cell_type": "markdown", "source": [ "Empezaremos por instalar la librería transformers." ], "metadata": { "id": "RmoRzsBZV41u" } }, { "cell_type": "markdown", "source": [ "Algunos imports que utilizaremos a lo largo de este notebook y algunas definiciones:" ], "metadata": { "id": "r288PtJmV2EU" } }, { "cell_type": "code", "source": [ "import time\n", "\n", "import pandas as pd\n", "import torch\n", "\n", "import transformers\n", "from transformers import DistilBertTokenizerFast\n", "from transformers import DistilBertForSequenceClassification\n", "from transformers import DistilBertForMaskedLM\n", "\n", "torch.backends.cudnn.deterministic = True\n", "RANDOM_SEED = 248\n", "torch.manual_seed(RANDOM_SEED)\n", "DEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n", "\n", "print(DEVICE)" ], "metadata": { "id": "CyC64OZJlE-1" }, "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "source": [ "## **Dataset**\n", "\n", "Vamos a utilizar una versión traducida al español del dataset presentado en *'From Group to Individual Labels using Deep Features'*, Kotzias et al. (2015), que es una compilación de reviews sobre productos, películas y restaurantes. Cada review tiene una valoración de 0 (negativo) o 1 (positivo). Ejecute el siguiente bloque para descargarlo:" ], "metadata": { "id": "tB-KtT-qWiYN" } }, { "cell_type": "code", "source": [ "! wget https://www.fing.edu.uy/owncloud/index.php/s/DRm5GJnSBbL6rny/download/senti_corpus.zip\n", "! unzip senti_corpus.zip" ], "metadata": { "id": "aDfflRqcWpfz" }, "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "source": [ "Cargamos el archivo en memoria con pandas:" ], "metadata": { "id": "ebqyJisqaY8s" } }, { "cell_type": "code", "source": [ "import numpy as np\n", "import pandas as pd\n", "from collections import Counter\n", "from sklearn.metrics import precision_recall_fscore_support, accuracy_score\n", "\n", "train_df = pd.read_csv('./senti-train.tsv',sep='\\\\t')\n", "dev_df = pd.read_csv('./senti-dev.tsv',sep='\\\\t')\n", "test_df = pd.read_csv('./senti-test.tsv',sep='\\\\t')\n", "\n", "train_df.head()" ], "metadata": { "id": "gidnXvr5lFGG" }, "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "source": [ "Desplegamos la cantidad de filas y columnas de nuestros datos, y los separamos en textos y labels:" ], "metadata": { "id": "3QQZvR-6A5tg" } }, { "cell_type": "code", "source": [ "print(\"Train:\",train_df.shape)\n", "print(\"Dev:\",dev_df.shape)\n", "print(\"Test:\",test_df.shape)\n", "\n", "train_texts = train_df.iloc[:,0].values\n", "train_labels = train_df.iloc[:,1].values\n", "\n", "dev_texts = dev_df.iloc[:,0].values\n", "dev_labels = dev_df.iloc[:,1].values\n", "\n", "test_texts = test_df.iloc[:,0].values\n", "test_labels = test_df.iloc[:,1].values\n", "\n", "print(train_texts[0])\n", "print(train_labels[0])" ], "metadata": { "id": "sdiLZO9JlFIt" }, "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "source": [ "## **Tokenización**\n", "\n", "Para utilizar un modelo preentrenado BERT es preciso tokenizar la entrada de la misma manera con la que el modelo fue preentrenado. En nuestro caso utilizaremos la versión de BERT en español **BETO** creada por la Universidad de Chile, pero en su variante DistilBERT que es una versión más pequeña y más rápida (https://huggingface.co/docs/transformers/model_doc/distilbert). En particular utilizaremos \"distilbert-base-spanish-uncased\", por lo tanto cargamos el tokenizer correspondiente:" ], "metadata": { "id": "Ny1Ob9Udbe8Z" } }, { "cell_type": "code", "source": [ "tokenizer = DistilBertTokenizerFast.from_pretrained('dccuchile/distilbert-base-spanish-uncased')\n", "\n", "print(\"Vocabulario:\",tokenizer.vocab_size)\n", "print(\"Índice de 'perro':\",tokenizer.vocab[\"perro\"])\n", "print(\"Índice de [MASK]:\",tokenizer.vocab[\"[MASK]\"])\n", "print(\"Índice de [CLS]:\",tokenizer.vocab[\"[CLS]\"])\n", "print(\"Índice de [SEP]:\",tokenizer.vocab[\"[SEP]\"])" ], "metadata": { "id": "8H2udnofbSo5" }, "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "source": [ "Observemos cómo funciona el tokenizador con un par de ejemplos.\n", "Utilice el método del tokenizador para obtener la representación de una oración y de un par de oraciones.\n", "- ¿Qué clase se utiliza para almacenar una representación tokenizada?\n", "- ¿Qué atributos tiene esta representación? Observe qué devuelve al imprimir el contenido de la variable y al imprimir su atributo \"encodings\"." ], "metadata": { "id": "m31C5Y1_JFok" } }, { "cell_type": "code", "source": [ "sentence1 = \"Juan habla rapidísimo y lleno de uruguayismos.\"\n", "sentence2 = \"Uruguay es el mejor país.\"\n", "\n", "sentence_encoding = # su código aquí\n", "pair_encoding = # su código aquí\n" ], "metadata": { "id": "Jm7G4e58F6S3" }, "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "source": [ "Ahora tokenice las tres particiones de train, dev y test. Para eso utilice los argumentos truncation=True y padding=True." ], "metadata": { "id": "xlHBS2IaC_Ax" } }, { "cell_type": "code", "source": [ "train_encodings = # su código aquí\n", "dev_encodings = # su código aquí\n", "test_encodings = # su código aquí" ], "metadata": { "id": "PkUfioYMbSr0" }, "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "source": [ "Analicemos un elemento tokenizado:" ], "metadata": { "id": "-A63A4Ztdjw8" } }, { "cell_type": "code", "source": [ "print(train_encodings[0])\n", "print(train_encodings[0].tokens)\n", "print(train_encodings[0].tokens.index('[PAD]'))\n", "print(train_encodings[0].attention_mask.index(0))\n", "print(len(train_encodings[0].tokens))" ], "metadata": { "id": "ZTkF--KobSuR" }, "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "source": [ "Observe algunos ejemplos y analice el contenido de cada campo (por dudas consulte https://huggingface.co/docs/tokenizers/api/encoding).\n", "\n", "Responda:\n", "- ¿Qué objetivo cumple el campo attention_mask?\n", "- ¿Qué objetivo cumple el campo special_tokens_mask?" ], "metadata": { "id": "4_Y3qLjDdqPJ" } }, { "cell_type": "code", "source": [ "print(train_encodings[0].attention_mask)\n", "print(train_encodings[0].special_tokens_mask)" ], "metadata": { "id": "fs0-h3iGbSw2" }, "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "source": [ "\n", "Una vez tokenizados, instanciamos los datos como Dataset de pytorch y los metemos en un DataLoader para cada partición." ], "metadata": { "id": "NXO6TGraevwC" } }, { "cell_type": "code", "source": [ "class SentiCorpusDataset(torch.utils.data.Dataset):\n", " def __init__(self, encodings, labels):\n", " self.encodings = encodings\n", " self.labels = labels\n", "\n", " def __getitem__(self, idx):\n", " item = {key: torch.tensor(val[idx]) for key, val in self.encodings.items()}\n", " item['labels'] = torch.tensor(self.labels[idx])\n", " return item\n", "\n", " def __len__(self):\n", " return len(self.labels)\n", "\n", "train_dataset = SentiCorpusDataset(train_encodings, train_labels)\n", "dev_dataset = SentiCorpusDataset(dev_encodings, dev_labels)\n", "test_dataset = SentiCorpusDataset(test_encodings, test_labels)\n", "\n", "train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=16, shuffle=True)\n", "dev_loader = torch.utils.data.DataLoader(dev_dataset, batch_size=16, shuffle=False)\n", "test_loader = torch.utils.data.DataLoader(test_dataset, batch_size=16, shuffle=False)\n" ], "metadata": { "id": "LTLWLRq6bS29" }, "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "source": [ "## **Carga y *fine-tuning* de un modelo BERT preentrenado**\n", "\n", "Cargamos \"distilbert-base-spanish-uncased\":" ], "metadata": { "id": "dJczS9mQlrTj" } }, { "cell_type": "code", "source": [ "model = DistilBertForSequenceClassification.from_pretrained('dccuchile/distilbert-base-spanish-uncased')\n", "model.to(DEVICE)" ], "metadata": { "id": "EbWij3kylV88" }, "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "source": [ "Evaluaremos los modelos resultantes con la siguente función para calcular el accuracy.\n", "\n", "Notar que al invocar el forward del modelo, se deben enviar los inputs (tokens) y además la máscada de atención obtenida como resultado de la tokenización." ], "metadata": { "id": "L9oKwDCWI_hU" } }, { "cell_type": "code", "source": [ "def compute_accuracy(model, data_loader, device):\n", " with torch.no_grad():\n", " correct_pred = 0\n", " num_examples = 0\n", "\n", " for batch_idx, batch in enumerate(data_loader):\n", "\n", " ### Obtener los token ids, la máscara de atención y los labels\n", " input_ids = batch['input_ids'].to(device)\n", " attention_mask = batch['attention_mask'].to(device)\n", " labels = batch['labels'].to(device)\n", "\n", " ### Pasada forward\n", " outputs = model(input_ids, attention_mask=attention_mask)\n", " logits = outputs['logits']\n", " predicted_labels = torch.argmax(logits, 1)\n", "\n", " num_examples += labels.size(0)\n", " correct_pred += (predicted_labels == labels).sum()\n", "\n", " return correct_pred.float()/num_examples * 100" ], "metadata": { "id": "tM2zotIhlWCd" }, "execution_count": null, "outputs": [] }, { "cell_type": "code", "source": [ "compute_accuracy(model,test_loader,DEVICE)" ], "metadata": { "id": "fSepKJUHP_Tc" }, "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "source": [ "Completar el siguiente ciclo de entrenamiento para realizar el fine-tuning del modelo preentrenado. En este caso además de enviar los tokens y la máscara de atención, se le deben enviar al modelo los labels esperados, y como parte de las salidas se obtendrá el *loss*." ], "metadata": { "id": "NCwz3JPRJ3M_" } }, { "cell_type": "code", "source": [ "NUM_EPOCHS = 5\n", "optim = torch.optim.Adam(model.parameters(), lr=5e-6)\n", "\n", "start_time = time.time()\n", "\n", "for epoch in range(NUM_EPOCHS):\n", "\n", " model.train()\n", "\n", " for batch_idx, batch in enumerate(train_loader):\n", "\n", " ### Obtener los token ids, la máscara de atención y los labels\n", " # su código aquí\n", "\n", " ### Pasada forward, invocar el modelo y obtener:\n", " ### - el loss para optimizar\n", " ### - los logits para calcular la performance\n", " outputs = # su código aquí\n", " loss = # su código aquí\n", " logits = # su código aquí\n", "\n", " ### Pasada backward\n", " optim.zero_grad()\n", " loss.backward()\n", " optim.step()\n", "\n", " ### Logging\n", " if not batch_idx % 20:\n", " print (f'Epoch: {epoch+1:04d}/{NUM_EPOCHS:04d} | '\n", " f'Batch {batch_idx:04d}/{len(train_loader):04d} | '\n", " f'Loss: {loss:.4f}')\n", "\n", " model.eval()\n", "\n", " with torch.set_grad_enabled(False):\n", " print(f'Accuracy en training: '\n", " f'{compute_accuracy(model, train_loader, DEVICE):.2f}%'\n", " f'\\nAccuracy en dev: '\n", " f'{compute_accuracy(model, dev_loader, DEVICE):.2f}%')\n", "\n", " print(f'Tiempo: {(time.time() - start_time)/60:.2f} min')\n", "\n", "print(f'Tiempo total: {(time.time() - start_time)/60:.2f} min')\n" ], "metadata": { "id": "EF2hspvSlWHX" }, "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "source": [ "Para completar el experimento evaluamos en **test** el modelo resultante del fine-tuning:" ], "metadata": { "id": "9hL2WFI_KGsY" } }, { "cell_type": "code", "source": [ "print(f'Accuracy en test: {compute_accuracy(model, test_loader, DEVICE):.2f}%')" ], "metadata": { "id": "xQByjodClWO6" }, "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "source": [ "# **(opcional) Uso de MLM**\n", "\n", "Ahora veremos el uso de BERT preentrenado como modelo de lenguaje enmascarado. Lo utilizaremos para predecir algunas palabras posibles para completar de la siguiente manera:\n", "\n", "- Instancie un modelo de BERT preentrenado para MLM usando la clase DistilBertForMaskedLM\n", "- Tokenize algunas oraciones, asegurándose de que incluyan al menos un token [MASK]\n", "- Haga la pasada forward de la red y obtenga los K tokens más probables predichos para cada máscara. ¿Las palabras predichas por el modelo se corresponden con su intuición?" ], "metadata": { "id": "eec9alDSZ5dJ" } }, { "cell_type": "code", "source": [ "# Ejemplos de oraciones (puede agregar todas las que quiera para probar)\n", "sentence1 = \"[MASK] es el mejor país.\"\n", "sentence2 = \"Caniche es una raza de [MASK].\"\n", "sentence3 = \"Juan [MASK] una carrera.\"\n" ], "metadata": { "id": "jSzk86DclWZl" }, "execution_count": null, "outputs": [] }, { "cell_type": "code", "source": [ "# Cargar el modelo tipo MLM\n", "# mlm = # su código aquí\n" ], "metadata": { "id": "UDBIYC6cLb3w" }, "execution_count": null, "outputs": [] }, { "cell_type": "code", "source": [ "def get_mlm_result(sentence, model, k):\n", " # Este método obtiene una oración con un token [MASK], un modelo y una cantidad de palabras k\n", " # y devuelve las k palabras más probables predichas por el modelo en esa posición\n", " # Escriba su código a partir de aquí\n", " pass\n", "\n", "print(sentence1, get_mlm_result(sentence1, mlm, 3))\n", "print(sentence2, get_mlm_result(sentence2, mlm, 3))\n", "print(sentence3, get_mlm_result(sentence3, mlm, 3))\n" ], "metadata": { "id": "yxj8z9MJuTHh" }, "execution_count": null, "outputs": [] } ], "metadata": { "colab": { "provenance": [], "gpuType": "T4" }, "kernelspec": { "display_name": "Python 3", "name": "python3" }, "language_info": { "name": "python" }, "accelerator": "GPU" }, "nbformat": 4, "nbformat_minor": 0 }