Passed
Pull Request — master (#78)
by
unknown
04:36
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\PartialTrainer;
14
use Phpml\Helper\PartiallyTrainable;
15
16
class Perceptron implements Classifier, PartialTrainer
17
{
18
    use Predictable, PartiallyTrainable, OneVsRest {
19
        reset as public resetOneVsRest;
20
        PartiallyTrainable::train insteadof OneVsRest;
21
    }
22
23
    /**
24
     * @var array
25
     */
26
    protected $labels = [];
27
28
    /**
29
     * @var \Phpml\Helper\Optimizer\Optimizer
30
     */
31
    protected $optimizer;
32
33
    /**
34
     * @var int
35
     */
36
    protected $featureCount = 0;
37
38
    /**
39
     * @var array
40
     */
41
    protected $weights;
42
43
    /**
44
     * @var float
45
     */
46
    protected $learningRate;
47
48
    /**
49
     * @var int
50
     */
51
    protected $maxIterations;
52
53
    /**
54
     * @var Normalizer
55
     */
56
    protected $normalizer;
57
58
    /**
59
     * @var bool
60
     */
61
    protected $enableEarlyStop = true;
62
63
    /**
64
     * @var array
65
     */
66
    protected $costValues = [];
67
68
    /**
69
     * Initalize a perceptron classifier with given learning rate and maximum
70
     * number of iterations used while training the perceptron <br>
71
     *
72
     * Learning rate should be a float value between 0.0(exclusive) and 1.0(inclusive) <br>
73
     * Maximum number of iterations can be an integer value greater than 0
74
     * @param int $learningRate
75
     * @param int $maxIterations
76
     */
77
    public function __construct(float $learningRate = 0.001, int $maxIterations = 1000,
78
        bool $normalizeInputs = true)
79
    {
80
        if ($learningRate <= 0.0 || $learningRate > 1.0) {
81
            throw new \Exception("Learning rate should be a float value between 0.0(exclusive) and 1.0(inclusive)");
82
        }
83
84
        if ($maxIterations <= 0) {
85
            throw new \Exception("Maximum number of iterations should be an integer greater than 0");
86
        }
87
88
        if ($normalizeInputs) {
89
            $this->normalizer = new Normalizer(Normalizer::NORM_STD);
90
        }
91
92
        $this->learningRate = $learningRate;
93
        $this->maxIterations = $maxIterations;
94
    }
95
96
    public function partialTrain(array $samples, array $targets, array $labels = array()) {
97
        return $this->trainByLabel($samples, $targets, $labels);
98
    }
99
100
   /**
101
     * @param array $samples
102
     * @param array $targets
103
     * @param array $labels
104
     */
105
    public function trainBinary(array $samples, array $targets, array $labels)
106
    {
107
108
        if ($this->normalizer) {
109
            $this->normalizer->transform($samples);
110
        }
111
112
        // Set all target values to either -1 or 1
113
        $this->labels = [1 => $labels[0], -1 => $labels[1]];
114
        foreach ($targets as $key => $target) {
115
            $targets[$key] = strval($target) == strval($this->labels[1]) ? 1 : -1;
116
        }
117
118
        // Set samples and feature count vars
119
        $this->featureCount = count($samples[0]);
120
121
        $this->runTraining($samples, $targets);
122
    }
123
124
    public function resetTrainer() {
125
        $this->labels = [];
126
        $this->optimizer = null;
127
        $this->featureCount = 0;
128
        $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...
129
        $this->costValues = [];
130
        $this->resetOneVsRest();
131
    }
132
133
    /**
134
     * Normally enabling early stopping for the optimization procedure may
135
     * help saving processing time while in some cases it may result in
136
     * premature convergence.<br>
137
     *
138
     * If "false" is given, the optimization procedure will always be executed
139
     * for $maxIterations times
140
     *
141
     * @param bool $enable
142
     */
143
    public function setEarlyStop(bool $enable = true)
144
    {
145
        $this->enableEarlyStop = $enable;
146
147
        return $this;
148
    }
149
150
    /**
151
     * Returns the cost values obtained during the training.
152
     *
153
     * @return array
154
     */
155
    public function getCostValues()
156
    {
157
        return $this->costValues;
158
    }
159
160
    /**
161
     * Trains the perceptron model with Stochastic Gradient Descent optimization
162
     * to get the correct set of weights
163
     *
164
     * @param array $samples
165
     * @param array $targets
166
     */
167
    protected function runTraining(array $samples, array $targets)
168
    {
169
        // The cost function is the sum of squares
170
        $callback = function ($weights, $sample, $target) {
171
            $this->weights = $weights;
172
173
            $prediction = $this->outputClass($sample);
174
            $gradient = $prediction - $target;
175
            $error = $gradient**2;
176
177
            return [$error, $gradient];
178
        };
179
180
        $this->runGradientDescent($samples, $targets, $callback);
181
    }
182
183
    /**
184
     * Executes a Gradient Descent algorithm for
185
     * the given cost function
186
     *
187
     * @param array $samples
188
     * @param array $targets
189
     */
190
    protected function runGradientDescent(array $samples, array $targets, \Closure $gradientFunc, bool $isBatch = false)
191
    {
192
        $class = $isBatch ? GD::class :  StochasticGD::class;
193
194
        if (empty($this->optimizer)) {
195
            $this->optimizer = (new $class($this->featureCount))
196
                ->setLearningRate($this->learningRate)
197
                ->setMaxIterations($this->maxIterations)
198
                ->setChangeThreshold(1e-6)
199
                ->setEarlyStop($this->enableEarlyStop);
200
        }
201
202
        $this->weights = $this->optimizer->runOptimization($samples, $targets, $gradientFunc);
203
        $this->costValues = $this->optimizer->getCostValues();
204
    }
205
206
    /**
207
     * Checks if the sample should be normalized and if so, returns the
208
     * normalized sample
209
     *
210
     * @param array $sample
211
     *
212
     * @return array
213
     */
214
    protected function checkNormalizedSample(array $sample)
215
    {
216
        if ($this->normalizer) {
217
            $samples = [$sample];
218
            $this->normalizer->transform($samples);
219
            $sample = $samples[0];
220
        }
221
222
        return $sample;
223
    }
224
225
    /**
226
     * Calculates net output of the network as a float value for the given input
227
     *
228
     * @param array $sample
229
     * @return int
230
     */
231
    protected function output(array $sample)
232
    {
233
        $sum = 0;
234
        foreach ($this->weights as $index => $w) {
235
            if ($index == 0) {
236
                $sum += $w;
237
            } else {
238
                $sum += $w * $sample[$index - 1];
239
            }
240
        }
241
242
        return $sum;
243
    }
244
245
    /**
246
     * Returns the class value (either -1 or 1) for the given input
247
     *
248
     * @param array $sample
249
     * @return int
250
     */
251
    protected function outputClass(array $sample)
252
    {
253
        return $this->output($sample) > 0 ? 1 : -1;
254
    }
255
256
    /**
257
     * Returns the probability of the sample of belonging to the given label.
258
     *
259
     * The probability is simply taken as the distance of the sample
260
     * to the decision plane.
261
     *
262
     * @param array $sample
263
     * @param mixed $label
264
     */
265 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...
266
    {
267
        $predicted = $this->predictSampleBinary($sample);
268
269
        if (strval($predicted) == strval($label)) {
270
            $sample = $this->checkNormalizedSample($sample);
271
            return abs($this->output($sample));
272
        }
273
274
        return 0.0;
275
    }
276
277
    /**
278
     * @param array $sample
279
     * @return mixed
280
     */
281
    protected function predictSampleBinary(array $sample)
282
    {
283
        $sample = $this->checkNormalizedSample($sample);
284
285
        $predictedClass = $this->outputClass($sample);
286
287
        return $this->labels[ $predictedClass ];
288
    }
289
}
290