From c8d777969925bcc0ba2559362fb6981e6c95d1f3 Mon Sep 17 00:00:00 2001 From: w1n5t0n Date: Thu, 2 Apr 2026 20:35:24 +0100 Subject: [PATCH] feat(nisps-core,wasm): add per-sample weights to MLP training MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add optional sample_weights parameter to MLP::Train() and the WASM nisps_mlp_train binding. When provided, weights replace the uniform 1/N scaling per sample — enabling recency, spatial, or any custom importance weighting without changing the training interface. --- nisps-core/include/nisps/mlp.hpp | 3 ++- nisps-core/include/nisps/mlp_impl.hpp | 30 +++++++++++--------------- playground/wasm/nisps.wasm | Bin 33677 -> 33776 bytes playground/wasm/nisps_bindings.cpp | 11 +++++++++- 4 files changed, 24 insertions(+), 20 deletions(-) diff --git a/nisps-core/include/nisps/mlp.hpp b/nisps-core/include/nisps/mlp.hpp index cfa6dc7..ae459f6 100644 --- a/nisps-core/include/nisps/mlp.hpp +++ b/nisps-core/include/nisps/mlp.hpp @@ -130,7 +130,8 @@ public: float learning_rate, int max_iterations = 5000, float min_error_cost = 0.001, - bool output_log = true); + bool output_log = true, + const std::vector* sample_weights = nullptr); /** * @brief Training with batch support diff --git a/nisps-core/include/nisps/mlp_impl.hpp b/nisps-core/include/nisps/mlp_impl.hpp index 6d17d3b..8f9acf9 100644 --- a/nisps-core/include/nisps/mlp_impl.hpp +++ b/nisps-core/include/nisps/mlp_impl.hpp @@ -516,39 +516,33 @@ T MLP::Train(const training_pair_t& training_sample_set_with_bias, float learning_rate, int max_iterations, float min_error_cost, - bool) { + bool, + const std::vector* sample_weights) { int i = 0; T current_iteration_cost_function = 0.f; - T sampleSizeReciprocal = 1.f / training_sample_set_with_bias.first.size(); + const size_t n_samples = training_sample_set_with_bias.first.size(); + T sampleSizeReciprocal = 1.f / n_samples; for (i = 0; i < max_iterations; i++) { current_iteration_cost_function = 0.f; auto training_features = training_sample_set_with_bias.first; auto training_labels = training_sample_set_with_bias.second; - auto t_feat = training_features.begin(); - auto t_label = training_labels.begin(); - while (t_feat != training_features.end() || t_label != training_labels.end()) { + for (size_t s = 0; s < n_samples; s++) { + T weight = sample_weights ? (*sample_weights)[s] : sampleSizeReciprocal; - // Payload current_iteration_cost_function += - _TrainOnExample(*t_feat, *t_label, learning_rate, sampleSizeReciprocal); - - // \Payload - if (t_feat != training_features.end()) - { - ++t_feat; - } - if (t_label != training_labels.end()) - { - ++t_label; - } + _TrainOnExample(training_features[s], training_labels[s], learning_rate, weight); } - current_iteration_cost_function *= sampleSizeReciprocal; + // When using custom weights (already normalized to sum to 1), loss is already scaled. + // With uniform weights, multiply by sampleSizeReciprocal for backward compat. + if (!sample_weights) { + current_iteration_cost_function *= sampleSizeReciprocal; + } ReportProgress(true, 100, i, current_iteration_cost_function); diff --git a/playground/wasm/nisps.wasm b/playground/wasm/nisps.wasm index e7bd20cd3beba8678ddcaeb10819b75a64dd4eef..28e296c418628fa0e05d02834405961848001ab1 100755 GIT binary patch delta 2701 zcmcgu&yU+g7#)utll9n%lQ>IuH*LNda)@9BJ*A~ZbO-3R62F9yID+8lv=Rtr!vk6A%YXL!-jg(A`m`h!*8B5D}LRk-;CSzPZg7MQW z<;e+Uk?Mb-+`mtGYDC${E8Bt51Qh2A@*}awDbtQ%P;M8Fx?xlJuj?E2N}*m<>eI`T z9)D}Ck61h(y}D=5#IR`l$LA7MJv<&CCI>)B@juR^pySG=4^%IO5M!I7SlC7B)3#VK z!zjU^TyhmUsz#EPQ(j#w5&$baH(>Qr!Js%JgWBZ`p|E`bKdQERsbCh>18{9qxEo2r z$ibFiHb8b@Te=~K+^LpyjHPXo9*SDwG~~@G&&5FBwlP>^8zJMCwQ4PfLAi{q;jWcS zTGDZ_9J$~eVL*0R2btc+aygu~wp=Wj1!=Oiu_)lz&|?@_qY6q{ksaAREHqeHJCh0E zCIFTJkJFP{*%^*L=~ZG#FPAaaaFhZ0XrhWlV}Ub}Ujkd>5<|`zX>d5esS5J))U4$- zWQqe2pt>!Tl3cbQM9Et@(5oIs!I2~i4k%Cng7ehY5aQei$r|dRZ?$bPhg);zwl6tz zo40q;-c2NO2nXpfjV6=(N$pixpwh-AsFC0YRqaW>&vn(5y9#=GTE~;xUS=8 z9~4TWg+6fV3a*TFn1qqp1&R!>PHX&3>L55W9V7^{4d__5;R)!4PO#Q%z-j6gI$!LQ zW)%b*Pz=RxfoH%60XQ?p_Eg4g6f{9}vrWn=qabVa&yDIKyO@0vkAxs&2HBF?+puGY zulxL2E>KGQ!&wH`CBFkt!UF7ZORb zs2NFy29a$@250f(A__zrMzYAa;H!MfSn!4+Z(Qc5G0Ojn!9XVvCF8>3YlsH+qs7Cw zz=CVAUq<)}v-}J|Qhg>B4!OZ(pGg@i-`^w^f@Sy^%cN?RuNY#4-Td}J2z=w63Q?Yf aSd_LBVN_~7Ml2mN>BpzMSD^3&%EVwL<*ihrYi?VUU#TlHjp^)|#*O)~YfNkPksY(0su86B z)kOpp*TVBYpU~Ek4MhXmhBUZ|Xb4^908*}GzG&$+~ij7xT$SEe>^46+0 z@E-HIh+>eU1ev7@ZAfN)xEhaQ3UNxVRERTZ1jXwYFVFgz0?cWK-PSSl1Ee&53}fL~ zRTVp1Df2WiZry=d(BwbBES0c>8(w&sYq7E8S8!wS; zUx2}=wNns4_!CI=omx8G9MWzh-cH3ELVR|Wj5QVP{iMV@4@x1UWfn%#q=SK{*pCb|*+#>S@dfv$^&TBJTiq zogIK`PSexu_Z+ozAWk`m=qv}Jh6>Nu3n=hXXNOSWkOR6%Lk?))kI0Xv6d-RINW%0! z|4rLQaKy3`fo6jF=ZiLmGKr47v%8T8oTT zG-5&;K_ge+hMY$55pJCW#)1Sq6p5%84o!WIZ_@hL*QRd`S#=4sH6k<{QIA;Euz3^J zHKYVyN9+k*_Bvas1tp9NX-j+>LYl3Q4lefRU;}8l=7mkw(hi!a&2!!lQ6TclGQr3n z-*1c;Go<@f28==E+`OPneStzy8$>wI3C8k;%Q%2Q*go!S(!35QPxw=`vwn2&P7g+n z4dG|Vn$4NvYK9UCKj=;^qfd|}3>HoxjE&z8wr`nH!0rf93-rPcvoo-d;|Z~%xQer( znp@T=h+Qh0k#H`0PUwmIC9hrFx^FJEkk7&>-F%Za>$rIoHb3heB_WT5Muswmz#QQo zIEjLm4M$iqkC$*gTv(32IW4}#{X-Rl6nfmd8}qhCp>ku~MIK7j4o&B2{#8AP8cp-u z-BeJpREJg`p_h^2NHUtg0;(l6yui~1`MQgWaElP}#();XzDp=-L1$exbmfgn(<{}Z zj%-=e%!uTAL9r_p6!T04D*>(ptr); @@ -104,8 +105,16 @@ float nisps_mlp_train(void* ptr, labels_flat + (i + 1) * label_dim); } + // Build weights vector if provided (non-null pointer) + std::vector weights_vec; + const std::vector* weights_ptr = nullptr; + if (sample_weights) { + weights_vec.assign(sample_weights, sample_weights + n_samples); + weights_ptr = &weights_vec; + } + nisps::MLP::training_pair_t data(features, labels); - return mlp->Train(data, learning_rate, max_iterations, min_error, false); + return mlp->Train(data, learning_rate, max_iterations, min_error, false, weights_ptr); } // ---- Weight manipulation with spread ----