MonoLayer Perceptron
All checks were successful
linter / quality (push) Successful in 6m16s
tests / ci (8.4) (push) Successful in 4m10s
tests / ci (8.5) (push) Successful in 4m29s

This commit is contained in:
2026-04-04 16:45:04 +02:00
parent f6620c2eca
commit 2f4db07918
21 changed files with 641 additions and 226 deletions

View File

@@ -5,6 +5,7 @@ namespace App\Http\Controllers;
use App\Events\PerceptronInitialization;
use App\Models\NetworksTraining\ADALINEPerceptronTraining;
use App\Models\NetworksTraining\GradientDescentPerceptronTraining;
use App\Models\NetworksTraining\MonoLayerPerceptronTraining;
use App\Models\NetworksTraining\SimpleBinaryPerceptronTraining;
use App\Services\DatasetReader\IDataSetReader;
use App\Services\DatasetReader\LinearOrderDataSetReader;
@@ -13,7 +14,11 @@ use App\Services\IterationEventBuffer\PerceptronIterationEventBuffer;
use App\Services\IterationEventBuffer\PerceptronLimitedEpochEventBuffer;
use App\Services\SynapticWeightsProvider\ISynapticWeightsProvider;
use App\Services\SynapticWeightsProvider\ZeroSynapticWeights;
use Illuminate\Contracts\Queue\Job;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Symfony\Contracts\EventDispatcher\Event;
use Tests\Services\IterationEventBuffer\DullIterationEventBuffer;
class PerceptronController extends Controller
{
@@ -126,6 +131,8 @@ class PerceptronController extends Controller
public function run(Request $request, ISynapticWeightsProvider $synapticWeightsProvider)
{
$startTime = microtime(true);
$perceptronType = $request->input('type');
$minError = $request->input('min_error', 0.01);
$weightInitMethod = $request->input('weight_init_method', 'random');
@@ -135,6 +142,9 @@ class PerceptronController extends Controller
$sessionId = $request->input('session_id', session()->getId());
$trainingId = $request->input('training_id');
// Remove the jobs for the sessionId
DB::table('jobs')->where('payload', 'like', '%s:9:\"sessionId\";s:40:\"'. $sessionId .'\";%')->delete();
if ($weightInitMethod === 'zeros') {
$synapticWeightsProvider = new ZeroSynapticWeights;
}
@@ -151,6 +161,7 @@ class PerceptronController extends Controller
'simple' => new SimpleBinaryPerceptronTraining($datasetReader, $learningRate, $maxEpochs, $synapticWeightsProvider, $iterationEventBuffer, $sessionId, $trainingId),
'gradientdescent' => new GradientDescentPerceptronTraining($datasetReader, $learningRate, $maxEpochs, $synapticWeightsProvider, $iterationEventBuffer, $sessionId, $trainingId, $minError),
'adaline' => new ADALINEPerceptronTraining($datasetReader, $learningRate, $maxEpochs, $synapticWeightsProvider, $iterationEventBuffer, $sessionId, $trainingId, $minError),
'monolayer' => new MonoLayerPerceptronTraining($datasetReader, $learningRate, $maxEpochs, $synapticWeightsProvider, $iterationEventBuffer, $sessionId, $trainingId, $minError),
default => null,
};
@@ -160,6 +171,7 @@ class PerceptronController extends Controller
return response()->json([
'message' => 'Training completed',
'execution_time' => microtime(true) - $startTime,
]);
}
}

View File

@@ -66,7 +66,7 @@ class ADALINEPerceptronTraining extends NetworkTraining
foreach ($inputsForCurrentEpoch as $inputsWithLabel) {
$inputs = array_slice($inputsWithLabel, 0, -1);
$correctOutput = (float) end($inputsWithLabel);
$output = $this->perceptron->test($inputs);
$output = $this->perceptron->test($inputs)[0];
$iterationError = $correctOutput - $output;
$this->epochError += ($iterationError ** 2) / 2; // Squared error for the example
}
@@ -92,7 +92,7 @@ class ADALINEPerceptronTraining extends NetworkTraining
private function iterationFunction(array $inputs, float $correctOutput): float
{
$output = $this->perceptron->test($inputs);
$output = $this->perceptron->test($inputs)[0];
$error = $correctOutput - $output;

View File

@@ -88,7 +88,7 @@ class GradientDescentPerceptronTraining extends NetworkTraining
private function iterationFunction(array $inputs, float $correctOutput): float
{
$output = $this->perceptron->test($inputs);
$output = $this->perceptron->test($inputs)[0];
$error = $correctOutput - $output;

View File

@@ -0,0 +1,157 @@
<?php
namespace App\Models\NetworksTraining;
use App\Events\PerceptronTrainingEnded;
use App\Models\ActivationsFunctions;
use App\Models\Perceptrons\GradientDescentPerceptron;
use App\Models\Perceptrons\NetworkPerceptron;
use App\Models\Perceptrons\Perceptron;
use App\Models\Perceptrons\SimpleBinaryPerceptron2;
use App\Models\Perceptrons\SimpleBinaryPerceptron;
use App\Services\DatasetReader\IDataSetReader;
use App\Services\IterationEventBuffer\IPerceptronIterationEventBuffer;
use App\Services\SynapticWeightsProvider\ISynapticWeightsProvider;
use App\Services\SynapticWeightsProvider\SimpleNetworkWeightsProvider;
use Illuminate\Support\Arr;
class MonoLayerPerceptronTraining extends NetworkTraining
{
private Perceptron $network;
private array $labels;
public ActivationsFunctions $activationFunction = ActivationsFunctions::LINEAR;
public ?ActivationsFunctions $presentationLayerActivationFunction = ActivationsFunctions::STEP;
private float $epochError;
public function __construct(
IDataSetReader $datasetReader,
protected float $learningRate,
int $maxEpochs,
ISynapticWeightsProvider $synapticWeightsProvider,
IPerceptronIterationEventBuffer $iterationEventBuffer,
string $sessionId,
string $trainingId,
private float $minError,
) {
parent::__construct($datasetReader, $maxEpochs, $iterationEventBuffer, $sessionId, $trainingId);
$networkWeightsProvider = new SimpleNetworkWeightsProvider($synapticWeightsProvider);
$this->network = new NetworkPerceptron(
$networkWeightsProvider->generate(
$datasetReader->getInputSize(),
$datasetReader->getOutputSize(),
0, // No hidden layer
0, // No hidden layer neurons
),
$datasetReader->getInputSize(),
GradientDescentPerceptron::class, // No hidden layer
SimpleBinaryPerceptron2::class,
);
$this->labels = $datasetReader->getLabels();
}
public function start(): void
{
$this->epoch = 0;
do {
$this->epochError = 0;
$this->epoch++;
$inputsForCurrentEpoch = [];
while ($nextRow = $this->datasetReader->getNextLine()) {
$inputsForCurrentEpoch[] = $nextRow;
$inputs = array_slice($nextRow, 0, -1);
$correctOutput = (int) end($nextRow);
$iterationError = $this->iterationFunction($inputs, $correctOutput);
// Synaptic weights correction after each example
$synaptic_weights = $this->network->getSynapticWeights();
$inputs_with_bias = array_merge([1], $inputs); // Add bias input
// Updates the weights
$this->network->setSynapticWeights(
$this->getUpdatedSynapticWeights($synaptic_weights, $iterationError, $inputs_with_bias)
);
// Broadcast the training iteration event
$this->addIterationToBuffer(array_sum($iterationError), $this->network->getSynapticWeights());
}
// Calculte the average error for the epoch with the last synaptic weights
foreach ($inputsForCurrentEpoch as $inputsWithLabel) {
$inputs = array_slice($inputsWithLabel, 0, -1);
$correctOutput = (float) end($inputsWithLabel);
$iterationError = $this->iterationFunction($inputs, $correctOutput);
foreach ($iterationError as $error) {
$this->epochError += ($error ** 2) / 2; // Squared error for the example
}
}
$this->epochError /= $this->datasetReader->getEpochExamplesCount(); // Average error for the epoch
$this->datasetReader->reset(); // Reset the dataset for the next iteration
} while ($this->epoch < $this->maxEpochs && ! $this->stopCondition());
$this->iterationEventBuffer->flush(); // Ensure all iterations are sent to the frontend
$this->checkPassedMaxIterations($this->epochError);
}
protected function stopCondition(): bool
{
$condition = $this->epochError <= $this->minError;
if ($condition === true) {
event(new PerceptronTrainingEnded('Le perceptron à atteint l\'erreur minimale', $this->sessionId, $this->trainingId));
}
return $condition;
}
private function iterationFunction(array $inputs, int $correctOutput): array
{
$outputs = $this->network->test($inputs);
$desiredOutput = $this->getDesiredOutputFromCorrectOutput($correctOutput);
$errors = [];
foreach ($outputs as $index => $output) {
$error = $desiredOutput[$index] - $output;
$errors[] = $error;
}
return $errors;
}
private function getUpdatedSynapticWeights(array $synaptic_weights, array $iterationError, array $inputs): array
{
$updatedWeights = [];
foreach ($synaptic_weights[0] as $neuronIndex => $neuronWeights) { // There is only one layer of weights
$updatedNeuronWeights = [];
foreach ($neuronWeights as $weightIndex => $weight) {
$updatedWeight = $weight + ($this->learningRate * $iterationError[$neuronIndex] * $inputs[$weightIndex]);
$updatedNeuronWeights[] = $updatedWeight;
}
$updatedWeights[] = $updatedNeuronWeights;
}
return [$updatedWeights];
}
private function getDesiredOutputFromCorrectOutput(int $correctOutput): array
{
$desiredOutput = array_fill(0, count($this->labels), -1);
$labelIndex = Arr::first(array_keys($this->labels), fn($key) => $this->labels[$key] == $correctOutput);
if ($labelIndex !== null) {
$desiredOutput[$labelIndex] = 1;
}
return $desiredOutput;
}
public function getSynapticWeights(): array
{
return [[$this->network->getSynapticWeights()]];
}
}

View File

@@ -16,6 +16,8 @@ abstract class NetworkTraining
*/
public ActivationsFunctions $activationFunction;
public ?ActivationsFunctions $presentationLayerActivationFunction = null;
public function __construct(
protected IDataSetReader $datasetReader,
protected int $maxEpochs,

View File

@@ -71,7 +71,7 @@ class SimpleBinaryPerceptronTraining extends NetworkTraining
private function iterationFunction(array $inputs, int $correctOutput)
{
$output = $this->perceptron->test($inputs);
$output = $this->perceptron->test($inputs)[0];
$error = $correctOutput - $output;
if (abs($error) > $this::MIN_ERROR) {

View File

@@ -0,0 +1,26 @@
<?php
namespace App\Models\Perceptrons;
class InputNeuron extends Perceptron
{
public function __construct(
) {
parent::__construct([]);
}
public function setInput(float $input): void
{
$this->input = $input;
}
public function test(array $inputs): array
{
return [$this->input];
}
public function activationFunction(float $input): float
{
return $input; // Identity function for input neurons
}
}

View File

@@ -0,0 +1,76 @@
<?php
namespace App\Models\Perceptrons;
class NetworkPerceptron extends Perceptron
{
public array $network = [];
public function __construct(
private array $synaptic_weights,
private int $inputLayerNeuronsCount,
private string $hiddenLayerNeuronClass,
private string $outputLayerNeuronClass,
) {
parent::__construct($synaptic_weights);
$this->initializeNetwork($synaptic_weights);
}
private function initializeNetwork(array $synaptic_weights): void
{
// Input Layer
$this->network[0] = [];
foreach (range(0, $this->inputLayerNeuronsCount - 1) as $i) {
$this->network[0][] = new InputNeuron();
}
// Hidden Layer
for ($layerIndex = 0; $layerIndex < count($synaptic_weights) - 2; $layerIndex++) {
$this->network[$layerIndex + 1] = [];
foreach ($synaptic_weights[$layerIndex] as $neuronWeights) {
$this->network[$layerIndex + 1][] = new $this->hiddenLayerNeuronClass($neuronWeights);
}
}
// Output Layer
$outputLayer = $synaptic_weights[count($synaptic_weights) - 1];
$this->network[count($synaptic_weights)] = [];
foreach ($outputLayer as $neuronWeights) {
$this->network[count($synaptic_weights)][] = new $this->outputLayerNeuronClass($neuronWeights);
}
}
public function test(array $inputs): array
{
// Set the inputs for the input layer
foreach ($this->network[0] as $index => $inputNeuron) {
$inputNeuron->setInput($inputs[$index]);
}
// Pass through the hidden and output layers
$output = [];
for ($layerIndex = 0; $layerIndex < count($this->network); $layerIndex++) {
$lastLayerOutput = $output;
$output = [];
foreach ($this->network[$layerIndex] as $neuron) {
$output[] = $neuron->test($lastLayerOutput)[0];
}
}
return $output;
}
public function activationFunction(float $weighted_sum): float
{
return $weighted_sum;
}
public function setSynapticWeights(array $synaptic_weights): void
{
parent::setSynapticWeights($synaptic_weights);
$this->network = [];
$this->initializeNetwork($synaptic_weights);
}
}

View File

@@ -2,9 +2,9 @@
namespace App\Models\Perceptrons;
use Illuminate\Database\Eloquent\Model;
// use Illuminate\Database\Eloquent\Model;
abstract class Perceptron extends Model
abstract class Perceptron
{
public function __construct(
private array $synaptic_weights,
@@ -12,7 +12,7 @@ abstract class Perceptron extends Model
$this->synaptic_weights = $synaptic_weights;
}
public function test(array $inputs): float
public function test(array $inputs): array
{
$inputs = array_merge([1], $inputs); // Add bias input
@@ -22,7 +22,7 @@ abstract class Perceptron extends Model
$weighted_sum = array_sum(array_map(fn ($input, $weight) => $input * $weight, $inputs, $this->synaptic_weights));
return $this->activationFunction($weighted_sum);
return [$this->activationFunction($weighted_sum)];
}
abstract public function activationFunction(float $weighted_sum): float;

View File

@@ -0,0 +1,18 @@
<?php
namespace App\Models\Perceptrons;
class SimpleBinaryPerceptron2 extends Perceptron
{
public function __construct(
array $synaptic_weights,
) {
parent::__construct($synaptic_weights);
}
public function activationFunction(float $weighted_sum): float
{
// return $weighted_sum >= 0.0 ? 1.0 : -1.0;
return $weighted_sum;
}
}

View File

@@ -8,6 +8,10 @@ interface IDataSetReader
public function getInputSize(): int;
public function getOutputSize(): int;
public function getLabels(): array;
public function reset(): void;
public function getLastReadLineIndex(): int;

View File

@@ -49,6 +49,19 @@ class LinearOrderDataSetReader implements IDataSetReader
return count($this->lines[0]) - 1; // Don't count the label
}
public function getOutputSize(): int
{
// Count the number of unique labels in the dataset
$labels = array_map(fn ($line) => end($line), $this->lines);
return count(array_unique($labels));
}
public function getLabels(): array
{
$labels = array_map(fn ($line) => end($line), $this->lines);
return array_values(array_unique($labels));
}
public function reset(): void
{
$this->currentLines = $this->lines;

View File

@@ -55,6 +55,19 @@ class RandomOrderDataSetReader implements IDataSetReader
return count($this->lines[0]) - 1; // Don't count the label
}
public function getOutputSize(): int
{
// Count the number of unique labels in the dataset
$labels = array_map(fn ($line) => end($line), $this->lines);
return count(array_unique($labels));
}
public function getLabels(): array
{
$labels = array_map(fn ($line) => end($line), $this->lines);
return array_values(array_unique($labels));
}
public function reset(): void
{
$this->currentLines = $this->lines;

View File

@@ -0,0 +1,8 @@
<?php
namespace App\Services\SynapticWeightsProvider;
interface INetworkSynapticWeightsProvider
{
public function generate(int $input_size, int $output_size, int $hidden_layers_count, int $hidden_layers_neurons_count): array;
}

View File

@@ -0,0 +1,35 @@
<?php
namespace App\Services\SynapticWeightsProvider;
use App\Services\SynapticWeightsProvider\INetworkSynapticWeightsProvider;
class SimpleNetworkWeightsProvider implements INetworkSynapticWeightsProvider
{
public function __construct(
private ISynapticWeightsProvider $synapticWeightsProvider,
) {
}
public function generate(int $input_size, int $output_size, int $hidden_layers_count, int $hidden_layers_neurons_count): array
{
$synaptic_weights = [];
$lastLayerSize = $input_size;
// Generate Hidden Layer weights
for ($hiddenLayerNeuronIndex = 0; $hiddenLayerNeuronIndex < $hidden_layers_count; $hiddenLayerNeuronIndex++) {
for ($neuronIndex = 0; $neuronIndex < $hidden_layers_neurons_count; $neuronIndex++) {
$synaptic_weights[] = $this->synapticWeightsProvider->generate($lastLayerSize);
}
$lastLayerSize = $hidden_layers_neurons_count;
}
// Generate Output Layer weights
$synaptic_weights[] = [];
for ($outputNeuronIndex = 0; $outputNeuronIndex < $output_size; $outputNeuronIndex++) {
$synaptic_weights[count($synaptic_weights) -1][] = $this->synapticWeightsProvider->generate($lastLayerSize);
}
return $synaptic_weights;
}
}