Repository navigation
NumSharp 0.80.0 on net8.0 / net10.0: bug fixes, a scikit-learn oracle, faster and leaner estimators - #3
Open
Nucs wants to merge 12 commits into
Open
NumSharp 0.80.0 on net8.0 / net10.0: bug fixes, a scikit-learn oracle, faster and leaner estimators#3Nucs wants to merge 12 commits into
Nucs wants to merge 12 commits into
Conversation
…NN, linear regression, MLP, ID3 and Baum-Welch; libsvm-exact SVM; every test runs (65/65 on both frameworks)
Bigtree.Algorithm 0.4.0 (2021-04, NumSharp 0.30.0, netstandard2.0, tests on netcoreapp2.1) used NumSharp in two
places only: Utils.ReadCsv (np.array + reshape) and the neural network, which indexed NDArrays one element at a
time per neuron. Gaussian/Bernoulli Naive Bayes, KNN, linear regression and the CNN were empty classes. As cloned,
6 of 20 tests passed (run on .NET 10 by roll-forward), and none of the 6 checked a result.
Design (decided with the owner on 2026-11-08, decision pages): keep every member CherubNLP calls (its only
consumer: MultinomiaNaiveBayes, the SVM, Probability/Feature, CRFLite.Utils, Shuffle/Split) source-compatible with
bit-identical numbers; give everything else an NDArray-first API with scikit-learn's semantics (Fit / Predict /
PredictProba / Score); NumSharp 0.80.0 from nuget.org; restructure each algorithm into whole-array operations,
because NumSharp spends ~1.5-3 us per call (a 5-element op costs ~100 scalar multiply-adds), so per-element or
per-sample NumSharp calls are a regression, not an optimization.
Build
- Bigtree.Algorithm: net8.0;net10.0 (NumSharp 0.80.0 ships only those), NumSharp 0.80.0, NumSharp.Build 0.80.0
(PrivateAssets=all: weaves [NDScoped] so temporaries return to the pool at method exit), version 0.5.0,
PackageLicenseExpression Apache-2.0 (PackageLicenseUrl is deprecated, NU5125), XML docs shipped.
- .editorconfig silences CS1570/1572/1573/1587 for HiddenMarkovModel/MathUtils only: the ported statistics
library's doc comments (unescaped '<', stale <param> tags) surface once the XML docs are generated.
- Tests: net8.0;net10.0, MSTest 3.8.3 (as NumSharp); MatplotlibCS (plotted from a path on the author's disk) and
Microsoft.Extensions.Configuration (read a Settings/*.json never committed) removed.
- Console: net10.0 tour of the estimators on the seeds dataset.
- Solution builds with 0 warnings, 0 errors.
Estimators (NumSharp)
- MultinomiaNaiveBayes: same public API; CalCondProb reads a per-value (labels, features) table built ONCE by
onehot(labels).T @ (X == value) (labels via np.unique on a text array). 0.4.0 filtered FeatureSet and copied the
label's matrix on EVERY call, so CherubNLP's training pass cost O(labels * rows * features^2). Measured
(prototype, Release): 300x100x4 52.8 ms -> 0.64 ms (x83), 1000x300x5 829.8 ms -> 2.91 ms (x285); all
log-probabilities bit-identical (counts are exact integers in float64, same formula order). CalPosteriorProb
builds the dictionary key only for features equal to 1 (0.4.0 formatted one per feature). Thread-safe lazy tables
(lock); rebuilt when FeatureSet is another list or its Count changed (documented footgun: in-place row edits).
- GaussianNaiveBayes (was empty): scikit-learn 1.8 GaussianNB operation for operation - per-class np.mean/np.var,
epsilon = var_smoothing * max(var(X, 0)), the per-class jll loop, stack(...).T layout, scikit-learn's own
log1p-based _logsumexp (NDHelpers.LogSumExp). Priors validated like sklearn. Text labels work.
- BernoulliNaiveBayes (was empty): scikit-learn BernoulliNB (binarize, alpha, fit_prior, class_prior); counts =
onehot.T @ X, jll = X @ (log p - log(1-p)).T + log prior + sum log(1-p).
- KnnModel (was empty): brute force |x|^2 - 2 x.t + |t|^2 (one matmul), np.argpartition for the k nearest,
one-hot vote; KNeighbors returns sorted distances/indices; vote ties -> first class (sklearn).
- LinearRegressionModel (was empty): centered normal equations solved by np.linalg.solve (managed LU, no native
backend), multi-output, R^2 Score with r2_score's constant-target rule; collinear features -> InvalidOperation
(lstsq/SVD needs NumSharp's OpenBLAS package; documented conditioning footgun).
- NetworkModel: one NDArray weight matrix per layer, mini-batch backprop as matrix products (batchSize, default 32;
1 = per-sample SGD exactly, verified against a scalar implementation to 1e-12), Predict over all rows at once,
Build(seed) uses np.random.default_rng (NumPy's PCG64). Fixes 0.4.0's two backprop bugs: the hidden error summed
deltas of the wrong layers, and every layer's update used the raw input x instead of its own input. Neuron and
Dendrite removed (no outside user); NeuralLayer = (Count, Name). Seeds 4-fold CV: 97.5% train / 95.2% test in 2 s
(0.4.0's own commit reported 96.75 / 89.50 before the bugs).
- MathFunctions.Function: NDArray overloads of Sigmoid / SigmoidDerivative.
- Utils: ReadCsv = np.loadtxt + np.unique (labels numbered in ascending value order; culture-invariant);
CrossValFolds(N, n_folds, seed) returns NDArray[] index folds from Generator.permutation (0.4.0's body was
commented out and returned no folds); Normalize = per-column min-max (what NumSharp 0.30's NDArray.Normalize did).
- DecisionTree (ID3): each column becomes a NumSharp text array; np.unique + np.bincount count the whole
(value x label) table per attribute in one pass; multi-class Shannon entropy (0.4.0 used "first row's label vs
rest" for attributes but not for the table); log2 taken as log/log(2) like Math.Log(p, 2) so two-label gains are
unchanged; edges in first-appearance order. Fixes: subtables kept only the columns RIGHT of the split
(Skip(TableIndex + 1)); contradictory rows now end in a majority leaf instead of an IndexOutOfRange.
- HMM Baum-Welch (discrete): forward/backward recursions stay scalar per sequence (sequential in t, N-sized), the
E-step (gamma (rows, N), ksi (transitions, N, N)) and the M-step (pi, A by log-sum-exp; B = exp(gamma - max).T @
onehot(obs)) run on NumSharp over all sequences stacked, one [NDScoped] call per iteration. Matches 0.4.0's scalar
re-estimation (verbatim reference in the tests) to 1e-9, weights included. Fixes: the convergence log-likelihood
was reset per sequence (only the last one counted) - now Accord.NET's log(sum of likelihoods) / K; validation of
empty input. The continuous variant keeps the shared mLogGamma/mLogKsi scratch.
- ForwardBackwardAlgorithm: the scaled Backward looped "for (i = 0; i < N; ++N)" (never ended, then overran); the
scaled Forward never stored scale_vector[0], so its log-likelihood was always -inf and Backward divided by 0.
SVM (libsvm 3.x port; solver stays scalar)
- RangeTransform.Compute sized its arrays by MaxIndex as if 0-based and swallowed the overflow of libsvm's 1-based
last feature in an empty catch; now covers max(MaxIndex - 1, largest index), so 0-based (CherubNLP, unchanged file
length) and 1-based problems both scale.
- Kernel.KernelFunction (the PREDICTION kernel) computed POLY as (Degree * <x,y> + coef0)^degree; training and libsvm
use Gamma. Every polynomial prediction used another kernel than the trained one.
- Model.Write wrote every real as 0.000000 (gamma, coef0, rho, probA/B, coefficients, SV values): a saved+reloaded
model (CherubNLP's SaveModel/LoadModel) had other coefficients, any below 5e-7 became 0. Now "R" (round-trip, as
libsvm's %.17g); old files read as before. Precomputed serial written as an integer ("R" on an int throws).
- svm_train warned "class label X specified in weight is not found" for every class WITHOUT a weight (inverted
libsvm loop), i.e. on every training with default parameters; now only for a weight whose label is no class.
- NDArray entry points: Problem.FromNDArray (dense, 1-based, zeros kept so scaling reaches them),
Prediction.DecisionValues / PredictLabels (whole batch: kernel matrix X @ SV.T on NumSharp, per class pair two
matrix-vector products; agree with the per-row path to rounding), PredictLabelsProbability (per-row Platt
coupling, NDArray in/out). 0-based models refuse the matrix path (InvalidOperation), PRECOMPUTED -> NotSupported.
- Evidence: on the tests' exact scaled problems, all 16 {C_SVC, NU_SVC} x {linear, poly, rbf, sigmoid} x
{2-class, 8-class} configurations give scikit-learn 1.8.0's (libsvm) test accuracy AND support-vector count
exactly. 0.4.0's tests expected 100% for every kernel; POLY's true accuracy here is 88%/51% (C_SVC) and 96%/81%
(NU_SVC). ClassificationTests now pin scikit-learn's numbers.
Tests (65, both frameworks; 0.4.0: 6/20 passing, none asserting a result)
- Restored data: DecisionTree.csv = WolfgangOfner/DecisionTree's NeedUmbrella table (Quinlan's 14 days, labels
inverted) - TestID3's expected "OUTLOOK -- rain --> WIND -- weak --> NO" is right for it; a 20-sentence
tab-separated NER corpus + CRF++ template for the CRF tests (0.4.0 read C:\Users\haipi\...).
- New: NaiveBayesTests (Multinomial bit-identical to a verbatim 0.4.0 copy, sklearn docstring examples via
np.random.seed parity), KNNTest, LinearRegression ModelTest, NetworkModelTest (AND gate, batch-1 = scalar
backprop, mini-batch = mean update), seeds CV, UtilsTests, BaumWelchTests (scalar reference, EM monotonicity,
scaled forward/backward), SVM NDArrayTests (batch == per-row for 4 types x 4 kernels), IO round-trips instead of
never-committed golden files, decision-tree shape/multi-class/column fixes, CRF encode + decode.
Found in NumSharp 0.80.0 on the way (worked around here, to report): no tuple-axis reductions (np.sum(axis=(1,2)));
no bulk string[] export of a text array (ToArray<T> needs unmanaged T; AsStringArray decodes the legacy char form);
NumSharp.Build NDW003 refuses [NDScoped] on a method returning double[] (cannot hold an NDArray).
…observations x states^2, where it measures faster; below that it keeps 0.4.0's scalar loops - never slower than 0.4.0, 1.4-2.1x faster above the gate 3226aad moved the discrete Baum-Welch E/M step to NumSharp for every problem size. Measured afterwards against a verbatim copy of 0.4.0's Run (10 iterations, Release, every implementation warmed 4x, paused 300 ms for tier-1, warmed 4x again, then 10 interleaved rounds, best of 10 - the first, cruder measurement left methods at tier-0 and was off by up to 2.8x), the NumSharp step pays a fixed ~100 calls x 1.5-3 us per iteration, which loses below about 20,000 (rows x N^2) elements: rows x N^2 before gate (always NumSharp) after gate 2,000 x0.36 (crude run) x1.00 (scalar path, same code as 0.4.0) 9,000 x0.91 x0.98 18,000 x0.94 x1.05 36,000 x1.64 x2.04 (NumSharp path) 50,000 x1.72 x1.71 64,000-1.28M x1.43-2.11 x1.43-2.06 (rows = total observations over all sequences, N = states; "x" = 0.4.0 time / 0.5.0 time, higher is better.) - BaumWelchLearning.Run validates, then dispatches on rows * N^2 < VectorizedMinimumWork (25,000, documented with the measurements) to RunScalar = 0.4.0's loops verbatim except the convergence log-likelihood fix (summed over all sequences once per iteration, not reset per sequence), which also fills mLogGamma / mLogKsi as before; otherwise RunVectorized (the NumSharp path of 3226aad, unchanged). - Class remarks state the gate, the measured gain and which path fills the scratch arrays. - Tests: MatchesTheScalarReEstimation and WeightedSequencesMatchTheScalarReEstimation are DataRows over both paths (750 x 2^2 scalar, 7,200 x 2^2 = 28,800 NumSharp) against the verbatim 0.4.0 re-estimation; 67/67 on net8.0 and net10.0.
… tests, both frameworks) - GaussianNaiveBayes bit-identical to GaussianNB; SVM batch prediction as one fused kernel pass + two matrix products (x1.5-4.7 vs per-row); in-repo benchmarks against 0.4.0's verbatim code; README
Oracle (Bigtree.Algorithm.Tests.Oracle, MSTest, net8.0;net10.0, NumSharp.Interop.pythonnet 0.80.0)
- Embeds the CPython named by NUMSHARP_PYTHONNET_PYTHON (the variable NumSharp's own pythonnet tests read; the
interpreter needs numpy, scipy, scikit-learn). Unset: Inconclusive locally, failure under CI=true. PythonEnvironment
/ OracleTest adapted from NumSharp's examples/NumSharpPythonnetGhActionsCicd: ProgramName set for every interpreter
(venv adoption), BeginAllowThreads, DoNotParallelize (one GIL), NoopFormatter shutdown, a fresh py module scope per
test, and the LiveExports / LiveImports leak check after each test.
- Assertion helpers take the oracle value as object and copy a PyObject in (PyObject.ToNDArray): a dynamic py.x is
converted by the codec on assignment only, not when passed as an argument (RuntimeBinderException otherwise).
- Results with ~/.claude/python (Python 3.12.12, NumPy 2.4.2, SciPy 1.16.3, scikit-learn 1.8.0):
* GaussianNaiveBayes vs GaussianNB on iris: theta_, var_, class_prior_, class_count_, epsilon_,
predict_joint_log_proba, predict_log_proba, predict_proba, predict - BIT-IDENTICAL (it repeats sklearn's ops incl.
the log1p-based _logsumexp and the stack(...).T layout; NumSharp's reductions/ufuncs are NumPy's). Wine with fixed
priors and var_smoothing 1e-3: var_ bit-identical, proba to 1e-12.
* BernoulliNaiveBayes vs BernoulliNB on digits (binarize 8, alpha 0.5): feature_count_, feature_log_prob_,
class_log_prior_ bit-identical; predict_log_proba to 1e-10 (sklearn's X @ W.T runs on BLAS); predict identical.
* MultinomiaNaiveBayes: CherubNLP's log-probabilities = NumPy's evaluation of the same formula, bit-identical.
* KnnModel vs KNeighborsClassifier(brute, k=7) on random data: identical neighbour indices, predict_proba, predict;
distances to 1e-12.
* LinearRegressionModel vs LinearRegression on diabetes: coef_ to 1e-10, intercept_/predict/R^2 to 1e-12.
* SVM vs SVC(rbf, gamma 0.25, C 2, ovo) on iris scaled by Bigtree's RangeTransform: identical n_support_ and
support_ indices, rho (= -intercept_) and |dual_coef_| and the ovo decision_function to 1e-9, identical labels.
* NetworkModel vs NumPy mini-batch backprop from the same weights (seeds, 30 epochs, batch 8): weights to 1e-12.
* Baum-Welch vs a NumPy/SciPy log-space re-estimation, both Run paths (scalar 8x40, NumSharp 80x100): pi, A, B to 1e-9.
* ID3 gains vs scipy.stats.entropy; Utils.CrossValFolds = NumPy default_rng(123).permutation(210), element for element.
SVM batch prediction (Prediction.DecisionValues / PredictLabels on an NDArray)
- All class pairs' decision values are ONE matrix product: the pair coefficients go into a dense (support vectors,
pairs) matrix, zero outside each pair's two classes. The first version sliced the kernel matrix per pair, and
NumSharp's dot on those strided column views was the bottleneck (5000 x 988: ~200 ms with or without OpenBLAS).
- The kernels' elementwise part is one fused np.evaluate pass (NDExpr.Exp / Maximum / Power / Tanh): the unfused RBF
chain made six passes and five matrix-sized temporaries. Measured in one process, interleaved, best of 10: fused
result bit-identical; 1000x281 x1.14, 5000x988 x2.18, 10000x1999 x2.08.
- Votes: votes = positive @ (firsts - seconds) + column sums of seconds, one small product instead of strided
per-pair column updates (the vote step now costs ~0.25 ms for 5000 rows).
- End to end vs per-row libsvm prediction (benchmarks/SvmBatchPrediction.cs, interleaved best of 10):
281 SVs / 1000 rows x1.5, 988 / 5000 x2.9, 1999 / 10000 x4.7; labels identical in every case.
- OpenBLAS (NumSharp.Interop.OpenBLAS 0.80.0, OpenBlasEngine.Enable(), 24 threads) was tried: no reliable gain on
this shared machine (~25% busy with other work), so the library stays managed-only; the products are contiguous
and would route to gemm once a consumer enables the backend.
Benchmarks (benchmarks/, .NET 10 file-based apps, 0.4.0's code copied verbatim as the baseline)
- MultinomialNaiveBayes.cs: CherubNLP's training pass x37 (300x100x4), x216 (1000x300x5), x194 (2000x500x6).
- BaumWelch.cs: x1.00-1.05 below the 25,000 gate (same code), x1.60-2.25 above (up to 1.28M rows x N^2).
- SvmBatchPrediction.cs: as above. benchmarks/README.md has the tables and the method.
README: models table (how each computes), a quick example (compiled and run verbatim: network 96.7%, SVM 93.3% train
accuracy on seeds), the scikit-learn parity list, how to run unit vs oracle tests, performance summary, memory /
ownership rules, and every change from 0.4.0 (API and behaviour).
Package check: dotnet pack -c Release -> lib/net8.0 + lib/net10.0 (dll + xml), dependency NumSharp 0.80.0 only
(NumSharp.Build stays a build-time tool), license expression Apache-2.0. (dotnet pack -o <dir> trips NU5026 with the
csproj's GeneratePackageOnBuild=true, kept from 0.4.0; plain dotnet pack / dotnet build -c Release produce the package.)
Solution: 0 warnings, 0 errors; unit 67/67 and oracle 12/12 on net8.0 and net10.0.
…aster than 0.4.0 at every size, and a 400x400x16-state run in 43 MB instead of 4.3 GB (0.42 s instead of 15.2 s) The baseline profile of 2026-11-08 (one scenario per process, peak working set) put Baum-Welch first: 400 sequences x 400 observations x 16 states x 32 symbols, 3 iterations, took 16.5 s and peaked at 4,257 MB on 88f05c6. Two causes: the log-space recursions pay an exp and a log1p per summed term (0.4.0's LogHelper.LogSum), and 3226aad's NumSharp E/M step materialized a (transitions, N, N) ksi tensor plus ~10 tensor-sized temporaries that its NDScope kept alive until the iteration returned. The fix is the algorithm, not the vectorization: Rabiner's scaling (BaumWelchLearning.Scaled.cs, RunScaled). - Forward: alpha_hat_t = (alpha_hat_t-1 A) o B[:, o_t] / c_t with c_t = P(o_t | o_1..o_t-1), accumulated as rows of A weighted by alpha_hat_t-1 (contiguous reads). Backward: beta_hat_t[i] = (row i of A) . (B[:, o_t+1] o beta_hat_t+1) / c_t+1 (a contiguous dot per state). Plain multiply-adds; no transcendental per term. - gamma = alpha_hat o beta_hat (sums to 1), log P(sequence) = sum log c_t. - Transition statistics: sum_t ksi_t = A o sum_t alpha_hat_t (x) w_t with w_t[j] = B[j, o_t+1] beta_hat_t+1[j] / c_t+1 - one outer-product accumulation per transition (a SAXPY per state), A applied once after all sequences. No per-step (N, N) posterior is stored: memory is the (T x N) buffers of the longest sequence plus O(N^2 + N M) accumulators. - M-step in probability space, logged into the model's arrays; 0.4.0's rules kept: logA = 0 where numerator == denominator (a never-visited state's 0/0 included), logB = log num - log den, logPi = log sum gamma_0 - log K; convergence value log(sum_k P(seq_k)) / K (Accord.NET's, as 3226aad fixed it). - A sequence impossible under the starting model (a scale of 0) cannot be scaled: Run snapshots the starting parameters, and on that signal restores them and runs RunScalar (0.4.0's log-space loops, verbatim but for the likelihood fix), reproducing 0.4.0 NaN for NaN. EM never lowers the likelihood, so possible data stays possible. - Sequence weights: in 0.4.0 (and 3226aad) they are added to every posterior of a time step before its normalization, so they cancel; RunScaled therefore ignores them, with the same results (documented in the class remarks). Removed (superseded): the 25,000 rows x N^2 gate (VectorizedMinimumWork), RunVectorized, Iterate, the Layout of stacked NDArrays, FromMatrix / CopyInto. BaumWelchLearning no longer uses NumSharp: the recursions are sequential in time over N-sized vectors, where NumSharp's ~1.5-3 us per call costs more than the arithmetic, and the scaled statistics need no whole-array step at all. Measured (benchmarks/BaumWelch.cs, 0.4.0's Run verbatim vs 0.5.0, 10 iterations, warmed + 300 ms pause + warmed, 10 interleaved rounds, best of each): 10x50 N2 16.4 -> 0.28 ms (x58); 20x50 N3 x20; 20x100 N3 x21; 40x100 N3 x24; 20x100 N5 x39; 40x100 N4 x37; 80x100 N3 x25; 20x200 N6 x45; 100x200 N8 2,726 -> 51.9 ms (x53). Against 88f05c6: x16-58, and 400x400 N16 (3 iterations) 15.2 s -> 0.42 s. Learned log-emissions agree with 88f05c6's to <= 7.2e-14. Profile (--no-cache build; a cached file-based app keeps the OLD library): 400x400 N16 14.8 s / 4,257 MB -> 0.39 s / 43 MB peak working set (the bare process starts at 41 MB). Tests: BaumWelchTests' DataRows are named by size now (both run the scaled path) and still match 0.4.0's verbatim log-space re-estimation at 1e-9; new ImpossibleSequenceFallsBackToTheLogSpaceLoops (a symbol no state emits) equals 0.4.0's result exactly, NaN for NaN. Unit 68/68 on net8.0 and net10.0; the Baum-Welch oracle (NumPy/SciPy log-space) 2/2 at 1e-9. README and benchmarks/README updated.
…ianNB frees each class's temporaries - KNN 8000x8000 3.0 GB -> 170 MB (2x faster), SVM 20000x1960 696 -> 186 MB, GaussianNB 100000x50x10 1.4 GB -> 319 MB (1.7x faster); results unchanged Root cause found by the baseline profile (2026-11-08, peak working set per scenario process): an NDScope keeps EVERY temporary its method creates alive until the method returns. A method that loops over classes or pairs, or that materializes (queries x training samples), therefore peaks at the SUM of its temporaries: - KnnModel: squared distances = queryNorms - 2 dot + trainNorms, maximum(., 0) - four (queries x training) matrices plus argpartition's; 8,000 x 8,000 x 30 peaked at 3,000 MB (the input is 4 MB). - GaussianNaiveBayes.PredictJointLogProba: per class (X - theta), square, / var - three (samples x features) temporaries x 10 classes alive at once; 100,000 x 50 peaked at 1,375 MB (input 40 MB). Fit kept every class's row copy until it returned. - Prediction.DecisionValues: the whole (rows x support vectors) kernel; 20,000 x 1,960 peaked at 695 MB. Changes - KnnModel: queries run in blocks of BlockElements = 2^22 distances (32 MB) - BlockRows = max(1, 2^22 / training rows) - each block in its own NDScope, results written into preallocated (rows, K) / (rows, classes) outputs by slice assignment. SquaredDistances fuses |x|^2 - 2 x.t + |t|^2 and the clamp into ONE np.evaluate pass (one result matrix instead of four temporaries; same per-element operations). Votes: one np.bincount over codes shifted into per-row slots (row * classes + code), instead of a (block, K, classes) one-hot tensor. - Prediction.DecisionValues: the dense support vectors (DenseSupportVectors), their RBF norms and the (SV, pairs) pair weights (PairWeights) are built once; the kernel (KernelBlock: dot + fused np.evaluate) and the decision values run per block of max(1, 2^22 / SV) rows, each in its own scope, into a preallocated result. - GaussianNaiveBayes: one NDScope per class in Fit and PredictJointLogProba, yielding the class's column / mean / var (scope.Returns) - the operations, and so the bits, are unchanged. Measured (one process per run; time + peak working set) - KNN predict 8,000 x 8,000 x 30: 1,871 ms / 3,000 MB -> 949 ms / 170 MB; 4,000 x 4,000: 517 ms / 798 MB -> 330 ms / 211 MB. - SVM batch predict 20,000 rows x 1,960 SVs, A/B against 010fa28 in a detached worktree, processes alternated 4x: before 1,051-1,231 ms / 696 MB, after 984-1,233 ms / 186 MB (same speed; the machine was ~25-50% busy). The interleaved per-row benchmark ratio rose to x1.8 / x3.5 / x6.2. - GaussianNB fit + predict_proba 100,000 x 50, 10 classes, A/B 3x alternated: before 853-1,072 ms / 1,375 MB, after 509-624 ms / 319 MB. Tests: new KNNTest.BlocksAgreeWithBruteForce (4,200 training samples -> 998-row blocks, 2,500 queries in three blocks; rows 0/997/998/1995/1996/2499 have a scalar brute-force search's neighbours, distances to 1e-12 and votes); unit tests (KNN 6, SVM 15, Bayes 12) pass on net8.0 and net10.0; oracle: KNN vs KNeighborsClassifier, SVM vs SVC, and GaussianNB still BIT-IDENTICAL to scikit-learn.
…Sharp text array, the tree grown over integer codes - x15 to x70 warm (5,000 rows 191 -> 2.7 ms), a fresh-process Learn of 50,000 rows 1.6 s -> 0.11 s, peak 189 -> 85 MB; trees bit-identical
Root cause (baseline profile 2026-11-08: 5,000 rows x 8 attributes 0.72-0.87 s, 50,000 rows 1.5-1.8 s): every node
re-encoded each of its columns with NumSharp (np.array of the strings, np.unique with return_index/return_inverse, two
argsorts, np.bincount, an entropy of five ufuncs) - a dozen NumSharp calls per attribute however few rows the node
held - and copied its string rows into a reduced TrainingData for every child.
Changes (Bigtree.Algorithm/DecisionTree/Tree.cs)
- CodedTable.Create encodes every column ONCE: np.unique(np.array(column, np.dtype("<U{width}")), return_inverse)
-> int codes + the code texts. The width (longest value in UTF-16 units, at least 1, never fewer than NumPy's code
points, so nothing is cut) is measured in C# and passed as the dtype: NumSharp 0.80.0's np.array(string[]) runs
NumPy's dtype discovery per value (DiscoverDTypeFromPyObject + FindScalarDescriptor + PromoteTypes into the running
descriptor, UnmanagedStorage.Coercion.Text.cs Discover), measured steady-state at 65-160 ns per string against 7-18 ns
for the sized request - 10x the cost of the copy; with it, sized array + np.unique costs about what a Dictionary
encoding does (5,000 strings 0.10 vs 0.06 ms; 50,000 0.9 vs 0.7 ms), so the encoding stays on NumSharp.
- Grow works on row-index subsets of the codes: one pass ranks the labels by first appearance and counts them; one pass
per attribute ranks its values AND counts the (value x label) table (CountByAppearance); the chosen attribute's table
says which values are leaves (exactly one nonzero label count; the leaf label is that label) and sizes each impure
child's row array exactly; one more pass hands every impure value its rows in table order. No string comparisons,
no reduced-table copies, no LINQ per child.
- Same entropy expression (p * (log(p) / log(2)) summed in first-appearance order, size / rows * H per value), same
strict-> tie rule (first attribute wins), same edge order, same TableIndex (position among the remaining attributes),
same majority leaf for contradictory rows.
- New: a null cell raises ArgumentException("Row i has no value in column 'c'.") up front. Before, NumSharp's discovery
refused it with a NotSupportedException about object arrays; a sized request would store the text "None".
Verification
- Byte-identical trees against the previous implementation (53ab91c) on 336 random tables (1-1,500 rows, 1-7
attributes, 1-5 labels, duplicated columns for exact gain ties): 4,480 internal nodes and 5,369 leaves, every name,
TableIndex, edge, value list and InformationGain's raw bits equal (sha256 6C6151D357256542 on both).
- New unit tests: WideAndNonAsciiValuesKeepTheirText (values of 1-25 chars, umlauts, a surrogate pair round-trip as
edges and leaves), NullCellIsRefused. Unit 71/71 on net8.0 and net10.0 (DecisionTree 7/7); oracle 12/12 on both
(DecisionTree_GainsMatchSciPyEntropy included).
Measured
- benchmarks/DecisionTree.cs (new; in one process, interleaved, best of 7; baseline = 0.5.0's first port 3226aad
verbatim, since 0.4.0's ID3 drops the columns left of the split and throws on contradictory rows or exhausted
columns, which noisy tables always reach; asserts the same tree, gains' bits included):
1,000 x 6: 27.7 -> 1.9 ms (x15); 5,000 x 8: 190.9 -> 2.7 ms (x70); 50,000 x 8: 576.4 -> 26.3 ms (x22).
- One Learn in a fresh process (time includes JIT; peak working set), A/B against 010fa28 in a detached worktree,
processes alternated 3x: 5,000 rows 793-874 ms / 88-90 MB -> 41-56 ms / 53 MB; 50,000 rows 1,499-1,811 ms /
188-189 MB -> 108-112 ms / 85 MB.
Docs: README (model table, performance line, a Memory paragraph on per-class / per-block scopes from 53ab91c, the
decision-tree changes bullet); benchmarks/README (decision-tree section; the SVM table re-measured after 53ab91c's
blocks: x1.6 / x2.8 / x7.0, and its memory note).
…MD pass over the samples' own vectors - 20,000 x 2,000 training pass 0.95-1.95 s / 1,358 MB -> 0.12-0.15 s / 376 MB (320 MB of it the input); x47-x1,147 vs 0.4.0; bits unchanged. NDHelpers.Text sizes text arrays up front
Root cause (baseline profile 2026-11-08: mnb 20000 x 2000 884-1,948 ms, peak 1,357 MB; 5000 x 1000 217-263 ms,
263 MB): CountValue built counts[label, x] = onehot(labels).T @ (X == value) as NumSharp whole-array ops, which first
needed the jagged FeatureSet (List<Tuple<string, double[]>>) as one matrix: a flat double[] copy, an unmanaged NDArray
copy of it, a bool mask and the mask's float64 copy - four (samples x features) temporaries, about 1 GB at
20,000 x 2,000, to produce a 5 x 2,000 table, repeated for every requested value (CherubNLP asks for 0 and 1).
Changes
- MultinomiaNaiveBayes.CountValue: one pass over the samples; per sample, the label's row of a long[labels x features]
table gets Vector.Equals(Vector<double>(vector, x), target) subtracted (a match is an all-ones lane, -1 as a long)
a SIMD vector of features at a time, then a scalar tail. IEEE equality as before (-0 counts as 0, NaN matches
nothing). The counts then go through the SAME NumSharp smoothing + np.log as before on the small table only, so every
log-probability keeps its bits (and stays bit-identical to 0.4.0's Math.Log formula). checked(labels * features):
an OverflowException instead of a wrapped size past the largest .NET array.
- NDHelpers.Text(string[]) (new): a 1-D '<U{width}' array with the width (longest value in UTF-16 units, at least 1)
measured in C# and passed as the dtype - NumSharp 0.80.0's np.array(string[]) runs dtype discovery per value (65-160
ns per string vs 7-18 ns sized). Null -> ArgumentException (a sized request would store "None"). Used by
Tree.CodedTable.Create (which still names the row and column of a null cell itself) and by
MultinomiaNaiveBayes.CountLabels, which now refuses a null label with ArgumentException("FeatureSet[i] has no label.")
instead of NumSharp's NotSupportedException about object arrays.
- Docs: class / CalCondProb / CountValue remarks describe the pass; CalCondProb documents the OverflowException and the
null-label ArgumentException.
Verification
- New unit test Multinomial_SignedZeroAndNaNCountLike040: 37 features (whole vectors + a scalar tail at any width),
values {0, -0, 1, NaN, 2.5}, every label x feature x value bit-identical to the verbatim 0.4.0 reference.
Multinomial_MatchesBigtree040BitForBit and Multinomial_MultiValuedFeaturesMatchBigtree040 unchanged and green.
- Unit 72/72 on net8.0 and net10.0; ID3 trees still byte-identical on the 336 random tables after the Text refactor
(sha256 6C6151D357256542).
Measured
- One training pass per process (time; peak working set), A/B against 010fa28 in a detached worktree, processes
alternated 3x: 5,000 x 1,000: 217-263 ms / 263 MB -> 28-30 ms / 97 MB; 20,000 x 2,000: 945-1,948 ms / 1,358 MB ->
123-154 ms / 376 MB.
- benchmarks/MultinomialNaiveBayes.cs (0.4.0 verbatim in the same process; 0.5.0 now gets 25 rounds since its rounds
take milliseconds): 300 x 100 x 4: 33.1 ms vs 0.47 ms (x71); 1,000 x 300 x 5: 1,415 vs 2.37 ms (x598);
2,000 x 500 x 6: 4,184 vs 4.79 ms (x874); a second run x47 / x574 / x1,147 (0.4.0 moved up to 1.5x with load).
The 2026-11-08 table had x37 / x216 / x194.
- README / benchmarks/README updated (model table, ratios, the memory note).
…r programs compiled once - 11 NumSharp calls per training step instead of ~24; 50-64-10 x 20,000 rows x 5 epochs: batch 1 7.2-8.2 s -> 2.4-2.6 s, batch 32 510-566 -> 305-328 ms; weights bit-identical Root cause: a training step on a small batch is dominated by NumSharp's fixed per-call cost, and NetworkModel.Step spent most of its calls on element-wise chains: the sigmoid (negative, exp, add, divide - four ufuncs, four temporaries, plus 0-d temporaries for the C# literals), the output delta (subtract, multiply, subtract, multiply), the hidden delta (three after its product) and the update (rate * gradient, then np.add into the weights). Changes - NDHelpers.Sigmoid: a float64 operand goes through SigmoidProgram = (1 / (1 + Exp(Negate(Input(0))))).Compile(Double), one fused pass and one result; any other dtype keeps the ufunc chain (its NEP 50 promotion; the compiled program is float64-only). Used by NetworkModel's forward pass and PredictProba. - NetworkModel.Kernels (new, private static): OutputDelta (Input(0) - Input(1)) * Input(1) * (1 - Input(1)), HiddenDelta Input(0) * Input(1) * (1 - Input(1)), Update Input(0) + Input(2) * Input(1) - positional float64 programs compiled once (CompiledExpression: no per-call tree building or typing, ~0.4 us of per-call work). Step evaluates them; the update writes into Weights[l] through out= (the program may alias an input), with the rate as a 0-d float64 operand (NDArray.Scalar). Same operations in the same order as the unfused formulas. - Train slices each batch with a Slice object instead of formatting and parsing "start:stop" per batch. - NetworkModel's "batch size is a speed choice" remark re-measured (it quoted "about 80 times slower than plain loops" at batch 1): the 7-5-3 seeds-sized network (210 samples, 50 epochs) against the same algorithm in plain C# loops over double[,] weights: batch 1 13.5x slower before -> 6.7x now (231 -> 120 ms vs 17-18 ms); batch 32 2.1x -> 3.9x FASTER than the loops (7.97 -> 4.29 ms vs 16.5-16.8 ms). Verification - Scratch check of each program against its unfused formula on 4,000 x 64 random float64 (activations x20 so exp over- and underflows): sigmoid, output delta, hidden delta and the in-place update all bit-identical. - Trained weights (every element's bits, SHA-256) after 50-64-10 and 20-16-8-4 networks at batch sizes 1, 7, 32 and 256 (partial last batches): identical before (010fa28, a detached worktree) and after (9E9A0895AAB7C250 on both). - Unit 72/72 and oracle 12/12 (NetworkModel vs NumPy's mini-batch backpropagation to 1e-12 included) on net8.0 and net10.0. Measured (one training per process; processes alternated 3x against 010fa28) - 50-64-10, 20,000 rows, 5 epochs: batch 1 7,180-8,245 ms -> 2,404-2,565 ms (one outlier 5,285); batch 32 510-566 -> 305-328 ms; batch 256 220-243 -> 201-228 ms; peak working set unchanged (70 MB at 32 / 256). - What is left at batch 32 is NumSharp's managed GEMM doing real work: the five products take ~55 of a step's ~96 us; a transposed operand costs nothing extra (x @ W0.T 15.4 us vs 22.8 us with a C-contiguous copy), so the weights keep their documented (out, in) layout. - README: the model table and a paragraph on fused / compiled element-wise formulas.
…blocks of centered rows, finiteness checks are one fused All(IsFinite) reduction, GaussianNB's distance is one compiled pass - LinearRegression 200,000 x 100 peak 385 -> 277 MB (same speed), GaussianNB distances x3.75 and peak 321 -> 280 MB; GaussianNB still bit-identical to scikit-learn Root cause (profile 2026-11-09 on b672cbd): LinearRegression.Fit peaked at 385 MB for a 160 MB input - the centered copy xc = X - mean (160 MB) and Float64Matrix's np.isfinite bool array (20 MB) both lived until the fit's scope closed. GaussianNaiveBayes.PredictJointLogProba spent most of fit + predict_proba (552-992 ms on a busy machine) in np.square(X - theta) / var: three (samples x features) passes and temporaries per class. Changes - LinearRegressionModel.Fit: gram = sum over row blocks of xc_b.T @ xc_b and moment = sum of xc_b.T @ yc_b, each block of BlockElements = 2^22 (32 MB, the KNN / SVM block size) centered in its own NDScope and accumulated with np.add(..., @out:); then np.linalg.solve(gram, moment) as before. One block (rows * features <= 2^22, every small fit) is the unblocked product bit for bit (0 + x is exact); more blocks group the row sums differently (rounding level). yc stays whole (samples x targets, small). Class remarks say so. - NDHelpers.AllFinite (new): (bool)np.evaluate(NDExpr.All(NDExpr.IsFinite(values))) - one fused reduction, no temporary. Float64Matrix (every estimator's X, and 2-D targets) and LinearRegressionModel.Target use it instead of np.all(np.isfinite(...)), whose bool array the caller's scope held until the estimator returned. - GaussianNaiveBayes.WeightedSquare (new): (Square(Input(0) - Input(1)) / Input(2)).Compile(Double, Double, Double), evaluated per class over (samples, theta_i, var_i), which broadcast as the ufuncs' operands did; np.sum(axis: 1) unchanged. NumPy's ** 2 is its square, so subtract / square / divide are scikit-learn's operations in its order. Verification - New unit test FitAcrossSeveralBlocksMatchesTheWholeProduct: 60,000 x 80 (two blocks, 52,428 + 7,572 rows), two targets: coefficients equal the whole-matrix normal-equation solve to 1e-12 and the generating weights to 1e-3. - Scratch check: the compiled distance against np.sum(np.square(X - theta) / var, axis: 1) over 10 classes of 100,000 x 50: bit-identical. - Unit 73/73 and oracle 12/12 on net8.0 and net10.0 (GaussianNB bit-identical to GaussianNB: theta_, var_, class_prior_, epsilon_ and every prediction output; LinearRegression vs scikit-learn to 1e-10). Measured - In process, interleaved, best of 9: the per-class distance over 10 classes x 100,000 x 50, 339.4 ms unfused -> 90.6 ms compiled (x3.75). - One run per process, A/B against b672cbd in a detached worktree, alternated 3x (the machine was busy: the same build varied up to 1.8x): GaussianNB fit + predict_proba 100,000 x 50, 10 classes 552-992 ms / 321 MB -> 286-541 ms / 280 MB; LinearRegression fit 200,000 x 100 373-590 ms / 385 MB -> 377-492 ms / 277 MB (same speed). - README: the fused / compiled paragraph and the Memory paragraph (linear regression blocks; GaussianNB at 280 MB).
…loat64 exp, which NumSharp 0.80.0 computes faster than NumPy 2.4.2 on this AVX2 machine (6.6 vs 7.5 ns per element) Measured 2026-11-09 to decide whether SVM batch prediction (945 ms for 20,000 rows x 1,960 support vectors) had NumSharp overhead left to remove. One block of 2,140 rows (in process, best of 9): cross product 13.1 ms, the fused RBF kernel pass 49.4 ms of which np.exp over its 4.2 M elements 33.8 ms, decision values 18.9 ms (the transposed formulation (weights.T @ kernel.T).T is the same 18.5 ms). exp over 4.2 M elements: NumSharp 0.80.0 np.exp float64 27.5 ms (6.55 ns/element), float32 4.7 ms (1.13 ns); a Math.Exp loop over double[] 31.1 ms (7.42 ns), MathF.Exp over float[] 16.8 ms (4.01 ns); NumPy 2.4.2 float64 31.4 ms (7.49 ns), float32 7.9 ms (1.88 ns). The machine has AVX2 and no AVX-512 (NumPy reports X86_V3 found): NumPy's float64 exp is vectorized only with AVX-512F, so both libraries run it scalar here. Nothing to remove on the Bigtree side; the batch path stays as it is.
…are spelled MultinomialNaiveBayes and MultinomialNaiveBayesModel - members, numbers and the JSON model shape unchanged; CherubNLP adopts the two names (three lines) in its net8.0 retarget Decided by the owner on the PR design pages (2026-11-09, round 2): "Rename outright", over keeping 0.4.0's spelling (round 1's "keep CherubNLP's surface" otherwise stands) or adding the correct names with the old ones as thin subclasses. CherubNLP has to change for 0.5.0 anyway (netstandard2.0 -> net8.0, which NumSharp 0.80.0 requires), so the two names ride on that change: CherubNLP/Classify/NaiveBayesClassifier.cs line 45 (new MultinomiaNaiveBayes()) and lines 126, 155 (MultinomiaNaiveBayesModel, saved and loaded through JsonConvert). Changes - git mv Bigtree.Algorithm/Bayes/MultinomiaNaiveBayes.cs -> MultinomialNaiveBayes.cs and MultinomiaNaiveBayesModel.cs -> MultinomialNaiveBayesModel.cs; the class names and the constructor follow. - Every use: the unit tests (NaiveBayesTests, including the test-local verbatim 0.4.0 copy, now LegacyMultinomialNaiveBayes), the oracle test, benchmarks/MultinomialNaiveBayes.cs and the README model table. - Comments that describe 0.4.0's own class keep its historical spelling (NaiveBayesTests' reference copy, the benchmark's Legacy class and header). - README "Changes from 0.4.0": the rename is listed; CherubNLP compiles once it targets net8.0 and uses the two new names; its saved JSON models load unchanged. Verification - The model class's members are 0.4.0's exactly (a diff of master's MultinomiaNaiveBayesModel.cs against the renamed file differs only in the class name), and Newtonsoft stores property names, not the type name, so CherubNLP's saved models deserialize into the renamed class. - Build 0 warnings / 0 errors; unit 73/73 and oracle 12/12 on net8.0 and net10.0 (Windows 11).
….Open() and no [NDScopedCovered] left in the repository, the analyzer's NDW0xx warnings off in .editorconfig; the tests, the oracle, the console and the benchmark scripts are woven too The owner's request (2026-11-09): "use [NDScoped] attribute instead of creating scopes. suppress all ND* warnings and remove [NDScopedCovered] as it adds noise". Warnings - .editorconfig [*.cs]: NDW012, NDW013, NDW016, NDW017, NDW020 = none. These are every warning-severity rule NumSharp 0.80.0's analyzer ships (AnalyzerReleases.Shipped.md; the analyzer comes inside the NumSharp package, so every project here had it). Its error rules (NDW002/003/005/006/009/010/011/015) stay: the weaver refuses those shapes at build time anyway. The benchmark scripts' #:property NoWarn=NDW012;NDW016;NDW017 lines are gone (BaumWelch.cs keeps CS8632): the root .editorconfig reaches file-based apps too. [NDScopedCovered] removed everywhere (24 in the library, 2 in benchmarks/DecisionTree.cs). It is a marker only: an NDArray belongs to the innermost scope open on its thread, so a helper's temporaries go to its caller's woven scope with or without it. NDHelpers' class remarks now say that instead. Hand-opened scopes -> [NDScoped] (17 library sites, 45 test / console sites, 3 benchmark sites) - Methods returning NDArray / NDArray[] / void take the attribute directly; scope.Returns(x) becomes return x (the woven scope hands a returned array to the caller's): NetworkModel.Train, Prediction.DecisionValues / PredictLabels / PredictLabelsProbability, Utils.CrossValFolds. - Methods the weaver refuses (NDW003: a returned reference type that is not an NDArray carrier) keep their signature and delegate to an [NDScoped] void body: Bernoulli / Gaussian / KNN / LinearRegression Fit -> FitCore (Fit still returns this); Problem.FromNDArray -> Read (fills a new Problem); Tree.CodedTable.Create -> Encode per column (each column's arrays now freed before the next, where the hand scope held all columns to the end); MultinomialNaiveBayes.CountValue (returned double[], which NDW003 also refuses: the bug pinned on NumSharp journey5) now stores its table into _logCondByValue itself. - Per-iteration scopes become [NDScoped] methods, same lifetimes: KnnModel.KNeighborsBlock / VotesBlock, Prediction.DecisionBlock, LinearRegressionModel.AccumulateBlock, GaussianNaiveBayes.ClassMoments (returns the class's (mean, variance, count)) and ClassJointLogLikelihood (returns the class's column). - Tests (34 unit, 10 oracle) and the console's Main: the scope line at the top of each synchronous void method became an [NDScoped] attribute (a script that refused any other shape: not the first statement, not void, async, or `scope` used later). Their projects now reference NumSharp.Build 0.80.0 with PrivateAssets="all", like the library, so the attributes are woven. - Benchmarks: SvmBatchPrediction.cs runs each configuration in [NDScoped] Benchmark.Run and each timed batch in [NDScoped] PredictBatch; DecisionTree.cs's first-port copy computes its gains in [NDScoped] BestAttribute (returns the best index; GetRootNode builds the TreeNode). Both scripts add #:package NumSharp.Build@0.80.0: a probe showed it weaves a file-based app (an [NDScoped] method's temporary was disposed on return; no NDW018). Verification - Build: 0 warnings, 0 errors (whole solution, both frameworks). Weaver summaries: Bigtree.Algorithm.dll woven 42 = the 42 [NDScoped] attributes in the library (24 before; "already-scoped 0"), UnitTest 34, Tests.Oracle 10, Console 1 = the 45 converted sites. - Unit 73/73 and oracle 12/12 on net8.0 and net10.0 (GaussianNB still bit-identical to scikit-learn). - Peak working set of the blocked workloads unchanged (one process each): KNN 8,000 x 8,000 173 MB (was 170), GaussianNB fit + predict_proba 100,000 x 50 275 MB (280), LinearRegression 200,000 x 100 278 MB (277), SVM predict 20,000 x 1,960 SVs 189 MB (186). - benchmarks/DecisionTree.cs: same tree on all three sizes (x14 / x81 / x22); SvmBatchPrediction.cs: 0 labels differing (x1.5 / x3.0 / x7.0). - README "Memory": scopes are [NDScoped] attributes, the FitCore pattern for NDW003 shapes, the warnings off.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Hi @Oceania2018, this moves Bigtree.Algorithm to NumSharp 0.80.0 on net8.0 / net10.0. The details are below.
After this PR I will contribute to CherubNLP, also improving it.
Kindly review and merge.
Summary
This moves Bigtree.Algorithm from NumSharp 0.30.0 / netstandard2.0 to NumSharp 0.80.0 on net8.0 and net10.0, and
proposes the result as version 0.5.0.
MultinomialNaiveBayesreturns 0.4.0'slog-probabilities to the last bit.
MultinomiaNaiveBayes→MultinomialNaiveBayesandMultinomiaNaiveBayesModel→MultinomialNaiveBayesModel. Saved JSON models still load.GaussianNaiveBayes,BernoulliNaiveBayes,KnnModel,LinearRegressionModel) areimplemented. They take and return NumSharp
NDArrays with scikit-learn's shape:Fit,Predict,PredictProba,Score, and fitted attributes as properties.There are now 73 unit tests, plus 12 oracle tests that compare each estimator with scikit-learn, NumPy or SciPy on
the same arrays.
Verified on Windows 11 and Ubuntu 24.04 (WSL2), for net8.0 and net10.0:
Per model
MultinomialNaiveBayes(wasMultinomiaNaiveBayes)CalCondProbcallGaussianNaiveBayesGaussianNB, bit-identical (theta_,var_,class_prior_,epsilon_, every prediction output)BernoulliNaiveBayesBernoulliNBKnnModelnp.argpartition, votes; queries in 32 MB blocksLinearRegressionModelnp.linalg.solve(fully managed, no native dependency)NetworkModelNeuron/Dendriteobjects, per-sample trainingDecisionTree.Tree(ID3)NDArrayentry points, and batch prediction from one kernel matrixBugs fixed
RangeTransformmixed 0- and 1-based feature indices, and an emptycatchhid the error.Degreewhere training usedGamma.Model.Writekept 6 decimals, so a saved model predicted differently after loading.++Nloop increment, and the first scale coefficient was neverstored.
Utils.CrossValFoldsreturned no folds.data/: the NeedUmbrella table,a CRF corpus and its template.
Breaking changes
MultinomiaNaiveBayes→MultinomialNaiveBayes,MultinomiaNaiveBayesModel→MultinomialNaiveBayesModelNetworkModel:Neuron/Dendriteremoved;NeuralLayeris(Count, Name);Train(..., batchSize);Build(seed)Weights, one matrix per layerUtils.CrossValFoldsreturnsNDArray[]foldsUtils.ReadCsvnumbers labels in ascending orderNDArray.Normalize()(NumSharp 0.30)Utils.Normalize(X)IDisposableusing var model = ...(the finalizer also releases them)MultinomialNaiveBayes)ArgumentExceptionnaming the rowSame numbers as scikit-learn
Bigtree.Algorithm.Tests.Oracleembeds CPython throughNumSharp.Interop.pythonnetand checks:GaussianNaiveBayesagainstGaussianNB: bit-identical.BernoulliNaiveBayes: counts and log-probabilities bit-identical; probabilities to 1e-10.KnnModelagainstKNeighborsClassifier: the same neighbours, votes and labels; distances to 1e-12.LinearRegressionModelagainstLinearRegression: coefficients to 1e-10.SVC: the same support vectors, dual coefficients, intercepts and one-vs-one decision values (to1e-9). On the unit tests' problems, every SVM type × kernel scores exactly
SVC/NuSVC's accuracy.NetworkModelagainst NumPy mini-batch backpropagation from the same weights: to 1e-12.scipy.stats.entropy.Utils.CrossValFoldsagainst NumPy'sdefault_rng(seed).permutation: element for element.The oracle needs an interpreter with NumPy, SciPy and scikit-learn: set
NUMSHARP_PYTHONNET_PYTHON. Without it theoracle tests report Inconclusive, and they fail under
CI=true. The unit tests need no Python.Performance and memory
benchmarks/holds .NET 10 file-based apps. Each one runs 0.4.0's code, copied verbatim, against this branch in oneprocess, with interleaved best-of-N rounds:
MultinomialNaiveBayesPeaks measured on large inputs (process peak working set, which includes the input):
MultinomialNaiveBayestraining pass, 20,000 sentences × 2,000 featuresGaussianNaiveBayesfit +PredictProba, 100,000 × 50, 10 classesLinearRegressionModel.Fit, 200,000 × 100Learnin a fresh processHow the estimators use NumSharp:
np.evaluatepass, or anNDExprprogram compiled once, with the bits of theufunc chain they replace.
NDScope/[NDScoped]. These are woven at build time byNumSharp.Build, a build-only dependency (PrivateAssets="all").Notes for the maintainers
NaiveBayesClassifier.cs). Its saved models load unchanged. Its training pass is theMultinomialNaiveBayesbenchmark above. It targets netstandard2.0 and references Bigtree 0.4.0 today. Once this is merged, I will open the
CherubNLP side: net8.0 / net10.0, Bigtree.Algorithm 0.5.0 and the new names.
GeneratePackageOnBuildis kept as it was. With it,dotnet pack -o <dir>fails with NU5026, whileplain
dotnet packworks.PackageLicenseUrl, which NuGet deprecated, becamePackageLicenseExpressionApache-2.0(the same license).
package are Apache-2.0. This PR leaves them as they are.
The 11 commits
3226aadNumSharp 0.80.0 on net8.0/net10.0: whole-array Naive Bayes, KNN, linear regression, MLP, ID3 andBaum-Welch; libsvm-exact SVM; every test runs
88f05c6Baum-Welch: a NumSharp E/M step above a measured size gate95f5aadthe scikit-learn oracle (12 tests), SVM batch prediction as one fused kernel pass, benchmarks against0.4.0, README
010fa28Baum-Welch on Rabiner's scaled forward-backward (×20-×58 against 0.4.0)53ab91cKNN and SVM batch prediction in 32 MB blocks; GaussianNB frees each class's temporariesf5ce17bID3: each column encoded once, the tree grown over integer codesec3072cMultinomial Naive Bayes: one vectorized counting pass per feature valueb672cbdthe neural network's element-wise formulas as compiledNDExprprogramscf269b9linear regression in 32 MB blocks, fused finiteness checks, GaussianNB's distance compiledae45740benchmark notes: SVM batch prediction is bound by float64exp0be5e1dMultinomialNaiveBayes/MultinomialNaiveBayesModel: the misspelled names correctedHow to check