MDL-58859 mlbackend_php: Upgrade php-ml to latest version

Part of MDL-57791 epic.
This commit is contained in:
David Monllao
2017-07-24 07:53:20 +02:00
parent 40fcb365c3
commit 589d7e8eb6
48 changed files with 1458 additions and 643 deletions
@@ -102,19 +102,21 @@ class DecisionTree implements Classifier
$this->columnNames = array_slice($this->columnNames, 0, $this->featureCount);
} elseif (count($this->columnNames) < $this->featureCount) {
$this->columnNames = array_merge($this->columnNames,
range(count($this->columnNames), $this->featureCount - 1));
range(count($this->columnNames), $this->featureCount - 1)
);
}
}
/**
* @param array $samples
*
* @return array
*/
public static function getColumnTypes(array $samples) : array
{
$types = [];
$featureCount = count($samples[0]);
for ($i=0; $i < $featureCount; $i++) {
for ($i = 0; $i < $featureCount; ++$i) {
$values = array_column($samples, $i);
$isCategorical = self::isCategoricalColumn($values);
$types[] = $isCategorical ? self::NOMINAL : self::CONTINUOUS;
@@ -125,7 +127,8 @@ class DecisionTree implements Classifier
/**
* @param array $records
* @param int $depth
* @param int $depth
*
* @return DecisionTreeLeaf
*/
protected function getSplitLeaf(array $records, int $depth = 0) : DecisionTreeLeaf
@@ -163,10 +166,10 @@ class DecisionTree implements Classifier
// Group remaining targets
$target = $this->targets[$recordNo];
if (! array_key_exists($target, $remainingTargets)) {
if (!array_key_exists($target, $remainingTargets)) {
$remainingTargets[$target] = 1;
} else {
$remainingTargets[$target]++;
++$remainingTargets[$target];
}
}
@@ -188,6 +191,7 @@ class DecisionTree implements Classifier
/**
* @param array $records
*
* @return DecisionTreeLeaf
*/
protected function getBestSplit(array $records) : DecisionTreeLeaf
@@ -251,7 +255,7 @@ class DecisionTree implements Classifier
protected function getSelectedFeatures() : array
{
$allFeatures = range(0, $this->featureCount - 1);
if ($this->numUsableFeatures === 0 && ! $this->selectedFeatures) {
if ($this->numUsableFeatures === 0 && !$this->selectedFeatures) {
return $allFeatures;
}
@@ -271,9 +275,10 @@ class DecisionTree implements Classifier
}
/**
* @param $baseValue
* @param mixed $baseValue
* @param array $colValues
* @param array $targets
*
* @return float
*/
public function getGiniIndex($baseValue, array $colValues, array $targets) : float
@@ -282,13 +287,15 @@ class DecisionTree implements Classifier
foreach ($this->labels as $label) {
$countMatrix[$label] = [0, 0];
}
foreach ($colValues as $index => $value) {
$label = $targets[$index];
$rowIndex = $value === $baseValue ? 0 : 1;
$countMatrix[$label][$rowIndex]++;
++$countMatrix[$label][$rowIndex];
}
$giniParts = [0, 0];
for ($i=0; $i<=1; $i++) {
for ($i = 0; $i <= 1; ++$i) {
$part = 0;
$sum = array_sum(array_column($countMatrix, $i));
if ($sum > 0) {
@@ -296,6 +303,7 @@ class DecisionTree implements Classifier
$part += pow($countMatrix[$label][$i] / floatval($sum), 2);
}
}
$giniParts[$i] = (1 - $part) * $sum;
}
@@ -304,6 +312,7 @@ class DecisionTree implements Classifier
/**
* @param array $samples
*
* @return array
*/
protected function preprocess(array $samples) : array
@@ -311,7 +320,7 @@ class DecisionTree implements Classifier
// Detect and convert continuous data column values into
// discrete values by using the median as a threshold value
$columns = [];
for ($i=0; $i<$this->featureCount; $i++) {
for ($i = 0; $i < $this->featureCount; ++$i) {
$values = array_column($samples, $i);
if ($this->columnTypes[$i] == self::CONTINUOUS) {
$median = Mean::median($values);
@@ -332,6 +341,7 @@ class DecisionTree implements Classifier
/**
* @param array $columnValues
*
* @return bool
*/
protected static function isCategoricalColumn(array $columnValues) : bool
@@ -348,6 +358,7 @@ class DecisionTree implements Classifier
if ($floatValues) {
return false;
}
if (count($numericValues) !== $count) {
return true;
}
@@ -365,7 +376,9 @@ class DecisionTree implements Classifier
* randomly selected for each split operation.
*
* @param int $numFeatures
*
* @return $this
*
* @throws InvalidArgumentException
*/
public function setNumFeatures(int $numFeatures)
@@ -394,7 +407,9 @@ class DecisionTree implements Classifier
* column importances are desired to be inspected.
*
* @param array $names
*
* @return $this
*
* @throws InvalidArgumentException
*/
public function setColumnNames(array $names)
@@ -458,8 +473,9 @@ class DecisionTree implements Classifier
* Collects and returns an array of internal nodes that use the given
* column as a split criterion
*
* @param int $column
* @param int $column
* @param DecisionTreeLeaf $node
*
* @return array
*/
protected function getSplitNodesByColumn(int $column, DecisionTreeLeaf $node) : array
@@ -478,9 +494,11 @@ class DecisionTree implements Classifier
if ($node->leftLeaf) {
$lNodes = $this->getSplitNodesByColumn($column, $node->leftLeaf);
}
if ($node->rightLeaf) {
$rNodes = $this->getSplitNodesByColumn($column, $node->rightLeaf);
}
$nodes = array_merge($nodes, $lNodes, $rNodes);
return $nodes;
@@ -488,6 +506,7 @@ class DecisionTree implements Classifier
/**
* @param array $sample
*
* @return mixed
*/
protected function predictSample(array $sample)
@@ -497,6 +516,7 @@ class DecisionTree implements Classifier
if ($node->isTerminal) {
break;
}
if ($node->evaluate($sample)) {
$node = $node->leftLeaf;
} else {
@@ -92,6 +92,8 @@ class DecisionTreeLeaf
* Returns Mean Decrease Impurity (MDI) in the node.
* For terminal nodes, this value is equal to 0
*
* @param int $parentRecordCount
*
* @return float
*/
public function getNodeImpurityDecrease(int $parentRecordCount)
@@ -133,7 +135,7 @@ class DecisionTreeLeaf
} else {
$col = "col_$this->columnIndex";
}
if (! preg_match("/^[<>=]{1,2}/", $value)) {
if (!preg_match("/^[<>=]{1,2}/", $value)) {
$value = "=$value";
}
$value = "<b>$col $value</b><br>Gini: ". number_format($this->giniIndex, 2);
@@ -75,6 +75,7 @@ class AdaBoost implements Classifier
* improve classification performance of 'weak' classifiers such as
* DecisionStump (default base classifier of AdaBoost).
*
* @param int $maxIterations
*/
public function __construct(int $maxIterations = 50)
{
@@ -96,6 +97,8 @@ class AdaBoost implements Classifier
/**
* @param array $samples
* @param array $targets
*
* @throws \Exception
*/
public function train(array $samples, array $targets)
{
@@ -123,7 +126,6 @@ class AdaBoost implements Classifier
// Execute the algorithm for a maximum number of iterations
$currIter = 0;
while ($this->maxIterations > $currIter++) {
// Determine the best 'weak' classifier based on current weights
$classifier = $this->getBestClassifier();
$errorRate = $this->evaluateClassifier($classifier);
@@ -181,7 +183,7 @@ class AdaBoost implements Classifier
$targets = [];
foreach ($weights as $index => $weight) {
$z = (int)round(($weight - $mean) / $std) - $minZ + 1;
for ($i=0; $i < $z; $i++) {
for ($i = 0; $i < $z; ++$i) {
if (rand(0, 1) == 0) {
continue;
}
@@ -197,6 +199,8 @@ class AdaBoost implements Classifier
* Evaluates the classifier and returns the classification error rate
*
* @param Classifier $classifier
*
* @return float
*/
protected function evaluateClassifier(Classifier $classifier)
{
@@ -59,13 +59,13 @@ class Bagging implements Classifier
private $samples = [];
/**
* Creates an ensemble classifier with given number of base classifiers<br>
* Default number of base classifiers is 100.
* Creates an ensemble classifier with given number of base classifiers
* Default number of base classifiers is 50.
* The more number of base classifiers, the better performance but at the cost of procesing time
*
* @param int $numClassifier
*/
public function __construct($numClassifier = 50)
public function __construct(int $numClassifier = 50)
{
$this->numClassifier = $numClassifier;
}
@@ -76,14 +76,17 @@ class Bagging implements Classifier
* to train each base classifier.
*
* @param float $ratio
*
* @return $this
* @throws Exception
*
* @throws \Exception
*/
public function setSubsetRatio(float $ratio)
{
if ($ratio < 0.1 || $ratio > 1.0) {
throw new \Exception("Subset ratio should be between 0.1 and 1.0");
}
$this->subsetRatio = $ratio;
return $this;
}
@@ -98,12 +101,14 @@ class Bagging implements Classifier
*
* @param string $classifier
* @param array $classifierOptions
*
* @return $this
*/
public function setClassifer(string $classifier, array $classifierOptions = [])
{
$this->classifier = $classifier;
$this->classifierOptions = $classifierOptions;
return $this;
}
@@ -138,11 +143,12 @@ class Bagging implements Classifier
$targets = [];
srand($index);
$bootstrapSize = $this->subsetRatio * $this->numSamples;
for ($i=0; $i < $bootstrapSize; $i++) {
for ($i = 0; $i < $bootstrapSize; ++$i) {
$rand = rand(0, $this->numSamples - 1);
$samples[] = $this->samples[$rand];
$targets[] = $this->targets[$rand];
}
return [$samples, $targets];
}
@@ -152,24 +158,25 @@ class Bagging implements Classifier
protected function initClassifiers()
{
$classifiers = [];
for ($i=0; $i<$this->numClassifier; $i++) {
for ($i = 0; $i < $this->numClassifier; ++$i) {
$ref = new \ReflectionClass($this->classifier);
if ($this->classifierOptions) {
$obj = $ref->newInstanceArgs($this->classifierOptions);
} else {
$obj = $ref->newInstance();
}
$classifiers[] = $this->initSingleClassifier($obj, $i);
$classifiers[] = $this->initSingleClassifier($obj);
}
return $classifiers;
}
/**
* @param Classifier $classifier
* @param int $index
*
* @return Classifier
*/
protected function initSingleClassifier($classifier, $index)
protected function initSingleClassifier($classifier)
{
return $classifier;
}
@@ -5,7 +5,6 @@ declare(strict_types=1);
namespace Phpml\Classification\Ensemble;
use Phpml\Classification\DecisionTree;
use Phpml\Classification\Classifier;
class RandomForest extends Bagging
{
@@ -24,9 +23,9 @@ class RandomForest extends Bagging
* may increase the prediction performance while it will also substantially
* increase the processing time and the required memory
*
* @param type $numClassifier
* @param int $numClassifier
*/
public function __construct($numClassifier = 50)
public function __construct(int $numClassifier = 50)
{
parent::__construct($numClassifier);
@@ -43,17 +42,21 @@ class RandomForest extends Bagging
* features to be taken into consideration while selecting subspace of features
*
* @param mixed $ratio string or float should be given
*
* @return $this
* @throws Exception
*
* @throws \Exception
*/
public function setFeatureSubsetRatio($ratio)
{
if (is_float($ratio) && ($ratio < 0.1 || $ratio > 1.0)) {
throw new \Exception("When a float given, feature subset ratio should be between 0.1 and 1.0");
}
if (is_string($ratio) && $ratio != 'sqrt' && $ratio != 'log') {
throw new \Exception("When a string given, feature subset ratio can only be 'sqrt' or 'log' ");
}
$this->featureSubsetRatio = $ratio;
return $this;
}
@@ -62,8 +65,11 @@ class RandomForest extends Bagging
* RandomForest algorithm is usable *only* with DecisionTree
*
* @param string $classifier
* @param array $classifierOptions
* @param array $classifierOptions
*
* @return $this
*
* @throws \Exception
*/
public function setClassifer(string $classifier, array $classifierOptions = [])
{
@@ -125,10 +131,10 @@ class RandomForest extends Bagging
/**
* @param DecisionTree $classifier
* @param int $index
*
* @return DecisionTree
*/
protected function initSingleClassifier($classifier, $index)
protected function initSingleClassifier($classifier)
{
if (is_float($this->featureSubsetRatio)) {
$featureCount = (int)($this->featureSubsetRatio * $this->featureCount);
@@ -4,11 +4,8 @@ declare(strict_types=1);
namespace Phpml\Classification\Linear;
use Phpml\Classification\Classifier;
class Adaline extends Perceptron
{
/**
* Batch training is the default Adaline training algorithm
*/
@@ -35,13 +32,17 @@ class Adaline extends Perceptron
* If normalizeInputs is set to true, then every input given to the algorithm will be standardized
* by use of standard deviation and mean calculation
*
* @param int $learningRate
* @param int $maxIterations
* @param float $learningRate
* @param int $maxIterations
* @param bool $normalizeInputs
* @param int $trainingType
*
* @throws \Exception
*/
public function __construct(float $learningRate = 0.001, int $maxIterations = 1000,
bool $normalizeInputs = true, int $trainingType = self::BATCH_TRAINING)
{
if (! in_array($trainingType, [self::BATCH_TRAINING, self::ONLINE_TRAINING])) {
if (!in_array($trainingType, [self::BATCH_TRAINING, self::ONLINE_TRAINING])) {
throw new \Exception("Adaline can only be trained with batch and online/stochastic gradient descent algorithm");
}
@@ -87,6 +87,8 @@ class DecisionStump extends WeightedClassifier
/**
* @param array $samples
* @param array $targets
* @param array $labels
*
* @throws \Exception
*/
protected function trainBinary(array $samples, array $targets, array $labels)
@@ -237,13 +239,13 @@ class DecisionStump extends WeightedClassifier
/**
*
* @param type $leftValue
* @param type $operator
* @param type $rightValue
* @param mixed $leftValue
* @param string $operator
* @param mixed $rightValue
*
* @return boolean
*/
protected function evaluate($leftValue, $operator, $rightValue)
protected function evaluate($leftValue, string $operator, $rightValue)
{
switch ($operator) {
case '>': return $leftValue > $rightValue;
@@ -288,10 +290,10 @@ class DecisionStump extends WeightedClassifier
$wrong += $this->weights[$index];
}
if (! isset($prob[$predicted][$target])) {
if (!isset($prob[$predicted][$target])) {
$prob[$predicted][$target] = 0;
}
$prob[$predicted][$target]++;
++$prob[$predicted][$target];
}
// Calculate probabilities: Proportion of labels in each leaf
@@ -4,21 +4,19 @@ declare(strict_types=1);
namespace Phpml\Classification\Linear;
use Phpml\Classification\Classifier;
use Phpml\Helper\Optimizer\ConjugateGradient;
class LogisticRegression extends Adaline
{
/**
* Batch training: Gradient descent algorithm (default)
*/
const BATCH_TRAINING = 1;
const BATCH_TRAINING = 1;
/**
* Online training: Stochastic gradient descent learning
*/
const ONLINE_TRAINING = 2;
const ONLINE_TRAINING = 2;
/**
* Conjugate Batch: Conjugate Gradient algorithm
@@ -74,13 +72,13 @@ class LogisticRegression extends Adaline
string $penalty = 'L2')
{
$trainingTypes = range(self::BATCH_TRAINING, self::CONJUGATE_GRAD_TRAINING);
if (! in_array($trainingType, $trainingTypes)) {
if (!in_array($trainingType, $trainingTypes)) {
throw new \Exception("Logistic regression can only be trained with " .
"batch (gradient descent), online (stochastic gradient descent) " .
"or conjugate batch (conjugate gradients) algorithms");
}
if (! in_array($cost, ['log', 'sse'])) {
if (!in_array($cost, ['log', 'sse'])) {
throw new \Exception("Logistic regression cost function can be one of the following: \n" .
"'log' for log-likelihood and 'sse' for sum of squared errors");
}
@@ -126,6 +124,8 @@ class LogisticRegression extends Adaline
*
* @param array $samples
* @param array $targets
*
* @throws \Exception
*/
protected function runTraining(array $samples, array $targets)
{
@@ -140,12 +140,18 @@ class LogisticRegression extends Adaline
case self::CONJUGATE_GRAD_TRAINING:
return $this->runConjugateGradient($samples, $targets, $callback);
default:
throw new \Exception('Logistic regression has invalid training type: %s.', $this->trainingType);
}
}
/**
* Executes Conjugate Gradient method to optimize the
* weights of the LogReg model
* Executes Conjugate Gradient method to optimize the weights of the LogReg model
*
* @param array $samples
* @param array $targets
* @param \Closure $gradientFunc
*/
protected function runConjugateGradient(array $samples, array $targets, \Closure $gradientFunc)
{
@@ -162,6 +168,8 @@ class LogisticRegression extends Adaline
* Returns the appropriate callback function for the selected cost function
*
* @return \Closure
*
* @throws \Exception
*/
protected function getCostFunction()
{
@@ -203,7 +211,7 @@ class LogisticRegression extends Adaline
return $callback;
case 'sse':
/**
/*
* Sum of squared errors or least squared errors cost function:
* J(x) = ∑ (y - h(x))^2
*
@@ -224,6 +232,9 @@ class LogisticRegression extends Adaline
};
return $callback;
default:
throw new \Exception(sprintf('Logistic regression has invalid cost function: %s.', $this->costFunction));
}
}
@@ -245,6 +256,7 @@ class LogisticRegression extends Adaline
* Returns the class value (either -1 or 1) for the given input
*
* @param array $sample
*
* @return int
*/
protected function outputClass(array $sample)
@@ -266,6 +278,8 @@ class LogisticRegression extends Adaline
*
* @param array $sample
* @param mixed $label
*
* @return float
*/
protected function predictProbability(array $sample, $label)
{
@@ -63,22 +63,22 @@ class Perceptron implements Classifier, IncrementalEstimator
/**
* Initalize a perceptron classifier with given learning rate and maximum
* number of iterations used while training the perceptron <br>
* number of iterations used while training the perceptron
*
* Learning rate should be a float value between 0.0(exclusive) and 1.0(inclusive) <br>
* Maximum number of iterations can be an integer value greater than 0
* @param int $learningRate
* @param int $maxIterations
* @param float $learningRate Value between 0.0(exclusive) and 1.0(inclusive)
* @param int $maxIterations Must be at least 1
* @param bool $normalizeInputs
*
* @throws \Exception
*/
public function __construct(float $learningRate = 0.001, int $maxIterations = 1000,
bool $normalizeInputs = true)
public function __construct(float $learningRate = 0.001, int $maxIterations = 1000, bool $normalizeInputs = true)
{
if ($learningRate <= 0.0 || $learningRate > 1.0) {
throw new \Exception("Learning rate should be a float value between 0.0(exclusive) and 1.0(inclusive)");
}
if ($maxIterations <= 0) {
throw new \Exception("Maximum number of iterations should be an integer greater than 0");
throw new \Exception("Maximum number of iterations must be an integer greater than 0");
}
if ($normalizeInputs) {
@@ -96,7 +96,7 @@ class Perceptron implements Classifier, IncrementalEstimator
*/
public function partialTrain(array $samples, array $targets, array $labels = [])
{
return $this->trainByLabel($samples, $targets, $labels);
$this->trainByLabel($samples, $targets, $labels);
}
/**
@@ -140,6 +140,8 @@ class Perceptron implements Classifier, IncrementalEstimator
* for $maxIterations times
*
* @param bool $enable
*
* @return $this
*/
public function setEarlyStop(bool $enable = true)
{
@@ -185,12 +187,14 @@ class Perceptron implements Classifier, IncrementalEstimator
* Executes a Gradient Descent algorithm for
* the given cost function
*
* @param array $samples
* @param array $targets
* @param array $samples
* @param array $targets
* @param \Closure $gradientFunc
* @param bool $isBatch
*/
protected function runGradientDescent(array $samples, array $targets, \Closure $gradientFunc, bool $isBatch = false)
{
$class = $isBatch ? GD::class : StochasticGD::class;
$class = $isBatch ? GD::class : StochasticGD::class;
if (empty($this->optimizer)) {
$this->optimizer = (new $class($this->featureCount))
@@ -262,6 +266,8 @@ class Perceptron implements Classifier, IncrementalEstimator
*
* @param array $sample
* @param mixed $label
*
* @return float
*/
protected function predictProbability(array $sample, $label)
{
@@ -277,6 +283,7 @@ class Perceptron implements Classifier, IncrementalEstimator
/**
* @param array $sample
*
* @return mixed
*/
protected function predictSampleBinary(array $sample)
@@ -285,6 +292,6 @@ class Perceptron implements Classifier, IncrementalEstimator
$predictedClass = $this->outputClass($sample);
return $this->labels[ $predictedClass ];
return $this->labels[$predictedClass];
}
}
@@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace Phpml\Classification;
use Phpml\Exception\InvalidArgumentException;
use Phpml\NeuralNetwork\Network\MultilayerPerceptron;
class MLPClassifier extends MultilayerPerceptron implements Classifier
{
/**
* @param mixed $target
* @return int
*/
public function getTargetClass($target): int
{
if (!in_array($target, $this->classes)) {
throw InvalidArgumentException::invalidTarget($target);
}
return array_search($target, $this->classes);
}
/**
* @param array $sample
*
* @return mixed
*/
protected function predictSample(array $sample)
{
$output = $this->setInput($sample)->getOutput();
$predictedClass = null;
$max = 0;
foreach ($output as $class => $value) {
if ($value > $max) {
$predictedClass = $class;
$max = $value;
}
}
return $this->classes[$predictedClass];
}
/**
* @param array $sample
* @param mixed $target
*/
protected function trainSample(array $sample, $target)
{
// Feed-forward.
$this->setInput($sample)->getOutput();
// Back-propagate.
$this->backpropagation->backpropagate($this->getLayers(), $this->getTargetClass($target));
}
}
@@ -89,7 +89,7 @@ class NaiveBayes implements Classifier
$this->mean[$label]= array_fill(0, $this->featureCount, 0);
$this->dataType[$label] = array_fill(0, $this->featureCount, self::CONTINUOS);
$this->discreteProb[$label] = array_fill(0, $this->featureCount, self::CONTINUOS);
for ($i=0; $i<$this->featureCount; $i++) {
for ($i = 0; $i < $this->featureCount; ++$i) {
// Get the values of nth column in the samples array
// Mean::arithmetic is called twice, can be optimized
$values = array_column($samples, $i);
@@ -114,16 +114,17 @@ class NaiveBayes implements Classifier
/**
* Calculates the probability P(label|sample_n)
*
* @param array $sample
* @param int $feature
* @param array $sample
* @param int $feature
* @param string $label
*
* @return float
*/
private function sampleProbability($sample, $feature, $label)
{
$value = $sample[$feature];
if ($this->dataType[$label][$feature] == self::NOMINAL) {
if (! isset($this->discreteProb[$label][$feature][$value]) ||
if (!isset($this->discreteProb[$label][$feature][$value]) ||
$this->discreteProb[$label][$feature][$value] == 0) {
return self::EPSILON;
}
@@ -145,13 +146,15 @@ class NaiveBayes implements Classifier
/**
* Return samples belonging to specific label
*
* @param string $label
*
* @return array
*/
private function getSamplesByLabel($label)
{
$samples = [];
for ($i=0; $i<$this->sampleCount; $i++) {
for ($i = 0; $i < $this->sampleCount; ++$i) {
if ($this->targets[$i] == $label) {
$samples[] = $this->samples[$i];
}
@@ -171,12 +174,13 @@ class NaiveBayes implements Classifier
$predictions = [];
foreach ($this->labels as $label) {
$p = $this->p[$label];
for ($i=0; $i<$this->featureCount; $i++) {
for ($i = 0; $i<$this->featureCount; ++$i) {
$Plf = $this->sampleProbability($sample, $i, $label);
$p += $Plf;
}
$predictions[$label] = $p;
}
arsort($predictions, SORT_NUMERIC);
reset($predictions);
return key($predictions);