MultilayerPerceptron.php 6.07 KB
Newer Older
1
2
<?php

Arkadiusz Kondas's avatar
Arkadiusz Kondas committed
3
declare(strict_types=1);
4
5
6

namespace Phpml\NeuralNetwork\Network;

7
use Phpml\Estimator;
8
use Phpml\Exception\InvalidArgumentException;
9
10
use Phpml\Helper\Predictable;
use Phpml\IncrementalEstimator;
11
use Phpml\NeuralNetwork\ActivationFunction;
12
use Phpml\NeuralNetwork\ActivationFunction\Sigmoid;
13
14
15
16
17
use Phpml\NeuralNetwork\Layer;
use Phpml\NeuralNetwork\Node\Bias;
use Phpml\NeuralNetwork\Node\Input;
use Phpml\NeuralNetwork\Node\Neuron;
use Phpml\NeuralNetwork\Node\Neuron\Synapse;
18
use Phpml\NeuralNetwork\Training\Backpropagation;
19

20
abstract class MultilayerPerceptron extends LayeredNetwork implements Estimator, IncrementalEstimator
21
{
22
23
    use Predictable;

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
41

    /**
     * @var int
     */
42
    private $inputLayerFeatures;
43

44
    /**
45
     * @var array
46
     */
47
    private $hiddenLayers = [];
48
49

    /**
50
     * @var float
51
     */
52
    private $learningRate;
53

54
55
56
57
58
    /**
     * @var int
     */
    private $iterations;

59
    /**
60
61
     * @throws InvalidArgumentException
     */
62
    public function __construct(int $inputLayerFeatures, array $hiddenLayers, array $classes, int $iterations = 10000, ?ActivationFunction $activationFunction = null, float $learningRate = 1)
63
    {
64
        if (empty($hiddenLayers)) {
65
            throw new InvalidArgumentException('Provide at least 1 hidden layer');
66
67
        }

68
        if (count($classes) < 2) {
69
            throw new InvalidArgumentException('Provide at least 2 different classes');
70
71
        }

72
73
74
75
        if (count($classes) !== count(array_unique($classes))) {
            throw new InvalidArgumentException('Classes must be unique');
        }

76
        $this->classes = array_values($classes);
77
        $this->iterations = $iterations;
78
79
80
        $this->inputLayerFeatures = $inputLayerFeatures;
        $this->hiddenLayers = $hiddenLayers;
        $this->activationFunction = $activationFunction;
81
        $this->learningRate = $learningRate;
82
83
84

        $this->initNetwork();
    }
85

Tomáš Votruba's avatar
Tomáš Votruba committed
86
    public function train(array $samples, array $targets): void
87
    {
88
89
90
91
92
93
        $this->reset();
        $this->initNetwork();
        $this->partialTrain($samples, $targets, $this->classes);
    }

    /**
94
     * @throws InvalidArgumentException
95
     */
Tomáš Votruba's avatar
Tomáš Votruba committed
96
    public function partialTrain(array $samples, array $targets, array $classes = []): void
97
98
99
    {
        if (!empty($classes) && array_values($classes) !== $this->classes) {
            // We require the list of classes in the constructor.
100
101
102
            throw new InvalidArgumentException(
                'The provided classes don\'t match the classes provided in the constructor'
            );
103
104
        }

105
106
107
        for ($i = 0; $i < $this->iterations; ++$i) {
            $this->trainSamples($samples, $targets);
        }
108
109
    }

110
111
112
113
114
115
    public function setLearningRate(float $learningRate): void
    {
        $this->learningRate = $learningRate;
        $this->backpropagation->setLearningRate($this->learningRate);
    }

116
117
118
119
120
121
122
123
124
125
    public function getOutput(): array
    {
        $result = [];
        foreach ($this->getOutputLayer()->getNodes() as $i => $neuron) {
            $result[$this->classes[$i]] = $neuron->getOutput();
        }

        return $result;
    }

126
127
128
    /**
     * @param mixed $target
     */
129
    abstract protected function trainSample(array $sample, $target);
130
131
132
133

    /**
     * @return mixed
     */
134
135
    abstract protected function predictSample(array $sample);

Tomáš Votruba's avatar
Tomáš Votruba committed
136
    protected function reset(): void
137
138
139
    {
        $this->removeLayers();
    }
140

141
142
143
144
    private function initNetwork(): void
    {
        $this->addInputLayer($this->inputLayerFeatures);
        $this->addNeuronLayers($this->hiddenLayers, $this->activationFunction);
145
146
147
148

        // Sigmoid function for the output layer as we want a value from 0 to 1.
        $sigmoid = new Sigmoid();
        $this->addNeuronLayers([count($this->classes)], $sigmoid);
149
150
151
152
153
154
155

        $this->addBiasNodes();
        $this->generateSynapses();

        $this->backpropagation = new Backpropagation($this->learningRate);
    }

Tomáš Votruba's avatar
Tomáš Votruba committed
156
    private function addInputLayer(int $nodes): void
157
158
159
160
    {
        $this->addLayer(new Layer($nodes, Input::class));
    }

161
    private function addNeuronLayers(array $layers, ?ActivationFunction $defaultActivationFunction = null): void
162
    {
163
164
165
166
167
168
169
170
171
        foreach ($layers as $layer) {
            if (is_array($layer)) {
                $function = $layer[1] instanceof ActivationFunction ? $layer[1] : $defaultActivationFunction;
                $this->addLayer(new Layer($layer[0], Neuron::class, $function));
            } elseif ($layer instanceof Layer) {
                $this->addLayer($layer);
            } else {
                $this->addLayer(new Layer($layer, Neuron::class, $defaultActivationFunction));
            }
172
173
174
        }
    }

Tomáš Votruba's avatar
Tomáš Votruba committed
175
    private function generateSynapses(): void
176
177
178
179
180
181
182
183
184
    {
        $layersNumber = count($this->layers) - 1;
        for ($i = 0; $i < $layersNumber; ++$i) {
            $currentLayer = $this->layers[$i];
            $nextLayer = $this->layers[$i + 1];
            $this->generateLayerSynapses($nextLayer, $currentLayer);
        }
    }

Tomáš Votruba's avatar
Tomáš Votruba committed
185
    private function addBiasNodes(): void
186
187
    {
        $biasLayers = count($this->layers) - 1;
Arkadiusz Kondas's avatar
Arkadiusz Kondas committed
188
        for ($i = 0; $i < $biasLayers; ++$i) {
189
190
191
192
            $this->layers[$i]->addNode(new Bias());
        }
    }

Tomáš Votruba's avatar
Tomáš Votruba committed
193
    private function generateLayerSynapses(Layer $nextLayer, Layer $currentLayer): void
194
195
196
197
198
199
200
201
    {
        foreach ($nextLayer->getNodes() as $nextNeuron) {
            if ($nextNeuron instanceof Neuron) {
                $this->generateNeuronSynapses($currentLayer, $nextNeuron);
            }
        }
    }

Tomáš Votruba's avatar
Tomáš Votruba committed
202
    private function generateNeuronSynapses(Layer $currentLayer, Neuron $nextNeuron): void
203
204
205
206
207
    {
        foreach ($currentLayer->getNodes() as $currentNeuron) {
            $nextNeuron->addSynapse(new Synapse($currentNeuron));
        }
    }
208

Tomáš Votruba's avatar
Tomáš Votruba committed
209
    private function trainSamples(array $samples, array $targets): void
210
211
212
213
214
    {
        foreach ($targets as $key => $target) {
            $this->trainSample($samples[$key], $target);
        }
    }
215
}