MDL-65769 lib: update PHP-ML to 0.8.0
This commit is contained in:
@@ -4,23 +4,40 @@ declare(strict_types=1);
|
||||
|
||||
namespace Phpml\Classification;
|
||||
|
||||
use Phpml\Classification\DecisionTree\DecisionTreeLeaf;
|
||||
use Phpml\Exception\InvalidArgumentException;
|
||||
use Phpml\Helper\Predictable;
|
||||
use Phpml\Helper\Trainable;
|
||||
use Phpml\Math\Statistic\Mean;
|
||||
use Phpml\Classification\DecisionTree\DecisionTreeLeaf;
|
||||
|
||||
class DecisionTree implements Classifier
|
||||
{
|
||||
use Trainable, Predictable;
|
||||
use Trainable;
|
||||
use Predictable;
|
||||
|
||||
const CONTINUOUS = 1;
|
||||
const NOMINAL = 2;
|
||||
public const CONTINUOUS = 1;
|
||||
|
||||
public const NOMINAL = 2;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
public $actualDepth = 0;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $columnTypes;
|
||||
protected $columnTypes = [];
|
||||
|
||||
/**
|
||||
* @var DecisionTreeLeaf
|
||||
*/
|
||||
protected $tree;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $maxDepth;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
@@ -32,21 +49,6 @@ class DecisionTree implements Classifier
|
||||
*/
|
||||
private $featureCount = 0;
|
||||
|
||||
/**
|
||||
* @var DecisionTreeLeaf
|
||||
*/
|
||||
protected $tree = null;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $maxDepth;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
public $actualDepth = 0;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
@@ -55,32 +57,24 @@ class DecisionTree implements Classifier
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $selectedFeatures;
|
||||
private $selectedFeatures = [];
|
||||
|
||||
/**
|
||||
* @var array|null
|
||||
*/
|
||||
private $featureImportances;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $featureImportances = null;
|
||||
private $columnNames = [];
|
||||
|
||||
/**
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $columnNames = null;
|
||||
|
||||
/**
|
||||
* @param int $maxDepth
|
||||
*/
|
||||
public function __construct(int $maxDepth = 10)
|
||||
{
|
||||
$this->maxDepth = $maxDepth;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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);
|
||||
@@ -96,23 +90,19 @@ class DecisionTree implements Classifier
|
||||
|
||||
// If column names are given or computed before, then there is no
|
||||
// need to init it and accidentally remove the previous given names
|
||||
if ($this->columnNames === null) {
|
||||
if ($this->columnNames === []) {
|
||||
$this->columnNames = range(0, $this->featureCount - 1);
|
||||
} elseif (count($this->columnNames) > $this->featureCount) {
|
||||
$this->columnNames = array_slice($this->columnNames, 0, $this->featureCount);
|
||||
} elseif (count($this->columnNames) < $this->featureCount) {
|
||||
$this->columnNames = array_merge($this->columnNames,
|
||||
$this->columnNames = array_merge(
|
||||
$this->columnNames,
|
||||
range(count($this->columnNames), $this->featureCount - 1)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $samples
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function getColumnTypes(array $samples) : array
|
||||
public static function getColumnTypes(array $samples): array
|
||||
{
|
||||
$types = [];
|
||||
$featureCount = count($samples[0]);
|
||||
@@ -126,12 +116,120 @@ class DecisionTree implements Classifier
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $records
|
||||
* @param int $depth
|
||||
*
|
||||
* @return DecisionTreeLeaf
|
||||
* @param mixed $baseValue
|
||||
*/
|
||||
protected function getSplitLeaf(array $records, int $depth = 0) : DecisionTreeLeaf
|
||||
public function getGiniIndex($baseValue, array $colValues, array $targets): float
|
||||
{
|
||||
$countMatrix = [];
|
||||
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];
|
||||
}
|
||||
|
||||
$giniParts = [0, 0];
|
||||
for ($i = 0; $i <= 1; ++$i) {
|
||||
$part = 0;
|
||||
$sum = array_sum(array_column($countMatrix, $i));
|
||||
if ($sum > 0) {
|
||||
foreach ($this->labels as $label) {
|
||||
$part += ($countMatrix[$label][$i] / (float) $sum) ** 2;
|
||||
}
|
||||
}
|
||||
|
||||
$giniParts[$i] = (1 - $part) * $sum;
|
||||
}
|
||||
|
||||
return array_sum($giniParts) / count($colValues);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is used to set number of columns to be used
|
||||
* when deciding a split at an internal node of the tree. <br>
|
||||
* If the value is given 0, then all features are used (default behaviour),
|
||||
* otherwise the given value will be used as a maximum for number of columns
|
||||
* randomly selected for each split operation.
|
||||
*
|
||||
* @return $this
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function setNumFeatures(int $numFeatures)
|
||||
{
|
||||
if ($numFeatures < 0) {
|
||||
throw new InvalidArgumentException('Selected column count should be greater or equal to zero');
|
||||
}
|
||||
|
||||
$this->numUsableFeatures = $numFeatures;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* A string array to represent columns. Useful when HTML output or
|
||||
* column importances are desired to be inspected.
|
||||
*
|
||||
* @return $this
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function setColumnNames(array $names)
|
||||
{
|
||||
if ($this->featureCount !== 0 && count($names) !== $this->featureCount) {
|
||||
throw new InvalidArgumentException(sprintf('Length of the given array should be equal to feature count %s', $this->featureCount));
|
||||
}
|
||||
|
||||
$this->columnNames = $names;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getHtml(): string
|
||||
{
|
||||
return $this->tree->getHTML($this->columnNames);
|
||||
}
|
||||
|
||||
/**
|
||||
* This will return an array including an importance value for
|
||||
* each column in the given dataset. The importance values are
|
||||
* normalized and their total makes 1.<br/>
|
||||
*/
|
||||
public function getFeatureImportances(): array
|
||||
{
|
||||
if ($this->featureImportances !== null) {
|
||||
return $this->featureImportances;
|
||||
}
|
||||
|
||||
$sampleCount = count($this->samples);
|
||||
$this->featureImportances = [];
|
||||
foreach ($this->columnNames as $column => $columnName) {
|
||||
$nodes = $this->getSplitNodesByColumn($column, $this->tree);
|
||||
|
||||
$importance = 0;
|
||||
foreach ($nodes as $node) {
|
||||
$importance += $node->getNodeImpurityDecrease($sampleCount);
|
||||
}
|
||||
|
||||
$this->featureImportances[$columnName] = $importance;
|
||||
}
|
||||
|
||||
// Normalize & sort the importances
|
||||
$total = array_sum($this->featureImportances);
|
||||
if ($total > 0) {
|
||||
array_walk($this->featureImportances, function (&$importance) use ($total): void {
|
||||
$importance /= $total;
|
||||
});
|
||||
arsort($this->featureImportances);
|
||||
}
|
||||
|
||||
return $this->featureImportances;
|
||||
}
|
||||
|
||||
protected function getSplitLeaf(array $records, int $depth = 0): DecisionTreeLeaf
|
||||
{
|
||||
$split = $this->getBestSplit($records);
|
||||
$split->level = $depth;
|
||||
@@ -143,7 +241,7 @@ class DecisionTree implements Classifier
|
||||
// otherwise group the records so that we can classify the leaf
|
||||
// in case maximum depth is reached
|
||||
$leftRecords = [];
|
||||
$rightRecords= [];
|
||||
$rightRecords = [];
|
||||
$remainingTargets = [];
|
||||
$prevRecord = null;
|
||||
$allSame = true;
|
||||
@@ -151,9 +249,10 @@ class DecisionTree implements Classifier
|
||||
foreach ($records as $recordNo) {
|
||||
// Check if the previous record is the same with the current one
|
||||
$record = $this->samples[$recordNo];
|
||||
if ($prevRecord && $prevRecord != $record) {
|
||||
if ($prevRecord !== null && $prevRecord != $record) {
|
||||
$allSame = false;
|
||||
}
|
||||
|
||||
$prevRecord = $record;
|
||||
|
||||
// According to the split criteron, this record will
|
||||
@@ -161,7 +260,7 @@ class DecisionTree implements Classifier
|
||||
if ($split->evaluate($record)) {
|
||||
$leftRecords[] = $recordNo;
|
||||
} else {
|
||||
$rightRecords[]= $recordNo;
|
||||
$rightRecords[] = $recordNo;
|
||||
}
|
||||
|
||||
// Group remaining targets
|
||||
@@ -174,31 +273,29 @@ class DecisionTree implements Classifier
|
||||
}
|
||||
|
||||
if ($allSame || $depth >= $this->maxDepth || count($remainingTargets) === 1) {
|
||||
$split->isTerminal = 1;
|
||||
$split->isTerminal = true;
|
||||
arsort($remainingTargets);
|
||||
$split->classValue = key($remainingTargets);
|
||||
$split->classValue = (string) key($remainingTargets);
|
||||
} else {
|
||||
if ($leftRecords) {
|
||||
if (isset($leftRecords[0])) {
|
||||
$split->leftLeaf = $this->getSplitLeaf($leftRecords, $depth + 1);
|
||||
}
|
||||
if ($rightRecords) {
|
||||
$split->rightLeaf= $this->getSplitLeaf($rightRecords, $depth + 1);
|
||||
|
||||
if (isset($rightRecords[0])) {
|
||||
$split->rightLeaf = $this->getSplitLeaf($rightRecords, $depth + 1);
|
||||
}
|
||||
}
|
||||
|
||||
return $split;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $records
|
||||
*
|
||||
* @return DecisionTreeLeaf
|
||||
*/
|
||||
protected function getBestSplit(array $records) : DecisionTreeLeaf
|
||||
protected function getBestSplit(array $records): DecisionTreeLeaf
|
||||
{
|
||||
$targets = array_intersect_key($this->targets, array_flip($records));
|
||||
$samples = array_intersect_key($this->samples, array_flip($records));
|
||||
$samples = array_combine($records, $this->preprocess($samples));
|
||||
$samples = (array) array_combine(
|
||||
$records,
|
||||
$this->preprocess(array_intersect_key($this->samples, array_flip($records)))
|
||||
);
|
||||
$bestGiniVal = 1;
|
||||
$bestSplit = null;
|
||||
$features = $this->getSelectedFeatures();
|
||||
@@ -207,26 +304,31 @@ class DecisionTree implements Classifier
|
||||
foreach ($samples as $index => $row) {
|
||||
$colValues[$index] = $row[$i];
|
||||
}
|
||||
|
||||
$counts = array_count_values($colValues);
|
||||
arsort($counts);
|
||||
$baseValue = key($counts);
|
||||
if ($baseValue === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$gini = $this->getGiniIndex($baseValue, $colValues, $targets);
|
||||
if ($bestSplit === null || $bestGiniVal > $gini) {
|
||||
$split = new DecisionTreeLeaf();
|
||||
$split->value = $baseValue;
|
||||
$split->giniIndex = $gini;
|
||||
$split->columnIndex = $i;
|
||||
$split->isContinuous = $this->columnTypes[$i] == self::CONTINUOUS;
|
||||
$split->isContinuous = $this->columnTypes[$i] === self::CONTINUOUS;
|
||||
$split->records = $records;
|
||||
|
||||
// If a numeric column is to be selected, then
|
||||
// the original numeric value and the selected operator
|
||||
// will also be saved into the leaf for future access
|
||||
if ($this->columnTypes[$i] == self::CONTINUOUS) {
|
||||
if ($this->columnTypes[$i] === self::CONTINUOUS) {
|
||||
$matches = [];
|
||||
preg_match("/^([<>=]{1,2})\s*(.*)/", strval($split->value), $matches);
|
||||
preg_match("/^([<>=]{1,2})\s*(.*)/", (string) $split->value, $matches);
|
||||
$split->operator = $matches[1];
|
||||
$split->numericValue = floatval($matches[2]);
|
||||
$split->numericValue = (float) $matches[2];
|
||||
}
|
||||
|
||||
$bestSplit = $split;
|
||||
@@ -249,17 +351,15 @@ class DecisionTree implements Classifier
|
||||
*
|
||||
* If any of above methods were not called beforehand, then all features
|
||||
* are returned by default.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getSelectedFeatures() : array
|
||||
protected function getSelectedFeatures(): array
|
||||
{
|
||||
$allFeatures = range(0, $this->featureCount - 1);
|
||||
if ($this->numUsableFeatures === 0 && !$this->selectedFeatures) {
|
||||
if ($this->numUsableFeatures === 0 && count($this->selectedFeatures) === 0) {
|
||||
return $allFeatures;
|
||||
}
|
||||
|
||||
if ($this->selectedFeatures) {
|
||||
if (count($this->selectedFeatures) > 0) {
|
||||
return $this->selectedFeatures;
|
||||
}
|
||||
|
||||
@@ -267,55 +367,15 @@ class DecisionTree implements Classifier
|
||||
if ($numFeatures > $this->featureCount) {
|
||||
$numFeatures = $this->featureCount;
|
||||
}
|
||||
|
||||
shuffle($allFeatures);
|
||||
$selectedFeatures = array_slice($allFeatures, 0, $numFeatures, false);
|
||||
$selectedFeatures = array_slice($allFeatures, 0, $numFeatures);
|
||||
sort($selectedFeatures);
|
||||
|
||||
return $selectedFeatures;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $baseValue
|
||||
* @param array $colValues
|
||||
* @param array $targets
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
public function getGiniIndex($baseValue, array $colValues, array $targets) : float
|
||||
{
|
||||
$countMatrix = [];
|
||||
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];
|
||||
}
|
||||
|
||||
$giniParts = [0, 0];
|
||||
for ($i = 0; $i <= 1; ++$i) {
|
||||
$part = 0;
|
||||
$sum = array_sum(array_column($countMatrix, $i));
|
||||
if ($sum > 0) {
|
||||
foreach ($this->labels as $label) {
|
||||
$part += pow($countMatrix[$label][$i] / floatval($sum), 2);
|
||||
}
|
||||
}
|
||||
|
||||
$giniParts[$i] = (1 - $part) * $sum;
|
||||
}
|
||||
|
||||
return array_sum($giniParts) / count($colValues);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $samples
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function preprocess(array $samples) : array
|
||||
protected function preprocess(array $samples): array
|
||||
{
|
||||
// Detect and convert continuous data column values into
|
||||
// discrete values by using the median as a threshold value
|
||||
@@ -326,25 +386,22 @@ class DecisionTree implements Classifier
|
||||
$median = Mean::median($values);
|
||||
foreach ($values as &$value) {
|
||||
if ($value <= $median) {
|
||||
$value = "<= $median";
|
||||
$value = "<= ${median}";
|
||||
} else {
|
||||
$value = "> $median";
|
||||
$value = "> ${median}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$columns[] = $values;
|
||||
}
|
||||
|
||||
// Below method is a strange yet very simple & efficient method
|
||||
// to get the transpose of a 2D array
|
||||
return array_map(null, ...$columns);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $columnValues
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected static function isCategoricalColumn(array $columnValues) : bool
|
||||
protected static function isCategoricalColumn(array $columnValues): bool
|
||||
{
|
||||
$count = count($columnValues);
|
||||
|
||||
@@ -355,7 +412,7 @@ class DecisionTree implements Classifier
|
||||
// all values in that column (Lower than or equal to %20 of all values)
|
||||
$numericValues = array_filter($columnValues, 'is_numeric');
|
||||
$floatValues = array_filter($columnValues, 'is_float');
|
||||
if ($floatValues) {
|
||||
if (count($floatValues) > 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -368,119 +425,21 @@ class DecisionTree implements Classifier
|
||||
return count($distinctValues) <= $count / 5;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is used to set number of columns to be used
|
||||
* when deciding a split at an internal node of the tree. <br>
|
||||
* If the value is given 0, then all features are used (default behaviour),
|
||||
* otherwise the given value will be used as a maximum for number of columns
|
||||
* randomly selected for each split operation.
|
||||
*
|
||||
* @param int $numFeatures
|
||||
*
|
||||
* @return $this
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function setNumFeatures(int $numFeatures)
|
||||
{
|
||||
if ($numFeatures < 0) {
|
||||
throw new InvalidArgumentException('Selected column count should be greater or equal to zero');
|
||||
}
|
||||
|
||||
$this->numUsableFeatures = $numFeatures;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to set predefined features to consider while deciding which column to use for a split
|
||||
*
|
||||
* @param array $selectedFeatures
|
||||
*/
|
||||
protected function setSelectedFeatures(array $selectedFeatures)
|
||||
protected function setSelectedFeatures(array $selectedFeatures): void
|
||||
{
|
||||
$this->selectedFeatures = $selectedFeatures;
|
||||
}
|
||||
|
||||
/**
|
||||
* A string array to represent columns. Useful when HTML output or
|
||||
* column importances are desired to be inspected.
|
||||
*
|
||||
* @param array $names
|
||||
*
|
||||
* @return $this
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function setColumnNames(array $names)
|
||||
{
|
||||
if ($this->featureCount !== 0 && count($names) !== $this->featureCount) {
|
||||
throw new InvalidArgumentException(sprintf('Length of the given array should be equal to feature count %s', $this->featureCount));
|
||||
}
|
||||
|
||||
$this->columnNames = $names;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getHtml()
|
||||
{
|
||||
return $this->tree->getHTML($this->columnNames);
|
||||
}
|
||||
|
||||
/**
|
||||
* This will return an array including an importance value for
|
||||
* each column in the given dataset. The importance values are
|
||||
* normalized and their total makes 1.<br/>
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getFeatureImportances()
|
||||
{
|
||||
if ($this->featureImportances !== null) {
|
||||
return $this->featureImportances;
|
||||
}
|
||||
|
||||
$sampleCount = count($this->samples);
|
||||
$this->featureImportances = [];
|
||||
foreach ($this->columnNames as $column => $columnName) {
|
||||
$nodes = $this->getSplitNodesByColumn($column, $this->tree);
|
||||
|
||||
$importance = 0;
|
||||
foreach ($nodes as $node) {
|
||||
$importance += $node->getNodeImpurityDecrease($sampleCount);
|
||||
}
|
||||
|
||||
$this->featureImportances[$columnName] = $importance;
|
||||
}
|
||||
|
||||
// Normalize & sort the importances
|
||||
$total = array_sum($this->featureImportances);
|
||||
if ($total > 0) {
|
||||
foreach ($this->featureImportances as &$importance) {
|
||||
$importance /= $total;
|
||||
}
|
||||
arsort($this->featureImportances);
|
||||
}
|
||||
|
||||
return $this->featureImportances;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collects and returns an array of internal nodes that use the given
|
||||
* column as a split criterion
|
||||
*
|
||||
* @param int $column
|
||||
* @param DecisionTreeLeaf $node
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getSplitNodesByColumn(int $column, DecisionTreeLeaf $node) : array
|
||||
protected function getSplitNodesByColumn(int $column, DecisionTreeLeaf $node): array
|
||||
{
|
||||
if (!$node || $node->isTerminal) {
|
||||
if ($node->isTerminal) {
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -491,22 +450,18 @@ class DecisionTree implements Classifier
|
||||
|
||||
$lNodes = [];
|
||||
$rNodes = [];
|
||||
if ($node->leftLeaf) {
|
||||
if ($node->leftLeaf !== null) {
|
||||
$lNodes = $this->getSplitNodesByColumn($column, $node->leftLeaf);
|
||||
}
|
||||
|
||||
if ($node->rightLeaf) {
|
||||
if ($node->rightLeaf !== null) {
|
||||
$rNodes = $this->getSplitNodesByColumn($column, $node->rightLeaf);
|
||||
}
|
||||
|
||||
$nodes = array_merge($nodes, $lNodes, $rNodes);
|
||||
|
||||
return $nodes;
|
||||
return array_merge($nodes, $lNodes, $rNodes);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $sample
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
protected function predictSample(array $sample)
|
||||
@@ -514,7 +469,7 @@ class DecisionTree implements Classifier
|
||||
$node = $this->tree;
|
||||
do {
|
||||
if ($node->isTerminal) {
|
||||
break;
|
||||
return $node->classValue;
|
||||
}
|
||||
|
||||
if ($node->evaluate($sample)) {
|
||||
@@ -524,6 +479,6 @@ class DecisionTree implements Classifier
|
||||
}
|
||||
} while ($node);
|
||||
|
||||
return $node ? $node->classValue : $this->labels[0];
|
||||
return $this->labels[0];
|
||||
}
|
||||
}
|
||||
|
||||
+45
-53
@@ -4,10 +4,12 @@ declare(strict_types=1);
|
||||
|
||||
namespace Phpml\Classification\DecisionTree;
|
||||
|
||||
use Phpml\Math\Comparison;
|
||||
|
||||
class DecisionTreeLeaf
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
* @var string|int
|
||||
*/
|
||||
public $value;
|
||||
|
||||
@@ -27,14 +29,14 @@ class DecisionTreeLeaf
|
||||
public $columnIndex;
|
||||
|
||||
/**
|
||||
* @var DecisionTreeLeaf
|
||||
* @var DecisionTreeLeaf|null
|
||||
*/
|
||||
public $leftLeaf = null;
|
||||
public $leftLeaf;
|
||||
|
||||
/**
|
||||
* @var DecisionTreeLeaf
|
||||
* @var DecisionTreeLeaf|null
|
||||
*/
|
||||
public $rightLeaf= null;
|
||||
public $rightLeaf;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
@@ -70,48 +72,44 @@ class DecisionTreeLeaf
|
||||
public $level = 0;
|
||||
|
||||
/**
|
||||
* @param array $record
|
||||
* @return bool
|
||||
* HTML representation of the tree without column names
|
||||
*/
|
||||
public function evaluate($record)
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->getHTML();
|
||||
}
|
||||
|
||||
public function evaluate(array $record): bool
|
||||
{
|
||||
$recordField = $record[$this->columnIndex];
|
||||
|
||||
if ($this->isContinuous) {
|
||||
$op = $this->operator;
|
||||
$value= $this->numericValue;
|
||||
$recordField = strval($recordField);
|
||||
eval("\$result = $recordField $op $value;");
|
||||
return $result;
|
||||
return Comparison::compare((string) $recordField, $this->numericValue, $this->operator);
|
||||
}
|
||||
|
||||
|
||||
return $recordField == $this->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
public function getNodeImpurityDecrease(int $parentRecordCount): float
|
||||
{
|
||||
if ($this->isTerminal) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
$nodeSampleCount = (float)count($this->records);
|
||||
$nodeSampleCount = (float) count($this->records);
|
||||
$iT = $this->giniIndex;
|
||||
|
||||
if ($this->leftLeaf) {
|
||||
$pL = count($this->leftLeaf->records)/$nodeSampleCount;
|
||||
if ($this->leftLeaf !== null) {
|
||||
$pL = count($this->leftLeaf->records) / $nodeSampleCount;
|
||||
$iT -= $pL * $this->leftLeaf->giniIndex;
|
||||
}
|
||||
|
||||
if ($this->rightLeaf) {
|
||||
$pR = count($this->rightLeaf->records)/$nodeSampleCount;
|
||||
if ($this->rightLeaf !== null) {
|
||||
$pR = count($this->rightLeaf->records) / $nodeSampleCount;
|
||||
$iT -= $pR * $this->rightLeaf->giniIndex;
|
||||
}
|
||||
|
||||
@@ -120,14 +118,11 @@ class DecisionTreeLeaf
|
||||
|
||||
/**
|
||||
* Returns HTML representation of the node including children nodes
|
||||
*
|
||||
* @param $columnNames
|
||||
* @return string
|
||||
*/
|
||||
public function getHTML($columnNames = null)
|
||||
public function getHTML(?array $columnNames = null): string
|
||||
{
|
||||
if ($this->isTerminal) {
|
||||
$value = "<b>$this->classValue</b>";
|
||||
$value = "<b>${this}->classValue</b>";
|
||||
} else {
|
||||
$value = $this->value;
|
||||
if ($columnNames !== null) {
|
||||
@@ -135,39 +130,36 @@ class DecisionTreeLeaf
|
||||
} else {
|
||||
$col = "col_$this->columnIndex";
|
||||
}
|
||||
if (!preg_match("/^[<>=]{1,2}/", $value)) {
|
||||
$value = "=$value";
|
||||
|
||||
if ((bool) preg_match('/^[<>=]{1,2}/', (string) $value) === false) {
|
||||
$value = "=${value}";
|
||||
}
|
||||
$value = "<b>$col $value</b><br>Gini: ". number_format($this->giniIndex, 2);
|
||||
|
||||
$value = "<b>${col} ${value}</b><br>Gini: ".number_format($this->giniIndex, 2);
|
||||
}
|
||||
$str = "<table ><tr><td colspan=3 align=center style='border:1px solid;'>
|
||||
$value</td></tr>";
|
||||
if ($this->leftLeaf || $this->rightLeaf) {
|
||||
$str .='<tr>';
|
||||
if ($this->leftLeaf) {
|
||||
$str .="<td valign=top><b>| Yes</b><br>" . $this->leftLeaf->getHTML($columnNames) . "</td>";
|
||||
|
||||
$str = "<table ><tr><td colspan=3 align=center style='border:1px solid;'>${value}</td></tr>";
|
||||
|
||||
if ($this->leftLeaf !== null || $this->rightLeaf !== null) {
|
||||
$str .= '<tr>';
|
||||
if ($this->leftLeaf !== null) {
|
||||
$str .= '<td valign=top><b>| Yes</b><br>'.$this->leftLeaf->getHTML($columnNames).'</td>';
|
||||
} else {
|
||||
$str .='<td></td>';
|
||||
$str .= '<td></td>';
|
||||
}
|
||||
$str .='<td> </td>';
|
||||
if ($this->rightLeaf) {
|
||||
$str .="<td valign=top align=right><b>No |</b><br>" . $this->rightLeaf->getHTML($columnNames) . "</td>";
|
||||
|
||||
$str .= '<td> </td>';
|
||||
if ($this->rightLeaf !== null) {
|
||||
$str .= '<td valign=top align=right><b>No |</b><br>'.$this->rightLeaf->getHTML($columnNames).'</td>';
|
||||
} else {
|
||||
$str .='<td></td>';
|
||||
$str .= '<td></td>';
|
||||
}
|
||||
|
||||
$str .= '</tr>';
|
||||
}
|
||||
|
||||
$str .= '</table>';
|
||||
|
||||
return $str;
|
||||
}
|
||||
|
||||
/**
|
||||
* HTML representation of the tree without column names
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return $this->getHTML();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,20 +4,24 @@ declare(strict_types=1);
|
||||
|
||||
namespace Phpml\Classification\Ensemble;
|
||||
|
||||
use Phpml\Classification\Classifier;
|
||||
use Phpml\Classification\Linear\DecisionStump;
|
||||
use Phpml\Classification\WeightedClassifier;
|
||||
use Phpml\Math\Statistic\Mean;
|
||||
use Phpml\Math\Statistic\StandardDeviation;
|
||||
use Phpml\Classification\Classifier;
|
||||
use Phpml\Exception\InvalidArgumentException;
|
||||
use Phpml\Helper\Predictable;
|
||||
use Phpml\Helper\Trainable;
|
||||
use Phpml\Math\Statistic\Mean;
|
||||
use Phpml\Math\Statistic\StandardDeviation;
|
||||
use ReflectionClass;
|
||||
|
||||
class AdaBoost implements Classifier
|
||||
{
|
||||
use Predictable, Trainable;
|
||||
use Predictable;
|
||||
use Trainable;
|
||||
|
||||
/**
|
||||
* Actual labels given in the targets array
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $labels = [];
|
||||
@@ -74,8 +78,6 @@ class AdaBoost implements Classifier
|
||||
* ADAptive BOOSTing (AdaBoost) is an ensemble algorithm to
|
||||
* improve classification performance of 'weak' classifiers such as
|
||||
* DecisionStump (default base classifier of AdaBoost).
|
||||
*
|
||||
* @param int $maxIterations
|
||||
*/
|
||||
public function __construct(int $maxIterations = 50)
|
||||
{
|
||||
@@ -84,32 +86,29 @@ class AdaBoost implements Classifier
|
||||
|
||||
/**
|
||||
* Sets the base classifier that will be used for boosting (default = DecisionStump)
|
||||
*
|
||||
* @param string $baseClassifier
|
||||
* @param array $classifierOptions
|
||||
*/
|
||||
public function setBaseClassifier(string $baseClassifier = DecisionStump::class, array $classifierOptions = [])
|
||||
public function setBaseClassifier(string $baseClassifier = DecisionStump::class, array $classifierOptions = []): void
|
||||
{
|
||||
$this->baseClassifier = $baseClassifier;
|
||||
$this->classifierOptions = $classifierOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $samples
|
||||
* @param array $targets
|
||||
*
|
||||
* @throws \Exception
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function train(array $samples, array $targets)
|
||||
public function train(array $samples, array $targets): void
|
||||
{
|
||||
// Initialize usual variables
|
||||
$this->labels = array_keys(array_count_values($targets));
|
||||
if (count($this->labels) != 2) {
|
||||
throw new \Exception("AdaBoost is a binary classifier and can classify between two classes only");
|
||||
if (count($this->labels) !== 2) {
|
||||
throw new InvalidArgumentException('AdaBoost is a binary classifier and can classify between two classes only');
|
||||
}
|
||||
|
||||
// Set all target values to either -1 or 1
|
||||
$this->labels = [1 => $this->labels[0], -1 => $this->labels[1]];
|
||||
$this->labels = [
|
||||
1 => $this->labels[0],
|
||||
-1 => $this->labels[1],
|
||||
];
|
||||
foreach ($targets as $target) {
|
||||
$this->targets[] = $target == $this->labels[1] ? 1 : -1;
|
||||
}
|
||||
@@ -140,25 +139,34 @@ class AdaBoost implements Classifier
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the classifier with the lowest error rate with the
|
||||
* consideration of current sample weights
|
||||
*
|
||||
* @return Classifier
|
||||
* @return mixed
|
||||
*/
|
||||
protected function getBestClassifier()
|
||||
public function predictSample(array $sample)
|
||||
{
|
||||
$ref = new \ReflectionClass($this->baseClassifier);
|
||||
if ($this->classifierOptions) {
|
||||
$classifier = $ref->newInstanceArgs($this->classifierOptions);
|
||||
} else {
|
||||
$classifier = $ref->newInstance();
|
||||
$sum = 0;
|
||||
foreach ($this->alpha as $index => $alpha) {
|
||||
$h = $this->classifiers[$index]->predict($sample);
|
||||
$sum += $h * $alpha;
|
||||
}
|
||||
|
||||
if (is_subclass_of($classifier, WeightedClassifier::class)) {
|
||||
return $this->labels[$sum > 0 ? 1 : -1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the classifier with the lowest error rate with the
|
||||
* consideration of current sample weights
|
||||
*/
|
||||
protected function getBestClassifier(): Classifier
|
||||
{
|
||||
$ref = new ReflectionClass($this->baseClassifier);
|
||||
/** @var Classifier $classifier */
|
||||
$classifier = count($this->classifierOptions) === 0 ? $ref->newInstance() : $ref->newInstanceArgs($this->classifierOptions);
|
||||
|
||||
if ($classifier instanceof WeightedClassifier) {
|
||||
$classifier->setSampleWeights($this->weights);
|
||||
$classifier->train($this->samples, $this->targets);
|
||||
} else {
|
||||
list($samples, $targets) = $this->resample();
|
||||
[$samples, $targets] = $this->resample();
|
||||
$classifier->train($samples, $targets);
|
||||
}
|
||||
|
||||
@@ -168,25 +176,24 @@ class AdaBoost implements Classifier
|
||||
/**
|
||||
* Resamples the dataset in accordance with the weights and
|
||||
* returns the new dataset
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function resample()
|
||||
protected function resample(): array
|
||||
{
|
||||
$weights = $this->weights;
|
||||
$std = StandardDeviation::population($weights);
|
||||
$mean= Mean::arithmetic($weights);
|
||||
$mean = Mean::arithmetic($weights);
|
||||
$min = min($weights);
|
||||
$minZ= (int)round(($min - $mean) / $std);
|
||||
$minZ = (int) round(($min - $mean) / $std);
|
||||
|
||||
$samples = [];
|
||||
$targets = [];
|
||||
foreach ($weights as $index => $weight) {
|
||||
$z = (int)round(($weight - $mean) / $std) - $minZ + 1;
|
||||
$z = (int) round(($weight - $mean) / $std) - $minZ + 1;
|
||||
for ($i = 0; $i < $z; ++$i) {
|
||||
if (rand(0, 1) == 0) {
|
||||
if (random_int(0, 1) == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$samples[] = $this->samples[$index];
|
||||
$targets[] = $this->targets[$index];
|
||||
}
|
||||
@@ -197,12 +204,8 @@ class AdaBoost implements Classifier
|
||||
|
||||
/**
|
||||
* Evaluates the classifier and returns the classification error rate
|
||||
*
|
||||
* @param Classifier $classifier
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
protected function evaluateClassifier(Classifier $classifier)
|
||||
protected function evaluateClassifier(Classifier $classifier): float
|
||||
{
|
||||
$total = (float) array_sum($this->weights);
|
||||
$wrong = 0;
|
||||
@@ -218,25 +221,20 @@ class AdaBoost implements Classifier
|
||||
|
||||
/**
|
||||
* Calculates alpha of a classifier
|
||||
*
|
||||
* @param float $errorRate
|
||||
* @return float
|
||||
*/
|
||||
protected function calculateAlpha(float $errorRate)
|
||||
protected function calculateAlpha(float $errorRate): float
|
||||
{
|
||||
if ($errorRate == 0) {
|
||||
$errorRate = 1e-10;
|
||||
}
|
||||
|
||||
return 0.5 * log((1 - $errorRate) / $errorRate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the sample weights
|
||||
*
|
||||
* @param Classifier $classifier
|
||||
* @param float $alpha
|
||||
*/
|
||||
protected function updateWeights(Classifier $classifier, float $alpha)
|
||||
protected function updateWeights(Classifier $classifier, float $alpha): void
|
||||
{
|
||||
$sumOfWeights = array_sum($this->weights);
|
||||
$weightsT1 = [];
|
||||
@@ -251,19 +249,4 @@ class AdaBoost implements Classifier
|
||||
|
||||
$this->weights = $weightsT1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $sample
|
||||
* @return mixed
|
||||
*/
|
||||
public function predictSample(array $sample)
|
||||
{
|
||||
$sum = 0;
|
||||
foreach ($this->alpha as $index => $alpha) {
|
||||
$h = $this->classifiers[$index]->predict($sample);
|
||||
$sum += $h * $alpha;
|
||||
}
|
||||
|
||||
return $this->labels[ $sum > 0 ? 1 : -1];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,25 +4,23 @@ declare(strict_types=1);
|
||||
|
||||
namespace Phpml\Classification\Ensemble;
|
||||
|
||||
use Phpml\Helper\Predictable;
|
||||
use Phpml\Helper\Trainable;
|
||||
use Phpml\Classification\Classifier;
|
||||
use Phpml\Classification\DecisionTree;
|
||||
use Phpml\Exception\InvalidArgumentException;
|
||||
use Phpml\Helper\Predictable;
|
||||
use Phpml\Helper\Trainable;
|
||||
use ReflectionClass;
|
||||
|
||||
class Bagging implements Classifier
|
||||
{
|
||||
use Trainable, Predictable;
|
||||
use Trainable;
|
||||
use Predictable;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $numSamples;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $targets = [];
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
@@ -34,7 +32,7 @@ class Bagging implements Classifier
|
||||
protected $numClassifier;
|
||||
|
||||
/**
|
||||
* @var Classifier
|
||||
* @var string
|
||||
*/
|
||||
protected $classifier = DecisionTree::class;
|
||||
|
||||
@@ -46,24 +44,17 @@ class Bagging implements Classifier
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $classifiers;
|
||||
protected $classifiers = [];
|
||||
|
||||
/**
|
||||
* @var float
|
||||
*/
|
||||
protected $subsetRatio = 0.7;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $samples = [];
|
||||
|
||||
/**
|
||||
* 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(int $numClassifier = 50)
|
||||
{
|
||||
@@ -75,19 +66,18 @@ class Bagging implements Classifier
|
||||
* e.g., random samples drawn from the original dataset with replacement (allow repeats),
|
||||
* to train each base classifier.
|
||||
*
|
||||
* @param float $ratio
|
||||
*
|
||||
* @return $this
|
||||
*
|
||||
* @throws \Exception
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
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");
|
||||
throw new InvalidArgumentException('Subset ratio should be between 0.1 and 1.0');
|
||||
}
|
||||
|
||||
$this->subsetRatio = $ratio;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -99,9 +89,6 @@ class Bagging implements Classifier
|
||||
* given in the order they are in the constructor of the classifier and parameter
|
||||
* names are neglected.
|
||||
*
|
||||
* @param string $classifier
|
||||
* @param array $classifierOptions
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setClassifer(string $classifier, array $classifierOptions = [])
|
||||
@@ -112,11 +99,7 @@ class Bagging implements Classifier
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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);
|
||||
@@ -127,24 +110,20 @@ class Bagging implements Classifier
|
||||
$this->classifiers = $this->initClassifiers();
|
||||
$index = 0;
|
||||
foreach ($this->classifiers as $classifier) {
|
||||
list($samples, $targets) = $this->getRandomSubset($index);
|
||||
[$samples, $targets] = $this->getRandomSubset($index);
|
||||
$classifier->train($samples, $targets);
|
||||
++$index;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $index
|
||||
* @return array
|
||||
*/
|
||||
protected function getRandomSubset(int $index)
|
||||
protected function getRandomSubset(int $index): array
|
||||
{
|
||||
$samples = [];
|
||||
$targets = [];
|
||||
srand($index);
|
||||
$bootstrapSize = $this->subsetRatio * $this->numSamples;
|
||||
for ($i = 0; $i < $bootstrapSize; ++$i) {
|
||||
$rand = rand(0, $this->numSamples - 1);
|
||||
$rand = random_int(0, $this->numSamples - 1);
|
||||
$samples[] = $this->samples[$rand];
|
||||
$targets[] = $this->targets[$rand];
|
||||
}
|
||||
@@ -152,50 +131,40 @@ class Bagging implements Classifier
|
||||
return [$samples, $targets];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
protected function initClassifiers()
|
||||
protected function initClassifiers(): array
|
||||
{
|
||||
$classifiers = [];
|
||||
for ($i = 0; $i < $this->numClassifier; ++$i) {
|
||||
$ref = new \ReflectionClass($this->classifier);
|
||||
if ($this->classifierOptions) {
|
||||
$obj = $ref->newInstanceArgs($this->classifierOptions);
|
||||
} else {
|
||||
$obj = $ref->newInstance();
|
||||
}
|
||||
$ref = new ReflectionClass($this->classifier);
|
||||
/** @var Classifier $obj */
|
||||
$obj = count($this->classifierOptions) === 0 ? $ref->newInstance() : $ref->newInstanceArgs($this->classifierOptions);
|
||||
|
||||
$classifiers[] = $this->initSingleClassifier($obj);
|
||||
}
|
||||
|
||||
return $classifiers;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Classifier $classifier
|
||||
*
|
||||
* @return Classifier
|
||||
*/
|
||||
protected function initSingleClassifier($classifier)
|
||||
protected function initSingleClassifier(Classifier $classifier): Classifier
|
||||
{
|
||||
return $classifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $sample
|
||||
* @return mixed
|
||||
*/
|
||||
protected function predictSample(array $sample)
|
||||
{
|
||||
$predictions = [];
|
||||
foreach ($this->classifiers as $classifier) {
|
||||
/* @var $classifier Classifier */
|
||||
/** @var Classifier $classifier */
|
||||
$predictions[] = $classifier->predict($sample);
|
||||
}
|
||||
|
||||
$counts = array_count_values($predictions);
|
||||
arsort($counts);
|
||||
reset($counts);
|
||||
|
||||
return key($counts);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,9 @@ declare(strict_types=1);
|
||||
|
||||
namespace Phpml\Classification\Ensemble;
|
||||
|
||||
use Phpml\Classification\Classifier;
|
||||
use Phpml\Classification\DecisionTree;
|
||||
use Phpml\Exception\InvalidArgumentException;
|
||||
|
||||
class RandomForest extends Bagging
|
||||
{
|
||||
@@ -14,16 +16,14 @@ class RandomForest extends Bagging
|
||||
protected $featureSubsetRatio = 'log';
|
||||
|
||||
/**
|
||||
* @var array
|
||||
* @var array|null
|
||||
*/
|
||||
protected $columnNames = null;
|
||||
protected $columnNames;
|
||||
|
||||
/**
|
||||
* Initializes RandomForest with the given number of trees. More trees
|
||||
* may increase the prediction performance while it will also substantially
|
||||
* increase the processing time and the required memory
|
||||
*
|
||||
* @param int $numClassifier
|
||||
*/
|
||||
public function __construct(int $numClassifier = 50)
|
||||
{
|
||||
@@ -41,40 +41,36 @@ class RandomForest extends Bagging
|
||||
* Default value for the ratio is 'log' which results in log(numFeatures, 2) + 1
|
||||
* features to be taken into consideration while selecting subspace of features
|
||||
*
|
||||
* @param mixed $ratio string or float should be given
|
||||
*
|
||||
* @return $this
|
||||
*
|
||||
* @throws \Exception
|
||||
* @param string|float $ratio
|
||||
*/
|
||||
public function setFeatureSubsetRatio($ratio)
|
||||
public function setFeatureSubsetRatio($ratio): self
|
||||
{
|
||||
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) && !is_float($ratio)) {
|
||||
throw new InvalidArgumentException('Feature subset ratio must be a string or a float');
|
||||
}
|
||||
|
||||
if (is_string($ratio) && $ratio != 'sqrt' && $ratio != 'log') {
|
||||
throw new \Exception("When a string given, feature subset ratio can only be 'sqrt' or 'log' ");
|
||||
if (is_float($ratio) && ($ratio < 0.1 || $ratio > 1.0)) {
|
||||
throw new InvalidArgumentException('When a float is given, feature subset ratio should be between 0.1 and 1.0');
|
||||
}
|
||||
|
||||
if (is_string($ratio) && $ratio !== 'sqrt' && $ratio !== 'log') {
|
||||
throw new InvalidArgumentException("When a string is given, feature subset ratio can only be 'sqrt' or 'log'");
|
||||
}
|
||||
|
||||
$this->featureSubsetRatio = $ratio;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* RandomForest algorithm is usable *only* with DecisionTree
|
||||
*
|
||||
* @param string $classifier
|
||||
* @param array $classifierOptions
|
||||
*
|
||||
* @return $this
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function setClassifer(string $classifier, array $classifierOptions = [])
|
||||
{
|
||||
if ($classifier != DecisionTree::class) {
|
||||
throw new \Exception("RandomForest can only use DecisionTree as base classifier");
|
||||
if ($classifier !== DecisionTree::class) {
|
||||
throw new InvalidArgumentException('RandomForest can only use DecisionTree as base classifier');
|
||||
}
|
||||
|
||||
return parent::setClassifer($classifier, $classifierOptions);
|
||||
@@ -84,15 +80,13 @@ class RandomForest extends Bagging
|
||||
* This will return an array including an importance value for
|
||||
* each column in the given dataset. Importance values for a column
|
||||
* is the average importance of that column in all trees in the forest
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getFeatureImportances()
|
||||
public function getFeatureImportances(): array
|
||||
{
|
||||
// Traverse each tree and sum importance of the columns
|
||||
$sum = [];
|
||||
foreach ($this->classifiers as $tree) {
|
||||
/* @var $tree DecisionTree */
|
||||
/** @var DecisionTree $tree */
|
||||
$importances = $tree->getFeatureImportances();
|
||||
|
||||
foreach ($importances as $column => $importance) {
|
||||
@@ -106,10 +100,9 @@ class RandomForest extends Bagging
|
||||
|
||||
// Normalize & sort the importance values
|
||||
$total = array_sum($sum);
|
||||
foreach ($sum as &$importance) {
|
||||
array_walk($sum, function (&$importance) use ($total): void {
|
||||
$importance /= $total;
|
||||
}
|
||||
|
||||
});
|
||||
arsort($sum);
|
||||
|
||||
return $sum;
|
||||
@@ -119,7 +112,6 @@ class RandomForest extends Bagging
|
||||
* A string array to represent the columns is given. They are useful
|
||||
* when trying to print some information about the trees such as feature importances
|
||||
*
|
||||
* @param array $names
|
||||
* @return $this
|
||||
*/
|
||||
public function setColumnNames(array $names)
|
||||
@@ -134,14 +126,14 @@ class RandomForest extends Bagging
|
||||
*
|
||||
* @return DecisionTree
|
||||
*/
|
||||
protected function initSingleClassifier($classifier)
|
||||
protected function initSingleClassifier(Classifier $classifier): Classifier
|
||||
{
|
||||
if (is_float($this->featureSubsetRatio)) {
|
||||
$featureCount = (int)($this->featureSubsetRatio * $this->featureCount);
|
||||
} elseif ($this->featureCount == 'sqrt') {
|
||||
$featureCount = (int)sqrt($this->featureCount) + 1;
|
||||
$featureCount = (int) ($this->featureSubsetRatio * $this->featureCount);
|
||||
} elseif ($this->featureSubsetRatio === 'sqrt') {
|
||||
$featureCount = (int) ($this->featureCount ** .5) + 1;
|
||||
} else {
|
||||
$featureCount = (int)log($this->featureCount, 2) + 1;
|
||||
$featureCount = (int) log($this->featureCount, 2) + 1;
|
||||
}
|
||||
|
||||
if ($featureCount >= $this->featureCount) {
|
||||
@@ -153,7 +145,7 @@ class RandomForest extends Bagging
|
||||
}
|
||||
|
||||
return $classifier
|
||||
->setColumnNames($this->columnNames)
|
||||
->setNumFeatures($featureCount);
|
||||
->setColumnNames($this->columnNames)
|
||||
->setNumFeatures($featureCount);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,8 @@ use Phpml\Math\Distance\Euclidean;
|
||||
|
||||
class KNearestNeighbors implements Classifier
|
||||
{
|
||||
use Trainable, Predictable;
|
||||
use Trainable;
|
||||
use Predictable;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
@@ -24,12 +25,11 @@ class KNearestNeighbors implements Classifier
|
||||
private $distanceMetric;
|
||||
|
||||
/**
|
||||
* @param int $k
|
||||
* @param Distance|null $distanceMetric (if null then Euclidean distance as default)
|
||||
*/
|
||||
public function __construct(int $k = 3, Distance $distanceMetric = null)
|
||||
public function __construct(int $k = 3, ?Distance $distanceMetric = null)
|
||||
{
|
||||
if (null === $distanceMetric) {
|
||||
if ($distanceMetric === null) {
|
||||
$distanceMetric = new Euclidean();
|
||||
}
|
||||
|
||||
@@ -40,17 +40,14 @@ class KNearestNeighbors implements Classifier
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $sample
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
protected function predictSample(array $sample)
|
||||
{
|
||||
$distances = $this->kNeighborsDistances($sample);
|
||||
$predictions = (array) array_combine(array_values($this->targets), array_fill(0, count($this->targets), 0));
|
||||
|
||||
$predictions = array_combine(array_values($this->targets), array_fill(0, count($this->targets), 0));
|
||||
|
||||
foreach ($distances as $index => $distance) {
|
||||
foreach (array_keys($distances) as $index) {
|
||||
++$predictions[$this->targets[$index]];
|
||||
}
|
||||
|
||||
@@ -61,13 +58,9 @@ class KNearestNeighbors implements Classifier
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $sample
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @throws \Phpml\Exception\InvalidArgumentException
|
||||
*/
|
||||
private function kNeighborsDistances(array $sample)
|
||||
private function kNeighborsDistances(array $sample): array
|
||||
{
|
||||
$distances = [];
|
||||
|
||||
|
||||
@@ -4,22 +4,24 @@ declare(strict_types=1);
|
||||
|
||||
namespace Phpml\Classification\Linear;
|
||||
|
||||
use Phpml\Exception\InvalidArgumentException;
|
||||
|
||||
class Adaline extends Perceptron
|
||||
{
|
||||
/**
|
||||
* Batch training is the default Adaline training algorithm
|
||||
*/
|
||||
const BATCH_TRAINING = 1;
|
||||
public const BATCH_TRAINING = 1;
|
||||
|
||||
/**
|
||||
* Online training: Stochastic gradient descent learning
|
||||
*/
|
||||
const ONLINE_TRAINING = 2;
|
||||
public const ONLINE_TRAINING = 2;
|
||||
|
||||
/**
|
||||
* Training type may be either 'Batch' or 'Online' learning
|
||||
*
|
||||
* @var string
|
||||
* @var string|int
|
||||
*/
|
||||
protected $trainingType;
|
||||
|
||||
@@ -32,18 +34,16 @@ 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 float $learningRate
|
||||
* @param int $maxIterations
|
||||
* @param bool $normalizeInputs
|
||||
* @param int $trainingType
|
||||
*
|
||||
* @throws \Exception
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
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])) {
|
||||
throw new \Exception("Adaline can only be trained with batch and online/stochastic gradient descent algorithm");
|
||||
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], true)) {
|
||||
throw new InvalidArgumentException('Adaline can only be trained with batch and online/stochastic gradient descent algorithm');
|
||||
}
|
||||
|
||||
$this->trainingType = $trainingType;
|
||||
@@ -54,11 +54,8 @@ class Adaline extends Perceptron
|
||||
/**
|
||||
* Adapts the weights with respect to given samples and targets
|
||||
* by use of gradient descent learning rule
|
||||
*
|
||||
* @param array $samples
|
||||
* @param array $targets
|
||||
*/
|
||||
protected function runTraining(array $samples, array $targets)
|
||||
protected function runTraining(array $samples, array $targets): void
|
||||
{
|
||||
// The cost function is the sum of squares
|
||||
$callback = function ($weights, $sample, $target) {
|
||||
@@ -73,6 +70,6 @@ class Adaline extends Perceptron
|
||||
|
||||
$isBatch = $this->trainingType == self::BATCH_TRAINING;
|
||||
|
||||
return parent::runGradientDescent($samples, $targets, $callback, $isBatch);
|
||||
parent::runGradientDescent($samples, $targets, $callback, $isBatch);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,16 +4,19 @@ declare(strict_types=1);
|
||||
|
||||
namespace Phpml\Classification\Linear;
|
||||
|
||||
use Phpml\Helper\Predictable;
|
||||
use Phpml\Helper\OneVsRest;
|
||||
use Phpml\Classification\WeightedClassifier;
|
||||
use Phpml\Classification\DecisionTree;
|
||||
use Phpml\Classification\WeightedClassifier;
|
||||
use Phpml\Exception\InvalidArgumentException;
|
||||
use Phpml\Helper\OneVsRest;
|
||||
use Phpml\Helper\Predictable;
|
||||
use Phpml\Math\Comparison;
|
||||
|
||||
class DecisionStump extends WeightedClassifier
|
||||
{
|
||||
use Predictable, OneVsRest;
|
||||
use Predictable;
|
||||
use OneVsRest;
|
||||
|
||||
const AUTO_SELECT = -1;
|
||||
public const AUTO_SELECT = -1;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
@@ -23,7 +26,7 @@ class DecisionStump extends WeightedClassifier
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $binaryLabels;
|
||||
protected $binaryLabels = [];
|
||||
|
||||
/**
|
||||
* Lowest error rate obtained while training/optimizing the model
|
||||
@@ -50,7 +53,7 @@ class DecisionStump extends WeightedClassifier
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $columnTypes;
|
||||
protected $columnTypes = [];
|
||||
|
||||
/**
|
||||
* @var int
|
||||
@@ -67,7 +70,7 @@ class DecisionStump extends WeightedClassifier
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $prob;
|
||||
protected $prob = [];
|
||||
|
||||
/**
|
||||
* A DecisionStump classifier is a one-level deep DecisionTree. It is generally
|
||||
@@ -76,22 +79,35 @@ class DecisionStump extends WeightedClassifier
|
||||
* If columnIndex is given, then the stump tries to produce a decision node
|
||||
* on this column, otherwise in cases given the value of -1, the stump itself
|
||||
* decides which column to take for the decision (Default DecisionTree behaviour)
|
||||
*
|
||||
* @param int $columnIndex
|
||||
*/
|
||||
public function __construct(int $columnIndex = self::AUTO_SELECT)
|
||||
{
|
||||
$this->givenColumnIndex = $columnIndex;
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return "IF ${this}->column ${this}->operator ${this}->value ".
|
||||
'THEN '.$this->binaryLabels[0].' '.
|
||||
'ELSE '.$this->binaryLabels[1];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $samples
|
||||
* @param array $targets
|
||||
* @param array $labels
|
||||
*
|
||||
* @throws \Exception
|
||||
* While finding best split point for a numerical valued column,
|
||||
* DecisionStump looks for equally distanced values between minimum and maximum
|
||||
* values in the column. Given <i>$count</i> value determines how many split
|
||||
* points to be probed. The more split counts, the better performance but
|
||||
* worse processing time (Default value is 10.0)
|
||||
*/
|
||||
protected function trainBinary(array $samples, array $targets, array $labels)
|
||||
public function setNumericalSplitCount(float $count): void
|
||||
{
|
||||
$this->numSplitCount = $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
protected function trainBinary(array $samples, array $targets, array $labels): void
|
||||
{
|
||||
$this->binaryLabels = $labels;
|
||||
$this->featureCount = count($samples[0]);
|
||||
@@ -103,13 +119,13 @@ class DecisionStump extends WeightedClassifier
|
||||
|
||||
// Check the size of the weights given.
|
||||
// If none given, then assign 1 as a weight to each sample
|
||||
if ($this->weights) {
|
||||
$numWeights = count($this->weights);
|
||||
if ($numWeights != count($samples)) {
|
||||
throw new \Exception("Number of sample weights does not match with number of samples");
|
||||
}
|
||||
} else {
|
||||
if (count($this->weights) === 0) {
|
||||
$this->weights = array_fill(0, count($samples), 1);
|
||||
} else {
|
||||
$numWeights = count($this->weights);
|
||||
if ($numWeights !== count($samples)) {
|
||||
throw new InvalidArgumentException('Number of sample weights does not match with number of samples');
|
||||
}
|
||||
}
|
||||
|
||||
// Determine type of each column as either "continuous" or "nominal"
|
||||
@@ -118,14 +134,17 @@ class DecisionStump extends WeightedClassifier
|
||||
// Try to find the best split in the columns of the dataset
|
||||
// by calculating error rate for each split point in each column
|
||||
$columns = range(0, count($samples[0]) - 1);
|
||||
if ($this->givenColumnIndex != self::AUTO_SELECT) {
|
||||
if ($this->givenColumnIndex !== self::AUTO_SELECT) {
|
||||
$columns = [$this->givenColumnIndex];
|
||||
}
|
||||
|
||||
$bestSplit = [
|
||||
'value' => 0, 'operator' => '',
|
||||
'prob' => [], 'column' => 0,
|
||||
'trainingErrorRate' => 1.0];
|
||||
'value' => 0,
|
||||
'operator' => '',
|
||||
'prob' => [],
|
||||
'column' => 0,
|
||||
'trainingErrorRate' => 1.0,
|
||||
];
|
||||
foreach ($columns as $col) {
|
||||
if ($this->columnTypes[$col] == DecisionTree::CONTINUOUS) {
|
||||
$split = $this->getBestNumericalSplit($samples, $targets, $col);
|
||||
@@ -144,30 +163,10 @@ class DecisionStump extends WeightedClassifier
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* While finding best split point for a numerical valued column,
|
||||
* DecisionStump looks for equally distanced values between minimum and maximum
|
||||
* values in the column. Given <i>$count</i> value determines how many split
|
||||
* points to be probed. The more split counts, the better performance but
|
||||
* worse processing time (Default value is 10.0)
|
||||
*
|
||||
* @param float $count
|
||||
*/
|
||||
public function setNumericalSplitCount(float $count)
|
||||
{
|
||||
$this->numSplitCount = $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines best split point for the given column
|
||||
*
|
||||
* @param array $samples
|
||||
* @param array $targets
|
||||
* @param int $col
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getBestNumericalSplit(array $samples, array $targets, int $col)
|
||||
protected function getBestNumericalSplit(array $samples, array $targets, int $col): array
|
||||
{
|
||||
$values = array_column($samples, $col);
|
||||
// Trying all possible points may be accomplished in two general ways:
|
||||
@@ -178,27 +177,35 @@ class DecisionStump extends WeightedClassifier
|
||||
$maxValue = max($values);
|
||||
$stepSize = ($maxValue - $minValue) / $this->numSplitCount;
|
||||
|
||||
$split = null;
|
||||
$split = [];
|
||||
|
||||
foreach (['<=', '>'] as $operator) {
|
||||
// Before trying all possible split points, let's first try
|
||||
// the average value for the cut point
|
||||
$threshold = array_sum($values) / (float) count($values);
|
||||
list($errorRate, $prob) = $this->calculateErrorRate($targets, $threshold, $operator, $values);
|
||||
if ($split == null || $errorRate < $split['trainingErrorRate']) {
|
||||
$split = ['value' => $threshold, 'operator' => $operator,
|
||||
'prob' => $prob, 'column' => $col,
|
||||
'trainingErrorRate' => $errorRate];
|
||||
[$errorRate, $prob] = $this->calculateErrorRate($targets, $threshold, $operator, $values);
|
||||
if (!isset($split['trainingErrorRate']) || $errorRate < $split['trainingErrorRate']) {
|
||||
$split = [
|
||||
'value' => $threshold,
|
||||
'operator' => $operator,
|
||||
'prob' => $prob,
|
||||
'column' => $col,
|
||||
'trainingErrorRate' => $errorRate,
|
||||
];
|
||||
}
|
||||
|
||||
// Try other possible points one by one
|
||||
for ($step = $minValue; $step <= $maxValue; $step+= $stepSize) {
|
||||
$threshold = (float)$step;
|
||||
list($errorRate, $prob) = $this->calculateErrorRate($targets, $threshold, $operator, $values);
|
||||
for ($step = $minValue; $step <= $maxValue; $step += $stepSize) {
|
||||
$threshold = (float) $step;
|
||||
[$errorRate, $prob] = $this->calculateErrorRate($targets, $threshold, $operator, $values);
|
||||
if ($errorRate < $split['trainingErrorRate']) {
|
||||
$split = ['value' => $threshold, 'operator' => $operator,
|
||||
'prob' => $prob, 'column' => $col,
|
||||
'trainingErrorRate' => $errorRate];
|
||||
$split = [
|
||||
'value' => $threshold,
|
||||
'operator' => $operator,
|
||||
'prob' => $prob,
|
||||
'column' => $col,
|
||||
'trainingErrorRate' => $errorRate,
|
||||
];
|
||||
}
|
||||
}// for
|
||||
}
|
||||
@@ -206,29 +213,25 @@ class DecisionStump extends WeightedClassifier
|
||||
return $split;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $samples
|
||||
* @param array $targets
|
||||
* @param int $col
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getBestNominalSplit(array $samples, array $targets, int $col) : array
|
||||
protected function getBestNominalSplit(array $samples, array $targets, int $col): array
|
||||
{
|
||||
$values = array_column($samples, $col);
|
||||
$valueCounts = array_count_values($values);
|
||||
$distinctVals= array_keys($valueCounts);
|
||||
$distinctVals = array_keys($valueCounts);
|
||||
|
||||
$split = null;
|
||||
$split = [];
|
||||
|
||||
foreach (['=', '!='] as $operator) {
|
||||
foreach ($distinctVals as $val) {
|
||||
list($errorRate, $prob) = $this->calculateErrorRate($targets, $val, $operator, $values);
|
||||
|
||||
if ($split == null || $split['trainingErrorRate'] < $errorRate) {
|
||||
$split = ['value' => $val, 'operator' => $operator,
|
||||
'prob' => $prob, 'column' => $col,
|
||||
'trainingErrorRate' => $errorRate];
|
||||
[$errorRate, $prob] = $this->calculateErrorRate($targets, $val, $operator, $values);
|
||||
if (!isset($split['trainingErrorRate']) || $split['trainingErrorRate'] < $errorRate) {
|
||||
$split = [
|
||||
'value' => $val,
|
||||
'operator' => $operator,
|
||||
'prob' => $prob,
|
||||
'column' => $col,
|
||||
'trainingErrorRate' => $errorRate,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -236,72 +239,42 @@ class DecisionStump extends WeightedClassifier
|
||||
return $split;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @param mixed $leftValue
|
||||
* @param string $operator
|
||||
* @param mixed $rightValue
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
protected function evaluate($leftValue, string $operator, $rightValue)
|
||||
{
|
||||
switch ($operator) {
|
||||
case '>': return $leftValue > $rightValue;
|
||||
case '>=': return $leftValue >= $rightValue;
|
||||
case '<': return $leftValue < $rightValue;
|
||||
case '<=': return $leftValue <= $rightValue;
|
||||
case '=': return $leftValue === $rightValue;
|
||||
case '!=':
|
||||
case '<>': return $leftValue !== $rightValue;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the ratio of wrong predictions based on the new threshold
|
||||
* value given as the parameter
|
||||
*
|
||||
* @param array $targets
|
||||
* @param float $threshold
|
||||
* @param string $operator
|
||||
* @param array $values
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function calculateErrorRate(array $targets, float $threshold, string $operator, array $values) : array
|
||||
protected function calculateErrorRate(array $targets, float $threshold, string $operator, array $values): array
|
||||
{
|
||||
$wrong = 0.0;
|
||||
$prob = [];
|
||||
$leftLabel = $this->binaryLabels[0];
|
||||
$rightLabel= $this->binaryLabels[1];
|
||||
$rightLabel = $this->binaryLabels[1];
|
||||
|
||||
foreach ($values as $index => $value) {
|
||||
if ($this->evaluate($value, $operator, $threshold)) {
|
||||
if (Comparison::compare($value, $threshold, $operator)) {
|
||||
$predicted = $leftLabel;
|
||||
} else {
|
||||
$predicted = $rightLabel;
|
||||
}
|
||||
|
||||
$target = $targets[$index];
|
||||
if (strval($predicted) != strval($targets[$index])) {
|
||||
if ((string) $predicted != (string) $targets[$index]) {
|
||||
$wrong += $this->weights[$index];
|
||||
}
|
||||
|
||||
if (!isset($prob[$predicted][$target])) {
|
||||
$prob[$predicted][$target] = 0;
|
||||
}
|
||||
|
||||
++$prob[$predicted][$target];
|
||||
}
|
||||
|
||||
// Calculate probabilities: Proportion of labels in each leaf
|
||||
$dist = array_combine($this->binaryLabels, array_fill(0, 2, 0.0));
|
||||
foreach ($prob as $leaf => $counts) {
|
||||
$leafTotal = (float)array_sum($prob[$leaf]);
|
||||
$leafTotal = (float) array_sum($prob[$leaf]);
|
||||
foreach ($counts as $label => $count) {
|
||||
if (strval($leaf) == strval($label)) {
|
||||
if ((string) $leaf == (string) $label) {
|
||||
$dist[$leaf] = $count / $leafTotal;
|
||||
}
|
||||
}
|
||||
@@ -316,15 +289,12 @@ class DecisionStump extends WeightedClassifier
|
||||
* Probability of a sample is calculated as the proportion of the label
|
||||
* within the labels of the training samples in the decision node
|
||||
*
|
||||
* @param array $sample
|
||||
* @param mixed $label
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
protected function predictProbability(array $sample, $label) : float
|
||||
protected function predictProbability(array $sample, $label): float
|
||||
{
|
||||
$predicted = $this->predictSampleBinary($sample);
|
||||
if (strval($predicted) == strval($label)) {
|
||||
if ((string) $predicted == (string) $label) {
|
||||
return $this->prob[$label];
|
||||
}
|
||||
|
||||
@@ -332,33 +302,18 @@ class DecisionStump extends WeightedClassifier
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $sample
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
protected function predictSampleBinary(array $sample)
|
||||
{
|
||||
if ($this->evaluate($sample[$this->column], $this->operator, $this->value)) {
|
||||
if (Comparison::compare($sample[$this->column], $this->value, $this->operator)) {
|
||||
return $this->binaryLabels[0];
|
||||
}
|
||||
|
||||
return $this->binaryLabels[1];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
protected function resetBinary()
|
||||
protected function resetBinary(): void
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return "IF $this->column $this->operator $this->value " .
|
||||
"THEN " . $this->binaryLabels[0] . " ".
|
||||
"ELSE " . $this->binaryLabels[1];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,9 @@ declare(strict_types=1);
|
||||
|
||||
namespace Phpml\Classification\Linear;
|
||||
|
||||
use Closure;
|
||||
use Exception;
|
||||
use Phpml\Exception\InvalidArgumentException;
|
||||
use Phpml\Helper\Optimizer\ConjugateGradient;
|
||||
|
||||
class LogisticRegression extends Adaline
|
||||
@@ -11,17 +14,17 @@ class LogisticRegression extends Adaline
|
||||
/**
|
||||
* Batch training: Gradient descent algorithm (default)
|
||||
*/
|
||||
const BATCH_TRAINING = 1;
|
||||
public const BATCH_TRAINING = 1;
|
||||
|
||||
/**
|
||||
* Online training: Stochastic gradient descent learning
|
||||
*/
|
||||
const ONLINE_TRAINING = 2;
|
||||
public const ONLINE_TRAINING = 2;
|
||||
|
||||
/**
|
||||
* Conjugate Batch: Conjugate Gradient algorithm
|
||||
*/
|
||||
const CONJUGATE_GRAD_TRAINING = 3;
|
||||
public const CONJUGATE_GRAD_TRAINING = 3;
|
||||
|
||||
/**
|
||||
* Cost function to optimize: 'log' and 'sse' are supported <br>
|
||||
@@ -30,7 +33,7 @@ class LogisticRegression extends Adaline
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $costFunction = 'sse';
|
||||
protected $costFunction = 'log';
|
||||
|
||||
/**
|
||||
* Regularization term: only 'L2' is supported
|
||||
@@ -59,32 +62,33 @@ class LogisticRegression extends Adaline
|
||||
*
|
||||
* Penalty (Regularization term) can be 'L2' or empty string to cancel penalty term
|
||||
*
|
||||
* @param int $maxIterations
|
||||
* @param bool $normalizeInputs
|
||||
* @param int $trainingType
|
||||
* @param string $cost
|
||||
* @param string $penalty
|
||||
*
|
||||
* @throws \Exception
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function __construct(int $maxIterations = 500, bool $normalizeInputs = true,
|
||||
int $trainingType = self::CONJUGATE_GRAD_TRAINING, string $cost = 'sse',
|
||||
string $penalty = 'L2')
|
||||
{
|
||||
public function __construct(
|
||||
int $maxIterations = 500,
|
||||
bool $normalizeInputs = true,
|
||||
int $trainingType = self::CONJUGATE_GRAD_TRAINING,
|
||||
string $cost = 'log',
|
||||
string $penalty = 'L2'
|
||||
) {
|
||||
$trainingTypes = range(self::BATCH_TRAINING, self::CONJUGATE_GRAD_TRAINING);
|
||||
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($trainingType, $trainingTypes, true)) {
|
||||
throw new InvalidArgumentException(
|
||||
'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'])) {
|
||||
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");
|
||||
if (!in_array($cost, ['log', 'sse'], true)) {
|
||||
throw new InvalidArgumentException(
|
||||
"Logistic regression cost function can be one of the following: \n".
|
||||
"'log' for log-likelihood and 'sse' for sum of squared errors"
|
||||
);
|
||||
}
|
||||
|
||||
if ($penalty != '' && strtoupper($penalty) !== 'L2') {
|
||||
throw new \Exception("Logistic regression supports only 'L2' regularization");
|
||||
if ($penalty !== '' && strtoupper($penalty) !== 'L2') {
|
||||
throw new InvalidArgumentException('Logistic regression supports only \'L2\' regularization');
|
||||
}
|
||||
|
||||
$this->learningRate = 0.001;
|
||||
@@ -99,10 +103,8 @@ class LogisticRegression extends Adaline
|
||||
/**
|
||||
* Sets the learning rate if gradient descent algorithm is
|
||||
* selected for training
|
||||
*
|
||||
* @param float $learningRate
|
||||
*/
|
||||
public function setLearningRate(float $learningRate)
|
||||
public function setLearningRate(float $learningRate): void
|
||||
{
|
||||
$this->learningRate = $learningRate;
|
||||
}
|
||||
@@ -110,10 +112,8 @@ class LogisticRegression extends Adaline
|
||||
/**
|
||||
* Lambda (λ) parameter of regularization term. If 0 is given,
|
||||
* then the regularization term is cancelled
|
||||
*
|
||||
* @param float $lambda
|
||||
*/
|
||||
public function setLambda(float $lambda)
|
||||
public function setLambda(float $lambda): void
|
||||
{
|
||||
$this->lambda = $lambda;
|
||||
}
|
||||
@@ -122,40 +122,40 @@ class LogisticRegression extends Adaline
|
||||
* Adapts the weights with respect to given samples and targets
|
||||
* by use of selected solver
|
||||
*
|
||||
* @param array $samples
|
||||
* @param array $targets
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function runTraining(array $samples, array $targets)
|
||||
protected function runTraining(array $samples, array $targets): void
|
||||
{
|
||||
$callback = $this->getCostFunction();
|
||||
|
||||
switch ($this->trainingType) {
|
||||
case self::BATCH_TRAINING:
|
||||
return $this->runGradientDescent($samples, $targets, $callback, true);
|
||||
$this->runGradientDescent($samples, $targets, $callback, true);
|
||||
|
||||
return;
|
||||
|
||||
case self::ONLINE_TRAINING:
|
||||
return $this->runGradientDescent($samples, $targets, $callback, false);
|
||||
$this->runGradientDescent($samples, $targets, $callback, false);
|
||||
|
||||
return;
|
||||
|
||||
case self::CONJUGATE_GRAD_TRAINING:
|
||||
return $this->runConjugateGradient($samples, $targets, $callback);
|
||||
$this->runConjugateGradient($samples, $targets, $callback);
|
||||
|
||||
return;
|
||||
|
||||
default:
|
||||
throw new \Exception('Logistic regression has invalid training type: %s.', $this->trainingType);
|
||||
// Not reached
|
||||
throw new Exception(sprintf('Logistic regression has invalid training type: %d.', $this->trainingType));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
protected function runConjugateGradient(array $samples, array $targets, Closure $gradientFunc): void
|
||||
{
|
||||
if (empty($this->optimizer)) {
|
||||
if ($this->optimizer === null) {
|
||||
$this->optimizer = (new ConjugateGradient($this->featureCount))
|
||||
->setMaxIterations($this->maxIterations);
|
||||
}
|
||||
@@ -167,14 +167,12 @@ class LogisticRegression extends Adaline
|
||||
/**
|
||||
* Returns the appropriate callback function for the selected cost function
|
||||
*
|
||||
* @return \Closure
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function getCostFunction()
|
||||
protected function getCostFunction(): Closure
|
||||
{
|
||||
$penalty = 0;
|
||||
if ($this->penalty == 'L2') {
|
||||
if ($this->penalty === 'L2') {
|
||||
$penalty = $this->lambda;
|
||||
}
|
||||
|
||||
@@ -190,7 +188,7 @@ class LogisticRegression extends Adaline
|
||||
* The gradient of the cost function to be used with gradient descent:
|
||||
* ∇J(x) = -(y - h(x)) = (h(x) - y)
|
||||
*/
|
||||
$callback = function ($weights, $sample, $y) use ($penalty) {
|
||||
return function ($weights, $sample, $y) use ($penalty) {
|
||||
$this->weights = $weights;
|
||||
$hX = $this->output($sample);
|
||||
|
||||
@@ -199,17 +197,18 @@ class LogisticRegression extends Adaline
|
||||
if ($hX == 1) {
|
||||
$hX = 1 - 1e-10;
|
||||
}
|
||||
|
||||
if ($hX == 0) {
|
||||
$hX = 1e-10;
|
||||
}
|
||||
|
||||
$y = $y < 0 ? 0 : 1;
|
||||
|
||||
$error = -$y * log($hX) - (1 - $y) * log(1 - $hX);
|
||||
$gradient = $hX - $y;
|
||||
|
||||
return [$error, $gradient, $penalty];
|
||||
};
|
||||
|
||||
return $callback;
|
||||
|
||||
case 'sse':
|
||||
/*
|
||||
* Sum of squared errors or least squared errors cost function:
|
||||
@@ -221,31 +220,27 @@ class LogisticRegression extends Adaline
|
||||
* The gradient of the cost function:
|
||||
* ∇J(x) = -(h(x) - y) . h(x) . (1 - h(x))
|
||||
*/
|
||||
$callback = function ($weights, $sample, $y) use ($penalty) {
|
||||
return function ($weights, $sample, $y) use ($penalty) {
|
||||
$this->weights = $weights;
|
||||
$hX = $this->output($sample);
|
||||
|
||||
$y = $y < 0 ? 0 : 1;
|
||||
|
||||
$error = ($y - $hX) ** 2;
|
||||
$gradient = -($y - $hX) * $hX * (1 - $hX);
|
||||
|
||||
return [$error, $gradient, $penalty];
|
||||
};
|
||||
|
||||
return $callback;
|
||||
|
||||
default:
|
||||
throw new \Exception(sprintf('Logistic regression has invalid cost function: %s.', $this->costFunction));
|
||||
// Not reached
|
||||
throw new Exception(sprintf('Logistic regression has invalid cost function: %s.', $this->costFunction));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the output of the network, a float value between 0.0 and 1.0
|
||||
*
|
||||
* @param array $sample
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
protected function output(array $sample)
|
||||
protected function output(array $sample): float
|
||||
{
|
||||
$sum = parent::output($sample);
|
||||
|
||||
@@ -254,16 +249,12 @@ 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)
|
||||
protected function outputClass(array $sample): int
|
||||
{
|
||||
$output = $this->output($sample);
|
||||
|
||||
if (round($output) > 0.5) {
|
||||
if ($output > 0.5) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -276,20 +267,17 @@ class LogisticRegression extends Adaline
|
||||
* The probability is simply taken as the distance of the sample
|
||||
* to the decision plane.
|
||||
*
|
||||
* @param array $sample
|
||||
* @param mixed $label
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
protected function predictProbability(array $sample, $label)
|
||||
protected function predictProbability(array $sample, $label): float
|
||||
{
|
||||
$predicted = $this->predictSampleBinary($sample);
|
||||
$sample = $this->checkNormalizedSample($sample);
|
||||
$probability = $this->output($sample);
|
||||
|
||||
if (strval($predicted) == strval($label)) {
|
||||
$sample = $this->checkNormalizedSample($sample);
|
||||
return abs($this->output($sample) - 0.5);
|
||||
if (array_search($label, $this->labels, true) > 0) {
|
||||
return $probability;
|
||||
}
|
||||
|
||||
return 0.0;
|
||||
return 1 - $probability;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,20 +4,24 @@ declare(strict_types=1);
|
||||
|
||||
namespace Phpml\Classification\Linear;
|
||||
|
||||
use Phpml\Helper\Predictable;
|
||||
use Phpml\Helper\OneVsRest;
|
||||
use Phpml\Helper\Optimizer\StochasticGD;
|
||||
use Phpml\Helper\Optimizer\GD;
|
||||
use Closure;
|
||||
use Phpml\Classification\Classifier;
|
||||
use Phpml\Preprocessing\Normalizer;
|
||||
use Phpml\Exception\InvalidArgumentException;
|
||||
use Phpml\Helper\OneVsRest;
|
||||
use Phpml\Helper\Optimizer\GD;
|
||||
use Phpml\Helper\Optimizer\Optimizer;
|
||||
use Phpml\Helper\Optimizer\StochasticGD;
|
||||
use Phpml\Helper\Predictable;
|
||||
use Phpml\IncrementalEstimator;
|
||||
use Phpml\Preprocessing\Normalizer;
|
||||
|
||||
class Perceptron implements Classifier, IncrementalEstimator
|
||||
{
|
||||
use Predictable, OneVsRest;
|
||||
use Predictable;
|
||||
use OneVsRest;
|
||||
|
||||
/**
|
||||
* @var \Phpml\Helper\Optimizer\Optimizer
|
||||
* @var Optimizer|GD|StochasticGD|null
|
||||
*/
|
||||
protected $optimizer;
|
||||
|
||||
@@ -34,7 +38,7 @@ class Perceptron implements Classifier, IncrementalEstimator
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $weights;
|
||||
protected $weights = [];
|
||||
|
||||
/**
|
||||
* @var float
|
||||
@@ -56,29 +60,23 @@ class Perceptron implements Classifier, IncrementalEstimator
|
||||
*/
|
||||
protected $enableEarlyStop = true;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $costValues = [];
|
||||
|
||||
/**
|
||||
* Initalize a perceptron classifier with given learning rate and maximum
|
||||
* number of iterations used while training the perceptron
|
||||
*
|
||||
* @param float $learningRate Value between 0.0(exclusive) and 1.0(inclusive)
|
||||
* @param int $maxIterations Must be at least 1
|
||||
* @param bool $normalizeInputs
|
||||
* @param float $learningRate Value between 0.0(exclusive) and 1.0(inclusive)
|
||||
* @param int $maxIterations Must be at least 1
|
||||
*
|
||||
* @throws \Exception
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
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)");
|
||||
throw new InvalidArgumentException('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 must be an integer greater than 0");
|
||||
throw new InvalidArgumentException('Maximum number of iterations must be an integer greater than 0');
|
||||
}
|
||||
|
||||
if ($normalizeInputs) {
|
||||
@@ -89,31 +87,24 @@ class Perceptron implements Classifier, IncrementalEstimator
|
||||
$this->maxIterations = $maxIterations;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $samples
|
||||
* @param array $targets
|
||||
* @param array $labels
|
||||
*/
|
||||
public function partialTrain(array $samples, array $targets, array $labels = [])
|
||||
public function partialTrain(array $samples, array $targets, array $labels = []): void
|
||||
{
|
||||
$this->trainByLabel($samples, $targets, $labels);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $samples
|
||||
* @param array $targets
|
||||
* @param array $labels
|
||||
*/
|
||||
public function trainBinary(array $samples, array $targets, array $labels)
|
||||
public function trainBinary(array $samples, array $targets, array $labels): void
|
||||
{
|
||||
if ($this->normalizer) {
|
||||
if ($this->normalizer !== null) {
|
||||
$this->normalizer->transform($samples);
|
||||
}
|
||||
|
||||
// Set all target values to either -1 or 1
|
||||
$this->labels = [1 => $labels[0], -1 => $labels[1]];
|
||||
$this->labels = [
|
||||
1 => $labels[0],
|
||||
-1 => $labels[1],
|
||||
];
|
||||
foreach ($targets as $key => $target) {
|
||||
$targets[$key] = strval($target) == strval($this->labels[1]) ? 1 : -1;
|
||||
$targets[$key] = (string) $target == (string) $this->labels[1] ? 1 : -1;
|
||||
}
|
||||
|
||||
// Set samples and feature count vars
|
||||
@@ -122,15 +113,6 @@ class Perceptron implements Classifier, IncrementalEstimator
|
||||
$this->runTraining($samples, $targets);
|
||||
}
|
||||
|
||||
protected function resetBinary()
|
||||
{
|
||||
$this->labels = [];
|
||||
$this->optimizer = null;
|
||||
$this->featureCount = 0;
|
||||
$this->weights = null;
|
||||
$this->costValues = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Normally enabling early stopping for the optimization procedure may
|
||||
* help saving processing time while in some cases it may result in
|
||||
@@ -139,8 +121,6 @@ class Perceptron implements Classifier, IncrementalEstimator
|
||||
* If "false" is given, the optimization procedure will always be executed
|
||||
* for $maxIterations times
|
||||
*
|
||||
* @param bool $enable
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setEarlyStop(bool $enable = true)
|
||||
@@ -152,22 +132,26 @@ class Perceptron implements Classifier, IncrementalEstimator
|
||||
|
||||
/**
|
||||
* Returns the cost values obtained during the training.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getCostValues()
|
||||
public function getCostValues(): array
|
||||
{
|
||||
return $this->costValues;
|
||||
}
|
||||
|
||||
protected function resetBinary(): void
|
||||
{
|
||||
$this->labels = [];
|
||||
$this->optimizer = null;
|
||||
$this->featureCount = 0;
|
||||
$this->weights = [];
|
||||
$this->costValues = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Trains the perceptron model with Stochastic Gradient Descent optimization
|
||||
* to get the correct set of weights
|
||||
*
|
||||
* @param array $samples
|
||||
* @param array $targets
|
||||
*/
|
||||
protected function runTraining(array $samples, array $targets)
|
||||
protected function runTraining(array $samples, array $targets): void
|
||||
{
|
||||
// The cost function is the sum of squares
|
||||
$callback = function ($weights, $sample, $target) {
|
||||
@@ -175,7 +159,7 @@ class Perceptron implements Classifier, IncrementalEstimator
|
||||
|
||||
$prediction = $this->outputClass($sample);
|
||||
$gradient = $prediction - $target;
|
||||
$error = $gradient**2;
|
||||
$error = $gradient ** 2;
|
||||
|
||||
return [$error, $gradient];
|
||||
};
|
||||
@@ -186,17 +170,12 @@ class Perceptron implements Classifier, IncrementalEstimator
|
||||
/**
|
||||
* Executes a Gradient Descent algorithm for
|
||||
* the given cost function
|
||||
*
|
||||
* @param array $samples
|
||||
* @param array $targets
|
||||
* @param \Closure $gradientFunc
|
||||
* @param bool $isBatch
|
||||
*/
|
||||
protected function runGradientDescent(array $samples, array $targets, \Closure $gradientFunc, bool $isBatch = false)
|
||||
protected function runGradientDescent(array $samples, array $targets, Closure $gradientFunc, bool $isBatch = false): void
|
||||
{
|
||||
$class = $isBatch ? GD::class : StochasticGD::class;
|
||||
|
||||
if (empty($this->optimizer)) {
|
||||
if ($this->optimizer === null) {
|
||||
$this->optimizer = (new $class($this->featureCount))
|
||||
->setLearningRate($this->learningRate)
|
||||
->setMaxIterations($this->maxIterations)
|
||||
@@ -211,14 +190,10 @@ class Perceptron implements Classifier, IncrementalEstimator
|
||||
/**
|
||||
* Checks if the sample should be normalized and if so, returns the
|
||||
* normalized sample
|
||||
*
|
||||
* @param array $sample
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function checkNormalizedSample(array $sample)
|
||||
protected function checkNormalizedSample(array $sample): array
|
||||
{
|
||||
if ($this->normalizer) {
|
||||
if ($this->normalizer !== null) {
|
||||
$samples = [$sample];
|
||||
$this->normalizer->transform($samples);
|
||||
$sample = $samples[0];
|
||||
@@ -230,8 +205,7 @@ class Perceptron implements Classifier, IncrementalEstimator
|
||||
/**
|
||||
* Calculates net output of the network as a float value for the given input
|
||||
*
|
||||
* @param array $sample
|
||||
* @return int
|
||||
* @return int|float
|
||||
*/
|
||||
protected function output(array $sample)
|
||||
{
|
||||
@@ -249,11 +223,8 @@ class Perceptron implements Classifier, IncrementalEstimator
|
||||
|
||||
/**
|
||||
* Returns the class value (either -1 or 1) for the given input
|
||||
*
|
||||
* @param array $sample
|
||||
* @return int
|
||||
*/
|
||||
protected function outputClass(array $sample)
|
||||
protected function outputClass(array $sample): int
|
||||
{
|
||||
return $this->output($sample) > 0 ? 1 : -1;
|
||||
}
|
||||
@@ -264,26 +235,22 @@ class Perceptron implements Classifier, IncrementalEstimator
|
||||
* The probability is simply taken as the distance of the sample
|
||||
* to the decision plane.
|
||||
*
|
||||
* @param array $sample
|
||||
* @param mixed $label
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
protected function predictProbability(array $sample, $label)
|
||||
protected function predictProbability(array $sample, $label): float
|
||||
{
|
||||
$predicted = $this->predictSampleBinary($sample);
|
||||
|
||||
if (strval($predicted) == strval($label)) {
|
||||
if ((string) $predicted == (string) $label) {
|
||||
$sample = $this->checkNormalizedSample($sample);
|
||||
return abs($this->output($sample));
|
||||
|
||||
return (float) abs($this->output($sample));
|
||||
}
|
||||
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $sample
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
protected function predictSampleBinary(array $sample)
|
||||
|
||||
@@ -9,22 +9,23 @@ use Phpml\NeuralNetwork\Network\MultilayerPerceptron;
|
||||
|
||||
class MLPClassifier extends MultilayerPerceptron implements Classifier
|
||||
{
|
||||
|
||||
/**
|
||||
* @param mixed $target
|
||||
* @return int
|
||||
* @param mixed $target
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function getTargetClass($target): int
|
||||
{
|
||||
if (!in_array($target, $this->classes)) {
|
||||
throw InvalidArgumentException::invalidTarget($target);
|
||||
if (!in_array($target, $this->classes, true)) {
|
||||
throw new InvalidArgumentException(
|
||||
sprintf('Target with value "%s" is not part of the accepted classes', $target)
|
||||
);
|
||||
}
|
||||
return array_search($target, $this->classes);
|
||||
|
||||
return array_search($target, $this->classes, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $sample
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
protected function predictSample(array $sample)
|
||||
@@ -39,18 +40,17 @@ class MLPClassifier extends MultilayerPerceptron implements Classifier
|
||||
$max = $value;
|
||||
}
|
||||
}
|
||||
return $this->classes[$predictedClass];
|
||||
|
||||
return $predictedClass;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $sample
|
||||
* @param mixed $target
|
||||
*/
|
||||
protected function trainSample(array $sample, $target)
|
||||
protected function trainSample(array $sample, $target): void
|
||||
{
|
||||
|
||||
// Feed-forward.
|
||||
$this->setInput($sample)->getOutput();
|
||||
$this->setInput($sample);
|
||||
|
||||
// Back-propagate.
|
||||
$this->backpropagation->backpropagate($this->getLayers(), $this->getTargetClass($target));
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Phpml\Classification;
|
||||
|
||||
use Phpml\Exception\InvalidArgumentException;
|
||||
use Phpml\Helper\Predictable;
|
||||
use Phpml\Helper\Trainable;
|
||||
use Phpml\Math\Statistic\Mean;
|
||||
@@ -11,11 +12,14 @@ use Phpml\Math\Statistic\StandardDeviation;
|
||||
|
||||
class NaiveBayes implements Classifier
|
||||
{
|
||||
use Trainable, Predictable;
|
||||
use Trainable;
|
||||
use Predictable;
|
||||
|
||||
const CONTINUOS = 1;
|
||||
const NOMINAL = 2;
|
||||
const EPSILON = 1e-10;
|
||||
public const CONTINUOS = 1;
|
||||
|
||||
public const NOMINAL = 2;
|
||||
|
||||
public const EPSILON = 1e-10;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
@@ -25,7 +29,7 @@ class NaiveBayes implements Classifier
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $mean= [];
|
||||
private $mean = [];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
@@ -57,19 +61,14 @@ class NaiveBayes implements Classifier
|
||||
*/
|
||||
private $labels = [];
|
||||
|
||||
/**
|
||||
* @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);
|
||||
$this->sampleCount = count($this->samples);
|
||||
$this->featureCount = count($this->samples[0]);
|
||||
|
||||
$labelCounts = array_count_values($this->targets);
|
||||
$this->labels = array_keys($labelCounts);
|
||||
$this->labels = array_map('strval', array_flip(array_flip($this->targets)));
|
||||
foreach ($this->labels as $label) {
|
||||
$samples = $this->getSamplesByLabel($label);
|
||||
$this->p[$label] = count($samples) / $this->sampleCount;
|
||||
@@ -77,16 +76,39 @@ class NaiveBayes implements Classifier
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
protected function predictSample(array $sample)
|
||||
{
|
||||
// Use NaiveBayes assumption for each label using:
|
||||
// P(label|features) = P(label) * P(feature0|label) * P(feature1|label) .... P(featureN|label)
|
||||
// Then compare probability for each class to determine which label is most likely
|
||||
$predictions = [];
|
||||
foreach ($this->labels as $label) {
|
||||
$p = $this->p[$label];
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates vital statistics for each label & feature. Stores these
|
||||
* values in private array in order to avoid repeated calculation
|
||||
* @param string $label
|
||||
* @param array $samples
|
||||
*/
|
||||
private function calculateStatistics($label, $samples)
|
||||
private function calculateStatistics(string $label, array $samples): void
|
||||
{
|
||||
$this->std[$label] = array_fill(0, $this->featureCount, 0);
|
||||
$this->mean[$label]= array_fill(0, $this->featureCount, 0);
|
||||
$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) {
|
||||
@@ -113,25 +135,25 @@ class NaiveBayes implements Classifier
|
||||
|
||||
/**
|
||||
* Calculates the probability P(label|sample_n)
|
||||
*
|
||||
* @param array $sample
|
||||
* @param int $feature
|
||||
* @param string $label
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
private function sampleProbability($sample, $feature, $label)
|
||||
private function sampleProbability(array $sample, int $feature, string $label): float
|
||||
{
|
||||
if (!isset($sample[$feature])) {
|
||||
throw new InvalidArgumentException('Missing feature. All samples must have equal number of features');
|
||||
}
|
||||
|
||||
$value = $sample[$feature];
|
||||
if ($this->dataType[$label][$feature] == self::NOMINAL) {
|
||||
if (!isset($this->discreteProb[$label][$feature][$value]) ||
|
||||
$this->discreteProb[$label][$feature][$value] == 0) {
|
||||
return self::EPSILON;
|
||||
}
|
||||
|
||||
return $this->discreteProb[$label][$feature][$value];
|
||||
}
|
||||
$std = $this->std[$label][$feature] ;
|
||||
$mean= $this->mean[$label][$feature];
|
||||
|
||||
$std = $this->std[$label][$feature];
|
||||
$mean = $this->mean[$label][$feature];
|
||||
// Calculate the probability density by use of normal/Gaussian distribution
|
||||
// Ref: https://en.wikipedia.org/wiki/Normal_distribution
|
||||
//
|
||||
@@ -139,19 +161,16 @@ class NaiveBayes implements Classifier
|
||||
// some libraries adopt taking log of calculations such as
|
||||
// scikit-learn did.
|
||||
// (See : https://github.com/scikit-learn/scikit-learn/blob/master/sklearn/naive_bayes.py)
|
||||
$pdf = -0.5 * log(2.0 * pi() * $std * $std);
|
||||
$pdf -= 0.5 * pow($value - $mean, 2) / ($std * $std);
|
||||
$pdf = -0.5 * log(2.0 * M_PI * $std * $std);
|
||||
$pdf -= 0.5 * (($value - $mean) ** 2) / ($std * $std);
|
||||
|
||||
return $pdf;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return samples belonging to specific label
|
||||
*
|
||||
* @param string $label
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function getSamplesByLabel($label)
|
||||
private function getSamplesByLabel(string $label): array
|
||||
{
|
||||
$samples = [];
|
||||
for ($i = 0; $i < $this->sampleCount; ++$i) {
|
||||
@@ -159,30 +178,7 @@ class NaiveBayes implements Classifier
|
||||
$samples[] = $this->samples[$i];
|
||||
}
|
||||
}
|
||||
|
||||
return $samples;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $sample
|
||||
* @return mixed
|
||||
*/
|
||||
protected function predictSample(array $sample)
|
||||
{
|
||||
// Use NaiveBayes assumption for each label using:
|
||||
// P(label|features) = P(label) * P(feature0|label) * P(feature1|label) .... P(featureN|label)
|
||||
// Then compare probability for each class to determine which label is most likely
|
||||
$predictions = [];
|
||||
foreach ($this->labels as $label) {
|
||||
$p = $this->p[$label];
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,20 +10,15 @@ use Phpml\SupportVectorMachine\Type;
|
||||
|
||||
class SVC extends SupportVectorMachine implements Classifier
|
||||
{
|
||||
/**
|
||||
* @param int $kernel
|
||||
* @param float $cost
|
||||
* @param int $degree
|
||||
* @param float|null $gamma
|
||||
* @param float $coef0
|
||||
* @param float $tolerance
|
||||
* @param int $cacheSize
|
||||
* @param bool $shrinking
|
||||
* @param bool $probabilityEstimates
|
||||
*/
|
||||
public function __construct(
|
||||
int $kernel = Kernel::LINEAR, float $cost = 1.0, int $degree = 3, float $gamma = null, float $coef0 = 0.0,
|
||||
float $tolerance = 0.001, int $cacheSize = 100, bool $shrinking = true,
|
||||
int $kernel = Kernel::RBF,
|
||||
float $cost = 1.0,
|
||||
int $degree = 3,
|
||||
?float $gamma = null,
|
||||
float $coef0 = 0.0,
|
||||
float $tolerance = 0.001,
|
||||
int $cacheSize = 100,
|
||||
bool $shrinking = true,
|
||||
bool $probabilityEstimates = false
|
||||
) {
|
||||
parent::__construct(Type::C_SVC, $kernel, $cost, 0.5, $degree, $gamma, $coef0, 0.1, $tolerance, $cacheSize, $shrinking, $probabilityEstimates);
|
||||
|
||||
@@ -9,14 +9,12 @@ abstract class WeightedClassifier implements Classifier
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $weights;
|
||||
protected $weights = [];
|
||||
|
||||
/**
|
||||
* Sets the array including a weight for each sample
|
||||
*
|
||||
* @param array $weights
|
||||
*/
|
||||
public function setSampleWeights(array $weights)
|
||||
public function setSampleWeights(array $weights): void
|
||||
{
|
||||
$this->weights = $weights;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user