{ "cells": [ { "cell_type": "markdown", "metadata": { "id": "t9ZNGOygrFsT" }, "source": [ "Redes Neuronales para Lenguaje Natural, 2023\n", "\n", "---\n", "# POS-tagging con redes LSTM\n", "\n", "En este notebook construiremos un etiquetador de categorías gramaticales (POS-tagger) usando una arquitectura LSTM en Pytorch. Los datos que utilizaremos son una versión reducida del corpus AnCora en español, etiquetada según el estándar Universal Dependencies.\n", "\n", "\n", "---\n", "\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "F8yN8Ywyushh" }, "outputs": [], "source": [ "import string\n", "import random\n", "import numpy as np\n", "import torch\n", "import torch.nn as nn\n", "import torch.optim as optim\n", "import torch.nn.functional as F\n", "\n", "BATCH_SIZE = 64" ] }, { "cell_type": "markdown", "metadata": { "id": "-wlcQnjW3PqL" }, "source": [ "Descargar y abrir los archivos de datos" ] }, { "cell_type": "code", "source": [ "! wget https://eva.fing.edu.uy/mod/resource/view.php?id=195738 -O ancora_mini.zip\n", "! unzip ancora_mini.zip" ], "metadata": { "id": "yO3KckvwvUGC" }, "execution_count": null, "outputs": [] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "d9xCY5UWoaCb" }, "outputs": [], "source": [ "def load_data_file(filename):\n", " with open(filename) as f:\n", " tokens = []\n", " sent = []\n", " for l in f:\n", " if l.startswith('#'):\n", " continue\n", " toks = l.strip().split('\\t')\n", " if (len(toks) >= 3) and ('-' not in toks[0]) and ('.' not in toks[0]):\n", " sent.append((toks[1],toks[3]))\n", " elif len(toks) <= 1:\n", " tokens.append(sent)\n", " sent = []\n", "\n", " if len(sent) > 0:\n", " tokens.append(sent)\n", "\n", " return tokens\n", "\n", "train = load_data_file('ancora_mini_train.txt')\n", "dev = load_data_file('ancora_mini_dev.txt')\n", "test = load_data_file('ancora_mini_test.txt')\n", "print(len(train),len(dev),len(test))" ] }, { "cell_type": "markdown", "source": [ "Imprimimos un par de ejemplos para ver el formato con el que estamos trabajando." ], "metadata": { "id": "4AaOmFrg77ar" } }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "EChHgBsovMrx" }, "outputs": [], "source": [ "print(train[0])\n", "print(train[20])" ] }, { "cell_type": "markdown", "metadata": { "id": "K4UUqZJT3XAR" }, "source": [ "Trabajaremos con la colección de embeddings de SBWC. Descargarlos y abrirlos con gensim.\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "U6Qas-YKZ2n5" }, "outputs": [], "source": [ "! wget https://cs.famaf.unc.edu.ar/~ccardellino/SBWCE/SBW-vectors-300-min5.bin.gz\n", "! gzip -d SBW-vectors-300-min5.bin.gz" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "h9s_pZExZ8kj" }, "outputs": [], "source": [ "from gensim.models import KeyedVectors\n", "wv = KeyedVectors.load_word2vec_format(\"./SBW-vectors-300-min5.bin\", binary=True)\n", "print(wv.vectors.shape)\n", "vocab_size = wv.vectors.shape[0]\n", "embeddings_size = wv.vectors.shape[1]" ] }, { "cell_type": "markdown", "source": [ "Haremos un pequeño análisis de cobertura de la colección de embeddings sobre los tokens utilizados en el corpus.\n", "\n", "Consideraremos cuatro clases de tokens: palabras conocidas (están en SBWC), números, símbolos de puntuación, y palabras desconocidas.\n", "\n", "La heurística para determinar si un token es número o símbolo de puntuación será si comienza con un dígito o símbolo de puntuación." ], "metadata": { "id": "QOaUInwP0fLr" } }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "e8DBHu1vUjFs" }, "outputs": [], "source": [ "words = []\n", "puncts = []\n", "nums = []\n", "unk = []\n", "labels = set()\n", "for sent in train+dev+test:\n", " for (t,p) in sent:\n", " if t in wv:\n", " words.append(t)\n", " elif t[0] in string.punctuation:\n", " puncts.append(t)\n", " elif t[0] in ['0','1','2','3','4','5','6','7','8','9']:\n", " nums.append(t)\n", " else:\n", " unk.append(t)\n", " labels.add(p)\n", "\n", "print(\"Total: w %s p %s n %s u %s\" % (len(words),len(puncts),len(nums),len(unk)))\n", "words = set(words)\n", "puncts = set(puncts)\n", "nums = set(nums)\n", "unk = set(unk)\n", "print(\"Únicas: w %s p %s n %s u %s\" % (len(words),len(puncts),len(nums),len(unk)))\n" ] }, { "cell_type": "markdown", "source": [ "Por lo que vemos del análisis, tenemos vectores para todas las palabras conocidas y para los dígitos, y solamente hay un 0.05% de palabras que no están en la colección de embeddings. Utilizaremos un embedding aleatorio para representar estas palabras desconocidas.\n", "\n", "A esto hay que agregarle los símbolos de puntuación: SBWC no contiene embeddings para estos símbolos. Como forma de paliar esta situación, también nos crearemos un embedding aleatorio.\n", "\n", "Pytorch trabajará con secuencias de números enteros, y no con las palabras en sí, por lo que construiremos una forma de pasar cada posible token y cada posible label a una representación numérica.\n", "\n", "Primero comenzamos creando los diccionarios de palabras y labels que utilizaremos. Estos diccionarios contendrán todos los posibles tokens y labels representables en nuestro corpus.\n", "\n", "A los tokens presentes en el corpus, les agregaremos cuatro tokens especiales:\n", "- PAD: para realizar padding de las secuencias (se verá más adelante)\n", "- UNK: representa las palabras desconocidas\n", "- PUNCT: representa símbolos de puntuación\n", "- NUM: representa números\n", "\n", "A los labels les agregaremos solo un label especial NONE que se corresponderá con los tokens de PAD." ], "metadata": { "id": "z5bt8iv_0_Wo" } }, { "cell_type": "code", "source": [ "words = ['','','',''] + sorted(words)\n", "wordict = { w:i for (i,w) in enumerate(words) }\n", "labels = ['NONE'] + sorted(labels)\n", "labeldict = { l:i for (i,l) in enumerate(labels) }\n", "\n", "def get_index(word,wordict):\n", " if word in wordict:\n", " return wordict[word]\n", " elif word[0] in string.punctuation:\n", " return wordict['']\n", " elif word[0] in [0,1,2,3,4,5,6,7,8,9]:\n", " return wordict['']\n", " else:\n", " return wordict['']\n", "\n", "print(\"words\",words[:10],\"...\")\n", "print(\"wordict\",[(w,wordict[w]) for w in words[:10]],\"...\")\n", "print(\"labels\",labels)\n", "print(\"labeldict\",labeldict)" ], "metadata": { "id": "wnpiB3-y044O" }, "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "source": [ "A continuación nos crearemos una matriz más pequeña que tenga solo los embeddings con los que vamos a trabajar, de forma de que ocupen la menor cantidad de memoria posible. Esta matriz servirá como pesos de inicialización de la capa de embeddings de la red.\n", "\n", "Crear una matriz de embeddings que sea del tamaño len(words) * 300 conteniendo los embeddings que representan todos los tokens en orden.\n", "\n", "El embedding correspondiente a los números en SBWC es \"DIGITO\", el embeddings correspondiente a \"PAD\" será un vector de ceros, y será necesario crearse dos embeddings aleatorios (distintos) del tamaño correcto para representar símbolos de puntuación y palabras desconocidas." ], "metadata": { "id": "Z_ldzX153qbl" } }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "E1AC-fBfjPU-" }, "outputs": [], "source": [ "# su código aquí...\n", "pad_vector = ...\n", "punct_vector = ...\n", "num_vector = ...\n", "unk_vector = ...\n", "embeddings = ...\n", "\n", "print(\"Matriz de embeddings:\",embeddings.shape)\n", "vocab_size = embeddings.shape[0]\n", "embeddings_dim = embeddings.shape[1]\n" ] }, { "cell_type": "markdown", "source": [ "Notar que hasta ahora nuestros ejemplos tienen este formato:\n", "\n", "`[ (token1,label1), (token2,label2), (token3,label3), ....]`\n", "\n", "\n", "Necesitamos que estén en el siguiente formato para poder presentarlos a la red y luego comparar la salida esperada a la obtenida:\n", "\n", "`( [num_token1, num_token2, num_token3,...], [num_label1, num_label2, num_label3,...] )`\n", "\n", "Escribir código para transformar las tres particiones al formato necesario." ], "metadata": { "id": "5eh6pVnj5A-d" } }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "ckTeiX2hjHI1" }, "outputs": [], "source": [ "# su código aquí\n", "train_word_labels = ...\n", "dev_word_labels = ...\n", "test_word_labels = ...\n", "\n", "print(\"train\",len(train_word_labels))\n", "print(\"dev\",len(dev_word_labels))\n", "print(\"test\",len(test_word_labels))\n" ] }, { "cell_type": "markdown", "source": [ "En este ejercicio crearemos los minibatches a mano. Para que sea más eficiente el entrenamiento, usaremos la técnica de ordenar las oraciones, de forma que dentro de un mismo batch queden oraciones de tamaños lo más similares posible.\n", "\n", "Empecemos por ordenar las oraciones dentro de cada partición por largo:" ], "metadata": { "id": "UKBcRroV5oMy" } }, { "cell_type": "code", "source": [ "train_word_labels.sort(key = lambda x:len(x[0]))\n", "dev_word_labels.sort(key = lambda x:len(x[0]))\n", "test_word_labels.sort(key = lambda x:len(x[0]))" ], "metadata": { "id": "Ox3pWdDeB6Uw" }, "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "source": [ "Ahora que las particiones tienen las oraciones ordenadas por largo, escriba una función que obtenga los ejemplos de una partición y genere minibatches de tamaño BATCH_SIZE particionando la lista.\n", "\n", "Estos minibatches serán utilizados como entrada y salida esperada de la red, por lo que tendrán que tener la forma requerida por Pytorch: la función debe devolver dos listas, word_batches y label_batches, que contengan tensores de tipo long.\n", "\n", "Notar que para poder crear un tensor a partir de un minibatch, todas las oraciones del minibatch deben tener el mismo largo. Para eso utilizaremos el token especial PAD y el label especial NONE que definimos antes." ], "metadata": { "id": "1lA5BgNhB-cc" } }, { "cell_type": "code", "source": [ "def get_batches(words_labels):\n", " # su código aquí\n", " ...\n", " return word_batches,label_batches\n" ], "metadata": { "id": "pFfb-h5RyDYd" }, "execution_count": null, "outputs": [] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "MYaSaDdllSts" }, "outputs": [], "source": [ "train_word_batches,train_label_batches = get_batches(train_word_labels)\n", "print(\"train\",train_word_batches[0].shape,train_word_batches[0].dtype)\n", "dev_word_batches,dev_label_batches = get_batches(dev_word_labels)\n", "print(\"dev\",dev_word_batches[0].shape,dev_word_batches[0].dtype)\n", "test_word_batches,test_label_batches = get_batches(test_word_labels)\n", "print(\"test\",test_word_batches[0].shape,test_word_batches[0].dtype)\n" ] }, { "cell_type": "markdown", "source": [ "Llegó la hora de implementar la red LSTM. Esta red debe tener:\n", "- Una capa de embeddings (torch.nn.Embeddings), la que inicializaremos con la matriz de embeddings que definimos más arriba (poner requires_grad en False para que el entrenamiento sea más rápido, no queremos modificar los pesos de los embeddings)\n", "- Una capa LSTM (torch.nn.LSTM), su entrada es la dimensión de los embeddings, y el tamaño de la capa oculta debe quedar como parámetro\n", "- Una capa lineal (torch.nn.Linear) que va de la capa oculta de la LSTM a la salida final, que tiene tamaño len(labels)\n" ], "metadata": { "id": "0upYt_I87w2J" } }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "680r8lnDt-80" }, "outputs": [], "source": [ "class LSTMTagger(nn.Module):\n", "\n", " def __init__(self, ....):\n", " super(LSTMTagger, self).__init__()\n", " # su código aquí\n", "\n", " def forward(self, batch):\n", " # su código aquí\n", " return tag_scores\n" ] }, { "cell_type": "markdown", "source": [ "Escribir una función para evaluar la red. Debe tomar los minibatches de palabras y de labels a evaluar, ejecutar la red y calcular el accuracy." ], "metadata": { "id": "Mdvn0Xm99Y0L" } }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "LpYoFJOnyFlm" }, "outputs": [], "source": [ "def evaluate(net,word_batches,label_batches):\n", " # su código aquí\n", " acc = ...\n", " print(\"accuracy\",acc)\n", " return acc\n" ] }, { "cell_type": "markdown", "source": [ "Instanciar y entrenar la red. Utilizaremos la función de pérdida NLLLoss, notar que para eso es necesario que lo que devuelva la red sea el logaritmo de las probabilidades (usar la función log_softmax como activación a la salida de la red)." ], "metadata": { "id": "mwV2j1ee-F16" } }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "QU9y99W07qBZ" }, "outputs": [], "source": [ "loss_function = nn.NLLLoss()\n", "\n", "lstmnet = LSTMTagger(...) # los parámetros apropiados\n", "evaluate(lstmnet,dev_word_batches,dev_label_batches) # seguro va a dar muy cerca de 0 al principio!\n", "\n", "optimizer = optim.SGD(lstmnet.parameters(), lr=0.1)\n", "\n", "for epoch in range(...): # elegir\n", " for word_batch,label_batch in zip(train_word_batches,train_label_batches):\n", " # su código aquí, se debe:\n", " # - inicializar los gradientes\n", " # - ejecutar la red sobre el batch (paso forward)\n", " # - usar la loss function para comparar la salida de la red con los labels esperados\n", " # - calcular los gradientes (paso backward)\n", " # - realizar el descenso por gradiente\n", "\n", " # ya que estamos, imprimimos la performance sobre dev luego de completada una epoch\n", " evaluate(lstmnet,dev_word_batches,dev_label_batches)\n", "\n" ] }, { "cell_type": "markdown", "source": [ "Finalmente, evaluar el resultado de la mejor red encontrada sobre el corpus de test." ], "metadata": { "id": "FRaogiT09l14" } }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "KitTLt2rSNRF" }, "outputs": [], "source": [ "evaluate(lstmnet,test_word_batches,test_label_batches)" ] } ], "metadata": { "accelerator": "GPU", "colab": { "provenance": [], "gpuType": "T4" }, "kernelspec": { "display_name": "Python 3", "name": "python3" }, "language_info": { "name": "python" } }, "nbformat": 4, "nbformat_minor": 0 }