Passed
Push — master ( 331d4b...653c7c )
by Arkadiusz
02:19
created

ConjugateGradient   A

Complexity

Total Complexity 18

Size/Duplication

Total Lines 169
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 2

Importance

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

7 Methods

Rating   Name   Duplication   Size   Complexity  
B runOptimization() 0 37 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
    public function runOptimization(array $samples, array $targets, \Closure $gradientCb) : array
21
    {
22
        $this->samples = $samples;
23
        $this->targets = $targets;
24
        $this->gradientCb = $gradientCb;
25
        $this->sampleCount = count($samples);
26
        $this->costValues = [];
27
28
        $d = mp::muls($this->gradient($this->theta), -1);
29
30
        for ($i = 0; $i < $this->maxIterations; ++$i) {
31
            // Obtain α that minimizes f(θ + α.d)
32
            $alpha = $this->getAlpha(array_sum($d));
33
34
            // θ(k+1) = θ(k) + α.d
35
            $thetaNew = $this->getNewTheta($alpha, $d);
36
37
            // β = ||∇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...
38
            $beta = $this->getBeta($thetaNew);
39
40
            // 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...
41
            $d = $this->getNewDirection($thetaNew, $beta, $d);
42
43
            // Save values for the next iteration
44
            $oldTheta = $this->theta;
45
            $this->costValues[] = $this->cost($thetaNew);
46
47
            $this->theta = $thetaNew;
48
            if ($this->enableEarlyStop && $this->earlyStop($oldTheta)) {
49
                break;
50
            }
51
        }
52
53
        $this->clear();
54
55
        return $this->theta;
56
    }
57
58
    /**
59
     * Executes the callback function for the problem and returns
60
     * sum of the gradient for all samples & targets.
61
     */
62
    protected function gradient(array $theta) : array
63
    {
64
        [, $gradient] = parent::gradient($theta);
0 ignored issues
show
Bug introduced by
The variable $gradient does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
65
66
        return $gradient;
67
    }
68
69
    /**
70
     * Returns the value of f(x) for given solution
71
     */
72
    protected function cost(array $theta) : float
73
    {
74
        [$cost] = parent::gradient($theta);
0 ignored issues
show
Bug introduced by
The variable $cost does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

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