From f7f27976a10eab719f5ca0388e943e4372600996 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 26 Oct 2024 08:43:40 +0000 Subject: [PATCH] Deployed 3dc0de3 with MkDocs version: 1.6.1 --- .nojekyll | 0 404.html | 609 ++ articles/benchmark.qmd | 144 + articles/benchmark/index.html | 887 +++ articles/get-started.qmd | 179 + articles/get-started/index.html | 923 +++ articles/images/F-top-terms-learned.png | Bin 0 -> 152926 bytes articles/images/F-top-terms-true.png | Bin 0 -> 149010 bytes articles/images/L-learned.png | Bin 0 -> 126593 bytes articles/images/L-true.png | Bin 0 -> 123618 bytes articles/images/loss.png | Bin 0 -> 49292 bytes articles/images/training-time-k-10.png | Bin 0 -> 57600 bytes articles/images/training-time-k-100.png | Bin 0 -> 56754 bytes articles/images/training-time-k-50.png | Bin 0 -> 55803 bytes articles/outputs/benchmark-results.csv | 49 + assets/_mkdocstrings.css | 143 + assets/favicon.png | Bin 0 -> 12261 bytes assets/images/favicon.png | Bin 0 -> 1870 bytes assets/javascripts/bundle.83f73b43.min.js | 16 + assets/javascripts/bundle.83f73b43.min.js.map | 7 + assets/javascripts/lunr/min/lunr.ar.min.js | 1 + assets/javascripts/lunr/min/lunr.da.min.js | 18 + assets/javascripts/lunr/min/lunr.de.min.js | 18 + assets/javascripts/lunr/min/lunr.du.min.js | 18 + assets/javascripts/lunr/min/lunr.el.min.js | 1 + assets/javascripts/lunr/min/lunr.es.min.js | 18 + assets/javascripts/lunr/min/lunr.fi.min.js | 18 + assets/javascripts/lunr/min/lunr.fr.min.js | 18 + assets/javascripts/lunr/min/lunr.he.min.js | 1 + assets/javascripts/lunr/min/lunr.hi.min.js | 1 + assets/javascripts/lunr/min/lunr.hu.min.js | 18 + assets/javascripts/lunr/min/lunr.hy.min.js | 1 + assets/javascripts/lunr/min/lunr.it.min.js | 18 + assets/javascripts/lunr/min/lunr.ja.min.js | 1 + assets/javascripts/lunr/min/lunr.jp.min.js | 1 + assets/javascripts/lunr/min/lunr.kn.min.js | 1 + assets/javascripts/lunr/min/lunr.ko.min.js | 1 + assets/javascripts/lunr/min/lunr.multi.min.js | 1 + assets/javascripts/lunr/min/lunr.nl.min.js | 18 + assets/javascripts/lunr/min/lunr.no.min.js | 18 + assets/javascripts/lunr/min/lunr.pt.min.js | 18 + assets/javascripts/lunr/min/lunr.ro.min.js | 18 + assets/javascripts/lunr/min/lunr.ru.min.js | 18 + assets/javascripts/lunr/min/lunr.sa.min.js | 1 + .../lunr/min/lunr.stemmer.support.min.js | 1 + assets/javascripts/lunr/min/lunr.sv.min.js | 18 + assets/javascripts/lunr/min/lunr.ta.min.js | 1 + assets/javascripts/lunr/min/lunr.te.min.js | 1 + assets/javascripts/lunr/min/lunr.th.min.js | 1 + assets/javascripts/lunr/min/lunr.tr.min.js | 18 + assets/javascripts/lunr/min/lunr.vi.min.js | 1 + assets/javascripts/lunr/min/lunr.zh.min.js | 1 + assets/javascripts/lunr/tinyseg.js | 206 + assets/javascripts/lunr/wordcut.js | 6708 +++++++++++++++++ .../workers/search.6ce7567c.min.js | 42 + .../workers/search.6ce7567c.min.js.map | 7 + assets/logo.png | Bin 0 -> 8339 bytes assets/stylesheets/main.0253249f.min.css | 1 + assets/stylesheets/main.0253249f.min.css.map | 1 + assets/stylesheets/palette.06af60db.min.css | 1 + .../stylesheets/palette.06af60db.min.css.map | 1 + changelog/index.html | 842 +++ index.html | 778 ++ objects.inv | Bin 0 -> 418 bytes reference/colors/index.html | 956 +++ reference/fit/index.html | 1090 +++ reference/models/index.html | 1122 +++ reference/plot/index.html | 1273 ++++ reference/utils/index.html | 1180 +++ scripts/logo.R | 20 + scripts/logo.sh | 25 + scripts/sync.sh | 37 + search/search_index.json | 1 + sitemap.xml | 39 + sitemap.xml.gz | Bin 0 -> 262 bytes stylesheets/extra.css | 52 + 76 files changed, 17626 insertions(+) create mode 100644 .nojekyll create mode 100644 404.html create mode 100644 articles/benchmark.qmd create mode 100644 articles/benchmark/index.html create mode 100644 articles/get-started.qmd create mode 100644 articles/get-started/index.html create mode 100644 articles/images/F-top-terms-learned.png create mode 100644 articles/images/F-top-terms-true.png create mode 100644 articles/images/L-learned.png create mode 100644 articles/images/L-true.png create mode 100644 articles/images/loss.png create mode 100644 articles/images/training-time-k-10.png create mode 100644 articles/images/training-time-k-100.png create mode 100644 articles/images/training-time-k-50.png create mode 100644 articles/outputs/benchmark-results.csv create mode 100644 assets/_mkdocstrings.css create mode 100644 assets/favicon.png create mode 100644 assets/images/favicon.png create mode 100644 assets/javascripts/bundle.83f73b43.min.js create mode 100644 assets/javascripts/bundle.83f73b43.min.js.map create mode 100644 assets/javascripts/lunr/min/lunr.ar.min.js create mode 100644 assets/javascripts/lunr/min/lunr.da.min.js create mode 100644 assets/javascripts/lunr/min/lunr.de.min.js create mode 100644 assets/javascripts/lunr/min/lunr.du.min.js create mode 100644 assets/javascripts/lunr/min/lunr.el.min.js create mode 100644 assets/javascripts/lunr/min/lunr.es.min.js create mode 100644 assets/javascripts/lunr/min/lunr.fi.min.js create mode 100644 assets/javascripts/lunr/min/lunr.fr.min.js create mode 100644 assets/javascripts/lunr/min/lunr.he.min.js create mode 100644 assets/javascripts/lunr/min/lunr.hi.min.js create mode 100644 assets/javascripts/lunr/min/lunr.hu.min.js create mode 100644 assets/javascripts/lunr/min/lunr.hy.min.js create mode 100644 assets/javascripts/lunr/min/lunr.it.min.js create mode 100644 assets/javascripts/lunr/min/lunr.ja.min.js create mode 100644 assets/javascripts/lunr/min/lunr.jp.min.js create mode 100644 assets/javascripts/lunr/min/lunr.kn.min.js create mode 100644 assets/javascripts/lunr/min/lunr.ko.min.js create mode 100644 assets/javascripts/lunr/min/lunr.multi.min.js create mode 100644 assets/javascripts/lunr/min/lunr.nl.min.js create mode 100644 assets/javascripts/lunr/min/lunr.no.min.js create mode 100644 assets/javascripts/lunr/min/lunr.pt.min.js create mode 100644 assets/javascripts/lunr/min/lunr.ro.min.js create mode 100644 assets/javascripts/lunr/min/lunr.ru.min.js create mode 100644 assets/javascripts/lunr/min/lunr.sa.min.js create mode 100644 assets/javascripts/lunr/min/lunr.stemmer.support.min.js create mode 100644 assets/javascripts/lunr/min/lunr.sv.min.js create mode 100644 assets/javascripts/lunr/min/lunr.ta.min.js create mode 100644 assets/javascripts/lunr/min/lunr.te.min.js create mode 100644 assets/javascripts/lunr/min/lunr.th.min.js create mode 100644 assets/javascripts/lunr/min/lunr.tr.min.js create mode 100644 assets/javascripts/lunr/min/lunr.vi.min.js create mode 100644 assets/javascripts/lunr/min/lunr.zh.min.js create mode 100644 assets/javascripts/lunr/tinyseg.js create mode 100644 assets/javascripts/lunr/wordcut.js create mode 100644 assets/javascripts/workers/search.6ce7567c.min.js create mode 100644 assets/javascripts/workers/search.6ce7567c.min.js.map create mode 100644 assets/logo.png create mode 100644 assets/stylesheets/main.0253249f.min.css create mode 100644 assets/stylesheets/main.0253249f.min.css.map create mode 100644 assets/stylesheets/palette.06af60db.min.css create mode 100644 assets/stylesheets/palette.06af60db.min.css.map create mode 100644 changelog/index.html create mode 100644 index.html create mode 100644 objects.inv create mode 100644 reference/colors/index.html create mode 100644 reference/fit/index.html create mode 100644 reference/models/index.html create mode 100644 reference/plot/index.html create mode 100644 reference/utils/index.html create mode 100644 scripts/logo.R create mode 100644 scripts/logo.sh create mode 100644 scripts/sync.sh create mode 100644 search/search_index.json create mode 100644 sitemap.xml create mode 100644 sitemap.xml.gz create mode 100644 stylesheets/extra.css diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 0000000..e69de29 diff --git a/404.html b/404.html new file mode 100644 index 0000000..e9ef583 --- /dev/null +++ b/404.html @@ -0,0 +1,609 @@ + + + +
+ + + + + + + + + + + + + + +In this article, we compare the topic model training speed on CPU +vs. GPU on mainstream consumer hardware. We will compare the time +consumed under combinations of the three key parameters defining the +problem size:
+n
).m
).k
).Experiment environment:
+n
) grows,
+ on both CPU and GPU.k
) grows.n
and k
fixed and vocabulary size (m
) grows, CPU time will
+ grow linearly while GPU time stays constant. For m
larger than a
+ certain threshold (1,000 to 5,000), training on GPU will be faster
+ than CPU.import time
+import torch
+import pandas as pd
+import matplotlib.pyplot as plt
+from tinytopics.fit import fit_model
+from tinytopics.utils import generate_synthetic_data, set_random_seed
+
Set seed for reproducibility:
+ +Define parameter grids:
+n_values = [1000, 5000] # Number of documents
+m_values = [500, 1000, 5000, 10000] # Vocabulary size
+k_values = [10, 50, 100] # Number of topics
+avg_doc_length = 256 * 256
+
Create a data frame to store the benchmark results.
+benchmark_results = pd.DataFrame()
+
+def benchmark(X, k, device):
+ start_time = time.time()
+ model, losses = fit_model(X, k, device=device)
+ elapsed_time = time.time() - start_time
+
+ return elapsed_time
+
for n in n_values:
+ for m in m_values:
+ for k in k_values:
+ print(f"Benchmarking for n={n}, m={m}, k={k}...")
+
+ X, true_L, true_F = generate_synthetic_data(n, m, k, avg_doc_length=avg_doc_length)
+
+ # Benchmark on CPU
+ cpu_time = benchmark(X, k, torch.device("cpu"))
+ cpu_result = pd.DataFrame([{"n": n, "m": m, "k": k, "device": "CPU", "time": cpu_time}])
+
+ if not cpu_result.isna().all().any():
+ benchmark_results = pd.concat([benchmark_results, cpu_result], ignore_index=True)
+
+ # Benchmark on GPU if available
+ if torch.cuda.is_available():
+ gpu_time = benchmark(X, k, torch.device("cuda"))
+ gpu_result = pd.DataFrame([{"n": n, "m": m, "k": k, "device": "GPU", "time": gpu_time}])
+
+ if not gpu_result.isna().all().any():
+ benchmark_results = pd.concat([benchmark_results, gpu_result], ignore_index=True)
+
Save results to a CSV file:
+ +Plot the number of terms (m
) against the time consumed, conditioning
+on the number of documents (n
), for each number of topics (k
).
for k in k_values:
+ plt.figure(figsize=(7, 4.3), dpi=300)
+
+ for n in n_values:
+ subset = benchmark_results[(benchmark_results["n"] == n) & (benchmark_results["k"] == k)]
+
+ plt.plot(subset[subset["device"] == "CPU"]["m"], subset[subset["device"] == "CPU"]["time"],
+ label=f"CPU (n={n})", linestyle="--", marker="o")
+ if torch.cuda.is_available():
+ plt.plot(subset[subset["device"] == "GPU"]["m"], subset[subset["device"] == "GPU"]["time"],
+ label=f"GPU (n={n})", linestyle="-", marker="x")
+
+ plt.xlabel("Vocabulary Size (m)")
+ plt.ylabel("Training Time (seconds)")
+ plt.title(f"Training Time vs. Vocabulary Size (k={k})")
+ plt.legend()
+ plt.grid(True)
+ plt.savefig(f"training-time-k-{k}.png", dpi=300)
+ plt.close()
+
Fitting topic models at scale using classical algorithms on CPUs can be +slow. Carbonetto et al. (2022) demonstrated the equivalence between +Poisson NMF and multinomial topic model likelihoods, and proposed a +novel optimization strategy: fit a Poisson NMF via coordinate descent, +then recover the corresponding topic model through a simple +transformation. This method was implemented in their R package, +fastTopics.
+Building on this theoretical insight, tinytopics adopts an alternative +approach by directly solving a sum-to-one constrained neural Poisson +NMF, optimized using stochastic gradient methods, implemented in +PyTorch. Although this approach may not have the same theoretical +guarantees, it does have a few potential practical benefits:
+This article shows a canonical tinytopics workflow using a simulated +dataset.
+from tinytopics.fit import fit_model
+from tinytopics.plot import plot_loss, plot_structure, plot_top_terms
+from tinytopics.utils import (
+ set_random_seed,
+ generate_synthetic_data,
+ align_topics,
+ sort_documents,
+)
+
Set random seed for reproducibility:
+ +Generate a synthetic dataset:
+n, m, k = 5000, 1000, 10
+X, true_L, true_F = generate_synthetic_data(n, m, k, avg_doc_length=256 * 256)
+
Fit the topic model and plot the loss curve. There will be a progress +bar.
+ + +Tip
+The performance of the model can be sensitive to the learning rate. +If you experience suboptimal results or observe performance discrepancies +between the model trained on CPU and GPU, tuning the learning rate can help.
+For example, using the default learning rate of 0.001 on this synthetic +dataset can lead to inconsistent results between devices (worse model +on CPU than GPU). Increasing the learning rate towards 0.01 +improves model fit and ensures consistent performance across both devices.
+Get the learned L and F matrices from the fitted topic model:
+ +To make it easier to inspect the results visually, we should try to +“align” the learned topics with the ground truth topics by their terms +similarity.
+aligned_indices = align_topics(true_F, learned_F)
+learned_F_aligned = learned_F[aligned_indices]
+learned_L_aligned = learned_L[:, aligned_indices]
+
Sort the documents in both the true document-topic matrix and the +learned document-topic matrix, grouped by dominant topics.
+sorted_indices = sort_documents(true_L)
+true_L_sorted = true_L[sorted_indices]
+learned_L_sorted = learned_L_aligned[sorted_indices]
+
Note
+Most of the alignment and sorting steps only apply to simulations +because we don't know the ground truth L and F for real datasets.
+We can use a “Structure plot” to visualize and compare the +document-topic distributions.
+plot_structure(
+ true_L_sorted,
+ normalize_rows=True,
+ title="True document-topic distributions (sorted)",
+ output_file="L-true.png",
+)
+
plot_structure(
+ learned_L_sorted,
+ normalize_rows=True,
+ title="Learned document-topic distributions (sorted and aligned)",
+ output_file="L-learned.png",
+)
+
We can also plot the top terms for each topic using bar charts.
+plot_top_terms(
+ true_F,
+ n_top_terms=15,
+ title="Top terms per topic - true F matrix",
+ output_file="F-top-terms-true.png",
+)
+
plot_top_terms(
+ learned_F_aligned,
+ n_top_terms=15,
+ title="Top terms per topic - learned F matrix (aligned)",
+ output_file="F-top-terms-learned.png",
+)
+
Carbonetto, P., Sarkar, A., Wang, Z., & Stephens, M. (2021). +Non-negative matrix factorization algorithms greatly improve topic model +fits. arXiv Preprint arXiv:2105.13440.
+ + + + + + + + + + + + + +Kw-UweE zNt{bRKCV$KV}S2-<~@Xc98GyZW5t7ajia>uX!sD%;$23#CgQA|M6h2UT#c?$!L!!B zygjb{;MPt5HjkBJ%cEOf^XG~_f7V@u>J0`=Kd%)v=y8f>hrt!`P8}56x_3t99r`Z^ z-#xz9TlH;7=J0+&QCtpkk7^ XXZNi@Y8N9>-DI#?;Mb>y)tn&0|)y%e8}vDV?oKe?`7iF7wUS26_v4 z$+y 6)||7km#!yA zD|y0TwlaRMqan|O?GyybZas>IX3AYkTvT1ciGm;#5ku!Q9+7=WtvADmRRzFAgm0RT z6b*|0DJ|C>ZX&zD`q!8s1LXEZ7)ewD;T;rblS1ghnn5O(p$!)MZ(v~ndz=`ram z=Yqy;%4EoKlIWOgnDnX#-j$Rn@bFcO#z_YF+r)wF?kaCLgts5l4*8v5EJn%H`5VLw zXxhuguB(*-Xkp0^Uy&TbEh+@9{=^mMpmxsqhnw{$jFn18TgH*NK3D{k*{A8x;%J#} zr%n#EQIzSkTO~2xI*hD;eW``;mSA5~FB@~Bq{Zr_Y?TRifFHv0$8}!zpG2>$WfO#G zubb1Fy%P1X|Iucai`;8TV=&_*#v(->wQf?jLU(;8!@Br7I0h=heh*yaaOTOWo)#Lb z<~b!Qh?tZ=Iin(W!{hFX1*KxiHfv|<=fs_9( 5+$gaN1z!Nj36rp zPyFQKQ6pRwRJnVcvDM@;lFWvQZtLdlL|b8L1^U@Amlqy9cwDQbbtIv|Lp0u?UGOqe z%U3uH-29?dfwlV7-h5kAm5bD%z`8aD-Imc$7aa9#qxd229o+hZ9$h$XK4~qCX3=q3 zT3S}l^7FB7yo#9}&+``d!a-erWO!XTy1SyKhfe?P9K<@@;xPCL_^|$@M*5Q0hapTX ziwLmysVT(iJw2QQ9|>#(5bBseKPZfeff8@t(Star9Y*?Yw6QNKF6mZB>qX$+?G&RK zIE^N@s9oxu2Dfh*hJ4#Z&7e_D?JUz6BH!M)iv}Gt9fX@=iatkF1-}A+L%?pF30fTF z=kV3 dkeNO{9)nBJx9pY4Ni9r$;#_t=(fGr>O?!nKH4n8Kha^(DZf4us@tv)OF zRU960HICoX0JyP9I_6-T3SV_%kCOVmY~#sf-X6ZRYEKI&29AV+Bd^ApdwL=#gyHv{ zZ=IOa&}Ep9JNNI&lA;3IFLV_%SNw8IvNyi$b)%)|kl*HItn(tQ@=Dcm)t&u#`{SU; z`u{9p(fzuA#St2bbE`1W>l1zQK#(DQEKn@tVBwV;pp0}>MRuHntY39ap*;^Mzu7}l z-W=*#`doDW@?J8a=6x{9iW6=rmrTrOs@D(iJ|+aPF6;p#|Nr|rd?S$hrLL!)%;!Am z_Z8@u-+mw)r=|_o=~L{rL(O1ejPVONo$y$N-hqg`#=wg4G47VR5Z8QAivP!NFR$2g z+w0mK#SJ%vQ{V+V(oR2X`s8&M+qQ!&=q_*Tb2JVvjyRRBvx{Z9`s#ex)(i&iZ z2H)%xT#P8;zt!+imFi~+(%CaPg4(U4AE0w|eey)nnJFr;;naM;=95vAO+)U+3d~42`X3ql#;e)X>u*8FxyZGN zTz#KBVKfAO1w(8W3#QsVj)Mez*=JhE-1zJ&Q`Nd@ifum^(!vh}o*eubCGTy5^V%zB zI=VHG4G7*wQ@ptWIoWR6;D6_bbdxB!4ql7Ep}rH;atOo>u08&!-itvDy-)C5>@V0l zt(RDTe& `xw~bMI(YiersL)bQ(p$ zpkp(-BV~j-()Z;Z7dloN_OYe+X3Oc{_*2t}tH9}LJHHcWje5qT4t_dGBMSI$H(oR2 zFVa4FAvzx_%aX!y1(L$j6&ppnKhAq|>}YbUz#83R*qMY>e 1LugJs1$@&-6 BZ@RVw>3jSk@L4|H4xsVezXZKPdlUTZ1k8DuA)U;f~bxkdfE?q zC16Hf!8 Ar 0rVAvkZYVVfawzlnjY<@dUP1eqM0@` ztS-tP$2+?oPFG?+!iTH&-YZ7pHGhIeHaeu{dbGLb0wS;`1b9&mKNvT;JFN3c;2lT; z$$?%-Xa(<&Q;BHY2HPJv7=q(_e-;3`aeRV)TQ?b1VBSRxaiC14@UQ;YF@ni29Srz6 zs}Iy(d^Z2$`TLVGa^`HM ~IKWoW?m+nObY2@|)_4aP`p zZfL=s?vq`)eg7_;O}FrqVVOk9NL#8E#mHZBi$@sN{qM!GFj6dqB0qKn(iKVrd371= zRDjIa`xt)#z6s-MSn#- j$UBOHj&0u%%U3NteyB9 6vCrXL-XB^RF+2iz}N7*F9%MM1boUJZ<}L)Yvf622SL#02+=P zkIQ%7Czbd0Gq?NLj1`xPdp;0C%>=B6qn>Vz_NLA+ZfhFwKgB#g1|gs271iKlmYzc} z?<*e$Y_BhXuVQw(tl&{HQg0=azTEf88{$9u!uHN?A>jg?FH!YP7_Q7vxca_TGEZ!U zeu+N6&GeKQUMnrSXzVeT6M3c;y3Ngl8^8Ny{k1U}o1j@Xg;bkP zzQ&4$KWVzp#|zW;>EopauMwJ IRT6+{=!xDjwl}Z7FS6 zr{v4CMa@~ZbbeylLfO+JY5q9054igd^Bx_RNF>uAFEY~UJu7mq$D!xhH;F5Huf!e* z+-8~9P9dvA4!q_h67{0E$UjK7wYh mz;Mtz-xvrXd%{*`jb*nFQfdXD=#D#tt^NAI_zevIcY4l?g z)bD01Y%e}70ZFPH#F(dv$~9{C@!KoBT32O&>t;+lMai!*SPT~@NtHr;M|TBBisd|t zYHtv2s4;2=VY0|c|6dxcjhZd|_Wi~3J_(unxJJ#X;+nd56m=>!O_wBkY2eq;w3_%c z#==!s10}&lgx-SPbyGrVjZ)R!Vq))D^-69iE5(|T<34>(q;+N8WPh-6nM5KwdR`U{ zvUQ9d&WLv2jQ6#7^BKtn4Q7BkVO4AaXT2uQ{w!8OYEraXmq$uwsASe%j9{DUDE^EK z9t_kUG4CBMOFMm!H+c{-OnH}Y-@$3>g|H4>byD)N)36q@SH DSvc7wGiibx?r>X?G?!+92Nkz_Z1bIg zo6YQXZ*CVcz}w7bHTz~hg5-P?oNpVW=ukch4 f6UM3YQKk26GPTJq!G)Q?`7B!w5rj9{)gQb^2$ zHDKU<8>C-cMofAo>#Pnu6&M-@4YC#PaYMA0S~P|(-)btZB6>Di)cVbIDo7mce%7B6 zu)uh`Qo|ulJxSh!V}@T|fi(QigLkNc&cS!bW;Iinj(#V}o!#y73z-e3SC1xxf8*b# z`gNm#Vw}T`1(1IP`=m#j^A|M<*&hpU0&P{f zCxd^S^y@|s_x>NRkSv 8Zg=ThX^@jF z0TA8G9L~*n34EseW9TE8{vS3;{`gDgJc&I(-T&~JAJcmCX#|BL64t)cuW-lS=f*kW z5p|%;h3~~dM9KiGZ)3&8Q>Ulzaf4vA7VHtsUKo>yL?r!f{%wT*&n7U&_GdC_RPOV? zlm9FGkFx(ul`^3F|7!ki{3}Zw0GAR?#wU2wP6rSDmNfjDERO3Xx&H!Dcmr}#i&^PT zt*hw=kYPuQyOyl1DlPm1d}rJ?Or7&Bq^AH#@|}dI_!u j;k6H;e#Ziq zg(Xos2sFhKiy|?dlVj}}-VcoaLcvZHmkLufEa*gzC>`Z5{Yp%*XzU+Ul* #n3o_4a>pmT)vPz^hR>5GHVQA96r(Fe6M}xYz+1a zwKrUkRW=ft5x|eh3m~_?EG`~UlV_#;bzsfv3m{uKtS}=ulVT|Rs0>^jt_47m!8w%c zzVCT5tufpcdU(2(wH6Ao1$f0~;8?2^=;F#bF?od6PGJeGU`>ShQUt$Nay=r59Vrhy z?y)>m2 VW#4X9FHZcZKI!&Lrc__>2#8%Y8z@lL?SiSqNeimT4FA4F$5DBS?r z-Zjr79I4y_@Ess91=*|E*yQ8{vjhy_LgLeyH(5I_Q%7Gn$0QgWGB2{2*K6V9^lRqr z54o`;kk(a5pZ9$dgEE?Ulh9;fh4!JQBXX%ayN0Do= dhT-{y??YG%K;i6r zT%>wC-Vn7O7nzv(d-_W=_8noL?|5e~N#^Glgz%V0JZ`XwAMG{l=hGMH#(na(ZzXm_ zI8!bGH+cFw%7`%WV%+>)dwlM!0-1#O7}jbRUNY;yXZ|IwRTJ1S*ekFX7iRmQ8LU|{ zOr)iH0A;5Vn09qqU3#^Ya}yCkCgPR{lr0{XMV`_$_6iT}gP8E=bme8KGt(myVER$% zub{HARg$Kb>m c|9NK3c6CMrMR7K<-!!$aDQ^X7e9C3i )8qFgip#s!^Z)}0QG^PF3u z6bnEyHid84DY}k)t!XR^JSbmkh~3Z|!iF&T1K!EhN7qP`NNZ|95N|>fPTObFCAi9# zUfx`ZH?FZ=t{#kjQ4vPfahIx^C#Q6LdW*VmDw&%{2uswt#C?15+_jGda^1ZL#9xUX zzefw_K~kA%Y3I?(Fo
A3Ib`OZ)dx>f z&zOm+VPjQIJT|mRiYDx5Z>-GvAtNFbhs{M!Ti*rV$lEvPHfg=Z4a|T*N7ELC<@}t} z>BZu|O|Jp#%r31K!0Cz^BDZRU0d1IdvoHtmikkSzbK@@lm4Xbu!@%v^FYdG1atPuT z^rXD45)6us!{yEG(*vqq*7zzb_E^%jxg?u5Wrz+>GOMXD*~nQp4 }u z@$MJy&OvjpecCElM|l$-cG}@Pvdrs-Hs;R5Vy|!mr^-+l{3l94PywC4WV!ZJ2@hUz zD>EbsSADK{>=!$xbqUjS!=V{CP$??ROUl@n9HUm+49${51Sv~N71>Cs@LWe5)|>;B z%{X#}Qa1GJ;u6UPb*UGWk0X{JZw**J_i12>ezPR`jzLmZ{05pL8i#3bs0&80Vj2#- z31wsL+9WPff#$e(kP1GGA$oM-!{HniV52DiP0^ZbSX!d1pofC|O)(oeKZ1i#)n!aJ z!Mv-}z)h(yXBTF(;b#sgKpH!Lg0~mWJa!(&6O5!xd3Ig|ObSLc^mFIi?T#Z7F;B*i zx$~M7@xM?_X#;)3jsj(R15H_(O-XgqJHJ1zedGs}sE@=^aLIzoo^LbzdXBzzbH=|! zo0nw;e1pz1y<1s^LNvRsfL2X)!%lCCF3=fp&vbv1eQP@u$Ojr=L-%_O=XfH-SSzOs zp8x4Eci?i6 A4CB*)Rq-4_jcH52i z7yVfTqcXrDnQJ_ClQz!+=C?+?dT#*yC8Aj}JDk@9)obdw0^V*YvgN#g?4Kwf6?Ik| zQsmp1r-;uQ1cwQW!_bjGit+zMY-VABIoVrPf1+((^@!zz5~}@+j{s_%ph2F6G!oF! zu~_Mu4|GP<3s3@MV1Y6WN?fk+G^)=kOV!zk`@RkkfiJSzo08LVIa1c@Rx?SazZ=sd zslmkcIOHCH$#6E%+TBWKdW$Be%3g|PVMWQ{i8N*fb`;1}hP+ho9?(Nd7i C(2?Sza!Fm`x#TIpnF$=1LMkROU+DEfut572-k!)bmW*fB_WG cw!jke~S{Cf-^5@u3;Zj7oSi)M B9OzU0_~Zy8rkck zRX5!$lwYLYIaal9TCaJVq?0>woBJsOR4nLy)HN;$ZhS8s?B*PsEQA44D=?=5EgSI# zQ{`{mIaT8I5dUiHz56W)ciH-MPW51UNmNK@KY!b;p!sj 6W`DJhnvvIm@-j@PU^*__zw`sl`yx$q|tPN8si z4(48&z6K}|lh`L4ZkmLq(-;HoGzLxzBVok%vamr#Qbnq tTe;%PTc7pDeMryL1DXdSGa5 z1#(+~%ZR*8gSI|VPc7BGi4;`-9z#-7_Hn`2FL~$0Z{=@+83Z@*3x=ezap#gyfC6PP z%NE)_k_3m6+~`SMdOy$$;{&Nz$t3Ba3qV7xPe>hk0hXs(z^T0CmO1){=Rs{m=pHmX zAj@9itL~HZu#pfgdJ+sBe8d7GRlV-TMYJ-W$(RwI9-We!5Y=wx4(Mo3dOE{ewRm{; zPt8Xp1s5cuB8;tls$_g$DRnMP8AI8hpu5M=yp#O9HUrc^iZs8$vBU(`lgc?Y2CJOw zD>-jg=jN!LN-EfL=k_)Wz-nO~Qozi>^}RQb-9X_{CyKCm*-B^P6sul@b1LlIY8Ccy zodxiE>EhIg`6T~dn*j#E62ysFqK7EFfWM%Divi*2U6xW8=hJr_HXfIIh;IAj?l4 f zBA!8D^M;wE?DG`j4V$~ Y~`;MhZH>x=aIS(qE-71WZLg`x0zacPyk zL*uZJ4VWxSfw8tsrH(ZSwG<)YR>$}2?7xTx`F?B}Syf)M%E|Yz+^5BmtKnO@my|6X z g+799>uIplX*yn^nFb3e5S;z5xnJ_A)ofOgI_}&z2hriw(^7gQK;7 zjL!cw(r$j)>+3*)puL9K4-@M`85H!`*jDA7ggz>&EwZ%V2jYu(5kRAPw@muPt~40& zV&EK!ID2-qPzu3L#q!t7Q&o(ab%Uv%P6$ph2YEA=-N8XH$A)vr!0vj4cAY_4pPvA5 zH8>SSNETN;WAZXCdikrAbwK_p&^CMAfbiF<^LQb1a2<$qBCwgtN%_eT`^Odh7-eMF z>dX5(NSC_z{=m~QAH7F+#*Lutz*hsR%GoLgW*S}6{dzfoK&XUf2FmLumme4D!ZYzq zu}q+ArHZ50GFbtxP~!NMsN}4mBzN`K9<~JaV>GbQgaMq*>g&c(VY$?ZjrEWNG!4*0 zFd}+*dmnMi1!UvCrcIXI+>gshrNRU&<3koIUiM!@0XMpXkQ01S_yHqP?2!l^0J)|M zlCdn2nits@KQk%QpEupxwPrC116I!R>d^4IaGztYzxv>FaPG3L{d-~ -9FF@>v;(!qR(+4#20k8e1BNqOjhT(r_oc}9w{hyh{egyG)P8eHV zPR9+^G9EYJ5308l@R6VN&Q0|L=@rWNQ!ttXv>0K5qKU9DSd3UTm>T?N@B@DYHg5H* z9BC@>&VnSM2>Z`FpzlwQA+NLj^*RpF^}ox6vHel_PtT!zI2-ED-!Wj$V#FKVb)dZ% z;ibPA@poCH2y { zxB}rl-~a_q?zFoMvqV_bAr>IbL{sYja8(Cn$3MiaQT*jGgS3D9f0D{g+8+MHFbddO z|7qy`cdF~ZqX74ZRDY*-l6G+aOeJj{{%N@V;{alCokM?!dul?GuGf0kaTcP%T)I#X z;`79LOK5f&fa879m}p*DXDYRR;D6dLL$t;A)L&bBeNQ{H&UpBp?X0Bfbprzfeb?{j z$2JTsPKlX^1-}!2pS5qU+o&n4mwDGIwU^Nqr 8 U($>{Hb*M7~Y)~S d! zfTzMS>A0s9H_sukpmRIhQ~P}Dd`*`DO~kgc YqP7G5fCp5<+iae4VF}x<+WG*4V;X8bqw!B}g7m>_Zrk z`VDr&rz#EM(du2<1bDe5Z%Wg@2e>` j^)BdB4?JZH7tbmIs`K=Hfl73@y_3@kR(Hc~f8!V`uja!xeqYnk3 zmfSq +|B-erA0|9)J$t0ogj#6Ic9R8V_ zpkPt^0P|+!B0zQbaq)%BIOe=}ANHlQz>=_0nAa^48X#^mW)BRmdqT(% X;L845b<)ixX;vG6oy`U%qkDjv# zns9iQLbKQ*q&E9AD=ENd 4Csf1duguWMp=$3CbNL0 z$T3dcCiIc5ItNydx>utxlz?Bcu1_3!t*v rfY zJPV+a{P|A`&Z<#UdM0KQES6ob49I`}DV)UHJ{9#_=webnd34gB#Zn`z;G$Hy=-b0@ z7%#BEPE~ACyXOEl@sm9U6zyO6mKZb4;WFR%FCl1|(0DvQ>cU$b-Ga@m_Y5g1@-_{P z X~s0i4{v-X`Ho@-6k~iRJ7APX9(ozz_H?Xz>^VzBH(4pR<~0cpTm?q#u;;wZ zBu&aSUzx1?FImr2M_mfvn%~hscPQMS$)n}Wcm~tThm_S=jK$+cS)H#VyvYGkVjm=2 zjj4!f!METXSd>($8s+#jVHGzAstaow{!{{l9$rmXhPj4F!lGr~UnX)j{1Q){tuxk> zTg$eaI;358Lo=R7-sCK>ctA9nz(SJ !m@ebAe{ z>RJUnST>)b_>Hr0lE8uPEU8|+niiLn@}$hN(TJD@xF@&_$V2AVl^Yh09&4$Q>f2Wa zV*@c|BjYZwnK=V~Yv4>gw9jn{cKR?XGWHCB+j;Vdc9g^Lq&?S<==P+(D&OzwyB@Tp zqIv*IGv-abXT7|jFEnS}lpvNMp86=792k 09gVu0Lez>|`TcIW<1jBC z(Do3v5>ali=a=BLWt% k?A3 J?Nr*B`d7Z&s zs}~SOTuD=>V3jTiiEo >fmHI6pfZ!J<0j$iL8aT4;; OieHFhM VvQI5fBog{?dFhVsArNZZj>9vwNDpP0S zq-tv_V-Vl-^agsxOy1j$RMq(hPF3AO@9&|&tOs14=YepP@ve1GlXtkOBR (Fis= z)@mRJ&@Yudh2$Xx9N<49eF3ZGAE0S@%kPP7tHC0IbVwokt6H7A&!N13edc-J;&aQi zuWE+SfLyUv{})3$#M%b`Irf%HyZ#obj;eMMvxu*3J#Y<9jGkThJN@CnY=-5`O`G}m zCef+a0IotQF$6+_)zP}yTr%qj al*G7v4*Oje;qwTis@Z4ETKUia z>>0pUE`B#ITSN`=PWSWFn+bKUQUSrq5XJX#xYuURcd+jb^vXng)WaXID!3i`93i9Q zZk2peMp*%m@+3c>ELRxbHILEh5KzWT;#)s(zpiUPgP^5DYsA^KodKxcJJH=LoIcn%RHLw`gI)Vmy3;>)i@()(&=F=% z8ZA~#%`OvkE)y$Jv11jbo5%*p=0mDY1l$hIEg-b;XtJ-&gDo?=-T{?47pTnFq8%pq z)t10Tzw=xh)Mz<@58DIawm^Ehpyz{p(C`0;ukVg)YWem(RumLOno<=7q>FSEkS-w7 z2_2;qqzh<5Q4#4NA|SojKthW&14jXA(g`JWkWN5asDU@(l>58)-S__BL$deGS~IK9 zcV_QgL#drOU!~K-r?EbUSFdFmbDkrK=H=t>b*DJ5s`kuve@gW#xVHDkN`_X)g|nj6 ztS*L?r(33?_tt6Nurx~~ue?X3;0|pXDJbR~>Ni^Hwis%Z(F`-c!1u!G!5rgDJZ0)i zF5!(*<1f8EF&eyfpd8-a2$IMRCnbQp?UgL0eN4g=d+nzTUa8ehmE*T*ONIBq+!S~* zh08xI?X=;ys@mpN6**Sr&yeyaP-~y@mqb=o-%;vjX`5Lrp5(gDPEGA!H@KYNIXQzl z_=)23?yRJb3*vEHyZ)r%ow-i&)RS45JxoVybu#?8@7+r_C(#T6b!w7fN_rqE5)d zxR^1iOd1b}DVsf;SH;c_ MN)u7xe?_}nGviZ ?Y1Q>XP!UI?zmH6|PXojm-TE ztm7n`HJf|3+udd_A?e8GAhgysIfzA_GxsD9YFjF9>T9L@ZJUd6Bk%N?*1YrEo#C1U zW%#pLbFrCA_9-t%MQIaw1|n9erLmd(x{oRC+1M*~w!s0zc_y{QO#1;FoWJN=Z8s@+ z9KRtjA$3jxM#QVL-DfM^O{A_mXx&@B1#%8sx4 H79Q-fFxONqJ>3g?^5=YR zyJzg#Y%AE- 1BrA>sys+m}h8*StnNtHYXRWEgawolvQj#7;s-cr{Y z^4;D=gd?n#B)9fIoIKJ5)AzHgHK|D7>+JpI_VNbJL&RpNR%avI;w!jFwxMOUQ~f~2 z0XE8CCaj=*Jz01|f9Z0V!j_T4f@$;kgn_LOrR58fz&zr#mYQU0 syS!|8!GC3JQFq#yvDr-roUr;yXD@f*?c0?k z_AjAT=-TPfa*Vdy-tlqBy0!Eh#el~<7>`X~b5Yh_&%#4&OcWPa$N;h*KHmm$J0_-M z8P<2*JR6!HoS-K5f@+)A`AG=0pjST6EQRdmYr>mMV$P;OguULepWbgCkK4D}Rv;a_ z26$By)V1d;9edS%B;0Gg*Ze`w9gpqMBdU?7sQr0)j-27g+8wb&!|Pn )Kl1$8_VT=y^cbtG z%Z-nEvzw6i3ZM|jLU=T1>|!=M>}t|Z-P|es8Kr}k{Mb4Cwzge@CVvm?lW~#Wy+T qTU9TP 4FDhL0xD*cYMru*nP&>!qUm=`yj){X71e_&-YS{kew zODxTCttkK)jFKQmxX5!PVyLp$`q* 9 ?vMzcphxaX>j$@Cicx>{K9nM{O_y%^8yfl>5c;nXShAjALOogb z03ZSw6zQ*Nt #&C%^ml-h-pf zk+gqNt{Bo5;NL)w-`_CHFLGSp-wpl~+yR&WLCpS3h}{2lSER!IKWP2i=|2#mzj34k zSz9E1Ji!s~vx1x#t2!8X$wsmB7w&Tu6^$6sK-Lnh>{jL(qDp=4ryUF=PF@52I+g+_ zHv3+V86~gyTP)3wIF`1Pc86p1_xJcFkYmL)mtr%k(augUFW}d)@rAUtJiIWDsQL&> zbB3R#h?1?%chg0$>@~MYVMnx@+=X+yPB%*Re3I&O(c;Jo?2hLpK+S6mg;4e(51~6~ zj*c!LYcVPt%x5~YuEq{_Vlk$hZH7uk- zrGZy%pUu?yI#y2UPRSKNA1QVvY8c3{%9tv`3H 6I63>Ct zOXjBJt>2?HaY11lhFu^s`j$AP2{k*f ogy{vl!Jn{x8qfD z>C$zH$%KwgCbvQ+pISQZnzHxC>;_PXDL3PaUE@%~;{%_5 <92obrV9sOQ0S!QYqRmc3DLH)a*!f`J1lu}0an z0W?KKH>-t)+QB0M$6_azU|Q)!woOaX>CP8^YIPq!xsIuy!iS_r#o2b7YruN_C;{&% zT!i|v`OdV?Ot~aGs>vdZdCUh!Qin2jwp}tRQ_|B3r}BW(n7ZBK)xlguZ)OyTs(+3v z^A23o`B85dn188rM{GhiIccKs$E>C?FNFf!N7U%@tj+s3L#gG?el@Y1bmU>LLD^_8 z>!i-hy}qxC_Y<8~KG>Ke96h^48o||eD!OGPTl6RX!8Olul_#7)C}fouc%I1mT}5lD zv_5mwB-5l9AYJ5FXoe**Fp1?)@5Ifuhki8uD5W=S?4}XMLIbvcEi`mp*mdW~N^el3 zo5_3ckq%~9I5}isYvW%1V7}b*6N$x<5+ZL;?D$I41?nP>$~Dp3Nqf>K?8bt}s0q#a zFF3byiSI8M!Fs_w(|eB|J%5+KbUaQ2cTNto2>Bo_vc_Bk0w*+4idG#gg2GfC62=YB zACMyZC;pDYV7p+zVke)abRgDKV0s?I9-r|0nmya7pC>1g+uoXUj~m~Vx-iuU61-dv zW=Pzh?^U5KRfy7I&H5V6{5{`}W?K@VEL)VCPRv-B8?D4Viwfx=r~r%A9PTAKEex5t z!7uy6p-YrU(2}O}T{Bq95fB*_Hr<9%7Q5ku;au=A3=Ju@d?IkT&8*a|XH1UDrRlFY zhQND^+NP&?|aMZiE;SvY~OyTrqd_N%P_usOskiY#9q ?D6bpm%cI} zBhAZMZ}{kJsWax(p0TqPBOb2lJvGJ$*wCQ=W+M(48keHrnP{%(a#j~+XgXv)gAW)* zN%ac5$z#GRu*zA!o-@ufmcRDI&3yg0I=u>u=$%pCBq`)O&thN7?QuD1t@t}LsXz1B z2t8Uo2sRER=_WCjEwqm&>dbvH`g`tfN)7_86@wO+VpeV!`sI=1&oE+4=FgFaSf7Z< zpEi}JV?fL%5hGxQ>fV`qB;%{G7{jDZeH3aiXy1^IT#HcaSXjFv H9DTN0eUGrPhi{HH|74@uJR0Xt3kRByFdjlt{1wY2jOIY+O#^rX`xlgV9p|syM|_ z&lmS+9Q-J9{U eL0tsxCJ&~ALCr^ zJI_~n%82V1$r^V3(g>rcvv9r?RE@|U%o1!S?IvJUifNSzlm>${Vv|(aUt(SBY+np` zqS=M^#dPf1elAFw;QA}r+5tl? Ub|rY{1g7`s4_8yv~S{# zsxb7oH<$1e n%X6ygLP1;w}S^YoN+Sg-1q}WBkj+B8DtM`caBtX!#l0>Dtqo| zr2|X8 a;_18^lW*LF>cdjmP zw9+lllv_@SGm%T%M;4Au9MwH s|wlB0;{lD!Eo#NFUY=T}enOJ+FlOTL3Rvors} z9?iRjw)u1Uah~Cb4c44Hjbt5QiycbQD_GB5?ssG>e5_<|E{d{PR$XeRKy{EZF;Y~v zj7$E{0*6sPxOP9o?IPZ=yh7T~kFSBug^GXan^nej{3~c^&QtV(DZezAqB^2>{O$-% z6uqJAD;-KW2+$_#NpXY~G}S%)w$jccrvgZ*c}UF65c%O#j!|sqr;(#5H*ngAN(X76 zbgwOBzISQr_sbPidjrGRRFRDJcE1EI)n7b>SKt`Y6^COBLT3eU^lgfUUj-g>KOa-D zzr7Q-QpO! {6Iw9 zh;6BcaPIB)WydQiA}bz@-;8$q{PV%wOZ+zJw&Fok0eSa5PbqDu3DzxZr|<1 C*Pcwidqi zpDcEo2W0}~ZJeHA*GmPX_i(QX-y$FkOCkCh-B2}?8=l#r#B{vWz;4oK*4IFa$~f}w zMSSRE$qI(@3>rtOq4c6uLpt)YP;X~B`0mo>{Tn~Nec7Z)=N IK zL*m%JPu`u;EAM}2Ip(>s(ql#b*JU+3Mzn_bw}ju=L&i2u3AN6oB; $i%V=ir?@$X_^z3m|m*1hydPH;hpQ)w`aRQ=A)j<|%*$CY(rXf~mLAe1uaz=yFsd zLijIBu$+aNRXc0?7CvjvwKfiMCM;gupfF)$;sS+M2FC83eb{&9DITFt@nB7{A8?PK zKUBM-uL5C8qdB1YaiTW5O-q>@DTItW*IgH5D>Qdo-va!qe7xwxc`%x_OAaX;ceWNR zEDWfB+A(#@RIRIN!JH-yokaE-&+_KeG}K+YUz}28p$sVnrw4G3DiuG&1$Q8V{i>GV z_7Xl^vH>SQeMI9E+55V8v(@1jx5zc20=}dAmSaca2k18snCIFwfp`CD -UsuiQB#*g{J=rQdL<;A{NHd->&*@%At<5p7Nc=;o^@Z)Ji zuVaf1@8PyH!r?i{aL*?GZr0_!OAaLOqqM-bL$p 95?R#`Ei^mYySu_NN4WXFZN2J+6-v$=8*?8VLD6*13A`{q wwPhW`*%IfmKb{b`z$O{qH