From 85971bc84dd8b6c58296cbf403e543e505cd20a7 Mon Sep 17 00:00:00 2001 From: Mahdi Dibaiee Date: Sat, 1 Oct 2016 12:24:36 +0330 Subject: [PATCH] feat(w2v): draw text charts for words --- examples/notmnist.hs | 3 --- examples/word2vec.hs | 19 ++++++++----------- sibe.cabal | 1 + src/Sibe.hs | 9 ++++----- src/Sibe/Word2Vec.hs | 29 +++++++++++++++++++++++++++++ w2v.png | Bin 0 -> 16438 bytes 6 files changed, 42 insertions(+), 19 deletions(-) create mode 100644 w2v.png diff --git a/examples/notmnist.hs b/examples/notmnist.hs index 80d384c..865ed55 100644 --- a/examples/notmnist.hs +++ b/examples/notmnist.hs @@ -17,9 +17,6 @@ module Main where import System.Random.Shuffle import Data.Default.Class - import qualified Graphics.Rendering.Chart.Easy as Chart - import Graphics.Rendering.Chart.Backend.Cairo - main = do -- random seed, you might comment this line to get real random results setStdGen (mkStdGen 100) diff --git a/examples/word2vec.hs b/examples/word2vec.hs index 1669c41..96875cc 100644 --- a/examples/word2vec.hs +++ b/examples/word2vec.hs @@ -42,28 +42,25 @@ module Main where let ds = ["the king loves the queen", "the queen loves the king", "the dwarf hates the king", "the queen hates the dwarf", - "the dwarf poisons the king", "the dwarf poisons the queen"] + "the dwarf poisons the king", "the dwarf poisons the queen", + "the man loves the woman", "the woman loves the man", + "the thief hates the man", "the woman hates the thief", + "the thief robs the man", "the thief robs the woman"] - let session = def { learningRate = 1e-1 + let session = def { learningRate = 5e-1 , batchSize = 1 - , epochs = 200 + , epochs = 1000 , debug = True } :: Session w2v = def { docs = ds , dimensions = 25 , method = SkipGram , window = 2 + , w2vDrawChart = True + , w2vChartName = "w2v.png" } :: Word2Vec - (computed, vocvec) <- word2vec w2v session - - mapM_ (\(w, v) -> do - putStr $ w ++ ": " - let similarities = map (similarity v . snd) computed - let sorted = sortBy (compare `on` similarity v . snd) computed - print . take 2 . drop 1 . reverse $ map fst sorted - ) computed return () diff --git a/sibe.cabal b/sibe.cabal index 5bde54d..0f84197 100644 --- a/sibe.cabal +++ b/sibe.cabal @@ -32,6 +32,7 @@ library , data-default-class , Chart , Chart-cairo + , lens default-language: Haskell2010 executable example-xor diff --git a/src/Sibe.hs b/src/Sibe.hs index 7460aab..f4ccede 100644 --- a/src/Sibe.hs +++ b/src/Sibe.hs @@ -26,7 +26,6 @@ module Sibe sigmoid', softmax, softmax', - sampledSoftmax, relu, relu', crossEntropy, @@ -183,10 +182,10 @@ module Sibe sig x = 1 / max (1 + exp (-x)) 1e-10 -- used for negative sampling - sampledSoftmax :: Int -> Vector Double -> Vector Double - sampledSoftmax n x = cmap (\a -> exp a / s) x - where - s = V.sum . exp $ V.take n x + {-sampledSoftmax :: Vector Double -> Vector Double-} + {-sampledSoftmax x = cmap (\a -> exp a / s) x-} + {-where-} + {-s = V.sum . exp $ x-} relu :: Vector Double -> Vector Double relu = cmap (max 0.1) diff --git a/src/Sibe/Word2Vec.hs b/src/Sibe/Word2Vec.hs index b18f8ae..7c52669 100644 --- a/src/Sibe/Word2Vec.hs +++ b/src/Sibe/Word2Vec.hs @@ -16,19 +16,28 @@ module Sibe.Word2Vec import Control.Monad import System.Random + import Graphics.Rendering.Chart as Chart + import Graphics.Rendering.Chart.Backend.Cairo + import Control.Lens + data W2VMethod = SkipGram | CBOW data Word2Vec = Word2Vec { docs :: [String] , window :: Int , dimensions :: Int , method :: W2VMethod + , w2vChartName :: String + , w2vDrawChart :: Bool } instance Default Word2Vec where def = Word2Vec { docs = [] , window = 2 + , w2vChartName = "w2v.png" + , w2vDrawChart = False } word2vec w2v session = do seed <- newStdGen + let s = session { training = trainingData , network = randomNetwork 0 (-1, 1) v [(dimensions w2v, (id, one))] (v, (softmax, crossEntropy')) } @@ -49,6 +58,26 @@ module Sibe.Word2Vec -- run words through the hidden layer alone to get the word vector let computedVocVec = map (\(w, v) -> (w, runLayer' v hidden)) vocvec + when (w2vDrawChart w2v) $ do + let mat = fromColumns . map snd $ computedVocVec + (u, s, v) = svd mat + cut = subMatrix (0, 0) (2, cols mat) + diagS = diagRect 0 (V.take 2 s) (rows mat) (cols mat) + + twoDimensions = cut $ u <> diagS <> tr v + textData = zipWith (\s l -> (V.head l, V.last l, s)) (map fst computedVocVec) (toColumns twoDimensions) + + chart = toRenderable layout + where + textP = plot_annotation_values .~ textData + $ def + layout = layout_title .~ "word vectors" + $ layout_plots .~ [toPlot textP] + $ def + + renderableToFile def (w2vChartName w2v) chart + return () + return (computedVocVec, vocvec) where -- clean documents diff --git a/w2v.png b/w2v.png new file mode 100644 index 0000000000000000000000000000000000000000..0183921893d3ee2742d52c1152322fe7d88b3d08 GIT binary patch literal 16438 zcmb_j2RPOJ+dt_+p;SslG|32+mDw))>B#0-MJOXgW+izl@{~=ebnKZu4_V19t8g5$ z_dLe2-up)b_57dr|6Z@ID;LiBo!_{>_x)MlM}Nv++(Na73WY*#k-BsajY4hQheDC5 zZX$!< zF3tBmm$aE4#>@C}tG&G{Woen*Sxw%ih8t{WLyOz&{-)HS2L+YX0ZY;1_rAe!6`bs|Nf`(0=yYH(ss^BL%V$)yQNBCq7) zjK1U2bgRAD{LI)!%B?gsH13|B+G#hwq?$BN%1slTam9L%+}-DU!#syGdsrkTanGOc zkgIi?N)Fw^B4(R^cf*}Kcbw;%30Z0tEt!@_>C5BNTkZNj9bO2?wV&>%{+wc@*h6;w z#0esKi{mt&Nyv1NM3rqb5+ZEzbts*{vTfToM#kC6S_Llb1&=-0&^)yC`Sa)1xP2cy z_@lA4>e<1I;j#fc=;`U7j8tkm6<`@ZgQ_p z%Vi9vwW-Ojw`M=K?sJm951Q3^er$H4#|JGr-%fkcW6v&n`i47M0}IoH5eI=M#p9%b zgwmp-bLY=%Wm!cfC0*tqe%AN&U_8MlB4YRP^?5w6L5Vw~bd*i&ot_UqTyQiCN5@=r z|Hq50O456U%}*^c<<~|iCVs0>5d)h_QRy6E_$N1RTWohxTB06}~pL6b5nwyWpGkhVchqdd831ExH+&QyqiKq zZHBWbwb=y)1y7z-<4AR!oBH}y4_i0cSGzDjpS9=>=Y_E$6y7JNhQgbS6A%y(${X~U zR?i&0?TxM$o2dAbZ0Ni+M}U%YsEfQT9i+QXB zA#J)5V}G+b?PgDPsO0Uz&yRO;O^yuZ99&GUmKbmlFeFq#F(NA?MkPT$kiRd$Fl4r! z);UtLdK8UXk{N$$hw5&;bMWKe`_J;rW!sLuUSz)|9c38mm}Z#{M;0mtsabmQr?#%Wy?ymp%C`N&F63=XeL2#R z+FJ$*xFx&|Ax<)ka2Fc{E0T3_)Xk!!WD5gdK7k-9C^)qnH*?;?V$5TAn~3;C#R-@$ zo40I%Rd0+&e+n|ooAr3InVOnO(kW-rw!0!gOGtd`v;GKf$w^tcA^+Y+CMKp4D7ejZ zY&oR2f&{{-?Q6@c;0KlPZbQGBP&>HLhH#dwu>%mV~yVqW*$pddscy@vd?o^#1+(Nhbt@ zD&HT5v+)l)Uz-xtqEwsG+uQs6`SXm73|QX+CJk@G!&@^Hu3qhQV|8MnqobpzH%M>x z!0^EZd^u@B5%c!#abaQMW5+%=HVVPa%*dE2afG&f=ImKmo9-I>QK(a_9p?&+uV3Fp zOWVkL@!UCne*Tt>Y8cjr75=XzP0h?aA3b{H=}E}2(68PNkf%mvrgc7Eip9X7rJR34lLji z&%L4z#DMN@gM|VFu?)9lR`tLoV-yu@Cx`*({`|8Bo&`P^x${DNWm;c`rm}~#OyFgn zV&m`ORCq8sk=~q~oYJzgvX?HY!c;={zjLJ~)mOt1h>eW}Un4JvLHCZ1i4ihub?AKm%nxHH#bMq4 z&J|X^X1d{-Gw+~385ac>-@cXdV%x$j%(rjfK3-n4k=EQ`dp36VkGrI|v%Zpp9+ zwr^-`q$DS=3>1h-OdK6;FT@YdS`!XT$HVFi5}sChko&>I#^vy#LpNG;W}_lP9=p3c zj1}*uq+v+ZDYy><&?L~;&(AL);LB|3^If}kNiOs$%n=86GcaUWbX*9kyp0bLa>^sZ zAs4$Bk2Q9GdUx#)o<}LjGa}pFFyPfRz>Y9J}SsHx)g|>H^)GzLLVTC2&gbsb8hhM<4mb>N7wdA4; z%kT%c`_K20<`3Cm>%JC0VRQxFK@#a+At5pkU}5}VBwbj4MfO#1V&JD@H+P^q__I!z z=?cf6t6H-EBP!owJ>3tH3=|uvf4(wNLb42hhzo8)02zAPO{rYukYHD0uTx=}2xVRQa zDj=G+wl*%6cz4DV-LKqO(9(NFZ^yoQQ)H0LfqnPx-R+Uqq1N<`lh#mb74Sg(e0+T5 z0{M-A#z}MJp77Ws5dHe~S7>7!Hf$&>D}%K$H#JmSZ4{@CTLi{2K_rShk|u#66ux`+ za0^p-Iv468-{pe)hgP0DCMYd5RJ)N92K?EFG+%)F85{Ql9??=)xBFW32(B9;lsP#$ zDJgXTN7&gTu-Nk))&BB;iK}8BcPKL4lNql{fd(K!>J6KkntJ;5 zDZE?T`M9{aW7h{C{nDp$3JL_kPJeAb$W1S!sHk}5iZ>3ov|HN&VHXpuL&5j5x8I|- zJZfI0v*9lLUmhNn<>lp7Re2LtqPU6RkzR%HhzMSdv>TJX9~b7PX=!Nc%<==p?u-K! zV!hM(Y{N#1L+tF?*7bKwkC(Hf#?@e*p%N*OJ3b*w8$sH@(3zxAIu;Us90(mF0$|4VS z*KJR$ zq_HgSgjY(uxyU*dYt#fxRz_O->({SnMMZY`7l0I-GE=p2PDCg$%zis^m%FQao4?7u z>0TOT;N#}z=6ZUs6B5jzz$`2*6fj$!ujv;4OjOEe&z?m{yzqeouV1}-HP8T+RF!ob2793-elEzXQNQQ&SVDEXS?;uVCw{f<-j|z6AzO zZtXP}ZUvnLsIGnkwTsow-fgFTw#Rt#*0B3s9UV~V{5P9YUY&J&$|{M$Z4x$Vhz$=9 zPd2QEiU#OFupv0k&dvq|1c=X`=3_36SXDEYM@L!kE?Dmm6%}H)2UQyl1A~Hm(7L+1 zK;6V0EZ`yK{k7ajS4{-X#cailhd&32CUpyU;dX{vxS3)PA64mqTyt2)H^Y7x{6f6G z7I%Pu8z>w9vDQufJD=t~S+ic2LB8xSU3*Q7kF8d?r6PS4ljE-jW&ScjADIVt3o_1a zT3yCJTK%`Va5~JKY|Twrmf!rzR#S|CNVp98aQDZ$+Y(pZu7L@!7P+@O;6qrBuUk^z z_{qQafi>4fw~KpuwpX_e43j&!o(_Ayc1HVmZiOqMV#`9VsEu*`K>wH7oG784=#MOE zv5l6?^V#6_avy6A`=A%!kjWadm9x{b|G_G7;c}tn zDAI%a7?slEV)4=ZjZk+`jZv_m-oLLAItuL-X03G8P;)xLek2!$3cLU&mXL&gdb7U1 zJ~b89ElW%20iQp8`dVW12g6gC+U94ht*tL#zI^_CF(`P}y*00#GZeJ6@LARaAY<9U z&rtgGyLRDc+|ki#X}4Q*?ugzVWDvZeDkb%3a1LeX_lRfvnH}G{$gmQ$4lg#jegY1M z?q8mYD9ydYyhrrlaqbWD)E|(l1}p;)km?8Jm`Z?N=kyWcx@KO+aaV0 zJxDPzXXm#f%B|E(vRW9g5*-=r?X6)2nMZ(+@6Xzl7cX9HVG^_&PBy9y9|%cm9)dna z(g#Wgje)rMPW%sCI|AMic*6b$i(Ocp#^J2`K6RD**$jSu<-JGD*4)w25qLcyYa^p5 zH0QyC`aM;_pzU3e77!8&1`UENn`5W61dA0A`ksYuP2 zvs%WoCsxBl26Di?3z${Y2MD3s!-IU`Fg9#ykP7;th5uL|IM*Gp%RWu3)32L4Z4L7K{AzhK7Qy>k#VYeoN`d5!DqV-sE{htbWet3uLn+4R#rlB z4Xe|(k+GZJYUIBOy!G(m!!XgJqN3>7~ zyWPjQTwi_fH;Jv(^F@#7U`4uZ-J50J{thssH~MzAxS(L#%a{9vglqt^9X)zfI?9Jj zrKGqR7ExDEPl8%85xz)B*9Dkm$8)b7R#OV{$&ty_&fCMG65l|ktM>)-|gZFT_q8zf}52+AMG-Ejvm zKAkL)u6Rg55aMvl;dfP~d-v`Y8m?(+nIA2r#Z5JtEXz?Yl(ZKvTmYJDU}#ucS_%Nu zdWl5;1Uq4&tJ@ALp9s!Mm|1J2op#HXA(H8r56c6cC5`xmH_soSU$jyT41Xo1dROY* zP0h_jxsM+|K700Tt6N7}v#F?g+bKD@N&v$4V_%DFYHBE19cofW{)%Hstmlw-AZu$c zbc6M%c=_@xZ}sfkLm+S{$7_DcOvPdki;B)sO3oB?cuG3qg+P}9RSXnHoAj*`Pqv|6 zdZRy>rZ3e>E-2dC+5&h5eB^_MGU4FhsIRYwX97_9?OQZQD%=~8labB?F3t={12Wh; zYTle}%*-tdOPL$43R^tl8O8X5x1fiTw_Dspmi zc9d{4M)KxN_4M@2Vp&ja&&bHgK!+6sZ~;_ec$yjP1JRKj%h~ZRAQd^p294w88}pvg zk(a}*jf}hwqUBkr9^gApWo8YrYH&<@C}yw|NW=kRCJl6DVf(2Yc6N5Qwle^0{6a%R zfp(jiZHVu+quNQf%-sgC`9RG_-9Wj<`eB+fEe(aTx3EfxfjSssKO6v0MSQx&viW8@ zu|!YSb-4gceW@)q5A0M{Qu6ux@3vtj+wIGox`ky1dZ}415uSg;+z8GYCI;w@rkd`k zbjsnI09($nPSK$5QUk)bf%W9;>&tk;z}6xAjP_}mgOfJ_xejB<%*Bz1vMfIGc8%-M zu7^J9z5|LEcJpF#u9oevNq{l<@}jI2t*=xx3&?u37T*a1fFTer*Y%;bg9Ksj_o&yP z5nyKYPh9r{`p@6UN4Xxh6KAD{+0;O~IYk@LyJJ6su~xwMcPsJVDZ(lQ-%^GY4;s`wg#_k%9T2GVZy*qpcIRJ9DzZtFnrnDfV^tVI~ zj`sv%e)%W(t^?D`*I^Sua3FG~{}(j;OWmr5hIYn#tLNArOTMrHbxOMwDIS!|FU7tx zKEpocIn0i8CFH!8OluxGc)^{o^O`sz;pt{H8C;q zU9^RbMGhh+`N<=hFF02H68gw>g6%~R->0TJv01pQm;+oUV2mnVx zMOD9YrE{1_#`mmsPc2M`Z7eMDl28Bo3kv&g9uN4GpwLQ3fjJ276x;#$e{k>ywho!D zo~$Fo6i@jS0MaE zM|1M?Pl9?4!zM1=h*olb6u@7o{ct82#ywqK99TiK))t)e5;zw&)Nqd$LW2r;W)YMxLr1tDkN?=;6Q z28KwpT!-0PXU`%Q#4yal-5A?~p^?TR4Ii|Ul9GXjkD-~hFf*H`PlV}ER$=8poYe5| ztpuBa#0<-D2TMy!7&&k+VYGq}hPW9F4F1oaje&RcP5UJz8+xKSB_tMwaI=+Ro)T~) zNZ`pI+qX{#1~I4%F#0b(Wxmsg_47vCsg%Ee-#gz!af-G6(r`94B`0He#n< zyM{#@7e3sASbG%#Cjc}nDk}?G_11uBZ`o4?=)D3jL!m>TYfY>CD^;(90xW^T3F0^U z1a8hMxiJY!Hk2V zaeeRrsa-hjMm&Q({$?V~da&XFK4Op8*Crjl*knEdwbjvYIwsHjX^_Uzh4 z?2k!+6Wz)x;Q%^#ghHsV_I^}tf1Jz%P}E^|)rQNN7#Rhev@}pxSLa>Ww}CL%It{+G zX?in}+7XR0CEa1gpcv^CIAo(3k!Sm<|-JYi;Ig;^Y1RQI;Pl9n_{o@ z+)O0(#RGwO`SN8^QBhT`*07@qDyxy$V7)NugYGY553OTG0u{6pGcTEZ$@3@e#*#Ok zSn*cu69`P7K7H~9~A;YQ{aIc5(*($Xu@87=%?JZR74jLD2 z3xE@zfVYni*xAzqjR`q9^Px+#p`zO65QJ&OQ;6!9xI^rPG&cuU$VmZ#`kQ7TIZW=M zbNrml`1wW}IW@gIi@17i3P_9<6|&eX4i57G$Gmgl`Ue+vFU zg?Dmez4!26|I^w!b#VrX5qLUHh7H`F8ChB!iP!FLB%fr`I(Xm!)Dz}Tmk$^>2iTA= z15jOq8yAlsKTZ#F836FAW*sOg@X-w_1FZq?FvVs8)Ist_8kYNM1$He$}%K}>)Kb;&P-28*ZMMZ}W zALhVbaA(-g$~w}~VF}ZzuD-rcqcK5ysnvdDmP87Xa3sO31=V+j-%3Y){`|QLUz3i= zHoaQl{*X4Zva&8Dd3shui}Xe#5fBLaf#!k3Im^pGbvj*BU7cX*>?kyOvI_5_q^=H$ zB$yP?$?Ax+z%dEVOAE>|)}Iq~0V36g(rn(G1X4&$y+JZ#8V1(*rFDMI@Nic?pZrd4 zxmttjkX!(8AOtv$7Em@Wu)s*724i4udIZTOLdGWCyB{tEnl^mN$$1pST=4w4LR#s( z*cIYqV(dGgT?G1=+1c3{G)qsm8~U`o<^eF~S7eea(l7%dLlhAn-X8>F8GL!8!7vaE z8iF}c=tgU!E3mD@TzE!wr_@Skw4DY=0ER!Y6sT|=>kNqmHZ&L36xb$`I?FQaYgvMc zm@`Jc8IcP`AqDd4)qXKCO=srq+dtOSn0H7(@(K}0fCxu;8?Q=uge8|lz4d0l5O%4z zc0Y6RVyTBHzmO2%xx$3pDfK`9{2Sd5#y_8|?eW_2Z{Ywwiq^efl9-cc@APTTWVi30%IwKSzWQ_7`Fk`a2( zmF~rHtM!^hB(8ggnQVpwF+zOAufPVa#3R?%MGlgK0`a98z8~~-B2L~^>u+F!Q zm9pZ7&r?xT$0-khR+z0pX_J4 z;sR(acCdOh)YT2DUSxusf~|Xb+6D3ul91l3sHmtCO<7zZEa453ySlqEIkq1Ogr!I9 zAv0wPPUBk!mI>$RPJ<``q|3A&lbu9t8e|+mGl5jDuU|jOFmlCz@nqbFUV#9-hMU0 z%EY7r&o?MY)Z**8g%gxo9a8tEH^-C9gFE=WAx^x>udS_}9%_#8j*g7XupQF_w+P@O z$SQ!DLDYg|kpx7mK$jXRq?NoPt)!w-9V9e@Bu5Y=s-o?f3Q)=hHy4b-d7~jkAqt2W zI0BN3gUF*6hmwYds99?cn8h?qf{0(-b>0r$=6EKQ7sQBbWzP=}W5`kY_G|oM$Pvaz zMt)8=(}wdo!hqk?dO+nGlIchfn*FC{k&L@@t9uGMq079pRI^D)oFnQ z9Ss)eiM6%gw$c@sQteY4_8zU5D~nu)W}+rm+Rg~AGVg#0E21qC>9T-Y0e z%|Nm)z_QmX`U~6vsL5gNtK*$zKzEQF5wvDMQ11t+#JSBUy!<;`?NHzOOG*U!A(@3Fudb?2x2 z4r&+89-pFranjc~Xy4QFn8$0J!EfP@{AF|91%YXZ4q{88ZNuBfrFq~FSl9(foMx@e zSi9zqn}MyoB!;1P=@`-vNHeGVUr|gL@l#*fWE! z9)?$lU;W|et#qkxqS=2Z@+{wWc>g!}hd|8Vr33y6(*K;cNV@Nz@F)aBzQuKZdWEOI zn{wY`OKV)20iy!WqeoM}+yLwE3H{M5Q^iY{{(|^`dYoWToSP(^?@QXgkFF=>YQdxX zksoM`Rr5mf0|F4YGEFB8imbRf=RK=*l$4bzC@I<5*hnN&_wu-BO68suTnjygX!hJ( z6T`H^H?8t5NWQy0FsZE(ytY1nxex=Ecvb0Y`Nq*6naoQD&*S26 z#zjXTZ;uwUj_H3V7Nf_dvE@rNk{CbU94%%aQ`pW2e{+gae33U{MC`8z9;_lg^d-5p z-5iif#Ov2*I8tv9H7Q2O$7yDcmzcr!f*%pg7MH^i!2B(vlh;1LedLJn@*FNLEG+yF zY0Rd&x;Hkb+zQ8rvqylZip;q{;gKR)v<^&;jd5^t1`6I(hYTZ}4e`m{WP{4+_;}MB zH!3PCb0G)~2^=~Fv1b7RMm6_!Y5c11yc1Z%EDbXGN>>Quic0!(_sfs+Um=}2=NnJ5 zu1!Cfe)k0bk!OuEBZ1H^AC<|{+u4pqe{I)F7Zef#ZO{on%>xm15d9UziC0E=K8hkx zca&L+y20cA{rhRAc#^e-Hqdj}1k=mw14K}YHfS?so3|w!0Pw}ZWYS+>@?-Fc@fy6O>VXN3V z&tg87|_k#QYr{Lu}%&& zA{z^Wt052R<&~^kNFfwgJ`vE%vdr3Q&VoRa0kVY9u`oHTIJ>c#)A1i57${Y-z;Qgiu{9& zV4Vz`6d;b)l1o*{w~n*d@h!&*0U_tEkGR!(>GXqrzqffAr*PBE~=D0sua(m+xATSTxe{~pjn z;DE68ObOYeg?;np2$sch@*|M7!|T$)wwCOk%d*AoKcG z&6Y)p9Ps9MCS7vTk`a@{#|jh)C9(H-~t5cmxEj zA<6{Oi_kC~>{*2HLP=54^6s~(AT3H9&s(H*{RmjD?0JCf?>d*stR(`{y0F`WjjiO1 zwQ%tlqpk8o1bfjSW%DSaiQ2UK0WOW3Q?%-*GFcmZYPK} zUwFRQ^Hxbe(0<{Q_&hK5ImXdK`JHsj;@8+WO!5^4{iOcoNQF6U?SN<|bn%7qe~43b zb?@$|tI8#PjXgJ0thSk}PlKzkez)P_*rFlc;lo@O1wuPxS8EC}_3GN%36ik;k(GtT z315YbM3B)aXjx*NVJ}0J_v6Qp1CL)r+R8Nflkzg?XZj9>;;Q825B|H($^!aj3-2oLdKsVF(8qrIgbu2N zv|8%*kFN&=A(Z7D1edC+`nl%SSz1k_%A-4Q?!U zy~sWxwh@ww}YyuB<=d}A=tm*mSva) zutd&jTTe3Mn%~VdYWUylD!NGyve~PNj@RIrfX^o)1nH7_QNnB$t<$IrXs)nhxdV1d z-m2Xreo@GX3kw?^M<+s;a-d-ks|FhOOPJ%Aw>XO15gez>cq$9=vg5s*ozh*$64LoL zmOpBy@@?okXb1N#%QOpcG+4LYRlyp7!;G3hTrF1VG0Ktxg`uvr)NNq8q@T@P%hGZJ zWR>|QlLWZH&GF?Ej{4)h`+pwtdI9X4g{OynojvPPZycA9&={HkNiA-7X%5)lOHU7e zZ+Az>1ni3gP9n|oNx$W4Q&(3P3>)36SFf6ynnKjkn?tcBrBz~tZKH~GYDx-XtWAK+ z!>4;+RJp$_OMykabR>;$kw|ZF^<KK~Fn!p25M z(7HG{`f3#<>-mQOK91oQ2XI*{bzlv;NspjZm=)a&XjrhpdfaSj%4`@DW=L`?D?IhB zz9@X@(szL%D*-ZOZ}0V(dJ>?A{%RU#VPq#VY$Yb(B##|C#>R&1G!SuK%wgKycPvXv zqjbM|FgAkfwuVS W<+HbSlTN{rQBvpS&&8k7x&J>j=HZ$E literal 0 HcmV?d00001