{
"cells": [
{
"cell_type": "markdown",
"metadata": {
"id": "EKOTlwcmxmej"
},
"source": [
"# BERT Fine-Tuning Tutorial with PyTorch\n",
"\n",
"By Chris McCormick and Nick Ryan"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "MPgpITmdwvX0"
},
"source": [
"*Revised on March 20, 2020 - Switched to `tokenizer.encode_plus` and added validation loss. See [Revision History](https://colab.research.google.com/drive/1pTuQhug6Dhl9XalKB0zUGf4FIdYFlpcX#scrollTo=IKzLS9ohzGVu) at the end for details.*\n",
"\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "BJR6t_gCQe_x"
},
"source": [
"In this tutorial I'll show you how to use BERT with the huggingface PyTorch library to quickly and efficiently fine-tune a model to get near state of the art performance in sentence classification. More broadly, I describe the practical application of transfer learning in NLP to create high performance models with minimal effort on a range of NLP tasks.\n",
"\n",
"This post is presented in two forms--as a blog post [here](http://mccormickml.com/2019/07/22/BERT-fine-tuning/) and as a Colab Notebook [here](https://colab.research.google.com/drive/1pTuQhug6Dhl9XalKB0zUGf4FIdYFlpcX).\n",
"\n",
"The content is identical in both, but:\n",
"* The blog post includes a comments section for discussion.\n",
"* The Colab Notebook will allow you to run the code and inspect it as you read through.\n",
"\n",
"I've also published a video walkthrough of this post on my YouTube channel! [Part 1](https://youtu.be/x66kkDnbzi4) and [Part 2](https://youtu.be/Hnvb9b7a_Ps).\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "jrC9__lXxTJz"
},
"source": [
"# Contents"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "p9MCBOq4xUpr"
},
"source": [
"See \"Table of contents\" in the sidebar to the left."
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "ADkUGTqixRWo"
},
"source": [
"# Introduction"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "L9vxxTBsuL24"
},
"source": [
"\n",
"## History\n",
"\n",
"2018 was a breakthrough year in NLP. Transfer learning, particularly models like Allen AI's ELMO, OpenAI's Open-GPT, and Google's BERT allowed researchers to smash multiple benchmarks with minimal task-specific fine-tuning and provided the rest of the NLP community with pretrained models that could easily (with less data and less compute time) be fine-tuned and implemented to produce state of the art results. Unfortunately, for many starting out in NLP and even for some experienced practicioners, the theory and practical application of these powerful models is still not well understood.\n"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "qCgvR9INuP5q"
},
"source": [
"\n",
"## What is BERT?\n",
"\n",
"BERT (Bidirectional Encoder Representations from Transformers), released in late 2018, is the model we will use in this tutorial to provide readers with a better understanding of and practical guidance for using transfer learning models in NLP. BERT is a method of pretraining language representations that was used to create models that NLP practicioners can then download and use for free. You can either use these models to extract high quality language features from your text data, or you can fine-tune these models on a specific task (classification, entity recognition, question answering, etc.) with your own data to produce state of the art predictions.\n",
"\n",
"This post will explain how you can modify and fine-tune BERT to create a powerful NLP model that quickly gives you state of the art results.\n"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "DaVGdtOkuXUZ"
},
"source": [
"\n",
"## Advantages of Fine-Tuning\n"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "5llwu8GBuqMb"
},
"source": [
"\n",
"In this tutorial, we will use BERT to train a text classifier. Specifically, we will take the pre-trained BERT model, add an untrained layer of neurons on the end, and train the new model for our classification task. Why do this rather than train a train a specific deep learning model (a CNN, BiLSTM, etc.) that is well suited for the specific NLP task you need?\n",
"\n",
"1. **Quicker Development**\n",
"\n",
" * First, the pre-trained BERT model weights already encode a lot of information about our language. As a result, it takes much less time to train our fine-tuned model - it is as if we have already trained the bottom layers of our network extensively and only need to gently tune them while using their output as features for our classification task. In fact, the authors recommend only 2-4 epochs of training for fine-tuning BERT on a specific NLP task (compared to the hundreds of GPU hours needed to train the original BERT model or a LSTM from scratch!).\n",
"\n",
"2. **Less Data**\n",
"\n",
" * In addition and perhaps just as important, because of the pre-trained weights this method allows us to fine-tune our task on a much smaller dataset than would be required in a model that is built from scratch. A major drawback of NLP models built from scratch is that we often need a prohibitively large dataset in order to train our network to reasonable accuracy, meaning a lot of time and energy had to be put into dataset creation. By fine-tuning BERT, we are now able to get away with training a model to good performance on a much smaller amount of training data.\n",
"\n",
"3. **Better Results**\n",
"\n",
" * Finally, this simple fine-tuning procedure (typically adding one fully-connected layer on top of BERT and training for a few epochs) was shown to achieve state of the art results with minimal task-specific adjustments for a wide variety of tasks: classification, language inference, semantic similarity, question answering, etc. Rather than implementing custom and sometimes-obscure architetures shown to work well on a specific task, simply fine-tuning BERT is shown to be a better (or at least equal) alternative.\n"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "ZEynC5F4u7Nb"
},
"source": [
"\n",
"### A Shift in NLP\n",
"\n",
"This shift to transfer learning parallels the same shift that took place in computer vision a few years ago. Creating a good deep learning network for computer vision tasks can take millions of parameters and be very expensive to train. Researchers discovered that deep networks learn hierarchical feature representations (simple features like edges at the lowest layers with gradually more complex features at higher layers). Rather than training a new network from scratch each time, the lower layers of a trained network with generalized image features could be copied and transfered for use in another network with a different task. It soon became common practice to download a pre-trained deep network and quickly retrain it for the new task or add additional layers on top - vastly preferable to the expensive process of training a network from scratch. For many, the introduction of deep pre-trained language models in 2018 (ELMO, BERT, ULMFIT, Open-GPT, etc.) signals the same shift to transfer learning in NLP that computer vision saw.\n",
"\n",
"Let's get started!"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "RX_ZDhicpHkV"
},
"source": [
"# 1. Setup"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "nSU7yERLP_66"
},
"source": [
"## 1.1. Using Colab GPU for Training\n"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "GI0iOY8zvZzL"
},
"source": [
"\n",
"Google Colab offers free GPUs and TPUs! Since we'll be training a large neural network it's best to take advantage of this (in this case we'll attach a GPU), otherwise training will take a very long time.\n",
"\n",
"A GPU can be added by going to the menu and selecting:\n",
"\n",
"`Edit 🡒 Notebook Settings 🡒 Hardware accelerator 🡒 (GPU)`\n",
"\n",
"Then run the following cell to confirm that the GPU is detected."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
},
"id": "DEfSbAA4QHas",
"outputId": "dc2bc085-7d9e-4490-896b-a3b1cc09a77d"
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Found GPU at: /device:GPU:0\n"
]
}
],
"source": [
"import tensorflow as tf\n",
"\n",
"# Get the GPU device name.\n",
"device_name = tf.test.gpu_device_name()\n",
"\n",
"# The device name should look like the following:\n",
"if device_name == '/device:GPU:0':\n",
" print('Found GPU at: {}'.format(device_name))\n",
"else:\n",
" raise SystemError('GPU device not found')"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "cqG7FzRVFEIv"
},
"source": [
"In order for torch to use the GPU, we need to identify and specify the GPU as the device. Later, in our training loop, we will load data onto the device."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
},
"id": "oYsV4H8fCpZ-",
"outputId": "4ab1b14c-d0c8-4136-bb5c-d3d012cfb3aa"
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"There are 1 GPU(s) available.\n",
"We will use the GPU: Tesla T4\n"
]
}
],
"source": [
"import torch\n",
"\n",
"# If there's a GPU available...\n",
"if torch.cuda.is_available():\n",
"\n",
" # Tell PyTorch to use the GPU.\n",
" device = torch.device(\"cuda\")\n",
"\n",
" print('There are %d GPU(s) available.' % torch.cuda.device_count())\n",
"\n",
" print('We will use the GPU:', torch.cuda.get_device_name(0))\n",
"\n",
"# If not...\n",
"else:\n",
" print('No GPU available, using the CPU instead.')\n",
" device = torch.device(\"cpu\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
},
"id": "Wsord9N7vT44",
"outputId": "56904325-0974-4687-d43c-d0d690b51cbe"
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Looking in indexes: https://pypi.org/simple, https://us-python.pkg.dev/colab-wheels/public/simple/\n",
"Requirement already satisfied: pandas-ml in /usr/local/lib/python3.8/dist-packages (0.6.1)\n",
"Requirement already satisfied: pandas>=0.19.0 in /usr/local/lib/python3.8/dist-packages (from pandas-ml) (1.3.5)\n",
"Requirement already satisfied: enum34 in /usr/local/lib/python3.8/dist-packages (from pandas-ml) (1.1.10)\n",
"Requirement already satisfied: python-dateutil>=2.7.3 in /usr/local/lib/python3.8/dist-packages (from pandas>=0.19.0->pandas-ml) (2.8.2)\n",
"Requirement already satisfied: pytz>=2017.3 in /usr/local/lib/python3.8/dist-packages (from pandas>=0.19.0->pandas-ml) (2022.7.1)\n",
"Requirement already satisfied: numpy>=1.17.3 in /usr/local/lib/python3.8/dist-packages (from pandas>=0.19.0->pandas-ml) (1.21.6)\n",
"Requirement already satisfied: six>=1.5 in /usr/local/lib/python3.8/dist-packages (from python-dateutil>=2.7.3->pandas>=0.19.0->pandas-ml) (1.15.0)\n"
]
}
],
"source": [
"pip install pandas-ml"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "2ElsnSNUridI"
},
"source": [
"## 1.2. Installing the Hugging Face Library\n"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "G_N2UDLevYWn"
},
"source": [
"\n",
"Next, let's install the [transformers](https://github.com/huggingface/transformers) package from Hugging Face which will give us a pytorch interface for working with BERT. (This library contains interfaces for other pretrained language models like OpenAI's GPT and GPT-2.) We've selected the pytorch interface because it strikes a nice balance between the high-level APIs (which are easy to use but don't provide insight into how things work) and tensorflow code (which contains lots of details but often sidetracks us into lessons about tensorflow, when the purpose here is BERT!).\n",
"\n",
"At the moment, the Hugging Face library seems to be the most widely accepted and powerful pytorch interface for working with BERT. In addition to supporting a variety of different pre-trained transformer models, the library also includes pre-built modifications of these models suited to your specific task. For example, in this tutorial we will use `BertForSequenceClassification`.\n",
"\n",
"The library also includes task-specific classes for token classification, question answering, next sentence prediciton, etc. Using these pre-built classes simplifies the process of modifying BERT for your purposes.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
},
"id": "0NmMdkZO8R6q",
"outputId": "f6eb304c-cb24-4aa6-9275-af4488e84eaa"
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Looking in indexes: https://pypi.org/simple, https://us-python.pkg.dev/colab-wheels/public/simple/\n",
"Requirement already satisfied: transformers in /usr/local/lib/python3.8/dist-packages (4.26.0)\n",
"Requirement already satisfied: requests in /usr/local/lib/python3.8/dist-packages (from transformers) (2.25.1)\n",
"Requirement already satisfied: numpy>=1.17 in /usr/local/lib/python3.8/dist-packages (from transformers) (1.21.6)\n",
"Requirement already satisfied: packaging>=20.0 in /usr/local/lib/python3.8/dist-packages (from transformers) (23.0)\n",
"Requirement already satisfied: regex!=2019.12.17 in /usr/local/lib/python3.8/dist-packages (from transformers) (2022.6.2)\n",
"Requirement already satisfied: tqdm>=4.27 in /usr/local/lib/python3.8/dist-packages (from transformers) (4.64.1)\n",
"Requirement already satisfied: huggingface-hub<1.0,>=0.11.0 in /usr/local/lib/python3.8/dist-packages (from transformers) (0.12.0)\n",
"Requirement already satisfied: tokenizers!=0.11.3,<0.14,>=0.11.1 in /usr/local/lib/python3.8/dist-packages (from transformers) (0.13.2)\n",
"Requirement already satisfied: pyyaml>=5.1 in /usr/local/lib/python3.8/dist-packages (from transformers) (6.0)\n",
"Requirement already satisfied: filelock in /usr/local/lib/python3.8/dist-packages (from transformers) (3.9.0)\n",
"Requirement already satisfied: typing-extensions>=3.7.4.3 in /usr/local/lib/python3.8/dist-packages (from huggingface-hub<1.0,>=0.11.0->transformers) (4.4.0)\n",
"Requirement already satisfied: urllib3<1.27,>=1.21.1 in /usr/local/lib/python3.8/dist-packages (from requests->transformers) (1.24.3)\n",
"Requirement already satisfied: certifi>=2017.4.17 in /usr/local/lib/python3.8/dist-packages (from requests->transformers) (2022.12.7)\n",
"Requirement already satisfied: chardet<5,>=3.0.2 in /usr/local/lib/python3.8/dist-packages (from requests->transformers) (4.0.0)\n",
"Requirement already satisfied: idna<3,>=2.5 in /usr/local/lib/python3.8/dist-packages (from requests->transformers) (2.10)\n"
]
}
],
"source": [
"!pip install transformers"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "lxddqmruamSj"
},
"source": [
"The code in this notebook is actually a simplified version of the [run_glue.py](https://github.com/huggingface/transformers/blob/master/examples/run_glue.py) example script from huggingface.\n",
"\n",
"`run_glue.py` is a helpful utility which allows you to pick which GLUE benchmark task you want to run on, and which pre-trained model you want to use (you can see the list of possible models [here](https://github.com/huggingface/transformers/blob/e6cff60b4cbc1158fbd6e4a1c3afda8dc224f566/examples/run_glue.py#L69)). It also supports using either the CPU, a single GPU, or multiple GPUs. It even supports using 16-bit precision if you want further speed up.\n",
"\n",
"Unfortunately, all of this configurability comes at the cost of *readability*. In this Notebook, we've simplified the code greatly and added plenty of comments to make it clear what's going on."
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "guw6ZNtaswKc"
},
"source": [
"# 2. Loading CoLA Dataset\n"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "_9ZKxKc04Btk"
},
"source": [
"We'll use [The Corpus of Linguistic Acceptability (CoLA)](https://nyu-mll.github.io/CoLA/) dataset for single sentence classification. It's a set of sentences labeled as grammatically correct or incorrect. It was first published in May of 2018, and is one of the tests included in the \"GLUE Benchmark\" on which models like BERT are competing.\n"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "4JrUHXms16cn"
},
"source": [
"## 2.1. Download & Extract"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "3ZNVW6xd0T0X"
},
"source": [
"We'll use the `wget` package to download the dataset to the Colab instance's file system."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
},
"id": "5m6AnuFv0QXQ",
"outputId": "ea0787e1-e1bb-461d-de0c-1fdfe5d241d9"
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Looking in indexes: https://pypi.org/simple, https://us-python.pkg.dev/colab-wheels/public/simple/\n",
"Requirement already satisfied: wget in /usr/local/lib/python3.8/dist-packages (3.2)\n"
]
}
],
"source": [
"!pip install wget"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "08pO03Ff1BjI"
},
"source": [
"The dataset is hosted on GitHub in this repo: https://nyu-mll.github.io/CoLA/"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "_mKctx-ll2FB"
},
"source": [
"Unzip the dataset to the file system. You can browse the file system of the Colab instance in the sidebar on the left."
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "oQUy9Tat2EF_"
},
"source": [
"## 2.2. Parse"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "xeyVCXT31EZQ"
},
"source": [
"We can see from the file names that both `tokenized` and `raw` versions of the data are available.\n",
"\n",
"We can't use the pre-tokenized version because, in order to apply the pre-trained BERT, we *must* use the tokenizer provided by the model. This is because (1) the model has a specific, fixed vocabulary and (2) the BERT tokenizer has a particular way of handling out-of-vocabulary words."
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "MYWzeGSY2xh3"
},
"source": [
"We'll use pandas to parse the \"in-domain\" training set and look at a few of its properties and data points."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/",
"height": 399
},
"id": "_UkeC7SG2krJ",
"outputId": "70b77ac5-bcc2-4019-e26f-e65214635cea"
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Number of training sentences: 12,740\n",
"\n"
]
},
{
"data": {
"text/html": [
"\n",
"
\n",
"
\n",
"
\n",
"\n",
"
\n",
" \n",
"
\n",
"
\n",
"
sentence
\n",
"
label
\n",
"
\n",
" \n",
" \n",
"
\n",
"
11531
\n",
"
here's what i tested before and after hurr irm...
\n",
"
1
\n",
"
\n",
"
\n",
"
11820
\n",
"
chainsawwielding nun helps clear irma debris
\n",
"
1
\n",
"
\n",
"
\n",
"
7080
\n",
"
guess which one works for the trump administra...
\n",
"
1
\n",
"
\n",
"
\n",
"
9592
\n",
"
i takes a hit from hurricane irma road closure...
\n",
"
0
\n",
"
\n",
"
\n",
"
6455
\n",
"
hurricane maria floods a quaer of dr banana farms
\n",
"
0
\n",
"
\n",
"
\n",
"
3222
\n",
"
lots of harvey pups waiting for owners here at...
\n",
"
0
\n",
"
\n",
"
\n",
"
173
\n",
"
the iran eahquake as recorded at the sep seism...
\n",
"
1
\n",
"
\n",
"
\n",
"
1346
\n",
"
askcaia for help pls mexico is in need after t...
\n",
"
1
\n",
"
\n",
"
\n",
"
11830
\n",
"
florida governor warns hurricane irma will be ...
\n",
"
1
\n",
"
\n",
"
\n",
"
1157
\n",
"
mexico eahquake survivor floors came down like...
\n",
"
0
\n",
"
\n",
" \n",
"
\n",
"
\n",
" \n",
" \n",
" \n",
"\n",
" \n",
"
\n",
"
\n",
" "
],
"text/plain": [
" sentence label\n",
"11531 here's what i tested before and after hurr irm... 1\n",
"11820 chainsawwielding nun helps clear irma debris 1\n",
"7080 guess which one works for the trump administra... 1\n",
"9592 i takes a hit from hurricane irma road closure... 0\n",
"6455 hurricane maria floods a quaer of dr banana farms 0\n",
"3222 lots of harvey pups waiting for owners here at... 0\n",
"173 the iran eahquake as recorded at the sep seism... 1\n",
"1346 askcaia for help pls mexico is in need after t... 1\n",
"11830 florida governor warns hurricane irma will be ... 1\n",
"1157 mexico eahquake survivor floors came down like... 0"
]
},
"execution_count": 86,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"import pandas as pd\n",
"\n",
"# Load the dataset into a pandas dataframe.\n",
"df = pd.read_csv(\"./train.tsv\", delimiter='\\t', header=None, names=['sentence', 'label'])\n",
"\n",
"# Report the number of sentences.\n",
"print('Number of training sentences: {:,}\\n'.format(df.shape[0]))\n",
"\n",
"# Display 10 random rows from the data.\n",
"df.sample(10)"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "kfWzpPi92UAH"
},
"source": [
"The two properties we actually care about are the the `sentence` and its `label`, which is referred to as the \"acceptibility judgment\" (0=unacceptable, 1=acceptable)."
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "H_LpQfzCn9_o"
},
"source": [
"Here are five sentences which are labeled as not grammatically acceptible. Note how much more difficult this task is than something like sentiment analysis!"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/",
"height": 206
},
"id": "blqIvQaQncdJ",
"outputId": "997b0d00-37cf-4541-80f7-ef629482dbcc"
},
"outputs": [
{
"data": {
"text/html": [
"\n",
"
\n",
"
\n",
"
\n",
"\n",
"
\n",
" \n",
"
\n",
"
\n",
"
sentence
\n",
"
label
\n",
"
\n",
" \n",
" \n",
"
\n",
"
3531
\n",
"
heroic hurricane harvey victims last words sav...
\n",
"
0
\n",
"
\n",
"
\n",
"
1074
\n",
"
mexico eahquake death toll surpasses via fox news
\n",
"
0
\n",
"
\n",
"
\n",
"
9694
\n",
"
hurricaineirma leaves behind nasty stew of bac...
\n",
"
0
\n",
"
\n",
"
\n",
"
9915
\n",
"
photos of the damage to the keys irma florid
\n",
"
0
\n",
"
\n",
"
\n",
"
1231
\n",
"
friend sent me this picture of a building on h...
\n",
"
0
\n",
"
\n",
" \n",
"
\n",
"
\n",
" \n",
" \n",
" \n",
"\n",
" \n",
"
\n",
"
\n",
" "
],
"text/plain": [
" sentence label\n",
"3531 heroic hurricane harvey victims last words sav... 0\n",
"1074 mexico eahquake death toll surpasses via fox news 0\n",
"9694 hurricaineirma leaves behind nasty stew of bac... 0\n",
"9915 photos of the damage to the keys irma florid 0\n",
"1231 friend sent me this picture of a building on h... 0"
]
},
"execution_count": 87,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"df.loc[df.label == 0].sample(5)[['sentence', 'label']]"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "4SMZ5T5Imhlx"
},
"source": [
"\n",
"\n",
"Let's extract the sentences and labels of our training set as numpy ndarrays."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "GuE5BqICAne2"
},
"outputs": [],
"source": [
"# Get the lists of sentences and their labels.\n",
"sentences = df.sentence.values\n",
"labels = df.label.values"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "ex5O1eV-Pfct"
},
"source": [
"# 3. Tokenization & Input Formatting\n",
"\n",
"In this section, we'll transform our dataset into the format that BERT can be trained on."
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "-8kEDRvShcU5"
},
"source": [
"## 3.1. BERT Tokenizer"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "bWOPOyWghJp2"
},
"source": [
"\n",
"To feed our text to BERT, it must be split into tokens, and then these tokens must be mapped to their index in the tokenizer vocabulary.\n",
"\n",
"The tokenization must be performed by the tokenizer included with BERT--the below cell will download this for us. We'll be using the \"uncased\" version here.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
},
"id": "Z474sSC6oe7A",
"outputId": "21d32170-863d-4395-9dcf-b03877c19fb9"
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Loading BERT tokenizer...\n"
]
}
],
"source": [
"from transformers import BertTokenizer\n",
"\n",
"# Load the BERT tokenizer.\n",
"print('Loading BERT tokenizer...')\n",
"tokenizer = BertTokenizer.from_pretrained('bert-base-uncased', do_lower_case=True)"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "dFzmtleW6KmJ"
},
"source": [
"Let's apply the tokenizer to one sentence just to see the output.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
},
"id": "dLIbudgfh6F0",
"outputId": "b3585f34-ba5c-45ea-9b05-e1d28a100ff2"
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
" Original: breakingnews death toll from . magnitude eahquake at iran iraq border now stands at kurdistan\n",
"Tokenized: ['breaking', '##ne', '##ws', 'death', 'toll', 'from', '.', 'magnitude', 'ea', '##h', '##qua', '##ke', 'at', 'iran', 'iraq', 'border', 'now', 'stands', 'at', 'kurdistan']\n",
"Token IDs: [4911, 2638, 9333, 2331, 9565, 2013, 1012, 10194, 19413, 2232, 16211, 3489, 2012, 4238, 5712, 3675, 2085, 4832, 2012, 23627]\n"
]
}
],
"source": [
"# Print the original sentence.\n",
"print(' Original: ', sentences[0])\n",
"\n",
"# Print the sentence split into tokens.\n",
"print('Tokenized: ', tokenizer.tokenize(sentences[0]))\n",
"\n",
"# Print the sentence mapped to token ids.\n",
"print('Token IDs: ', tokenizer.convert_tokens_to_ids(tokenizer.tokenize(sentences[0])))"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "WeNIc4auFUdF"
},
"source": [
"When we actually convert all of our sentences, we'll use the `tokenize.encode` function to handle both steps, rather than calling `tokenize` and `convert_tokens_to_ids` separately.\n",
"\n",
"Before we can do that, though, we need to talk about some of BERT's formatting requirements."
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "viKGCCh8izww"
},
"source": [
"## 3.2. Required Formatting"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "yDcqNlvVhL5W"
},
"source": [
"The above code left out a few required formatting steps that we'll look at here.\n",
"\n",
"*Side Note: The input format to BERT seems \"over-specified\" to me... We are required to give it a number of pieces of information which seem redundant, or like they could easily be inferred from the data without us explicity providing it. But it is what it is, and I suspect it will make more sense once I have a deeper understanding of the BERT internals.*\n",
"\n",
"We are required to:\n",
"1. Add special tokens to the start and end of each sentence.\n",
"2. Pad & truncate all sentences to a single constant length.\n",
"3. Explicitly differentiate real tokens from padding tokens with the \"attention mask\".\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "V6mceWWOjZnw"
},
"source": [
"### Special Tokens\n"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "Ykk0P9JiKtVe"
},
"source": [
"\n",
"**`[SEP]`**\n",
"\n",
"At the end of every sentence, we need to append the special `[SEP]` token.\n",
"\n",
"This token is an artifact of two-sentence tasks, where BERT is given two separate sentences and asked to determine something (e.g., can the answer to the question in sentence A be found in sentence B?).\n",
"\n",
"I am not certain yet why the token is still required when we have only single-sentence input, but it is!\n"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "86C9objaKu8f"
},
"source": [
"**`[CLS]`**\n",
"\n",
"For classification tasks, we must prepend the special `[CLS]` token to the beginning of every sentence.\n",
"\n",
"This token has special significance. BERT consists of 12 Transformer layers. Each transformer takes in a list of token embeddings, and produces the same number of embeddings on the output (but with the feature values changed, of course!).\n",
"\n",
"\n",
"\n",
"On the output of the final (12th) transformer, *only the first embedding (corresponding to the [CLS] token) is used by the classifier*.\n",
"\n",
"> \"The first token of every sequence is always a special classification token (`[CLS]`). The final hidden state\n",
"corresponding to this token is used as the aggregate sequence representation for classification\n",
"tasks.\" (from the [BERT paper](https://arxiv.org/pdf/1810.04805.pdf))\n",
"\n",
"You might think to try some pooling strategy over the final embeddings, but this isn't necessary. Because BERT is trained to only use this [CLS] token for classification, we know that the model has been motivated to encode everything it needs for the classification step into that single 768-value embedding vector. It's already done the pooling for us!\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "u51v0kFxeteu"
},
"source": [
"### Sentence Length & Attention Mask\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "qPNuwqZVK3T6"
},
"source": [
"The sentences in our dataset obviously have varying lengths, so how does BERT handle this?\n",
"\n",
"BERT has two constraints:\n",
"1. All sentences must be padded or truncated to a single, fixed length.\n",
"2. The maximum sentence length is 512 tokens.\n",
"\n",
"Padding is done with a special `[PAD]` token, which is at index 0 in the BERT vocabulary. The below illustration demonstrates padding out to a \"MAX_LEN\" of 8 tokens.\n",
"\n",
"\n",
"\n",
"The \"Attention Mask\" is simply an array of 1s and 0s indicating which tokens are padding and which aren't (seems kind of redundant, doesn't it?!). This mask tells the \"Self-Attention\" mechanism in BERT not to incorporate these PAD tokens into its interpretation of the sentence.\n",
"\n",
"The maximum length does impact training and evaluation speed, however.\n",
"For example, with a Tesla K80:\n",
"\n",
"`MAX_LEN = 128 --> Training epochs take ~5:28 each`\n",
"\n",
"`MAX_LEN = 64 --> Training epochs take ~2:57 each`\n",
"\n",
"\n",
"\n",
"\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "2-Th8bRio6A4"
},
"source": [
"\n",
"[](https://www.chrismccormick.ai/bert-ebook?utm_source=colab&utm_medium=banner&utm_campaign=bert_ebook&utm_content=colab0)\n"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "l6w8elb-58GJ"
},
"source": [
"## 3.3. Tokenize Dataset"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "U28qy4P-NwQ9"
},
"source": [
"The transformers library provides a helpful `encode` function which will handle most of the parsing and data prep steps for us.\n",
"\n",
"Before we are ready to encode our text, though, we need to decide on a **maximum sentence length** for padding / truncating to.\n",
"\n",
"The below cell will perform one tokenization pass of the dataset in order to measure the maximum sentence length."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
},
"id": "cKsH2sU0OCQA",
"outputId": "7576d88a-8b08-48b3-f059-c15bf34008b8"
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Max sentence length: 512\n"
]
}
],
"source": [
"max_len = 512\n",
"\n",
"# For every sentence...\n",
"for sent in sentences:\n",
"\n",
" # Tokenize the text and add `[CLS]` and `[SEP]` tokens.\n",
" input_ids = tokenizer.encode(sent, add_special_tokens=True)\n",
"\n",
" # Update the maximum sentence length.\n",
" max_len = max(max_len, len(input_ids))\n",
"\n",
"print('Max sentence length: ', max_len)"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "1M296yz577fV"
},
"source": [
"Just in case there are some longer test sentences, I'll set the maximum length to 64.\n"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "tIWAoWL2RK1p"
},
"source": [
"Now we're ready to perform the real tokenization.\n",
"\n",
"The `tokenizer.encode_plus` function combines multiple steps for us:\n",
"\n",
"1. Split the sentence into tokens.\n",
"2. Add the special `[CLS]` and `[SEP]` tokens.\n",
"3. Map the tokens to their IDs.\n",
"4. Pad or truncate all sentences to the same length.\n",
"5. Create the attention masks which explicitly differentiate real tokens from `[PAD]` tokens.\n",
"\n",
"The first four features are in `tokenizer.encode`, but I'm using `tokenizer.encode_plus` to get the fifth item (attention masks). Documentation is [here](https://huggingface.co/transformers/main_classes/tokenizer.html?highlight=encode_plus#transformers.PreTrainedTokenizer.encode_plus).\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
},
"id": "2bBdb3pt8LuQ",
"outputId": "761c3d67-dc1f-4580-f8ec-6cf0f9f209e0"
},
"outputs": [
{
"name": "stderr",
"output_type": "stream",
"text": [
"Truncation was not explicitly activated but `max_length` is provided a specific value, please use `truncation=True` to explicitly truncate examples to max length. Defaulting to 'longest_first' truncation strategy. If you encode pairs of sequences (GLUE-style) with the tokenizer you can select this strategy more precisely by providing a specific strategy to `truncation`.\n",
"/usr/local/lib/python3.8/dist-packages/transformers/tokenization_utils_base.py:2339: FutureWarning: The `pad_to_max_length` argument is deprecated and will be removed in a future version, use `padding=True` or `padding='longest'` to pad to the longest sequence in the batch, or use `padding='max_length'` to pad to a max length. In this case, you can give a specific length with `max_length` (e.g. `max_length=45`) or leave max_length to None to pad to the maximal input size of the model (e.g. 512 for Bert).\n",
" warnings.warn(\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Original: breakingnews death toll from . magnitude eahquake at iran iraq border now stands at kurdistan\n",
"Token IDs: tensor([ 101, 4911, 2638, 9333, 2331, 9565, 2013, 1012, 10194, 19413,\n",
" 2232, 16211, 3489, 2012, 4238, 5712, 3675, 2085, 4832, 2012,\n",
" 23627, 102, 0, 0, 0, 0, 0, 0, 0, 0,\n",
" 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,\n",
" 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,\n",
" 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,\n",
" 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,\n",
" 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,\n",
" 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,\n",
" 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,\n",
" 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,\n",
" 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,\n",
" 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,\n",
" 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,\n",
" 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,\n",
" 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,\n",
" 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,\n",
" 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,\n",
" 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,\n",
" 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,\n",
" 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,\n",
" 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,\n",
" 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,\n",
" 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,\n",
" 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,\n",
" 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,\n",
" 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,\n",
" 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,\n",
" 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,\n",
" 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,\n",
" 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,\n",
" 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,\n",
" 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,\n",
" 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,\n",
" 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,\n",
" 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,\n",
" 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,\n",
" 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,\n",
" 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,\n",
" 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,\n",
" 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,\n",
" 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,\n",
" 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,\n",
" 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,\n",
" 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,\n",
" 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,\n",
" 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,\n",
" 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,\n",
" 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,\n",
" 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,\n",
" 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,\n",
" 0, 0])\n"
]
}
],
"source": [
"# Tokenize all of the sentences and map the tokens to thier word IDs.\n",
"input_ids = []\n",
"attention_masks = []\n",
"\n",
"# For every sentence...\n",
"for sent in sentences:\n",
" # `encode_plus` will:\n",
" # (1) Tokenize the sentence.\n",
" # (2) Prepend the `[CLS]` token to the start.\n",
" # (3) Append the `[SEP]` token to the end.\n",
" # (4) Map tokens to their IDs.\n",
" # (5) Pad or truncate the sentence to `max_length`\n",
" # (6) Create attention masks for [PAD] tokens.\n",
" encoded_dict = tokenizer.encode_plus(\n",
" sent, # Sentence to encode.\n",
" add_special_tokens = True, # Add '[CLS]' and '[SEP]'\n",
" max_length = 512, # Pad & truncate all sentences.\n",
" pad_to_max_length = True,\n",
" return_attention_mask = True, # Construct attn. masks.\n",
" return_tensors = 'pt', # Return pytorch tensors.\n",
" )\n",
"\n",
" # Add the encoded sentence to the list.\n",
" input_ids.append(encoded_dict['input_ids'])\n",
"\n",
" # And its attention mask (simply differentiates padding from non-padding).\n",
" attention_masks.append(encoded_dict['attention_mask'])\n",
"\n",
"# Convert the lists into tensors.\n",
"input_ids = torch.cat(input_ids, dim=0)\n",
"attention_masks = torch.cat(attention_masks, dim=0)\n",
"labels = torch.tensor(labels)\n",
"\n",
"# Print sentence 0, now as a list of IDs.\n",
"print('Original: ', sentences[0])\n",
"print('Token IDs:', input_ids[0])"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "aRp4O7D295d_"
},
"source": [
"## 3.4. Training & Validation Split\n"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "qu0ao7p8rb06"
},
"source": [
"Divide up our training set to use 90% for training and 10% for validation."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
},
"id": "GEgLpFVlo1Z-",
"outputId": "623f7c37-dce5-4fa3-9d16-9c655056488a"
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"11,466 training samples\n",
"1,274 validation samples\n"
]
}
],
"source": [
"from torch.utils.data import TensorDataset, random_split\n",
"\n",
"# Combine the training inputs into a TensorDataset.\n",
"dataset = TensorDataset(input_ids, attention_masks, labels)\n",
"\n",
"# Create a 90-10 train-validation split.\n",
"\n",
"# Calculate the number of samples to include in each set.\n",
"train_size = int(0.9 * len(dataset))\n",
"val_size = len(dataset) - train_size\n",
"\n",
"# Divide the dataset by randomly selecting samples.\n",
"train_dataset, val_dataset = random_split(dataset, [train_size, val_size])\n",
"\n",
"print('{:>5,} training samples'.format(train_size))\n",
"print('{:>5,} validation samples'.format(val_size))"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "dD9i6Z2pG-sN"
},
"source": [
"We'll also create an iterator for our dataset using the torch DataLoader class. This helps save on memory during training because, unlike a for loop, with an iterator the entire dataset does not need to be loaded into memory."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "XGUqOCtgqGhP"
},
"outputs": [],
"source": [
"from torch.utils.data import DataLoader, RandomSampler, SequentialSampler\n",
"\n",
"# The DataLoader needs to know our batch size for training, so we specify it\n",
"# here. For fine-tuning BERT on a specific task, the authors recommend a batch\n",
"# size of 16 or 32.\n",
"batch_size = 16\n",
"\n",
"# Create the DataLoaders for our training and validation sets.\n",
"# We'll take training samples in random order.\n",
"train_dataloader = DataLoader(\n",
" train_dataset, # The training samples.\n",
" sampler = RandomSampler(train_dataset), # Select batches randomly\n",
" batch_size = batch_size # Trains with this batch size.\n",
" )\n",
"\n",
"# For validation the order doesn't matter, so we'll just read them sequentially.\n",
"validation_dataloader = DataLoader(\n",
" val_dataset, # The validation samples.\n",
" sampler = SequentialSampler(val_dataset), # Pull out batches sequentially.\n",
" batch_size = batch_size # Evaluate with this batch size.\n",
" )"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "8bwa6Rts-02-"
},
"source": [
"# 4. Train Our Classification Model"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "3xYQ3iLO08SX"
},
"source": [
"Now that our input data is properly formatted, it's time to fine tune the BERT model."
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "D6TKgyUzPIQc"
},
"source": [
"## 4.1. BertForSequenceClassification"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "1sjzRT1V0zwm"
},
"source": [
"For this task, we first want to modify the pre-trained BERT model to give outputs for classification, and then we want to continue training the model on our dataset until that the entire model, end-to-end, is well-suited for our task.\n",
"\n",
"Thankfully, the huggingface pytorch implementation includes a set of interfaces designed for a variety of NLP tasks. Though these interfaces are all built on top of a trained BERT model, each has different top layers and output types designed to accomodate their specific NLP task. \n",
"\n",
"Here is the current list of classes provided for fine-tuning:\n",
"* BertModel\n",
"* BertForPreTraining\n",
"* BertForMaskedLM\n",
"* BertForNextSentencePrediction\n",
"* **BertForSequenceClassification** - The one we'll use.\n",
"* BertForTokenClassification\n",
"* BertForQuestionAnswering\n",
"\n",
"The documentation for these can be found under [here](https://huggingface.co/transformers/v2.2.0/model_doc/bert.html)."
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "BXYitPoE-cjH"
},
"source": [
"\n",
"\n",
"We'll be using [BertForSequenceClassification](https://huggingface.co/transformers/v2.2.0/model_doc/bert.html#bertforsequenceclassification). This is the normal BERT model with an added single linear layer on top for classification that we will use as a sentence classifier. As we feed input data, the entire pre-trained BERT model and the additional untrained classification layer is trained on our specific task.\n"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "WnQW9E-bBCRt"
},
"source": [
"OK, let's load BERT! There are a few different pre-trained BERT models available. \"bert-base-uncased\" means the version that has only lowercase letters (\"uncased\") and is the smaller version of the two (\"base\" vs \"large\").\n",
"\n",
"The documentation for `from_pretrained` can be found [here](https://huggingface.co/transformers/v2.2.0/main_classes/model.html#transformers.PreTrainedModel.from_pretrained), with the additional parameters defined [here](https://huggingface.co/transformers/v2.2.0/main_classes/configuration.html#transformers.PretrainedConfig)."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
},
"id": "gFsCTp_mporB",
"outputId": "c5d2d8c6-1ace-46f4-dab5-021d4274fa9c"
},
"outputs": [
{
"name": "stderr",
"output_type": "stream",
"text": [
"Some weights of the model checkpoint at bert-base-uncased were not used when initializing BertForSequenceClassification: ['cls.predictions.transform.dense.weight', 'cls.predictions.decoder.weight', 'cls.seq_relationship.weight', 'cls.predictions.transform.LayerNorm.bias', 'cls.predictions.transform.dense.bias', 'cls.seq_relationship.bias', 'cls.predictions.transform.LayerNorm.weight', 'cls.predictions.bias']\n",
"- This IS expected if you are initializing BertForSequenceClassification from the checkpoint of a model trained on another task or with another architecture (e.g. initializing a BertForSequenceClassification model from a BertForPreTraining model).\n",
"- This IS NOT expected if you are initializing BertForSequenceClassification from the checkpoint of a model that you expect to be exactly identical (initializing a BertForSequenceClassification model from a BertForSequenceClassification model).\n",
"Some weights of BertForSequenceClassification were not initialized from the model checkpoint at bert-base-uncased and are newly initialized: ['classifier.bias', 'classifier.weight']\n",
"You should probably TRAIN this model on a down-stream task to be able to use it for predictions and inference.\n"
]
},
{
"data": {
"text/plain": [
"BertForSequenceClassification(\n",
" (bert): BertModel(\n",
" (embeddings): BertEmbeddings(\n",
" (word_embeddings): Embedding(30522, 768, padding_idx=0)\n",
" (position_embeddings): Embedding(512, 768)\n",
" (token_type_embeddings): Embedding(2, 768)\n",
" (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)\n",
" (dropout): Dropout(p=0.1, inplace=False)\n",
" )\n",
" (encoder): BertEncoder(\n",
" (layer): ModuleList(\n",
" (0): BertLayer(\n",
" (attention): BertAttention(\n",
" (self): BertSelfAttention(\n",
" (query): Linear(in_features=768, out_features=768, bias=True)\n",
" (key): Linear(in_features=768, out_features=768, bias=True)\n",
" (value): Linear(in_features=768, out_features=768, bias=True)\n",
" (dropout): Dropout(p=0.1, inplace=False)\n",
" )\n",
" (output): BertSelfOutput(\n",
" (dense): Linear(in_features=768, out_features=768, bias=True)\n",
" (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)\n",
" (dropout): Dropout(p=0.1, inplace=False)\n",
" )\n",
" )\n",
" (intermediate): BertIntermediate(\n",
" (dense): Linear(in_features=768, out_features=3072, bias=True)\n",
" (intermediate_act_fn): GELUActivation()\n",
" )\n",
" (output): BertOutput(\n",
" (dense): Linear(in_features=3072, out_features=768, bias=True)\n",
" (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)\n",
" (dropout): Dropout(p=0.1, inplace=False)\n",
" )\n",
" )\n",
" (1): BertLayer(\n",
" (attention): BertAttention(\n",
" (self): BertSelfAttention(\n",
" (query): Linear(in_features=768, out_features=768, bias=True)\n",
" (key): Linear(in_features=768, out_features=768, bias=True)\n",
" (value): Linear(in_features=768, out_features=768, bias=True)\n",
" (dropout): Dropout(p=0.1, inplace=False)\n",
" )\n",
" (output): BertSelfOutput(\n",
" (dense): Linear(in_features=768, out_features=768, bias=True)\n",
" (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)\n",
" (dropout): Dropout(p=0.1, inplace=False)\n",
" )\n",
" )\n",
" (intermediate): BertIntermediate(\n",
" (dense): Linear(in_features=768, out_features=3072, bias=True)\n",
" (intermediate_act_fn): GELUActivation()\n",
" )\n",
" (output): BertOutput(\n",
" (dense): Linear(in_features=3072, out_features=768, bias=True)\n",
" (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)\n",
" (dropout): Dropout(p=0.1, inplace=False)\n",
" )\n",
" )\n",
" (2): BertLayer(\n",
" (attention): BertAttention(\n",
" (self): BertSelfAttention(\n",
" (query): Linear(in_features=768, out_features=768, bias=True)\n",
" (key): Linear(in_features=768, out_features=768, bias=True)\n",
" (value): Linear(in_features=768, out_features=768, bias=True)\n",
" (dropout): Dropout(p=0.1, inplace=False)\n",
" )\n",
" (output): BertSelfOutput(\n",
" (dense): Linear(in_features=768, out_features=768, bias=True)\n",
" (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)\n",
" (dropout): Dropout(p=0.1, inplace=False)\n",
" )\n",
" )\n",
" (intermediate): BertIntermediate(\n",
" (dense): Linear(in_features=768, out_features=3072, bias=True)\n",
" (intermediate_act_fn): GELUActivation()\n",
" )\n",
" (output): BertOutput(\n",
" (dense): Linear(in_features=3072, out_features=768, bias=True)\n",
" (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)\n",
" (dropout): Dropout(p=0.1, inplace=False)\n",
" )\n",
" )\n",
" (3): BertLayer(\n",
" (attention): BertAttention(\n",
" (self): BertSelfAttention(\n",
" (query): Linear(in_features=768, out_features=768, bias=True)\n",
" (key): Linear(in_features=768, out_features=768, bias=True)\n",
" (value): Linear(in_features=768, out_features=768, bias=True)\n",
" (dropout): Dropout(p=0.1, inplace=False)\n",
" )\n",
" (output): BertSelfOutput(\n",
" (dense): Linear(in_features=768, out_features=768, bias=True)\n",
" (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)\n",
" (dropout): Dropout(p=0.1, inplace=False)\n",
" )\n",
" )\n",
" (intermediate): BertIntermediate(\n",
" (dense): Linear(in_features=768, out_features=3072, bias=True)\n",
" (intermediate_act_fn): GELUActivation()\n",
" )\n",
" (output): BertOutput(\n",
" (dense): Linear(in_features=3072, out_features=768, bias=True)\n",
" (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)\n",
" (dropout): Dropout(p=0.1, inplace=False)\n",
" )\n",
" )\n",
" (4): BertLayer(\n",
" (attention): BertAttention(\n",
" (self): BertSelfAttention(\n",
" (query): Linear(in_features=768, out_features=768, bias=True)\n",
" (key): Linear(in_features=768, out_features=768, bias=True)\n",
" (value): Linear(in_features=768, out_features=768, bias=True)\n",
" (dropout): Dropout(p=0.1, inplace=False)\n",
" )\n",
" (output): BertSelfOutput(\n",
" (dense): Linear(in_features=768, out_features=768, bias=True)\n",
" (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)\n",
" (dropout): Dropout(p=0.1, inplace=False)\n",
" )\n",
" )\n",
" (intermediate): BertIntermediate(\n",
" (dense): Linear(in_features=768, out_features=3072, bias=True)\n",
" (intermediate_act_fn): GELUActivation()\n",
" )\n",
" (output): BertOutput(\n",
" (dense): Linear(in_features=3072, out_features=768, bias=True)\n",
" (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)\n",
" (dropout): Dropout(p=0.1, inplace=False)\n",
" )\n",
" )\n",
" (5): BertLayer(\n",
" (attention): BertAttention(\n",
" (self): BertSelfAttention(\n",
" (query): Linear(in_features=768, out_features=768, bias=True)\n",
" (key): Linear(in_features=768, out_features=768, bias=True)\n",
" (value): Linear(in_features=768, out_features=768, bias=True)\n",
" (dropout): Dropout(p=0.1, inplace=False)\n",
" )\n",
" (output): BertSelfOutput(\n",
" (dense): Linear(in_features=768, out_features=768, bias=True)\n",
" (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)\n",
" (dropout): Dropout(p=0.1, inplace=False)\n",
" )\n",
" )\n",
" (intermediate): BertIntermediate(\n",
" (dense): Linear(in_features=768, out_features=3072, bias=True)\n",
" (intermediate_act_fn): GELUActivation()\n",
" )\n",
" (output): BertOutput(\n",
" (dense): Linear(in_features=3072, out_features=768, bias=True)\n",
" (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)\n",
" (dropout): Dropout(p=0.1, inplace=False)\n",
" )\n",
" )\n",
" (6): BertLayer(\n",
" (attention): BertAttention(\n",
" (self): BertSelfAttention(\n",
" (query): Linear(in_features=768, out_features=768, bias=True)\n",
" (key): Linear(in_features=768, out_features=768, bias=True)\n",
" (value): Linear(in_features=768, out_features=768, bias=True)\n",
" (dropout): Dropout(p=0.1, inplace=False)\n",
" )\n",
" (output): BertSelfOutput(\n",
" (dense): Linear(in_features=768, out_features=768, bias=True)\n",
" (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)\n",
" (dropout): Dropout(p=0.1, inplace=False)\n",
" )\n",
" )\n",
" (intermediate): BertIntermediate(\n",
" (dense): Linear(in_features=768, out_features=3072, bias=True)\n",
" (intermediate_act_fn): GELUActivation()\n",
" )\n",
" (output): BertOutput(\n",
" (dense): Linear(in_features=3072, out_features=768, bias=True)\n",
" (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)\n",
" (dropout): Dropout(p=0.1, inplace=False)\n",
" )\n",
" )\n",
" (7): BertLayer(\n",
" (attention): BertAttention(\n",
" (self): BertSelfAttention(\n",
" (query): Linear(in_features=768, out_features=768, bias=True)\n",
" (key): Linear(in_features=768, out_features=768, bias=True)\n",
" (value): Linear(in_features=768, out_features=768, bias=True)\n",
" (dropout): Dropout(p=0.1, inplace=False)\n",
" )\n",
" (output): BertSelfOutput(\n",
" (dense): Linear(in_features=768, out_features=768, bias=True)\n",
" (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)\n",
" (dropout): Dropout(p=0.1, inplace=False)\n",
" )\n",
" )\n",
" (intermediate): BertIntermediate(\n",
" (dense): Linear(in_features=768, out_features=3072, bias=True)\n",
" (intermediate_act_fn): GELUActivation()\n",
" )\n",
" (output): BertOutput(\n",
" (dense): Linear(in_features=3072, out_features=768, bias=True)\n",
" (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)\n",
" (dropout): Dropout(p=0.1, inplace=False)\n",
" )\n",
" )\n",
" (8): BertLayer(\n",
" (attention): BertAttention(\n",
" (self): BertSelfAttention(\n",
" (query): Linear(in_features=768, out_features=768, bias=True)\n",
" (key): Linear(in_features=768, out_features=768, bias=True)\n",
" (value): Linear(in_features=768, out_features=768, bias=True)\n",
" (dropout): Dropout(p=0.1, inplace=False)\n",
" )\n",
" (output): BertSelfOutput(\n",
" (dense): Linear(in_features=768, out_features=768, bias=True)\n",
" (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)\n",
" (dropout): Dropout(p=0.1, inplace=False)\n",
" )\n",
" )\n",
" (intermediate): BertIntermediate(\n",
" (dense): Linear(in_features=768, out_features=3072, bias=True)\n",
" (intermediate_act_fn): GELUActivation()\n",
" )\n",
" (output): BertOutput(\n",
" (dense): Linear(in_features=3072, out_features=768, bias=True)\n",
" (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)\n",
" (dropout): Dropout(p=0.1, inplace=False)\n",
" )\n",
" )\n",
" (9): BertLayer(\n",
" (attention): BertAttention(\n",
" (self): BertSelfAttention(\n",
" (query): Linear(in_features=768, out_features=768, bias=True)\n",
" (key): Linear(in_features=768, out_features=768, bias=True)\n",
" (value): Linear(in_features=768, out_features=768, bias=True)\n",
" (dropout): Dropout(p=0.1, inplace=False)\n",
" )\n",
" (output): BertSelfOutput(\n",
" (dense): Linear(in_features=768, out_features=768, bias=True)\n",
" (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)\n",
" (dropout): Dropout(p=0.1, inplace=False)\n",
" )\n",
" )\n",
" (intermediate): BertIntermediate(\n",
" (dense): Linear(in_features=768, out_features=3072, bias=True)\n",
" (intermediate_act_fn): GELUActivation()\n",
" )\n",
" (output): BertOutput(\n",
" (dense): Linear(in_features=3072, out_features=768, bias=True)\n",
" (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)\n",
" (dropout): Dropout(p=0.1, inplace=False)\n",
" )\n",
" )\n",
" (10): BertLayer(\n",
" (attention): BertAttention(\n",
" (self): BertSelfAttention(\n",
" (query): Linear(in_features=768, out_features=768, bias=True)\n",
" (key): Linear(in_features=768, out_features=768, bias=True)\n",
" (value): Linear(in_features=768, out_features=768, bias=True)\n",
" (dropout): Dropout(p=0.1, inplace=False)\n",
" )\n",
" (output): BertSelfOutput(\n",
" (dense): Linear(in_features=768, out_features=768, bias=True)\n",
" (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)\n",
" (dropout): Dropout(p=0.1, inplace=False)\n",
" )\n",
" )\n",
" (intermediate): BertIntermediate(\n",
" (dense): Linear(in_features=768, out_features=3072, bias=True)\n",
" (intermediate_act_fn): GELUActivation()\n",
" )\n",
" (output): BertOutput(\n",
" (dense): Linear(in_features=3072, out_features=768, bias=True)\n",
" (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)\n",
" (dropout): Dropout(p=0.1, inplace=False)\n",
" )\n",
" )\n",
" (11): BertLayer(\n",
" (attention): BertAttention(\n",
" (self): BertSelfAttention(\n",
" (query): Linear(in_features=768, out_features=768, bias=True)\n",
" (key): Linear(in_features=768, out_features=768, bias=True)\n",
" (value): Linear(in_features=768, out_features=768, bias=True)\n",
" (dropout): Dropout(p=0.1, inplace=False)\n",
" )\n",
" (output): BertSelfOutput(\n",
" (dense): Linear(in_features=768, out_features=768, bias=True)\n",
" (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)\n",
" (dropout): Dropout(p=0.1, inplace=False)\n",
" )\n",
" )\n",
" (intermediate): BertIntermediate(\n",
" (dense): Linear(in_features=768, out_features=3072, bias=True)\n",
" (intermediate_act_fn): GELUActivation()\n",
" )\n",
" (output): BertOutput(\n",
" (dense): Linear(in_features=3072, out_features=768, bias=True)\n",
" (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)\n",
" (dropout): Dropout(p=0.1, inplace=False)\n",
" )\n",
" )\n",
" )\n",
" )\n",
" (pooler): BertPooler(\n",
" (dense): Linear(in_features=768, out_features=768, bias=True)\n",
" (activation): Tanh()\n",
" )\n",
" )\n",
" (dropout): Dropout(p=0.1, inplace=False)\n",
" (classifier): Linear(in_features=768, out_features=2, bias=True)\n",
")"
]
},
"execution_count": 95,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"from transformers import BertForSequenceClassification, AdamW, BertConfig\n",
"\n",
"# Load BertForSequenceClassification, the pretrained BERT model with a single\n",
"# linear classification layer on top.\n",
"model = BertForSequenceClassification.from_pretrained(\n",
" \"bert-base-uncased\", # Use the 12-layer BERT model, with an uncased vocab.\n",
" num_labels = 2, # The number of output labels--2 for binary classification.\n",
" # You can increase this for multi-class tasks.\n",
" output_attentions = False, # Whether the model returns attentions weights.\n",
" output_hidden_states = False, # Whether the model returns all hidden-states.\n",
")\n",
"\n",
"# Tell pytorch to run this model on the GPU.\n",
"model.cuda()"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "e0Jv6c7-HHDW"
},
"source": [
"Just for curiosity's sake, we can browse all of the model's parameters by name here.\n",
"\n",
"In the below cell, I've printed out the names and dimensions of the weights for:\n",
"\n",
"1. The embedding layer.\n",
"2. The first of the twelve transformers.\n",
"3. The output layer.\n",
"\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
},
"id": "8PIiVlDYCtSq",
"outputId": "ffcefa35-ebe5-48a6-b891-f763cd55b41b"
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"The BERT model has 201 different named parameters.\n",
"\n",
"==== Embedding Layer ====\n",
"\n",
"bert.embeddings.word_embeddings.weight (30522, 768)\n",
"bert.embeddings.position_embeddings.weight (512, 768)\n",
"bert.embeddings.token_type_embeddings.weight (2, 768)\n",
"bert.embeddings.LayerNorm.weight (768,)\n",
"bert.embeddings.LayerNorm.bias (768,)\n",
"\n",
"==== First Transformer ====\n",
"\n",
"bert.encoder.layer.0.attention.self.query.weight (768, 768)\n",
"bert.encoder.layer.0.attention.self.query.bias (768,)\n",
"bert.encoder.layer.0.attention.self.key.weight (768, 768)\n",
"bert.encoder.layer.0.attention.self.key.bias (768,)\n",
"bert.encoder.layer.0.attention.self.value.weight (768, 768)\n",
"bert.encoder.layer.0.attention.self.value.bias (768,)\n",
"bert.encoder.layer.0.attention.output.dense.weight (768, 768)\n",
"bert.encoder.layer.0.attention.output.dense.bias (768,)\n",
"bert.encoder.layer.0.attention.output.LayerNorm.weight (768,)\n",
"bert.encoder.layer.0.attention.output.LayerNorm.bias (768,)\n",
"bert.encoder.layer.0.intermediate.dense.weight (3072, 768)\n",
"bert.encoder.layer.0.intermediate.dense.bias (3072,)\n",
"bert.encoder.layer.0.output.dense.weight (768, 3072)\n",
"bert.encoder.layer.0.output.dense.bias (768,)\n",
"bert.encoder.layer.0.output.LayerNorm.weight (768,)\n",
"bert.encoder.layer.0.output.LayerNorm.bias (768,)\n",
"\n",
"==== Output Layer ====\n",
"\n",
"bert.pooler.dense.weight (768, 768)\n",
"bert.pooler.dense.bias (768,)\n",
"classifier.weight (2, 768)\n",
"classifier.bias (2,)\n"
]
}
],
"source": [
"# Get all of the model's parameters as a list of tuples.\n",
"params = list(model.named_parameters())\n",
"\n",
"print('The BERT model has {:} different named parameters.\\n'.format(len(params)))\n",
"\n",
"print('==== Embedding Layer ====\\n')\n",
"\n",
"for p in params[0:5]:\n",
" print(\"{:<55} {:>12}\".format(p[0], str(tuple(p[1].size()))))\n",
"\n",
"print('\\n==== First Transformer ====\\n')\n",
"\n",
"for p in params[5:21]:\n",
" print(\"{:<55} {:>12}\".format(p[0], str(tuple(p[1].size()))))\n",
"\n",
"print('\\n==== Output Layer ====\\n')\n",
"\n",
"for p in params[-4:]:\n",
" print(\"{:<55} {:>12}\".format(p[0], str(tuple(p[1].size()))))"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "qRWT-D4U_Pvx"
},
"source": [
"## 4.2. Optimizer & Learning Rate Scheduler"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "8o-VEBobKwHk"
},
"source": [
"Now that we have our model loaded we need to grab the training hyperparameters from within the stored model.\n",
"\n",
"For the purposes of fine-tuning, the authors recommend choosing from the following values (from Appendix A.3 of the [BERT paper](https://arxiv.org/pdf/1810.04805.pdf)):\n",
"\n",
">- **Batch size:** 16, 32 \n",
"- **Learning rate (Adam):** 5e-5, 3e-5, 2e-5 \n",
"- **Number of epochs:** 2, 3, 4\n",
"\n",
"We chose:\n",
"* Batch size: 32 (set when creating our DataLoaders)\n",
"* Learning rate: 2e-5\n",
"* Epochs: 4 (we'll see that this is probably too many...)\n",
"\n",
"The epsilon parameter `eps = 1e-8` is \"a very small number to prevent any division by zero in the implementation\" (from [here](https://machinelearningmastery.com/adam-optimization-algorithm-for-deep-learning/)).\n",
"\n",
"You can find the creation of the AdamW optimizer in `run_glue.py` [here](https://github.com/huggingface/transformers/blob/5bfcd0485ece086ebcbed2d008813037968a9e58/examples/run_glue.py#L109)."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
},
"id": "GLs72DuMODJO",
"outputId": "6a6a2ba5-9620-48a8-f78e-c8183427b096"
},
"outputs": [
{
"name": "stderr",
"output_type": "stream",
"text": [
"/usr/local/lib/python3.8/dist-packages/transformers/optimization.py:306: FutureWarning: This implementation of AdamW is deprecated and will be removed in a future version. Use the PyTorch implementation torch.optim.AdamW instead, or set `no_deprecation_warning=True` to disable this warning\n",
" warnings.warn(\n"
]
}
],
"source": [
"# Note: AdamW is a class from the huggingface library (as opposed to pytorch)\n",
"# I believe the 'W' stands for 'Weight Decay fix\"\n",
"optimizer = AdamW(model.parameters(),\n",
" lr = 2e-5, # args.learning_rate - default is 5e-5, our notebook had 2e-5\n",
" eps = 1e-8 # args.adam_epsilon - default is 1e-8.\n",
" )\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "-p0upAhhRiIx"
},
"outputs": [],
"source": [
"from transformers import get_linear_schedule_with_warmup\n",
"\n",
"# Number of training epochs. The BERT authors recommend between 2 and 4.\n",
"# We chose to run for 4, but we'll see later that this may be over-fitting the\n",
"# training data.\n",
"epochs = 4\n",
"\n",
"# Total number of training steps is [number of batches] x [number of epochs].\n",
"# (Note that this is not the same as the number of training samples).\n",
"total_steps = len(train_dataloader) * epochs\n",
"\n",
"# Create the learning rate scheduler.\n",
"scheduler = get_linear_schedule_with_warmup(optimizer,\n",
" num_warmup_steps = 0, # Default value in run_glue.py\n",
" num_training_steps = total_steps)"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "RqfmWwUR_Sox"
},
"source": [
"## 4.3. Training Loop"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "_QXZhFb4LnV5"
},
"source": [
"Below is our training loop. There's a lot going on, but fundamentally for each pass in our loop we have a trianing phase and a validation phase.\n",
"\n",
"> *Thank you to [Stas Bekman](https://ca.linkedin.com/in/stasbekman) for contributing the insights and code for using validation loss to detect over-fitting!*\n",
"\n",
"**Training:**\n",
"- Unpack our data inputs and labels\n",
"- Load data onto the GPU for acceleration\n",
"- Clear out the gradients calculated in the previous pass.\n",
" - In pytorch the gradients accumulate by default (useful for things like RNNs) unless you explicitly clear them out.\n",
"- Forward pass (feed input data through the network)\n",
"- Backward pass (backpropagation)\n",
"- Tell the network to update parameters with optimizer.step()\n",
"- Track variables for monitoring progress\n",
"\n",
"**Evalution:**\n",
"- Unpack our data inputs and labels\n",
"- Load data onto the GPU for acceleration\n",
"- Forward pass (feed input data through the network)\n",
"- Compute loss on our validation data and track variables for monitoring progress\n",
"\n",
"Pytorch hides all of the detailed calculations from us, but we've commented the code to point out which of the above steps are happening on each line.\n",
"\n",
"> *PyTorch also has some [beginner tutorials](https://pytorch.org/tutorials/beginner/blitz/cifar10_tutorial.html#sphx-glr-beginner-blitz-cifar10-tutorial-py) which you may also find helpful.*"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "pE5B99H5H2-W"
},
"source": [
"Define a helper function for calculating accuracy."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "9cQNvaZ9bnyy"
},
"outputs": [],
"source": [
"import numpy as np\n",
"\n",
"# Function to calculate the accuracy of our predictions vs labels\n",
"def flat_accuracy(preds, labels):\n",
" pred_flat = np.argmax(preds, axis=1).flatten()\n",
" labels_flat = labels.flatten()\n",
" return np.sum(pred_flat == labels_flat) / len(labels_flat)"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "KNhRtWPXH9C3"
},
"source": [
"Helper function for formatting elapsed times as `hh:mm:ss`\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "gpt6tR83keZD"
},
"outputs": [],
"source": [
"import time\n",
"import datetime\n",
"\n",
"def format_time(elapsed):\n",
" '''\n",
" Takes a time in seconds and returns a string hh:mm:ss\n",
" '''\n",
" # Round to the nearest second.\n",
" elapsed_rounded = int(round((elapsed)))\n",
"\n",
" # Format as hh:mm:ss\n",
" return str(datetime.timedelta(seconds=elapsed_rounded))\n"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "cfNIhN19te3N"
},
"source": [
"We're ready to kick off the training!"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"colab": {
"background_save": true,
"base_uri": "https://localhost:8080/"
},
"id": "6J-FYdx6nFE_",
"outputId": "d8066df8-e3f6-4b9a-e1e1-f920c592561b"
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n",
"======== Epoch 1 / 4 ========\n",
"Training...\n",
" Batch 40 of 717. Elapsed: 0:00:56.\n",
" Batch 80 of 717. Elapsed: 0:01:52.\n",
" Batch 120 of 717. Elapsed: 0:02:48.\n",
" Batch 160 of 717. Elapsed: 0:03:43.\n",
" Batch 200 of 717. Elapsed: 0:04:39.\n",
" Batch 240 of 717. Elapsed: 0:05:34.\n",
" Batch 280 of 717. Elapsed: 0:06:30.\n",
" Batch 320 of 717. Elapsed: 0:07:25.\n",
" Batch 360 of 717. Elapsed: 0:08:21.\n",
" Batch 400 of 717. Elapsed: 0:09:16.\n",
" Batch 440 of 717. Elapsed: 0:10:12.\n",
" Batch 480 of 717. Elapsed: 0:11:08.\n",
" Batch 520 of 717. Elapsed: 0:12:03.\n",
" Batch 560 of 717. Elapsed: 0:12:59.\n",
" Batch 600 of 717. Elapsed: 0:13:54.\n",
" Batch 640 of 717. Elapsed: 0:14:50.\n",
" Batch 680 of 717. Elapsed: 0:15:46.\n",
"\n",
" Average training loss: 0.28\n",
" Training epcoh took: 0:16:37\n",
"\n",
"Running Validation...\n",
" Accuracy: 0.91\n",
" Validation Loss: 0.22\n",
" Validation took: 0:00:41\n",
"\n",
"======== Epoch 2 / 4 ========\n",
"Training...\n",
" Batch 40 of 717. Elapsed: 0:00:56.\n",
" Batch 80 of 717. Elapsed: 0:01:51.\n",
" Batch 120 of 717. Elapsed: 0:02:47.\n",
" Batch 160 of 717. Elapsed: 0:03:42.\n",
" Batch 200 of 717. Elapsed: 0:04:38.\n",
" Batch 240 of 717. Elapsed: 0:05:34.\n",
" Batch 280 of 717. Elapsed: 0:06:29.\n",
" Batch 320 of 717. Elapsed: 0:07:25.\n",
" Batch 360 of 717. Elapsed: 0:08:20.\n",
" Batch 400 of 717. Elapsed: 0:09:16.\n",
" Batch 440 of 717. Elapsed: 0:10:12.\n",
" Batch 480 of 717. Elapsed: 0:11:07.\n",
" Batch 520 of 717. Elapsed: 0:12:03.\n",
" Batch 560 of 717. Elapsed: 0:12:58.\n",
" Batch 600 of 717. Elapsed: 0:13:54.\n",
" Batch 640 of 717. Elapsed: 0:14:50.\n",
" Batch 680 of 717. Elapsed: 0:15:45.\n",
"\n",
" Average training loss: 0.19\n",
" Training epcoh took: 0:16:36\n",
"\n",
"Running Validation...\n",
" Accuracy: 0.90\n",
" Validation Loss: 0.25\n",
" Validation took: 0:00:41\n",
"\n",
"======== Epoch 3 / 4 ========\n",
"Training...\n",
" Batch 40 of 717. Elapsed: 0:00:56.\n",
" Batch 80 of 717. Elapsed: 0:01:51.\n",
" Batch 120 of 717. Elapsed: 0:02:47.\n",
" Batch 160 of 717. Elapsed: 0:03:43.\n",
" Batch 200 of 717. Elapsed: 0:04:38.\n",
" Batch 240 of 717. Elapsed: 0:05:34.\n",
" Batch 280 of 717. Elapsed: 0:06:30.\n",
" Batch 320 of 717. Elapsed: 0:07:25.\n",
" Batch 360 of 717. Elapsed: 0:08:21.\n",
" Batch 400 of 717. Elapsed: 0:09:17.\n",
" Batch 440 of 717. Elapsed: 0:10:12.\n",
" Batch 480 of 717. Elapsed: 0:11:08.\n",
" Batch 520 of 717. Elapsed: 0:12:03.\n",
" Batch 560 of 717. Elapsed: 0:12:59.\n",
" Batch 600 of 717. Elapsed: 0:13:55.\n",
" Batch 640 of 717. Elapsed: 0:14:50.\n",
" Batch 680 of 717. Elapsed: 0:15:46.\n",
"\n",
" Average training loss: 0.12\n",
" Training epcoh took: 0:16:37\n",
"\n",
"Running Validation...\n",
" Accuracy: 0.91\n",
" Validation Loss: 0.35\n",
" Validation took: 0:00:41\n",
"\n",
"======== Epoch 4 / 4 ========\n",
"Training...\n",
" Batch 40 of 717. Elapsed: 0:00:56.\n",
" Batch 80 of 717. Elapsed: 0:01:51.\n",
" Batch 120 of 717. Elapsed: 0:02:47.\n",
" Batch 160 of 717. Elapsed: 0:03:42.\n",
" Batch 200 of 717. Elapsed: 0:04:38.\n",
" Batch 240 of 717. Elapsed: 0:05:34.\n",
" Batch 280 of 717. Elapsed: 0:06:29.\n",
" Batch 320 of 717. Elapsed: 0:07:25.\n",
" Batch 360 of 717. Elapsed: 0:08:21.\n",
" Batch 400 of 717. Elapsed: 0:09:16.\n",
" Batch 440 of 717. Elapsed: 0:10:12.\n",
" Batch 480 of 717. Elapsed: 0:11:08.\n",
" Batch 520 of 717. Elapsed: 0:12:03.\n",
" Batch 560 of 717. Elapsed: 0:12:59.\n",
" Batch 600 of 717. Elapsed: 0:13:55.\n",
" Batch 640 of 717. Elapsed: 0:14:50.\n",
" Batch 680 of 717. Elapsed: 0:15:46.\n",
"\n",
" Average training loss: 0.07\n",
" Training epcoh took: 0:16:37\n",
"\n",
"Running Validation...\n",
" Accuracy: 0.91\n",
" Validation Loss: 0.42\n",
" Validation took: 0:00:41\n",
"\n",
"Training complete!\n",
"Total training took 1:09:11 (h:mm:ss)\n"
]
}
],
"source": [
"import random\n",
"import numpy as np\n",
"\n",
"# This training code is based on the `run_glue.py` script here:\n",
"# https://github.com/huggingface/transformers/blob/5bfcd0485ece086ebcbed2d008813037968a9e58/examples/run_glue.py#L128\n",
"\n",
"# Set the seed value all over the place to make this reproducible.\n",
"seed_val = 42\n",
"\n",
"random.seed(seed_val)\n",
"np.random.seed(seed_val)\n",
"torch.manual_seed(seed_val)\n",
"torch.cuda.manual_seed_all(seed_val)\n",
"\n",
"# We'll store a number of quantities such as training and validation loss,\n",
"# validation accuracy, and timings.\n",
"training_stats = []\n",
"\n",
"# Measure the total training time for the whole run.\n",
"total_t0 = time.time()\n",
"\n",
"# For each epoch...\n",
"for epoch_i in range(0, epochs):\n",
"\n",
" # ========================================\n",
" # Training\n",
" # ========================================\n",
"\n",
" # Perform one full pass over the training set.\n",
"\n",
" print(\"\")\n",
" print('======== Epoch {:} / {:} ========'.format(epoch_i + 1, epochs))\n",
" print('Training...')\n",
"\n",
" # Measure how long the training epoch takes.\n",
" t0 = time.time()\n",
"\n",
" # Reset the total loss for this epoch.\n",
" total_train_loss = 0\n",
"\n",
" # Put the model into training mode. Don't be mislead--the call to\n",
" # `train` just changes the *mode*, it doesn't *perform* the training.\n",
" # `dropout` and `batchnorm` layers behave differently during training\n",
" # vs. test (source: https://stackoverflow.com/questions/51433378/what-does-model-train-do-in-pytorch)\n",
" model.train()\n",
"\n",
" # For each batch of training data...\n",
" for step, batch in enumerate(train_dataloader):\n",
"\n",
" # Progress update every 40 batches.\n",
" if step % 40 == 0 and not step == 0:\n",
" # Calculate elapsed time in minutes.\n",
" elapsed = format_time(time.time() - t0)\n",
"\n",
" # Report progress.\n",
" print(' Batch {:>5,} of {:>5,}. Elapsed: {:}.'.format(step, len(train_dataloader), elapsed))\n",
"\n",
" # Unpack this training batch from our dataloader.\n",
" #\n",
" # As we unpack the batch, we'll also copy each tensor to the GPU using the\n",
" # `to` method.\n",
" #\n",
" # `batch` contains three pytorch tensors:\n",
" # [0]: input ids\n",
" # [1]: attention masks\n",
" # [2]: labels\n",
" b_input_ids = batch[0].to(device)\n",
" b_input_mask = batch[1].to(device)\n",
" b_labels = batch[2].to(device)\n",
"\n",
" # Always clear any previously calculated gradients before performing a\n",
" # backward pass. PyTorch doesn't do this automatically because\n",
" # accumulating the gradients is \"convenient while training RNNs\".\n",
" # (source: https://stackoverflow.com/questions/48001598/why-do-we-need-to-call-zero-grad-in-pytorch)\n",
" model.zero_grad()\n",
"\n",
" # Perform a forward pass (evaluate the model on this training batch).\n",
" # In PyTorch, calling `model` will in turn call the model's `forward`\n",
" # function and pass down the arguments. The `forward` function is\n",
" # documented here:\n",
" # https://huggingface.co/transformers/model_doc/bert.html#bertforsequenceclassification\n",
" # The results are returned in a results object, documented here:\n",
" # https://huggingface.co/transformers/main_classes/output.html#transformers.modeling_outputs.SequenceClassifierOutput\n",
" # Specifically, we'll get the loss (because we provided labels) and the\n",
" # \"logits\"--the model outputs prior to activation.\n",
" result = model(b_input_ids,\n",
" token_type_ids=None,\n",
" attention_mask=b_input_mask,\n",
" labels=b_labels,\n",
" return_dict=True)\n",
"\n",
" loss = result.loss\n",
" logits = result.logits\n",
"\n",
" # Accumulate the training loss over all of the batches so that we can\n",
" # calculate the average loss at the end. `loss` is a Tensor containing a\n",
" # single value; the `.item()` function just returns the Python value\n",
" # from the tensor.\n",
" total_train_loss += loss.item()\n",
"\n",
" # Perform a backward pass to calculate the gradients.\n",
" loss.backward()\n",
"\n",
" # Clip the norm of the gradients to 1.0.\n",
" # This is to help prevent the \"exploding gradients\" problem.\n",
" torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)\n",
"\n",
" # Update parameters and take a step using the computed gradient.\n",
" # The optimizer dictates the \"update rule\"--how the parameters are\n",
" # modified based on their gradients, the learning rate, etc.\n",
" optimizer.step()\n",
"\n",
" # Update the learning rate.\n",
" scheduler.step()\n",
"\n",
" # Calculate the average loss over all of the batches.\n",
" avg_train_loss = total_train_loss / len(train_dataloader)\n",
"\n",
" # Measure how long this epoch took.\n",
" training_time = format_time(time.time() - t0)\n",
"\n",
" print(\"\")\n",
" print(\" Average training loss: {0:.2f}\".format(avg_train_loss))\n",
" print(\" Training epcoh took: {:}\".format(training_time))\n",
"\n",
" # ========================================\n",
" # Validation\n",
" # ========================================\n",
" # After the completion of each training epoch, measure our performance on\n",
" # our validation set.\n",
"\n",
" print(\"\")\n",
" print(\"Running Validation...\")\n",
"\n",
" t0 = time.time()\n",
"\n",
" # Put the model in evaluation mode--the dropout layers behave differently\n",
" # during evaluation.\n",
" model.eval()\n",
"\n",
" # Tracking variables\n",
" total_eval_accuracy = 0\n",
" total_eval_loss = 0\n",
" nb_eval_steps = 0\n",
"\n",
" # Evaluate data for one epoch\n",
" for batch in validation_dataloader:\n",
"\n",
" # Unpack this training batch from our dataloader.\n",
" #\n",
" # As we unpack the batch, we'll also copy each tensor to the GPU using\n",
" # the `to` method.\n",
" #\n",
" # `batch` contains three pytorch tensors:\n",
" # [0]: input ids\n",
" # [1]: attention masks\n",
" # [2]: labels\n",
" b_input_ids = batch[0].to(device)\n",
" b_input_mask = batch[1].to(device)\n",
" b_labels = batch[2].to(device)\n",
"\n",
" # Tell pytorch not to bother with constructing the compute graph during\n",
" # the forward pass, since this is only needed for backprop (training).\n",
" with torch.no_grad():\n",
"\n",
" # Forward pass, calculate logit predictions.\n",
" # token_type_ids is the same as the \"segment ids\", which\n",
" # differentiates sentence 1 and 2 in 2-sentence tasks.\n",
" result = model(b_input_ids,\n",
" token_type_ids=None,\n",
" attention_mask=b_input_mask,\n",
" labels=b_labels,\n",
" return_dict=True)\n",
"\n",
" # Get the loss and \"logits\" output by the model. The \"logits\" are the\n",
" # output values prior to applying an activation function like the\n",
" # softmax.\n",
" loss = result.loss\n",
" logits = result.logits\n",
"\n",
" # Accumulate the validation loss.\n",
" total_eval_loss += loss.item()\n",
"\n",
" # Move logits and labels to CPU\n",
" logits = logits.detach().cpu().numpy()\n",
" label_ids = b_labels.to('cpu').numpy()\n",
"\n",
" # Calculate the accuracy for this batch of test sentences, and\n",
" # accumulate it over all batches.\n",
" total_eval_accuracy += flat_accuracy(logits, label_ids)\n",
"\n",
"\n",
" # Report the final accuracy for this validation run.\n",
" avg_val_accuracy = total_eval_accuracy / len(validation_dataloader)\n",
" print(\" Accuracy: {0:.2f}\".format(avg_val_accuracy))\n",
"\n",
" # Calculate the average loss over all of the batches.\n",
" avg_val_loss = total_eval_loss / len(validation_dataloader)\n",
"\n",
" # Measure how long the validation run took.\n",
" validation_time = format_time(time.time() - t0)\n",
"\n",
" print(\" Validation Loss: {0:.2f}\".format(avg_val_loss))\n",
" print(\" Validation took: {:}\".format(validation_time))\n",
"\n",
" # Record all statistics from this epoch.\n",
" training_stats.append(\n",
" {\n",
" 'epoch': epoch_i + 1,\n",
" 'Training Loss': avg_train_loss,\n",
" 'Valid. Loss': avg_val_loss,\n",
" 'Valid. Accur.': avg_val_accuracy,\n",
" 'Training Time': training_time,\n",
" 'Validation Time': validation_time\n",
" }\n",
" )\n",
"\n",
"print(\"\")\n",
"print(\"Training complete!\")\n",
"\n",
"print(\"Total training took {:} (h:mm:ss)\".format(format_time(time.time()-total_t0)))"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "VQTvJ1vRP7u4"
},
"source": [
"Let's view the summary of the training process."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"colab": {
"background_save": true
},
"id": "6O_NbXFGMukX",
"outputId": "301bb33d-db5d-48e9-ecb2-d9fdc36f81ff"
},
"outputs": [
{
"data": {
"text/html": [
"\n",
"
\n",
"
\n",
"
\n",
"\n",
"
\n",
" \n",
"
\n",
"
\n",
"
Training Loss
\n",
"
Valid. Loss
\n",
"
Valid. Accur.
\n",
"
Training Time
\n",
"
Validation Time
\n",
"
\n",
"
\n",
"
epoch
\n",
"
\n",
"
\n",
"
\n",
"
\n",
"
\n",
"
\n",
" \n",
" \n",
"
\n",
"
1
\n",
"
0.28
\n",
"
0.22
\n",
"
0.91
\n",
"
0:16:37
\n",
"
0:00:41
\n",
"
\n",
"
\n",
"
2
\n",
"
0.19
\n",
"
0.25
\n",
"
0.90
\n",
"
0:16:36
\n",
"
0:00:41
\n",
"
\n",
"
\n",
"
3
\n",
"
0.12
\n",
"
0.35
\n",
"
0.91
\n",
"
0:16:37
\n",
"
0:00:41
\n",
"
\n",
"
\n",
"
4
\n",
"
0.07
\n",
"
0.42
\n",
"
0.91
\n",
"
0:16:37
\n",
"
0:00:41
\n",
"
\n",
" \n",
"
\n",
"
\n",
" \n",
" \n",
" \n",
"\n",
" \n",
"
\n",
"
\n",
" "
],
"text/plain": [
" Training Loss Valid. Loss Valid. Accur. Training Time Validation Time\n",
"epoch \n",
"1 0.28 0.22 0.91 0:16:37 0:00:41\n",
"2 0.19 0.25 0.90 0:16:36 0:00:41\n",
"3 0.12 0.35 0.91 0:16:37 0:00:41\n",
"4 0.07 0.42 0.91 0:16:37 0:00:41"
]
},
"execution_count": 102,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"import pandas as pd\n",
"\n",
"# Display floats with two decimal places.\n",
"pd.set_option('precision', 2)\n",
"\n",
"# Create a DataFrame from our training statistics.\n",
"df_stats = pd.DataFrame(data=training_stats)\n",
"\n",
"# Use the 'epoch' as the row index.\n",
"df_stats = df_stats.set_index('epoch')\n",
"\n",
"# A hack to force the column headers to wrap.\n",
"#df = df.style.set_table_styles([dict(selector=\"th\",props=[('max-width', '70px')])])\n",
"\n",
"# Display the table.\n",
"df_stats"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "1-G03mmwH3aI"
},
"source": [
"Notice that, while the the training loss is going down with each epoch, the validation loss is increasing! This suggests that we are training our model too long, and it's over-fitting on the training data.\n",
"\n",
"(For reference, we are using 7,695 training samples and 856 validation samples).\n",
"\n",
"Validation Loss is a more precise measure than accuracy, because with accuracy we don't care about the exact output value, but just which side of a threshold it falls on.\n",
"\n",
"If we are predicting the correct answer, but with less confidence, then validation loss will catch this, while accuracy will not."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"colab": {
"background_save": true
},
"id": "68xreA9JAmG5",
"outputId": "0162a2b9-c417-4a0c-f8a6-4e8618ee6791"
},
"outputs": [
{
"data": {
"image/png": "iVBORw0KGgoAAAANSUhEUgAAAvAAAAGaCAYAAABpIXfbAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4yLjIsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy+WH4yJAAAgAElEQVR4nOzdeUBU5f748TcDM+y7IMiiiA4gIqK5JWnuuKUZpmnZ4m23+tm3W3rbvd9u92uWLVb3Zru55JpYZiVqaJpetTSVRXEDQUCGYYeZYc7vD3NuE6ig4Az4ef0Vz3nOcz7nyInPPPMsDoqiKAghhBBCCCFaBZWtAxBCCCGEEEI0niTwQgghhBBCtCKSwAshhBBCCNGKSAIvhBBCCCFEKyIJvBBCCCGEEK2IJPBCCCGEEEK0IpLACyGue7m5uURFRfHOO+9ccRtz5swhKiqqGaNquy72vKOiopgzZ06j2njnnXeIiooiNze32eNbu3YtUVFR7N69u9nbFkKI5uBk6wCEEOLPmpIIp6amEhoa2oLRtD5VVVX861//YuPGjRQWFuLn50fv3r155JFHiIyMbFQbjz/+ON999x1fffUVMTExDdZRFIVhw4ZRVlbGjh07cHFxac7baFG7d+9mz5493H333Xh5edk6nHpyc3MZNmwY06dP54UXXrB1OEIIOyMJvBDC7syfP9/q53379vHll18yZcoUevfubXXMz8/vqq8XEhLCwYMHcXR0vOI2/v73v/Pyyy9fdSzN4bnnnuObb75h3Lhx9O3bl6KiIrZs2cKBAwcancAnJyfz3XffsWbNGp577rkG6/z888+cOXOGKVOmNEvyfvDgQVSqa/PF8J49e1i0aBG33nprvQR+woQJjB07FrVafU1iEUKIppIEXghhdyZMmGD1c11dHV9++SU9e/asd+zPKioq8PDwaNL1HBwccHZ2bnKcf2QvyV51dTWbNm0iMTGR119/3VI+a9YsDAZDo9tJTEwkODiYDRs28PTTT6PRaOrVWbt2LXA+2W8OV/tv0FwcHR2v6sOcEEK0NBkDL4RotYYOHcpdd93FkSNHmDlzJr179+aWW24BzifyCxcuZPLkyfTr14/u3bszYsQIFixYQHV1tVU7DY3J/mPZ1q1bue2224iLiyMxMZH/+7//w2QyWbXR0Bj4C2Xl5eW8+OKLDBgwgLi4OKZOncqBAwfq3U9JSQlz586lX79+JCQkMGPGDI4cOcJdd93F0KFDG/VMHBwccHBwaPADRUNJ+MWoVCpuvfVW9Ho9W7ZsqXe8oqKC77//Hq1WS48ePZr0vC+moTHwZrOZf//73wwdOpS4uDjGjRtHSkpKg+dnZ2fz0ksvMXbsWBISEoiPj2fSpEmsWrXKqt6cOXNYtGgRAMOGDSMqKsrq3/9iY+B1Oh0vv/wygwcPpnv37gwePJiXX36ZkpISq3oXzt+1axcfffQRw4cPp3v37owaNYp169Y16lk0RUZGBo8++ij9+vUjLi6OMWPGsHjxYurq6qzq5efnM3fuXIYMGUL37t0ZMGAAU6dOtYrJbDbz6aefMn78eBISEujVqxejRo3ib3/7G0ajsdljF0JcGemBF0K0anl5edx9990kJSUxcuRIqqqqACgoKGD16tWMHDmScePG4eTkxJ49e/jwww9JT0/no48+alT7P/74I8uWLWPq1KncdtttpKam8vHHH+Pt7c1DDz3UqDZmzpyJn58fjz76KHq9nk8++YQHHniA1NRUy7cFBoOBe++9l/T0dCZNmkRcXByZmZnce++9eHt7N/p5uLi4MHHiRNasWcPXX3/NuHHjGn3un02aNIn333+ftWvXkpSUZHXsm2++oaamhttuuw1ovuf9Z6+++iqff/45ffr04Z577qG4uJh58+YRFhZWr+6ePXvYu3cvN998M6GhoZZvI5577jl0Oh0PPvggAFOmTKGiooIffviBuXPn4uvrC1x67kV5eTl33HEHp06d4rbbbqNbt26kp6ezfPlyfv75Z1atWlXvm5+FCxdSU1PDlClT0Gg0LF++nDlz5hAeHl5vKNiV+u2337jrrrtwcnJi+vTptGvXjq1bt7JgwQIyMjIs38KYTCbuvfdeCgoKmDZtGp06daKiooLMzEz27t3LrbfeCsD777/P22+/zZAhQ5g6dSqOjo7k5uayZcsWDAaD3XzTJMR1TxFCCDu3Zs0aRavVKmvWrLEqHzJkiKLVapWVK1fWO6e2tlYxGAz1yhcuXKhotVrlwIEDlrKcnBxFq9Uqb7/9dr2y+Ph4JScnx1JuNpuVsWPHKgMHDrRq95lnnlG0Wm2DZS+++KJV+caNGxWtVqssX77cUvbFF18oWq1Wee+996zqXigfMmRIvXtpSHl5uXL//fcr3bt3V7p166Z88803jTrvYmbMmKHExMQoBQUFVuW33367EhsbqxQXFyuKcvXPW1EURavVKs8884zl5+zsbCUqKkqZMWOGYjKZLOWHDh1SoqKiFK1Wa/VvU1lZWe/6dXV1yp133qn06tXLKr6333673vkXXPh9+/nnny1lb7zxhqLVapUvvvjCqu6Ff5+FCxfWO3/ChAlKbW2tpfzs2bNKbGysMnv27HrX/LMLz+jll1++ZL0pU6YoMTExSnp6uqXMbDYrjz/+uKLVapWdO3cqiqIo6enpilarVT744INLtjdx4kRl9OjRl41PCGFbMoRGCNGq+fj4MGnSpHrlGo3G0ltoMpkoLS1Fp9Nx4403AjQ4hKUhw4YNs1rlxsHBgX79+lFUVERlZWWj2rjnnnusfu7fvz8Ap06dspRt3boVR0dHZsyYYVV38uTJeHp6Nuo6ZrOZJ554goyMDL799lsGDRrEU089xYYNG6zqPf/888TGxjZqTHxycjJ1dXV89dVXlrLs7Gx+/fVXhg4daplE3FzP+49SU1NRFIV7773Xakx6bGwsAwcOrFffzc3N8t+1tbWUlJSg1+sZOHAgFRUVHD9+vMkxXPDDDz/g5+fHlClTrMqnTJmCn58fmzdvrnfOtGnTrIYttW/fnoiICE6ePHnFcfxRcXExv/zyC0OHDiU6OtpS7uDgwMMPP2yJG7D8Du3evZvi4uKLtunh4UFBQQF79+5tlhiFEC1DhtAIIVq1sLCwi044XLp0KStWrODYsWOYzWarY6WlpY1u/898fHwA0Ov1uLu7N7mNC0M29Hq9pSw3N5fAwMB67Wk0GkJDQykrK7vsdVJTU9mxYwevvfYaoaGhvPXWW8yaNYunn34ak8lkGSaRmZlJXFxco8bEjxw5Ei8vL9auXcsDDzwAwJo1awAsw2cuaI7n/Uc5OTkAdO7cud6xyMhIduzYYVVWWVnJokWL+Pbbb8nPz693TmOe4cXk5ubSvXt3nJys/2w6OTnRqVMnjhw5Uu+ci/3unDlz5orj+HNMAF26dKl3rHPnzqhUKsszDAkJ4aGHHuKDDz4gMTGRmJgY+vfvT1JSEj169LCc9+STT/Loo48yffp0AgMD6du3LzfffDOjRo1q0hwKIUTLkgReCNGqubq6Nlj+ySef8M9//pPExERmzJhBYGAgarWagoIC5syZg6IojWr/UquRXG0bjT2/sS5MuuzTpw9wPvlftGgRDz/8MHPnzsVkMhEdHc2BAwd45ZVXGtWms7Mz48aNY9myZezfv5/4+HhSUlIICgripptustRrrud9Nf7nf/6Hbdu2cfvtt9OnTx98fHxwdHTkxx9/5NNPP633oaKlXaslMRtr9uzZJCcns23bNvbu3cvq1av56KOP+Mtf/sJf//pXABISEvjhhx/YsWMHu3fvZvfu3Xz99de8//77LFu2zPLhVQhhW5LACyHapPXr1xMSEsLixYutEqm0tDQbRnVxISEh7Nq1i8rKSqteeKPRSG5ubqM2G7pwn2fOnCE4OBg4n8S/9957PPTQQzz//POEhISg1WqZOHFio2NLTk5m2bJlrF27ltLSUoqKinjooYesnmtLPO8LPdjHjx8nPDzc6lh2drbVz2VlZWzbto0JEyYwb948q2M7d+6s17aDg0OTYzlx4gQmk8mqF95kMnHy5MkGe9tb2oWhXceOHat37Pjx45jN5npxhYWFcdddd3HXXXdRW1vLzJkz+fDDD7nvvvvw9/cHwN3dnVGjRjFq1Cjg/Dcr8+bNY/Xq1fzlL39p4bsSQjSGfXUPCCFEM1GpVDg4OFj1/JpMJhYvXmzDqC5u6NCh1NXV8fnnn1uVr1y5kvLy8ka1MXjwYOD86id/HN/u7OzMG2+8gZeXF7m5uYwaNareUJBLiY2NJSYmho0bN7J06VIcHBzqrf3eEs976NChODg48Mknn1gtiXj48OF6SfmFDw1/7ukvLCyst4wk/He8fGOH9gwfPhydTlevrZUrV6LT6Rg+fHij2mlO/v7+JCQksHXrVrKysizliqLwwQcfADBixAjg/Co6f14G0tnZ2TI86cJz0Ol09a4TGxtrVUcIYXvSAy+EaJOSkpJ4/fXXuf/++xkxYgQVFRV8/fXXTUpcr6XJkyezYsUK3nzzTU6fPm1ZRnLTpk107Nix3rrzDRk4cCDJycmsXr2asWPHMmHCBIKCgsjJyWH9+vXA+WTs3XffJTIyktGjRzc6vuTkZP7+97+zfft2+vbtW69ntyWed2RkJNOnT+eLL77g7rvvZuTIkRQXF7N06VKio6Otxp17eHgwcOBAUlJScHFxIS4ujjNnzvDll18SGhpqNd8AID4+HoAFCxYwfvx4nJ2d6dq1K1qttsFY/vKXv7Bp0ybmzZvHkSNHiImJIT09ndWrVxMREdFiPdOHDh3ivffeq1fu5OTEAw88wLPPPstdd93F9OnTmTZtGgEBAWzdupUdO3Ywbtw4BgwYAJwfXvX8888zcuRIIiIicHd359ChQ6xevZr4+HhLIj9mzBh69uxJjx49CAwMpKioiJUrV6JWqxk7dmyL3KMQouns8y+ZEEJcpZkzZ6IoCqtXr+aVV14hICCA0aNHc9tttzFmzBhbh1ePRqPhs88+Y/78+aSmpvLtt9/So0cPPv30U5599llqamoa1c4rr7xC3759WbFiBR999BFGo5GQkBCSkpK477770Gg0TJkyhb/+9a94enqSmJjYqHbHjx/P/Pnzqa2trTd5FVrueT/77LO0a9eOlStXMn/+fDp16sQLL7zAqVOn6k0cfe2113j99dfZsmUL69ato1OnTsyePRsnJyfmzp1rVbd379489dRTrFixgueffx6TycSsWbMumsB7enqyfPly3n77bbZs2cLatWvx9/dn6tSpPPbYY03e/bexDhw40OAKPhqNhgceeIC4uDhWrFjB22+/zfLly6mqqiIsLIynnnqK++67z1I/KiqKESNGsGfPHjZs2IDZbCY4OJgHH3zQqt59993Hjz/+yJIlSygvL8ff35/4+HgefPBBq5VuhBC25aBci5lFQgghrkhdXR39+/enR48eV7wZkhBCiLZFxsALIYSdaKiXfcWKFZSVlTW47rkQQojrkwyhEUIIO/Hcc89hMBhISEhAo9Hwyy+/8PXXX9OxY0duv/12W4cnhBDCTsgQGiGEsBNfffUVS5cu5eTJk1RVVeHv78/gwYN54oknaNeuna3DE0IIYSckgRdCCCGEEKIVkTHwQgghhBBCtCKSwAshhBBCCNGKyCTWJiopqcRsvvajjvz9PSgurrjm1xWitZF3RYjGkXdFiMaxxbuiUjng6+t+0eOSwDeR2azYJIG/cG0hxOXJuyJE48i7IkTj2Nu7IkNohBBCCCGEaEUkgRdCCCGEEKIVkQReCCGEEEKIVkQSeCGEEEIIIVoRSeCFEEIIIYRoRWQVmhZQXV1JRUUpdXXGZmuzsFCF2WxutvaEbTk6qvHw8MbV9eJLRAkhhBBCNEQS+GZmNBooLy/Bx6cdarUzDg4OzdKuk5MKk0kS+LZAURSMxlr0+nM4OalRqzW2DkkIIYQQrYgMoWlm5eV6PDy80Whcmi15F22Lg4MDGo0L7u7eVFTobR2OEEIIIVoZSeCbmclkwNnZ1dZhiFbAxcUVo9Fg6zCEEEII0crIEJpmZjbXoVI52joM0QqoVI6YzXW2DkMIIYQQDdhzdj8p2ZvQ1+rxcfbhlsgk+gb1snVYgCTwLUKGzojGkN8TIYQQwj7tObufZRlrMJrPL0hSUqtnWcYaALtI4mUIjRBCCCGEEL+rM9ex9ujXluT9AqPZSEr2JhtFZU164IVdmDXrAQAWLfrgmp4rhBBCCHGuWke6Lot0XRaZumPU1NU0WK+k1j4Wn5AEXlxSYuINjaq3alUKwcEdWjgaIYQQQoirV2Oq5ag++3zSXpxFYfU5AHydfejdvgcHig5TYaysd56vs8+1DrVBksCLS3r++XlWP69cuZyCgnwee+xJq3IfH9+rus7Che/a5FwhhBBCtH1mxcyZinzSi7M4osvkeOkp6pQ6NCo1XX0jGRR6IzF+Wtq7BeDg4EAXn85WY+AB1Co1t0Qm2fAu/ksSeHFJo0aNsfp527ZUSkv19cr/rKamBhcXl0ZfR61WX1F8V3uuEEIIIdqmMkM56cXnh8Vk6I5SbqwAIMQjmKFhNxHt15VInwjUqvrp8IWJqrIKjWizZs16gIqKCp5++m+8885CMjMzmD59BjNnPsj27dtISVlHVlYmZWWlBAQEMmbMeO66614cHR2t2oD/jmPfv38vjz/+EK+8Mp8TJ47z1VdrKCsrJS4unr/+9W+EhoY1y7kAa9asZMWKpRQXnyMyMpJZs2azePH7Vm0KIYQQwr4ZzSaO60+Srjvfy36mIh8AD7U7MX5aYvy0RPtp8Xb2bFR7fYN60TeoFwEBnhQVlbdk6E0mCXwrsOvwWdamHae4tAZ/L2cmDY5kQGyQrcOyoteX8PTTsxk5MomkpLG0b38+vo0bv8bV1Y0pU6bj5ubKvn17+fDDf1FZWcmjjz5x2XY/++wjVCpHpk2bQXl5GcuXL+Hll59j8eLPmuXcdetWs3DhfHr27MWUKXeQn5/P3LlP4enpSUBA4JU/ECGEEEK0KEVRKKwq4sjvk0+PlmRjMBtxdHCks3dHbumcRIy/llCPDqgc2tbCi5LA27ldh8/y2bcZGExmAIrLavns2wwAu0riz50rYs6c5xk3boJV+Usv/S/Ozv8dSjNxYjKvvfYP1q1bxf33P4xGo7lkuyaTiY8//gwnp/O/ql5e3rz11gKOHz9G585drupco9HIhx++T2xsHG+++Z6lXpcuXXnllZckgRdCCCHsTJWxmsySY6TrMknXHUVXUwJAoGs7+gf3oZu/lq4+nXFxavww3tZIEvhr4Kff8tlxMP+Kzs3OK8VUp1iVGUxmPtmYTtqveU1qK7FHMAPjgq8ojstxcXEhKWlsvfI/Ju9VVZUYDEbi4xNYv34tp06dpGtX7SXbHTv2FktiDRAf3xOAvLwzl03gL3duRsYRSktLeeSRW63qjRiRxNtvv3HJtoUQQgjR8syKmVNlORzRZZGhy+JkWQ5mxYyLozNRvl0Y2fFmYvyiaOfqZ+tQrylJ4O3cn5P3y5XbSkBAoFUSfMHx49ksXvw++/f/h8pK6+WYKisrLtvuhaE4F3h6egFQXn75sWiXO/fs2fMfqv48Jt7JyYng4Jb5oCOEEEKISyup0XPk9x72TN1RqkzVOOBAuGcoIzsOIcZPS4RXOI4qx8s31kZJAn8NDIy78p7vv773E8VltfXK/b2ceWa6fcyEBuue9gvKy8t57LEHcHPzYObMhwgJCUWj0ZCVlcH777+D2Wy+bLuqi7ycinL5DzBXc64QQgghrg1DnYGj+hPnh8UUZ3G2qhAAb40XPQJi6eanJcqvKx5qdxtHaj9smsAbDAbeeust1q9fT1lZGdHR0cyePZsBAwY0qZ3777+ftLQ0ZsyYwbPPPlvv+KpVq/j444/Jzc2lQ4cOzJgxg+nTpzfXbbSoSYMjrcbAA2icVEwaHGnDqBrnl1/2UVpayiuvvEbPnv/9sJGf37ShPy0lKOj8h6rc3Bzi4xMs5SaTifz8fCIjLz1ERwghhBBNpygKeZVnLZsoHSs9gclsQq1yootPZ27s0JcYPy3B7u1xcHCwdbh2yaYJ/Jw5c/j++++ZMWMGHTt2ZN26ddx///0sWbKEhISEyzcAbNu2jb179170+IoVK3jxxRdJSkri3nvvZe/evcybN4/a2lruu+++5rqVFnNhoqq9r0LTEJXq/IzvP/Z4G41G1q1bZauQrERHd8Pb25uUlHWMGjXGMgTohx82UV5eZuPohBBCiLaj3FBBpu6oZSx7qeH8cNZg9/YMChlAN78oIn0i0DjK3i6NYbME/uDBg3zzzTfMnTuXe+65B4CJEycybtw4FixYwNKlSy/bhsFg4NVXX2XmzJm888479Y7X1NSwcOFChg0bxltvvQXA7bffjtlsZtGiRUyePBlPz8atBWpLA2KDuCm+AybT5Yec2JO4uB54enrxyisvkZw8BQcHB777biP2MoJFrVZz330PsHDha/y///cIQ4YMIz8/n2+/3UBISKh86hdCCCGuUJ25juOlp873susyySnPQ0HB3cmNaL+uv6/J3hVfFx9bh9oq2SyB37RpE2q1msmTJ1vKnJ2dSU5OZuHChRQWFhIYeOll/D7//HNqamoumsDv3r0bvV7PtGnTrMqnT5/Ohg0bSEtLY+zY+iuniObh7e3D/PkLWbToTRYvfh9PTy9GjhzNDTf05cknZ9k6PABuu20KiqKwYsVS3n33LSIju/LPf77Bm28uQKNxtnV4QgghRKtRVFVMui6TI7osskqOUVtnQOWgIsIrnLERI4nx70q4Z2ibW5PdFmyWwKenpxMREYG7u/WEhB49eqAoCunp6ZdM4IuKinjvvfd44YUXcHV1bbDOkSNHAOjevbtVeWxsLCqViiNHjkgC30Svvvp6vbJL7VYaFxfPv//9Sb3yHTushz39uY1evW6oVwcgOLhDs54LkJw8leTkqZafzWYz+fl5aLVRDdyREEIIIQCqTTVklWT/PpY9k3M1OgD8XfzoE9SLGD8tUb6RuDo1nKeJK2ezBL6oqIj27dvXKw8ICACgsLDwkue/8cYbREREMGHChIvWKSoqQqPR4ONj/fXMhbLLXUO0fbW1tTg7W/e0b9r0DWVlpSQk9LZRVEIIIYT9MStmcsrPkK7L4khxFifKTmFWzGgcNUT5RjIk/Ca6+WkJcG0nw1BbmM0S+JqaGtTq+hMVLiRTtbX1l0684ODBg3z11VcsWbLkkr8gF7vGhetc6hoX4+/vccnjhYUqnJxa5quhlmr3erZ//0HeffcthgwZhre3N5mZGWzYsJ7IyC6MGDGyxZ+5SqUiIMD+52G0NvJMhWgceVfE5eiq9Rw8m86vZ4/w29l0yg3n93SJ8A3jlugRxAd1I8q/M06ObXtlcnt7V2z2tF1cXDAajfXKLyTVf+4VvUBRFF555RVGjhzJDTfccNlrGAyGBo811PPaGMXFFZjNF5+FaTabW2SyqZOTqtVNYm0N2rcPxt8/gJUrV1BWVoqXlzdJSWN56KFZODg4tvgzN5vNFBVdflMq0XgBAZ7yTIVoBHlXREOMdUaOlZ4gvTiLdF0WeZVnAfDUeBDjF0WMn5YYPy2emv92aJboqm0V7jVhi3dFpXK4ZKexzRL4gICABoewFBUVAVx0/PsPP/zAwYMHmT17Nrm5uVbHKioqyM3NpV27dri4uBAQEIDRaESv11sNozEYDOj1+stOkhVtX0hIKPPnL7R1GEIIIYRNKIrC2apC0ovP73x6VH8co9mIk4MjnX0imBg0hhg/LSEewTIsxo7YLIGPjo5myZIlVFZWWk1kPXDggOV4Q/Ly8jCbzdx99931jq1du5a1a9eyePFiBg0aRExMDACHDh0iMTHRUu/QoUOYzWbLcSGEEEKI60WlsYrMkmOkF59fMUZfWwpAe7cABv6+iVJX30icHTU2jlRcjM0S+KSkJD7++GNWrVplWQfeYDCwdu1aevXqZZngmpeXR3V1NZGR53ceHTp0KKGhofXae/TRRxkyZAjJycnExsYC0L9/f3x8fFi2bJlVAr98+XLc3NwYNGhQC9+lEEIIIYRt1ZnrOFmW8/ua7FmcKstBQcHVyYUo365089MS7afF39XX1qGKRrJZAh8fH09SUhILFiygqKiI8PBw1q1bR15eHq+++qql3jPPPMOePXvIzMwEIDw8nPDw8AbbDAsLY/jw4ZafXVxcePzxx5k3bx5PPPEEiYmJ7N27l5SUFJ566im8vLxa9iaFEEIIIWyguFpnSdgzS45RbarBAQc6eYUxutMwYvyj6OgZiqPK0dahiitg0ynD8+fP580332T9+vWUlpYSFRXFBx98QO/ezbd83/Tp01Gr1Xz88cekpqYSHBzMs88+y4wZM5rtGkIIIYQQtlRbZ+BoSTZHft/5tLDqHAC+zj4kBPQgxl9LlG8X3NVuNo5UNAcHRbGXje1bh8utQnP27CmCgjo2+3VlFZq2qaV+X65nsrKGEI0j70rrZlbMnKk4S7ouk/TiLLJLT1Kn1KFWqenq25luflHE+HWlvVugTD69SrIKjRBCCCGEuCJlhnIydEc5UpxFRkkW5YYKAEI8ghkSlkiMn5ZI706oHRveA0e0HZLACyGEEELYIZPZxPHSkxz5fU323Io8ADzU7kT7dbWsye7tLHP6rjeSwItrbuPGDfzjHy+zalUKwcEdAEhOHk9CQm+effalJp97tfbv38vjjz/E22//i169Lr05mBBCCNFSFEWhsPqcZROlLH02hjoDKgcVnb07Mr5zEt38tIR6dkDlILuzX88kgReX9fTTs9m//z9s2PADrq6uDdZ58slZHD78Gykp31/RDrfXwubN36HTFXP77dNsHYoQQggBQLWpmkzdMcuKMcU1JQAEuPrTP6g3MX5atL6RuDi52DhSYU8kgReXNWLEKHbu3M6OHT8yYkRSveMlJTr27fsPI0eOvuLkfdmyNahULdubkJr6PUePZtVL4Hv27EVq6k+o1TJmUAghRMsyK2ZOleWen3yqO8rJstOYFTMujs5ofbswPPxmuvlraefqb+tQhR2TBEAoYT0AACAASURBVF5c1k033YyrqxubN3/XYAK/Zctm6urqGDmy/rHG0mhst9ubSqWy228NhBBCtH4lNXrSdUdJ12WSqTtGpakKBxwI8wxhZPjNxPhHEeEVLmuyi0aTBF5clouLCzfdNJitWzdTVlZWbwOszZu/w9/fn7CwjixY8E/27dtDQUEBLi4u9Op1A48++sRlx6s3NAb++PFs3nzzNQ4d+g1vb28mTJhEu3YB9c7dvn0bKSnryMrKpKyslICAQMaMGc9dd92Lo+P5/xnOmvUAv/66H4DExPPj3IOCglm9esNFx8Cnpn7PF198yqlTJ3Fzc2fgwJt4+OHH8fHxsdSZNesBKioqeOGFebzxxnzS0w/j6enF5MlTmT797qY9aCGEEG2Coc7IUf1xMnRZHNFlcbayAABvjRdx7boR468l2rcrHhp3G0cqWitJ4FuBPWf3s+H4JnQ1enydfbglMom+Qb2uaQwjRiTx/fffsm1bKrfccqul/OzZfA4dOkhy8lTS0w9z6NBBhg8fRUBAIPn5eXz11Roee+xBvvhiFS4ujR+/V1x8jscffwiz2cydd96Ni4srKSnrGuwp37jxa1xd3ZgyZTpubq7s27eXDz/8F5WVlTz66BMA3H33fVRXV1NQkM9jjz0JgKvrxTezuDBZNjY2jocffpzCwgLWrPmS9PTDLF78uVUcZWWl/M//PM6QIcMYNmwkW7du5v3336Fz5y4MGDCw0fcshBCidVIUhbzKs+fHsRdncaz0BCazCSeVE128IxgQfAPd/KIIdm8va7KLZiEJvJ3bc3Y/yzLWYDQbASip1bMsYw3ANU3i+/Tph4+PL5s3f2eVwG/e/B2KojBixCgiI7swZMhwq/MGDhzEQw/dy7ZtqSQljW309ZYu/YzSUj0ffriEqKhoAEaPHscdd9xar+5LL/0vzs7//XAwcWIyr732D9atW8X99z+MRqOhT5/+rF27itJSPaNGjbnktU0mE++//w5dumh5551/W4b3REVF89JLz7JhwzqSk6da6hcWFvDii/9rGV40btwEkpPH8c036yWBF0KINqrCUElGyVHLijGlhjIAgtzbMyhkADF+Wrr4RKBxtN0QUdF2SQJ/DezO38eu/P9c0bknSk9jUkxWZUazkaXpq9mZt6dJbQ0I7kO/4N5XFIeTkxNDhw7nq6/WcO7cOdq1awfA5s3fExoaRrdu3a3qm0wmKisrCA0Nw8PDk6ysjCYl8Lt2/URcXLwleQfw9fVlxIjRrFu3yqruH5P3qqpKDAYj8fEJrF+/llOnTtK1q7ZJ95qRcYSSEp0l+b9g6NARvPvuW+zc+ZNVAu/h4cHw4aMsP6vVamJiYsnLO9Ok6wohhLBfdeY6TpSdJr04kyO6LHLKz6Cg4ObkarUmu6+Lz+UbE+IqSQJv5/6cvF+uvCWNGJHE2rWr2LLle26/fRonT57g2LEs7r33fgBqa2tYsuRTNm7cQFFRIYqiWM6tqKho0rUKCs4SFxdfrzw8vGO9suPHs1m8+H327/8PlZWVVscqK5t2XTg/LKiha6lUKkJDwygoyLcqDwys/5Wop6cX2dnHmnxtIYQQ9uNcdbFlE6WskmPU1NWiclDRySucMRHDifGLoqNXqKzJLq45SeCvgX7Bva+45/u5n/5BSa2+Xrmvsw//r9dDVxtak8TFxRMcHMIPP2zi9tun8cMPmwAsQ0cWLnyNjRs3MHnyHXTvHoeHhwfgwEsv/c0qmW9O5eXlPPbYA7i5eTBz5kOEhISi0WjIysrg/fffwWw2t8h1/0h1kVUDWuqehRBCtIwaUw1ZJdmk/z759Fx1MQD+Lr7c0L7n72uyd8FN3fCeKEJcK5LA27lbIpOsxsADqFVqbom88iUbr8bw4SNZsuQTcnNzSE39nqioGEtP9YVx7o89NttSv7a2tsm97wDt2weRm5tTr/z06VNWP//yyz5KS0t55ZXX6Nnzv3MC8vPzGmi1cROHgoKCLdf6Y5uKopCbm0NERGSj2hFCCGHfzIqZ3PI8juiyyNBlkV16ErNiRqNSo/WNZEhoIjH+WgJd28nkU2FXJIG3cxcmqtp6FZoLRo4czZIln7Bo0UJyc3OskvWGeqLXrPmSurq6Jl9nwICBrFq1gszMDMs4+JKSEn744Vurehc2f/pjb7fRaKw3Th7A1dW1UR8moqO74evrx1dfrWb06HGWDZ62bk2lqKiQ6dNnNPl+hBBC2IfS2jLLrqcZuqNUGM8PvQzz6MCwsEF089cS4d0JtUpSJGG/5LezFegb1IsbQ2/AZGr54SCXExHRmS5dtOzYkYZKpWLYsP9O3rzxxkS++24j7u4edOoUweHDv7F37x68vb2bfJ1p0+7mu+828uSTj5KcPBVnZxdSUtbRvn0wFRVHLfXi4nrg6enFK6+8RHLyFBwcHPjuu400NHolKiqa77//lnfeeYPo6G64urqRmDioXj0nJycefvgx/vGPl3nssQcZPnwkhYUFrF79JZ07RzJ+fP2VcIQQQtgnY52R7NKTHNFlkqE7ypmK8/OYPNUexPhF0c1fS7RfV7w0njaOVIjGkwReNNnIkUkcO5ZFQkJvy2o0AE888RQqlYoffviW2loDcXHxvPnmuzz55GNNvka7du14++1/s3DhfJYs+dRqI6d//vPvlnre3j7Mn7+QRYveZPHi9/H09GLkyNHccENfnnxyllWbEybcRlZWBhs3fs2XXy4jKCi4wQQeYMyY8Wg0GpYu/Yx3330Ld3d3RoxI4qGHHpNdW4UQwo4pikJBVSHpuqMc0WVytOQ4RrMRRwdHIr07MSFyNDF+UYR4BMnkU9FqOSgy065JiosrMJsv/sjOnj1FUFD9lVKulpOTyi564EXzaqnfl+tZQIAnRUXltg5DCLvXlt6VKmMVGSXHLGuyX1j8IdCt3fledj8tXXw64+IkHTCi6WzxrqhUDvj7e1z0uPTACyGEEKJVqTPXcao8x5KwnyzLQUHB1cmFKN8uJPkNJcZPi7+rn61DFaJFSAIvhBBCCLunqykhvfj88o6ZJceoNlXjgAMdvcJI6jSMbv5aOnqG4XiRpX2FaEskgRdCCCGE3amtM3D09zXZ03VZFFQVAeDj7E1CQHei/c5PPnVXu9k4UiGuPUnghRBCCGFziqJwpiLfsonScf0JTEodapWarj6dSezQjxj/KILcAmVNdnHdkwReCCGEEDZRbqiw9LCn67IoN5zfq6ODexCDwwYS46eli3cEake1jSMVwr5IAi+EEEKIa8JkNnG89JQlYc8pPwOAu9qNaN+uxPhHEePXFR/npu8fIsT1RBJ4IYQQQrQIRVEoqj7HEV0WGbosskqyqa0zoHJQEeHVkfGdRxHjpyXMM0TWZBeiCSSBbwGKosj4PHFZsgWDEKItqjZVk1mSTXpxJum6oxTX6ABo5+JH36DexPhp0fpG4urkYuNIhWi9JIFvZo6OThiNBjQa2SxCXJrRaMDRUV5BIUTrZlbMnC7PJb34KOm6TE6UncasmHF21KD17cLw8EHE+EUR4OZv61CFaDMke2hmHh4+6PVF+PgEoFZrpCde1KMoCkajAb2+CE9PX1uHI4QQTaavLeVI8flhMRm6o1SaqgAI9wxhRPjNxPhpifAOx0klaYYQLUHerGbm6uoOQGnpOerqTM3Wrkqlwmw2N1t7wrYcHZ3w9PS1/L4IIYQ9M9QZOaY/bpl8ml9ZAIC3xpPu7WLo5qclyq8rnpqLb/0uhGg+ksC3AFdX92ZPzAICPCkqKm/WNoUQQoiGKIpCfmWBJWE/pj+O0WzCSeVEF+8I+gffQIyflg7uQfJNsxA2YNME3mAw8NZbb7F+/XrKysqIjo5m9uzZDBgw4JLnpaSksHr1arKzsyktLSUwMJB+/foxa9YsQkJCrOpGRUU12MZLL73EHXfc0Wz3IoQQQrQGe87uJyV7E/paPT7OPtwSmUTfoF5UGCvJ1B39fcWYo+hrSwEIcgskMaQ/MX5auvp0RuOosfEdCCFsmsDPmTOH77//nhkzZtCxY0fWrVvH/fffz5IlS0hISLjoeRkZGbRv357Bgwfj7e1NXl4eK1euZNu2baSkpBAQEGBVPzExkVtuucWqLD4+vkXuSQghhLBXe87uZ1nGGoxmIwAltXqWpK/k6+zv0dWWoKDg5uRKlF9XYvy6EuOnxc9F5uoIYW9slsAfPHiQb775hrlz53LPPfcAMHHiRMaNG8eCBQtYunTpRc99+umn65UNGzaMSZMmkZKSwsyZM62Ode7cmQkTJjRr/EIIIURrk5K9yZK8X2BWzJQaShkdMZxuflo6eoXJmuxC2DmbvaGbNm1CrVYzefJkS5mzszPJycns27ePwsLCJrXXoUMHAMrKyho8XlNTQ21t7ZUHLIQQQrRSdeY69hb8SkmtvsHjJqWOsREjiPDuKMm7EK2AzXrg09PTiYiIwN3derJnjx49UBSF9PR0AgMDL9mGXq+nrq6OvLw83n33XYAGx8+vXr2aJUuWoCgKWq2Wxx9/nBEjRjTfzQghhBB2qNpUw095u9mW8xMltXpUDirMSv0VzXydfWwQnRDiStksgS8qKqJ9+/b1yi+MX29MD/yoUaPQ68/3Jvj4+PDCCy/Qv39/qzoJCQmMGTOG0NBQ8vPz+fzzz5k1axavv/4648aNa4Y7EUIIIeyLrqaErTk72Jm3h5q6Wrr6dGZK1ESqjNUsz1xrNYxGrVJzS2SSDaMVQjSVzRL4mpoa1Gp1vXJn5/M7mDZmuMuiRYuoqqrixIkTpKSkUFlZWa/OihUrrH6+9dZbGTduHK+99hpjx45t8vJX/v62W+M2IMDTZtcWojWRd0Vcr7J1p9iQuZmfc/YDMCCsF+OihhPp19FSx9vbjeUH11NcpcPfzY87ekzgpo59bRWyEK2Cvf1dsVkC7+LigtForFd+IXG/kMhfSp8+fQAYPHgww4YNY/z48bi5uXHnnXde9Bw3NzemTp3K66+/zvHjx4mMjGxS3MXFFZjNSpPOaQ6yDrwQjSPvirjemBUzh86lk5qTxjH9CVwcXRgSmsjNYQPPryBTh9U7Ee0Ww8v9Y6zeFXlnhLg4W/xdUakcLtlpbLMEPiAgoMFhMkVFRQCXHf/+Z2FhYcTGxrJhw4ZLJvAAwcHBAJSWljbpGkIIIYS9MNQZ+Dl/H1tztlNYfQ5fZx9u6zKOAR364urkYuvwhBAtyGYJfHR0NEuWLKGystJqIuuBAwcsx5uqpqaG6urqy9bLyckBwM/Pr8nXEEIIIWyptLactDM72X5mF5XGKjp6hnFf7DR6BsThqHK0dXhCiGvAZgl8UlISH3/8MatWrbKsA28wGFi7di29evWyTHDNy8ujurraaqiLTqerl3wfOnSIjIwMxowZc8l6JSUlLFu2jNDQUDp16tQyNyeEEEI0s7yKs2zJ2c5/zu6nTjET164bw8IHEendqcnzuYQQrZvNEvj4+HiSkpJYsGABRUVFhIeHs27dOvLy8nj11Vct9Z555hn27NlDZmampWzIkCGMHj0arVaLm5sbx44dY82aNbi7u/PII49Y6i1dupTU1FRuvvlmOnToQEFBAV9++SU6nc6y7KQQQghhrxRFIaPkKFtOb+eILhO1Ss2ADn0ZGpZIoFvA5RsQQrRJNkvgAebPn8+bb77J+vXrKS0tJSoqig8++IDevXtf8rxp06axa9cuNm/eTE1NDQEBASQlJfHII48QFhZmqZeQkMD+/ftZtWoVpaWluLm50bNnTx588MHLXkMIIYSwFZPZxN6CX9mSs50zFfl4ajwY33kUiSH98VC7X74BIUSb5qAoyrVfUqUVk1VohLBv8q6I1qzSWMWOMz/zY+5PlBrK6eAexNCwm7ghKAG1qnn73ORdEaJxZBUaIYQQQtRTVFXM1tzt7Mr7DwazkRg/LXeG3U6Mn1bGtwsh6pEEXgghhLABRVE4XnqK1Jw0DhYdRuWgok/7BIaG30SIR7CtwxNC2DFJ4IUQQohrqM5cx4Fzh0k9ncbJstO4ObkysuMQBofeiLezl63DE0K0ApLACyGEENdAjamGXfl72ZqzneKaEtq5+nO7diL9g2/A2VFj6/CEEK2IJPBCCCFECyqp0bMt9yd+yttNtamGSO9O3NZ1PHHtuqFyUNk6PCFEKyQJvBBCCNECcsrPkHo6jX2FB1AUhYTAOIaFD6KTV7itQxNCtHKSwAshhBDNxKyYOVKcSerpNLL02Tg7ahgceiNDQhPxd/W7fANCCNEIksALIYQQV8lQZ2TP2X1sydlBQVUhPs7e3NplLDcG98VN7Wrr8IQQbYwk8EIIIcQVKjdUkJa7k7Qzu6gwVhLmGcI93e6gV2APHFWOtg5PCNFGSQIvhBBCNNHZygK25Gxn99n9mMwmuvvHMCx8EF19OsvGS0KIFicJvBBCCNEIiqKQVZJNak4ah4szUKuc6BfUm6FhNxHkHmjr8IQQ1xFJ4IUQQohLqDPXsa/wAFtOp5FTkYeH2p2xESO4KWQAnhoPW4cnhLgOSQIvhBBCNKDKWM1PebvZlvsT+tpSgtwCmRZ9G33b90LtqLZ1eEKI65gk8EIIIcQfnKvWsS1nBzvz91BbZ0Dr24U7oibRzT9KNl4SQtgFSeCFEEII4ETpKVJPp/Fr0SEcHBzoHdiTYeGDCPPsYOvQhBDCiiTwQgghrltmxczBosOk5qRxvPQUrk6uDA8fzM1hA/Fx9rZ1eEII0SBJ4IUQQlx3ausM7Mr/D1tzdnCuuhh/Fz+Su97CgOA+uDg52zo8IYS4JEnghRBCXDf0taX8mLuTHWd+pspUTYRXOBMiR9MzoLuMbxdCtBqSwAshhGjzcsvz2JKznb0Fv2JWzMQHdGdY+CA6e3e0dWhCCNFkksALIYRokxRF4Yguiy2n08goOYrGUUNiSH+GhiXSztXf1uEJIcQVkwReCCFEm2KsM/Kfgl9IzdnO2coCvDVeTOg8msSQfrip3WwdnhBCXDVJ4IUQQrQJFYZKtp/ZxY+5Oyk3VhDiEcyMmCn0bh+Pk0r+3Akh2g75P5oQQohWraCqiC0529mdvw+j2Ug3/yiGhQ0iyrcLDg4Otg5PCCGanSTwQgghWh1FUTimP05qznYOnUvH0UFF36BeDA0fRLB7e1uHJ4QQLUoSeCGEEK1GnbmOXwoPkpqzndPluXio3UnqNIxBoQPw0njaOjwhhLgmJIEXQghh96pN1fyUt4dtOT9RUqsn0K0dU6Mm0S+oNxpHta3DE0KIa0oSeCGEEHZLV1PC1pwd7MzbQ01dLV19OjMlaiKx/tGy8ZIQ4rolCbwQQgi7c6osh9TTafxS9BsAvQJ7MCxsEOFeoTaOTAghbE8SeCGEEHbBrJj57Vw6qafTyC49gYujC0PCErk5dCB+Lr62Dk8IIeyGTRN4g8HAW2+9xfr16ykrKyM6OprZs2czYMCAS56XkpLC6tWryc7OprS0lMDAQPr168esWbMICQmpV3/VqlV8/PHH5Obm0qFDB2bMmMH06dNb6raEEEI0gaHOwM/5+9ias53C6nP4ufhyW5dxDOjQF1cnF1uHJ4QQdsemCfycOXP4/vvvmTFjBh07dmTdunXcf//9LFmyhISEhIuel5GRQfv27Rk8eDDe3t7k5eWxcuVKtm3bRkpKCgEBAZa6K1as4MUXXyQpKYl7772XvXv3Mm/ePGpra7nvvvuuxW0KIYRoQGltOWm5P7H9zM9Umqro6BnGfbHT6BkQh6PK0dbhCSGE3XJQFEWxxYUPHjzI5MmTmTt3Lvfccw8AtbW1jBs3jsDAQJYuXdqk9g4fPsykSZN4+umnmTlzJgA1NTUMHjyY3r17895771nqPvXUU2zZsoUff/wRT8+mLTtWXFyB2XztH1lAgCdFReXX/LpCtDbyrti/vIqzpOaksffsL9QpZnq068bQ8EFEeneSjZeuIXlXhGgcW7wrKpUD/v4eFz1usx74TZs2oVarmTx5sqXM2dmZ5ORkFi5cSGFhIYGBgY1ur0OHDgCUlZVZynbv3o1er2fatGlWdadPn86GDRtIS0tj7NixV3knQgghLkdRFDJKjpJ6Oo10XRZqlZobO/RlSFgigW4Bl29ACCGEhc0S+PT0dCIiInB3d7cq79GjB4qikJ6eftkEXq/XU1dXR15eHu+++y6A1fj5I0eOANC9e3er82JjY1GpVBw5ckQSeCGEaEFGs4l9Bb+yJWc7Zyry8dJ4Mr7zKBJD+uOhdr98A0IIIeqxWQJfVFRE+/b1t7u+MH69sLDwsm2MGjUKvV4PgI+PDy+88AL9+/e3uoZGo8HHx8fqvAtljbmGEEKIpqs0VrH9zM+k5f5EqaGcDu5B3Bk9mRuCElCrZAE0IYS4Gjb7v2hNTQ1qdf3d85ydnYHz4+EvZ9GiRVRVVXHixAlSUlKorKxs1DUuXKcx1/izS41HamkBAbJNuBCNIe+K7ZwtL+SbrC1sO7GL2joD8UExjIsaTo/2MTK+3Q7JuyJE49jbu2KzBN7FxQWj0Viv/EJSfSGRv5Q+ffoAMHjwYIYNG8b48eNxc3PjzjvvtFzDYDA0eG5tbW2jrvFnMolVCPsm78q1pygK2aUn2ZKznYNFh1E5qOjTPoGh4TcR4hEMwLlzFTaOUvyZvCtCNI5MYv2DgICABoewFBUVATRpAitAWFgYsbGxbNiwwZLABwQEYDQa0ev1VsNoDAYDer2+ydcQQgjxX3XmOn4tOkRqThqnynJwc3JlZMchDA69EW9nL1uHJ4QQbZbNEvjo6GiWLFlCZWWl1UTWAwcOWI43VU1NDdXV1ZafY2JiADh06BCJiYmW8kOHDmE2my3HhRBCNF6NqYad+f9hW84OimtKCHD1Z4p2Iv2Cb8DZUWPr8IQQos1T2erCSUlJGI1GVq1aZSkzGAysXbuWXr16WSa45uXlkZ2dbXWuTqer196hQ4fIyMggNjbWUta/f398fHxYtmyZVd3ly5fj5ubGoEGDmvOWhBCiTSup0bPu2Dc8t/MfrDm6AR9nbx6Im8EL/f/KoNAbJXkXQohrxGY98PHx8SQlJbFgwQKKiooIDw9n3bp15OXl8eqrr1rqPfPMM+zZs4fMzExL2ZAhQxg9ejRarRY3NzeOHTvGmjVrcHd355FHHrHUc3Fx4fHHH2fevHk88cQTJCYmsnfvXlJSUnjqqafw8pKveIUQ4nJOl+eSejqN/YUHURSFhMA4hoUPopNXuK1DE0KI65JN1/KaP38+b775JuvXr6e0tJSoqCg++OADevfufcnzpk2bxq5du9i8eTM1NTUEBASQlJTEI488QlhYmFXd6dOno1ar+fjjj0lNTSU4OJhnn32WGTNmtOStCSFEq2ZWzBwuziD1dBpH9cdxdtRwc+hAbg4diL+rn63DE0KI65qDoijXfkmVVkxWoRHCvsm7cnUMdUb2nN3HlpwdFFQV4uPszZCwRAZ26Iurk6utwxPNSN4VIRpHVqERQghhl8oNFfyYu5PtZ3ZRYawkzDOEe7rdQa/AHjiqHG0dnhBCiD+QBF4IIa5jZysLSD29nT0F+zGZTcS1i2Fo2CC6+nSWjZeEEMJOSQIvhBDXGUVRyCrJJjUnjcPFGahVTvQP6s3QsJto7y77YwghhL2TBF4IIa4TJrOJ/YUHST2dRm5FHp5qD8ZGjOCmkAF4ai4+1lIIIYR9kQTezu06fJa1P2ajK6vFz8uZSYMjGRAbZOuwhBCtSJWxih15u/kxdyf62lKC3AKZFn0bfdv3Qu2otnV4QgghmkgSeDu26/BZPvs2A4PJDEBxWS2ffZsBIEm8EOKyzlXr2JqznZ35/8FQZyDKtwvTom8jxk+LysFm+/gJIYS4SpLA27G1P2ZbkvcLDCYza37MlgReCHFRJ0pPkXo6jV+LDuHg4MAN7XsyNGwQYZ4dbB2aEEKIZiAJvB0rLqttsFxXVsv6HSdIjAvG39vlGkclhLBHZsXMgaLDpJ5O40TZKVydXBkePpibwwbi4+xt6/CEEEI0I0ng7Zi/l3ODSbzaUUXKjhOk7DhBbGc/BvXoQM+u7XBylK/Ehbje1Jhq+Tl/L1tztnOuRoe/ix/JXW9hQHAfXJycbR2eEEKIFiAJvB2bNDjSagw8gMZJxd2jo+ka4s2O3/LZfjCf9746hKebmoHdg7kpPphgf3cbRi2EuBb0taW/b7z0M9WmaiK8OjKxy1jiA2JlfLsQQrRxDoqiKLYOojUpLq7AbL52j+xyq9CYzQqHTujYfiCPX4+do86s0CXUm0E9OtAnOhBnjeygKK4vbX17+NzyPLbkbGdvwa+YFTPxAd0ZFj6Izt4dbR2aaGXa+rsiRHOxxbuiUjng73/x5X0lgW+ia53AX9CYX57SSgM7D+WTdiCfAl0Vrs6O9Itpz03xHegU5Cm7KorrQltMShRF4Yguk9TTaWSWHEPjqGFAcB+GhiXSztXf1uGJVqotvitCtAR7TOBlCE0b4u2uYXS/jiT1DedobilpB/LYeegs237NIyzQg0HxHegf2x53F1n3WYjWwFhn5D8Fv5Cas52zlQV4a7yYEDmaxA79cFO72To8IYQQNiI98E1kzz3wDamqMbL7SAFpB/I5VVCOk6OKG6IDGNSjA1HhPtIrL9qcttCrWGGoJO3MTtJyd1FurCDEI5hhYYPo3T4eJ5X0u4jm0RbeFSGuBemBF9ecm4uaIb1CGdIrlFNny0k7mMfPhwv4+XABgb6u3NQjmIFxwfh4yGoVQthaQWUhW3K2s/vsPoxmE938oxgWNogo3y7yYVsIIYSF9MA3UWvrgW9IrbGOfZmFpB3IJytHj8rBgfgu/twU34G4zn44qmQFC9F6tbZeRUVROKY/TmpOGr+dS8dJ5UTf9r0YGn4Twe7tbR2eaMNa27sihK1ID7ywC85qR27sHsyN3YPJL65kx8F8fvotn1+OnsPHQ0Nij2AS9Vq78gAAIABJREFUe3Qg0MfV1qEK0WbVmev4pfAgqTlpnC4/g4fandGdhjModABeGk9bhyeEEMKOSQ98E7WFHviGmOrMHDhWzPaDefx2vBhFgZiOvgyK70AvbTvUTrIcpWgd7L1XsdpUzU95e9iW8xMltXoC3doxNGwQ/YJ6o3GUCebi2rH3d0UIeyE98MJuOTmq6B0VQO+oAHRlNec3iTqQz79TDuPu4sSA7kEM6tGB0MCL/zIJIS6uuLqEbbk72Pn/2bvz8CbrdA/432xNmiZpliZt0r2ltIUubAIVqCDoMIAbI+O4wKiDx3E54zK+lzq+13XOmeXoODjK6xlnFPGMcFAUBCqOOgg4FBBBtqaFlqULbUn3LV2TtMn7R0ogtmAKpUna7+e6uMY+yZPn9zj8zM3N/dy35RB6+mxIUSfhntQ7MVGXxsFLREQ0JAzgaQCtSobbZyViyY0JKK5oQX6BBf86dh47D1cjyaRCbrZ7SFSolL99iH7IOWsVdlXm41hDIQBgiiEL82NzEaeK8fPKiIgoWDECo8sSCgSYmKjFxEQt2rvsOHCiDvkFFvz9ixJ8uPMMpqcbkJttQpJJxQ4ZRJdwupwobDyJXZX5KG2rgEwkw7zY2ZgXMxsamdrfyyMioiDHAJ58opSH4NYbYnHLtBiUWqzYW2DBoeJ67DXXIDoiDHOyTciZGAmlPMTfSyXyG3ufHd/WHMbuqr1o6G6CVqbBT8YtQY5pOkLFMn8vj4iIRolheYi1t7cXu3btQltbG+bNmwe9Xj8cawtIo/Uh1qvRbevFdyX1yC+woMxihVgkwJTxeszJNiE9XgMhs/LkB/7YK222duRX78fe89+is7cL8apYzI/NxSR9BkRCPgBOgSkQv1eIAtGoeIj11VdfxcGDB/HJJ58AcPcwfuihh3D48GG4XC6o1Wp8/PHHiIuLu/pVU1AIlYqRm21CbrYJ1fUdyDdbcKCoFoeK6xERLvMMidKqmHmk0cnSUYtdlfk4XHcMfS4nsiIm4Oa4XCSHJ7CsjIiIrpshB/B79+7FjTfe6Pl59+7d+O6777By5Uqkp6fjd7/7Hd555x38/ve/H9aFUmCLMShw34LxWDY3GUdPNyK/wIKte8uxbV85MpN0mJNlQvY4HcQidtug4OZyuVDSfAa7qvJR3HwaEqEEN5qmY17sbBjko/dvH4mIKHAMOYCvra1FfHy85+evv/4aMTExeO655wAAZ86cwfbt24dvhRRUJGIRZkyIxIwJkahv7cY+swX7zDX4y9ZCqMJCMCsjCrnZJkRq5f5eKtGQOJy9OFx3HLsr82HprIUqRInbkn6E2dEzoZCE+Xt5REQ0hgw5gHc4HBCLL5528OBBr4x8bGwsGhoahmd1FNQM6lAszU3GHbMTUVjWjL0FFvzzUBW+OFiJ8bFq5GYbMTXVAKmENcIUuDocndh3/iD2VO+H1d4OU1gUHkj/KaZFToJEyD4AREQ08ob87RMVFYVjx47hpz/9Kc6cOYOqqir86le/8rze1NQEuZzZVbpIJBRi0rgITBoXgdYOG/b3D4l697NibPjqDGZOjERulgnxURwfT4GjvqsRX1ftxbc1h2F3OpCuHY8VsfcgTZvC+nYiIvKrIQfwixcvxltvvYXm5macOXMGCoUCN910k+f14uJiPsBKl6VWSLE4JwE/nhmP05WtyDdbsLegBl8fPY/4SCVys42YMSEKchkzmzTyXC4XStsqsLsyH+bGkxAKhLghcjJujpuDaIXR38sjIiICcBUB/KOPPoqamhrs2rULCoUCf/zjH6FSqQAA7e3t2L17Nx588EGfPstut2P16tXIy8uD1WpFWloannnmGeTk5FzxvB07duDzzz+H2WxGU1MTjEYj5s2bh8cffxxKpXcWNzU1ddDP+M///E/ce++9Pq2Thp9QIEBavAZp8Rrcf4sD356ow57jFqzfcRof7T6LaWnuIVEpMeHMdtJ11+fsw/GGIuyqysc5axXCxHL8KH4ecmNuRLhU5e/lEREReRmWPvAXOJ1OdHZ2QiaTQSKR/OD7n332WezYsQMrVqxAfHw8tm7diqKiIqxfvx6TJ0++7HkzZsyAwWDAggULYDKZcOrUKWzcuBEJCQn45JNPIJVKPe9NTU3F7Nmzcfvtt3t9RnZ2NhISEoZ8j+wDf/24XC5U1LZjb4EF356sQ4+9D5FaOXKzjbgxw4jwMA6Joh82lL3S09uDbyyH8HX1fjT3tEAfqsPNsXMwwzgNUhF/v9HoNha+V4iGQyD2gR/WAN5utyMkxLcvPbPZjGXLluHFF1/0ZOxtNhuWLFkCg8GADRs2XPbcgwcPYsaMGV7Htm3bhueffx4vv/wyli5d6jmempqKFStW4KWXXhr6DQ2CAfzIsNn73EOizBacrW6DSCjApHERmJNtQkaiFkIhs/I0OF/2SktPK76u3of95w+hp68HyeGJmB83B5kREyAUsNUpjQ1j7XuF6GoFYgA/5BKaPXv2wGw249///d89xzZs2IDXXnsNPT09+PGPf4xXXnnlBzPwX375JSQSCZYtW+Y5JpVKcffdd+P1119HfX09DAbDoOd+P3gHgAULFgAASktLBz2np6cHAoHAKztPgUsaIsLsLCNmZxlhaezEXrMF+wtrceR0AzRKKeZkGTE704gIdai/l0pBpLK9Grsq83G03gwAmKzPxM1xc5Cg4nM7REQUPIYcwK9duxY6nc7zc2lpKf77v/8bsbGxiImJweeff47MzMwfrIMvLi5GYmIiwsK8+ydnZWXB5XKhuLj4sgH8YBobGwEAGo1mwGubN2/G+vXr4XK5MH78ePzqV7/CLbfc4vNnk3+ZIsJwz80p+MlNyTh+xj0kavv+CmzfX4EJiVrkZpswaVwEJGJmTmkgp8uJE00l2FWZjzOtZZCJpJgbMwtzY2ZBF6r19/KIiIiGbMgBfFlZmVfXmc8//xxSqRSbN2+GQqHAr3/9a2zbtu0HA/iGhgZERkYOOK7XuycZ1tfXD2lda9asgUgkwq233up1fPLkyVi0aBFiYmJQU1ODdevW4cknn8Rrr72GJUuWDOka5F9ikRDT0gyYlmZAY1s39plrsK+wBn/dVgRFqAQ3ZkRhTrYJ0REcqjMWHao9ik9Lv0SrrRVqqRqLEm9Bn6sPX1ftRV1XA9TScNw1bjFmmaYjVMy/uSEiouA15AC+ra3NK8v9zTffYObMmVAo3HU606dPx549e37wc3p6egYts7lQ4mKz2Xxe0/bt27F582Y8+uijA1pYbty40evnu+66C0uWLMGf/vQnLF68eMgdTq5Uj3S96fXsk36BXq9E+jgDHr4zC8dP12PHwXPYdaQaO76rQnqCFrfOiMPs7GjIpGxHORbsPXcIH57aAnufHQDQYmvFhpJNAIBETSx+lfUQZsZOhVjIoWFEl+L3CpFvAm2vDDm60Wg0sFgsAICOjg4UFhbi2Wef9bze29uLvr6+H/wcmUwGh8Mx4PiFwN3XWvXDhw/jpZdewty5c/HUU0/94Pvlcjl+9rOf4bXXXkNZWRmSk5N9us4FfIg18MTp5Fi5KB0/vSkZ3xTVIr/AgtUfHcfbWwsxY0IkcrNNSIhSsh3lKGPvc6ClpwXNPa1478RGT/B+KaVEgV9PehICgQAtTV1+WCVR4OL3CpFvRsVDrJMmTcLGjRsxbtw45Ofno6+vD7m5uZ7Xz50751Ptul6vH7RMpqGhAQB8+oySkhI89thjSE1Nxeuvvw6RyLfsmtHoHsjS1tbm0/spOKjCQrBwRhx+ND0WZ6rbsLfAggNFtdhz3IIYvQK52UbMnBgFRegPtzgl/3K5XOju7UZTTyuae1ou+XXx5w5H5w9+Trujg39wIyKiUWfIAfyvfvUrrFixAk8//TQAd0nKuHHjALi/dHfu3Dlol5jvS0tLw/r169HZ2en1IGtBQYHn9SuprKzEypUrodVq8fbbb0Mul/t8D1VVVQAArZYPsI1GAoEA42PVGB+rxr0LxuNQcR32FFjwwc4z+PjrUkxL1WNOtgmpcWoIGdz5hdPlRLu9Y9DA/MI/9/R5l9FJhGJoZRpoZRrEKk3QSDXQytTQyjT43xMfoM1uHXAdjVQ9UrdEREQ0Yq6qD3xrayuOHj0KpVKJG264wXO8ra0N27Ztw4wZM34wAC8oKMBPf/pTrz7wdrsdS5YsgU6nw4cffggAsFgs6O7u9ip1aWhowL333gubzYYPP/wQMTExg16jubl5QJDe0tKC2267DVKpFLt27RrqrbOEJohV1rVjb0ENDpyoRZetFwZ1KOb0D4nSKNledDj1OnvRamtDc0/L97Lo7n9u7WlFr8u71C5UHOoJyN2/3P+s6/9ZIQm7bDb9UO1RfFDyCRzOi2V5EqEE96X9BNOjplzXeyUKVvxeIfJNIJbQDOsgp6F66qmnsGvXLvz85z9HXFycZxLr+++/j6lTpwIAli9fjkOHDuHUqVOe8+644w6UlJRg5cqVGD9+vNdnxsXFeaa4vvnmm9i1axfmzp0Lk8mEuro6fPTRR2hubsZf/vIXzJs3b8hrZgAf/OyOPhw53YC9BRaUVLZCKBAgK1mH3GwTMpO1EAnZjvKH2Prsly1tae5pRZvNChe890l4iPKS4FwDjUztFbCHimXXtKbvd6G5PXkhg3eiK+D3CpFvAjGAv+oWHZWVldi1a5enHCU2Nhbz588f0AXmSl599VW88cYbyMvLQ1tbG1JTU/HOO+94gvfLKSkpAQC8++67A1676667PAH85MmTcfToUWzatAltbW2Qy+WYNGkSHn300R+8Bo1eIRIRciZGIWdiFOqau7C3vx3l8bONCFeEYHamEXOyjDBofC/LGk1cLhc6HV1XDNA7e70fCBUKhNBI3QF5qmbcgCy6RqaGRHh9OwJNj5qC6VFTGJQQEdGod1UZ+DfeeANr1qwZ0G1GKBTi0Ucf9akbTLBiBn506u1zorC0CfkFFpjLmuByAenxGszJMmJqqh4S8ehpP+h0OdFms34vKPcO0u1O7w5RIaIQ77KW/vpzTf+xcKkKQkFg/M0F9wqRb7hXiHwzKjLwmzdvxt/+9jdMnjwZK1euREpKCgDgzJkzWLt2Lf72t78hNjYWS5cuvfpVE40wsUiIyeP1mDxej5Z2G/YV1mBvgQXvbD+JsK/EmDkxCrnZJsQa/DcHwFcOZ6+nveJgwXmLrQ1Ol9PrnDCJHFqZBpFhBqTrxg/IoIeJ5ezmQkREFCCGnIFfunQpJBIJNmzYALHYO/7v7e3F/fffD4fDgS1btgzrQgMFM/Bjh9PlQsm5FuQXWHD0dAN6+1xINCoxJ9uEGemRCPXTkKju3u5LAvKBQbrV7v37RAABwqUqr4D8+wG6VBTil3u5HrhXiHzDvULkm1GRgS8tLcWzzz47IHgHALFYjEWLFuHPf/7zUD+WKOAIBQJMSNBiQoIWHd0OHOgfErXuy1PYuOsMpqe5h0QlR6uGLTvtcrnQ7ugYJDi/+HN3b4/XOWKBqP+BUA0m6tIGBOkaaThEnEBKREQ0agw5gJdIJOjquvxEw87OTkgkHJRDo4siVIJbbojFgmkxKKuxYm+BBQdP1mNfYQ2MOjlys03IyYiCSn7lTHafs6+/veIgGXRbC1p6WuFw9nqdIxPJPEF5cnjigABdGRIWMPXnREREdP0NOYDPzMzERx99hGXLliEiIsLrtaamJnz88cfIzs4etgUSBRKBQIBkUziSTeG45+YUfFdSj70FFny0+yw2/6sU2eM1yE6XQ6tzosXWOqDEpdXWNqC9olKigFamQXSYEZm6CQNKXeSSUD/dLREREQWiIQfwjz/+OB588EEsWrQIP/nJTzxTWM+ePYstW7ags7MTq1atGvaFEvmby+VCV2+3d0mLvAWGya1wpjShqasZxYIeFJ8HcN59jhBCqGXh0MrUSNEkDQjONVI1QkT8GysiIiLy3ZAD+BtuuAFvvvkmfve73+F///d/vV4zmUz44x//iGnTpg3bAolGitPlhNXeftnuLc09LbD12b3OkQgl7mA8VI1kTQzCQ9SwtohQWmFH+blewC5FUpIeudlGZI+LgFjEUhciIiK6Nlc9idXpdKKoqAjV1dUA3IOcJk6ciI8//hjr1q3D559/PqwLDRTsQhO8ep29aOlpu6Tm3LsOvbWnFb0u79kGcnHogI4tl/6zQhJ22QdYG1q7sddcg/2FNWhpt0Ell+DG/iFRRl3YSNzymMS9QuQb7hUi34yKLjQXP1iIrKwsZGVleR1vaWlBeXn51X4s0VXr6bVddnLohfaKl9afCyCAKkQJrUyDeGUMJuszBwTpMrHsqtejV4diaW4S7pidgKKyZuQXWLDjUBW+PFiJ8THhmJNtwrQ0A6QSdoghIiIi3/mnkTXRELlcLnQ4Oj0B+WCDijp7vbsjiQQiaKTh0Mo0SNeOH9C9RS0Lh0R4/beASChE9rgIZI+LQFuHDfuLarG3wIK1/yjGBztPY+YE95Co+CjldV8LERERBT8G8BQQnC4n2mxWNF0mg97S0wK70+F1jlQU4gnGE8Lj3QG6VA1tqPuYKkQZcO0VwxVSLJoZjx/PiMPpqlbkF1iwr7AGXx87j7hIBXKzTZg5IRJyGR9sJSIiosExgKcR4ehzXFJzfiEov/hzi60NTpfT6xyFJAxamRrGMAMm6lIHtlcUhw7bAKWRJhAIkBqnQWqcBvff4sC3J+uQf9yC/9txGh/tPotpqQbkZhsxPlYdtPdIRERE1wcDeBoW3b3dnqz5YFn0dnuH1/sFEEAtdbdXTApP+F5wroZGpoFUdOWhSKOFXCbBzVNicPOUGJyrbUd+gQXfnqzFgRO1iNSEIjfbhBszjQgPGxv/PoiIiOjKfArgv98u8kqOHj161YuhwORyuWC1d3i1U2zuaUWL7WKQ3t3b43WOWCh2l7PINMjUpQ/o3qKWhkMk5MOb3xcfpcTyqFT89OZxONw/JGrTv0qxJb8M2eMikJttREaiDkIhs/JERERjlU8B/B//+MchfSj/yj+49Dn70GprG7T2/EK7xV5nr9c5oWKZJyAfp070CtA1Ug2UIWEBV38eTKQSEWZlGjEr04iapk5PO8qjpxugUUoxu78dZYSaU1qJiIjGGp/6wB86dGjIHzx9+vSrWlCgC8Y+8PY+e39pi/dQIncNeitabW1e7RUBQBmiGJA1113yc6iYgeNI6+1zouBsI/ILalBU1gQAmJCgwZxsEyan6CER8w9MAHtbE/mKe4XIN4HYB/6qBzmNVSMdwB+qPYpPS79Eq60VaqkatycvxPSoKZ7XXS4Xunq7Lzs5tLmnFR2OTq/PFAqEnvaKgw0o0kjVkIjYBSWQNbX1YH9hDfaaLWiy2qAIleDGjCjMyTIiWn/5DT8WMCgh8g33CpFvGMCPAiMZwB+qPYoPSj6B45L2iSKBEMnhiRCLxJ4uLrY+u9d5EqHkspNDdTINwqUqlreMEk6nCyfPNSO/oAbHTjegz+lCcrQKuVkm3JBugCxk7D2nzqCEyDfcK0S+YQA/CoxkAP//7v9vtNhaBxwXQIAYhfEyGXQNwiRyPocwBlm77DhQVIv8AgtqmrogDRFhRroBudnRSDQqx8zvCQYlRL7hXiHyTSAG8GMvPRdEBgveAcAFF16Y/vQIr4YCnUoegh9Nj8OtN8Si9Ly1vx1lHfILahCjD8OcLBNyMqKgCGV5FBERUTBjAB/ANFL1oEG8Rqr2w2ooWAgEAoyLCce4mHDcuyAFB4vrsLfAgg93ncGmf53FlPF65GabkBavgXCMZOWJiIhGEwbwAez25IUDauAlQgluT17ox1VRMAmVijF3UjTmTopGZV079pprcKCoFoeK6xERLsOcbBNmZxqhUUr9vVQiIiLyEWvghyjQutAQDZXd0YejpxuQX2BBSWUrBAIgK0mH3GwTMpN1EIuC+wFn1vUS+YZ7hcg3gVgDzwB+iIKxDzzR5dS1dGGfuQb7zDVo67QjPCwEszKNmJNtRKRG7u/lXRXuFSLfcK8Q+YYB/CjAAJ5Goz6nE+bSJuwtqIG5tAlOlwtpcWrMyTZh6ng9QiQify/RZ9wrRL7hXiHyTSAG8KyBJyKIhEJMTtFjcooeLe02fFNUg/wCC9ZsP4kNUjFyJkZhTrYRcZFKfy+ViIhozGMAT0ReNEopFuck4Mcz43HqXAv2mmuwp8CCXUerkRClRG62CTMmRCJUyv98EBER+QNLaIaIJTQ0FnV0O/DtCfeQqOqGToRIhLghzYDcbBPGRYcH1JAo7hUi33CvEPmGJTREFJQUoRIsmBaL+VNjUFHb7hkStb+wFkadHHOyTLgxMwoqeYi/l0pERDTq+TUDb7fbsXr1auTl5cFqtSItLQ3PPPMMcnJyrnjejh078Pnnn8NsNqOpqQlGoxHz5s3D448/DqVyYI3upk2b8N5776G6uhomkwkrVqzA/ffff1VrZgaeyK3H3ovvSuqxt6AGZ8+3QSQUYHJKBHKzTZiQoIVQ6J+sPPcKkW+4V4h8E4gZeL8G8M8++yx27NiBFStWID4+Hlu3bkVRURHWr1+PyZMnX/a8GTNmwGAwYMGCBTCZTDh16hQ2btyIhIQEfPLJJ5BKLw6l2bhxI/7jP/4DCxcuxKxZs3D48GHk5eXh+eefx8MPPzzkNTOAJxrofGMn9hZY8E1RLTq6HdCppJid5R4SpQuXjehauFeIfMO9QuQbBvCXMJvNWLZsGV588UU8+OCDAACbzYYlS5bAYDBgw4YNlz334MGDmDFjhtexbdu24fnnn8fLL7+MpUuXAgB6enpw0003YerUqXjrrbc8733uueewe/du7NmzZ9CM/ZUwgCe6PEevE8fPNiK/wIKT5c0AgIlJWuRmmTApJWJEhkRxrxD5hnuFyDeBGMD7beTil19+CYlEgmXLlnmOSaVS3H333Thy5Ajq6+sve+73g3cAWLBgAQCgtLTUc+zgwYNobW3Ffffd5/Xe+++/H52dncjPz7/W2yCiS0jE7odbf33PJPzxlzm4bVYCzjd04q1tRfj1X/bj491nUdPU6e9lEhERBTW/PcRaXFyMxMREhIWFeR3PysqCy+VCcXExDAaDz5/X2NgIANBoNJ5jJ0+eBABkZGR4vXfixIkQCoU4efIkFi9efLW3QERXEKEOxZ1zknD7rEQUlTdjb4EFXx2uwpeHKpESE47cbBOmpRogDQmeIVFERESBwG8BfENDAyIjIwcc1+v1AHDFDPxg1qxZA5FIhFtvvdXrGiEhIVCr1V7vvXBsqNcgoqETCgXIStYhK1mHtk57/5CoGqz9RzE+2HkaMyZEITfbiPhIZUC1oyQiIgpUfgvge3p6IJFIBhy/8ACqzWbz+bO2b9+OzZs349FHH0VcXNwPXuPCdYZyjQuuVI90ven1nIJJwU2vB8Yl6LB88UScLG/GjoPnsK/Agn8dO48kUzhunRGHm6bEQHGN7Si5V4h8w71C5JtA2yt+C+BlMhkcDseA4xeC6ks7yVzJ4cOH8dJLL2Hu3Ll46qmnBlzDbrcPep7NZvP5GpfiQ6xEw8OgDMEDC1KwdHYCDp6sQ35BDf62tRBrt5/A1FQ9bso2YXyseshZee4VIt9wrxD5JhAfYvVbAK/X6wctYWloaAAAn+rfS0pK8NhjjyE1NRWvv/46RCLvWlq9Xg+Hw4HW1lavMhq73Y7W1tYh1dgT0fUhl0kwb0oM5k2JwbnaduSbLfj2RB2+PVEHgyYUc7KMmJVphFox9D9wExERjUZ+60KTlpaG8vJydHZ6d6QoKCjwvH4llZWVWLlyJbRaLd5++23I5fIB70lPTwcAFBUVeR0vKiqC0+n0vE5EgSE+Sonlt6biz0/Owsol6VArpPhkTxme+8s3ePMTM46fbUSf0+nvZRIREfmV3wL4hQsXwuFwYNOmTZ5jdrsdW7ZswZQpUzwPuFosFq/WkIA7S//www9DIBBg7dq10Gq1g15j5syZUKvV+OCDD7yOf/jhh5DL5cjNzR3muyKi4SCViHBjhhEv3D8F//1vM/Gj6bEoPd+G/2+zGf/PW99gS34p6lu7/b1MIiIiv/DrJNannnoKu3btws9//nPExcV5JrG+//77mDp1KgBg+fLlOHToEE6dOuU574477kBJSQlWrlyJ8ePHe31mXFyc1xTXDRs24Le//S0WLlyI2bNn4/Dhw9i2bRuee+45PPLII0NeM2vgifyjt8+JgrNN2Gu2oLCsCS4XkB6vQW62CVPGR+DwqQZs2VOKZqsNWpUUS29KRs7EKH8vmyhg8XuFyDeBWAPv1wDeZrPhjTfewPbt29HW1obU1FQ8++yzuPHGGz3vGSyAT01Nvexn3nXXXXjllVe8jn388cd47733UF1dDaPRiOXLl2PFihVXtWYG8ET+12ztwf7CGuw116CxrQchYgF6nfDamyFiIX7+4zQG8USXwe8VIt8wgB8FGMATBQ6ny4Xicy14c7MZ9t6BtfFalRSrHp/lh5URBT5+rxD5JhADeL/VwBMRXSuhQICJCdpBg3cAaLba8OYnZuw5fh7N1p4RXh0REdH14bc2kkREw0WnkqLJOnAwm1QiQmVdO46daQQAxOjDkJmkQ2aSDuNiwiEWMYdBRETBhwE8EQW9pTcl4/0vSrwy8SFiIVYsTMXMCZGwNHaisKwZ5tJG7PiuCl8crESoVIQJ8VpkJrsDeo2SfeaJiCg4MIAnoqB34UHVy3WhidYrEK1XYOGMOHTbenGyogWFZU0oLGvCkdPu4XGxBkV/dl6L5Ghm54mIKHDxIdYh4kOsRIFtKHvF5XLhfEMnCsuaYC5twtnzbehzuhAqFWNigsaTnecUWBqN+L1C5JtAfIiVGXgiGrMEAgFiDArEGBT48cx4dPX04mRFszugL2vC4VPu7HxcpDs7n5WsQ5JJBZGQ2XkiIvIfBvBERP3kMjGmpRkmuyx8AAAgAElEQVQwLc0Al8uFqvoOd6lNaRO++LYS/zhwDnKpGBMTtchK1iEjSYfwsBB/L5uIiMYYBvBERIMQCASIi1QiLlKJxTkJ6Opx4ERFCwpL3bXz35XUAwDio5QXs/NGFYRCgZ9XTkREox0DeCIiH8hlEtyQZsANaQY4XS5U1XXA3P8g7D8OVOCzbyoQJrskO5+og4rZeSIiug4YwBMRDZFQIEB8lBLxUUrcdmMCOrod7tr5/uz8oeJ6CAAkGN3Z+cxkHRKjmJ0nIqLhwQCeiOgaKUIlmJ4eienpkXC6XKisa4e5P5jfvr8Cn+6vgCJUgowkLTKTdMhI1EIpZ3aeiIiuDgN4IqJhJBQIkBClQkKUCrfPSkRHtwNF5U0oLG1GUXkTvj1RBwGARJPKUzsfH6WEUMDsPBER+YYBPBHRdaQIlWDmhCjMnBAFp8uFc7UXs/Of7itH3r5yKOUSZCTqkJmsRUaiDopQib+XTUREAYwBPBHRCBEKBEg0qpBoVOGO2Ymwdtlxovxi7fyBE7UQCICkS7LzcZHMzhMRkTdOYh0iTmIlCmzBulecThfKa62eYL68xn0PKrnE8yDsxEQtwmTMztPwCNa9QjTSOImViIgGJRQKkGwKR7IpHHfOSYK1046i8iaYS5tw/Gwj9he5s/PJ0eHIStIhM0mHuEgFBMzOExGNOczADxEz8ESBbTTuFafThbIaq6d2/lyt+/7Cw0IuZucTNJAzO09DMBr3CtH1wAw8ERENmVAowLjocIyLDsfS3CS0ddhQVN4Mc2kTjp5uwL7CGggFAoyLViEz2Z2djzUwO09ENFoxAz9EzMATBbaxtlf6nE6UWS5m5yvrOgAAakV/dj5JhwkJWshlzNeQt7G2V4iuFjPwREQ0rERCIVJi1EiJUeMnNyWjtcOGwrImFJY24fCpBuw110DUn8HP6s/OR+vDmJ0nIgpizMAPETPwRIGNe+Wi3j7v7HxVvTs7r1FKL8nOaxAqZS5nLOJeIfINM/BERDRixCIhxseqMT5WjbvnJqOl/WJ2/lBxHfILLBAJBUiJCUdWcgQyk7QwRTA7T0QU6JiBHyJm4IkCG/eKb3r7nCg93+bJzlc3dAIAdKqL2fn0BA1kIczzjFbcK0S+YQaeiIgCglgkRGqcBqlxGiybNw7N1h4Ulrn7zh84WYd/HbdALBIgJUbtqZ036uTMzhMRBQBm4IeIGXiiwMa9cu16+5w4U9WKwrJmmMuaYGl0Z+cjwmUXs/PxGkhDRH5eKV0L7hUi3zADT0REAU8sEiI9QYv0BC1+evM4NLZ1o6jM3Xf+m6JafH3sPMQiAVJj1cjsr52P0jI7T0Q0UpiBHyJm4IkCG/fK9eXodeJ0dSsK+2vna5q6AAB6tTs7n5WsQ2qcBlIJs/OBjnuFyDeBmIFnAD9EDOCJAhv3yshqbO321M4XV7bA7nBCLBIiLU6NzGQdspJ0iNTK/b1MGgT3CpFvGMCPAgzgiQIb94r/OHr7cKqqFYWl7tr5umZ3dt6gCb2YnY9VI4TZ+YDAvULkm0AM4P1aA2+327F69Wrk5eXBarUiLS0NzzzzDHJycq54ntlsxpYtW2A2m3H69Gk4HA6cOnVqwPuqq6sxf/78QT9jzZo1yM3NHZb7ICIiQCIWISNRh4xEHe5FCupbulBY1ozCsibsLbBg15FqSMRCpMVp+jvbaGHQMDtPRDRUfg3gX3jhBezYsQMrVqxAfHw8tm7dikceeQTr16/H5MmTL3venj17sGnTJqSmpiI2NhZlZWVXvM7tt9+O2bNnex1LS0sblnsgIqLBGTRyzJ8qx/ypMbA7LmTnm2Aua8KGr5oAAJFaOTKTtJ7svETM7DwR0Q/xWwmN2WzGsmXL8OKLL+LBBx8EANhsNixZsgQGgwEbNmy47LmNjY1QKBSQyWT4wx/+gHXr1l0xA3/pNa4VS2iIAhv3SnCoa+6Cucz9IOypylY4ep0IkQiRHqdBZn/feb061N/LHNW4V4h8wxKaS3z55ZeQSCRYtmyZ55hUKsXdd9+N119/HfX19TAYDIOeGxERMeTrdXV1QSwWIyQk5KrXTEREwyNSK8ctWjlumRYLm6MPpypb+mvnG1FQ6s7OG3Vyd9/5ZB3Gx6ghEQv9vGoiosDgtwC+uLgYiYmJCAsL8zqelZUFl8uF4uLiywbwQ7V69Wq8/PLLEAgEyM7OxnPPPYcbbrhhWD6biIiujVQiQlZyBLKSI3CfKwV1Ld0w97ep3H20Gju+q4JUIkJ6/IXsvBYR4czOE9HY5bcAvqGhAZGRkQOO6/V6AEB9ff01X0MoFGL27Nm45ZZbYDAYcO7cOaxduxYPPfQQ/v73v2PatGnXfA0iIho+AoEAUVo5orRy3HpDLGz2PhRXtqCwrAmFpU04frYRAGCKCHPXzifpkBKrhljE7DwRjR1+C+B7enogkUgGHJdKpQDc9fDXymQyYe3atV7HFi1ahMWLF2PVqlXYuHHjkD/zSvVI15ter/TbtYmCCffK6BITrcYtOYlwuVyoru/AkZI6HCmux64j1fjnoSqESkXITtFjalokpqZFQq9hdt5X3CtEvgm0veK3AF4mk8HhcAw4fiFwvxDID7fIyEgsXrwYH3/8Mbq7uxEaOrT/0PMhVqLAxr0yusmEwKwJkZg1IRI99l4Un2txt6osbcS3RbUAgGh9mLvvfJIO42LCmZ2/DO4VIt/wIdZL6PX6QctkGhoaAGDY6t8HYzQa4XQ6YbVahxzAExFRYJCFiDE5RY/JKXq4XONhaepCYX/t/FffVeHLg5WQhYgwMUHr6WyjUV6f5BAR0UjyWwCflpaG9evXo7Oz0+tB1oKCAs/r10tVVRVEIhHCw8Ov2zWIiGjkCAQCREeEIToiDAtnxKHb5s7OX3gY9shpd3IoRq9AZrK7dj45mtl5IgpOfgvgFy5ciPfeew+bNm3y9Gi32+3YsmULpkyZ4nnA1WKxoLu7G8nJyUO+RnNzM7Rardexc+fO4R//+AemTZsGmUx2zfdBRESBJ1QqxpTxekwZr4fL5cL5hk73g7BlTdhxqApffFuJUGl/dj5Jhwxm54koiPgtgM/OzsbChQuxatUqNDQ0IC4uDlu3boXFYsHLL7/sed/zzz+PQ4cOeQ1qOn/+PPLy8gAAhYWFAIC33noLgDtzf/PNNwMA/vSnP6GqqgozZ86EwWBAZWWl58HV559/fkTuk4iI/EsgECDGoECMQYEfz4xHt60XJyuaPdn5w6fc2fk4g8JTapMcrYJIyOw8EQUmvwXwAPDqq6/ijTfeQF5eHtra2pCamop33nkHU6dOveJ51dXVWL16tdexCz/fddddngB+1qxZ2LhxI/7v//4P7e3tUKlUmDVrFp588kmkpKRcn5siIqKAFioVY2qqAVNTDXC5XKiq7+jPzjfji28r8Y8D5yCXijEx0Z2dz0zSIlzB7DwRBQ6By+Ua+ZYqQYxdaIgCG/cKXYuuHgdOVlysnW/rtAMA4iOVyEx2d7ZJMqkgFAr8vNJrx71C5Bt2oSEiIgpgcpkE09IMmJbmzs5X1nV4auf/caACn31TgTDZpdl5HVRhIf5eNhGNMQzgiYiIBiEQCBAfpUR8lBJLbkxAZ48DJ8qb3a0qy5txqNjdCjkhSoms/tr5ROPoyM4TUWBjAE9EROSDMJkE09MjMT09Ek6XC5V17SgsbYK5rAnbv6nAp/sroAiVICPR3Xc+I1ELpZzZeSIafgzgiYiIhkgoECAhSoWEKBVum5WIjm53dt5c2oSi8iZ8e7IOAgAJRpUnO59gVEIoYHaeiK4dA3giIqJrpAiVYMaESMyY4M7On6u9mJ3/dF858vaVQym/NDuvgyJU4u9lE1GQYgBPREQ0jIQCARKNKiQaVbh9diLau+zu7Hx/q8oDJ+ogEABJRpWn73x8FLPzROQ7BvBERETXkVIegpkTozBzYhScThfKa63uB2HLmpC3txzb9pZDJZcgI0mHrGQdJiZqESZjdp6ILo8BPBER0QgRCgVINoUj2RSOO+ckwdplx4kyd3a+4GwjvimqhUAAJEeHIzPJ3Xc+NlLB7DwReeEgpyHiICeiwMa9QsHK6XShrMbqqZ0/V+v+fRweFoKMJC2ykiMwMUED+TBl57lXiHzDQU5EREQ0KKFQgHHR4RgXHY67cpPQ1mlHUf8QqeNnGrG/sBZCgQDjoi/WzscaFBAwO0805jADP0TMwBMFNu4VGo36nE6UWawoLGuCubQJlXUdAAC1IsRdO5+kw4QELeQy3/Ny3CtEvmEGnoiIiIZMJBQiJUaNlBg1luYmo7XDhsL+rjZHTjVgn7kGov4M/oXsfIw+jNl5olGKGfghYgaeKLBxr9BY0+d0ovT8xex8Vb07O69RSpGZpEVmUgQmJGgQKnXn7A6cqMWWPaVottqgVUmx9KZk5EyM8uctEAW0QMzAM4AfIgbwRIGNe4XGupb2C9n5JpysaEa3rQ8ioQApMeFQhYXg2OlGOPqcnveHiIX4+Y/TGMQTXUYgBvAsoSEiIhpFNEopcrNNyM02obfPidLzbe4hUqVNKKlsHfB+e68Tn/yrlAE8URBhBn6ImIEnCmzcK0SX9/Aruy/7WlykAklGFRKMKiQZVTBFhEEoZA09ETPwRERE5Dc6lRRNVtuA46EhIihCJThYXI9/HbcAAKQSEeIjFUg0qZBodP+KCJfxwViiAMAAnoiIaIxYelMy3v+iBPZe7xr4B36UipyJUXC6XKhv6Ua5xYryGvevXUfOo7evCgCgCJX0B/NKJJnc2XqVPMRft0M0ZjGAJyIiGiMu1LlfrguNUCBAlFaOKK0cORnuY719Tpxv6ERZzcWgvqisCReKSSPCZZ4MfaJRifgoJWQhDC+IrifWwA8Ra+CJAhv3CpFvrmWv9Nh7ca62HeU17SirsaKixorGth4AgEAAmCLCPEF9klGFaH0YxCLhcC6faMSwBp6IiIiCnixEjNQ4DVLjNJ5j1k47KmqtKLNYUV7TjuNnGrHPXAMAkIiFiDMo3EF9f029QRMKIevpia4KA3giIiK6ZqqwEGQlRyArOQIA4HK50NjW4ym7KbdYkW+2YOeRagCAXCpGglHpydInGFXQKKX+vAWioMEAnoiIiIadQCCAXh0KvToU09MjAbinxtY0dnnV03/xbSWc/dW8GqXUU0ufaFQhIUoFuYyhCtH3cVcQERHRiBAJhYgxKBBjUCA32wQAsDv6UFnXcTFTX2PF0dMNnnOitHJ3lt6kQoJRiTiDAhKxyF+3QBQQGMATERGR34RIRBgXE45xMeGeYx3dDlTUumvpyy1WnKxoxoETtQAAkVCA2Av19P3ZeqOOQ6dobGEAT0RERAFFESpBRqIOGYk6AO56+pZ2W3+Gvh3lNVYcOFGLr4+dBwBIQ0RIjFJ6psgmGlXQqqQcOkWjFgN4IiIiCmgCgQBalQxalQxTUw0AAKfLhbrmrv6uN+7AfufhKvT2uevpVXKJV9ebRKMKilCJP2+DaNgwgCciIqKgIxQIYNSFwagLw6xMIwDA0etEdUOHp+tNWY0V5tKLQ6cM6lAkGJWerjfxUUpIJaynp+DDAJ6IiIhGBYlY6Mm2Y4r7WLetFxW17aiocQf0Z8+34VBxPQD3HwKi9WGerjeJ/UOnREIOnaLA5tcA3m63Y/Xq1cjLy4PVakVaWhqeeeYZ5OTkXPE8s9mMLVu2wGw24/Tp03A4HDh16tSg73U6nVi7di0+/PBDNDQ0ICEhAY899hgWLVp0PW6JiIiIAkioVIz0eA3S4y8OnWrrsHlNkT1yqgH5Be6hUyFiIeKiLmTp3f+rV4eynp4Cil8D+BdeeAE7duzAihUrEB8fj61bt+KRRx7B+vXrMXny5Muet2fPHmzatAmpqamIjY1FWVnZZd/7+uuv45133sE999yDjIwM7Nq1C8888wyEQiEWLlx4PW6LiIiIAli4QopJKVJMSrk4dKq+tbu/9KYd5bVWfH3sPBzfOQEAYTLxJV1v3HX14WEh/rwFGuMELpfL9cNvG35msxnLli3Diy++iAcffBAAYLPZsGTJEhgMBmzYsOGy5zY2NkKhUEAmk+EPf/gD1q1bN2gGvq6uDvPnz8e9996Ll156CYB7kz7wwAOoqanBzp07IRziX5M1NXXA6Rz5f2V6vRINDe0jfl2iYMO9QuQb7pUr6+1zwtLY6elNX2Zpx/nGDlyImnQqqVfXm/goJUKlrEwejfyxV4RCAXQ6xWVf99vvtC+//BISiQTLli3zHJNKpbj77rvx+uuvo76+HgaDYdBzIyIifLrGzp074XA4cN9993mOCQQC3Hvvvfj1r38Ns9mMSZMmXduNEBER0agjFgkRF6lEXKQSN02KBgDY7H04V3exnr68v/wGAAQAjBHe9fSxBgXEItbT0/DzWwBfXFyMxMREhIWFeR3PysqCy+VCcXHxZQP4oVxDoVAgMTFxwDUA4OTJkwzgiYiIyCfSEBHGx6oxPlbtOdbeZUdFrXvgVHmNFYWlTdhf6B46JRYJEGtw19EnmtyBfaRWDiHr6eka+S2Ab2hoQGRk5IDjer0eAFBfXz8s1xgsWz+c1yAiIqKxSykPQWaSDplJF4dONVl7UNH/kGy5xYp9RTXYdbQaABAqFSEhSuU1SVaj5NApGhq/BfA9PT2QSAYOVJBKpQDc9fDDcY2QkIEPmVzLNa5Uj3S96fVKv12bKJhwrxD5hnvl+jAYVEgfd7GKoM/pQnV9O85UtuJ0VQvOVLZgx3eVnqFTWpUUKbEapMSpMT5Wg5RYNRRyPiQbSAJtr/gtgJfJZHA4HAOOXwiqLwTZ13oNu90+rNfgQ6xEgY17hcg33CsjSy4SIDtRg+xEdztLR28fKus7+ktv2lFeY8XBE7We90dqQt1TZKPcXW/iDAqEcOiUX/Ah1kvo9fpBS1gaGtwPg1xr/fuFaxw+fPi6XoOIiIhoqCRiEZJN4Ug2hXuOdfU43PX0Ne6g/lRlK749UQcAEAndQ6eSLmlnaYoIg1DI0puxyG8BfFpaGtavX4/Ozk6vB1kLCgo8r1+r9PR0bNq0CeXl5V4Psl64Rnp6+jVfg4iIiGg4yGUSTEjQYkKC1nOspd3m1fXmYHE9/nXcAgCQSkSIj1J6db6JCJexnn4M8FsAv3DhQrz33nvYtGmTpw+83W7Hli1bMGXKFM8DrhaLBd3d3UhOTh7yNebPn4+XX34ZH3zwgVcf+I0bN8JkMiE7O3vY7oeIiIhouGmUUmiUekwe727A4XS5UN9yYeiUO6jfdeQ8evuqAACKUAmSTCokRCnd/2tUQcV6+lHHbwF8dnY2Fi5ciFWrVqGhoQFxcXHYunUrLBYLXn75Zc/7nn/+eRw6dMhrUNP58+eRl5cHACgsLAQAvPXWWwDcmfubb74ZABAVFYUVK1bgvffeg81mQ2ZmJnbu3InDhw/j9ddfH/IQJyIiIiJ/EgoEiNLKEaWVI2diFAD30KnzDZ2eLP2FdpYXntiLCJd5db2Jj1JCFsKhU8HMr//vvfrqq3jjjTeQl5eHtrY2pKam4p133sHUqVOveF51dTVWr17tdezCz3fddZcngAeA5557DuHh4fjoo4+wZcsWJCYm4rXXXsOiRYuG/4aIiIiIRphYJER8lDswnzfZPXSqx96Lc7XtKL/QzrLGiu9K3M8eCgRAdESY1yTZaH0Yh04FEYHL5Rr5lipBjF1oiAIb9wqRb7hXxh5rpx0VtVaUXdL5pqPb3RFQIhYiLlLh6XqTZFTBoAllPT3YhYaIiIiI/EQVFoKs5AhkJbuHXLpcLjS29XjKbsotVuSbLdh5xD10Si4Vux+QvaSdpVpx7W2+6doxgCciIiIagwQCAfTqUOjVoZie7m4e0ud0oqaxC2U1Vk/3m88PVMLZX7ChUUo9tfRJRhXio1SQyxhOjjT+GyciIiIiAIBIKESMQYEYgwK52SYAgN1x6dAp96+jpxs85xh1ciREqZBkctfTxxoUkIhZT389MYAnIiIiossKkYgwLjoc46IvDp3q6Hagora/lt5ixcmKZhzonyQrEgoQa1B4ld4YtXIOnRpGDOCJiIiIaEgUoRJkJOqQkagD4K6nb2m3eabIltdYcaCoFl8fPQ8AkIWIkBClvKSdpQpalZQPyV4lBvBEREREdE0EAgG0Khm0KhmmphoAuIdO1TV39Xe9cQf2Xx2uQm+fu55eFRaCxCilp+tNglEFRajEn7cRNBjAExEREdGwEwoEMOrCYNSFYVamEQDg6HWiuqHj4iTZ2naYLxk6ZVCH9pfeuAP7uEglpBKR/24iQDGAJyIiIqIRIRELPSU0mOI+1m3rRUVtu6frzZnqVhw8WQfA/YeAaH0YEo3uh2QTopSI1odBJBzbD8kygCciIiIivwmVipEer0F6vMZzrK3D5qmlL6+x4sipeuQXWAAAIWIh4qKUnimyiUYl9OqxNXSKATwRERERBZRwhRSTUqSYlHJx6FRDazfKaqwot7SjvNaKr4+dx47vqgAAYTKxV9ebRKMK4WEh/ryF64oBPBEREREFNIFAAINGDoNGjpkTogAAvX1OWBo7PVn6Mks7PiuvQP/MKehU/UOn+gP7+CglQqWjI/QdHXdBRERERGOKWCREXKQScZFK3DQpGgBgs/fhXN3FevryGisOn3IPnRIAMEWEIaF/imxC/9ApsWjwevoDJ2qxZU8pmq02aFVSLL0pGTkTo0bq9q6IATwRERERjQrSEBHGx6oxPlbtOdbeZUdFbbtnkmxhaRP2F7qHTolFAsRFKvtLb9x96iO1chw8WYf3vyiBvdcJAGiy2vD+FyUAEBBBPAN4IiIiIhq1lPIQZCbpkJl0cehUs9U9dKqsxoqKGiv2FdVg19FqAECoVARHrwu9fU6vz7H3OrFlTykDeCIiIiKikSQQCKALl0EXLsO0tP6hU04Xapo6PZ1vvj52ftBzm6y2kVzqZTGAJyIiIqIxTSgUIFqvQLRegdlZRphLGwcN1nUqqR9WN9DY7oJPRERERPQ9S29KRojYO0wOEQux9KZkP63IGzPwRERERESXuFDnzi40RERERERBImdiFHImRkGvV6Khod3fy/HCEhoiIiIioiDCAJ6IiIiIKIgwgCciIiIiCiIM4ImIiIiIgggDeCIiIiKiIMIAnoiIiIgoiDCAJyIiIiIKIgzgiYiIiIiCCAN4IiIiIqIgwkmsQyQUCsbktYmCCfcKkW+4V4h8M9J75YeuJ3C5XK4RWgsREREREV0jltAQEREREQURBvBEREREREGEATwRERERURBhAE9EREREFEQYwBMRERERBREG8EREREREQYQBPBERERFREGEAT0REREQURBjAExEREREFEQbwRERERERBROzvBdDg6uvrsW7dOhQUFKCoqAhdXV1Yt24dZsyY4e+lEQUUs9mMrVu34uDBg7BYLFCr1Zg8eTKefvppxMfH+3t5RAGjsLAQf/vb33Dy5Ek0NTVBqVQiLS0NTzzxBKZMmeLv5REFrDVr1mDVqlVIS0tDXl6ev5cDgAF8wCovL8eaNWsQHx+P1NRUHDt2zN9LIgpI7777Lo4ePYqFCxciNTUVDQ0N2LBhA+68805s3rwZycnJ/l4iUUCoqqpCX18fli1bBr1ej/b2dmzfvh0PPPAA1qxZg1mzZvl7iUQBp6GhAX/9618hl8v9vRQvApfL5fL3Imigjo4OOBwOaDQa7Ny5E0888QQz8ESDOHr0KDIyMhASEuI5VlFRgdtuuw2LFy/GK6+84sfVEQW27u5uLFiwABkZGXj77bf9vRyigPPCCy/AYrHA5XLBarUGTAaeNfABSqFQQKPR+HsZRAFvypQpXsE7ACQkJCAlJQWlpaV+WhVRcAgNDYVWq4XVavX3UogCjtlsxqeffooXX3zR30sZgAE8EY06LpcLjY2N/EMw0SA6OjrQ3NyMsrIy/PnPf8bp06eRk5Pj72URBRSXy4Xf/e53uPPOO5Genu7v5QzAGngiGnU+/fRT1NXV4ZlnnvH3UogCzm9+8xv885//BABIJBL87Gc/wy9/+Us/r4oosGzbtg1nz57FX/7yF38vZVAM4IloVCktLcVvf/tbTJ06FXfccYe/l0MUcJ544gncc889qK2tRV5eHux2OxwOx4BSNKKxqqOjA6+99hr+7d/+DQaDwd/LGRRLaIho1GhoaMCjjz6K8PBwrF69GkIh/xNH9H2pqamYNWsWfvKTn2Dt2rU4ceJEQNb4EvnLX//6V0gkEjz00EP+Xspl8duNiEaF9vZ2PPLII2hvb8e7774LvV7v7yURBTyJRIL58+djx44d6Onp8fdyiPyuvr4e77//Pu677z40Njaiuroa1dXVsNlscDgcqK6uRltbm7+XyRIaIgp+NpsNv/zlL1FRUYG///3vSEpK8veSiIJGT08PXC4XOjs7IZPJ/L0cIr9qamqCw+HAqlWrsGrVqgGvz58/H4888giee+45P6zuIgbwRBTU+vr68PTTT+P48eN46623MGnSJH8viSggNTc3Q6vVeh3r6OjAP//5TxiNRuh0Oj+tjChwxMTEDPrg6htvvIGuri785je/QUJCwsgv7HsYwAewt956CwA8vazz8vJw5MgRqFQqPPDAA/5cGlHAeOWVV7B7927MmzcPra2tXkM2wsLCsGDBAj+ujihwPP3005BKpZg8eTL0ej1qamqwZcsW1NbW4s9//rO/l0cUEJRK5aDfG++//z5EIlHAfKdwEmsAS01NHfR4dHQ0du/ePcKrIQpMy5cvx6FDhwZ9jXuF6KLNmzcjLy8PZ8+ehdVqhVKpxKRJk/Dwww9j+vTp/l4eUUBbvnx5QE1iZQBPRERERBRE2IWGiIiIiCiIMIAnIiIiIgoiDOCJiIiIiIIIA3giIiIioiDCAJ6IiIiIKIgwgCciIiIiCiIM4ImIiIiIgggDeCIiChdYDuQAAATOSURBVHjLly/HzTff7O9lEBEFBLG/F0BERP5x8OBBrFix4rKvi0QinDx5cgRXREREvmAAT0Q0xi1ZsgS5ubkDjguF/EtaIqJAxACeiGiMmzBhAu644w5/L4OIiHzE9AoREV1RdXU1UlNT8eabb+Kzzz7DbbfdhszMTMydOxdvvvkment7B5xTUlKCJ554AjNmzEBmZiYWLVqENWvWoK+vb8B7Gxoa8Pvf/x7z589HRkYGcnJy8NBDD2H//v0D3ltXV4dnn30WN9xwA7Kzs/GLX/wC5eXl1+W+iYgCFTPwRERjXHd3N5qbmwccDwkJgUKh8Py8e/duVFVV4f7770dERAR2796N//mf/4HFYsHLL7/seV9hYSGWL18OsVjsee/XX3+NVatWoaSkBK+99prnvdXV1bj33nvR1NSEO+64AxkZGeju7kZBQQG++eYbzJo1y/Perq4uPPDAA8jOzsYzzzyD6upqrFu3Do8//jg+++wziESi6/RviIgosDCAJyIa49588028+eabA47PnTsXb7/9tufnkpISbN68GRMnTgQAPPDAA3jyySexZcsW3HPPPZg0aRIA4A9/+APsdjs2btyItLQ0z3uffvppfPbZZ7j77ruRk5MDAPiv//ov1NfX491338WcOXO8ru90Or1+bmlpwS9+8Qs88sgjnmNarRZ/+tOf8M033ww4n4hotGIAT0Q0xt1zzz1YuHDhgONardbr5xtvvNETvAOAQCDAypUrsXPnTnz11VeYNGkSmpqacOzYMdxyyy2e4P3Cex977DF8+eWX+Oqrr5CTk4PW1lbs3bsXc+bMGTT4/v5DtEKhcEDXnJkzZwIAzp07xwCeiMYMBvD/fzv3z9I6GIZh/KqLmwilLhpF61AUh64ODv7ZBLsJFhEEl6KbTuKXEBwUd10cCp0EKRTJ7mAFUQvqB3ASXMwZ5BSL4unikddevy1PniRvtpvkSSSpww0NDTE5OfnPvmw2+6E2OjoKwMPDA/A2EvO+/t7IyAhdXV3N3vv7e5IkYWxsrK119vX10d3d3VLr7e0F4Onpqa1zSNJv4EeskqQgfDXjniTJf1yJJP0sA7wkqS23t7cfajc3NwBEUQTAwMBAS/29u7s7Xl9fm72Dg4OkUimurq6+a8mS9CsZ4CVJbYnjmMvLy+Z2kiQcHh4CMDs7C0A6nSafz1OtVrm+vm7pPTg4AGBubg54G3+ZmpqiVqsRx/GH6/lUXZI+5wy8JHW4er1OuVz+dN/fYA6Qy+VYWVmhWCySyWQ4OzsjjmMWFhbI5/PNvu3tbZaXlykWiywtLZHJZKhWq5yfnzM/P9/8Aw3Azs4O9XqdtbU1CoUC4+PjvLy8cHFxQX9/P1tbW99345IUKAO8JHW4SqVCpVL5dN/p6Wlz9nx6eprh4WH29/dpNBqk02lKpRKlUqnlmImJCY6Pj9nd3eXo6Ijn52eiKGJzc5PV1dWW3iiKODk5YW9vj1qtRrlcpqenh1wux+Li4vfcsCQFLpX4jlKS9IXHx0dmZmZYX19nY2Pjp5cjSR3PGXhJkiQpIAZ4SZIkKSAGeEmSJCkgzsBLkiRJAfEJvCRJkhQQA7wkSZIUEAO8JEmSFBADvCRJkhQQA7wkSZIUEAO8JEmSFJA/mkfTA8tJV50AAAAASUVORK5CYII=\n",
"text/plain": [
""
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"import matplotlib.pyplot as plt\n",
"#% matplotlib inline\n",
"\n",
"import seaborn as sns\n",
"\n",
"# Use plot styling from seaborn.\n",
"sns.set(style='darkgrid')\n",
"\n",
"# Increase the plot size and font size.\n",
"sns.set(font_scale=1.5)\n",
"plt.rcParams[\"figure.figsize\"] = (12,6)\n",
"\n",
"# Plot the learning curve.\n",
"plt.plot(df_stats['Training Loss'], 'b-o', label=\"Training\")\n",
"plt.plot(df_stats['Valid. Loss'], 'g-o', label=\"Validation\")\n",
"\n",
"# Label the plot.\n",
"plt.title(\"Training & Validation Loss\")\n",
"plt.xlabel(\"Epoch\")\n",
"plt.ylabel(\"Loss\")\n",
"plt.legend()\n",
"plt.xticks([1, 2, 3, 4])\n",
"\n",
"plt.show()"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "mkyubuJSOzg3"
},
"source": [
"# 5. Performance On Test Set"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "DosV94BYIYxg"
},
"source": [
"Now we'll load the holdout dataset and prepare inputs just as we did with the training set. Then we'll evaluate predictions using [Matthew's correlation coefficient](https://scikit-learn.org/stable/modules/generated/sklearn.metrics.matthews_corrcoef.html) because this is the metric used by the wider NLP community to evaluate performance on CoLA. With this metric, +1 is the best score, and -1 is the worst score. This way, we can see how well we perform against the state of the art models for this specific task."
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "Tg42jJqqM68F"
},
"source": [
"### 5.1. Data Preparation\n"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "xWe0_JW21MyV"
},
"source": [
"\n",
"We'll need to apply all of the same steps that we did for the training data to prepare our test data set."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"colab": {
"background_save": true
},
"id": "mAN0LZBOOPVh",
"outputId": "6322aba3-c93a-40e2-c215-588cc0248635"
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Number of test sentences: 800\n",
"\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"/usr/local/lib/python3.8/dist-packages/transformers/tokenization_utils_base.py:2339: FutureWarning: The `pad_to_max_length` argument is deprecated and will be removed in a future version, use `padding=True` or `padding='longest'` to pad to the longest sequence in the batch, or use `padding='max_length'` to pad to a max length. In this case, you can give a specific length with `max_length` (e.g. `max_length=45`) or leave max_length to None to pad to the maximal input size of the model (e.g. 512 for Bert).\n",
" warnings.warn(\n"
]
}
],
"source": [
"import pandas as pd\n",
"\n",
"# Load the dataset into a pandas dataframe.\n",
"df = pd.read_csv(\"./7_irma.tsv\", delimiter='\\t', header=None, names=['sentence', 'label'])\n",
"\n",
"# Report the number of sentences.\n",
"print('Number of test sentences: {:,}\\n'.format(df.shape[0]))\n",
"\n",
"# Create sentence and label lists\n",
"sentences = df.sentence.values\n",
"labels = df.label.values\n",
"\n",
"# Tokenize all of the sentences and map the tokens to thier word IDs.\n",
"input_ids = []\n",
"attention_masks = []\n",
"\n",
"# For every sentence...\n",
"for sent in sentences:\n",
" # `encode_plus` will:\n",
" # (1) Tokenize the sentence.\n",
" # (2) Prepend the `[CLS]` token to the start.\n",
" # (3) Append the `[SEP]` token to the end.\n",
" # (4) Map tokens to their IDs.\n",
" # (5) Pad or truncate the sentence to `max_length`\n",
" # (6) Create attention masks for [PAD] tokens.\n",
" encoded_dict = tokenizer.encode_plus(\n",
" sent, # Sentence to encode.\n",
" add_special_tokens = True, # Add '[CLS]' and '[SEP]'\n",
" max_length = 512, # Pad & truncate all sentences.\n",
" pad_to_max_length = True,\n",
" return_attention_mask = True, # Construct attn. masks.\n",
" return_tensors = 'pt', # Return pytorch tensors.\n",
" )\n",
"\n",
" # Add the encoded sentence to the list.\n",
" input_ids.append(encoded_dict['input_ids'])\n",
"\n",
" # And its attention mask (simply differentiates padding from non-padding).\n",
" attention_masks.append(encoded_dict['attention_mask'])\n",
"\n",
"# Convert the lists into tensors.\n",
"input_ids = torch.cat(input_ids, dim=0)\n",
"attention_masks = torch.cat(attention_masks, dim=0)\n",
"labels = torch.tensor(labels)\n",
"\n",
"# Set the batch size.\n",
"batch_size = 16\n",
"\n",
"# Create the DataLoader.\n",
"prediction_data = TensorDataset(input_ids, attention_masks, labels)\n",
"prediction_sampler = SequentialSampler(prediction_data)\n",
"prediction_dataloader = DataLoader(prediction_data, sampler=prediction_sampler, batch_size=batch_size)"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "16lctEOyNFik"
},
"source": [
"## 5.2. Evaluate on Test Set\n"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "rhR99IISNMg9"
},
"source": [
"\n",
"With the test set prepared, we can apply our fine-tuned model to generate predictions on the test set."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "Hba10sXR7Xi6"
},
"outputs": [],
"source": [
"# Prediction on test set\n",
"\n",
"print('Predicting labels for {:,} test sentences...'.format(len(input_ids)))\n",
"\n",
"# Put model in evaluation mode\n",
"model.eval()\n",
"\n",
"# Tracking variables\n",
"predictions , true_labels = [], []\n",
"\n",
"# Predict\n",
"for batch in prediction_dataloader:\n",
" # Add batch to GPU\n",
" batch = tuple(t.to(device) for t in batch)\n",
"\n",
" # Unpack the inputs from our dataloader\n",
" b_input_ids, b_input_mask, b_labels = batch\n",
"\n",
" # Telling the model not to compute or store gradients, saving memory and\n",
" # speeding up prediction\n",
" with torch.no_grad():\n",
" # Forward pass, calculate logit predictions.\n",
" result = model(b_input_ids,\n",
" token_type_ids=None,\n",
" attention_mask=b_input_mask,\n",
" return_dict=True)\n",
"\n",
" logits = result.logits\n",
"\n",
" # Move logits and labels to CPU\n",
" logits = logits.detach().cpu().numpy()\n",
" label_ids = b_labels.to('cpu').numpy()\n",
"\n",
" # Store predictions and true labels\n",
" predictions.append(logits)\n",
" true_labels.append(label_ids)\n",
"\n",
"print(' DONE.')"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "-5jscIM8R4Gv"
},
"source": [
"Accuracy on the CoLA benchmark is measured using the \"[Matthews correlation coefficient](https://scikit-learn.org/stable/modules/generated/sklearn.metrics.matthews_corrcoef.html)\" (MCC).\n",
"\n",
"We use MCC here because the classes are imbalanced:\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "hWcy0X1hirdx"
},
"outputs": [],
"source": [
"print('Positive samples: %d of %d (%.2f%%)' % (df.label.sum(), len(df.label), (df.label.sum() / len(df.label) * 100.0)))"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "cRaZQ4XC7kLs"
},
"outputs": [],
"source": [
"from sklearn.metrics import matthews_corrcoef\n",
"\n",
"matthews_set = []\n",
"\n",
"# Evaluate each test batch using Matthew's correlation coefficient\n",
"print('Calculating Matthews Corr. Coef. for each batch...')\n",
"\n",
"# For each input batch...\n",
"for i in range(len(true_labels)):\n",
"\n",
" # The predictions for this batch are a 2-column ndarray (one column for \"0\"\n",
" # and one column for \"1\"). Pick the label with the highest value and turn this\n",
" # in to a list of 0s and 1s.\n",
" pred_labels_i = np.argmax(predictions[i], axis=1).flatten()\n",
"\n",
" # Calculate and store the coef for this batch.\n",
" matthews = matthews_corrcoef(true_labels[i], pred_labels_i)\n",
" matthews_set.append(matthews)"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "IUM0UA1qJaVB"
},
"source": [
"The final score will be based on the entire test set, but let's take a look at the scores on the individual batches to get a sense of the variability in the metric between batches.\n",
"\n",
"Each batch has 32 sentences in it, except the last batch which has only (516 % 32) = 4 test sentences in it.\n"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "1YrjAPX2V-l4"
},
"source": [
"Now we'll combine the results for all of the batches and calculate our final MCC score."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "oCYZa1lQ8Jn8"
},
"outputs": [],
"source": [
"# Combine the results across all batches.\n",
"flat_predictions = np.concatenate(predictions, axis=0)\n",
"\n",
"# For each sample, pick the label (0 or 1) with the higher score.\n",
"flat_predictions = np.argmax(flat_predictions, axis=1).flatten()\n",
"\n",
"# Combine the correct labels for each batch into a single list.\n",
"flat_true_labels = np.concatenate(true_labels, axis=0)\n",
"\n",
"# Calculate the MCC\n",
"mcc = matthews_corrcoef(flat_true_labels, flat_predictions)\n",
"\n",
"print('Total MCC: %.3f' % mcc)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "3VneGDFV3D7D"
},
"outputs": [],
"source": [
"\n",
"from sklearn import metrics\n",
"confusion_matrix= metrics.confusion_matrix(flat_true_labels, flat_predictions)\n",
"print(confusion_matrix)\n",
"\n",
"\n",
"# TN FP\n",
"# FN TP"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "EpfSqnZE3Jrg"
},
"outputs": [],
"source": [
"from sklearn.metrics import precision_score, recall_score, f1_score, accuracy_score\n",
"print('Acuuracy Score %.3f:'% accuracy_score(flat_true_labels, flat_predictions))\n",
"print('Precision Score %.3f:'% precision_score(flat_true_labels, flat_predictions))\n",
"print('Recall Score %.3f:'% recall_score(flat_true_labels, flat_predictions))\n",
"print('F1 Score %.3f:'% f1_score(flat_true_labels, flat_predictions))\n"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "jXx0jPc4HUfZ"
},
"source": [
"Cool! In about half an hour and without doing any hyperparameter tuning (adjusting the learning rate, epochs, batch size, ADAM properties, etc.) we are able to get a good score.\n",
"\n",
"> *Note: To maximize the score, we should remove the \"validation set\" (which we used to help determine how many epochs to train for) and train on the entire training set.*\n",
"\n",
"The library documents the expected accuracy for this benchmark [here](https://huggingface.co/transformers/examples.html#glue) as `49.23`.\n",
"\n",
"You can also look at the official leaderboard [here](https://gluebenchmark.com/leaderboard/submission/zlssuBTm5XRs0aSKbFYGVIVdvbj1/-LhijX9VVmvJcvzKymxy).\n",
"\n",
"Note that (due to the small dataset size?) the accuracy can vary significantly between runs.\n"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "GfjYoa6WmkN6"
},
"source": [
"# Conclusion"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "xlQG7qgkmf4n"
},
"source": [
"This post demonstrates that with a pre-trained BERT model you can quickly and effectively create a high quality model with minimal effort and training time using the pytorch interface, regardless of the specific NLP task you are interested in."
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "YUmsUOIv8EUO"
},
"source": [
"# Appendix\n"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "q2079Qyn8Mt8"
},
"source": [
"## A1. Saving & Loading Fine-Tuned Model\n",
"\n",
"This first cell (taken from `run_glue.py` [here](https://github.com/huggingface/transformers/blob/35ff345fc9df9e777b27903f11fa213e4052595b/examples/run_glue.py#L495)) writes the model and tokenizer out to disk."
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "SZqpiHEnGqYR"
},
"source": [
"**Version 4** - *Feb 2nd, 2020* - (current)\n",
"* Updated all calls to `model` (fine-tuning and evaluation) to use the [`SequenceClassifierOutput`](https://huggingface.co/transformers/main_classes/output.html#transformers.modeling_outputs.SequenceClassifierOutput) class.\n",
"* Moved illustration images to Google Drive--Colab appears to no longer support images at external URLs.\n",
"\n",
"**Version 3** - *Mar 18th, 2020*\n",
"* Simplified the tokenization and input formatting (for both training and test) by leveraging the `tokenizer.encode_plus` function.\n",
"`encode_plus` handles padding *and* creates the attention masks for us.\n",
"* Improved explanation of attention masks.\n",
"* Switched to using `torch.utils.data.random_split` for creating the training-validation split.\n",
"* Added a summary table of the training statistics (validation loss, time per epoch, etc.).\n",
"* Added validation loss to the learning curve plot, so we can see if we're overfitting.\n",
" * Thank you to [Stas Bekman](https://ca.linkedin.com/in/stasbekman) for contributing this!\n",
"* Displayed the per-batch MCC as a bar plot.\n",
"\n",
"**Version 2** - *Dec 20th, 2019* - [link](https://colab.research.google.com/drive/1Y4o3jh3ZH70tl6mCd76vz_IxX23biCPP)\n",
"* huggingface renamed their library to `transformers`.\n",
"* Updated the notebook to use the `transformers` library.\n",
"\n",
"**Version 1** - *July 22nd, 2019*\n",
"* Initial version."
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "FL_NnDGxRpEI"
},
"source": [
"## Further Work\n",
"\n",
"* It might make more sense to use the MCC score for “validation accuracy”, but I’ve left it out so as not to have to explain it earlier in the Notebook.\n",
"* Seeding -- I’m not convinced that setting the seed values at the beginning of the training loop is actually creating reproducible results…\n",
"* The MCC score seems to vary substantially across different runs. It would be interesting to run this example a number of times and show the variance.\n"
]
}
],
"metadata": {
"accelerator": "GPU",
"colab": {
"provenance": []
},
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.7.6"
}
},
"nbformat": 4,
"nbformat_minor": 0
}