From 4c16268996eb78073cf6ac628f6824655d744754 Mon Sep 17 00:00:00 2001 From: Ronan Giron Date: Fri, 26 Jan 2024 03:28:57 +0100 Subject: [PATCH 1/2] Wrapper interface (#314) * Add Wrapper interface for models wrappers * Add WrapperAware trait * Fix PhpDoc * Revert "Add WrapperAware trait" This reverts commit 241abc4317eec701211b7a88a17a1b610c366dfe. * Rename Wrapper interface to EstimatorWrapper * PHP CS fix --- src/AnomalyDetectors/LocalOutlierFactor.php | 2 +- src/AnomalyDetectors/Loda.php | 2 +- src/AnomalyDetectors/OneClassSVM.php | 4 ++-- src/BootstrapAggregator.php | 2 +- src/Classifiers/AdaBoost.php | 2 +- src/Classifiers/KDNeighbors.php | 2 +- src/Classifiers/KNearestNeighbors.php | 2 +- src/Classifiers/LogisticRegression.php | 6 +++--- src/Classifiers/LogitBoost.php | 4 ++-- src/Classifiers/MultilayerPerceptron.php | 8 ++++---- src/Classifiers/OneVsRest.php | 2 +- src/Classifiers/RadiusNeighbors.php | 2 +- src/Classifiers/RandomForest.php | 2 +- src/Classifiers/SoftmaxClassifier.php | 6 +++--- src/Clusterers/DBSCAN.php | 2 +- src/Clusterers/FuzzyCMeans.php | 4 ++-- src/Clusterers/GaussianMixture.php | 2 +- src/Clusterers/KMeans.php | 4 ++-- src/Clusterers/MeanShift.php | 4 ++-- src/Clusterers/Seeders/KMC2.php | 2 +- src/Clusterers/Seeders/PlusPlus.php | 2 +- src/Datasets/Generators/Blob.php | 2 +- src/Datasets/Generators/Circle.php | 2 +- src/Datasets/Generators/HalfMoon.php | 2 +- src/Datasets/Generators/Hyperplane.php | 2 +- src/Datasets/Generators/SwissRoll.php | 2 +- src/EstimatorWrapper.php | 20 +++++++++++++++++++ src/Extractors/SQLTable.php | 2 +- src/Graph/Nodes/Clique.php | 2 +- src/Graph/Nodes/Neighborhood.php | 2 +- .../Nodes/Traits/HasBinaryChildrenTrait.php | 4 ++-- src/Graph/Trees/BallTree.php | 4 ++-- src/Graph/Trees/DecisionTree.php | 2 +- src/Graph/Trees/ITree.php | 2 +- src/Graph/Trees/KDTree.php | 4 ++-- src/GridSearch.php | 8 ++++---- src/NeuralNet/FeedForward.php | 6 +++--- src/NeuralNet/Layers/Activation.php | 6 +++--- src/NeuralNet/Layers/BatchNorm.php | 10 +++++----- src/NeuralNet/Layers/Binary.php | 8 ++++---- src/NeuralNet/Layers/Continuous.php | 4 ++-- src/NeuralNet/Layers/Dense.php | 10 +++++----- src/NeuralNet/Layers/Dropout.php | 2 +- src/NeuralNet/Layers/Multiclass.php | 8 ++++---- src/NeuralNet/Layers/PReLU.php | 6 +++--- src/NeuralNet/Layers/Swish.php | 10 +++++----- src/NeuralNet/Parameter.php | 2 +- src/PersistentModel.php | 8 ++++---- src/Pipeline.php | 4 ++-- src/Regressors/Adaline.php | 6 +++--- src/Regressors/GradientBoost.php | 4 ++-- src/Regressors/KDNeighborsRegressor.php | 2 +- src/Regressors/KNNRegressor.php | 2 +- src/Regressors/MLPRegressor.php | 8 ++++---- src/Regressors/RadiusNeighborsRegressor.php | 2 +- src/Regressors/Ridge.php | 2 +- src/Regressors/SVR.php | 4 ++-- src/Serializers/GzipNative.php | 2 +- src/Serializers/RBX.php | 2 +- .../DatasetHasDimensionality.php | 2 +- src/Specifications/DatasetIsLabeled.php | 2 +- src/Specifications/DatasetIsNotEmpty.php | 2 +- .../EstimatorIsCompatibleWithMetric.php | 4 ++-- .../LabelsAreCompatibleWithLearner.php | 4 ++-- .../SamplesAreCompatibleWithDistance.php | 4 ++-- .../SamplesAreCompatibleWithEstimator.php | 4 ++-- .../SamplesAreCompatibleWithTransformer.php | 4 ++-- src/Tokenizers/KSkipNGram.php | 4 ++-- src/Tokenizers/NGram.php | 4 ++-- src/Traits/LoggerAware.php | 2 +- src/Traits/Multiprocessing.php | 2 +- src/Transformers/GaussianRandomProjector.php | 2 +- src/Transformers/HotDeckImputer.php | 2 +- src/Transformers/KNNImputer.php | 2 +- .../LinearDiscriminantAnalysis.php | 2 +- src/Transformers/MissingDataImputer.php | 4 ++-- .../PrincipalComponentAnalysis.php | 2 +- src/Transformers/TSNE.php | 2 +- src/Transformers/TokenHashingVectorizer.php | 2 +- src/Transformers/TruncatedSVD.php | 2 +- src/Transformers/WordCountVectorizer.php | 2 +- tests/Graph/Nodes/NeighborhoodTest.php | 2 +- tests/NeuralNet/ParameterTest.php | 2 +- tests/Transformers/ImageRotatorTest.php | 2 +- 84 files changed, 165 insertions(+), 145 deletions(-) create mode 100644 src/EstimatorWrapper.php diff --git a/src/AnomalyDetectors/LocalOutlierFactor.php b/src/AnomalyDetectors/LocalOutlierFactor.php index 4ebda0f00..5c798b4b6 100644 --- a/src/AnomalyDetectors/LocalOutlierFactor.php +++ b/src/AnomalyDetectors/LocalOutlierFactor.php @@ -67,7 +67,7 @@ class LocalOutlierFactor implements Estimator, Learner, Scoring, Persistable * * @var Spatial */ - protected \Rubix\ML\Graph\Trees\Spatial $tree; + protected Spatial $tree; /** * The precomputed k distances between each training sample and its k'th nearest neighbor. diff --git a/src/AnomalyDetectors/Loda.php b/src/AnomalyDetectors/Loda.php index f06b9a4a7..cf76762e3 100644 --- a/src/AnomalyDetectors/Loda.php +++ b/src/AnomalyDetectors/Loda.php @@ -100,7 +100,7 @@ class Loda implements Estimator, Learner, Online, Scoring, Persistable * * @var \Tensor\Matrix|null */ - protected ?\Tensor\Matrix $r = null; + protected ?Matrix $r = null; /** * The edges and bin counts of each histogram. diff --git a/src/AnomalyDetectors/OneClassSVM.php b/src/AnomalyDetectors/OneClassSVM.php index faf1111be..10969ab6f 100644 --- a/src/AnomalyDetectors/OneClassSVM.php +++ b/src/AnomalyDetectors/OneClassSVM.php @@ -44,7 +44,7 @@ class OneClassSVM implements Estimator, Learner * * @var svm */ - protected \svm $svm; + protected svm $svm; /** * The hyper-parameters of the model. @@ -58,7 +58,7 @@ class OneClassSVM implements Estimator, Learner * * @var \svmmodel|null */ - protected ?\svmmodel $model = null; + protected ?svmmodel $model = null; /** * @param float $nu diff --git a/src/BootstrapAggregator.php b/src/BootstrapAggregator.php index 742dcc6ab..fc30cc882 100644 --- a/src/BootstrapAggregator.php +++ b/src/BootstrapAggregator.php @@ -64,7 +64,7 @@ class BootstrapAggregator implements Estimator, Learner, Parallel, Persistable * * @var Learner */ - protected \Rubix\ML\Learner $base; + protected Learner $base; /** * The number of base learners to train in the ensemble. diff --git a/src/Classifiers/AdaBoost.php b/src/Classifiers/AdaBoost.php index 4f428c5f4..9c74fbd7a 100644 --- a/src/Classifiers/AdaBoost.php +++ b/src/Classifiers/AdaBoost.php @@ -72,7 +72,7 @@ class AdaBoost implements Estimator, Learner, Probabilistic, Verbose, Persistabl * * @var Learner */ - protected \Rubix\ML\Learner $base; + protected Learner $base; /** * The learning rate of the ensemble i.e. the *shrinkage* applied to each step. diff --git a/src/Classifiers/KDNeighbors.php b/src/Classifiers/KDNeighbors.php index 642b64e16..4ef9f5864 100644 --- a/src/Classifiers/KDNeighbors.php +++ b/src/Classifiers/KDNeighbors.php @@ -60,7 +60,7 @@ class KDNeighbors implements Estimator, Learner, Probabilistic, Persistable * * @var Spatial */ - protected \Rubix\ML\Graph\Trees\Spatial $tree; + protected Spatial $tree; /** * The zero vector for the possible class outcomes. diff --git a/src/Classifiers/KNearestNeighbors.php b/src/Classifiers/KNearestNeighbors.php index d5293c932..ee5c4b39b 100644 --- a/src/Classifiers/KNearestNeighbors.php +++ b/src/Classifiers/KNearestNeighbors.php @@ -62,7 +62,7 @@ class KNearestNeighbors implements Estimator, Learner, Online, Probabilistic, Pe * * @var Distance */ - protected \Rubix\ML\Kernels\Distance\Distance $kernel; + protected Distance $kernel; /** * The zero vector for the possible class outcomes. diff --git a/src/Classifiers/LogisticRegression.php b/src/Classifiers/LogisticRegression.php index ba67b5f57..b48ea7239 100644 --- a/src/Classifiers/LogisticRegression.php +++ b/src/Classifiers/LogisticRegression.php @@ -67,7 +67,7 @@ class LogisticRegression implements Estimator, Learner, Online, Probabilistic, R * * @var Optimizer */ - protected \Rubix\ML\NeuralNet\Optimizers\Optimizer $optimizer; + protected Optimizer $optimizer; /** * The amount of L2 regularization applied to the weights of the output layer. @@ -103,14 +103,14 @@ class LogisticRegression implements Estimator, Learner, Online, Probabilistic, R * * @var ClassificationLoss */ - protected \Rubix\ML\NeuralNet\CostFunctions\ClassificationLoss $costFn; + protected ClassificationLoss $costFn; /** * The underlying neural network instance. * * @var \Rubix\ML\NeuralNet\FeedForward|null */ - protected ?\Rubix\ML\NeuralNet\FeedForward $network = null; + protected ?FeedForward $network = null; /** * The unique class labels. diff --git a/src/Classifiers/LogitBoost.php b/src/Classifiers/LogitBoost.php index 071dfed39..f12588cfb 100644 --- a/src/Classifiers/LogitBoost.php +++ b/src/Classifiers/LogitBoost.php @@ -89,7 +89,7 @@ class LogitBoost implements Estimator, Learner, Probabilistic, RanksFeatures, Ve * * @var Learner */ - protected \Rubix\ML\Learner $booster; + protected Learner $booster; /** * The learning rate of the ensemble i.e. the *shrinkage* applied to each step. @@ -138,7 +138,7 @@ class LogitBoost implements Estimator, Learner, Probabilistic, RanksFeatures, Ve * * @var Metric */ - protected \Rubix\ML\CrossValidation\Metrics\Metric $metric; + protected Metric $metric; /** * The ensemble of boosters. diff --git a/src/Classifiers/MultilayerPerceptron.php b/src/Classifiers/MultilayerPerceptron.php index 1018d10c4..233c8b1eb 100644 --- a/src/Classifiers/MultilayerPerceptron.php +++ b/src/Classifiers/MultilayerPerceptron.php @@ -85,7 +85,7 @@ class MultilayerPerceptron implements Estimator, Learner, Online, Probabilistic, * * @var Optimizer */ - protected \Rubix\ML\NeuralNet\Optimizers\Optimizer $optimizer; + protected Optimizer $optimizer; /** * The amount of L2 regularization applied to the weights of the output layer. @@ -127,21 +127,21 @@ class MultilayerPerceptron implements Estimator, Learner, Online, Probabilistic, * * @var ClassificationLoss */ - protected \Rubix\ML\NeuralNet\CostFunctions\ClassificationLoss $costFn; + protected ClassificationLoss $costFn; /** * The validation metric used to score the generalization performance of the model during training. * * @var Metric */ - protected \Rubix\ML\CrossValidation\Metrics\Metric $metric; + protected Metric $metric; /** * The underlying neural network instance. * * @var \Rubix\ML\NeuralNet\FeedForward|null */ - protected ?\Rubix\ML\NeuralNet\FeedForward $network = null; + protected ?FeedForward $network = null; /** * The unique class labels. diff --git a/src/Classifiers/OneVsRest.php b/src/Classifiers/OneVsRest.php index 7c07e2627..841fb2751 100644 --- a/src/Classifiers/OneVsRest.php +++ b/src/Classifiers/OneVsRest.php @@ -51,7 +51,7 @@ class OneVsRest implements Estimator, Learner, Probabilistic, Parallel, Persista * * @var Learner */ - protected \Rubix\ML\Learner $base; + protected Learner $base; /** * A map of each class to its binary classifier. diff --git a/src/Classifiers/RadiusNeighbors.php b/src/Classifiers/RadiusNeighbors.php index b1e3544c9..1dc670186 100644 --- a/src/Classifiers/RadiusNeighbors.php +++ b/src/Classifiers/RadiusNeighbors.php @@ -60,7 +60,7 @@ class RadiusNeighbors implements Estimator, Learner, Probabilistic, Persistable * * @var Spatial */ - protected \Rubix\ML\Graph\Trees\Spatial $tree; + protected Spatial $tree; /** * The class label for any samples that have 0 neighbors within the specified radius. diff --git a/src/Classifiers/RandomForest.php b/src/Classifiers/RandomForest.php index 5d2b8d5cb..eb62f5e32 100644 --- a/src/Classifiers/RandomForest.php +++ b/src/Classifiers/RandomForest.php @@ -73,7 +73,7 @@ class RandomForest implements Estimator, Learner, Probabilistic, Parallel, Ranks * * @var Learner */ - protected \Rubix\ML\Learner $base; + protected Learner $base; /** * The number of learners to train in the ensemble. diff --git a/src/Classifiers/SoftmaxClassifier.php b/src/Classifiers/SoftmaxClassifier.php index 998035701..3038c04a3 100644 --- a/src/Classifiers/SoftmaxClassifier.php +++ b/src/Classifiers/SoftmaxClassifier.php @@ -64,7 +64,7 @@ class SoftmaxClassifier implements Estimator, Learner, Online, Probabilistic, Ve * * @var Optimizer */ - protected \Rubix\ML\NeuralNet\Optimizers\Optimizer $optimizer; + protected Optimizer $optimizer; /** * The amount of L2 regularization applied to the weights of the output layer. @@ -99,14 +99,14 @@ class SoftmaxClassifier implements Estimator, Learner, Online, Probabilistic, Ve * * @var ClassificationLoss */ - protected \Rubix\ML\NeuralNet\CostFunctions\ClassificationLoss $costFn; + protected ClassificationLoss $costFn; /** * The underlying neural network instance. * * @var \Rubix\ML\NeuralNet\FeedForward|null */ - protected ?\Rubix\ML\NeuralNet\FeedForward $network = null; + protected ?FeedForward $network = null; /** * The unique class labels. diff --git a/src/Clusterers/DBSCAN.php b/src/Clusterers/DBSCAN.php index 44112e02a..c24546d37 100644 --- a/src/Clusterers/DBSCAN.php +++ b/src/Clusterers/DBSCAN.php @@ -73,7 +73,7 @@ class DBSCAN implements Estimator * * @var Spatial */ - protected \Rubix\ML\Graph\Trees\Spatial $tree; + protected Spatial $tree; /** * @param float $radius diff --git a/src/Clusterers/FuzzyCMeans.php b/src/Clusterers/FuzzyCMeans.php index 49d5716ff..dd3b27b84 100644 --- a/src/Clusterers/FuzzyCMeans.php +++ b/src/Clusterers/FuzzyCMeans.php @@ -92,14 +92,14 @@ class FuzzyCMeans implements Estimator, Learner, Probabilistic, Verbose, Persist * * @var Distance */ - protected \Rubix\ML\Kernels\Distance\Distance $kernel; + protected Distance $kernel; /** * The cluster centroid seeder. * * @var Seeder */ - protected \Rubix\ML\Clusterers\Seeders\Seeder $seeder; + protected Seeder $seeder; /** * The computed centroid vectors of the training data. diff --git a/src/Clusterers/GaussianMixture.php b/src/Clusterers/GaussianMixture.php index 4445d0422..68338cfd9 100644 --- a/src/Clusterers/GaussianMixture.php +++ b/src/Clusterers/GaussianMixture.php @@ -97,7 +97,7 @@ class GaussianMixture implements Estimator, Learner, Probabilistic, Verbose, Per * * @var Seeder */ - protected \Rubix\ML\Clusterers\Seeders\Seeder $seeder; + protected Seeder $seeder; /** * The precomputed log prior probabilities of each cluster. diff --git a/src/Clusterers/KMeans.php b/src/Clusterers/KMeans.php index 852b178c4..280e70922 100644 --- a/src/Clusterers/KMeans.php +++ b/src/Clusterers/KMeans.php @@ -96,14 +96,14 @@ class KMeans implements Estimator, Learner, Online, Probabilistic, Verbose, Pers * * @var Distance */ - protected \Rubix\ML\Kernels\Distance\Distance $kernel; + protected Distance $kernel; /** * The cluster centroid seeder. * * @var Seeder */ - protected \Rubix\ML\Clusterers\Seeders\Seeder $seeder; + protected Seeder $seeder; /** * The computed centroid vectors of the training data. diff --git a/src/Clusterers/MeanShift.php b/src/Clusterers/MeanShift.php index 0d89ce00f..97af51353 100644 --- a/src/Clusterers/MeanShift.php +++ b/src/Clusterers/MeanShift.php @@ -104,14 +104,14 @@ class MeanShift implements Estimator, Learner, Probabilistic, Verbose, Persistab * * @var Spatial */ - protected \Rubix\ML\Graph\Trees\Spatial $tree; + protected Spatial $tree; /** * The cluster centroid seeder. * * @var Seeder */ - protected \Rubix\ML\Clusterers\Seeders\Seeder $seeder; + protected Seeder $seeder; /** * The computed centroid vectors of the training data. diff --git a/src/Clusterers/Seeders/KMC2.php b/src/Clusterers/Seeders/KMC2.php index d4e155e5e..717a29426 100644 --- a/src/Clusterers/Seeders/KMC2.php +++ b/src/Clusterers/Seeders/KMC2.php @@ -39,7 +39,7 @@ class KMC2 implements Seeder * * @var Distance */ - protected \Rubix\ML\Kernels\Distance\Distance $kernel; + protected Distance $kernel; /** * @param int $m diff --git a/src/Clusterers/Seeders/PlusPlus.php b/src/Clusterers/Seeders/PlusPlus.php index ad4f82e24..4a59d98b4 100644 --- a/src/Clusterers/Seeders/PlusPlus.php +++ b/src/Clusterers/Seeders/PlusPlus.php @@ -32,7 +32,7 @@ class PlusPlus implements Seeder * * @var Distance */ - protected \Rubix\ML\Kernels\Distance\Distance $kernel; + protected Distance $kernel; /** * @param \Rubix\ML\Kernels\Distance\Distance|null $kernel diff --git a/src/Datasets/Generators/Blob.php b/src/Datasets/Generators/Blob.php index 994d6fa52..62f703ae6 100644 --- a/src/Datasets/Generators/Blob.php +++ b/src/Datasets/Generators/Blob.php @@ -32,7 +32,7 @@ class Blob implements Generator * * @var Vector */ - protected \Tensor\Vector $center; + protected Vector $center; /** * The standard deviation of the blob. diff --git a/src/Datasets/Generators/Circle.php b/src/Datasets/Generators/Circle.php index d0a5ee14c..aed785d65 100644 --- a/src/Datasets/Generators/Circle.php +++ b/src/Datasets/Generators/Circle.php @@ -27,7 +27,7 @@ class Circle implements Generator * * @var Vector */ - protected \Tensor\Vector $center; + protected Vector $center; /** * The scaling factor of the circle. diff --git a/src/Datasets/Generators/HalfMoon.php b/src/Datasets/Generators/HalfMoon.php index 26486240e..e41a4a265 100644 --- a/src/Datasets/Generators/HalfMoon.php +++ b/src/Datasets/Generators/HalfMoon.php @@ -26,7 +26,7 @@ class HalfMoon implements Generator * * @var Vector */ - protected \Tensor\Vector $center; + protected Vector $center; /** * The scaling factor of the half moon. diff --git a/src/Datasets/Generators/Hyperplane.php b/src/Datasets/Generators/Hyperplane.php index 8afa59934..a5ae532bc 100644 --- a/src/Datasets/Generators/Hyperplane.php +++ b/src/Datasets/Generators/Hyperplane.php @@ -27,7 +27,7 @@ class Hyperplane implements Generator * * @var Vector */ - protected \Tensor\Vector $coefficients; + protected Vector $coefficients; /** * The y intercept term. diff --git a/src/Datasets/Generators/SwissRoll.php b/src/Datasets/Generators/SwissRoll.php index 8cd017ffa..f0899a284 100644 --- a/src/Datasets/Generators/SwissRoll.php +++ b/src/Datasets/Generators/SwissRoll.php @@ -33,7 +33,7 @@ class SwissRoll implements Generator * * @var Vector */ - protected \Tensor\Vector $center; + protected Vector $center; /** * The scaling factor of the swiss roll. diff --git a/src/EstimatorWrapper.php b/src/EstimatorWrapper.php new file mode 100644 index 000000000..aafb3ac8e --- /dev/null +++ b/src/EstimatorWrapper.php @@ -0,0 +1,20 @@ +assertInstanceOf(NeighborHood::class, $node); + $this->assertInstanceOf(Neighborhood::class, $node); $this->assertInstanceOf(Labeled::class, $node->dataset()); $this->assertEquals(self::BOX, iterator_to_array($node->sides())); } diff --git a/tests/NeuralNet/ParameterTest.php b/tests/NeuralNet/ParameterTest.php index 80fc03603..ec54adc51 100644 --- a/tests/NeuralNet/ParameterTest.php +++ b/tests/NeuralNet/ParameterTest.php @@ -16,7 +16,7 @@ class ParameterTest extends TestCase /** * @var Parameter */ - protected \Rubix\ML\NeuralNet\Parameter $param; + protected Parameter $param; /** * @var \Rubix\ML\NeuralNet\Optimizers\Optimizer diff --git a/tests/Transformers/ImageRotatorTest.php b/tests/Transformers/ImageRotatorTest.php index 5a88c0082..31297da76 100644 --- a/tests/Transformers/ImageRotatorTest.php +++ b/tests/Transformers/ImageRotatorTest.php @@ -17,7 +17,7 @@ class RandomizedImageRotatorTest extends TestCase /** * @var ImageRotator */ - protected \Rubix\ML\Transformers\ImageRotator $transformer; + protected ImageRotator $transformer; /** * @before From a354df50ab0d9c31295640dabd9a017767c6d0cb Mon Sep 17 00:00:00 2001 From: Mateusz Charytoniuk Date: Fri, 26 Jan 2024 03:29:09 +0100 Subject: [PATCH 2/2] Swoole Backend (#312) * add Swoole backend * phpstan: ignore swoole * feat: swoole process scheduler * fix(swoole): redo tasks when hash collision happens * chore(swoole): make sure coroutines are at the root of the scheduler * chore(swoole): set affinity / bind worker to a specific CPU core * chore(swoole): use igbinary if available * fix: remove comment * fix(swoole): worker cpu affinity * fix(swoole): cpu num * feat: scheduler improvements * style * chore(swoole): remove unnecessary atomics * chore(swoole): php backwards compatibility * fix: phpstan, socket message size * fix: uncomment test * style: composer fix --- .github/workflows/ci.yml | 2 +- benchmarks/Classifiers/OneVsRestBench.php | 10 +- benchmarks/Classifiers/RandomForestBench.php | 16 +- composer.json | 3 +- phpstan.neon | 1 + phpunit.xml | 15 +- src/Backends/Swoole.php | 173 ++++++++++++++++++ src/Classifiers/LogisticRegression.php | 16 ++ .../SwooleExtensionIsLoaded.php | 31 ++++ tests/Backends/SwooleTest.php | 89 +++++++++ tests/BootstrapAggregatorTest.php | 11 +- tests/Classifiers/OneVsRestTest.php | 13 +- tests/Classifiers/RandomForestTest.php | 13 +- tests/CommitteeMachineTest.php | 11 +- tests/CrossValidation/KFoldTest.php | 13 +- tests/CrossValidation/LeavePOutTest.php | 13 +- tests/CrossValidation/MonteCarloTest.php | 13 +- tests/DataProvider/BackendProviderTrait.php | 43 +++++ .../{SQTableTest.php => SQLTableTest.php} | 0 tests/GridSearchTest.php | 11 +- tests/Transformers/ImageRotatorTest.php | 2 +- 21 files changed, 463 insertions(+), 36 deletions(-) create mode 100644 src/Backends/Swoole.php create mode 100644 src/Specifications/SwooleExtensionIsLoaded.php create mode 100644 tests/Backends/SwooleTest.php create mode 100644 tests/DataProvider/BackendProviderTrait.php rename tests/Extractors/{SQTableTest.php => SQLTableTest.php} (100%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b992d288c..84e25068b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,7 @@ jobs: with: php-version: ${{ matrix.php-versions }} tools: composer, pecl - extensions: svm, mbstring, gd, fileinfo + extensions: svm, mbstring, gd, fileinfo, swoole ini-values: memory_limit=-1 - name: Validate composer.json diff --git a/benchmarks/Classifiers/OneVsRestBench.php b/benchmarks/Classifiers/OneVsRestBench.php index 19e450c42..b56b1d504 100644 --- a/benchmarks/Classifiers/OneVsRestBench.php +++ b/benchmarks/Classifiers/OneVsRestBench.php @@ -2,11 +2,13 @@ namespace Rubix\ML\Benchmarks\Classifiers; +use Rubix\ML\Backends\Backend; use Rubix\ML\Classifiers\OneVsRest; use Rubix\ML\Datasets\Generators\Blob; use Rubix\ML\Classifiers\LogisticRegression; use Rubix\ML\NeuralNet\Optimizers\Stochastic; use Rubix\ML\Datasets\Generators\Agglomerate; +use Rubix\ML\Tests\DataProvider\BackendProviderTrait; /** * @Groups({"Classifiers"}) @@ -14,6 +16,8 @@ */ class OneVsRestBench { + use BackendProviderTrait; + protected const TRAINING_SIZE = 10000; protected const TESTING_SIZE = 10000; @@ -52,9 +56,13 @@ public function setUp() : void * @Subject * @Iterations(5) * @OutputTimeUnit("seconds", precision=3) + * @ParamProviders("provideBackends") + * @param array{ backend: Backend } $params */ - public function trainPredict() : void + public function trainPredict(array $params) : void { + $this->estimator->setBackend($params['backend']); + $this->estimator->train($this->training); $this->estimator->predict($this->testing); diff --git a/benchmarks/Classifiers/RandomForestBench.php b/benchmarks/Classifiers/RandomForestBench.php index 8fae90db7..674090014 100644 --- a/benchmarks/Classifiers/RandomForestBench.php +++ b/benchmarks/Classifiers/RandomForestBench.php @@ -2,10 +2,12 @@ namespace Rubix\ML\Benchmarks\Classifiers; +use Rubix\ML\Backends\Backend; use Rubix\ML\Classifiers\RandomForest; use Rubix\ML\Datasets\Generators\Blob; use Rubix\ML\Classifiers\ClassificationTree; use Rubix\ML\Datasets\Generators\Agglomerate; +use Rubix\ML\Tests\DataProvider\BackendProviderTrait; use Rubix\ML\Transformers\IntervalDiscretizer; /** @@ -13,6 +15,8 @@ */ class RandomForestBench { + use BackendProviderTrait; + protected const TRAINING_SIZE = 10000; protected const TESTING_SIZE = 10000; @@ -70,9 +74,13 @@ public function setUpCategorical() : void * @Iterations(5) * @BeforeMethods({"setUpContinuous"}) * @OutputTimeUnit("seconds", precision=3) + * @ParamProviders("provideBackends") + * @param array{ backend: Backend } $params */ - public function continuous() : void + public function continuous(array $params) : void { + $this->estimator->setBackend($params['backend']); + $this->estimator->train($this->training); $this->estimator->predict($this->testing); @@ -83,9 +91,13 @@ public function continuous() : void * @Iterations(5) * @BeforeMethods({"setUpCategorical"}) * @OutputTimeUnit("seconds", precision=3) + * @ParamProviders("provideBackends") + * @param array{ backend: Backend } $params */ - public function categorical() : void + public function categorical(array $params) : void { + $this->estimator->setBackend($params['backend']); + $this->estimator->train($this->training); $this->estimator->predict($this->testing); diff --git a/composer.json b/composer.json index 8cb313510..0f34eedb4 100644 --- a/composer.json +++ b/composer.json @@ -49,7 +49,8 @@ "phpstan/extension-installer": "^1.0", "phpstan/phpstan": "^1.0", "phpstan/phpstan-phpunit": "^1.0", - "phpunit/phpunit": "^9.0" + "phpunit/phpunit": "^9.0", + "swoole/ide-helper": "^5.1" }, "suggest": { "ext-tensor": "For fast Matrix/Vector computing", diff --git a/phpstan.neon b/phpstan.neon index 90d5425c3..4d1ae5782 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -6,3 +6,4 @@ parameters: - 'benchmarks' excludePaths: - src/Backends/Amp.php + - src/Backends/Swoole.php diff --git a/phpunit.xml b/phpunit.xml index 33e100832..f2656a836 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,5 +1,18 @@ - + src diff --git a/src/Backends/Swoole.php b/src/Backends/Swoole.php new file mode 100644 index 000000000..34eb894b8 --- /dev/null +++ b/src/Backends/Swoole.php @@ -0,0 +1,173 @@ +check(); + + $this->cpus = swoole_cpu_num(); + $this->hasIgbinary = ExtensionIsLoaded::with('igbinary')->passes(); + } + + /** + * Queue up a deferred task for backend processing. + * + * @internal + * + * @param Task $task + * @param callable(mixed,mixed):void $after + * @param mixed $context + */ + public function enqueue(Task $task, ?callable $after = null, $context = null) : void + { + $this->queue[] = function () use ($task, $after, $context) { + $result = $task(); + + if ($after) { + $after($result, $context); + } + + return $result; + }; + } + + /** + * Process the queue and return the results. + * + * @internal + * + * @return mixed[] + */ + public function process() : array + { + $results = []; + + $maxMessageLength = new Atomic(0); + $workerProcesses = []; + + $currentCpu = 0; + + foreach ($this->queue as $index => $queueItem) { + $workerProcess = new Process( + function (Process $worker) use ($maxMessageLength, $queueItem) { + $serialized = $this->serialize($queueItem()); + + $serializedLength = strlen($serialized); + $currentMaxSerializedLength = $maxMessageLength->get(); + + if ($serializedLength > $currentMaxSerializedLength) { + $maxMessageLength->set($serializedLength); + } + + $worker->exportSocket()->send($serialized); + }, + // redirect_stdin_and_stdout + false, + // pipe_type + SOCK_DGRAM, + // enable_coroutine + true, + ); + + $workerProcess->setAffinity([$currentCpu]); + $workerProcess->setBlocking(false); + $workerProcess->start(); + + $workerProcesses[$index] = $workerProcess; + + $currentCpu = ($currentCpu + 1) % $this->cpus; + } + + run(function () use ($maxMessageLength, &$results, $workerProcesses) { + foreach ($workerProcesses as $index => $workerProcess) { + $status = $workerProcess->wait(); + + if (0 !== $status['code']) { + throw new RuntimeException('Worker process exited with an error'); + } + + $socket = $workerProcess->exportSocket(); + + if ($socket->isClosed()) { + throw new RuntimeException('Coroutine socket is closed'); + } + + $maxMessageLengthValue = $maxMessageLength->get(); + + $receivedData = $socket->recv($maxMessageLengthValue); + $unserialized = $this->unserialize($receivedData); + + $results[] = $unserialized; + } + }); + + return $results; + } + + /** + * Flush the queue + */ + public function flush() : void + { + $this->queue = []; + } + + private function serialize(mixed $data) : string + { + if ($this->hasIgbinary) { + return igbinary_serialize($data); + } + + return serialize($data); + } + + private function unserialize(string $serialized) : mixed + { + if ($this->hasIgbinary) { + return igbinary_unserialize($serialized); + } + + return unserialize($serialized); + } + + /** + * Return the string representation of the object. + * + * @internal + * + * @return string + */ + public function __toString() : string + { + return 'Swoole'; + } +} diff --git a/src/Classifiers/LogisticRegression.php b/src/Classifiers/LogisticRegression.php index b48ea7239..81eef3ac3 100644 --- a/src/Classifiers/LogisticRegression.php +++ b/src/Classifiers/LogisticRegression.php @@ -491,4 +491,20 @@ public function __toString() : string { return 'Logistic Regression (' . Params::stringify($this->params()) . ')'; } + + /** + * Without this method, causes errors with Swoole backend + Igbinary + * serialization. + * + * Can be removed if it's no longer the case. + * + * @internal + * @param array $data + */ + public function __unserialize(array $data) : void + { + foreach ($data as $propertyName => $propertyValue) { + $this->{$propertyName} = $propertyValue; + } + } } diff --git a/src/Specifications/SwooleExtensionIsLoaded.php b/src/Specifications/SwooleExtensionIsLoaded.php new file mode 100644 index 000000000..d15342649 --- /dev/null +++ b/src/Specifications/SwooleExtensionIsLoaded.php @@ -0,0 +1,31 @@ +passes() + || ExtensionIsLoaded::with('openswoole')->passes() + ) { + return; + } + + throw new MissingExtension('swoole'); + } +} diff --git a/tests/Backends/SwooleTest.php b/tests/Backends/SwooleTest.php new file mode 100644 index 000000000..ab6ac6cb1 --- /dev/null +++ b/tests/Backends/SwooleTest.php @@ -0,0 +1,89 @@ +passes()) { + $this->markTestSkipped( + 'Swoole/OpenSwoole extension is not available.' + ); + } + + $this->backend = new SwooleBackend(); + } + + /** + * @after + */ + protected function tearDown() : void + { + Event::wait(); + } + + /** + * @test + */ + public function build() : void + { + $this->assertInstanceOf(SwooleBackend::class, $this->backend); + $this->assertInstanceOf(Backend::class, $this->backend); + } + + /** + * @test + */ + public function enqueueProcess() : void + { + for ($i = 0; $i < 10; ++$i) { + $this->backend->enqueue(new Task([self::class, 'foo'], [$i])); + } + + $results = $this->backend->process(); + + $this->assertCount(10, $results); + $this->assertEquals([ + 0, + 2, + 4, + 6, + 8, + 10, + 12, + 14, + 16, + 18, + ], $results); + } +} diff --git a/tests/BootstrapAggregatorTest.php b/tests/BootstrapAggregatorTest.php index c8062dc5f..7d14108a8 100644 --- a/tests/BootstrapAggregatorTest.php +++ b/tests/BootstrapAggregatorTest.php @@ -7,7 +7,6 @@ use Rubix\ML\Estimator; use Rubix\ML\Persistable; use Rubix\ML\EstimatorType; -use Rubix\ML\Backends\Serial; use Rubix\ML\Datasets\Unlabeled; use Rubix\ML\BootstrapAggregator; use Rubix\ML\Regressors\RegressionTree; @@ -15,6 +14,8 @@ use Rubix\ML\CrossValidation\Metrics\RSquared; use Rubix\ML\Exceptions\RuntimeException; use PHPUnit\Framework\TestCase; +use Rubix\ML\Backends\Backend; +use Rubix\ML\Tests\DataProvider\BackendProviderTrait; /** * @group MetaEstimators @@ -22,6 +23,8 @@ */ class BootstrapAggregatorTest extends TestCase { + use BackendProviderTrait; + protected const TRAIN_SIZE = 512; protected const TEST_SIZE = 256; @@ -111,11 +114,13 @@ public function params() : void } /** + * @dataProvider provideBackends * @test + * @param Backend $backend */ - public function trainPredict() : void + public function trainPredict(Backend $backend) : void { - $this->estimator->setBackend(new Serial()); + $this->estimator->setBackend($backend); $training = $this->generator->generate(self::TRAIN_SIZE); $testing = $this->generator->generate(self::TEST_SIZE); diff --git a/tests/Classifiers/OneVsRestTest.php b/tests/Classifiers/OneVsRestTest.php index 26bcc2d24..efceb881a 100644 --- a/tests/Classifiers/OneVsRestTest.php +++ b/tests/Classifiers/OneVsRestTest.php @@ -9,7 +9,6 @@ use Rubix\ML\Persistable; use Rubix\ML\Probabilistic; use Rubix\ML\EstimatorType; -use Rubix\ML\Backends\Serial; use Rubix\ML\Datasets\Unlabeled; use Rubix\ML\Classifiers\OneVsRest; use Rubix\ML\Classifiers\GaussianNB; @@ -18,6 +17,8 @@ use Rubix\ML\Datasets\Generators\Agglomerate; use Rubix\ML\Exceptions\RuntimeException; use PHPUnit\Framework\TestCase; +use Rubix\ML\Backends\Backend; +use Rubix\ML\Tests\DataProvider\BackendProviderTrait; /** * @group Classifiers @@ -25,6 +26,8 @@ */ class OneVsRestTest extends TestCase { + use BackendProviderTrait; + /** * The number of samples in the training set. * @@ -81,8 +84,6 @@ protected function setUp() : void $this->estimator = new OneVsRest(new GaussianNB()); - $this->estimator->setBackend(new Serial()); - $this->metric = new FBeta(); srand(self::RANDOM_SEED); @@ -139,10 +140,14 @@ public function params() : void } /** + * @dataProvider provideBackends * @test + * @param Backend $backend */ - public function trainPredictProba() : void + public function trainPredictProba(Backend $backend) : void { + $this->estimator->setBackend($backend); + $training = $this->generator->generate(self::TRAIN_SIZE); $testing = $this->generator->generate(self::TEST_SIZE); diff --git a/tests/Classifiers/RandomForestTest.php b/tests/Classifiers/RandomForestTest.php index 9572a9cf2..fc5a309d9 100644 --- a/tests/Classifiers/RandomForestTest.php +++ b/tests/Classifiers/RandomForestTest.php @@ -9,7 +9,6 @@ use Rubix\ML\Probabilistic; use Rubix\ML\RanksFeatures; use Rubix\ML\EstimatorType; -use Rubix\ML\Backends\Serial; use Rubix\ML\Datasets\Unlabeled; use Rubix\ML\Classifiers\RandomForest; use Rubix\ML\Datasets\Generators\Blob; @@ -19,6 +18,8 @@ use Rubix\ML\Exceptions\InvalidArgumentException; use Rubix\ML\Exceptions\RuntimeException; use PHPUnit\Framework\TestCase; +use Rubix\ML\Backends\Backend; +use Rubix\ML\Tests\DataProvider\BackendProviderTrait; /** * @group Classifiers @@ -26,6 +27,8 @@ */ class RandomForestTest extends TestCase { + use BackendProviderTrait; + /** * The number of samples in the training set. * @@ -82,8 +85,6 @@ protected function setUp() : void $this->estimator = new RandomForest(new ClassificationTree(3), 50, 0.2, true); - $this->estimator->setBackend(new Serial()); - $this->metric = new FBeta(); srand(self::RANDOM_SEED); @@ -154,10 +155,14 @@ public function params() : void } /** + * @dataProvider provideBackends * @test + * @param Backend $backend */ - public function trainPredictImportances() : void + public function trainPredictImportances(Backend $backend) : void { + $this->estimator->setBackend($backend); + $training = $this->generator->generate(self::TRAIN_SIZE); $testing = $this->generator->generate(self::TEST_SIZE); diff --git a/tests/CommitteeMachineTest.php b/tests/CommitteeMachineTest.php index f9af35bfb..471505dd8 100644 --- a/tests/CommitteeMachineTest.php +++ b/tests/CommitteeMachineTest.php @@ -8,7 +8,6 @@ use Rubix\ML\Estimator; use Rubix\ML\Persistable; use Rubix\ML\EstimatorType; -use Rubix\ML\Backends\Serial; use Rubix\ML\CommitteeMachine; use Rubix\ML\Datasets\Unlabeled; use Rubix\ML\Classifiers\GaussianNB; @@ -20,6 +19,8 @@ use Rubix\ML\Exceptions\InvalidArgumentException; use Rubix\ML\Exceptions\RuntimeException; use PHPUnit\Framework\TestCase; +use Rubix\ML\Backends\Backend; +use Rubix\ML\Tests\DataProvider\BackendProviderTrait; /** * @group MetaEstimators @@ -27,6 +28,8 @@ */ class CommitteeMachineTest extends TestCase { + use BackendProviderTrait; + protected const TRAIN_SIZE = 512; protected const TEST_SIZE = 256; @@ -131,11 +134,13 @@ public function params() : void } /** + * @dataProvider provideBackends * @test + * @param Backend $backend */ - public function trainPredict() : void + public function trainPredict(Backend $backend) : void { - $this->estimator->setBackend(new Serial()); + $this->estimator->setBackend($backend); $training = $this->generator->generate(self::TRAIN_SIZE); $testing = $this->generator->generate(self::TEST_SIZE); diff --git a/tests/CrossValidation/KFoldTest.php b/tests/CrossValidation/KFoldTest.php index 805236cbe..8a5fc54a8 100644 --- a/tests/CrossValidation/KFoldTest.php +++ b/tests/CrossValidation/KFoldTest.php @@ -3,7 +3,6 @@ namespace Rubix\ML\Tests\CrossValidation; use Rubix\ML\Parallel; -use Rubix\ML\Backends\Serial; use Rubix\ML\CrossValidation\KFold; use Rubix\ML\Datasets\Generators\Blob; use Rubix\ML\CrossValidation\Validator; @@ -11,6 +10,8 @@ use Rubix\ML\Datasets\Generators\Agglomerate; use Rubix\ML\CrossValidation\Metrics\Accuracy; use PHPUnit\Framework\TestCase; +use Rubix\ML\Backends\Backend; +use Rubix\ML\Tests\DataProvider\BackendProviderTrait; /** * @group Validators @@ -18,6 +19,8 @@ */ class KFoldTest extends TestCase { + use BackendProviderTrait; + protected const DATASET_SIZE = 50; /** @@ -54,8 +57,6 @@ protected function setUp() : void $this->validator = new KFold(10); - $this->validator->setBackend(new Serial()); - $this->metric = new Accuracy(); } @@ -70,10 +71,14 @@ public function build() : void } /** + * @dataProvider provideBackends * @test + * @param Backend $backend */ - public function test() : void + public function test(Backend $backend) : void { + $this->validator->setBackend($backend); + [$min, $max] = $this->metric->range()->list(); $dataset = $this->generator->generate(self::DATASET_SIZE); diff --git a/tests/CrossValidation/LeavePOutTest.php b/tests/CrossValidation/LeavePOutTest.php index dbb54bf57..aa2a8c995 100644 --- a/tests/CrossValidation/LeavePOutTest.php +++ b/tests/CrossValidation/LeavePOutTest.php @@ -3,7 +3,6 @@ namespace Rubix\ML\Tests\CrossValidation; use Rubix\ML\Parallel; -use Rubix\ML\Backends\Serial; use Rubix\ML\Datasets\Generators\Blob; use Rubix\ML\CrossValidation\LeavePOut; use Rubix\ML\CrossValidation\Validator; @@ -11,6 +10,8 @@ use Rubix\ML\Datasets\Generators\Agglomerate; use Rubix\ML\CrossValidation\Metrics\Accuracy; use PHPUnit\Framework\TestCase; +use Rubix\ML\Backends\Backend; +use Rubix\ML\Tests\DataProvider\BackendProviderTrait; /** * @group Validators @@ -18,6 +19,8 @@ */ class LeavePOutTest extends TestCase { + use BackendProviderTrait; + protected const DATASET_SIZE = 50; /** @@ -54,8 +57,6 @@ protected function setUp() : void $this->validator = new LeavePOut(10); - $this->validator->setBackend(new Serial()); - $this->metric = new Accuracy(); } @@ -70,10 +71,14 @@ public function build() : void } /** + * @dataProvider provideBackends * @test + * @param Backend $backend */ - public function test() : void + public function test(Backend $backend) : void { + $this->validator->setBackend($backend); + [$min, $max] = $this->metric->range()->list(); $dataset = $this->generator->generate(self::DATASET_SIZE); diff --git a/tests/CrossValidation/MonteCarloTest.php b/tests/CrossValidation/MonteCarloTest.php index ca8967bfb..c0c044644 100644 --- a/tests/CrossValidation/MonteCarloTest.php +++ b/tests/CrossValidation/MonteCarloTest.php @@ -3,7 +3,6 @@ namespace Rubix\ML\Tests\CrossValidation; use Rubix\ML\Parallel; -use Rubix\ML\Backends\Serial; use Rubix\ML\Datasets\Generators\Blob; use Rubix\ML\CrossValidation\Validator; use Rubix\ML\CrossValidation\MonteCarlo; @@ -11,6 +10,8 @@ use Rubix\ML\Datasets\Generators\Agglomerate; use Rubix\ML\CrossValidation\Metrics\Accuracy; use PHPUnit\Framework\TestCase; +use Rubix\ML\Backends\Backend; +use Rubix\ML\Tests\DataProvider\BackendProviderTrait; /** * @group Validators @@ -18,6 +19,8 @@ */ class MonteCarloTest extends TestCase { + use BackendProviderTrait; + protected const DATASET_SIZE = 50; /** @@ -54,8 +57,6 @@ protected function setUp() : void $this->validator = new MonteCarlo(3, 0.2); - $this->validator->setBackend(new Serial()); - $this->metric = new Accuracy(); } @@ -70,10 +71,14 @@ public function build() : void } /** + * @dataProvider provideBackends * @test + * @param Backend $backend */ - public function test() : void + public function test(Backend $backend) : void { + $this->validator->setBackend($backend); + [$min, $max] = $this->metric->range()->list(); $dataset = $this->generator->generate(self::DATASET_SIZE); diff --git a/tests/DataProvider/BackendProviderTrait.php b/tests/DataProvider/BackendProviderTrait.php new file mode 100644 index 000000000..08851742f --- /dev/null +++ b/tests/DataProvider/BackendProviderTrait.php @@ -0,0 +1,43 @@ +> + */ + public static function provideBackends() : Generator + { + $serialBackend = new Serial(); + + yield (string) $serialBackend => [ + 'backend' => $serialBackend, + ]; + + // $ampBackend = new Amp(); + + // yield (string) $ampBackend => [ + // 'backend' => $ampBackend, + // ]; + + if ( + SwooleExtensionIsLoaded::create()->passes() + && ExtensionIsLoaded::with('igbinary')->passes() + ) { + $swooleProcessBackend = new Swoole(); + + yield (string) $swooleProcessBackend => [ + 'backend' => $swooleProcessBackend, + ]; + } + } +} diff --git a/tests/Extractors/SQTableTest.php b/tests/Extractors/SQLTableTest.php similarity index 100% rename from tests/Extractors/SQTableTest.php rename to tests/Extractors/SQLTableTest.php diff --git a/tests/GridSearchTest.php b/tests/GridSearchTest.php index 0a61ddfdc..9c69f479d 100644 --- a/tests/GridSearchTest.php +++ b/tests/GridSearchTest.php @@ -9,7 +9,6 @@ use Rubix\ML\GridSearch; use Rubix\ML\Persistable; use Rubix\ML\EstimatorType; -use Rubix\ML\Backends\Serial; use Rubix\ML\Loggers\BlackHole; use Rubix\ML\CrossValidation\HoldOut; use Rubix\ML\Kernels\Distance\Euclidean; @@ -20,6 +19,8 @@ use Rubix\ML\Datasets\Generators\Agglomerate; use Rubix\ML\CrossValidation\Metrics\Accuracy; use PHPUnit\Framework\TestCase; +use Rubix\ML\Backends\Backend; +use Rubix\ML\Tests\DataProvider\BackendProviderTrait; /** * @group MetaEstimators @@ -27,6 +28,8 @@ */ class GridSearchTest extends TestCase { + use BackendProviderTrait; + protected const TRAIN_SIZE = 512; protected const TEST_SIZE = 256; @@ -121,12 +124,14 @@ public function params() : void } /** + * @dataProvider provideBackends * @test + * @param Backend $backend */ - public function trainPredictBest() : void + public function trainPredictBest(Backend $backend) : void { $this->estimator->setLogger(new BlackHole()); - $this->estimator->setBackend(new Serial()); + $this->estimator->setBackend($backend); $training = $this->generator->generate(self::TRAIN_SIZE); $testing = $this->generator->generate(self::TEST_SIZE); diff --git a/tests/Transformers/ImageRotatorTest.php b/tests/Transformers/ImageRotatorTest.php index 31297da76..10ccd5bef 100644 --- a/tests/Transformers/ImageRotatorTest.php +++ b/tests/Transformers/ImageRotatorTest.php @@ -12,7 +12,7 @@ * @requires extension gd * @covers \Rubix\ML\Transformers\ImageRotator */ -class RandomizedImageRotatorTest extends TestCase +class ImageRotatorTest extends TestCase { /** * @var ImageRotator