diff --git a/notebooks/13_Recurrent_Neural_Networks.ipynb b/notebooks/13_Recurrent_Neural_Networks.ipynb new file mode 100644 index 0000000..e89dfee --- /dev/null +++ b/notebooks/13_Recurrent_Neural_Networks.ipynb @@ -0,0 +1,2421 @@ +{ + "nbformat": 4, + "nbformat_minor": 0, + "metadata": { + "colab": { + "name": "13_Recurrent_Neural_Networks", + "version": "0.3.2", + "provenance": [], + "collapsed_sections": [], + "toc_visible": true + }, + "kernelspec": { + "name": "python3", + "display_name": "Python 3" + }, + "accelerator": "GPU" + }, + "cells": [ + { + "metadata": { + "id": "bOChJSNXtC9g", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "# Recurrent Neural Networks" + ] + }, + { + "metadata": { + "id": "OLIxEDq6VhvZ", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "\n", + "\n", + "When working with sequential data (time-series, sentences, etc.) the order of the inputs is crucial for the task at hand. Recurrent neural networks (RNNs) process sequential data by accounting for the current input and also what has been learned from previous inputs. In this notebook, we'll learn how to create and train RNNs on sequential data.\n", + "\n", + "\n", + "\n", + "\n" + ] + }, + { + "metadata": { + "id": "VoMq0eFRvugb", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "# Overview" + ] + }, + { + "metadata": { + "id": "qWro5T5qTJJL", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "* **Objective:** Process sequential data by accounting for the currend input and also what has been learned from previous inputs.\n", + "* **Advantages:** \n", + " * Account for order and previous inputs in a meaningful way.\n", + " * Conditioned generation for generating sequences.\n", + "* **Disadvantages:** \n", + " * Each time step's prediction depends on the previous prediction so it's difficult to parallelize RNN operations. \n", + " * Processing long sequences can yield memory and computation issues.\n", + " * Interpretability is difficult but there are few [techniques](https://arxiv.org/abs/1506.02078) that use the activations from RNNs to see what parts of the inputs are processed. \n", + "* **Miscellaneous:** \n", + " * Architectural tweaks to make RNNs faster and interpretable is an ongoing area of research." + ] + }, + { + "metadata": { + "id": "rsHeBbehrKzl", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "\n", + "\n", + "RNN forward pass for a single time step $X_t$:\n", + "\n", + "$h_t = tanh(W_{hh}h_{t-1} + W_{xh}X_t+b_h)$\n", + "\n", + "$y_t = W_{hy}h_t + b_y $\n", + "\n", + "$ P(y) = softmax(y_t) = \\frac{e^y}{\\sum e^y} $\n", + "\n", + "*where*:\n", + "* $X_t$ = input at time step t | $\\in \\mathbb{R}^{NXE}$ ($N$ is the batch size, $E$ is the embedding dim)\n", + "* $W_{hh}$ = hidden units weights| $\\in \\mathbb{R}^{HXH}$ ($H$ is the hidden dim)\n", + "* $h_{t-1}$ = previous timestep's hidden state $\\in \\mathbb{R}^{NXH}$\n", + "* $W_{xh}$ = input weights| $\\in \\mathbb{R}^{EXH}$\n", + "* $b_h$ = hidden units bias $\\in \\mathbb{R}^{HX1}$\n", + "* $W_{hy}$ = output weights| $\\in \\mathbb{R}^{HXC}$ ($C$ is the number of classes)\n", + "* $b_y$ = output bias $\\in \\mathbb{R}^{CX1}$\n", + "\n", + "You repeat this for every time step's input ($X_{t+1}, X_{t+2}, ..., X_{N})$ to the get the predicted outputs at each time step.\n", + "\n", + "**Note**: At the first time step, the previous hidden state $h_{t-1}$ can either be a zero vector (unconditioned) or initialize (conditioned). If we are conditioning the RNN, the first hidden state $h_0$ can belong to a specific condition or we can concat the specific condition to the randomly initialized hidden vectors at each time step. More on this in the subsequent notebooks on RNNs." + ] + }, + { + "metadata": { + "id": "dIXlGMExJD6w", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "Let's see what the forward pass looks like with an RNN for a synthetic task such as processing reviews (a sequence of words) to predict the sentiment at the end of processing the review." + ] + }, + { + "metadata": { + "id": "RcWE5cw0_cKA", + "colab_type": "code", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 34 + }, + "outputId": "a44156b9-b43f-409c-f0ce-4a4bd871d6a0" + }, + "cell_type": "code", + "source": [ + "# Load PyTorch library\n", + "!pip3 install torch" + ], + "execution_count": 1, + "outputs": [ + { + "output_type": "stream", + "text": [ + "Requirement already satisfied: torch in /usr/local/lib/python3.6/dist-packages (1.0.0)\n" + ], + "name": "stdout" + } + ] + }, + { + "metadata": { + "id": "o6eEK1wM_dXG", + "colab_type": "code", + "colab": {} + }, + "cell_type": "code", + "source": [ + "import torch\n", + "import torch.nn as nn\n", + "import torch.nn.functional as F" + ], + "execution_count": 0, + "outputs": [] + }, + { + "metadata": { + "id": "Qi9hIEV6COLF", + "colab_type": "code", + "colab": {} + }, + "cell_type": "code", + "source": [ + "batch_size = 5\n", + "seq_size = 10 # max length per input (masking will be used for sequences that aren't this max length)\n", + "x_lengths = [8, 5, 4, 10, 5] # lengths of each input sequence\n", + "embedding_dim = 100\n", + "rnn_hidden_dim = 256\n", + "output_dim = 4" + ], + "execution_count": 0, + "outputs": [] + }, + { + "metadata": { + "id": "bLEzfxjhB94C", + "colab_type": "code", + "outputId": "f2feefbf-8635-4b23-ef53-b5713cf2cdb2", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 34 + } + }, + "cell_type": "code", + "source": [ + "# Initialize synthetic inputs\n", + "x_in = torch.randn(batch_size, seq_size, embedding_dim)\n", + "x_lengths = torch.tensor(x_lengths)\n", + "print (x_in.size())" + ], + "execution_count": 4, + "outputs": [ + { + "output_type": "stream", + "text": [ + "torch.Size([5, 10, 100])\n" + ], + "name": "stdout" + } + ] + }, + { + "metadata": { + "id": "dr6oLqtXB98N", + "colab_type": "code", + "outputId": "9817e88d-6e73-414a-dfa6-2386f40db0d9", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 34 + } + }, + "cell_type": "code", + "source": [ + "# Initialize hidden state\n", + "hidden_t = torch.zeros((batch_size, rnn_hidden_dim))\n", + "print (hidden_t.size())" + ], + "execution_count": 5, + "outputs": [ + { + "output_type": "stream", + "text": [ + "torch.Size([5, 256])\n" + ], + "name": "stdout" + } + ] + }, + { + "metadata": { + "id": "ryZMOLLgB9-v", + "colab_type": "code", + "outputId": "14ec0a2a-bf37-4e03-b69b-099180f8f149", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 34 + } + }, + "cell_type": "code", + "source": [ + "# Initialize RNN cell\n", + "rnn_cell = nn.RNNCell(embedding_dim, rnn_hidden_dim)\n", + "print (rnn_cell)" + ], + "execution_count": 6, + "outputs": [ + { + "output_type": "stream", + "text": [ + "RNNCell(100, 256)\n" + ], + "name": "stdout" + } + ] + }, + { + "metadata": { + "id": "rlbZ7ujxExXb", + "colab_type": "code", + "outputId": "6c83ba2b-94c5-4f76-c8fb-ef0c1ccdeb37", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 34 + } + }, + "cell_type": "code", + "source": [ + "# Forward pass through RNN\n", + "x_in = x_in.permute(1, 0, 2) # RNN needs batch_size to be at dim 1\n", + "\n", + "# Loop through the inputs time steps\n", + "hiddens = []\n", + "for t in range(seq_size):\n", + " hidden_t = rnn_cell(x_in[t], hidden_t)\n", + " hiddens.append(hidden_t)\n", + "hiddens = torch.stack(hiddens)\n", + "hiddens = hiddens.permute(1, 0, 2) # bring batch_size back to dim 0\n", + "print (hiddens.size())" + ], + "execution_count": 7, + "outputs": [ + { + "output_type": "stream", + "text": [ + "torch.Size([5, 10, 256])\n" + ], + "name": "stdout" + } + ] + }, + { + "metadata": { + "id": "3TTL-jmg-MHa", + "colab_type": "code", + "outputId": "3fae323f-c37d-4dac-c8a8-7fea7a45c95c", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 51 + } + }, + "cell_type": "code", + "source": [ + "# We also could've used a more abstracted layer\n", + "x_in = torch.randn(batch_size, seq_size, embedding_dim)\n", + "rnn = nn.RNN(embedding_dim, rnn_hidden_dim, batch_first=True)\n", + "out, h_n = rnn(x_in) #h_n is the last hidden state\n", + "print (\"out: \", out.size())\n", + "print (\"h_n: \", h_n.size())" + ], + "execution_count": 8, + "outputs": [ + { + "output_type": "stream", + "text": [ + "out: torch.Size([5, 10, 256])\n", + "h_n: torch.Size([1, 5, 256])\n" + ], + "name": "stdout" + } + ] + }, + { + "metadata": { + "id": "iAsyRNnbHwcT", + "colab_type": "code", + "colab": {} + }, + "cell_type": "code", + "source": [ + "def gather_last_relevant_hidden(hiddens, x_lengths):\n", + " x_lengths = x_lengths.long().detach().cpu().numpy() - 1\n", + " out = []\n", + " for batch_index, column_index in enumerate(x_lengths):\n", + " out.append(hiddens[batch_index, column_index])\n", + " return torch.stack(out)" + ], + "execution_count": 0, + "outputs": [] + }, + { + "metadata": { + "id": "PVhp1KLqHqpA", + "colab_type": "code", + "outputId": "d04be3ef-c2d6-48b9-f0f5-a93f619ec594", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 34 + } + }, + "cell_type": "code", + "source": [ + "# Gather the last relevant hidden state\n", + "z = gather_last_relevant_hidden(hiddens, x_lengths)\n", + "print (z.size())" + ], + "execution_count": 10, + "outputs": [ + { + "output_type": "stream", + "text": [ + "torch.Size([5, 256])\n" + ], + "name": "stdout" + } + ] + }, + { + "metadata": { + "id": "yGk_iZ5cITZl", + "colab_type": "code", + "outputId": "84749ff2-1e45-4599-a38d-8c83cee116a9", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 119 + } + }, + "cell_type": "code", + "source": [ + "# Forward pass through FC layer\n", + "fc1 = nn.Linear(rnn_hidden_dim, output_dim)\n", + "y_pred = fc1(z)\n", + "y_pred = F.softmax(y_pred, dim=1)\n", + "print (y_pred.size())\n", + "print (y_pred)" + ], + "execution_count": 11, + "outputs": [ + { + "output_type": "stream", + "text": [ + "torch.Size([5, 4])\n", + "tensor([[0.3030, 0.2351, 0.2168, 0.2452],\n", + " [0.2614, 0.1912, 0.2617, 0.2858],\n", + " [0.2428, 0.2600, 0.2254, 0.2717],\n", + " [0.2379, 0.2226, 0.1901, 0.3494],\n", + " [0.2629, 0.2854, 0.2146, 0.2371]], grad_fn=)\n" + ], + "name": "stdout" + } + ] + }, + { + "metadata": { + "id": "hPBQpki_n6yY", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "# Sequential data" + ] + }, + { + "metadata": { + "id": "kP1awuluoCSr", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "There are a variety of different sequential tasks that RNNs can help with.\n", + "\n", + "1. **One to one**: there is one input and produces one output. \n", + " * Ex. Given a word predict it's class (verb, noun, etc.).\n", + "2. **One to many**: one input generates many outputs.\n", + " * Ex. Given a sentiment (positive, negative, etc.) generate a review.\n", + "3. **Many to one**: Many inputs are sequentially processed to generate one output.\n", + " * Ex. Process the words in a review to predict the sentiment.\n", + "4. **Many to many**: Many inputs are sequentially processed to generate many outputs.\n", + " * Ex. Given a sentence in French, processes the entire sentence and then generate the English translation.\n", + " * Ex. Given a sequence of time-series data, predict the probability of an event (risk of disease) at each time step.\n", + "\n", + "" + ] + }, + { + "metadata": { + "id": "tnxUIEMdukYY", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "# Issues with vanilla RNNs" + ] + }, + { + "metadata": { + "id": "uMx2s93VLUTt", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "There are several issues with the vanilla RNN that we've seen so far. \n", + "\n", + "1. When we have an input sequence that has many time steps, it becomes difficult for the model to retain information seen earlier as we process more and more of the downstream timesteps. The goals of the model is to retain the useful components in the previously seen time steps but this becomes cumbersome when we have so many time steps to process. \n", + "\n", + "2. During backpropagation, the gradient from the loss has to travel all the way back towards the first time step. If our gradient is larger than 1 (${1.01}^{1000} = 20959$) or less than 1 (${0.99}^{1000} = 4.31e-5$) and we have lot's of time steps, this can quickly spiral out of control.\n", + "\n", + "To address both these issues, the concept of gating was introduced to RNNs. Gating allows RNNs to control the information flow between each time step to optimize on the task. Selectively allowing information to pass through allows the model to process inputs with many time steps. The most common RNN gated varients are the long short term memory ([LSTM](https://pytorch.org/docs/stable/nn.html#torch.nn.LSTM)) units and gated recurrent units ([GRUs](https://pytorch.org/docs/stable/nn.html#torch.nn.GRU)). You can read more about how these units work [here](http://colah.github.io/posts/2015-08-Understanding-LSTMs/).\n", + "\n", + "" + ] + }, + { + "metadata": { + "id": "tirko0kwp-9J", + "colab_type": "code", + "colab": {} + }, + "cell_type": "code", + "source": [ + "# GRU in PyTorch\n", + "gru = nn.GRU(input_size=embedding_dim, hidden_size=rnn_hidden_dim, \n", + " batch_first=True)" + ], + "execution_count": 0, + "outputs": [] + }, + { + "metadata": { + "id": "UZjUhh4VBWxM", + "colab_type": "code", + "outputId": "9fe275fe-c8d9-42f0-e5d0-0295268ed83d", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 34 + } + }, + "cell_type": "code", + "source": [ + "# Initialize synthetic input\n", + "x_in = torch.randn(batch_size, seq_size, embedding_dim)\n", + "print (x_in.size())" + ], + "execution_count": 13, + "outputs": [ + { + "output_type": "stream", + "text": [ + "torch.Size([5, 10, 100])\n" + ], + "name": "stdout" + } + ] + }, + { + "metadata": { + "id": "xJ_SE7AvBfa4", + "colab_type": "code", + "outputId": "b9411aaa-fab1-4104-aee7-8f9a423332ab", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 51 + } + }, + "cell_type": "code", + "source": [ + "# Forward pass\n", + "out, h_n = gru(x_in)\n", + "print (\"out:\", out.size())\n", + "print (\"h_n:\", h_n.size())" + ], + "execution_count": 14, + "outputs": [ + { + "output_type": "stream", + "text": [ + "out: torch.Size([5, 10, 256])\n", + "h_n: torch.Size([1, 5, 256])\n" + ], + "name": "stdout" + } + ] + }, + { + "metadata": { + "id": "ij_GA2Rr9BbA", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "**Note**: Choosing whether to use GRU or LSTM really depends on the data and empirical performance. GRUs offer comparable performance with reduce number of parameters while LSTMs are more efficient and may make the difference in performance for your particular task." + ] + }, + { + "metadata": { + "id": "9agJw4gwK1LC", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "# Bidirectional RNNs" + ] + }, + { + "metadata": { + "id": "Xck0n-KpmXkV", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "There have been many advancements with RNNs ([attention](https://www.oreilly.com/ideas/interpretability-via-attentional-and-memory-based-interfaces-using-tensorflow), Quasi RNNs, etc.) that we will cover in later lessons but one of the basic and widely used ones are bidirectional RNNs (Bi-RNNs). The motivation behind bidirectional RNNs is to process an input sequence by both directions. Accounting for context from both sides can aid in performance when the entire input sequence is known at time of inference. A common application of Bi-RNNs is in translation where it's advantageous to look at an entire sentence from both sides when translating to another language (ie. Japanese → English).\n", + "\n", + "" + ] + }, + { + "metadata": { + "id": "gSk_5XrvApCd", + "colab_type": "code", + "colab": {} + }, + "cell_type": "code", + "source": [ + "# BiGRU in PyTorch\n", + "bi_gru = nn.GRU(input_size=embedding_dim, hidden_size=rnn_hidden_dim, \n", + " batch_first=True, bidirectional=True)" + ], + "execution_count": 0, + "outputs": [] + }, + { + "metadata": { + "id": "Fx7-GTptBCtZ", + "colab_type": "code", + "outputId": "f0242cc5-534a-460b-ebe0-4e8c504fab22", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 51 + } + }, + "cell_type": "code", + "source": [ + "# Forward pass\n", + "out, h_n = bi_gru(x_in)\n", + "print (\"out:\", out.size()) # collection of all hidden states from the RNN for each time step\n", + "print (\"h_n:\", h_n.size()) # last hidden state from the RNN" + ], + "execution_count": 16, + "outputs": [ + { + "output_type": "stream", + "text": [ + "out: torch.Size([5, 10, 512])\n", + "h_n: torch.Size([2, 5, 256])\n" + ], + "name": "stdout" + } + ] + }, + { + "metadata": { + "id": "k5lvJirLBjI6", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "Notice that the output for each sample at each timestamp has size 512 (double the hidden dim). This is because this includes both the forward and backward directions from the BiRNN. " + ] + }, + { + "metadata": { + "id": "mJSknbofK2S9", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "# Document classification with RNNs" + ] + }, + { + "metadata": { + "id": "JgYdEZmHlmft", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "Let's apply RNNs to the document classification task from the [emebddings notebook](https://colab.research.google.com/drive/1yDa5ZTqKVoLl-qRgH-N9xs3pdrDJ0Fb4) where we want to predict an article's category given its title." + ] + }, + { + "metadata": { + "id": "eIvXqvPQEiDC", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "## Set up" + ] + }, + { + "metadata": { + "id": "muTcvMynlmAu", + "colab_type": "code", + "colab": {} + }, + "cell_type": "code", + "source": [ + "import os\n", + "from argparse import Namespace\n", + "import collections\n", + "import copy\n", + "import json\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import pandas as pd\n", + "import re\n", + "import torch" + ], + "execution_count": 0, + "outputs": [] + }, + { + "metadata": { + "id": "00ESjecep-_y", + "colab_type": "code", + "colab": {} + }, + "cell_type": "code", + "source": [ + "# Set Numpy and PyTorch seeds\n", + "def set_seeds(seed, cuda):\n", + " np.random.seed(seed)\n", + " torch.manual_seed(seed)\n", + " if cuda:\n", + " torch.cuda.manual_seed_all(seed)\n", + " \n", + "# Creating directories\n", + "def create_dirs(dirpath):\n", + " if not os.path.exists(dirpath):\n", + " os.makedirs(dirpath)" + ], + "execution_count": 0, + "outputs": [] + }, + { + "metadata": { + "id": "m67THDvxEl1e", + "colab_type": "code", + "outputId": "7118c77b-cbf9-4d7e-ff7a-b9dc1fb63cbb", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 34 + } + }, + "cell_type": "code", + "source": [ + "# Arguments\n", + "args = Namespace(\n", + " seed=1234,\n", + " cuda=True,\n", + " shuffle=True,\n", + " data_file=\"news.csv\",\n", + " split_data_file=\"split_news.csv\",\n", + " vectorizer_file=\"vectorizer.json\",\n", + " model_state_file=\"model.pth\",\n", + " save_dir=\"news\",\n", + " train_size=0.7,\n", + " val_size=0.15,\n", + " test_size=0.15,\n", + " pretrained_embeddings=None,\n", + " cutoff=25, # token must appear at least times to be in SequenceVocabulary\n", + " num_epochs=5,\n", + " early_stopping_criteria=5,\n", + " learning_rate=1e-3,\n", + " batch_size=64,\n", + " embedding_dim=100,\n", + " rnn_hidden_dim=128,\n", + " hidden_dim=100,\n", + " num_layers=1,\n", + " bidirectional=False,\n", + " dropout_p=0.1,\n", + ")\n", + "\n", + "# Set seeds\n", + "set_seeds(seed=args.seed, cuda=args.cuda)\n", + "\n", + "# Create save dir\n", + "create_dirs(args.save_dir)\n", + "\n", + "# Expand filepaths\n", + "args.vectorizer_file = os.path.join(args.save_dir, args.vectorizer_file)\n", + "args.model_state_file = os.path.join(args.save_dir, args.model_state_file)\n", + "\n", + "# Check CUDA\n", + "if not torch.cuda.is_available():\n", + " args.cuda = False\n", + "args.device = torch.device(\"cuda\" if args.cuda else \"cpu\")\n", + "print(\"Using CUDA: {}\".format(args.cuda))" + ], + "execution_count": 22, + "outputs": [ + { + "output_type": "stream", + "text": [ + "Using CUDA: True\n" + ], + "name": "stdout" + } + ] + }, + { + "metadata": { + "id": "s7T-_kGvExVW", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "## Data" + ] + }, + { + "metadata": { + "id": "XVyK25xOEwjN", + "colab_type": "code", + "colab": {} + }, + "cell_type": "code", + "source": [ + "import re\n", + "import urllib" + ], + "execution_count": 0, + "outputs": [] + }, + { + "metadata": { + "id": "M_gclwECEwll", + "colab_type": "code", + "colab": {} + }, + "cell_type": "code", + "source": [ + "# Upload data from GitHub to notebook's local drive\n", + "url = \"https://raw.githubusercontent.com/GokuMohandas/practicalAI/master/data/news.csv\"\n", + "response = urllib.request.urlopen(url)\n", + "html = response.read()\n", + "with open(args.data_file, 'wb') as fp:\n", + " fp.write(html)" + ], + "execution_count": 0, + "outputs": [] + }, + { + "metadata": { + "id": "V244zOIPEwoP", + "colab_type": "code", + "outputId": "ab8b5cab-4e25-436e-9cb3-0db6f524eb9a", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 204 + } + }, + "cell_type": "code", + "source": [ + "# Raw data\n", + "df = pd.read_csv(args.data_file, header=0)\n", + "df.head()" + ], + "execution_count": 25, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
categorytitle
0BusinessWall St. Bears Claw Back Into the Black (Reuters)
1BusinessCarlyle Looks Toward Commercial Aerospace (Reu...
2BusinessOil and Economy Cloud Stocks' Outlook (Reuters)
3BusinessIraq Halts Oil Exports from Main Southern Pipe...
4BusinessOil prices soar to all-time record, posing new...
\n", + "
" + ], + "text/plain": [ + " category title\n", + "0 Business Wall St. Bears Claw Back Into the Black (Reuters)\n", + "1 Business Carlyle Looks Toward Commercial Aerospace (Reu...\n", + "2 Business Oil and Economy Cloud Stocks' Outlook (Reuters)\n", + "3 Business Iraq Halts Oil Exports from Main Southern Pipe...\n", + "4 Business Oil prices soar to all-time record, posing new..." + ] + }, + "metadata": { + "tags": [] + }, + "execution_count": 25 + } + ] + }, + { + "metadata": { + "id": "ICl2MNK4EwrL", + "colab_type": "code", + "outputId": "d2073597-71e5-40b1-a845-90bf4913ea7a", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 85 + } + }, + "cell_type": "code", + "source": [ + "# Split by category\n", + "by_category = collections.defaultdict(list)\n", + "for _, row in df.iterrows():\n", + " by_category[row.category].append(row.to_dict())\n", + "for category in by_category:\n", + " print (\"{0}: {1}\".format(category, len(by_category[category])))" + ], + "execution_count": 26, + "outputs": [ + { + "output_type": "stream", + "text": [ + "Business: 30000\n", + "Sci/Tech: 30000\n", + "Sports: 30000\n", + "World: 30000\n" + ], + "name": "stdout" + } + ] + }, + { + "metadata": { + "id": "76PwKQHLEww5", + "colab_type": "code", + "colab": {} + }, + "cell_type": "code", + "source": [ + "# Create split data\n", + "final_list = []\n", + "for _, item_list in sorted(by_category.items()):\n", + " if args.shuffle:\n", + " np.random.shuffle(item_list)\n", + " n = len(item_list)\n", + " n_train = int(args.train_size*n)\n", + " n_val = int(args.val_size*n)\n", + " n_test = int(args.test_size*n)\n", + "\n", + " # Give data point a split attribute\n", + " for item in item_list[:n_train]:\n", + " item['split'] = 'train'\n", + " for item in item_list[n_train:n_train+n_val]:\n", + " item['split'] = 'val'\n", + " for item in item_list[n_train+n_val:]:\n", + " item['split'] = 'test' \n", + "\n", + " # Add to final list\n", + " final_list.extend(item_list)" + ], + "execution_count": 0, + "outputs": [] + }, + { + "metadata": { + "id": "CQeS0KHOEwzm", + "colab_type": "code", + "outputId": "93c9aadb-25c4-4029-f002-8a43f3956045", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 85 + } + }, + "cell_type": "code", + "source": [ + "# df with split datasets\n", + "split_df = pd.DataFrame(final_list)\n", + "split_df[\"split\"].value_counts()" + ], + "execution_count": 28, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + "train 84000\n", + "val 18000\n", + "test 18000\n", + "Name: split, dtype: int64" + ] + }, + "metadata": { + "tags": [] + }, + "execution_count": 28 + } + ] + }, + { + "metadata": { + "id": "pPJDyVusEw3-", + "colab_type": "code", + "colab": {} + }, + "cell_type": "code", + "source": [ + "# Preprocessing\n", + "def preprocess_text(text):\n", + " text = ' '.join(word.lower() for word in text.split(\" \"))\n", + " text = re.sub(r\"([.,!?])\", r\" \\1 \", text)\n", + " text = re.sub(r\"[^a-zA-Z.,!?]+\", r\" \", text)\n", + " text = text.strip()\n", + " return text\n", + " \n", + "split_df.title = split_df.title.apply(preprocess_text)" + ], + "execution_count": 0, + "outputs": [] + }, + { + "metadata": { + "id": "IAetKendEw6b", + "colab_type": "code", + "outputId": "d5946f7e-840e-4a0b-e492-d3da68cefd44", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 204 + } + }, + "cell_type": "code", + "source": [ + "# Save to CSV\n", + "split_df.to_csv(args.split_data_file, index=False)\n", + "split_df.head()" + ], + "execution_count": 30, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
categorysplittitle
0Businesstraingeneral electric posts higher rd quarter profit
1Businesstrainlilly to eliminate up to us jobs
2Businesstrains amp p lowers america west outlook to negative
3Businesstraindoes rand walk the talk on labor policy ?
4Businesstrainhousekeeper advocates for changes
\n", + "
" + ], + "text/plain": [ + " category split title\n", + "0 Business train general electric posts higher rd quarter profit\n", + "1 Business train lilly to eliminate up to us jobs\n", + "2 Business train s amp p lowers america west outlook to negative\n", + "3 Business train does rand walk the talk on labor policy ?\n", + "4 Business train housekeeper advocates for changes" + ] + }, + "metadata": { + "tags": [] + }, + "execution_count": 30 + } + ] + }, + { + "metadata": { + "id": "NHzGXAI3E7lF", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "## Vocabulary" + ] + }, + { + "metadata": { + "id": "ZIRUjX0MEw88", + "colab_type": "code", + "colab": {} + }, + "cell_type": "code", + "source": [ + "class Vocabulary(object):\n", + " def __init__(self, token_to_idx=None):\n", + "\n", + " # Token to index\n", + " if token_to_idx is None:\n", + " token_to_idx = {}\n", + " self.token_to_idx = token_to_idx\n", + "\n", + " # Index to token\n", + " self.idx_to_token = {idx: token \\\n", + " for token, idx in self.token_to_idx.items()}\n", + "\n", + " def to_serializable(self):\n", + " return {'token_to_idx': self.token_to_idx}\n", + "\n", + " @classmethod\n", + " def from_serializable(cls, contents):\n", + " return cls(**contents)\n", + "\n", + " def add_token(self, token):\n", + " if token in self.token_to_idx:\n", + " index = self.token_to_idx[token]\n", + " else:\n", + " index = len(self.token_to_idx)\n", + " self.token_to_idx[token] = index\n", + " self.idx_to_token[index] = token\n", + " return index\n", + "\n", + " def add_tokens(self, tokens):\n", + " return [self.add_token[token] for token in tokens]\n", + "\n", + " def lookup_token(self, token):\n", + " return self.token_to_idx[token]\n", + "\n", + " def lookup_index(self, index):\n", + " if index not in self.idx_to_token:\n", + " raise KeyError(\"the index (%d) is not in the Vocabulary\" % index)\n", + " return self.idx_to_token[index]\n", + "\n", + " def __str__(self):\n", + " return \"\" % len(self)\n", + "\n", + " def __len__(self):\n", + " return len(self.token_to_idx)" + ], + "execution_count": 0, + "outputs": [] + }, + { + "metadata": { + "id": "1LtYf3lpExBb", + "colab_type": "code", + "outputId": "617297a7-3fdb-4789-bbca-dea82d06c8ce", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 85 + } + }, + "cell_type": "code", + "source": [ + "# Vocabulary instance\n", + "category_vocab = Vocabulary()\n", + "for index, row in df.iterrows():\n", + " category_vocab.add_token(row.category)\n", + "print (category_vocab) # __str__\n", + "print (len(category_vocab)) # __len__\n", + "index = category_vocab.lookup_token(\"Business\")\n", + "print (index)\n", + "print (category_vocab.lookup_index(index))" + ], + "execution_count": 38, + "outputs": [ + { + "output_type": "stream", + "text": [ + "\n", + "4\n", + "0\n", + "Business\n" + ], + "name": "stdout" + } + ] + }, + { + "metadata": { + "id": "Z0zkF6CsE_yH", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "## Sequence vocabulary" + ] + }, + { + "metadata": { + "id": "QtntaISyE_1c", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "Next, we're going to create our Vocabulary classes for the article's title, which is a sequence of tokens." + ] + }, + { + "metadata": { + "id": "ovI8QRefEw_p", + "colab_type": "code", + "colab": {} + }, + "cell_type": "code", + "source": [ + "from collections import Counter\n", + "import string" + ], + "execution_count": 0, + "outputs": [] + }, + { + "metadata": { + "id": "4W3ZouuTEw1_", + "colab_type": "code", + "colab": {} + }, + "cell_type": "code", + "source": [ + "class SequenceVocabulary(Vocabulary):\n", + " def __init__(self, token_to_idx=None, unk_token=\"\",\n", + " mask_token=\"\", begin_seq_token=\"\",\n", + " end_seq_token=\"\"):\n", + "\n", + " super(SequenceVocabulary, self).__init__(token_to_idx)\n", + "\n", + " self.mask_token = mask_token\n", + " self.unk_token = unk_token\n", + " self.begin_seq_token = begin_seq_token\n", + " self.end_seq_token = end_seq_token\n", + "\n", + " self.mask_index = self.add_token(self.mask_token)\n", + " self.unk_index = self.add_token(self.unk_token)\n", + " self.begin_seq_index = self.add_token(self.begin_seq_token)\n", + " self.end_seq_index = self.add_token(self.end_seq_token)\n", + " \n", + " # Index to token\n", + " self.idx_to_token = {idx: token \\\n", + " for token, idx in self.token_to_idx.items()}\n", + "\n", + " def to_serializable(self):\n", + " contents = super(SequenceVocabulary, self).to_serializable()\n", + " contents.update({'unk_token': self.unk_token,\n", + " 'mask_token': self.mask_token,\n", + " 'begin_seq_token': self.begin_seq_token,\n", + " 'end_seq_token': self.end_seq_token})\n", + " return contents\n", + "\n", + " def lookup_token(self, token):\n", + " return self.token_to_idx.get(token, self.unk_index)\n", + " \n", + " def lookup_index(self, index):\n", + " if index not in self.idx_to_token:\n", + " raise KeyError(\"the index (%d) is not in the SequenceVocabulary\" % index)\n", + " return self.idx_to_token[index]\n", + " \n", + " def __str__(self):\n", + " return \"\" % len(self.token_to_idx)\n", + "\n", + " def __len__(self):\n", + " return len(self.token_to_idx)\n" + ], + "execution_count": 0, + "outputs": [] + }, + { + "metadata": { + "id": "g5UHjpi3El37", + "colab_type": "code", + "outputId": "cb20aa34-2bd5-4178-b219-d845fdc4968e", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 85 + } + }, + "cell_type": "code", + "source": [ + "# Get word counts\n", + "word_counts = Counter()\n", + "for title in split_df.title:\n", + " for token in title.split(\" \"):\n", + " if token not in string.punctuation:\n", + " word_counts[token] += 1\n", + "\n", + "# Create SequenceVocabulary instance\n", + "title_vocab = SequenceVocabulary()\n", + "for word, word_count in word_counts.items():\n", + " if word_count >= args.cutoff:\n", + " title_vocab.add_token(word)\n", + "print (title_vocab) # __str__\n", + "print (len(title_vocab)) # __len__\n", + "index = title_vocab.lookup_token(\"general\")\n", + "print (index)\n", + "print (title_vocab.lookup_index(index))" + ], + "execution_count": 41, + "outputs": [ + { + "output_type": "stream", + "text": [ + "\n", + "4400\n", + "4\n", + "general\n" + ], + "name": "stdout" + } + ] + }, + { + "metadata": { + "id": "4Dag6H0SFHAG", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "## Vectorizer" + ] + }, + { + "metadata": { + "id": "VQIfxcUuKwzz", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "Something new that we introduce in this Vectorizer is calculating the length of our input sequence. We will use this later on to extract the last relevant hidden state for each input sequence." + ] + }, + { + "metadata": { + "id": "tsNtEnhBEl6s", + "colab_type": "code", + "colab": {} + }, + "cell_type": "code", + "source": [ + "class NewsVectorizer(object):\n", + " def __init__(self, title_vocab, category_vocab):\n", + " self.title_vocab = title_vocab\n", + " self.category_vocab = category_vocab\n", + "\n", + " def vectorize(self, title):\n", + " indices = [self.title_vocab.lookup_token(token) for token in title.split(\" \")]\n", + " indices = [self.title_vocab.begin_seq_index] + indices + \\\n", + " [self.title_vocab.end_seq_index]\n", + " \n", + " # Create vector\n", + " title_length = len(indices)\n", + " vector = np.zeros(title_length, dtype=np.int64)\n", + " vector[:len(indices)] = indices\n", + "\n", + " return vector, title_length\n", + " \n", + " def unvectorize(self, vector):\n", + " tokens = [self.title_vocab.lookup_index(index) for index in vector]\n", + " title = \" \".join(token for token in tokens)\n", + " return title\n", + "\n", + " @classmethod\n", + " def from_dataframe(cls, df, cutoff):\n", + " \n", + " # Create class vocab\n", + " category_vocab = Vocabulary() \n", + " for category in sorted(set(df.category)):\n", + " category_vocab.add_token(category)\n", + "\n", + " # Get word counts\n", + " word_counts = Counter()\n", + " for title in df.title:\n", + " for token in title.split(\" \"):\n", + " word_counts[token] += 1\n", + " \n", + " # Create title vocab\n", + " title_vocab = SequenceVocabulary()\n", + " for word, word_count in word_counts.items():\n", + " if word_count >= cutoff:\n", + " title_vocab.add_token(word)\n", + " \n", + " return cls(title_vocab, category_vocab)\n", + "\n", + " @classmethod\n", + " def from_serializable(cls, contents):\n", + " title_vocab = SequenceVocabulary.from_serializable(contents['title_vocab'])\n", + " category_vocab = Vocabulary.from_serializable(contents['category_vocab'])\n", + " return cls(title_vocab=title_vocab, category_vocab=category_vocab)\n", + " \n", + " def to_serializable(self):\n", + " return {'title_vocab': self.title_vocab.to_serializable(),\n", + " 'category_vocab': self.category_vocab.to_serializable()}" + ], + "execution_count": 0, + "outputs": [] + }, + { + "metadata": { + "id": "JtRRXU53El9Y", + "colab_type": "code", + "outputId": "ba63f1e4-d50e-458c-cb38-da4cc69e5dfa", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 119 + } + }, + "cell_type": "code", + "source": [ + "# Vectorizer instance\n", + "vectorizer = NewsVectorizer.from_dataframe(split_df, cutoff=args.cutoff)\n", + "print (vectorizer.title_vocab)\n", + "print (vectorizer.category_vocab)\n", + "vectorized_title, title_length = vectorizer.vectorize(preprocess_text(\n", + " \"Roger Federer wins the Wimbledon tennis tournament.\"))\n", + "print (np.shape(vectorized_title))\n", + "print (\"title_length:\", title_length)\n", + "print (vectorized_title)\n", + "print (vectorizer.unvectorize(vectorized_title))" + ], + "execution_count": 50, + "outputs": [ + { + "output_type": "stream", + "text": [ + "\n", + "\n", + "(10,)\n", + "title_length: 10\n", + "[ 2 1 4151 1231 25 1 2392 4076 38 3]\n", + " federer wins the tennis tournament . \n" + ], + "name": "stdout" + } + ] + }, + { + "metadata": { + "id": "uk_QvpVfFM0S", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "## Dataset" + ] + }, + { + "metadata": { + "id": "oU7oDdelFMR9", + "colab_type": "code", + "colab": {} + }, + "cell_type": "code", + "source": [ + "from torch.utils.data import Dataset, DataLoader" + ], + "execution_count": 0, + "outputs": [] + }, + { + "metadata": { + "id": "pB7FHmiSFMXA", + "colab_type": "code", + "colab": {} + }, + "cell_type": "code", + "source": [ + "class NewsDataset(Dataset):\n", + " def __init__(self, df, vectorizer):\n", + " self.df = df\n", + " self.vectorizer = vectorizer\n", + "\n", + " # Data splits\n", + " self.train_df = self.df[self.df.split=='train']\n", + " self.train_size = len(self.train_df)\n", + " self.val_df = self.df[self.df.split=='val']\n", + " self.val_size = len(self.val_df)\n", + " self.test_df = self.df[self.df.split=='test']\n", + " self.test_size = len(self.test_df)\n", + " self.lookup_dict = {'train': (self.train_df, self.train_size), \n", + " 'val': (self.val_df, self.val_size),\n", + " 'test': (self.test_df, self.test_size)}\n", + " self.set_split('train')\n", + "\n", + " # Class weights (for imbalances)\n", + " class_counts = df.category.value_counts().to_dict()\n", + " def sort_key(item):\n", + " return self.vectorizer.category_vocab.lookup_token(item[0])\n", + " sorted_counts = sorted(class_counts.items(), key=sort_key)\n", + " frequencies = [count for _, count in sorted_counts]\n", + " self.class_weights = 1.0 / torch.tensor(frequencies, dtype=torch.float32)\n", + "\n", + " @classmethod\n", + " def load_dataset_and_make_vectorizer(cls, split_data_file, cutoff):\n", + " df = pd.read_csv(split_data_file, header=0)\n", + " train_df = df[df.split=='train']\n", + " return cls(df, NewsVectorizer.from_dataframe(train_df, cutoff))\n", + "\n", + " @classmethod\n", + " def load_dataset_and_load_vectorizer(cls, split_data_file, vectorizer_filepath):\n", + " df = pd.read_csv(split_data_file, header=0)\n", + " vectorizer = cls.load_vectorizer_only(vectorizer_filepath)\n", + " return cls(df, vectorizer)\n", + "\n", + " def load_vectorizer_only(vectorizer_filepath):\n", + " with open(vectorizer_filepath) as fp:\n", + " return NewsVectorizer.from_serializable(json.load(fp))\n", + "\n", + " def save_vectorizer(self, vectorizer_filepath):\n", + " with open(vectorizer_filepath, \"w\") as fp:\n", + " json.dump(self.vectorizer.to_serializable(), fp)\n", + "\n", + " def set_split(self, split=\"train\"):\n", + " self.target_split = split\n", + " self.target_df, self.target_size = self.lookup_dict[split]\n", + "\n", + " def __str__(self):\n", + " return \" software firm to cut jobs \n", + "tensor([3.3333e-05, 3.3333e-05, 3.3333e-05, 3.3333e-05])\n" + ], + "name": "stdout" + } + ] + }, + { + "metadata": { + "id": "_IUIqtbvFUAG", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "## Model" + ] + }, + { + "metadata": { + "id": "xJV5WlDiFVVz", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "input → embedding → RNN → FC " + ] + }, + { + "metadata": { + "id": "rZCzdZZ9FMhm", + "colab_type": "code", + "colab": {} + }, + "cell_type": "code", + "source": [ + "import torch.nn as nn\n", + "import torch.nn.functional as F" + ], + "execution_count": 0, + "outputs": [] + }, + { + "metadata": { + "id": "wbWO4lZcIdqZ", + "colab_type": "code", + "colab": {} + }, + "cell_type": "code", + "source": [ + "def gather_last_relevant_hidden(hiddens, x_lengths):\n", + " x_lengths = x_lengths.long().detach().cpu().numpy() - 1\n", + " out = []\n", + " for batch_index, column_index in enumerate(x_lengths):\n", + " out.append(hiddens[batch_index, column_index])\n", + " return torch.stack(out)" + ], + "execution_count": 0, + "outputs": [] + }, + { + "metadata": { + "id": "9TT66Y-UFMcZ", + "colab_type": "code", + "colab": {} + }, + "cell_type": "code", + "source": [ + "class NewsModel(nn.Module):\n", + " def __init__(self, embedding_dim, num_embeddings, rnn_hidden_dim, \n", + " hidden_dim, output_dim, num_layers, bidirectional, dropout_p, \n", + " pretrained_embeddings=None, freeze_embeddings=False, \n", + " padding_idx=0):\n", + " super(NewsModel, self).__init__()\n", + " \n", + " if pretrained_embeddings is None:\n", + " self.embeddings = nn.Embedding(embedding_dim=embedding_dim,\n", + " num_embeddings=num_embeddings,\n", + " padding_idx=padding_idx)\n", + " else:\n", + " pretrained_embeddings = torch.from_numpy(pretrained_embeddings).float()\n", + " self.embeddings = nn.Embedding(embedding_dim=embedding_dim,\n", + " num_embeddings=num_embeddings,\n", + " padding_idx=padding_idx,\n", + " _weight=pretrained_embeddings)\n", + " \n", + " # Conv weights\n", + " self.gru = nn.GRU(input_size=embedding_dim, hidden_size=rnn_hidden_dim, \n", + " num_layers=num_layers, batch_first=True, \n", + " bidirectional=bidirectional)\n", + " \n", + " # FC weights\n", + " self.dropout = nn.Dropout(dropout_p)\n", + " self.fc1 = nn.Linear(rnn_hidden_dim, hidden_dim)\n", + " self.fc2 = nn.Linear(hidden_dim, output_dim)\n", + " \n", + " if freeze_embeddings:\n", + " self.embeddings.weight.requires_grad = False\n", + "\n", + " def forward(self, x_in, x_lengths, apply_softmax=False):\n", + " \n", + " # Embed\n", + " x_in = self.embeddings(x_in)\n", + " \n", + " # Feed into RNN\n", + " out, h_n = self.gru(x_in)\n", + " \n", + " # Gather the last relevant hidden state\n", + " out = gather_last_relevant_hidden(out, x_lengths)\n", + "\n", + " # FC layers\n", + " z = self.dropout(out)\n", + " z = self.fc1(z)\n", + " z = self.dropout(z)\n", + " y_pred = self.fc2(z)\n", + "\n", + " if apply_softmax:\n", + " y_pred = F.softmax(y_pred, dim=1)\n", + " return y_pred" + ], + "execution_count": 0, + "outputs": [] + }, + { + "metadata": { + "id": "jHPYCPd7Fl3M", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "## Training" + ] + }, + { + "metadata": { + "id": "D3seBMA7FlcC", + "colab_type": "code", + "colab": {} + }, + "cell_type": "code", + "source": [ + "import torch.optim as optim" + ], + "execution_count": 0, + "outputs": [] + }, + { + "metadata": { + "id": "HnRKWLekFlnM", + "colab_type": "code", + "colab": {} + }, + "cell_type": "code", + "source": [ + "class Trainer(object):\n", + " def __init__(self, dataset, model, model_state_file, save_dir, device, shuffle, \n", + " num_epochs, batch_size, learning_rate, early_stopping_criteria):\n", + " self.dataset = dataset\n", + " self.class_weights = dataset.class_weights.to(device)\n", + " self.model = model.to(device)\n", + " self.save_dir = save_dir\n", + " self.device = device\n", + " self.shuffle = shuffle\n", + " self.num_epochs = num_epochs\n", + " self.batch_size = batch_size\n", + " self.loss_func = nn.CrossEntropyLoss(self.class_weights)\n", + " self.optimizer = optim.Adam(self.model.parameters(), lr=learning_rate)\n", + " self.scheduler = optim.lr_scheduler.ReduceLROnPlateau(\n", + " optimizer=self.optimizer, mode='min', factor=0.5, patience=1)\n", + " self.train_state = {\n", + " 'stop_early': False, \n", + " 'early_stopping_step': 0,\n", + " 'early_stopping_best_val': 1e8,\n", + " 'early_stopping_criteria': early_stopping_criteria,\n", + " 'learning_rate': learning_rate,\n", + " 'epoch_index': 0,\n", + " 'train_loss': [],\n", + " 'train_acc': [],\n", + " 'val_loss': [],\n", + " 'val_acc': [],\n", + " 'test_loss': -1,\n", + " 'test_acc': -1,\n", + " 'model_filename': model_state_file}\n", + " \n", + " def update_train_state(self):\n", + "\n", + " # Verbose\n", + " print (\"[EPOCH]: {0:02d} | [LR]: {1} | [TRAIN LOSS]: {2:.2f} | [TRAIN ACC]: {3:.1f}% | [VAL LOSS]: {4:.2f} | [VAL ACC]: {5:.1f}%\".format(\n", + " self.train_state['epoch_index'], self.train_state['learning_rate'], \n", + " self.train_state['train_loss'][-1], self.train_state['train_acc'][-1], \n", + " self.train_state['val_loss'][-1], self.train_state['val_acc'][-1]))\n", + "\n", + " # Save one model at least\n", + " if self.train_state['epoch_index'] == 0:\n", + " torch.save(self.model.state_dict(), self.train_state['model_filename'])\n", + " self.train_state['stop_early'] = False\n", + "\n", + " # Save model if performance improved\n", + " elif self.train_state['epoch_index'] >= 1:\n", + " loss_tm1, loss_t = self.train_state['val_loss'][-2:]\n", + "\n", + " # If loss worsened\n", + " if loss_t >= self.train_state['early_stopping_best_val']:\n", + " # Update step\n", + " self.train_state['early_stopping_step'] += 1\n", + "\n", + " # Loss decreased\n", + " else:\n", + " # Save the best model\n", + " if loss_t < self.train_state['early_stopping_best_val']:\n", + " torch.save(self.model.state_dict(), self.train_state['model_filename'])\n", + "\n", + " # Reset early stopping step\n", + " self.train_state['early_stopping_step'] = 0\n", + "\n", + " # Stop early ?\n", + " self.train_state['stop_early'] = self.train_state['early_stopping_step'] \\\n", + " >= self.train_state['early_stopping_criteria']\n", + " return self.train_state\n", + " \n", + " def compute_accuracy(self, y_pred, y_target):\n", + " _, y_pred_indices = y_pred.max(dim=1)\n", + " n_correct = torch.eq(y_pred_indices, y_target).sum().item()\n", + " return n_correct / len(y_pred_indices) * 100\n", + " \n", + " def pad_seq(self, seq, length):\n", + " vector = np.zeros(length, dtype=np.int64)\n", + " vector[:len(seq)] = seq\n", + " vector[len(seq):] = self.dataset.vectorizer.title_vocab.mask_index\n", + " return vector\n", + " \n", + " def collate_fn(self, batch):\n", + " \n", + " # Make a deep copy\n", + " batch_copy = copy.deepcopy(batch)\n", + " processed_batch = {\"title\": [], \"title_length\": [], \"category\": []}\n", + " \n", + " # Get max sequence length\n", + " get_length = lambda sample: len(sample[\"title\"])\n", + " max_seq_length = max(map(get_length, batch))\n", + " \n", + " # Pad\n", + " for i, sample in enumerate(batch_copy):\n", + " padded_seq = self.pad_seq(sample[\"title\"], max_seq_length)\n", + " processed_batch[\"title\"].append(padded_seq)\n", + " processed_batch[\"title_length\"].append(sample[\"title_length\"])\n", + " processed_batch[\"category\"].append(sample[\"category\"])\n", + " \n", + " # Convert to appropriate tensor types\n", + " processed_batch[\"title\"] = torch.LongTensor(\n", + " processed_batch[\"title\"])\n", + " processed_batch[\"title_length\"] = torch.LongTensor(\n", + " processed_batch[\"title_length\"])\n", + " processed_batch[\"category\"] = torch.LongTensor(\n", + " processed_batch[\"category\"])\n", + " \n", + " return processed_batch \n", + " \n", + " def run_train_loop(self):\n", + " for epoch_index in range(self.num_epochs):\n", + " self.train_state['epoch_index'] = epoch_index\n", + " \n", + " # Iterate over train dataset\n", + "\n", + " # initialize batch generator, set loss and acc to 0, set train mode on\n", + " self.dataset.set_split('train')\n", + " batch_generator = self.dataset.generate_batches(\n", + " batch_size=self.batch_size, collate_fn=self.collate_fn, \n", + " shuffle=self.shuffle, device=self.device)\n", + " running_loss = 0.0\n", + " running_acc = 0.0\n", + " self.model.train()\n", + "\n", + " for batch_index, batch_dict in enumerate(batch_generator):\n", + " # zero the gradients\n", + " self.optimizer.zero_grad()\n", + "\n", + " # compute the output\n", + " y_pred = self.model(batch_dict['title'], batch_dict['title_length'])\n", + "\n", + " # compute the loss\n", + " loss = self.loss_func(y_pred, batch_dict['category'])\n", + " loss_t = loss.item()\n", + " running_loss += (loss_t - running_loss) / (batch_index + 1)\n", + "\n", + " # compute gradients using loss\n", + " loss.backward()\n", + "\n", + " # use optimizer to take a gradient step\n", + " self.optimizer.step()\n", + " \n", + " # compute the accuracy\n", + " acc_t = self.compute_accuracy(y_pred, batch_dict['category'])\n", + " running_acc += (acc_t - running_acc) / (batch_index + 1)\n", + "\n", + " self.train_state['train_loss'].append(running_loss)\n", + " self.train_state['train_acc'].append(running_acc)\n", + "\n", + " # Iterate over val dataset\n", + "\n", + " # # initialize batch generator, set loss and acc to 0; set eval mode on\n", + " self.dataset.set_split('val')\n", + " batch_generator = self.dataset.generate_batches(\n", + " batch_size=self.batch_size, collate_fn=self.collate_fn, \n", + " shuffle=self.shuffle, device=self.device)\n", + " running_loss = 0.\n", + " running_acc = 0.\n", + " self.model.eval()\n", + "\n", + " for batch_index, batch_dict in enumerate(batch_generator):\n", + "\n", + " # compute the output\n", + " y_pred = self.model(batch_dict['title'], batch_dict['title_length'])\n", + "\n", + " # compute the loss\n", + " loss = self.loss_func(y_pred, batch_dict['category'])\n", + " loss_t = loss.to(\"cpu\").item()\n", + " running_loss += (loss_t - running_loss) / (batch_index + 1)\n", + "\n", + " # compute the accuracy\n", + " acc_t = self.compute_accuracy(y_pred, batch_dict['category'])\n", + " running_acc += (acc_t - running_acc) / (batch_index + 1)\n", + "\n", + " self.train_state['val_loss'].append(running_loss)\n", + " self.train_state['val_acc'].append(running_acc)\n", + "\n", + " self.train_state = self.update_train_state()\n", + " self.scheduler.step(self.train_state['val_loss'][-1])\n", + " if self.train_state['stop_early']:\n", + " break\n", + " \n", + " def run_test_loop(self):\n", + " # initialize batch generator, set loss and acc to 0; set eval mode on\n", + " self.dataset.set_split('test')\n", + " batch_generator = self.dataset.generate_batches(\n", + " batch_size=self.batch_size, collate_fn=self.collate_fn, \n", + " shuffle=self.shuffle, device=self.device)\n", + " running_loss = 0.0\n", + " running_acc = 0.0\n", + " self.model.eval()\n", + "\n", + " for batch_index, batch_dict in enumerate(batch_generator):\n", + " # compute the output\n", + " y_pred = self.model(batch_dict['title'], batch_dict['title_length'])\n", + "\n", + " # compute the loss\n", + " loss = self.loss_func(y_pred, batch_dict['category'])\n", + " loss_t = loss.item()\n", + " running_loss += (loss_t - running_loss) / (batch_index + 1)\n", + "\n", + " # compute the accuracy\n", + " acc_t = self.compute_accuracy(y_pred, batch_dict['category'])\n", + " running_acc += (acc_t - running_acc) / (batch_index + 1)\n", + "\n", + " self.train_state['test_loss'] = running_loss\n", + " self.train_state['test_acc'] = running_acc\n", + " \n", + " def plot_performance(self):\n", + " # Figure size\n", + " plt.figure(figsize=(15,5))\n", + "\n", + " # Plot Loss\n", + " plt.subplot(1, 2, 1)\n", + " plt.title(\"Loss\")\n", + " plt.plot(trainer.train_state[\"train_loss\"], label=\"train\")\n", + " plt.plot(trainer.train_state[\"val_loss\"], label=\"val\")\n", + " plt.legend(loc='upper right')\n", + "\n", + " # Plot Accuracy\n", + " plt.subplot(1, 2, 2)\n", + " plt.title(\"Accuracy\")\n", + " plt.plot(trainer.train_state[\"train_acc\"], label=\"train\")\n", + " plt.plot(trainer.train_state[\"val_acc\"], label=\"val\")\n", + " plt.legend(loc='lower right')\n", + "\n", + " # Save figure\n", + " plt.savefig(os.path.join(self.save_dir, \"performance.png\"))\n", + "\n", + " # Show plots\n", + " plt.show()\n", + " \n", + " def save_train_state(self):\n", + " with open(os.path.join(self.save_dir, \"train_state.json\"), \"w\") as fp:\n", + " json.dump(self.train_state, fp)" + ], + "execution_count": 0, + "outputs": [] + }, + { + "metadata": { + "id": "ICkiOaGtFlk-", + "colab_type": "code", + "outputId": "57f7f143-7899-407a-acbd-17f767eb56c3", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 136 + } + }, + "cell_type": "code", + "source": [ + "# Initialization\n", + "dataset = NewsDataset.load_dataset_and_make_vectorizer(args.split_data_file,\n", + " args.cutoff)\n", + "dataset.save_vectorizer(args.vectorizer_file)\n", + "vectorizer = dataset.vectorizer\n", + "model = NewsModel(embedding_dim=args.embedding_dim, \n", + " num_embeddings=len(vectorizer.title_vocab), \n", + " rnn_hidden_dim=args.rnn_hidden_dim,\n", + " hidden_dim=args.hidden_dim,\n", + " output_dim=len(vectorizer.category_vocab),\n", + " num_layers=args.num_layers,\n", + " bidirectional=args.bidirectional,\n", + " dropout_p=args.dropout_p, \n", + " pretrained_embeddings=None, \n", + " padding_idx=vectorizer.title_vocab.mask_index)\n", + "print (model.named_modules)" + ], + "execution_count": 88, + "outputs": [ + { + "output_type": "stream", + "text": [ + "\n" + ], + "name": "stdout" + } + ] + }, + { + "metadata": { + "id": "tuaRZ4DiFlh1", + "colab_type": "code", + "outputId": "fba7ac04-7e1d-4372-b358-7340a013960d", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 102 + } + }, + "cell_type": "code", + "source": [ + "# Train\n", + "trainer = Trainer(dataset=dataset, model=model, \n", + " model_state_file=args.model_state_file, \n", + " save_dir=args.save_dir, device=args.device,\n", + " shuffle=args.shuffle, num_epochs=args.num_epochs, \n", + " batch_size=args.batch_size, learning_rate=args.learning_rate, \n", + " early_stopping_criteria=args.early_stopping_criteria)\n", + "trainer.run_train_loop()" + ], + "execution_count": 89, + "outputs": [ + { + "output_type": "stream", + "text": [ + "[EPOCH]: 00 | [LR]: 0.001 | [TRAIN LOSS]: 0.75 | [TRAIN ACC]: 70.7% | [VAL LOSS]: 0.54 | [VAL ACC]: 80.5%\n", + "[EPOCH]: 01 | [LR]: 0.001 | [TRAIN LOSS]: 0.48 | [TRAIN ACC]: 82.7% | [VAL LOSS]: 0.49 | [VAL ACC]: 82.3%\n", + "[EPOCH]: 02 | [LR]: 0.001 | [TRAIN LOSS]: 0.41 | [TRAIN ACC]: 85.0% | [VAL LOSS]: 0.47 | [VAL ACC]: 83.1%\n", + "[EPOCH]: 03 | [LR]: 0.001 | [TRAIN LOSS]: 0.37 | [TRAIN ACC]: 86.6% | [VAL LOSS]: 0.47 | [VAL ACC]: 83.3%\n", + "[EPOCH]: 04 | [LR]: 0.001 | [TRAIN LOSS]: 0.33 | [TRAIN ACC]: 88.2% | [VAL LOSS]: 0.49 | [VAL ACC]: 83.0%\n" + ], + "name": "stdout" + } + ] + }, + { + "metadata": { + "id": "mzRJIz88Flfe", + "colab_type": "code", + "outputId": "a7ac8786-01ea-4421-e70c-d79c22c7ed4a", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 335 + } + }, + "cell_type": "code", + "source": [ + "# Plot performance\n", + "trainer.plot_performance()" + ], + "execution_count": 73, + "outputs": [ + { + "output_type": "display_data", + "data": { + "image/png": "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\n", + "text/plain": [ + "" + ] + }, + "metadata": { + "tags": [] + } + } + ] + }, + { + "metadata": { + "id": "4EmFhiX-FMaV", + "colab_type": "code", + "outputId": "c689c3e6-972b-4499-81b6-8812a25076d1", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 51 + } + }, + "cell_type": "code", + "source": [ + "# Test performance\n", + "trainer.run_test_loop()\n", + "print(\"Test loss: {0:.2f}\".format(trainer.train_state['test_loss']))\n", + "print(\"Test Accuracy: {0:.1f}%\".format(trainer.train_state['test_acc']))" + ], + "execution_count": 74, + "outputs": [ + { + "output_type": "stream", + "text": [ + "Test loss: 0.49\n", + "Test Accuracy: 82.9%\n" + ], + "name": "stdout" + } + ] + }, + { + "metadata": { + "id": "zVU1zakYFMVF", + "colab_type": "code", + "colab": {} + }, + "cell_type": "code", + "source": [ + "# Save all results\n", + "trainer.save_train_state()" + ], + "execution_count": 0, + "outputs": [] + }, + { + "metadata": { + "id": "qLoKfjSpFw7t", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "## Inference" + ] + }, + { + "metadata": { + "id": "ANrPcS7Hp_CP", + "colab_type": "code", + "colab": {} + }, + "cell_type": "code", + "source": [ + "class Inference(object):\n", + " def __init__(self, model, vectorizer):\n", + " self.model = model\n", + " self.vectorizer = vectorizer\n", + " \n", + " def predict_category(self, title):\n", + " # Vectorize\n", + " vectorized_title, title_length = self.vectorizer.vectorize(title)\n", + " vectorized_title = torch.tensor(vectorized_title).unsqueeze(0)\n", + " title_length = torch.tensor([title_length]).long()\n", + " \n", + " # Forward pass\n", + " self.model.eval()\n", + " y_pred = self.model(x_in=vectorized_title, x_lengths=title_length, \n", + " apply_softmax=True)\n", + "\n", + " # Top category\n", + " y_prob, indices = y_pred.max(dim=1)\n", + " index = indices.item()\n", + "\n", + " # Predicted category\n", + " category = vectorizer.category_vocab.lookup_index(index)\n", + " probability = y_prob.item()\n", + " return {'category': category, 'probability': probability}\n", + " \n", + " def predict_top_k(self, title, k):\n", + " # Vectorize\n", + " vectorized_title, title_length = self.vectorizer.vectorize(title)\n", + " vectorized_title = torch.tensor(vectorized_title).unsqueeze(0)\n", + " title_length = torch.tensor([title_length]).long()\n", + " \n", + " # Forward pass\n", + " self.model.eval()\n", + " y_pred = self.model(x_in=vectorized_title, x_lengths=title_length, \n", + " apply_softmax=True)\n", + " \n", + " # Top k categories\n", + " y_prob, indices = torch.topk(y_pred, k=k)\n", + " probabilities = y_prob.detach().numpy()[0]\n", + " indices = indices.detach().numpy()[0]\n", + "\n", + " # Results\n", + " results = []\n", + " for probability, index in zip(probabilities, indices):\n", + " category = self.vectorizer.category_vocab.lookup_index(index)\n", + " results.append({'category': category, 'probability': probability})\n", + "\n", + " return results" + ], + "execution_count": 0, + "outputs": [] + }, + { + "metadata": { + "id": "W6wr68o2p_Eh", + "colab_type": "code", + "outputId": "3e94c736-3ad3-4c70-b24c-591edbe069ad", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 136 + } + }, + "cell_type": "code", + "source": [ + "# Load the model\n", + "dataset = NewsDataset.load_dataset_and_load_vectorizer(\n", + " args.split_data_file, args.vectorizer_file)\n", + "vectorizer = dataset.vectorizer\n", + "model = NewsModel(embedding_dim=args.embedding_dim, \n", + " num_embeddings=len(vectorizer.title_vocab), \n", + " rnn_hidden_dim=args.rnn_hidden_dim,\n", + " hidden_dim=args.hidden_dim,\n", + " output_dim=len(vectorizer.category_vocab),\n", + " num_layers=args.num_layers,\n", + " bidirectional=args.bidirectional,\n", + " dropout_p=args.dropout_p, \n", + " pretrained_embeddings=None, \n", + " padding_idx=vectorizer.title_vocab.mask_index)\n", + "model.load_state_dict(torch.load(args.model_state_file))\n", + "model = model.to(\"cpu\")\n", + "print (model.named_modules)" + ], + "execution_count": 77, + "outputs": [ + { + "output_type": "stream", + "text": [ + "\n" + ], + "name": "stdout" + } + ] + }, + { + "metadata": { + "id": "JPKgHxsfN954", + "colab_type": "code", + "outputId": "c9f21a76-8307-4737-c785-01f1004891b6", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 51 + } + }, + "cell_type": "code", + "source": [ + "# Inference\n", + "inference = Inference(model=model, vectorizer=vectorizer)\n", + "title = input(\"Enter a title to classify: \")\n", + "prediction = inference.predict_category(preprocess_text(title))\n", + "print(\"{} → {} (p={:0.2f})\".format(title, prediction['category'], \n", + " prediction['probability']))" + ], + "execution_count": 80, + "outputs": [ + { + "output_type": "stream", + "text": [ + "Enter a title to classify: President Obama signed the petition during the White House dinner.\n", + "President Obama signed the petition during the White House dinner. → World (p=0.62)\n" + ], + "name": "stdout" + } + ] + }, + { + "metadata": { + "id": "JRdz4wzuQR4N", + "colab_type": "code", + "outputId": "9a349bf0-16ba-402d-9133-11ce27e1ec59", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 102 + } + }, + "cell_type": "code", + "source": [ + "# Top-k inference\n", + "top_k = inference.predict_top_k(preprocess_text(title), k=len(vectorizer.category_vocab))\n", + "print (\"{}\".format(title))\n", + "for result in top_k:\n", + " print (\"{} (p={:0.2f})\".format(result['category'], \n", + " result['probability']))" + ], + "execution_count": 82, + "outputs": [ + { + "output_type": "stream", + "text": [ + "President Obama signed the petition during the White House dinner.\n", + "World (p=0.62)\n", + "Sci/Tech (p=0.17)\n", + "Business (p=0.15)\n", + "Sports (p=0.06)\n" + ], + "name": "stdout" + } + ] + }, + { + "metadata": { + "id": "noPtpaHZ6NAW", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "# Layer normalization" + ] + }, + { + "metadata": { + "id": "t3bu5cEP6PSb", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "Recall from our [CNN notebook](https://colab.research.google.com/github/GokuMohandas/practicalAI/blob/master/notebooks/11_Convolutional_Neural_Networks.ipynb) that we used batch normalization to deal with internal covariant shift. Our activations will experience the same issues with RNNs but we will use a technique known as [layer normalization](https://arxiv.org/abs/1607.06450) (layernorm) to maintain zero mean unit variance on the activations. \n", + "\n", + "With layernorm it's a bit different from batchnorm. We compute the mean and var for every single sample (instead of each hidden dim) for each layer independently and then do the operations on the activations before they go through the nonlinearities. PyTorch's [LayerNorm](https://pytorch.org/docs/stable/nn.html#torch.nn.LayerNorm) class abstracts all of this for us when we feed in inputs to the layer." + ] + }, + { + "metadata": { + "id": "-G-mVmUL61Fk", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "$ LN = \\frac{a - \\mu_{L}}{\\sqrt{\\sigma^2_{L} + \\epsilon}} * \\gamma + \\beta $\n", + "\n", + "where:\n", + "* $a$ = activation | $\\in \\mathbb{R}^{NXH}$ ($N$ is the number of samples, $H$ is the hidden dim)\n", + "* $ \\mu_{L}$ = mean of input| $\\in \\mathbb{R}^{NX1}$\n", + "* $\\sigma^2_{L}$ = variance of input | $\\in \\mathbb{R}^{NX1}$\n", + "* $epsilon$ = noise\n", + "* $\\gamma$ = scale parameter (learned parameter)\n", + "* $\\beta$ = shift parameter (learned parameter)" + ] + }, + { + "metadata": { + "id": "P0e9TnQ581-1", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "" + ] + }, + { + "metadata": { + "id": "oAhAHcgZBMFe", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "The most useful location to apply layernorm will be inside the RNN on the activations before the non-linearities. However, this is a bit involved and though PyTorch has a [LayerNorm](https://pytorch.org/docs/stable/nn.html#torch.nn.LayerNorm) class, they do not have an RNN that has built in layernorm yet. You could implement the RNN yourself and manually add layernorm by following a similar setup like below.\n", + "\n", + "```python\n", + "# Layernorm\n", + "for t in range(seq_size):\n", + " # Normalize over hidden dim\n", + " layernorm = nn.LayerNorm(args.hidden_dim)\n", + " # Activating the module\n", + " a = layernorm(x)\n", + "```" + ] + }, + { + "metadata": { + "id": "1YHneO3SStOp", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "# TODO" + ] + }, + { + "metadata": { + "id": "gGHaKTe1SuEk", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "- interpretability with task to see which words were most influential" + ] + } + ] +} \ No newline at end of file diff --git a/notebooks/14_Advanced_RNNs.ipynb b/notebooks/14_Advanced_RNNs.ipynb new file mode 100644 index 0000000..cbc9522 --- /dev/null +++ b/notebooks/14_Advanced_RNNs.ipynb @@ -0,0 +1,2916 @@ +{ + "nbformat": 4, + "nbformat_minor": 0, + "metadata": { + "colab": { + "name": "14_Advanced_RNNs", + "version": "0.3.2", + "provenance": [], + "collapsed_sections": [], + "toc_visible": true + }, + "kernelspec": { + "name": "python3", + "display_name": "Python 3" + }, + "accelerator": "GPU" + }, + "cells": [ + { + "metadata": { + "id": "bOChJSNXtC9g", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "# Advanced RNNs" + ] + }, + { + "metadata": { + "id": "OLIxEDq6VhvZ", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "\n", + "\n", + "In this notebook we're going to cover some advanced topics related to RNNs.\n", + "\n", + "1. Conditioned hidden state\n", + "2. Char-level embeddings\n", + "3. Encoder and decoder\n", + "4. Attentional mechanisms\n", + "5. Implementation\n", + "\n", + "\n" + ] + }, + { + "metadata": { + "id": "41r7MWJnY0m8", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "# Set up" + ] + }, + { + "metadata": { + "id": "EJDhjHCHY0_a", + "colab_type": "code", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 34 + }, + "outputId": "beb1c764-e47f-41a6-f8cf-e4150ee3befd" + }, + "cell_type": "code", + "source": [ + "# Load PyTorch library\n", + "!pip3 install torch" + ], + "execution_count": 1, + "outputs": [ + { + "output_type": "stream", + "text": [ + "Requirement already satisfied: torch in /usr/local/lib/python3.6/dist-packages (1.0.0)\n" + ], + "name": "stdout" + } + ] + }, + { + "metadata": { + "id": "p0FbOd6IZmzX", + "colab_type": "code", + "colab": {} + }, + "cell_type": "code", + "source": [ + "import os\n", + "from argparse import Namespace\n", + "import collections\n", + "import copy\n", + "import json\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import pandas as pd\n", + "import re\n", + "import torch" + ], + "execution_count": 0, + "outputs": [] + }, + { + "metadata": { + "id": "bOsqAo4XZpXQ", + "colab_type": "code", + "colab": {} + }, + "cell_type": "code", + "source": [ + "# Set Numpy and PyTorch seeds\n", + "def set_seeds(seed, cuda):\n", + " np.random.seed(seed)\n", + " torch.manual_seed(seed)\n", + " if cuda:\n", + " torch.cuda.manual_seed_all(seed)\n", + " \n", + "# Creating directories\n", + "def create_dirs(dirpath):\n", + " if not os.path.exists(dirpath):\n", + " os.makedirs(dirpath)" + ], + "execution_count": 0, + "outputs": [] + }, + { + "metadata": { + "id": "QHfvEzQ9ZweF", + "colab_type": "code", + "outputId": "a69944ff-021d-4d04-e920-cfc49112a34c", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 34 + } + }, + "cell_type": "code", + "source": [ + "# Arguments\n", + "args = Namespace(\n", + " seed=1234,\n", + " cuda=True,\n", + " batch_size=4,\n", + " condition_vocab_size=3, # vocabulary for condition possibilities\n", + " embedding_dim=100,\n", + " rnn_hidden_dim=100,\n", + " hidden_dim=100,\n", + " num_layers=1,\n", + " bidirectional=False,\n", + ")\n", + "\n", + "# Set seeds\n", + "set_seeds(seed=args.seed, cuda=args.cuda)\n", + "\n", + "# Check CUDA\n", + "if not torch.cuda.is_available():\n", + " args.cuda = False\n", + "args.device = torch.device(\"cuda\" if args.cuda else \"cpu\")\n", + "print(\"Using CUDA: {}\".format(args.cuda))" + ], + "execution_count": 5, + "outputs": [ + { + "output_type": "stream", + "text": [ + "Using CUDA: True\n" + ], + "name": "stdout" + } + ] + }, + { + "metadata": { + "id": "VoMq0eFRvugb", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "# Conditioned RNNs" + ] + }, + { + "metadata": { + "id": "ZUsj7HjBp69f", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "Conditioning an RNN is to add extra information that will be helpful towards a prediction. We can encode (embed it) this information and feed it along with the sequential input into our model. For example, suppose in our document classificaiton example in the previous notebook, we knew the publisher of each news article (NYTimes, ESPN, etc.). We could have encoded that information to help with the prediction. There are several different ways of creating a conditioned RNN.\n", + "\n", + "**Note**: If the conditioning information is novel for each input in the sequence, just concatenate it along with each time step's input." + ] + }, + { + "metadata": { + "id": "Kc8H9JySmtLa", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "1. Make the initial hidden state the encoded information instead of using the initial zerod hidden state. Make sure that the size of the encoded information is the same as the hidden state for the RNN.\n" + ] + }, + { + "metadata": { + "id": "pKlb9SjfpbED", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "" + ] + }, + { + "metadata": { + "id": "jbrlQHx2x8Aa", + "colab_type": "code", + "colab": {} + }, + "cell_type": "code", + "source": [ + "import torch.nn as nn\n", + "import torch.nn.functional as F" + ], + "execution_count": 0, + "outputs": [] + }, + { + "metadata": { + "id": "cFoiV-fqmvRo", + "colab_type": "code", + "outputId": "9843f756-8d71-4686-b479-b521df9b6f3c", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 34 + } + }, + "cell_type": "code", + "source": [ + "# Condition\n", + "condition = torch.LongTensor([0, 2, 1, 2]) # batch size of 4 with a vocab size of 3\n", + "condition_embeddings = nn.Embedding(\n", + " embedding_dim=args.embedding_dim, # should be same as RNN hidden dim\n", + " num_embeddings=args.condition_vocab_size) # of unique conditions\n", + "\n", + "# Initialize hidden state\n", + "num_directions = 1\n", + "if args.bidirectional:\n", + " num_directions = 2\n", + " \n", + "# If using multiple layers and directions, the hidden state needs to match that size\n", + "hidden_t = condition_embeddings(condition).unsqueeze(0).repeat(\n", + " args.num_layers * num_directions, 1, 1).to(args.device) # initial state to RNN\n", + "print (hidden_t.size())\n", + "\n", + "# Feed into RNN\n", + "# y_out, _ = self.rnn(x_embedded, hidden_t)\n" + ], + "execution_count": 7, + "outputs": [ + { + "output_type": "stream", + "text": [ + "torch.Size([1, 4, 100])\n" + ], + "name": "stdout" + } + ] + }, + { + "metadata": { + "id": "REgyaMDgmtHw", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "2. Concatenate the encoded information with the hidden state at each time step. Do not replace the hidden state because the RNN needs that to learn. " + ] + }, + { + "metadata": { + "id": "yUIg5o-dpiZF", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "" + ] + }, + { + "metadata": { + "id": "eQ-h28o-pi4X", + "colab_type": "code", + "outputId": "4143190d-c452-48cc-cc96-1a2f0f7fc5ee", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 34 + } + }, + "cell_type": "code", + "source": [ + "# Initialize hidden state\n", + "hidden_t = torch.zeros((args.num_layers * num_directions, args.batch_size, args.rnn_hidden_dim))\n", + "print (hidden_t.size())" + ], + "execution_count": 8, + "outputs": [ + { + "output_type": "stream", + "text": [ + "torch.Size([1, 4, 100])\n" + ], + "name": "stdout" + } + ] + }, + { + "metadata": { + "id": "2Z6hYSIdqBQ4", + "colab_type": "code", + "colab": {} + }, + "cell_type": "code", + "source": [ + "def concat_condition(condition_embeddings, condition, hidden_t, num_layers, num_directions):\n", + " condition_t = condition_embeddings(condition).unsqueeze(0).repeat(\n", + " num_layers * num_directions, 1, 1)\n", + " hidden_t = torch.cat([hidden_t, condition_t], 2)\n", + " return hidden_t" + ], + "execution_count": 0, + "outputs": [] + }, + { + "metadata": { + "id": "Tjyzq_s5pixL", + "colab_type": "code", + "outputId": "f4f62742-044e-46ef-cc46-fc21a3c52c78", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 34 + } + }, + "cell_type": "code", + "source": [ + "# Loop through the inputs time steps\n", + "hiddens = []\n", + "seq_size = 1\n", + "for t in range(seq_size):\n", + " hidden_t = concat_condition(condition_embeddings, condition, hidden_t, \n", + " args.num_layers, num_directions).to(args.device)\n", + " print (hidden_t.size())\n", + " \n", + " # Feed into RNN\n", + " # hidden_t = rnn_cell(x_in[t], hidden_t)\n", + " ..." + ], + "execution_count": 10, + "outputs": [ + { + "output_type": "stream", + "text": [ + "torch.Size([1, 4, 200])\n" + ], + "name": "stdout" + } + ] + }, + { + "metadata": { + "id": "A-0_81jMXg_J", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "# Char-level embeddings" + ] + }, + { + "metadata": { + "id": "w0yUKKpq3pu_", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "Our conv operations will have inputs that are words in a sentence represented at the character level| $\\in \\mathbb{R}^{NXSXWXE}$ and outputs are embeddings for each word (based on convlutions applied at the character level.) \n", + "\n", + "**Word embeddings**: capture the temporal correlations among\n", + "adjacent tokens so that similar words have similar representations. Ex. \"New Jersey\" is close to \"NJ\" is close to \"Garden State\", etc.\n", + "\n", + "**Char embeddings**: create representations that map words at a character level. Ex. \"toy\" and \"toys\" will be close to each other." + ] + }, + { + "metadata": { + "id": "-SZgVuwebm_4", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "" + ] + }, + { + "metadata": { + "id": "QOdIvz0G3O8C", + "colab_type": "code", + "colab": {} + }, + "cell_type": "code", + "source": [ + "# Arguments\n", + "args = Namespace(\n", + " seed=1234,\n", + " cuda=False,\n", + " shuffle=True,\n", + " batch_size=64,\n", + " vocab_size=20, # vocabulary\n", + " seq_size=10, # max length of each sentence\n", + " word_size=15, # max length of each word\n", + " embedding_dim=100,\n", + " num_filters=100, # filters per size\n", + ")" + ], + "execution_count": 0, + "outputs": [] + }, + { + "metadata": { + "id": "raztXIeYXYJT", + "colab_type": "code", + "colab": {} + }, + "cell_type": "code", + "source": [ + "class Model(nn.Module):\n", + " def __init__(self, embedding_dim, num_embeddings, num_input_channels, \n", + " num_output_channels, padding_idx):\n", + " super(Model, self).__init__()\n", + " \n", + " # Char-level embedding\n", + " self.embeddings = nn.Embedding(embedding_dim=embedding_dim,\n", + " num_embeddings=num_embeddings,\n", + " padding_idx=padding_idx)\n", + " \n", + " # Conv weights\n", + " self.conv = nn.ModuleList([nn.Conv1d(num_input_channels, num_output_channels, \n", + " kernel_size=f) for f in [2,3,4]])\n", + "\n", + " def forward(self, x, channel_first=False, apply_softmax=False):\n", + " \n", + " # x: (N, seq_len, word_len)\n", + " input_shape = x.size()\n", + " batch_size, seq_len, word_len = input_shape\n", + " x = x.view(-1, word_len) # (N*seq_len, word_len)\n", + " \n", + " # Embedding\n", + " x = self.embeddings(x) # (N*seq_len, word_len, embedding_dim)\n", + " \n", + " # Rearrange input so num_input_channels is in dim 1 (N, embedding_dim, word_len)\n", + " if not channel_first:\n", + " x = x.transpose(1, 2)\n", + " \n", + " # Convolution\n", + " z = [F.relu(conv(x)) for conv in self.conv]\n", + " \n", + " # Pooling\n", + " z = [F.max_pool1d(zz, zz.size(2)).squeeze(2) for zz in z] \n", + " z = [zz.view(batch_size, seq_len, -1) for zz in z] # (N, seq_len, embedding_dim)\n", + " \n", + " # Concat to get char-level embeddings\n", + " z = torch.cat(z, 2) # join conv outputs\n", + " \n", + " return z" + ], + "execution_count": 0, + "outputs": [] + }, + { + "metadata": { + "id": "MzHVs8Xe0Zph", + "colab_type": "code", + "outputId": "ff91c1ac-5bc4-446c-9047-8b4b58570e13", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 34 + } + }, + "cell_type": "code", + "source": [ + "# Input\n", + "input_size = (args.batch_size, args.seq_size, args.word_size)\n", + "x_in = torch.randint(low=0, high=args.vocab_size, size=input_size).long()\n", + "print (x_in.size())" + ], + "execution_count": 13, + "outputs": [ + { + "output_type": "stream", + "text": [ + "torch.Size([64, 10, 15])\n" + ], + "name": "stdout" + } + ] + }, + { + "metadata": { + "id": "0B_Xscby2PMQ", + "colab_type": "code", + "outputId": "05b0c3ac-429f-47aa-9526-718e55dfc897", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 153 + } + }, + "cell_type": "code", + "source": [ + "# Initial char-level embedding model\n", + "model = Model(embedding_dim=args.embedding_dim, \n", + " num_embeddings=args.vocab_size, \n", + " num_input_channels=args.embedding_dim, \n", + " num_output_channels=args.num_filters,\n", + " padding_idx=0)\n", + "print (model.named_modules)" + ], + "execution_count": 14, + "outputs": [ + { + "output_type": "stream", + "text": [ + "\n" + ], + "name": "stdout" + } + ] + }, + { + "metadata": { + "id": "8DIgeEZFXYR2", + "colab_type": "code", + "outputId": "ffdbfabf-5f60-4045-be84-23dfb65fd424", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 34 + } + }, + "cell_type": "code", + "source": [ + "# Forward pass to get char-level embeddings\n", + "z = model(x_in)\n", + "print (z.size())" + ], + "execution_count": 15, + "outputs": [ + { + "output_type": "stream", + "text": [ + "torch.Size([64, 10, 300])\n" + ], + "name": "stdout" + } + ] + }, + { + "metadata": { + "id": "nzTscaE10HFA", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "There are several different ways you can use these char-level embeddings:\n", + "\n", + "1. Concat char-level embeddings with word-level embeddings, since we have an embedding for each word (at a char-level) and then feed it into an RNN. \n", + "2. You can feed the char-level embeddings into an RNN to processes them." + ] + }, + { + "metadata": { + "id": "nyCQ13_ckV_c", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "# Encoder and decoder" + ] + }, + { + "metadata": { + "id": "_sixbu74kbJk", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "So far we've used RNNs to `encode` a sequential input and generate hidden states. We use these hidden states to `decode` the predictions. So far, the encoder was an RNN and the decoder was just a few fully connected layers followed by a softmax layer (for classification). But the encoder and decoder can assume other architectures as well. For example, the decoder could be an RNN that processes the hidden state outputs from the encoder RNN. " + ] + }, + { + "metadata": { + "id": "kfK1mAp1dlpT", + "colab_type": "code", + "colab": {} + }, + "cell_type": "code", + "source": [ + "# Arguments\n", + "args = Namespace(\n", + " batch_size=64,\n", + " embedding_dim=100,\n", + " rnn_hidden_dim=100,\n", + " hidden_dim=100,\n", + " num_layers=1,\n", + " bidirectional=False,\n", + " dropout=0.1,\n", + ")" + ], + "execution_count": 0, + "outputs": [] + }, + { + "metadata": { + "id": "p_OJFyY97bF_", + "colab_type": "code", + "colab": {} + }, + "cell_type": "code", + "source": [ + "class Encoder(nn.Module):\n", + " def __init__(self, embedding_dim, num_embeddings, rnn_hidden_dim, \n", + " num_layers, bidirectional, padding_idx=0):\n", + " super(Encoder, self).__init__()\n", + " \n", + " # Embeddings\n", + " self.word_embeddings = nn.Embedding(embedding_dim=embedding_dim,\n", + " num_embeddings=num_embeddings,\n", + " padding_idx=padding_idx)\n", + " \n", + " # GRU weights\n", + " self.gru = nn.GRU(input_size=embedding_dim, hidden_size=rnn_hidden_dim, \n", + " num_layers=num_layers, batch_first=True, \n", + " bidirectional=bidirectional)\n", + "\n", + " def forward(self, x_in, x_lengths):\n", + " \n", + " # Word level embeddings\n", + " z_word = self.word_embeddings(x_in)\n", + " \n", + " # Feed into RNN\n", + " out, h_n = self.gru(z)\n", + " \n", + " # Gather the last relevant hidden state\n", + " out = gather_last_relevant_hidden(out, x_lengths)\n", + " \n", + " return out" + ], + "execution_count": 0, + "outputs": [] + }, + { + "metadata": { + "id": "HRXtaGPlpyH7", + "colab_type": "code", + "colab": {} + }, + "cell_type": "code", + "source": [ + "class Decoder(nn.Module):\n", + " def __init__(self, rnn_hidden_dim, hidden_dim, output_dim, dropout_p):\n", + " super(Decoder, self).__init__()\n", + " \n", + " # FC weights\n", + " self.dropout = nn.Dropout(dropout_p)\n", + " self.fc1 = nn.Linear(rnn_hidden_dim, hidden_dim)\n", + " self.fc2 = nn.Linear(hidden_dim, output_dim)\n", + "\n", + " def forward(self, encoder_output, apply_softmax=False):\n", + " \n", + " # FC layers\n", + " z = self.dropout(encoder_output)\n", + " z = self.fc1(z)\n", + " z = self.dropout(z)\n", + " y_pred = self.fc2(z)\n", + "\n", + " if apply_softmax:\n", + " y_pred = F.softmax(y_pred, dim=1)\n", + " return y_pred" + ], + "execution_count": 0, + "outputs": [] + }, + { + "metadata": { + "id": "SnKyCPVj-OVi", + "colab_type": "code", + "colab": {} + }, + "cell_type": "code", + "source": [ + "class Model(nn.Module):\n", + " def __init__(self, embedding_dim, num_embeddings, rnn_hidden_dim, \n", + " hidden_dim, num_layers, bidirectional, output_dim, dropout_p, \n", + " padding_idx=0):\n", + " super(Model, self).__init__()\n", + " self.encoder = Encoder(embedding_dim, num_embeddings, rnn_hidden_dim, \n", + " num_layers, bidirectional, padding_idx=0)\n", + " self.decoder = Decoder(rnn_hidden_dim, hidden_dim, output_dim, dropout_p)\n", + " \n", + " def forward(self, x_in, x_lengths, apply_softmax=False):\n", + " encoder_outputs = self.encoder(x_in, x_lengths)\n", + " y_pred = self.decoder(encoder_outputs, apply_softmax)\n", + " return y_pred" + ], + "execution_count": 0, + "outputs": [] + }, + { + "metadata": { + "id": "hfeoErsc-Tum", + "colab_type": "code", + "outputId": "8faa37ab-4c38-4ace-bb96-e5dc7e1483bf", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 204 + } + }, + "cell_type": "code", + "source": [ + "model = Model(embedding_dim=args.embedding_dim, num_embeddings=1000, \n", + " rnn_hidden_dim=args.rnn_hidden_dim, hidden_dim=args.hidden_dim, \n", + " num_layers=args.num_layers, bidirectional=args.bidirectional, \n", + " output_dim=4, dropout_p=args.dropout, padding_idx=0)\n", + "print (model.named_parameters)" + ], + "execution_count": 20, + "outputs": [ + { + "output_type": "stream", + "text": [ + "\n" + ], + "name": "stdout" + } + ] + }, + { + "metadata": { + "id": "LAsOI6jEmTd0", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "# Attentional mechanisms" + ] + }, + { + "metadata": { + "id": "vJN5ft5Sc_kb", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "When processing an input sequence with an RNN, recall that at each time step we process the input and the hidden state at that time step. For many use cases, it's advantageous to have access to the inputs at all time steps and pay selective attention to the them at each time step. For example, in machine translation, it's advantageous to have access to all the words when translating to another language because translations aren't necessarily word for word. " + ] + }, + { + "metadata": { + "id": "jb6A6WfbXje6", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "" + ] + }, + { + "metadata": { + "id": "mNkayU0rf-ua", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "Attention can sound a bit confusing so let's see what happens at each time step. At time step j, the model has processed inputs $x_0, x_1, x_2, ..., x_j$ and has generted hidden states $h_0, h_1, h_2, ..., h_j$. The idea is to use all the processed hidden states to make the prediction and not just the most recent one. There are several approaches to how we can do this.\n", + "\n", + "With **soft attention**, we learn a vector of floating points (probabilities) to multiply with the hidden states to create the context vector.\n", + "\n", + "Ex. [0.1, 0.3, 0.1, 0.4, 0.1]\n", + "\n", + "With **hard attention**, we can learn a binary vector to multiply with the hidden states to create the context vector. \n", + "\n", + "Ex. [0, 0, 0, 1, 0]" + ] + }, + { + "metadata": { + "id": "gYSIAVQqu3Ab", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "We're going to focus on soft attention because it's more widley used and we can visualize how much of each hidden state helps with the prediction, which is great for interpretability. " + ] + }, + { + "metadata": { + "id": "9Ch21nZNvDHO", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "" + ] + }, + { + "metadata": { + "id": "o_jPXuT8xlqd", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "We're going to implement attention in the document classification task below." + ] + }, + { + "metadata": { + "colab_type": "text", + "id": "0iNnQzdxnGvn" + }, + "cell_type": "markdown", + "source": [ + "# Document classification with RNNs" + ] + }, + { + "metadata": { + "colab_type": "text", + "id": "n38ZJoVZnGaE" + }, + "cell_type": "markdown", + "source": [ + "We're going to implement the same document classification task as in the previous notebook but we're going to use an attentional interface for interpretability.\n", + "\n", + "**Why not machine translation?** Normally, machine translation is the go-to example for demonstrating attention but it's not really practical. How many situations can you think of that require a seq to generate another sequence? Instead we're going to apply attention with our document classification example to see which input tokens are more influential towards predicting the genre." + ] + }, + { + "metadata": { + "colab_type": "text", + "id": "Fu7HgEqbnGFY" + }, + "cell_type": "markdown", + "source": [ + "## Set up" + ] + }, + { + "metadata": { + "colab_type": "code", + "id": "elL6BxtCmNGf", + "colab": {} + }, + "cell_type": "code", + "source": [ + "from argparse import Namespace\n", + "import collections\n", + "import copy\n", + "import json\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import pandas as pd\n", + "import re\n", + "import torch" + ], + "execution_count": 0, + "outputs": [] + }, + { + "metadata": { + "id": "DCf2fLmPbKKI", + "colab_type": "code", + "colab": {} + }, + "cell_type": "code", + "source": [ + "def set_seeds(seed, cuda):\n", + " np.random.seed(seed)\n", + " torch.manual_seed(seed)\n", + " if cuda:\n", + " torch.cuda.manual_seed_all(seed)\n", + " \n", + "# Creating directories\n", + "def create_dirs(dirpath):\n", + " if not os.path.exists(dirpath):\n", + " os.makedirs(dirpath)" + ], + "execution_count": 0, + "outputs": [] + }, + { + "metadata": { + "colab_type": "code", + "outputId": "291c03d4-6143-4395-b5c9-ab386b061737", + "id": "TTwkuoZdmMlF", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 34 + } + }, + "cell_type": "code", + "source": [ + "args = Namespace(\n", + " seed=1234,\n", + " cuda=True,\n", + " shuffle=True,\n", + " data_file=\"news.csv\",\n", + " split_data_file=\"split_news.csv\",\n", + " vectorizer_file=\"vectorizer.json\",\n", + " model_state_file=\"model.pth\",\n", + " save_dir=\"news\",\n", + " train_size=0.7,\n", + " val_size=0.15,\n", + " test_size=0.15,\n", + " pretrained_embeddings=None,\n", + " cutoff=25,\n", + " num_epochs=5,\n", + " early_stopping_criteria=5,\n", + " learning_rate=1e-3,\n", + " batch_size=128,\n", + " embedding_dim=100,\n", + " kernels=[3,5],\n", + " num_filters=100,\n", + " rnn_hidden_dim=128,\n", + " hidden_dim=200,\n", + " num_layers=1,\n", + " bidirectional=False,\n", + " dropout_p=0.25,\n", + ")\n", + "\n", + "# Set seeds\n", + "set_seeds(seed=args.seed, cuda=args.cuda)\n", + "\n", + "# Create save dir\n", + "create_dirs(args.save_dir)\n", + "\n", + "# Expand filepaths\n", + "args.vectorizer_file = os.path.join(args.save_dir, args.vectorizer_file)\n", + "args.model_state_file = os.path.join(args.save_dir, args.model_state_file)\n", + "\n", + "# Check CUDA\n", + "if not torch.cuda.is_available():\n", + " args.cuda = False\n", + "args.device = torch.device(\"cuda\" if args.cuda else \"cpu\")\n", + "print(\"Using CUDA: {}\".format(args.cuda))" + ], + "execution_count": 28, + "outputs": [ + { + "output_type": "stream", + "text": [ + "Using CUDA: True\n" + ], + "name": "stdout" + } + ] + }, + { + "metadata": { + "colab_type": "text", + "id": "xfiWhgX5mMQ5" + }, + "cell_type": "markdown", + "source": [ + "## Data" + ] + }, + { + "metadata": { + "colab_type": "code", + "id": "baAsxXNFmMCF", + "colab": {} + }, + "cell_type": "code", + "source": [ + "import urllib" + ], + "execution_count": 0, + "outputs": [] + }, + { + "metadata": { + "colab_type": "code", + "id": "3tJi_HyOmLw-", + "colab": {} + }, + "cell_type": "code", + "source": [ + "url = \"https://raw.githubusercontent.com/GokuMohandas/practicalAI/master/data/news.csv\"\n", + "response = urllib.request.urlopen(url)\n", + "html = response.read()\n", + "with open(args.data_file, 'wb') as fp:\n", + " fp.write(html)" + ], + "execution_count": 0, + "outputs": [] + }, + { + "metadata": { + "colab_type": "code", + "outputId": "a51463a7-f37e-41e7-aca4-74038c7c6e8e", + "id": "wrI_df4bmLjB", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 204 + } + }, + "cell_type": "code", + "source": [ + "df = pd.read_csv(args.data_file, header=0)\n", + "df.head()" + ], + "execution_count": 31, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
categorytitle
0BusinessWall St. Bears Claw Back Into the Black (Reuters)
1BusinessCarlyle Looks Toward Commercial Aerospace (Reu...
2BusinessOil and Economy Cloud Stocks' Outlook (Reuters)
3BusinessIraq Halts Oil Exports from Main Southern Pipe...
4BusinessOil prices soar to all-time record, posing new...
\n", + "
" + ], + "text/plain": [ + " category title\n", + "0 Business Wall St. Bears Claw Back Into the Black (Reuters)\n", + "1 Business Carlyle Looks Toward Commercial Aerospace (Reu...\n", + "2 Business Oil and Economy Cloud Stocks' Outlook (Reuters)\n", + "3 Business Iraq Halts Oil Exports from Main Southern Pipe...\n", + "4 Business Oil prices soar to all-time record, posing new..." + ] + }, + "metadata": { + "tags": [] + }, + "execution_count": 31 + } + ] + }, + { + "metadata": { + "colab_type": "code", + "outputId": "36145f0d-7316-4341-f270-1d8c8037c661", + "id": "TreK7nqEmLTN", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 85 + } + }, + "cell_type": "code", + "source": [ + "by_category = collections.defaultdict(list)\n", + "for _, row in df.iterrows():\n", + " by_category[row.category].append(row.to_dict())\n", + "for category in by_category:\n", + " print (\"{0}: {1}\".format(category, len(by_category[category])))" + ], + "execution_count": 32, + "outputs": [ + { + "output_type": "stream", + "text": [ + "Business: 30000\n", + "Sci/Tech: 30000\n", + "Sports: 30000\n", + "World: 30000\n" + ], + "name": "stdout" + } + ] + }, + { + "metadata": { + "colab_type": "code", + "id": "35nb3LxLmLCA", + "colab": {} + }, + "cell_type": "code", + "source": [ + "final_list = []\n", + "for _, item_list in sorted(by_category.items()):\n", + " if args.shuffle:\n", + " np.random.shuffle(item_list)\n", + " n = len(item_list)\n", + " n_train = int(args.train_size*n)\n", + " n_val = int(args.val_size*n)\n", + " n_test = int(args.test_size*n)\n", + "\n", + " # Give data point a split attribute\n", + " for item in item_list[:n_train]:\n", + " item['split'] = 'train'\n", + " for item in item_list[n_train:n_train+n_val]:\n", + " item['split'] = 'val'\n", + " for item in item_list[n_train+n_val:]:\n", + " item['split'] = 'test' \n", + "\n", + " # Add to final list\n", + " final_list.extend(item_list)" + ], + "execution_count": 0, + "outputs": [] + }, + { + "metadata": { + "colab_type": "code", + "outputId": "3b188412-5c0a-4e71-ef50-20c4ba18082b", + "id": "Y48IvuSfmK07", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 85 + } + }, + "cell_type": "code", + "source": [ + "split_df = pd.DataFrame(final_list)\n", + "split_df[\"split\"].value_counts()" + ], + "execution_count": 34, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + "train 84000\n", + "val 18000\n", + "test 18000\n", + "Name: split, dtype: int64" + ] + }, + "metadata": { + "tags": [] + }, + "execution_count": 34 + } + ] + }, + { + "metadata": { + "colab_type": "code", + "id": "RWuNBxAXmKk2", + "colab": {} + }, + "cell_type": "code", + "source": [ + "def preprocess_text(text):\n", + " text = ' '.join(word.lower() for word in text.split(\" \"))\n", + " text = re.sub(r\"([.,!?])\", r\" \\1 \", text)\n", + " text = re.sub(r\"[^a-zA-Z.,!?]+\", r\" \", text)\n", + " text = text.strip()\n", + " return text\n", + " \n", + "split_df.title = split_df.title.apply(preprocess_text)" + ], + "execution_count": 0, + "outputs": [] + }, + { + "metadata": { + "colab_type": "code", + "outputId": "7bb68022-5848-44ac-f90c-7cdf6a7eb988", + "id": "fG9n77eLmKWB", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 204 + } + }, + "cell_type": "code", + "source": [ + "split_df.to_csv(args.split_data_file, index=False)\n", + "split_df.head()" + ], + "execution_count": 36, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
categorysplittitle
0Businesstraingeneral electric posts higher rd quarter profit
1Businesstrainlilly to eliminate up to us jobs
2Businesstrains amp p lowers america west outlook to negative
3Businesstraindoes rand walk the talk on labor policy ?
4Businesstrainhousekeeper advocates for changes
\n", + "
" + ], + "text/plain": [ + " category split title\n", + "0 Business train general electric posts higher rd quarter profit\n", + "1 Business train lilly to eliminate up to us jobs\n", + "2 Business train s amp p lowers america west outlook to negative\n", + "3 Business train does rand walk the talk on labor policy ?\n", + "4 Business train housekeeper advocates for changes" + ] + }, + "metadata": { + "tags": [] + }, + "execution_count": 36 + } + ] + }, + { + "metadata": { + "colab_type": "text", + "id": "m-a0OpqhmKJc" + }, + "cell_type": "markdown", + "source": [ + "## Vocabulary" + ] + }, + { + "metadata": { + "colab_type": "code", + "id": "RUMQ_MwumJ8F", + "colab": {} + }, + "cell_type": "code", + "source": [ + "class Vocabulary(object):\n", + " def __init__(self, token_to_idx=None):\n", + "\n", + " # Token to index\n", + " if token_to_idx is None:\n", + " token_to_idx = {}\n", + " self.token_to_idx = token_to_idx\n", + "\n", + " # Index to token\n", + " self.idx_to_token = {idx: token \\\n", + " for token, idx in self.token_to_idx.items()}\n", + "\n", + " def to_serializable(self):\n", + " return {'token_to_idx': self.token_to_idx}\n", + "\n", + " @classmethod\n", + " def from_serializable(cls, contents):\n", + " return cls(**contents)\n", + "\n", + " def add_token(self, token):\n", + " if token in self.token_to_idx:\n", + " index = self.token_to_idx[token]\n", + " else:\n", + " index = len(self.token_to_idx)\n", + " self.token_to_idx[token] = index\n", + " self.idx_to_token[index] = token\n", + " return index\n", + "\n", + " def add_tokens(self, tokens):\n", + " return [self.add_token[token] for token in tokens]\n", + "\n", + " def lookup_token(self, token):\n", + " return self.token_to_idx[token]\n", + "\n", + " def lookup_index(self, index):\n", + " if index not in self.idx_to_token:\n", + " raise KeyError(\"the index (%d) is not in the Vocabulary\" % index)\n", + " return self.idx_to_token[index]\n", + "\n", + " def __str__(self):\n", + " return \"\" % len(self)\n", + "\n", + " def __len__(self):\n", + " return len(self.token_to_idx)" + ], + "execution_count": 0, + "outputs": [] + }, + { + "metadata": { + "id": "1LtYf3lpExBb", + "colab_type": "code", + "outputId": "0870e7a9-d843-4549-97ae-d8cf5c3e7e3e", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 85 + } + }, + "cell_type": "code", + "source": [ + "# Vocabulary instance\n", + "category_vocab = Vocabulary()\n", + "for index, row in df.iterrows():\n", + " category_vocab.add_token(row.category)\n", + "print (category_vocab) # __str__\n", + "print (len(category_vocab)) # __len__\n", + "index = category_vocab.lookup_token(\"Business\")\n", + "print (index)\n", + "print (category_vocab.lookup_index(index))" + ], + "execution_count": 38, + "outputs": [ + { + "output_type": "stream", + "text": [ + "\n", + "4\n", + "0\n", + "Business\n" + ], + "name": "stdout" + } + ] + }, + { + "metadata": { + "id": "Z0zkF6CsE_yH", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "## Sequence vocabulary" + ] + }, + { + "metadata": { + "id": "QtntaISyE_1c", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "Next, we're going to create our Vocabulary classes for the article's title, which is a sequence of words." + ] + }, + { + "metadata": { + "id": "ovI8QRefEw_p", + "colab_type": "code", + "colab": {} + }, + "cell_type": "code", + "source": [ + "from collections import Counter\n", + "import string" + ], + "execution_count": 0, + "outputs": [] + }, + { + "metadata": { + "id": "4W3ZouuTEw1_", + "colab_type": "code", + "colab": {} + }, + "cell_type": "code", + "source": [ + "class SequenceVocabulary(Vocabulary):\n", + " def __init__(self, token_to_idx=None, unk_token=\"\",\n", + " mask_token=\"\", begin_seq_token=\"\",\n", + " end_seq_token=\"\"):\n", + "\n", + " super(SequenceVocabulary, self).__init__(token_to_idx)\n", + "\n", + " self.mask_token = mask_token\n", + " self.unk_token = unk_token\n", + " self.begin_seq_token = begin_seq_token\n", + " self.end_seq_token = end_seq_token\n", + "\n", + " self.mask_index = self.add_token(self.mask_token)\n", + " self.unk_index = self.add_token(self.unk_token)\n", + " self.begin_seq_index = self.add_token(self.begin_seq_token)\n", + " self.end_seq_index = self.add_token(self.end_seq_token)\n", + " \n", + " # Index to token\n", + " self.idx_to_token = {idx: token \\\n", + " for token, idx in self.token_to_idx.items()}\n", + "\n", + " def to_serializable(self):\n", + " contents = super(SequenceVocabulary, self).to_serializable()\n", + " contents.update({'unk_token': self.unk_token,\n", + " 'mask_token': self.mask_token,\n", + " 'begin_seq_token': self.begin_seq_token,\n", + " 'end_seq_token': self.end_seq_token})\n", + " return contents\n", + "\n", + " def lookup_token(self, token):\n", + " return self.token_to_idx.get(token, self.unk_index)\n", + " \n", + " def lookup_index(self, index):\n", + " if index not in self.idx_to_token:\n", + " raise KeyError(\"the index (%d) is not in the SequenceVocabulary\" % index)\n", + " return self.idx_to_token[index]\n", + " \n", + " def __str__(self):\n", + " return \"\" % len(self.token_to_idx)\n", + "\n", + " def __len__(self):\n", + " return len(self.token_to_idx)\n" + ], + "execution_count": 0, + "outputs": [] + }, + { + "metadata": { + "id": "g5UHjpi3El37", + "colab_type": "code", + "outputId": "75875a36-e34f-4e25-aa96-656bdfe4f210", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 85 + } + }, + "cell_type": "code", + "source": [ + "# Get word counts\n", + "word_counts = Counter()\n", + "for title in split_df.title:\n", + " for token in title.split(\" \"):\n", + " if token not in string.punctuation:\n", + " word_counts[token] += 1\n", + "\n", + "# Create SequenceVocabulary instance\n", + "title_word_vocab = SequenceVocabulary()\n", + "for word, word_count in word_counts.items():\n", + " if word_count >= args.cutoff:\n", + " title_word_vocab.add_token(word)\n", + "print (title_word_vocab) # __str__\n", + "print (len(title_word_vocab)) # __len__\n", + "index = title_word_vocab.lookup_token(\"general\")\n", + "print (index)\n", + "print (title_word_vocab.lookup_index(index))" + ], + "execution_count": 41, + "outputs": [ + { + "output_type": "stream", + "text": [ + "\n", + "4400\n", + "4\n", + "general\n" + ], + "name": "stdout" + } + ] + }, + { + "metadata": { + "id": "1_wja0EfQNpA", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "We're also going to create an instance fo SequenceVocabulary that processes the input on a character level." + ] + }, + { + "metadata": { + "id": "5SpfS0BXP9pz", + "colab_type": "code", + "outputId": "383414b5-1274-499a-cd2f-d83cfc17bec6", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 85 + } + }, + "cell_type": "code", + "source": [ + "# Create SequenceVocabulary instance\n", + "title_char_vocab = SequenceVocabulary()\n", + "for title in split_df.title:\n", + " for token in title:\n", + " title_char_vocab.add_token(token)\n", + "print (title_char_vocab) # __str__\n", + "print (len(title_char_vocab)) # __len__\n", + "index = title_char_vocab.lookup_token(\"g\")\n", + "print (index)\n", + "print (title_char_vocab.lookup_index(index))" + ], + "execution_count": 42, + "outputs": [ + { + "output_type": "stream", + "text": [ + "\n", + "35\n", + "4\n", + "g\n" + ], + "name": "stdout" + } + ] + }, + { + "metadata": { + "id": "4Dag6H0SFHAG", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "## Vectorizer" + ] + }, + { + "metadata": { + "id": "VQIfxcUuKwzz", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "Something new that we introduce in this Vectorizer is calculating the length of our input sequence. We will use this later on to extract the last relevant hidden state for each input sequence." + ] + }, + { + "metadata": { + "id": "tsNtEnhBEl6s", + "colab_type": "code", + "colab": {} + }, + "cell_type": "code", + "source": [ + "class NewsVectorizer(object):\n", + " def __init__(self, title_word_vocab, title_char_vocab, category_vocab):\n", + " self.title_word_vocab = title_word_vocab\n", + " self.title_char_vocab = title_char_vocab\n", + " self.category_vocab = category_vocab\n", + "\n", + " def vectorize(self, title):\n", + " \n", + " # Word-level vectorization\n", + " word_indices = [self.title_word_vocab.lookup_token(token) for token in title.split(\" \")]\n", + " word_indices = [self.title_word_vocab.begin_seq_index] + word_indices + \\\n", + " [self.title_word_vocab.end_seq_index]\n", + " title_length = len(word_indices)\n", + " word_vector = np.zeros(title_length, dtype=np.int64)\n", + " word_vector[:len(word_indices)] = word_indices\n", + " \n", + " # Char-level vectorization\n", + " word_length = max([len(word) for word in title.split(\" \")])\n", + " char_vector = np.zeros((len(word_vector), word_length), dtype=np.int64)\n", + " char_vector[0, :] = self.title_word_vocab.mask_index # \n", + " char_vector[-1, :] = self.title_word_vocab.mask_index # \n", + " for i, word in enumerate(title.split(\" \")):\n", + " char_vector[i+1,:len(word)] = [title_char_vocab.lookup_token(char) \\\n", + " for char in word] # i+1 b/c of token\n", + " \n", + " return word_vector, char_vector, len(word_indices)\n", + " \n", + " def unvectorize_word_vector(self, word_vector):\n", + " tokens = [self.title_word_vocab.lookup_index(index) for index in word_vector]\n", + " title = \" \".join(token for token in tokens)\n", + " return title\n", + " \n", + " def unvectorize_char_vector(self, char_vector):\n", + " title = \"\"\n", + " for word_vector in char_vector:\n", + " for index in word_vector:\n", + " if index == self.title_char_vocab.mask_index:\n", + " break\n", + " title += self.title_char_vocab.lookup_index(index)\n", + " title += \" \"\n", + " return title\n", + " \n", + " @classmethod\n", + " def from_dataframe(cls, df, cutoff):\n", + " \n", + " # Create class vocab\n", + " category_vocab = Vocabulary() \n", + " for category in sorted(set(df.category)):\n", + " category_vocab.add_token(category)\n", + "\n", + " # Get word counts\n", + " word_counts = Counter()\n", + " for title in df.title:\n", + " for token in title.split(\" \"):\n", + " word_counts[token] += 1\n", + " \n", + " # Create title vocab (word level)\n", + " title_word_vocab = SequenceVocabulary()\n", + " for word, word_count in word_counts.items():\n", + " if word_count >= cutoff:\n", + " title_word_vocab.add_token(word)\n", + " \n", + " # Create title vocab (char level)\n", + " title_char_vocab = SequenceVocabulary()\n", + " for title in df.title:\n", + " for token in title:\n", + " title_char_vocab.add_token(token)\n", + " \n", + " return cls(title_word_vocab, title_char_vocab, category_vocab)\n", + "\n", + " @classmethod\n", + " def from_serializable(cls, contents):\n", + " title_word_vocab = SequenceVocabulary.from_serializable(contents['title_word_vocab'])\n", + " title_char_vocab = SequenceVocabulary.from_serializable(contents['title_char_vocab'])\n", + " category_vocab = Vocabulary.from_serializable(contents['category_vocab'])\n", + " return cls(title_word_vocab=title_word_vocab, \n", + " title_char_vocab=title_char_vocab, \n", + " category_vocab=category_vocab)\n", + " \n", + " def to_serializable(self):\n", + " return {'title_word_vocab': self.title_word_vocab.to_serializable(),\n", + " 'title_char_vocab': self.title_char_vocab.to_serializable(),\n", + " 'category_vocab': self.category_vocab.to_serializable()}" + ], + "execution_count": 0, + "outputs": [] + }, + { + "metadata": { + "id": "JtRRXU53El9Y", + "colab_type": "code", + "outputId": "659ad7a1-38a4-46ca-98b8-a72ba0c9fff0", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 340 + } + }, + "cell_type": "code", + "source": [ + "# Vectorizer instance\n", + "vectorizer = NewsVectorizer.from_dataframe(split_df, cutoff=args.cutoff)\n", + "print (vectorizer.title_word_vocab)\n", + "print (vectorizer.title_char_vocab)\n", + "print (vectorizer.category_vocab)\n", + "word_vector, char_vector, title_length = vectorizer.vectorize(preprocess_text(\n", + " \"Roger Federer wins the Wimbledon tennis tournament.\"))\n", + "print (\"word_vector:\", np.shape(word_vector))\n", + "print (\"char_vector:\", np.shape(char_vector))\n", + "print (\"title_length:\", title_length)\n", + "print (word_vector)\n", + "print (char_vector)\n", + "print (vectorizer.unvectorize_word_vector(word_vector))\n", + "print (vectorizer.unvectorize_char_vector(char_vector))" + ], + "execution_count": 81, + "outputs": [ + { + "output_type": "stream", + "text": [ + "\n", + "\n", + "\n", + "word_vector: (10,)\n", + "char_vector: (10, 10)\n", + "title_length: 10\n", + "[ 2 1 4151 1231 25 1 2392 4076 38 3]\n", + "[[ 0 0 0 0 0 0 0 0 0 0]\n", + " [ 7 15 4 5 7 0 0 0 0 0]\n", + " [21 5 18 5 7 5 7 0 0 0]\n", + " [26 13 6 16 0 0 0 0 0 0]\n", + " [12 17 5 0 0 0 0 0 0 0]\n", + " [26 13 23 25 9 5 18 15 6 0]\n", + " [12 5 6 6 13 16 0 0 0 0]\n", + " [12 15 20 7 6 8 23 5 6 12]\n", + " [30 0 0 0 0 0 0 0 0 0]\n", + " [ 0 0 0 0 0 0 0 0 0 0]]\n", + " federer wins the tennis tournament . \n", + " roger federer wins the wimbledon tennis tournament . \n" + ], + "name": "stdout" + } + ] + }, + { + "metadata": { + "id": "uk_QvpVfFM0S", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "## Dataset" + ] + }, + { + "metadata": { + "id": "oU7oDdelFMR9", + "colab_type": "code", + "colab": {} + }, + "cell_type": "code", + "source": [ + "from torch.utils.data import Dataset, DataLoader" + ], + "execution_count": 0, + "outputs": [] + }, + { + "metadata": { + "id": "pB7FHmiSFMXA", + "colab_type": "code", + "colab": {} + }, + "cell_type": "code", + "source": [ + "class NewsDataset(Dataset):\n", + " def __init__(self, df, vectorizer):\n", + " self.df = df\n", + " self.vectorizer = vectorizer\n", + "\n", + " # Data splits\n", + " self.train_df = self.df[self.df.split=='train']\n", + " self.train_size = len(self.train_df)\n", + " self.val_df = self.df[self.df.split=='val']\n", + " self.val_size = len(self.val_df)\n", + " self.test_df = self.df[self.df.split=='test']\n", + " self.test_size = len(self.test_df)\n", + " self.lookup_dict = {'train': (self.train_df, self.train_size), \n", + " 'val': (self.val_df, self.val_size),\n", + " 'test': (self.test_df, self.test_size)}\n", + " self.set_split('train')\n", + "\n", + " # Class weights (for imbalances)\n", + " class_counts = df.category.value_counts().to_dict()\n", + " def sort_key(item):\n", + " return self.vectorizer.category_vocab.lookup_token(item[0])\n", + " sorted_counts = sorted(class_counts.items(), key=sort_key)\n", + " frequencies = [count for _, count in sorted_counts]\n", + " self.class_weights = 1.0 / torch.tensor(frequencies, dtype=torch.float32)\n", + "\n", + " @classmethod\n", + " def load_dataset_and_make_vectorizer(cls, split_data_file, cutoff):\n", + " df = pd.read_csv(split_data_file, header=0)\n", + " train_df = df[df.split=='train']\n", + " return cls(df, NewsVectorizer.from_dataframe(train_df, cutoff))\n", + "\n", + " @classmethod\n", + " def load_dataset_and_load_vectorizer(cls, split_data_file, vectorizer_filepath):\n", + " df = pd.read_csv(split_data_file, header=0)\n", + " vectorizer = cls.load_vectorizer_only(vectorizer_filepath)\n", + " return cls(df, vectorizer)\n", + "\n", + " def load_vectorizer_only(vectorizer_filepath):\n", + " with open(vectorizer_filepath) as fp:\n", + " return NewsVectorizer.from_serializable(json.load(fp))\n", + "\n", + " def save_vectorizer(self, vectorizer_filepath):\n", + " with open(vectorizer_filepath, \"w\") as fp:\n", + " json.dump(self.vectorizer.to_serializable(), fp)\n", + "\n", + " def set_split(self, split=\"train\"):\n", + " self.target_split = split\n", + " self.target_df, self.target_size = self.lookup_dict[split]\n", + "\n", + " def __str__(self):\n", + " return \" delta bankruptcy with labor deal \n", + " delta dodges bankruptcy with labor deal \n", + "tensor([3.3333e-05, 3.3333e-05, 3.3333e-05, 3.3333e-05])\n" + ], + "name": "stdout" + } + ] + }, + { + "metadata": { + "id": "_IUIqtbvFUAG", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "## Model" + ] + }, + { + "metadata": { + "id": "xJV5WlDiFVVz", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "embed → encoder → attend → predict" + ] + }, + { + "metadata": { + "id": "rZCzdZZ9FMhm", + "colab_type": "code", + "colab": {} + }, + "cell_type": "code", + "source": [ + "import torch.nn as nn\n", + "import torch.nn.functional as F" + ], + "execution_count": 0, + "outputs": [] + }, + { + "metadata": { + "id": "c9wipRZt7feC", + "colab_type": "code", + "colab": {} + }, + "cell_type": "code", + "source": [ + "class NewsEncoder(nn.Module):\n", + " def __init__(self, embedding_dim, num_word_embeddings, num_char_embeddings,\n", + " kernels, num_input_channels, num_output_channels, \n", + " rnn_hidden_dim, num_layers, bidirectional, \n", + " word_padding_idx=0, char_padding_idx=0):\n", + " super(NewsEncoder, self).__init__()\n", + " \n", + " self.num_layers = num_layers\n", + " self.bidirectional = bidirectional\n", + " \n", + " # Embeddings\n", + " self.word_embeddings = nn.Embedding(embedding_dim=embedding_dim,\n", + " num_embeddings=num_word_embeddings,\n", + " padding_idx=word_padding_idx)\n", + " self.char_embeddings = nn.Embedding(embedding_dim=embedding_dim,\n", + " num_embeddings=num_char_embeddings,\n", + " padding_idx=char_padding_idx)\n", + " \n", + " # Conv weights\n", + " self.conv = nn.ModuleList([nn.Conv1d(num_input_channels, \n", + " num_output_channels, \n", + " kernel_size=f) for f in kernels])\n", + " \n", + " \n", + " # GRU weights\n", + " self.gru = nn.GRU(input_size=embedding_dim*(len(kernels)+1), \n", + " hidden_size=rnn_hidden_dim, num_layers=num_layers, \n", + " batch_first=True, bidirectional=bidirectional)\n", + " \n", + " def initialize_hidden_state(self, batch_size, rnn_hidden_dim, device):\n", + " \"\"\"Modify this to condition the RNN.\"\"\"\n", + " num_directions = 1\n", + " if self.bidirectional:\n", + " num_directions = 2\n", + " hidden_t = torch.zeros(self.num_layers * num_directions, \n", + " batch_size, rnn_hidden_dim).to(device)\n", + " \n", + " def get_char_level_embeddings(self, x):\n", + " # x: (N, seq_len, word_len)\n", + " input_shape = x.size()\n", + " batch_size, seq_len, word_len = input_shape\n", + " x = x.view(-1, word_len) # (N*seq_len, word_len)\n", + " \n", + " # Embedding\n", + " x = self.char_embeddings(x) # (N*seq_len, word_len, embedding_dim)\n", + " \n", + " # Rearrange input so num_input_channels is in dim 1 (N, embedding_dim, word_len)\n", + " x = x.transpose(1, 2)\n", + " \n", + " # Convolution\n", + " z = [F.relu(conv(x)) for conv in self.conv]\n", + " \n", + " # Pooling\n", + " z = [F.max_pool1d(zz, zz.size(2)).squeeze(2) for zz in z] \n", + " z = [zz.view(batch_size, seq_len, -1) for zz in z] # (N, seq_len, embedding_dim)\n", + " \n", + " # Concat to get char-level embeddings\n", + " z = torch.cat(z, 2) # join conv outputs\n", + " \n", + " return z\n", + " \n", + " def forward(self, x_word, x_char, x_lengths, device):\n", + " \"\"\"\n", + " x_word: word level representation (N, seq_size)\n", + " x_char: char level representation (N, seq_size, word_len)\n", + " \"\"\"\n", + " \n", + " # Word level embeddings\n", + " z_word = self.word_embeddings(x_word)\n", + " \n", + " # Char level embeddings\n", + " z_char = self.get_char_level_embeddings(x=x_char)\n", + " \n", + " # Concatenate\n", + " z = torch.cat([z_word, z_char], 2)\n", + " \n", + " # Feed into RNN\n", + " initial_h = self.initialize_hidden_state(\n", + " batch_size=z.size(0), rnn_hidden_dim=self.gru.hidden_size,\n", + " device=device)\n", + " out, h_n = self.gru(z, initial_h)\n", + " \n", + " return out" + ], + "execution_count": 0, + "outputs": [] + }, + { + "metadata": { + "id": "zeEcdA287gz4", + "colab_type": "code", + "colab": {} + }, + "cell_type": "code", + "source": [ + "class NewsDecoder(nn.Module):\n", + " def __init__(self, rnn_hidden_dim, hidden_dim, output_dim, dropout_p):\n", + " super(NewsDecoder, self).__init__()\n", + " \n", + " # Attention FC layer\n", + " self.fc_attn = nn.Linear(rnn_hidden_dim, rnn_hidden_dim)\n", + " self.v = nn.Parameter(torch.rand(rnn_hidden_dim))\n", + " \n", + " # FC weights\n", + " self.dropout = nn.Dropout(dropout_p)\n", + " self.fc1 = nn.Linear(rnn_hidden_dim, hidden_dim)\n", + " self.fc2 = nn.Linear(hidden_dim, output_dim)\n", + "\n", + " def forward(self, encoder_outputs, apply_softmax=False):\n", + " \n", + " # Attention\n", + " z = torch.tanh(self.fc_attn(encoder_outputs))\n", + " z = z.transpose(2,1) # [B*H*T]\n", + " v = self.v.repeat(encoder_outputs.size(0),1).unsqueeze(1) #[B*1*H]\n", + " z = torch.bmm(v,z).squeeze(1) # [B*T]\n", + " attn_scores = F.softmax(z, dim=1)\n", + " context = torch.matmul(encoder_outputs.transpose(-2, -1), \n", + " attn_scores.unsqueeze(dim=2)).squeeze()\n", + " if len(context.size()) == 1:\n", + " context = context.unsqueeze(0)\n", + " \n", + " # FC layers\n", + " z = self.dropout(context)\n", + " z = self.fc1(z)\n", + " z = self.dropout(z)\n", + " y_pred = self.fc2(z)\n", + "\n", + " if apply_softmax:\n", + " y_pred = F.softmax(y_pred, dim=1)\n", + " return attn_scores, y_pred" + ], + "execution_count": 0, + "outputs": [] + }, + { + "metadata": { + "id": "yVDftS-G7gwy", + "colab_type": "code", + "colab": {} + }, + "cell_type": "code", + "source": [ + "class NewsModel(nn.Module):\n", + " def __init__(self, embedding_dim, num_word_embeddings, num_char_embeddings,\n", + " kernels, num_input_channels, num_output_channels, \n", + " rnn_hidden_dim, hidden_dim, output_dim, num_layers, \n", + " bidirectional, dropout_p, word_padding_idx, char_padding_idx):\n", + " super(NewsModel, self).__init__()\n", + " self.encoder = NewsEncoder(embedding_dim, num_word_embeddings,\n", + " num_char_embeddings, kernels, \n", + " num_input_channels, num_output_channels, \n", + " rnn_hidden_dim, num_layers, bidirectional, \n", + " word_padding_idx, char_padding_idx)\n", + " self.decoder = NewsDecoder(rnn_hidden_dim, hidden_dim, output_dim, \n", + " dropout_p)\n", + " \n", + " def forward(self, x_word, x_char, x_lengths, device, apply_softmax=False):\n", + " encoder_outputs = self.encoder(x_word, x_char, x_lengths, device)\n", + " y_pred = self.decoder(encoder_outputs, apply_softmax)\n", + " return y_pred" + ], + "execution_count": 0, + "outputs": [] + }, + { + "metadata": { + "id": "jHPYCPd7Fl3M", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "## Training" + ] + }, + { + "metadata": { + "id": "D3seBMA7FlcC", + "colab_type": "code", + "colab": {} + }, + "cell_type": "code", + "source": [ + "import torch.optim as optim" + ], + "execution_count": 0, + "outputs": [] + }, + { + "metadata": { + "id": "HnRKWLekFlnM", + "colab_type": "code", + "colab": {} + }, + "cell_type": "code", + "source": [ + "class Trainer(object):\n", + " def __init__(self, dataset, model, model_state_file, save_dir, device, \n", + " shuffle, num_epochs, batch_size, learning_rate, \n", + " early_stopping_criteria):\n", + " self.dataset = dataset\n", + " self.class_weights = dataset.class_weights.to(device)\n", + " self.device = device\n", + " self.model = model.to(device)\n", + " self.save_dir = save_dir\n", + " self.device = device\n", + " self.shuffle = shuffle\n", + " self.num_epochs = num_epochs\n", + " self.batch_size = batch_size\n", + " self.loss_func = nn.CrossEntropyLoss(self.class_weights)\n", + " self.optimizer = optim.Adam(self.model.parameters(), lr=learning_rate)\n", + " self.scheduler = optim.lr_scheduler.ReduceLROnPlateau(\n", + " optimizer=self.optimizer, mode='min', factor=0.5, patience=1)\n", + " self.train_state = {\n", + " 'stop_early': False, \n", + " 'early_stopping_step': 0,\n", + " 'early_stopping_best_val': 1e8,\n", + " 'early_stopping_criteria': early_stopping_criteria,\n", + " 'learning_rate': learning_rate,\n", + " 'epoch_index': 0,\n", + " 'train_loss': [],\n", + " 'train_acc': [],\n", + " 'val_loss': [],\n", + " 'val_acc': [],\n", + " 'test_loss': -1,\n", + " 'test_acc': -1,\n", + " 'model_filename': model_state_file}\n", + " \n", + " def update_train_state(self):\n", + "\n", + " # Verbose\n", + " print (\"[EPOCH]: {0:02d} | [LR]: {1} | [TRAIN LOSS]: {2:.2f} | [TRAIN ACC]: {3:.1f}% | [VAL LOSS]: {4:.2f} | [VAL ACC]: {5:.1f}%\".format(\n", + " self.train_state['epoch_index'], self.train_state['learning_rate'], \n", + " self.train_state['train_loss'][-1], self.train_state['train_acc'][-1], \n", + " self.train_state['val_loss'][-1], self.train_state['val_acc'][-1]))\n", + "\n", + " # Save one model at least\n", + " if self.train_state['epoch_index'] == 0:\n", + " torch.save(self.model.state_dict(), self.train_state['model_filename'])\n", + " self.train_state['stop_early'] = False\n", + "\n", + " # Save model if performance improved\n", + " elif self.train_state['epoch_index'] >= 1:\n", + " loss_tm1, loss_t = self.train_state['val_loss'][-2:]\n", + "\n", + " # If loss worsened\n", + " if loss_t >= self.train_state['early_stopping_best_val']:\n", + " # Update step\n", + " self.train_state['early_stopping_step'] += 1\n", + "\n", + " # Loss decreased\n", + " else:\n", + " # Save the best model\n", + " if loss_t < self.train_state['early_stopping_best_val']:\n", + " torch.save(self.model.state_dict(), self.train_state['model_filename'])\n", + "\n", + " # Reset early stopping step\n", + " self.train_state['early_stopping_step'] = 0\n", + "\n", + " # Stop early ?\n", + " self.train_state['stop_early'] = self.train_state['early_stopping_step'] \\\n", + " >= self.train_state['early_stopping_criteria']\n", + " return self.train_state\n", + " \n", + " def compute_accuracy(self, y_pred, y_target):\n", + " _, y_pred_indices = y_pred.max(dim=1)\n", + " n_correct = torch.eq(y_pred_indices, y_target).sum().item()\n", + " return n_correct / len(y_pred_indices) * 100\n", + " \n", + " def pad_word_seq(self, seq, length):\n", + " vector = np.zeros(length, dtype=np.int64)\n", + " vector[:len(seq)] = seq\n", + " vector[len(seq):] = self.dataset.vectorizer.title_word_vocab.mask_index\n", + " return vector\n", + " \n", + " def pad_char_seq(self, seq, seq_length, word_length):\n", + " vector = np.zeros((seq_length, word_length), dtype=np.int64)\n", + " vector.fill(self.dataset.vectorizer.title_char_vocab.mask_index)\n", + " for i in range(len(seq)):\n", + " char_padding = np.zeros(word_length-len(seq[i]), dtype=np.int64)\n", + " vector[i] = np.concatenate((seq[i], char_padding), axis=None)\n", + " return vector\n", + " \n", + " def collate_fn(self, batch):\n", + " \n", + " # Make a deep copy\n", + " batch_copy = copy.deepcopy(batch)\n", + " processed_batch = {\"title_word_vector\": [], \"title_char_vector\": [], \n", + " \"title_length\": [], \"category\": []}\n", + " \n", + " # Max lengths\n", + " get_seq_length = lambda sample: len(sample[\"title_word_vector\"])\n", + " get_word_length = lambda sample: len(sample[\"title_char_vector\"][0])\n", + " max_seq_length = max(map(get_seq_length, batch))\n", + " max_word_length = max(map(get_word_length, batch))\n", + "\n", + "\n", + " # Pad\n", + " for i, sample in enumerate(batch_copy):\n", + " padded_word_seq = self.pad_word_seq(\n", + " sample[\"title_word_vector\"], max_seq_length)\n", + " padded_char_seq = self.pad_char_seq(\n", + " sample[\"title_char_vector\"], max_seq_length, max_word_length)\n", + " processed_batch[\"title_word_vector\"].append(padded_word_seq)\n", + " processed_batch[\"title_char_vector\"].append(padded_char_seq)\n", + " processed_batch[\"title_length\"].append(sample[\"title_length\"])\n", + " processed_batch[\"category\"].append(sample[\"category\"])\n", + " \n", + " # Convert to appropriate tensor types\n", + " processed_batch[\"title_word_vector\"] = torch.LongTensor(\n", + " processed_batch[\"title_word_vector\"])\n", + " processed_batch[\"title_char_vector\"] = torch.LongTensor(\n", + " processed_batch[\"title_char_vector\"])\n", + " processed_batch[\"title_length\"] = torch.LongTensor(\n", + " processed_batch[\"title_length\"])\n", + " processed_batch[\"category\"] = torch.LongTensor(\n", + " processed_batch[\"category\"])\n", + " \n", + " return processed_batch \n", + " \n", + " def run_train_loop(self):\n", + " for epoch_index in range(self.num_epochs):\n", + " self.train_state['epoch_index'] = epoch_index\n", + " \n", + " # Iterate over train dataset\n", + "\n", + " # initialize batch generator, set loss and acc to 0, set train mode on\n", + " self.dataset.set_split('train')\n", + " batch_generator = self.dataset.generate_batches(\n", + " batch_size=self.batch_size, collate_fn=self.collate_fn, \n", + " shuffle=self.shuffle, device=self.device)\n", + " running_loss = 0.0\n", + " running_acc = 0.0\n", + " self.model.train()\n", + "\n", + " for batch_index, batch_dict in enumerate(batch_generator):\n", + " # zero the gradients\n", + " self.optimizer.zero_grad()\n", + " \n", + " # compute the output\n", + " _, y_pred = self.model(x_word=batch_dict['title_word_vector'],\n", + " x_char=batch_dict['title_char_vector'],\n", + " x_lengths=batch_dict['title_length'],\n", + " device=self.device)\n", + " \n", + " # compute the loss\n", + " loss = self.loss_func(y_pred, batch_dict['category'])\n", + " loss_t = loss.item()\n", + " running_loss += (loss_t - running_loss) / (batch_index + 1)\n", + "\n", + " # compute gradients using loss\n", + " loss.backward()\n", + "\n", + " # use optimizer to take a gradient step\n", + " self.optimizer.step()\n", + " \n", + " # compute the accuracy\n", + " acc_t = self.compute_accuracy(y_pred, batch_dict['category'])\n", + " running_acc += (acc_t - running_acc) / (batch_index + 1)\n", + "\n", + " self.train_state['train_loss'].append(running_loss)\n", + " self.train_state['train_acc'].append(running_acc)\n", + "\n", + " # Iterate over val dataset\n", + "\n", + " # initialize batch generator, set loss and acc to 0, set eval mode on\n", + " self.dataset.set_split('val')\n", + " batch_generator = self.dataset.generate_batches(\n", + " batch_size=self.batch_size, collate_fn=self.collate_fn, \n", + " shuffle=self.shuffle, device=self.device)\n", + " running_loss = 0.\n", + " running_acc = 0.\n", + " self.model.eval()\n", + "\n", + " for batch_index, batch_dict in enumerate(batch_generator):\n", + "\n", + " # compute the output\n", + " _, y_pred = self.model(x_word=batch_dict['title_word_vector'],\n", + " x_char=batch_dict['title_char_vector'],\n", + " x_lengths=batch_dict['title_length'],\n", + " device=self.device)\n", + "\n", + " # compute the loss\n", + " loss = self.loss_func(y_pred, batch_dict['category'])\n", + " loss_t = loss.to(\"cpu\").item()\n", + " running_loss += (loss_t - running_loss) / (batch_index + 1)\n", + "\n", + " # compute the accuracy\n", + " acc_t = self.compute_accuracy(y_pred, batch_dict['category'])\n", + " running_acc += (acc_t - running_acc) / (batch_index + 1)\n", + "\n", + " self.train_state['val_loss'].append(running_loss)\n", + " self.train_state['val_acc'].append(running_acc)\n", + "\n", + " self.train_state = self.update_train_state()\n", + " self.scheduler.step(self.train_state['val_loss'][-1])\n", + " if self.train_state['stop_early']:\n", + " break\n", + " \n", + " def run_test_loop(self):\n", + " # initialize batch generator, set loss and acc to 0, set eval mode on\n", + " self.dataset.set_split('test')\n", + " batch_generator = self.dataset.generate_batches(\n", + " batch_size=self.batch_size, collate_fn=self.collate_fn, \n", + " shuffle=self.shuffle, device=self.device)\n", + " running_loss = 0.0\n", + " running_acc = 0.0\n", + " self.model.eval()\n", + "\n", + " for batch_index, batch_dict in enumerate(batch_generator):\n", + " # compute the output\n", + " _, y_pred = self.model(x_word=batch_dict['title_word_vector'],\n", + " x_char=batch_dict['title_char_vector'],\n", + " x_lengths=batch_dict['title_length'],\n", + " device=self.device)\n", + "\n", + " # compute the loss\n", + " loss = self.loss_func(y_pred, batch_dict['category'])\n", + " loss_t = loss.item()\n", + " running_loss += (loss_t - running_loss) / (batch_index + 1)\n", + "\n", + " # compute the accuracy\n", + " acc_t = self.compute_accuracy(y_pred, batch_dict['category'])\n", + " running_acc += (acc_t - running_acc) / (batch_index + 1)\n", + "\n", + " self.train_state['test_loss'] = running_loss\n", + " self.train_state['test_acc'] = running_acc\n", + " \n", + " def plot_performance(self):\n", + " # Figure size\n", + " plt.figure(figsize=(15,5))\n", + "\n", + " # Plot Loss\n", + " plt.subplot(1, 2, 1)\n", + " plt.title(\"Loss\")\n", + " plt.plot(trainer.train_state[\"train_loss\"], label=\"train\")\n", + " plt.plot(trainer.train_state[\"val_loss\"], label=\"val\")\n", + " plt.legend(loc='upper right')\n", + "\n", + " # Plot Accuracy\n", + " plt.subplot(1, 2, 2)\n", + " plt.title(\"Accuracy\")\n", + " plt.plot(trainer.train_state[\"train_acc\"], label=\"train\")\n", + " plt.plot(trainer.train_state[\"val_acc\"], label=\"val\")\n", + " plt.legend(loc='lower right')\n", + "\n", + " # Save figure\n", + " plt.savefig(os.path.join(self.save_dir, \"performance.png\"))\n", + "\n", + " # Show plots\n", + " plt.show()\n", + " \n", + " def save_train_state(self):\n", + " with open(os.path.join(self.save_dir, \"train_state.json\"), \"w\") as fp:\n", + " json.dump(self.train_state, fp)" + ], + "execution_count": 0, + "outputs": [] + }, + { + "metadata": { + "id": "ICkiOaGtFlk-", + "colab_type": "code", + "outputId": "18174034-ce3e-444a-a968-aba51eb03b3e", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 306 + } + }, + "cell_type": "code", + "source": [ + "# Initialization\n", + "dataset = NewsDataset.load_dataset_and_make_vectorizer(args.split_data_file,\n", + " args.cutoff)\n", + "dataset.save_vectorizer(args.vectorizer_file)\n", + "vectorizer = dataset.vectorizer\n", + "model = NewsModel(embedding_dim=args.embedding_dim, \n", + " num_word_embeddings=len(vectorizer.title_word_vocab), \n", + " num_char_embeddings=len(vectorizer.title_char_vocab),\n", + " kernels=args.kernels,\n", + " num_input_channels=args.embedding_dim,\n", + " num_output_channels=args.num_filters,\n", + " rnn_hidden_dim=args.rnn_hidden_dim,\n", + " hidden_dim=args.hidden_dim,\n", + " output_dim=len(vectorizer.category_vocab),\n", + " num_layers=args.num_layers,\n", + " bidirectional=args.bidirectional,\n", + " dropout_p=args.dropout_p, \n", + " word_padding_idx=vectorizer.title_word_vocab.mask_index,\n", + " char_padding_idx=vectorizer.title_char_vocab.mask_index)\n", + "print (model.named_modules)" + ], + "execution_count": 149, + "outputs": [ + { + "output_type": "stream", + "text": [ + "\n" + ], + "name": "stdout" + } + ] + }, + { + "metadata": { + "id": "tuaRZ4DiFlh1", + "colab_type": "code", + "outputId": "6496aa05-de58-4913-a56a-9885bd60d9ad", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 102 + } + }, + "cell_type": "code", + "source": [ + "# Train\n", + "trainer = Trainer(dataset=dataset, model=model, \n", + " model_state_file=args.model_state_file, \n", + " save_dir=args.save_dir, device=args.device,\n", + " shuffle=args.shuffle, num_epochs=args.num_epochs, \n", + " batch_size=args.batch_size, learning_rate=args.learning_rate, \n", + " early_stopping_criteria=args.early_stopping_criteria)\n", + "trainer.run_train_loop()" + ], + "execution_count": 150, + "outputs": [ + { + "output_type": "stream", + "text": [ + "[EPOCH]: 00 | [LR]: 0.001 | [TRAIN LOSS]: 0.78 | [TRAIN ACC]: 68.6% | [VAL LOSS]: 0.58 | [VAL ACC]: 78.5%\n", + "[EPOCH]: 01 | [LR]: 0.001 | [TRAIN LOSS]: 0.50 | [TRAIN ACC]: 82.0% | [VAL LOSS]: 0.48 | [VAL ACC]: 83.2%\n", + "[EPOCH]: 02 | [LR]: 0.001 | [TRAIN LOSS]: 0.43 | [TRAIN ACC]: 84.6% | [VAL LOSS]: 0.47 | [VAL ACC]: 83.1%\n", + "[EPOCH]: 03 | [LR]: 0.001 | [TRAIN LOSS]: 0.39 | [TRAIN ACC]: 86.2% | [VAL LOSS]: 0.46 | [VAL ACC]: 83.7%\n", + "[EPOCH]: 04 | [LR]: 0.001 | [TRAIN LOSS]: 0.35 | [TRAIN ACC]: 87.4% | [VAL LOSS]: 0.44 | [VAL ACC]: 84.2%\n" + ], + "name": "stdout" + } + ] + }, + { + "metadata": { + "id": "mzRJIz88Flfe", + "colab_type": "code", + "outputId": "dece6240-57ab-4abc-f9cc-ecd11dabcdc6", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 335 + } + }, + "cell_type": "code", + "source": [ + "# Plot performance\n", + "trainer.plot_performance()" + ], + "execution_count": 151, + "outputs": [ + { + "output_type": "display_data", + "data": { + "image/png": "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\nxIcQGxGI0aCCzVs1tbTz3pYTBPiaWTgp1t1xRES8xty5C/jd757n9tvvYOvWLTz44I958801vPHG\nGtra2vD19R2wLCrQRESucqE2H2akRTEjLQqAytpmjjinRFbx1bFyvjpWDnRNn0yJC+GaiTGMHRaq\nYs3L/O3LfOqb2rh1bhJ+PjrEi4h3um340l67Xa6QlJRMRUUZJSXF1NXV8cUXmxkyJIInnvjfHD58\nkN/97vkBy6J3bxGRQSYsyJeZY6KYOaarYCuvaepadCSna9GR3UfL2H20jF/9cAYRof5uTiuX4kB2\nJTZ/C9eqeyYicslmzryGP/zhJebMmUd1dRXJySMA2LJlE+3t7QOWQwWaiMggNyTYjyFj/Zg9NhqA\nsuomjFYz4f4WNyeTS/WDpaMJDvHH1Nnp7igiIl5n3rwF/PM/f5//+q83aG5u4qmnnmTTps+4/fY7\n+OyzT/noo/UDkkMFmoiI9GAP8cNut1FWVufuKHKJwoJ8sYcH6L+diMhlGDUqjS1bMpz3X399rfP2\nNdfMA+Cmm75BQEAAjY2ue5/VxXFEREREREQ8hAo0ERERERERD6ECTURERERExEOoQBMREREREfEQ\nKtBEREREREQ8hAo0ERERERERD6ECTURERERExEOoQBMREREREfEQKtBEREREREQ8hAo0ERERERER\nD2FwOBwOd4cQERERERERddBEREREREQ8hgo0ERERERERD6ECTURERERExEOoQBMREREREfEQKtBE\nREREREQ8hAo0ERERERERD2F2d4C+PP300+zduxeDwcCKFSsYN26c87nt27fz3HPPYTKZmDt3Lg88\n8IAbk/aedeHChURFRWEymQBYtWoVkZGR7ooKwNGjR7n//vv53ve+x913393jOU8b296yetrY/vrX\nv2b37t20t7fzwx/+kMWLFzuf87Rx7S2rJ41rU1MTjz32GBUVFbS0tHD//fezYMEC5/OeNK59ZfWk\ncT1bc3MzS5cu5f777+e2225zPu5JYys9edPxEbzrGKnjo+voGNn/dIx0LbccHx0eLCMjw/FP//RP\nDofD4Th+/Ljjjjvu6PH8DTfc4CgsLHR0dHQ47rrrLsexY8fcEdPhcPSddcGCBY76+np3RDuvhoYG\nx9133+14/PHHHWvWrPna8540tn1l9aSxTU9Pd/zgBz9wOBwOR2VlpWPevHk9nvekce0rqyeN60cf\nfeT4wx/+4HA4HI78/HzH4sWLezzvSePaV1ZPGtezPffcc47bbrvN8e677/Z43JPGVs7wpuOjw+Fd\nx0gdH11Hx0jX0DHStdxxfPToKY7p6elcd911ACQnJ1NTU0N9fT0AeXl5BAcHEx0djdFoZN68eaSn\np3tkVk9ktVp59dVXiYiI+Npznja2vWX1NFOnTuU3v/kNAEFBQTQ1NdHR0QF43rj2ltXT3Hjjjfzj\nP/4jAEVFRT2+TfO0ce0tq6c6ceIEx48fZ/78+T0e97SxlTO86fgI3nWM1PHRdXSMdA0dI13HXcdH\nj57iWF5eTlpamvN+WFgYZWVlBAYGUlZWRlhYWI/n8vLy3BET6D3raU8++SQFBQVMnjyZRx55BIPB\n4I6oAJjNZszm8//n97Sx7S3raZ4ytiaTCX9/fwDWrl3L3LlznW16TxvX3rKe5injetqdd95JcXEx\nr7zyivMxTxvX086X9TRPG9dnn32WJ554gvfee6/H4546tuJdx0fwrmOkjo+uo2Oka+kY2f/cdXz0\n6ALtXA6Hw90RLtq5WR966CHmzJlDcHAwDzzwABs3bmTJkiVuSnd18cSx/eyzz1i7di1//OMf3Zrj\nYlwoqyeO65tvvsmhQ4f46U9/yvr1691+MOzNhbJ62ri+9957TJgwgbi4OLdlkCvnTcdH0DFyoHjq\nuOoY6Ro6RvYvdx4fPXqKY0REBOXl5c77paWl2O328z5XUlLi1hZ/b1kBbrnlFsLDwzGbzcydO5ej\nR4+6I+ZF8bSx7Yunje0XX3zBK6+8wquvvorNZnM+7onjeqGs4FnjmpWVRVFREQCjRo2io6ODyspK\nwPPGtbes4FnjCrB582Y+//xz7rjjDt555x1eeukltm/fDnje2MoZ3nR8hKvnGOmJY9sbTxxXHSP7\nn46RruHO46NHF2izZ89m48aNABw4cICIiAjndIjY2Fjq6+vJz8+nvb2dTZs2MXv2bI/MWldXx733\n3ktraysAO3fuZMSIEW7L2hdPG9veeNrY1tXV8etf/5rf//73hISE9HjO08a1t6yeNq67du1yfntZ\nXl5OY2MjoaGhgOeNa29ZPW1cAZ5//nneffdd3n77bZYtW8b999/PrFmzAM8bWznDm46PcPUcIz1x\nbC/EE8dVx0jX0DHSNdx5fDQ4PHxexKpVq9i1axcGg4Enn3ySgwcPYrPZWLRoETt37mTVqlUALF68\nmHvvvddjs/75z3/mvffew8fHh9GjR/PEE0+4tfWclZXFs88+S0FBAWazmcjISBYuXEhsbKzHjW1f\nWT1pbN966y1eeOEFEhMTnY9Nnz6dlJQUjxvXvrJ60rg2Nzfz85//nKKiIpqbm3nwwQeprq72yPeC\nvrJ60rie64UXXiAmJgbAI8dWevKm4yN4zzFSx0fX0THSNXSMdL2BPj56fIEmIiIiIiIyWHj0FEcR\nEREREZHBRAWaiIiIiIiIh1CBJiIiIiIi4iFUoImIiIiIiHgIFWgiIiIiIiIeQgWaiIiIiIiIh1CB\nJiIiIiIi4iFUoImIiIiIiHiI/w/8BGbATisZAgAAAABJRU5ErkJggg==\n", + "text/plain": [ + "" + ] + }, + "metadata": { + "tags": [] + } + } + ] + }, + { + "metadata": { + "id": "4EmFhiX-FMaV", + "colab_type": "code", + "outputId": "29ef6d38-6258-429b-841f-7345b7cd0695", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 51 + } + }, + "cell_type": "code", + "source": [ + "# Test performance\n", + "trainer.run_test_loop()\n", + "print(\"Test loss: {0:.2f}\".format(trainer.train_state['test_loss']))\n", + "print(\"Test Accuracy: {0:.1f}%\".format(trainer.train_state['test_acc']))" + ], + "execution_count": 152, + "outputs": [ + { + "output_type": "stream", + "text": [ + "Test loss: 0.44\n", + "Test Accuracy: 84.4%\n" + ], + "name": "stdout" + } + ] + }, + { + "metadata": { + "id": "zVU1zakYFMVF", + "colab_type": "code", + "colab": {} + }, + "cell_type": "code", + "source": [ + "# Save all results\n", + "trainer.save_train_state()" + ], + "execution_count": 0, + "outputs": [] + }, + { + "metadata": { + "id": "qLoKfjSpFw7t", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "## Inference" + ] + }, + { + "metadata": { + "id": "ANrPcS7Hp_CP", + "colab_type": "code", + "colab": {} + }, + "cell_type": "code", + "source": [ + "class Inference(object):\n", + " def __init__(self, model, vectorizer):\n", + " self.model = model\n", + " self.vectorizer = vectorizer\n", + " \n", + " def predict_category(self, title):\n", + " # Vectorize\n", + " word_vector, char_vector, title_length = self.vectorizer.vectorize(title)\n", + " title_word_vector = torch.tensor(word_vector).unsqueeze(0)\n", + " title_char_vector = torch.tensor(char_vector).unsqueeze(0)\n", + " title_length = torch.tensor([title_length]).long() \n", + " \n", + " # Forward pass\n", + " self.model.eval()\n", + " attn_scores, y_pred = self.model(x_word=title_word_vector, \n", + " x_char=title_char_vector,\n", + " x_lengths=title_length, \n", + " device=\"cpu\",\n", + " apply_softmax=True)\n", + "\n", + " # Top category\n", + " y_prob, indices = y_pred.max(dim=1)\n", + " index = indices.item()\n", + "\n", + " # Predicted category\n", + " category = vectorizer.category_vocab.lookup_index(index)\n", + " probability = y_prob.item()\n", + " return {'category': category, 'probability': probability, \n", + " 'attn_scores': attn_scores}\n", + " \n", + " def predict_top_k(self, title, k):\n", + " # Vectorize\n", + " word_vector, char_vector, title_length = self.vectorizer.vectorize(title)\n", + " title_word_vector = torch.tensor(word_vector).unsqueeze(0)\n", + " title_char_vector = torch.tensor(char_vector).unsqueeze(0)\n", + " title_length = torch.tensor([title_length]).long()\n", + " \n", + " # Forward pass\n", + " self.model.eval()\n", + " _, y_pred = self.model(x_word=title_word_vector,\n", + " x_char=title_char_vector,\n", + " x_lengths=title_length, \n", + " device=\"cpu\",\n", + " apply_softmax=True)\n", + " \n", + " # Top k categories\n", + " y_prob, indices = torch.topk(y_pred, k=k)\n", + " probabilities = y_prob.detach().numpy()[0]\n", + " indices = indices.detach().numpy()[0]\n", + "\n", + " # Results\n", + " results = []\n", + " for probability, index in zip(probabilities, indices):\n", + " category = self.vectorizer.category_vocab.lookup_index(index)\n", + " results.append({'category': category, 'probability': probability})\n", + "\n", + " return results" + ], + "execution_count": 0, + "outputs": [] + }, + { + "metadata": { + "id": "W6wr68o2p_Eh", + "colab_type": "code", + "outputId": "87886e24-350d-433e-981d-b2907b0c95cf", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 306 + } + }, + "cell_type": "code", + "source": [ + "# Load the model\n", + "dataset = NewsDataset.load_dataset_and_load_vectorizer(\n", + " args.split_data_file, args.vectorizer_file)\n", + "vectorizer = dataset.vectorizer\n", + "model = NewsModel(embedding_dim=args.embedding_dim, \n", + " num_word_embeddings=len(vectorizer.title_word_vocab), \n", + " num_char_embeddings=len(vectorizer.title_char_vocab),\n", + " kernels=args.kernels,\n", + " num_input_channels=args.embedding_dim,\n", + " num_output_channels=args.num_filters,\n", + " rnn_hidden_dim=args.rnn_hidden_dim,\n", + " hidden_dim=args.hidden_dim,\n", + " output_dim=len(vectorizer.category_vocab),\n", + " num_layers=args.num_layers,\n", + " bidirectional=args.bidirectional,\n", + " dropout_p=args.dropout_p, \n", + " word_padding_idx=vectorizer.title_word_vocab.mask_index,\n", + " char_padding_idx=vectorizer.title_char_vocab.mask_index)\n", + "model.load_state_dict(torch.load(args.model_state_file))\n", + "model = model.to(\"cpu\")\n", + "print (model.named_modules)" + ], + "execution_count": 155, + "outputs": [ + { + "output_type": "stream", + "text": [ + "\n" + ], + "name": "stdout" + } + ] + }, + { + "metadata": { + "id": "JPKgHxsfN954", + "colab_type": "code", + "outputId": "0445e3a7-24a9-4c77-829d-a25681768ab1", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 51 + } + }, + "cell_type": "code", + "source": [ + "# Inference\n", + "inference = Inference(model=model, vectorizer=vectorizer)\n", + "title = input(\"Enter a title to classify: \")\n", + "prediction = inference.predict_category(preprocess_text(title))\n", + "print(\"{} → {} (p={:0.2f})\".format(title, prediction['category'], \n", + " prediction['probability']))" + ], + "execution_count": 158, + "outputs": [ + { + "output_type": "stream", + "text": [ + "Enter a title to classify: Sale of Apple's new iphone are skyrocketing.\n", + "Sale of Apple's new iphone are skyrocketing. → Sci/Tech (p=0.86)\n" + ], + "name": "stdout" + } + ] + }, + { + "metadata": { + "id": "JRdz4wzuQR4N", + "colab_type": "code", + "outputId": "f2c91b24-a36a-4e35-b06a-f6618497d64f", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 102 + } + }, + "cell_type": "code", + "source": [ + "# Top-k inference\n", + "top_k = inference.predict_top_k(preprocess_text(title), k=len(vectorizer.category_vocab))\n", + "print (\"{}: \".format(title))\n", + "for result in top_k:\n", + " print (\"{} (p={:0.2f})\".format(result['category'], \n", + " result['probability']))" + ], + "execution_count": 159, + "outputs": [ + { + "output_type": "stream", + "text": [ + "Sale of Apple's new iphone are skyrocketing.: \n", + "Sci/Tech (p=0.86)\n", + "Business (p=0.12)\n", + "World (p=0.01)\n", + "Sports (p=0.00)\n" + ], + "name": "stdout" + } + ] + }, + { + "metadata": { + "id": "R3jrZ6ZkxN4r", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "# Interpretability" + ] + }, + { + "metadata": { + "id": "qrAieHoHxOt2", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "We can inspect the probability vector that is generated at each time step to visualize the importance of each of the previous hidden states towards a particular time step's prediction. " + ] + }, + { + "metadata": { + "id": "k6uZY4J8vYgw", + "colab_type": "code", + "colab": {} + }, + "cell_type": "code", + "source": [ + "import seaborn as sns\n", + "import matplotlib.pyplot as plt" + ], + "execution_count": 0, + "outputs": [] + }, + { + "metadata": { + "id": "2PNuY7GLoEi4", + "colab_type": "code", + "outputId": "24b2e48f-da5b-4251-c2eb-81e72603a6f4", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 330 + } + }, + "cell_type": "code", + "source": [ + "attn_matrix = prediction['attn_scores'].detach().numpy()\n", + "ax = sns.heatmap(attn_matrix, linewidths=2, square=True)\n", + "tokens = [\"\"]+preprocess_text(title).split(\" \")+[\"\"]\n", + "ax.set_xticklabels(tokens, rotation=45)\n", + "ax.set_xlabel(\"Token\")\n", + "ax.set_ylabel(\"Importance\\n\")\n", + "plt.show()" + ], + "execution_count": 0, + "outputs": [ + { + "output_type": "display_data", + "data": { + "image/png": "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\n", + "text/plain": [ + "" + ] + }, + "metadata": { + "tags": [] + } + } + ] + }, + { + "metadata": { + "id": "1YHneO3SStOp", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "# TODO" + ] + }, + { + "metadata": { + "id": "gGHaKTe1SuEk", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "- attn visualization isn't always great\n", + "- bleu score\n", + "- ngram-overlap\n", + "- perplexity\n", + "- beamsearch\n", + "- hierarchical softmax\n", + "- hierarchical attention\n", + "- Transformer networks\n", + "- attention interpretability is hit/miss\n" + ] + } + ] +} \ No newline at end of file diff --git a/notebooks/15_Computer_Vision.ipynb b/notebooks/15_Computer_Vision.ipynb new file mode 100644 index 0000000..7affe3d --- /dev/null +++ b/notebooks/15_Computer_Vision.ipynb @@ -0,0 +1,2187 @@ +{ + "nbformat": 4, + "nbformat_minor": 0, + "metadata": { + "colab": { + "name": "15_Computer_Vision", + "version": "0.3.2", + "provenance": [], + "collapsed_sections": [], + "toc_visible": true + }, + "kernelspec": { + "name": "python3", + "display_name": "Python 3" + }, + "accelerator": "GPU" + }, + "cells": [ + { + "metadata": { + "id": "bOChJSNXtC9g", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "# Computer Vision" + ] + }, + { + "metadata": { + "id": "OLIxEDq6VhvZ", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "\n", + "\n", + "In this notebook we're going to cover the basics of computer vision using CNNs. So far we've explored using CNNs for text but their initial origin began with computer vision tasks.\n", + "\n", + "\n" + ] + }, + { + "metadata": { + "id": "wKX2R_FT4hSQ", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "" + ] + }, + { + "metadata": { + "id": "zOUWqHjL6hmU", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "# Set up" + ] + }, + { + "metadata": { + "id": "kjXAaAyx6i5W", + "colab_type": "code", + "colab": {} + }, + "cell_type": "code", + "source": [ + "!pip3 install torch torchvision\n", + "!pip install Pillow==4.0.0\n", + "!pip install PIL\n", + "!pip install image" + ], + "execution_count": 0, + "outputs": [] + }, + { + "metadata": { + "id": "vXjCadon6toa", + "colab_type": "code", + "colab": {} + }, + "cell_type": "code", + "source": [ + "import os\n", + "from argparse import Namespace\n", + "import collections\n", + "import json\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import pandas as pd\n", + "from PIL import Image\n", + "import re\n", + "import torch" + ], + "execution_count": 0, + "outputs": [] + }, + { + "metadata": { + "id": "N518ySE16trp", + "colab_type": "code", + "colab": {} + }, + "cell_type": "code", + "source": [ + "# Set Numpy and PyTorch seeds\n", + "def set_seeds(seed, cuda):\n", + " np.random.seed(seed)\n", + " torch.manual_seed(seed)\n", + " if cuda:\n", + " torch.cuda.manual_seed_all(seed)\n", + " \n", + "# Creating directories\n", + "def create_dirs(dirpath):\n", + " if not os.path.exists(dirpath):\n", + " os.makedirs(dirpath)" + ], + "execution_count": 0, + "outputs": [] + }, + { + "metadata": { + "id": "KG_bcOZ58vhB", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "# Data" + ] + }, + { + "metadata": { + "id": "PGQLzyss8wja", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "We're going to first get some data. A popular computer vision classification dataset is [CIFAR10](https://www.cs.toronto.edu/~kriz/cifar.html) which contains images from ten unique classes." + ] + }, + { + "metadata": { + "id": "NYy0WlkB9AoK", + "colab_type": "code", + "colab": {} + }, + "cell_type": "code", + "source": [ + "# Don't worry we aren't using tensorflow, just using it to get some data\n", + "import tensorflow as tf\n", + "import matplotlib.pyplot as plt" + ], + "execution_count": 0, + "outputs": [] + }, + { + "metadata": { + "id": "Ka-WxeEJ8vAd", + "colab_type": "code", + "outputId": "ec24e935-0562-4c55-c2ab-2bfe59a49128", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 85 + } + }, + "cell_type": "code", + "source": [ + "# Load data and combine\n", + "(x_train, y_train), (x_test, y_test) = tf.keras.datasets.cifar10.load_data()\n", + "X = np.vstack([x_train, x_test])\n", + "y = np.vstack([y_train, y_test]).squeeze(1)\n", + "print (\"x:\", X.shape)\n", + "print (\"y:\", y.shape)" + ], + "execution_count": 8, + "outputs": [ + { + "output_type": "stream", + "text": [ + "Downloading data from https://www.cs.toronto.edu/~kriz/cifar-10-python.tar.gz\n", + "170500096/170498071 [==============================] - 50s 0us/step\n", + "x: (60000, 32, 32, 3)\n", + "y: (60000,)\n" + ], + "name": "stdout" + } + ] + }, + { + "metadata": { + "id": "0YIiwLWcBH07", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "Each image has length 32, width 32 and three color channels (RGB). We are going to save these images in a directory. Each image will have it's own directory (name will be the class)." + ] + }, + { + "metadata": { + "colab_type": "code", + "id": "xWqzC-M1NCzx", + "colab": {} + }, + "cell_type": "code", + "source": [ + "!rm -rf cifar10_data" + ], + "execution_count": 0, + "outputs": [] + }, + { + "metadata": { + "id": "AdZjOciC-Bzm", + "colab_type": "code", + "colab": {} + }, + "cell_type": "code", + "source": [ + "# Classes\n", + "classes = {0: 'plane', 1: 'car', 2: 'bird', 3: 'cat', 4: 'deer', 5: 'dog', \n", + " 6: 'frog', 7: 'horse', 8: 'ship', 9: 'truck'}" + ], + "execution_count": 0, + "outputs": [] + }, + { + "metadata": { + "id": "DbNtoIxD8dxc", + "colab_type": "code", + "colab": {} + }, + "cell_type": "code", + "source": [ + "# Create image directories\n", + "data_dir = \"cifar10_data\"\n", + "os.mkdir(data_dir)\n", + "for _class in classes.values():\n", + " os.mkdir(os.path.join(data_dir, _class))" + ], + "execution_count": 0, + "outputs": [] + }, + { + "metadata": { + "id": "wf5EY4Ey8kFq", + "colab_type": "code", + "colab": {} + }, + "cell_type": "code", + "source": [ + "# Save images for each class\n", + "for i, (image, label) in enumerate(zip(X, y)):\n", + " _class = classes[label]\n", + " im = Image.fromarray(image)\n", + " im.save(os.path.join(data_dir, _class, \"{0:02d}.png\".format(i)))" + ], + "execution_count": 0, + "outputs": [] + }, + { + "metadata": { + "id": "PrD2oUFu_KVF", + "colab_type": "code", + "outputId": "e336f8c8-5cf5-4235-ec10-68761b6c725c", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 101 + } + }, + "cell_type": "code", + "source": [ + "# Visualize some samples\n", + "num_samples = len(classes)\n", + "for i, _class in enumerate(classes.values()): \n", + " for file in os.listdir(os.path.join(data_dir, _class)):\n", + " if file.endswith(\".png\"):\n", + " plt.subplot(1, num_samples, i+1)\n", + " plt.title(\"{0}\".format(_class))\n", + " img = Image.open(os.path.join(data_dir, _class, file))\n", + " plt.imshow(img)\n", + " plt.axis(\"off\")\n", + " break" + ], + "execution_count": 14, + "outputs": [ + { + "output_type": "display_data", + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAd8AAABUCAYAAADDAD33AAAABHNCSVQICAgIfAhkiAAAAAlwSFlz\nAAALEgAACxIB0t1+/AAAADl0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uIDIuMS4yLCBo\ndHRwOi8vbWF0cGxvdGxpYi5vcmcvNQv5yAAAIABJREFUeJzsvXm4XVV9//9aa+29z3zumHtv5puE\nTAQpiFgHMCQINwFRDGgtBae29qnap0i1aMXqY621Po/a1mprbX9Pa60jUpywSouggiAQ5iGEhISb\n5CY3ufOZ9rDW+v2x9jlJlNxL/cKNbc/7efLknnP22eez1/6sz/z5bGGttbTRRhtttNFGG/MGebIJ\naKONNtpoo43/a2gr3zbaaKONNtqYZ7SVbxtttNFGG23MM9rKt4022mijjTbmGW3l20YbbbTRRhvz\njLbybaONNtpoo415xq+c8n3ve9/LZz/72ZNNxv963HXXXVxwwQW/8P4nPvEJvvzlLz+rc1xwwQXc\nddddzzVpzzmiKOLGG288qTTcc889bN68+aTS8P+C/wn0v/vd72bjxo38+Mc/PtmknBD79u3j1FNP\nPdlkPOeY7bq++MUv8ld/9VfzTNFRfO1rX3tOzvNc3zvvOTtTG/8r8Ed/9Ecnm4TnHI8++ig33ngj\nl1566ckmpY3nEd/97nf5/ve/z7Jly042KW0cgyuvvPKk/bbWmo9//OO8/vWvP2k0nAgnzfO96667\nuOSSS/jYxz7G0NAQmzdv5v777z/umPvuu49t27axZcsWLrroIu644w7AWSDnnHMOX/jCF7jkkks4\n99xzuemmmwCw1vK3f/u3DA0NsWnTJj7ykY+gtZ6Xa7rxxhsZGhpiaGiI97znPURRxNe//nW2bt3K\nhRdeyG/91m+xf/9+AG644Qbe+c538qY3vYmPf/zj80LfM+Ev//IvGRoaYsuWLWzfvv24yMPmzZtb\na3ngwAEefvhhLr74YoaGhvjoRz960miGZ7/WR44c4Z3vfCf3338/V1xxxbzS+NnPfpaNGzdy6aWX\ntng3iiI+8pGPtHj+7//+71vHP/nkk1x55ZUMDQ1xySWX8NBDDwFur7zhDW/gD//wD+fVOHom+sMw\n5E//9E8ZGhpi69atfOxjH2vtrx//+Mds3LiRrVu38tWvfpUXvvCF7Nu3b15oveqqqzDG8Nu//dtc\nfvnlfOpTn2Lr1q1s376dyclJ/vAP/5ChoSEuuugi/uEf/qH1vRtuuIGXv/zlvPrVr+aGG25g7dq1\n80Lv9ddfzyWXXMLGjRv5zne+gzGGT33qU2zZsoUtW7bw3ve+l1qt1rq2Y6/nZz/7Ga997Wu56KKL\n2Lp1K9/73vcAmJ6e5j3veQ9DQ0Ocf/75fOMb33heaE+ShPe///0MDQ1xwQUX8M53vpNKpfKM1wXw\n6U9/mve///2Akymf//zn2bZtGy95yUued4/4LW95CzMzM2zZsoVNmzYdt45XXXUV3/zmN1vHHvv6\nRz/6UUvW/d7v/R6Tk5O/cO53v/vd/Nmf/dkvT5w9Sbjzzjvt+vXr7Xe/+11rrbVf+9rX7Gte8xp7\n7bXX2s985jPWWmtf9apX2e985zvWWmv//d//3b7yla+01lo7PDxsTz31VPuv//qv1lprb7rpJnvB\nBRe0jrv44ovt9PS0jePYvu1tb2sd93xieHjYvuQlL7EHDx60xhj7jne8w37uc5+zp512mh0ZGbHW\nWvve977X/smf/Im11tpvfOMb9owzzrBPPfXU807bM6G5/s31/epXv/oL679p0yZ73XXXtb5z2WWX\n2a985SvWWrfm69ats3feeee80/7LrPWb3vSmeaVx586d9uyzz7aHDx+2SZLYt7/97XbTpk32b//2\nb+2b3vQmG4ahrVar9tJLL7W33HKL1VrbCy+80H7ta1+z1lp7zz332HPOOcfGcWzvvPNO+4IXvMDe\ncccdJ53+z33uc/Z3f/d3bRzHtl6v28suu8zeeOONNkkS+7KXvczeeuut1lprP/axj9l169bZ4eHh\neaN5zZo1dmRkxF555ZX2rW99q9VaW2ut/cAHPmA/8IEPWGutnZiYsOedd569++677cTEhD399NPt\njh07rNbavutd77Jr1qx5XmkcHh62a9eutV/60pestdZ+73vfs+eff779zne+Yy+99FJbrVZtkiT2\n93//91v78OevZ9u2bfauu+6y1lr71FNP2WuuucZaa+373vc++8d//MdWa23Hxsbsxo0b7Y4dO57z\na/jhD39o3/jGN1pjjDXG2E996lP2y1/+8jNel7XW/s3f/E1rL27atMm+/e1vt0mS2CNHjtizzz7b\nPvbYY885jU0MDw/b9evXW2t/cR2vvPJKe+ONN7aObb6uVqv2xS9+cWvtPvKRj9gPfehDx52ruQ+S\n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\n", + "text/plain": [ + "" + ] + }, + "metadata": { + "tags": [] + } + } + ] + }, + { + "metadata": { + "id": "IgvzGk0S4wPY", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "# Classification" + ] + }, + { + "metadata": { + "id": "gVOkpS6O5b-B", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "Our task will be to classify the class given the image. We're going to architect a basic CNN to process the input images and produce a classification." + ] + }, + { + "metadata": { + "id": "Y5C78l1j5UTm", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "### Arguments" + ] + }, + { + "metadata": { + "id": "yLW2_1CG2Eyg", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "With image data, we won't be save our split data files. We will only read from the image directory." + ] + }, + { + "metadata": { + "id": "RTMvq5A849-w", + "colab_type": "code", + "outputId": "f9142a73-fbf6-4504-f064-70417c6f8e6d", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 34 + } + }, + "cell_type": "code", + "source": [ + "args = Namespace(\n", + " seed=1234,\n", + " cuda=True,\n", + " shuffle=True,\n", + " data_dir=\"cifar10_data\",\n", + " vectorizer_file=\"vectorizer.json\",\n", + " model_state_file=\"model.pth\",\n", + " save_dir=\"cifar10_model\",\n", + " train_size=0.7,\n", + " val_size=0.15,\n", + " test_size=0.15,\n", + " num_epochs=10,\n", + " early_stopping_criteria=5,\n", + " learning_rate=1e-3,\n", + " batch_size=128,\n", + " num_filters=100,\n", + " hidden_dim=100,\n", + " dropout_p=0.1,\n", + ")\n", + "\n", + "# Set seeds\n", + "set_seeds(seed=args.seed, cuda=args.cuda)\n", + "\n", + "# Create save dir\n", + "create_dirs(args.save_dir)\n", + "\n", + "# Expand filepaths\n", + "args.vectorizer_file = os.path.join(args.save_dir, args.vectorizer_file)\n", + "args.model_state_file = os.path.join(args.save_dir, args.model_state_file)\n", + "\n", + "# Check CUDA\n", + "if not torch.cuda.is_available():\n", + " args.cuda = False\n", + "args.device = torch.device(\"cuda\" if args.cuda else \"cpu\")\n", + "print(\"Using CUDA: {}\".format(args.cuda))" + ], + "execution_count": 17, + "outputs": [ + { + "output_type": "stream", + "text": [ + "Using CUDA: True\n" + ], + "name": "stdout" + } + ] + }, + { + "metadata": { + "id": "xaYCCEHOrpGB", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "### Data" + ] + }, + { + "metadata": { + "id": "8iF6nxgDtOWk", + "colab_type": "code", + "colab": {} + }, + "cell_type": "code", + "source": [ + "# Convert image file to NumPy array\n", + "def img_to_array(fp):\n", + " img = Image.open(fp)\n", + " array = np.asarray(img, dtype=\"float32\")\n", + " return array" + ], + "execution_count": 0, + "outputs": [] + }, + { + "metadata": { + "id": "3VlHdV9r5VzN", + "colab_type": "code", + "colab": {} + }, + "cell_type": "code", + "source": [ + "# Load data\n", + "data = []\n", + "for i, _class in enumerate(classes.values()): \n", + " for file in os.listdir(os.path.join(data_dir, _class)):\n", + " if file.endswith(\".png\"):\n", + " full_filepath = os.path.join(data_dir, _class, file)\n", + " data.append({\"image\": img_to_array(full_filepath), \"category\": _class})" + ], + "execution_count": 0, + "outputs": [] + }, + { + "metadata": { + "id": "WvknlOjM5V1z", + "colab_type": "code", + "outputId": "69e2f3bf-42df-4b08-b086-fc744e263db8", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 221 + } + }, + "cell_type": "code", + "source": [ + "# Convert to Pandas DataFrame\n", + "df = pd.DataFrame(data)\n", + "print (\"Image shape:\", df.image[0].shape)\n", + "df.head()" + ], + "execution_count": 20, + "outputs": [ + { + "output_type": "stream", + "text": [ + "Image shape: (32, 32, 3)\n" + ], + "name": "stdout" + }, + { + "output_type": "execute_result", + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
categoryimage
0plane[[[160.0, 173.0, 167.0], [151.0, 164.0, 155.0]...
1plane[[[190.0, 228.0, 243.0], [188.0, 223.0, 238.0]...
2plane[[[255.0, 255.0, 255.0], [253.0, 254.0, 251.0]...
3plane[[[193.0, 216.0, 227.0], [191.0, 213.0, 225.0]...
4plane[[[234.0, 234.0, 234.0], [231.0, 231.0, 231.0]...
\n", + "
" + ], + "text/plain": [ + " category image\n", + "0 plane [[[160.0, 173.0, 167.0], [151.0, 164.0, 155.0]...\n", + "1 plane [[[190.0, 228.0, 243.0], [188.0, 223.0, 238.0]...\n", + "2 plane [[[255.0, 255.0, 255.0], [253.0, 254.0, 251.0]...\n", + "3 plane [[[193.0, 216.0, 227.0], [191.0, 213.0, 225.0]...\n", + "4 plane [[[234.0, 234.0, 234.0], [231.0, 231.0, 231.0]..." + ] + }, + "metadata": { + "tags": [] + }, + "execution_count": 20 + } + ] + }, + { + "metadata": { + "id": "GXtRpahp5V6p", + "colab_type": "code", + "outputId": "6ea08026-d651-4a75-d40f-94086fb211ce", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 187 + } + }, + "cell_type": "code", + "source": [ + "by_category = collections.defaultdict(list)\n", + "for _, row in df.iterrows():\n", + " by_category[row.category].append(row.to_dict())\n", + "for category in by_category:\n", + " print (\"{0}: {1}\".format(category, len(by_category[category])))" + ], + "execution_count": 21, + "outputs": [ + { + "output_type": "stream", + "text": [ + "plane: 6000\n", + "car: 6000\n", + "bird: 6000\n", + "cat: 6000\n", + "deer: 6000\n", + "dog: 6000\n", + "frog: 6000\n", + "horse: 6000\n", + "ship: 6000\n", + "truck: 6000\n" + ], + "name": "stdout" + } + ] + }, + { + "metadata": { + "id": "AYVNBhLgt-38", + "colab_type": "code", + "colab": {} + }, + "cell_type": "code", + "source": [ + "final_list = []\n", + "for _, item_list in sorted(by_category.items()):\n", + " if args.shuffle:\n", + " np.random.shuffle(item_list)\n", + " n = len(item_list)\n", + " n_train = int(args.train_size*n)\n", + " n_val = int(args.val_size*n)\n", + " n_test = int(args.test_size*n)\n", + "\n", + " # Give data point a split attribute\n", + " for item in item_list[:n_train]:\n", + " item['split'] = 'train'\n", + " for item in item_list[n_train:n_train+n_val]:\n", + " item['split'] = 'val'\n", + " for item in item_list[n_train+n_val:]:\n", + " item['split'] = 'test' \n", + "\n", + " # Add to final list\n", + " final_list.extend(item_list)" + ], + "execution_count": 0, + "outputs": [] + }, + { + "metadata": { + "id": "o8GNPotNt-6X", + "colab_type": "code", + "outputId": "162a2ddb-db83-4708-b48d-ecbf1d61dd4f", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 85 + } + }, + "cell_type": "code", + "source": [ + "split_df = pd.DataFrame(final_list)\n", + "split_df[\"split\"].value_counts()" + ], + "execution_count": 23, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + "train 42000\n", + "test 9000\n", + "val 9000\n", + "Name: split, dtype: int64" + ] + }, + "metadata": { + "tags": [] + }, + "execution_count": 23 + } + ] + }, + { + "metadata": { + "id": "cLdJQPBmX0yJ", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "### Vocabulary" + ] + }, + { + "metadata": { + "id": "EB-kpxhct-_S", + "colab_type": "code", + "colab": {} + }, + "cell_type": "code", + "source": [ + "class Vocabulary(object):\n", + " def __init__(self, token_to_idx=None):\n", + "\n", + " # Token to index\n", + " if token_to_idx is None:\n", + " token_to_idx = {}\n", + " self.token_to_idx = token_to_idx\n", + "\n", + " # Index to token\n", + " self.idx_to_token = {idx: token \\\n", + " for token, idx in self.token_to_idx.items()}\n", + "\n", + " def to_serializable(self):\n", + " return {'token_to_idx': self.token_to_idx}\n", + "\n", + " @classmethod\n", + " def from_serializable(cls, contents):\n", + " return cls(**contents)\n", + "\n", + " def add_token(self, token):\n", + " if token in self.token_to_idx:\n", + " index = self.token_to_idx[token]\n", + " else:\n", + " index = len(self.token_to_idx)\n", + " self.token_to_idx[token] = index\n", + " self.idx_to_token[index] = token\n", + " return index\n", + "\n", + " def add_tokens(self, tokens):\n", + " return [self.add_token[token] for token in tokens]\n", + "\n", + " def lookup_token(self, token):\n", + " return self.token_to_idx[token]\n", + "\n", + " def lookup_index(self, index):\n", + " if index not in self.idx_to_token:\n", + " raise KeyError(\"the index (%d) is not in the Vocabulary\" % index)\n", + " return self.idx_to_token[index]\n", + "\n", + " def __str__(self):\n", + " return \"\" % len(self)\n", + "\n", + " def __len__(self):\n", + " return len(self.token_to_idx)" + ], + "execution_count": 0, + "outputs": [] + }, + { + "metadata": { + "id": "QcpS2G28t_Bv", + "colab_type": "code", + "outputId": "d0f38e9b-311a-42e1-a00a-ca75c8cf672e", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 85 + } + }, + "cell_type": "code", + "source": [ + "# Vocabulary instance\n", + "category_vocab = Vocabulary()\n", + "for index, row in df.iterrows():\n", + " category_vocab.add_token(row.category)\n", + "print (category_vocab) # __str__\n", + "print (len(category_vocab)) # __len__\n", + "index = category_vocab.lookup_token(\"dog\")\n", + "print (index)\n", + "print (category_vocab.lookup_index(index))" + ], + "execution_count": 25, + "outputs": [ + { + "output_type": "stream", + "text": [ + "\n", + "10\n", + "5\n", + "dog\n" + ], + "name": "stdout" + } + ] + }, + { + "metadata": { + "id": "ubECmrcqZIHI", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "### Sequence vocbulary" + ] + }, + { + "metadata": { + "id": "37pGFTBiZIbm", + "colab_type": "code", + "colab": {} + }, + "cell_type": "code", + "source": [ + "from collections import Counter\n", + "import string" + ], + "execution_count": 0, + "outputs": [] + }, + { + "metadata": { + "id": "YvWL2JcgZPaw", + "colab_type": "code", + "colab": {} + }, + "cell_type": "code", + "source": [ + "class SequenceVocabulary():\n", + " def __init__(self, train_means, train_stds):\n", + " \n", + " self.train_means = train_means\n", + " self.train_stds = train_stds\n", + " \n", + " def to_serializable(self):\n", + " contents = {'train_means': self.train_means,\n", + " 'train_stds': self.train_stds}\n", + " return contents\n", + " \n", + " @classmethod\n", + " def from_dataframe(cls, df):\n", + " train_data = df[df.split == \"train\"]\n", + " means = {0:[], 1:[], 2:[]}\n", + " stds = {0:[], 1:[], 2:[]}\n", + " for image in train_data.image:\n", + " for dim in range(3):\n", + " means[dim].append(np.mean(image[:, :, dim]))\n", + " stds[dim].append(np.std(image[:, :, dim]))\n", + " train_means = np.array((np.mean(means[0]), np.mean(means[1]), \n", + " np.mean(means[2])), dtype=\"float64\").tolist()\n", + " train_stds = np.array((np.mean(stds[0]), np.mean(stds[1]), \n", + " np.mean(stds[2])), dtype=\"float64\").tolist()\n", + " \n", + " return cls(train_means, train_stds)\n", + " \n", + " def __str__(self):\n", + " return \"\".format(\n", + " self.train_means, self.train_stds)" + ], + "execution_count": 0, + "outputs": [] + }, + { + "metadata": { + "id": "-ODlh2wcahqH", + "colab_type": "code", + "outputId": "b165d02a-3b92-40b8-92f5-5055533e6447", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 54 + } + }, + "cell_type": "code", + "source": [ + "# Create SequenceVocabulary instance\n", + "image_vocab = SequenceVocabulary.from_dataframe(split_df)\n", + "print (image_vocab) # __str__" + ], + "execution_count": 28, + "outputs": [ + { + "output_type": "stream", + "text": [ + "\n" + ], + "name": "stdout" + } + ] + }, + { + "metadata": { + "id": "lUZKa0c9YD0V", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "### Vectorizer" + ] + }, + { + "metadata": { + "id": "RyxHZLTFX5VC", + "colab_type": "code", + "colab": {} + }, + "cell_type": "code", + "source": [ + "class ImageVectorizer(object):\n", + " def __init__(self, image_vocab, category_vocab):\n", + " self.image_vocab = image_vocab\n", + " self.category_vocab = category_vocab\n", + "\n", + " def vectorize(self, image):\n", + " \n", + " # Avoid modifying the actual df\n", + " image = np.copy(image)\n", + " \n", + " # Normalize\n", + " for dim in range(3):\n", + " mean = self.image_vocab.train_means[dim]\n", + " std = self.image_vocab.train_stds[dim]\n", + " image[:, :, dim] = ((image[:, :, dim] - mean) / std)\n", + " \n", + " # Reshape frok (32, 32, 3) to (3, 32, 32)\n", + " image = np.swapaxes(image, 0, 2)\n", + " image = np.swapaxes(image, 1, 2)\n", + " \n", + " return image\n", + " \n", + " @classmethod\n", + " def from_dataframe(cls, df):\n", + " \n", + " # Create class vocab\n", + " category_vocab = Vocabulary() \n", + " for category in sorted(set(df.category)):\n", + " category_vocab.add_token(category)\n", + " \n", + " # Create image vocab\n", + " image_vocab = SequenceVocabulary.from_dataframe(df)\n", + " \n", + " return cls(image_vocab, category_vocab)\n", + "\n", + " @classmethod\n", + " def from_serializable(cls, contents):\n", + " image_vocab = SequenceVocabulary.from_serializable(contents['image_vocab'])\n", + " category_vocab = Vocabulary.from_serializable(contents['category_vocab'])\n", + " return cls(image_vocab=image_vocab, \n", + " category_vocab=category_vocab)\n", + " \n", + " def to_serializable(self):\n", + " return {'image_vocab': self.image_vocab.to_serializable(),\n", + " 'category_vocab': self.category_vocab.to_serializable()}" + ], + "execution_count": 0, + "outputs": [] + }, + { + "metadata": { + "id": "yXWIhtFUiDUe", + "colab_type": "code", + "outputId": "62f2c017-da89-4333-8cd3-f84abe05723b", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 88 + } + }, + "cell_type": "code", + "source": [ + "# Vectorizer instance\n", + "vectorizer = ImageVectorizer.from_dataframe(split_df)\n", + "print (vectorizer.image_vocab)\n", + "print (vectorizer.category_vocab)\n", + "image_vector = vectorizer.vectorize(split_df.iloc[0].image)\n", + "print (image_vector.shape)" + ], + "execution_count": 30, + "outputs": [ + { + "output_type": "stream", + "text": [ + "\n", + "\n", + "(3, 32, 32)\n" + ], + "name": "stdout" + } + ] + }, + { + "metadata": { + "id": "Xm7s9RPThF3c", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "### Dataset" + ] + }, + { + "metadata": { + "id": "2mL4eEdNX5c1", + "colab_type": "code", + "colab": {} + }, + "cell_type": "code", + "source": [ + "from torch.utils.data import Dataset, DataLoader" + ], + "execution_count": 0, + "outputs": [] + }, + { + "metadata": { + "id": "Dzegh16nX5fY", + "colab_type": "code", + "colab": {} + }, + "cell_type": "code", + "source": [ + "class ImageDataset(Dataset):\n", + " def __init__(self, df, vectorizer):\n", + " self.df = df\n", + " self.vectorizer = vectorizer\n", + "\n", + " # Data splits\n", + " self.train_df = self.df[self.df.split=='train']\n", + " self.train_size = len(self.train_df)\n", + " self.val_df = self.df[self.df.split=='val']\n", + " self.val_size = len(self.val_df)\n", + " self.test_df = self.df[self.df.split=='test']\n", + " self.test_size = len(self.test_df)\n", + " self.lookup_dict = {'train': (self.train_df, self.train_size), \n", + " 'val': (self.val_df, self.val_size),\n", + " 'test': (self.test_df, self.test_size)}\n", + " self.set_split('train')\n", + "\n", + " # Class weights (for imbalances)\n", + " class_counts = df.category.value_counts().to_dict()\n", + " def sort_key(item):\n", + " return self.vectorizer.category_vocab.lookup_token(item[0])\n", + " sorted_counts = sorted(class_counts.items(), key=sort_key)\n", + " frequencies = [count for _, count in sorted_counts]\n", + " self.class_weights = 1.0 / torch.tensor(frequencies, dtype=torch.float32)\n", + "\n", + " @classmethod\n", + " def load_dataset_and_make_vectorizer(cls, df):\n", + " train_df = df[df.split=='train']\n", + " return cls(df, ImageVectorizer.from_dataframe(train_df))\n", + "\n", + " @classmethod\n", + " def load_dataset_and_load_vectorizer(cls, df, vectorizer_filepath):\n", + " vectorizer = cls.load_vectorizer_only(vectorizer_filepath)\n", + " return cls(df, vectorizer)\n", + "\n", + " def load_vectorizer_only(vectorizer_filepath):\n", + " with open(vectorizer_filepath) as fp:\n", + " return ImageVectorizer.from_serializable(json.load(fp))\n", + "\n", + " def save_vectorizer(self, vectorizer_filepath):\n", + " with open(vectorizer_filepath, \"w\") as fp:\n", + " json.dump(self.vectorizer.to_serializable(), fp)\n", + "\n", + " def set_split(self, split=\"train\"):\n", + " self.target_split = split\n", + " self.target_df, self.target_size = self.lookup_dict[split]\n", + "\n", + " def __str__(self):\n", + " return \"= 1:\n", + " loss_tm1, loss_t = self.train_state['val_loss'][-2:]\n", + "\n", + " # If loss worsened\n", + " if loss_t >= self.train_state['early_stopping_best_val']:\n", + " # Update step\n", + " self.train_state['early_stopping_step'] += 1\n", + "\n", + " # Loss decreased\n", + " else:\n", + " # Save the best model\n", + " if loss_t < self.train_state['early_stopping_best_val']:\n", + " torch.save(self.model.state_dict(), self.train_state['model_filename'])\n", + "\n", + " # Reset early stopping step\n", + " self.train_state['early_stopping_step'] = 0\n", + "\n", + " # Stop early ?\n", + " self.train_state['stop_early'] = self.train_state['early_stopping_step'] \\\n", + " >= self.train_state['early_stopping_criteria']\n", + " return self.train_state\n", + " \n", + " def compute_accuracy(self, y_pred, y_target):\n", + " _, y_pred_indices = y_pred.max(dim=1)\n", + " n_correct = torch.eq(y_pred_indices, y_target).sum().item()\n", + " return n_correct / len(y_pred_indices) * 100\n", + " \n", + " def run_train_loop(self):\n", + " for epoch_index in range(self.num_epochs):\n", + " self.train_state['epoch_index'] = epoch_index\n", + " \n", + " # Iterate over train dataset\n", + "\n", + " # initialize batch generator, set loss and acc to 0, set train mode on\n", + " self.dataset.set_split('train')\n", + " batch_generator = self.dataset.generate_batches(\n", + " batch_size=self.batch_size, shuffle=self.shuffle, \n", + " device=self.device)\n", + " running_loss = 0.0\n", + " running_acc = 0.0\n", + " self.model.train()\n", + "\n", + " for batch_index, batch_dict in enumerate(batch_generator):\n", + " # zero the gradients\n", + " self.optimizer.zero_grad()\n", + " \n", + " # compute the output\n", + " y_pred = self.model(x=batch_dict['image'])\n", + " \n", + " # compute the loss\n", + " loss = self.loss_func(y_pred, batch_dict['category'])\n", + " loss_t = loss.item()\n", + " running_loss += (loss_t - running_loss) / (batch_index + 1)\n", + "\n", + " # compute gradients using loss\n", + " loss.backward()\n", + "\n", + " # use optimizer to take a gradient step\n", + " self.optimizer.step()\n", + " \n", + " # compute the accuracy\n", + " acc_t = self.compute_accuracy(y_pred, batch_dict['category'])\n", + " running_acc += (acc_t - running_acc) / (batch_index + 1)\n", + "\n", + " self.train_state['train_loss'].append(running_loss)\n", + " self.train_state['train_acc'].append(running_acc)\n", + "\n", + " # Iterate over val dataset\n", + "\n", + " # initialize batch generator, set loss and acc to 0, set eval mode on\n", + " self.dataset.set_split('val')\n", + " batch_generator = self.dataset.generate_batches(\n", + " batch_size=self.batch_size, shuffle=self.shuffle, device=self.device)\n", + " running_loss = 0.\n", + " running_acc = 0.\n", + " self.model.eval()\n", + "\n", + " for batch_index, batch_dict in enumerate(batch_generator):\n", + "\n", + " # compute the output\n", + " y_pred = self.model(x=batch_dict['image'])\n", + "\n", + " # compute the loss\n", + " loss = self.loss_func(y_pred, batch_dict['category'])\n", + " loss_t = loss.to(\"cpu\").item()\n", + " running_loss += (loss_t - running_loss) / (batch_index + 1)\n", + "\n", + " # compute the accuracy\n", + " acc_t = self.compute_accuracy(y_pred, batch_dict['category'])\n", + " running_acc += (acc_t - running_acc) / (batch_index + 1)\n", + "\n", + " self.train_state['val_loss'].append(running_loss)\n", + " self.train_state['val_acc'].append(running_acc)\n", + "\n", + " self.train_state = self.update_train_state()\n", + " self.scheduler.step(self.train_state['val_loss'][-1])\n", + " if self.train_state['stop_early']:\n", + " break\n", + " \n", + " def run_test_loop(self):\n", + " # initialize batch generator, set loss and acc to 0, set eval mode on\n", + " self.dataset.set_split('test')\n", + " batch_generator = self.dataset.generate_batches(\n", + " batch_size=self.batch_size, shuffle=self.shuffle, device=self.device)\n", + " running_loss = 0.0\n", + " running_acc = 0.0\n", + " self.model.eval()\n", + "\n", + " for batch_index, batch_dict in enumerate(batch_generator):\n", + " # compute the output\n", + " y_pred = self.model(x=batch_dict['image'])\n", + "\n", + " # compute the loss\n", + " loss = self.loss_func(y_pred, batch_dict['category'])\n", + " loss_t = loss.item()\n", + " running_loss += (loss_t - running_loss) / (batch_index + 1)\n", + "\n", + " # compute the accuracy\n", + " acc_t = self.compute_accuracy(y_pred, batch_dict['category'])\n", + " running_acc += (acc_t - running_acc) / (batch_index + 1)\n", + "\n", + " self.train_state['test_loss'] = running_loss\n", + " self.train_state['test_acc'] = running_acc\n", + " \n", + " def plot_performance(self):\n", + " # Figure size\n", + " plt.figure(figsize=(15,5))\n", + "\n", + " # Plot Loss\n", + " plt.subplot(1, 2, 1)\n", + " plt.title(\"Loss\")\n", + " plt.plot(trainer.train_state[\"train_loss\"], label=\"train\")\n", + " plt.plot(trainer.train_state[\"val_loss\"], label=\"val\")\n", + " plt.legend(loc='upper right')\n", + "\n", + " # Plot Accuracy\n", + " plt.subplot(1, 2, 2)\n", + " plt.title(\"Accuracy\")\n", + " plt.plot(trainer.train_state[\"train_acc\"], label=\"train\")\n", + " plt.plot(trainer.train_state[\"val_acc\"], label=\"val\")\n", + " plt.legend(loc='lower right')\n", + "\n", + " # Save figure\n", + " plt.savefig(os.path.join(self.save_dir, \"performance.png\"))\n", + "\n", + " # Show plots\n", + " plt.show()\n", + " \n", + " def save_train_state(self):\n", + " with open(os.path.join(self.save_dir, \"train_state.json\"), \"w\") as fp:\n", + " json.dump(self.train_state, fp)" + ], + "execution_count": 0, + "outputs": [] + }, + { + "metadata": { + "id": "Ug60AELzX5vT", + "colab_type": "code", + "outputId": "74f5b9db-ebc0-47d5-e496-afabe9c0ca7b", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 153 + } + }, + "cell_type": "code", + "source": [ + "# Initialization\n", + "dataset = ImageDataset.load_dataset_and_make_vectorizer(split_df)\n", + "dataset.save_vectorizer(args.vectorizer_file)\n", + "vectorizer = dataset.vectorizer\n", + "model = ImageModel(num_hidden_units=args.hidden_dim, \n", + " num_classes=len(vectorizer.category_vocab),\n", + " dropout_p=args.dropout_p)\n", + "print (model.named_modules)" + ], + "execution_count": 38, + "outputs": [ + { + "output_type": "stream", + "text": [ + "\n" + ], + "name": "stdout" + } + ] + }, + { + "metadata": { + "id": "vF9kAEXEX5a4", + "colab_type": "code", + "colab": {} + }, + "cell_type": "code", + "source": [ + "# Train\n", + "trainer = Trainer(dataset=dataset, model=model, \n", + " model_state_file=args.model_state_file, \n", + " save_dir=args.save_dir, device=args.device,\n", + " shuffle=args.shuffle, num_epochs=args.num_epochs, \n", + " batch_size=args.batch_size, learning_rate=args.learning_rate, \n", + " early_stopping_criteria=args.early_stopping_criteria)\n", + "trainer.run_train_loop()" + ], + "execution_count": 0, + "outputs": [] + }, + { + "metadata": { + "id": "2G6I5YWtt_Ea", + "colab_type": "code", + "outputId": "0ca459c6-0053-4a43-82a3-6a1e1c6e5c33", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 335 + } + }, + "cell_type": "code", + "source": [ + "# Plot performance\n", + "trainer.plot_performance()" + ], + "execution_count": 40, + "outputs": [ + { + "output_type": "display_data", + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAA2sAAAE+CAYAAAATaYj9AAAABHNCSVQICAgIfAhkiAAAAAlwSFlz\nAAALEgAACxIB0t1+/AAAADl0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uIDIuMS4yLCBo\ndHRwOi8vbWF0cGxvdGxpYi5vcmcvNQv5yAAAIABJREFUeJzs3Xd81eXd//HXGTk52TshG0IGeysg\nyAgiIFjFiW3dtaVWrdparXfVtra3+mvt3Sra3uJdrbNWRMSJQgEB2XuHsDLJ3vOs3x+BKJWV5MDJ\nOXk/H494cr7nfK/zuRJJzjvX+BpcLpcLERERERER6VaMni5AREREREREvk1hTUREREREpBtSWBMR\nEREREemGFNZERERERES6IYU1ERERERGRbkhhTUREREREpBtSWBPppKysLI4dO+bpMkRERC6IOXPm\n8J3vfMfTZYj0KAprIiIiInJGOTk5hISEkJCQwNatWz1djkiPobAm4mYtLS08/vjjTJs2jRkzZvD0\n00/jcDgAeOONN5gxYwbTp0/nuuuu48CBA2c8LiIi0h28//77TJ8+nVmzZrFo0aL244sWLWLatGlM\nmzaNhx56iNbW1tMeX79+PVOnTm0/95v3n3/+eX71q19x3XXX8eqrr+J0OvnNb37DtGnTyM7O5qGH\nHsJmswFQWVnJ3LlzmTJlCldeeSWrV69mxYoVzJo166Sar7nmGpYuXXq+vzQi55XZ0wWI+Jp//OMf\nHDt2jI8//hi73c73v/99PvroI6ZMmcJf/vIXli9fTnBwMJ9++ikrVqwgPj7+lMczMjI83RUREREc\nDgdffPEFP/nJTzCZTDz77LO0trZSWlrKM888w6JFi4iNjeXee+/ltddeY/r06ac8Pnjw4DO+zsqV\nK/nggw+IjIxkyZIlbNq0iY8++gin08ns2bP55JNPuOqqq3j22Wfp27cvf/vb39izZw+33347q1at\noqysjH379tGvXz+KiorIy8tjwoQJF+irJHJ+KKyJuNmKFSu44447MJvNmM1mrrzyStasWcMVV1yB\nwWBgwYIFzJo1ixkzZgBgs9lOeVxERKQ7WL16NYMHDyY4OBiAiy++mOXLl1NdXc3w4cOJi4sD4Nln\nn8VkMvHee++d8vjmzZvP+DpDhw4lMjISgGnTpjF58mT8/PwAGDx4MPn5+UBbqJs/fz4AAwYMYNmy\nZVgsFqZNm8bHH39Mv379WLp0KVOmTMFisbj/CyJyAWkapIibVVZWEhYW1n4/LCyMiooK/Pz8ePXV\nV9myZQvTpk3ju9/9Lvv37z/tcRERke5g4cKFrFixglGjRjFq1Cg+//xz3n//faqqqggNDW1/nr+/\nP2az+bTHz+abvzsrKyt5+OGHmTZtGtOnT2fZsmW4XC4AqqurCQkJaX/uiRA5c+ZMPv74YwCWLl3K\nFVdc0bWOi3QDCmsibhYdHU11dXX7/erqaqKjo4G2vwA+99xzrF27lvHjx/PEE0+c8biIiIgn1dTU\nsGHDBtavX8+mTZvYtGkTGzduZOfOnRiNRqqqqtqfW19fT3l5OREREac8bjKZ2tdwA9TW1p72df/n\nf/4Hs9nMhx9+yGeffcbEiRPbHwsPDz+p/YKCAmw2GxdddBF2u53ly5dz4MABLrnkEnd9GUQ8RmFN\nxM0mTZrEggULcDgcNDY28sEHHzBx4kT279/PfffdR2trKxaLhUGDBmEwGE57XERExNM+/vhjxowZ\nc9J0QrPZzPjx42ltbWXLli0UFBTgcrl44oknWLBgARMnTjzl8ZiYGMrKyqioqMDhcPDhhx+e9nUr\nKirIzMzEYrGwb98+tm7dSmNjIwDZ2dm8//77AOTm5nLNNdfgcDgwGo1cccUVPPnkk2RnZ7dPoRTx\nZlqzJtIFN998MyaTqf3+7373O26++Wby8/OZOXMmBoOB6dOnt69DS0pKYtasWfj5+REUFMTjjz9O\nZmbmKY+LiIh42qJFi7j11lu/dXzq1Km8+OKL/Pa3v+XWW2/FZDIxePBgbr/9dvz9/U97/Nprr+Xq\nq68mISGBq666ir17957yde+44w4efvhhFi5cyKhRo3j44Yf5r//6L4YMGcJDDz3Eww8/THZ2NkFB\nQfzxj3/EarUCbVMhX3nlFU2BFJ9hcJ2YACwiIiIi4sXKy8uZPXs2K1asOOmPqSLeStMgRURERMQn\nPPfcc9x0000KauIzFNZERERExKuVl5czZcoUysvLueOOOzxdjojbaBqkiIiIiIhIN6SRNRERERER\nkW5IYU1ERERERKQbuuBb95eV1XW5jYiIQKqqGt1QjeepL92T+tI9qS/d05n6EhMTcoGr8W76HXky\n9aV78pW++Eo/QH3prtzx+9ErR9bMZt/Z4Ud96Z7Ul+5JfemefKkvvsCXvh/qS/fkK33xlX6A+tJd\nuaMvXhnWREREREREfJ3CmoiIiIiISDeksCYiIiIiItINKayJiIiIiIh0QwprIiIiIiIi3ZDCmoiI\niIiISDeksCYiIiIiItINKayJiPQgK1YsO6fn/eUvz1JUVHieqxEREZEzUVgTEekhiouLWLp0yTk9\n96c//RkJCYnnuSIRERE5E7OnC+iophY7H646xPC+kfj7+c4VzkVEzrc//ekZ9u7dzaWXXsTll8+g\nuLiIP//5RZ566reUlZXS1NTEHXf8kHHjLuWee37Igw/+guXLl9HQUE9e3lEKCwu4776fMXbsOE93\nRURE5IJwuVw0NNspq26irLqJ8ppm+qVEkJYQekFe3+vC2t6jVby0aCffvSyDy0Yle7ocERGvcdNN\nN7Nw4b/o06cveXlHePHFl6mqquTii8cwY8YsCgsLeOyxRxg37tKTzistLeGPf3yOdeu+4oMP3lNY\nExERn2J3OKmsbaasupnS46Hsmx9NLY6Tnj8yK4afzB58QWrzurCWFBMEQE5+tcKaiHitv3+4my+3\nFLi1zYv6xXJDdvo5Pbd//4EAhISEsnfvbhYvXojBYKS2tuZbzx0yZBgAsbGx1NfXu69gERGRC6Sh\n2UZZdROlVSdCWHN7GKuobcbl+vY5Fj8jMeEBxIQFtN2GW4kJDyAzOfyC1e11YS0mPIDIUCs5BTW4\nXC4MBoOnSxIR8Tp+fn4AfPHFZ9TW1vLCCy9TW1vLD35w87eeazJ9PeXcdarfZiIiIt3Q/rwqFqw8\nSHF5I40t9lM+JzzYQnpi2PEwFkBs+NfBLDTI4vGs4XVhzWAwMDAtilXbCimpaqJXZKCnSxIR6bA7\nrhzIlWNSLuhrGo1GHI6Tp3JUV1cTH5+A0Whk5cp/Y7PZLmhNIiIi7uZ0uvh47REWrT4MQHxUEBlJ\nxwNZREB7MIsOs3b7PTC8LqwBDOwTyaptheTkVyusiYico9TUPuzfv4/4+ATCw9umcEyalM0jjzzI\nnj27mDnzO8TGxvLKK/M9XKmIiEjn1NS38NKHe9h7tIrIUH9+9J2BZCRduGmL7uadYa1vNNC2bm3C\n0AQPVyMi4h0iIiJYuPDjk47Fxyfwj3/8s/3+5ZfPAOD22+8CIC3t6zVwaWnpzJv30gWoVEREpON2\nH6lk/uLd1DbaGJYezR0z+xMc4OfpsrrEK8NaSlwIQVYzOfnVni5FREREREQ8yOF08sHqw3z81VGM\n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\ncBXvv/8ub731+pDXpLAmIiK9/Cwm4iN8d8pkOTVXkD/hoYEjZlY4EZGhlJ4+jo8+2tB7+YUXXun9\n+YILume8v+SSy4e0JuOQPpqIiIiIiIicFoU1ERERERERH6SwJiIiIiIi4oMU1kRERERERHyQwpqI\niIiIiIgPUlgTERERERHxQQprIiIiIiIiPkhhTURERERExAcprImIiIiIiPgghTUREREREREfZPB4\nPB5vFyEiIiIiIiLH0pk1ERERERERH6SwJiIiIiIi4oMU1kRERERERHyQwpqIiIiIiIgPUlgTERER\nERHxQQprIiIiIiIiPsjs7QL68qtf/Yrt27djMBj48Y9/zIQJE3q3rV27lkceeQSTycTcuXO55ZZb\nvFhp337961+zZcsWOjo6+Pa3v82SJUt6ty1cuJDIyEhMJhMADz/8MBEREd4q9ZQ2bNjAHXfcQVpa\nGgCjRo3i3nvv7d0+nJ6Xf/7zn7z++uu9l3ft2sW2bdt6L48bN47Jkyf3Xn722Wd7nyNfsX//fm6+\n+Wauv/56li9fTklJCXfddRednZ2Eh4fz0EMP4efnd8x9TvV/5U0nass999xDR0cHZrOZhx56iPDw\n8N7b9/W36E3/3Za7776bzMxMgoODAbjhhhuYP3/+MfcZLs/L7bffTnV1NQA1NTVkZGTw85//vPf2\nr732Gr/97W+Jj48HYNasWXznO9/xSu0jmY6PvkfHRx0fB4uOj8PjeRmU46PHh23YsMHzrW99y+Px\neDzZ2dmeq6666pjtS5cu9Rw8eNDT2dnpueaaazxZWVneKPO0rFu3znPjjTd6PB6P59ChQ5558+Yd\ns33BggWehoYGL1R25tavX++57bbbTrp9OD0vR9uwYYPn/vvvP+a66dOne6ma09PY2OhZvny55//9\nv//nee655zwej8dz9913e95++22Px+Px/OY3v/G88MILx9ynr/8rbzlRW+666y7PW2+95fF4PJ7n\nn3/e8+CDDx5zn77+Fr3lRG350Y9+5Pnwww9Pep/h9Lwc7e677/Zs3779mOteffVVz//+7/8OVYnn\nJB0ffZOOj75Dx0cdHwfbUB0ffbob5Lp161i8eDEAKSkp1NbW0tDQAEBhYSFOp5OoqCiMRiPz5s1j\n3bp13iz3lKZNm8Zvf/tbAIKCgmhubqazs9PLVQ284fa8HO33v/89N998s7fLOCN+fn489dRTuN3u\n3us2bNjAokWLAFiwYMFxv/9T/V9504nact9993HRRRcBEBISQk1NjbfKOyMnaktfhtPzctiBAweo\nr6/3mU84zyU6Pg4/w+15OZqOj96l4+PweV4OG8jjo0+HtcrKSkJCQnovu1wuKioqAKioqMDlcp1w\nmy8ymUzYbDYAXnnlFebOnXtcd4H77ruPa665hocffhiPx+ONMk9bdnY2N910E9dccw2ffvpp7/XD\n7Xk5bMeOHURFRR3ThQCgra2NO++8k6uvvppnnnnGS9WdnNlsxt/f/5jrmpube7t1hIaGHvf7P9X/\nlTedqC02mw2TyURnZyd///vfueyyy46738n+Fr3pRG0BeP7557nuuuv43ve+x6FDh47ZNpyel8P+\n9re/sXz58hNu27hxIzfccANf+9rX2L1792CWeE7S8dF36fjoG3R81PFxsA3V8dHnx6wdzddfoE/H\n+++/zyuvvMJf/vKXY66//fbbmTNnDk6nk1tuuYWVK1fyuc99zktVnlpiYiK33norS5cupbCwkOuu\nu4533333uH7fw8krr7zC5z//+eOuv+uuu7j88ssxGAwsX76cqVOnct5553mhwv45nf8ZX/+/6uzs\n5K677mLGjBnMnDnzmG3D6W9x2bJlBAcHk56ezpNPPsnjjz/OT37yk5Pe3tefl7a2NrZs2cL9999/\n3LaJEyficrmYP38+27Zt40c/+hFvvPHG0Bd5DvH1v5fToeOjb9Lx0Xfp+OibBvr46NNn1txuN5WV\nlb2Xy8vLez/Z+e9tZWVlZ3RK1RvWrFnDE088wVNPPYXD4Thm2xVXXEFoaChms5m5c+eyf/9+L1XZ\nt4iICC6++GIMBgPx8fGEhYVRVlYGDM/nBbq7RkyaNOm466+55hoCAwOx2WzMmDHDp5+Xw2w2Gy0t\nLcCJf/+n+r/yRffccw8JCQnceuutx2071d+ir5k5cybp6elA94QJ//23NNyel02bNp20e0dKSkrv\n4PBJkyZx6NChEdmtzZt0fPRNOj76Nh0fdXwcCgN9fPTpsDZ79mxWrlwJQGZmJm63G7vdDkBsbCwN\nDQ0UFRXR0dHBqlWrmD17tjfLPaX6+np+/etf86c//al3tpujt91www20tbUB3U/y4dl7fNHrr7/O\n008/DXR366iqquqdmWu4PS/Q/YIdGBh43KdNBw4c4M4778Tj8dDR0cHWrVt9+nk5bNasWb3/N+++\n+y5z5sw5Zvup/q98zeuvv47FYuH2228/6faT/S36mttuu43CwkKg+83Pf/8tDafnBWDnzp2MGTPm\nhNueeuop3nzzTaB7piyXy+Vzs8QNdzo++iYdH32bjo86Pg6FgT4+Gjw+fi7x4YcfZvPmzRgMBu67\n7z52796Nw+HgwgsvZNOmTTz88MMALFmyhBtuuMHL1Z7cyy+/zGOPPUZSUlLvdeeffz6jR4/mwgsv\n5K9//SsrVqzAarUyduxY7r33XgwGgxcrPrmGhgZ+8IMfUFdXR3t7O7feeitVVVXD8nmB7umIH330\nUf785z8D8OSTTzJt2jQmTZrEQw89xPr16zEajSxcuNDnph/ftWsXDz74IMXFxZjNZiIiInj44Ye5\n++67aW1tJTo6mgceeACLxcL3vvc9HnjgAfz9/Y/7vzrZi4q321JVVYXVau19UU5JSeH+++/vbUtH\nR8dxf4vz5s3zcktO3Jbly5fz5JNPEhAQgM1m44EHHiA0NHRYPi+PPfYYjz32GFOmTOHiiy/uve13\nvvMd/vjHP1JaWsoPf/jD3jdyvjTN8kii46Pv0fHRd+j4qOOjN9oyGMdHnw9rIiIiIiIi5yKf7gYp\nIiIiIiJyrlJYExERERER8UEKayIiIiIiIj5IYU1ERERERMQHKayJiIiIiIj4IIU1ERERERERH6Sw\nJiIiIiIi4oMU1kRERERERHzQ/wctCrC08ooCUgAAAABJRU5ErkJggg==\n", + "text/plain": [ + "" + ] + }, + "metadata": { + "tags": [] + } + } + ] + }, + { + "metadata": { + "id": "Iz3G5eaTS04m", + "colab_type": "code", + "outputId": "9d3a0b4e-59d3-4fcd-860f-f0a062c4e8e9", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 51 + } + }, + "cell_type": "code", + "source": [ + "# Test performance\n", + "trainer.run_test_loop()\n", + "print(\"Test loss: {0:.2f}\".format(trainer.train_state['test_loss']))\n", + "print(\"Test Accuracy: {0:.1f}%\".format(trainer.train_state['test_acc']))" + ], + "execution_count": 41, + "outputs": [ + { + "output_type": "stream", + "text": [ + "Test loss: 0.94\n", + "Test Accuracy: 67.7%\n" + ], + "name": "stdout" + } + ] + }, + { + "metadata": { + "id": "kqMzljfpS09F", + "colab_type": "code", + "colab": {} + }, + "cell_type": "code", + "source": [ + "# Save all results\n", + "trainer.save_train_state()" + ], + "execution_count": 0, + "outputs": [] + }, + { + "metadata": { + "id": "1fMNOVJUYvhs", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "~66% test performance for our Cifar10 dataset is not bad but we can do way better." + ] + }, + { + "metadata": { + "id": "P9DcE8tHYvfX", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "# Transfer learning" + ] + }, + { + "metadata": { + "id": "EclYytw6Swh-", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "In this section, we're going to use a pretrained model that performs very well on a different dataset. We're going to take the architecture and the initial convolutional weights from the model to use on our data. We will freeze the initial convolutional weights and fine tune the later convolutional and fully-connected layers. \n", + "\n", + "Transfer learning works here because the initial convolution layers act as excellent feature extractors for common spatial features that are shared across images regardless of their class. We're going to leverage these large, pretrained models' feature extractors for our own dataset." + ] + }, + { + "metadata": { + "id": "mxl4PEfqTMwm", + "colab_type": "code", + "colab": {} + }, + "cell_type": "code", + "source": [ + "from torchvision import models" + ], + "execution_count": 0, + "outputs": [] + }, + { + "metadata": { + "id": "GjufXPDJTB7W", + "colab_type": "code", + "outputId": "709a1c26-7c67-420f-83be-bdf5409cf4cc", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 54 + } + }, + "cell_type": "code", + "source": [ + "model_names = sorted(name for name in models.__dict__\n", + " if name.islower() and not name.startswith(\"__\")\n", + " and callable(models.__dict__[name]))\n", + "print (model_names)" + ], + "execution_count": 44, + "outputs": [ + { + "output_type": "stream", + "text": [ + "['alexnet', 'densenet121', 'densenet161', 'densenet169', 'densenet201', 'inception_v3', 'resnet101', 'resnet152', 'resnet18', 'resnet34', 'resnet50', 'squeezenet1_0', 'squeezenet1_1', 'vgg11', 'vgg11_bn', 'vgg13', 'vgg13_bn', 'vgg16', 'vgg16_bn', 'vgg19', 'vgg19_bn']\n" + ], + "name": "stdout" + } + ] + }, + { + "metadata": { + "id": "daJN4BSWS016", + "colab_type": "code", + "outputId": "0608949f-b8ec-471e-c32b-04079b8980b9", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 1173 + } + }, + "cell_type": "code", + "source": [ + "model_name = 'vgg19_bn'\n", + "vgg_19bn = models.__dict__[model_name](pretrained=True) # Set false to train from scratch\n", + "print (vgg_19bn.named_parameters)" + ], + "execution_count": 45, + "outputs": [ + { + "output_type": "stream", + "text": [ + "Downloading: \"https://download.pytorch.org/models/vgg19_bn-c79401a0.pth\" to /root/.torch/models/vgg19_bn-c79401a0.pth\n", + "100%|██████████| 574769405/574769405 [00:29<00:00, 19305160.82it/s]\n" + ], + "name": "stderr" + }, + { + "output_type": "stream", + "text": [ + "\n" + ], + "name": "stdout" + } + ] + }, + { + "metadata": { + "id": "XBudDGFz1j87", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "The VGG model we chose has a `features` and a `classifier` component. The `features` component is composed of convolution and pooling layers which act as feature extractors. The `classifier` component is composed on fully connected layers. We're going to freeze most of the `feature` component and design our own FC layers for our CIFAR10 task. You can access the default code for all models at `/usr/local/lib/python3.6/dist-packages/torchvision/models` if you prefer cloning and modifying that instead." + ] + }, + { + "metadata": { + "id": "YmzQIXsd59Rj", + "colab_type": "code", + "colab": {} + }, + "cell_type": "code", + "source": [ + "class ImageModel(nn.Module):\n", + " def __init__(self, feature_extractor, num_hidden_units, \n", + " num_classes, dropout_p):\n", + " super(ImageModel, self).__init__()\n", + " \n", + " # Pretrained feature extractor\n", + " self.feature_extractor = feature_extractor\n", + " \n", + " # FC weights\n", + " self.classifier = nn.Sequential(\n", + " nn.Linear(512, 250, bias=True),\n", + " nn.ReLU(),\n", + " nn.Dropout(0.5),\n", + " nn.Linear(250, 100, bias=True),\n", + " nn.ReLU(),\n", + " nn.Dropout(0.5),\n", + " nn.Linear(100, 10, bias=True),\n", + " )\n", + "\n", + " def forward(self, x, apply_softmax=False):\n", + " \n", + " # Feature extractor\n", + " z = self.feature_extractor(x)\n", + " z = z.view(x.size(0), -1)\n", + " \n", + " # FC\n", + " y_pred = self.classifier(z)\n", + "\n", + " if apply_softmax:\n", + " y_pred = F.softmax(y_pred, dim=1)\n", + " return y_pred " + ], + "execution_count": 0, + "outputs": [] + }, + { + "metadata": { + "id": "czo1bGBwXKNj", + "colab_type": "code", + "outputId": "9d407e14-2415-41ba-9f20-37e33f8ab716", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 1139 + } + }, + "cell_type": "code", + "source": [ + "# Initialization\n", + "dataset = ImageDataset.load_dataset_and_make_vectorizer(split_df)\n", + "dataset.save_vectorizer(args.vectorizer_file)\n", + "vectorizer = dataset.vectorizer\n", + "model = ImageModel(feature_extractor=vgg_19bn.features, \n", + " num_hidden_units=args.hidden_dim,\n", + " num_classes=len(vectorizer.category_vocab), \n", + " dropout_p=args.dropout_p)\n", + "print (model.named_parameters)" + ], + "execution_count": 47, + "outputs": [ + { + "output_type": "stream", + "text": [ + "\n" + ], + "name": "stdout" + } + ] + }, + { + "metadata": { + "id": "hZybxGHoDTwQ", + "colab_type": "code", + "colab": {} + }, + "cell_type": "code", + "source": [ + "# Finetune last few conv layers and FC layers\n", + "for i, param in enumerate(model.feature_extractor.parameters()):\n", + " if i < 36:\n", + " param.requires_grad = False\n", + " else:\n", + " param.requires_grad = True" + ], + "execution_count": 0, + "outputs": [] + }, + { + "metadata": { + "id": "GTbYKussTvB2", + "colab_type": "code", + "colab": {} + }, + "cell_type": "code", + "source": [ + "# Train\n", + "trainer = Trainer(dataset=dataset, model=model, \n", + " model_state_file=args.model_state_file, \n", + " save_dir=args.save_dir, device=args.device,\n", + " shuffle=args.shuffle, num_epochs=args.num_epochs, \n", + " batch_size=args.batch_size, learning_rate=args.learning_rate, \n", + " early_stopping_criteria=args.early_stopping_criteria)\n", + "trainer.run_train_loop()" + ], + "execution_count": 0, + "outputs": [] + }, + { + "metadata": { + "id": "NCLCnQgATvMj", + "colab_type": "code", + "outputId": "20bca437-868c-41c3-c95c-4b328c9024fc", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 335 + } + }, + "cell_type": "code", + "source": [ + "# Plot performance\n", + "trainer.plot_performance()" + ], + "execution_count": 0, + "outputs": [ + { + "output_type": "display_data", + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAA2gAAAE+CAYAAAD4XjP+AAAABHNCSVQICAgIfAhkiAAAAAlwSFlz\nAAALEgAACxIB0t1+/AAAADl0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uIDIuMS4yLCBo\ndHRwOi8vbWF0cGxvdGxpYi5vcmcvNQv5yAAAIABJREFUeJzs3Xl4lOWh/vHvzGRmsu+ZEFYhIEkg\nISBBBRRFEQTcECEqllO0PVqXVpHCL+2p1lqXU1BRK2pLdxRcgqhYtXjAilJZQkIgCausYpLJvs/6\n+yM0SlmVTCaT3J/r8jLzzrzv3DNsc8/7vM9j8Hq9XkRERERERMTvjP4OICIiIiIiIq1U0ERERERE\nRDoJFTQREREREZFOQgVNRERERESkk1BBExERERER6SRU0ERERERERDoJFTSR72jw4MF89dVX/o4h\nIiLSIbKzs7n22mv9HUOky1NBExEREZHT2rVrFxEREfTs2ZOtW7f6O45Il6aCJtLOWlpa+MUvfsHE\niRO5+uqreeKJJ3C73QD87W9/4+qrr2bSpElMnz6d3bt3n3a7iIhIZ7By5UomTZrE1KlTeeutt9q2\nv/XWW0ycOJGJEycyb948HA7HKbd//vnnTJgwoW3fb95+7rnn+PnPf8706dP505/+hMfj4Ze//CUT\nJ05k/PjxzJs3D6fTCUBlZSV33nknV1xxBddccw3r169n3bp1TJ069bjM06ZNY82aNb5+a0TaXZC/\nA4h0NX/+85/56quvWL16NS6Xi1mzZvHuu+9yxRVXsHjxYtauXUt4eDh///vfWbduHUlJSSfdPmjQ\nIH+/FBEREdxuN//4xz+4++67MZlMLFq0CIfDQVlZGU8++SRvvfUWNpuNe++9l7/85S9MmjTppNvT\n09NP+zwff/wxq1atIjY2lg8++IDNmzfz7rvv4vF4uOGGG3jvvfe47rrrWLRoEcnJybz44osUFRXx\n/e9/n08++YTy8nJKSkpISUnhyy+/5ODBg1x66aUd9C6JtB8VNJF2tm7dOubMmUNQUBBBQUFcc801\nfPrpp0yePBmDwcAbb7zB1KlTufrqqwFwOp0n3S4iItIZrF+/nvT0dMLDwwEYNWoUa9eupbq6muHD\nh5OYmAjAokWLMJlMvPnmmyfdvmXLltM+z7Bhw4iNjQVg4sSJXH755ZjNZgDS09M5dOgQ0Frkfve7\n3wGQlpbGRx99hMViYeLEiaxevZqUlBTWrFnDFVdcgcViaf83RMTHNMRRpJ1VVlYSFRXVdjsqKoqK\nigrMZjN/+tOfyMvLY+LEidxyyy3s3LnzlNtFREQ6g9zcXNatW8fIkSMZOXIkH374IStXrqSqqorI\nyMi2x1mtVoKCgk65/Uy++W9nZWUl8+fPZ+LEiUyaNImPPvoIr9cLQHV1NREREW2P/XdxnDJlCqtX\nrwZgzZo1TJ48+dxeuIifqKCJtLP4+Hiqq6vbbldXVxMfHw+0ftP37LPPsmHDBsaOHctDDz102u0i\nIiL+VFNTw8aNG/n888/ZvHkzmzdvZtOmTRQWFmI0Gqmqqmp7bH19PXa7nZiYmJNuN5lMbddkA9TW\n1p7yeZ9++mmCgoJ45513eP/99xk3blzbfdHR0ccd//DhwzidTrKysnC5XKxdu5bdu3czevTo9nob\nRDqUCppIO7vssst44403cLvdNDY2smrVKsaNG8fOnTu57777cDgcWCwWhg4disFgOOV2ERERf1u9\nejUXXXTRcUMFg4KCGDt2LA6Hg7y8PA4fPozX6+Whhx7ijTfeYNy4cSfdnpCQQHl5ORUVFbjdbt55\n551TPm9FRQXnn38+FouFkpIStm7dSmNjIwDjx49n5cqVAOzZs4dp06bhdrsxGo1MnjyZX/3qV4wf\nP75teKRIoNE1aCLn4LbbbsNkMrXdfvTRR7nttts4dOgQU6ZMwWAwMGnSpLbrynr37s3UqVMxm82E\nhYXxi1/8gvPPP/+k20VERPztrbfeYvbs2SdsnzBhAi+88AKPPPIIs2fPxmQykZ6ezve//32sVusp\nt994441cf/319OzZk+uuu47i4uKTPu+cOXOYP38+ubm5jBw5kvnz5/Ozn/2MjIwM5s2bx/z58xk/\nfjxhYWEsXLiQ4OBgoHWY4x//+EcNb5SAZvD+e0CviIiIiEgAs9vt3HDDDaxbt+64L1BFAomGOIqI\niIhIl/Dss89y8803q5xJQFNBExEREZGAZrfbueKKK7Db7cyZM8ffcUTOiYY4ioiIiIiIdBI6gyYi\nIiIiItJJqKCJiIiIiIh0Eh0+zX55ed05HyMmJpSqqsZ2SNMxAimvsvpGIGWFwMqrrL7RXlkTEiLa\nIU330d3+jQykrBBYeZXVN5TVdwIpb3tkPd2/jwF5Bi0oKLBm5gmkvMrqG4GUFQIrr7L6RiBlleMF\n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\nwPjx49m6dWur+bZu3crEiRMBGDlyJF9//XWXZxUREeejgiYiItJOGRkZWK1WTCYTvr6+LY8HBQVR\nXFzcat6SkhICAwMBMBqNGAwG6urqujSviIg4Hw1xFBERaafVq1czZcqU8x632WxtLtueeQICPHFx\nMV1Wtu+zWHyu+DW6ijNlBefKq6ydQ1k7jzPl7cysKmgiIiLttG3bNp599lkMBgPl5eUtjxcWFhIS\nEtJq3pCQEIqLixk0aBD19fXYbDbM5ovfk6ysrOqKMzrT+YPOlBWcK6+ydg5l7TzOlLcjsl6s4GmI\no4iISDsUFhbi5eWF2WzG1dWVqKgodu7cCcAnn3zC6NGjW82fkpLCRx99BMCGDRsYNmxYl2cWERHn\no4ImIiLSDsXFxS3nlAHMnTuXhQsXMn36dMLDwxk5ciQAs2bNAuDmm2+mqamJGTNmsHTpUmbPnm2X\n3CIi4lw0xFFERKQd4uPjeeONN1qm+/fvz7///e/z5luyZAlAy73PRERELoWOoImIiIiIiDgIFTQR\nEREREREHoYImIiIiIiLiIAy29tyYRURERERERDqdjqCJiIiIiIg4CBU0ERERERERB6GCJiIiIiIi\n4iBU0ERERERERByECpqIiIiIiIiDUEETERERERFxEC72DnAhL7zwAnv27MFgMDB37lwSExNbntuy\nZQsLFy7EZDIxZswYHnrooTaXsVfWr776ioULF2I0GomMjOT5559nx44dPPbYY8TExAAwYMAAfvvb\n33ZJ1rbyTpgwgbCwMEwmEwDz588nNDTU4d7bwsJCnnrqqZb5jh8/zuzZs6mvr2fRokWEh4cDMHLk\nSGbNmtUlWbOzs3nwwQf5+c9/zt13393qOUdbZ9vK62jr7cWyOto6+2NZHXGd/eMf/8iuXbtoaGjg\n17/+NTfccEPLc464zsp3tI3s+qyO9lnjTNtHcK5tpLaPXZ/XEddbu28jbQ5m27Zttl/96lc2m81m\ny83NtU2dOrXV8zfddJPt1KlTtsbGRtuMGTNsOTk5bS5jr6wTJ0605efn22w2m+2RRx6xbdy40fbV\nV1/ZHnnkkS7J90Nt5R0/frytsrLykpaxV9Zv1dfX26ZPn26rrKy0vfXWW7aXXnqpS/J937lz52x3\n33237dlnn7W9+eab5z3vSOtse/I60nrbVlZHWmfbyvotR1hnt27darv//vttNpvNVlpaahs7dmyr\n5x1tnZXvaBtpn6yO9FnjTNtHm825tpHaPnYebSMvjcMNcdy6dSvXX389ANHR0Zw5c4bKykqguVH7\n+flhtVoxGo2MHTuWrVu3XnQZe2UFWLNmDWFhYQAEBgZSVlbW6Zku5nLeJ0d9b7/19ttvc+ONN+Ll\n5dXpmX6M2Wzm9ddfJyQk5LznHG2dbSsvONZ621bWC7HXe9verI6wzl5zzTUsWrQIAF9fX6qrq2ls\nbAQcc52V72gb2Tm0few8zrSN1Pax82gbeWkcrqCVlJQQEBDQMh0YGEhxcTEAxcXFBAYGnvfcxZax\nV1YAb29vAIqKiti8eTNjx44FIDc3lwceeIAZM2awefPmTs/Z3rwA8+bNY8aMGcyfPx+bzeaw7+23\nVq1axZ133tkyvX37dmbOnMm9997Lvn37Oj0ngIuLC+7u7hd8ztHWWbh4XnCs9batrOA462x7soJj\nrLMmkwlPT08AVq9ezZgxY1qGwTjiOivf0TbSPlnBcT5rnGn7CM61jdT2sfNoG3lpHPIctO+z2Wxd\nskxHuNDPPX36NA888ADz5s0jICCAiIgIHn74YW666SaOHz/OPffcwyeffILZbLZ73kcffZTRo0fj\n5+fHQw89xMcff9zmMl3lQj939+7dREVFtXxgJiUlERgYyLhx49i9ezfPPPMM7733XldHvSz2el9/\njCOvt9/nyOvshTjaOrt+/XpWr17N//zP/1zyso70vvZk2kZ2TVZH/qzp7ttHcKzPG0ddZ3/IkdfZ\nH+No6609t5EOV9BCQkIoKSlpmS4qKsJisVzwucLCQkJCQnB1df3RZeyVFaCyspJf/vKXPP7444wa\nNQqA0NBQbr75ZgDCw8MJDg6msLCQvn372j3v5MmTW74fM2YM2dnZbS5jr6wAGzduZMSIES3T0dHR\nREdHAzBkyBBKS0tpbGxs+auHPTjaOtsejrbeXowjrbPt4Ujr7Jdffslrr73GG2+8gY+PT8vjzrjO\n9iTaRtonqyN91nSX7SM43jrbFkdaZ9viSOtseznSemvvbaTDDXFMSUlpaflZWVmEhIS0NOk+ffpQ\nWVnJiRMnaGhoYMOGDaSkpFx0GXtlBXjppZe49957GTNmTMtja9eu5e9//zvQfJj09OnThIaGdnrW\ntvJWVFQwc+ZM6urqANixYwcxMTEO+94CZGZmMmjQoJbp119/nffffx9ovlJQYGCg3Tc+jrbOtoej\nrbc/xtHW2fZwlHW2oqKCP/7xj/z1r3/F39+/1XPOuM72JNpGdn1WR/us6S7bR3C8dbYtjrTOXoyj\nrbPt5SjrrSNsIw02Rzu+SfOlQHfu3InBYGDevHns27cPHx8fJk6cyI4dO5g/fz4AN9xwAzNnzrzg\nMt//B7ZH1lGjRnHNNdcwZMiQlnknTZrELbfcwlNPPcXZs2epr6/n4YcfbhnDbM+8EydO5F//+hfv\nvPMObm5uxMbG8tvf/haDweBw7+3EiRMBuPXWW/nHP/5BcHAwAAUFBTz99NPYbDYaGhq67PKxe/fu\n5Q9/+AMnT57ExcWF0NBQJkyYQJ8+fRxynb1YXkdbb9t6bx1pnW0rKzjOOrtixQoWL15MZGRky2PD\nhg1j4MCBDrnOSmvaRnZtVkf7rGkrKzjOZw041zZS20f75QXHWW8dYRvpkAVNRERERESkJ3K4IY4i\nIiIiIiI9lQqaiIiIiIiIg1BBExERERERcRAqaCIiIiIiIg5CBU1ERERERMRBqKCJiIiIiIg4CBU0\nERERERERB6GCJiIiIiIi4iD+H4+u0AEVg8YvAAAAAElFTkSuQmCC\n", + "text/plain": [ + "" + ] + }, + "metadata": { + "tags": [] + } + } + ] + }, + { + "metadata": { + "id": "Hjn0HJVoTvJ0", + "colab_type": "code", + "outputId": "789735eb-61fc-4745-e133-c161f9d6fec9", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 51 + } + }, + "cell_type": "code", + "source": [ + "# Test performance\n", + "trainer.run_test_loop()\n", + "print(\"Test loss: {0:.2f}\".format(trainer.train_state['test_loss']))\n", + "print(\"Test Accuracy: {0:.1f}%\".format(trainer.train_state['test_acc']))" + ], + "execution_count": 0, + "outputs": [ + { + "output_type": "stream", + "text": [ + "Test loss: 0.53\n", + "Test Accuracy: 84.5%\n" + ], + "name": "stdout" + } + ] + }, + { + "metadata": { + "id": "ZQVrGTNNTvH0", + "colab_type": "code", + "colab": {} + }, + "cell_type": "code", + "source": [ + "# Save all results\n", + "trainer.save_train_state()" + ], + "execution_count": 0, + "outputs": [] + }, + { + "metadata": { + "id": "7CL689FebJhf", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "Much better performance! If you let it train long enough, we'll actually reah ~95% accuracy :)" + ] + }, + { + "metadata": { + "id": "02iDXCtiYo5K", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "## Inference" + ] + }, + { + "metadata": { + "id": "cVT--tAvnOu7", + "colab_type": "code", + "colab": {} + }, + "cell_type": "code", + "source": [ + "from pylab import rcParams\n", + "rcParams['figure.figsize'] = 1, 1" + ], + "execution_count": 0, + "outputs": [] + }, + { + "metadata": { + "id": "1qQjnXpnYoMM", + "colab_type": "code", + "colab": {} + }, + "cell_type": "code", + "source": [ + "class Inference(object):\n", + " def __init__(self, model, vectorizer):\n", + " self.model = model\n", + " self.model.to(\"cpu\")\n", + " self.vectorizer = vectorizer\n", + " \n", + " def predict_category(self, image):\n", + " # Vectorize\n", + " image_vector = self.vectorizer.vectorize(image)\n", + " image_vector = torch.tensor(image_vector).unsqueeze(0)\n", + " \n", + " # Forward pass\n", + " self.model.eval()\n", + " y_pred = self.model(x=image_vector, apply_softmax=True)\n", + "\n", + " # Top category\n", + " y_prob, indices = y_pred.max(dim=1)\n", + " index = indices.item()\n", + "\n", + " # Predicted category\n", + " category = vectorizer.category_vocab.lookup_index(index)\n", + " probability = y_prob.item()\n", + " return {'category': category, 'probability': probability}\n", + " \n", + " def predict_top_k(self, image, k):\n", + " # Vectorize\n", + " image_vector = self.vectorizer.vectorize(image)\n", + " image_vector = torch.tensor(image_vector).unsqueeze(0)\n", + " \n", + " # Forward pass\n", + " self.model.eval()\n", + " y_pred = self.model(x=image_vector, apply_softmax=True)\n", + " \n", + " # Top k categories\n", + " y_prob, indices = torch.topk(y_pred, k=k)\n", + " probabilities = y_prob.detach().numpy()[0]\n", + " indices = indices.detach().numpy()[0]\n", + "\n", + " # Results\n", + " results = []\n", + " for probability, index in zip(probabilities, indices):\n", + " category = self.vectorizer.category_vocab.lookup_index(index)\n", + " results.append({'category': category, 'probability': probability})\n", + "\n", + " return results" + ], + "execution_count": 0, + "outputs": [] + }, + { + "metadata": { + "id": "MbTRzW8CYoWc", + "colab_type": "code", + "colab": {} + }, + "cell_type": "code", + "source": [ + "# Get a sample\n", + "sample = split_df[split_df.split==\"test\"].iloc[1000]" + ], + "execution_count": 0, + "outputs": [] + }, + { + "metadata": { + "id": "DswQ0pikYoR_", + "colab_type": "code", + "outputId": "dedeef0c-be8e-4015-b401-7bb57080a641", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 139 + } + }, + "cell_type": "code", + "source": [ + "# Inference\n", + "inference = Inference(model=model, vectorizer=vectorizer)\n", + "prediction = inference.predict_category(sample.image)\n", + "print (\"Actual:\", sample.category)\n", + "plt.imshow(sample.image)\n", + "plt.axis(\"off\")\n", + "print(\"({} → p={:0.2f})\".format(prediction['category'], \n", + " prediction['probability']))" + ], + "execution_count": 0, + "outputs": [ + { + "output_type": "stream", + "text": [ + "Actual: car\n", + "(car → p=1.00)\n" + ], + "name": "stdout" + }, + { + "output_type": "display_data", + "data": { + "image/png": "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\n", + "text/plain": [ + "" + ] + }, + "metadata": { + "tags": [] + } + } + ] + }, + { + "metadata": { + "id": "72_-iRQxYoQK", + "colab_type": "code", + "outputId": "7a65b46b-fc51-4b24-eba1-d2a404af6e91", + "colab": { + "base_uri": "https://localhost:8080/", + "height": 292 + } + }, + "cell_type": "code", + "source": [ + "# # Top-k inference\n", + "top_k = inference.predict_top_k(sample.image, k=len(vectorizer.category_vocab))\n", + "print (\"Actual:\", sample.category)\n", + "plt.imshow(sample.image)\n", + "plt.axis(\"off\")\n", + "for result in top_k:\n", + " print (\"{} → (p={:0.2f})\".format(result['category'], \n", + " result['probability']))" + ], + "execution_count": 0, + "outputs": [ + { + "output_type": "stream", + "text": [ + "Actual: car\n", + "car → (p=1.00)\n", + "ship → (p=0.00)\n", + "truck → (p=0.00)\n", + "plane → (p=0.00)\n", + "cat → (p=0.00)\n", + "bird → (p=0.00)\n", + "frog → (p=0.00)\n", + "deer → (p=0.00)\n", + "horse → (p=0.00)\n", + "dog → (p=0.00)\n" + ], + "name": "stdout" + }, + { + "output_type": "display_data", + "data": { + "image/png": "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\n", + "text/plain": [ + "" + ] + }, + "metadata": { + "tags": [] + } + } + ] + }, + { + "metadata": { + "id": "1YHneO3SStOp", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "# TODO" + ] + }, + { + "metadata": { + "id": "gGHaKTe1SuEk", + "colab_type": "text" + }, + "cell_type": "markdown", + "source": [ + "- segmentation\n", + "- interpretability via activation maps\n", + "- processing images of different sizes\n", + "- save split_dataframe (wiht numpy image arrays) to csv and reload dataframe from csv during inference" + ] + } + ] +} \ No newline at end of file