Passed
Push — master ( 47cdff...ed5fc8 )
by Arkadiusz
03:38
created

src/Phpml/Helper/Optimizer/ConjugateGradient.php (9 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Phpml\Helper\Optimizer;
6
7
/**
8
 * Conjugate Gradient method to solve a non-linear f(x) with respect to unknown x
9
 * See https://en.wikipedia.org/wiki/Nonlinear_conjugate_gradient_method)
10
 *
11
 * The method applied below is explained in the below document in a practical manner
12
 *  - http://web.cs.iastate.edu/~cs577/handouts/conjugate-gradient.pdf
13
 *
14
 * However it is compliant with the general Conjugate Gradient method with
15
 * Fletcher-Reeves update method. Note that, the f(x) is assumed to be one-dimensional
16
 * and one gradient is utilized for all dimensions in the given data.
17
 */
18
class ConjugateGradient extends GD
19
{
20
    /**
21
     * @param array    $samples
22
     * @param array    $targets
23
     * @param \Closure $gradientCb
24
     *
25
     * @return array
26
     */
27
    public function runOptimization(array $samples, array $targets, \Closure $gradientCb)
28
    {
29
        $this->samples = $samples;
30
        $this->targets = $targets;
31
        $this->gradientCb = $gradientCb;
32
        $this->sampleCount = count($samples);
33
        $this->costValues = [];
34
35
        $d = mp::muls($this->gradient($this->theta), -1);
36
37
        for ($i = 0; $i < $this->maxIterations; ++$i) {
38
            // Obtain α that minimizes f(θ + α.d)
39
            $alpha = $this->getAlpha(array_sum($d));
40
41
            // θ(k+1) = θ(k) + α.d
42
            $thetaNew = $this->getNewTheta($alpha, $d);
43
44
            // β = ||∇f(x(k+1))||²  ∕  ||∇f(x(k))||²
0 ignored issues
show
Unused Code Comprehensibility introduced by
44% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
45
            $beta = $this->getBeta($thetaNew);
46
47
            // d(k+1) =–∇f(x(k+1)) + β(k).d(k)
0 ignored issues
show
Unused Code Comprehensibility introduced by
40% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
48
            $d = $this->getNewDirection($thetaNew, $beta, $d);
49
50
            // Save values for the next iteration
51
            $oldTheta = $this->theta;
52
            $this->costValues[] = $this->cost($thetaNew);
53
54
            $this->theta = $thetaNew;
55
            if ($this->enableEarlyStop && $this->earlyStop($oldTheta)) {
56
                break;
57
            }
58
        }
59
60
        $this->clear();
61
62
        return $this->theta;
63
    }
64
65
    /**
66
     * Executes the callback function for the problem and returns
67
     * sum of the gradient for all samples & targets.
68
     *
69
     * @param array $theta
70
     *
71
     * @return array
72
     */
73
    protected function gradient(array $theta)
74
    {
75
        list(, $gradient) = parent::gradient($theta);
76
77
        return $gradient;
78
    }
79
80
    /**
81
     * Returns the value of f(x) for given solution
82
     *
83
     * @param array $theta
84
     *
85
     * @return float
86
     */
87
    protected function cost(array $theta)
88
    {
89
        list($cost) = parent::gradient($theta);
0 ignored issues
show
Comprehensibility Bug introduced by
It seems like you call parent on a different method (gradient() instead of cost()). Are you sure this is correct? If so, you might want to change this to $this->gradient().

This check looks for a call to a parent method whose name is different than the method from which it is called.

Consider the following code:

class Daddy
{
    protected function getFirstName()
    {
        return "Eidur";
    }

    protected function getSurName()
    {
        return "Gudjohnsen";
    }
}

class Son
{
    public function getFirstName()
    {
        return parent::getSurname();
    }
}

The getFirstName() method in the Son calls the wrong method in the parent class.

Loading history...
90
91
        return array_sum($cost) / $this->sampleCount;
92
    }
93
94
    /**
95
     * Calculates alpha that minimizes the function f(θ + α.d)
96
     * by performing a line search that does not rely upon the derivation.
97
     *
98
     * There are several alternatives for this function. For now, we
99
     * prefer a method inspired from the bisection method for its simplicity.
100
     * This algorithm attempts to find an optimum alpha value between 0.0001 and 0.01
101
     *
102
     * Algorithm as follows:
103
     *  a) Probe a small alpha  (0.0001) and calculate cost function
104
     *  b) Probe a larger alpha (0.01) and calculate cost function
105
     *		b-1) If cost function decreases, continue enlarging alpha
106
     *		b-2) If cost function increases, take the midpoint and try again
107
     *
108
     * @param float $d
109
     *
110
     * @return float
111
     */
112
    protected function getAlpha(float $d)
113
    {
114
        $small = 0.0001 * $d;
115
        $large = 0.01 * $d;
116
117
        // Obtain θ + α.d for two initial values, x0 and x1
118
        $x0 = mp::adds($this->theta, $small);
119
        $x1 = mp::adds($this->theta, $large);
120
121
        $epsilon = 0.0001;
122
        $iteration = 0;
123
        do {
124
            $fx1 = $this->cost($x1);
125
            $fx0 = $this->cost($x0);
126
127
            // If the difference between two values is small enough
128
            // then break the loop
129
            if (abs($fx1 - $fx0) <= $epsilon) {
130
                break;
131
            }
132
133
            if ($fx1 < $fx0) {
134
                $x0 = $x1;
135
                $x1 = mp::adds($x1, 0.01); // Enlarge second
136
            } else {
137
                $x1 = mp::divs(mp::add($x1, $x0), 2.0);
138
            } // Get to the midpoint
139
140
            $error = $fx1 / $this->dimensions;
141
        } while ($error <= $epsilon || $iteration++ < 10);
142
143
        //  Return α = θ / d
144
        if ($d == 0) {
145
            return $x1[0] - $this->theta[0];
146
        }
147
148
        return ($x1[0] - $this->theta[0]) / $d;
149
    }
150
151
    /**
152
     * Calculates new set of solutions with given alpha (for each θ(k)) and
153
     * gradient direction.
154
     *
155
     * θ(k+1) = θ(k) + α.d
156
     *
157
     * @param float $alpha
158
     * @param array $d
159
     *
160
     * @return array
161
     */
162
    protected function getNewTheta(float $alpha, array $d)
163
    {
164
        $theta = $this->theta;
165
166
        for ($i = 0; $i < $this->dimensions + 1; ++$i) {
167
            if ($i === 0) {
168
                $theta[$i] += $alpha * array_sum($d);
169
            } else {
170
                $sum = 0.0;
171
                foreach ($this->samples as $si => $sample) {
172
                    $sum += $sample[$i - 1] * $d[$si] * $alpha;
173
                }
174
175
                $theta[$i] += $sum;
176
            }
177
        }
178
179
        return $theta;
180
    }
181
182
    /**
183
     * Calculates new beta (β) for given set of solutions by using
184
     * Fletcher–Reeves method.
185
     *
186
     * β = ||f(x(k+1))||²  ∕  ||f(x(k))||²
187
     *
188
     * See:
189
     *  R. Fletcher and C. M. Reeves, "Function minimization by conjugate gradients", Comput. J. 7 (1964), 149–154.
190
     *
191
     * @param array $newTheta
192
     *
193
     * @return float
194
     */
195
    protected function getBeta(array $newTheta)
196
    {
197
        $dNew = array_sum($this->gradient($newTheta));
198
        $dOld = array_sum($this->gradient($this->theta)) + 1e-100;
199
200
        return  $dNew ** 2 / $dOld ** 2;
201
    }
202
203
    /**
204
     * Calculates the new conjugate direction
205
     *
206
     * d(k+1) =–∇f(x(k+1)) + β(k).d(k)
207
     *
208
     * @param array $theta
209
     * @param float $beta
210
     * @param array $d
211
     *
212
     * @return array
213
     */
214
    protected function getNewDirection(array $theta, float $beta, array $d)
215
    {
216
        $grad = $this->gradient($theta);
217
218
        return mp::add(mp::muls($grad, -1), mp::muls($d, $beta));
219
    }
220
}
221
222
/**
223
 * Handles element-wise vector operations between vector-vector
224
 * and vector-scalar variables
225
 */
226
class mp
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class should be in its own file to aid autoloaders.

Having each class in a dedicated file usually plays nice with PSR autoloaders and is therefore a well established practice. If you use other autoloaders, you might not want to follow this rule.

Loading history...
227
{
228
    /**
229
     * Element-wise <b>multiplication</b> of two vectors of the same size
230
     *
231
     * @param array $m1
232
     * @param array $m2
233
     *
234
     * @return array
235
     */
236 View Code Duplication
    public static function mul(array $m1, array $m2)
0 ignored issues
show
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...
237
    {
238
        $res = [];
239
        foreach ($m1 as $i => $val) {
240
            $res[] = $val * $m2[$i];
241
        }
242
243
        return $res;
244
    }
245
246
    /**
247
     * Element-wise <b>division</b> of two vectors of the same size
248
     *
249
     * @param array $m1
250
     * @param array $m2
251
     *
252
     * @return array
253
     */
254 View Code Duplication
    public static function div(array $m1, array $m2)
0 ignored issues
show
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...
255
    {
256
        $res = [];
257
        foreach ($m1 as $i => $val) {
258
            $res[] = $val / $m2[$i];
259
        }
260
261
        return $res;
262
    }
263
264
    /**
265
     * Element-wise <b>addition</b> of two vectors of the same size
266
     *
267
     * @param array $m1
268
     * @param array $m2
269
     * @param int   $mag
270
     *
271
     * @return array
272
     */
273 View Code Duplication
    public static function add(array $m1, array $m2, int $mag = 1)
0 ignored issues
show
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...
274
    {
275
        $res = [];
276
        foreach ($m1 as $i => $val) {
277
            $res[] = $val + $mag * $m2[$i];
278
        }
279
280
        return $res;
281
    }
282
283
    /**
284
     * Element-wise <b>subtraction</b> of two vectors of the same size
285
     *
286
     * @param array $m1
287
     * @param array $m2
288
     *
289
     * @return array
290
     */
291
    public static function sub(array $m1, array $m2)
292
    {
293
        return self::add($m1, $m2, -1);
294
    }
295
296
    /**
297
     * Element-wise <b>multiplication</b> of a vector with a scalar
298
     *
299
     * @param array $m1
300
     * @param float $m2
301
     *
302
     * @return array
303
     */
304
    public static function muls(array $m1, float $m2)
305
    {
306
        $res = [];
307
        foreach ($m1 as $val) {
308
            $res[] = $val * $m2;
309
        }
310
311
        return $res;
312
    }
313
314
    /**
315
     * Element-wise <b>division</b> of a vector with a scalar
316
     *
317
     * @param array $m1
318
     * @param float $m2
319
     *
320
     * @return array
321
     */
322 View Code Duplication
    public static function divs(array $m1, float $m2)
0 ignored issues
show
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...
323
    {
324
        $res = [];
325
        foreach ($m1 as $val) {
326
            $res[] = $val / ($m2 + 1e-32);
327
        }
328
329
        return $res;
330
    }
331
332
    /**
333
     * Element-wise <b>addition</b> of a vector with a scalar
334
     *
335
     * @param array $m1
336
     * @param float $m2
337
     * @param int   $mag
338
     *
339
     * @return array
340
     */
341 View Code Duplication
    public static function adds(array $m1, float $m2, int $mag = 1)
0 ignored issues
show
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...
342
    {
343
        $res = [];
344
        foreach ($m1 as $val) {
345
            $res[] = $val + $mag * $m2;
346
        }
347
348
        return $res;
349
    }
350
351
    /**
352
     * Element-wise <b>subtraction</b> of a vector with a scalar
353
     *
354
     * @param array $m1
355
     * @param float $m2
356
     *
357
     * @return array
358
     */
359
    public static function subs(array $m1, float $m2)
360
    {
361
        return self::adds($m1, $m2, -1);
362
    }
363
}
364