{ "cells": [ { "cell_type": "markdown", "metadata": { "id": "c3hpiPPEqmf6" }, "source": [ "##### Copyright 2024 Google LLC." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "cellView": "form", "id": "bVm-2hW9z9HR" }, "outputs": [], "source": [ "# @title Licensed under the Apache License, Version 2.0 (the \"License\");\n", "# you may not use this file except in compliance with the License.\n", "# You may obtain a copy of the License at\n", "#\n", "# https://www.apache.org/licenses/LICENSE-2.0\n", "#\n", "# Unless required by applicable law or agreed to in writing, software\n", "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", "# See the License for the specific language governing permissions and\n", "# limitations under the License." ] }, { "cell_type": "markdown", "metadata": { "id": "u71STQRgnQ3a" }, "source": [ "# Fine-tune PaliGemma for Image Description with Custom Dataset" ] }, { "cell_type": "markdown", "metadata": { "id": "wR53lePHuiP-" }, "source": [ "This notebook guides you through the process of fine-tuning [PaliGemma](https://ai.google.dev/gemma/docs/paligemma), a powerful vision-language model, for bird description using [JAX](https://jax.readthedocs.io/en/latest/installation.html). We will leverage a curated subset of a bird species dataset and enrich it with descriptive text for each bird. The resulting dataset, comprising 3,692 image-description pairs, will be used to fine-tune PaliGemma, enabling it to generate accurate and detailed descriptions of bird images.\n", "\n", "\n", " \n", "
\n", " Run in Google Colab\n", "
" ] }, { "cell_type": "markdown", "metadata": { "id": "qRi1rF4MWlQi" }, "source": [ "### Get access to PaliGemma\n", "\n", "Before using PaliGemma for the first time, you must request access to the model through Kaggle by setup instructions at [Gemma setup](https://ai.google.dev/gemma/docs/setup), or completing the following steps:\n", "\n", "1. Log in to [Kaggle](https://www.kaggle.com), or create a new Kaggle account if you don't already have one.\n", "1. Go to the [Gemma model card](https://www.kaggle.com/models/google/paligemma/), as PaliGemma is a Gemma variant and click **Request Access**.\n", "1. Complete the consent form and accept the terms and conditions.\n", "\n", "To generate a Kaggle API key, open your [**Settings** page in Kaggle](https://www.kaggle.com/settings) and click **Create New Token**. This triggers the download of a `kaggle.json` file containing your API credentials.\n", "\n", "Then, in Colab, select **Secrets** (πŸ”‘) in the left pane and add your Kaggle username and Kaggle API key. Store your username under the name `KAGGLE_USERNAME` and your API key under the name `KAGGLE_KEY`." ] }, { "cell_type": "markdown", "metadata": { "id": "KHskrDmKpNGS" }, "source": [ "### Select the runtime\n", "\n", "To complete this tutorial, you'll need to have a Colab runtime with sufficient resources to run the PaliGemma model. In this case, you can use a T4 GPU:\n", "\n", "1. In the upper-right of the Colab window, click the **β–Ύ (Additional connection options)** dropdown menu.\n", "1. Select **Change runtime type**.\n", "1. Under **Hardware accelerator**, select **T4 GPU**." ] }, { "cell_type": "markdown", "metadata": { "id": "Kp6XQ2hQB8lv" }, "source": [ "### Set environment variables for Kaggle API credentials" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "4VpqN2dKjqjl" }, "outputs": [], "source": [ "import os\n", "from google.colab import userdata\n", "\n", "os.environ[\"KAGGLE_USERNAME\"] = userdata.get('KAGGLE_USERNAME')\n", "os.environ[\"KAGGLE_KEY\"] = userdata.get('KAGGLE_KEY')" ] }, { "cell_type": "markdown", "metadata": { "id": "nCE3e7NFpjxZ" }, "source": [ "### Fetch the `big_vision` repository and install related dependencies" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "DfxKb3F839Ks" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m77.9/77.9 kB\u001b[0m \u001b[31m1.5 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", "\u001b[?25h Preparing metadata (setup.py) ... \u001b[?25l\u001b[?25hdone\n", "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m43.2/43.2 kB\u001b[0m \u001b[31m3.3 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", "\u001b[?25h Building wheel for ml_collections (setup.py) ... \u001b[?25l\u001b[?25hdone\n" ] } ], "source": [ "import sys\n", "\n", "# TPUs with\n", "if \"COLAB_TPU_ADDR\" in os.environ:\n", " raise \"It seems you are using Colab with remote TPUs which is not supported.\"\n", "\n", "# Fetch big_vision repository if python doesn't know about it and install\n", "# dependencies needed for this notebook.\n", "if not os.path.exists(\"big_vision_repo\"):\n", " !git clone --quiet --branch=main --depth=1 \\\n", " https://github.com/google-research/big_vision big_vision_repo\n", "\n", "# Append big_vision code to python import path\n", "if \"big_vision_repo\" not in sys.path:\n", " sys.path.append(\"big_vision_repo\")\n", "\n", "# Install missing dependencies. Assume jax~=0.4.25 with GPU available.\n", "!pip3 install -q \"overrides\" \"ml_collections\" \"einops~=0.7\" \"sentencepiece\"" ] }, { "cell_type": "markdown", "metadata": { "id": "zDoq0O77GF30" }, "source": [ "### Import JAX and other dependencies\n", "\n", "Import JAX and other dependencies required for PaliGemma, like TensorFlow and NumPy." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "dTfe2k8J4Bw0" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "JAX version: 0.4.26\n", "JAX platform: gpu\n", "JAX devices: 1\n" ] } ], "source": [ "# Import necessary libraries\n", "import base64\n", "import functools\n", "import html\n", "import io\n", "import glob\n", "\n", "import warnings\n", "\n", "import jax\n", "import jax.numpy as jnp\n", "import numpy as np\n", "import ml_collections\n", "\n", "import tensorflow as tf\n", "import sentencepiece\n", "\n", "import pandas as pd\n", "import random\n", "import json\n", "\n", "from IPython.core.display import display, HTML\n", "from PIL import Image\n", "import matplotlib.pyplot as plt\n", "\n", "# Import model definition from big_vision\n", "from big_vision.models.proj.paligemma import paligemma\n", "from big_vision.trainers.proj.paligemma import predict_fns\n", "\n", "# Import big vision utilities\n", "import big_vision.datasets.jsonl\n", "import big_vision.utils\n", "import big_vision.sharding\n", "\n", "# Don't let TF use the GPU or TPUs\n", "tf.config.set_visible_devices([], \"GPU\")\n", "tf.config.set_visible_devices([], \"TPU\")\n", "\n", "backend = jax.lib.xla_bridge.get_backend()\n", "print(f\"JAX version: {jax.__version__}\")\n", "print(f\"JAX platform: {backend.platform}\")\n", "print(f\"JAX devices: {jax.device_count()}\")" ] }, { "cell_type": "markdown", "metadata": { "id": "b9kSadtIhjlX" }, "source": [ "## Download and configure the model\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "gQNOTfF24AV4" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Downloading the checkpoint from Kaggle, this could take a few minutes....\n", "Downloading from https://www.kaggle.com/api/v1/models/google/paligemma/jax/paligemma-3b-pt-224/1/download/paligemma-3b-pt-224.f16.npz...\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "100%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ| 5.45G/5.45G [01:03<00:00, 91.8MB/s]\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "Model path: /root/.cache/kagglehub/models/google/paligemma/jax/paligemma-3b-pt-224/1/paligemma-3b-pt-224.f16.npz\n", "Downloading the model tokenizer...\n", "Copying gs://big_vision/paligemma_tokenizer.model...\n", "- [1 files][ 4.1 MiB/ 4.1 MiB] \n", "Operation completed over 1 objects/4.1 MiB. \n", "Tokenizer path: ./paligemma_tokenizer.model\n" ] } ], "source": [ "import kagglehub\n", "\n", "MODEL_PATH = \"./pt_224_128.params.f16.npz\"\n", "if not os.path.exists(MODEL_PATH):\n", " print(\"Downloading the checkpoint from Kaggle, this could take a few minutes....\")\n", " # Note: kaggle archive contains the same checkpoint in multiple formats.\n", " # Download only the float16 model.\n", " MODEL_PATH = kagglehub.model_download('google/paligemma/jax/paligemma-3b-pt-224', 'paligemma-3b-pt-224.f16.npz')\n", " print(f\"Model path: {MODEL_PATH}\")\n", "\n", "TOKENIZER_PATH = \"./paligemma_tokenizer.model\"\n", "if not os.path.exists(TOKENIZER_PATH):\n", " print(\"Downloading the model tokenizer...\")\n", " !gsutil cp gs://big_vision/paligemma_tokenizer.model {TOKENIZER_PATH}\n", " print(f\"Tokenizer path: {TOKENIZER_PATH}\")" ] }, { "cell_type": "markdown", "metadata": { "id": "R9R0nS8qjqjo" }, "source": [ "# Prepare Dataset for Fine-tunning\n", "Here, we process the bird image dataset and descriptions for use with PaliGemma.\n", "\n", "1. Curating the Dataset:\n", "\n", "* The **525 Bird Species dataset** [(`gpiosenka/100-bird-species`)](https://www.kaggle.com/datasets/gpiosenka/100-bird-species)from Kaggle contains a comprehensive collection of images representing various bird species. Each image is labeled with its corresponding bird species, providing diverse visual data for training and validation.\n", "\n", "* **Bird Species Descriptions Dataset**: The Bird Species Description DataFrame [(`selamw/birds-discription-df`)](https://www.kaggle.com/datasets/selamw/birds-discription-df) complements the image dataset by providing textual descriptions for the first 23 out of the 525 bird species. This enriches our training data with descriptive text, facilitating a vision-language learning approach with PaliGemma.\n", "\n", "2. Downloading the Datasets from Kaggle:\n", "\n", "* To obtain the datasets containing bird species images and their descriptions, download them directly from Kaggle using the following commands:\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "QXGmAq4wK56p" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Dataset URL: https://www.kaggle.com/datasets/gpiosenka/100-bird-species\n", "License(s): CC0-1.0\n", "Downloading 100-bird-species.zip to /content\n", " 99% 1.94G/1.96G [00:15<00:00, 242MB/s]\n", "100% 1.96G/1.96G [00:15<00:00, 138MB/s]\n", "Dataset URL: https://www.kaggle.com/datasets/selamw/birds-discription-df\n", "License(s): Apache 2.0\n", "Downloading birds-discription-df.zip to /content\n", " 0% 0.00/24.2k [00:00\n", "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
class idfilepathslabelsdata setscientific namebird_description
00.0ABBOTTS BABBLER/001.jpgABBOTTS BABBLERtrainMALACOCINCLA ABBOTTIAbbott's Babbler: Look for this small insectiv...
10.0ABBOTTS BABBLER/007.jpgABBOTTS BABBLERtrainMALACOCINCLA ABBOTTIAbbott's Babbler: Look for this small insectiv...
20.0ABBOTTS BABBLER/008.jpgABBOTTS BABBLERtrainMALACOCINCLA ABBOTTIAbbott's Babbler: Look for this small insectiv...
30.0ABBOTTS BABBLER/009.jpgABBOTTS BABBLERtrainMALACOCINCLA ABBOTTIAbbott's Babbler: Look for this small insectiv...
40.0ABBOTTS BABBLER/002.jpgABBOTTS BABBLERtrainMALACOCINCLA ABBOTTIAbbott's Babbler: Look for this small insectiv...
.....................
3917524.0BLACK BREASTED PUFFBIRD/3.jpgBLACK BREASTED PUFFBIRDvalidNOTHARCHUS PECTORALISBlack-breasted Puffbird: Observe the medium-si...
3918524.0BLACK BREASTED PUFFBIRD/4.jpgBLACK BREASTED PUFFBIRDvalidNOTHARCHUS PECTORALISBlack-breasted Puffbird: Observe the medium-si...
3919524.0BLACK BREASTED PUFFBIRD/1.jpgBLACK BREASTED PUFFBIRDvalidNOTHARCHUS PECTORALISBlack-breasted Puffbird: Observe the medium-si...
3920524.0BLACK BREASTED PUFFBIRD/2.jpgBLACK BREASTED PUFFBIRDvalidNOTHARCHUS PECTORALISBlack-breasted Puffbird: Observe the medium-si...
3921524.0BLACK BREASTED PUFFBIRD/5.jpgBLACK BREASTED PUFFBIRDvalidNOTHARCHUS PECTORALISBlack-breasted Puffbird: Observe the medium-si...
\n", "

3922 rows Γ— 6 columns

\n", "
\n", "
\n", "\n", "
\n", " \n", "\n", " \n", "\n", " \n", "
\n", "\n", "\n", "
\n", " \n", "\n", "\n", "\n", " \n", "
\n", "\n", "
\n", " \n", " \n", " \n", "
\n", "\n", "
\n", " \n" ], "text/plain": [ " class id filepaths labels \\\n", "0 0.0 ABBOTTS BABBLER/001.jpg ABBOTTS BABBLER \n", "1 0.0 ABBOTTS BABBLER/007.jpg ABBOTTS BABBLER \n", "2 0.0 ABBOTTS BABBLER/008.jpg ABBOTTS BABBLER \n", "3 0.0 ABBOTTS BABBLER/009.jpg ABBOTTS BABBLER \n", "4 0.0 ABBOTTS BABBLER/002.jpg ABBOTTS BABBLER \n", "... ... ... ... \n", "3917 524.0 BLACK BREASTED PUFFBIRD/3.jpg BLACK BREASTED PUFFBIRD \n", "3918 524.0 BLACK BREASTED PUFFBIRD/4.jpg BLACK BREASTED PUFFBIRD \n", "3919 524.0 BLACK BREASTED PUFFBIRD/1.jpg BLACK BREASTED PUFFBIRD \n", "3920 524.0 BLACK BREASTED PUFFBIRD/2.jpg BLACK BREASTED PUFFBIRD \n", "3921 524.0 BLACK BREASTED PUFFBIRD/5.jpg BLACK BREASTED PUFFBIRD \n", "\n", " data set scientific name \\\n", "0 train MALACOCINCLA ABBOTTI \n", "1 train MALACOCINCLA ABBOTTI \n", "2 train MALACOCINCLA ABBOTTI \n", "3 train MALACOCINCLA ABBOTTI \n", "4 train MALACOCINCLA ABBOTTI \n", "... ... ... \n", "3917 valid NOTHARCHUS PECTORALIS \n", "3918 valid NOTHARCHUS PECTORALIS \n", "3919 valid NOTHARCHUS PECTORALIS \n", "3920 valid NOTHARCHUS PECTORALIS \n", "3921 valid NOTHARCHUS PECTORALIS \n", "\n", " bird_description \n", "0 Abbott's Babbler: Look for this small insectiv... \n", "1 Abbott's Babbler: Look for this small insectiv... \n", "2 Abbott's Babbler: Look for this small insectiv... \n", "3 Abbott's Babbler: Look for this small insectiv... \n", "4 Abbott's Babbler: Look for this small insectiv... \n", "... ... \n", "3917 Black-breasted Puffbird: Observe the medium-si... \n", "3918 Black-breasted Puffbird: Observe the medium-si... \n", "3919 Black-breasted Puffbird: Observe the medium-si... \n", "3920 Black-breasted Puffbird: Observe the medium-si... \n", "3921 Black-breasted Puffbird: Observe the medium-si... \n", "\n", "[3922 rows x 6 columns]" ] }, "execution_count": 9, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Load the descriptions DataFrame\n", "birds_discription_df = pd.read_csv(\"birds_description.csv\")\n", "birds_discription_df" ] }, { "cell_type": "markdown", "metadata": { "id": "CZg6Rj0Njqjp" }, "source": [ "### Display number of unique species, number of training and validation samples" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "uA-l53mTjqjp" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Number of unique bird species: 23\n", "Number of training samples: 3692\n", "Number of validation samples: 115\n" ] } ], "source": [ "print(\"Number of unique bird species:\", len(birds_discription_df['labels'].unique()))\n", "print(\"Number of training samples:\", (birds_discription_df['data set'] == \"train\").sum())\n", "print(\"Number of validation samples:\", (birds_discription_df['data set'] == \"valid\").sum())" ] }, { "cell_type": "markdown", "metadata": { "id": "1Qkqjs2Yjqjp" }, "source": [ "## Convert DataFrame to JSON Lines for Finetuning\n", "\n", "Since PaliGemma expects data in JSON Lines format for finetuning, we convert the DataFrame containing bird information into separate JSON Lines files for training and validation data.\n", "\n", "For fine-tuning, we only need two columns from the DataFrame:\n", "\n", "* `\"filepaths\"`: This column contains the paths to the bird images.\n", "* `\"bird_description\"`: This column contains descriptions for each bird species." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "jzFnL_wujqjp" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "DataFrame converted to JSON Lines format and saved to train.jsonl and valid.jsonl\n" ] } ], "source": [ "def df_to_jsonl(df):\n", " \"\"\"Converts a pandas DataFrame to separate JSON Lines files for train and validation data.\n", "\n", " Args:\n", " df: The pandas DataFrame to convert.\n", "\n", " Returns:\n", " None. Writes the JSON Lines to separate 'train.jsonl' and 'valid.jsonl' files.\n", " \"\"\"\n", "\n", " train_file = open('train.jsonl', 'w')\n", " valid_file = open('valid.jsonl', 'w')\n", "\n", " try:\n", " for index, row in df.iterrows():\n", " if row['data set'] == 'train':\n", " data = {\n", " 'prefix': '',\n", " 'suffix': row['bird_description'],\n", " 'image': row['filepaths']\n", " }\n", " json.dump(data, train_file)\n", " train_file.write('\\n')\n", " elif row['data set'] == 'valid':\n", " data = {\n", " 'prefix': '',\n", " 'suffix': row['bird_description'],\n", " 'image': row['filepaths']\n", " }\n", " json.dump(data, valid_file)\n", " valid_file.write('\\n')\n", "\n", " finally:\n", " train_file.close()\n", " valid_file.close()\n", "\n", " print(\"DataFrame converted to JSON Lines format and saved to train.jsonl and valid.jsonl\")\n", "\n", "# Convert the birds description DataFrame to JSON Lines format\n", "df_to_jsonl(birds_discription_df)\n" ] }, { "cell_type": "markdown", "metadata": { "id": "aMyiB9bUjqjp" }, "source": [ "## Display first record from JSON lines file" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "xnud6HhMjqjp" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "First Record:\n", "{\n", " \"prefix\": \"\",\n", " \"suffix\": \"Abbott's Babbler: Look for this small insectivorous bird with distinctive streaked brown plumage and pale buff underparts, making its home in the diverse landscapes of South Asia.\",\n", " \"image\": \"ABBOTTS BABBLER/001.jpg\"\n", "}\n" ] } ], "source": [ "def display_first_record(filename):\n", " \"\"\"Opens a JSON Lines file and displays only the first record.\n", "\n", " Args:\n", " filename: The path to the JSON Lines file.\n", " \"\"\"\n", " try:\n", " with open(filename, 'r') as f:\n", " first_line = f.readline().strip()\n", " if first_line: # Check if there's data in the file\n", " data = json.loads(first_line)\n", " print(f\"First Record:\\n{json.dumps(data, indent=2)}\")\n", " else:\n", " print(\"File is empty or corrupt.\")\n", " except FileNotFoundError:\n", " print(f\"Error: File '{filename}' not found.\")\n", "\n", "# Display the first record in 'train.jsonl'\n", "display_first_record('train.jsonl')" ] }, { "cell_type": "markdown", "metadata": { "id": "rv7w-cGuLj5o" }, "source": [ "# Download and configure PaliGemma model\n", "This section retrieves the pre-trained model and tokenizer weights." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "v5e1RGCDjqjq" }, "outputs": [], "source": [ "# Define model\n", "model_config = ml_collections.FrozenConfigDict({\n", " \"llm\": {\"vocab_size\": 257_152},\n", " \"img\": {\"variant\": \"So400m/14\", \"pool_type\": \"none\", \"scan\": True, \"dtype_mm\": \"float16\"}\n", "})\n", "model = paligemma.Model(**model_config)\n", "tokenizer = sentencepiece.SentencePieceProcessor(TOKENIZER_PATH)\n", "\n", "# Load params - this can take up to 1 minute in a notebook.\n", "params = paligemma.load(None, MODEL_PATH, model_config)\n", "\n", "# Define `decode` function to sample outputs from the model.\n", "decode_fn = predict_fns.get_all(model)['decode']\n", "decode = functools.partial(decode_fn, devices=jax.devices(), eos_token=tokenizer.eos_id())" ] }, { "cell_type": "markdown", "metadata": { "id": "uidBwmb8LwZ5" }, "source": [ "### Move model parameters into GPU/TPU memory" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "RWOdf_fw2SAO" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ " == Model params == \n", "img/Transformer/encoder_norm/bias (1152,) float16\n", "img/Transformer/encoder_norm/scale (1152,) float16\n", "img/Transformer/encoderblock/LayerNorm_0/bias (27, 1152) float16\n", "img/Transformer/encoderblock/LayerNorm_0/scale (27, 1152) float16\n", "img/Transformer/encoderblock/LayerNorm_1/bias (27, 1152) float16\n", "img/Transformer/encoderblock/LayerNorm_1/scale (27, 1152) float16\n", "img/Transformer/encoderblock/MlpBlock_0/Dense_0/bias (27, 4304) float16\n", "img/Transformer/encoderblock/MlpBlock_0/Dense_0/kernel (27, 1152, 4304) float16\n", "img/Transformer/encoderblock/MlpBlock_0/Dense_1/bias (27, 1152) float16\n", "img/Transformer/encoderblock/MlpBlock_0/Dense_1/kernel (27, 4304, 1152) float16\n", "img/Transformer/encoderblock/MultiHeadDotProductAttention_0/key/bias (27, 16, 72) float16\n", "img/Transformer/encoderblock/MultiHeadDotProductAttention_0/key/kernel (27, 1152, 16, 72) float16\n", "img/Transformer/encoderblock/MultiHeadDotProductAttention_0/out/bias (27, 1152) float16\n", "img/Transformer/encoderblock/MultiHeadDotProductAttention_0/out/kernel (27, 16, 72, 1152) float16\n", "img/Transformer/encoderblock/MultiHeadDotProductAttention_0/query/bias (27, 16, 72) float16\n", "img/Transformer/encoderblock/MultiHeadDotProductAttention_0/query/kernel (27, 1152, 16, 72) float16\n", "img/Transformer/encoderblock/MultiHeadDotProductAttention_0/value/bias (27, 16, 72) float16\n", "img/Transformer/encoderblock/MultiHeadDotProductAttention_0/value/kernel (27, 1152, 16, 72) float16\n", "img/embedding/bias (1152,) float16\n", "img/embedding/kernel (14, 14, 3, 1152) float16\n", "img/head/bias (2048,) float16\n", "img/head/kernel (1152, 2048) float16\n", "img/pos_embedding (1, 256, 1152) float16\n", "llm/embedder/input_embedding (257152, 2048) float16\n", "llm/final_norm/scale (2048,) float16\n", "llm/layers/attn/attn_vec_einsum/w (18, 8, 256, 2048) float32\n", "llm/layers/attn/kv_einsum/w (18, 2, 1, 2048, 256) float32\n", "llm/layers/attn/q_einsum/w (18, 8, 2048, 256) float32\n", "llm/layers/mlp/gating_einsum (18, 2, 2048, 16384) float16\n", "llm/layers/mlp/linear (18, 16384, 2048) float16\n", "llm/layers/pre_attention_norm/scale (18, 2048) float16\n", "llm/layers/pre_ffw_norm/scale (18, 2048) float16\n" ] } ], "source": [ "# Create a pytree mask of the trainable params.\n", "def is_trainable_param(name, param): # pylint: disable=unused-argument\n", " if name.startswith(\"llm/layers/attn/\"): return True\n", " if name.startswith(\"llm/\"): return False\n", " if name.startswith(\"img/\"): return False\n", " raise ValueError(f\"Unexpected param name {name}\")\n", "trainable_mask = big_vision.utils.tree_map_with_names(is_trainable_param, params)\n", "\n", "# If more than one device is available (e.g. multiple GPUs) the parameters can\n", "# be sharded across them to reduce HBM usage per device.\n", "mesh = jax.sharding.Mesh(jax.devices(), (\"data\"))\n", "\n", "data_sharding = jax.sharding.NamedSharding(\n", " mesh, jax.sharding.PartitionSpec(\"data\"))\n", "\n", "params_sharding = big_vision.sharding.infer_sharding(\n", " params, strategy=[('.*', 'fsdp(axis=\"data\")')], mesh=mesh)\n", "\n", "# Yes: Some donated buffers are not usable.\n", "warnings.filterwarnings(\n", " \"ignore\", message=\"Some donated buffers were not usable\")\n", "\n", "@functools.partial(jax.jit, donate_argnums=(0,), static_argnums=(1,))\n", "def maybe_cast_to_f32(params, trainable):\n", " return jax.tree.map(lambda p, m: p.astype(jnp.float32) if m else p,\n", " params, trainable)\n", "\n", "# Loading all params in simultaneous - albeit much faster and more succinct -\n", "# requires more RAM than the notebook runtimes have by default.\n", "# Instead we do it param by param.\n", "params, treedef = jax.tree.flatten(params)\n", "sharding_leaves = jax.tree.leaves(params_sharding)\n", "trainable_leaves = jax.tree.leaves(trainable_mask)\n", "for idx, (sharding, trainable) in enumerate(zip(sharding_leaves, trainable_leaves)):\n", " params[idx] = big_vision.utils.reshard(params[idx], sharding)\n", " params[idx] = maybe_cast_to_f32(params[idx], trainable)\n", " params[idx].block_until_ready()\n", "params = jax.tree.unflatten(treedef, params)\n", "\n", "# Print params to show what the model is made of.\n", "def parameter_overview(params):\n", " for path, arr in big_vision.utils.tree_flatten_with_names(params)[0]:\n", " print(f\"{path:80s} {str(arr.shape):22s} {arr.dtype}\")\n", "\n", "print(\" == Model params == \")\n", "parameter_overview(params)" ] }, { "cell_type": "markdown", "metadata": { "id": "iD_9XXQkn1Mv" }, "source": [ "# Prepare to tune the model" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "8SRW0NuU4UcW" }, "outputs": [], "source": [ "def preprocess_image(image, size=224):\n", " # Model has been trained to handle images of different aspects ratios\n", " # resized to 224x224 in the range [-1, 1]. Bilinear and antialias resize\n", " # options are helpful to improve quality in some tasks.\n", " image = np.asarray(image)\n", " if image.ndim == 2: # Convert image without last channel into greyscale.\n", " image = np.stack((image,)*3, axis=-1)\n", " image = image[..., :3] # Remove alpha layer.\n", " assert image.shape[-1] == 3\n", "\n", " image = tf.constant(image)\n", " image = tf.image.resize(image, (size, size), method='bilinear', antialias=True)\n", " return image.numpy() / 127.5 - 1.0 # [0, 255]->[-1,1]\n", "\n", "def preprocess_tokens(prefix, suffix=None, seqlen=None):\n", " # Model has been trained to handle tokenized text composed of a prefix with\n", " # full attention and a suffix with causal attention.\n", " separator = \"\\n\"\n", " tokens = tokenizer.encode(prefix, add_bos=True) + tokenizer.encode(separator)\n", " mask_ar = [0] * len(tokens) # 0 to use full attention for prefix.\n", " mask_loss = [0] * len(tokens) # 0 to not use prefix tokens in the loss.\n", "\n", " if suffix:\n", " suffix = tokenizer.encode(suffix, add_eos=True)\n", " tokens += suffix\n", " mask_ar += [1] * len(suffix) # 1 to use causal attention for suffix.\n", " mask_loss += [1] * len(suffix) # 1 to use suffix tokens in the loss.\n", "\n", " mask_input = [1] * len(tokens) # 1 if it's a token, 0 if padding.\n", " if seqlen:\n", " padding = [0] * max(0, seqlen - len(tokens))\n", " tokens = tokens[:seqlen] + padding\n", " mask_ar = mask_ar[:seqlen] + padding\n", " mask_loss = mask_loss[:seqlen] + padding\n", " mask_input = mask_input[:seqlen] + padding\n", "\n", " return jax.tree.map(np.array, (tokens, mask_ar, mask_loss, mask_input))\n", "\n", "def postprocess_tokens(tokens):\n", " tokens = tokens.tolist() # np.array to list[int]\n", " try: # Remove tokens at and after EOS if any.\n", " eos_pos = tokens.index(tokenizer.eos_id())\n", " tokens = tokens[:eos_pos]\n", " except ValueError:\n", " pass\n", " return tokenizer.decode(tokens)\n" ] }, { "cell_type": "markdown", "metadata": { "id": "ovgWBgdHJZq3" }, "source": [ "### Create the training and validation iterators" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "whzWOojGOtzi" }, "outputs": [], "source": [ "SEQLEN = 128\n", "\n", "TRAIN_DATA_DIR = 'train/'\n", "VALID_DATA_DIR = 'valid/'\n", "\n", "# Load training data\n", "train_dataset = big_vision.datasets.jsonl.DataSource(\n", " os.path.join(\"train.jsonl\"),\n", " fopen_keys={\"image\": TRAIN_DATA_DIR})\n", "\n", "# Load validation data\n", "val_dataset = big_vision.datasets.jsonl.DataSource(\n", " os.path.join(\"valid.jsonl\"),\n", " fopen_keys={\"image\": VALID_DATA_DIR})\n", "\n", "def train_data_iterator():\n", " \"\"\"Never ending iterator over training examples.\"\"\"\n", " # Shuffle examples and repeat so one can train for many epochs.\n", " dataset = train_dataset.get_tfdata().shuffle(1_000).repeat()\n", " for example in dataset.as_numpy_iterator():\n", " image = Image.open(io.BytesIO(example[\"image\"]))\n", " image = preprocess_image(image)\n", "\n", " # Define prefix for tokenization\n", " prefix = \"describe en\"\n", " suffix = example[\"suffix\"].decode().lower()\n", " tokens, mask_ar, mask_loss, _ = preprocess_tokens(prefix, suffix, SEQLEN)\n", "\n", " yield {\n", " \"image\": np.asarray(image),\n", " \"text\": np.asarray(tokens),\n", " \"mask_ar\": np.asarray(mask_ar),\n", " \"mask_loss\": np.asarray(mask_loss),\n", " }\n", "\n", "\n", "def validation_data_iterator():\n", " \"\"\"Single iterator over validation examples.\"\"\"\n", " for example in val_dataset.get_tfdata(ordered=True).as_numpy_iterator():\n", " image = Image.open(io.BytesIO(example[\"image\"]))\n", " image = preprocess_image(image)\n", "\n", " # Define prefix for tokenization\n", " prefix = \"describe en\" # Could also be a different prefix per example 'describe en'\n", " tokens, mask_ar, _, mask_input = preprocess_tokens(prefix, seqlen=SEQLEN)\n", "\n", " yield {\n", " \"image\": np.asarray(image),\n", " \"text\": np.asarray(tokens),\n", " \"mask_ar\": np.asarray(mask_ar),\n", " \"mask_input\": np.asarray(mask_input),\n", " }\n" ] }, { "cell_type": "markdown", "metadata": { "id": "84olaM5dCiAl" }, "source": [ "### View sample training examples" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "BzJfb5t0nsLq" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Training Examples\n" ] }, { "data": { "text/html": [ "\n", "
\n", " \n", "

yellow-billed chough: observe this medium-sized crow easily recognizable by its sleek black feathers and contrasting long yellow bill, a common sight in the mountainous regions of europe and asia, noted for its acrobatic flight and yellow bill.

\n", "
\n", " \n", "
\n", " \n", "

american bittern: notice this medium-sized heron, a master of camouflage, blending seamlessly into the marshes of north and central america, distinguished by its cryptic plumage and elongated neck.

\n", "
\n", " \n", "
\n", " \n", "

american pipit: look for this small, ground-dwelling songbird with streaked brown plumage and long tail, filling the air with melodic tunes across the grasslands of north america, europe, and asia.

\n", "
\n", " \n", "
\n", " \n", "

alexandrine parakeet: notice this medium-sized parrot bursting with a rainbow of colors, a vibrant resident of the forests and woodlands of south and southeast asia, distinguished by its large size and distinctive red beak.

\n", "
\n", " \n", "
\n", " \n", "

crowned crane: observe this epitome of elegance with a golden crown, calling the wetlands and grasslands of sub-saharan africa home, distinguished by its tall stature, long legs, and regal posture.

\n", "
\n", " \n", "
\n", " \n", "

african pygmy goose: look for this small freshwater goose with dark brown feathers and contrasting white markings, gracing the lakes and rivers of sub-saharan africa, noted for its petite size and distinctive facial markings.

\n", "
\n", " \n", "
\n", " \n", "

american bittern: notice this medium-sized heron, a master of camouflage, blending seamlessly into the marshes of north and central america, distinguished by its cryptic plumage and elongated neck.

\n", "
\n", " \n", "
\n", " \n", "

african pygmy goose: look for this small freshwater goose with dark brown feathers and contrasting white markings, gracing the lakes and rivers of sub-saharan africa, noted for its petite size and distinctive facial markings.

\n", "
\n", " " ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "def render_inline(image, resize=(128, 128)):\n", " \"\"\"Converts an image array into inline HTML.\n", "\n", " Args:\n", " image (numpy.ndarray): The image array to convert.\n", " resize (tuple): Optional. Size to resize the image to before conversion. Default is (128, 128).\n", "\n", " Returns:\n", " str: HTML representation of the image encoded in base64.\n", " \"\"\"\n", " image = Image.fromarray(image)\n", " image.resize(resize)\n", " with io.BytesIO() as buffer:\n", " image.save(buffer, format='jpeg')\n", " image_b64 = str(base64.b64encode(buffer.getvalue()), \"utf-8\")\n", " return f\"data:image/jpeg;base64,{image_b64}\"\n", "\n", "def render_example(image, description):\n", " \"\"\"Generates HTML for displaying an image with its description.\n", "\n", " Args:\n", " image (numpy.ndarray): The image array to display.\n", " description (str): The description of the image.\n", "\n", " Returns:\n", " str: HTML representation of the image with description.\n", " \"\"\"\n", " image = ((image + 1)/2 * 255).astype(np.uint8) # [-1,1] -> [0, 255]\n", " return f\"\"\"\n", "
\n", " \n", "

{html.escape(description)}

\n", "
\n", " \"\"\"\n", "\n", "html_out = \"\"\n", "for idx, example in zip(range(8), train_data_iterator()):\n", " description = postprocess_tokens(example[\"text\"]) # detokenize model input.\n", " description = description[len(\"describe en\\n\"):]\n", " html_out += render_example(example[\"image\"], description)\n", "\n", "print(\"Training Examples\")\n", "display(HTML(html_out))" ] }, { "cell_type": "markdown", "metadata": { "id": "N2BwpXkfI8OT" }, "source": [ "### Define the training and evaluation loops" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "dwUV_imW3WQJ" }, "outputs": [], "source": [ "# The main update_fn using a simple stochastic gradient descent (SGD).\n", "@functools.partial(jax.jit, donate_argnums=(0,))\n", "def update_fn(params, batch, learning_rate):\n", " \"\"\"Performs one update step using stochastic gradient descent (SGD).\n", "\n", " Args:\n", " params (dict): Current model parameters.\n", " batch (dict): Batch of data containing images, texts, and masks.\n", " learning_rate (float): Learning rate for the update.\n", "\n", " Returns:\n", " tuple: Updated parameters and the calculated loss.\n", " \"\"\"\n", "\n", " imgs, txts, mask_ar = batch[\"image\"], batch[\"text\"], batch[\"mask_ar\"]\n", "\n", " def loss_fn(params):\n", " text_logits, _ = model.apply({\"params\": params}, imgs, txts[:, :-1], mask_ar[:, :-1], train=True)\n", " logp = jax.nn.log_softmax(text_logits, axis=-1)\n", "\n", " # The model takes as input txts[:, :-1] but the loss is defined as predicting\n", " # next tokens txts[:, 1:]. Additionally, mask_loss[:, 1:] indicates which tokens\n", " # are part of the loss (e.g. prefix and padded tokens are not included).\n", " mask_loss = batch[\"mask_loss\"][:, 1:]\n", " targets = jax.nn.one_hot(txts[:, 1:], text_logits.shape[-1])\n", "\n", " # Compute the loss per example. i.e. the mean of per token pplx.\n", " # Since each example has a different number of tokens we normalize it.\n", " token_pplx = jnp.sum(logp * targets, axis=-1) # sum across vocab_size.\n", " example_loss = -jnp.sum(token_pplx * mask_loss, axis=-1) # sum across seq_len.\n", " example_loss /= jnp.clip(jnp.sum(mask_loss, -1), 1) # weight by num of tokens.\n", "\n", " # batch_loss: mean of per example loss.\n", " return jnp.mean(example_loss)\n", "\n", " loss, grads = jax.value_and_grad(loss_fn)(params)\n", "\n", " # Apply gradients to trainable params using SGD.\n", " def apply_grad(param, gradient, trainable):\n", " if not trainable: return param\n", " return param - learning_rate * gradient\n", "\n", " params = jax.tree_util.tree_map(apply_grad, params, grads, trainable_mask)\n", "\n", " return params, loss\n", "\n", "# Evaluation/inference loop.\n", "def make_predictions(data_iterator, *, num_examples=None,\n", " batch_size=4, seqlen=SEQLEN, sampler=\"greedy\"):\n", " \"\"\"Generates model predictions for given data iterator.\n", "\n", " Args:\n", " data_iterator (iterator): Iterator yielding batches of data.\n", " num_examples (int, optional): Maximum number of examples to generate predictions for.\n", " batch_size (int, optional): Batch size for inference. Default is 4.\n", " seqlen (int, optional): Maximum sequence length for decoding. Default is SEQLEN.\n", " sampler (str, optional): Sampling method for generating predictions. Default is \"greedy\".\n", "\n", " Returns:\n", " list: List of tuples containing image and corresponding model response.\n", " \"\"\"\n", " outputs = []\n", " while True:\n", " # Construct a list of examples in the batch.\n", " examples = []\n", " try:\n", " for _ in range(batch_size):\n", " examples.append(next(data_iterator))\n", " examples[-1][\"_mask\"] = np.array(True) # Indicates true example.\n", " except StopIteration:\n", " if len(examples) == 0:\n", " return outputs\n", "\n", " # Not enough examples to complete a batch. Pad by repeating last example.\n", " while len(examples) % batch_size:\n", " examples.append(dict(examples[-1]))\n", " examples[-1][\"_mask\"] = np.array(False) # Indicates padding example.\n", "\n", " # Convert list of examples into a dict of np.arrays and load onto devices.\n", " batch = jax.tree.map(lambda *x: np.stack(x), *examples)\n", " batch = big_vision.utils.reshard(batch, data_sharding)\n", "\n", " # Make model predictions\n", " tokens = decode({\"params\": params}, batch=batch,\n", " max_decode_len=seqlen, sampler=sampler)\n", "\n", " # Fetch model predictions to device and detokenize.\n", " tokens, mask = jax.device_get((tokens, batch[\"_mask\"]))\n", " tokens = tokens[mask] # remove padding examples.\n", " responses = [postprocess_tokens(t) for t in tokens]\n", "\n", "\n", " # Append to html output.\n", " for example, response in zip(examples, responses):\n", " outputs.append((example[\"image\"], response))\n", " if num_examples and len(outputs) >= num_examples:\n", " return outputs" ] }, { "cell_type": "markdown", "metadata": { "id": "n9r9V1jwJvu9" }, "source": [ "# Finetune the model" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "kXIT0lB9jqjs" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "step: 1/128 lr: 0.00005 loss: 3.5541\n", "step: 2/128 lr: 0.00009 loss: 3.2215\n", "step: 3/128 lr: 0.00014 loss: 3.7010\n", "step: 4/128 lr: 0.00019 loss: 3.2698\n", "step: 5/128 lr: 0.00023 loss: 3.5187\n", "step: 6/128 lr: 0.00028 loss: 3.3232\n", "step: 7/128 lr: 0.00033 loss: 3.2168\n", "step: 8/128 lr: 0.00038 loss: 3.2819\n", "step: 9/128 lr: 0.00042 loss: 3.2132\n", "step: 10/128 lr: 0.00047 loss: 3.2118\n", "step: 11/128 lr: 0.00052 loss: 3.1371\n", "step: 12/128 lr: 0.00056 loss: 3.1616\n", "step: 13/128 lr: 0.00061 loss: 3.5072\n", "step: 14/128 lr: 0.00066 loss: 2.9888\n", "step: 15/128 lr: 0.00070 loss: 3.2852\n", "step: 16/128 lr: 0.00075 loss: 3.1900\n", "step: 17/128 lr: 0.00080 loss: 3.1098\n", "step: 18/128 lr: 0.00084 loss: 3.0129\n", "step: 19/128 lr: 0.00089 loss: 3.0052\n", "step: 20/128 lr: 0.00094 loss: 2.9630\n", "step: 21/128 lr: 0.00098 loss: 2.8648\n", "step: 22/128 lr: 0.00103 loss: 2.9091\n", "step: 23/128 lr: 0.00108 loss: 2.7960\n", "step: 24/128 lr: 0.00112 loss: 2.9222\n", "step: 25/128 lr: 0.00117 loss: 2.7515\n", "step: 26/128 lr: 0.00122 loss: 2.7756\n", "step: 27/128 lr: 0.00127 loss: 2.6340\n", "step: 28/128 lr: 0.00131 loss: 2.9309\n", "step: 29/128 lr: 0.00136 loss: 2.7362\n", "step: 30/128 lr: 0.00141 loss: 2.4330\n", "step: 31/128 lr: 0.00145 loss: 2.4895\n", "step: 32/128 lr: 0.00150 loss: 2.7376\n", "step: 33/128 lr: 0.00155 loss: 2.3519\n", "step: 34/128 lr: 0.00159 loss: 2.3741\n", "step: 35/128 lr: 0.00164 loss: 2.1744\n", "step: 36/128 lr: 0.00169 loss: 2.2894\n", "step: 37/128 lr: 0.00173 loss: 2.5071\n", "step: 38/128 lr: 0.00178 loss: 2.2978\n", "step: 39/128 lr: 0.00183 loss: 2.4412\n", "step: 40/128 lr: 0.00188 loss: 2.1825\n", "step: 41/128 lr: 0.00192 loss: 2.1376\n", "step: 42/128 lr: 0.00197 loss: 2.1129\n", "step: 43/128 lr: 0.00202 loss: 2.3329\n", "step: 44/128 lr: 0.00206 loss: 1.9307\n", "step: 45/128 lr: 0.00211 loss: 2.0200\n", "step: 46/128 lr: 0.00216 loss: 1.9894\n", "step: 47/128 lr: 0.00220 loss: 1.8897\n", "step: 48/128 lr: 0.00225 loss: 1.9321\n", "step: 49/128 lr: 0.00230 loss: 1.8077\n", "step: 50/128 lr: 0.00234 loss: 1.7965\n", "step: 51/128 lr: 0.00239 loss: 1.8647\n", "step: 52/128 lr: 0.00244 loss: 1.6083\n", "step: 53/128 lr: 0.00248 loss: 1.7444\n", "step: 54/128 lr: 0.00253 loss: 1.6344\n", "step: 55/128 lr: 0.00258 loss: 1.6643\n", "step: 56/128 lr: 0.00262 loss: 1.7359\n", "step: 57/128 lr: 0.00267 loss: 1.5264\n", "step: 58/128 lr: 0.00272 loss: 1.6980\n", "step: 59/128 lr: 0.00277 loss: 1.7417\n", "step: 60/128 lr: 0.00281 loss: 1.5004\n", "step: 61/128 lr: 0.00286 loss: 1.6187\n", "step: 62/128 lr: 0.00291 loss: 1.4576\n", "step: 63/128 lr: 0.00295 loss: 1.4337\n", "step: 64/128 lr: 0.00300 loss: 1.4088\n", "step: 65/128 lr: 0.00300 loss: 1.6668\n", "step: 66/128 lr: 0.00299 loss: 1.4565\n", "step: 67/128 lr: 0.00298 loss: 1.3991\n", "step: 68/128 lr: 0.00297 loss: 1.5546\n", "step: 69/128 lr: 0.00296 loss: 1.4990\n", "step: 70/128 lr: 0.00294 loss: 0.9921\n", "step: 71/128 lr: 0.00291 loss: 1.4244\n", "step: 72/128 lr: 0.00289 loss: 1.2937\n", "step: 73/128 lr: 0.00286 loss: 1.1714\n", "step: 74/128 lr: 0.00283 loss: 0.9958\n", "step: 75/128 lr: 0.00279 loss: 1.3715\n", "step: 76/128 lr: 0.00275 loss: 1.1864\n", "step: 77/128 lr: 0.00271 loss: 1.2027\n", "step: 78/128 lr: 0.00267 loss: 1.0820\n", "step: 79/128 lr: 0.00262 loss: 1.1740\n", "step: 80/128 lr: 0.00257 loss: 1.4427\n", "step: 81/128 lr: 0.00252 loss: 1.0802\n", "step: 82/128 lr: 0.00247 loss: 0.8756\n", "step: 83/128 lr: 0.00241 loss: 1.3305\n", "step: 84/128 lr: 0.00235 loss: 0.8690\n", "step: 85/128 lr: 0.00229 loss: 1.0759\n", "step: 86/128 lr: 0.00223 loss: 1.0876\n", "step: 87/128 lr: 0.00216 loss: 0.8533\n", "step: 88/128 lr: 0.00210 loss: 0.7881\n", "step: 89/128 lr: 0.00203 loss: 0.7177\n", "step: 90/128 lr: 0.00196 loss: 0.8630\n", "step: 91/128 lr: 0.00189 loss: 0.8725\n", "step: 92/128 lr: 0.00182 loss: 0.8491\n", "step: 93/128 lr: 0.00175 loss: 0.9923\n", "step: 94/128 lr: 0.00168 loss: 0.6511\n", "step: 95/128 lr: 0.00161 loss: 0.4120\n", "step: 96/128 lr: 0.00154 loss: 0.5675\n", "step: 97/128 lr: 0.00146 loss: 0.9469\n", "step: 98/128 lr: 0.00139 loss: 1.1420\n", "step: 99/128 lr: 0.00132 loss: 0.5743\n", "step: 100/128 lr: 0.00125 loss: 0.7502\n", "step: 101/128 lr: 0.00118 loss: 0.8339\n", "step: 102/128 lr: 0.00111 loss: 0.7139\n", "step: 103/128 lr: 0.00104 loss: 0.4566\n", "step: 104/128 lr: 0.00097 loss: 0.8023\n", "step: 105/128 lr: 0.00090 loss: 0.5136\n", "step: 106/128 lr: 0.00084 loss: 0.7242\n", "step: 107/128 lr: 0.00077 loss: 0.8242\n", "step: 108/128 lr: 0.00071 loss: 0.5768\n", "step: 109/128 lr: 0.00065 loss: 0.6741\n", "step: 110/128 lr: 0.00059 loss: 0.4422\n", "step: 111/128 lr: 0.00053 loss: 0.3808\n", "step: 112/128 lr: 0.00048 loss: 0.7980\n", "step: 113/128 lr: 0.00043 loss: 0.6143\n", "step: 114/128 lr: 0.00038 loss: 0.7009\n", "step: 115/128 lr: 0.00033 loss: 0.6710\n", "step: 116/128 lr: 0.00029 loss: 0.3956\n", "step: 117/128 lr: 0.00025 loss: 0.7030\n", "step: 118/128 lr: 0.00021 loss: 0.7158\n", "step: 119/128 lr: 0.00017 loss: 0.4146\n", "step: 120/128 lr: 0.00014 loss: 0.6061\n", "step: 121/128 lr: 0.00011 loss: 0.4536\n", "step: 122/128 lr: 0.00009 loss: 0.5641\n", "step: 123/128 lr: 0.00006 loss: 0.6973\n", "step: 124/128 lr: 0.00004 loss: 0.6476\n", "step: 125/128 lr: 0.00003 loss: 0.7660\n", "step: 126/128 lr: 0.00002 loss: 0.4655\n", "step: 127/128 lr: 0.00001 loss: 0.6795\n", "step: 128/128 lr: 0.00000 loss: 0.5314\n" ] }, { "data": { "image/png": "iVBORw0KGgoAAAANSUhEUgAAAjcAAAHHCAYAAABDUnkqAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/bCgiHAAAACXBIWXMAAA9hAAAPYQGoP6dpAACJAklEQVR4nO3dd3hUZfo38O+ZmcxMyqT3kEYChN5LQIqCAioC6loWF+yrwqqrvirrDwvqYllXdy1g2ZW1sIpKcbEgoID0jtQQICSB9N6TKef9Y+acmUlmkklIMsnk+7muuUzOnDnzzCGSm/u+n+cRRFEUQUREROQhFO4eABEREVF7YnBDREREHoXBDREREXkUBjdERETkURjcEBERkUdhcENEREQehcENEREReRQGN0RERORRGNwQERGRR2FwQ9TF3XnnnUhISGjTa59//nkIgtC+AyIi6uIY3BC1kSAILj22bt3q7qG6xZ133gk/Pz93D8Mloiji008/xaRJkxAYGAgfHx8MHjwYS5cuRXV1tbuH59COHTswc+ZMxMTEQKvVIi4uDrNmzcKqVavkc2pqavD888/32J9B6rkE7i1F1DafffaZ3feffPIJNm3ahE8//dTu+NVXX42IiIg2v49er4fJZIJGo2n1aw0GAwwGA7RabZvfv63uvPNOfP3116iqqur0924No9GI3//+91i9ejUmTpyIG2+8ET4+Pvj111+xatUqDBgwAJs3b76sP8P29tVXX+HWW2/FsGHDcNtttyEoKAgZGRnYvn07vLy88MsvvwAAioqKEBYWhueeew7PP/+8ewdN1IlU7h4AUXd1xx132H2/Z88ebNq0qcnxxmpqauDj4+Py+3h5ebVpfACgUqmgUvF/8+a89tprWL16NZ544gm8/vrr8vH7778ft9xyC+bMmYM777wTP/zwQ6eOq7mfk+effx4DBgzAnj17oFar7Z4rKCjojOERdWksSxF1oClTpmDQoEE4ePAgJk2aBB8fH/zlL38BAKxfvx7XXXcdoqOjodFokJSUhBdffBFGo9HuGo17bi5cuABBEPC3v/0NH3zwAZKSkqDRaDB69Gjs37/f7rWOem4EQcCiRYuwbt06DBo0CBqNBgMHDsSPP/7YZPxbt27FqFGjoNVqkZSUhPfff7/d+3i++uorjBw5Et7e3ggNDcUdd9yBS5cu2Z2Tl5eHu+66C7169YJGo0FUVBRmz56NCxcuyOccOHAA06dPR2hoKLy9vZGYmIi777672feura3F66+/jr59+2LZsmVNnp81axYWLFiAH3/8EXv27AEAXH/99ejdu7fD66WmpmLUqFF2xz777DP58wUHB+O2225Ddna23TnN/Zw4cu7cOYwePbpJYAMA4eHhAMw/J2FhYQCAF154QS6T2mZwTp8+jZtvvhnBwcHQarUYNWoUvv32W7vrrVy5EoIgYPv27fjjH/+IkJAQ+Pv7Y/78+SgtLbU7ty1/BkQdgf+kI+pgxcXFmDlzJm677Tbccccdcnlj5cqV8PPzw2OPPQY/Pz/8/PPPePbZZ1FRUWGXQXBm1apVqKysxB//+EcIgoDXXnsNN954I86fP99itmfHjh1Ys2YNHnroIeh0Ovzzn//ETTfdhKysLISEhAAADh8+jBkzZiAqKgovvPACjEYjli5dKv/CbA8rV67EXXfdhdGjR2PZsmXIz8/HP/7xD+zcuROHDx9GYGAgAOCmm27CiRMn8Kc//QkJCQkoKCjApk2bkJWVJX9/zTXXICwsDE8//TQCAwNx4cIFrFmzpsX7UFpaikceecRphmv+/Pn4+OOPsWHDBowbNw633nor5s+fj/3792P06NHyeZmZmdizZ4/dn93LL7+MJUuW4JZbbsG9996LwsJCvP3225g0aZLd5wOc/5w4Eh8fjy1btuDixYvo1auXw3PCwsKwfPlyPPjgg5g7dy5uvPFGAMCQIUMAACdOnMCECRMQExODp59+Gr6+vli9ejXmzJmDb775BnPnzrW73qJFixAYGIjnn38eaWlpWL58OTIzM7F161YIgtDmPwOiDiESUbtYuHCh2Ph/qcmTJ4sAxBUrVjQ5v6ampsmxP/7xj6KPj49YV1cnH1uwYIEYHx8vf5+RkSECEENCQsSSkhL5+Pr160UA4v/+9z/52HPPPddkTABEtVotnj17Vj529OhREYD49ttvy8dmzZol+vj4iJcuXZKPpaeniyqVqsk1HVmwYIHo6+vr9PmGhgYxPDxcHDRokFhbWysf37BhgwhAfPbZZ0VRFMXS0lIRgPj66687vdbatWtFAOL+/ftbHJett956SwQgrl271uk5JSUlIgDxxhtvFEVRFMvLy0WNRiM+/vjjdue99tproiAIYmZmpiiKonjhwgVRqVSKL7/8st15x44dE1Uqld3x5n5OHPnXv/4l/zleeeWV4pIlS8Rff/1VNBqNducVFhaKAMTnnnuuyTWmTp0qDh482O5nzWQyiePHjxf79OkjH/v4449FAOLIkSPFhoYGu88LQFy/fr0oim3/MyDqCCxLEXUwjUaDu+66q8lxb29v+evKykoUFRVh4sSJqKmpwenTp1u87q233oqgoCD5+4kTJwIAzp8/3+Jrp02bhqSkJPn7IUOGwN/fX36t0WjE5s2bMWfOHERHR8vnJScnY+bMmS1e3xUHDhxAQUEBHnroIbuG5+uuuw4pKSn47rvvAJjvk1qtxtatW5uUQSRSBmTDhg3Q6/Uuj6GyshIAoNPpnJ4jPVdRUQEA8Pf3x8yZM7F69WqINvMxvvzyS4wbNw5xcXEAgDVr1sBkMuGWW25BUVGR/IiMjESfPn3kpl+Js58TR+6++278+OOPmDJlCnbs2IEXX3wREydORJ8+fbBr164WX19SUoKff/4Zt9xyi/yzV1RUhOLiYkyfPh3p6elNSoP333+/XUbwwQcfhEqlwvfffw+g7X8GRB2BwQ1RB4uJiXHYG3HixAnMnTsXAQEB8Pf3R1hYmNyMXF5e3uJ1pV+iEinQcRYANPda6fXSawsKClBbW4vk5OQm5zk61haZmZkAgH79+jV5LiUlRX5eo9Hg1VdfxQ8//ICIiAhMmjQJr732GvLy8uTzJ0+ejJtuugkvvPACQkNDMXv2bHz88ceor69vdgxS4CIFOY44CoBuvfVWZGdnY/fu3QDMPTAHDx7ErbfeKp+Tnp4OURTRp08fhIWF2T1OnTrVpPHX2c+JM9OnT8fGjRtRVlaG7du3Y+HChcjMzMT111/fYlPx2bNnIYoilixZ0mRszz33HICmjcl9+vSx+97Pzw9RUVFy31Nb/wyIOgJ7bog6mG2GRlJWVobJkyfD398fS5cuRVJSErRaLQ4dOoSnnnoKJpOpxesqlUqHx0UXVne4nNe6w6OPPopZs2Zh3bp12LhxI5YsWYJly5bh559/xvDhwyEIAr7++mvs2bMH//vf/7Bx40bcfffdeOONN7Bnzx6n6+30798fAPDbb79hzpw5Ds/57bffAAADBgyQj82aNQs+Pj5YvXo1xo8fj9WrV0OhUOB3v/udfI7JZIIgCPjhhx8c3u/GY3L0c+IKHx8fTJw4ERMnTkRoaCheeOEF/PDDD1iwYIHT10g/X0888QSmT5/u8JzWBrFt/TMg6ggMbojcYOvWrSguLsaaNWswadIk+XhGRoYbR2UVHh4OrVaLs2fPNnnO0bG2iI+PBwCkpaXhqquusnsuLS1Nfl6SlJSExx9/HI8//jjS09MxbNgwvPHGG3brDY0bNw7jxo3Dyy+/jFWrVmHevHn44osvcO+99zocwxVXXIHAwECsWrUKzzzzjMMg5JNPPgFgniUl8fX1xfXXX4+vvvoKf//73/Hll19i4sSJdiW8pKQkiKKIxMRE9O3bt5V3p22kmVq5ubkA4HRWmzTby8vLC9OmTXPp2unp6bjyyivl76uqqpCbm4trr73W7rzW/hkQdQSWpYjcQPolapspaWhowHvvveeuIdlRKpWYNm0a1q1bh5ycHPn42bNn2229l1GjRiE8PBwrVqywK1388MMPOHXqFK677joA5vVe6urq7F6blJQEnU4nv660tLRJ1mnYsGEA0GxZxMfHB0888QTS0tLwzDPPNHn+u+++w8qVKzF9+nSMGzfO7rlbb70VOTk5+Oijj3D06FG7khQA3HjjjVAqlXjhhReajE0URRQXFzsdV0u2bNni8LjU/yKV+qR1csrKyuzOCw8Px5QpU/D+++/LgZCtwsLCJsc++OADu16a5cuXw2AwyD1Ybf0zIOoIzNwQucH48eMRFBSEBQsW4OGHH4YgCPj000+7VFno+eefx08//YQJEybgwQcfhNFoxDvvvINBgwbhyJEjLl1Dr9fjpZdeanI8ODgYDz30EF599VXcddddmDx5Mm6//XZ5KnhCQgL+/Oc/AwDOnDmDqVOn4pZbbsGAAQOgUqmwdu1a5Ofn47bbbgMA/Oc//8F7772HuXPnIikpCZWVlfjwww/h7+/fJLPQ2NNPP43Dhw/j1Vdfxe7du3HTTTfB29sbO3bswGeffYb+/fvjP//5T5PXXXvttdDpdHjiiSegVCpx00032T2flJSEl156CYsXL8aFCxcwZ84c6HQ6ZGRkYO3atbj//vvxxBNPuHQfG5s9ezYSExMxa9YsJCUlobq6Gps3b8b//vc/jB49GrNmzQJgLnUNGDAAX375Jfr27Yvg4GAMGjQIgwYNwrvvvosrrrgCgwcPxn333YfevXsjPz8fu3fvxsWLF3H06FG792xoaJD/HNLS0vDee+/hiiuuwA033HDZfwZE7c49k7SIPI+zqeADBw50eP7OnTvFcePGid7e3mJ0dLT45JNPihs3bhQBiL/88ot8nrOp4I6mRqPRtF9nU8EXLlzY5LXx8fHiggUL7I5t2bJFHD58uKhWq8WkpCTxo48+Eh9//HFRq9U6uQtWCxYsEAE4fCQlJcnnffnll+Lw4cNFjUYjBgcHi/PmzRMvXrwoP19UVCQuXLhQTElJEX19fcWAgABx7Nix4urVq+VzDh06JN5+++1iXFycqNFoxPDwcPH6668XDxw40OI4RVEUjUaj+PHHH4sTJkwQ/f39Ra1WKw4cOFB84YUXxKqqKqevmzdvnghAnDZtmtNzvvnmG/GKK64QfX19RV9fXzElJUVcuHChmJaWJp/T3M+JI//973/F2267TUxKShK9vb1FrVYrDhgwQHzmmWfEiooKu3N37doljhw5UlSr1U1+Ps6dOyfOnz9fjIyMFL28vMSYmBjx+uuvF7/++mv5HGkq+LZt28T7779fDAoKEv38/MR58+aJxcXF8nmX+2dA1J64txQRtcqcOXNw4sQJpKenu3so1AmkhRb379/fZPVloq6KPTdE5FRtba3d9+np6fj+++8xZcoU9wyIiMgF7LkhIqd69+6NO++8E71790ZmZiaWL18OtVqNJ5980t1DIyJyisENETk1Y8YM/Pe//0VeXh40Gg1SU1Px17/+tcmCbkREXQl7boiIiMijsOeGiIiIPAqDGyIiIvIoPa7nxmQyIScnBzqdzunS5ERERNS1iKKIyspKREdHQ6FoPjfT44KbnJwcxMbGunsYRERE1AbZ2dno1atXs+f0uOBGp9MBMN8cf39/N4+GiIiIXFFRUYHY2Fj593hzelxwI5Wi/P39GdwQERF1M660lLChmIiIiDwKgxsiIiLyKAxuiIiIyKP0uJ4bIiJyD5PJhIaGBncPg7owtVrd4jRvVzC4ISKiDtfQ0ICMjAyYTCZ3D4W6MIVCgcTERKjV6su6DoMbIiLqUKIoIjc3F0qlErGxse3yL3PyPNIiu7m5uYiLi7ushXYZ3BARUYcyGAyoqalBdHQ0fHx83D0c6sLCwsKQk5MDg8EALy+vNl+H4TMREXUoo9EIAJddaiDPJ/2MSD8zbcXghoiIOgX386OWtNfPCIMbIiIi8igMboiIiDpJQkIC3nrrLZfP37p1KwRBQFlZWYeNyRMxuCEiImpEEIRmH88//3ybrrt//37cf//9Lp8/fvx45ObmIiAgoE3v5ypPC6I4W6qT1BuMUCkUUCpYcyYi6upyc3Plr7/88ks8++yzSEtLk4/5+fnJX4uiCKPRCJWq5V+pYWFhrRqHWq1GZGRkq15DzNx0ijq9EVNe34pb3t/t7qEQEZELIiMj5UdAQAAEQZC/P336NHQ6HX744QeMHDkSGo0GO3bswLlz5zB79mxERETAz88Po0ePxubNm+2u27gsJQgCPvroI8ydOxc+Pj7o06cPvv32W/n5xhmVlStXIjAwEBs3bkT//v3h5+eHGTNm2AVjBoMBDz/8MAIDAxESEoKnnnoKCxYswJw5c9p8P0pLSzF//nwEBQXBx8cHM2fORHp6uvx8ZmYmZs2ahaCgIPj6+mLgwIH4/vvv5dfOmzcPYWFh8Pb2Rp8+ffDxxx+3eSyuYHDTCbJLapBbXoeDmaXQG7k6JxH1bKIooqbB4JaHKIrt9jmefvppvPLKKzh16hSGDBmCqqoqXHvttdiyZQsOHz6MGTNmYNasWcjKymr2Oi+88AJuueUW/Pbbb7j22msxb948lJSUOD2/pqYGf/vb3/Dpp59i+/btyMrKwhNPPCE//+qrr+Lzzz/Hxx9/jJ07d6KiogLr1q27rM9655134sCBA/j222+xe/duiKKIa6+9Fnq9HgCwcOFC1NfXY/v27Th27BheffVVObu1ZMkSnDx5Ej/88ANOnTqF5cuXIzQ09LLG0xKWpTpBaY3e+nV1A8L9tW4cDRGRe9XqjRjw7Ea3vPfJpdPho26fX31Lly7F1VdfLX8fHByMoUOHyt+/+OKLWLt2Lb799lssWrTI6XXuvPNO3H777QCAv/71r/jnP/+Jffv2YcaMGQ7P1+v1WLFiBZKSkgAAixYtwtKlS+Xn3377bSxevBhz584FALzzzjtyFqUt0tPT8e2332Lnzp0YP348AODzzz9HbGws1q1bh9/97nfIysrCTTfdhMGDBwMAevfuLb8+KysLw4cPx6hRowCYs1cdjZmbTlBaY90orqSGm8YREXkC6Ze1pKqqCk888QT69++PwMBA+Pn54dSpUy1mboYMGSJ/7evrC39/fxQUFDg938fHRw5sACAqKko+v7y8HPn5+RgzZoz8vFKpxMiRI1v12WydOnUKKpUKY8eOlY+FhISgX79+OHXqFADg4YcfxksvvYQJEybgueeew2+//Saf++CDD+KLL77AsGHD8OSTT2LXrl1tHourmLnpBOU2mZuSKgY3RNSzeXspcXLpdLe9d3vx9fW1+/6JJ57Apk2b8Le//Q3Jycnw9vbGzTff3OJO6I23GRAEodkNRh2d357ltra49957MX36dHz33Xf46aefsGzZMrzxxhv405/+hJkzZyIzMxPff/89Nm3ahKlTp2LhwoX429/+1mHjYeamE9hmboqrGdwQUc8mCAJ81Cq3PDpyleSdO3fizjvvxNy5czF48GBERkbiwoULHfZ+jgQEBCAiIgL79++XjxmNRhw6dKjN1+zfvz8MBgP27t0rHysuLkZaWhoGDBggH4uNjcUDDzyANWvW4PHHH8eHH34oPxcWFoYFCxbgs88+w1tvvYUPPvigzeNxBTM3ncC256aEwQ0RkUfq06cP1qxZg1mzZkEQBCxZsqTZDExH+dOf/oRly5YhOTkZKSkpePvtt1FaWupSYHfs2DHodDr5e0EQMHToUMyePRv33Xcf3n//feh0Ojz99NOIiYnB7NmzAQCPPvooZs6cib59+6K0tBS//PIL+vfvDwB49tlnMXLkSAwcOBD19fXYsGGD/FxHYXDTCcprmbkhIvJ0f//733H33Xdj/PjxCA0NxVNPPYWKiopOH8dTTz2FvLw8zJ8/H0qlEvfffz+mT58OpbLlktykSZPsvlcqlTAYDPj444/xyCOP4Prrr0dDQwMmTZqE77//Xi6RGY1GLFy4EBcvXoS/vz9mzJiBN998E4B5rZ7FixfjwoUL8Pb2xsSJE/HFF1+0/we3IYjuLtR1soqKCgQEBKC8vBz+/v6d8p4PfHoQP57IAwDcMS4OL80Z3CnvS0TUFdTV1SEjIwOJiYnQajlbtLOZTCb0798ft9xyC1588UV3D6dZzf2stOb3NzM3ncButhQzN0RE1IEyMzPx008/YfLkyaivr8c777yDjIwM/P73v3f30DoNG4o7QRl7boiIqJMoFAqsXLkSo0ePxoQJE3Ds2DFs3ry5w/tcuhJmbjpBWS0zN0RE1DliY2Oxc+dOdw/DrZi56WCiKHK2FBERUSdicNPBavVGNBisUwFLa/QwmXpUDzcREQC4faE56vra62eEwU07qao34GBmCfacL7Y7LmVtVArz+gJGk4jyWn2T1xMReSppCnJLK/USST8jrkxbbw57btrJb9ll+P1He5EU5ostj0+Rj5dZZkoF+apRpzeiss6A4uoGBPmq3TRSIqLOpVKp4OPjg8LCQnh5eUGh4L+rqSmTyYTCwkL4+PhApbq88ITBTTuJCDDPx8+vqLc7Ls2UCvLxQoNBico6A/tuiKhHEQQBUVFRyMjIQGZmpruHQ12YQqFAXFzcZW+T4dbgZvny5Vi+fLm898bAgQPx7LPPYubMmQ7PX7lyJe666y67YxqNBnV1dR091BZF+puDm6p6A6rqDfDTmG+ttMZNoLcaBpMJF4prGNwQUY+jVqvRp08flqaoWWq1ul0ye24Nbnr16oVXXnkFffr0gSiK+M9//oPZs2fj8OHDGDhwoMPX+Pv7Iy0tTf6+IzdBaw1fjQo6jQqV9QbkldchOdwPgLXnJtDHC1IfcePg5rM9mag3mHDPFYmdOmYios6kUCi4QjF1CrcGN7NmzbL7/uWXX8by5cuxZ88ep8GNIAiIjIzsjOG1WkSAFpUFVcivsAY35VLPjY8aIszRTUm1tXRVVW/AkvXHIYrAdYOjEBnQ8f/jF1TW4acT+ZgzPEbOMBEREXmKLtPVZTQa8cUXX6C6uhqpqalOz6uqqkJ8fDxiY2Mxe/ZsnDhxotnr1tfXo6Kiwu7RUaTSVF65tUwmZ258vRDsqwFgv3nmhaJqSDPfTuV1zgZr7/x8Fv+37ji+OpDdKe9HRETUmdwe3Bw7dgx+fn7QaDR44IEHsHbtWgwYMMDhuf369cO///1vrF+/Hp999hlMJhPGjx+PixcvOr3+smXLEBAQID9iY2M76qMgQgpuKmyDG2vPTYhlhpRtWSqzuEb++nRuZYeNzdYFy3vaBmFERESewu3BTb9+/XDkyBHs3bsXDz74IBYsWICTJ086PDc1NRXz58/HsGHDMHnyZKxZswZhYWF4//33nV5/8eLFKC8vlx/Z2R2XrYgMMGdm8m2Cm3Kb2VLBDoKbC8XV8tenOylzU2AZH9fbISIiT+T2hgu1Wo3k5GQAwMiRI7F//3784x//aDZgkXh5eWH48OE4e/as03M0Gg00Gk27jbc5jstSlsyNjxoaL3MsaZ+5sQluOilzk8/ghoiIPJjbMzeNmUwm1NfXt3wizH06x44dQ1RUVAePyjVSWco2c1NmM1sq2MdR5sZaljpXWGW3VUNHqDcY5T4g293KiYiIPIVbMzeLFy/GzJkzERcXh8rKSqxatQpbt27Fxo0bAQDz589HTEwMli1bBgBYunQpxo0bh+TkZJSVleH1119HZmYm7r33Xnd+DJk008lRz02Qjxo+avNy0sXVDRBFEYIg2GVuDCYR5wqr0D/Kv8PGWGCzyCAzN0RE5IncGtwUFBRg/vz5yM3NRUBAAIYMGYKNGzfi6quvBgBkZWXZLeZTWlqK++67D3l5eQgKCsLIkSOxa9cupw3InU0qSxVW1sNgNEEhCHIAEeTjBT+t+XY3GEyobjBCIVhXNO4b4Ycz+VU4nVfRscFNpU0/EIMbIiLyQG4Nbv71r381+/zWrVvtvn/zzTfx5ptvduCILk+InwZKhQCjSURRVQO8vZTywn0BPl7QqJTQeilQpzehpKoBNXqD+TlvL4zrHWIObnIrgeEdN8Z8Zm6IiMjDdbmem+5MqRAQrjM3L+dV1MklKR+1EhqVuSQVIq91U48LReZ+m4QQH6REmrM1p/I6tqnYth+oqt4Ag7Fje3yIiIg6G4ObdhZhM2PKtt9GYjsdXOq3iQ/xRUqUDgBwOrdjp4M33tizos7Qoe9HRETU2RjctLNImxlTZbXWmVKSIJvgRpoplRDig34R5uCmoLIexVWuzRZri4IK+4X7ymq4iR0REXkWBjftzHbGVJmDzE2Ik8yNr0aF+BAfAEBaB5am8ivtgxv23RARkadhcNPO5LVuyutQWm0OHAJsMjf2ZSlL5ibUHNSkRJqzNx3Zd9O4LMXghoiIPA2Dm3YmbcGQZ1OWCnIQ3OSW1yGnvBaAOXMDQG4q7si+G6mhWGp8ZnBDRESehsFNO7PdPLO5stSR7DKIIuCnUcnH+luaitPyOyZzU9NgQKWlgbivpceHwQ0REXkaBjftLNJutpSlLOXdNHOTVWIuScWH+EAQBADWzE1aXiWM0gI57UhandjbS4leQd4ArBt7EhEReQoGN+1MaiiuaTAi2xLAOJoKLkkI9ZW/jgv2gbeXEvUGk91u4e1FKklF+GvkPqAyZm6IiMjDMLhpZz5qFXSWbRbOWMpLQb5NMzeSBMsMKQBQKAT0i5TWu2n/0lR+pTlzE+6vlbNJLEsREZGnYXDTAaTSVE2DEQAQaNdzo7E7V2omlkh9N6c6oKm4QM7cMLghIiLPxeCmA0ilKUmgTc+Nv7cKKoUgf5/QKLgZHBMIANhxtqjdxyWXpXQaBHqbAy4GN0RE5GkY3HQAacaUxLbnRhAEeZViwL4sBQDTBoRDEMyzqS6V1bbruKQ1buwyN2woJiIiD8PgpgNE2gQ3ggD422RuAOt0cG8vJcJ09mWqcJ0WoxOCAQA/HMtt13HJa9z4a1iWIiIij8XgpgNE2JSlAry9oLQpQwHWTI7tNHBb1w2OAgB83yi4Ka1uwOI1v2FXG0tWBZXWzI2031Xj4Gbn2SIMW/pTk/cmIiLqLhjcdADbzE1go6wNAAT7mYObxv02khmDIiEIwKGsMuSWW0tTr208jf/uy8ajXx5Bnd7YqjGJomgzFVwrZ5Nq9UbUG6zX2ngiD2U1evxjczpEsf3X2iEiIupoDG46gF1w46N2+nxSuOPgJsJfi1HxQQCAH47lAQDOFlThy/3ZAMwZmM/2ZLZqTFX1Bnn2VrhOA51GBSlpZJu9uVRqDqbS8itxsgO3gSAiIuooDG46QESAtY/Gdl8pyd1XJOJPVyVjwfgEp9eYOchcmvrhuLk89PrG0zCJQKif+dortp1DTYPB5TFJzcQ6jQq+GhUUCkHuu6mwDW5smpi/OXjJ5esTERF1FQxuOkCor0ae7h3kIHMTE+iNx6/ph3CdtslzkpmDIwEABzJL8ePxXGw8kQ+FAHxy9xjEBfugqKoBn+x2PXtTUGltJpZIwU2ZzYypHJvg5tujl6A3mlx+DyIioq6AwU0HUCgEedftAAeZG1dEBXhjRFwgRBF45IsjAICbR/bCgGh/PDy1DwDg/W3nUFXvWvamwGYauKTxjKnKOj0qLBtrBvp4oaiqAb+mF7Zp/ERERO7C4KaDSDOmHGVuXHWtZdZUvcEEjUqBR6f1BQDMGRaN3qG+KK3R4+MdGS5dy7aZWNI4uMkpq5OP3zi8FwCWpoiIqPthcNNBEi0zoaIDvdt8jZmW4AYA7pyQIF9LpVTgkWnm7M2Kbedw98r9eOjzg3h89VGn08SlnpvmylJSSSom0Bs3jogBAGw6lc+F/oiIqFthcNNBnpyRgldvGozrh0S1fLITMYHeuH1MHIbFBuKhycl2z10/JBopkTpUNxjx8+kCfH8sD98cuoiFqw45bDTOr5S2XnCeubloCW6iA70xMNofKZE6NBhM2HAsp82fgYiIqLOp3D0ATxUZoMWto+Mu+zrLbhzs8LhSIeDTe8Ziz/li1DYYUas34sNfz+NiaS2+3J+NuyYk2p1f4FJZSsrcaCEIAm4cEYO/fn8aaw5dwryx8Zf9WYiIiDoDMzfdWJhOg1lDo3HL6FgsGJ+AB6ckAQA+3H6+ySwn675S1rJU41WK5eAmyFz+mjMsBgoBOJhZirMFlR37YYiIiNoJgxsPctOIXgjTaZBTXodvj1hLSY1XJ5Y4y9xIvT3h/lpM6x8BAPh454UOHz8REVF7YHDjQbReStxzhbkctXzbOZhM5u0Tymv1qDeYMzm2G3U2Dm6k1Yltm6Dvtlzvm0MXUVbT0MGfgIiI6PIxuPEw88bGQadV4WxBFTafykdGUTUWfLwfgHnbB62XUj43wNs8Tb2spgEGowl5luxOjE1wMzYxGAOi/FGnN+G/+7I78ZMQERG1DYMbD6PTeuEP48zNv0s3nMS1//gVR7PL4K9V4a83DrI715q5MSC/sh4mEfBSCgjzs2Z3BEGQszef7L7AFYuJiKjLY3Djge6akAi1SoGLpbWo1RsxPikEPz46CVelRNidJ62eXFGrl0tSUQHeUFi2jpDMGhqFUD81csvr8OPxvM75EERERG3E4MYDhek0eHRaHwT7qvF/1/XHZ/eMdbiYYKAlc9NgNOFsQRUAIDqw6X5XGpUSd1iyQf/e6dqKyERERO7C4MZDPTQlGYeWXI17J/ZukomR+KiV8gafp3IrAAAxgT4Oz503Nh5qpQKHs8pwKKu0YwZNRETUDhjc9GCCIMh9Nyfl4MbxTuVhOg1uGBYNAPhk14VOGR8REVFbMLjp4aS+m9OW4Ka5vbCk0tSPJ/Jc3o2ciIioszG46eGkzE11gxGAdXViR4b2CkDvUF/U6U3YyMZiIiLqohjc9HBScCNpLnMjCALmDjfvFr728KUOHRcREVFbMbjp4QIbBzcBzoMbAJhjCW52nitCXnldh42LiIiorRjc9HC2mZtgXzW81cpmzgZig30wOiEIogisP8LsDRERdT0Mbno42+AmppmSlK25w3sBYGmKiIi6JgY3PVyAj1r+2tECfo5cNzgKaqUCp/Mq5fVxiIiIugq3BjfLly/HkCFD4O/vD39/f6SmpuKHH35o9jVfffUVUlJSoNVqMXjwYHz//fedNFrPZJu5aa6Z2O41Pl64KiUcALM3RETU9bg1uOnVqxdeeeUVHDx4EAcOHMBVV12F2bNn48SJEw7P37VrF26//Xbcc889OHz4MObMmYM5c+bg+PHjnTxyz9GWshQAzB1hbixed/gSDmaWot5gbPexERERtYUgiqLo7kHYCg4Oxuuvv4577rmnyXO33norqqursWHDBvnYuHHjMGzYMKxYscKl61dUVCAgIADl5eXw9/dvt3F3V/svlOB3K3YDAJbPG4GZg6Ncel2DwYQxf92Msho9AECtVGBAtD8WXpmMqwdEtPBqIiKi1mnN7+8u03NjNBrxxRdfoLq6GqmpqQ7P2b17N6ZNm2Z3bPr06di9e7fT69bX16OiosLuQVZtKUsBgFqlwFu3DsO0/uEI8VWjwWjCkewyPPT5Qew9X9wRQyUiInKJyt0DOHbsGFJTU1FXVwc/Pz+sXbsWAwYMcHhuXl4eIiLsswIRERHIy3O+Wu6yZcvwwgsvtOuYPYldWaqZ1YkdmdIvHFP6hUMURWSX1OKVH0/h+2N5eOCzg1i/8ArEhTjehJOIiKgjuT1z069fPxw5cgR79+7Fgw8+iAULFuDkyZPtdv3FixejvLxcfmRnZ7fbtT1BmJ8GQ3sFYExCMEJ81S2/wAFBEBAX4oO/3zIMQ3oFoLRGj3s/2Y/KOn07j5aIiKhlbg9u1Go1kpOTMXLkSCxbtgxDhw7FP/7xD4fnRkZGIj8/3+5Yfn4+IiMjnV5fo9HIs7GkB1kpFALWLZyAL/84DoIgXNa1tF5KfDh/FCL8NTiTX4VHvjgCo6lLtXQREVEP4PbgpjGTyYT6+nqHz6WmpmLLli12xzZt2uS0R4dcIwjCZQc2kgh/LT6cPwoalQI/ny7A98dy2+W6RERErnJrcLN48WJs374dFy5cwLFjx7B48WJs3boV8+bNAwDMnz8fixcvls9/5JFH8OOPP+KNN97A6dOn8fzzz+PAgQNYtGiRuz4COTCkVyDun9QbANfBISKizufW4KagoADz589Hv379MHXqVOzfvx8bN27E1VdfDQDIyspCbq71X/7jx4/HqlWr8MEHH2Do0KH4+uuvsW7dOgwaNMhdH4GckDbY3HamEMVVjjNx7a2oqh4mlsGIiHq8LrfOTUfjOjed54Z3duC3i+V44YaBWDA+waXXGE0iTuSUY1B0ABQK10tlh7NKcePyXbhjbDxenMNgl4jI03TLdW7I88weZlnFuBW7h6/Ydg43vLMTn+y+0Kr3OphZClEEtqcXtup1RETkeRjcUIeZNTQKCgE4nFWGC0XV8vGPfj2PcX/dguOXypu8ZsNv5jLk9vSiVr1XTlkdACCzuAbV9YbLGDUREXV3DG6ow4TrtJiQHAoAWH8kBwCw+WQ+XvruFPIq6vDp7ky78wsq6+Rdxn+7WIbWVExzymrlr9PyKy936ERE1I0xuKEONXe4tTR1vrAKf/7yiPzcltP5duvg/HrGmq0pqmpAbnmdy++TU24Nbk7nMrghIurJGNxQh7pmYCS0XgpkFFXjtg/2oLLegFHxQdBpVSiqasDhrFL53G1n7PtlfrtY5vL7XCq1CW7yuH8YEVFPxuCGOpSfRoVrBphXkC6orEeEvwbv3TECV6WEAwA2nTSvOG0yidhx1py5SQ73AwAcvdi0J8eROr0RxdUN8vfM3BAR9WwMbqjDSaUpL6WA9+aNRLhOi6sHmDdA/elkPkRRxPGccpRUN8BPo8L81HgAwDEXgxvbfhsAOJVX0ap+HSIi8ixu3xWcPN+UfmF4btYAJIf7YWR8EABgct8wqJXmctW5wipst5SkxieFYESc+RypqbilrSGkmVKJob7ILqlBZZ0BOeV1iAls3S7nRETkGZi5oQ4nCALumpCIiX3C5GM6rRdSk0IAmLM32y3NxJP6hqFfpA5qlQIVdQZkFte0eP1LZeZz4kN85JLWqRz23RAR9VQMbshtpNLU+sM5OGRpLJ7cNwxeSgUGRJlXnzzqQlPxJUvmJjrQG/0tr2NTMRFRz8XghtxGCm7S8ithMIlIDPVFbLAPAGBorwAAwG8u9N1IPTcxgd5IidQBAE7lsamYiKinYnBDbhPhr8XQ2ED5+0l9QuWvB/cyH3elqdguuJEyN7nM3BAR9VQMbsitrrFkbwBgcj9rT46UuTmeU2630J8jlyzBTXSgN/pbMjcZRdWo0xvbe7hERNQNMLght5o+0BzcaL0UGNc7RD7eO8wPvmolahqMOFdY5fT1JpOIXLnnRoswnQbBvmqYRCA93/nriIjIczG4IbdKDtdhxR0j8O8Fo+Gjtq5MoFQIGBhjzt4czS4DAJTX6rF6fzaKqurl84qq69FgNEEhAJH+WgiCgP5RUt8NS1NERD0RgxtyuxmDojA+ObTJcdum4h3pRZjx1nY8+c1veOWH0/I50ho3kf5aqJTmH+eUSKnvhk3FREQ9ERfxoy5riKWpeM2hi/h0j3UH8Z1ni+TF/aQ9paJtFuyTZ0yxqZiIqEdi5oa6rCGWzE11g7kx+Pdj4+ClFJBbXoesEvPCfTllTYMb27VuuA0DEVHPw+CGuqy4YB+MiAtETKA3Prl7DP46d7Cczdl7vgSAdaZUTJA1uEkO94NSIaC0Ro+Cyvom123so1/Pyxt4EhFR98fghrosQRDw9QPjseOpKzGpr3ma+LjewQCAPeeLATjO3Gi9lEgM9QUApLWwmF96fiVe+u4Unvz6aLuPn4iI3IPBDXVpCoVgt3Hm2ETzdPG9GSUQRdGauQnU2r0uzrLSceMdwxuTppmX1ui5Lg4RkYdgcEPdysj4IKgUAi6V1eJiaa3DzA0ARAWYg52WgpsLNhtzFrpQwiIioq6PwQ11K74aFQZbGo1/SStAaY0egHnrBVtSsJNTXtfs9TKLq+WvbdfPISKi7ovBDXU70krGaw5dAgDotCrotF5250jBTkuZm4wi2+CmoT2HSUREbsLghrqdsYnmpuIjlpWLG2dtANfLUpk2ZSlmboiIPAODG+p2RiUEQ6mwNhk7Cm5sy1LO1rqp0xuRa1O2Ys8NEZFnYHBD3Y6fRoVBln2ngKbNxAAQGaCFIAANBhOKqx2Xm6SFACXM3BAReQYGN9QtSevdAI6DGy+lAuE6DQDIu4Y3dsGm3wZgcENE5CkY3FC3NM6y3g0ARDda48Z63Bz0XHLSd3PBMlNKrTL/b1BUyYZiIiJPwOCGuqVRCUGQ2m56BTXN3ABAdEDzM6akNW6GWbZ0KGTmhojIIzC4oW5Jp/XCfRN7Y2KfULv+G1tSRie33HFwI61xMyohCABQxIZiIiKPoHL3AIjaavG1/Zt9PkrO3DjruTFnbqTgprLegDq9EVovZTuOkoiIOhszN+SxrNPBm2Zu6g1G+figmAC574bTwYmIuj8GN+SxmlulOLukFqII+KqVCPPTIMzPPLOKM6aIiLo/BjfksaIsPTcFlfVoMJjsnpOmgSeE+kIQBIT6qQFwCwYiIk/A4IY8VoivGmqVAqII5FfY991I08ATQnwBAKHM3BAReQwGN+SxBEFwWpqS9pSKD/EBAIRZFvxjzw0RUffH4IY8mryBZqOmYmZuiIg8F4Mb8mjyjKlG08GlzE1CqBTcSD03DG6IiLo7twY3y5Ytw+jRo6HT6RAeHo45c+YgLS2t2desXLkSgiDYPbRax8vvE0VLmRubslSDwYSLpZbgRi5Lmc9jWYqIqPtza3Czbds2LFy4EHv27MGmTZug1+txzTXXoLq6utnX+fv7Izc3V35kZmZ20oipu5EyN7nl1szNxdIamETA20sp99pwthQRkedw6wrFP/74o933K1euRHh4OA4ePIhJkyY5fZ0gCIiMjOzo4ZEHiHbQUGzbTCwI5g2qQi1BTlu3YBBFEcculaNPuA7eaq5wTETkTl2q56a8vBwAEBwc3Ox5VVVViI+PR2xsLGbPno0TJ044Pbe+vh4VFRV2D+o5pP2lbHcGl5qJEy39NoC1oVjagqG1fjqZjxve2YkXvzt5OcMlIqJ20GWCG5PJhEcffRQTJkzAoEGDnJ7Xr18//Pvf/8b69evx2WefwWQyYfz48bh48aLD85ctW4aAgAD5ERsb21EfgbogaX+pyjoDKuv0AGwzN9bgxl+ruqwtGA5llgIADl4ovazxEhHR5esywc3ChQtx/PhxfPHFF82el5qaivnz52PYsGGYPHky1qxZg7CwMLz//vsOz1+8eDHKy8vlR3Z2dkcMn7ooX40KgT5eAMx9N/UGI7amFQAAettkbgRBuKwtGM4WVAEAzhdVQW80tXA2ERF1pC6xK/iiRYuwYcMGbN++Hb169WrVa728vDB8+HCcPXvW4fMajQYajaY9hkndVFSAN8pq9LhUVovvj+XiQnENwnQaTB9k37cV6qfGpbLaNjUVp1uCG71RRGZxNZLDde0ydiIiaj23Zm5EUcSiRYuwdu1a/Pzzz0hMTGz1NYxGI44dO4aoqKgOGCF5ghhL382us0V475dzAIBnrx+AAG8vu/OkmVOtzdzU6Y3ItkwtB4Az+VWXM1wiIrpMbg1uFi5ciM8++wyrVq2CTqdDXl4e8vLyUFtrbf6cP38+Fi9eLH+/dOlS/PTTTzh//jwOHTqEO+64A5mZmbj33nvd8RGoG5D6bj7akYEGowmT+obh+iFNg2Gpqbi1PTfnC6shitbvz+RXtn2wRER02dxallq+fDkAYMqUKXbHP/74Y9x5550AgKysLCgU1histLQU9913H/Ly8hAUFISRI0di165dGDBgQGcNm7oZaTq4KAIalQIvzR4kTwG31dYtGM4W2mdq0htlbowmESu2ncP4pBAMjwtq1bWJiKj13BrciLb/3HVi69atdt+/+eabePPNNztoROSJpOngAPDw1D6Is6xK3Fhby1JnLZmaqAAtcsvrkF5gn7nZ8FsOXt+YhqGxgVi/cEKrrk1ERK3XZWZLEXWUQTEBUAhASqQO903s7fS8tpalpMzNDEuDckZRtd2MqT3nSwAA5wuqXAroiYjo8jC4IY+XFOaHTY9NxtcPjpfXsnGkrVswSNPAJ/UNg69aCb1RxIUi6xYiezOKAZgXCCyr0bd2+ERE1EoMbqhHSArzg5+m+SpsW7ZgMBhNyLAEMn3C/ZAcYZ4CLs2YKqisw/lCa6CTWVLT9CJERNSuGNwQWUg9N63ZgiGzpAZ6owhvLyWiA7zRN9wPgHXG1P4M+xWLM4ub3xSWiIguH4MbIgudpvVbMEglqaRwXygUAvpaMjdSU7FUkpJkM3NDRNThGNwQWTTegiGruAYLPz+E/1t3zGkjsBTc9LGsSNwnQsrcmI/vyzA3Ew+I8gdg3deKiIg6TpfYfoGoqwjVaXCprBbLt57D9vRC1OnNs57+dFUfRPhrm5x/zhLcJFvKUVLm5kJRNQoq6nA6z5zBuWlkL5zccJI9N0REnYCZGyIbYZYZUz+dzJcDGwBykNKYtKdUUpg5uIkK0EKnUcFgErH6gHmT1j7hfhgeFwiAZSkios7A4IbIRq8g8wJ/gT5eeP3mIZgx0Lx2zRkHwY3JJOJcoX3mRhAEJFtKU6v2ZgEAxiQGIz7YfN28ijqXm5WJiKhtWJYisrHoqmQkh/thxqBIhPqZS1Q/nshzmLnJrahDTYMRXkoB8TarHvcN1+FwVhlyyusAAGN7hyDYVw0/jQpV9QZcLK1xedfwD7afQ6C3GreMjm2fD0hE1AMwc0NkI9RPgzvGxcurFadEmoOQtPyKJudKzcQJIb7wUlr/V5KaiiVjE4MhCAJiLdkbV5uKs0tq8NfvT+OZdcdgNHFlYyIiVzG4IWpGv0jzLKf0/KomAUa6ZS0bqSQl6RNhzcokhPjIjchSaSrLxb4baa0cvVFEaU3rVk0mIurJGNwQNSMu2AdaLwXqDSZcaLQAX+N+G0lfm8zNmMRg+WupdOVq5kbKDAFAcSu3hCAi6skY3BA1Q2mzMF9ao76bswWOg5tIf/OMKQAYmxgiH49tZeZGCp6A1u9UTkTUkzG4IWpBP0twY9tUbDSJ8kJ9jYMbQRBw54QEDI0NxLT+EfJxKXPjanBjm7lhcENE5DrOliJqQT+pqTjP2lR8JLsM5bV66LQqeXViW49f0w+PX9PP7lh8sC8Ac3BjMolQKASn7ymKIstSRERtxMwNUQtSLE3FtmWpzafyAQBT+oXL+1G1JDpQC6VCQIPBhPzKumbPLayqR0WdQf6+uJqZGyIiVzG4IWqBlLnJLKlBTYM54Nh80hzcTOsf7vJ1VEoFYgK9AQBZLTQV22ZtAKCokpkbIiJXMbghakGYToMQXzVE0Twl/EJRNdILqqBSCJjSz/XgBrCZMdVC3825RsENMzdERK5jcEPkAmvfTaVckhrbOxgB3l6tuk6cNGOqhczNuULztPPEUHOfThF7boiIXMbghsgFUnBzOq8Sm+SSVERzL3FICm5aytxIZalxvc3r5HC2FBGR6xjcELlA2oZhb0YxDmSWAmhbcOPqdHApuJHWyeFsKSIi1zG4IXKBtA3DiZwKGE0iUiJ18qJ8rREnTQdvtNqxrco6PfIqzLOppBWOa/VGuZmZiIiax+CGyAV9I/wg2CxL05asDQDEWTI3pTV6VNTpHZ4j9duE+mkQFaCF1sv8vylnTBERuYbBDZELfNQquV8GAKYNaFtw46dRIcRXDcB5U7F1WwdfCIIg71BexBlTREQuYXBD5CJpG4ZwnQZDYgLafB1pBtT6I5ccPt94Q84QS3DDvhsiItcwuCFy0Yj4IADAjEGRzW6d0JJ7J/YGAHz4awZ+OJbb5Hk5cxNmDm5CLZkezpgiInIN95YictFdExIQFaDF1W0sSUlmDIrE/ZN644Pt5/H/vv4NfSJ0dptvnpPLUuZMUaicuWFwQ0TkijZlbrKzs3Hx4kX5+3379uHRRx/FBx980G4DI+pqNColZg+LgY/68v9N8OT0fhjXOxhV9QY88NlBVNebZ0I1GEzyGjhJ4ebyVYiflLlhWYqIyBVtCm5+//vf45dffgEA5OXl4eqrr8a+ffvwzDPPYOnSpe06QCJPpFIq8PbtIxDhr8HZgio8+PkhlFY34EJxNYwmEX4aFSL9tQCsPTcsSxERuaZNwc3x48cxZswYAMDq1asxaNAg7Nq1C59//jlWrlzZnuMj8lhhOg3emzcSaqUC288UYvpb27FqbxYAICnMPFMKAEItmRs2FBMRuaZNwY1er4dGY/7X5ObNm3HDDTcAAFJSUpCb27RBkogcGxkfhDUPjUdSmC8KKuuxctcFAECSTQ+O3HPDqeBERC5pU3AzcOBArFixAr/++is2bdqEGTNmAABycnIQEhLSrgMk8nSDYgKw4U8T8Ydx8fKxPpZmYoA9N0RErdWmzshXX30Vc+fOxeuvv44FCxZg6NChAIBvv/1WLlcRkeu81Uq8OGcQpvYPx6aT+fjdqF7yc1LmprSmAQajCSolV3AgImpOm4KbKVOmoKioCBUVFQgKCpKP33///fDxaf1+O0RkNqVfOKb0C7c7FuSjhiAAomjetiFMp3HT6IiIuoc2/ROwtrYW9fX1cmCTmZmJt956C2lpaQgPD2/h1UTUGkqFgGAfLuRHROSqNgU3s2fPxieffAIAKCsrw9ixY/HGG29gzpw5WL58ebsOkIhsF/Jj3w0RUUvaFNwcOnQIEydOBAB8/fXXiIiIQGZmJj755BP885//bNcBEpG1qZgzpoiIWtam4KampgY6nXk2x08//YQbb7wRCoUC48aNQ2ZmZrsOkIisC/kVVjK4ISJqSZuCm+TkZKxbtw7Z2dnYuHEjrrnmGgBAQUEB/P3923WARGSzkF81y1JERC1pU3Dz7LPP4oknnkBCQgLGjBmD1NRUAOYszvDhw12+zrJlyzB69GjodDqEh4djzpw5SEtLa/F1X331FVJSUqDVajF48GB8//33bfkYRN0GN88kInJdm4Kbm2++GVlZWThw4AA2btwoH586dSrefPNNl6+zbds2LFy4EHv27MGmTZug1+txzTXXoLq62ulrdu3ahdtvvx333HMPDh8+jDlz5mDOnDk4fvx4Wz4KUbcQ4mu/BYPJJOIP/9qLGW9tx/nCKncOjYioyxFEURQv5wLS7uC9evVq4cyWFRYWIjw8HNu2bcOkSZMcnnPrrbeiuroaGzZskI+NGzcOw4YNw4oVK1p8j4qKCgQEBKC8vJwlNOo2Np/Mx72fHMDQXgFYv+gK7DpbhN9/tBcAEOjjhY/mj8KohGA3j5KIqOO05vd3mzI3JpMJS5cuRUBAAOLj4xEfH4/AwEC8+OKLMJlMbRo0AJSXlwMAgoOd/yW9e/duTJs2ze7Y9OnTsXv3bofn19fXo6Kiwu5B1N003oLhv/uzAQBqlQJlNXr8/qO9+O437utGRAS0Mbh55pln8M477+CVV17B4cOHcfjwYfz1r3/F22+/jSVLlrRpICaTCY8++igmTJiAQYMGOT0vLy8PERERdsciIiKQl5fn8Pxly5YhICBAfsTGxrZpfETuZLt5Zkl1AzYeN/+8f37vWFw9IAINBhMWrjqE9UcuuXOYRERdQpuCm//85z/46KOP8OCDD2LIkCEYMmQIHnroIXz44YdYuXJlmwaycOFCHD9+HF988UWbXu/M4sWLUV5eLj+ys7Pb9fpEnUHK3NTpTfhsTyYajCYMivHH6IRgrLhjJH4/Ng4A8O4vZ3GZlWYiom6vTcFNSUkJUlJSmhxPSUlBSUlJq6+3aNEibNiwAb/88kuLvTuRkZHIz8+3O5afn4/IyEiH52s0Gvj7+9s9iLobH7UKPmolAODfOzMAALeONgc0SoWAp2akQK1U4Ex+FU7nVbptnEREXUGbgpuhQ4finXfeaXL8nXfewZAhQ1y+jiiKWLRoEdauXYuff/4ZiYmJLb4mNTUVW7ZssTu2adMmeTo6kaeSsjdlNXp4eykxe1i0/FyAtxeuTAkDAKw/kuOW8RERdRVt2hX8tddew3XXXYfNmzfLQcXu3buRnZ3dqjVnFi5ciFWrVmH9+vXQ6XRy30xAQAC8vb0BAPPnz0dMTAyWLVsGAHjkkUcwefJkvPHGG7juuuvwxRdf4MCBA/jggw/a8lGIuo1QPw2yS2oBANcNiYK/1svu+dnDYrDxRD7+dzQHT07vB4VCcMcwiYjcrk2Zm8mTJ+PMmTOYO3cuysrKUFZWhhtvvBEnTpzAp59+6vJ1li9fjvLyckyZMgVRUVHy48svv5TPycrKQm6udRbI+PHjsWrVKnzwwQcYOnQovv76a6xbt67ZJmQiTxDiq5G/vm1008b4q1LC4adR4VJZLQ5mldo9V6c3dvj4iIi6iste58bW0aNHMWLECBiNXfcvUq5zQ93V09/8hi/2ZyM53A+b/jwJgtA0M/P46qP45tBF3DEuDi/NGQwA+OlEHv7038O4Y1w8llw/oLOHTUTULjp8nRsi6nwTkkOhEIA/XZXsMLABIPfhfPdbLvRGE07lVuDRL4+g3mDC1wcvwmjiTCoi8nxt6rkhos43a2g0pg+MhFrl/N8k45NCEOqnRlFVA9YdvoS3NqejpsGcSS2v1ePoxTKMiAvqrCETEbkFMzdE3UhzgQ0AqJQKXD/EnL158pvfcKmsFgkhPpjYJxQAsDWtsMPHSETkbq3K3Nx4443NPl9WVnY5YyGidnDDsGis3HUBogjotCp8tGA0DmWW4tf0Imw7U4jHru7r7iESEXWoVgU3AQEBLT4/f/78yxoQEV2e4bGBSInUIb2gCm/fPhzJ4X7w05j/V//tYhlKqhsQbNllnIjIE7UquPn44487ahxE1E4EQcAX949DZZ0BscE+AIDIAC1SInU4nVeJX9MLMXtYjMPXNhhMOFdYhf5RnElIRN0Xe26IPFCgj1oObCST+5pXMN52xnnfzaNfHsbMf/yK7c2cQ0TU1TG4IeohJvczBzfbzxTC5GBK+OGsUnx/zLxK+MHM0ibPExF1FwxuiHqIUfHB8FErUVTVgJO5FU2e//umM/LXWSU1nTk0IqJ2xeCGqIdQqxQYn2SeEt64NLX3fDF+TS+Sv88sru7UsRERtScGN0Q9iFSa2maz3o0oinjjJ3PWZkRcIABmboioe2NwQ9SDTLE0FR/MKsXxS+UQRRE7zhZh34USqFUKvHrTEABAUVUDquoN7hwqEVGbcfsFoh4kNtgHSWG+OFdYjevf3oGEEB8YLXvn3jE2Hn0idAjy8UJpjR5ZxTUYEM0p4UTU/TBzQ9TDvP67obh6QAQ0KgUuFNcgu6QW3l5KPDglCQAQF+ILAMgqYd8NEXVPzNwQ9TAj4oLw4fxRqK43YNuZQvyaXohJfcIQptMAAOKDfXA0uwyZxey7IaLuicENUQ/lq1Hh2sFRuHZwlN3x+BDz4n+ZbComom6KZSkishNnWdk4i5kbIuqmGNwQkZ14S89NZqOeG1EU0WAwuWNIREStwuCGiOxIZamcsjrojdZg5r/7stH3/37AppP5LV6jvEaPgsq6Fs/LKavFXR/vw1cHsts+YCKiRhjcEJGdcJ0GWi8FjCYRl0pr5eOrLQHIJ7svNPv6Or0Rs9/dgal/24bS6oZmz/vjpwfxS1ohlm891y5jJyICGNwQUSOCIMh9N1JTcWWdHsculQMAdp8rRnmN3unrvzp4EReKa1BZb5Bf05goivjL2mPy81klNTAYWfIiovbB4IaImogLtqx1Y9ljav+FEhgtO4kbTCK2nHZcmmowmLD8l7Py92fyKx2e9/HOC1hz6BKUCgEqhQCDScSlslqH5xIRtRaDGyJqQp4ObpkxtetsMQBArTT/lbHxRJ7D131z6CJyyq29Nun5VU3O2XWuCC9/fwoA8Jdr+6N3mDmQyijiooFE1D4Y3BBRE43Xutl1zhzc3H1FIgDzruI1DfZ7T+mNJry31Zy1GRkfBABIL2iauVn6v5MwmkTcODwGd09IQIJldtYFBjdE1E4Y3BBRE3LPTXE1SqsbcDK3AgBw9xUJ6BXkjTq9CdvPFNq9Zt3hS8guqUWonxpLrh8AwJy5ES17VwHm3p00S6nqL9f1hyAISGTmhojaGYMbImoiXt5fqga7z5uzNn0j/BCu02LGwEgAwMYT1r4bg9GE9ywznu6b2Bv9o3RQKgRU1huQV2EtUx2/VAFRBGICvRHqZ97uIdHyXhntuGhgUVU9csvZw0PUUzG4IaImYgK9oRCAOr0J6w5fAgCMTwoFAEwfZA5uNp/KR4PBBFEU8d7Wc8goqkaQjxfuGBcPjUqJBEtp64xN382xS2UAgCG9AuRjCaHtW5YSRRGz39mJa/6+HVX1hpZfQEQeh8ENETWhVikQHegNANhyugAAkJoUAsC88WaonwaVdQbsOFuIv6w9jr9vOgMAWHRVH/hqzFvW9Y3QAQDSbWZMHb1onvo92Ca4SbQENxdLa9plBeSCynpcKqtFZb0Bpy3lNCLqWRjcEJFDUlOx0SRCEIBxiebgRqkQcPWACADAws8P47/7siAIwJLrB+DuCQny6/tYghvb6eDHLMHN0F6B8rFwnQY+aiVMIpBdevmlKdvdzM84mK1FRJ6PwQ0ROSStdQMAg6IDEODjJX8/w1KaqtUb4aNW4sM/jMI9VyRCEAT5nL4RfgCsAUZZTQOyLLOvBkVbMzeCIMgzpjIKL780lVlsvYaj2VpE5PkY3BCRQ1LmBgDGW0pSktTeIegd5ouYQG+s/mMqplkyObakstTZAvOMqd8sWZuEEB+7QAmwlqYuFNsHN0eyy5Bd0rpsju35jtbZISLPp3L3AIioa4oPtgY3qY2CG7VKgZ8enQQRgJfS8b+REkJ8oVIIqKo3ILe8Tt5qYYhNSUo+N9T8XrbTwU/nVeDG93aid5gfNj822eVxZ5bYlqWYuSHqiZi5ISKHpFlMKoWA0QnBTZ5XKRVOAxvAHABJGZkz+ZU4ml0GwH6mlPxeIU0zNxuP58MkmjM/+RUt7zAuse25Kaisb3YfLCLyTAxuiMihlEgdFl2ZjJfmDJJnQLWWdcZUlZy5GRzTNLiRt2Cw6bn52Wb/qsNZZS6/p9TXI7X/sO+GqOdhcENEDgmCgCem98NtY+LafI0+lqbiXeeKkFteB0EABjkIbqTMTU55Her0RhRU1snTxgHgcHapS+9XWadHSXUDAGB4bCAAzpgi6okY3BBRh+kTbs7cbE8vAgAkh/k5zAIF+6qh05qPZxbXYOtp+60dXM3cSFmbYF81hsc539+KiDwbgxsi6jDSdHCjyby/1GAH/TaAOUsk9edkFFVji6Ukdf2QKADm9XEMxpYX+Muy9NvEBfvI780ZU0Q9D4MbIuowCaG+8FJa174Z6mCmlMS2+fhXS6bnvom9odOqUKs3yhtuNkeaKRUf4uNwEcHG8ivqcMM7O7ByZ0aL1yai7oPBDRF1GC+ldcYU4DxzA1j7br46mI2aBiPCdRoMjgnAMEvvjCulKWmmVHywD5LDzZmb5mZMfXskB79dLMeL353CiZxyh+cQUffj1uBm+/btmDVrFqKjoyEIAtatW9fs+Vu3boUgCE0eeXl5nTNgImo1KYOiUggYEOXv9DwpCMouMe/mfVVKOBQKQW4MdiW4ySoxz7aKC/GFv9YLUQFaAM77bg5mmhuVjSYRf1lzTC6fEVH35tbgprq6GkOHDsW7777bqtelpaUhNzdXfoSHh3fQCInocvW1NBX3jdBB66V0el6CTYYHAK5MMf9/LTUGuzJjSmoojrMsQGgtTTXtuxFFEQezzNdUCOZNPT/ZfaHF9yCirs+twc3MmTPx0ksvYe7cua16XXh4OCIjI+WHQsHqGlFXdd2QSITrNLh9TGyz5yWGWIMbtVKBK5JDAQBDLZmb84XVzS7IpzeakFNmXuxP2jqib7i0v1XTzM3F0loUVtZDpRDwzHUDAAB/25iGnLJaFz8ZEXVV3TIqGDZsGKKionD11Vdj586d7h4OETUjOVyHfc9Mwx9SE5o9L8DHC8G+agDAuKQQecp4sK8aCZZg5cjFMqevv1RaC6NJhNZLgXCdBoB1nZ2zBU0zN4csWZuBMQG4a3wCRsUHobrBiGfXH4cosjxF1J11q+AmKioKK1aswDfffINvvvkGsbGxmDJlCg4dOuT0NfX19aioqLB7EFHXlBxmDkamptiXmuXSVJbz0lSmTUlK2p28uRlTUr/NiLhAKBQC/nrjYHgpBWw+VYAjlq0iiKh76lbBTb9+/fDHP/4RI0eOxPjx4/Hvf/8b48ePx5tvvun0NcuWLUNAQID8iI1tPjVORO7zl+v6Y9GVybitUQlreFwggOabirMs+1LF2Wz42aeZGVNScDMy3hw49Y3QYVS8eQ+trFbuRE5EXUu3Cm4cGTNmDM6ePev0+cWLF6O8vFx+ZGdnd+LoiKg1hsUG4onp/aBRKZscB4Aj2WVOS0bWZmJr747OyYyp6noDTueZv5eCGwAI9PECAJTXcrNNou6s2wc3R44cQVRUlNPnNRoN/P397R5E1L2kRPpDo1KgvFaPjKJqh+fIa9yE+NgddzRj6ujFMhhNIqIDtIgK8JaPy8ENdxIn6tbcGtxUVVXhyJEjOHLkCAAgIyMDR44cQVZWFgBz1mX+/Pny+W+99RbWr1+Ps2fP4vjx43j00Ufx888/Y+HChe4YPhF1ErVKIe8mvnzrOdTpjU3OkTM3jYIbacaU7SJ9hywlqeE2WRsACPA2NzSXMXND1K25Nbg5cOAAhg8fjuHDhwMAHnvsMQwfPhzPPvssACA3N1cOdACgoaEBjz/+OAYPHozJkyfj6NGj2Lx5M6ZOneqW8RNR57lltLkP56uDFzHr7R04fskarIiiKAc38cH2wc3Y3iEAgK8PXkS25ZxDlt6dkXGNgxtz5qasC2ZuTuZU4J2f0x0GdkRkr+n2vJ1oypQpzU65XLlypd33Tz75JJ588skOHhURdUW3jIpFqJ8aT359DOkFVZj73k78+eq++OOkJBRX16OmwQhBAGKCvO1eN61/OMYnhWDXuWI89+0JfDR/lDwNfGSjzI2156ahcz5UKyz74RR+TS9CQqgvrh8S7e7hEHVp3b7nhoh6jqtSIrDx0YmYPjACeqOI135Mw63v78bOs+aNNqMDvJs0IwuCgKWzB8FLKeDn0wVYsf0cymr00KgU6N9oO4hAb+cNxYezSvGXtcfc1mx8vtDca3SplIsMErWEwQ0RdSshfhqsuGMkXr95CPw0KhzILMWfvzwKwH4auK3kcD/cP6k3AOC1H9MAmHcoV6vs/woM8HFelnr3l7NYtTcLn+y60F4fxWUNBhNyys1BTUFlfae/P1F3w+CGiLodQRDwu1Gx+OGRiRiTECwfbzxTytaiK/ugl03JakSjkhRg03PjIDtTaAkqfrVkiTpTTlktpAo+gxuiljG4IaJuKzbYB/+9fxyenpmC3mG+uGGo814Ub7USL9wwUP6+cb8NAAT6mGdLldfqm/QDFleb+3AOZ5Wiut7QHsN3WXapdVHBgoq6Tn1vou7IrQ3FRESXS6kQ8MDkJDwwOanFc6f2j8A9VyTiVG4FJiSHNHle6rlpMJhQpzfBW23t3ymxBDd6o4h9GSXyruWdwXbF5MIqZm6IWsLghoh6lCXXD3D6nI9aCZVCgMEkoqy2Ad5qcxmrTm9ETYN1Cvav6UUuBzcNBhN2nSvC6IRgeTPQ1sousTYRF1YwuCFqCctSREQWgiDI08Ftm4qlrI1kx9lCl6/55f4s3PnxfvxjS3qbx5Vtk7mprDegtoFr3RA1h8ENEZGNAAfTwaXgxk+jgiCYt3JwtfflnGUK9/4LJW0ek23PDQAUVLLvhqg5DG6IiGxITcW2mRupmbhXkDcGRZu3gdjh4qwpaZbV6dxKGE3OFy1tjtRzIwj21yQixxjcEBHZsGZurKWokmpzMBHip8aE5FAAwI701gU3tXqj000/m1NZp5cDrX6WTUBbmg5eUt3Anc2pR2NwQ0RkI9DB/lLFVeZAJ9hXg4l9LMHN2aJmt4+RFNnMbjqZW9Hq8UjNxMG+aiSG+gJofjp4nd6IqW9sxXX//NWl8RF5IgY3REQ2Anya9tyU1piDmxBfNUbGB0GjUqCgsh7pBVUtXs+2hGS7M7mrpJJUbJA3wnUaAM1nbvLK61Bao8fF0lpUdvJ6PERdBYMbIiIbjlYplhqKg3zU0HopMSbRvCryry2UpmobjHYBxsmc1mduLlqaiWODfRDurwXQfHBTUmMtp5VVszRFPRODGyIiG/LmmY7KUn7mZmO5NJXe/JTwokYL7p3MqWh1qUjO3AT7IMySuWmuobjMJrixDXSIehIGN0RENmy3YJBImZsQX/NzUlPxnvMlqDc4X3NGyrCE+mmgVAgorm5o9d5Q0ho3cTbBTbOZm+qm5TSinobBDRGRDXln8NqmGZBgS3AzIMofYToNavVG7M8odXotKcMSG+yNpDBzM3Br+26sPTc+cs9NYTPr3JTaLDhYWs3ghnomBjdERDYCHMyWkjI3UnAjCAKm9A0DAGxNK3B6LWkfqDA/DQZE+QNoXd+NKIq4WGqeLRUb7I1wnbnnpri6AQajyeFrbLM1pTXsuaGeicENEZGNxj03BqNJDnSk4AYApvQz7y31S3PBjSVzE6bTYKBl8b8TrQhuCivrUW8wQSEA0YHeCPZVQ6kQIIrWhQUbswtumLmhHorBDRGRDannprLeAIPRJGc/BME8W0pyRZ9QKBUCzhVW2+39ZKvQpudmQLQlc9OKtW6kklRUgDe8lAooFYLc91PgZANN232w2HNDPRWDGyIiG/5a687dFXUGOVgI9PaCUiHIzwV4e2FkXBAAYOsZx7OmbDM3Ulkqs7gGFXWulYukPaXign3kY+H+UlOx476bUpuG4jKWpaiHYnBDRGRDpVRApzEHOGU1DdY1bmxKUpIpKZa+m9OOS1PSVPAwnQZBvmpEB5h7Zk7nVro0lqxia7+NROq7cTZjyjZb03g3c6KegsENEVEjtqsUN54GbmtKX3Pfza5zxajTN50Sbpu5AYABct+NazOmHGZupOngTspS9g3FDG6oZ2JwQ0TUSKCPdZViadPMYAfBTf8oHSL8zVPC92WU2D0niqLdbCkA1r4bS1Ox0STi+KVy1DY4XivHdgE/iTwdvKppWcpkEu1mSDG4oZ6KwQ0RUSMBNjOmiuVp4Jom55mnhJuzN1vT7PtuKuoMaDCYp2tLmZuBluDmt4vl+HxvJqa+sRXXv70Dz64/7nAcFy3BTa8ga3AT1kzmprLOAKPJugJyaY2em2dSj8TghoiokUBvc5amrKZBnk4d7Ovl8NwrUxyvdyOVpHRaFbReSgCQm4rT8ivxzNrjuFBsDl5+OJ4nB0KSBoMJuZbdv23LUmHN9NxImRqVpfG5wWBCjZOsEJEnY3BDRNSItefG0GzmBjBvxaBSCDhfVI2sYuuU8Mb9NgDQK8gbUZam4ugALZ69fgBCfNWoqjfgQKZ9WetSWS1EEfD2UiLUz1oSk2ZLOdpfSlpJOcJfC7XK/Nc7S1PUEzG4ISJqxLozeEOzDcUAoNN6YVSCeUr4tjPW7E3jfhvAXMb65O4x+Pedo7DtyStx9xWJmNxPyvzYl7XS8sx9ObHB3hAE6xT0cJvNMxuXnKQsU4ifGkGWAK2UO4NTD8TghoioEdtVihtvveBIam/zRpqHs8rkY44yNwDQJ0KHq1Ii4KU0//V7pWWl458bTSdfc+gSAGBinzC746GWYKnBaLLb3BOwbrcQ5KOWFxxk5oZ6IgY3RESNBNpMBS92IbgZ0ss8xfu3S9Yp3s6Cm8Ym9QmDUiHgbEGVvNJxUVW9HOzcMirW7nytl1LOLDXuu5EyN0E+Xh4X3JhMIipdXPyQiMENEVEjAd7WwKDUheBmUIw5uDlXWIXqegMA14ObAB/rSsfSPlXrDl+CwSRiaK8A9IvUNXmNs7VupJ6bIF81gnylspRnBDd/+uIwRr20GRdLHW91QWSLwQ0RUSNSZiS7tBYGy9Tq5oKbMJ0Gkf5aiKJ176giBz03zlyZYtmE83QBRFHE6gPZAIDfNcraSJxtwSAHYjZlqRIP2ILBZBKx9XQB6g0mpOW5troz9WwMboiIGpHKUlL2xVetlKdzOyNlb45dLLd7bUuZG8A6nXzXuWLszSjBmfwqaFQKzBoa7fB8aQuGxjOmpBJUoK81uCnzgLLUxdJaVFumtLu6Lxf1bAxuiIgakYIbSbCf86yNZLAluDlu6bsprHI9uOkXoUNUgBb1BhP+suYYAGDGoEg5g9SYvJBfk54b8y/+YB+1vBdWqQdkbk7lWXdSr6g1uHEk1F0wuCEiaqRxUBHs40Jw08uy+vClchhNIopbEdwIgiCXps4XVQNo2khsK9xJcGPtufGymQre/TM3thuNVtR2/2CNOh6DGyKiRry9lFArrX89NtdvI7FtKr5YWgOTCAiCa4ERYJ0SDgAxgd5I7R3i9FzrFgxOem58bTM3joObmgYDNvyWg6X/O4n0/K7dx3LaJnNTWc/MDbVM5e4BEBF1NYIgIMDHS+5pcbY6sa1wnRYR/hrkV9Rj2xnzgnwhvmqolK79G3JCcgjUSgUajCbcPLIXFArB6blyz02VNXNjMokoq7Wuc1OnN2/n0Dhzs/d8Mf6z+wJ+Pl0gn5NbXovld4x0aZzucDqPmRtqHWZuiIgcCLQpTYW40HMDWPtupDVqQl2YKSXxUatw54QEJIf7Yd7YuGbPjQn0BmButJU2yrTdNDPQx0vOGNn23BiMJtzznwP4/lge6vQmuXR1tqDK5XF2tpoGAy4UV8vfs6GYXMHghojIAdu+myAXS0uDYwIBALvPFQNwrd/G1l+u7Y/Nj01GuL+22fN6BXlD66VAg8GETMsvfqnfxk+jgkalRKBlnZtavRF1evNMo8ySGlTVG+DtpcSGP12BbxddYT5eXGO3m3hXcia/Cra7TLChmFzB4IaIyAHbGVPO9pVqTGoqrrfs8N3a4MZVCoWAvhHmxf3OWPplSm2aiQFAp1HJu4NLz52xlHf6RPhhUEwAogO9oVaZS2GXSms7ZKyX67Rl3SDpszBzQ65gcENE5IC0SjHgWkMxYG0qlnRUcANADm7S8swlJevWC+axCoKAQGkhP8tzZ/Kr7F6rVAhICPEBAJwv6pqlKanfRrq37LkhV7g1uNm+fTtmzZqF6OhoCIKAdevWtfiarVu3YsSIEdBoNEhOTsbKlSs7fJxE1PPYlqVcWecGsDYVS1xZnbit+jXK3JQ0Cm4AINiSxSmz9N1I5/aN8JPPSQz1BQBkFFn7WrqSU5bMzdjEYADm3iKilrg1uKmursbQoUPx7rvvunR+RkYGrrvuOlx55ZU4cuQIHn30Udx7773YuHFjB4+UiHoa27KUq9O5AWtTMdCxmZs+lgAlrVFZyjbL1DRzI5WlrPtV9Q4zX+d8YdcLbkRRlDM3YyzBTUWdHqLYNfuDqOtw61TwmTNnYubMmS6fv2LFCiQmJuKNN94AAPTv3x87duzAm2++ienTp3fUMImoB7ILblzM3ADm8snmU+bZUh0Z3EgbamYUVaPeYJRnRdlmbqTZUGU1DWgwmOTsTD+b4KYrZ27yKupQXquHSiFguGVzUb1RRJ3eBG9189thUM/WrXpudu/ejWnTptkdmz59Onbv3u30NfX19aioqLB7EBG1RCpLeSkF6DSu/zvQNnMT3oHBTaS/FjqtCkaTiPOF1TY9NzZBmc0WDBlF1TCYROg0KkQFWGdj9e7CwY20MnFSmB+CfLwgLf3DpmJqSbcKbvLy8hAREWF3LCIiAhUVFaitddzpv2zZMgQEBMiP2FjnS5oTEUmk4CbYVw1BcL6gXmODe9mUpfyan9J9OQRBsOu7kXtunJSlrCUpP7vPI5WlLpXVotayOWV72nIqH+P+ugX7L5S0+rXSnlIpUToIggB/y58Jm4qpJd0quGmLxYsXo7y8XH5kZ2e7e0hE1A0MjA6Av1aF8UmhrXpduE6Lp2em4P9N74cAH8cbX7aXvpHW4MZRz02wzc7g1mZind01gny85EDOdrG89rJy1wXkVdTh+2O5rX6tlLlJiTRPsffXWoIbNhVTC7rV9guRkZHIz8+3O5afnw9/f394e3s7fI1Go4FG03GpYSLyTGE6Dfb/3zS7PaZc9cDkpA4YUVP9bKaDO+q5kfqGSmr0qNU3bSYGzBmgxFBfHMkuQ0ZRNfpH+bfb+PRGEw5mlgIACirqWzi7qdM2mRsA8Pc2/8piWYpa0q0yN6mpqdiyZYvdsU2bNiE1NdVNIyIiT6ZRKVtVkupstgv5yT03vk17bspqGpBuWeOmX6PgBgB6h5n7bs4Xtu9aN8cvlaPGUurKb7TJZ0vqDUacs8zg6t84c8OyFLXArcFNVVUVjhw5giNHjgAwT/U+cuQIsrKyAJhLSvPnz5fPf+CBB3D+/Hk8+eSTOH36NN577z2sXr0af/7zn90xfCIit5LWq8kqqbGWpXya9tzkltfJJSfbNW4kUlPx+XZuKt6XYe2zyWtlcHO2oApGk4hAHy957SCdVsrcsCxFzXNrcHPgwAEMHz4cw4cPBwA89thjGD58OJ599lkAQG5urhzoAEBiYiK+++47bNq0CUOHDsUbb7yBjz76iNPAiahHCvHTINQyTV3aGirQp+nKyoWV9TCJ5jKVo+npiaHmgKe9Z0zttQluCirqW7U+jW2mScqeMXNDrnJrz82UKVOa/WF3tPrwlClTcPjw4Q4cFRFR99E3QoeiKvNGnTqNCmqV9d+sQY0amvuG6xyW2axlqWqIotgupTijScR+m+CmwWhCWY3ebjZXc6RMT0yQtZ9Sni3FnhtqQbfquSEiInu2s58Cfe2DGX+tdW0YwLqqcWMJIebgprxWLzcmX65TuRWorDfAT6OSZ2PlV7pemiqqNDcg225hIWVuuAUDtYTBDRFRNyatVAw03SZCoRDsylS259ryVisRbVnYL6OdNtCUSlKjEoLkRQPzyl0PbgqrLMGNTRlNni3FshS1gMENEVE3Zpu5cVTysd1Gok+44+AGsC7md66d9pjae95cKhubGIJIS3DTmunghZbMTaiDzA0biqklDG6IiLox29lPQQ42+LTN5jiaKSVpzz2mTCYR+ywrEo/tHYwInTm4ac108CIHmRt5thQzN9QCBjdERN2YTuuFmEBz062j4EYqS4X6qRHi53xBUzm4aYfMTXpBFcpq9PD2UmJwTIA8lbs108GlzI19WYoNxeSabrVCMRERNdUnwg+XymoR7Nt0uwfpWHMlKcBmxpSLPTeiKCKjqBo7zhbh1/QiFFXV494reuO6IVHYm2EuSY2MD4KXUoGIAClz41pZSm80yY3NjspSbCimljC4ISLq5u4YG4/S6gZcMzCyyXORAeaszoDo5rdV6G1Z6+ZCcQ2MJhFKhfPp4OU1evzu/V04k28fCC1cdQjfHYuUg4+xicEAIJelClycLVVcZV6QUKUQEOhtDdjYUEyuYnBDRNTNTRsQgWkDIhw+Nz81Ht5eStw8slez14gJ8oZaqUCDwYSLpTWIt0wPd2TrmQKcya+Cl1LAyPggTOwThup6Az7Yfh7fH8uTzxvbOwQAEOHfutlSts3ECpsgSypL1RtMqNMbofVSunQ96nnYc0NE5MFC/TR4cEqSw5WJbSkVAob0CgAA/GdXZrPnStO856cm4Iv7U7HwymQ8OSMF6xZOQIplurm3lxJDY83Xiwgwv3dRVT0MRlOLYy6sMgdBoTr7HiI/tQrS+oIsTVFzGNwQEREA4JFpfQAAn+3JRHZJjdPzpD2jxljKTpJBMQH4dtEVWDp7IN6bNwIalTmzEuKrgVIhwCQCxZYNPptTVGk+J6xRA7RCIcBP0/zO4BV1esx5dyf+svZYi+9DnovBDRERAQAm9gnDhOQQNBhNeHPTGYfnFFfV42yBuddmdEJwk+fVKgXmpybgypRw+ZhSIciBiiulKUcL+ElaaipevT8bR7LLsGpvllzeag/PrT+Omf/4FZWcqdUtMLghIiLZUzNSAABrj1zCyZyKJs/vv1AKwLxmTrCL+0QBsJkx5UJw42ABP4k8HdxBU7HJJOLTPdaS2ta0ApfH15y88jp8sicTp3IrcCCztF2uSR2LwQ0REcmG9ArE9UOiIIrAaxtPN3neWUmqJRGWLEy+C9mU5jM3zstS29MLkVlsLaf90kJwk55fifs+OYDjl8qbPW/DbzmQ9ni+cJmLHKblVeJiqfOSH7UPBjdERGTniWv6QaUQsDWtELvPFds9t++C+XtHJanmSDOm8l0pSzlYwE9izdw0LUt9sjvTMrYgAMCvZ4qgb6aBefWBbGw6mY9V+7KaHc+3R3Pkr22Dp9Yqr9Fj1js7cPPy3TCZxDZfh1rG4IaIiOwkhPri9jFxAMzZG9GStqis08ulqtZmbiJbUZYqaq4spXW8SnFWcY2cqXnlpiEI8VWjst6A/ZZtIBzJsywqeLG01uk55wur8NtFa2bnQnHbMzfZpTVoMJiQV1GHbGZvOhSDGyIiauJPU5OhUSlwOKsMv6YXAQAOZpbCJAJxwT6IsiwO6Krw1pSlmsncONtf6rO9mRBFYGKfUCSF+WFyvzAAwC+nnZempECruTKRlLUJsmxAejllqRKbmWInHPQzdWUmkygHud0BgxsiImoiXKfFvLHxAIC3Np+BKIpt7rcBXC9L1emNqKw3l5yaK0vZzpaq0xux+kA2AGBBagIA4CrLbK1f0gqdvleBJbjJKat1+ItbFEV8e8Qc3Nw7sTcAc5anuVJXc4qrrYHdiZzm+3y6ElEUcfuHezD59a2o0xvdPRyXMLghIiKHHpjcGxqVAoeyyrDjbNFlBTdyWaqFLRikrI1apYBO03QRfUcNxd8ezUFZjR4xgd7yFPSJfcKgVAg4W1DlcM0eURTlva7q9CaH6++cyKnA+aJqaFQKzE+Nh9ZLAYNJRE6Z8zJWc6RtJaRrdxdp+ZXYm1GCrJIaeRmAro7BDRERORTur8Xvx5p7b/62MQ1HL5YBsO4Z1RrS/lJlNfpm//Uvz5Ty00AQmu5v5Wgq+PojlwAA88bFyXtiBXh7YWS8ubH4Zwelqcp6A2ptxnHJQd+NdN1pAyKg03ohPti8JcWFNjYVF3fTstSWU9b7d6mNgV1nY3BDREROPTg5CRqVAkcvlkNvFBGu0yAu2KfV1/H3VkGjMv/KaW5xvaJm+m0A24Zic1lKFEW54Xdy3zC7c6XSlKPgpqBRY3PjX9pGkyj328weGg0AiA8xf+629t2U2GRuCivrXd5I1N1s719bs1adjcENERE5ZZu9AcwlKUcZlZYIgiCXpvKamTElZW4czZQCmu4Mnllcg8o6A9QqBfpG6OzOlYKb3eeLUdNgP3VcKklJGmdu9mWUIL+iHv5aldycnBgqZW7aFtzY9twA3SN7U1LdgMNZ1oULGdwQEZFHeGByEtSWrEtbSlISqTTV3HTw5mZKAU23XzhmWYCvf6QOXkr7X2l9wv0QE+iNBoMJu87ar9fTeAyNMzdSf9FVKeHyHlnSTultXetGKktJ+2M5WgG6NXLLa7F86zlc8+Y2THztZxRVtd92E5JtZwpguyRPjos7u7sbgxsiImpWhL8Wz14/ABOSQ3D9kOg2Xyfc3zIdvKKZslQzqxMDTde5kVYXHhQT0ORcQRBwRXIoAMj9QhJpDJYWnSZr3ZwpqAQA9I/yl48lXGZZSmooTk0KAdB0xtS2M4X4/lhui1OuS6obsODf+zD+lZ/x6o+ncSa/Ctkltdh73vmaPq6oNxib9ENJ/TbJ4X4AmLkhIiIPcse4eHx+7zgEtWI/qcYi/VuRufFz/D5SWaqmwQi90SRnbgY7CG4AoHeY42yLNAaplNV4rZv0/Eq75wEg3lKWyi6tgaEN08GldW4mWXqDbDM3ueW1uGflfjz0+SE8u/5Es9f/6UQetp0phCiay4RSAJZb3vbAo05vxFV/24ar39yGYkuAqTeasO2MeSr9HZbSJIMbIiIiGxGtCW6cZG78bKaHV9Tqm83cAJCbnzMbTQeXmnlHWGZU2Zal9EYTMizZGSljAQBR/lqoVQrojSJyW1meqdMbUWVZv2dSH3M26UJxjbzL+Jf7s2Gw1H8+3ZOJu/9zwOH+WbZjvX1MHFb/MVVupL6cmUwHM0txqawW2SW1WLzmGERRxIELpaisMyDYV41rB0cBAAoq69FgsA+8CirrLrvE1t4Y3BARUaewlqWcBwZFltKNs4ZilVIhBzgncipQUWeAWtm0mVgSZyklZTVqApbKUiPizMFNZZ1BDiYyi6uhN4rwUSsRE2hdiVmhEBBvCZZsm4q/P5aLhasOobq+6X5XEqnfxkspIC7YB9GW5upTuZUwGE34Yp95EcJbR8XC20uJ7WcKcfPyXchzEETllJmP9Qoyjy0mUGs53vbgZufZIvnrn07m48v92fj5dD4AYEq/MITpNFCrFBDFpn9+D3x6ELPe2YEzlmxXV8DghoiIOoW1LOW450YUxRYzN4B1Ib+d58y/kFOidHLDc2NSE3Bpjd4uEyL9gk4M9ZW3VpBmTKXnmxeq6xPuB4XCfmaYdD2p70ZvNGHJuuP47rdc/Hg8z+mYpWngwb5qCIKAAdHmUtKJnHL8klaIvIo6BPuqsXTOQHz1QCoi/DU4k1+Fd3852+RaUvkpyhIgSVthSEFPW0jBzYi4QADAC/87if8dzQUATE2JgCAIcqBnmyGqNxhxJLsMRpOI7Wecrwbd2RjcEBFRp7AtSzlqmq1uMMoL6znL3ACAztJULM2AclaSAsxlrBBLn1CWpe9GFEUUWAKsCH8NYiwZECm4OWMJbpLDm2aDEkOlzI35Wr+mF8pZmfNFzlfvLbJMAw/2NX+uAdHmMZ/IqcDne827mf9uZC9oVEoMignA0zNTAADpBU2zIVKGRgpqogO97Y63VnmtXu5deuf3I5DaOwS1eiPyKuqgUgiY2DfU8j5NM0QZRdXybKrmNintbAxuiIioU0jBTU2DERW1TUs4UtbGR62Er4OtFyRSU/HxnOabiSVSaUpqKi6r0aPB0rAbptM0yUhIM6X6Rvg1vpTNdHBz5mbt4Rz5uYxmZlGVyOU2c6A10JK5+TW9UG7alXZiB4DYIPOYs0vsAxZRtPb7SOOW/ltc3dCmvZ/2nC+GSTQ3X0cHeuONW4bK2bExicHyDLXogKZBlJTlAoADF0q7zOaaDG6IiKhTeKuVcrnJ0UJ4LU0Dl0i/bKXfoy0FN/FyU7H5PaX9rYJ91dColIgJND8vBTdnLb+wHfXxJFiCm4yialTW6fHTCWsp6nyh8+BGWsBPyiJJwU1+RT1EEbgiORQJltlYABBrGXNeRZ3dzKmS6gbUWxp6IwLM98nfWwVftXktntY2OgPWkpQ0bT460Btv3joMscHeuOeKRPm8KClDZPMetntNFVc34Pxl7JrenhjcEBFRp2lulV/rNPAWghvL/lIAmm0mlsRZAhJpA02p5yfcEkTZlqX0RpNcXrKdKSWRtmDILqnFD8fyUG8wIcAynoyiaphMjjMXUulKKkvFBHrLrwOAeTarQAPme6BWKWA02c/Mkr4O9dPIiwsKgnBZpSkpuBmfFCofm9o/Ar8+eRWm9o+QjzlqXG68keb+jK5RmmJwQ0REnaa3JbhxlOWQgpvm+m0Aa0MxAPSLdN5MLJEzN8VScGMOEKQymVTWuVhag8ziGoczpSTRgd5QKxVoMJqwYts5AMBdExLgpRRQbzAhx8laM9ICfiGWspQgCHL2JkynwbQBEXbnKxQCelne33ZXcymwkPpfJFEOmn1dkVdeh3OF1VAIQGrvkGbPdRRAST1BUvZsXxfpu2FwQ0REnSahmcyNy2Upm4xHc83EkvhGPTcFcnBjfh9pSvWlslp58T5HM6UAQKkQEBtsPl8qwdw0opfci+OsNCUt4BdiswjiBEsZaEFqfJOtIwCglyUos109WQ5uAuwDL2fTwdcevog57+50mtGRsjaDYwIQ4OPl8ByJFNxcKq2FKIp26wFJ+48duFDq9PWdicENERF1Gqks5aj51pVp4ACgs8nctNRvA1gbinPLa9FgMMllKSlzIwU3RVUN+M0ya6hPM6Uuqe8GAEYnBCE22McmI+V4xpS06m+ITVbqvom98c2DqXhoSrLD18RaxpVts3qyVJaKapS5cdTsCwD/2pGBI9ll+ObgRYfvIQU3UqDVHOk9qhuMqKgzyFkuX7US1w2JgkIAskpqml3HqLMwuCEiok4jBzeF1U1m1rhelrJmGIb0ajm4CfPTwNtLCZNoLj1Jv3zDLcFNgLeX3JC7Lc08c6mPg34biW3j75zhMebPZdnmwVlDrbXnxpq5UasUGBkf7DBDBAC9ghxkbizBTePMjbVkZA0sDEaTPK39QGbTjIooivJaQa4EN95qpTz+nLJanLWUpJLD/eCv9ZK3gegKU8IZ3BARUaeJC/aBIACV9Qb5F76ktWUpV5qJAXN/i+02DPmWICrC8j6CIMhNxSdzzdsINHddaQNNtVKB6webNxJNCjUHQ86mgxc3mgruCqn8ZdtzkyutcdOk58ZSlrLp+TlfVC1vlXAoq7RJs/O5wmrkV9RDo1JgpGUbipbYrnUjTQNPsgSCoxPMO8Z3haZiBjdERNRptF5KOevQOBBwtSwlrQEzLC6wxWZiiXUbhhqbnhtrgNC4ebiPgzVuJBOSQ6FWKXDbmFi5T0XaoNNRz01Ng0FenDC4FRuPSpkbR2Wp6MDGPTfWspSUETuVa93vqbLOIK/fI5FKUqMSgqD1Uro0Jrn8VV6H9AJpJWdzICgFN/u6QN+N81WSiIiIOkDvMF9cKqtFRlG1/AuxvEYvl1ykLIszg2L8seresXLGwBXSjKmMomoUVNr33ADW6eCAeRHBxmUf+/H74cQL06EUBLtjgLkpubbBCG+1NViQsjZqlcJu48+WSD03+RX1qDcYoVIokFfhuCwVadmKoU5vQmmNHsG+ajkLJTmYWYqUSH/5+61pBQBcK0lJbGdMWYMbKXNjzv6czqtARZ3ernzY2Zi5ISKiTmW7EJ7kULb5X/uJob4tZjcEQcD45FC74KQl0oypw5Z9kATBvkQkLeQHOJ8pZctLqbA7J8jHS163pvFMMNuZUoLQ/HVtBfuq4WMJki6V1qKgsg5GkwiVQmiS3dKorAskSk3Fp3LNmRppVthBm4xKZZ0eOy3bV1zd334aenOkslR2SQ3OWZqnpSxXuL8W8SE+EEVzIOVODG6IiKhTyQv52QY3ll+G0i7d7U1ayO+EZTZUqJ8GKpvp17aZm+ZmSjkjCILT0pS8OnEr+m2ka0ozuS6W1srNwhH+WigdBF/Rjda6kcpS0rYOtk3Fv6QVosFoQu8wX4eLFTojvcfejBI0GEzQqBRy+QywlqYOuLmpuEsEN++++y4SEhKg1WoxduxY7Nu3z+m5K1euhCAIdg+t1vXonYiI3MvRdHDpX/quNra2llSWMliaaqVshqSXbXDTil/2tnpbmoobTwcvrrJfnbg1Ym36bqTdwBsv4CeJDrA2+xZV1aOwsh6CYA5uBMs07QLL1hMbLdtGTB8Y2apskhTcSP1RSWF+doGWVJran9HDMzdffvklHnvsMTz33HM4dOgQhg4diunTp6OgoMDpa/z9/ZGbmys/MjMzO3HERER0OWyDG5NJhMFowtHsMgAdF9zEBHnb/RKO0NkHCL1sGnRdmYHlSG8n08GlWWGhrWgmlsclrXVTUotcS+Ymykk/kBR45JbXyVmb+GAfRPhr0c/ymQ5eKEWd3oitp82/Y6cPjGzVeFpqvB6TGIKB0f4YHh/Yquu2N7c3FP/973/Hfffdh7vuugsAsGLFCnz33Xf497//jaefftrhawRBQGRk6/5AiIioa+gV5A2VwrxdQV5FHUprGlDdYIROo2pz1qQlXkoFogO18i7b4Y36dUL9NNBpVKhqMCAlqo3BTaiT4MYyxb01M6UksfIqxTWoN5hnXDWeBi6xLUtJwY209syohCCczqvEgcxSqFUKVDcYEemvxRAXFkG0FeangZdSgN5ozoA1/vNKDPXFdw9PbNU1O4JbMzcNDQ04ePAgpk2bJh9TKBSYNm0adu/e7fR1VVVViI+PR2xsLGbPno0TJ044Pbe+vh4VFRV2DyIich+VUiHPiMooqpb7bYbFBbbYyHs54oOti+81LkspFALe/8NIvPv7EU4zIy2RZkydL6yyW6BQytyEtLA4oSPW6eC1TrdekNhuwSA1E8vBTbylFyaz1KYkFdHq+61QCPLMLABIDm9bINjR3BrcFBUVwWg0IiLCvlM7IiICeXl5Dl/Tr18//Pvf/8b69evx2WefwWQyYfz48bh40fHS0suWLUNAQID8iI2NbffPQURErWNbmurofhtJrM0U83Bd0+zH+ORQXDs4qs3Xjw+xLFBYZ0BRlXWBQnnTzMsoS10sqbFuvRDQfOYmx0HmRrq3Jy6VY9PJfACtL0nJ7xNg23zdMZm2y+X2npvWSk1Nxfz58zFs2DBMnjwZa9asQVhYGN5//32H5y9evBjl5eXyIzs7u5NHTEREjSXYBDeHssoAdHxwI00HB5pmbtqD1su6k7hts7Q8FbyVs6UAa0BWXN2ADMssrMYL+EmkjFNBZT3OWtag6W8psfUK8ka4TgODSURpjR6BPl4Ykxjc6vEA1r4bL6UgN2p3NW4NbkJDQ6FUKpGfn293PD8/3+WeGi8vLwwfPhxnz551+LxGo4G/v7/dg4iI3EvK3Oy/UIKskhoIAjAsNrBD39P2F3Fr1shpDdvSlMTRppmuCvD2kjcKraw3AHAe3IT4qqFWKSCK5llh/lqVHIgIgoBRCdbgcWpKhN1U+NaQen56h/q1+Rodza2jUqvVGDlyJLZs2SIfM5lM2LJlC1JTU126htFoxLFjxxAV1fZUIhERdS4puPntonndmX4ROug6eEXbOJvMTXgHZG6Apk3Foihae27aUJYCrNPBAUCjUiDIx/F9UigEeTo4AKRE+dtN8x4Zb83UzBjU9kk50mwyVzYtdRe3z5Z67LHHsGDBAowaNQpjxozBW2+9herqann21Pz58xETE4Nly5YBAJYuXYpx48YhOTkZZWVleP3115GZmYl7773XnR+DiIhaIdFmZ20AGNHBJSnAnGnw16qg9VIipA1rzrj0HvJCfubMTXWDEfWWzSvbUpYCzBtoSlspRAd6N7suTXSgNy4Um/eiGhBlX6kYaylD+aqVmNjH9S0XGrtucBR0WlWHLbjYHtwe3Nx6660oLCzEs88+i7y8PAwbNgw//vij3GSclZUFhcKaYCotLcV9992HvLw8BAUFYeTIkdi1axcGDBjgro9AREStFOmvhUalkH/xj+yEX5TeaiV+eHQSVArB4Qq/7SHJUpY6erEcdXojSizNxFovBXzUbfuVa7sCsLMF/CS2M736N5rSPigmAK/eNBgxgT4ub5TpiEqpwFUprm/Z4A5uD24AYNGiRVi0aJHD57Zu3Wr3/Ztvvok333yzE0ZFREQdRaEQkBjqi9N55inLHd1MLGm8CF17GxkfhKgALXLL6/CvHRlITQoBgMvKFMXarJ7c0jT1GJvgx3aTTMmto+PaPI7upGt2AhERkceTNtAM9lXbzWTqzrReSjw1IwUA8O4vZ3Hast5MW0tSgP0U9mgn08Dl5y3Bm0IA+kV2zTVoOgODGyIicotES3/KiLjAVu1v1NXdMDQaw2IDUdNgxOsbTwNoezMxYF+Wimoh8yRNse8bobus0lN3x+CGiIjc4o5x8bh6QAQentrH3UNpVwqFgGdnmftAS2v0ANq2aabEdlNPZ9PAJWMSgvHi7IH42++Gtvn9PAGDGyIicouYQG98OH8UhvQKdPdQ2t2IuCDMGRYtfx96GWUpX411vZrejWaZNaZQCPhDagIGtXLPKE/TJRqKiYiIPM2TM1Lw44k81OlNbdo009YH80cit6zOrv+GnGPmhoiIqANEB3rj+VkDkRDig6n9wy/rWgOjAzBtQNeeft2VMHNDRETUQW4bE4fbxvSM6dddCTM3RERE5FEY3BAREZFHYXBDREREHoXBDREREXkUBjdERETkURjcEBERkUdhcENEREQehcENEREReRQGN0RERORRGNwQERGRR2FwQ0RERB6FwQ0RERF5FAY3RERE5FEY3BAREZFHUbl7AJ1NFEUAQEVFhZtHQkRERK6Sfm9Lv8eb0+OCm8rKSgBAbGysm0dCRERErVVZWYmAgIBmzxFEV0IgD2IymZCTkwOdTgdBENr12hUVFYiNjUV2djb8/f3b9drdHe+Nc7w3zvHeOMd74xzvjXPd+d6IoojKykpER0dDoWi+q6bHZW4UCgV69erVoe/h7+/f7X5oOgvvjXO8N87x3jjHe+Mc741z3fXetJSxkbChmIiIiDwKgxsiIiLyKAxu2pFGo8Fzzz0HjUbj7qF0Obw3zvHeOMd74xzvjXO8N871lHvT4xqKiYiIyLMxc0NEREQehcENEREReRQGN0RERORRGNwQERGRR2Fw007effddJCQkQKvVYuzYsdi3b5+7h9Tpli1bhtGjR0On0yE8PBxz5sxBWlqa3Tl1dXVYuHAhQkJC4Ofnh5tuugn5+fluGrH7vPLKKxAEAY8++qh8rCffm0uXLuGOO+5ASEgIvL29MXjwYBw4cEB+XhRFPPvss4iKioK3tzemTZuG9PR0N464cxiNRixZsgSJiYnw9vZGUlISXnzxRbu9dXrKvdm+fTtmzZqF6OhoCIKAdevW2T3vyn0oKSnBvHnz4O/vj8DAQNxzzz2oqqrqxE/RMZq7N3q9Hk899RQGDx4MX19fREdHY/78+cjJybG7hqfdGwY37eDLL7/EY489hueeew6HDh3C0KFDMX36dBQUFLh7aJ1q27ZtWLhwIfbs2YNNmzZBr9fjmmuuQXV1tXzOn//8Z/zvf//DV199hW3btiEnJwc33nijG0fd+fbv34/3338fQ4YMsTveU+9NaWkpJkyYAC8vL/zwww84efIk3njjDQQFBcnnvPbaa/jnP/+JFStWYO/evfD19cX06dNRV1fnxpF3vFdffRXLly/HO++8g1OnTuHVV1/Fa6+9hrfffls+p6fcm+rqagwdOhTvvvuuw+dduQ/z5s3DiRMnsGnTJmzYsAHbt2/H/fff31kfocM0d29qampw6NAhLFmyBIcOHcKaNWuQlpaGG264we48j7s3Il22MWPGiAsXLpS/NxqNYnR0tLhs2TI3jsr9CgoKRADitm3bRFEUxbKyMtHLy0v86quv5HNOnTolAhB3797trmF2qsrKSrFPnz7ipk2bxMmTJ4uPPPKIKIo9+9489dRT4hVXXOH0eZPJJEZGRoqvv/66fKysrEzUaDTif//7384Yottcd9114t1332137MYbbxTnzZsnimLPvTcAxLVr18rfu3IfTp48KQIQ9+/fL5/zww8/iIIgiJcuXeq0sXe0xvfGkX379okAxMzMTFEUPfPeMHNzmRoaGnDw4EFMmzZNPqZQKDBt2jTs3r3bjSNzv/LycgBAcHAwAODgwYPQ6/V29yolJQVxcXE95l4tXLgQ1113nd09AHr2vfn2228xatQo/O53v0N4eDiGDx+ODz/8UH4+IyMDeXl5dvcmICAAY8eO9fh7M378eGzZsgVnzpwBABw9ehQ7duzAzJkzAfTse2PLlfuwe/duBAYGYtSoUfI506ZNg0KhwN69ezt9zO5UXl4OQRAQGBgIwDPvTY/bOLO9FRUVwWg0IiIiwu54REQETp8+7aZRuZ/JZMKjjz6KCRMmYNCgQQCAvLw8qNVq+X8oSUREBPLy8twwys71xRdf4NChQ9i/f3+T53ryvTl//jyWL1+Oxx57DH/5y1+wf/9+PPzww1Cr1ViwYIH8+R39P+bp9+bpp59GRUUFUlJSoFQqYTQa8fLLL2PevHkA0KPvjS1X7kNeXh7Cw8PtnlepVAgODu5R96qurg5PPfUUbr/9dnnjTE+8NwxuqEMsXLgQx48fx44dO9w9lC4hOzsbjzzyCDZt2gStVuvu4XQpJpMJo0aNwl//+lcAwPDhw3H8+HGsWLECCxYscPPo3Gv16tX4/PPPsWrVKgwcOBBHjhzBo48+iujo6B5/b6j19Ho9brnlFoiiiOXLl7t7OB2KZanLFBoaCqVS2WRWS35+PiIjI900KvdatGgRNmzYgF9++QW9evWSj0dGRqKhoQFlZWV25/eEe3Xw4EEUFBRgxIgRUKlUUKlU2LZtG/75z39CpVIhIiKix96bqKgoDBgwwO5Y//79kZWVBQDy5++J/4/9v//3//D000/jtttuw+DBg/GHP/wBf/7zn7Fs2TIAPfve2HLlPkRGRjaZ5GEwGFBSUtIj7pUU2GRmZmLTpk1y1gbwzHvD4OYyqdVqjBw5Elu2bJGPmUwmbNmyBampqW4cWecTRRGLFi3C2rVr8fPPPyMxMdHu+ZEjR8LLy8vuXqWlpSErK8vj79XUqVNx7NgxHDlyRH6MGjUK8+bNk7/uqfdmwoQJTZYMOHPmDOLj4wEAiYmJiIyMtLs3FRUV2Lt3r8ffm5qaGigU9n9NK5VKmEwmAD373thy5T6kpqairKwMBw8elM/5+eefYTKZMHbs2E4fc2eSApv09HRs3rwZISEhds975L1xd0ezJ/jiiy9EjUYjrly5Ujx58qR4//33i4GBgWJeXp67h9apHnzwQTEgIEDcunWrmJubKz9qamrkcx544AExLi5O/Pnnn8UDBw6IqampYmpqqhtH7T62s6VEsefem3379okqlUp8+eWXxfT0dPHzzz8XfXx8xM8++0w+55VXXhEDAwPF9evXi7/99ps4e/ZsMTExUaytrXXjyDveggULxJiYGHHDhg1iRkaGuGbNGjE0NFR88skn5XN6yr2prKwUDx8+LB4+fFgEIP79738XDx8+LM/4ceU+zJgxQxw+fLi4d+9ecceOHWKfPn3E22+/3V0fqd00d28aGhrEG264QezVq5d45MgRu7+b6+vr5Wt42r1hcNNO3n77bTEuLk5Uq9XimDFjxD179rh7SJ0OgMPHxx9/LJ9TW1srPvTQQ2JQUJDo4+Mjzp07V8zNzXXfoN2ocXDTk+/N//73P3HQoEGiRqMRU1JSxA8++MDueZPJJC5ZskSMiIgQNRqNOHXqVDEtLc1No+08FRUV4iOPPCLGxcWJWq1W7N27t/jMM8/Y/VLqKffml19+cfj3y4IFC0RRdO0+FBcXi7fffrvo5+cn+vv7i3fddZdYWVnphk/Tvpq7NxkZGU7/bv7ll1/ka3javRFE0WapSyIiIqJujj03RERE5FEY3BAREZFHYXBDREREHoXBDREREXkUBjdERETkURjcEBERkUdhcENEREQehcENEREReRQGN0TUZRQWFuLBBx9EXFwcNBoNIiMjMX36dOzcuRMAIAgC1q1b595BElGXp3L3AIiIJDfddBMaGhrwn//8B71790Z+fj62bNmC4uJidw+NiLoRZm6IqEsoKyvDr7/+ildffRVXXnkl4uPjMWbMGCxevBg33HADEhISAABz586FIAjy9wCwfv16jBgxAlqtFr1798YLL7wAg8EgPy8IApYvX46ZM2fC29sbvXv3xtdffy0/39DQgEWLFiEqKgparRbx8fFYtmxZZ310ImpnDG6IqEvw8/ODn58f1q1bh/r6+ibP79+/HwDw8ccfIzc3V/7+119/xfz58/HII4/g5MmTeP/997Fy5Uq8/PLLdq9fsmQJbrrpJhw9ehTz5s3DbbfdhlOnTgEA/vnPf+Lbb7/F6tWrkZaWhs8//9wueCKi7oUbZxJRl/HNN9/gvvvuQ21tLUaMGIHJkyfjtttuw5AhQwCYMzBr167FnDlz5NdMmzYNU6dOxeLFi+Vjn332GZ588knk5OTIr3vggQewfPly+Zxx48ZhxIgReO+99/Dwww/jxIkT2Lx5MwRB6JwPS0QdhpkbIuoybrrpJuTk5ODbb7/FjBkzsHXrVowYMQIrV650+pqjR49i6dKlcubHz88P9913H3Jzc1FTUyOfl5qaave61NRUOXNz55134siRI+jXrx8efvhh/PTTTx3y+YioczC4IaIuRavV4uqrr8aSJUuwa9cu3HnnnXjuueecnl9VVYUXXngBR44ckR/Hjh1Deno6tFqtS+85YsQIZGRk4MUXX0RtbS1uueUW3Hzzze31kYiokzG4IaIubcCAAaiurgYAeHl5wWg02j0/YsQIpKWlITk5uclDobD+Fbdnzx671+3Zswf9+/eXv/f398ett96KDz/8EF9++SW++eYblJSUdOAnI6KOwqngRNQlFBcX43e/+x3uvvtuDBkyBDqdDgcOHMBrr72G2bNnAwASEhKwZcsWTJgwARqNBkFBQXj22Wdx/fXXIy4uDjfffDMUCgWOHj2K48eP46WXXpKv/9VXX2HUqFG44oor8Pnnn2Pfvn3417/+BQD4+9//jqioKAwfPhwKhQJfffUVIiMjERgY6I5bQUSXSyQi6gLq6urEp59+WhwxYoQYEBAg+vj4iP369RP/7//+T6ypqRFFURS//fZbMTk5WVSpVGJ8fLz82h9//FEcP3686O3tLfr7+4tjxowRP/jgA/l5AOK7774rXn311aJGoxETEhLEL7/8Un7+gw8+EIcNGyb6+vqK/v7+4tSpU8VDhw512mcnovbF2VJE5PEczbIiIs/FnhsiIiLyKAxuiIiIyKOwoZiIPB6r70Q9CzM3RERE5FEY3BAREZFHYXBDREREHoXBDREREXkUBjdERETkURjcEBERkUdhcENEREQehcENEREReRQGN0RERORR/j+Kz9e3SA3qAwAAAABJRU5ErkJggg==\n", "text/plain": [ "
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "# Fine-tune the model parameters.\n", "BATCH_SIZE = 4\n", "TRAIN_EXAMPLES = 512\n", "LEARNING_RATE = 0.003\n", "\n", "TRAIN_STEPS = TRAIN_EXAMPLES // BATCH_SIZE\n", "EVAL_STEPS = TRAIN_STEPS // 4 # Number of evaluation steps\n", "\n", "# Lists to store training losses\n", "losses = []\n", "train_data_it = train_data_iterator()\n", "\n", "# Learning rate schedule using cosine decay with warmup\n", "sched_fn = big_vision.utils.create_learning_rate_schedule(\n", " total_steps=TRAIN_STEPS+1, base=LEARNING_RATE,\n", " decay_type=\"cosine\", warmup_percent=0.50)\n", "\n", "# Perform training steps\n", "for step in range(1, TRAIN_STEPS+1):\n", " # Make list of N training examples.\n", " examples = [next(train_data_it) for _ in range(BATCH_SIZE)]\n", "\n", " # Convert list of examples into a dict of np.arrays and load onto devices.\n", " batch = jax.tree.map(lambda *x: np.stack(x), *examples)\n", " batch = big_vision.utils.reshard(batch, data_sharding)\n", "\n", " # Training step and report training loss\n", " learning_rate = sched_fn(step)\n", " params, loss = update_fn(params, batch, learning_rate)\n", "\n", " loss = jax.device_get(loss)\n", " losses.append(loss)\n", " print(f\"step: {step:2d}/{TRAIN_STEPS:2d} lr: {learning_rate:.5f} loss: {loss:.4f}\")\n", "\n", "# Plotting the loss graph\n", "plt.plot(losses, label='Training Loss')\n", "plt.title('Training Loss Over Steps')\n", "plt.xlabel('Steps')\n", "plt.ylabel('Loss')\n", "plt.legend()\n", "plt.show()" ] }, { "cell_type": "markdown", "metadata": { "id": "glScsFLVJ52c" }, "source": [ "# Predictions and Validation Comparison" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "R7Xjc9vyjqjt" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Predictions and Validation Examples: 10 Randomly Selected Images \n", "\n" ] }, { "data": { "text/html": [ "\n", "
\n", "
Prediction: juvenile in nest
\n", "
\n", "
\n", " \n", "
\n", "
\n", "
Validation: abbott's booby: keep an eye out for this majestic seabird soaring over the tropical indian and pacific oceans, especially around christmas island, noted for its large size and long wingspan while in flight.
\n", "
\n", " \n", "
\n", "
Prediction: albatross: observe this large seabird known for its brooding behavior, providing a visual spectacle with its wingspan and long flight feathers, and for nesting on remote islands in the southern ocean.
\n", "
\n", "
\n", " \n", "
\n", "
\n", "
Validation: albatross: observe this large seafaring bird known for its impressive wingspan and effortless gliding flight, a majestic sight in the southern hemisphere, noted for its long wings and buoyant flight.
\n", "
\n", " \n", "
\n", "
Prediction: african black-crowned night-jar: observe this large nocturnal bird of prey in the savannas and woodlands of sub-saharan africa, noted for its distinctive black crown and yellow bill.
\n", "
\n", "
\n", " \n", "
\n", "
\n", "
Validation: yellow-billed chough: observe this medium-sized crow easily recognizable by its sleek black feathers and contrasting long yellow bill, a common sight in the mountainous regions of europe and asia, noted for its acrobatic flight and yellow bill.
\n", "
\n", " \n", "
\n", "
Prediction: american redstart: observe this small insectivorous bird with red markings on its wings and tail, found in the deciduous forests of north america.
\n", "
\n", "
\n", " \n", "
\n", "
\n", "
Validation: american redstart: observe this small insectivorous warbler with contrasting black feathers and vibrant orange patches on its wings, a resident of north america, noted for its active foraging behavior and constant tail flicking.
\n", "
\n", " \n", "
\n", "
Prediction: american redstart: observe this small insectivorous bird with red underparts and black feathers, found in the woodlands of north and central america, noted for its slender body and long tail.
\n", "
\n", "
\n", " \n", "
\n", "
\n", "
Validation: american redstart: observe this small insectivorous warbler with contrasting black feathers and vibrant orange patches on its wings, a resident of north america, noted for its active foraging behavior and constant tail flicking.
\n", "
\n", " \n", "
\n", "
Prediction: american flamingo: observe this large wading bird, a standout in any tropical landscape, known for its vibrant pink plumage and long legs, a true icon of the caribbean.
\n", "
\n", "
\n", " \n", "
\n", "
\n", "
Validation: american flamingo: look for this large wading bird recognizable by its vibrant pink to red plumage, long legs, and distinctively curved bill, a striking inhabitant of the caribbean, galΓ‘pagos, and coastal regions of south america, noted for its long neck and legs.
\n", "
\n", " \n", "
\n", "
Prediction: cuban toreador: a close-knit family of medium-sized birds with elongated snouts and robust wings, inhabiting the forests of central and south america, noted for their impressive casque of feathers.
\n", "
\n", "
\n", " \n", "
\n", "
\n", "
Validation: african emerald cuckoo: look for this small parasitic bird with shimmering emerald feathers, a unique inhabitant of the forests of sub-saharan africa, characterized by its slender body and long tail.
\n", "
\n", " \n", "
\n", "
Prediction: african black hornbill: observe this medium-sized african bird with an impressive red bill and long beak, a distinctive sight endemic to the forests of southern and east africa, known for its impressive size and robust build.
\n", "
\n", "
\n", " \n", "
\n", "
\n", "
Validation: abyssinian ground hornbill: notice this large terrestrial bird commanding attention with its distinctive helmet-like structure on its head, a resident of the savannas and woodlands of sub-saharan africa, characterized by its powerful bill and long eyelashes.
\n", "
\n", " \n", "
\n", "
Prediction: american goldfinch: observe this small, colorful finch familiarizing himself with backyard feeders, noted for its distinctive yellow plumage and black cap.
\n", "
\n", "
\n", " \n", "
\n", "
\n", "
Validation: american goldfinch: notice this small, colorful finch with bright yellow feathers and contrasting black wings, bringing joy to the landscapes of north america, noted for its conical bill and black cap.
\n", "
\n", " \n", "
\n", "
Prediction: long-billed dowitch: look for this large, long-billed shorebird in the grasslands and savannas of southern africa, characterized by its distinctive black and white patterning and long, slender bill.
\n", "
\n", "
\n", " \n", "
\n", "
\n", "
Validation: alexandrine parakeet: notice this medium-sized parrot bursting with a rainbow of colors, a vibrant resident of the forests and woodlands of south and southeast asia, distinguished by its large size and distinctive red beak.
\n", "
\n", " " ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "def data_iterator(data_type=None):\n", " \"\"\"Iterates over examples for validation or prediction.\n", "\n", " Args:\n", " data_type (str): Type of data to iterate ('prediction' or 'validation').\n", "\n", " Yields:\n", " dict: Dictionary containing image and text data based on data_type.\n", " For 'prediction': {'image': np.array, 'text': np.array, 'mask_ar': np.array, 'mask_input': np.array}\n", " For 'validation': {'image': np.array, 'text': np.array, 'mask_ar': np.array, 'mask_loss': np.array}\n", " \"\"\"\n", " for example in val_dataset.get_tfdata(ordered=True).as_numpy_iterator():\n", " image = Image.open(io.BytesIO(example[\"image\"]))\n", " image = preprocess_image(image)\n", "\n", " prefix = \"describe en\"\n", " if data_type == \"prediction\":\n", " tokens, mask_ar, _, mask_input = preprocess_tokens(prefix, seqlen=SEQLEN)\n", "\n", " yield {\n", " \"image\": np.asarray(image),\n", " \"text\": np.asarray(tokens),\n", " \"mask_ar\": np.asarray(mask_ar),\n", " \"mask_input\": np.asarray(mask_input),\n", " }\n", " elif data_type == \"validation\":\n", " suffix = example[\"suffix\"].decode().lower()\n", " tokens, mask_ar, mask_loss, _ = preprocess_tokens(prefix, suffix, SEQLEN)\n", "\n", " yield {\n", " \"image\": np.asarray(image),\n", " \"text\": np.asarray(tokens),\n", " \"mask_ar\": np.asarray(mask_ar),\n", " \"mask_loss\": np.asarray(mask_loss),\n", " }\n", "\n", "def render_example(image, description):\n", " \"\"\"Renders an image with description in HTML format.\n", "\n", " Args:\n", " image (np.array): Image data as numpy array.\n", " description (str): Description text to display alongside the image.\n", "\n", " Returns:\n", " str: HTML formatted string for displaying the image and description.\n", " \"\"\"\n", " image = ((image + 1) / 2 * 255).astype(np.uint8) # [-1,1] -> [0, 255]\n", " return f\"\"\"\n", "
\n", " \n", "
\n", " \"\"\"\n", "\n", "def display_comparisons(predictions, validations):\n", " \"\"\"Displays side-by-side comparisons of predictions and validations.\n", "\n", " Args:\n", " predictions (list): List of tuples (image, description) for predictions.\n", " validations (list): List of tuples (image, description) for validations.\n", "\n", " Prints:\n", " Displays HTML output showing 10 randomly selected images with predictions and validations.\n", " \"\"\"\n", " html_out = \"\"\n", "\n", " # Select 10 random indices\n", " num_comparisons = min(10, len(predictions), len(validations))\n", " random_indices = random.sample(range(min(len(predictions), len(validations))), num_comparisons)\n", "\n", " for random_index in random_indices:\n", " pred_image, pred_description = predictions[random_index]\n", " val_image, val_description = validations[random_index]\n", "\n", " # Call render_example to get image content with description\n", " pred_content = render_example(pred_image, f\"Prediction: {pred_description}\")\n", " val_content = render_example(val_image, f\"Validation: {val_description}\")\n", "\n", " # Structure container with three columns and set widths\n", " html_out += f\"\"\"\n", "
\n", "
Prediction: {pred_description}
\n", "
{pred_content}
\n", "
Validation: {val_description}
\n", "
\n", " \"\"\"\n", " display(HTML(html_out))\n", "\n", "\n", "# Generate predictions and validations\n", "predictions = []\n", "for image, description_pred in make_predictions(data_iterator(\"prediction\"), batch_size=4):\n", " # Only append if both image and description_pred are not None\n", " if image is not None and description_pred is not None:\n", " predictions.append((image, description_pred))\n", "\n", "validations = []\n", "for example in data_iterator(\"validation\"):\n", " description = postprocess_tokens(example[\"text\"])\n", " description = description[len(\"describe en\\n\"):] # Strip prefix describe the image\n", " # Only append if image and description are not None\n", " if example[\"image\"] is not None and description is not None:\n", " validations.append((example[\"image\"], description))\n", "\n", "# Display predictions and validations side-by-side\n", "print(\"Predictions and Validation Examples: 10 Randomly Selected Images \\n\")\n", "display_comparisons(predictions, validations)" ] }, { "cell_type": "markdown", "metadata": { "id": "k-8GGxGMjqjt" }, "source": [ "# Conclusion\n", "\n", "This notebook fine-tuned PaliGemma on 3,692 image-description pairs from a diverse bird species dataset. Using 23 curated species with text descriptions, the model generally produced accurate descriptions similar to validation data. However, improvements are needed for better species identification accuracy, suggesting potential benefits from expanding training to include more of the 525 available species." ] }, { "cell_type": "markdown", "metadata": { "id": "e5HltsZsjqjt" }, "source": [ "# Reference\n", "- [Fine-tune PaliGemma with JAX](https://www.kaggle.com/code/nilaychauhan/fine-tune-paligemma-with-jax)\n", "- [PaliGemma model README](https://github.com/google-research/big_vision/blob/main/big_vision/configs/proj/paligemma/README.md)\n", "- [BIRDS 525 SPECIES- IMAGE CLASSIFICATION](https://www.kaggle.com/datasets/gpiosenka/100-bird-species)" ] } ], "metadata": { "accelerator": "GPU", "colab": { "name": "Finetune_PaliGemma_for_image_description.ipynb", "toc_visible": true }, "kernelspec": { "display_name": "Python 3", "name": "python3" } }, "nbformat": 4, "nbformat_minor": 0 }