From dd0b3d9c3276366cd23106c8c0f9474085487056 Mon Sep 17 00:00:00 2001 From: Ziyue Xu Date: Fri, 21 Feb 2025 10:50:48 -0500 Subject: [PATCH] updates to PR comments --- examples/advanced/sklearn-kmeans/README.md | 3 +- .../sklearn-kmeans/figs/minibatch.png | Bin 20640 -> 15686 bytes .../sklearn-kmeans/kmeans_job_clientapi.py | 93 +--------------- .../advanced/sklearn-kmeans/prepare_data.sh | 1 + .../src/{kmeans_trainer.py => kmeans_fl.py} | 2 +- .../sklearn-kmeans/utils/prepare_data.py | 3 +- .../sklearn-kmeans/utils/split_data.py | 104 ++++++++++++++++++ .../code/kmeans_job.py | 93 +--------------- .../src/{kmeans_trainer.py => kmeans_fl.py} | 2 +- .../code/utils/split_data.py | 104 ++++++++++++++++++ .../convert_kmeans_to_fl.ipynb | 9 ++ 11 files changed, 227 insertions(+), 187 deletions(-) rename examples/advanced/sklearn-kmeans/src/{kmeans_trainer.py => kmeans_fl.py} (98%) create mode 100644 examples/advanced/sklearn-kmeans/utils/split_data.py rename examples/tutorials/self-paced-training/part-1_federated_learning_introduction/chapter-2_develop_federated_learning_applications/02.3_convert_machine_learning_to_federated_learning/02.3.2_convert_kmeans_to_federated_learning/code/src/{kmeans_trainer.py => kmeans_fl.py} (98%) create mode 100644 examples/tutorials/self-paced-training/part-1_federated_learning_introduction/chapter-2_develop_federated_learning_applications/02.3_convert_machine_learning_to_federated_learning/02.3.2_convert_kmeans_to_federated_learning/code/utils/split_data.py diff --git a/examples/advanced/sklearn-kmeans/README.md b/examples/advanced/sklearn-kmeans/README.md index 3aeeab7b5a..5372e9fc52 100644 --- a/examples/advanced/sklearn-kmeans/README.md +++ b/examples/advanced/sklearn-kmeans/README.md @@ -127,5 +127,4 @@ The resulting curve for `homogeneity_score` is It can be visualized using ```commandline tensorboard --logdir /tmp/nvflare/workspace/works/kmeans/sklearn_kmeans_uniform_3_clients -``` -Note that there will be certain amount of randomness in the results. \ No newline at end of file +``` \ No newline at end of file diff --git a/examples/advanced/sklearn-kmeans/figs/minibatch.png b/examples/advanced/sklearn-kmeans/figs/minibatch.png index 6c18b663a3b6481c28e00ad96a3a1160fec5fe93..48fefcefbbfdbbeea073f24035ac045e5d4bce5a 100644 GIT binary patch literal 15686 zcmaib2Rznoy#CYHMkOhup|S}XWmHCpva>}=gt97GMWu*BB^5$uXc*ZlB_w1ONf}v5 zLN@>FetzEfoc}rJbB>Sq^XhqiOguX9#B?5yoz<$UId<1yNCdk4E?0#25W$Bx-M zSvxrQ&nuLr(YR1z4_R&V}^yE z`DJBgjqL-^o+duVJ?1qpuz#%{$G+kGOtekGbVG-1CSpEl*3vZRkc*Uw!kLmXs4Uai!hP zZ0yU)EsS$y$GbM)x^*itHC3Z2(@s=c0qR6`0jJhWG8>`Q-2_>J2gIBbFTA2yrMsgxcx*zVq$gPxj`|DVus=V7PAIpr|Siy ztxMS>VzRwvJeyq_Or(cvwZ+{>PgUHP(vDFMs#oUps157Ro|7Tgvx6I8j**&A+~ zdLmg!ls+7Gzkajtz)$#O)Ay{5AA)SkHCwrhU-~Q>uG+WqL|a^_vpsZX0^dIelowm3>)X9R2O{hxJknGgM1UOAVh}UEiu69@}J}tB!B;&yUojrQ1{* zb6k?I`TL98*7HAbZ9IW)jgR-H4Oc63o+$QXdUlwfZO2hR&%t{w+eJmSrzS=k$Qy%$ znO(X+Dsp;FKHaKuKf$R|MxVuBan;rXRbR55*6X`&P*+mAyl^GQ$0@lS~gsi(iK2kk+P`{KGW@l|J)}-~UbYix-jx{4AeO?nEf! zs2t2?_nNu`ymA9!(dQjNKeT8oT<^ z-yXmq>Lu&GG(WWK%&%H9C{Ha*I0mxsV{We}uhY`fTFuGX(r$U@&Yj+S4h7ZNe&QbE zLvKga)zq|gb#vz~$_(*IAtXzuR_gSYg0_d-JsWh)2F@T8?X9(mnee6|81k_nFYp)h@rj zjD>MF^$zoH+_)#J+4bJS~`zUorWIfO?-T6iHV71nM`^Xp4@paDJeZag00G{H@pD582zNE?K&io1Ir<($nj?!3qai9v??woO){D#NWQ;yu95q z6F;A;YG~+jF;*=V%8NpDR z*tVso9K7$uN*a}AMdMURYG*k%+l0^@^uhgw6Q>wIV-=P zvMvkd!7j(6ZX;f&iGGmWw{rJy@0>IJg56c$)b5l&YMK6I+7(e%{I1#X=W&I}dJTiJzWtFvd%1vI=a%}fqY@=IYQGvmG49V`91(bPfN zvGz7Y0j5O$m#^osZVEscd}DDFb1%$V`|Mcx&}dsw@AoU5vKF^}n0DGV#u_}gI? z9sjU_gtqML?8_ZC*49;*m#-iCQ!d1Z=(k|Og3HI~gB#WWp7YvqWa;y(@0d3U3Tjxs z4XD0j=9!qBR$zz)iYo~K?hqASUaqUB7q^JxtUM17PtM?nU9;08vQ&pPGx7XKS54^y#T}Y_mS931?PhMUv zMAK@Ks=2-ODWcMI4Y8066%`fnfTcgb<{Nc%bdWz!etDs3n%2<8YJIu9<74sFRpRzv zBt1YJlu8!!^r%rCH(&8@Pkt&bqW6Gi*9O9*i;xu%cv|Q2e<8;5p=ZBcSmX@8XFc|( zLrHn@Rf;EjZ)akd&o&dr^zT?L)TV+l_pgEp`{BMO{hjwJy0&lBq{fMn8`0#a>!4}R zb9Va3dv723=CiV=j4!SB+8VxqKxXFICuzRw94ze}F6$pz&(6K&-x>JPyq*4hnqddF z$VY4j{wuGKIEUVtoe@7~*IX0C6QvSK-b_n+<HmJV%dQ=ah?h8A|^k%H!N_4*bqG z)%(-i1JtlY7YD2pt<62-z`b=V+kd<@S^?n{+n)@~Nov(`sRCOzhfAK0o}TLW`cEy4 z_HF9Bjo*e|acR~4|BiAPwYua(!(=^j+(v$-#KvxkiSa#}!`uqACxM92yf-B!z0b-8 zYy6ltAI+K(vN>@g(e!+uxE-c+U|@hv+$Pj>^ff21tnAmKc6v`(`9wrC0V-nBZ#D1Q z>{3G1#VG1%X+@WCoIfD(^=!;n&k4IVYu1Q!w2e#*BbpllNykBWq5!c5obJD0GEp63 z^q>BHMb7$TtO`jGsx|lS&|Um}a4h|JB?p03hX?;8!@Ww*($_nI|F1&g`( zG722yv0gIx%ZB`!>51{_k-{{fX7lM1_MYmP5=~ACMusrg9ott7jJZE+>RYnV=RRM^ z;Fx=G@wsa>&%(%lFQ!eb%dj|{Qa8=-ES;S6l#|m|@RR2%8YyIO9PN}3yHnYzEzBx5 zT*(s0yD@g3&`;08!h!xPg?=s56RmIjbu~2~f^ur+y7qlEoBp%*fnymD+wQZc-j#eQ z^eW`#<7<-HrEwt3zPa5oJN?|CRZPq`Ry5mPma~&*?)YhIRR4IV&2Uy;}DEy_ocp@TaHS zl!kj74z9TJx@)`SLcko}oCs`JQkLS?o+^7|pT!5>2Vl@wRJ;Tr?r_b$b?+WuWMw=E z=I1_Fm;SFNE$)NYZES5b&h(qI?>>77n4wfc4?jhR4(=ZCP0jyn2=-lucySlwVVJ@u zV&UuP=qPsX&(UW0!3sh!|NQxrZdJji9w}=AVw5xar~FoMaP&Z1N$;=R{`xQxOEDyM zF>UnVVxE9x2h;Sf&y{RZZM%{iu|q@7&7j}vUrmuny?XWPZDD1Y$Uaf4_YB9y$eyx3 zvpn*bxb)stfqivi8;~N|WT?*NCdvfLXN9GQzU@2NSsV7WV{CWttZ&PCpE? zoxCJV3tLPc>1Svsb41p@cTZjF^y4?^af^Z^9^wZ-+ps$<&uHv3a|{VK8VZ;H+6OsUw^y=k|JR>}oVIlVhS=g|yP zElUE+N<~^u)w~8Ca4pQ}oGQs?wa-7x9=@#7c{H=3e$@|KTiaWnM+#XuD-~sFTwfwQ z$4`ihi4j(}EN*`IV4u-6O;Y4Sz;76=A@4FKc)J1K8meJp6eXzFj?jO5zaDf&7(vF$@cPrgL2 z09_%Nz<7)|%~N}MBulw;g8%NI)k(@h97@W-HTkeFj^zS9U-NT!sJrzVVO{xcf&?^r zL~6-y&%mP}kDVnel=H!^GkTQ~GS`7ebOO`(HfyQhkiL+FG~Pg4CX9@{`PA#>j?mT6YBy(c?$Y|+;h;=&TSGaU|H!i8zp0Mrr8BSyjEq~gQFBt;yOSz>WllDLM6H)E=MNXpl}l)VCZX(vHr3E1EO=I%{QmGmB=(;%Qt=Up%k+Fdv#& zCP+|b7f<>;daGDB3JC>aA{f=^Z#NHJu#^Rw(ur5Ti%DRwJ{=AY?Y)ye612^BEd?f+ zKhTxg`KwZ*-{x&;iS?L%;q?OFE!-_TYQK@F?WY^ERhTC1`Wc8=fHm$UO#1<;5bX$NZy6f9*wO2<+r>kJ%7a?I{ zR_|E_j9(lwP0K1hoqMLg-Y%;-4yuLCn`>)8NfyQ~U@WKlvJ*tS@!2tdY%g>=I8kpe zI07`717P@s-MRk!^iay_M*`p*sm}EhK>vukh081#Gc8+~;o4_N_S@YZm*&ou|3shYqQ{d2vnpy zZ9Jg6cXY}#+gPxpz5N|Dn(wE!BL&uo*9Px^@Z?!&hICj{Mdd0o`z`S`iM?rsrbJjp z^1n|u6`K{AR+!xS^vf;%`XGAk!){XgTRTT4knmaj-7WrQC}OhruS#%h`Hcdi8pdEz(EXqnoEQ=OenXxs%3PQ=q?5<-+kq1 zYno3<+jpro8=-Q5grsAlCsLmhMv0Q42PD27+L$y4-e`xY#UB_ut~0vjXX$r6Y_ne26oOHHuxX~gm!EB zYqzfHR2%cA#XW5&)_n7flpg3H+ zv7$Esm81uE@7`5X-Xprx{!0S%8L15`2$eM!yo39(o7Zr1I+XSM#3eob>6(ja$nczU z)78;QNKeujB^}ybU1)J+@DKvw;q78_s9+;!RVGBcBdL^gr`mJ0dQy0!Q!q@w!KfwtlNevitoQCc)3 zJ%(&5^B*BP1O1}MKh#3Qf4y$Ge859* zyk9;R;!x0u{(y~lpCH7GQn<2(i2GpfsV`cW5WQBZZl>AatLpAC$VZnW@v}hLk z11%~(K2oDsE}(`{BPFjxHH^RU$q^sfiJzjkZr`4TQOzs#F%;frwU_3{OCj$uurpmp z$7kas{SH)+R#;BqRnDAw3x&g&X^rdYuj0xdt~YXPzo|@RUcMM_u2y<`X;&Vb6`B$m z9jVgn2>L0}M^Iq}dDUSGk#5oxWgebV#6p090yVHviO@9fW_sTs12C6*;KO`qOyg~w zGYORZLX-->?bK7MQ%rL8W{{hbgaw#K+F?N_QnZ`bG1Qyp(wkeA=Py)n7^#wUN2vC! zgi%8uN1CD4yBiz9E4|yOX-@x=+h35pEz(`fD9fHG1Rx{khV-||_dYgwu7ph&AK{in z$Fcic4=|KxrtY?p34ALOHf!-s=_2kWm zsCG4ty# zHr^ed;Wli;y=4m#J=i4d?{8S49Aa5sRwm{#?mXO8y$|swh?$vr+5Tl*tErbxevq4S zM+7AUao~T4c;yYf!)_U82Q2U|9wDL1up-)Cs(k^cp?V01J$nR*A$25DEQWzSB0|}9;Uk~)L z{5>(XWd7WTl{I#pye|qSt7g1fCz5)Ogh)nSJ}vzKD6l(zlQ-(aAS4jA9h_IE(<*re zopPHo)c2DazBy*M51d6I8mMjs?$g(lb&#(7NiGB=c9h>K?{-eY{*T*WV2)-7uO3Xf zR7<06cu%|l%NE|ttFW0eJAg}3hJ@;Q5NU)h|5h67&s__AkD0Hv97K$y3A1ab{D(~P z08Ap!G;LNXIlS{{bFRMsTA_MK=)+iU5(N&>&mzwKxO*cA038kXdbz`?XaI_mVZK|2 z+fdib>>W~gBLVmi+8uQcN#6?>%r_E#KP!}w4wrXmVU%WNl9K{5B#aF~3HGVQH>@@Y2RaDen;q*+nc5Mk5ZE2q?(MkaO zi&k>zAd?%Op6FMTPUoMzrEgmOHUGRz3Huq=`y77i8BVg63+JQj=FFWD=LLOujLK{> zY*1$`D=n>VFAZ@StdOAV-R0#qq_}~l+xPi-Nmm$cI@$HZ@uUZXiB#w-T87eLB2o5H zeG^0Ai8EZhQ3W;S96UEXFaODtL)gyBc@DEXH|zm*n6BB}RjjvZfkMe0L0v`F>o2d8 zb>REw`v4q@ah?!JK=(x8?XR6XDj&KUnXRTva~S0-wb!aapgC)?j;zm)xkV zefaS9dlgn?x{Szby<-@3<;lmwS)7yK79o}~8c@OZ-OxY@XYX)>8Ku6{Q|JvB=q>a! zoQ)x?-42iMSkaMx!cd0kPzKki0kUxb1RKhOTW!9dLN$B!Rt^K-{M ze;ntY&p3~IQ`j85X}dBVdWr+VW|2{ZQ`4#-PDRE{V-%mr0D4ky`ZDL`o@0^lvfB`%=(*>J#`1nWwO@1S}>t`aBisfC^SK zZCXNyIq&_i@anx35g1Jc3E9rN?4O=dlSq3Qz`pA?vew2G)GaZeU!0Eldajcl^S_Sj z857msqNn@@&lCXQ#7IDwP6+aNGf)lZ`iPa`N23o9&9Jpr`yj+$u?}7a5IkWS3Y-L7 zP}CL;V=fw?6Ixmo+r6E3AJ6ya$9qitoqgF>s^d`^n8z=YVLSNv%ApD%0Tpl|QpVX{~g3VVE{r(v>w znjT67#7c3x?QQAzQ(_#?9a>~%2H%U0!?+QO=^^$b_>}aGjqhJ5y-+{f`TEqnKdKA0 zGj&kaK;2Tu&@gFiu(S8WuKwC!Znbn!+qag$FVu*+uDpH-+~^`-89C%XLgLLDw!xa6 zYFghB6wJn9je_P+Dn$n}m{D6wgb@krO9x5?O0c(X-h2p;#+ucu#W>FNB*yasg5Uvx zk4hWme+JQ5Y(L!k8hEaglk_NY_g);e8%lSfdTIR{Vgf*mP6J^gP$ z;Hq}ipAb-!aRI`agtW%JLD@phuQM!+1C^8V>gQ2`Uy3UUykee-pn zE9z77>gq&Skf)hbpeAq9S<&`@!aFQ{RG6?K$qQ}7L41($SwHB@ZOss8lwiX z8EMdk&Hp8FJ{0T>TUIal#>z8dQMy!s|F%C4fy$cN@kI)bO?L>vF07uL# zz1~9?M5C5dQB(LM=oZmChr4T(p-bV?DHh|P?x=r)=x79$Bg@OjUeSVDxiAbJrFJi=~iBh;uz*T!CiD9BSTt5rfEgx z)*BX4+uTabYua6A^qbE(podZ(t5m7SpK?dSmrf;XG|^r#2COQcEDIzU^8T~^$hA$& zN1T_BrO<%A{6d^VVC<-u)|gUV%weX}$b6*tVI0|YId9d&2r ze}aDdj~`{7gLf&>z?VX6E@mRh0xdUDlBxH8U1MW@6w1H=us-S#&xKdb9c7{Vb@L~} zOcgQXK{Fv(s><-KkeatB4~!v2E4;gIgoI!jCPSj2Qt;y4)cw`1aKnIUnd?!55v>AR z3>3u9K;?`G*kOw=%&HJvjk}@IGHFnI$=>!={4=7y)y3Tu7%Oe6fz&y0pcL%GXj$lCk$=O%2Lnp`oFaP|mo98fSTA z0K7gt7yeT7N4tU$a-t#C1XI{JJL{8`D7>b?D|j7i$cS1)-tS%=V3?I2Xx2%Hd?Z?0 zSVRLLqkZLzDV+1Jvd`{dffvD!lAUOQzrMZnL9(Xj;gLUkrCcD(8UR5_ zFig~F(l&95ZZvnJ2B&HVaqd}IY(kUUsD(%qkg#hM!%Iqh=)+1gj7MFbXn2r2@sexr zn4_bk_l`85i6ru!h~-=B@2@UlhLuY8uBUdNcl~)-jfY?aHRkjfx=C(&(Z_m0UcUUc zfHs!YkIKk2yx01}eJS4U+YiB_R6S6&kQzWPX+>jVC-|Qf?Zw;*tNqUB@6YfO44xg| zlv_|u;WtEbjs+j<$F~vME&?V>!ACO;J5EW4-nhXPUvsW@Brz>b-_FjCRXaKZmpIBL zXipZFZBe;8s8m){7_^!>Vb_|N@oG>!NW zSPG(1=6h*=1E_-q*VEH;G)wsl_t67*NrSpXb1bk>Etn_Lh-u6WUaFISb|w#ZwF%Ux z7&Szku2OQGnJ6?fjf@Mww)~^qPS)^6C7C2m-yC%b(yO)ee6FOQBr{{MeyDzL=Q{Q| ze!P>DYuEMk_7cezJq>#%w!eI=ckK*FWkRdWMpgMy31Ro$Hq2H;DCq4m|fg=a5 z*8iE9GEB~RX53&lmE$Tdy&~`>d=}s{uy6|l!Zo$ENhCm}o4_KOWL4E`a0Z)`Qh!Mx zJ8=^R1YWRuVJrwBMaiNZt|O&m#~y81Q6l1X`-&8){6kWB+$bs)R1(uN`9p&EfsvJ^ zz)QW3J@G1L1aL}ZU7I>yB(&Wyv49Vhd^)G)ro3tKz;{Y|&7-W0Q${w?jzKaRZj7a&5DA1?lX&1wQ|5SyH(k=}I-yVV6(EQjk0 zJiDjrp$}>?uWtP50^*Z<J)kBE z7;5?cCAK1R;$gE=Koz_w>8HN$J(vG=Ysmqa`Saa|wNQbqE%fpt`u2S}pD}mY6r(H+ zQpo@{PD8GE>HT2f6hd}?tCOQ-s&g{j(yrD?i9hR79#qi=-7Y-+cY0|msh55X)TaLpib znzl^+I+HWn>QC~{-kZuCn!37mOeIJen!RSGu2&|S=DT&HOc`9+DZ1U|WI{rMwac81 zJV)oC1i5C}LR|xc&+VNPr~FEp_e!VF=N1>&B}^%H>g7hH6mC<)4KORmBQdjXe|OUj zuej&_eBW7k&Hqzj^jF@Xh5*uLC^<5%lGw!o0n4TeXGUhyJtkc3P%VLE)vej2=UWn| zes2iOuDopk>DQo7rpixLp(=L|1`w~&u-m@rCoBx}u_nZim94h>{7kVa%c0@of~BD# zit-j4^b8CVAs2V&4SuL;nVCLwolRVIDGUE0M7`>23q!R^GF)x0xkR#shqU*)#MdfR zmoiR%-hoPVbC_JB85~}d12?!$VQP4nMYo>oVMuiK^|RD|SYFL$Zavle zdTEeUZ9_vZcmUhZlVQOjA>z(mhtB1jdiEX{22X5lwOGDE@(@a+Jxy6@IAMb7sAKVI z3_6fu^|`H1SdP6QaCNYMLjJIJ-Y`juiyY^FiZpvp^dUwsJ9hv@Q>;Su$pov7mz(=d*I|*gN1c}%cn4MX@2^6I!WkTalogSDl0-+m+#Kd| za*zQ>bl_g7IeWU16z&2CQ3TF5R#jK;2*IAdT{8Ikhr*|WEn#)A5v^!F9FAC$yyL=Kmfl7iiLRO-;d`RRlwKnJnJgTDh>3aXe%?ASS_gH#Zx&on)?fu+}vBy)DjhjStlv z9EL#CQz6k(n`sB&oUa0cQPt#7m{K^^=RoWZdXEpEE*>Nszrpl;3YkRWsRSV~d}2-} z>_TFUeuC~2Sd-mQigK=2T+8m_@@NY-m;?(GRJ&ZIzzvD0Wo2|(rpJfzZQKIP7N5TA z@872QP9B~IB)8Z5i~z~}EKAk4F1LFA8QeS;272rmYzO$Zwo6Hk`CZ(OESDVof*trV z@?~TMvX)y-=L@7JJ2QqAzP0w&!lF2ciU3 z-;BCVAApkn5poDc6X5~skhpaf7gpBqCTE`oK70C5X8`ShqqUB_k0gk2kxE;1oQ2FK z_~P5#2dCezV-q&_7R>#*0})VrZ+b|7YU9Y5JDHYSAt5mk8q-JbWFd#zcwE7EoV8Ej zhpOLeBM~f4BxdlKPS`Iex)>~Qy`)SBBR3`hC%4L&^?hKagj>l5qCrwZ!XXfEXsO8M zI>4?Gk4Jr4gggh9k~Tt$Xv{yKMMyKu7SpS}{ovKcH9UwQKoDzq{z+{da6rneE?rf7 z=uTp*14|eB5F{a%bXa5WfXxd_GduqrY<0{=_6_~%o$al~u%G1PaDrRMEq%7E@)yna zE`5!4Bur8V$lQjimEm-dg_ppZSk;=2t|-*A`y=lT#o& zHKc9gSjtU~o*+=A;ylZZ&Q(=Qh@}TE24YrS&M9k54p;&yhKYPcj(dTPrqaEOqH&mf z+pl@&8VfvfJtvx4RD($*$5}Lw{_`zu=Ogw6x|-VObjQ8THUEO$nWuM`B!|QYN2;he z9gz*XGW0xh>HtntDJlD(K%~XF2~9h@6wmRRNY!AvrvXs}6yDZ7g_obdmYkXdSI~q; zM^4(ffa7VB<@e!y7fgjn(S*9I#DC#{iUd|i_i?$2yI9`bQx02;p}{&9v%vpuWew$H z9H%qIPJJtS`>ZvWGbZ2Q;!j07TPg|#ZCL9l*-@?PXUW6Irwv$%ow>CJroE-MOtVKV zyj5U4Lf<(XCKmd zowRvhJ$Cfyb`g=M>YSo7GBQ60xwib{$>y@)aQh_0; zWHVAbBzVYwZX17W>T`Yb=FNku`oPS+)T3#yh)06_{7a1g>miP!8HbX<1>};2V@8!0 x`bXx|meOzL-$DrOlN;pzqP=^`760EC4C^?@BcCn_q#x{3QPfaK-D`gF{{dB7oE!iE literal 20640 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

H3W8==;RP1YpRKaHDwn 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 List[int]: - split = [] - ratio_vec = get_split_ratios(site_num, split_method) - total = sum(ratio_vec) - left = n - for site in range(site_num - 1): - x = int(n * ratio_vec[site] / total) - left = left - x - split.append(x) - split.append(left) - return split - - -def assign_data_index_to_sites( - data_size: int, - valid_fraction: float, - num_sites: int, - split_method: SplitMethod = SplitMethod.UNIFORM, -) -> dict: - if valid_fraction > 1.0: - raise ValueError("validation percent should be less than or equal to 100% of the total data") - elif valid_fraction < 1.0: - valid_size = int(round(data_size * valid_fraction, 0)) - train_size = data_size - valid_size - else: - valid_size = data_size - train_size = data_size - - site_sizes = split_num_proportion(train_size, num_sites, split_method) - split_data_indices = { - "valid": {"start": 0, "end": valid_size}, - } - for site in range(num_sites): - site_id = site + 1 - if valid_fraction < 1.0: - idx_start = valid_size + sum(site_sizes[:site]) - idx_end = valid_size + sum(site_sizes[: site + 1]) - else: - idx_start = sum(site_sizes[:site]) - idx_end = sum(site_sizes[: site + 1]) - split_data_indices[site_id] = {"start": idx_start, "end": idx_end} - - return split_data_indices - - -def get_file_line_count(input_path: str) -> int: - count = 0 - with open(input_path, "r") as fp: - for i, _ in enumerate(fp): - count += 1 - return count - - -def split_data( - data_path: str, - num_clients: int, - valid_frac: float, - split_method: SplitMethod = SplitMethod.UNIFORM, -): - size_total_file = get_file_line_count(data_path) - site_indices = assign_data_index_to_sites(size_total_file, valid_frac, num_clients, split_method) - return site_indices - - def define_parser(): parser = argparse.ArgumentParser() parser.add_argument( @@ -171,7 +83,7 @@ def main(): split_mode = args.split_mode valid_frac = args.valid_frac job_name = f"sklearn_kmeans_{split_mode}_{num_clients}_clients" - train_script = "src/kmeans_trainer.py" + train_script = "src/kmeans_fl.py" # Set the output workspace and job directories workspace_dir = os.path.join(args.workspace_dir, job_name) @@ -209,7 +121,6 @@ def main(): data_path, num_clients, valid_frac, - SplitMethod(split_mode), ) for i in range(1, num_clients + 1): diff --git a/examples/advanced/sklearn-kmeans/prepare_data.sh b/examples/advanced/sklearn-kmeans/prepare_data.sh index a99701a077..a5100f8f52 100755 --- a/examples/advanced/sklearn-kmeans/prepare_data.sh +++ b/examples/advanced/sklearn-kmeans/prepare_data.sh @@ -8,6 +8,7 @@ if [ -f "$DATASET_PATH" ]; then else python3 "${script_dir}"/utils/prepare_data.py \ --dataset_name iris \ + --randomize 0 \ --out_path ${DATASET_PATH} echo "Data loaded and saved in ${DATASET_PATH}" fi diff --git a/examples/advanced/sklearn-kmeans/src/kmeans_trainer.py b/examples/advanced/sklearn-kmeans/src/kmeans_fl.py similarity index 98% rename from examples/advanced/sklearn-kmeans/src/kmeans_trainer.py rename to examples/advanced/sklearn-kmeans/src/kmeans_fl.py index e985fc4deb..b2af57b507 100644 --- a/examples/advanced/sklearn-kmeans/src/kmeans_trainer.py +++ b/examples/advanced/sklearn-kmeans/src/kmeans_fl.py @@ -1,4 +1,4 @@ -# Copyright (c) 2023, NVIDIA CORPORATION. All rights reserved. +# Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/examples/advanced/sklearn-kmeans/utils/prepare_data.py b/examples/advanced/sklearn-kmeans/utils/prepare_data.py index cfc12462d1..59388f7bee 100644 --- a/examples/advanced/sklearn-kmeans/utils/prepare_data.py +++ b/examples/advanced/sklearn-kmeans/utils/prepare_data.py @@ -43,7 +43,8 @@ def prepare_data( x = dataset.data y = dataset.target if randomize: - np.random.seed(0) + print("Randomizing data sequence") + idx_random = np.random.permutation(len(y)) x = x[idx_random, :] y = y[idx_random] diff --git a/examples/advanced/sklearn-kmeans/utils/split_data.py b/examples/advanced/sklearn-kmeans/utils/split_data.py new file mode 100644 index 0000000000..08f2a0617c --- /dev/null +++ b/examples/advanced/sklearn-kmeans/utils/split_data.py @@ -0,0 +1,104 @@ +# Copyright (c) 2023, NVIDIA CORPORATION. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from enum import Enum +from typing import List + +import numpy as np + + +class SplitMethod(Enum): + UNIFORM = "uniform" + LINEAR = "linear" + SQUARE = "square" + EXPONENTIAL = "exponential" + + +def get_split_ratios(site_num: int, split_method: SplitMethod): + if split_method == SplitMethod.UNIFORM: + ratio_vec = np.ones(site_num) + elif split_method == SplitMethod.LINEAR: + ratio_vec = np.linspace(1, site_num, num=site_num) + elif split_method == SplitMethod.SQUARE: + ratio_vec = np.square(np.linspace(1, site_num, num=site_num)) + elif split_method == SplitMethod.EXPONENTIAL: + ratio_vec = np.exp(np.linspace(1, site_num, num=site_num)) + else: + raise ValueError(f"Split method {split_method.name} not implemented!") + + return ratio_vec + + +def split_num_proportion(n, site_num, split_method: SplitMethod) -> List[int]: + split = [] + ratio_vec = get_split_ratios(site_num, split_method) + total = sum(ratio_vec) + left = n + for site in range(site_num - 1): + x = int(n * ratio_vec[site] / total) + left = left - x + split.append(x) + split.append(left) + return split + + +def assign_data_index_to_sites( + data_size: int, + valid_fraction: float, + num_sites: int, + split_method: SplitMethod = SplitMethod.UNIFORM, +) -> dict: + if valid_fraction > 1.0: + raise ValueError("validation percent should be less than or equal to 100% of the total data") + elif valid_fraction < 1.0: + valid_size = int(round(data_size * valid_fraction, 0)) + train_size = data_size - valid_size + else: + valid_size = data_size + train_size = data_size + + site_sizes = split_num_proportion(train_size, num_sites, split_method) + split_data_indices = { + "valid": {"start": 0, "end": valid_size}, + } + for site in range(num_sites): + site_id = site + 1 + if valid_fraction < 1.0: + idx_start = valid_size + sum(site_sizes[:site]) + idx_end = valid_size + sum(site_sizes[: site + 1]) + else: + idx_start = sum(site_sizes[:site]) + idx_end = sum(site_sizes[: site + 1]) + split_data_indices[site_id] = {"start": idx_start, "end": idx_end} + + return split_data_indices + + +def get_file_line_count(input_path: str) -> int: + count = 0 + with open(input_path, "r") as fp: + for i, _ in enumerate(fp): + count += 1 + return count + + +def split_data( + data_path: str, + num_clients: int, + valid_frac: float, + split_method: SplitMethod = SplitMethod.UNIFORM, +): + size_total_file = get_file_line_count(data_path) + site_indices = assign_data_index_to_sites(size_total_file, valid_frac, num_clients, split_method) + return site_indices diff --git a/examples/tutorials/self-paced-training/part-1_federated_learning_introduction/chapter-2_develop_federated_learning_applications/02.3_convert_machine_learning_to_federated_learning/02.3.2_convert_kmeans_to_federated_learning/code/kmeans_job.py b/examples/tutorials/self-paced-training/part-1_federated_learning_introduction/chapter-2_develop_federated_learning_applications/02.3_convert_machine_learning_to_federated_learning/02.3.2_convert_kmeans_to_federated_learning/code/kmeans_job.py index 0482de7efd..3437a4ed2d 100644 --- a/examples/tutorials/self-paced-training/part-1_federated_learning_introduction/chapter-2_develop_federated_learning_applications/02.3_convert_machine_learning_to_federated_learning/02.3.2_convert_kmeans_to_federated_learning/code/kmeans_job.py +++ b/examples/tutorials/self-paced-training/part-1_federated_learning_introduction/chapter-2_develop_federated_learning_applications/02.3_convert_machine_learning_to_federated_learning/02.3.2_convert_kmeans_to_federated_learning/code/kmeans_job.py @@ -14,11 +14,9 @@ import argparse import os -from enum import Enum -from typing import List -import numpy as np from src.kmeans_assembler import KMeansAssembler +from utils.split_data import split_data from nvflare import FedJob from nvflare.app_common.aggregators.collect_and_assemble_aggregator import CollectAndAssembleAggregator @@ -28,92 +26,6 @@ from nvflare.job_config.script_runner import ScriptRunner -class SplitMethod(Enum): - UNIFORM = "uniform" - LINEAR = "linear" - SQUARE = "square" - EXPONENTIAL = "exponential" - - -def get_split_ratios(site_num: int, split_method: SplitMethod): - if split_method == SplitMethod.UNIFORM: - ratio_vec = np.ones(site_num) - elif split_method == SplitMethod.LINEAR: - ratio_vec = np.linspace(1, site_num, num=site_num) - elif split_method == SplitMethod.SQUARE: - ratio_vec = np.square(np.linspace(1, site_num, num=site_num)) - elif split_method == SplitMethod.EXPONENTIAL: - ratio_vec = np.exp(np.linspace(1, site_num, num=site_num)) - else: - raise ValueError(f"Split method {split_method.name} not implemented!") - - return ratio_vec - - -def split_num_proportion(n, site_num, split_method: SplitMethod) -> List[int]: - split = [] - ratio_vec = get_split_ratios(site_num, split_method) - total = sum(ratio_vec) - left = n - for site in range(site_num - 1): - x = int(n * ratio_vec[site] / total) - left = left - x - split.append(x) - split.append(left) - return split - - -def assign_data_index_to_sites( - data_size: int, - valid_fraction: float, - num_sites: int, - split_method: SplitMethod = SplitMethod.UNIFORM, -) -> dict: - if valid_fraction > 1.0: - raise ValueError("validation percent should be less than or equal to 100% of the total data") - elif valid_fraction < 1.0: - valid_size = int(round(data_size * valid_fraction, 0)) - train_size = data_size - valid_size - else: - valid_size = data_size - train_size = data_size - - site_sizes = split_num_proportion(train_size, num_sites, split_method) - split_data_indices = { - "valid": {"start": 0, "end": valid_size}, - } - for site in range(num_sites): - site_id = site + 1 - if valid_fraction < 1.0: - idx_start = valid_size + sum(site_sizes[:site]) - idx_end = valid_size + sum(site_sizes[: site + 1]) - else: - idx_start = sum(site_sizes[:site]) - idx_end = sum(site_sizes[: site + 1]) - split_data_indices[site_id] = {"start": idx_start, "end": idx_end} - - return split_data_indices - - -def get_file_line_count(input_path: str) -> int: - count = 0 - with open(input_path, "r") as fp: - for i, _ in enumerate(fp): - count += 1 - return count - - -def split_data( - data_path: str, - num_clients: int, - valid_frac: float, - split_method: SplitMethod = SplitMethod.UNIFORM, -): - size_total_file = get_file_line_count(data_path) - site_indices = assign_data_index_to_sites(size_total_file, valid_frac, num_clients, split_method) - return site_indices - - def define_parser(): parser = argparse.ArgumentParser() parser.add_argument( @@ -171,7 +83,7 @@ def main(): split_mode = args.split_mode valid_frac = args.valid_frac job_name = f"sklearn_kmeans_{split_mode}_{num_clients}_clients" - train_script = "src/kmeans_trainer.py" + train_script = "src/kmeans_fl.py" # Set the output workspace and job directories workspace_dir = os.path.join(args.workspace_dir, job_name) @@ -209,7 +121,6 @@ def main(): data_path, num_clients, valid_frac, - SplitMethod(split_mode), ) for i in range(1, num_clients + 1): diff --git a/examples/tutorials/self-paced-training/part-1_federated_learning_introduction/chapter-2_develop_federated_learning_applications/02.3_convert_machine_learning_to_federated_learning/02.3.2_convert_kmeans_to_federated_learning/code/src/kmeans_trainer.py b/examples/tutorials/self-paced-training/part-1_federated_learning_introduction/chapter-2_develop_federated_learning_applications/02.3_convert_machine_learning_to_federated_learning/02.3.2_convert_kmeans_to_federated_learning/code/src/kmeans_fl.py similarity index 98% rename from examples/tutorials/self-paced-training/part-1_federated_learning_introduction/chapter-2_develop_federated_learning_applications/02.3_convert_machine_learning_to_federated_learning/02.3.2_convert_kmeans_to_federated_learning/code/src/kmeans_trainer.py rename to examples/tutorials/self-paced-training/part-1_federated_learning_introduction/chapter-2_develop_federated_learning_applications/02.3_convert_machine_learning_to_federated_learning/02.3.2_convert_kmeans_to_federated_learning/code/src/kmeans_fl.py index e985fc4deb..b2af57b507 100644 --- a/examples/tutorials/self-paced-training/part-1_federated_learning_introduction/chapter-2_develop_federated_learning_applications/02.3_convert_machine_learning_to_federated_learning/02.3.2_convert_kmeans_to_federated_learning/code/src/kmeans_trainer.py +++ b/examples/tutorials/self-paced-training/part-1_federated_learning_introduction/chapter-2_develop_federated_learning_applications/02.3_convert_machine_learning_to_federated_learning/02.3.2_convert_kmeans_to_federated_learning/code/src/kmeans_fl.py @@ -1,4 +1,4 @@ -# Copyright (c) 2023, NVIDIA CORPORATION. All rights reserved. +# Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/examples/tutorials/self-paced-training/part-1_federated_learning_introduction/chapter-2_develop_federated_learning_applications/02.3_convert_machine_learning_to_federated_learning/02.3.2_convert_kmeans_to_federated_learning/code/utils/split_data.py b/examples/tutorials/self-paced-training/part-1_federated_learning_introduction/chapter-2_develop_federated_learning_applications/02.3_convert_machine_learning_to_federated_learning/02.3.2_convert_kmeans_to_federated_learning/code/utils/split_data.py new file mode 100644 index 0000000000..08f2a0617c --- /dev/null +++ b/examples/tutorials/self-paced-training/part-1_federated_learning_introduction/chapter-2_develop_federated_learning_applications/02.3_convert_machine_learning_to_federated_learning/02.3.2_convert_kmeans_to_federated_learning/code/utils/split_data.py @@ -0,0 +1,104 @@ +# Copyright (c) 2023, NVIDIA CORPORATION. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from enum import Enum +from typing import List + +import numpy as np + + +class SplitMethod(Enum): + UNIFORM = "uniform" + LINEAR = "linear" + SQUARE = "square" + EXPONENTIAL = "exponential" + + +def get_split_ratios(site_num: int, split_method: SplitMethod): + if split_method == SplitMethod.UNIFORM: + ratio_vec = np.ones(site_num) + elif split_method == SplitMethod.LINEAR: + ratio_vec = np.linspace(1, site_num, num=site_num) + elif split_method == SplitMethod.SQUARE: + ratio_vec = np.square(np.linspace(1, site_num, num=site_num)) + elif split_method == SplitMethod.EXPONENTIAL: + ratio_vec = np.exp(np.linspace(1, site_num, num=site_num)) + else: + raise ValueError(f"Split method {split_method.name} not implemented!") + + return ratio_vec + + +def split_num_proportion(n, site_num, split_method: SplitMethod) -> List[int]: + split = [] + ratio_vec = get_split_ratios(site_num, split_method) + total = sum(ratio_vec) + left = n + for site in range(site_num - 1): + x = int(n * ratio_vec[site] / total) + left = left - x + split.append(x) + split.append(left) + return split + + +def assign_data_index_to_sites( + data_size: int, + valid_fraction: float, + num_sites: int, + split_method: SplitMethod = SplitMethod.UNIFORM, +) -> dict: + if valid_fraction > 1.0: + raise ValueError("validation percent should be less than or equal to 100% of the total data") + elif valid_fraction < 1.0: + valid_size = int(round(data_size * valid_fraction, 0)) + train_size = data_size - valid_size + else: + valid_size = data_size + train_size = data_size + + site_sizes = split_num_proportion(train_size, num_sites, split_method) + split_data_indices = { + "valid": {"start": 0, "end": valid_size}, + } + for site in range(num_sites): + site_id = site + 1 + if valid_fraction < 1.0: + idx_start = valid_size + sum(site_sizes[:site]) + idx_end = valid_size + sum(site_sizes[: site + 1]) + else: + idx_start = sum(site_sizes[:site]) + idx_end = sum(site_sizes[: site + 1]) + split_data_indices[site_id] = {"start": idx_start, "end": idx_end} + + return split_data_indices + + +def get_file_line_count(input_path: str) -> int: + count = 0 + with open(input_path, "r") as fp: + for i, _ in enumerate(fp): + count += 1 + return count + + +def split_data( + data_path: str, + num_clients: int, + valid_frac: float, + split_method: SplitMethod = SplitMethod.UNIFORM, +): + size_total_file = get_file_line_count(data_path) + site_indices = assign_data_index_to_sites(size_total_file, valid_frac, num_clients, split_method) + return site_indices diff --git a/examples/tutorials/self-paced-training/part-1_federated_learning_introduction/chapter-2_develop_federated_learning_applications/02.3_convert_machine_learning_to_federated_learning/02.3.2_convert_kmeans_to_federated_learning/convert_kmeans_to_fl.ipynb b/examples/tutorials/self-paced-training/part-1_federated_learning_introduction/chapter-2_develop_federated_learning_applications/02.3_convert_machine_learning_to_federated_learning/02.3.2_convert_kmeans_to_federated_learning/convert_kmeans_to_fl.ipynb index fc44aab9a2..9a8cb7a548 100644 --- a/examples/tutorials/self-paced-training/part-1_federated_learning_introduction/chapter-2_develop_federated_learning_applications/02.3_convert_machine_learning_to_federated_learning/02.3.2_convert_kmeans_to_federated_learning/convert_kmeans_to_fl.ipynb +++ b/examples/tutorials/self-paced-training/part-1_federated_learning_introduction/chapter-2_develop_federated_learning_applications/02.3_convert_machine_learning_to_federated_learning/02.3.2_convert_kmeans_to_federated_learning/convert_kmeans_to_fl.ipynb @@ -120,6 +120,7 @@ "metadata": {}, "outputs": [], "source": [ + "%cd code\n", "! python kmeans_job.py --num_clients 3 --split_mode uniform" ] }, @@ -150,6 +151,14 @@ "%load_ext tensorboard\n", "%tensorboard --logdir /tmp/nvflare/workspace/works/kmeans/sklearn_kmeans_uniform_3_clients" ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "88a470ec-c411-4f4f-b5d4-9bb66377583d", + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": {