From 4bc30afad3aa8faaf172cf10d147575088d720ef Mon Sep 17 00:00:00 2001 From: Guillaume Dubuisson Duplessis Date: Sun, 3 May 2020 17:09:55 +0200 Subject: [PATCH] Initial commit --- .gitignore | 107 +++ LICENSE | 29 + README.md | 348 ++++++++++ examples/README.ipynb | 618 ++++++++++++++++++ gowpy/__init__.py | 0 gowpy/feature_extraction/__init__.py | 0 gowpy/feature_extraction/gow/__init__.py | 2 + .../feature_extraction/gow/gow_vectorizer.py | 225 +++++++ gowpy/feature_extraction/gow/tw_vectorizer.py | 427 ++++++++++++ gowpy/gow/__init__.py | 0 gowpy/gow/builder.py | 264 ++++++++ gowpy/gow/io.py | 226 +++++++ gowpy/gow/miner.py | 72 ++ gowpy/gow/typing.py | 12 + gowpy/summarization/__init__.py | 0 gowpy/summarization/unsupervised/__init__.py | 1 + .../unsupervised/keyword_extractor_gow.py | 48 ++ gowpy/utils/__init__.py | 0 gowpy/utils/defaults.py | 5 + requirements.txt | 3 + resources/gow.png | Bin 0 -> 47504 bytes setup.py | 32 + 22 files changed, 2419 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 examples/README.ipynb create mode 100644 gowpy/__init__.py create mode 100644 gowpy/feature_extraction/__init__.py create mode 100644 gowpy/feature_extraction/gow/__init__.py create mode 100644 gowpy/feature_extraction/gow/gow_vectorizer.py create mode 100644 gowpy/feature_extraction/gow/tw_vectorizer.py create mode 100644 gowpy/gow/__init__.py create mode 100644 gowpy/gow/builder.py create mode 100644 gowpy/gow/io.py create mode 100644 gowpy/gow/miner.py create mode 100644 gowpy/gow/typing.py create mode 100644 gowpy/summarization/__init__.py create mode 100644 gowpy/summarization/unsupervised/__init__.py create mode 100644 gowpy/summarization/unsupervised/keyword_extractor_gow.py create mode 100644 gowpy/utils/__init__.py create mode 100644 gowpy/utils/defaults.py create mode 100644 requirements.txt create mode 100644 resources/gow.png create mode 100644 setup.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b81fba6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,107 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ + +# pycharm +.idea diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b01acfd --- /dev/null +++ b/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2020, Guillaume Dubuisson Duplessis +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..fcf4985 --- /dev/null +++ b/README.md @@ -0,0 +1,348 @@ +# gowpy + +A very simple framework for exploiting graph-of-words in NLP. +Currently at version **0.1.0** (alpha). + +gowpy leverages graph-of-words representation in order to do: +- **document classification** in a [scikit-learn](https://scikit-learn.org)-like + way via useful vectorizers, and +- **keyword extraction** from a document. + +## Quick Start +### Requirements and Installation +This project is based on Python 3.6+, [scikit-learn](https://github.com/scikit-learn/scikit-learn) and +[NetworkX](https://github.com/networkx/networkx). + +#### Installation from PyPI +```bash +pip install gowpy +``` + +#### Installation from the GitHub Source +First, clone the project: +```bash +git clone https://github.com/GuillaumeDD/gowpy.git +``` + +Then, `cd` to the project folder and run the install command: +```bash +cd gowpy/ +python setup.py install +``` + +### Example Usage + +#### Building a Graph-of-Words from a Document + +```python +from gowpy.gow.builder import GoWBuilder + +# Creation of a graph-of-words builder +# Here: +# - the graph-of-words will be directed, and +# - an edge will link every tokens co-occurring in a sliding window of size 4 +# +builder = GoWBuilder(directed=True, window_size=4) + +text = """gowpy is a simple framework for exploiting graph-of-words in nlp gowpy +leverages graph-of-words representation for document classification and for keyword extraction +from a document""" + +# Here, a preprocessing step fitted to the need of the project should be carried out + +# Creation of the graph-of-words +gow = builder.compute_gow_from_document(text) +``` + +Then, it is possible to visualize the document as a graph-of-words: +```python +import matplotlib.pyplot as plt +import networkx as nx + +g = gow.to_labeled_graph() + +options = { + "font_weight" : 'normal', + "font_color" : 'darkblue', + # + "edge_color" : 'lightgray', + # + "node_size" : 200, + "node_color": 'white', + "with_labels": True, +} +nx.draw(g, **options) +``` + +![A graph-of-words example](./resources/gow.png) + +#### Unsupervised Keywords Extraction +Graph-of-words can be leveraged to extract an automatically adaptative number of +cohesive keywords from a text document in an unsupervised fashion [[2,3]](#references). + +```python +from gowpy.summarization.unsupervised import GoWKeywordExtractor + +# Initialization of the keyword extractor +extractor_kw = GoWKeywordExtractor(directed=False, window_size=4) + +# +# Note that preprocessing is particularly important for keyword extraction +# in order to keep and normalize important terms such as adjectives and nouns. +# +# An already preprocessed text in which to extract keywords +preprocessed_text = """gowpy simple framework exploiting graph-of-words nlp gowpy +leverages graph-of-words representation document classification keyword extraction +document""" + +extractor_kw.extract(preprocessed_text) +``` + +Returns: +```text +[('gowpy', 4), + ('simple', 4), + ('framework', 4), + ('exploiting', 4), + ('graph-of-words', 4), + ('nlp', 4)] +``` + + +#### Classification with TW-IDF: a graph-based term weighting score +TW-IDF [[0]](#references) challenges the term independence assumption behind +the bag-of-words model by (i) exploiting a graph-of-words representation of a +document (here an unweighted directed graph of terms), and by (ii) leveraging +this new representation to replace the term frequency (TF) by graph-based term +weights (TW). + +TW-IDF is accessible via a dedicated vectorizer: +```python +from gowpy.feature_extraction.gow import TwidfVectorizer + +corpus = [ + 'hello world !', + 'foo bar' +] + +vectorizer_gow = TwidfVectorizer( + # Graph-of-words specificities + directed=True, + window_size=4, + # Token frequency filtering + min_df=0.0, + max_df=1.0, + # Graph-based term weighting approach + term_weighting='degree' +) + +X = vectorizer_gow.fit_transform(corpus) +X +``` +Returns: +```text +<2x5 sparse matrix of type '' + with 3 stored elements in Compressed Sparse Row format> +``` + +TW-IDF vectorizer fits seamlessly in a grid search: +```python +from sklearn.pipeline import Pipeline +from sklearn.svm import SVC + +from sklearn.model_selection import GridSearchCV + +pipeline = Pipeline([ + ('gow', TwidfVectorizer()), + ('svm', SVC()), +]) + +parameters = { + 'gow__directed' : [True, False], + 'gow__window_size' : [2, 4, 8, 16], + 'gow__b' : [0.0, 0.003], + 'gow__term_weighting' : ['degree', 'pagerank'], + 'gow__min_df' : [0, 5, 10], + 'gow__max_df' : [0.8, 0.9, 1.0], +# + 'svm__C' : [0.1, 1, 10], + 'svm__kernel' : ['linear'] +} + +# find the best parameters for both the feature extraction and the +# classifier +grid_search = GridSearchCV(pipeline, + parameters, + cv=10, + n_jobs=-1) +``` + +#### Going further: classification based on frequent subgraphs +Frequent subgraphs corresponding to long range n-gram can be mined and +subsequently used for document classification [[1]](#references). + +Classification with frequent subgraphs happens in a 3-step process: +1. Conversion of the corpus of already preprocessed documents into a collection + of graph-of-words +1. Mining the frequent subgraphs +1. Loading the frequent subgraphs and exploiting them for classification + +##### Conversion of the corpus into a collection of graph-of-words +The first step consists in turning the corpus into a graph-of-words and collection +and then export that collection into a file format suited for frequent subgraph +mining. +```python +from gowpy.gow.miner import GoWMiner +import gowpy.gow.io + +corpus = [ + 'hello world !', + 'foo bar', + # and many more... +] + +# Conversation of the corpus into a collection of graph-of-words +gow_miner = GoWMiner(directed=False, window_size=4) +corpus_gows = gow_miner.compute_gow_from_corpus(corpus) + +# Exportation of the collection of graph-of-words into a file for +# interoperability with other languages such as C++ +with open("corpus_gows.data", "w") as f_output: + data = gowpy.gow.io.gow_to_data(corpus_gows) + f_output.write(data) +``` + +##### Mining the frequent subgraphs +Frequent subgraphs mining can be realized via the [gSpan algorithm](https://www.cs.ucsb.edu/~xyan/software/gSpan.htm). +This step is not included in this project and has to be carried out by another +program. + +This project supports the reimplementation from [gBolt available at GitHub](https://github.com/Jokeren/gBolt). +Currently this implementation is limited to **undirected graph**. +To mine frequent subgraphs (after having installed gBolt on your machine): +```bash +OMP_NUM_THREADS=1 ./gbolt --input corpus_gows.data --output gbolt-mining-corpus_gow --dfs --nodes --support 0.01 +``` +Notice the **support parameter** which defines the minimum frequency of a subgraph +to be considered as frequent. Here it is set to 1% (0.01). +This parameter is **corpus specific** and should be carefully tuned (see [[1]](#references)). + +Mining produces two files: +- `gbolt-mining-corpus_gow.t0`: the frequent subgraphs with more than one node +- `gbolt-mining-corpus_gow.nodes`: the frequent single nodes + +These two files can be loaded by the same `gow_miner` used for exportation: +```python +gow_miner.load_graphs('gbolt-mining-corpus_gow.t0', + 'gbolt-mining-corpus_gow.nodes') +gow_miner +``` +Returns: +```text +Graph-of-word miner: + - is_directed: False + - window_size: 4 + - edge_labeling: True + + - Number of tokens: 5 + - Number of links between tokens: 4 + + - Number of loaded subgraph: 13 +``` + +##### Classification with frequent subgraphs +Classification with frequent subgraphs is accessible via a dedicated vectorizer: +```python +from gowpy.feature_extraction.gow import GoWVectorizer + +vectorizer_gow = GoWVectorizer(gow_miner) +X = vectorizer_gow.fit_transform(corpus) +# X is a sparse matrix +``` + +Before tuning the `min_df` (the minimum being the support chosen during mining) +and the `max_df`, it is possible the have a look at the normalized frequency +distribution: +```python +import pandas as pd +s_freq_per_pattern = pd.Series(gow_miner.stat_relative_freq_per_pattern()) +s_freq_per_pattern.describe() +``` +For instance, it can returns the following distribution: +```text +count 10369.000000 +mean 0.026639 +std 0.046551 +min 0.008333 +25% 0.010000 +50% 0.013333 +75% 0.022778 +max 0.865000 +dtype: float64 +``` + + +GoW vectorizer fits nicely in a grid search: +```python +from sklearn.pipeline import Pipeline +from sklearn.svm import SVC +from sklearn.feature_extraction.text import TfidfTransformer + +from sklearn.model_selection import GridSearchCV + +pipeline = Pipeline([ + ('gow', GoWVectorizer(gow_miner)), + ('tfidf', TfidfTransformer()), + ('svm', SVC()), +]) + +parameters = { + 'gow__subgraph_matching' : ['partial', 'induced'], + 'gow__min_df' : [0.00833, 0.01, 0.013333], + 'gow__max_df' : [0.022778, 0.5, 0.865], +# + 'svm__C' : [0.1, 1, 10], + 'svm__kernel' : ['linear'] +} + +# find the best parameters for both the feature extraction and the +# classifier +grid_search = GridSearchCV(pipeline, + parameters, + cv=10, + n_jobs=-1) +``` + +## References + +Detailed explanations, evaluations and discussions can be found in these papers: +- Information retrieval (TW-IDF) + + [0] [Graph-of-word and TW-IDF: New Approach to Ad Hoc IR](https://dl.acm.org/doi/abs/10.1145/2505515.2505671). + *Rousseau, François, and Michalis Vazirgiannis*. + *Proceedings of the 22nd ACM international conference on Information & Knowledge Management*.(**CIKM 2013**) +- Document classification with frequent subgraphs + + [1] [Text Categorization as a Graph Classification Problem](http://www.aclweb.org/anthology/P15-1164). + *Rousseau, François, Emmanouil Kiagias, and Michalis Vazirgiannis*. + *Proceedings of the 53rd Annual Meeting of the Association for Computational Linguistics and the 7th International + Joint Conference on Natural Language Processing* (**ACL 2015**) +- Keyword extraction from graph-of-words + + [2] [Main Core Retention on Graph-of-words for Single-Document Keyword Extraction](https://link.springer.com/chapter/10.1007/978-3-319-16354-3_42). + *Rousseau, François, and Michalis Vazirgiannis*. + *Proceedings of the 37th European Conference on Information Retrieval*. + (**ECIR 2015**) + + [3] [A Graph Degeneracy-based Approach to Keyword Extraction](https://www.aclweb.org/anthology/D16-1191/). + *Tiwier, Antoine Tixier, Malliaros Fragkiskos, and Vazirgiannis, Michalis*. + *Proceedings of the 2016 Conference on Empirical Methods in Natural Language Processing*. + (**EMNLP 2016**) + +This library involves the following algorithms: +- Frequent subgraph Mining (**currently not included in this library**) + + gSpan algorithm implementation for subgraph mining: [gBolt--very fast implementation for gSpan algorithm in data mining ](https://github.com/Jokeren/gBolt) +- Subgraph matching + + VF2 algorithm for subgraph isomorphism matching: [VF2 algorithm for subgraph isomorphism from NetworkX](https://networkx.github.io/documentation/stable/reference/algorithms/isomorphism.vf2.html) +- Graph degeneracy + + [k-core decomposition with NetworkX](https://networkx.github.io/documentation/stable/reference/algorithms/core.html) + + +## License +Released under the 3-Clause BSD license (see [LICENSE file](./LICENSE)) diff --git a/examples/README.ipynb b/examples/README.ipynb new file mode 100644 index 0000000..fc18ddc --- /dev/null +++ b/examples/README.ipynb @@ -0,0 +1,618 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# gowpy: README.md examples" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Building a Graph-of-Words from a Document" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "from gowpy.gow.builder import GoWBuilder" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "builder = GoWBuilder(directed=True, \n", + " window_size=4)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "text = \"\"\"gowpy is a simple framework for exploiting graph-of-words in nlp gowpy \n", + "leverages graph-of-words representation for document classification and for keyword extraction \n", + "from a document\"\"\"\n", + "# ...\n", + "preprocessed_text = \"\"\"gowpy simple framework exploiting graph-of-words nlp gowpy \n", + "leverages graph-of-words representation document classification keyword extraction document\"\"\"" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "gow = builder.compute_gow_from_document(text)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Graph-of-words\n", + "Nodes: ['gowpy', 'is', 'a', 'simple', 'framework', 'for', 'exploiting', 'graph-of-words', 'in', 'nlp', 'leverages', 'representation', 'document', 'classification', 'and', 'keyword', 'extraction', 'from']\n", + "Edges: ['framework__graph-of-words', 'is__simple', 'exploiting__nlp', 'leverages__representation', 'for__exploiting', 'gowpy__a', 'graph-of-words__document', 'in__gowpy', 'extraction__document', 'for__classification', 'gowpy__graph-of-words', 'extraction__a', 'in__nlp', 'document__for', 'keyword__extraction', 'gowpy__leverages', 'a__document', 'graph-of-words__representation', 'a__for', 'gowpy__simple', 'for__in', 'is__a', 'extraction__from', 'nlp__gowpy', 'exploiting__graph-of-words', 'and__for', 'representation__for', 'leverages__graph-of-words', 'document__classification', 'for__document', 'in__leverages', 'from__a', 'gowpy__representation', 'simple__exploiting', 'simple__framework', 'nlp__graph-of-words', 'representation__classification', 'document__and', 'framework__for', 'for__from', 'classification__keyword', 'is__framework', 'nlp__leverages', 'graph-of-words__for', 'a__simple', 'and__keyword', 'for__keyword', 'representation__document', 'simple__for', 'gowpy__is', 'graph-of-words__nlp', 'leverages__for', 'keyword__from', 'graph-of-words__gowpy', 'framework__exploiting', 'exploiting__in', 'and__extraction', 'classification__and', 'for__extraction', 'keyword__a', 'for__graph-of-words', 'classification__for', 'for__and', 'from__document', 'graph-of-words__in', 'a__framework']" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "gow" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt\n", + "import networkx as nx" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "g = gow.to_labeled_graph()" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "options = {\n", + " \"font_weight\" : 'normal',\n", + " \"font_color\" : 'darkblue',\n", + " #\n", + " \"edge_color\" : 'lightgray',\n", + " #\n", + " \"node_size\" : 200,\n", + " \"node_color\": 'white',\n", + " \"with_labels\": True,\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAb4AAAEuCAYAAADx63eqAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4yLjEsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy+j8jraAAAgAElEQVR4nOy9aZxrZZXv/8s8J5V5qEolNZ1zOMgkzo0CKqgo+scJQbGd6KZbHNrho1euw7022to2V8WB6SLiAALXCRUVUZxF1EYEDnVqSCWVeWeekz08/xfl3ic5lapKUqn5+b6pITvZT3ayn/Ws9az1WzJCCAGFQqFQKAcE+U4PgEKhUCiU7YQaPgqFQqEcKKjho1AoFMqBgho+CoVCoRwoqOGjUCgUyoGCGj4KhUKhHCio4aNQKBTKgYIaPgqFQqEcKKjho1AoFMqBgho+CoVCoRwoqOGjUCgUyoGCGj4KhUKhHCio4aNQKBTKgYIaPgqFQqEcKKjho1AoFMqBgho+CoVCoRwoqOGjUCgUyoGCGj4KhUKhHCio4aNQKBTKgYIaPgqFQqEcKKjho1AoFMqBgho+CoVCoRwoqOGjUCgUyoGCGj4KhUKhHCio4aNQKBTKgYIaPgqFAgCQyT6D+fn8hsd94xtP4MIL717z8V//OorDh//vMIdGoQwVGSGE7PQgKBTKziOTfQZzc2/F9LR1W55HoewU1OOjUCgUyoGCGj7KvuLUU7+CBx+M7PQwtpx4vIJXvep7cDq/iImJm/D5z/8FuVwdY2M34N57FwAAlUoL09O34PbbHwcAvOlN9+Gqq+7HBRfcDZPpczj33DsRDhe7vn6x2MQb3/gjOJ1fRCBwI/79338PQVgJDt1222M455w7AADPe96dAIAzzvgqjMbP4VvfehIPPhjB2NgN0msFgzfhM595GKeffhssls/j0kvvRaPBSY9/+tN/hNf7Zfh8X8Yttzzac8iVQhkUavgo+4rHH38zzjtvfKeHsaUIAsHFF38HZ5zhRCx2FR544LX47Gf/jIcfTuLWW1+MK6/8CdLpKv7t336BM8904o1vPFV67je+8QQ+/OFnIZN5O84804XXv/5HXc/xjnc8gGKxicXFK/HLX74Ot9/+BL7ylcdWHferX70OAPDXv/4jKpV34dJLj3R9vbvumsWPf/xqhEJX4tFHGdx228pr/fjHIVx33Z/ws5+9BvPzb8ODDy5v9vJQKBtCDR+F8nc4jgPP8zs9jA15+OEkGKaGj3zkOVCrFZicHMGVV56OO+98EhdeGMSrX30IL3jBXfjRjxbxhS+cj2aziUajAY7j8KIXBXDWWSNoNmt4//tPw+9/H8ejj4bBMIz0+jwv4M47n8QnP/lcmExqBIMWvPe9T8PXvvb4wGN+5zufCp/PCJtNh4svnsIjj6yc7667ZvHmNz8Fp57qgF6vwsc+9pxNXx8KZSOUOz0ACmWYBIM34ZZbXoTf/CaKJ57IQqtV4jvfmcP4uBlf/epL8LSneVY9h+d5ZDIZZDIZOBwOuN3uoY6JEAJCCARBkH62/77Wz7Uee/jhMOLxCszmzwEgf38PBGefbcPjjz+O5z/fgC9+MYt/+qcpFApxFIsyyOVy1Go12O1aZDIZyOVyyOVyjIyoEQ4XYLefmAoymTpYVkAgYJb+FwiYEYtVBr4GHo9B+l2vVyIeX3mteLyCpz3txPX2+00Dn4NC6RVq+Cj7lu9/fwHf/vYr8JWvvBj/83/+Bldf/QD+8IfXS48TQsAwDDKZjGScGo0GyuXyhsapn8cIIZDJVozPWj/XekyhUECpVHb8/8gRD4JBEx599A2rnkMI8La3fQtvfONR3HXXAt7//vOkbEuzeR7lMge/3496vQ6GKaJQaMJg4FGpnDBqDocOKpUc4XAJR486AACRSAmjo8ahf0ZerwHR6IlzLy+Xh34OCuVkqOGj7FvOOWcUF100CQC44oqj+Oxn/7LqmHQ63fF3vV5HNptd0yjJ5XKoVKoNDdbJP2Uy2dDe1/nnm2A2/x7XX/83vPOdT4VSqcCxY1nU6xx+/OMlyGQy3Hrri/GpT/0Rb3zjj/DTn/5/aDYbqFaruO++KO644w94xjO8+K//OoanP92N5z73NKhUKgD3AgAUCjle+9rDuOaa3+D22y9CLlfHddf9Ge9739O6jsft1mNxsThQOcNrX3sYb3nLT3DFFUcRCJjx8Y//fjOXhkLpCWr4KPuWzvCaCo0GB44ToFSubG3LZDLMzMwgnU6jVCqBEAK9Xo/x8d2dHKNQyPGDH7wS733vg5iYuBnNJo/Dh2245JJpXHfdn/Dzn78CDJPGa17jxN13P46PfOQXeM97zoRSqcRllx3G7bfH8S//8mc89alu3HHHxVCpVKhWqwAAQRAAANdf/wK84x0PYHLyZmi1Slx55Wl4y1tOWzUWQgg+/OFn4R//8T7U6xxuuukCuFz6nt/LS14yiXe+86k4//xvQS6X4cMffjZuv/0JaDSK4VwsCqULtICdsq9o3+Obny/g619/KQBgaamIiYmbwbLvkQxfOxzHIZPJQKVSwW63b/ewB0IQBNTrddTrddRqNdRqNcl463Q66adCsWJE3vSm+zA2ZsK///s50mvwPI9kMolyuQyfzwez2bzW6brCcRxmZ2dBCIFCoYBKpYLX64XBYNj4yV04diyLpzzlNjSb/9b1c6JQhgH1+Ch7FkIIqtUqVCoVNBrNpl5LqVTC41md+LJbIISg1Wp1GLlmswmtVgudTgez2Qy32w21Wt1TWJUQglKphEQiAbPZjEAggEwmg2azCYfD0XNoVjS2wIoR5XleMrS98p3vzOGiiyZQq3H4wAd+hYsvnqJGj7KlUMNH2XMQQlAul8EwDDiOkyZ9uXz/TJY8z3cYuXq9DrlcLnlyIyMj0Gq1A73nVquFRCKBVquFkZERVKtV5HI5AECxWAQhBC6Xq6fXMplM0Ol0qNfrAACVStX3mG688a9405vug0Ihx7nnjuFLX3phf2+IQukTGuqk7BlEL4VhGBBCoNVqUalU4HK5YLPZhppAsp0QQtBsNjuMHMuy0Gq10Ov1UshyJQFlc+fJZrNgGAZ2ux0KhQKJRGLVcT6fDzabrefXbTQamJ+fh1qthsViQS6Xg9vthtVq3bOfCWV/Qw0fZddDCEGxWATDMJDL5bBYLCgUClAqlfD5fFAqlcjlcrDb7XtiomVZVvLmxD06pVLZYeS0Wu1Q30u9Xkc8HodcLofP54NGowEhBKFQCLVaTTpOJpNhcnISOp2ur9fPZrMwGAzQarVoNBqIxWId56JQdhPU8FF2LYQQFAoFMAwDpVIJu92Oer2OfD4Pj8cDo9GIbDaLbDYLQgimp6eh1Wp3etgdCIKARqMhGblarQZBEDqST/R6fd/7Yv2cP51OS9dsZGREMqgsy2JhYUXXk+NOaGeecsopmx5Pu3fpdDr3zKKEcjCgho+y6xAEQTJ4arUaTqcTABCPx6HVauHz+ZDJZCSDB6x4KkePHpUmV0EQpPo5MTFkqz0PQghYlu0wco1GAxqNpsPI9ZqAslnK5TLi8Tj0ej28Xi+UyhNb+jzPIxQKwWw2Qy6XS+FjADh69OjQxtBsNhGLxUAIwdjYGPX+KLsCmtxC2TUIgoB8Pg+GYaDVauH3+6HRaJBKpVAqlTrS7dVqdcdz9Xq9ZEwIIVhYWECz2cSRI0egUCiwuLiIqampVc/bDO0JKOJPmUwmGTmPxwOdTrftSTccxyGRSKBWq8Hn88Fk6pQBEwQB4XAYBoMBJpMJS0tLmJychFKpRKPRGOpYNBoNJiYmkMvlsLi4CIfD0VfWKIWyFVCPj7Lj8DyPXC6HbDYLnU4Hl8sFnU4neSxGoxEej6cj/NZu3MQsRDETsVgsIhqNdvw/Go1Cp9MNXKPXnoAiGrn2BBTRm9tsAspmEEPDyWQSIyMjXTNdCSEIh8NQKpXwer0IhUKw2+2wWre+iWyr1UIsFgPP8xgbG9t1YWnKwYEaPsqOwfO8tEdnNBrhdDqh1Wo7PJbR0VEYjas1IhOJBJrNJrxeL6LRKHw+H3Q6HQRBwOzsrNRlQaFQ4MiRIyiVSsjlcpiYmOhpbBzHdRi59gQU0cgNOwFlMzSbTcTjcfA8j9HR0a7JKYQQRKNRCIKA8fFxpFIptFot+P3+bXsfhBDk83mkUinYbDY4nc59VYZC2RtQw0fZdjiOQzabRS6Xg8lkgtPplLIMi8UikskkLBbLmrV5Yjh0ampqVRJGNptdlaIfCASg1+sxOzuLw4cPr3qOmIDSXjfH8/wqBZT2PbLdAiFE6iyxXhIJIQSJRAKNRgPBYBC1Wg3RaBTT09M78r5YlkU8Hker1cLY2FjfWaQUymagho+ybYiyYPl8HmazGQ6HQ0p2aJ8IR0dHodd313us1WoIh8OYmJjoGioTZbzS6bSk6GI2m6HRaLC0tISRkRHo9foOIycmoLQbOY1Gs2u8ubWo1WqIxWJQqVTw+Xzr7l+mUimUy2XJ452fn++6/7ediAudRCIBq9UKl8tFvT/KtkANH2XLYVkWmUwGhUIBFosFDodDmqTbQ192ux0Oh2PNyU9Mv+9FUzISicBiscBoNEq1coVCAa1WCwqFosPItetZ7gV4npcSfjweDywWy7pGWgwnT05OQqFQIBqNQqFQwOfzbeOo14bjOMTjcTQaDYyNja256KFQhgU1fJQto9VqIZPJoFgsYmRkBA6HoyP5oz3VfXR0dN1kB0EQEAqFYDKZ1pTTEhNQ6vU6UqkU5HJ5RwKKRqNBMpnEkSNH9qxnIeprGgwGeDyeDcOUYrLL5OQk1Gq1VCYyNTW1666B6P2tF+amUIbB7tu0oOx5ms0mGIZBuVyG1WrFzMxMxwTd675U+/HxeBwqlUqq6QNWPIWT9SwVCoXkMbhcLqlOTSSfz6NWq3VNmNnNsCwr7dGtlfBzMuVyGclkEsFgEGq1WtLoDAaDu9KoWCwWGAwGJBIJzM3N9fw+KZR+oR4fZWg0Gg0wDINKpQK73Q6bzbbKI6nX64jFYlAoFBgdHe2prk4Mk3q93g4VFI7jVimgiOc7duwYpqenV5UXpNNpcBy3a8J8GzFoFmS1WkUkEpESe0R5MjGZaLdTKpUQj8clAfK9FIqm7H6ox0fZNPV6HQzDoFqtwuFwwOfzdc2cZBgGuVxulXRWN0QFlEKhgHK5DJlMJqmQGAwGKRN0rQxGnue7hgHNZjPC4TAIIbs+eaXRaCAej4MQsmYyTzfq9ToikQj8fr/k/WYyGQCAw+HYsvEOE7PZDIPBgGQyuSsScSj7C2r4KANTq9XAMAzq9TocDgfGxsa6eiPVahWxWAxarbarFyZmYrbXzYndF2q1GjweD6xWa8+rfkEQIJfLuxo2MYu00Wjs2hT69kVCv50nms0mwuEwfD6fFCas1+vIZDKYmpra9ca+HTEqUKlUEIvFYDAY4PV6qfdH2TQ01EnZkEajgWg0iomJCSgUClSrVTAMIzUttVqtXQ1ee/ah1+uFxWKRdDPbjZzYULU9ZCmXyxEKhWCz2fpWWxEn/0OHDnV9PJFIQKFQ9NxzbjsRFwkajQY+n68vJRiWZbG4uAin0ym1FRIEAQsLC3A6nRgZGdmqYW857d+lQTrFUyjtUI+Psi48zyMcDkvJFa1WCyzLShPpWvtN7QLJHo8HzWYTS0tLUkNV0ch1a6hKCEEkEoFer++rL1z7mNfzCkwmE5LJ5K4yfDzPI5lMolwuDzSxcxyHpaUl2Gy2jmuWTCah1WphsViGPeRtRSy/sFgsiMViKBaLq4S3KZReod+aA8qDD0bwhjf8CNHoVWseI0pcsSwLYCU13uPxrKsOUq1WkUql0Gw2IZfLUS6XpSQUm83WU0PVdDoNnucHltLiOG5dw2cwGMCyLFiW3VFtTeBEc91EIgGz2YyZmZm+Q3mi6LTRaOzYwyuXyyiXy5ient5TIc71MBgMmJ6eRjqdxvz8PLxeL8xm8755f5TtgRo+ypqIHkg7lUpFmlzbG6pWq1U0Gg0QQqDRaOByuWA0GvtWQCkWiygUCpuqM1srsUVEJpPBaDSiVCoNLFo9DMTyglarhfHx8YEKtwVBQCQSgUajgcfjka41x3GIxWIYGxvbd3ticrkcHo8HZrMZsVgMhUKh77Aw5WBDDR9lTXieh06nA8uyUqNSMWOwXq9LDVXFhBGVSrUp5Q2xS3gwGNxUCGujUCewkjWYz+d3xPC1N2m12+3w+/0DGXlCCGKxGGQyGUZHRzvaMsViMYyMjOzrOji9Xo+pqSkwDIP5+fmesoUpFADYfVWslKESDN6Ez3zmYZx++m2wWD6PSy+9F40G1/W4T37yIRw9eius1uvx5jffB5vNhVar1dGdWxAEmM1mBINBHD58GGazGYVCAUajEdPT0wMbPY7jEIlE4PV6N51tuVGoEwCMRqMkRr2d1Ot1LC4uolwuY3JycmB9SlF0muO4VSHhQqEAlmV31R7mViGXy+F2uxEMBpHNZhEOh9FqtXZ6WJRdDjV8B4C77prFj3/8aoRCV+LRRxncdttjXY/7xjeewE9+8mosLLwNx4/nce21D3XsycnlchBCYDAYAADhcBj5fB4TExObkpgihGB5eRkWi2UomYcbhToBSAovlUpl0+frBUEQkEwmpQSUYDC4qW7k6XQatVoN4+PjHde92WwimUyuWVqyX9HpdJiamoJer8fCwgJyuRxowjplLWio8wDwznc+FT7fSsjr4oun8MgjDI4cWZ0tefXVZ8HvX8kmvOaaZ+Ed73gA1177XAArE3e1WkW1WkU+n0c2m+1JbqwXEomEtHIfBr14fMBKdme5XN7yjMf2DNeT5dsGQdQ/FUWnRcRkJLGv4UFDJpNJMnVi5mev6kCUgwU1fAcAj8cg/a7XKxGPd/dy/P4TyhiBgBnxeFX6Wy6XQ6VSoVqtQi6XY3JyclMei0gul0OlUhlqcXUvHh+wYvjS6fSWqbi0N9QdlvKIuOiYmJhY9R4ZhoFcLt/RhJ3dgFarxeTkJLLZLBYWFvoWAaDsf6jho0gsL5/I4IxESvD5Vgxmu5KI2+2G1WodyiQilj6c7Llsll6SWwBArVZDpVKhVqtJ4dthQAiRuiKMjIxgZmZmKGHHUqmEVColiU63U6vVkM1m91XpwmaQyWRwOBwwmUwd3t8wFmuUvc/B2QSgbMgXv/gIotEycrk6rr32IVx66RHUajUsLCyg0Whgenp6aCvnVquF5eVljI2NDX0y4jiu53Ci2WxGqVQa2rnFQv1sNotgMAiv1zsUoycquoyPj68KY/I8j2g0SlP6u6DRaDAxMQGLxYLFxUUwDEP3/ijU4zso9BLOu/zyU3DhhXcjHq/iFa+YwtvffkTKtBxmkbBYe2a324cuPCwKVPfqQZpMJiwvL3fUwA163n5aLfVDN9HpdpLJJPR6/Z5XZ9kqZDKZ9F2LxWIolUob9n+k7G+oVucBIJ/Pw2QyresFBYM34ZZbXoQXvjAAYMWLSCQSPTU77QcxAQMAxsbGhh6W4zgOx48fx9GjR3sez/HjxxEIBAaeCGu1GuLxOJRKJXw+31CTKZrNJkKh0JoyZqLqy/T09L4rVN8K2ts82e12OByOA5X9SlmBfuL7GLHWi2GYvurVBEFAIpHA6Ojo0LUQM5kMms1mR8H1MOk1sUVEJpNJ2Z2DnCuRSCASicDhcCAQCAzV6LEsi6WlJbjd7q5Gj2VZxOPxfanOslXIZDLYbDZMTU2hVqthcXER9Xp9p4dF2Wao4duniKLFjUZjVQamIAhdn8PzPARBgCAI4DgO0Wh0qPsh5XIZ2WwWgUBgy1bZ/YQ5RQbZ5yuVSpifnwfP85ienh66Yki76LTVal31uKjOYrVah5qYc1BQq9UIBAKw2+1YWlpCMplc876g7D/oHt8+pNFoIBwOw2w2r9q7KhaLUmsXhUIhGbbZ2X8EIQRyuRxyuRzj4+MIh8OIxWJD8c6azSai0SjGx8e3NAGjn8QWEb1ej2az2ZNotdilotFoYHR0dEskwcSOGOt1S8/lcuB5/kCos2wVMpkMVqsVRqMRiUQCCwsLGB0dHVh9iLJ3oB7fPqNYLCIUCsHtdsPr9XYYrFKphOXlZRSLRdTrdeTzeRw/fhzpdBoqlapjj0sulyMQCKDVakldwAdFnMjdbveWeyeDeHxyuXzDcCchBLlcDvPz89BoNJient4Soycm/mi12jUL+pvNJtLp9JbskR5EVCoV/H4/XC4XIpEIEokE9f72OTS5ZZ9ACEE6nUahUMD4+PgqvctCoYBYLCYZMLFrwujo6LramKLR0mq1qwxpr+MKh8NQq9Xw+Xz9v7E+YRgGHMfB6/X29bxCoYBisYhAILDqsUajIRn/rcwGFKXbAKzZkokQgoWFhVV99yjDQRQdqNfrGB0dpWHkfQr1+PYBPM8jEomgWq1icnJylSETBKHD6AGAUqnE1NTUhoLQCoUCgUAA9XodyWSyb88vlUqBENK3IRqUfpNbZmdzOPPMr8Lv/ypuvPHxjpW+IAhIpVIIhUKwWCyYnJzcUqMnCAIUCsW6npzonXfb91uLYPAm/Oxn4WENdV+jVCrh9/vh8XiwvLyMeDy+7ULmlK2HGr49TrPZxOLiIpRKJYLBYNc9KjFs2T6ZqtXqnr030fhVq1VJ4qsXRC9q0Iayg9BvqPPTn/4jzj9/HOXyu3DlladIotXVarWjcH+YdXndkMlkUpfxtRJ/RJ3UrcqIpZxAbAosCALm5+cHyvql7F5ocsseplwuIxqNwu12rxn2EgQBmUwG2WwWAOBwOAbKQBQNaygUksSA16NeryORSHTVlNxK+k1uCYdLeN3rjgBYmeyKxaLUudzt9sBq3Xy3iH5Y63NpV2fp9f1xnAClkq5tB0X0vkWRcYPBAK/XS0tH9gH0rtiDiCohooTVWkZPlBur1+vwer0ghMDlckGr1Q4kEyYav0KhAIZh1jxO7K3n8/m2XR2jH4/v+c//Fn7xi2VcffUDMBo/h3/6p1/ife/7Ha644kGcffaP8ec/F/HDHy7grLNuh9n8efj9N+JjH/ut9PylpSJkss/gK1/5G/z+G2G1Xo8bbngEDz+cwOmn34aRketx9dU/6zjnrbf+DaecstLz8EUvugfhcBEA8NGP/hbveMcDAACW5WEwfBbvf/+DAIB6nYXB8DmwrApmsxnf//48Tj31KxgZuR7nnXcnjh3LSq8fDN6ET33qIZx++m0wGD4HjutM0jh2LIuJiZtwxx3H+r62BxWTyYTp6WnI5XLMzc0NVeKOskMQyp6C53kSiUTI3NwcaTabax4Tj8fJsWPHSD6fJ4IgkMXFRXL8+PGhjKHVapHZ2VnCMEzXcy8sLJBkMjmUc/XL8ePHSb1e7/n4c8+9g3z5y38mS0tL5JJL7iRG4/8h998/R3heIPU6S37xizB59NE04XmB/PWvaeJyfZF85zsr1zEUKhDgP8k///NPSb3Okp/8JEQ0muvIK17xHZJKVUg0WiJO5xfIgw9GCCGEfPe7c2Rq6mbyxBMZwrI8+fjHf0ee/exvEEIIeeCBMHnKU75CCCHkt7+NksnJm8gznvE1Qggh3/72o+TQoRtIOBwms7NZotf/H/LTn4ZIq8WRT33qITI1dTNpNjlCCCGBwI3kjDNuI5FIkdRqLel/99+/RP785yTx+28g9947P5RrfRCpVCpkdnaWRCIRwrLsTg+HMiDU49tDtFotLC4uAgAmJye7qoRUKhXMzc2B4zipsJoQgmq1OrR2NSqVSup4ncvlpP+TvyvFKBSKHasv6yfUSQgBy7JgGAY6nQ4mkwkvfvE4nvIUI+RyGbRaJc47bxynneaEXC7D6ac7cdllR/DLX0Y7XufDH34WtFolLrwwCINBhcsuOwKXy4DRUROe+9wx/Pd/pwEAN9zwCP7H/3gmTjnFDqVSjg996Fl45JE0wuEinv1sL+bm8shm6/jVr6J4y1tOQzRaxl//+iR++tN5PO1pVjSbTXzrW7N46UsnccEFQahUCrzvfU9Hvc7hd7+LSeN55zufCr/fDJ3uxH7vr38dxctf/h3cfvtL8LKXTQ3hSh9MDAYDpqenoVQqMT8/j2KxuNNDogwA3ePbI1SrVSwvL0v6gifvBfE8j2QyiUqlsqr3W6FQAIChdDcXUavVHXt+VqsVuVwOtVoNk5OTO5J8QfoQqK7X61LGnsPhhMvlgkwmw8SEFaVSCR6PBwDw0EMJfPCDv8Jjj2XQavFoNnm85jWHO17L7T6R8q7TKeF26zv+rlRaAFb2E9/1rp/jve99sG3MQCxWQSBgwdOe5sEvf7mMX/0qig996Jn49a/n8Yc/JPCnP+Vw+eUBNJtNHDsWg8WiQjgclsQGPB4tnngijtNOM0AQBNhsClQqFcjl8r9fC4Ibbvgrzj13DOedN76JK0wBVpLFvF4vLBaL1PLI6/XSzhh7CGr49gC5XA6pVApjY2NduxmUSiXE43GYzeauYsXZbBYGg2HoMmFiy5dQKIRms4l8Pj/03nr9wPM85HL5ukZXEASk02nk83l4PB5otVqoVCduA5VKCUEQ0Gw2odFocPnlP8DVV5+F++57FbRaJd797p8jkxlM29HvN+Oaa56F17++u4D2ueeO4ec/j+C//zuNZzzDi3PP9eN3v2Pw2GNFnH22DXq9HtPTLjz2WBZWq1WSlovHq/B4dGg2myBEQLVaAcMwEAQBPM+DZTl86ENHcOuti3jjG+/BNdecJhlF0XiKv6/3v/bHDnJW6Sc+8QcsLhZxyy0vwtTUFNLpNObn5yVjuJlrc955d+INbziKt73t9CGOmHIy1PDtYgRBQDKZlOrzTk5IWZn04mg0GvD7/V2LbcVJXPRgho1Go8Ho6CjC4TCcTueONvrcqIavUqkgFotBr9djZmam67EymUzS7nQ6nSiXW7DZtNBqlfjjHxP45jeP4cILgz2Nh+M4CMKJ0o+rrjoDH/7wb3DmmS6ceqoDxWITP/3pkuRBnnuuH69+9ffx9Kd7APA45RQ1rr02itFRPWw2NQRBwOtf/xQ89am34+GH83je88bwuc/9BTqdGi972VOgViugUCjhdrsxMXGiEJd9AU0AACAASURBVF+lehCnnjqNBx98Dl74wrtxyy1xfPzjz+7QZhV/XzGUbMffJx8jCMIq47iRoVzrf3vRgH7oQ8+Sfl/xuD2S91ev1zfd4oqy9VDDt0sRMyMVCsUqL4q0dfi2Wq0YGxtb05vLZDKSJNdWIBZ52+125HI56HS6rp0EtoO1wpwcx0kLiJPDwN0wmUxgGAZOpxNf+tIL8d73Poirr34A557rx2tfexiFQnPd53PcStg5n8+DZVmIZY+XXDKDSqWF173uBwiHS7BYNLjggoBk+J7zHB/qdQ7Pfe4o0uk0pqaM0GjkOPvslWJ1t9sNk8mEr3/9pXjHOx5ALFbBmWe6cO+9l0Ct3tjLtlp1uP/+1+D88++CVqvCxz9+zobP6Qb5e7H9yQbz5P+xLLvhcWL94maMp/j7TqLT6TA5OQlg7ZIUyu6BSpbtQsTGoyMjI9Lek4ionclx3IZyYwAwOzsLnU6H8fHh7+2Qv0tsyeVyjI6OotFoYGlpac2Q7FZTKpWQz+cl2bH2BYJ4LXsJwwqCgCeffBKHDh3qqyaw1Wohk8mgWCxiZGQEdrt9oDZFgiBI17VUKkmCAXq9HjabDWazeccn+mFACJH2ZdcykL0+BmDTxlP0QNvvt0996iF8/vP/jVKpCZ/PiC996YX49a+jmJ8v4OtffymWloqYmLgZt976InzkI79DpdLCJz/5XJx9thtvfetPEImU8YY3nIIvfOGFAIDbbnsMN9/8KM46y4Wvfe0JeL1GfPGLL8ALXrDynT051HnrrX/Df/7nw0gmq3jGM7y46aYLEAjQhsObhXp8u4xCoYBEIgGfz9fRUZv8XSQ5nU7D4XB0TXA5mVarBZZlEQwGt2SsDMOAZVlMTExAJpNBp9MhEAggHA7D7/dviYjzerR7fM1mU0peCQaDGy4Q2pHL5TAajSiXyz1JgzUaDTAMg0qlApvN1jWMSgjpyRMghCASiUg99iKRCMrlMhwOB3Q6HfL5PBKJBCwWC6xWa1/va7chGplhGPFu4dhuBrLZbK57HPl7hxKFQoGlpSo++9k/4bvffT68XgPi8ToIaaJaraLZbCKXy6FUqgEAfvvbZczOvhm/+U0cL3/5d/DiF0/gZz97DVhWwFln3Y7XvOYwzj3XD2AlYerVrz6ETObt+Pa35/DKV34PodCVsNk6P8vvfW8en/jEQ7j33kswM2PFf/zHQ7jssh/id7+7fNPX66BDDd8ugRCCVCqFYrGIiYmJjsJvUSQZQNe9vrVIp9NQKpVbsu9WKpWQy+UwNTXVMXHp9XqMj48jEolgfHx8W0V+OY6DXC4HwzDIZDJwOp0DS42ZTCaUSqV1DV+tVgPDMKjX67Db7VKrp25UKhXo9fo1E0NEry4SicBut0uG0+/3I5lMwm63Q6VSwWKxoNVqIZ/PS6Fwq9WKkZGRA60oInpvm1UJavdAOS4HjiNIpYDJSSMOHTJCEAQQsgRBEFCr1SQpsze/eRw6nWpVSQsAqaRFNHwulx7vfvfZkMlkuPTSI/iv//oTfvjDRVxxxakdY2kvfwFW9hY/8YmHEA4Xqde3Sajh2wXwPI/l5WUQQjA1NSXdvIQQMAyDbDYLl8sFm83W1yS+0cQ9KI1GA7FYDIFAoGsKt8FggN/vRyQSQSAQ2Lb+Zo1GA9VqFVqtFlNTU5vqhm4ymaT2NO2GXayJZBgGrVYLDocDfr9/Xa9FFAlfKwEJAJLJJHK5HIxGY0eYWC6Xr+pqoVar4Xa74XK5UKlUkM/nkUqlYDKZYLOtZH/SfabBkMlk0v136qkefO5zL8CnP/0IHn88gxe9KIjrrjsfRqMBOh2LsbExcNxKHd8zn3lUuubrlbQAwOiosePzCQTMiMcrq8ayUfkLZXCo4dthGo0GIpEITCZTRzZYvV5HLBaTuij0O4nXajUIgrBmI9NBEZNuPB7PugbNaDRibGwM4XC471Bjv/A8j3Q6jVKpBIvFMhQRZ6VSCa1Wi0qlArPZDEIIyuUyGIYBz/NwOp09a54yDAODwbCu91utVqVzJBIJuN3uDUOAMpkMJpMJJpMJHMehUChI7ZNEL5DWlm2Oyy8/BZdffgpKpSb++Z/vxwc+8CtMTW3O6MRilY7QdyRSwstfvlpUYKPyF8rg7P0d8j1MqVRCKBSC0+mUet2JJQxLS0twOBwIBAIDeS7pdBpqtXqoAtFiMovJZOrJkzSZTFKpQ6PRGNo42imVSpifnwfP8zAYDDCbzUPzdsSyhkKhgPn5eWl/dWZmBlartafztFot5HK5dctJWq0Wms0TmaLZbBahUKivsSqVSjgcDkxPT2NsbAytVgtzc3MIh8MdCTKU3pmdzeHnP4+g2eSg1Sqh0ymx1lqkWCx2fIbrkU7X8PnP/wUsy+Puu2dx7FgOF100ueq4q646A5/85EN4/PHM38/RxN13zw78fignoB7fDiCGMHO5XEcosFKpIB6PQ6fTrVln1ivVanXNDt6DkkwmAaCvmkDRW1paWkIwGByaaDXLskgkEmg0GhgdHYXRaMTi4uLQ9rnExIdCoSDVQRqNxr6Navv+XDcIIYhGo9BqtajXVwrjFQrFwPJyMpkMer0eer0eHo8HpVIJmUwG8XgcIyMjsFqtO1pruZdoNnl88IO/wrFjWahUCjznOT7cdNOFuOmmv4LneTAMg2h0RY4uGo3C5XL0dG8885kr8nQOxxfhdhtwzz0vh92+OiKyUfkLZXBoOcM2w/M8YrEYWJbF+Pg4VCqVJDdWLpfh8/k2XQdXLBaxvLyMo0ePDi3tPZ/Pg2EYTE5ODmSQxbKCiYmJTU28hBBpT8tms8HpdErv8fjx4xgfH9+UceV5Hvl8HplMBjqdThIHGGSfUiyYn5mZWfNzYBgG5XJZytg0GAwghAw9E7fRaCCfz6NQKECj0cBqtcJiseyLsojthmVZLC4ugmXZjv/L5XIcPbp+WPK22x7DLbf8Db/5zWVbOUTKBlCPbxtptVoIh8PQ6/VS0bkoN2YymTAzMzMUj0WctIc1qdVqNcloDeqFjoyMQBAELC0tYWJiYqDwrZjdSghZlfkK9N99vR2O4yTRbaPRiEAgAJ1Oh2QyiVKp1LfhEwW7PR7Pmp9DvV5HJpOREppED31ubg7VanWoGbFarRZerxdutxvlchn5fB7JZFIqi9BqtTQhZgMajQYymQxKpRKMRuMqw9fvYkUQBOTzeVitVroA2Wao4dsmKpUKlpeXpexM0fOr1+sYGxsbWs2bIAio1+tDK1hnWRaRSASjo6ObDlPabDYQQhAKhfoyfu3NdNfKbu1HoLodlmWRyWRQKBRgNptXlYuYzWbEYrG+Jd9yuRyUSuWa3rsgCIhGo/B6vauug8vlQiqVkuojh4lcLofFYpHKIgqFAi2LWAdCCCqVCjKZDJrNJmw2G9xuN1KpVMdxZrO578VRPB6XIiEWiwV2u31P12XuJWioc4shhCCbzSKTyWBsbAwGgwHFYhGJRAJWqxUul2uoqz2GYZBOp3HqqadufPAGCIKAUCgEk8k01DZDmUwGuVwOExMTG2YdVqtVxONxqNVq+Hy+NY/nOA5zc3M45ZRTehpDs9mUVu8jIyNwOBxdX5sQgtnZ2b5CtOJYunmlImJxvd/v73rO+fl5eDyebVHAEUs08vk8yuWylLxkMBgOrBcoCAKKxSIymZXEEofDAY1Gg3g8jlarBYVCgdHRUSwtLUEmk+HIkSN9LxiKxSKi0WhH4tHExMS21r4eVKjHt4UIgiCJSIs6fuFwWFJT2YrVXT6fH4r3SAhBPB6HSqUaekmEw+GQEl7WCp+273t6vd4NszV79fZ6UVlpRywZKJfLfQkHWCyWNY1euVxGuVzG9PT0mucUvYpBEmr6RSaTwWg0wmg0guM4aWEmCAKsViusVuuBKYvgOA65XA65XA5arVbq4JFKpZBIJAAAdrtd2ls+dOgQOI4byEs+2cCJNZiUrYcavi1CDBGqVCpMTEygUCggnU5L/fS2IqbPcRxarVZXL6JfstksGo3GloTbAMDpdEoeZbvxI4SgVCohkUjAbDb3vO+50eTTj8rKyZhMJmQyGTgcjg2PbTQaKBaLmJmZWXOcsVhMkiRb75wMw0i6n9uFUqmE3W6HzWZDvV5HPp/H/Pw89Ho9rFYrTCbTvvQCm80mstmsFPIOBoPQaDTI5XKSbqrYiaR9QaNWqwcWSlAqlVAqlWBZFhqNRtIcpWw91PBtAbVaTZKeMplMCIfDayZkDBOGYaBQKDbtSYp7GlvdW8/lcnV4fjzPI5FISMa7n5BPt8SWQVRWumE0GhGNRjfs7i4mtLhcrq7HEUIQi8UwMjKyoVcuen3xeHzTPd4Gob0swuv1SmG//VYWUavVkMlkUK1WYbVaMTMzA5VKhUqlgkgkIoUhnU5n38pJvSAKFRiNRoRCIaTT6aGXIVFWQw3fEKhUKtJ+iNg0dnR0FM1mE6FQaCC5sUEQV6ubodlsYnl5GX6/f1OSX70gTu6CIEhF6IMap3aPr11lRRAEOByOnlVWuiGXy2EwGDYUrS6VSuA4DjabrevjhUIBLMv27JEbjUaoVCrk8/k1X3M7kMvlUshTbDgcCoWgVqv3ZFmEGFXIZDLgOA4OhwOjo6NQKBRotVqIRCKoVquS8Pp6e8ubpd2bDwQCWFxchEql2tHP+yBADd8mqdfrWFpagsFggEajQaVSgc/nkwSiN6sZ2SuNRgM8z28qCYXneUQiEbhcrm3rrNBoNFCr1UAIgVqtht1uH2gSFff4xBo8uVwOp9M5tNCc2Wxe1/CJijtryaU1m02pJKSf9+d2u6UWVbvBuGg0Gng8nlVlEWazWeoWsVtDoTzPo1AoIJPJSEo34t6xIAhIp9PIZDLQaDSQyWQ97S0PE6VSiUAggFAotG5GMGXzUMO3SdLpFeWGarWKRqMBi8WCeDwOj8ezKS9jkHGoVKqBV6aigojY822rESeafD4Pt9uNkZERxONxSdi6n0leEARUKhXU63XodDp4vd6hZySuJVotItZOdlswiNfW6XT2HeoWw43ZbHboSUabQexUbzabwbIs8vm8tBdms9lgsViGKpe3GViWRTablQQC2gUJCCEoFotIJpNQKpWQy+XQarUIBoM7Utah0WgwPj6OcDgMpVJJk122CFrOsAlEPcT2Syg2fd3uLLjHH38cTqdzYI8vlUqhWq0iGAxuuWfRLs3m8XikayUaCJ7nMT4+vuE4eJ5HLpdDNpsFsBI26rferh8WFxclL7KdVquFhYWFNb37dDotXdtBjHGj0UAoFMKhQ4d2dY3dbiuLEAvOy+WyVCfXvi/ZaDSQSCTAsiyUSiU4joPP59v2PpLdKJfLiMVim1Y6onRn52MnAxAM3oSf/Sy808OQVETaEQRh21e65XIZhJCesg67USwWUSgUejI2m4HjOESjUcRiMXi9Xvj9/o4FgkwmkxRtlpeX18xy4zgOqVQKx48fR6PRkEpDtnp1LJY1nIwon9bN6NVqNWSzWYyNjQ08+Wu1WimzdDcjlkX4/X4cPnwYer0eyWQSx48fRzqdRqvV2vhFNom4vxsKhbC0tASNRoOZmRn4fD7JgIhJVIuLi1AqleB5Hnq9HtPT07vC6AGQamfD4TA4jtvp4ew7dkcsYo/SzasTm1hu58qcYRhotdqBjJYoAxYMBrfMYBNCUCgUkEqlYLFYMD09veb1kclkUi+/aDQKv98vGYxWq9WRct6usjKIaku/mM1mhEIhqZMGsBLirtVqGB0dXXU8z/OIRqNDSY5wuVxYWFiAzWbbEzV1otC2zWaTdEIXFhag0+mksohhLrK6FZyfnHQj6rym02no9XpotVo0m80tb5s1KDabDSzLIhwO9703TFkfavh6gOcFKBSrv3TiKlKv10OlUkGpVG57SEfsBD02Ntb3czmOQzgchtfr3bIbv9lsSiolov7lRpxs/JxOJ7LZrKSyMj09vWryH7SIuB80Gg3kcjkajQZ0Op1UvrBW77xkMgm9Xg+LZfNNQ9VqNUZGRsAwzKrGtLsZMTNSDGsXi0Vks9mOsojNlPh0KzjvVvRfq9UQj8cBrCxgisUinE4n7Hb7rk3GAVYWPCzLYnl5GePj47t6rHuJPb2EEASC//iPhzA1dTPs9i/gta/9PnK5ldYuL3nJPfjCF/7ScfwZZ3wV3/72cQDAk09mccEFd8Nm+wIOH/6/uOuuJ6Xj3vSm+/Av/3I/Lrro/8Fg+Cx+8Ytl/PCHCzjrrNthNn8efv+N+NjHfguZTAaHwwG9Xo877jiOYPAm2O1fwMc//vuOcOx642w0OLzhDT+E3f4FjIxcj6c//WtIpao9X4NCoSAlGvSD2FvPYrFsSYG02HppcXERJpMJU1NTfRlXMSuzUqlgYWEBCoUCMzMz8Hq9a3ra2xFiFnv0ASsqOaL25cmUSiVUKhV4vd6hndvpdKJYLG5LyHArEMsiJicnMTk5CZlMhqWlJSwuLiKfz4Pn+Z5fS1xQzc3NodVqIRgMIhgMrsriZVkW0WgUkUhEukeazSampqbgcDh2vSGRyWQYHR2VlJRoSsZw2NOG7/rr/4Lvfncev/zl6xCPXwWrVYu3v/0BAMBll52CO+44YcyeeCKDcLiEl750EtVqCxdccA8uv/wI0ul/xZ13vgz/+q8P4IknTuyhfPObx3DNNc9CufwunHPOKAwGFW6//SUoFN6BH/7wlfjyl/+K7353Tnrtf/3Xn+Eb33gpEol/QbHYRCxW6WmcX/3q4ygWm1he/mdks2/HDTdcAJ2u9wk8m81Cr9f3HQZJJBJSHd2wqdVqWFhYQLVaHWiCqVarWFpawvLyMux2O7Ra7bqhzEEFqgdB3OcTu763hz1FWJZFPB7fUJ2lX5RKJWw2m5RJvJcRyyIOHz4Mh8OBUqmE2dlZxGIxqbylG6I4xOLiIuRyudR492SvURAEMAyD+fl5KBQKmM1mZLNZ2O12BIPBbSkxGhZiBKRer4NhmJ0ezr5gTxu+G274K6699hyMjZmg0SjxsY89B/fccxwcJ+CSS2bwyCNphMNFAMA3vnEMr3zlDDQaJX7wg0UEg2a8+c2nQamU46yz3HjVq2Zw993Hpdd+xSum8Q//MAq5XAatVonzzhvHaac5IZfLcPrpTlx22RH88pdRAMA99xzHxRdP4ZxzxqBWK/C///c/oH0uXG+cKpUc2WwD8/MFKBRynH22B2Zzb1lcPM+j2Wz2ncmZy+VQqVQ69s+GgZg0EIlE+u4eLyYlLC4uIhqNwmw249ChQ3C5XAgGg1IGXrcJUTR627F61+v1UhNck8m0yosV1VnEbMZh43A4UC6Xt6yj/XYjRisCgQBmZmagVqsRjUYxPz8vFZiLJQcLCwtYXl6GwWDA4cOHOzKC2ymXy5ifn5eaMZdKJfA8j5mZGVit1l3v5XVDoVAgEAggn88jn8/v9HD2PHt6jy8cLuGSS74HufzEF1mhkCGVqmJ01ISXvnQSd975JD7wgWfijjuexM03Xyg976GHEhgZuV56HscJuOKKE00k/f7OlPWHHkrggx/8FR57LINWi0ezyUudkOPxasfxer2qo6PyeuO84oqjWF4u43WvuxeFQhNveMNRXHvtOVCpNvYUGIaRVEV6pVarIZVKDV2OTNTXNBgMmJ6e7jnsKKpoMAwDQgicTucqiS6FQoFgMIhQKIRUKgW3293x+HZ5e8AJKa9isYjDh1d3ws7lcpsWElgPhUIBp9OJVCqFQCCwJefYKURBdIfDIWXDiu1/1Go1XC7XugXlokiAuBisVCrSnuh2dLnYalQqVUeB+354TzvFnjZ8fr8Jt976YvzDP6zOqANWwp3/63/9Ds97nh+NBofzzx+XnnfuuX7cf/9r1nztk2+uyy//Aa6++izcd9+roNUq8e53/xyZzMo+nddrwOxsDsDKRF6rtZDN1nse50c/+hx89KPPwdJSERdd9P9w+LANb33raRu+/0Kh0NeXX5RjGhsbG1ptkOj9NBoNjI6O9pwOLmbhifqiLpdrXZUV0fiJbWDaQ7TbkdgiQggBy7JSMlM7zWYT6XRa2r/aKmw2GzKZDGq12r4scOY4DuVyGdVqFUajEWq1GtVqVTJqIyMjHZEEnufBMAzy+TzsdjvMZrPU4269DOK9iFarxfj4OCKRyK7NRt0L7OlQ51VXnYFrrvm1FM5kmBq+97156fGLLppAOFzCRz7yW1x66RHJ43rZy6Zw/HgOX/va42BZHizL4+GHEzh2LLvmucrlFmw2LbRaJf74xwS++c1jAFZuumc+U4/vf38ed9zxOzzyyN/wrnf9EO0RufXG+YtfRPC3vzHgeQFmsxoqlQK9bNe1Wi1wHNfzHp0gCB3C2ZuFEIJcLof5+XloNJqea6AEQUA2m8Xc3BwKhQJ8Ph8mJyd7koZSKpUIBoMolUod+1zbldgCrITRBEEAx3EdyRiCIGB5eRlut3vLC47lcrnUrHY/JTs0Gg0pzCkIAqamphAIBOD1ejE9PY3x8XFwHIeFhQUsLS2hUCggn89jbm4OLMsiEAhIotPj4+Pwer37yuiJGAwG+Hw+hMPhPZvotNPsaY/vXe86G4QAF154D+LxClwuPS699Ahe8YqVPmcajRKvfOUMbr31MXziE+dIzzOZ1PjpT1+D97znF3jPex6EIBCccYYT1113/prn+tKXXoj3vvdBXH31Azj3XD9e+9rDyOcbqFQqGBtT4oMfPIL3vOcvqNd5vOUtM3C59NBoFBuOM5ms4qqr7kc0WobRqMallx7GFVds3EQ2lUpBqVT2tIcm7jtpNJqBi9zbaTabiMVifXWcaFdZ0el0HbJR/SAav1AoBLlcDofDsW0en6jH6fV6kcvlUC6XpYxYhmGgUqnWFbEeJlarFZlMBpVKZU+HvE7ucG6329c0WO1lEWKnCEEQYDKZoFKpEA6HpV55e3Efrx8sFktHjd9ukYfbK1DJsk0gCAIWFhYgl8tRr58IbdbrPJ797J/hL3+5FE95im9LCk+feOIJ2Gy2niS6xL5uk5OTmxqLIAjIZDLIZrM9d5zgOA6ZTEZqkDuIXmU3WJbF4uIiHA4HBEEAz/NbKlcGrFzHWq2GQCCAXC6HarUKv9+ParWK5eXlvvY2h4EYKp6amtpzE30vBefdEFV7SqUS3G43lEol4vE4OI6DRqOB3W6HxWLZl55eNxKJBOr1+rZIDe4n6DJhQMRw1/T0NHK5HH7yk2U8/elWEEJw881JHD1qhcHQxOzsLMxmMywWy9A0C6vVKgRB6Em0uFwuI5vNbtroVatVxONxqNXqnjpOtFotZDIZFItFWCyWoXepEBv8hkIhaDSaLcmgbIdlWalHIbBS1pBMJiUZNp/Pt+2rbrPZDIZhUCqVhlIkvx2cXHDeq6C4GFoXu9uL952Y7DQyMoJKpdLRLcJms+3qbhHDwOPxYHl5eZXKEWV9qMc3IKKi+8TEBNRqNd761h/jnntmIQgEZ57pwI03vghHjzrRarVQKpVQKBTAcRwsFgssFsumbsilpSWwLLtml2+RZrOJxcVFjI+PD2wYeJ5HMplEuVzuqU1Ls9kEwzBSCx+73b6lElvNZhPz8/OwWCwDqdf0SjQahVKp7PAqRW9frVZ3lSzbDsrlMhKJBGZmZnb1pHdyh3OHw9Gz51+pVJBIJKBUKuH1esHzPGKxmGQ4T/5+sSwr7f/JZDJYrVaMjIzs23CgIAhYWlqSOpNQNoYavg0QL0+3SSWbzSKTyUjGD1jxdNLpNCqVClwuV0fdULPZRLFYRLFYhCAIkmqK2P+rFwRBwLFjx+DxeGC329c8jud5LCwswOFwDNRmSCwzSCQSMJvNcLvd64aPxOLaarUqaTRu10SzuLiIZrMJr9e7JSo0YtH0zMxMxzVYXl5GuVzGkSNHdizMRAhBKBTCyMjIrmxe2t7h3Gaz9aU12mq1kEwmUa/X4fF4YDAYkEqlUC6X4fP5NlQrWsmwriGfz6NUKsFoNMJqtXaVNNvr8DyPxcVFWK3Woezj73eo4duASCQieWndyGQyyOVymJiY6Lih6/W6FAo7WT+QEIJGoyEZQVH2ymKxbJgRWCgUEI1GcfTo0TUnW0IIIpEIVCrVQLqOrVYLiUQCrVYLPp9vTW9RnFgYhkGj0YDD4YDVat32/RVRvDmVSsHr9Q417EcIweLiImw2W0fiCsuymJubg0wmw5EjR3Z0IhUN86FDh3bFPk+3DudWq7XnsbXvJdtsNkm6Lh6Pw2QywePx9P0d43kexWIR+XweLMtKHeX3koLLRrRaLSwuLg79HtiP7E/ff4hsVBwtJlcsLS11ZFfpdDoEg0GUy2WpyaXH45FCnGKGmtvtRq1WQ7FYxOLiItRqtWQEu62MM5nMhhJlqVRK6mnXD+37KHa7HX6/v+t5xEw8hmGkiW2rWxqth9hWJhAISHV+w+peXSgUAKDDkxT7BtrtdhSLRUm0eqfQ6/XQ6XTI5XI7utpv73CuUqk6Opz3gqjek0gkoNPppKSdaDSKRqMBv98/cMheoVBIHmd7twitVgur1Qqz2bwrFg2bQa1WS/eAUqnc8n3vvQw1fBvQiyqIy+UCIUQyfuLx4gRsMpmQz+cRDodhNBrhcrmklaZMJoPBYIDBYIDX60WlUpGy9bRaLSwWC8xmM5RKJQRBQKPRWFexo1AooFgs9p3p12g0EIvFIJPJOtr9tNOLyspOIH5GGo0GgUAA4XAYMpls02n+PM8jlUqtUsXPZrMQBAEulwuCIKBcLu94IbHb7UYoFNoRj3u9Due9IkrScRyH0dFRGAwG5PN5qdeh2KdxGIh7g6KcWT6fRyKRGEq3iJ1GLBWKRCI9lxodRKjh2wBBEHq64cRJcGlpCcFgsGPykclksNlssFgsyGQyUmjO4XCsOs5kMsFkMkEQBFQqFRQKBam9jVwuX3dCr9frSCQSffXWEwQBsTr2CwAAIABJREFU6XQa+Xwebre7q5ahIAjSSr4XlZXt5GSBar1eLylb+P3+TTUWZRgGRqOxYxJvNBpgGEZSZzGbzUgkElsmUdYr7c1qt0J4vBsndzgfJHNXFPsuFApSm6BWq4VQKNRXneggyOVyjIyMYGRkBK1WC/l8HktLS1I95l4tizAajfB4PAiHw5icnNwT/Ru3G7rHtwHHjh3DzMxMT4ZE7M8memVr3TTrJcB0g+d5lMtlqWjcbDZjZGQERqNRMsqiooXH4+k5vi/um4hFwSffIIIgIJfLIZPJQKvVwul0Qq/X7wqDJ8JxHObm5nDKKad0/L9SqUg9zAYJ+YgZse29/8S6TXHPClj5zJ988smhl2sMQqvVwsLCQs/f10HoVnBus9n6NhDtzYlNJpPU07DfOtFhI76/fD6PSqUCs9kMq9W66773vSDW77ZHoSgrUMO3DoQQPP744zj11FP72qeIxWKShNJ63mJ7Aozb7V7Xi2JZFrOzswgGg2i1WtLekslkgtlsRiaTgcFg6Gm1z3EckskkqtVqVwFfnueRzWaRzWZhMBjgdDp3PJS3Fo1GQ0rsOJlyuYxoNIpAINB36C0cDkOv13fUSiYSCbAsu6peKhqNQqfTrZtlu13E43HIZLKhp7UPWnDejVqthkQiAQDwer3Q6/Wo1WqIxWJSQtZOLyKAlftELIsghEgJMXulLEJciDebzQ3nooMGNXzrwPM8nnzySZx66sYSYu2IyQ9igsl6XzhxhXlyAszJxONxFIvFDs+GZVlpP5Dnealeaa3VqdjeRRTwdblcHSvBdpUVk8nUV63VTlGtVqVuE90olUqIxWJ9CfqKCRbT09PSZ1epVBCNRruqs4jZgsFgcFPvZRiwLIv5+fmheaAnF5w7HI6BhRhYlkUqlUKlUpGKzsVQe7FYlKIVu82zIoSgXq8jl8vtubIIMcNboVBgdHR01493u9gbS5cdQhCEgUIEMpkMY2NjWF5elsJta33hxD07o9G4ZgIMAEkBpR2VSgW5XA6lUolAICCFLsUaQYvFAq1WC5lMhlarhVgsBp7nEQgEOozAVqusbCUbJR+ZzWYQQhAOhxEMBjc05OIq2ePxSEZPLJgeHR3tuto3Go3Std3pkJJKpZKa1W6moF8sOC8WizCZTD1du7UQQ+YMw8BqtUr1kOVyGfF4vO9WVtuN2IpKr9dLZRHpdFrqu7ibyyLEJrahUAjpdHrb9n93O9TjW4dGo4Hl5eUNFVLWQlTsF798vay2eJ6XagPFBBhx7+bIkSMdk0O1WkUkEunIwiSEoNlsStmdMpkMKpUK9Xpd6nUmjqM9OUEsfN2tk89a5HI51Gq1DSd5MUloYmJi3VpJUfg5EAhAJpNJ3rtCoVi3JnJpaUlKiNhpeJ7H8ePH+04MEesys9nsQAXn3RC9Z7VaDa/XC41GA47jkEgkUKvV+mpltdsQyyIKhcKuL4vgOE7Stt2NQgfbDTV867BRGK0XBEFAOByGSqXqK9QgKq83Gg2pu/iRI0ekx8Vi1bGxsTUnjlqthmg0CkEQQAiBSqWSepkVCgVJZcVut++4pzIoYpi3F4FqMT1+LeMnJsq0LyQKhYIkBL3ehNYuWr0bYBgG9Xq9p1rO9oJznudht9v7KjjvhiiC0Gg04PV6pX1kcQEyMjIiJbTsdcSSlnw+j3q9DovFAqvVuuv2xZvNJkKhUE+qN/udvbW832Z6LWVYD7lcLhWVxuNx+Hy+noyfaKSSyaTU9+348eOS2HQkEoHD4ehq9MQU8fZ9E0KIJLHGcZxUYLwTdV/DhOO4nr1Uq9XaUW95cngqlUpJEnLAicm7F+V7k8kk9cfbDfsodrsdx48fX7dZ7ckF506nc9NlKoIggGEYqZheFEFoD7Xvtwaq7cpLYlmEuK8m7rvvhntMo9FgfHwc4XAYSqVyXzYx7hXq8a1DoVBAuVweyiqe53ksLS1Br9fDarUiEolgfHx83VBUqVTC8vJyh16oqPyylgfZvm8iSjuVy2XJM3I6nTCbzahWqygWiyiXy9Dr9VKh/G64QfshGo3CYDD01QdPXAC01zjV63WEw2Fp/0nUwDSZTD11wQBWpNPcbveuCd3lcjkpnb2dkwvOHQ7HpifB9sQp8bunUqmkBRfDMHA4HB2h9v3Mbi6LKJVKiMfjG4b99zPU41uHYSYrKBQKBINBLCwsIJfLSXVM64XoxIkDgCTJFY1GUS6XYTabwbKs5LWwLCuFlkTlCzHjUyaTSQZPvOn+f/a+NEzSsjz3rq+2r/Z976V6mRkYJCqggAiDUU8wghoFIQoqionBXbzOdVyO6DFoTqIeiejBoEIQDApoEDQmJ4ogyCIaZBlmeq193/eqbzs/mvedqu6q6qrq7ll66r6uubqnu+pbqqrf532e537u22g0wmg0QhRFlEolFItFxONx6PV6mEwmGAyGE6IMNcp7ZLPZaGAjMnNkCJ0cq522PygMBgPK5fJxE/jazWr1ej3q9Tqy2Sw10N0uEhMRThBFsUNWrF6vIxqNQi6X91QD2q1oF6MgYxGxWIyORZjN5mM2WG40GsHzPB1wP9H6+tuBk++Oh4AgCNu6+LdaLfA8T4MZKUX2AvlAsiwLv9+PSqWCer2OPXv2IBgMYnFxETabDQqFgjLmvF4v3dGR8Yh+tOt29Qqe51EqlZDL5RCNRqmP4PFM2x7Vfb1dY9VqtUIURZo11ut1ZDKZoWXfjEYjgsEg3G73cfF6kQ1PLBaDUqnc1OF8WPA8T0vq7ao/7WpAbrcbZrP5uHg9jhUUCgXsdjtsNhvq9Try+TwWFxdppeJYqCBZrdYOB/cTYZO7nRgHvj4YdZyhF0qlEkRRpGxBjuPQarV67rqbzSYYhsHc3ByazSai0Simp6fRarXAcRwtI0mSRAkqS0tLYFmWZn3DQKFQUCYfx3EolUpIpVKIRCK0h3E8lGraIQjCyDtWIjMXj8fpyIkoiohEIvB4PENnQySjaTabx3z+sX3gnOM46PX6bRtiXm8K264UQ0ZqWJbtUL0Zo3Mswu12U93bWCxGdUKPZlbsdDrBcdymI1e7EeMeXx9Eo1FoNJptpf+Sskcul0Or1YLFYulpYkp0KBmGoVJZJpMJCwsL4HmePs5kMlF7I6fTCZvNtq0fYqIUUygUIAgC9REkM4LHEsNIynVDIpFAqVQCwzCYmZmhzhaj9nXj8TjVMz0W6DZwLooikskk5ufnt/x+VatVxONxMAwDr9dLAzwxLK5UKtSweIzB0D4WoVarYbVaj9pYBGGdq1SqgYl3uwHjwNcHm3nxdUMgUMTMzC3guE9Aoej/wa3VatRVoBfqdQ6XXXY/fvObCP7bf/Pj//yfM6hVDoFKpcLU1BQ4jttUAWZUfP7zj2JpqYDvfOe1NAjKZLKOQfmjjVEk5dpB5iPn5uZo70sUxQ2Gs8OgUqkgmUxibm5upOePivUD5+2qO6SfSYatR0Gr1UIymUStVoPb7ab94mENi8fojWM1FiEIAlZXV2E0Go+52PrRwrjU2Qfb3eNbj0GYdPfcs4BUqoZs9kNQKBgEg8Guj1Or1WBZtkMBhmh3bqeqBMuyYFkWTqcT9XodxWKR+n+RIHi0VCwIsWXUXWoikYDNZoNKpYLdbkcul4NKpdrSrlen09FS9E6X+cjAeSaTQa1Wg9Vq7VpelMlkcLlctGQ9zGd6vSmsz+ejz+c4DrFYDK1Wa0teeWOsYf1YRKFQOCpjEXK5HNPT01hZWaHOFLsd48C3Djwv0kxtu3t8oyAYLGHvXgu9psnJSSwvL9PrkiSpYyHrZoFksVjgcDgGvpf216AX1vcryHjE8vIy1Go1/QPeScbYqMQWAJQoNDExAUmSEIvFYLVaIQgCgsHgyP0wmUwGvV6Pcrm8YwoZ3QbOe5kGE+h0OqjVauTz+YHEtNtNYVmW7WCADmpYPMboUKlUcDqdcDgcqFarVHzBYDDAYrGMrJfaC0qlEtPT01hdXYVSqTxumMk7hZPm0/qHPyTx8pffDoPhRlx22U9x+eX347OffQS//nUIExM343//7yfgdn8LV1/9C+TzDVx88Y/xilf8DD7fd3HxxT9GJFKmx7rwwrvwqU89jFe+8g4Yjf+IN7/5J8jl6h3nu/POg5ia+jbs9m/ihhse73ttL7yQxYUX3gWz+Rs47bRb8dOfLgEArr/+Ufyv//UYfvjDw9Drb8R3v/ssGIbBnj17XvSDs+LMM3+O6Wk/ZDIZ3v/+f4fT+U0Aa7u4T37y9/j5z8vgeR6PPvosLrroh7Bab8L8/Hdwyy3P0PN//vOP4tJL78OVV/4MRuM/4rbbnsPqagEHDtwFg+FGvP71dyOTOXJ/jQaPK6/8GWy2m2A2fwOvfOUdqFZl8Pl82LdvHxwOB2q1GhYWFrC6uop8Pk+H8LcToxJb1utx5vN5cBwHl8tF9ThDoRBEURzpuoxGI0ql0kjP7QciZ7ewsIBsNguHw4E9e/bAZrMNFHhcLhed5+yHZrOJYDCIZDIJr9eL6elpGvQajQZWV1dRKBQwMzMDp9M5Dno7CLKRmpycxN69e6HVahGPx7GwsIBUKgWO47btXCzLYmpqCuFwGPV6ffMnnMiQTgI0m7w0NXWz9PWvPyW1Wrx0772HJaXyq9JnPvMb6cEHg5Jc/hXpv//3X0uNBifVai0pk6lJ99xzWPr975+VcrmqdOml90lvfvNP6PEOHPgXyev9v9Kzz6akSqUpvfWt/yq9850PSJIkSaurBQn4B+maa34h1Wot6emnk5JK9TXp4MFM12trtXhpbu4W6YYbHpOaTV765S+Dkl7/denQoawkSZJ0/fWP0GN3w+TkzdJTT8UlSZKkvXu/I83M/BM91+TkzdIf/pCQJEmSzjvvDumd77xXeuaZg9JvfrMs2e03Sb/8ZZCeQ6H4qvSTnyxIgiBKtVpLOuecO6SPf/xXUqPBSQ89FJL0+q/T67j55qeliy++V6pWWxLPC9JTT8WlYrGx4doEQZAKhYIUCASk559/XgoEAlKhUJAEQRjq/euFYrEoBQKBoZ+XyWSklZUVSRRFqdFoSAcPHpTq9Tr9vSiKUjAYlAKBgCSK4tDH53leev755yWe54d+bje0Wi0pHo9LBw8elILBoFStVkc+VigUkpLJZNff8TwvxWIx6eDBg1I6ne64d0EQpGQyKR08eFDKZDIjvS5jbA9EUZSq1aoUiUSkgwcPSoFAQCoWi9v2nhQKBemFF16Qms3mthzveMRJsVV7/PEYeF7CRz5yBpRKOd761r145SuP+JUxjAxf+MJ5UKsV0GiUsNk0eOtb90CtlsFkYvGZz5yDhx4Kdxzzqqv24yUvcUCnU+GLXzwPP/rRYQjCkQzh+utfBY1GiZe+1ImXvtSBP/4x3ePa4qhUWvgf/+NsqFRy/OmfTuHii+fwL//ywqb3JUkSXv1qL372s0NYWVk7/qWX7sVDD4WxulpAqdTCS1/qRDhcwmOPJfCtb70Rfv8EXC4Bl146iVtvPZL1nXuuF295yx4wjAzpdB2/+10CX/zi2mtywQWTuOSSI2QNpZJBNtvA0lIBcjmDM890w2jcSNAhPYvp6Wns27cPRqMRuVwOhw4dQjgcpmSSUTGMXFn7c1KpFPWri0QicDgcHeQc4q4BoEM5Z1DI5XJoNBpUKpWhnrce9XodkUgES0tLkCQJc3NzmJqa2pLKitPpRC6X68j6JEmi4zVqtRp79uzpUFip1WpYXl5GvV7H3NzctrOGxxgOpM1AKizEj/Pw4cNIJBJoNptbOr7JZILdbkcwGNyRSs3xgJOixxeLVeHzdQ5hT04eMV91OLRg2SMvRa3G4WMf+xUeeGARlcovAQDlcguCIEIuZzY8f3raCI4TO8qBbveRRr9Wq0Cl0gIA6PU30p8fPHg1YrEKJicNYBhZx/Gi0Y2L5pe+9Di+9KUnAABvetMEPvvZU7FvnxIPPhiE16vDBRdM4MILJ/H97x8Eyypw/vk+MIwMsVgVViv7YnBSQ6/XY34+jXvvXUA4HH6Rvn/kfmKxCiwWFjrdEZLK9LQR4fBaufeqq/YjHC7jiivuR6HQxJVX7scNN7waSmXvfhtp0FssFvA8T1VlIpEIHZQftm8ximoLmT1jWRapVAoMw3TteTEMg8nJSYRCIUQiEUxMTAw9zF4ul4d2a5C6OJxv18A5ABrY2u+FfE+ISwSCICCZTKJUKtERhXHAO77AMAz9u2o2m8jn81hdXYVKpaJuIaOUou12Ox1wH0Sr9kTD7rqbHvB4dIhGKx07d7KIA8D6v+WvfvUpHD6cww9/+GqUSh/Bww9fAQBo3/i3Pz8UKkOpZGC3b047rlQ+Sv9NTRnh9eoRDpchikcOHgqV4PNtbC5/+tPnoFL5KMrlj+DTn94HURRx1lkW/P73eTz4YBAvf7kJZ55pw6OPRvHQQ2EcOLA2i+b16pDLNVAut168XxmyWQF79qx5/uVyeTQadbq783h0yOcbqFZbHfdIoFTKcf31r8LBg+/Fb3/7DjzwwDJuv/3gpvdOoFAoYLPZMDs7S0kTiUQChw8fpnY1g2RZw5JbGo0GisUinE4ntd/pF9AYhsHU1BR4nkc0Gh0q8yPyZYM+RxRF5PN5LC0tUbHsvXv3DkVKGhRyuXzThUx60Y5JFEXMz88flwaxY3RCrVbD7XZj3759sNvtKJVKOHz4MKLRKOr1+tCVC7fbDYVCgUgkMvRzj3ecFIHv3HO9kMtluOmm/wLPi7jvviU8+WS85+PL5RZYVg6zWY1cro4vfOG3Gx5zxx0HcfBgBrUah8997lFceulemg0Og7PP9kCrVeLv//5JcJyAX/86hPvvX8YVV5zS8zntpYzpaR1YVo777otg3z4l0ukwjEYZ7r77MGZn8aL0mIRzz/XgU596GI0Gj2eeSeO7330WV111GlwuF6xWKyRJoqSJqSkjzjrLjeuv/y1aLQGPPBLB/fcv03M++GAIzz6bhiCIMBpVUCrlGHVDqFKp4HA4MD8/T3eWkUgEi4uLSCaTaDQaPZ87DLlFepHQ4nQ6IZPJEIlE4PV6Nx05IMGPODUMugCoVCoolUrUarW+jyOl14WFBRSLRXg8HszNzW3ZFmirkMlkmJqawsTExEmp5XgiQyaTwWg0Ynp6mo63hEIhLC8vI5vNdohfbHaciYkJ8DyPRCKxw1d9dHFSBD6VSo4f//jN+O53n4XZ/A3cccdBXHzxHNTq7jvpj33sTNTrPM455xc455wf4KKLZjY85qqr9uM97/kF3O7/i0aDxz/+45+OfG333/8X+Ld/W4Xd/k1ce+1/4vbb/xynnNKbcr6+hn/WWRbYbBq89rVnYv/+/Xjd69b8A1/6UjsqlQoikQg+//m9ePbZKNzub+KSS+7BJz/5Jzj//DWdULmcgVarhd/vR6lUwuLiIr797QN44ok4rNab8IUvPIZ3vWs/PV8iUcWll/4URuM/4tRTb8WBAxO46qrTRrr/drAsC5fLhT179mBycpJqaS4uLiKdTqPVanU8fphSZ6lUAs/zsFqtSCQS1JFiEJA5p3q9jkQiMXDwMxgMPdmdzWYTsVgMCwsLaLVa8Pv98Pv9O6qLuv66/+7vnsDc3C0wGG7E/v3fw09+stjx+27XIUnSlvqyYxxdKJVKOJ1O7N27F263m7Ktw+EwKpXKpp9lsvEj5ffdgpNWueXss+/ABz7wUlx99eldf18ul5HNZuH3+zf87sIL78KVV+7HNdf8yQ5fZW9kMhnq/yaXyyEIAlWEdzqdG5RUiAB1tVpFo9EAx3F0ASPqMRqNhg4hJ5NJyOVyuN3uY+rbJb04pF0sFlEsFqFSqWA2m2E0GhEKheDxeDa9PlEUsbi4CJ/PR7U55+fnhy4hEoULvV4Pl8u1aYCq1+sIh8O0pyZ1GTjfqsP5ICAbhvXCAnfffRjnneeD263D3Xcfxnvf+wssLV0Dj6e7x2Mul0Mmk4HJZOrrRj/G8Q3SYydjRqRH2O9zSIyvPR7P0H3r4xEnTQ3joYfC2LfPCrtdgzvvPIhnnsl0zeQIttOSqB8qlQq0Wu3QZS2iwZhKpegins1mkcvlsLS0BLlcDrPZDIfDAYVC0SFATSCKImq1GiqVCg0u2WyW/l4ul2N1dRUsyw4UYHYCMpkMOp0OOp0OHo8HlUoFxWKRBv1KpQK1Wt33vcpkMtBoNFCr1VheXsbk5ORI7y2xllpdXaVqKP3AsixEUUSz2USz2Rxq4Hw70G4Ke8opG0vnl122j35/+eWn4MtffgJPPpnAm988T38uSRJCoRBlqEov6sfyPA+GYSCTyca9vxMMpMdutVqpTujS0hI0Gg11i1j/2VSpVNRQW6VS4ayz7sI3v/laXHjh1DG6i63hpAl8hw/n8Pa3349qlcPsrAn33HNJ150twXa4r2+GVquFcDhMDSGHVYpxOp0dvl4OhwMOhwOtVovawmSzWSrJZTabO+6JYRjo9foNKg2tVgulUgm1Wg31eh31eh0rKysA1hZ/lmWh0WhgMBig0WiOWi+q3eNMFEW88MILqNfrOHz4MHQ6HTXTbb+eVquFbDaL2dlZRKNRqnoxKhQKBWZmZrC6ugqGYfqa1IqiCJVKhZWVFbAsuy0O54NAelHZhZR0e+mG3n778/ja155CILBWjq1UWh3MZIJqtdpREiuVSqhUKhBFkTrOkyDIMMxA3w/z2PYAOw6y2wdibK3RaOB2u+nGNxaL0SywXUdYo9FgenoaarUazz9/9TG88q3jpC11bgaicNHPL28rEEURq6urdGamXC6jXq9vu0hstVpFOp2mi5dOp4PD4Rhakoj0pGq1GhQKBQRBoKVShmGgUqmoVqher99xQoTUJlDdbqZbq9VgMBioj2A0GoVKpYJCoUChUHhR8WbriyfHcVhdXYXVat1gVtvucK5WqyEIAvbs2bPlcw6CRqOBWCxGXTQEQUC5XN4wwhAMFrF37/fwy19e9iL5i8HLXvbP+NCHXr6hhE/Ez0ulEiRJgtPppJ9TMgMoiiINhO1ft+t7cp6dCqrdjnGygoxFFAoFOhZhNBqPuXzjduKkyfiGxU6XOhOJBJRKJZ0hK5VKO+LFRcqE7f5sgUAADMPAYDAMLGKtVqsxMzODRqOBeDwOjuPgdruhVCpRqVRQrVapXifJAhQKBViWhVarhcFg2FYHh3aB6m4zgplMhtKw3W43UqnUtgU9YI000F72JCaj6x3OFQoFDh06NNKw/TBoNBpIJBKoVqu0n5jNZmnlguf5jh5OtcpBJlubYQWAW299Fs891528oFQqMTk5Sc+xftifBIudxqhBVRAE2tMeNEi339dOBNX1mezxBDIW4XK5qFsEkfgjZsN+/z/hO9/5MxiNKlx77X9iYSEPjUaBd77zVHzta6851rewKcaBrwdEUdwx0kGxWES5XKb+aKQ0tZNWNu2DroIgIJ1Oo1AoYGFhAQqFggpZb7aAsSyLmZkZlMtlJBIJSoBpL/nxPI9KpYJKpYJGo4F0Oo1kMglgrVSqUqmg0WhodjjKotkrkLT3L5aWlqBWq5FIJCCTyZDL5WAymaDRaLZlsVGpVPD7/VhZWaFBZv3AuSRJ0Gg0iEQi4DgOTqdzW8gBHMfRzUa5XKYzjXK5nNLVyVymJEkbPsv799tx3XVn4dxzfwCGkeFd79qP887r7gtJwLJsV7LX0QLZ5Ow0SHY5SnZKDKKHyWSPp1IxWYv0ej3kcjmMRiOMRiM4juv6/I9+9Ff46EfPwFVXnYZKpdVz83S8YRz4emCnMj5SMpyenqbHr9VqUCqVR83OhwQrt9uNZrOJVCqFbDaLdDpNzUs3M8I0GAzUAikUCnVYICkUCpjNZpjNZvp4URRRr9dpSbdUKiGXywFYW9CUSiVYloVOp4PBYNj0tdjs/cnn8zTIEhZmqVRCNBqFKIodZrqjQBRFFAoFZLNZGmzcbncHeYgIOrcvcKOi1WrRQFer1SAIAtRqNRqNBl3glEoldDpdB0EJAKxWK1WfaccNN5yPG244f+Rr2q1oX+B3OtBupVRM2g3bUSpu/0pGcNRqNbRaLVQqFRiG6eo2olTKsbRUQCZTg92uxTnnnBhs33Hg64GdCHyiKCIcDsPpdHYwJEulEgwGQ59n7hzUajV1Gy+Xy1RGjLApXS5XTyPMdgukbDbb1wKJYRhadm1Hq9XqyA6JFQ6wlr2pVCpaKm0n0vRTbREEAalUCk6nk7JeSdnV4XBQBZdgMNjhgTZIqXm9w7nH44FOp0Oz2UQgEIBcLodWq0U0GoXX64VCoaBzlzKZbKBzSJJEA12tVkO1WoUoitDpdNBqtVCr1chms3TDZLFYwLIsLUsplUqq2i+TyZDJZI7apmqM4XAsSsUkaPI83/GV/CMgbGTyd94N3/3un+Fzn3sUp5xyK2ZmjLj++lfh4ouPrgnzKBiTW3pgeXl52yn8sVgMPM9jcnKS7iiJYsrU1NSOOy0PClEU6eLearVoycPlcvXtU3Ech1QqhVKpBKfTObL6iCiKqFar1DOv2WzSP0iS2ZDd6dTU1IZrisfjEAQB1WqVakx2Q7cZQRIE15cGicN5oVCA0WjscDgnIBkeWWB8Ph+MRiOWl5fpLN3+/fs3vCaSJKHZbNIgV61WARzpz2o0Gsq0JQQTnU4Ht9tNjWJrtVoHRT0QCNDjy2QyzM7OguM4lMtl+Hz9S5pjHL/olvG1fx30ZyTYkvI4+Z5hGBQKBXo+mUwGi8WyYW6T9Phe97ppAIAoSvjxjxdw5ZU/Rzb7wQ6d3+MR44yvB7Z7nGF9X4+AZAPbSfzYKhiGgd1uh91uB8/ztB9Isgmr1drVA06pVMLn88FmsyGRSCCbzcLtdg9N4SfEm/VZMMkI27OgQ4cOUSKNWq2GSqVCoVCATqeDXq/vGfSA3jOCpORrNBqhVCqRz+fpwPmePXt69n7J60HYroVCARalQSbTAAAgAElEQVSLBbOzs1hYWKCPkSQJjUajI9CRjJiUZeVyOSULESIUz/PQarXwer2o1+u0bGu32zvmAsnMIvlsqdVqpFIplMtlyGSyDYGP7H2PN5LFbgMJWoMGp16PIb3O9QGr/atKper7mH5rG3FNMZvNm25277jjIP7sz/xwOLQwm9fWsHbB/eMV48DXA9tZ6iR9Pb/fv+GYpVLpuFa9VygU8Hg88Hg8qNfrSKVSSKVSSCaT0Gg0cDqdGwIUIUEQAkwmk9kWBZh294B4PE7LfOQ8zWaTDlqXy2UwDINGo0HHLLoN5hK0zwiSUmkymYQoilCr1XC5XDCZTH0/E4lEoqNURIKzXC6H1+tFoVBAMBikJUqNRgOj0Qi32w2VSgVRFOm9VCoVaDQaaLVasCwLnufh9XrBcRwCgQCVolq/qWg2mwiFQvS5+XwezWaTap52K7XGYjHk83nMzs4eU5We4xlkcH8rAYtspntlWuQrqWj0esxOrxVutxsajWagzfgvfrGKT3ziQdRqPKanjbjrrouh0eysEtF2YFzq7IHnn38ep5566pazPlEUsbKyAovF0tX+ZmlpifaJTiSQkYF6vd5XKk2SJBQKBSSTyQ4CzFYRiUSg0+lgsVjAcRwOHz5MGbIymQwejwc8z6NWq6HZbHb0vJRKJW3cm0wmej2CINChf6VSCbvdDq1WSzPBarUKvV4Ps9nclY1KlGTy+TwlCKhUKvA8D5VKBZvNRklD7aVu4hRBVHyMRiN0Oh3y+Tzy+TysVisl0+h0Onpd3d6TWCzWITy+urraocxvNBoxNdWptnH48GFwHAeFQoE9e/bsqnktAEOXBrs9hhBD2gNQv4yr12OO1w3uyYZxxtcFpFS1HR/SeDwOtVrdtTncarXAcdwJucsmvTDSYyKyRwqFAiaTiUqlkR6B0WjclAAzDNozckEQwDAMfd8kSer6mq/XK61Wq3TMggRNEqCsVisNbIShSp6fzWYRjUZhMBhgNpuh0Wjo8arVKur1OpVR0+v1sFqtPctFRD1jYmKCZoeFQgGrq6tUOzWXy9G5wG6bBkmSkEwmUSwW4ff7aa+Y9PYCgQAVMFj/fI7jOsYfotHohsB4rECYiINkU/1+R/Rs+wUlhULRN2CNg9buwjjwdQEpSWz1g14oFFCtVjE3N9f1WITNeSL/QTEMQ5U81kulqdVq2Gw2mM1myOVySnghNjxbIcC0z/GRHTmBxWLpShRar1dar9eRTqdRLpdpCZBYsCQSCVp2ImMWRqMRVqsVZrMZpVKJjnKQgGswGGC1WukM1DCvIbAWqKLRKFXHqdfrsNls8Pl8PY/H8zzC4TBkMhkdmG9HrVZDo9HA/Pw8stnsBsUeQpZp/3+9Xt8y0WozEsagGRd5ffoFJZLB93rM8TgkPsaxxTjwdcF29PeazSbi8XjXvh5BqVTaIHd1IkOlUtFZsWq1ilQqhVgshlgsBp1OB6fTCZ1Ot4EA43K5YDQa0Wg0EI1GMTMzs+nrT3bxwFrmTBbvycnJjgHx9SSlbg7n3QJLu14pYX7GYjH6e4ZhoFar4XA4aLAgM4pmsxkmk6kjixgEpES7srICm822qXt2rVZDKBSCxWKhPoPtEEUR0WgUbrcbarW6q6MCIfiQuUCHwwGVSjVwNtXvKyFh9AtchITRL9MaY4ztxjjwdcFWA58oigiFQn1n4HieR6PRGFoz80SBTqfDzMxMx6A3EXYmUmnrCTA8z4PjOMRiMTpb2Avt8mMsy0Iul1MCCkE2m0UqlcK+fWsuBOQ6iMRYr8AiCAIajQa9HkEQoNVqodFo8NxzeXz8479FIFDBeefZIZMBU1NafOQje3HPPWF873urKBY5nHGGBX//9+fgzDPncf31jyKXa+Ab33gtOE6A2fwNXHvty/AP/3Ah6nUOFstNiMU+gFKphZmZW3Dzza/HF75wFyQJuO66s/DJT74CiUQVs7O3IBz+a1itLHK5HH7960X89V8/hXj8bzp6hiTwZDIZWrkoFAp9+1c8z6PZbFJGXzsJo1+m1e8x4yxrjOMV48DXBVsdZYjH42BZFhaLpedjyuXyyHJdJxKI4oPVat0glUZYmbOzswiFQpSAQubV4nEel19+P5aXC7jhhvPxkY+c0XFcArVajb1793b8rFqtUtfoUChE2Z2ESNS+KBMSDOnRtVot2l8jDDeGYdBqCbjmmp/iE584C9de+zLcf/8yrrjiAbznPX488UQWN964gG9/+yzMzxvwla8cwsc//hQeeWQvDhyYxEc/+isAwO9+l4DbrcPDD0cAAI89Fse+fVZYrRqUSmuzfg8+GMIzz7wTy8t5vPGN92NuToMLLvDgVa9y4ZZbnsRb37pG3PnZzxL48z/3IhBY6coc5HkeLMtSFZv2oKRUKjv+32q1kMlkMDs7O+5njbHrMQ58XbCVjG+zvh5BqVTaFYaOw2C9VFoymUQ6nUYqlep4nCRJCIfD+MpXgnjNa6bw9NPv7nvc9ZuHVquFYDBIy5+VSqWDqs9xXEegIwQjnU4Hr9cLlmW7bkgefzwGnpfwkY+cAY7jcMEFVpx++pos289/HscVV8zh4otfDo1Gg5tv3gub7VsIBIo491wPFhfzyGbrePjhCN73vtPxrW89jUqlhYceCuPAgU4pseuvPxeFQgoOB4NLL53GD394GGefbcXb3jaNW255AZddNgGLxYqf/exB/OhHb8DkpLcj8wKA1dVVOByOrkzibmBZFvF4fBz0xjgpMA58XTBq4CPOBZv1qIiqyHrtxJMJarWasgdzuVxH/wxY6z2trhZw5ZXDv0YLC0sAxI6fEZugarUKnufp4DphZQ6y2AeDBbhca2a2HMfBaDRidtYKh8OGcDiJl7zER4Or0cjCZmMRjVbg95tw1lluPPRQGA8/HMFnPnMOnn46hUcfjeKhh8L48IfP6DjP1JQROt1a7/f004t44IFl6PV6nHuuEZ/6VBUymQ2//30eZjOLAwdmN1xnNpuFJEk9Zaa6gQRNjuPG8mZj7Hrs7jrbiBil1El0OF0u16aDn2Rea7fNS42Kdjkygquvfhy/+U0cH/rQL6HX34g//jGFd73r53A4vonp6W/jb//2MYjiWkZ3223P4bzzfoCPf/xB2Gw34bbbYhsCWa1Wo7qkp556Kqanp+k8XL+g12g0kEqlsLS0BEkqIR6vweVy4ZRTToHP50M8vjbH6PXqEQyW6POq1Ray2QZ8vrUe7oEDE/jVr0L4r/9K4RWvcOPAgUn8+78H8OSTCVxwQWdwD4fL9PtgsASLRY54PI69e2dx+eWn4M47X8D3v38QV121f8P1Ematz+cbOnNrV3sZY4zdjHHg64JRMr5YLLZpX4/gWIpSH49gWRZerxd+vx/79u3Daaedhsceew9e/WofbrrptahUPoqvfvUpFItNrKy8Hw89dAVuv/0gbr31OXqMJ56IY3bWhGTyWnzuc+dtkOEiA+mbZXdETiyZTGJxcRGBQACCIMDj8eDyy8+GUqnAbbctQhAk3HffEp58ck1Q+y//8lTceutzePrpFJpNHp/+9CM4+2w3/P61cvaBA5O4/faD2L/fBpVKjgsvnMR3vvMsZmZM1BOP4ItffBy1Goc//jGJ733vGVx0kQdzc3PQarV417tOw223PYef/nRpQ+CTJAnxeBw2m20kCTyWZanCyxhj7GaMS51dQKjdgyKfz6Nerw9kdCqKIiqVyo45u5+ISKVSqNfrMBqNsNlsUCgUL/aa1n4vCCLuuusQnn76XTAYVDAYVLjuurPw/e8/j/e973QAgNerpyVDo1GD0047jfbyKpVK3wyeBLtisUjn2oxGI3w+34ZA+eMfvxnXXPPv+NSnfoM3vGEGF188B7Vajte9bhpf/OJ5eNvb7kM+38QrXuHA17/+Slo9eNWrvKjXeZrd7d9vA8vKN2R7wFp2ODd3C3hewN/8zX5cddXZ9BrOO88HhpHhjDNcmJ7u7BGXSiW0Wq1NGbG9oFarUavVRnruGGOcSBgHvi4YJuMjrtSDzJ4BR9Q0dsrk9kQBoc+TcQHgCJsTQIeEWyZTB8eJmJ4+Ijg9PW1ENFqh/5+c7MygZTIZVCoVVCpVhy8ggSRJ1BewWCxCJpPBaDRicnISLMv23MCcdZa7g2xz9tl34JJL1vpsH/jAy3D11aciFotBEAR4vV4acPV6FTjuEx3Xl0p9sOs53vIWH84//zXU3WE9JicNeMc7Tu34Gc/ziMfjmJqaGpkpTBigY4yx2zEOfF0waI9PEASEw2G43e6BS0v9PNk+//lHsbRUwB13vHGo6z3WEEURrVaLSrC1Wi3wPN/h89VujNkOIhXWDrlc3lEKtts1UCoZBIMl7N+/RvoIhUq0f0aOsxmILiYJsDKZDCaTCVNTU32DXTseeiiMffussNs1uPPOg3jmmQwuumhtXjGdTiOXy1E25bA9NvI6ZLNZzM7Odv2c/O53cfzhDyncd99fdPw8kUjAZDJtSf6O9Pi2apo7xhjHO8aBrwsGyfhIP0Wj0QzU1zuR0J6NkWDWblrZHsTWBy0yQ9Y+L6bRaKBUKjusg9qp94FAgLoq6HQ6qNXqF50N1o4tlzN4+9v34TOfeQS33/7nyOXq+NrXfo9PfvKsTe9FkiRUq1Ua7ORyOUwmE6anp6FWq4de4A8fzuHtb78f1SqH2VkT7rnnEuj1EhYXF6HRaDA/Pz9SNr/2eVqbO1zT5Nz4p/nud/8c//qvS7jxxj+FwXCEeVkul1GtVjE/Pz/0edsxZnaOcbJgHPi6YJDAVygUUK/XMTe3/W7DgiAgl8vBarVumfkpiiINYCSItQeyzbKxdu8vuVwOjUYDhUIBpVJJNRJVKtWWBvF1Oh0NfO3+dMCRwPqNb7wWH/7wLzE7ewtYVoH3v/90vPe9p3c9Hgl2pGenVCphNBoxMzMzVO+2G/7qr16Kv/qrlwJYmweMx+OIxWLwer0jE5aI9Ni+fQ6I4nU9g/E///Ofb/iZKIr0/NvBEiZZ3zjwjbGbMbYl6oJDhw5hdna25x8/cdqemZkBy7L4u797Arfc8gxSqRomJw244Ybz8Rd/sQe33fYcvvOdZ3HOOR5897vPwmxW41vfeh3e8Ia1ntDqagHvec8v8Ic/JHHOOV7s3WtBJlPB5z+/D4IgwO/3b5A0W5+NtctqDZuNES1JlUrVMxvbaRSLRaRSqQ4avUqlwvz8/EjXIAgCFhcXabAzGo1bDnbrIUkScrkcUqkUrFYrHA7HSNcqSRIV9Z6amhqpTBmPx8Hz/MiElm7HUygUcDgc23K8McY4HjHO+LqA2MN0gyAICIVCHX29uTkzfvObv4TbrcPddx/GlVf+DEtL1wBYo9m/+92nIZP5IP7pn57B+97374hGPwCZTIZ3vONnOPdcL/7jPy7Fb38bwcUX/wSveY2Tkj3C4TC9nn7ZGMMwUCgUYFmWlhRVKtW2ZGM7gVarhUQigXK5DEmSoNFoYDAYUC6XoVAoYDQasbKygkaj0TX494IoiggGg7BarXA6nTty7cT5XC6X043PKOB5HpFIBKIoYn5+vq/LdS8Q8eytljjbMWZ2jnEyYBz41oGI/HYLFpIkIRaLQavVdvT1LrtsH/3+8stPwZe//ASefHKtXzM9bcT73/8nAIB3v/s0XHvtfyKZrKHVEvC73yXwn/95GdRqBS68cBoHDnQu1iqVCnq9HkqlkjIUCdX/RIMoishms8jlctT01OFwwG63g2EYKpDM8zwymQyAI84BAKjaf7d7F0WR/s7n8yEUCoHnebjd7m17rQRBQDKZRKlUgsvlgtlsHpkAUq/XEQqFYDKZ4HK5RjpOu/PCKEGzF1h2TQB7jDF2M8aBbx1If6/bYpTP59FoNDb09W6//Xl87WtPIRBYo+JXKi1kMnXI5TK43Udo+VqtsuP3FgsLnW6tnCqTyXD66T6srOSgUCjA8zz0ej1cLtdO3epRQaVSQSqVQq1Wo07tfr9/Q/lRrVbD4/EgHl8bCJfJZHA4HJDJZKjVaggGg3A4HNDr9R3PlSQJ0WgURqMRJpMJarUas7OziEQiCAQCmJqa2lJgkCQJxWIRiUQCBoNh5OyMIJfLIZlMwuv1bkmrNZPJQKlUbrve65jZOcbJgHHgW4de2V69XkcymaTq9QTBYBHvf/9/4Je/vAznnuuFXM7gZS/75xdLk70XDo9Hh3y+gWq1RYNfJFKFWq3Gvn37qBnpiQie56kjuCiKYFkWExMTXefpCCRJAsdxYBiGlnXlcjmCwSDK5TIt2SoUCuRyOTSbTXi9XhqYisUieJ6nhKCpqSmkUiksLy9jampqJGPVZrNJZ/JG7cERiKKIeDyOWq3Wc1RhmOvKZrObCqGPAkJiGjM7x9jNODFX1h1EN0YnmdfzeDwbFqxqlYNMBio7deutz+K55zKbnmd6ek24+Prrf4svfel8/Pa3Ydx//zLe9Ka5jhLfiQLiu5fJZNBqtSCXy6lB6mZsQ0EQEIlEIAgCHA4HMpkMBEFApVKBRqNBrVbrILu0z/619z4TiQRqtRp8Ph8YhqG6qYFAAB6Pp2/gXX8vmUwG2Wx25Jm8drRaLYRCIahUKszOzm6JfUkyXKfTuWOBiUiXjQPfGLsV48C3DusDH+nrESX/9di/347rrjsL5577AzCMDO96136cd55voHP94AdvxLvf/W+wWm/CGWdY8cY3ulEqlRAMBmEwGIZS1z9WIJkwGUfQ6/WYnJwcOMOq1+sIBoMAjrz2brcbBoMBCoUCoVCI9gG7gZTkCIu1WCzCYDDQ94qUP4PBIBqNxqY9tUqlQnVX1+bptrb4l8tlRCKRbQmgwFqpdFjnhWExFqseY7djPM6wDqRs1m6Zk8vlNpQ4e2FUZ4cXXnihI3shPS6LxXLcyZvxPE8NZQVBgEqlgt1uh9lsHujeeZ5HqVRCLpdDo9GAWq2Gw+GAwWDo2HQ0Gg0EAoENJrO5XA71eh0+nw8cx+Hw4cP0d71YoDzPIxwOQyaTYXJyckPWxXEczRg9Hk9XqbBhIEkSVXKZnJzclgye4zgsLS1tiU06CHK5HGq12kltmzXG7sY441uH9sDVq6+3HoSRmM/n0Ww2MT8/P9TCxDAMLBZLB5vO6XSi1WphcXERer0eVqt1g3P40QYpZTYaDTAMQ1mJg/QiOY6jupjEDZ0IKvciaKRSKdhstp4MW2CtJ2W322EymbCysoJKpdI18CkUCvj9fiQSCdr3Y1m2YybPYrFgz549W2aCtpdu5+bmtmXjQioPozovDIMxs3OM3Y5x4FsHUm7r19cjkCQJCwsL4Hm+YyFuf3y/0QhBECBJEpRKJdxuN8rlMnUDz+Vy0Gg08Pv9qNfrSCQSEEURFosFFovlqBFfiFM6mbnTarUDz9a1Wi0qFdZsNmEwGGCz2dBoNFAoFDAzM9OzJNpoNHpmHe3Bn2EY6nRhtVqRzWbhdDq7vuYymQwejwcsy1KHciJQvV1ZFBlVMBqNcLvd27ZR2arzwjAYMzvH2O0YB751EAQBDMMgGo1Cr9f3JUQQRf/23fH6Pk4ul4PZbN4QqGQyGXK5HCqVCmZmZsAwDKamphAKhajCfjabRTAYpNqSHMchl8thYWGB9gA3M1IdBYTckcvlwPM8lEolnE5nz+yrHc1mkwa7VqsFg8EAh8NBS33RaBStVmtTh4pUKtW3t9cNLpeLzgra7faejzMajSiXy3REYXJyclvm/fL5PBKJxJZHFdZjO5wXhsGY2TnGbsc48K1Du7blID0Okt2R4NM+2C6KIlKpVM/g6XA4UKlUkMlk4HA4oNFosHfvXnos0uNLp9NYWlqCzWaD1+uFx+NBPp9HLBaj5+wWXIdFuVym3ngkqLtcrk0Xv2azSXUxOY6jz2svzfI8j1AoBIVCQQN9L/TL9vqBlF/T6XTXwCdJEkqlEuLxOJ3Ji0ajiEQi8Pl8I7MtyahCtVrdkf7bdjgvDIsxs3OM3Yxx4FuHZrNJle43211nMhlkMhnMzs4imUzSsiUBoeP3CkgymQwTExNYXl6GXq/v6g6uUCjg8XhgtVqRSqWwsLBAsy+bzYZarUZ7VEajEVardVOX8Xa0Wi1ayiQzd/36bsBaACGZXbFYhCAItLTXrQ/ZaDQQDAZhNpvhdDo3vbZBsr1enCyPx4NDhw6hWCx23EOz2UQ8HgfHcR1kk5mZGcTjcaysrGBqamro+bpWq4VwOAylUom5ubltEYpuR6VS2RbnhWExZnaOsZsxDnxtEAQB1WoVNput7wJIxIWLxSIVs56ent6wGJdKpU3ZgSqVCl6vF+FwuO/CqVarMTk5iVqthkQigWw2C7fbDb1eD51OB57nUSgUEIlEIJPJYLVaYTabux5PFEXkcjlks1kqH0b0LfuNDTQaDVrGFAQBJpMJXq+3b7mV0PndbvdA9k2DZHv9AqdCoYBer6dZUvtMnt1uh91u39Aj9Hq9yOVyWFlZweTk5MDaoJVKBZFIBDabbcNxtwNElmy7nBeGAcuyqFarR/WcY4xxtDAOfC+CDAbL5fK+Cx/x4SMKHCSbI6LR7Y8rl8sDiSWbTCbac/L5+s8AarVazMzM0McrFAq43W5oNBrY7XbYbDZUq1UqjWUymWgWWK1WkUqlUK1WIZPJoNfrMT093bM0R4IdKWNKkgSTyQSfzzdQVpnNZpFOpzE1NTUwnX+U3t56eL1eLCws0D6lWq3uO5Mnk8koWzIcDtPXsdf9tY8qTExMDBwoh0UymYRWqx3Z7mgrUKvVyGazR/28Y4xxNDAOfC8im81SxZFeu2uyA+c4DjMzM3134dVqlYpLDwKPx4OlpaUNJbpuIP03g8GAfD6PYDAInU5H+3F6vR56vZ6SYVZXVyGKIoAjGWavmTtJklCv12mwI+eanJwc2KWcbA6q1Wpfe6f1GLW3tx7EdimRSGBqamrgmTydTofZ2VmEQiE6J7j+NSKjCjzPb9uoQjfshPPCMBgzO8fYzRgHPqwtMul0GnNzc1hdXe1ZHgyFQpDJZPD7/ZtmJIOUOdshl8sxOTmJYDAIrVY70ILaXtLMZDJYXl6G2WyG3W6npJlms0mzO2CNbt9oNNBsNukogSRJqNVqtGcnl8thNBpHciknYyAAhpbnGibb69bjkyQJ+XweyWQSer0exWJxaHIGkRWLRqO070eO0Wg0EAqFqDrNTjEsd8p5YRgQv8ZWq7XtfoZjjHGscdIHPrJQe71eqFSqrlqdPM8jGAxCrVbD5/NtGghImXN6enqoa9FqtbDZbIhEIvD7/QMHHIZh4HQ6odVqEY1GaYmKlEXby4wcx9EskSxujUaDGrf6/f6RWYmtVotmnx6PZ6iAOUy21+249XodsVisYyaP/Gx2dnao+2AYBhMTE8hkMrTvR9zWh9H8HBU75bwwLEjWNw58Y+w2nNSBT5IkRCIRamnTzYuP4zgEAgHo9fqBB5IbjQZkMtlIC8b6EYfNQEYmCoUCnbmz2+1oNptoNBpotVqUfEJ6dhzHUQ87juMgSRJ0Oh2MRuPIQa9arSIcDlNNymExam9PEAR6/y6XCxaLhb5HHo8HwWAQPM8PnTkRyTi1Wo1AIACGYXZcKgzYWeeFYTFmdo6xW3FSB75sNgue56kaBgl6ZMFpNpsIBAKwWCzUG24QkDLnKAtXtxGHXudIp9Oo1+tgGAYGg2HDzF21WkUikUAqlYJKpaJzWSaTCfPz8/SxrVYL+XwegUAAKpUKVqsVRqNx4CBEBrcnJiZGImKM0ttrn8nT6XTYs2fPhuBGhK5jsRjVXh0GHMchnU5Dq9WC4zhks1l4PJ4dK3EeDeeFYcCyLBUfH2OM3YSTNvC19/XIQtZe5iSuAU6nc2gl/FKptCk7sx9UKhU8Hg/C4XDHPCGZuSMMS41Gg+np6Q3BRhRFVCoVFItFNJtNWs4kZq/rg6lKpYLL5YLT6aTi0fF4nMqj9ZNsI9nWVrKhYbM9nudRr9epyEA/xqjT6UQsFhtaPHz9qIIoittmbtsLR8N5YRiMmZ1j7FaclIGPKPX7fL6OnTUJfNVqFaFQaCTpqWazCUEQRjI+bYfZbEa5XEY8HodKpUIul6Mzdw6HY0OgEEUR5XIZpVIJ5XIZGo2GDpUrlUoqxhwMBqHX67tmFTKZDCaTCSaTCc1mE/l8HisrK2BZlmaBJIslgYCwG0cNBMNke6Io0hEJMjC+WTCzWq1IJBKIx+MDbUYkSaJzf+2jCttlbtsLHMchlUphZmbmmJc4CcbMzjF2K066wEfKSUajcQPrUhRFSJKEUCg0ctluK2XOdlSrVbRaLdTrdQCgLMv2rEoQBBrsiEqMyWSCx+Ppqg1qs9k6GKCkhNuNealWq+F2u2kWmM1mEY/HYTabYTAYkEgkoFKpBmK49sOg2V61WkUsFusQ9B70vHa7Hel0etMypSAIdFyl2xiGTCYb2dy2H46m88IwGDM7x9itOOkCXyaT6ejrtYMIK8/MzIysi1gqlQYaWu8GnueRTCZRLBYhiiL1qcvn8/B4PFAqlTTYFYtFVKtVaLVaqqAySNYll8vhcrlgtVqRTCapBJrFYukaFBiGgdlshtlsRqPRQDqdxurqKlQq1ZYD/CDZHs/zSCQSqFQq1CevXC4PdR4S+DKZTM/3pn1UYWJiom+AbDe3rdfrW3ZhOJrOC8NizOwcYzfipAp8tVoNmUyma4ksm82iUChAr9ePHPSIuPUwpqOiKFKfOzJATzIxEsgkSUIgEIBCoUC9XqcMzImJiZGlrJRKJSYmJtBoNJBIJJDJZOB2u/sGs1arhUqlQkuGmUyG9gKtVuvQw9z9sr32mTyz2Yw9e/aMfK/E77BX4CsWi4jFYgPLqgGgDu3hcBiBQACTk5MjlXuPtvPCsBgzO8fYjThpAl+vvl677qbFYqEKJ6OgXC5Dr9cPtIARk9tqtQpJkmimQYIuz/PI5XIolUqoVqKleyoAACAASURBVKtgGAYsy2Lfvn3bqtvIsiz8fj8qlUpHAGwP3u19r+npaXqNFosFjUYDuVwOS0tL0Gq1sFqt0Ov1m2ZA9Xq9Z7bXaDQQi8UgSRL8fv+GXhoZzRgGbrcbuVwOuVyOkkckSUIikUCpVOp6ns3Qbm5Lht2HLVUmEgkYjcaj6rwwDMbMzjF2I06KwEfm9UwmU0dfb73uZi6X23LJql/G0D5zJggCVCoVzTIYhqGU+VKphHq9DoPBAIvFgqmpKfA8j+XlZbRarW0lVRDo9XrMzc2hWCwiEomAZVk6HhGLxdBoNLr2vViWhdfrhcvlQrFYRCqVQiwWo4zQXlkgsQ5aT9BJpVLI5/MbZvK2CoZhYDQakUqlYLVawXEcwuEw5HI55ufnR95MrDe39fl8Ayv2HCvnhWEwZnaOsRtxUgS+TCYDURThcrnoz7rpbgqCMLL2oiAIqNVqXfs0xWIR6XQajUaDLsAulwtKpZIqqRSLRTQaDepSvj5zJCMOkUhkIDbjKJDJZDCbzdRcd3V1lQ7ib6ZNKpfLYbVaYbVaUa/XkcvlsLi4CL1eD6vV2mFX1C3bIzN5Wq2260zedsDr9eLQoUNIp9PIZrOwWq1DzWf2g8ViAcuyVOdzM/ulY+m8MAzGzM4xdiN2feCrVqu0r9dOxe+muykIwsisunK5DJ1ORxexZrNJfe4kSYJWq6Uzd61WC8ViEcVikbqU2+32TcukZMSBuHzvFMhAfDabpTOAmUwGdrt9oEVao9HA5/PB7XajUCggkUhAFEWaBbZne61WC/F4HM1mEz6fb2Cng2FLncBacFapVEgmk13nH7cKjUaD2dlZhMNhygzu9XodS+eFYTBmdo6xG7GrAx/p601MTNASnSAICAaDUKlUG3Q3hx1ybkepVIJer0cqlUIul6PyYWTmjuM4lEolpFIptFotGI1GOJ1O6HS6oc7p9XqxtLQ0tAj2MKhUKgiHw5T9SQbnFxcXKQN0kN2/XC6HzWbryAIXFhYgSRLMZjNSqRSy2SxsNtuOij4DR0YVyHXvVJalVCrh9/v7mtsea+eFYTFmdo6x2yCTRtk6nwCQJAnBYBAsy8LtdgPYXHdzdXUVDodjaH+1YrFIHQlkMhkMBgPcbjckSaL2PjzP09nBbi7lw4DoYu6ELQ7x8etmyFqv15FIJMBxHNxuNwwGw9D3EQwGIQgCnU8kyijDlDYrlQrS6TRmZmYGenyz2UQoFIJWq4XH48HKygpkMhnm5uaGuvZhQV7L9plQSZKwtLQEh8Ox42LX24VEIkGF0McYYzdg12Z86XS6o683iO5mN2eGXuA4jsqHEcFnn88HlmVRLBbpAm80GuHxePq6lA8LnU4Hi8WCaDSK6enpbTkuYTiWy2XMzs523d1rNJquDNBBGYmVSgWVSgUMw8Dn80GhUCCfz2NhYQFGo5Ea5m5nL4mMKpDsFVgTr15dXUWr1dpRTUyr1Qq1Wo1wOEwDPFGdOdbOC8NArVaPmZ1j7CrsysBXqVSQy+VoX29Q3U1BEPqW20RRRD6fp6a1CoUCFouFNv9TqRQkSYLRaBzYpXxUOJ1OrKysIJvNwm63b+lYxFxVFMUOV/luIBmtXq9HoVCgmZTL5eqr6VkoFBCLxai+KNlg6PV68DyPQqGASCTS4TG4lXKkJElUDGD9qIJOp4NSqUQsFoPf7x/5HIOg3dy2Wq2iXq8fF84Lw2DM7Bxjt2HXBD7COuN5HpFIBD6fD0qlcijdTVEUuy62tVqNztzJZDLodDo4HA40m01qB2SxWOByuXY02LVDJpNhcnKSujhsxUMvFApBo9HA6/UOfO0ymQwWiwUmkwnZbBYrKyswmUxwOp0dgZPM5JFNRTeJM4VCAbvdDpvNhmq1SkuEJpOJZoHr0a9CT3q7pJzZLZC73W6Ew+GRLIuGhUqlwszMDBYWFk6ogEcwZnaOsduwKwIfz/NYWFiA2+2mg+gGgwHlchmRSGQg3U1JkjpKnTzPI51Od8zc2e12CIJAe0xkoc/lcltyYxgVZA6Q9PuGJYfUajWEQiEadEZZ1BiGgcPhoGzNxcVFSmjJZDLI5/NwOp2oVCo9ZdEIiFO8Xq8Hx3E0oySjEiaTCXK5vO91knuyWCx9RwpMJhNisRji8fhRkQojbvBGo5Ga2w6j8HMsMWZ2jrHbsCsCX71ep0K/REW/UCggHo93KI30A+nTkZm7ZrMJhmHoiEKlUkG5XKZi0Wq1GjKZDPF4fMfYlYPAbDbTntswIw6k9zXMwHU/KBQKeDweWK1WRKNRpFIpaDQazM3NQRAEpNPpoQJMOyOWlK5JFtgtuyXuE6lUauB7cjqd1LVhJxmlpB9MrJvIvB8pvZ8IWdSY2TnGbsKuYHWmUimkUin6f5IV+P3+gUqAjUYD8Xgc1WoVwJoaiVKpRL1eh0KhoA7t6//oJUnCwsLCtlvUDAtBELC0tASv1ztQZptOp5HP53fEWicej6PRaMBms6FYLNISp9FoHMhRfrPjkx6rJEnweDy0fB2NRtFsNjE1NTUwYUWSJLzwwguwWCzweDxburZ+5wiFQlQJh2A90/R41Olsx5jZOcZuwq7I+EjAasfMzEzf3akoijQA8DzfUUKTyWTQarVwu919j9FoNADgmFvJyOVyTExMUOPaXj0rohbSarUwOzu7baMQkiRRnzyr1UrdDaxWK7LZLBKJBC1jbiXQKpVKOvsYjUZRLBYRj8fp+zU7OztUACFEmmw2C5fLtSPBp5fzglqtxuzsLDW3nZyc3PbRlO3EmNk5xm7Crgh8ZCZMo9FAFEUqr9VoNDYEpXK5jFQqhXq9DplMBoVCAZlMBrlcDoZhMDMzM3DGsF3ee9sBMuIQiUS6jjjwPI9gMAilUomZmZltW+RrtRotMa8fg5DJZKhWq3C5XJDL5QgGg9DpdFQDdFSQ94sMxms0GtRqNQQCAWqYO+j9OZ1OZLNZZLPZLWek67GZ8wIpy6fTaWpuezyLVY+ZnWPsFmx74HvPe/4NExMG/O3fvnq7D90Tp5xyClKpFBWbZhgG4XAYpVIJ8/PzYBiGztxJkkQXIWLcajQaUa/Xkc/nh1qQy+XyjpXIRgEZccjlcrDZbPTnjUYDwWAQZrN5Uw3JQSEIAp37c7vdMJlMG45br9dRr9epKgthgC4vL8NsNndYLw0DSZJoWZX0cCVJQqlUQi6Xo1ZJFotl054Uua50Or3tgW8Q5wWZTAan0wmWZREMBoeyRjqaUKlUY2bnGLsGuyLja+89MAyDaDSKUqkEAFheXqZ/rJIkQaPRUCHm9tISsf4hCASKmJm5BRz3CSgUG3frrVYLHMcdVzt0mUyGiYkJrKysQKfTgWVZymzdTrfwYrFIF/V+Pnnr/fbkcjmVPEulUlhcXITD4YDVah04QyPGtKIoYu/evTRwymQymEwmmEwmNJtN5PN5rKysgGVZWK1WGAyGnufweDwoFAooFArbpqYyrPOC0WiESqVCKBRCo9HYsrntdmPM7BxjN2FXBD4AdFGLRCIoFAr050QgmgS7XhnGMKotwPFV5myHWq2mIw7EfHVQZutmaDabdCZvs7Jce7a3HkqlEj6fDzabDclkkvbYumWN7ajVagiHw9DpdBBFsed7SV4Dp9OJUqmEbDaLeDwOs9kMq9W6IauXy+UwGAxIJBLbEvhGdV5gWXZD32+nZwyHAcuyY2bnGLsCW270/Nd/JXHGGbfDYLgRl19+PxoNnv7ulluewfz8d2C13oQ3vekniMWONMeffz6D17/+blitN8Hl+ha+9KXHAayVSj/72Ufo43796xAmJm6m//f7/wn/8A9P4k/+5DbodF/H+973CySTVbzhDffAYLgRV1zxIIpFjj7+j38s4B3veBSzs3fizDPvxK9/HaK/u/DCu/A//+cjOO+8H2B6+vt4xzseRCZTAwBccMFdAACz+RvQ62/EY4/FOu57J0WitwqTyUTZm7Ozs1sOeqIoIplMYmVlBQaDAXNzc5seM5VKweFw9M3kWJbF9PQ0JiYmaAm0G4GCjCoEg0F4PJ6BZw4ZhoHZbMbs7Cz8fj8kScLy8jICgQAtexN4PB7wPI9yubzpcTfDVpwXFAoFpqenodFosLy8TAlUxwPUavVxdT1jjDEqthT4Wi0Bb3nLv+Kqq/Yjl/sQLrtsL+69dxEA8KtfhfCpT/0GP/rRJYjHP4DpaSOuuOIBAEC53MLrXnc3LrrIj1jsA1haugavfe30wOe9995F/L//dxkWFt6H++9fxhvecC++9KXzkU5/EEqlEj/+cRIajQbZLIcPfvD3uO66lyGX+xC+8pUDeNvbfop0ukaP9YMfvIBbb70Izz57KThOxFe+8hQA4OGHrwAAFAofRqXyUZx77pEZOZ7n0Wg0jssBZOI+oVQqIZPJ0Gw2t3S8crmMxcVFNJtNzM/Pw263D+SuXq/XB+5VEVkvh8OBWCyGQCBAF1iSPWWzWczOzo682WBZFh6PB/v27aM9vcOHDyOZTFLNTq1Wi0QiMdLxCWq1GgqFwpZ6vzKZDG63Gy6XC6urqygWi1u6pu0CmeUbY4wTHVsKfI8/HgPHifjYx86EUinHpZfuwyteseaEcOedB/He974EZ5zhglqtwJe/fD4eeyyGQKCIBx5Yhtutw3XXvQIsq4DBoMLZZw++UHz4wy+Hy6WDz2fA+edP4OyzPXj5y11gWQXe+ta9WF5uYG5uDo8/3sIll+zBZZedDoaR4fWv9+Oss9z4+c9X6LGuvvol2LvXCpVKhre8ZRZPP53qc+Y1EAui4232qtls0r6W3+/HxMQEotEoeJ7f/MnrwHEcQqEQYrEYvF4vpqamBqbbD5LtrQfp0c3Pz8NgMGB1dRXBYJD2aOfm5ralxMYwDCwWC+bm5uD3+yEIApaXlxEMBml/cNSshogoeDyebSlRms1m+P1+JBIJJJPJkTwItxOk1DnGGCc6trRyx2JV+Hz6jgxgetr44u8q9HsA0OtVsNlYRKMVhMNlzM2Nrk7vch3JtDQaBVyuI2U3lpWjUmkBAILBEu6++zDM5m/Qf488EkE8fmTuz+1eO5YgCNDpVKhUjpRJe4EouBxPqFarWF1dhdVqhcfjoXNzZrMZ0Wh04EWTzOQtLS1B/f/bO/PwqOqz/d+zT2bfkslMkknCJIEEFYqAglqUupWiiErVFqgbrQtqLX1bKr6tVt9a1Noq9oeCFeuKS1FE0KoULGgV1CI72SeZmUwms2T25cyc8/tjPIdM9skChHw/15Urk8zJnDNLzn2e5/s8zy2RoLKyMqeUXa7RXnf4fD70ej1MJhPC4TA3DLz78Y+ECEilUpjNZlRVVUGpVHJrw83NzaCogT8H3eno6IBQKBxR5wV2+g07czadTo/YY+dK15mdBMJYZliXpSaTHA5HOKvEuaUlCKtVA7NZAZstyG0biSTh9cZRVKRASYkSGzce7fUx5XIRotHjJx2XK9rrdv0RDkdw+PBhSCQxzJ9vwhNPzIZarYZYLIZYLO614CAzYeS4gPeVzUun04hEIiguLs75uEYLv98Pl8vV60zSgoICNDU19Whx6I2uPXnseK1cGUq01xXW5cLv96O8vBwikQjt7e2ora3NyQQ3F9ieQLaZva2tDXV1dVAoFNDpdIPyT0wkEvB6vaPivCAUCrnIry9z2xMBn88nlZ2E04JhRXyzZpkhFPLx1FNfg6LS2LSpFnv2ZNZIbrihGhs2HMS+fW4kEincd99unHOOCWVlasyfb0VbWwR/+ctXSCRSCIWS+OKLNgDA1KkF2LatCT5fDC5XZptcYNN6NE3jBz8wYedON3bubIPX60dDgw0vvfQZduz4GvX19UgkEggEAvB6vaAoCjRNA8hczebny8Dn89DY2Jn1+OFwGDKZbNQcvHOB9dBzu90oLy/vNTLj8/koLi6G2+3uM4WXTqfhdDrR0tICvV4/6FFv3RlutMc22UejUVRUVEAmk0EkEqG4uBjl5eUIhUKor69HOBwetahDr9dDJBJBJpNBLpfD5XKhrq4OHR0dfaaMGYaBw+FAQUHBqPn78fl8mM1m6PV6NDY2jkgRzlCQSqWkwIUw5hmW8InFAmzatAAvvHAIOt3TeP31Y7j66koAwMUXl+Khh87DNddshsn0DBoaOrFx43wAgFIpxkcfXYstWxpQWLgWlZXPYceOTLXlkiU1mDIlH2Vl63HppW/iuusmDvp4GIbJqgosLMzDU09Nw5o1hzBt2ru48MKP8frrLlgspd+WmgvB5/MRj8dBURQCgSCi0RiOHj0Kl6sVd91Vg1mzXoFa/RR27GhAMplEIBA4JdKcNE2jtbUV0WgUVqu1X6GSSCQwGo2c5x4L65NXV1cHhmFQUVExrIhqONFeLBZDQ0MDtz7ZfY2M/b3ZbIbf7wdFUb2OqhsJjEYjwuEw1Go1rFYriouLkUgkUFtbi9bWVkQikSzh9fv9YBimX6/HkUKn08FiscDhcKCjo+OEpx1JgQvhdOC0GFLNwjAMjh49mrUOwuPxUFlZiUgkwjUVA5lKQvZLLBbj6NGjXDM2RVFcg3oymcy6nUqlIBKJIJFIIBKJuPSpWCyGSCQa0DZnJKAoCjabjVujGozQMAyD1tZWiEQimEymrJ48s9k87JYH1uy3qqoqZ+FjnRcG45nYdV88Hg9SqXTAmapD4ciRI1AqlVkp7XQ6Db/fD7/fDwDQarWQy+Vobm4ecmp4qLDFR2KxeNTdJbri9/sRDodPiJUTgTBanFbCB2TWWhoaGrjIhs/no7CwEBqNBnw+HwzDIJlMIhKJcF9AJs1mMpmgVCq5VoDuhEIhtLe3o6SkhBPE7gIJgBPB7qIoFouHfYJiT/p6vX5QrQVdSaVSqK+vh1wuRzgcRn5+/pB9+Lpjs9mgUCgGXEfsCk3TcDqdiMViOa1bJRIJ2Gw2VFRUwOfzoaOjAyqVCgUFBSM26LmjowPt7e2oqanp8Z4xDINoNAqv14tgMAiJRIKioqITZkLMwrZ65OpKMRxisRjsdjsqKytHfV8Ewmhx6oyFGCEkEglKSko4A9OioiIuotDpdNDr9ZBIJJBIJNDpdGAYBvF4HA0NDYhGo+jo6OBc1rtGhECmjYG1J+rrJJ1Op7NEMZFIIBQKcQIpEAiyRLH77f5OnMFgkJsIMpTKQXZtJhgMory8fMTGrfU3paUvWOd31qVgKGumfD4fBoMBGo0GHR0dqK+vh16vh16vH/YarF6v5+yuCgsLs+5jPx9sP6dGo4HdbufcHjQazQlZA2bXb71e7wkzt5VIJEgmk2RmJ2FMM6aFL5VKIRwO9xgzpVQquV4qpVIJpVKJeDwOr9eL2tpaqNVqGAwGzkxWKBRCKBSipKQkKyIMh8Nob2/nTnTBYBAWi6XfYxIIBMjLy+vVfodhGKRSqaxIkW14pigKqVQKQqGwhyiKRCLOjLWsrCxnax+KouByuRCNRmE2mzmBt1gsI3LyynVtLxQKweFwDMv5vSusCS47Aq2urm7YFaBdbZUKCgp6PLeuzgsymQwGgwGRSCTLMFen0426TyOPx4PBYDhh5rakspNwOjCmU52tra0IBALQaDQoKioa1D97KpWC1+uFz+eDTCbjooO+0jesEPr9fvh8PvB4PM6ZvXtEOFxomu51fTESiSCdToPH4/WaPu26vtj92FlXcq1Wy53AaZpGY2MjV8I/HHJZ22PHqPl8vmFFJ4lEAs3NzZg4sffCp1gsBpfLhVQqBaPRCKVSOSQhoGkahw8fRmFhIQwGQ9Z9drudq7TsDkVR6OzshM/n41ol1Gr1qEeBJ8rclnX6GMl+RQLhRDKmhc/pdHJixM59HOzEDJqm0dnZCY/HAyBzUq6qqurzBMmaqRYUFCCRSGStEQoEgiwhHKl1plQqxaVs2Wi0qyB2F0g+n8+JIJ/P5xwnjEZjj0kz7JSX4RZlDHZtL51Oo7W1FTRND9t0NZlMoqmpqU/hA45X+LpcLggEAhQWFg4ptdva2opQKASFQgGKomC1WhEOh+FwOFBRUdGvmLHH4PP5EI1GoVarodVqRzUKTKfTcDgcoCgqp2k7uUDc2AljnTEtfN2dGNhJI7nANkz7fD5uaohWq806oTEMg7q6OpSUlPQ4aTEMMypCyBZwqFQqGI3GASMWhmGQTqcRj8fh8XgQiUS4Ygu2GlUoFGZFiclkEtFoFKWlpRCLxTlHRYON9mKxGFpaWqBSqUbEbmcwwsfCtmywg6ONRuOgU3SxWIwrvgEyKdWqqirU1dXBbDbnNNGGoiguayASibgocDSisq6R9WiY25LKTsJYZ0yv8XXtSWOHEOcKj8eDRCKBQqGAwWCAx+NBR0cHNBoN9Hp9lgFnb5ERG21KpVLo9fosIQwGg2hra8tZCMPhMFpbW3M2JY1EImhra4NSqcTEiROzot/u0SLbsM9WegI9q1G73mYvBFghUalUg1rbY6fKDLUgZ7jweDxotVqo1Wp4PB40NjYO2gS3s7OTEz32sYbqvCASiVBQUID8/HyEQiH4fD7OBkmr1Y5oK8Rom9tKpVIuU0IgjEXGdMTndru5WY4URQ35CtTr9SIej6OoqAhAJqLw+Xzw+/1cipDH4/W6njMQbNUoGw1Go1EIhcIsIex6AmbX5HJZA0skEmhrawNFUTCbzTmtnbHCx/b2xWIxKBSKHqlUICOMQqEwa0hAeXk58vLyeogfTdNoa2tDJBKBxWIZ0RN7MplEY2MjJk2alPPfplIpuN1uBAIBrrimL+FmIye3OzO4nBX/ysrKERlCza4d+/1+iMVi6HQ6qFSqEY0C4/E4WlpaoFQqR8zclqZpHDlyBDU1NaSykzAmGdPCx5JOp1FbWwur1TqkQpOOjg6k0+keZetsw7LL5eKmnwy1UIKluxBGIhFuRBZFUUgkEigrKxtUOo6maXg8Hni9XhgMhpz7+lgCgQDsdjuATNVedXV1j2Nm2zTYwpGuHxsejweBQMBFh3w+H6FQiGuWl0qlI3qCpCgKDQ0NQxI+lkQigfb2dkSjURiNRmg0mj6PMRQKwWazAQCKi4tHzKWdhWEYBINB+Hw+xONxaLVaaLXaEauaZNdXGYYZMXPb2traEb+gIRBOFKeF8AFAW1sb52OWK2wBRH5+fo/7kskk6uvrYTab4fV6kU6nud6xkbgyZxgGkUiEm6LCMAxEIlGfESFLOByG0+mERCKByWQacmVpPB5HU1NT1rSb6urqPos2kskkN+KMTfNaLBYulcqewNlWEYqikE6nIRKJ+mzqz3XazUgIH0s0GoXL5eIufBQKRa/HYrfbEQgEeqSQR5pEIsFFgVKpFDqdDkqlctifNYZh0N7ejkAggNLS0mELFqnsJIxlxvQaX1f0ej0aGhqQn5+fc9l4Op3uUzhYCyL2nzwajcLj8WQ1xA/nREhRFNra2iCXy7lUajweRzgcht/vh8Ph4IRQoVBAIpGgo6MDkUgEJpNp2HNDWQHj8Xjc7WQy2W/lIXutpNfrucIbdu2PPbEqFApu+65tGmz6NBAI9Dvtpuvt3k76b7/diq1b/4vdu28Y1vOXyWTcAOy2tjaIRCIUFhbi6qu34vrrJ+EnPzkDiUQCf/zjPmzc2Ayh8BPs2bMYNTUbEAjcBYFgZItT9PpnsH//TzBx4kQEg0HOLUKj0UCn0w35Aoe9KJRKpWhqahr2miuZ2UkYy5w2ER8Aroepe8/VQLS2tkKpVPaawmpqaoJer+8hMKwNTWdnJ9RqNfR6fc5X0dFoFC0tLf02cjMMg1gshnA4jEAggEQiAYFAAJVKBaVSCblcPuz+MLbfr729HTRNw2g09hr9ApmIj01zsa9JOp2G3W5HKpUaUgl912k3vbVrdJ92w+fzsW7df7FtWwd27/7RiKVRGYaB3++H2+2GXC6H0WiESCTC3r11mDNnG2y2ZSgoGLnJKBdeuBGLF9fg1lvP6ne7eDwOv9+Pzs5O5OXlcVHgUJ83W2Wr0WhQUFAwpMfp7Owc1EAHAuFU5LSJ+ADAYDCgtbU152kg6XS6V/FIpVJcsUd3JBIJzGYzCgoK4PP50NzcDKlUCoPBMCj/ts7OTrS1tfXqodcVNhoLhUIQCASwWq1cetTn88Fut0MsFmelRnMVQh6Px7VxOJ3OfoVLLBbjjDPO4H5miycUCgVKSkqGlJLLddpNPB4Hw9CIx+M4fPhwr9NuulajDuazEAgEuJFjbAUo6xYRi0mh10tHVPRyga1YNhqNCAQC8Hg8cDqd0Gq10Ol0OV9osOa2LS0taGlpQXFxcc6fGRLxEcYyJ2ak+wmC9W8LBoMDb9yFvoSPbVzu72QuFApRUFCAqqoqqFQqtLW1oaGhAX6/P6vdgoVda2lvb+/TQ6/rcbW1taG5uRlarZaroJTJZMjPz0dZWRmqq6u/tVgSwOv14tixY6ivr0dbWxuCwWBOjt3s7MfBFm+EQiE0NTWhoKCgh0tES0sQCsWTSKd7vga5wOPx4HLFsGTJx6iqehXV1W/gkUeOgs/nIy9PhurqapSVleGxx45h5sx3YbW+hgsu+AfeffcA6urqcPjwYbz11h6cddbfoFT+BQUFT2P58n8iFoshEklg8eKt0OufRknJC5g9eyO++qoWAHDddTuwY0cEn3/uxeWX/wNOZxgKxZO48cb30dwcAI/3OFKpzHPz+WK46ab3YTavhVa7Bldd9Q4AwO+PY/78TcjP/yu02jWYP38T7PaMj96qVbuwa5cDy5dvh0LxJJYv//jb5/s46usz7g+BQAJLl25Dfv5fUVr6LP7why+gVmswYcIE7NoVwxVXfICf/vRdqNVPoqzsWWzb1jjo15U1txWJRGhsbMxZxLrO7CQQxhqnVcQHZKI+dlr/YKM+mqZ7FbdgMDjoNTR2tqNWq0U4HObWAfV6PXQ6HQQCATdNP5lMwmq19rk2yDAMt+Ykl8v7LZ/n8XiQyWRckzJN09+ehQKSEgAAIABJREFU1CPwer2w2+2QSCS4775vYLGo8cc/Xjhio7PkcjkmTJgAiUSCsrJ1eO65y3DxxaUAAItFhXD4nmHvI52mMX/+25g714KXXloGgYCPzz934LPPjgHIvO4SiQSzZ1vw0ENzoFZL8OSTX+HnP9+L5uafQiTi4eabX8Xy5VNw9dVl8PsjOHgw87q8/HI9HA4vPvnkEtA0hSNHAqCoKI4dOwaKSiEUCmHZsvNhMhVg8eJtsNtvAwA0NweyjnHJkm1QKMQ4dOgmKBQifPaZEwBA0wxuuukMvPHGFUinGdx88wdYvnw73nnnKvzf/12ATz919JvqvOuu7QgEEmhsXAavN4ZLL30LJpMCt9xyJkQiEf77Xy9uuWUKHn10Dtau/S9uvHErDhz44aCjQHbkms/nQ2Nj44DZh+5/KxKJkEgkSGUnYcxx2gmfUqmEy+VCLBYb9MSK3iI+mqYRiUS43r7BwuPxuMHYsVgMHo8HtbW1UKlUiEajkEqlKC8v7zOKTCaTaGtrQzKZRHFxcc7zLLvOEWWfRywWA4+3H7FYDMeOHYNEIuG2Yd3kUykaQmFuCQBWdEaTPXtccDrDeOyxOdzxnX9+ESd8LIsX13C3V6yYgYcf/hzHjvkwZUoBJBIRHI4EABkqKw2orGTFOYZ43IdIJA9yOYXJkzPFHjRNI5lMcNNu+qOtLYz332+C17scWm1GAObMyfST6vV5uOaaKm7bVavOxUUXvT6o551O09i48Sj27VsKpVIMpVKMFSum46WXDuGWW84EAJSWqvCzn00FAPzylxfid7/7Bk5nCF6vFwqFAjqdblBpd51OB4lEwi0TDLYthk13EuEjjDVOq1QncHy9KpfJEr0JXzgcRl5e3rAqNvPy8lBSUoKSkhKuipHt4+sOTdPo6OhAQ0MDZDIZrFZrn6LndIZxzTWbkZ//V5SXr8NTT30Nny+G4uJnsGVLw7fHn0RFxXN4+eUjeOWVevzjH01Yt64WM2d+hLvu+vrbfr2X8D//sw2TJq2DXP4XhEIRPPLI57Ba10OpfBI1Nc/j7bfrsva9fv1+VFc/z93/9dftWLJkG1pagrjiirehUDyJRx/d0yMd6HSGceWVb0OnexoVFc9h/fr93GM+8MCn+OEP38XSpdugVD6JyZM34MsvXQCA1tYgSktVWaKcqSLNfk0ef3wvqqufh1r9FDSaNQgEEvB4MlNX/va3y1Bb68ekSRswY8ZLeO+9zGu0dOlkXH55OW688SPMnbsDTzxxDDyekKtUHYy7Q2trCDpdHid6XYlGKfzsZx+itPRZqFRP4bvf3YjOzsSg0r8eTwwURaO09HjGobRUBYfj+PCAwsLjnw+ZLBPhKZV6TJw4EXK5HC6XC3V1dejo6EAqlep3f3K5HFarFcFgEHa7vdc0fXfIOh9hrHLaRXwAoNFo4Ha7kUgkBoxI2H/w7ie4XNKc/dHVQ0+pVMLv93NO6AaDAUqlEtFolCsqGagJn6YZXHHF21iwwIrXXpsPuz2Eiy9+ExMnavH885dj6dJt2L//J1i1ajemTs3H0qWTAQCffeZEcbESDz98PvdYQqEQ//qXD++8cyWk0jTEYiEqKrTYtesGFBbK8eabx7B48VbU198Kk0mBN988hgce+AzvvLMA06cXoqGhEyIRHy+9NA+7dtmxbt0lmD49UxgSCGSnA6+//j2ccYYeTudtOHrUh0sueRNWqwZz52aqAt99twGbNi3Ahg2X4/77d2P58u34/PMfo6REhZaWYL8R6a5ddjz66B5s3/5DTJ5sAJ/Pg1a7hhPHykotXnttPmiawaZNtbj22nfh9d4JuVyM3/1uNu6//xx8/PFXuOOOr1BWJsfVV6fAMMygLnpKSpTw+WLo7IxDozkufgzD4P77P8KBAx588MEPYLFoceRIADNmvModV3+iajDkQSTiw2YLoqYmU6Xc0hJEUVHPQqvuCAQCLsUei8Xg8/lQW1sLpVIJnU4HmUzW675FIhHKy8vhdDrR2NjI2VbZ7XZYLJYeF4dSqTTn9XQC4VTgtIv4gMw/vlarhdfrHXBbdn2v64mAXWMbjvAxDMNV35WWlnLmpAaDAVVVVZzR6ZEjR2Cz2WAwGLhh0f2xd68LHR1R/Pa3syEWCzBhggbLlp2FjRuP4tJLy7Bo0UR873tvYtu2Jjz77KUDHufdd0/DpEmFKCsrgkQiwaJFE2E2K8Dn83DddZNQWanFnj2Z6Ou55w7gV7+agRkzTODxeKio0KK0VP3tZJeMkwSbpu1aVNPaGsSnnzqwevUcSKVCTJ1agFtvPRMvvniI2+b884swb94ECAR8LFlSg2++6QAAzJxZCJNJgZUr/41IJIl4PIVPP3VkPYdQKAmhkI/8fBlSKRq///1nCAaT3P0vv3wYHR1RMAwNNivndruxceOXeO+9r3Do0BEoFEIIhXywHwPW4WIgTCYFvv/9ctxxx8fw++OgqDT+/e/Wb18TIfLyRBCJ0jh2rBUrV24HADQ0NMBut0OjEeDIETc3C7YrAgEfP/zhRKxatRuhUBI2WwBPPPFVVkp3INj13+LiYkycOBEymQxOpxP19fXweDy9RoF8Ph9FRUXQaDRoaGhAY2MjV0HcHRLxEcYqp6XwAZnm6kAgMGBVY29pzkgkwpXGDwWGYeB0OuH3+zFhwoRe1xrZ3jWFQsGlpdxuNyiK6vexbbYAnM4wNJo13Ncf/vA52tujAICf/vQsHDzowY03ToZeP7D9TUlJdjHDiy8ewtSpf+ce++BBD5cybG0NwmrtveKTphkwDMOdwNlUc11dHfburYVGI0JnZzucTidcLhd0Oj6amzO9aYlEEgaDBPF4HBRFQSLhIx5PIZWiIRDwsWXLQtTXd8JiWYfi4mfx5pu1Wfu+7LIyXH55Oaqq/obS0nUQiXgoLs4YBzudTrz11gFMmvQclMqncO+9O/H00+dCLhcjEuHhF7/4L2bN+hgLFnyK6dO1uOIKM8xmc06N4i+9NA8iER+TJj2PgoL/h7/8JZNK/vWvZyOd5mHq1Hdw/fWf4tprM2tzrEXSzTdXYdOmehQUPIPFi99CXV0mrdzR4YHf78fq1bMgkwkxYcJ6nH/+RvzoR5Nw881nDvq4usJGgRUVFTCbzYjFYqitrYXdbkc0Gu0xgo4d0M5+Hj0eTw9xZis7B5MWJRBOJU6rBvbusBWNfTVjA5km8ra2NlitVu53TqeTa1PIlXQ6jZaWFvB4PJSUlPQQ1Xg8DqfTCYZhYDabud61rg3xKpWKc9Xuzn/+48TSpdtQV3drL/umcf75r6GqSot3323A3r2LUVGRmcp/003vo6goO9XZvRLTZgugqup5bN++CLNmmSEQ8DF16t+xfPl3cOutZ+Gyy97CvHnluOees3vsu6zsWTzwwBmYMSMjjKmUElOnvoFQ6A44HCHU1LyM5uYlkMkESKfTePjhr+ByRfGnP83En/60H01NYTz++LRv50qGcemlO/HNN5dDIskY7PL5fO47n8+H3+/n3DDS6TRSqRTX78c6bkgkEkilUu62SCTqM73IejOKxeKT0pSdKahJIpFIZH2xPovscxCLxYN6PoMhlUpxhrlsDyObmXA4HPD7/Vnbm83mHsbFiURiSJZWBMLJ5LRc42PR6/Ww2Wz9TuBPp9NZ97EDg8vKynLeH+uh19skfJqm4Xa74ff7YTQaexRODLYhfubMQiiVYqxe/QXuvnsaxGIBjhzxIhZL4YMPmsHj8fD885dj9eo9WLr0fezadT0EAj6MRjkaGzt7HHNXIhEKPB6Qn5+JUDdsOICDB48XCd1665n4xS924vzzizBtmpFb4ystVaOwUIFEQgGTyYT29nZubVUqlWLiRBlmzzZj9er9ePzxC1Fb68frrzfhlVd+gJKSEmg0LVAqwXkpSqUBADtRVVUFhsl4DHYVAzYK8fv9XIM6+/qIRCLQdKa5PRaLZYlmX7f5fD4XgRuNRiQSCe6+ro89mvD5fM7eqitsA3/X5x8KhZBIJJBOp3sVRIlE0uPz3v0iB8is8bJTg9h0Znt7O9RqNUKhTL8he+GWTqe5MX1dGe2q3htvfL/H2jSBMFxOa+HLy8uDRCJBMBjssymbpumsqCwWi3EnoVyIRCJoaWmB0WjscXJgfflkMtmAljZspGkwGBAIBLjh23q9Hmq1GgIBH++9dzVWrNiJ8vL1SCTSmDhRh4ULK/DEE19i797FEAj4+PWvZ2Lr1kb88Y97sGrVubjlljOxaNG70GjW4MILS/DOO1f12HdNjQErVkzHrFmvgs/nYenSGpx33vF2jkWLJsLrjeFHP9oKhyOMsjIVXnppHkpL1fjNb87BXXdtx69//W/cf/+5uPbabAun116bj9tu+whm8zPQaiV48MHZ3EmYYTLvQzgc/tY1IbM2W19fDz4f3Ek9Ly8PGo2Ga7quqel/vYumadA0jXQ63eM7e5uiKKRSmZ49oVAIt9uddT+QESV2rYzH4/V6EcVuG4/HuW26RqhDhcfjcQO+u08QYsWaFcRgMMhFiUKhMEsUGSbjvcjOY+2+D4VCwbnMt7W1cet/6XQaCoUCRUVFA6bhCYSxwmmd6gQy00Xa29thtVp7vXL3+XyIxWJcv57LlSnkyMXlgbUuKikpyTo5sT15iUQCZrO519FnA8EwDNcQn0gkuNFio+kQMFr0ls5jf+bz+RCLxZBKpQOm81g/uMmTJ4/IcbFDv0tLS3vdV1exZBu3u4pZKpVCMBhEJBLJEs2u4tk9whwoAu1+Xy5RJ8Mw3Ig39nU+55zNePDBMzFrlr7XKLHrMPBMA3+2yKnV6lF3XE+n6R5Dv0nERxgNxt7ZM0cUCgVcLhcikUivwtO9uCUUCg26aZ0dPxYMBrkJJuzvPR4PPB4P9Hr9kGdYAtkN8fF4HB6PB3V1ddxg7NFONfUHTdO9RtPpdLrHWhWbohSJRNzJlnWbkEgkOU+TGanrtUQiAY/H0+eFERux9XehIRKJoNfrodfrez1Odh2ye7TZ9Xsikej3ftbzcCAB7fo7Nupjb5eWWlBVVYT9+11YuPA9rFw5BRIJ8NhjB+BwRFFRocQf/jAdJSVCbNjQhP37O/HUU9Oh0+mgUChw993bwePxcNVVFbj77n/hwIEbAQCXXPImOjvj2Lt3CQDgggtew4oV03HVVZU4csSL22//CPv2daCoSIFHHrkAV15ZASAjanl5QthsQXzySSs2b14IvV6KW275J+rq/Jg3bwLI0iFhNDjthY9NE7LTLLrTdVwZu27SnyUPC+tIkE6nMWHCBO7EyHrriUSiLDEcCaRSKYqLi0FRFLxeLxobGyGXy2EwGAY9pWakYBgGiUQCLpcLnZ2dEIlEXIRB03RWNKHRaHpEFcNhJN0YHA4H8vPzh2z3MxDsGuFwnjfDML2mbbvfTqVSfd5PUUnYbDZs3uzCXXd9id//fipMJil+8pPd2LDhu/jOdwx4660mLFv2GTZvPg/z55uwdm09/P440ukOKBQqbNx4FO+/fw1qavSoq/PD44lCrZZg//4OCIX8b9tKePjyy3ZccEExKCqNK654GzfffAY+/HARdu92YMGCt/Hll0swcWJmOeDVV49g27Zr8N57VyMcTuLMM1/Az39+NpYv/w42b67HDTdsxa9/PXOk3g4CAcA4ED4g09De3t6e1dDu9XrR0dHBpaK8Xi/y8vIGZfdCURRsNhukUikXzaVSKbS3tyMUCnE+eaNVFMF6xuXn56OzsxOtra1cocJI77d7ZEVRFHw+H/x+P3g8HsRiMWKxGBiG4ayZhltteKLw+/3ccZ/KdI32htpiIxLtRnMzDxs2fI0XXrgM551nwj33fIKbbqrG7NnFoGka119fgTVrDuGbbzoxY4YOZ5+txccfu7FokQV///vnMBjycPbZmSWAGTMK8e9/22E2KzBlSj40Ggk+/dQBiUSAykoN9Po87NplRzicxMqV54DP52HuXAvmz7fitdeO4IEHzgMALFhQwa0j79vnBkXR+PnPzwaPx8O1107EE098NTIvIoHQhXEhfOwAaY/Hw6UxJRIJ53jOEg5nxkElEgmUlpb2mn6LxWJcpSjr++f3+9He3g6VSoXKysoRGwI9EF0ndASDQXg8HrhcLs4hPpfjGCg92XXtTaVSZRn+plIp2Gw2hEKhYfnEnUgoiuIcMsbC8Y4E69YdwJw5xbj00kyq0emM4dVXa7F+/RFum2QyjXQ6U4R1551CrF27D7/97WV48MH6rOb5OXNKsHNnK4qLlZgzpwRarQSffNIKiUTAzSp1OsMoKVGCzz/++nYfu9a1j9TpjKCoSJH1fnQd2UYgjBTjQviATGtDbW0tjEYjhEIh5HI5hEJhr5Vq7JpKdwKBAJxOJ4qKiqBSqbJ68kpLSweVIh0NeDwe1Gp1lkO82+2GVquFXq/nogSGYUBRVFZRCfs1nPQka3HT0tICu93OVT+O5vNln89Q9sMOGNDpdONqwPIzz1yC1av34N57d+DPf74IJSVKrFp1LlatOrfX7RcssOL22z/C7t312LKlHqtXf5e7b86cYqxYsRMWiworV86EVivFsmUfQiIR4M47M4OzzWYFWltDoGmGE7+WliCqqrTc43R9/0wmORyOcNb72tLS99AEAmGojBvhEwqFUKvV8Pl8nOu0wWDg2gXYyE8sFmPChAk9evs6Ojrg8/lQVlYGiUQCl8sFv9+PgoIC6HS6UyZqYMvuWVui2tpaCIVC8Hg8pFKprGZoNnqTSCTcNkNFIBCgtLSUMzcdTkHPaBMMBpFMJke9SvFUQ6kU44MPrsH3vvcmVq78N5YtOwsLF27GxReXYubMQkSjFLZvt2HaNA14vCTC4TAuvrgAy5btwNlnF8BiOR59zZ5txrFjfrhcEcycaYJYLIDNFoTfH8frr88HAJxzjgkymQiPProHK1ZMx6efOrBlS2awQm/MmmWGUMjHU099jTvumIotWxqwZ48LF11EXN4JI8u4ET4gE/U1NzfDYDCAz+dDo9Ggra2N8/ATCASYMGFCVoowlUqhsbERfD4fVquVcxyXyWSoqKgY8prLSDCY6kmtVotUKoVIJIK8vDwYDAYoFIpREWo+nw+LxQK73Q6bzdbrYOOTTSqVQltbGywWyykrzKOJRiPFRx9di4suegMiER/r1l2CO+74EPX1nZBI+PjOd7T405/OQWFhJltw990yfPe7r+NXvzo36zMjl4sxbVoBpFIhxOLMezxrlgmHDnk5p3qxWIAtWxbijjs+xiOPfIGiIgVefHEeJk3qfU1VLBZg06YFWLbsQ9x//27MmzcBV19dOfovCmHccdr38XWnubkZarUaWm0m3RKLxSCVStHa2orCwsKs6r5UKoX6+nqkUilotVqk05kpIiaTadCGncOla3qye/8bTdNZfVjsOlxv6UmaphEIBOD1esEwDAwGA9Rq9aic/NlqyWQy2eda6XA5ePAgJk+enLOA2+12zoB1PML2+IXDYYTDYW4uLdvALpPJsj4TLS1BTJr0PFyu26FSnbzWGQJhJBl3wsf+w7Oea30Rj8fR3NycNcFep9OhsLBwVMSir1mNiUQCAoEgKz3Jfg0lPckwDCKRCDweD+LxOHQ6HXQ63Yg3xDMMw/VPlpWVjfjjHzp0CNXV1Tm9F+FwGA6HAxUVFadcJDqasBE/+9lnGIYTOoVC0ed7Q9MMfvGLHQgGk3j++ctP8FETCKPHuEp1AuBcx/sTDHb8WHcyTuZDSxHedttHKCpS4De/mdGrwFEUxUVrbHM326A+kifpruOp2Ib42tpaaDSaEW2I5/F4KCwshNvtRlNTE8rKyk5qWpimac4X8XQXPZqmEYvFOKFLJBKQy+WQy+XcezzQ5zgSScJoXIvSUhU++OCaE3TkBMKJYdxFfIOBpmn4fD5ufNn99x+AySTDr351FsrKygY8aWzYcADPPbcfH3xwZQ+BYxim1+htsP5vowHbm+fz+SCTybiG+JFaB2QLg8rLy0esUTzXiM/lcoGiqNOyoIUdJsBGdZFIhLt4UigUyMvLG5frmQRCX4x6xNefc/bJfKz+4PP53FBooVAIjaYVer0S5eXlWdv1lZ50Op1cNMVa46jV6hGpnhwNRCIRjEYj8vPz4ff74XA4ONPckWiIz8/PB5/PR2NjI8rKyk54C0EsFoPf7+fcH04HUqkUF9GFw2EuktdoNCgqKhqTs1wJhBPFqKhIWdk6rF79Bc466wXI5U9i9247Zs9+FRrNGkyZ8nfs3Hk8jXjhhRvxm9/8GzNnvgyV6iksWPA2fL6M8WlzcwA83uP4298OwGJ5FnPnvgEAeP75A6iufh5a7RpcdtlbsNkCADJXvvfeuwMFBX+FSvUUzjzzBRw8mHHyTiRS+OUvd8JieRZG4//Dbbd9hFgs08O3c2cLioufwZ/+tBcFBX+FybQWL7xwECKRCOvX78crrxzBo4/ugULxF1x66Wuw2Wz45S+3orR0LQyGtZg+/XW8914zFAoFgkEpHnroMPbt68SZZ76Lmpp/QK/XY/nyXXjwwS84EVm/fj8qKp6DTvc0rrzybTidx5t6ebzH8cwz+1BZ+Rw0mjW4886PR2w2ZX+wgl9ZWYn8/HyuHcLj8Qxo6DsQer0eRqMRzc3NiMViI3K8g3lN2EKbwsLCMS0GrHuFy+VCfX09amtrEQgEkJeXh/LyclRVVaGoqIi7WCMQCP3AjAKlpc8yU6a8wLS0BBi7PcjodGuYrVsbmHSaZj78sInR6dYwbneEYRiGmTPnNcZsXsscOOBmwuEEc/XV7zA//vF7DMMwTFNTJwM8xixZspUJhxNMNJpk3nmnjrFa1zOHD3sYikozDz30GTNr1isMwzDMBx80MtOmvcj4/TGGpmnm8GEP43SGGIZhmJ///F/MFVdsYrzeKBMMJpj58//BrFz5CcMwDLNjh40RCB5n/vd/dzHJZIrZurWBycv7M9PW1skcPnyYWbDgVeaOO95lWltbGbfbzQQCAeaVVw4wdnuQSadpZuPGI4xM9mduXxs2HGDOO+/VrNfkJz/ZxqxatYthGIbZvt3G6PVPM1995WLicYpZvvxj5oILXuO2BR5jfvCDfzB+f4yx2QKMwfA08/77jaPxVg1IJBJhbDYbc/jwYaatrY1JJpPDerzOzsxrGolEhvU4hw4dYlKp1IDbud1upqmpiaFpelj7O9HQNM3EYjGmo6ODaWpqYg4dOsTU19czLpeLCYfDY+75EAinEqOWN7z77mkoKVHh5ZcPY968CZg3bwL4fB4uuaQM06cXYtu2Rm7bJUtqcMYZ+ZDLxXjoofPwxhvHkE7T3P0PPDAbcrkYeXkiPPPMPvzmN+eguloPoZCP++47F/v2uWGzBSASCRAKJXH0qA8MA1RX62EyKcAwDNat+wZ//vNF0OnyoFSKcd9952LjxqPcPkQiAX7729kQiQSYN28CFAoRmprCqKiogEajgVarRXFxMfLz86FSqfCjH52BoqLMOKbrrpuEykot9uxxDeq1eeWVw7j55jMwbZoREokQjzxyAf7zHyeamwPcNitXzoRGI4XFosJFF5Vg3z73CLwruSOTyWCxWGC1WsEwDOrr69Ha2jrkqE2tVqO4uBg2m40bETdasM4LZrP5lEsv9wbriG6323Hs2DHYbDYkEglotVpMnDgRVqsVRqMxy5iYQCDkzqjlRNgZfDZbEG++eQxbtjRw91FUGhddVNJjWyAzm4+iaHg8sV7vt9mCuOeef2HFip3c7xgGcDjCmDvXguXLv4M779wOmy2Iq6+uxOOPz0E8nkI0msLZZ7/U5W8YpNPHU2V6vTRr/VAmEyESSfVZifjii4fwxBNfork5CAAIh5NZx9wfTmcY06YZuZ8VCjH0eum35q5qAEBhoTzrWMLhk2sCKhaLYTKZOId4m80GiUQypIZ4pVIJi8WClpYWbvzbSMOcAOeF4ULTNKLRKLdOl0wmIZfLoVAouOMmAkcgjDyjJnzsP2xJiQpLltRg/frL+ty2tTXE3W5pCUEk4sNgyON+3/Wfv6REhVWrzsWPf9y7+/bdd0/D3XdPg9sdwQ9/uAWPPbYXDz54HvLyhDh06EYUFeXeeN793GOzBbBs2YfYvn0RZs0yQyDgY+rUv3NrTgOdq8xmBWy2IPdzJJKE1xtHUVHuRrUnGoFAgPz8fOj1egSDQbS3t8PlckGv10Oj0Qy6elAul3Mjzmia7uHpNxiYftb4TkXnBebb6ktW6KLRKFd9aTKZRrSSlkAg9M2ol0guXlyNLVsa8M9/NiGdphGPp7BzZwvs9uNi9/LLh3H4sAfRKIXf/vZTXHttVQ8nZpbbbpuCRx75AocOeQAAgUACb755DACwd28bvviiDRSVhlwuglQqBJ/PA5/Pw7JlZ+Hee3fA7Y4AAByOEP75z6ZBPQejUY7Gxk7u50iEAo8H5OdnPPA2bDiAgwc9Wdvb7SEkk70XhNxwQzU2bDiIffvcSCRSuO++3TjnHBMX7Y0F2JFvVqsVJpMJwWAQtbW1cLvdWU3//SGTyVBWVgaXywWfzzeov2lqasKRI0dA0zRqa2tx7NixXq2T2tvbUVRUdNKFhKIo+P1+Ln3Z0tKCZDIJnU5H0pcEwkli1Mu/SkpU2Lx5IX71q09www1bIRDwMHNmIdauvYTbZsmSGtx44wc4etSHOXOKsXZt31MiFi6sRDicxPXXvwebLQi1WoJLLinFokUTEQwmce+9O9DYGIBUKsRll5Xhf/5nBgBg9erv4ve//w/OPfdVeDwxFBUpcPvtU3HZZeV97ovlllvOxKJF70KjWYMLLyzBO+9chRUrpmPWrFfB5/OwdGkN5ykGAHPnWjB5sh6FhWvB5/Pg8dyZ9XgXX1yKhx46D9dcsxl+fwKzZ5uxceP8XF/aU4LuDfFsJaharYbBYBiwIV4qlaK8vBzNzc2gaZqzeuqgabi0AAAFJ0lEQVQLiUSCaDQKIJMqzMvLyxIM5iQ7L9A0nTUlhaIo7vUpKCg4ZdOuBMJ44qQ3sF944UYsXlyDW28962QeBmEESaVS8Hq9OTXEJ5NJNDc3Q6PRID8/H8lkEslkssdM1FQqxUV5PB4P5eXlWe7zgUAAbrcbVqv1hDRtMwyDeDzOCR07+7Vr8ziJ5AiEUwvS8EMYcYRCIdcQ39nZCYfDAT6fzw3G7k0IWDuopqYmJJNJhEKZVPikSZOythcKhdDpdPB6vZBKpVmixzovjLYlEkVRWc3jAoGAGzEnl8tP+5FoBMJYhwgfYdTg8zPO91qtFqFQCB6PB+3t7dDr9dBqtT0EQigUori4GA0NDdzfRyIRKBTZRT9sc31BQUHW710uF1QqFeRyOUaS7unLVCrFVV8ajUaSviQQxhgnPdVJGF/EYjF4PB6Ew2FuMDYrHAzDoLa2FhR1vHVDpVLBYulpRJpKpbImlIyk8wJJXxIIpzdE+AgnhWQyCa/Xi87OTigUChgMBkilUvh8PnR2diIej3PVmtXV1f2KGfOtBZJcLh9yT2AymcyK6oRCIRQKBedqQNKXBMLpAxE+wkklnU7D7/dj714bVqz4Gq2tUTz88PlYvnwqlx61WCwDphNpmu51XY+maTidTmg0mqyUaTqdRiQS4cQulUpxEZ1cLifpSwLhNIYIH+GU4OabP4BUCvziFxVcW0MuDfG9kU6nuaHYKpUKBoOBi+ji8Tjy8vI4sZNKpSR9SSCME0hxC+GUoKUliOuvnwSr1YpoNMoVwuh0Ouj1BgiFuaUaKYpCY2Mjt14YDAYRj8ehVCqRn58PuVxOPOoIhHEKifgIJ525c1/HJ5/YIRLxIRTyceWVVqjVEjQ1dWLXLgc2bboSFosat9/+Efbt60BRkQKPPHIBrryyAgBw443vQyYToqkpgF27HJgyJR9r1szAn/+8D5s3O6DXS/DYY1Mwf/60k9LUTiAQTi3IJS/hpPOvf12HCy4owtNPfw/h8D0QiwV49dUj+N//nY1Q6B7Mnl2MK654G5deWga3+w6sWfM9/PjHW3Hs2PExZ2+8UYuHHz4fHs+dkEgEuOaaHZg9uxSHDl2L73+/CI8+ehSJROIkPksCgXCqQISPcEqyYEEFzjuvCHw+D/v2uREOJ7Fy5TkQiwWYO9eC+fOteO21I9z2CxdW4OyzCyGVCrFwYSWkUiFuv30mSkqKcfvts1BbG4FaPXZmoRIIhNGDCB/hlKSrFZXTGUZJScb7kKW0VAWH47ifn9F4vGk9L08Io1GW9XM4nBzlIyYQCGMFInyEU5KuFZZmswKtrSHQ9PHl6JaW4JiwcSIQCKceRPgIpzznnGOCTCbCo4/uAUWlsXNnC7ZsacD110862YdGIBDGIET4CKc8YrEAW7YsxPvvN8Fg+CvuuONjvPjiPEyadOqYzBIIhLEDaWcgEAgEwriCRHwEAoFAGFcQ4SMQCATCuIIIH4FAIBDGFUT4CAQCgTCuIMJHIBAIhHEFET4CgUAgjCuI8BEIBAJhXEGEj0AgEAjjCiJ8BAKBQBhXEOEjEAgEwriCCB+BQCAQxhVE+AgEAoEwriDCRyAQCIRxBRE+AoFAIIwriPARCAQCYVxBhI9AIBAI4woifAQCgUAYVxDhIxAIBMK4gggfgUAgEMYVRPgIBAKBMK4gwkcgEAiEcQURPgKBQCCMK4jwEQgEAmFcQYSPQCAQCOMKInwEAoFAGFcQ4SMQCATCuIIIH4FAIBDGFf8fWeQcGvYD6W8AAAAASUVORK5CYII=\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "nx.draw(g, **options)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Unsupervised Keywords Extraction" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "from gowpy.summarization.unsupervised import GoWKeywordExtractor" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "extractor_kw = GoWKeywordExtractor(directed=False, window_size=4)" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "15" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "preprocessed_text = \"\"\"gowpy simple framework exploiting graph-of-words nlp gowpy \n", + "leverages graph-of-words representation document classification keyword extraction \n", + "document\"\"\"\n", + "len(preprocessed_text.split())" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[('gowpy', 4),\n", + " ('simple', 4),\n", + " ('framework', 4),\n", + " ('exploiting', 4),\n", + " ('graph-of-words', 4),\n", + " ('nlp', 4)]" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "extractor_kw.extract(preprocessed_text)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Classification with TW-IDF: a graph-based term weighting score" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [], + "source": [ + "from gowpy.feature_extraction.gow import TwidfVectorizer" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [], + "source": [ + "corpus = [\n", + " 'hello world !',\n", + " 'foo bar'\n", + "]" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [], + "source": [ + "vectorizer_gow = TwidfVectorizer( \n", + " # Graph-of-words specificities\n", + " directed=True,\n", + " window_size=4,\n", + " # Token frequency filtering\n", + " min_df=0.0,\n", + " max_df=1.0,\n", + " # Graph-based term weighting approach\n", + " term_weighting='degree'\n", + ")\n", + "\n", + "X = vectorizer_gow.fit_transform(corpus)" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([[0.89442719, 0. , 0. , 0. , 0.4472136 ],\n", + " [0. , 1. , 0. , 0. , 0. ]])" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "X.toarray()" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "<2x5 sparse matrix of type ''\n", + "\twith 3 stored elements in Compressed Sparse Row format>" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "X" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [], + "source": [ + "from sklearn.pipeline import Pipeline\n", + "from sklearn.svm import SVC\n", + "\n", + "from sklearn.model_selection import GridSearchCV\n", + "\n", + "pipeline = Pipeline([\n", + " ('gow', TwidfVectorizer()),\n", + " ('svm', SVC()),\n", + "])\n", + "\n", + "parameters = {\n", + " 'gow__directed' : [True, False],\n", + " 'gow__window_size' : [2, 4, 8, 16],\n", + " 'gow__b' : [0.0, 0.003],\n", + " 'gow__term_weighting' : ['degree', 'pagerank'],\n", + " 'gow__min_df' : [0, 5, 10],\n", + " 'gow__max_df' : [0.8, 0.9, 1.0],\n", + "#\n", + " 'svm__C' : [0.1, 1, 10],\n", + " 'svm__kernel' : ['linear']\n", + "}\n", + "\n", + "# find the best parameters for both the feature extraction and the\n", + "# classifier\n", + "grid_search = GridSearchCV(pipeline, \n", + " parameters, \n", + " cv=10,\n", + " n_jobs=-1)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Going further: classification based on frequent subgraphs" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Conversion of the corpus into a collection of graph-of-words" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [], + "source": [ + "from gowpy.gow.miner import GoWMiner\n", + "import gowpy.gow.io" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [], + "source": [ + "corpus = [\n", + " 'hello world !',\n", + " 'foo bar',\n", + " # and many more...\n", + "]" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [], + "source": [ + "gow_miner = GoWMiner(directed=False, window_size=4)\n", + "corpus_gows = gow_miner.compute_gow_from_corpus(corpus)" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [], + "source": [ + "with open(\"corpus_gows.data\", \"w\") as f_output:\n", + " data = gowpy.gow.io.gow_to_data(corpus_gows)\n", + " f_output.write(data)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Mining the frequent subgraphs" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Graph-of-word miner:\n", + " - is_directed: False\n", + " - window_size: 4\n", + " - edge_labeling: True\n", + "\n", + " - Number of tokens: 5\n", + " - Number of links between tokens: 4\n", + "\n", + " - Number of loaded subgraph: 13\n", + " " + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "gow_miner.load_graphs('gbolt-mining-corpus_gow.t0', \n", + " 'gbolt-mining-corpus_gow.nodes')\n", + "gow_miner" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Classification with frequent subgraphs" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "count 13.0\n", + "mean 0.5\n", + "std 0.0\n", + "min 0.5\n", + "25% 0.5\n", + "50% 0.5\n", + "75% 0.5\n", + "max 0.5\n", + "dtype: float64" + ] + }, + "execution_count": 25, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import pandas as pd\n", + "s_freq_per_pattern = pd.Series(gow_miner.stat_relative_freq_per_pattern())\n", + "s_freq_per_pattern.describe()" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "<2x13 sparse matrix of type ''\n", + "\twith 13 stored elements in Compressed Sparse Row format>" + ] + }, + "execution_count": 26, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from gowpy.feature_extraction.gow import GoWVectorizer\n", + "\n", + "vectorizer_gow = GoWVectorizer(gow_miner)\n", + "X = vectorizer_gow.fit_transform(corpus)\n", + "X" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": {}, + "outputs": [], + "source": [ + "feature_names = vectorizer_gow.get_feature_names()" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Nombre de features: 10\n", + "\t- hello world hello__world\n", + "\t- hello world ! world__! hello__world\n", + "\t- hello world ! world__! hello__world hello__!\n", + "\t- hello world ! hello__world hello__!\n", + "\t- hello ! hello__!\n", + "\t- hello world ! world__! hello__!\n", + "\t- world ! world__!\n", + "\t- hello\n", + "\t- !\n", + "\t- world\n" + ] + } + ], + "source": [ + "features = [feature for presence, feature in zip(X.toarray()[0], feature_names) if presence > 0]\n", + "print(\"Nombre de features: {}\".format(len(features)))\n", + "for feature in features:\n", + " print(f'\\t- {feature}')" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": {}, + "outputs": [], + "source": [ + "from sklearn.pipeline import Pipeline\n", + "from sklearn.svm import SVC\n", + "from sklearn.feature_extraction.text import TfidfTransformer\n", + "\n", + "from sklearn.model_selection import GridSearchCV" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": {}, + "outputs": [], + "source": [ + "pipeline = Pipeline([\n", + " ('gow', GoWVectorizer(gow_miner)),\n", + " ('tfidf', TfidfTransformer()),\n", + " ('svm', SVC()),\n", + "])\n", + "\n", + "parameters = {\n", + " 'gow__subgraph_matching' : ['partial', 'induced'],\n", + " 'gow__min_df' : [0.00833, 0.01, 0.013333],\n", + " 'gow__max_df' : [0.022778, 0.25, 0.5, 1.0],\n", + "#\n", + " 'svm__C' : [0.1, 1, 10],\n", + " 'svm__kernel' : ['linear']\n", + "}\n", + "\n", + "# find the best parameters for both the feature extraction and the\n", + "# classifier\n", + "grid_search = GridSearchCV(pipeline, \n", + " parameters, \n", + " cv=10,\n", + " n_jobs=-1)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python [conda env:gowpy36]", + "language": "python", + "name": "conda-env-gowpy36-py" + }, + "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.6.10" + }, + "toc": { + "base_numbering": 1, + "nav_menu": {}, + "number_sections": true, + "sideBar": true, + "skip_h1_title": false, + "title_cell": "Table of Contents", + "title_sidebar": "Contents", + "toc_cell": false, + "toc_position": {}, + "toc_section_display": true, + "toc_window_display": true + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/gowpy/__init__.py b/gowpy/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gowpy/feature_extraction/__init__.py b/gowpy/feature_extraction/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gowpy/feature_extraction/gow/__init__.py b/gowpy/feature_extraction/gow/__init__.py new file mode 100644 index 0000000..bc2fa07 --- /dev/null +++ b/gowpy/feature_extraction/gow/__init__.py @@ -0,0 +1,2 @@ +from .gow_vectorizer import GoWVectorizer +from .tw_vectorizer import TwVectorizer, TwidfVectorizer diff --git a/gowpy/feature_extraction/gow/gow_vectorizer.py b/gowpy/feature_extraction/gow/gow_vectorizer.py new file mode 100644 index 0000000..1918667 --- /dev/null +++ b/gowpy/feature_extraction/gow/gow_vectorizer.py @@ -0,0 +1,225 @@ +import networkx.algorithms.isomorphism as iso +from networkx.algorithms import isomorphism + +import numbers + +from scipy.sparse import csr_matrix + +from typing import Sequence, Tuple, Generator + +from gowpy.gow.builder import GraphOfWords +from gowpy.gow.typing import Nodes + +from sklearn.base import BaseEstimator +from gowpy.gow.miner import GoWMiner + +SUBGRAPH_MATCHING_INDUCED = "induced" +SUBGRAPH_MATCHING_PARTIAL = "partial" + + +class GoWVectorizer(BaseEstimator): + """Convert a collection of text documents to a matrix of frequent subgraphs matching counts + + Frequent subgraphs have to be mined before using this vectorizer. + + This implementation produces a sparse representation of the counts using + scipy.sparse.csr_matrix. + + Parameters + ---------- + graph_of_words: GoWMiner + A graph-of-words miner containing the frequent subgraphs. + max_df : float in range [0.0, 1.0] or int, default=1.0 + Ignore frequent subgraphs that have a document frequency strictly + higher than the given threshold (corpus-specific frequent subgraphs). + If float, the parameter represents a proportion of documents, integer + absolute counts. + min_df : float in range [0.0, 1.0] or int, default=0.0 + Ignore frequent subgraphs that have a document frequency strictly + lower than the given threshold. This value is also called support + in the literature. + Note that the smallest value is defined with the support used when + mining frequent subgraphs. + If float, the parameter represents a proportion of documents, integer + absolute counts. + in the mapping should not be repeated and should not have any gap + between 0 and the largest index. + indexing : boolean, True by default + Frequent subgraphs are indexed for faster retrieval when computing + document features. + subgraph_matching : string {'induced', 'partial'} + Frequent subgraph matching approach + 'partial' (default) : subgraph matching corresponding to node and + edge inclusion. + 'induced' : slower approach, node-induced subgraph matching + """ + + def __init__(self, + graph_of_words: GoWMiner, + min_df: float = 0.0, + max_df: float = 1.0, + subgraph_matching: str = SUBGRAPH_MATCHING_PARTIAL, + indexing: bool = True): + self.graph_of_words = graph_of_words + + # Subgraph mining patterns + self.min_df: float = min_df + self.max_df: float = max_df + if self.min_df < 0.0: + raise ValueError("min_df is smaller than 0%") + + if self.max_df < 0.0: + raise ValueError("max_df is smaller than 0%") + + self.subgraph_matching: str = subgraph_matching + + if self.graph_of_words is None: + raise ValueError("No provided graph-of-words miner to compute features (graph_of_words is None)") + + self.indexing = indexing + + def __compute_subpatterns(self) -> Sequence[Tuple[int, GraphOfWords]]: + # Filtering patterns out by support + if self.graph_of_words is not None: + max_doc_count = (self.max_df / float(self.graph_of_words.corpus_size) + if isinstance(self.max_df, numbers.Integral) + else self.max_df) + min_doc_count = (self.min_df / float(self.graph_of_words.corpus_size) + if isinstance(self.min_df, numbers.Integral) + else self.min_df) + # Selecting subpatterns + subpatterns = [subgraph for subgraph in self.graph_of_words.frequent_subgraphs + if (float(subgraph.freq) / float(self.graph_of_words.corpus_size)) >= min_doc_count + if (float(subgraph.freq) / float(self.graph_of_words.corpus_size)) <= max_doc_count + ] + else: + subpatterns = [] + + if self.indexing: + # Indexing patterns by node codes + self.node_code_to_feature_i_s_ = {} + for feature_i, subgraph in enumerate(subpatterns): + for node_code in subgraph.nodes: + if node_code not in self.node_code_to_feature_i_s_: + self.node_code_to_feature_i_s_[node_code] = set() + + self.node_code_to_feature_i_s_[node_code].add(feature_i) + + return [(i, subgraph) for i, subgraph in enumerate(subpatterns)] + + def fit(self, raw_documents: Sequence[str], y=None): + self.selected_subpatterns_: Sequence[Tuple[int, GraphOfWords]] = self.__compute_subpatterns() + self.node_matcher_ = iso.categorical_node_match('label', -1) + + return self + + def fit_transform(self, raw_documents: Sequence[str], y=None): + self.fit(raw_documents, y) + return self.transform(raw_documents) + + def __get_probable_features_via_nodes(self, document_nodes: Nodes) -> Generator[ + Tuple[int, GraphOfWords], None, None]: + subpatterns = self.selected_subpatterns_ + + feature_i_s = set() + for node_code in document_nodes: + if node_code in self.node_code_to_feature_i_s_: + # Getting the feature indices in which the node code appears + temp_feature_i_s = self.node_code_to_feature_i_s_[node_code] + feature_i_s.update(temp_feature_i_s) + + for feature_i in sorted(feature_i_s): + _, subgraph = subpatterns[feature_i] + yield (feature_i, subgraph) + + def __iterate_over_features(self, document_nodes: Nodes) -> Generator[Tuple[int, GraphOfWords], None, None]: + if self.indexing: + return self.__get_probable_features_via_nodes(document_nodes) + else: + subpatterns = self.selected_subpatterns_ + return subpatterns + + def __is_iso_induced(self, + feature_gow: GraphOfWords, + document_gow: GraphOfWords) -> bool: + is_iso = False + document_nodes = document_gow.nodes + document_edges = document_gow.edges + + # optimisation: + # checking nodes and edges inclusion in document before running + # subgraph matching algorithms + # + if (feature_gow.nodes.issubset(document_nodes)) and \ + (feature_gow.edges.issubset(document_edges)): + if len(feature_gow.nodes) <= 2: + is_iso = True + else: + document_graph = document_gow.to_graph() + feature_graph = feature_gow.to_graph() + GM = isomorphism.GraphMatcher(document_graph, feature_graph, + node_match=self.node_matcher_) + is_iso = GM.subgraph_is_isomorphic() + + return is_iso + + @staticmethod + def __is_iso_partial(feature_gow: GraphOfWords, + document_gow: GraphOfWords) -> bool: + return (feature_gow.nodes.issubset(document_gow.nodes)) and \ + (feature_gow.edges.issubset(document_gow.edges)) + + def transform(self, raw_documents: Sequence[str]): + indptr = [0] + indices = [] + data = [] + + subpatterns = self.selected_subpatterns_ + temp_num_features = len(subpatterns) + + if temp_num_features > 0: + for document in raw_documents: + # Document to gowpy + document_gow = self.graph_of_words.compute_gow_from_document(document) + if self.subgraph_matching == SUBGRAPH_MATCHING_INDUCED: + # Feature computation + retained_features = [i_feature + for i_feature, feature_gow in self.__iterate_over_features(document_gow.nodes) + if self.__is_iso_induced(feature_gow, document_gow) + ] + else: + # Feature computation + retained_features = [i_feature + for i_feature, feature_gow in self.__iterate_over_features(document_gow.nodes) + if GoWVectorizer.__is_iso_partial(feature_gow, document_gow) + ] + + # Building blocks of the sparse matrix + for i_feature in retained_features: + indices.append(i_feature) + data.append(1) + indptr.append(len(indices)) + + resulting_matrix = csr_matrix((data, indices, indptr), dtype=int) + else: + resulting_matrix = csr_matrix((len(raw_documents), 0)) + return resulting_matrix + + def get_feature_names(self) -> Sequence[str]: + feature_names = [] + + subpatterns = self.selected_subpatterns_ + + for _, subgraph in subpatterns: + temp = [] + for n in subgraph.nodes_str(): + temp.append(n) + for e in subgraph.edges_str(): + temp.append(e) + + feature_names.append(' '.join(temp)) + + return feature_names + + def _more_tags(self): + return {'X_types': ['string']} diff --git a/gowpy/feature_extraction/gow/tw_vectorizer.py b/gowpy/feature_extraction/gow/tw_vectorizer.py new file mode 100644 index 0000000..6b518ba --- /dev/null +++ b/gowpy/feature_extraction/gow/tw_vectorizer.py @@ -0,0 +1,427 @@ +import networkx as nx +from networkx.algorithms.link_analysis.pagerank_alg import pagerank_numpy +from networkx.algorithms.centrality import degree_centrality, closeness_centrality, betweenness_centrality + +from typing import Sequence, Dict + +from gowpy.gow.builder import GoWBuilder, Tokenized_document +from gowpy.gow.typing import Tokenizer +from gowpy.utils.defaults import default_tokenizer + +from sklearn.base import BaseEstimator +from sklearn.feature_extraction.text import TfidfTransformer +from sklearn.pipeline import Pipeline + +from operator import itemgetter +import numbers +from collections import defaultdict + +import numpy as np +import scipy.sparse as sp + + +TERM_WEIGHT_DEGREE = "degree" +TERM_WEIGHT_DEGREE_CENTRALITY = "degree_centrality" +TERM_WEIGHT_CLOSENESS_CENTRALITY = "closeness_centrality" +TERM_WEIGHT_BETWEENNESS_CENTRALITY = "betweenness_centrality" +TERM_WEIGHT_PAGERANK = "pagerank" + + +# +# From: https://github.com/scikit-learn/scikit-learn/blob/95d4f0841d57e8b5f6b2a570312e9d832e69debc/sklearn/feature_extraction/text.py#L820 +# +def _document_frequency(X): + """Count the number of non-zero values for each feature in sparse X.""" + if sp.isspmatrix_csr(X): + return np.bincount(X.indices, minlength=X.shape[1]) + else: + return np.diff(X.indptr) + + +class TwVectorizer(BaseEstimator): + """Convert a collection of text documents to a matrix of graph-based weight for each token + + This implementation produces a sparse representation of the counts using + scipy.sparse.csr_matrix. + + Parameters + ---------- + max_df : float in range [0.0, 1.0] or int, default=1.0 + Ignore tokens that have a document frequency strictly + higher than the given threshold (corpus-specific stop words). + If float, the parameter represents a proportion of documents, integer + absolute counts. + min_df : float in range [0.0, 1.0] or int, default=1 + Ignore frequent subgraphs that have a document frequency strictly + lower than the given threshold. This value is also called support + in the literature. + If float, the parameter represents a proportion of documents, integer + absolute counts. + in the mapping should not be repeated and should not have any gap + between 0 and the largest index. + b : float {0.0, 0.003}, default=0.0 + Slope parameter of the tilting. + directed : boolean, True by default + If True, the graph-of-words is directed, else undirected + window_size : int, default=4 + Size of the window (in token) to build the graph-of-words. + term_weighting : string {'degree', 'degree_centrality', 'closeness_centrality', 'betweenness_centrality', 'pagerank'} + Graph-based term weighting approach for the nodes in the graph-of-words + 'degree' (default) : degree (undirected) or indegree (directed) of the nodes. + 'degree_centrality' : normalized degree centrality of the nodes + 'closeness_centrality' : very slow, closeness centrality of the nodes + 'betweenness_centrality' : very slow, the shortest-path betweenness centrality of the nodes + 'pagerank' : slow, the PageRank of the nodes + tokenizer : callable or None (default) + Override the string tokenization step. + """ + def __init__(self, + min_df: float = 0.0, + max_df: float = 1.0, + b: float = 0.0, + directed: bool = True, + window_size: int = 4, + term_weighting: str = TERM_WEIGHT_DEGREE, + tokenizer: Tokenizer = None): + # Subgraph mining patterns + self.min_df: float = min_df + self.max_df: float = max_df + if self.min_df < 0.0: + raise ValueError("min_df is smaller than 0%") + + if self.max_df < 0.0: + raise ValueError("max_df is smaller than 0%") + + self.term_weighting = term_weighting + + self.b = b + + self.tokenizer: Tokenizer = tokenizer if tokenizer is not None else default_tokenizer + + self.window_size = window_size + if self.window_size < 2: + raise ValueError("window_size < 2") + + self.directed = directed + + def __tw(self, tokens: Tokenized_document) -> Dict[str, int]: + """Computes the graph-based weight for each token of the document""" + gow = self.gow_builder_.compute_gow_from_tokenized_document(tokens) + graph = gow.to_graph() + tw = {} + if self.term_weighting == TERM_WEIGHT_DEGREE: + if graph.is_directed(): + dgraph = nx.DiGraph(graph) + for (node, degree) in dgraph.in_degree(graph.nodes): + token = self.gow_builder_.get_token_(node) + tw[token] = degree + else: + for (node, degree) in graph.degree(graph.nodes): + token = self.gow_builder_.get_token_(node) + tw[token] = degree + else: + degree_centrality, closeness_centrality, betweenness_centrality + if self.term_weighting == TERM_WEIGHT_DEGREE_CENTRALITY: + weighting_fct = degree_centrality + elif self.term_weighting == TERM_WEIGHT_CLOSENESS_CENTRALITY: + weighting_fct = closeness_centrality + elif self.term_weighting == TERM_WEIGHT_BETWEENNESS_CENTRALITY: + weighting_fct = betweenness_centrality + elif self.term_weighting == TERM_WEIGHT_PAGERANK: + weighting_fct = pagerank_numpy + else: + weighting_fct = lambda x: 1 + + if graph.is_directed(): + dgraph = nx.DiGraph(graph) + node_to_weight = weighting_fct(dgraph) + for (node, p) in node_to_weight.items(): + token = self.gow_builder_.get_token_(node) + tw[token] = p + else: + node_to_weight = weighting_fct(graph) + for (node, p) in node_to_weight.items(): + token = self.gow_builder_.get_token_(node) + tw[token] = p + return tw + + # + # Largely inspired by the CountVectorizer from scikit-learn + # See: https://github.com/scikit-learn/scikit-learn/blob/95d4f0841d57e8b5f6b2a570312e9d832e69debc/sklearn/feature_extraction/text.py#L1113 + # + def __count_vocab(self, tokenized_documents: Sequence[Tokenized_document], fixed_vocab: bool): + if fixed_vocab: + vocabulary = self.vocabulary_ + else: + vocabulary = defaultdict() + vocabulary.default_factory = vocabulary.__len__ + + j_indices = [] + indptr = [0] + data = [] + + for tokens in tokenized_documents: + feature_counter = {} + + tw = self.__tw(tokens) + + document_length = len(tokens) + denominator = 1.0 - self.b + self.b * (float(document_length) / self.avdl_) + + for feature in tokens: + try: + feature_idx = vocabulary[feature] + + if feature_idx not in feature_counter: + feature_counter[feature_idx] = tw[feature] / denominator + + except KeyError: + # Ignore out-of-vocabulary items for fixed_vocab=True + continue + + j_indices.extend(feature_counter.keys()) + data.extend(feature_counter.values()) + indptr.append(len(j_indices)) + + # disable defaultdict behaviour + if not fixed_vocab: + # disable defaultdict behaviour + vocabulary = dict(vocabulary) + if not vocabulary: + raise ValueError("empty vocabulary; perhaps the documents only" + " contain stop words") + + X = sp.csr_matrix((data, j_indices, indptr), + shape=(len(indptr) - 1, len(vocabulary)), + dtype=float) + + X.sort_indices() + + return vocabulary, X + + # + # Inspired by: https://github.com/scikit-learn/scikit-learn/blob/95d4f0841d57e8b5f6b2a570312e9d832e69debc/sklearn/feature_extraction/text.py#L1058 + # + def __sort_features(self, X, vocabulary): + """Sort features by name + Returns a reordered matrix and modifies the vocabulary in place + """ + sorted_features = sorted(vocabulary.items()) + map_index = np.empty(len(sorted_features), dtype=X.indices.dtype) + for new_val, (term, old_val) in enumerate(sorted_features): + vocabulary[term] = new_val + map_index[old_val] = new_val + + X.indices = map_index.take(X.indices, mode='clip') + return X + + # + # Inspired by: https://github.com/scikit-learn/scikit-learn/blob/95d4f0841d57e8b5f6b2a570312e9d832e69debc/sklearn/feature_extraction/text.py#L1072 + # + def __limit_features(self, X, vocabulary, high=None, low=None): + """Remove too rare or too common features. + Prune features that are non zero in more samples than high or less + documents than low, modifying the vocabulary, and restricting it to + at most the limit most frequent. + This does not prune samples with zero features. + """ + if high is None and low is None: + return X, set() + + # Calculate a mask based on document frequencies + dfs = _document_frequency(X) + mask = np.ones(len(dfs), dtype=bool) + if high is not None: + mask &= dfs <= high + if low is not None: + mask &= dfs >= low + + new_indices = np.cumsum(mask) - 1 # maps old indices to new + removed_terms = set() + for term, old_index in list(vocabulary.items()): + if mask[old_index]: + vocabulary[term] = new_indices[old_index] + else: + del vocabulary[term] + removed_terms.add(term) + kept_indices = np.where(mask)[0] + if len(kept_indices) == 0: + raise ValueError("After pruning, no terms remain. Try a lower" + " min_df or a higher max_df.") + return X[:, kept_indices], removed_terms + + def fit(self, raw_documents: Sequence[str], y=None): + self.fit_transform(raw_documents, y) + return self + + def fit_transform(self, raw_documents: Sequence[str], y=None): + max_df = self.max_df + min_df = self.min_df + + self.gow_builder_ = GoWBuilder(window_size=self.window_size, + directed=self.directed, + tokenizer=self.tokenizer) + N = len(raw_documents) + self.N_ = N + + avdl = 0.0 + tokenized_documents = [] + for document in raw_documents: + tok_document = self.tokenizer(document) + tokenized_documents.append(tok_document) + avdl += len(tok_document) + + avdl = avdl / float(N) + self.avdl_ = avdl + + vocabulary, X = self.__count_vocab(tokenized_documents, fixed_vocab=False) + X = self.__sort_features(X, vocabulary) + + max_doc_count = (max_df + if isinstance(max_df, numbers.Integral) + else max_df * N) + min_doc_count = (min_df + if isinstance(min_df, numbers.Integral) + else min_df * N) + + X, self.stop_words_ = self.__limit_features(X, vocabulary, max_doc_count, min_doc_count) + + self.vocabulary_ = vocabulary + + return X + + def transform(self, raw_documents: Sequence[str]): + _, X = self.__count_vocab([self.tokenizer(doc) for doc in raw_documents], fixed_vocab=True) + + return X + + def get_feature_names(self) -> Sequence[str]: + return [t for t, i in sorted(self.vocabulary_.items(), key=itemgetter(1))] + + def _more_tags(self): + return {'X_types': ['string']} + + +class TwidfVectorizer(BaseEstimator): + """Convert a collection of text documents to a TW-IDF matrix + + Equivalent to :class:`TwVectorizer` followed by + :class:`TfidfTransformer`. + + This implementation produces a sparse representation of the counts using + scipy.sparse.csr_matrix. + + Parameters + ---------- + max_df : float in range [0.0, 1.0] or int, default=1.0 + Ignore tokens that have a document frequency strictly + higher than the given threshold (corpus-specific stop words). + If float, the parameter represents a proportion of documents, integer + absolute counts. + min_df : float in range [0.0, 1.0] or int, default=1 + Ignore frequent subgraphs that have a document frequency strictly + lower than the given threshold. This value is also called support + in the literature. + If float, the parameter represents a proportion of documents, integer + absolute counts. + in the mapping should not be repeated and should not have any gap + between 0 and the largest index. + b : float {0.0, 0.003}, default=0.0 + Slope parameter of the tilting. + directed : boolean, True by default + If True, the graph-of-words is directed, else undirected + window_size : int, default=4 + Size of the window (in token) to build the graph-of-words. + term_weighting : string {'degree', 'degree_centrality', 'closeness_centrality', 'betweenness_centrality', 'pagerank'} + Graph-based term weighting approach for the nodes in the graph-of-words + 'degree' (default) : degree (undirected) or indegree (directed) of the nodes. + 'degree_centrality' : normalized degree centrality of the nodes + 'closeness_centrality' : very slow, closeness centrality of the nodes + 'betweenness_centrality' : very slow, the shortest-path betweenness centrality of the nodes + 'pagerank' : slow, the PageRank of the nodes + tokenizer : callable or None (default) + Override the string tokenization step. + norm : 'l1', 'l2' or None, optional (default='l2') + Each output row will have unit norm, either: + * 'l2': Sum of squares of vector elements is 1. The cosine + similarity between two vectors is their dot product when l2 norm has + been applied. + * 'l1': Sum of absolute values of vector elements is 1. + See :func:`preprocessing.normalize` + use_idf : boolean (default=True) + Enable inverse-document-frequency reweighting. + smooth_idf : boolean (default=True) + Smooth idf weights by adding one to document frequencies, as if an + extra document was seen containing every term in the collection + exactly once. Prevents zero divisions. + """ + + def __init__(self, + min_df: float = 0.0, + max_df: float = 1.0, + b: float = 0.0, + directed: bool = True, + window_size: int = 4, + term_weighting: str = TERM_WEIGHT_DEGREE, + tokenizer: Tokenizer = None, + # + norm='l2', + use_idf=True, + smooth_idf=True): + # Subgraph mining patterns + self.min_df: float = min_df + self.max_df: float = max_df + if self.min_df < 0.0: + raise ValueError("min_df is smaller than 0%") + + if self.max_df < 0.0: + raise ValueError("max_df is smaller than 0%") + + self.term_weighting = term_weighting + + self.b = b + + self.tokenizer: Tokenizer = tokenizer if tokenizer is not None else default_tokenizer + + self.window_size = window_size + if self.window_size < 2: + raise ValueError("window_size < 2") + + self.directed = directed + + self.norm = norm + self.use_idf = use_idf + self.smooth_idf = smooth_idf + + def fit(self, raw_documents: Sequence[str], y=None): + self.fit_transform(raw_documents, y) + return self + + def fit_transform(self, raw_documents: Sequence[str], y=None): + self.tw_vectorizer_ = TwVectorizer( + min_df=self.min_df, + max_df=self.max_df, + b=self.b, + directed=self.directed, + window_size=self.window_size, + term_weighting=self.term_weighting, + tokenizer=self.tokenizer) + self.tfidf_transformer_ = TfidfTransformer( + norm=self.norm, + use_idf=self.use_idf, + smooth_idf=self.smooth_idf) + + self.pipeline_ = Pipeline([ + ('tw', self.tw_vectorizer_), + ('idf', self.tfidf_transformer_) + ]) + return self.pipeline_.fit_transform(raw_documents, y) + + def transform(self, raw_documents: Sequence[str]): + return self.pipeline_.transform(raw_documents) + + def get_feature_names(self) -> Sequence[str]: + return [t for t, i in sorted(self.tw_vectorizer_.vocabulary_.items(), key=itemgetter(1))] + + def _more_tags(self): + return {'X_types': ['string']} diff --git a/gowpy/gow/__init__.py b/gowpy/gow/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gowpy/gow/builder.py b/gowpy/gow/builder.py new file mode 100644 index 0000000..83fedbd --- /dev/null +++ b/gowpy/gow/builder.py @@ -0,0 +1,264 @@ +from typing import Sequence, Dict, Optional, Union, List, Callable + +from gowpy.gow.typing import Token, Tokenized_document, Tokenizer, \ + Edge, Edge_with_code, Edge_label, Edges, Nodes +import networkx as nx +from gowpy.utils.defaults import default_tokenizer + + +def mk_undirected_edge(node_start_code: int, + node_end_code: int, + code: Optional[int] = None) -> Union[Edge, Edge_with_code]: + """Builds an unambiguous representation of an undirected edge""" + if node_start_code < node_end_code: + n1, n2, label = node_start_code, node_end_code, code + else: + n1, n2, label = node_end_code, node_start_code, code + + if code is None: + return n1, n2 + else: + return n1, n2, label + + +def mk_directed_edge(node_start_code: int, + node_end_code: int, + code: Optional[int] = None) -> Union[Edge, Edge_with_code]: + """Builds an unambiguous representation of a directed edge""" + if code is None: + return node_start_code, node_end_code + else: + return node_start_code, node_end_code, code + + +class GraphOfWords(object): + """ + Represents a graph-of-words + + .. seealso:: gowpy.gow.builder.GoWBuilder + .. note:: this class should not be used directly, see GoWBuilder + """ + def __init__(self, + nodes: Nodes, + edges: Edges, + get_token: Callable[[int], str], + get_label: Optional[Callable[[int], Edge_label]], + freq: int = 1, + directed: bool = False): + self.get_token = get_token + self.get_label = get_label + + self.nodes = nodes + self.edges = edges + self.directed = directed + self.freq = freq + + self.graph_: Optional[nx.Graph] = None + + def is_edge_labeling(self): + return self.get_label is not None + + def __str__(self): + nodes = self.nodes_str() + edges = self.edges_str() + return """Graph-of-words\nNodes: {}\nEdges: {}\n""".format(nodes, edges) + + def __repr__(self): + return self.__str__() + + def nodes_str(self) -> List[str]: + return [self.get_token(node_code) for node_code in self.nodes] + + def __edges_to_str(self, edge: Edge) -> str: + start_node, end_node = edge + return f'{self.get_token(start_node)}__{self.get_token(end_node)}' + + def edges_str(self) -> List[str]: + if self.is_edge_labeling(): + return [self.__edges_to_str(self.get_label(edge_label_code)) + for _, _, edge_label_code in self.edges] + else: + return [self.__edges_to_str(mk_directed_edge(node_start, node_end) if self.directed else mk_undirected_edge(node_start, node_end)) + for node_start, node_end in self.edges] + + def to_graph(self) -> nx.Graph: + """Computes and memoize a NetworkX representation + + This representation is suited for algorithms rather than visualisation. + """ + if self.graph_ is None: + g = nx.Graph() if not self.directed else nx.DiGraph() + + [g.add_node(node, label=node) for node in self.nodes] + + if self.is_edge_labeling(): + [g.add_edge(node_start_code, node_end_code, label=edge_code) + for node_start_code, node_end_code, edge_code in self.edges] + else: + g.add_edges_from(self.edges) + + self.graph_ = g + + return self.graph_ + + def to_labeled_graph(self) -> nx.Graph: + """Computes a NetworkX representation suited for drawing""" + g = nx.Graph() if not self.directed else nx.DiGraph() + + [g.add_node(self.get_token(node)) for node in self.nodes] + + if self.is_edge_labeling(): + [g.add_edge(self.get_token(node_start_code), self.get_token(node_end_code)) + for node_start_code, node_end_code, _ in self.edges] + else: + g.add_edges_from([(self.get_token(node_start_code), self.get_token(node_end_code)) + for node_start_code, node_end_code in self.edges]) + + return g + + +class GoWBuilder(object): + """Builder to construct graph-of-words from a single document or a corpus of documents + + Parameters + ---------- + directed : boolean, False by default + If True, the graph-of-words is directed, else undirected + window_size : int, default=4 + Size of the window (in token) to build the graph-of-words. + edge_labeling : boolean, False by default + If True, edges are labeled with a unique code, else edges are not labeled. + tokenizer : callable or None (default) + Override the string tokenization step. + """ + + def __init__(self, + directed: bool = False, + window_size: int = 4, + tokenizer: Tokenizer = None, + edge_labeling: bool = False): + # Graph parameters + self.directed: bool = directed + self.window_size: int = window_size + + self.corpus_size: Optional[int] = None + + self.tokenizer: Tokenizer = tokenizer if tokenizer is not None else default_tokenizer + + self.TOKEN_TO_INT_: Dict[Token, int] = {} + self.INT_TO_TOKEN_: Dict[int, Token] = {} + + self.edge_labeling = edge_labeling + if self.edge_labeling: + self.LABEL_TO_INT_: Dict[Edge_label, int] = {} + self.INT_TO_LABEL_: Dict[int, Edge_label] = {} + + # TODO generate a real formal python representation + def __repr__(self): + return f'''Graph-of-word builder: + - is_directed: {self.directed} + - window_size: {self.window_size} + - edge_labeling: {self.edge_labeling} + + - Number of tokens: {len(self.TOKEN_TO_INT_)} + - Number of links between tokens: {len(self.LABEL_TO_INT_)} + '''.lstrip() + + def __str__(self): + return self.__repr__() + + # Node + def get_code_(self, token: Token) -> int: + if token not in self.TOKEN_TO_INT_: + last_token_id_ = len(self.TOKEN_TO_INT_) + self.TOKEN_TO_INT_[token] = last_token_id_ + self.INT_TO_TOKEN_[last_token_id_] = token + + return self.TOKEN_TO_INT_[token] + + def get_token_(self, code: int) -> Token: + return self.INT_TO_TOKEN_[code] + + # Edge + def get_label_id_(self, label: Edge_label) -> int: + if label not in self.LABEL_TO_INT_: + last_label_id_ = len(self.LABEL_TO_INT_) + self.LABEL_TO_INT_[label] = last_label_id_ + self.INT_TO_LABEL_[last_label_id_] = label + + return self.LABEL_TO_INT_[label] + + def get_label_(self, code: int) -> Edge_label: + return self.INT_TO_LABEL_[code] + + def get_edge_code_(self, edge: Edge) -> int: + node_start_code, node_end_code = edge + # Computation of the edge label and edge label ID + if self.directed: + t1, t2 = (node_start_code, node_end_code) + else: + if node_start_code < node_end_code: + t1, t2 = (node_start_code, node_end_code) + else: + t1, t2 = (node_end_code, node_start_code) + + edge_label = (t1, t2) + + edge_code = self.get_label_id_(edge_label) + + return edge_code + + def compute_gow_from_corpus(self, raw_documents: Sequence[str]) -> Sequence[GraphOfWords]: + """Computes a graph-of-words representation for each given documents""" + result_graph_of_words = [] + + for raw_document in raw_documents: + gow = self.compute_gow_from_document(raw_document) + result_graph_of_words.append(gow) + + self.corpus_size = len(result_graph_of_words) + + return result_graph_of_words + + def compute_gow_from_tokenized_document(self, tokens: Tokenized_document) -> GraphOfWords: + nodes = set() + token_ids = [] + for token in tokens: + token_id = self.get_code_(token) + token_ids.append(token_id) + nodes.add(token_id) + + N = len(tokens) + + edges = set() + if self.edge_labeling: + for j in range(N): + for i in range(max(j - self.window_size + 1, 0), j): + # Only keep edges between two *different* tokens + if token_ids[i] != token_ids[j]: + edge = (token_ids[i], token_ids[j]) + edge_code = self.get_edge_code_(edge) + if self.directed: + edges.add(mk_directed_edge(token_ids[i], token_ids[j], edge_code)) + else: + edges.add(mk_undirected_edge(token_ids[i], token_ids[j], edge_code)) + else: + for j in range(N): + for i in range(max(j - self.window_size + 1, 0), j): + # Only keep edges between two *different* tokens + if token_ids[i] != token_ids[j]: + if self.directed: + edges.add(mk_directed_edge(token_ids[i], token_ids[j])) + else: + edges.add(mk_undirected_edge(token_ids[i], token_ids[j])) + + return GraphOfWords(nodes=nodes, + edges=edges, + get_label=self.get_label_ if self.edge_labeling else None, + get_token=self.get_token_, + directed=self.directed) + + def compute_gow_from_document(self, raw_document: str) -> GraphOfWords: + """Computes a graph-of-words representation from a document""" + tokens = self.tokenizer(raw_document) + return self.compute_gow_from_tokenized_document(tokens) diff --git a/gowpy/gow/io.py b/gowpy/gow/io.py new file mode 100644 index 0000000..7428d2a --- /dev/null +++ b/gowpy/gow/io.py @@ -0,0 +1,226 @@ +import re + +from typing import Tuple, List, Sequence, Callable + +from gowpy.gow.builder import mk_undirected_edge, mk_directed_edge +from gowpy.gow.builder import GraphOfWords +from gowpy.gow.typing import Edge_label + + +def gow_to_data(gows: Sequence[GraphOfWords]) -> str: + """ + Convert a sequence of graph-of-words into a text representation for interoperability with other programs + + Format: + - "t # N" means the Nth graph, + - "v M L" means that the Mth vertex in this graph has label L, + - "e P Q L" means that there is an edge connecting the Pth vertex with the Qth vertex. The edge has label L. + + :param gows: + :return: + """ + result_data = [] + + for i, gow in enumerate(gows): + nodes = gow.nodes + edges = gow.edges + + if len(nodes) > 0: + result_data.append(u"t # {}\n".format(i)) + + node_label_to_id = {} + + for node_label in nodes: + if not (node_label in node_label_to_id): + new_id = len(node_label_to_id) + node_label_to_id[node_label] = new_id + + node_id = node_label_to_id[node_label] + result_data.append(u"v {} {}\n".format(node_id, node_label)) + + edge_tuples = [] # TODO implementation with a heap to be more efficient? + for (node_start_label, node_end_label, edge_label_id) in edges: + # Computation of the node IDs in this graph given their node labels + node_start_id = node_label_to_id[node_start_label] + node_end_id = node_label_to_id[node_end_label] + + edge_tuples.append((node_start_id, node_end_id, edge_label_id)) + edge_tuples.sort() + + for node_start_id, node_end_id, edge_label_id in edge_tuples: + result_data.append(u"e {} {} {}\n".format(node_start_id, + node_end_id, + edge_label_id)) + + result_data.append(u"t # {}".format(-1)) + return u"".join(result_data) + + +r_new_graph_ = re.compile(u't +# +(\d+) +\\* +(\d+)') +r_new_vertex_ = re.compile(u'v +(\d+) +(\d+)') +r_new_edge_ = re.compile(u'e +(\d+) +(\d+) +(\d+)') +r_new_parent_graphs_ = re.compile(u'x: +([\d ]+)') + + +def load_graphs(input_file_subgraph: str, + input_file_frequent_nodes: str, + get_token: Callable[[int], str], + get_label: Callable[[int], Edge_label], + is_directed: bool=False) -> Sequence[GraphOfWords]: + # + current_id = None + current_freq = None + current_vertices = None + current_edges = None + current_parent_graph_ids = None + + subgraphs = [] + + with open(input_file_subgraph, 'r') as f_input_file: + for line in f_input_file: + m_new_graph = r_new_graph_.search(line) + m_new_vertex = r_new_vertex_.search(line) + m_new_edge = r_new_edge_.search(line) + m_new_parent_graphs = r_new_parent_graphs_.search(line) + + if m_new_graph: + # Saving + if current_id is not None: + subgraphs.append(_to_gow(current_id, + current_freq, + (current_vertices, current_edges), + current_parent_graph_ids, + get_token, get_label, + is_directed)) + + # Initialisation of the new graph + current_id = int(m_new_graph.group(1)) + current_freq = int(m_new_graph.group(2)) + current_vertices = [] + current_edges = [] + current_parent_graph_ids = None + + elif m_new_vertex: + vertex_id = int(m_new_vertex.group(1)) + vertex_label = int(m_new_vertex.group(2)) + + current_vertices.append((vertex_id, vertex_label)) + + elif m_new_edge: + node_start = int(m_new_edge.group(1)) + node_end = int(m_new_edge.group(2)) + edge_label = int(m_new_edge.group(3)) + + current_edges.append((node_start, node_end, edge_label)) + + elif m_new_parent_graphs: + current_parent_graph_ids = [int(graph_id) for graph_id in + m_new_parent_graphs.group(1).strip().split(' ')] + # assert len(current_parent_graph_ids) == current_freq + + else: + pass # other lines (probably empty) + + # Last line + if current_id and current_parent_graph_ids: + subgraphs.append( + _to_gow(current_id, current_freq, (current_vertices, current_edges), current_parent_graph_ids, + get_token, get_label, + is_directed)) + + current_id = None + PADDING_ID = len(subgraphs) + current_freq = None + current_vertices = None + current_edges = None + current_parent_graph_ids = None + + with open(input_file_frequent_nodes, 'r') as f_input_file: + for line in f_input_file: + m_new_graph = r_new_graph_.search(line) + m_new_vertex = r_new_vertex_.search(line) + m_new_parent_graphs = r_new_parent_graphs_.search(line) + + if m_new_graph: + # Saving + if current_id is not None: + subgraphs.append(_to_gow(current_id, + current_freq, + (current_vertices, current_edges), + current_parent_graph_ids, + get_token, get_label, + is_directed)) + + # Initialisation of the new graph + current_id = int(m_new_graph.group(1)) + PADDING_ID + current_freq = int(m_new_graph.group(2)) + current_vertices = [] + current_edges = [] + current_parent_graph_ids = None + + elif m_new_vertex: + vertex_id = int(m_new_vertex.group(1)) + vertex_label = int(m_new_vertex.group(2)) + + current_vertices.append((vertex_id, vertex_label)) + + elif m_new_parent_graphs: + current_parent_graph_ids = [int(graph_id) for graph_id in + m_new_parent_graphs.group(1).strip().split(' ')] + # assert len(current_parent_graph_ids) == current_freq + + else: + pass # other lines (probably empty) + + # Last line + if current_id and current_parent_graph_ids: + subgraphs.append( + _to_gow(current_id, current_freq, (current_vertices, current_edges), current_parent_graph_ids, + get_token, get_label, is_directed)) + + return subgraphs + +IO_Nodes = List[Tuple[int, int]] # (node_id, node_code) +IO_Edges = List[Tuple[int, int, int]] # (node_start_id, node_end_id, edge_code) +IO_Subgraph = Tuple[IO_Nodes, IO_Edges] + +def _to_gow(subg_id: int, + subg_freq: int, + subgraph: IO_Subgraph, + subg_current_parent_graph_ids: Sequence[int], + get_token: Callable[[int], str], + get_label: Callable[[int], Edge_label], + is_directed: bool) -> GraphOfWords: + id_: int = subg_id + freq: int = subg_freq + + subg_vertices, subg_edges = subgraph + + size = len(subg_vertices) + parents = subg_current_parent_graph_ids + + # Recomputation of nodes + # Dealing with nodes: + # Node = (node id in *this* graph, node code) + node_id_to_node_code = {} + nodes = set() + for node_id, node_code in subg_vertices: + node_id_to_node_code[node_id] = node_code + nodes.add(node_code) + + # Dealing with edges + edges = set() + for node_start_id, node_end_id, edge_label_code in subg_edges: + node_start_code = node_id_to_node_code[node_start_id] + node_end_code = node_id_to_node_code[node_end_id] + if is_directed: + edges.add(mk_directed_edge(node_start_code, node_end_code, edge_label_code)) + else: + edges.add(mk_undirected_edge(node_start_code, node_end_code, edge_label_code)) + + return GraphOfWords(nodes=nodes, + edges=edges, + get_token=get_token, + get_label=get_label, + freq=freq, + directed=is_directed) \ No newline at end of file diff --git a/gowpy/gow/miner.py b/gowpy/gow/miner.py new file mode 100644 index 0000000..0fccce4 --- /dev/null +++ b/gowpy/gow/miner.py @@ -0,0 +1,72 @@ +import numpy as np +from typing import Sequence, Optional + +import gowpy.gow.io +from gowpy.gow.builder import GraphOfWords +from gowpy.gow.typing import Tokenizer +from gowpy.gow.builder import GoWBuilder + + +class GoWMiner(GoWBuilder): + """A miner of frequent subgraphs for a collection of graph-of-words + + Currently, the mining operation is delegated to a C++ program. This class makes it possible to load the + mined sub-graphs-of-words. + + Parameters + ---------- + directed : boolean, False by default + If True, the graph-of-words is directed, else undirected + window_size : int, default=4 + Size of the window (in token) to build the graph-of-words. + tokenizer : callable or None (default) + Override the string tokenization step. + """ + + def __init__(self, + directed: bool = False, + window_size: int = 4, + tokenizer: Tokenizer = None): + # /!\ Edge labeling is important for IO + super().__init__(directed, window_size, tokenizer, edge_labeling=True) + self.frequent_subgraphs: Optional[Sequence[GraphOfWords]] = None + + # TODO generate a real formal python representation + def __repr__(self): + if self.frequent_subgraphs is None: + len_frequent_subgraphs = "not loaded yet" + else: + len_frequent_subgraphs = len(self.frequent_subgraphs) + return f'''Graph-of-word miner: + - is_directed: {self.directed} + - window_size: {self.window_size} + - edge_labeling: {self.edge_labeling} + + - Number of tokens: {len(self.TOKEN_TO_INT_)} + - Number of links between tokens: {len(self.LABEL_TO_INT_)} + + - Number of loaded subgraph: {len_frequent_subgraphs} + '''.lstrip() + + def load_graphs(self, + input_file_subgraph: str, + input_file_frequent_nodes: str) -> None: + self.frequent_subgraphs = gowpy.gow.io.load_graphs(input_file_subgraph, input_file_frequent_nodes, + self.get_token_, self.get_label_, + self.directed) + + def stat_freq_per_pattern(self) -> np.array: + """Computes the subgraph frequency series""" + return np.array([pattern.freq for pattern in self.frequent_subgraphs]) + + def stat_relative_freq_per_pattern(self) -> np.array: + """Computes the subgraph normalised frequency series""" + return np.array([pattern.freq / float(self.corpus_size) for pattern in self.frequent_subgraphs]) + + def stat_num_nodes_per_pattern(self) -> np.array: + """Computes the number of nodes per subgraph series""" + return np.array([len(pattern.nodes) for pattern in self.frequent_subgraphs]) + + def stat_num_edges_per_pattern(self) -> np.array: + """Computes the number of edges per subgraph series""" + return np.array([len(pattern.edges) for pattern in self.frequent_subgraphs]) diff --git a/gowpy/gow/typing.py b/gowpy/gow/typing.py new file mode 100644 index 0000000..325512a --- /dev/null +++ b/gowpy/gow/typing.py @@ -0,0 +1,12 @@ +from typing import Tuple, Callable, Sequence, Set, Union + +Token = str +Tokenized_document = Sequence[Token] +Tokenizer = Callable[[str], Tokenized_document] +Node = int +Nodes = Set[Node] + +Edge_label = Tuple[int, int] +Edge = Tuple[Node, Node] +Edge_with_code = Tuple[Node, Node, int] +Edges = Union[Set[Edge], Set[Edge_with_code]] diff --git a/gowpy/summarization/__init__.py b/gowpy/summarization/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gowpy/summarization/unsupervised/__init__.py b/gowpy/summarization/unsupervised/__init__.py new file mode 100644 index 0000000..50b2e0d --- /dev/null +++ b/gowpy/summarization/unsupervised/__init__.py @@ -0,0 +1 @@ +from .keyword_extractor_gow import GoWKeywordExtractor \ No newline at end of file diff --git a/gowpy/summarization/unsupervised/keyword_extractor_gow.py b/gowpy/summarization/unsupervised/keyword_extractor_gow.py new file mode 100644 index 0000000..27ca107 --- /dev/null +++ b/gowpy/summarization/unsupervised/keyword_extractor_gow.py @@ -0,0 +1,48 @@ +from typing import Sequence, Tuple + +from gowpy.gow.builder import GoWBuilder +from gowpy.gow.typing import Tokenizer + +from networkx.algorithms.core import core_number + + +class GoWKeywordExtractor(object): + """Extract keywords from a text document based on a graph-of-words representation + + Parameters + ---------- + directed : boolean, False by default + If True, the graph-of-words is directed, else undirected + window_size : int, default=4 + Size of the window (in token) to build the graph-of-words. + tokenizer : callable or None (default) + Override the string tokenization step. + """ + def __init__(self, + directed: bool = False, + window_size: int = 4, + tokenizer: Tokenizer = None): + # TODO is_weighted + self.builder = GoWBuilder( + directed=directed, + window_size=window_size, + tokenizer=tokenizer) + + def extract(self, document: str) -> Sequence[Tuple[str, float]]: + gow = self.builder.compute_gow_from_document(document) + graph = gow.to_graph() + kcore = core_number(graph) + + keywords = [] + k_max = 0 + for v, k in kcore.items(): + if k > k_max: + keywords.clear() + k_max = k + + if k == k_max: + token_code = graph.nodes[v]['label'] + token = self.builder.get_token_(token_code) + keywords.append((token, k)) + + return keywords diff --git a/gowpy/utils/__init__.py b/gowpy/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gowpy/utils/defaults.py b/gowpy/utils/defaults.py new file mode 100644 index 0000000..3b9447d --- /dev/null +++ b/gowpy/utils/defaults.py @@ -0,0 +1,5 @@ +from gowpy.gow.typing import Tokenized_document + + +def default_tokenizer(document: str) -> Tokenized_document: + return document.split() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..9110b1b --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +networkx>=2.4 +scikit-learn>=0.22.2 +matplotlib>=3.1 \ No newline at end of file diff --git a/resources/gow.png b/resources/gow.png new file mode 100644 index 0000000000000000000000000000000000000000..6dd3760a9e1f7552b437fd2a76041d2a4b055c75 GIT binary patch literal 47504 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 literal 0 HcmV?d00001 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..f577190 --- /dev/null +++ b/setup.py @@ -0,0 +1,32 @@ +from setuptools import setup, find_packages + +with open("requirements.txt") as f: + required = f.read().splitlines() + +setup( + name="gowpy", + version="0.1.0", + description="A very simple graph-of-words framework for NLP", + long_description=open("README.md", encoding="utf-8").read(), + long_description_content_type="text/markdown", + author="Guillaume Dubuisson Duplessis", + author_email="guillaume@dubuissonduplessis.fr", + url="https://github.com/GuillaumeDD/gowpy.git", + packages=find_packages(exclude="tests"), + license="new BSD", + install_requires=required, + include_package_data=True, + python_requires=">=3.6", + classifiers=[ + 'Development Status :: 3 - Alpha', + 'Intended Audience :: Developers', + 'Intended Audience :: Education', + 'Intended Audience :: Science/Research', + 'License :: OSI Approved :: BSD License', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Topic :: Software Development :: Libraries', + 'Topic :: Software Development :: Libraries :: Python Modules' + ], +)