{ "cells": [ { "cell_type": "code", "execution_count": 1, "metadata": { "id": "uq9k8YYUKjnp" }, "outputs": [], "source": [ "import os\n", "import urllib.request\n", "import zipfile\n", "import json\n", "import pandas as pd\n", "import time\n", "import torch\n", "import numpy as np\n", "import pandas as pd\n", "import torch.nn as nn\n", "import torch.nn.functional as F\n", "import torch.optim as optim\n", "from torch.utils.data import DataLoader, TensorDataset\n", "from sklearn.model_selection import train_test_split\n", "import matplotlib.pyplot as plt" ] }, { "cell_type": "code", "execution_count": 2, "metadata": { "id": "L5h3Tsa0LIoo" }, "outputs": [], "source": [ "def unzip_archive(filepath, dir_path):\n", " with zipfile.ZipFile(f\"{filepath}\", 'r') as zip_ref:\n", " zip_ref.extractall(dir_path)\n", "\n", "unzip_archive(os.getcwd() + '/data/raw/spotify_million_playlist_dataset.zip', os.getcwd() + '/data/raw/playlists')\n" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [], "source": [ "import shutil\n", "\n", "def make_dir(directory):\n", " if os.path.exists(directory):\n", " shutil.rmtree(directory)\n", " os.makedirs(directory)\n", " else:\n", " os.makedirs(directory)\n", " \n", "directory = os.getcwd() + '/data/raw/data'\n", "make_dir(directory)" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [], "source": [ "cols = [\n", " 'name',\n", " 'pid',\n", " 'num_followers',\n", " 'pos',\n", " 'artist_name',\n", " 'track_name',\n", " 'album_name'\n", "]" ] }, { "cell_type": "code", "execution_count": 5, "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "qyCujIu8cDGg", "outputId": "0964ace3-2916-49e3-eebf-2e08e61d95d9" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "mpd.slice.188000-188999.json\t100/1000\t10.0%" ] } ], "source": [ "\n", "directory = os.getcwd() + '/data/raw/playlists/data'\n", "df = pd.DataFrame()\n", "index = 0\n", "# Loop through all files in the directory\n", "for filename in os.listdir(directory):\n", " # Check if the item is a file (not a subdirectory)\n", " if os.path.isfile(os.path.join(directory, filename)):\n", " if filename.find('.json') != -1 :\n", " index += 1\n", "\n", " # Print the filename or perform operations on the file\n", " print(f'\\r{filename}\\t{index}/1000\\t{((index/1000)*100):.1f}%', end='')\n", "\n", " # If you need the full file path, you can use:\n", " full_path = os.path.join(directory, filename)\n", "\n", " with open(full_path, 'r') as file:\n", " json_data = json.load(file)\n", "\n", " temp = pd.DataFrame(json_data['playlists'])\n", " expanded_df = temp.explode('tracks').reset_index(drop=True)\n", "\n", " # Normalize the JSON data\n", " json_normalized = pd.json_normalize(expanded_df['tracks'])\n", "\n", " # Concatenate the original DataFrame with the normalized JSON data\n", " result = pd.concat([expanded_df.drop(columns=['tracks']), json_normalized], axis=1)\n", " \n", " result = result[cols]\n", "\n", " df = pd.concat([df, result], axis=0, ignore_index=True)\n", " \n", " if index % 50 == 0:\n", " df.to_parquet(f'{os.getcwd()}/data/raw/data/playlists_{index % 1000}.parquet')\n", " del df\n", " df = pd.DataFrame()\n", " if index % 100 == 0:\n", " break" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [], "source": [ "import pyarrow.parquet as pq\n", "\n", "def read_parquet_folder(folder_path):\n", " dataframes = []\n", " for file in os.listdir(folder_path):\n", " if file.endswith('.parquet'):\n", " file_path = os.path.join(folder_path, file)\n", " df = pd.read_parquet(file_path)\n", " dataframes.append(df)\n", " \n", " return pd.concat(dataframes, ignore_index=True)\n", "\n", "folder_path = os.getcwd() + '/data/raw/data'\n", "df = read_parquet_folder(folder_path)" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [], "source": [ "directory = os.getcwd() + '/data/raw/mappings'\n", "make_dir(directory)" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [], "source": [ "def create_ids(df, col, name):\n", " # Create a dictionary mapping unique values to IDs\n", " value_to_id = {val: i for i, val in enumerate(df[col].unique())}\n", "\n", " # Create a new column with the IDs\n", " df[f'{name}_id'] = df[col].map(value_to_id)\n", " df[[f'{name}_id', col]].drop_duplicates().to_csv(os.getcwd() + f'/data/raw/mappings/{name}.csv')\n", " # df = df.drop(col, axis=1)\n", " return df" ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [], "source": [ "df = create_ids(df, 'artist_name', 'artist')\n", "df = create_ids(df, 'pid', 'playlist')\n", "df = create_ids(df, 'track_name', 'song')\n", "df = create_ids(df, 'album_name', 'album')" ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [], "source": [ "df['artist_count'] = df.groupby(['playlist_id','artist_id'])['song_id'].transform('nunique')\n", "df['album_count'] = df.groupby(['playlist_id','artist_id'])['album_id'].transform('nunique')\n", "df['song_count'] = df.groupby(['playlist_id','artist_id'])['song_id'].transform('count')" ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [], "source": [ "df['playlist_songs'] = df.groupby(['playlist_id'])['pos'].transform('max')\n", "df['playlist_songs'] += 1" ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [], "source": [ "df['artist_percent'] = df['artist_count'] / df['playlist_songs']\n", "df['song_percent'] = df['song_count'] / df['playlist_songs']\n", "df['album_percent'] = df['album_count'] / df['playlist_songs']" ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \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", " \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", "
namepidnum_followersposartist_nametrack_namealbum_nameartist_idplaylist_idsong_idalbum_idartist_countalbum_countsong_countplaylist_songsartist_percentsong_percentalbum_percent
212throwbacks14300520R. KellyIgnition - RemixChocolate Factory10852031521111930.0051810.0051810.005181
213throwbacks14300521Backstreet BoysI Want It That WayOriginal Album Classics10952041531111930.0051810.0051810.005181
214throwbacks14300522*NSYNCBye Bye ByeNo Strings Attached11052051541111930.0051810.0051810.005181
215throwbacks14300523Fountains Of WayneStacy's MomWelcome Interstate Managers11152061551111930.0051810.0051810.005181
216throwbacks14300524Bowling For Soup1985A Hangover You Don't Deserve11252071561111930.0051810.0051810.005181
.........................................................
400throwbacks1430052188JoJoToo Little, Too Late - Radio VersionToo Little, Too Late19953902931111930.0051810.0051810.005181
401throwbacks1430052189Spice GirlsWannabe - Radio EditSpice20053912941111930.0051810.0051810.005181
402throwbacks1430052190MiMSThis Is Why I'm HotMusic Is My Savior20153922951111930.0051810.0051810.005181
403throwbacks1430052191RihannaDisturbiaGood Girl Gone Bad11553932963331930.0155440.0155440.015544
404throwbacks1430052192DEVBass Down LowThe Night The Sun Came Up17953942642121930.0103630.0103630.005181
\n", "

193 rows × 18 columns

\n", "
" ], "text/plain": [ " name pid num_followers pos artist_name \\\n", "212 throwbacks 143005 2 0 R. Kelly \n", "213 throwbacks 143005 2 1 Backstreet Boys \n", "214 throwbacks 143005 2 2 *NSYNC \n", "215 throwbacks 143005 2 3 Fountains Of Wayne \n", "216 throwbacks 143005 2 4 Bowling For Soup \n", ".. ... ... ... ... ... \n", "400 throwbacks 143005 2 188 JoJo \n", "401 throwbacks 143005 2 189 Spice Girls \n", "402 throwbacks 143005 2 190 MiMS \n", "403 throwbacks 143005 2 191 Rihanna \n", "404 throwbacks 143005 2 192 DEV \n", "\n", " track_name album_name \\\n", "212 Ignition - Remix Chocolate Factory \n", "213 I Want It That Way Original Album Classics \n", "214 Bye Bye Bye No Strings Attached \n", "215 Stacy's Mom Welcome Interstate Managers \n", "216 1985 A Hangover You Don't Deserve \n", ".. ... ... \n", "400 Too Little, Too Late - Radio Version Too Little, Too Late \n", "401 Wannabe - Radio Edit Spice \n", "402 This Is Why I'm Hot Music Is My Savior \n", "403 Disturbia Good Girl Gone Bad \n", "404 Bass Down Low The Night The Sun Came Up \n", "\n", " artist_id playlist_id song_id album_id artist_count album_count \\\n", "212 108 5 203 152 1 1 \n", "213 109 5 204 153 1 1 \n", "214 110 5 205 154 1 1 \n", "215 111 5 206 155 1 1 \n", "216 112 5 207 156 1 1 \n", ".. ... ... ... ... ... ... \n", "400 199 5 390 293 1 1 \n", "401 200 5 391 294 1 1 \n", "402 201 5 392 295 1 1 \n", "403 115 5 393 296 3 3 \n", "404 179 5 394 264 2 1 \n", "\n", " song_count playlist_songs artist_percent song_percent album_percent \n", "212 1 193 0.005181 0.005181 0.005181 \n", "213 1 193 0.005181 0.005181 0.005181 \n", "214 1 193 0.005181 0.005181 0.005181 \n", "215 1 193 0.005181 0.005181 0.005181 \n", "216 1 193 0.005181 0.005181 0.005181 \n", ".. ... ... ... ... ... \n", "400 1 193 0.005181 0.005181 0.005181 \n", "401 1 193 0.005181 0.005181 0.005181 \n", "402 1 193 0.005181 0.005181 0.005181 \n", "403 3 193 0.015544 0.015544 0.015544 \n", "404 2 193 0.010363 0.010363 0.005181 \n", "\n", "[193 rows x 18 columns]" ] }, "execution_count": 13, "metadata": {}, "output_type": "execute_result" } ], "source": [ "df[df['playlist_id'] == 5]" ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
playlist_idartist_idartist_percent
0000.571429
1000.571429
2000.571429
3000.571429
4000.571429
\n", "
" ], "text/plain": [ " playlist_id artist_id artist_percent\n", "0 0 0 0.571429\n", "1 0 0 0.571429\n", "2 0 0 0.571429\n", "3 0 0 0.571429\n", "4 0 0 0.571429" ] }, "execution_count": 14, "metadata": {}, "output_type": "execute_result" } ], "source": [ "artists = df.loc[:,['playlist_id','artist_id','album_id','album_percent']]\n", "artists.head()" ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [], "source": [ "X = artists.loc[:,['playlist_id','artist_id','album_id']]\n", "y = artists.loc[:,'album_percent']\n", "\n", "# Split our data into training and test sets\n", "X_train, X_val, y_train, y_val = train_test_split(X,y,random_state=0, test_size=0.2)" ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [], "source": [ "def prep_dataloaders(X_train,y_train,X_val,y_val,batch_size):\n", " # Convert training and test data to TensorDatasets\n", " trainset = TensorDataset(torch.from_numpy(np.array(X_train)).long(), \n", " torch.from_numpy(np.array(y_train)).float())\n", " valset = TensorDataset(torch.from_numpy(np.array(X_val)).long(), \n", " torch.from_numpy(np.array(y_val)).float())\n", "\n", " # Create Dataloaders for our training and test data to allow us to iterate over minibatches \n", " trainloader = torch.utils.data.DataLoader(trainset, batch_size=batch_size, shuffle=True)\n", " valloader = torch.utils.data.DataLoader(valset, batch_size=batch_size, shuffle=False)\n", "\n", " return trainloader, valloader\n", "\n", "batchsize = 64\n", "trainloader,valloader = prep_dataloaders(X_train,y_train,X_val,y_val,batchsize)" ] }, { "cell_type": "code", "execution_count": 17, "metadata": {}, "outputs": [], "source": [ "class NNColabFiltering(nn.Module):\n", " \n", " def __init__(self, n_playlists, n_artists, embedding_dim_users, embedding_dim_items, n_activations, rating_range):\n", " super().__init__()\n", " self.user_embeddings = nn.Embedding(num_embeddings=n_playlists,embedding_dim=embedding_dim_users)\n", " self.item_embeddings = nn.Embedding(num_embeddings=n_artists,embedding_dim=embedding_dim_items)\n", " self.fc1 = nn.Linear(embedding_dim_users+embedding_dim_items,n_activations)\n", " self.fc2 = nn.Linear(n_activations,1)\n", " self.rating_range = rating_range\n", "\n", " def forward(self, X):\n", " # Get embeddings for minibatch\n", " embedded_users = self.user_embeddings(X[:,0])\n", " embedded_items = self.item_embeddings(X[:,1])\n", " # Concatenate user and item embeddings\n", " embeddings = torch.cat([embedded_users,embedded_items],dim=1)\n", " # Pass embeddings through network\n", " preds = self.fc1(embeddings)\n", " preds = F.relu(preds)\n", " preds = self.fc2(preds)\n", " # Scale predicted ratings to target-range [low,high]\n", " preds = torch.sigmoid(preds) * (self.rating_range[1]-self.rating_range[0]) + self.rating_range[0]\n", " return preds" ] }, { "cell_type": "code", "execution_count": 19, "metadata": {}, "outputs": [], "source": [ "def train_model(model, criterion, optimizer, dataloaders, device, num_epochs=5, scheduler=None):\n", " model = model.to(device) # Send model to GPU if available\n", " since = time.time()\n", "\n", " costpaths = {'train':[],'val':[]}\n", "\n", " for epoch in range(num_epochs):\n", " print('Epoch {}/{}'.format(epoch, num_epochs - 1))\n", " print('-' * 10)\n", "\n", " # Each epoch has a training and validation phase\n", " for phase in ['train', 'val']:\n", " if phase == 'train':\n", " model.train() # Set model to training mode\n", " else:\n", " model.eval() # Set model to evaluate mode\n", "\n", " running_loss = 0.0\n", "\n", " # Get the inputs and labels, and send to GPU if available\n", " index = 0\n", " for (inputs,labels) in dataloaders[phase]:\n", " inputs = inputs.to(device)\n", " labels = labels.to(device)\n", "\n", " # Zero the weight gradients\n", " optimizer.zero_grad()\n", "\n", " # Forward pass to get outputs and calculate loss\n", " # Track gradient only for training data\n", " with torch.set_grad_enabled(phase == 'train'):\n", " outputs = model.forward(inputs).view(-1)\n", " loss = criterion(outputs, labels)\n", "\n", " # Backpropagation to get the gradients with respect to each weight\n", " # Only if in train\n", " if phase == 'train':\n", " loss.backward()\n", " # Update the weights\n", " optimizer.step()\n", "\n", " # Convert loss into a scalar and add it to running_loss\n", " running_loss += np.sqrt(loss.item()) * labels.size(0)\n", " print(f'\\r{running_loss} {index} {index / len(dataloaders[phase])}', end='')\n", " index +=1\n", "\n", " # Step along learning rate scheduler when in train\n", " if (phase == 'train') and (scheduler is not None):\n", " scheduler.step()\n", "\n", " # Calculate and display average loss and accuracy for the epoch\n", " epoch_loss = running_loss / len(dataloaders[phase].dataset)\n", " costpaths[phase].append(epoch_loss)\n", " print('{} loss: {:.4f}'.format(phase, epoch_loss))\n", "\n", " time_elapsed = time.time() - since\n", " print('Training complete in {:.0f}m {:.0f}s'.format(\n", " time_elapsed // 60, time_elapsed % 60))\n", "\n", " return costpaths" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Epoch 0/2\n", "----------\n", "910724978601.7391 123493 100.00%\n", "train loss: 115229.4395\n", "227700857865.127 30873 100.00%\n", "val loss: 115239.3512\n", "Epoch 1/2\n", "----------\n", "910727409277.4519 123493 100.00%\n", "train loss: 115229.7471\n", "227700857865.127 30873 100.00%\n", "val loss: 115239.3512\n", "Epoch 2/2\n", "----------\n", "910734475316.9005 123493 100.00%\n", "train loss: 115230.6411\n", "227700857865.127 30873 100.00%\n", "val loss: 115239.3512\n", "Training complete in 71m 54s\n" ] } ], "source": [ "dataloaders = {'train':trainloader, 'val':valloader}\n", "n_playlists = X.loc[:,'playlist_id'].max()+1\n", "n_artists = X.loc[:,'artist_id'].max()+1\n", "n_albums = X.loc[:,'album_id'].max()+1\n", "model = NNColabFiltering(\n", " n_playlists,\n", " n_artists,\n", " embedding_dim_users=50,\n", " embedding_dim_items=50,\n", " n_activations = 100,\n", " rating_range=[0.,n_albums]\n", ")\n", "criterion = nn.MSELoss()\n", "lr=0.001\n", "n_epochs=10\n", "wd=1e-3\n", "optimizer = optim.Adam(model.parameters(), lr=lr, weight_decay=wd)\n", "device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n", "\n", "costpaths = train_model(model,criterion,optimizer,dataloaders, device, n_epochs, scheduler=None)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [ { "data": { "image/png": "iVBORw0KGgoAAAANSUhEUgAABNoAAAHWCAYAAAChceSWAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/bCgiHAAAACXBIWXMAAA9hAAAPYQGoP6dpAACCgElEQVR4nOzde1zUVf7H8fdwGxCZQRBEBJWyxCsK3ii7WAaVmRdctcxL2sVW3dTd1txKrbYs27bsl+m2a2oXy7xmWpppaiXeQLwmmnlDBDRkRlGuM78/XGeX9RIq+h3g9Xw85rE753vmzOfMmB7efM/3a3I6nU4BAAAAAAAAuCoeRhcAAAAAAAAAVAUEbQAAAAAAAEAFIGgDAAAAAAAAKgBBGwAAAAAAAFABCNoAAAAAAACACkDQBgAAAAAAAFQAgjYAAAAAAACgAhC0AQAAAAAAABWAoA0AAAAAAACoAARtAFBBGjZsqEGDBhldBgAAAK6hmTNnymQy6cCBA0aXAsANEbQBqFbWrVunCRMmKC8vz+hSAAAAAABVjJfRBQDA9bRu3Tq9+OKLGjRokAIDAyt07PT0dHl48PsLAAAAAKiu+IkQAC7A4XCooKDgsl5jNpvl7e19jSoCAAAAALg7gjYA1caECRP0zDPPSJKioqJkMplc19cwmUwaPny4PvnkEzVr1kxms1nLli2TJP3tb3/TLbfcouDgYPn5+SkuLk7z5s07b/z/vUbbuet3/Pjjjxo9erRCQkLk7++vHj166NixY9dlzgAAANXdvHnzZDKZtGbNmvOO/eMf/5DJZNKOHTu0bds2DRo0SDfccIN8fX0VFhamwYMH69dffzWgagCVFVtHAVQbPXv21J49e/Tpp5/qrbfeUu3atSVJISEhkqRVq1bp888/1/Dhw1W7dm01bNhQkjR58mQ9+OCD6tevn4qKivTZZ5/pd7/7nZYsWaIuXbr85vuOGDFCtWrV0vjx43XgwAG9/fbbGj58uObMmXPN5goAAICzunTpopo1a+rzzz/XHXfcUebYnDlz1KxZMzVv3lxvvvmmfvnlFz366KMKCwvTzp079f7772vnzp1av369TCaTQTMAUJkQtAGoNlq2bKnY2Fh9+umn6t69uytIOyc9PV3bt29X06ZNy7Tv2bNHfn5+rufDhw9XbGys/v73v5craAsODtY333zjWpw5HA698847stlsslqtVz8xAAAAXJSfn5+6du2qefPm6Z133pGnp6ckKSsrS2vWrNGECRMkSb///e/1xz/+scxrO3TooIceekg//PCDbrvttutdOoBKiK2jAPBvd9xxx3khm6QyIduJEydks9l02223KTU1tVzjPvHEE2V+A3rbbbeptLRUBw8evPqiAQAA8Jv69OmjnJwcrV692tU2b948ORwO9enTR1LZNV9BQYGOHz+uDh06SFK5130AQNBWgV555RXdcsstqlGjRrnvZrhgwQIlJCQoODhYJpNJaWlp5/W58847XdeSOvcYOnSo6/jWrVv10EMPKTIyUn5+fmrSpIkmT5582fX/1vsAVV1UVNQF25csWaIOHTrI19dXQUFBCgkJ0dSpU2Wz2co1bv369cs8r1WrlqSzoR0AAACuvXvvvVdWq7XMpTvmzJmjVq1a6eabb5Yk5ebm6umnn1adOnXk5+enkJAQ1/qwvOs+ACBou0x33nmnZs6cecFjRUVF+t3vfqennnqq3OPl5+erY8eOev311y/Z7/HHH9fRo0ddj0mTJrmOpaSkKDQ0VB9//LF27typ5557TmPHjtW7775b7jrK8z5AVfffv8U85/vvv9eDDz4oX19fvffee/rqq6+0YsUKPfzww3I6neUa99z2hP9V3tcDAADg6pjNZnXv3l0LFy5USUmJjhw5oh9//NF1Npsk9e7dW//85z81dOhQLViwQN98843r5lgOh8Oo0gFUMlyjrQK9+OKLknTRIO5C+vfvL0k6cODAJfvVqFFDYWFhFzw2ePDgMs9vuOEGJScna8GCBRo+fLir/YsvvtCLL76oXbt2KTw8XAMHDtRzzz0nL6///DG41PsAVcHlXsR2/vz58vX11fLly2U2m13tM2bMqOjSAAAAcA316dNHs2bN0sqVK/XTTz/J6XS6grYTJ05o5cqVevHFFzVu3DjXa/bu3WtUuQAqKc5oqyQ++eQT1a5dW82bN9fYsWN1+vTpS/a32WwKCgpyPf/+++81YMAAPf3009q1a5f+8Y9/aObMmXrllVeu6n2Aysbf31+SlJeXV67+np6eMplMKi0tdbUdOHBAixYtugbVAQAA4Frp3LmzgoKCNGfOHM2ZM0ft2rVzbQ09twPhf3ccvP3229e7TACVHGe0VQIPP/ywGjRooPDwcG3btk1jxoxRenq6FixYcMH+69at05w5c7R06VJX24svvqhnn31WAwcOlHT2rLeXX35Zf/7znzV+/Pgreh+gMoqLi5MkPffcc+rbt6+8vb3VtWvXi/bv0qWL/v73v+vee+/Vww8/rJycHE2ZMkWNGjXStm3brlfZAAAAuEre3t7q2bOnPvvsM+Xn5+tvf/ub65jFYtHtt9+uSZMmqbi4WPXq1dM333yj/fv3G1gxgMqIoO03vPrqq3r11Vddz8+cOaP169eX2ZK5a9eu8y52XpGeeOIJ1/9v0aKF6tatq7vvvlv79u3TjTfeWKbvjh071K1bN40fP14JCQmu9q1bt+rHH38scwZbaWmpCgoKdPr0adWoUeOy3georNq2bauXX35Z06ZN07Jly+RwOC65gLrrrrs0ffp0vfbaaxo5cqSioqL0+uuv68CBAwRtAAAAlUyfPn30r3/9SyaTSb179y5zbPbs2RoxYoSmTJkip9OphIQEff311woPDzeoWgCVkcnJ1bgvKTc3V7m5ua7n/fr1U1JSknr27Olqa9iwYZnrnM2cOVMjR44s99Y06exWtKioKG3ZskWtWrW6ZN/8/HzVrFlTy5YtU2Jioqt9165d6tSpkx577LHztoT6+fnpxRdfLFP3OTfccIM8PM7fRXyx9wEAAAAAAMD5OKPtNwQFBZW51pmfn59CQ0PVqFEjw2pKS0uTJNWtW9fVtnPnTt11110aOHDgeSGbJMXGxio9Pf2y6r7Q+wAAAAAAAODCCNoq0KFDh5Sbm6tDhw6ptLTUFVQ1atRINWvWlCRFR0dr4sSJ6tGjhyS5+mdmZkqS0tPTJUlhYWEKCwvTvn37NHv2bN1///0KDg7Wtm3bNGrUKN1+++1q2bKlpLPbRe+66y4lJiZq9OjRysrKknT2gp4hISGSpHHjxumBBx5Q/fr11atXL3l4eGjr1q3asWOH/vrXv5brfQAAAAAAAHBx3HW0Ao0bN06tW7fW+PHjderUKbVu3VqtW7fW5s2bXX3S09Nls9lczxcvXqzWrVurS5cukqS+ffuqdevWmjZtmiTJx8dH3377rRISEhQdHa0//vGPSkpK0pdffukaY968eTp27Jg+/vhj1a1b1/Vo27atq09iYqKWLFmib775Rm3btlWHDh301ltvqUGDBuV+HwAAAAAAAFwc12gDAAAAAAAAKgBntAEAAAAAAAAVgKANAAAAAAAAqADcDOECHA6HMjMzFRAQIJPJZHQ5AACgknA6nTp58qTCw8Pl4cHvM90R6zwAAHAlyrvOI2i7gMzMTEVGRhpdBgAAqKQOHz6siIgIo8vABbDOAwAAV+O31nkEbRcQEBAg6eyHZ7FYDK4GAABUFna7XZGRka61BNwP6zwAAHAlyrvOI2i7gHPbCCwWCwswAABw2diS6L5Y5wEAgKvxW+s8Lh4CAAAAAAAAVACCNgAAAAAAAKACELQBAAAAAAAAFYCgDQAAAAAAAKgABG0AAAAAAABABSBoAwAAAAAAACoAQRsAAAAAAABQAQjaAAAAAAAAgApA0AYAAAAAAABUAII2AAAAAAAAoAIQtAEAAAAAAAAVgKANAAAAAAAAqAAEbQAAoNqwnS42ugQAAABUYQRtAACgWlj/y6/q+Poqfb39qNGlAAAAoIoiaAMAAFXejiM2PTZrs04WlmhR2hE5nU6jSwIAAEAVRNAGAACqtF+OndLADzbqVGGJ2kcFaXLf1jKZTEaXBQAAgCqIoA0AAFRZWbYC9Z++Ub/mF6lZuEX/HNhGvt6eRpcFAACAKoqgDQAAVEl5p4vUf/oGHck7o6ja/po1uJ0svt5GlwUAAIAqjKANAABUOfmFJRo0Y5P25pxSHYtZHw5up9o1zUaXBQAAgCqOoA0AAFQpRSUODf04RWmH82T189ZHQ9orMqiG0WUBAACgGiBoAwAAVUapw6nRn6fp+73HVcPHUzMebaub6wQYXRYAAACqCYI2AABQJTidTo37YoeWbDsqb0+Tpj0Sp9j6tYwuCwAAANWIoUHb1KlT1bJlS1ksFlksFsXHx+vrr792HS8oKNCwYcMUHBysmjVrKikpSdnZ2b857k8//aQHH3xQVqtV/v7+atu2rQ4dOnQtpwIAAAz29xV79MmGQzKZpLf6tNLtN4cYXRIAAACqGUODtoiICL322mtKSUnR5s2bddddd6lbt27auXOnJGnUqFH68ssvNXfuXK1Zs0aZmZnq2bPnJcfct2+fOnbsqOjoaK1evVrbtm3TCy+8IF9f3+sxJQAAYIDpP+zX/636WZL0crfmeqBluMEVAQAAoDoyOZ1Op9FF/LegoCC98cYb6tWrl0JCQjR79mz16tVLkrR79241adJEycnJ6tChwwVf37dvX3l7e+ujjz664hrsdrusVqtsNpssFssVjwMAAK69BakZGv35VknSnxJu1vC7bjKsFtYQ7o/vCAAAXInyriHc5hptpaWl+uyzz5Sfn6/4+HilpKSouLhYnTt3dvWJjo5W/fr1lZycfMExHA6Hli5dqptvvlmJiYkKDQ1V+/bttWjRoku+d2Fhoex2e5kHAABwf9/uytYz87ZJkgbfGqVhnRoZXBEAAACqM8ODtu3bt6tmzZoym80aOnSoFi5cqKZNmyorK0s+Pj4KDAws079OnTrKysq64Fg5OTk6deqUXnvtNd1777365ptv1KNHD/Xs2VNr1qy5aA0TJ06U1Wp1PSIjIytyigAA4BrYuD9Xw2anqtThVM/W9fR8lyYymUxGlwUAAIBqzMvoAho3bqy0tDTZbDbNmzdPAwcOvGQodikOh0OS1K1bN40aNUqS1KpVK61bt07Tpk3THXfcccHXjR07VqNHj3Y9t9vthG0AALixnZk2DZm5SYUlDt0dHarXe7WUhwchGwAAAIxleNDm4+OjRo3ObvOIi4vTpk2bNHnyZPXp00dFRUXKy8src1Zbdna2wsLCLjhW7dq15eXlpaZNm5Zpb9KkiX744YeL1mA2m2U2m69+MgAA4Jo7cDxfAz/YpJOFJWrXMEhT+sXK29Pwk/QBAAAA47eO/i+Hw6HCwkLFxcXJ29tbK1eudB1LT0/XoUOHFB8ff8HX+vj4qG3btkpPTy/TvmfPHjVo0OCa1g0AAK69bHuBHpm+QcdPFapJXYv+ObCNfL09jS4LAAAAkGTwGW1jx47Vfffdp/r16+vkyZOaPXu2Vq9ereXLl8tqtWrIkCEaPXq0goKCZLFYNGLECMXHx5e542h0dLQmTpyoHj16SJKeeeYZ9enTR7fffrs6deqkZcuW6csvv9Tq1asNmiUAAKgIeaeLNGD6RmWcOKOGwTX04eB2svp5G10WAAAA4GJo0JaTk6MBAwbo6NGjslqtatmypZYvX6577rlHkvTWW2/Jw8NDSUlJKiwsVGJiot57770yY6Snp8tms7me9+jRQ9OmTdPEiRP1hz/8QY0bN9b8+fPVsWPH6zo3AABQcU4XlWjwzE1Kzz6p0ACzPhrSXiEBXPYBAAAA7sXkdDqdRhfhbux2u6xWq2w2mywWi9HlAABQrRWVOPT4h5u1Zs8xWXy9NHfoLWocFmB0WRfEGsL98R0BAIArUd41hNtdow0AAOAch8OpP87dqjV7jsnP21MzHm3ntiEbAAAAQNAGAADcktPp1IQvd+rLrZny8jBp6iOximtQy+iyAAAAgIsiaAMAAG7p7W/36sPkgzKZpDd7x+jOxqFGlwQAAABcEkEbAABwOzN/3K/JK/dKkl56sJm6tapncEUAAADAbyNoAwAAbmXRliOa8OUuSdKozjerf3xDYwsCAAAAyomgDQAAuI3vdufoT3O3SpIG3dJQf7i7kcEVAQAAAOVH0AYAANzCpgO5euqTFJU4nOreKlzjHmgqk8lkdFkAAABAuRG0AQAAw/101K7BMzepoNihu6JD9cbvYuThQcgGAACAyoWgDQAAGOrgr/ka8MFGnSwoUZsGtTTl4Vh5e7JEuV7Wrl2rrl27Kjw8XCaTSYsWLXIdKy4u1pgxY9SiRQv5+/srPDxcAwYMUGZmZpkxcnNz1a9fP1ksFgUGBmrIkCE6depUmT7btm3TbbfdJl9fX0VGRmrSpEnn1TJ37lxFR0fL19dXLVq00FdffVXmuNPp1Lhx41S3bl35+fmpc+fO2rt3b8V9GAAAAFeJVSwAADBMjr1A/adv1LGThYoOC9D0QW3l5+NpdFnVSn5+vmJiYjRlypTzjp0+fVqpqal64YUXlJqaqgULFig9PV0PPvhgmX79+vXTzp07tWLFCi1ZskRr167VE0884Tput9uVkJCgBg0aKCUlRW+88YYmTJig999/39Vn3bp1euihhzRkyBBt2bJF3bt3V/fu3bVjxw5Xn0mTJumdd97RtGnTtGHDBvn7+ysxMVEFBQXX4JMBAAC4fCan0+k0ugh3Y7fbZbVaZbPZZLFYjC4HAIAqyXa6WH3eT9burJOqH1RD84bGK9Tia3RZV6WyryFMJpMWLlyo7t27X7TPpk2b1K5dOx08eFD169fXTz/9pKZNm2rTpk1q06aNJGnZsmW6//77lZGRofDwcE2dOlXPPfecsrKy5OPjI0l69tlntWjRIu3evVuS1KdPH+Xn52vJkiWu9+rQoYNatWqladOmyel0Kjw8XH/84x/1pz/9SZJks9lUp04dzZw5U3379i3XHCv7dwQAAIxR3jUEZ7QBAIDr7kxRqYbM2qTdWScVEmDWx0PaV/qQrbqw2WwymUwKDAyUJCUnJyswMNAVsklS586d5eHhoQ0bNrj63H777a6QTZISExOVnp6uEydOuPp07ty5zHslJiYqOTlZkrR//35lZWWV6WO1WtW+fXtXnwspLCyU3W4v8wAAALhWCNoAAMB1VVzq0O8/SdHmgycU4OulDwe3U/3gGkaXhXIoKCjQmDFj9NBDD7l+k5uVlaXQ0NAy/by8vBQUFKSsrCxXnzp16pTpc+75b/X57+P//boL9bmQiRMnymq1uh6RkZGXNWcAAIDLQdAGAACuG4fDqWfmbtV36cfk6+2hGYPaqkldtu9VBsXFxerdu7ecTqemTp1qdDnlNnbsWNlsNtfj8OHDRpcEAACqMC+jCwAAANWD0+nUS0t2aVFaprw8TJraL05tGgYZXRbK4VzIdvDgQa1atarMdUnCwsKUk5NTpn9JSYlyc3MVFhbm6pOdnV2mz7nnv9Xnv4+fa6tbt26ZPq1atbpo7WazWWaz+XKmCwAAcMU4ow0AAFwX76z8WTPXHZAkvdk7Rp2iQy/9AriFcyHb3r179e233yo4OLjM8fj4eOXl5SklJcXVtmrVKjkcDrVv397VZ+3atSouLnb1WbFihRo3bqxatWq5+qxcubLM2CtWrFB8fLwkKSoqSmFhYWX62O12bdiwwdUHAADAaARtAADgmvsw+YDe+naPJGlC16bq1qqewRXhnFOnTiktLU1paWmSzt50IC0tTYcOHVJxcbF69eqlzZs365NPPlFpaamysrKUlZWloqIiSVKTJk1077336vHHH9fGjRv1448/avjw4erbt6/Cw8MlSQ8//LB8fHw0ZMgQ7dy5U3PmzNHkyZM1evRoVx1PP/20li1bpjfffFO7d+/WhAkTtHnzZg0fPlzS2Tuijhw5Un/961+1ePFibd++XQMGDFB4ePgl75IKAABwPZmcTqfT6CLcDbd9BwCg4nyRdkQj56TJ6ZSevvsmjbrnZqNLumYq4xpi9erV6tSp03ntAwcO1IQJExQVFXXB13333Xe68847JUm5ubkaPny4vvzyS3l4eCgpKUnvvPOOatas6eq/bds2DRs2TJs2bVLt2rU1YsQIjRkzpsyYc+fO1fPPP68DBw7opptu0qRJk3T//fe7jjudTo0fP17vv/++8vLy1LFjR7333nu6+eby/5mqjN8RAAAwXnnXEARtF8ACDACAirE6PUePzdqsEodTA+Ib6MUHm8lkMhld1jXDGsL98R0BAIArUd41BFtHAQDANZFyMFdDP05RicOpB2PCNaFr1Q7ZAAAAAII2AABQ4XZn2fXojE0qKHbojptD9LffxcjDg5ANAAAAVRtBGwAAqFCHc09rwPSNsheUKLZ+oKY+EisfL5YcAAAAqPpY9QIAgApz7GShHpm+QTknC9W4ToA+GNRWNXy8jC4LAAAAuC4I2gAAQIWwnSnWgA826uCvpxUZ5KcPh7RTYA0fo8sCAAAArhuCNgAAcNXOFJXq8Vmb9dNRu2rXNOujwe1Vx+JrdFkAAADAdUXQBgAArkpxqUPDZ6dq44FcBfh66cPB7dSwtr/RZQEAAADXHUEbAAC4Yg6HU3+et00rd+fI7OWh6QPbqmm4xeiyAAAAAEMQtAEAgCvidDr18tJdWrjliDw9THqvX6zaRQUZXRYAAABgGII2AABwRaZ897Nm/HhAkvRGr5a6u0kdYwsCAAAADEbQBgAALtvH6w/qb9/skSSNe6CpesZGGFwRAAAAYDyCNgAAcFmWbMvUC1/skCSNuKuRBneMMrgiAAAAwD0QtAEAgHJbu+eYRs1Jk9Mp9WtfX6PvudnokgAAAAC3QdAGAADKJfXQCT35UYqKS516oGVdvdStuUwmk9FlAQAAAG6DoA0AAPymPdkn9eiMTTpTXKrbbqqtv/duJU8PQjYAAADgvxG0AQCASzqce1r9p2+Q7UyxWtcP1D/6x8nHiyUEAAAA8L9YJQMAgIs6drJQ/advULa9UDfXqakZg9qqho+X0WUBAAAAbomgDQAAXJC9oFiDZmzUgV9Pq16gnz4c3F6BNXyMLgsAAABwWwRtAADgPAXFpXps1mbtzLQr2N9HHz/WXmFWX6PLAgAAANwaQRsAACijpNSh4bO3aOP+XAWYvTRrcDtF1fY3uiwAAADA7RG0AQAAF4fDqTHzt+vbn7Ll4+Whfw5so+b1rEaXBQAAAFQKBG0AAECS5HQ69epXP2l+aoY8PUya8nCsOtwQbHRZAAAAQKVB0AYAACRJU9fs079+2C9JmpTUUvc0rWNwRQAAAEDlQtAGAAA0e8MhTVqWLkl6vksTJcVFGFwRAAAAUPkQtAEAUM19tf2onlu0XZI0rNONeuy2GwyuCAAAAKicCNoAAKjGvt97TE9/tkVOp/RQu/r6U0Jjo0sCAAAAKi2CNgAAqqm0w3l68qMUFZc6dX+LMP21e3OZTCajywIAAAAqLYI2AACqoZ9zTmrQjI06XVSqjo1q660+reTpQcgGAAAAXA2CNgAAqpmME6f1yL82Ku90sWIiA/WP/nEye3kaXRYAAABQ6RkatE2dOlUtW7aUxWKRxWJRfHy8vv76a9fxgoICDRs2TMHBwapZs6aSkpKUnZ1d7vGHDh0qk8mkt99++xpUDwBA5fPrqUINmL5RWfYCNQqtqRmD2srf7GV0WQAAAECVYGjQFhERoddee00pKSnavHmz7rrrLnXr1k07d+6UJI0aNUpffvml5s6dqzVr1igzM1M9e/Ys19gLFy7U+vXrFR4efi2nAABApXGyoFiDZmzSL8fzVS/QTx8Naacgfx+jywIAAACqDEN/hd21a9cyz1955RVNnTpV69evV0REhKZPn67Zs2frrrvukiTNmDFDTZo00fr169WhQ4eLjnvkyBGNGDFCy5cvV5cuXX6zjsLCQhUWFrqe2+32K5wRAADuqaC4VE98mKLtR2wK9vfRR0Paqa7Vz+iyAAAAgCrFba7RVlpaqs8++0z5+fmKj49XSkqKiouL1blzZ1ef6Oho1a9fX8nJyRcdx+FwqH///nrmmWfUrFmzcr33xIkTZbVaXY/IyMirng8AAO6ipNShP3y6Rcm//KqaZi/NfLSdbgipaXRZAAAAQJVjeNC2fft21axZU2azWUOHDtXChQvVtGlTZWVlycfHR4GBgWX616lTR1lZWRcd7/XXX5eXl5f+8Ic/lLuGsWPHymazuR6HDx++0ukAAOBWnE6nxi7Yrm92ZcvHy0P/HNBGLSKsRpcFAAAAVEmGX/24cePGSktLk81m07x58zRw4ECtWbPmisZKSUnR5MmTlZqaKpPJVO7Xmc1mmc3mK3pPAADc2Wtf79bclAx5mKT/e6i14m8MNrokAAAAoMoy/Iw2Hx8fNWrUSHFxcZo4caJiYmI0efJkhYWFqaioSHl5eWX6Z2dnKyws7IJjff/998rJyVH9+vXl5eUlLy8vHTx4UH/84x/VsGHDaz8ZAADcyLQ1+/SPtb9Ikl5LaqnEZhf+9xMAAABAxTA8aPtfDodDhYWFiouLk7e3t1auXOk6lp6erkOHDik+Pv6Cr+3fv7+2bdumtLQ01yM8PFzPPPOMli9ffr2mAACA4T7beEivfb1bkvSX+6PVuw3XHwUAAACuNUO3jo4dO1b33Xef6tevr5MnT2r27NlavXq1li9fLqvVqiFDhmj06NEKCgqSxWLRiBEjFB8fX+aOo9HR0Zo4caJ69Oih4OBgBQeX3RLj7e2tsLAwNW7c+HpPDwAAQyzbcVR/WbhdkjT0jhv1xO03GlwRAAAAUD0YGrTl5ORowIABOnr0qKxWq1q2bKnly5frnnvukSS99dZb8vDwUFJSkgoLC5WYmKj33nuvzBjp6emy2WxGlA8AgNtZ9/Nx/eHTNDmcUt+2kRpzL79oAgAAAK4Xk9PpdBpdhLux2+2yWq2y2WyyWCxGlwMAQLlsy8jTQ++vV35Rqe5tFqYp/WLl6VH+mwPh6rGGcH98RwAA4EqUdw3hdtdoAwAAl+/nnFMaNGOT8otKdWujYE1+qBUhGwAAAHCdEbQBAFDJHck7owHTNyg3v0gtI6z6R/82Mnt5Gl0WAAAAUO0QtAEAUIn9eqpQ/advUKatQDeE+Gvmo+1U02zoJVgBAACAaougDQCASupUYYkenblJvxzLV7jVVx8Paa8gfx+jywIAAACqLYI2AAAqocKSUj3x4WZty7CpVg1vfTikvcID/YwuCwAAAKjWCNoAAKhkSh1OPf1pmtbt+1X+Pp6a+Wg7NQqtaXRZAAAAQLVH0AYAQCXidDr13MLtWrYzSz6eHnp/QBvFRAYaXRYAAAAAEbQBAFCpTFqers82HZaHSXrnoVa6tVFto0sCAAAA8G8EbQAAVBLvr92nqav3SZIm9myhe5vXNbgiAAAAAP+NoA0AgErg882H9epXuyVJz94XrT5t6xtcEaqKtWvXqmvXrgoPD5fJZNKiRYvKHF+wYIESEhIUHBwsk8mktLS088bIyspS//79FRYWJn9/f8XGxmr+/Pll+uTm5qpfv36yWCwKDAzUkCFDdOrUqTJ9tm3bpttuu02+vr6KjIzUpEmTznuvuXPnKjo6Wr6+vmrRooW++uqrq/4MAAAAKgpBGwAAbm75ziw9O3+bJOnJ22/Q0DtuNLgiVCX5+fmKiYnRlClTLnq8Y8eOev311y86xoABA5Senq7Fixdr+/bt6tmzp3r37q0tW7a4+vTr1087d+7UihUrtGTJEq1du1ZPPPGE67jdbldCQoIaNGiglJQUvfHGG5owYYLef/99V59169bpoYce0pAhQ7RlyxZ1795d3bt3144dOyrgkwAAALh6JqfT6TS6CHdjt9tltVpls9lksViMLgcAUI2t23dcg2ZsUlGJQ73bROj1pJYymUxGl4WLqOxrCJPJpIULF6p79+7nHTtw4ICioqK0ZcsWtWrVqsyxmjVraurUqerfv7+rLTg4WK+//roee+wx/fTTT2ratKk2bdqkNm3aSJKWLVum+++/XxkZGQoPD9fUqVP13HPPKSsrSz4+PpKkZ599VosWLdLu3WfP5uzTp4/y8/O1ZMkS1/t06NBBrVq10rRp08o1x8r+HQEAAGOUdw3BGW0AALip7Rk2PfFhiopKHEpoWkev9mhByAa3dMstt2jOnDnKzc2Vw+HQZ599poKCAt15552SpOTkZAUGBrpCNknq3LmzPDw8tGHDBlef22+/3RWySVJiYqLS09N14sQJV5/OnTuXee/ExEQlJydftLbCwkLZ7fYyDwAAgGuFoA0AADe079gpDZyxUacKS9ThhiC981BreXnyzzbc0+eff67i4mIFBwfLbDbrySef1MKFC9WoUSNJZ6/hFhoaWuY1Xl5eCgoKUlZWlqtPnTp1yvQ59/y3+pw7fiETJ06U1Wp1PSIjI69usgAAAJfAih0AADdz1HZGA6ZvVG5+kZrXs+ifA9rI19vT6LKAi3rhhReUl5enb7/9Vps3b9bo0aPVu3dvbd++3ejSNHbsWNlsNtfj8OHDRpcEAACqMC+jCwAAAP9xIr9I/adv1JG8M7qhtr9mPtpOAb7eRpcFXNS+ffv07rvvaseOHWrWrJkkKSYmRt9//72mTJmiadOmKSwsTDk5OWVeV1JSotzcXIWFhUmSwsLClJ2dXabPuee/1efc8Qsxm80ym81XN0kAAIBy4ow2AADcRH5hiQbN3KSfc06prtVXHw5pp9o1CQjg3k6fPi1J8vAou6z09PSUw+GQJMXHxysvL08pKSmu46tWrZLD4VD79u1dfdauXavi4mJXnxUrVqhx48aqVauWq8/KlSvLvM+KFSsUHx9f8RMDAAC4AgRtAAC4gcKSUg39OEVbD+epVg1vfTSknSJq1TC6LFQDp06dUlpamtLS0iRJ+/fvV1pamg4dOiRJys3NVVpamnbt2iVJSk9PV1pamuu6aNHR0WrUqJGefPJJbdy4Ufv27dObb76pFStWuO5e2qRJE9177716/PHHtXHjRv34448aPny4+vbtq/DwcEnSww8/LB8fHw0ZMkQ7d+7UnDlzNHnyZI0ePdpV69NPP61ly5bpzTff1O7duzVhwgRt3rxZw4cPv06fFgAAwKURtAEAYLBSh1Oj52zV93uPq4aPp2Y82k6NQgOMLgvVxObNm9W6dWu1bt1akjR69Gi1bt1a48aNkyQtXrxYrVu3VpcuXSRJffv2VevWrTVt2jRJkre3t7766iuFhISoa9euatmypT788EPNmjVL999/v+t9PvnkE0VHR+vuu+/W/fffr44dO+r99993Hbdarfrmm2+0f/9+xcXF6Y9//KPGjRunJ554wtXnlltu0ezZs/X+++8rJiZG8+bN06JFi9S8efNr/jkBAACUh8npdDqNLsLd2O12Wa1W2Ww2WSwWo8sBAFRhTqdTf1m4Q59uPCRvT5NmDGqnjjfVNrosXCHWEO6P7wgAAFyJ8q4hOKMNAAAD/e2bdH268ZBMJmly39aEbAAAAEAlRtAGAIBB/vX9L5ry3T5J0ivdW+j+FnUNrggAAADA1SBoAwDAAPNSMvTXpT9Jkp5JbKyH29c3uCIAAAAAV4ugDQCA62zFrmyNmb9NkvRYxyj9/s4bDa4IAAAAQEUgaAMA4Dpa/8uvGjY7VaUOp5JiI/SX+5vIZDIZXRYAAACACkDQBgDAdbLjiE2Pz9qsohKHOjepo9eTWsjDg5ANAAAAqCoI2gAAuA72H8/XoBkbdbKwRO2jgvTuw63l5ck/wwAAAEBVwgofAIBrLMtWoEf+tUHHTxWpWbhF/xzYRr7enkaXBQAAAKCCEbQBAHAN5Z0u0oAPNuhI3hlF1fbXrMHtZPH1NrosAAAAANcAQRsAANdIfmGJBs3YpD3Zp1THYtaHg9updk2z0WUBAAAAuEYI2gAAuAaKShwa+nGK0g7nyernrY+GtFdkUA2jywIAAABwDRG0AQBQwUodTo3+PE3f7z0uP29PzXi0rW6uE2B0WQAAAACuMYI2AAAqkNPp1LgvdmjJtqPy9jRpWv84xdavZXRZAAAAAK4DgjYAACrQWyv26JMNh2QySX/v3Up33BxidEkAAAAArhOCNgAAKsgHP+zXO6t+liS93K25usaEG1wRAAAAgOuJoA0AgAqwcEuGXlqyS5L0p4Sb9UiHBgZXBAAAAOB6I2gDAOAqrdqdrT/N3SZJGnxrlIZ1amRwRQAAAACMQNAGAMBV2Lg/V099nKpSh1M9W9fT812ayGQyGV0WAAAAAAMQtAEAcIV2Zto0ZOYmFZY4dHd0qF7v1VIeHoRsAAAAQHVF0AYAwBU4cDxfAz/YpJOFJWrXMEhT+sXK25N/VgEAAIDqjJ8IAAC4TNn2Aj0yfYOOnypUk7oW/XNgG/l6expdFgAAAACDEbQBAHAZbKeLNWD6RmWcOKMGwTU0a3BbWf28jS4LAAAAgBsgaAMAoJxOF5Vo8KxNSs8+qdAAsz4e0l6hAb5GlwUAAADATRC0AQBQDkUlDj31capSDp6QxddLHw1pr8igGkaXBQAAAMCNELQBAPAbHA6n/jR3q9bsOSY/b0/NeLSdGocFGF0WAAAAADdD0AYAwCU4nU69+OVOLd6aKS8Pk6Y+Equ4BrWMLgsAAACAGyJoAwDgEt7+dq9mJR+UySS92TtGdzYONbokAAAAAG7K0KBt6tSpatmypSwWiywWi+Lj4/X111+7jhcUFGjYsGEKDg5WzZo1lZSUpOzs7IuOV1xcrDFjxqhFixby9/dXeHi4BgwYoMzMzOsxHQBAFTPzx/2avHKvJOmlB5upW6t6BlcEAAAAwJ0ZGrRFRETotddeU0pKijZv3qy77rpL3bp1086dOyVJo0aN0pdffqm5c+dqzZo1yszMVM+ePS863unTp5WamqoXXnhBqampWrBggdLT0/Xggw9erykBAKqIRVuOaMKXuyRJozrfrP7xDY0tCAAAAIDbMzmdTqfRRfy3oKAgvfHGG+rVq5dCQkI0e/Zs9erVS5K0e/duNWnSRMnJyerQoUO5xtu0aZPatWungwcPqn79+uV6jd1ul9Vqlc1mk8ViueK5AAAqp+925+jxDzerxOHUoFsaanzXpjKZTEaXhUqANYT74zsCAABXorxrCLe5Rltpaak+++wz5efnKz4+XikpKSouLlbnzp1dfaKjo1W/fn0lJyeXe1ybzSaTyaTAwMCL9iksLJTdbi/zAABUT5sP5OqpT1JU4nCqW6twjXuAkA0AAABA+RgetG3fvl01a9aU2WzW0KFDtXDhQjVt2lRZWVny8fE5LyCrU6eOsrKyyjV2QUGBxowZo4ceeuiSaePEiRNltVpdj8jIyKuZEgCgkvrpqF2DZ25SQbFDnRqH6G+/i5GHByEbAAAAgPIxPGhr3Lix0tLStGHDBj311FMaOHCgdu3addXjFhcXq3fv3nI6nZo6deol+44dO1Y2m831OHz48FW/PwCgcjn062kN+GCj7AUlatOglt7rFydvT8P/mQQAAABQiXgZXYCPj48aNWokSYqLi9OmTZs0efJk9enTR0VFRcrLyytzVlt2drbCwsIuOea5kO3gwYNatWrVb15/w2w2y2w2X/VcAACVU87JAj0yfYOOnSxUdFiApg9qKz8fT6PLAgAAAFDJuN2v6h0OhwoLCxUXFydvb2+tXLnSdSw9PV2HDh1SfHz8RV9/LmTbu3evvv32WwUHB1+PsgEAlZTtTLEGTN+oQ7mnVT+ohj4c3E5WP2+jywIAAABQCRl6RtvYsWN13333qX79+jp58qRmz56t1atXa/ny5bJarRoyZIhGjx6toKAgWSwWjRgxQvHx8WXuOBodHa2JEyeqR48eKi4uVq9evZSamqolS5aotLTUdT23oKAg+fj4GDVVAIAbOlNUqiEzN2l31kmFBJj18ZD2CrX4Gl0WAAAAgErK0KAtJydHAwYM0NGjR2W1WtWyZUstX75c99xzjyTprbfekoeHh5KSklRYWKjExES99957ZcZIT0+XzWaTJB05ckSLFy+WJLVq1apMv++++0533nnnNZ8TAKByKC516PefpGjzwRMK8PXSh4PbqX5wDaPLAgAAAFCJmZxOp9PoItyN3W6X1WqVzWb7zeu7AQAqH4fDqdGfp2lRWqZ8vT300ZD2atswyOiyUAWwhnB/fEcAAOBKlHcN4XbXaAMA4FpyOp16ackuLUrLlJeHSVP7xRGyAQAAAKgQBG0AgGrl/1b9rJnrDkiS/va7GHWKDjW2IAAAAABVBkEbAKDa+Cj5gP6+Yo8kaULXpureup7BFQEAAACoSgjaAADVwuKtmRq3eKck6em7b9KgW6MMrggAAABAVUPQBgCo8lan52j0nDQ5ndKA+AYa2fkmo0sCAAAAUAURtAEAqrSUgyf01MepKnE49WBMuCZ0bSaTyWR0WQAAAACqIII2AECVlZ51UoNnbtKZ4lLdcXOI/va7GHl4ELIBAAAAuDYI2gAAVdLh3NPqP32DbGeKFVs/UFMfiZWPF//sAQAAALh2+IkDAFDlHDtZqEemb1DOyUI1rhOgDwa1VQ0fL6PLAgAAAFDFEbQBAKoU25liDfhgow7+eloRtfz04ZB2CqzhY3RZAAAAAKoBgjYAQJVRUFyqx2dt1k9H7apd00cfD2mvOhZfo8sCAAAAUE0QtAEAqoTiUoeGz07VxgO5CjB7adbgdmpY29/osgAAAABUIwRtAIBKz+Fwasy8bfr2pxyZvTw0fVBbNQu3Gl0WUCmsXbtWXbt2VXh4uEwmkxYtWlTm+IIFC5SQkKDg4GCZTCalpaVdcJzk5GTddddd8vf3l8Vi0e23364zZ864jufm5qpfv36yWCwKDAzUkCFDdOrUqTJjbNu2Tbfddpt8fX0VGRmpSZMmnfc+c+fOVXR0tHx9fdWiRQt99dVXV/0ZAAAAVBSCNgBApeZ0OvXXpT9pwZYj8vQw6b1+sWoXFWR0WUClkZ+fr5iYGE2ZMuWixzt27KjXX3/9omMkJyfr3nvvVUJCgjZu3KhNmzZp+PDh8vD4z1KzX79+2rlzp1asWKElS5Zo7dq1euKJJ1zH7Xa7EhIS1KBBA6WkpOiNN97QhAkT9P7777v6rFu3Tg899JCGDBmiLVu2qHv37urevbt27NhRAZ8EAADA1TM5nU6n0UW4G7vdLqvVKpvNJovFYnQ5AIBLmPLdz3pjebok6e+9Y9QzNsLgilCdVfY1hMlk0sKFC9W9e/fzjh04cEBRUVHasmWLWrVqVeZYhw4ddM899+jll1++4Lg//fSTmjZtqk2bNqlNmzaSpGXLlun+++9XRkaGwsPDNXXqVD333HPKysqSj8/ZG5g8++yzWrRokXbv3i1J6tOnj/Lz87VkyZIy792qVStNmzatXHOs7N8RAAAwRnnXEJzRBgCotD5ef9AVso17oCkhG2CAnJwcbdiwQaGhobrllltUp04d3XHHHfrhhx9cfZKTkxUYGOgK2SSpc+fO8vDw0IYNG1x9br/9dlfIJkmJiYlKT0/XiRMnXH06d+5c5v0TExOVnJx80foKCwtlt9vLPAAAAK4VgjYAQKW0ZFumXvji7HaxEXc10uCOUQZXBFRPv/zyiyRpwoQJevzxx7Vs2TLFxsbq7rvv1t69eyVJWVlZCg0NLfM6Ly8vBQUFKSsry9WnTp06Zfqce/5bfc4dv5CJEyfKarW6HpGRkVcxWwAAgEsjaAMAVDpr9xzTqDlpcjqlfu3ra/Q9NxtdElBtORwOSdKTTz6pRx99VK1bt9Zbb72lxo0b64MPPjC4Omns2LGy2Wyux+HDh40uCQAAVGFeRhcAAMDlSD10Qk9+lKLiUqe6tKyrl7o1l8lkMrosoNqqW7euJKlp06Zl2ps0aaJDhw5JksLCwpSTk1PmeElJiXJzcxUWFubqk52dXabPuee/1efc8Qsxm80ym82XOy0AAIArwhltAIBKY0/2SQ2euUlnikt120219VbvVvL0IGQDjNSwYUOFh4crPT29TPuePXvUoEEDSVJ8fLzy8vKUkpLiOr5q1So5HA61b9/e1Wft2rUqLi529VmxYoUaN26sWrVqufqsXLmyzPusWLFC8fHx12RuAAAAl4sz2gAAlcLh3NPqP32D8k4Xq3X9QE17JE4+Xvy+CLhap06d0s8//+x6vn//fqWlpSkoKEj169dXbm6uDh06pMzMTElyBWphYWEKCwuTyWTSM888o/HjxysmJkatWrXSrFmztHv3bs2bN0/S2bPb7r33Xj3++OOaNm2aiouLNXz4cPXt21fh4eGSpIcfflgvvviihgwZojFjxmjHjh2aPHmy3nrrLVdtTz/9tO644w69+eab6tKliz777DNt3rxZ77///vX6uAAAAC7J5HQ6nUYX4W647TsAuJfjpwr1u2nJ2n88XzfXqanPn4xXYA2f334hcJ1VxjXE6tWr1alTp/PaBw4cqJkzZ2rmzJl69NFHzzs+fvx4TZgwwfX8tdde05QpU5Sbm6uYmBhNmjRJHTt2dB3Pzc3V8OHD9eWXX8rDw0NJSUl65513VLNmTVefbdu2adiwYdq0aZNq166tESNGaMyYMWXed+7cuXr++ed14MAB3XTTTZo0aZLuv//+cs+3Mn5HAADAeOVdQxC0XQALMABwHycLitX3/fXamWlXvUA/zX/qFoVZfY0uC7gg1hDuj+8IAABcifKuIdhzAwBwWwXFpXps1mbtzLQr2N9HHz/WnpANAAAAgNsiaAMAuKWSUoeGz96iDftzFWD20qzB7RRV29/osgAAAADgogjaAABux+Fwasz87fr2p2z5eHnonwPbqHk9q9FlAQAAAMAlEbQBANyK0+nUq1/9pPmpGfL0MGnKw7HqcEOw0WUBAAAAwG8iaAMAuJWpa/bpXz/slyS9ntRS9zStY3BFAAAAAFA+VxS0HT58WBkZGa7nGzdu1MiRI/X+++9XWGEAgOrn042HNGlZuiTp+S5N1CsuwuCKAPfFegwAAMD9XFHQ9vDDD+u7776TJGVlZemee+7Rxo0b9dxzz+mll16q0AIBANXDV9uP6rmF2yVJwzrdqMduu8HgigD3xnoMAADA/VxR0LZjxw61a9dOkvT555+refPmWrdunT755BPNnDmzIusDAFQDP+w9rpGfpcnhlB5qV19/SmhsdEmA22M9BgAA4H6uKGgrLi6W2WyWJH377bd68MEHJUnR0dE6evRoxVUHAKjy0g7n6YmPNquo1KH7W4Tpr92by2QyGV0W4PZYjwEAALifKwramjVrpmnTpun777/XihUrdO+990qSMjMzFRzMneEAAOXzc85JPTpjo04Xlapjo9p6q08reXoQsgHlwXoMAADA/VxR0Pb666/rH//4h+6880499NBDiomJkSQtXrzYtYUBAIBLOZJ3Rv2nb9SJ08WKiQzUP/rHyezlaXRZQKXBegwAAMD9mJxOp/NKXlhaWiq73a5atWq52g4cOKAaNWooNDS0wgo0gt1ul9Vqlc1mk8ViMbocAKhyfj1VqN9NS9Yvx/PVKLSmPn8yXkH+PkaXBVy1672GqMrrsWuFdR4AALgS5V1DXNEZbWfOnFFhYaFrUXfw4EG9/fbbSk9PZ1EHALikkwXFGjRjk345nq96gX76aEg7QjbgCrAeAwAAcD9XFLR169ZNH374oSQpLy9P7du315tvvqnu3btr6tSpFVogAKDqKCgu1RMfpmj7EZuC/H304ZB2qmv1M7osoFJiPQYAAOB+vK7kRampqXrrrbckSfPmzVOdOnW0ZcsWzZ8/X+PGjdNTTz1VoUUCACq/klKH/vDpFiX/8qtqmr0069F2ujGkptFlAZUW6zH343Q6daa41OgyAACo1vy8PWUyGXeDtSsK2k6fPq2AgABJ0jfffKOePXvKw8NDHTp00MGDByu0QABA5ed0OvWXhdv1za5s+Xh56J8D2qhFhNXosoBKjfWY+zlTXKqm45YbXQYAANXarpcSVcPniuKuCnFFW0cbNWqkRYsW6fDhw1q+fLkSEhIkSTk5OVxUFgBwnteW7dbnmzPkYZL+76HWir8x2OiSgEqP9RgAAID7uaKIb9y4cXr44Yc1atQo3XXXXYqPj5d09reprVu3rtACAQCV27Q1+/SPNb9Ikl5LaqnEZmEGVwRUDazH3I+ft6d2vZRodBkAAFRrft6ehr6/yel0Oq/khVlZWTp69KhiYmLk4XH2xLiNGzfKYrEoOjq6Qou83rjtOwBUjDmbDmnM/O2SpL/cH60nbr/R4IqAa+t6ryGq8nrsWmGdBwAArkR51xBXvGk1LCxMYWFhysjIkCRFRESoXbt2VzocAKCKWbbjqMYuOBuyDb3jRkI24BpgPQYAAOBerugabQ6HQy+99JKsVqsaNGigBg0aKDAwUC+//LIcDkdF1wgAqGTW/Xxcf/g0TQ6n1KdNpMbc29jokoAqh/UYAACA+7miM9qee+45TZ8+Xa+99ppuvfVWSdIPP/ygCRMmqKCgQK+88kqFFgkAqDy2ZeTp8Q83q6jUoXubhemVHs0Nvb02UFWxHgMAAHA/V3SNtvDwcE2bNk0PPvhgmfYvvvhCv//973XkyJEKK9AIXLsDAK7Mzzmn1PsfycrNL9ItNwbrg0Ft5WvwxUiB6+l6riGq+nrsWmGdBwAArkR51xBXtHU0Nzf3ghfYjY6OVm5u7pUMCQCo5DLzzmjA9A3KzS9Sywir3h/QhpANuIZYjwEAALifKwraYmJi9O67757X/u6776ply5blHmfq1Klq2bKlLBaLLBaL4uPj9fXXX7uOFxQUaNiwYQoODlbNmjWVlJSk7OzsS47pdDo1btw41a1bV35+furcubP27t1b/skBAC5bbn6R+k/foExbgW4I8dfMR9uppvmK77cDoBwqaj0GAACAinNFPwVNmjRJXbp00bfffqv4+HhJUnJysg4fPqyvvvqq3ONERETotdde00033SSn06lZs2apW7du2rJli5o1a6ZRo0Zp6dKlmjt3rqxWq4YPH66ePXvqxx9/vGRt77zzjmbNmqWoqCi98MILSkxM1K5du+Tr63sl0wUAXMKpwhI9OmOj9h3LV7jVVx8Paa8gfx+jywKqvIpajwEAAKDiXNEZbXfccYf27NmjHj16KC8vT3l5eerZs6d27typjz76qNzjdO3aVffff79uuukm3XzzzXrllVdUs2ZNrV+/XjabTdOnT9ff//533XXXXYqLi9OMGTO0bt06rV+//oLjOZ1Ovf3223r++efVrVs3tWzZUh9++KEyMzO1aNGiK5kqAOASCktK9eRHm7U1w6ZaNbz14ZD2Cg/0M7osoFqoqPUYAAAAKs4V3QzhYrZu3arY2FiVlpZe9mtLS0s1d+5cDRw4UFu2bFFWVpbuvvtunThxQoGBga5+DRo00MiRIzVq1Kjzxvjll1904403asuWLWrVqpWr/Y477lCrVq00efLkC753YWGhCgsLXc/tdrsiIyO5SC4AXEKpw6nhs1P19Y4s+ft4avbjHRQTGWh0WYCh3OFC+1ezHqsO3OE7AgAAlc81vRlCRdq+fbtq1qwps9msoUOHauHChWratKmysrLk4+NTJmSTpDp16igrK+uCY51rr1OnTrlfI0kTJ06U1Wp1PSIjI69uUgBQxTmdTj23cLu+3pElH08PvT+gDSEbAAAAgGrP8KCtcePGSktL04YNG/TUU09p4MCB2rVr13WtYezYsbLZbK7H4cOHr+v7A0BlM2l5uj7bdFgeJumdh1rp1ka1jS4JAAAAAAxn+C3hfHx81KhRI0lSXFycNm3apMmTJ6tPnz4qKipSXl5embPasrOzFRYWdsGxzrVnZ2erbt26ZV7z31tJ/5fZbJbZbL76yQBANfD+2n2aunqfJOnVHi10b/O6v/EKAAAAAKgeLito69mz5yWP5+XlXU0tkiSHw6HCwkLFxcXJ29tbK1euVFJSkiQpPT1dhw4dct1Z639FRUUpLCxMK1eudAVrdrvddbYcAODqfL75sF79arckacy90erbrr7BFQHVz/VYjwEAAODKXFbQZrVaf/P4gAEDyj3e2LFjdd9996l+/fo6efKkZs+erdWrV2v58uWyWq0aMmSIRo8eraCgIFksFo0YMULx8fHq0KGDa4zo6GhNnDhRPXr0kMlk0siRI/XXv/5VN910k6KiovTCCy8oPDxc3bt3v5ypAgD+x/KdWXp2/jZJ0hO336Chd9xgcEVA9VTR6zEAAABUnMsK2mbMmFGhb56Tk6MBAwbo6NGjslqtatmypZYvX6577rlHkvTWW2/Jw8NDSUlJKiwsVGJiot57770yY6Snp8tms7me//nPf1Z+fr6eeOIJ5eXlqWPHjlq2bJl8fX0rtHYAqE6S9/2qEZ9ukcMp9W4TobH3RctkMhldFlAtVfR6DAAAABXH5HQ6nUYX4W647TsA/MeOIzb1fX+9ThWWKKFpHb3XL1ZenobfSwdwS6wh3B/fEQAAuBLlXUPwkxIA4KJ+OXZKAz/YqFOFJepwQ5Deeag1IRsAAAAAXAQ/LQEALuio7Yz6T9+oX/OL1LyeRf8c0Ea+3p5GlwUAAAAAbougDQBwnhP5RRowfaOO5J3RDbX9NfPRdgrw9Ta6LAAAAABwawRtAIAy8gtLNGjmJu3NOaUwi68+HNJOtWuajS4LAAAAANweQRsAwKWwpFRDP07R1sN5CqzhrY+GtFNErRpGlwUAAAAAlQJBGwBAklTqcGr0nK36fu9x1fDx1IxBbXVTnQCjywIAAACASoOgDQAgp9OpF77YoaXbj8rb06R/9I9T6/q1jC4LAAAAACoVgjYAgN78Zo9mbzgkk0ma3Le1brspxOiSAAAAAKDSIWgDgGruX9//one/+1mS9Er3Frq/RV2DKwIAAACAyomgDQCqsfkpGfrr0p8kSc8kNtbD7esbXBEAAAAAVF4EbQBQTX27K1t/nr9NkvRYxyj9/s4bDa4IAAAAACo3gjYAqIY2/PKrhs1OVanDqaTYCP3l/iYymUxGlwUAAAAAlRpBGwBUMzuO2PTYrM0qLHGoc5M6ej2phTw8CNkAAAAA4GoRtAFANbL/eL4Gzdiok4UlahcVpHcfbi0vT/4pAAAAAICKwE9XAFBNZNkK9Mi/Nuj4qSI1C7foXwPbyNfb0+iyAAAAAKDKIGgDgGog73SRBnywQUfyziiqtr9mDW4ni6+30WUBcANr165V165dFR4eLpPJpEWLFpU5vmDBAiUkJCg4OFgmk0lpaWkXHcvpdOq+++674DiHDh1Sly5dVKNGDYWGhuqZZ55RSUlJmT6rV69WbGyszGazGjVqpJkzZ573HlOmTFHDhg3l6+ur9u3ba+PGjVc4cwAAgIpH0AYAVdzpohI9OnOT9mSfUh2LWR8ObqfaNc1GlwXATeTn5ysmJkZTpky56PGOHTvq9ddf/82x3n777QveWKW0tFRdunRRUVGR1q1bp1mzZmnmzJkaN26cq8/+/fvVpUsXderUSWlpaRo5cqQee+wxLV++3NVnzpw5Gj16tMaPH6/U1FTFxMQoMTFROTk5VzBzAACAimdyOp1Oo4twN3a7XVarVTabTRaLxehyAOCKFZU49NiHm7V2zzFZ/bw1d2i8bq4TYHRZQJVV2dcQJpNJCxcuVPfu3c87duDAAUVFRWnLli1q1arVecfT0tL0wAMPaPPmzapbt26Zcb7++ms98MADyszMVJ06dSRJ06ZN05gxY3Ts2DH5+PhozJgxWrp0qXbs2OEas2/fvsrLy9OyZcskSe3bt1fbtm317rvvSpIcDociIyM1YsQIPfvss+WaY2X/jgAAgDHKu4bgjDYAqKJKHU6N/jxNa/cck5+3p2Y82paQDcA1cfr0aT388MOaMmWKwsLCzjuenJysFi1auEI2SUpMTJTdbtfOnTtdfTp37lzmdYmJiUpOTpYkFRUVKSUlpUwfDw8Pde7c2dXnQgoLC2W328s8AAAArhWCNgCogpxOpyYs3qkl247K29Okaf3jFFu/ltFlAaiiRo0apVtuuUXdunW74PGsrKwyIZsk1/OsrKxL9rHb7Tpz5oyOHz+u0tLSC/Y5N8aFTJw4UVar1fWIjIy87PkBAACUF0EbAFRBb63Yo4/WH5TJJP29dyvdcXOI0SUBqKIWL16sVatW6e233za6lAsaO3asbDab63H48GGjSwIAAFUYQRsAVDEf/LBf76z6WZL0Urfm6hoTbnBFAKqyVatWad++fQoMDJSXl5e8vLwkSUlJSbrzzjslSWFhYcrOzi7zunPPz201vVgfi8UiPz8/1a5dW56enhfsc6HtqueYzWZZLJYyDwAAgGuFoA0AqpCFWzL00pJdkqQ/3nOz+ndoYHBFAKq6Z599Vtu2bVNaWprrIUlvvfWWZsyYIUmKj4/X9u3by9wddMWKFbJYLGratKmrz8qVK8uMvWLFCsXHx0uSfHx8FBcXV6aPw+HQypUrXX0AAACM5mV0AQCAirFqd7b+NHebJOnRWxtq+F2NDK4IQGVw6tQp/fzzz67n+/fvV1pamoKCglS/fn3l5ubq0KFDyszMlCSlp6dLOnsG2n8//lf9+vUVFRUlSUpISFDTpk3Vv39/TZo0SVlZWXr++ec1bNgwmc1mSdLQoUP17rvv6s9//rMGDx6sVatW6fPPP9fSpUtdY44ePVoDBw5UmzZt1K5dO7399tvKz8/Xo48+es0+HwAAgMtB0AYAVcDG/bl66uNUlTqc6tm6nl7o0lQmk8nosgBUAps3b1anTp1cz0ePHi1JGjhwoGbOnKnFixeXCbL69u0rSRo/frwmTJhQrvfw9PTUkiVL9NRTTyk+Pl7+/v4aOHCgXnrpJVefqKgoLV26VKNGjdLkyZMVERGhf/3rX0pMTHT16dOnj44dO6Zx48YpKytLrVq10rJly867QQIAAIBRTE6n02l0Ee7GbrfLarXKZrNxHQ8Abm9Xpl193k/WyYIS3R0dqmn94+TtyZUBACOwhnB/fEcAAOBKlHcNwU9iAFCJHfw1XwM+2KiTBSVq1zBIU/rFErIBAAAAgEH4aQwAKqkce4Eemb5Bx08Vqkldi/45sI18vT2NLgsAAAAAqi2CNgCohGynizXgg406nHtGDYJraNbgtrL6eRtdFgAAAABUawRtAFDJnC4q0eBZm7Q766RCA8z6eEh7hQb4Gl0WAAAAAFR7BG0AUIkUlTj01MepSjl4QhZfL304pJ0ig2oYXRYAAAAAQARtAFBpOBxO/WnuVq3Zc0y+3h6a8WhbRYdxxzwAAAAAcBcEbQBQCTidTr345U4t3popLw+Tpj0Sp7gGQUaXBQAAAAD4LwRtAFAJTF65V7OSD8pkkt7sHaM7G4caXRIAAAAA4H8QtAGAm5u17oDe/navJOmlB5upW6t6BlcEAAAAALgQgjYAcGNfpB3R+MU7JUmjOt+s/vENjS0IAAAAAHBRBG0A4Ka+S8/RHz/fKkkadEtD/eHuRgZXBAAAAAC4FII2AHBDKQdz9dTHKSpxONWtVbjGPdBUJpPJ6LIAAAAAAJdA0AYAbuano3Y9OmOTCoodurNxiP72uxh5eBCyAQAAAIC7I2gDADdy6NfTGvDBRtkLStSmQS1N7Rcnb0/+qgYAAACAyoCf3gDATeScLNAj0zfo2MlCRYcFaPrAtvLz8TS6LAAAAABAORG0AYAbsJ0p1oDpG3Uo97TqB9XQh4PbyVrD2+iyAAAAAACXgaANAAx2pqhUj83apN1ZJxUSYNbHQ9or1OJrdFkAAAAAgMtE0AYABioudWjY7FRtOnBCAb5e+nBwO9UPrmF0WQAAAACAK0DQBgAGcTic+vO8bVq1O0e+3h76YFBbNalrMbosAAAAAMAVImgDAAM4nU69tGSXFm45Ii8Pk6b2i1PbhkFGlwUAAAAAuAoEbQBggHdX/ayZ6w5Ikv72uxh1ig41tiAAAAAAwFUzNGibOHGi2rZtq4CAAIWGhqp79+5KT08v02ffvn3q0aOHQkJCZLFY1Lt3b2VnZ19y3NLSUr3wwguKioqSn5+fbrzxRr388styOp3XcjoAUC4frT+oN1fskSSN79pU3VvXM7giAAAAAEBFMDRoW7NmjYYNG6b169drxYoVKi4uVkJCgvLz8yVJ+fn5SkhIkMlk0qpVq/Tjjz+qqKhIXbt2lcPhuOi4r7/+uqZOnap3331XP/30k15//XVNmjRJ//d//3e9pgYAF7R4a6bGfbFDkvSHu2/So7dGGVwRAAAAAKCieBn55suWLSvzfObMmQoNDVVKSopuv/12/fjjjzpw4IC2bNkii+XsBcJnzZqlWrVqadWqVercufMFx123bp26deumLl26SJIaNmyoTz/9VBs3bry2EwKAS1idnqPRc9LkdEr9OzTQqM43GV0SAAAAAKACudU12mw2myQpKOjsBcELCwtlMplkNptdfXx9feXh4aEffvjhouPccsstWrlypfbsObs1a+vWrfrhhx903333XbB/YWGh7HZ7mQcAVKSUgyf01MepKnE41TUmXC8+2Ewmk8nosgAAAAAAFchtgjaHw6GRI0fq1ltvVfPmzSVJHTp0kL+/v8aMGaPTp08rPz9ff/rTn1RaWqqjR49edKxnn31Wffv2VXR0tLy9vdW6dWuNHDlS/fr1u2D/iRMnymq1uh6RkZHXZI4Aqqf0rJMaPHOTzhSX6o6bQ/Tm72Lk4UHIBgAAAABVjdsEbcOGDdOOHTv02WefudpCQkI0d+5cffnll6pZs6asVqvy8vIUGxsrD4+Ll/7555/rk08+0ezZs5WamqpZs2bpb3/7m2bNmnXB/mPHjpXNZnM9Dh8+XOHzA1A9Hc49rf7TN8h2plix9QM19ZFY+Xi5zV+9AAAAAIAKZOg12s4ZPny4lixZorVr1yoiIqLMsYSEBO3bt0/Hjx+Xl5eXAgMDFRYWphtuuOGi4z3zzDOus9okqUWLFjp48KAmTpyogQMHntffbDaX2Z4KABXh2MlC9Z++QTknC9W4ToA+GNRWNXzc4q9dAAAAAMA1YOhPfE6nUyNGjNDChQu1evVqRUVd/O57tWvXliStWrVKOTk5evDBBy/a9/Tp0+ed8ebp6XnJO5UCQEWyFxRr4AcbdeDX04qo5acPh7RTYA0fo8sCAAAAAFxDhgZtw4YN0+zZs/XFF18oICBAWVlZkiSr1So/Pz9J0owZM9SkSROFhIQoOTlZTz/9tEaNGqXGjRu7xrn77rvVo0cPDR8+XJLUtWtXvfLKK6pfv76aNWumLVu26O9//7sGDx58/ScJoNopKC7VY7M2a9dRu2rX9NHHQ9qrjsXX6LIAAAAAANeYoUHb1KlTJUl33nlnmfYZM2Zo0KBBkqT09HSNHTtWubm5atiwoZ577jmNGjWqTP9zW0vP+b//+z+98MIL+v3vf6+cnByFh4frySef1Lhx467pfACguNSh4bNTtXF/rgLMXpo1uJ0a1vY3uiwAAAAAwHVgcjqdTqOLcDd2u11Wq1U2m00Wi8XocgBUEg6HU3+au1ULthyR2ctDHw5up/Y3BBtdFoDriDWE++M7AgAAV6K8awhufQcAFcDpdOqvS3/Sgi1H5Olh0pSHYwnZAAAAAKCaIWgDgArw3up9+uDH/ZKkN3q1VOemdQyuCAAAAABwvRG0AcBV+mTDQb2xPF2SNO6BpuoZG2FwRQAAAAAAIxC0AcBVWLrtqJ5ftEOSNOKuRhrcMcrgigAAAAAARiFoA4Ar9P3eYxo5Z4ucTqlf+/oafc/NRpcEAAAAADAQQRsAXIEth07oyY9SVFzqVJeWdfVSt+YymUxGlwUAAAAAMBBBGwBcpr3ZJ/XozE06XVSq226qrbd6t5KnByEbAAAAAFR3BG0AcBl2Z9nVf/pG5Z0uVqvIQE17JE4+XvxVCgAAAACQvIwuAADcne10sRZvy9T8lAylHc6TJN0UWlMzBrWVv5m/RgEAAAAAZ/ETIgBcQEmpQ2v3HtP8lCNasStbRaUOSZKnh0l3RYfq5W7NVcvfx+AqAQAAAADuhKANAP5LetZJzUs5rEVpmTp2stDVHh0WoF5xEerWqp5CAswGVggAAAAAcFcEbQCqvdz8Ii1OO6J5qRnaccTuag/y91G3VuFKio1Qs3ALdxUFAAAAAFwSQRuAaqm41KHvdudofmqGVu3OUXGpU5Lk9e+tob3iInRn41BudAAAAAAAKDeCNgDVys5Mm+alZGhxWqZ+zS9ytTevZ1FSbIQejAlXcE22hgIAAAAALh9BG4Aq79jJQn2RdkTzUjK0O+ukq712TbN6tA5XUlyEosMsBlYIAAAAAKgKCNoAVEmFJaVa9dPZraHfpR9TqePs1lAfTw/d07SOkuLq6fabQuTlydZQAAAAAEDFIGgDUGU4nU5ty7BpfmqGFm/NVN7pYtexmMhA9YqLUNeWdRVYw8fAKgEAAAAAVRVBG4BKL9teoIVbjmh+Sob25pxytdexmNWjdYR6xdVTo9AAAysEAAAAAFQH7JkCUCkVFJfqy62ZGjRjo+InrtRrX+/W3pxTMnt56MGYcM0a3E7rnr1bz94XTcgGAJewdu1ade3aVeHh4TKZTFq0aFGZ4wsWLFBCQoKCg4NlMpmUlpZW5nhubq5GjBihxo0by8/PT/Xr19cf/vAH2Wy2Mv0OHTqkLl26qEaNGgoNDdUzzzyjkpKSMn1Wr16t2NhYmc1mNWrUSDNnzjyv3ilTpqhhw4by9fVV+/bttXHjxor4GAAAACoEZ7QBqDScTqdSD+VpfmqGlmzNlL3gPz+gxTWopV5xEerSsq4svt4GVgkAlUt+fr5iYmI0ePBg9ezZ84LHO3bsqN69e+vxxx8/73hmZqYyMzP1t7/9TU2bNtXBgwc1dOhQZWZmat68eZKk0tJSdenSRWFhYVq3bp2OHj2qAQMGyNvbW6+++qokaf/+/erSpYuGDh2qTz75RCtXrtRjjz2munXrKjExUZI0Z84cjR49WtOmTVP79u319ttvKzExUenp6QoNDb2GnxIAAED5mJxOp9PoItyN3W6X1WqVzWaTxcKdCAGjZeadcW0N/eV4vqs93OqrnrER6hlbTzeE1DSwQgA4q7KvIUwmkxYuXKju3bufd+zAgQOKiorSli1b1KpVq0uOM3fuXD3yyCPKz8+Xl5eXvv76az3wwAPKzMxUnTp1JEnTpk3TmDFjdOzYMfn4+GjMmDFaunSpduzY4Rqnb9++ysvL07JlyyRJ7du3V9u2bfXuu+9KkhwOhyIjIzVixAg9++yz5ZpjZf+OAACAMcq7huCMNgBu6UxRqZbvzNK8lAz9uO+4zv1KwM/bU/c1D1NSXITibwiWh4fJ2EIBAOc5twD18jq71ExOTlaLFi1cIZskJSYm6qmnntLOnTvVunVrJScnq3PnzmXGSUxM1MiRIyVJRUVFSklJ0dixY13HPTw81LlzZyUnJ1+0lsLCQhUWFrqe2+32ipgiAADABRG0AXAbTqdTmw6c0PyUDC3dflSnCv+zNbRdVJB6xUXo/hZ1VdPMX10A4K6OHz+ul19+WU888YSrLSsrq0zIJsn1PCsr65J97Ha7zpw5oxMnTqi0tPSCfXbv3n3ReiZOnKgXX3zxquYEAABQXvy0CsBwh3NPa0HqES3YkqGDv552tUcG+aln6wglxUaofnANAysEAJSH3W5Xly5d1LRpU02YMMHociRJY8eO1ejRo13P7Xa7IiMjDawIAABUZQRtAAyRX1iir3dkaV7KYa3/JdfV7u/jqftb1FWvuAi1bRjE1lAAqCROnjype++9VwEBAVq4cKG8vf9zY5qwsLDz7g6anZ3tOnbuf8+1/Xcfi8UiPz8/eXp6ytPT84J9zo1xIWazWWaz+armBgAAUF4EbQCuG4fDqfX7f9X8lCP6esdRnS4qlSSZTNItNwYrKTZC9zYPUw0f/moCgMrEbrcrMTFRZrNZixcvlq+vb5nj8fHxeuWVV5STk+O6O+iKFStksVjUtGlTV5+vvvqqzOtWrFih+Ph4SZKPj4/i4uK0cuVK180aHA6HVq5cqeHDh1/jGQIAAJQPP80CuOYO/pqv+SkZmp96REfyzrjaGwbXUK+4CPWIjVC9QD8DKwSA6uvUqVP6+eefXc/379+vtLQ0BQUFqX79+srNzdWhQ4eUmZkpSUpPT5d09gy0sLAw2e12JSQk6PTp0/r4449lt9tdNxwICQmRp6enEhIS1LRpU/Xv31+TJk1SVlaWnn/+eQ0bNsx1ttnQoUP17rvv6s9//rMGDx6sVatW6fPPP9fSpUtdtY0ePVoDBw5UmzZt1K5dO7399tvKz8/Xo48+er0+LgAAgEsiaANwTZwsKNbSbUc1PzVDmw6ccLUHmL30QMzZraGx9WvJZGJrKAAYafPmzerUqZPr+bnrmQ0cOFAzZ87U4sWLywRZffv2lSSNHz9eEyZMUGpqqjZs2CBJatSoUZmx9+/fr4YNG8rT01NLlizRU089pfj4ePn7+2vgwIF66aWXXH2joqK0dOlSjRo1SpMnT1ZERIT+9a9/KTEx0dWnT58+OnbsmMaNG6esrCy1atVKy5YtO+8GCQAAAEYxOZ1Op9FFuBu73S6r1eq6NT2A8il1OLVu33HNS8nQ8p1ZKih2SJI8TFLHm0KUFFtPic3C5OvtaXClAHBtsIZwf3xHAADgSpR3DcEZbQCu2r5jpzQ/JUMLtxzRUVuBq/3GEH/1iotUj9b1FGb1vcQIAAAAAABUfgRtAK6I7XSxvtyWqfmpGdpyKM/VbvXzVteYuuoVF6mYCCtbQwEAAAAA1QZBG4ByKyl16Pufz24NXbErW0UlZ7eGenqYdMfNIeoVF6G7m4TK7MXWUAAAAABA9UPQBuA37ck+qfkpGVqw5YiOnSx0tTeuE6BecRHq1jpcoQFsDQUAAAAAVG8EbQAu6ER+kRZvPbs1dFuGzdVeq4a3urWqp15xEWoWbmFrKAAAAAAA/0bQBsCluNShNenHNC8lQyt3Z6u49OxNib08TOoUHapecRHq1DhUPl4eBlcKAAAAAID7IWgDoF2Zds1PzdAXaUd0/FSRq71ZuEVJsRHq1ipcwTXNBlYIAAAAAID7I2gDqqnjpwr1RVqm5qdkaNdRu6u9dk0fdW9VT0lxEWpS12JghQAAAAAAVC4EbUA1UlTi0Krd2ZqXckSr03NU4ji7NdTH00N3Nzm7NfT2m0Pk7cnWUAAAAAAALhdBG1DFOZ1O7Thi17yUw1q8NVMnThe7jsVEWJUUF6GuLcNVy9/HwCoBAAAAAKj8CNqAKirHXqBFaUc0LyVDe7JPudpDA8zqEVtPvWIjdFOdAAMrBAAAAACgaiFoA6qQguJSfftTtuanZGjNnmP6985Q+Xh5KLFZmJJi66ljo9ryYmsoAAAAAAAVjqANqOScTqfSDudpXkqGvtyaKXtBietYbP1A9YqLVJeWdWX18zawSgAAAAAAqj6CNqCSOmo7o4Vbzm4N/eVYvqu9rtVXPWPrqWdshG4MqWlghQAAAAAAVC8EbUAlcqaoVN/sytK8lAz98PNxOf+9NdTX20P3Na+rpNgIxd8YLE8Pk7GFAgAAAABQDRG0AW7O6XQq5eAJzUvJ0NJtR3Wy8D9bQ9s1DFKvuAjd1yJMAb5sDQUAAAAAwEgEbYCbyjhxWgtSj2hBaoYO/Hra1R5Ry089YyOUFFtPDYL9DawQAAAAAAD8N4I2wI2cLirR19vPbg1N/uVXV3sNH0/d3+Ls1tD2UUHyYGsoAAAAAABuh6ANMJjD4dTGA7mal5Khr7cfVX5RqetY/A3B6hUXoXubh8nfzH+uAAAAAAC4Mw8j33zixIlq27atAgICFBoaqu7duys9Pb1Mn3379qlHjx4KCQmRxWJR7969lZ2d/ZtjHzlyRI888oiCg4Pl5+enFi1aaPPmzddqKsBlO/hrvv6+Yo9uf+M79X1/vealZCi/qFQNgmto9D036/s/d9KnT3RQUlwEIRsAAAAAAJWAoT+9r1mzRsOGDVPbtm1VUlKiv/zlL0pISNCuXbvk7++v/Px8JSQkKCYmRqtWrZIkvfDCC+ratavWr18vD48L54QnTpzQrbfeqk6dOunrr79WSEiI9u7dq1q1al3P6QHnOVVYoq+2HdW8lAxtPJDraq9p9tIDLeuqV1yE4hrUksnE1lAAAAAAACobQ4O2ZcuWlXk+c+ZMhYaGKiUlRbfffrt+/PFHHThwQFu2bJHFYpEkzZo1S7Vq1dKqVavUuXPnC477+uuvKzIyUjNmzHC1RUVFXbSOwsJCFRYWup7b7farmRZQRqnDqeR9v2p+aoa+3nFUBcUOSZLJJHVsVFu94iKU0DRMfj6eBlcKAAAAAACuhlvtR7PZbJKkoKAgSWcDMJPJJLPZ7Orj6+srDw8P/fDDDxcN2hYvXqzExET97ne/05o1a1SvXj39/ve/1+OPP37B/hMnTtSLL75YwbNBdffLsVOan5qhhalHlGkrcLXfEOKvpNgI9Yytp7pWPwMrBAAAAAAAFcltgjaHw6GRI0fq1ltvVfPmzSVJHTp0kL+/v8aMGaNXX31VTqdTzz77rEpLS3X06NGLjvXLL79o6tSpGj16tP7yl79o06ZN+sMf/iAfHx8NHDjwvP5jx47V6NGjXc/tdrsiIyMrfpKo8mxnirV021HNSzms1EN5rnaLr5e6xoSrV1yEWkUGsjUUAAAAAIAqyG2CtmHDhmnHjh364YcfXG0hISGaO3eunnrqKb3zzjvy8PDQQw89pNjY2Iten006G9q1adNGr776qiSpdevW2rFjh6ZNm3bBoM1sNpc5aw64HKUOp77fe0zzU49o+c4sFZWc3RrqYZLuuDlESXER6tykjny92RoKAAAAAEBV5hZB2/Dhw7VkyRKtXbtWERERZY4lJCRo3759On78uLy8vBQYGKiwsDDdcMMNFx2vbt26atq0aZm2Jk2aaP78+dekflRPe7NPal5qhhZtOaJs+3+u8XdznZrqFReh7q3qKdTia2CFAAAAAADgejI0aHM6nRoxYoQWLlyo1atXX/KGBbVr15YkrVq1Sjk5OXrwwQcv2vfWW29Venp6mbY9e/aoQYMGFVM4qq2800VavDVT81MytDXD5moPrOGtbjHh6hUXqeb1LGwNBQAAAACgGjI0aBs2bJhmz56tL774QgEBAcrKypIkWa1W+fmdvUj8jBkz1KRJE4WEhCg5OVlPP/20Ro0apcaNG7vGufvuu9WjRw8NHz5ckjRq1CjdcsstevXVV9W7d29t3LhR77//vt5///3rP0lUesWlDq3dc0zzUjK08qccFZWe3Rrq5WHSnY1D1SuunjpFh8rsxdZQAAAAAACqM0ODtqlTp0qS7rzzzjLtM2bM0KBBgyRJ6enpGjt2rHJzc9WwYUM999xzGjVqVJn+57aWntO2bVstXLhQY8eO1UsvvaSoqCi9/fbb6tev3zWdD6qWn47aNT8lQ4vSjuj4qSJXe5O6FvWKi1C3VuGqXZNr+wEAAAAAgLNMTqfTaXQR7sZut8tqtcpms8lisRhdDq6jX08V6ou0TM1PzdDOTLurPdjfR91a1VNSXD01C7caWCEAwJ2xhnB/fEcAAOBKlHcN4RY3QwCMVFTi0HfpOZqXkqHvdueoxHE2e/b2NOnu6DrqFRehOxqHyNvz4ne6BQAAAAAAIGhDteR0OrUz0655KRlavDVTufn/2RraMsKqpNgIPRgTrlr+PgZWCQAAAAAAKhOCNlQrOScL9MWWs1tDd2eddLWHBJjVs3U9JcVF6OY6AQZWCAAAAAAAKiuCNlR5hSWlWvnT2a2ha/YcU+m/t4b6eHnonqZnt4be1qi2vNgaCgAAAAAArgJBG6okp9OprRk2zf/31lDbmWLXsdb1A5UUG6GuLcNlreFtYJUAAAAAAKAqIWhDlZJlK9DCLUc0L+Ww9h3Ld7WHWXzVM/bs1tAbQ2oaWCEAAAAAAKiqCNpQ6RUUl+qbXdmal5KhH/Ye0793hsrs5aF7m4epV1yEbrmxtjw9TMYWCgAAAAAAqjSCNlRKTqdTqYdOaF5KhpZsO6qTBSWuY20b1lJSbITub1lXFl+2hgIAAAAAgOuDoA2VypG8M1qYmqH5qUe0//h/tobWC/RTUmw99YyNUMPa/gZWCAAAAAAAqiuCNri900UlWrYjS/NTM7Ru369y/ntraA0fT93XvK6S4uqpQ1SwPNgaCgAAAAAADETQBrfkcDi16UCu5qVk6KvtR5VfVOo61uGGIPWKi9R9zcPkb+aPMAAAAAAAcA+kFHArh3NPa35qhuanZuhw7hlXe/2gGkqKjVDP2HqKDKphYIUAAAAAAAAXRtAGw50qLNFX249qfkqGNuzPdbXXNHupS4u6SoqLUNuGtWQysTUUAAAAAAC4L4I2GMLhcGr9L79qXkqGvt6RpTPFZ7eGmkzSrTfWVq+4CCU2C5Ofj6fBlQIAAAAAAJQPQRuuqwPH8zU/NUMLUo/oSN5/tobeUNtfSXER6tG6nsID/QysEAAAAAAA4MoQtOGasxcUa+m2s1tDNx884WoP8PVS15hw9YqLUOvIQLaGAgAAAACASo2gDddEqcOpH34+rvkpGVq+M0uFJQ5JkodJuu2mEPWKi9A9TevI15utoQAAAAAAoGogaEOF+jnnpOalHNGiLUeUZS9wtd8UWtO1NbSOxdfACgEAAAAAAK4NgjZcNdvpYi3elql5KRnaejjP1R5Yw1sP/ntraIt6VraGAgAAAACAKs3D6AJQOZWUOrRqd7aGfZKqtq98qxcW7dDWw3ny9DCpc5NQTe0Xqw1/uVsvdWuulhFcfw0AAHe1du1ade3aVeHh4TKZTFq0aFGZ4wsWLFBCQoKCg4NlMpmUlpZ23hgFBQUaNmyYgoODVbNmTSUlJSk7O7tMn0OHDqlLly6qUaOGQkND9cwzz6ikpKRMn9WrVys2NlZms1mNGjXSzJkzz3uvKVOmqGHDhvL19VX79u21cePGq/0IAAAAKgxntOGy7M6ya35KhhalZerYyUJXe3RYgHrFRahbq3oKCTAbWCEAALgc+fn5iomJ0eDBg9WzZ88LHu/YsaN69+6txx9//IJjjBo1SkuXLtXcuXNltVo1fPhw9ezZUz/++KMkqbS0VF26dFFYWJjWrVuno0ePasCAAfL29tarr74qSdq/f7+6dOmioUOH6pNPPtHKlSv12GOPqW7dukpMTJQkzZkzR6NHj9a0adPUvn17vf3220pMTFR6erpCQ0Ov0ScEAABQfian0+k0ugh3Y7fbZbVaZbPZZLFYjC7HcLn5RVqcdkTzUjO044jd1R7k76Nurc5uDW0WbjWwQgAA3ENlX0OYTCYtXLhQ3bt3P+/YgQMHFBUVpS1btqhVq1audpvNppCQEM2ePVu9evWSJO3evVtNmjRRcnKyOnTooK+//loPPPCAMjMzVadOHUnStGnTNGbMGB07dkw+Pj4aM2aMli5dqh07drjG7tu3r/Ly8rRs2TJJUvv27dW2bVu9++67kiSHw6HIyEiNGDFCzz77bLnmWNm/IwAAYIzyriE4ow0XVFzq0He7czQ/NUOrdueouPRsHuvtadJd0aFKio3QnY1D5ePF7mMAAKqzlJQUFRcXq3Pnzq626Oho1a9f3xW0JScnq0WLFq6QTZISExP11FNPaefOnWrdurWSk5PLjHGuz8iRIyVJRUVFSklJ0dixY13HPTw81LlzZyUnJ1+0vsLCQhUW/ucsfLvdftG+AAAAV4ugDWXszLRpXkqGFqdl6tf8Ild783oW9YqN0IOt6inI38fACgEAgDvJysqSj4+PAgMDy7TXqVNHWVlZrj7/HbKdO37u2KX62O12nTlzRidOnFBpaekF++zevfui9U2cOFEvvvjiFc0NAADgchG0QcdOFuqLtCOal5Kh3VknXe21a5rVo3W4kuIiFB3G1goAAFD5jB07VqNHj3Y9t9vtioyMNLAiAABQlRG0VVOFJaVa9VOO5qVkaPWeYyp1nN0a6uPpoXua1lFSXD3dflOIvDzZGgoAAC4uLCxMRUVFysvLK3NWW3Z2tsLCwlx9/vfuoOfuSvrfff73TqXZ2dmyWCzy8/OTp6enPD09L9jn3BgXYjabZTZzoyYAAHB9ELRVI06nU9sybJqfmqHFWzOVd7rYdSwmMlC94iLUtWVdBdZgaygAACifuLg4eXt7a+XKlUpKSpIkpaen69ChQ4qPj5ckxcfH65VXXlFOTo7r7qArVqyQxWJR06ZNXX2++uqrMmOvWLHCNYaPj4/i4uK0cuVK180aHA6HVq5cqeHDh1+PqQIAAPwmgrZqINteoIVbjmh+Sob25pxytdexmNUzNkJJsfXUKDTAwAoBAIBRTp06pZ9//tn1fP/+/UpLS1NQUJDq16+v3NxcHTp0SJmZmZLOhmjS2TPQwsLCZLVaNWTIEI0ePVpBQUGyWCwaMWKE4uPj1aFDB0lSQkKCmjZtqv79+2vSpEnKysrS888/r2HDhrnONhs6dKjeffdd/fnPf9bgwYO1atUqff7551q6dKmrttGjR2vgwIFq06aN2rVrp7ffflv5+fl69NFHr9fHBQAAcEkEbVVUQXGpVuzK1ryUDH2/95j+vTNUZi8PJTYLU6+4CN3aqLY8PUzGFgoAAAy1efNmderUyfX83PXMBg4cqJkzZ2rx4sVlgqy+fftKksaPH68JEyZIkt566y15eHgoKSlJhYWFSkxM1Hvvved6jaenp5YsWaKnnnpK8fHx8vf318CBA/XSSy+5+kRFRWnp0qUaNWqUJk+erIiICP3rX/9SYmKiq0+fPn107NgxjRs3TllZWWrVqpWWLVt23g0SAAAAjGJyOp1Oo4twN3a7XVarVTabTRZL5bkJgNPpVOqhPM1PzdCXWzN1sqDEdaxNg1pKiotQl5Z1ZfH1NrBKAACqrsq6hqhO+I4AAMCVKO8agjPaqoDMvDOuraG/HM93tYdbfZUUF6GesRGKqu1vYIUAAAAAAABVH0FbJXWmqFTLd2ZpXkqGftx3XOfOS/Tz9tR9zc9uDe1wQ7A82BoKAAAAAABwXRC0VSJOp1ObDpzQ/JQMLd1+VKcK/7M1tH1UkJLiInR/i7qqaeZrBQAAAAAAuN5IZCqBw7mntSD1iBZsydDBX0+72iOD/JQUG6Gk2AhFBtUwsEIAAAAAAAAQtLmp/MISfbX9qOanZmj9L7mudn8fT93foq56xUWobcMgtoYCAAAAAAC4CYI2N+JwOLV+/6+an3JEX+84qtNFpZIkk0m65cZgJcVG6N7mYarhw9cGAAAAAADgbkhs3MCB4/lakJqh+alHdCTvjKs9qra/kmLrqUdshOoF+hlYIQAAAAAAAH4LQZtBThYUa+m2s1tDNx044WoPMHvpgZhw9Yqrp9j6tWQysTUUAAAAAACgMiBou85OF5Vo7ILtWr4zSwXFDkmSh0nqeFOIesVFKKFpHfl6expcJQAAAAAAAC4XQdt15uftqR1HbCoodqhRaE0lxUaoR+t6CrP6Gl0aAAAAAAAArgJB23VmMpk0vmszWfy8FRNhZWsoAAAAAABAFUHQZoDbbw4xugQAAAAAAABUMA+jCwAAAAAAAACqAoI2AAAAAAAAoAIQtAEAAAAAAAAVgKANAAAAAAAAqAAEbQAAAAAAAEAFMDRomzhxotq2bauAgACFhoaqe/fuSk9PL9Nn37596tGjh0JCQmSxWNS7d29lZ2eX+z1ee+01mUwmjRw5soKrBwAAAAAAAP7D0KBtzZo1GjZsmNavX68VK1aouLhYCQkJys/PlyTl5+crISFBJpNJq1at0o8//qiioiJ17dpVDofjN8fftGmT/vGPf6hly5bXeioAAAAAAACo5ryMfPNly5aVeT5z5kyFhoYqJSVFt99+u3788UcdOHBAW7ZskcVikSTNmjVLtWrV0qpVq9S5c+eLjn3q1Cn169dP//znP/XXv/71ms4DAAAAAAAAcKtrtNlsNklSUFCQJKmwsFAmk0lms9nVx9fXVx4eHvrhhx8uOdawYcPUpUuXS4Zx5xQWFsput5d5AAAAAAAAAJfDbYI2h8OhkSNH6tZbb1Xz5s0lSR06dJC/v7/GjBmj06dPKz8/X3/6059UWlqqo0ePXnSszz77TKmpqZo4cWK53nvixImyWq2uR2RkZIXMCQAAAAAAANWH2wRtw4YN044dO/TZZ5+52kJCQjR37lx9+eWXqlmzpqxWq/Ly8hQbGysPjwuXfvjwYT399NP65JNP5OvrW673Hjt2rGw2m+tx+PDhCpkTAAAAAAAAqg9Dr9F2zvDhw7VkyRKtXbtWERERZY4lJCRo3759On78uLy8vBQYGKiwsDDdcMMNFxwrJSVFOTk5io2NdbWVlpZq7dq1evfdd1VYWChPT88yrzGbzWW2pwIAAAAAAACXy9Cgzel0asSIEVq4cKFWr16tqKioi/atXbu2JGnVqlXKycnRgw8+eMF+d999t7Zv316m7dFHH1V0dLTGjBlzXsgGAAAAAAAAVARDg7Zhw4Zp9uzZ+uKLLxQQEKCsrCxJktVqlZ+fnyRpxowZatKkiUJCQpScnKynn35ao0aNUuPGjV3j3H333erRo4eGDx+ugIAA1zXezvH391dwcPB57QAAAAAAAEBFMTRomzp1qiTpzjvvLNM+Y8YMDRo0SJKUnp6usWPHKjc3Vw0bNtRzzz2nUaNGlel/bmtpRXE6nZLE3UcBAMBlObd2OLeWgPthnQcAAK5Eedd5JicrwfNkZGRw51EAAHDFDh8+fN51Z+EeWOcBAICr8VvrPIK2C3A4HMrMzFRAQIBMJlOFj2+32xUZGanDhw/LYrFU+PjuhvlWbcy3amO+VVt1m6907efsdDp18uRJhYeHX/QO6TAW67yKxXyrNuZbtVW3+UrVb87Mt2KVd53nFncddTceHh7X5bfQFoulWvxhP4f5Vm3Mt2pjvlVbdZuvdG3nbLVar8m4qBis864N5lu1Md+qrbrNV6p+c2a+Fac86zx+1QoAAAAAAABUAII2AAAAAAAAoAIQtBnAbDZr/PjxMpvNRpdyXTDfqo35Vm3Mt2qrbvOVqueccX1Vtz9jzLdqY75VW3Wbr1T95sx8jcHNEAAAAAAAAIAKwBltAAAAAAAAQAUgaAMAAAAAAAAqAEEbAAAAAAAAUAEI2gAAAAAAAIAKQNBWQaZMmaKGDRvK19dX7du318aNGy/Zf+7cuYqOjpavr69atGihr776qsxxp9OpcePGqW7duvLz81Pnzp21d+/eazmFy3I58/3nP/+p2267TbVq1VKtWrXUuXPn8/oPGjRIJpOpzOPee++91tMot8uZ78yZM8+bi6+vb5k+Ven7vfPOO8+br8lkUpcuXVx93Pn7Xbt2rbp27arw8HCZTCYtWrToN1+zevVqxcbGymw2q1GjRpo5c+Z5fS7374Tr5XLnu2DBAt1zzz0KCQmRxWJRfHy8li9fXqbPhAkTzvt+o6Ojr+Esyu9y57t69eoL/nnOysoq06+qfL8X+m/TZDKpWbNmrj7u+v1OnDhRbdu2VUBAgEJDQ9W9e3elp6f/5usq+7+/MAbrPNZ557DOY50nVZ11AOs81nnu+v1W9nUeQVsFmDNnjkaPHq3x48crNTVVMTExSkxMVE5OzgX7r1u3Tg899JCGDBmiLVu2qHv37urevbt27Njh6jNp0iS98847mjZtmjZs2CB/f38lJiaqoKDgek3roi53vqtXr9ZDDz2k7777TsnJyYqMjFRCQoKOHDlSpt+9996ro0ePuh6ffvrp9ZjOb7rc+UqSxWIpM5eDBw+WOV6Vvt8FCxaUmeuOHTvk6emp3/3ud2X6uev3m5+fr5iYGE2ZMqVc/ffv368uXbqoU6dOSktL08iRI/XYY4+VWZRcyZ+Z6+Vy57t27Vrdc889+uqrr5SSkqJOnTqpa9eu2rJlS5l+zZo1K/P9/vDDD9ei/Mt2ufM9Jz09vcx8QkNDXceq0vc7efLkMvM8fPiwgoKCzvvv1x2/3zVr1mjYsGFav369VqxYoeLiYiUkJCg/P/+ir6ns//7CGKzzWOf9L9Z5rPOqyjqAdR7rPMk9v99Kv85z4qq1a9fOOWzYMNfz0tJSZ3h4uHPixIkX7N+7d29nly5dyrS1b9/e+eSTTzqdTqfT4XA4w8LCnG+88YbreF5entNsNjs//fTTazCDy3O58/1fJSUlzoCAAOesWbNcbQMHDnR269atokutEJc73xkzZjitVutFx6vq3+9bb73lDAgIcJ46dcrV5s7f73+T5Fy4cOEl+/z5z392NmvWrExbnz59nImJia7nV/sZXi/lme+FNG3a1Pniiy+6no8fP94ZExNTcYVdI+WZ73fffeeU5Dxx4sRF+1Tl7/f/27v/mCrL/4/jr8OPg8BSMBKOZQSmzEyyMhlq04JS7A9tNmVDRi1zmjrdsuVWTp1rw83pH81Rbqj9MBnqFJcTDQz/YJrNn2jo1KjmDE1bCf6qed7fP/xwvt7iL47Hcw6H52M7g3Od69xcF+9zn/vl5c25N23aZC6Xy3799VdfW2ep77lz50yS7dq16459OvvxF6FBziPn3YycR86L5BxgRs6L5PqS84L3/swZbQ/o33//1b59+5Sfn+9ri4qKUn5+vnbv3n3b5+zevdvRX5JGjx7t69/U1KTm5mZHnx49eignJ+eO2wwWf+Z7q8uXL+u///5Tz549He11dXXq1auXsrKyNH36dF24cCGgY/eHv/NtbW1Venq6+vTpo3Hjxuno0aO+xyK9vuXl5SosLFRiYqKjPRzr64977b+B+B2GM6/Xq5aWlnb774kTJ9S7d29lZmaqqKhIv//+e4hGGBiDBw+Wx+PRa6+9pvr6el97pNe3vLxc+fn5Sk9Pd7R3hvr+888/ktTutXmzznz8RWiQ88h5t0POI+dFag4g50V2fcl5wXt/ZqHtAZ0/f17Xr19Xamqqoz01NbXd33q3aW5uvmv/tq8d2Waw+DPfW3300Ufq3bu34wU+ZswYffXVV6qtrdWSJUu0a9cuFRQU6Pr16wEdf0f5M9+srCytWrVKVVVV+uabb+T1ejVs2DCdPn1aUmTXd+/evTpy5IimTJniaA/X+vrjTvvvxYsXdeXKlYDsI+Fs6dKlam1t1cSJE31tOTk5WrNmjaqrq1VWVqampia9/PLLamlpCeFI/ePxePT5559r48aN2rhxo/r06aNRo0Zp//79kgLzHhiuzpw5o23btrXbfztDfb1er+bMmaPhw4fr2WefvWO/znz8RWiQ824g5/0/ch45L1JzgETOi+T6kvOC+/4cE9CtAfdQWlqqiooK1dXVOT44trCw0Pf9oEGDlJ2drb59+6qurk55eXmhGKrfcnNzlZub67s/bNgwDRgwQF988YUWL14cwpE9fOXl5Ro0aJCGDh3qaI+k+nZl3377rRYtWqSqqirHZ1kUFBT4vs/OzlZOTo7S09NVWVmpd999NxRD9VtWVpaysrJ894cNG6ZTp05p+fLl+vrrr0M4sofvyy+/VFJSksaPH+9o7wz1nTFjho4cORIWnykCdGXkPHJeZ69vV0bOI+eFa307Y87jjLYHlJKSoujoaJ09e9bRfvbsWaWlpd32OWlpaXft3/a1I9sMFn/m22bp0qUqLS3Vjh07lJ2dfde+mZmZSklJ0cmTJx94zA/iQebbJjY2Vs8//7xvLpFa30uXLqmiouK+3pDDpb7+uNP+2717d8XHxwfkNROOKioqNGXKFFVWVrY7JftWSUlJ6t+/f6es7+0MHTrUN5dIra+ZadWqVSouLpbb7b5r33Cr78yZM/Xdd9/phx9+0BNPPHHXvp35+IvQIOfdQM67M3Jee+FSX3+Q88h5kVhfcl7wj78stD0gt9utF198UbW1tb42r9er2tpax/923Sw3N9fRX5K+//57X/+MjAylpaU5+ly8eFE//vjjHbcZLP7MV7pxdY/FixerurpaQ4YMuefPOX36tC5cuCCPxxOQcfvL3/ne7Pr162poaPDNJRLrK924lPK1a9c0efLke/6ccKmvP+61/wbiNRNu1q1bp3feeUfr1q3TG2+8cc/+ra2tOnXqVKes7+0cPHjQN5dIrK9048pOJ0+evK9/QIVLfc1MM2fO1KZNm7Rz505lZGTc8zmd+fiL0CDnkfPuhZzXXrjU1x/kPHJepNVXIueF5Pgb0EsrdFEVFRUWFxdna9assZ9//tmmTp1qSUlJ1tzcbGZmxcXFNm/ePF//+vp6i4mJsaVLl1pjY6MtWLDAYmNjraGhwdentLTUkpKSrKqqyg4fPmzjxo2zjIwMu3LlStDnd6uOzre0tNTcbrdt2LDB/vjjD9+tpaXFzMxaWlps7ty5tnv3bmtqarKamhp74YUXrF+/fnb16tWQzPFmHZ3vokWLbPv27Xbq1Cnbt2+fFRYWWrdu3ezo0aO+PpFU3zYjRoywSZMmtWsP9/q2tLTYgQMH7MCBAybJli1bZgcOHLDffvvNzMzmzZtnxcXFvv6//PKLJSQk2IcffmiNjY22YsUKi46Oturqal+fe/0OQ6mj8127dq3FxMTYihUrHPvv33//7evzwQcfWF1dnTU1NVl9fb3l5+dbSkqKnTt3Lujzu1VH57t8+XLbvHmznThxwhoaGmz27NkWFRVlNTU1vj6RVN82kydPtpycnNtuM1zrO336dOvRo4fV1dU5XpuXL1/29Ym04y9Cg5xHziPnkfPIeeGXA8zIeeS88D3+stAWIJ999pk9+eST5na7bejQobZnzx7fYyNHjrSSkhJH/8rKSuvfv7+53W4bOHCgbd261fG41+u1+fPnW2pqqsXFxVleXp4dP348GFO5Lx2Zb3p6uklqd1uwYIGZmV2+fNlef/11e+yxxyw2NtbS09PtvffeC4s3szYdme+cOXN8fVNTU23s2LG2f/9+x/Yiqb5mZseOHTNJtmPHjnbbCvf6tl3m+9Zb2xxLSkps5MiR7Z4zePBgc7vdlpmZaatXr2633bv9DkOpo/MdOXLkXfub3bjsvcfjMbfbbY8//rhNmjTJTp48GdyJ3UFH57tkyRLr27evdevWzXr27GmjRo2ynTt3tttupNTX7MZlzePj423lypW33Wa41vd285Tk2B8j8fiL0CDnkfPakPOcwr2+5DxyHjmPnHezYL0/u/43CQAAAAAAAAAPgM9oAwAAAAAAAAKAhTYAAAAAAAAgAFhoAwAAAAAAAAKAhTYAAAAAAAAgAFhoAwAAAAAAAAKAhTYAAAAAAAAgAFhoAwAAAAAAAAKAhTYAAAAAAAAgAFhoA4AgcLlc2rx5c6iHAQAAgIeArAegDQttACLe22+/LZfL1e42ZsyYUA8NAAAAD4isByCcxIR6AAAQDGPGjNHq1asdbXFxcSEaDQAAAAKJrAcgXHBGG4AuIS4uTmlpaY5bcnKypBun+peVlamgoEDx8fHKzMzUhg0bHM9vaGjQq6++qvj4eD366KOaOnWqWltbHX1WrVqlgQMHKi4uTh6PRzNnznQ8fv78eb355ptKSEhQv379tGXLloc7aQAAgC6CrAcgXLDQBgCS5s+frwkTJujQoUMqKipSYWGhGhsbJUmXLl3S6NGjlZycrJ9++knr169XTU2NI1yVlZVpxowZmjp1qhoaGrRlyxY9/fTTjp+xaNEiTZw4UYcPH9bYsWNVVFSkv/76K6jzBAAA6IrIegCCxgAgwpWUlFh0dLQlJiY6bp9++qmZmUmyadOmOZ6Tk5Nj06dPNzOzlStXWnJysrW2tvoe37p1q0VFRVlzc7OZmfXu3ds+/vjjO45Bkn3yySe++62trSbJtm3bFrB5AgAAdEVkPQDhhM9oA9AlvPLKKyorK3O09ezZ0/d9bm6u47Hc3FwdPHhQktTY2KjnnntOiYmJvseHDx8ur9er48ePy+Vy6cyZM8rLy7vrGLKzs33fJyYmqnv37jp37py/UwIAAMD/kPUAhAsW2gB0CYmJie1O7w+U+Pj4++oXGxvruO9yueT1eh/GkAAAALoUsh6AcMFntAGApD179rS7P2DAAEnSgAEDdOjQIV26dMn3eH19vaKiopSVlaVHHnlETz31lGpra4M6ZgAAANwfsh6AYOGMNgBdwrVr19Tc3Oxoi4mJUUpKiiRp/fr1GjJkiEaMGKG1a9dq7969Ki8vlyQVFRVpwYIFKikp0cKFC/Xnn39q1qxZKi4uVmpqqiRp4cKFmjZtmnr16qWCggK1tLSovr5es2bNCu5EAQAAuiCyHoBwwUIbgC6hurpaHo/H0ZaVlaVjx45JunGVqIqKCr3//vvyeDxat26dnnnmGUlSQkKCtm/frtmzZ+ull15SQkKCJkyYoGXLlvm2VVJSoqtXr2r58uWaO3euUlJS9NZbbwVvggAAAF0YWQ9AuHCZmYV6EAAQSi6XS5s2bdL48eNDPRQAAAAEGFkPQDDxGW0AAAAAAABAALDQBgAAAAAAAAQAfzoKAAAAAAAABABntAEAAAAAAAABwEIbAAAAAAAAEAAstAEAAAAAAAABwEIbAAAAAAAAEAAstAEAAAAAAAABwEIbAAAAAAAAEAAstAEAAAAAAAABwEIbAAAAAAAAEAD/B7HEIDyYWbXeAAAAAElFTkSuQmCC", "text/plain": [ "
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "# Plot the cost over training and validation sets\n", "fig,ax = plt.subplots(1,2,figsize=(15,5))\n", "for i,key in enumerate(costpaths.keys()):\n", " ax_sub=ax[i%3]\n", " ax_sub.plot(costpaths[key])\n", " ax_sub.set_title(key)\n", " ax_sub.set_xlabel('Epoch')\n", " ax_sub.set_ylabel('Loss')\n", "plt.show()" ] }, { "cell_type": "code", "execution_count": 22, "metadata": {}, "outputs": [], "source": [ "# Save the entire model\n", "torch.save(model, os.getcwd() + '/models/recommender.pt')" ] }, { "cell_type": "code", "execution_count": 24, "metadata": {}, "outputs": [], "source": [ "def generate_recommendations(artist_album, playlists, model, playlist_id, device, top_n=10, batch_size=1024):\n", " model.eval()\n", "\n", "\n", " all_movie_ids = torch.tensor(artist_album['artist_album_id'].values, dtype=torch.long, device=device)\n", " user_ids = torch.full((len(all_movie_ids),), playlist_id, dtype=torch.long, device=device)\n", "\n", " # Initialize tensor to store all predictions\n", " all_predictions = torch.zeros(len(all_movie_ids), device=device)\n", "\n", " # Generate predictions in batches\n", " with torch.no_grad():\n", " for i in range(0, len(all_movie_ids), batch_size):\n", " batch_user_ids = user_ids[i:i+batch_size]\n", " batch_movie_ids = all_movie_ids[i:i+batch_size]\n", "\n", " input_tensor = torch.stack([batch_user_ids, batch_movie_ids], dim=1)\n", " batch_predictions = model(input_tensor).squeeze()\n", " all_predictions[i:i+batch_size] = batch_predictions\n", "\n", " # Convert to numpy for easier handling\n", " predictions = all_predictions.cpu().numpy()\n", "\n", " albums_listened = set(playlists.loc[playlists['playlist_id'] == playlist_id, 'artist_album_id'].tolist())\n", "\n", " unlistened_mask = np.isin(artist_album['artist_album_id'].values, list(albums_listened), invert=True)\n", "\n", " # Get top N recommendations\n", " top_indices = np.argsort(predictions[unlistened_mask])[-top_n:][::-1]\n", " recs = artist_album['artist_album_id'].values[unlistened_mask][top_indices]\n", "\n", " recs_names = artist_album.loc[artist_album['artist_album_id'].isin(recs)]\n", " album, artist = recs_names['album_name'].values, recs_names['artist_name'].values\n", "\n", " return album.tolist(), artist.tolist() " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Precision: 5.0609978643478826e-06\n", "Recall: 5.0609978643478826e-06\n" ] } ], "source": [ "from torchmetrics import Precision, Recall\n", "\n", "precision = Precision(task=\"multiclass\", num_classes=num_classes).to(device) \n", "recall = Recall(task=\"multiclass\", num_classes=num_classes).to(device) \n", "\n", "\n", "model.eval()\n", "with torch.no_grad():\n", " for batch in dataloaders['val']:\n", " inputs, targets = batch\n", " inputs = inputs.to(device)\n", " targets = targets.to(device)\n", "\n", " outputs = model(inputs)\n", "\n", " # For binary classification\n", " preds = torch.argmax(outputs, dim=1)\n", "\n", " # Update metrics\n", " precision(preds, targets)\n", " recall(preds, targets)\n", "\n", "# Compute final metrics\n", "final_precision = precision.compute()\n", "final_recall = recall.compute()\n", "\n", "print(f\"Precision: {final_precision}\")\n", "print(f\"Recall: {final_recall}\")" ] } ], "metadata": { "colab": { "machine_shape": "hm", "provenance": [] }, "kernelspec": { "display_name": "Python 3", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.9.19" } }, "nbformat": 4, "nbformat_minor": 0 }