Passed
Push — master ( c44f3b...492344 )
by Arkadiusz
02:48
created

ConjugateGradient   A

Complexity

Total Complexity 18

Size/Duplication

Total Lines 201
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 2

Importance

Changes 0
Metric Value
wmc 18
lcom 1
cbo 2
dl 0
loc 201
rs 10
c 0
b 0
f 0

7 Methods

Rating   Name   Duplication   Size   Complexity  
B runOptimization() 0 35 4
A gradient() 0 6 1
A cost() 0 6 1
B getAlpha() 0 38 6
A getNewTheta() 0 19 4
A getBeta() 0 7 1
A getNewDirection() 0 6 1
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
        return $this->theta;
61
    }
62
63
    /**
64
     * Executes the callback function for the problem and returns
65
     * sum of the gradient for all samples & targets.
66
     *
67
     * @param array $theta
68
     *
69
     * @return float
70
     */
71
    protected function gradient(array $theta)
72
    {
73
        list($_, $gradient, $_) = parent::gradient($theta);
0 ignored issues
show
Unused Code introduced by
The assignment to $_ is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
74
75
        return $gradient;
76
    }
77
78
    /**
79
     * Returns the value of f(x) for given solution
80
     *
81
     * @param array $theta
82
     *
83
     * @return float
84
     */
85
    protected function cost(array $theta)
86
    {
87
        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...
Bug introduced by
Why assign $_ to itself?

This checks looks for cases where a variable has been assigned to itself.

This assignement can be removed without consequences.

Loading history...
Unused Code introduced by
The assignment to $_ is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
88
89
        return array_sum($cost) / $this->sampleCount;
90
    }
91
92
    /**
93
     * Calculates alpha that minimizes the function f(θ + α.d)
94
     * by performing a line search that does not rely upon the derivation.
95
     *
96
     * There are several alternatives for this function. For now, we
97
     * prefer a method inspired from the bisection method for its simplicity.
98
     * This algorithm attempts to find an optimum alpha value between 0.0001 and 0.01
99
     *
100
     * Algorithm as follows:
101
     *  a) Probe a small alpha  (0.0001) and calculate cost function
102
     *  b) Probe a larger alpha (0.01) and calculate cost function
103
     *		b-1) If cost function decreases, continue enlarging alpha
104
     *		b-2) If cost function increases, take the midpoint and try again
105
     *
106
     * @param float $d
107
     *
108
     * @return array
109
     */
110
    protected function getAlpha(float $d)
111
    {
112
        $small = 0.0001 * $d;
113
        $large = 0.01 * $d;
114
115
        // Obtain θ + α.d for two initial values, x0 and x1
116
        $x0 = mp::adds($this->theta, $small);
117
        $x1 = mp::adds($this->theta, $large);
118
119
        $epsilon = 0.0001;
120
        $iteration = 0;
121
        do {
122
            $fx1 = $this->cost($x1);
123
            $fx0 = $this->cost($x0);
124
125
            // If the difference between two values is small enough
126
            // then break the loop
127
            if (abs($fx1 - $fx0) <= $epsilon) {
128
                break;
129
            }
130
131
            if ($fx1 < $fx0) {
132
                $x0 = $x1;
133
                $x1 = mp::adds($x1, 0.01); // Enlarge second
134
            } else {
135
                $x1 = mp::divs(mp::add($x1, $x0), 2.0);
136
            } // Get to the midpoint
137
138
            $error = $fx1 / $this->dimensions;
139
        } while ($error <= $epsilon || $iteration++ < 10);
140
141
        //  Return α = θ / d
142
        if ($d == 0) {
143
            return $x1[0] - $this->theta[0];
144
        }
145
146
        return ($x1[0] - $this->theta[0]) / $d;
147
    }
148
149
    /**
150
     * Calculates new set of solutions with given alpha (for each θ(k)) and
151
     * gradient direction.
152
     *
153
     * θ(k+1) = θ(k) + α.d
154
     *
155
     * @param float $alpha
156
     * @param array $d
157
     *
158
     * return array
159
     */
160
    protected function getNewTheta(float $alpha, array $d)
161
    {
162
        $theta = $this->theta;
163
164
        for ($i=0; $i < $this->dimensions + 1; $i++) {
165
            if ($i == 0) {
166
                $theta[$i] += $alpha * array_sum($d);
167
            } else {
168
                $sum = 0.0;
169
                foreach ($this->samples as $si => $sample) {
170
                    $sum += $sample[$i - 1] * $d[$si] * $alpha;
171
                }
172
173
                $theta[$i] += $sum;
174
            }
175
        }
176
177
        return $theta;
178
    }
179
180
    /**
181
     * Calculates new beta (β) for given set of solutions by using
182
     * Fletcher–Reeves method.
183
     *
184
     * β = ||f(x(k+1))||²  ∕  ||f(x(k))||²
185
     *
186
     * See:
187
     *  R. Fletcher and C. M. Reeves, "Function minimization by conjugate gradients", Comput. J. 7 (1964), 149–154.
188
     *
189
     * @param array $newTheta
190
     *
191
     * @return float
192
     */
193
    protected function getBeta(array $newTheta)
194
    {
195
        $dNew = array_sum($this->gradient($newTheta));
196
        $dOld = array_sum($this->gradient($this->theta)) + 1e-100;
197
198
        return  $dNew ** 2 / $dOld ** 2;
199
    }
200
201
    /**
202
     * Calculates the new conjugate direction
203
     *
204
     * d(k+1) =–∇f(x(k+1)) + β(k).d(k)
205
     *
206
     * @param array $theta
207
     * @param float $beta
208
     * @param array $d
209
     *
210
     * @return array
211
     */
212
    protected function getNewDirection(array $theta, float $beta, array $d)
213
    {
214
        $grad = $this->gradient($theta);
215
216
        return mp::add(mp::muls($grad, -1), mp::muls($d, $beta));
217
    }
218
}
219
220
/**
221
 * Handles element-wise vector operations between vector-vector
222
 * and vector-scalar variables
223
 */
224
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...
225
{
226
    /**
227
     * Element-wise <b>multiplication</b> of two vectors of the same size
228
     *
229
     * @param array $m1
230
     * @param array $m2
231
     *
232
     * @return array
233
     */
234 View Code Duplication
    public static function mul(array $m1, array $m2)
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...
235
    {
236
        $res = [];
237
        foreach ($m1 as $i => $val) {
238
            $res[] = $val * $m2[$i];
239
        }
240
241
        return $res;
242
    }
243
244
    /**
245
     * Element-wise <b>division</b> of two vectors of the same size
246
     *
247
     * @param array $m1
248
     * @param array $m2
249
     *
250
     * @return array
251
     */
252 View Code Duplication
    public static function div(array $m1, array $m2)
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...
253
    {
254
        $res = [];
255
        foreach ($m1 as $i => $val) {
256
            $res[] = $val / $m2[$i];
257
        }
258
259
        return $res;
260
    }
261
262
    /**
263
     * Element-wise <b>addition</b> of two vectors of the same size
264
     *
265
     * @param array $m1
266
     * @param array $m2
267
     *
268
     * @return array
269
     */
270 View Code Duplication
    public static function add(array $m1, array $m2, $mag = 1)
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...
271
    {
272
        $res = [];
273
        foreach ($m1 as $i => $val) {
274
            $res[] = $val + $mag * $m2[$i];
275
        }
276
277
        return $res;
278
    }
279
280
    /**
281
     * Element-wise <b>subtraction</b> of two vectors of the same size
282
     *
283
     * @param array $m1
284
     * @param array $m2
285
     *
286
     * @return array
287
     */
288
    public static function sub(array $m1, array $m2)
289
    {
290
        return self::add($m1, $m2, -1);
291
    }
292
293
    /**
294
     * Element-wise <b>multiplication</b> of a vector with a scalar
295
     *
296
     * @param array $m1
297
     * @param float $m2
298
     *
299
     * @return array
300
     */
301
    public static function muls(array $m1, float $m2)
302
    {
303
        $res = [];
304
        foreach ($m1 as $val) {
305
            $res[] = $val * $m2;
306
        }
307
308
        return $res;
309
    }
310
311
    /**
312
     * Element-wise <b>division</b> of a vector with a scalar
313
     *
314
     * @param array $m1
315
     * @param float $m2
316
     *
317
     * @return array
318
     */
319 View Code Duplication
    public static function divs(array $m1, float $m2)
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...
320
    {
321
        $res = [];
322
        foreach ($m1 as $val) {
323
            $res[] = $val / ($m2 + 1e-32);
324
        }
325
326
        return $res;
327
    }
328
329
    /**
330
     * Element-wise <b>addition</b> of a vector with a scalar
331
     *
332
     * @param array $m1
333
     * @param float $m2
334
     *
335
     * @return array
336
     */
337 View Code Duplication
    public static function adds(array $m1, float $m2, $mag = 1)
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...
338
    {
339
        $res = [];
340
        foreach ($m1 as $val) {
341
            $res[] = $val + $mag * $m2;
342
        }
343
344
        return $res;
345
    }
346
347
    /**
348
     * Element-wise <b>subtraction</b> of a vector with a scalar
349
     *
350
     * @param array $m1
351
     * @param float $m2
352
     *
353
     * @return array
354
     */
355
    public static function subs(array $m1, array $m2)
356
    {
357
        return self::adds($m1, $m2, -1);
0 ignored issues
show
Documentation introduced by
$m2 is of type array, but the function expects a double.

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...
358
    }
359
}
360