2016-05-05 21:29:11 +00:00
|
|
|
<?php
|
|
|
|
|
2016-11-20 21:53:17 +00:00
|
|
|
declare(strict_types=1);
|
2016-05-05 21:29:11 +00:00
|
|
|
|
|
|
|
namespace Phpml\SupportVectorMachine;
|
|
|
|
|
2017-09-02 20:44:19 +00:00
|
|
|
use Phpml\Exception\InvalidArgumentException;
|
2018-02-06 19:39:25 +00:00
|
|
|
use Phpml\Exception\InvalidOperationException;
|
2018-01-25 15:12:13 +00:00
|
|
|
use Phpml\Exception\LibsvmCommandException;
|
2017-03-17 10:44:45 +00:00
|
|
|
use Phpml\Helper\Trainable;
|
|
|
|
|
2016-05-05 21:29:11 +00:00
|
|
|
class SupportVectorMachine
|
|
|
|
{
|
2017-04-19 20:28:07 +00:00
|
|
|
use Trainable;
|
2017-05-13 10:58:06 +00:00
|
|
|
|
|
|
|
/**
|
2016-05-05 21:29:11 +00:00
|
|
|
* @var int
|
|
|
|
*/
|
|
|
|
private $type;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var int
|
|
|
|
*/
|
|
|
|
private $kernel;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var float
|
|
|
|
*/
|
|
|
|
private $cost;
|
|
|
|
|
2016-05-07 20:17:12 +00:00
|
|
|
/**
|
|
|
|
* @var float
|
|
|
|
*/
|
|
|
|
private $nu;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var int
|
|
|
|
*/
|
|
|
|
private $degree;
|
|
|
|
|
|
|
|
/**
|
2018-01-06 12:09:33 +00:00
|
|
|
* @var float|null
|
2016-05-07 20:17:12 +00:00
|
|
|
*/
|
|
|
|
private $gamma;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var float
|
|
|
|
*/
|
|
|
|
private $coef0;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var float
|
|
|
|
*/
|
|
|
|
private $epsilon;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var float
|
|
|
|
*/
|
|
|
|
private $tolerance;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var int
|
|
|
|
*/
|
|
|
|
private $cacheSize;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var bool
|
|
|
|
*/
|
|
|
|
private $shrinking;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var bool
|
|
|
|
*/
|
|
|
|
private $probabilityEstimates;
|
|
|
|
|
2016-05-05 21:29:11 +00:00
|
|
|
/**
|
|
|
|
* @var string
|
|
|
|
*/
|
|
|
|
private $binPath;
|
|
|
|
|
|
|
|
/**
|
2016-05-06 20:33:04 +00:00
|
|
|
* @var string
|
2016-05-05 21:29:11 +00:00
|
|
|
*/
|
|
|
|
private $varPath;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var string
|
|
|
|
*/
|
|
|
|
private $model;
|
|
|
|
|
2016-05-06 20:55:41 +00:00
|
|
|
/**
|
|
|
|
* @var array
|
|
|
|
*/
|
2017-03-17 10:44:45 +00:00
|
|
|
private $targets = [];
|
2016-05-06 20:55:41 +00:00
|
|
|
|
2016-05-07 20:17:12 +00:00
|
|
|
public function __construct(
|
2017-07-26 06:24:47 +00:00
|
|
|
int $type,
|
|
|
|
int $kernel,
|
|
|
|
float $cost = 1.0,
|
|
|
|
float $nu = 0.5,
|
|
|
|
int $degree = 3,
|
2017-11-14 20:21:23 +00:00
|
|
|
?float $gamma = null,
|
2017-07-26 06:24:47 +00:00
|
|
|
float $coef0 = 0.0,
|
|
|
|
float $epsilon = 0.1,
|
|
|
|
float $tolerance = 0.001,
|
|
|
|
int $cacheSize = 100,
|
|
|
|
bool $shrinking = true,
|
|
|
|
bool $probabilityEstimates = false
|
2016-05-07 20:17:12 +00:00
|
|
|
) {
|
2016-05-05 21:29:11 +00:00
|
|
|
$this->type = $type;
|
|
|
|
$this->kernel = $kernel;
|
|
|
|
$this->cost = $cost;
|
2016-05-07 20:17:12 +00:00
|
|
|
$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;
|
2016-05-05 21:29:11 +00:00
|
|
|
|
2018-02-10 11:08:58 +00:00
|
|
|
$rootPath = realpath(implode(DIRECTORY_SEPARATOR, [__DIR__, '..', '..'])).DIRECTORY_SEPARATOR;
|
2016-05-05 21:29:11 +00:00
|
|
|
|
|
|
|
$this->binPath = $rootPath.'bin'.DIRECTORY_SEPARATOR.'libsvm'.DIRECTORY_SEPARATOR;
|
|
|
|
$this->varPath = $rootPath.'var'.DIRECTORY_SEPARATOR;
|
|
|
|
}
|
|
|
|
|
2017-11-14 20:21:23 +00:00
|
|
|
public function setBinPath(string $binPath): void
|
2017-05-13 10:58:06 +00:00
|
|
|
{
|
2017-09-02 20:44:19 +00:00
|
|
|
$this->ensureDirectorySeparator($binPath);
|
|
|
|
$this->verifyBinPath($binPath);
|
2017-05-13 10:58:06 +00:00
|
|
|
|
2017-09-02 20:44:19 +00:00
|
|
|
$this->binPath = $binPath;
|
2017-05-13 10:58:06 +00:00
|
|
|
}
|
|
|
|
|
2017-11-14 20:21:23 +00:00
|
|
|
public function setVarPath(string $varPath): void
|
2017-05-13 10:58:06 +00:00
|
|
|
{
|
2017-09-02 20:44:19 +00:00
|
|
|
if (!is_writable($varPath)) {
|
2018-03-03 15:03:53 +00:00
|
|
|
throw new InvalidArgumentException(sprintf('The specified path "%s" is not writable', $varPath));
|
2017-09-02 20:44:19 +00:00
|
|
|
}
|
2017-05-13 10:58:06 +00:00
|
|
|
|
2017-09-02 20:44:19 +00:00
|
|
|
$this->ensureDirectorySeparator($varPath);
|
|
|
|
$this->varPath = $varPath;
|
2017-05-13 10:58:06 +00:00
|
|
|
}
|
|
|
|
|
2017-11-14 20:21:23 +00:00
|
|
|
public function train(array $samples, array $targets): void
|
2016-05-05 21:29:11 +00:00
|
|
|
{
|
2017-03-17 10:44:45 +00:00
|
|
|
$this->samples = array_merge($this->samples, $samples);
|
|
|
|
$this->targets = array_merge($this->targets, $targets);
|
|
|
|
|
2018-02-16 06:25:24 +00:00
|
|
|
$trainingSet = DataTransformer::trainingSet($this->samples, $this->targets, in_array($this->type, [Type::EPSILON_SVR, Type::NU_SVR], true));
|
2016-12-06 07:50:18 +00:00
|
|
|
file_put_contents($trainingSetFileName = $this->varPath.uniqid('phpml', true), $trainingSet);
|
2016-05-05 21:29:11 +00:00
|
|
|
$modelFileName = $trainingSetFileName.'-model';
|
|
|
|
|
2016-05-07 20:17:12 +00:00
|
|
|
$command = $this->buildTrainCommand($trainingSetFileName, $modelFileName);
|
2018-01-26 21:07:22 +00:00
|
|
|
$output = [];
|
|
|
|
exec(escapeshellcmd($command).' 2>&1', $output, $return);
|
|
|
|
|
|
|
|
unlink($trainingSetFileName);
|
2018-01-25 15:12:13 +00:00
|
|
|
|
|
|
|
if ($return !== 0) {
|
2018-03-03 15:03:53 +00:00
|
|
|
throw new LibsvmCommandException(
|
|
|
|
sprintf('Failed running libsvm command: "%s" with reason: "%s"', $command, array_pop($output))
|
|
|
|
);
|
2018-01-25 15:12:13 +00:00
|
|
|
}
|
2016-05-05 21:29:11 +00:00
|
|
|
|
2018-10-28 06:44:52 +00:00
|
|
|
$this->model = (string) file_get_contents($modelFileName);
|
2016-05-05 21:29:11 +00:00
|
|
|
|
|
|
|
unlink($modelFileName);
|
|
|
|
}
|
|
|
|
|
2017-11-06 07:56:37 +00:00
|
|
|
public function getModel(): string
|
2016-05-05 21:29:11 +00:00
|
|
|
{
|
|
|
|
return $this->model;
|
|
|
|
}
|
2016-05-06 20:33:04 +00:00
|
|
|
|
2016-05-06 20:55:41 +00:00
|
|
|
/**
|
2017-11-22 21:16:10 +00:00
|
|
|
* @return array|string
|
2018-01-25 15:12:13 +00:00
|
|
|
*
|
|
|
|
* @throws LibsvmCommandException
|
2016-05-06 20:55:41 +00:00
|
|
|
*/
|
2016-05-06 20:33:04 +00:00
|
|
|
public function predict(array $samples)
|
2018-02-06 19:39:25 +00:00
|
|
|
{
|
|
|
|
$predictions = $this->runSvmPredict($samples, false);
|
|
|
|
|
2018-02-16 06:25:24 +00:00
|
|
|
if (in_array($this->type, [Type::C_SVC, Type::NU_SVC], true)) {
|
2018-02-06 19:39:25 +00:00
|
|
|
$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);
|
|
|
|
|
2018-02-16 06:25:24 +00:00
|
|
|
if (in_array($this->type, [Type::C_SVC, Type::NU_SVC], true)) {
|
2018-02-06 19:39:25 +00:00
|
|
|
$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
|
2016-05-06 20:33:04 +00:00
|
|
|
{
|
2016-05-06 20:55:41 +00:00
|
|
|
$testSet = DataTransformer::testSet($samples);
|
2016-12-06 07:50:18 +00:00
|
|
|
file_put_contents($testSetFileName = $this->varPath.uniqid('phpml', true), $testSet);
|
2016-05-07 20:17:12 +00:00
|
|
|
file_put_contents($modelFileName = $testSetFileName.'-model', $this->model);
|
2016-05-06 20:55:41 +00:00
|
|
|
$outputFileName = $testSetFileName.'-output';
|
|
|
|
|
2018-02-06 19:39:25 +00:00
|
|
|
$command = $this->buildPredictCommand(
|
|
|
|
$testSetFileName,
|
|
|
|
$modelFileName,
|
|
|
|
$outputFileName,
|
|
|
|
$probabilityEstimates
|
|
|
|
);
|
2018-01-26 21:07:22 +00:00
|
|
|
$output = [];
|
|
|
|
exec(escapeshellcmd($command).' 2>&1', $output, $return);
|
2016-05-06 20:55:41 +00:00
|
|
|
|
|
|
|
unlink($testSetFileName);
|
|
|
|
unlink($modelFileName);
|
2018-10-28 06:44:52 +00:00
|
|
|
$predictions = (string) file_get_contents($outputFileName);
|
2018-01-26 21:07:22 +00:00
|
|
|
|
2016-05-06 20:55:41 +00:00
|
|
|
unlink($outputFileName);
|
|
|
|
|
2018-01-26 21:07:22 +00:00
|
|
|
if ($return !== 0) {
|
2018-03-03 15:03:53 +00:00
|
|
|
throw new LibsvmCommandException(
|
|
|
|
sprintf('Failed running libsvm command: "%s" with reason: "%s"', $command, array_pop($output))
|
|
|
|
);
|
2018-01-26 21:07:22 +00:00
|
|
|
}
|
|
|
|
|
2016-05-07 20:17:12 +00:00
|
|
|
return $predictions;
|
2016-05-06 20:33:04 +00:00
|
|
|
}
|
2016-05-07 09:22:37 +00:00
|
|
|
|
2017-11-06 07:56:37 +00:00
|
|
|
private function getOSExtension(): string
|
2016-05-07 09:22:37 +00:00
|
|
|
{
|
2016-07-01 20:25:57 +00:00
|
|
|
$os = strtoupper(substr(PHP_OS, 0, 3));
|
|
|
|
if ($os === 'WIN') {
|
2016-05-07 09:22:37 +00:00
|
|
|
return '.exe';
|
2016-07-01 20:25:57 +00:00
|
|
|
} elseif ($os === 'DAR') {
|
|
|
|
return '-osx';
|
2016-05-07 09:22:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return '';
|
|
|
|
}
|
2016-05-07 20:17:12 +00:00
|
|
|
|
|
|
|
private function buildTrainCommand(string $trainingSetFileName, string $modelFileName): string
|
|
|
|
{
|
2017-07-26 06:24:47 +00:00
|
|
|
return sprintf(
|
2018-07-04 21:42:22 +00:00
|
|
|
'%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',
|
2016-05-07 20:17:12 +00:00
|
|
|
$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,
|
2016-07-04 20:22:22 +00:00
|
|
|
escapeshellarg($trainingSetFileName),
|
|
|
|
escapeshellarg($modelFileName)
|
2016-05-07 20:17:12 +00:00
|
|
|
);
|
|
|
|
}
|
2017-09-02 20:44:19 +00:00
|
|
|
|
2018-02-06 19:39:25 +00:00
|
|
|
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)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2017-11-14 20:21:23 +00:00
|
|
|
private function ensureDirectorySeparator(string &$path): void
|
2017-09-02 20:44:19 +00:00
|
|
|
{
|
|
|
|
if (substr($path, -1) !== DIRECTORY_SEPARATOR) {
|
|
|
|
$path .= DIRECTORY_SEPARATOR;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-11-14 20:21:23 +00:00
|
|
|
private function verifyBinPath(string $path): void
|
2017-09-02 20:44:19 +00:00
|
|
|
{
|
|
|
|
if (!is_dir($path)) {
|
2018-03-03 15:03:53 +00:00
|
|
|
throw new InvalidArgumentException(sprintf('The specified path "%s" does not exist', $path));
|
2017-09-02 20:44:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
$osExtension = $this->getOSExtension();
|
|
|
|
foreach (['svm-predict', 'svm-scale', 'svm-train'] as $filename) {
|
|
|
|
$filePath = $path.$filename.$osExtension;
|
|
|
|
if (!file_exists($filePath)) {
|
2018-03-03 15:03:53 +00:00
|
|
|
throw new InvalidArgumentException(sprintf('File "%s" not found', $filePath));
|
2017-09-02 20:44:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (!is_executable($filePath)) {
|
2018-03-03 15:03:53 +00:00
|
|
|
throw new InvalidArgumentException(sprintf('File "%s" is not executable', $filePath));
|
2017-09-02 20:44:19 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2016-05-05 21:29:11 +00:00
|
|
|
}
|