From edbad414e250ac17c8c96fba189316c5597ba00b Mon Sep 17 00:00:00 2001 From: franksim Date: Sun, 16 Nov 2025 16:28:13 +0100 Subject: [PATCH] adds solutions --- mmp/a4/dataset.py | 2 +- mmp/a5/image.png | Bin 0 -> 40602 bytes mmp/a5/main.py | 246 ++++++++++++++++++++++++++++++++++++++++++++-- mmp/a5/model.py | 39 +++++++- 4 files changed, 274 insertions(+), 13 deletions(-) create mode 100644 mmp/a5/image.png diff --git a/mmp/a4/dataset.py b/mmp/a4/dataset.py index a46852b..d2cc72b 100644 --- a/mmp/a4/dataset.py +++ b/mmp/a4/dataset.py @@ -255,7 +255,7 @@ def draw_positive_boxes( def main(): anchor_grid = get_anchor_grid( anchor_widths=[8, 16, 32, 64, 96, 128, 160, 192], - aspect_ratios=[1 / 3, 1 / 2, 3 / 5, 2 / 3, 3 / 4, 1, 4 / 3, 5 / 3, 2, 2.5, 3], + aspect_ratios=[1 / 2, 2 / 3, 1, 4 / 3, 5 / 3, 2, 2.5, 3], num_rows=28, num_cols=28, scale_factor=8, diff --git a/mmp/a5/image.png b/mmp/a5/image.png new file mode 100644 index 0000000000000000000000000000000000000000..8dc1af70e5ff7df06ed701e0fb8224cc38d0c9a8 GIT binary patch literal 40602 zcmc$`by!u|_dkwT(2J;ml#0Lw2BkZsuTlaMf`EX8bfbWDT^$h#0VyS~q>|Dd4&5an zt#l*Z@Y~0kc@OvM=lkz(p2r7O`GX0{G1 zt}UA2!+!Jo4;&sS+!ZpkwPt^0WNTo|?ri-S`UFSBSqS`UZS3#};cRVXV=v?^O84^( zA@Do&XAU~V&sQ8QMd=4z3Xa3m1 z+|~vGjr+*J_Njv?9UU~$|Ni@XoyN}Q|IK7$|IfC-205T799-<29RC{|d@2I{RY=*~ z+1N@`(%c%52h1VH$H^=5^Zj3*{5Ru?FCYB(B^M7D#-}G9VSlP-Z)_)FYYpaf5c}_F z{+ajW!+$;$;efV&f)sz#`R88&p~cRLaQyGk#Lf{K(PrV`AaP_RZ{2r3wK#%b{!q>1 z=8sTOBh;Trcp1W<^^y^Og2CwzXh!*A#&aJE*rg0^F~%Per;m3BkzKu%RH#MvDM95u zGv1S1cp12)v}gXr^$mV;8LoZjua>=z6`%dL0xm7(+a}X(t%q?PrrbPE%c9#cD<$s) z)}!;zyFMO02$ZEmdgGje;lha!Cw@U*!1H;1nTyVPn2}sl!cdds_QXE^nNHd26spmnsA}*x`}pARjWp7E8~f)!@1FP?nS+N* z=Ra~}q4X<>(Cjc+$kRuTmwvPT|Mx24l6@}*WHIUeb8b>?BqI0gQuY?sVL%V(={WO? zOb3de7;077=xCKWS`QXKk+3B!691JFr(g=~oMxThc&)yE;x(G> zPo$WT7 zSZlfzRmci2=~dUpza3HReCIdRxdZFE#bxkk*CQ?ipYSAIJi7kuv0xuU8eQ4St=}9F z5?zN!r+%5MMouLg7Q$rxg(UZq+Wv1ihta!w`Xi-)`u!f=GjuG?J&M1f6`W`mfgl<{ z)sXz=Y;Y}T5k#5ZZtVDM=2KunhSq z@|%0Nq5kcq_`xGTZHBp|P(`^Xp;0nmPcDOUvcJ4cM$j59z~QmtHYQI%&iY~J?Xu&r zVUTEd+1TN7euvn;Ls4`;+<}SHY$~mnvdq0;@r4>oywZ?$ka}1eHTL-FJd1GYMP|c?Y8IPg5FBu53nRFKg7h&>O|QH$Ft`yaU;tQyeZa^<}S$m z^rh2MDueX=)cy3n!#X7H(ABwexyMRumaM{!N|#}NWHkx-FBzGTwT%W!#WO6E-68aaB}vCUprBa z1(y9LQ*H;V4VF>}J?sHL;~gzj9kGaRbV|&9e@Uab`oMw>b@??_&D{E~=-E=o)7-qm z&-6LUfBfa$E82eioaO*gcHseS%v)?$;H-w3#<`gIMSsadiY=DTh^jJ=6uIv>GBjvd z#y@yqhJI-n*=LM|g(Tz6EwCYO11w+j5ENzU3;SaLjcf)BN~wTS+zLuB?R5`MDo^9bQN4`P-vzsUuEf6(0NJ zb^bkUMZ<5-T(}xGx>GNZLgo0C_HehuOVMkmhNhb-HKx0AY`DZ;b%QRzgYNKXyQ;$` zq<=y0crPD_kAfBLx#w!>>d8YkWo*%=w9Rgfv@eZb)|V69i#*&K;y7G16_=NId9jT( z!OCU6$>MNtQ*-yP_1;{hdhckWMjBQHMjTZA_Fk@Ml`Ri`fBNek+$4RFV;~ij z>hDqinrdjm)6t9-b?Qpg2+3#h!yfVE9z|g`(VcJ4=k$)I^t$nH#029Vj~Bx;DyGQu3yvlM-_a(u)= zz$*QC9lB+XcPB-G0CpI4nK3@qn?PgU9 zs485(jE|)>IuBr79~YWgvKOC9jlXs6pt=%U;3^ls*rQ5a;UVhEA2HUv&gkWVaFdW8 zFz8);AS!CJ>Wo&c!J|to8t3bO80xv#6Q3f_QNeq-*I%Vba{X>2>^ipjG<^ zU;dq{g#`u1GXV$--0So>SC5;d11^VnZl>!QXKxy9 zRvqtGy))dn7&tMp>#;o&>A77tX57#q&ZU9Oh$Ab4OOCp)w=HG6U`u`8Dr7AW9Xk#qYvEMg6e%y5^FX1tD48&vDWaXEQWg~F*OQ_kpY76SnH%Su!{wvjz&Y)8*zeWmdZBVo!FJFf zq4yZ@SG%jow8V!*3Im6kK%_}Ixp}m8VH3=8_2#qJ#P9%pW85PHE01A1sp=ndsqyVT z{Nihk^aG!k-+n6)VDgEl@!a{oH1j&QmW{mZ6|YI_Thh;8?r@15W*^OG=RTTQac&S+ zQBrpr^KjyCbvxKu9az0L15cCYw;+GW&o$QD}m{v*IEPTi&6HMKN`Vd<1LLv@NT33|vlnQk4DR!PE^`!etxt$rJ5q(L(!ah*N2yiex;f8Uc2n$GU^0ti1JUy5=VwjV?@24w6xY?c z&%S?Y(ooC!<8lhszLf3f%hIu__8p=E1U}N7T+9aYSLWCxhSJvfUQwn6IFYa&Fe=$zc#Wo1e+(ig6>OB68r#X`{N=_#v zS(oE-BXyM8_83L>$-`9elz1@!Z5GC+5|$^xl|fE}mK-zEW{9-evmT#UgMcX`}Ogyt)zB zIPLkc#Ll=-YeUI%Ei_~15w9{UD+xhqt@12fM4FQWX31sc(iX)xV8BWo6ENx%GQK%0 zb>*XbNYq#ITeYVz-b7HEP<^f|t~R9Wb7yp3B&o@(+A7F4KGiB4dc7`X%U+tW{=ue@ zkBCfmefhR=p(@_7dJkiaqz{ zHL9XE?P3J&P5L$kU_mRu5X$pNEHb?N=}Nkc%pl;jygCDe2M)OS990Kv8T|&V>Lm7} z^$-2BfndpVp-6Q4c0+u(X|ebq9)7?68BbT)8l-LMWnQ;VzYDORVS;&aX&B;et&xf{ z7}m|*-0F!xa)=UfvE$b>#^rGfO}JrlURZn~BQKXazN=SI{jb+{QiDJl=Z(J2*?WpB zYxuw=qn(P$RnpP4V5M^3gy5*wK!~g3_7__62K?>OGk5by_FiIH3ua8H*?@2k1Cp9g z3j?Qf2N3dZ;*NdmIr(DA`w=$T+Y=9%gyXw8h4QBs;=Yt!+uc2r`LLjLd_Ybk)4kH; zdzi>`t@CDD<>O@8jjomvGZYlYqh(yKBGO{Xn}NHmhEqRg@vc@#7@E0i7_(&C?v}*4 z&VS5bDjbTi-HO3p{ymh)gFYQ)R_ToTw6DZtzUC7Ka3_tAZw^X@3bThO&CMLmsY-Ka zE!^gEHr=mL^{j>08P@)gqR_b?+9SR@8EXDzmP>ScWV5$-;(fu`dtOEj&%y`#Dd`BK zMQu;34aWd&r;q6fJ*NiA$DhjVYIE$TTbqs~B4>Dj7T;3nI-OhIy2N4QN+i)yCO<+& z*?@oBF?EIyQyePaMZrbt8QW%m^y(1hz7?|2u}1k^lvFRvJsqxRtI^Zma-}9$Z!0%) zN@?U5UzC~Lm-=xR@DbQaxYozBVB4AFLu8lIIZ^jp!|X; zuP{fF8|LglN1;aXC)ZC;OW3qAjAc z6DGd%nJ~BRh0@LMfBLYSh7jB!(wywmaQbD2Q?RF(KJXF=7AsCYx%!)@xaU}bgY~Um zjvI>89wlH|p!YO&a)Kzbu9aB=At7?wn^Jq|F;{|}q@5h|BqviPqb$)(Z1th4Wb0R;gPn%CQhaa}pui`RaDn35i@0ssSXBq5Q z#NA`zbXp$%(%08FW_4{$9u=x2Cb(2G6KCEYBUmAl>sdx1+1D}n?l%IJR9)Zfk!9BS zZz!iF`R$9Uk^#lHaO_@$zrZrWu!xXkfOfb5Yusc8dPjbM1tIGiU4c zP5bqcvK49tm~iI6U^k!ZPQE}uOEmCFAyNM~3t}emMNYQ9#GVdw>Uee6E-pF5=1wDnE5jA3`kaqhwGh9`wP8$g9zb+^FE8luo35;rC(d_@b)hL2(j4=5J{^mP4 zsR9^O%JRu3!!M_3CU`2{r08L28qsfeJv&C|jII7tCIFDNIu+P6DFgno4ELVo`dEZq}Gz7NDl^gWM zDyZAlVbg1m1uXsOoKPGN$`6lP+{S&slEtLW#%mE9LG9dw4*8H@T>^H2;nVn1nbA}i zOYZw9sYh5h8KnU8F)2F_&kwHlD_SWv_iSX+4BYUudheq~hRGkD&ylR^JRW-+yidk! zskg2bN3yB&Jl<-*i%x-cv2-W!@i9aQVp7fQGRouupLS5APsu~lKao#y>2^M77(Hd3 zx{Y;7?kfSI?;2r8*U1TUQEHqe8{l_)QWGYB3wyPLa6l>kEIS4y8ip#@n2ZvpQ2SJ* zgZ-KbIba-~qPScVGdonbZU}jn`C9#bcdQM;z}Ab9^l30u7Ow6*SxHj5jup0Q(Sq*7 zPx*s>hnJu+IqgD6VO<(;O}1oWbJrN_?Gb2ol-D?j5y~`Rbw;>)kyryQKHIg z?eb_vK(@7H{mDh4*j{t&9k`wYL&Xh_#8^($2SOrlZf7D$=|-HhK`F4AN(Y3K2{_6* zbv$>cMwTa}U~P&$B6X*Ib7c*#@fzB;pJj_Dxjdu$7^5BeX6-A50)suKHWBy zzyO(nyug~;cu(lTc;PCqnSZ}EqO%o791xHM7Fd*!i?zUZXn`g2BSq|bl_FFwlOanp zBLI9 zC+mwIE2HMKMKh@lV*8=_%N6@yO7E=MK0<}FXrw${=u1wGb4`0;`OzYfj>t>iFzL3b zR~cn;Ky#N6EL|hJUF@tF09rCtnfS~fpa2e@@4PzB>oD8t!S3)Zs(s=40>R`D#Y z@J?-tk=D`T5&6aiuS$uglm^4UXdzs0*KZcK@eh63yT5iLIN^rTf$!o=BMJ#o+@rO~ zDx-d_gW?;+9}18Gd;Z8I8Z8SpwqqjS(b3fbHlda~nQ!{Yd~04-v?f zT{63fddtE+{i=1wuXX0N)$48+226Q|=zNF7rJHGe7y4Ro`mN6eNpgcvN2?7q@lxaT zpLV7NsstejVSpp3U(d^9>*iJ4F^_G9{iPy3NB<8`Hr57=BXxufR2M9h$*ftasT*)`@QEA23i~~oIzormJ0z5n*VD4Mg zWemt>H%3_KhGPT>P_T2g!HXYNQ&V1%+~BubA|Uo{-4u?V96hI4@_Ii3UoyMV-M0{F z)TO28fnkKBdw&{aABiOLC@YA=f~t{l$seo9w1atP6X5OGfeq3-KyD)gGt+RJ@qCm9 zLsu~*ZNaj8^6W$dAJ`Hf;?5&D$9-cr{W6~CsbW$9+-XUK z-E`|a^MPFB0x`Atbqe1_p4ij)`94T7@yY33x(i)4f$C}mSn}K%n2JA5F!VBP_hHCW z@f;HD$d4K^V3!o7c$?nEG_&SEaZph|NrAU?Q+8B7*W^YR7uJa@t3j4TxT*<25L6H= zWai(aw3u6>F)bicjq1+D@xqEIl39fEAc0*StG$$C^(t1Fk+&EabCl&M(jXkvYnZE) z5sJjSPsY=p-FD%{0w|>!9`Oszz>J}F+Cf%A$#peaSRE1L1?j>z-G>2k>DXMa zta?f!&$5}t9@fSZ+(A|{-7eLpjn9B}%6P;;=ia)i#fP7tfHco;B+Fxc*yCr7J*5yd zWo+Sn64(n;XCT+!-0ss$#$Z9cNNgquvH8%V6vX6~x|sGXiu@9ol7KHI zjfgP~x#2fl{%k8&y_0=j3O$Np_;vn(Cf&KNf`s9^Uh>rq*(I{BpmXb96T&*}&T#Md z13FD#6}_v;VO=~b*yi&ID<4UNyWzKZI!XeA^xjA+Kzkh-t5iB92qx|R#pf#3*QdFR zu^c2?4exxLO>cx62aJ!1Q^V+jvN6`pN<#ocj~UVY3N)B?PoP7mv#+nTgp$t0j4W5OTaS0E6;|9dF&i5HPn}|mL3Y3FS1wV zxbTT)W09jQ-GA{_*yjmg4)!4&@TesN3y5@`xes{s^-Y7wjPooeb7X?(!0(e-8Q?xj^$LJ`X>#^`Dl*FOx|%y(kPm=vMR z+<89TgpiHxQv+D_Y$j6d58-$gZm-cH_eJb1fDIi4ECkp!vJKbl??wSlGsctlJ;YVA zk4U2r*Y7&f=R4_U6q&u)UgQ1i03Uf5S^v-C=`9C-AJFO&3*bSOVJVQ>TbF|6&A z1<(-R+2zTu19SFr1l8xpJJByK38edEMl~^C_JtAqLI|o>p_OKhx*j6;QMB}DIEb6z z(l^A3GNj+ZrO*5@a$ucy28%zkVG{eU=m{{I@+)M<2P!7@p4Zw*?%G?m;+SuGBdhQX za-#mxXj{VBTE>91ESkHQ5S{G7XQ}b%hh#;tJZLotx>pl4mi9*S5J+-z)1*`gA_U9c zr6(?856*#ij!R~;K9I5};0jOolLo3tMG$s#0C>Ii=X{%yQeMlx3|{NO{EeI-57xu) zgkE0_Xehy{#(V1<9Uoj%z?9IXkX+K>emd&8kXe+XZBxRa* z+VN`x$dyQ=hm`No^ANyX_|obx)z~p9f_{&8F6TiTK!e`B7FlcD^7#s71guT948=D* z2jO7R%m+Di3w*nI@rSIj9$N$EAOnbMi^*fHmP9E`Y-!`kM(OaUM-qPTzyGw*sYQi^ zr5O56GFw#c9l2dpk}#nP<@a2#B>mi&oa!ll!S(o*`48H_vQiDHP3A^LYisMd0n_-+ z=L)w5-O%YJCT&rRuKsSXkeR@2orS{S{#`r;V%Z1&Oho-dJxb!S>X}--$~aQTN8BI* z#>XPag6w({RoIe?bA@i{b}Ac#Rr^ zzYKk?-WsyiiyS8(_w&`)H@_<4{5bXV=bh)2G-t*)ljS2LsB`Nkdeb#*b28l}%DWn9 za^$wy#QW@F6=c2wQ7L7kTa?k=k-D)y?&AXHH6vcK<~^B9Z~BvYU2fZ^+a3!M)b3l9 z>$!TR^SIwqqM$Ltqa_&JIjy{O;L75PFu|E6aA_`XA3w}I*q5W_x?egC+ubBzv4_Z4 zyGb6KH|~~pUmW#-l0hkOPbZTI)wnv*kotmvV%j<+f>ra4fwsAy9&MtGW-B4E$W2rF z%cuF~AaofLR&zE>K~U^zt3gN9d811vPoxM$;jQ%9K!)8pR4h5Te#hjci~YrVv(L{M)aYi8pKp09S6-*Nqfd* zapeG~Sva;HUtX7#UWqksiAX+StzXu?JZGu5uT?tk$CBHyib5Ra!4W3cj%bxBP(UKS zsRz|00+rxaluaQ%6|q#DrFK`koeI~21Om^=ntQb|Jmu;!++c9elB}+sj1>e zt3h*1wq94tUovY#arQrVN@c;@uD8}F5W)h z_S)NE!?D#(Xxg6iZZtnzEO78Nd2Gv;3g^U+Cd5@Xg1zcHZTlKigo zU#UKI$)~&H!b#;2>g{olH}f;vi31*b?rpwEt7^vBG7d@|VEa}-wGJDj^3?#V=!!d> zxp+elFr$#@W1yUttKMG&;ysw|a!F5lp1{b;n>Br4(A61XXb@oYQ9yLG5~b70!iDn& zR{xo<#Dp)cTg1f3ba{X`FcpolY-MKv_y45_!-4ZgBB=ucR2pvcKMQH=^^}vx5XA-m z+53Y(n!^ET3K0Z2O#>E#!~xgbw!##O=>3}CW7>;IgeKpR6cjoD$n`faSa67e)Drxz z9L5qr@L=NH<*1Qj>qH}5_yz)TdEj(#B!(r6<^%*_zpI+gcnWr!0dZLn!hJB-7x)J- zYNRbL7z3lm7?19<;P-iR%|nB?TY#l)2TIehAagTypunRGroy6eKwcQo^(8-beTcr% zSt`tI7<4b>1I)8!cmisM0TQ_%tqH4A2V7gvVPYzj$J_}z9D2DqGu;NfoK!unKgksR zEU2vfazMFF&*8WaYx-?WloLe<8R$uU>KZV-OK%@A`EhfxSdmcP5(u<>I{yU%EqyXyA&qMI1D^qW zP5}h(F2?7|4~j@?bLK`Bu3?z^C=|sfi>>M+)Ft4VqILAw{RVq-XS#&#cIWzws;05g zR%LDAB=+}d05hNWEui(aC$;P!t+x_be;Q)X!&RPOGCOhwiX;egxB)N>B$q}%4@pgm zd=}DgeNOJ2bRXUrriqWf4!9@_Km9J?EoCYo_G~XoOWSZc0kiGtLu@Ejh=h~3jE8Ip zP6Wi(Ki1dE^b0E!ij-w$ISk&*qz7*K-FUH)w1Lc?B#YbVCwSzdl_H0;GgV42sT?0n zkmuf#Cugyc@x!wkCubUq5M9NJu9-as`xbCM%|s0(!h@Zi(7Eq2kx497`xEME>Lc@) zPz~j#lhfWwaUoA@oG`-;jDG@8>xG#~IP0_Oke7?mYU^n$Bj5@(oS_Irh=AeeqN z;7`$S3TPZ$KZYMZi5HuguYGQGn`viJpGvj>?QpW;fbS(E=H{{a#^LIjM> zi~)X)Y_w!3;ZVRTK`cIdr#qqpRPgR9Y%gTaNTVE>5+Wlj_TYlmC2l-B)nX)-pQd&18`AT`Kb2`1A` z8vf$z0?wK4L{Fi^77*QH#*z8c?QQAiGwBvG$uie+F8NsV`l(jHUdWM9)L6&ro)%s(sQdN7B)rI!{zGb20QdTXHxdtP)&`Z)G~h6NoXVK z?=%=|`d{4I{|3>z6u7D>BLZg`l$y9Zp1)cBgYa&|S%!&%o6=Z^*h>U3Cy;O&1x)!Q z+0e8Q#4;9#!6-NIR@^$pw?&q8_4&MCk-T{v&(YVy;7vft>VEXApwez`{#WdqWW=CsH5)c59;yrQ5)!;Q-A3XY0tzU;YAK{s%<6oAoXzZ@OE z{B9q;OGlaUq{U5si}z1qLL>eMe59CqV;)k=LbtXZqO@%*_(tqn*yiTbvlW9xw;t@x z%7{nV0IQDIZnAMpg8q&R0C|(svr3uU;>E=k;>AP{0cs6`0)#eW4X0t5j6~qJ6I~Id zr=JpK(9X$f(@KR-Inf=y?Mv?{Ta&t&KUe54$PiudJ{$v9JR}1Co%oM5msu*O;gHv2 zd$U%#?QcKXgET-{5Nwe@OXZ-vZ%)T}<#Pi_!%1!my^#tB?s+CIGsR^g2m7-#qq?^Hu=-_d#mh`@@p`gw&!-wEIeM{!G=**9z6|%#F$-yy2}Rt8 zwz251_jynt+1n`o^m zjMW^uVA`a@<&;w@MCu^r*FjyMb>%y7V5O4W8*GcAb5{F~#8}=rJQaHn%vnWgX7jnHVG%B^^W^TR6l{hj$4zcy1VEy_$C? z$$orenV0vb_6dMss`T%bTDI5tm^t};X&f52tuN#6up9zl5Wj63Sw`vH*W3JzQ8w`+-{zm5JK04THkqd}etZbeAi~M7n1+OTIW-HNy~mGyF*!7%%ob_KVk0*yk& z24qB{+03$~2s%#j#akRMS3kCg#h*r=u8!9+6^5toZ0={|waZ27x|(mjNIiJkfuCPm zolN0Hmu}EX;WFjqH9X&IJ+N07^9JZW;~r}*a_<1YLSQMeiyxNe8^vozFK3T!xIby> z)2qWU8qi!ngIIzzE9L9$q&m+d*|a0r9*~6W&XQfJp`{ZAK+K#3&7Sh!Myh!1Q$y?4 zX%%Kz0ohykZ(>a3*{{MlbXA9Vnkyn;E=apn7gMLJVWhzs$(7|NEqrA?)PoEY5S<<1 z<=GUqpE6~UM65x9S|B!_3T=Hg?1=`6oerPzD2!^Xv#O5v7N0!UaU3va@=*b~_3=u! z9t+xe+8JmzV#X+BMig)|H~p+)`+z!el`T|^PQysvmcs*qEr3C0km$~BNxMGu6{Iul ztzpS3rpweSV2+MQ#4mad=9|w!S<-_O3I6P$>H|)6XW$^Xiyo5lpJhMt;nE=l$9+|E znpv|Ws4tcLp)H}nhDo`ruott$?<7Dh(ZZihAk+8lLh+5xdm#F8<)_uPu#;fQ zMvIO#lIKm7I~lq`{OyNs%=JKU0Ht~o5ZRRKdH@Oh1nD_g$c(Oo>|WjG@o06jTK*Ay zmSKQKwBe+0iRSt{{G<#%tl$a@LMGx%azaX_&Cj^9QQ9(gFieJ{X|Nc$awlPFm2UcO zHQ=cLd%xUCce50>t&bUXCP+x>`{`nZ%RuF#@XWz(~?mv7Lw zs6a5vn&s3L6NIrET~dIY%A;^l5kx>Ga!&`8PgdJ&C+ISF34>&WHw0N#EP;DR@jZKE z9M*bIgp-8@HeU}jv^an}Im6JSQk$K>AOz%M=;H7RGj+bGO<<1(zNuJONZAx#c(J`=<8FcI)GOyD=eM z+Q7`+Wm7bSJCS|D2x_6OlK?y+_l)X8*lD)lvo!QX=Du6VB5A{T@s-V+5WhZ?*6$`- zuD9VyJ^}ZmU4B9asK)7v%CMMfZ@?-LpTB5)9(r0Gal_`^DT(6fgm2=gn}tJff;_deym_iZ4{c5>FB+u*+%f~ zKhsk+UCQ=0>`p{D(61nys5UTl78iaW#1k3?x7#|V0|NHt*~N--PcKPhch^8{xDkSQ;41UvGnzX$OA=5``Gg;Dh5F z9e;fBY&RV@2JeFxKfD9jhNU!%Aq+PPvM;%mj9RJQbx^vj47;?cUzoyx>A)O&jQVDd zG~cg*h{OtBE9HP2kIYH=3AQT-38T55)Kn0+SI^z|UgU8=w$CR4SWXKu9az3rCf=^z z`#QXUUMffyJAO194OX*1JTO_eZvgGmahWJFPMa|wLUTqAn3cSdwzvkPjXa}=eT%_w1-Pe!!>|vy zkNDFAg@yd&C?Ydgl>6r>xMaO`yT`gJE+ckc6xt0^r1M`s0>Ch?DIJj3Sr*XqEKRX3 zxo^4Y^7FZ`s$wzh|Hq-v055xaxK16c)PV(T+{UFGt57vc1)QkDfYmt#^kuMdR%v0Z zDEEdQc;QeW>@*0hQ24$dO!oikKAs>?z@D&&;XNVsXQj?5m>fn6i5{|$sdsc*WOUPq!Sz|@O)5jrPL2qcKKfk+F`nVC^K#St2hKt=U+ zqIGHA-4PnobPK)B3d-Dd$zKkeR12jL+fk|rLJ?^Mw+(P`#>9e*He!A` zi+V`J{+;#7OysL?)OzI#su3KUO^AVZWpN^l_Z1F1L>pp+#(>3hw~)PKE5?J)N18|BNT3*BjI z>D?|fu|;(Y{2&Xr9JYHcB(nTws?nFEbhJ^q3EYYC4klZmU12XSM|u1&-uJXuZ4N3r zrP*zi>0T-ti<5qRZGe3@UGI?jkotl2tZO7)Eez@mnyf4SiF%Z4NCS_Q$*FZ`@#Q_29%LQxxHu13#VElQ|W_5aw~Y< z7IS2P17_YBOq;Ca=I##aNw0`>jB0k)TiXva zJrAy|+~iLmwOd#h`cH~mb>u6W!t-%((LRCk-B(L#9DA`;d zc+2c61-(zbamQV?a|~WASbHD%I}R3)QiC$KMO%xm`K-!K3f~3mql4Y7qP-r9wSXrT zP$g$p(d|OdLzW5E2rlPL%`eSLWy3LO}?0IC^Bi+!nZ&wqHNJKdtQk zWZC+;L!*9ADCADM_mFo$g=0|s3xW*Tbn{Ti0z9b1(WJtY1w_O=met!hSd~P+2WoCk z4-1Hw9d8u~q`0j_N78Zbb{6p;H*)TJdvv>v?M66so^mT)S3$o_(O|(9eO9!@9)q?(83iGPjy8KEy1yM_vH9 zyUyt^l}1-2&Spau-l2}tD-G_CqRe|zl7XXNA`xNh(xR<03`wL7bp+v^`X{k?4D^UA z>v!rs7FjTqB@W60%G=g@qt<+b{56x3x;q=iY7F=*eDYQk{EqZW&_5H*HxeC#4wB?MS2d(Rx$(M3>D_B8V$fv#gAOloOlozD^<$nA#l}QEGYe zd?MGOFqsV=z_fr+k6p*J8Gk|zJmvEjFs&0gVBYGaegBzWcmI!Ia6U64KgSk>y$NY| z-qzlH)A|RNuxmpAYl$cS4fiF4@&i=Xvp1(yu7-)sdRGjc4s*RrzGZjs$qC!!93&cH z0+ne{P-PGX^uP+km0cN5?y9PJHvyxdiTG~a(A9^Ju~60gEU*vlBI{l#7w`0Su(CuP z;;;bHQ%%4EUm<$iT4XA0wReM1ak}CL-VOUJ+b1c8P$mQh)x!J5Yv8XRLXgyVTUXY5 zB)z!_73&L~hc#E4*1my~vp5Ya;EA6&7jzC}?SEZLbO7I(X`)eEzDKwn?+_dbfG%XBZ+{=TaPlj^l z8u3ZiB8N}GV|;7Kn3-Ce;<2PJaz-PjD@lDU~v;aHVU;*8P7X^dL2(-1vR+2Eu? z&@HMGUs69e@0-yixkQ;pwr(u7`%ccqb;W7ZW;eyFeX;(<7y$1QblPr)rqGsALim#? zu&zU>W(hyq$3(o;Ow(~IYdW^opZs%^d%^P8h?(%JWsJ#jwe&z1SD&pRA8=KWZMbXR z2BzzZ5od20Qa8J6lYZd6`EEuv?^!cOn*=s?uwrss{a(mNM6m&$GysmP9?^|u`Z^Eu z$KAopY;4vTj#Dxep1I!)Vx&OJcf*KdT2F`F>jFzEe`!P=7SO+jG!`L`Igf|I%bB!* zEiLmCwIJ8$BQD^4m+Ynwk_?}lAYbj{eEO%8Eb^fM@RubUs~5zoW>)|khjvNW0V_Dq zHDW8q>YZOY{G{{oU*aG5s~?N9Sd#D`YmY|i)b81z;B`1}YW25i)h~ydOSMI^Q^yC z9D*gOHYVcyw8z58Bx~YGC!1U#yQB%Du5n8N$7uw0U#gC-_QkQ7(;}au2lp)`OA<<9tb74q9uFfH&kD9R{@w6N@zOa7*xF|840gfG&lRY*IbQhPN6Pk43_Ip4{>hR4 zU9;I4D9|gx!bJbe7J%@Fg4q`o6D#t88R`I%+>@hiN|^#dOQ~3|qusd=iLWj44VbY) z$3Gj~@wva_DXj&^`kSvant4JM+6Uc7=-EC@PjK z_m>?Oa~e{Bl`TeP*GOv)g4cI~MAq)C@GfB`=0SVY$N6{Ga!&P(2O$?hZxV%~|qW&T5db(HO~)rZobWgrj|5A5HEo8`uL&=0Hk zwM%?w3P^ny*p4FV$L{~khjJ|gMV30Y1wIj8*Fj+5G@-~hS9{SfQi!Y~V5gUi&L=GW z0W4V16(*}lNbj89;k-r<$gqn5v4uB|AX7^>_7F4&neRRF7|}$mNtKu-6;GM({P;d{ z^l|{Su2|P8EB9J1nd$!YsHR(8+q!p3OcwzxL@37(s>!Z!Ne@p2Id2R<7V|nxR5fa2 zN`EGj5k6ln`m(8RX}`6r+C?DBxS6;Ahim!%D{K8S!1zv{_ zoZjj0w_q)i97>?fXMxD{=F4xTe;f9ROuZHvauq2xV&&2#h2bzy4`n+w77leFKeg+Ovvw9@9c) z9tTm>s;O3aZTtmd;RpCe6)(nX_CfH$93+k*H++Ww4m|LQzat^lMYy`H04GbRne<5R z17S7jJ<~JlwzBkca_@0b{M2ch9i&#NgSqR-f^H;a9#>X`P{SoFzg`9L3a`@L>FA!) zrwb|@Mge*^+=c=RLawS~pnZQczaMl9uUjktr-*w5Og-(M>RfCd?AHCA4Bn6c^&d3O7qM|JFl8qkf;B)k zZEkoT<$ICX@LP!7pLr?&%J2UPA>I%meR<$u&T`3rzAf%e0OFQHvTLCQ~I)Z6bQ!0``x&{rUVd_FS?;3p?X z`)D7|evRj_cciM!nH{|%}4ZV-X4NJR#j&x2i<42zxq zKs7#0dbN_cu;@p|(hWeUJh)>(&6r8FbyTsm->5I_iAP(EAkPQ{+*~6BP=Kz`{D;J= zIUkz8z+kH~^B}IbtQ>lLICh-5UrtAL?wf22erv3-fDY&iGZ!pzQTAsvaj(v81Om5g zO6O9nW*c{B;#VrNrnEfunMwPH2t9v1cL42l3iWJ97tW-xX&q+BN3tu7fo@*iwqE-~ z`9N!>=Cup4K*D-p`vuhPWiH6;z;F-@**zn+U1H^y?)E{?8gIE`d(>enTqETrwJT%A z^3!bjo>ZldN|xIFA?GA%RID|%x`TSYy3pr~(Su)mVu zVrzO0k?so%FWEJ$a&F{C;P8NfiDfU!IIKv^lTggDa}I*msg$^4cqx|mpT#? zrqKrL*>Y{nfiS*{Tg2$XAd|?41mwZnT&SVi@oxNa6YS0E!5evY+2bwS;|LEMfBjB1 z4V`eY!#O3>Y>m|~xy)~o?%M(F_{2D_bu79ecwU|d6Evw#mFuxgqhoYZ!Q4t>EmN14 z_vwIvkt(+NaRcg0x4*a*bi5N}zO^_o$CvG*uoXvcPY5-tGA%vYTj2N zN27W>e^IF5G7*Bj`9+z-I)92(C;}b(r2fhP*QPtohn4P|CAV)a)4NgyUOClR44)tP zgh#91rT}vqej+2ZH=Fn~Q~c3Ost5=ntC0Ont4x5}`Ek{21Kfzn!ZFYN#nSXTmr2cs zD8P6V0~9|%AG7S!w|Cui9zI`1-KFcKK^?gI6D}q*jV>gZMs1g>kOl3tfMgJ;yW74l zCrI|(+Ws_4dnNIL!DsKHs>7u@kj%Zf^6{G9V-%l`OHeDlL$A<&&Jb{i+6{wg~(45Jbt)mcqn!Jp+_QD8YHK~ym zi^g-H3bs`&x^^aXN1Xa|^W~?M6${&-I>KrLx1gL(eVm)AE7$%N9rHC z{S-P4VFb0xbs_XJCehg?(@_rNuWpzG$&ZbV%T&(R5=pH60rMd9eT@&bAd97n<6}S2^pUXZ}5_yR;bDz)B#5{$+qWuaWYs0-7nxo)>hOyV6WF+;_ zR$)$)4td}NI^=0)A3*i&`|c#D#>S#i&<}*!uK|yw2#%CCoG#kwAg7RIEu*f=kOxgD zf;%WfX0G&W?^0?UMY+Ka$$Y7=tv=$1jdoG8dA<|={wK-C&N4^Fgc~WNSGGUL{OE$K zQJw@1UU~l}ar+}O`~#4|)eKD1^5Br>v9b9Pp5%nvLm$oYj^^Tzm=-okE9mL#_O)AU zV!R@tlbgwV*i&d@NG3AP)oMt`rS}N8qgg!N#cQFYt+`Du2tKxR{@STg=a|$_K~)UI zlY!ck#QWi?zW1iY|EixrSrCfSwP>f%bR=q9wR#alg%ien2~CjAyx{&)s?NKA`@mW zL5E9vQ3L9Zx7jy+%=_&_qRQ{WLP{jVm)CKOj3_D)>)*dVc_JBU{0gA$%9*jY2XuUGGSjB7UyC zzqf=~>xV-Qkw%6u6{@lOp33SGy+)G05=0hios^x>{Ch- zOBrU5Y=5-!!W?plkJ*y(feUMw-=JmY) znpz^qZ!TSTGi}R51ktIDdJwPQ9L8i0DkOa`FVoyw zzD#@7|3m#35+Kc*gqjbaewE}TuaHwvpU3-^$cJ9-Lw*g?2aGyhC?aRB?zc=E2%V+f~3;j-Sw`0@1ys*&#T|>Kk)j~{dl+M?6cR} zbIm!%m}4Hxok>S`3BR(7ID%ikVaV{}j3gGxZNG;|?Mb_Og`pqWx8>XGNV)w&yiUE9 zM9H!-v#ugR4z#pk84k>OfKaLX$y2|+HNsnT%cb?%mB)za^O&7heQJQ!ZrL5Eu}%uq z5Tg)hOeqVfpB!NMVp0<(xmk5Z!zl`~oOGYEEx}kYwc?S8_DnsT42#9zY`WJ!eI>f7 z>l3@rK@x(!tnrD&vs&1opv;fb?3IMMW~tv%P58TYt()MTd@LR((~9ss=FY>h(%T4= zQ}6v4BUlh?lB0T;s#)IFOkk}=&DYQGQJRus?Mb`FpOW~&h1X8fw;yD5_ga~f{4Qa3 z!46fA+L?RQfaO@IN_>4jQL)58A^J~NSLBGmE}%e=Tmk^DcpbMf(8u(rutW0eAxIsy%HEgk{96xk@ zyp8!cnSHW~(ZTA_2I^o-Y7C^~dq^JWHP&UoYji+V;W)G!`6}Ie=*v>Mc{B6G)L+>0 z|JJVGG&2heSz6iGH5+zcjfI)2bU^+jKpB`rf7AU}m8JXtQ)Nk&*Wy(Cwx)cKf-J2O z`9StJ_YOcIV3_?1{%=wEe{S&6eNfL^#c}?otP;;;2I8k8v;WH>2dXhMtpJGdI6Dse zcRN@g+>9%o=^x5LpoAyRf&ZpZ#adPbHv_pM{cfKof9ij~K%4FA4`rYqWS~mU1Gw#` zLoDv$h{%6|44@64S))MS7jU`*9kDcn61T{{`IJGn)WF}`ZEQ~pQ-m9xi1W=w<-n~G zJ*WGpz~?g#YX8GKQvgvHS{R602bh)l)B%umMsveTL`6TU#%@A9$mManSe2$g!_gM7 z7uo3Z^w>TE_L?hDDyOOM@>sRj6;=+52CCDk$%kd*d6ATmJzKdhc}&5fz`Wcqs3%h!5#%f>xd5xWN$m^#zpDw( zmc*{y5062kvCY)tb$`X14ckF%W)dC8QAbcFvqH87ldD-Ps(o-twhpqI`={WhW8+zL zHJ-*FT3`3SIGPgk1|Osf>Kee(!--xN)$M*UIC5N%GVXjqH|FBfPUdD-O9q?v*N)zR zIq4=Y`fgX)BMISlc}@&JNY)k(T1F} znq#o@95!#JvuymvqSllysO8&SVJ>>|KHo$fIO&ees$r>DRO`ExZ2xTUCl3DA^&7U@ z=)S<)wPJ+Z7t{rikQ_4N9wBJPFX4R)kRjk%19?T1I9hwu{%~~z4}^?s7gs~Zxn%aAuK#6$T{f*&=QFHrSt+); zI(|G$(3q&N2^rB2>Q3&)uM5Jxa~5$!sm37B1CNo~$yeT2(ZrCMptekw<{^Xz6nJOL z%>moZI1_m{_tn)4Mi1w9p2Mr}Lbx!2yuIj*j_1fIFX$0Vk?u&}$x*D(d88}zIDhtv}!GYNs$@mW_Y8TaQRfc^ySw904ar+>OSMy}#OS z%1@5gJNfb#J6ZE3^clriW4nJ*9DSW|U&=;%*;L1i|Yn`q32}bU)3+%N8Uyp%_Gj%L8- zCIP}0Qm8Ti)86ahR75PolWvami&9)wVU9!QeE|~N3U##7B0LrSLNTb19gn!v8juIZ zpmhWV2*|^ZFpl~^`QMl;y9VePMO4g-3{PO z4wqa4@sqgz6%=#Wjhg})<*Rtu2Hk8C&eqFS;bwD5s9%#0PDA~gUzl{zr&1Z?im2ojN*w= z<>ix{R~omUGNJHJDi)U$t%H7((gG-w{s3JMJ9zDH3B138x`rH!G8%Y3!J;aac~G43 z{m)-L6w23#kpI|iRT@BC)CYhp6v*jmf@N)@)=jzV6o=5z=9LXF0V{Ck?+t$>q`8BD zsR-?tzaNLZ5jL$~$Sfc>vXMJk#|e!qM}fr?!b~X)v-B%+7gde}Oj&KI7|MQg?@{=c z7o`Y28$l@zdqKsHogAG&gl!e72pcMs6aTm{LBeF4A?iKb1x_HLOY`y+VA9W-c4&u- zC7w75TJE#9LlT&4jmstQmCC;s+rvxN%|tx*Lz|2}`lfda9zE z$$DXCd*q%M2P3A)Vn>+iQK3Pw>Qa1)^a^dN7cMfTb)GctMRe3p8J_Wqeoc<}q+?xp zM@8&npMS>x1`c@<)HmXcl|=_82e6>{gTAPU+NW$)T5H#as`lj;oJZez&UeH*^r6Iu ziU+pPkox0EC5(dAowGirr#Aq58{dD}&Bt=c1e@)sUkVc)O%G6FWFt?Es^CZWq`knl zY;_UGcR=J-{b%LG5z-_x!P)O%%RL-2e_gotGa3S&%8fv_*|9bcYkpK(Y^KMATRA;^ zQ^F-H%@B)uEFR7H3T=yj9dx>clRPY=C+*_oSRBD8LkJZFmTRa`v^Fxuk}SuLE*a7^ z6uFeaYJ%F_w!tAM-KQ|QG(ln`+T`6jd5yo&rnD7bFN=rj`Y@7t^W9Ut;c)3~Q!br& zt2y!yfYO>fErmTDEH+~)7E?UBWAI_9={-to+hD8v^AFL`*R|*xRwSgf+{l1QRJ$Vj+<%C~3N#GidXu ziJm6(TA*B+F6+z`jP_{+zCVX)TZ?5ah=OdBd|r!C%Md?6cTJ_V8>8ou9UOTPUZ}uzxiS%Hv_cJ2Eu!fk4sqtCr_XqCeKWCs@t0Owp!tSGX zi@6_jdbDL&svp>Hu@#_rgk(@22%95of7TJWnrmKn*JAZ^F1?=|@0yIpn%IN6-P3F! zx(}p_*P;$~&D%e?Y{d@u>GdoyFv+!M_6J5c!3|3F)nN52i?EaA@;)yOWVN8Pwyo3s zJb0j2&z}_GAUwt_mVtUYGVV@>WBC3zU_6zKtT6axD?sNcxR)v76S{gf3bNlpsGs|V z`gnJhtoj|*a`z~D{~~d5sxoG;+rtw7y|UA0q^t1o!S5!_H z{`i=KMi!3jeRgQ;Qczo$B;x7%0U+~DkX?TXNII&mfHO@a8PpY6GHMbL1g*`z%5M(} zdm9hOt!IED6cs2go-TufXXOWAT$`YlMUTFS*HR&E8S9^+CdmQ-{EkWGjMz1tSTN$< zHvYH}&rGJow%C_JaS00KU_iCyNEKeHr1AsGVvqril)q$XXP2*X_w@ys2ZNGd+~Bb? z0H#Io2?GS*zp5C7hQjej`4fN!uutT&RADNnOVHyi03B{QM|FvOb_RZ3z<7t|NQ1$f z=GVhZhKo;*zN=$$H|_Ii+a79jJ%E4yvNn5Gq?$RPRlhbePR#pUe3Hx7-CcMJ>gqHG zA^__HG$q0Gq05UcUrjIGXVPN3Ak_;7PCh|*H1$gBk{c4lrBL=0$2jxV>`+*^EWhDt zuJ+^$PvpPM3l5?1q*qSmrc?Gd)e#o4|9IWFl`5dA<6s1X8LgMV<;&jM`>n>K`mG>{ z>Omu9&wSJhoqq*Y<5oFGJ(sf}l-*`7D3KfmV?mr!b|E0}9MCyoCWK^bZ`HG#h%8gF zi;YW;z>FH6dmtRBlcor{J%ehttZiOk7G)2ZwqPKPkbZ{j1iVMBj=_i$0VQdgtbuZQl&c|%oD@Rg0|QmettOD25vd5S zyn)d*EdvoTA;jHvtY4mOVX~itub}8#s!Tx)tp_4(jCzlHzN?Y}MdqLgwg!HKh-)aI z0qG-c?g`i=^SFS8(*VFEya^j#7$dJEmIxFpFH4 zgVM@lVee1ZZ@n=A340JVMC>8O)4lwA@}vrgYEBFHk+!75VSQ4Gb#J7 zZD<)?na5)^YmnnirXFgH8ntY*o22m&>&9YqklA&ZMhVb^)nO%5(^1$|6mh#@BMGc$*zJ=p=O*{89PG3?mgQyl~{+04O_&{WQY zp6%&o)!^@2?vy`{rg4pT<3^f=%9mOw44l@hW(5Xfs)*J>b80x*?W}%?kM_G|$SKWU zcN2SLl|!1h?rM8mDpATgtN9F+5rm?LrO`nUEBOPwgE<9QyS>7D-OF?#gz`%~pxJR7 zOcJB90Rw_&AmvylZVP1Wx}CP=#bA&Abf>`70;$>D3sAyQWVDmM8JGKcb8E1&W7M1! z!B~bYa@Kjm1Jn3h*fC}Ai6&k1QXLf)p3oo?vw}Gvj&<{ZxPqHd8;^zB#6^$L%JOsE z5S5mPW0_0l>^y0~F*QhVl0_5+I<>puVcUTy>4{@%cDtW10khXOX1}WaEHoKFNKT_b z6o2=&_t?JsiJGX3C>Vf~WodFKn26PuQTMbOke%$H<8;(uV4*U_sf)GNra)H!7il=b z(8rx0aQ`OAUbD$8-$(v=+k^qOoVLfOidxmhVzmp?c zi40nFO()g)g7)%-Hm$`7(|so4cAUa?M-#$6&wEOTfC6YcIyj`U9m43p3qTOuE^$6_ zv{ojhcD!Ct47WI4z7Y4sU3RA0y$nf5Ru#K?0K05GLVT-;PO2#^M?B!_LL+=M(s=2T zI$_in&>JuoWfm&hElkPi4LIs(a;p`+oW=r?=Svi?TLZhwa;au%(#TI}5G?eoIx~0( zlJsBGNY+V_Hcuq)2clo?MEk;JV)+gr=7t2mut{tFPC?}b6fn4Xu&Igsz8eoyElWmH`<;!DYl=1 zV@nVKI2mEj>9&mRLAQVmrjp+Goh)T)x~1>mk9RXK1H*miX(+-ZSE9i{n45PO#jZ|N z=fjui5SVQq)^;UP#+Ow+5zH8Q*2@ua_OZ{K6(sP2GhK$4jsJcFff%jl@Rx# zK<2-?CG*ch94EhqDqGo_Ytv3~apbIusfmy>~n z20VgP8T%~Jb4m|t(%IEW`2_fVBS1^3A`YXrpholHYRhpD892s8f5sYx2v`M^?4*`moW*;^4Q8VaLdMOQX zpGS_$`w%J{rSN>PC+)K4a{)*B1+{6Q?k?2dKj#wHnSkF-4ruoe!24wp%4Pq~MxIz8 zRJN&%RSgk)vUU)rpM#Yd+x4Cx{p%h2ieNp)i0~@?X}S5%y&fknPg<<|a?(>6LhEXZ z&&$zCN*ysIu=-l?az^5P-V}~YyhCUu5XPh~D)YS=#S!Th+#A2Pwkel67}PN)bKP?M zYB;dFSrvL`Qh2O2&`8?$)VOIuUHel>!tYl{P^V!>iK~8q=nPlY3eurRr^#QR)Q3sMr}NaHf|;^oTk> zFP2OUM5g_XK76^G-_rNowPcYV30L}y8T*oxe1@Gtt-}RFi!Ts9My3pJC&&1Jccwt1 zX{o~!{t5o!6ioTZ$vS8LC&P;pFD!jnB~jUyjw%@3M-pJ(b>!lN)j^WaCx=>KPJ{f& z6C5!?H_%CfW?Od5WbL$?%H1RBqK6CQtZI52|A_?Je}`=KbY7uDy+jVLNy2-&4N!^%f`EXI5zF@! zDCe!ZcZ!fjs(pCfG~i*XMmm@x_JMR92wIy#&WGE#Q+J<{&y&sQkOn)T5uXYnja8b} z0M7;q&#k@4{0)_@CSToP;*gOB;*rgd_b-uULz-kc-$@y3Aby7TFZXgvhA^n45-uNO zsh}`fQpQ`t&Z(qv#v}5m#Vk`DWPeOVtZAK5Gb3tziEz0yW{*S_@51;~m=^iAQLepN<5yssKw~3>|$Em>(8;?}F0QxK9kun?W zB*?!1#?j4yDF{XgelnLEJ;M=xEZ@Of{t!9ZhOQinSe;reXEWa1+s*|Hm#_gbpFzV^3s2vfoE0~-Z zb8bHz@yIpD^w%`>=E))gqxbZvBQ5QUnkB-Xa5ohJLS`s2II@_(-32CvL!Z{|LmRuj zl_j33X~}1@9TJaeUDV(RyYN54S18n=0EtRI$>xwVF;rXaTbQajL5V8H&9Aq!xkisxsKEsnb_snc?l~TxUF}dFUV>XSTu;PbptRYez6o6q9MT-{+0clzD{3ch7E{q{nZopMp9KaTwc1H3cM# z_RTDALA6_+Z&rK9tP83fM;!;zn=!sWBSJ#=@@4sSSRm)Q)VletpwrLjQRlWRjWBOv zGdq-(8<`Js!b~*YsLRSh-lj68SePnxr1?YDI%M_?d1MPCR93S@Uve1&-z9tpFzXu5 z>RST%mywWreZ>Qw!=i5ty18BAN`q+@SPDLLPbZ!>SN+&Fn7{nISMdU5V??|~_o=Qg zjLdya(zh!=uiN^C2Z)zrzvXo`DbQ)aHYzN&JRxi_z^`mGI#@KVDUzTB`;f=#2}=#a z^;7b6^Fc1(_qZ3zI!j_?O1^u&W6`vVmmBX9N5ks39$Z*y6b}Z;TOA({&NXF6i;<9f zu6|^{Tu?ARz2YriF+^)8uiA|%xva4cLYGCuq#4%Ewg^yG6Z0>?(!P3}`v;8V(;4FG z9cm#F?v%SU?5vG8S0(Fy=1na_xqU&wq}c2 zn04^<)kMN`mW1CzF90IcZ=xxqA+{p=71(}moBsrV@iW}FBQ#~Z2*DeM6i$D*I{+|nz+Y;2LhJ^At;oM`xnCVzAQWD)@T>aybv*rb z`e%{=5NW-QqT&AUe`}1m95%RsaUlL5DU0n2!L`&~Q8o=8A{_L2Y90Vpkm z&OyzN0o5{EK(QwU7y_N4BT#;$LYR$~anKz}Fr-vz35*8Cj<(!#S79yh=FH))gI%61 zH3P%5kyR~^y~Px}VC`~?APC$cyL*u)=Ic{12ylMI_A)lmJ3x3cyjp9uAa(Zi##OO) z{_wMb`JH=sw`TyzfDBxhRPu!>fObnK&RH_?utY>r2DJ`ej7Oe z34L}$Qn$YEHL89LkuwQ`7r`OxAj(~yhW74w?I1;lyJXf$0yGc=Ht;S;l-KTjpMB|4 zv#Abcu<5)Lh>p_(&Vmi{(U84)iHJSW*BblHorBPT*WBMYl9|lQpCFR{YmbcwArR~2 zT99Jb;9fgFVvr;Z#uLtfdIlAOy1RFq5=|amyGF96AT9A|Sl1363eLx%z!)%)xu+&7 zN8#;!evSzk%pYqc`coNTNOElJ=~^*GP%~)r34$~L!zjkwF0+l&FIRk5!r4Q(RH%c`#_i_S9%B_YBP^b0zPG{9| zpo9@!v9s39zffwL{v6N@5#lk3a#4m(Jv_$E=zMU=&tPH-9t1yO#_a%QhA$X%^eoiG ztPo`xQ*Yg$RVowgovugy;m(ZybsWC#GRp*z0HOq-a<@`3p=m26K+D@^202B0^^O_^ zpwDt)x4Y*y!Cd*lN@jB*tB~_Q(Q|0Mv*WE?;@7z9jiLW(y}p11CsEMFYPt4N<2Z|4 zXu(n5+ew1PzVA~OV#4pXhXs!AHch4J$ug3m%2vbE#}5ML zY|Vg&)nvRa_PEb&IBfP>sTy9pVE!?y{xb*Hh7zDuCxC$4Hjf4%>ka}Y76jq5MqY+^ z`?n$S;cG?={O}t)k`9#mj6OtdPOWVH5TW#d(Sx+*2tpT(LHL0epSY~#B;W%jL&m__ zdIZJQaeL5~*OE{W5D8Ro1NepC^Sv70)NRAlfrQzE6q_=I*Nji91P#4H8Sz@%wGy&E zO|6oS;8XD5mQk-$1E_c!v+v6URR`Ddl2&}(mMVJQmD4)cky&k0Exd4CET;S>z`|>M z0Qau6Rn8V35ZBZW@Eu9@^B(fU%AcW<2N`Pdau8O-BQpI0p=3d`((a^PafZo!^i&%V zS==tvs<1JT7&6z2(!(EoSEXaON-b3xZVK7vv>&aEKo#XNkmVRK3R2*Or^l|8_@+R} zb4h2iiOY9?@j?#Ys!+fiG99AJsE)%DEoswTUhF78a4re8i!f0!PIDP%!jsuLzQoNq zmYx9ua$7~6V}R=z93K?>>U(UVqB!3{coKZD9Fazj>&Gy^Uds^Z_RLGPyTQ~eQEX>J zEs=)}TF@Om4)mufpIK{`v-r!^@z}FQaMK@lBG1t}SHiO2NPYCZ0+tmrc3i?=fHw$H zefKv1UyxV$tTe=oHv|4q?@Kl#eh^omFjwzBC$lHS$$=KjAUm%)5@Uc~Vn7WQrlSl5 z>=6&}{1HZC;>V0FEZFthz)`Wbe8BsD+4c}%#DWb`3=xfLHSciU@n!M|%en;WrV|%F z0t*EbZ6@!NDV>v-4CV`X$jfAsHj|vO7a?Yo>fE8ssC#0ZKBeickbyEES zI$FMF+jg;3!w(dWuBAHw(P$Xaj=O`tdg5)0i)FdFivUlq_(HY}vdPMBlN}e)xo>y(wb!Z_p#5dGLTp6cH;P(co)K+*F$;V$lce3bjd%f!c=Y;?LQNO zw~~H4U7^@Cim~$2_*gEVM&JC{ZKH*6gyR;@fX3I`kP_o*w-mW_2%bz^34m4F?`eAk zbEvC3hi;v;9^={N0?kILb2mYc0;QeJXevwDFsfAMaH3cWU*G)2*%2E{CAZCJ$#H6L zIZ~{Vbja5XBWcw#+ChNM6ZUO@K(#YQzaZBr%dPXV^0Cm{BY}!KFvZiz7{$2;OM(9| zHH1Fsqw4*_as1vMmRzV#Y?6p5$K`P)px!;@X8*$|0-L z#l6Cc0YfRc!lRu%=A)4l^{P5U&QZi<>oRF1?@7X`))V#v-vLn?fbCxV1KV*m4S2XC zzx0~HNmdjnux$Sb8ddpJa#YHu4xmB^wx5wxS|6u*K;yJI^b-6j4=lH}_D>^5s4A$o>p=wB5p3!} ziM)@*9H=sU=l$?-T-CXq6^V6_9ynG@Z30bOo`r%tA(!A)4hfB*k~`>+c`j57mfcm z@d_|2JEBuSe=#f4UT>gUYrQisjLG!l#t&R!Mk=oa65HuLIrz}ejr4@Xe|HBJH>H3m zt)XkJCvwYL+CKw)h67q3?TKym1U{Q33tyZ3eCLC^@XuJxNhQi{fqZj2Ci`-N^HcP6 zF?_r>1N7P(L6mCN-5EW}mS%{u=|LboG`3nkPE~->fBJf(o$B;!>crp*{fX`AGXVPD8P+n59L1g< z3LZ^>_adpoO{F?7Klu4Q)5I7K$5tqw68!Kk1s^iLc?2TR7Fel9Eqd^Z{lON z9F>b?Nz>Zx84Ptsq8m9``EFWIb6#bQ5LoY*%Bpa-2z_ZCjP)l$T`PObk{*QY40{^O zfI#Q+=?oqfH@A?I^hUVcmwBPjU0R~<6JyE&*UqQxhvqRyAia@M$5!P>WU~l-LA5)J z9-`J|R8D9>dh@=)Lki*3ySA|B{Vg6O6%><+D!aga?56_H5Rz9T@*(By0p6P7x8Fl%749Wi9`h^cs;qfX{yqm~nP z6yX}RxybIc)=Gm6UK_KY1PlyQ-+k%NOQrULf!eN;H^&-(kqwjV-agzCVe(_aLg3qo zn4WrFw17~tv8w>9g`LJ$@6Tp`kxyGpyUf*!8!Aul?#z>t6T#%BvA?UInlL39aqd8PZV2dF6Zv+Sy%nEcNg1CXuU*Akck&Kl(&vcCbUqq?U7*HvYb@#R{%m4yO?Y?=lm6iW(X zOLGg-A_;e5!Cp|wQ-9zPr_!q?my+%Rpr#C(GnTSvr5sEc5yF7*@T9ku37tGO&>0sa z+FFQA*dVeJE^#-DJOYvCJ!fsCQAfh3rm9!G)cqE2wu>Sp4Ufw1;SQKfot3YYuN>RF z<qmR%TU$vOR`1@oAqhOCA?mT-F8JO ztG{hv#OcRo^+kI0ewK*sd-H9^B6l4el!&#`7SCOfe)+5nSZ&Zt@T8~e0>iJl3mM{F z^EujhMS0mN{}2vL?q7hjErXhtz3t>Lj^(YRaz9Zh3b3m;6Ojvdw zK}^{Lho$L=&s2ln^2CSX^yU@~T)`AE*rSbl2f^2c2;l&ZUaM?gC?A5|L9&;pIQ_wX zr5eBfNNUcBXjmwD_ir>jB3PB<=eg13NCoFENRkhU;-jw8KGso7pNRf~da*u0ZXoVw;^^|qG0;SX3d^)&9}C)N7kJa$PMt`}Ng81yqnHm!t- zo}7a&wFs`PABI2HGETqcDUug5*$qrHg%K3~RH=!7M`5R@e85D6Edt*wGfi}6-C~=& z-gd0~jH^xls~DU`>nKv;yrSjySN`*o_1j&8F_#IA=jP`Mz1QInchX58w`MaHv!<`< z0oEWmO;<>13k_kY?R>%IHCt0Dy^P?FSIZ<)0dDlmTPk-~v?DALB|nC3D)w>RD-Le+ zYv>>DjN7d_Hb6+bKkcnqukv1@50mAN2Wz8Q&wzr|c+oTG+sNqM;nQE^gje74k=-Wg z?0;?49gc-G!Pr-#B_CvT9pcz48;!LFTBru@lQdJ10Re zr+p)}vZ-{PpCg)pgpAY2>_v=&=-l;|?#f{uW}gHu>~uucHZ*u~7^?}qkC66)b_sDf z<7XQ|d+faM}hY0iQUCxfF{gSo1EbBkPY5q_amxg5>Pay?BmFgKjVNRIr# z{HvMf)@@&PS*0n9*vKD3-Jk~lyWaN~3Ac^d$O8y=# zeQ=RCvv-?=e8kJHq>a>U#glTA9$po+8L`~Hqk z4ACL*(Xhn=d9yCJE$PdVXTw|Fb77Wf(0cX(d2>=!uetK#F-BQA#hQgu=n3`Up?yMrj=u z?vL>-Igs>zSgst#AEH)UBU%O0o~XVIo73 zG_Di`77`OGgFu*oM!sF8;Iy&4(3fJVZ)R%-6Iq9T+y3%_l71Dsw=rf%dK3P#S9{`XUCen$bT^WD1X&7 z+qI^4NzNv=i7Q$g#mSrex-RI>nY-5O%O7hSPrX!>wc@U>DlCZ>>HY@KmNyXzl{k56 ztr)D~kHa24iCBoFA!0CZFR<+#spyf(R5xv2%rdgI#?IWD8DkRJ%iDMNmYMwMT<=TQ z{}`nN-*GC(N(jrf1`FkrK(sPO`BoCvqVTp=;?X(D%mm-mcN>#Z^9Yjif{%L35&ms{4<~ZFTnH3 zJ>YdmE#YqoysHgS_UM&S_ZX_B1x0Qhq1S5=VV&A=Ry-A^A~`FqvfH%bZGPq(*vI>l z1GmSdp6!lQ5el}2WvQ%9@}RQvm$kduj?@jmfo(I@5tCIyZSauyUi}*k{zsQbESG6O zwOdanO00?L6#u8IXtD69>rsC5BmPEygCYGPzX|Gp{knhuC-~R5{@=g;iRJvAuKNGK zr}pm$@{5o7|JW`4_mL|qDguGDBk!S3KGqo1D9fWiu?LBG$fARa*8ZdT=KzZ@hlz2GM z3m~47I8+c7tYWPaoy1ap`yhUj(@B<)-`=Rc^FG6`#rdDt*qPlQ$nS17l>j98LW|Q{ zvNwf~t)OB+5&c~!7`!Jnm2*1OtxHu#PmTK%xc(>j{O7ZVkiRiY6DR9OX14#~*HQYr z(g2b4GsYhIKdc1KTUkIZ5feo#{SN{6>$h(}7fh%n$Npj0(W*uINt*F+lK&=<{{5Nb z&;_-xxoQ5eYVw)HfTXiB6K4I>)2bi=7r072mHk<<{_isp=K(6|p$?edpMK^OK5)TC z@BsXO*5TI&`C9>=FF$tY!=HX;|1)sGck~>hUkvyE-t7=ji)+x#IsEZwPJj!lNEEsL zu(A9Q-vg?!K(=k}k3X{(`$;y|7)^JESmVTPzCS-uca*xkJBM%cuiN5KdmifD19INH zBKUpU|Gnj(UvzIAvH$j0w6#)$16swW9!J{3-RUAi4I99Pr!*#gCC>Mc8|<0nHC1_O z`3gzyW{-luC@&A3T<#!As_qOvHpov1@Ag)1^W`58(i#+hauIygde0*a5zm9$A|Bre zk|!*v%2G-yl78OZuSNRL?TZWHyk+yNB}3(A{^PSggIHIejE^jTIGn{pAl}w~9-r~| z&%}RI4($6g@5L&uKkO+o6>vdwtZwEXc1#Tg*tDBRN#9NWaFfpb-~tS}Xa0Zqoy$HD mH$7$fEaDGOzkdetl44Wr`e0Fc#q>4sM?pqKx=7OK`TqegG5&@C literal 0 HcmV?d00001 diff --git a/mmp/a5/main.py b/mmp/a5/main.py index f05f48b..fcdad53 100644 --- a/mmp/a5/main.py +++ b/mmp/a5/main.py @@ -1,7 +1,16 @@ +import argparse import torch import torch.optim as optim +import torch.nn as nn +from torch.utils.data import DataLoader +from torch import Tensor +from tqdm import tqdm +import datetime from .model import MmpNet +from ..a4.anchor_grid import get_anchor_grid +from ..a4.dataset import get_dataloader +from ..a2.main import get_criterion_optimizer def step( @@ -11,11 +20,19 @@ def step( img_batch: torch.Tensor, lbl_batch: torch.Tensor, ) -> float: - """Performs one update step for the model + model.train() + optimizer.zero_grad() - @return: The loss for the specified batch. Return a float and not a PyTorch tensor - """ - raise NotImplementedError() + device = next(model.parameters()).device + img_batch = img_batch.to(device) + lbl_batch = lbl_batch.to(device) + + outputs = model(img_batch) + loss = criterion(outputs, lbl_batch) + loss.backward() + optimizer.step() + + return loss.item() def get_random_sampling_mask(labels: torch.Tensor, neg_ratio: float) -> torch.Tensor: @@ -26,13 +43,226 @@ def get_random_sampling_mask(labels: torch.Tensor, neg_ratio: float) -> torch.Te Hint: after computing the mask, check if the neg_ratio is fulfilled. @return: A tensor with the same shape as labels """ - assert labels.min() >= 0 and labels.max() <= 1 # remove this line if you want - raise NotImplementedError() + # Flatten for easier indexing + labels_flat = labels.view(-1) + pos_indices = (labels_flat == 1).nonzero(as_tuple=True)[0] + neg_indices = (labels_flat == 0).nonzero(as_tuple=True)[0] + + num_pos = pos_indices.numel() + num_neg = neg_indices.numel() + + num_neg_to_sample = min(int(neg_ratio * num_pos), num_neg) + + perm = torch.randperm(num_neg, device=labels.device) + sampled_neg_indices = neg_indices[perm[:num_neg_to_sample]] + + mask_flat = torch.zeros_like(labels_flat, dtype=torch.long) + mask_flat[pos_indices] = 1 + mask_flat[sampled_neg_indices] = 1 + + # Reshape to original shape + mask = mask_flat.view_as(labels) + return mask + + +def get_detection_metrics( + output: Tensor, labels: torch.Tensor, threshold: float +) -> tuple[float, float, float]: + """ + Returns precision, recall, f1 for the positive (human) class. + """ + with torch.no_grad(): + probs = torch.softmax(output, dim=-1)[..., 1] + + preds = probs >= threshold + + TP = ((preds == 1) & (labels == 1)).sum().item() + FP = ((preds == 1) & (labels == 0)).sum().item() + FN = ((preds == 0) & (labels == 1)).sum().item() + TN = ((preds == 0) & (labels == 0)).sum().item() + + precision = TP / (TP + FP) if (TP + FP) > 0 else 0.0 + neg_precision = TN / (TN + FN) if (TN + FN) > 0 else 0.0 + recall = TP / (TP + FN) if (TP + FN) > 0 else 0.0 + neg_recall = TN / (TN + FP) if (TN + FP) > 0 else 0.0 + f1 = ( + 2 * precision * recall / (precision + recall) + if (precision + recall) > 0 + else 0.0 + ) + + return precision, recall, f1, neg_precision, neg_recall + + +def evaluate( + model: MmpNet, + criterion, + dataloader: DataLoader, +) -> tuple[float, float, float, float]: + device = next(model.parameters()).device + model.eval() + total_loss = 0.0 + total_samples = 0 + all_outputs = [] + all_labels = [] + with torch.no_grad(): + for img_batch, lbl_batch, _ in dataloader: + img_batch = img_batch.to(device) + lbl_batch = lbl_batch.to(device) + + outputs = model(img_batch) + loss = criterion(outputs, lbl_batch) + batch_size = img_batch.size(0) + total_loss += loss.item() * batch_size + total_samples += batch_size + + all_outputs.append(outputs.cpu()) + all_labels.append(lbl_batch.cpu()) + avg_loss = total_loss / total_samples if total_samples > 0 else 0.0 + if all_outputs and all_labels: + outputs_cat = torch.cat(all_outputs) + labels_cat = torch.cat(all_labels) + precision, recall, f1, neg_precision, neg_recall = get_detection_metrics( + outputs_cat, labels_cat, threshold=0.5 + ) + else: + precision = recall = f1 = 0.0 + return avg_loss, precision, recall, f1, neg_precision, neg_recall + + +def train( + model: MmpNet, + loader: DataLoader, + criterion: nn.Module, + optimizer: optim.Optimizer, +): + model.train() + running_loss = 0.0 + total_samples = 0 + + progress_bar = tqdm(loader, desc="Training", unit="batch") + + for img_batch, lbl_batch, _ in progress_bar: + loss = step( + model=model, + criterion=criterion, + optimizer=optimizer, + img_batch=img_batch, + lbl_batch=lbl_batch, + ) + batch_size = img_batch.size(0) + running_loss += loss * batch_size + total_samples += batch_size + progress_bar.set_postfix( + {"loss": running_loss / total_samples if total_samples > 0 else 0.0} + ) + + epoch_loss = running_loss / total_samples if total_samples > 0 else 0.0 + progress_bar.close() + return epoch_loss + + +class NegativeMiningCriterion(nn.Module): + def __init__(self, neg_ratio=3.0, enable_negative_mining: bool = True): + super().__init__() + self.backbone = nn.CrossEntropyLoss(reduction="none") + self.neg_ratio = neg_ratio + self.enable_negative_mining = enable_negative_mining + + def forward(self, outputs, labels): + outputs_flat = outputs.view(-1, outputs.shape[-1]) + labels_flat = labels.view(-1).long() + unfiltered = self.backbone(outputs_flat, labels_flat) + assert unfiltered.shape == labels_flat.shape + + if not self.enable_negative_mining: + return unfiltered.mean() + + mask = get_random_sampling_mask(labels_flat, self.neg_ratio) + filtered_loss = unfiltered[mask == 1] + return filtered_loss.mean() def main(): - """Put your training code for exercises 5.2 and 5.3 here""" - raise NotImplementedError() + parser = argparse.ArgumentParser() + parser.add_argument( + "--tensorboard", + nargs="?", + const=True, + default=False, + help="Enable TensorBoard logging. If a label is provided, it will be used in the log directory name.", + ) + args = parser.parse_args() + + if args.tensorboard: + from torch.utils.tensorboard import SummaryWriter + + timestamp = datetime.datetime.now().strftime("%Y%m%d-%H%M%S") + if isinstance(args.tensorboard, str): + label = args.tensorboard + log_dir = f"runs/a5_mmpnet_{label}_{timestamp}" + else: + log_dir = f"runs/a5_mmpnet_{timestamp}" + writer = SummaryWriter(log_dir=log_dir) + else: + writer = None + + device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + model = MmpNet(num_aspect_ratios=8, num_widths=8).to(device) + + anchor_grid = get_anchor_grid( + anchor_widths=[8, 16, 32, 64, 96, 128, 160, 192], + aspect_ratios=[1 / 2, 2 / 3, 1, 4 / 3, 5 / 3, 2, 2.5, 3], + num_rows=7, + num_cols=7, + scale_factor=32, + ) + + dataloader_train = get_dataloader( + path_to_data=".data/mmp-public-3.2/train", + image_size=224, + batch_size=32, + num_workers=9, + is_test=False, + is_train=True, + anchor_grid=anchor_grid, + ) + dataloader_val = get_dataloader( + path_to_data=".data/mmp-public-3.2/val", + image_size=224, + batch_size=32, + num_workers=9, + is_test=False, + is_train=False, + anchor_grid=anchor_grid, + ) + _, optimizer = get_criterion_optimizer(model=model) + criterion = NegativeMiningCriterion(enable_negative_mining=True) + criterion_eval = NegativeMiningCriterion(enable_negative_mining=False) + num_epochs = 7 + + for epoch in range(num_epochs): + train_loss = train( + model=model, + loader=dataloader_train, + criterion=criterion, + optimizer=optimizer, + ) + avg_loss, precision, recall, f1, neg_precision, neg_recall = evaluate( + model=model, criterion=criterion_eval, dataloader=dataloader_val + ) + + if writer is not None: + writer.add_scalar("Loss/train_epoch", train_loss, epoch) + writer.add_scalar("Loss/eval_epoch", avg_loss, epoch) + writer.add_scalar("Acc/precision", precision, epoch) + writer.add_scalar("Acc/recall", recall, epoch) + writer.add_scalar("Acc/neg_precision", neg_precision, epoch) + writer.add_scalar("Acc/neg_recall", neg_recall, epoch) + writer.add_scalar("Acc/f1", f1, epoch) + + if writer is not None: + writer.close() if __name__ == "__main__": diff --git a/mmp/a5/model.py b/mmp/a5/model.py index fef4c95..a45f852 100644 --- a/mmp/a5/model.py +++ b/mmp/a5/model.py @@ -1,9 +1,40 @@ import torch +from torchvision import models +from torchvision.models import MobileNet_V2_Weights +from torch import nn class MmpNet(torch.nn.Module): - def __init__(self, num_widths: int, num_aspect_ratios: int): - raise NotImplementedError() + def __init__(self, num_widths: int, num_aspect_ratios: int, num_classes: int = 2): + super().__init__() + self.backbone = models.mobilenet_v2( + weights=MobileNet_V2_Weights.DEFAULT + ).features + self.num_widths = num_widths + self.num_aspect_ratios = num_aspect_ratios + self.num_classes = num_classes - def forward(self, x: torch.Tensor) -> torch.Tensor: - raise NotImplementedError() + with torch.no_grad(): + dummy = torch.zeros(1, 3, 224, 224) + backbone_out = self.backbone(dummy) + in_channels = backbone_out.shape[1] + + self.head = nn.Conv2d( + in_channels=in_channels, + kernel_size=3, + out_channels=self.get_required_output_channels(), + stride=1, + padding=1, + ) + + def get_required_output_channels(self): + return self.num_widths * self.num_aspect_ratios * self.num_classes + + def forward(self, x: torch.Tensor): + x = self.backbone(x) + x = self.head(x) + b, out_c, h, w = x.shape + x = x.view(b, self.num_widths, self.num_aspect_ratios, self.num_classes, h, w) + x = x.permute(0, 1, 2, 4, 5, 3).contiguous() + # Now: (batch, num_widths, num_aspect_ratios, h, w, num_classes) + return x