Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
root = true

# NumSharp's NDArray-ownership analyzer (shipped inside the NumSharp package, so every project here gets it): its four
# warnings are off everywhere. NDW012 (an NDArray left to the finalizer), NDW016 / NDW017 (a type or member holding
# NDArrays without disposing them) and NDW020 (culture in exception texts) flag patterns this library uses on purpose:
# helpers that create temporaries inside a caller's [NDScoped] method, and estimators that own fitted arrays. NDW013
# (a scope attribute without the weaver) cannot apply: every project that uses [NDScoped] references NumSharp.Build.
# The analyzer's ERRORS (NDW002 / NDW003 / NDW005 / NDW006 / NDW009 / NDW010 / NDW011 / NDW015) stay: the weaver refuses
# those shapes at build time anyway.
[*.cs]
dotnet_diagnostic.NDW012.severity = none
dotnet_diagnostic.NDW013.severity = none
dotnet_diagnostic.NDW016.severity = none
dotnet_diagnostic.NDW017.severity = none
dotnet_diagnostic.NDW020.severity = none

# The statistics library under HiddenMarkovModel/MathUtils came with its doc comments as they were (an unescaped
# '<', <param> tags for parameters that no longer exist, comments above a namespace). Since 0.5.0 the package ships
# its XML documentation, so the compiler checks every doc comment; these four diagnostics are silenced for that
# folder only, so they still report anywhere else in the library.
[Bigtree.Algorithm/HiddenMarkovModel/MathUtils/**.cs]
dotnet_diagnostic.CS1570.severity = none
dotnet_diagnostic.CS1572.severity = none
dotnet_diagnostic.CS1573.severity = none
dotnet_diagnostic.CS1587.severity = none
8 changes: 7 additions & 1 deletion Bigtree.Algorithm.Console/Bigtree.Algorithm.Console.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,17 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.1</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\Bigtree.Algorithm\Bigtree.Algorithm.csproj" />
<!-- Build-time only: weaves Main's [NDScoped], so the tour's NDArrays return to NumSharp's pool. -->
<PackageReference Include="NumSharp.Build" Version="0.80.0" PrivateAssets="all" />
</ItemGroup>

<ItemGroup>
<None Include="..\data\seeds_dataset.csv" Link="data\seeds_dataset.csv" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>

</Project>
65 changes: 46 additions & 19 deletions Bigtree.Algorithm.Console/Program.cs
Original file line number Diff line number Diff line change
@@ -1,34 +1,61 @@
using Bigtree.Algorithm.NeuralNetwork;
using Bigtree.Algorithm.Bayes;
using Bigtree.Algorithm.KNN;
using Bigtree.Algorithm.NeuralNetwork;
using Bigtree.Algorithm.SVM;
using NumSharp;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Diagnostics;
using System.IO;

namespace Bigtree.Algorithm.Console
{
/// <summary>
/// A tour of the NumSharp estimators on the UCI seeds dataset: every model is fitted on the same 80% and scored
/// on the same held-out 20%.
/// </summary>
class Program
{
/// <summary>Loads the data, fits each model, prints its held-out accuracy and fit time.</summary>
/// <param name="args">Unused.</param>
[NDScoped]
static void Main(string[] args)
{
/*NetworkModel model = new NetworkModel();
model.Layers.Add(new NeuralLayer(2, "INPUT"));
model.Layers.Add(new NeuralLayer(1, "OUTPUT"));
var (X, y) = Utils.ReadCsv(Path.Combine(AppContext.BaseDirectory, "data", "seeds_dataset.csv"));
NDArray scaled = Utils.Normalize(X);
NDArray test = Utils.CrossValFolds((int)X.shape[0], 5, seed: 0)[0];
NDArray train = np.setdiff1d(np.arange(X.shape[0]), test);
NDArray Xtrain = scaled[train], ytrain = y[train], Xtest = scaled[test], ytest = y[test];

model.Build();
System.Console.WriteLine($"seeds: {X.shape[0]} samples, {X.shape[1]} features, {np.unique(y).values.size} classes; train {train.size}, test {test.size}");

NeuralData X = new NeuralData(4);
X.Add(0, 0);
X.Add(0, 1);
X.Add(1, 0);
X.Add(1, 1);

NeuralData Y = new NeuralData(4);
Y.Add(0);
Y.Add(0);
Y.Add(0);
Y.Add(1);
Report("Gaussian Naive Bayes", () => new GaussianNaiveBayes().Fit(Xtrain, ytrain).Score(Xtest, ytest));
Report("Bernoulli Naive Bayes (binarize at 0.5)", () => new BernoulliNaiveBayes(binarize: 0.5).Fit(Xtrain, ytrain).Score(Xtest, ytest));
Report("KNN (k = 5)", () => new KnnModel(k: 5).Fit(Xtrain, ytrain).Score(Xtest, ytest));
Report("Neural network 7-5-3 (1000 epochs, batch 8)", () =>
{
using var model = new NetworkModel();
model.AddLayer(new NeuralLayer(7, "INPUT"));
model.AddLayer(new NeuralLayer(5, "HIDDEN"));
model.AddLayer(new NeuralLayer(3, "OUTPUT"));
model.Build(seed: 0);
model.Train(Xtrain, ytrain, iterations: 1000, learningRate: 1.5, batchSize: 8);
return np.mean((model.Predict(Xtest) == ytest).astype(np.float64)).GetDouble();
});
Report("SVM (C-SVC, RBF)", () =>
{
Model model = Training.Train(Problem.FromNDArray(Xtrain, ytrain), new Parameter { Gamma = 1.0 / 7 });
return np.mean((model.PredictLabels(Xtest) == ytest.astype(np.float64)).astype(np.float64)).GetDouble();
});
}

model.Train(X, Y, iterations: 10, learningRate: 0.1);*/
/// <summary>Times one fit-and-score and prints the accuracy.</summary>
/// <param name="name">The model's description.</param>
/// <param name="fitAndScore">Fits the model and returns its held-out accuracy.</param>
static void Report(string name, Func<double> fitAndScore)
{
var clock = Stopwatch.StartNew();
double accuracy = fitAndScore();
System.Console.WriteLine($" {name,-46} accuracy {accuracy:P1} ({clock.Elapsed.TotalMilliseconds:F0} ms)");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net8.0;net10.0</TargetFrameworks>
<IsPackable>false</IsPackable>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="MSTest" Version="3.8.3" />
<PackageReference Include="NumSharp.Interop.pythonnet" Version="0.80.0" />
<!-- Build-time only: weaves the tests' [NDScoped] methods, so each test's NDArrays return to NumSharp's pool. -->
<PackageReference Include="NumSharp.Build" Version="0.80.0" PrivateAssets="all" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Bigtree.Algorithm\Bigtree.Algorithm.csproj" />
</ItemGroup>

<ItemGroup>
<None Include="..\data\seeds_dataset.csv" Link="data\seeds_dataset.csv" CopyToOutputDirectory="PreserveNewest" />
<None Include="..\Bigtree.Algorithm.UnitTest\data\DecisionTree.csv" Link="data\DecisionTree.csv" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>

</Project>
115 changes: 115 additions & 0 deletions Bigtree.Algorithm.Tests.Oracle/EstimatorOracleTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
using System.Linq;
using Bigtree.Algorithm.KNN;
using Bigtree.Algorithm.LinearRegression;
using Bigtree.Algorithm.SVM;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NumSharp;
using Python.Runtime;

namespace Bigtree.Algorithm.Tests.Oracle
{
/// <summary>KNN, linear regression and the SVM against scikit-learn 1.8 on the same arrays.</summary>
[TestClass]
public class EstimatorOracleTests : OracleTest
{
/// <summary>
/// KNeighborsClassifier (brute force, k = 7) on random data without distance ties: the neighbour indices,
/// the vote fractions and the labels are identical; the distances agree to rounding (both use the
/// |x|² - 2 x·t + |t|² expansion, summed in other orders).
/// </summary>
[TestMethod]
[NDScoped]
public void Knn_MatchesKNeighborsClassifier()
{
using (Py.GIL())
{
py.Exec(
"from sklearn.neighbors import KNeighborsClassifier\n" +
"rng = np.random.default_rng(42)\n" +
"X = rng.normal(size=(300, 6)); y = (X[:, 0] + X[:, 1] ** 2 > 1).astype(np.int64) + (X[:, 2] > 0.5)\n" +
"Q = rng.normal(size=(80, 6))\n" +
"m = KNeighborsClassifier(n_neighbors=7, algorithm='brute').fit(X, y)\n" +
"dist, ind = m.kneighbors(Q)\n" +
"proba, pred = m.predict_proba(Q), m.predict(Q)");
NDArray X = py.X, y = py.y, Q = py.Q;
using var model = new KnnModel(k: 7).Fit(X, y);
var (distances, indices) = model.KNeighbors(Q);

AssertEqual(py.ind, indices, "kneighbors indices");
AssertClose(py.dist, distances, 1e-12, 1e-12, "kneighbors distances");
AssertEqual(py.proba, model.PredictProba(Q), "predict_proba");
AssertEqual(py.pred, model.Predict(Q), "predict");
}
}

/// <summary>
/// LinearRegression on the diabetes dataset: scikit-learn solves the centered least squares by SVD
/// (LAPACK), Bigtree by the normal equations (managed LU); on these well-conditioned features the
/// coefficients, intercept, predictions and R² agree to 1e-10.
/// </summary>
[TestMethod]
[NDScoped]
public void LinearRegression_Diabetes()
{
using (Py.GIL())
{
py.Exec(
"from sklearn.datasets import load_diabetes\n" +
"from sklearn.linear_model import LinearRegression\n" +
"X, y = load_diabetes(return_X_y=True)\n" +
"m = LinearRegression().fit(X, y)\n" +
"coef, intercept, pred = m.coef_, np.array(m.intercept_), m.predict(X)\n" +
"r2 = np.array(m.score(X, y))");
NDArray X = py.X, y = py.y;
using var model = new LinearRegressionModel().Fit(X, y);

AssertClose(py.coef, model.Coef, 1e-10, 1e-8, "coef_");
AssertClose(py.intercept, model.Intercept, 1e-12, 0, "intercept_");
AssertClose(py.pred, model.Predict(X), 1e-12, 1e-9, "predict");
AssertClose(py.r2, np.array(new[] { model.Score(X, y) }).reshape(new Shape()), 1e-12, 0, "score");
}
}

/// <summary>
/// C-SVC with an RBF kernel on iris, both trained on the same [-1, 1]-scaled data: the same support vectors
/// (count per class and their indices), the same intercepts (scikit-learn's intercept_ is -rho) and dual
/// coefficients, the same one-vs-one decision values and the same labels. libsvm's solver stops at a
/// tolerance, but both run it identically, so they agree far below it.
/// </summary>
[TestMethod]
[NDScoped]
public void Svm_Iris_MatchesSVC()
{
using (Py.GIL())
{
py.Exec(
"from sklearn.datasets import load_iris\n" +
"X, y = load_iris(return_X_y=True)\n" +
"y = y.astype(np.float64)");
NDArray X = py.X, y = py.y;

// Scale with Bigtree's own range transform, then hand the very same matrix to scikit-learn.
Problem raw = Problem.FromNDArray(X, y);
Problem scaled = RangeTransform.Compute(raw).Scale(raw);
var dense = np.array(scaled.X.SelectMany(row => row.Select(node => node.Value)).ToArray()).reshape(150, 4);
Model model = Training.Train(scaled, new Parameter { KernelType = KernelType.RBF, Gamma = 0.25, C = 2.0 });

py.Xs = dense;
py.Exec(
"from sklearn.svm import SVC\n" +
"m = SVC(kernel='rbf', gamma=0.25, C=2.0, decision_function_shape='ovo').fit(Xs, y)\n" +
"n_support, support = m.n_support_.astype(np.int64), np.sort(m.support_).astype(np.int64)\n" +
"rho, dual = -m.intercept_, np.sort(np.abs(m.dual_coef_), axis=1)\n" +
"dec, pred = m.decision_function(Xs), m.predict(Xs)");

AssertEqual(py.n_support, np.array(model.NumberOfSVPerClass.Select(n => (long)n).ToArray()), "n_support_");
AssertEqual(py.support, np.sort(np.array(model.SupportVectorIndices.Select(i => (long)i - 1).ToArray())), "support_");
AssertClose(py.rho, np.array(model.Rho), 1e-9, 1e-9, "rho (-intercept_)");
NDArray ours = np.sort(np.abs(np.array(model.SupportVectorCoefficients.SelectMany(row => row).ToArray()).reshape(2, -1)), axis: 1);
AssertClose(py.dual, ours, 1e-9, 1e-9, "|dual_coef_|");
AssertClose(py.dec, model.DecisionValues(dense), 1e-9, 1e-9, "decision_function (ovo)");
AssertEqual(py.pred, model.PredictLabels(dense), "predict");
}
}
}
}
Loading