Passed
Push — master ( c0463a...e1854d )
by Arkadiusz
02:54
created

Perceptron::predictSampleBinary()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 8
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 4
nc 1
nop 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Phpml\Classification\Linear;
6
7
use Phpml\Helper\Predictable;
8
use Phpml\Helper\OneVsRest;
9
use Phpml\Helper\Optimizer\StochasticGD;
10
use Phpml\Helper\Optimizer\GD;
11
use Phpml\Classification\Classifier;
12
use Phpml\Preprocessing\Normalizer;
13
use Phpml\IncrementalEstimator;
14
use Phpml\Helper\PartiallyTrainable;
15
16
class Perceptron implements Classifier, IncrementalEstimator
17
{
18
    use Predictable, OneVsRest;
19
20
    /**
21
     * @var \Phpml\Helper\Optimizer\Optimizer
22
     */
23
    protected $optimizer;
24
25
    /**
26
     * @var array
27
     */
28
    protected $labels = [];
29
30
    /**
31
     * @var int
32
     */
33
    protected $featureCount = 0;
34
35
    /**
36
     * @var array
37
     */
38
    protected $weights;
39
40
    /**
41
     * @var float
42
     */
43
    protected $learningRate;
44
45
    /**
46
     * @var int
47
     */
48
    protected $maxIterations;
49
50
    /**
51
     * @var Normalizer
52
     */
53
    protected $normalizer;
54
55
    /**
56
     * @var bool
57
     */
58
    protected $enableEarlyStop = true;
59
60
    /**
61
     * @var array
62
     */
63
    protected $costValues = [];
64
65
    /**
66
     * Initalize a perceptron classifier with given learning rate and maximum
67
     * number of iterations used while training the perceptron <br>
68
     *
69
     * Learning rate should be a float value between 0.0(exclusive) and 1.0(inclusive) <br>
70
     * Maximum number of iterations can be an integer value greater than 0
71
     * @param int $learningRate
72
     * @param int $maxIterations
73
     */
74
    public function __construct(float $learningRate = 0.001, int $maxIterations = 1000,
75
        bool $normalizeInputs = true)
76
    {
77
        if ($learningRate <= 0.0 || $learningRate > 1.0) {
78
            throw new \Exception("Learning rate should be a float value between 0.0(exclusive) and 1.0(inclusive)");
79
        }
80
81
        if ($maxIterations <= 0) {
82
            throw new \Exception("Maximum number of iterations should be an integer greater than 0");
83
        }
84
85
        if ($normalizeInputs) {
86
            $this->normalizer = new Normalizer(Normalizer::NORM_STD);
87
        }
88
89
        $this->learningRate = $learningRate;
90
        $this->maxIterations = $maxIterations;
91
    }
92
93
    /**
94
     * @param array $samples
95
     * @param array $targets
96
     * @param array $labels
97
     */
98
    public function partialTrain(array $samples, array $targets, array $labels = array())
99
    {
100
        return $this->trainByLabel($samples, $targets, $labels);
101
    }
102
103
   /**
104
     * @param array $samples
105
     * @param array $targets
106
     * @param array $labels
107
     */
108
    public function trainBinary(array $samples, array $targets, array $labels)
109
    {
110
111
        if ($this->normalizer) {
112
            $this->normalizer->transform($samples);
113
        }
114
115
        // Set all target values to either -1 or 1
116
        $this->labels = [1 => $labels[0], -1 => $labels[1]];
117
        foreach ($targets as $key => $target) {
118
            $targets[$key] = strval($target) == strval($this->labels[1]) ? 1 : -1;
119
        }
120
121
        // Set samples and feature count vars
122
        $this->featureCount = count($samples[0]);
123
124
        $this->runTraining($samples, $targets);
125
    }
126
127
    protected function resetBinary()
128
    {
129
        $this->labels = [];
130
        $this->optimizer = null;
131
        $this->featureCount = 0;
132
        $this->weights = null;
0 ignored issues
show
Documentation Bug introduced by
It seems like null of type null is incompatible with the declared type array of property $weights.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
133
        $this->costValues = [];
134
    }
135
136
    /**
137
     * Normally enabling early stopping for the optimization procedure may
138
     * help saving processing time while in some cases it may result in
139
     * premature convergence.<br>
140
     *
141
     * If "false" is given, the optimization procedure will always be executed
142
     * for $maxIterations times
143
     *
144
     * @param bool $enable
145
     */
146
    public function setEarlyStop(bool $enable = true)
147
    {
148
        $this->enableEarlyStop = $enable;
149
150
        return $this;
151
    }
152
153
    /**
154
     * Returns the cost values obtained during the training.
155
     *
156
     * @return array
157
     */
158
    public function getCostValues()
159
    {
160
        return $this->costValues;
161
    }
162
163
    /**
164
     * Trains the perceptron model with Stochastic Gradient Descent optimization
165
     * to get the correct set of weights
166
     *
167
     * @param array $samples
168
     * @param array $targets
169
     */
170
    protected function runTraining(array $samples, array $targets)
171
    {
172
        // The cost function is the sum of squares
173
        $callback = function ($weights, $sample, $target) {
174
            $this->weights = $weights;
175
176
            $prediction = $this->outputClass($sample);
177
            $gradient = $prediction - $target;
178
            $error = $gradient**2;
179
180
            return [$error, $gradient];
181
        };
182
183
        $this->runGradientDescent($samples, $targets, $callback);
184
    }
185
186
    /**
187
     * Executes a Gradient Descent algorithm for
188
     * the given cost function
189
     *
190
     * @param array $samples
191
     * @param array $targets
192
     */
193
    protected function runGradientDescent(array $samples, array $targets, \Closure $gradientFunc, bool $isBatch = false)
194
    {
195
        $class = $isBatch ? GD::class :  StochasticGD::class;
196
197
        if (empty($this->optimizer)) {
198
            $this->optimizer = (new $class($this->featureCount))
199
                ->setLearningRate($this->learningRate)
200
                ->setMaxIterations($this->maxIterations)
201
                ->setChangeThreshold(1e-6)
202
                ->setEarlyStop($this->enableEarlyStop);
203
        }
204
205
        $this->weights = $this->optimizer->runOptimization($samples, $targets, $gradientFunc);
206
        $this->costValues = $this->optimizer->getCostValues();
207
    }
208
209
    /**
210
     * Checks if the sample should be normalized and if so, returns the
211
     * normalized sample
212
     *
213
     * @param array $sample
214
     *
215
     * @return array
216
     */
217
    protected function checkNormalizedSample(array $sample)
218
    {
219
        if ($this->normalizer) {
220
            $samples = [$sample];
221
            $this->normalizer->transform($samples);
222
            $sample = $samples[0];
223
        }
224
225
        return $sample;
226
    }
227
228
    /**
229
     * Calculates net output of the network as a float value for the given input
230
     *
231
     * @param array $sample
232
     * @return int
233
     */
234
    protected function output(array $sample)
235
    {
236
        $sum = 0;
237
        foreach ($this->weights as $index => $w) {
238
            if ($index == 0) {
239
                $sum += $w;
240
            } else {
241
                $sum += $w * $sample[$index - 1];
242
            }
243
        }
244
245
        return $sum;
246
    }
247
248
    /**
249
     * Returns the class value (either -1 or 1) for the given input
250
     *
251
     * @param array $sample
252
     * @return int
253
     */
254
    protected function outputClass(array $sample)
255
    {
256
        return $this->output($sample) > 0 ? 1 : -1;
257
    }
258
259
    /**
260
     * Returns the probability of the sample of belonging to the given label.
261
     *
262
     * The probability is simply taken as the distance of the sample
263
     * to the decision plane.
264
     *
265
     * @param array $sample
266
     * @param mixed $label
267
     */
268 View Code Duplication
    protected function predictProbability(array $sample, $label)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
269
    {
270
        $predicted = $this->predictSampleBinary($sample);
271
272
        if (strval($predicted) == strval($label)) {
273
            $sample = $this->checkNormalizedSample($sample);
274
            return abs($this->output($sample));
275
        }
276
277
        return 0.0;
278
    }
279
280
    /**
281
     * @param array $sample
282
     * @return mixed
283
     */
284
    protected function predictSampleBinary(array $sample)
285
    {
286
        $sample = $this->checkNormalizedSample($sample);
287
288
        $predictedClass = $this->outputClass($sample);
289
290
        return $this->labels[ $predictedClass ];
291
    }
292
}
293