From 96081587f3253fd4296d40123c798f847d1f6904 Mon Sep 17 00:00:00 2001 From: hamshkhawar Date: Tue, 28 May 2024 10:36:36 -0500 Subject: [PATCH 1/5] tabnet-training --- .../tabnet-classifier-tools/CHANGELOG.md | 1 + clustering/tabnet-classifier-tools/Dockerfile | 20 + clustering/tabnet-classifier-tools/README.md | 73 ++ clustering/tabnet-classifier-tools/VERSION | 1 + .../tabnet-classifier-tools/build-docker.sh | 4 + .../examples/feature_importances.json | 16 + .../examples/tabnet_adult.zip | Bin 0 -> 41356 bytes .../tabnet-classifier-tools/plugin.json | 672 ++++++++++++++++++ .../tabnet-classifier-tools/pyproject.toml | 31 + .../tabnet-classifier-tools/run-plugin.sh | 93 +++ .../clustering/pytorch_tabnet/__init__.py | 3 + .../clustering/pytorch_tabnet/__main__.py | 348 +++++++++ .../clustering/pytorch_tabnet/tabnet.py | 241 +++++++ .../clustering/pytorch_tabnet/utils.py | 224 ++++++ .../tabnet-classifier-tools/tests/__init__.py | 1 + .../tabnet-classifier-tools/tests/conftest.py | 208 ++++++ .../tabnet-classifier-tools/tests/test_cli.py | 50 ++ .../tests/test_tabnet.py | 151 ++++ 18 files changed, 2137 insertions(+) create mode 100644 clustering/tabnet-classifier-tools/CHANGELOG.md create mode 100644 clustering/tabnet-classifier-tools/Dockerfile create mode 100644 clustering/tabnet-classifier-tools/README.md create mode 100644 clustering/tabnet-classifier-tools/VERSION create mode 100644 clustering/tabnet-classifier-tools/build-docker.sh create mode 100644 clustering/tabnet-classifier-tools/examples/feature_importances.json create mode 100644 clustering/tabnet-classifier-tools/examples/tabnet_adult.zip create mode 100644 clustering/tabnet-classifier-tools/plugin.json create mode 100644 clustering/tabnet-classifier-tools/pyproject.toml create mode 100644 clustering/tabnet-classifier-tools/run-plugin.sh create mode 100644 clustering/tabnet-classifier-tools/src/polus/tabular/clustering/pytorch_tabnet/__init__.py create mode 100644 clustering/tabnet-classifier-tools/src/polus/tabular/clustering/pytorch_tabnet/__main__.py create mode 100644 clustering/tabnet-classifier-tools/src/polus/tabular/clustering/pytorch_tabnet/tabnet.py create mode 100644 clustering/tabnet-classifier-tools/src/polus/tabular/clustering/pytorch_tabnet/utils.py create mode 100644 clustering/tabnet-classifier-tools/tests/__init__.py create mode 100644 clustering/tabnet-classifier-tools/tests/conftest.py create mode 100644 clustering/tabnet-classifier-tools/tests/test_cli.py create mode 100644 clustering/tabnet-classifier-tools/tests/test_tabnet.py diff --git a/clustering/tabnet-classifier-tools/CHANGELOG.md b/clustering/tabnet-classifier-tools/CHANGELOG.md new file mode 100644 index 0000000..425b725 --- /dev/null +++ b/clustering/tabnet-classifier-tools/CHANGELOG.md @@ -0,0 +1 @@ +# PyTorch TabNet tool(0.1.0-dev0) diff --git a/clustering/tabnet-classifier-tools/Dockerfile b/clustering/tabnet-classifier-tools/Dockerfile new file mode 100644 index 0000000..48dd58e --- /dev/null +++ b/clustering/tabnet-classifier-tools/Dockerfile @@ -0,0 +1,20 @@ +FROM polusai/bfio:2.3.6 + +# environment variables defined in polusai/bfio +ENV EXEC_DIR="/opt/executables" +ENV POLUS_IMG_EXT=".ome.tif" +ENV POLUS_TAB_EXT=".arrow" +ENV POLUS_LOG="INFO" + +# Work directory defined in the base container +WORKDIR ${EXEC_DIR} + +COPY pyproject.toml ${EXEC_DIR} +COPY VERSION ${EXEC_DIR} +COPY README.md ${EXEC_DIR} +COPY src ${EXEC_DIR}/src + +RUN pip3 install ${EXEC_DIR} --no-cache-dir + +ENTRYPOINT ["python3", "-m", "polus.tabular.clustering.pytorch_tabnet"] +CMD ["--help"] diff --git a/clustering/tabnet-classifier-tools/README.md b/clustering/tabnet-classifier-tools/README.md new file mode 100644 index 0000000..642cb8c --- /dev/null +++ b/clustering/tabnet-classifier-tools/README.md @@ -0,0 +1,73 @@ +# PyTorch TabNet tool(v0.1.0-dev0) + +This tool uses [tabnet](https://github.com/dreamquark-ai/tabnet/tree/develop), a deep learning model designed for tabular data structured in rows and columns. TabNet is suitable for classification, regression, and multi-task learning. + +## Inputs: + +### Input data: +The input tabular data that need to be trained. This plugin supports `.csv`, `.feather`and `.arrow` file formats + +### Details: + +PyTorch-TabNet can be employed for:. + +1. TabNetClassifier: For binary and multi-class classification problems +2. TabNetRegressor: For simple and multi-task regression problems +3. TabNetMultiTaskClassifier: multi-task multi-classification problems + + +## Building + +To build the Docker image for the conversion plugin, run +`./build-docker.sh`. + +## Install WIPP Plugin + +If WIPP is running, navigate to the plugins page and add a new plugin. Paste the contents of `plugin.json` into the pop-up window and submit. +For more information on WIPP, visit the [official WIPP page](https://isg.nist.gov/deepzoomweb/software/wipp). + +## Options + +This plugin takes 38 input arguments and one output argument: + +| Name | Description | I/O | Type | +| ---------------- | --------------------------------------------------------------------------- | ------ | ------------- | +| `--inpdir` | Input tabular data | Input | genericData | +| `--filePattern` | Pattern to parse tabular files | Input | string | +| `--testSize` | Proportion of the dataset to include in the test set | Input | number | +| `--nD` | Width of the decision prediction layer | Input | integer | +| `--nA` | Width of the attention embedding for each mask | Input | integer | +| `--nSteps` | Number of steps in the architecture | Input | integer | +| `--gamma` | Coefficient for feature reuse in the masks | Input | number | +| `--catEmbDim` | List of embedding sizes for each categorical feature | Input | integer | +| `--nIndependent` | Number of independent Gated Linear Unit layers at each step | Input | integer | +| `--nShared` | Number of shared Gated Linear Unit layers at each step | Input | integer | +| `--epsilon` | Constant value | Input | number | +| `--seed` | Random seed for reproducibility | Input | integer | +| `--momentum` | Momentum for batch normalization | Input | number | +| `--clipValue` | Clipping of the gradient value | Input | number | +| `--lambdaSparse` | Extra sparsity loss coefficient | Input | number | +| `--optimizerFn` | Pytorch optimizer function | Input | enum | +| `--lr` | learning rate for the optimizer | Input | number | +| `--schedulerFn` | Parameters used initialize the optimizer | Input | enum | +| `--stepSize` | Parameter to apply to the scheduler_fn | Input | integer | +| `--deviceName` | Platform used for training | Input | enum | +| `--maskType` | A masking function for feature selection | Input | enum | +| `--groupedFeatures` | Allow the model to share attention across features within the same group | Input | integer | +| `--nSharedDecoder` | Number of shared GLU block in decoder | Input | integer | +| `--nIndepDecoder` | Number of independent GLU block in decoder | Input | integer | +| `--evalMetric` | Metrics utilized for early stopping evaluation | Input | enum | +| `--maxEpochs` | Maximum number of epochs for training | Input | integer | +| `--patience` | Consecutive epochs without improvement before early stopping | Input | integer | +| `--weights` | Sampling parameter only for TabNetClassifier | Input | integer | +| `--lossFn` | Loss function | Input | enum | +| `--batchSize` | Batch size | Input | integer | +| `--virtualBatchSize` | Size of mini-batches for Ghost Batch Normalization | Input | integer | +| `--numWorkers` | Number or workers used in torch.utils.data.Dataloader | Input | integer | +| `--dropLast` | Option to drop incomplete last batch during training | Input | boolean | +| `--warmStart` | For scikit-learn compatibility, enabling fitting the same model twice | Input | boolean | +| `--targetVar` | Target feature containing classification labels | Input | string | +| `--computeImportance` | Compute feature importance | Input | boolean | +| `--classifier` | Pytorch tabnet Classifier for training | Input | enum | +| `--preview` | Generate JSON file of sample outputs | Input | boolean | +| `--outdir` | Output collection | Output | genericData | diff --git a/clustering/tabnet-classifier-tools/VERSION b/clustering/tabnet-classifier-tools/VERSION new file mode 100644 index 0000000..206c085 --- /dev/null +++ b/clustering/tabnet-classifier-tools/VERSION @@ -0,0 +1 @@ +0.1.0-dev0 diff --git a/clustering/tabnet-classifier-tools/build-docker.sh b/clustering/tabnet-classifier-tools/build-docker.sh new file mode 100644 index 0000000..eff76ca --- /dev/null +++ b/clustering/tabnet-classifier-tools/build-docker.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +version=$(gn8=Lp?(f6* zF%BCa;^e3-~9jDG+V;NL6QkFUg&>f*s`WZ>COC>Aim zH^5)`Bjd7ffblXgie~s zj1D(b;>j2oc*+ZPCJ9ISj1Az)mR+Lc6f)6=Kd9_~D)Hnz8wtml@EUu{2>Z_A2D(bQy0|u)&r@`5B>G1wHb}(QgwJd0!E5$MK+_69{u2vOYbDnmYt`4i)p-i`9nQkXOPuGLj`H#956}oy=bS-%LLS1=2&p@Or zFB0rhrfXQHYsBYu_26~;qpoFzu5lGzOI~-Ot_q)LBGOe637VGanw9CA^LZ8?Jj*}o zT2<&;RnfKLSqpU=^LaKR-Nqt8+cMoAWx95Jp1lW;^G99l3SCr1*O~`HT{%9FE7Fw{ z2|ASNI+p47@?Wy)S<$|Lx^ksiEJe^zEK=Ur9Vls8(aJciF3BT^nClJqH4_AOHu2KD#g z1^ii=HCyG%B+TsBH1JEu%` zE}s|e!J9`D`e)g2ABH!-s;U)lL78eyxoWIPbzzz6B0ew9gSXhTbG53WlP8S!_2K&k z1xy;vXMFs|2l(@rl&P23`+gHc$Fk)ca~BR+4LFkqK1Wq}g-yhIP)a!(y` zpny=niK1O0xq3-eXJEryQ8t5>cLxGIo;I=(^X8XDr?J2FO$tE zm(3K(t|^mU%jd20;H~%6`>Sk~)!a};-pquzu}pqbx%_64d{&wK7CvvQ2QRyF-v1&m zTGee;)lGTZ%hY$2tM3%4?y3zF}jFAOITO-jdA zc|H%jDvP{N%0lP!d51lCN9rD$t!GE8hPL1x6NZ)&7Wd<2p;hPePL!R^C(A;g;`2^> z@XpjG^w^Ne;wcCERfIlU5jv=1y64#Go-dp3g|e6z`MgUWyvv^4I>r=jeAZM~s?OMw zcU8EwI(*(W8hjq_x@ZFxlm))Q=iT(+-SR~B3QSFRyJ}b~USV0-qVll+iNf9~3wxK( zEB4^s^K`0L*ea$hsT$szcfTzBgYxhXMd2Tng@4TFJ@Mc@_3Ygs;YFr>RyDc}?|E7D z7v<4kilV)F3v(W^7{yQ-$P<&~D1`h6u+f8g^zdhkBgaeJFq+TNep z?OkE1FRZ1$mRagsSLanGAp+VhOOD0CGZQ$~TsQ554SmE$yFahg(`W)w$R zgi|*!m{#KjMnwY4j8SE=nv1d2%CTCoSS=}*I>piuVb#qezhenK!nBg0GG|(|RBgmm zZOf@NS*muFsy(IB5>eI7JEnj34x=rmE3=Qe1*20&*ND$_D5EPk5z~?KbfP@EBAy!U z?63g8;J{$NNq;ymn9dRjvSjoqg2-Krenp0a24f6FITCq{>B45okWv{@s;(lco^_|H zY&oWz1eg_L%&uH_@yeN$ube5%WJa0HDU*eW$)UbX)OeN>MAnQIOJpr3vMDFBWr=!F zB0EZCFCucTFHsfmaU{rW7{ro+n2cLa=D?CUQnH?ute1$aZ$puZ?AKcY%$9Lt!TN~7 z`j&$^vtTY1%$0)m6M?zc7pyu5_W#|17AA}v>%ak39q3Mx22v!CdO5Jtk;V-AJx3O% z%wVwthg5dpP}YIN*hG0!s^OH%t6mPQdgw7DB*4s=k?i7)5-*;2`QnXcnZ{5iAIjug zF9%jR`urq_%o%@{C_qdUSWYySB?_WM!IWrRy&PEW={H`2%z_DF$tH-&Ld(e}vSeYD zj7Q0sdO5JV18|ZAm?gt!!6u8r!pp%TSg(j|y& zm<*OEQ%tm`oM*UyRHYtmlpwQZHnC)z#bjCKWLsFWt&}XAl5P7# zJ*pd-s2*+q9n8{%*};PC6oc(52iwhp?V(_MDcHV0)T7#gRacMp|6Y$QO_?0F9_3c8 zM+YdgBsCeZW}>B6H>(OLSgLbfKK+B1?3M5?!W5SL)@vYWskz5@Z(4 zHJ0qUn5>|j>;_AAlak${WVh?(yXyLYLJ2TSricamPYiaa9PBO&R!qU}QLvJF`R;Fh zzu`rES#)#8JwJS1zclk5uCiV z2u?u+=h}d9B10-lu$eMSEL#&XThns3W-OaBWmBPSsv@>Q4Zv2NC!0(E=1D6vMoqfH zlP#pHc(Ns>Qm0fJ()ILY<>RoGM5fG{*3zse+elaPWLxP9PinI1YDdA^Q!p**dV2D= zvr$`u&4STkSFwY56+4!%VkZ_$mtu9MSbEa+^kkLeQC|Yek}+VRx`?3+%b|=|sIC;M z8-+5iuP3WLA-hY!SurLooT(VjtQ^jqg|nb=mK4sazMib^khGRyvu12qHd`@Uk8(CU zmd&2BaVQ(AuP6U@PJ)CdZ5S@=Nr$SQbfi>0DOE4&n)aur|F~c7T`5iMeW8;#yx8_I|lvWtNIwxqs!7;Kr7O0W6C zlqH);$-*cZPr9akdrinhC5e#$Gh-&PV0l$CPkV>k!IJ+caR#o3^p-5XPQg&_j`oHd%w@IYQmf0@N_U1b(n{X#< z!d+~xc2lrD6l||_O?&e{f>rV1zTeqwOql)b66T1PFt>aO53pDVDb^v1l_y=(-uxd} z)QSQLC{qTrP(%!sUk-Jcg*rl^j#8*&(lzbP{{dCSjmIV6%$O4_+(|LqsdBi}EZi9i zcb3APldfrRUPCyMAg&nh&c^!^ zY*x$zb`>9rSMgE#Dn4eho=~i(6zf@iJz3>=d@ccH&AectUW%b!l|#K|q25raw-oAK zeLY$230W!uXT!W_;Xa7rK9<9MV&OhhxGxm$YkfUg-68o+g3Xrs&a(Xwv;8b*`^BX-QesDT_w! zS^m&Gw5l#Itt;oH4V#y?EQ=;(X-8Sw%ha@PR?kwUHfhQHwwrBD8Eu)0-K-j_*XmduLj$1Yia@shcfFWCT=$(=F{q)Z<5@?DjNV2}inH8Yqc8X_hd zT23^KCGw<1!zqzhy?j?~H!wni%!V1sl8q9Rd6$!oX354-G9OCjTQA>L*9!PafY~zs zELeaTEU+AGEDIJy!GbB+xO(~SZ+*b{%Dxl5V%XHggb87dH=(NWLMhWk$`n?c&Hk?& zW?tnym0_4Nj11eIPpWJ~K5N6tY_`HFSp+4UB2&}u{EuW+oH$hy&WxGHuHtm@D$Xch z#hEPEEXp;Taz)D2v_1a^7j*+-ZmCR78}u5|iCno%63&uIVBr$QaLdc#l32JE6mBJj zOO~l=hyD+^>YSM(=}aqT73<8@s?JxFir`p3(?WI)vDAoS@I8j2=L$S_MtaBnPBS|bD zzQk|9#ST0#nGKc$v-16$)^b0$dXT7}p4(iqWo1 zGFUJLWem!E=0*j>JmzNky4+&p-=^^kY5XEl{I0c#PYv~-WOPgB4jcWhIC^n;^m}ad z5*qzJjs8Fs-J%xJs~G2@B!w08h^2Terg&0L@sy=_Mk$_CiWedZ^Eyz74DwPE!kT%- zLcA73yeWrx%R;=P5Tz92y$Hgt77*1L;e(_RY?zO%5k6Hl!e<)(3l0CZrbeiI6n&GN zyDjsbHNua|M)=7Z;TJo9DOvj9nk;>AO}2(c`0d0Iy;D$C1~U^zR<`Qh z4>r1;IJ$j#bPgLG(da;-tge~L3Lb!?{`jtcUXCd4u!~hE6UQ;8O{ly3aC5&Kh!g#Po7*y2=gK797H2hFe zcujHmz~E4ykkJ!-!oq|Tt=M$KB<5;v%6OK^tMZxQ<#Ux5ZG10wMHGZ{b8V9|T9g$olAJTa8< zrzxG}DSGd?inm;10_fO4;osvbe%?Z=+P7q4#?qldm4~WVKh)TS38sU`)nu?KGoB6( zsmWk7W&#}?T9d)%%tSgktR{mk7#r7*JQ92Gnoz!ugPF*CV~#0Qj@_p z%v3sfT1^JqGSlhc8I=cX{Ap24n3;6+tQw6rWoFaSku@4^#zfK4b1IK+%=(F$OMe<& z`KQgrZWF&n5i^etoX-xFp5&=4Q5Bd4bW}`bxi*ruqmpbayZm33@-nKH&>(82Od?fVURlkw z=4#^GUM7hOt*9(i&AC*Q_r_sXQmN!R&hWR#iJ26twyGi2gqtyys;#c9W?N@9kzg7X zOs~AV>y|fDCWA_4)_Ra@H-YQkx*qiXBxpSSX6n%O`FH&)JDmzoGxJY>Uc zq5_+1?eJ?ytSo3>A3>4k zE~>e^@`Bbmho;OPsw7QQ_bJff;mbBk2RE_ zDRZ0(p7=-IsnP3ynUhrO)IaJ@4PV30oTh4L>OR%q>&{s!c&>p2h5P$F6}<2d&(!KY z3z&;k^U^=+P7Pm=&0MBxR~lr>wC-G`g4Y^IP&nV~RIs2ya#&G!Zcxpem2+6l12cbp zxgK+iYTT|%A*k+1E~Iirb;(=xr}BSP@J?Ow_Q!+yF4Zht9c0#^NDJG zuJw^y?aKg}FI4So-KY9{MfpYrzc-MeaG(C5f#EIq#83j)rQj)u0?aIsU}z3BjWF`h+-p1q4HT_8BHqNPOi2`t{KywUa2Kl+atH)Ikk*770{`* z)2e^<3Dbefb*#11{`A5crV|y^Z74xgrZW}PldJ8KTg|hO8GWi{AXnQXx7t?{FkPsc zVcnboM5_BO_N$5xz#=^oH3G{1+5w+hZXh1nrhnA`pB*HL_x-uYV@c}A*k-iwWD(Ob;(=xM=pm7qPpbmk4G+0 zO>V+&?`Rl<7?cof>NH)GU3dX5WU>6i(ckYPvL3 zJ}WHaN@e?1&Sy2hyZ!x%B20gJnOkL=4mEgwi3u}+Ug%!=Lfx8NC`#)4r@-el|n5j5pK2*WCwiz>H{OE=LwVk!-X?RQky*9A+*9xaR zmR=iF+l-Yz>52)amyfG$#>~VS8&4HNDg(3>D^z*f6*Ga34XreG@+4nlcUO4@`E5)5 z-Y`;9qCf6`{QS)3!v8|cKR?rVDnDSN=%X>69Gg}B08P>F#6ki@s`EJa+lZdCBKq@Z zRb?quac4`0X<}Yx6a!(I(tx?=4aq??_&M=$7AgSj+q@eTDp&npXLqqub=` zKK)9H7c3{MkL@CVC-Ary zg^Lw6;r6Po&|&m-lv45pgrpE3@i!Pn+Ox+4O8Jx#FR(iLXE-G?sfEdXx7Ni^2i z4iZ;>AS${_z?^Y`mIfW+$(a$z@zf~P)V>g%8PziXx~UoV3v~h0?MLj+ZIFY0Lx+H+ zwIA&6G!EWQ_khTCO(5yBV*ca4Q^3loJ?ZE>5lnQQxl_h>;Cct$BEMojVkXEL+U{|M z=mFYX<)9?|C1WL-Gu{w++-?f~&)aj;LT%w}=dKXktZjIbESfdNuu=+;@?FJ zloTGI*^#pOTZ4gng}()6nO=hRT~EXHM;YjPm+LnfJG&#`v)jpb<$j0k zJ)}YDd!q(D`*Z{B%E7uI?-gUzejph>GXlSkV5d2y#Bv09E4d{&Z% z?Cd$m|{|Xiw+wf{*-ukk_FR zcS}=i?7T`kZ}*b{_{lyq(la?0+L;wYTRCso7q=R|8SjK(|31W4*MVG|ss#PNnZx@- z!$|n0AUHVH58Y1N3A+l{djb@nu1;iC5=x$6VBc%!0FdO*qAqcar4mgcR7GMcFg8VMND~ z#1B@GlVGbjldO|jhHiT`Bk$4_@kJ#d5pQl1$M1SX`^FyJbB!B`d#nMm_kVF(Jl&0gKIp@h zfp(;A={Dq@xf%q$JP0mYhx{jplcNP$@Oso92+WrvK|fEzJBkz0&16i0~=e(!jbD8#peMvf0q zhHMsDte=4H_Zfw7h&`lgWCB;d328pV6y#l20&lJj?2LE-i)Lit#T{*7uwUoASHDI< zV5S@_FOG&gR}aHEc?Wzne>7MR4h4;4_C(ER9N9Lc0IQ^ZM-f<;tQ@}EdF&vZ?AU~)$`ip`&+g=m#w~R1Wh%kKwb^zh8hKxSgyhr)k`?}Q$ZPYDXwWrT zq*c>|Yy z#JWfuTP%+!aT?>u#2h|}JklTE%J;yV>{Us2vO2VyYYg(UpV@nO&Vv!9>SRk_UG$+( zZ$zSw+52Q~!tLB1lBeDKp>>LGu<7*x;D5h_o{u!*b}k+WQMdM^{Jp)A>T7*?FgXGn z?`VsDE<2CEt2@HaE5c3etc*?_=|dtyWZ=hRDRODw1hU#Q2_LfEinkdWgP;c@Z%oy( zOA8;|eaw8+*taWj>Us{th;7*adq&=k8Ar()$Bxj%I1WM@FXXhn{RGZGIhj}J(G`AV zwu9$Ia?t!tJaOx`#csURc4BqV8H$HSlhXCCu|=X9q^GOHwTCxhl}0c&)|AI2CxhQ*KbNvp3X zafEX_^61@ef~Nn#BW?BZc8zVw?$kBBQt1O)F`RF-fa zX(pq^k&TIwc{VXhUx0!~s*{z(9WvsFlV9Pk@LJuH=nS>B`+oNXdgs*<8zD8Kb#)rv zGCK)2hhK#4#ve%Q$Rs>(@&XwC(u26Iu)3w-09g666m>0hLCt;n(3P0Oz1JIIWxvhP zs?Ra>u`~f399qJ+9$2Wu0gL(BVAReQ_G*4b#$TqustJ)GXP88LzK%Kq&q(16BmdEN zNzC88{_|hFQJJ{47=yll&?6(-oyUp;pJO+ZC_HB4C&AUQK=gXfQi1-a=l09oRZxKZ z5Ki91IY|3ZF~{lcQY_uB9iZZeSbDBCvD?y;JQ%7=`h?u(Y)nkXhHhLGVslf_?yUw< zJ~auii%}-8nrJ|9_8$B0PIGaa&fe%-zh*=<|4&CJ$Jc587WJQh$tzXv^QF6poKc3L z)!B6P%6$f5^8SOIW*s?!DobI)3q9DRFc!z!xr605H)Nop4F`I6f;Xd@<}LlSkeueb z0%>iIKTRlr``#l_&&F|ZGByCLk64l?N4}D*Mc?qAR`O)K&n7r^LWY#OFF>Eo$8+#8 zBMu1rUeWx$yKY!sr}a6?dxoZCG%1IXC5im&3b*XSm<(f*mqBnq0@6@ATew*tzjL z>NJ0=`p>^X?tiT7e*bekWC!UqXa@=pzK1^LUl8Q)G9#TjMO>OeJcN)J!k#@k3N&4= zU?=6)NHd`axirBIayqXPEZ-!H9JIUU_dShJ&o^MNeRd5DxwwxLI#reHe>N57j97r@ zHd7%Nk9xtB?f0=o-f|LOdIQF&cf(sp9wj$zxv1%wv#8nSPq@pO4X@CM8 z;_-nS2jFw-E~tlEFH#bA2K~3UJ6!tm>rlUqDX{WQGER&djs|ZTM}B-h45t}Aj&grr z((AxJ;^v{ly=&@AI@(SlvbTz{QjsrCid#rrCVaJ*PAbJV^SY8yMSZ02D-hgVwF0vH zT5;4a&f!Q|$V05%NC;dh4cfn~;n9cy)M5p^M+zuu#w z9?#KPUkB)Q$bkg+GJ@l{Ird93M}7{0XeI#Bdg_Xo&Ql}%el@~x+vyYDj}OSzbO2u3 z%ZeDCh!%J)YX{Z}2cZ9OX;|?jG;gTR7kFUDg}xE3$zAdi0-9cR{G%Mr^+~5s!+Q3B@ha$!-7LcwT5KN}t#RmMr@L+w+srgA;v8 zM!Ft+J@^>y@7sf{dtr@DUM(OOrln%9jXH3qSuRQEKO$U#58RkjguP5Rqol3J@x%2= zM19m!?C4;Mi}#!a%Z01pmDF{t-Qy%Ky{sX~yzvZH?NCKaN52qsEAmDbeGj05fj%(h zEwiG?(Rr3uN}lmneB=5FeT*G>mbhT z{0a{+l7e@mXJqr1ykA8;EAzum?W6h46uyN@rXj61k(D~FXn6P>c&Q6~N0ha)t zY3Y$0=Z1irPxt({cXyDr-^UBiSZk3R{yfr^e;r-EB10k%EP~#VMfg#e9_U-{Aofvv zu&sq7xthQb!yP+O?^6Id((^b43ajy|SDgynhtMG#E zox$*iJ<${_1LLI@&@CsO^F4Jn`f^>5tiQe$N~9N{fs>Mvx_$zQ&blG6)4wV($a8=- zGW%eh%T3(=@nnc@Y9$C8Jy#%oJ03f4KZa(1(ZWq~qhLh8OL$`XMe-wauRvLPE(s`{ zkL2#%z*Ch9(S;8kaprPq;-oc-^tzr2zF80Rq8hJ(vtL^Y%u4o?d28aJB;|~tL(w8Q zyT=-D$qGXAzsGQ_qvF9WX&jz@Z?<d^_@PLl2yJB_6FF!$A%*Bhbw&U5VwUc132<|C&gd4aQw(2k*R^AUM?~HQt9*+w`1Dj4krM)rf=FdYhQ!WU2ow5YnJE!n- zrEd145}K14w~phY#=FQ{w~^$f={WG#T8p1d+Cp|N9Yj2@PXj^Ehgkh>Fo~Yn9$B5y zCW(2yA@$v2cy!`5+O}62Qnn^Q&a_>`Hb9Q_`Sl2g6f70o4ZVN|oM{8o@Adf z>E`TC?q75y*}>m9dLz;~Bkm0%&cBX9m%|Jwo$Z?6L7jl(^zLY{?P{d*Spf=1ECRX3 zk+37;V4g~cx%lfP7v%A31v+=u6@RjsfQ!1BkZU`Hb!2o?f!)pn_>_q|QFnfTY(3i% zOh#Y}*N&*Ab^+c#+?cfP6U}MQa}m5DZQ=Eje8Kzv10bb9gS^V^OP&Rp3OYy47R<=$ zO0=VcK~HH8wmTyQ@K&FA#)snTVO?QqTSdsf>u|0;I7&4T>HQB)9Ib zhdZS|@$rDQD0$`~!6fkX zY7a+SA3`l#ECs)yE9hU>i{?Kvh`g#(|7TnO`G0@MA+r1gPQgbtVlZSiDI8=;j-79V z|GTG3dbln?k{ngP3fr7VZZ?zQh*wV>*!TfjcYP-I;qDS_uyW$uxV-D1*VFr7l@*)* zJ%1Icp1;lC)X5R*Ieo7w$J#*A4*UEmMI*u5@HOmLEHR z9tvx-m+V%_5|oU{N7r8d5ZHU(%2QjHNaVitA>)k(;ui`V2=48PminK?Av!I=dxjj@ zdLR;3%pC(3jEnF*&qcE$?+6Al?xe)nkUQ#}Cu#ILnOw;T#u~@(;oJ~?Zi|o%tkwMq zx-}^aPagILOiGWFX^%DYPcB%89`v||U!9%_g}pw*p3;0cHuxky$r;YQW7;SGzC{vR z7m|Rxmjd~f@)~5XZh+A@lCZuFCNe>C#Hyq_Y4m+q{>L7_*uLMK2FTW z4uVPyk|0C(e`Z2avgo6$BmeO`pWpYt=$Cu--T!Bu4xtyDoJrzD zdx6e^M&NSLfH=k*a<{pk!jX^KlhPxW9QkSb#Qa4Xv6vQ03Jss2H4~l4>3MevvE7Oi zm*sGLk6(vAUi;Bo&xgdjeLQ%#P3Aa^-b8pe_mZR_4S0WGJ||CF0}pMH$JuzM1-E_4 z4AjfE7~lBXf!_Zv*fzGPF8&u8puY3(%pdE3Rc1LjBm_Hboch?Ii@cgcK+ApHaicI- zcf&b{882FK7nwS9)8jiiObRUJ`n(S0`l@Pj$N$&L;TzZ4VZCA$cbNNThsTXha%H;j z<@%kEa_~;S#U0B$Qi#B8;c`x+WqmkvO(sI@$z^2Z zgI_2rc_l7h8jM;`?FW9v&hYD7IN}AL7X)c4L;nvyacNvQ5||D|<`YV>d6!M(PpMlf96 zjq{;>226~yhSljE^1qnNknrw9p=*cMpyaBG_6%J^Tuh&#m*;cwnz_qKg7ZP7*8K=t z6cK?PW-lahZxGqxdzxbqtRpZUHXZfPZHbigi*c(te)uryO8RyGgnc_iLc-+^@b2vK zLwCNWB7K1jiS4eB7x{M+jJVXB6c?Z8tZiotE<+2@fgoM-So^XdI6jt)dy+|hZIdD! z>|#muPfr|kv@wag(ioU`o$%&P{GHKw)kfA#;6%rj`Wj#1>MiJ#%CsUgznz^ z;lO!Ua5R_?cg+aFsfuLOi!MY(Wj%VQrA6+k=+OGL;ryFpqK}%+uWkKmfXDAP{342RC&$L++yk`F*}uLaM~aOG;Lq)y$;;>Q_+UyrUb$}wDqWn8lDuQE zxAr?+Fiso3C1{cH1tWmBKLLy4r`T-lSEu+^;`meavg}{n|A97Ga>(u3;b5fq6Q8-> zUXcAJ`_RuBj%3U*2jc(Z0=~D|2cM9MCmQFXVOMc$8edWMmY`1Yt;O+QSS{lw*xU#0 z;#J%+UVQGJ)pKBW#D4A@RT+nIm*TkI-QU7QRX48tz*Mf@PhakC%@z*s7iAqp@mDz- zmewi0jW~XT)Gyn**DsxB^3eY0P89p#6G~A`gAF?efZ^FwFeOrjEZ7~71{7$LKr1z2 z{*zJR+u5AGTgM8PS}!6oz2`%!<~H*A)IiS8n^O62+>F6wX;aY1wL>=tIdJZ&^3lWn zF>u_x6velV#GJJ`c*!gevf9s(_&;t2cQ$C^ftTlCu5&v2gf9!!mTkgQI?sdiSEEoj zt=afzv+FpnpAMO)dsi@Q@p4YYeZm=l=R!bHJn)RB5dDN$j%h>z-WqWm9g!J?=9}f< zx9e}ByFb^$Ve4g_9fdP-qo>o+%sYSv5?Pd-QYZ%6~&zl?VzC6lmpf0%v$ z5XPNvVy^3E?BII>wjpzTd*KPeej;{$Lf~A)D>{MQ6F*9y@$YSwI6=HDnQvi zHxg#EAwCfkhs=UFWc7o?Sc#hoAHMg1$xH;KUDG2M){H~W8SRPo2ZF}~O&Dkv1P9l2f_Gh!$`DHNH;q&BXB=NAw}hpY*WTd9(x_ zI%&&Uqy7;aURaDL`efj&*mHSuFRtKG8ZVIUp)TazLv@&xE{EI{wCMa}6^5;@Q+#uA z{Gmxs4k`)e+!+(SxX+5LxHrG0at~TIcW9TS>ELDE&SAe(7w-3ElN=_DlXe(y@`>xY z=010-{e0TDemnI!zE1Hi8n%9$S=PONM`%10=Z5yx%m8lSy?oZSgoJXhAE<;SQGyal37f-O-jXFgyhFfkU$?JiMq+`h? ze08rcG1)7Jl(R>mZaw-zR}(8_5M>BCdkb*0W=Du1aTxJ*T8lpwzDHdLb%7hVrOB{U z3COfZDYkh0fundd1;ThEp?~{(I4f&D+WV<7`Oq{SZ!<_HGP+T?zs@wUnQ|PtoWI3s z+1wASx$(d;b!{E*)3Am|iG@9_<0lx0c!;kmf#a=GMN!TTM%-x;) z=-k+ty!-R_pvztD$uAoY3Ab=1`6+MmI#2RN%MC)|veh-rIWz#q&fFs4#A7&DvYZn+ zWhI_17)riuv;eXu4ITUM1KwojK;l|jkgsd?4q0`+Onorzy~@10^q*!8@cYA`O{m|5b4{4DQ(PAWgKNto};zomCpD#$neJ*TZ z|2p}nQva21)|Bj>@Ek`z{DlL&cjF|bt7zuAOw{agH{3BonrNxMz@DM4V2h4E%q+-6 z%XjoAx!aU!Jy&Rv>|Ce#mHIC|uV^xUS_x^rYbt!ya)L&~xx`{n0x8xr1~O&vR8c?;i`u!BLy({I6xc_p4KUTXFn#oz1!TpTs&Gix1^$%uC~5E_HN> z{vqSA%}dvz^PLIY!LC7Ex6ik^wsz_cm!IzDZnnzdnw%L=>sNL9e(e> zEE=Hyl8jTcFA=x%hErGTvVSbA?4SRCe5?QK{*{bxUCsPOdck))2K;;fsz}-WrY9^% z)^*(nV7>r6Lu}xvgC^)@6{3p;F|hW+9XPmR5Zu0R2+!`=g6{IAFn#%Q;5sgc-Luz# z)AZGJ{_~aY7u03`ww2~zC~reF0#@Vgw~~6?GoMSk?Ct}Fvh&c$oc1Iw-;cEVPZ50y zjo^eS9Y&k2Rnc4iGBhu;nD(DHzD=&d;|I!8O{>NicApAtx(2L!j!pLLBkPVW5v1fa z%3nL~ES`Ven?zd-f&5(;@UlC72(*YGZnNhR*~!Irc=mC~kZn)qmB!#HC+^TZ3{ASS zfh$#O|B7x4>h0g17oG^Rk8;6aj!oZ;M>5DLXeV}wQYSH!TcfTgcL`*c{Sruf|G>|r z7NUKsgV5*nMfPtsdG!3*^lSC0I;@}Q_JLi$^TTtXWUuf(U9qEklV#CxXzd!*S{}ik zsZ-#BJRk0yT+QinJ{^rqnZr3}dXB?c8zubz0$TS}IzAj!m-uFt;&Y|N{@K=4r?3wG z5#0{d**|+XEyt;EGO^!+cR0LY7FeI{!VT)u3|4&dfR@?&VNd#Iawq%^-m+>weCjTR zV;)75qY;V7YeW=DiijaIoAIF6qqjISGZFH_FQD7+29c|4=i472o+R+!S%lR__>g1m zy5V5e@gz|BDS4{Dg~;W92mSPBU_3OIkh^OTv3xA}()%E8(z6S`ncE9y6k3s>zAxbY zvtzJw%x03j&G3)gpJ(79w?>4qQK(b-=X(e9SK#n`~%;)~<3C zINECC7hEZUlU6a&J$C?Q42#CkI$XeU-xFb?vN4(2dJGx)BAGD1ZXs@`CIl;eLMeg} zI56x8l4+?<7MJ!SE!;6&T%}E9KKVla_SR53_9d1&=Z@rmFvi4(gN~q&J7gz_7vuA zmL@L_+yFGnlNj$a#r9S`$&Y>+P&jxGI;+FS3-kTZoqjQJFKs(+yqgEB4d%dsX}2Nw z>MLYBE|#1*Hw~`~(;-=p-N3~$L-6%LZ?sD{5zqYP2D|t1NRdhgv5s6w#uy#J&s!fw zcDxj{1r-S%Z(N0S##)lzGPjXo_B+n8-SXIb!c5}0wUywqbxR!6JsT^|OeOpKheO1t zDR_#`JaVtwesX{JY%*iec9iP(7CG$i2K#px;3N0Ar0teTPCH*QpNob<{K|$%tX*e@%B}{BL2j5?6Mmk?= zLW=#mfX}pj5aT}^G)4?3bEizlFmX6KowJhsJk$lIXD5=O%bwy-HgRxvcs5zsY&RC1 zABlBe+(C*9Ly5QRdydo3jW`HRBWKgDpwi?t&WKG*3Gb>3GH%uuHCfwMuUDz z2<9p0;I2OCyxek<^f`yz)!PD==10jACu{P&=qWn%UFf5?USz=33FO(<%P?Cv8x1)e zON>^u#UUHhI2`{X-0k{e68(NRcD>UY#*f+xN2kb=-QVUx0>_Pn{|q6IR<1$Zf``O@ zjR1OVy9RU4K1Gfn1L4S{g)lm)BigyOgmc5`3`%LnL;W>X@yDSP@Zsd!NIz;JTqZhD zlDGrujYuOylr9VI9U|!KU^xJEK#RY$Y4kjphLefPkOg?K1Au1kOtNC6Hw=AmPPFg# zh6CoiAp86XociG&y5Dp$94#zHB+eNhKC+x7hit}2wDQP|EIGKJ>qt6Xy8sXKM&kEc zD&%X^>1g87M^GYn0{2+B1THplg~5*sQ1I=&`1$HQ?4(vIn3?$a`t{o!z2R=mVhnJA%ONNp=%LwPqu>{CnJ0EP1W#O;IVPIiB z5atDY!^^?4I54q*qj$Lv1i73grWS|E20=@>>k>qUe$Y0Xz8 zeR>(O9Wn-n&fZGy{Wq03Kh3uK~7>>pUIcr9Ak?kpMSdV|wzv+kje3%F3y#z&wqG@BT9x-FQUoD6IK zBXIpp6TJA#Nsh^_L9i!Rg*1LS7q!)zM7VOh;J9u}lHbFes9NtuTC+C`8CK)(gVn+3 zvMwHXvpXJQGaq1%9I@b7qs(vX@xYG#@P?`R&@pZq>Ncw>iCb%rqV~sb3!cr2$NUxVFZiin@lhlItk6LH_BWxgU+{T5 zFh@%C$r$pr>wg;J{$)Y*AnucihkCd;ufRiBXp@|`y6E1~CgdXI2{NpQz)LTJqn?k( zS9c8&-a1$Q0cN{So6@^AQ_JUR!Y1r$+3DW;kB%DkqIPvm1G(d47 z*?wX;Shlbw+s#Vwt_@RA-nXe_+Rzasq4^E$a%dquD$pb|wtXTWl@)*)(;c74xJ({A zNE2ND@_yg|`*6?uN+e%OlZ5r%1Sh6M3vTk2^7k8zCq0%kaNzT4 z&St+TywzThjETA>ICi`bZhk5lq?atkZ-1U5$4%Rzgq*%)I?m!mbvQ`&ClJC*{EAv# z97ATdTrDW>n~HpHAkyw-b1;AR1uL(RBG0B+LMP>cWP#5;l;3PVDO67d=~H3k;rjRJ z!u91OrM(PjT~fpa{^!tX-DI@y$W%O8FBR^6c!bx_$w4=dSztGZ(V)<4JXpn9;E}m& z!F9$^a*T6NKxD!>?&@EOSz;vC-`R-!vBw=8J~b7FSoK3piz#`F)E;wYc`QLK+YBH} zwl5+-$DYQ0Rb0?UvuB)arxL+F?qob#p_^c#^*Z9$#E@jVgo4tr^PIxAtB7CQB(!l_ zJep(V111Rp1e6f0h!>D`8{VRrt$VP7^?sbQ-U<%%89~-R-w17AU*a&iGTfhyuczPpV<%DO=%2Qg>}2J zksn_A<_PRKqYHtnc9Z2Kgbd1P4^1qZz!1a5DE7rM&>ue*j`?Px!9GJcs+^uA>Be5p zGp#1zH~B91@;-%{Je4A8{Wl@UEpxE^wGzRN;)TR8qZ@oOibT0*N8wfDH{jWw_u~hL z*As#2F0=&iA(M|S#*c@`pk#+o`0n$BG+n+1)LPFar%$_)bH|lo#tJWpy%mp=hF?T0 zZ;i)J@s|&MEaa2P@1^0(kyvuo+LX*_QAk__Ey$~n9!UGVAvWH5i<8jyAr`ouBd7q# z#}4h_ZL28Q`*AMxJ)KHqx}DC;9^Mbu`31r>scU(KB?Hir%VR;w@VmXkXC0I+*AaXT z-=YppN1;5E96%w7Xhu*VDwyB~vy;QnNU63Y@!1;GGG-Z!93q zM6I(!{pEL&xN~wG8^;GI*yQA&eUBt$s-Ui%o6G$;+y zph-z3L(yO!GS5RYg)$3~p|kI0NF^GT<`SZLPHFu2^nQBZ=fm%Pd;0J{U(T1a?|rUy z-+S%ry4KqFX3p=KNkrIjM9|=m4|W2$D(-_7yk^MGY(&ZO5ZW1#0DVr{aOwC9q5{Ua z|6>B?yeWs8>NuPz%Y(8b0Vs9p85!v$3$o><7#4hibPv~p&rJcSB1mGw4~il)FA5jF zox#?>-EK}tECizsSg`sPUDj1!Bgyj~o+Z|OtdPd`gP<{YNp26C+E zL`TTbHAL6*0dRAA4Y7B%LKBTn^gf^qyIg!>qKz~Rb}whVWFOJV0Yl(bP$vW>{93n* z*FcEPemV+7z*nx7yCgb_+t_9T{?)d)A-ab-EzqO}Jb(JK(3`ey84E*ho}tV8R^j8$ z;TU-c$y%)~6sNY5j_2kCOIMKVnFHKepDRRXWfd;DF&tHAzb7wd_tV)u?nL6$7l`jM zqy6?@>5!|UaNv$I%oAD)HJ>X;vu!vm+-Z!)!@rPe6H2%X+Aqiu!;7S#`&X{_GQtSm zFw%`D8UDZ&I^|Rbwkrw2^%Jq2S)4Q+ZnY-+Cmw`gX;E~TmrSQTU54Z18d0^S3Mb{< zCHuC&A=Z0_W5MWPMzC8N3?0L;=}|Clkm&{{_$awKRF0IjY{vjIbNbAr4!fq+kl|x* zP{XclI)2JiIg%w9b@QZa+Mnw@G zNZkX2MlmQz(Sp@y6=>tWao`m{6UwZIu#rc0a1UjMq1KLG?(M~`OsRqzzKm#xZ`amC zMTs^H$%?15Uu{9f_K&oSSTa9Mx8Q_*lQ6SwG!E-hCUO-*z#0PYHreOJ2|kooC?LK6Nrr`~}s^8`%drqq9LS zzYMgmxkGGGHaN>?gSWsATo)~XC8rm{58oXSEmuP77sZ2&!x<2gOol68VVgMjz7TfZUf(3Z)!(GnK;SN=}ah(e?91VHPD@e_$UV8STKgm-rBt{RrnCPV} ziF#E+8Uu^CFW2PAnfcXBzVmus@8(OSUZRec50o;8*4mMZv?qeYb&HVrul-bwl(?K+fo9P|fYWd$f|KMd#WC$c4G0VKaB_}9I;o;!PXB0O2}24ANs zG-wr!wlrr zOpiaoKld--Z@qDv|9?rW{wSj^{;eQUzVOYdd>aQNeqG00zQfz;e2w{% z{GOAPpY%eEpL?{Q4P$}tZl}%{tDC{U=J@Mdrh|PZ{)jzY5W!#Pxr{H{x|!d(X9Pb@ zHJabD_5y35euKTvljlE_8o?jD5Y2bh*5=EPoAA4jYF<&M*Pq}w{0sR1@AW51A>M!g zAG#JMbN~2noIm**M!iph^(`6ja{fd(6KDjDnUmrES~-Zmvy6nvv~b_Kv$Sr1KUMuO z9-j>NMLWezbTeth*<0TIu7kc+dqG&C|FeIs`#1dSDa{t(E>!@pnF4TmIUk;n4g`+} zelY0B!Wj>B*gSq76a;0%%t`wpN9$Mq!&DIYk_Q@BBEfZ90PtVe1D7Plet8}aDfa^) zz^eev#!rJI(p$km_86!Nc|fRJ3P@H*LgI}$_~4oXQ@aO<ZMtabsK@ zx8BhlCAKP2O_N(>bM+E1i=D^J$SeKr!$+pAc={*!|3?2;=C+l*gj4G<=hGGH6C$7= z=FFyhp3i{M>s`3VM#%XciK8x|KbdZKKkWJzPPr3R%w@$kT0OgvNgTK&PgK!}1&$7|RrLYAp??xTt+1t@QG6JLcmFCl!1A^jnzbql2Zkm=hH*XR zTFoIway#r9eF;TlL!j!#1FHF83AvWvNKcuDVDBLnZcgz9><|~BTMW;V>m3B%);Azm z$cM7?e!ukOY?z`EfI}qj;i0}IsPxbrT#cnbC%P7EURr>E>1ceT;XuzlNJ80+CfH}< zjy8s|D00VxWd7j6**b4p-Zh!Lm-E1x%@g5e76HcnLWLaxGL96s)Q zOB)V}gTE%ldz>Upd)!E`I0b>EXb9-*=EGI-Z@h7L%IV5?;Y9w$Oc*UStHrgI|nVIw~la^?VMfdDY^yYyp zjE~WDVE2bpPdOoOKW7TYzj%(%5O>^mGy#h5xsxB~CgYG{FR%uei^^Jq{)$3wvQ8I!(sUnR&r>0@R$02lc@gwr++ISe& zw1J*hl;S3+#-OuT2OZ<&L5k*@agndLL+qPS@=2)*ySD29D-}i7ua2d*{Av)r?1lRZ zM*(*&n$F8?;_?gUVy{{ZV|8By^#f$E$>23jytERgKHCbwAevvuhe+CwP zRfI*y&eBs4wRnTqk5HM71=z6a7**h1fY4q_TukDyT1yI3zxR+Uqh?aMn5nd)HVL91 zkAaPs$IzY0MKpcmSZE%mL3*zopl#o0;nN%O^tMwaJ+n#}e8U~!kZ&NFtq@EjUl_yl zl^G<#=sEUE4Pfh@qr|7Sh1#avqPDY>DCO%B{T((?K3|Q#@*738pNlu>FYO|mEgsTU zhZbV! z^^BnA+-D-yox|CwNC1}F!l3;H=9IHC^K?cq&(`G@$xh5C0dgAn=IV9weytAdb&H{{ zdt>0btRpVgYJr7cBcMleI9ro!j_=ONpoVlSxmG^|u8PcuGd*jGiI5Q#OJ-t;o*Bfc z?k3vu1E8wM!PZ@ruKFy6+8M*~Lv1#wsuR4oMGQ@v{74-64C<32z=2l{FB>CCK#?+d zen|mUk46l<`;ji&lS;$~MA4Z`hV%MLFlBB49N91*6(*;X?O)!2rDzX5>$CzjxI8Ac z<}1DF`4U?_Z_(?M(rEoD35Yh?3aS=Dc(0&}>rV+pX%BtyU6cvmKSZPQuj@9jF@=mQ zuHiEJZ1FZPg&K7vGmFS{kjj+C>CN&WGPQ#zA~po1zV*{qhiG_Vx*kfTL{N~i3YA|h zz@WHRZu+Vo?z`9~nzUdxXx(29bBGhL!-8n|hht=FT`UB}tMj7Drvp#T0DtZj(2w*6 zeZE(`amjjV+u@|-$5ByThP3+03*c>L4Nlt^h__nrify??wf%2a0x_N zM40Sp7Ld(Hevr~PM=|tGC_ETfNStn(VeZtQ4NcXWUhA+hR@~0#6>%a%QYF|eXr>$VJh{16a@6tIPt#&;k*Y_>+dA0GXl|Tqa+E6ctOW3aE3zx zHQW%93&09rBdW)EcyrWOPAYvYsLOgXW2#2Naf5L3^WqvDQaGAqDoleDOGaYsvKPcc z^E}$-e&PnQN8;&$#k6XdJy~RE$Cw&;f!FEb#KiX$k?YsvrY?^s9UE-Pk6QtPheLy~ zLT@;_1dpd1sXGSmXrlbN6QJ^?5z2H=6EVQLWjUwe*PEA5u}1OG_(Z}=y#r4Pai!$N2f z3I%tWN^n|j2EG-)xW|=JP&$zUk+s{PvMCUB!^0z|CXuLFF`&3*1DYkuBBwEqyf;gRA@)z`RFi4& zG3*VuD{mxP>m7z=^XGu2(Na$Pagab|rWnpK-3EnnA+2;(xVsKZPVQn9yzc;y@< zMJ-pDX{lp>&;OmFdy@YI|KIRW_D6)V*XG}5Yj~}!8~2TkTvyEQtF2|XL|tWXE=ypE zZw>4ArIR&}e#u@eeaptaZ(@BL#rY?UGFihoNq%cS!@m}t$gZd|XD?mpV4V-wuyK>( z*gK|+*abeh?6yr=EPvJ=cH+A*cK%9TcFT1U_BqvHqu*>~H>D~6_K#k;CPw`;eo3LB z{~iA?`Sa_Sy2$jO%EpOO?YKbiDAP78fb_M5liSO)etVN;fx!<|ejTmy53yDMA7g7< zm`wWL#P%=fC%xJKfquGjWfW2AI7N;n*umF)f0*{c018&TrmwVras9{bgzshrH|%BU zu9^pQIBUm!zQUvSrR5kn-+=@yp9E~oe!AQ(n5T3r8$G=(Xheb$My?YfU6$o=XPZ8p zZgq$0&ATY?bs1GV9R<6areV9$Mk4nmlS-&0Q5_v=V%u0q0@76k2}T04a_A54X?zBvqO+mNGY-Q(CSdWm)y#wyW%OR}US?)Z6LEN+LQ90RP}s7abD8}K&v|~L-)G(> zt-&vOQ@#zu>f2&)X;LjUENLJ%q5V96+p#dQ*$GzJ$Z~V`7^3OzSBzz5G&S5+NAe^t z3FbR`!b5F79O+m~k3`oH)oC)gQZ^Y!&e#r9YVGlTgEO^#yNvkkjDieJeQMs(#Tcz! zj*(|4;aKZ+n7w`y6C!4bCb?atV}B#}smh41RODm%LJin&tp!b$3fScEf!la=ykRlQ z3X72w86wc0F9BtF1V#x@rag&a#I!FM?unnKmp^tvN`5Xxzf&Nx-wN=j&MrZV$2c?( z359&|bM(o+^E73oGKyWWWD4r@@rbu3$&ENcE)2L}#iT2kqnaS7tGwbdsnXBLBJA8aN2WZv`Em?aPo^Js2#X(lx;8ADGw zu45*C*MiMMp3_ABUIN$1;aOh zv_=a#su+v`n|9F4iH3My`6P~BEQyP%UD3_m02MY@kyo#0z_!{nx;H`_78|nE$2)Cn1$lXKKdhrRmviJ;f)IP{;HCu(}q<_%mAAouX_rUZOqR`{z4eQ*8V$zE; zaz9BPZ=D|k3Uyj&Jeg39oFF>gJ{ROi93y+D^njGyA&fnp1=q8V)54EsoYK{s#Jx@* z);sMcFMG7%@GK4J(o_Sjo31o_k~KbwJdR!?Vqxd!DB=~703Hs9p>p&_D%{El(uPb! zFXISsS$~a?+dVW5t4ZaDbZVg~O!r+~3PT@9agQe@lC1>}P_QeR*pFNXH5c1q#zrkL zYpeoW)B&ITCVF+80emqqqaIIR5K;LvjAGMt^jrOa&Wf(!)hAB@nWwV=$4`Vk$L>*? z0U5}THT{9u{W5u9chOG6|IXv~EZT#m>gh}>=n<$fjP`S!WQTdfFhd>p+lyazPw)X1|H9LV*%fbVx* zsJ$Fd3Ktw9_B&I_tiUwl=39@W4n>hM$yXQ~iDHcCTt>_D_T%aOkvM_G!F8b&dVQfO zXXe&R3x4Sfv0ofIpmnmq;n_~ST>1fZ6XQ{L*-RArkVlsvoQx{FJ#m|G4tIl#5fqs? zgWfK26rCYM3!kjPF3ka2;w6Q~WA?))1qC`R+<-=pT}?M-{-8p|8<}>G9B>#^h0eu$ zVZ?_>6jQ0C6+Z%TT3#_87p^A#yabqTYKyy$eI&7SdAM`V4p=Jukv4CRfK;}FB-Rdv zmN))bFwPtwe%*w3M=7C+OeRxXqfM@}?eMasmOOn}O6?W(aen?&Vjg-3EEBx(Ku$Cb zexl3RN)=EQw|RKVDj!F_xlR_Zm&bSZW{_eN3Xd;0P$NAR5;<+2Y~O)6f_a_ zz%dRHxIXCzMMbVD*pOj5xIG0tfHUWM*jEQd|Lu_V97ljL>@ zWBIyl-2HqfRXNs3-Z?&?T6_1xFlQF_PkX}CkrhX)?oHHg;u@&dt;ROzaC*%12i5%i zlq4J;1xkyAP})6*z7Y4X8g^wWE^)}gF^o^KyjzC4#FwTt&Ap57? z;~YYQK+@5Sp4S)+8w|uz$VUz|w=PCwv5&N>@rrTWja{IAaShmv`9OE4>BDxJT;@V_ zKcqEULfa52Du3h|Bh@)b1UF9+=ha2nKlKiwvX0!1?kD7N^={B?E&t7*oi4;o{{#J` z_c!VX|MZ+r&%0tMt_cLIy2IfA96+;Z6|Bm=OjavJ!{FMIe|*j-);9|x2J_*G<6iLU zodT1Z2z-f9hbZw6FmU0&_AlN%MkM$T__N;Ms2}|MdH(xnWu^o3c5fXS+q;HriC)e< z&5I-n+ZK|`^X=d8t=9Q3JF-_zN{$7C|3JC)4-lvQRiw4$DeRmpXoMOwtO;PzYE-R(ZlhZa($;d7w-V=yV0 zA_UbMT8zZwUZNJA_8UKY-pb`q@c#|}!p0|ynNo6t9Pbc;vMrT@q!cux|!X3gaT1lF%aycypAyV&oh@0wJ&SX^UlTAaLnN7Be z7Ys`4^|K7L=d4F%QBX(H&`iBr3ERMLHT`)9V^&%ij;(ce$GNpFOho<_dwn-!=<`%#UBcFau>Ep)^+thR zbWH@-@4E_%THDD6trn8#^NGkkoD4suhO&9DzmlC>Hp8_mV_BbVL*al3`R~5m|j80dki#!FIou@MNF^QZ%cfufGXOO-!IQkU;;@beQII8KQ3c zK>y-UnD(t28Xrdhr#=%}wLM_`(Huyx{7hCo{xAPgE9OzdpTz%f@Tam5>zMNU(X?#d zP%JtshJq9YGAwN>&ar0c$mwcecl11U_@RJ5&*YG&T0hCf({c1_#yNWBQWKL04{3za z1~O${3g@qp$-O_5LhFmqkpib?E;eu{T_*3&Gn#ja^NxQ{ixyv{k2)%8+t6-$cR?i) zwNioU!iP`)3;+2s3IB`zU;iEd-{4PmuavM#T^4JUYe0Sii}*2?tf(mj*DWh>ZD2AT zD|!+Y!e+sw$h(;IG>dzkD}kHiH-Ssx1dNOqLf^Di*tn;I=oUocn*%;@D@zIe{YJu& z%n!Iebt-yG=P?QPQMAfs4+=gBLGQ&F>QtFaDjyR3_HE#I|Bcc&lL>!<|8MZ8Zv$>lvej1@@urWp7m;PtN*4384qRl{?FnW_4J5MPNAF@6SZ!dV zzv;3I;wo8B)f#rVTsQj=yx0sLkCjh|i9P^B)g}u}Kpd)^TV9>oR3JyMgze z9n=kEReH5phc0_|;SS5+`A4_=V&R|Q|Nn$P&G}FGllB5(8gRJ|pEOT^EtU?1el3Bn zg$H5O&ztDq@|wu1X~WKJeQG+rk7RXOVbw_^`ruYR-8CkUTE=fA^X7Qdm)}!iJ`us} z`vKfe?M1}Kv>Y|6J?QzQ60G+Pz$=m&u&rtZZ2ZCizQ50j{744adJRM279LP7=hQk* zL!!Jk9u)dcy0@v)GmCW~=Z-%$OPhpV>0v#f25(|0HCh3hP4$^&Gi$Hr3CvSnc3WQY2<@*u_C9{MF+iRczX9NUr% zX}gDm+?^oOFS&(m`xpTEvrcndLp%NacntE#77>fBZ(yyV6?rx63+WFIBdHPTRJHLl z`YkUe<6jq&q9wv$zpay~@ij?`P$PKEDJ_N}0jevV^dZ^o+K=8ZGqW{t;(q3E$PgCaO^7oz4wzHB64svIz2aA|c z=MwUH!F$H^j4E^|^H403Pt9iDputD;VE-p^+A>2Aex&6wPbao<2SU;~ZJ~vNo)N>L z!PAqTYyL*46KLGCb~#=8^aqRNxW7epj!`CkfMkZPC>k#(SCe|+_&9`BR17T-l9V=;>1$S z^|FPN5{Gc&y(6&B`8yq5;znjvQ2g4llz!g(iY^$}N_#w1!E?k5Zsqx0T;F4l%adc6 z@|`^6;U)Scv-LPwm-JGrQZ>9fw3z#{fxx!A0pRUZM^-Sa>Bx{0T9jqW7!?RWUbG0! z9>&tt0wow){gXRhr%t78d1U>Haiq$#21YG8L9#+SV0xz|*~kO{)mMkLfu30ZxPeL! zIb!_j!YMq(Fy!^jO6<;GK-P+zlJ18K@i{XZ9`Yul%Z5o^SbcfS8H6oG#(#GctQEx zrC9KNG_JlLNw=SNhm;FSpdTZ`9Q3~lnTu!P!ORe5>Z@bOX-~#aX(qHeYy#9&sgN#9 z9Xy|J&fQBZq$ef~!;y{lplsMOO5J%v98RF$R_E^ib1%I6KkK9T;{zZOeR1LljPNIYab7 zFc3x`<@!PNfsy!Fdl=HAjc{>}B-Qz8L~c~LB4J+AJ2l~Ox4fBdl5i)AFawKThEX+< zqxk(oB+>5VK=q^>cK8nirEl9{$-*vTku{zenMa~pfC&Ptr5@ieOxDB5dq7 zg5x6!(8)NP^!@lOI1rEkaVAaFz&eznzPqsfa~3ohZ>1{Tl5A?AG>YHXg4;WfLt4=m z?2O+H{-vMEHL-Z0rVq%+1YIH>`;t3->p0F{w-h6Gxqx~1Fo^f6MZCL}TV0Vz8n=w6 zK~X`_rIkYE0&h^<>Q2$*@OB7QL@opr-T+xF@^=w520yTF2Oirr;L1 z(`tx0i3Tvae+mQx;?BE6v8wzUeWyDCn;w-g50u8?%wbtnzP*HMd->Lz^WTxs8!u_b z;bM5Sc@Ot2dXVPxY?#IR%VFw^AZ*s|#4EZf%&9$VP;%2FJiKl=oGFTCIMd7Im1+gd z?%791KbS|9W;tNXf!^@NZ4^~Dz)O0i0b_=dV7RCDf*BPukDQm z_bRJE_Qx|SZJiCD=bR&_w3G2;bviMQR|QeCQOrvDWB36BaQ8|=+lEz2{s=fl4eds)Mp0tpHXfQmjYe@fjlb8O1e>~@J_!nOWqQL3m8W=X_5JbH@49jzNK;*LbMnY3Uet&5~WPYc>zQADawr zY6GAWkpp8G4JhzSfaU=;NN)=TnTJJi>Wd3R6lDL_Kl7!`cK-?g`y2j6`x!etVq`>} zW=7N8878YT? zZk={vx<7Uax<*}Pa(YgY3k!3&zO&D%^+^r1NK_*W4U|y*b3WBMQ^|symBZ)5L+~kec4|Pq+tw5I_&+eD^k91 zD^mP*q~brsb;kb~SKGow{eK%*%c#}FnQR4Wo=5gxm#yY%pdnk^# zP=eHCfAqhbhj*(1OzKPs72Zyk8l0wL?X`GtiScio&yME|`-AnP|2O=L_s`z3XI|;@ z$4%4b&wM=8#HUV@-(K0t2ECljAJx~$E(zMl_iRt(`|V6*+41!(PezMBOgw;Z7JZ4` zlsJYT=n~2<$~WNW{L)Y(@~rsX`x;nhjgu@U2Cz=Lr`V!6d%pW;AHIBvBA>Y}!(V&K zhwuOK5_{CBm_H+5?05gp<2AJ4Pw@YZ`qN*~Px}AK{!?zfE_n4&Iq8F6_kzq26iJ(k zszbBzpx-vKIz=5*?rT%~*QY>4Jcj;UH3^Rhy&^(RqwrPK6tFVfPDY$jWbV&ei|YsT zn44QeaSc;VbT1{5?$kI~|FeZVZ<0XvRxN@}U82PPz;d$dR5n+ps0ChEVo>D30*oAN zK}Gp&V1_5dX>m*3{cR+S|0*Ci4t}HwcCPsKh%>wq|3MBY&cQh&l*sFrYI0`<4~53K z0+-H%170s^QC$P!i|AorEMiY*C@8+mB3gyFNkr^kG~d-h;@I~XZgqs}c8?}Sc1dK1 zZ4J&ADJ7HQL&<{;qXo6hShBBq1TMEu#|47>_%r1Ny>dL2+FqmNqEj~vADT^7s2K$L zHA9$%IvQ) z+a|~pc_#&UmAZ|9?Op2pKmxOJccGuRIzE!N0*OhYOoLJ#MvC~O`X2Fy$r57N(zYL0 zbTpAA7eu)r{YdV*HsbhGo!p(;RETPk`=vL0sb}R6n4RR0vj?XDZ~O^ZxUGiHygtax zjf&>HjGSok4<+ooIu5+I1=1h3{aF8U51}tYKq0f7IWC+9@mpr0liYI>KV$`1^^SnN zGEJDRYCwW@ACnV(`{3-)96a6lmEMz$#Ef=3Iz93`S}jc_arY$fVyf;h{+dqqdtb$I z60fMmhyXASwIS~uj6ijy6RsZlk{k%ygJZ)OE>153Gf&z>$+i%ztTLrDz8hmkcQ(X>NAjX&7&Lw!Pxaih$c-Oc=-~b< zBwZnd1k?sYyqFRmVbh7y@>AqucplL?a~&SfNu}AQmZVf^1}vQD1KA%#$eXVLf*tE8 zpwhcX)Npt*21X0;@Ydm=c|wUhX_Wxx1U?x@paq;SiLTc)9_I_u>=bXk)|WM+F}&{0ctJu?YHhOEUp?~Q1>w~8EJA}(;l9=vzq6RFdv zClll*<3yeBB&VQ~R4>V<>h0@MwWXJ2mMYVhm;Q9HTcRP=Yy~EM z`c9P&!~!ndft}B$(3JjhxINB+c(9IWz}-OetG3*+4U@s&R1XWylE5-hl$xJg2A7h= zv9&%49}M-x?K>hscElYj6fE6fQz1s?of(eJ2V$VFE{GaVOT>hyZy@!Wl)z<#Gm_hP z>C<;7QRCAFNEO>eT_se=$PuX+*fE;e6;)#2Qdb(`X#v*1baLm@^VDTj2${nZ#>)zGWFDCJ4kcy>kKtgQ zF6i48QsrEGvUYe7EwPsX$Iu|$)OSaa zVWWUHo7Bib-)Ne!b^~5{7y*4&>*!X?1hVenE|h6{NDZsoxz(2QvHsC+WZg2UvP(A; zHI5H+N7SNgV;{3=*fC_^)X|$>!c1&I4Tg<7NHhYT5J@dB_-5TpBt;dO>QP6T{qr2~ z>E~?b-DE$q@B*KDA2i0CJ1kbrOTqKc7l6|7Oe#b#Qqd2yu`xpx*If1l*V5x8*)EiD z4)5rN)7doGSrdy})rh(EUed2!K{YgwlI2_V;jWJ^+7wTOi5R>c-2HH#aU}^wf=5ys970 z&Y1UGKRh4TaOMy6lm6eRAG8!tf&%$F@TJWfEEbEvtpgb#et82K`Y8^)Xgf%@NP&&W zdU&O*4~hy1Cr{VIba4Tx*5|?Ow0%(0z5_Z{>LBcsK4{IK12QfjiNtw5*k6|i-ZO<* z-3^MMZFmSCK6L{<$8>OHLqP4o6mUAX8>$K%f8+PqWWXOMGz(K+UWfw5U$Uah#!A7v=JUkmzzsoDstIqzumt+#iW_rfh9ub$RX`efC1igG zL$^h32g!~w+7uK^!?m-CmA5308-Kz0%z>jsWxxkbj6%5EEt+&zO)qn1bUI@;O&ROt z=h1Z{i+<}TspbUJKf(Vu>Ibt&JcGGT8whnYf!Pm+u%U7z*kF?naD9&vT|7sfy`BsXv5o#w((>gg!dy*Kp>6m&q5WeA)yOtmMv5xU%B|cVM3sj%^;pO08T6 zM6(7I^hN-0r9U1Z7s!^&X|TO$A5L4VidkoriTx%1@A*HjzT)Md;Qt%XuXOae^Cwmm zvs-S?;;(wG%-6}7$Uo40i$89AJ1ZI4#CI{4;Oj-D@PF94@tYEZ`NtGe_;k+>e(S;s z{Ora;R#Kvl^>uULOJB}pWvl#H!Q@tU@a_`6gs}%dsd*MZaq3L|y_3sLK8N(OeDx6i zSh2DEvWnxZ$AszpWTj=l^^e35!T;j@;s1{RZ}=yFL;o252l}VWqXl?FW8sk6d!oB6 z3s#7>!H~AKFsnrmE?%&N11&uGBApBqW~hRecNqj5WWse)0*ll}LhiC2vTT7Wl+S+( z#wrPZw z7A7xFGZBRPO5el;gM zM5W2%-a}mT17CV|b`rzbu_S%g65IrnjS&wqY$Q8J%Ci7v!bP!G>FsG56s=N zUA%wcw=kG${wMhVhJVraGzNMkPLZC%$HcFsfpG8*^t)Qg?VUlS|9URT3iv@JKh~0v zMTawUF}ZFbOhvMBdKKCh5;2$ffn`0OHj^!aj?X3I4y)|M3^{mx11YkiTZ! zaR$-7HBh;70prQ;h4RXGpk(d}8K0~{UZV(X^>|Qw=r&W`zX+nwr$X$^?O=Sq1|pNw zU}$^>sI_MiF98KyGYKAcZUV=k6gar`5a{1J40>9rU~oVMY{j?1ysrcXrEkD{`&&@p zZ~}(fABRqzqTl&TuPL-lp2n{~|o%Bb~qr=Ph68ilxO|)#INeTD4^h+b5 zJHww7ztl~I%lPEseF4p?y-s+aHe#plQ|7T*46W_oO+-ziiQSDbjBZV$s#ZEsu;V*@ zde8vFPcDZ9!wC9PR2Y4$>zMxc4cx7!ft(GSj5|hd|DAvLZXfq2{mTY_gFkr)mGX<5Bv>mg8@6!iHFnzbG*(RP7Q5oH zD7*bYEgKmV$huq4WnZ+XLRhUXfA8u@R&s4S>)Rn~SQsr3ZLK$|>mTq>g=>q5F~QZ#t107}!NnXqtqHdHtity24_**=Ea9!0`wv{Q7l#>azx#4mj`L@hW7llvMOp)3vP9+<}9 z=e5+eVgq>o*aR}8{J5O&7eLv49PZ2ANJQ;YLFc0sv}z?`XzD{MD|-kQ#vdW^!2u98 zGnJ9sBaLHi!eRbYb-HHZ8u~K-Fs+M@BI}WU_tUnXbd|}qD{Br;$M8S z(`ZP;rX^G1v`-DV1>azP3|&L{^4l@)P$hTq=m};>?-F2gUy_wo!E{nOiJuXZJQGsu@=nFj!;aneU1UEk1>XsnGQ%_#XsW~z*Z4>bVk zvk2$DdLGWXesBry?(R;4ySqzpmviB(dUdProB1(&de+{(*4{NgW~QfGPxXpG zO6Jn=`G6s?jn2VQkv#nyzz?el$n#_C6!(l@mnIc|$U!B-s)l2Y&_~CvS7aKDXNDgt zzl`*_lv5?O(H@1bIUY(pi&?YwfT;3FIiPcVaWwju+!&I!VtpseNXaZ18W5>gwSEYH zsD1j`im3{1!lW!c-SSdN9(Zasy7w`JR|yU_(vjO{4U|^!L?n^{sFJ#ngUE=)UaK(J z4W_9I=tVxQY46J9i#j5$E4w*TBO|O)wkBPv?1D?e z(zB2wxZnHSWdMC1heQGWsxC3#w_-C4^a3W!Z%5`(GoV#B-FJKX7=8e%fn703J%m01 z7Pm{Vw#YBScE}T<7?Y5OrcP1#$o2zuWSav zzT$2c?LEl%=e|;QSQVQKQ-B%maB$Op>T0P9OlbeBmxAz z;e~hKDwBKSl$)#8#;2xHYN=e4qT;!o} z`(8SBfj?`|*wf4w=bqM;z&uP|xo?1|H~s)na?SST^lT}{lPV!`E+mWrI?^x;ZOWZL zwU0pG@XKtaO_tKh+}{IHv$Bp+IR}4Tdb$#8yL%$opbaR87kcbn4=;+jy)vC-0-)9p zA;G)DgdK{heBh@72YN;< zyz!iYm`9Ak<~r+2?SF77Uod-bmhbF|8n#?#<)&DDV-)>p0(5>-Vl8tg+}`Rs@p3F= zW))tgQ?I_8R>+!8?aH%xzkG!ezmcq?Rh?^5hwV~E()_8FuO;DHp&K!-+DDRq%@EI9 zBF-a8wf);%(Xw)YoNX_=PxRZjEw4MGZ~HV`zv4(&kZ^{a7)_Z+9uY=QaYI;9d71i` ztg1%x;S_Fv1WvCyJ2SHeq*TYE=LT1|K=pgNpJ+wyZWP7c=iA!zMkx*rW^5Nn(oWCy zeWe*dfkd(0r)2J2Lzz@}4)y!#L*E_2jJ;S8Q!Qet7Rj-=v@BysFex@f7I?p^C3-MK z&ZK1;UfzruzaWL^O;z`GT*vjx;K!OHXU+(%E>&3-*||t7^-apVWs?J%8W4qE=@xbE zWL2%x(W1>IqI1z`N37gg5^{3>vNXO^W#mvcClGkN7o=|53tzExO`t0gVBF{4AvEOV znHNwP@@(@AO3tneTRd-g>&iQVQVSzz&-0W!yemblrT`Lq&a2_ltF)f~?!}o(I%FuK zGkUphp$LIpNF4a7B6_c16FMsC!bm93qjSbtg+>#r#S9~%3*l{ z?lielq$dRv&*uq=B!4ht;V5X}v4DAOCBJ5JF!NXvO=C_fuyRL~4yfR_nXyPk(`+3# zAO0->y#UpM{{SyeJ5F8M>^=`G)|K`5cDPz`WHU9JBr%zpU1-^{k(7j)2E@XTbWBDzDL}Qs_}due7k_rv0rGiE)$ab{t=ru) zO`pJ*JWOwmEz-!y0avrtEXLVSLT1H0<~A;`?{)J#4%wG-i^WcO?bCD`w$AL z_b^XUBUl2bX3@M^y8i8%NZ;lfPoxw$-TY)LNP}@!V#OGE#qGVch^y+Odlpcg92Uj- zrOmH00wrC~iA^q~ZVM#J2eau9{u1~VzIL0u^?Bq{a!qE4{IN0>(pp!gcGzcGE^CQi z`ZXRHqiZacrUIS*>`iTlq{bNN0wr?PkWt+KT)V_<&@OAKzc44%M}TEF2=riBEGSX&%K$?li9KqgP0DM z7lf+d6cT}**XjY_`pZx*K7+t1CZIX&env5i3yu9UMb;UNQ0KuJIL@exQd1Fv^&yxATWs{C3?p%9(tUq& zt*!m>0S(aJl)uFzx+gsYFPH?w&Ul#Y>gaeGCe;X+Jkchr+`Qw4#O^*ebj1WmTE5-M zIA1klX##yj;SQgCUK_Ovi6_>sqg1p!k8Csr$S9&3S`moLuzCqVUKx>8h_#9BMsaZ3 zS88g7=0iGyP{i`WM)Z0i6hAEN&hPD^B3?(^=JFbG=HPkSwZD7*r<@&W;Zm?*o z72TEAkSwG4wYGx;Nhh|P9MbH~&7t;U=&}EKUkDAUNn&uenv!d)kYy3^CAmB6i;FFX zZXawX*$-$0IA@rm+R+{46QR~^9hb9R(^wH#UA%odA5S%HV7 zx$7}pH&Q7T5aCB=DUv+@ zU9_+XfyrK;b4O_dH+0>m$4E2KNGC-om8k|VW3n5jD^k-R)T0z@U2-0-J#WlX6~n7$iq_-eB>zyLMO^kiCJ#luY`nP zrB@fVAU)Zs*EYamI;L=xfdCuFPk)8e@zMRAe(HyP*E|dh3C9f=a#@3QbcRIF;9^^r zfZS@XE3(ixa{C)gbk{=8bc`LO_89(QEY6b}u_Imr8%_dK#)I$|fl~(O#&Vp=Gfd5v zyG(cnmnBFuY)vJhTC8;YW`ytYdWamfh_$A|=EmeHk`0>93UzV{If-ysOUW5gz$h#FM4(q+z%LBLd1(j(Vi6Om*d(*l=FY+OKXY_2jO9eG^@ZaWG$hM-bRa z8l?;v7EBKM_b;I>8BLJCikJHj09*qLLjGwTFuLWvY$1v~M;axnkzZ@g;w*IdMg8SO zqH5J=C;xN(<+hU?(!ASSB6l&i510bgbl5FQ_zE1y;u%V40XL#H4r0!MHye0*G`rd$ z#&Vx606%F~4>LH{yE3@RB8GP3th*E^M}H9zW|YM;66U4&8uUF*Y8?sv&@{JYU*XbK znW;*Njn*?gWCU^X8<9lqb&{U@DXOI3h6ZAu##c$2jDKeN{(2^BuC>RC zE~!PC^Nn)R{;l2SwLmu9-g-=9XKfU6;v9}*gFTh}(U!_+{AX#WQH_1237z8^eXPbx zQ6H_BZcDQ_?;L+?=*UQW$)ttJo|ouv_fCah_)i4fvx4H@9%_cU`N6!)m$=Axv(5VH->Txak5ob_`()U?t7BRcZN3FWV0u8)TeEghga`p)%+CiksevL5$f-_+qubxRZ z0v$jte3X85z_3v7D)AcI9ANEaBCf7X2VW?&<>hBQYt-9Zj^6%hIxL$(j<=IHt$&Y=bqU=j#4piw+>WDrk&S=rTI+4+Y zd6$b>kn%)hA3f$XW=$tHuLmgMyUvgvm$bMuVoW zyJ{BA$ZvfC2n;?v0pT+)iRw86>dA<`6*|}~>&<;5HAqrX+=!^nbG^mQtXo3P@JQ{} zurgG!JxHy`_FmuF!XOWOIwZi31vqGe;Pc#eb8)-JB-NAy%Zh_ApcO$t40HQlmIq&W=bZ+|yt*aHWyz@My zM%8-6W@`cLmqlAF&)Cx-TOPvUFUL{OR!3S=;wf<1)fbQ)Z?Sm}EHeZ8H2M}?GFi?# z4{%=OjKO(~KinVoE|Z^PlX5}j2H!vzk8eWGGO*tA*}DzSF7{R}qE@%&TZf(-IeJ{n zeHwQBnRV;o$^j>@21dSRv02cf9Hh5ZsLrXZJ)MakT-+{GBAIdZ?Y zHJ#lE{kY!L`cv>sUdp?~}+V@4Hj7wFQ}9ntUB;`C!#4i-d?4 z^QDS5jS9H#Fw!||Srb=F`U@T*ob4?AjLmDII;z@*rrTAFw3j8Xg}W2iKE95+Y50*^ z0VI1mUvWx`g(-1OPN0%5%t+j2Li2Xr^oe6J0behrLvCl)VN2sEubI7J<`{L6aPiqA zV!mt8dB#_2IXf7mAy}X3t64L{+q!Cp{5YL}WQ`;R{@{e5`90K!iwWDfp8!^EP5!n3 z)f1q5n!CmerZdayb_wTogV7>YhPZ*u#|U6L2@=x!^>?Nta4u+Txm7iHM)B+8!Z zDA{RiI!{otTx zg`j*!kG%c2csJ)>nrqXTpJ>e>DpXd099Hr*IMke||83}ea8jaRyC2rrUb_+3jVNm? zCD*_8aFCk!1#IEG1^Ms&Q$!&vbf)4x%-M=$9(8w}W_!+&gQ}HnWx=n3=2P>B z2)zA>_{PFNvHy}bnLz9>iaXsBkMp8hLk?fL>={#SWT0y`P^h`ZgRW_*bZxzT)6RBT*!^~7K3Xh zje;)4_bf+nT>X1WKqmLR3)yXczv+E} zg3c|=k~c4?6fx4P)1meC)7SJoB{gn*I;|H0U)wE! zg_9ivcaHHn9V~d#fIHG)T?69N{%WP-zzuRqSwJT2Sl{=*|8`}|nu8-<&-_RAinB{!!v}&>!;Zto$4i^#`eZ=O!cv4s3<#!HD@+tYx zZ0_`1@(d6>UnbPJ1Wvnx3xe1@8+C?v^nO*h4B3&Q9N|+$rJo1%OKRuid^Z{IP6GYi zQ|@+rPg%au@^d`PMNQ~~m`*HA>2bV=nR@0J4(o`1U6CZKr!~sKsYAVPU&$L8R3)KP zfUucaGwNBAYj7*1oui@Y^=eI1B2f}96|T$SttUHUW(vV)W!W0zr(&aA2CJLqtN+IyAL9Kar2b~nOp;I1>5r%d*3kD z!8cbCcqKPPGCaw9SMFSDPSr8&chP$a7m!V3X?tF(hM0J(P;X0Xpxrw6GCQm1xdRP%C!EY{AqT(mwz(+nfn|*N_CRb~{O8eehbKxK)AbqJuL&)Gcmj~C` zczG3(!PMS#?e*$?99Cjb3fg3Tmrqj)ayUaXTF(=y;O0-zYkI~&MQsoMM9>R0)lwOyPQgKc zk|N4Z)7cqG*&|st@0b07J2>?(cYJ=pRX$&`;#OH5Q!nB~dNfXh&V^^|+u%C3~LOEc&La#t~ z0hDsV@)CL|N#}HT?_jWZ$9vFy-o2k=y!!f0W!zHgjSaSwJmi%ZHiTY&Yxm?_V0d=j zFUHQv_DAbvV-wqJC@u>NJ1YkZD;iu>D4bjf3ZWI>*qrmFA?q&54^g>j`L2)AdQ9vr z?6w}0e#YTJ`c4A3I*#9-=<7if#d;GkDViJT=$lm&auZJ*Um5hbq-Tx#VDak4vp1_4 zDy1e4{VWc#x~d{O4%<9uKjxk|$M777oUzj^)^N{LJRN|=oj$AW`vtXrFlnc0tx=O` z3_Dw4ahy&#J#OjSo~TnR`88QO71Ut)-K6v{ZffqK*LmAZHG9TFLmNPp6?btOT@exZ zXzOjwLm8GEI(7T9Q;5Hs6NI~xTJ#%t}-iQ7ti-Oy*)dedBZED@vt_pzT1hw z14ZzJkEv)yS@S3nU6gwSVInJ|$!gsicK#;InkVr` zQT~mY--=e0l^dk>Y-ECOS-D*>qonePLM2q`h}^5gxdMilCByk&-ik0v1mSAC_bP6g(NoI`i{?sGLwzRHGp ze-!ohIbiM4;g2-Zn7iZ6uF5LMpN@e_ZF)rY&@acUlEpK2zn$2Wsb{rMS;%g`Y@}{xj!9+=HD&ti(3AXw{ z5Zl;N+9KI7xzx^!Qa-z$Z(s;6Z|m(&I>(kb-j6eaYNM@H7(Jo>sNeZ@O;3&BI5SKp zRNb_>hRn#Ut_!DgqzXHdt_uB_V-1*~DUTCrd5h%Q1V3Y5Xq@Q}AgIL&K|uP$`L(|9 z+ao42sA02c%lISiUiOT%iwM%#(x~7LQ058ZBiQw1Xu($G2c1ztz4$=J|J+SQ5W3_( z-Sc5}SC?Be1JnN4wioi@CjZvGnSjjmWP75ah zhOnC*AkwSO_Bw0H4!xQCdF2?O_LKFK+7VLOOL@^}Q!56hcglzCCs@gbrutVRXx6(y z!J1r`zNQDu`{GlHIevA~|41N|D1-%k;2hpdb9W&>Wx$RWZ-stxj%8BpjkS0LEaBd8 zeu^<^Zwi?Q%zktv$V8KLE&BVOnP0l z+VZiuoFDTwu8O_gr(V0B{AY#l2tSsZbZK%J$Lmga zJHA_~$^PdbiE~$R{2$pjWz35IjB#inGlmN?laTvB-BeEM*kx|F17mX)%R2)JT^k$>tQZ0e4DF-!r=6*pje)(9qtQ<%1}i5!+hApFg?^?loickDO5EQq zBK;BVp?+0Z^7)H*jL52&IJ*7F6hZVRHoL-qp=T99*p<@&CtV`9P0v3fq5VnYuE)6g zW0hn5?zZZ<*$Q80+D*Z*E60bYrf|mbeU2k3+I1L#ZfZK$FPkxII`o9bI;|jO-hyRA zPTD<|`>d;Epckq!G?!R;Uk+W^V(sHk70AEh;1piMW=Nw*8_K2qw6E73g~_* zu>ykkYS%X^_aGH3O`InU35FGwWWgaYA^*1$%?s`@irHXbR?6UD2p^UBe_KJor)6vA q>}Ka^&0z2RfBpV%1^?I2?IYU%S5GBbXqZn3#K-RTaRlXjp8XH(PXqt} literal 0 HcmV?d00001 diff --git a/clustering/tabnet-classifier-tools/plugin.json b/clustering/tabnet-classifier-tools/plugin.json new file mode 100644 index 0000000..6c65b41 --- /dev/null +++ b/clustering/tabnet-classifier-tools/plugin.json @@ -0,0 +1,672 @@ +{ + "name": "Pytorch Tabnet", + "version": "0.1.0-dev0", + "title": "Pytorch Tabnet", + "description": "Pytorch tabnet tool for training tabular data.", + "author": "Hamdah Shafqat Abbasi (hamdahshafqat.abbasi@nih.gov)", + "institution": "National Center for Advancing Translational Sciences, National Institutes of Health", + "repository": "https://github.com/polusai/tabular-tools", + "website": "https://ncats.nih.gov/preclinical/core/informatics", + "citation": "", + "containerId": "polusai/pytorch-tabnet-tool:0.1.0-dev0", + "baseCommand": [ + "python3", + "-m", + "polus.tabular.clustering.pytorch_tabnet" + ], + "inputs": [ + { + "name": "inpDir", + "type": "genericData", + "description": "Input tabular data", + "required": "True" + }, + { + "name": "filePattern", + "type": "string", + "description": "Pattern to parse input files", + "required": "False" + }, + { + "name": "testSize", + "type": "number", + "description": "Proportion of the dataset to include in the test set", + "required": "False", + "default": 0.2 + }, + { + "name": "nD", + "type": "integer", + "description": "Width of the decision prediction layer", + "required": "False", + "default": 8 + }, + { + "name": "nA", + "type": "integer", + "description": "Width of the attention embedding for each mask", + "required": "False", + "default": 8 + }, + { + "name": "nSteps", + "type": "integer", + "description": "Number of steps in the architecture", + "required": "False", + "default": 8 + }, + { + "name": "gamma", + "type": "number", + "description": "Coefficient for feature reuse in the masks", + "required": "False", + "default": 1.3 + }, + { + "name": "catEmbDim", + "type": "integer", + "description": "List of embedding sizes for each categorical feature", + "required": "False", + "default": 1 + }, + { + "name": "nIndependent", + "type": "integer", + "description": "Number of independent Gated Linear Unit layers at each step", + "required": "False", + "default": 2 + }, + { + "name": "nShared", + "type": "integer", + "description": "Number of shared Gated Linear Unit layers at each step", + "required": "False", + "default": 2 + }, + { + "name": "epsilon", + "type": "number", + "description": "Constant value", + "required": "False", + "default": 1e-15 + }, + { + "name": "seed", + "type": "integer", + "description": "Random seed for reproducibility", + "required": "False", + "default": 0 + }, + { + "name": "momentum", + "type": "number", + "description": "Momentum for batch normalization", + "required": "False", + "default": 0.02 + }, + { + "name": "clipValue", + "type": "number", + "description": "Clipping of the gradient value", + "required": "False" + }, + { + "name": "lambdaSparse", + "type": "number", + "description": "Extra sparsity loss coefficient", + "required": "False", + "default": 0.001 + }, + { + "name": "optimizerFn", + "type": "enum", + "description": "Pytorch optimizer function", + "options": { + "values": [ + "Adadelta", + "Adagrad", + "Adam", + "AdamW", + "SparseAdam", + "Adamax", + "ASGD", + "LBFGS", + "NAdam", + "RAdam", + "RMSprop", + "Rprop", + "SGD", + "Default" + ] + }, + "required": "True" + }, + { + "name": "lr", + "type": "number", + "description": "learning rate for the optimizer", + "required": "False", + "default": 0.02 + }, + { + "name": "schedulerFn", + "type": "enum", + "description": "Parameters used initialize the optimizer", + "options": { + "values": [ + "LambdaLR", + "MultiplicativeLR", + "StepLR", + "MultiStepLR", + "ConstantLR", + "LinearLR", + "ExponentialLR", + "PolynomialLR", + "CosineAnnealingLR", + "ChainedScheduler", + "SequentialLR", + "CyclicLR", + "OneCycleLR", + "CosineAnnealingWarmRestarts", + "Default" + ] + }, + "required": "True" + }, + { + "name": "stepSize", + "type": "integer", + "description": "Parameter to apply to the scheduler_fn", + "required": "False", + "default": 10 + }, + { + "name": "deviceName", + "type": "enum", + "description": "Device used for training", + "options": { + "values": [ + "cpu", + "gpu", + "auto", + "DEFAULT" + ] + }, + "required": "True" + }, + { + "name": "maskType", + "type": "enum", + "description": "A masking function for feature selection", + "options": { + "values": [ + "sparsemax", + "entmax", + "DEFAULT" + ] + }, + "required": "True" + }, + { + "name": "groupedFeatures", + "type": "integer", + "description": "Allow the model to share attention across features within the same group", + "required": "False" + }, + { + "name": "nSharedDecoder", + "type": "integer", + "description": "Number of shared GLU block in decoder", + "required": "False", + "default": 1 + }, + { + "name": "nIndepDecoder", + "type": "integer", + "description": "Number of independent GLU block in decoder", + "required": "False", + "default": 1 + }, + { + "name": "evalMetric", + "type": "enum", + "description": "Metrics utilized for early stopping evaluation", + "options": { + "values": [ + "auc", + "accuracy", + "balanced_accuracy", + "logloss", + "mse", + "mae", + "rmse", + "rmsle", + "DEFAULT" + ] + }, + "required": "True" + }, + { + "name": "maxEpochs", + "type": "integer", + "description": "Maximum number of epochs for training", + "required": "False", + "default": 200 + }, + { + "name": "patience", + "type": "integer", + "description": "Consecutive epochs without improvement before early stopping", + "required": "False", + "default": 10 + }, + { + "name": "weights", + "type": "integer", + "description": "Sampling parameter only for TabNetClassifier", + "required": "False", + "default": 10 + }, + { + "name": "lossFn", + "type": "enum", + "description": "Loss function", + "options": { + "values": [ + "L1Loss", + "NLLLoss", + "NLLLoss2d", + "PoissonNLLLoss", + "GaussianNLLLoss", + "KLDivLoss", + "MSELoss", + "BCELoss", + "BCEWithLogitsLoss", + "HingeEmbeddingLoss", + "SmoothL1Loss", + "HuberLoss", + "SoftMarginLoss", + "CrossEntropyLoss", + "MultiLabelSoftMarginLoss", + "CosineEmbeddingLoss", + "MarginRankingLoss", + "MultiMarginLoss", + "TripletMarginLoss", + "TripletMarginWithDistanceLoss", + "CTCLoss", + "DEFAULT" + ] + }, + "required": "True" + }, + { + "name": "batchSize", + "type": "integer", + "description": "Batch size", + "required": "False", + "default": 1024 + }, + { + "name": "virtualBatchSize", + "type": "integer", + "description": "Size of mini-batches for Ghost Batch Normalization", + "required": "False", + "default": 128 + }, + { + "name": "numWorkers", + "type": "integer", + "description": "Number or workers used in torch.utils.data.Dataloader", + "required": "False", + "default": 0 + }, + { + "name": "dropLast", + "type": "boolean", + "description": "Option to drop incomplete last batch during training", + "required": "False" + }, + { + "name": "warmStart", + "type": "boolean", + "description": "For scikit-learn compatibility, enabling fitting the same model twice", + "required": "False" + }, + { + "name": "computeImportance", + "type": "boolean", + "description": "Compute feature importance?", + "required": "False" + }, + { + "name": "targetVar", + "type": "string", + "description": "Target feature for classification", + "required": "True" + }, + { + "name": "classifier", + "type": "enum", + "description": "Tabnet Classifier", + "options": { + "values": [ + "TabNetClassifier", + "TabNetRegressor", + "TabNetMultiTaskClassifier", + "DEFAULT" + ] + }, + "required": "True" + }, + { + "name": "preview", + "type": "boolean", + "description": "Output a JSON preview of outputs produced by this plugin", + "required": "False" + } + ], + "outputs": [ + { + "name": "outDir", + "description": "Output collection", + "type": "genericData", + "required": "True" + } + ], + "ui": [ + { + "key": "inputs.inpDir", + "type": "genericData", + "title": "Input tabular data", + "description": "Input tabular data for clustering" + }, + { + "key": "inputs.filePattern", + "type": "string", + "title": "FilePattern", + "description": "Pattern to parse input files", + "default": ".+" + }, + { + "key": "inputs.testSize", + "type": "number", + "title": "FilePattern", + "description": "Proportion of the dataset to include in the test set", + "required": "False", + "default": 0.2 + }, + { + "key": "inputs.nD", + "type": "integer", + "title": "nD", + "description": "Width of the decision prediction layer", + "required": "False", + "default": 8 + }, + { + "key": "inputs.nA", + "type": "integer", + "title": "nA", + "description": "Width of the attention embedding for each mask", + "required": "False", + "default": 8 + }, + { + "key": "inputs.nSteps", + "type": "integer", + "title": "nSteps", + "description": "Number of steps in the architecture", + "required": "False", + "default": 8 + }, + { + "key": "inputs.gamma", + "type": "number", + "title": "gamma", + "description": "Coefficient for feature reuse in the masks", + "required": "False", + "default": 1.3 + }, + { + "key": "inputs.catEmbDim", + "type": "integer", + "title": "catEmbDim", + "description": "List of embedding sizes for each categorical feature", + "required": "False", + "default": 1 + }, + { + "key": "inputs.nIndependent", + "type": "integer", + "title": "nIndependent", + "description": "Number of independent Gated Linear Unit layers at each step", + "required": "False", + "default": 2 + }, + { + "key": "inputs.nShared", + "type": "integer", + "title": "nShared", + "description": "Number of shared Gated Linear Unit layers at each step", + "required": "False", + "default": 2 + }, + { + "key": "inputs.epsilon", + "type": "number", + "title": "epsilon", + "description": "Constant value", + "required": "False", + "default": 1e-15 + }, + { + "key": "inputs.seed", + "type": "integer", + "title": "seed", + "description": "Random seed for reproducibility", + "required": "False", + "default": 0 + }, + { + "key": "inputs.momentum", + "type": "number", + "title": "momentum", + "description": "Momentum for batch normalization", + "required": "False", + "default": 0.02 + }, + { + "key": "inputs.clipValue", + "type": "number", + "title": "clipValue", + "description": "Clipping of the gradient value", + "required": "False" + }, + { + "key": "inputs.lambdaSparse", + "type": "number", + "title": "lambdaSparse", + "description": "Extra sparsity loss coefficient", + "required": "False", + "default": 0.001 + }, + { + "key": "inputs.optimizerFn", + "type": "enum", + "title": "optimizerFn", + "description": "Pytorch optimizer function", + "required": "True", + "default": "Adam" + }, + { + "key": "inputs.lr", + "type": "number", + "title": "lr", + "description": "learning rate for the optimizer", + "required": "False", + "default": 0.02 + }, + { + "key": "inputs.schedulerFn", + "type": "enum", + "title": "schedulerFn", + "description": "Parameters used initialize the optimizer", + "required": "True", + "default": "StepLR" + }, + { + "key": "inputs.stepSize", + "type": "integer", + "title": "stepSize", + "description": "Parameter to apply to the scheduler_fn", + "required": "False", + "default": 10 + }, + { + "key": "inputs.deviceName", + "type": "enum", + "title": "deviceName", + "description": "Device used for training", + "required": "True", + "default": "auto" + }, + { + "key": "inputs.maskType", + "type": "enum", + "title": "maskType", + "description": "A masking function for feature selection", + "required": "True", + "default": "entmax" + }, + { + "key": "inputs.groupedFeature", + "type": "integer", + "title": "groupedFeatures", + "description": "Allow the model to share attention across features within the same group", + "required": "False" + }, + { + "key": "inputs.nSharedDecoder", + "type": "integer", + "title": "nSharedDecoder", + "description": "Number of shared GLU block in decoder", + "required": "False", + "default": 1 + }, + { + "key": "inputs.nIndepDecoder", + "type": "integer", + "title": "nIndepDecoder", + "description": "Number of independent GLU block in decoder", + "required": "False", + "default": 1 + }, + { + "key": "inputs.evalMetric", + "type": "enum", + "title": "evalMetric", + "description": "Metrics utilized for early stopping evaluation", + "required": "True", + "default": "auc" + }, + { + "key": "inputs.maxEpochs", + "type": "integer", + "title": "maxEpochs", + "description": "Maximum number of epochs for training", + "required": "False", + "default": 200 + }, + { + "key": "inputs.patience", + "type": "integer", + "title": "patience", + "description": "Consecutive epochs without improvement before early stopping", + "required": "False", + "default": 10 + }, + { + "key": "inputs.weights", + "type": "integer", + "title": "weights", + "description": "Sampling parameter only for TabNetClassifier", + "required": "False", + "default": 10 + }, + { + "key": "inputs.lossFn", + "type": "enum", + "title": "lossFn", + "description": "Loss function", + "required": "True", + "default": "L1Loss" + }, + { + "key": "inputs.batchSize", + "type": "integer", + "title": "batchSize", + "description": "Batch size", + "required": "False", + "default": 1024 + }, + { + "key": "inputs.virtualBatchSize", + "type": "integer", + "title": "virtualBatchSize", + "description": "Size of mini-batches for Ghost Batch Normalization", + "required": "False", + "default": 128 + }, + { + "key": "inputs.numWorkers", + "type": "integer", + "title": "numWorkers", + "description": "Number or workers used in torch.utils.data.Dataloader", + "required": "False", + "default": 0 + }, + { + "key": "inputs.dropLast", + "type": "boolean", + "title": "dropLast", + "description": "Option to drop incomplete last batch during training", + "required": "False" + }, + { + "key": "inputs.warmStart", + "type": "boolean", + "title": "warmStart", + "description": "For scikit-learn compatibility, enabling fitting the same model twice", + "required": "False" + }, + { + "key": "inputs.computeImportance", + "type": "boolean", + "title": "computeImportance", + "description": "Compute feature importance?", + "required": "False" + }, + { + "key": "inputs.targetVar", + "type": "string", + "title": "targetVar", + "description": "Target feature for classification", + "required": "True" + }, + { + "key": "inputs.classifier", + "type": "enum", + "title": "classifier", + "description": "Tabnet Classifier", + "required": "True", + "default": "TabNetClassifier" + }, + { + "key": "inputs.preview", + "type": "boolean", + "title": "preview", + "description": "Output a JSON preview of outputs produced by this plugin", + "required": "False" + } + ] +} diff --git a/clustering/tabnet-classifier-tools/pyproject.toml b/clustering/tabnet-classifier-tools/pyproject.toml new file mode 100644 index 0000000..5e64613 --- /dev/null +++ b/clustering/tabnet-classifier-tools/pyproject.toml @@ -0,0 +1,31 @@ +[tool.poetry] +name = "polus-tabular-clustering-pytorch-tabnet" +version = "0.1.0-dev0" +description = "PyTorch TabNet tool" +authors = [ +"Hamdah Shafqat Abbasi " +] +readme = "README.md" +packages = [{include = "polus", from = "src"}] + +[tool.poetry.dependencies] +python = ">=3.9,<3.12" +filepattern = "^2.0.4" +typer = "^0.12.3" +vaex = "^4.7.0" +torch = "2.2.2" +pytorch-tabnet = "^4.1.0" +scikit-learn = "^1.5.0" + +[tool.poetry.group.dev.dependencies] +bump2version = "^1.0.1" +pre-commit = "^3.0.4" +black = "^23.1.0" +flake8 = "^6.0.0" +mypy = "^1.0.0" +pytest = "^7.2.1" +ipykernel = "^6.29.4" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/clustering/tabnet-classifier-tools/run-plugin.sh b/clustering/tabnet-classifier-tools/run-plugin.sh new file mode 100644 index 0000000..3f9e094 --- /dev/null +++ b/clustering/tabnet-classifier-tools/run-plugin.sh @@ -0,0 +1,93 @@ +#!/bin/bash + +version=$( None: + """Tool for training tabular data using PyTorch TabNet.""" + logger.info(f"--inpDir = {inp_dir}") + logger.info(f"--filePattern = {file_pattern}") + logger.info(f"--testSize = {test_size}") + logger.info(f"--nD = {n_d}") + logger.info(f"--nA = {n_a}") + logger.info(f"--nSteps = {n_steps}") + logger.info(f"--gamma = {gamma}") + logger.info(f"--catEmbDim = {cat_emb_dim}") + logger.info(f"--nIndependent = {n_independent}") + logger.info(f"--nShared = {n_shared}") + logger.info(f"--epsilon = {epsilon}") + logger.info(f"--seed = {seed}") + logger.info(f"--momentum = {momentum}") + logger.info(f"--clipValue = {clip_value}") + logger.info(f"--lambdaSparse = {lambda_sparse}") + logger.info(f"--optimizerFn = {optimizer_fn}") + logger.info(f"--lr = {lr}") + logger.info(f"--schedulerFn = {scheduler_fn}") + logger.info(f"--stepSize = {step_size}") + logger.info(f"--deviceName = {device_name}") + logger.info(f"--maskType = {mask_type}") + logger.info(f"--groupedFeatures = {grouped_features}") + logger.info(f"--nSharedDecoder = {n_shared_decoder}") + logger.info(f"--nIndepDecode = {n_indep_decoder}") + logger.info(f"--evalMetric = {eval_metric}") + logger.info(f"--maxEpochs = {max_epochs}") + logger.info(f"--patience = {patience}") + logger.info(f"--weights = {weights}") + logger.info(f"--lossFn = {loss_fn}") + logger.info(f"--batch_size = {batch_size}") + logger.info(f"--virtualBatchSize = {virtual_batch_size}") + logger.info(f"--numWorkers = {num_workers}") + logger.info(f"--dropLast = {drop_last}") + logger.info(f"--warm_start = {warm_start}") + logger.info(f"--computeImportance = {compute_importance}") + logger.info(f"--targetVar = {target_var}") + logger.info(f"--classifier = {classifier}") + logger.info(f"--outDir = {out_dir}") + + if not Path(inp_dir).exists(): + msg = f"The input directory {Path(inp_dir).stem} does not exist." + raise FileNotFoundError(msg) + + if not Path(out_dir).exists(): + Path(out_dir).mkdir(exist_ok=False, parents=True) + msg = f"The output directory {out_dir} created." + logger.info(msg) + + params = { + "test_size": test_size, + "n_d": n_d, + "n_a": n_a, + "n_steps": n_steps, + "gamma": gamma, + "cat_emb_dim": cat_emb_dim, + "n_independent": n_independent, + "n_shared": n_shared, + "epsilon": epsilon, + "seed": seed, + "momentum": momentum, + "clip_value": clip_value, + "lambda_sparse": lambda_sparse, + "optimizer_fn": ut.Map_OptimizersFn[optimizer_fn], + "optimizer_params": {"lr": lr}, + "scheduler_fn": ut.Map_SchedulerFn[scheduler_fn], + "scheduler_params": {"step_size": step_size, "gamma": 0.95}, + "device_name": device_name.value, + "mask_type": mask_type.value, + "grouped_features": grouped_features, + "n_shared_decoder": n_shared_decoder, + "n_indep_decoder": n_indep_decoder, + "eval_metric": eval_metric.value, + "max_epochs": max_epochs, + "patience": patience, + "weights": weights, + "loss_fn": loss_fn.value, + "batch_size": batch_size, + "virtual_batch_size": virtual_batch_size, + "num_workers": num_workers, + "drop_last": drop_last, + "warm_start": warm_start, + "compute_importance": compute_importance, + } + + fps = fp.FilePattern(inp_dir, file_pattern) + + flist = [f[1][0] for f in fps()] + + if len(flist) == 0: + msg = f"No files found with pattern: {file_pattern}." + raise ValueError(msg) + + if preview: + ut.generate_preview(out_dir) + + if not preview: + for file in flist: + model = pt.PytorchTabnet( + **params, + file_path=file, + target_var=target_var, + classifier=classifier, + out_dir=out_dir, + ) + mod_params = dict(model) + + model.fit_model(params=mod_params) + + +if __name__ == "__main__": + app() diff --git a/clustering/tabnet-classifier-tools/src/polus/tabular/clustering/pytorch_tabnet/tabnet.py b/clustering/tabnet-classifier-tools/src/polus/tabular/clustering/pytorch_tabnet/tabnet.py new file mode 100644 index 0000000..04d4c16 --- /dev/null +++ b/clustering/tabnet-classifier-tools/src/polus/tabular/clustering/pytorch_tabnet/tabnet.py @@ -0,0 +1,241 @@ +"""Pytorch TabNet tool.""" + +import json +import logging +import os +from pathlib import Path +from typing import Any + +import numpy as np +import polus.tabular.clustering.pytorch_tabnet.utils as ut +import vaex +from scipy.sparse import csr_matrix +from sklearn.model_selection import train_test_split +from sklearn.preprocessing import LabelEncoder + +from pytorch_tabnet.multitask import TabNetMultiTaskClassifier +from pytorch_tabnet.tab_model import TabNetClassifier +from pytorch_tabnet.tab_model import TabNetRegressor + +# Initialize the logger +logger = logging.getLogger(__name__) +logger.setLevel(os.environ.get("POLUS_LOG", logging.INFO)) + + +class PytorchTabnet(ut.TabnetParameters): + """Train a Pytorch TabNet model and evaluate it on validation data. + + Args: + file_path: Path to the tabular data used for training. + target_var: Dataset feature with classification labels. + classifier: Select TabNetClassifier,TabNetMultiTaskClassifier,TabNetRegressor. + out_dir: Path to the output directory. + """ + + file_path: Path + target_var: str + classifier: ut.Classifier + out_dir: Path + + @property + def convert_vaex_dataframe(self) -> vaex.dataframe.DataFrame: + """Vaex supports reading tabular data in .csv, .feather, and .arrow formats.""" + extensions = [".arrow", ".feather", ".parquet"] + if self.file_path.name.endswith(".csv"): + return vaex.read_csv( + Path(self.file_path), + convert=True, + chunk_size=5_000_000, + ) + if self.file_path.name.endswith(tuple(extensions)): + return vaex.open(Path(self.file_path)) + return None + + @property + def get_data(self) -> ut.MyTupleType: + """Subsetting for train/validation,extracting categorical indices/dimensions.""" + data = self.convert_vaex_dataframe + + if not isinstance(data.shape, tuple) and all(el != 0 for el in data.shape): + msg = "Vaex dataframe is empty" + raise ValueError(msg) + + if self.target_var not in list(data.columns): + msg = f"{self.target_var} does not exist!!" + raise ValueError(msg) + + features = [ + feature for feature in data.get_column_names() if feature != self.target_var + ] + + cat_idxs = [] + cat_dims = [] + for i, col in enumerate(list(data.columns)): + unique_values = 200 + if data[col].dtype == "string" or len(data[col].unique()) < unique_values: + l_enc = LabelEncoder() + data[col] = data[col].fillna("fillna") + data[col] = l_enc.fit_transform(data[col].values) + if col != self.target_var: + cat_idxs.append(i) + cat_dims.append(len(l_enc.classes_)) + else: + # Calculate the mean of the column, ignoring NA values + column_mean = data[col].mean() + # Replace NA values with the column mean + data[col] = data[col].fillna(column_mean) + + if len(cat_idxs) == 0 and len(cat_dims) == 0: + cat_idxs = [] + cat_dims = [] + logger.info("Categorical features are not dectected") + + features = [ + feature for feature in data.get_column_names() if feature != self.target_var + ] + + x = np.array(data[features]) + if self.classifier.value in ["TabNetRegressor", "TabNetMultiTaskClassifier"]: + y = data[self.target_var].to_numpy().reshape(-1, 1) + else: + y = data[self.target_var].to_numpy() + + x_train, x_test, y_train, y_test = train_test_split( + x, + y, + test_size=self.test_size, + random_state=42, + stratify=y, + ) + x_train, x_val, y_train, y_val = train_test_split( + x_train, + y_train, + test_size=self.test_size, + random_state=42, + stratify=y_train, + ) + + return ( + x_train, + y_train, + x_test, + y_test, + x_val, + y_val, + cat_idxs, + cat_dims, + features, + ) + + @staticmethod + def parameters(params: dict[str, Any]) -> tuple[dict[str, Any], dict[str, Any]]: + """Segmenting input parameters for model and evaluation.""" + exclude_params = [ + "out_dir", + "file_path", + "classifier", + "test_size", + "target_var", + ] + + evalparams = [ + "eval_metric", + "max_epochs", + "patience", + "weights", + "loss_fn", + "batch_size", + "virtual_batch_size", + "num_workers", + "drop_last", + "warm_start", + "compute_importance", + ] + + params = { + k: v for k, v in params.items() if k not in exclude_params if v is not None + } + + model_params = {k: v for k, v in params.items() if k not in evalparams} + eval_params = {k: v for k, v in params.items() if k in evalparams} + + return model_params, eval_params + + def fit_model(self, params: dict[str, Any]) -> None: + """Train a PyTorch tabNet model.""" + ( + x_train, + y_train, + _, + _, + x_val, + y_val, + cat_idxs, + cat_dims, + features, + ) = self.get_data + + model_params, eval_params = self.parameters(params) + + model_params["cat_idxs"] = cat_idxs + model_params["cat_dims"] = cat_dims + + eval_metric = eval_params["eval_metric"] + + if self.classifier.value == "TabNetClassifier": + model = TabNetClassifier(**model_params) + if eval_metric not in ut.BINARY_EVAL_METRIC: + msg = f"Invalid eval_metric: {eval_metric} for {self.classifier.value}" + raise ValueError(msg) + + if self.classifier.value == "TabNetMultiTaskClassifier": + model = TabNetMultiTaskClassifier(**model_params) + if eval_metric not in ut.MULTICLASS_EVAL_METRIC: + msg = f"Invalid eval_metric: {eval_metric} for {self.classifier.value}" + raise ValueError(msg) + + if self.classifier.value == "TabNetRegressor": + model = TabNetRegressor(**model_params) + if eval_metric not in ut.REGRESSION_EVAL_METRIC: + msg = f"Invalid eval_metric: {eval_metric} for {self.classifier.value}" + raise ValueError(msg) + + # This illustrates the behaviour of the model's fit method using + # Compressed Sparse Row matrices + sparse_x_train = csr_matrix(x_train) + sparse_x_val = csr_matrix(x_val) + + model.fit( + X_train=sparse_x_train, + y_train=y_train, + eval_set=[(x_train, y_train), (sparse_x_val, y_val)], + eval_name=["train", "valid"], + eval_metric=[eval_params["eval_metric"]], + max_epochs=eval_params["max_epochs"], + patience=eval_params["patience"], + weights=eval_params["weights"], + batch_size=eval_params["batch_size"], + virtual_batch_size=eval_params["virtual_batch_size"], + num_workers=eval_params["num_workers"], + drop_last=eval_params["drop_last"], + warm_start=eval_params["warm_start"], + compute_importance=eval_params["compute_importance"], + ) + + # save tabnet model + model_name = f"tabnet_{Path(self.file_path.name).stem}" + model_path = self.out_dir.joinpath(model_name) + logger.info("Saving of trained model") + model.save_model(model_path) + + imp_features = [round(i, 4) for i in model.feature_importances_] + + feature_importance_pairs = list(zip(features, imp_features)) + sorted_feature_importance_pairs = dict( + sorted(feature_importance_pairs, key=lambda x: x[1], reverse=True), + ) + + save_feat_path = self.out_dir.joinpath("feature_importances.json") + with Path.open(save_feat_path, "w") as jf: + logger.info("Save feature importances") + json.dump(sorted_feature_importance_pairs, jf, indent=4) diff --git a/clustering/tabnet-classifier-tools/src/polus/tabular/clustering/pytorch_tabnet/utils.py b/clustering/tabnet-classifier-tools/src/polus/tabular/clustering/pytorch_tabnet/utils.py new file mode 100644 index 0000000..115b322 --- /dev/null +++ b/clustering/tabnet-classifier-tools/src/polus/tabular/clustering/pytorch_tabnet/utils.py @@ -0,0 +1,224 @@ +"""Pytorch TabNet tool.""" + +import os +import shutil +from enum import Enum +from pathlib import Path +from typing import Any +from typing import Optional +from typing import Union + +import numpy as np +import torch +from pydantic import BaseModel +from pydantic import Field + +POLUS_TAB_EXT = os.environ.get("POLUS_TAB_EXT", ".arrow") + +BINARY_EVAL_METRIC = ["auc", "accuracy", "balanced_accuracy", "logloss"] +MULTICLASS_EVAL_METRIC = ["accuracy", "balanced_accuracy", "logloss"] +REGRESSION_EVAL_METRIC = ["mse", "mae", "rmse", "rmsle"] + + +MyTupleType = tuple[ + np.ndarray, + np.array, + np.ndarray, + np.array, + np.ndarray, + np.array, + list[int], + list[int], + list[str], +] + + +def generate_preview( + path: Path, +) -> None: + """Generate preview of the plugin outputs.""" + source_path = Path(__file__).parents[5].joinpath("examples") + shutil.copytree(source_path, path, dirs_exist_ok=True) + + +class OptimizersFn(str, Enum): + """Optimizers Function.""" + + Adadelta = "Adadelta" + Adagrad = "Adagrad" + Adam = "Adam" + AdamW = "AdamW" + SparseAdam = "SparseAdam" + Adamax = "Adamax" + ASGD = "ASGD" + LBFGS = "LBFGS" + NAdam = "NAdam" + RAdam = "RAdam" + RMSprop = "RMSprop" + Rprop = "Rprop" + SGD = "SGD" + Default = "Adam" + + +Map_OptimizersFn = { + OptimizersFn.Adadelta: torch.optim.Adadelta, + OptimizersFn.Adagrad: torch.optim.Adagrad, + OptimizersFn.Adam: torch.optim.Adam, + OptimizersFn.AdamW: torch.optim.AdamW, + OptimizersFn.SparseAdam: torch.optim.SparseAdam, + OptimizersFn.Adamax: torch.optim.Adamax, + OptimizersFn.ASGD: torch.optim.ASGD, + OptimizersFn.LBFGS: torch.optim.LBFGS, + OptimizersFn.ASGD: torch.optim.ASGD, + OptimizersFn.LBFGS: torch.optim.LBFGS, + OptimizersFn.NAdam: torch.optim.NAdam, + OptimizersFn.RAdam: torch.optim.RAdam, + OptimizersFn.RMSprop: torch.optim.RMSprop, + OptimizersFn.Rprop: torch.optim.Rprop, + OptimizersFn.SGD: torch.optim.SGD, + OptimizersFn.Default: torch.optim.Adam, +} + + +class SchedulerFn(str, Enum): + """Scheduler Function.""" + + LambdaLR = "LambdaLR" + MultiplicativeLR = "MultiplicativeLR" + StepLR = "StepLR" + MultiStepLR = "MultiStepLR" + ConstantLR = "ConstantLR" + LinearLR = "LinearLR" + ExponentialLR = "ExponentialLR" + PolynomialLR = "PolynomialLR" + CosineAnnealingLR = "CosineAnnealingLR" + ChainedScheduler = "ChainedScheduler" + SequentialLR = "SequentialLR" + CyclicLR = "CyclicLR" + OneCycleLR = "OneCycleLR" + CosineAnnealingWarmRestarts = "CosineAnnealingWarmRestarts" + Default = "StepLR" + + +Map_SchedulerFn = { + SchedulerFn.LambdaLR: torch.optim.lr_scheduler.LambdaLR, + SchedulerFn.MultiplicativeLR: torch.optim.lr_scheduler.MultiplicativeLR, + SchedulerFn.StepLR: torch.optim.lr_scheduler.StepLR, + SchedulerFn.MultiStepLR: torch.optim.lr_scheduler.MultiStepLR, + SchedulerFn.ConstantLR: torch.optim.lr_scheduler.ConstantLR, + SchedulerFn.LinearLR: torch.optim.lr_scheduler.LinearLR, + SchedulerFn.ExponentialLR: torch.optim.lr_scheduler.ExponentialLR, + SchedulerFn.PolynomialLR: torch.optim.lr_scheduler.PolynomialLR, + SchedulerFn.CosineAnnealingLR: torch.optim.lr_scheduler.CosineAnnealingLR, + SchedulerFn.ChainedScheduler: torch.optim.lr_scheduler.ChainedScheduler, + SchedulerFn.SequentialLR: torch.optim.lr_scheduler.SequentialLR, + SchedulerFn.CyclicLR: torch.optim.lr_scheduler.CyclicLR, + SchedulerFn.OneCycleLR: torch.optim.lr_scheduler.OneCycleLR, + SchedulerFn.CosineAnnealingWarmRestarts: torch.optim.lr_scheduler.CosineAnnealingWarmRestarts, # noqa : E501 + SchedulerFn.Default: torch.optim.lr_scheduler.StepLR, +} + + +class Evalmetric(str, Enum): + """Evaluation Metric.""" + + AUC = "auc" + ACCURACY = "accuracy" + BALANCEDACCURACY = "balanced_accuracy" + LOGLOSS = "logloss" + MSE = "mse" + MAE = "mae" + RMSE = "rmse" + RMSLE = "rmsle" + DEFAULT = "auc" + + +class MaskType(str, Enum): + """Masking Function.""" + + SPARSEMAX = "sparsemax" + ENTMAX = "entmax" + DEFAULT = "entmax" + + +class DeviceName(str, Enum): + """Platform Name.""" + + CPU = "cpu" + GPU = "gpu" + AUTO = "auto" + DEFAULT = "auto" + + +class Classifier(str, Enum): + """Pytorch TabNet Classifier.""" + + TabNetClassifier = "TabNetClassifier" + TabNetRegressor = "TabNetRegressor" + TabNetMultiTaskClassifier = "TabNetMultiTaskClassifier" + DEFAULT = "TabNetClassifier" + + +class LossFunctions(str, Enum): + """Loss Functions.""" + + L1Loss = "L1Loss" + NLLLoss = "NLLLoss" + NLLLoss2d = "NLLLoss2d" + PoissonNLLLoss = "PoissonNLLLoss" + GaussianNLLLoss = "GaussianNLLLoss" + KLDivLoss = "KLDivLoss" + MSELoss = "MSELoss" + BCELoss = "BCELoss" + BCEWithLogitsLoss = "BCEWithLogitsLoss" + HingeEmbeddingLoss = "HingeEmbeddingLoss" + SmoothL1Loss = "SmoothL1Loss" + HuberLoss = "HuberLoss" + SoftMarginLoss = "SoftMarginLoss" + CrossEntropyLoss = "CrossEntropyLoss" + MultiLabelSoftMarginLoss = "MultiLabelSoftMarginLoss" + CosineEmbeddingLoss = "CosineEmbeddingLoss" + MarginRankingLoss = "MarginRankingLoss" + MultiMarginLoss = "MultiMarginLoss" + TripletMarginLoss = "TripletMarginLoss" + TripletMarginWithDistanceLoss = "TripletMarginWithDistanceLoss" + CTCLoss = "CTCLoss" + DEFAULT = "MSELoss" + + +class TabnetParameters(BaseModel): + """Parameters for Pytorch TabNet model.""" + + test_size: float = Field(default=0.2, ge=0.1, le=0.4) + n_d: int = Field(default=8, ge=8, le=64) + n_a: int = Field(default=8, ge=8, le=64) + n_steps: int = Field(default=3, ge=3, le=10) + gamma: float = Field(default=1.3, ge=1.0, le=2.0) + cat_emb_dim: int = Field(default=1) + n_independent: int = Field(default=2, ge=1, le=5) + n_shared: int = Field(default=2, ge=1, le=5) + epsilon: float = Field(default=1e-15) + seed: int = Field(default=0) + momentum: float = Field(default=0.02, ge=0.01, le=0.4) + clip_value: Union[float, None] = Field(default=None) + lambda_sparse: float = Field(default=1e-3) + optimizer_fn: Any = Field(default=torch.optim.Adam) + optimizer_params: dict = Field(default={"lr": 0.02}) + scheduler_fn: Any = Field(default=torch.optim.lr_scheduler.StepLR) + scheduler_params: dict = Field(default={"step_size": 10, "gamma": 0.95}) + device_name: str = Field(default="auto") + mask_type: str = Field(default="entmax") + grouped_features: Optional[Union[list[int], None]] = Field(default=None) + n_shared_decoder: int = Field(default=1) + n_indep_decoder: int = Field(default=1) + eval_metric: str = Field(default="auc") + max_epochs: int = Field(default=200) + patience: int = Field(default=10) + weights: int = Field(default=0) + loss_fn: str = Field(default="MSELoss") + batch_size: int = Field(default=1024) + virtual_batch_size: int = Field(default=128) + num_workers: int = Field(default=0) + drop_last: bool = Field(default=False) + warm_start: bool = Field(default=False) + compute_importance: bool = Field(default=True) diff --git a/clustering/tabnet-classifier-tools/tests/__init__.py b/clustering/tabnet-classifier-tools/tests/__init__.py new file mode 100644 index 0000000..4d84872 --- /dev/null +++ b/clustering/tabnet-classifier-tools/tests/__init__.py @@ -0,0 +1 @@ +"""Pytorch TabNet tool.""" diff --git a/clustering/tabnet-classifier-tools/tests/conftest.py b/clustering/tabnet-classifier-tools/tests/conftest.py new file mode 100644 index 0000000..defa87b --- /dev/null +++ b/clustering/tabnet-classifier-tools/tests/conftest.py @@ -0,0 +1,208 @@ +"""Test Fixtures.""" + +import shutil +import tempfile +from pathlib import Path +from typing import Union + +import numpy as np +import pandas as pd +import pytest + +EXT = None + + +def pytest_addoption(parser: pytest.Parser) -> None: + """Add options to pytest.""" + parser.addoption( + "--slow", + action="store_true", + dest="slow", + default=False, + help="run slow tests", + ) + + +def clean_directories() -> None: + """Remove all temporary directories.""" + for d in Path(".").cwd().iterdir(): + if d.is_dir() and d.name.startswith("tmp"): + shutil.rmtree(d) + + +@pytest.fixture() +def input_directory() -> Union[str, Path]: + """Create input directory.""" + return Path(tempfile.mkdtemp(dir=Path.cwd())) + + +@pytest.fixture() +def output_directory() -> Union[str, Path]: + """Create output directory.""" + return Path(tempfile.mkdtemp(dir=Path.cwd())) + + +@pytest.fixture() +def create_dataset() -> Union[str, Path]: + """Create output directory.""" + size = 5000 + + inp_dir = Path(tempfile.mkdtemp(dir=Path.cwd())) + + rng = np.random.default_rng() + + workclass = [ + "Private", + "Local-gov", + "Self-emp-not-inc", + "Federal-gov", + "State-gov", + "Self-emp-inc", + "Without-pay", + "Never-worked", + ] + + education = [ + "11th", + "HS-grad", + "Assoc-acdm", + "Some-college", + "10th", + "Prof-school", + "7th-8th", + "Bachelors", + "Masters", + "Doctorate", + "5th-6th", + "Assoc-voc", + "9th", + "12th", + "1st-4th", + "Preschool", + ] + + marital_status = [ + "Never-married", + "Married-civ-spouse", + "Widowed", + "Divorced", + "Separated", + "Married-spouse-absent", + "Married-AF-spouse", + ] + + occupation = [ + "Machine-op-inspct", + "Farming-fishing", + "Protective-serv", + "?", + "Other-service", + "Prof-specialty", + "Craft-repair", + "Adm-clerical", + "Exec-managerial", + "Tech-support", + "Sales", + "Priv-house-serv", + "Transport-moving", + "Handlers-cleaners", + "Armed-Forces", + ] + + relationship = [ + "Own-child", + "Husband", + "Not-in-family", + "Unmarried", + "Wife", + "Other-relative", + ] + + race = ["Black", "White", "Asian-Pac-Islander", "Other", "Amer-Indian-Eskimo"] + + gender = ["Male", "Female"] + + income = ["<=50K", ">50K"] + + countries = [ + "United-States", + "Peru", + "Guatemala", + "Mexico", + "Dominican-Republic", + "Ireland", + "Germany", + "Philippines", + "Thailand", + "Haiti", + "El-Salvador", + "Puerto-Rico", + "Vietnam", + "South", + "Columbia", + "Japan", + "India", + "Cambodia", + "Poland", + "Laos", + "England", + "Cuba", + "Taiwan", + "Italy", + "Canada", + "Portugal", + "China", + "Nicaragua", + "Honduras", + "Iran", + "Scotland", + "Jamaica", + "Ecuador", + "Yugoslavia", + "Hungary", + "Hong", + "Greece", + "Trinadad&Tobago", + "Outlying-US(Guam-USVI-etc)", + "France", + "Holand-Netherlands", + ] + + diction_1 = { + "age": rng.integers(low=15, high=80, size=size), + "workclass": rng.choice(workclass, size), + "fnlwgt": rng.integers(low=12285, high=1490400, size=size), + "education": rng.choice(education, size), + "educational-num": rng.integers(low=1, high=16, size=size), + "marital-status": rng.choice(marital_status, size), + "occupation": rng.choice(occupation, size), + "relationship": rng.choice(relationship, size), + "race": rng.choice(race, size), + "gender": rng.choice(gender, size), + "capital-gain": rng.integers(low=0, high=99999, size=size), + "capital-loss": rng.integers(low=0, high=4356, size=size), + "hours-per-week": rng.integers(low=1, high=99, size=size), + "native-country": rng.choice(countries, size), + "income": rng.choice(income, size), + } + + data = pd.DataFrame(diction_1) + + data.to_csv(Path(inp_dir, "adult.csv"), index=False) + data.to_feather(Path(inp_dir, "adult.arrow")) + data.to_parquet(Path(inp_dir, "adult.parquet")) + + return inp_dir + + +@pytest.fixture( + params=[ + (0.3, "Adam", "StepLR", "accuracy", "L1Loss", "TabNetMultiTaskClassifier"), + (0.2, "Adadelta", "StepLR", "mse", "GaussianNLLLoss", "TabNetRegressor"), + (0.2, "Adagrad", "StepLR", "logloss", "MSELoss", "TabNetClassifier"), + (0.2, "RAdam", "StepLR", "auc", "CrossEntropyLoss", "TabNetClassifier"), + ], +) +def get_params(request: pytest.FixtureRequest) -> pytest.FixtureRequest: + """To get the parameter of the fixture.""" + return request.param diff --git a/clustering/tabnet-classifier-tools/tests/test_cli.py b/clustering/tabnet-classifier-tools/tests/test_cli.py new file mode 100644 index 0000000..a0493e1 --- /dev/null +++ b/clustering/tabnet-classifier-tools/tests/test_cli.py @@ -0,0 +1,50 @@ +"""Test Command line Tool.""" + +from typer.testing import CliRunner +from pathlib import Path +import pytest +from polus.tabular.clustering.pytorch_tabnet.__main__ import app +from .conftest import clean_directories +from typing import Union + + +def test_cli( + output_directory: Path, + create_dataset: Union[str, Path], + get_params: pytest.FixtureRequest, +) -> None: + """Test the command line.""" + + inp_dir = create_dataset + + runner = CliRunner() + + test_size, optimizer_fn, scheduler_fn, eval_metric, loss_fn, classifier = get_params + + result = runner.invoke( + app, + [ + "--inpDir", + inp_dir, + "--filePattern", + ".*.csv", + "--testSize", + test_size, + "--optimizerFn", + optimizer_fn, + "--evalMetric", + eval_metric, + "--schedulerFn", + scheduler_fn, + "--lossFn", + loss_fn, + "--targetVar", + "income", + "--classifier", + classifier, + "--outDir", + output_directory, + ], + ) + assert result.exit_code == 0 + clean_directories() diff --git a/clustering/tabnet-classifier-tools/tests/test_tabnet.py b/clustering/tabnet-classifier-tools/tests/test_tabnet.py new file mode 100644 index 0000000..0b4cf45 --- /dev/null +++ b/clustering/tabnet-classifier-tools/tests/test_tabnet.py @@ -0,0 +1,151 @@ +"""Pytorch TabNet tool.""" + +import filepattern as fp +import pytest +import torch +import polus.tabular.clustering.pytorch_tabnet.tabnet as tb +from .conftest import clean_directories +from pathlib import Path +from typing import Union +import numpy as np + + +# @pytest.mark.skipif("not config.getoption('slow')") +def test_convert_vaex_dataframe( + output_directory: Path, + create_dataset: Union[str, Path], + get_params: pytest.FixtureRequest, +) -> None: + """Testing reading vaex dataframe.""" + + inp_dir = create_dataset + test_size, optimizer_fn, scheduler_fn, eval_metric, loss_fn, classifier = get_params + + params = { + "test_size": test_size, + "n_d": 8, + "n_a": 8, + "seed": 0, + "optimizer_fn": optimizer_fn, + "optimizer_params": {"lr": 0.001}, + "scheduler_fn": scheduler_fn, + "device_name": "cpu", + "eval_metric": eval_metric, + "max_epochs": 10, + "loss_fn": loss_fn, + } + + patterns = [".*.csv", ".*.arrow", ".*.parquet"] + + for pat in patterns: + fps = fp.FilePattern(inp_dir, pat) + + for f in fps: + model = tb.PytorchTabnet( + **params, + file_path=f[1][0], + target_var="income", + classifier=classifier, + out_dir=output_directory, + ) + df = model.convert_vaex_dataframe + assert df.shape == (5000, 15) + assert df is not None + + clean_directories() + + +def test_get_data( + output_directory: Path, + create_dataset: Union[str, Path], + get_params: pytest.FixtureRequest, +) -> None: + """Testing getting data.""" + + inp_dir = create_dataset + test_size, optimizer_fn, scheduler_fn, eval_metric, loss_fn, classifier = get_params + + params = { + "test_size": test_size, + "n_d": 8, + "n_a": 8, + "seed": 0, + "optimizer_fn": optimizer_fn, + "optimizer_params": {"lr": 0.001}, + "scheduler_fn": scheduler_fn, + "device_name": "cpu", + "eval_metric": eval_metric, + "max_epochs": 10, + "loss_fn": loss_fn, + } + fps = fp.FilePattern(inp_dir, ".*.csv") + file = [f[1][0] for f in fps()][0] + + model = tb.PytorchTabnet( + **params, + file_path=file, + target_var="income", + classifier=classifier, + out_dir=output_directory, + ) + + ( + X_train, + y_train, + X_test, + y_test, + X_val, + y_val, + cat_idxs, + cat_dims, + features, + ) = model.get_data + + assert all( + isinstance(arr, np.ndarray) + for arr in [X_train, y_train, X_test, y_test, X_val, y_val] + ) + assert all(isinstance(i, list) for i in [cat_idxs, cat_dims, features]) + + +def test_fit_model( + output_directory: Path, + create_dataset: Union[str, Path], + get_params: pytest.FixtureRequest, +) -> None: + """Testing fitting model.""" + + inp_dir = create_dataset + test_size, _, _, eval_metric, loss_fn, classifier = get_params + + params = { + "test_size": test_size, + "n_d": 8, + "n_a": 8, + "seed": 0, + "optimizer_fn": torch.optim.Adam, + "scheduler_fn": torch.optim.lr_scheduler.StepLR, + "device_name": "cpu", + "eval_metric": eval_metric, + "max_epochs": 10, + "loss_fn": loss_fn, + } + fps = fp.FilePattern(inp_dir, ".*.csv") + file = [f[1][0] for f in fps()][0] + + model = tb.PytorchTabnet( + **params, + file_path=file, + target_var="income", + classifier=classifier, + out_dir=output_directory, + ) + mod_params = dict(model) + model.fit_model(params=mod_params) + + files = [ + f for f in Path(output_directory).iterdir() if f.suffix in [".zip", ".json"] + ] + + assert len(files) != 0 + clean_directories() From 785667fb1762149c610f44a6011c1df199ead149 Mon Sep 17 00:00:00 2001 From: hamshkhawar Date: Wed, 29 May 2024 10:27:43 -0500 Subject: [PATCH 2/5] fix plugin manifest & generated ict and clt --- .../tabnet-classifier-tools/PytorchTabnet.cwl | 172 ++++++ clustering/tabnet-classifier-tools/ict.yaml | 502 ++++++++++++++++++ .../tabnet-classifier-tools/plugin.json | 4 +- 3 files changed, 676 insertions(+), 2 deletions(-) create mode 100644 clustering/tabnet-classifier-tools/PytorchTabnet.cwl create mode 100644 clustering/tabnet-classifier-tools/ict.yaml diff --git a/clustering/tabnet-classifier-tools/PytorchTabnet.cwl b/clustering/tabnet-classifier-tools/PytorchTabnet.cwl new file mode 100644 index 0000000..96f5fa5 --- /dev/null +++ b/clustering/tabnet-classifier-tools/PytorchTabnet.cwl @@ -0,0 +1,172 @@ +class: CommandLineTool +cwlVersion: v1.2 +inputs: + batchSize: + inputBinding: + prefix: --batchSize + type: string? + catEmbDim: + inputBinding: + prefix: --catEmbDim + type: string? + classifier: + inputBinding: + prefix: --classifier + type: string + clipValue: + inputBinding: + prefix: --clipValue + type: double? + computeImportance: + inputBinding: + prefix: --computeImportance + type: boolean? + deviceName: + inputBinding: + prefix: --deviceName + type: string + dropLast: + inputBinding: + prefix: --dropLast + type: boolean? + epsilon: + inputBinding: + prefix: --epsilon + type: double? + evalMetric: + inputBinding: + prefix: --evalMetric + type: string + filePattern: + inputBinding: + prefix: --filePattern + type: string? + gamma: + inputBinding: + prefix: --gamma + type: double? + groupedFeatures: + inputBinding: + prefix: --groupedFeatures + type: string? + inpDir: + inputBinding: + prefix: --inpDir + type: Directory + lambdaSparse: + inputBinding: + prefix: --lambdaSparse + type: double? + lossFn: + inputBinding: + prefix: --lossFn + type: string + lr: + inputBinding: + prefix: --lr + type: double? + maskType: + inputBinding: + prefix: --maskType + type: string + maxEpochs: + inputBinding: + prefix: --maxEpochs + type: string? + momentum: + inputBinding: + prefix: --momentum + type: double? + nA: + inputBinding: + prefix: --nA + type: string? + nD: + inputBinding: + prefix: --nD + type: string? + nIndepDecoder: + inputBinding: + prefix: --nIndepDecoder + type: string? + nIndependent: + inputBinding: + prefix: --nIndependent + type: string? + nShared: + inputBinding: + prefix: --nShared + type: string? + nSharedDecoder: + inputBinding: + prefix: --nSharedDecoder + type: string? + nSteps: + inputBinding: + prefix: --nSteps + type: string? + numWorkers: + inputBinding: + prefix: --numWorkers + type: string? + optimizerFn: + inputBinding: + prefix: --optimizerFn + type: string + outDir: + inputBinding: + prefix: --outDir + type: Directory + patience: + inputBinding: + prefix: --patience + type: string? + preview: + inputBinding: + prefix: --preview + type: boolean? + schedulerFn: + inputBinding: + prefix: --schedulerFn + type: string + seed: + inputBinding: + prefix: --seed + type: string? + stepSize: + inputBinding: + prefix: --stepSize + type: string? + targetVar: + inputBinding: + prefix: --targetVar + type: string + testSize: + inputBinding: + prefix: --testSize + type: double? + virtualBatchSize: + inputBinding: + prefix: --virtualBatchSize + type: string? + warmStart: + inputBinding: + prefix: --warmStart + type: boolean? + weights: + inputBinding: + prefix: --weights + type: string? +outputs: + outDir: + outputBinding: + glob: $(inputs.outDir.basename) + type: Directory +requirements: + DockerRequirement: + dockerPull: polusai/pytorch-tabnet-tool:0.1.0-dev0 + InitialWorkDirRequirement: + listing: + - entry: $(inputs.outDir) + writable: true + InlineJavascriptRequirement: {} diff --git a/clustering/tabnet-classifier-tools/ict.yaml b/clustering/tabnet-classifier-tools/ict.yaml new file mode 100644 index 0000000..e7fa7db --- /dev/null +++ b/clustering/tabnet-classifier-tools/ict.yaml @@ -0,0 +1,502 @@ +author: +- Hamdah Shafqat +contact: hamdahshafqat.abbasi@nih.gov +container: polusai/pytorch-tabnet-tool:0.1.0-dev0 +description: Pytorch tabnet tool for training tabular data. +entrypoint: python3 -m polus.tabular.clustering.pytorch_tabnet +inputs: +- description: Input tabular data + format: + - genericData + name: inpDir + required: true + type: path +- description: Pattern to parse input files + format: + - string + name: filePattern + required: false + type: string +- description: Proportion of the dataset to include in the test set + format: + - number + name: testSize + required: false + type: number +- description: Width of the decision prediction layer + format: + - integer + name: nD + required: false + type: number +- description: Width of the attention embedding for each mask + format: + - integer + name: nA + required: false + type: number +- description: Number of steps in the architecture + format: + - integer + name: nSteps + required: false + type: number +- description: Coefficient for feature reuse in the masks + format: + - number + name: gamma + required: false + type: number +- description: List of embedding sizes for each categorical feature + format: + - integer + name: catEmbDim + required: false + type: number +- description: Number of independent Gated Linear Unit layers at each step + format: + - integer + name: nIndependent + required: false + type: number +- description: Number of shared Gated Linear Unit layers at each step + format: + - integer + name: nShared + required: false + type: number +- description: Constant value + format: + - number + name: epsilon + required: false + type: number +- description: Random seed for reproducibility + format: + - integer + name: seed + required: false + type: number +- description: Momentum for batch normalization + format: + - number + name: momentum + required: false + type: number +- description: Clipping of the gradient value + format: + - number + name: clipValue + required: false + type: number +- description: Extra sparsity loss coefficient + format: + - number + name: lambdaSparse + required: false + type: number +- description: Pytorch optimizer function + format: + - enum + name: optimizerFn + required: true + type: string +- description: learning rate for the optimizer + format: + - number + name: lr + required: false + type: number +- description: Parameters used initialize the optimizer + format: + - enum + name: schedulerFn + required: true + type: string +- description: Parameter to apply to the scheduler_fn + format: + - integer + name: stepSize + required: false + type: number +- description: Device used for training + format: + - enum + name: deviceName + required: true + type: string +- description: A masking function for feature selection + format: + - enum + name: maskType + required: true + type: string +- description: Allow the model to share attention across features within the same + group + format: + - integer + name: groupedFeatures + required: false + type: number +- description: Number of shared GLU block in decoder + format: + - integer + name: nSharedDecoder + required: false + type: number +- description: Number of independent GLU block in decoder + format: + - integer + name: nIndepDecoder + required: false + type: number +- description: Metrics utilized for early stopping evaluation + format: + - enum + name: evalMetric + required: true + type: string +- description: Maximum number of epochs for training + format: + - integer + name: maxEpochs + required: false + type: number +- description: Consecutive epochs without improvement before early stopping + format: + - integer + name: patience + required: false + type: number +- description: Sampling parameter only for TabNetClassifier + format: + - integer + name: weights + required: false + type: number +- description: Loss function + format: + - enum + name: lossFn + required: true + type: string +- description: Batch size + format: + - integer + name: batchSize + required: false + type: number +- description: Size of mini-batches for Ghost Batch Normalization + format: + - integer + name: virtualBatchSize + required: false + type: number +- description: Number or workers used in torch.utils.data.Dataloader + format: + - integer + name: numWorkers + required: false + type: number +- description: Option to drop incomplete last batch during training + format: + - boolean + name: dropLast + required: false + type: boolean +- description: For scikit-learn compatibility, enabling fitting the same model twice + format: + - boolean + name: warmStart + required: false + type: boolean +- description: Compute feature importance? + format: + - boolean + name: computeImportance + required: false + type: boolean +- description: Target feature for classification + format: + - string + name: targetVar + required: true + type: string +- description: Tabnet Classifier + format: + - enum + name: classifier + required: true + type: string +- description: Output a JSON preview of outputs produced by this plugin + format: + - boolean + name: preview + required: false + type: boolean +name: polusai/PytorchTabnet +outputs: +- description: Output collection + format: + - genericData + name: outDir + required: true + type: path +repository: https://github.com/polusai/tabular-tools +specVersion: 1.0.0 +title: Pytorch Tabnet +ui: +- description: Input tabular data for clustering + key: inputs.inpDir + title: Input tabular data + type: path +- description: Pattern to parse input files + key: inputs.filePattern + title: FilePattern + type: text +- default: 0.2 + description: Proportion of the dataset to include in the test set + key: inputs.testSize + title: FilePattern + type: number +- default: 8 + description: Width of the decision prediction layer + key: inputs.nD + title: nD + type: number +- default: 8 + description: Width of the attention embedding for each mask + key: inputs.nA + title: nA + type: number +- default: 8 + description: Number of steps in the architecture + key: inputs.nSteps + title: nSteps + type: number +- default: 1.3 + description: Coefficient for feature reuse in the masks + key: inputs.gamma + title: gamma + type: number +- default: 1 + description: List of embedding sizes for each categorical feature + key: inputs.catEmbDim + title: catEmbDim + type: number +- default: 2 + description: Number of independent Gated Linear Unit layers at each step + key: inputs.nIndependent + title: nIndependent + type: number +- default: 2 + description: Number of shared Gated Linear Unit layers at each step + key: inputs.nShared + title: nShared + type: number +- default: 1.0e-15 + description: Constant value + key: inputs.epsilon + title: epsilon + type: number +- default: 0 + description: Random seed for reproducibility + key: inputs.seed + title: seed + type: number +- default: 0.02 + description: Momentum for batch normalization + key: inputs.momentum + title: momentum + type: number +- description: Clipping of the gradient value + key: inputs.clipValue + title: clipValue + type: number +- default: 0.001 + description: Extra sparsity loss coefficient + key: inputs.lambdaSparse + title: lambdaSparse + type: number +- description: Pytorch optimizer function + fields: + - Adadelta + - Adagrad + - Adam + - AdamW + - SparseAdam + - Adamax + - ASGD + - LBFGS + - NAdam + - RAdam + - RMSprop + - Rprop + - SGD + - Default + key: inputs.optimizerFn + title: optimizerFn + type: select +- default: 0.02 + description: learning rate for the optimizer + key: inputs.lr + title: lr + type: number +- description: Parameters used initialize the optimizer + fields: + - LambdaLR + - MultiplicativeLR + - StepLR + - MultiStepLR + - ConstantLR + - LinearLR + - ExponentialLR + - PolynomialLR + - CosineAnnealingLR + - ChainedScheduler + - SequentialLR + - CyclicLR + - OneCycleLR + - CosineAnnealingWarmRestarts + - Default + key: inputs.schedulerFn + title: schedulerFn + type: select +- default: 10 + description: Parameter to apply to the scheduler_fn + key: inputs.stepSize + title: stepSize + type: number +- description: Device used for training + fields: + - cpu + - gpu + - auto + - DEFAULT + key: inputs.deviceName + title: deviceName + type: select +- description: A masking function for feature selection + fields: + - sparsemax + - entmax + - DEFAULT + key: inputs.maskType + title: maskType + type: select +- description: Allow the model to share attention across features within the same + group + key: inputs.groupedFeatures + title: groupedFeatures + type: number +- default: 1 + description: Number of shared GLU block in decoder + key: inputs.nSharedDecoder + title: nSharedDecoder + type: number +- default: 1 + description: Number of independent GLU block in decoder + key: inputs.nIndepDecoder + title: nIndepDecoder + type: number +- description: Metrics utilized for early stopping evaluation + fields: + - auc + - accuracy + - balanced_accuracy + - logloss + - mse + - mae + - rmse + - rmsle + - DEFAULT + key: inputs.evalMetric + title: evalMetric + type: select +- default: 200 + description: Maximum number of epochs for training + key: inputs.maxEpochs + title: maxEpochs + type: number +- default: 10 + description: Consecutive epochs without improvement before early stopping + key: inputs.patience + title: patience + type: number +- default: 10 + description: Sampling parameter only for TabNetClassifier + key: inputs.weights + title: weights + type: number +- description: Loss function + fields: + - L1Loss + - NLLLoss + - NLLLoss2d + - PoissonNLLLoss + - GaussianNLLLoss + - KLDivLoss + - MSELoss + - BCELoss + - BCEWithLogitsLoss + - HingeEmbeddingLoss + - SmoothL1Loss + - HuberLoss + - SoftMarginLoss + - CrossEntropyLoss + - MultiLabelSoftMarginLoss + - CosineEmbeddingLoss + - MarginRankingLoss + - MultiMarginLoss + - TripletMarginLoss + - TripletMarginWithDistanceLoss + - CTCLoss + - DEFAULT + key: inputs.lossFn + title: lossFn + type: select +- default: 1024 + description: Batch size + key: inputs.batchSize + title: batchSize + type: number +- default: 128 + description: Size of mini-batches for Ghost Batch Normalization + key: inputs.virtualBatchSize + title: virtualBatchSize + type: number +- default: 0 + description: Number or workers used in torch.utils.data.Dataloader + key: inputs.numWorkers + title: numWorkers + type: number +- description: Option to drop incomplete last batch during training + key: inputs.dropLast + title: dropLast + type: checkbox +- description: For scikit-learn compatibility, enabling fitting the same model twice + key: inputs.warmStart + title: warmStart + type: checkbox +- description: Compute feature importance? + key: inputs.computeImportance + title: computeImportance + type: checkbox +- description: Target feature for classification + key: inputs.targetVar + title: targetVar + type: text +- description: Tabnet Classifier + fields: + - TabNetClassifier + - TabNetRegressor + - TabNetMultiTaskClassifier + - DEFAULT + key: inputs.classifier + title: classifier + type: select +- description: Output a JSON preview of outputs produced by this plugin + key: inputs.preview + title: preview + type: checkbox +version: 0.1.0-dev0 diff --git a/clustering/tabnet-classifier-tools/plugin.json b/clustering/tabnet-classifier-tools/plugin.json index 6c65b41..6ad359b 100644 --- a/clustering/tabnet-classifier-tools/plugin.json +++ b/clustering/tabnet-classifier-tools/plugin.json @@ -539,7 +539,7 @@ "default": "entmax" }, { - "key": "inputs.groupedFeature", + "key": "inputs.groupedFeatures", "type": "integer", "title": "groupedFeatures", "description": "Allow the model to share attention across features within the same group", @@ -669,4 +669,4 @@ "required": "False" } ] -} +} \ No newline at end of file From 37ea70aa739c6bc706a295f96337527350e56ba5 Mon Sep 17 00:00:00 2001 From: hamshkhawar Date: Wed, 29 May 2024 11:53:23 -0500 Subject: [PATCH 3/5] adding missing file --- .../tabnet-classifier-tools/.bumpversion.cfg | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 clustering/tabnet-classifier-tools/.bumpversion.cfg diff --git a/clustering/tabnet-classifier-tools/.bumpversion.cfg b/clustering/tabnet-classifier-tools/.bumpversion.cfg new file mode 100644 index 0000000..986af44 --- /dev/null +++ b/clustering/tabnet-classifier-tools/.bumpversion.cfg @@ -0,0 +1,29 @@ +[bumpversion] +current_version = 0.1.0-dev0 +commit = True +tag = False +parse = (?P\d+)\.(?P\d+)\.(?P\d+)(\-(?P[a-z]+)(?P\d+))? +serialize = + {major}.{minor}.{patch}-{release}{dev} + {major}.{minor}.{patch} + +[bumpversion:part:release] +optional_value = _ +first_value = dev +values = + dev + _ + +[bumpversion:part:dev] + +[bumpversion:file:pyproject.toml] +search = version = "{current_version}" +replace = version = "{new_version}" + +[bumpversion:file:plugin.json] +[bumpversion:file:PytorchTabnet.cwl] +[bumpversion:file:ict.yaml] + +[bumpversion:file:VERSION] + +[bumpversion:file:src/polus/tabular/clustering/pytorch_tabnet/__init__.py] \ No newline at end of file From 50f515ee88c92ef6a0a90d7c7a8b9f18ff507c88 Mon Sep 17 00:00:00 2001 From: hamshkhawar Date: Wed, 29 May 2024 11:59:23 -0500 Subject: [PATCH 4/5] change yaml to yml --- clustering/tabnet-classifier-tools/{ict.yaml => ict.yml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename clustering/tabnet-classifier-tools/{ict.yaml => ict.yml} (100%) diff --git a/clustering/tabnet-classifier-tools/ict.yaml b/clustering/tabnet-classifier-tools/ict.yml similarity index 100% rename from clustering/tabnet-classifier-tools/ict.yaml rename to clustering/tabnet-classifier-tools/ict.yml From 844cf0dbc975850bed21773f2e3f69a33cc17d10 Mon Sep 17 00:00:00 2001 From: hamshkhawar Date: Wed, 29 May 2024 12:25:09 -0500 Subject: [PATCH 5/5] fix feature_importance.json --- .../examples/feature_importances.json | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/clustering/tabnet-classifier-tools/examples/feature_importances.json b/clustering/tabnet-classifier-tools/examples/feature_importances.json index 8e56739..6f03300 100644 --- a/clustering/tabnet-classifier-tools/examples/feature_importances.json +++ b/clustering/tabnet-classifier-tools/examples/feature_importances.json @@ -1,16 +1,16 @@ { - "capital-gain": 0.2208, - "marital-status": 0.1949, - "educational-num": 0.1724, - "hours-per-week": 0.1041, - "age": 0.0789, - "gender": 0.0756, - "occupation": 0.0635, - "relationship": 0.0264, - "capital-loss": 0.0225, - "native-country": 0.0223, - "race": 0.0102, - "fnlwgt": 0.0061, - "education": 0.0016, - "workclass": 0.001 -} \ No newline at end of file + "capital-gain": 0.2208, + "marital-status": 0.1949, + "educational-num": 0.1724, + "hours-per-week": 0.1041, + "age": 0.0789, + "gender": 0.0756, + "occupation": 0.0635, + "relationship": 0.0264, + "capital-loss": 0.0225, + "native-country": 0.0223, + "race": 0.0102, + "fnlwgt": 0.0061, + "education": 0.0016, + "workclass": 0.001 +}