MultilayerPerceptron::initNetwork()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 7
dl 0
loc 13
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Phpml\NeuralNetwork\Network;
6
7
use Phpml\Estimator;
8
use Phpml\Exception\InvalidArgumentException;
9
use Phpml\Helper\Predictable;
10
use Phpml\IncrementalEstimator;
11
use Phpml\NeuralNetwork\ActivationFunction;
12
use Phpml\NeuralNetwork\ActivationFunction\Sigmoid;
13
use Phpml\NeuralNetwork\Layer;
14
use Phpml\NeuralNetwork\Node\Bias;
15
use Phpml\NeuralNetwork\Node\Input;
16
use Phpml\NeuralNetwork\Node\Neuron;
17
use Phpml\NeuralNetwork\Node\Neuron\Synapse;
18
use Phpml\NeuralNetwork\Training\Backpropagation;
19
20
abstract class MultilayerPerceptron extends LayeredNetwork implements Estimator, IncrementalEstimator
21
{
22
    use Predictable;
23
24
    /**
25
     * @var array
26
     */
27
    protected $classes = [];
28
29
    /**
30
     * @var ActivationFunction|null
31
     */
32
    protected $activationFunction;
33
34
    /**
35
     * @var Backpropagation
36
     */
37
    protected $backpropagation;
38
39
    /**
40
     * @var int
41
     */
42
    private $inputLayerFeatures;
43
44
    /**
45
     * @var array
46
     */
47
    private $hiddenLayers = [];
48
49
    /**
50
     * @var float
51
     */
52
    private $learningRate;
53
54
    /**
55
     * @var int
56
     */
57
    private $iterations;
58
59
    /**
60
     * @throws InvalidArgumentException
61
     */
62
    public function __construct(
63
        int $inputLayerFeatures,
64
        array $hiddenLayers,
65
        array $classes,
66
        int $iterations = 10000,
67
        ?ActivationFunction $activationFunction = null,
68
        float $learningRate = 1.
69
    ) {
70
        if (count($hiddenLayers) === 0) {
71
            throw new InvalidArgumentException('Provide at least 1 hidden layer');
72
        }
73
74
        if (count($classes) < 2) {
75
            throw new InvalidArgumentException('Provide at least 2 different classes');
76
        }
77
78
        if (count($classes) !== count(array_unique($classes))) {
79
            throw new InvalidArgumentException('Classes must be unique');
80
        }
81
82
        $this->classes = array_values($classes);
83
        $this->iterations = $iterations;
84
        $this->inputLayerFeatures = $inputLayerFeatures;
85
        $this->hiddenLayers = $hiddenLayers;
86
        $this->activationFunction = $activationFunction;
87
        $this->learningRate = $learningRate;
88
89
        $this->initNetwork();
90
    }
91
92
    public function train(array $samples, array $targets): void
93
    {
94
        $this->reset();
95
        $this->initNetwork();
96
        $this->partialTrain($samples, $targets, $this->classes);
97
    }
98
99
    /**
100
     * @throws InvalidArgumentException
101
     */
102
    public function partialTrain(array $samples, array $targets, array $classes = []): void
103
    {
104
        if (count($classes) > 0 && array_values($classes) !== $this->classes) {
105
            // We require the list of classes in the constructor.
106
            throw new InvalidArgumentException(
107
                'The provided classes don\'t match the classes provided in the constructor'
108
            );
109
        }
110
111
        for ($i = 0; $i < $this->iterations; ++$i) {
112
            $this->trainSamples($samples, $targets);
113
        }
114
    }
115
116
    public function setLearningRate(float $learningRate): void
117
    {
118
        $this->learningRate = $learningRate;
119
        $this->backpropagation->setLearningRate($this->learningRate);
120
    }
121
122
    public function getOutput(): array
123
    {
124
        $result = [];
125
        foreach ($this->getOutputLayer()->getNodes() as $i => $neuron) {
126
            $result[$this->classes[$i]] = $neuron->getOutput();
127
        }
128
129
        return $result;
130
    }
131
132
    public function getLearningRate(): float
133
    {
134
        return $this->learningRate;
135
    }
136
137
    public function getBackpropagation(): Backpropagation
138
    {
139
        return $this->backpropagation;
140
    }
141
142
    /**
143
     * @param mixed $target
144
     */
145
    abstract protected function trainSample(array $sample, $target): void;
146
147
    /**
148
     * @return mixed
149
     */
150
    abstract protected function predictSample(array $sample);
151
152
    protected function reset(): void
153
    {
154
        $this->removeLayers();
155
    }
156
157
    private function initNetwork(): void
158
    {
159
        $this->addInputLayer($this->inputLayerFeatures);
160
        $this->addNeuronLayers($this->hiddenLayers, $this->activationFunction);
161
162
        // Sigmoid function for the output layer as we want a value from 0 to 1.
163
        $sigmoid = new Sigmoid();
164
        $this->addNeuronLayers([count($this->classes)], $sigmoid);
165
166
        $this->addBiasNodes();
167
        $this->generateSynapses();
168
169
        $this->backpropagation = new Backpropagation($this->learningRate);
170
    }
171
172
    private function addInputLayer(int $nodes): void
173
    {
174
        $this->addLayer(new Layer($nodes, Input::class));
175
    }
176
177
    private function addNeuronLayers(array $layers, ?ActivationFunction $defaultActivationFunction = null): void
178
    {
179
        foreach ($layers as $layer) {
180
            if (is_array($layer)) {
181
                $function = $layer[1] instanceof ActivationFunction ? $layer[1] : $defaultActivationFunction;
182
                $this->addLayer(new Layer($layer[0], Neuron::class, $function));
183
            } elseif ($layer instanceof Layer) {
184
                $this->addLayer($layer);
185
            } else {
186
                $this->addLayer(new Layer($layer, Neuron::class, $defaultActivationFunction));
187
            }
188
        }
189
    }
190
191
    private function generateSynapses(): void
192
    {
193
        $layersNumber = count($this->layers) - 1;
194
        for ($i = 0; $i < $layersNumber; ++$i) {
195
            $currentLayer = $this->layers[$i];
196
            $nextLayer = $this->layers[$i + 1];
197
            $this->generateLayerSynapses($nextLayer, $currentLayer);
198
        }
199
    }
200
201
    private function addBiasNodes(): void
202
    {
203
        $biasLayers = count($this->layers) - 1;
204
        for ($i = 0; $i < $biasLayers; ++$i) {
205
            $this->layers[$i]->addNode(new Bias());
206
        }
207
    }
208
209
    private function generateLayerSynapses(Layer $nextLayer, Layer $currentLayer): void
210
    {
211
        foreach ($nextLayer->getNodes() as $nextNeuron) {
212
            if ($nextNeuron instanceof Neuron) {
213
                $this->generateNeuronSynapses($currentLayer, $nextNeuron);
214
            }
215
        }
216
    }
217
218
    private function generateNeuronSynapses(Layer $currentLayer, Neuron $nextNeuron): void
219
    {
220
        foreach ($currentLayer->getNodes() as $currentNeuron) {
221
            $nextNeuron->addSynapse(new Synapse($currentNeuron));
222
        }
223
    }
224
225
    private function trainSamples(array $samples, array $targets): void
226
    {
227
        foreach ($targets as $key => $target) {
228
            $this->trainSample($samples[$key], $target);
229
        }
230
    }
231
}
232