Passed
Push — master ( ed775f...ec091b )
by Arkadiusz
02:43
created

SupportVectorMachine::predictProbability()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 20
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 20
rs 9.2
cc 4
eloc 11
nc 5
nop 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Phpml\SupportVectorMachine;
6
7
use Phpml\Exception\InvalidArgumentException;
8
use Phpml\Exception\InvalidOperationException;
9
use Phpml\Exception\LibsvmCommandException;
10
use Phpml\Helper\Trainable;
11
12
class SupportVectorMachine
13
{
14
    use Trainable;
15
16
    /**
17
     * @var int
18
     */
19
    private $type;
20
21
    /**
22
     * @var int
23
     */
24
    private $kernel;
25
26
    /**
27
     * @var float
28
     */
29
    private $cost;
30
31
    /**
32
     * @var float
33
     */
34
    private $nu;
35
36
    /**
37
     * @var int
38
     */
39
    private $degree;
40
41
    /**
42
     * @var float|null
43
     */
44
    private $gamma;
45
46
    /**
47
     * @var float
48
     */
49
    private $coef0;
50
51
    /**
52
     * @var float
53
     */
54
    private $epsilon;
55
56
    /**
57
     * @var float
58
     */
59
    private $tolerance;
60
61
    /**
62
     * @var int
63
     */
64
    private $cacheSize;
65
66
    /**
67
     * @var bool
68
     */
69
    private $shrinking;
70
71
    /**
72
     * @var bool
73
     */
74
    private $probabilityEstimates;
75
76
    /**
77
     * @var string
78
     */
79
    private $binPath;
80
81
    /**
82
     * @var string
83
     */
84
    private $varPath;
85
86
    /**
87
     * @var string
88
     */
89
    private $model;
90
91
    /**
92
     * @var array
93
     */
94
    private $targets = [];
95
96
    public function __construct(
97
        int $type,
98
        int $kernel,
99
        float $cost = 1.0,
100
        float $nu = 0.5,
101
        int $degree = 3,
102
        ?float $gamma = null,
103
        float $coef0 = 0.0,
104
        float $epsilon = 0.1,
105
        float $tolerance = 0.001,
106
        int $cacheSize = 100,
107
        bool $shrinking = true,
108
        bool $probabilityEstimates = false
109
    ) {
110
        $this->type = $type;
111
        $this->kernel = $kernel;
112
        $this->cost = $cost;
113
        $this->nu = $nu;
114
        $this->degree = $degree;
115
        $this->gamma = $gamma;
116
        $this->coef0 = $coef0;
117
        $this->epsilon = $epsilon;
118
        $this->tolerance = $tolerance;
119
        $this->cacheSize = $cacheSize;
120
        $this->shrinking = $shrinking;
121
        $this->probabilityEstimates = $probabilityEstimates;
122
123
        $rootPath = realpath(implode(DIRECTORY_SEPARATOR, [__DIR__, '..', '..', '..'])).DIRECTORY_SEPARATOR;
124
125
        $this->binPath = $rootPath.'bin'.DIRECTORY_SEPARATOR.'libsvm'.DIRECTORY_SEPARATOR;
126
        $this->varPath = $rootPath.'var'.DIRECTORY_SEPARATOR;
127
    }
128
129
    public function setBinPath(string $binPath): void
130
    {
131
        $this->ensureDirectorySeparator($binPath);
132
        $this->verifyBinPath($binPath);
133
134
        $this->binPath = $binPath;
135
    }
136
137
    public function setVarPath(string $varPath): void
138
    {
139
        if (!is_writable($varPath)) {
140
            throw InvalidArgumentException::pathNotWritable($varPath);
141
        }
142
143
        $this->ensureDirectorySeparator($varPath);
144
        $this->varPath = $varPath;
145
    }
146
147
    public function train(array $samples, array $targets): void
148
    {
149
        $this->samples = array_merge($this->samples, $samples);
150
        $this->targets = array_merge($this->targets, $targets);
151
152
        $trainingSet = DataTransformer::trainingSet($this->samples, $this->targets, in_array($this->type, [Type::EPSILON_SVR, Type::NU_SVR]));
153
        file_put_contents($trainingSetFileName = $this->varPath.uniqid('phpml', true), $trainingSet);
154
        $modelFileName = $trainingSetFileName.'-model';
155
156
        $command = $this->buildTrainCommand($trainingSetFileName, $modelFileName);
157
        $output = [];
158
        exec(escapeshellcmd($command).' 2>&1', $output, $return);
159
160
        unlink($trainingSetFileName);
161
162
        if ($return !== 0) {
163
            throw LibsvmCommandException::failedToRun($command, array_pop($output));
0 ignored issues
show
Documentation introduced by
array_pop($output) is of type array|null, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
164
        }
165
166
        $this->model = file_get_contents($modelFileName);
167
168
        unlink($modelFileName);
169
    }
170
171
    public function getModel(): string
172
    {
173
        return $this->model;
174
    }
175
176
    /**
177
     * @return array|string
178
     *
179
     * @throws LibsvmCommandException
180
     */
181
    public function predict(array $samples)
182
    {
183
        $predictions = $this->runSvmPredict($samples, false);
184
185
        if (in_array($this->type, [Type::C_SVC, Type::NU_SVC])) {
186
            $predictions = DataTransformer::predictions($predictions, $this->targets);
187
        } else {
188
            $predictions = explode(PHP_EOL, trim($predictions));
189
        }
190
191
        if (!is_array($samples[0])) {
192
            return $predictions[0];
193
        }
194
195
        return $predictions;
196
    }
197
198
    /**
199
     * @return array|string
200
     *
201
     * @throws LibsvmCommandException
202
     */
203
    public function predictProbability(array $samples)
204
    {
205
        if (!$this->probabilityEstimates) {
206
            throw new InvalidOperationException('Model does not support probabiliy estimates');
207
        }
208
209
        $predictions = $this->runSvmPredict($samples, true);
210
211
        if (in_array($this->type, [Type::C_SVC, Type::NU_SVC])) {
212
            $predictions = DataTransformer::probabilities($predictions, $this->targets);
213
        } else {
214
            $predictions = explode(PHP_EOL, trim($predictions));
215
        }
216
217
        if (!is_array($samples[0])) {
218
            return $predictions[0];
219
        }
220
221
        return $predictions;
222
    }
223
224
    private function runSvmPredict(array $samples, bool $probabilityEstimates): string
225
    {
226
        $testSet = DataTransformer::testSet($samples);
227
        file_put_contents($testSetFileName = $this->varPath.uniqid('phpml', true), $testSet);
228
        file_put_contents($modelFileName = $testSetFileName.'-model', $this->model);
229
        $outputFileName = $testSetFileName.'-output';
230
231
        $command = $this->buildPredictCommand(
232
            $testSetFileName,
233
            $modelFileName,
234
            $outputFileName,
235
            $probabilityEstimates
236
        );
237
        $output = [];
238
        exec(escapeshellcmd($command).' 2>&1', $output, $return);
239
240
        unlink($testSetFileName);
241
        unlink($modelFileName);
242
        $predictions = file_get_contents($outputFileName);
243
244
        unlink($outputFileName);
245
246
        if ($return !== 0) {
247
            throw LibsvmCommandException::failedToRun($command, array_pop($output));
0 ignored issues
show
Documentation introduced by
array_pop($output) is of type array|null, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
248
        }
249
250
        return $predictions;
251
    }
252
253
    private function getOSExtension(): string
254
    {
255
        $os = strtoupper(substr(PHP_OS, 0, 3));
256
        if ($os === 'WIN') {
257
            return '.exe';
258
        } elseif ($os === 'DAR') {
259
            return '-osx';
260
        }
261
262
        return '';
263
    }
264
265
    private function buildTrainCommand(string $trainingSetFileName, string $modelFileName): string
266
    {
267
        return sprintf(
268
            '%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',
269
            $this->binPath,
270
            $this->getOSExtension(),
271
            $this->type,
272
            $this->kernel,
273
            $this->cost,
274
            $this->nu,
275
            $this->degree,
276
            $this->gamma !== null ? ' -g '.$this->gamma : '',
277
            $this->coef0,
278
            $this->epsilon,
279
            $this->cacheSize,
280
            $this->tolerance,
281
            $this->shrinking,
282
            $this->probabilityEstimates,
283
            escapeshellarg($trainingSetFileName),
284
            escapeshellarg($modelFileName)
285
        );
286
    }
287
288
    private function buildPredictCommand(
289
        string $testSetFileName,
290
        string $modelFileName,
291
        string $outputFileName,
292
        bool $probabilityEstimates
293
    ): string {
294
        return sprintf(
295
            '%ssvm-predict%s -b %d %s %s %s',
296
            $this->binPath,
297
            $this->getOSExtension(),
298
            $probabilityEstimates ? 1 : 0,
299
            escapeshellarg($testSetFileName),
300
            escapeshellarg($modelFileName),
301
            escapeshellarg($outputFileName)
302
        );
303
    }
304
305
    private function ensureDirectorySeparator(string &$path): void
306
    {
307
        if (substr($path, -1) !== DIRECTORY_SEPARATOR) {
308
            $path .= DIRECTORY_SEPARATOR;
309
        }
310
    }
311
312
    private function verifyBinPath(string $path): void
313
    {
314
        if (!is_dir($path)) {
315
            throw InvalidArgumentException::pathNotFound($path);
316
        }
317
318
        $osExtension = $this->getOSExtension();
319
        foreach (['svm-predict', 'svm-scale', 'svm-train'] as $filename) {
320
            $filePath = $path.$filename.$osExtension;
321
            if (!file_exists($filePath)) {
322
                throw InvalidArgumentException::fileNotFound($filePath);
323
            }
324
325
            if (!is_executable($filePath)) {
326
                throw InvalidArgumentException::fileNotExecutable($filePath);
327
            }
328
        }
329
    }
330
}
331