From a96f03e8ddf1f4caf1a99fd14a8f00785b9ce5df Mon Sep 17 00:00:00 2001 From: Arkadiusz Kondas Date: Fri, 23 Feb 2018 23:05:46 +0100 Subject: [PATCH] Fix Optimizer initial theta randomization (#239) * Fix Optimizer initial theta randomization * Add more tests for LUDecomposition and FuzzyCMeans --- src/Helper/Optimizer/Optimizer.php | 17 ++++----- src/Helper/Optimizer/StochasticGD.php | 12 ++++++ src/Math/LinearAlgebra/LUDecomposition.php | 15 +------- tests/Clustering/FuzzyCMeansTest.php | 16 ++++++++ .../Optimizer/ConjugateGradientTest.php | 37 +++++++++++++++++++ .../LinearAlgebra/LUDecompositionTest.php | 31 ++++++++++++++++ 6 files changed, 105 insertions(+), 23 deletions(-) create mode 100644 tests/Math/LinearAlgebra/LUDecompositionTest.php diff --git a/src/Helper/Optimizer/Optimizer.php b/src/Helper/Optimizer/Optimizer.php index 7ef317c..9bac3be 100644 --- a/src/Helper/Optimizer/Optimizer.php +++ b/src/Helper/Optimizer/Optimizer.php @@ -5,10 +5,12 @@ declare(strict_types=1); namespace Phpml\Helper\Optimizer; use Closure; -use Exception; +use Phpml\Exception\InvalidArgumentException; abstract class Optimizer { + public $initialTheta; + /** * Unknown variables to be found * @@ -33,21 +35,16 @@ abstract class Optimizer // Inits the weights randomly $this->theta = []; for ($i = 0; $i < $this->dimensions; ++$i) { - $this->theta[] = random_int(0, getrandmax()) / (float) getrandmax(); + $this->theta[] = (random_int(0, PHP_INT_MAX) / PHP_INT_MAX) + 0.1; } + + $this->initialTheta = $this->theta; } - /** - * Sets the weights manually - * - * @return $this - * - * @throws \Exception - */ public function setInitialTheta(array $theta) { if (count($theta) != $this->dimensions) { - throw new Exception("Number of values in the weights array should be ${this}->dimensions"); + throw new InvalidArgumentException(sprintf('Number of values in the weights array should be %s', $this->dimensions)); } $this->theta = $theta; diff --git a/src/Helper/Optimizer/StochasticGD.php b/src/Helper/Optimizer/StochasticGD.php index 18a5f0c..e1cbeea 100644 --- a/src/Helper/Optimizer/StochasticGD.php +++ b/src/Helper/Optimizer/StochasticGD.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Phpml\Helper\Optimizer; use Closure; +use Phpml\Exception\InvalidArgumentException; /** * Stochastic Gradient Descent optimization method @@ -88,6 +89,17 @@ class StochasticGD extends Optimizer $this->dimensions = $dimensions; } + public function setInitialTheta(array $theta) + { + if (count($theta) != $this->dimensions + 1) { + throw new InvalidArgumentException(sprintf('Number of values in the weights array should be %s', $this->dimensions + 1)); + } + + $this->theta = $theta; + + return $this; + } + /** * Sets minimum value for the change in the theta values * between iterations to continue the iterations.
diff --git a/src/Math/LinearAlgebra/LUDecomposition.php b/src/Math/LinearAlgebra/LUDecomposition.php index 151e2cc..1594837 100644 --- a/src/Math/LinearAlgebra/LUDecomposition.php +++ b/src/Math/LinearAlgebra/LUDecomposition.php @@ -225,25 +225,14 @@ class LUDecomposition return true; } - /** - * Count determinants - * - * @return float|int d matrix determinant - * - * @throws MatrixException - */ - public function det() + public function det(): float { - if ($this->m !== $this->n) { - throw MatrixException::notSquareMatrix(); - } - $d = $this->pivsign; for ($j = 0; $j < $this->n; ++$j) { $d *= $this->LU[$j][$j]; } - return $d; + return (float) $d; } /** diff --git a/tests/Clustering/FuzzyCMeansTest.php b/tests/Clustering/FuzzyCMeansTest.php index 7245481..b2005a1 100644 --- a/tests/Clustering/FuzzyCMeansTest.php +++ b/tests/Clustering/FuzzyCMeansTest.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Phpml\Tests\Clustering; use Phpml\Clustering\FuzzyCMeans; +use Phpml\Exception\InvalidArgumentException; use PHPUnit\Framework\TestCase; class FuzzyCMeansTest extends TestCase @@ -45,4 +46,19 @@ class FuzzyCMeansTest extends TestCase $this->assertEquals(1, array_sum($col)); } } + + /** + * @dataProvider invalidClusterNumberProvider + */ + public function testInvalidClusterNumber(int $clusters): void + { + $this->expectException(InvalidArgumentException::class); + + new FuzzyCMeans($clusters); + } + + public function invalidClusterNumberProvider(): array + { + return [[0], [-1]]; + } } diff --git a/tests/Helper/Optimizer/ConjugateGradientTest.php b/tests/Helper/Optimizer/ConjugateGradientTest.php index b05f998..78eb718 100644 --- a/tests/Helper/Optimizer/ConjugateGradientTest.php +++ b/tests/Helper/Optimizer/ConjugateGradientTest.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Phpml\Tests\Helper\Optimizer; +use Phpml\Exception\InvalidArgumentException; use Phpml\Helper\Optimizer\ConjugateGradient; use PHPUnit\Framework\TestCase; @@ -35,6 +36,34 @@ class ConjugateGradientTest extends TestCase $this->assertEquals([-1, 2], $theta, '', 0.1); } + public function testRunOptimizationWithCustomInitialTheta(): void + { + // 200 samples from y = -1 + 2x (i.e. theta = [-1, 2]) + $samples = []; + $targets = []; + for ($i = -100; $i <= 100; ++$i) { + $x = $i / 100; + $samples[] = [$x]; + $targets[] = -1 + 2 * $x; + } + + $callback = function ($theta, $sample, $target) { + $y = $theta[0] + $theta[1] * $sample[0]; + $cost = ($y - $target) ** 2 / 2; + $grad = $y - $target; + + return [$cost, $grad]; + }; + + $optimizer = new ConjugateGradient(1); + // set very weak theta to trigger very bad result + $optimizer->setInitialTheta([0.0000001, 0.0000001]); + + $theta = $optimizer->runOptimization($samples, $targets, $callback); + + $this->assertEquals([-1.087708, 2.212034], $theta, '', 0.000001); + } + public function testRunOptimization2Dim(): void { // 100 samples from y = -1 + 2x0 - 3x1 (i.e. theta = [-1, 2, -3]) @@ -62,4 +91,12 @@ class ConjugateGradientTest extends TestCase $this->assertEquals([-1, 2, -3], $theta, '', 0.1); } + + public function testThrowExceptionOnInvalidTheta(): void + { + $opimizer = new ConjugateGradient(2); + + $this->expectException(InvalidArgumentException::class); + $opimizer->setInitialTheta([0.15]); + } } diff --git a/tests/Math/LinearAlgebra/LUDecompositionTest.php b/tests/Math/LinearAlgebra/LUDecompositionTest.php new file mode 100644 index 0000000..b678673 --- /dev/null +++ b/tests/Math/LinearAlgebra/LUDecompositionTest.php @@ -0,0 +1,31 @@ +expectException(MatrixException::class); + + new LUDecomposition(new Matrix([1, 2, 3, 4, 5])); + } + + public function testSolveWithInvalidMatrix(): void + { + $this->expectException(MatrixException::class); + + $lu = new LUDecomposition(new Matrix([[1, 2], [3, 4]])); + $lu->solve(new Matrix([1, 2, 3])); + } +}