From b321b3819c8b43d8a5ed02b4574bf56f7fb8378e Mon Sep 17 00:00:00 2001 From: Samuel Akopyan Date: Wed, 11 Mar 2026 02:20:38 +0200 Subject: [PATCH 1/4] ML-400 Converted back Network interface --- CHANGELOG.md | 4 +- src/Classifiers/LogisticRegression.php | 3 +- src/Classifiers/MultilayerPerceptron.php | 3 +- src/Classifiers/SoftmaxClassifier.php | 3 +- src/NeuralNet/FeedForward.php | 2 +- src/NeuralNet/Network.php | 254 +--------------- .../Networks/Base/Contracts/Network.php | 49 ++++ .../Base/FeedForward}/FeedForward.php | 13 +- src/NeuralNet/Networks/Network.php | 275 ------------------ src/NeuralNet/Snapshots/Snapshot.php | 2 +- src/Regressors/Adaline.php | 3 +- src/Regressors/MLPRegressor.php | 3 +- .../FeedForwards/FeedForwardTest.php | 24 +- tests/NeuralNet/Layers/Swish/SwishTest.php | 2 +- tests/NeuralNet/NetworkTest.php | 3 +- tests/NeuralNet/Networks/NetworkTest.php | 5 +- tests/NeuralNet/SnapshotTest.php | 3 +- tests/NeuralNet/Snapshots/SnapshotTest.php | 5 +- 18 files changed, 96 insertions(+), 560 deletions(-) create mode 100644 src/NeuralNet/Networks/Base/Contracts/Network.php rename src/NeuralNet/{FeedForwards => Networks/Base/FeedForward}/FeedForward.php (97%) delete mode 100644 src/NeuralNet/Networks/Network.php diff --git a/CHANGELOG.md b/CHANGELOG.md index b70609e33..831396a04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,9 @@ - Removed output layer L2 Penalty parameter from MLP Learners - Remove Network interface - RBX Serializer only tracks major library version number - + - Convert NeuralNet classes to use NDArray instead of Matrix + - Converted back Network interface + - 2.5.0 - Added Vantage Point Spatial tree - Blob Generator can now `simulate()` a Dataset object diff --git a/src/Classifiers/LogisticRegression.php b/src/Classifiers/LogisticRegression.php index 8f5f4c2c0..3d749dab7 100644 --- a/src/Classifiers/LogisticRegression.php +++ b/src/Classifiers/LogisticRegression.php @@ -2,6 +2,7 @@ namespace Rubix\ML\Classifiers; +use Rubix\ML\NeuralNet\FeedForward; use Rubix\ML\Online; use Rubix\ML\Learner; use Rubix\ML\Verbose; @@ -289,7 +290,7 @@ public function train(Dataset $dataset) : void $classes = $dataset->possibleOutcomes(); - $this->network = new Network( + $this->network = new FeedForward( new Placeholder1D($dataset->numFeatures()), [new Dense(1, $this->l2Penalty, true, new Xavier1())], new Binary($classes, $this->costFn), diff --git a/src/Classifiers/MultilayerPerceptron.php b/src/Classifiers/MultilayerPerceptron.php index e296915af..34e3d8fe6 100644 --- a/src/Classifiers/MultilayerPerceptron.php +++ b/src/Classifiers/MultilayerPerceptron.php @@ -2,6 +2,7 @@ namespace Rubix\ML\Classifiers; +use Rubix\ML\NeuralNet\FeedForward; use Rubix\ML\Online; use Rubix\ML\Learner; use Rubix\ML\Verbose; @@ -370,7 +371,7 @@ public function train(Dataset $dataset) : void $hiddenLayers[] = new Dense(count($classes), 0.0, true, new Xavier1()); - $this->network = new Network( + $this->network = new FeedForward( new Placeholder1D($dataset->numFeatures()), $hiddenLayers, new Multiclass($classes, $this->costFn), diff --git a/src/Classifiers/SoftmaxClassifier.php b/src/Classifiers/SoftmaxClassifier.php index 3b8581771..560000671 100644 --- a/src/Classifiers/SoftmaxClassifier.php +++ b/src/Classifiers/SoftmaxClassifier.php @@ -2,6 +2,7 @@ namespace Rubix\ML\Classifiers; +use Rubix\ML\NeuralNet\FeedForward; use Rubix\ML\Online; use Rubix\ML\Learner; use Rubix\ML\Verbose; @@ -285,7 +286,7 @@ public function train(Dataset $dataset) : void $classes = $dataset->possibleOutcomes(); - $this->network = new Network( + $this->network = new FeedForward( new Placeholder1D($dataset->numFeatures()), [new Dense(count($classes), $this->l2Penalty, true, new Xavier1())], new Multiclass($classes, $this->costFn), diff --git a/src/NeuralNet/FeedForward.php b/src/NeuralNet/FeedForward.php index 5cffe79b1..4849f1681 100644 --- a/src/NeuralNet/FeedForward.php +++ b/src/NeuralNet/FeedForward.php @@ -27,7 +27,7 @@ * @package Rubix/ML * @author Andrew DalPino */ -class FeedForward extends Network +class FeedForward implements Network { /** * The input layer to the network. diff --git a/src/NeuralNet/Network.php b/src/NeuralNet/Network.php index 57e7cfd25..26a57e9d4 100644 --- a/src/NeuralNet/Network.php +++ b/src/NeuralNet/Network.php @@ -2,270 +2,24 @@ namespace Rubix\ML\NeuralNet; -use Tensor\Matrix; -use Rubix\ML\Encoding; -use Rubix\ML\Datasets\Dataset; -use Rubix\ML\Datasets\Labeled; -use Rubix\ML\NeuralNet\Layers\Input; -use Rubix\ML\NeuralNet\Layers\Output; -use Rubix\ML\NeuralNet\Layers\Parametric; -use Rubix\ML\NeuralNet\Optimizers\Adaptive; -use Rubix\ML\NeuralNet\Optimizers\Optimizer; use Traversable; -use function array_reverse; - /** * Network * - * A neural network implementation consisting of an input and output layer and any number - * of intermediate hidden layers. - * * @internal * * @category Machine Learning * @package Rubix/ML * @author Andrew DalPino + * @author Samuel Akopyan */ -class Network +interface Network { /** - * The input layer to the network. - * - * @var Input - */ - protected Input $input; - - /** - * The hidden layers of the network. - * - * @var list - */ - protected array $hidden = [ - // - ]; - - /** - * The pathing of the backward pass through the hidden layers. - * - * @var list - */ - protected array $backPass = [ - // - ]; - - /** - * The output layer of the network. - * - * @var Output - */ - protected Output $output; - - /** - * The gradient descent optimizer used to train the network. - * - * @var Optimizer - */ - protected Optimizer $optimizer; - - /** - * @param Input $input - * @param Layers\Hidden[] $hidden - * @param Output $output - * @param Optimizer $optimizer - */ - public function __construct(Input $input, array $hidden, Output $output, Optimizer $optimizer) - { - $hidden = array_values($hidden); - - $backPass = array_reverse($hidden); - - $this->input = $input; - $this->hidden = $hidden; - $this->output = $output; - $this->optimizer = $optimizer; - $this->backPass = $backPass; - } - - /** - * Return the input layer. - * - * @return Input - */ - public function input() : Input - { - return $this->input; - } - - /** - * Return an array of hidden layers indexed left to right. - * - * @return list - */ - public function hidden() : array - { - return $this->hidden; - } - - /** - * Return the output layer. - * - * @return Output - */ - public function output() : Output - { - return $this->output; - } - - /** - * Return all the layers in the network. + * Return the layers of the network. * * @return Traversable */ - public function layers() : Traversable - { - yield $this->input; - - yield from $this->hidden; - - yield $this->output; - } - - /** - * Return the number of trainable parameters in the network. - * - * @return int - */ - public function numParams() : int - { - $numParams = 0; - - foreach ($this->layers() as $layer) { - if ($layer instanceof Parametric) { - foreach ($layer->parameters() as $parameter) { - $numParams += $parameter->param()->size(); - } - } - } - - return $numParams; - } - - /** - * Initialize the parameters of the layers and warm the optimizer cache. - */ - public function initialize() : void - { - $fanIn = 1; - - foreach ($this->layers() as $layer) { - $fanIn = $layer->initialize($fanIn); - } - - if ($this->optimizer instanceof Adaptive) { - foreach ($this->layers() as $layer) { - if ($layer instanceof Parametric) { - foreach ($layer->parameters() as $param) { - $this->optimizer->warm($param); - } - } - } - } - } - - /** - * Run an inference pass and return the activations at the output layer. - * - * @param Dataset $dataset - * @return Matrix - */ - public function infer(Dataset $dataset) : Matrix - { - $input = Matrix::quick($dataset->samples())->transpose(); - - foreach ($this->layers() as $layer) { - $input = $layer->infer($input); - } - - return $input->transpose(); - } - - /** - * Perform a forward and backward pass of the network in one call. Returns - * the loss from the backward pass. - * - * @param Labeled $dataset - * @return float - */ - public function roundtrip(Labeled $dataset) : float - { - $input = Matrix::quick($dataset->samples())->transpose(); - - $this->feed($input); - - $loss = $this->backpropagate($dataset->labels()); - - return $loss; - } - - /** - * Feed a batch through the network and return a matrix of activations at the output later. - * - * @param Matrix $input - * @return Matrix - */ - public function feed(Matrix $input) : Matrix - { - foreach ($this->layers() as $layer) { - $input = $layer->forward($input); - } - - return $input; - } - - /** - * Backpropagate the gradient of the cost function and return the loss. - * - * @param list $labels - * @return float - */ - public function backpropagate(array $labels) : float - { - [$gradient, $loss] = $this->output->back($labels, $this->optimizer); - - foreach ($this->backPass as $layer) { - $gradient = $layer->back($gradient, $this->optimizer); - } - - return $loss; - } - - /** - * Export the network architecture as a graph in dot format. - * - * @return Encoding - */ - public function exportGraphviz() : Encoding - { - $dot = 'digraph Tree {' . PHP_EOL; - $dot .= ' node [shape=box, fontname=helvetica];' . PHP_EOL; - - $layerNum = 0; - - foreach ($this->layers() as $layer) { - ++$layerNum; - - $dot .= " N$layerNum [label=\"$layer\",style=\"rounded\"]" . PHP_EOL; - - if ($layerNum > 1) { - $parentId = $layerNum - 1; - - $dot .= " N{$parentId} -> N{$layerNum};" . PHP_EOL; - } - } - - $dot .= '}'; - - return new Encoding($dot); - } + public function layers() : Traversable; } diff --git a/src/NeuralNet/Networks/Base/Contracts/Network.php b/src/NeuralNet/Networks/Base/Contracts/Network.php new file mode 100644 index 000000000..c6f34abbf --- /dev/null +++ b/src/NeuralNet/Networks/Base/Contracts/Network.php @@ -0,0 +1,49 @@ + + */ +interface Network +{ + /** + * Return the layers of the network. + * + * @return Traversable + */ + public function layers() : Traversable; + + /** + * Return the input layer. + * + * @return Input + */ + public function input() : Input; + + /** + * Return an array of hidden layers indexed left to right. + * + * @return list + */ + public function hidden() : array; + + /** + * Return the output layer. + * + * @return Output + */ + public function output() : Output; +} diff --git a/src/NeuralNet/FeedForwards/FeedForward.php b/src/NeuralNet/Networks/Base/FeedForward/FeedForward.php similarity index 97% rename from src/NeuralNet/FeedForwards/FeedForward.php rename to src/NeuralNet/Networks/Base/FeedForward/FeedForward.php index aea7fe6ed..79e52ef26 100644 --- a/src/NeuralNet/FeedForwards/FeedForward.php +++ b/src/NeuralNet/Networks/Base/FeedForward/FeedForward.php @@ -1,22 +1,21 @@ */ -class FeedForward extends Network +class FeedForward implements Network { /** * The input layer to the network. diff --git a/src/NeuralNet/Networks/Network.php b/src/NeuralNet/Networks/Network.php deleted file mode 100644 index 6554940b3..000000000 --- a/src/NeuralNet/Networks/Network.php +++ /dev/null @@ -1,275 +0,0 @@ - - */ -class Network -{ - /** - * The input layer to the network. - * - * @var Input - */ - protected Input $input; - - /** - * The hidden layers of the network. - * - * @var list - */ - protected array $hidden = [ - // - ]; - - /** - * The pathing of the backward pass through the hidden layers. - * - * @var list - */ - protected array $backPass = [ - // - ]; - - /** - * The output layer of the network. - * - * @var Output - */ - protected Output $output; - - /** - * The gradient descent optimizer used to train the network. - * - * @var Optimizer - */ - protected Optimizer $optimizer; - - /** - * @param Input $input - * @param Hidden[] $hidden - * @param Output $output - * @param Optimizer $optimizer - */ - public function __construct(Input $input, array $hidden, Output $output, Optimizer $optimizer) - { - $hidden = array_values($hidden); - - $backPass = array_reverse($hidden); - - $this->input = $input; - $this->hidden = $hidden; - $this->output = $output; - $this->optimizer = $optimizer; - $this->backPass = $backPass; - } - - /** - * Return the input layer. - * - * @return Input - */ - public function input() : Input - { - return $this->input; - } - - /** - * Return an array of hidden layers indexed left to right. - * - * @return list - */ - public function hidden() : array - { - return $this->hidden; - } - - /** - * Return the output layer. - * - * @return Output - */ - public function output() : Output - { - return $this->output; - } - - /** - * Return all the layers in the network. - * - * @return Traversable - */ - public function layers() : Traversable - { - yield $this->input; - - yield from $this->hidden; - - yield $this->output; - } - - /** - * Return the number of trainable parameters in the network. - * - * @return int - */ - public function numParams() : int - { - $numParams = 0; - - foreach ($this->layers() as $layer) { - if ($layer instanceof Parametric) { - foreach ($layer->parameters() as $parameter) { - $numParams += $parameter->param()->size(); - } - } - } - - return $numParams; - } - - /** - * Initialize the parameters of the layers and warm the optimizer cache. - */ - public function initialize() : void - { - $fanIn = 1; - - foreach ($this->layers() as $layer) { - $fanIn = $layer->initialize($fanIn); - } - - if ($this->optimizer instanceof Adaptive) { - foreach ($this->layers() as $layer) { - if ($layer instanceof Parametric) { - foreach ($layer->parameters() as $param) { - $this->optimizer->warm($param); - } - } - } - } - } - - /** - * Run an inference pass and return the activations at the output layer. - * - * @param Dataset $dataset - * @return NDArray - */ - public function infer(Dataset $dataset) : NDArray - { - $input = NumPower::transpose(NumPower::array($dataset->samples()), [1, 0]); - - foreach ($this->layers() as $layer) { - $input = $layer->infer($input); - } - - return NumPower::transpose($input, [1, 0]); - } - - /** - * Perform a forward and backward pass of the network in one call. Returns - * the loss from the backward pass. - * - * @param Labeled $dataset - * @return float - */ - public function roundtrip(Labeled $dataset) : float - { - $input = NumPower::transpose(NumPower::array($dataset->samples()), [1, 0]); - - $this->feed($input); - - $loss = $this->backpropagate($dataset->labels()); - - return $loss; - } - - /** - * Feed a batch through the network and return a matrix of activations at the output later. - * - * @param NDArray $input - * @return NDArray - */ - public function feed(NDArray $input) : NDArray - { - foreach ($this->layers() as $layer) { - $input = $layer->forward($input); - } - - return $input; - } - - /** - * Backpropagate the gradient of the cost function and return the loss. - * - * @param list $labels - * @return float - */ - public function backpropagate(array $labels) : float - { - [$gradient, $loss] = $this->output->back($labels, $this->optimizer); - - foreach ($this->backPass as $layer) { - $gradient = $layer->back($gradient, $this->optimizer); - } - - return $loss; - } - - /** - * Export the network architecture as a graph in dot format. - * - * @return Encoding - */ - public function exportGraphviz() : Encoding - { - $dot = 'digraph Tree {' . PHP_EOL; - $dot .= ' node [shape=box, fontname=helvetica];' . PHP_EOL; - - $layerNum = 0; - - foreach ($this->layers() as $layer) { - ++$layerNum; - - $dot .= " N$layerNum [label=\"$layer\",style=\"rounded\"]" . PHP_EOL; - - if ($layerNum > 1) { - $parentId = $layerNum - 1; - - $dot .= " N{$parentId} -> N{$layerNum};" . PHP_EOL; - } - } - - $dot .= '}'; - - return new Encoding($dot); - } -} diff --git a/src/NeuralNet/Snapshots/Snapshot.php b/src/NeuralNet/Snapshots/Snapshot.php index 033224d5c..04119ee7b 100644 --- a/src/NeuralNet/Snapshots/Snapshot.php +++ b/src/NeuralNet/Snapshots/Snapshot.php @@ -4,7 +4,7 @@ use Rubix\ML\NeuralNet\Layers\Base\Contracts\Parametric; use Rubix\ML\Exceptions\InvalidArgumentException; -use Rubix\ML\NeuralNet\Networks\Network; +use Rubix\ML\NeuralNet\Networks\Base\Contracts\Network; use Rubix\ML\NeuralNet\Parameters\Parameter; /** diff --git a/src/Regressors/Adaline.php b/src/Regressors/Adaline.php index 22e8201d8..40940a1f0 100644 --- a/src/Regressors/Adaline.php +++ b/src/Regressors/Adaline.php @@ -2,6 +2,7 @@ namespace Rubix\ML\Regressors; +use Rubix\ML\NeuralNet\FeedForward; use Rubix\ML\Online; use Rubix\ML\Learner; use Rubix\ML\Verbose; @@ -277,7 +278,7 @@ public function train(Dataset $dataset) : void { DatasetIsNotEmpty::with($dataset)->check(); - $this->network = new Network( + $this->network = new FeedForward( new Placeholder1D($dataset->numFeatures()), [new Dense(1, $this->l2Penalty, true, new Xavier2())], new Continuous($this->costFn), diff --git a/src/Regressors/MLPRegressor.php b/src/Regressors/MLPRegressor.php index 710e83f76..769eee4f9 100644 --- a/src/Regressors/MLPRegressor.php +++ b/src/Regressors/MLPRegressor.php @@ -2,6 +2,7 @@ namespace Rubix\ML\Regressors; +use Rubix\ML\NeuralNet\FeedForward; use Rubix\ML\Online; use Rubix\ML\Learner; use Rubix\ML\Verbose; @@ -356,7 +357,7 @@ public function train(Dataset $dataset) : void $hiddenLayers[] = new Dense(1, 0.0, true, new Xavier2()); - $this->network = new Network( + $this->network = new FeedForward( new Placeholder1D($dataset->numFeatures()), $hiddenLayers, new Continuous($this->costFn), diff --git a/tests/NeuralNet/FeedForwards/FeedForwardTest.php b/tests/NeuralNet/FeedForwards/FeedForwardTest.php index 84226fc70..e2ac22286 100644 --- a/tests/NeuralNet/FeedForwards/FeedForwardTest.php +++ b/tests/NeuralNet/FeedForwards/FeedForwardTest.php @@ -2,25 +2,24 @@ namespace Rubix\ML\Tests\NeuralNet\FeedForwards; +use PHPUnit\Framework\Attributes\Before; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\Attributes\TestDox; +use PHPUnit\Framework\TestCase; use Rubix\ML\Datasets\Labeled; -use Rubix\ML\NeuralNet\FeedForwards\FeedForward; +use Rubix\ML\NeuralNet\ActivationFunctions\ReLU\ReLU; +use Rubix\ML\NeuralNet\CostFunctions\CrossEntropy\CrossEntropy; +use Rubix\ML\NeuralNet\Layers\Activation\Activation; use Rubix\ML\NeuralNet\Layers\Base\Contracts\Hidden; use Rubix\ML\NeuralNet\Layers\Base\Contracts\Input; +use Rubix\ML\NeuralNet\Layers\Base\Contracts\Output; use Rubix\ML\NeuralNet\Layers\Dense\Dense; -use Rubix\ML\NeuralNet\Layers\Activation\Activation; use Rubix\ML\NeuralNet\Layers\Multiclass\Multiclass; use Rubix\ML\NeuralNet\Layers\Placeholder1D\Placeholder1D; -use Rubix\ML\NeuralNet\Layers\Base\Contracts\Output; -use Rubix\ML\NeuralNet\Networks\Network; +use Rubix\ML\NeuralNet\Networks\Base\FeedForward\FeedForward; use Rubix\ML\NeuralNet\Optimizers\Adam\Adam; -use Rubix\ML\NeuralNet\ActivationFunctions\ReLU\ReLU; -use Rubix\ML\NeuralNet\CostFunctions\CrossEntropy\CrossEntropy; -use PHPUnit\Framework\TestCase; -use PHPUnit\Framework\Attributes\Before; -use PHPUnit\Framework\Attributes\CoversClass; -use PHPUnit\Framework\Attributes\Group; -use PHPUnit\Framework\Attributes\Test; -use PHPUnit\Framework\Attributes\TestDox; #[Group('NeuralNet')] #[CoversClass(FeedForward::class)] @@ -80,7 +79,6 @@ protected function setUp() : void public function build() : void { self::assertInstanceOf(FeedForward::class, $this->network); - self::assertInstanceOf(Network::class, $this->network); } #[Test] diff --git a/tests/NeuralNet/Layers/Swish/SwishTest.php b/tests/NeuralNet/Layers/Swish/SwishTest.php index 5f8d55503..0fb9da270 100644 --- a/tests/NeuralNet/Layers/Swish/SwishTest.php +++ b/tests/NeuralNet/Layers/Swish/SwishTest.php @@ -73,7 +73,7 @@ public static function initializeForwardBackInferProvider() : array 'backExpected' => [ [0.2319176, 0.7695808, 0.0450083], [0.2749583, 0.1099833, 0.0108810], - [0.1252499, -0.0012326, 0.2314345], + [0.1252494, -0.0012326, 0.2314345], ], 'inferExpected' => [ [0.7306671, 2.3094806, -0.0475070], diff --git a/tests/NeuralNet/NetworkTest.php b/tests/NeuralNet/NetworkTest.php index 1421c0a35..fed2bb57d 100644 --- a/tests/NeuralNet/NetworkTest.php +++ b/tests/NeuralNet/NetworkTest.php @@ -7,6 +7,7 @@ use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Group; use Rubix\ML\Datasets\Labeled; +use Rubix\ML\NeuralNet\FeedForward; use Rubix\ML\NeuralNet\Layers\Hidden; use Rubix\ML\NeuralNet\Layers\Input; use Rubix\ML\NeuralNet\Network; @@ -63,7 +64,7 @@ classes: ['yes', 'no', 'maybe'], costFn: new CrossEntropy() ); - $this->network = new Network( + $this->network = new FeedForward( input: $this->input, hidden: $this->hidden, output: $this->output, diff --git a/tests/NeuralNet/Networks/NetworkTest.php b/tests/NeuralNet/Networks/NetworkTest.php index 0197c225d..5cd719f2a 100644 --- a/tests/NeuralNet/Networks/NetworkTest.php +++ b/tests/NeuralNet/Networks/NetworkTest.php @@ -14,7 +14,8 @@ use Rubix\ML\NeuralNet\Layers\Activation\Activation; use Rubix\ML\NeuralNet\Layers\Multiclass\Multiclass; use Rubix\ML\NeuralNet\Layers\Placeholder1D\Placeholder1D; -use Rubix\ML\NeuralNet\Networks\Network; +use Rubix\ML\NeuralNet\Networks\Base\Contracts\Network; +use Rubix\ML\NeuralNet\Networks\Base\FeedForward\FeedForward; use Rubix\ML\NeuralNet\Optimizers\Adam\Adam; use Rubix\ML\NeuralNet\ActivationFunctions\ReLU\ReLU; use Rubix\ML\NeuralNet\CostFunctions\CrossEntropy\CrossEntropy; @@ -63,7 +64,7 @@ classes: ['yes', 'no', 'maybe'], costFn: new CrossEntropy() ); - $this->network = new Network( + $this->network = new FeedForward( input: $this->input, hidden: $this->hidden, output: $this->output, diff --git a/tests/NeuralNet/SnapshotTest.php b/tests/NeuralNet/SnapshotTest.php index bdf41829e..5cad02e30 100644 --- a/tests/NeuralNet/SnapshotTest.php +++ b/tests/NeuralNet/SnapshotTest.php @@ -6,6 +6,7 @@ use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Group; +use Rubix\ML\NeuralNet\FeedForward; use Rubix\ML\NeuralNet\Snapshot; use Rubix\ML\NeuralNet\Network; use Rubix\ML\NeuralNet\Layers\Dense; @@ -27,7 +28,7 @@ class SnapshotTest extends TestCase public function testTake() : void { - $network = new Network( + $network = new FeedForward( input: new Placeholder1D(1), hidden: [ new Dense(10), diff --git a/tests/NeuralNet/Snapshots/SnapshotTest.php b/tests/NeuralNet/Snapshots/SnapshotTest.php index ecde317e3..496567fbd 100644 --- a/tests/NeuralNet/Snapshots/SnapshotTest.php +++ b/tests/NeuralNet/Snapshots/SnapshotTest.php @@ -7,8 +7,9 @@ use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Group; use Rubix\ML\Exceptions\InvalidArgumentException; +use Rubix\ML\NeuralNet\Networks\Base\Contracts\Network; +use Rubix\ML\NeuralNet\Networks\Base\FeedForward\FeedForward; use Rubix\ML\NeuralNet\Snapshots\Snapshot; -use Rubix\ML\NeuralNet\Networks\Network; use Rubix\ML\NeuralNet\Layers\Dense\Dense; use Rubix\ML\NeuralNet\Layers\Binary\Binary; use Rubix\ML\NeuralNet\Layers\Activation\Activation; @@ -39,7 +40,7 @@ public function testConstructorThrowsWithWrongParameters() : void public function testTake() : void { - $network = new Network( + $network = new FeedForward( input: new Placeholder1D(1), hidden: [ new Dense(10), From eec032e49abd745caccd3c65a04a1d7670e26f99 Mon Sep 17 00:00:00 2001 From: Samuel Akopyan Date: Wed, 11 Mar 2026 02:25:17 +0200 Subject: [PATCH 2/4] ML-400 added normalizeSamples to FeedForward --- .../Networks/Base/FeedForward/FeedForward.php | 46 +++++++++++++++++-- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/src/NeuralNet/Networks/Base/FeedForward/FeedForward.php b/src/NeuralNet/Networks/Base/FeedForward/FeedForward.php index 79e52ef26..881f1fc53 100644 --- a/src/NeuralNet/Networks/Base/FeedForward/FeedForward.php +++ b/src/NeuralNet/Networks/Base/FeedForward/FeedForward.php @@ -72,6 +72,13 @@ class FeedForward implements Network */ protected Optimizer $optimizer; + /** + * Whether to normalize the samples. + * + * @var bool + */ + private bool $normalizeSamples; + /** * @param Input $input * @param Hidden[] $hidden @@ -89,6 +96,8 @@ public function __construct(Input $input, array $hidden, Output $output, Optimiz $this->output = $output; $this->optimizer = $optimizer; $this->backPass = $backPass; + + $this->normalizeSamples = false; } /** @@ -185,10 +194,29 @@ public function initialize() : void */ public function infer(Dataset $dataset) : NDArray { - $input = NumPower::transpose(NumPower::array($dataset->samples()), [1, 0]); + if ($this->normalizeSamples) { + if ($dataset->empty()) { + return NumPower::array([]); + } - foreach ($this->layers() as $layer) { - $input = $layer->infer($input); + $normalizedSamples = $this->normalizeSamples($dataset->samples()); + $input = NumPower::transpose(NumPower::array($normalizedSamples), [1, 0]); + + foreach ($this->layers() as $layer) { + $input = $layer->infer($input); + } + + $shape = $input->shape(); + + if (count($shape) === 1) { + $input = NumPower::reshape($input, [1, $shape[0]]); + } + } else { + $input = NumPower::transpose(NumPower::array($dataset->samples()), [1, 0]); + + foreach ($this->layers() as $layer) { + $input = $layer->infer($input); + } } return NumPower::transpose($input, [1, 0]); @@ -272,4 +300,16 @@ public function exportGraphviz() : Encoding return new Encoding($dot); } + + /** + * Normalize samples to a strict list-of-lists with sequential numeric keys. + * NumPower's C extension expects packed arrays and can error or behave unpredictably + * when given arrays with non-sequential keys (e.g. after randomize/take/fold operations). + * @param array $samples + * @return array + */ + private function normalizeSamples(array $samples) : array + { + return array_map('array_values', array_values($samples)); + } } From 34a1d8cf5b868dce4b7d22ac9a4eba2d459efef7 Mon Sep 17 00:00:00 2001 From: Samuel Akopyan Date: Wed, 11 Mar 2026 02:33:19 +0200 Subject: [PATCH 3/4] ML-400 changed path to FeedForward --- .../{Base => }/FeedForward/FeedForward.php | 2 +- tests/NeuralNet/FeedForwards/FeedForwardTest.php | 2 +- tests/NeuralNet/Networks/NetworkTest.php | 12 ++++++------ tests/NeuralNet/Snapshots/SnapshotTest.php | 16 ++++++++-------- 4 files changed, 16 insertions(+), 16 deletions(-) rename src/NeuralNet/Networks/{Base => }/FeedForward/FeedForward.php (99%) diff --git a/src/NeuralNet/Networks/Base/FeedForward/FeedForward.php b/src/NeuralNet/Networks/FeedForward/FeedForward.php similarity index 99% rename from src/NeuralNet/Networks/Base/FeedForward/FeedForward.php rename to src/NeuralNet/Networks/FeedForward/FeedForward.php index 881f1fc53..5726b21c6 100644 --- a/src/NeuralNet/Networks/Base/FeedForward/FeedForward.php +++ b/src/NeuralNet/Networks/FeedForward/FeedForward.php @@ -1,6 +1,6 @@ Date: Wed, 11 Mar 2026 23:07:20 +0200 Subject: [PATCH 4/4] ML-400 changed path to FeedForward --- CHANGELOG.md | 1 - .../Networks/Base/Contracts/Network.php | 29 +++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 831396a04..93958bbf6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,6 @@ - Exportable Extractors now append by default with option to overwrite - Added validation interval parameter to MLPs and GBM Learners - Removed output layer L2 Penalty parameter from MLP Learners - - Remove Network interface - RBX Serializer only tracks major library version number - Convert NeuralNet classes to use NDArray instead of Matrix - Converted back Network interface diff --git a/src/NeuralNet/Networks/Base/Contracts/Network.php b/src/NeuralNet/Networks/Base/Contracts/Network.php index c6f34abbf..641649aba 100644 --- a/src/NeuralNet/Networks/Base/Contracts/Network.php +++ b/src/NeuralNet/Networks/Base/Contracts/Network.php @@ -2,6 +2,9 @@ namespace Rubix\ML\NeuralNet\Networks\Base\Contracts; +use NDArray; +use Rubix\ML\Datasets\Dataset; +use Rubix\ML\Datasets\Labeled; use Rubix\ML\NeuralNet\Layers\Base\Contracts\Hidden; use Rubix\ML\NeuralNet\Layers\Base\Contracts\Input; use Rubix\ML\NeuralNet\Layers\Base\Contracts\Output; @@ -26,6 +29,32 @@ interface Network */ public function layers() : Traversable; + /** + * Return the number of trainable parameters in the network. + * + * @return int + */ + public function numParams() : int; + + /** + * Initialize the parameters of the layers and warm the optimizer cache. + */ + public function initialize() : void; + + /** + * Run an inference pass and return the activations at the output layer. + * + * @param Dataset $dataset + * @return NDArray + */ + public function infer(Dataset $dataset): NDArray; + + /** + * @param Labeled $dataset + * @return float + */ + public function roundtrip(Labeled $dataset): float; + /** * Return the input layer. *