Added EasyCodingStandard + lots of code fixes (#156)

* travis: move coveralls here, decouple from package

* composer: use PSR4

* phpunit: simpler config

* travis: add ecs run

* composer: add ecs dev

* use standard vendor/bin directory for dependency bins, confuses with local bins and require gitignore handling

* ecs: add PSR2

* [cs] PSR2 spacing fixes

* [cs] PSR2 class name fix

* [cs] PHP7 fixes - return semicolon spaces, old rand functions, typehints

* [cs] fix less strict typehints

* fix typehints to make tests pass

* ecs: ignore typehint-less elements

* [cs] standardize arrays

* [cs] standardize docblock, remove unused comments

* [cs] use self where possible

* [cs] sort class elements, from public to private

* [cs] do not use yoda (found less yoda-cases, than non-yoda)

* space

* [cs] do not assign in condition

* [cs] use namespace imports if possible

* [cs] use ::class over strings

* [cs] fix defaults for arrays properties, properties and constants single spacing

* cleanup ecs comments

* [cs] use item per line in multi-items array

* missing line

* misc

* rebase
This commit is contained in:
Tomáš Votruba 2017-11-22 22:16:10 +01:00 committed by Arkadiusz Kondas
parent b1d40bfa30
commit 726cf4cddf
139 changed files with 3080 additions and 1514 deletions

4
.gitignore vendored
View File

@ -1,8 +1,4 @@
/vendor/ /vendor/
humbuglog.* humbuglog.*
/bin/phpunit
.coverage
.php_cs.cache .php_cs.cache
/bin/php-cs-fixer
/bin/coveralls
/build /build

View File

@ -6,7 +6,7 @@ matrix:
include: include:
- os: linux - os: linux
php: '7.1' php: '7.1'
env: DISABLE_XDEBUG="true" env: DISABLE_XDEBUG="true" STATIC_ANALYSIS="true"
- os: linux - os: linux
php: '7.2' php: '7.2'
@ -21,7 +21,7 @@ matrix:
before_install: before_install:
- if [[ "${TRAVIS_OS_NAME}" == "osx" ]]; then /usr/bin/env bash bin/prepare_osx_env.sh ; fi - if [[ "${TRAVIS_OS_NAME}" == "osx" ]]; then /usr/bin/env bash bin/prepare_osx_env.sh ; fi
- if [[ DISABLE_XDEBUG == "true" ]]; then phpenv config-rm xdebug.ini; fi - if [[ $DISABLE_XDEBUG == "true" ]]; then phpenv config-rm xdebug.ini; fi
install: install:
- if [[ "${TRAVIS_OS_NAME}" == "osx" ]]; then /usr/bin/env bash bin/handle_brew_pkg.sh "${_PHP}" ; fi - if [[ "${TRAVIS_OS_NAME}" == "osx" ]]; then /usr/bin/env bash bin/handle_brew_pkg.sh "${_PHP}" ; fi
@ -29,10 +29,12 @@ install:
- php composer.phar install --dev --no-interaction --ignore-platform-reqs - php composer.phar install --dev --no-interaction --ignore-platform-reqs
script: script:
- bin/phpunit $PHPUNIT_FLAGS - vendor/bin/phpunit $PHPUNIT_FLAGS
- if [[ $STATIC_ANALYSIS != "" ]]; then vendor/bin/ecs check src tests; fi
after_success: after_success:
- | - |
if [[ $PHPUNIT_FLAGS != "" ]]; then if [[ $PHPUNIT_FLAGS != "" ]]; then
php bin/coveralls -v wget https://github.com/satooshi/php-coveralls/releases/download/v1.0.1/coveralls.phar;
php coveralls.phar --verbose;
fi fi

View File

@ -12,8 +12,8 @@
} }
], ],
"autoload": { "autoload": {
"psr-0": { "psr-4": {
"Phpml": "src/" "Phpml\\": "src/Phpml"
} }
}, },
"require": { "require": {
@ -22,9 +22,8 @@
"require-dev": { "require-dev": {
"phpunit/phpunit": "^6.0", "phpunit/phpunit": "^6.0",
"friendsofphp/php-cs-fixer": "^2.4", "friendsofphp/php-cs-fixer": "^2.4",
"php-coveralls/php-coveralls": "^1.0" "symplify/easy-coding-standard": "dev-master as 2.5",
}, "symplify/coding-standard": "dev-master as 2.5",
"config": { "symplify/package-builder": "dev-master#3604bea as 2.5"
"bin-dir": "bin"
} }
} }

1968
composer.lock generated

File diff suppressed because it is too large Load Diff

39
easy-coding-standard.neon Normal file
View File

@ -0,0 +1,39 @@
includes:
- vendor/symplify/easy-coding-standard/config/psr2.neon
- vendor/symplify/easy-coding-standard/config/php70.neon
- vendor/symplify/easy-coding-standard/config/clean-code.neon
- vendor/symplify/easy-coding-standard/config/common/array.neon
- vendor/symplify/easy-coding-standard/config/common/docblock.neon
- vendor/symplify/easy-coding-standard/config/common/namespaces.neon
- vendor/symplify/easy-coding-standard/config/common/control-structures.neon
# many errors, need help
#- vendor/symplify/easy-coding-standard/config/common/strict.neon
checkers:
- Symplify\CodingStandard\Fixer\Import\ImportNamespacedNameFixer
- Symplify\CodingStandard\Fixer\Php\ClassStringToClassConstantFixer
- Symplify\CodingStandard\Fixer\Property\ArrayPropertyDefaultValueFixer
- Symplify\CodingStandard\Fixer\ClassNotation\PropertyAndConstantSeparationFixer
- Symplify\CodingStandard\Fixer\ArrayNotation\StandaloneLineInMultilineArrayFixer
parameters:
exclude_checkers:
# from strict.neon
- PhpCsFixer\Fixer\PhpUnit\PhpUnitStrictFixer
skip:
PhpCsFixer\Fixer\Alias\RandomApiMigrationFixer:
# random_int() breaks code
- src/Phpml/CrossValidation/RandomSplit.php
SlevomatCodingStandard\Sniffs\Classes\UnusedPrivateElementsSniff:
# magic calls
- src/Phpml/Preprocessing/Normalizer.php
skip_codes:
# missing typehints
- SlevomatCodingStandard\Sniffs\TypeHints\TypeHintDeclarationSniff.MissingParameterTypeHint
- SlevomatCodingStandard\Sniffs\TypeHints\TypeHintDeclarationSniff.MissingTraversableParameterTypeHintSpecification
- SlevomatCodingStandard\Sniffs\TypeHints\TypeHintDeclarationSniff.MissingReturnTypeHint
- SlevomatCodingStandard\Sniffs\TypeHints\TypeHintDeclarationSniff.MissingTraversableReturnTypeHintSpecification
- SlevomatCodingStandard\Sniffs\TypeHints\TypeHintDeclarationSniff.MissingPropertyTypeHint
- SlevomatCodingStandard\Sniffs\TypeHints\TypeHintDeclarationSniff.MissingTraversablePropertyTypeHintSpecification

View File

@ -6,11 +6,9 @@
beStrictAboutTestSize="true" beStrictAboutTestSize="true"
beStrictAboutChangesToGlobalState="true" beStrictAboutChangesToGlobalState="true"
> >
<testsuites>
<testsuite name="PHP-ML Test Suite"> <testsuite name="PHP-ML Test Suite">
<directory>tests/*</directory> <directory>tests/*</directory>
</testsuite> </testsuite>
</testsuites>
<filter> <filter>
<whitelist processUncoveredFilesFromWhitelist="true"> <whitelist processUncoveredFilesFromWhitelist="true">

View File

@ -31,7 +31,7 @@ class Apriori implements Associator
* *
* @var mixed[][][] * @var mixed[][][]
*/ */
private $large; private $large = [];
/** /**
* Minimum relative frequency of transactions. * Minimum relative frequency of transactions.
@ -45,7 +45,7 @@ class Apriori implements Associator
* *
* @var mixed[][] * @var mixed[][]
*/ */
private $rules; private $rules = [];
/** /**
* Apriori constructor. * Apriori constructor.
@ -133,7 +133,8 @@ class Apriori implements Associator
private function generateRules(array $frequent): void private function generateRules(array $frequent): void
{ {
foreach ($this->antecedents($frequent) as $antecedent) { foreach ($this->antecedents($frequent) as $antecedent) {
if ($this->confidence <= ($confidence = $this->confidence($frequent, $antecedent))) { $confidence = $this->confidence($frequent, $antecedent);
if ($this->confidence <= $confidence) {
$consequent = array_values(array_diff($frequent, $antecedent)); $consequent = array_values(array_diff($frequent, $antecedent));
$this->rules[] = [ $this->rules[] = [
self::ARRAY_KEY_ANTECEDENT => $antecedent, self::ARRAY_KEY_ANTECEDENT => $antecedent,

View File

@ -15,22 +15,18 @@ class DecisionTree implements Classifier
use Trainable, Predictable; use Trainable, Predictable;
public const CONTINUOUS = 1; public const CONTINUOUS = 1;
public const NOMINAL = 2; public const NOMINAL = 2;
/**
* @var array
*/
protected $columnTypes;
/**
* @var array
*/
private $labels = [];
/** /**
* @var int * @var int
*/ */
private $featureCount = 0; public $actualDepth = 0;
/**
* @var array
*/
protected $columnTypes = [];
/** /**
* @var DecisionTreeLeaf * @var DecisionTreeLeaf
@ -42,10 +38,15 @@ class DecisionTree implements Classifier
*/ */
protected $maxDepth; protected $maxDepth;
/**
* @var array
*/
private $labels = [];
/** /**
* @var int * @var int
*/ */
public $actualDepth = 0; private $featureCount = 0;
/** /**
* @var int * @var int
@ -55,7 +56,7 @@ class DecisionTree implements Classifier
/** /**
* @var array * @var array
*/ */
private $selectedFeatures; private $selectedFeatures = [];
/** /**
* @var array * @var array
@ -113,6 +114,121 @@ class DecisionTree implements Classifier
return $types; return $types;
} }
/**
* @param mixed $baseValue
*/
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] / (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) {
foreach ($this->featureImportances as &$importance) {
$importance /= $total;
}
arsort($this->featureImportances);
}
return $this->featureImportances;
}
protected function getSplitLeaf(array $records, int $depth = 0): DecisionTreeLeaf protected function getSplitLeaf(array $records, int $depth = 0): DecisionTreeLeaf
{ {
$split = $this->getBestSplit($records); $split = $this->getBestSplit($records);
@ -136,6 +252,7 @@ class DecisionTree implements Classifier
if ($prevRecord && $prevRecord != $record) { if ($prevRecord && $prevRecord != $record) {
$allSame = false; $allSame = false;
} }
$prevRecord = $record; $prevRecord = $record;
// According to the split criteron, this record will // According to the split criteron, this record will
@ -163,6 +280,7 @@ class DecisionTree implements Classifier
if ($leftRecords) { if ($leftRecords) {
$split->leftLeaf = $this->getSplitLeaf($leftRecords, $depth + 1); $split->leftLeaf = $this->getSplitLeaf($leftRecords, $depth + 1);
} }
if ($rightRecords) { if ($rightRecords) {
$split->rightLeaf = $this->getSplitLeaf($rightRecords, $depth + 1); $split->rightLeaf = $this->getSplitLeaf($rightRecords, $depth + 1);
} }
@ -184,6 +302,7 @@ class DecisionTree implements Classifier
foreach ($samples as $index => $row) { foreach ($samples as $index => $row) {
$colValues[$index] = $row[$i]; $colValues[$index] = $row[$i];
} }
$counts = array_count_values($colValues); $counts = array_count_values($colValues);
arsort($counts); arsort($counts);
$baseValue = key($counts); $baseValue = key($counts);
@ -242,6 +361,7 @@ class DecisionTree implements Classifier
if ($numFeatures > $this->featureCount) { if ($numFeatures > $this->featureCount) {
$numFeatures = $this->featureCount; $numFeatures = $this->featureCount;
} }
shuffle($allFeatures); shuffle($allFeatures);
$selectedFeatures = array_slice($allFeatures, 0, $numFeatures, false); $selectedFeatures = array_slice($allFeatures, 0, $numFeatures, false);
sort($selectedFeatures); sort($selectedFeatures);
@ -249,38 +369,6 @@ class DecisionTree implements Classifier
return $selectedFeatures; return $selectedFeatures;
} }
/**
* @param mixed $baseValue
*/
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] / (float) $sum, 2);
}
}
$giniParts[$i] = (1 - $part) * $sum;
}
return array_sum($giniParts) / count($colValues);
}
protected function preprocess(array $samples): array protected function preprocess(array $samples): array
{ {
// Detect and convert continuous data column values into // Detect and convert continuous data column values into
@ -298,8 +386,10 @@ class DecisionTree implements Classifier
} }
} }
} }
$columns[] = $values; $columns[] = $values;
} }
// Below method is a strange yet very simple & efficient method // Below method is a strange yet very simple & efficient method
// to get the transpose of a 2D array // to get the transpose of a 2D array
return array_map(null, ...$columns); return array_map(null, ...$columns);
@ -329,28 +419,6 @@ class DecisionTree implements Classifier
return count($distinctValues) <= $count / 5; 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.
*
* @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 * Used to set predefined features to consider while deciding which column to use for a split
*/ */
@ -359,66 +427,6 @@ class DecisionTree implements Classifier
$this->selectedFeatures = $selectedFeatures; $this->selectedFeatures = $selectedFeatures;
} }
/**
* 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) {
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 * Collects and returns an array of internal nodes that use the given
* column as a split criterion * column as a split criterion

View File

@ -71,6 +71,14 @@ class DecisionTreeLeaf
*/ */
public $level = 0; public $level = 0;
/**
* HTML representation of the tree without column names
*/
public function __toString(): string
{
return $this->getHTML();
}
public function evaluate(array $record): bool public function evaluate(array $record): bool
{ {
$recordField = $record[$this->columnIndex]; $recordField = $record[$this->columnIndex];
@ -154,12 +162,4 @@ class DecisionTreeLeaf
return $str; return $str;
} }
/**
* HTML representation of the tree without column names
*/
public function __toString() : string
{
return $this->getHTML();
}
} }

View File

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Phpml\Classification\Ensemble; namespace Phpml\Classification\Ensemble;
use Exception;
use Phpml\Classification\Classifier; use Phpml\Classification\Classifier;
use Phpml\Classification\Linear\DecisionStump; use Phpml\Classification\Linear\DecisionStump;
use Phpml\Classification\WeightedClassifier; use Phpml\Classification\WeightedClassifier;
@ -11,6 +12,7 @@ use Phpml\Helper\Predictable;
use Phpml\Helper\Trainable; use Phpml\Helper\Trainable;
use Phpml\Math\Statistic\Mean; use Phpml\Math\Statistic\Mean;
use Phpml\Math\Statistic\StandardDeviation; use Phpml\Math\Statistic\StandardDeviation;
use ReflectionClass;
class AdaBoost implements Classifier class AdaBoost implements Classifier
{ {
@ -98,11 +100,14 @@ class AdaBoost implements Classifier
// Initialize usual variables // Initialize usual variables
$this->labels = array_keys(array_count_values($targets)); $this->labels = array_keys(array_count_values($targets));
if (count($this->labels) != 2) { if (count($this->labels) != 2) {
throw new \Exception('AdaBoost is a binary classifier and can classify between two classes only'); throw new Exception('AdaBoost is a binary classifier and can classify between two classes only');
} }
// Set all target values to either -1 or 1 // 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) { foreach ($targets as $target) {
$this->targets[] = $target == $this->labels[1] ? 1 : -1; $this->targets[] = $target == $this->labels[1] ? 1 : -1;
} }
@ -132,13 +137,27 @@ class AdaBoost implements Classifier
} }
} }
/**
* @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];
}
/** /**
* Returns the classifier with the lowest error rate with the * Returns the classifier with the lowest error rate with the
* consideration of current sample weights * consideration of current sample weights
*/ */
protected function getBestClassifier(): Classifier protected function getBestClassifier(): Classifier
{ {
$ref = new \ReflectionClass($this->baseClassifier); $ref = new ReflectionClass($this->baseClassifier);
if ($this->classifierOptions) { if ($this->classifierOptions) {
$classifier = $ref->newInstanceArgs($this->classifierOptions); $classifier = $ref->newInstanceArgs($this->classifierOptions);
} else { } else {
@ -173,9 +192,10 @@ class AdaBoost implements Classifier
foreach ($weights as $index => $weight) { 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) { for ($i = 0; $i < $z; ++$i) {
if (rand(0, 1) == 0) { if (random_int(0, 1) == 0) {
continue; continue;
} }
$samples[] = $this->samples[$index]; $samples[] = $this->samples[$index];
$targets[] = $this->targets[$index]; $targets[] = $this->targets[$index];
} }
@ -231,18 +251,4 @@ class AdaBoost implements Classifier
$this->weights = $weightsT1; $this->weights = $weightsT1;
} }
/**
* @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];
}
} }

View File

@ -4,10 +4,12 @@ declare(strict_types=1);
namespace Phpml\Classification\Ensemble; namespace Phpml\Classification\Ensemble;
use Exception;
use Phpml\Classification\Classifier; use Phpml\Classification\Classifier;
use Phpml\Classification\DecisionTree; use Phpml\Classification\DecisionTree;
use Phpml\Helper\Predictable; use Phpml\Helper\Predictable;
use Phpml\Helper\Trainable; use Phpml\Helper\Trainable;
use ReflectionClass;
class Bagging implements Classifier class Bagging implements Classifier
{ {
@ -18,11 +20,6 @@ class Bagging implements Classifier
*/ */
protected $numSamples; protected $numSamples;
/**
* @var array
*/
private $targets = [];
/** /**
* @var int * @var int
*/ */
@ -46,13 +43,18 @@ class Bagging implements Classifier
/** /**
* @var array * @var array
*/ */
protected $classifiers; protected $classifiers = [];
/** /**
* @var float * @var float
*/ */
protected $subsetRatio = 0.7; protected $subsetRatio = 0.7;
/**
* @var array
*/
private $targets = [];
/** /**
* @var array * @var array
*/ */
@ -80,7 +82,7 @@ class Bagging implements Classifier
public function setSubsetRatio(float $ratio) public function setSubsetRatio(float $ratio)
{ {
if ($ratio < 0.1 || $ratio > 1.0) { if ($ratio < 0.1 || $ratio > 1.0) {
throw new \Exception('Subset ratio should be between 0.1 and 1.0'); throw new Exception('Subset ratio should be between 0.1 and 1.0');
} }
$this->subsetRatio = $ratio; $this->subsetRatio = $ratio;
@ -130,7 +132,7 @@ class Bagging implements Classifier
srand($index); srand($index);
$bootstrapSize = $this->subsetRatio * $this->numSamples; $bootstrapSize = $this->subsetRatio * $this->numSamples;
for ($i = 0; $i < $bootstrapSize; ++$i) { for ($i = 0; $i < $bootstrapSize; ++$i) {
$rand = rand(0, $this->numSamples - 1); $rand = random_int(0, $this->numSamples - 1);
$samples[] = $this->samples[$rand]; $samples[] = $this->samples[$rand];
$targets[] = $this->targets[$rand]; $targets[] = $this->targets[$rand];
} }
@ -142,7 +144,7 @@ class Bagging implements Classifier
{ {
$classifiers = []; $classifiers = [];
for ($i = 0; $i < $this->numClassifier; ++$i) { for ($i = 0; $i < $this->numClassifier; ++$i) {
$ref = new \ReflectionClass($this->classifier); $ref = new ReflectionClass($this->classifier);
if ($this->classifierOptions) { if ($this->classifierOptions) {
$obj = $ref->newInstanceArgs($this->classifierOptions); $obj = $ref->newInstanceArgs($this->classifierOptions);
} else { } else {
@ -155,12 +157,7 @@ class Bagging implements Classifier
return $classifiers; return $classifiers;
} }
/** protected function initSingleClassifier(Classifier $classifier): Classifier
* @param Classifier $classifier
*
* @return Classifier
*/
protected function initSingleClassifier($classifier)
{ {
return $classifier; return $classifier;
} }

View File

@ -4,6 +4,8 @@ declare(strict_types=1);
namespace Phpml\Classification\Ensemble; namespace Phpml\Classification\Ensemble;
use Exception;
use Phpml\Classification\Classifier;
use Phpml\Classification\DecisionTree; use Phpml\Classification\DecisionTree;
class RandomForest extends Bagging class RandomForest extends Bagging
@ -48,11 +50,11 @@ class RandomForest extends Bagging
public function setFeatureSubsetRatio($ratio) public function setFeatureSubsetRatio($ratio)
{ {
if (is_float($ratio) && ($ratio < 0.1 || $ratio > 1.0)) { 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'); throw new Exception('When a float given, feature subset ratio should be between 0.1 and 1.0');
} }
if (is_string($ratio) && $ratio != 'sqrt' && $ratio != 'log') { if (is_string($ratio) && $ratio != 'sqrt' && $ratio != 'log') {
throw new \Exception("When a string given, feature subset ratio can only be 'sqrt' or 'log' "); throw new Exception("When a string given, feature subset ratio can only be 'sqrt' or 'log' ");
} }
$this->featureSubsetRatio = $ratio; $this->featureSubsetRatio = $ratio;
@ -70,7 +72,7 @@ class RandomForest extends Bagging
public function setClassifer(string $classifier, array $classifierOptions = []) public function setClassifer(string $classifier, array $classifierOptions = [])
{ {
if ($classifier != DecisionTree::class) { if ($classifier != DecisionTree::class) {
throw new \Exception('RandomForest can only use DecisionTree as base classifier'); throw new Exception('RandomForest can only use DecisionTree as base classifier');
} }
return parent::setClassifer($classifier, $classifierOptions); return parent::setClassifer($classifier, $classifierOptions);
@ -127,7 +129,7 @@ class RandomForest extends Bagging
* *
* @return DecisionTree * @return DecisionTree
*/ */
protected function initSingleClassifier($classifier) protected function initSingleClassifier(Classifier $classifier): Classifier
{ {
if (is_float($this->featureSubsetRatio)) { if (is_float($this->featureSubsetRatio)) {
$featureCount = (int) ($this->featureSubsetRatio * $this->featureCount); $featureCount = (int) ($this->featureSubsetRatio * $this->featureCount);

View File

@ -28,7 +28,7 @@ class KNearestNeighbors implements Classifier
*/ */
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(); $distanceMetric = new Euclidean();
} }

View File

@ -4,6 +4,8 @@ declare(strict_types=1);
namespace Phpml\Classification\Linear; namespace Phpml\Classification\Linear;
use Exception;
class Adaline extends Perceptron class Adaline extends Perceptron
{ {
/** /**
@ -41,7 +43,7 @@ class Adaline extends Perceptron
int $trainingType = self::BATCH_TRAINING int $trainingType = self::BATCH_TRAINING
) { ) {
if (!in_array($trainingType, [self::BATCH_TRAINING, self::ONLINE_TRAINING])) { if (!in_array($trainingType, [self::BATCH_TRAINING, self::ONLINE_TRAINING])) {
throw new \Exception('Adaline can only be trained with batch and online/stochastic gradient descent algorithm'); throw new Exception('Adaline can only be trained with batch and online/stochastic gradient descent algorithm');
} }
$this->trainingType = $trainingType; $this->trainingType = $trainingType;

View File

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Phpml\Classification\Linear; namespace Phpml\Classification\Linear;
use Exception;
use Phpml\Classification\DecisionTree; use Phpml\Classification\DecisionTree;
use Phpml\Classification\WeightedClassifier; use Phpml\Classification\WeightedClassifier;
use Phpml\Helper\OneVsRest; use Phpml\Helper\OneVsRest;
@ -24,7 +25,7 @@ class DecisionStump extends WeightedClassifier
/** /**
* @var array * @var array
*/ */
protected $binaryLabels; protected $binaryLabels = [];
/** /**
* Lowest error rate obtained while training/optimizing the model * Lowest error rate obtained while training/optimizing the model
@ -51,7 +52,7 @@ class DecisionStump extends WeightedClassifier
/** /**
* @var array * @var array
*/ */
protected $columnTypes; protected $columnTypes = [];
/** /**
* @var int * @var int
@ -68,7 +69,7 @@ class DecisionStump extends WeightedClassifier
* *
* @var array * @var array
*/ */
protected $prob; protected $prob = [];
/** /**
* A DecisionStump classifier is a one-level deep DecisionTree. It is generally * A DecisionStump classifier is a one-level deep DecisionTree. It is generally
@ -83,6 +84,25 @@ class DecisionStump extends WeightedClassifier
$this->givenColumnIndex = $columnIndex; $this->givenColumnIndex = $columnIndex;
} }
public function __toString(): string
{
return "IF $this->column $this->operator $this->value ".
'THEN '.$this->binaryLabels[0].' '.
'ELSE '.$this->binaryLabels[1];
}
/**
* 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)
*/
public function setNumericalSplitCount(float $count): void
{
$this->numSplitCount = $count;
}
/** /**
* @throws \Exception * @throws \Exception
*/ */
@ -101,7 +121,7 @@ class DecisionStump extends WeightedClassifier
if ($this->weights) { if ($this->weights) {
$numWeights = count($this->weights); $numWeights = count($this->weights);
if ($numWeights != count($samples)) { if ($numWeights != count($samples)) {
throw new \Exception('Number of sample weights does not match with number of samples'); throw new Exception('Number of sample weights does not match with number of samples');
} }
} else { } else {
$this->weights = array_fill(0, count($samples), 1); $this->weights = array_fill(0, count($samples), 1);
@ -118,9 +138,12 @@ class DecisionStump extends WeightedClassifier
} }
$bestSplit = [ $bestSplit = [
'value' => 0, 'operator' => '', 'value' => 0,
'prob' => [], 'column' => 0, 'operator' => '',
'trainingErrorRate' => 1.0]; 'prob' => [],
'column' => 0,
'trainingErrorRate' => 1.0,
];
foreach ($columns as $col) { foreach ($columns as $col) {
if ($this->columnTypes[$col] == DecisionTree::CONTINUOUS) { if ($this->columnTypes[$col] == DecisionTree::CONTINUOUS) {
$split = $this->getBestNumericalSplit($samples, $targets, $col); $split = $this->getBestNumericalSplit($samples, $targets, $col);
@ -139,18 +162,6 @@ 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)
*/
public function setNumericalSplitCount(float $count): void
{
$this->numSplitCount = $count;
}
/** /**
* Determines best split point for the given column * Determines best split point for the given column
*/ */
@ -173,9 +184,13 @@ class DecisionStump extends WeightedClassifier
$threshold = array_sum($values) / (float) count($values); $threshold = array_sum($values) / (float) count($values);
[$errorRate, $prob] = $this->calculateErrorRate($targets, $threshold, $operator, $values); [$errorRate, $prob] = $this->calculateErrorRate($targets, $threshold, $operator, $values);
if ($split == null || $errorRate < $split['trainingErrorRate']) { if ($split == null || $errorRate < $split['trainingErrorRate']) {
$split = ['value' => $threshold, 'operator' => $operator, $split = [
'prob' => $prob, 'column' => $col, 'value' => $threshold,
'trainingErrorRate' => $errorRate]; 'operator' => $operator,
'prob' => $prob,
'column' => $col,
'trainingErrorRate' => $errorRate,
];
} }
// Try other possible points one by one // Try other possible points one by one
@ -183,9 +198,13 @@ class DecisionStump extends WeightedClassifier
$threshold = (float) $step; $threshold = (float) $step;
[$errorRate, $prob] = $this->calculateErrorRate($targets, $threshold, $operator, $values); [$errorRate, $prob] = $this->calculateErrorRate($targets, $threshold, $operator, $values);
if ($errorRate < $split['trainingErrorRate']) { if ($errorRate < $split['trainingErrorRate']) {
$split = ['value' => $threshold, 'operator' => $operator, $split = [
'prob' => $prob, 'column' => $col, 'value' => $threshold,
'trainingErrorRate' => $errorRate]; 'operator' => $operator,
'prob' => $prob,
'column' => $col,
'trainingErrorRate' => $errorRate,
];
} }
}// for }// for
} }
@ -206,9 +225,13 @@ class DecisionStump extends WeightedClassifier
[$errorRate, $prob] = $this->calculateErrorRate($targets, $val, $operator, $values); [$errorRate, $prob] = $this->calculateErrorRate($targets, $val, $operator, $values);
if ($split == null || $split['trainingErrorRate'] < $errorRate) { if ($split == null || $split['trainingErrorRate'] < $errorRate) {
$split = ['value' => $val, 'operator' => $operator, $split = [
'prob' => $prob, 'column' => $col, 'value' => $val,
'trainingErrorRate' => $errorRate]; 'operator' => $operator,
'prob' => $prob,
'column' => $col,
'trainingErrorRate' => $errorRate,
];
} }
} }
} }
@ -242,6 +265,7 @@ class DecisionStump extends WeightedClassifier
if (!isset($prob[$predicted][$target])) { if (!isset($prob[$predicted][$target])) {
$prob[$predicted][$target] = 0; $prob[$predicted][$target] = 0;
} }
++$prob[$predicted][$target]; ++$prob[$predicted][$target];
} }
@ -292,11 +316,4 @@ class DecisionStump extends WeightedClassifier
protected function resetBinary(): void protected function resetBinary(): void
{ {
} }
public function __toString() : string
{
return "IF $this->column $this->operator $this->value ".
'THEN '.$this->binaryLabels[0].' '.
'ELSE '.$this->binaryLabels[1];
}
} }

View File

@ -4,6 +4,8 @@ declare(strict_types=1);
namespace Phpml\Classification\Linear; namespace Phpml\Classification\Linear;
use Closure;
use Exception;
use Phpml\Helper\Optimizer\ConjugateGradient; use Phpml\Helper\Optimizer\ConjugateGradient;
class LogisticRegression extends Adaline class LogisticRegression extends Adaline
@ -70,18 +72,18 @@ class LogisticRegression extends Adaline
) { ) {
$trainingTypes = range(self::BATCH_TRAINING, self::CONJUGATE_GRAD_TRAINING); $trainingTypes = range(self::BATCH_TRAINING, self::CONJUGATE_GRAD_TRAINING);
if (!in_array($trainingType, $trainingTypes)) { if (!in_array($trainingType, $trainingTypes)) {
throw new \Exception('Logistic regression can only be trained with '. throw new Exception('Logistic regression can only be trained with '.
'batch (gradient descent), online (stochastic gradient descent) '. 'batch (gradient descent), online (stochastic gradient descent) '.
'or conjugate batch (conjugate gradients) algorithms'); 'or conjugate batch (conjugate gradients) algorithms');
} }
if (!in_array($cost, ['log', 'sse'])) { if (!in_array($cost, ['log', 'sse'])) {
throw new \Exception("Logistic regression cost function can be one of the following: \n". 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"); "'log' for log-likelihood and 'sse' for sum of squared errors");
} }
if ($penalty != '' && strtoupper($penalty) !== 'L2') { if ($penalty != '' && strtoupper($penalty) !== 'L2') {
throw new \Exception("Logistic regression supports only 'L2' regularization"); throw new Exception("Logistic regression supports only 'L2' regularization");
} }
$this->learningRate = 0.001; $this->learningRate = 0.001;
@ -132,14 +134,14 @@ class LogisticRegression extends Adaline
return $this->runConjugateGradient($samples, $targets, $callback); return $this->runConjugateGradient($samples, $targets, $callback);
default: default:
throw new \Exception('Logistic regression has invalid training type: %s.', $this->trainingType); throw new Exception('Logistic regression has invalid training type: %s.', $this->trainingType);
} }
} }
/** /**
* Executes Conjugate Gradient method to optimize the weights of the LogReg model * Executes Conjugate Gradient method to optimize the weights of the LogReg model
*/ */
protected function runConjugateGradient(array $samples, array $targets, \Closure $gradientFunc): void protected function runConjugateGradient(array $samples, array $targets, Closure $gradientFunc): void
{ {
if (empty($this->optimizer)) { if (empty($this->optimizer)) {
$this->optimizer = (new ConjugateGradient($this->featureCount)) $this->optimizer = (new ConjugateGradient($this->featureCount))
@ -155,7 +157,7 @@ class LogisticRegression extends Adaline
* *
* @throws \Exception * @throws \Exception
*/ */
protected function getCostFunction() : \Closure protected function getCostFunction(): Closure
{ {
$penalty = 0; $penalty = 0;
if ($this->penalty == 'L2') { if ($this->penalty == 'L2') {
@ -183,9 +185,11 @@ class LogisticRegression extends Adaline
if ($hX == 1) { if ($hX == 1) {
$hX = 1 - 1e-10; $hX = 1 - 1e-10;
} }
if ($hX == 0) { if ($hX == 0) {
$hX = 1e-10; $hX = 1e-10;
} }
$error = -$y * log($hX) - (1 - $y) * log(1 - $hX); $error = -$y * log($hX) - (1 - $y) * log(1 - $hX);
$gradient = $hX - $y; $gradient = $hX - $y;
@ -218,16 +222,14 @@ class LogisticRegression extends Adaline
return $callback; return $callback;
default: default:
throw new \Exception(sprintf('Logistic regression has invalid cost function: %s.', $this->costFunction)); 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 * Returns the output of the network, a float value between 0.0 and 1.0
*
* @return float
*/ */
protected function output(array $sample) protected function output(array $sample): float
{ {
$sum = parent::output($sample); $sum = parent::output($sample);
@ -253,7 +255,7 @@ class LogisticRegression extends Adaline
* *
* The probability is simply taken as the distance of the sample * The probability is simply taken as the distance of the sample
* to the decision plane. * to the decision plane.
*
* @param mixed $label * @param mixed $label
*/ */
protected function predictProbability(array $sample, $label): float protected function predictProbability(array $sample, $label): float

View File

@ -4,6 +4,8 @@ declare(strict_types=1);
namespace Phpml\Classification\Linear; namespace Phpml\Classification\Linear;
use Closure;
use Exception;
use Phpml\Classification\Classifier; use Phpml\Classification\Classifier;
use Phpml\Helper\OneVsRest; use Phpml\Helper\OneVsRest;
use Phpml\Helper\Optimizer\GD; use Phpml\Helper\Optimizer\GD;
@ -34,7 +36,7 @@ class Perceptron implements Classifier, IncrementalEstimator
/** /**
* @var array * @var array
*/ */
protected $weights; protected $weights = [];
/** /**
* @var float * @var float
@ -73,11 +75,11 @@ class Perceptron implements Classifier, IncrementalEstimator
public function __construct(float $learningRate = 0.001, int $maxIterations = 1000, bool $normalizeInputs = true) public function __construct(float $learningRate = 0.001, int $maxIterations = 1000, bool $normalizeInputs = true)
{ {
if ($learningRate <= 0.0 || $learningRate > 1.0) { 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 Exception('Learning rate should be a float value between 0.0(exclusive) and 1.0(inclusive)');
} }
if ($maxIterations <= 0) { if ($maxIterations <= 0) {
throw new \Exception('Maximum number of iterations must be an integer greater than 0'); throw new Exception('Maximum number of iterations must be an integer greater than 0');
} }
if ($normalizeInputs) { if ($normalizeInputs) {
@ -100,7 +102,10 @@ class Perceptron implements Classifier, IncrementalEstimator
} }
// Set all target values to either -1 or 1 // 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) { foreach ($targets as $key => $target) {
$targets[$key] = (string) $target == (string) $this->labels[1] ? 1 : -1; $targets[$key] = (string) $target == (string) $this->labels[1] ? 1 : -1;
} }
@ -111,15 +116,6 @@ class Perceptron implements Classifier, IncrementalEstimator
$this->runTraining($samples, $targets); $this->runTraining($samples, $targets);
} }
protected function resetBinary(): void
{
$this->labels = [];
$this->optimizer = null;
$this->featureCount = 0;
$this->weights = null;
$this->costValues = [];
}
/** /**
* Normally enabling early stopping for the optimization procedure may * Normally enabling early stopping for the optimization procedure may
* help saving processing time while in some cases it may result in * help saving processing time while in some cases it may result in
@ -145,11 +141,18 @@ class Perceptron implements Classifier, IncrementalEstimator
return $this->costValues; return $this->costValues;
} }
protected function resetBinary(): void
{
$this->labels = [];
$this->optimizer = null;
$this->featureCount = 0;
$this->weights = null;
$this->costValues = [];
}
/** /**
* Trains the perceptron model with Stochastic Gradient Descent optimization * Trains the perceptron model with Stochastic Gradient Descent optimization
* to get the correct set of weights * to get the correct set of weights
*
* @return void|mixed
*/ */
protected function runTraining(array $samples, array $targets) protected function runTraining(array $samples, array $targets)
{ {
@ -171,7 +174,7 @@ class Perceptron implements Classifier, IncrementalEstimator
* Executes a Gradient Descent algorithm for * Executes a Gradient Descent algorithm for
* the given cost function * the given cost function
*/ */
protected function runGradientDescent(array $samples, array $targets, \Closure $gradientFunc, bool $isBatch = false): void protected function runGradientDescent(array $samples, array $targets, Closure $gradientFunc, bool $isBatch = false): void
{ {
$class = $isBatch ? GD::class : StochasticGD::class; $class = $isBatch ? GD::class : StochasticGD::class;
@ -205,7 +208,7 @@ class Perceptron implements Classifier, IncrementalEstimator
/** /**
* Calculates net output of the network as a float value for the given input * Calculates net output of the network as a float value for the given input
* *
* @return int * @return int|float
*/ */
protected function output(array $sample) protected function output(array $sample)
{ {

View File

@ -14,7 +14,9 @@ class NaiveBayes implements Classifier
use Trainable, Predictable; use Trainable, Predictable;
public const CONTINUOS = 1; public const CONTINUOS = 1;
public const NOMINAL = 2; public const NOMINAL = 2;
public const EPSILON = 1e-10; public const EPSILON = 1e-10;
/** /**
@ -73,6 +75,31 @@ 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 * Calculates vital statistics for each label & feature. Stores these
* values in private array in order to avoid repeated calculation * values in private array in order to avoid repeated calculation
@ -119,6 +146,7 @@ class NaiveBayes implements Classifier
return $this->discreteProb[$label][$feature][$value]; return $this->discreteProb[$label][$feature][$value];
} }
$std = $this->std[$label][$feature] ; $std = $this->std[$label][$feature] ;
$mean = $this->mean[$label][$feature]; $mean = $this->mean[$label][$feature];
// Calculate the probability density by use of normal/Gaussian distribution // Calculate the probability density by use of normal/Gaussian distribution
@ -148,28 +176,4 @@ class NaiveBayes implements Classifier
return $samples; return $samples;
} }
/**
* @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);
}
} }

View File

@ -9,7 +9,7 @@ abstract class WeightedClassifier implements Classifier
/** /**
* @var array * @var array
*/ */
protected $weights; protected $weights = [];
/** /**
* Sets the array including a weight for each sample * Sets the array including a weight for each sample

View File

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Phpml\Clustering; namespace Phpml\Clustering;
use array_merge;
use Phpml\Math\Distance; use Phpml\Math\Distance;
use Phpml\Math\Distance\Euclidean; use Phpml\Math\Distance\Euclidean;
@ -26,7 +27,7 @@ class DBSCAN implements Clusterer
public function __construct(float $epsilon = 0.5, int $minSamples = 3, ?Distance $distanceMetric = null) public function __construct(float $epsilon = 0.5, int $minSamples = 3, ?Distance $distanceMetric = null)
{ {
if (null === $distanceMetric) { if ($distanceMetric === null) {
$distanceMetric = new Euclidean(); $distanceMetric = new Euclidean();
} }
@ -44,6 +45,7 @@ class DBSCAN implements Clusterer
if (isset($visited[$index])) { if (isset($visited[$index])) {
continue; continue;
} }
$visited[$index] = true; $visited[$index] = true;
$regionSamples = $this->getSamplesInRegion($sample, $samples); $regionSamples = $this->getSamplesInRegion($sample, $samples);
@ -84,7 +86,8 @@ class DBSCAN implements Clusterer
$cluster[$index] = $sample; $cluster[$index] = $sample;
} }
$cluster = \array_merge($cluster, ...$clusterMerge);
$cluster = array_merge($cluster, ...$clusterMerge);
return $cluster; return $cluster;
} }

View File

@ -30,7 +30,7 @@ class FuzzyCMeans implements Clusterer
/** /**
* @var array|float[][] * @var array|float[][]
*/ */
private $membership; private $membership = [];
/** /**
* @var float * @var float
@ -55,7 +55,7 @@ class FuzzyCMeans implements Clusterer
/** /**
* @var array * @var array
*/ */
private $samples; private $samples = [];
/** /**
* @throws InvalidArgumentException * @throws InvalidArgumentException
@ -65,12 +65,63 @@ class FuzzyCMeans implements Clusterer
if ($clustersNumber <= 0) { if ($clustersNumber <= 0) {
throw InvalidArgumentException::invalidClustersNumber(); throw InvalidArgumentException::invalidClustersNumber();
} }
$this->clustersNumber = $clustersNumber; $this->clustersNumber = $clustersNumber;
$this->fuzziness = $fuzziness; $this->fuzziness = $fuzziness;
$this->epsilon = $epsilon; $this->epsilon = $epsilon;
$this->maxIterations = $maxIterations; $this->maxIterations = $maxIterations;
} }
public function getMembershipMatrix(): array
{
return $this->membership;
}
/**
* @param array|Point[] $samples
*/
public function cluster(array $samples): array
{
// Initialize variables, clusters and membership matrix
$this->sampleCount = count($samples);
$this->samples = &$samples;
$this->space = new Space(count($samples[0]));
$this->initClusters();
// Our goal is minimizing the objective value while
// executing the clustering steps at a maximum number of iterations
$lastObjective = 0.0;
$iterations = 0;
do {
// Update the membership matrix and cluster centers, respectively
$this->updateMembershipMatrix();
$this->updateClusters();
// Calculate the new value of the objective function
$objectiveVal = $this->getObjective();
$difference = abs($lastObjective - $objectiveVal);
$lastObjective = $objectiveVal;
} while ($difference > $this->epsilon && $iterations++ <= $this->maxIterations);
// Attach (hard cluster) each data point to the nearest cluster
for ($k = 0; $k < $this->sampleCount; ++$k) {
$column = array_column($this->membership, $k);
arsort($column);
reset($column);
$i = key($column);
$cluster = $this->clusters[$i];
$cluster->attach(new Point($this->samples[$k]));
}
// Return grouped samples
$grouped = [];
foreach ($this->clusters as $cluster) {
$grouped[] = $cluster->getPoints();
}
return $grouped;
}
protected function initClusters(): void protected function initClusters(): void
{ {
// Membership array is a matrix of cluster number by sample counts // Membership array is a matrix of cluster number by sample counts
@ -87,7 +138,7 @@ class FuzzyCMeans implements Clusterer
$row = []; $row = [];
$total = 0.0; $total = 0.0;
for ($k = 0; $k < $cols; ++$k) { for ($k = 0; $k < $cols; ++$k) {
$val = rand(1, 5) / 10.0; $val = random_int(1, 5) / 10.0;
$row[] = $val; $row[] = $val;
$total += $val; $total += $val;
} }
@ -187,54 +238,4 @@ class FuzzyCMeans implements Clusterer
return $sum; return $sum;
} }
public function getMembershipMatrix() : array
{
return $this->membership;
}
/**
* @param array|Point[] $samples
*/
public function cluster(array $samples) : array
{
// Initialize variables, clusters and membership matrix
$this->sampleCount = count($samples);
$this->samples = &$samples;
$this->space = new Space(count($samples[0]));
$this->initClusters();
// Our goal is minimizing the objective value while
// executing the clustering steps at a maximum number of iterations
$lastObjective = 0.0;
$iterations = 0;
do {
// Update the membership matrix and cluster centers, respectively
$this->updateMembershipMatrix();
$this->updateClusters();
// Calculate the new value of the objective function
$objectiveVal = $this->getObjective();
$difference = abs($lastObjective - $objectiveVal);
$lastObjective = $objectiveVal;
} while ($difference > $this->epsilon && $iterations++ <= $this->maxIterations);
// Attach (hard cluster) each data point to the nearest cluster
for ($k = 0; $k < $this->sampleCount; ++$k) {
$column = array_column($this->membership, $k);
arsort($column);
reset($column);
$i = key($column);
$cluster = $this->clusters[$i];
$cluster->attach(new Point($this->samples[$k]));
}
// Return grouped samples
$grouped = [];
foreach ($this->clusters as $cluster) {
$grouped[] = $cluster->getPoints();
}
return $grouped;
}
} }

View File

@ -10,6 +10,7 @@ use Phpml\Exception\InvalidArgumentException;
class KMeans implements Clusterer class KMeans implements Clusterer
{ {
public const INIT_RANDOM = 1; public const INIT_RANDOM = 1;
public const INIT_KMEANS_PLUS_PLUS = 2; public const INIT_KMEANS_PLUS_PLUS = 2;
/** /**

View File

@ -76,7 +76,8 @@ class Cluster extends Point implements IteratorAggregate, Countable
public function updateCentroid(): void public function updateCentroid(): void
{ {
if (!$count = count($this->points)) { $count = count($this->points);
if (!$count) {
return; return;
} }

View File

@ -16,7 +16,7 @@ class Point implements ArrayAccess
/** /**
* @var array * @var array
*/ */
protected $coordinates; protected $coordinates = [];
public function __construct(array $coordinates) public function __construct(array $coordinates)
{ {

View File

@ -177,18 +177,6 @@ class Space extends SplObjectStorage
return $convergence; return $convergence;
} }
private function initializeRandomClusters(int $clustersNumber) : array
{
$clusters = [];
[$min, $max] = $this->getBoundaries();
for ($n = 0; $n < $clustersNumber; ++$n) {
$clusters[] = new Cluster($this, $this->getRandomPoint($min, $max)->getCoordinates());
}
return $clusters;
}
protected function initializeKMPPClusters(int $clustersNumber): array protected function initializeKMPPClusters(int $clustersNumber): array
{ {
$clusters = []; $clusters = [];
@ -218,4 +206,16 @@ class Space extends SplObjectStorage
return $clusters; return $clusters;
} }
private function initializeRandomClusters(int $clustersNumber): array
{
$clusters = [];
[$min, $max] = $this->getBoundaries();
for ($n = 0; $n < $clustersNumber; ++$n) {
$clusters[] = new Cluster($this, $this->getRandomPoint($min, $max)->getCoordinates());
}
return $clusters;
}
} }

View File

@ -31,16 +31,15 @@ abstract class Split
public function __construct(Dataset $dataset, float $testSize = 0.3, ?int $seed = null) public function __construct(Dataset $dataset, float $testSize = 0.3, ?int $seed = null)
{ {
if (0 >= $testSize || 1 <= $testSize) { if ($testSize <= 0 || $testSize >= 1) {
throw InvalidArgumentException::percentNotInRange('testSize'); throw InvalidArgumentException::percentNotInRange('testSize');
} }
$this->seedGenerator($seed); $this->seedGenerator($seed);
$this->splitDataset($dataset, $testSize); $this->splitDataset($dataset, $testSize);
} }
abstract protected function splitDataset(Dataset $dataset, float $testSize);
public function getTrainSamples(): array public function getTrainSamples(): array
{ {
return $this->trainSamples; return $this->trainSamples;
@ -61,9 +60,11 @@ abstract class Split
return $this->testLabels; return $this->testLabels;
} }
abstract protected function splitDataset(Dataset $dataset, float $testSize);
protected function seedGenerator(?int $seed = null): void protected function seedGenerator(?int $seed = null): void
{ {
if (null === $seed) { if ($seed === null) {
mt_srand(); mt_srand();
} else { } else {
mt_srand($seed); mt_srand($seed);

View File

@ -11,7 +11,7 @@ class CsvDataset extends ArrayDataset
/** /**
* @var array * @var array
*/ */
protected $columnNames; protected $columnNames = [];
/** /**
* @throws FileException * @throws FileException
@ -22,7 +22,8 @@ class CsvDataset extends ArrayDataset
throw FileException::missingFile(basename($filepath)); throw FileException::missingFile(basename($filepath));
} }
if (false === $handle = fopen($filepath, 'rb')) { $handle = fopen($filepath, 'rb');
if ($handle === false) {
throw FileException::cantOpenFile(basename($filepath)); throw FileException::cantOpenFile(basename($filepath));
} }

View File

@ -4,6 +4,8 @@ declare(strict_types=1);
namespace Phpml\DimensionReduction; namespace Phpml\DimensionReduction;
use Closure;
use Exception;
use Phpml\Math\Distance\Euclidean; use Phpml\Math\Distance\Euclidean;
use Phpml\Math\Distance\Manhattan; use Phpml\Math\Distance\Manhattan;
use Phpml\Math\Matrix; use Phpml\Math\Matrix;
@ -11,8 +13,11 @@ use Phpml\Math\Matrix;
class KernelPCA extends PCA class KernelPCA extends PCA
{ {
public const KERNEL_RBF = 1; public const KERNEL_RBF = 1;
public const KERNEL_SIGMOID = 2; public const KERNEL_SIGMOID = 2;
public const KERNEL_LAPLACIAN = 3; public const KERNEL_LAPLACIAN = 3;
public const KERNEL_LINEAR = 4; public const KERNEL_LINEAR = 4;
/** /**
@ -34,7 +39,7 @@ class KernelPCA extends PCA
* *
* @var array * @var array
*/ */
protected $data; protected $data = [];
/** /**
* Kernel principal component analysis (KernelPCA) is an extension of PCA using * Kernel principal component analysis (KernelPCA) is an extension of PCA using
@ -54,7 +59,7 @@ class KernelPCA extends PCA
{ {
$availableKernels = [self::KERNEL_RBF, self::KERNEL_SIGMOID, self::KERNEL_LAPLACIAN, self::KERNEL_LINEAR]; $availableKernels = [self::KERNEL_RBF, self::KERNEL_SIGMOID, self::KERNEL_LAPLACIAN, self::KERNEL_LINEAR];
if (!in_array($kernel, $availableKernels)) { if (!in_array($kernel, $availableKernels)) {
throw new \Exception('KernelPCA can be initialized with the following kernels only: Linear, RBF, Sigmoid and Laplacian'); throw new Exception('KernelPCA can be initialized with the following kernels only: Linear, RBF, Sigmoid and Laplacian');
} }
parent::__construct($totalVariance, $numFeatures); parent::__construct($totalVariance, $numFeatures);
@ -88,6 +93,27 @@ class KernelPCA extends PCA
return Matrix::transposeArray($this->eigVectors); return Matrix::transposeArray($this->eigVectors);
} }
/**
* Transforms the given sample to a lower dimensional vector by using
* the variables obtained during the last run of <code>fit</code>.
*
* @throws \Exception
*/
public function transform(array $sample): array
{
if (!$this->fit) {
throw new Exception('KernelPCA has not been fitted with respect to original dataset, please run KernelPCA::fit() first');
}
if (is_array($sample[0])) {
throw new Exception('KernelPCA::transform() accepts only one-dimensional arrays');
}
$pairs = $this->getDistancePairs($sample);
return $this->projectSample($pairs);
}
/** /**
* Calculates similarity matrix by use of selected kernel function<br> * Calculates similarity matrix by use of selected kernel function<br>
* An n-by-m matrix is given and an n-by-n matrix is returned * An n-by-m matrix is given and an n-by-n matrix is returned
@ -140,7 +166,7 @@ class KernelPCA extends PCA
* *
* @throws \Exception * @throws \Exception
*/ */
protected function getKernel(): \Closure protected function getKernel(): Closure
{ {
switch ($this->kernel) { switch ($this->kernel) {
case self::KERNEL_LINEAR: case self::KERNEL_LINEAR:
@ -173,7 +199,7 @@ class KernelPCA extends PCA
}; };
default: default:
throw new \Exception(sprintf('KernelPCA initialized with invalid kernel: %d', $this->kernel)); throw new Exception(sprintf('KernelPCA initialized with invalid kernel: %d', $this->kernel));
} }
} }
@ -203,25 +229,4 @@ class KernelPCA extends PCA
// return k.dot(eig) // return k.dot(eig)
return Matrix::dot($pairs, $eig); return Matrix::dot($pairs, $eig);
} }
/**
* Transforms the given sample to a lower dimensional vector by using
* the variables obtained during the last run of <code>fit</code>.
*
* @throws \Exception
*/
public function transform(array $sample) : array
{
if (!$this->fit) {
throw new \Exception('KernelPCA has not been fitted with respect to original dataset, please run KernelPCA::fit() first');
}
if (is_array($sample[0])) {
throw new \Exception('KernelPCA::transform() accepts only one-dimensional arrays');
}
$pairs = $this->getDistancePairs($sample);
return $this->projectSample($pairs);
}
} }

View File

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Phpml\DimensionReduction; namespace Phpml\DimensionReduction;
use Exception;
use Phpml\Math\Matrix; use Phpml\Math\Matrix;
class LDA extends EigenTransformerBase class LDA extends EigenTransformerBase
@ -16,22 +17,22 @@ class LDA extends EigenTransformerBase
/** /**
* @var array * @var array
*/ */
public $labels; public $labels = [];
/** /**
* @var array * @var array
*/ */
public $means; public $means = [];
/** /**
* @var array * @var array
*/ */
public $counts; public $counts = [];
/** /**
* @var float[] * @var float[]
*/ */
public $overallMean; public $overallMean = [];
/** /**
* Linear Discriminant Analysis (LDA) is used to reduce the dimensionality * Linear Discriminant Analysis (LDA) is used to reduce the dimensionality
@ -50,18 +51,21 @@ class LDA extends EigenTransformerBase
public function __construct(?float $totalVariance = null, ?int $numFeatures = null) public function __construct(?float $totalVariance = null, ?int $numFeatures = null)
{ {
if ($totalVariance !== null && ($totalVariance < 0.1 || $totalVariance > 0.99)) { if ($totalVariance !== null && ($totalVariance < 0.1 || $totalVariance > 0.99)) {
throw new \Exception('Total variance can be a value between 0.1 and 0.99'); throw new Exception('Total variance can be a value between 0.1 and 0.99');
} }
if ($numFeatures !== null && $numFeatures <= 0) { if ($numFeatures !== null && $numFeatures <= 0) {
throw new \Exception('Number of features to be preserved should be greater than 0'); throw new Exception('Number of features to be preserved should be greater than 0');
} }
if ($totalVariance !== null && $numFeatures !== null) { if ($totalVariance !== null && $numFeatures !== null) {
throw new \Exception('Either totalVariance or numFeatures should be specified in order to run the algorithm'); throw new Exception('Either totalVariance or numFeatures should be specified in order to run the algorithm');
} }
if ($numFeatures !== null) { if ($numFeatures !== null) {
$this->numFeatures = $numFeatures; $this->numFeatures = $numFeatures;
} }
if ($totalVariance !== null) { if ($totalVariance !== null) {
$this->totalVariance = $totalVariance; $this->totalVariance = $totalVariance;
} }
@ -86,6 +90,25 @@ class LDA extends EigenTransformerBase
return $this->reduce($data); return $this->reduce($data);
} }
/**
* Transforms the given sample to a lower dimensional vector by using
* the eigenVectors obtained in the last run of <code>fit</code>.
*
* @throws \Exception
*/
public function transform(array $sample): array
{
if (!$this->fit) {
throw new Exception('LDA has not been fitted with respect to original dataset, please run LDA::fit() first');
}
if (!is_array($sample[0])) {
$sample = [$sample];
}
return $this->reduce($sample);
}
/** /**
* Returns unique labels in the dataset * Returns unique labels in the dataset
*/ */
@ -113,6 +136,7 @@ class LDA extends EigenTransformerBase
if (!isset($means[$label][$col])) { if (!isset($means[$label][$col])) {
$means[$label][$col] = 0.0; $means[$label][$col] = 0.0;
} }
$means[$label][$col] += $val; $means[$label][$col] += $val;
$overallMean[$col] += $val; $overallMean[$col] += $val;
} }
@ -195,23 +219,4 @@ class LDA extends EigenTransformerBase
return $diff->transpose()->multiply($diff); return $diff->transpose()->multiply($diff);
} }
/**
* Transforms the given sample to a lower dimensional vector by using
* the eigenVectors obtained in the last run of <code>fit</code>.
*
* @throws \Exception
*/
public function transform(array $sample) : array
{
if (!$this->fit) {
throw new \Exception('LDA has not been fitted with respect to original dataset, please run LDA::fit() first');
}
if (!is_array($sample[0])) {
$sample = [$sample];
}
return $this->reduce($sample);
}
} }

View File

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Phpml\DimensionReduction; namespace Phpml\DimensionReduction;
use Exception;
use Phpml\Math\Statistic\Covariance; use Phpml\Math\Statistic\Covariance;
use Phpml\Math\Statistic\Mean; use Phpml\Math\Statistic\Mean;
@ -35,18 +36,21 @@ class PCA extends EigenTransformerBase
public function __construct(?float $totalVariance = null, ?int $numFeatures = null) public function __construct(?float $totalVariance = null, ?int $numFeatures = null)
{ {
if ($totalVariance !== null && ($totalVariance < 0.1 || $totalVariance > 0.99)) { if ($totalVariance !== null && ($totalVariance < 0.1 || $totalVariance > 0.99)) {
throw new \Exception('Total variance can be a value between 0.1 and 0.99'); throw new Exception('Total variance can be a value between 0.1 and 0.99');
} }
if ($numFeatures !== null && $numFeatures <= 0) { if ($numFeatures !== null && $numFeatures <= 0) {
throw new \Exception('Number of features to be preserved should be greater than 0'); throw new Exception('Number of features to be preserved should be greater than 0');
} }
if ($totalVariance !== null && $numFeatures !== null) { if ($totalVariance !== null && $numFeatures !== null) {
throw new \Exception('Either totalVariance or numFeatures should be specified in order to run the algorithm'); throw new Exception('Either totalVariance or numFeatures should be specified in order to run the algorithm');
} }
if ($numFeatures !== null) { if ($numFeatures !== null) {
$this->numFeatures = $numFeatures; $this->numFeatures = $numFeatures;
} }
if ($totalVariance !== null) { if ($totalVariance !== null) {
$this->totalVariance = $totalVariance; $this->totalVariance = $totalVariance;
} }
@ -73,6 +77,27 @@ class PCA extends EigenTransformerBase
return $this->reduce($data); return $this->reduce($data);
} }
/**
* Transforms the given sample to a lower dimensional vector by using
* the eigenVectors obtained in the last run of <code>fit</code>.
*
* @throws \Exception
*/
public function transform(array $sample): array
{
if (!$this->fit) {
throw new Exception('PCA has not been fitted with respect to original dataset, please run PCA::fit() first');
}
if (!is_array($sample[0])) {
$sample = [$sample];
}
$sample = $this->normalize($sample, count($sample[0]));
return $this->reduce($sample);
}
protected function calculateMeans(array $data, int $n): void protected function calculateMeans(array $data, int $n): void
{ {
// Calculate means for each dimension // Calculate means for each dimension
@ -102,25 +127,4 @@ class PCA extends EigenTransformerBase
return $data; return $data;
} }
/**
* Transforms the given sample to a lower dimensional vector by using
* the eigenVectors obtained in the last run of <code>fit</code>.
*
* @throws \Exception
*/
public function transform(array $sample) : array
{
if (!$this->fit) {
throw new \Exception('PCA has not been fitted with respect to original dataset, please run PCA::fit() first');
}
if (!is_array($sample[0])) {
$sample = [$sample];
}
$sample = $this->normalize($sample, count($sample[0]));
return $this->reduce($sample);
}
} }

View File

@ -4,9 +4,11 @@ declare(strict_types=1);
namespace Phpml\Exception; namespace Phpml\Exception;
class DatasetException extends \Exception use Exception;
class DatasetException extends Exception
{ {
public static function missingFolder(string $path) : DatasetException public static function missingFolder(string $path): self
{ {
return new self(sprintf('Dataset root folder "%s" missing.', $path)); return new self(sprintf('Dataset root folder "%s" missing.', $path));
} }

View File

@ -4,19 +4,21 @@ declare(strict_types=1);
namespace Phpml\Exception; namespace Phpml\Exception;
class FileException extends \Exception use Exception;
class FileException extends Exception
{ {
public static function missingFile(string $filepath) : FileException public static function missingFile(string $filepath): self
{ {
return new self(sprintf('File "%s" missing.', $filepath)); return new self(sprintf('File "%s" missing.', $filepath));
} }
public static function cantOpenFile(string $filepath) : FileException public static function cantOpenFile(string $filepath): self
{ {
return new self(sprintf('File "%s" can\'t be open.', $filepath)); return new self(sprintf('File "%s" can\'t be open.', $filepath));
} }
public static function cantSaveFile(string $filepath) : FileException public static function cantSaveFile(string $filepath): self
{ {
return new self(sprintf('File "%s" can\'t be saved.', $filepath)); return new self(sprintf('File "%s" can\'t be saved.', $filepath));
} }

View File

@ -4,39 +4,41 @@ declare(strict_types=1);
namespace Phpml\Exception; namespace Phpml\Exception;
class InvalidArgumentException extends \Exception use Exception;
class InvalidArgumentException extends Exception
{ {
public static function arraySizeNotMatch() : InvalidArgumentException public static function arraySizeNotMatch(): self
{ {
return new self('Size of given arrays does not match'); return new self('Size of given arrays does not match');
} }
public static function percentNotInRange($name) : InvalidArgumentException public static function percentNotInRange($name): self
{ {
return new self(sprintf('%s must be between 0.0 and 1.0', $name)); return new self(sprintf('%s must be between 0.0 and 1.0', $name));
} }
public static function arrayCantBeEmpty() : InvalidArgumentException public static function arrayCantBeEmpty(): self
{ {
return new self('The array has zero elements'); return new self('The array has zero elements');
} }
public static function arraySizeToSmall(int $minimumSize = 2) : InvalidArgumentException public static function arraySizeToSmall(int $minimumSize = 2): self
{ {
return new self(sprintf('The array must have at least %d elements', $minimumSize)); return new self(sprintf('The array must have at least %d elements', $minimumSize));
} }
public static function matrixDimensionsDidNotMatch() : InvalidArgumentException public static function matrixDimensionsDidNotMatch(): self
{ {
return new self('Matrix dimensions did not match'); return new self('Matrix dimensions did not match');
} }
public static function inconsistentMatrixSupplied() : InvalidArgumentException public static function inconsistentMatrixSupplied(): self
{ {
return new self('Inconsistent matrix supplied'); return new self('Inconsistent matrix supplied');
} }
public static function invalidClustersNumber() : InvalidArgumentException public static function invalidClustersNumber(): self
{ {
return new self('Invalid clusters number'); return new self('Invalid clusters number');
} }
@ -44,57 +46,57 @@ class InvalidArgumentException extends \Exception
/** /**
* @param mixed $target * @param mixed $target
*/ */
public static function invalidTarget($target) : InvalidArgumentException public static function invalidTarget($target): self
{ {
return new self(sprintf('Target with value "%s" is not part of the accepted classes', $target)); return new self(sprintf('Target with value "%s" is not part of the accepted classes', $target));
} }
public static function invalidStopWordsLanguage(string $language) : InvalidArgumentException public static function invalidStopWordsLanguage(string $language): self
{ {
return new self(sprintf('Can\'t find "%s" language for StopWords', $language)); return new self(sprintf('Can\'t find "%s" language for StopWords', $language));
} }
public static function invalidLayerNodeClass() : InvalidArgumentException public static function invalidLayerNodeClass(): self
{ {
return new self('Layer node class must implement Node interface'); return new self('Layer node class must implement Node interface');
} }
public static function invalidLayersNumber() : InvalidArgumentException public static function invalidLayersNumber(): self
{ {
return new self('Provide at least 1 hidden layer'); return new self('Provide at least 1 hidden layer');
} }
public static function invalidClassesNumber() : InvalidArgumentException public static function invalidClassesNumber(): self
{ {
return new self('Provide at least 2 different classes'); return new self('Provide at least 2 different classes');
} }
public static function inconsistentClasses() : InvalidArgumentException public static function inconsistentClasses(): self
{ {
return new self('The provided classes don\'t match the classes provided in the constructor'); return new self('The provided classes don\'t match the classes provided in the constructor');
} }
public static function fileNotFound(string $file) : InvalidArgumentException public static function fileNotFound(string $file): self
{ {
return new self(sprintf('File "%s" not found', $file)); return new self(sprintf('File "%s" not found', $file));
} }
public static function fileNotExecutable(string $file) : InvalidArgumentException public static function fileNotExecutable(string $file): self
{ {
return new self(sprintf('File "%s" is not executable', $file)); return new self(sprintf('File "%s" is not executable', $file));
} }
public static function pathNotFound(string $path) : InvalidArgumentException public static function pathNotFound(string $path): self
{ {
return new self(sprintf('The specified path "%s" does not exist', $path)); return new self(sprintf('The specified path "%s" does not exist', $path));
} }
public static function pathNotWritable(string $path) : InvalidArgumentException public static function pathNotWritable(string $path): self
{ {
return new self(sprintf('The specified path "%s" is not writable', $path)); return new self(sprintf('The specified path "%s" is not writable', $path));
} }
public static function invalidOperator(string $operator) : InvalidArgumentException public static function invalidOperator(string $operator): self
{ {
return new self(sprintf('Invalid operator "%s" provided', $operator)); return new self(sprintf('Invalid operator "%s" provided', $operator));
} }

View File

@ -4,19 +4,21 @@ declare(strict_types=1);
namespace Phpml\Exception; namespace Phpml\Exception;
class MatrixException extends \Exception use Exception;
class MatrixException extends Exception
{ {
public static function notSquareMatrix() : MatrixException public static function notSquareMatrix(): self
{ {
return new self('Matrix is not square matrix'); return new self('Matrix is not square matrix');
} }
public static function columnOutOfRange() : MatrixException public static function columnOutOfRange(): self
{ {
return new self('Column out of range'); return new self('Column out of range');
} }
public static function singularMatrix() : MatrixException public static function singularMatrix(): self
{ {
return new self('Matrix is singular'); return new self('Matrix is singular');
} }

View File

@ -4,9 +4,11 @@ declare(strict_types=1);
namespace Phpml\Exception; namespace Phpml\Exception;
class NormalizerException extends \Exception use Exception;
class NormalizerException extends Exception
{ {
public static function unknownNorm() : NormalizerException public static function unknownNorm(): self
{ {
return new self('Unknown norm supplied.'); return new self('Unknown norm supplied.');
} }

View File

@ -4,14 +4,16 @@ declare(strict_types=1);
namespace Phpml\Exception; namespace Phpml\Exception;
class SerializeException extends \Exception use Exception;
class SerializeException extends Exception
{ {
public static function cantUnserialize(string $filepath) : SerializeException public static function cantUnserialize(string $filepath): self
{ {
return new self(sprintf('"%s" can not be unserialized.', $filepath)); return new self(sprintf('"%s" can not be unserialized.', $filepath));
} }
public static function cantSerialize(string $classname) : SerializeException public static function cantSerialize(string $classname): self
{ {
return new self(sprintf('Class "%s" can not be serialized.', $classname)); return new self(sprintf('Class "%s" can not be serialized.', $classname));
} }

View File

@ -11,7 +11,7 @@ class StopWords
/** /**
* @var array * @var array
*/ */
protected $stopWords; protected $stopWords = [];
public function __construct(array $stopWords) public function __construct(array $stopWords)
{ {
@ -23,7 +23,7 @@ class StopWords
return isset($this->stopWords[$token]); return isset($this->stopWords[$token]);
} }
public static function factory(string $language = 'English') : StopWords public static function factory(string $language = 'English'): self
{ {
$className = __NAMESPACE__."\\StopWords\\$language"; $className = __NAMESPACE__."\\StopWords\\$language";

View File

@ -11,7 +11,7 @@ class TfIdfTransformer implements Transformer
/** /**
* @var array * @var array
*/ */
private $idf; private $idf = [];
public function __construct(?array $samples = null) public function __construct(?array $samples = null)
{ {

View File

@ -27,21 +27,18 @@ class TokenCountVectorizer implements Transformer
/** /**
* @var array * @var array
*/ */
private $vocabulary; private $vocabulary = [];
/** /**
* @var array * @var array
*/ */
private $frequencies; private $frequencies = [];
public function __construct(Tokenizer $tokenizer, ?StopWords $stopWords = null, float $minDF = 0.0) public function __construct(Tokenizer $tokenizer, ?StopWords $stopWords = null, float $minDF = 0.0)
{ {
$this->tokenizer = $tokenizer; $this->tokenizer = $tokenizer;
$this->stopWords = $stopWords; $this->stopWords = $stopWords;
$this->minDF = $minDF; $this->minDF = $minDF;
$this->vocabulary = [];
$this->frequencies = [];
} }
public function fit(array $samples): void public function fit(array $samples): void
@ -80,7 +77,7 @@ class TokenCountVectorizer implements Transformer
foreach ($tokens as $token) { foreach ($tokens as $token) {
$index = $this->getTokenIndex($token); $index = $this->getTokenIndex($token);
if (false !== $index) { if ($index !== false) {
$this->updateFrequency($token); $this->updateFrequency($token);
if (!isset($counts[$index])) { if (!isset($counts[$index])) {
$counts[$index] = 0; $counts[$index] = 0;

View File

@ -36,6 +36,18 @@ trait OneVsRest
$this->trainBylabel($samples, $targets); $this->trainBylabel($samples, $targets);
} }
/**
* Resets the classifier and the vars internally used by OneVsRest to create multiple classifiers.
*/
public function reset(): void
{
$this->classifiers = [];
$this->allLabels = [];
$this->costValues = [];
$this->resetBinary();
}
protected function trainByLabel(array $samples, array $targets, array $allLabels = []): void protected function trainByLabel(array $samples, array $targets, array $allLabels = []): void
{ {
// Overwrites the current value if it exist. $allLabels must be provided for each partialTrain run. // Overwrites the current value if it exist. $allLabels must be provided for each partialTrain run.
@ -44,6 +56,7 @@ trait OneVsRest
} else { } else {
$this->allLabels = array_keys(array_count_values($targets)); $this->allLabels = array_keys(array_count_values($targets));
} }
sort($this->allLabels, SORT_STRING); sort($this->allLabels, SORT_STRING);
// If there are only two targets, then there is no need to perform OvR // If there are only two targets, then there is no need to perform OvR
@ -77,18 +90,6 @@ trait OneVsRest
} }
} }
/**
* Resets the classifier and the vars internally used by OneVsRest to create multiple classifiers.
*/
public function reset(): void
{
$this->classifiers = [];
$this->allLabels = [];
$this->costValues = [];
$this->resetBinary();
}
/** /**
* Returns an instance of the current class after cleaning up OneVsRest stuff. * Returns an instance of the current class after cleaning up OneVsRest stuff.
* *
@ -105,29 +106,6 @@ trait OneVsRest
return $classifier; return $classifier;
} }
/**
* Groups all targets into two groups: Targets equal to
* the given label and the others
*
* $targets is not passed by reference nor contains objects so this method
* changes will not affect the caller $targets array.
*
* @param mixed $label
*
* @return array Binarized targets and target's labels
*/
private function binarizeTargets(array $targets, $label) : array
{
$notLabel = "not_$label";
foreach ($targets as $key => $target) {
$targets[$key] = $target == $label ? $label : $notLabel;
}
$labels = [$label, $notLabel];
return [$targets, $labels];
}
/** /**
* @return mixed * @return mixed
*/ */
@ -155,8 +133,6 @@ trait OneVsRest
/** /**
* To be overwritten by OneVsRest classifiers. * To be overwritten by OneVsRest classifiers.
*
* @return void
*/ */
abstract protected function resetBinary(): void; abstract protected function resetBinary(): void;
@ -174,4 +150,27 @@ trait OneVsRest
* @return mixed * @return mixed
*/ */
abstract protected function predictSampleBinary(array $sample); abstract protected function predictSampleBinary(array $sample);
/**
* Groups all targets into two groups: Targets equal to
* the given label and the others
*
* $targets is not passed by reference nor contains objects so this method
* changes will not affect the caller $targets array.
*
* @param mixed $label
*
* @return array Binarized targets and target's labels
*/
private function binarizeTargets(array $targets, $label): array
{
$notLabel = "not_$label";
foreach ($targets as $key => $target) {
$targets[$key] = $target == $label ? $label : $notLabel;
}
$labels = [$label, $notLabel];
return [$targets, $labels];
}
} }

View File

@ -4,6 +4,8 @@ declare(strict_types=1);
namespace Phpml\Helper\Optimizer; namespace Phpml\Helper\Optimizer;
use Closure;
/** /**
* Conjugate Gradient method to solve a non-linear f(x) with respect to unknown x * Conjugate Gradient method to solve a non-linear f(x) with respect to unknown x
* See https://en.wikipedia.org/wiki/Nonlinear_conjugate_gradient_method) * See https://en.wikipedia.org/wiki/Nonlinear_conjugate_gradient_method)
@ -17,7 +19,7 @@ namespace Phpml\Helper\Optimizer;
*/ */
class ConjugateGradient extends GD class ConjugateGradient extends GD
{ {
public function runOptimization(array $samples, array $targets, \Closure $gradientCb) : array public function runOptimization(array $samples, array $targets, Closure $gradientCb): array
{ {
$this->samples = $samples; $this->samples = $samples;
$this->targets = $targets; $this->targets = $targets;
@ -25,7 +27,7 @@ class ConjugateGradient extends GD
$this->sampleCount = count($samples); $this->sampleCount = count($samples);
$this->costValues = []; $this->costValues = [];
$d = mp::muls($this->gradient($this->theta), -1); $d = MP::muls($this->gradient($this->theta), -1);
for ($i = 0; $i < $this->maxIterations; ++$i) { for ($i = 0; $i < $this->maxIterations; ++$i) {
// Obtain α that minimizes f(θ + α.d) // Obtain α that minimizes f(θ + α.d)
@ -96,8 +98,8 @@ class ConjugateGradient extends GD
$large = 0.01 * $d; $large = 0.01 * $d;
// Obtain θ + α.d for two initial values, x0 and x1 // Obtain θ + α.d for two initial values, x0 and x1
$x0 = mp::adds($this->theta, $small); $x0 = MP::adds($this->theta, $small);
$x1 = mp::adds($this->theta, $large); $x1 = MP::adds($this->theta, $large);
$epsilon = 0.0001; $epsilon = 0.0001;
$iteration = 0; $iteration = 0;
@ -113,9 +115,9 @@ class ConjugateGradient extends GD
if ($fx1 < $fx0) { if ($fx1 < $fx0) {
$x0 = $x1; $x0 = $x1;
$x1 = mp::adds($x1, 0.01); // Enlarge second $x1 = MP::adds($x1, 0.01); // Enlarge second
} else { } else {
$x1 = mp::divs(mp::add($x1, $x0), 2.0); $x1 = MP::divs(MP::add($x1, $x0), 2.0);
} // Get to the midpoint } // Get to the midpoint
$error = $fx1 / $this->dimensions; $error = $fx1 / $this->dimensions;
@ -181,7 +183,7 @@ class ConjugateGradient extends GD
{ {
$grad = $this->gradient($theta); $grad = $this->gradient($theta);
return mp::add(mp::muls($grad, -1), mp::muls($d, $beta)); return MP::add(MP::muls($grad, -1), MP::muls($d, $beta));
} }
} }
@ -189,7 +191,7 @@ class ConjugateGradient extends GD
* Handles element-wise vector operations between vector-vector * Handles element-wise vector operations between vector-vector
* and vector-scalar variables * and vector-scalar variables
*/ */
class mp class MP
{ {
/** /**
* Element-wise <b>multiplication</b> of two vectors of the same size * Element-wise <b>multiplication</b> of two vectors of the same size

View File

@ -4,6 +4,8 @@ declare(strict_types=1);
namespace Phpml\Helper\Optimizer; namespace Phpml\Helper\Optimizer;
use Closure;
/** /**
* Batch version of Gradient Descent to optimize the weights * Batch version of Gradient Descent to optimize the weights
* of a classifier given samples, targets and the objective function to minimize * of a classifier given samples, targets and the objective function to minimize
@ -17,7 +19,7 @@ class GD extends StochasticGD
*/ */
protected $sampleCount = null; protected $sampleCount = null;
public function runOptimization(array $samples, array $targets, \Closure $gradientCb) : array public function runOptimization(array $samples, array $targets, Closure $gradientCb): array
{ {
$this->samples = $samples; $this->samples = $samples;
$this->targets = $targets; $this->targets = $targets;

View File

@ -4,6 +4,9 @@ declare(strict_types=1);
namespace Phpml\Helper\Optimizer; namespace Phpml\Helper\Optimizer;
use Closure;
use Exception;
abstract class Optimizer abstract class Optimizer
{ {
/** /**
@ -11,7 +14,7 @@ abstract class Optimizer
* *
* @var array * @var array
*/ */
protected $theta; protected $theta = [];
/** /**
* Number of dimensions * Number of dimensions
@ -30,7 +33,7 @@ abstract class Optimizer
// Inits the weights randomly // Inits the weights randomly
$this->theta = []; $this->theta = [];
for ($i = 0; $i < $this->dimensions; ++$i) { for ($i = 0; $i < $this->dimensions; ++$i) {
$this->theta[] = rand() / (float) getrandmax(); $this->theta[] = random_int(0, getrandmax()) / (float) getrandmax();
} }
} }
@ -44,7 +47,7 @@ abstract class Optimizer
public function setInitialTheta(array $theta) public function setInitialTheta(array $theta)
{ {
if (count($theta) != $this->dimensions) { if (count($theta) != $this->dimensions) {
throw new \Exception("Number of values in the weights array should be $this->dimensions"); throw new Exception("Number of values in the weights array should be $this->dimensions");
} }
$this->theta = $theta; $this->theta = $theta;
@ -56,5 +59,5 @@ abstract class Optimizer
* Executes the optimization with the given samples & targets * Executes the optimization with the given samples & targets
* and returns the weights * and returns the weights
*/ */
abstract public function runOptimization(array $samples, array $targets, \Closure $gradientCb); abstract public function runOptimization(array $samples, array $targets, Closure $gradientCb);
} }

View File

@ -4,6 +4,8 @@ declare(strict_types=1);
namespace Phpml\Helper\Optimizer; namespace Phpml\Helper\Optimizer;
use Closure;
/** /**
* Stochastic Gradient Descent optimization method * Stochastic Gradient Descent optimization method
* to find a solution for the equation A.ϴ = y where * to find a solution for the equation A.ϴ = y where
@ -66,6 +68,7 @@ class StochasticGD extends Optimizer
* @var bool * @var bool
*/ */
protected $enableEarlyStop = true; protected $enableEarlyStop = true;
/** /**
* List of values obtained by evaluating the cost function at each iteration * List of values obtained by evaluating the cost function at each iteration
* of the algorithm * of the algorithm
@ -141,7 +144,7 @@ class StochasticGD extends Optimizer
* The cost function to minimize and the gradient of the function are to be * The cost function to minimize and the gradient of the function are to be
* handled by the callback function provided as the third parameter of the method. * handled by the callback function provided as the third parameter of the method.
*/ */
public function runOptimization(array $samples, array $targets, \Closure $gradientCb) : array public function runOptimization(array $samples, array $targets, Closure $gradientCb): array
{ {
$this->samples = $samples; $this->samples = $samples;
$this->targets = $targets; $this->targets = $targets;
@ -181,6 +184,15 @@ class StochasticGD extends Optimizer
return $this->theta = $bestTheta; return $this->theta = $bestTheta;
} }
/**
* Returns the list of cost values for each iteration executed in
* last run of the optimization
*/
public function getCostValues(): array
{
return $this->costValues;
}
protected function updateTheta(): float protected function updateTheta(): float
{ {
$jValue = 0.0; $jValue = 0.0;
@ -237,15 +249,6 @@ class StochasticGD extends Optimizer
return false; return false;
} }
/**
* Returns the list of cost values for each iteration executed in
* last run of the optimization
*/
public function getCostValues() : array
{
return $this->costValues;
}
/** /**
* Clears the optimizer internal vars after the optimization process. * Clears the optimizer internal vars after the optimization process.
*/ */

View File

@ -7,10 +7,10 @@ namespace Phpml\Math;
interface Kernel interface Kernel
{ {
/** /**
* @param float $a * @param float|array $a
* @param float $b * @param float|array $b
* *
* @return float * @return float|array
*/ */
public function compute($a, $b); public function compute($a, $b);
} }

View File

@ -23,12 +23,11 @@ class RBF implements Kernel
* @param array $a * @param array $a
* @param array $b * @param array $b
*/ */
public function compute($a, $b) public function compute($a, $b): float
{ {
$score = 2 * Product::scalar($a, $b); $score = 2 * Product::scalar($a, $b);
$squares = Product::scalar($a, $a) + Product::scalar($b, $b); $squares = Product::scalar($a, $a) + Product::scalar($b, $b);
$result = exp(-$this->gamma * ($squares - $score));
return $result; return exp(-$this->gamma * ($squares - $score));
} }
} }

View File

@ -1,6 +1,7 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
/** /**
* Class to obtain eigenvalues and eigenvectors of a real matrix. * Class to obtain eigenvalues and eigenvectors of a real matrix.
* *
@ -54,6 +55,7 @@ class EigenvalueDecomposition
* @var array * @var array
*/ */
private $d = []; private $d = [];
private $e = []; private $e = [];
/** /**
@ -75,7 +77,7 @@ class EigenvalueDecomposition
* *
* @var array * @var array
*/ */
private $ort; private $ort = [];
/** /**
* Used for complex scalar division. * Used for complex scalar division.
@ -83,6 +85,7 @@ class EigenvalueDecomposition
* @var float * @var float
*/ */
private $cdivr; private $cdivr;
private $cdivi; private $cdivi;
/** /**
@ -116,6 +119,71 @@ class EigenvalueDecomposition
} }
} }
/**
* Return the eigenvector matrix
*/
public function getEigenvectors(): array
{
$vectors = $this->V;
// Always return the eigenvectors of length 1.0
$vectors = new Matrix($vectors);
$vectors = array_map(function ($vect) {
$sum = 0;
for ($i = 0; $i < count($vect); ++$i) {
$sum += $vect[$i] ** 2;
}
$sum = sqrt($sum);
for ($i = 0; $i < count($vect); ++$i) {
$vect[$i] /= $sum;
}
return $vect;
}, $vectors->transpose()->toArray());
return $vectors;
}
/**
* Return the real parts of the eigenvalues<br>
* d = real(diag(D));
*/
public function getRealEigenvalues(): array
{
return $this->d;
}
/**
* Return the imaginary parts of the eigenvalues <br>
* d = imag(diag(D))
*/
public function getImagEigenvalues(): array
{
return $this->e;
}
/**
* Return the block diagonal eigenvalue matrix
*/
public function getDiagonalEigenvalues(): array
{
$D = [];
for ($i = 0; $i < $this->n; ++$i) {
$D[$i] = array_fill(0, $this->n, 0.0);
$D[$i][$i] = $this->d[$i];
if ($this->e[$i] == 0) {
continue;
}
$o = ($this->e[$i] > 0) ? $i + 1 : $i - 1;
$D[$i][$o] = $this->e[$i];
}
return $D;
}
/** /**
* Symmetric Householder reduction to tridiagonal form. * Symmetric Householder reduction to tridiagonal form.
*/ */
@ -158,6 +226,7 @@ class EigenvalueDecomposition
for ($j = 0; $j < $i; ++$j) { for ($j = 0; $j < $i; ++$j) {
$this->e[$j] = 0.0; $this->e[$j] = 0.0;
} }
// Apply similarity transformation to remaining columns. // Apply similarity transformation to remaining columns.
for ($j = 0; $j < $i; ++$j) { for ($j = 0; $j < $i; ++$j) {
$f = $this->d[$j]; $f = $this->d[$j];
@ -168,6 +237,7 @@ class EigenvalueDecomposition
$g += $this->V[$k][$j] * $this->d[$k]; $g += $this->V[$k][$j] * $this->d[$k];
$this->e[$k] += $this->V[$k][$j] * $f; $this->e[$k] += $this->V[$k][$j] * $f;
} }
$this->e[$j] = $g; $this->e[$j] = $g;
} }
@ -185,16 +255,19 @@ class EigenvalueDecomposition
for ($j = 0; $j < $i; ++$j) { for ($j = 0; $j < $i; ++$j) {
$this->e[$j] -= $hh * $this->d[$j]; $this->e[$j] -= $hh * $this->d[$j];
} }
for ($j = 0; $j < $i; ++$j) { for ($j = 0; $j < $i; ++$j) {
$f = $this->d[$j]; $f = $this->d[$j];
$g = $this->e[$j]; $g = $this->e[$j];
for ($k = $j; $k <= $i_; ++$k) { for ($k = $j; $k <= $i_; ++$k) {
$this->V[$k][$j] -= ($f * $this->e[$k] + $g * $this->d[$k]); $this->V[$k][$j] -= ($f * $this->e[$k] + $g * $this->d[$k]);
} }
$this->d[$j] = $this->V[$i - 1][$j]; $this->d[$j] = $this->V[$i - 1][$j];
$this->V[$i][$j] = 0.0; $this->V[$i][$j] = 0.0;
} }
} }
$this->d[$i] = $h; $this->d[$i] = $h;
} }
@ -207,16 +280,19 @@ class EigenvalueDecomposition
for ($k = 0; $k <= $i; ++$k) { for ($k = 0; $k <= $i; ++$k) {
$this->d[$k] = $this->V[$k][$i + 1] / $h; $this->d[$k] = $this->V[$k][$i + 1] / $h;
} }
for ($j = 0; $j <= $i; ++$j) { for ($j = 0; $j <= $i; ++$j) {
$g = 0.0; $g = 0.0;
for ($k = 0; $k <= $i; ++$k) { for ($k = 0; $k <= $i; ++$k) {
$g += $this->V[$k][$i + 1] * $this->V[$k][$j]; $g += $this->V[$k][$i + 1] * $this->V[$k][$j];
} }
for ($k = 0; $k <= $i; ++$k) { for ($k = 0; $k <= $i; ++$k) {
$this->V[$k][$j] -= $g * $this->d[$k]; $this->V[$k][$j] -= $g * $this->d[$k];
} }
} }
} }
for ($k = 0; $k <= $i; ++$k) { for ($k = 0; $k <= $i; ++$k) {
$this->V[$k][$i + 1] = 0.0; $this->V[$k][$i + 1] = 0.0;
} }
@ -241,6 +317,7 @@ class EigenvalueDecomposition
for ($i = 1; $i < $this->n; ++$i) { for ($i = 1; $i < $this->n; ++$i) {
$this->e[$i - 1] = $this->e[$i]; $this->e[$i - 1] = $this->e[$i];
} }
$this->e[$this->n - 1] = 0.0; $this->e[$this->n - 1] = 0.0;
$f = 0.0; $f = 0.0;
$tst1 = 0.0; $tst1 = 0.0;
@ -254,8 +331,10 @@ class EigenvalueDecomposition
if (abs($this->e[$m]) <= $eps * $tst1) { if (abs($this->e[$m]) <= $eps * $tst1) {
break; break;
} }
++$m; ++$m;
} }
// If m == l, $this->d[l] is an eigenvalue, // If m == l, $this->d[l] is an eigenvalue,
// otherwise, iterate. // otherwise, iterate.
if ($m > $l) { if ($m > $l) {
@ -270,6 +349,7 @@ class EigenvalueDecomposition
if ($p < 0) { if ($p < 0) {
$r *= -1; $r *= -1;
} }
$this->d[$l] = $this->e[$l] / ($p + $r); $this->d[$l] = $this->e[$l] / ($p + $r);
$this->d[$l + 1] = $this->e[$l] * ($p + $r); $this->d[$l + 1] = $this->e[$l] * ($p + $r);
$dl1 = $this->d[$l + 1]; $dl1 = $this->d[$l + 1];
@ -277,6 +357,7 @@ class EigenvalueDecomposition
for ($i = $l + 2; $i < $this->n; ++$i) { for ($i = $l + 2; $i < $this->n; ++$i) {
$this->d[$i] -= $h; $this->d[$i] -= $h;
} }
$f += $h; $f += $h;
// Implicit QL transformation. // Implicit QL transformation.
$p = $this->d[$m]; $p = $this->d[$m];
@ -303,12 +384,14 @@ class EigenvalueDecomposition
$this->V[$k][$i] = $c * $this->V[$k][$i] - $s * $h; $this->V[$k][$i] = $c * $this->V[$k][$i] - $s * $h;
} }
} }
$p = -$s * $s2 * $c3 * $el1 * $this->e[$l] / $dl1; $p = -$s * $s2 * $c3 * $el1 * $this->e[$l] / $dl1;
$this->e[$l] = $s * $p; $this->e[$l] = $s * $p;
$this->d[$l] = $c * $p; $this->d[$l] = $c * $p;
// Check for convergence. // Check for convergence.
} while (abs($this->e[$l]) > $eps * $tst1); } while (abs($this->e[$l]) > $eps * $tst1);
} }
$this->d[$l] = $this->d[$l] + $f; $this->d[$l] = $this->d[$l] + $f;
$this->e[$l] = 0.0; $this->e[$l] = 0.0;
} }
@ -323,6 +406,7 @@ class EigenvalueDecomposition
$p = $this->d[$j]; $p = $this->d[$j];
} }
} }
if ($k != $i) { if ($k != $i) {
$this->d[$k] = $this->d[$i]; $this->d[$k] = $this->d[$i];
$this->d[$i] = $p; $this->d[$i] = $p;
@ -354,6 +438,7 @@ class EigenvalueDecomposition
for ($i = $m; $i <= $high; ++$i) { for ($i = $m; $i <= $high; ++$i) {
$scale = $scale + abs($this->H[$i][$m - 1]); $scale = $scale + abs($this->H[$i][$m - 1]);
} }
if ($scale != 0.0) { if ($scale != 0.0) {
// Compute Householder transformation. // Compute Householder transformation.
$h = 0.0; $h = 0.0;
@ -361,10 +446,12 @@ class EigenvalueDecomposition
$this->ort[$i] = $this->H[$i][$m - 1] / $scale; $this->ort[$i] = $this->H[$i][$m - 1] / $scale;
$h += $this->ort[$i] * $this->ort[$i]; $h += $this->ort[$i] * $this->ort[$i];
} }
$g = sqrt($h); $g = sqrt($h);
if ($this->ort[$m] > 0) { if ($this->ort[$m] > 0) {
$g *= -1; $g *= -1;
} }
$h -= $this->ort[$m] * $g; $h -= $this->ort[$m] * $g;
$this->ort[$m] -= $g; $this->ort[$m] -= $g;
// Apply Householder similarity transformation // Apply Householder similarity transformation
@ -374,21 +461,25 @@ class EigenvalueDecomposition
for ($i = $high; $i >= $m; --$i) { for ($i = $high; $i >= $m; --$i) {
$f += $this->ort[$i] * $this->H[$i][$j]; $f += $this->ort[$i] * $this->H[$i][$j];
} }
$f /= $h; $f /= $h;
for ($i = $m; $i <= $high; ++$i) { for ($i = $m; $i <= $high; ++$i) {
$this->H[$i][$j] -= $f * $this->ort[$i]; $this->H[$i][$j] -= $f * $this->ort[$i];
} }
} }
for ($i = 0; $i <= $high; ++$i) { for ($i = 0; $i <= $high; ++$i) {
$f = 0.0; $f = 0.0;
for ($j = $high; $j >= $m; --$j) { for ($j = $high; $j >= $m; --$j) {
$f += $this->ort[$j] * $this->H[$i][$j]; $f += $this->ort[$j] * $this->H[$i][$j];
} }
$f = $f / $h; $f = $f / $h;
for ($j = $m; $j <= $high; ++$j) { for ($j = $m; $j <= $high; ++$j) {
$this->H[$i][$j] -= $f * $this->ort[$j]; $this->H[$i][$j] -= $f * $this->ort[$j];
} }
} }
$this->ort[$m] = $scale * $this->ort[$m]; $this->ort[$m] = $scale * $this->ort[$m];
$this->H[$m][$m - 1] = $scale * $g; $this->H[$m][$m - 1] = $scale * $g;
} }
@ -400,16 +491,19 @@ class EigenvalueDecomposition
$this->V[$i][$j] = ($i == $j ? 1.0 : 0.0); $this->V[$i][$j] = ($i == $j ? 1.0 : 0.0);
} }
} }
for ($m = $high - 1; $m >= $low + 1; --$m) { for ($m = $high - 1; $m >= $low + 1; --$m) {
if ($this->H[$m][$m - 1] != 0.0) { if ($this->H[$m][$m - 1] != 0.0) {
for ($i = $m + 1; $i <= $high; ++$i) { for ($i = $m + 1; $i <= $high; ++$i) {
$this->ort[$i] = $this->H[$i][$m - 1]; $this->ort[$i] = $this->H[$i][$m - 1];
} }
for ($j = $m; $j <= $high; ++$j) { for ($j = $m; $j <= $high; ++$j) {
$g = 0.0; $g = 0.0;
for ($i = $m; $i <= $high; ++$i) { for ($i = $m; $i <= $high; ++$i) {
$g += $this->ort[$i] * $this->V[$i][$j]; $g += $this->ort[$i] * $this->V[$i][$j];
} }
// Double division avoids possible underflow // Double division avoids possible underflow
$g = ($g / $this->ort[$m]) / $this->H[$m][$m - 1]; $g = ($g / $this->ort[$m]) / $this->H[$m][$m - 1];
for ($i = $m; $i <= $high; ++$i) { for ($i = $m; $i <= $high; ++$i) {
@ -469,6 +563,7 @@ class EigenvalueDecomposition
$this->d[$i] = $this->H[$i][$i]; $this->d[$i] = $this->H[$i][$i];
$this->e[$i] = 0.0; $this->e[$i] = 0.0;
} }
for ($j = max($i - 1, 0); $j < $nn; ++$j) { for ($j = max($i - 1, 0); $j < $nn; ++$j) {
$norm = $norm + abs($this->H[$i][$j]); $norm = $norm + abs($this->H[$i][$j]);
} }
@ -484,11 +579,14 @@ class EigenvalueDecomposition
if ($s == 0.0) { if ($s == 0.0) {
$s = $norm; $s = $norm;
} }
if (abs($this->H[$l][$l - 1]) < $eps * $s) { if (abs($this->H[$l][$l - 1]) < $eps * $s) {
break; break;
} }
--$l; --$l;
} }
// Check for convergence // Check for convergence
// One root found // One root found
if ($l == $n) { if ($l == $n) {
@ -513,11 +611,13 @@ class EigenvalueDecomposition
} else { } else {
$z = $p - $z; $z = $p - $z;
} }
$this->d[$n - 1] = $x + $z; $this->d[$n - 1] = $x + $z;
$this->d[$n] = $this->d[$n - 1]; $this->d[$n] = $this->d[$n - 1];
if ($z != 0.0) { if ($z != 0.0) {
$this->d[$n] = $x - $w / $z; $this->d[$n] = $x - $w / $z;
} }
$this->e[$n - 1] = 0.0; $this->e[$n - 1] = 0.0;
$this->e[$n] = 0.0; $this->e[$n] = 0.0;
$x = $this->H[$n][$n - 1]; $x = $this->H[$n][$n - 1];
@ -533,18 +633,21 @@ class EigenvalueDecomposition
$this->H[$n - 1][$j] = $q * $z + $p * $this->H[$n][$j]; $this->H[$n - 1][$j] = $q * $z + $p * $this->H[$n][$j];
$this->H[$n][$j] = $q * $this->H[$n][$j] - $p * $z; $this->H[$n][$j] = $q * $this->H[$n][$j] - $p * $z;
} }
// Column modification // Column modification
for ($i = 0; $i <= $n; ++$i) { for ($i = 0; $i <= $n; ++$i) {
$z = $this->H[$i][$n - 1]; $z = $this->H[$i][$n - 1];
$this->H[$i][$n - 1] = $q * $z + $p * $this->H[$i][$n]; $this->H[$i][$n - 1] = $q * $z + $p * $this->H[$i][$n];
$this->H[$i][$n] = $q * $this->H[$i][$n] - $p * $z; $this->H[$i][$n] = $q * $this->H[$i][$n] - $p * $z;
} }
// Accumulate transformations // Accumulate transformations
for ($i = $low; $i <= $high; ++$i) { for ($i = $low; $i <= $high; ++$i) {
$z = $this->V[$i][$n - 1]; $z = $this->V[$i][$n - 1];
$this->V[$i][$n - 1] = $q * $z + $p * $this->V[$i][$n]; $this->V[$i][$n - 1] = $q * $z + $p * $this->V[$i][$n];
$this->V[$i][$n] = $q * $this->V[$i][$n] - $p * $z; $this->V[$i][$n] = $q * $this->V[$i][$n] - $p * $z;
} }
// Complex pair // Complex pair
} else { } else {
$this->d[$n - 1] = $x + $p; $this->d[$n - 1] = $x + $p;
@ -552,6 +655,7 @@ class EigenvalueDecomposition
$this->e[$n - 1] = $z; $this->e[$n - 1] = $z;
$this->e[$n] = -$z; $this->e[$n] = -$z;
} }
$n = $n - 2; $n = $n - 2;
$iter = 0; $iter = 0;
// No convergence yet // No convergence yet
@ -564,16 +668,19 @@ class EigenvalueDecomposition
$y = $this->H[$n - 1][$n - 1]; $y = $this->H[$n - 1][$n - 1];
$w = $this->H[$n][$n - 1] * $this->H[$n - 1][$n]; $w = $this->H[$n][$n - 1] * $this->H[$n - 1][$n];
} }
// Wilkinson's original ad hoc shift // Wilkinson's original ad hoc shift
if ($iter == 10) { if ($iter == 10) {
$exshift += $x; $exshift += $x;
for ($i = $low; $i <= $n; ++$i) { for ($i = $low; $i <= $n; ++$i) {
$this->H[$i][$i] -= $x; $this->H[$i][$i] -= $x;
} }
$s = abs($this->H[$n][$n - 1]) + abs($this->H[$n - 1][$n - 2]); $s = abs($this->H[$n][$n - 1]) + abs($this->H[$n - 1][$n - 2]);
$x = $y = 0.75 * $s; $x = $y = 0.75 * $s;
$w = -0.4375 * $s * $s; $w = -0.4375 * $s * $s;
} }
// MATLAB's new ad hoc shift // MATLAB's new ad hoc shift
if ($iter == 30) { if ($iter == 30) {
$s = ($y - $x) / 2.0; $s = ($y - $x) / 2.0;
@ -583,14 +690,17 @@ class EigenvalueDecomposition
if ($y < $x) { if ($y < $x) {
$s = -$s; $s = -$s;
} }
$s = $x - $w / (($y - $x) / 2.0 + $s); $s = $x - $w / (($y - $x) / 2.0 + $s);
for ($i = $low; $i <= $n; ++$i) { for ($i = $low; $i <= $n; ++$i) {
$this->H[$i][$i] -= $s; $this->H[$i][$i] -= $s;
} }
$exshift += $s; $exshift += $s;
$x = $y = $w = 0.964; $x = $y = $w = 0.964;
} }
} }
// Could check iteration count here. // Could check iteration count here.
$iter = $iter + 1; $iter = $iter + 1;
// Look for two consecutive small sub-diagonal elements // Look for two consecutive small sub-diagonal elements
@ -609,18 +719,22 @@ class EigenvalueDecomposition
if ($m == $l) { if ($m == $l) {
break; break;
} }
if (abs($this->H[$m][$m - 1]) * (abs($q) + abs($r)) < if (abs($this->H[$m][$m - 1]) * (abs($q) + abs($r)) <
$eps * (abs($p) * (abs($this->H[$m - 1][$m - 1]) + abs($z) + abs($this->H[$m + 1][$m + 1])))) { $eps * (abs($p) * (abs($this->H[$m - 1][$m - 1]) + abs($z) + abs($this->H[$m + 1][$m + 1])))) {
break; break;
} }
--$m; --$m;
} }
for ($i = $m + 2; $i <= $n; ++$i) { for ($i = $m + 2; $i <= $n; ++$i) {
$this->H[$i][$i - 2] = 0.0; $this->H[$i][$i - 2] = 0.0;
if ($i > $m + 2) { if ($i > $m + 2) {
$this->H[$i][$i - 3] = 0.0; $this->H[$i][$i - 3] = 0.0;
} }
} }
// Double QR step involving rows l:n and columns m:n // Double QR step involving rows l:n and columns m:n
for ($k = $m; $k <= $n - 1; ++$k) { for ($k = $m; $k <= $n - 1; ++$k) {
$notlast = ($k != $n - 1); $notlast = ($k != $n - 1);
@ -635,19 +749,23 @@ class EigenvalueDecomposition
$r = $r / $x; $r = $r / $x;
} }
} }
if ($x == 0.0) { if ($x == 0.0) {
break; break;
} }
$s = sqrt($p * $p + $q * $q + $r * $r); $s = sqrt($p * $p + $q * $q + $r * $r);
if ($p < 0) { if ($p < 0) {
$s = -$s; $s = -$s;
} }
if ($s != 0) { if ($s != 0) {
if ($k != $m) { if ($k != $m) {
$this->H[$k][$k - 1] = -$s * $x; $this->H[$k][$k - 1] = -$s * $x;
} elseif ($l != $m) { } elseif ($l != $m) {
$this->H[$k][$k - 1] = -$this->H[$k][$k - 1]; $this->H[$k][$k - 1] = -$this->H[$k][$k - 1];
} }
$p = $p + $s; $p = $p + $s;
$x = $p / $s; $x = $p / $s;
$y = $q / $s; $y = $q / $s;
@ -661,9 +779,11 @@ class EigenvalueDecomposition
$p = $p + $r * $this->H[$k + 2][$j]; $p = $p + $r * $this->H[$k + 2][$j];
$this->H[$k + 2][$j] = $this->H[$k + 2][$j] - $p * $z; $this->H[$k + 2][$j] = $this->H[$k + 2][$j] - $p * $z;
} }
$this->H[$k][$j] = $this->H[$k][$j] - $p * $x; $this->H[$k][$j] = $this->H[$k][$j] - $p * $x;
$this->H[$k + 1][$j] = $this->H[$k + 1][$j] - $p * $y; $this->H[$k + 1][$j] = $this->H[$k + 1][$j] - $p * $y;
} }
// Column modification // Column modification
for ($i = 0; $i <= min($n, $k + 3); ++$i) { for ($i = 0; $i <= min($n, $k + 3); ++$i) {
$p = $x * $this->H[$i][$k] + $y * $this->H[$i][$k + 1]; $p = $x * $this->H[$i][$k] + $y * $this->H[$i][$k + 1];
@ -671,9 +791,11 @@ class EigenvalueDecomposition
$p = $p + $z * $this->H[$i][$k + 2]; $p = $p + $z * $this->H[$i][$k + 2];
$this->H[$i][$k + 2] = $this->H[$i][$k + 2] - $p * $r; $this->H[$i][$k + 2] = $this->H[$i][$k + 2] - $p * $r;
} }
$this->H[$i][$k] = $this->H[$i][$k] - $p; $this->H[$i][$k] = $this->H[$i][$k] - $p;
$this->H[$i][$k + 1] = $this->H[$i][$k + 1] - $p * $q; $this->H[$i][$k + 1] = $this->H[$i][$k + 1] - $p * $q;
} }
// Accumulate transformations // Accumulate transformations
for ($i = $low; $i <= $high; ++$i) { for ($i = $low; $i <= $high; ++$i) {
$p = $x * $this->V[$i][$k] + $y * $this->V[$i][$k + 1]; $p = $x * $this->V[$i][$k] + $y * $this->V[$i][$k + 1];
@ -681,6 +803,7 @@ class EigenvalueDecomposition
$p = $p + $z * $this->V[$i][$k + 2]; $p = $p + $z * $this->V[$i][$k + 2];
$this->V[$i][$k + 2] = $this->V[$i][$k + 2] - $p * $r; $this->V[$i][$k + 2] = $this->V[$i][$k + 2] - $p * $r;
} }
$this->V[$i][$k] = $this->V[$i][$k] - $p; $this->V[$i][$k] = $this->V[$i][$k] - $p;
$this->V[$i][$k + 1] = $this->V[$i][$k + 1] - $p * $q; $this->V[$i][$k + 1] = $this->V[$i][$k + 1] - $p * $q;
} }
@ -719,6 +842,7 @@ class EigenvalueDecomposition
} else { } else {
$this->H[$i][$n] = -$r / ($eps * $norm); $this->H[$i][$n] = -$r / ($eps * $norm);
} }
// Solve real equations // Solve real equations
} else { } else {
$x = $this->H[$i][$i + 1]; $x = $this->H[$i][$i + 1];
@ -732,6 +856,7 @@ class EigenvalueDecomposition
$this->H[$i + 1][$n] = (-$s - $y * $t) / $z; $this->H[$i + 1][$n] = (-$s - $y * $t) / $z;
} }
} }
// Overflow control // Overflow control
$t = abs($this->H[$i][$n]); $t = abs($this->H[$i][$n]);
if (($eps * $t) * $t > 1) { if (($eps * $t) * $t > 1) {
@ -741,6 +866,7 @@ class EigenvalueDecomposition
} }
} }
} }
// Complex vector // Complex vector
} elseif ($q < 0) { } elseif ($q < 0) {
$l = $n - 1; $l = $n - 1;
@ -753,6 +879,7 @@ class EigenvalueDecomposition
$this->H[$n - 1][$n - 1] = $this->cdivr; $this->H[$n - 1][$n - 1] = $this->cdivr;
$this->H[$n - 1][$n] = $this->cdivi; $this->H[$n - 1][$n] = $this->cdivi;
} }
$this->H[$n][$n - 1] = 0.0; $this->H[$n][$n - 1] = 0.0;
$this->H[$n][$n] = 1.0; $this->H[$n][$n] = 1.0;
for ($i = $n - 2; $i >= 0; --$i) { for ($i = $n - 2; $i >= 0; --$i) {
@ -763,6 +890,7 @@ class EigenvalueDecomposition
$ra = $ra + $this->H[$i][$j] * $this->H[$j][$n - 1]; $ra = $ra + $this->H[$i][$j] * $this->H[$j][$n - 1];
$sa = $sa + $this->H[$i][$j] * $this->H[$j][$n]; $sa = $sa + $this->H[$i][$j] * $this->H[$j][$n];
} }
$w = $this->H[$i][$i] - $p; $w = $this->H[$i][$i] - $p;
if ($this->e[$i] < 0.0) { if ($this->e[$i] < 0.0) {
$z = $w; $z = $w;
@ -783,6 +911,7 @@ class EigenvalueDecomposition
if ($vr == 0.0 & $vi == 0.0) { if ($vr == 0.0 & $vi == 0.0) {
$vr = $eps * $norm * (abs($w) + abs($q) + abs($x) + abs($y) + abs($z)); $vr = $eps * $norm * (abs($w) + abs($q) + abs($x) + abs($y) + abs($z));
} }
$this->cdiv($x * $r - $z * $ra + $q * $sa, $x * $s - $z * $sa - $q * $ra, $vr, $vi); $this->cdiv($x * $r - $z * $ra + $q * $sa, $x * $s - $z * $sa - $q * $ra, $vr, $vi);
$this->H[$i][$n - 1] = $this->cdivr; $this->H[$i][$n - 1] = $this->cdivr;
$this->H[$i][$n] = $this->cdivi; $this->H[$i][$n] = $this->cdivi;
@ -795,6 +924,7 @@ class EigenvalueDecomposition
$this->H[$i + 1][$n] = $this->cdivi; $this->H[$i + 1][$n] = $this->cdivi;
} }
} }
// Overflow control // Overflow control
$t = max(abs($this->H[$i][$n - 1]), abs($this->H[$i][$n])); $t = max(abs($this->H[$i][$n - 1]), abs($this->H[$i][$n]));
if (($eps * $t) * $t > 1) { if (($eps * $t) * $t > 1) {
@ -824,81 +954,9 @@ class EigenvalueDecomposition
for ($k = $low; $k <= min($j, $high); ++$k) { for ($k = $low; $k <= min($j, $high); ++$k) {
$z = $z + $this->V[$i][$k] * $this->H[$k][$j]; $z = $z + $this->V[$i][$k] * $this->H[$k][$j];
} }
$this->V[$i][$j] = $z; $this->V[$i][$j] = $z;
} }
} }
} }
/**
* Return the eigenvector matrix
*
* @return array
*/
public function getEigenvectors()
{
$vectors = $this->V;
// Always return the eigenvectors of length 1.0
$vectors = new Matrix($vectors);
$vectors = array_map(function ($vect) {
$sum = 0;
for ($i = 0; $i < count($vect); ++$i) {
$sum += $vect[$i] ** 2;
}
$sum = sqrt($sum);
for ($i = 0; $i < count($vect); ++$i) {
$vect[$i] /= $sum;
}
return $vect;
}, $vectors->transpose()->toArray());
return $vectors;
}
/**
* Return the real parts of the eigenvalues<br>
* d = real(diag(D));
*
* @return array
*/
public function getRealEigenvalues()
{
return $this->d;
}
/**
* Return the imaginary parts of the eigenvalues <br>
* d = imag(diag(D))
*
* @return array
*/
public function getImagEigenvalues()
{
return $this->e;
}
/**
* Return the block diagonal eigenvalue matrix
*
* @return array
*/
public function getDiagonalEigenvalues()
{
$D = [];
for ($i = 0; $i < $this->n; ++$i) {
$D[$i] = array_fill(0, $this->n, 0.0);
$D[$i][$i] = $this->d[$i];
if ($this->e[$i] == 0) {
continue;
}
$o = ($this->e[$i] > 0) ? $i + 1 : $i - 1;
$D[$i][$o] = $this->e[$i];
}
return $D;
}
} }

View File

@ -1,6 +1,7 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
/** /**
* @package JAMA * @package JAMA
* *
@ -90,6 +91,7 @@ class LUDecomposition
for ($i = 0; $i < $this->m; ++$i) { for ($i = 0; $i < $this->m; ++$i) {
$this->piv[$i] = $i; $this->piv[$i] = $i;
} }
$this->pivsign = 1; $this->pivsign = 1;
$LUcolj = []; $LUcolj = [];
@ -99,6 +101,7 @@ class LUDecomposition
for ($i = 0; $i < $this->m; ++$i) { for ($i = 0; $i < $this->m; ++$i) {
$LUcolj[$i] = &$this->LU[$i][$j]; $LUcolj[$i] = &$this->LU[$i][$j];
} }
// Apply previous transformations. // Apply previous transformations.
for ($i = 0; $i < $this->m; ++$i) { for ($i = 0; $i < $this->m; ++$i) {
$LUrowi = $this->LU[$i]; $LUrowi = $this->LU[$i];
@ -108,8 +111,10 @@ class LUDecomposition
for ($k = 0; $k < $kmax; ++$k) { for ($k = 0; $k < $kmax; ++$k) {
$s += $LUrowi[$k] * $LUcolj[$k]; $s += $LUrowi[$k] * $LUcolj[$k];
} }
$LUrowi[$j] = $LUcolj[$i] -= $s; $LUrowi[$j] = $LUcolj[$i] -= $s;
} }
// Find pivot and exchange if necessary. // Find pivot and exchange if necessary.
$p = $j; $p = $j;
for ($i = $j + 1; $i < $this->m; ++$i) { for ($i = $j + 1; $i < $this->m; ++$i) {
@ -117,17 +122,20 @@ class LUDecomposition
$p = $i; $p = $i;
} }
} }
if ($p != $j) { if ($p != $j) {
for ($k = 0; $k < $this->n; ++$k) { for ($k = 0; $k < $this->n; ++$k) {
$t = $this->LU[$p][$k]; $t = $this->LU[$p][$k];
$this->LU[$p][$k] = $this->LU[$j][$k]; $this->LU[$p][$k] = $this->LU[$j][$k];
$this->LU[$j][$k] = $t; $this->LU[$j][$k] = $t;
} }
$k = $this->piv[$p]; $k = $this->piv[$p];
$this->piv[$p] = $this->piv[$j]; $this->piv[$p] = $this->piv[$j];
$this->piv[$j] = $k; $this->piv[$j] = $k;
$this->pivsign = $this->pivsign * -1; $this->pivsign = $this->pivsign * -1;
} }
// Compute multipliers. // Compute multipliers.
if (($j < $this->m) && ($this->LU[$j][$j] != 0.0)) { if (($j < $this->m) && ($this->LU[$j][$j] != 0.0)) {
for ($i = $j + 1; $i < $this->m; ++$i) { for ($i = $j + 1; $i < $this->m; ++$i) {
@ -268,11 +276,13 @@ class LUDecomposition
} }
} }
} }
// Solve U*X = Y; // Solve U*X = Y;
for ($k = $this->n - 1; $k >= 0; --$k) { for ($k = $this->n - 1; $k >= 0; --$k) {
for ($j = 0; $j < $nx; ++$j) { for ($j = 0; $j < $nx; ++$j) {
$X[$k][$j] /= $this->LU[$k][$k]; $X[$k][$j] /= $this->LU[$k][$k];
} }
for ($i = 0; $i < $k; ++$i) { for ($i = 0; $i < $k; ++$i) {
for ($j = 0; $j < $nx; ++$j) { for ($j = 0; $j < $nx; ++$j) {
$X[$i][$j] -= $X[$k][$j] * $this->LU[$i][$k]; $X[$i][$j] -= $X[$k][$j] * $this->LU[$i][$k];

View File

@ -13,7 +13,7 @@ class Matrix
/** /**
* @var array * @var array
*/ */
private $matrix; private $matrix = [];
/** /**
* @var int * @var int
@ -56,7 +56,7 @@ class Matrix
$this->matrix = $matrix; $this->matrix = $matrix;
} }
public static function fromFlatArray(array $array) : Matrix public static function fromFlatArray(array $array): self
{ {
$matrix = []; $matrix = [];
foreach ($array as $value) { foreach ($array as $value) {
@ -123,7 +123,7 @@ class Matrix
return $this->columns === $this->rows; return $this->columns === $this->rows;
} }
public function transpose() : Matrix public function transpose(): self
{ {
if ($this->rows == 1) { if ($this->rows == 1) {
$matrix = array_map(function ($el) { $matrix = array_map(function ($el) {
@ -136,7 +136,7 @@ class Matrix
return new self($matrix, false); return new self($matrix, false);
} }
public function multiply(Matrix $matrix) : Matrix public function multiply(self $matrix): self
{ {
if ($this->columns != $matrix->getRows()) { if ($this->columns != $matrix->getRows()) {
throw InvalidArgumentException::inconsistentMatrixSupplied(); throw InvalidArgumentException::inconsistentMatrixSupplied();
@ -157,7 +157,7 @@ class Matrix
return new self($product, false); return new self($product, false);
} }
public function divideByScalar($value) : Matrix public function divideByScalar($value): self
{ {
$newMatrix = []; $newMatrix = [];
for ($i = 0; $i < $this->rows; ++$i) { for ($i = 0; $i < $this->rows; ++$i) {
@ -169,7 +169,7 @@ class Matrix
return new self($newMatrix, false); return new self($newMatrix, false);
} }
public function multiplyByScalar($value) : Matrix public function multiplyByScalar($value): self
{ {
$newMatrix = []; $newMatrix = [];
for ($i = 0; $i < $this->rows; ++$i) { for ($i = 0; $i < $this->rows; ++$i) {
@ -184,7 +184,7 @@ class Matrix
/** /**
* Element-wise addition of the matrix with another one * Element-wise addition of the matrix with another one
*/ */
public function add(Matrix $other) : Matrix public function add(self $other): self
{ {
return $this->_add($other); return $this->_add($other);
} }
@ -192,30 +192,12 @@ class Matrix
/** /**
* Element-wise subtracting of another matrix from this one * Element-wise subtracting of another matrix from this one
*/ */
public function subtract(Matrix $other) : Matrix public function subtract(self $other): self
{ {
return $this->_add($other, -1); return $this->_add($other, -1);
} }
/** public function inverse(): self
* Element-wise addition or substraction depending on the given sign parameter
*/
protected function _add(Matrix $other, int $sign = 1) : Matrix
{
$a1 = $this->toArray();
$a2 = $other->toArray();
$newMatrix = [];
for ($i = 0; $i < $this->rows; ++$i) {
for ($k = 0; $k < $this->columns; ++$k) {
$newMatrix[$i][$k] = $a1[$i][$k] + $sign * $a2[$i][$k];
}
}
return new self($newMatrix, false);
}
public function inverse() : Matrix
{ {
if (!$this->isSquare()) { if (!$this->isSquare()) {
throw MatrixException::notSquareMatrix(); throw MatrixException::notSquareMatrix();
@ -228,20 +210,7 @@ class Matrix
return new self($inverse, false); return new self($inverse, false);
} }
/** public function crossOut(int $row, int $column): self
* Returns diagonal identity matrix of the same size of this matrix
*/
protected function getIdentity() : Matrix
{
$array = array_fill(0, $this->rows, array_fill(0, $this->columns, 0));
for ($i = 0; $i < $this->rows; ++$i) {
$array[$i][$i] = 1;
}
return new self($array, false);
}
public function crossOut(int $row, int $column) : Matrix
{ {
$newMatrix = []; $newMatrix = [];
$r = 0; $r = 0;
@ -254,6 +223,7 @@ class Matrix
++$c; ++$c;
} }
} }
++$r; ++$r;
} }
} }
@ -263,7 +233,7 @@ class Matrix
public function isSingular(): bool public function isSingular(): bool
{ {
return 0 == $this->getDeterminant(); return $this->getDeterminant() == 0;
} }
/** /**
@ -285,4 +255,35 @@ class Matrix
return $m1->multiply($m2->transpose())->toArray()[0]; return $m1->multiply($m2->transpose())->toArray()[0];
} }
/**
* Element-wise addition or substraction depending on the given sign parameter
*/
protected function _add(self $other, int $sign = 1): self
{
$a1 = $this->toArray();
$a2 = $other->toArray();
$newMatrix = [];
for ($i = 0; $i < $this->rows; ++$i) {
for ($k = 0; $k < $this->columns; ++$k) {
$newMatrix[$i][$k] = $a1[$i][$k] + $sign * $a2[$i][$k];
}
}
return new self($newMatrix, false);
}
/**
* Returns diagonal identity matrix of the same size of this matrix
*/
protected function getIdentity(): self
{
$array = array_fill(0, $this->rows, array_fill(0, $this->columns, 0));
for ($i = 0; $i < $this->rows; ++$i) {
$array[$i][$i] = 1;
}
return new self($array, false);
}
} }

View File

@ -4,12 +4,15 @@ declare(strict_types=1);
namespace Phpml\Math; namespace Phpml\Math;
class Set implements \IteratorAggregate use ArrayIterator;
use IteratorAggregate;
class Set implements IteratorAggregate
{ {
/** /**
* @var string[]|int[]|float[] * @var string[]|int[]|float[]
*/ */
private $elements; private $elements = [];
/** /**
* @param string[]|int[]|float[] $elements * @param string[]|int[]|float[] $elements
@ -22,7 +25,7 @@ class Set implements \IteratorAggregate
/** /**
* Creates the union of A and B. * Creates the union of A and B.
*/ */
public static function union(Set $a, Set $b) : Set public static function union(self $a, self $b): self
{ {
return new self(array_merge($a->toArray(), $b->toArray())); return new self(array_merge($a->toArray(), $b->toArray()));
} }
@ -30,7 +33,7 @@ class Set implements \IteratorAggregate
/** /**
* Creates the intersection of A and B. * Creates the intersection of A and B.
*/ */
public static function intersection(Set $a, Set $b) : Set public static function intersection(self $a, self $b): self
{ {
return new self(array_intersect($a->toArray(), $b->toArray())); return new self(array_intersect($a->toArray(), $b->toArray()));
} }
@ -38,7 +41,7 @@ class Set implements \IteratorAggregate
/** /**
* Creates the difference of A and B. * Creates the difference of A and B.
*/ */
public static function difference(Set $a, Set $b) : Set public static function difference(self $a, self $b): self
{ {
return new self(array_diff($a->toArray(), $b->toArray())); return new self(array_diff($a->toArray(), $b->toArray()));
} }
@ -48,7 +51,7 @@ class Set implements \IteratorAggregate
* *
* @return Set[] * @return Set[]
*/ */
public static function cartesian(Set $a, Set $b) : array public static function cartesian(self $a, self $b): array
{ {
$cartesian = []; $cartesian = [];
@ -66,7 +69,7 @@ class Set implements \IteratorAggregate
* *
* @return Set[] * @return Set[]
*/ */
public static function power(Set $a) : array public static function power(self $a): array
{ {
$power = [new self()]; $power = [new self()];
@ -79,24 +82,10 @@ class Set implements \IteratorAggregate
return $power; return $power;
} }
/**
* Removes duplicates and rewrites index.
*
* @param string[]|int[]|float[] $elements
*
* @return string[]|int[]|float[]
*/
private static function sanitize(array $elements) : array
{
sort($elements, SORT_ASC);
return array_values(array_unique($elements, SORT_ASC));
}
/** /**
* @param string|int|float $element * @param string|int|float $element
*/ */
public function add($element) : Set public function add($element): self
{ {
return $this->addAll([$element]); return $this->addAll([$element]);
} }
@ -104,7 +93,7 @@ class Set implements \IteratorAggregate
/** /**
* @param string[]|int[]|float[] $elements * @param string[]|int[]|float[] $elements
*/ */
public function addAll(array $elements) : Set public function addAll(array $elements): self
{ {
$this->elements = self::sanitize(array_merge($this->elements, $elements)); $this->elements = self::sanitize(array_merge($this->elements, $elements));
@ -114,7 +103,7 @@ class Set implements \IteratorAggregate
/** /**
* @param string|int|float $element * @param string|int|float $element
*/ */
public function remove($element) : Set public function remove($element): self
{ {
return $this->removeAll([$element]); return $this->removeAll([$element]);
} }
@ -122,7 +111,7 @@ class Set implements \IteratorAggregate
/** /**
* @param string[]|int[]|float[] $elements * @param string[]|int[]|float[] $elements
*/ */
public function removeAll(array $elements) : Set public function removeAll(array $elements): self
{ {
$this->elements = self::sanitize(array_diff($this->elements, $elements)); $this->elements = self::sanitize(array_diff($this->elements, $elements));
@ -153,9 +142,9 @@ class Set implements \IteratorAggregate
return $this->elements; return $this->elements;
} }
public function getIterator() : \ArrayIterator public function getIterator(): ArrayIterator
{ {
return new \ArrayIterator($this->elements); return new ArrayIterator($this->elements);
} }
public function isEmpty(): bool public function isEmpty(): bool
@ -167,4 +156,18 @@ class Set implements \IteratorAggregate
{ {
return count($this->elements); return count($this->elements);
} }
/**
* Removes duplicates and rewrites index.
*
* @param string[]|int[]|float[] $elements
*
* @return string[]|int[]|float[]
*/
private static function sanitize(array $elements): array
{
sort($elements, SORT_ASC);
return array_values(array_unique($elements, SORT_ASC));
}
} }

View File

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Phpml\Math\Statistic; namespace Phpml\Math\Statistic;
use Exception;
use Phpml\Exception\InvalidArgumentException; use Phpml\Exception\InvalidArgumentException;
class Covariance class Covariance
@ -63,7 +64,7 @@ class Covariance
} }
if ($i < 0 || $k < 0 || $i >= $n || $k >= $n) { if ($i < 0 || $k < 0 || $i >= $n || $k >= $n) {
throw new \Exception('Given indices i and k do not match with the dimensionality of data'); throw new Exception('Given indices i and k do not match with the dimensionality of data');
} }
if ($meanX === null || $meanY === null) { if ($meanX === null || $meanY === null) {
@ -92,10 +93,12 @@ class Covariance
if ($index == $i) { if ($index == $i) {
$val[0] = $col - $meanX; $val[0] = $col - $meanX;
} }
if ($index == $k) { if ($index == $k) {
$val[1] = $col - $meanY; $val[1] = $col - $meanY;
} }
} }
$sum += $val[0] * $val[1]; $sum += $val[0] * $val[1];
} }
} }

View File

@ -32,7 +32,7 @@ class Mean
sort($numbers, SORT_NUMERIC); sort($numbers, SORT_NUMERIC);
$median = $numbers[$middleIndex]; $median = $numbers[$middleIndex];
if (0 === $count % 2) { if ($count % 2 === 0) {
$median = ($median + $numbers[$middleIndex - 1]) / 2; $median = ($median + $numbers[$middleIndex - 1]) / 2;
} }

View File

@ -93,6 +93,7 @@ class ClassificationReport
$this->average[$metric] = 0.0; $this->average[$metric] = 0.0;
continue; continue;
} }
$this->average[$metric] = array_sum($values) / count($values); $this->average[$metric] = array_sum($values) / count($values);
} }
} }
@ -102,7 +103,8 @@ class ClassificationReport
*/ */
private function computePrecision(int $truePositive, int $falsePositive) private function computePrecision(int $truePositive, int $falsePositive)
{ {
if (0 == ($divider = $truePositive + $falsePositive)) { $divider = $truePositive + $falsePositive;
if ($divider == 0) {
return 0.0; return 0.0;
} }
@ -114,7 +116,8 @@ class ClassificationReport
*/ */
private function computeRecall(int $truePositive, int $falseNegative) private function computeRecall(int $truePositive, int $falseNegative)
{ {
if (0 == ($divider = $truePositive + $falseNegative)) { $divider = $truePositive + $falseNegative;
if ($divider == 0) {
return 0.0; return 0.0;
} }
@ -123,7 +126,8 @@ class ClassificationReport
private function computeF1Score(float $precision, float $recall): float private function computeF1Score(float $precision, float $recall): float
{ {
if (0 == ($divider = $precision + $recall)) { $divider = $precision + $recall;
if ($divider == 0) {
return 0.0; return 0.0;
} }

View File

@ -28,20 +28,6 @@ class Layer
} }
} }
/**
* @param ActivationFunction|null $activationFunction
*
* @return Neuron
*/
private function createNode(string $nodeClass, ?ActivationFunction $activationFunction = null)
{
if (Neuron::class == $nodeClass) {
return new Neuron($activationFunction);
}
return new $nodeClass();
}
public function addNode(Node $node): void public function addNode(Node $node): void
{ {
$this->nodes[] = $node; $this->nodes[] = $node;
@ -54,4 +40,16 @@ class Layer
{ {
return $this->nodes; return $this->nodes;
} }
/**
* @return Neuron
*/
private function createNode(string $nodeClass, ?ActivationFunction $activationFunction = null): Node
{
if ($nodeClass == Neuron::class) {
return new Neuron($activationFunction);
}
return new $nodeClass();
}
} }

View File

@ -8,14 +8,9 @@ interface Network
{ {
/** /**
* @param mixed $input * @param mixed $input
*
* @return self
*/ */
public function setInput($input); public function setInput($input): self;
/**
* @return array
*/
public function getOutput(): array; public function getOutput(): array;
public function addLayer(Layer $layer); public function addLayer(Layer $layer);

View File

@ -14,7 +14,7 @@ abstract class LayeredNetwork implements Network
/** /**
* @var Layer[] * @var Layer[]
*/ */
protected $layers; protected $layers = [];
public function addLayer(Layer $layer): void public function addLayer(Layer $layer): void
{ {
@ -54,7 +54,7 @@ abstract class LayeredNetwork implements Network
* *
* @return $this * @return $this
*/ */
public function setInput($input) public function setInput($input): Network
{ {
$firstLayer = $this->layers[0]; $firstLayer = $this->layers[0];

View File

@ -20,41 +20,36 @@ abstract class MultilayerPerceptron extends LayeredNetwork implements Estimator,
{ {
use Predictable; use Predictable;
/**
* @var int
*/
private $inputLayerFeatures;
/**
* @var array
*/
private $hiddenLayers;
/** /**
* @var array * @var array
*/ */
protected $classes = []; protected $classes = [];
/**
* @var int
*/
private $iterations;
/** /**
* @var ActivationFunction * @var ActivationFunction
*/ */
protected $activationFunction; protected $activationFunction;
/**
* @var float
*/
private $learningRate;
/** /**
* @var Backpropagation * @var Backpropagation
*/ */
protected $backpropagation = null; protected $backpropagation = null;
/**
* @var int
*/
private $inputLayerFeatures;
/**
* @var array
*/
private $hiddenLayers = [];
/**
* @var float
*/
private $learningRate;
/** /**
* @throws InvalidArgumentException * @throws InvalidArgumentException
*/ */
@ -78,18 +73,6 @@ abstract class MultilayerPerceptron extends LayeredNetwork implements Estimator,
$this->initNetwork(); $this->initNetwork();
} }
private function initNetwork(): void
{
$this->addInputLayer($this->inputLayerFeatures);
$this->addNeuronLayers($this->hiddenLayers, $this->activationFunction);
$this->addNeuronLayers([count($this->classes)], $this->activationFunction);
$this->addBiasNodes();
$this->generateSynapses();
$this->backpropagation = new Backpropagation($this->learningRate);
}
public function train(array $samples, array $targets): void public function train(array $samples, array $targets): void
{ {
$this->reset(); $this->reset();
@ -127,6 +110,18 @@ abstract class MultilayerPerceptron extends LayeredNetwork implements Estimator,
$this->removeLayers(); $this->removeLayers();
} }
private function initNetwork(): void
{
$this->addInputLayer($this->inputLayerFeatures);
$this->addNeuronLayers($this->hiddenLayers, $this->activationFunction);
$this->addNeuronLayers([count($this->classes)], $this->activationFunction);
$this->addBiasNodes();
$this->generateSynapses();
$this->backpropagation = new Backpropagation($this->learningRate);
}
private function addInputLayer(int $nodes): void private function addInputLayer(int $nodes): void
{ {
$this->addLayer(new Layer($nodes, Input::class)); $this->addLayer(new Layer($nodes, Input::class));

View File

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Phpml\NeuralNetwork\Node; namespace Phpml\NeuralNetwork\Node;
use Phpml\NeuralNetwork\ActivationFunction; use Phpml\NeuralNetwork\ActivationFunction;
use Phpml\NeuralNetwork\ActivationFunction\Sigmoid;
use Phpml\NeuralNetwork\Node; use Phpml\NeuralNetwork\Node;
use Phpml\NeuralNetwork\Node\Neuron\Synapse; use Phpml\NeuralNetwork\Node\Neuron\Synapse;
@ -13,7 +14,7 @@ class Neuron implements Node
/** /**
* @var Synapse[] * @var Synapse[]
*/ */
protected $synapses; protected $synapses = [];
/** /**
* @var ActivationFunction * @var ActivationFunction
@ -27,7 +28,7 @@ class Neuron implements Node
public function __construct(?ActivationFunction $activationFunction = null) public function __construct(?ActivationFunction $activationFunction = null)
{ {
$this->activationFunction = $activationFunction ?: new ActivationFunction\Sigmoid(); $this->activationFunction = $activationFunction ?: new Sigmoid();
$this->synapses = []; $this->synapses = [];
$this->output = 0; $this->output = 0;
} }
@ -47,7 +48,7 @@ class Neuron implements Node
public function getOutput(): float public function getOutput(): float
{ {
if (0 === $this->output) { if ($this->output === 0) {
$sum = 0; $sum = 0;
foreach ($this->synapses as $synapse) { foreach ($this->synapses as $synapse) {
$sum += $synapse->getOutput(); $sum += $synapse->getOutput();

View File

@ -27,11 +27,6 @@ class Synapse
$this->weight = $weight ?: $this->generateRandomWeight(); $this->weight = $weight ?: $this->generateRandomWeight();
} }
protected function generateRandomWeight() : float
{
return 1 / random_int(5, 25) * (random_int(0, 1) ? -1 : 1);
}
public function getOutput(): float public function getOutput(): float
{ {
return $this->weight * $this->node->getOutput(); return $this->weight * $this->node->getOutput();
@ -51,4 +46,9 @@ class Synapse
{ {
return $this->node; return $this->node;
} }
protected function generateRandomWeight(): float
{
return 1 / random_int(5, 25) * (random_int(0, 1) ? -1 : 1);
}
} }

View File

@ -47,6 +47,7 @@ class Backpropagation
} }
} }
} }
$this->prevSigmas = $this->sigmas; $this->prevSigmas = $this->sigmas;
} }
@ -65,6 +66,7 @@ class Backpropagation
if ($targetClass === $key) { if ($targetClass === $key) {
$value = 1; $value = 1;
} }
$sigma *= ($value - $neuronOutput); $sigma *= ($value - $neuronOutput);
} else { } else {
$sigma *= $this->getPrevSigma($neuron); $sigma *= $this->getPrevSigma($neuron);

View File

@ -9,7 +9,7 @@ class Pipeline implements Estimator
/** /**
* @var array|Transformer[] * @var array|Transformer[]
*/ */
private $transformers; private $transformers = [];
/** /**
* @var Estimator * @var Estimator

View File

@ -9,6 +9,7 @@ use Phpml\Preprocessing\Imputer\Strategy;
class Imputer implements Preprocessor class Imputer implements Preprocessor
{ {
public const AXIS_COLUMN = 0; public const AXIS_COLUMN = 0;
public const AXIS_ROW = 1; public const AXIS_ROW = 1;
/** /**
@ -66,7 +67,7 @@ class Imputer implements Preprocessor
private function getAxis(int $column, array $currentSample): array private function getAxis(int $column, array $currentSample): array
{ {
if (self::AXIS_ROW === $this->axis) { if ($this->axis === self::AXIS_ROW) {
return array_diff($currentSample, [$this->missingValue]); return array_diff($currentSample, [$this->missingValue]);
} }

View File

@ -11,7 +11,9 @@ use Phpml\Math\Statistic\StandardDeviation;
class Normalizer implements Preprocessor class Normalizer implements Preprocessor
{ {
public const NORM_L1 = 1; public const NORM_L1 = 1;
public const NORM_L2 = 2; public const NORM_L2 = 2;
public const NORM_STD = 3; public const NORM_STD = 3;
/** /**
@ -27,12 +29,12 @@ class Normalizer implements Preprocessor
/** /**
* @var array * @var array
*/ */
private $std; private $std = [];
/** /**
* @var array * @var array
*/ */
private $mean; private $mean = [];
/** /**
* @throws NormalizerException * @throws NormalizerException
@ -69,7 +71,7 @@ class Normalizer implements Preprocessor
$methods = [ $methods = [
self::NORM_L1 => 'normalizeL1', self::NORM_L1 => 'normalizeL1',
self::NORM_L2 => 'normalizeL2', self::NORM_L2 => 'normalizeL2',
self::NORM_STD => 'normalizeSTD' self::NORM_STD => 'normalizeSTD',
]; ];
$method = $methods[$this->norm]; $method = $methods[$this->norm];
@ -87,7 +89,7 @@ class Normalizer implements Preprocessor
$norm1 += abs($feature); $norm1 += abs($feature);
} }
if (0 == $norm1) { if ($norm1 == 0) {
$count = count($sample); $count = count($sample);
$sample = array_fill(0, $count, 1.0 / $count); $sample = array_fill(0, $count, 1.0 / $count);
} else { } else {
@ -103,9 +105,10 @@ class Normalizer implements Preprocessor
foreach ($sample as $feature) { foreach ($sample as $feature) {
$norm2 += $feature * $feature; $norm2 += $feature * $feature;
} }
$norm2 = sqrt((float) $norm2); $norm2 = sqrt((float) $norm2);
if (0 == $norm2) { if ($norm2 == 0) {
$sample = array_fill(0, count($sample), 1); $sample = array_fill(0, count($sample), 1);
} else { } else {
foreach ($sample as &$feature) { foreach ($sample as &$feature) {

View File

@ -28,7 +28,7 @@ class LeastSquares implements Regression
/** /**
* @var array * @var array
*/ */
private $coefficients; private $coefficients = [];
public function train(array $samples, array $targets): void public function train(array $samples, array $targets): void
{ {

View File

@ -167,7 +167,7 @@ class SupportVectorMachine
} }
/** /**
* @return array * @return array|string
*/ */
public function predict(array $samples) public function predict(array $samples)
{ {

View File

@ -7,6 +7,7 @@ namespace tests\Phpml\Classification;
use Phpml\Association\Apriori; use Phpml\Association\Apriori;
use Phpml\ModelManager; use Phpml\ModelManager;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use ReflectionClass;
class AprioriTest extends TestCase class AprioriTest extends TestCase
{ {
@ -172,7 +173,6 @@ class AprioriTest extends TestCase
/** /**
* Invokes objects method. Private/protected will be set accessible. * Invokes objects method. Private/protected will be set accessible.
* *
* @param object &$object Instantiated object to be called on
* @param string $method Method name to be called * @param string $method Method name to be called
* @param array $params Array of params to be passed * @param array $params Array of params to be passed
* *
@ -180,7 +180,7 @@ class AprioriTest extends TestCase
*/ */
public function invoke(&$object, $method, array $params = []) public function invoke(&$object, $method, array $params = [])
{ {
$reflection = new \ReflectionClass(get_class($object)); $reflection = new ReflectionClass(get_class($object));
$method = $reflection->getMethod($method); $method = $reflection->getMethod($method);
$method->setAccessible(true); $method->setAccessible(true);
@ -195,7 +195,7 @@ class AprioriTest extends TestCase
$testSamples = [['alpha', 'epsilon'], ['beta', 'theta']]; $testSamples = [['alpha', 'epsilon'], ['beta', 'theta']];
$predicted = $classifier->predict($testSamples); $predicted = $classifier->predict($testSamples);
$filename = 'apriori-test-'.rand(100, 999).'-'.uniqid(); $filename = 'apriori-test-'.random_int(100, 999).'-'.uniqid();
$filepath = tempnam(sys_get_temp_dir(), $filename); $filepath = tempnam(sys_get_temp_dir(), $filename);
$modelManager = new ModelManager(); $modelManager = new ModelManager();
$modelManager->saveToFile($classifier, $filepath); $modelManager->saveToFile($classifier, $filepath);

View File

@ -24,7 +24,7 @@ class DecisionTreeTest extends TestCase
['sunny', 75, 70, 'true', 'Play'], ['sunny', 75, 70, 'true', 'Play'],
['overcast', 72, 90, 'true', 'Play'], ['overcast', 72, 90, 'true', 'Play'],
['overcast', 81, 75, 'false', 'Play'], ['overcast', 81, 75, 'false', 'Play'],
['rain', 71, 80, 'true', 'Dont_play'] ['rain', 71, 80, 'true', 'Dont_play'],
]; ];
private $extraData = [ private $extraData = [
@ -32,16 +32,6 @@ class DecisionTreeTest extends TestCase
['scorching', 100, 93, 'true', 'Dont_play'], ['scorching', 100, 93, 'true', 'Dont_play'],
]; ];
private function getData($input)
{
$targets = array_column($input, 4);
array_walk($input, function (&$v): void {
array_splice($v, 4, 1);
});
return [$input, $targets];
}
public function testPredictSingleSample() public function testPredictSingleSample()
{ {
[$data, $targets] = $this->getData($this->data); [$data, $targets] = $this->getData($this->data);
@ -68,7 +58,7 @@ class DecisionTreeTest extends TestCase
$testSamples = [['sunny', 78, 72, 'false'], ['overcast', 60, 60, 'false']]; $testSamples = [['sunny', 78, 72, 'false'], ['overcast', 60, 60, 'false']];
$predicted = $classifier->predict($testSamples); $predicted = $classifier->predict($testSamples);
$filename = 'decision-tree-test-'.rand(100, 999).'-'.uniqid(); $filename = 'decision-tree-test-'.random_int(100, 999).'-'.uniqid();
$filepath = tempnam(sys_get_temp_dir(), $filename); $filepath = tempnam(sys_get_temp_dir(), $filename);
$modelManager = new ModelManager(); $modelManager = new ModelManager();
$modelManager->saveToFile($classifier, $filepath); $modelManager->saveToFile($classifier, $filepath);
@ -83,6 +73,16 @@ class DecisionTreeTest extends TestCase
[$data, $targets] = $this->getData($this->data); [$data, $targets] = $this->getData($this->data);
$classifier = new DecisionTree(5); $classifier = new DecisionTree(5);
$classifier->train($data, $targets); $classifier->train($data, $targets);
$this->assertTrue(5 >= $classifier->actualDepth); $this->assertTrue($classifier->actualDepth <= 5);
}
private function getData($input)
{
$targets = array_column($input, 4);
array_walk($input, function (&$v): void {
array_splice($v, 4, 1);
});
return [$input, $targets];
} }
} }

View File

@ -52,7 +52,7 @@ class AdaBoostTest extends TestCase
$testSamples = [[0, 1], [1, 1], [0.2, 0.1]]; $testSamples = [[0, 1], [1, 1], [0.2, 0.1]];
$predicted = $classifier->predict($testSamples); $predicted = $classifier->predict($testSamples);
$filename = 'adaboost-test-'.rand(100, 999).'-'.uniqid(); $filename = 'adaboost-test-'.random_int(100, 999).'-'.uniqid();
$filepath = tempnam(sys_get_temp_dir(), $filename); $filepath = tempnam(sys_get_temp_dir(), $filename);
$modelManager = new ModelManager(); $modelManager = new ModelManager();
$modelManager->saveToFile($classifier, $filepath); $modelManager->saveToFile($classifier, $filepath);

View File

@ -26,7 +26,7 @@ class BaggingTest extends TestCase
['sunny', 75, 70, 'true', 'Play'], ['sunny', 75, 70, 'true', 'Play'],
['overcast', 72, 90, 'true', 'Play'], ['overcast', 72, 90, 'true', 'Play'],
['overcast', 81, 75, 'false', 'Play'], ['overcast', 81, 75, 'false', 'Play'],
['rain', 71, 80, 'true', 'Dont_play'] ['rain', 71, 80, 'true', 'Dont_play'],
]; ];
private $extraData = [ private $extraData = [
@ -61,7 +61,7 @@ class BaggingTest extends TestCase
$testSamples = [['sunny', 78, 72, 'false'], ['overcast', 60, 60, 'false']]; $testSamples = [['sunny', 78, 72, 'false'], ['overcast', 60, 60, 'false']];
$predicted = $classifier->predict($testSamples); $predicted = $classifier->predict($testSamples);
$filename = 'bagging-test-'.rand(100, 999).'-'.uniqid(); $filename = 'bagging-test-'.random_int(100, 999).'-'.uniqid();
$filepath = tempnam(sys_get_temp_dir(), $filename); $filepath = tempnam(sys_get_temp_dir(), $filename);
$modelManager = new ModelManager(); $modelManager = new ModelManager();
$modelManager->saveToFile($classifier, $filepath); $modelManager->saveToFile($classifier, $filepath);
@ -105,7 +105,7 @@ class BaggingTest extends TestCase
{ {
return [ return [
DecisionTree::class => ['depth' => 5], DecisionTree::class => ['depth' => 5],
NaiveBayes::class => [] NaiveBayes::class => [],
]; ];
} }
@ -117,6 +117,7 @@ class BaggingTest extends TestCase
for ($i = 0; $i < 20; ++$i) { for ($i = 0; $i < 20; ++$i) {
$populated = array_merge($populated, $input); $populated = array_merge($populated, $input);
} }
shuffle($populated); shuffle($populated);
$targets = array_column($populated, 4); $targets = array_column($populated, 4);
array_walk($populated, function (&$v): void { array_walk($populated, function (&$v): void {

View File

@ -7,9 +7,20 @@ namespace tests\Phpml\Classification\Ensemble;
use Phpml\Classification\DecisionTree; use Phpml\Classification\DecisionTree;
use Phpml\Classification\Ensemble\RandomForest; use Phpml\Classification\Ensemble\RandomForest;
use Phpml\Classification\NaiveBayes; use Phpml\Classification\NaiveBayes;
use Throwable;
class RandomForestTest extends BaggingTest class RandomForestTest extends BaggingTest
{ {
public function testOtherBaseClassifier(): void
{
try {
$classifier = new RandomForest();
$classifier->setClassifer(NaiveBayes::class);
$this->assertEquals(0, 1);
} catch (Throwable $ex) {
$this->assertEquals(1, 1);
}
}
protected function getClassifier($numBaseClassifiers = 50) protected function getClassifier($numBaseClassifiers = 50)
{ {
$classifier = new RandomForest($numBaseClassifiers); $classifier = new RandomForest($numBaseClassifiers);
@ -22,15 +33,4 @@ class RandomForestTest extends BaggingTest
{ {
return [DecisionTree::class => ['depth' => 5]]; return [DecisionTree::class => ['depth' => 5]];
} }
public function testOtherBaseClassifier(): void
{
try {
$classifier = new RandomForest();
$classifier->setClassifer(NaiveBayes::class);
$this->assertEquals(0, 1);
} catch (\Exception $ex) {
$this->assertEquals(1, 1);
}
}
} }

View File

@ -73,7 +73,7 @@ class KNearestNeighborsTest extends TestCase
$classifier->train($trainSamples, $trainLabels); $classifier->train($trainSamples, $trainLabels);
$predicted = $classifier->predict($testSamples); $predicted = $classifier->predict($testSamples);
$filename = 'knearest-neighbors-test-'.rand(100, 999).'-'.uniqid(); $filename = 'knearest-neighbors-test-'.random_int(100, 999).'-'.uniqid();
$filepath = tempnam(sys_get_temp_dir(), $filename); $filepath = tempnam(sys_get_temp_dir(), $filename);
$modelManager = new ModelManager(); $modelManager = new ModelManager();
$modelManager->saveToFile($classifier, $filepath); $modelManager->saveToFile($classifier, $filepath);

View File

@ -35,7 +35,7 @@ class AdalineTest extends TestCase
$samples = [ $samples = [
[0, 0], [0, 1], [1, 0], [1, 1], // First group : a cluster at bottom-left corner in 2D [0, 0], [0, 1], [1, 0], [1, 1], // First group : a cluster at bottom-left corner in 2D
[5, 5], [6, 5], [5, 6], [7, 5], // Second group: another cluster at the middle-right [5, 5], [6, 5], [5, 6], [7, 5], // Second group: another cluster at the middle-right
[3, 10],[3, 10],[3, 8], [3, 9] // Third group : cluster at the top-middle [3, 10], [3, 10], [3, 8], [3, 9], // Third group : cluster at the top-middle
]; ];
$targets = [0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2]; $targets = [0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2];
@ -55,7 +55,7 @@ class AdalineTest extends TestCase
$samples = [ $samples = [
[0, 0], [0, 1], [1, 0], [1, 1], // First group : a cluster at bottom-left corner in 2D [0, 0], [0, 1], [1, 0], [1, 1], // First group : a cluster at bottom-left corner in 2D
[5, 5], [6, 5], [5, 6], [7, 5], // Second group: another cluster at the middle-right [5, 5], [6, 5], [5, 6], [7, 5], // Second group: another cluster at the middle-right
[3, 10],[3, 10],[3, 8], [3, 9] // Third group : cluster at the top-middle [3, 10], [3, 10], [3, 8], [3, 9], // Third group : cluster at the top-middle
]; ];
$targets = [2, 2, 2, 2, 0, 0, 0, 0, 1, 1, 1, 1]; $targets = [2, 2, 2, 2, 0, 0, 0, 0, 1, 1, 1, 1];
$classifier->train($samples, $targets); $classifier->train($samples, $targets);
@ -74,7 +74,7 @@ class AdalineTest extends TestCase
$testSamples = [[0, 1], [1, 1], [0.2, 0.1]]; $testSamples = [[0, 1], [1, 1], [0.2, 0.1]];
$predicted = $classifier->predict($testSamples); $predicted = $classifier->predict($testSamples);
$filename = 'adaline-test-'.rand(100, 999).'-'.uniqid(); $filename = 'adaline-test-'.random_int(100, 999).'-'.uniqid();
$filepath = tempnam(sys_get_temp_dir(), $filename); $filepath = tempnam(sys_get_temp_dir(), $filename);
$modelManager = new ModelManager(); $modelManager = new ModelManager();
$modelManager->saveToFile($classifier, $filepath); $modelManager->saveToFile($classifier, $filepath);

View File

@ -40,7 +40,7 @@ class DecisionStumpTest extends TestCase
$samples = [ $samples = [
[0, 0], [0, 1], [1, 0], [1, 1], // First group : a cluster at bottom-left corner in 2D [0, 0], [0, 1], [1, 0], [1, 1], // First group : a cluster at bottom-left corner in 2D
[5, 5], [6, 5], [5, 6], [7, 5], // Second group: another cluster at the middle-right [5, 5], [6, 5], [5, 6], [7, 5], // Second group: another cluster at the middle-right
[3, 10],[3, 10],[3, 8], [3, 9] // Third group : cluster at the top-middle [3, 10], [3, 10], [3, 8], [3, 9], // Third group : cluster at the top-middle
]; ];
$targets = [0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2]; $targets = [0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2];
@ -63,7 +63,7 @@ class DecisionStumpTest extends TestCase
$testSamples = [[0, 1], [1, 1], [0.2, 0.1]]; $testSamples = [[0, 1], [1, 1], [0.2, 0.1]];
$predicted = $classifier->predict($testSamples); $predicted = $classifier->predict($testSamples);
$filename = 'dstump-test-'.rand(100, 999).'-'.uniqid(); $filename = 'dstump-test-'.random_int(100, 999).'-'.uniqid();
$filepath = tempnam(sys_get_temp_dir(), $filename); $filepath = tempnam(sys_get_temp_dir(), $filename);
$modelManager = new ModelManager(); $modelManager = new ModelManager();
$modelManager->saveToFile($classifier, $filepath); $modelManager->saveToFile($classifier, $filepath);

View File

@ -37,7 +37,7 @@ class PerceptronTest extends TestCase
$samples = [ $samples = [
[0, 0], [0, 1], [1, 0], [1, 1], // First group : a cluster at bottom-left corner in 2D [0, 0], [0, 1], [1, 0], [1, 1], // First group : a cluster at bottom-left corner in 2D
[5, 5], [6, 5], [5, 6], [7, 5], // Second group: another cluster at the middle-right [5, 5], [6, 5], [5, 6], [7, 5], // Second group: another cluster at the middle-right
[3, 10],[3, 10],[3, 8], [3, 9] // Third group : cluster at the top-middle [3, 10], [3, 10], [3, 8], [3, 9], // Third group : cluster at the top-middle
]; ];
$targets = [0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2]; $targets = [0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2];
@ -58,7 +58,7 @@ class PerceptronTest extends TestCase
$samples = [ $samples = [
[0, 0], [0, 1], [1, 0], [1, 1], // First group : a cluster at bottom-left corner in 2D [0, 0], [0, 1], [1, 0], [1, 1], // First group : a cluster at bottom-left corner in 2D
[5, 5], [6, 5], [5, 6], [7, 5], // Second group: another cluster at the middle-right [5, 5], [6, 5], [5, 6], [7, 5], // Second group: another cluster at the middle-right
[3, 10],[3, 10],[3, 8], [3, 9] // Third group : cluster at the top-middle [3, 10], [3, 10], [3, 8], [3, 9], // Third group : cluster at the top-middle
]; ];
$targets = [2, 2, 2, 2, 0, 0, 0, 0, 1, 1, 1, 1]; $targets = [2, 2, 2, 2, 0, 0, 0, 0, 1, 1, 1, 1];
$classifier->train($samples, $targets); $classifier->train($samples, $targets);
@ -77,7 +77,7 @@ class PerceptronTest extends TestCase
$testSamples = [[0, 1], [1, 1], [0.2, 0.1]]; $testSamples = [[0, 1], [1, 1], [0.2, 0.1]];
$predicted = $classifier->predict($testSamples); $predicted = $classifier->predict($testSamples);
$filename = 'perceptron-test-'.rand(100, 999).'-'.uniqid(); $filename = 'perceptron-test-'.random_int(100, 999).'-'.uniqid();
$filepath = tempnam(sys_get_temp_dir(), $filename); $filepath = tempnam(sys_get_temp_dir(), $filename);
$modelManager = new ModelManager(); $modelManager = new ModelManager();
$modelManager->saveToFile($classifier, $filepath); $modelManager->saveToFile($classifier, $filepath);

View File

@ -150,7 +150,7 @@ class MLPClassifierTest extends TestCase
$testSamples = [[0, 0], [1, 0], [0, 1], [1, 1]]; $testSamples = [[0, 0], [1, 0], [0, 1], [1, 1]];
$predicted = $classifier->predict($testSamples); $predicted = $classifier->predict($testSamples);
$filename = 'perceptron-test-'.rand(100, 999).'-'.uniqid(); $filename = 'perceptron-test-'.random_int(100, 999).'-'.uniqid();
$filepath = tempnam(sys_get_temp_dir(), $filename); $filepath = tempnam(sys_get_temp_dir(), $filename);
$modelManager = new ModelManager(); $modelManager = new ModelManager();
$modelManager->saveToFile($classifier, $filepath); $modelManager->saveToFile($classifier, $filepath);

View File

@ -59,7 +59,7 @@ class NaiveBayesTest extends TestCase
$classifier->train($trainSamples, $trainLabels); $classifier->train($trainSamples, $trainLabels);
$predicted = $classifier->predict($testSamples); $predicted = $classifier->predict($testSamples);
$filename = 'naive-bayes-test-'.rand(100, 999).'-'.uniqid(); $filename = 'naive-bayes-test-'.random_int(100, 999).'-'.uniqid();
$filepath = tempnam(sys_get_temp_dir(), $filename); $filepath = tempnam(sys_get_temp_dir(), $filename);
$modelManager = new ModelManager(); $modelManager = new ModelManager();
$modelManager->saveToFile($classifier, $filepath); $modelManager->saveToFile($classifier, $filepath);

View File

@ -57,7 +57,7 @@ class SVCTest extends TestCase
$classifier->train($trainSamples, $trainLabels); $classifier->train($trainSamples, $trainLabels);
$predicted = $classifier->predict($testSamples); $predicted = $classifier->predict($testSamples);
$filename = 'svc-test-'.rand(100, 999).'-'.uniqid(); $filename = 'svc-test-'.random_int(100, 999).'-'.uniqid();
$filepath = tempnam(sys_get_temp_dir(), $filename); $filepath = tempnam(sys_get_temp_dir(), $filename);
$modelManager = new ModelManager(); $modelManager = new ModelManager();
$modelManager->saveToFile($classifier, $filepath); $modelManager->saveToFile($classifier, $filepath);

View File

@ -34,10 +34,25 @@ class DBSCANTest extends TestCase
public function testDBSCANSamplesClusteringAssociative(): void public function testDBSCANSamplesClusteringAssociative(): void
{ {
$samples = ['a' => [1, 1], 'b' => [9, 9], 'c' => [1, 2], 'd' => [9, 8], 'e' => [7, 7], 'f' => [8, 7]]; $samples = [
'a' => [1, 1],
'b' => [9, 9],
'c' => [1, 2],
'd' => [9, 8],
'e' => [7, 7],
'f' => [8, 7],
];
$clustered = [ $clustered = [
['a' => [1, 1], 'c' => [1, 2]], [
['b' => [9, 9], 'd' => [9, 8], 'e' => [7, 7], 'f' => [8, 7]], 'a' => [1, 1],
'c' => [1, 2],
],
[
'b' => [9, 9],
'd' => [9, 8],
'e' => [7, 7],
'f' => [8, 7],
],
]; ];
$dbscan = new DBSCAN($epsilon = 3, $minSamples = 2); $dbscan = new DBSCAN($epsilon = 3, $minSamples = 2);

View File

@ -20,6 +20,7 @@ class FuzzyCMeansTest extends TestCase
unset($samples[$index]); unset($samples[$index]);
} }
} }
$this->assertCount(0, $samples); $this->assertCount(0, $samples);
return $fcm; return $fcm;
@ -35,6 +36,7 @@ class FuzzyCMeansTest extends TestCase
foreach ($matrix as $row) { foreach ($matrix as $row) {
$this->assertCount($sampleCount, $row); $this->assertCount($sampleCount, $row);
} }
// Transpose of the matrix // Transpose of the matrix
array_unshift($matrix, null); array_unshift($matrix, null);
$matrix = call_user_func_array('array_map', $matrix); $matrix = call_user_func_array('array_map', $matrix);

View File

@ -23,6 +23,7 @@ class KMeansTest extends TestCase
unset($samples[$index]); unset($samples[$index]);
} }
} }
$this->assertCount(0, $samples); $this->assertCount(0, $samples);
} }

View File

@ -21,7 +21,7 @@ class KernelPCATest extends TestCase
[1., 2.5], [1., 2.7], [1., 3.], [1, 3], [1., 2.5], [1., 2.7], [1., 3.], [1, 3],
[1, 2], [1.5, 2], [1.5, 2.2], [1.3, 1.7], [1, 2], [1.5, 2], [1.5, 2.2], [1.3, 1.7],
[1.7, 1.3], [1.5, 1.5], [1.5, 1.6], [1.6, 2], [1.7, 1.3], [1.5, 1.5], [1.5, 1.6], [1.6, 2],
[1.7,2.1], [1.3,1.3], [1.3,2.2], [1.4,2.4] [1.7, 2.1], [1.3, 1.3], [1.3, 2.2], [1.4, 2.4],
]; ];
$transformed = [ $transformed = [
[0.016485613899708], [-0.089805657741674], [-0.088695974245924], [-0.069761503810802], [0.016485613899708], [-0.089805657741674], [-0.088695974245924], [-0.069761503810802],
@ -29,7 +29,7 @@ class KernelPCATest extends TestCase
[-0.10098315410297], [-0.15617881000654], [-0.21266832077299], [-0.21266832077299], [-0.10098315410297], [-0.15617881000654], [-0.21266832077299], [-0.21266832077299],
[-0.039234518840831], [0.40858295942991], [0.40110375047242], [-0.10555116296691], [-0.039234518840831], [0.40858295942991], [0.40110375047242], [-0.10555116296691],
[-0.13128352866095], [-0.20865959471756], [-0.17531601535848], [0.4240660966961], [-0.13128352866095], [-0.20865959471756], [-0.17531601535848], [0.4240660966961],
[0.36351946685163], [-0.14334173054136], [0.22454914091011], [0.15035027480881]]; [0.36351946685163], [-0.14334173054136], [0.22454914091011], [0.15035027480881], ];
$kpca = new KernelPCA(KernelPCA::KERNEL_RBF, null, 1, 15); $kpca = new KernelPCA(KernelPCA::KERNEL_RBF, null, 1, 15);
$reducedData = $kpca->fit($data); $reducedData = $kpca->fit($data);

View File

@ -28,7 +28,7 @@ class LDATest extends TestCase
[4.7, 3.2, 1.3, 0.2], [4.7, 3.2, 1.3, 0.2],
[6.5, 3.0, 5.2, 2.0], [6.5, 3.0, 5.2, 2.0],
[6.2, 3.4, 5.4, 2.3], [6.2, 3.4, 5.4, 2.3],
[5.9, 3.0, 5.1, 1.8] [5.9, 3.0, 5.1, 1.8],
]; ];
$transformed2 = [ $transformed2 = [
[-1.4922092756753, 1.9047102045574], [-1.4922092756753, 1.9047102045574],
@ -36,7 +36,7 @@ class LDATest extends TestCase
[-1.3487505965419, 1.749846351699], [-1.3487505965419, 1.749846351699],
[1.7759343101456, 2.0371552314006], [1.7759343101456, 2.0371552314006],
[2.0059819019159, 2.4493123003226], [2.0059819019159, 2.4493123003226],
[1.701474913008, 1.9037880473772] [1.701474913008, 1.9037880473772],
]; ];
$control = []; $control = [];

View File

@ -26,12 +26,12 @@ class PCATest extends TestCase
[2.0, 1.6], [2.0, 1.6],
[1.0, 1.1], [1.0, 1.1],
[1.5, 1.6], [1.5, 1.6],
[1.1, 0.9] [1.1, 0.9],
]; ];
$transformed = [ $transformed = [
[-0.827970186], [1.77758033], [-0.992197494], [-0.827970186], [1.77758033], [-0.992197494],
[-0.274210416], [-1.67580142], [-0.912949103], [0.0991094375], [-0.274210416], [-1.67580142], [-0.912949103], [0.0991094375],
[1.14457216], [0.438046137], [1.22382056]]; [1.14457216], [0.438046137], [1.22382056], ];
$pca = new PCA(0.90); $pca = new PCA(0.90);
$reducedData = $pca->fit($data); $reducedData = $pca->fit($data);

View File

@ -14,13 +14,41 @@ class TfIdfTransformerTest extends TestCase
// https://en.wikipedia.org/wiki/Tf-idf // https://en.wikipedia.org/wiki/Tf-idf
$samples = [ $samples = [
[0 => 1, 1 => 1, 2 => 2, 3 => 1, 4 => 0, 5 => 0], [
[0 => 1, 1 => 1, 2 => 0, 3 => 0, 4 => 2, 5 => 3], 0 => 1,
1 => 1,
2 => 2,
3 => 1,
4 => 0,
5 => 0,
],
[
0 => 1,
1 => 1,
2 => 0,
3 => 0,
4 => 2,
5 => 3,
],
]; ];
$tfIdfSamples = [ $tfIdfSamples = [
[0 => 0, 1 => 0, 2 => 0.602, 3 => 0.301, 4 => 0, 5 => 0], [
[0 => 0, 1 => 0, 2 => 0, 3 => 0, 4 => 0.602, 5 => 0.903], 0 => 0,
1 => 0,
2 => 0.602,
3 => 0.301,
4 => 0,
5 => 0,
],
[
0 => 0,
1 => 0,
2 => 0,
3 => 0,
4 => 0.602,
5 => 0.903,
],
]; ];
$transformer = new TfIdfTransformer($samples); $transformer = new TfIdfTransformer($samples);

View File

@ -33,9 +33,42 @@ class TokenCountVectorizerTest extends TestCase
]; ];
$tokensCounts = [ $tokensCounts = [
[0 => 1, 1 => 1, 2 => 2, 3 => 1, 4 => 1, 5 => 0, 6 => 0, 7 => 0, 8 => 0, 9 => 0], [
[0 => 0, 1 => 1, 2 => 1, 3 => 0, 4 => 0, 5 => 1, 6 => 1, 7 => 0, 8 => 0, 9 => 0], 0 => 1,
[0 => 0, 1 => 0, 2 => 0, 3 => 0, 4 => 0, 5 => 1, 6 => 0, 7 => 2, 8 => 1, 9 => 1], 1 => 1,
2 => 2,
3 => 1,
4 => 1,
5 => 0,
6 => 0,
7 => 0,
8 => 0,
9 => 0,
],
[
0 => 0,
1 => 1,
2 => 1,
3 => 0,
4 => 0,
5 => 1,
6 => 1,
7 => 0,
8 => 0,
9 => 0,
],
[
0 => 0,
1 => 0,
2 => 0,
3 => 0,
4 => 0,
5 => 1,
6 => 0,
7 => 2,
8 => 1,
9 => 1,
],
]; ];
$vectorizer = new TokenCountVectorizer(new WhitespaceTokenizer()); $vectorizer = new TokenCountVectorizer(new WhitespaceTokenizer());
@ -66,10 +99,34 @@ class TokenCountVectorizerTest extends TestCase
]; ];
$tokensCounts = [ $tokensCounts = [
[0 => 1, 1 => 1, 2 => 0, 3 => 1, 4 => 1], [
[0 => 1, 1 => 1, 2 => 0, 3 => 1, 4 => 1], 0 => 1,
[0 => 0, 1 => 1, 2 => 0, 3 => 1, 4 => 1], 1 => 1,
[0 => 0, 1 => 1, 2 => 0, 3 => 1, 4 => 1], 2 => 0,
3 => 1,
4 => 1,
],
[
0 => 1,
1 => 1,
2 => 0,
3 => 1,
4 => 1,
],
[
0 => 0,
1 => 1,
2 => 0,
3 => 1,
4 => 1,
],
[
0 => 0,
1 => 1,
2 => 0,
3 => 1,
4 => 1,
],
]; ];
$vectorizer = new TokenCountVectorizer(new WhitespaceTokenizer(), null, 0.5); $vectorizer = new TokenCountVectorizer(new WhitespaceTokenizer(), null, 0.5);
@ -88,9 +145,39 @@ class TokenCountVectorizerTest extends TestCase
]; ];
$tokensCounts = [ $tokensCounts = [
[0 => 1, 1 => 0, 2 => 0, 3 => 0, 4 => 0, 5 => 0, 6 => 0, 7 => 0, 8 => 0], [
[0 => 1, 1 => 0, 2 => 0, 3 => 0, 4 => 0, 5 => 0, 6 => 0, 7 => 0, 8 => 0], 0 => 1,
[0 => 1, 1 => 0, 2 => 0, 3 => 0, 4 => 0, 5 => 0, 6 => 0, 7 => 0, 8 => 0], 1 => 0,
2 => 0,
3 => 0,
4 => 0,
5 => 0,
6 => 0,
7 => 0,
8 => 0,
],
[
0 => 1,
1 => 0,
2 => 0,
3 => 0,
4 => 0,
5 => 0,
6 => 0,
7 => 0,
8 => 0,
],
[
0 => 1,
1 => 0,
2 => 0,
3 => 0,
4 => 0,
5 => 0,
6 => 0,
7 => 0,
8 => 0,
],
]; ];
$vectorizer = new TokenCountVectorizer(new WhitespaceTokenizer(), null, 1); $vectorizer = new TokenCountVectorizer(new WhitespaceTokenizer(), null, 1);
@ -124,9 +211,36 @@ class TokenCountVectorizerTest extends TestCase
]; ];
$tokensCounts = [ $tokensCounts = [
[0 => 1, 1 => 1, 2 => 1, 3 => 1, 4 => 0, 5 => 0, 6 => 0, 7 => 0], [
[0 => 0, 1 => 1, 2 => 0, 3 => 0, 4 => 1, 5 => 1, 6 => 0, 7 => 0], 0 => 1,
[0 => 0, 1 => 0, 2 => 0, 3 => 0, 4 => 1, 5 => 0, 6 => 1, 7 => 1], 1 => 1,
2 => 1,
3 => 1,
4 => 0,
5 => 0,
6 => 0,
7 => 0,
],
[
0 => 0,
1 => 1,
2 => 0,
3 => 0,
4 => 1,
5 => 1,
6 => 0,
7 => 0,
],
[
0 => 0,
1 => 0,
2 => 0,
3 => 0,
4 => 1,
5 => 0,
6 => 1,
7 => 1,
],
]; ];
$vectorizer = new TokenCountVectorizer(new WhitespaceTokenizer(), $stopWords); $vectorizer = new TokenCountVectorizer(new WhitespaceTokenizer(), $stopWords);

View File

@ -31,10 +31,7 @@ class ComparisonTest extends TestCase
Comparison::compare(1, 1, '~='); Comparison::compare(1, 1, '~=');
} }
/** public function provideData(): array
* @return array
*/
public function provideData()
{ {
return [ return [
// Greater // Greater

View File

@ -19,7 +19,7 @@ class EigenDecompositionTest extends TestCase
// http://www.cs.otago.ac.nz/cosc453/student_tutorials/principal_components.pdf // http://www.cs.otago.ac.nz/cosc453/student_tutorials/principal_components.pdf
$matrix = [ $matrix = [
[0.616555556, 0.615444444], [0.616555556, 0.615444444],
[0.614444444, 0.716555556] [0.614444444, 0.716555556],
]; ];
$knownEigvalues = [0.0490833989, 1.28402771]; $knownEigvalues = [0.0490833989, 1.28402771];
$knownEigvectors = [[-0.735178656, 0.677873399], [-0.677873399, -0.735178656]]; $knownEigvectors = [[-0.735178656, 0.677873399], [-0.677873399, -0.735178656]];
@ -43,7 +43,7 @@ class EigenDecompositionTest extends TestCase
if ($i > $k) { if ($i > $k) {
$A[$i][$k] = $A[$k][$i]; $A[$i][$k] = $A[$k][$i];
} else { } else {
$A[$i][$k] = rand(0, 10); $A[$i][$k] = random_int(0, 10);
} }
} }
} }

View File

@ -239,12 +239,12 @@ class MatrixTest extends TestCase
{ {
$array = [ $array = [
[1, 1, 1], [1, 1, 1],
[2, 2, 2] [2, 2, 2],
]; ];
$transposed = [ $transposed = [
[1, 2], [1, 2],
[1, 2], [1, 2],
[1, 2] [1, 2],
]; ];
$this->assertEquals($transposed, Matrix::transposeArray($array)); $this->assertEquals($transposed, Matrix::transposeArray($array));

View File

@ -6,6 +6,7 @@ namespace tests\Phpml\Math;
use Phpml\Math\Product; use Phpml\Math\Product;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use stdClass;
class ProductTest extends TestCase class ProductTest extends TestCase
{ {
@ -16,6 +17,6 @@ class ProductTest extends TestCase
$this->assertEquals(8, Product::scalar([2], [4])); $this->assertEquals(8, Product::scalar([2], [4]));
//test for non numeric values //test for non numeric values
$this->assertEquals(0, Product::scalar(['', null, [], new \stdClass()], [null])); $this->assertEquals(0, Product::scalar(['', null, [], new stdClass()], [null]));
} }
} }

View File

@ -13,7 +13,7 @@ class SetTest extends TestCase
{ {
$union = Set::union(new Set([3, 1]), new Set([3, 2, 2])); $union = Set::union(new Set([3, 1]), new Set([3, 2, 2]));
$this->assertInstanceOf('\Phpml\Math\Set', $union); $this->assertInstanceOf(Set::class, $union);
$this->assertEquals(new Set([1, 2, 3]), $union); $this->assertEquals(new Set([1, 2, 3]), $union);
$this->assertEquals(3, $union->cardinality()); $this->assertEquals(3, $union->cardinality());
} }
@ -22,7 +22,7 @@ class SetTest extends TestCase
{ {
$intersection = Set::intersection(new Set(['C', 'A']), new Set(['B', 'C'])); $intersection = Set::intersection(new Set(['C', 'A']), new Set(['B', 'C']));
$this->assertInstanceOf('\Phpml\Math\Set', $intersection); $this->assertInstanceOf(Set::class, $intersection);
$this->assertEquals(new Set(['C']), $intersection); $this->assertEquals(new Set(['C']), $intersection);
$this->assertEquals(1, $intersection->cardinality()); $this->assertEquals(1, $intersection->cardinality());
} }
@ -31,7 +31,7 @@ class SetTest extends TestCase
{ {
$difference = Set::difference(new Set(['C', 'A', 'B']), new Set(['A'])); $difference = Set::difference(new Set(['C', 'A', 'B']), new Set(['A']));
$this->assertInstanceOf('\Phpml\Math\Set', $difference); $this->assertInstanceOf(Set::class, $difference);
$this->assertEquals(new Set(['B', 'C']), $difference); $this->assertEquals(new Set(['B', 'C']), $difference);
$this->assertEquals(2, $difference->cardinality()); $this->assertEquals(2, $difference->cardinality());
} }

View File

@ -31,7 +31,7 @@ class CovarianceTest extends TestCase
]; ];
$knownCovariance = [ $knownCovariance = [
[0.616555556, 0.615444444], [0.616555556, 0.615444444],
[0.615444444, 0.716555556]]; [0.615444444, 0.716555556], ];
$x = array_column($matrix, 0); $x = array_column($matrix, 0);
$y = array_column($matrix, 1); $y = array_column($matrix, 1);

View File

@ -16,11 +16,31 @@ class ClassificationReportTest extends TestCase
$report = new ClassificationReport($labels, $predicted); $report = new ClassificationReport($labels, $predicted);
$precision = ['cat' => 0.5, 'ant' => 0.0, 'bird' => 1.0]; $precision = [
$recall = ['cat' => 1.0, 'ant' => 0.0, 'bird' => 0.67]; 'cat' => 0.5,
$f1score = ['cat' => 0.67, 'ant' => 0.0, 'bird' => 0.80]; 'ant' => 0.0,
$support = ['cat' => 1, 'ant' => 1, 'bird' => 3]; 'bird' => 1.0,
$average = ['precision' => 0.75, 'recall' => 0.83, 'f1score' => 0.73]; ];
$recall = [
'cat' => 1.0,
'ant' => 0.0,
'bird' => 0.67,
];
$f1score = [
'cat' => 0.67,
'ant' => 0.0,
'bird' => 0.80,
];
$support = [
'cat' => 1,
'ant' => 1,
'bird' => 3,
];
$average = [
'precision' => 0.75,
'recall' => 0.83,
'f1score' => 0.73,
];
$this->assertEquals($precision, $report->getPrecision(), '', 0.01); $this->assertEquals($precision, $report->getPrecision(), '', 0.01);
$this->assertEquals($recall, $report->getRecall(), '', 0.01); $this->assertEquals($recall, $report->getRecall(), '', 0.01);
@ -36,11 +56,31 @@ class ClassificationReportTest extends TestCase
$report = new ClassificationReport($labels, $predicted); $report = new ClassificationReport($labels, $predicted);
$precision = [0 => 0.5, 1 => 0.0, 2 => 1.0]; $precision = [
$recall = [0 => 1.0, 1 => 0.0, 2 => 0.67]; 0 => 0.5,
$f1score = [0 => 0.67, 1 => 0.0, 2 => 0.80]; 1 => 0.0,
$support = [0 => 1, 1 => 1, 2 => 3]; 2 => 1.0,
$average = ['precision' => 0.75, 'recall' => 0.83, 'f1score' => 0.73]; ];
$recall = [
0 => 1.0,
1 => 0.0,
2 => 0.67,
];
$f1score = [
0 => 0.67,
1 => 0.0,
2 => 0.80,
];
$support = [
0 => 1,
1 => 1,
2 => 3,
];
$average = [
'precision' => 0.75,
'recall' => 0.83,
'f1score' => 0.73,
];
$this->assertEquals($precision, $report->getPrecision(), '', 0.01); $this->assertEquals($precision, $report->getPrecision(), '', 0.01);
$this->assertEquals($recall, $report->getRecall(), '', 0.01); $this->assertEquals($recall, $report->getRecall(), '', 0.01);
@ -56,7 +96,10 @@ class ClassificationReportTest extends TestCase
$report = new ClassificationReport($labels, $predicted); $report = new ClassificationReport($labels, $predicted);
$this->assertEquals([1 => 0.0, 2 => 0.5], $report->getPrecision(), '', 0.01); $this->assertEquals([
1 => 0.0,
2 => 0.5,
], $report->getPrecision(), '', 0.01);
} }
public function testPreventDivideByZeroWhenTruePositiveAndFalseNegativeSumEqualsZero(): void public function testPreventDivideByZeroWhenTruePositiveAndFalseNegativeSumEqualsZero(): void
@ -66,7 +109,11 @@ class ClassificationReportTest extends TestCase
$report = new ClassificationReport($labels, $predicted); $report = new ClassificationReport($labels, $predicted);
$this->assertEquals([1 => 0.0, 2 => 1, 3 => 0], $report->getPrecision(), '', 0.01); $this->assertEquals([
1 => 0.0,
2 => 1,
3 => 0,
], $report->getPrecision(), '', 0.01);
} }
public function testPreventDividedByZeroWhenPredictedLabelsAllNotMatch(): void public function testPreventDividedByZeroWhenPredictedLabelsAllNotMatch(): void

View File

@ -19,10 +19,7 @@ class BinaryStepTest extends TestCase
$this->assertEquals($expected, $binaryStep->compute($value)); $this->assertEquals($expected, $binaryStep->compute($value));
} }
/** public function binaryStepProvider(): array
* @return array
*/
public function binaryStepProvider()
{ {
return [ return [
[1, 1], [1, 1],

View File

@ -19,10 +19,7 @@ class GaussianTest extends TestCase
$this->assertEquals($expected, $gaussian->compute($value), '', 0.001); $this->assertEquals($expected, $gaussian->compute($value), '', 0.001);
} }
/** public function gaussianProvider(): array
* @return array
*/
public function gaussianProvider()
{ {
return [ return [
[0.367, 1], [0.367, 1],

View File

@ -19,10 +19,7 @@ class HyperboliTangentTest extends TestCase
$this->assertEquals($expected, $tanh->compute($value), '', 0.001); $this->assertEquals($expected, $tanh->compute($value), '', 0.001);
} }
/** public function tanhProvider(): array
* @return array
*/
public function tanhProvider()
{ {
return [ return [
[1.0, 0.761, 1], [1.0, 0.761, 1],

View File

@ -19,10 +19,7 @@ class PReLUTest extends TestCase
$this->assertEquals($expected, $prelu->compute($value), '', 0.001); $this->assertEquals($expected, $prelu->compute($value), '', 0.001);
} }
/** public function preluProvider(): array
* @return array
*/
public function preluProvider()
{ {
return [ return [
[0.01, 0.367, 0.367], [0.01, 0.367, 0.367],

View File

@ -19,10 +19,7 @@ class SigmoidTest extends TestCase
$this->assertEquals($expected, $sigmoid->compute($value), '', 0.001); $this->assertEquals($expected, $sigmoid->compute($value), '', 0.001);
} }
/** public function sigmoidProvider(): array
* @return array
*/
public function sigmoidProvider()
{ {
return [ return [
[1.0, 1, 7.25], [1.0, 1, 7.25],

View File

@ -19,16 +19,13 @@ class ThresholdedReLUTest extends TestCase
$this->assertEquals($expected, $thresholdedReLU->compute($value)); $this->assertEquals($expected, $thresholdedReLU->compute($value));
} }
/** public function thresholdProvider(): array
* @return array
*/
public function thresholdProvider()
{ {
return [ return [
[1.0, 0, 1.0], [1.0, 0, 1.0],
[0.5, 3.75, 3.75], [0.5, 3.75, 3.75],
[0.0, 0.5, 0.5], [0.0, 0.5, 0.5],
[0.9, 0, 0.1] [0.9, 0, 0.1],
]; ];
} }
} }

View File

@ -8,6 +8,7 @@ use Phpml\NeuralNetwork\Layer;
use Phpml\NeuralNetwork\Node\Bias; use Phpml\NeuralNetwork\Node\Bias;
use Phpml\NeuralNetwork\Node\Neuron; use Phpml\NeuralNetwork\Node\Neuron;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use stdClass;
class LayerTest extends TestCase class LayerTest extends TestCase
{ {
@ -43,7 +44,7 @@ class LayerTest extends TestCase
*/ */
public function testThrowExceptionOnInvalidNodeClass(): void public function testThrowExceptionOnInvalidNodeClass(): void
{ {
new Layer(1, \stdClass::class); new Layer(1, stdClass::class);
} }
public function testAddNodesToLayer(): void public function testAddNodesToLayer(): void

View File

@ -44,10 +44,7 @@ class LayeredNetworkTest extends TestCase
$this->assertEquals([0.5], $network->getOutput()); $this->assertEquals([0.5], $network->getOutput());
} }
/** private function getLayeredNetworkMock(): LayeredNetwork
* @return LayeredNetwork
*/
private function getLayeredNetworkMock()
{ {
return $this->getMockForAbstractClass(LayeredNetwork::class); return $this->getMockForAbstractClass(LayeredNetwork::class);
} }

Some files were not shown because too many files have changed in this diff Show More