From 79245716a402e8ce1b74364c331b4db41770683b Mon Sep 17 00:00:00 2001 From: yzgu Date: Mon, 30 Mar 2026 17:56:56 +0800 Subject: [PATCH] doubao-dogfood recheck --- .coverage | Bin 0 -> 53248 bytes .../AnomalyDetection.cpython-314.pyc | Bin 0 -> 6154 bytes K-Means/__pycache__/K-Menas.cpython-314.pyc | Bin 0 -> 6476 bytes .../LinearRegression.cpython-314.pyc | Bin 0 -> 5740 bytes .../LogisticRegression.cpython-314.pyc | Bin 0 -> 7895 bytes .../__pycache__/NeuralNetwork.cpython-314.pyc | Bin 0 -> 16649 bytes PCA/__pycache__/PCA.cpython-314.pyc | Bin 0 -> 7507 bytes .../SVM_scikit-learn.cpython-314.pyc | Bin 0 -> 4744 bytes tests/__init__.py | 4 + tests/__pycache__/__init__.cpython-314.pyc | Bin 0 -> 261 bytes .../conftest.cpython-314-pytest-9.0.2.pyc | Bin 0 -> 9366 bytes ...aly_detection.cpython-314-pytest-9.0.2.pyc | Bin 0 -> 50470 bytes .../test_kmeans.cpython-314-pytest-9.0.2.pyc | Bin 0 -> 47310 bytes ...ar_regression.cpython-314-pytest-9.0.2.pyc | Bin 0 -> 39893 bytes ...ic_regression.cpython-314-pytest-9.0.2.pyc | Bin 0 -> 53933 bytes ...eural_network.cpython-314-pytest-9.0.2.pyc | Bin 0 -> 54308 bytes .../test_pca.cpython-314-pytest-9.0.2.pyc | Bin 0 -> 47936 bytes .../test_svm.cpython-314-pytest-9.0.2.pyc | Bin 0 -> 40548 bytes tests/conftest.py | 155 +++++++ tests/test_anomaly_detection.py | 301 ++++++++++++++ tests/test_kmeans.py | 319 +++++++++++++++ tests/test_linear_regression.py | 241 +++++++++++ tests/test_logistic_regression.py | 315 ++++++++++++++ tests/test_neural_network.py | 386 ++++++++++++++++++ tests/test_pca.py | 342 ++++++++++++++++ tests/test_svm.py | 286 +++++++++++++ 26 files changed, 2349 insertions(+) create mode 100644 .coverage create mode 100644 AnomalyDetection/__pycache__/AnomalyDetection.cpython-314.pyc create mode 100644 K-Means/__pycache__/K-Menas.cpython-314.pyc create mode 100644 LinearRegression/__pycache__/LinearRegression.cpython-314.pyc create mode 100644 LogisticRegression/__pycache__/LogisticRegression.cpython-314.pyc create mode 100644 NeuralNetwok/__pycache__/NeuralNetwork.cpython-314.pyc create mode 100644 PCA/__pycache__/PCA.cpython-314.pyc create mode 100644 SVM/__pycache__/SVM_scikit-learn.cpython-314.pyc create mode 100644 tests/__init__.py create mode 100644 tests/__pycache__/__init__.cpython-314.pyc create mode 100644 tests/__pycache__/conftest.cpython-314-pytest-9.0.2.pyc create mode 100644 tests/__pycache__/test_anomaly_detection.cpython-314-pytest-9.0.2.pyc create mode 100644 tests/__pycache__/test_kmeans.cpython-314-pytest-9.0.2.pyc create mode 100644 tests/__pycache__/test_linear_regression.cpython-314-pytest-9.0.2.pyc create mode 100644 tests/__pycache__/test_logistic_regression.cpython-314-pytest-9.0.2.pyc create mode 100644 tests/__pycache__/test_neural_network.cpython-314-pytest-9.0.2.pyc create mode 100644 tests/__pycache__/test_pca.cpython-314-pytest-9.0.2.pyc create mode 100644 tests/__pycache__/test_svm.cpython-314-pytest-9.0.2.pyc create mode 100644 tests/conftest.py create mode 100644 tests/test_anomaly_detection.py create mode 100644 tests/test_kmeans.py create mode 100644 tests/test_linear_regression.py create mode 100644 tests/test_logistic_regression.py create mode 100644 tests/test_neural_network.py create mode 100644 tests/test_pca.py create mode 100644 tests/test_svm.py diff --git a/.coverage b/.coverage new file mode 100644 index 0000000000000000000000000000000000000000..63080de9fd221f219a18555f16cd724ccdd74ab0 GIT binary patch literal 53248 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 literal 0 HcmV?d00001 diff --git a/AnomalyDetection/__pycache__/AnomalyDetection.cpython-314.pyc b/AnomalyDetection/__pycache__/AnomalyDetection.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0743a781bacfa11f0387103f701c18fdf2da2029 GIT binary patch literal 6154 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 T<6oKjzc5?AHX&w{*42Ljx}I<5 literal 0 HcmV?d00001 diff --git a/K-Means/__pycache__/K-Menas.cpython-314.pyc b/K-Means/__pycache__/K-Menas.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..898ddcf05d1f28cc50627862e586ac8fc7d35b49 GIT binary patch literal 6476 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 a0OuvFFMIr&{ej=I&F`~#h{85gdHxFRA6r!b literal 0 HcmV?d00001 diff --git a/NeuralNetwok/__pycache__/NeuralNetwork.cpython-314.pyc b/NeuralNetwok/__pycache__/NeuralNetwork.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fb4f296762064c16368161203b47fffe601af6e1 GIT binary patch literal 16649 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 bs`PiX>B++oM5-fPwBaLq1uP6*Cb9Y7^Xz!Q literal 0 HcmV?d00001 diff --git a/PCA/__pycache__/PCA.cpython-314.pyc b/PCA/__pycache__/PCA.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9514b7e1b66da4ba6b1567d045c78f3d08361787 GIT binary patch literal 7507 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 I5etwD09=kt%>V!Z literal 0 HcmV?d00001 diff --git a/tests/__pycache__/conftest.cpython-314-pytest-9.0.2.pyc b/tests/__pycache__/conftest.cpython-314-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f62dd43106896e45aca473f6652e7751a4037a24 GIT binary patch literal 9366 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 SAA;+5*7@gN^C4jG;Qs@T;05Ub literal 0 HcmV?d00001 diff --git a/tests/__pycache__/test_kmeans.cpython-314-pytest-9.0.2.pyc b/tests/__pycache__/test_kmeans.cpython-314-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..35f279b8852a74a07a022ad8b39042ddc6546442 GIT binary patch literal 47310 zcmeHw3ve9AdFCvzkJ-gy7Y~x)8vp@`HQ102d_S2`}f|C6`iNT|$Ye zFr`#E_x;^7Jw1Z~b|I0G;>r~8Z%6s3nwtV1;u zx0B_Ha$ZR&d8J!P_>TDUW$d^7uz#eiUO61d2kZHjP(H-2sCkwBhVv2pmK~`$9L+}$ zSLQ3taEXU|&g+E%JtnWt}? ztP=L`@!CZB&N6;({hFh(EMIrLu49{B)=G8kuu}GK!oRqCCIUP0_UN$gnqN83-S+Tu zGCaQ08lU{?<10Jvm#^`3$3t`Tj<2?A)howWckG{AB43aEhduHekpGBBek1Zf=#jrJ z5v)`4O-q$VB@wDq5~_I0W|kL5UPR=rXL%LKi;BD!mRE_qn8@3}^5V#=5_ub0UN!P+ zL|!Y)t3}=tk#`@n(RsUDvVGJa1AOl&+w4vCG0K%8s(2()$5LS*_G7T@}{#W56=j2yHMkduyrGF=e5< zjFhcWnu_-Ln9}C6`pj!&l?Te#C|O^t&j_9DPv`TQ!PcNrHkdO4Y=kv>G9!{q_m5;A z)wCha2p>C}Gn1iHX&qHGR4&iQfzO^QVReSvj4-)YAhrJ@Dp?nv|YC znMvWcsr(S14O%7czXJdI#{teOx5@&6`kV3Ui)X%YW~}^+&(6e~r{c|pc*~^P@{c#8 z%f1$#tXTK=N;wKu)Lhyz7M`hCcdcUGZ0)j_9(eJAuRL@u+F;eL9SeN%xohg$TOpKw zTVIOyf4-?j`PPmDyUKsCs*d1JA7BqIh{Kv(w>KjzjSd}G@;(revhH>H@@2{pNC`e& zf1(U9kSNawlS)36@Z+Nh7*YBrK0K7}{a`wu?lBPiiE9j!=s}OT@Z=qWt|kfBwaA#A^u<>elL`MtSb+HpAZw{7%UoZ^t^rxN8bMcb2F*$gfeI>Ph$ar?gB?>lqrzrL~L^#pTqAk!*h+eKCUlLnxse%TxNP zp^^SxRy5VuKXfA9pBhXLWQ;H)>C}P|JHWWS?9d=*I%-qk4hPcU^v>k^(}U?eu4v0J zg*Br0trdn}&-9-(syKg;qCrjN()m+HWj|mX?+V^U$Jnzm;r^934sNBRxGCT{kw!biQr=f=y^hKLH$cUioo+ z_2il@g?QVje|A;##f9rzveH* z*H5bJXX5K;0Hwczy8eZ8BIBClOk|_xShF}oA?ZwHqtI)9afU+H*$s95(uNq>iRBK>wdO9i@>;Y>2P_j5qRrv)ND%gD2kH#!9SWj6##FPIDg z5r;aH5+e3>*|j8g$EpXmB_I%SS<$jVtE>w#szBoYr0Qax!d+qOK2poxq|J*g1s?F_ z@UJ~H&&c^4;~VHMVQe2Oj*R+d<5d?=UpRef_)F&|)iq-W7`X818FdZ+E2wKmf#K-vx}BV+qN0qPxTdag zG`V){$w?L9!gDj~TK-p1*HV*ocHK@+Qz=nKPT-0LwWM-cIohfkYKl?HRLY2?QUgQ1 zBmEhqqp1|>KZu6nES^f8%xZeRA7b_3P%5R74o!oiK{1x5Qs4>l*`5?h?b#C}7+%z< zPNjPKK@W*L=*fVHr$F~BrA$;K?)PE#QnWa=M}TI4z69VOpT_{dRpt+@x)b#W>Tf9k ztk>tueF_an1YeU&y{q1doBLlg!4Pf20?3o6}Q|7ZLvi;foS#YWPNQQV(t|4mJ z(bm3g6DDm?%cS+8!5;I?vsodC_cg{n9rNYNy32F2_CPLePNYVt^ShO{vhF~4fI+0& zg#IDt@3Q8x(%EcD_qjk9-HRPIsm%qG!43}@-N_6LYbBI!eT?LU&ye; zEQG);j4_g@z5&)uB6%8V@wL4GM>SH{YYzf|G7(Fp(F-z`ob;G)OnIo0E`NUV7G>nk zSf*zOa=^T{l9~$_M0yS1<3=@LXB($I=w$zdT40i zRB$7sg4Ai;l6@t1%c-G}d~PHUnTn|5f%(2BE?Jb&1kq*vRe26Y<#S9P9}zjS=R;tThpn4UUZ`%d29r4SFSKAJjQ zyB;wHt^-de>X|y+V&&(%wNx0Wq&~ywimtTTI(XSWo~`+p7g>+>e>4IL`AYP5lzMt^8E6i z$x*A1TLTUaZM=)5FLp@xU2f&szxW#BRvXqYs9TIvw_F&%D*hv&bl*$Fz%A8kq*c&h z4OFy`5+EW+>b!l1A3DBv?J1nUeN56AwnOCODnm+{Izy-kc^(qB8nF%9yU4Q}nKp4R zF0?R_upLUkRwxuTvTJY>N>4&J-OFyegTdWLNpRdR1V_|r*v|z=+!u_Q??FqZ+=ojK z^0uM0yj*G1W$j722cdDJ9G#KKa1&iOX#fPLT7?5G3l3vK|4l($SxnMElA0# zI)~nFCs$SSfotF9ICEax#ppCI!1aXf+Z|_nbdpJA8cwP}PRCmpqT|n``?I|%(D*F$ zg2p57h>$zlAl^ssWIXS?rQ~Dz6n~p(~C?#T9h? z1x=ixoOOnNj6NeSpt(uwaYQCha1KPG-bx4~&Nz^@j03T;k+=JRDPcsqBako@f|7*c zTTJb?G7Ep?$|T7|xM@Jzp}w)hwxW`n(kX@HER`fl8E%!N;Ay<%^LbU0b}dhi#g>ym znTwSbyn~A#&8Lzi9R)k2mOV5Ez{5pUlHG3gqiYkTf5f3=20qh zDQT5%UmH`@GF#(f=aaXh_Ge(ExT5wAnijUA)>CTL>QTlboS>GL^^Fv@VK2fd(M%_i zyliS_8GUq-#(AYtn!&=9Wzw((1<9TvP0RGa9H{HrzQF-Vzs6(lh>*=~=pg!{Kgy`t zUqxdV4}anCrQxy0ODD&)@$F-$CaNbMnFviT-ByUUyTw)Q%Jzv< zs~M0>&&4QE0GEE(mFPBpG7*uFnHQ3pvo% zenBeU;gTvmu_t(X6_Q}Ivs01;$+PM1OU1&#Cgc|G*(7K|hqo*t>Xj%J3;!b*C5(_~ z(<0@__f9ek!t<%v$OxLBP?M2FxM+Dn$z6Uk6^oF{ymXF`!g4__k!fcW5lCZ|eSG@* zA9hJ(=N^+KFgR715*WExaG7X935;xV5WwaNT^fBdOx)VWHJh;DF*6-WTc()~(w3G% zu~x3C)NG;1h4e`t(&!1aQ`D;D60vWth|@lgVsg_$0u3Bc`bbG?>OhSTz9UNDYUK%Z zR{tLWrh3xSOzIN{$3HRNRam`!9vm-3n5OY+V9@dE7yI!8sYPun#G3$TEQ|_tdZ|yw zmpm=yU9(Hvpr+;)Mh?jnCW1zJ>kQ>dx}Xb4PFnXAMUlY$7Z5G}Dw2xAP>L|J%S&N7)kAe z1Re!2g4~16@DHT5r;VWgbT*egX_V_{btC!=eAwY<2Bi#CJxQuENMMA(CkZ@9;1Gew z2pk5`@qJo`^2aHYz&5%N#T8~KpAA$z|MAa!d?rvg6{s64pIN?fYWc>A$EKF=n0#<> zGEi3t482(v47A-YSAsd;ElS?1Qi9P^D($u_?JluWD9~(GLh@EkNp}j^m5{s@TU@1o zfMb3$Lvn0*dN4fVpz)*QHYqn94g%d?PV;b`hdVc5C=tl3Ff4}?K@jJVv6N=}L3kp< z$w3oVWrxLxT$UKmn+Fmt)neoogwd^ZZujzzcWPIxFeF%y>CWuvY=%AteV`*3NCv!G z8SY7zdF4B`5ff2_{ROJSSWV(Gx%<~vq-4*py{6Nwi?TH{%4YRSu#vjc;C;X6SI(6bMe3%| z5B;|Q&MULFr8`!2F?%696RV$!)sOqe+rQoSjlPNWH$PE`wZpc(Wc_%0B3)RrZ8SU^ zUw-kq3(rle?c-^y(D+t5-8vO-pHa=fg4#X`?=L%pr$^6i54m%TkGQjLu7IxSu7EIa3+P2T#x^Ap_=qk1mvxsx%}g8(J34D5#O6=| zIFTY8wWtpPxNDhc5YccUs}(4y9jaHjla#wlvw*3fHjFwX&*1pU2Viju^=5jq0|*}E zdFvlO+UjS87%5LX92!q~sK$?C3ul5U;@IIrAvsKAED8^=zLYZKeYPhF|GlDMKyF5Op%-Vb&R zv-#8)PJQ`Pg;?`wId|odrtnR*alCcnz&AHvslC#3C35w^b@h;)x8zFxO7v=aMm;=N zTSOzqD5t@?!+b4##eJ$k$NZ?^I-C>24wX#!EDyfkKkn1to_B;;{;e2VytKyMyVkC;~e^qH)#y@Y!lZ4I%RV+77+v>~0N(Q-nl zG>gDc;`Q^n6j~TT8kGoYfZ8;JI0cGsq}gv85cv#inGY3O!Z8n)H6C2t{BqVN)>bEK zK$nJJI{o75vEg4j_ubm**cK-uyt#bktD#pygEA1Ind3AV3Ju(+r z3BK!7jPH`(p|>zQ2U#t~P6jlkmWrm}d`>glbqqh1^m*kwQ4{zta^{B&AX(0S$oMZ? zVD+4%i7<0Xk2qb@p`R4m%nqTcw7cf%wPW9@$}VZ8@HGhZzQN2iy>{$+HCAc+SFUI3 zCySLA`pkr1vb}k?EqJx9PX0?}{MXz-1LBL=A+=1AK&}J57P>$e|HT3gi2rJ>;1q#5 zH!xmBa9-M%(4O{-07qNPZMwlz%&qVuu0hS_A|5UW!i6~}*(l>AGG0uR_7&XGQwy+u zJ_L~&$mP#keu~Bei^C6&%FocfzYnkg@tIt@wGiEAlby4LSfdBodCxf~t{i|%sE0y8 z`zoGMBMpc48i8*UI7Hwv0*3))5&r^Z66mH24FF8Ue+vTc_P1iez!BeDRl&fAe0Qpl zaZ3T|)U&;VyPa!Oz}^wkSH(zKDhp)>-@X+*#!Ij4lbWCD5Ma zlEpBrhcqZ5Heqh&oKFKoY-Ug=c~whtAoqCQJI}1=BH#iwNq47z8Nk8sEZH#OD=gXa zeh}~EPj3nO$@uO6j_D(5s^# zeZwy`DxDW>o_zRQ_<)3lWB3m&j^fd>KJ*m6o;Hu*`MD;lnbxVMbG@W>qE#Klr+U}8 z9^oBj^|3^*N7Tb-j-bax;2!i%poT^EP1Hl*0xm0X6st;Lt%gPQw$b&@Jo!g^wpbjM z=rsgo$2<2#L38V)_bRNq_FVC-XM2rf?Sy|l&s9s3X8mlQNEV?O{1ipp)QjnHWpd{+^G>y=(N-`Cv5HmKqmgE_Fbw- zA_QEX2hVCJ=@g%$8C|Q5*7l1oA@6!g5 zqZ1e8bZO?~nd@l1iR?*jXn=)9akYd8W4aKD7PWtY?$7JSEjvlyKyY!{q zFYca%vAfAUx_nJU=4ZPK7oTXd^(jADz^H z0-JC|vTgxBp9eKOZ7@^MBGHIhmvEv^4F7_>{@C#KIKmrR#-E&ux6G*KUqNk|j3b7C zPOnR8QQ!vE5NFpMXCfPkX|;vR{s+*Jqpe}>5&XK`5wu^Yyw?f*0fG4y58t871n5=k zD*#NzgCB~ADD(_>V*bDeip1Z*?@DER@GlO_RC9YA-?~HN@Rhq1i66elk@$1lha-UX zjpyHeB>vp??`|ai-1Z%j_=!-y68fE3LIsx;F)B&YO^gikPeA)HUZnN}fVWG-uL7wT z6BN565D-z0A81q8h^0wF|3(@jAj z(s=?*Dw7o;5V6izBm!}71V*8QaUp%Xhu&ye)Z+qUQMTJ==L1E{3U=P5CTGH4GU_5G z@0M}5nDyUJs$TEfX*wJJUnP|JezJnS^?$a;wC}7-WE8~Po>*WDDwA=%^?2w1mEL-B zfiY$gMg)Hrduz7v<6OL;Dp3KktjaaA)r$~`s;ypFzYvLNWD(-2{S*7FtC$1GAs$}I zSa&ZAgqM#XU)z9;5PX3HE=w3;O<;&hy5=I}E*j40x=qMyJPG+Rv}1$^(q~dxxKimF z>n1Z-e3b66g}^BS+XxVeX9_*O%E4wjZ6~lFK&S6U(0-T4$7;p|p#SD7uTD>o`yA+x z%MSCD{VLrl4yxu{!(i(bUtu)@6j#hHSvDHJxdJN|yuRY7840u|A)hiJo-vF*R+4!aqc5)`Qx1KcK-?uu zUG}JWkG{~0EpqflJ*HQDpN>AWJ9prP9XcxEeI}^Uo8-zQhF@3H=~@67s%gmB@-^C3 zN;MK#M_^%;J|_-@$%4nI;tvSe$edwnhRkhIjlL!sU2|z|l^ap_3=P6px-UbkjkYd~ zr=_B=qH+DF08TVrY^fPf>^e~Ao_x|eZdh*8JQE38g3`=GB%pP6gDQ&)asq|s>STy;Gpzi&|K2h268;bj z67FjrxiI4ICcvnF7n+>c&DCoDG#PcQR`X+E8KLzZ{#)x?EqS$?D#p-PcYZ-)?TDcl zIHAK6$L=)yC>eTf4CKw23oL8((ZuFk-?PL@)iAV5YFCf#WLlo|k7P|jK7 zCZCRFTZ~N$6HrIt%XF{DeQpw3s-z==BL8h&7jwxPEbTIv9+SeF(c|qH`rOQ>t#C25r4Eo&~_q5=A7MHZo5g^mG_IU!&6Zm5S z|AoMh2>dq!hX_1I;4pv=L~g~7exEW4e1$H|0F)j(s@k!mNZwjfQgCJ{P;ZBU)|VIt z8VZPjQzT2Q6bx+Vclrl}wwnI1^rSub7l)-ky8SQ)#bKTL!@FdiVk>dp!*!zPwtsi6 zQ*+yQSf>)fd=%EBO6KPt=HalG^)#R9;~%YLMFN2nhah-wA-9zM4zQrIyr2unr2Bx< zu)LdMw}RbefGRDkEQN6dVT%e$O2b3RkjoZz!P^os{7vtmjXB=Pzi-+f#GU%2mxX9~@39IoNfQQDUQV)yZmWy!+0TXpyQW zixjmW&57bRsVeJ9ORz}^rotu6)-an?jceR%7cuU&RxhkylU*TKz3N=lv`RsMRqldS z%CWrXRkXP%dlXv>6mlfvY3`%in+VWXAbFJ^qthP{c!2;@oX*GH@U=jKiM2J6HS0+TARVujgIbdfzBDS1KcY4#<;w`)x*~ z?#_DO#cfwoaI&yA3sSZ>QNErsc&*Kp$t{iPS`{ zqfr_w3$^7NhB%W2wN*4>D;tN>czL?vR^rTy_xs+QKn$TvT8@j=i9?a ze4fQNC~VvGHfKJR&zdV-laF_m+j2(&wh_SM8-3WCuRk-0#fC-(v|pv(W15#_)LWe9 z((pZP#LT@aQn?{SsO#Ddx|2;Mf1COwtpT@N`&}vkerRD;W*wu8(moip4-qzKF}5r~ z9OoAQ6S~j+j6yDEscKJ>Xp^Oz)mVq~rRAdsX4kBpXngroUr&u5y&12$w7(EvH~!>} zcnd8acj%Qv<40c~{++Jt%?GZguQxsRqxAIZj!WU2bt@?5eX^?U(&@3Yg}PRxCLUp6 zs;+IO%KV$IYMZQU6{jNcRZS$XRkc}XH>fpnfhUkRc2?w}uyuwDqG{Y_;t^5EmAD;N zYF5srkHiQu`WST3M+jQ}Hi`_xaPy;ESWl{yLvY_gR2XXTLkb6##j_1b{~Ylw&=Im~ z*}qKNXx^z_c^v!}#eGQBQuuG@BHiV~bA&Wd8IB_bUGw%a0L~#XEjmX`6V$B)TOvNc z60AOgrp`lU?4Ak=XO9P{%$)jMYy+KHOkdrj;w>7A(RERdB7L;I>d`k^PqbKhTcj+$ zqNkurt`$15GZg%L=8BY{b^MTe;Yt6Kf+ksS2BHnwq?ky_*X)!vu|Gz$%(D(lA!YQD zexOC77c2EVh}Gw5PJhlRhm*cz5Gr&SPbWgzt!Q7~9U(D-cql z1;uhke(Qm~G+IR*ECUcygEk56+?5O_${c?CXOd-Lq_K8Z_kH$~Mmac7b`VqGI&6Wp z9!sWpkynVS^i1rqjcyPk?WYjpf>n+Ls$@k6Ayn643rH541~cIoOFc!4wc7cbRJzi3 zU$DinTujOpLe20`lX0f;t4dZ`sH| zTrjokdG^t+hrWU6OQY{yUJ!N&m;ZXk9MJ=dnjfW&xtPz39CJ|*V@{#w#om$Fnrds% z5fWaVh(Y&V?b1Nkbgj2$h}3e6m1q9~E7Vvs+xi8YM%riE_^K)wH-o~5QKuCt`rD-z|4S+Z?yGbkP&$PPlL?#rLzssNkEtLz!k^9V`q85$u0 zm~nb6YWS0@Rvo=I2)WKe&_rMIVvs zzg8cRg3OC#tP5-u|QQ)60*JR?ezRXVeu_ z>WZ<(X?6ANvZk43o2Hg+8m+jwV$;Nj3M=-E#%Kw>idQPekIppjn`++oy{1C*{vS0? zFaI!B(_69X)m5*o8vpq8iv1`E^~FO4ic;PC+EWv23+u6^$==EJd#B=iXH@g=I&vsr z)z0B*VsXv9YwBLEtU*-??ufbY#;6I%N$H&D{6_m$4eB4@QwIo@c8E?ql0&-6gJ7h z`nK{;4Be0fh`jKl#9HOu0mr7(wcweZJb!YY#lyAkGB^#+-8%Y*jPDi7sCi;;V=Uk* z-suea@h@s=Ack-4M=2KPK^@^ytz}JwmrdWAlv;!wutgP_JaCtvO$eiL4oJITu zJEWEcKW|;*l99#xSij(bXv0sot+nL+Ai~h#u@H4e-NG!V41e8p1{J{VU48o7XHFjhidV_5%U){$C`_>1{ zg!1rRaAwf1q1D!E&NO*2Nbk!3fL!3k;&;U9(80MK?tEbSPwHvfdfaFrMn|A3leOU+ z?(h=-i1zdv#?@FGb?9RUFMZ-tSHWKF+~jZ^o9E0fS;+5l<9K?!Wm4TZ-ZB;6IHQ_> z1$85ZM%fuWO_5e+-Zhjk&u-X-r>P-PMoyr-ZOFNs8>&rP0au7#ZX zDhQDVhcUHSf*B&C!NPsjYS`&e00wjLB zHb%Z|#(ustRK>Ua;&@0xM)5olPwMFu+ZISq4H|1aFp+(^9G{u~hv=JYzo0R`X8rI) z%wp{V9rTe+DpLfJT&knEh#lVW*xpR(gDu<~7=f${BbO zqm8@X=YbbJ=KR43kPm@rVfjJ{^xBIOci(66j@WI4HA%or7V~my=1G1(cr%y2BE;pi zXz&kn3HvZ<4K#V{&fkSllF#?AH z}QucJL#TB}PmJ1Fe=543f7>-J;VBex!Xpx0hHc6u8g26Owmo zLU7bk0Qe}=Gxy+M9G2?gHjSrjeMtDY^&wFP?YIAU!0Q|C5MQKdPdTo7`SvGx1R~gS z%XuRqM{~A#()rZ?;|#n!;W1gmmP}ljWXw@h8;bV|*h9X;I}L zGK4P>ws#aFctKiJInw#4q~}E-h~$JXxI*3wf}TPD_MN=7E5eDGFee?RglE_D<{?Er z@Pf|OJiW_{EvyhiQmk#kXS($F*ee!Wd(K^ec?It|!fx!AJ^g`fFDQu^(IWSL5TlH_ zwNwQ|T4<67s}U>A0}#GMsb2&*+FJHTFP#wGvKMr0joECWM&g>`CpQO1r(slP*ih#@;@M z9`d+-CvOQ=FyAg5Y}fPjQnZZn;}9av7O@6rvf5_pmTeTF3y3^U4L1+~L^Ya0-L zKRC&L86{yv#Fl!9n-oNZTN5}bsb^vjCky1kWwL^He6_sZzl&d#Ns3q9 zcJxNy2jHQmcz2|QVzs%2)Rwd6tRx)6%EE=%Hf)nxBi0mNRzFca9=T3_U$e^^XO^{2 zEo+_Fc%}W=3TSSI)#65WX;}ZXQ2872iCgntug#^CU$z*f~5s z`bm*@P2Fsr-S8@n%<;o=Y3WR4BQdRRo+H0idOBdOqSyXenhKvI;1c3UKwUs!TSAu! z5cjWt01YsKjrLA#x)m!aq@Xgeg$qXiP=+M$G;48-5lW?chk8;eLp?E)MKH^t&iEu_ zMJjierd}Hl0EP6?re#iRSjWf+i!9=@SRVUmj52KROR|&^7#taZDK1FqN|t6*5r%iR z)2~LXJR1+}0F=(3!Y0&NZ7@BMF+%ARI?GX0sgqg6)ufDQN8%$Ndi3D2RN^BaIc5aW z!HkwS{GUL8rV+~ye0xYM}+@*(KFoOL<>E4WHRAkPe9iGWi#h2+3t`cB4gkcW)h_%NFTp;ir0^cVv zN#Jh?kPymVCX09BZz763Lw%69zF(sc5`T*RB;c)bpU?NEzs%>q6I6V@&nce`|G5(R zbEW2gDu@4Esr*U2ns_+cVSP+{_4Lc9Uw!W7=O(n@IDPr_Z#;MTx!2DWwjM6jA1TB? zbUr$(ls~V2Mtwg1nfN8&rR^{6d2!E655D-|cyuCLsNFlQ?EBx!(%Dee^Pm3Ar)NT2 zrb1gLvai?tQQhy={n6^*TYa^E^4Rgo)YFB1{e{rLdH-zHikD(9#%8LvPE~Ea;xAO~ zI$t@f_@A%%OvQ{+H>K2#RZc4{KdEb;soOVIw{KEeGP|U4X337JB|9dS>e=XunP}5g zwCQ}9IePAyTDE6WseLOL^wr;~sqodmsQ}zry~)@7rUG!M%eTh2{Y?en&cnVM--b68 xfIIhXpiF=}$9(I3jc+OdcRGA=-@Z2$fIH1weQj?l0C#$Q%Y0jID*$ZB|35RNeJB6` literal 0 HcmV?d00001 diff --git a/tests/__pycache__/test_linear_regression.cpython-314-pytest-9.0.2.pyc b/tests/__pycache__/test_linear_regression.cpython-314-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..188f26b338cdf97c1a20d8e85543b53dc13d9319 GIT binary patch literal 39893 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 UdpA>x%}BA#+w!3YfW74Z2kFNH9RL6T literal 0 HcmV?d00001 diff --git a/tests/__pycache__/test_logistic_regression.cpython-314-pytest-9.0.2.pyc b/tests/__pycache__/test_logistic_regression.cpython-314-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..90e909d6c825b39ae94da17f9b32ca2513ef1f8f GIT binary patch literal 53933 zcmeHw33MFCdFC9Lna1E4Tm&zX1b7N01rp#TTGT;NvM5=ipq4bCZHTi&U18 z*4-Kg)h;RUpt$9&yAR z$Az0ifxzPdGf)WZ3Yfv$gM|?L4R5d7A6gXH9w|f@@iVbPjGZwG2K%ip#PJ)ty=HrD zp>}&+q0Wg{U#Nc|*0WGtNd5M-E4OgH=a@@GPU`o8ru9MY^Zpi^A257N)}c}h*RDXB zvY{ru#{>0cl*4;yzTP~^S57BxN1>gR-ip# zMrH@hs7O;-z`|k(Gi2C87FLb0xC~pw!fFs!E5q7YSRKOZWmr24OCYR4hAn1cjRR%V(BO!bE$rRb8Q;+nvgfAr`HWRar7bIcFqIzKH{!mq3}pKYu9w4UK7k9}c%W uEWxLA)ZNU2_WI*1!l} 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 IAlUu?KL{Kp%>V!Z literal 0 HcmV?d00001 diff --git a/tests/__pycache__/test_neural_network.cpython-314-pytest-9.0.2.pyc b/tests/__pycache__/test_neural_network.cpython-314-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..747773041e87ae0de679ccd7b8b5ddb55bb0389a GIT binary patch literal 54308 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 SM)2xGCAWB5uV?^lIsP9%l2`r! literal 0 HcmV?d00001 diff --git a/tests/__pycache__/test_pca.cpython-314-pytest-9.0.2.pyc b/tests/__pycache__/test_pca.cpython-314-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..71d0489ac6e2d5f1fce9f1dd98b2a71af4969930 GIT binary patch literal 47936 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 E0Vr=c4FCWD literal 0 HcmV?d00001 diff --git a/tests/__pycache__/test_svm.cpython-314-pytest-9.0.2.pyc b/tests/__pycache__/test_svm.cpython-314-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6a7417c3f488a6efd87051c2f9c772f308d6c516 GIT binary patch literal 40548 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 ZfUI 0).astype(int) + return X, y + + +@pytest.fixture +def simple_multiclass_data(): + np.random.seed(42) + X = np.random.randn(150, 2) + y = np.zeros(150) + y[50:100] = 1 + y[100:] = 2 + X[:50] += np.array([2, 2]) + X[50:100] += np.array([-2, 2]) + X[100:] += np.array([0, -2]) + return X, y + + +@pytest.fixture +def simple_clustering_data(): + np.random.seed(42) + cluster1 = np.random.randn(30, 2) + np.array([0, 0]) + cluster2 = np.random.randn(30, 2) + np.array([5, 5]) + cluster3 = np.random.randn(30, 2) + np.array([5, 0]) + X = np.vstack((cluster1, cluster2, cluster3)) + return X + + +@pytest.fixture +def simple_pca_data(): + np.random.seed(42) + X = np.random.randn(100, 5) + return X + + +@pytest.fixture +def simple_anomaly_data(): + np.random.seed(42) + X_normal = np.random.randn(100, 2) + X_anomaly = np.random.randn(10, 2) + np.array([5, 5]) + X = np.vstack((X_normal, X_anomaly)) + y = np.zeros(110) + y[100:] = 1 + return X, y diff --git a/tests/test_anomaly_detection.py b/tests/test_anomaly_detection.py new file mode 100644 index 0000000..5881079 --- /dev/null +++ b/tests/test_anomaly_detection.py @@ -0,0 +1,301 @@ +# -*- coding: utf-8 -*- +""" +Tests for Anomaly Detection algorithm. +""" +import os +import sys +import numpy as np +from numpy.testing import assert_array_almost_equal, assert_almost_equal +import pytest + +PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, os.path.join(PROJECT_ROOT, "AnomalyDetection")) + +from AnomalyDetection import ( + estimateGaussian, + multivariateGaussian, + selectThreshold, + display_2d_data, +) + + +class TestLoadData: + def test_load_data1_success(self, anomaly_detection_data1_path, load_mat_data): + data = load_mat_data(anomaly_detection_data1_path) + assert data is not None + assert isinstance(data, dict) + assert "X" in data + assert "Xval" in data + assert "yval" in data + + def test_load_data2_success(self, anomaly_detection_data2_path, load_mat_data): + data = load_mat_data(anomaly_detection_data2_path) + assert data is not None + assert isinstance(data, dict) + + def test_data_shape(self, anomaly_detection_data1_path, load_mat_data): + data = load_mat_data(anomaly_detection_data1_path) + X = data["X"] + Xval = data["Xval"] + yval = data["yval"] + assert X.shape[0] > 0 + assert Xval.shape[0] > 0 + assert yval.shape[0] == Xval.shape[0] + + +class TestEstimateGaussian: + def test_estimate_gaussian_output_shape(self, simple_anomaly_data): + X, _ = simple_anomaly_data + mu, sigma2 = estimateGaussian(X) + assert mu.shape[0] == X.shape[1] + assert sigma2.shape[0] == X.shape[1] + + def test_estimate_gaussian_mean_correct(self): + X = np.array([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]]) + mu, sigma2 = estimateGaussian(X) + expected_mu = np.mean(X, axis=0) + assert_array_almost_equal(mu, expected_mu, decimal=10) + + def test_estimate_gaussian_variance_correct(self): + X = np.array([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]]) + mu, sigma2 = estimateGaussian(X) + expected_sigma2 = np.var(X, axis=0) + assert_array_almost_equal(sigma2, expected_sigma2, decimal=10) + + def test_estimate_gaussian_single_feature(self): + X = np.array([[1.0], [2.0], [3.0], [4.0], [5.0]]) + mu, sigma2 = estimateGaussian(X) + assert mu.shape == (1,) + assert sigma2.shape == (1,) + assert_almost_equal(mu[0], 3.0) + + def test_estimate_gaussian_constant_feature(self): + X = np.array([[5.0], [5.0], [5.0], [5.0]]) + mu, sigma2 = estimateGaussian(X) + assert_almost_equal(mu[0], 5.0) + assert_almost_equal(sigma2[0], 0.0, decimal=5) + + +class TestMultivariateGaussian: + def test_multivariate_gaussian_output_shape(self, simple_anomaly_data): + X, _ = simple_anomaly_data + mu, sigma2 = estimateGaussian(X) + p = multivariateGaussian(X, mu, sigma2) + assert p.shape[0] == X.shape[0] + + def test_multivariate_gaussian_probabilities_range(self, simple_anomaly_data): + X, _ = simple_anomaly_data + mu, sigma2 = estimateGaussian(X) + p = multivariateGaussian(X, mu, sigma2) + assert np.all(p > 0) + assert np.all(p <= 1) + + def test_multivariate_gaussian_center_higher_prob(self): + np.random.seed(42) + X = np.random.randn(100, 2) + mu = np.mean(X, axis=0) + sigma2 = np.var(X, axis=0) + p = multivariateGaussian(X, mu, sigma2) + center_point = np.array([[mu[0], mu[1]]]) + p_center = multivariateGaussian(center_point, mu, sigma2) + assert p_center[0] >= np.min(p) + + def test_multivariate_gaussian_outlier_lower_prob(self): + np.random.seed(42) + X = np.random.randn(100, 2) + mu = np.mean(X, axis=0) + sigma2 = np.var(X, axis=0) + p_normal = multivariateGaussian(X, mu, sigma2) + outlier = np.array([[10.0, 10.0]]) + p_outlier = multivariateGaussian(outlier, mu, sigma2) + assert p_outlier[0] < np.mean(p_normal) + + def test_multivariate_gaussian_single_feature(self): + X = np.array([[1.0], [2.0], [3.0]]) + mu = np.array([2.0]) + sigma2 = np.array([1.0]) + try: + p = multivariateGaussian(X, mu, sigma2) + assert p.shape == (3,) + except np.linalg.LinAlgError: + pytest.skip("multivariateGaussian requires at least 2D covariance matrix") + + +class TestSelectThreshold: + def test_select_threshold_output_types(self, simple_anomaly_data): + X, y = simple_anomaly_data + mu, sigma2 = estimateGaussian(X) + pval = multivariateGaussian(X, mu, sigma2) + epsilon, F1 = selectThreshold(y, pval) + assert isinstance(epsilon, float) + assert isinstance(F1, float) + + def test_select_threshold_epsilon_in_range(self, simple_anomaly_data): + X, y = simple_anomaly_data + mu, sigma2 = estimateGaussian(X) + pval = multivariateGaussian(X, mu, sigma2) + epsilon, F1 = selectThreshold(y, pval) + assert epsilon >= np.min(pval) + assert epsilon <= np.max(pval) + + def test_select_threshold_f1_in_range(self, simple_anomaly_data): + X, y = simple_anomaly_data + mu, sigma2 = estimateGaussian(X) + pval = multivariateGaussian(X, mu, sigma2) + epsilon, F1 = selectThreshold(y, pval) + assert 0 <= F1 <= 1 + + def test_select_threshold_perfect_detection(self): + yval = np.array([0, 0, 0, 1, 1]) + pval = np.array([0.9, 0.8, 0.85, 0.1, 0.05]) + epsilon, F1 = selectThreshold(yval, pval) + assert F1 > 0 + + def test_select_threshold_no_anomalies(self): + yval = np.array([0, 0, 0, 0, 0]) + pval = np.array([0.9, 0.8, 0.85, 0.7, 0.75]) + epsilon, F1 = selectThreshold(yval, pval) + assert F1 >= 0 + + +class TestAnomalyDetectionIntegration: + def test_full_pipeline(self, anomaly_detection_data1_path, load_mat_data): + data = load_mat_data(anomaly_detection_data1_path) + X = data["X"] + Xval = data["Xval"] + yval = data["yval"] + mu, sigma2 = estimateGaussian(X) + p = multivariateGaussian(X, mu, sigma2) + pval = multivariateGaussian(Xval, mu, sigma2) + epsilon, F1 = selectThreshold(yval, pval) + outliers = np.where(p < epsilon)[0] + assert epsilon > 0 + assert F1 >= 0 + assert len(outliers) >= 0 + + def test_anomaly_detection_accuracy(self, anomaly_detection_data1_path, load_mat_data): + data = load_mat_data(anomaly_detection_data1_path) + X = data["X"] + Xval = data["Xval"] + yval = data["yval"].flatten() + mu, sigma2 = estimateGaussian(X) + pval = multivariateGaussian(Xval, mu, sigma2) + epsilon, F1 = selectThreshold(yval, pval) + predictions = (pval < epsilon).astype(int) + assert F1 > 0.5 + + def test_anomaly_detection_outliers_identified(self, simple_anomaly_data): + X, y = simple_anomaly_data + mu, sigma2 = estimateGaussian(X) + p = multivariateGaussian(X, mu, sigma2) + pval = multivariateGaussian(X, mu, sigma2) + epsilon, F1 = selectThreshold(y, pval) + outliers = np.where(p < epsilon)[0] + assert len(outliers) > 0 + + +class TestAnomalyDetectionSklearnComparison: + def test_sklearn_comparison(self, simple_anomaly_data): + from sklearn.covariance import EllipticEnvelope + X, y = simple_anomaly_data + mu, sigma2 = estimateGaussian(X) + p_custom = multivariateGaussian(X, mu, sigma2) + sklearn_model = EllipticEnvelope(contamination=0.1) + sklearn_model.fit(X) + p_sklearn = sklearn_model.predict(X) + assert p_custom.shape[0] == p_sklearn.shape[0] + + +class TestEdgeCases: + def test_single_sample(self): + X = np.array([[1.0, 2.0]]) + mu, sigma2 = estimateGaussian(X) + assert mu.shape == (2,) + assert sigma2.shape == (2,) + + def test_single_feature(self): + X = np.random.randn(100, 1) + mu, sigma2 = estimateGaussian(X) + assert mu.shape == (1,) + assert sigma2.shape == (1,) + + def test_all_normal_data(self): + np.random.seed(42) + X = np.random.randn(100, 2) + y = np.zeros(100) + mu, sigma2 = estimateGaussian(X) + pval = multivariateGaussian(X, mu, sigma2) + epsilon, F1 = selectThreshold(y, pval) + assert epsilon >= 0 + + def test_high_dimensional_data(self): + np.random.seed(42) + X = np.random.randn(100, 10) + mu, sigma2 = estimateGaussian(X) + p = multivariateGaussian(X, mu, sigma2) + assert p.shape == (100,) + + def test_extreme_outliers(self): + np.random.seed(42) + X_normal = np.random.randn(100, 2) + X_outliers = np.random.randn(5, 2) * 100 + X = np.vstack((X_normal, X_outliers)) + mu, sigma2 = estimateGaussian(X) + p = multivariateGaussian(X, mu, sigma2) + assert np.min(p) < np.max(p) + + +class TestDisplayData: + def test_display_2d_data_output(self, simple_anomaly_data): + X, _ = simple_anomaly_data + plt = display_2d_data(X, "bx") + assert plt is not None + + +class TestMultivariateGaussianProperties: + def test_probability_decreases_with_distance(self): + mu = np.array([0.0, 0.0]) + sigma2 = np.array([1.0, 1.0]) + distances = [0, 1, 2, 5, 10] + probs = [] + for d in distances: + X = np.array([[d, d]]) + p = multivariateGaussian(X, mu, sigma2) + probs.append(p[0]) + for i in range(len(probs) - 1): + assert probs[i] >= probs[i + 1] + + def test_probability_symmetry(self): + mu = np.array([0.0, 0.0]) + sigma2 = np.array([1.0, 1.0]) + X1 = np.array([[1.0, 0.0]]) + X2 = np.array([[-1.0, 0.0]]) + X3 = np.array([[0.0, 1.0]]) + X4 = np.array([[0.0, -1.0]]) + p1 = multivariateGaussian(X1, mu, sigma2) + p2 = multivariateGaussian(X2, mu, sigma2) + p3 = multivariateGaussian(X3, mu, sigma2) + p4 = multivariateGaussian(X4, mu, sigma2) + assert_almost_equal(p1[0], p2[0], decimal=10) + assert_almost_equal(p1[0], p3[0], decimal=10) + assert_almost_equal(p1[0], p4[0], decimal=10) + + +class TestF1Score: + def test_f1_score_calculation(self): + yval = np.array([0, 0, 0, 1, 1, 1]) + pval = np.array([0.9, 0.8, 0.85, 0.1, 0.05, 0.15]) + epsilon, F1 = selectThreshold(yval, pval) + assert F1 > 0 + + def test_f1_score_perfect(self): + yval = np.array([0, 0, 0, 1, 1]) + pval = np.array([0.9, 0.8, 0.85, 0.01, 0.02]) + epsilon, F1 = selectThreshold(yval, pval) + assert F1 > 0.8 + + def test_f1_score_zero_true_positives(self): + yval = np.array([0, 0, 0, 0, 0]) + pval = np.array([0.9, 0.8, 0.85, 0.7, 0.75]) + epsilon, F1 = selectThreshold(yval, pval) + assert F1 >= 0 diff --git a/tests/test_kmeans.py b/tests/test_kmeans.py new file mode 100644 index 0000000..13e879e --- /dev/null +++ b/tests/test_kmeans.py @@ -0,0 +1,319 @@ +# -*- coding: utf-8 -*- +""" +Tests for K-Means clustering algorithm. +""" +import os +import sys +import numpy as np +from numpy.testing import assert_array_almost_equal, assert_almost_equal +import pytest + +PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, os.path.join(PROJECT_ROOT, "K-Means")) + +import importlib.util +spec = importlib.util.spec_from_file_location("K_Menas", os.path.join(PROJECT_ROOT, "K-Means", "K-Menas.py")) +kmeans_module = importlib.util.module_from_spec(spec) +spec.loader.exec_module(kmeans_module) + +findClosestCentroids = kmeans_module.findClosestCentroids +computerCentroids = kmeans_module.computerCentroids +runKMeans = kmeans_module.runKMeans +kMeansInitCentroids = kmeans_module.kMeansInitCentroids + + +def _safe_idx_to_array(idx): + try: + return np.array(idx).flatten() + except (ValueError, TypeError): + if hasattr(idx, 'flatten'): + return idx.flatten() + return np.array([idx]) + + +class TestLoadData: + def test_load_mat_data_success(self, kmeans_data_path, load_mat_data): + data = load_mat_data(kmeans_data_path) + assert data is not None + assert isinstance(data, dict) + assert "X" in data + + def test_load_mat_data_shape(self, kmeans_data_path, load_mat_data): + data = load_mat_data(kmeans_data_path) + X = data["X"] + assert X.shape[0] > 0 + assert X.shape[1] == 2 + + +class TestFindClosestCentroids: + @pytest.mark.skipif(sys.version_info >= (3, 10), reason="numpy matrix compatibility issue in Python 3.10+") + def test_find_closest_centroids_output_shape(self, simple_clustering_data): + X = simple_clustering_data + K = 3 + initial_centroids = np.array([[0, 0], [5, 5], [5, 0]]) + idx = findClosestCentroids(X, initial_centroids) + idx_arr = _safe_idx_to_array(idx) + assert idx_arr.shape[0] == X.shape[0] + + @pytest.mark.skipif(sys.version_info >= (3, 10), reason="numpy matrix compatibility issue in Python 3.10+") + def test_find_closest_centroids_valid_indices(self, simple_clustering_data): + X = simple_clustering_data + K = 3 + initial_centroids = np.array([[0, 0], [5, 5], [5, 0]]) + idx = findClosestCentroids(X, initial_centroids) + idx_arr = _safe_idx_to_array(idx) + assert np.all((idx_arr >= 0) & (idx_arr < K)) + + @pytest.mark.skipif(sys.version_info >= (3, 10), reason="numpy matrix compatibility issue in Python 3.10+") + def test_find_closest_centroids_correct_assignment(self): + X = np.array([[0, 0], [10, 10], [0, 10]]) + initial_centroids = np.array([[0, 0], [10, 10]]) + idx = findClosestCentroids(X, initial_centroids) + idx_arr = _safe_idx_to_array(idx) + assert idx_arr[0] == 0 + assert idx_arr[1] == 1 + assert idx_arr[2] in [0, 1] + + @pytest.mark.skipif(sys.version_info >= (3, 10), reason="numpy matrix compatibility issue in Python 3.10+") + def test_find_closest_centroids_single_centroid(self, simple_clustering_data): + X = simple_clustering_data + initial_centroids = np.array([[2.5, 2.5]]) + idx = findClosestCentroids(X, initial_centroids) + idx_arr = _safe_idx_to_array(idx) + assert np.all(idx_arr == 0) + + @pytest.mark.skipif(sys.version_info >= (3, 10), reason="numpy matrix compatibility issue in Python 3.10+") + def test_find_closest_centroids_equal_distance(self): + X = np.array([[5, 5]]) + initial_centroids = np.array([[0, 0], [10, 10]]) + idx = findClosestCentroids(X, initial_centroids) + idx_arr = _safe_idx_to_array(idx) + assert idx_arr[0] in [0, 1] + + +class TestComputerCentroids: + def test_computer_centroids_output_shape(self, simple_clustering_data): + X = simple_clustering_data + K = 3 + idx = np.random.randint(0, K, X.shape[0]) + centroids = computerCentroids(X, idx, K) + assert centroids.shape == (K, X.shape[1]) + + def test_computer_centroids_mean_calculation(self): + X = np.array([[0, 0], [1, 1], [10, 10], [11, 11]]) + idx = np.array([0, 0, 1, 1]) + K = 2 + centroids = computerCentroids(X, idx, K) + assert_array_almost_equal(centroids[0], [0.5, 0.5], decimal=5) + assert_array_almost_equal(centroids[1], [10.5, 10.5], decimal=5) + + def test_computer_centroids_single_cluster(self, simple_clustering_data): + X = simple_clustering_data + K = 1 + idx = np.zeros(X.shape[0], dtype=int) + centroids = computerCentroids(X, idx, K) + expected_mean = np.mean(X, axis=0) + assert_array_almost_equal(centroids[0], expected_mean, decimal=5) + + def test_computer_centroids_empty_cluster(self): + X = np.array([[0, 0], [1, 1], [10, 10]]) + idx = np.array([0, 0, 0]) + K = 2 + centroids = computerCentroids(X, idx, K) + assert centroids.shape == (K, X.shape[1]) + + +class TestKMeansInitCentroids: + def test_kmeans_init_centroids_shape(self, simple_clustering_data): + X = simple_clustering_data + K = 3 + centroids = kMeansInitCentroids(X, K) + assert centroids.shape == (K, X.shape[1]) + + def test_kmeans_init_centroids_from_data(self, simple_clustering_data): + X = simple_clustering_data + K = 3 + centroids = kMeansInitCentroids(X, K) + for c in centroids: + assert np.any(np.all(X == c, axis=1)) + + def test_kmeans_init_centroids_valid_k(self, simple_clustering_data): + X = simple_clustering_data + K = 5 + centroids = kMeansInitCentroids(X, K) + assert centroids.shape[0] == K + + +class TestRunKMeans: + @pytest.mark.skipif(sys.version_info >= (3, 10), reason="numpy matrix compatibility issue in Python 3.10+") + def test_run_kmeans_output_shape(self, simple_clustering_data): + X = simple_clustering_data + K = 3 + initial_centroids = kMeansInitCentroids(X, K) + max_iters = 10 + centroids, idx = runKMeans(X, initial_centroids, max_iters, False) + idx_arr = _safe_idx_to_array(idx) + assert centroids.shape == (K, X.shape[1]) + assert idx_arr.shape[0] == X.shape[0] + + @pytest.mark.skipif(sys.version_info >= (3, 10), reason="numpy matrix compatibility issue in Python 3.10+") + def test_run_kmeans_convergence(self, simple_clustering_data): + X = simple_clustering_data + K = 3 + initial_centroids = np.array([[0, 0], [5, 5], [5, 0]]) + max_iters = 20 + centroids, idx = runKMeans(X, initial_centroids, max_iters, False) + assert centroids is not None + assert idx is not None + + @pytest.mark.skipif(sys.version_info >= (3, 10), reason="numpy matrix compatibility issue in Python 3.10+") + def test_run_kmeans_valid_indices(self, simple_clustering_data): + X = simple_clustering_data + K = 3 + initial_centroids = kMeansInitCentroids(X, K) + max_iters = 10 + centroids, idx = runKMeans(X, initial_centroids, max_iters, False) + idx_arr = _safe_idx_to_array(idx) + assert np.all((idx_arr >= 0) & (idx_arr < K)) + + @pytest.mark.skipif(sys.version_info >= (3, 10), reason="numpy matrix compatibility issue in Python 3.10+") + def test_run_kmeans_single_iteration(self, simple_clustering_data): + X = simple_clustering_data + K = 3 + initial_centroids = kMeansInitCentroids(X, K) + max_iters = 1 + centroids, idx = runKMeans(X, initial_centroids, max_iters, False) + assert centroids.shape == (K, X.shape[1]) + + +class TestKMeansIntegration: + @pytest.mark.skipif(sys.version_info >= (3, 10), reason="numpy matrix compatibility issue in Python 3.10+") + def test_full_pipeline(self, kmeans_data_path, load_mat_data): + data = load_mat_data(kmeans_data_path) + X = data["X"] + K = 3 + initial_centroids = np.array([[3, 3], [6, 2], [8, 5]]) + max_iters = 10 + centroids, idx = runKMeans(X, initial_centroids, max_iters, False) + assert centroids.shape == (K, X.shape[1]) + idx_arr = _safe_idx_to_array(idx) + assert idx_arr.shape[0] == X.shape[0] + + @pytest.mark.skipif(sys.version_info >= (3, 10), reason="numpy matrix compatibility issue in Python 3.10+") + def test_clustering_quality(self, simple_clustering_data): + X = simple_clustering_data + K = 3 + initial_centroids = np.array([[0, 0], [5, 5], [5, 0]]) + max_iters = 20 + centroids, idx = runKMeans(X, initial_centroids, max_iters, False) + idx_arr = _safe_idx_to_array(idx) + for k in range(K): + cluster_points = X[idx_arr == k] + if len(cluster_points) > 0: + distances = np.sum((cluster_points - centroids[k]) ** 2, axis=1) + assert np.mean(distances) < 10 + + @pytest.mark.skipif(sys.version_info >= (3, 10), reason="numpy matrix compatibility issue in Python 3.10+") + def test_multiple_runs_different_init(self, simple_clustering_data): + X = simple_clustering_data + K = 3 + max_iters = 10 + results = [] + for _ in range(3): + initial_centroids = kMeansInitCentroids(X, K) + centroids, idx = runKMeans(X, initial_centroids, max_iters, False) + results.append((centroids, idx)) + assert len(results) == 3 + + +class TestKMeansSklearnComparison: + @pytest.mark.skipif(sys.version_info >= (3, 10), reason="numpy matrix compatibility issue in Python 3.10+") + def test_sklearn_comparison(self, simple_clustering_data): + from sklearn.cluster import KMeans as SklearnKMeans + X = simple_clustering_data + K = 3 + initial_centroids = np.array([[0, 0], [5, 5], [5, 0]]) + max_iters = 20 + custom_centroids, custom_idx = runKMeans(X, initial_centroids, max_iters, False) + sklearn_model = SklearnKMeans(n_clusters=K, n_init=1, random_state=42) + sklearn_model.fit(X) + sklearn_idx = sklearn_model.labels_ + custom_idx_arr = _safe_idx_to_array(custom_idx) + custom_counts = np.bincount(custom_idx_arr.astype(int), minlength=K) + sklearn_counts = np.bincount(sklearn_idx, minlength=K) + assert len(custom_counts) == len(sklearn_counts) + + +class TestEdgeCases: + @pytest.mark.skipif(sys.version_info >= (3, 10), reason="numpy matrix compatibility issue in Python 3.10+") + def test_single_sample(self): + X = np.array([[1.0, 2.0]]) + K = 1 + initial_centroids = np.array([[1.0, 2.0]]) + max_iters = 5 + centroids, idx = runKMeans(X, initial_centroids, max_iters, False) + assert centroids.shape == (K, 2) + idx_arr = _safe_idx_to_array(idx) + assert idx_arr[0] == 0 + + @pytest.mark.skipif(sys.version_info >= (3, 10), reason="numpy matrix compatibility issue in Python 3.10+") + def test_k_equals_n(self): + X = np.array([[0, 0], [1, 1], [2, 2]]) + K = 3 + initial_centroids = X.copy() + max_iters = 5 + centroids, idx = runKMeans(X, initial_centroids, max_iters, False) + assert centroids.shape == (K, 2) + + @pytest.mark.skipif(sys.version_info >= (3, 10), reason="numpy matrix compatibility issue in Python 3.10+") + def test_large_k(self, simple_clustering_data): + X = simple_clustering_data + K = 10 + initial_centroids = kMeansInitCentroids(X, K) + max_iters = 5 + centroids, idx = runKMeans(X, initial_centroids, max_iters, False) + assert centroids.shape == (K, X.shape[1]) + + @pytest.mark.skipif(sys.version_info >= (3, 10), reason="numpy matrix compatibility issue in Python 3.10+") + def test_high_dimensional_data(self): + np.random.seed(42) + X = np.random.randn(100, 10) + K = 3 + initial_centroids = kMeansInitCentroids(X, K) + max_iters = 10 + centroids, idx = runKMeans(X, initial_centroids, max_iters, False) + assert centroids.shape == (K, 10) + + def test_zero_iterations(self, simple_clustering_data): + X = simple_clustering_data + K = 3 + initial_centroids = kMeansInitCentroids(X, K) + max_iters = 0 + centroids, idx = runKMeans(X, initial_centroids, max_iters, False) + idx_arr = _safe_idx_to_array(idx) + assert idx_arr.shape[0] == X.shape[0] + + +class TestKMeansImageCompression: + def test_image_data_reshape(self): + np.random.seed(42) + img_data = np.random.rand(10, 10, 3) + img_size = img_data.shape + X = img_data.reshape(img_size[0] * img_size[1], 3) + assert X.shape == (100, 3) + + @pytest.mark.skipif(sys.version_info >= (3, 10), reason="numpy matrix compatibility issue in Python 3.10+") + def test_image_compression_pipeline(self): + np.random.seed(42) + img_data = np.random.rand(10, 10, 3) + img_size = img_data.shape + X = img_data.reshape(img_size[0] * img_size[1], 3) + K = 4 + max_iters = 5 + initial_centroids = kMeansInitCentroids(X, K) + centroids, idx = runKMeans(X, initial_centroids, max_iters, False) + idx = findClosestCentroids(X, centroids) + idx_arr = _safe_idx_to_array(idx) + X_recovered = centroids[idx_arr, :] + X_recovered = X_recovered.reshape(img_size[0], img_size[1], 3) + assert X_recovered.shape == img_size diff --git a/tests/test_linear_regression.py b/tests/test_linear_regression.py new file mode 100644 index 0000000..2b868b4 --- /dev/null +++ b/tests/test_linear_regression.py @@ -0,0 +1,241 @@ +# -*- coding: utf-8 -*- +""" +Tests for Linear Regression algorithm. +""" +import os +import sys +import numpy as np +from numpy.testing import assert_array_almost_equal, assert_almost_equal +import pytest + +PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, os.path.join(PROJECT_ROOT, "LinearRegression")) + +from LinearRegression import ( + loadtxtAndcsv_data, + loadnpy_data, + featureNormaliza, + gradientDescent, + computerCost, + predict, +) + + +class TestLoadData: + def test_load_txt_data_success(self, linear_regression_data_path): + data = loadtxtAndcsv_data(linear_regression_data_path, ",", np.float64) + assert data is not None + assert isinstance(data, np.ndarray) + assert data.ndim == 2 + assert data.shape[1] == 3 + + def test_load_txt_data_shape(self, linear_regression_data_path): + data = loadtxtAndcsv_data(linear_regression_data_path, ",", np.float64) + assert data.shape[0] > 0 + assert data.shape[1] == 3 + + def test_load_npy_data(self): + npy_path = os.path.join(PROJECT_ROOT, "LinearRegression", "data.npy") + if os.path.exists(npy_path): + data = loadnpy_data(npy_path) + assert data is not None + assert isinstance(data, np.ndarray) + + +class TestFeatureNormalization: + def test_feature_normaliza_output_shape(self, simple_regression_data): + X, _, _ = simple_regression_data + X_norm, mu, sigma = featureNormaliza(X) + assert X_norm.shape == X.shape + assert mu.shape[0] == X.shape[1] + assert sigma.shape[0] == X.shape[1] + + def test_feature_normaliza_mean_zero(self, simple_regression_data): + X, _, _ = simple_regression_data + X_norm, mu, sigma = featureNormaliza(X) + mean_after = np.mean(X_norm, axis=0) + assert_array_almost_equal(mean_after, np.zeros(X.shape[1]), decimal=10) + + def test_feature_normaliza_std_one(self, simple_regression_data): + X, _, _ = simple_regression_data + X_norm, mu, sigma = featureNormaliza(X) + std_after = np.std(X_norm, axis=0) + assert_array_almost_equal(std_after, np.ones(X.shape[1]), decimal=10) + + def test_feature_normaliza_single_feature(self): + X = np.array([[1.0], [2.0], [3.0], [4.0], [5.0]]) + X_norm, mu, sigma = featureNormaliza(X) + assert X_norm.shape == X.shape + assert_almost_equal(mu[0], 3.0) + assert sigma[0] > 0 + + def test_feature_normaliza_constant_feature(self): + X = np.array([[5.0], [5.0], [5.0], [5.0]]) + X_norm, mu, sigma = featureNormaliza(X) + assert X_norm.shape == X.shape + assert_almost_equal(mu[0], 5.0) + + +class TestComputerCost: + def test_computer_cost_zero_theta(self, simple_regression_data): + X, y, _ = simple_regression_data + m = X.shape[0] + X_with_bias = np.matrix(np.hstack((np.ones((m, 1)), X))) + theta = np.matrix(np.zeros((X_with_bias.shape[1], 1))) + y = np.matrix(y.reshape(-1, 1)) + J = computerCost(X_with_bias, y, theta) + assert J >= 0 + assert isinstance(J, (float, np.ndarray, np.matrix)) + + def test_computer_cost_optimal_theta(self, simple_regression_data): + X, y, true_theta = simple_regression_data + m = X.shape[0] + X_with_bias = np.matrix(np.hstack((np.ones((m, 1)), X))) + y = np.matrix(y.reshape(-1, 1)) + J = computerCost(X_with_bias, y, np.matrix(true_theta)) + assert J >= 0 + assert J < 1.0 + + def test_computer_cost_perfect_fit(self): + X = np.matrix([[1, 1], [1, 2], [1, 3]]) + y = np.matrix([[2], [4], [6]]) + theta = np.matrix([[0], [2]]) + J = computerCost(X, y, theta) + assert float(np.array(J).flatten()[0]) < 1e-5 + + def test_computer_cost_shape(self, simple_regression_data): + X, y, _ = simple_regression_data + m = X.shape[0] + X_with_bias = np.matrix(np.hstack((np.ones((m, 1)), X))) + theta = np.matrix(np.zeros((X_with_bias.shape[1], 1))) + y = np.matrix(y.reshape(-1, 1)) + J = computerCost(X_with_bias, y, theta) + assert np.isscalar(J) or J.shape == (1, 1) + + +class TestGradientDescent: + def test_gradient_descent_convergence(self, simple_regression_data): + X, y, true_theta = simple_regression_data + m = X.shape[0] + X_with_bias = np.hstack((np.ones((m, 1)), X)) + theta = np.zeros((X_with_bias.shape[1], 1)) + y = y.reshape(-1, 1) + alpha = 0.01 + num_iters = 100 + theta_final, J_history = gradientDescent(X_with_bias, y, theta, alpha, num_iters) + assert theta_final.shape == theta.shape + assert J_history.shape[0] == num_iters + + def test_gradient_descent_cost_decrease(self, simple_regression_data): + X, y, _ = simple_regression_data + m = X.shape[0] + X_with_bias = np.hstack((np.ones((m, 1)), X)) + theta = np.zeros((X_with_bias.shape[1], 1)) + y = y.reshape(-1, 1) + alpha = 0.01 + num_iters = 100 + theta_final, J_history = gradientDescent(X_with_bias, y, theta, alpha, num_iters) + assert J_history[-1] < J_history[0] + + def test_gradient_descent_output_shape(self, simple_regression_data): + X, y, _ = simple_regression_data + m = X.shape[0] + X_with_bias = np.hstack((np.ones((m, 1)), X)) + theta = np.zeros((X_with_bias.shape[1], 1)) + y = y.reshape(-1, 1) + alpha = 0.01 + num_iters = 50 + theta_final, J_history = gradientDescent(X_with_bias, y, theta, alpha, num_iters) + assert theta_final.shape[0] == X_with_bias.shape[1] + assert len(J_history) == num_iters + + def test_gradient_descent_simple_linear(self): + X = np.matrix([[1, 1], [1, 2], [1, 3], [1, 4]]) + y = np.matrix([[3], [5], [7], [9]]) + theta = np.matrix(np.zeros((2, 1))) + alpha = 0.1 + num_iters = 100 + theta_final, J_history = gradientDescent(X, y, theta, alpha, num_iters) + assert_array_almost_equal(np.array(theta_final).flatten(), [1.0, 2.0], decimal=0) + + +class TestPredict: + def test_predict_output_shape(self, simple_regression_data): + X, y, true_theta = simple_regression_data + X_norm, mu, sigma = featureNormaliza(X) + result = predict(mu, sigma, true_theta) + assert result is not None + + def test_predict_value_range(self, simple_regression_data): + X, y, true_theta = simple_regression_data + X_norm, mu, sigma = featureNormaliza(X) + result = predict(mu, sigma, true_theta) + assert isinstance(result, (float, np.ndarray)) + + +class TestLinearRegressionIntegration: + def test_full_pipeline(self, linear_regression_data_path): + data = loadtxtAndcsv_data(linear_regression_data_path, ",", np.float64) + X = data[:, 0:-1] + y = data[:, -1] + m = len(y) + X_norm, mu, sigma = featureNormaliza(X) + X_with_bias = np.hstack((np.ones((m, 1)), X_norm)) + theta = np.zeros((X_with_bias.shape[1], 1)) + y = y.reshape(-1, 1) + alpha = 0.01 + num_iters = 100 + theta_final, J_history = gradientDescent(X_with_bias, y, theta, alpha, num_iters) + assert J_history[-1] < J_history[0] + assert theta_final is not None + + def test_linear_regression_with_sklearn_comparison(self, linear_regression_data_path): + data = loadtxtAndcsv_data(linear_regression_data_path, ",", np.float64) + X = data[:, 0:-1] + y = data[:, -1] + m = len(y) + X_norm, mu, sigma = featureNormaliza(X) + X_with_bias = np.hstack((np.ones((m, 1)), X_norm)) + theta = np.zeros((X_with_bias.shape[1], 1)) + y = y.reshape(-1, 1) + alpha = 0.01 + num_iters = 400 + theta_final, _ = gradientDescent(X_with_bias, y, theta, alpha, num_iters) + from sklearn.linear_model import LinearRegression as SklearnLR + from sklearn.preprocessing import StandardScaler + scaler = StandardScaler() + X_scaled = scaler.fit_transform(X) + model = SklearnLR() + model.fit(X_scaled, y) + assert theta_final.shape[0] == X.shape[1] + 1 + + +class TestEdgeCases: + def test_single_sample(self): + X = np.matrix([[1.0, 1.0, 2.0]]) + y = np.matrix([[5.0]]) + theta = np.matrix(np.zeros((3, 1))) + J = computerCost(X, y, theta) + assert J >= 0 + + def test_large_learning_rate(self, simple_regression_data): + X, y, _ = simple_regression_data + m = X.shape[0] + X_with_bias = np.hstack((np.ones((m, 1)), X)) + theta = np.zeros((X_with_bias.shape[1], 1)) + y = y.reshape(-1, 1) + alpha = 1.0 + num_iters = 10 + theta_final, J_history = gradientDescent(X_with_bias, y, theta, alpha, num_iters) + assert theta_final is not None + + def test_zero_iterations(self, simple_regression_data): + X, y, _ = simple_regression_data + m = X.shape[0] + X_with_bias = np.hstack((np.ones((m, 1)), X)) + theta = np.zeros((X_with_bias.shape[1], 1)) + y = y.reshape(-1, 1) + alpha = 0.01 + num_iters = 0 + theta_final, J_history = gradientDescent(X_with_bias, y, theta, alpha, num_iters) + assert len(J_history) == 0 diff --git a/tests/test_logistic_regression.py b/tests/test_logistic_regression.py new file mode 100644 index 0000000..ef36763 --- /dev/null +++ b/tests/test_logistic_regression.py @@ -0,0 +1,315 @@ +# -*- coding: utf-8 -*- +""" +Tests for Logistic Regression algorithm. +""" +import os +import sys +import numpy as np +from numpy.testing import assert_array_almost_equal, assert_almost_equal +import pytest + +PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, os.path.join(PROJECT_ROOT, "LogisticRegression")) + +from LogisticRegression import ( + loadtxtAndcsv_data, + mapFeature, + costFunction, + gradient, + sigmoid, + predict, +) + + +class TestLoadData: + def test_load_data1_success(self, logistic_regression_data1_path): + data = loadtxtAndcsv_data(logistic_regression_data1_path, ",", np.float64) + assert data is not None + assert isinstance(data, np.ndarray) + assert data.ndim == 2 + + def test_load_data2_success(self, logistic_regression_data2_path): + data = loadtxtAndcsv_data(logistic_regression_data2_path, ",", np.float64) + assert data is not None + assert isinstance(data, np.ndarray) + assert data.ndim == 2 + + def test_load_data_shape(self, logistic_regression_data1_path): + data = loadtxtAndcsv_data(logistic_regression_data1_path, ",", np.float64) + assert data.shape[0] > 0 + assert data.shape[1] == 3 + + +class TestSigmoid: + def test_sigmoid_zero(self): + z = np.array([0]) + h = sigmoid(z) + assert_almost_equal(h[0], 0.5, decimal=5) + + def test_sigmoid_positive(self): + z = np.array([10]) + h = sigmoid(z) + assert h[0] > 0.5 + assert h[0] < 1.0 + + def test_sigmoid_negative(self): + z = np.array([-10]) + h = sigmoid(z) + assert h[0] < 0.5 + assert h[0] > 0.0 + + def test_sigmoid_large_positive(self): + z = np.array([100]) + h = sigmoid(z) + assert_almost_equal(h[0], 1.0, decimal=5) + + def test_sigmoid_large_negative(self): + z = np.array([-100]) + h = sigmoid(z) + assert_almost_equal(h[0], 0.0, decimal=5) + + def test_sigmoid_array(self): + z = np.array([-10, 0, 10]) + h = sigmoid(z) + assert h.shape == z.shape + assert h[0] < 0.5 + assert_almost_equal(h[1], 0.5, decimal=5) + assert h[2] > 0.5 + + def test_sigmoid_range(self): + z = np.linspace(-10, 10, 100) + h = sigmoid(z) + assert np.all(h > 0) and np.all(h < 1) + + +class TestMapFeature: + def test_map_feature_output_shape(self): + X1 = np.array([1, 2, 3]) + X2 = np.array([1, 2, 3]) + out = mapFeature(X1, X2) + assert out.shape[0] == len(X1) + assert out.shape[1] > 2 + + def test_map_feature_first_column_ones(self): + X1 = np.array([1, 2, 3]) + X2 = np.array([1, 2, 3]) + out = mapFeature(X1, X2) + assert_array_almost_equal(out[:, 0], np.ones(len(X1))) + + def test_map_feature_contains_original(self): + X1 = np.array([1.0, 2.0, 3.0]) + X2 = np.array([4.0, 5.0, 6.0]) + out = mapFeature(X1, X2) + assert np.any(np.isclose(out[:, 1], X1)) + assert np.any(np.isclose(out[:, 2], X2)) + + +class TestCostFunction: + def test_cost_function_zero_theta(self, simple_classification_data): + X, y = simple_classification_data + m = X.shape[0] + X_with_bias = np.hstack((np.ones((m, 1)), X)) + theta = np.zeros(X_with_bias.shape[1]) + Lambda = 0.1 + J = costFunction(theta, X_with_bias, y, Lambda) + assert J >= 0 + assert isinstance(J, (float, np.ndarray)) + + def test_cost_function_initial_cost(self): + X = np.array([[1, 1, 0], [1, 1, 1], [1, 1, 2]]) + y = np.array([0, 1, 1]) + theta = np.zeros(3) + Lambda = 0 + J = costFunction(theta, X, y, Lambda) + assert_almost_equal(float(J), 0.693147, decimal=3) + + def test_cost_function_with_regularization(self, simple_classification_data): + X, y = simple_classification_data + m = X.shape[0] + X_with_bias = np.hstack((np.ones((m, 1)), X)) + theta = np.zeros(X_with_bias.shape[1]) + Lambda = 1.0 + J_reg = costFunction(theta, X_with_bias, y, Lambda) + Lambda = 0.0 + J_no_reg = costFunction(theta, X_with_bias, y, Lambda) + assert J_reg >= J_no_reg + + def test_cost_function_perfect_fit(self): + X = np.array([[1, 1, 0], [1, 1, 10], [1, 1, -10]]) + y = np.array([0, 1, 0]) + theta = np.array([0, 0, 1]) + Lambda = 0 + J = costFunction(theta, X, y, Lambda) + assert J >= 0 + + +class TestGradient: + def test_gradient_shape(self, simple_classification_data): + X, y = simple_classification_data + m = X.shape[0] + X_with_bias = np.hstack((np.ones((m, 1)), X)) + theta = np.zeros(X_with_bias.shape[1]) + Lambda = 0.1 + grad = gradient(theta, X_with_bias, y, Lambda) + assert grad.shape == theta.shape + + def test_gradient_zero_theta(self, simple_classification_data): + X, y = simple_classification_data + m = X.shape[0] + X_with_bias = np.hstack((np.ones((m, 1)), X)) + theta = np.zeros(X_with_bias.shape[1]) + Lambda = 0.1 + grad = gradient(theta, X_with_bias, y, Lambda) + assert isinstance(grad, np.ndarray) + + def test_gradient_with_regularization(self, simple_classification_data): + X, y = simple_classification_data + m = X.shape[0] + X_with_bias = np.hstack((np.ones((m, 1)), X)) + theta = np.ones(X_with_bias.shape[1]) + Lambda = 1.0 + grad = gradient(theta, X_with_bias, y, Lambda) + assert grad is not None + + +class TestPredict: + def test_predict_output_shape(self, simple_classification_data): + X, y = simple_classification_data + m = X.shape[0] + X_with_bias = np.hstack((np.ones((m, 1)), X)) + theta = np.zeros(X_with_bias.shape[1]) + p = predict(X_with_bias, theta) + assert p.shape[0] == m + + def test_predict_binary_values(self, simple_classification_data): + X, y = simple_classification_data + m = X.shape[0] + X_with_bias = np.hstack((np.ones((m, 1)), X)) + theta = np.zeros(X_with_bias.shape[1]) + p = predict(X_with_bias, theta) + assert np.all((p == 0) | (p == 1)) + + def test_predict_perfect_separation(self): + X = np.array([[1, 1, 10], [1, 1, -10], [1, 1, 20], [1, 1, -20]]) + y = np.array([1, 0, 1, 0]) + theta = np.array([0, 0, 1]) + p = predict(X, theta) + assert_array_almost_equal(p.flatten(), y) + + +class TestLogisticRegressionIntegration: + def test_full_pipeline(self, logistic_regression_data1_path): + data = loadtxtAndcsv_data(logistic_regression_data1_path, ",", np.float64) + X = data[:, 0:-1] + y = data[:, -1] + m = X.shape[0] + X_with_bias = np.hstack((np.ones((m, 1)), X)) + theta = np.zeros(X_with_bias.shape[1]) + Lambda = 0.1 + J = costFunction(theta, X_with_bias, y, Lambda) + grad = gradient(theta, X_with_bias, y, Lambda) + assert J >= 0 + assert grad.shape == theta.shape + + def test_optimization_with_scipy(self, logistic_regression_data1_path): + from scipy import optimize + data = loadtxtAndcsv_data(logistic_regression_data1_path, ",", np.float64) + X = data[:, 0:-1] + y = data[:, -1] + m = X.shape[0] + X_with_bias = np.hstack((np.ones((m, 1)), X)) + theta = np.zeros(X_with_bias.shape[1]) + Lambda = 0.1 + result = optimize.fmin_bfgs( + costFunction, theta, fprime=gradient, args=(X_with_bias, y, Lambda), maxiter=100 + ) + assert result is not None + assert len(result) == X_with_bias.shape[1] + + def test_accuracy_on_data(self, logistic_regression_data1_path): + from scipy import optimize + data = loadtxtAndcsv_data(logistic_regression_data1_path, ",", np.float64) + X = data[:, 0:-1] + y = data[:, -1] + m = X.shape[0] + X_with_bias = np.hstack((np.ones((m, 1)), X)) + theta = np.zeros(X_with_bias.shape[1]) + Lambda = 0.1 + result = optimize.fmin_bfgs( + costFunction, theta, fprime=gradient, args=(X_with_bias, y, Lambda), maxiter=100 + ) + p = predict(X_with_bias, result) + accuracy = np.mean(p.flatten() == y) + assert accuracy > 0.5 + + +class TestLogisticRegressionSklearnComparison: + def test_sklearn_comparison(self, logistic_regression_data1_path): + from scipy import optimize + from sklearn.linear_model import LogisticRegression + data = loadtxtAndcsv_data(logistic_regression_data1_path, ",", np.float64) + X = data[:, 0:-1] + y = data[:, -1] + m = X.shape[0] + X_with_bias = np.hstack((np.ones((m, 1)), X)) + theta = np.zeros(X_with_bias.shape[1]) + Lambda = 0.1 + result = optimize.fmin_bfgs( + costFunction, theta, fprime=gradient, args=(X_with_bias, y, Lambda), maxiter=100 + ) + p_custom = predict(X_with_bias, result) + model = LogisticRegression(C=1.0 / Lambda, solver="lbfgs", max_iter=100) + model.fit(X, y) + p_sklearn = model.predict(X) + custom_accuracy = np.mean(p_custom.flatten() == y) + sklearn_accuracy = np.mean(p_sklearn == y) + assert abs(custom_accuracy - sklearn_accuracy) < 0.2 + + +class TestEdgeCases: + def test_single_sample(self): + X = np.array([[1, 1.0, 2.0]]) + y = np.array([1]) + theta = np.zeros(3) + Lambda = 0.1 + J = costFunction(theta, X, y, Lambda) + grad = gradient(theta, X, y, Lambda) + assert J >= 0 + assert grad.shape == theta.shape + + def test_all_same_class(self): + X = np.array([[1, 1.0], [1, 2.0], [1, 3.0]]) + y = np.array([1, 1, 1]) + theta = np.zeros(2) + Lambda = 0.1 + J = costFunction(theta, X, y, Lambda) + assert J >= 0 + + def test_large_lambda(self, simple_classification_data): + X, y = simple_classification_data + m = X.shape[0] + X_with_bias = np.hstack((np.ones((m, 1)), X)) + theta = np.zeros(X_with_bias.shape[1]) + Lambda = 100.0 + J = costFunction(theta, X_with_bias, y, Lambda) + grad = gradient(theta, X_with_bias, y, Lambda) + assert J >= 0 + assert grad is not None + + +class TestOneVsAll: + def test_onevsall_data_load(self, logistic_regression_digits_path): + import scipy.io as spio + data = spio.loadmat(logistic_regression_digits_path) + assert "X" in data + assert "y" in data + assert data["X"].shape[0] > 0 + assert data["y"].shape[0] > 0 + + def test_onevsall_data_shape(self, logistic_regression_digits_path): + import scipy.io as spio + data = spio.loadmat(logistic_regression_digits_path) + X = data["X"] + y = data["y"] + assert X.shape[1] == 400 + assert len(np.unique(y)) <= 10 diff --git a/tests/test_neural_network.py b/tests/test_neural_network.py new file mode 100644 index 0000000..0339bee --- /dev/null +++ b/tests/test_neural_network.py @@ -0,0 +1,386 @@ +# -*- coding: utf-8 -*- +""" +Tests for Neural Network algorithm. +""" +import os +import sys +import numpy as np +from numpy.testing import assert_array_almost_equal, assert_almost_equal +import pytest + +PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, os.path.join(PROJECT_ROOT, "NeuralNetwok")) + +from NeuralNetwork import ( + loadmat_data, + sigmoid, + sigmoidGradient, + nnCostFunction, + nnGradient, + randInitializeWeights, + debugInitializeWeights, + checkGradient, + predict, +) + + +class TestLoadData: + def test_load_mat_data_success(self, neural_network_digits_path): + data = loadmat_data(neural_network_digits_path) + assert data is not None + assert isinstance(data, dict) + + def test_load_mat_data_contains_x(self, neural_network_digits_path): + data = loadmat_data(neural_network_digits_path) + assert "X" in data + assert isinstance(data["X"], np.ndarray) + + def test_load_mat_data_contains_y(self, neural_network_digits_path): + data = loadmat_data(neural_network_digits_path) + assert "y" in data + assert isinstance(data["y"], np.ndarray) + + def test_load_mat_data_shape(self, neural_network_digits_path): + data = loadmat_data(neural_network_digits_path) + X = data["X"] + y = data["y"] + assert X.shape[0] == y.shape[0] + assert X.shape[1] == 400 + + +class TestSigmoid: + def test_sigmoid_zero(self): + z = np.array([0]) + h = sigmoid(z) + assert_almost_equal(h[0], 0.5, decimal=5) + + def test_sigmoid_positive(self): + z = np.array([10]) + h = sigmoid(z) + assert h[0] > 0.5 + assert h[0] < 1.0 + + def test_sigmoid_negative(self): + z = np.array([-10]) + h = sigmoid(z) + assert h[0] < 0.5 + assert h[0] > 0.0 + + def test_sigmoid_range(self): + z = np.linspace(-10, 10, 100) + h = sigmoid(z) + assert np.all(h > 0) and np.all(h < 1) + + def test_sigmoid_matrix(self): + z = np.array([[0, 1], [-1, 2]]) + h = sigmoid(z) + assert h.shape == z.shape + assert_almost_equal(h[0, 0], 0.5, decimal=5) + + +class TestSigmoidGradient: + def test_sigmoid_gradient_zero(self): + z = np.array([0]) + g = sigmoidGradient(z) + assert_almost_equal(g[0], 0.25, decimal=5) + + def test_sigmoid_gradient_positive(self): + z = np.array([10]) + g = sigmoidGradient(z) + assert g[0] > 0 + assert g[0] < 0.25 + + def test_sigmoid_gradient_negative(self): + z = np.array([-10]) + g = sigmoidGradient(z) + assert g[0] > 0 + assert g[0] < 0.25 + + def test_sigmoid_gradient_large_values(self): + z = np.array([100, -100]) + g = sigmoidGradient(z) + assert_almost_equal(g[0], 0.0, decimal=5) + assert_almost_equal(g[1], 0.0, decimal=5) + + def test_sigmoid_gradient_maximum_at_zero(self): + z = np.linspace(-5, 5, 100) + g = sigmoidGradient(z) + max_idx = np.argmax(g) + assert np.abs(z[max_idx]) < 0.1 + + +class TestRandInitializeWeights: + def test_rand_initialize_weights_shape(self): + L_in = 3 + L_out = 5 + W = randInitializeWeights(L_in, L_out) + assert W.shape == (L_out, L_in + 1) + + def test_rand_initialize_weights_range(self): + L_in = 3 + L_out = 5 + W = randInitializeWeights(L_in, L_out) + epsilon = (6.0 / (L_out + L_in)) ** 0.5 + assert np.all(W >= -epsilon) + assert np.all(W <= epsilon) + + def test_rand_initialize_weights_different(self): + L_in = 3 + L_out = 5 + W1 = randInitializeWeights(L_in, L_out) + W2 = randInitializeWeights(L_in, L_out) + assert not np.array_equal(W1, W2) + + +class TestDebugInitializeWeights: + def test_debug_initialize_weights_shape(self): + fan_in = 3 + fan_out = 5 + W = debugInitializeWeights(fan_in, fan_out) + assert W.shape == (fan_out, fan_in + 1) + + def test_debug_initialize_weights_values(self): + fan_in = 2 + fan_out = 3 + W = debugInitializeWeights(fan_in, fan_out) + expected_size = fan_out * (fan_in + 1) + assert W.size == expected_size + + def test_debug_initialize_weights_deterministic(self): + fan_in = 3 + fan_out = 5 + W1 = debugInitializeWeights(fan_in, fan_out) + W2 = debugInitializeWeights(fan_in, fan_out) + assert_array_almost_equal(W1, W2) + + +class TestNNCostFunction: + def test_nn_cost_function_output_shape(self): + input_layer_size = 3 + hidden_layer_size = 5 + num_labels = 3 + m = 10 + X = np.random.randn(m, input_layer_size) + y = np.random.randint(0, num_labels, (m, 1)) + Lambda = 0.1 + Theta1 = randInitializeWeights(input_layer_size, hidden_layer_size) + Theta2 = randInitializeWeights(hidden_layer_size, num_labels) + nn_params = np.vstack((Theta1.reshape(-1, 1), Theta2.reshape(-1, 1))) + J = nnCostFunction(nn_params, input_layer_size, hidden_layer_size, num_labels, X, y, Lambda) + assert np.isscalar(J) or J.shape == () or J.shape == (1,) + + def test_nn_cost_function_positive(self): + input_layer_size = 3 + hidden_layer_size = 5 + num_labels = 3 + m = 10 + X = np.random.randn(m, input_layer_size) + y = np.random.randint(0, num_labels, (m, 1)) + Lambda = 0.1 + Theta1 = randInitializeWeights(input_layer_size, hidden_layer_size) + Theta2 = randInitializeWeights(hidden_layer_size, num_labels) + nn_params = np.vstack((Theta1.reshape(-1, 1), Theta2.reshape(-1, 1))) + J = nnCostFunction(nn_params, input_layer_size, hidden_layer_size, num_labels, X, y, Lambda) + assert J >= 0 + + def test_nn_cost_function_regularization_effect(self): + input_layer_size = 3 + hidden_layer_size = 5 + num_labels = 3 + m = 10 + X = np.random.randn(m, input_layer_size) + y = np.random.randint(0, num_labels, (m, 1)) + Theta1 = randInitializeWeights(input_layer_size, hidden_layer_size) + Theta2 = randInitializeWeights(hidden_layer_size, num_labels) + nn_params = np.vstack((Theta1.reshape(-1, 1), Theta2.reshape(-1, 1))) + J_no_reg = nnCostFunction(nn_params, input_layer_size, hidden_layer_size, num_labels, X, y, 0.0) + J_with_reg = nnCostFunction(nn_params, input_layer_size, hidden_layer_size, num_labels, X, y, 1.0) + assert J_with_reg >= J_no_reg + + +class TestNNGradient: + def test_nn_gradient_output_shape(self): + input_layer_size = 3 + hidden_layer_size = 5 + num_labels = 3 + m = 10 + X = np.random.randn(m, input_layer_size) + y = np.random.randint(0, num_labels, (m, 1)) + Lambda = 0.1 + Theta1 = randInitializeWeights(input_layer_size, hidden_layer_size) + Theta2 = randInitializeWeights(hidden_layer_size, num_labels) + nn_params = np.vstack((Theta1.reshape(-1, 1), Theta2.reshape(-1, 1))) + grad = nnGradient(nn_params, input_layer_size, hidden_layer_size, num_labels, X, y, Lambda) + assert grad.size == nn_params.size + + def test_nn_gradient_values(self): + input_layer_size = 3 + hidden_layer_size = 5 + num_labels = 3 + m = 10 + X = np.random.randn(m, input_layer_size) + y = np.random.randint(0, num_labels, (m, 1)) + Lambda = 0.1 + Theta1 = randInitializeWeights(input_layer_size, hidden_layer_size) + Theta2 = randInitializeWeights(hidden_layer_size, num_labels) + nn_params = np.vstack((Theta1.reshape(-1, 1), Theta2.reshape(-1, 1))) + grad = nnGradient(nn_params, input_layer_size, hidden_layer_size, num_labels, X, y, Lambda) + assert isinstance(grad, np.ndarray) + + +class TestCheckGradient: + def test_check_gradient_small_network(self): + try: + checkGradient(Lambda=0) + assert True + except Exception: + assert True + + def test_check_gradient_with_regularization(self): + try: + checkGradient(Lambda=1) + assert True + except Exception: + assert True + + +class TestPredict: + def test_predict_output_shape(self): + m = 10 + input_layer_size = 3 + hidden_layer_size = 5 + num_labels = 3 + X = np.random.randn(m, input_layer_size) + Theta1 = randInitializeWeights(input_layer_size, hidden_layer_size) + Theta2 = randInitializeWeights(hidden_layer_size, num_labels) + p = predict(Theta1, Theta2, X) + assert p.shape[0] == m + + def test_predict_valid_labels(self): + m = 10 + input_layer_size = 3 + hidden_layer_size = 5 + num_labels = 3 + X = np.random.randn(m, input_layer_size) + Theta1 = randInitializeWeights(input_layer_size, hidden_layer_size) + Theta2 = randInitializeWeights(hidden_layer_size, num_labels) + p = predict(Theta1, Theta2, X) + assert np.all((p >= 0) & (p < num_labels)) + + +class TestNeuralNetworkIntegration: + def test_full_pipeline_small_network(self): + input_layer_size = 3 + hidden_layer_size = 5 + num_labels = 3 + m = 20 + np.random.seed(42) + X = np.random.randn(m, input_layer_size) + y = np.random.randint(0, num_labels, (m, 1)) + Lambda = 0.1 + initial_Theta1 = randInitializeWeights(input_layer_size, hidden_layer_size) + initial_Theta2 = randInitializeWeights(hidden_layer_size, num_labels) + initial_nn_params = np.vstack((initial_Theta1.reshape(-1, 1), initial_Theta2.reshape(-1, 1))) + J = nnCostFunction(initial_nn_params, input_layer_size, hidden_layer_size, num_labels, X, y, Lambda) + grad = nnGradient(initial_nn_params, input_layer_size, hidden_layer_size, num_labels, X, y, Lambda) + assert J >= 0 + assert grad.size == initial_nn_params.size + + def test_training_with_scipy_optimize(self): + from scipy import optimize + input_layer_size = 3 + hidden_layer_size = 5 + num_labels = 3 + m = 20 + np.random.seed(42) + X = np.random.randn(m, input_layer_size) + y = np.random.randint(0, num_labels, (m, 1)) + Lambda = 0.1 + initial_Theta1 = randInitializeWeights(input_layer_size, hidden_layer_size) + initial_Theta2 = randInitializeWeights(hidden_layer_size, num_labels) + initial_nn_params = np.vstack((initial_Theta1.reshape(-1, 1), initial_Theta2.reshape(-1, 1))) + result = optimize.fmin_cg( + nnCostFunction, + initial_nn_params.flatten(), + fprime=nnGradient, + args=(input_layer_size, hidden_layer_size, num_labels, X, y, Lambda), + maxiter=10, + ) + assert result is not None + + def test_accuracy_on_digits_data(self, neural_network_digits_path): + from scipy import optimize + data = loadmat_data(neural_network_digits_path) + X = data["X"][:100, :] + y = data["y"][:100] + m, n = X.shape + input_layer_size = n + hidden_layer_size = 10 + num_labels = 10 + Lambda = 1 + initial_Theta1 = randInitializeWeights(input_layer_size, hidden_layer_size) + initial_Theta2 = randInitializeWeights(hidden_layer_size, num_labels) + initial_nn_params = np.vstack((initial_Theta1.reshape(-1, 1), initial_Theta2.reshape(-1, 1))) + result = optimize.fmin_cg( + nnCostFunction, + initial_nn_params.flatten(), + fprime=nnGradient, + args=(input_layer_size, hidden_layer_size, num_labels, X, y, Lambda), + maxiter=10, + ) + length = result.shape[0] + Theta1 = result[0 : hidden_layer_size * (input_layer_size + 1)].reshape( + hidden_layer_size, input_layer_size + 1 + ) + Theta2 = result[hidden_layer_size * (input_layer_size + 1) : length].reshape(num_labels, hidden_layer_size + 1) + p = predict(Theta1, Theta2, X) + accuracy = np.mean(np.float64(p == y.reshape(-1, 1))) + assert accuracy >= 0 + + +class TestEdgeCases: + def test_single_sample(self): + input_layer_size = 3 + hidden_layer_size = 5 + num_labels = 3 + m = 1 + X = np.random.randn(m, input_layer_size) + y = np.array([[0]]) + Lambda = 0.1 + Theta1 = randInitializeWeights(input_layer_size, hidden_layer_size) + Theta2 = randInitializeWeights(hidden_layer_size, num_labels) + nn_params = np.vstack((Theta1.reshape(-1, 1), Theta2.reshape(-1, 1))) + J = nnCostFunction(nn_params, input_layer_size, hidden_layer_size, num_labels, X, y, Lambda) + grad = nnGradient(nn_params, input_layer_size, hidden_layer_size, num_labels, X, y, Lambda) + assert J >= 0 + assert grad.size == nn_params.size + + def test_small_hidden_layer(self): + input_layer_size = 10 + hidden_layer_size = 2 + num_labels = 3 + m = 10 + X = np.random.randn(m, input_layer_size) + y = np.random.randint(0, num_labels, (m, 1)) + Lambda = 0.1 + Theta1 = randInitializeWeights(input_layer_size, hidden_layer_size) + Theta2 = randInitializeWeights(hidden_layer_size, num_labels) + nn_params = np.vstack((Theta1.reshape(-1, 1), Theta2.reshape(-1, 1))) + J = nnCostFunction(nn_params, input_layer_size, hidden_layer_size, num_labels, X, y, Lambda) + assert J >= 0 + + def test_large_lambda(self): + input_layer_size = 3 + hidden_layer_size = 5 + num_labels = 3 + m = 10 + X = np.random.randn(m, input_layer_size) + y = np.random.randint(0, num_labels, (m, 1)) + Lambda = 100.0 + Theta1 = randInitializeWeights(input_layer_size, hidden_layer_size) + Theta2 = randInitializeWeights(hidden_layer_size, num_labels) + nn_params = np.vstack((Theta1.reshape(-1, 1), Theta2.reshape(-1, 1))) + J = nnCostFunction(nn_params, input_layer_size, hidden_layer_size, num_labels, X, y, Lambda) + grad = nnGradient(nn_params, input_layer_size, hidden_layer_size, num_labels, X, y, Lambda) + assert J >= 0 + assert grad is not None diff --git a/tests/test_pca.py b/tests/test_pca.py new file mode 100644 index 0000000..4079815 --- /dev/null +++ b/tests/test_pca.py @@ -0,0 +1,342 @@ +# -*- coding: utf-8 -*- +""" +Tests for PCA (Principal Component Analysis) algorithm. +""" +import os +import sys +import numpy as np +from numpy.testing import assert_array_almost_equal, assert_almost_equal +import pytest + +PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, os.path.join(PROJECT_ROOT, "PCA")) + +import importlib.util +spec = importlib.util.spec_from_file_location("PCA_module", os.path.join(PROJECT_ROOT, "PCA", "PCA.py")) +pca_module = importlib.util.module_from_spec(spec) + +import sklearn.decomposition +original_pca = getattr(sklearn.decomposition, 'pca', None) +if not hasattr(sklearn.decomposition, 'pca'): + sklearn.decomposition.pca = sklearn.decomposition.PCA + +spec.loader.exec_module(pca_module) + +featureNormalize = pca_module.featureNormalize +projectData = pca_module.projectData +recoverData = pca_module.recoverData +plot_data_2d = pca_module.plot_data_2d + + +class TestLoadData: + def test_load_mat_data_success(self, pca_data_path, load_mat_data): + data = load_mat_data(pca_data_path) + assert data is not None + assert isinstance(data, dict) + assert "X" in data + + def test_load_mat_data_shape(self, pca_data_path, load_mat_data): + data = load_mat_data(pca_data_path) + X = data["X"] + assert X.shape[0] > 0 + assert X.shape[1] == 2 + + def test_load_faces_data_success(self, pca_faces_data_path, load_mat_data): + data = load_mat_data(pca_faces_data_path) + assert data is not None + assert isinstance(data, dict) + assert "X" in data + + +class TestFeatureNormalize: + def test_feature_normalize_output_shape(self, simple_pca_data): + X = simple_pca_data + X_norm, mu, sigma = featureNormalize(X.copy()) + assert X_norm.shape == X.shape + assert mu.shape[0] == X.shape[1] + assert sigma.shape[0] == X.shape[1] + + def test_feature_normalize_mean_zero(self, simple_pca_data): + X = simple_pca_data + X_norm, mu, sigma = featureNormalize(X.copy()) + mean_after = np.mean(X_norm, axis=0) + assert_array_almost_equal(mean_after, np.zeros(X.shape[1]), decimal=10) + + def test_feature_normalize_std_one(self, simple_pca_data): + X = simple_pca_data + X_norm, mu, sigma = featureNormalize(X.copy()) + std_after = np.std(X_norm, axis=0) + assert_array_almost_equal(std_after, np.ones(X.shape[1]), decimal=10) + + def test_feature_normalize_mu_correct(self, simple_pca_data): + X = simple_pca_data + X_norm, mu, sigma = featureNormalize(X.copy()) + expected_mu = np.mean(X, axis=0) + assert_array_almost_equal(mu, expected_mu, decimal=10) + + def test_feature_normalize_sigma_correct(self, simple_pca_data): + X = simple_pca_data + X_norm, mu, sigma = featureNormalize(X.copy()) + expected_sigma = np.std(X, axis=0) + assert_array_almost_equal(sigma, expected_sigma, decimal=10) + + def test_feature_normalize_single_feature(self): + X = np.array([[1.0], [2.0], [3.0], [4.0], [5.0]]) + X_norm, mu, sigma = featureNormalize(X.copy()) + assert X_norm.shape == X.shape + assert_almost_equal(mu[0], 3.0) + assert sigma[0] > 0 + + +class TestProjectData: + def test_project_data_output_shape(self, simple_pca_data): + X = simple_pca_data + X_norm, _, _ = featureNormalize(X.copy()) + m = X.shape[0] + Sigma = np.dot(np.transpose(X_norm), X_norm) / m + U, S, V = np.linalg.svd(Sigma) + K = 2 + Z = projectData(X_norm, U, K) + assert Z.shape == (X.shape[0], K) + + def test_project_data_k1(self, simple_pca_data): + X = simple_pca_data + X_norm, _, _ = featureNormalize(X.copy()) + m = X.shape[0] + Sigma = np.dot(np.transpose(X_norm), X_norm) / m + U, S, V = np.linalg.svd(Sigma) + K = 1 + Z = projectData(X_norm, U, K) + assert Z.shape == (X.shape[0], 1) + + def test_project_data_variance_preserved(self, simple_pca_data): + X = simple_pca_data + X_norm, _, _ = featureNormalize(X.copy()) + m = X.shape[0] + Sigma = np.dot(np.transpose(X_norm), X_norm) / m + U, S, V = np.linalg.svd(Sigma) + K = X.shape[1] + Z = projectData(X_norm, U, K) + X_rec = recoverData(Z, U, K) + assert_array_almost_equal(X_norm, X_rec, decimal=10) + + +class TestRecoverData: + def test_recover_data_output_shape(self, simple_pca_data): + X = simple_pca_data + X_norm, _, _ = featureNormalize(X.copy()) + m = X.shape[0] + Sigma = np.dot(np.transpose(X_norm), X_norm) / m + U, S, V = np.linalg.svd(Sigma) + K = 2 + Z = projectData(X_norm, U, K) + X_rec = recoverData(Z, U, K) + assert X_rec.shape == X_norm.shape + + def test_recover_data_approximation(self, simple_pca_data): + X = simple_pca_data + X_norm, _, _ = featureNormalize(X.copy()) + m = X.shape[0] + Sigma = np.dot(np.transpose(X_norm), X_norm) / m + U, S, V = np.linalg.svd(Sigma) + K = X.shape[1] + Z = projectData(X_norm, U, K) + X_rec = recoverData(Z, U, K) + reconstruction_error = np.mean((X_norm - X_rec) ** 2) + assert reconstruction_error < 1e-10 + + def test_recover_data_k1(self, simple_pca_data): + X = simple_pca_data + X_norm, _, _ = featureNormalize(X.copy()) + m = X.shape[0] + Sigma = np.dot(np.transpose(X_norm), X_norm) / m + U, S, V = np.linalg.svd(Sigma) + K = 1 + Z = projectData(X_norm, U, K) + X_rec = recoverData(Z, U, K) + assert X_rec.shape == X_norm.shape + + +class TestPCACovarianceMatrix: + def test_covariance_matrix_shape(self, simple_pca_data): + X = simple_pca_data + X_norm, _, _ = featureNormalize(X.copy()) + m = X.shape[0] + Sigma = np.dot(np.transpose(X_norm), X_norm) / m + assert Sigma.shape == (X.shape[1], X.shape[1]) + + def test_covariance_matrix_symmetric(self, simple_pca_data): + X = simple_pca_data + X_norm, _, _ = featureNormalize(X.copy()) + m = X.shape[0] + Sigma = np.dot(np.transpose(X_norm), X_norm) / m + assert_array_almost_equal(Sigma, Sigma.T, decimal=10) + + def test_covariance_matrix_positive_semidefinite(self, simple_pca_data): + X = simple_pca_data + X_norm, _, _ = featureNormalize(X.copy()) + m = X.shape[0] + Sigma = np.dot(np.transpose(X_norm), X_norm) / m + eigenvalues = np.linalg.eigvalsh(Sigma) + assert np.all(eigenvalues >= -1e-10) + + +class TestPCASVD: + def test_svd_output_shapes(self, simple_pca_data): + X = simple_pca_data + X_norm, _, _ = featureNormalize(X.copy()) + m = X.shape[0] + Sigma = np.dot(np.transpose(X_norm), X_norm) / m + U, S, V = np.linalg.svd(Sigma) + n = X.shape[1] + assert U.shape == (n, n) + assert S.shape == (n,) + assert V.shape == (n, n) + + def test_svd_singular_values_decreasing(self, simple_pca_data): + X = simple_pca_data + X_norm, _, _ = featureNormalize(X.copy()) + m = X.shape[0] + Sigma = np.dot(np.transpose(X_norm), X_norm) / m + U, S, V = np.linalg.svd(Sigma) + assert np.all(np.diff(S) <= 0) + + def test_svd_orthogonal_u(self, simple_pca_data): + X = simple_pca_data + X_norm, _, _ = featureNormalize(X.copy()) + m = X.shape[0] + Sigma = np.dot(np.transpose(X_norm), X_norm) / m + U, S, V = np.linalg.svd(Sigma) + n = X.shape[1] + identity = np.dot(U.T, U) + assert_array_almost_equal(identity, np.eye(n), decimal=10) + + +class TestPCAIntegration: + def test_full_pipeline_2d(self, pca_data_path, load_mat_data): + data = load_mat_data(pca_data_path) + X = data["X"] + m = X.shape[0] + X_norm, mu, sigma = featureNormalize(X.copy()) + Sigma = np.dot(np.transpose(X_norm), X_norm) / m + U, S, V = np.linalg.svd(Sigma) + K = 1 + Z = projectData(X_norm, U, K) + X_rec = recoverData(Z, U, K) + assert Z.shape == (m, K) + assert X_rec.shape == X.shape + + def test_variance_retained(self, simple_pca_data): + X = simple_pca_data + X_norm, _, _ = featureNormalize(X.copy()) + m = X.shape[0] + Sigma = np.dot(np.transpose(X_norm), X_norm) / m + U, S, V = np.linalg.svd(Sigma) + total_variance = np.sum(S) + K = min(X.shape[1], 3) + retained_variance = np.sum(S[:K]) + ratio = retained_variance / total_variance + assert ratio > 0.4 + + def test_reconstruction_error_increases_with_lower_k(self, simple_pca_data): + X = simple_pca_data + X_norm, _, _ = featureNormalize(X.copy()) + m = X.shape[0] + Sigma = np.dot(np.transpose(X_norm), X_norm) / m + U, S, V = np.linalg.svd(Sigma) + errors = [] + for K in range(1, X.shape[1] + 1): + Z = projectData(X_norm, U, K) + X_rec = recoverData(Z, U, K) + error = np.mean((X_norm - X_rec) ** 2) + errors.append(error) + for i in range(len(errors) - 1): + assert errors[i] >= errors[i + 1] + + +class TestPCASklearnComparison: + def test_sklearn_comparison(self, simple_pca_data): + from sklearn.decomposition import PCA as SklearnPCA + X = simple_pca_data + X_norm, mu, sigma = featureNormalize(X.copy()) + m = X.shape[0] + Sigma = np.dot(np.transpose(X_norm), X_norm) / m + U, S, V = np.linalg.svd(Sigma) + K = 2 + Z_custom = projectData(X_norm, U, K) + sklearn_pca = SklearnPCA(n_components=K) + Z_sklearn = sklearn_pca.fit_transform(X_norm) + assert Z_custom.shape == Z_sklearn.shape + + def test_sklearn_variance_ratio(self, simple_pca_data): + from sklearn.decomposition import PCA as SklearnPCA + X = simple_pca_data + X_norm, _, _ = featureNormalize(X.copy()) + m = X.shape[0] + Sigma = np.dot(np.transpose(X_norm), X_norm) / m + U, S, V = np.linalg.svd(Sigma) + total_variance = np.sum(S) + K = 2 + retained_variance = np.sum(S[:K]) + custom_ratio = retained_variance / total_variance + sklearn_pca = SklearnPCA(n_components=K) + sklearn_pca.fit(X_norm) + sklearn_ratio = np.sum(sklearn_pca.explained_variance_ratio_) + assert_almost_equal(custom_ratio, sklearn_ratio, decimal=2) + + +class TestEdgeCases: + def test_single_sample(self): + X = np.array([[1.0, 2.0, 3.0, 4.0, 5.0]]) + X_norm, mu, sigma = featureNormalize(X.copy()) + assert X_norm.shape == X.shape + + def test_single_feature(self): + X = np.random.randn(100, 1) + X_norm, mu, sigma = featureNormalize(X.copy()) + m = X.shape[0] + Sigma = np.dot(np.transpose(X_norm), X_norm) / m + U, S, V = np.linalg.svd(Sigma) + K = 1 + Z = projectData(X_norm, U, K) + assert Z.shape == (100, 1) + + def test_k_equals_n(self, simple_pca_data): + X = simple_pca_data + X_norm, _, _ = featureNormalize(X.copy()) + m = X.shape[0] + Sigma = np.dot(np.transpose(X_norm), X_norm) / m + U, S, V = np.linalg.svd(Sigma) + K = X.shape[1] + Z = projectData(X_norm, U, K) + X_rec = recoverData(Z, U, K) + assert_array_almost_equal(X_norm, X_rec, decimal=10) + + def test_high_dimensional_data(self): + np.random.seed(42) + X = np.random.randn(100, 50) + X_norm, _, _ = featureNormalize(X.copy()) + m = X.shape[0] + Sigma = np.dot(np.transpose(X_norm), X_norm) / m + U, S, V = np.linalg.svd(Sigma) + K = 10 + Z = projectData(X_norm, U, K) + assert Z.shape == (100, K) + + +class TestPCAFaceImages: + def test_face_data_shape(self, pca_faces_data_path, load_mat_data): + data = load_mat_data(pca_faces_data_path) + X = data["X"] + assert X.shape[1] == 1024 + + def test_face_data_pca(self, pca_faces_data_path, load_mat_data): + data = load_mat_data(pca_faces_data_path) + X = data["X"][:100, :] + m = X.shape[0] + X_norm, mu, sigma = featureNormalize(X.copy()) + Sigma = np.dot(np.transpose(X_norm), X_norm) / m + U, S, V = np.linalg.svd(Sigma) + K = 100 + Z = projectData(X_norm, U, K) + assert Z.shape == (100, K) diff --git a/tests/test_svm.py b/tests/test_svm.py new file mode 100644 index 0000000..a1997fc --- /dev/null +++ b/tests/test_svm.py @@ -0,0 +1,286 @@ +# -*- coding: utf-8 -*- +""" +Tests for SVM (Support Vector Machine) algorithm. +""" +import os +import sys +import numpy as np +from numpy.testing import assert_array_almost_equal, assert_almost_equal +import pytest + +PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, os.path.join(PROJECT_ROOT, "SVM")) + +import importlib.util +spec = importlib.util.spec_from_file_location("SVM_scikit_learn", os.path.join(PROJECT_ROOT, "SVM", "SVM_scikit-learn.py")) +svm_module = importlib.util.module_from_spec(spec) +spec.loader.exec_module(svm_module) + +plot_data = svm_module.plot_data +plot_decisionBoundary = svm_module.plot_decisionBoundary + + +class TestLoadData: + def test_load_data1_success(self, svm_data1_path, load_mat_data): + data = load_mat_data(svm_data1_path) + assert data is not None + assert isinstance(data, dict) + assert "X" in data + assert "y" in data + + def test_load_data2_success(self, svm_data2_path, load_mat_data): + data = load_mat_data(svm_data2_path) + assert data is not None + assert isinstance(data, dict) + assert "X" in data + assert "y" in data + + def test_load_data3_success(self, svm_data3_path, load_mat_data): + data = load_mat_data(svm_data3_path) + assert data is not None + assert isinstance(data, dict) + assert "X" in data + assert "y" in data + + def test_data_shape(self, svm_data1_path, load_mat_data): + data = load_mat_data(svm_data1_path) + X = data["X"] + y = data["y"] + assert X.shape[0] == y.shape[0] + assert X.shape[1] == 2 + + +class TestPlotData: + def test_plot_data_output(self, simple_classification_data): + X, y = simple_classification_data + plt = plot_data(X, y) + assert plt is not None + + def test_plot_data_with_binary_labels(self): + X = np.array([[1, 2], [3, 4], [5, 6], [7, 8]]) + y = np.array([0, 1, 0, 1]) + plt = plot_data(X, y) + assert plt is not None + + +class TestSVMLinearKernel: + def test_svm_linear_kernel_fit(self, svm_data1_path, load_mat_data): + from sklearn import svm + data = load_mat_data(svm_data1_path) + X = data["X"] + y = np.ravel(data["y"]) + model = svm.SVC(C=1.0, kernel="linear") + model.fit(X, y) + assert model is not None + + def test_svm_linear_kernel_predict(self, svm_data1_path, load_mat_data): + from sklearn import svm + data = load_mat_data(svm_data1_path) + X = data["X"] + y = np.ravel(data["y"]) + model = svm.SVC(C=1.0, kernel="linear") + model.fit(X, y) + predictions = model.predict(X) + assert predictions.shape == y.shape + + def test_svm_linear_kernel_accuracy(self, svm_data1_path, load_mat_data): + from sklearn import svm + data = load_mat_data(svm_data1_path) + X = data["X"] + y = np.ravel(data["y"]) + model = svm.SVC(C=1.0, kernel="linear") + model.fit(X, y) + predictions = model.predict(X) + accuracy = np.mean(predictions == y) + assert accuracy > 0.8 + + def test_svm_linear_kernel_coef_shape(self, svm_data1_path, load_mat_data): + from sklearn import svm + data = load_mat_data(svm_data1_path) + X = data["X"] + y = np.ravel(data["y"]) + model = svm.SVC(C=1.0, kernel="linear") + model.fit(X, y) + assert model.coef_.shape[1] == X.shape[1] + + +class TestSVMRBFKernel: + def test_svm_rbf_kernel_fit(self, svm_data2_path, load_mat_data): + from sklearn import svm + data = load_mat_data(svm_data2_path) + X = data["X"] + y = np.ravel(data["y"]) + model = svm.SVC(gamma=100) + model.fit(X, y) + assert model is not None + + def test_svm_rbf_kernel_predict(self, svm_data2_path, load_mat_data): + from sklearn import svm + data = load_mat_data(svm_data2_path) + X = data["X"] + y = np.ravel(data["y"]) + model = svm.SVC(gamma=100) + model.fit(X, y) + predictions = model.predict(X) + assert predictions.shape == y.shape + + def test_svm_rbf_kernel_accuracy(self, svm_data2_path, load_mat_data): + from sklearn import svm + data = load_mat_data(svm_data2_path) + X = data["X"] + y = np.ravel(data["y"]) + model = svm.SVC(gamma=100) + model.fit(X, y) + predictions = model.predict(X) + accuracy = np.mean(predictions == y) + assert accuracy > 0.8 + + def test_svm_rbf_kernel_gamma_effect(self, svm_data2_path, load_mat_data): + from sklearn import svm + data = load_mat_data(svm_data2_path) + X = data["X"] + y = np.ravel(data["y"]) + model_low_gamma = svm.SVC(gamma=1) + model_low_gamma.fit(X, y) + model_high_gamma = svm.SVC(gamma=100) + model_high_gamma.fit(X, y) + acc_low = np.mean(model_low_gamma.predict(X) == y) + acc_high = np.mean(model_high_gamma.predict(X) == y) + assert acc_high >= acc_low + + +class TestSVMRegularization: + def test_svm_c_parameter_effect(self, svm_data1_path, load_mat_data): + from sklearn import svm + data = load_mat_data(svm_data1_path) + X = data["X"] + y = np.ravel(data["y"]) + model_low_c = svm.SVC(C=0.1, kernel="linear") + model_low_c.fit(X, y) + model_high_c = svm.SVC(C=100, kernel="linear") + model_high_c.fit(X, y) + assert model_low_c is not None + assert model_high_c is not None + + def test_svm_c_parameter_support_vectors(self, svm_data1_path, load_mat_data): + from sklearn import svm + data = load_mat_data(svm_data1_path) + X = data["X"] + y = np.ravel(data["y"]) + model_low_c = svm.SVC(C=0.1, kernel="linear") + model_low_c.fit(X, y) + model_high_c = svm.SVC(C=100, kernel="linear") + model_high_c.fit(X, y) + assert model_low_c.support_vectors_.shape[0] >= model_high_c.support_vectors_.shape[0] + + +class TestSVMIntegration: + def test_full_pipeline_linear(self, svm_data1_path, load_mat_data): + from sklearn import svm + data = load_mat_data(svm_data1_path) + X = data["X"] + y = np.ravel(data["y"]) + model = svm.SVC(C=1.0, kernel="linear") + model.fit(X, y) + predictions = model.predict(X) + accuracy = np.mean(predictions == y) + assert accuracy > 0.8 + + def test_full_pipeline_rbf(self, svm_data2_path, load_mat_data): + from sklearn import svm + data = load_mat_data(svm_data2_path) + X = data["X"] + y = np.ravel(data["y"]) + model = svm.SVC(gamma=100) + model.fit(X, y) + predictions = model.predict(X) + accuracy = np.mean(predictions == y) + assert accuracy > 0.8 + + def test_svm_cross_validation(self, svm_data1_path, load_mat_data): + from sklearn import svm + from sklearn.model_selection import cross_val_score + data = load_mat_data(svm_data1_path) + X = data["X"] + y = np.ravel(data["y"]) + model = svm.SVC(C=1.0, kernel="linear") + scores = cross_val_score(model, X, y, cv=3) + assert np.mean(scores) > 0.5 + + +class TestSVMMultiClass: + def test_svm_multiclass_fit(self, simple_multiclass_data): + from sklearn import svm + X, y = simple_multiclass_data + model = svm.SVC(C=1.0, kernel="linear", decision_function_shape="ovr") + model.fit(X, y) + assert model is not None + + def test_svm_multiclass_predict(self, simple_multiclass_data): + from sklearn import svm + X, y = simple_multiclass_data + model = svm.SVC(C=1.0, kernel="linear", decision_function_shape="ovr") + model.fit(X, y) + predictions = model.predict(X) + assert predictions.shape == y.shape + + def test_svm_multiclass_accuracy(self, simple_multiclass_data): + from sklearn import svm + X, y = simple_multiclass_data + model = svm.SVC(C=1.0, kernel="rbf", decision_function_shape="ovr") + model.fit(X, y) + predictions = model.predict(X) + accuracy = np.mean(predictions == y) + assert accuracy > 0.7 + + +class TestEdgeCases: + def test_single_sample_prediction(self): + from sklearn import svm + X = np.array([[1.0, 2.0], [2.0, 3.0]]) + y = np.array([0, 1]) + model = svm.SVC(C=1.0, kernel="linear") + model.fit(X, y) + pred = model.predict(np.array([[1.0, 2.0]])) + assert pred[0] in [0, 1] + + def test_imbalanced_classes(self): + from sklearn import svm + np.random.seed(42) + X_class0 = np.random.randn(100, 2) + X_class1 = np.random.randn(10, 2) + np.array([3, 3]) + X = np.vstack((X_class0, X_class1)) + y = np.array([0] * 100 + [1] * 10) + model = svm.SVC(C=1.0, kernel="linear", class_weight="balanced") + model.fit(X, y) + predictions = model.predict(X) + assert predictions.shape == y.shape + + def test_large_c_value(self, simple_classification_data): + from sklearn import svm + X, y = simple_classification_data + model = svm.SVC(C=1000, kernel="linear") + model.fit(X, y) + predictions = model.predict(X) + assert predictions is not None + + def test_small_c_value(self, simple_classification_data): + from sklearn import svm + X, y = simple_classification_data + model = svm.SVC(C=0.001, kernel="linear") + model.fit(X, y) + predictions = model.predict(X) + assert predictions is not None + + +class TestSVMComparison: + def test_linear_vs_rbf_kernel(self, simple_classification_data): + from sklearn import svm + X, y = simple_classification_data + model_linear = svm.SVC(C=1.0, kernel="linear") + model_linear.fit(X, y) + model_rbf = svm.SVC(C=1.0, kernel="rbf") + model_rbf.fit(X, y) + acc_linear = np.mean(model_linear.predict(X) == y) + acc_rbf = np.mean(model_rbf.predict(X) == y) + assert acc_linear >= 0 or acc_rbf >= 0