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/
humbuglog.*
/bin/phpunit
.coverage
.php_cs.cache
/bin/php-cs-fixer
/bin/coveralls
/build

View File

@ -6,7 +6,7 @@ matrix:
include:
- os: linux
php: '7.1'
env: DISABLE_XDEBUG="true"
env: DISABLE_XDEBUG="true" STATIC_ANALYSIS="true"
- os: linux
php: '7.2'
@ -21,7 +21,7 @@ matrix:
before_install:
- 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:
- 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
script:
- bin/phpunit $PHPUNIT_FLAGS
- vendor/bin/phpunit $PHPUNIT_FLAGS
- if [[ $STATIC_ANALYSIS != "" ]]; then vendor/bin/ecs check src tests; fi
after_success:
- |
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

View File

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

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"
beStrictAboutChangesToGlobalState="true"
>
<testsuites>
<testsuite name="PHP-ML Test Suite">
<directory>tests/*</directory>
</testsuite>
</testsuites>
<filter>
<whitelist processUncoveredFilesFromWhitelist="true">

View File

@ -31,7 +31,7 @@ class Apriori implements Associator
*
* @var mixed[][][]
*/
private $large;
private $large = [];
/**
* Minimum relative frequency of transactions.
@ -45,7 +45,7 @@ class Apriori implements Associator
*
* @var mixed[][]
*/
private $rules;
private $rules = [];
/**
* Apriori constructor.
@ -133,7 +133,8 @@ class Apriori implements Associator
private function generateRules(array $frequent): void
{
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));
$this->rules[] = [
self::ARRAY_KEY_ANTECEDENT => $antecedent,

View File

@ -15,22 +15,18 @@ class DecisionTree implements Classifier
use Trainable, Predictable;
public const CONTINUOUS = 1;
public const NOMINAL = 2;
/**
* @var array
*/
protected $columnTypes;
/**
* @var array
*/
private $labels = [];
/**
* @var int
*/
private $featureCount = 0;
public $actualDepth = 0;
/**
* @var array
*/
protected $columnTypes = [];
/**
* @var DecisionTreeLeaf
@ -42,10 +38,15 @@ class DecisionTree implements Classifier
*/
protected $maxDepth;
/**
* @var array
*/
private $labels = [];
/**
* @var int
*/
public $actualDepth = 0;
private $featureCount = 0;
/**
* @var int
@ -55,7 +56,7 @@ class DecisionTree implements Classifier
/**
* @var array
*/
private $selectedFeatures;
private $selectedFeatures = [];
/**
* @var array
@ -113,6 +114,121 @@ class DecisionTree implements Classifier
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
{
$split = $this->getBestSplit($records);
@ -136,6 +252,7 @@ class DecisionTree implements Classifier
if ($prevRecord && $prevRecord != $record) {
$allSame = false;
}
$prevRecord = $record;
// According to the split criteron, this record will
@ -163,6 +280,7 @@ class DecisionTree implements Classifier
if ($leftRecords) {
$split->leftLeaf = $this->getSplitLeaf($leftRecords, $depth + 1);
}
if ($rightRecords) {
$split->rightLeaf = $this->getSplitLeaf($rightRecords, $depth + 1);
}
@ -184,6 +302,7 @@ class DecisionTree implements Classifier
foreach ($samples as $index => $row) {
$colValues[$index] = $row[$i];
}
$counts = array_count_values($colValues);
arsort($counts);
$baseValue = key($counts);
@ -242,6 +361,7 @@ class DecisionTree implements Classifier
if ($numFeatures > $this->featureCount) {
$numFeatures = $this->featureCount;
}
shuffle($allFeatures);
$selectedFeatures = array_slice($allFeatures, 0, $numFeatures, false);
sort($selectedFeatures);
@ -249,38 +369,6 @@ class DecisionTree implements Classifier
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
{
// Detect and convert continuous data column values into
@ -298,8 +386,10 @@ class DecisionTree implements Classifier
}
}
}
$columns[] = $values;
}
// Below method is a strange yet very simple & efficient method
// to get the transpose of a 2D array
return array_map(null, ...$columns);
@ -329,28 +419,6 @@ class DecisionTree implements Classifier
return count($distinctValues) <= $count / 5;
}
/**
* This method is used to set number of columns to be used
* when deciding a split at an internal node of the tree. <br>
* If the value is given 0, then all features are used (default behaviour),
* otherwise the given value will be used as a maximum for number of columns
* randomly selected for each split operation.
*
* @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
*/
@ -359,66 +427,6 @@ class DecisionTree implements Classifier
$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
* column as a split criterion

View File

@ -71,6 +71,14 @@ class DecisionTreeLeaf
*/
public $level = 0;
/**
* HTML representation of the tree without column names
*/
public function __toString(): string
{
return $this->getHTML();
}
public function evaluate(array $record): bool
{
$recordField = $record[$this->columnIndex];
@ -154,12 +162,4 @@ class DecisionTreeLeaf
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;
use Exception;
use Phpml\Classification\Classifier;
use Phpml\Classification\Linear\DecisionStump;
use Phpml\Classification\WeightedClassifier;
@ -11,6 +12,7 @@ use Phpml\Helper\Predictable;
use Phpml\Helper\Trainable;
use Phpml\Math\Statistic\Mean;
use Phpml\Math\Statistic\StandardDeviation;
use ReflectionClass;
class AdaBoost implements Classifier
{
@ -98,11 +100,14 @@ class AdaBoost implements Classifier
// Initialize usual variables
$this->labels = array_keys(array_count_values($targets));
if (count($this->labels) != 2) {
throw new \Exception('AdaBoost is a binary classifier and can classify between two classes only');
throw new Exception('AdaBoost is a binary classifier and can classify between two classes only');
}
// Set all target values to either -1 or 1
$this->labels = [1 => $this->labels[0], -1 => $this->labels[1]];
$this->labels = [
1 => $this->labels[0],
-1 => $this->labels[1],
];
foreach ($targets as $target) {
$this->targets[] = $target == $this->labels[1] ? 1 : -1;
}
@ -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
* consideration of current sample weights
*/
protected function getBestClassifier(): Classifier
{
$ref = new \ReflectionClass($this->baseClassifier);
$ref = new ReflectionClass($this->baseClassifier);
if ($this->classifierOptions) {
$classifier = $ref->newInstanceArgs($this->classifierOptions);
} else {
@ -173,9 +192,10 @@ class AdaBoost implements Classifier
foreach ($weights as $index => $weight) {
$z = (int) round(($weight - $mean) / $std) - $minZ + 1;
for ($i = 0; $i < $z; ++$i) {
if (rand(0, 1) == 0) {
if (random_int(0, 1) == 0) {
continue;
}
$samples[] = $this->samples[$index];
$targets[] = $this->targets[$index];
}
@ -231,18 +251,4 @@ class AdaBoost implements Classifier
$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;
use Exception;
use Phpml\Classification\Classifier;
use Phpml\Classification\DecisionTree;
use Phpml\Helper\Predictable;
use Phpml\Helper\Trainable;
use ReflectionClass;
class Bagging implements Classifier
{
@ -18,11 +20,6 @@ class Bagging implements Classifier
*/
protected $numSamples;
/**
* @var array
*/
private $targets = [];
/**
* @var int
*/
@ -46,13 +43,18 @@ class Bagging implements Classifier
/**
* @var array
*/
protected $classifiers;
protected $classifiers = [];
/**
* @var float
*/
protected $subsetRatio = 0.7;
/**
* @var array
*/
private $targets = [];
/**
* @var array
*/
@ -80,7 +82,7 @@ class Bagging implements Classifier
public function setSubsetRatio(float $ratio)
{
if ($ratio < 0.1 || $ratio > 1.0) {
throw new \Exception('Subset ratio should be between 0.1 and 1.0');
throw new Exception('Subset ratio should be between 0.1 and 1.0');
}
$this->subsetRatio = $ratio;
@ -130,7 +132,7 @@ class Bagging implements Classifier
srand($index);
$bootstrapSize = $this->subsetRatio * $this->numSamples;
for ($i = 0; $i < $bootstrapSize; ++$i) {
$rand = rand(0, $this->numSamples - 1);
$rand = random_int(0, $this->numSamples - 1);
$samples[] = $this->samples[$rand];
$targets[] = $this->targets[$rand];
}
@ -142,7 +144,7 @@ class Bagging implements Classifier
{
$classifiers = [];
for ($i = 0; $i < $this->numClassifier; ++$i) {
$ref = new \ReflectionClass($this->classifier);
$ref = new ReflectionClass($this->classifier);
if ($this->classifierOptions) {
$obj = $ref->newInstanceArgs($this->classifierOptions);
} else {
@ -155,12 +157,7 @@ class Bagging implements Classifier
return $classifiers;
}
/**
* @param Classifier $classifier
*
* @return Classifier
*/
protected function initSingleClassifier($classifier)
protected function initSingleClassifier(Classifier $classifier): Classifier
{
return $classifier;
}

View File

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

View File

@ -28,7 +28,7 @@ class KNearestNeighbors implements Classifier
*/
public function __construct(int $k = 3, ?Distance $distanceMetric = null)
{
if (null === $distanceMetric) {
if ($distanceMetric === null) {
$distanceMetric = new Euclidean();
}

View File

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

View File

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Phpml\Classification\Linear;
use Exception;
use Phpml\Classification\DecisionTree;
use Phpml\Classification\WeightedClassifier;
use Phpml\Helper\OneVsRest;
@ -24,7 +25,7 @@ class DecisionStump extends WeightedClassifier
/**
* @var array
*/
protected $binaryLabels;
protected $binaryLabels = [];
/**
* Lowest error rate obtained while training/optimizing the model
@ -51,7 +52,7 @@ class DecisionStump extends WeightedClassifier
/**
* @var array
*/
protected $columnTypes;
protected $columnTypes = [];
/**
* @var int
@ -68,7 +69,7 @@ class DecisionStump extends WeightedClassifier
*
* @var array
*/
protected $prob;
protected $prob = [];
/**
* A DecisionStump classifier is a one-level deep DecisionTree. It is generally
@ -83,6 +84,25 @@ class DecisionStump extends WeightedClassifier
$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
*/
@ -101,7 +121,7 @@ class DecisionStump extends WeightedClassifier
if ($this->weights) {
$numWeights = count($this->weights);
if ($numWeights != count($samples)) {
throw new \Exception('Number of sample weights does not match with number of samples');
throw new Exception('Number of sample weights does not match with number of samples');
}
} else {
$this->weights = array_fill(0, count($samples), 1);
@ -118,9 +138,12 @@ class DecisionStump extends WeightedClassifier
}
$bestSplit = [
'value' => 0, 'operator' => '',
'prob' => [], 'column' => 0,
'trainingErrorRate' => 1.0];
'value' => 0,
'operator' => '',
'prob' => [],
'column' => 0,
'trainingErrorRate' => 1.0,
];
foreach ($columns as $col) {
if ($this->columnTypes[$col] == DecisionTree::CONTINUOUS) {
$split = $this->getBestNumericalSplit($samples, $targets, $col);
@ -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
*/
@ -173,9 +184,13 @@ class DecisionStump extends WeightedClassifier
$threshold = array_sum($values) / (float) count($values);
[$errorRate, $prob] = $this->calculateErrorRate($targets, $threshold, $operator, $values);
if ($split == null || $errorRate < $split['trainingErrorRate']) {
$split = ['value' => $threshold, 'operator' => $operator,
'prob' => $prob, 'column' => $col,
'trainingErrorRate' => $errorRate];
$split = [
'value' => $threshold,
'operator' => $operator,
'prob' => $prob,
'column' => $col,
'trainingErrorRate' => $errorRate,
];
}
// Try other possible points one by one
@ -183,9 +198,13 @@ class DecisionStump extends WeightedClassifier
$threshold = (float) $step;
[$errorRate, $prob] = $this->calculateErrorRate($targets, $threshold, $operator, $values);
if ($errorRate < $split['trainingErrorRate']) {
$split = ['value' => $threshold, 'operator' => $operator,
'prob' => $prob, 'column' => $col,
'trainingErrorRate' => $errorRate];
$split = [
'value' => $threshold,
'operator' => $operator,
'prob' => $prob,
'column' => $col,
'trainingErrorRate' => $errorRate,
];
}
}// for
}
@ -206,9 +225,13 @@ class DecisionStump extends WeightedClassifier
[$errorRate, $prob] = $this->calculateErrorRate($targets, $val, $operator, $values);
if ($split == null || $split['trainingErrorRate'] < $errorRate) {
$split = ['value' => $val, 'operator' => $operator,
'prob' => $prob, 'column' => $col,
'trainingErrorRate' => $errorRate];
$split = [
'value' => $val,
'operator' => $operator,
'prob' => $prob,
'column' => $col,
'trainingErrorRate' => $errorRate,
];
}
}
}
@ -242,6 +265,7 @@ class DecisionStump extends WeightedClassifier
if (!isset($prob[$predicted][$target])) {
$prob[$predicted][$target] = 0;
}
++$prob[$predicted][$target];
}
@ -292,11 +316,4 @@ class DecisionStump extends WeightedClassifier
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;
use Closure;
use Exception;
use Phpml\Helper\Optimizer\ConjugateGradient;
class LogisticRegression extends Adaline
@ -70,18 +72,18 @@ class LogisticRegression extends Adaline
) {
$trainingTypes = range(self::BATCH_TRAINING, self::CONJUGATE_GRAD_TRAINING);
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) '.
'or conjugate batch (conjugate gradients) algorithms');
}
if (!in_array($cost, ['log', 'sse'])) {
throw new \Exception("Logistic regression cost function can be one of the following: \n".
throw new Exception("Logistic regression cost function can be one of the following: \n".
"'log' for log-likelihood and 'sse' for sum of squared errors");
}
if ($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;
@ -132,14 +134,14 @@ class LogisticRegression extends Adaline
return $this->runConjugateGradient($samples, $targets, $callback);
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
*/
protected function runConjugateGradient(array $samples, array $targets, \Closure $gradientFunc): void
protected function runConjugateGradient(array $samples, array $targets, Closure $gradientFunc): void
{
if (empty($this->optimizer)) {
$this->optimizer = (new ConjugateGradient($this->featureCount))
@ -155,7 +157,7 @@ class LogisticRegression extends Adaline
*
* @throws \Exception
*/
protected function getCostFunction() : \Closure
protected function getCostFunction(): Closure
{
$penalty = 0;
if ($this->penalty == 'L2') {
@ -183,9 +185,11 @@ class LogisticRegression extends Adaline
if ($hX == 1) {
$hX = 1 - 1e-10;
}
if ($hX == 0) {
$hX = 1e-10;
}
$error = -$y * log($hX) - (1 - $y) * log(1 - $hX);
$gradient = $hX - $y;
@ -218,16 +222,14 @@ class LogisticRegression extends Adaline
return $callback;
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
*
* @return float
*/
protected function output(array $sample)
protected function output(array $sample): float
{
$sum = parent::output($sample);
@ -253,7 +255,7 @@ class LogisticRegression extends Adaline
*
* The probability is simply taken as the distance of the sample
* to the decision plane.
*
* @param mixed $label
*/
protected function predictProbability(array $sample, $label): float

View File

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

View File

@ -14,7 +14,9 @@ class NaiveBayes implements Classifier
use Trainable, Predictable;
public const CONTINUOS = 1;
public const NOMINAL = 2;
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
* values in private array in order to avoid repeated calculation
@ -119,6 +146,7 @@ class NaiveBayes implements Classifier
return $this->discreteProb[$label][$feature][$value];
}
$std = $this->std[$label][$feature] ;
$mean = $this->mean[$label][$feature];
// Calculate the probability density by use of normal/Gaussian distribution
@ -148,28 +176,4 @@ class NaiveBayes implements Classifier
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
*/
protected $weights;
protected $weights = [];
/**
* Sets the array including a weight for each sample

View File

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Phpml\Clustering;
use array_merge;
use Phpml\Math\Distance;
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)
{
if (null === $distanceMetric) {
if ($distanceMetric === null) {
$distanceMetric = new Euclidean();
}
@ -44,6 +45,7 @@ class DBSCAN implements Clusterer
if (isset($visited[$index])) {
continue;
}
$visited[$index] = true;
$regionSamples = $this->getSamplesInRegion($sample, $samples);
@ -84,7 +86,8 @@ class DBSCAN implements Clusterer
$cluster[$index] = $sample;
}
$cluster = \array_merge($cluster, ...$clusterMerge);
$cluster = array_merge($cluster, ...$clusterMerge);
return $cluster;
}

View File

@ -30,7 +30,7 @@ class FuzzyCMeans implements Clusterer
/**
* @var array|float[][]
*/
private $membership;
private $membership = [];
/**
* @var float
@ -55,7 +55,7 @@ class FuzzyCMeans implements Clusterer
/**
* @var array
*/
private $samples;
private $samples = [];
/**
* @throws InvalidArgumentException
@ -65,12 +65,63 @@ class FuzzyCMeans implements Clusterer
if ($clustersNumber <= 0) {
throw InvalidArgumentException::invalidClustersNumber();
}
$this->clustersNumber = $clustersNumber;
$this->fuzziness = $fuzziness;
$this->epsilon = $epsilon;
$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
{
// Membership array is a matrix of cluster number by sample counts
@ -87,7 +138,7 @@ class FuzzyCMeans implements Clusterer
$row = [];
$total = 0.0;
for ($k = 0; $k < $cols; ++$k) {
$val = rand(1, 5) / 10.0;
$val = random_int(1, 5) / 10.0;
$row[] = $val;
$total += $val;
}
@ -187,54 +238,4 @@ class FuzzyCMeans implements Clusterer
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
{
public const INIT_RANDOM = 1;
public const INIT_KMEANS_PLUS_PLUS = 2;
/**

View File

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

View File

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

View File

@ -177,18 +177,6 @@ class Space extends SplObjectStorage
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
{
$clusters = [];
@ -218,4 +206,16 @@ class Space extends SplObjectStorage
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)
{
if (0 >= $testSize || 1 <= $testSize) {
if ($testSize <= 0 || $testSize >= 1) {
throw InvalidArgumentException::percentNotInRange('testSize');
}
$this->seedGenerator($seed);
$this->splitDataset($dataset, $testSize);
}
abstract protected function splitDataset(Dataset $dataset, float $testSize);
public function getTrainSamples(): array
{
return $this->trainSamples;
@ -61,9 +60,11 @@ abstract class Split
return $this->testLabels;
}
abstract protected function splitDataset(Dataset $dataset, float $testSize);
protected function seedGenerator(?int $seed = null): void
{
if (null === $seed) {
if ($seed === null) {
mt_srand();
} else {
mt_srand($seed);

View File

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

View File

@ -4,6 +4,8 @@ declare(strict_types=1);
namespace Phpml\DimensionReduction;
use Closure;
use Exception;
use Phpml\Math\Distance\Euclidean;
use Phpml\Math\Distance\Manhattan;
use Phpml\Math\Matrix;
@ -11,8 +13,11 @@ use Phpml\Math\Matrix;
class KernelPCA extends PCA
{
public const KERNEL_RBF = 1;
public const KERNEL_SIGMOID = 2;
public const KERNEL_LAPLACIAN = 3;
public const KERNEL_LINEAR = 4;
/**
@ -34,7 +39,7 @@ class KernelPCA extends PCA
*
* @var array
*/
protected $data;
protected $data = [];
/**
* 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];
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);
@ -88,6 +93,27 @@ class KernelPCA extends PCA
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>
* 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
*/
protected function getKernel(): \Closure
protected function getKernel(): Closure
{
switch ($this->kernel) {
case self::KERNEL_LINEAR:
@ -173,7 +199,7 @@ class KernelPCA extends PCA
};
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 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;
use Exception;
use Phpml\Math\Matrix;
class LDA extends EigenTransformerBase
@ -16,22 +17,22 @@ class LDA extends EigenTransformerBase
/**
* @var array
*/
public $labels;
public $labels = [];
/**
* @var array
*/
public $means;
public $means = [];
/**
* @var array
*/
public $counts;
public $counts = [];
/**
* @var float[]
*/
public $overallMean;
public $overallMean = [];
/**
* 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)
{
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) {
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) {
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) {
$this->numFeatures = $numFeatures;
}
if ($totalVariance !== null) {
$this->totalVariance = $totalVariance;
}
@ -86,6 +90,25 @@ class LDA extends EigenTransformerBase
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
*/
@ -113,6 +136,7 @@ class LDA extends EigenTransformerBase
if (!isset($means[$label][$col])) {
$means[$label][$col] = 0.0;
}
$means[$label][$col] += $val;
$overallMean[$col] += $val;
}
@ -195,23 +219,4 @@ class LDA extends EigenTransformerBase
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;
use Exception;
use Phpml\Math\Statistic\Covariance;
use Phpml\Math\Statistic\Mean;
@ -35,18 +36,21 @@ class PCA extends EigenTransformerBase
public function __construct(?float $totalVariance = null, ?int $numFeatures = null)
{
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) {
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) {
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) {
$this->numFeatures = $numFeatures;
}
if ($totalVariance !== null) {
$this->totalVariance = $totalVariance;
}
@ -73,6 +77,27 @@ class PCA extends EigenTransformerBase
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
{
// Calculate means for each dimension
@ -102,25 +127,4 @@ class PCA extends EigenTransformerBase
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;
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));
}

View File

@ -4,19 +4,21 @@ declare(strict_types=1);
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));
}
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));
}
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));
}

View File

@ -4,39 +4,41 @@ declare(strict_types=1);
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');
}
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));
}
public static function arrayCantBeEmpty() : InvalidArgumentException
public static function arrayCantBeEmpty(): self
{
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));
}
public static function matrixDimensionsDidNotMatch() : InvalidArgumentException
public static function matrixDimensionsDidNotMatch(): self
{
return new self('Matrix dimensions did not match');
}
public static function inconsistentMatrixSupplied() : InvalidArgumentException
public static function inconsistentMatrixSupplied(): self
{
return new self('Inconsistent matrix supplied');
}
public static function invalidClustersNumber() : InvalidArgumentException
public static function invalidClustersNumber(): self
{
return new self('Invalid clusters number');
}
@ -44,57 +46,57 @@ class InvalidArgumentException extends \Exception
/**
* @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));
}
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));
}
public static function invalidLayerNodeClass() : InvalidArgumentException
public static function invalidLayerNodeClass(): self
{
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');
}
public static function invalidClassesNumber() : InvalidArgumentException
public static function invalidClassesNumber(): self
{
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');
}
public static function fileNotFound(string $file) : InvalidArgumentException
public static function fileNotFound(string $file): self
{
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));
}
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));
}
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));
}
public static function invalidOperator(string $operator) : InvalidArgumentException
public static function invalidOperator(string $operator): self
{
return new self(sprintf('Invalid operator "%s" provided', $operator));
}

View File

@ -4,19 +4,21 @@ declare(strict_types=1);
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');
}
public static function columnOutOfRange() : MatrixException
public static function columnOutOfRange(): self
{
return new self('Column out of range');
}
public static function singularMatrix() : MatrixException
public static function singularMatrix(): self
{
return new self('Matrix is singular');
}

View File

@ -4,9 +4,11 @@ declare(strict_types=1);
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.');
}

View File

@ -4,14 +4,16 @@ declare(strict_types=1);
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));
}
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));
}

View File

@ -11,7 +11,7 @@ class StopWords
/**
* @var array
*/
protected $stopWords;
protected $stopWords = [];
public function __construct(array $stopWords)
{
@ -23,7 +23,7 @@ class StopWords
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";

View File

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

View File

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

View File

@ -36,6 +36,18 @@ trait OneVsRest
$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
{
// Overwrites the current value if it exist. $allLabels must be provided for each partialTrain run.
@ -44,6 +56,7 @@ trait OneVsRest
} else {
$this->allLabels = array_keys(array_count_values($targets));
}
sort($this->allLabels, SORT_STRING);
// 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.
*
@ -105,29 +106,6 @@ trait OneVsRest
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
*/
@ -155,8 +133,6 @@ trait OneVsRest
/**
* To be overwritten by OneVsRest classifiers.
*
* @return void
*/
abstract protected function resetBinary(): void;
@ -174,4 +150,27 @@ trait OneVsRest
* @return mixed
*/
abstract protected function predictSampleBinary(array $sample);
/**
* Groups all targets into two groups: Targets equal to
* the given label and the others
*
* $targets is not passed by reference nor contains objects so this method
* changes will not affect the caller $targets array.
*
* @param mixed $label
*
* @return array Binarized targets and target's labels
*/
private function binarizeTargets(array $targets, $label): array
{
$notLabel = "not_$label";
foreach ($targets as $key => $target) {
$targets[$key] = $target == $label ? $label : $notLabel;
}
$labels = [$label, $notLabel];
return [$targets, $labels];
}
}

View File

@ -4,6 +4,8 @@ declare(strict_types=1);
namespace Phpml\Helper\Optimizer;
use Closure;
/**
* Conjugate Gradient method to solve a non-linear f(x) with respect to unknown x
* See https://en.wikipedia.org/wiki/Nonlinear_conjugate_gradient_method)
@ -17,7 +19,7 @@ namespace Phpml\Helper\Optimizer;
*/
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->targets = $targets;
@ -25,7 +27,7 @@ class ConjugateGradient extends GD
$this->sampleCount = count($samples);
$this->costValues = [];
$d = mp::muls($this->gradient($this->theta), -1);
$d = MP::muls($this->gradient($this->theta), -1);
for ($i = 0; $i < $this->maxIterations; ++$i) {
// Obtain α that minimizes f(θ + α.d)
@ -96,8 +98,8 @@ class ConjugateGradient extends GD
$large = 0.01 * $d;
// Obtain θ + α.d for two initial values, x0 and x1
$x0 = mp::adds($this->theta, $small);
$x1 = mp::adds($this->theta, $large);
$x0 = MP::adds($this->theta, $small);
$x1 = MP::adds($this->theta, $large);
$epsilon = 0.0001;
$iteration = 0;
@ -113,9 +115,9 @@ class ConjugateGradient extends GD
if ($fx1 < $fx0) {
$x0 = $x1;
$x1 = mp::adds($x1, 0.01); // Enlarge second
$x1 = MP::adds($x1, 0.01); // Enlarge second
} else {
$x1 = mp::divs(mp::add($x1, $x0), 2.0);
$x1 = MP::divs(MP::add($x1, $x0), 2.0);
} // Get to the midpoint
$error = $fx1 / $this->dimensions;
@ -181,7 +183,7 @@ class ConjugateGradient extends GD
{
$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
* and vector-scalar variables
*/
class mp
class MP
{
/**
* 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;
use Closure;
/**
* Batch version of Gradient Descent to optimize the weights
* of a classifier given samples, targets and the objective function to minimize
@ -17,7 +19,7 @@ class GD extends StochasticGD
*/
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->targets = $targets;

View File

@ -4,6 +4,9 @@ declare(strict_types=1);
namespace Phpml\Helper\Optimizer;
use Closure;
use Exception;
abstract class Optimizer
{
/**
@ -11,7 +14,7 @@ abstract class Optimizer
*
* @var array
*/
protected $theta;
protected $theta = [];
/**
* Number of dimensions
@ -30,7 +33,7 @@ abstract class Optimizer
// Inits the weights randomly
$this->theta = [];
for ($i = 0; $i < $this->dimensions; ++$i) {
$this->theta[] = rand() / (float) getrandmax();
$this->theta[] = random_int(0, getrandmax()) / (float) getrandmax();
}
}
@ -44,7 +47,7 @@ abstract class Optimizer
public function setInitialTheta(array $theta)
{
if (count($theta) != $this->dimensions) {
throw new \Exception("Number of values in the weights array should be $this->dimensions");
throw new Exception("Number of values in the weights array should be $this->dimensions");
}
$this->theta = $theta;
@ -56,5 +59,5 @@ abstract class Optimizer
* Executes the optimization with the given samples & targets
* 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;
use Closure;
/**
* Stochastic Gradient Descent optimization method
* to find a solution for the equation A.ϴ = y where
@ -66,6 +68,7 @@ class StochasticGD extends Optimizer
* @var bool
*/
protected $enableEarlyStop = true;
/**
* List of values obtained by evaluating the cost function at each iteration
* 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
* 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->targets = $targets;
@ -181,6 +184,15 @@ class StochasticGD extends Optimizer
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
{
$jValue = 0.0;
@ -237,15 +249,6 @@ class StochasticGD extends Optimizer
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.
*/

View File

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

View File

@ -23,12 +23,11 @@ class RBF implements Kernel
* @param array $a
* @param array $b
*/
public function compute($a, $b)
public function compute($a, $b): float
{
$score = 2 * Product::scalar($a, $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
declare(strict_types=1);
/**
* Class to obtain eigenvalues and eigenvectors of a real matrix.
*
@ -54,6 +55,7 @@ class EigenvalueDecomposition
* @var array
*/
private $d = [];
private $e = [];
/**
@ -75,7 +77,7 @@ class EigenvalueDecomposition
*
* @var array
*/
private $ort;
private $ort = [];
/**
* Used for complex scalar division.
@ -83,6 +85,7 @@ class EigenvalueDecomposition
* @var float
*/
private $cdivr;
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.
*/
@ -158,6 +226,7 @@ class EigenvalueDecomposition
for ($j = 0; $j < $i; ++$j) {
$this->e[$j] = 0.0;
}
// Apply similarity transformation to remaining columns.
for ($j = 0; $j < $i; ++$j) {
$f = $this->d[$j];
@ -168,6 +237,7 @@ class EigenvalueDecomposition
$g += $this->V[$k][$j] * $this->d[$k];
$this->e[$k] += $this->V[$k][$j] * $f;
}
$this->e[$j] = $g;
}
@ -185,16 +255,19 @@ class EigenvalueDecomposition
for ($j = 0; $j < $i; ++$j) {
$this->e[$j] -= $hh * $this->d[$j];
}
for ($j = 0; $j < $i; ++$j) {
$f = $this->d[$j];
$g = $this->e[$j];
for ($k = $j; $k <= $i_; ++$k) {
$this->V[$k][$j] -= ($f * $this->e[$k] + $g * $this->d[$k]);
}
$this->d[$j] = $this->V[$i - 1][$j];
$this->V[$i][$j] = 0.0;
}
}
$this->d[$i] = $h;
}
@ -207,16 +280,19 @@ class EigenvalueDecomposition
for ($k = 0; $k <= $i; ++$k) {
$this->d[$k] = $this->V[$k][$i + 1] / $h;
}
for ($j = 0; $j <= $i; ++$j) {
$g = 0.0;
for ($k = 0; $k <= $i; ++$k) {
$g += $this->V[$k][$i + 1] * $this->V[$k][$j];
}
for ($k = 0; $k <= $i; ++$k) {
$this->V[$k][$j] -= $g * $this->d[$k];
}
}
}
for ($k = 0; $k <= $i; ++$k) {
$this->V[$k][$i + 1] = 0.0;
}
@ -241,6 +317,7 @@ class EigenvalueDecomposition
for ($i = 1; $i < $this->n; ++$i) {
$this->e[$i - 1] = $this->e[$i];
}
$this->e[$this->n - 1] = 0.0;
$f = 0.0;
$tst1 = 0.0;
@ -254,8 +331,10 @@ class EigenvalueDecomposition
if (abs($this->e[$m]) <= $eps * $tst1) {
break;
}
++$m;
}
// If m == l, $this->d[l] is an eigenvalue,
// otherwise, iterate.
if ($m > $l) {
@ -270,6 +349,7 @@ class EigenvalueDecomposition
if ($p < 0) {
$r *= -1;
}
$this->d[$l] = $this->e[$l] / ($p + $r);
$this->d[$l + 1] = $this->e[$l] * ($p + $r);
$dl1 = $this->d[$l + 1];
@ -277,6 +357,7 @@ class EigenvalueDecomposition
for ($i = $l + 2; $i < $this->n; ++$i) {
$this->d[$i] -= $h;
}
$f += $h;
// Implicit QL transformation.
$p = $this->d[$m];
@ -303,12 +384,14 @@ class EigenvalueDecomposition
$this->V[$k][$i] = $c * $this->V[$k][$i] - $s * $h;
}
}
$p = -$s * $s2 * $c3 * $el1 * $this->e[$l] / $dl1;
$this->e[$l] = $s * $p;
$this->d[$l] = $c * $p;
// Check for convergence.
} while (abs($this->e[$l]) > $eps * $tst1);
}
$this->d[$l] = $this->d[$l] + $f;
$this->e[$l] = 0.0;
}
@ -323,6 +406,7 @@ class EigenvalueDecomposition
$p = $this->d[$j];
}
}
if ($k != $i) {
$this->d[$k] = $this->d[$i];
$this->d[$i] = $p;
@ -354,6 +438,7 @@ class EigenvalueDecomposition
for ($i = $m; $i <= $high; ++$i) {
$scale = $scale + abs($this->H[$i][$m - 1]);
}
if ($scale != 0.0) {
// Compute Householder transformation.
$h = 0.0;
@ -361,10 +446,12 @@ class EigenvalueDecomposition
$this->ort[$i] = $this->H[$i][$m - 1] / $scale;
$h += $this->ort[$i] * $this->ort[$i];
}
$g = sqrt($h);
if ($this->ort[$m] > 0) {
$g *= -1;
}
$h -= $this->ort[$m] * $g;
$this->ort[$m] -= $g;
// Apply Householder similarity transformation
@ -374,21 +461,25 @@ class EigenvalueDecomposition
for ($i = $high; $i >= $m; --$i) {
$f += $this->ort[$i] * $this->H[$i][$j];
}
$f /= $h;
for ($i = $m; $i <= $high; ++$i) {
$this->H[$i][$j] -= $f * $this->ort[$i];
}
}
for ($i = 0; $i <= $high; ++$i) {
$f = 0.0;
for ($j = $high; $j >= $m; --$j) {
$f += $this->ort[$j] * $this->H[$i][$j];
}
$f = $f / $h;
for ($j = $m; $j <= $high; ++$j) {
$this->H[$i][$j] -= $f * $this->ort[$j];
}
}
$this->ort[$m] = $scale * $this->ort[$m];
$this->H[$m][$m - 1] = $scale * $g;
}
@ -400,16 +491,19 @@ class EigenvalueDecomposition
$this->V[$i][$j] = ($i == $j ? 1.0 : 0.0);
}
}
for ($m = $high - 1; $m >= $low + 1; --$m) {
if ($this->H[$m][$m - 1] != 0.0) {
for ($i = $m + 1; $i <= $high; ++$i) {
$this->ort[$i] = $this->H[$i][$m - 1];
}
for ($j = $m; $j <= $high; ++$j) {
$g = 0.0;
for ($i = $m; $i <= $high; ++$i) {
$g += $this->ort[$i] * $this->V[$i][$j];
}
// Double division avoids possible underflow
$g = ($g / $this->ort[$m]) / $this->H[$m][$m - 1];
for ($i = $m; $i <= $high; ++$i) {
@ -469,6 +563,7 @@ class EigenvalueDecomposition
$this->d[$i] = $this->H[$i][$i];
$this->e[$i] = 0.0;
}
for ($j = max($i - 1, 0); $j < $nn; ++$j) {
$norm = $norm + abs($this->H[$i][$j]);
}
@ -484,11 +579,14 @@ class EigenvalueDecomposition
if ($s == 0.0) {
$s = $norm;
}
if (abs($this->H[$l][$l - 1]) < $eps * $s) {
break;
}
--$l;
}
// Check for convergence
// One root found
if ($l == $n) {
@ -513,11 +611,13 @@ class EigenvalueDecomposition
} else {
$z = $p - $z;
}
$this->d[$n - 1] = $x + $z;
$this->d[$n] = $this->d[$n - 1];
if ($z != 0.0) {
$this->d[$n] = $x - $w / $z;
}
$this->e[$n - 1] = 0.0;
$this->e[$n] = 0.0;
$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][$j] = $q * $this->H[$n][$j] - $p * $z;
}
// Column modification
for ($i = 0; $i <= $n; ++$i) {
$z = $this->H[$i][$n - 1];
$this->H[$i][$n - 1] = $q * $z + $p * $this->H[$i][$n];
$this->H[$i][$n] = $q * $this->H[$i][$n] - $p * $z;
}
// Accumulate transformations
for ($i = $low; $i <= $high; ++$i) {
$z = $this->V[$i][$n - 1];
$this->V[$i][$n - 1] = $q * $z + $p * $this->V[$i][$n];
$this->V[$i][$n] = $q * $this->V[$i][$n] - $p * $z;
}
// Complex pair
} else {
$this->d[$n - 1] = $x + $p;
@ -552,6 +655,7 @@ class EigenvalueDecomposition
$this->e[$n - 1] = $z;
$this->e[$n] = -$z;
}
$n = $n - 2;
$iter = 0;
// No convergence yet
@ -564,16 +668,19 @@ class EigenvalueDecomposition
$y = $this->H[$n - 1][$n - 1];
$w = $this->H[$n][$n - 1] * $this->H[$n - 1][$n];
}
// Wilkinson's original ad hoc shift
if ($iter == 10) {
$exshift += $x;
for ($i = $low; $i <= $n; ++$i) {
$this->H[$i][$i] -= $x;
}
$s = abs($this->H[$n][$n - 1]) + abs($this->H[$n - 1][$n - 2]);
$x = $y = 0.75 * $s;
$w = -0.4375 * $s * $s;
}
// MATLAB's new ad hoc shift
if ($iter == 30) {
$s = ($y - $x) / 2.0;
@ -583,14 +690,17 @@ class EigenvalueDecomposition
if ($y < $x) {
$s = -$s;
}
$s = $x - $w / (($y - $x) / 2.0 + $s);
for ($i = $low; $i <= $n; ++$i) {
$this->H[$i][$i] -= $s;
}
$exshift += $s;
$x = $y = $w = 0.964;
}
}
// Could check iteration count here.
$iter = $iter + 1;
// Look for two consecutive small sub-diagonal elements
@ -609,18 +719,22 @@ class EigenvalueDecomposition
if ($m == $l) {
break;
}
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])))) {
break;
}
--$m;
}
for ($i = $m + 2; $i <= $n; ++$i) {
$this->H[$i][$i - 2] = 0.0;
if ($i > $m + 2) {
$this->H[$i][$i - 3] = 0.0;
}
}
// Double QR step involving rows l:n and columns m:n
for ($k = $m; $k <= $n - 1; ++$k) {
$notlast = ($k != $n - 1);
@ -635,19 +749,23 @@ class EigenvalueDecomposition
$r = $r / $x;
}
}
if ($x == 0.0) {
break;
}
$s = sqrt($p * $p + $q * $q + $r * $r);
if ($p < 0) {
$s = -$s;
}
if ($s != 0) {
if ($k != $m) {
$this->H[$k][$k - 1] = -$s * $x;
} elseif ($l != $m) {
$this->H[$k][$k - 1] = -$this->H[$k][$k - 1];
}
$p = $p + $s;
$x = $p / $s;
$y = $q / $s;
@ -661,9 +779,11 @@ class EigenvalueDecomposition
$p = $p + $r * $this->H[$k + 2][$j];
$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 + 1][$j] = $this->H[$k + 1][$j] - $p * $y;
}
// Column modification
for ($i = 0; $i <= min($n, $k + 3); ++$i) {
$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];
$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 + 1] = $this->H[$i][$k + 1] - $p * $q;
}
// Accumulate transformations
for ($i = $low; $i <= $high; ++$i) {
$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];
$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 + 1] = $this->V[$i][$k + 1] - $p * $q;
}
@ -719,6 +842,7 @@ class EigenvalueDecomposition
} else {
$this->H[$i][$n] = -$r / ($eps * $norm);
}
// Solve real equations
} else {
$x = $this->H[$i][$i + 1];
@ -732,6 +856,7 @@ class EigenvalueDecomposition
$this->H[$i + 1][$n] = (-$s - $y * $t) / $z;
}
}
// Overflow control
$t = abs($this->H[$i][$n]);
if (($eps * $t) * $t > 1) {
@ -741,6 +866,7 @@ class EigenvalueDecomposition
}
}
}
// Complex vector
} elseif ($q < 0) {
$l = $n - 1;
@ -753,6 +879,7 @@ class EigenvalueDecomposition
$this->H[$n - 1][$n - 1] = $this->cdivr;
$this->H[$n - 1][$n] = $this->cdivi;
}
$this->H[$n][$n - 1] = 0.0;
$this->H[$n][$n] = 1.0;
for ($i = $n - 2; $i >= 0; --$i) {
@ -763,6 +890,7 @@ class EigenvalueDecomposition
$ra = $ra + $this->H[$i][$j] * $this->H[$j][$n - 1];
$sa = $sa + $this->H[$i][$j] * $this->H[$j][$n];
}
$w = $this->H[$i][$i] - $p;
if ($this->e[$i] < 0.0) {
$z = $w;
@ -783,6 +911,7 @@ class EigenvalueDecomposition
if ($vr == 0.0 & $vi == 0.0) {
$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->H[$i][$n - 1] = $this->cdivr;
$this->H[$i][$n] = $this->cdivi;
@ -795,6 +924,7 @@ class EigenvalueDecomposition
$this->H[$i + 1][$n] = $this->cdivi;
}
}
// Overflow control
$t = max(abs($this->H[$i][$n - 1]), abs($this->H[$i][$n]));
if (($eps * $t) * $t > 1) {
@ -824,81 +954,9 @@ class EigenvalueDecomposition
for ($k = $low; $k <= min($j, $high); ++$k) {
$z = $z + $this->V[$i][$k] * $this->H[$k][$j];
}
$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
declare(strict_types=1);
/**
* @package JAMA
*
@ -90,6 +91,7 @@ class LUDecomposition
for ($i = 0; $i < $this->m; ++$i) {
$this->piv[$i] = $i;
}
$this->pivsign = 1;
$LUcolj = [];
@ -99,6 +101,7 @@ class LUDecomposition
for ($i = 0; $i < $this->m; ++$i) {
$LUcolj[$i] = &$this->LU[$i][$j];
}
// Apply previous transformations.
for ($i = 0; $i < $this->m; ++$i) {
$LUrowi = $this->LU[$i];
@ -108,8 +111,10 @@ class LUDecomposition
for ($k = 0; $k < $kmax; ++$k) {
$s += $LUrowi[$k] * $LUcolj[$k];
}
$LUrowi[$j] = $LUcolj[$i] -= $s;
}
// Find pivot and exchange if necessary.
$p = $j;
for ($i = $j + 1; $i < $this->m; ++$i) {
@ -117,17 +122,20 @@ class LUDecomposition
$p = $i;
}
}
if ($p != $j) {
for ($k = 0; $k < $this->n; ++$k) {
$t = $this->LU[$p][$k];
$this->LU[$p][$k] = $this->LU[$j][$k];
$this->LU[$j][$k] = $t;
}
$k = $this->piv[$p];
$this->piv[$p] = $this->piv[$j];
$this->piv[$j] = $k;
$this->pivsign = $this->pivsign * -1;
}
// Compute multipliers.
if (($j < $this->m) && ($this->LU[$j][$j] != 0.0)) {
for ($i = $j + 1; $i < $this->m; ++$i) {
@ -268,11 +276,13 @@ class LUDecomposition
}
}
}
// Solve U*X = Y;
for ($k = $this->n - 1; $k >= 0; --$k) {
for ($j = 0; $j < $nx; ++$j) {
$X[$k][$j] /= $this->LU[$k][$k];
}
for ($i = 0; $i < $k; ++$i) {
for ($j = 0; $j < $nx; ++$j) {
$X[$i][$j] -= $X[$k][$j] * $this->LU[$i][$k];

View File

@ -13,7 +13,7 @@ class Matrix
/**
* @var array
*/
private $matrix;
private $matrix = [];
/**
* @var int
@ -56,7 +56,7 @@ class Matrix
$this->matrix = $matrix;
}
public static function fromFlatArray(array $array) : Matrix
public static function fromFlatArray(array $array): self
{
$matrix = [];
foreach ($array as $value) {
@ -123,7 +123,7 @@ class Matrix
return $this->columns === $this->rows;
}
public function transpose() : Matrix
public function transpose(): self
{
if ($this->rows == 1) {
$matrix = array_map(function ($el) {
@ -136,7 +136,7 @@ class Matrix
return new self($matrix, false);
}
public function multiply(Matrix $matrix) : Matrix
public function multiply(self $matrix): self
{
if ($this->columns != $matrix->getRows()) {
throw InvalidArgumentException::inconsistentMatrixSupplied();
@ -157,7 +157,7 @@ class Matrix
return new self($product, false);
}
public function divideByScalar($value) : Matrix
public function divideByScalar($value): self
{
$newMatrix = [];
for ($i = 0; $i < $this->rows; ++$i) {
@ -169,7 +169,7 @@ class Matrix
return new self($newMatrix, false);
}
public function multiplyByScalar($value) : Matrix
public function multiplyByScalar($value): self
{
$newMatrix = [];
for ($i = 0; $i < $this->rows; ++$i) {
@ -184,7 +184,7 @@ class Matrix
/**
* 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);
}
@ -192,30 +192,12 @@ class Matrix
/**
* 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);
}
/**
* 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
public function inverse(): self
{
if (!$this->isSquare()) {
throw MatrixException::notSquareMatrix();
@ -228,20 +210,7 @@ class Matrix
return new self($inverse, false);
}
/**
* 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
public function crossOut(int $row, int $column): self
{
$newMatrix = [];
$r = 0;
@ -254,6 +223,7 @@ class Matrix
++$c;
}
}
++$r;
}
}
@ -263,7 +233,7 @@ class Matrix
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];
}
/**
* 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;
class Set implements \IteratorAggregate
use ArrayIterator;
use IteratorAggregate;
class Set implements IteratorAggregate
{
/**
* @var string[]|int[]|float[]
*/
private $elements;
private $elements = [];
/**
* @param string[]|int[]|float[] $elements
@ -22,7 +25,7 @@ class Set implements \IteratorAggregate
/**
* 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()));
}
@ -30,7 +33,7 @@ class Set implements \IteratorAggregate
/**
* 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()));
}
@ -38,7 +41,7 @@ class Set implements \IteratorAggregate
/**
* 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()));
}
@ -48,7 +51,7 @@ class Set implements \IteratorAggregate
*
* @return Set[]
*/
public static function cartesian(Set $a, Set $b) : array
public static function cartesian(self $a, self $b): array
{
$cartesian = [];
@ -66,7 +69,7 @@ class Set implements \IteratorAggregate
*
* @return Set[]
*/
public static function power(Set $a) : array
public static function power(self $a): array
{
$power = [new self()];
@ -79,24 +82,10 @@ class Set implements \IteratorAggregate
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
*/
public function add($element) : Set
public function add($element): self
{
return $this->addAll([$element]);
}
@ -104,7 +93,7 @@ class Set implements \IteratorAggregate
/**
* @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));
@ -114,7 +103,7 @@ class Set implements \IteratorAggregate
/**
* @param string|int|float $element
*/
public function remove($element) : Set
public function remove($element): self
{
return $this->removeAll([$element]);
}
@ -122,7 +111,7 @@ class Set implements \IteratorAggregate
/**
* @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));
@ -153,9 +142,9 @@ class Set implements \IteratorAggregate
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
@ -167,4 +156,18 @@ class Set implements \IteratorAggregate
{
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;
use Exception;
use Phpml\Exception\InvalidArgumentException;
class Covariance
@ -63,7 +64,7 @@ class Covariance
}
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) {
@ -92,10 +93,12 @@ class Covariance
if ($index == $i) {
$val[0] = $col - $meanX;
}
if ($index == $k) {
$val[1] = $col - $meanY;
}
}
$sum += $val[0] * $val[1];
}
}

View File

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

View File

@ -93,6 +93,7 @@ class ClassificationReport
$this->average[$metric] = 0.0;
continue;
}
$this->average[$metric] = array_sum($values) / count($values);
}
}
@ -102,7 +103,8 @@ class ClassificationReport
*/
private function computePrecision(int $truePositive, int $falsePositive)
{
if (0 == ($divider = $truePositive + $falsePositive)) {
$divider = $truePositive + $falsePositive;
if ($divider == 0) {
return 0.0;
}
@ -114,7 +116,8 @@ class ClassificationReport
*/
private function computeRecall(int $truePositive, int $falseNegative)
{
if (0 == ($divider = $truePositive + $falseNegative)) {
$divider = $truePositive + $falseNegative;
if ($divider == 0) {
return 0.0;
}
@ -123,7 +126,8 @@ class ClassificationReport
private function computeF1Score(float $precision, float $recall): float
{
if (0 == ($divider = $precision + $recall)) {
$divider = $precision + $recall;
if ($divider == 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
{
$this->nodes[] = $node;
@ -54,4 +40,16 @@ class Layer
{
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
*
* @return self
*/
public function setInput($input);
public function setInput($input): self;
/**
* @return array
*/
public function getOutput(): array;
public function addLayer(Layer $layer);

View File

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

View File

@ -20,41 +20,36 @@ abstract class MultilayerPerceptron extends LayeredNetwork implements Estimator,
{
use Predictable;
/**
* @var int
*/
private $inputLayerFeatures;
/**
* @var array
*/
private $hiddenLayers;
/**
* @var array
*/
protected $classes = [];
/**
* @var int
*/
private $iterations;
/**
* @var ActivationFunction
*/
protected $activationFunction;
/**
* @var float
*/
private $learningRate;
/**
* @var Backpropagation
*/
protected $backpropagation = null;
/**
* @var int
*/
private $inputLayerFeatures;
/**
* @var array
*/
private $hiddenLayers = [];
/**
* @var float
*/
private $learningRate;
/**
* @throws InvalidArgumentException
*/
@ -78,18 +73,6 @@ abstract class MultilayerPerceptron extends LayeredNetwork implements Estimator,
$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
{
$this->reset();
@ -127,6 +110,18 @@ abstract class MultilayerPerceptron extends LayeredNetwork implements Estimator,
$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
{
$this->addLayer(new Layer($nodes, Input::class));

View File

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

View File

@ -27,11 +27,6 @@ class Synapse
$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
{
return $this->weight * $this->node->getOutput();
@ -51,4 +46,9 @@ class Synapse
{
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;
}
@ -65,6 +66,7 @@ class Backpropagation
if ($targetClass === $key) {
$value = 1;
}
$sigma *= ($value - $neuronOutput);
} else {
$sigma *= $this->getPrevSigma($neuron);

View File

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

View File

@ -9,6 +9,7 @@ use Phpml\Preprocessing\Imputer\Strategy;
class Imputer implements Preprocessor
{
public const AXIS_COLUMN = 0;
public const AXIS_ROW = 1;
/**
@ -66,7 +67,7 @@ class Imputer implements Preprocessor
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]);
}

View File

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

View File

@ -28,7 +28,7 @@ class LeastSquares implements Regression
/**
* @var array
*/
private $coefficients;
private $coefficients = [];
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)
{

View File

@ -7,6 +7,7 @@ namespace tests\Phpml\Classification;
use Phpml\Association\Apriori;
use Phpml\ModelManager;
use PHPUnit\Framework\TestCase;
use ReflectionClass;
class AprioriTest extends TestCase
{
@ -172,7 +173,6 @@ class AprioriTest extends TestCase
/**
* 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 array $params Array of params to be passed
*
@ -180,7 +180,7 @@ class AprioriTest extends TestCase
*/
public function invoke(&$object, $method, array $params = [])
{
$reflection = new \ReflectionClass(get_class($object));
$reflection = new ReflectionClass(get_class($object));
$method = $reflection->getMethod($method);
$method->setAccessible(true);
@ -195,7 +195,7 @@ class AprioriTest extends TestCase
$testSamples = [['alpha', 'epsilon'], ['beta', 'theta']];
$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);
$modelManager = new ModelManager();
$modelManager->saveToFile($classifier, $filepath);

View File

@ -24,7 +24,7 @@ class DecisionTreeTest extends TestCase
['sunny', 75, 70, 'true', 'Play'],
['overcast', 72, 90, 'true', 'Play'],
['overcast', 81, 75, 'false', 'Play'],
['rain', 71, 80, 'true', 'Dont_play']
['rain', 71, 80, 'true', 'Dont_play'],
];
private $extraData = [
@ -32,16 +32,6 @@ class DecisionTreeTest extends TestCase
['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()
{
[$data, $targets] = $this->getData($this->data);
@ -68,7 +58,7 @@ class DecisionTreeTest extends TestCase
$testSamples = [['sunny', 78, 72, 'false'], ['overcast', 60, 60, 'false']];
$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);
$modelManager = new ModelManager();
$modelManager->saveToFile($classifier, $filepath);
@ -83,6 +73,16 @@ class DecisionTreeTest extends TestCase
[$data, $targets] = $this->getData($this->data);
$classifier = new DecisionTree(5);
$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]];
$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);
$modelManager = new ModelManager();
$modelManager->saveToFile($classifier, $filepath);

View File

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

View File

@ -7,9 +7,20 @@ namespace tests\Phpml\Classification\Ensemble;
use Phpml\Classification\DecisionTree;
use Phpml\Classification\Ensemble\RandomForest;
use Phpml\Classification\NaiveBayes;
use Throwable;
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)
{
$classifier = new RandomForest($numBaseClassifiers);
@ -22,15 +33,4 @@ class RandomForestTest extends BaggingTest
{
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);
$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);
$modelManager = new ModelManager();
$modelManager->saveToFile($classifier, $filepath);

View File

@ -35,7 +35,7 @@ class AdalineTest extends TestCase
$samples = [
[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
[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];
@ -55,7 +55,7 @@ class AdalineTest extends TestCase
$samples = [
[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
[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];
$classifier->train($samples, $targets);
@ -74,7 +74,7 @@ class AdalineTest extends TestCase
$testSamples = [[0, 1], [1, 1], [0.2, 0.1]];
$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);
$modelManager = new ModelManager();
$modelManager->saveToFile($classifier, $filepath);

View File

@ -40,7 +40,7 @@ class DecisionStumpTest extends TestCase
$samples = [
[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
[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];
@ -63,7 +63,7 @@ class DecisionStumpTest extends TestCase
$testSamples = [[0, 1], [1, 1], [0.2, 0.1]];
$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);
$modelManager = new ModelManager();
$modelManager->saveToFile($classifier, $filepath);

View File

@ -37,7 +37,7 @@ class PerceptronTest extends TestCase
$samples = [
[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
[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];
@ -58,7 +58,7 @@ class PerceptronTest extends TestCase
$samples = [
[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
[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];
$classifier->train($samples, $targets);
@ -77,7 +77,7 @@ class PerceptronTest extends TestCase
$testSamples = [[0, 1], [1, 1], [0.2, 0.1]];
$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);
$modelManager = new ModelManager();
$modelManager->saveToFile($classifier, $filepath);

View File

@ -150,7 +150,7 @@ class MLPClassifierTest extends TestCase
$testSamples = [[0, 0], [1, 0], [0, 1], [1, 1]];
$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);
$modelManager = new ModelManager();
$modelManager->saveToFile($classifier, $filepath);

View File

@ -59,7 +59,7 @@ class NaiveBayesTest extends TestCase
$classifier->train($trainSamples, $trainLabels);
$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);
$modelManager = new ModelManager();
$modelManager->saveToFile($classifier, $filepath);

View File

@ -57,7 +57,7 @@ class SVCTest extends TestCase
$classifier->train($trainSamples, $trainLabels);
$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);
$modelManager = new ModelManager();
$modelManager->saveToFile($classifier, $filepath);

View File

@ -34,10 +34,25 @@ class DBSCANTest extends TestCase
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 = [
['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);

View File

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

View File

@ -23,6 +23,7 @@ class KMeansTest extends TestCase
unset($samples[$index]);
}
}
$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], [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,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 = [
[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.039234518840831], [0.40858295942991], [0.40110375047242], [-0.10555116296691],
[-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);
$reducedData = $kpca->fit($data);

View File

@ -28,7 +28,7 @@ class LDATest extends TestCase
[4.7, 3.2, 1.3, 0.2],
[6.5, 3.0, 5.2, 2.0],
[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 = [
[-1.4922092756753, 1.9047102045574],
@ -36,7 +36,7 @@ class LDATest extends TestCase
[-1.3487505965419, 1.749846351699],
[1.7759343101456, 2.0371552314006],
[2.0059819019159, 2.4493123003226],
[1.701474913008, 1.9037880473772]
[1.701474913008, 1.9037880473772],
];
$control = [];

View File

@ -26,12 +26,12 @@ class PCATest extends TestCase
[2.0, 1.6],
[1.0, 1.1],
[1.5, 1.6],
[1.1, 0.9]
[1.1, 0.9],
];
$transformed = [
[-0.827970186], [1.77758033], [-0.992197494],
[-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);
$reducedData = $pca->fit($data);

View File

@ -14,13 +14,41 @@ class TfIdfTransformerTest extends TestCase
// https://en.wikipedia.org/wiki/Tf-idf
$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 = [
[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);

View File

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

View File

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

View File

@ -19,7 +19,7 @@ class EigenDecompositionTest extends TestCase
// http://www.cs.otago.ac.nz/cosc453/student_tutorials/principal_components.pdf
$matrix = [
[0.616555556, 0.615444444],
[0.614444444, 0.716555556]
[0.614444444, 0.716555556],
];
$knownEigvalues = [0.0490833989, 1.28402771];
$knownEigvectors = [[-0.735178656, 0.677873399], [-0.677873399, -0.735178656]];
@ -43,7 +43,7 @@ class EigenDecompositionTest extends TestCase
if ($i > $k) {
$A[$i][$k] = $A[$k][$i];
} 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 = [
[1, 1, 1],
[2, 2, 2]
[2, 2, 2],
];
$transposed = [
[1, 2],
[1, 2],
[1, 2]
[1, 2],
];
$this->assertEquals($transposed, Matrix::transposeArray($array));

View File

@ -6,6 +6,7 @@ namespace tests\Phpml\Math;
use Phpml\Math\Product;
use PHPUnit\Framework\TestCase;
use stdClass;
class ProductTest extends TestCase
{
@ -16,6 +17,6 @@ class ProductTest extends TestCase
$this->assertEquals(8, Product::scalar([2], [4]));
//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]));
$this->assertInstanceOf('\Phpml\Math\Set', $union);
$this->assertInstanceOf(Set::class, $union);
$this->assertEquals(new Set([1, 2, 3]), $union);
$this->assertEquals(3, $union->cardinality());
}
@ -22,7 +22,7 @@ class SetTest extends TestCase
{
$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(1, $intersection->cardinality());
}
@ -31,7 +31,7 @@ class SetTest extends TestCase
{
$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(2, $difference->cardinality());
}

View File

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

View File

@ -16,11 +16,31 @@ class ClassificationReportTest extends TestCase
$report = new ClassificationReport($labels, $predicted);
$precision = ['cat' => 0.5, 'ant' => 0.0, 'bird' => 1.0];
$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];
$precision = [
'cat' => 0.5,
'ant' => 0.0,
'bird' => 1.0,
];
$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($recall, $report->getRecall(), '', 0.01);
@ -36,11 +56,31 @@ class ClassificationReportTest extends TestCase
$report = new ClassificationReport($labels, $predicted);
$precision = [0 => 0.5, 1 => 0.0, 2 => 1.0];
$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];
$precision = [
0 => 0.5,
1 => 0.0,
2 => 1.0,
];
$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($recall, $report->getRecall(), '', 0.01);
@ -56,7 +96,10 @@ class ClassificationReportTest extends TestCase
$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
@ -66,7 +109,11 @@ class ClassificationReportTest extends TestCase
$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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -19,16 +19,13 @@ class ThresholdedReLUTest extends TestCase
$this->assertEquals($expected, $thresholdedReLU->compute($value));
}
/**
* @return array
*/
public function thresholdProvider()
public function thresholdProvider(): array
{
return [
[1.0, 0, 1.0],
[0.5, 3.75, 3.75],
[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\Neuron;
use PHPUnit\Framework\TestCase;
use stdClass;
class LayerTest extends TestCase
{
@ -43,7 +44,7 @@ class LayerTest extends TestCase
*/
public function testThrowExceptionOnInvalidNodeClass(): void
{
new Layer(1, \stdClass::class);
new Layer(1, stdClass::class);
}
public function testAddNodesToLayer(): void

View File

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

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