php-ml/src/SupportVectorMachine/SupportVectorMachine.php

331 lines
8.2 KiB
PHP

<?php
declare(strict_types=1);
namespace Phpml\SupportVectorMachine;
use Phpml\Exception\InvalidArgumentException;
use Phpml\Exception\InvalidOperationException;
use Phpml\Exception\LibsvmCommandException;
use Phpml\Helper\Trainable;
class SupportVectorMachine
{
use Trainable;
/**
* @var int
*/
private $type;
/**
* @var int
*/
private $kernel;
/**
* @var float
*/
private $cost;
/**
* @var float
*/
private $nu;
/**
* @var int
*/
private $degree;
/**
* @var float|null
*/
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;
/**
* @var string
*/
private $binPath;
/**
* @var string
*/
private $varPath;
/**
* @var string
*/
private $model;
/**
* @var array
*/
private $targets = [];
public function __construct(
int $type,
int $kernel,
float $cost = 1.0,
float $nu = 0.5,
int $degree = 3,
?float $gamma = null,
float $coef0 = 0.0,
float $epsilon = 0.1,
float $tolerance = 0.001,
int $cacheSize = 100,
bool $shrinking = true,
bool $probabilityEstimates = false
) {
$this->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 InvalidArgumentException::pathNotWritable($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 LibsvmCommandException::failedToRun($command, array_pop($output));
}
$this->model = 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 = file_get_contents($outputFileName);
unlink($outputFileName);
if ($return !== 0) {
throw LibsvmCommandException::failedToRun($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 %s -d %s%s -r %s -p %s -m %s -e %s -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 InvalidArgumentException::pathNotFound($path);
}
$osExtension = $this->getOSExtension();
foreach (['svm-predict', 'svm-scale', 'svm-train'] as $filename) {
$filePath = $path.$filename.$osExtension;
if (!file_exists($filePath)) {
throw InvalidArgumentException::fileNotFound($filePath);
}
if (!is_executable($filePath)) {
throw InvalidArgumentException::fileNotExecutable($filePath);
}
}
}
}