MDL-65769 lib: update PHP-ML to 0.8.0
This commit is contained in:
@@ -4,6 +4,8 @@ declare(strict_types=1);
|
||||
|
||||
namespace Phpml\Helper;
|
||||
|
||||
use Phpml\Classification\Classifier;
|
||||
|
||||
trait OneVsRest
|
||||
{
|
||||
/**
|
||||
@@ -25,39 +27,37 @@ trait OneVsRest
|
||||
|
||||
/**
|
||||
* Train a binary classifier in the OvR style
|
||||
*
|
||||
* @param array $samples
|
||||
* @param array $targets
|
||||
*/
|
||||
public function train(array $samples, array $targets)
|
||||
public function train(array $samples, array $targets): void
|
||||
{
|
||||
// Clears previous stuff.
|
||||
$this->reset();
|
||||
|
||||
$this->trainBylabel($samples, $targets);
|
||||
$this->trainByLabel($samples, $targets);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $samples
|
||||
* @param array $targets
|
||||
* @param array $allLabels All training set labels
|
||||
*
|
||||
* @return void
|
||||
* Resets the classifier and the vars internally used by OneVsRest to create multiple classifiers.
|
||||
*/
|
||||
protected function trainByLabel(array $samples, array $targets, array $allLabels = [])
|
||||
public function reset(): void
|
||||
{
|
||||
$this->classifiers = [];
|
||||
$this->allLabels = [];
|
||||
$this->costValues = [];
|
||||
|
||||
$this->resetBinary();
|
||||
}
|
||||
|
||||
protected function trainByLabel(array $samples, array $targets, array $allLabels = []): void
|
||||
{
|
||||
// Overwrites the current value if it exist. $allLabels must be provided for each partialTrain run.
|
||||
if (!empty($allLabels)) {
|
||||
$this->allLabels = $allLabels;
|
||||
} else {
|
||||
$this->allLabels = array_keys(array_count_values($targets));
|
||||
}
|
||||
$this->allLabels = count($allLabels) === 0 ? array_keys(array_count_values($targets)) : $allLabels;
|
||||
sort($this->allLabels, SORT_STRING);
|
||||
|
||||
// If there are only two targets, then there is no need to perform OvR
|
||||
if (count($this->allLabels) == 2) {
|
||||
if (count($this->allLabels) === 2) {
|
||||
// Init classifier if required.
|
||||
if (empty($this->classifiers)) {
|
||||
if (count($this->classifiers) === 0) {
|
||||
$this->classifiers[0] = $this->getClassifierCopy();
|
||||
}
|
||||
|
||||
@@ -67,11 +67,11 @@ trait OneVsRest
|
||||
|
||||
foreach ($this->allLabels as $label) {
|
||||
// Init classifier if required.
|
||||
if (empty($this->classifiers[$label])) {
|
||||
if (!isset($this->classifiers[$label])) {
|
||||
$this->classifiers[$label] = $this->getClassifierCopy();
|
||||
}
|
||||
|
||||
list($binarizedTargets, $classifierLabels) = $this->binarizeTargets($targets, $label);
|
||||
[$binarizedTargets, $classifierLabels] = $this->binarizeTargets($targets, $label);
|
||||
$this->classifiers[$label]->trainBinary($samples, $binarizedTargets, $classifierLabels);
|
||||
}
|
||||
}
|
||||
@@ -85,64 +85,26 @@ trait OneVsRest
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the classifier and the vars internally used by OneVsRest to create multiple classifiers.
|
||||
*/
|
||||
public function reset()
|
||||
{
|
||||
$this->classifiers = [];
|
||||
$this->allLabels = [];
|
||||
$this->costValues = [];
|
||||
|
||||
$this->resetBinary();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an instance of the current class after cleaning up OneVsRest stuff.
|
||||
*
|
||||
* @return \Phpml\Estimator
|
||||
*/
|
||||
protected function getClassifierCopy()
|
||||
protected function getClassifierCopy(): Classifier
|
||||
{
|
||||
// Clone the current classifier, so that
|
||||
// we don't mess up its variables while training
|
||||
// multiple instances of this classifier
|
||||
$classifier = clone $this;
|
||||
$classifier->reset();
|
||||
|
||||
return $classifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* Groups all targets into two groups: Targets equal to
|
||||
* the given label and the others
|
||||
*
|
||||
* $targets is not passed by reference nor contains objects so this method
|
||||
* changes will not affect the caller $targets array.
|
||||
*
|
||||
* @param array $targets
|
||||
* @param mixed $label
|
||||
* @return array Binarized targets and target's labels
|
||||
*/
|
||||
private function binarizeTargets($targets, $label)
|
||||
{
|
||||
$notLabel = "not_$label";
|
||||
foreach ($targets as $key => $target) {
|
||||
$targets[$key] = $target == $label ? $label : $notLabel;
|
||||
}
|
||||
|
||||
$labels = [$label, $notLabel];
|
||||
return [$targets, $labels];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param array $sample
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
protected function predictSample(array $sample)
|
||||
{
|
||||
if (count($this->allLabels) == 2) {
|
||||
if (count($this->allLabels) === 2) {
|
||||
return $this->classifiers[0]->predictSampleBinary($sample);
|
||||
}
|
||||
|
||||
@@ -153,32 +115,24 @@ trait OneVsRest
|
||||
}
|
||||
|
||||
arsort($probs, SORT_NUMERIC);
|
||||
|
||||
return key($probs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Each classifier should implement this method instead of train(samples, targets)
|
||||
*
|
||||
* @param array $samples
|
||||
* @param array $targets
|
||||
* @param array $labels
|
||||
*/
|
||||
abstract protected function trainBinary(array $samples, array $targets, array $labels);
|
||||
|
||||
/**
|
||||
* To be overwritten by OneVsRest classifiers.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
abstract protected function resetBinary();
|
||||
abstract protected function resetBinary(): void;
|
||||
|
||||
/**
|
||||
* Each classifier that make use of OvR approach should be able to
|
||||
* return a probability for a sample to belong to the given label.
|
||||
*
|
||||
* @param array $sample
|
||||
* @param string $label
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
abstract protected function predictProbability(array $sample, string $label);
|
||||
@@ -186,9 +140,30 @@ trait OneVsRest
|
||||
/**
|
||||
* Each classifier should implement this method instead of predictSample()
|
||||
*
|
||||
* @param array $sample
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
abstract protected function predictSampleBinary(array $sample);
|
||||
|
||||
/**
|
||||
* Groups all targets into two groups: Targets equal to
|
||||
* the given label and the others
|
||||
*
|
||||
* $targets is not passed by reference nor contains objects so this method
|
||||
* changes will not affect the caller $targets array.
|
||||
*
|
||||
* @param mixed $label
|
||||
*
|
||||
* @return array Binarized targets and target's labels
|
||||
*/
|
||||
private function binarizeTargets(array $targets, $label): array
|
||||
{
|
||||
$notLabel = "not_${label}";
|
||||
foreach ($targets as $key => $target) {
|
||||
$targets[$key] = $target == $label ? $label : $notLabel;
|
||||
}
|
||||
|
||||
$labels = [$label, $notLabel];
|
||||
|
||||
return [$targets, $labels];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ declare(strict_types=1);
|
||||
|
||||
namespace Phpml\Helper\Optimizer;
|
||||
|
||||
use Closure;
|
||||
|
||||
/**
|
||||
* Conjugate Gradient method to solve a non-linear f(x) with respect to unknown x
|
||||
* See https://en.wikipedia.org/wiki/Nonlinear_conjugate_gradient_method)
|
||||
@@ -17,14 +19,7 @@ namespace Phpml\Helper\Optimizer;
|
||||
*/
|
||||
class ConjugateGradient extends GD
|
||||
{
|
||||
/**
|
||||
* @param array $samples
|
||||
* @param array $targets
|
||||
* @param \Closure $gradientCb
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function runOptimization(array $samples, array $targets, \Closure $gradientCb)
|
||||
public function runOptimization(array $samples, array $targets, Closure $gradientCb): array
|
||||
{
|
||||
$this->samples = $samples;
|
||||
$this->targets = $targets;
|
||||
@@ -32,11 +27,11 @@ class ConjugateGradient extends GD
|
||||
$this->sampleCount = count($samples);
|
||||
$this->costValues = [];
|
||||
|
||||
$d = mp::muls($this->gradient($this->theta), -1);
|
||||
$d = MP::muls($this->gradient($this->theta), -1);
|
||||
|
||||
for ($i = 0; $i < $this->maxIterations; ++$i) {
|
||||
// Obtain α that minimizes f(θ + α.d)
|
||||
$alpha = $this->getAlpha(array_sum($d));
|
||||
$alpha = $this->getAlpha($d);
|
||||
|
||||
// θ(k+1) = θ(k) + α.d
|
||||
$thetaNew = $this->getNewTheta($alpha, $d);
|
||||
@@ -65,30 +60,38 @@ class ConjugateGradient extends GD
|
||||
/**
|
||||
* Executes the callback function for the problem and returns
|
||||
* sum of the gradient for all samples & targets.
|
||||
*
|
||||
* @param array $theta
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function gradient(array $theta)
|
||||
protected function gradient(array $theta): array
|
||||
{
|
||||
list(, $gradient) = parent::gradient($theta);
|
||||
[, $updates, $penalty] = parent::gradient($theta);
|
||||
|
||||
// Calculate gradient for each dimension
|
||||
$gradient = [];
|
||||
for ($i = 0; $i <= $this->dimensions; ++$i) {
|
||||
if ($i === 0) {
|
||||
$gradient[$i] = array_sum($updates);
|
||||
} else {
|
||||
$col = array_column($this->samples, $i - 1);
|
||||
$error = 0;
|
||||
foreach ($col as $index => $val) {
|
||||
$error += $val * $updates[$index];
|
||||
}
|
||||
|
||||
$gradient[$i] = $error + $penalty * $theta[$i];
|
||||
}
|
||||
}
|
||||
|
||||
return $gradient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value of f(x) for given solution
|
||||
*
|
||||
* @param array $theta
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
protected function cost(array $theta)
|
||||
protected function cost(array $theta): float
|
||||
{
|
||||
list($cost) = parent::gradient($theta);
|
||||
[$cost] = parent::gradient($theta);
|
||||
|
||||
return array_sum($cost) / $this->sampleCount;
|
||||
return array_sum($cost) / (int) $this->sampleCount;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -104,19 +107,15 @@ class ConjugateGradient extends GD
|
||||
* b) Probe a larger alpha (0.01) and calculate cost function
|
||||
* b-1) If cost function decreases, continue enlarging alpha
|
||||
* b-2) If cost function increases, take the midpoint and try again
|
||||
*
|
||||
* @param float $d
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
protected function getAlpha(float $d)
|
||||
protected function getAlpha(array $d): float
|
||||
{
|
||||
$small = 0.0001 * $d;
|
||||
$large = 0.01 * $d;
|
||||
$small = MP::muls($d, 0.0001);
|
||||
$large = MP::muls($d, 0.01);
|
||||
|
||||
// Obtain θ + α.d for two initial values, x0 and x1
|
||||
$x0 = mp::adds($this->theta, $small);
|
||||
$x1 = mp::adds($this->theta, $large);
|
||||
$x0 = MP::add($this->theta, $small);
|
||||
$x1 = MP::add($this->theta, $large);
|
||||
|
||||
$epsilon = 0.0001;
|
||||
$iteration = 0;
|
||||
@@ -132,20 +131,28 @@ class ConjugateGradient extends GD
|
||||
|
||||
if ($fx1 < $fx0) {
|
||||
$x0 = $x1;
|
||||
$x1 = mp::adds($x1, 0.01); // Enlarge second
|
||||
$x1 = MP::adds($x1, 0.01); // Enlarge second
|
||||
} else {
|
||||
$x1 = mp::divs(mp::add($x1, $x0), 2.0);
|
||||
$x1 = MP::divs(MP::add($x1, $x0), 2.0);
|
||||
} // Get to the midpoint
|
||||
|
||||
$error = $fx1 / $this->dimensions;
|
||||
} while ($error <= $epsilon || $iteration++ < 10);
|
||||
|
||||
// Return α = θ / d
|
||||
if ($d == 0) {
|
||||
return $x1[0] - $this->theta[0];
|
||||
// Return α = θ / d
|
||||
// For accuracy, choose a dimension which maximize |d[i]|
|
||||
$imax = 0;
|
||||
for ($i = 1; $i <= $this->dimensions; ++$i) {
|
||||
if (abs($d[$i]) > abs($d[$imax])) {
|
||||
$imax = $i;
|
||||
}
|
||||
}
|
||||
|
||||
return ($x1[0] - $this->theta[0]) / $d;
|
||||
if ($d[$imax] == 0) {
|
||||
return $x1[$imax] - $this->theta[$imax];
|
||||
}
|
||||
|
||||
return ($x1[$imax] - $this->theta[$imax]) / $d[$imax];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -153,30 +160,10 @@ class ConjugateGradient extends GD
|
||||
* gradient direction.
|
||||
*
|
||||
* θ(k+1) = θ(k) + α.d
|
||||
*
|
||||
* @param float $alpha
|
||||
* @param array $d
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getNewTheta(float $alpha, array $d)
|
||||
protected function getNewTheta(float $alpha, array $d): array
|
||||
{
|
||||
$theta = $this->theta;
|
||||
|
||||
for ($i = 0; $i < $this->dimensions + 1; ++$i) {
|
||||
if ($i === 0) {
|
||||
$theta[$i] += $alpha * array_sum($d);
|
||||
} else {
|
||||
$sum = 0.0;
|
||||
foreach ($this->samples as $si => $sample) {
|
||||
$sum += $sample[$i - 1] * $d[$si] * $alpha;
|
||||
}
|
||||
|
||||
$theta[$i] += $sum;
|
||||
}
|
||||
}
|
||||
|
||||
return $theta;
|
||||
return MP::add($this->theta, MP::muls($d, $alpha));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -187,35 +174,31 @@ class ConjugateGradient extends GD
|
||||
*
|
||||
* See:
|
||||
* R. Fletcher and C. M. Reeves, "Function minimization by conjugate gradients", Comput. J. 7 (1964), 149–154.
|
||||
*
|
||||
* @param array $newTheta
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
protected function getBeta(array $newTheta)
|
||||
protected function getBeta(array $newTheta): float
|
||||
{
|
||||
$dNew = array_sum($this->gradient($newTheta));
|
||||
$dOld = array_sum($this->gradient($this->theta)) + 1e-100;
|
||||
$gNew = $this->gradient($newTheta);
|
||||
$gOld = $this->gradient($this->theta);
|
||||
$dNew = 0;
|
||||
$dOld = 1e-100;
|
||||
for ($i = 0; $i <= $this->dimensions; ++$i) {
|
||||
$dNew += $gNew[$i] ** 2;
|
||||
$dOld += $gOld[$i] ** 2;
|
||||
}
|
||||
|
||||
return $dNew ** 2 / $dOld ** 2;
|
||||
return $dNew / $dOld;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the new conjugate direction
|
||||
*
|
||||
* d(k+1) =–∇f(x(k+1)) + β(k).d(k)
|
||||
*
|
||||
* @param array $theta
|
||||
* @param float $beta
|
||||
* @param array $d
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getNewDirection(array $theta, float $beta, array $d)
|
||||
protected function getNewDirection(array $theta, float $beta, array $d): array
|
||||
{
|
||||
$grad = $this->gradient($theta);
|
||||
|
||||
return mp::add(mp::muls($grad, -1), mp::muls($d, $beta));
|
||||
return MP::add(MP::muls($grad, -1), MP::muls($d, $beta));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -223,17 +206,12 @@ class ConjugateGradient extends GD
|
||||
* Handles element-wise vector operations between vector-vector
|
||||
* and vector-scalar variables
|
||||
*/
|
||||
class mp
|
||||
class MP
|
||||
{
|
||||
/**
|
||||
* Element-wise <b>multiplication</b> of two vectors of the same size
|
||||
*
|
||||
* @param array $m1
|
||||
* @param array $m2
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function mul(array $m1, array $m2)
|
||||
public static function mul(array $m1, array $m2): array
|
||||
{
|
||||
$res = [];
|
||||
foreach ($m1 as $i => $val) {
|
||||
@@ -245,13 +223,8 @@ class mp
|
||||
|
||||
/**
|
||||
* Element-wise <b>division</b> of two vectors of the same size
|
||||
*
|
||||
* @param array $m1
|
||||
* @param array $m2
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function div(array $m1, array $m2)
|
||||
public static function div(array $m1, array $m2): array
|
||||
{
|
||||
$res = [];
|
||||
foreach ($m1 as $i => $val) {
|
||||
@@ -263,14 +236,8 @@ class mp
|
||||
|
||||
/**
|
||||
* Element-wise <b>addition</b> of two vectors of the same size
|
||||
*
|
||||
* @param array $m1
|
||||
* @param array $m2
|
||||
* @param int $mag
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function add(array $m1, array $m2, int $mag = 1)
|
||||
public static function add(array $m1, array $m2, int $mag = 1): array
|
||||
{
|
||||
$res = [];
|
||||
foreach ($m1 as $i => $val) {
|
||||
@@ -282,26 +249,16 @@ class mp
|
||||
|
||||
/**
|
||||
* Element-wise <b>subtraction</b> of two vectors of the same size
|
||||
*
|
||||
* @param array $m1
|
||||
* @param array $m2
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function sub(array $m1, array $m2)
|
||||
public static function sub(array $m1, array $m2): array
|
||||
{
|
||||
return self::add($m1, $m2, -1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Element-wise <b>multiplication</b> of a vector with a scalar
|
||||
*
|
||||
* @param array $m1
|
||||
* @param float $m2
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function muls(array $m1, float $m2)
|
||||
public static function muls(array $m1, float $m2): array
|
||||
{
|
||||
$res = [];
|
||||
foreach ($m1 as $val) {
|
||||
@@ -313,13 +270,8 @@ class mp
|
||||
|
||||
/**
|
||||
* Element-wise <b>division</b> of a vector with a scalar
|
||||
*
|
||||
* @param array $m1
|
||||
* @param float $m2
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function divs(array $m1, float $m2)
|
||||
public static function divs(array $m1, float $m2): array
|
||||
{
|
||||
$res = [];
|
||||
foreach ($m1 as $val) {
|
||||
@@ -331,14 +283,8 @@ class mp
|
||||
|
||||
/**
|
||||
* Element-wise <b>addition</b> of a vector with a scalar
|
||||
*
|
||||
* @param array $m1
|
||||
* @param float $m2
|
||||
* @param int $mag
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function adds(array $m1, float $m2, int $mag = 1)
|
||||
public static function adds(array $m1, float $m2, int $mag = 1): array
|
||||
{
|
||||
$res = [];
|
||||
foreach ($m1 as $val) {
|
||||
@@ -350,13 +296,8 @@ class mp
|
||||
|
||||
/**
|
||||
* Element-wise <b>subtraction</b> of a vector with a scalar
|
||||
*
|
||||
* @param array $m1
|
||||
* @param array $m2
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function subs(array $m1, array $m2)
|
||||
public static function subs(array $m1, float $m2): array
|
||||
{
|
||||
return self::adds($m1, $m2, -1);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,9 @@ declare(strict_types=1);
|
||||
|
||||
namespace Phpml\Helper\Optimizer;
|
||||
|
||||
use Closure;
|
||||
use Phpml\Exception\InvalidOperationException;
|
||||
|
||||
/**
|
||||
* Batch version of Gradient Descent to optimize the weights
|
||||
* of a classifier given samples, targets and the objective function to minimize
|
||||
@@ -13,18 +16,11 @@ class GD extends StochasticGD
|
||||
/**
|
||||
* Number of samples given
|
||||
*
|
||||
* @var int
|
||||
* @var int|null
|
||||
*/
|
||||
protected $sampleCount = null;
|
||||
protected $sampleCount;
|
||||
|
||||
/**
|
||||
* @param array $samples
|
||||
* @param array $targets
|
||||
* @param \Closure $gradientCb
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function runOptimization(array $samples, array $targets, \Closure $gradientCb)
|
||||
public function runOptimization(array $samples, array $targets, Closure $gradientCb): array
|
||||
{
|
||||
$this->samples = $samples;
|
||||
$this->targets = $targets;
|
||||
@@ -38,11 +34,11 @@ class GD extends StochasticGD
|
||||
$theta = $this->theta;
|
||||
|
||||
// Calculate update terms for each sample
|
||||
list($errors, $updates, $totalPenalty) = $this->gradient($theta);
|
||||
[$errors, $updates, $totalPenalty] = $this->gradient($theta);
|
||||
|
||||
$this->updateWeightsWithUpdates($updates, $totalPenalty);
|
||||
|
||||
$this->costValues[] = array_sum($errors)/$this->sampleCount;
|
||||
$this->costValues[] = array_sum($errors) / $this->sampleCount;
|
||||
|
||||
if ($this->earlyStop($theta)) {
|
||||
break;
|
||||
@@ -57,22 +53,22 @@ class GD extends StochasticGD
|
||||
/**
|
||||
* Calculates gradient, cost function and penalty term for each sample
|
||||
* then returns them as an array of values
|
||||
*
|
||||
* @param array $theta
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function gradient(array $theta)
|
||||
protected function gradient(array $theta): array
|
||||
{
|
||||
$costs = [];
|
||||
$gradient= [];
|
||||
$gradient = [];
|
||||
$totalPenalty = 0;
|
||||
|
||||
if ($this->gradientCb === null) {
|
||||
throw new InvalidOperationException('Gradient callback is not defined');
|
||||
}
|
||||
|
||||
foreach ($this->samples as $index => $sample) {
|
||||
$target = $this->targets[$index];
|
||||
|
||||
$result = ($this->gradientCb)($theta, $sample, $target);
|
||||
list($cost, $grad, $penalty) = array_pad($result, 3, 0);
|
||||
[$cost, $grad, $penalty] = array_pad($result, 3, 0);
|
||||
|
||||
$costs[] = $cost;
|
||||
$gradient[] = $grad;
|
||||
@@ -84,11 +80,7 @@ class GD extends StochasticGD
|
||||
return [$costs, $gradient, $totalPenalty];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $updates
|
||||
* @param float $penalty
|
||||
*/
|
||||
protected function updateWeightsWithUpdates(array $updates, float $penalty)
|
||||
protected function updateWeightsWithUpdates(array $updates, float $penalty): void
|
||||
{
|
||||
// Updates all weights at once
|
||||
for ($i = 0; $i <= $this->dimensions; ++$i) {
|
||||
@@ -110,10 +102,8 @@ class GD extends StochasticGD
|
||||
|
||||
/**
|
||||
* Clears the optimizer internal vars after the optimization process.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function clear()
|
||||
protected function clear(): void
|
||||
{
|
||||
$this->sampleCount = null;
|
||||
parent::clear();
|
||||
|
||||
@@ -4,6 +4,9 @@ declare(strict_types=1);
|
||||
|
||||
namespace Phpml\Helper\Optimizer;
|
||||
|
||||
use Closure;
|
||||
use Phpml\Exception\InvalidArgumentException;
|
||||
|
||||
abstract class Optimizer
|
||||
{
|
||||
/**
|
||||
@@ -11,7 +14,7 @@ abstract class Optimizer
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $theta;
|
||||
protected $theta = [];
|
||||
|
||||
/**
|
||||
* Number of dimensions
|
||||
@@ -22,8 +25,6 @@ abstract class Optimizer
|
||||
|
||||
/**
|
||||
* Inits a new instance of Optimizer for the given number of dimensions
|
||||
*
|
||||
* @param int $dimensions
|
||||
*/
|
||||
public function __construct(int $dimensions)
|
||||
{
|
||||
@@ -32,23 +33,14 @@ abstract class Optimizer
|
||||
// Inits the weights randomly
|
||||
$this->theta = [];
|
||||
for ($i = 0; $i < $this->dimensions; ++$i) {
|
||||
$this->theta[] = rand() / (float) getrandmax();
|
||||
$this->theta[] = (random_int(0, PHP_INT_MAX) / PHP_INT_MAX) + 0.1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the weights manually
|
||||
*
|
||||
* @param array $theta
|
||||
*
|
||||
* @return $this
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function setInitialTheta(array $theta)
|
||||
public function setTheta(array $theta): self
|
||||
{
|
||||
if (count($theta) != $this->dimensions) {
|
||||
throw new \Exception("Number of values in the weights array should be $this->dimensions");
|
||||
if (count($theta) !== $this->dimensions) {
|
||||
throw new InvalidArgumentException(sprintf('Number of values in the weights array should be %s', $this->dimensions));
|
||||
}
|
||||
|
||||
$this->theta = $theta;
|
||||
@@ -59,10 +51,6 @@ abstract class Optimizer
|
||||
/**
|
||||
* Executes the optimization with the given samples & targets
|
||||
* and returns the weights
|
||||
*
|
||||
* @param array $samples
|
||||
* @param array $targets
|
||||
* @param \Closure $gradientCb
|
||||
*/
|
||||
abstract protected function runOptimization(array $samples, array $targets, \Closure $gradientCb);
|
||||
abstract public function runOptimization(array $samples, array $targets, Closure $gradientCb): array;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,10 @@ declare(strict_types=1);
|
||||
|
||||
namespace Phpml\Helper\Optimizer;
|
||||
|
||||
use Closure;
|
||||
use Phpml\Exception\InvalidArgumentException;
|
||||
use Phpml\Exception\InvalidOperationException;
|
||||
|
||||
/**
|
||||
* Stochastic Gradient Descent optimization method
|
||||
* to find a solution for the equation A.ϴ = y where
|
||||
@@ -29,9 +33,9 @@ class StochasticGD extends Optimizer
|
||||
* Callback function to get the gradient and cost value
|
||||
* for a specific set of theta (ϴ) and a pair of sample & target
|
||||
*
|
||||
* @var \Closure
|
||||
* @var \Closure|null
|
||||
*/
|
||||
protected $gradientCb = null;
|
||||
protected $gradientCb;
|
||||
|
||||
/**
|
||||
* Maximum number of iterations used to train the model
|
||||
@@ -66,18 +70,17 @@ class StochasticGD extends Optimizer
|
||||
* @var bool
|
||||
*/
|
||||
protected $enableEarlyStop = true;
|
||||
|
||||
/**
|
||||
* List of values obtained by evaluating the cost function at each iteration
|
||||
* of the algorithm
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $costValues= [];
|
||||
protected $costValues = [];
|
||||
|
||||
/**
|
||||
* Initializes the SGD optimizer for the given number of dimensions
|
||||
*
|
||||
* @param int $dimensions
|
||||
*/
|
||||
public function __construct(int $dimensions)
|
||||
{
|
||||
@@ -87,6 +90,17 @@ class StochasticGD extends Optimizer
|
||||
$this->dimensions = $dimensions;
|
||||
}
|
||||
|
||||
public function setTheta(array $theta): Optimizer
|
||||
{
|
||||
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.<br>
|
||||
@@ -94,8 +108,6 @@ class StochasticGD extends Optimizer
|
||||
* If change in the theta is less than given value then the
|
||||
* algorithm will stop training
|
||||
*
|
||||
* @param float $threshold
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setChangeThreshold(float $threshold = 1e-5)
|
||||
@@ -109,8 +121,6 @@ class StochasticGD extends Optimizer
|
||||
* Enable/Disable early stopping by checking at each iteration
|
||||
* whether changes in theta or cost value are not large enough
|
||||
*
|
||||
* @param bool $enable
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setEarlyStop(bool $enable = true)
|
||||
@@ -121,8 +131,6 @@ class StochasticGD extends Optimizer
|
||||
}
|
||||
|
||||
/**
|
||||
* @param float $learningRate
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setLearningRate(float $learningRate)
|
||||
@@ -133,8 +141,6 @@ class StochasticGD extends Optimizer
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $maxIterations
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setMaxIterations(int $maxIterations)
|
||||
@@ -150,14 +156,8 @@ class StochasticGD extends Optimizer
|
||||
*
|
||||
* The cost function to minimize and the gradient of the function are to be
|
||||
* handled by the callback function provided as the third parameter of the method.
|
||||
*
|
||||
* @param array $samples
|
||||
* @param array $targets
|
||||
* @param \Closure $gradientCb
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function runOptimization(array $samples, array $targets, \Closure $gradientCb)
|
||||
public function runOptimization(array $samples, array $targets, Closure $gradientCb): array
|
||||
{
|
||||
$this->samples = $samples;
|
||||
$this->targets = $targets;
|
||||
@@ -176,7 +176,7 @@ class StochasticGD extends Optimizer
|
||||
|
||||
// Save the best theta in the "pocket" so that
|
||||
// any future set of theta worse than this will be disregarded
|
||||
if ($bestTheta == null || $cost <= $bestScore) {
|
||||
if ($bestTheta === null || $cost <= $bestScore) {
|
||||
$bestTheta = $theta;
|
||||
$bestScore = $cost;
|
||||
}
|
||||
@@ -194,23 +194,33 @@ class StochasticGD extends Optimizer
|
||||
|
||||
// Solution in the pocket is better than or equal to the last state
|
||||
// so, we use this solution
|
||||
return $this->theta = $bestTheta;
|
||||
return $this->theta = (array) $bestTheta;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return float
|
||||
* Returns the list of cost values for each iteration executed in
|
||||
* last run of the optimization
|
||||
*/
|
||||
protected function updateTheta()
|
||||
public function getCostValues(): array
|
||||
{
|
||||
return $this->costValues;
|
||||
}
|
||||
|
||||
protected function updateTheta(): float
|
||||
{
|
||||
$jValue = 0.0;
|
||||
$theta = $this->theta;
|
||||
|
||||
if ($this->gradientCb === null) {
|
||||
throw new InvalidOperationException('Gradient callback is not defined');
|
||||
}
|
||||
|
||||
foreach ($this->samples as $index => $sample) {
|
||||
$target = $this->targets[$index];
|
||||
|
||||
$result = ($this->gradientCb)($theta, $sample, $target);
|
||||
|
||||
list($error, $gradient, $penalty) = array_pad($result, 3, 0);
|
||||
[$error, $gradient, $penalty] = array_pad($result, 3, 0);
|
||||
|
||||
// Update bias
|
||||
$this->theta[0] -= $this->learningRate * $gradient;
|
||||
@@ -231,19 +241,17 @@ class StochasticGD extends Optimizer
|
||||
/**
|
||||
* Checks if the optimization is not effective enough and can be stopped
|
||||
* in case large enough changes in the solution do not happen
|
||||
*
|
||||
* @param array $oldTheta
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
protected function earlyStop($oldTheta)
|
||||
protected function earlyStop(array $oldTheta): bool
|
||||
{
|
||||
// Check for early stop: No change larger than threshold (default 1e-5)
|
||||
$diff = array_map(
|
||||
function ($w1, $w2) {
|
||||
return abs($w1 - $w2) > $this->threshold ? 1 : 0;
|
||||
},
|
||||
$oldTheta, $this->theta);
|
||||
$oldTheta,
|
||||
$this->theta
|
||||
);
|
||||
|
||||
if (array_sum($diff) == 0) {
|
||||
return true;
|
||||
@@ -251,30 +259,17 @@ class StochasticGD extends Optimizer
|
||||
|
||||
// Check if the last two cost values are almost the same
|
||||
$costs = array_slice($this->costValues, -2);
|
||||
if (count($costs) == 2 && abs($costs[1] - $costs[0]) < $this->threshold) {
|
||||
if (count($costs) === 2 && abs($costs[1] - $costs[0]) < $this->threshold) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of cost values for each iteration executed in
|
||||
* last run of the optimization
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getCostValues()
|
||||
{
|
||||
return $this->costValues;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the optimizer internal vars after the optimization process.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function clear()
|
||||
protected function clear(): void
|
||||
{
|
||||
$this->samples = [];
|
||||
$this->targets = [];
|
||||
|
||||
@@ -7,8 +7,6 @@ namespace Phpml\Helper;
|
||||
trait Predictable
|
||||
{
|
||||
/**
|
||||
* @param array $samples
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function predict(array $samples)
|
||||
@@ -26,8 +24,6 @@ trait Predictable
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $sample
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
abstract protected function predictSample(array $sample);
|
||||
|
||||
@@ -16,11 +16,7 @@ trait Trainable
|
||||
*/
|
||||
private $targets = [];
|
||||
|
||||
/**
|
||||
* @param array $samples
|
||||
* @param array $targets
|
||||
*/
|
||||
public function train(array $samples, array $targets)
|
||||
public function train(array $samples, array $targets): void
|
||||
{
|
||||
$this->samples = array_merge($this->samples, $samples);
|
||||
$this->targets = array_merge($this->targets, $targets);
|
||||
|
||||
Reference in New Issue
Block a user