Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401]

   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  }