From b6bf5b438134709dcb9736b1d8f3d88af8eed294 Mon Sep 17 00:00:00 2001 From: Po-Sen Huang Date: Mon, 29 Jan 2018 13:25:22 -0800 Subject: [PATCH 1/2] Initial commit a322972 --- CMakeLists.txt | 23 +- README.md | 255 ++--- data/prepare-iwslt14.sh | 5 +- data/prepare-iwslt15.sh | 34 + de-en_example.png | Bin 0 -> 116024 bytes fairseq/models/DummyCriterion.lua | 25 + fairseq/models/avgpool_model.lua | 11 +- fairseq/models/bgru_model.lua | 174 +++ fairseq/models/c_sample_dp.cc | 220 ++++ fairseq/models/compute_logpy.cu | 158 +++ fairseq/models/init.lua | 10 + fairseq/models/mRNN.lua | 254 +++++ fairseq/models/npmt.lua | 1143 +++++++++++++++++++ fairseq/models/npmt_model.lua | 598 ++++++++++ fairseq/models/npmt_utils.lua | 767 +++++++++++++ fairseq/models/utils.lua | 8 + fairseq/models/window_attn.lua | 128 +++ fairseq/torchnet/ResumableDPOptimEngine.lua | 25 +- fairseq/torchnet/hooks.lua | 37 +- generate-lines.lua | 26 +- generate.lua | 32 +- npmt.png | Bin 0 -> 161584 bytes rocks/fairseq-scm-1.rockspec | 2 +- train.lua | 144 ++- 24 files changed, 3872 insertions(+), 207 deletions(-) create mode 100755 data/prepare-iwslt15.sh create mode 100755 de-en_example.png create mode 100755 fairseq/models/DummyCriterion.lua create mode 100755 fairseq/models/bgru_model.lua create mode 100755 fairseq/models/c_sample_dp.cc create mode 100755 fairseq/models/compute_logpy.cu create mode 100755 fairseq/models/mRNN.lua create mode 100755 fairseq/models/npmt.lua create mode 100755 fairseq/models/npmt_model.lua create mode 100755 fairseq/models/npmt_utils.lua create mode 100755 fairseq/models/window_attn.lua create mode 100755 npmt.png diff --git a/CMakeLists.txt b/CMakeLists.txt index 925d58a..3b6093e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,12 +4,16 @@ # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. An additional grant # of patent rights can be found in the PATENTS file in the same directory. +# +# Copyright (c) Microsoft Corporation. All rights reserved +# Licensed under the BSD License -CMAKE_MINIMUM_REQUIRED(VERSION 2.6 FATAL_ERROR) -CMAKE_POLICY(VERSION 2.6) +CMAKE_MINIMUM_REQUIRED(VERSION 2.8 FATAL_ERROR) +CMAKE_POLICY(VERSION 2.8) FIND_PACKAGE(Torch REQUIRED) FIND_PACKAGE(OpenMP) +FIND_PACKAGE(CUDA REQUIRED) SET(CMAKE_CXX_FLAGS "-std=c++11 -Ofast") IF(OpenMP_FOUND) @@ -17,6 +21,13 @@ IF(OpenMP_FOUND) SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${OpenMP_CXX_FLAGS}") ENDIF() +SET(CUDA_NVCC_FLAGS "${CUDA_NVCC_FLAGS} -O3 -shared -Xcompiler -fPIC + -gencode arch=compute_30,code=sm_30 -gencode arch=compute_35,code=sm_35 + -gencode arch=compute_37,code=sm_37 -gencode arch=compute_50,code=sm_50 + -gencode arch=compute_52,code=sm_52 -gencode arch=compute_52,code=compute_52") + +INCLUDE_DIRECTORIES(${CMAKE_PREFIX_PATH}/include/THC) + # C++ library IF(APPLE) SET(CMAKE_SHARED_LIBRARY_SUFFIX ".so") @@ -25,6 +36,14 @@ FILE(GLOB CPPSRC fairseq/clib/*.cpp) ADD_LIBRARY(fairseq_clib SHARED ${CPPSRC}) INSTALL(TARGETS fairseq_clib DESTINATION "${ROCKS_LIBDIR}") +ADD_LIBRARY(dp_lib SHARED fairseq/models/c_sample_dp.cc) +INSTALL(TARGETS dp_lib DESTINATION "${ROCKS_LIBDIR}") + +CUDA_ADD_LIBRARY(compute_logpy_lib SHARED fairseq/models/compute_logpy.cu) +INSTALL(TARGETS compute_logpy_lib DESTINATION "${ROCKS_LIBDIR}") + +TARGET_LINK_LIBRARIES(compute_logpy_lib ${CUDA_LIBRARIES}) + # Lua library INSTALL(DIRECTORY "fairseq" DESTINATION "${ROCKS_LUADIR}" FILES_MATCHING PATTERN "*.lua") diff --git a/README.md b/README.md index d62c767..59cf395 100644 --- a/README.md +++ b/README.md @@ -1,46 +1,60 @@ # Introduction -This is fairseq, a sequence-to-sequence learning toolkit for [Torch](http://torch.ch/) from Facebook AI Research tailored to Neural Machine Translation (NMT). -It implements the convolutional NMT models proposed in [Convolutional Sequence to Sequence Learning](https://arxiv.org/abs/1705.03122) and [A Convolutional Encoder Model for Neural Machine Translation](https://arxiv.org/abs/1611.02344) as well as a standard LSTM-based model. -It features multi-GPU training on a single machine as well as fast beam search generation on both CPU and GPU. -We provide pre-trained models for English to French, English to German and English to Romanian translation. +This is NPMT, the source codes of [Towards Nerual Phrase-based Machine Translation](https://arxiv.org/abs/1706.05565) and [Sequence Modeling via Segmentations](https://arxiv.org/abs/1702.07463) from Microsoft Research. +It is built on top of the [fairseq toolkit](https://github.com/facebookresearch/fairseq) in [Torch](http://torch.ch/). +We present the setup and Neural Machine Translation (NMT) experiments in [Towards Nerual Phrase-based Machine Translation](https://arxiv.org/abs/1706.05565). -Note, there is now a PyTorch version [fairseq-py](https://github.com/facebookresearch/fairseq-py) of this toolkit and new development efforts will focus on it. +## NPMT +Neural Phrase-based Machine Translation (NPMT) explicitly models the phrase structures in output sequences using Sleep-WAke Networks (SWAN), a recently proposed segmentation-based sequence modeling method. +To mitigate the monotonic alignment requirement of SWAN, we introduce a new layer to perform (soft) local reordering of input sequences. +Different from existing neural machine translation (NMT) approaches, NPMT does not use attention-based decoding mechanisms. +Instead, it directly outputs phrases in a sequential order and can decode in linear time. + +Model architecture +![Example](npmt.png) + +An illustration of using NPMT in German-English translation +![Example](de-en_example.png) + + +Please refer to the [PR](TBD) for our implementations. Our implementation is based on the [lastest version](https://github.com/posenhuang/NPMT/commit/7d017f0a46a3cddfc420a4778d9541ba38b6a43d) of fairseq. -![Model](fairseq.gif) # Citation If you use the code in your paper, then please cite it as: ``` -@article{gehring2017convs2s, - author = {Gehring, Jonas, and Auli, Michael and Grangier, David and Yarats, Denis and Dauphin, Yann N}, - title = "{Convolutional Sequence to Sequence Learning}", - journal = {ArXiv e-prints}, - archivePrefix = "arXiv", - eprinttype = {arxiv}, - eprint = {1705.03122}, - primaryClass = "cs.CL", - keywords = {Computer Science - Computation and Language}, - year = 2017, - month = May, +@article{pshuang2018NPMT, + author = {Po{-}Sen Huang and + Chong Wang and + Sitao Huang and + Dengyong Zhou and + Li Deng}, + title = {Towards Neural Phrase-based Machine Translation}, + journal = {CoRR}, + volume = {abs/1706.05565}, + year = {2017}, + url = {http://arxiv.org/abs/1706.05565}, + archivePrefix = {arXiv}, + eprint = {1706.05565}, } ``` and ``` -@article{gehring2016convenc, - author = {Gehring, Jonas, and Auli, Michael and Grangier, David and Dauphin, Yann N}, - title = "{A Convolutional Encoder Model for Neural Machine Translation}", - journal = {ArXiv e-prints}, - archivePrefix = "arXiv", - eprinttype = {arxiv}, - eprint = {1611.02344}, - primaryClass = "cs.CL", - keywords = {Computer Science - Computation and Language}, - year = 2016, - month = Nov, +@inproceedings{wang2017SWAN, + author = {Chong Wang and + Yining Wang and + Po{-}Sen Huang and + Abdelrahman Mohamed and + Dengyong Zhou and + Li Deng}, + title = {Sequence Modeling via Segmentations}, + booktitle = {Proceedings of the 34th International Conference on Machine Learning, + {ICML} 2017, Sydney, NSW, Australia, 6-11 August 2017}, + pages = {3674--3683}, + year = {2017}, } ``` @@ -71,41 +85,6 @@ The LuaRocks installation provides a command-line tool that includes the followi # Quick Start -## Evaluating Pre-trained Models -First, download a pre-trained model along with its vocabularies: -``` -$ curl https://s3.amazonaws.com/fairseq/models/wmt14.en-fr.fconv-cuda.tar.bz2 | tar xvjf - -``` - -This will unpack vocabulary files and a serialized model for English to French translation to `wmt14.en-fr.fconv-cuda/`. - -Alternatively, use a CPU-based model: -``` -$ curl https://s3.amazonaws.com/fairseq/models/wmt14.en-fr.fconv-float.tar.bz2 | tar xvjf - -``` - -Let's use `fairseq generate-lines` to translate some text. -This model uses a [Byte Pair Encoding (BPE) vocabulary](https://arxiv.org/abs/1508.07909), so we'll have to apply the encoding to the source text. -This can be done with [apply_bpe.py](https://github.com/rsennrich/subword-nmt/blob/master/apply_bpe.py) using the `bpecodes` file in within `wmt14.en-fr.fconv-cuda/`. -`@@` is used as a continuation marker and the original text can be easily recovered with e.g. `sed s/@@ //g`. -Prior to BPE, input text needs to be tokenized using `tokenizer.perl` from [mosesdecoder](https://github.com/moses-smt/mosesdecoder). -Here, we use a beam size of 5: -``` -$ fairseq generate-lines -path wmt14.en-fr.fconv-cuda/model.th7 -sourcedict wmt14.en-fr.fconv-cuda/dict.en.th7 \ - -targetdict wmt14.en-fr.fconv-cuda/dict.fr.th7 -beam 5 -| [target] Dictionary: 44666 types -| [source] Dictionary: 44409 types -> Why is it rare to discover new marine mam@@ mal species ? -S Why is it rare to discover new marine mam@@ mal species ? -O Why is it rare to discover new marine mam@@ mal species ? -H -0.068684287369251 Pourquoi est-il rare de découvrir de nouvelles espèces de mammifères marins ? -A 1 1 4 4 6 6 7 11 9 9 9 12 13 -``` - -This generation script produces four types of output: a line prefixed with *S* shows the supplied source sentence after applying the vocabulary; *O* is a copy of the original source sentence; *H* is the hypothesis along with an average log-likelihood and *A* are attention maxima for each word in the hypothesis (including the end-of-sentence marker which is omitted from the text). - -Check [below](#pre-trained-models) for a full list of pre-trained models available. - ## Training a New Model ### Data Pre-processing @@ -123,118 +102,102 @@ $ fairseq preprocess -sourcelang de -targetlang en \ ``` This will write binarized data that can be used for model training to data-bin/iwslt14.tokenized.de-en. +We also provide an example of pre-processing script for the IWSLT15 English-Vietnamese corpus. +Pre-process and binarize the data as follows: +``` +$ cd data/ +$ bash prepare-iwslt15.sh +$ cd .. +$ TEXT=data/iwslt15 +$ fairseq preprocess -sourcelang en -targetlang vi \ + -trainpref $TEXT/train -validpref $TEXT/tst2012 -testpref $TEXT/tst2013 \ + -thresholdsrc 5 -thresholdtgt 5 -destdir data-bin/iwslt15.tokenized.en-vi +``` + ### Training Use `fairseq train` to train a new model. -Here a few example settings that work well for the IWSLT14 dataset: +Here a few example settings that work well for the IWSLT14, IWSLT15 datasets: ``` -# Standard bi-directional LSTM model -$ mkdir -p trainings/blstm -$ fairseq train -sourcelang de -targetlang en -datadir data-bin/iwslt14.tokenized.de-en \ - -model blstm -nhid 512 -dropout 0.2 -dropout_hid 0 -optim adam -lr 0.0003125 -savedir trainings/blstm - -# Fully convolutional sequence-to-sequence model -$ mkdir -p trainings/fconv -$ fairseq train -sourcelang de -targetlang en -datadir data-bin/iwslt14.tokenized.de-en \ - -model fconv -nenclayer 4 -nlayer 3 -dropout 0.2 -optim nag -lr 0.25 -clip 0.1 \ - -momentum 0.99 -timeavg -bptt 0 -savedir trainings/fconv - -# Convolutional encoder, LSTM decoder -$ mkdir -p trainings/convenc +# NPMT model (IWSLT DE-EN) +$ mkdir -p trainings/iwslt_de_en $ fairseq train -sourcelang de -targetlang en -datadir data-bin/iwslt14.tokenized.de-en \ - -model conv -nenclayer 6 -dropout 0.2 -dropout_hid 0 -savedir trainings/convenc + -sourcelang de -targetlang en -model npmt -nhid 256 -dec_unit_size 512 -dropout .5 \ + -dropout_hid 0 -npmt_dropout .5 -optim adam -lr 0.001 -batchsize 32 -log_interval 100 \ + -nlayer 2 -nenclayer 2 -kwidth 7 -max_segment_len 6 -rnn_mode GRU -group_size 500 \ + -use_resnet_enc -use_resnet_dec -log -momentum 0.99 -clip 10 -maxbatch 600 -bptt 0 \ + -maxepoch 100 -ndatathreads 4 -seed 1002 -maxsourcelen 75 -num_lower_win_layers 1 \ + -save_interval 250 -use_accel -noearlystop -validbleu -lrshrink 1.25 -minepochtoanneal 18 \ + -annealing_type slow -savedir trainings/iwslt_de_en + +# NPMT model (IWSLT EN-DE) +$ mkdir -p trainings/iwslt_en_de +$ fairseq train -sourcelang en -targetlang de -datadir data-bin/iwslt14.tokenized.en-de \ + -model npmt -nhid 256 -dec_unit_size 512 -dropout .5 -dropout_hid 0 -npmt_dropout .5 \ + -optim adam -lr 0.001 -batchsize 32 -log_interval 100 -nlayer 2 -nenclayer 2 -kwidth 7 \ + -max_segment_len 6 -rnn_mode GRU -group_size 500 -use_resnet_enc -use_resnet_dec \ + -log -momentum 0.99 -clip 10 -maxbatch 800 -bptt 0 -maxepoch 100 -ndatathreads 4 \ + -seed 1002 -maxsourcelen 75 -num_lower_win_layers 1 -save_interval 250 -use_accel \ + -noearlystop -validbleu -lrshrink 1.25 -minepochtoaneal 15 \ + -annealing_type slow -savedir trainings/iwslt_en_de + +# NPMT model (IWSLT EN-VI) +$ mkdir -p trainings/iwslt_en_vi +$ fairseq train -sourcelang en -targetlang vi -datadir data-bin/iwslt15.tokenized.en-vi \ + -model npmt -nhid 512 -dec_unit_size 512 -dropout .4 -dropout_hid 0 -npmt_dropout .4 \ + -optim adam -lr 0.001 -batchsize 48 -log_interval 100 -nlayer 3 -nenclayer 2 -kwidth 7 \ + -max_segment_len 7 -rnn_mode LSTM -group_size 800 -use_resnet_enc -use_resnet_dec -log \ + -momentum 0.99 -clip 500 -maxbatch 800 -bptt 0 -maxepoch 50 -ndatathreads 4 -seed 1002 \ + -maxsourcelen 75 -num_lower_win_layers 1 -save_interval 250 -use_accel -noearlystop \ + -validbleu -nembed 512 -lrshrink 1.25 -minepochtoanneal 8 -annealing_type slow \ + -savedir trainings/iwslt_en_vi ``` + By default, `fairseq train` will use all available GPUs on your machine. Use the [CUDA_VISIBLE_DEVICES](http://acceleware.com/blog/cudavisibledevices-masking-gpus) environment variable to select specific GPUs or `-ngpus` to change the number of GPU devices that will be used. ### Generation Once your model is trained, you can translate with it using `fairseq generate` (for binarized data) or `fairseq generate-lines` (for text). -Here, we'll do it for a fully convolutional model: +Here, we'll do it for a NPMT model: ``` -# Optional: optimize for generation speed -$ fairseq optimize-fconv -input_model trainings/fconv/model_best.th7 -output_model trainings/fconv/model_best_opt.th7 # Translate some text $ DATA=data-bin/iwslt14.tokenized.de-en $ fairseq generate-lines -sourcedict $DATA/dict.de.th7 -targetdict $DATA/dict.en.th7 \ - -path trainings/fconv/model_best_opt.th7 -beam 10 -nbest 2 -| [target] Dictionary: 24738 types -| [source] Dictionary: 35474 types -> eine sprache ist ausdruck des menschlichen geistes . -S eine sprache ist ausdruck des menschlichen geistes . -O eine sprache ist ausdruck des menschlichen geistes . -H -0.23804219067097 a language is expression of human mind . -A 2 2 3 4 5 6 7 8 9 -H -0.23861141502857 a language is expression of the human mind . -A 2 2 3 4 5 7 6 7 9 9 -``` + -path trainings/iwslt_de_en/model_bestbleu.th7 -beam 1 -model npmt +| [target] Dictionary: 22823 types +| [source] Dictionary: 32010 types +> danke , aber das beste kommt noch . +max decoding: | 1:184 1:15| 2:4| 3:28| 4:6 4:282| 6:16 6:201 6:311| 8:5| +avg. phrase size 1.666667 +S danke , aber das beste kommt noch . +O danke , aber das beste kommt noch . +H -0.10934638977051 thank you , but the best is still coming . +A 1 -### CPU Generation -Use `fairseq tofloat` to convert a trained model to use CPU-only operations (this has to be done on a GPU machine): ``` -# Optional: optimize for generation speed -$ fairseq optimize-fconv -input_model trainings/fconv/model_best.th7 -output_model trainings/fconv/model_best_opt.th7 +where the ``max decoding`` suggests the output segments are ``| thank you | , | but | the best | is still coming | . |``, and ``avg. phrase size`` represents the average phrase length ``10/6 = 1.666667``. -# Convert to float -$ fairseq tofloat -input_model trainings/fconv/model_best_opt.th7 \ - -output_model trainings/fconv/model_best_opt-float.th7 -# Translate some text -$ fairseq generate-lines -sourcedict $DATA/dict.de.th7 -targetdict $DATA/dict.en.th7 \ - -path trainings/fconv/model_best_opt-float.th7 -beam 10 -nbest 2 -> eine sprache ist ausdruck des menschlichen geistes . -S eine sprache ist ausdruck des menschlichen geistes . -O eine sprache ist ausdruck des menschlichen geistes . -H -0.2380430996418 a language is expression of human mind . -A 2 2 3 4 5 6 7 8 9 -H -0.23861189186573 a language is expression of the human mind . -A 2 2 3 4 5 7 6 7 9 9 +Generation with the binarized test sets can be run as follows (not in batched mode), e.g. for German-English: ``` -# Pre-trained Models - -We provide the following pre-trained fully convolutional sequence-to-sequence models: - -* [wmt14.en-fr.fconv-cuda.tar.bz2](https://s3.amazonaws.com/fairseq/models/wmt14.en-fr.fconv-cuda.tar.bz2): Pre-trained model for [WMT14 English-French](http://statmt.org/wmt14/translation-task.html#Download) including vocabularies -* [wmt14.en-fr.fconv-float.tar.bz2](https://s3.amazonaws.com/fairseq/models/wmt14.en-fr.fconv-float.tar.bz2): CPU version of the above -* [wmt14.en-de.fconv-cuda.tar.bz2](https://s3.amazonaws.com/fairseq/models/wmt14.en-de.fconv-cuda.tar.bz2): Pre-trained model for [WMT14 English-German](https://nlp.stanford.edu/projects/nmt) including vocabularies -* [wmt14.en-de.fconv-float.tar.bz2](https://s3.amazonaws.com/fairseq/models/wmt14.en-de.fconv-float.tar.bz2): CPU version of the above -* [wmt16.en-ro.fconv-cuda.tar.bz2](https://s3.amazonaws.com/fairseq/models/wmt16.en-ro.fconv-cuda.tar.bz2): Pre-trained model for WMT16 English-Romanian including vocabularies. - This model was trained on the [original WMT bitext](http://statmt.org/wmt16/translation-task.html#Download) as well as [back-translated data](http://data.statmt.org/rsennrich/wmt16_backtranslations/en-ro) provided by Rico Sennrich. -* [wmt16.en-ro.fconv-float.tar.bz2](https://s3.amazonaws.com/fairseq/models/wmt16.en-ro.fconv-float.tar.bz2): CPU version of the above - -In addition, we provide pre-processed and binarized test sets for the models above: - -* [wmt14.en-fr.newstest2014.tar.bz2](https://s3.amazonaws.com/fairseq/data/wmt14.en-fr.newstest2014.tar.bz2): newstest2014 test set for WMT14 English-French -* [wmt14.en-fr.ntst1213.tar.bz2](https://s3.amazonaws.com/fairseq/data/wmt14.en-fr.ntst1213.tar.bz2): newstest2012 and newstest2013 test sets for WMT14 English-French -* [wmt14.en-de.newstest2014.tar.bz2](https://s3.amazonaws.com/fairseq/data/wmt14.en-de.newstest2014.tar.bz2): newstest2014 test set for WMT14 English-German -* [wmt16.en-ro.newstest2014.tar.bz2](https://s3.amazonaws.com/fairseq/data/wmt16.en-ro.newstest2016.tar.bz2): newstest2016 test set for WMT16 English-Romanian - -Generation with the binarized test sets can be run in batch mode as follows, e.g. for English-French on a GTX-1080ti: -``` -$ curl https://s3.amazonaws.com/fairseq/data/wmt14.en-fr.newstest2014.tar.bz2 | tar xvjf - - -$ fairseq generate -sourcelang en -targetlang fr -datadir data-bin/wmt14.en-fr -dataset newstest2014 \ - -path wmt14.en-fr.fconv-cuda/model.th7 -beam 5 -batchsize 128 | tee /tmp/gen.out +$ fairseq generate -sourcelang de -targetlang en -datadir data-bin/iwslt14.tokenized.de-en \ + -path trainings/iwslt_de_en/model_bestbleu.th7 -beam 10 -lenpen 1 -dataset test -model npmt | tee /tmp/gen.out ... -| Translated 3003 sentences (95451 tokens) in 136.3s (700.49 tokens/s) -| Timings: setup 0.1s (0.1%), encoder 1.9s (1.4%), decoder 108.9s (79.9%), search_results 0.0s (0.0%), search_prune 12.5s (9.2%) -| BLEU4 = 43.43, 68.2/49.2/37.4/28.8 (BP=0.996, ratio=1.004, sys_len=92087, ref_len=92448) +| Translated 6750 sentences (137891 tokens) in 3013.7s (45.75 tokens/s) +| Timings: setup 10.7s (0.4%), encoder 28.2s (0.9%), decoder 2747.9s (91.2%), search_results 0.0s (0.0%), search_prune 0.0s (0.0%) +| BLEU4 = 29.92, 64.7/37.9/23.8/15.3 (BP=0.973, ratio=1.027, sys_len=127660, ref_len=131141) # Word-level BLEU scoring: $ grep ^H /tmp/gen.out | cut -f3- | sed 's/@@ //g' > /tmp/gen.out.sys $ grep ^T /tmp/gen.out | cut -f2- | sed 's/@@ //g' > /tmp/gen.out.ref $ fairseq score -sys /tmp/gen.out.sys -ref /tmp/gen.out.ref -BLEU4 = 40.55, 67.6/46.5/34.0/25.3 (BP=1.000, ratio=0.998, sys_len=81369, ref_len=81194) -``` +BLEU4 = 29.92, 64.7/37.9/23.8/15.3 (BP=0.973, ratio=1.027, sys_len=127660, ref_len=131141) -# Join the fairseq community +``` -* Facebook page: https://www.facebook.com/groups/fairseq.users -* Google group: https://groups.google.com/forum/#!forum/fairseq-users -* Contact: [jgehring@fb.com](mailto:jgehring@fb.com), [michaelauli@fb.com](mailto:michaelauli@fb.com) # License -fairseq is BSD-licensed. -The license applies to the pre-trained models as well. -We also provide an additional patent grant. +fairseq is BSD-licensed. The released codes modified the original fairseq are BSD-licensed. +The rest of the codes are MIT-licensed. diff --git a/data/prepare-iwslt14.sh b/data/prepare-iwslt14.sh index 2effe47..db28fcb 100644 --- a/data/prepare-iwslt14.sh +++ b/data/prepare-iwslt14.sh @@ -57,7 +57,10 @@ for l in $src $tgt; do perl $TOKENIZER -threads 8 -l $l > $tmp/$tok echo "" done -perl $CLEAN -ratio 1.5 $tmp/train.tags.$lang.tok $src $tgt $tmp/train.tags.$lang.clean 1 175 + +# Only use up to 50 as in https://github.com/facebookresearch/MIXER/blob/master/prepareData.sh +# and https://github.com/rizar/actor-critic-public +perl $CLEAN -ratio 1.5 $tmp/train.tags.$lang.tok $src $tgt $tmp/train.tags.$lang.clean 1 50 for l in $src $tgt; do perl $LC < $tmp/train.tags.$lang.clean.$l > $tmp/train.tags.$lang.$l done diff --git a/data/prepare-iwslt15.sh b/data/prepare-iwslt15.sh new file mode 100755 index 0000000..52d7938 --- /dev/null +++ b/data/prepare-iwslt15.sh @@ -0,0 +1,34 @@ +#!/bin/sh +# Copied from https://github.com/tensorflow/nmt/blob/master/nmt/scripts/download_iwslt15.sh +# +# Download small-scale IWSLT15 Vietnames to English translation data for NMT +# model training. +# +# Usage: +# ./download_iwslt15.sh path-to-output-dir +# +# If output directory is not specified, "./iwslt15" will be used as the default +# output directory. + +OUT_DIR="${1:-iwslt15}" +SITE_PREFIX="https://nlp.stanford.edu/projects/nmt/data" + +mkdir -v -p $OUT_DIR + +# Download iwslt15 small dataset from standford website. +echo "Download training dataset train.en and train.vi." +curl -o "$OUT_DIR/train.en" "$SITE_PREFIX/iwslt15.en-vi/train.en" +curl -o "$OUT_DIR/train.vi" "$SITE_PREFIX/iwslt15.en-vi/train.vi" + +echo "Download dev dataset tst2012.en and tst2012.vi." +curl -o "$OUT_DIR/tst2012.en" "$SITE_PREFIX/iwslt15.en-vi/tst2012.en" +curl -o "$OUT_DIR/tst2012.vi" "$SITE_PREFIX/iwslt15.en-vi/tst2012.vi" + +echo "Download test dataset tst2013.en and tst2013.vi." +curl -o "$OUT_DIR/tst2013.en" "$SITE_PREFIX/iwslt15.en-vi/tst2013.en" +curl -o "$OUT_DIR/tst2013.vi" "$SITE_PREFIX/iwslt15.en-vi/tst2013.vi" + +echo "Download vocab file vocab.en and vocab.vi." +curl -o "$OUT_DIR/vocab.en" "$SITE_PREFIX/iwslt15.en-vi/vocab.en" +curl -o "$OUT_DIR/vocab.vi" "$SITE_PREFIX/iwslt15.en-vi/vocab.vi" + diff --git a/de-en_example.png b/de-en_example.png new file mode 100755 index 0000000000000000000000000000000000000000..9da94596c0c19baca758087ed8d06662ba7ebcf3 GIT binary patch literal 116024 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%0DK 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 F@*f}R@Sp$y literal 0 HcmV?d00001 diff --git a/fairseq/models/DummyCriterion.lua b/fairseq/models/DummyCriterion.lua new file mode 100755 index 0000000..df1c624 --- /dev/null +++ b/fairseq/models/DummyCriterion.lua @@ -0,0 +1,25 @@ +-- Copyright (c) Microsoft Corporation. All rights reserved. +-- Licensed under the MIT License. +-- +--[[ +-- +-- Dummy Criterion +-- +--]] + +local DummyCriterion, parent = torch.class('nn.DummyCriterion', 'nn.Criterion') + +function DummyCriterion:__init() + parent.__init(self) +end + +function DummyCriterion:updateOutput(input, target) + self.output = torch.mean(input) + return self.output +end + +function DummyCriterion:updateGradInput(input, target) + local n = input:nElement() + self.gradInput = input.new(input:size()):fill(1.0/n) + return self.gradInput +end diff --git a/fairseq/models/avgpool_model.lua b/fairseq/models/avgpool_model.lua index 7ea1b93..d815ce1 100644 --- a/fairseq/models/avgpool_model.lua +++ b/fairseq/models/avgpool_model.lua @@ -5,6 +5,9 @@ -- the root directory of this source tree. An additional grant of patent rights -- can be found in the PATENTS file in the same directory. -- +-- Copyright (c) Microsoft Corporation. All rights reserved. +-- Licensed under the BSD License. +-- --[[ -- -- This model closely follows the conditional setup of rnn-lib v1, with -name @@ -101,7 +104,7 @@ AvgpoolModel.makeAttention = argcheck{ local encoderOutPooled, encoderOutSingle = encoderOut:split(2) -- Projection of previous hidden state onto source word space - local prevhProj = nn.Linear(config.nhid, config.nembed)(prevh) + local prevhProj = nn.Linear(config.dec_unit_size, config.nembed)(prevh) local decoderRep = nn.CAddTable()({prevhProj, input}) -- Compute scores (usually denoted with alpha) using a simple dot @@ -148,7 +151,7 @@ AvgpoolModel.makeDecoderRNN = argcheck{ local rnn = nn.CLSTM{ attention = attnmodule, inputsize = config.nembed, - hidsize = config.nhid, + hidsize = config.dec_unit_size, nlayer = config.nlayer, winitfun = function(network) rmutils.defwinitfun(network, config.init_range) @@ -171,8 +174,8 @@ AvgpoolModel.makeDecoderRNN = argcheck{ end local scaleHidden = nn.Identity() - if config.nhid ~= config.nembed then - scaleHidden = nn.Linear(config.nhid, config.nembed) + if config.dec_unit_size ~= config.nembed then + scaleHidden = nn.Linear(config.dec_unit_size, config.nembed) end local decoderRNNOut = scaleHidden( diff --git a/fairseq/models/bgru_model.lua b/fairseq/models/bgru_model.lua new file mode 100755 index 0000000..47a3e78 --- /dev/null +++ b/fairseq/models/bgru_model.lua @@ -0,0 +1,174 @@ +-- Copyright (c) 2017-present, Facebook, Inc. +-- All rights reserved. +-- +-- This source code is licensed under the license found in the LICENSE file in +-- the root directory of this source tree. An additional grant of patent rights +-- can be found in the PATENTS file in the same directory. +-- +-- Copyright (c) Microsoft Corporation. All rights reserved. +-- Licensed under the BSD License. +-- +--[[ +-- +-- This model uses a bi-directional LSTM encoder. The direction is reversed +-- between layers and two separate columns run in parallel: one on the normal +-- input and one on the reversed input (as described in +-- http://arxiv.org/abs/1606.04199). +-- +-- The attention mechanism and the decoder setup are identical to the avgpool +-- model. +-- +--]] + +require 'nn' +require 'rnnlib' +local usecudnn = pcall(require, 'cudnn') +local argcheck = require 'argcheck' +local mutils = require 'fairseq.models.utils' +local rutils = require 'rnnlib.mutils' + +local BGRUModel = torch.class('BGRUModel', 'AvgpoolModel') + +BGRUModel.makeEncoderColumn = argcheck{ + {name='self', type='BGRUModel'}, + {name='config', type='table'}, + {name='inith', type='nngraph.Node'}, + {name='input', type='nngraph.Node'}, + {name='nlayers', type='number'}, + call = function(self, config, inith, input, nlayers) + local rnnconfig = { + inputsize = config.nembed, + hidsize = config.nhid, + nlayer = 1, + winitfun = function(network) + rutils.defwinitfun(network, config.init_range) + end, + usecudnn = usecudnn, + } + + local rnn = nn.GRU(rnnconfig) + rnn.saveHidden = false + local output = nn.SelectTable(-1)(nn.SelectTable(2)( + rnn({inith, input}):annotate{name = 'encoderRNN'} + )) + rnnconfig.inputsize = config.nhid + + for i = 2, nlayers do + if config.dropout_hid > 0 then + output = nn.MapTable(nn.Dropout(config.dropout_hid))(output) + end + local rnn = nn.GRU(rnnconfig) + rnn.saveHidden = false + output = nn.SelectTable(-1)(nn.SelectTable(2)( + rnn({ + inith, + nn.ReverseTable()(output), + }) + )) + end + return output + end +} + +BGRUModel.makeEncoder = argcheck{ + doc=[[ +This encoder runs a forward and backward LSTM network and concatenates their +top-most hidden states. +]], + {name='self', type='BGRUModel'}, + {name='config', type='table'}, + call = function(self, config) + local sourceIn = nn.Identity()() + local inith, tokens = sourceIn:split(2) + + local dict = config.srcdict + local lut = mutils.makeLookupTable(config, dict:size(), + dict.pad_index) + local embed + if config.dropout_src > 0 then + embed = nn.MapTable(nn.Sequential() + :add(lut) + :add(nn.Dropout(config.dropout_src)))(tokens) + else + embed = nn.MapTable(lut)(tokens) + end + + local col1 = self:makeEncoderColumn{ + config = config, + inith = inith, + input = embed, + nlayers = config.nenclayer, + } + local col2 = self:makeEncoderColumn{ + config = config, + inith = inith, + input = nn.ReverseTable()(embed), + nlayers = config.nenclayer, + } + + -- Each column will switch direction between layers. Before merging, + -- they should both run in the same direction (here: forward). + if config.nenclayer % 2 == 0 then + col1 = nn.ReverseTable()(col1) + else + col2 = nn.ReverseTable()(col2) + end + + local prepare = nn.Sequential() + -- Concatenate forward and backward states + prepare:add(nn.JoinTable(2, 2)) + -- Scale down to nhid for further processing + prepare:add(nn.Linear(config.nhid * 2, config.nembed, false)) + -- Add singleton dimension for subsequent joining + prepare:add(nn.View(-1, 1, config.nembed)) + + local joinedOutput = nn.JoinTable(1, 2)( + nn.MapTable(prepare)( + nn.ZipTable()({col1, col2}) + ) + ) + if config.dropout_hid > 0 then + joinedOutput = nn.Dropout(config.dropout_hid)(joinedOutput) + end + + -- avgpool_model.makeDecoder() expects two encoder outputs, one for + -- attention score computation and the other one for applying them. + -- We'll just use the same output for both. + return nn.gModule({sourceIn}, { + joinedOutput, nn.Identity()(joinedOutput) + }) + end +} + +BGRUModel.prepareSource = argcheck{ + {name='self', type='BGRUModel'}, + call = function(self) + -- Device buffers for samples + local buffers = { + source = {}, + } + + -- NOTE: It's assumed that all encoders start from the same hidden + -- state. + local encoderRNN = mutils.findAnnotatedNode( + self:network(), 'encoderRNN' + ) + assert(encoderRNN ~= nil) + + return function(sample) + -- Encoder input + local source = {} + for i = 1, sample.source:size(1) do + buffers.source[i] = buffers.source[i] + or torch.Tensor():type(self:type()) + source[i] = mutils.sendtobuf(sample.source[i], + buffers.source[i]) + end + + local initialHidden = encoderRNN:initializeHidden(sample.bsz) + return {initialHidden, source} + end + end +} + +return BGRUModel diff --git a/fairseq/models/c_sample_dp.cc b/fairseq/models/c_sample_dp.cc new file mode 100755 index 0000000..ae2ed81 --- /dev/null +++ b/fairseq/models/c_sample_dp.cc @@ -0,0 +1,220 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +// +// Forward-backward probability computation using dynamic programming. +// +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +using namespace std; + +extern "C" { + #include "lua.h" + #include "lualib.h" + #include "lauxlib.h" +}; + +#define real double + +#define IND_LOGPY(i,j,k) ((i)*(T2+1)*(max_segment_len+1)+(j)*(max_segment_len+1)+(k)) +#define IND_ALPHA(i,j) ((i)*(T2+1)+(j)) +#define IND_BETA(i,j) ((i)*(T2+1)+(j)) + +#define LOGINF 1000000 +#define EPS 1e-10 +#define MAX(x,y) ((x)>(y))?(x):(y) + +inline real log1sub(real log_a) { + return log1p(-exp(log_a)); +} + +inline real logadd(real log_a, real log_b) { + return max(log_a, log_b) + log1p(exp(-abs(log_a-log_b))); +} + +void set_all(real* x, int start, int end, real val) { + for (int i = start; i < end; ++i) + x[i] = val; +} + +void in_place_softmax(real* x, int start, int end) { + real max_x = x[start]; + for (int i = start+1; i < end; ++i) { + max_x = max(max_x, x[i]); + } + real sum_x = 0.; + for (int i = start; i < end; ++i) { + x[i] = exp(x[i] - max_x); + sum_x += x[i]; + } + for (int i = start; i < end; ++i) { + x[i] /= sum_x; + } +} + +void in_place_cumsum(real* x, int start, int end) { + real sum_x = 0.0; + for (int i = start; i < end; ++i) { + sum_x += x[i]; + x[i] = sum_x; + } +} + +extern "C" { + static int c_sample_dp(lua_State*); + static int c_reverse_log_cumsum(lua_State*); + int luaopen_libdp_lib(lua_State*); +}; + +typedef struct { + int batch_size; + int T1; + int T2; + int max_segment_len; + real* logpy; + real* alpha; + real* beta; + real* seg_weight; + real* ylength; + real* xlength; +} strct_states; + +static void subprocess_c_sample_dp(strct_states* s, int p_batch) { + //default_random_engine generator; + //uniform_real_distribution distribution(0.0,1.0); + int batch_size = s->batch_size; + int T1 = s->T1; + int T2 = s->T2; + int max_segment_len = s->max_segment_len; + real* logpy = s->logpy + p_batch * T1 * (T2+1) * (max_segment_len+1); + real* alpha = s->alpha + p_batch * (T1+1) * (T2+1); + real* beta = s->beta + p_batch * (T1+1) * (T2+1); + real* seg_weight = s->seg_weight + p_batch * T1 * (T2+1) * (max_segment_len+1); + int ylength = (int)(s->ylength[p_batch]); + int xlength = (int)(s->xlength[p_batch]); + alpha[IND_ALPHA(0, 0)] = 0.; + for (int t = 1; t <= T1; ++t) { + for (int j = 0; j <= ylength; ++j) { + int j_low = max(1, j - max_segment_len + 1); + for (int j_start = j_low; j_start <= j+1; ++j_start) { + real prob = alpha[IND_ALPHA(t-1, j_start-1)] + logpy[IND_LOGPY(t-1, j_start-1, j-j_start+1)]; + alpha[IND_ALPHA(t, j)] = logadd(alpha[IND_ALPHA(t, j)], prob); + } + } + } + beta[IND_BETA(xlength, ylength)] = 0.; + for (int t = xlength-1; t >= 0; --t) { + for (int j = 0; j <= ylength; ++j) { + int j_high = min(ylength, j + max_segment_len); + for (int j_end = j; j_end <= j_high; ++j_end) { + real prob = beta[IND_BETA(t+1, j_end)] + logpy[IND_LOGPY(t, j, j_end-j)]; + beta[IND_BETA(t, j)] = logadd(beta[IND_BETA(t, j)], prob); + } + } + } + for (int t = 1; t <= T1; ++t) { + int jstart_l = max(1, ylength - (T1 - t + 1) * max_segment_len +1); + int jstart_u = min(ylength+1, (t - 1) * max_segment_len + 1); + for (int j_start = jstart_l; j_start <= jstart_u; j_start++) { + int j_len = min(max_segment_len, ylength-j_start+1); + int j_end = j_start + j_len - 1; + for (int j = j_start-1; j <= j_end; ++j) { + seg_weight[IND_LOGPY(t-1, j_start-1, j-j_start+1)] + = logpy[IND_LOGPY(t-1, j_start-1,j-j_start+1)] + + alpha[IND_ALPHA(t-1, j_start-1)] + + beta[IND_BETA(t, j)]; + } + } + } +} + +static int c_sample_dp(lua_State* L) { + strct_states s; + s.batch_size = (int)(lua_tonumber(L, 1)); + s.T1 = (int)(lua_tonumber(L, 2)); + s.T2 = (int)(lua_tonumber(L, 3)); + s.max_segment_len = (int)(lua_tonumber(L, 4)); + int num_thread = (int)(lua_tonumber(L, 5)); + s.logpy = (real*)((unsigned long long)(lua_tonumber(L, 6))); + s.alpha = (real*)((unsigned long long)(lua_tonumber(L, 7))); + s.beta = (real*)((unsigned long long)(lua_tonumber(L, 8))); + s.seg_weight = (real*)((unsigned long long)(lua_tonumber(L, 9))); + s.ylength = (real*)((unsigned long long)(lua_tonumber(L, 10))); + s.xlength = (real*)((unsigned long long)(lua_tonumber(L, 11))); + + int p = 0; + std::thread* ths = new std::thread[num_thread]; + while (p < s.batch_size) { + int p_ths = 0; + for(int i = 0; i < num_thread; i++) { + ths[p_ths++] = std::thread(subprocess_c_sample_dp, &s, p++); + if (p >= s.batch_size) break; + } + for(int i = 0; i < p_ths; i++) { + ths[i].join(); + } + } + delete[] ths; + return 0; +} + +static void subprocess_c_reverse_log_cumsum(strct_states* s, int p_batch) { + int batch_size = s->batch_size; + int T1 = s->T1; + int T2 = s->T2; + int max_segment_len = s->max_segment_len; + real* seg_weight = s->seg_weight + p_batch * T1 * (T2+1) * (max_segment_len+1); + int ylength = (int)(s->ylength[p_batch]); + for (int t = 1; t <= T1; ++t) { + int jstart_l = max(1, ylength - (T1 - t + 1) * max_segment_len +1); + int jstart_u = min(ylength+1, (t - 1) * max_segment_len + 1); + for (int j_start = jstart_l; j_start <= jstart_u; j_start++) { + int j_len = min(max_segment_len, ylength-j_start+1); + int j_end = j_start + j_len - 1; + for (int j = j_end-1; j >= j_start; --j) { + seg_weight[IND_LOGPY(t-1, j_start-1, j-j_start+1)] = logadd(seg_weight[IND_LOGPY(t-1, j_start-1, j-j_start+1)], + seg_weight[IND_LOGPY(t-1, j_start-1, j-j_start+2)]); + } + } + } +} + +static int c_reverse_log_cumsum(lua_State* L) { + strct_states s; + s.batch_size = (int)(lua_tonumber(L, 1)); + s.T1 = (int)(lua_tonumber(L, 2)); + s.T2 = (int)(lua_tonumber(L, 3)); + s.max_segment_len = (int)(lua_tonumber(L, 4)); + int num_thread = (int)(lua_tonumber(L, 5)); + s.seg_weight = (real*)((unsigned long long)(lua_tonumber(L, 6))); + s.ylength = (real*)((unsigned long long)(lua_tonumber(L, 7))); + + int p = 0; + std::thread* ths = new std::thread[num_thread]; + while (p < s.batch_size) { + int p_ths = 0; + for(int i = 0; i < num_thread; i++) { + ths[p_ths++] = std::thread(subprocess_c_reverse_log_cumsum, &s, p++); + if (p >= s.batch_size) break; + } + for(int i = 0; i < p_ths; i++) { + ths[i].join(); + } + } + delete[] ths; + return 0; + +} + +int luaopen_libdp_lib(lua_State* L) { + lua_register(L, "c_sample_dp", c_sample_dp); + lua_register(L, "c_reverse_log_cumsum", c_reverse_log_cumsum); + return 0; +} diff --git a/fairseq/models/compute_logpy.cu b/fairseq/models/compute_logpy.cu new file mode 100755 index 0000000..9c739c1 --- /dev/null +++ b/fairseq/models/compute_logpy.cu @@ -0,0 +1,158 @@ +/* Copyright (c) Microsoft Corporation. All rights reserved. + Licensed under the MIT License. + + CUDA implementation of the compute_logpy_post + +*/ + +#include +#include + +#include "THC.h" +#include "THCTensor.h" + +extern "C" { + #include "lua.h" + #include "luaT.h" + #include "lualib.h" + #include "lauxlib.h" +}; + +extern "C" { + static int compute_logpy_prep(lua_State* L); + static int compute_logpy_post(lua_State* L); + int luaopen_libcompute_logpy_lib(lua_State* L); +}; + +const float loginf = 1000000.0; + +template +T *getStoragePtr(lua_State* L, THCT * tct) +{ + T *ptr; + if (tct->storage) { + ptr = (T*)(tct->storage->data + tct->storageOffset); + } else { + lua_pushfstring(L, "THCudaTensor cannot be an empty tensor"); + lua_error(L); + } + return ptr; +} + +int compute_logpy_prep(lua_State* L) +{ + THCudaTensor *hidden_inputs_tensor = static_cast(luaT_checkudata(L, 1, "torch.CudaTensor")); + THCudaTensor *xlength_tensor = static_cast(luaT_checkudata(L, 2, "torch.CudaTensor")); + THCudaTensor *yref_tensor = static_cast(luaT_checkudata(L, 3, "torch.CudaTensor")); + THCudaTensor *ylength_tensor = static_cast(luaT_checkudata(L, 4, "torch.CudaTensor")); + + int batch_size = (int)(lua_tonumber(L, 5)); + int T1 = (int)(lua_tonumber(L, 6)); + int T2 = (int)(lua_tonumber(L, 7)); + + THCudaTensor *concat_hts_g_tensor = static_cast(luaT_checkudata(L, 8, "torch.CudaTensor")); + THCudaTensor *concat_inputs_g_tensor = static_cast(luaT_checkudata(L, 9, "torch.CudaTensor")); + + float *hidden_inputs = getStoragePtr(L, hidden_inputs_tensor); + float *xlength = getStoragePtr(L, xlength_tensor); + float *yref = getStoragePtr(L, yref_tensor); + float *ylength = getStoragePtr(L, ylength_tensor); + float *concat_hts_g = getStoragePtr(L, concat_hts_g_tensor); + float *concat_inputs_g = getStoragePtr(L, concat_inputs_g_tensor); + + + + return 0; +} + +__global__ void compute_logpy_post_kernel( float *t_prob_all, + float *yref, + float *ylength, + float *logpy, + int *sorted_schedule, + int s, + int max_jlen, + int vocab_size, + int batch_size, + int batch_max_segment_len, + int T1, + int T2, + int si) +{ + unsigned int idx = blockIdx.x * blockDim.x + threadIdx.x; + + int local_ylength = int(ylength[threadIdx.x]); + + int t = sorted_schedule[(blockIdx.x + si - 1) * 4]; + int j_start = sorted_schedule[(blockIdx.x + si - 1) * 4 + 1]; + int j_len = sorted_schedule[(blockIdx.x + si - 1) * 4 + 2]; + + float local_t_vec = 0; + + if (j_start <= (local_ylength + 1)) + { + local_t_vec = loginf + t_prob_all[idx * (max_jlen+1) * vocab_size + vocab_size - 1]; + + atomicAdd(&(logpy[threadIdx.x * T1 * (T2+1) * (batch_max_segment_len+1) + + (t-1) * (T2+1) * (batch_max_segment_len+1) + + (j_start-1) * (batch_max_segment_len+1) ]), + local_t_vec + ); + } + + float tmp_result = 0; + for (int i = 1; (i < j_len + 1) && (i + j_start <= (local_ylength+1)); i++) + { + tmp_result += t_prob_all[idx * (max_jlen+1) * vocab_size + (i-1) * vocab_size + int(yref[threadIdx.x * T2 + j_start + i - 2]) - 1]; + + local_t_vec = loginf + tmp_result + + t_prob_all[idx * (max_jlen+1) * vocab_size + (i) * vocab_size + vocab_size - 1]; + + atomicAdd(&(logpy[threadIdx.x * T1 * (T2+1) * (batch_max_segment_len+1) + + (t-1) * (T2+1) * (batch_max_segment_len+1) + + (j_start-1) * (batch_max_segment_len+1) + i ]), + local_t_vec + ); + } +} + +int compute_logpy_post(lua_State* L) +{ + THCudaTensor *t_prob_all_tensor = static_cast(luaT_checkudata(L, 1, "torch.CudaTensor")); + THCudaTensor *yref_tensor = static_cast(luaT_checkudata(L, 2, "torch.CudaTensor")); + THCudaTensor *ylength_tensor = static_cast(luaT_checkudata(L, 3, "torch.CudaTensor")); + THCudaTensor *logpy_tensor = static_cast(luaT_checkudata(L, 4, "torch.CudaTensor")); + THCudaIntTensor *sorted_schedule_tensor = static_cast(luaT_checkudata(L, 5, "torch.CudaIntTensor")); + + int s = (int)(lua_tonumber(L, 6)); + int max_jlen = (int)(lua_tonumber(L, 7)); + int vocab_size = (int)(lua_tonumber(L, 8)); + int batch_size = (int)(lua_tonumber(L, 9)); + int batch_max_segment_len = (int)(lua_tonumber(L, 10)); + int T1 = (int)(lua_tonumber(L, 11)); + int T2 = (int)(lua_tonumber(L, 12)); + int si = (int)(lua_tonumber(L, 13)); + + float *t_prob_all = getStoragePtr(L, t_prob_all_tensor); + float *yref = getStoragePtr(L, yref_tensor); + float *ylength = getStoragePtr(L, ylength_tensor); + float *logpy = getStoragePtr(L, logpy_tensor); + int *sorted_schedule = getStoragePtr(L, sorted_schedule_tensor); + + dim3 blockDim(batch_size); + dim3 gridDim(s); + compute_logpy_post_kernel<<>>(t_prob_all, yref, ylength, logpy, sorted_schedule, + s, max_jlen, vocab_size, batch_size, batch_max_segment_len, + T1, T2, si); + + cudaDeviceSynchronize(); + + return 0; +} + +int luaopen_libcompute_logpy_lib(lua_State* L) { + lua_register(L, "compute_logpy_prep", compute_logpy_prep); + lua_register(L, "compute_logpy_post", compute_logpy_post); + return 0; +} + diff --git a/fairseq/models/init.lua b/fairseq/models/init.lua index 62e099d..526327c 100644 --- a/fairseq/models/init.lua +++ b/fairseq/models/init.lua @@ -5,6 +5,9 @@ -- the root directory of this source tree. An additional grant of patent rights -- can be found in the PATENTS file in the same directory. -- +-- Copyright (c) Microsoft Corporation. All rights reserved. +-- Licensed under the BSD License. +-- --[[ -- -- init files for the models. @@ -14,7 +17,14 @@ require 'fairseq.models.model' require 'fairseq.models.avgpool_model' require 'fairseq.models.blstm_model' +require 'fairseq.models.bgru_model' require 'fairseq.models.fconv_model' require 'fairseq.models.selection_blstm_model' require 'fairseq.models.conv_model' require 'fairseq.models.ensemble_model' +require 'fairseq.models.npmt_model' +require 'fairseq.models.mRNN' +require 'fairseq.models.npmt' +require 'fairseq.models.npmt_utils' +require 'fairseq.models.DummyCriterion' +require 'fairseq.models.window_attn' \ No newline at end of file diff --git a/fairseq/models/mRNN.lua b/fairseq/models/mRNN.lua new file mode 100755 index 0000000..665a346 --- /dev/null +++ b/fairseq/models/mRNN.lua @@ -0,0 +1,254 @@ +-- Copyright (c) Microsoft Corporation. All rights reserved. +-- Licensed under the MIT License. +-- +--[[ +-- +-- Input: a tensor of input: eq_length * batch_size * input_dim or a table of h0 and input +-- where h0: num_layer * batch_size * hidden_dim; the first dimension optional if num_layer == 1 +-- Output: a tensor of eq_length * batch_size * output_dim +-- +--]] +-- + +require("nn") +require("cudnn") +require("cutorch") + +local mRNN, parent = torch.class('nn.mRNN', 'nn.Container') + +function mRNN:__init(input_dim, hidden_dim, bd, mode, has_otherOutput, use_resnet, use_skip_mode, dropout, add_output) + parent.__init(self) + local batchFirst = batchFirst or true + assert(batchFirst == true) + + if use_resnet then + print("resnet is used in current layer") + end + + self.bd = bd or false + self.mode = mode or "CUDNN_GRU" + self.has_otherOutput = has_otherOutput or false + self.dropout = dropout or 0 + self.add_output = add_output or false + self.use_resnet = use_resnet or false + + local rnn_hidden_dim = hidden_dim + if self.bd then + if self.add_output then + print("bd rnn output is added") + else + rnn_hidden_dim = rnn_hidden_dim / 2 + end + end + + self.rnn_hidden_dim = rnn_hidden_dim + local rnn = cudnn.RNN(input_dim, rnn_hidden_dim, 1, batchFirst) + if use_skip_mode then + assert(not self.bd and input_dim == hidden_dim) + rnn.inputMode = 'CUDNN_SKIP_INPUT' + end + self.rnn = rnn + + rnn.mode = self.mode + if self.bd then + rnn.numDirections = 2 + rnn.bidirectional = 'CUDNN_BIDIRECTIONAL' + end + rnn:reset() + self:add(rnn) + if use_resnet and input_dim ~= hidden_dim then + self.input_proj = nn.Bottle(nn.Linear(input_dim, hidden_dim, false)) + self:add(self.input_proj) + end +end + +function mRNN:setStates(h0, c0) + self.rnn.hiddenInput = h0:clone() + if c0 then + self.rnn.cellInput = c0:clone() + end +end + +function mRNN:getNextMemInput() + if self.rnn.cellOutput then + return self.rnn.cellOutput:clone() + else + return self.rnn.hiddenOutput:clone() + end +end + +function mRNN:updateOutput(input) + self.recompute_backward = true + local c0, h0, x + if torch.type(input) == "table" then + assert(not self.bd) + if #input == 2 then + h0, x = unpack(input) + if (h0:dim() == 2) then + h0 = h0:view(1, h0:size(1), h0:size(2)) + end + if self.mode == "CUDNN_LSTM" then + self.rnn.cellInput = h0 + else + self.rnn.hiddenInput = h0 + end + elseif #input == 3 then + c0, h0, x = unpack(input) + if (h0:dim() == 2) then + h0 = h0:view(1, h0:size(1), h0:size(2)) + end + if (c0:dim() == 2) then + c0 = c0:view(1, c0:size(1), c0:size(2)) + end + self.rnn.hiddenInput = h0 + self.rnn.cellInput = c0 + end + else + x = input + end + local rnn_output = self.rnn:updateOutput(x) + if self.bd and self.add_output then + rnn_output = torch.add(rnn_output[{{}, {}, {1,self.rnn_hidden_dim}}], + rnn_output[{{}, {}, {self.rnn_hidden_dim+1,2*self.rnn_hidden_dim}}]) + end + local output = rnn_output + if self.use_resnet then + if self.input_proj then + output = torch.add(output, self.input_proj:updateOutput(x)) + else + output = torch.add(output, x) + end + end + + if self.has_otherOutput then + local otherOutput + if self.mode == "CUDNN_LSTM" then + otherOutput = self.rnn.cellOutput:clone() + else + otherOutput = self.rnn.hiddenOutput:clone() + end + assert(otherOutput:dim() == 3) + if self.bd then + if self.add_output then + otherOutput = torch.add(otherOutput[1], otherOutput[2]) + else + otherOutput = torch.cat(otherOutput[1], otherOutput[2], 2) + end + else + assert(otherOutput:size(1) == 1) + otherOutput = otherOutput[1] + end + assert(otherOutput:dim() == 2) + self.output = {output, otherOutput} + else + self.output = output + end + return self.output +end + +function mRNN:backward(input, gradOutput, scale) + scale = scale or 1 + self.recompute_backward = false + if self.has_otherOutput then + local otherGradOutput + gradOutput, otherGradOutput = unpack(gradOutput) + assert(otherGradOutput:dim() == 2) + if self.bd then + local forwardGradOutput, backwardGradOutput + if self.add_output then + forwardGradOutput = otherGradOutput + backwardGradOutput = otherGradOutput + else + forwardGradOutput, backwardGradOutput = unpack(torch.chunk(otherGradOutput, 2, 2)) + end + otherGradOutput = torch.cat(forwardGradOutput:reshape(1, forwardGradOutput:size(1), forwardGradOutput:size(2)), + backwardGradOutput:reshape(1, backwardGradOutput:size(1), backwardGradOutput:size(2)), 1) + else + otherGradOutput = otherGradOutput:view(1, otherGradOutput:size(1), otherGradOutput:size(2)) + end + assert(otherGradOutput:dim() == 3) + if self.mode == "CUDNN_LSTM" then + self.rnn.gradCellOutput = otherGradOutput + else + self.rnn.gradHiddenOutput = otherGradOutput + end + end + + local h0, c0, x + if torch.type(input) == "table" then + if #input == 2 then + h0, x = unpack(input) + elseif #input == 3 then + c0, h0, x = unpack(input) + end + else + x = input + end + + local gradInput = x.new(x:size()):zero() + if self.use_resnet then + if self.input_proj then + gradInput:add(self.input_proj:backward(x, gradOutput)) + else + gradInput:add(gradOutput) + end + end + + if self.bd and self.add_output then + gradInput:add(self.rnn:backward(x, torch.cat(gradOutput, gradOutput, 3), scale)) + else + gradInput:add(self.rnn:backward(x, gradOutput, scale)) + end + + local h0_grad, c0_grad + if h0 and c0 then + h0_grad = self.rnn.gradHiddenInput + c0_grad = self.rnn.gradCellInput + self.gradInput = {c0_grad:view(h0:size()), h0_grad:view(c0:size()), gradInput} + elseif h0 then + if self.mode == "CUDNN_LSTM" then + h0_grad = self.rnn.gradCellInput + else + h0_grad = self.rnn.gradHiddenInput + end + self.gradInput = {h0_grad:view(h0:size()), gradInput} + else + self.gradInput = gradInput + end + return self.gradInput +end + +function mRNN:updateGradInput(input, gradOutput) + if self.recompute_backward then + self:backward(input, gradOutput, 1.0) + end + return self.gradInput +end + +function mRNN:accGradParameters(input, gradOutput, scale) + if self.recompute_backward then + self:backward(input, gradOutput, scale) + end +end + +function mRNN:clearState() + parent.clearState(self) + self.rnn:resetStates() +end + +function mRNN:__tostring__() + local tab = ' ' + local line = '\n' + local next = ' -> ' + local str = 'nn.Container' + str = str .. ' {' .. line .. tab .. '[input' + for i=1,#self.modules do + str = str .. next .. '(' .. i .. ')' + end + str = str .. next .. 'output]' + for i=1,#self.modules do + str = str .. line .. tab .. '(' .. i .. '): ' .. tostring(self.modules[i]):gsub(line, line .. tab) + end + str = str .. line .. '}' + return str +end diff --git a/fairseq/models/npmt.lua b/fairseq/models/npmt.lua new file mode 100755 index 0000000..860fb99 --- /dev/null +++ b/fairseq/models/npmt.lua @@ -0,0 +1,1143 @@ +-- Copyright (c) Microsoft Corporation. All rights reserved. +-- Licensed under the MIT License. +-- +--[[ +-- +-- NPMT model +-- it should actually be a criterion, but since it itself has +-- parameters, we still treat it as a module. +-- +--]] +-- +local NPMT, parent = torch.class('nn.NPMT', 'nn.Container') + +function torch.tab_to_ngram(tab) + local ngram = {} + for i = 1, #tab do + ngram[i] = " " .. table.concat(tab[i], " ") + end + return ngram +end + +function NPMT:__init(configs) + parent.__init(self) + self.use_cuda = configs.use_cuda or true + self.num_thread = self.num_thread or 5 + self.max_segment_len = configs.max_segment_len or 5 + self.ngpus = configs.ngpus or 1 + print("npmt is running on ", self.ngpus, " GPUs") + + self.use_cimpl = configs.use_cimpl or true + self.use_accel = configs.use_accel or false + + if self.use_cimpl then + require "libdp_lib" + end + if self.use_accel then -- TODO + print('Use CUDA accel') + require "libcompute_logpy_lib" + end + + self.vocab_size = configs.target_vocab_size or configs.dict:size() + 2 + self.embedding_size = configs.nembed or 256 + self.dec_unit_size = configs.dec_unit_size or configs.nhid or 256 + self.num_layers = configs.num_dec_layers or configs.nlayer or 1 + self.grad_check = configs.grad_check or false + self.rnn_mode = configs.npmt_rnn_mode or configs.rnn_mode or "LSTM" + self.nnlm_rnn_mode = configs.npmt_nnlm_rnn_mode or self.rnn_mode + if self.grad_check then + require('rnn') + self.start_symbol = configs.start_symbol or self.vocab_size + else + self.start_symbol = configs.start_symbol or self.vocab_size - 1 + end + + self.end_segment_symbol = configs.end_segment_symbol or self.vocab_size + self.pad_index = configs.dict.pad_index + self.use_nnlm = configs.use_nnlm or false + self.group_size = configs.group_size or 512 + + self.report_time = configs.report_time or false + self.precompute_gradInput = true + self.lm_concat = configs.lm_concat or false + self.dropout = configs.npmt_dropout or configs.dropout or 0 + self.nnlm_dropout = (configs.nnlm_dropout and configs.nnlm_dropout > 0 and configs.nnlm_dropout) or self.dropout or 0 + + if self.dropout > 0 then + print("npmt is using dropout ", self.dropout) + end + + self.seq = nn.Sequential() + if self.use_cuda then + require "cudnn" + local cudnn_mode = string.format("CUDNN_%s", self.rnn_mode) + local rnn = nn.mRNN(self.embedding_size, self.dec_unit_size, false, cudnn_mode, false, configs.use_resnet_dec) + self.seq:add(rnn) + if self.dropout > 0 then + self.seq:add(nn.Dropout(self.dropout)) + end + for i = 2, self.num_layers do + local rnn = nn.mRNN(self.dec_unit_size, self.dec_unit_size, false, cudnn_mode, false, configs.use_resnet_dec) + self.seq:add(rnn) + if self.dropout > 0 then + self.seq:add(nn.Dropout(self.dropout)) + end + end + else + local rnn_class + if self.rnn_mode == "GRU" then + rnn_class = nn.SeqGRU + else + rnn_class = nn.SeqLSTM + end + local rnn = rnn_class(self.embedding_size, self.dec_unit_size) + rnn.batchfirst = true + self.seq:add(rnn) + for i = 2, self.num_layers do + rnn = rnn_class(self.dec_unit_size, self.dec_unit_size) + rnn.batchfirst = true + self.seq:add(rnn) + end + end + + self.sub_outnet = nn.Sequential() + self.sub_outnet:add(self.seq) + self.sub_outnet:add(nn.Contiguous()) + self.sub_outnet:add(nn.View(-1, self.dec_unit_size)) + self.sub_outnet:add(nn.Linear(self.dec_unit_size, self.vocab_size)) + self.sub_outnet:add(nn.LogSoftMax()) + + self.outnet = nn.Sequential() + self.dict = nn.LookupTable(self.vocab_size, self.embedding_size) + self.outnet:add(nn.ParallelTable():add(nn.Identity()):add(self.dict)) + self.outnet:add(self.sub_outnet) + + self:add(self.outnet) + + if self.use_nnlm then -- if we opt to use an additional language model for input + self.nnlm = nn.Sequential() + if configs.npmt_separate_embeddding then + self.nnlm_dict = nn.LookupTable(self.vocab_size, self.embedding_size) + else + self.nnlm_dict = self.dict:clone("weight", "gradWeight") -- sharing the weights with self.dict + end + self.nnlm:add(self.nnlm_dict) + self.nnlm_rnn = nn.Sequential() + local nnlm_rnn_inst + if self.use_cuda then + local cudnn_mode = string.format("CUDNN_%s", self.nnlm_rnn_mode) + nnlm_rnn_inst = nn.mRNN(self.embedding_size, self.dec_unit_size, false, cudnn_mode, false, configs.use_resnet_dec) + self.nnlm_rnn:add(nnlm_rnn_inst) + if self.nnlm_dropout> 0 then + self.nnlm_rnn:add(nn.Dropout(self.nnlm_dropout)) + end + for i = 2, self.num_layers do + nnlm_rnn_inst = nn.mRNN(self.dec_unit_size, self.dec_unit_size, false, cudnn_mode, false, configs.use_resnet_dec) + self.nnlm_rnn:add(nnlm_rnn_inst) + if self.nnlm_dropout > 0 then + self.nnlm_rnn:add(nn.Dropout(self.nnlm_dropout)) + end + end + else + if self.rnn_mode == "GRU" then + nnlm_rnn_inst = nn.SeqGRU(self.embedding_size, self.dec_unit_size) + else + nnlm_rnn_inst = nn.SeqLSTM(self.embedding_size, self.dec_unit_size) + end + nnlm_rnn_inst.batchfirst = true + self.nnlm_rnn:add(nnlm_rnn_inst) + for i = 2, self.num_layers do + if self.rnn_mode == "GRU" then + nnlm_rnn_inst = nn.SeqGRU(self.dec_unit_size, self.dec_unit_size) + else + nnlm_rnn_inst = nn.SeqLSTM(self.dec_unit_size, self.dec_unit_size) + end + nnlm_rnn_inst.batchfirst = true + self.nnlm_rnn:add(nnlm_rnn_inst) + end + end + self.nnlm:add(self.nnlm_rnn) + self:add(self.nnlm) + if self.lm_concat then + self.lm_concat_proj = nn.Linear(self.dec_unit_size*2, self.dec_unit_size, false) + self:add(lm_concat_proj) + end + end + + self.logpy = torch.Tensor() + self.alpha = torch.Tensor() + self.beta = torch.Tensor() + self.logpy_per_data = torch.Tensor() + self.seg_weight = torch.Tensor() + self.seg_weight_cum = torch.Tensor() + + if self.use_cuda then + cudnn.convert(self.outnet, cudnn) + self:cuda() + end +end + +function NPMT:get_jstart_range(t, T1, minT2, maxT2) + return math.max(1, minT2 - (T1-t+1)* self.batch_max_segment_len + 1), math.min(maxT2+1, (t-1) * self.batch_max_segment_len + 1) +end + +function NPMT:compute_logpy(hidden_inputs, xlength, yref, ylength, batch_size, T1, T2) + self.outnet:evaluate() --- set outnet in evalate mode + self.logpy:resize(batch_size, T1, T2+1, self.batch_max_segment_len+1):fill(-torch.loginf()) +-- self.logpy_c:resize(batch_size, T1, T2+1, self.batch_max_segment_len+1):fill(-torch.loginf()) + -- for word-based, each word is padded with zero (so that we can -- + -- easily know how long each word is), then in the following code, + -- we will turn this into a proper sequence and padded with + -- end-symbol + -- + -- for letter-based, the entire sequence is padded with end-symbol + + local start_vector = yref.new(batch_size,1):fill(self.start_symbol) + if torch.type(start_vector) ~= "torch.CudaTensor" then + start_vector = start_vector:long() + end + + if self.use_nnlm then + self.nnlm_input = torch.cat(start_vector, yref) + self.nnlm_output = self.nnlm:forward(self.nnlm_input) + end + + local y_input + local minT2 = ylength:min() + + local schedule = {} + for t = 1, T1 do + local jstart_l, jstart_u = self:get_jstart_range(t, T1, minT2, T2) + for j_start = jstart_l, jstart_u do + local j_len = math.min(self.batch_max_segment_len, T2-j_start+1) + local j_end = j_start + j_len - 1 + table.insert(schedule, {t, j_start, j_len, j_end}) + end + end + if #schedule == 0 then + return nil + end + local _, schedule_order = torch.sort(torch.Tensor(schedule)[{{}, 3}]) + local sorted_schedule = {} + for si = 1, #schedule do + table.insert(sorted_schedule, schedule[schedule_order[si]]) + end + + self.sorted_schedule = sorted_schedule + local sorted_schedule_tensor = torch.CudaIntTensor(sorted_schedule) -- for compute_logpy_post use + +-- print(os.clock(), "forward", self.sorted_schedule[{1,1}], T1, hidden_inputs:size(2)) + + local concat_inputs = torch.Tensor() + local concat_hts = torch.Tensor() + if self.use_cuda then + concat_inputs = concat_inputs:cuda() + concat_hts = concat_hts:cuda() + end + self.group_size = math.max(self.group_size, batch_size) + + concat_inputs:resize(self.group_size, self.batch_max_segment_len + 1) + concat_hts:resize(self.group_size, self.dec_unit_size) + + local si = 1 + while si <= #sorted_schedule do + local si_next = math.min(si + math.floor(self.group_size / batch_size) - 1, #sorted_schedule) + local s = si_next - si + 1 + local max_jlen = sorted_schedule[si_next][3] + + local t_concatInputs = concat_inputs[{{1, s * batch_size}, {1, 1 + max_jlen}}] + local t_concatHts = concat_hts[{{1, s * batch_size}, {}}] + t_concatInputs:fill(self.end_segment_symbol) + t_concatHts:zero() + + for ell = si, si_next do + local t, j_start, j_len, j_end = unpack(sorted_schedule[ell]) + local low_idx, high_idx = (ell-si)*batch_size+1, (ell-si+1)*batch_size + y_input = start_vector:clone() + if j_len > 0 then + y_input = torch.cat({y_input, yref[{{}, {j_start,j_end}}]}) + end + local hidden_input = hidden_inputs[{{}, t, {}}] + if self.use_nnlm then + if self.lm_concat then + local hidden_input_concat = torch.cat(hidden_input, self.nnlm_output[{{}, j_start, {}}], 2) + hidden_input = self.lm_concat_proj:updateOutput(hidden_input_concat) + else + hidden_input = torch.add(hidden_input, self.nnlm_output[{{}, j_start, {}}]) + end + end + t_concatHts[{{low_idx, high_idx}, {}}]:copy(hidden_input) + t_concatInputs[{{low_idx, high_idx}, {1, y_input:size(2)}}]:copy(y_input) + end + + local t_prob_all = self.outnet:updateOutput({t_concatHts, t_concatInputs}):view(s*batch_size, max_jlen+1, self.vocab_size) + + if self.use_accel then + compute_logpy_post( t_prob_all, yref, ylength, self.logpy, sorted_schedule_tensor, + s, max_jlen, self.vocab_size, batch_size, self.batch_max_segment_len, T1, T2, si) + else + -- Torch version of compute_logpy_post + local t_vec = t_prob_all.new(batch_size) + local t_valid = t_prob_all.new(batch_size) + for ell = si, si_next do + local t, j_start, j_len, j_end = unpack(sorted_schedule[ell]) + local low_idx, high_idx = (ell-si)*batch_size+1, (ell-si+1)*batch_size + local t_prob = t_prob_all[{{low_idx, high_idx}, {}, {}}] + + local t_vec_whole = nil + if j_len > 0 then + t_vec_whole = t_prob[{{},{1, j_len},{}}]:gather(3, yref[{{},{j_start, j_end}}]:contiguous():view(batch_size, j_len, 1)):view(batch_size, j_len) + end + + t_valid:copy(ylength:ge(j_start-1)) -- a 0/1 vector of length batch_size + self.logpy[{{},t,j_start,1}]:add(torch.cmul(t_valid, torch.loginf() + t_prob[{{},1,self.end_segment_symbol}])) + + t_vec:zero() + for j = j_start, j_end do --- this implies j_end >= j_start (when j=j_start-1, it means an empty segment) + t_valid:copy(ylength:ge(j)) -- a 0/1 vector of length batch_size + -- Use gather to fetch the corresponding values in the yref + t_vec:add(t_vec_whole[{{}, j-j_start+1}]) + -- when j = j_start-1, this j-j_start+2 is 1, which is the first index, in WASM, + -- index 1 is for empty segment (segment length 0) while in segment.lua, index 1 is for + -- segment length 1. So they differ by shifting 1 index. + -- If non-empty, add end_symbol + t_vec; else add end_symbol + self.logpy[{{},t,j_start,j-j_start+2}]:add( + torch.cmul(t_valid, torch.loginf() + t_vec + t_prob[{{},j-j_start+2,self.end_segment_symbol}])) + end + end + end + si = si_next + 1 + end + -- For debug use. Need to declare and use logpy_c +-- print(torch.all(torch.eq(self.logpy_c, self.logpy))) +-- io.write("finall: Press to continue...") +-- io.read() +end + +function NPMT:print_best_path(xlength_, yref_, ylength_, vocab) + assert(self.alpha:size(1) == 1) -- only work for batch size 1 + local T1 = xlength_[1] + local T2 = ylength_[1] + local yref = yref_[1] + local logpy = self.logpy[{1, {}, {}, {}}] + local alpha = logpy.new(T1+1, T2+1) + local prev = logpy.new(T1+1, T2+1):fill(-1) + alpha:fill(-torch.loginf()) + alpha[{1,1}] = 0 + for t = 1, T1 do + for j = 0, T2 do + local j_low = math.max(1, j-self.batch_max_segment_len+1) + for j_start = j_low, j+1 do + local logprob = alpha[{t, j_start}] + logpy[{t, j_start, j-j_start+2}] + if logprob > alpha[{t+1, j+1}] then + alpha[{t+1, j+1}] = logprob + prev[{t, j+1}] = j_start-1 + end + end + end + end + local j = T2 + local out_str = "|" + for t = T1, 1, -1 do + local prev_j = prev[{t, j+1}] + for k = j, prev_j+1, -1 do + out_str = vocab[yref[k]] .. out_str + end + if j > prev_j then + out_str = "|" .. out_str + end + j = prev_j + end + print("best path: ", out_str) + return out_str +end + +function NPMT:alpha_and_beta(xlength, ylength, batch_size, T1, T2) + self.alpha:resize(batch_size, T1+1, T2+1):fill(-torch.loginf()) + self.beta:resize(batch_size, T1+1, T2+1):fill(-torch.loginf()) + self.seg_weight:resizeAs(self.logpy):fill(-torch.loginf()) + + if self.use_cimpl then + self.logpy = self.logpy:double() + self.alpha = self.alpha:double() + self.beta = self.beta:double() + ylength = ylength:double() + xlength = xlength:double() + self.seg_weight = self.seg_weight:double() + + c_sample_dp( + batch_size, + T1, + T2, + self.batch_max_segment_len, + self.num_thread, + tonumber(torch.data(self.logpy, true)), + tonumber(torch.data(self.alpha, true)), + tonumber(torch.data(self.beta, true)), + tonumber(torch.data(self.seg_weight, true)), + tonumber(torch.data(ylength, true)), + tonumber(torch.data(xlength, true))) + + if (self.use_cuda) then + self.logpy = self.logpy:cuda() + self.alpha = self.alpha:cuda() + self.beta = self.beta:cuda() + self.seg_weight = self.seg_weight:cuda() + ylength = ylength:cuda() + xlength = xlength:cuda() + else + ylength = ylength:long() + xlength = xlength:long() + end + else + --- not use c implementation --- + self.alpha[{{}, 1, 1}]:zero() + for t = 1, T1 do + for j = 0, T2 do + local j_low = math.max(1, j-self.batch_max_segment_len+1) + for j_start = j_low, j+1 do + local logprob = self.alpha[{{}, t, j_start}] + self.logpy[{{}, t, j_start, j-j_start+2}] + self.alpha[{{}, t+1, j+1}] = torch.logadd(self.alpha[{{}, t+1, j+1}], logprob) + end + end + end + for i = 1, batch_size do + self.beta[{i, xlength[i]+1, ylength[i]+1}] = 0 + end + for t = T1-1, 0, -1 do + for j = 0, T2 do + for j_end = j, math.min(T2, j + self.batch_max_segment_len) do + local logprob = self.beta[{{}, t+2, j_end+1}] + self.logpy[{{}, t+1, j+1, j_end-j+1}] + self.beta[{{}, t+1, j+1}] = torch.logadd(self.beta[{{}, t+1, j+1}], logprob) + end + end + end + + local minT2 = ylength:min() + for t = 1, T1 do + local jstart_l, jstart_u = self:get_jstart_range(t, T1, minT2, T2) + for j_start = jstart_l, jstart_u do + local j_len = math.min(self.batch_max_segment_len, T2-j_start+1) + local j_end = j_start + j_len - 1 + for j = j_start-1, j_end do + self.seg_weight[{{}, t, j_start, j-j_start+2}] = self.logpy[{{}, t, j_start, j-j_start+2}] + + self.alpha[{{}, t, j_start}] + + self.beta[{{}, t+1, j+1}] + end + end + end + end + + self.logpy_per_data = self.beta[{{}, 1, 1}]:clone() + if self.report_time then + local logpy_per_data_alpha = self.alpha.new(batch_size) + for i = 1, batch_size do + logpy_per_data_alpha[i] = self.alpha[{i, xlength[i]+1, ylength[i]+1}] + end + print(string.format("%.25f", torch.sum(self.logpy_per_data) - torch.sum(logpy_per_data_alpha))) + print(torch.sum(self.logpy_per_data)) + end + + self.seg_weight:add(-self.logpy_per_data:view(batch_size, 1, 1, 1):repeatTensor(1, T1, T2+1, self.batch_max_segment_len+1)) + self.seg_weight_cum:resizeAs(self.seg_weight):fill(-torch.loginf()) + + if self.use_cimpl then + self.seg_weight_cum:copy(self.seg_weight) + self.seg_weight_cum = self.seg_weight_cum:double() + ylength = ylength:double() + c_reverse_log_cumsum( + batch_size, + T1, + T2, + self.batch_max_segment_len, + self.num_thread, + tonumber(torch.data(self.seg_weight_cum, true)), + tonumber(torch.data(ylength, true))) + if (self.use_cuda) then + ylength= ylength:cuda() + self.seg_weight_cum = self.seg_weight_cum:cuda() + else + ylength = ylength:long() + end + self.seg_weight:exp() -- make it actual weight + self.seg_weight_cum:exp() + else + self.seg_weight:exp() -- make it actual weight + self.seg_weight_cum:copy(self.seg_weight) + self.seg_weight_cum = self.seg_weight_cum:index(4, torch.linspace(self.batch_max_segment_len+1, 1, self.batch_max_segment_len+1):long()) + :cumsum(4) + :index(4, torch.linspace(self.batch_max_segment_len+1, 1, self.batch_max_segment_len+1):long()) + end +end + +function NPMT:compute_gradients(hidden_inputs, xlength, yref, ylength, batch_size, T1, T2) + self.outnet:training() --- set outnet in training mode + local grad_hidden_inputs = hidden_inputs.new(hidden_inputs:size()):zero() + + local start_vector = yref.new(batch_size,1):fill(self.start_symbol) + if torch.type(start_vector) ~= "torch.CudaTensor" then + start_vector = start_vector:long() + end + + local nnlm_gradOutput + if self.use_nnlm then + nnlm_gradOutput = self.nnlm_output.new(self.nnlm_output:size()):zero() + end + + local sorted_schedule = self.sorted_schedule -- copy from forward + assert(#sorted_schedule > 0) +-- print(os.clock(), "backward", self.sorted_schedule[{1,1}], T1, hidden_inputs:size(2)) + + local concat_inputs = torch.Tensor() + local concat_hts = torch.Tensor() + local gradOutput = torch.Tensor() + if (self.use_cuda) then + concat_inputs = concat_inputs:cuda() + concat_hts = concat_hts:cuda() + gradOutput = gradOutput:cuda() + end + + concat_inputs:resize(self.group_size, self.batch_max_segment_len + 1) + concat_hts:resize(self.group_size, self.dec_unit_size) + gradOutput:resize(self.group_size, self.batch_max_segment_len + 1, self.vocab_size) + + local grad_scale = -1.0 / (yref:size(1) * self.ngpus) + local y_input + local si = 1 + + local skip_sample = false + for si = 1, #sorted_schedule do + if sorted_schedule[si][1] > hidden_inputs:size(2) then + skip_sample = true + end + end + if skip_sample then + print('skip') + else + while si <= #sorted_schedule do + local si_next = math.min(si + math.floor(self.group_size / batch_size) - 1, #sorted_schedule) + local s = si_next - si + 1 + local max_jlen = sorted_schedule[si_next][3] + + local t_concatInputs = concat_inputs[{{1, s * batch_size}, {1, max_jlen + 1}}] + local t_concatHts = concat_hts[{{1, s * batch_size}, {}}] + local t_gradOutput = gradOutput[{{1, s * batch_size}, {1, max_jlen +1}, {}}] + t_concatInputs:fill(self.end_segment_symbol) + t_concatHts:zero() + t_gradOutput:zero() + + for ell = si, si_next do + local t, j_start, j_len, j_end = unpack(sorted_schedule[ell]) + local low_idx, high_idx = (ell-si)*batch_size+1, (ell-si+1)*batch_size + y_input = start_vector:clone() + if j_end >= j_start then + y_input = torch.cat({y_input, yref[{{}, {j_start,j_end}}]}) + end +-- if t > hidden_inputs:size(2) then +-- print("xlength", xlength) +-- print("ylength", ylength) +-- print("ylength", yref) +-- print("size", hidden_inputs:size()) +-- print("t", t, j_start, j_len, j_end, T1) +-- end + local hidden_input = hidden_inputs[{{}, t, {}}] + if self.use_nnlm then + if self.lm_concat then + local hidden_input_concat = torch.cat(hidden_input, self.nnlm_output[{{}, j_start, {}}], 2) + hidden_input = self.lm_concat_proj:updateOutput(hidden_input_concat) + else + hidden_input = torch.add(hidden_input, self.nnlm_output[{{}, j_start, {}}]) + end + end + t_concatHts[{{low_idx, high_idx}, {}}]:copy(hidden_input) + t_concatInputs[{{low_idx, high_idx}, {1, y_input:size(2)}}]:copy(y_input) + if j_len > 0 then + local yweight = self.seg_weight_cum[{{}, t, j_start, {2, j_len+1}}]:contiguous() + local ysnipt = yref[{{}, {j_start, j_end}}]:contiguous() + -- Use scatter to put batch of y batch to corresponding place + t_gradOutput[{{low_idx, high_idx}, {1, j_len}, {}}]:scatter(3, ysnipt:view(batch_size, j_len, 1), yweight:view(batch_size, j_len, 1)) + end + t_gradOutput[{{low_idx, high_idx}, {1,j_len+1}, self.end_segment_symbol}]:copy(self.seg_weight[{{}, t, j_start, {1,j_len+1}}]) + end + + t_gradOutput:mul(grad_scale) + local reshaped_t_gradOutput = t_gradOutput:reshape(s*batch_size*(max_jlen+1), self.vocab_size) + self.outnet:forward({t_concatHts, t_concatInputs}) + self.outnet:backward({t_concatHts, t_concatInputs}, reshaped_t_gradOutput) + reshaped_t_gradOutput:set() + + + for ell = si, si_next do + local t, j_start, j_len, j_end = unpack(sorted_schedule[ell]) + local t_valid = gradOutput.new(batch_size):zero() + t_valid:copy(xlength:ge(t)) + local low_idx, high_idx = (ell-si)*batch_size+1, (ell-si+1)*batch_size + local grad_input = torch.cmul(self.outnet.gradInput[1][{{low_idx, high_idx}, {}}], + t_valid:view(batch_size, 1):expand(batch_size, hidden_inputs:size(3))) + if self.nnlm then + if self.lm_concat then + local hidden_input_concat = torch.cat(hidden_inputs[{{}, t, {}}], self.nnlm_output[{{}, j_start, {}}], 2) + local concat_grad_input = self.lm_concat_proj:backward(hidden_input_concat, grad_input) + grad_hidden_inputs[{{},t,{}}]:add(concat_grad_input[{{}, {1, self.dec_unit_size}}]) + nnlm_gradOutput[{{}, j_start, {}}]:add(concat_grad_input[{{}, {self.dec_unit_size + 1, 2*self.dec_unit_size}}]) + else + nnlm_gradOutput[{{}, j_start, {}}]:add(grad_input) + grad_hidden_inputs[{{},t,{}}]:add(grad_input) + end + else + grad_hidden_inputs[{{},t,{}}]:add(grad_input) + end + end + si = si_next + 1 + end + end + if self.use_nnlm then + self.nnlm:backward(self.nnlm_input, nnlm_gradOutput) + end + + if (self.report_time) then + print(' compute time => ', os.clock() - t_clock) + end + + self.gradInput = {grad_hidden_inputs, + xlength.new(xlength:size()):zero(), + yref.new(yref:size()):zero(), + ylength.new(ylength:size()):zero()} +end + +function NPMT:forward_and_backward(input) + local t_clock = nil + if self.report_time then + t_clock = os.clock() + end + local hidden_inputs, xlength, yref, ylength = unpack(input) + -- hidden_inputs: [torch.CudaTensor of size batch_size, T1, hidden] + -- yref: [torch.CudaTensor of size batch_size, T2] + -- xlength: [torch.CudaTensor of size batch_size] + -- ylength: [torch.CudaTensor of size batch_size] + + local batch_size = hidden_inputs:size(1) + local T1, T2 = hidden_inputs:size(2), yref:size(2) + self.batch_max_segment_len = math.min(self.max_segment_len, T2) + + assert(hidden_inputs:dim() == 3) + if torch.type(yref) ~= "torch.CudaTensor" then + xlength = xlength:long() + yref = yref:long() + ylength = ylength:long() + else + xlength = xlength:cuda() + yref = yref:cuda() + ylength = ylength:cuda() + end + + --Initialization + self.output = nil + if self.precompute_gradInput then + self.gradInput = {} + end + + if (self.report_time) then + print(' Initialization time => ', os.clock() - t_clock) + end + + -- Step 1: compute log p(y_{j1:j2}|h_t) + if (self.report_time) then + t_clock = os.clock() + end + + self:compute_logpy(hidden_inputs, xlength, yref, ylength, batch_size, T1, T2) + + if (self.report_time) then + print(' Time for phase 1 ==> ', os.clock() - t_clock) + end + + -- Step 2: sum over the probs + if (self.report_time) then + t_clock = os.clock() + end + + self:alpha_and_beta(xlength, ylength, batch_size, T1, T2) + self.output = self.logpy_per_data.new(1):fill(-torch.sum(self.logpy_per_data)) + + if (self.report_time) then + print(' Time for phase 2 ==> ', os.clock() - t_clock) + end + + -- Step 3: compute gradients + if (self.report_time) then + t_clock = os.clock() + end + + if (self.precompute_gradInput) then + self:compute_gradients(hidden_inputs, xlength, yref, ylength, batch_size, T1, T2) + end + + if (self.report_time) then + print(' Time for phase 3 ==> ', os.clock() - t_clock) + end +end + +function NPMT:forward(input) + self:forward_and_backward(input) + return self.output +end + +function NPMT:backward(input, gradOutput, scale) + assert(self.precompute_gradInput) + return self.gradInput +end + +function NPMT:updateOutput(input) + return self:forward(input) +end + +function NPMT:updateGradInput(input, gradOutput) + if self.precompute_gradInput then + self:backward(input, gradOutput, 1.0) + end + return self.gradInput +end + +function NPMT:accGradParameters(input, gradOutput, scale) + if self.precompute_gradInput then + self:backward(input, gradOutput, scale) + end +end + +function NPMT:SelectRememberRNNStates(rnns, new_ht_idx) + local ht + if self.use_cuda then + if self.rnn_mode == "LSTM" then + ht = rnns[1].cellOutput:view(-1, self.dec_unit_size) + :index(1, torch.LongTensor(new_ht_idx)) + :view(1, #new_ht_idx, self.dec_unit_size) + else + ht = rnns[1].hiddenOutput:view(-1, self.dec_unit_size) + :index(1, torch.LongTensor(new_ht_idx)) + :view(1, #new_ht_idx, self.dec_unit_size) + end + for i = 1, #rnns do + rnns[i].hiddenInput = rnns[i].hiddenOutput:view(-1, self.dec_unit_size) + :index(1, torch.LongTensor(new_ht_idx)) + :view(1, #new_ht_idx, self.dec_unit_size) + if self.rnn_mode == "LSTM" then + rnns[i].cellInput = rnns[i].cellOutput:view(-1, self.dec_unit_size) + :index(1, torch.LongTensor(new_ht_idx)) + :view(1, #new_ht_idx, self.dec_unit_size) + end + end + else + --TODO fix lstm on cpu + ht = rnns[1]._output[1]:view(-1, self.dec_unit_size) + :index(1, torch.LongTensor(new_ht_idx)) + :view(#new_ht_idx, self.dec_unit_size) + for i = 1, #rnns do + rnns[i].h0 = rnns[i]._output[1]:view(-1, self.dec_unit_size) + :index(1, torch.LongTensor(new_ht_idx)) + :view(#new_ht_idx, self.dec_unit_size) + if rnns[i].cell then + rnns[i].c0 = rnns[i].cell[1]:view(-1, self.dec_unit_size) + :index(1, torch.LongTensor(new_ht_idx)) + :view(#new_ht_idx, self.dec_unit_size) + end + end + end + return ht +end + +function NPMT:rememberRNNStates(rnns) + local ht + if self.use_cuda then + if self.rnn_mode == "LSTM" then + ht = rnns[1].cellOutput:clone() + else + ht = rnns[1].hiddenOutput:clone() + end + for i = 1, #rnns do --remember old states + rnns[i].hiddenInput = rnns[i].hiddenOutput:clone() + if self.rnn_mode == "LSTM" then + rnns[i].cellInput = rnns[i].cellOutput:clone() + end + end + else + --TODO fix lstm on cpu + ht = rnns[1]._output[1]:clone() + for i = 1, #rnns do --remember old states + rnns[i].h0 = rnns[i]._output[1]:clone() + if rnns[i].cell then + rnns[i].c0 = rnns[i].cell[1]:clone() + end + end + end + return ht +end + +function NPMT:GetNNLMRnns() + local rnns, rnn_containers + if self.use_cuda then + rnns, rnn_containers = self.nnlm:findModules("cudnn.RNN") + else + if self.rnn_mode == "LSTM" then + rnns, rnn_containers = self.nnlm:findModules("nn.SeqLSTM") + elseif self.rnn_mode == "GRU" then + rnns, rnn_containers = self.nnlm:findModules("nn.SeqGRU") + else + assert(false) + end + end + return rnns +end + +function NPMT:GetOutnetRnns() + local rnns, rnn_containers + if self.use_cuda then + rnns, rnn_containers = self.outnet:findModules("cudnn.RNN") + else + if self.rnn_mode == "LSTM" then + rnns, rnn_containers = self.outnet:findModules("nn.SeqLSTM") + elseif self.rnn_mode == "GRU" then + rnns, rnn_containers = self.outnet:findModules("nn.SeqGRU") + else + assert(false) + end + end + return rnns +end + +function NPMT:clearOutnetStates() + local rnns = self:GetOutnetRnns() + for i = 1, #rnns do + rnns[i]:resetStates() + end +end + +function NPMT:resetRNNStates() + local rnns = self:GetOutnetRnns() + for i = 1, #rnns do + rnns[i]:resetStates() + end + if self.use_nnlm then + rnns = self:GetNNLMRnns() + for i = 1, #rnns do + rnns[i]:resetStates() + end + end +end + +function NPMT:training() + self.precompute_gradInput = true + self:resetRNNStates() + parent.training(self) +end + +function NPMT:evaluate() + self.precompute_gradInput = false + self:resetRNNStates() + parent.evaluate(self) +end + +function NPMT:clearState() + parent.clearState(self) + self:resetRNNStates() + self.logpy:set() + self.alpha:set() + self.beta:set() + self.logpy_per_data:set() + self.seg_weight:set() + self.seg_weight_cum:set() + self.output = 0. + self.gradInput = {} + self.sorted_schedule = {} + if self.use_nnlm then + if self.nnlm_output ~= nil then + self.nnlm_output:set() + end + if self.nnlm_input ~= nil then + self.nnlm_input:set() + end + end +end + +function NPMT:predict(input, xlength, test_mode) + -- TODO fixes + local batch_size, T1 = input:size(1), input:size(2) +-- assert(batch_size == 1) + local max_segment_len = self.max_segment_len + local sts_input = torch.Tensor()--:fill(self.start_symbol) + if (self.use_cuda) then + sts_input = sts_input:cuda() + end + local start_symbol = self.start_symbol + local rnns = self:GetOutnetRnns() + + local tab_output_seqs = {} + local tab_output_probs = {} + local test_segments = 0 + + for b = 1, batch_size do + local out_str = "|" + local output_symbol = nil + local output_seq = {} + local output_probs = {} + local num_segments = 0 + sts_input:resize(1, 1):fill(self.start_symbol) + + local nnlm_rnns, nnlm_output + if self.use_nnlm then + nnlm_rnns = self:GetNNLMRnns() + nnlm_output = self.nnlm:updateOutput(sts_input):view(-1, self.dec_unit_size) + self:rememberRNNStates(nnlm_rnns) + end + + for t = 1, xlength[b] do + sts_input[1][1] = start_symbol + local ht = input[{{b},t,{}}]:clone() + self:clearOutnetStates() + if self.use_nnlm then + if self.lm_concat then + ht = self.lm_concat_proj:updateOutput(torch.cat(ht, nnlm_output, 2)) + else + ht:add(nnlm_output) + end + end + local new_segment = false + for j = 1, max_segment_len do + local output_prob = self.outnet:updateOutput({ht, sts_input}):view(-1) -- 1-dimensional vector of length V + local max_prob, output_symbol = output_prob:view(-1):max(1) + output_symbol = output_symbol:squeeze() + if output_symbol == self.end_segment_symbol then + break -- finished reading this segment + else + if not new_segment then + num_segments = num_segments + 1 + new_segment = true + end + + table.insert(output_seq, output_symbol) + table.insert(output_probs, max_prob[1]) + if test_mode then +-- out_str = out_str .. " " .. output_symbol + -- input time t, output corresponding segments + out_str = out_str .. " " .. t .. ':' .. output_symbol +-- out_str = out_str .. " " .. vocab[output_symbol] + end + sts_input[1][1] = output_symbol + ht = self:rememberRNNStates(rnns) + if self.use_nnlm then + nnlm_output = self.nnlm:updateOutput(sts_input):view(-1, self.dec_unit_size) + self:rememberRNNStates(nnlm_rnns) + end + end + end + if new_segment and test_mode then + out_str = out_str .. '|' + end + end + + if #output_seq == 0 then + local eos_index = 3 + table.insert(output_seq, eos_index) -- eos + table.insert(output_probs, 1) + end + self:clearState() -- don't leave things for next one example + if test_mode then + print("max decoding:", out_str) + end + + table.insert(tab_output_seqs, torch.Tensor(output_seq)) + table.insert(tab_output_probs, output_probs) -- place holder, dummy + test_segments = test_segments + num_segments + end + tab_output_seqs = nn.FlattenTable():forward(tab_output_seqs) + local output_count = 0 + for i = 1, #tab_output_seqs do + output_count = output_count + tab_output_seqs[i]:nElement() + end + return tab_output_seqs, nn.FlattenTable():forward(tab_output_probs), output_count, test_segments +end + +function NPMT:beam_search(input, xlength, configs) + local configs = configs or {} + local word_weight = configs.lenpen or configs.word_weight or 0 + local beam_size = configs.beam_size or configs.beam or 20 + + local lm_weight = configs.lm_weight or 0 + local lm = configs.lm + + local batch_size, T1 = input:size(1), input:size(2) + local rnns = self:GetOutnetRnns() + + local max_segment_len = self.max_segment_len + local sts_input = torch.Tensor():cuda() + + local tab_output_seqs = {} + local tab_output_probs = {} + + for b = 1, batch_size do + sts_input:resize(1, 1):fill(self.start_symbol) + + local fin_trans = {} + local fin_probs = {} + + local nnlm_rnns, nnlm_output + if self.use_nnlm then + nnlm_output = self.nnlm:updateOutput(sts_input):view(-1, self.dec_unit_size) + end + + for t = 1, xlength[b] do + local trans_t = {} + local probs_t = {} + local fin_trans_t = {} + local fin_probs_t = {} + + local ht = input[{{b},t,{}}]:clone() + if t > 1 then + sts_input:resize(#fin_trans, 1):fill(self.start_symbol) + for i = 1, #fin_trans do + table.insert(trans_t, torch.copy_array(fin_trans[i])) + end + probs_t = torch.copy_array(fin_probs) + ht = ht:repeatTensor(#fin_trans, 1) + else + table.insert(trans_t, {}) + table.insert(probs_t, 0.) + sts_input:resize(1, 1):fill(self.start_symbol) + end + + local ngrams + if lm_weight > 0 then + ngrams = torch.tab_to_ngram(trans_t) + end + + -- nnlm 1. dropout, 2. nnlm with increasing/decreasing weights + if self.use_nnlm then + if self.lm_concat then + ht = self.lm_concat_proj:updateOutput(torch.cat(ht, nnlm_output, 2)) + elseif self.schedule_nnlm > 0 then + ht = ht + self.schedule_nnlm * nnlm_output + ht = self.lm_concat_proj:updateOutput(torch.cat(ht, nnlm_output, 2)) + else + ht:add(nnlm_output) + end + end + self:clearOutnetStates() + local nsamples = beam_size + for j = 1, max_segment_len + 1 do + local output_prob = self.outnet:updateOutput({ht, sts_input}) + local new_trans_t = {} + local new_probs_t = {} + local new_ht_idx = {} + if j == max_segment_len + 1 then -- we have to force to have the end_segment_symbol for each input + probs_t = torch.Tensor(probs_t):cuda() + output_prob[{{}, self.end_segment_symbol}] -- + word_weight + for k = 1, nsamples do + table.insert(fin_trans_t, trans_t[k]) + table.insert(fin_probs_t, probs_t[k]) + end + else + probs_t = torch.Tensor(probs_t):cuda():view(-1, 1):repeatTensor(1, self.vocab_size) + output_prob -- + word_weight + probs_t = probs_t:view(-1) + local _, sorted_idx = torch.sort(probs_t, true) + for k = 1, math.min(nsamples, sorted_idx:nElement()) do + local tran_id = math.floor((sorted_idx[k]-1) / self.vocab_size) + 1 + local word_id = (sorted_idx[k]-1) % self.vocab_size + 1 + if word_id == self.end_segment_symbol then + --- end symbol + table.insert(fin_trans_t, trans_t[tran_id]) + table.insert(fin_probs_t, probs_t[sorted_idx[k]]) + else + --- continue in the pool + local new_tran = torch.copy_array(trans_t[tran_id]) + table.insert(new_tran, word_id) + table.insert(new_trans_t, new_tran) + + local new_prob = probs_t[sorted_idx[k]] + word_weight + if lm_weight > 0 then + new_prob = new_prob + lm_weight * lookup_lm_prob(lm, ngrams[tran_id], tostring(word_id))[1] + end + table.insert(new_probs_t, new_prob) + table.insert(new_ht_idx, tran_id) + end + end + end + trans_t = new_trans_t + probs_t = new_probs_t + nsamples = #trans_t + if nsamples == 0 then + break + end + if lm_weight > 0 then + ngrams = torch.tab_to_ngram(trans_t) + end + sts_input:resize(#trans_t, 1):copy(torch.last_nelements(trans_t, 1, self.start_symbol)) + ht = self:SelectRememberRNNStates(rnns, new_ht_idx) + end + if t > 1 then + --- merge same sequences + local merge_fin_trans_t = {} + merge_fin_trans_t[table.concat(fin_trans_t[1], '-')] = {fin_trans_t[1], fin_probs_t[1]} + for i = 2, #fin_trans_t do + local tran_str = table.concat(fin_trans_t[i], '-') + if merge_fin_trans_t[tran_str] ~= nil then + merge_fin_trans_t[tran_str][2] = torch.logadd(merge_fin_trans_t[tran_str][2], fin_probs_t[i]) + else + merge_fin_trans_t[tran_str] = {fin_trans_t[i], fin_probs_t[i]} + end + end + fin_trans_t = {} + fin_probs_t = {} + for key, value in pairs(merge_fin_trans_t) do + table.insert(fin_trans_t, value[1]) + table.insert(fin_probs_t, value[2]) + end + end + fin_trans = fin_trans_t + fin_probs = fin_probs_t + + if self.use_nnlm then + nnlm_output:resize(#fin_trans, self.dec_unit_size):zero() + local max_len = #(fin_trans[1]) + for i = 1, #fin_trans do + max_len = math.max(max_len, #(fin_trans[i])) + end + local lm_input = nnlm_output.new(#fin_trans, max_len+1):fill(self.start_symbol) + for i = 1, #fin_trans do + if #fin_trans[i] > 0 then + lm_input[{i, {2,#(fin_trans[i])+1}}]:copy(torch.Tensor(fin_trans[i])) + end + end + local lm_output = self.nnlm:updateOutput(lm_input) + for i = 1, #fin_trans do + nnlm_output[{i, {}}]:copy(lm_output[{i, #(fin_trans[i])+1, {}}]) + end + end + end + + if configs.use_avg_prob then + for i = 1, #fin_probs do + fin_probs[i] = fin_probs[i] / (#fin_trans[i]) + end + end + + local _, sorted_idx = torch.sort(torch.Tensor(fin_probs), true) + + local output_seqs = {} + local output_probs = {} + + for i = 1, math.min(configs.beam_size, #fin_probs) do + if #fin_trans[sorted_idx[i]] > 0 then + table.insert(output_seqs, torch.Tensor(fin_trans[sorted_idx[i]])) + else + local eos_index = 3 + table.insert(output_seqs, torch.Tensor(1):fill(eos_index)) + end + table.insert(output_probs, fin_probs[sorted_idx[i]]) + end + self:clearState() -- don't leave things for next one example + table.insert(tab_output_seqs, output_seqs) + table.insert(tab_output_probs, output_probs) + end + + return nn.FlattenTable():forward(tab_output_seqs), nn.FlattenTable():forward(tab_output_probs) + +end diff --git a/fairseq/models/npmt_model.lua b/fairseq/models/npmt_model.lua new file mode 100755 index 0000000..5ac112b --- /dev/null +++ b/fairseq/models/npmt_model.lua @@ -0,0 +1,598 @@ +-- Copyright (c) 2017-present, Facebook, Inc. +-- All rights reserved. +-- +-- This source code is licensed under the license found in the LICENSE file in +-- the root directory of this source tree. An additional grant of patent rights +-- can be found in the PATENTS file in the same directory. +-- +-- Copyright (c) Microsoft Corporation. All rights reserved. +-- Licensed under the BSD License. +-- +--[[ +-- +-- This model closely follows the conditional setup of rnn-lib v1, with -name +-- clstm and -aux conv_attn. See the individual functions (makeEncoder, +-- makeDecoder) for detailed comments regarding the model architecture. +-- +--]] + +require 'nn' +require 'nngraph' +require 'rnnlib' +local argcheck = require 'argcheck' +local mutils = require 'fairseq.models.utils' +local rutils = require 'rnnlib.mutils' +local utils = require 'fairseq.utils' + +local cuda = utils.loadCuda() + +local NPMTModel, parent = torch.class('NPMTModel', 'Model') + +NPMTModel.make = argcheck{ + {name='self', type='NPMTModel'}, + {name='config', type='table'}, + call = function(self, config) + config.use_cuda = true + local encoder = self:makeEncoder(config) + local decoder = self:makeDecoder(config) + -- Wire up encoder and decoder + local input = nn.Identity()() + local sourceIn, xlength, targetIn, ylength = input:split(4) + -- reformat the shape + -- input to npmt is {hidden_inputs, xlength, yref, ylength} + local output = decoder({ + encoder(sourceIn):annotate{name = 'encoder'}, + xlength, + targetIn, + ylength + }):annotate{name = 'decoder'} + + return nn.gModule({input}, {output}) + end +} + +-- Use the same encoder as BLSTMModel + + +NPMTModel.makeEncoderColumn = argcheck{ + {name='self', type='NPMTModel'}, + {name='config', type='table'}, + {name='inith', type='nngraph.Node'}, + {name='input', type='nngraph.Node'}, + {name='nlayers', type='number'}, + call = function(self, config, inith, input, nlayers) + local rnnconfig = { + inputsize = config.nembed, + hidsize = config.nhid, + nlayer = 1, + winitfun = function(network) + rutils.defwinitfun(network, config.init_range) + end, + usecudnn = usecudnn, + } + + local rnn_class = nn.LSTM + if config.rnn_mode == "GRU" then + rnn_class = nn.GRU + end + + local rnn = rnn_class(rnnconfig) + rnn.saveHidden = false + local output = nn.SelectTable(-1)(nn.SelectTable(2)( + rnn({inith, input}):annotate{name = 'encoderRNN'} + )) + + if config.use_resnet_enc then + if config.nembed ~= config.nhid then + local input_proj = nn.MapTable(nn.Linear(config.nembed, config.nhid, false))(input) + output = nn.MapTable(nn.CAddTable())(nn.ZipTable()({input_proj, output})) + else + output = nn.MapTable(nn.CAddTable())(nn.ZipTable()({input, output})) + end + end + + rnnconfig.inputsize = config.nhid + + for i = 2, nlayers do + if config.dropout_hid > 0 then + output = nn.MapTable(nn.Dropout(config.dropout_hid))(output) + end + local rnn = rnn_class(rnnconfig) + rnn.saveHidden = false + local prev_input + if config.use_resnet_enc then + prev_input = nn.Identity()(output) + end + output = nn.SelectTable(-1)(nn.SelectTable(2)( + rnn({ + inith, + nn.ReverseTable()(output), + }) + )) + if config.use_resnet_enc then + output = nn.MapTable(nn.CAddTable())(nn.ZipTable()({prev_input, output})) + end + end + return output + end +} + +NPMTModel.makeEncoder = argcheck{ + doc=[[ +This encoder runs a forward and backward LSTM network and concatenates their +top-most hidden states. +]], + {name='self', type='NPMTModel'}, + {name='config', type='table'}, + call = function(self, config) + local sourceIn = nn.Identity()() + local inith, tokens = sourceIn:split(2) + + local dict = config.srcdict + local lut = mutils.makeLookupTable(config, dict:size(), + dict.pad_index) + local embed + if config.dropout_src > 0 then + embed = nn.MapTable(nn.Sequential() + :add(lut) + :add(nn.Dropout(config.dropout_src)))(tokens) + else + embed = nn.MapTable(lut)(tokens) + end + assert(config.num_lower_conv_layers + config.num_mid_conv_layers + config.num_high_conv_layers <= 1) + + -- Low level - Add temporal conv stride to reduced computations + if config.num_lower_conv_layers > 0 then + local conv_embed = nn.Sequential() + conv_embed:add(nn.MapTable(nn.View(-1, 1, config.nembed))) + conv_embed:add(nn.JoinTable(2)) -- Split table to tensor as it expects tensor {batch_size x T x nembed} + conv_embed:add(nn.Padding(2, 1-config.conv_kW_size))-- pad left with zeros + conv_embed:add(nn.TemporalConvolution(config.nembed, config.nembed, config.conv_kW_size, config.conv_dW_size)) + conv_embed:add(nn.ReLU()) + embed = conv_embed(embed):annotate{name = 'TemporalConv'} + end + + if config.num_lower_win_layers > 0 then + local reorder_embed = nn.Sequential() + -- Reshape as a table T elements of (batch_size x 1 x nembed) + if config.num_lower_conv_layers == 0 then + reorder_embed:add(nn.MapTable(nn.View(-1, 1, config.nembed))) + reorder_embed:add(nn.JoinTable(2)) -- Split table to tensor as it expects tensor {batch_size x T x nembed} + end + + if config.num_lower_win_layers > 0 then + local winattn_layer + if config.win_attn_type == 'ori' then + winattn_layer = nn.winAttn(config.nembed, config.kwidth, config.use_win_middle) + else + winattn_layer = nil -- Error + end + for i = 1, config.num_lower_win_layers do + reorder_embed:add(winattn_layer) + end + embed = reorder_embed(embed) + end + end + + -- Mid level - Add temporal conv stride to reduced computations + if config.num_mid_conv_layers > 0 then + local conv_embed = nn.Sequential() + if config.num_lower_win_layers == 0 and config.num_lower_conv_layers == 0 then + conv_embed:add(nn.MapTable(nn.View(-1, 1, config.nembed))) + conv_embed:add(nn.JoinTable(2)) -- Split table to tensor as it expects tensor {batch_size x T x nembed} + end + conv_embed:add(nn.Padding(2, 1-config.conv_kW_size))-- pad left with zeros + conv_embed:add(nn.TemporalConvolution(config.nembed, config.nembed, config.conv_kW_size, config.conv_dW_size)) + conv_embed:add(nn.ReLU()) + embed = conv_embed(embed):annotate{name = 'TemporalConv'} + end + if config.num_lower_conv_layers > 0 or config.num_lower_win_layers > 0 or config.num_mid_conv_layers > 0 then + embed = nn.SplitTable(2)(embed) + end + + local col1 = self:makeEncoderColumn{ + config = config, + inith = inith, + input = embed, + nlayers = config.nenclayer, + } + local col2 = self:makeEncoderColumn{ + config = config, + inith = inith, + input = nn.ReverseTable()(embed), + nlayers = config.nenclayer, + } + + -- Each column will switch direction between layers. Before merging, + -- they should both run in the same direction (here: forward). + if config.nenclayer % 2 == 0 then + col1 = nn.ReverseTable()(col1) + else + col2 = nn.ReverseTable()(col2) + end + + local prepare = nn.Sequential() + -- Concatenate forward and backward states + prepare:add(nn.JoinTable(2, 2)) + -- Scale down to nhid for further processing + prepare:add(nn.Linear(config.nhid * 2, config.dec_unit_size, false)) + -- Add singleton dimension for subsequent joining + prepare:add(nn.View(-1, 1, config.dec_unit_size)) + + local joinedOutput = nn.JoinTable(1, 2)( + nn.MapTable(prepare)( + nn.ZipTable()({col1, col2}) + ) + ) + if config.dropout_hid > 0 then + joinedOutput = nn.Dropout(config.dropout_hid)(joinedOutput) + end + + -- TODO add attention layer + + -- TODO add temporal conv stride to reduced computations + if config.num_high_conv_layers > 0 then + local conv_embed = nn.Sequential() + conv_embed:add(nn.Padding(2, 1-config.conv_kW_size))-- pad left with zeros + conv_embed:add(nn.TemporalConvolution(config.dec_unit_size, config.dec_unit_size, config.conv_kW_size, config.conv_dW_size)) + conv_embed:add(nn.ReLU()) + joinedOutput = conv_embed(joinedOutput):annotate{name = 'TemporalConv'} + end + + -- avgpool_model.makeDecoder() expects two encoder outputs, one for + -- attention score computation and the other one for applying them. + -- We'll just use the same output for both. + return nn.gModule({sourceIn}, {joinedOutput}) + end +} + + +NPMTModel.makeDecoder = argcheck{ + doc=[[ + Constructs a WASM. + ]], + {name='self', type='NPMTModel'}, + {name='config', type='table'}, + call = function(self, config) + -- input to npmt is {hidden_inputs, xlength, yref, ylength} + local input = nn.Identity()() + local encoderOut, xlength, targetIn, ylength = input:split(4) + local output = nn.NPMT(config)({encoderOut, xlength, targetIn, ylength}):annotate{name = 'npmt'} + return nn.gModule({input}, {output}) + end +} + + +NPMTModel.prepareSource = argcheck{ + {name='self', type='NPMTModel'}, + call = function(self) + -- Device buffers for samples + local buffers = { + source = {}, + xlength = {} + } + + -- NOTE: It's assumed that all encoders start from the same hidden + -- state. + local encoderRNN = mutils.findAnnotatedNode( + self:network(), 'encoderRNN' + ) + assert(encoderRNN ~= nil) + local conv_kW_size, conv_dW_size = 0, 0 + if mutils.findAnnotatedNode(self:network(), 'TemporalConv') then + if #mutils.findAnnotatedNode(self:network(), 'TemporalConv') > 3 then + conv_kW_size = mutils.findAnnotatedNode(self:network(), 'TemporalConv'):get(4).kW + conv_dW_size = mutils.findAnnotatedNode(self:network(), 'TemporalConv'):get(4).dW + else + conv_kW_size = mutils.findAnnotatedNode(self:network(), 'TemporalConv'):get(2).kW + conv_dW_size = mutils.findAnnotatedNode(self:network(), 'TemporalConv'):get(2).dW + end + end + + return function(sample) + -- Encoder input + local source = {} + local xlength = torch.Tensor(sample.bsz):zero() + local source_t = sample.source:t() + + local pad_index = 2 + local eos_index = 3 + local max_xlength = 0 + for i = 1, sample.bsz do + buffers.xlength[i] = buffers.xlength[i] or torch.Tensor():type(self:type()) + xlength[i] = source_t:size(2) - torch.sum(source_t[i]:eq(pad_index)) + xlength[i] = xlength[i] - torch.sum(source_t[i]:eq(eos_index)) + max_xlength = math.max(max_xlength, xlength[i]) + + source_t[{i, xlength[i]+1}] = pad_index + end + source_t = source_t[{{}, {1, max_xlength}}]:clone() + + for j = 1, source_t:size(2) do + buffers.source[j] = buffers.source[j] or torch.Tensor():type(self:type()) + source[j] = mutils.sendtobuf(source_t[{{}, j}], buffers.source[j]) + end + -- change xlength when there is a TemporalConv layer + if conv_dW_size > 0 then + for i = 1, sample.bsz do + xlength[i] = math.floor((xlength[i] - 1)/ conv_dW_size) + 1 -- Using temporal convolution + end + end + + local initialHidden = encoderRNN:initializeHidden(sample.bsz) + return {{initialHidden, source}, xlength} + end + end +} + + +NPMTModel.prepareHidden = argcheck{ + {name='self', type='NPMTModel'}, + call = function(self) + local decoderRNN = mutils.findAnnotatedNode( + self:network(), + 'decoder' + ) + assert(decoderRNN ~= nil) + + return function(sample) + -- The sample contains a _cont entry if this sample is a + -- continuation of a previous one (for truncated bptt training). In + -- that case, start from the RNN's previous hidden state. + if not sample._cont then + return decoderRNN:initializeHidden(sample.bsz) + else + return decoderRNN:getLastHidden() + end + end + end +} + +NPMTModel.prepareInput = argcheck{ + {name='self', type='NPMTModel'}, + call = function(self) + local buffers = { + input = {}, + } + + return function(sample) + -- Copy data to device buffers. Recurrent modules expect a table of + -- tensors as their input. + local input = {} + for i = 1, sample.input:size(1) do + buffers.input[i] = buffers.input[i] + or torch.Tensor():type(self:type()) + input[i] = mutils.sendtobuf(sample.input[i], + buffers.input[i]) + end + return input + end + end +} + +NPMTModel.prepareTarget = argcheck{ + {name='self', type='NPMTModel'}, + call = function(self) + local buffers = { + target = torch.Tensor():type(self:type()), + ylength = torch.Tensor():type(self:type()) + } + + return function(sample) + local target = mutils.sendtobuf(sample.target:t(), buffers.target) + local ylength = torch.Tensor(target:size(1)):zero() + + local pad_index = 2 + local eos_index = 3 + local max_ylength = 0 + for i = 1, target:size(1) do + ylength[i] = target:size(2) - torch.sum(target[i]:eq(pad_index)) + ylength[i] = ylength[i] - torch.sum(target[i]:eq(eos_index)) + max_ylength = math.max(ylength[i], max_ylength) + target[{i, ylength[i]+1}] = pad_index + end + target = target[{{},{1,max_ylength}}]:clone() + local ylength = mutils.sendtobuf(ylength, buffers.ylength) + + return {target, ylength} + end + end +} + +NPMTModel.prepareSample = argcheck{ + {name='self', type='NPMTModel'}, + call = function(self) + local prepareSource = self:prepareSource() + local prepareTarget = self:prepareTarget() + return function(sample) + local source = prepareSource(sample) + local target = prepareTarget(sample) + + local source, xlength = source[1], source[2] + local target, ylength = target[1], target[2] + sample.target = target + sample.input = {source, xlength, target, ylength} + end + end +} + + +NPMTModel.generate = argcheck{ + doc=[[ +Sentence generation. See search.lua for a description of search functions. +]], + {name='self', type='Model'}, + {name='config', type='table'}, + {name='sample', type='table'}, + {name='search', type='table'}, + call = function(self, config, sample, search) + local dict = config.dict + local minlen = config.minlen + local maxlen = config.maxlen + local bsz = sample.source:size(2) + local bbsz = config.beam * bsz + local callbacks = self:generationCallbacks(config, bsz) + local vocabsize = sample.targetVocab and sample.targetVocab:size(1) or dict:size() + + local timers = { + setup = torch.Timer(), + encoder = torch.Timer(), + decoder = torch.Timer(), + search_prune = torch.Timer(), + search_results = torch.Timer(), + } + + for k, v in pairs(timers) do + v:stop() + v:reset() + end + + timers.setup:resume() + local state = callbacks.setup(sample) + if cuda.cutorch then + cuda.cutorch.synchronize() + end + timers.setup:stop() + + timers.encoder:resume() + callbacks.encode(state) + timers.encoder:stop() + + timers.decoder:resume() + local results, output_count, num_segments = callbacks.decode(state) + if cuda.cutorch then + cuda.cutorch.synchronize() + end + timers.decoder:stop() + + timers.search_results:resume() +-- local results = table.pack(search.results()) + callbacks.finalize(state, sample, results) + timers.search_results:stop() + + local times = {} + for k, v in pairs(timers) do + times[k] = v:time() + end + -- hypos, scores, attns, t + local attns = {} + for i = 1, #results[2] do + attns[i] = torch.zeros(1, vocabsize) + end + table.insert(results, attns) + table.insert(results, times) + table.insert(results, output_count) + table.insert(results, num_segments) + -- TODO expect hypos, scores, attns, t + return table.unpack(results) + end +} + + +NPMTModel.generationSetup = argcheck{ + {name='self', type='NPMTModel'}, + {name='config', type='table'}, + {name='bsz', type='number'}, + call = function(self, config, bsz) + local beam = config.beam + local bbsz = beam * bsz + local m = self:network() + local prepareSource = self:prepareSource() + return function(sample) + m:evaluate() + local source = prepareSource(sample) + local state = { + sourceIn = source[1], + xlength = source[2], + } + return state + end + end +} + +NPMTModel.generationEncode = argcheck{ + {name='self', type='NPMTModel'}, + {name='config', type='table'}, + {name='bsz', type='number'}, + call = function(self, config, bsz) + local m = self:network() + local encoder = mutils.findAnnotatedNode(m, 'encoder') + local beam = config.beam + local bbsz = beam * bsz + + return function(state) + local encoderOut = encoder:forward(state.sourceIn) + + -- There will be 'beam' hypotheses for each sentence in the batch, + -- so duplicate the encoder output accordingly. +-- local index = torch.range(1, bsz + 1, 1 / beam) +-- index = index:narrow(1, 1, bbsz):floor():long() +-- for i = 1, encoderOut:size(1) do +-- encoderOut[i] = encoderOut[i]:index(1, index) +-- end + state.encoderOut = encoderOut + end + end +} + +NPMTModel.generationDecode = argcheck{ + {name='self', type='NPMTModel'}, + {name='config', type='table'}, + {name='bsz', type='number'}, + call = function(self, config, bsz) + local m = self:network() + + local npmt = mutils.findAnnotatedNode(m, 'npmt') + assert(npmt ~= nil) + -- TODO add more parameters for beam search + config.beam_size = config.beam + config.word_weight = config.lenpen + return function(state, targetIn) + local output_seqs, output_probs + local output_counts, num_segments = 0, 0 + if config.beam == 1 then + output_seqs, output_probs, output_counts, num_segments = npmt:predict(state.encoderOut, state.xlength, config.verbose or false) + else + output_seqs, output_probs = npmt:beam_search(state.encoderOut, state.xlength, config) + end + return {output_seqs, output_probs}, output_counts, num_segments + end + end +} + +NPMTModel.generationUpdate = argcheck{ + {name='self', type='NPMTModel'}, + {name='config', type='table'}, + {name='bsz', type='number'}, + call = function(self, config, bsz) + local bbsz = config.beam * bsz + local m = self:network() + local decoderRNN = mutils.findAnnotatedNode(m, 'decoder') + assert(decoderRNN ~= nil) + + return function(state, indexH) + local lastH = decoderRNN:getLastHidden(bbsz) + for i = 1, #state.prevhIn do + for j = 1, #state.prevhIn[i] do + local dim = lastH[i][j]:dim() - 1 + state.prevhIn[i][j]:copy(lastH[i][j]:index(dim, indexH)) + end + end + end + end +} + +function NPMTModel:float(...) + self.module:replace(function(m) + if torch.isTypeOf(m, 'nn.WrappedCudnnRnn') then + return mutils.wrappedCudnnRnnToLSTMs(m) + elseif torch.typename(m) == 'nn.SequenceTable' then + -- Use typename() to avoid matching RecurrentTables + return mutils.replaceCudnnRNNs(m) + end + return m + end) + return parent.float(self, ...) +end + +return NPMTModel diff --git a/fairseq/models/npmt_utils.lua b/fairseq/models/npmt_utils.lua new file mode 100755 index 0000000..e75ebc1 --- /dev/null +++ b/fairseq/models/npmt_utils.lua @@ -0,0 +1,767 @@ +-- Copyright (c) Microsoft Corporation. All rights reserved. +-- Licensed under the MIT License. +-- +--[[ +-- +-- Auxiliary functions +-- +--]] +-- + +-- log(ret) = log(a + b) +function torch.logadd(log_a, log_b) + if (type(log_a) == 'number') then + return math.max(log_a, log_b) + torch.log1p(torch.exp(-torch.abs(log_a - log_b))) + else + return torch.cmax(log_a, log_b) + torch.log1p(torch.exp(-torch.abs(log_a - log_b))) + end +end + +-- log(ret) = log(1-a) +function torch.log1sub(log_a) + return torch.log1p(-torch.exp(log_a)) +end + +-- log(ret) = log(a - b) +function torch.logsub(log_a, log_b) + assert(torch.all(torch.ge(log_a, log_b))) + return log_a + torch.log1p(-torch.exp(log_b - log_a) + 1e-15) +end + +-- log(ret) = log(+inf) +function torch.loginf() + return 1000000 +end + +function torch.copy_array(tab) + return torch.Tensor(tab):totable() +end + +function torch.last_nelements(tab, n, pad) + local n = n or 1 + local pad = pad or -1 + local out = torch.Tensor(#tab, n) + for i = 1, #tab do + for j = 1, n do + out[{i,j}] = tab[i][#(tab[i]) - n + j] or pad + end + end + return out +end + +-- generate n random rules for a vocabulary of V (excluding 1 and V) and probabilities specified in probs +-- return: a table of size n, each being a table of two 1-dimensional tensors +function gen_random_rules(V, n, probs, use_ctc, longer_input) + local lens_src + if use_ctc then + lens_src = torch.Tensor(n):fill(probs:nElement()) + assert(not longer_input, "use_ctc can not be used with longer input") + else + if longer_input then + lens_src = torch.Tensor(n):random(3, 15) + else + lens_src = torch.multinomial(probs, n, true) + end + end + local lens_trgt = torch.multinomial(probs, n, true) + if longer_input then + lens_trgt = torch.Tensor(n):random(1, 6) + end + local rules = {} + for i = 1, n do + local t_src = torch.Tensor(lens_src[i]):random(1, V) + local t_trgt = torch.Tensor(lens_trgt[i]):random(1, V) + table.insert(rules, {t_src, t_trgt}) + end + return rules +end + +-- generate n sequences each of length T, using those specified in rules +-- return three tables: input, output and markers, each has exactly n elements +function gen_random_data(rules, n, T, sort_output) + if sort_output then + print("--- the outputs are sorted ---") + end + local input = {} + local output = {} + local markers = {} + + for i = 1, n do + local output_indices = {} + local t_src = {} + local t_trgt = {} + local t_marker = {} + local count = 0 + local random_T = torch.random(1, T) + local rule_idx = {} + for k = 1, random_T do + local j = torch.random(1, #rules) + table.insert(rule_idx, j) + end + rule_idx = torch.sort(torch.Tensor(rule_idx)) + for k = 1, random_T do + local j = rule_idx[k] + table.insert(t_src, rules[j][1]) + table.insert(t_trgt, rules[j][2]) + count = count + rules[j][1]:nElement() + table.insert(t_marker, count) + table.insert(output_indices, j) + end + table.insert(input, torch.cat(t_src)) + table.insert(markers, torch.Tensor(t_marker)) + if sort_output then + local t_trgt_sorted = {} + _, indices = torch.sort(torch.Tensor(output_indices)) + for j = 1, indices:nElement() do + table.insert(t_trgt_sorted, t_trgt[indices[j]]) + end + table.insert(output, torch.cat(t_trgt_sorted)) + else + table.insert(output, torch.cat(t_trgt)) + end + end + return input, output, markers +end + +function toy_prepare_minibatch(input_table, output_table, markers_table, p, batch_size) + + local T1 = input_table[p]:nElement() + local T2 = 0 + local input = torch.Tensor(batch_size, T1):fill(2) + local markers = {} + local ylength = torch.Tensor(batch_size) + for i = 1, batch_size do + input[{i,{}}]:copy(input_table[p+i-1]) + table.insert(markers, markers_table[p+i-1]) + ylength[i] = output_table[p+i-1]:nElement() + if (ylength[i] > T2) then + T2 = ylength[i] + end + end + local yref = torch.Tensor(batch_size, T2):fill(2) + for i = 1, batch_size do + yref[{i,{1,ylength[i]}}]:copy(output_table[p+i-1]) + end + + return input, yref, ylength, markers +end + +function toy_evaluate_zeta_loss(logprob, markers) + + local loss = 0 + local T1 = logprob:size(2) + for i = 1, #markers do + loss = loss + torch.sum(logprob[{i,{}}]:gather(1, markers[i]:long())) + end + + return loss + +end + +function get_toy_data(V, sort_output, use_ctc, longer_input) + local n_train = 16384 + local n_test = 128 + local m = 100 + local T1 = 6 + local V = V + + -- generate data + local rules = gen_random_rules(V, m, torch.Tensor({1/3,1/3,1/3}), false, longer_input) + local input_train, output_train, markers_train = gen_random_data(rules, n_train, T1, sort_output) + local input_test, output_test, markers_test = gen_random_data(rules, n_test, T1, sort_output) + return {rules, + input_train, + output_train, + markers_train, + input_test, + output_test, + markers_test} +end + +function sort_data_by_length(input, output, max_sen_len) + max_sen_len = max_sen_len or nil + local all_lengths = {} + for i = 1, #input do + table.insert(all_lengths, input[i]:nElement()) + end + local _, sorted_idx = torch.sort(torch.Tensor(all_lengths)) + local sorted_input = {} + local sorted_output = {} + for i = 1, #input do + local j = sorted_idx[i] + if not max_sen_len then + table.insert(sorted_input, input[j]) + table.insert(sorted_output, output[j]) + elseif input[j]:nElement() <= max_sen_len then + table.insert(sorted_input, input[j]) + table.insert(sorted_output, output[j]) + end + end + return sorted_input, sorted_output +end + + +local log0 = -1000000. +function prepare_minibatch_3d(input, output, s, t, params) + local y_vocab = params.target_vocab_size + + local input_max_len = 0 + local input_feature_dim = 123 + local output_max_len = 0 + local batch_size = t - s + 1 + for i = 1, batch_size do + input_max_len = math.max(input_max_len, input[s+i-1]:nElement()/input_feature_dim) + output_max_len = math.max(output_max_len, output[s+i-1]:nElement()) + end + local batch_input = torch.Tensor(batch_size, input_max_len, input_feature_dim):fill(params.start_symbol) + local batch_output = torch.Tensor(batch_size, output_max_len):fill(y_vocab) + local xlength = torch.Tensor(batch_size):zero() + local ylength = torch.Tensor(batch_size):zero() + for i = 1, batch_size do + local input_sequence_length = input[s+i-1]:nElement() / input_feature_dim + batch_input[{i, {1, input_sequence_length}, {}}]:copy(input[s+i-1]:reshape(input_sequence_length, input_feature_dim)) + batch_output[{i, {1,output[s+i-1]:nElement()}}]:copy(output[s+i-1]) + if params.input_temporalconv_stride > 0 then + xlength[i] = math.floor((input_sequence_length - params.input_temporalconv_width)/params.input_temporalconv_stride) + 1 -- Using temporal convolution + else + xlength[i] = input_sequence_length + end + ylength[i] = output[s+i-1]:nElement() + end +-- local debugger = require('fb.debugger') +-- debugger.enter() + + if params.temporal_sampling == 'TemporalSampling' then + for i = 1, batch_size do + xlength[i] = math.floor(xlength[i] / params.temporalsampling_stride) + if xlength[i] == 0 then + xlength[i] = 1 + end + end + elseif params.temporal_sampling == 'TemporalConvolution' then + for i = 1, batch_size do + xlength[i] = math.floor((xlength[i] - params.temporalconv_width) / params.temporalconv_stride) + 1 + end + end + + return batch_input, batch_output, xlength, ylength +end + +local log0 = -1000000. +function prepare_minibatch(input, output, s, t, x_vocab, y_vocab, end_symbol, conv_dW_size, params) + local input_max_len = 0 + local output_max_len = 0 + local batch_size = t - s + 1 + for i = 1, batch_size do + input_max_len = math.max(input_max_len, input[s+i-1]:nElement()) + output_max_len = math.max(output_max_len, output[s+i-1]:nElement()) + end + if end_symbol and end_symbol > 0 then + input_max_len = input_max_len + 1 + output_max_len = output_max_len + 1 + end + local batch_input = torch.Tensor(batch_size, input_max_len):fill(params.start_symbol) + local mask_input = torch.Tensor(batch_size, input_max_len):fill(log0) -- logspace + local batch_output = torch.Tensor(batch_size, output_max_len):fill(y_vocab) + local xlength = torch.Tensor(batch_size):zero() + local ylength = torch.Tensor(batch_size):zero() + for i = 1, batch_size do + batch_input[{i, {1,input[s+i-1]:nElement()}}]:copy(input[s+i-1]) + mask_input[{i, {1, input[s+i-1]:nElement()}}]:zero() -- logspace + batch_output[{i, {1,output[s+i-1]:nElement()}}]:copy(output[s+i-1]) + xlength[i] = input[s+i-1]:nElement() + ylength[i] = output[s+i-1]:nElement() + if end_symbol and end_symbol > 0 then + batch_input[{i, input[s+i-1]:nElement()+1}] = end_symbol + mask_input[{i, input[s+i-1]:nElement()+1}] = 0 + batch_output[{i, output[s+i-1]:nElement()+1}] = end_symbol + xlength[i] = xlength[i] + 1 + ylength[i] = ylength[i] + 1 + end + end + + if params.input_temporalconv_stride > 0 then + input_max_len = math.floor((input_max_len - 1)/params.input_temporalconv_stride) + 1 -- Using temporal convolution + mask_input:resize(batch_size, input_max_len):fill(log0) + for i = 1, batch_size do + xlength[i] = math.floor((xlength[i] - 1)/params.input_temporalconv_stride) + 1 -- Using temporal convolution + mask_input[{i, {1, xlength[i]}}] = 0 + end + end + + if conv_dW_size then + input_max_len = math.floor((input_max_len - 1) / conv_dW_size) + 1 + mask_input:resize(batch_size, input_max_len):fill(log0) + for i = 1, batch_size do + xlength[i] = math.floor((xlength[i] - 1) / conv_dW_size) + 1 + mask_input[{i, {1, xlength[i]}}] = 0 + end + end + + return batch_input, batch_output, xlength, ylength, mask_input +end + +function g_cloneManyTimes(net, T) + net:clearState() + local clones = {} + local params, gradParams = net:parameters() + local mem = torch.MemoryFile("w"):binary() + mem:writeObject(net) + for t = 1, T do + -- We need to use a new reader for each clone. + -- We don't want to use the pointers to already read objects. + local reader = torch.MemoryFile(mem:storage(), "r"):binary() + local clone = reader:readObject() + reader:close() + local cloneParams, cloneGradParams = clone:parameters() + for i = 1, #params do + cloneParams[i]:set(params[i]) + cloneGradParams[i]:set(gradParams[i]) + end + clones[t] = clone + collectgarbage() + end + mem:close() + return clones +end + +function toy_evaluate_predict_loss_mapping(ypred, yref, use_crossentropy, mapping_path, without_mapping) + local phones_mapping = {} + if without_mapping then + for _id = 1, 29 do + phones_mapping[_id] = _id + end + else + for line in io.lines('%s/phones.60-39.mapping' % mapping_path) do + splits = line:split('\t') + phones_mapping[tonumber(splits[1]) + 1] = splits[2] + end + end + + assert(#ypred == #yref) + + local count = 0 + local loss = 0 + local WER = 0 + local total_num_words = 0 + + local sequenceError = SequenceError() + for i = 1, #ypred do + local ypred_mapping = '' + local prevalue = -1 + for j = 1, ypred[i]:nElement() do + if use_crossentropy then + _value = torch.floor((ypred[i][j] - 1) / 3) + 1 + else + _value = ypred[i][j] + end + if phones_mapping[_value] then + ypred_mapping = ypred_mapping .. ' ' .. phones_mapping[_value] + end + end + + local yref_mapping = '' + local prevalue = -1 + for j = 1, yref[i]:nElement() do + if use_crossentropy then + _value = torch.floor((yref[i][j] - 1) / 3) + 1 + else + _value = yref[i][j] + end + if phones_mapping[_value] then -- and _value ~= prevalue then + yref_mapping = yref_mapping .. ' ' .. phones_mapping[_value] + end + end + + local word_errs , num_words = sequenceError:calculateWER(yref_mapping:gsub("^%s*(.-)%s*$", "%1"), ypred_mapping:gsub("^%s*(.-)%s*$", "%1")) + WER = WER + word_errs + total_num_words = total_num_words + num_words + + -- unmerged edit distance, count + loss = loss + EditDistance(torch.totable(ypred[i]:long()), torch.totable(yref[i]:long())) + count = count + yref[i]:nElement() + end + print('WER: ', WER, ' total num words: ', total_num_words) + print(' ==> test predict WER: ', WER / total_num_words * 100) + print(' ==> test predict edit distance (normalized): ', loss / count) + return WER / total_num_words * 100 +end + + +function toy_evaluate_predict_loss(ypred, yref) + assert(#ypred == #yref) + + local count = 0 + local loss = 0 + for i = 1, #ypred do + loss = loss + EditDistance(torch.totable(ypred[i]:long()), torch.totable(yref[i]:long())) + count = count + yref[i]:nElement() + end + return loss / count +end + +function generate_batches_by_length(input_train, batch_size, req_same_length) + --- input train must have sorted + local batches = {} + if req_same_length then + local count = 0 + local cur_len = input_train[1]:nElement() + local s = 1 + local t = 1 + for i = 1, #input_train do + if count < batch_size and input_train[i]:nElement() == cur_len then + t, count = i, count + 1 + else + table.insert(batches, {s, t}) + s, t = i, i + count = 1 + cur_len = input_train[i]:nElement() + end + end + if count > 0 then + table.insert(batches, {s, t}) + end + else + local num_batches = math.ceil(#input_train / batch_size) + for i = 1, num_batches do + local s = (i-1)*batch_size + 1 + local t = math.min(s + batch_size - 1, #input_train) + table.insert(batches, {s,t}) + end + end + return batches +end + +---- machine translation specific --- + +function load_data(filename, note, type) + local note = note or filename + print(string.format("--loading %s data from %s", note, filename)) + local data = {} + for line in io.lines(filename) do + splits = line:split(" ") + if type == 'Double' then + table.insert(data, torch.DoubleTensor(splits)) + else + table.insert(data, torch.LongTensor(splits)) + end + end + print(string.format(" %s data size: %d", note, #data)) + return data +end + +function load_vocab(filename, note) + local note = note or filename + print(string.format("--loading %s vocab from %s", note, filename)) + local vocab = {} + for line in io.lines(filename) do + splits = line:split(" ") + if splits[1] == '' then + vocab[tonumber(splits[2])] = ' ' + else + vocab[tonumber(splits[2])] = splits[1] + end + if (not splits[2]) or splits[3] then + print("error") + end + end + -- the actual size is #vocab + 1, since we use 0 as padding + print(string.format(" %s vocab size: %d", note, #vocab)) + return vocab +end + +function tensor_to_string(x) + local phrases = {} + for i = 1, x:nElement() do + table.insert(phrases, x[i]) + end + return table.concat(phrases, ' ') +end + +function decipher_tables(x, vocab) + local decipher_tables = {} + -- sed -r 's/(@@ )|(@@ ?$)//g' + for i = 1, #x do + local decipher_str = decipher(x[i]:view(-1, 1), vocab) + local decipher_str_merge = string.gsub(string.gsub(decipher_str, "@@ ", ""), "@@ ", "") + table.insert(decipher_tables, decipher_str_merge) + end + return decipher_tables +end + + +function decipher(x, ivocab) + local phrases = {} + for i = 1, x:nElement() do + local phrase = ivocab[torch.totable(x[i])[1]] or 'UNK' + table.insert(phrases, phrase) + end + return table.concat(phrases, ' ') +end + +function eval_bleu_score(refs, outputs, work_dir, iter, unk_as_error, config_dir, unk_symbol, is_string) + assert(#outputs == #refs) + local ref_filename + if is_string then + ref_filename = string.format("%s/refs-at-iter-%d.txt", work_dir, iter) + ref_f = assert(io.open(ref_filename, "w")) + for i = 1, #refs do + ref_f:write(refs[i], "\n") + end + ref_f:close() + else -- otherwise, table of tensor + if unk_as_error then + ref_filename = string.format("%s/refs-at-iter-%d-unk-as-error.txt", work_dir, iter) + ref_f = assert(io.open(ref_filename, "w")) + for i = 1, #refs do + local new_ref = refs[i]:long() - torch.cmul(refs[i]:long(), refs[i]:eq(unk_symbol):long()) + ref_f:write(table.concat(new_ref:totable(), " "), "\n") + end + ref_f:close() + else + ref_filename = string.format("%s/refs-at-iter-%d.txt", work_dir, iter) + ref_f = assert(io.open(ref_filename, "w")) + for i = 1, #refs do + ref_f:write(table.concat(refs[i]:totable(), " "), "\n") + end + ref_f:close() + end + end + + local output_filename + if unk_as_error then + output_filename = string.format("%s/results-at-iter-%d-unk-as-error.txt", work_dir, iter) + else + output_filename = string.format("%s/results-at-iter-%d.txt", work_dir, iter) + end + local out_f = assert(io.open(output_filename, "w")) + + if is_string then + for i = 1, #outputs do + out_f:write(outputs[i], "\n") + end + else -- otherwise, table of tensor + for i = 1, #outputs do + out_f:write(table.concat(outputs[i]:totable(), " "), "\n") + end + end + out_f:close() + if config_dir == nil then + config_dir = './' + end + local cmd = string.format("perl %s/data_processing/multi-bleu.perl %s < %s", config_dir, ref_filename, output_filename) + os.execute(cmd) + local cmd = string.format("perl %s/data_processing/multi-bleu.perl %s < %s", config_dir, ref_filename, output_filename) + local handle = io.popen(cmd) + local result = handle:read("*a") + handle:close() + _, str_end = string.find(result, 'EVALERR =') + return tonumber(string.sub(result, str_end + 1)) +end + +function eval_wer_score(refs, outputs, work_dir, iter, without_mapping) + assert(#outputs == #refs) + local ref_filename + + ref_filename = string.format("%s/refs-at-iter-%d.txt", work_dir, iter) + ref_f = assert(io.open(ref_filename, "w")) + for i = 1, #refs do + ref_f:write(i, ' ', table.concat(refs[i]:totable(), " "), "\n") + end + ref_f:close() + -- end + + local output_filename + output_filename = string.format("%s/results-at-iter-%d.txt", work_dir, iter) + local out_f = assert(io.open(output_filename, "w")) + for i = 1, #outputs do + out_f:write(i, ' ', table.concat(outputs[i]:totable(), " "), "\n") + end + out_f:close() + if without_mapping then + local cmd = string.format("sh /home/pshuang/work/tools/kaldi/egs/timit/s5/local/score_timit_womapping.sh %s %s %s %s", output_filename, ref_filename, work_dir, iter) + os.execute(cmd) + else + local cmd = string.format("sh /home/pshuang/work/tools/kaldi/egs/timit/s5/local/score_timit.sh %s %s %s %s", output_filename, ref_filename, work_dir, iter) + os.execute(cmd) + end +end + +function yref_totable(yref, ylength) + local bsize = yref:size(1) + local yref_tab = {} + for i = 1, bsize do + table.insert(yref_tab, torch.totable(yref[{i, {1, ylength[i]}}])) + end + return yref_tab +end + +function ctc_decodeOutput(predictions) + --[[ + Turns the predictions tensor into a list of the most likely tokens + NOTE: + to compute WER we strip the begining and ending spaces + --]] + local tokens = {} + local blankToken = 0 + local preToken = blankToken + -- The prediction is a sequence of likelihood vectors + local _, maxIndices = torch.max(predictions, 2) + maxIndices = maxIndices:float():squeeze() + + for i=1, maxIndices:size(1) do + local token = maxIndices[i] - 1 -- CTC indexes start from 1, while token starts from 0 + -- add token if it's not blank, and is not the same as pre_token + if token ~= blankToken and token ~= preToken then + table.insert(tokens, token) + end + preToken = token + end + return torch.Tensor(tokens) +end + +function ctc_beam_search(predictions, configs) + local beam_size = configs.beam_size or 40 + local use_avg_prob = configs.use_avg_prob + local softmax = nn.LogSoftMax():cuda() + local preds = softmax:forward(predictions):double() + + local T, V = preds:size(1), preds:size(2) + local B = {{}} + local B_inv = nil + local Pr = {0} + local pnb_ = {-torch.loginf()} + local pb_ = {0} + for t = 1, T do + local B_new, Pr_new, pnb_new, pb_new = {}, {}, {}, {} + local _, ind = torch.sort(torch.Tensor(Pr), true) + B_inv = {} + for i = 1, math.min(beam_size, ind:nElement()) do + local j = ind[i] + B_inv[table.concat(B[j], "-")] = j + end + for i = 1, math.min(beam_size, ind:nElement()) do + local j = ind[i] + local y = B[j] + local pnb = -torch.loginf() + if #y > 0 then + pnb = pnb_[j] + preds[{t, y[#y]+1}] + local y_1_str = '' + if #y > 1 then + y_1_str = table.concat(torch.totable(torch.Tensor(y)[{{1,#y-1}}]), "-") + end + local jj = B_inv[y_1_str] + if jj ~= nil then + local y_1 = B[jj] + if y_1_str:len() > 0 and y[#y] == y_1[#y_1] then + pnb = torch.logadd(pnb, pb_[jj] + preds[{t, y[#y]+1}]) + else + pnb = torch.logadd(pnb, Pr[jj] + preds[{t, y[#y]+1}]) + end + end + end + local pb = Pr[j] + preds[{t, 1}] -- 1 is the blank symbol + table.insert(B_new, torch.copy_array(y)) + table.insert(Pr_new, torch.logadd(pnb, pb)) + table.insert(pnb_new, pnb) + table.insert(pb_new, pb) + for v = 2, V do + pb = -torch.loginf() + if #y > 0 and v-1 == y[#y] then + pnb = pb_[j] + preds[{t, v}] + else + pnb = Pr[j] + preds[{t, v}] + end + table.insert(pb_new, pb) + table.insert(pnb_new, pnb) + table.insert(Pr_new, torch.logadd(pnb, pb)) + local y_ = torch.copy_array(y) + table.insert(y_, v-1) + table.insert(B_new, y_) + end + end + B = B_new + Pr = Pr_new + pnb_ = pnb_new + pb_ = pb_new + end + if use_avg_prob then + for i = 1, #Pr do + Pr[i] = Pr[i] / #(B[i]) + end + end + local _, indx = torch.sort(torch.Tensor(Pr), true) + return torch.Tensor(B[indx[1]]) +end + +function xent_decodeOutput(predictions) + --[[ + Turns the predictions tensor into a list of the most likely tokens + NOTE: + to compute WER we strip the begining and ending spaces + --]] + local tokens = {} + local _, maxIndices = torch.max(predictions, 2) + maxIndices = maxIndices:float():squeeze() + for i=1, maxIndices:size(1) do + local token = maxIndices[i] + table.insert(tokens, token) + end + return torch.Tensor(tokens) +end + +function file_exists(file) + local f = io.open(file, "rb") + if f then f:close() end + return f ~= nil +end + +function line_from(file) + if not file_exists(file) then return 0 end + local f = io.open(file, "r") + io.input(f) + local data = io.read() + io.close(f) + return data +end + +function lines_from(file, type) + if not file_exists(file) then return 0 end + local data = {} + for line in io.lines(file) do + line = string.gsub(line, "\n", "") + if type == 'Int' then + table.insert(data, tonumber(line)) + elseif type == 'phrases' then + local line_split = line:split(" ") + local subtable = {} + for i = 1, #line_split do + table.insert(subtable, tonumber(line_split[i])) + end + table.insert(data, table.concat(subtable, '-')) + else + table.insert(data, line) + end + end + return data +end + + +function Set(list) + local set = {} + for _, l in ipairs(list) do set[l] = true end + return set +end + +function tensor_to_table(input) + local table_input = input:totable() + return Set(table_input) +end + +function table.slice(tbl, first, last, step) + local sliced = {} + for i = first or 1, last or #tbl, step or 1 do + sliced[#sliced+1] = tbl[i] + end + return sliced +end diff --git a/fairseq/models/utils.lua b/fairseq/models/utils.lua index 0de0d28..5be8964 100644 --- a/fairseq/models/utils.lua +++ b/fairseq/models/utils.lua @@ -5,6 +5,9 @@ -- the root directory of this source tree. An additional grant of patent rights -- can be found in the PATENTS file in the same directory. -- +-- Copyright (c) Microsoft Corporation. All rights reserved. +-- Licensed under the BSD License. +-- --[[ -- -- Shared utility functions used for model construction. @@ -112,6 +115,11 @@ function mutils.loadLegacyModel(path, typename) return model end +function mutils.loadModel(path, typename) + return torch.load(path) +end + + function mutils.sendtobuf(data, buffer) assert(data and torch.isTensor(data)) assert(buffer and torch.isTensor(buffer)) diff --git a/fairseq/models/window_attn.lua b/fairseq/models/window_attn.lua new file mode 100755 index 0000000..4347f73 --- /dev/null +++ b/fairseq/models/window_attn.lua @@ -0,0 +1,128 @@ +-- Copyright (c) Microsoft Corporation. All rights reserved. +-- Licensed under the MIT License. +-- +--[[ +-- +-- Reordering layer +-- +--]] +-- +require("nn") +require("nngraph") + +local winAttn, parent = torch.class('nn.winAttn', 'nn.Container') + +function make_win_unit(input_size, kW) + local inputs = {} + table.insert(inputs, nn.Identity()()) + local x = unpack(inputs) -- B * kW * d + local reshaped_x = nn.Reshape(input_size*kW, true)(x) -- B * (kW*d) + local weight = nn.Sigmoid()(nn.Linear(input_size*kW, kW)(reshaped_x)):annotate{name = 'winAtt_weight'} -- B * kW + weight = nn.Replicate(input_size, 3)(weight) -- B * kW * d + local output = nn.CMulTable()({x, weight}) -- B * kW * d + output = nn.Tanh()(nn.Sum(2)(output)) -- B * d + return nn.gModule(inputs, {output}) +end + +function winAttn:__init(input_size, kW, use_middle) + parent.__init(self) + self.gradInput = torch.Tensor() + self.output = torch.Tensor() + self.padded_input = torch.Tensor() + + self.input_size = input_size + self.kW = kW + if use_middle then + local width = math.floor(self.kW / 2) + self.kW = width * 2 + 1 + self.padding = nn.Sequential():add(nn.Padding(2, -width)):add(nn.Padding(2, width)) + else + self.padding = nn.Padding(2, 1 - self.kW) + end + + self:add(self.padding) + self.win_unit = make_win_unit(input_size, self.kW) + self:add(self.win_unit) + self.win_unit_clones = {} + self.max_T = 0 +end + +function winAttn:updateOutput(input) + self.recompute_backward = true + + local T = input:size(2) + if self.max_T < T then + self.win_unit:clearState() + local more_win_units = g_cloneManyTimes(self.win_unit, T - self.max_T) + for i = 1, T - self.max_T do + table.insert(self.win_unit_clones, more_win_units[i]) + end + self.max_T = T + end + for t = 1, T do + self.win_unit_clones[t]:clearState() + end + self.padded_input = self.padding:updateOutput(input) + self.output = input.new(input:size()):zero() +-- local mutils = require 'fairseq.models.utils' + for t = 1, T do + local x = self.padded_input[{{}, {t, t+self.kW-1}, {}}] + local y = self.win_unit_clones[t]:updateOutput(x) + self.output[{{}, t, {}}]:add(y) +-- local weight = mutils.findAnnotatedNode(self.win_unit_clones[t], 'winAtt_weight') +-- print('t', t, 'weight', weight.output) +-- print(self.win_unit_clones[t].forwardnodes[6].data.input[1]) + end + return self.output +end + +function winAttn:backward(input, gradOutput, scale) + local scale = scale or 1 + self.recompute_backward = false + local grad_padded_input = self.padded_input.new(self.padded_input:size()):zero() + local T = input:size(2) + for t = 1, T do + local x = self.padded_input[{{}, {t, t+self.kW-1}, {}}] + local grad_x = self.win_unit_clones[t]:backward(x, gradOutput[{{}, t, {}}]) + grad_padded_input[{{}, {t, t+self.kW-1}, {}}]:add(grad_x) + end + self.gradInput = self.padding:backward(input, grad_padded_input, scale) + return self.gradInput +end + +function winAttn:updateGradInput(input, gradOutput) + if self.recompute_backward then + self:backward(input, gradOutput, 1.0) + end + return self.gradInput +end + +function winAttn:accGradParameters(input, gradOutput, scale) + if self.recompute_backward then + self:backward(input, gradOutput, scale) + end +end + +function winAttn:training() + parent.training(self) + for t = 1, self.max_T do + self.win_unit_clones[t]:training() + end +end + +function winAttn:evaluate() + parent.evaluate(self) + for t = 1, self.max_T do + self.win_unit_clones[t]:evaluate() + end +end + +function winAttn:clearState() + parent.clearState(self) + self.output:set() + self.padded_input:set() + self.gradInput:set() + for t = 1, self.max_T do + self.win_unit_clones[t]:clearState() + end +end diff --git a/fairseq/torchnet/ResumableDPOptimEngine.lua b/fairseq/torchnet/ResumableDPOptimEngine.lua index 9575e18..bddf99b 100644 --- a/fairseq/torchnet/ResumableDPOptimEngine.lua +++ b/fairseq/torchnet/ResumableDPOptimEngine.lua @@ -5,6 +5,9 @@ -- the root directory of this source tree. An additional grant of patent rights -- can be found in the PATENTS file in the same directory. -- +-- Copyright (c) Microsoft Corporation. All rights reserved. +-- Licensed under the BSD License. +-- --[[ -- -- A version of OptimEngine that implements data parallelism and @@ -16,7 +19,7 @@ local tnt = require 'torchnet' local argcheck = require 'argcheck' local utils = require 'fairseq.utils' local threads = require 'threads' - +local mutils = require 'fairseq.models.utils' local cuda = utils.loadCuda() local ResumableDPOptimEngine = @@ -215,10 +218,24 @@ ResumableDPOptimEngine.test = argcheck{ _G.prepareSample(sample) _G.model:resizeCriterionWeights( _G.criterion, _G.critweights, sample) + -- HACK for not having OOM + local group_size + if torch.typename(_G.model) == 'NPMTModel' then + local npmt = mutils.findAnnotatedNode(_G.model:network(), 'npmt') + group_size = npmt.group_size + npmt.group_size = 64 + end local net = _G.model:network() local crit = _G.criterion net:forward(sample.input) crit:forward(net.output, sample.target) + if torch.typename(_G.model) == 'NPMTModel' then + local npmt = mutils.findAnnotatedNode(_G.model:network(), 'npmt') + npmt.group_size = group_size + npmt:clearState() + _G.model:network():clearState() + end + collectgarbage() collectgarbage() return crit.output end, @@ -350,7 +367,7 @@ ResumableDPOptimEngine.doTrain = argcheck{ if sample then state.ntokens = state.ntokens + sample.ntokens self.pool:addjob(shardid, - function(optconfig, sample, clipv, prevn) + function(optconfig, sample, clipv, prevn, epoch) -- Clip gradients and update parameters. -- Note: this is being done for the -- previous sample. @@ -362,7 +379,6 @@ ResumableDPOptimEngine.doTrain = argcheck{ optconfig.method(_G.feval, _G.params, optconfig, _G.optstate) end - -- Process the current sample. _G.prepareSample(sample) _G.model:resizeCriterionWeights( @@ -378,13 +394,14 @@ ResumableDPOptimEngine.doTrain = argcheck{ crit:backward(net.output, sample.target) net:backward(sample.input, crit.gradInput) collectgarbage() + collectgarbage() return crit.output end, function(loss) state.loss = state.loss + loss end, state.epoch_t > 0 and state.optconfig or nil, - sample, clipv, prevn + sample, clipv, prevn, state.epoch ) end end diff --git a/fairseq/torchnet/hooks.lua b/fairseq/torchnet/hooks.lua index 44425bc..466787f 100644 --- a/fairseq/torchnet/hooks.lua +++ b/fairseq/torchnet/hooks.lua @@ -5,6 +5,9 @@ -- the root directory of this source tree. An additional grant of patent rights -- can be found in the PATENTS file in the same directory. -- +-- Copyright (c) Microsoft Corporation. All rights reserved. +-- Licensed under the BSD License. +-- --[[ -- -- Common torchnet engine hooks. The general format for functions declared @@ -185,11 +188,16 @@ hooks.runGeneration = argcheck{ local scorer = clib.bleu(dict:getPadIndex(), dict:getEosIndex()) local fp = outfile and io.open(outfile, 'a') local targetBuf = torch.IntTensor() + local output_counts, num_segments = 0, 0 for samples in iterator() do -- We don't shard generation computeSampleStats({samples = samples}) local sample = samples[1] - local hypos, scores, attns, _ = generate(model, sample) + local hypos, scores, attns, _, output_count, num_segment = generate(model, sample) + if output_count and num_segment then + output_counts = output_counts + output_count + num_segments = num_segments + num_segment + end local targetTT = sample.target:t() local targetT = targetBuf:resizeAs(targetTT):copy(targetTT) local beam = #hypos / sample.bsz @@ -233,7 +241,9 @@ hooks.runGeneration = argcheck{ scorer:add(ref, hypo:int()) end end - + if num_segments > 0 then + print(string.format("avg. phrase size %f", output_counts / num_segments)) + end if fp then fp:close() end @@ -263,6 +273,7 @@ hooks.onCheckpoint = argcheck{ isAnnealing = false, prevvalloss = nil, bestvalloss = nil, + bestvalbleu = nil, } end @@ -293,8 +304,8 @@ hooks.onCheckpoint = argcheck{ stats['wordspersec'] = lossMeter.n / cptime stats['current_lr'] = state.optconfig.learningRate * config.lrscale - local loss = lossMeter:value() / math.log(2) - local ppl = math.pow(2, loss) + local loss = lossMeter:value() + local ppl = math.min(math.pow(2, loss / math.log(2)), 1000) -- Hack print(string.format( '%s | trainloss %8.2f | train ppl %8.2f', logPrefix, loss, ppl) @@ -308,8 +319,8 @@ hooks.onCheckpoint = argcheck{ for name, set in pairs(testsets) do meter:reset() runTest(state, set, meter) - local loss = meter:value() / math.log(2) - local ppl = math.pow(2, loss) + local loss = meter:value() + local ppl = math.pow(2, loss / math.log(2)) str2print = string.format('%s | %sloss %8.2f | %s ppl %8.2f', str2print, name, loss, name, ppl) stats[name .. 'ppl'] = ppl @@ -351,6 +362,7 @@ hooks.onCheckpoint = argcheck{ local valloss = stats['validloss'] + local valbleu = stats['validbleu'] -- Save model and best model if not config.nosave then @@ -374,6 +386,19 @@ hooks.onCheckpoint = argcheck{ state._onCheckpoint.bestvalloss = valloss end + if valbleu + and (not state._onCheckpoint.bestvalbleu + or valbleu > state._onCheckpoint.bestvalbleu) then + local bestmodelpath = plpath.join(config.savedir, + 'model_bestbleu.th7') + if utils.retry(3, engine.saveModel, engine, bestmodelpath) + then + print(string.format( + '%s | saved new best bleu model to %s', logPrefix, + bestmodelpath)) + end + state._onCheckpoint.bestvalbleu = valbleu + end end io.stdout:flush() diff --git a/generate-lines.lua b/generate-lines.lua index 2a84869..7e2d109 100644 --- a/generate-lines.lua +++ b/generate-lines.lua @@ -5,6 +5,9 @@ -- the root directory of this source tree. An additional grant of patent rights -- can be found in the PATENTS file in the same directory. -- +-- Copyright (c) Microsoft Corporation. All rights reserved. +-- Licensed under the BSD License. +-- --[[ -- -- Hypothesis generation script with text file input, processed line-by-line. @@ -51,6 +54,8 @@ cmd:option('-freqthreshold', -1, 'the minimum frequency for an alignment candidate in order' .. 'to be considered (default no limit)') cmd:option('-fconvfast', false, 'make fconv model faster') +cmd:option('-lm_weight', 0.0, 'external lm weight.') +cmd:option('-lm_path', "", 'external lm path.') local config = cmd:parse(arg) @@ -62,6 +67,13 @@ print(string.format('| [target] Dictionary: %d types', config.dict:size())) config.srcdict = torch.load(config.sourcedict) print(string.format('| [source] Dictionary: %d types', config.srcdict:size())) +if config.lm_weight > 0 and config.lm_path:len() > 0 then +-- os.execute('./compile_lm.sh') + require "lua_lm" + config['lm'] = create_lm_instance(config.lm_path) +end + + if config.aligndictpath ~= '' then config.aligndict = tnt.IndexedDatasetReader{ indexfilename = config.aligndictpath .. '.idx', @@ -145,7 +157,9 @@ local dataset = tnt.DatasetIterator{ } local model -if config.model ~= '' then +if config.model == 'npmt' then + model = mutils.loadModel(config.path, config.model) +elseif config.model ~= '' then model = mutils.loadLegacyModel(config.path, config.model) else model = require( @@ -215,7 +229,15 @@ until dict:getIndex(runk) == dict:getUnkIndex() for sample in dataset() do sample.bsz = 1 - local hypos, scores, attns = model:generate(config, sample, searchf) + local hypos, scores, attns, t, num_counts, num_segments + if config.model == 'npmt' then + -- TODO fetch reordering layer weights + config.verbose = true + hypos, scores, attns, t, num_counts, num_segments = model:generate(config, sample, searchf) + print(string.format("avg. phrase size %f", num_counts / num_segments)) + else + hypos, scores, attns, t = model:generate(config, sample, searchf) + end -- Print results local sourceString = config.srcdict:getString(sample.source:t()[1]) diff --git a/generate.lua b/generate.lua index 1f125f6..b532c2b 100644 --- a/generate.lua +++ b/generate.lua @@ -5,6 +5,9 @@ -- the root directory of this source tree. An additional grant of patent rights -- can be found in the PATENTS file in the same directory. -- +-- Copyright (c) Microsoft Corporation. All rights reserved. +-- Licensed under the BSD License. +-- --[[ -- -- Batch hypothesis generation script. @@ -61,6 +64,10 @@ cmd:option('-freqthreshold', -1, 'the minimum frequency for an alignment candidate in order' .. 'to be considered (default no limit)') cmd:option('-fconvfast', false, 'make fconv model faster') +cmd:option('-lm_weight', 0.0, 'external lm weight.') +cmd:option('-lm_path', "", 'external lm path.') +cmd:option('-verbose', false, 'True print test_mode details') + local cuda = utils.loadCuda() @@ -70,6 +77,14 @@ if cuda.cutorch then cutorch.manualSeed(config.seed) end + +if config.lm_weight > 0 and config.lm_path:len() > 0 then + -- os.execute('./compile_lm.sh') + require "lua_lm" + config['lm'] = create_lm_instance(config.lm_path) +end + + local function accTime() local total = {} return function(times) @@ -133,7 +148,9 @@ local _, test = data.loadCorpus{config = config, testsets = {config.dataset}} local dataset = test[config.dataset] local model -if config.model ~= '' then +if config.model == 'npmt' then + model = mutils.loadModel(config.path, config.model) +elseif config.model ~= '' then model = mutils.loadLegacyModel(config.path, config.model) else model = require( @@ -203,6 +220,7 @@ local addBleu = accBleu(config.beam, dict) local addTime = accTime() local timer = torch.Timer() local nsents, ntoks, nbatch = 0, 0, 0 +local total_count, total_segments, num_counts, num_segments = 0, 0, 0, 0 local state = {} for samples in dataset() do if (nbatch % nparts == partidx - 1) then @@ -210,7 +228,14 @@ for samples in dataset() do state.samples = samples computeSampleStats(state) local sample = state.samples[1] - local hypos, scores, attns, t = model:generate(config, sample, searchf) + local hypos, scores, attns, t + if config.model == 'npmt' then + hypos, scores, attns, t, num_counts, num_segments = model:generate(config, sample, searchf) + total_count = total_count + num_counts + total_segments = total_segments + num_segments + else + hypos, scores, attns, t = model:generate(config, sample, searchf) + end nsents = nsents + sample.bsz ntoks = ntoks + sample.ntokens addTime(t) @@ -227,6 +252,9 @@ for samples in dataset() do end nbatch = nbatch + 1 end +if num_segments > 0 then + print(string.format("avg. phrase size %f", total_count / total_segments)) +end -- report overall stats local elapsed = timer:time().real diff --git a/npmt.png b/npmt.png new file mode 100755 index 0000000000000000000000000000000000000000..96e16560835b9572b27031bf8fc482476c23c997 GIT binary patch literal 161584 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⁣n5V#ikB>IKhJ<7bsq<}2*nDEcvxhIn+q_4&Oopbn7DIPLWqTxv+gpM|Gk3Y@4pY|DfB~cXYYm9k?>(&ibh;(^19ZEK^rc>Qa z24xI<6izY;iy8%;M$6_82{rXVjx|&ZNB966RL7m`0(sw60McsJHMqoDJl6?Z2tl8LZ}@KV%;!gKu3e zCL!8fn`;rB0KUxt8%PP`TgzL0_U)iF)$*4CVEGi&K*5UU3*C!^`x3#cwiFW=PUN$u zhsG-IK?%%C`|nI*uUQb~Uy(RH7}cks#l0<8b;T+2E937mSYW3MTmbg6H!QRdAjN$XdiE&-5cZ#wY<8!T-KKm zKslV_p|rU*bjuKx^W{rR$>t~W3iph=8yYb$cmRtI%EA`_z{hFq;_Z6E$&&VkB3Pv+SwG0NGIuEi31LA-xWN`Ic)DON`6ao4=ojpb6_ac${?PwNe5mX1N94tE7 z&PsoIaZi(?quvC)7bi|;Hf2aRC($IiXYfW&qBRrc%G&yc@LsXkkN+ z+5L85C+(qmbnB$uRXU9c&dxE$CZor<;teEHQjJocB>ItI&7 z`nlWal$S}({6+vF!Y%6$g`?yr=$>$%XEHZ8m&23c9EEeR+zDcp z<-y1?H+>|*yP>*!kWM?O&nD3$jjH70bfaRm%W4*7ucxiMAY z%oa3xM$2nS2N8}E{pU&A|&{TW~i6$qeWkxQlL_RzhL^{D`vl`4gyntKl#R+IJEcE8`T@5WLQ z+-ui5Nf(yAH%1x@y0qT;tm8UUNE)hkeP&ri#bSFR5Scv$1Z3j7@X~Mvy*y-Mj~hNPY(Ynj;U$b3 zL2W0S=ZHJ4y66I5FD%V}S=|!)7OX^E7vxuUyQBjLnK<|1b-mk;xZ0kB=?CMnldD$P z^5(L>8$B22wfFDKc=&#bIG1v91x;RZV4yjv+8Bky%l6zDMSUfKEmdpi6Kp9ceiog=vmK%zS< zV@yLhkBwb7NNE5lVH^Nw#RdQZR%mc^JtMx*Vkn63?|}cDYR!W$!(nDI^lcf%l1I~5 z4yW$%2n={q;q_|~q`JG`7Ctmv$bTn8Gd7PfOku^3okLp7_m?8@Mm(aJ!6riPHISy* zr_{+NVPh+>TK6ugvXGrj5j8?f_Ha8}{b2seqNuh+Q_=z}>ihf$#_3BS{k8j-;3Q>F?33ADp+R zK@_8dBvt_9(|#K4^@W}&HA_hj-T>QsHvH^dcly12B1EzRLv^9V(|cqj^^xE!#VtyL zOP@vZqmrD4l&?W(wpjT^DgFSf8E@$NY#iWYy)G0K(GZ<4Mzcs5iQK4`E+mjL*EvY8 zTDF(-rXS-}Da)!|&s&sRtN&7m$?L~6*Pc(mAt$VCql-Ww7+Vppo{6E1id?ykx07aK zLyc)Fz~aO{4$5t_i|;cQ!UWLcb&Q; z3+C21qvv>t-O7o*g65h#8iy}B%b)|MEQ@H(I!rtdTvD6!^+PWb3E|OuaqG0wp{q>d zX5Ch6(nS$`BMHb{$kU(a4tlej6gX0L&eR2z2U%F%dOKIKVOMuc_PpBjcGecVndjUl zuUb&?w8cOj+-z3w?f391*vJ#BD~-QGvqsA+`7RwHj`eZLui zi&kGv#zi2uKSb4!IF;%ZrGGYpboYfu_h>uF@LB#KG9eL7eQ<+tx9XNai-F>)dzXN# zMGy-&W1IR|ci}npzLHv#(hAG@a_xCzqx-zD)AcVU8WxS+7^lzBe2JLjPmWJAXyeRM z6=1yBr9T)nOJASA2SnjKmY{XhTElSG<>nO^n=)HY%ChFV#L6q6EPgecu)`muT<^EH z>G2IWyu;*3NnZ$01a%afD?^3`%FDL148S*&p3*NhpN+`KmW||mgV-Y-CvzQ$G0Cir zdk`r7DNK*F33tF{E#9vVWOK~X(HrcIhH+sVz6%?SFqD&dvdZ63X0WNn-&uX?7Rk5b z%Il}>pz@sg+P;zbwN9&rRF-opCH_`3+EE?)uHp_Iz9FG4haFxKWhd{m#j&NK#k{~N zHpke;}*WV}^hmaur4rd5BRA)rsD&zB0h$;?| zg>y=kJ$}UzQJONlpl;)kW?N_@o1gIPM&wSr4wu0Y6D=sCuPJN1+qv2(Qu%T*DX)>l zh_hGf`TXP?|0aA}_=~|V^PJTJwLUqizE(3$PObO^CH;uP-2g`SKApf%X}H)@)&=Y4edmF>r8Uv!#bbv-j*vi?1`KQF zhWhHnzT`gNt9tl09e%i+A5m{aPq?t<@L`sLrs+xn@CHd&VL+uVDGn>XblM_LQ)1ndwSxa|^Zx-pw5MP zj-f7vRImK&nJr#4W3Abyf!r1~2u>@(>;yfdFsFSS$bb66{$m7BNj zN3-u20$_2*{o3;8dH0$GT)6tV`iyEtBC!mCgnIakxs`Ua2Lz4eyHdX{+V`tuExNyT zUjDLr`0dlQvX!@baM{w~<%vPmXj`eES7nCMA{hf%R`KTSgcU~qUZ9Q zkrfuNRA9`IR90zPeBlhdC;)RYdr#_ldsT7#2sgH^&7?5*dOwQ)x7>{)bmQBhOi-cx z-ZwlNr?yy7w_4LuLuv;~-eD@25h$>H>P;R>olSYnP-&u#{_3gWow-ZYe&4|=rFL;6 zv)VynypGYcCU}fUU<%LOm>xH>r;+d7Y8DO5zuGfUI?(r?K2LU9LpbmAZ!29-H^JmnlMR_C{0vt#4(RGx?MfO#&jK?qaEAo1<{Nn!Ok{oUpHtq|M+=v7Bp z98`gU-Wy`n^r&XjI79ZtDOZzQ*OeZL8CIP}ygdQ%*DRraL!i(C=d}XF_s2hrIKk;~ zds(QjMIzeJ!!D1>LfY_Pt~UNkA=xM3(*fkj<&%-BE zKpDt4Tw-p9PV)h^lXfO0u?932oMn|Ss?H&LjVFdOTkf5JIu=5vu82in|AIg;K3xSs zE|m;kaI{@K3m5fJA`29lu;66NGI^(gC9j6cGyt~$0Ose*u_TvmTy}{VG}cE%V+8cK zTHZzdXu)grNvoeeFh83D>|BJLHgcpK8VHD{nsZ_Ea(dop%}g8AP(XwX5E}rOw;4oz z7m@hr6)}w`qz#i*Ek~?J)8Vi)nrlr6-z)e$8SBNW>IoqyM}GkDZ5m0R#V`z_c~7R7 z?a2T2<*jbS4-l5CTx|qEG6G(p6PLHGlu$S(06Nb=%SwsypY8j*z$d~7^Fcf#toqiV zKR#+YSmpps*-t}((wjm*c$=fo9f`*+`HC3g?V|@fT@n2U9R9>~?0H}m?n-27Z9zbM zut#5@iPP@!phDaBHe3R)CJB0-Z9~%~}zD83t=GW#0r&J)z z9B;Dj2it7KaFi&I$kLmbDE0^cPJ_20+da=mLcG2MQh| z^b>K5d;knR=RWXk;7vIS*8lRj{#dnVM}kotggK7*d3(~B1QsJBDaokcmm_uKT0UZ& zK(}yIAs0O*Bf~KCmXDwy0rGk9?*kCm;IX{sCq(I_Txc9``tuEPkV}Z-+%wq}=7}ax zsr2)O({~<_z(zC_(fU5e|Ap;cfr#RYDWmm$vRc(FL|78UE6)M+B6-|7QVyDtY=b)A zYsLW3_2{R<4T}7Bb$1JIy_lVw^V|J#1BN3EQLFvz8IS7J*;Cl0{>4AZLf)bQ#mO;w zc``_o486xg0|jO%^)L)7KYw=Q_{kF3$U{JYssFVCESrsapHn{@BDaOU&kK~YJ_ls2 zkpJx589*01w#sFxze8)jF*rE5a`&%WXThzPGYboWAA75-J&7k`e%?Aw2p0V#Sw>_0 z??UyZWM#3>o~@1j*%=NMB;L4;3<=}O65MyrH-En8c>9XeNgN6nOMOMl56Hwg| z2UsASGf7hQwE@_G`$~Q1Z@f00k&Ks3S4>)e0ko{>Ck;5~AnG7CyAqDzqYg)wI)QFy|oWmU}Zl#m4jY{4DG=Jw}vSJHi@3 zjXUB{cB0@Y<8sZ#Vg9833jfCwayM=z$=|`t{EQPjG{ciny&iPWzImaagML(}0 zRr%UTws;XN8n@1;nLzH1Ue?phy1qdl4ODXCmjZzpjxRow$Wg(vP%P9#V6`U)d^Fs| zx)(xk%Dl)j?@`ZG^~)`$a>ox#9b}eE{iY62uT%X>rDq{_k(NcS7}=JR-&g1rubX(V zUPtyUQJDIPg<1xb_gDp#4QMZ#nSS_q^k{_fJ0b&#a+dnMQK?uRi_(+5eSiKsoAm;&VR{%M^4jj)bwqDZg%4Cuh_Tsb8Rj5E(ypUQAx z5|BTiN!ao)C1ksYPov=|8`pLMV=Hx;t#Rh3Ay#~k=YDVI42rlZ>S=m5^;vM^hS%ju z)1TJ+R_!}JzqLSO>U${8L6_+vvyLxt&g3tXPcUBfLCCtn6E6bH4_-mwhNBy$E8EX` z!V%M0we4!@MD;sy*vPdSk^fF3I$Nf;?~w5rFEudqWAR7!Oq}|M@ACas_){h!!$PAE zcs!PM%TK_KM@Hkf6MxPUFYk-r42{blxZ8$7UNUZ(1a5rfy*z!ygRN)b^5GBxg+B@S zF_Z96UvYzIho&85!$0n{3cXbYG$AG~;}VE(KYmPcKLmrm5K|wLA3yFyIMYzg(6K^p z7V4)e3Z>hyMWYlb&297VW zUbASh2zXRz2A`cf1i;k>Fdowv{dsA{#R}lh@B#ZNjBRe`u)k5~^h>+5DH%CNA2Tw7 z`WumC|IIQn$*vl_?Rc_QP<{;c)n3>E_TWGF$R|6mpi6 zxGlp5mZG~E1N?6Zr|_}b@lpSqouRGPYz%&W7X3|u4}w=qVIa}+O~H9_hy7_*epIn5 z2A`YL^khM5N=7$L6&AUCo__yEq}PK1*yyBrWko-hx6WZezoP)6VgA$4--Drj*EqOC zNLG;$EDbJ<@>9j4<+ssPdE)HA!&W`fLLUq`k{~Z(k>BtDcqeC!PQ1lGy5fKswsAOr zS*fo9k6-okTrp5*5PnJQ`C#C5SRL*FKluD;b^q@yL~jaEc;6tOfMu#8uZ&j0S2>cctj z+u287^cDI8`XYS97wz5ui@jT!$Y+k;so**K4k|e$e_$1jpnV`k7=8<~BNticdtg3V zIrW#?j|zadc&;qml|=S){$xFpglCVC7f_r-z^c*-=-!R6WaPIRB3C%rzvzpH5<*5} zjp<0e%Q|WfEb;hzinMQko%2sYGsg5Qwd)mLO9<#*TQh+{aU1_*`6imoU?YPjqBa4tsR zcp$!%?U(rqpBVWl2`ls)3{V=*AS97hfk*8-F1-ZpB2=;(#PWb*use^hg#d4ZZl3zy z?#_;Ho%(}(T%N{SghQjMwmR9=SAGD3CZh3AR&t{s)IA3tQ053yBbTAykdz!3&ua8&8A%t4^6EM z5v9s?MS(fq4K6?xpm?mHpdcwpIgG&oVm5l4GW_T0B!sAymcJ(<#)A>`8{*fK6)B|Q z8L+%@WVKF69Px{=1Ucj?`NYE^e$|(g+1c3vUNPKA2n`Kw3S&*Yv0vm3gMG$=_rEfF zb8h>?v-YyvSN(*n9{KCE@Ey>zlpyR9`#I=0G;SCok^1nKI0MN}*u+Hckxg;zL43Kz z$J%#{z@t_O*&G9+VI#0fPfNRI2|`H1p!avY6+06#zHY`f&fsxiTx*<=udxbO3Lxc? zISN{Y3~_V`vU_44^d+R#JBQC^qzT+s+J5`9XU~o+u2@af2XxYbaF$n&;dwSTQ!z2D zVTuo5K}%qop$+p!%Hn6!%OgkgWORuw;LTDZjpN5vW#l+L&!&fHx&>;|ATUW zH4Xn(G#Kn!M^8__@r3X;!ECT^BEtzpD=X2b{2tv}O%8)b3 z>CnX^_5Xa-I8Wi$Eu{KSuP7i{fyt(UV8yzwn z%xOgl`RSK0fIHCC`^fqY;4CzQ^Fj|p77*j8xfy#Xycfy2%m|K~OK!xG-z7_5J}-}a zzWB+}!&CwDt@#L8FU^axgJbAveHUCOgC*emm;@At=cU0j5n*kbI*Ec8wgzNk!S6YC{dlnKPTd*YX z?Ls`_>3}$zBMM`V{*X%ka5VY)KY5It1IXpwkV<=tn@tWO?63BziiPc#&7BY{uf3OZ z4-04DaFa&L`$;O2B)9aRO{yY|F^WHb=24Rmpr?VrZ?Pkr#I)t!+1VKg;$lRzP=Y-Gc!TdoQY}`S$R&sdP9wLiQwP_mK+r z_yQl8OMy!m>x+ieuah`O&tQ>{zoJ3h3-Sr4f(9CT0ojQYCsq^_#X1gDtbq|1j?c^M z-n!u{cr}jJ)`Jr;c#%whgZU9jy4(4qD?BfuX2IIT@jWJZ7)%^W{KlaO zo_x2Ddvbj&=@u~NP{2r-#z*kdFn@*ZeH=AxtafQ5R4`=%iJGDx&7mXE1&DYCG~^XP zyyKB_H@-spOhKOElt;t^Uxq1iwmyJ19)$g)ov9qK2__9Gt4WFCQnJvHbHwzSBQF~D zM;{uekyo(DPk%YG<=W`rVQAbCun9n};^~*NR9Yl&3Ky<{o!Vz$r>8pKCo~j@lMF$& z+$&(O5M(1?Equvm zA6(L~nheo8P+6#@36ASQ@5QB??lW6aa^?Mh+ z2q`pfRsu(@^&j=A6f(f{I2H0o^*Y=9r~7f6> zPO}O{inU|H?!5Qo5uyJM&cg!yQLoNF?>TUetd~2&^lN~}D!?Q)JjIzVxRdcJE9TAfO~hLeJnm-jxb=^RKYa>nF0)FK;st6 z2cP~C%H2f2^a9_{4Q6Rq^9KAOU?s-X17&81j`0lhBKfwaA+ksX3dcj?BqSZT9*wNMt%}xr``qshdrwE zFr*|ZB;m?(0M0EfAv$R7YU^o`IQ$vpOE}W_j`sqF;hX@;mzV!nvES<=TL5Ai_m6~3 zLT7wKj9Tdm50qkXY*o4^KQ0vtg>CA`#9ViM2@`Q}r)ne6j zwjE%OCyqHte(4hjJyupCpP98Y&0h11opkk-Fr!IxtcYw^cX|HY=QeOE^GdByzI@LB zvwn4QAh9ufo{KmummD)ZQlp8$4X@vOV{_wf0XTJZF)t@q#Xq;ec)Ra=!Ob(DT`n^U zMaS~|{Yd?l2!mL=B&7_x^vLJ7p)aI4KTUgMvG=vCd(XCM6&>PSLFgvK z##QfG0`twYbz`MQoaa=OFK<-dsxoPuKw|&)L&V+P-J3J)WMzh(< zwCw|*ceBqWNuM4sWzKG2ZCm}W*skOW#ztFhD|nbEnf~zLj($sL_|Q={m!Y2ZD*6<2 zqe{_amrDY#Nkhzr8t3b*lkxqWaGQMQQl)%dpB*c)Fd4JqiU2c1hl0&-(%5V(PgNQ) zvo+AU)AY)34ZgWF&(_`Do(i8`ah=Z|O_q~jj@ZDn9OfF%V_bf$W;ci_GS%!GaJ{i$ zF-9fbN{qOVlcY2f)r#C}+~i?6A^OUbkT`;WZ?B*M1NoJ}@>N{t?8~-*4-|V~`@}%i z<=omccRN@KPbHqpFi-3YB|i96Yv8-4M4va5Rd>IGRh+f_ZqKcU=?6*;0ne@CB9Is} z?m{V(2g{P&tBxJ9vg|}b(hc%wrMZ>%AJ^cMO(b+o;Yx&{tmS|+G?`U4&Y$Tg0;h@# zq-3kap0#L7*&XG-5+*XdpriLRaJOEjN&aa*ZI{6O*#hb)($Pkm_d)f4O#}Xf+XxpSYdoBA^j9#$2k)2U5aNt`l ztoSgRNvwakYcCgOurWIrBou{WI6iiKc0ZVg&bDLh7*BQ`nw}=}W7qdwPs9zpUdX&& zW6-cKOqo<&BTZ;3rWRBTV;kI@neBC(-d!-$Kd2n?(4!-Gxj(6=KWBVPj&uK@C}ZWE zD>f3Xf%+D6+U@;1F@X#MrbTOz3%*yU(~3A5EHvaCd62E+TgF%(swnIyy ztF1iKape*m0r=WQ<&K$V!V9nG`c~+}$_5XDQsweok~OTTCA5mnQV*uGq!LcCn4aBj zRPLAxyI3!e04Fr0n*kt}sf9}(0mZwQvu!i@n^`%p&E6j>DCa zzVBjs;-wfDPgTfca{7S3a}0j9fG?o5JfbL)5xV*&j2Nw3!r?(S^Kr$Yq*2$gqeidn zP1?3-;Vw)K)vJo3ZFG19zHE28mte#9@v|8&Tjg(q`wK#r2KuT}k1hZw_LlKvFlYOlQlDX7xk(aI#Y)UQ{-)%w@de>2lrV5 zGNDsO^@puKJvl&=H`#nh+_$@LV3JcRZclzYJ3RtMwA8!>(-~)ZAbLCr)(&XP1w@>b zP$?^xSTHf16>d*|($MePJ*XCOdvA3|Eq@&4+zVl=-LNklW)ovj;;~R7tC}4hePg$5 zjPat|Ab~s4eF;$cvcDFRIGLoWtkhY}0g9Pf#+O2WX z(Bbzh$}IN~3T=IHR`-mVL(LBQPBnv=!!tYMr=;K2$F=u4<~U|Uyjjhx8m&7hYgn2X z1Iwo3SseTVAp1dxvi4e!>)@s5opXdGqKoEg3{H{YNas=0j_m9;;zCV2eQ?GBuCfP7 zN)W~*I0Kn>hdI^h(rqptqC!s`zh7<0zq*pY+7E_qw4&L%>W+uT8Ke; zF)5Q}w$sU*XRTJ~MhZ#U9WnkH+p0r(B*K(SH^^i;TYAN?EnZmiDD-K}OeXd9ge)j@ zjc!~u+%aBd_!>NDJC=EVB@+&Su|tRaC5phg&R)W z$22_;s*P2lbKC3RwMu6PUTgNW9=MIc^yt#qi8!DB(|4?r{?=g?a1R%T7FNLZy~^-z4?(@OVPvp2^6pP z{C?%4ny2$4)g426-g*hImg-U-CDJb_8)Xay#LUhln3N?vr5&eg?W2NV?4Bnbq!mNw z%q$TWeZ)!`^}!)aHC80$$9W18JLD!TS$HqBc_)Xld}Yo8TQ{EjQeaB*Gw2 zuAXJqe=N@DexyRoogb%~b?w1{!yP8C%Vs6niAx7c6z?2b4<)EkdSR?Q9bO3No)aVn z=Iq|o<@*x)gCaAFqL9V7biPQ#opP8uGM9sSm@&=d8-V)c1c;3+w}c~7bca%04GS8| zg`%A*E|HOgg<$;B;&Yu#W4DiE?rqzH6lA!c+1g4aYjUj>SLJhCmfg(3lq7iEywXv$ zF}HG>eFhUu_Oy3Xq`)+7El8?EJ=nFjCA!}R4y;@@)3nk(w^>SSuF~ik7$BKL_O11f zqUA~bC>}lbuHmwgD-CON7}D9YOb*<$s&io8d+ve#{y1yTUkwKT4DI21m@eOi@DTw_ zJ??{z+oHm~qha1awG3QOe ze8p(K-r8UiK(E==qC8+2k{6x8CxvBLX zX>MBx1Y1AB1!-3fqD8s#gTLHy>INW{LCx)C{(9ZnVu`Do3;&_GHP!Z$s%8r<2+870 zG^Ob6^*3jTzrKy3rpD7svM!kkw}#L3!yO8TE#F^gmFX^yWU-UW7W*>i!Ms&+dv*Nk zd@^K-vz6}enD>hWrbthzRAI4VWhB>IiAnZ1Bpd%4FMkZ2{OTXgx9`{F zr|oZxj*a=Nbd}(Hx@LG>^iR+wzP#{l$uad#M=Vy-eJjDhx+?2rM{w}v^Rl6Uujb|} z<3yT`hIbbd!kt=H2@ ziwQu$=06ehhaXV=G`BKdJZ9D#N!hh=QNQotLanE&6?*V_XZ#ZfMI|US!JW~JE1yYE zs7Z_29t42qQfQ5=8yO+`qR;HJmUFy0$>#K|zFZ1Tb|sczzxoYng7OJhXT2}t+%E547?qoIpXzX&JL$NkA4lW2-T(lO6cto~@2l;Og_+ zT}vNRZ**|d=MFZ9B(hr2+|S6W-GB@xzq7u|G<(KcVU;95EMil&E5UR) zF~##UyI&{vW{n+Gn+ToIQ{VGR_dYyE`Rjq-kJ;oH0UjSptqu@Y+6n-WK09koS}7!I z)5%e3`#Jp)%ww>VTh&EF0=>L8}$qj zY%0}V22fr{9?UUgl@xTG))Ngb8rYK|XO-4lRmw@$7;`Od3^e&Zrrq6;HQOZi{&f90 zCji}bG{YyvCWUH_1Y4DqDtaj>sWJexlb^ey%}N_vw#Ni;v2QGwM#ZcE;D8ex#ziwp za`izALy4i{;*)#85IP-m-b2J(d55PrU0FEdPfNWSroEg(eMUkir8#)?g#wR_uCHkV zG#Vuo8SahYHp}6~DoIigpntUfaK%hPX2Gc(DuLHO6_5cwJJ;t(+Gjj8}s2f;y9 z?7rGR6_s;!?nZS{QIA5i*oBW9)Ppu3&7E{9)*zJ5tzfh?xMY73Tb7q7qs(4Zexwg< zCo-tdU`lRtdSY_Ijjb%Z$98X-=v{&q$FOnboS(RN9FeX^;w&{|x94TOkA(^i9rkYe_qk7-`b`_H zWIHNJh)Lh0-vr$w+&1;xH)4b>;UW=rV z%eDL-LX$U(HhJ46!%xi+6`LP=EV`KNr!3yg_^5?V)>#`j1`od+khbQ4K*5V}^P_MF z5ROL## zF<7}Bmp5^Sxno{4@{KyG-KeL@TF4Bl9L}q`g@S%jJ!C z6g$ScrMGCla0002NdvW+8HsCU^xD;#&-eJ}7B8GP?ZU%K>R-V*^;Hxc_CzGAFUKJg z&~toAz4b^Wnj|I*!`RHw=9*$_7PVmJy8 zNZ??YUVye7e-l1Fa*!(G3^!LA&-6(%o4M|mIHw{UV3mt3O*>{LEpG=F!*Zq8p<}i< zC&s!lrEKWGMBG8K?#oFGZ6??&;Wccy(bW}{$Q`yd^p&bzEA(Qh4RiSw>Dm{oLNf?0 z7W)bf;c+}(R1);x?2kdq9kAsFD$Sg36V5$#Ff^aPJ4ax)OJ5*r-N9P+F)YXgV`Mk* zTG5$w%rwH%fpC1U-H`b(#;$KB7z=F;#j+A#zgzU=Vc~0?G3~}L$d&?UcO;ETz!25> z=R95Y)Yu8(!Z#b5k5A6g>gy3()Kh=ShcwD$}BrK-ATR$uGMJG^J-C>T=Sl$3MYJj-rp)svw`fctFfPIvz9Fyrd__2!P& z-SRY*;7kQDk#L&EK2@_{=YbD+L|p*;wSQLXUm`g%?Ze8fHCx}uKPKB3EyO-d8oDo1 z5JC#)wz#|8ULnWE9JZ*GGs4rN?UU7tK^ULBXq5!d^JY$TWoO|aPg#Vh+fl%+M zQt&cyT6u^2Gl}x@g_QpGnYOLEYS(oUs^ai3;brDKpVP624=O**&Wi}T6Lz_Gm-KEu z2D>#G#pzZyMXxz)`ml4Es6&aAmCML?aeHI{Z0XLJf>_h;J!^wuLXMv95_oJi1lk$O z*uk)tki1K3ZvdYI`$2j`K`RAXY%bR3<&X4J{ekiOdQ4B}R{{B!b?uBQbtFhP*{m;l z-YM0}IoxaOu5yv~mg!H3AWUAc*?Z#>*qM9Yu2onj?Jt#3TkqP_ecsU4?O{Y2N?+jy$wk+XS*c5%u6lo3+OcLjTQg z$Id?}V1e1+;*n;$`mWzXS$yJeHzRU=AbOpjI#{~ z(8>yfJVzdkYkqZ04#amhTFHN#G=$jf6EAu|{#gkW@3TbegQ|L{{@nl3)pQKi`I!5^ zO;Iu(ukFnVzS>G;Rn(DSW?>-$b)5udacksg1uA0sqJ= z&fFf*n-)$b=jLf(BO*aVV3YF2dIH0^>$)|>LH>(C7Q*v=R(QxW9q7AW!&T-J;V!GB+e@SQ!bg} zj5vIKeO;`3R5?o>gTNUi(tTyN+SY0U03&g%YwF%+e@I>{@Qv(iD7i&>1*8Phd}tIw zWSU#Y@|es2@%z^S^Rg?QA-U187!KW3p{LkFf4HIW#}_NXizZcycp$6rg6gwp3dCNe3w_fA*FU`?)O}W3?-D$ z>8~%#&|stoieBo}LH^$Nb|AiH>^_iJ9D9jdZyaO#&&@!at^p?8AZb`nS67|Gl$h0E z9u$8<&?ZsDdpTbA5*6#e(7ErPFyTSA$bs?>B*T)3T|4?6FALLRCfXv%zFdzVL z6_u3ehA)|dByf;IOnP2yMf49zi9aTh2bp9^Qa4s9yj;LI1w{^|9G35A0k}t%jEag1 zL&!jhRrX5CgSbR43E!#MKeHe5@-BXmv;hgcIgyHyQj(I{xS%oqNJDh~jmvE*TncsB z?Vth^k&62J^d`)~q*AFHxkh8v;izHYOGEqm`hfkD5#4z4ehMsLf0qLK!_&DqBxwUa z&qolLZeJ!MPy*CZcJ{?kN#V*Xt*3P&27gKwp<_tg0Me6$sV2q5+&{qPX~bcx@w62K znXLal_w+}{)k&PNn80@0)dnmW51(H~-5(v%ySb;xJ2&Qgvw^`aM1g(OUSWn3LjP?B z?ay7z#0{9UJTM35RdKi=x7KNy&dT@i(n^2Kr{R4kaCCv9^qpq}g%0{Ko(YDsYZ8Cr zgr}qp0!*mot|}ES2AGYcZjA+V|8Xl=9FB521Ddr28j&-2v@v1Lp3vW=*8Xr(jC8Si zEFoN^b0On6ASu?kiuE`g|A|o)Jz95u_*mTQ9OsM13S%`4WC#9@u-DeHP*_1<}(hV+OXf~ zp$`oLkXf!m#K187yQ1>&y^jt12R%1Hcu==>3;}2rDHg1a+Lh%W^z2hs#>K`z86bF7 z9R>E5i!J9Ee1@i_mtbhTEeL_7C8eipf;xV;D}U^^)+bvBq^v&+U_>b@Dw02tkOlRR zhly&4w`-VxPc+Exhlc(KP(SkaGE@NyiYZCQ67CoGg2Fw3eMK44eULKP8Fa!?o6*bo z4?aE3WyoSZxDgPVAYs~lAW-z;NeJKrWy(;2Q7k|p@h22u`;$utk@FxXAwd%qkFv>H z1VZ)e3F-`TN=gxgvL|?W5!J81Q$3IWbbtc-jAms2`;P>Slkbp%fPXAKy{@e168ZfzyTs!alS55k#*}dfZE6%|0 z=5Eglg!LCfi-Evcju0Du<1g@!KvQQ4B*h;PHUC%H|06t&=wZsdiDLVWvKiVtI^c<0rz3X;fTb6pmZ|L zYy(-6a6HXo11e?#QTfCZ@OcQdvO2xL! zdPfA8k=Nyb#nms~e(PTN?M{`~o$4)@w8=o(NuaW33978Y z1(h}AzT$)_t5)4Y_^^5yhUur;c%}%c(&~6l*uk|a5l?>@EMBB}>mT$2^S{M5Hun#{Ex zb_A#2kux33EHW9_APnMp=i?~>{{lk%V46Ojt*#|OSZQ9I<7p;QAyOw#KRDh5LTT}( zAcvKUixlONJ%T14wi!4)AOrQ9*q?Mvd%U?kXrp4}rKF@JSbHnJ2G9cEcV?pYZ!(#lK){ zspK)5K(;CCWHPEx|_cJo31*u9OF&SA#N{U~KM-iPu9xugt zD-dpU)YR7gn48NCm(`n%x_{V5uJGYc-&InQL-3BbU9;!xn+F~czDY0P;y_#H|Do=y z-QCJgmHy`gIMr7Xk)&?@(ZDohRXP*YbadP|=byU+VW`iQ z85tSV2anJ5zKb2Q`Z*OwIvPPOQSzd?3=Y?#1^Te#z(SvXmom4g`SZE_u5MXfM0EA^ za?ZRF)ng(cSc{Q_eEHz=bI-2GtfZ&D(lJCX0YIYbggrJtdbP##fsT$4FE4`z zAQ^{p2xccRxJ3O(DG~Y;?*uqQf(F8-YkB+|U*!WK_31%CRDsC&A!`esD_{}4KGZDt zQ+8vMc?yiv)fsmzVG-4j=1s4cWHh*Q^>2i%mR6Q^@HD!cD00H%j zjSsE`d~5j&Z*~n7KKzB{cB77_?MynrLzHt7NEbeAGnBY>j^@s~e(asUICojyM95rU zn_UHBs`nGWU1#$;L;PWv<7b`+=!kWaD}GX{c#4cqfC%pjT-CrY{SRw|PPmdLkoe)!;v#T}6Fj^A!%py6>|J2h z+k1!xvcEEm{cL1_aJQFLIIS29K-=)zC=f#WoP>l#j#!+@Q{k^H6;^8z6TRwXAbV5y zIsC^PMnw5vKJcuy3~=LTOiaw_T4jDCc~o`7>CQJ-%{PBxbQHbD%8q%J0wm?~z$SnC z4Ic3_aJ7)=FT4@mU(jDZ0_y*Jkt_q*34a-o?|Ja0Wm1Io(O)tK48Jt}dgTKT_P!&CZ&9JXVdCybDTa9H@~7^2$= z5t5B_|JyG+Wk`r_Y6$%}4L*t3{)u^eL^3yVMyPDotI`A3Ox$cxI-on;2 zAX)aay9PgVq9ksvs|)59V^x;xhe1LcOCotWSE6G^I3Eh1jMXjV>{nW;5~ly1wU>>j z3|-DQTz}?QgPi5m(Qjbp7N!s1?cSUpjFQ=|hg)EAPfKSP3(4mRY>bK1;g@WHdxfk6 z_8)0$sqsl1%f%a#*RYVr7A*9B@wqhY&pg)cpI&^FSlAQ`4q(p23Fn(kKZ2(Bi_>uZ z%pM2cmQ$~i>ku;BRq+bTP4**;t{}c8NJGAT%Q3xv#BuM!i@Y?=eF-0rM-It)rQa9A z503cqE4X_d%@1-ZEDLVRQuS*y_BRo zO@XO|hRTaA613DocxnyrG*cv&bg*@l8+tf%d@SdV;8BK`79bpXw}l~<7ljvYkN zOqn#D6H8;qj}=zWL>6|-HG1Sofyep&@8B0+oU!bt9=n|_OOW`x{)jwF;NEZxp9Y_< zpv!w!icN<(oVF*K$i3FDz1{mPIxTE;q}fTw9Aj(!)qTn-PYG#YHkbK5YOhM)J5Ne2 zK0(=UpKgIAz0CZ^s0#q>jOUY{ASXj4(se$$4RS&_=hy~U1&a0c%L`c?c}2jzxKI({ zl+>z~F~O}LHBCFM!hPc#vPTs39DNf+riBez_RgldLk6WU-H#Bh&t`c-CG<2_veSUr zf#m;=Vc>V%wr^mG06Ra`v)$IA{sid`3-WcJ{V%JU*>hLzFVPiQ)lV*qr~g7-D%@Ae zZ?i;s21~!`@fb+y~lNL~)P(DZ6jSXvL-PXJYp zW$vrZj3?e4Pv|Eh?Koh{cdInL;~7+_NGc=lJ+&a;Pd*&vsv>kHW2XqJn0ED1=h4DY zlz^k~ylOX(Bi-or)X4K?8^9proNoNYAc06MLyynHaXQ@_t56e7d&{q#^ZBk?*-yOf zmGeAPe$yz{PdDNUi!oDn4~^FKNd~i(m#oH3ebLf}&sScIRlGW|KgfpfxJgS=^ue>-;eo(QmUIdtUddzlfUxsPa=s|#xi%@KYb zf?Q+%o}sesCkk9;8k1k#)qKID+q+@qmZMl_H(=-mo6RluT$8(|9Zd<5Tk zj^$?I75xMkH{&;Tz;?9wZL+KtI1^RBVf54(kGuDldwRU zHeQOh9>}HJ7i?Vra4C2~i^&$_a{H>TT5g_Widgk^vU%TF4W`F$9p%%Fy_0yIy$U?( z#>a#f%|Wvde79ww?do+_=WPM;gDez0xxr+xxmhg5 zyVxa^Tg;0sjs@?w+hUp z!y0h!*Dr+Sdt?-09%;?7`)ayR{Ii`+fVoPSOg}xQajI@f=kayrc zkZyN^cpf=SP0t0V+Eyo)V)6p+9q^YJSPRpEzt*+iC|PL`oO~Fmo*O`K6S@@JEr6_Y zy0H;gY$fWz_&8FS=e9*Y59*^LcW$Z>-SghSjefGY7wf%`unWn$yNp^b!(8ScI$8 zFlCw0_-R*o+s_NokO*Qin+xf4;@T$^OOlThZiyc0Y~;?7l$!REo10D>btnXr7sVUa zyDYWPL?C_yejIK6F~QR!UTFB7G90?1Q+FH_7nf`HPN$}`i{J9CCOR>SP(7tCf_ohD zvT^dV&`f0Wd%4Juh-vf2f`F;ekJ_7Apy6QwYc!>$?HH`IGPnJzg2thpcZX1|;|2@7 zgm$7vejlX#bS5Z{Nzx0rhlK?GX8=`u2ugTi~8u4 zyIY+d*Z92a9nnZA8S67r|7P!$^7D_g9ZzbP;&Pg%cQ{L|xKQ`5rQ8z5>@9=|@QcP= z7iFF&-rU@p($g|Kah5Dx;-~&ZA*HfS=UH#&d+c(_M-|gID3Qd~;={GJkjor3QEc=D zFfvTX-Z_0yNe6&Onr)Y&#&?#koImrG$FzQtUTdd>N3UVDH@wcl>zjeZoRIJK-+mj( zj>(td6e$POm6vn~=Rb-I;H2DrYJdPvKgO9)JV(a<#wE=&lh`c_9X3y$`mB zIb%RUo8SQSXFoy4oD&s_-Ag zcLg!_w9i+^ZUt7iu+C~7d3UL(F&!&MI>*<>FQ07Apavi-D)7`g3mlp0ru9@%_7OMS zp%Emo+*I;NB|Qc#rE(i&8}EpW^e0!tT^Hd7)t63mZ3-1>@4n~|aPg7;utPN3@%urv z8^6r*m#H+OQ3{6+`)Gikjjz~E(tv--mpKX91ddPsVh-{iOy1Mz01DFk%+B0=s+b;= zl&3m5SJ)^S^TpU%LCNS*6o(@`)Hn}*OynX)cG}iOYs3 zNwY;G-N-;p+1d6dCbhH@oaiyW6gFBFZ{c^of(pC6h3>}SlB zm8|7`Yatr>+`4WnFYo-8Tzj5Ou@i5CV*Al|h?5yr9(1hw)6sgi+g9pU0J!7vIb%pZ z#k<%Wg?G#`3^_Yc4#E~p(*-9z5VBVdPC5a_Y15Ug9{SN@AW=;S;cKp zp8+zO^;UoL^!?%;mNka($hHq{Be9~>?sfaRvfRbRlf`_pirf71{;5KG-uY2}_4cIh zgz`_7GgReVg>6jAytibo>YfVo@+Gk(+R<*z+N+N)uW;&}?On3-Ck!*`JECO^(CG`$ zDfcpuBX(_d*LEvqD-F-x$5a-1%6iTgR)>QdKJcpwJ*swhSW?;Qs@du7O3~Rlc!?f; ziW_^!#zQNjkRDYnmf7cH@ad(}eEU1m=-zo$b0Q3$SbLaKPHF2%LoU`|#b8r1VW?5@ z@TME*$zya3+FvVCndezWsj)V#fIKGr#zKRftbMIjKk0NJ7v90Be6iyL@5P1)AM~@O$_tW)u(`Q1KtsnNW!we==J&{Jq7X~^+`ai$M(k6+VF-^6<4_) z*YDh3A@Z67N_mrpi;DAg>WhDTZi;?YORt~g!=PvG{Z`Mkz{=5sNNn!MRzO2ki&}O_ zLb02PV_Z{vUW1KJRO%)C$&>QxThRg0s6H4uCU}Tj245=+M=HB8mtogo&$|{R2xXni zji(xM-*hN#L)e?|g{ovX`#I9t3MO!=^HcM03{{4<8cPSlRJH1hTJHIV5StJdg>1R{ zXv0OZv=fOzbhf*&;q~ec;U$ew>B=JR%U;85ZZ&&GL)Yf~8eLZIT1q8J2|ayIW^Vd@%^%o}EC=Lkx8J^6avB)qPh3eo4;kh%DoI&n>Yi90 zniv^so7&pRBMs$6a;i1Ikysn^S^ARI9y8n0Kb%rc?jV=0zv(h+GT@kY#oL(mby!Yf z%1MNHhGS)QA@jY^j%kZ~7Ee6S^~z3>xtDP$?1JWNggTOVFoMFuecMLuKfI6%0ipDz zB!n@NtN>MI?dZhGQBXi1sie|V4eWV+j8J%*hD({jmJV>RD2Uv7+LTq|YMQW6|C-oo zT~8x-Qa>lM;fHY<^q={ToatR1eW14CnCU<24#><6Tg7}+Yn^P=mI9$!#gkO8Zu#i| zG52b6i^9~dES43t8{1M<14DCQbW#6vSF{_SdQ>H3MQ^+hBr?~4(n*VdxfikD-8uCV zl+YDpLnb$|$F#X={nARTMmOr_9<*!>DVRqzY5~p*M;2>eTZE5yxW=RcOXZ^0^mQwM z+X)V(e^q8|JUXzdQ)8TNs$toqTUuZ$Nz3y}kht*TmfjAGj(i?}zGSL7!7AQK(b-yk z#8hqP=PUe)FhumX(mzNGM_fh^ zW2eq4rw60|rtSR3BFwn6GCJ^w)zOg_J#!e{1l{QNsK15+caFr37*@k^h|hpd;S@(o zSJy~s=nmr?hYG)8h0s!2do9uEqs!1S@xwkbT$zyL3T9+z7~Cj8A%)CS;t~gSf7M{e zwCWo7*-`+G-VwmRATsbae@7KL_$YX-e`)Al@hyL93VL8Jd4G26S0$g+I?5R;jrLuaB7;w(AB;=+2KM7b$JYGy|8fSz`Bv& z2nT4qT)Np*3H6hIx5@ray3DYZ-FN7|fC`^@g``2^a%z*ajO|F`%8))&+}q8|71zn< z(E-EEXL~4{r)nZ=W`#Bktf}nnH^8ibIT3pPZ4!}(S^5lW6V_h`3i4sd4sK?8Xzo#+ z+xHM%b04?^noq2!GDDSCL9_IG+ib8aj{63Dqfkq1QX27)F0Ck)$Iy~a)>f;RY~Bb~ zpx9I^f0`cr>GT$07L%E4G_N}b3s5V|ad6wkwmoPRGjtZl4K5rHNcMh0JuvnvI(yew zS;1~2?#2VHq__@wzLl(=!k}2mB(HPJ%ZW~Abd3G?;qx+5Z^*(^wbbR+4Y(i+8%c>0 zBE7hd0V0G6+5AXbrqu>_x!mm)_w3`~2vL(B+cJX+orGo(GGXI$RM(u@s;uERqMEfL z7Pa+a)t|{x{azQr(1Uo%(^8u4MefnJES`BIT)?n2%stuEA=jq6{c7W7SfPBEjy$KY z@@meZ=~Mq>(p|POAmNZiG&=H<4lwhzA)t>wO^^4VH`pzV7I_LEFu{w--I}Uqp$#;;!3T!&*`fvja(jGa}A8sP!}J!+m-hkbL4(}K3bR1duOH7e0V85 z@d{QcyMTjnI~Ur8bUS$?qgI#@uREr7udbTQ zX@rN;s3p@sN?198t@QfgriFfWU_W~$T^50Dl9>;VG|lEwv~H-Xi|%_2e_WyiO~-B( z99T>T@5}UsB~^LeY#0z<3#A}-%FzAI=J{`e5jh2Nj$k3!P5O^AO}uc}hee?}7yJWE z&prSq1gMiU#)Mew_jUwi^V-EMC4ous>~%$T+RngNU5>LwHXSNeDTU_5u+k`vL9|D* z|6=o7OF83j*)73#44omV9);aC1H~?XpWk&^gL!}vTG?^mH^2gTGpTs%h05dE+f1|7Xf1I zJlZ?H8!%-=LV^Gt{XpZi%ihotw};X@2Sb}BVP5lH&@gy*V8E3^|4@4PB^)5;g+C-L}=r>xF%MOf=_}NJ4Rhc->#mDw4xmYWz ztlt!;1i~NpoNMnVqdvNKxB21u&)!hI{Z9VE9+|J&+3SJP zV?AE?w+nG{>E$HYqTL_YvPho+9JAIlTl^sDf#p>o9;v#4gQZs){isnXC-LXY zJ9WaVZ|vBJ@-xLO9So0gLKWTCBpA{vRValG4E0Ob;%suWLWAQZF&YRPofO(fBKuF@ zu$;OG;kRdtE%t_B^=HM&n!{}BI2n0ib$QN0j6DSlzLg{~xHsyTQY}TD`+q!29!V0y zka*WH!$$2?EZf^60swyC%^qy9-rP8$GmC5v*nb16@X=0aebG!BM zn)OYZ{A?B@LT@cV8&|_T^-gC~51$i*OFWjL*p_`b5jI|iX=n4*baidcsJ{`sbcJL@ zyxmAzWNBuiPdQA*(SM;oO0dJ@2vxw%=ndptiT(rw0c6yHt?|G&m2zQs{4H+Lg#Y~b zO$lFqnr-2{%o;9-7UlU{?}>C17Do?1)LCmb(U(xum3jwEEkF;3H7Iwoa7y!Xk5`2P z;UH<1`dXt$P05O9(lz{vYK$~GUW=NwS%6uMQ%+t9zJ~I1}+_S=LG!re&HYuDwo=!AA3Y zWn$aa4{;p6-G)CYcRnv8wepkwZ*nUR-8cPieG|~>t}$Tr5akByY&bYLp0rIV^D-8~ zf#v8M%FtgYpRaXYO}2JpsL#-C-d@RnPIY(FX*G|^=az`*Y_TOGQn(IPw*zwzsYR4q zrqfD*8a!cleN)Tp_6o*LdfdURT2d+;(Q}L3;l|1o^;KAza=PqgiMspq5ieg<&v>Vw zdOoQSV)>*CWy(F1C86k%`$IcuUQDBj9`MyNA@?5B19<>;=kV_EdWV%MG^Yur zy*Lb_q}#vhzF-&2lxNqAUPtS9l>3dRJsVptRPm@iYb9mj>wVF9(u&?UbzyW<($W>J z0w|wW&D?{XA_Y<-EW>2GiH!bYiL%!RNi8 zc2=BSpM>gL`v}|2b?cW-1vR}YR5V1D-i!F5`D+Xf@p+{3{4gDDyFp< zbtPQ4!t&iVw>TON&DjQ2}? zx;mExGN8_ecJg{Sr7dMT*_>1SHp3sq%QgO0W=Xt)Mgq#JSQ}v5DYir^5)?W1TpIBa z@{RXVnpX9sD;>;v>vD+YQMzLIwoI6}cEe=G2$R;aIcg`QdYmolM}Bc-@3Pq-sVH2+ z^K*i9wPWd?T)$@(Q)h{;Ci%{e#^}6B%AnUcc^*806aqIA>Jc^jWMgAFmV;P{z|ib# z(xsIE(bwII#IueRg;#O{z7>bxpBc@Fg9WyL#t-IayL&suit%aR)C*kr= zj2jjib9kf9)H}jqZgWUxx{a7}ww+c8=96LXss)7gb7qY}2Qtje{s*4QG#03RNGy9_ z*pIU+$3F%kH7cH}U&m&^{@}4dKcrTejpvG7`Q2WCBC_9M(@SaBT4phQL&uR05Ym)K zOXbnWl@xsfPuScF)(5%@4@@`J8*$l))PtAfr4h@X82*{&h?*w3mQQaBhtrB!4EwYg zdLG^RGki#dq^BaK*l?LRyR|}PdO@y>v3C%r#ISraPl3YJC$u1Qaj%(Pi<|T;f4f~k zuU`c_W+d&>7@uj)$!Gs*#iqWoG+QzE$wpXPDLKAU?BvUjRM@wcE`ApBxa}5jV{(Df zdPmx}$YMg|Byi>Rf9+(_FeQ(+^Iw{-SwU7!*c*ED)uEG%T|`0xvEZ77zLm zyc(jarg@Ifu^gIMKJhxi?W>f;cd`~Ml6sZ$MSRf`#l6^&3*HrK>_p}oM}w3M#~Zri zBfE>n))G+Ipnk$uHkO-gr!nXt`>z|=P`iFAmMP9R0qAYuN%2#wszCnTfQF5S{@elVZ%oar9A0-a;E ze5!2{qW(kM{5!-7uW4%Lqkja;Y_DGQ6Cc}_9v@8{=q(n4etc2sqPCIozGzt{;TB>D z1L5m)c3Z8)oUrxFF77IJ2h`rvIw&D&4!B40j(Z$q&~WJEKD<18;;pZ@3R~aqbiB!C zpnviR9f*o=nc-Iszw$!(s3|W44$&~?7Av#;g4+TIj3NXF`!N_W)M{mH-cmQk)X##2 z)?mB~TI7ZJ;nj6#(u@vKcu27eJ z^e|JK*UsLNw$@w^q^ggYI-nY6AC!19NijzDHp^lKG7ze6;{C#U}zvbE< zqY}4(BEjQKYU_(Ppc(+yhjeM6(rL++tqk zs=$O<7q{^~+6OZ5TXbpr91CZ8=vxfA_O5+-lQuxN*q>%r=Us6&s(OCh@wob`g;-SE zrX>1Yl>Np7O}{V!^aOq4Sgj=9!lps<=!nl=2_sjs*FCFWH>~knr6vF!z_-_*?*TBs z`{YN=qb_x&uavr$UrcS>WG~VwFD=cPlImMqfo5n(=vI{GS1eiLM*8o(SH3JQtUHl! zSSxHHz7+%{3^j|6oF6;t4G&%${t?Z+j$LT1otW5tp}N|Yz|&y4mk9C9+AHsWyCVIK z1-P_o6;oqCw^zWwW$)~|ys+FcMS$*%Ug(ADF9f`^d{o%Dh_P6Be|Z+cI1b_@802mzWMj5{HJ_hKvlb2}!Q5B!Y>{G=BDTFE*k`cRBFJm6zPX zW>f30<;ldqPLqHz7@$ml)xL>EKP3(fmQT1P z5u%{4uYU-YwX%W&aP`xcuKw-U-u!IvXsIbZsw)9GcsLFl0Cia}0hp+s{FhF%^79`c zDipneO%YTGhYK8wWtWwy$|J6WO#m3`ukG`>>Q5hm7jh9DW+zCE(M>bl1K8ejnAa7m zYHEZZ?5Wm8*yFJ@r|mb1#v_@^4QT=* zYn@|&9V6)rptE?7Nxq$smwrQ2GXG%@RJ?&3o14p4x^*YZGTg(%%cKg5i^CvC=9ZR` zk%mqG)d%R--v<5r41;>ZreF0G6&0xh z6TJU=eSQ5KP5O}pf$j~k<$o*(096CvgWm*d%gXvr9!CKE+c7d3K5uXi2@h8oY?&+y zjrxNg@V&>!=*~;#Xb&Ji2<1{C2LQNJbxXja7=YOR+OBFh)%H+c378pOOcnz0ra%Ob zKr(f6d%ImLRLBva@XB8F>@)huxbJ)l^2jI$U|Jj{WKwtl;35{Qq*$e@uTS%S4?Z<% z6ZVfG5NVcjIt>jCwcpT`0<_Tw=C<|neY(XooaEJSNi_dh>z(?{k2_SBjs?)_3K%LkdlzJ83P|GzmvMVC!Ya;WIu{t zrS5b3mvPor_XvrLr;5_J0x&~BYDIyLUp@wd!K1^0FO?7}cL31l@2f==8%H6W1k~PH zLIH#)KUrWazH>@oS@XPuwW}RWC8t0!c%0~O1G%bAYdEi1YoApB{Mrr<286zei z+7S!mBvn#Unwpxzg%Ct`;i1XDHo}gBODVv|_$BTru4!*?2Y`Z^z`CRZD+s{R^nvMn zJ@3)b(u(^`#8SoufRumzqvXrI87UA0nH(a!Bvxx&57kL0>WnpDwqjA!J z85qd+5tF4_r`FQf_XcpKd$+IImC5~UA4^go0GwxUsRW4eJAjuavMqJWq!B=7|N4~I zpK%L*-FAX?z!U(rQcVru+uF5(l>>IYmDLg+fvk1^Zdt^?c6lTnS*6OEB zfN@N_e|7z0 z6Egl3<@YDQf&zM3IbrPLZGd3|Pmb`Lc)Wc3LVEyDKQd4BT@4TufPuu0)tn$lUfJ;* zaBuu+qmHz~tFTb36ap_60D`-x+E@f2p?+(Ta_=*LJ;Qbf@BnbH{HXJ%Uop-PZ8fI7 zJAuuFW1wC~V6!-X5Zr6L&*7ezencKBqD2GW2ZD(>2!GD?{_;9zs6VYmk9`n z1~+p`ibXd9`a4V(e9U&Xq8gvOKRoH1SlJ|B-AcRv9epE}+a0&j!C3KB)*dQwh{n`u zZoNSq8nuXkz{+iJDXTqid}oCh!v(T)nwoQd?A@edt`jZ3h{zgS#9CsGjtj@J`t|VL zN56bXA6s0q#4yr*h@bUt@dS6ZX5A;~R=9824(BmEcEN=*k_VcC`Y)ecUVqPU|JQT= zHQg4VtJ=8R@N<3zP-;Hoflr`BlTs}erc)Ho*4{V6xr|dG5s^GgqFCvU4a-D&C9??`nOpwwR zVA6tENy6Q~_h&Z zCkHTKX69#ShL68oB?al0wKTiGxZ%}q)5pU#LT-y0WGiul%pt-y-l|=G{^S8mKTv>& z6a=opUe?^S-cavX6`S<7#(f9%%0Z}KGR8|DRKbf{{R9BY831nhIXpzou7&HG00EH< z9vJatH~Ash2K`?2m!|9x7en`ZjFlnmD$58i7M1(4Kw<;2@cW2NnT08B-2}v_x&gx7 z(2XjV-8g8rOv1cS^o3lqM6~BFhFSiZ5K|8=q**Tgx$N^9Yj0Hql+!bOe^N_5Csy*H z78u6U--qG)^~a~=2PD7=Gc89z&_IPPEQ#!QEyG6&*GkqWlZ4#wMBdi3?0{+sHwPbt z=l|PKW|%yRrlfS9ejz{rRqd<{J%PX#8sgG~p*U`L?*p~`1p6FxMjKvC z0NME@Nhk*R8jP=rYT{+nHvkHv*A2i)DWqif450N?IZLoRim%3VSW)J0KtvIYo^cg+ zNs|j@uP}AQw7v0bm?iumdGPyG=2f#_oMaxMR+*bJPn1n!BvHKktah_|tN>NB=qfXrjRR<3Ni0u`|n zaUueRE3s&sUpg`FF!c$^NxVXLNS80RA)v5BeXLCD%n%NJ(@-t%5gnNNxC=L}(}fi~ zTrFpK0(PeRkdpYUTY~d`3(@G*=}vo4kX)of`ee{_^*Nm)GN-belz0=?x79JgU)3FQ z`t~w^e|z*Y;j}T1CYqPuo_qcJRv`86<1Xuq^(HpPdM443AWzf%zI+5p36oJrE31s{ zi^Ti;4cWl*Ll1lZG4Ol7-OC;wbp0N)U|Nw|2=;K{$QF$+m@y_X^0yR-F@dCKSX6wM0P)fgU$J3z_}v2Ysa;3}%~ERdIvk8yV1?6m7Ageml7^L^5U`fp^@ zd4l}mbqPZ}w#HNc(kVmF(-wE)nP8$(mB|dbH@|8uisqJD(gk?^ZXAMz=BnTkgqLB4?DM^?T1VqgQIWxQ>Bo6}r zdCXn{Off)5Uj_yWm&WrISpbi*va%YSgPz=FB)GtaCln%zen>VCL~VdAJx>OV{~9sx zi#rRxzYsgIkj-aK)xE<#CB!m2yn2o&90Tl(f(RxcW+{tkiXGlmxgBaW%{O(I1 zmnLV?yuduMggzO3iO!J&wG@uCt2P&t(CxA~H_U@A5mqlBZ<=YzxUJ3^3e+7->rU!z zeWg0V^$vSVkU~6GW}`*XXAK#8;2y}86Vbz78Ic0DVV$k7A`Rl{w#R|Rcb}%}3hHX* z!GBPQ+zxxUU~Xn2c89hd9N9I&aR$UXx~~RethP3>Nu_gt-dP^+=4mLrBRFQ`oGNB7i~OgZf&q<*1p zh`+(_%P%1{bG1ult_hPVGKNslGwjxVf zH}XE8L<7mNWkmT#De4<+0q$C(3kup`-bg5raNT=^G=+?baqP{3-wO%pYj6tksTE?nWsNn;R&3w?6+SLPlJ# z&Bz&+ptUg?cVt_Zpe;O6#qX|>FrHVz9YECjb+d_{B26}EGq&C%VbkAdccT-3 zEyovaKJa-#!g`vP3b9*MET3M|6$1 zVbuW%K;Sf8jZ*SE_Imgl_W@v6)|t3-BoUTFML?Fkb`k2-A6HXix{{ndGk9s0ny$NU zN>v9OA;(D>2S}61ciuyn*`YyWD@a5%4Pvdi;V^%$rqMO|_M$s8cG%Ll+;0>yMqd$JT6B73YmC+z^aYdrTfj zs*MtSo#K1zDpz)xY0P`uF0p(19NHOaNMfHu_v}*R>Vomur7xo+?nD2 z5j&j^CwAsa?6hs$R6*JOXA+%dBS|dI0kkW-gHv)~lQLe>T1INb%B0tn(2S;`t49F{ z=4jZmH(l3qYWR8I7aB32aIM>kO``kUWJLB@I?{J^cST%lHIkyqyubmv(=nY=b?^)+ zyV(0tk!1TF+O=bflGh3hUVW&UyN`gupHuR|UGggWIzwFE-7Zt5XD(9VyS9|1uSrL! z!RE(9EZ(KV+*8Tlb!0|)eMT)R4e9nAsi1ad`~h9)tUh+!WygVRW+;YJZF{!c+t8r2 zVor4T@HXl=U8i)Xn&aXOIss`JMb;9d1}bTL%moN2vzn4ZRO*Mi!;O@sX0R1JQ-ABh zgW2v}N`VIV`Ws^C^$Qn-g-ApwOB_ZS>6F5e zp%NV;qp(tM1CGM7p++sFI0ErJZc;S=uF`Vu1#Lt2Rz?|d+x!~!w}L`im&%h?3ez|_ z`!oh!!`id>pB{){DiXw)^~$!#IWKY3>QY{+w`skOLAbQ_O1N&6Mb@^P@5Ds2z0fzP z@8e2+#lDhhI0c=cqW;j5d1hRzzj-T{!%U~-fJnF>o#LKz5GLBxs!xbTMA)O=?p+S< zGQ4Yo$=sx4G%@V9-rbyiU7pp=KXd5I^w(6;n?7B!v zU-`XDT?dHe7`H~#K4V;&K8^PkP7Eu2zs5S(yv+sfx=_TKn8`OdQhHDWYAMT^(?g*` zx^nl0lfgQ|!N$@yZgComv9`k*w&&kIipl?CtA}zd%O;iLN0HBO4)BPeb#~7&siQu#k#3SNKoxYB=U*zQAt`y z;)<&r_bNp#6}Cr28p|rSyBVSCf}Vt8?mix??E^C9Y~e~i+Z0s3OW1_Btd~oiYYrXu z&HS?%TP9p8+hT(kILWvpgJ0p@$Y&qcj=2r&YQ`SCh8aANuuO zPgH$ND%TT#B()t*WxjSwrZVA4!xb$D*N;|6OkT-1vyc>Vx6!7uX#r1CAz8V4+4L-d z6CCr@J9K57;qr+6;PGo*Cov3aCb&Fdw8r9F8Im^GbkA3_7}9H-Y(Bc(x3+wK*cK#2 zMi?bYQAHPNqMWW@@#BZ<(&*c3Qmpi10WY3hAp=)+>N+UrUCz3kVlgqy_ub1pOXR7I zRHL#nbNi}9@MsFGtsEV>``-H zo5~eEzaDYrbTF+8q76j>lHmJr=@q^lFXIZp7payD1WKTO$D_3Y~?JzZEM^(j&s>T%5h#1B_1Q9d6X}`f} zNbEEV_gu1++0IwznXS;NUV`3gWWa{S@thnFKB-h{|MYZw`~89u{kBV^^>k^uZ#BNl zfGkfXqh-bh36}|6-PL+`^V%Z|csaYWY9C;ixT6%$-*xgQ5)Lp^Z}3^}tSTHQ6_rcR zikcJO&Dts)Kmo;S9tX^nd8?JRCM1S$0V@yzybQpsGjH5Esj5f!ayr#L>>XZbCcZ=+ z+(aedsc*_&oulS1Cm%#EMNykiNmfXm_$uMkO+nw(MXVjh*TtwI(AUn&*3%qhwPQCV z+3E&c9T^rAq9W!&@|L^Z<`&50bE32-p^{9y}nankxSOyb%i!5XI==|2e` zgI-mY`{dVIl@(NkX4K~kJf{tWVXx$1-d3*>J2k&PL?rI-NZCY0SA@c$3)_}o>ja_>uRQJGs1Z{edq5ZWS`80nm)u>tZ|Ry}u&%L6az!zu&e*EZ#sILK z*UOvc=c+Ed7l|eY)*{OIIL4YIy~SMstv8#cU^@k}aj%&B9D3*@%WFm-4=J5{kAjLZ;%64hJt9^k4GEVYEb?^&V3i}dAbmwW9I27*mB{}bLr~?%;ce&S@r?4+Vo0S zhW_2g+ZU51K_LvTD@%f?ah~|H*!(Z4D^Zjy7K=V?`T9-Z0-ZN}k;at+hjS|t)|ktV zvZ_yK51Pzm@3~e4)FN{xlHH6x%_+L>*N3u`HYMnZ$v7a9?C127sIgtOS7VtDWfQQj?9#nf@nzEMP>pRqnX~PU8aanQHtmcV4)DI5*?E|t5E z3iCO7nWXd1`s7?^u7GbC=arR7YktbAnd@Wi6~*-l%ov}!;qzY)Y9mYNKN>A8Pte=w zth{;ZS6$rCRDH#dao#$u!ZUYb8BRq-kxN)2Ie7}Y8l$=^9P3=;YH`BU? z?x*YM1|1i$qi6V_YlTU#-uU_H7-Aq!WbLCSYvl4?moXjiz*fG+YEuVk()sc{K zs_a6yxUMjn+|A&7j9hpNRb>H-^1H81pbYosbmgi?%UP^+D+g zO4OLm3GJuu#0aayLT>k3K!p35``_;EI+!))36xKmyYH*I>RuUlywfojG~_vb8d>f< zVBVoZd*C{F34GWgGoIi0h9py<;h49- zC=27hl>C3Vd+Wa_yY2BC5d#%a5fCXQ1qlg<7|H;oRRp9}V8~(Up%E1jDRm@=G(cK% zXjHm}&LJg+8fxf#_TYV=bDwiQ-+$oYCtfdxxvpznYp=cbiuanb`07SkFbf-GX|k2I zDr%-uGXSS1o26SOn$CMWBcNvbE6d*X@m->+v;D@vwd~h)%WX%jrcQX%m#PwK#t?-mw zFl@^%amBbpPXs}0%+sE7JPAFLkh>N2Uhgzq_yGFu$iCH_{I`8OA>Lo)`o8>Y!|wHG zp`dNg_5jX<2ASZA|4{wy zhmWX8%R1qzk4{Xv(d~;f=j8amdk34FuhyjQkWICE^W>LD)UxIt+?Ef>9W3(QAA7SK zRGb?4ODLt{rhBgBbWiK0p&0HIA3Ti_qi=PBN{LU=uya9n=&mM%fFA^>b_(NE$;48% z*Nb>$VmBXkjGl7sEx{r`v0+g$a1&C@>PHBD&fR`!f744V!3s4{-sOrgWu}ckCti-; zaonv=@&>QAlZM{2Yp0%q5ypYMd9ET_vYAYWN*IF!wX?z@!7Hm|BTp^s{#3 zTRrSQ^H{$`kN>dmR^3)fLTncJUKe869@1-i+HcGE42OB5p&^rxh{r%{l>$GlTMSJQ zKaA4OykzEsvAvdaS<~B}=dYU-XQ#%iUZ)R!;Mx=4tT*i>SEVp}VK$}wAhXtJPj`{Qm9{h?XQ=}y+0je;@{gD1PfzMU z7x-<|dztq~)$kB!JM4H8UWV3jL({>uM%H4a?T+B|@8%X;_QeI%)8G(szsTBNm6nsw z3@Y2*)32ZemZXVGCh8K4kI}8sm#Hc&Iv-+xTSPSC=-5GBJlXJmi{Awv(c?;|1UC?F zJ#*T68x0u@aP@G6lz$n)L{7pCase~N*)Pn9AQuSEiioBn7P^ghKi!Jr9?#w1k`(Nc z9*Ml`68upfy&f`A#*1F~Y_c$fcG-%u(psMiEj*QN9y2fT(-!7pqFG~9K(VvSR%Gbn z=<_&+YR|=sV|)pFU~ScO<$!xWD-*TF?YHrbEpMT9Nv6*M+gm@rvys|qGT#+c+^QH= zxinT3Kh43R;CIn7KRGTeaES44>z$#@?y?k^^7mMS>f?f1ToI_Jyqds|K0gxQ8|?Y( z)uKkPVu~pojdWR)lI*C#rxkfbL_uqH13yh!49yciB+|~@Vc~-%>I63)|Dd*3kh+e^ z-@f>k%i#KMznN~1bgQg5usUQjUs0#3HFj)NO1i~LZp~-p*)H@lEcF9-ZdZHx%yZMlwMhYVw}iE2q#t%#-J}l{ z6MP>vKV8ue`B1;Kl0xfX5PC-^hAKhB$TqWvzwRLLI3psCi#attHpD%(%T3!Nm9eSD zk7o6PT!z|6HfRQ#wG(J4?~U1fGrXl|I{4I}%E!LRIo%@TVFO?O&#tuK(1>V-FY~z4 zJeIX-@x+7ra6u@uZo0@QMFwc%QBG>*<~dZy^=WY2KwX=*V|Rt;GRfJf?e_MDUzC0r zN3k=GG?_2hyZVS_90R+#-Q8{>n+}g9~CpA;OHyQJW1l)hRHQ?+bOhS4`8tMDf*9HQ4IC5Pbl9W%~`)#XMrwvxxoFqsFapCTqYW|b8u~n3vV(I4{`1F z;DRCGrf1Ak)2t0s%86N(s71jTJ;>ddg)J@4b;{gY^`p(O99UyHrSG|H!@{E+)Jt`6 zDSElxh0*Mo*Z#ezhi!&-PZC!@SQHVq>%YkL)YgUcawFgu$j~ERgC2%ClDc3yeULAU zi5vRZxLf+Bzo*t?mGBsLcM=1MyV0N|h*VHcf{%Jg#wb7E5!ZAvwD;kRE@c4!rE`fccag=r>bMU)CQ>wetd2b9 zHZ_jF*Wk}|JU&Op+{ec3)K4_W2;ov9%UeO&iCP(v3tSJ?Xsc&~8rJ=2H;)0x;cz@w1R=hE*9_oI@=1iaB#9Z#l1)& zKlr^ode{^S=}%3?WpKPc&`a^Yb-`Z7tES{eGk0sZc%2Fw z>%u$3yoUuV$?2lqug~vov{8nMKAiYw=)&95ppPmlc~jcTJ}K@a3EwQMz)-G`4R^qx`z)$2NW?GO*Lz< zQaz{owRZip)S=2n3URipl|_qV^ZeR9F;nq^_|dTD-jLZ~CBZ8vY4PX5iDobnPchhv zxZpTu7kutq^-9mzo0J9anP`_9xQAsH*T*_eoO_(DOU77b8*&dmsG6A?f&JyBv6Ok+ zN*EaTUGcK=pgf|6E5`3+z{AzKM@1lZvyCxrSu|K!J^0CuHjnt51(VwBTJfbC<={W6})q*r`mU-^;j%>-blfc|md?sMM$LGU1 zeu|~!%anz{;LE_FCp>65VBGxa-WU$rOKBpQX)Pz3Ux}O?B<$fFG(;bwg5p`!tqHG{ zafT zVbU<7or_W;a(J}f^pf1Q?VruJs86(IzKwR_;&x9BE{GoSgJ}|^wo?bZ*ANxi_an^`#He2XDaP*nEHa>a4nhp z-$Q`ci`Hz;+`wmV(0Z46<%^a*%+5GDIMJiAGMpSOGr0ECJ&l=mX6U3an%UIOBXf*d zDOx8kyM3*w>6B)}=WPY@_=&49RN~o5*McpYqhQ*Pe{7nU)fAcLBQxQ)!6Bu)MF~w- zBrqUE;6;LH6}PiCt4kffUxLYK%;!a8K8{(*{K?HhBYr>J~k5t`Nl9r4S+onw_-L*a5nP=51Q zufz63(D5p7y^Z-ICoN5l+`QMbTj6oMWwjJdEXj|H;@UHA&s9|j!|4?p4#-> zH>C5QA;Yiv$nMvide!F?lBm77s?TEpheS(Y1e>tK^o%i_?fy1J0}sYR+`m5yo2iM` zH+7D6;1Wz3gRtn#A8@xjz~)v08|{dtZa~;9hKr=2A$P91aa0m#JL|^XW3-kS zB?Z6e$w%2rilwbbN;@k|`FNEtoU#C;F7WiFx!l`ki@KblQhQ6?ot4-x?z+2u7CUy5 zQXZ|;`r@}g8yOQWjk3JeGnfw~KGF!>=Z{*-Uj6+*SouBra7gAke)KA>H;eCwD9%fE zkX=K!1bK7_4>s$V-ldyYFF824$)J(U&|jDoe?uBMIJfz6#sDQ z6)v*K5be|>&q>)CtM6l7qwaoU0neh@mb1sxuEG5qQbAcD4LhAG3iG2qHR5#Z-zt3K zTU&WV(e~i#TZaIdcb!EktUZi1w{q*qm}?SDlEpJqF&vH;W={Ixx+n99$lO3kM575kg5|~ zH<2pYM9r9`D5~*B#d3#Y=8K?iQlGcJ3T;=jk@QM;?+d9~{I9Wc4WB^Z(qsN-yRd3+ zFsj0fE~3VZg3`(bN}4|m zSMUX&yAd*Sd2h48-pIf)J{WhTVZh@QLBA=R-5*)=5vlo+=8)3(#2OV59SYM>n*x<6 z&OY?P16bvx303emsH&u-1rZSsB{|y%cKj#?>7w+4tDTz`j8+@h39wLUyL53%omimh z!lXbOHR*24aAh8%nl0+&@oZR{+RVUrY*3z((Wx_`+nenRT+ ze?1c0e8iI>550Ht_-EU-VMy#qi$@9<*p9y@$cszwZhW2ZJ!})$rWpLhc0b9;;cqoV z#BQ=I6<)qI|Ds-fo$sg0!)OXAtSj|!d5+BwFA5$?XVzk=|5CAVYPxkNbI8{sQ3&_q z1~sKKBX|3>mNnEclV`E$Dnk`%Sm4oa`;Sp)AEHurpaxg^1mB-De=7bOdzW11XqVj( zm}u#*dn&OnS%(&9s(-}vTmd!H5&g(w`l`12Z?LzT8H|$(HK-gaOXEPW2fMO2i6_=~eXKq>T+y=r{E4ewZ^9k{K&*`4p`7h2 zwZL7<*b+Sj3Q2-{XgJ%mLGF(^MGLm{yR!Afl!KOx>h}+HB7XFum>qV;lg|N-<_?;> z-R`Ag*=bS{lJ}phT4TaoDv{ zn=}aVf9h2xPknx06t5R>k91rYG5ruXcGE9R@d{N3lGNUx+&JrG*=9Jm8~BkYnzKFt z|0Nt%B(8;oE#{e#e7M~+5kbeUWZAem661#8OeGh-N7u ziK+Z8oi}hC2xf9|QI1jz7A0QD2%bQd6y9ROV98SRjToMa%~Rndc7RY-%#K+G9{-X; zKf>yaT^R^GChl|9ceFV;(veE`j*@QwoL{%CR8U16F4XQU6>Gl2p_3d{O|fI=`%N(+ z^86c$L1*^GN%v@~3A8%ObmGaJZjN(^g>lel;Pn2H*DptZT8^di2*@>eTle1;^9($3 zhxva*)(X_){de`nqK1Z7M_vsH~ z>#*(KE=~Cpb7m?T5+p7-XHA3FvdgM?9y)CFRfrE2RE(M)fDBD+vr$Kn)VD}PO5s%E z)c@sj^#XgelY_Xwr0Fv2GyUVu1k)YTa)cKue?(g`;9mjFTfKVZyIH-<9cOK0T#T}1 zR*i=PWYAb|^y}CFriW0|km?>TNoVov0L8L8nTbik3SFGp`W;9yXr>*3sHAsA=3-eu zg&gp_wn1!RrD1osblAw=C8jXCoZqI?bOUmtRcx48l*YYee3G(2vBYJ(UC`7L1_|pm zn?ExkpX2&=iCUpPOJKu2?Hs5_yIKz2)%OVZTCc%Laa$(c9#>Sg= zMgrwmn}0+2Bnn71OgI>3(m!aAUH__uk{Gh~R{$9J4e!bcgjS;A`meAG$ySIR8>O?? z%3X4AjL_b~?{01F$^%=~T*ZvqEq1?+?(lg&P@fw?zZ1==-Yg!kRvSyN(D>Ex> zlaF&w{~-;xUZLfu9RdCn5jSuJI=b-9+ZXBfU49f)I32|kFhvb`E)?s*_lo+J0o+ts z##^6nms708V?>iKk^EU{-^LnWh|*aH>@^U;jvHL{tu!U}wVk-y*Z#k1ir=IizW2o; zMojFdqg&ntg-1dD$W8oaIRj4djPEgCUEpstGYAlQarK3{jS?I?2iO(z_0r6#;njC}oorH@ zE{ihtEm>ZH=R>t|Y{Lc28z%J>Px+Z2_%C1b!m+(r=fWiO?1Ydp<1lMr!BM&`l`y7(KzrXdL0cPp1(Q00uSau^NobL9Gr|B9ElSgy_f;#QL&7MUj>? zw;xvI0_NI&tB9-Q#A96D>4Aqnwo+C_^FhUk1e@J@Kq+JG&$^*V4);J>X9P?Eht_Ka zAaz*e%5$%oYtQ95w~a4NuKXf`xhO_*A0LuusjV@Kgk6H$EjeuY3Yl9cH7^(^17WG- z76jw@#DK6CDGRJlT^=3e|K^IiH8?S#v4TsU`Ba#Y7!?PRkD1%;y!>VNmLJ6D{N>~D z@CiB%Md*R#`Zl;y-bA@{g3?riJX2Y$aQp1h5s6f%Oit$Zz|A{)xq zcm>t0URh{z9Z)EB-&5}g$h6@`vw64aoOna*P%LVl?N;J=M7KbvNcX zM^UE;(mksy3$nfMSK4p)MNmpQ&jwlZo3)d)C1-#wInIF;RQ;+@MW39b{CKEf-xfK& zdlzTsfrq)sKwS-8oGW0fEq*LLRyT5Qak)8Mv?OlV9j<(C#^y*$X@^ld8wd7Ji~CjX z4iA_Js2ib6_o{!|kB3u)nq0KZ*-6d>l-{f5xM-Bv(%ej8lrW4^|1rocER8H$9X?2a z&TF!G%eDYCb$Z}4JU>m~NNh#nELDQj{^`?GItbleyI??sF46LGxvpQ>OC)~SRvUwy zs>dHJ*2+Cr^X2~}i;?k4S&CE&GP=EW?XUeMP3U11Df5H%*nw?fMq%*T@;)WIFA+7%`ld>l}%euaB7wCgmAil_t0LMyMs#wI_xu_|Defh^us)e7+-7 zWU6Wgc;olmfeTsu9vpcyf-4m3)J&v$Ft<;7Z8BfRdc7+KCbJEwop%3R8uXbWU3qZ4B2nH`hhnCtaJF$n;}9Y(p`&OEw+7) zhgz}{IAC2Z+?THVVQ*Hz{pzx&Ysyk~K)TVUcDI(%!{%Z!%*_NO^|Z@q*87>srG(wa zRb?DnJX-#GKSjOs#76-!KSmLDR5WICZp76^R%X6w)G@Q}@{IlYMdzrK-pG@X<-6o? z*$bG`%s44e^@Qi+I%_RBl)*$!lXOBq1IdDb7_E0!(Spp}^b?0cTuODTsGus(W~JQl zy7HVOe{8Dn%f=qj|MdB<>S}o>TTHj=k*acc2|mrY)cxT@2?OfnS(P|Y2(*YwHa0OKJN=!`F*7cirJuFWP zHV=XGsS-!F3HUxlV^@nhmC;BEfu$=-SNyeSmVwq63jN`hkv8NNB`YW!frXZ(%4hON z0^Ny_{!l=XMr*7tIKF`=&|12L8&b&~?swDGMFv>N6;Dfeg=!c}46S6;gT1lTzAEbw zzjKs&Z340J#01^^vI@_urkd(h&KQG}^e0r3dbFnOk@Fhv9v`N?jgE(LoHw`r&XZ{u zoGCiSyU-k7(mLJSO`Q6}S1 zRz`|z+hK6Ef6~IYQA!#KO0yf|od&k|hvGJhI-x8oi#IUMplizn z(+W(GoUEuLLG3>eDh)k74?+GhxCq7zTwSAt8y2ST+c-F}-RlqJ~Oy^G=;4Jb+ z++}yXK)&bjuWT!&>we^qwnMBD17fkYv#Ln^;3d!yQ` z2nz;`I0SoB_DLjH)j<9}aXA(LD2MtU!qw$9DEu>c%LFK0ADT80)>xWT%ebqeru^W3^ z<9$__72A}v_3oNUJ(~o?c01?QzSusWpS=R@pZkP-SI2)^&{Ad(U+z(&Ea2IHU{HaJ%WfX_UC2DtCV` zR^CEVdgwvfOgUTNyWHP7?N%%2N_;Jc#Lw`u^Z)SpUw}n zK@Wfetc_OMJU@eZ3z7Xt0}2Ql-%m93ZQky-pkXyT@LqF|(0^M8Wy*qR-NKEm2bM?! z&h^b?5F2R6_LDuQ-Q4GQ9@tUM#ZgGndt&y{jfO)Gxnt6otVI<&K*NjY-D$X>21wXk zK(0P_ptXGOAO`nxIEw48&4chsvEj36raOnTibWUdfZiZOTf}s5vmd-e%1C~l$k9>F zJ-3*$;Idp^tC)O5ZSAEYN=!DZ0a^xBke3q!1<_R&LeI$ow>S`caLu&$psvbmZGEJz z?aJqwwB+VQA6B~{Dh~}$O3<5y>chxITsaRj5m8D#aX+cY+XIy5t4}9|361WizVfxE zWP>Pv*AlNj^-I9_E!Bp)r}9l&OPZbJg^QN!z8_1%eQ@62ERw|cv1upNl1vXrTSahi z{^YsEMd1Kz{YO3CQV=iE_>33z^g^+7C<>_w=DwKA;W5Bi)mV^esQUNN>^}{6N2X=M z5Lg`91JjoYHM{Y@QFl9xZDgc{tQ^(NkSwz$x6C9w_R;bg2GY0s;TvUVqDTR-b=*_S zD$aIm3%re6_UH0muo+KePQ=hF#gv+>erj6RZ291cX~uFRUnNh?MUlDyNf`~$Kn~ak z?{0vDe>bVbw?FnL&YL{zie#6i?D;`DCZ_J=1yo!%kaJJqI0G&>UGy2ozCE>S?>9p{KeRR-)w%0{q&v$H(_V2bx7xomekb zr1>Z(#k1h-@{0z{>GPnet+b4WhKjvznwX4qku)xhfR}6~o&S;-exi z7prOypQ>Y=%)4WrH=+5yyRRO3hG2e17?ZN7;Z++g2?wZ>xl^)lAaXQzC=v1K(s| zqyn`MVC-)}5;F#DA3}*&X7bAY(}D)dJ^>o2jyX%$?ERmp55FNjpt`4YT;sX)gz$v( zC6`!IYZ1iaIQGxI_`1Hu2Y%Y`^>8*;d7$V)tvB*S%BZt{^eI<6w`r2-5Vsep#(6VS z&57n)rcdNUvq~_OLejroM?7>xlRVzLcV+GCvS|`>Q4FoN3)MTlyb6-XE?gof0!_y@M$Qo;Enn^IAPjh2$$QO>E}VYWXQITD=85m zpd^=SziC`HSoUE_B`zr3^(`jwQ+}_9y|GHl(A<8f&*_AXyRWZuwbymDG5*G%OF1dz z9PQeju$)wOILq*26kwz$w`W6?EvCPM_C-P88#WC!U?=l%GeU3IjKJu3WZS1#oKB(` zEU0@^$_*nyj04~MSb6eF^nY0lwvLYbyV#v8xGuv{T`jh`1*99}^}$il*EM;&g=lTO z+c~T9oD?t@8G0{8TljOy-xVp<&`eZ_k>1uG4OnB6R5IT7s(Ef}^LAD>HROh5{`imf zulz{wW({~hJ_?2Sx>w`5&l8X(b$wrZxNV`*r*ltQztGuTjWJkPwO^;X^ zwz$?H05A9ydM%l6olrW6I?j&4Py^SJp;4N;ts9i%@|3mpD^n9QuB*4|)~BSZPzPK} zjlEp{G-iTex5V18tgHpJCB8`ZCT%yXR`>&?+?dvAM*TGP+hM7J$+||yExS|-`~&_~ zci#BHhjTcCFJng)<*o?Fb^nU`;d0LvnuxTKumValGj<0hHM|>QgvE6=hw$=IW&HLk zM%jI?v$5hD60=S>d&3!-IQGqP-*Wl_CaoDOp4w(y(37}rg%E+fY=pIC%aGC7O*k5ZvH4hAsE{+|h(7t-jtr6HzA zw^HuaIuNbrkm)pq3WgfHambDX?6CDK*Vxn!Z5t+2gFN6uu(zF(78mYa0lm=^FgXCC9hKhh`{-vR|O(86nO@SMeN|8`)O9Tb4cgf#*~dXj`Rp5jAM(nnC(_U+tyj z%Kd5_1aOVYs&1YtYuCrkn;){B2wZV3+G~{d-(RUh7JLi$aoo6*#zwhz_gO8E-u~cC z4x9SfMNyeq-r&URn4RbO)v#zfS6xpDh()Fx}c%{DO-|f2=2TaqHh)#QC1I~$*^yG zxp$-vIB^_9BMc^trX89PFsyRV#B$H*Is)HPCogqIrE38@FXv z%5m$y&qQwHtFqLxu#z)HqKq+;#eA~@-BZ5mCFo#PNngAi73%BKUQNdm-(=H&8HYcp zoAc-EwlSllEUeCwwgU@wgvGv-#pZFoyK?Byg5lMTmi2O}nN8b+ocB8ZGdA<*zhC~0 zX2gpiT-S6qkLjuZ3Ja5+j=b}bQPi}a2;+&-DfNjdE#jg@R7g%tev3R$V!HfKzU2>d z=i^J941rDMF+V7~U;(8lq9?D^Ld5bneZRu$>{3DSSv86AI2G9mF1#whQvKC+e{>Ho z9@*!6j#3A`t<4UT?b~F6Ob;xHV>Umw4=-FeJ*u{psnHsuJ5;i;gQTmI_H3*f`5fk> zbB%%C1?VD!)UmCoqnO=`4;-nIe*>>nUXaRfT^VLrc1saWpEcA$S#1`7s$|76-H$}% zfs>Ur%d?Uq4W z*0{)eKX&0TKVy*#JNF!%9*4Iu8%9*7iyv%b*8%8+^$)nMxSjGpJJA1d4Ii8Vt)Z^U zPo!<|mCG)Hq7ItpT-ijj8{pAYlfO4we$oZ>Z;?6xscGsAI84-SS{7liB}^xvc)oQ8!^+o&}lrvBTUbqjjyPv0HhAMwRL*W{kCIdFGK(eTE`N4I-1dDq0h?6PvFKC#&)y)z*=R3sgjhwQwvbMEhk&x}jz|6TI6RFV_ZRsFI zULE2aAg*S2BK}FF#-g`x={#z-d9(>-p%5`&pHjAA){xKx`V6v--Lq){K!SsdZF%Bv z1Xoj&%B%n&bxAr|+t?Tw7}VF-Pchw1d)M0a7f>we-gT|xM!O>B?SduI^-K+)zd6G3 zlf4Cd67{vU(j#{S`K>{!UDmn@5W22k0@%7p4M6(3nGf(@kFL-O7<~QvF=iVOrz=3{ z;l&oCaWnXpWEF!#XE$s8v2Gi-%xjrC;0}5MkH{0e6~dZiWLHrmBgX582LM_J2pYos zez7}Tx4pxpp$%r9fAhqy3&bGswkAM2_HARYGcPO~*5M>^CO{7G1W+EJ^W;u>6^T06 zC3r(uM+YFy6tbE{0eEhJ>B<#rIQ*7k2Y{&EJ`9bu4f-3A#dW`|%&FKrinD2hFx2Pp zc7d_-%kXf?1O_$Rkv9dx6hFgNMLa>-F^NjHMWR8y%t$FME#+KVI!#8l%5^cQ8?fkf zPlfL6Uu%DrY%cn*do|a;#Nw1^I;x=@(7)zj%-@jy_t$!iWLlBz@h>pnL2qORRi{hdb(5(*MQ$>*wgB-*vU9<2(X9stg_blAW!g;zNh9_#g1xrE*>Xfm?yY$N(tkdMw%gNe%t9e}5rB zuL29L{|qNAD=UbGT4(^^-qC#uIf&2n_UimUxRtihFjko>ql16ZJTEB#Y3zBDC^BCs zRsS!9<#$4Vp1G=*4FDE`qCv6;mw+CvEL}?nq|UURa4}#>{s@iiFB%>od42}i%*w*Q zfSk1-qTfK7sZ|?&e+z{C3)e^@$C7UU81B9F@_iDQPT^y87}upU%Kxv=T?o4O8?A3( zaDyaec9o$sr6kD#kmr}ZKU)YH*+z|!z+b%42ie=?WG^1oE{*?ncOSoRt+!DCBg&YvD1bwJ&9zARNKcQX2}eKI=|CGKCD{2WNkTVYL2!V{x_|vm#outBM9l+q zD6*uFwMQ7;fBl=|x)#{H0pAE*mHrU&=|k0Dca`+Gp+}PxWcmkU7r-G&KQA6#)0G2K z`2WGhPJB^27YB8)dh%ZG*d>!a+#8jH|9!|a_hMoacnR(5F~!@%GX;CczC7YS$<87W z%A_lhU?Obcwn}#_BX#23=o&=5*KtW5F<^vp8fQ8t5UZ$N59A# zZdC2L8x~h?-C~1aGxlwZbGN}MP9l7@>UJ~>ZdI)yY$A1Aqgu6E4GNs6O4Xs%@dvIi z9kQqFJl5tKId(^I1+MO$7V`U-J_44R=#>F25}zN|YLZv#qiN=>$a>66=-~F_zh-Ow z4cYt)@(KuO+c#wZ27B&pllzKH!o%9Tw}iwOXUj#TvJ?uNz^qX^rydXWZzcwzzPzUU zCblLR=Vx9}k>b?3lIgA*$Eo`OYFFUhb@rC_*~usOY4<4fXAie#Pc$Zb)V@rWTg3A) zoIWjStNaJPs2~i1xE}0n14|bf85s{nR&I`Xj(Ts#1LElI^DJeO13OKV7KrB2W${1G z5s(>&xyM*@T@MY9g;wba?dAh86=Z;em=W32-Ob;nba7WY5q&V-Kh0BUxsp&jFDo%3 zDwceY=cJ}@DX+1bBEN0|>?F=7hqdF@E#JiP7c?H^G8e22xp5*kq`_OPr}ost^hf|_ zu%jNUAx^i+9hMgs-bxTpoh1+=Qpx4d56$kFEyVNGUGZgRDQD-}LjS>Hake-3|Ct!= z`5P*g{b|*r>M$v5zVpFz6>Ia~iim}<;;QsUvHLp-QPiUwHntgt(6vsQ!UwrWvnXnm z5cRyI(Q)RdlY^$3aBIl%fW$h*4W6@JyRtSflnJe{kBCH+{)0bubmfCoN$g09VN8^f9?MpfY3tRNZK&p>Hl|n`P z3+OUkz24MyA2(Pqs(()6rXreM0M;eD~A81wF zY_O&_@uB;4?5_Orgb&q`Bt$jv6~1CHo0E+w0)bHGN^p!7PnGmND9^k^wg;aJp3bcU znP6f+RI|bKG&v9hfSspAyK;Y9^>{Wl^-9Yww}_m2dhZ zUOmzXt&~$z$nG42!so340h;XGjOC8_-`P|+O@jd9bvjUe;~4C80jTyap&?<1j;8j} z%(kjd-;yx2`&i;z-}Bo4WVBy0b{r`;t)~tOBPQqPYmFQy^Wt|OButvG=3}N9|8Z|D z)tTiq@Wt3XId&0&*9j)HrF`LA^+(eysdh3c*PD-1tH)Wot&Kc-bM=|7GIzWDi$=%MWQvA{E7G*cQ zfR!|DM5*R=Q1&0?)N#mLHyPWO75H>JuxatY+&VOClVSZzIDOShUjp*An zO7`;UZo2Q#-q`Vh$Uypv_0LtILUSluhxL{4J3+R=V7KHwAc+%M9C`By{|ltB$;T!QzqS%ldStNq#@Jx2B>xwJb2rHi!FF zT)*ey4|lxSvo*|mqACycN9g4J3NaYfOA9B~hrhCrjmd%4F_FyB^$Rv3Qn~NjeD=i< zxEngqFtySJXW*H7c#5QlS2tGY;4&La>_!xlnxaROPAcV%RVnn0?T_FLwa&3m zHda>e70F&4s`+fVMp|J|QE5W4Q@7+nG4l9ZVZQ{?qv7SusSQn!`=$fCb$10m0SJ_4 z>-l)lEnksnx0QU(xA*+*NZN~*KTIZ3_kk18kfFXLN81LDW3_$ss&w-0hC4g+tuz8E zXM-yAM|L_!Rr1Lu$B1omQEJ6+NIDT253U1>v=Kpez(1Ij4 z8XFN53=VzBZ8w@A_@ho^l$H9|1%Gmqn4Im!#EGT`WR<;g_Ey#CLs8I!+6;6FFsjLC zI{Qq-y{h;G&TW`U?%91Vpp>_h1$In1Ao%4L;|sZgI9I59jHSDt@wv$muy$I&SSmL= zufvq+T~w=Y$>L2sqa@3qp~;~S{(Bdg(ZyQXp1V{b4#`Z@E4|LUk`--IXxlTmQ}@6V zOjd|WM$JqF2A=f359Tf6lv zI+jKuV9BzrQ4)-XL*VmiKheIjpuSbLGt@b^gMFAbkfe6*o|OI-6XS<^xqRJ;oh(iF zOz;VHp=}42MXGoqy~11vK(V5(k8piZC}w9!bnDrx@xZ6W7sIM;aGTy27+VO@3P}wI zJbu(+hxo&7U*N<}(U2|2V7n5zU?Up*YzOYa(&w&5+tG|J%f9xpd|z-<9t)Nv7qg)B zZws;mv4Neptt8c(8KZNTc_#|Yg6LqOl@$kzI^qp$%tu5jB(=!H#dXS=x^)?UDu$t;zLTpQg_%3hv(Usu?ZHsF|kLN0C8 z*XMELH9xEqNpK&75Sna*xw9oo>+p;`l)UP4xkJzIfy)w-ehJJoLmP(Lb25Dv0$v4+ zT|NwVKc(g%?R*_(7of47Vc+8J*ii&5I9;51arw%XN4w7k^2}Go$+j8;eMde-%G5_4 z_^Ga*BfDMX-6NNh`b)u3)@Pf`>eLIb>)4w!EKt{m-D58hH7UbB_@DyI0j$VKxuVXo z$>Wq_Lr2rXM5m#fLOF+PP4^xrpiR2^pqTw~zLj2LMjvExK4gt51CgqBZc|Zev#QKT zPe0rBaJ4kD!7@^~cl&;lMJYt)oy>{~YLuk06@xv!ahJ}|aXLRv*`=|45_fIRp!2fu zfYF?47$nm}qEeeK*k^5FXD0v-z1td!hV=G6W_;B7mMt40X<>RfH~VT?-ueBcO3TUT z&8opDbm{s;*g#5xF^EBDE{lT8Qa?n{=eHe?Wsp+Z@+1T-ppEo92Z8{yz zcbnt0HO&;{Wr2kjn8Fpg(9|MxW8Q9u9~jJZBe&IEK6ap5+hjiR=>HiC`VuTOlbMo~bJiS61 z8-9mXb!*Pq*SPYF292vfB+Dp4+SBEXfb(NTsJNU*&f^d@_Rg3Nt)=WINm&KIPtC`M zZ>H{lxeC$8__*&;=F?|UulEnkp6HJ_F?TA9DpYf}(Y!7N41dy}KaiDK2^BShzx7&o zWrbc#Z!4;JFgGj0(_W{%pj88^)Yc4}cWS)W>4+HzmD8!bqThKMWg#+p)_IjUvKjDEt?+foi+WrKSEhdU$k(UDls`BtfWaYiv&0Oni?&cAYYC z6t31B5d6KIh+=ET9dAek&b^9`AsHaD@6`<#;PRV+XZH4m^fkW#&!!)VCf~%$nAS3P zqx$w|yF<5H9h_WA17UXB>zMwea5pZ=PfBF=W` z=TCQ5+4mcPi*LA(wDqbs|fz-Pf`U84qb_!RCQ@z*!sI8I-{EInU%4= z5yTDMio14*W|TZoVOON5Yi82WQ$U^=i142|RmfwPpc0gr57B0n*1>ddVRg#n!YktI zHQKpP0;8Upk^m|2)uitSs>J`&SuIO0QZ6db<_S>EsJ3NjGU z3^$y$6p8t``}1&yIsg4_%{Xhr1UJml=)-sq#WqN&Cn|(nI>b0S09so{S6$gKRfH$Qs%=qsuuDGbat1;Rz%569ApTtt0KU%W>PnR z&3TgDY79mX)=V{Zb+krmXI=#u?x)K=nH-|_DW#-&R1(CHb8f37q0~406C!SGQWC|z zE~gtsOQUlgMKA8iDaS0zgxl`$Z5!kt9UE3I-P8(66=gajx4A-KSTtLle7?}MxAw|| z`CI|~+eamQi{2C5YG{wpWo`|z%Yh6Q!}}-Z30^KQtzm1EheipkxWQ)2&lXprz7+mG zNKg0AJ?*v4HkdsG5i^Q8NSP|}&!_5q3}3h$Suc7;rsuoYhB*<;#%{F7me3AtxOwo) zB4=Yqdw6X6M@`+Iu-`Nj2TT2@|5=7F9AaP~!YUb=+hi+^$^B+@Xmb`W(&RB2Mcdb$ zDj!1CA*LX%SUz}1aW$K4HS2tJQV{S!l)}2~c!Yo>miGt@bOr}X&ueROl62Z_Ro=@l z!5Hq;=n{vmse!BZ*ovOeR5(>8Bq&zxIOJ6lcRO+^tRPFhd6#c7-gl`Y06jZW;$Yo2 z?UG?*t&?LTVJ~cMxOBfSg{_gbVyTaX-c&OpExR>fpdXy#x1jFQM!Bx$tUKe`tAt9t z#piba8?i$D{XwF4;Z4QJu1^+@~loYUk9G8HUgoQY~QM@2RGY_8Yds_JSa*(#xIsS-6L z;p^e)X-*&;i=XLNhYu3gt_}>74wb!UCuu0f9K}ViP-dwPdEP?1N8B(T_I0Rnz>01} z?VLjW`dXqHSeYB4AFHHJrzxhGDKaz0-JXIO^Ik@>a10R3^purHh5{k}sbAs-CRcWL zyL?~16F$5#nB7vsw#aupP@F%KA)Wpp58g0h6Iz0p9yaM8!B)`1tWb`r-(%!8N$vkP z?scACIQQv&9oMQ^eC;Xw0voN>ioT_^5m(uHra0Wj=Yr(9@NOgwn+b+_pnw>j4&)Vx z*2SG=5Cp#8gM0C*a?P?t*XIMhk9cy9J$MVemHx^(wl4CCa-q30umN9x5gADjj%H%*jDXFD zJI#`GH}33jn2?6kCiG^%^`EEFGgJQwYG{lBOXAjkWsiV&5p)4&u}?GU;woD5v#2n5 zl8$n-+__((%JCu6JZwK{C`e4FO|gPBeiw%H;hHG#Vnq7h_B4nv<>5px(qw&bIgJKQ zI}93N$ne=W`M$4V8W8C?FSFJhN~4It7Ih=5gP5*oy{82S8o~U*cg2*>zw*qRCxywb zN75XYa|yt(dCmz3YQ`s%oaw|~>*xw1jwjs=W0qu}b-)5=-Axce*w{FXk7UD_ied;QM+pftX}|I77U*L}w4 ze9q@w7ek#&gKb3>Ph6u+eQ5qI<&xT`|Aa_GfdkK9j_H{6eq4T6)M)8a(%tg1yDG|r z&oAUdlIax_VPnq%2FsC$fyRnC1|}WPxc{r2Yzec5-Ez@YN(jq;+0Dr#mAm*4_T~AR z?~FtS0a`8CN^-B1kq%`5|B8A$+-bKyoW5>k@JN#0Lx0hFmT$gGoNNHJxmF!n1D6)v#yEtC`#AGOgWS z7{^xol;x6a8aW&)udDl2NOSWp=ij;J@tyADK^4=*B4zrIHdqj6|I}jkxX?2U69{38 zv%~r6SC?B$gY^-CDUA^Njc#^47acRD+&h!{8{IZlVivA9-A^+5iD_}MJ4>iLS1RH> ziy+@PlfCp@z;Ma@3AT8xYRBnqUYZ%O8UJ$jlQ}Ouzc<+u=_pwkna$|)q%$z@s%Dh{ zcI0rE_qBh~SMW-vtm1SJs7;j6lRJy}LU`_h7FOhmVQq;vSNS6-brL*s7y z*sS%#c5-Rw;9=7s&d<4+LY5Hce=9hhouZ4xdyF^Q97pcXTdRiAeEj5>ODZE{$5lR( zj+WuvJn(3vRTa8Lo*wwP{k7rr^@vB`blEJCnNtV}8>zvoXswM_T?e9n-UMBbEHvp_ z4Zu5Tl6aI(<*%ia^17x4Jks$P9kU7dsy7X-r(K^xT@+Z+Y` z?l9FGo1gbg#NfMTYVTVhw|##8+Tio@5z0V)4{@S{`sRfYX}qeR!c*Y-VcxH~VTRtZ z^>TScg+NzO{{3|A1Ttx%TU<4CW5sv09JtcNVdkDP zD{_WcEz)YPd*-GuSBh!4oS(oc3z{fdNsEgUxYi>J#7mLwRLttPLtwV)H!4jfvWp@} zjtw|HKB(@uaNW0xc>}y&=daXj3LY5B+;~Sr zxAvrcA%)LTh2mPGpOg5mF*CU6QNp&@PpH?9-+ZmIE8Os6#>1LEoRAGtA(A{(!qV0G z%xu-R{%N37Fv>_^SVuW~*HV>FqfDJEM6Y;acmrev|7UnDL3uyj; z-f-(koX$-&n5{QpBW&=>M)%;FrmfEbYOhPL7FzaA0B`FtKPWe0) zBan z*DGWDjpIs(T{E-MDzC57PK81_dqyLga&XAcW|wc<=g*1`4u$XDCj#eiFMwXMddA8~ ziXs=yH+eDI?3GXDV|6Q*)Z$AFg4wT^RrO(o|GT{Z3&WDzr0;z2iS|dcdb>;?RXIVe zb;RJstoOX`E)xfZ{zD}Pvr|OZ@^73vGWd2r_e?lY@VAsAEe1^QB=9J1grr;_l}UII zy<#;O&Ncmy=#x=hmxqPxW?Ij63-GP3nV6(<6Q3#ADB#XFzGY9N+N)cyAff-n?EyAn zNLd$*a%J9Z+4D>lfOeR(ej2zGU;gBX*FBWgZb%>)Z4=Rn+ zqI3k*@{910BBGOpN|l-$XU}TZJB#E1nP4Xg}`ZxKT~#R+8*>$3&uZVD;{R%JQX$)!HtJE*CDE zwzH%OW>h&dO*po0WJ2N?PlvS7o9IgFZYC6MJ5rb}``VypW2NF*Hty1GU>Bii5%COA zV;r{E@kXjYf3qRWx`9y&erMc~_t5O+MkKuKp1!uQF+Wwqe39maa7$ZTm?0}r--X;~ zaknx8N3o zfs>NKV!KSPFAFMgcd%%hGpOzLC}y}9%s-HqCjir_?S!f)qD6V~WEmaP`oawxuNyCS ztc{RVH}=aiuCKi!vzu^Sd^8GoJW$p+6Aj*XcgM5C`RdnBXd0_q{$6Q)jM~4h2<*@d zDARb|DkhRUw3&j4?gTJiTnC?1D$_7a#H7))W!d!0CHh41ex|F(t41S#mQKC4b_*Y~ zLMs(B22>5Mgm4SEwX$=>$`NRF+!C6s%GoqSf`{&0dCje*E!MQ>T(}YrP$a7uu^n3O z>Z=WDD31wCyGa`b9ETmxJmu%= za7jBdxC(Zfuv_`^nwoLUG*Bp5+B4&b5JQTFkG zgD|`4^eKYm)Ig>ed%E9T3=JkHYlF{WgY-*N(11&6fsfhi6?4BNSt(hIPjYH%yssZI z47_Qw`-ey89DlvGsvB=0t_auK?d3yJf;v|&ttLiakGB`6EsuWeO}~%r0*2e;!|EZc z9xrdA9-7s=7fgv7`FxIO!4$^n8h@UaT!PGqtn*H^W+rXZ8X_tUD{>=mm2a6CI!t&o zwg}xCJCj(;dc=USLAd65GhL%6ePneEeR_pB!R6AKw~WVUjgQM*I~3P$|EBmespN;F z8o9GHBA;eh+@k6p>4raQW>#6xo=rb77`G(LMD2Lf@$vhc|Ehl!OIS1zV(g(B)V;nd z_31ut)8s_O1htwx&02;DMboY^yVa04Zz%D;EQB*o6lKy)ug@4)b%by^eOc#Ncp$mJ zYImR~YymX28ZK)&#<)6lV-K}Eh^~Ea3X058x6@**-&2XN=1~;!eRN$siWGUf%UNSA z_k2ONZCa);ZBC(Cj=1DwiO+2EO)WwvPd1QY4As-wMdnj-MN9XD-#USoy!XOKN2D2b z3mEMTX+JkINqR(3;p$6Cha1v-RE(|K+svHG66VouxTH07c-H5-iIBRNDur^Et~hmF z(#;0)tl>QNha~wrrq|*ZNBweLvuo*?i3Z$9nXcBlOIV? z3RmZDV53Q$QTZ&EFmE8#tFep6YqM_WczohN%xQ~uk!J+P zT`(UP7#v*9#xHnhP9kGWRGs#S$DjU}l>rfceYehGN4wpEvUgcCb+1?-t! zE5}OY?HH>92Zw8EP41Ay-=J2oD^`D4eroA9cX(|}cQ^AI3G>AU`CG9B$N^){`)SOr z@2fnS25hPuPbpx^lmeUdp!rTR;vn>Xc1&h}uCjd;6GySgfw=hE;EOj8cCL<{GxSlC zCBI^lS-4Ew#`c}wjAK2*cw**~TNJI`n!p{K%vfjB>O9ie?_^@RFOF{_+5{6$3coKYN{_C+7-cabPe&@ zHOW0<;JH*$7V>(d9A4W={^j#3#@3goF1e-3egKo;f2o6o4_)78(b?5en(?<7k~madt< zb{pzZ&shzejl`N2tEwr{peUh*K3zofpbsafUhU&_If?*^nnrp3#4gnshiOHQLapNE zYvPXu*_OY1GM`S&c+zZihcmLl@WGAvt3-Ub7-{YK@&Z*mH-XN7qjquJ5Uh=fWos0E z_DWI4Em!-~`+EwhT+3l*XF2L^eQ32_FFoinq0UOXcqQ(NB;A+G_D^2h%gdzgSIHSU zxlvTj(QV@>&*{eW^0sa4`Q~6EP6?hNZZ*d6+B2VL-*o!zacQPnTQ|(c0Lle{Q?Hg|054~Ldi_E=gn_w8eEv>V7S%(H?{>U6-=uBF zVPTX|yqNpK?2}(hVBxo%Mcb?|>~pfwQgJiBY$H!< z7Dr?tqg3u6!l)73_uz>nrL38#_^y2|!L3f03S1RgKX*IMrpFV`CwaJ+m?t)gb|+V^|2CAgcGr1IS-o_A|g=V}(>ias)UR&wN(x1Sk(QP*TBa>D#A zX$2$`YNb)I6gHD~nhsp|Pb2u_DQe$3u1&n0Qa9DWXsEnf`m$v9u->!@PDoKgt&2W= zH!_32=+u5_c{S~1Lq<%Kf?;x2L}v!?{I!A@HYfXGr<`zOI!m0z%oSnq zS!vF)?M9AusndK4llp)6SF>ay_iLD|+B}l2r?|C0mj159qj7mk(=aECrIaXVvyD4% zM>XW$w0G)vP|z=(yw>Bx$5qUgiJ7lgH(T`KkkD_J=wtCIZ)&8dX$i~bO8T1qgl&Od z>{^2RxIC@tLUm%D(nB8^r|b~BL)p6`6Il1Uv`a2@uSyz<>%Dc-3Nb!K;>#~-9kr~q zDiuNRHlKUaPQ?CcRHtokcfsDu#+eBMrK?N?M+~kj!;xy2tj(*~?JK*m>gL-v+!%+c zVeTlmxi>x>=^OIKvCe5aui%5NZVjcWhOfdd*9T!y@#GSBWq3~JugHsy=IEU_N^Tdj z$Hn_Lj!|Ves(c zd$9u#TgH=?SL^pjD05u4NH7Yph6^3OsaP*KhBI_uUbRo1B~7kQpWPEsDK7J-grV=% zqjWe3r}dBrz0xv1WYs$1F9#$XFTeUK%rJGRHnU~6caVLq)EQ!I)EE*4xVx^@AAUnp0~!)6EakN4s2lDU*_tB$QHyqn~GtoY&~Q zBWP8)H-1g`ww1sJ&RgH!7`WUf_I=lG&i-2V?!W}(K!2~dmfHOyXPry z0ez(U3;NhR*=rkD?Xri&aqn^DQRYeR+t%gwcE11Nluju3y`qtcNoEhpZdM9C;{9RT zzF4Oo$xwVOGSgVsE%fwSb%wM?n#t4Dz{JkeYkt}~MO%qc%=f}8X}L*hfw=*n$^f57 zH}~+^7*$gfv1^0ne4oE-O2;!7Qt$Jil{{+dOL9Ww3rjPu%k4@sB>Q)}$eS){``4Wa zxXxaS?@7H8Q@}+vHbZ92A$z7nfY^AsbAay#eY*G_ioD3qV(aGiywMrjbk9p21YR!l z^Kd`bxRi`!=`CBQW#42o^??W0!ow*Gg=gm#Vj!$LT$=GHq^iut^cJg%sk3+14wNGo7LuUhh!X4&oV^8F6si%cT1u7XbsU=W6 zy6YWOi_4Jz?0eJVi*P?O+)9l76D;3|=Xf#nyG8>>cd zC5{#?*GGz^1$^xZpP(rzPf@m+&EhCcVww9qb!*Lrmt^9Bf{*%`($_s+DW~;AcemRH zTSNW>9k1Q#GTQ=sdeK|6Ra0+k<_Y(j@sVFraT4)2o?DXaIySD>)@)@_=OO9NyHM3# zErZJ+Jo&ma_ugI!Qqp0fK?49pajA8ui``_UkH*2{)D8W%-~qm>-;WODfA77xqGiHv zf=@6nLf=b%oN+6{AK}IVpbSqB5{>3q1$?jbF65yziz!X$2fJ{2e50nUtS722myC4X zyndFQPERHyt*F5wH8pF%{EJ1)n@?Bo$mnOyb>3%=ydl0JlVtv~W3G0zY*ujG%v6I% zZmNMiecrfAWkbiz;VZ@D6FNsy9i!Q+w z_|!V-LK9CW=cu#qAHes8aCTJ0pGPHCKB4jK8iH7UylG02MH%egChj=zmungOvYx6I zBfz4#m11Z^_hp}SXwf-`1!lssMC4AZ&uX2Q|Ft!mDB8DAW5q#C%*VxW;f37IJ!?F* z&1Pm^Dt@*k-$)CJT@|hBlsZL%WH|y3%+rSP7TT-5&u3OJo}V%HjG@o#;HB!IV<(>F zc`bigfBD$lTZV^CjrrNcUJ8@4@#9xsk}eWPDm*EEYD~YsC3x-L3;OAxyPuDc)sp+eP zw;*BgX@G=n*s|z;s2-v97p8RoBvu@TSS=}zIuROx?2nsV08WTP;61YQHS@3*0Q zP_fsc#3CY{b2hj?FsanjKCnC4)L_VwJ2Q4vsb(?3qP=OcjB<`E-+npMx%KD-O%|C?*WglD=h$kr3HwHEmnMf?ToSje z6mLgyw!X|{&HnC-j?pr`AN={r>iyxyG#O>(bM`u!ZR!j~HP-r0mNq(bM{g2zKWup`#;D@t@aE1SuSfR3SMh=yIxYhPd*~XIGF|L49_eN$+UwbWH+mrZh))4H`a73$m8+CEvj zqomZQ2u>NkZ)MT6gPUSu5TL1PnCZ4(5 z`HEidvj4k|xXbtG%?{8rjYQVlQ*^cp)a+**cAq|q_^Rx3?oLSP;)8Mzo~k3xNldMK z8)(3Izosv<&Dfu<((($qx>;t9I)W&h2ZWaEN=;eb5I!EIp5u@qNkJ`Cbz#f65h~A{ z-kg%9%c_g%H@~Hy^a)|Nu|tvb@(r4g9CVI$;sKQv)GMTPa(0@H@i(WrnCpj|llzY2 zl+WzF=D-_8f1gbygynTQ?@ZtPr(XQL<%a!XYLSG&Y%s2> zVbZF$iT_Q!cye%v;N6UUu|m3f(3bnj|F^o@D{(Iy2VW<23xpxQId@H0R37-1UXK6X z7%3T?Z)?~>>{ztwDVmRXGblEhC^9A>SfxDB){Bw{1h;B{V{0%I zvFEuuw-g}$SU&uNyoSQWePdr2rYjN#mVGXZwjuLc{uaa2ymtE#%pHonm7Q=gmiv`SUL7w+@uUN;pf>i6LyMTX*`zkDBT&qS6bI}nG5073uH%4=`xIZ8yYFO*Bh67UwsD~}A zwr6IwIHcL>NHI7MAf3w{L6kv)=SW!?mI?EiZ$#}zriD}@Cvq^amGS2<{$t%(pL(DoC98|H<(942^{9w=b)x}6y$33MJq+5Il8df=&0tlfiI z`@6%I!7YOt!YUu+yXPD{jD1J4MP|Nbk?rO7i9MxdYW3eIYdqJY@8*T@!y=fNn&2g6 zNLV$Ug6`|n68@uYMme035<`0t#n_80-(D{+HKfPlVAw!9?>fq=*?n^9649%qnCE7y zPf5&-Ry&f-N)1?uOLT24i3Z8372w4Nzmz5@(7GIFZrT}9Il%8J;9^g{+>qN~x(SHn z2bfZibK7ZOxx=NoyTEHHz=e;RKuUz}dh%QYJ~Ay=HgPLCOV*6()BE~Y7S`pD8;-g2$LHCsRV@18Flb!JKU^c1_U(}-mnB3TiQ_kmEEk}utoECpl5H2~k z9#%CwTJa+K;O;Oa@>cIIxYgXB7##Wj^8M@AYt$c83Uj$o>Ey!Ywv#2EXG`6iLkhp} zCV3X$(XFpk&+#$#y{@O`;vk%@2RqH<3~Sx>wk&mpq}-Fh`lz z_S5rIBjOc_1u)F6uT?ws=%YZ=H znh`U4(TD?U`7_->vK}ItT;b)JTW=#6xie*%^75w9Uf>N} zM+YqO(^T_M1GI;kMa2?L$wGiBqa*%oR9x7w7&txlpjF>=cWO?5pT`S*X4P9uNm&C^ zuETpbUVq=HnU%!#ap0WD?abC#oE$7s*smbAaW~{#Qpe+H%b*C|BL?M)Q{ntTD;F!P zBbaMp1w>iP1uIMkhv2x{lty@3xxW)KjwY)KUY=O)$Z9a9l|x*2cjhVk-+au(B&$Ms z$4b~thd!i6H>(Z8`M~WfMe6CZxCmpjjz-9TXB+80N{+J%llkU=m>f}J!16S+GtWp9 zG;5P9)2!CZilZ$Y5)kksDo zG8?VbP|Lc1tN`0sP_|zccdEN%@fH|#?f2LWEh@U3Q4GRZ+pt(Br;TCl*DA8|;waG)^EJ8$nh)Xsv?LF?l! zG|w!aWYIcpwrdTN%~64{7bi}fc>1Bk%6YC^#Sk)qKr+@m zP@~~OhGd+kxMQW7^f_EdbaqLN3y=nw<0C95dE`nZwF923tgS`DDJJLKz{7mhm1x*; zRV7(sjoEhCe1@u<&!Sz{mxR-b3>)dP+E%;QJ6r zbwsn3w*$0PN+|&%NHn{DxoBE60Mn6xq^SM{Zw%fMPQ@EC+@xoaoDv(iC#5XTV%2E*pmfb!ntG z6O(W_XbbM`!e{k;kk&0TuSVYRzJ4p)zoY!83h!*$at8njaMZD{ZjJ9BEbr!2Hp(*r zl2+J^Q3R5wURJ&;6C_XJuvcQG4Hfl3;Wr?R;Rj-l8(LvUo_<;Atq&g?D=X&e#W27d zu_UhFz)JxGi^ShZes4UdRvO~3B9@8ntPqep+Gt#~i$6-Lp3GelIQaetPXII2r@%O8 zk7ZE;=n-|wXGLo0s13BFbrE#njVeTd*ZXDft0~O-0}*29)mMp$A_w=u79)YK027?Q zzsMmkdJ7LFDdu+T-qY2FsDL>RFiZM<7i5|GiFWfmzp08P+8sW4G<-kQt9&Ob$f&5(p~Nej;<=V?Jl8Lq2HzYmW(7G4NbYrLouO&U zRwAL%C`a@w`GhBPiLV6W+T^h1b~oqmh>&F*(b;`|vn}@9cZ1_}UKz_n&0RmrPS6NP zC-u)T)feCzw0>$k;aR3Y; z#dZ4u@AdWd{7n6a<=^J)Yiy@06%5((3 zT{AE;@>osMugky#2gJBWMMZ%RX0GZZmpQ3f{jE{Y5&M&m5 zZ>qzM5`rF$*ZX?xUO)V3V@f`I0VqGX&^td%{6)+esGgyk%Ti87c{`<^C!!{+5M~|* zY8m26^SjrhM5**^N~KrhFy)TXnwy*R^Ev&6h=vT1*V5=OX=y{hmL6^wyh-D%2d?OP z?(0HMvr-a!2oz>$*KjSTTDy&xmQ!SY)QU5X2%)+j^~La!CltN!>V+>K=S6){Y|J@D zC68&kV}5p4m>6b);R4GFGFWk8qC4cAjt_gc<0Xq_*_1@wUV3oQG&>rUiV579Dc$Ig zcBupxK~jImx)Y<{tT-VH@2;CFS^U0#Xg7k1*a0DRbr#Wj@O;jS`S*8^F=jRIsNwDh zz4ksu(80x$w3S2VUxHjct&W979Z`q)Dxt921V+z{;Wyff&h9LfMaG_42M`<>eRFTG zrEdK;Il>7?e-t6d|6IAEHw-(9$n<0e^ zF8+ai;8p-F0G@Rd?aw`<^+nCU#d#D%mo_%<&1+Sr@LD=RUvs70@F6u10rVr~n?3JPopf_^v)`qc?b(I_O}j7-(W@(A z_w*OW*9LBJ@vh3Fhz5A!+RgG77b)}Mv;Q)_GaFv43mp8-T^oPFe#2Y8pn8Ei_s~Mv zr7t1-5hPYbQc|*)=#i0ufv}OE+<#f0Q8wKJ@=~C2TkPbo)Y!~1fAVG>e6?^@=ts{4?kfR3OW{jQ#E1O$$5i`>YENY;0~4aau;%tL zzu&LngcHPns0P$ZD>>s3S4c1&){|vJ->*6CI(a^)BNse5)~4W$hd+&jb-ro?A)d>M54JwEHear3=ZU4rFg$=D4g6d<%4NQK>O7( zIx@!aZD@N7Zd!+Rd&02Nr1ssMAWSmhx3Bt)wf3j;*9(uucbbMM!?6)JcXyYXb{1fS z4w`QG;hs)lLAM)c*V-*H{xgOFv;=4!dV~OZG*r^e!b8h>$_<#WJ7F?0l-#zyG!F7d z1(tCTFM8I`37*l>UkrWOsK#_gvEjpMMM^`1xwN3lYGZv>Lt!t1Cu&IBzszJv3XD7$aP9`A2gUy74v{~CSV|on z?_YaJJv&OsiVIM4c%uCyG#iQG`f_ZU)A-uQu1Kjhwi$6k#H8sqgHNUP!axjFP=Pst|gF$yrlyhor)UF5Vw?Cd+eYsnH3SKt~CIup2(VSi&&Q|#IeKkun5 z(Rp6+q(cILT(g>B-w{8{H7Eyw0`!BRvvht(N_JTqa}&FB=g5^?ir5|x8L(0BfU#h5 zMg?ye)~#hfJI^jGm~|G&9Tc<`Fr~W=#p51nK4F6_5xIG1UtbL>ST&BPCN3@xA~#{I z{@PgY|A<|_-J9Umc}jm^ov*ZHk$K~^TUJHfe$w@abD6V14*1+&RgH&I6m_gJWXsM1 zR?tgJ0O@d0XsP6xnwXh|=z&M?URzt!uZ!^WIj98=HZnBi*Uv~(Di|tzDhVwVysKno zWof_)_@9E0zoq;~&6Kip=9K~|-!0GfqyAvW=#U|wHAwsoEP+B-UOM)Z@K9s(wU zAJc#fu)g@!tBZ4&?#&0+ABG=SHSb`DF*`yh_Xw!8X_@$zZi%#?Mc|K=3jRmW6jGy? zB?7fW57C5R=D_P8L^k9)P2t8VOQ(D`tOK%@U4VlDZn*4FAqyLuJOrIeL2Wn!SkKVN zXn1Jo_=yw#T>vErUs4h_5h4)rhacX5_f<)kEj-J9*wSm27kuT+5d$j1<67{wein?< z%9m`@?Cziag!s}IsZXf3WtD$h^x<2d0^a(Uq7h?>gB;NN;CzM2+4M6rfvX?*MyTv@ zS2t^21A~_(CF<;y5EJ<6^)msflz{cMA=?A z0}l1{)^E3k+sW(ovW8l0AkP7*6NAM$&&R&vpb-fN!F%Sp&7dIt+V9+o=I54G#9A0Pikg+#!)_(I}cck-ubf+_UM|9|41vq#L!4R${)-nWG8&Foe=mV1)hcausP zXKRmRN2n}+;?%O3WA;?(2?>>^dC?q}Knh)AKY_S9&Qy?@y^O|bBE$upTK{du9AidC zC->3!IuYCwVG%g>zf2Hgw5cP6J#W(2A+mp^kqHoZqIPBhN~Fwmv37*<0InT!z{J@2 zgXus`DPj?JKUqt9k9TvjLd~a(VJK$=3C1&17f#xKlXOkC1oh`f<2lgAV(@?wpL$QK z0Vi$ttu3o=GaB8Q%F2D23g9vpc6=UmL`TKK*xa0fwlT!b&^%zZ>q0hEIz;rDnsWk( zA`8AkdF;LuC`n@_4yTT0G|Nm7Km6UCh=B!fHVP`^P=`)J4C6@J28d@GWhSCKGt>cI zdbGz{|65oEKP(XcY2YtH`*C4i&e62r76h#_5PMl*;+l<-5#5o){ z6p%+c$%m!)5SZ*z1Nu4#u@Ey&fgJKH@ShTNNvA&p_j<9^_F2Gx1=z(welWe2VNL$& zZ<6960z{ERJH$E88(T%8TNx+xlP;K8Nx zvHk6f9D5;L{55c4sM1>iuBe25w=}Q{;oeDb45;INS*tXe+2?(|QB~DkUO?ey zZwjI^XfMA)V>tc>E`QQlj|Q03GPEob*RAIS?g8}K1wi(I9@iyx`WU(t{)B78r*(K$gz7NCq^&QG&JPw}IHfr0M}Z5Co;Vv+N%5tGnp$eyw^P5&Pm zl7U`i-Z)1&QX*mYlKW;MNf&m+$+`8(xXSqkGUP&ZRSDC3X-HC!quq$#@Y5qb9x(87 zSLG@$Ms9P#N2w2=L%6qiY`Qj9x-zMoBiKHz_&+QtxN^u!6rA6+Ela*^Vr8~z&`tjP3OW@L9F~G75UM;(hid5N2b3$A1G!hU2<}^%80I{m z@uknnp3ymIt2u*{9bpXH0h#3jM-{DOR^)4D0|)Q#=G@l&BhL>10xA{RP}c|q{9Ja* z7}84vIG{jRKLqm=-OekiCd5Sx1lf^Qh6aF)J?rLBEC97ZR^= zb8tdrr3!zwKJWjNU;Sj~(4= zQB2Hf4t_cr85xuWxRz2{oC#?fdhR@w4x7orh*Ew4&o}z0fFTrK*&JOUU$EeEXt8hl zWWGc8Vi3}W_%`0<8QaQMt|_!X7mZK~`T9f?Hk&Wh^%dQ$@H4lFH_IJo8%FRc%x3A8 zA{|y9-qTA?Pqw+n20T{?jYF<-szid!a$kUJck8AI-7{LdZ`6G*I?7%H>Oq9RZo2?n zHXN9UBf#jf7@?JC1$bw9VtRUrP4#Xt-~b7bVe}F(0bccikVgQ49q(`OJiiw8$p|XG zEd5Au`bL=;q9cQfQYy{7U_2HA!pG(7CYma?sQ$jKQ3p2(#61g5Qwb5ZYby&tSoZF= zvq8<)VWyV74+#_3%TfYjml$ID4&rLG@QT%1)M?m5+HI_w1*{h(jNYzvdAW8k3?(kq zn<}Qmeg#a7lC!|d`AtKgM32UuOXFruYB8^_+vy$FY3S0{S7O*YOP z{Hw%BCdr7I{*AxKApRDMZNJf}uj>3wS&8j!A{dFEJ+$84n*$Bn;Ha+*wB7ST5+ec= zAo#;v;j8!C&_>6Txx7m?e?#1DXMp?LY>!@--qb_%w-wXO<%0)nj-zO$e^tEaN9(!Qs4X_!05t18%Ojvs>rfxpP|! z5bCHeCRY4mu+sSnV-=p=OWkDmB!jFV+T?7Bq2w zOh@1xdtkZ?-le4PO~`;!+87EhiVFlMkZF8?JH?7F9hgnb9GO!E$r;NPm`3d@(1G+!-ob)V9Nn;T(ljBgXmoQp~HB1 z)VAr<#cc*9XGPu<3Ow9@2$y3O)N?-(#MZ=34CVK0T( z!j;3mBfUOXLuGL;aJO{Vabmgj2q@OKYPg|+ZE6=2)kybs0Y(khvdCISAXrL;Hyt!$ zhhm?FCiVcpp!P+(FH&bs_?i4qp=3I%`y`^%_4)H>0An2;bIH#zE4rbw0+0mI>;p^y zx_0B%+HA14dT|uW`Nmyd1}Kv2a>XEb846t;TtqmIrp*$?wYL*o4-A$xTkOm^PEDUq z72{Biq55MoKA$#1pfX)tgM@g#8AMBt*io}-HU z1^)H2ZVit#;b^K(J0Ay_;97KJ7lF=x_-uQBHyKZV?oFcB{nvB?$9IwTM41N+I-YhG z?AkgocwoMQ7Coyy2u2J@j_HLm9s7ym1?YNC0im`&IJX-bkW8tcD(hcmu9aWiQnnyv zyNwntQ*u-(9WkxDfNB?Ok7rT9?vP~CqWHQQDu<2mME+1gB`6g$J5t05sl5R3ru#U2LjFkk$?r_!XIH`u@bWt zt=+%D=qnjt);gQ)Zw5h?{=<*vW+>V$*I#!S@a?P|?Lh@cfLI>0ku~bkk1OGhJpd6@ za_ehB;@`Fp4k0W&#)sAo^-8+y#sse;2MM}jHWc9?gA%(n3-Qu;M;&e<>*BJ3rCBEJ ziyH`+evTS$_2_arA#A$Zf~?@(`}$y(@A|#xH%%SQzFStpU+0&X5BO~tToQ>vM~Gf6 zDxHS|R#GcX2u?Ouv)QGtF|8z$;GfQla+%QtAxi9g1BHusZga9yR|I$gkv zzn$&{xQ4v)jJOfbF;^&bWI`51a4(QyTXH)3CYjCo3n=U86J+bA`s|D-q=h~U5k9I> zDWS^W(ji70N;r==aY!&@tHoeFBAde*$4YUxo!Qs2*92hO1u_mn6pS}ojhIpL7W&QQ zz2g5npG}nv^Qzbi5@=P?c`q86cUFl+F(X<=HpN3zAst$DNN*TODz=GyjVfELVztKx z@e=D|26_Xrcv4gg+5^At8(AGtwCP<9u~CYomgZ=c-$usPq25$*nf58{VP z?o^`~Q4SGM@rhT~W77{5?KcdG87@Y=u#Vynb)bh4Yh1D0XPM(Lk%*QZ%gi1=W7HbxUMzk%9 z%{mK!rd)^R^5x4=BOlOOGl|6jCkF#+(6jzQ3c7ycww=^W8_kC)52@GrT)`uM>)YdE z1bzi@?8lk06&BWDRrDaNZ5ixaV{nw~=wg9?N%SFvQiAMXM;Ls^gHPe^`n(TR8d!hwGtw8&h;4I}S=*(2!Kc z-lAZ&HL znz7Et@)BxpJMGumf|T2B+HMdgXX3a9#sBm>Z>$5v*;_saDR&S6Bv1~Oa%Z{N^;rI1e=1vFyFXy#1-(iLtT0QrSSJN(G06K!!Lw`UK zo0V^&#t799R|3htPCZ8YZ{?5chwbhyw(G<{j}kHFB6Tpahiu2+y&U3b{5=nLtu*|n zIq-Y6j@3C)%nUIk9W?}*AeOYFO1@ybx9l~I&ObkMcIZ9wc$Boa zhx}zcfYl3)rrgZsTmZHQuKP|Uva21L<0L-_R8pI9F5u>&y}cg4PltG&{lxziBv+ea z3#{fJ!*-wx!Y%$zU+t%Qc-9z|9^k|t88M>{*%2CE-UJd9CfrM;j>bVV35s&IaNv3& zfsyhI55AUt#SWK7z{CM?%zj;n^jUyc12#rI0}zY+*$|~i*f4ZzXBZ+!h55o^;!syW zWC(&2-Spq$k&!1o}L)r>0Q7BMBUfC{;CfY|ruuj{> zvLglePxgnKRr|4oeb9sEsxSfAdkJ_FbnC5K=u{#du!P)EIKgYsx`mrd&Zu_rN9>eU z8gSW`^;m-micR-QTvuH+K1=P4MU)0aohH)C{)ZSG=$}1^Fj_drJ;f*sSK7 z3Dx&M4dVh$daKy{fs{u(3oopm(m?auFQvWfd*3&iXnBhb8e8a_+%6n)@Q3F>m^=xr z$_9;Z;+~8KFv5?+|08A`bKjw(L75C3MXz7z@Ir6vAp*^VKUGEui3lEcs&?}*u~pdi zVLpTg5r;ILjFgmL(=J6+HvFOQ&-ig+mw;3gp#*f`L3t;C1IuK9ymGTaGz(lX5N;G>u<9CxEa@W8b3Os zg=p>U-1viSq8GmWWu<3W^|T$FsRf*=;_IK44ROwp?ThHMpM>dQh!PwvQ=h6C#;O2m z10$qh#smUf$XEY*V-v|%Y2ixi_s18cDy`CZ0D&>%8aks@fhzhKH>ufQ(u(h{?K&+T z1!k^me4@2=u>|eJeEG#TMDY|;$Oydtv-!MMIW!G0e7oCogV9-jKP)O{y%zN! z%w0lX5;&%)TNKc)GY+#kW=Z`M*ZX!y zxvbkQm1}N6-o8(%rD||V5G$g1?vD~?DN%9XkA9mU4=x_`zMz?~Rmp>H2JTA|__ml2 z)g@MLhXmi;Jk%ZaFI6cZn<`4YBL_7d%h z{k_{%<@QjR7yS;+#!=2_pdBHqFsO5K6H>=K^(tKtIpsEI(Xxy&BjGMGh8eEK{|E4OxP#ixjVd zlj&^AXm(IxfI>0!zj)c}bFL9El?DSJKYCgM_PA|d;fj;ji*3>{7!bgQFo^!eCJc!N zLC-1!=SBhN=HBp!IdDq8R!xIDmY`GbGkP3j_c!3CDf=eMdTX)*k$<{r1G-mVHa0d| zZmc_TK@E?kuc)E{_a;)}qT?Kwa3*_vhUHG7UkfZrGX5%n z#6r6=&FwU_DH-tHwRx<1A9oidBqWrStb4O}`M%f#FSVhJYkdSqV^*AJTqwP`=X zn`0o+0qs!GJ$Qrc(8RmPL_9n^)1PpbV1_~wSTDTHKkD7){4D6~{g9GkQy@nBPZH0U zW2ZI;IZI8At?mWee4aml4n#iY)#E(q3jvy_F$c+M5vnoHC%GZTN0FVES5x^A0^bi#S&P`E6}*Xy}tSolrdMVQvtQ z-2kc7W6$`{W7dXs64SR}6v5yB9x)&<68E`0w}@ZsWpv63q5d)G4O=Iv`(y0Qk7Bbf zAFw&Xhigc^weK&4g`G@$snOQ;mzKjY8^JKuf6}aI%3b+vZJVI;LtxCLlNHi~dvsZx z*5+E_{Bh9pzxXH1Z_3xJvM%UH<6vrHB6qY!YH!9-i26U&gWJfGYR@c;P8wqcI|sv#;t^3Q8V8r2CA%DdwctAo5%8e5;{X3U#9n?2Y@z|;CF2DFH2Bu`<8<$ z>w`R0<}JZMj>2$8N?qjDO^p~f7L7{sL*}$ zm_Ra7cqJAj6L0~L!dMUCeI48sUntOP^-xt9AtKF)&BCBS+3dzHgH!`e!iruy=B22L zw|+zFidJ4nUmm?Ps>Br~yH*MvC8cbPs7^F>>^>1t#(KVmNrgX2y-sJ$i8~DZ+hT5b ztn&eEJ|4=<*Ju&mzAMbF-b(7*Ij zizV`k`86W~$@wedNxnz%Su@T+oTuss?3UFG3g#TN@uE8OPqHQR%^#h+y+{~LIQ=Hk z2yKN^2J#f;zFoy3v~r6-tE{NN3flX!ZYjeHxBti9dxvBBhyTMjqNFGlDMcZbne1K4 z9%Y6?WzRzPtZ3L|uZ9wmO)^vV%*ZC$d++s}*L@?Y&+q&F=Xs9fIiAOV9o<~_bzSfG zd7kg{e2tRRaJcoUJ|4R~KQ0lUR5}KYe_z1%d&8r~^mUqHfjM1)00##gs6UX!ZBP0H zR=Oq*z0)Q_Wf?@}q7I>s7VRip8lgOW}kgjFjsIAF|khwo$^Egr7VV z?fGG$R#npkdDwb+I&cu6hU|j!g`mdSG0=$+I1t2{hPb(@SaLrA=dYeB*{yN&p95)1 z*wAhZWcin)?!GjGZb8u=DGb~Vg%ZV@eIzpCT4RxQOMgFoG62V1q9BvuLiY+qfN;lt z6E^iJ94V=fQ&w!^#bPLwfIy|`v!iy^5NDM~DoN;tBg75V0j3i;;V|xgw(lTd*A-xf zhLSBZ&Jf|Z)huKkDETNXdc=1L=>%DAc7mQF6d01YfAs|0ZZIfsPTz5yK0wK`y^2lw zY1<*{1MTpz&X^636^imw*>3K%LoaP@Y@$dYborxS_rmQJy&9iYcklBRBA!anYOR4o zE2{iH^qRJJjHv~b`sZwN2%iX{+b15dkO}uJ*zZJh1+_@A3p)G(YRET~<<}<7!kIa4 zl6?xCPEqH6AZ({y$;R#6F)Xnq4shFjRK;}A4b??fR4?d6KwWG3!v=w=pdl4%xgXf# zx7wdjDsP`x#o9-4n70200in!97lUq%2)hG-0-E9|?>k^klSFh1p&Y3QJs;70EPtEd zG`j#QrzjG^qfn~dP=>A{w20849AwVS#k-w?p$KfvOn_SdF^mRqj^wi{qd#CkPFztI zFQ?a5mwkLLb;}5Vfh-q{gD?72pBf^P)e`jyM;>xCOic-cOY32pm4ntlOOm zM^RQ={W-Yv=_qlK;cetDFM*~g`Us_Hm`#Q&<3lk2wF_&knG zMu6n2Cp0hn^sn{kH*(C@yM!f@{Xe&BB*WgP$zayuF-JfxI}d|c$3xvnJWOa_qGzK z2>%l;iD1&VVO}-9{7uBJh(_1}!5DUcU@j7?g+a~d&(KvFXGXVufVdCX{eKm_Ma)15 zxIc-;3pq`e9z}O95#!$yEqH(DtqpmT9bw#nY@iEB)}_NRVYV+CAZ5s&+oVHkxa`<5 zx5ZNrZf;aKTt)47a{lkD=pGvAk91VicV77l86a8u&zef;`MWDT;nHyp=bg>6A>jcB zQq%+5qko~GYK@_OO?shsNcWcxrE1Ahbj$1So5J-LA~~ZVxCYZZ5c7m|{+prKz>nq; zM*s~CUe^(t6J`lmsPj9w0z|{w8LmrGIPbqaj{Yig>wSQlw3%o`DJ8Vlwp)65gFyb$?b_K1hroe zMof;#4OZ=_iVz$=j3M4r>;vfmKndJLZ@8nfJI2}`TA{j2g1_xriF* z&VAk@n}bG;z=0j3!zp(Z>MOy5{dN#j!HWScn+;AOW-6uVgM~iSBZk4=<)WaBc^N;Jq3K(>DTR{=Sb z4Lo^Mg!Jhxv9h%dIjdh0pbo;JBSv|z{)ug*^7%Hho%D-J4%p|7G$i4? z6U@psHn>=J+dAV;cL;V}$a$S>Lq6DFz84c2%TSC2HU|9|Nf!th;`NRwcXuOmJ%Sb| zDLDcM3R8V4OpD5t6uy*G+3;+YKVM{SO>)*dfdIbYvMQDF-nr{gUCRKi+w>nvPY%}e zo^6ehX35P^<2*wk^2%|cQSM3EUV_u>;stv~WlwQVErhlbO894*hxcjp3t@*xlpAs- zuyD1Q<-}%N*<`UXAivBr5P{fGn8D!^+I+%h3N?-Gck(7=p8V>wuhOoJTb8l2 z>hg2UYM!gix1wLcUuqAxY&BLhT9_E`cXSP;^R&~@OXN|>HMbygFTYqJPIk}1CavpD ztdLpn!*>r0Sj-wkl=w&IdNmHPaWzeOWks)6j^y#<&xkKgS5nzpjCU-Gcj(n*p4wA2qweSp^@8%I0!$DT# zvjmpm%LFtE(yk7PQ$0(kUsYT+)rV1O2^SKNOb{RtLSjXu=D8Up|11O^L7qUNG`T>j z5=gJWI|dmOBp35EZVt>(jOYcjqqr^!7h@9g^cSm0`}L87Q@n24B2v~Zp2jbve(`3_ zJ~Svb^N(&LO)r~%zqd0vYnsmCgdSH^Xt)G+`1xmbM$=Tzif{W_RLkO+cOM(1Y9z(i zV)T^*R-9lJQZ-nYY#R-qe|~zEWc;r(!oiN~BoH|NAxanScKH#YVKF>0@Eb74WLa(j z-qFm77DKX`9|U7AxFvEzmE+mW<&s1F9jp3-od-^W=4bSUBZg@+yp<`(^Ig|8t-1Kq zsGOz!YedU$w1)y+c>4_>_t0thp-1}S9zPdtb<6j zY?P@V7_{Rs^DvZm?@sp&Bfg92VJXV@#LAc6?eRu<9a$T^j!ZzwWAz-_JgIDGm8o9O zpzsU^tl%6PD+pkLd7FhiZdA|1otz6u&%L!T2DUf7GD-$+pq((xJAzC(vPmcoN~vC% z;hD`@eXyFNrXY}DN+{t~7>uaHvQLr6&G9&td|j{d5sW6npV@~)QzIBsq9ukM?gq6& zE3zLW01JuHZB!dd)?vWN@E|I-0tV$!X6jP_q}#g}BYA}(IgREvoCa(SfKSn5owo?W z?0XX0re$E|0+)Fmd7kuG>chrkB)rQ_trk5r&4$8X2t@cPdN8DLvXi?fCQJ4oLZtS5t(f}N9NJ25H=q$_7+7LC2;z)&?gKR7|X_ zFWw=x1h!A4)};M0(d}%Zr&ZT`auQywWbK||-TowL@XZ-dI}3kYx@B*@G2AVo*_tLq zVu9A;20lf{2vF{@77;3_Vc(xawZa9t*U&on)!l#MTeV=7-{u?6eqCyxM&g4dFlf-^ z$LY~Bll)fazWR!XR+n38p4t9ax0bvGt0|p@*L=!-cX5;Y2vX77kF9VRt=NtE<@rOM z<^c2}=urumM?AntJ$@cR!|c(*QWJTUs1n?N_4^;;7guYwKnyxztc=^kzxAvxUNlfQ zIC4^->;>U;Fj$=YINvPU?JpPpruOcs7YT7{?!4s%ji_qAKi(zrz)es zXJ>e_I9$8a9p{gn$QG&3v;8=u{^$p32U!)y-!{jhzEX( zMo#y=CGr|)q6=;geORDmk+(V~qSX4?@u<3?6^A`_p6QK__@+;c7ka~wQ7qi8bBPr) z$(B7D?3NIb)i{@@+F2j2nU>sSy)LC(+A^Uylyv@Ft7Cy_PF+gkz5BVlxLNrZqN|Uu zEe@EZSB52&NN|qX(e@j}D6Z_O7OmC_a8-InzV2wV>>saC9G56ZLd>rD4Pw`6DxcT_9-PN@E5$UkR2&6h2y|5kiN$*M@T+S1qXh%Z{a z9I}$qq?rBdiR5jrqHRZQWFnOO1E$8%#z$}(py(WHzv8TAHw&T#`~Di1TAqQqMR8$u zpaac*)YR{|7a7C9Jj`@O+M~#VqS%sh!hkn zcqrAz6oFE7P;5B_;cQ}!d|y7A|i^`cbX zOf-5Ho2|clHCdd&@Fcyc`2Hj~VSFAu7G7qKeS&;w@#|m)yYR*K=6E%wH5rb))A+|v2FWtcx-!TnmZ;I}IZ?}{7;)>g`5GSoecZ?c$RGki(0c~yDy zNXsspg6`}Ii_(8LO3=4#x$^AC<5beL9FEb)-{k-Gw!5s@H{TvX5vnZ|cUzR=4IdV7#y^_S-n*fELTe3k6X81E8;R^y ztP0it99b0_CP~=NT7BdcN%oH#4#ydm=^=t!OFRpaRRnnq&#np>q@PS)`O-YCvu-s| z9CP2%(Q8_}-ADG8^h#awAy245tX-8QaYZOB(^++2C!7_ldiM|~@?<-wi03SX=pUaP z^m}-UlRR2(#b?enRD4_;ESHIWeQ*R+#qF*wimyyZGdU|dX3Y&f4?JzFa_!S7he&KJ zQ_~1oUX6!X7Wa`AA6{uVSnOEAKFz~AHoV$5$0pHWBB3RnqDbGn-$P)Hch1zz)}s4a zYi_Vo@X>{-n}uP|X5Al3QM8>9GyND{=_4rPIoH$~JWOA#oHG_L(Mg^4+FWsHPxTqT zKxJ`tEpRSbPH~mnP0Z4sNjy-q@Upzw4kXu2^dI&I>0+N)sm@|Ep(a|1o}U|tcEf+w zAZNWiGZtC;!@%Kw>Z=?b4OULJF|&1<$2+VG1RZOUTWx%I`jR* zV8~~p0FF2J>%+3R+1B0j9B{G z+rnTwV_DEaQ(@-uLi~xi%-rsWUV(EzIrfoy8dJZ1kVoMnXwsm z1%>s6`4Fxj7vj`DEO9T!f7O z>!~}QySTen3PYt|MhmZhz2B4Yx>!D^c1?SpshecDQ8d@xS#gEq`j1?_MQu*wuQ7t1 zmFxU|b+i}PXN;pf!*&r5kkjl2-iZ@EBE4wpv?-OBW!fultW&M>?^B||4y91B!50I+ z)#gwoXTf~&aMqy|#V;})t1Q;;9A-p=3Ia^4SMRu{#xB&a_E`E6BBoD0WF)4Y1JUc> zCv41zx>goDzX_VXW!!td=|=MhqF4T71bOkC^2!RcUgIG;<*yp$xgrTuSBy$$=mJhI zzVA|xJEU}KtvN=z-BWVE^7Y8F}I4Jfv%2ZpLJnPWo)c;Xol6$0GN9^Trf}%*C=CwrQc^@JsyI;D-2AON6 zE-4~(Y7tap^f#6Ra!euyQ|N3Tcf%=BqASjwJ5F5EKwR8%yyI4ECf%Gn*PWJ_uHuiy zkKUBM?{8(&3GTf)Ddtrtd(t5xC|5fq>u!c7q34Xffm|s7AbWmI1cJKDwJ`^;LvazyF3`g$f31`rnGM8 zPD4SZ@-JB|rK&$CN#5XrxOk4W*1|48>}gyj#}nVH*`2o*w5at}W`E>m@<)!6IZSmZ zm70c6o|Mr0)VZRU!QZTs)!D(pC}kExvsXpIV$HGATK&f}fmX*ikx#Pv252p&30mIT zM7!FKtP4o>ERMXBoBw6WKcL6;Sz1Nv9chKgj{&jQ8W$$ocGLYJemjAg%C`ziBR z>iZZ9m5y_S^I=ItX%d}Pnew(HJwv>zQx>b?&dp42F+Q*ejye|Yby({Qtt&PBQgqjo zYuuB!qw(6oyylR`V{K-m!RJpotIgIGL`n;MZMmi3xZtNwab93{e#IeO)hVtom_M|P zDlDR$-iUO%XW)_JxjNbmf~R^yj>*{{mX((4e2Hh<-$QO(X0UgD{FUR#O2=gS+VDY| zaR;{XlC=Sj2Ca*s@%(Kpje*(`b2qJ|DBKckwARN@ADX{9D>a`zt|5Aaiw>*-Vqx&< z8IBoz3MwMShw=tB-vp@ne+@pT?WHJC8ixOBy3Qwqh|{eN{eLGcZ^3>#L>StIx z;g{>m_l)$lUVCR}m=^8>kH?2H^w%q?4|QtaDzr{NH+Auyui~sSQkLsh3vrg}zGD|7+7~?S$5iv^_yWQw1Dg?Z1M0=k9{NMJ%flg- zpBkR12uIO;fANcV@wK`8d2m#uUc_~p@$tKi9sUNg3BqF%oHO07Pry>$ST!y6SAP@8 zCa`C=PhYbYtqnmvFM690I8=tzHP+?p4OuBs26YCM)h9!APm?GM+NMs|)A{U;S1{pn z2%0DBtJF51oU=2aH>e=NJu;FyQ!%ODsq9xu>NwvZI(1L{Zdsj$`5VLc;v_Of68rMx z!UPS7(-s>gBH8a}3`H-QbBYm?4kcyN=X{HuqMMFaUhRIdT%b63QXw&IXspIAlu!Sx zTcl;^%s{1Sgx}30*Uj1pS2B%A*Y5sEco#jfCmMp%d?C5o3!bXW!~@@&=R%GU^m$v| z53y+79d%#v^L&O*hT1(Ed*}5}iBgs^hOwqq;mTAwJz=o8KwC?b4QUm<-RAMZo6t&B zj6k|<(U30SgWB@Nki*Gw`J_9yu=Y zL5HL!f~Nj@WiUx`#O>Mg00Ej?s^=FT-OW4W$bRHS>+U{$YPNT6`x6Fhiyo4C+VP8! z%rxbyHgXMKxt9|Y%@+`Vk?a??BB__yO-ar){F%g*q5i4M1eHbFS?UWZ#Vy2*T%L$8 zj`W>NdDmW!J{@Dsk=VyXH-ckk91tX!0EU!y=Q8xg2YKW3R8#cjq~_@jw4%q*&uYDp zoIFuU)G^6>myMXJl-=w{MoQ`2Cw@e+>b1G}@y$k#^inAjVU+^wtOijYVIZkNK8sK~ zk}hH>9f*7bmgb4=jj4`I(yKze|L*^UZz4d&=&gQiBeaod{4oK}XBK@q-Q?F4(xhTm z(H<7uy)b=VXJ)*aUO7SElk7EBB*n82W2%T#4b3eLWr5Ip9p6+$C^TeL(>f==ez-4p z8T;16@h3W6Nj~i6@-B_E>hB*1s$J8~kWcDPP?>Fw;#lzvzNg+q6I#)v(#|Ksn3=kZ z`vSRIjrKGAnQEB_cu^NF6}VM!yk*ly3_jO_4O#D|Kk_a>N6%GKvtx&J?O*d&pOoe^ zKI`XjaQaM5N<5agxt3LwUl=hwAy91VZGwhHE0V27er-a-KcZDEH2>Lh3dgV*JiRdL zKceHAd}ypkWT)85zWg9~9Lm4$q~A_y%Tx3lEMj=MI%B(>a7bvRnWQ;_{Ij0tl_oQ$ z83y72Ej4*!cB7%)#i~o~AqLvhQ+G60M^n{Jtg`bP3KRwDoBTc2qU5Fnz@ zpYLoT`c`eIkaMkTK`lZ-M-SgzrJ(%rE}J#M3;N=VLwxO(&vghA<*XmqnFVY8yd!-V zJEw&2W*P3qX6qrH797s;%blVWTvQPEfpw$&99a!MzX5pVW;^jz8W+^R??xS!+yMjt z#o@jX;&`tOD;+!(9?WnWFL&N*B8d!ooMRrvuvT7v(JXwn&Id8W3_tL)u+q;nXep&T zUi5iJUmBieEsC2{!;#J*nuM6CFx2!`Q><#U^~d8!nP~B(OU{rc#p`o1<=mhGU*oCs z7Qxr6<6Gh4M&oM;eCSUXs z-)Jo!stE47Q(IkF)zPgj_@zw%ZYt8+*_)v^IG)7|t=ZX=dHo~GGHhOjvNnGco*9X6-N zLJyzgEr1y!i|K{q?9S5J8huN9-4qMTCbn)85Ur(flW3KAW)RW9)7XY0mfgT1mPep* zh|o+|tHxey-H&1hxWLAE0;8{6AMxr7a5}-iN2o^(g)*Jtj7q&!kPv-I%jeliU7^C)#^THQ z>^|9rd%r#&V|OM@J@Nh98-kMB0BW5>1JPc`)dENFnDYdGMsL=Oq$&3g4GbPhd#heQ zqJH~(ZD+%BGnJ89M=<#yO*Ha8VVe%aHbr%tZQ4?4R_pZbk#F)tOiPk6B2PX@L=bFcUuJo+M z{Q4GMV@|kQI%TSPwKdbY=z|aE{Jl$)z8&;3cViQYsLx23KXpE&cDJhZONwfd1XBRx zap~{%6=6~@qtE1Wzgkv#>_97LiQa>Kci4@wSG#K}2ja)?LWb$C<>qOxB`_%0vHyDI zObOR1=c?;1yOtLFBLh|P^PvWC@&P4%L~LFwsgtny`9C{$^uLASXlgng6Q$6aC*^o3 z4Gl>8CVhmJlk5%V0^zcuI`25EGo9utsP*C>8jre+BndmrbxU_+R4rC37j z<)OM7f0>tjp3fJof4EA$oZaoe%jQ1*jF;RP!K8%zuqBHN(sxwWX4J-^tx3tg@X;n| zXgI3dXwve)Q&ll+vNNpdg@cy)i9QN0)6^!AS zujopfy`3Ao!`j*-BaHDFh^Qm?B1&De2du0;E3SDW{;=0aPuzRJ@#IIykb#v{USHY} zX+yg+g(LYW@2q&w38Uwqr9Tf@aL|j;&X4OXU?>@wzS}QI%f*?2KSzppDt<;rQ>3tuE~_}yaljJSL9Uc zj=+a7ib{`s{@q8XpLzf0&GMBPMu(59j;jK9%-#U*6FoA)%GVHmIoa(+#qdP?*-HY4 zsj)!kV7AW$h_TfA6|nMbRDU{Cw?~J$EO2~{nz)cmOGU7;G-Ie!Jqai~HSA z4#H|Xzwd#RT~72Pq~ao0nN#@YE4Q5GtTtDfDRz8r{C>x~)@JcDo@cGPjEd*KD0g~1 z6lk-fojY?Qagri$=IzhOqXFX5`|!=jeD6YA#OV~`jPYi26J`bjArMj7iyl+uQpcGE z0wXN@Lff!%{(2RNf=&a=7rs>Eb}{v`5zob@hNjdMz1t@xmr78nSVDd!)%<`*9A@Geqj zwJl4XVM$onIi0GytnXLzT_fjj>bD_w+|kXLFp6gAeoK1plw-SmZK4eiFvD>pd%nZAANw-GH^?i*WnXv+lSIU-%eKos~7p`KdNSJ@Dovjm|B9cwrldM6JBu?KER8vS1iLj);HT=J zpZFC2?oRkzJ2a8##%~yRj9RY*e)&Ry*ESzYKc1a@hFCyAb!x6xnj*uIJ#3;m_{@e0 zPU2AnpR^$Q^0qt-vhq3q)X}kkqzD3>h+?*1%d5CRlS*u-S1#TCh^{`QhbWAtg9S*2 zBl7H+={BgEOg(pZ!%W$zMoL%Z3R5a>dp>VF#GT1?M%K**?%e2`a+(CbQvRQvGw&Gp350!s-((MN~6Qy8y=O`IoK zcBwt6_R$yzQ2?$|mno zP}=W4X7H(d#oZ#phQ>5kJ1OSf@?dsmS-oFXU19R2hq#Pzx26MqCWi_puDaZ=JChZs z?rTKlFcr{Uow7<;Uh?)ba*_|7&q3Nzw6(kmy;ttIu}b7FxDAap6E7~F>r`u+E+~%< ze-@YAZ;TzE7e<^%?6a9C&C;oB1f)IIzRRv%^c!?hB3w0lv_u>K#WeQ(yQNx+mON|T zbN36yHF3bYVW-r#8XO<(odMtH0$Yi5f+Wql6;;wCTt22>+}$NnlW4^coOImcaP2W z+d=1%YRy{%CY`sfI#aV$t~cAtG5drOlbM7W`#y-ij7Imp7e45X4oBy$I9yWA zx9W-VgO?AiuqV};vW@{N`A3mMcF|^njo>jp#nFbbX#7xh3v`(=_?sD&pevAtVTQ7Q z`R#4~onU%Ue007^(|rdaeo@yv4bdXjyzwQ%H5b=4?-0_F>M?#lNs_zEvZ-F(IQN93 z+^vSw1(Runr)k3> z5BmsyEkW)QZ!-yJ11&tcpSS0qMN+Ln+jU?fAZ}T{oP@y@+ceV+MCRjxS4^n{VI7S; zV7(JD!!{yT;YFZkFra#Gf~BExLgkkn{T=+Vr{@Z-r>-`0zUygEAmLTlEPHS}&O)k3 znd<9Pt-|~isYZ4L4pNb)o2jqNlp~sw;G>o}_$Z5tAoq*K#AKzVc)dqoSW9Re}p?r^D3C+AdV<{ zb)B68N}KswC>KDAVC(=|J&HgffYThX+bL%^mCf8dBJchaJ^<()GExl4sK~u}syov- zlJ`lB08hu#Gs9zP?`b|2%15rwl4>r$&+NZ2?2?<6Xw*k4mV0z+>dsH3Hz?+QqA}Ov zl}<;tzWzX8>k?Q0kvV6Vyt&hrxxzMITBbgm2$LT0!CMaXT5FiObW7oT4F{t?cV@6) zu}C@6A+DG_AH#z#`|n*i&g~pAm3p%E=;G<};r(9tFH=+(c%}V{OSll|p63R1k7X1r zyg>@L_r^*E)GW2Y=G@fqU{BT=0O1HAZETRQ=1rI13nad`#zi?@wf*fGRt`i4EgNz3 z%h)IA^yy%klT&Uk^BU0#`~GbEshh;c`Pof0pRV>8f0{ac=VM{ytZTS}K%kVo!F+4K zxMVa$INS-A6bt)a@m}lf#qDxKyoXvf#y)^2uNPrPAtLUrn};(P!d;PmYJ7X}4OO$@H%N zMAyts?ZLbtobYJYb@}L3^YzO07e!oBpO{8`$L?U^&uI3?L>7*B^D0GWjFw2$)`8O_ z)%p)U&fKTtzw$MDBz-YSp$-&v*+~TA5MW_c$m_EJmq6x1PP-ugZMl+c!HyDN|G~ah z#sABje!nKu=Bja3oY-CLVDvcs#b7y7*j#XpS9n=Zxa{9t>0#zLi|epUZAu0eUV z{oqM)h1%KF*5p|(>vQ1`rIzCocpfnEvGF%ezg?}*CFZ(}%alF3;4s!5={*$^VLKnU zs5BzpW_T%Dqh49zbbBGe5zq0rNZ$kvGQ)ae{mb?VSBL=V2m73^tR4y3jfT^FVVD&$ z=~SiDw|F()T6Ss=v*3lHTkXN;7CgRQoF_dYtU@@b^B{o{@lK{%zoww@pzMyq`bU>r zCW}KKFTQT&V;*IC^oC%e1GiS9>J-$Sr=4A~5Y)vj#E49A@1F`{E})nKFmM{>@GPLr zAxBlB^m!&=ww?lW~K&=21rYbmoOp zm(;{vPh6Z0toCwgq=}epi;E6_W2U1b_a>Z8D~WSNr#C?0%;1NR(T)rO+hy(A8tyOk zCE>ABEv?eUA7D_;V;~XTdgxiyyN7RZ_|zR_71VfFCE{BZm+}TLGRC_|QL4R&iSA^} z=C@aG&5S>HuKmO$2aQ20U;iyBmGxN{J+1v-{d_NkI1aI^mSit}2@rCrz4gJNc`7%sm^iyAFoU3f zqRb>?-s+6ukDe5{T3Z(+56Fu4@vzTR*3~uQ>xYVnOM~G=c?-DMrk-D;NYLtLU{9q9 zaWTekLp<%qzJGT666H5J)|T?(s%8I{SxI)@_Z(B<35gsUSw(f2Dsbta1GS>ED9IG-}OwX>T((VnW;5c@zxV7hYF#BeC7yYX`ZC;b4}U4DQnQz?$S0w8k08g&FPP@yGa#8!P%3R?{(@ZMH01(~`+E zu^R%=fVyLKduV{A+Ow*&3}+Wdar+&sE-W*14q4aRM>ns1ckIkD9#alADEG9}G|8N< zoca8jKtsmZa6qHrhIGH7Kb00=5aSwNjPh|}s z&X&Kvy4bC3si)tz9_Z{`%vaUtXm9D+nWNDj;h4IXl@e{D@pE?`Z>jvfdLHJVW4Y5Y zv+dz23_m}>IDGD7T66QXmBvpd2mEIZfuS9-&1&{%Ek1U~sOHR<_RmLW9Gmbgia&PZ z*th}Lk>JGRIuC9Sn#9qs&sV0elc|X{TW5?CDWx_Rg{YIKRVSfl!xYE%3?l>2=C~_N zK~?F;N!RXIe^##UnqiYu)64wvBPOD#>G_W@-@i<_Xxl5zD;`it?DH35BYf~CH`p<; z?s#Tirv0pZp@dm>eF}}qs$b5iv#Mj7Wau&bnu)o0TQF zmz@lGB7YKvD|zOG7}65_2v~jeMu(H!qHr!sqq0c-$=A$P3@wU=KWILAA3BOZSpc#pU?#md5$>dq+a3K&tuAQQmt~*`W3NXn{^$R75Sf_zotD`?7GiT|brV%p#v4^|sY9AGy zhtewdXTz^4s!X3u&jwfXccm~(oumS^=hr>Rnd7-tj@py2h}KzX<&Vxzrd6jIj5FhJ z)^OAuqAGrSQ;qaEE&6TlpgJHv7mvIzZJe=DsML24BY4<)1wz+w(k=3GOm4=KGO%`7 zu7*E|tB-ybPvhkt#P5F_NFt)4f*h=@q72@`2s-!3;j3Ve3$9*+yEWzijJsNTXHh{k zpvYKUv;HJ!hZHSFYM158<_^R{1zlhp;$0mw>k?3R$+O^9>uBr6pLe-6bjU|gM{&Ur zsl~5?^t^ORY1A5hn&2^HP5Zyy95j4T9DcN$D2hUZ`tPnoV#o<6-Xq&RA;?0}N`)i8 zwV2T~KEamK%_M+AUB8sE8edRC!6b)3*voJ7^1!LuDow9JqJfAc*PBBzEbJQbK$tML zM+JgoqXV%vv>T*`{67oZDOZf`nCr;*e@SwRl9CQ!(nv zf|j#PlJ5$H7#_(wOL|`v#}2Q9aW zIjMm9l0U2FDKv$V5+nTAW<-4iyH@1)YuFQNcWqUc-iiH)JST5$;uX2vYR`|VT1!ZG zu{=z|L(2z!dGK`ZVGWf;+Z;yWLf_}c!418-=QB9TAbE+n5AQ(rX6kMoguyl%(RlvL zRTCOqwSwEe%<;r=&z1J)H_&G{DK`$bwb6zQb=F{nr?DvRs9lQBb`P6jSwuK@A97AR zAokKn4S6byK=!ND_5*#*uU(Y3!c{=_-g!XndA=d$g*KEhE%?hk2+T@on>ZeGA0?pUA| z>Cay%DB^$j!dW2w#yaHR!RS*C?9WSLfG*MPvkKD!`r3?62Re}&O`E=89f)Wk0*IHW zxefG>Sd$1)S_1A8q~*jx*f}K~1o4OGZ{VQ>NkBndmR<{HCy0$v`7O{M_{*@9!^xlW z(MF#Jg}Q>&i}roOH7W*bA31I23eo}+xNT1oy?2=KqFc+8TxXs56g_R+Dz)rFLiO17>xj)_D?x7AG88P8sQd+XEag$5zYq+b#?Wke?jkBlX_l1%6 z^bL^j!#J;lq#oEMQm#5>|Jm9B`!!_#<&-3~O%Qpi;$MwSplUtcC@fK^W0%G`=DHI; zDqr4r3#uVYdImm+cl^iQ!zchYPb;|BVG-|vC>$z+3sj>J7Qsa_(P+j6^CZtw)K|~=1IPeW{$2!1m$HrZ*Z_t%0-&<;XQTaNXE);rO7_l3Z9*zq*`HN|Y$v%5 zZ6J?;4WM!|Rqm;KDt_{~*NA$M{Ow7^D+P81I@Nsu?QJ~fIHuf%%RscztQN$>u%>-~ z_qGv3wftm^5T;8)+{e_v51aW;f6F=0pTuXCeF7_CfOA_yzk1u3#{2dk_*^hCYT$z4cH!4&^9BELjR^3;gK6Abk{js}BJ*iz3Yc|OuPNv=T}9uI6BZz|U+$lV zj$wZ$P~K*9Bjy zI%ZA1kp-Ouj%^|bXu&PPbd2u8wuT$x4uSFjg`#@Iz7H+6tXZ-{BnqQ$>P_;GYxW>O zhHhPq$XcR2_@(XU^@_q0JbCT#TM$ddtjj8l?eXTJ09@3NLxDy*S^~4hTkoF|QK)kP zp1VGS>m{PkRQo%y?%W1sN7ADAXXzMZ(IH8~QCYr<{w#VWU%;K&1<@#ynso0*3HGm3<{5K4AM9!xB+)611QK zs4YG#eE9N+2Dxw3|cVx+jlfM>J9$SZI@;SdETs5 zaP3ZMyfS=2ZiUx+)MScG`zc+tG)?FgRpNn|(uJ9kE*s@zLDzd8+<0EG*I9!@OE`ND zUT%uQlI_!1;61GE;$?W}dBsC;UpY&@YJy`{R_I0=7Z*okRHYy%XExm@rA(OL_QR5A zZ7t~)Eqf4dK10pa*9XR*gQHDqCRNM{DKTxd!)HbjE+)pOCvq~9U)6RIOblgdsHwf3f8CtF&ExDa6ceq<_!AHLV&5u5IRMYkpl8N-m%sTU>TO*6p}eKU>Q`c^ zli!EJGuA8~NWxb-^6TsBJ7~YAoL}5`vAOx{tg6F=Jv(f}v?N=-A#CW)_yy;6jUfm~p?mXeRJL_j<{PYvLxFA@E#4dgetpr%4A#?&SVp z0^K+3MjDKttf#f5q&%W3!FMQ*NiF+qIhIpS4(*f3MY)7?4pk8mO?K1aLOAvr-Lj~B zcYpWDjQA4INRiV_WjaAeKDV~4O_h}sO1-AbO?iLc;4S_B$Nt%b<=&ZZNfs>VZ+&LX z`HEZ=ghfzT$H~aV9C0qBrl*&+UGt5X^iiI8yz7|DOZXN)j-Nbf42pQET#X-ZWq|_R zrw3`azu>Z7Ml8kflHifdMQCzbYv3J^vGH86_S9`t$y~JUWGk$$tIIg~eyuw4K}K3> ztzb#XaN@pTRefiZ;`+FJZ%Ze96iVAK=9OT<`hM!1a6J}+2yekR2WCKM`wn6AbX7P{ zXlSSw7iywM*HCk|N|i9jOdvg`-)ng(!9qq93w80EXiwqDg0)ra^M!k`D3oKf;^UVD z9OsHMPBPQh1nW)D=SukLKK9+0$UU)xlv9fwsNX9T%?|EcOH0}EGyCnC+r~}4# zDe^)O37A+vT(FkbYg19448IENLf>YZVr`8nAzKo@!INwP-JJ1?t!QOTz-2ZC#z#kfAyDX z`8BSciM-92X*yKzsUTpXOPtfYyHmjSlUe|txJZYKR!N&IU6F-mhEPkoQD_Bh$vuL$ zwzf;EPfo4uf7!*vHF=JOpybI~hI4cOuR^zmh6Z2u{?oR2*H~?32nD92)#O&f`1q0* z`*@zY|>gA1(7Bbmpx<8Yo`WCosRdS|(boi}s;_Y#YpM}B+QgWkSz`^LipGBTYuKQUWs*OiUe`^7?&sOFPtZ&^o&+9E8l{G5@_42GMZ5+UdC z=eI$TFNIuKx3aR5$8zjNJ{KvxT(Tj!Sy<2~TQxmlFE{X*jPh~OnhM?Jx>7_)`bvSDoq~9 zcuRbRx7bHeXg4I2i-AIOZ8bH_ypqf~8b$xHq;8*4@e2OcUuxj!SB*+Ao^RK0Q(5d2 zSjjo(5E0_;HN>8+Pk#p2sh}ha9X@GVJw>Tci=5V4U79ryf*3k;g4YVZhs%`I!>SA) zSp@|e(r?+;oeM=AnvC8qu)4{Hwstm4E-{>QgS22PcM7o2eCrE+2iwYKD$nFtIfT>* zNwwwHd%ejcyKK;jZQ?5=w$d}-1Au(T!p|1q^A;;%gTktEASTA}C&E4Eg^(gH(n{mx@Y~61teAz^) zI0}U_n(SKZVvx}+u7WnBjd6dX{voI(JLXe_p3GKaeUS=B(S8kWm~7bdna7mcN4-Ktognd|B*|b z-e|658(pN_w@sQ294O1k_>T=a<>x#&V^{^sM%-rEetydwvCRj61F-tkak|A71Lj(>~BY zvIwsznH*Jd0h0MR`LTPj4JG3&J6ddglklB*g-3g??_SN{0dj z*)hj}upmKwRU7LZvP&qD_?Kx$3sq0l>gZ4tQtf!rz$)zEVx@G$PCUB2(EdB&;vK%; z{iCe1bNM5(P%Sc~j`h?#=@e*4D-{5&HE9gpwHballOi;&}A>^TJSL!%oL2Fu67m)En~ zr!(U2PF--i3D-|X_GhM`So>I3!>6?5Up#KfLIV(pd+z36Us7l~428|b?pr#LTMfiN z>qw)_$|%+sL>3)UeeSxfA#2f&jQghPJNL_JlI)#m%v+m1eojF_frUU+kAHb`_j=0r z1@%P})G1gm9o{Uw71!ROein52eXU-JnTLXB*|->LSGQ+B?si8ikgXpd&peaj1Yr-I z^ViDhm@J{fqNk*=P6~p&<3SpZx*VUAW8a!?G(~}8K3;116`ni13FPNkPvISi)og0! z*n84okOI67*)<%LHr1jrR3MnopkOiGsDP5C$W0pc3Rx@@yce)2_Y5YuLYcPl8P*cF zLZqI{9@uq2Um}o^Eg}ePfTuc9%)?Q6$KI9FZ+xzXl8NLZq@~H%hXk!W+})!hgM37< zXk+Dn@N=kn`|pokee-?hklE}|16)lzeC83Ols44O$d3~q+~nz$%T92U1qR3+)>DHg zwQcELMp(uhpNTk-tI~6o)@ecosWme7G|uXxQv7-QWHsRgfc~ktfqZS*oSYouk|^fG ztWM41C(WP~+W2p6dgSrMz^)df^h*ZrG^`D6QtOJhC7X8@lw|cCpEBx4f;yA4WKl`U zQ#(9V%2VF%zb@fhqzUmT=s*5j_`9xrZ4mu%V?v-8mIjHB&dui)AgPP|IFY#__oxf8 z)VHYTHI*?c#f$gn&cDx1MMXtic%$YZhf}l2$$L9*Aso4d!*`S57L@m(v&g6m1!TR#q2*LQ{F&Pq*sy0H`~!GEcL?N}D35u3RL|pI zjbwd<$7~QqVes!Iych1W$nQQRQuL{aJx0>NU>M8|J-A?z`6;8A{oY2J{>QJz$=TT2 z_U6g;SO=d@VX37O&H$eTM;thlIh6f-g%3i~zSjIwIU?{J3!Q|m?J{DcC;-?M-{RjZ z0JVSW&IJf?$C$XWUNn`Ilt4)B5il*CX1S`}J2X0K1XcAq$|+cE`_kRME!C#7;ik>%>;7s#f6%#GUoUCe3)#cSf%nsC~n zs>eF8aox@T5%oDhgWSd|LZ=Deeyyz)4yWi-qDl;XKPUg>(1x*-n=2|62(L*s|ff2@_wM_Iu=6(j-gAM^9_k{Z1>yr6C* zFk)VVld{PF{YxkR1C>|i@0YYa|7^UYpa(q{0=J^Y_PyLi<5hwD>Yoo|0KY5lDiWX* zW`r(6X4r9sizKjL9tZAX`Fmw!qINd5x7c6X0uzI}Xq+WseeQTLm~_B|uZhH@Kc6b| zU36Z9Gyym8=PTI=;1bxuf8PQo|ENLofi?Q){bQo#gs!o%t>Fruuc6YyraUs> z!-D0#cSbZAC5n42N{~5ec*)u3v=ac6Sp<)ERUf?^)zZ5Ck1%G|><6peFSw)>SL{I$luWx<)uP?Lx zumAU#!Tfdj?%O|({xXzy1zB1sN|~C?hX^+d-~Rvnb^f~9|N1!G?7yDPe|`Kv9_7}z z|L3#$Uw@g+zkZPAR69M2xupBz<_h>Fi8oF~A&{?g^YFk=xVbWp^Z#i7kX}#ZBmN-9e}2g8tvp7UgsR zosfz7qqil)$4Akrc!F!I0)x!{4V{D^S0hn#^XC*g{ZiXW|NmbRDQo_pc#4~k9)()D zgp8;+e0f&-;AdKi+?R zlANdGcg*kcJ&qrLu%D}^pH~)K>dwl4Qq(i|kpSd`UWuN&H1AuPUg!Ie@L$=y*u9Mk zmTgY$_QLUS8yLEpVC@ew&)da_jtN1&IUXc?u?rO_e^^z1we8LgOaISrz8(Sk-O5is zdP~8~eAC`_U)wubjNnNK;lG&r-_Mvv0-w?J{CzwNWtLy4rfNh(-}V0IRKA-7cltCw z-R0VkLh^d`=E6k-&NR*c3ggPZ zzj~D~*i1$g2WEWmzrP9m8>in)4&7CQQZy&t!;ZJG*Bf>acwnZL z9X$9uCDNPER)SNx_%9C{da2_X2b$ExltfRT7krQ5lbCz!sQ8kw(*Dv{$`4p%BkNLF zRM_o(X;K%=+3qTj4&h~QB~X+5{S~#;tB=COzH47R^Niyyseh76?pCLMP6{lAb{$aM z4*KG;diSbYULdubF?+crCr2ml*)-P|TGrpPvR`7yZ)+M>3XIYT%<0p%LDB+$_B8t`J*>hX3(Ia%=+a>{u2k?ugNz^ zjuVg?^2i$K{^tfUlR9UMxrzD6=f**Fx|hwN6A6XW%K9$I+_g;y>CY=^kSa%%!Tpj| zlm^D{rP^RLL3!+_sDy-PIesESxs|~8_{jC=f^(;yW;~(SiBBtiZM%{y&+o{%M#H6J zb0H8%`m@yY(3&_aukeir(Vb_g(nux}BODq%5p5nf7FPVUPl~Za$~c#PPucm7&ni0) zeeHDcbC8+@;;RLRwIT~c%gNZ2z}#v9V&tSo-VH>Wx^>0Y_S6%*gG*r1Bum7+=Oae; zp=j(5oe%xlGg@~hHm7oyZp%vt>=ikN_&c{l1X(TWvYZlZmGqab-`3ThdB7^$Q9NqW z=pS}w-JB*zhtbv7MggRwQv@ld-Q~HV?p0bTpfQI}a@-~Lud=8FICvNxr=-M3R{Gyb zqmeYI+d4jM*P$=%-cIWmYj%d{Gw6_DffTA$r~@~ z<2YE0Kn(h?x8=VN??Q)sF}Gx?FRFwqojfy=DZC#v6*k0V&h-j?hd)@1v)KP-$F?+Z zW11S6DlxD**FxNPTyRJVkm4&-S6q_u2WhF-92GLN1QhKYfJ59?>Yw>;Cx?HR`>B0V zwV&YzI7~YH0rgey(emczxgxc?7fNcd zEb2}C0hU38gbJ6km%t+Jth+p*D}E)c)V<%5w^5Aks4_Jg(F-#R1#MW-jz@J-k(?Ac z+?AxZ^kkEw zK^UyqB}Ri|VmhWX7*skH?3jw71HSUsg}WxL`E+{cC4V)T65&W>uLobuMqOM(s`}eCwo&|Lj|rabB>#Yb2}N&RkZTI=a5AXSbYCv z<=I3d{9OIJGpttr=@j*q8FxzZ;aSr&Km%~Qa9(;z^J5ifQfE$%fpkTG{>Acc^QYPdd1#r{N}=G{miUgPP!P5vXl5<;n?PTN{4?(->9NzHTXUuduJ)q{5mvaJJ%omn zoE2gHAxd4iD!Ke+NXH$ST{pu-U_<#7HD)z%jTN<+O)Pj>&mga zl|69QeiXIISC(GAS3^qcaM0JqkT~MY8niC^8&O^<1UBqt>}bFR8|!!vRZ9GY%8eCF z_hVL9?p$Nn!Btv9_FB zxt(8mIk3D#5QXuSq(qXR3aV0x|de-UqQQqxh!8YK8kSCKbU=Tk*la&6vepL|M+T)xYWs? zzqMtK{}3iu<$|1^#n-TN<2*&o%3*7J-2N+LD?MWY9UH(Q0QUILA!JeyTUS78&1%F1 zWy6Yc^PRt;9&=$O0@SrP*-#;-R8q%rhP=e9A}4evMOUBtzz$-MYQoIgJy`CR|(c2_9n%CVd581v#QZWyh&n^U_t)c)AMd`8ht z8t^$ap@7d3e8I7Z+wk}tuH%Lux+Yp1G*MRu?C3NLB2~>P+E`DACoinYs%wT5q<10+N(;J`;tr)-~z(z}mHdzeZ6l@rue z;X>);flDdwz9#B7&Yv~)?lcY>`%=5}rtaODxGVHhWxzgoXWl+lF5W);lXMBhCsnWx zZ(sQtdQXXH)m}Fn3q?$4_s8pgu;kU5{QJOSr)HJ+ynToxG$Bb(>Fw5^lIEh%;Mw38 zdrBs5ZLUi#mHF*D(McJrX#_53EEOv&(QkV6sT`N`E2qbv=Hku49kn*>kBQN z-z?Egy(J*)HuHVUO17#;0>$MxvJR4$lsT}zW~X%gM>|4xZp%DW92z9U85cMg9fmiv zT5s2l3+nbPlFQ+2m0LQ?m3?=R29r*yWxUMR`B6_u*G+wgtMe5-oyE8KCJe)q!@P(=)bmGjjvcR zcXdfho>+WE85;|F2~Jn@NMHS=DzKwVJ~Gz6UF+kyr5O9tr!1HkMW(K#E^FjsU*eY) z7sM1BXUndVvq&M2h8!4r0|kUxPWGA^5*$CH`>zJ{_n0Aru#%}M9f!{m{NYz0 z`xUj)ou(6d*)QGfpcrwn(5alsz(EO=uK(48=$4Or{!le3sy3gxk`nwhJ{-ttXX4yQ zYGv=HC{_f;?VFh9(dFV6de7Gyf`BFV-uJ7WjxPBbv(-R3MzQ-gsucuEmwl}p84m;f zd3!GX9qYMkH>zI(dmR@|yP*TIH(-i-d|luIXGb)2&C?-k|3#s0&thE7@z|T1MAd}- z3j(Oe7kPuNhcG`wMPup0TR@@IxnCHDRt6JzQpMkn^Gg?AGS^>Kq$|C3<6i{o>c<@!t87_6E93k4l**@{{1v#zoY zo+WB4!<6|=s;Dq}@0D-4Zce&0Gh(ePV!k1h1z{$fpu6zV&dOphuX3Z10gC9-`sHv2E?&9+db_K>`FUG3wbzN2lt5Z zpEOEVaZ2a%;g!d_ySNO^w&>0x+DEIXs$oydy?gx2H?mh*O>N?)oz%ML!oQ^4bE4t= z^IYo^g^csanVwprU7AR^CEb|M8>|ZtI&AvglOSrc8H7kB$x_eX+;2L2=G{3!6psha zT>=JHJfu)!d>HNT6*oVrhOpCn?sxy7_4DRIdB;>&*x;6cQx1BJtK;B2%?85(&s@su ztcl|9BR{n3rLWkih86gY{}$bIZPZo0*%H~*A^SMGlwz2iI*AS{!%=zYV{#(nWH zDHaxxpN8QFPTIeu5Lot6QdF?E_jMbdPA&>zuuOFK*F$ zqMoETN74EP-$8jZN4fg9d%mh`Ydft#uK^XPjppwddkvZZ#If06?7UH*)nuRbNdu?L zjijlfYbuGc#ESFOIvMx*_?d4_Lg%{Yn7VHLWf+f?;YTtZYxLO9bC)SVz5=%@L=MEI zuQg1SvuWEU@p41huTT9sbLIz%*3XD%33VR0p@4ZYbEby&DGy5Q3LsR#li%m<(|wYD zRp{TQlFUcHP3$OV%ZWw}c00di;E}8woDOeE*b%ptc1Shyo-CU@ncG5($?&+j9Stn) z7M!nLs5w_(X8G1PeewXh?A1osb5Z~F;FFoxi3!zul_JQWj_q%kv?LfA&)nuXb+$2F zCcseCU6hytaKKW`i!{-yA>6<GRLAaupB%QlpiTby9c!Er`a?w>dc`gR-DqfTSt zsg&>d-(p}3S7>Qs=Ahjqpx^bxYZ}DduO}u;F57&;GfOc zYJY+H%`qOTC*e$@7CF6E=Dt1M*Q>7l^H34sq4O56loMT9=l+~|B``5kC%~L1W`!Ap z|2$TQ%4pn;26d-qQ?`85D<_7(?{y{w8Op#qGo?RI4Y%ABnwT75CGK_Pg<&UT_!`0~ zE2Oj_ALmJdezog|?RHK~52IC?s?t=}HMg6Y5Te}{3z^i>%`pX=Jur5A{^+dblwQba z9Yf==p_`OndN%agHKCf87wt7Bt1kMm&$BXuhB$WJR%>Iww(bx?Da~F-FuIiJknCJw zC!DkD`k~Fb)PwVFQ<^m_dvf%*uMAw(5QZNauBpP%SQXNtRSQa<)jT?pcV7~+?hhuG zxkoD~lMDKE(phFe#)Zc3!2;?|^W)NzJ2k9vx7Q|l)8J?Kg#o1GcLHQWQI_+AYr`%1 z5xi&fJb0%jS+)J>!U}lzH)s9f)1J1Jfa&_ka{|cal8;NuS3URcN%&`#kZs9%9R?0a zZ!O`Cx!ENUBA+`|_bK*-qTAKfao1mvqyp+M3(8#Dv(Y!q&0R*sY?TBGhC2ZPW+t>4 zEMVr27)gxze63;VrB3<@=r`q_It}MqZ7ua}b8nzSZQB~PHqV}VpzYdw^ul6{Q;L`jZJ>FYwNsHT;);sbRf?|myc^*F0aEFlOI zu~!A_=09qX(y`+ zs7-={j&LbE!#diN`I8HLP_Snz9N16!zSO$?fW5}yuJV9ah1i0>V2;&l{{C$Kk#Lhr zXj5dO{9t*0F|0N*b&vJa_Q&0pNZWSye_%3;MiwwXUwmRqaOp-VF;WDeyy)c99Wde@KGQ-)vv-{I%uqhE=V%2K9{8y!IwvlU<@{r@^ z@M;f&nQOG9@J&Z{B|gEA*FkH zbY`fc$DE#Q#X`b~aMz>F@GWy$328sWwz-fe165tc4I4M6i3y5B4VHhjKCbvxyMkA? zwJak*<+%%;Ux~%K=JLLxiWL9p`W}2hq!)jiE_ZEQWqA?2rA+n#pn40H-KBYK^Sal0 za^T!f=lS|J$iOD@CxdR{b-aCKrnuNs@MKp*5bAd_5YG~@n=8&knVUjaRg=Df?5=l^>{~7#$%{+yL$IBZ-u5te|5d&2i8U3u%Bu0Dyc}&pfyJGh z-sJttiAev2UO2&=G@xJ=+LO%G023lxcPc+nF7EtWwf&9;hQ;}QkCP1^&efZ*En%pH zlek3^TAV%)#7EoH_z0#Uj_nV~X1^E(QQgvqHs34$yzicjH9Jm7toGtB+fDOeD(I*W z$CM~2e%bG8;oy!3LGYxhqIW-Bbkk21wM@fOR96bkwVyLGzCNB#ZO0ayiXbv`)Wsj0 zNe8=PmHR+SYSXFR5b5#-m+&DM5Z4prVoPf+EWXZr?1)rMDcrE*L(DWU1f|8kXUC5< z!f*DElbUvi?bX8_QajWvE%Rv=`n{$kyH^3?cw2Yi0v22)MahC8D8l z*46r+x2V7+*?&-p;lft6ZK&v`B^c{}Z7tG5R(hd3)#Z)l8hwRD#DZlwFNHyG{jG$Nqt@E^LY^ZXtT-WqAt1%D`7`lETS`Db zrtk#sAQC+#=~YbO=a#RvxO!4+-a(v*ucumDhT4CGxhj!fnjPZ;G|azz$$pYP18k}N zIXYFfyB%(UD_)V+F9lWr!-Bh-A=NFaV^^8--nOs@+KamW;L;XWL*zR{YKgM2?Lr?2 zH0_1&XmCDJLu~F+DGfNCM;z-_cD!hYNbYhx-+upyt36IJc;-9piwX80z_1G*rf}iiC6!6?KpzBy&z{ z7pA8C$`8Ok;~58=O<7b`(MITAt(am3yLS)tvyy8@Xw98U^wRifmRfTe*QGJgCaX{5 zE#!O_r1fG3jAgNIqg>(uqY5$Gu1)0PgeoK~ydL52!&t9j6F!pQzpg(FkwcN;o{e2S zu?1jUUW|BmU~$}A^R-tz_ ze)IV@T^OzcdiYO@Sw@T8GxdHsaV)*lK-;P=FL#E6fm3|%nu>tac88S-4GF~A%dC2_ z$k}(b_+_y>L)&bbSIONr%i|ER_&jmUhZ@a!NOV=yan&2h6zFSM%?ZCU%8wFzAsOGW zvs81{6xsJ!FH6ijbLVvdTi#lJ#7+bPyD00}oqZJvVFj4ee=dlRcnB8Kdw$x3$A}LF zzYl_?e-@3}3Qm@fgWl&i7Jl9~T)Gg+IEFDhJ-{aqP`Z~9-gBOJiZT9A(@D^A|N4%a zG^4RP0wzIJs?>9k?5D7|5}UQmSw6y3ADfxNaSq=2m1WUq`tio@?hn6D4eFT`uGFVVU34CXj|E`K zvo{)gKGG`e&wT3oM=%Xr3!J47#inb3$@hP1J|Ja|Oe-)?Ik`5*zw+0=j`2Gme=V3) zwi0)QNqf}rlVsabcHM4rEYNgq^6LB0(*9qs@r0iqmLquAHZI2JG@RqlJAqooVdTB< zmQ^A5dL5H#nA@u}a*I$gtGigMDs`w3&yjokb1uiCNfrsFW{y`hFmz;)m+t{G`jC)P zdAX9HF;RZA-H(!aeZ`*~2^oBIa2*-t+^DWsX`!p*YUD0Eu|whh1?_WG2YD~61x+t( zSL`Epax3%(?h5o(;cB*v4w+QsgXA61?PAzRD#y==PfZo=jQRRN=X3tRshhFNL!}|e z*mI>+K@sn-VXKiQge$;HuxlC($)tu>?#vci*X4?Ox7nNMufa4e?rjqCnF~=gGuipn zPzE{Pv!OIG1}{}?3w4-laCdIuJ}Js=ID*G&5`A4}>Jp}BUySwt*xFKxC}K>*&@Zx53AKdg?B;8~x^bV+f7j^Q+aMt8$dC*bzomMWk(21OmHu zpmX|sOmspay64;?I*R3S&QX|y^{^oTcCMc=LTJwWg#d$QzBdmWkp+(tGs+dNDss4=-TGjvQ3 zNYDTF|62JHVS}EzPF2JzQ~$gVac49N<13ft)@xQVPT3&{cdciWz!Q!85%W{OWpA}+ z?|)A|#Qcs?_;1-7AzgG7CKBUDjk-Hj=YPNf16AW{kU|1tZB1ui2v1z~9Y|T9zXY90 z?T+4@(-Znhs}Lx?`Q@%Irtx@CA$KFUT`{ULX{64jm~yp0Bka=--3uc|;ErzdkJE+A zmzChx$3xjgk5&~YCpO1w7g6!6*I9yX6>w{g_02chw1qQC2eP$^^q!d~vCir#$Ke)e zaMG?=yAOQq+TizLYT3#0_UoRiLUiq14VHCdD~yyz?o#-<5nT-CU@R%%=JScY@nNLu zq%a?*fKFZKYeN>d^vEpP+5gSoC-YL%khGz#`<9~1Se5n+c1*_rn`j7J6r>Q$qL#@g zm0|mxaiV;^CCy7k&aWsuKX{dI6vW2pdrNycDo>St73=q>ceMV-BU35hE7t0d{a%x@ zFB)*YMOW~!yI5gh{!`~y8qLq&b+t6fx8Slqlvw6j55afzo`3e>))k}=l`jlQp}Q$o zKDPRlMcdwJ85I_XONQ@O?*sI2_bZFrUmBV19RBq1Xd*8f6{%0G6c*qSvB#m}Ps+~e2 z2To}uMwibJW?65S8E^!%89mL=6%tv(`4zhcSH`bFqv;_XMkS#S-WWe>Hdhd#BfEKv)F~^d@Xm5LV@igm4ouTfr0*Ok;zLQ zezMBmOT&4xu#gYM`#LtIhd0oMr!6Y=RaqqSq7QV=S}=Zda8gDvHXm4jEO=^0%Ti9; zj;S{kENXs?Gag_T7-#E(bjgUq;N)@EN}=pObd-^k#=^C3^PpBaxihODtBjQS5fgFhB{D0X zCLm9-GJCgAGpg-$%T^V$KWlZ81_KaKa-i?zvr zAtg7uVAou(uBmwabX{B&MT^gp@@swQE8Jzh&<_d}yLYSK<~S`ub{I&@J1rWuyD;Ob zc<+rYTKMW=O%YIGu9U7g%1R6eD#6ED%xQ`)YkRWW+QSD-W7`mPd2K zWfo*#DvFeP{BWK(rUCV{5QSTF9Wf;vWRqr$|BUJwk>)}CxBAoeI~nC(H?Qrlr{y{1 zQ`5x27a;cjK1ycmNbfmC;STk{@O_`_*c>iv_3_a-v~;|9)aC1_7+9xP_<8Gaq{S-{5t7YJr^o0q0dM(%H?=|hpH)$ ztt#FNJKP_RVk1otF=1wz>?|%sSU%`SX?o13Vz@Yu_DdqWo#}uEzq^Oy5n}IUbiqRJdhit7t0sZI`K67Mh*VcHcK^wYg7&Ap zv3JHq)9pRs;^m#cIdf=hAGP11(x3FCO%|@{l?W(%=h>H(Rc>R4Qil#61e^a)j>kt| zQR)kJU~u#A#mYPyq<-@Ii-!d^me4tU7n}z`_I@BUfeXsQL2_7B{(RA(~<7}#Ixb*9S~n%J_Sp>yuiWj95}e$`dVBZv@pk0+$BJT3lXmIgSYM z+FElDhn_sR4W_BLZubpQR*0PrI1*CAe&6lP$;i+Z`-~l$`_AlW2>#(3?4kP+!GZOx z;V_Y0jB$ZsXoZ6m)zO!l2cK^WqZ){WbrkFMKuqByu)!eu+wPUFk-XfkiO;)V;+Bgy zUcDV7JC$Ozfj-wsqk#dUNkfNIA!u1J@b+|BIc-86q9t`vdBk2Q5#%JJ?nOM0ZBqM| zEU;SlZ7ys+JX+)s{Sr^ahVPH0e--;qJ37B`e)OEE004k=O6dn?&3r8DQ4-*z!f9O0 zoS@XhCbUwADKA}q*9Zd6>`S#L*?WPM`>w^8cYN*JF$>`qsUQ%zWw zo0ZQv2m-&r->D%Bs>%Q%-Qk)*UuYx%^eb#tFghue*U$#k z5NviAtnah*Cp_73Py5c47;1Ot%PBrh8@R^@G>Psl=sj_}$_8c(Xz*$cu-gL4C;>JA zw*Pij#kgiTZd2K4WO+|fO`N_W|Lpd$!(Fl^#cXTGcPs6q&*I4-CZC5~m-<3rgpo!x zmY`+O?Z3s!u@*|vyUMlF-s|Lj;PkKxoip=j;uBWE_X#pxaZc|eYtoKWh6c4NnGx^<$MUT%)8f5P~bH&q{||5z2$q0 zcIn6k$}CQzjlw%*7AQH0c0St$3aP~95xaSKh+wYN=(Z;~_ zpMr4#o`u;K*YFDpEQT_#EgyKxEzkSi0-KM>R0&!MhNKd=r#b6Oro&hXiRQGWz!G}H zN<}72Dr|PRy5?p@!S19hnu zm0ZSTV8hZI0w=L@M$p4lVR{$nUo<4SjJzOwkF4)7h?)f@1uLL>x_ik7_6Jt5#!xex zeWaCZ9qD+@Ajso@8vkDM7pD>01ZrwDx67>to|SUibxf_ctQ4vD6%0$Q1^Q+$wZXYt zon%Pq&eolX75mJSzR@5p{_efP{Mz`fp$`h1bqg9};!ej`CAf&|qWUip^cB4Vpl?}m zSC5Ga`(>2#P(3C1L;%>ZYWx@Cy8X1n++sh9o8ycO>&UmZ>K<3$C+Tec`^V~Y?Zk#; ziKvE`iB#*8*HJ+ag>im8Lw}8=J1eCsIu$-*i*7Jj+yATs+TV^+saxJ`|8rqSY!;di zNuf0xls{No2G^YM==ZmbYqwyMJ!ckJ=Sl|;pT7|ZbLmYV+142j0X6aVg~7bh*ZBoU zkIzRZ$2mhu%p$~nq1Ia>H@7_m8<`zj8x}3h6ylCG>bQ+G@%85D^*`KF#SJQ8MD zeAGYQ=Nqk=uY156ag_XJv8`9IQ)lpS=dE#7M&a0Hxn>x@GlO)5V7fWp)xqoZAGoj* zQq8BEehNfI zOF<5Nwe_kYd?>Lgw{MCWwHqFtFJ>&El8fA{i+}y0m%;&T3E41g%LQT_q7Zh(^yNd} z&+joA2v==mWBP3X+`OF&%8qyh#~&7A`{s=jYYNd)YZNxa1Q%H$iB~oAb0JUpE;GRu zknI$9tu@qmTdP8JuK0uJ+4fNcEW1@QIRx1>V+f1asoPSY6(D?(OS&x1U?VV8+6O04{t?kzg>V52Cmep(>rP#KSu>A18 zs|ZTZSAi0vx;XvWBoZ_$$0WZ&CFwy?EgDj`hdZBoJ=a}t zo6gAtZ!trb10A;OZw`1&N2uRIa=y#F1m6bY9vYO*8RmDgIwoIa3n|e3wuzWPYxmC& z>b&DHXLoGuiSu)`y*`2#&Q=u6u7k3wZ+@xC4ibAW1!RPJrD99V*Mz@SBf~C>Pd@q8 z25>7mRelM&Z&ZSEjLHv;GxN=|F4+NyhsAJB&CJJz$@W>|#}*U3Mp4ZrUIFoq^1q*3 zsM1$#YS7Dr^x+&hq=8HC1G)^{ryJWY;blIiZg8vpcMZDKyIC0!Iv-m55FsC|% zKnm=g6!`Lq-JJ#U3_=8A@VyVJl_+FHQv}*}mr=JqM1Z9&+^HJb)&&)QC`L^hi_V#J zvSV%0?9pa-cbpo*&ggVcxc8STk9yWH_DIJ##WqV*3fVs_MfDTX9%WpV*{Hn`%hwG==b z6vA)XF{v1rVrz>JnV2|r}WaX zv>+41pEWP7)&)Rcv*L{20gRHqvMFS~4cegURjcslAo)XIh(lT_cW7t^Iu+&+NvPZE zYQLrxeH5tYHNu~GaTf@RHYdh|dx7~8LEhtlJasweyCLQy%XS5YoNoVgMSD;5tAu*+ z=7TXrS0we2LEACefv+<%Cl#_NBx$pD?y~UKsuJ4bBdwWqTUGmo76Px_r1&{jbpD17 zjpW?@De=Go?iQ<#{MUUp?&(iA-#kgbv?3}KA}zfE@)IEvgd+1p>h&$}_~l9iG4Rke zsE|-5N#1(G2E~KbUUbW9d+TZW4OQcs7&(2wSGn zN*Cz;ZTi+D^%_YmZ}(?FPZ38{T~To_{%=ACqaBfns?=)1?+lB9K1E3v7`mWCDj;lk z8c(|Di6zrOfNM_pq4EeUWv0!;ZQa&|Pb|g@#<7q2rH)vWeA%Qzl_el8g6@K}d^&+k*RGR7`-#81O5lb57KTb_*d}eTTL;`#ygk%2Mb&U@6mQu_z;^ zoRvhmWVIE8SB^SmTP+=7ewB~ccdiuX|Il4Kf#=G@+D?0#qTL@ZeMn=-x46iQ+T9GN z1uLyahh@t*NU+JHc1Nk+$pudh`U}bnxQZ_%<-HdL(nPT5bjtWND1-m#CJm zl)Ppa1<*}CUDg2-=r?&&Ia8STP?d)4V(rhW0>sAI_nc{7jF7*GHG`X!t#I?!Uct~6 zX%&e^N|nUtz!4ZEEYo9VUv;L0oDfwmVp6x9J^4AIu}opmT`DLFUp!YKDvVs5OYZU} z(J`~6R*H)}rR!^RB|CZ@j@g_NF2XGHw)%cDT4-Rlt}EBiY-I#$qyPB7MPHlwLgyJ5 z=CK#rQUvbeer{f)yV=s~gtB;eW;O2+C-cjy!XD}2kRiVNehly{7b}K>yYlCMck!fd z=BXIypg7sDp^?41;{HbuPa5sIQ+oMn^bk5UBDoHH_!J!W1VH#~}!n0C_ zkXehPl*r9r+UaaM+MsrLgT3hPREV=2WKZWD-r&Fy_7rI#!N4~@YAs-*70B1O?3xtu zwgxxlsnKxOB=zfs&YcRBpO%-N7ILV%wkh`Ex_Pn(z30n_M7HaR3C(j_$HCbLWghV1 z`H6D;CCFHhLh++$PiN02*!q=1zb@<4kz)OuD(#qq#dvJ{yY){)eq+$VpPdJE3%lQ& zx$@rKMtQSonpK-+xIdk1&Yjbr1UOE^A51sGQ9p{~mBK!$s`$w*I~^Im<;KAS&^e>s z4$w`CFnbtnZACk9z5G)kpmo1;&{osoWzNg@Kgo9B^8LPDHI(ntlsN|_G}fQIePX+Z z%1XdbMgiki3ME*8;$P#MRC{7Q@`ev8GbmM6VY#w;GX||_`jxbmg_AAx>*I&lBKq+q z8n=2Yye2q0ohVTHW3t7@Ko>P8$%`7ZKzU~BG5e@|%Eu^tIPN^P%(X;K->Li&qPYt% znoF|HwaUNm-ZX6poy=GGJwBQ=vgg7GV#nQ@dwc@{!ckJRo)%nvuy>Lx$%K?I$v2$==hZon%Q%gw#pjCW*t@2BO z03~%Y0MD%EkpJ5vRX9%cc(0Qsnfi^+i~j7lT*p@_r9L`#0V~g%u!&=|e(8Y`2NK_2 z_|De)FGmWYyl<^fuo&vUIIeG}_K5U11WoUaKogI@+tZ6PgAk>*(*$k~ylc z-9Z^$jw11$(|yj@*E1t=6sKCC8=sp|xTTlPf?+>21Zl-HnUFW*hdaP9LgvJ|Yj$nrtpe<-c$JN0}J%H8e}pw9ENWga(7ZmajbY z;$9-z-!bU6B%ygAe!cNkJzuZY6bxOhnp5k?kKEzwim0!O5zj$icDkR z+B5?riw@=AB)pJQVM2cSJyb5^z}LpAnl`1cg;qrvM$CygJ^g!kdDWPP*f?1>atI1Z zkU?>U%brLf4qCI0B9|t*!?v=y>ycNcY5l&rtzmTvHe&*Ee8#(<@%Ckd7?t_@5IAQ_ zN;$L5rc~U{IMCXKbD-h#vA#ho_I&z9kqSIrc1D&tzx(to+!YTY2^%d~FR6U|3Q>l`Ii8OPwXKHTGLZ_I=!vQ_y7;1zXI@_qtic$UU%%CSssMsk{Z?LY zQ5t@rWF|XzQKfEswM;!PXU&dDX>(OO=w1-4IanpW1X8aAl9Z=TEeJ(9hxmtvLL4Z1 z6kGd0R*HM#nr*l052-Hvk4t{TO_r+7@Cx<3ed_W{7aAk=qSqSCPX~EQN?z%PcQsT_ z?~D?ZBdp$oFD*0Eybzc$-J8A}g7l?iN~8Cr5Pm^|(=sUUjl|8CJzBF7)?pc@s_m=Z_LUQKC|&&`yu_f`Lz<$Wt^ui9@v4e4VtY@cwS8T0F2M=b+`IQ^ z*sKoFstK>4=bnDs&XQA+q_q6W=D~FjLF4m@47ojCmuqYN46hd@q(3*#)75fXyJZZ7 zoXfr0Z^VwAxS+DECwkAn{4}Jpu=jN+YMgd4@9Mu<=?qNH=W!iV+SkQnJmj?-hVZGB zmQjC#H=iZ$kBrYnBamIG?>&OAD+wc`eeAy*nhLUbpY}JCZ&7Z&k+h)LzKNjkFb^L< zs`_;y_p=PjNQz&q(A(iGaHSyHVwBeG^If=~Zzgyl?Hj72xxoFeTl%joas=MXp^R+f z8+0Xsf{f&L@jhzT@8z$TynTs^ar6}uDiZxeW{o6luP8)n$8F@CcD%}70k&WnjO)Ra zKfP?OSb7w3-AF9SnFsUKImqzp!FHLvF^GZI}gQpsVc`E*PVcGpM*t+6o@HPV z+ArQKLk>g`x7xBLId6)aREYSl_~Lv9?3mli#`ce>YekMUdF}OTRtFoK38UXhC=5QZH2S2=-KGb^`Q4MPTa`?&a-9g#q|u6h}jf6Xdd70t@p z3Yd|=zo!Wd<@1VRVRP(v%_@D9uNTSkDG2g4Kv@SBEg$|b0nsLeZPMKYXp`2vjSGn! zE-D%O&p)`%EWODC1QbbucI1~Mm2^GRT)GZ_CYEh2iHMnLLjc>Uw=F%mH*|9)sxJ4h z3|T~q;5r-$SWZHH73Oc7zwAxy08c9w0>Hu7*d0rHc>6Rv{{(dmH=Jxp;396Ef@Aia zr4a1KDK!FM1Z$?N0kl*PU;4N}i@llv+2QX;L0b8d)syvZt>*Q78U9%_PYJSY>JaBaEmj6t zZtZ4I7XU2sg?G2xr*1$%jWX|7^78r`szI}rmb=Nc3cw8s@(HT!SoNbvv;E_Gc}~En zMlPeSH$$hj=G%>}qqS9tljQ?>kOE;qO#8eYnZJ*%(FwSK%%vN+IlI>l7-3{#cKpT3 z+t>F97@)^EF7$0Bz(KiqK|~@Tq^?ONa{gJYm>u`f2nmhf1ad8!UsJ{Inf+%CI~ z+6C6G7K<(%ah7m1JDPUH-HcZ?9>h5I;++YpfT%Q?Mao|=FG`U(YV)hIA;b@{w4-zf zYq!*QA`_n7z-XVh*u|eRySlS8SM|m?BO-ADUseO6~2WHk;2yk#I{uJP1V~@hTOR zyo)mOZaJA2z}NpW9hDW1wShvGu|bm$9_+OJ7=2=vdx^|zXjTXz1iL42z8&}jIBsuk z8W_Bys3~UHw}%cWjaQqw)YJ7^M7oQZU11w6BJc0=_c;L^;`j?_*LQo5k-vg2eH9zM z4Sx3jnVuZ!C2@Q>0h6Cozvi82*GG~IplIb@PQKd2I8HZ*z3(nglZ8(s0nN2)BuH5| zG8_Ll7Ht-Lc)sYe*AJNmrepz1TU2dtmeOn`X4mYqD)O)3t<7&5S{@+OE-O9sE~-Hh zERJ@vNOg^I(}<6vHcDe36I@{0qzdMGv1)zRj2ye$&WJ*{Y6-g#^0m`y z@{%H>4yoj3f@;BZ^d8h^VnXQv$oxY<`OY-mQkV386d4+T0ELeGC$TH4Ny_IevR->L z{#`FMZeT(>Y}5h{15$ktQIxKy#-8ougOPv>A9pKK!%Y=$07G#K?BS^^_tbF<=3$Z5 z<%c6xA2*Jh73Qkv2vxszh1~!(1e$=$g5ljfR0WpiR10W#J3i66_cuc|rET#}$xhW! znCcMu(e<`dJ$wVTPjq3*C;<4T{Ji|MkR_aKs}_g?-;?C0-LYMmu^4uWzae z`}WMU1;n{A&rPeK)BnxKkB{&|ujEwWq>il6LSjCYt>;%m79cOLGB4sd(^d~%$v?s= z(4mGme(XcCGr%8DO3k#D^}N1xtLY2ateyMdGL8Kh3`8{^s{QTI35M#;3J>0JE; zzga_L(h)KfbhiGT=!mA^lEn=9g6z9LHDBa@a7$Ix1YSfI5}%E0w+?`Dna!`S=mRfC z4DqJ?osMUP<>eY%cVDvb`cGZ%R3tg!Ri-0-d%MOejl*kiefOW*T>by)>dWJy-oO7P zDN2e&N^V8SmV_A6&6ZtRGq&uEv1Tk2k{d-#U3>N{bM4z8#&XM+vc?Q%7|Ynr7-We- z-`CW=_w)Gu-hX+|crWu>&-0w~d>*mw`Fl}}CCr!&V?WiN8&(v3bdyF&H_XNs`PS{B z18#jb0wLL6QYiz$OT)^)@xBDr zSe|~2Wu!`=aALhJtbQ4Hpka4!dXe^cmybR}NwDWHC0g$G*FH%YtV2icjt2Ji!Hz7 zL2`lgbPEbBhq}T4PTR<7N%_jlCS1IA(oN=Nr`IB=@J9!Wf=t~fqQjy?46@0GH>EB4 zEmbQDnze3o$`a$=OD>kH5uJ!-wWX)4iuWu=-D>CK-qS%VjbN_PBgfCR-^)(SHJ&G4 zZ|QDiQIa7b+6WIYML9jQ^dVqz$GE@m|BcIaRvxK~j?R7zg@Cu6Jyc;<%3jpu7#pnf zpBMU4pp;ncD3w*H0}=;~*f0W~K&+i}O*mrV1@WsgA;-D|ZcfC@<5zO^LF}Vb0{`Yf zNh|6yEftz1g6Z`dr)j-CY-N~Ts+9(u ze-RxZRihy~-SpfbBROdH+vIaVHe396YVrWmZf3KVjfRVa=pVR>^$JWV9{9y&VT z1PZOHkWIT5E@k#j2Y1NmkBOv%C}V^mjgRxU2&pM;u_xUboHuI^zD*jF9SE`RD#BG(0nmhzq2 zrEo)#4L8dt2S*0@N<5W5oTHzuQm26k z!2Ak1u1*hpI(TqmwL}|iEF2Hx_w=%3!_>}ihY~Th&wpB=sKdOVs7l^HZZ#PNJ&d3w z-Ehvt#|F?GYeR3J83~Zpt68m3^of3zdwt{^{K(1%~ZAK%wsnFD}U077?SNakUW3xfT|GVgUY zc63iWe284AkNdDXcXrf7-wv4j^@9mIb8gs@doti;*N?uIvl#?{kaMNMSgEYclYDY& z`tw0!N@~NQQd$<6^?k_F?>x_B+`Qa18@=qdGtG##>le4~xs?IdIf(0GgaU%Vhz98b zD~ARJo2tE>GG+4A-u%LrU-$=G;*iU)yT%91A}3W`EWD=6BU=#vlK@le`M(e^G$q=UfTI0Ft<4Yp^71pbFNIXe$e2~pAhP&$;hc$Gv$ydx3en`3O3}KwUJ^zg#>X`PD4HKOAKyCAb zg>5r!C_VzNWjfw%z$O}AyGWAHMif84Hh_-=CLFw--tHD$e9Q{IWQu5mkND;3SMw~m z_JM|P8(4cQJ*zD?ZcW|=6KStLZ-s4dzbInBb3R7lJ=*?ha&&_T6Z9uLu)Ay{VPmb> zof$b*qf%dqY5lF=E&9pYGz_VCJPrd`0nj-JQPBG_+B5-DW9*aD1Y^& z2I#RBT!8PFf`X9FMrGmgi*TnHKcX8z#M>g4Tk{549T5jHVlmgwNoI$X_OU&FB4Jr6 z=K?QWr1O}2@sdwH|Cs~w5B9Tfb*cODq+Z+*u6lk%LE#VF<|kjj<|$VO7!qS=@*4`& znn~UHcD8&pxY2l<^QhDHbgw_0K~B$o9PU%RZDM}n*^@&U3W z`m_JZ;L!2Yr^`^b3OZFa`6nRNf0av_>w678n#qhzLQv;YK4Wmr)EcU!ku`GftwVl% zgg|8hff9e7z|!zIAQlWtmOf!Dhxf6ZPNbCFs6y!3M}Ra?#xY#vp4Mx#z$ekZ%;(QN zlCGUgt@8im@+Co`!)zTy9s2C4K^89d7*F(4N+ZA`;uDFKOXZS|&}&*~K?O@wqe!}C7Pl+;yDjQ~0s>fi20VQ$jZ;cbVo}!a9=knc zuMqlkS2RAHL!a5YfqaKq;Ob8SJa6IUI>@)=WOD_~oHl}yXhYuq=(_-G!aZ#FyYg+ka{s;UqC+cIPq)&E4qCZa{k51%JuIFsDgXj?1a zg@PCEt}I6ehUppumMxEp*O!f$r4R#wJ_9jJX84&0aVBs*z{qSJaVpKii8M&1_mY;g znTt$B{wuLn;r&)9W7x4`_4`$UTv$LgnQ+p%GMLRsyP;&zPSgZURrx0L`o z@^h)0Rx><=%H;BZ-vhHF;B%n>O5fJJDPz(to%e9l<+<~lDeOy!j@}*v*s=#q1 z$os~8RK=S5Fq&JEhVrULOfbN=eUv9f&GD@kPFtev4Urjw+JD0S!(<7tcwDW zJ)a8wcVynTp{sVDH&fm=@6)1S@AgyQa$64!$`Q9he|)HGZdz$DR<%$O($x zSRXnoZ{^Ex0n=$*UOM1BsR~J)Typ;Fl1{d+|38)>P|Yi|K%!DwTzls3IU0t*R?4fR zESIm~y5!r~Z9{JO%JpUC*>Ep7&RPEu*RUOP>l#w#Dy-8S0J;41%S#+m503HyPgXTr zIpR*tn_Obzp(4GD4U?HS0`~{yde>^PZeNNe->SZrwr7T0u{F)x*7Oi24tAo@TOqzA zeht*Njmm~L=IZFesEMPzNPob-D^uP4J!k+nXl96sdtoNcE{fzC&t@utcjmx7&^D!b zRP7X`qMvG+57fyEfzc#5j3^n_Gh>$gn!%Lz;1PL?>Sin`uGUOS3xOSW*YB@RILJ$w zE?WzXNZze?9I~|&#nN4gcIC569B#~T%1WT2-exX4s>t0C!&@hV0ojQZvA*B>Kh_Gs zgV%?Cf-j5A4+W0*qlhK{3 zMK9$ouZ*l6xLRZ8avdFPjo+lE-btSOXGaD-W#t124aeAKhAzUX2Y?hB^s!8`8h_2uxg~Jg@w?(0TPgCk`->ynoY90f2Kkx7| z#ikK5PU?#q=nT;KX$pbbS-RSBvE&S4^1L<<2i8@kuHmG9P1AOPw2AsFD}bpvKu8*B z*ru3Immc?wh_b8sTf(f1u0(yyo5jeRBpxS#IkaliN%w5Z@u;NQN2?esWUshWkHc=d!XGqv_oRSVnW=q$+`UX&*?;6_DKe{C!bXS~Mk+Bz94&}TkV zQv2Gii`0=;Wv_KfH=9F4706pW0wN8d1xyOdcrhG z?#TFLK2j=Qn3WJWBVa+0DBhu+(8=b3XTv*=2~yo4ylKwEYfmptc-?q1_unghRI9M` zK3i<`Nr~G*U$AB_N#79o1uMI*16NKc#sASI3^{v%VH@^d?T>tRccC#tQ@pNJoeksH zNuCq?F5i#C74>33PCKUohSH~p1U}+aWB1_C$@{#HjpcJks8-crK7FV>Qc=vX7&bc$ zPTDfVMOXd-LZI>9-+V0fZUqV{Z->=h?3>tCEbV6@oDy_bc`cV-eKcm+1P_~?J@Dph zAJw3ZZZ!q*emxRUyGAO;5$i@2h7oVgJWw`6{Q8PeP?}ZkNnY`XN2g%1ZtYu!sM_Vd z)i7s~e(jeZV4Y~Kz@9G&)I_0hd7%!Zlrx9k@`ubwS7@cCq$D0639JBwC8Wc6%U*>f zP#ya|ogq}&N8DA6s%ts389wO=@`7Z+Wa})w!X6hPqMNm68)w~BePdn=|!{PfL z+^R%ATK!O-vJOZFwjm`Vr>{p>$B?S2$ZuPOUv40+voNzsB{=IJwH$N6=^jemT<{*h z=w!2U3*Ov7k0#ftFs!v?Fyuzs*@!8Fp@c0u#JnXz?$)vU%y8v+Pra34;$U`XxLIeP ze1{ZB$jmUdD##)YoYVfC7C67NiMM%cYCO;x-peJ3HOZ2DRT;Io)&$|lZcMwViE_KD zokksIdz|u981(Om??x~+{~cdyW?4%^qw?3W#W}fv#_FKuw-n9pZ^te{Ko`H* z9`7NEV2Wi@tcz3$bkFA z8N z;@Z|#td3Dr-&|hK)KT6VK+A>E6{X6RK*&HdCB`H)86DZB0ZbYXvY0=x18qL$tms98K zE(a@aaMS!Ryx^Fp*?cPRIWKUKxAFjAF8qpnfV{u-nIXxsHsLZ`sN1on!Oo8#ZoJD$ zX6fX%?xds<=L~Q(z4Vq6Q8| zyG%4qPyIn^e!S8X*5~fxWyL4At9s-P|4;c^b{U}5B5nUyw{{GWTaqr9zg^_DIkKJI z0yGGauSyHe&;v1eldAy^h9S#OB{=mRlA-VDw_mOp)kND=?hn2-e0S8cfz@d$RUC*8 zU@Ft(mTm@r@ml`#=p!?VTh7=0lFX{dp9zoU5oJB;Rexv$+T7^xCHkEe*z@nYJRQ`k zkITYf$CF7Dt_GN^InI=5n@BLWd@coL=x< zPG+6+6G+;sPuM%BE`jJcqtW*iZGSOWKwq${_gHzvv`9lPQsUfW_^U9Z`#QO;`aoaB z;IGiP-Wcr4OwV1vkTdp_b;mCUFO6vyHwcMjeWZxx(s7;7tqgX@;y$;2{qO5Up#Mpd z9at|!+Yx*Jo|!UzXv+5!dMk9BVY-@97cQn0w^;y3iX z+QaRJ5P}9f%=G>d_{h-I z2W2=}88~R@XJ*<@VCn`HyTKC8IBq_aCEwlciR0{8a+G5*5&PTDFfqIIygI(9fV@1g z{$;F2^5XbjuTY<#9T3$5b*~sl_96n>dX$Ujc)7K=NFkRi;m+YZG8;f0Zg{MF1jak2 zUBvQMzW?tFATj0F1$T_YN)XBF?e+o2QS;&Er9oSCQx15w(*-2cv1*1$;hb}a_=?r$ zUd&}!W>etf2^nTryWamjPlUn8;6D;<4ll92?GFQi5=_;z-3?YIS6!>dZ)oBA)U~gb zNzyaMb((RpX^{z1e;Il)$%4YrzaGz|kC*SKnG3w)2xN=>nXh50M?6nWecgA6=}=W| zxQ#OZXx8ewvS5mG)Zcp+MA>S4D{o)8?I(RDp|z`1=L$C%xC1f zS()DZBiC2ibv^bLECv;mQBy#NWG5v^C9~CoshZ_~Z}4)U_R3slb$k&*A$^4}Jt6;s zFe?$tzjd$v!p*0wPv5;1=}k^^AkSbNJ!~DTY-uhU`vVZVnI+y%4}3qT#HdjI^B|m2 z``xgRFz$WrEC^EYVK7g0Uu|2xZ12^?jaKLE+LfTTe3^`tqjR#+1;P$=QkLtes?VnG z!$3>rfz3trFE(kEQHJP%Te8z3hKSrhx7`p1R*F0IhAT>WgUrDsYNYIw8|)D;rYhyV z-j7mv`tSv#L^b1hDt-{zkL0%>Z|h?+Vh4YVcS5DOR1aVOueH2;LT|Z*0Pn{;xJqu7 zCet15k(2W!s*>l3=5nKQqT(#q=Rw&0*WFgH$~E|59@Pnb-Yo)ADiI{>)Bbz0D`OjU z89z*VK=5PAsx7D(`fGb*Tq0_8E!se7&;%+eR;|s@Q<~wLS7Kev