diff --git a/.upgradenotes/MDL-84107-2025020406045257.yml b/.upgradenotes/MDL-84107-2025020406045257.yml new file mode 100644 index 00000000000..96db6355f9c --- /dev/null +++ b/.upgradenotes/MDL-84107-2025020406045257.yml @@ -0,0 +1,12 @@ +issueNumber: MDL-84107 +notes: + mlbackend_php: + - message: >- + The plugin `mlbackend_php` has been removed and replaced by `mlbackend_python` as the + new default value for the Analytics setting `predictionsprocessor`. + The plugin is available at https://github.com/moodlehq/moodle-mlbackend_php. + type: removed + core: + - message: >- + Analytics is now disabled by default on new installs. + type: changed diff --git a/admin/presets/tests/helper_test.php b/admin/presets/tests/helper_test.php index 3b75bd9ea6c..6256bb19e08 100644 --- a/admin/presets/tests/helper_test.php +++ b/admin/presets/tests/helper_test.php @@ -283,10 +283,6 @@ final class helper_test extends \advanced_testcase { $this->resetAfterTest(); $this->setAdminUser(); - // We need to change some of the default values; otherwise, the full preset won't be applied, because all the settings - // and plugins are the same. - set_config('enableanalytics', '0'); - $generator = $this->getDataGenerator()->get_plugin_generator('core_adminpresets'); $generator->create_preset(['name' => 'Preset 1']); diff --git a/admin/settings/subsystems.php b/admin/settings/subsystems.php index 409f902227a..6562a315eaa 100644 --- a/admin/settings/subsystems.php +++ b/admin/settings/subsystems.php @@ -46,9 +46,9 @@ if ($hassiteconfig) { // speedup for non-admins, add all caps used on this page $optionalsubsystems->add(new admin_setting_configcheckbox('allowstealth', new lang_string('allowstealthmodules'), new lang_string('allowstealthmodules_help'), 0, 1, 0)); - + // TODO: Analytics is now disable on new installs until MDL-84481 overcomes the config of mlbackend_python. $optionalsubsystems->add(new admin_setting_configcheckbox('enableanalytics', new lang_string('enableanalytics', 'admin'), - new lang_string('configenableanalytics', 'admin'), 1, 1, 0)); + new lang_string('configenableanalytics', 'admin'), 0, 1, 0)); $optionalsubsystems->add(new admin_setting_configcheckbox('core_competency/enabled', new lang_string('enablecompetencies', 'core_competency'), diff --git a/admin/tool/analytics/tests/behat/restoredefault.feature b/admin/tool/analytics/tests/behat/restoredefault.feature index b9a4578323a..ec6debfad2d 100644 --- a/admin/tool/analytics/tests/behat/restoredefault.feature +++ b/admin/tool/analytics/tests/behat/restoredefault.feature @@ -11,6 +11,8 @@ Feature: Restoring default models And the following "role assigns" exist: | user | role | contextlevel | reference | | manager | manager | System | | + And the following config values are set as admin: + | enableanalytics | 1 | Scenario: Restore a single deleted default model Given I log in as "manager" diff --git a/analytics/classes/manager.php b/analytics/classes/manager.php index 4cb2882e321..38f3f3eb0c0 100644 --- a/analytics/classes/manager.php +++ b/analytics/classes/manager.php @@ -38,7 +38,7 @@ class manager { /** * Default mlbackend */ - const DEFAULT_MLBACKEND = '\mlbackend_php\processor'; + const DEFAULT_MLBACKEND = '\mlbackend_python\processor'; /** * Name of the file where components declare their models. @@ -257,11 +257,16 @@ class manager { public static function is_mlbackend_used($plugin) { $models = self::get_all_models(); foreach ($models as $model) { - $processor = $model->get_predictions_processor(); - $noprefixnamespace = ltrim(get_class($processor), '\\'); - $processorplugin = substr($noprefixnamespace, 0, strpos($noprefixnamespace, '\\')); - if ($processorplugin == $plugin) { - return true; + try { + $processor = $model->get_predictions_processor(); + $noprefixnamespace = ltrim(get_class($processor), '\\'); + $processorplugin = substr($noprefixnamespace, 0, strpos($noprefixnamespace, '\\')); + if ($processorplugin == $plugin) { + return true; + } + } catch (\Exception $e) { + // The model does not have a predictions processor. + continue; } } diff --git a/lib/db/upgrade.php b/lib/db/upgrade.php index 1a0cf7a874a..155847d2d5b 100644 --- a/lib/db/upgrade.php +++ b/lib/db/upgrade.php @@ -1464,5 +1464,34 @@ function xmldb_main_upgrade($oldversion) { upgrade_main_savepoint(true, 2025022100.02); } + if ($oldversion < 2025020600.01) { + // If mlbackend_php is no longer present, remove it. + if (!file_exists($CFG->dirroot . '/lib/mlbackend/php/version.php')) { + // Clean config. + uninstall_plugin('mlbackend', 'php'); + + // Change the processor if mlbackend_php is set. + if (get_config('analytics', 'predictionsprocessor') === '\mlbackend_php\processor') { + set_config('predictionsprocessor', '\mlbackend_python\processor', 'analytics'); + // We can't be sure mlbackend_python is set up correctly, so we disable analytics. + set_config('enableanalytics', false); + } + + // Cleanup any references to mlbackend_php. + $select = $DB->sql_like('predictionsprocessor', ':predictionsprocessor', false); + $params = ['predictionsprocessor' => '%' . $DB->sql_like_escape('mlbackend_php') . '%']; + $DB->set_field_select( + table: 'analytics_models', + newfield: 'predictionsprocessor', + newvalue: null, + select: $select, + params: $params, + ); + } + + // Main savepoint reached. + upgrade_main_savepoint(true, 2025020600.01); + } + return true; } diff --git a/lib/mlbackend/php/classes/privacy/provider.php b/lib/mlbackend/php/classes/privacy/provider.php deleted file mode 100644 index abf9403fd4e..00000000000 --- a/lib/mlbackend/php/classes/privacy/provider.php +++ /dev/null @@ -1,46 +0,0 @@ -. - -/** - * Privacy Subsystem implementation for mlbackend_php. - * - * @package mlbackend_php - * @copyright 2018 David Monllao - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -namespace mlbackend_php\privacy; - -defined('MOODLE_INTERNAL') || die(); - -/** - * Privacy Subsystem for mlbackend_php implementing null_provider. - * - * @copyright 2018 David Monllao - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class provider implements \core_privacy\local\metadata\null_provider { - - /** - * Get the language string identifier with the component's language - * file to explain why this plugin stores no data. - * - * @return string - */ - public static function get_reason(): string { - return 'privacy:metadata'; - } -} diff --git a/lib/mlbackend/php/classes/processor.php b/lib/mlbackend/php/classes/processor.php deleted file mode 100644 index bf9c5fea0ce..00000000000 --- a/lib/mlbackend/php/classes/processor.php +++ /dev/null @@ -1,548 +0,0 @@ -. - -/** - * Php predictions processor - * - * @package mlbackend_php - * @copyright 2016 David Monllao {@link http://www.davidmonllao.com} - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -namespace mlbackend_php; - -defined('MOODLE_INTERNAL') || die(); - -use Phpml\Preprocessing\Normalizer; -use Phpml\CrossValidation\RandomSplit; -use Phpml\Dataset\ArrayDataset; -use Phpml\ModelManager; -use Phpml\Classification\Linear\LogisticRegression; -use Phpml\Metric\ClassificationReport; - -/** - * PHP predictions processor. - * - * @package mlbackend_php - * @copyright 2016 David Monllao {@link http://www.davidmonllao.com} - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class processor implements \core_analytics\classifier, \core_analytics\regressor, \core_analytics\packable { - - /** - * Size of training / prediction batches. - */ - const BATCH_SIZE = 5000; - - /** - * Number of train iterations. - */ - const TRAIN_ITERATIONS = 500; - - /** - * File name of the serialised model. - */ - const MODEL_FILENAME = 'model.ser'; - - /** - * @var bool - */ - protected $limitedsize = false; - - /** - * Checks if the processor is ready to use. - * - * @return bool - */ - public function is_ready() { - return true; - } - - /** - * Delete the stored models. - * - * @param string $uniqueid - * @param string $modelversionoutputdir - * @return null - */ - public function clear_model($uniqueid, $modelversionoutputdir) { - remove_dir($modelversionoutputdir); - } - - /** - * Delete the output directory. - * - * @param string $modeloutputdir - * @param string $uniqueid - * @return null - */ - public function delete_output_dir($modeloutputdir, $uniqueid) { - remove_dir($modeloutputdir); - } - - /** - * Train this processor classification model using the provided supervised learning dataset. - * - * @param string $uniqueid - * @param \stored_file $dataset - * @param string $outputdir - * @return \stdClass - */ - public function train_classification($uniqueid, \stored_file $dataset, $outputdir) { - - $modelfilepath = $this->get_model_filepath($outputdir); - - $modelmanager = new ModelManager(); - - if (file_exists($modelfilepath)) { - $classifier = $modelmanager->restoreFromFile($modelfilepath); - } else { - $classifier = $this->instantiate_algorithm(); - } - - $fh = $dataset->get_content_file_handle(); - - // The first lines are var names and the second one values. - $metadata = $this->extract_metadata($fh); - - // Skip headers. - fgets($fh); - - $samples = array(); - $targets = array(); - while (($data = fgetcsv($fh, escape: '\\')) !== false) { - $sampledata = array_map('floatval', $data); - $samples[] = array_slice($sampledata, 0, $metadata['nfeatures']); - $targets[] = intval($data[$metadata['nfeatures']]); - - $nsamples = count($samples); - if ($nsamples === self::BATCH_SIZE) { - // Training it batches to avoid running out of memory. - $classifier->partialTrain($samples, $targets, json_decode($metadata['targetclasses'])); - $samples = array(); - $targets = array(); - } - if (empty($morethan1sample) && $nsamples > 1) { - $morethan1sample = true; - } - } - fclose($fh); - - if (empty($morethan1sample)) { - $resultobj = new \stdClass(); - $resultobj->status = \core_analytics\model::NO_DATASET; - $resultobj->info = array(); - return $resultobj; - } - - // Train the remaining samples. - if ($samples) { - $classifier->partialTrain($samples, $targets, json_decode($metadata['targetclasses'])); - } - - $resultobj = new \stdClass(); - $resultobj->status = \core_analytics\model::OK; - $resultobj->info = array(); - - // Store the trained model. - $modelmanager->saveToFile($classifier, $modelfilepath); - - return $resultobj; - } - - /** - * Classifies the provided dataset samples. - * - * @param string $uniqueid - * @param \stored_file $dataset - * @param string $outputdir - * @return \stdClass - */ - public function classify($uniqueid, \stored_file $dataset, $outputdir) { - - $classifier = $this->load_classifier($outputdir); - - $fh = $dataset->get_content_file_handle(); - - // The first lines are var names and the second one values. - $metadata = $this->extract_metadata($fh); - - // Skip headers. - fgets($fh); - - $sampleids = array(); - $samples = array(); - $predictions = array(); - while (($data = fgetcsv($fh, escape: '\\')) !== false) { - $sampledata = array_map('floatval', $data); - $sampleids[] = $data[0]; - $samples[] = array_slice($sampledata, 1, $metadata['nfeatures']); - - if (count($samples) === self::BATCH_SIZE) { - // Prediction it batches to avoid running out of memory. - - // Append predictions incrementally, we want $sampleids keys in sync with $predictions keys. - $newpredictions = $classifier->predict($samples); - foreach ($newpredictions as $prediction) { - array_push($predictions, $prediction); - } - $samples = array(); - } - } - fclose($fh); - - // Finish the remaining predictions. - if ($samples) { - $predictions = $predictions + $classifier->predict($samples); - } - - $resultobj = new \stdClass(); - $resultobj->status = \core_analytics\model::OK; - $resultobj->info = array(); - - foreach ($predictions as $index => $prediction) { - $resultobj->predictions[$index] = array($sampleids[$index], $prediction); - } - - return $resultobj; - } - - /** - * Evaluates this processor classification model using the provided supervised learning dataset. - * - * During evaluation we need to shuffle the evaluation dataset samples to detect deviated results, - * if the dataset is massive we can not load everything into memory. We know that 2GB is the - * minimum memory limit we should have (\core_analytics\model::heavy_duty_mode), if we substract the memory - * that we already consumed and the memory that Phpml algorithms will need we should still have at - * least 500MB of memory, which should be enough to evaluate a model. In any case this is a robust - * solution that will work for all sites but it should minimize memory limit problems. Site admins - * can still set $CFG->mlbackend_php_no_evaluation_limits to true to skip this 500MB limit. - * - * @param string $uniqueid - * @param float $maxdeviation - * @param int $niterations - * @param \stored_file $dataset - * @param string $outputdir - * @param string $trainedmodeldir - * @return \stdClass - */ - public function evaluate_classification($uniqueid, $maxdeviation, $niterations, \stored_file $dataset, - $outputdir, $trainedmodeldir) { - $fh = $dataset->get_content_file_handle(); - - if ($trainedmodeldir) { - // We overwrite the number of iterations as the results will always be the same. - $niterations = 1; - $classifier = $this->load_classifier($trainedmodeldir); - } - - // The first lines are var names and the second one values. - $metadata = $this->extract_metadata($fh); - - // Skip headers. - fgets($fh); - - if (empty($CFG->mlbackend_php_no_evaluation_limits)) { - $samplessize = 0; - $limit = get_real_size('500MB'); - - // Just an approximation, will depend on PHP version, compile options... - // Double size + zval struct (6 bytes + 8 bytes + 16 bytes) + array bucket (96 bytes) - // https://nikic.github.io/2011/12/12/How-big-are-PHP-arrays-really-Hint-BIG.html. - $floatsize = (PHP_INT_SIZE * 2) + 6 + 8 + 16 + 96; - } - - $samples = array(); - $targets = array(); - while (($data = fgetcsv($fh, escape: '\\')) !== false) { - $sampledata = array_map('floatval', $data); - - $samples[] = array_slice($sampledata, 0, $metadata['nfeatures']); - $targets[] = intval($data[$metadata['nfeatures']]); - - if (empty($CFG->mlbackend_php_no_evaluation_limits)) { - // We allow admins to disable evaluation memory usage limits by modifying config.php. - - // We will have plenty of missing values in the dataset so it should be a conservative approximation. - $samplessize = $samplessize + (count($sampledata) * $floatsize); - - // Stop fetching more samples. - if ($samplessize >= $limit) { - $this->limitedsize = true; - break; - } - } - } - fclose($fh); - - // We need at least 2 samples belonging to each target. - $counts = array_count_values($targets); - $ntargets = count(explode(',', $metadata['targetclasses'])); - foreach ($counts as $count) { - if ($count < 2) { - $notenoughdata = true; - } - } - if ($ntargets > count($counts)) { - $notenoughdata = true; - } - if (!empty($notenoughdata)) { - $resultobj = new \stdClass(); - $resultobj->status = \core_analytics\model::NOT_ENOUGH_DATA; - $resultobj->score = 0; - $resultobj->info = array(get_string('errornotenoughdata', 'mlbackend_php')); - return $resultobj; - } - - $scores = array(); - - // Evaluate the model multiple times to confirm the results are not significantly random due to a short amount of data. - for ($i = 0; $i < $niterations; $i++) { - - if (!$trainedmodeldir) { - $classifier = $this->instantiate_algorithm(); - - // Split up the dataset in classifier and testing. - $data = new RandomSplit(new ArrayDataset($samples, $targets), 0.2); - - $classifier->train($data->getTrainSamples(), $data->getTrainLabels()); - $predictedlabels = $classifier->predict($data->getTestSamples()); - $report = new ClassificationReport($data->getTestLabels(), $predictedlabels, - ClassificationReport::WEIGHTED_AVERAGE); - } else { - $predictedlabels = $classifier->predict($samples); - $report = new ClassificationReport($targets, $predictedlabels, - ClassificationReport::WEIGHTED_AVERAGE); - } - $averages = $report->getAverage(); - $scores[] = $averages['f1score']; - } - - // Let's fill the results changing the returned status code depending on the phi-related calculated metrics. - return $this->get_evaluation_result_object($dataset, $scores, $maxdeviation); - } - - /** - * Returns the results objects from all evaluations. - * - * @param \stored_file $dataset - * @param array $scores - * @param float $maxdeviation - * @return \stdClass - */ - protected function get_evaluation_result_object(\stored_file $dataset, $scores, $maxdeviation) { - - // Average f1 score of all evaluations as final score. - if (count($scores) === 1) { - $avgscore = reset($scores); - } else { - $avgscore = \Phpml\Math\Statistic\Mean::arithmetic($scores); - } - - // Standard deviation should ideally be calculated against the area under the curve. - if (count($scores) === 1) { - $modeldev = 0; - } else { - $modeldev = \Phpml\Math\Statistic\StandardDeviation::population($scores); - } - - // Let's fill the results object. - $resultobj = new \stdClass(); - - // Zero is ok, now we add other bits if something is not right. - $resultobj->status = \core_analytics\model::OK; - $resultobj->info = array(); - $resultobj->score = $avgscore; - - // If each iteration results varied too much we need more data to confirm that this is a valid model. - if ($modeldev > $maxdeviation) { - $resultobj->status = $resultobj->status + \core_analytics\model::NOT_ENOUGH_DATA; - $a = new \stdClass(); - $a->deviation = $modeldev; - $a->accepteddeviation = $maxdeviation; - $resultobj->info[] = get_string('errornotenoughdatadev', 'mlbackend_php', $a); - } - - if ($resultobj->score < \core_analytics\model::MIN_SCORE) { - $resultobj->status = $resultobj->status + \core_analytics\model::LOW_SCORE; - $a = new \stdClass(); - $a->score = $resultobj->score; - $a->minscore = \core_analytics\model::MIN_SCORE; - $resultobj->info[] = get_string('errorlowscore', 'mlbackend_php', $a); - } - - if ($this->limitedsize === true) { - $resultobj->info[] = get_string('datasetsizelimited', 'mlbackend_php', display_size($dataset->get_filesize())); - } - - return $resultobj; - } - - /** - * Loads the pre-trained classifier. - * - * @throws \moodle_exception - * @param string $outputdir - * @return \Phpml\Classification\Linear\LogisticRegression - */ - protected function load_classifier($outputdir) { - $modelfilepath = $this->get_model_filepath($outputdir); - - if (!file_exists($modelfilepath)) { - throw new \moodle_exception('errorcantloadmodel', 'mlbackend_php', '', $modelfilepath); - } - - $modelmanager = new ModelManager(); - return $modelmanager->restoreFromFile($modelfilepath); - } - - /** - * Train this processor regression model using the provided supervised learning dataset. - * - * @throws new \coding_exception - * @param string $uniqueid - * @param \stored_file $dataset - * @param string $outputdir - * @return \stdClass - */ - public function train_regression($uniqueid, \stored_file $dataset, $outputdir) { - throw new \coding_exception('This predictor does not support regression yet.'); - } - - /** - * Estimates linear values for the provided dataset samples. - * - * @throws new \coding_exception - * @param string $uniqueid - * @param \stored_file $dataset - * @param mixed $outputdir - * @return void - */ - public function estimate($uniqueid, \stored_file $dataset, $outputdir) { - throw new \coding_exception('This predictor does not support regression yet.'); - } - - /** - * Evaluates this processor regression model using the provided supervised learning dataset. - * - * @throws new \coding_exception - * @param string $uniqueid - * @param float $maxdeviation - * @param int $niterations - * @param \stored_file $dataset - * @param string $outputdir - * @param string $trainedmodeldir - * @return \stdClass - */ - public function evaluate_regression($uniqueid, $maxdeviation, $niterations, \stored_file $dataset, - $outputdir, $trainedmodeldir) { - throw new \coding_exception('This predictor does not support regression yet.'); - } - - /** - * Exports the machine learning model. - * - * @throws \moodle_exception - * @param string $uniqueid The model unique id - * @param string $modeldir The directory that contains the trained model. - * @return string The path to the directory that contains the exported model. - */ - public function export(string $uniqueid, string $modeldir): string { - - $modelfilepath = $this->get_model_filepath($modeldir); - - if (!file_exists($modelfilepath)) { - throw new \moodle_exception('errorexportmodelresult', 'analytics'); - } - - // We can use the actual $modeldir as the directory is not modified during export, just copied into a zip. - return $modeldir; - } - - /** - * Imports the provided machine learning model. - * - * @param string $uniqueid The model unique id - * @param string $modeldir The directory that will contain the trained model. - * @param string $importdir The directory that contains the files to import. - * @return bool Success - */ - public function import(string $uniqueid, string $modeldir, string $importdir): bool { - - $importmodelfilepath = $this->get_model_filepath($importdir); - $modelfilepath = $this->get_model_filepath($modeldir); - - $modelmanager = new ModelManager(); - - // Copied from ModelManager::restoreFromFile to validate the serialised contents - // before restoring them. - $importconfig = file_get_contents($importmodelfilepath); - - // Clean stuff like function calls. - $importconfig = preg_replace('/[^a-zA-Z0-9\{\}%\.\*\;\,\:\"\-\0\\\]/', '', $importconfig); - - $object = unserialize($importconfig, - ['allowed_classes' => ['Phpml\\Classification\\Linear\\LogisticRegression']]); - if (!$object) { - return false; - } - - if (get_class($object) == '__PHP_Incomplete_Class') { - return false; - } - - $classifier = $modelmanager->restoreFromFile($importmodelfilepath); - - // This would override any previous classifier. - $modelmanager->saveToFile($classifier, $modelfilepath); - - return true; - } - - /** - * Returns the path to the serialised model file in the provided directory. - * - * @param string $modeldir The model directory - * @return string The model file - */ - protected function get_model_filepath(string $modeldir): string { - // Output directory is already unique to the model. - return $modeldir . DIRECTORY_SEPARATOR . self::MODEL_FILENAME; - } - - /** - * Extracts metadata from the dataset file. - * - * The file poiter should be located at the top of the file. - * - * @param resource $fh - * @return array - */ - protected function extract_metadata($fh) { - $metadata = fgetcsv($fh, escape: '\\'); - return array_combine($metadata, fgetcsv($fh, escape: '\\')); - } - - /** - * Instantiates the ML algorithm. - * - * @return \Phpml\Classification\Linear\LogisticRegression - */ - protected function instantiate_algorithm(): \Phpml\Classification\Linear\LogisticRegression { - return new LogisticRegression(self::TRAIN_ITERATIONS, true, - LogisticRegression::CONJUGATE_GRAD_TRAINING, 'log'); - } -} diff --git a/lib/mlbackend/php/lang/en/mlbackend_php.php b/lib/mlbackend/php/lang/en/mlbackend_php.php deleted file mode 100644 index 819f6736ab4..00000000000 --- a/lib/mlbackend/php/lang/en/mlbackend_php.php +++ /dev/null @@ -1,31 +0,0 @@ -. - -/** - * Strings for component 'mlbackend_php' - * - * @package mlbackend_php - * @copyright 2017 David Monllao {@link http://www.davidmonllao.com} - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -$string['datasetsizelimited'] = 'Only part of the dataset has been evaluated due to its size. Set $CFG->mlbackend_php_no_memory_limit if you are confident that your system can cope with a {$a} dataset.'; -$string['errorcantloadmodel'] = 'Model file {$a} does not exist. The model should been trained before using it to predict.'; -$string['errorlowscore'] = 'The evaluated model prediction accuracy is not very high, so some predictions may not be accurate. Model score = {$a->score}, minimum score = {$a->minscore}'; -$string['errornotenoughdata'] = 'There is not enough data to evaluate this model using the provided analysis interval.'; -$string['errornotenoughdatadev'] = 'The evaluation results varied too much. It is recommended that more data is gathered to ensure the model is valid. Evaluation results standard deviation = {$a->deviation}, maximum recommended standard deviation = {$a->accepteddeviation}'; -$string['pluginname'] = 'PHP machine learning backend'; -$string['privacy:metadata'] = 'The PHP machine learning backend plugin does not store any personal data.'; diff --git a/lib/mlbackend/php/phpml/LICENSE b/lib/mlbackend/php/phpml/LICENSE deleted file mode 100644 index 22c7c89cc7d..00000000000 --- a/lib/mlbackend/php/phpml/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2016-2020 Arkadiusz Kondas - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/lib/mlbackend/php/phpml/readme_moodle.txt b/lib/mlbackend/php/phpml/readme_moodle.txt deleted file mode 100644 index c926e0db2b3..00000000000 --- a/lib/mlbackend/php/phpml/readme_moodle.txt +++ /dev/null @@ -1,10 +0,0 @@ -Current version is 0.10.0 - -# Download latest stable version from https://gitlab.com/php-ai/php-ml -# Remove all files but: - * src/ - * LICENSE -# Copy content of src/ to /path/to/moodle/lib/mlbackend/php/phpml/src/Phpml -# Copy LICENSE file to /path/to/moodle/lib/mlbackend/php/phpml -# Applied patch https://github.com/jorgecasas/php-ml/pull/5 - diff --git a/lib/mlbackend/php/phpml/src/Phpml/Association/Apriori.php b/lib/mlbackend/php/phpml/src/Phpml/Association/Apriori.php deleted file mode 100644 index 1f736790b5b..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/Association/Apriori.php +++ /dev/null @@ -1,332 +0,0 @@ -support = $support; - $this->confidence = $confidence; - } - - /** - * Get all association rules which are generated for every k-length frequent item set. - * - * @return mixed[][] - */ - public function getRules(): array - { - if (count($this->large) === 0) { - $this->large = $this->apriori(); - } - - if (count($this->rules) > 0) { - return $this->rules; - } - - $this->rules = []; - - $this->generateAllRules(); - - return $this->rules; - } - - /** - * Generates frequent item sets. - * - * @return mixed[][][] - */ - public function apriori(): array - { - $L = []; - - $items = $this->frequent($this->items()); - for ($k = 1; isset($items[0]); ++$k) { - $L[$k] = $items; - $items = $this->frequent($this->candidates($items)); - } - - return $L; - } - - /** - * @param mixed[] $sample - * - * @return mixed[][] - */ - protected function predictSample(array $sample): array - { - $predicts = array_values(array_filter($this->getRules(), function ($rule) use ($sample): bool { - return $this->equals($rule[self::ARRAY_KEY_ANTECEDENT], $sample); - })); - - return array_map(static function ($rule) { - return $rule[self::ARRAY_KEY_CONSEQUENT]; - }, $predicts); - } - - /** - * Generate rules for each k-length frequent item set. - */ - private function generateAllRules(): void - { - for ($k = 2; isset($this->large[$k]); ++$k) { - foreach ($this->large[$k] as $frequent) { - $this->generateRules($frequent); - } - } - } - - /** - * Generate confident rules for frequent item set. - * - * @param mixed[] $frequent - */ - private function generateRules(array $frequent): void - { - foreach ($this->antecedents($frequent) as $antecedent) { - $confidence = $this->confidence($frequent, $antecedent); - if ($this->confidence <= $confidence) { - $consequent = array_values(array_diff($frequent, $antecedent)); - $this->rules[] = [ - self::ARRAY_KEY_ANTECEDENT => $antecedent, - self::ARRAY_KEY_CONSEQUENT => $consequent, - self::ARRAY_KEY_SUPPORT => $this->support($frequent), - self::ARRAY_KEY_CONFIDENCE => $confidence, - ]; - } - } - } - - /** - * Generates the power set for given item set $sample. - * - * @param mixed[] $sample - * - * @return mixed[][] - */ - private function powerSet(array $sample): array - { - $results = [[]]; - foreach ($sample as $item) { - foreach ($results as $combination) { - $results[] = array_merge([$item], $combination); - } - } - - return $results; - } - - /** - * Generates all proper subsets for given set $sample without the empty set. - * - * @param mixed[] $sample - * - * @return mixed[][] - */ - private function antecedents(array $sample): array - { - $cardinality = count($sample); - $antecedents = $this->powerSet($sample); - - return array_filter($antecedents, static function ($antecedent) use ($cardinality): bool { - return (count($antecedent) != $cardinality) && ($antecedent != []); - }); - } - - /** - * Calculates frequent k = 1 item sets. - * - * @return mixed[][] - */ - private function items(): array - { - $items = []; - - foreach ($this->samples as $sample) { - foreach ($sample as $item) { - if (!in_array($item, $items, true)) { - $items[] = $item; - } - } - } - - return array_map(static function ($entry): array { - return [$entry]; - }, $items); - } - - /** - * Returns frequent item sets only. - * - * @param mixed[][] $samples - * - * @return mixed[][] - */ - private function frequent(array $samples): array - { - return array_values(array_filter($samples, function ($entry): bool { - return $this->support($entry) >= $this->support; - })); - } - - /** - * Calculates frequent k item sets, where count($samples) == $k - 1. - * - * @param mixed[][] $samples - * - * @return mixed[][] - */ - private function candidates(array $samples): array - { - $candidates = []; - - foreach ($samples as $p) { - foreach ($samples as $q) { - if (count(array_merge(array_diff($p, $q), array_diff($q, $p))) != 2) { - continue; - } - - $candidate = array_values(array_unique(array_merge($p, $q))); - - if ($this->contains($candidates, $candidate)) { - continue; - } - - foreach ($this->samples as $sample) { - if ($this->subset($sample, $candidate)) { - $candidates[] = $candidate; - - continue 2; - } - } - } - } - - return $candidates; - } - - /** - * Calculates confidence for $set. Confidence is the relative amount of sets containing $subset which also contain - * $set. - * - * @param mixed[] $set - * @param mixed[] $subset - */ - private function confidence(array $set, array $subset): float - { - return $this->support($set) / $this->support($subset); - } - - /** - * Calculates support for item set $sample. Support is the relative amount of sets containing $sample in the data - * pool. - * - * @see \Phpml\Association\Apriori::samples - * - * @param mixed[] $sample - */ - private function support(array $sample): float - { - return $this->frequency($sample) / count($this->samples); - } - - /** - * Counts occurrences of $sample as subset in data pool. - * - * @see \Phpml\Association\Apriori::samples - * - * @param mixed[] $sample - */ - private function frequency(array $sample): int - { - return count(array_filter($this->samples, function ($entry) use ($sample): bool { - return $this->subset($entry, $sample); - })); - } - - /** - * Returns true if set is an element of system. - * - * @see \Phpml\Association\Apriori::equals() - * - * @param mixed[][] $system - * @param mixed[] $set - */ - private function contains(array $system, array $set): bool - { - return (bool) array_filter($system, function ($entry) use ($set): bool { - return $this->equals($entry, $set); - }); - } - - /** - * Returns true if subset is a (proper) subset of set by its items string representation. - * - * @param mixed[] $set - * @param mixed[] $subset - */ - private function subset(array $set, array $subset): bool - { - return count(array_diff($subset, array_intersect($subset, $set))) === 0; - } - - /** - * Returns true if string representation of items does not differ. - * - * @param mixed[] $set1 - * @param mixed[] $set2 - */ - private function equals(array $set1, array $set2): bool - { - return array_diff($set1, $set2) == array_diff($set2, $set1); - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/Association/Associator.php b/lib/mlbackend/php/phpml/src/Phpml/Association/Associator.php deleted file mode 100644 index c339b5e65f3..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/Association/Associator.php +++ /dev/null @@ -1,11 +0,0 @@ -maxDepth = $maxDepth; - } - - public function train(array $samples, array $targets): void - { - $this->samples = array_merge($this->samples, $samples); - $this->targets = array_merge($this->targets, $targets); - - $this->featureCount = count($this->samples[0]); - $this->columnTypes = self::getColumnTypes($this->samples); - $this->labels = array_keys(array_count_values($this->targets)); - $this->tree = $this->getSplitLeaf(range(0, count($this->samples) - 1)); - - // Each time the tree is trained, feature importances are reset so that - // we will have to compute it again depending on the new data - $this->featureImportances = null; - - // If column names are given or computed before, then there is no - // need to init it and accidentally remove the previous given names - if ($this->columnNames === []) { - $this->columnNames = range(0, $this->featureCount - 1); - } elseif (count($this->columnNames) > $this->featureCount) { - $this->columnNames = array_slice($this->columnNames, 0, $this->featureCount); - } elseif (count($this->columnNames) < $this->featureCount) { - $this->columnNames = array_merge( - $this->columnNames, - range(count($this->columnNames), $this->featureCount - 1) - ); - } - } - - public static function getColumnTypes(array $samples): array - { - $types = []; - $featureCount = count($samples[0]); - for ($i = 0; $i < $featureCount; ++$i) { - $values = array_column($samples, $i); - $isCategorical = self::isCategoricalColumn($values); - $types[] = $isCategorical ? self::NOMINAL : self::CONTINUOUS; - } - - 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 += ($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.
- * 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.
- */ - public function getFeatureImportances(): array - { - if ($this->featureImportances !== null) { - return $this->featureImportances; - } - - $sampleCount = count($this->samples); - $this->featureImportances = []; - foreach ($this->columnNames as $column => $columnName) { - $nodes = $this->getSplitNodesByColumn($column, $this->tree); - - $importance = 0; - foreach ($nodes as $node) { - $importance += $node->getNodeImpurityDecrease($sampleCount); - } - - $this->featureImportances[$columnName] = $importance; - } - - // Normalize & sort the importances - $total = array_sum($this->featureImportances); - if ($total > 0) { - array_walk($this->featureImportances, function (&$importance) use ($total): void { - $importance /= $total; - }); - arsort($this->featureImportances); - } - - return $this->featureImportances; - } - - protected function getSplitLeaf(array $records, int $depth = 0): DecisionTreeLeaf - { - $split = $this->getBestSplit($records); - $split->level = $depth; - if ($this->actualDepth < $depth) { - $this->actualDepth = $depth; - } - - // Traverse all records to see if all records belong to the same class, - // otherwise group the records so that we can classify the leaf - // in case maximum depth is reached - $leftRecords = []; - $rightRecords = []; - $remainingTargets = []; - $prevRecord = null; - $allSame = true; - - foreach ($records as $recordNo) { - // Check if the previous record is the same with the current one - $record = $this->samples[$recordNo]; - if ($prevRecord !== null && $prevRecord != $record) { - $allSame = false; - } - - $prevRecord = $record; - - // According to the split criteron, this record will - // belong to either left or the right side in the next split - if ($split->evaluate($record)) { - $leftRecords[] = $recordNo; - } else { - $rightRecords[] = $recordNo; - } - - // Group remaining targets - $target = $this->targets[$recordNo]; - if (!array_key_exists($target, $remainingTargets)) { - $remainingTargets[$target] = 1; - } else { - ++$remainingTargets[$target]; - } - } - - if ($allSame || $depth >= $this->maxDepth || count($remainingTargets) === 1) { - $split->isTerminal = true; - arsort($remainingTargets); - $split->classValue = (string) key($remainingTargets); - } else { - if (isset($leftRecords[0])) { - $split->leftLeaf = $this->getSplitLeaf($leftRecords, $depth + 1); - } - - if (isset($rightRecords[0])) { - $split->rightLeaf = $this->getSplitLeaf($rightRecords, $depth + 1); - } - } - - return $split; - } - - protected function getBestSplit(array $records): DecisionTreeLeaf - { - $targets = array_intersect_key($this->targets, array_flip($records)); - $samples = (array) array_combine( - $records, - $this->preprocess(array_intersect_key($this->samples, array_flip($records))) - ); - $bestGiniVal = 1; - $bestSplit = null; - $features = $this->getSelectedFeatures(); - foreach ($features as $i) { - $colValues = []; - foreach ($samples as $index => $row) { - $colValues[$index] = $row[$i]; - } - - $counts = array_count_values($colValues); - arsort($counts); - $baseValue = key($counts); - if ($baseValue === null) { - continue; - } - - $gini = $this->getGiniIndex($baseValue, $colValues, $targets); - if ($bestSplit === null || $bestGiniVal > $gini) { - $split = new DecisionTreeLeaf(); - $split->value = $baseValue; - $split->giniIndex = $gini; - $split->columnIndex = $i; - $split->isContinuous = $this->columnTypes[$i] === self::CONTINUOUS; - $split->records = $records; - - // If a numeric column is to be selected, then - // the original numeric value and the selected operator - // will also be saved into the leaf for future access - if ($this->columnTypes[$i] === self::CONTINUOUS) { - $matches = []; - preg_match("/^([<>=]{1,2})\s*(.*)/", (string) $split->value, $matches); - $split->operator = $matches[1]; - $split->numericValue = (float) $matches[2]; - } - - $bestSplit = $split; - $bestGiniVal = $gini; - } - } - - return $bestSplit; - } - - /** - * Returns available features/columns to the tree for the decision making - * process.
- * - * If a number is given with setNumFeatures() method, then a random selection - * of features up to this number is returned.
- * - * If some features are manually selected by use of setSelectedFeatures(), - * then only these features are returned
- * - * If any of above methods were not called beforehand, then all features - * are returned by default. - */ - protected function getSelectedFeatures(): array - { - $allFeatures = range(0, $this->featureCount - 1); - if ($this->numUsableFeatures === 0 && count($this->selectedFeatures) === 0) { - return $allFeatures; - } - - if (count($this->selectedFeatures) > 0) { - return $this->selectedFeatures; - } - - $numFeatures = $this->numUsableFeatures; - if ($numFeatures > $this->featureCount) { - $numFeatures = $this->featureCount; - } - - shuffle($allFeatures); - $selectedFeatures = array_slice($allFeatures, 0, $numFeatures); - sort($selectedFeatures); - - return $selectedFeatures; - } - - protected function preprocess(array $samples): array - { - // Detect and convert continuous data column values into - // discrete values by using the median as a threshold value - $columns = []; - for ($i = 0; $i < $this->featureCount; ++$i) { - $values = array_column($samples, $i); - if ($this->columnTypes[$i] == self::CONTINUOUS) { - $median = Mean::median($values); - foreach ($values as &$value) { - if ($value <= $median) { - $value = "<= {$median}"; - } else { - $value = "> {$median}"; - } - } - } - - $columns[] = $values; - } - - // Below method is a strange yet very simple & efficient method - // to get the transpose of a 2D array - return array_map(null, ...$columns); - } - - protected static function isCategoricalColumn(array $columnValues): bool - { - $count = count($columnValues); - - // There are two main indicators that *may* show whether a - // column is composed of discrete set of values: - // 1- Column may contain string values and non-float values - // 2- Number of unique values in the column is only a small fraction of - // all values in that column (Lower than or equal to %20 of all values) - $numericValues = array_filter($columnValues, 'is_numeric'); - $floatValues = array_filter($columnValues, 'is_float'); - if (count($floatValues) > 0) { - return false; - } - - if (count($numericValues) !== $count) { - return true; - } - - $distinctValues = array_count_values($columnValues); - - return count($distinctValues) <= $count / 5; - } - - /** - * Used to set predefined features to consider while deciding which column to use for a split - */ - protected function setSelectedFeatures(array $selectedFeatures): void - { - $this->selectedFeatures = $selectedFeatures; - } - - /** - * Collects and returns an array of internal nodes that use the given - * column as a split criterion - */ - protected function getSplitNodesByColumn(int $column, DecisionTreeLeaf $node): array - { - if ($node->isTerminal) { - return []; - } - - $nodes = []; - if ($node->columnIndex === $column) { - $nodes[] = $node; - } - - $lNodes = []; - $rNodes = []; - if ($node->leftLeaf !== null) { - $lNodes = $this->getSplitNodesByColumn($column, $node->leftLeaf); - } - - if ($node->rightLeaf !== null) { - $rNodes = $this->getSplitNodesByColumn($column, $node->rightLeaf); - } - - return array_merge($nodes, $lNodes, $rNodes); - } - - /** - * @return mixed - */ - protected function predictSample(array $sample) - { - $node = $this->tree; - do { - if ($node->isTerminal) { - return $node->classValue; - } - - if ($node->evaluate($sample)) { - $node = $node->leftLeaf; - } else { - $node = $node->rightLeaf; - } - } while ($node); - - return $this->labels[0]; - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/Classification/DecisionTree/DecisionTreeLeaf.php b/lib/mlbackend/php/phpml/src/Phpml/Classification/DecisionTree/DecisionTreeLeaf.php deleted file mode 100644 index 3f8bd2c5fc3..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/Classification/DecisionTree/DecisionTreeLeaf.php +++ /dev/null @@ -1,165 +0,0 @@ -getHTML(); - } - - public function evaluate(array $record): bool - { - $recordField = $record[$this->columnIndex]; - - if ($this->isContinuous) { - return Comparison::compare((string) $recordField, $this->numericValue, $this->operator); - } - - return $recordField == $this->value; - } - - /** - * Returns Mean Decrease Impurity (MDI) in the node. - * For terminal nodes, this value is equal to 0 - */ - public function getNodeImpurityDecrease(int $parentRecordCount): float - { - if ($this->isTerminal) { - return 0.0; - } - - $nodeSampleCount = (float) count($this->records); - $iT = $this->giniIndex; - - if ($this->leftLeaf !== null) { - $pL = count($this->leftLeaf->records) / $nodeSampleCount; - $iT -= $pL * $this->leftLeaf->giniIndex; - } - - if ($this->rightLeaf !== null) { - $pR = count($this->rightLeaf->records) / $nodeSampleCount; - $iT -= $pR * $this->rightLeaf->giniIndex; - } - - return $iT * $nodeSampleCount / $parentRecordCount; - } - - /** - * Returns HTML representation of the node including children nodes - */ - public function getHTML(?array $columnNames = null): string - { - if ($this->isTerminal) { - $value = "{$this}->classValue"; - } else { - $value = $this->value; - if ($columnNames !== null) { - $col = $columnNames[$this->columnIndex]; - } else { - $col = "col_$this->columnIndex"; - } - - if ((bool) preg_match('/^[<>=]{1,2}/', (string) $value) === false) { - $value = "={$value}"; - } - - $value = "{$col} {$value}
Gini: ".number_format($this->giniIndex, 2); - } - - $str = ""; - - if ($this->leftLeaf !== null || $this->rightLeaf !== null) { - $str .= ''; - if ($this->leftLeaf !== null) { - $str .= ''; - } else { - $str .= ''; - } - - $str .= ''; - if ($this->rightLeaf !== null) { - $str .= ''; - } else { - $str .= ''; - } - - $str .= ''; - } - - $str .= '
{$value}
| Yes
'.$this->leftLeaf->getHTML($columnNames).'
 No |
'.$this->rightLeaf->getHTML($columnNames).'
'; - - return $str; - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/Classification/Ensemble/AdaBoost.php b/lib/mlbackend/php/phpml/src/Phpml/Classification/Ensemble/AdaBoost.php deleted file mode 100644 index fdaeb63eade..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/Classification/Ensemble/AdaBoost.php +++ /dev/null @@ -1,252 +0,0 @@ -maxIterations = $maxIterations; - } - - /** - * Sets the base classifier that will be used for boosting (default = DecisionStump) - */ - public function setBaseClassifier(string $baseClassifier = DecisionStump::class, array $classifierOptions = []): void - { - $this->baseClassifier = $baseClassifier; - $this->classifierOptions = $classifierOptions; - } - - /** - * @throws InvalidArgumentException - */ - public function train(array $samples, array $targets): void - { - // Initialize usual variables - $this->labels = array_keys(array_count_values($targets)); - if (count($this->labels) !== 2) { - throw new InvalidArgumentException('AdaBoost is a binary classifier and can classify between two classes only'); - } - - // Set all target values to either -1 or 1 - $this->labels = [ - 1 => $this->labels[0], - -1 => $this->labels[1], - ]; - foreach ($targets as $target) { - $this->targets[] = $target == $this->labels[1] ? 1 : -1; - } - - $this->samples = array_merge($this->samples, $samples); - $this->featureCount = count($samples[0]); - $this->sampleCount = count($this->samples); - - // Initialize AdaBoost parameters - $this->weights = array_fill(0, $this->sampleCount, 1.0 / $this->sampleCount); - $this->classifiers = []; - $this->alpha = []; - - // Execute the algorithm for a maximum number of iterations - $currIter = 0; - while ($this->maxIterations > $currIter++) { - // Determine the best 'weak' classifier based on current weights - $classifier = $this->getBestClassifier(); - $errorRate = $this->evaluateClassifier($classifier); - - // Update alpha & weight values at each iteration - $alpha = $this->calculateAlpha($errorRate); - $this->updateWeights($classifier, $alpha); - - $this->classifiers[] = $classifier; - $this->alpha[] = $alpha; - } - } - - /** - * @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); - /** @var Classifier $classifier */ - $classifier = count($this->classifierOptions) === 0 ? $ref->newInstance() : $ref->newInstanceArgs($this->classifierOptions); - - if ($classifier instanceof WeightedClassifier) { - $classifier->setSampleWeights($this->weights); - $classifier->train($this->samples, $this->targets); - } else { - [$samples, $targets] = $this->resample(); - $classifier->train($samples, $targets); - } - - return $classifier; - } - - /** - * Resamples the dataset in accordance with the weights and - * returns the new dataset - */ - protected function resample(): array - { - $weights = $this->weights; - $std = StandardDeviation::population($weights); - $mean = Mean::arithmetic($weights); - $min = min($weights); - $minZ = (int) round(($min - $mean) / $std); - - $samples = []; - $targets = []; - foreach ($weights as $index => $weight) { - $z = (int) round(($weight - $mean) / $std) - $minZ + 1; - for ($i = 0; $i < $z; ++$i) { - if (random_int(0, 1) == 0) { - continue; - } - - $samples[] = $this->samples[$index]; - $targets[] = $this->targets[$index]; - } - } - - return [$samples, $targets]; - } - - /** - * Evaluates the classifier and returns the classification error rate - */ - protected function evaluateClassifier(Classifier $classifier): float - { - $total = (float) array_sum($this->weights); - $wrong = 0; - foreach ($this->samples as $index => $sample) { - $predicted = $classifier->predict($sample); - if ($predicted != $this->targets[$index]) { - $wrong += $this->weights[$index]; - } - } - - return $wrong / $total; - } - - /** - * Calculates alpha of a classifier - */ - protected function calculateAlpha(float $errorRate): float - { - if ($errorRate == 0) { - $errorRate = 1e-10; - } - - return 0.5 * log((1 - $errorRate) / $errorRate); - } - - /** - * Updates the sample weights - */ - protected function updateWeights(Classifier $classifier, float $alpha): void - { - $sumOfWeights = array_sum($this->weights); - $weightsT1 = []; - foreach ($this->weights as $index => $weight) { - $desired = $this->targets[$index]; - $output = $classifier->predict($this->samples[$index]); - - $weight *= exp(-$alpha * $desired * $output) / $sumOfWeights; - - $weightsT1[] = $weight; - } - - $this->weights = $weightsT1; - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/Classification/Ensemble/Bagging.php b/lib/mlbackend/php/phpml/src/Phpml/Classification/Ensemble/Bagging.php deleted file mode 100644 index 2c9de71b69a..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/Classification/Ensemble/Bagging.php +++ /dev/null @@ -1,170 +0,0 @@ - 20]; - - /** - * @var array - */ - protected $classifiers = []; - - /** - * @var float - */ - protected $subsetRatio = 0.7; - - /** - * Creates an ensemble classifier with given number of base classifiers - * Default number of base classifiers is 50. - * The more number of base classifiers, the better performance but at the cost of procesing time - */ - public function __construct(int $numClassifier = 50) - { - $this->numClassifier = $numClassifier; - } - - /** - * This method determines the ratio of samples used to create the 'bootstrap' subset, - * e.g., random samples drawn from the original dataset with replacement (allow repeats), - * to train each base classifier. - * - * @return $this - * - * @throws InvalidArgumentException - */ - public function setSubsetRatio(float $ratio) - { - if ($ratio < 0.1 || $ratio > 1.0) { - throw new InvalidArgumentException('Subset ratio should be between 0.1 and 1.0'); - } - - $this->subsetRatio = $ratio; - - return $this; - } - - /** - * This method is used to set the base classifier. Default value is - * DecisionTree::class, but any class that implements the Classifier - * can be used.
- * While giving the parameters of the classifier, the values should be - * given in the order they are in the constructor of the classifier and parameter - * names are neglected. - * - * @return $this - */ - public function setClassifer(string $classifier, array $classifierOptions = []) - { - $this->classifier = $classifier; - $this->classifierOptions = $classifierOptions; - - return $this; - } - - public function train(array $samples, array $targets): void - { - $this->samples = array_merge($this->samples, $samples); - $this->targets = array_merge($this->targets, $targets); - $this->featureCount = count($samples[0]); - $this->numSamples = count($this->samples); - - // Init classifiers and train them with bootstrap samples - $this->classifiers = $this->initClassifiers(); - $index = 0; - foreach ($this->classifiers as $classifier) { - [$samples, $targets] = $this->getRandomSubset($index); - $classifier->train($samples, $targets); - ++$index; - } - } - - protected function getRandomSubset(int $index): array - { - $samples = []; - $targets = []; - srand($index); - $bootstrapSize = $this->subsetRatio * $this->numSamples; - for ($i = 0; $i < $bootstrapSize; ++$i) { - $rand = random_int(0, $this->numSamples - 1); - $samples[] = $this->samples[$rand]; - $targets[] = $this->targets[$rand]; - } - - return [$samples, $targets]; - } - - protected function initClassifiers(): array - { - $classifiers = []; - for ($i = 0; $i < $this->numClassifier; ++$i) { - $ref = new ReflectionClass($this->classifier); - /** @var Classifier $obj */ - $obj = count($this->classifierOptions) === 0 ? $ref->newInstance() : $ref->newInstanceArgs($this->classifierOptions); - - $classifiers[] = $this->initSingleClassifier($obj); - } - - return $classifiers; - } - - protected function initSingleClassifier(Classifier $classifier): Classifier - { - return $classifier; - } - - /** - * @return mixed - */ - protected function predictSample(array $sample) - { - $predictions = []; - foreach ($this->classifiers as $classifier) { - /** @var Classifier $classifier */ - $predictions[] = $classifier->predict($sample); - } - - $counts = array_count_values($predictions); - arsort($counts); - reset($counts); - - return key($counts); - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/Classification/Ensemble/RandomForest.php b/lib/mlbackend/php/phpml/src/Phpml/Classification/Ensemble/RandomForest.php deleted file mode 100644 index 71ea8d1ebaf..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/Classification/Ensemble/RandomForest.php +++ /dev/null @@ -1,157 +0,0 @@ -setSubsetRatio(1.0); - } - - /** - * This method is used to determine how many of the original columns (features) - * will be used to construct subsets to train base classifiers.
- * - * Allowed values: 'sqrt', 'log' or any float number between 0.1 and 1.0
- * - * Default value for the ratio is 'log' which results in log(numFeatures, 2) + 1 - * features to be taken into consideration while selecting subspace of features - * - * @param mixed $ratio - */ - public function setFeatureSubsetRatio($ratio): self - { - if (!is_string($ratio) && !is_float($ratio)) { - throw new InvalidArgumentException('Feature subset ratio must be a string or a float'); - } - - if (is_float($ratio) && ($ratio < 0.1 || $ratio > 1.0)) { - throw new InvalidArgumentException('When a float is given, feature subset ratio should be between 0.1 and 1.0'); - } - - if (is_string($ratio) && $ratio !== 'sqrt' && $ratio !== 'log') { - throw new InvalidArgumentException("When a string is given, feature subset ratio can only be 'sqrt' or 'log'"); - } - - $this->featureSubsetRatio = $ratio; - - return $this; - } - - /** - * RandomForest algorithm is usable *only* with DecisionTree - * - * @return $this - */ - public function setClassifer(string $classifier, array $classifierOptions = []) - { - if ($classifier !== DecisionTree::class) { - throw new InvalidArgumentException('RandomForest can only use DecisionTree as base classifier'); - } - - parent::setClassifer($classifier, $classifierOptions); - - return $this; - } - - /** - * This will return an array including an importance value for - * each column in the given dataset. Importance values for a column - * is the average importance of that column in all trees in the forest - */ - public function getFeatureImportances(): array - { - // Traverse each tree and sum importance of the columns - $sum = []; - foreach ($this->classifiers as $tree) { - /** @var DecisionTree $tree */ - $importances = $tree->getFeatureImportances(); - - foreach ($importances as $column => $importance) { - if (array_key_exists($column, $sum)) { - $sum[$column] += $importance; - } else { - $sum[$column] = $importance; - } - } - } - - // Normalize & sort the importance values - $total = array_sum($sum); - array_walk($sum, function (&$importance) use ($total): void { - $importance /= $total; - }); - arsort($sum); - - return $sum; - } - - /** - * A string array to represent the columns is given. They are useful - * when trying to print some information about the trees such as feature importances - * - * @return $this - */ - public function setColumnNames(array $names) - { - $this->columnNames = $names; - - return $this; - } - - /** - * @return DecisionTree - */ - protected function initSingleClassifier(Classifier $classifier): Classifier - { - if (!$classifier instanceof DecisionTree) { - throw new InvalidArgumentException( - sprintf('Classifier %s expected, got %s', DecisionTree::class, get_class($classifier)) - ); - } - - if (is_float($this->featureSubsetRatio)) { - $featureCount = (int) ($this->featureSubsetRatio * $this->featureCount); - } elseif ($this->featureSubsetRatio === 'sqrt') { - $featureCount = (int) ($this->featureCount ** .5) + 1; - } else { - $featureCount = (int) log($this->featureCount, 2) + 1; - } - - if ($featureCount >= $this->featureCount) { - $featureCount = $this->featureCount; - } - - if ($this->columnNames === null) { - $this->columnNames = range(0, $this->featureCount - 1); - } - - return $classifier - ->setColumnNames($this->columnNames) - ->setNumFeatures($featureCount); - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/Classification/KNearestNeighbors.php b/lib/mlbackend/php/phpml/src/Phpml/Classification/KNearestNeighbors.php deleted file mode 100644 index 9b78baa8377..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/Classification/KNearestNeighbors.php +++ /dev/null @@ -1,75 +0,0 @@ -k = $k; - $this->samples = []; - $this->targets = []; - $this->distanceMetric = $distanceMetric; - } - - /** - * @return mixed - */ - protected function predictSample(array $sample) - { - $distances = $this->kNeighborsDistances($sample); - $predictions = (array) array_combine(array_values($this->targets), array_fill(0, count($this->targets), 0)); - - foreach (array_keys($distances) as $index) { - ++$predictions[$this->targets[$index]]; - } - - arsort($predictions); - reset($predictions); - - return key($predictions); - } - - /** - * @throws \Phpml\Exception\InvalidArgumentException - */ - private function kNeighborsDistances(array $sample): array - { - $distances = []; - - foreach ($this->samples as $index => $neighbor) { - $distances[$index] = $this->distanceMetric->distance($sample, $neighbor); - } - - asort($distances); - - return array_slice($distances, 0, $this->k, true); - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/Classification/Linear/Adaline.php b/lib/mlbackend/php/phpml/src/Phpml/Classification/Linear/Adaline.php deleted file mode 100644 index e5bc9d9af3c..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/Classification/Linear/Adaline.php +++ /dev/null @@ -1,75 +0,0 @@ - - * - * Learning rate should be a float value between 0.0(exclusive) and 1.0 (inclusive)
- * Maximum number of iterations can be an integer value greater than 0
- * If normalizeInputs is set to true, then every input given to the algorithm will be standardized - * by use of standard deviation and mean calculation - * - * @throws InvalidArgumentException - */ - public function __construct( - float $learningRate = 0.001, - int $maxIterations = 1000, - bool $normalizeInputs = true, - int $trainingType = self::BATCH_TRAINING - ) { - if (!in_array($trainingType, [self::BATCH_TRAINING, self::ONLINE_TRAINING], true)) { - throw new InvalidArgumentException('Adaline can only be trained with batch and online/stochastic gradient descent algorithm'); - } - - $this->trainingType = $trainingType; - - parent::__construct($learningRate, $maxIterations, $normalizeInputs); - } - - /** - * Adapts the weights with respect to given samples and targets - * by use of gradient descent learning rule - */ - protected function runTraining(array $samples, array $targets): void - { - // The cost function is the sum of squares - $callback = function ($weights, $sample, $target): array { - $this->weights = $weights; - - $output = $this->output($sample); - $gradient = $output - $target; - $error = $gradient ** 2; - - return [$error, $gradient]; - }; - - $isBatch = $this->trainingType == self::BATCH_TRAINING; - - parent::runGradientDescent($samples, $targets, $callback, $isBatch); - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/Classification/Linear/DecisionStump.php b/lib/mlbackend/php/phpml/src/Phpml/Classification/Linear/DecisionStump.php deleted file mode 100644 index ae81d563da0..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/Classification/Linear/DecisionStump.php +++ /dev/null @@ -1,319 +0,0 @@ - - * - * If columnIndex is given, then the stump tries to produce a decision node - * on this column, otherwise in cases given the value of -1, the stump itself - * decides which column to take for the decision (Default DecisionTree behaviour) - */ - public function __construct(int $columnIndex = self::AUTO_SELECT) - { - $this->givenColumnIndex = $columnIndex; - } - - public function __toString(): string - { - return "IF {$this->column} {$this->operator} {$this->value} ". - 'THEN '.$this->binaryLabels[0].' '. - 'ELSE '.$this->binaryLabels[1]; - } - - /** - * 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 $count 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 InvalidArgumentException - */ - protected function trainBinary(array $samples, array $targets, array $labels): void - { - $this->binaryLabels = $labels; - $this->featureCount = count($samples[0]); - - // If a column index is given, it should be among the existing columns - if ($this->givenColumnIndex > count($samples[0]) - 1) { - $this->givenColumnIndex = self::AUTO_SELECT; - } - - // Check the size of the weights given. - // If none given, then assign 1 as a weight to each sample - if (count($this->weights) === 0) { - $this->weights = array_fill(0, count($samples), 1); - } else { - $numWeights = count($this->weights); - if ($numWeights !== count($samples)) { - throw new InvalidArgumentException('Number of sample weights does not match with number of samples'); - } - } - - // Determine type of each column as either "continuous" or "nominal" - $this->columnTypes = DecisionTree::getColumnTypes($samples); - - // Try to find the best split in the columns of the dataset - // by calculating error rate for each split point in each column - $columns = range(0, count($samples[0]) - 1); - if ($this->givenColumnIndex !== self::AUTO_SELECT) { - $columns = [$this->givenColumnIndex]; - } - - $bestSplit = [ - '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); - } else { - $split = $this->getBestNominalSplit($samples, $targets, $col); - } - - if ($split['trainingErrorRate'] < $bestSplit['trainingErrorRate']) { - $bestSplit = $split; - } - } - - // Assign determined best values to the stump - foreach ($bestSplit as $name => $value) { - $this->{$name} = $value; - } - } - - /** - * Determines best split point for the given column - */ - protected function getBestNumericalSplit(array $samples, array $targets, int $col): array - { - $values = array_column($samples, $col); - // Trying all possible points may be accomplished in two general ways: - // 1- Try all values in the $samples array ($values) - // 2- Artificially split the range of values into several parts and try them - // We choose the second one because it is faster in larger datasets - $minValue = min($values); - $maxValue = max($values); - $stepSize = ($maxValue - $minValue) / $this->numSplitCount; - - $split = []; - - foreach (['<=', '>'] as $operator) { - // Before trying all possible split points, let's first try - // the average value for the cut point - $threshold = array_sum($values) / (float) count($values); - [$errorRate, $prob] = $this->calculateErrorRate($targets, $threshold, $operator, $values); - if (!isset($split['trainingErrorRate']) || $errorRate < $split['trainingErrorRate']) { - $split = [ - 'value' => $threshold, - 'operator' => $operator, - 'prob' => $prob, - 'column' => $col, - 'trainingErrorRate' => $errorRate, - ]; - } - - // Try other possible points one by one - for ($step = $minValue; $step <= $maxValue; $step += $stepSize) { - $threshold = (float) $step; - [$errorRate, $prob] = $this->calculateErrorRate($targets, $threshold, $operator, $values); - if ($errorRate < $split['trainingErrorRate']) { - $split = [ - 'value' => $threshold, - 'operator' => $operator, - 'prob' => $prob, - 'column' => $col, - 'trainingErrorRate' => $errorRate, - ]; - } - }// for - } - - return $split; - } - - protected function getBestNominalSplit(array $samples, array $targets, int $col): array - { - $values = array_column($samples, $col); - $valueCounts = array_count_values($values); - $distinctVals = array_keys($valueCounts); - - $split = []; - - foreach (['=', '!='] as $operator) { - foreach ($distinctVals as $val) { - [$errorRate, $prob] = $this->calculateErrorRate($targets, $val, $operator, $values); - if (!isset($split['trainingErrorRate']) || $split['trainingErrorRate'] < $errorRate) { - $split = [ - 'value' => $val, - 'operator' => $operator, - 'prob' => $prob, - 'column' => $col, - 'trainingErrorRate' => $errorRate, - ]; - } - } - } - - return $split; - } - - /** - * Calculates the ratio of wrong predictions based on the new threshold - * value given as the parameter - */ - protected function calculateErrorRate(array $targets, float $threshold, string $operator, array $values): array - { - $wrong = 0.0; - $prob = []; - $leftLabel = $this->binaryLabels[0]; - $rightLabel = $this->binaryLabels[1]; - - foreach ($values as $index => $value) { - if (Comparison::compare($value, $threshold, $operator)) { - $predicted = $leftLabel; - } else { - $predicted = $rightLabel; - } - - $target = $targets[$index]; - if ((string) $predicted != (string) $targets[$index]) { - $wrong += $this->weights[$index]; - } - - if (!isset($prob[$predicted][$target])) { - $prob[$predicted][$target] = 0; - } - - ++$prob[$predicted][$target]; - } - - // Calculate probabilities: Proportion of labels in each leaf - $dist = array_combine($this->binaryLabels, array_fill(0, 2, 0.0)); - foreach ($prob as $leaf => $counts) { - $leafTotal = (float) array_sum($prob[$leaf]); - foreach ($counts as $label => $count) { - if ((string) $leaf == (string) $label) { - $dist[$leaf] = $count / $leafTotal; - } - } - } - - return [$wrong / (float) array_sum($this->weights), $dist]; - } - - /** - * Returns the probability of the sample of belonging to the given label - * - * Probability of a sample is calculated as the proportion of the label - * within the labels of the training samples in the decision node - * - * @param mixed $label - */ - protected function predictProbability(array $sample, $label): float - { - $predicted = $this->predictSampleBinary($sample); - if ((string) $predicted == (string) $label) { - return $this->prob[$label]; - } - - return 0.0; - } - - /** - * @return mixed - */ - protected function predictSampleBinary(array $sample) - { - if (Comparison::compare($sample[$this->column], $this->value, $this->operator)) { - return $this->binaryLabels[0]; - } - - return $this->binaryLabels[1]; - } - - protected function resetBinary(): void - { - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/Classification/Linear/LogisticRegression.php b/lib/mlbackend/php/phpml/src/Phpml/Classification/Linear/LogisticRegression.php deleted file mode 100644 index 4014fb073dc..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/Classification/Linear/LogisticRegression.php +++ /dev/null @@ -1,283 +0,0 @@ - - * - 'log' : log likelihood
- * - 'sse' : sum of squared errors
- * - * @var string - */ - protected $costFunction = 'log'; - - /** - * Regularization term: only 'L2' is supported - * - * @var string - */ - protected $penalty = 'L2'; - - /** - * Lambda (λ) parameter of regularization term. If λ is set to 0, then - * regularization term is cancelled. - * - * @var float - */ - protected $lambda = 0.5; - - /** - * Initalize a Logistic Regression classifier with maximum number of iterations - * and learning rule to be applied
- * - * Maximum number of iterations can be an integer value greater than 0
- * If normalizeInputs is set to true, then every input given to the algorithm will be standardized - * by use of standard deviation and mean calculation
- * - * Cost function can be 'log' for log-likelihood and 'sse' for sum of squared errors
- * - * Penalty (Regularization term) can be 'L2' or empty string to cancel penalty term - * - * @throws InvalidArgumentException - */ - public function __construct( - int $maxIterations = 500, - bool $normalizeInputs = true, - int $trainingType = self::CONJUGATE_GRAD_TRAINING, - string $cost = 'log', - string $penalty = 'L2' - ) { - $trainingTypes = range(self::BATCH_TRAINING, self::CONJUGATE_GRAD_TRAINING); - if (!in_array($trainingType, $trainingTypes, true)) { - throw new InvalidArgumentException( - 'Logistic regression can only be trained with '. - 'batch (gradient descent), online (stochastic gradient descent) '. - 'or conjugate batch (conjugate gradients) algorithms' - ); - } - - if (!in_array($cost, ['log', 'sse'], true)) { - throw new InvalidArgumentException( - "Logistic regression cost function can be one of the following: \n". - "'log' for log-likelihood and 'sse' for sum of squared errors" - ); - } - - if ($penalty !== '' && strtoupper($penalty) !== 'L2') { - throw new InvalidArgumentException('Logistic regression supports only \'L2\' regularization'); - } - - $this->learningRate = 0.001; - - parent::__construct($this->learningRate, $maxIterations, $normalizeInputs); - - $this->trainingType = $trainingType; - $this->costFunction = $cost; - $this->penalty = $penalty; - } - - /** - * Sets the learning rate if gradient descent algorithm is - * selected for training - */ - public function setLearningRate(float $learningRate): void - { - $this->learningRate = $learningRate; - } - - /** - * Lambda (λ) parameter of regularization term. If 0 is given, - * then the regularization term is cancelled - */ - public function setLambda(float $lambda): void - { - $this->lambda = $lambda; - } - - /** - * Adapts the weights with respect to given samples and targets - * by use of selected solver - * - * @throws \Exception - */ - protected function runTraining(array $samples, array $targets): void - { - $callback = $this->getCostFunction(); - - switch ($this->trainingType) { - case self::BATCH_TRAINING: - $this->runGradientDescent($samples, $targets, $callback, true); - - return; - - case self::ONLINE_TRAINING: - $this->runGradientDescent($samples, $targets, $callback, false); - - return; - - case self::CONJUGATE_GRAD_TRAINING: - $this->runConjugateGradient($samples, $targets, $callback); - - return; - - default: - // Not reached - throw new Exception(sprintf('Logistic regression has invalid training type: %d.', $this->trainingType)); - } - } - - /** - * Executes Conjugate Gradient method to optimize the weights of the LogReg model - */ - protected function runConjugateGradient(array $samples, array $targets, Closure $gradientFunc): void - { - if ($this->optimizer === null) { - $this->optimizer = (new ConjugateGradient($this->featureCount)) - ->setMaxIterations($this->maxIterations); - } - - $this->weights = $this->optimizer->runOptimization($samples, $targets, $gradientFunc); - $this->costValues = $this->optimizer->getCostValues(); - } - - /** - * Returns the appropriate callback function for the selected cost function - * - * @throws \Exception - */ - protected function getCostFunction(): Closure - { - $penalty = 0; - if ($this->penalty === 'L2') { - $penalty = $this->lambda; - } - - switch ($this->costFunction) { - case 'log': - /* - * Negative of Log-likelihood cost function to be minimized: - * J(x) = ∑( - y . log(h(x)) - (1 - y) . log(1 - h(x))) - * - * If regularization term is given, then it will be added to the cost: - * for L2 : J(x) = J(x) + λ/m . w - * - * The gradient of the cost function to be used with gradient descent: - * ∇J(x) = -(y - h(x)) = (h(x) - y) - */ - return function ($weights, $sample, $y) use ($penalty): array { - $this->weights = $weights; - $hX = $this->output($sample); - - // In cases where $hX = 1 or $hX = 0, the log-likelihood - // value will give a NaN, so we fix these values - if ($hX == 1) { - $hX = 1 - 1e-10; - } - - if ($hX == 0) { - $hX = 1e-10; - } - - $y = $y < 0 ? 0 : 1; - - $error = -$y * log($hX) - (1 - $y) * log(1 - $hX); - $gradient = $hX - $y; - - return [$error, $gradient, $penalty]; - }; - case 'sse': - /* - * Sum of squared errors or least squared errors cost function: - * J(x) = ∑ (y - h(x))^2 - * - * If regularization term is given, then it will be added to the cost: - * for L2 : J(x) = J(x) + λ/m . w - * - * The gradient of the cost function: - * ∇J(x) = -(h(x) - y) . h(x) . (1 - h(x)) - */ - return function ($weights, $sample, $y) use ($penalty): array { - $this->weights = $weights; - $hX = $this->output($sample); - - $y = $y < 0 ? 0 : 1; - - $error = (($y - $hX) ** 2); - $gradient = -($y - $hX) * $hX * (1 - $hX); - - return [$error, $gradient, $penalty]; - }; - default: - // Not reached - throw new Exception(sprintf('Logistic regression has invalid cost function: %s.', $this->costFunction)); - } - } - - /** - * Returns the output of the network, a float value between 0.0 and 1.0 - */ - protected function output(array $sample): float - { - $sum = parent::output($sample); - - return 1.0 / (1.0 + exp(-$sum)); - } - - /** - * Returns the class value (either -1 or 1) for the given input - */ - protected function outputClass(array $sample): int - { - $output = $this->output($sample); - - if ($output > 0.5) { - return 1; - } - - return -1; - } - - /** - * Returns the probability of the sample of belonging to the given label. - * - * 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 - { - $sample = $this->checkNormalizedSample($sample); - $probability = $this->output($sample); - - if (array_search($label, $this->labels, true) > 0) { - return $probability; - } - - return 1 - $probability; - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/Classification/Linear/Perceptron.php b/lib/mlbackend/php/phpml/src/Phpml/Classification/Linear/Perceptron.php deleted file mode 100644 index 44220a60580..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/Classification/Linear/Perceptron.php +++ /dev/null @@ -1,264 +0,0 @@ - 1.0) { - throw new InvalidArgumentException('Learning rate should be a float value between 0.0(exclusive) and 1.0(inclusive)'); - } - - if ($maxIterations <= 0) { - throw new InvalidArgumentException('Maximum number of iterations must be an integer greater than 0'); - } - - if ($normalizeInputs) { - $this->normalizer = new Normalizer(Normalizer::NORM_STD); - } - - $this->learningRate = $learningRate; - $this->maxIterations = $maxIterations; - } - - public function partialTrain(array $samples, array $targets, array $labels = []): void - { - $this->trainByLabel($samples, $targets, $labels); - } - - public function trainBinary(array $samples, array $targets, array $labels): void - { - if ($this->normalizer !== null) { - $this->normalizer->transform($samples); - } - - // Set all target values to either -1 or 1 - $this->labels = [ - 1 => $labels[0], - -1 => $labels[1], - ]; - foreach ($targets as $key => $target) { - $targets[$key] = (string) $target == (string) $this->labels[1] ? 1 : -1; - } - - // Set samples and feature count vars - $this->featureCount = count($samples[0]); - - $this->runTraining($samples, $targets); - } - - /** - * Normally enabling early stopping for the optimization procedure may - * help saving processing time while in some cases it may result in - * premature convergence.
- * - * If "false" is given, the optimization procedure will always be executed - * for $maxIterations times - * - * @return $this - */ - public function setEarlyStop(bool $enable = true) - { - $this->enableEarlyStop = $enable; - - return $this; - } - - /** - * Returns the cost values obtained during the training. - */ - public function getCostValues(): array - { - return $this->costValues; - } - - protected function resetBinary(): void - { - $this->labels = []; - $this->optimizer = null; - $this->featureCount = 0; - $this->weights = []; - $this->costValues = []; - } - - /** - * Trains the perceptron model with Stochastic Gradient Descent optimization - * to get the correct set of weights - */ - protected function runTraining(array $samples, array $targets): void - { - // The cost function is the sum of squares - $callback = function ($weights, $sample, $target): array { - $this->weights = $weights; - - $prediction = $this->outputClass($sample); - $gradient = $prediction - $target; - $error = $gradient ** 2; - - return [$error, $gradient]; - }; - - $this->runGradientDescent($samples, $targets, $callback); - } - - /** - * Executes a Gradient Descent algorithm for - * the given cost function - */ - protected function runGradientDescent(array $samples, array $targets, Closure $gradientFunc, bool $isBatch = false): void - { - $class = $isBatch ? GD::class : StochasticGD::class; - - if ($this->optimizer === null) { - $this->optimizer = (new $class($this->featureCount)) - ->setLearningRate($this->learningRate) - ->setMaxIterations($this->maxIterations) - ->setChangeThreshold(1e-6) - ->setEarlyStop($this->enableEarlyStop); - } - - $this->weights = $this->optimizer->runOptimization($samples, $targets, $gradientFunc); - $this->costValues = $this->optimizer->getCostValues(); - } - - /** - * Checks if the sample should be normalized and if so, returns the - * normalized sample - */ - protected function checkNormalizedSample(array $sample): array - { - if ($this->normalizer !== null) { - $samples = [$sample]; - $this->normalizer->transform($samples); - $sample = $samples[0]; - } - - return $sample; - } - - /** - * Calculates net output of the network as a float value for the given input - * - * @return int|float - */ - protected function output(array $sample) - { - $sum = 0; - foreach ($this->weights as $index => $w) { - if ($index == 0) { - $sum += $w; - } else { - $sum += $w * $sample[$index - 1]; - } - } - - return $sum; - } - - /** - * Returns the class value (either -1 or 1) for the given input - */ - protected function outputClass(array $sample): int - { - return $this->output($sample) > 0 ? 1 : -1; - } - - /** - * Returns the probability of the sample of belonging to the given label. - * - * 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 - { - $predicted = $this->predictSampleBinary($sample); - - if ((string) $predicted == (string) $label) { - $sample = $this->checkNormalizedSample($sample); - - return (float) abs($this->output($sample)); - } - - return 0.0; - } - - /** - * @return mixed - */ - protected function predictSampleBinary(array $sample) - { - $sample = $this->checkNormalizedSample($sample); - - $predictedClass = $this->outputClass($sample); - - return $this->labels[$predictedClass]; - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/Classification/MLPClassifier.php b/lib/mlbackend/php/phpml/src/Phpml/Classification/MLPClassifier.php deleted file mode 100644 index 35678d5b667..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/Classification/MLPClassifier.php +++ /dev/null @@ -1,58 +0,0 @@ -classes, true)) { - throw new InvalidArgumentException( - sprintf('Target with value "%s" is not part of the accepted classes', $target) - ); - } - - return array_search($target, $this->classes, true); - } - - /** - * @return mixed - */ - protected function predictSample(array $sample) - { - $output = $this->setInput($sample)->getOutput(); - - $predictedClass = null; - $max = 0; - foreach ($output as $class => $value) { - if ($value > $max) { - $predictedClass = $class; - $max = $value; - } - } - - return $predictedClass; - } - - /** - * @param mixed $target - */ - protected function trainSample(array $sample, $target): void - { - // Feed-forward. - $this->setInput($sample); - - // Back-propagate. - $this->backpropagation->backpropagate($this->getLayers(), $this->getTargetClass($target)); - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/Classification/NaiveBayes.php b/lib/mlbackend/php/phpml/src/Phpml/Classification/NaiveBayes.php deleted file mode 100644 index 079b6f7ff11..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/Classification/NaiveBayes.php +++ /dev/null @@ -1,184 +0,0 @@ -samples = array_merge($this->samples, $samples); - $this->targets = array_merge($this->targets, $targets); - $this->sampleCount = count($this->samples); - $this->featureCount = count($this->samples[0]); - - $this->labels = array_map('strval', array_flip(array_flip($this->targets))); - foreach ($this->labels as $label) { - $samples = $this->getSamplesByLabel($label); - $this->p[$label] = count($samples) / $this->sampleCount; - $this->calculateStatistics($label, $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); - } - - /** - * Calculates vital statistics for each label & feature. Stores these - * values in private array in order to avoid repeated calculation - */ - private function calculateStatistics(string $label, array $samples): void - { - $this->std[$label] = array_fill(0, $this->featureCount, 0); - $this->mean[$label] = array_fill(0, $this->featureCount, 0); - $this->dataType[$label] = array_fill(0, $this->featureCount, self::CONTINUOS); - $this->discreteProb[$label] = array_fill(0, $this->featureCount, self::CONTINUOS); - for ($i = 0; $i < $this->featureCount; ++$i) { - // Get the values of nth column in the samples array - // Mean::arithmetic is called twice, can be optimized - $values = array_column($samples, $i); - $numValues = count($values); - // if the values contain non-numeric data, - // then it should be treated as nominal/categorical/discrete column - if ($values !== array_filter($values, 'is_numeric')) { - $this->dataType[$label][$i] = self::NOMINAL; - $this->discreteProb[$label][$i] = array_count_values($values); - $db = &$this->discreteProb[$label][$i]; - $db = array_map(function ($el) use ($numValues) { - return $el / $numValues; - }, $db); - } else { - $this->mean[$label][$i] = Mean::arithmetic($values); - // Add epsilon in order to avoid zero stdev - $this->std[$label][$i] = 1e-10 + StandardDeviation::population($values, false); - } - } - } - - /** - * Calculates the probability P(label|sample_n) - */ - private function sampleProbability(array $sample, int $feature, string $label): float - { - if (!isset($sample[$feature])) { - throw new InvalidArgumentException('Missing feature. All samples must have equal number of features'); - } - - $value = $sample[$feature]; - if ($this->dataType[$label][$feature] == self::NOMINAL) { - if (!isset($this->discreteProb[$label][$feature][$value]) || - $this->discreteProb[$label][$feature][$value] == 0) { - return self::EPSILON; - } - - return $this->discreteProb[$label][$feature][$value]; - } - - $std = $this->std[$label][$feature]; - $mean = $this->mean[$label][$feature]; - // Calculate the probability density by use of normal/Gaussian distribution - // Ref: https://en.wikipedia.org/wiki/Normal_distribution - // - // In order to avoid numerical errors because of small or zero values, - // some libraries adopt taking log of calculations such as - // scikit-learn did. - // (See : https://github.com/scikit-learn/scikit-learn/blob/master/sklearn/naive_bayes.py) - $pdf = -0.5 * log(2.0 * M_PI * $std * $std); - $pdf -= 0.5 * (($value - $mean) ** 2) / ($std * $std); - - return $pdf; - } - - /** - * Return samples belonging to specific label - */ - private function getSamplesByLabel(string $label): array - { - $samples = []; - for ($i = 0; $i < $this->sampleCount; ++$i) { - if ($this->targets[$i] == $label) { - $samples[] = $this->samples[$i]; - } - } - - return $samples; - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/Classification/SVC.php b/lib/mlbackend/php/phpml/src/Phpml/Classification/SVC.php deleted file mode 100644 index fbc47bacef2..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/Classification/SVC.php +++ /dev/null @@ -1,26 +0,0 @@ -weights = $weights; - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/Clustering/Clusterer.php b/lib/mlbackend/php/phpml/src/Phpml/Clustering/Clusterer.php deleted file mode 100644 index 22e65e6c042..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/Clustering/Clusterer.php +++ /dev/null @@ -1,10 +0,0 @@ -epsilon = $epsilon; - $this->minSamples = $minSamples; - $this->distanceMetric = $distanceMetric; - } - - public function cluster(array $samples): array - { - $labels = []; - $n = 0; - - foreach ($samples as $index => $sample) { - if (isset($labels[$index])) { - continue; - } - - $neighborIndices = $this->getIndicesInRegion($sample, $samples); - - if (count($neighborIndices) < $this->minSamples) { - $labels[$index] = self::NOISE; - - continue; - } - - $labels[$index] = $n; - - $this->expandCluster($samples, $neighborIndices, $labels, $n); - - ++$n; - } - - return $this->groupByCluster($samples, $labels, $n); - } - - private function expandCluster(array $samples, array $seeds, array &$labels, int $n): void - { - while (($index = array_pop($seeds)) !== null) { - if (isset($labels[$index])) { - if ($labels[$index] === self::NOISE) { - $labels[$index] = $n; - } - - continue; - } - - $labels[$index] = $n; - - $sample = $samples[$index]; - $neighborIndices = $this->getIndicesInRegion($sample, $samples); - - if (count($neighborIndices) >= $this->minSamples) { - $seeds = array_unique(array_merge($seeds, $neighborIndices)); - } - } - } - - private function getIndicesInRegion(array $center, array $samples): array - { - $indices = []; - - foreach ($samples as $index => $sample) { - if ($this->distanceMetric->distance($center, $sample) < $this->epsilon) { - $indices[] = $index; - } - } - - return $indices; - } - - private function groupByCluster(array $samples, array $labels, int $n): array - { - $clusters = array_fill(0, $n, []); - - foreach ($samples as $index => $sample) { - if ($labels[$index] !== self::NOISE) { - $clusters[$labels[$index]][$index] = $sample; - } - } - - // Reindex (i.e. to 0, 1, 2, ...) integer indices for backword compatibility - foreach ($clusters as $index => $cluster) { - $clusters[$index] = array_merge($cluster, []); - } - - return $clusters; - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/Clustering/FuzzyCMeans.php b/lib/mlbackend/php/phpml/src/Phpml/Clustering/FuzzyCMeans.php deleted file mode 100644 index ce86f5f9929..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/Clustering/FuzzyCMeans.php +++ /dev/null @@ -1,236 +0,0 @@ -clustersNumber = $clustersNumber; - $this->fuzziness = $fuzziness; - $this->epsilon = $epsilon; - $this->maxIterations = $maxIterations; - } - - public function getMembershipMatrix(): array - { - return $this->membership; - } - - 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); - $cluster = $this->clusters[key($column)]; - $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 - // We initilize the membership array with random values - $dim = $this->space->getDimension(); - $this->generateRandomMembership($dim, $this->sampleCount); - $this->updateClusters(); - } - - protected function generateRandomMembership(int $rows, int $cols): void - { - $this->membership = []; - for ($i = 0; $i < $rows; ++$i) { - $row = []; - $total = 0.0; - for ($k = 0; $k < $cols; ++$k) { - $val = random_int(1, 5) / 10.0; - $row[] = $val; - $total += $val; - } - - $this->membership[] = array_map(static function ($val) use ($total): float { - return $val / $total; - }, $row); - } - } - - protected function updateClusters(): void - { - $dim = $this->space->getDimension(); - if (count($this->clusters) === 0) { - for ($i = 0; $i < $this->clustersNumber; ++$i) { - $this->clusters[] = new Cluster($this->space, array_fill(0, $dim, 0.0)); - } - } - - for ($i = 0; $i < $this->clustersNumber; ++$i) { - $cluster = $this->clusters[$i]; - $center = $cluster->getCoordinates(); - for ($k = 0; $k < $dim; ++$k) { - $a = $this->getMembershipRowTotal($i, $k, true); - $b = $this->getMembershipRowTotal($i, $k, false); - $center[$k] = $a / $b; - } - - $cluster->setCoordinates($center); - } - } - - protected function getMembershipRowTotal(int $row, int $col, bool $multiply): float - { - $sum = 0.0; - for ($k = 0; $k < $this->sampleCount; ++$k) { - $val = $this->membership[$row][$k] ** $this->fuzziness; - if ($multiply) { - $val *= $this->samples[$k][$col]; - } - - $sum += $val; - } - - return $sum; - } - - protected function updateMembershipMatrix(): void - { - for ($i = 0; $i < $this->clustersNumber; ++$i) { - for ($k = 0; $k < $this->sampleCount; ++$k) { - $distCalc = $this->getDistanceCalc($i, $k); - $this->membership[$i][$k] = 1.0 / $distCalc; - } - } - } - - protected function getDistanceCalc(int $row, int $col): float - { - $sum = 0.0; - $distance = new Euclidean(); - $dist1 = $distance->distance( - $this->clusters[$row]->getCoordinates(), - $this->samples[$col] - ); - - for ($j = 0; $j < $this->clustersNumber; ++$j) { - $dist2 = $distance->distance( - $this->clusters[$j]->getCoordinates(), - $this->samples[$col] - ); - - $val = (($dist1 / $dist2) ** 2.0) / ($this->fuzziness - 1); - $sum += $val; - } - - return $sum; - } - - /** - * The objective is to minimize the distance between all data points - * and all cluster centers. This method returns the summation of all - * these distances - */ - protected function getObjective(): float - { - $sum = 0.0; - $distance = new Euclidean(); - for ($i = 0; $i < $this->clustersNumber; ++$i) { - $clust = $this->clusters[$i]->getCoordinates(); - for ($k = 0; $k < $this->sampleCount; ++$k) { - $point = $this->samples[$k]; - $sum += $distance->distance($clust, $point); - } - } - - return $sum; - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/Clustering/KMeans.php b/lib/mlbackend/php/phpml/src/Phpml/Clustering/KMeans.php deleted file mode 100644 index 1aff1c4eecf..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/Clustering/KMeans.php +++ /dev/null @@ -1,50 +0,0 @@ -clustersNumber = $clustersNumber; - $this->initialization = $initialization; - } - - public function cluster(array $samples): array - { - $space = new Space(count(reset($samples))); - foreach ($samples as $key => $sample) { - $space->addPoint($sample, $key); - } - - $clusters = []; - foreach ($space->cluster($this->clustersNumber, $this->initialization) as $cluster) { - $clusters[] = $cluster->getPoints(); - } - - return $clusters; - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/Clustering/KMeans/Cluster.php b/lib/mlbackend/php/phpml/src/Phpml/Clustering/KMeans/Cluster.php deleted file mode 100644 index f4c3d3ee12b..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/Clustering/KMeans/Cluster.php +++ /dev/null @@ -1,117 +0,0 @@ -space = $space; - $this->points = new SplObjectStorage(); - } - - public function getPoints(): array - { - $points = []; - foreach ($this->points as $point) { - if ($point->label === null) { - $points[] = $point->toArray(); - } else { - $points[$point->label] = $point->toArray(); - } - } - - return $points; - } - - public function toArray(): array - { - return [ - 'centroid' => parent::toArray(), - 'points' => $this->getPoints(), - ]; - } - - public function attach(Point $point): Point - { - if ($point instanceof self) { - throw new LogicException('Cannot attach a cluster to another'); - } - - $this->points->attach($point); - - return $point; - } - - public function detach(Point $point): Point - { - $this->points->detach($point); - - return $point; - } - - public function attachAll(SplObjectStorage $points): void - { - $this->points->addAll($points); - } - - public function detachAll(SplObjectStorage $points): void - { - $this->points->removeAll($points); - } - - public function updateCentroid(): void - { - $count = count($this->points); - if ($count === 0) { - return; - } - - $centroid = $this->space->newPoint(array_fill(0, $this->dimension, 0)); - - foreach ($this->points as $point) { - for ($n = 0; $n < $this->dimension; ++$n) { - $centroid->coordinates[$n] += $point->coordinates[$n]; - } - } - - for ($n = 0; $n < $this->dimension; ++$n) { - $this->coordinates[$n] = $centroid->coordinates[$n] / $count; - } - } - - /** - * @return Point[]|SplObjectStorage - */ - public function getIterator() - { - return $this->points; - } - - public function count(): int - { - return count($this->points); - } - - public function setCoordinates(array $newCoordinates): void - { - $this->coordinates = $newCoordinates; - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/Clustering/KMeans/Point.php b/lib/mlbackend/php/phpml/src/Phpml/Clustering/KMeans/Point.php deleted file mode 100644 index a3f195d8651..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/Clustering/KMeans/Point.php +++ /dev/null @@ -1,125 +0,0 @@ -dimension = count($coordinates); - $this->coordinates = $coordinates; - $this->label = $label; - } - - public function toArray(): array - { - return $this->coordinates; - } - - /** - * @return float|int - */ - public function getDistanceWith(self $point, bool $precise = true) - { - $distance = 0; - for ($n = 0; $n < $this->dimension; ++$n) { - $difference = $this->coordinates[$n] - $point->coordinates[$n]; - $distance += $difference * $difference; - } - - return $precise ? $distance ** .5 : $distance; - } - - /** - * @param Point[] $points - */ - public function getClosest(array $points): ?self - { - $minPoint = null; - - foreach ($points as $point) { - $distance = $this->getDistanceWith($point, false); - - if (!isset($minDistance)) { - $minDistance = $distance; - $minPoint = $point; - - continue; - } - - if ($distance < $minDistance) { - $minDistance = $distance; - $minPoint = $point; - } - } - - return $minPoint; - } - - public function getCoordinates(): array - { - return $this->coordinates; - } - - /** - * @param mixed $offset - */ - public function offsetExists($offset): bool - { - return isset($this->coordinates[$offset]); - } - - /** - * @param mixed $offset - * - * @return mixed - */ - public function offsetGet($offset) - { - return $this->coordinates[$offset]; - } - - /** - * @param mixed $offset - * @param mixed $value - */ - public function offsetSet($offset, $value): void - { - $this->coordinates[$offset] = $value; - } - - /** - * @param mixed $offset - */ - public function offsetUnset($offset): void - { - unset($this->coordinates[$offset]); - } - - public function count(): int - { - return count($this->coordinates); - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/Clustering/KMeans/Space.php b/lib/mlbackend/php/phpml/src/Phpml/Clustering/KMeans/Space.php deleted file mode 100644 index e4207cca6bc..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/Clustering/KMeans/Space.php +++ /dev/null @@ -1,263 +0,0 @@ -dimension = $dimension; - } - - public function toArray(): array - { - $points = []; - - /** @var Point $point */ - foreach ($this as $point) { - $points[] = $point->toArray(); - } - - return ['points' => $points]; - } - - /** - * @param mixed $label - */ - public function newPoint(array $coordinates, $label = null): Point - { - if (count($coordinates) !== $this->dimension) { - throw new LogicException('('.implode(',', $coordinates).') is not a point of this space'); - } - - return new Point($coordinates, $label); - } - - /** - * @param mixed $label - * @param mixed $data - */ - public function addPoint(array $coordinates, $label = null, $data = null): void - { - $this->attach($this->newPoint($coordinates, $label), $data); - } - - /** - * @param object $point - * @param mixed $data - */ - public function attach($point, $data = null): void - { - if (!$point instanceof Point) { - throw new InvalidArgumentException('can only attach points to spaces'); - } - - parent::attach($point, $data); - } - - public function getDimension(): int - { - return $this->dimension; - } - - /** - * @return array|bool - */ - public function getBoundaries() - { - if (count($this) === 0) { - return false; - } - - $min = $this->newPoint(array_fill(0, $this->dimension, null)); - $max = $this->newPoint(array_fill(0, $this->dimension, null)); - - /** @var Point $point */ - foreach ($this as $point) { - for ($n = 0; $n < $this->dimension; ++$n) { - if ($min[$n] === null || $min[$n] > $point[$n]) { - $min[$n] = $point[$n]; - } - - if ($max[$n] === null || $max[$n] < $point[$n]) { - $max[$n] = $point[$n]; - } - } - } - - return [$min, $max]; - } - - public function getRandomPoint(Point $min, Point $max): Point - { - $point = $this->newPoint(array_fill(0, $this->dimension, null)); - - for ($n = 0; $n < $this->dimension; ++$n) { - $point[$n] = random_int($min[$n], $max[$n]); - } - - return $point; - } - - /** - * @return Cluster[] - */ - public function cluster(int $clustersNumber, int $initMethod = KMeans::INIT_RANDOM): array - { - $clusters = $this->initializeClusters($clustersNumber, $initMethod); - - do { - } while (!$this->iterate($clusters)); - - return $clusters; - } - - /** - * @return Cluster[] - */ - protected function initializeClusters(int $clustersNumber, int $initMethod): array - { - switch ($initMethod) { - case KMeans::INIT_RANDOM: - $clusters = $this->initializeRandomClusters($clustersNumber); - - break; - - case KMeans::INIT_KMEANS_PLUS_PLUS: - $clusters = $this->initializeKMPPClusters($clustersNumber); - - break; - - default: - return []; - } - - $clusters[0]->attachAll($this); - - return $clusters; - } - - /** - * @param Cluster[] $clusters - */ - protected function iterate(array $clusters): bool - { - $convergence = true; - - $attach = new SplObjectStorage(); - $detach = new SplObjectStorage(); - - foreach ($clusters as $cluster) { - foreach ($cluster as $point) { - $closest = $point->getClosest($clusters); - - if ($closest === null) { - continue; - } - - if ($closest !== $cluster) { - $attach[$closest] ?? $attach[$closest] = new SplObjectStorage(); - $detach[$cluster] ?? $detach[$cluster] = new SplObjectStorage(); - - $attach[$closest]->attach($point); - $detach[$cluster]->attach($point); - - $convergence = false; - } - } - } - - /** @var Cluster $cluster */ - foreach ($attach as $cluster) { - $cluster->attachAll($attach[$cluster]); - } - - /** @var Cluster $cluster */ - foreach ($detach as $cluster) { - $cluster->detachAll($detach[$cluster]); - } - - foreach ($clusters as $cluster) { - $cluster->updateCentroid(); - } - - return $convergence; - } - - /** - * @return Cluster[] - */ - protected function initializeKMPPClusters(int $clustersNumber): array - { - $clusters = []; - $this->rewind(); - - /** @var Point $current */ - $current = $this->current(); - - $clusters[] = new Cluster($this, $current->getCoordinates()); - - $distances = new SplObjectStorage(); - - for ($i = 1; $i < $clustersNumber; ++$i) { - $sum = 0; - /** @var Point $point */ - foreach ($this as $point) { - $closest = $point->getClosest($clusters); - if ($closest === null) { - continue; - } - - $distance = $point->getDistanceWith($closest); - $sum += $distances[$point] = $distance; - } - - $sum = random_int(0, (int) $sum); - /** @var Point $point */ - foreach ($this as $point) { - $sum -= $distances[$point]; - - if ($sum > 0) { - continue; - } - - $clusters[] = new Cluster($this, $point->getCoordinates()); - - break; - } - } - - return $clusters; - } - - /** - * @return Cluster[] - */ - 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; - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/CrossValidation/RandomSplit.php b/lib/mlbackend/php/phpml/src/Phpml/CrossValidation/RandomSplit.php deleted file mode 100644 index 8507ee58161..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/CrossValidation/RandomSplit.php +++ /dev/null @@ -1,26 +0,0 @@ -getSamples(); - $labels = $dataset->getTargets(); - $datasetSize = count($samples); - $testCount = count($this->testSamples); - - for ($i = $datasetSize; $i > 0; --$i) { - $key = mt_rand(0, $datasetSize - 1); - $setName = (count($this->testSamples) - $testCount) / $datasetSize >= $testSize ? 'train' : 'test'; - - $this->{$setName.'Samples'}[] = $samples[$key]; - $this->{$setName.'Labels'}[] = $labels[$key]; - } - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/CrossValidation/Split.php b/lib/mlbackend/php/phpml/src/Phpml/CrossValidation/Split.php deleted file mode 100644 index e9d401c477c..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/CrossValidation/Split.php +++ /dev/null @@ -1,73 +0,0 @@ -= 1) { - throw new InvalidArgumentException('testsize must be between 0.0 and 1.0'); - } - - $this->seedGenerator($seed); - - $this->splitDataset($dataset, $testSize); - } - - public function getTrainSamples(): array - { - return $this->trainSamples; - } - - public function getTestSamples(): array - { - return $this->testSamples; - } - - public function getTrainLabels(): array - { - return $this->trainLabels; - } - - public function getTestLabels(): array - { - return $this->testLabels; - } - - abstract protected function splitDataset(Dataset $dataset, float $testSize): void; - - protected function seedGenerator(?int $seed = null): void - { - if ($seed === null) { - mt_srand(); - } else { - mt_srand($seed); - } - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/CrossValidation/StratifiedRandomSplit.php b/lib/mlbackend/php/phpml/src/Phpml/CrossValidation/StratifiedRandomSplit.php deleted file mode 100644 index 3b3acc47845..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/CrossValidation/StratifiedRandomSplit.php +++ /dev/null @@ -1,49 +0,0 @@ -splitByTarget($dataset); - - foreach ($datasets as $targetSet) { - parent::splitDataset($targetSet, $testSize); - } - } - - /** - * @return Dataset[] - */ - private function splitByTarget(Dataset $dataset): array - { - $targets = $dataset->getTargets(); - $samples = $dataset->getSamples(); - - $uniqueTargets = array_unique($targets); - /** @var array $split */ - $split = array_combine($uniqueTargets, array_fill(0, count($uniqueTargets), [])); - - foreach ($samples as $key => $sample) { - $split[$targets[$key]][] = $sample; - } - - return $this->createDatasets($uniqueTargets, $split); - } - - private function createDatasets(array $uniqueTargets, array $split): array - { - $datasets = []; - foreach ($uniqueTargets as $target) { - $datasets[$target] = new ArrayDataset($split[$target], array_fill(0, count($split[$target]), $target)); - } - - return $datasets; - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/Dataset/ArrayDataset.php b/lib/mlbackend/php/phpml/src/Phpml/Dataset/ArrayDataset.php deleted file mode 100644 index 095c5976467..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/Dataset/ArrayDataset.php +++ /dev/null @@ -1,62 +0,0 @@ -samples = $samples; - $this->targets = $targets; - } - - public function getSamples(): array - { - return $this->samples; - } - - public function getTargets(): array - { - return $this->targets; - } - - /** - * @param int[] $columns - */ - public function removeColumns(array $columns): void - { - foreach ($this->samples as &$sample) { - $this->removeColumnsFromSample($sample, $columns); - } - } - - private function removeColumnsFromSample(array &$sample, array $columns): void - { - foreach ($columns as $index) { - unset($sample[$index]); - } - - $sample = array_values($sample); - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/Dataset/CsvDataset.php b/lib/mlbackend/php/phpml/src/Phpml/Dataset/CsvDataset.php deleted file mode 100644 index 483664d8903..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/Dataset/CsvDataset.php +++ /dev/null @@ -1,52 +0,0 @@ -columnNames = array_slice((array) $data, 0, $features); - } else { - $this->columnNames = range(0, $features - 1); - } - - $samples = $targets = []; - while ($data = fgetcsv($handle, $maxLineLength, $delimiter)) { - $samples[] = array_slice($data, 0, $features); - $targets[] = $data[$features]; - } - - fclose($handle); - - parent::__construct($samples, $targets); - } - - public function getColumnNames(): array - { - return $this->columnNames; - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/Dataset/Dataset.php b/lib/mlbackend/php/phpml/src/Phpml/Dataset/Dataset.php deleted file mode 100644 index 0c775a95805..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/Dataset/Dataset.php +++ /dev/null @@ -1,12 +0,0 @@ -scanRootPath($rootPath); - } - - private function scanRootPath(string $rootPath): void - { - $dirs = glob($rootPath.DIRECTORY_SEPARATOR.'*', GLOB_ONLYDIR); - - if ($dirs === false) { - throw new DatasetException(sprintf('An error occurred during directory "%s" scan', $rootPath)); - } - - foreach ($dirs as $dir) { - $this->scanDir($dir); - } - } - - private function scanDir(string $dir): void - { - $target = basename($dir); - - $files = glob($dir.DIRECTORY_SEPARATOR.'*'); - if ($files === false) { - return; - } - - foreach (array_filter($files, 'is_file') as $file) { - $this->samples[] = file_get_contents($file); - $this->targets[] = $target; - } - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/Dataset/MnistDataset.php b/lib/mlbackend/php/phpml/src/Phpml/Dataset/MnistDataset.php deleted file mode 100644 index 59a3a26dc1f..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/Dataset/MnistDataset.php +++ /dev/null @@ -1,101 +0,0 @@ -samples = $this->readImages($imagePath); - $this->targets = $this->readLabels($labelPath); - - if (count($this->samples) !== count($this->targets)) { - throw new InvalidArgumentException('Must have the same number of images and labels'); - } - } - - private function readImages(string $imagePath): array - { - $stream = fopen($imagePath, 'rb'); - - if ($stream === false) { - throw new InvalidArgumentException('Could not open file: '.$imagePath); - } - - $images = []; - - try { - $header = fread($stream, 16); - - $fields = unpack('Nmagic/Nsize/Nrows/Ncols', (string) $header); - - if ($fields['magic'] !== self::MAGIC_IMAGE) { - throw new InvalidArgumentException('Invalid magic number: '.$imagePath); - } - - if ($fields['rows'] != self::IMAGE_ROWS) { - throw new InvalidArgumentException('Invalid number of image rows: '.$imagePath); - } - - if ($fields['cols'] != self::IMAGE_COLS) { - throw new InvalidArgumentException('Invalid number of image cols: '.$imagePath); - } - - for ($i = 0; $i < $fields['size']; $i++) { - $imageBytes = fread($stream, $fields['rows'] * $fields['cols']); - - // Convert to float between 0 and 1 - $images[] = array_map(function ($b) { - return $b / 255; - }, array_values(unpack('C*', (string) $imageBytes))); - } - } finally { - fclose($stream); - } - - return $images; - } - - private function readLabels(string $labelPath): array - { - $stream = fopen($labelPath, 'rb'); - - if ($stream === false) { - throw new InvalidArgumentException('Could not open file: '.$labelPath); - } - - $labels = []; - - try { - $header = fread($stream, 8); - - $fields = unpack('Nmagic/Nsize', (string) $header); - - if ($fields['magic'] !== self::MAGIC_LABEL) { - throw new InvalidArgumentException('Invalid magic number: '.$labelPath); - } - - $labels = fread($stream, $fields['size']); - } finally { - fclose($stream); - } - - return array_values(unpack('C*', (string) $labels)); - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/Dataset/SvmDataset.php b/lib/mlbackend/php/phpml/src/Phpml/Dataset/SvmDataset.php deleted file mode 100644 index 4ac951c649c..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/Dataset/SvmDataset.php +++ /dev/null @@ -1,131 +0,0 @@ - $maxIndex) { - $maxIndex = $index; - $sample = array_pad($sample, $maxIndex + 1, 0); - } - - $sample[$index] = $value; - } - - return [$sample, $target, $maxIndex]; - } - - private static function parseLine(string $line): array - { - $line = explode('#', $line, 2)[0]; - $line = rtrim($line); - $line = str_replace("\t", ' ', $line); - - return explode(' ', $line); - } - - private static function parseTargetColumn(string $column): float - { - if (!is_numeric($column)) { - throw new DatasetException(sprintf('Invalid target "%s".', $column)); - } - - return (float) $column; - } - - private static function parseFeatureColumn(string $column): array - { - $feature = explode(':', $column, 2); - if (count($feature) !== 2) { - throw new DatasetException(sprintf('Invalid value "%s".', $column)); - } - - $index = self::parseFeatureIndex($feature[0]); - $value = self::parseFeatureValue($feature[1]); - - return [$index, $value]; - } - - private static function parseFeatureIndex(string $index): int - { - if (!is_numeric($index) || !ctype_digit($index)) { - throw new DatasetException(sprintf('Invalid index "%s".', $index)); - } - - if ((int) $index < 1) { - throw new DatasetException(sprintf('Invalid index "%s".', $index)); - } - - return (int) $index - 1; - } - - private static function parseFeatureValue(string $value): float - { - if (!is_numeric($value)) { - throw new DatasetException(sprintf('Invalid value "%s".', $value)); - } - - return (float) $value; - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/DimensionReduction/EigenTransformerBase.php b/lib/mlbackend/php/phpml/src/Phpml/DimensionReduction/EigenTransformerBase.php deleted file mode 100644 index ec64163f37d..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/DimensionReduction/EigenTransformerBase.php +++ /dev/null @@ -1,94 +0,0 @@ -getRealEigenvalues(); - $eigVects = $eig->getEigenvectors(); - - $totalEigVal = array_sum($eigVals); - // Sort eigenvalues in descending order - arsort($eigVals); - - $explainedVar = 0.0; - $vectors = []; - $values = []; - foreach ($eigVals as $i => $eigVal) { - $explainedVar += $eigVal / $totalEigVal; - $vectors[] = $eigVects[$i]; - $values[] = $eigVal; - - if ($this->numFeatures !== null) { - if (count($vectors) == $this->numFeatures) { - break; - } - } else { - if ($explainedVar >= $this->totalVariance) { - break; - } - } - } - - $this->eigValues = $values; - $this->eigVectors = $vectors; - } - - /** - * Returns the reduced data - */ - protected function reduce(array $data): array - { - $m1 = new Matrix($data); - $m2 = new Matrix($this->eigVectors); - - return $m1->multiply($m2->transpose())->toArray(); - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/DimensionReduction/KernelPCA.php b/lib/mlbackend/php/phpml/src/Phpml/DimensionReduction/KernelPCA.php deleted file mode 100644 index beeaba41b0b..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/DimensionReduction/KernelPCA.php +++ /dev/null @@ -1,234 +0,0 @@ -
- * Example: $kpca = new KernelPCA(KernelPCA::KERNEL_RBF, null, 2, 15.0); - * will initialize the algorithm with an RBF kernel having the gamma parameter as 15,0.
- * This transformation will return the same number of rows with only 2 columns. - * - * @param float $totalVariance Total variance to be preserved if numFeatures is not given - * @param int $numFeatures Number of columns to be returned - * @param float $gamma Gamma parameter is used with RBF and Sigmoid kernels - * - * @throws InvalidArgumentException - */ - public function __construct(int $kernel = self::KERNEL_RBF, ?float $totalVariance = null, ?int $numFeatures = null, ?float $gamma = null) - { - if (!in_array($kernel, [self::KERNEL_RBF, self::KERNEL_SIGMOID, self::KERNEL_LAPLACIAN, self::KERNEL_LINEAR], true)) { - throw new InvalidArgumentException('KernelPCA can be initialized with the following kernels only: Linear, RBF, Sigmoid and Laplacian'); - } - - parent::__construct($totalVariance, $numFeatures); - - $this->kernel = $kernel; - $this->gamma = $gamma; - } - - /** - * Takes a data and returns a lower dimensional version - * of this data while preserving $totalVariance or $numFeatures.
- * $data is an n-by-m matrix and returned array is - * n-by-k matrix where k <= m - */ - public function fit(array $data): array - { - $numRows = count($data); - $this->data = $data; - - if ($this->gamma === null) { - $this->gamma = 1.0 / $numRows; - } - - $matrix = $this->calculateKernelMatrix($this->data, $numRows); - $matrix = $this->centerMatrix($matrix, $numRows); - - $this->eigenDecomposition($matrix); - - $this->fit = true; - - return Matrix::transposeArray($this->eigVectors); - } - - /** - * Transforms the given sample to a lower dimensional vector by using - * the variables obtained during the last run of fit. - * - * @throws InvalidArgumentException - * @throws InvalidOperationException - */ - public function transform(array $sample): array - { - if (!$this->fit) { - throw new InvalidOperationException('KernelPCA has not been fitted with respect to original dataset, please run KernelPCA::fit() first'); - } - - if (is_array($sample[0])) { - throw new InvalidArgumentException('KernelPCA::transform() accepts only one-dimensional arrays'); - } - - $pairs = $this->getDistancePairs($sample); - - return $this->projectSample($pairs); - } - - /** - * Calculates similarity matrix by use of selected kernel function
- * An n-by-m matrix is given and an n-by-n matrix is returned - */ - protected function calculateKernelMatrix(array $data, int $numRows): array - { - $kernelFunc = $this->getKernel(); - - $matrix = []; - for ($i = 0; $i < $numRows; ++$i) { - for ($k = 0; $k < $numRows; ++$k) { - if ($i <= $k) { - $matrix[$i][$k] = $kernelFunc($data[$i], $data[$k]); - } else { - $matrix[$i][$k] = $matrix[$k][$i]; - } - } - } - - return $matrix; - } - - /** - * Kernel matrix is centered in its original space by using the following - * conversion: - * - * K′ = K − N.K − K.N + N.K.N where N is n-by-n matrix filled with 1/n - */ - protected function centerMatrix(array $matrix, int $n): array - { - $N = array_fill(0, $n, array_fill(0, $n, 1.0 / $n)); - $N = new Matrix($N, false); - $K = new Matrix($matrix, false); - - // K.N (This term is repeated so we cache it once) - $K_N = $K->multiply($N); - // N.K - $N_K = $N->multiply($K); - // N.K.N - $N_K_N = $N->multiply($K_N); - - return $K->subtract($N_K) - ->subtract($K_N) - ->add($N_K_N) - ->toArray(); - } - - /** - * Returns the callable kernel function - * - * @throws \Exception - */ - protected function getKernel(): Closure - { - switch ($this->kernel) { - case self::KERNEL_LINEAR: - // k(x,y) = xT.y - return function ($x, $y) { - return Matrix::dot($x, $y)[0]; - }; - case self::KERNEL_RBF: - // k(x,y)=exp(-γ.|x-y|) where |..| is Euclidean distance - $dist = new Euclidean(); - - return function ($x, $y) use ($dist): float { - return exp(-$this->gamma * $dist->sqDistance($x, $y)); - }; - - case self::KERNEL_SIGMOID: - // k(x,y)=tanh(γ.xT.y+c0) where c0=1 - return function ($x, $y): float { - $res = Matrix::dot($x, $y)[0] + 1.0; - - return tanh((float) $this->gamma * $res); - }; - - case self::KERNEL_LAPLACIAN: - // k(x,y)=exp(-γ.|x-y|) where |..| is Manhattan distance - $dist = new Manhattan(); - - return function ($x, $y) use ($dist): float { - return exp(-$this->gamma * $dist->distance($x, $y)); - }; - - default: - // Not reached - throw new InvalidArgumentException(sprintf('KernelPCA initialized with invalid kernel: %d', $this->kernel)); - } - } - - protected function getDistancePairs(array $sample): array - { - $kernel = $this->getKernel(); - - $pairs = []; - foreach ($this->data as $row) { - $pairs[] = $kernel($row, $sample); - } - - return $pairs; - } - - protected function projectSample(array $pairs): array - { - // Normalize eigenvectors by eig = eigVectors / eigValues - $func = function ($eigVal, $eigVect) { - $m = new Matrix($eigVect, false); - $a = $m->divideByScalar($eigVal)->toArray(); - - return $a[0]; - }; - $eig = array_map($func, $this->eigValues, $this->eigVectors); - - // return k.dot(eig) - return Matrix::dot($pairs, $eig); - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/DimensionReduction/LDA.php b/lib/mlbackend/php/phpml/src/Phpml/DimensionReduction/LDA.php deleted file mode 100644 index 68ab0cda95e..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/DimensionReduction/LDA.php +++ /dev/null @@ -1,223 +0,0 @@ -
- * The algorithm can be initialized by speciyfing - * either with the totalVariance(a value between 0.1 and 0.99) - * or numFeatures (number of features in the dataset) to be preserved. - * - * @param float|null $totalVariance Total explained variance to be preserved - * @param int|null $numFeatures Number of features to be preserved - * - * @throws InvalidArgumentException - */ - public function __construct(?float $totalVariance = null, ?int $numFeatures = null) - { - if ($totalVariance !== null && ($totalVariance < 0.1 || $totalVariance > 0.99)) { - throw new InvalidArgumentException('Total variance can be a value between 0.1 and 0.99'); - } - - if ($numFeatures !== null && $numFeatures <= 0) { - throw new InvalidArgumentException('Number of features to be preserved should be greater than 0'); - } - - if (($totalVariance !== null) === ($numFeatures !== null)) { - throw new InvalidArgumentException('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; - } - } - - /** - * Trains the algorithm to transform the given data to a lower dimensional space. - */ - public function fit(array $data, array $classes): array - { - $this->labels = $this->getLabels($classes); - $this->means = $this->calculateMeans($data, $classes); - - $sW = $this->calculateClassVar($data, $classes); - $sB = $this->calculateClassCov(); - - $S = $sW->inverse()->multiply($sB); - $this->eigenDecomposition($S->toArray()); - - $this->fit = true; - - return $this->reduce($data); - } - - /** - * Transforms the given sample to a lower dimensional vector by using - * the eigenVectors obtained in the last run of fit. - * - * @throws InvalidOperationException - */ - public function transform(array $sample): array - { - if (!$this->fit) { - throw new InvalidOperationException('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 - */ - protected function getLabels(array $classes): array - { - $counts = array_count_values($classes); - - return array_keys($counts); - } - - /** - * Calculates mean of each column for each class and returns - * n by m matrix where n is number of labels and m is number of columns - */ - protected function calculateMeans(array $data, array $classes): array - { - $means = []; - $counts = []; - $overallMean = array_fill(0, count($data[0]), 0.0); - - foreach ($data as $index => $row) { - $label = array_search($classes[$index], $this->labels, true); - - foreach ($row as $col => $val) { - if (!isset($means[$label][$col])) { - $means[$label][$col] = 0.0; - } - - $means[$label][$col] += $val; - $overallMean[$col] += $val; - } - - if (!isset($counts[$label])) { - $counts[$label] = 0; - } - - ++$counts[$label]; - } - - foreach ($means as $index => $row) { - foreach ($row as $col => $sum) { - $means[$index][$col] = $sum / $counts[$index]; - } - } - - // Calculate overall mean of the dataset for each column - $numElements = array_sum($counts); - $map = function ($el) use ($numElements) { - return $el / $numElements; - }; - $this->overallMean = array_map($map, $overallMean); - $this->counts = $counts; - - return $means; - } - - /** - * Returns in-class scatter matrix for each class, which - * is a n by m matrix where n is number of classes and - * m is number of columns - */ - protected function calculateClassVar(array $data, array $classes): Matrix - { - // s is an n (number of classes) by m (number of column) matrix - $s = array_fill(0, count($data[0]), array_fill(0, count($data[0]), 0)); - $sW = new Matrix($s, false); - - foreach ($data as $index => $row) { - $label = array_search($classes[$index], $this->labels, true); - $means = $this->means[$label]; - - $row = $this->calculateVar($row, $means); - - $sW = $sW->add($row); - } - - return $sW; - } - - /** - * Returns between-class scatter matrix for each class, which - * is an n by m matrix where n is number of classes and - * m is number of columns - */ - protected function calculateClassCov(): Matrix - { - // s is an n (number of classes) by m (number of column) matrix - $s = array_fill(0, count($this->overallMean), array_fill(0, count($this->overallMean), 0)); - $sB = new Matrix($s, false); - - foreach ($this->means as $index => $classMeans) { - $row = $this->calculateVar($classMeans, $this->overallMean); - $N = $this->counts[$index]; - $sB = $sB->add($row->multiplyByScalar($N)); - } - - return $sB; - } - - /** - * Returns the result of the calculation (x - m)T.(x - m) - */ - protected function calculateVar(array $row, array $means): Matrix - { - $x = new Matrix($row, false); - $m = new Matrix($means, false); - $diff = $x->subtract($m); - - return $diff->transpose()->multiply($diff); - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/DimensionReduction/PCA.php b/lib/mlbackend/php/phpml/src/Phpml/DimensionReduction/PCA.php deleted file mode 100644 index 5556558feb9..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/DimensionReduction/PCA.php +++ /dev/null @@ -1,131 +0,0 @@ - - * - * @param float $totalVariance Total explained variance to be preserved - * @param int $numFeatures Number of features to be preserved - * - * @throws InvalidArgumentException - */ - public function __construct(?float $totalVariance = null, ?int $numFeatures = null) - { - if ($totalVariance !== null && ($totalVariance < 0.1 || $totalVariance > 0.99)) { - throw new InvalidArgumentException('Total variance can be a value between 0.1 and 0.99'); - } - - if ($numFeatures !== null && $numFeatures <= 0) { - throw new InvalidArgumentException('Number of features to be preserved should be greater than 0'); - } - - if (($totalVariance !== null) === ($numFeatures !== null)) { - throw new InvalidArgumentException('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; - } - } - - /** - * Takes a data and returns a lower dimensional version - * of this data while preserving $totalVariance or $numFeatures.
- * $data is an n-by-m matrix and returned array is - * n-by-k matrix where k <= m - */ - public function fit(array $data): array - { - $n = count($data[0]); - - $data = $this->normalize($data, $n); - - $covMatrix = Covariance::covarianceMatrix($data, array_fill(0, $n, 0)); - - $this->eigenDecomposition($covMatrix); - - $this->fit = true; - - return $this->reduce($data); - } - - /** - * Transforms the given sample to a lower dimensional vector by using - * the eigenVectors obtained in the last run of fit. - * - * @throws InvalidOperationException - */ - public function transform(array $sample): array - { - if (!$this->fit) { - throw new InvalidOperationException('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 - $this->means = []; - for ($i = 0; $i < $n; ++$i) { - $column = array_column($data, $i); - $this->means[] = Mean::arithmetic($column); - } - } - - /** - * Normalization of the data includes subtracting mean from - * each dimension therefore dimensions will be centered to zero - */ - protected function normalize(array $data, int $n): array - { - if (count($this->means) === 0) { - $this->calculateMeans($data, $n); - } - - // Normalize data - foreach (array_keys($data) as $i) { - for ($k = 0; $k < $n; ++$k) { - $data[$i][$k] -= $this->means[$k]; - } - } - - return $data; - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/Estimator.php b/lib/mlbackend/php/phpml/src/Phpml/Estimator.php deleted file mode 100644 index a0541089ca1..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/Estimator.php +++ /dev/null @@ -1,15 +0,0 @@ -stopWords = array_fill_keys($stopWords, true); - } - - public function isStopWord(string $token): bool - { - return isset($this->stopWords[$token]); - } - - public static function factory(string $language = 'English'): self - { - $className = __NAMESPACE__."\\StopWords\\{$language}"; - - if (!class_exists($className)) { - throw new InvalidArgumentException(sprintf('Can\'t find "%s" language for StopWords', $language)); - } - - return new $className(); - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/FeatureExtraction/StopWords/English.php b/lib/mlbackend/php/phpml/src/Phpml/FeatureExtraction/StopWords/English.php deleted file mode 100644 index fab079b1221..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/FeatureExtraction/StopWords/English.php +++ /dev/null @@ -1,33 +0,0 @@ -stopWords); - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/FeatureExtraction/StopWords/French.php b/lib/mlbackend/php/phpml/src/Phpml/FeatureExtraction/StopWords/French.php deleted file mode 100644 index 96cc11096a4..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/FeatureExtraction/StopWords/French.php +++ /dev/null @@ -1,29 +0,0 @@ -stopWords); - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/FeatureExtraction/StopWords/Polish.php b/lib/mlbackend/php/phpml/src/Phpml/FeatureExtraction/StopWords/Polish.php deleted file mode 100644 index e452ebf2551..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/FeatureExtraction/StopWords/Polish.php +++ /dev/null @@ -1,30 +0,0 @@ -stopWords); - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/FeatureExtraction/StopWords/Russian.php b/lib/mlbackend/php/phpml/src/Phpml/FeatureExtraction/StopWords/Russian.php deleted file mode 100644 index d26902da6bd..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/FeatureExtraction/StopWords/Russian.php +++ /dev/null @@ -1,30 +0,0 @@ -stopWords); - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/FeatureExtraction/TfIdfTransformer.php b/lib/mlbackend/php/phpml/src/Phpml/FeatureExtraction/TfIdfTransformer.php deleted file mode 100644 index 34f75330488..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/FeatureExtraction/TfIdfTransformer.php +++ /dev/null @@ -1,54 +0,0 @@ - 0) { - $this->fit($samples); - } - } - - public function fit(array $samples, ?array $targets = null): void - { - $this->countTokensFrequency($samples); - - $count = count($samples); - foreach ($this->idf as &$value) { - $value = log((float) ($count / $value), 10.0); - } - } - - public function transform(array &$samples, ?array &$targets = null): void - { - foreach ($samples as &$sample) { - foreach ($sample as $index => &$feature) { - $feature *= $this->idf[$index]; - } - } - } - - private function countTokensFrequency(array $samples): void - { - $this->idf = array_fill_keys(array_keys($samples[0]), 0); - - foreach ($samples as $sample) { - foreach ($sample as $index => $count) { - if ($count > 0) { - ++$this->idf[$index]; - } - } - } - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/FeatureExtraction/TokenCountVectorizer.php b/lib/mlbackend/php/phpml/src/Phpml/FeatureExtraction/TokenCountVectorizer.php deleted file mode 100644 index 5cc5e8d35ec..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/FeatureExtraction/TokenCountVectorizer.php +++ /dev/null @@ -1,166 +0,0 @@ -tokenizer = $tokenizer; - $this->stopWords = $stopWords; - $this->minDF = $minDF; - } - - public function fit(array $samples, ?array $targets = null): void - { - $this->buildVocabulary($samples); - } - - public function transform(array &$samples, ?array &$targets = null): void - { - array_walk($samples, function (string &$sample): void { - $this->transformSample($sample); - }); - - $this->checkDocumentFrequency($samples); - } - - public function getVocabulary(): array - { - return array_flip($this->vocabulary); - } - - private function buildVocabulary(array &$samples): void - { - foreach ($samples as $sample) { - $tokens = $this->tokenizer->tokenize($sample); - foreach ($tokens as $token) { - $this->addTokenToVocabulary($token); - } - } - } - - private function transformSample(string &$sample): void - { - $counts = []; - $tokens = $this->tokenizer->tokenize($sample); - - foreach ($tokens as $token) { - $index = $this->getTokenIndex($token); - if ($index !== false) { - $this->updateFrequency($token); - if (!isset($counts[$index])) { - $counts[$index] = 0; - } - - ++$counts[$index]; - } - } - - foreach ($this->vocabulary as $index) { - if (!isset($counts[$index])) { - $counts[$index] = 0; - } - } - - ksort($counts); - - $sample = $counts; - } - - /** - * @return int|bool - */ - private function getTokenIndex(string $token) - { - if ($this->isStopWord($token)) { - return false; - } - - return $this->vocabulary[$token] ?? false; - } - - private function addTokenToVocabulary(string $token): void - { - if ($this->isStopWord($token)) { - return; - } - - if (!isset($this->vocabulary[$token])) { - $this->vocabulary[$token] = count($this->vocabulary); - } - } - - private function isStopWord(string $token): bool - { - return $this->stopWords !== null && $this->stopWords->isStopWord($token); - } - - private function updateFrequency(string $token): void - { - if (!isset($this->frequencies[$token])) { - $this->frequencies[$token] = 0; - } - - ++$this->frequencies[$token]; - } - - private function checkDocumentFrequency(array &$samples): void - { - if ($this->minDF > 0) { - $beyondMinimum = $this->getBeyondMinimumIndexes(count($samples)); - foreach ($samples as &$sample) { - $this->resetBeyondMinimum($sample, $beyondMinimum); - } - } - } - - private function resetBeyondMinimum(array &$sample, array $beyondMinimum): void - { - foreach ($beyondMinimum as $index) { - $sample[$index] = 0; - } - } - - private function getBeyondMinimumIndexes(int $samplesCount): array - { - $indexes = []; - foreach ($this->frequencies as $token => $frequency) { - if (($frequency / $samplesCount) < $this->minDF) { - $indexes[] = $this->getTokenIndex((string) $token); - } - } - - return $indexes; - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/FeatureSelection/ScoringFunction.php b/lib/mlbackend/php/phpml/src/Phpml/FeatureSelection/ScoringFunction.php deleted file mode 100644 index 4c925f650a1..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/FeatureSelection/ScoringFunction.php +++ /dev/null @@ -1,10 +0,0 @@ - $sample) { - $grouped[$targets[$index]][] = $sample; - } - - return ANOVA::oneWayF(array_values($grouped)); - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/FeatureSelection/ScoringFunction/UnivariateLinearRegression.php b/lib/mlbackend/php/phpml/src/Phpml/FeatureSelection/ScoringFunction/UnivariateLinearRegression.php deleted file mode 100644 index 13bdc9913b3..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/FeatureSelection/ScoringFunction/UnivariateLinearRegression.php +++ /dev/null @@ -1,81 +0,0 @@ -center = $center; - } - - public function score(array $samples, array $targets): array - { - if ($this->center) { - $this->centerTargets($targets); - $this->centerSamples($samples); - } - - $correlations = []; - foreach (array_keys($samples[0]) as $index) { - $featureColumn = array_column($samples, $index); - $correlations[$index] = - Matrix::dot($targets, $featureColumn)[0] / (new Matrix($featureColumn, false))->transpose()->frobeniusNorm() - / (new Matrix($targets, false))->frobeniusNorm(); - } - - $degreesOfFreedom = count($targets) - ($this->center ? 2 : 1); - - return array_map(function (float $correlation) use ($degreesOfFreedom): float { - return $correlation ** 2 / (1 - $correlation ** 2) * $degreesOfFreedom; - }, $correlations); - } - - private function centerTargets(array &$targets): void - { - $mean = Mean::arithmetic($targets); - array_walk($targets, function (&$target) use ($mean): void { - $target -= $mean; - }); - } - - private function centerSamples(array &$samples): void - { - $means = []; - foreach ($samples[0] as $index => $feature) { - $means[$index] = Mean::arithmetic(array_column($samples, $index)); - } - - foreach ($samples as &$sample) { - foreach ($sample as $index => &$feature) { - $feature -= $means[$index]; - } - } - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/FeatureSelection/SelectKBest.php b/lib/mlbackend/php/phpml/src/Phpml/FeatureSelection/SelectKBest.php deleted file mode 100644 index 16e52782f05..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/FeatureSelection/SelectKBest.php +++ /dev/null @@ -1,78 +0,0 @@ -scoringFunction = $scoringFunction; - $this->k = $k; - } - - public function fit(array $samples, ?array $targets = null): void - { - if ($targets === null || count($targets) === 0) { - throw new InvalidArgumentException('The array has zero elements'); - } - - $this->scores = $sorted = $this->scoringFunction->score($samples, $targets); - if ($this->k >= count($sorted)) { - return; - } - - arsort($sorted); - $this->keepColumns = array_slice($sorted, 0, $this->k, true); - } - - public function transform(array &$samples, ?array &$targets = null): void - { - if ($this->keepColumns === null) { - return; - } - - foreach ($samples as &$sample) { - $sample = array_values(array_intersect_key($sample, $this->keepColumns)); - } - } - - public function scores(): array - { - if ($this->scores === null) { - throw new InvalidOperationException('SelectKBest require to fit first to get scores'); - } - - return $this->scores; - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/FeatureSelection/VarianceThreshold.php b/lib/mlbackend/php/phpml/src/Phpml/FeatureSelection/VarianceThreshold.php deleted file mode 100644 index 0c3154e0b1f..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/FeatureSelection/VarianceThreshold.php +++ /dev/null @@ -1,57 +0,0 @@ -threshold = $threshold; - } - - public function fit(array $samples, ?array $targets = null): void - { - $this->variances = array_map(static function (array $column): float { - return Variance::population($column); - }, Matrix::transposeArray($samples)); - - foreach ($this->variances as $column => $variance) { - if ($variance > $this->threshold) { - $this->keepColumns[$column] = true; - } - } - } - - public function transform(array &$samples, ?array &$targets = null): void - { - foreach ($samples as &$sample) { - $sample = array_values(array_intersect_key($sample, $this->keepColumns)); - } - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/FeatureUnion.php b/lib/mlbackend/php/phpml/src/Phpml/FeatureUnion.php deleted file mode 100644 index 3751e6fee02..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/FeatureUnion.php +++ /dev/null @@ -1,72 +0,0 @@ -pipelines = array_map(static function (Pipeline $pipeline): Pipeline { - return $pipeline; - }, $pipelines); - } - - public function fit(array $samples, ?array $targets = null): void - { - $originSamples = $samples; - foreach ($this->pipelines as $pipeline) { - foreach ($pipeline->getTransformers() as $transformer) { - $transformer->fit($samples, $targets); - $transformer->transform($samples, $targets); - } - $samples = $originSamples; - } - } - - public function transform(array &$samples, ?array &$targets = null): void - { - $this->transformSamples($samples, $targets); - } - - public function fitAndTransform(array &$samples, ?array &$targets = null): void - { - $this->transformSamples($samples, $targets, true); - } - - private function transformSamples(array &$samples, ?array &$targets = null, bool $fit = false): void - { - $union = []; - $originSamples = $samples; - foreach ($this->pipelines as $pipeline) { - foreach ($pipeline->getTransformers() as $transformer) { - if ($fit) { - $transformer->fit($samples, $targets); - } - $transformer->transform($samples, $targets); - } - - foreach ($samples as $index => $sample) { - $union[$index] = array_merge($union[$index] ?? [], is_array($sample) ? $sample : [$sample]); - } - $samples = $originSamples; - } - - $samples = $union; - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/Helper/OneVsRest.php b/lib/mlbackend/php/phpml/src/Phpml/Helper/OneVsRest.php deleted file mode 100644 index b17fc025e12..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/Helper/OneVsRest.php +++ /dev/null @@ -1,169 +0,0 @@ -reset(); - - $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. - $this->allLabels = count($allLabels) === 0 ? array_keys(array_count_values($targets)) : $allLabels; - sort($this->allLabels, SORT_STRING); - - // If there are only two targets, then there is no need to perform OvR - if (count($this->allLabels) === 2) { - // Init classifier if required. - if (count($this->classifiers) === 0) { - $this->classifiers[0] = $this->getClassifierCopy(); - } - - $this->classifiers[0]->trainBinary($samples, $targets, $this->allLabels); - } else { - // Train a separate classifier for each label and memorize them - - foreach ($this->allLabels as $label) { - // Init classifier if required. - if (!isset($this->classifiers[$label])) { - $this->classifiers[$label] = $this->getClassifierCopy(); - } - - [$binarizedTargets, $classifierLabels] = $this->binarizeTargets($targets, $label); - $this->classifiers[$label]->trainBinary($samples, $binarizedTargets, $classifierLabels); - } - } - - // If the underlying classifier is capable of giving the cost values - // during the training, then assign it to the relevant variable - // Adding just the first classifier cost values to avoid complex average calculations. - $classifierref = reset($this->classifiers); - if (method_exists($classifierref, 'getCostValues')) { - $this->costValues = $classifierref->getCostValues(); - } - } - - /** - * Returns an instance of the current class after cleaning up OneVsRest stuff. - */ - protected function getClassifierCopy(): Classifier - { - // Clone the current classifier, so that - // we don't mess up its variables while training - // multiple instances of this classifier - $classifier = clone $this; - $classifier->reset(); - - return $classifier; - } - - /** - * @return mixed - */ - protected function predictSample(array $sample) - { - if (count($this->allLabels) === 2) { - return $this->classifiers[0]->predictSampleBinary($sample); - } - - $probs = []; - - foreach ($this->classifiers as $label => $predictor) { - $probs[$label] = $predictor->predictProbability($sample, $label); - } - - arsort($probs, SORT_NUMERIC); - - return key($probs); - } - - /** - * Each classifier should implement this method instead of train(samples, targets) - */ - abstract protected function trainBinary(array $samples, array $targets, array $labels); - - /** - * To be overwritten by OneVsRest classifiers. - */ - abstract protected function resetBinary(): void; - - /** - * Each classifier that make use of OvR approach should be able to - * return a probability for a sample to belong to the given label. - * - * @return mixed - */ - abstract protected function predictProbability(array $sample, string $label); - - /** - * Each classifier should implement this method instead of predictSample() - * - * @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]; - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/Helper/Optimizer/ConjugateGradient.php b/lib/mlbackend/php/phpml/src/Phpml/Helper/Optimizer/ConjugateGradient.php deleted file mode 100644 index d7c064f1238..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/Helper/Optimizer/ConjugateGradient.php +++ /dev/null @@ -1,304 +0,0 @@ -samples = $samples; - $this->targets = $targets; - $this->gradientCb = $gradientCb; - $this->sampleCount = count($samples); - $this->costValues = []; - - $d = MP::muls($this->gradient($this->theta), -1); - - for ($i = 0; $i < $this->maxIterations; ++$i) { - // Obtain α that minimizes f(θ + α.d) - $alpha = $this->getAlpha($d); - - // θ(k+1) = θ(k) + α.d - $thetaNew = $this->getNewTheta($alpha, $d); - - // β = ||∇f(x(k+1))||² ∕ ||∇f(x(k))||² - $beta = $this->getBeta($thetaNew); - - // d(k+1) =–∇f(x(k+1)) + β(k).d(k) - $d = $this->getNewDirection($thetaNew, $beta, $d); - - // Save values for the next iteration - $oldTheta = $this->theta; - $this->costValues[] = $this->cost($thetaNew); - - $this->theta = $thetaNew; - if ($this->enableEarlyStop && $this->earlyStop($oldTheta)) { - break; - } - } - - $this->clear(); - - return $this->theta; - } - - /** - * Executes the callback function for the problem and returns - * sum of the gradient for all samples & targets. - */ - protected function gradient(array $theta): array - { - [, $updates, $penalty] = parent::gradient($theta); - - // Calculate gradient for each dimension - $gradient = []; - for ($i = 0; $i <= $this->dimensions; ++$i) { - if ($i === 0) { - $gradient[$i] = array_sum($updates); - } else { - $col = array_column($this->samples, $i - 1); - $error = 0; - foreach ($col as $index => $val) { - $error += $val * $updates[$index]; - } - - $gradient[$i] = $error + $penalty * $theta[$i]; - } - } - - return $gradient; - } - - /** - * Returns the value of f(x) for given solution - */ - protected function cost(array $theta): float - { - [$cost] = parent::gradient($theta); - - return array_sum($cost) / (int) $this->sampleCount; - } - - /** - * Calculates alpha that minimizes the function f(θ + α.d) - * by performing a line search that does not rely upon the derivation. - * - * There are several alternatives for this function. For now, we - * prefer a method inspired from the bisection method for its simplicity. - * This algorithm attempts to find an optimum alpha value between 0.0001 and 0.01 - * - * Algorithm as follows: - * a) Probe a small alpha (0.0001) and calculate cost function - * b) Probe a larger alpha (0.01) and calculate cost function - * b-1) If cost function decreases, continue enlarging alpha - * b-2) If cost function increases, take the midpoint and try again - */ - protected function getAlpha(array $d): float - { - $small = MP::muls($d, 0.0001); - $large = MP::muls($d, 0.01); - - // Obtain θ + α.d for two initial values, x0 and x1 - $x0 = MP::add($this->theta, $small); - $x1 = MP::add($this->theta, $large); - - $epsilon = 0.0001; - $iteration = 0; - do { - $fx1 = $this->cost($x1); - $fx0 = $this->cost($x0); - - // If the difference between two values is small enough - // then break the loop - if (abs($fx1 - $fx0) <= $epsilon) { - break; - } - - if ($fx1 < $fx0) { - $x0 = $x1; - $x1 = MP::adds($x1, 0.01); // Enlarge second - } else { - $x1 = MP::divs(MP::add($x1, $x0), 2.0); - } // Get to the midpoint - - $error = $fx1 / $this->dimensions; - } while ($error <= $epsilon || $iteration++ < 10); - - // Return α = θ / d - // For accuracy, choose a dimension which maximize |d[i]| - $imax = 0; - for ($i = 1; $i <= $this->dimensions; ++$i) { - if (abs($d[$i]) > abs($d[$imax])) { - $imax = $i; - } - } - - if ($d[$imax] == 0) { - return $x1[$imax] - $this->theta[$imax]; - } - - return ($x1[$imax] - $this->theta[$imax]) / $d[$imax]; - } - - /** - * Calculates new set of solutions with given alpha (for each θ(k)) and - * gradient direction. - * - * θ(k+1) = θ(k) + α.d - */ - protected function getNewTheta(float $alpha, array $d): array - { - return MP::add($this->theta, MP::muls($d, $alpha)); - } - - /** - * Calculates new beta (β) for given set of solutions by using - * Fletcher–Reeves method. - * - * β = ||f(x(k+1))||² ∕ ||f(x(k))||² - * - * See: - * R. Fletcher and C. M. Reeves, "Function minimization by conjugate gradients", Comput. J. 7 (1964), 149–154. - */ - protected function getBeta(array $newTheta): float - { - $gNew = $this->gradient($newTheta); - $gOld = $this->gradient($this->theta); - $dNew = 0; - $dOld = 1e-100; - for ($i = 0; $i <= $this->dimensions; ++$i) { - $dNew += $gNew[$i] ** 2; - $dOld += $gOld[$i] ** 2; - } - - return $dNew / $dOld; - } - - /** - * Calculates the new conjugate direction - * - * d(k+1) =–∇f(x(k+1)) + β(k).d(k) - */ - protected function getNewDirection(array $theta, float $beta, array $d): array - { - $grad = $this->gradient($theta); - - return MP::add(MP::muls($grad, -1), MP::muls($d, $beta)); - } -} - -/** - * Handles element-wise vector operations between vector-vector - * and vector-scalar variables - */ -class MP -{ - /** - * Element-wise multiplication of two vectors of the same size - */ - public static function mul(array $m1, array $m2): array - { - $res = []; - foreach ($m1 as $i => $val) { - $res[] = $val * $m2[$i]; - } - - return $res; - } - - /** - * Element-wise division of two vectors of the same size - */ - public static function div(array $m1, array $m2): array - { - $res = []; - foreach ($m1 as $i => $val) { - $res[] = $val / $m2[$i]; - } - - return $res; - } - - /** - * Element-wise addition of two vectors of the same size - */ - public static function add(array $m1, array $m2, int $mag = 1): array - { - $res = []; - foreach ($m1 as $i => $val) { - $res[] = $val + $mag * $m2[$i]; - } - - return $res; - } - - /** - * Element-wise subtraction of two vectors of the same size - */ - public static function sub(array $m1, array $m2): array - { - return self::add($m1, $m2, -1); - } - - /** - * Element-wise multiplication of a vector with a scalar - */ - public static function muls(array $m1, float $m2): array - { - $res = []; - foreach ($m1 as $val) { - $res[] = $val * $m2; - } - - return $res; - } - - /** - * Element-wise division of a vector with a scalar - */ - public static function divs(array $m1, float $m2): array - { - $res = []; - foreach ($m1 as $val) { - $res[] = $val / ($m2 + 1e-32); - } - - return $res; - } - - /** - * Element-wise addition of a vector with a scalar - */ - public static function adds(array $m1, float $m2, int $mag = 1): array - { - $res = []; - foreach ($m1 as $val) { - $res[] = $val + $mag * $m2; - } - - return $res; - } - - /** - * Element-wise subtraction of a vector with a scalar - */ - public static function subs(array $m1, float $m2): array - { - return self::adds($m1, $m2, -1); - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/Helper/Optimizer/GD.php b/lib/mlbackend/php/phpml/src/Phpml/Helper/Optimizer/GD.php deleted file mode 100644 index 40c65c703cd..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/Helper/Optimizer/GD.php +++ /dev/null @@ -1,111 +0,0 @@ -samples = $samples; - $this->targets = $targets; - $this->gradientCb = $gradientCb; - $this->sampleCount = count($this->samples); - - // Batch learning is executed: - $currIter = 0; - $this->costValues = []; - while ($this->maxIterations > $currIter++) { - $theta = $this->theta; - - // Calculate update terms for each sample - [$errors, $updates, $totalPenalty] = $this->gradient($theta); - - $this->updateWeightsWithUpdates($updates, $totalPenalty); - - $this->costValues[] = array_sum($errors) / (int) $this->sampleCount; - - if ($this->earlyStop($theta)) { - break; - } - } - - $this->clear(); - - return $this->theta; - } - - /** - * Calculates gradient, cost function and penalty term for each sample - * then returns them as an array of values - */ - protected function gradient(array $theta): array - { - $costs = []; - $gradient = []; - $totalPenalty = 0; - - if ($this->gradientCb === null) { - throw new InvalidOperationException('Gradient callback is not defined'); - } - - foreach ($this->samples as $index => $sample) { - $target = $this->targets[$index]; - - $result = ($this->gradientCb)($theta, $sample, $target); - [$cost, $grad, $penalty] = array_pad($result, 3, 0); - - $costs[] = $cost; - $gradient[] = $grad; - $totalPenalty += $penalty; - } - - $totalPenalty /= $this->sampleCount; - - return [$costs, $gradient, $totalPenalty]; - } - - protected function updateWeightsWithUpdates(array $updates, float $penalty): void - { - // Updates all weights at once - for ($i = 0; $i <= $this->dimensions; ++$i) { - if ($i === 0) { - $this->theta[0] -= $this->learningRate * array_sum($updates); - } else { - $col = array_column($this->samples, $i - 1); - - $error = 0; - foreach ($col as $index => $val) { - $error += $val * $updates[$index]; - } - - $this->theta[$i] -= $this->learningRate * - ($error + $penalty * $this->theta[$i]); - } - } - } - - /** - * Clears the optimizer internal vars after the optimization process. - */ - protected function clear(): void - { - $this->sampleCount = null; - parent::clear(); - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/Helper/Optimizer/Optimizer.php b/lib/mlbackend/php/phpml/src/Phpml/Helper/Optimizer/Optimizer.php deleted file mode 100644 index 54331e9e469..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/Helper/Optimizer/Optimizer.php +++ /dev/null @@ -1,61 +0,0 @@ -dimensions = $dimensions; - - // Inits the weights randomly - $this->theta = []; - for ($i = 0; $i < $this->dimensions; ++$i) { - $this->theta[] = (random_int(0, PHP_INT_MAX) / PHP_INT_MAX) + 0.1; - } - } - - public function setTheta(array $theta): self - { - if (count($theta) !== $this->dimensions) { - throw new InvalidArgumentException(sprintf('Number of values in the weights array should be %s', $this->dimensions)); - } - - $this->theta = $theta; - - return $this; - } - - public function theta(): array - { - return $this->theta; - } - - /** - * Executes the optimization with the given samples & targets - * and returns the weights - */ - abstract public function runOptimization(array $samples, array $targets, Closure $gradientCb): array; -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/Helper/Optimizer/StochasticGD.php b/lib/mlbackend/php/phpml/src/Phpml/Helper/Optimizer/StochasticGD.php deleted file mode 100644 index 9927c3f29b6..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/Helper/Optimizer/StochasticGD.php +++ /dev/null @@ -1,278 +0,0 @@ - - * - * Larger values of lr may overshoot the optimum or even cause divergence - * while small values slows down the convergence and increases the time - * required for the training - * - * @var float - */ - protected $learningRate = 0.001; - - /** - * Minimum amount of change in the weights and error values - * between iterations that needs to be obtained to continue the training - * - * @var float - */ - protected $threshold = 1e-4; - - /** - * Enable/Disable early stopping by checking the weight & cost values - * to see whether they changed large enough to continue the optimization - * - * @var bool - */ - protected $enableEarlyStop = true; - - /** - * List of values obtained by evaluating the cost function at each iteration - * of the algorithm - * - * @var array - */ - protected $costValues = []; - - /** - * Initializes the SGD optimizer for the given number of dimensions - */ - public function __construct(int $dimensions) - { - // Add one more dimension for the bias - parent::__construct($dimensions + 1); - - $this->dimensions = $dimensions; - } - - public function setTheta(array $theta): Optimizer - { - if (count($theta) !== $this->dimensions + 1) { - throw new InvalidArgumentException(sprintf('Number of values in the weights array should be %s', $this->dimensions + 1)); - } - - $this->theta = $theta; - - return $this; - } - - /** - * Sets minimum value for the change in the theta values - * between iterations to continue the iterations.
- * - * If change in the theta is less than given value then the - * algorithm will stop training - * - * @return $this - */ - public function setChangeThreshold(float $threshold = 1e-5) - { - $this->threshold = $threshold; - - return $this; - } - - /** - * Enable/Disable early stopping by checking at each iteration - * whether changes in theta or cost value are not large enough - * - * @return $this - */ - public function setEarlyStop(bool $enable = true) - { - $this->enableEarlyStop = $enable; - - return $this; - } - - /** - * @return $this - */ - public function setLearningRate(float $learningRate) - { - $this->learningRate = $learningRate; - - return $this; - } - - /** - * @return $this - */ - public function setMaxIterations(int $maxIterations) - { - $this->maxIterations = $maxIterations; - - return $this; - } - - /** - * Optimization procedure finds the unknow variables for the equation A.ϴ = y - * for the given samples (A) and targets (y).
- * - * 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 - { - $this->samples = $samples; - $this->targets = $targets; - $this->gradientCb = $gradientCb; - - $currIter = 0; - $bestTheta = null; - $bestScore = 0.0; - $this->costValues = []; - - while ($this->maxIterations > $currIter++) { - $theta = $this->theta; - - // Update the guess - $cost = $this->updateTheta(); - - // Save the best theta in the "pocket" so that - // any future set of theta worse than this will be disregarded - if ($bestTheta === null || $cost <= $bestScore) { - $bestTheta = $theta; - $bestScore = $cost; - } - - // Add the cost value for this iteration to the list - $this->costValues[] = $cost; - - // Check for early stop - if ($this->enableEarlyStop && $this->earlyStop($theta)) { - break; - } - } - - $this->clear(); - - // Solution in the pocket is better than or equal to the last state - // so, we use this solution - return $this->theta = (array) $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; - $theta = $this->theta; - - if ($this->gradientCb === null) { - throw new InvalidOperationException('Gradient callback is not defined'); - } - - foreach ($this->samples as $index => $sample) { - $target = $this->targets[$index]; - - $result = ($this->gradientCb)($theta, $sample, $target); - - [$error, $gradient, $penalty] = array_pad($result, 3, 0); - - // Update bias - $this->theta[0] -= $this->learningRate * $gradient; - - // Update other values - for ($i = 1; $i <= $this->dimensions; ++$i) { - $this->theta[$i] -= $this->learningRate * - ($gradient * $sample[$i - 1] + $penalty * $this->theta[$i]); - } - - // Sum error rate - $jValue += $error; - } - - return $jValue / count($this->samples); - } - - /** - * Checks if the optimization is not effective enough and can be stopped - * in case large enough changes in the solution do not happen - */ - protected function earlyStop(array $oldTheta): bool - { - // Check for early stop: No change larger than threshold (default 1e-5) - $diff = array_map( - function ($w1, $w2) { - return abs($w1 - $w2) > $this->threshold ? 1 : 0; - }, - $oldTheta, - $this->theta - ); - - if (array_sum($diff) == 0) { - return true; - } - - // Check if the last two cost values are almost the same - $costs = array_slice($this->costValues, -2); - if (count($costs) === 2 && abs($costs[1] - $costs[0]) < $this->threshold) { - return true; - } - - return false; - } - - /** - * Clears the optimizer internal vars after the optimization process. - */ - protected function clear(): void - { - $this->samples = []; - $this->targets = []; - $this->gradientCb = null; - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/Helper/Predictable.php b/lib/mlbackend/php/phpml/src/Phpml/Helper/Predictable.php deleted file mode 100644 index 74d1cc07634..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/Helper/Predictable.php +++ /dev/null @@ -1,30 +0,0 @@ -predictSample($samples); - } - - $predicted = []; - foreach ($samples as $index => $sample) { - $predicted[$index] = $this->predictSample($sample); - } - - return $predicted; - } - - /** - * @return mixed - */ - abstract protected function predictSample(array $sample); -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/Helper/Trainable.php b/lib/mlbackend/php/phpml/src/Phpml/Helper/Trainable.php deleted file mode 100644 index 1388760748e..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/Helper/Trainable.php +++ /dev/null @@ -1,24 +0,0 @@ -samples = array_merge($this->samples, $samples); - $this->targets = array_merge($this->targets, $targets); - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/IncrementalEstimator.php b/lib/mlbackend/php/phpml/src/Phpml/IncrementalEstimator.php deleted file mode 100644 index 600bfbb2d6e..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/IncrementalEstimator.php +++ /dev/null @@ -1,10 +0,0 @@ -': - return $a > $b; - case '>=': - return $a >= $b; - case '=': - case '==': - return $a == $b; - case '===': - return $a === $b; - case '<=': - return $a <= $b; - case '<': - return $a < $b; - case '!=': - case '<>': - return $a != $b; - case '!==': - return $a !== $b; - default: - throw new InvalidArgumentException(sprintf('Invalid operator "%s" provided', $operator)); - } - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/Math/Distance.php b/lib/mlbackend/php/phpml/src/Phpml/Math/Distance.php deleted file mode 100644 index f49bd331df2..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/Math/Distance.php +++ /dev/null @@ -1,10 +0,0 @@ -deltas($a, $b)); - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/Math/Distance/Distance.php b/lib/mlbackend/php/phpml/src/Phpml/Math/Distance/Distance.php deleted file mode 100644 index ad9cdb991f2..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/Math/Distance/Distance.php +++ /dev/null @@ -1,61 +0,0 @@ -norm = $norm; - } - - /** - * @throws InvalidArgumentException - */ - public function distance(array $a, array $b): float - { - $distance = 0; - - foreach ($this->deltas($a, $b) as $delta) { - $distance += $delta ** $this->norm; - } - - return $distance ** (1 / $this->norm); - } - - /** - * @throws InvalidArgumentException - */ - protected function deltas(array $a, array $b): array - { - $count = count($a); - - if ($count !== count($b)) { - throw new InvalidArgumentException('Size of given arrays does not match'); - } - - $deltas = []; - - for ($i = 0; $i < $count; $i++) { - $deltas[] = abs($a[$i] - $b[$i]); - } - - return $deltas; - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/Math/Distance/Euclidean.php b/lib/mlbackend/php/phpml/src/Phpml/Math/Distance/Euclidean.php deleted file mode 100644 index 4b7abc40a11..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/Math/Distance/Euclidean.php +++ /dev/null @@ -1,31 +0,0 @@ -distance($a, $b) ** 2; - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/Math/Distance/Manhattan.php b/lib/mlbackend/php/phpml/src/Phpml/Math/Distance/Manhattan.php deleted file mode 100644 index 21ddee2ed60..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/Math/Distance/Manhattan.php +++ /dev/null @@ -1,21 +0,0 @@ -gamma = $gamma; - } - - public function compute($a, $b): float - { - if (!is_array($a) || !is_array($b)) { - throw new InvalidArgumentException(sprintf('Arguments of %s must be arrays', __METHOD__)); - } - - $score = 2 * Product::scalar($a, $b); - $squares = Product::scalar($a, $a) + Product::scalar($b, $b); - - return exp(-$this->gamma * ($squares - $score)); - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/Math/LinearAlgebra/EigenvalueDecomposition.php b/lib/mlbackend/php/phpml/src/Phpml/Math/LinearAlgebra/EigenvalueDecomposition.php deleted file mode 100644 index 94f0a9e823f..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/Math/LinearAlgebra/EigenvalueDecomposition.php +++ /dev/null @@ -1,960 +0,0 @@ -n = count($arg[0]); - $symmetric = true; - - for ($j = 0; ($j < $this->n) & $symmetric; ++$j) { - for ($i = 0; ($i < $this->n) & $symmetric; ++$i) { - $symmetric = $arg[$i][$j] == $arg[$j][$i]; - } - } - - if ($symmetric) { - $this->V = $arg; - // Tridiagonalize. - $this->tred2(); - // Diagonalize. - $this->tql2(); - } else { - $this->H = $arg; - $this->ort = []; - // Reduce to Hessenberg form. - $this->orthes(); - // Reduce Hessenberg to real Schur form. - $this->hqr2(); - } - } - - /** - * 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; - $count = count($vect); - for ($i = 0; $i < $count; ++$i) { - $sum += $vect[$i] ** 2; - } - - $sum **= .5; - for ($i = 0; $i < $count; ++$i) { - $vect[$i] /= $sum; - } - - return $vect; - }, $vectors->transpose()->toArray()); - - return $vectors; - } - - /** - * Return the real parts of the eigenvalues
- * d = real(diag(D)); - */ - public function getRealEigenvalues(): array - { - return $this->d; - } - - /** - * Return the imaginary parts of the eigenvalues
- * 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. - */ - private function tred2(): void - { - // This is derived from the Algol procedures tred2 by - // Bowdler, Martin, Reinsch, and Wilkinson, Handbook for - // Auto. Comp., Vol.ii-Linear Algebra, and the corresponding - // Fortran subroutine in EISPACK. - $this->d = $this->V[$this->n - 1]; - // Householder reduction to tridiagonal form. - for ($i = $this->n - 1; $i > 0; --$i) { - $i_ = $i - 1; - // Scale to avoid under/overflow. - $h = $scale = 0.0; - $scale += array_sum(array_map('abs', $this->d)); - if ($scale == 0.0) { - $this->e[$i] = $this->d[$i_]; - $this->d = array_slice($this->V[$i_], 0, $this->n - 1); - for ($j = 0; $j < $i; ++$j) { - $this->V[$j][$i] = $this->V[$i][$j] = 0.0; - } - } else { - // Generate Householder vector. - for ($k = 0; $k < $i; ++$k) { - $this->d[$k] /= $scale; - $h += $this->d[$k] ** 2; - } - - $f = $this->d[$i_]; - $g = $h ** .5; - if ($f > 0) { - $g = -$g; - } - - $this->e[$i] = $scale * $g; - $h -= $f * $g; - $this->d[$i_] = $f - $g; - - 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]; - $this->V[$j][$i] = $f; - $g = $this->e[$j] + $this->V[$j][$j] * $f; - - for ($k = $j + 1; $k <= $i_; ++$k) { - $g += $this->V[$k][$j] * $this->d[$k]; - $this->e[$k] += $this->V[$k][$j] * $f; - } - - $this->e[$j] = $g; - } - - $f = 0.0; - - if ($h == 0.0) { - $h = 1e-32; - } - - for ($j = 0; $j < $i; ++$j) { - $this->e[$j] /= $h; - $f += $this->e[$j] * $this->d[$j]; - } - - $hh = $f / (2 * $h); - 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; - } - - // Accumulate transformations. - for ($i = 0; $i < $this->n - 1; ++$i) { - $this->V[$this->n - 1][$i] = $this->V[$i][$i]; - $this->V[$i][$i] = 1.0; - $h = $this->d[$i + 1]; - if ($h != 0.0) { - 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; - } - } - - $this->d = $this->V[$this->n - 1]; - $this->V[$this->n - 1] = array_fill(0, $this->n, 0.0); - $this->V[$this->n - 1][$this->n - 1] = 1.0; - $this->e[0] = 0.0; - } - - /** - * Symmetric tridiagonal QL algorithm. - * - * This is derived from the Algol procedures tql2, by - * Bowdler, Martin, Reinsch, and Wilkinson, Handbook for - * Auto. Comp., Vol.ii-Linear Algebra, and the corresponding - * Fortran subroutine in EISPACK. - */ - private function tql2(): void - { - 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; - $eps = 2.0 ** -52.0; - - for ($l = 0; $l < $this->n; ++$l) { - // Find small subdiagonal element - $tst1 = max($tst1, abs($this->d[$l]) + abs($this->e[$l])); - $m = $l; - while ($m < $this->n) { - if (abs($this->e[$m]) <= $eps * $tst1) { - break; - } - - ++$m; - } - - // If m == l, $this->d[l] is an eigenvalue, - // otherwise, iterate. - if ($m > $l) { - do { - // Compute implicit shift - $g = $this->d[$l]; - $p = ($this->d[$l + 1] - $g) / (2.0 * $this->e[$l]); - $r = hypot($p, 1.0); - 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]; - $h = $g - $this->d[$l]; - for ($i = $l + 2; $i < $this->n; ++$i) { - $this->d[$i] -= $h; - } - - $f += $h; - // Implicit QL transformation. - $p = $this->d[$m]; - $c = 1.0; - $c2 = $c3 = $c; - $el1 = $this->e[$l + 1]; - $s = $s2 = 0.0; - for ($i = $m - 1; $i >= $l; --$i) { - $c3 = $c2; - $c2 = $c; - $s2 = $s; - $g = $c * $this->e[$i]; - $h = $c * $p; - $r = hypot($p, $this->e[$i]); - $this->e[$i + 1] = $s * $r; - $s = $this->e[$i] / $r; - $c = $p / $r; - $p = $c * $this->d[$i] - $s * $g; - $this->d[$i + 1] = $h + $s * ($c * $g + $s * $this->d[$i]); - // Accumulate transformation. - for ($k = 0; $k < $this->n; ++$k) { - $h = $this->V[$k][$i + 1]; - $this->V[$k][$i + 1] = $s * $this->V[$k][$i] + $c * $h; - $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] += $f; - $this->e[$l] = 0.0; - } - - // Sort eigenvalues and corresponding vectors. - for ($i = 0; $i < $this->n - 1; ++$i) { - $k = $i; - $p = $this->d[$i]; - for ($j = $i + 1; $j < $this->n; ++$j) { - if ($this->d[$j] < $p) { - $k = $j; - $p = $this->d[$j]; - } - } - - if ($k != $i) { - $this->d[$k] = $this->d[$i]; - $this->d[$i] = $p; - for ($j = 0; $j < $this->n; ++$j) { - $p = $this->V[$j][$i]; - $this->V[$j][$i] = $this->V[$j][$k]; - $this->V[$j][$k] = $p; - } - } - } - } - - /** - * Nonsymmetric reduction to Hessenberg form. - * - * This is derived from the Algol procedures orthes and ortran, - * by Martin and Wilkinson, Handbook for Auto. Comp., - * Vol.ii-Linear Algebra, and the corresponding - * Fortran subroutines in EISPACK. - */ - private function orthes(): void - { - $low = 0; - $high = $this->n - 1; - - for ($m = $low + 1; $m <= $high - 1; ++$m) { - // Scale column. - $scale = 0.0; - for ($i = $m; $i <= $high; ++$i) { - $scale += abs($this->H[$i][$m - 1]); - } - - if ($scale != 0.0) { - // Compute Householder transformation. - $h = 0.0; - for ($i = $high; $i >= $m; --$i) { - $this->ort[$i] = $this->H[$i][$m - 1] / $scale; - $h += $this->ort[$i] * $this->ort[$i]; - } - - $g = $h ** .5; - if ($this->ort[$m] > 0) { - $g *= -1; - } - - $h -= $this->ort[$m] * $g; - $this->ort[$m] -= $g; - // Apply Householder similarity transformation - // H = (I -u * u' / h) * H * (I -u * u') / h) - for ($j = $m; $j < $this->n; ++$j) { - $f = 0.0; - 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 /= $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; - } - } - - // Accumulate transformations (Algol's ortran). - for ($i = 0; $i < $this->n; ++$i) { - for ($j = 0; $j < $this->n; ++$j) { - $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 /= $this->ort[$m]; - $g /= $this->H[$m][$m - 1]; - for ($i = $m; $i <= $high; ++$i) { - $this->V[$i][$j] += $g * $this->ort[$i]; - } - } - } - } - } - - /** - * Performs complex division. - * - * @param int|float $xr - * @param int|float $xi - * @param int|float $yr - * @param int|float $yi - */ - private function cdiv($xr, $xi, $yr, $yi): void - { - if (abs($yr) > abs($yi)) { - $r = $yi / $yr; - $d = $yr + $r * $yi; - $this->cdivr = ($xr + $r * $xi) / $d; - $this->cdivi = ($xi - $r * $xr) / $d; - } else { - $r = $yr / $yi; - $d = $yi + $r * $yr; - $this->cdivr = ($r * $xr + $xi) / $d; - $this->cdivi = ($r * $xi - $xr) / $d; - } - } - - /** - * Nonsymmetric reduction from Hessenberg to real Schur form. - * - * Code is derived from the Algol procedure hqr2, - * by Martin and Wilkinson, Handbook for Auto. Comp., - * Vol.ii-Linear Algebra, and the corresponding - * Fortran subroutine in EISPACK. - */ - private function hqr2(): void - { - // Initialize - $nn = $this->n; - $n = $nn - 1; - $low = 0; - $high = $nn - 1; - $eps = 2.0 ** -52.0; - $exshift = 0.0; - $p = $q = $r = $s = $z = 0; - // Store roots isolated by balanc and compute matrix norm - $norm = 0.0; - - for ($i = 0; $i < $nn; ++$i) { - if (($i < $low) or ($i > $high)) { - $this->d[$i] = $this->H[$i][$i]; - $this->e[$i] = 0.0; - } - - for ($j = max($i - 1, 0); $j < $nn; ++$j) { - $norm += abs($this->H[$i][$j]); - } - } - - // Outer loop over eigenvalue index - $iter = 0; - while ($n >= $low) { - // Look for single small sub-diagonal element - $l = $n; - while ($l > $low) { - $s = abs($this->H[$l - 1][$l - 1]) + abs($this->H[$l][$l]); - 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) { - $this->H[$n][$n] += $exshift; - $this->d[$n] = $this->H[$n][$n]; - $this->e[$n] = 0.0; - --$n; - $iter = 0; - // Two roots found - } elseif ($l == $n - 1) { - $w = $this->H[$n][$n - 1] * $this->H[$n - 1][$n]; - $p = ($this->H[$n - 1][$n - 1] - $this->H[$n][$n]) / 2.0; - $q = $p * $p + $w; - $z = abs($q) ** .5; - $this->H[$n][$n] += $exshift; - $this->H[$n - 1][$n - 1] += $exshift; - $x = $this->H[$n][$n]; - // Real pair - if ($q >= 0) { - if ($p >= 0) { - $z = $p + $z; - } 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]; - $s = abs($x) + abs($z); - $p = $x / $s; - $q = $z / $s; - $r = ($p * $p + $q * $q) ** .5; - $p /= $r; - $q /= $r; - // Row modification - for ($j = $n - 1; $j < $nn; ++$j) { - $z = $this->H[$n - 1][$j]; - $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; - $this->d[$n] = $x + $p; - $this->e[$n - 1] = $z; - $this->e[$n] = -$z; - } - - $n -= 2; - $iter = 0; - // No convergence yet - } else { - // Form shift - $x = $this->H[$n][$n]; - $y = 0.0; - $w = 0.0; - if ($l < $n) { - $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; - $s *= $s + $w; - if ($s > 0) { - $s **= .5; - 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; - // Look for two consecutive small sub-diagonal elements - $m = $n - 2; - while ($m >= $l) { - $z = $this->H[$m][$m]; - $r = $x - $z; - $s = $y - $z; - $p = ($r * $s - $w) / $this->H[$m + 1][$m] + $this->H[$m][$m + 1]; - $q = $this->H[$m + 1][$m + 1] - $z - $r - $s; - $r = $this->H[$m + 2][$m + 1]; - $s = abs($p) + abs($q) + abs($r); - $p /= $s; - $q /= $s; - $r /= $s; - 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; - if ($k != $m) { - $p = $this->H[$k][$k - 1]; - $q = $this->H[$k + 1][$k - 1]; - $r = ($notlast ? $this->H[$k + 2][$k - 1] : 0.0); - $x = abs($p) + abs($q) + abs($r); - if ($x != 0.0) { - $p /= $x; - $q /= $x; - $r /= $x; - } - } - - if ($x == 0.0) { - break; - } - - $s = ($p * $p + $q * $q + $r * $r) ** .5; - 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 += $s; - $x = $p / $s; - $y = $q / $s; - $z = $r / $s; - $q /= $p; - $r /= $p; - // Row modification - for ($j = $k; $j < $nn; ++$j) { - $p = $this->H[$k][$j] + $q * $this->H[$k + 1][$j]; - if ($notlast) { - $p += $r * $this->H[$k + 2][$j]; - $this->H[$k + 2][$j] -= $p * $z; - } - - $this->H[$k][$j] -= $p * $x; - $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]; - if ($notlast) { - $p += $z * $this->H[$i][$k + 2]; - $this->H[$i][$k + 2] -= $p * $r; - } - - $this->H[$i][$k] -= $p; - $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]; - if ($notlast) { - $p += $z * $this->V[$i][$k + 2]; - $this->V[$i][$k + 2] -= $p * $r; - } - - $this->V[$i][$k] -= $p; - $this->V[$i][$k + 1] -= $p * $q; - } - } // ($s != 0) - } // k loop - } // check convergence - } // while ($n >= $low) - - // Backsubstitute to find vectors of upper triangular form - if ($norm == 0.0) { - return; - } - - for ($n = $nn - 1; $n >= 0; --$n) { - $p = $this->d[$n]; - $q = $this->e[$n]; - // Real vector - if ($q == 0) { - $l = $n; - $this->H[$n][$n] = 1.0; - for ($i = $n - 1; $i >= 0; --$i) { - $w = $this->H[$i][$i] - $p; - $r = 0.0; - for ($j = $l; $j <= $n; ++$j) { - $r += $this->H[$i][$j] * $this->H[$j][$n]; - } - - if ($this->e[$i] < 0.0) { - $z = $w; - $s = $r; - } else { - $l = $i; - if ($this->e[$i] == 0.0) { - if ($w != 0.0) { - $this->H[$i][$n] = -$r / $w; - } else { - $this->H[$i][$n] = -$r / ($eps * $norm); - } - - // Solve real equations - } else { - $x = $this->H[$i][$i + 1]; - $y = $this->H[$i + 1][$i]; - $q = ($this->d[$i] - $p) * ($this->d[$i] - $p) + $this->e[$i] * $this->e[$i]; - $t = ($x * $s - $z * $r) / $q; - $this->H[$i][$n] = $t; - if (abs($x) > abs($z)) { - $this->H[$i + 1][$n] = (-$r - $w * $t) / $x; - } else { - $this->H[$i + 1][$n] = (-$s - $y * $t) / $z; - } - } - - // Overflow control - $t = abs($this->H[$i][$n]); - if (($eps * $t) * $t > 1) { - for ($j = $i; $j <= $n; ++$j) { - $this->H[$j][$n] /= $t; - } - } - } - } - - // Complex vector - } elseif ($q < 0) { - $l = $n - 1; - // Last vector component imaginary so matrix is triangular - if (abs($this->H[$n][$n - 1]) > abs($this->H[$n - 1][$n])) { - $this->H[$n - 1][$n - 1] = $q / $this->H[$n][$n - 1]; - $this->H[$n - 1][$n] = -($this->H[$n][$n] - $p) / $this->H[$n][$n - 1]; - } else { - $this->cdiv(0.0, -$this->H[$n - 1][$n], $this->H[$n - 1][$n - 1] - $p, $q); - $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) { - // double ra,sa,vr,vi; - $ra = 0.0; - $sa = 0.0; - for ($j = $l; $j <= $n; ++$j) { - $ra += $this->H[$i][$j] * $this->H[$j][$n - 1]; - $sa += $this->H[$i][$j] * $this->H[$j][$n]; - } - - $w = $this->H[$i][$i] - $p; - if ($this->e[$i] < 0.0) { - $z = $w; - $r = $ra; - $s = $sa; - } else { - $l = $i; - if ($this->e[$i] == 0) { - $this->cdiv(-$ra, -$sa, $w, $q); - $this->H[$i][$n - 1] = $this->cdivr; - $this->H[$i][$n] = $this->cdivi; - } else { - // Solve complex equations - $x = $this->H[$i][$i + 1]; - $y = $this->H[$i + 1][$i]; - $vr = ($this->d[$i] - $p) * ($this->d[$i] - $p) + $this->e[$i] * $this->e[$i] - $q * $q; - $vi = ($this->d[$i] - $p) * 2.0 * $q; - 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; - if (abs($x) > (abs($z) + abs($q))) { - $this->H[$i + 1][$n - 1] = (-$ra - $w * $this->H[$i][$n - 1] + $q * $this->H[$i][$n]) / $x; - $this->H[$i + 1][$n] = (-$sa - $w * $this->H[$i][$n] - $q * $this->H[$i][$n - 1]) / $x; - } else { - $this->cdiv(-$r - $y * $this->H[$i][$n - 1], -$s - $y * $this->H[$i][$n], $z, $q); - $this->H[$i + 1][$n - 1] = $this->cdivr; - $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) { - for ($j = $i; $j <= $n; ++$j) { - $this->H[$j][$n - 1] /= $t; - $this->H[$j][$n] /= $t; - } - } - } // end else - } // end for - } // end else for complex case - } // end for - - // Vectors of isolated roots - for ($i = 0; $i < $nn; ++$i) { - if ($i < $low || $i > $high) { - for ($j = $i; $j < $nn; ++$j) { - $this->V[$i][$j] = $this->H[$i][$j]; - } - } - } - - // Back transformation to get eigenvectors of original matrix - for ($j = $nn - 1; $j >= $low; --$j) { - for ($i = $low; $i <= $high; ++$i) { - $z = 0.0; - for ($k = $low; $k <= min($j, $high); ++$k) { - $z += $this->V[$i][$k] * $this->H[$k][$j]; - } - - $this->V[$i][$j] = $z; - } - } - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/Math/LinearAlgebra/LUDecomposition.php b/lib/mlbackend/php/phpml/src/Phpml/Math/LinearAlgebra/LUDecomposition.php deleted file mode 100644 index 61f7c3f27c5..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/Math/LinearAlgebra/LUDecomposition.php +++ /dev/null @@ -1,299 +0,0 @@ -= n, the LU decomposition is an m-by-n - * unit lower triangular matrix L, an n-by-n upper triangular matrix U, - * and a permutation vector piv of length m so that A(piv,:) = L*U. - * If m < n, then L is m-by-m and U is m-by-n. - * - * The LU decompostion with pivoting always exists, even if the matrix is - * singular, so the constructor will never fail. The primary use of the - * LU decomposition is in the solution of square systems of simultaneous - * linear equations. This will fail if isNonsingular() returns false. - * - * @author Paul Meagher - * @author Bartosz Matosiuk - * @author Michael Bommarito - * - * @version 1.1 - * - * @license PHP v3.0 - * - * Slightly changed to adapt the original code to PHP-ML library - * @date 2017/04/24 - * - * @author Mustafa Karabulut - */ - -namespace Phpml\Math\LinearAlgebra; - -use Phpml\Exception\MatrixException; -use Phpml\Math\Matrix; - -class LUDecomposition -{ - /** - * Decomposition storage - * - * @var array - */ - private $LU = []; - - /** - * Row dimension. - * - * @var int - */ - private $m; - - /** - * Column dimension. - * - * @var int - */ - private $n; - - /** - * Pivot sign. - * - * @var int - */ - private $pivsign; - - /** - * Internal storage of pivot vector. - * - * @var array - */ - private $piv = []; - - /** - * Constructs Structure to access L, U and piv. - * - * @param Matrix $A Rectangular matrix - * - * @throws MatrixException - */ - public function __construct(Matrix $A) - { - if ($A->getRows() !== $A->getColumns()) { - throw new MatrixException('Matrix is not square matrix'); - } - - // Use a "left-looking", dot-product, Crout/Doolittle algorithm. - $this->LU = $A->toArray(); - $this->m = $A->getRows(); - $this->n = $A->getColumns(); - for ($i = 0; $i < $this->m; ++$i) { - $this->piv[$i] = $i; - } - - $this->pivsign = 1; - $LUcolj = []; - - // Outer loop. - for ($j = 0; $j < $this->n; ++$j) { - // Make a copy of the j-th column to localize references. - 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]; - // Most of the time is spent in the following dot product. - $kmax = min($i, $j); - $s = 0.0; - 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) { - if (abs($LUcolj[$i] ?? 0) > abs($LUcolj[$p] ?? 0)) { - $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 *= -1; - } - - // Compute multipliers. - if (($j < $this->m) && ($this->LU[$j][$j] != 0.0)) { - for ($i = $j + 1; $i < $this->m; ++$i) { - $this->LU[$i][$j] /= $this->LU[$j][$j]; - } - } - } - } - - /** - * Get lower triangular factor. - * - * @return Matrix Lower triangular factor - */ - public function getL(): Matrix - { - $L = []; - for ($i = 0; $i < $this->m; ++$i) { - for ($j = 0; $j < $this->n; ++$j) { - if ($i > $j) { - $L[$i][$j] = $this->LU[$i][$j]; - } elseif ($i == $j) { - $L[$i][$j] = 1.0; - } else { - $L[$i][$j] = 0.0; - } - } - } - - return new Matrix($L); - } - - /** - * Get upper triangular factor. - * - * @return Matrix Upper triangular factor - */ - public function getU(): Matrix - { - $U = []; - for ($i = 0; $i < $this->n; ++$i) { - for ($j = 0; $j < $this->n; ++$j) { - if ($i <= $j) { - $U[$i][$j] = $this->LU[$i][$j]; - } else { - $U[$i][$j] = 0.0; - } - } - } - - return new Matrix($U); - } - - /** - * Return pivot permutation vector. - * - * @return array Pivot vector - */ - public function getPivot(): array - { - return $this->piv; - } - - /** - * Alias for getPivot - * - * @see getPivot - */ - public function getDoublePivot(): array - { - return $this->getPivot(); - } - - /** - * Is the matrix nonsingular? - * - * @return bool true if U, and hence A, is nonsingular. - */ - public function isNonsingular(): bool - { - for ($j = 0; $j < $this->n; ++$j) { - if ($this->LU[$j][$j] == 0) { - return false; - } - } - - return true; - } - - public function det(): float - { - $d = $this->pivsign; - for ($j = 0; $j < $this->n; ++$j) { - $d *= $this->LU[$j][$j]; - } - - return (float) $d; - } - - /** - * Solve A*X = B - * - * @param Matrix $B A Matrix with as many rows as A and any number of columns. - * - * @return array X so that L*U*X = B(piv,:) - * - * @throws MatrixException - */ - public function solve(Matrix $B): array - { - if ($B->getRows() != $this->m) { - throw new MatrixException('Matrix is not square matrix'); - } - - if (!$this->isNonsingular()) { - throw new MatrixException('Matrix is singular'); - } - - // Copy right hand side with pivoting - $nx = $B->getColumns(); - $X = $this->getSubMatrix($B->toArray(), $this->piv, 0, $nx - 1); - // Solve L*Y = B(piv,:) - for ($k = 0; $k < $this->n; ++$k) { - for ($i = $k + 1; $i < $this->n; ++$i) { - for ($j = 0; $j < $nx; ++$j) { - $X[$i][$j] -= $X[$k][$j] * $this->LU[$i][$k]; - } - } - } - - // 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]; - } - } - } - - return $X; - } - - protected function getSubMatrix(array $matrix, array $RL, int $j0, int $jF): array - { - $m = count($RL); - $n = $jF - $j0; - $R = array_fill(0, $m, array_fill(0, $n + 1, 0.0)); - - for ($i = 0; $i < $m; ++$i) { - for ($j = $j0; $j <= $jF; ++$j) { - $R[$i][$j - $j0] = $matrix[$RL[$i]][$j]; - } - } - - return $R; - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/Math/Matrix.php b/lib/mlbackend/php/phpml/src/Phpml/Math/Matrix.php deleted file mode 100644 index 6f07d5d51b0..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/Math/Matrix.php +++ /dev/null @@ -1,327 +0,0 @@ -rows = 1; - $this->columns = count($matrix); - $matrix = [$matrix]; - } else { - $this->rows = count($matrix); - $this->columns = count($matrix[0]); - } - - if ($validate) { - for ($i = 0; $i < $this->rows; ++$i) { - if (count($matrix[$i]) !== $this->columns) { - throw new InvalidArgumentException('Matrix dimensions did not match'); - } - } - } - - $this->matrix = $matrix; - } - - public static function fromFlatArray(array $array): self - { - $matrix = []; - foreach ($array as $value) { - $matrix[] = [$value]; - } - - return new self($matrix); - } - - public function toArray(): array - { - return $this->matrix; - } - - public function toScalar(): float - { - return $this->matrix[0][0]; - } - - public function getRows(): int - { - return $this->rows; - } - - public function getColumns(): int - { - return $this->columns; - } - - /** - * @throws MatrixException - */ - public function getColumnValues(int $column): array - { - if ($column >= $this->columns) { - throw new MatrixException('Column out of range'); - } - - return array_column($this->matrix, $column); - } - - /** - * @return float|int - * - * @throws MatrixException - */ - public function getDeterminant() - { - if ($this->determinant !== null) { - return $this->determinant; - } - - if (!$this->isSquare()) { - throw new MatrixException('Matrix is not square matrix'); - } - - $lu = new LUDecomposition($this); - - return $this->determinant = $lu->det(); - } - - public function isSquare(): bool - { - return $this->columns === $this->rows; - } - - public function transpose(): self - { - if ($this->rows === 1) { - $matrix = array_map(static function ($el): array { - return [$el]; - }, $this->matrix[0]); - } else { - $matrix = array_map(null, ...$this->matrix); - } - - return new self($matrix, false); - } - - public function multiply(self $matrix): self - { - if ($this->columns !== $matrix->getRows()) { - throw new InvalidArgumentException('Inconsistent matrix supplied'); - } - - $array1 = $this->toArray(); - $array2 = $matrix->toArray(); - $colCount = $matrix->columns; - - /* - - To speed-up multiplication, we need to avoid use of array index operator [ ] as much as possible( See #255 for details) - - A combination of "foreach" and "array_column" works much faster then accessing the array via index operator - */ - $product = []; - foreach ($array1 as $row => $rowData) { - for ($col = 0; $col < $colCount; ++$col) { - $columnData = array_column($array2, $col); - $sum = 0; - foreach ($rowData as $key => $valueData) { - $sum += $valueData * $columnData[$key]; - } - - $product[$row][$col] = $sum; - } - } - - return new self($product, false); - } - - /** - * @param float|int $value - */ - public function divideByScalar($value): self - { - $newMatrix = []; - for ($i = 0; $i < $this->rows; ++$i) { - for ($j = 0; $j < $this->columns; ++$j) { - $newMatrix[$i][$j] = $this->matrix[$i][$j] / $value; - } - } - - return new self($newMatrix, false); - } - - /** - * @param float|int $value - */ - public function multiplyByScalar($value): self - { - $newMatrix = []; - for ($i = 0; $i < $this->rows; ++$i) { - for ($j = 0; $j < $this->columns; ++$j) { - $newMatrix[$i][$j] = $this->matrix[$i][$j] * $value; - } - } - - return new self($newMatrix, false); - } - - /** - * Element-wise addition of the matrix with another one - */ - public function add(self $other): self - { - return $this->sum($other); - } - - /** - * Element-wise subtracting of another matrix from this one - */ - public function subtract(self $other): self - { - return $this->sum($other, -1); - } - - public function inverse(): self - { - if (!$this->isSquare()) { - throw new MatrixException('Matrix is not square matrix'); - } - - $LU = new LUDecomposition($this); - $identity = $this->getIdentity(); - $inverse = $LU->solve($identity); - - return new self($inverse, false); - } - - public function crossOut(int $row, int $column): self - { - $newMatrix = []; - $r = 0; - for ($i = 0; $i < $this->rows; ++$i) { - $c = 0; - if ($row != $i) { - for ($j = 0; $j < $this->columns; ++$j) { - if ($column != $j) { - $newMatrix[$r][$c] = $this->matrix[$i][$j]; - ++$c; - } - } - - ++$r; - } - } - - return new self($newMatrix, false); - } - - public function isSingular(): bool - { - return $this->getDeterminant() == 0; - } - - /** - * Frobenius norm (Hilbert–Schmidt norm, Euclidean norm) (‖A‖F) - * Square root of the sum of the square of all elements. - * - * https://en.wikipedia.org/wiki/Matrix_norm#Frobenius_norm - * - * _____________ - * /ᵐ ⁿ - * ‖A‖F = √ Σ Σ |aᵢⱼ|² - * ᵢ₌₁ ᵢ₌₁ - */ - public function frobeniusNorm(): float - { - $squareSum = 0; - for ($i = 0; $i < $this->rows; ++$i) { - for ($j = 0; $j < $this->columns; ++$j) { - $squareSum += $this->matrix[$i][$j] ** 2; - } - } - - return $squareSum ** .5; - } - - /** - * Returns the transpose of given array - */ - public static function transposeArray(array $array): array - { - return (new self($array, false))->transpose()->toArray(); - } - - /** - * Returns the dot product of two arrays
- * Matrix::dot(x, y) ==> x.y' - */ - public static function dot(array $array1, array $array2): array - { - $m1 = new self($array1, false); - $m2 = new self($array2, false); - - return $m1->multiply($m2->transpose())->toArray()[0]; - } - - /** - * Element-wise addition or substraction depending on the given sign parameter - */ - private function sum(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 - */ - private 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); - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/Math/Product.php b/lib/mlbackend/php/phpml/src/Phpml/Math/Product.php deleted file mode 100644 index 78f369311c5..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/Math/Product.php +++ /dev/null @@ -1,23 +0,0 @@ - $value) { - if (is_numeric($value) && is_numeric($b[$index])) { - $product += (float) $value * (float) $b[$index]; - } - } - - return $product; - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/Math/Set.php b/lib/mlbackend/php/phpml/src/Phpml/Math/Set.php deleted file mode 100644 index b22d2f81b5e..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/Math/Set.php +++ /dev/null @@ -1,173 +0,0 @@ -elements = self::sanitize($elements); - } - - /** - * Creates the union of A and B. - */ - public static function union(self $a, self $b): self - { - return new self(array_merge($a->toArray(), $b->toArray())); - } - - /** - * Creates the intersection of A and B. - */ - public static function intersection(self $a, self $b): self - { - return new self(array_intersect($a->toArray(), $b->toArray())); - } - - /** - * Creates the difference of A and B. - */ - public static function difference(self $a, self $b): self - { - return new self(array_diff($a->toArray(), $b->toArray())); - } - - /** - * Creates the Cartesian product of A and B. - * - * @return Set[] - */ - public static function cartesian(self $a, self $b): array - { - $cartesian = []; - - foreach ($a as $multiplier) { - foreach ($b as $multiplicand) { - $cartesian[] = new self(array_merge([$multiplicand], [$multiplier])); - } - } - - return $cartesian; - } - - /** - * Creates the power set of A. - * - * @return Set[] - */ - public static function power(self $a): array - { - $power = [new self()]; - - foreach ($a as $multiplicand) { - foreach ($power as $multiplier) { - $power[] = new self(array_merge([$multiplicand], $multiplier->toArray())); - } - } - - return $power; - } - - /** - * @param string|int|float|bool $element - */ - public function add($element): self - { - return $this->addAll([$element]); - } - - /** - * @param string[]|int[]|float[]|bool[] $elements - */ - public function addAll(array $elements): self - { - $this->elements = self::sanitize(array_merge($this->elements, $elements)); - - return $this; - } - - /** - * @param string|int|float $element - */ - public function remove($element): self - { - return $this->removeAll([$element]); - } - - /** - * @param string[]|int[]|float[] $elements - */ - public function removeAll(array $elements): self - { - $this->elements = self::sanitize(array_diff($this->elements, $elements)); - - return $this; - } - - /** - * @param string|int|float $element - */ - public function contains($element): bool - { - return $this->containsAll([$element]); - } - - /** - * @param string[]|int[]|float[] $elements - */ - public function containsAll(array $elements): bool - { - return count(array_diff($elements, $this->elements)) === 0; - } - - /** - * @return string[]|int[]|float[]|bool[] - */ - public function toArray(): array - { - return $this->elements; - } - - public function getIterator(): ArrayIterator - { - return new ArrayIterator($this->elements); - } - - public function isEmpty(): bool - { - return $this->cardinality() === 0; - } - - public function cardinality(): int - { - return count($this->elements); - } - - /** - * Removes duplicates and rewrites index. - * - * @param string[]|int[]|float[]|bool[] $elements - * - * @return string[]|int[]|float[]|bool[] - */ - private static function sanitize(array $elements): array - { - sort($elements, SORT_ASC); - - return array_values(array_unique($elements, SORT_ASC)); - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/Math/Statistic/ANOVA.php b/lib/mlbackend/php/phpml/src/Phpml/Math/Statistic/ANOVA.php deleted file mode 100644 index 16291819fad..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/Math/Statistic/ANOVA.php +++ /dev/null @@ -1,141 +0,0 @@ - $msbValue) { - $f[$index] = $msbValue / $msw[$index]; - } - - return $f; - } - - private static function sumOfSquaresPerFeature(array $samples): array - { - $sum = array_fill(0, count($samples[0][0]), 0); - foreach ($samples as $class) { - foreach ($class as $sample) { - foreach ($sample as $index => $feature) { - $sum[$index] += $feature ** 2; - } - } - } - - return $sum; - } - - private static function sumOfFeaturesPerClass(array $samples): array - { - return array_map(static function (array $class): array { - $sum = array_fill(0, count($class[0]), 0); - foreach ($class as $sample) { - foreach ($sample as $index => $feature) { - $sum[$index] += $feature; - } - } - - return $sum; - }, $samples); - } - - private static function sumOfSquares(array $sums): array - { - $squares = array_fill(0, count($sums[0]), 0); - foreach ($sums as $row) { - foreach ($row as $index => $sum) { - $squares[$index] += $sum; - } - } - - return array_map(static function ($sum) { - return $sum ** 2; - }, $squares); - } - - private static function squaresSum(array $sums): array - { - foreach ($sums as &$row) { - foreach ($row as &$sum) { - $sum **= 2; - } - } - - return $sums; - } - - private static function calculateSsbn(array $samples, array $sumSamplesSquare, array $samplesPerClass, array $squareSumSamples, int $allSamples): array - { - $ssbn = array_fill(0, count($samples[0][0]), 0); - foreach ($sumSamplesSquare as $classIndex => $class) { - foreach ($class as $index => $feature) { - $ssbn[$index] += $feature / $samplesPerClass[$classIndex]; - } - } - - foreach ($squareSumSamples as $index => $sum) { - $ssbn[$index] -= $sum / $allSamples; - } - - return $ssbn; - } - - private static function calculateSswn(array $ssbn, array $ssAllSamples, array $squareSumSamples, int $allSamples): array - { - $sswn = []; - foreach ($ssAllSamples as $index => $ss) { - $sswn[$index] = ($ss - $squareSumSamples[$index] / $allSamples) - $ssbn[$index]; - } - - return $sswn; - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/Math/Statistic/Correlation.php b/lib/mlbackend/php/phpml/src/Phpml/Math/Statistic/Correlation.php deleted file mode 100644 index c730c473c8a..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/Math/Statistic/Correlation.php +++ /dev/null @@ -1,41 +0,0 @@ - $xi) { - $yi = $y[$index]; - $sum += ($xi - $meanX) * ($yi - $meanY); - } - - if ($sample) { - --$n; - } - - return $sum / $n; - } - - /** - * Calculates covariance of two dimensions, i and k in the given data. - * - * @throws InvalidArgumentException - * @throws \Exception - */ - public static function fromDataset(array $data, int $i, int $k, bool $sample = true, ?float $meanX = null, ?float $meanY = null): float - { - if (count($data) === 0) { - throw new InvalidArgumentException('The array has zero elements'); - } - - $n = count($data); - if ($sample && $n === 1) { - throw new InvalidArgumentException('The array must have at least 2 elements'); - } - - if ($i < 0 || $k < 0 || $i >= $n || $k >= $n) { - throw new InvalidArgumentException('Given indices i and k do not match with the dimensionality of data'); - } - - if ($meanX === null || $meanY === null) { - $x = array_column($data, $i); - $y = array_column($data, $k); - - $meanX = Mean::arithmetic($x); - $meanY = Mean::arithmetic($y); - $sum = 0.0; - foreach ($x as $index => $xi) { - $yi = $y[$index]; - $sum += ($xi - $meanX) * ($yi - $meanY); - } - } else { - // In the case, whole dataset given along with dimension indices, i and k, - // we would like to avoid getting column data with array_column and operate - // over this extra copy of column data for memory efficiency purposes. - // - // Instead we traverse through the whole data and get what we actually need - // without copying the data. This way, memory use will be reduced - // with a slight cost of CPU utilization. - $sum = 0.0; - foreach ($data as $row) { - $val = [0, 0]; - foreach ($row as $index => $col) { - if ($index == $i) { - $val[0] = $col - $meanX; - } - - if ($index == $k) { - $val[1] = $col - $meanY; - } - } - - $sum += $val[0] * $val[1]; - } - } - - if ($sample) { - --$n; - } - - return $sum / $n; - } - - /** - * Returns the covariance matrix of n-dimensional data - * - * @param array|null $means - */ - public static function covarianceMatrix(array $data, ?array $means = null): array - { - $n = count($data[0]); - - if ($means === null) { - $means = []; - for ($i = 0; $i < $n; ++$i) { - $means[] = Mean::arithmetic(array_column($data, $i)); - } - } - - $cov = []; - for ($i = 0; $i < $n; ++$i) { - for ($k = 0; $k < $n; ++$k) { - if ($i > $k) { - $cov[$i][$k] = $cov[$k][$i]; - } else { - $cov[$i][$k] = self::fromDataset( - $data, - $i, - $k, - true, - $means[$i], - $means[$k] - ); - } - } - } - - return $cov; - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/Math/Statistic/Gaussian.php b/lib/mlbackend/php/phpml/src/Phpml/Math/Statistic/Gaussian.php deleted file mode 100644 index 649063d8be7..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/Math/Statistic/Gaussian.php +++ /dev/null @@ -1,50 +0,0 @@ -mean = $mean; - $this->std = $std; - } - - /** - * Returns probability density of the given $value - * - * @return float|int - */ - public function pdf(float $value) - { - // Calculate the probability density by use of normal/Gaussian distribution - // Ref: https://en.wikipedia.org/wiki/Normal_distribution - $std2 = $this->std ** 2; - $mean = $this->mean; - - return exp(-(($value - $mean) ** 2) / (2 * $std2)) / ((2 * $std2 * M_PI) ** .5); - } - - /** - * Returns probability density value of the given $value based on - * given standard deviation and the mean - */ - public static function distributionPdf(float $mean, float $std, float $value): float - { - $normal = new self($mean, $std); - - return $normal->pdf($value); - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/Math/Statistic/Mean.php b/lib/mlbackend/php/phpml/src/Phpml/Math/Statistic/Mean.php deleted file mode 100644 index 2ae55aa8c45..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/Math/Statistic/Mean.php +++ /dev/null @@ -1,65 +0,0 @@ - $label) { - if ($label == $predictedLabels[$index]) { - ++$score; - } - } - - if ($normalize) { - $score /= count($actualLabels); - } - - return $score; - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/Metric/ClassificationReport.php b/lib/mlbackend/php/phpml/src/Phpml/Metric/ClassificationReport.php deleted file mode 100644 index 0abac76933a..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/Metric/ClassificationReport.php +++ /dev/null @@ -1,226 +0,0 @@ -aggregateClassificationResults($actualLabels, $predictedLabels); - $this->computeMetrics(); - $this->computeAverage($average); - } - - public function getPrecision(): array - { - return $this->precision; - } - - public function getRecall(): array - { - return $this->recall; - } - - public function getF1score(): array - { - return $this->f1score; - } - - public function getSupport(): array - { - return $this->support; - } - - public function getAverage(): array - { - return $this->average; - } - - private function aggregateClassificationResults(array $actualLabels, array $predictedLabels): void - { - $truePositive = $falsePositive = $falseNegative = $support = self::getLabelIndexedArray($actualLabels, $predictedLabels); - - foreach ($actualLabels as $index => $actual) { - $predicted = $predictedLabels[$index]; - ++$support[$actual]; - - if ($actual === $predicted) { - ++$truePositive[$actual]; - } else { - ++$falsePositive[$predicted]; - ++$falseNegative[$actual]; - } - } - - $this->truePositive = $truePositive; - $this->falsePositive = $falsePositive; - $this->falseNegative = $falseNegative; - $this->support = $support; - } - - private function computeMetrics(): void - { - foreach ($this->truePositive as $label => $tp) { - $this->precision[$label] = $this->computePrecision($tp, $this->falsePositive[$label]); - $this->recall[$label] = $this->computeRecall($tp, $this->falseNegative[$label]); - $this->f1score[$label] = $this->computeF1Score((float) $this->precision[$label], (float) $this->recall[$label]); - } - } - - private function computeAverage(int $average): void - { - switch ($average) { - case self::MICRO_AVERAGE: - $this->computeMicroAverage(); - - return; - case self::MACRO_AVERAGE: - $this->computeMacroAverage(); - - return; - case self::WEIGHTED_AVERAGE: - $this->computeWeightedAverage(); - - return; - } - } - - private function computeMicroAverage(): void - { - $truePositive = (int) array_sum($this->truePositive); - $falsePositive = (int) array_sum($this->falsePositive); - $falseNegative = (int) array_sum($this->falseNegative); - - $precision = $this->computePrecision($truePositive, $falsePositive); - $recall = $this->computeRecall($truePositive, $falseNegative); - $f1score = $this->computeF1Score($precision, $recall); - - $this->average = compact('precision', 'recall', 'f1score'); - } - - private function computeMacroAverage(): void - { - foreach (['precision', 'recall', 'f1score'] as $metric) { - $values = $this->{$metric}; - if (count($values) == 0) { - $this->average[$metric] = 0.0; - - continue; - } - - $this->average[$metric] = array_sum($values) / count($values); - } - } - - private function computeWeightedAverage(): void - { - foreach (['precision', 'recall', 'f1score'] as $metric) { - $values = $this->{$metric}; - if (count($values) == 0) { - $this->average[$metric] = 0.0; - - continue; - } - - $sum = 0; - foreach ($values as $i => $value) { - $sum += $value * $this->support[$i]; - } - - $this->average[$metric] = $sum / array_sum($this->support); - } - } - - private function computePrecision(int $truePositive, int $falsePositive): float - { - $divider = $truePositive + $falsePositive; - if ($divider == 0) { - return 0.0; - } - - return $truePositive / $divider; - } - - private function computeRecall(int $truePositive, int $falseNegative): float - { - $divider = $truePositive + $falseNegative; - if ($divider == 0) { - return 0.0; - } - - return $truePositive / $divider; - } - - private function computeF1Score(float $precision, float $recall): float - { - $divider = $precision + $recall; - if ($divider == 0) { - return 0.0; - } - - return 2.0 * (($precision * $recall) / $divider); - } - - private static function getLabelIndexedArray(array $actualLabels, array $predictedLabels): array - { - $labels = array_values(array_unique(array_merge($actualLabels, $predictedLabels))); - sort($labels); - - return (array) array_combine($labels, array_fill(0, count($labels), 0)); - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/Metric/ConfusionMatrix.php b/lib/mlbackend/php/phpml/src/Phpml/Metric/ConfusionMatrix.php deleted file mode 100644 index 5b8021cb25d..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/Metric/ConfusionMatrix.php +++ /dev/null @@ -1,53 +0,0 @@ - $actual) { - $predicted = $predictedLabels[$index]; - - if (!isset($labels[$actual], $labels[$predicted])) { - continue; - } - - if ($predicted === $actual) { - $row = $column = $labels[$actual]; - } else { - $row = $labels[$actual]; - $column = $labels[$predicted]; - } - - ++$matrix[$row][$column]; - } - - return $matrix; - } - - private static function generateMatrixWithZeros(array $labels): array - { - $count = count($labels); - $matrix = []; - - for ($i = 0; $i < $count; ++$i) { - $matrix[$i] = array_fill(0, $count, 0); - } - - return $matrix; - } - - private static function getUniqueLabels(array $labels): array - { - $labels = array_values(array_unique($labels)); - sort($labels); - - return array_flip($labels); - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/Metric/Regression.php b/lib/mlbackend/php/phpml/src/Phpml/Metric/Regression.php deleted file mode 100644 index c833f6aaef6..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/Metric/Regression.php +++ /dev/null @@ -1,86 +0,0 @@ - $target) { - $errors[] = (($target - $predictions[$index]) ** 2); - } - - return Mean::arithmetic($errors); - } - - public static function meanSquaredLogarithmicError(array $targets, array $predictions): float - { - self::assertCountEquals($targets, $predictions); - - $errors = []; - foreach ($targets as $index => $target) { - $errors[] = log((1 + $target) / (1 + $predictions[$index])) ** 2; - } - - return Mean::arithmetic($errors); - } - - public static function meanAbsoluteError(array $targets, array $predictions): float - { - self::assertCountEquals($targets, $predictions); - - $errors = []; - foreach ($targets as $index => $target) { - $errors[] = abs($target - $predictions[$index]); - } - - return Mean::arithmetic($errors); - } - - public static function medianAbsoluteError(array $targets, array $predictions): float - { - self::assertCountEquals($targets, $predictions); - - $errors = []; - foreach ($targets as $index => $target) { - $errors[] = abs($target - $predictions[$index]); - } - - return (float) Mean::median($errors); - } - - public static function r2Score(array $targets, array $predictions): float - { - self::assertCountEquals($targets, $predictions); - - return Correlation::pearson($targets, $predictions) ** 2; - } - - public static function maxError(array $targets, array $predictions): float - { - self::assertCountEquals($targets, $predictions); - - $errors = []; - foreach ($targets as $index => $target) { - $errors[] = abs($target - $predictions[$index]); - } - - return (float) max($errors); - } - - private static function assertCountEquals(array &$targets, array &$predictions): void - { - if (count($targets) !== count($predictions)) { - throw new InvalidArgumentException('Targets count must be equal with predictions count'); - } - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/ModelManager.php b/lib/mlbackend/php/phpml/src/Phpml/ModelManager.php deleted file mode 100644 index e0f5be57926..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/ModelManager.php +++ /dev/null @@ -1,42 +0,0 @@ -= 0 ? 1.0 : 0.0; - } - - /** - * @param float|int $value - * @param float|int $computedvalue - */ - public function differentiate($value, $computedvalue): float - { - if ($value === 0 || $value === 0.0) { - return 1; - } - - return 0; - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/NeuralNetwork/ActivationFunction/Gaussian.php b/lib/mlbackend/php/phpml/src/Phpml/NeuralNetwork/ActivationFunction/Gaussian.php deleted file mode 100644 index 29cfef2d3ca..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/NeuralNetwork/ActivationFunction/Gaussian.php +++ /dev/null @@ -1,27 +0,0 @@ -beta = $beta; - } - - /** - * @param float|int $value - */ - public function compute($value): float - { - return tanh($this->beta * $value); - } - - /** - * @param float|int $value - * @param float|int $computedvalue - */ - public function differentiate($value, $computedvalue): float - { - return 1 - $computedvalue ** 2; - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/NeuralNetwork/ActivationFunction/PReLU.php b/lib/mlbackend/php/phpml/src/Phpml/NeuralNetwork/ActivationFunction/PReLU.php deleted file mode 100644 index 88212d1a1df..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/NeuralNetwork/ActivationFunction/PReLU.php +++ /dev/null @@ -1,37 +0,0 @@ -beta = $beta; - } - - /** - * @param float|int $value - */ - public function compute($value): float - { - return $value >= 0 ? $value : $this->beta * $value; - } - - /** - * @param float|int $value - * @param float|int $computedvalue - */ - public function differentiate($value, $computedvalue): float - { - return $computedvalue >= 0 ? 1.0 : $this->beta; - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/NeuralNetwork/ActivationFunction/Sigmoid.php b/lib/mlbackend/php/phpml/src/Phpml/NeuralNetwork/ActivationFunction/Sigmoid.php deleted file mode 100644 index edad3d6e1ff..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/NeuralNetwork/ActivationFunction/Sigmoid.php +++ /dev/null @@ -1,37 +0,0 @@ -beta = $beta; - } - - /** - * @param float|int $value - */ - public function compute($value): float - { - return 1 / (1 + exp(-$this->beta * $value)); - } - - /** - * @param float|int $value - * @param float|int $computedvalue - */ - public function differentiate($value, $computedvalue): float - { - return $computedvalue * (1 - $computedvalue); - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/NeuralNetwork/ActivationFunction/ThresholdedReLU.php b/lib/mlbackend/php/phpml/src/Phpml/NeuralNetwork/ActivationFunction/ThresholdedReLU.php deleted file mode 100644 index f8f8247cd4d..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/NeuralNetwork/ActivationFunction/ThresholdedReLU.php +++ /dev/null @@ -1,37 +0,0 @@ -theta = $theta; - } - - /** - * @param float|int $value - */ - public function compute($value): float - { - return $value > $this->theta ? $value : 0.0; - } - - /** - * @param float|int $value - * @param float|int $calculatedvalue - */ - public function differentiate($value, $calculatedvalue): float - { - return $calculatedvalue >= $this->theta ? 1.0 : 0.0; - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/NeuralNetwork/Layer.php b/lib/mlbackend/php/phpml/src/Phpml/NeuralNetwork/Layer.php deleted file mode 100644 index 1c67c0412ea..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/NeuralNetwork/Layer.php +++ /dev/null @@ -1,52 +0,0 @@ -nodes[] = $this->createNode($nodeClass, $activationFunction); - } - } - - public function addNode(Node $node): void - { - $this->nodes[] = $node; - } - - /** - * @return Node[] - */ - public function getNodes(): array - { - return $this->nodes; - } - - private function createNode(string $nodeClass, ?ActivationFunction $activationFunction = null): Node - { - if ($nodeClass === Neuron::class) { - return new Neuron($activationFunction); - } - - return new $nodeClass(); - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/NeuralNetwork/Network.php b/lib/mlbackend/php/phpml/src/Phpml/NeuralNetwork/Network.php deleted file mode 100644 index 0b0ce651133..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/NeuralNetwork/Network.php +++ /dev/null @@ -1,22 +0,0 @@ -layers[] = $layer; - } - - /** - * @return Layer[] - */ - public function getLayers(): array - { - return $this->layers; - } - - public function removeLayers(): void - { - unset($this->layers); - } - - public function getOutputLayer(): Layer - { - return $this->layers[count($this->layers) - 1]; - } - - public function getOutput(): array - { - $result = []; - foreach ($this->getOutputLayer()->getNodes() as $neuron) { - $result[] = $neuron->getOutput(); - } - - return $result; - } - - /** - * @param mixed $input - */ - public function setInput($input): Network - { - $firstLayer = $this->layers[0]; - - foreach ($firstLayer->getNodes() as $key => $neuron) { - if ($neuron instanceof Input) { - $neuron->setInput($input[$key]); - } - } - - foreach ($this->getLayers() as $layer) { - foreach ($layer->getNodes() as $node) { - if ($node instanceof Neuron) { - $node->reset(); - } - } - } - - return $this; - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/NeuralNetwork/Network/MultilayerPerceptron.php b/lib/mlbackend/php/phpml/src/Phpml/NeuralNetwork/Network/MultilayerPerceptron.php deleted file mode 100644 index beefb1e2b84..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/NeuralNetwork/Network/MultilayerPerceptron.php +++ /dev/null @@ -1,231 +0,0 @@ -classes = array_values($classes); - $this->iterations = $iterations; - $this->inputLayerFeatures = $inputLayerFeatures; - $this->hiddenLayers = $hiddenLayers; - $this->activationFunction = $activationFunction; - $this->learningRate = $learningRate; - - $this->initNetwork(); - } - - public function train(array $samples, array $targets): void - { - $this->reset(); - $this->initNetwork(); - $this->partialTrain($samples, $targets, $this->classes); - } - - /** - * @throws InvalidArgumentException - */ - public function partialTrain(array $samples, array $targets, array $classes = []): void - { - if (count($classes) > 0 && array_values($classes) !== $this->classes) { - // We require the list of classes in the constructor. - throw new InvalidArgumentException( - 'The provided classes don\'t match the classes provided in the constructor' - ); - } - - for ($i = 0; $i < $this->iterations; ++$i) { - $this->trainSamples($samples, $targets); - } - } - - public function setLearningRate(float $learningRate): void - { - $this->learningRate = $learningRate; - $this->backpropagation->setLearningRate($this->learningRate); - } - - public function getOutput(): array - { - $result = []; - foreach ($this->getOutputLayer()->getNodes() as $i => $neuron) { - $result[$this->classes[$i]] = $neuron->getOutput(); - } - - return $result; - } - - public function getLearningRate(): float - { - return $this->learningRate; - } - - public function getBackpropagation(): Backpropagation - { - return $this->backpropagation; - } - - /** - * @param mixed $target - */ - abstract protected function trainSample(array $sample, $target): void; - - /** - * @return mixed - */ - abstract protected function predictSample(array $sample); - - protected function reset(): void - { - $this->removeLayers(); - } - - private function initNetwork(): void - { - $this->addInputLayer($this->inputLayerFeatures); - $this->addNeuronLayers($this->hiddenLayers, $this->activationFunction); - - // Sigmoid function for the output layer as we want a value from 0 to 1. - $sigmoid = new Sigmoid(); - $this->addNeuronLayers([count($this->classes)], $sigmoid); - - $this->addBiasNodes(); - $this->generateSynapses(); - - $this->backpropagation = new Backpropagation($this->learningRate); - } - - private function addInputLayer(int $nodes): void - { - $this->addLayer(new Layer($nodes, Input::class)); - } - - private function addNeuronLayers(array $layers, ?ActivationFunction $defaultActivationFunction = null): void - { - foreach ($layers as $layer) { - if (is_array($layer)) { - $function = $layer[1] instanceof ActivationFunction ? $layer[1] : $defaultActivationFunction; - $this->addLayer(new Layer($layer[0], Neuron::class, $function)); - } elseif ($layer instanceof Layer) { - $this->addLayer($layer); - } else { - $this->addLayer(new Layer($layer, Neuron::class, $defaultActivationFunction)); - } - } - } - - private function generateSynapses(): void - { - $layersNumber = count($this->layers) - 1; - for ($i = 0; $i < $layersNumber; ++$i) { - $currentLayer = $this->layers[$i]; - $nextLayer = $this->layers[$i + 1]; - $this->generateLayerSynapses($nextLayer, $currentLayer); - } - } - - private function addBiasNodes(): void - { - $biasLayers = count($this->layers) - 1; - for ($i = 0; $i < $biasLayers; ++$i) { - $this->layers[$i]->addNode(new Bias()); - } - } - - private function generateLayerSynapses(Layer $nextLayer, Layer $currentLayer): void - { - foreach ($nextLayer->getNodes() as $nextNeuron) { - if ($nextNeuron instanceof Neuron) { - $this->generateNeuronSynapses($currentLayer, $nextNeuron); - } - } - } - - private function generateNeuronSynapses(Layer $currentLayer, Neuron $nextNeuron): void - { - foreach ($currentLayer->getNodes() as $currentNeuron) { - $nextNeuron->addSynapse(new Synapse($currentNeuron)); - } - } - - private function trainSamples(array $samples, array $targets): void - { - foreach ($targets as $key => $target) { - $this->trainSample($samples[$key], $target); - } - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/NeuralNetwork/Node.php b/lib/mlbackend/php/phpml/src/Phpml/NeuralNetwork/Node.php deleted file mode 100644 index 0b7726ff59c..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/NeuralNetwork/Node.php +++ /dev/null @@ -1,10 +0,0 @@ -input = $input; - } - - public function getOutput(): float - { - return $this->input; - } - - public function setInput(float $input): void - { - $this->input = $input; - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/NeuralNetwork/Node/Neuron.php b/lib/mlbackend/php/phpml/src/Phpml/NeuralNetwork/Node/Neuron.php deleted file mode 100644 index 6681e66b103..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/NeuralNetwork/Node/Neuron.php +++ /dev/null @@ -1,76 +0,0 @@ -activationFunction = $activationFunction ?? new Sigmoid(); - } - - public function addSynapse(Synapse $synapse): void - { - $this->synapses[] = $synapse; - } - - /** - * @return Synapse[] - */ - public function getSynapses(): array - { - return $this->synapses; - } - - public function getOutput(): float - { - if ($this->output === 0.0) { - $this->z = 0; - foreach ($this->synapses as $synapse) { - $this->z += $synapse->getOutput(); - } - - $this->output = $this->activationFunction->compute($this->z); - } - - return $this->output; - } - - public function getDerivative(): float - { - return $this->activationFunction->differentiate($this->z, $this->output); - } - - public function reset(): void - { - $this->output = 0.0; - $this->z = 0.0; - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/NeuralNetwork/Node/Neuron/Synapse.php b/lib/mlbackend/php/phpml/src/Phpml/NeuralNetwork/Node/Neuron/Synapse.php deleted file mode 100644 index d749937f28f..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/NeuralNetwork/Node/Neuron/Synapse.php +++ /dev/null @@ -1,54 +0,0 @@ -node = $node; - $this->weight = $weight ?? $this->generateRandomWeight(); - } - - public function getOutput(): float - { - return $this->weight * $this->node->getOutput(); - } - - public function changeWeight(float $delta): void - { - $this->weight += $delta; - } - - public function getWeight(): float - { - return $this->weight; - } - - public function getNode(): Node - { - return $this->node; - } - - protected function generateRandomWeight(): float - { - return (1 / random_int(5, 25) * random_int(0, 1)) > 0 ? -1 : 1; - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/NeuralNetwork/Training/Backpropagation.php b/lib/mlbackend/php/phpml/src/Phpml/NeuralNetwork/Training/Backpropagation.php deleted file mode 100644 index 69a3e2a8420..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/NeuralNetwork/Training/Backpropagation.php +++ /dev/null @@ -1,100 +0,0 @@ -setLearningRate($learningRate); - } - - public function setLearningRate(float $learningRate): void - { - $this->learningRate = $learningRate; - } - - public function getLearningRate(): float - { - return $this->learningRate; - } - - /** - * @param mixed $targetClass - */ - public function backpropagate(array $layers, $targetClass): void - { - $layersNumber = count($layers); - - // Backpropagation. - for ($i = $layersNumber; $i > 1; --$i) { - $this->sigmas = []; - foreach ($layers[$i - 1]->getNodes() as $key => $neuron) { - if ($neuron instanceof Neuron) { - $sigma = $this->getSigma($neuron, $targetClass, $key, $i == $layersNumber); - foreach ($neuron->getSynapses() as $synapse) { - $synapse->changeWeight($this->learningRate * $sigma * $synapse->getNode()->getOutput()); - } - } - } - - $this->prevSigmas = $this->sigmas; - } - - // Clean some memory (also it helps make MLP persistency & children more maintainable). - $this->sigmas = []; - $this->prevSigmas = []; - } - - private function getSigma(Neuron $neuron, int $targetClass, int $key, bool $lastLayer): float - { - $neuronOutput = $neuron->getOutput(); - $sigma = $neuron->getDerivative(); - - if ($lastLayer) { - $value = 0; - if ($targetClass === $key) { - $value = 1; - } - - $sigma *= ($value - $neuronOutput); - } else { - $sigma *= $this->getPrevSigma($neuron); - } - - $this->sigmas[] = new Sigma($neuron, $sigma); - - return $sigma; - } - - private function getPrevSigma(Neuron $neuron): float - { - $sigma = 0.0; - - foreach ($this->prevSigmas as $neuronSigma) { - $sigma += $neuronSigma->getSigmaForNeuron($neuron); - } - - return $sigma; - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/NeuralNetwork/Training/Backpropagation/Sigma.php b/lib/mlbackend/php/phpml/src/Phpml/NeuralNetwork/Training/Backpropagation/Sigma.php deleted file mode 100644 index f21c7b12557..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/NeuralNetwork/Training/Backpropagation/Sigma.php +++ /dev/null @@ -1,49 +0,0 @@ -neuron = $neuron; - $this->sigma = $sigma; - } - - public function getNeuron(): Neuron - { - return $this->neuron; - } - - public function getSigma(): float - { - return $this->sigma; - } - - public function getSigmaForNeuron(Neuron $neuron): float - { - $sigma = 0.0; - - foreach ($this->neuron->getSynapses() as $synapse) { - if ($synapse->getNode() == $neuron) { - $sigma += $synapse->getWeight() * $this->getSigma(); - } - } - - return $sigma; - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/Pipeline.php b/lib/mlbackend/php/phpml/src/Phpml/Pipeline.php deleted file mode 100644 index 200173bdb78..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/Pipeline.php +++ /dev/null @@ -1,87 +0,0 @@ -transformers = array_map(static function (Transformer $transformer): Transformer { - return $transformer; - }, $transformers); - $this->estimator = $estimator; - } - - /** - * @return Transformer[] - */ - public function getTransformers(): array - { - return $this->transformers; - } - - public function getEstimator(): ?Estimator - { - return $this->estimator; - } - - public function train(array $samples, array $targets): void - { - if ($this->estimator === null) { - throw new InvalidOperationException('Pipeline without estimator can\'t use train method'); - } - - foreach ($this->transformers as $transformer) { - $transformer->fit($samples, $targets); - $transformer->transform($samples, $targets); - } - - $this->estimator->train($samples, $targets); - } - - /** - * @return mixed - */ - public function predict(array $samples) - { - $this->transform($samples); - - if ($this->estimator === null) { - throw new InvalidOperationException('Pipeline without estimator can\'t use predict method'); - } - - return $this->estimator->predict($samples); - } - - public function fit(array $samples, ?array $targets = null): void - { - foreach ($this->transformers as $transformer) { - $transformer->fit($samples, $targets); - $transformer->transform($samples, $targets); - } - } - - public function transform(array &$samples, ?array &$targets = null): void - { - foreach ($this->transformers as $transformer) { - $transformer->transform($samples, $targets); - } - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/Preprocessing/ColumnFilter.php b/lib/mlbackend/php/phpml/src/Phpml/Preprocessing/ColumnFilter.php deleted file mode 100644 index afe2db7dae1..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/Preprocessing/ColumnFilter.php +++ /dev/null @@ -1,42 +0,0 @@ -datasetColumns = array_map(static function (string $column): string { - return $column; - }, $datasetColumns); - $this->filterColumns = array_map(static function (string $column): string { - return $column; - }, $filterColumns); - } - - public function fit(array $samples, ?array $targets = null): void - { - //nothing to do - } - - public function transform(array &$samples, ?array &$targets = null): void - { - $keys = array_intersect($this->datasetColumns, $this->filterColumns); - - foreach ($samples as &$sample) { - $sample = array_values(array_intersect_key($sample, $keys)); - } - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/Preprocessing/Imputer.php b/lib/mlbackend/php/phpml/src/Phpml/Preprocessing/Imputer.php deleted file mode 100644 index 88ee2dd6795..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/Preprocessing/Imputer.php +++ /dev/null @@ -1,87 +0,0 @@ -missingValue = $missingValue; - $this->strategy = $strategy; - $this->axis = $axis; - $this->samples = $samples; - } - - public function fit(array $samples, ?array $targets = null): void - { - $this->samples = $samples; - } - - public function transform(array &$samples, ?array &$targets = null): void - { - if ($this->samples === []) { - throw new InvalidOperationException('Missing training samples for Imputer.'); - } - - foreach ($samples as &$sample) { - $this->preprocessSample($sample); - } - } - - private function preprocessSample(array &$sample): void - { - foreach ($sample as $column => &$value) { - if ($value === $this->missingValue) { - $value = $this->strategy->replaceValue($this->getAxis($column, $sample)); - } - } - } - - private function getAxis(int $column, array $currentSample): array - { - if ($this->axis === self::AXIS_ROW) { - return array_diff($currentSample, [$this->missingValue]); - } - - $axis = []; - foreach ($this->samples as $sample) { - if ($sample[$column] !== $this->missingValue) { - $axis[] = $sample[$column]; - } - } - - return $axis; - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/Preprocessing/Imputer/Strategy.php b/lib/mlbackend/php/phpml/src/Phpml/Preprocessing/Imputer/Strategy.php deleted file mode 100644 index 96397c12f46..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/Preprocessing/Imputer/Strategy.php +++ /dev/null @@ -1,13 +0,0 @@ -classes = []; - - foreach ($samples as $sample) { - if (!isset($this->classes[(string) $sample])) { - $this->classes[(string) $sample] = count($this->classes); - } - } - } - - public function transform(array &$samples, ?array &$targets = null): void - { - foreach ($samples as &$sample) { - $sample = $this->classes[(string) $sample]; - } - } - - public function inverseTransform(array &$samples): void - { - $classes = array_flip($this->classes); - foreach ($samples as &$sample) { - $sample = $classes[$sample]; - } - } - - /** - * @return string[] - */ - public function classes(): array - { - return array_keys($this->classes); - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/Preprocessing/LambdaTransformer.php b/lib/mlbackend/php/phpml/src/Phpml/Preprocessing/LambdaTransformer.php deleted file mode 100644 index f6b5a8becd9..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/Preprocessing/LambdaTransformer.php +++ /dev/null @@ -1,30 +0,0 @@ -lambda = $lambda; - } - - public function fit(array $samples, ?array $targets = null): void - { - // nothing to do - } - - public function transform(array &$samples, ?array &$targets = null): void - { - foreach ($samples as &$sample) { - $sample = call_user_func($this->lambda, $sample); - } - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/Preprocessing/Normalizer.php b/lib/mlbackend/php/phpml/src/Phpml/Preprocessing/Normalizer.php deleted file mode 100644 index 5ba43e65b27..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/Preprocessing/Normalizer.php +++ /dev/null @@ -1,131 +0,0 @@ -norm = $norm; - } - - public function fit(array $samples, ?array $targets = null): void - { - if ($this->fitted) { - return; - } - - if ($this->norm === self::NORM_STD) { - $features = range(0, count($samples[0]) - 1); - foreach ($features as $i) { - $values = array_column($samples, $i); - $this->std[$i] = StandardDeviation::population($values); - $this->mean[$i] = Mean::arithmetic($values); - } - } - - $this->fitted = true; - } - - public function transform(array &$samples, ?array &$targets = null): void - { - $methods = [ - self::NORM_L1 => 'normalizeL1', - self::NORM_L2 => 'normalizeL2', - self::NORM_STD => 'normalizeSTD', - ]; - $method = $methods[$this->norm]; - - $this->fit($samples); - - foreach ($samples as &$sample) { - $this->{$method}($sample); - } - } - - private function normalizeL1(array &$sample): void - { - $norm1 = 0; - foreach ($sample as $feature) { - $norm1 += abs($feature); - } - - if ($norm1 == 0) { - $count = count($sample); - $sample = array_fill(0, $count, 1.0 / $count); - } else { - array_walk($sample, function (&$feature) use ($norm1): void { - $feature /= $norm1; - }); - } - } - - private function normalizeL2(array &$sample): void - { - $norm2 = 0; - foreach ($sample as $feature) { - $norm2 += $feature * $feature; - } - - $norm2 **= .5; - - if ($norm2 == 0) { - $sample = array_fill(0, count($sample), 1); - } else { - array_walk($sample, function (&$feature) use ($norm2): void { - $feature /= $norm2; - }); - } - } - - private function normalizeSTD(array &$sample): void - { - foreach (array_keys($sample) as $i) { - if ($this->std[$i] != 0) { - $sample[$i] = ($sample[$i] - $this->mean[$i]) / $this->std[$i]; - } else { - // Same value for all samples. - $sample[$i] = 0; - } - } - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/Preprocessing/NumberConverter.php b/lib/mlbackend/php/phpml/src/Phpml/Preprocessing/NumberConverter.php deleted file mode 100644 index 68247b13770..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/Preprocessing/NumberConverter.php +++ /dev/null @@ -1,47 +0,0 @@ -transformTargets = $transformTargets; - $this->nonNumericPlaceholder = $nonNumericPlaceholder; - } - - public function fit(array $samples, ?array $targets = null): void - { - //nothing to do - } - - public function transform(array &$samples, ?array &$targets = null): void - { - foreach ($samples as &$sample) { - foreach ($sample as &$feature) { - $feature = is_numeric($feature) ? (float) $feature : $this->nonNumericPlaceholder; - } - } - - if ($this->transformTargets && is_array($targets)) { - foreach ($targets as &$target) { - $target = is_numeric($target) ? (float) $target : $this->nonNumericPlaceholder; - } - } - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/Preprocessing/OneHotEncoder.php b/lib/mlbackend/php/phpml/src/Phpml/Preprocessing/OneHotEncoder.php deleted file mode 100644 index c9d4d0af528..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/Preprocessing/OneHotEncoder.php +++ /dev/null @@ -1,66 +0,0 @@ -ignoreUnknown = $ignoreUnknown; - } - - public function fit(array $samples, ?array $targets = null): void - { - foreach (array_keys(array_values(current($samples))) as $column) { - $this->fitColumn($column, array_values(array_unique(array_column($samples, $column)))); - } - } - - public function transform(array &$samples, ?array &$targets = null): void - { - foreach ($samples as &$sample) { - $sample = $this->transformSample(array_values($sample)); - } - } - - private function fitColumn(int $column, array $values): void - { - $count = count($values); - foreach ($values as $index => $value) { - $map = array_fill(0, $count, 0); - $map[$index] = 1; - $this->categories[$column][$value] = $map; - } - } - - private function transformSample(array $sample): array - { - $encoded = []; - foreach ($sample as $column => $feature) { - if (!isset($this->categories[$column][$feature]) && !$this->ignoreUnknown) { - throw new InvalidArgumentException(sprintf('Missing category "%s" for column %s in trained encoder', $feature, $column)); - } - - $encoded = array_merge( - $encoded, - $this->categories[$column][$feature] ?? array_fill(0, count($this->categories[$column]), 0) - ); - } - - return $encoded; - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/Preprocessing/Preprocessor.php b/lib/mlbackend/php/phpml/src/Phpml/Preprocessing/Preprocessor.php deleted file mode 100644 index 3ec1566b88c..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/Preprocessing/Preprocessor.php +++ /dev/null @@ -1,11 +0,0 @@ -samples = array_merge($this->samples, $samples); - $this->targets = array_merge($this->targets, $targets); - - $this->computeCoefficients(); - } - - /** - * @return mixed - */ - public function predictSample(array $sample) - { - $result = $this->intercept; - foreach ($this->coefficients as $index => $coefficient) { - $result += $coefficient * $sample[$index]; - } - - return $result; - } - - public function getCoefficients(): array - { - return $this->coefficients; - } - - public function getIntercept(): float - { - return $this->intercept; - } - - /** - * coefficient(b) = (X'X)-1X'Y. - */ - private function computeCoefficients(): void - { - $samplesMatrix = $this->getSamplesMatrix(); - $targetsMatrix = $this->getTargetsMatrix(); - - $ts = $samplesMatrix->transpose()->multiply($samplesMatrix)->inverse(); - $tf = $samplesMatrix->transpose()->multiply($targetsMatrix); - - $this->coefficients = $ts->multiply($tf)->getColumnValues(0); - $this->intercept = array_shift($this->coefficients); - } - - /** - * Add one dimension for intercept calculation. - */ - private function getSamplesMatrix(): Matrix - { - $samples = []; - foreach ($this->samples as $sample) { - array_unshift($sample, 1); - $samples[] = $sample; - } - - return new Matrix($samples); - } - - private function getTargetsMatrix(): Matrix - { - if (is_array($this->targets[0])) { - return new Matrix($this->targets); - } - - return Matrix::fromFlatArray($this->targets); - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/Regression/Regression.php b/lib/mlbackend/php/phpml/src/Phpml/Regression/Regression.php deleted file mode 100644 index 542685ccb28..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/Regression/Regression.php +++ /dev/null @@ -1,11 +0,0 @@ - $label) { - $set .= sprintf('%s %s %s', ($targets ? $label : $numericLabels[$label]), self::sampleRow($samples[$index]), PHP_EOL); - } - - return $set; - } - - public static function testSet(array $samples): string - { - if (count($samples) === 0) { - throw new InvalidArgumentException('The array has zero elements'); - } - - if (!is_array($samples[0])) { - $samples = [$samples]; - } - - $set = ''; - foreach ($samples as $sample) { - $set .= sprintf('0 %s %s', self::sampleRow($sample), PHP_EOL); - } - - return $set; - } - - public static function predictions(string $rawPredictions, array $labels): array - { - $numericLabels = self::numericLabels($labels); - $results = []; - foreach (explode(PHP_EOL, $rawPredictions) as $result) { - if (isset($result[0])) { - $results[] = array_search((int) $result, $numericLabels, true); - } - } - - return $results; - } - - public static function probabilities(string $rawPredictions, array $labels): array - { - $numericLabels = self::numericLabels($labels); - - $predictions = explode(PHP_EOL, trim($rawPredictions)); - - $header = array_shift($predictions); - $headerColumns = explode(' ', (string) $header); - array_shift($headerColumns); - - $columnLabels = []; - foreach ($headerColumns as $numericLabel) { - $columnLabels[] = array_search((int) $numericLabel, $numericLabels, true); - } - - $results = []; - foreach ($predictions as $rawResult) { - $probabilities = explode(' ', $rawResult); - array_shift($probabilities); - - $result = []; - foreach ($probabilities as $i => $prob) { - $result[$columnLabels[$i]] = (float) $prob; - } - - $results[] = $result; - } - - return $results; - } - - public static function numericLabels(array $labels): array - { - $numericLabels = []; - foreach ($labels as $label) { - if (isset($numericLabels[$label])) { - continue; - } - - $numericLabels[$label] = count($numericLabels); - } - - return $numericLabels; - } - - private static function sampleRow(array $sample): string - { - $row = []; - foreach ($sample as $index => $feature) { - $row[] = sprintf('%s:%F', $index + 1, $feature); - } - - return implode(' ', $row); - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/SupportVectorMachine/Kernel.php b/lib/mlbackend/php/phpml/src/Phpml/SupportVectorMachine/Kernel.php deleted file mode 100644 index af76e6db52e..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/SupportVectorMachine/Kernel.php +++ /dev/null @@ -1,28 +0,0 @@ -type = $type; - $this->kernel = $kernel; - $this->cost = $cost; - $this->nu = $nu; - $this->degree = $degree; - $this->gamma = $gamma; - $this->coef0 = $coef0; - $this->epsilon = $epsilon; - $this->tolerance = $tolerance; - $this->cacheSize = $cacheSize; - $this->shrinking = $shrinking; - $this->probabilityEstimates = $probabilityEstimates; - - $rootPath = realpath(implode(DIRECTORY_SEPARATOR, [__DIR__, '..', '..'])).DIRECTORY_SEPARATOR; - - $this->binPath = $rootPath.'bin'.DIRECTORY_SEPARATOR.'libsvm'.DIRECTORY_SEPARATOR; - $this->varPath = $rootPath.'var'.DIRECTORY_SEPARATOR; - } - - public function setBinPath(string $binPath): void - { - $this->ensureDirectorySeparator($binPath); - $this->verifyBinPath($binPath); - - $this->binPath = $binPath; - } - - public function setVarPath(string $varPath): void - { - if (!is_writable($varPath)) { - throw new InvalidArgumentException(sprintf('The specified path "%s" is not writable', $varPath)); - } - - $this->ensureDirectorySeparator($varPath); - $this->varPath = $varPath; - } - - public function train(array $samples, array $targets): void - { - $this->samples = array_merge($this->samples, $samples); - $this->targets = array_merge($this->targets, $targets); - - $trainingSet = DataTransformer::trainingSet($this->samples, $this->targets, in_array($this->type, [Type::EPSILON_SVR, Type::NU_SVR], true)); - file_put_contents($trainingSetFileName = $this->varPath.uniqid('phpml', true), $trainingSet); - $modelFileName = $trainingSetFileName.'-model'; - - $command = $this->buildTrainCommand($trainingSetFileName, $modelFileName); - $output = []; - exec(escapeshellcmd($command).' 2>&1', $output, $return); - - unlink($trainingSetFileName); - - if ($return !== 0) { - throw new LibsvmCommandException( - sprintf('Failed running libsvm command: "%s" with reason: "%s"', $command, array_pop($output)) - ); - } - - $this->model = (string) file_get_contents($modelFileName); - - unlink($modelFileName); - } - - public function getModel(): string - { - return $this->model; - } - - /** - * @return array|string - * - * @throws LibsvmCommandException - */ - public function predict(array $samples) - { - $predictions = $this->runSvmPredict($samples, false); - - if (in_array($this->type, [Type::C_SVC, Type::NU_SVC], true)) { - $predictions = DataTransformer::predictions($predictions, $this->targets); - } else { - $predictions = explode(PHP_EOL, trim($predictions)); - } - - if (!is_array($samples[0])) { - return $predictions[0]; - } - - return $predictions; - } - - /** - * @return array|string - * - * @throws LibsvmCommandException - */ - public function predictProbability(array $samples) - { - if (!$this->probabilityEstimates) { - throw new InvalidOperationException('Model does not support probabiliy estimates'); - } - - $predictions = $this->runSvmPredict($samples, true); - - if (in_array($this->type, [Type::C_SVC, Type::NU_SVC], true)) { - $predictions = DataTransformer::probabilities($predictions, $this->targets); - } else { - $predictions = explode(PHP_EOL, trim($predictions)); - } - - if (!is_array($samples[0])) { - return $predictions[0]; - } - - return $predictions; - } - - private function runSvmPredict(array $samples, bool $probabilityEstimates): string - { - $testSet = DataTransformer::testSet($samples); - file_put_contents($testSetFileName = $this->varPath.uniqid('phpml', true), $testSet); - file_put_contents($modelFileName = $testSetFileName.'-model', $this->model); - $outputFileName = $testSetFileName.'-output'; - - $command = $this->buildPredictCommand( - $testSetFileName, - $modelFileName, - $outputFileName, - $probabilityEstimates - ); - $output = []; - exec(escapeshellcmd($command).' 2>&1', $output, $return); - - unlink($testSetFileName); - unlink($modelFileName); - $predictions = (string) file_get_contents($outputFileName); - - unlink($outputFileName); - - if ($return !== 0) { - throw new LibsvmCommandException( - sprintf('Failed running libsvm command: "%s" with reason: "%s"', $command, array_pop($output)) - ); - } - - return $predictions; - } - - private function getOSExtension(): string - { - $os = strtoupper(substr(PHP_OS, 0, 3)); - if ($os === 'WIN') { - return '.exe'; - } elseif ($os === 'DAR') { - return '-osx'; - } - - return ''; - } - - private function buildTrainCommand(string $trainingSetFileName, string $modelFileName): string - { - return sprintf( - '%ssvm-train%s -s %s -t %s -c %s -n %F -d %s%s -r %s -p %F -m %F -e %F -h %d -b %d %s %s', - $this->binPath, - $this->getOSExtension(), - $this->type, - $this->kernel, - $this->cost, - $this->nu, - $this->degree, - $this->gamma !== null ? ' -g '.$this->gamma : '', - $this->coef0, - $this->epsilon, - $this->cacheSize, - $this->tolerance, - $this->shrinking, - $this->probabilityEstimates, - escapeshellarg($trainingSetFileName), - escapeshellarg($modelFileName) - ); - } - - private function buildPredictCommand( - string $testSetFileName, - string $modelFileName, - string $outputFileName, - bool $probabilityEstimates - ): string { - return sprintf( - '%ssvm-predict%s -b %d %s %s %s', - $this->binPath, - $this->getOSExtension(), - $probabilityEstimates ? 1 : 0, - escapeshellarg($testSetFileName), - escapeshellarg($modelFileName), - escapeshellarg($outputFileName) - ); - } - - private function ensureDirectorySeparator(string &$path): void - { - if (substr($path, -1) !== DIRECTORY_SEPARATOR) { - $path .= DIRECTORY_SEPARATOR; - } - } - - private function verifyBinPath(string $path): void - { - if (!is_dir($path)) { - throw new InvalidArgumentException(sprintf('The specified path "%s" does not exist', $path)); - } - - $osExtension = $this->getOSExtension(); - foreach (['svm-predict', 'svm-scale', 'svm-train'] as $filename) { - $filePath = $path.$filename.$osExtension; - if (!file_exists($filePath)) { - throw new InvalidArgumentException(sprintf('File "%s" not found', $filePath)); - } - - if (!is_executable($filePath)) { - throw new InvalidArgumentException(sprintf('File "%s" is not executable', $filePath)); - } - } - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/SupportVectorMachine/Type.php b/lib/mlbackend/php/phpml/src/Phpml/SupportVectorMachine/Type.php deleted file mode 100644 index 1dea9df0167..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/SupportVectorMachine/Type.php +++ /dev/null @@ -1,33 +0,0 @@ - $maxGram) { - throw new InvalidArgumentException(sprintf('Invalid (%s, %s) minGram and maxGram value combination', $minGram, $maxGram)); - } - - $this->minGram = $minGram; - $this->maxGram = $maxGram; - } - - /** - * {@inheritdoc} - */ - public function tokenize(string $text): array - { - $words = []; - preg_match_all('/\w\w+/u', $text, $words); - - $nGrams = []; - foreach ($words[0] as $word) { - $this->generateNGrams($word, $nGrams); - } - - return $nGrams; - } - - private function generateNGrams(string $word, array &$nGrams): void - { - $length = mb_strlen($word); - - for ($j = 1; $j <= $this->maxGram; $j++) { - for ($k = 0; $k < $length - $j + 1; $k++) { - if ($j >= $this->minGram) { - $nGrams[] = mb_substr($word, $k, $j); - } - } - } - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/Tokenization/NGramWordTokenizer.php b/lib/mlbackend/php/phpml/src/Phpml/Tokenization/NGramWordTokenizer.php deleted file mode 100644 index 20ee28cbf71..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/Tokenization/NGramWordTokenizer.php +++ /dev/null @@ -1,64 +0,0 @@ - $maxGram) { - throw new InvalidArgumentException(sprintf('Invalid (%s, %s) minGram and maxGram value combination', $minGram, $maxGram)); - } - - $this->minGram = $minGram; - $this->maxGram = $maxGram; - } - - /** - * {@inheritdoc} - */ - public function tokenize(string $text): array - { - preg_match_all('/\w\w+/u', $text, $words); - - $words = $words[0]; - - $nGrams = []; - for ($j = $this->minGram; $j <= $this->maxGram; $j++) { - $nGrams = array_merge($nGrams, $this->getNgrams($words, $j)); - } - - return $nGrams; - } - - private function getNgrams(array $match, int $n = 2): array - { - $ngrams = []; - $len = count($match); - for ($i = 0; $i < $len; $i++) { - if ($i > ($n - 2)) { - $ng = ''; - for ($j = $n - 1; $j >= 0; $j--) { - $ng .= ' '.$match[$i - $j]; - } - $ngrams[] = trim($ng); - } - } - - return $ngrams; - } -} diff --git a/lib/mlbackend/php/phpml/src/Phpml/Tokenization/Tokenizer.php b/lib/mlbackend/php/phpml/src/Phpml/Tokenization/Tokenizer.php deleted file mode 100644 index f2dffd976d9..00000000000 --- a/lib/mlbackend/php/phpml/src/Phpml/Tokenization/Tokenizer.php +++ /dev/null @@ -1,10 +0,0 @@ - - - - phpml - PHP-ML - Machine learning library used to generate data for the Analytics system. - 0.10.0 - MIT - https://gitlab.com/php-ai/php-ml - - 2016-2020 Arkadiusz Kondas arkadiusz.kondas[at]gmail - - - diff --git a/lib/mlbackend/php/upgrade.txt b/lib/mlbackend/php/upgrade.txt deleted file mode 100644 index 71390ec2abb..00000000000 --- a/lib/mlbackend/php/upgrade.txt +++ /dev/null @@ -1,13 +0,0 @@ -=== 4.5 Onwards === - -This file has been replaced by UPGRADING.md. See MDL-81125 for further information. - -=== -This files describes API changes in the mlbackend_php code, the -information provided here is intended especially for developers. - -=== 3.8 === - -* The phi coefficient (Matthews' correlation coefficient) has been replaced by - the F1 score as the main accuracy metric. Therefore, \mlbackend_php\processor::get_phi - method has been removed. diff --git a/lib/mlbackend/php/version.php b/lib/mlbackend/php/version.php deleted file mode 100644 index b47bd251e32..00000000000 --- a/lib/mlbackend/php/version.php +++ /dev/null @@ -1,29 +0,0 @@ -. - -/** - * Version details. - * - * @package mlbackend_php - * @copyright 2017 David Monllao {@link http://www.davidmonllao.com/} - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -defined('MOODLE_INTERNAL') || die(); - -$plugin->version = 2024121800; // The current plugin version (Date: YYYYMMDDXX). -$plugin->requires = 2024100100; // Requires this Moodle version. -$plugin->component = 'mlbackend_php'; // Full name of the plugin (used for diagnostics).