Added configuration panel datasets, back-end refactor and others
Some checks failed
linter / quality (push) Failing after 7s
tests / ci (8.4) (push) Failing after 6s
tests / ci (8.5) (push) Failing after 5s

This commit is contained in:
2026-03-12 16:38:50 +01:00
parent 650cf56045
commit 83b7aa3f3a
39 changed files with 3176 additions and 425 deletions

View File

@@ -0,0 +1,49 @@
<?php
namespace App\Events;
use App\Models\ActivationsFunctions;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class PerceptronInitialization implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
/**
* Create a new event instance.
*/
public function __construct(
public array $dataset,
public ActivationsFunctions $activationFunction,
public string $sessionId,
public string $trainingId,
)
{
//
}
/**
* Get the channels the event should broadcast on.
*
* @return array<int, \Illuminate\Broadcasting\Channel>
*/
public function broadcastOn(): array
{
return [
new Channel($this->sessionId . '-perceptron-training'),
];
}
public function broadcastWith(): array
{
return [
'dataset' => $this->dataset,
'activationFunction' => $this->activationFunction,
'trainingId' => $this->trainingId,
];
}
}

View File

@@ -20,6 +20,7 @@ class PerceptronTrainingEnded implements ShouldBroadcast
public function __construct(
public string $reason,
public string $sessionId,
public string $trainingId,
)
{
//
@@ -41,6 +42,7 @@ class PerceptronTrainingEnded implements ShouldBroadcast
{
return [
'reason' => $this->reason,
'trainingId' => $this->trainingId,
];
}
}

View File

@@ -17,11 +17,9 @@ class PerceptronTrainingIteration implements ShouldBroadcast
* Create a new event instance.
*/
public function __construct(
public int $iteration,
public int $exampleIndex,
public float $error,
public array $synaptic_weights,
public array $iterations, // ["iteration" => int, "exampleIndex" => int, "error" => float, "synaptic_weights" => array]
public string $sessionId,
public string $trainingId,
)
{
//
@@ -43,10 +41,8 @@ class PerceptronTrainingIteration implements ShouldBroadcast
public function broadcastWith(): array
{
return [
'iteration' => $this->iteration,
'exampleIndex' => $this->exampleIndex,
'error' => $this->error,
'synaptic_weights' => $this->synaptic_weights,
'iterations' => $this->iterations,
'trainingId' => $this->trainingId,
];
}
}

View File

@@ -3,8 +3,11 @@
namespace App\Http\Controllers;
use App\Models\SimplePerceptron;
use App\Models\SimplePerceptronTraining;
use App\Services\DataSetReader;
use App\Services\ISynapticWeights;
use App\Services\ISynapticWeightsProvider;
use App\Services\PerceptronIterationEventBuffer;
use App\Services\ZeroSynapticWeights;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
@@ -16,111 +19,86 @@ class PerceptronController extends Controller
public function index(Request $request)
{
$perceptronType = $request->query('type', 'simple');
$dataSet = $request->input('data_set', 'logic_and');
$dataSetReader = $this->getDataSetReader($dataSet);
$learningRate = 0.1;
$maxIterations = 100;
switch ($perceptronType) {
case 'simple':
$learningRate = 0.015;
$maxIterations = 100;
break;
}
return inertia('PerceptronViewer', [
'type' => $perceptronType,
'sessionId' => session()->getId(),
'dataset' => $dataSetReader->lines,
'csrf_token' => csrf_token(),
'datasets' => $this->getDatasets(),
'minError' => 0.01,
'learningRate' => $learningRate,
'maxIterations' => $maxIterations,
]);
}
private function getDatasets(): array
{
$dataSetsDirectory = public_path('data_sets');
$files = scandir($dataSetsDirectory);
$datasets = [];
foreach ($files as $file) {
if (pathinfo($file, PATHINFO_EXTENSION) === 'csv') {
$dataset = [];
$dataset['label'] = str_replace('.csv', '', $file);
$dataSetReader = new DataSetReader($dataSetsDirectory . '/' . $file);
$dataset['data'] = $dataSetReader->lines;
switch ($dataset['label']) {
case '2.9':
$dataset['defaultLearningRate'] = 0.015;
break;
}
$datasets[] = $dataset;
}
}
return $datasets;
}
private function getDataSetReader(string $dataSet): DataSetReader
{
$dataSetFileName = "data_sets/{$dataSet}.csv";
return new DataSetReader($dataSetFileName);
}
public function run(Request $request, ISynapticWeights $synapticWeightsProvider)
public function run(Request $request, ISynapticWeightsProvider $synapticWeightsProvider)
{
$perceptronType = $request->query('type', 'simple');
$minError = $request->query('min_error', 0.01);
$dataSet = $request->input('data_set', 'logic_and');
$learningRate = $request->input('learning_rate', 0.1);
$perceptronType = $request->input('type', 'simple');
$minError = $request->input('min_error', 0.01);
$weightInitMethod = $request->input('weight_init_method', 'random');
$dataSet = $request->input('dataset');
$learningRate = $request->input('learning_rate', 0.015);
$maxIterations = $request->input('max_iterations', 100);
$sessionId = $request->input('session_id', session()->getId());
$trainingId = $request->input('training_id');
if ($weightInitMethod === 'zeros') {
$synapticWeightsProvider = new ZeroSynapticWeights();
}
$dataSetReader = $this->getDataSetReader($dataSet);
$MAX_ITERATIONS = 100;
$stopCondition;
$trainFunction;
$trainFunctionState = [];
$iterationEventBuffer = new PerceptronIterationEventBuffer($sessionId, $trainingId);
switch ($perceptronType) {
case 'simple':
$stopCondition = function($iteration, $iterationErrorCounter) use ($sessionId) {
$condition = $iterationErrorCounter == 0;
if ($condition === true) {
Log::info("Perceptron training ended after {$iteration} iterations with no errors.");
event(new \App\Events\PerceptronTrainingEnded('Le perceptron ne commet plus d\'erreurs sur aucune des données', $sessionId));
}
return $iterationErrorCounter == 0;
};
$iterationFunction = function(&$state) use ($synapticWeightsProvider, $learningRate, $minError) {
if (!isset($state['perceptron'])) {
$state['perceptron'] = new SimplePerceptron($synapticWeightsProvider->generate(2));
}
$networkTraining = match ($perceptronType) {
'simple' => new SimplePerceptronTraining($dataSetReader, $learningRate, $maxIterations, $synapticWeightsProvider, $iterationEventBuffer, $sessionId, $trainingId),
default => null,
};
$perceptron = $state['perceptron'];
$inputs = $state['inputs'];
$correctOutput = $state['correctOutput'];
$iterationErrorCounter = $state['iterationErrorCounter'] ?? 0;
event(new \App\Events\PerceptronInitialization($dataSetReader->lines, $networkTraining->activationFunction, $sessionId, $trainingId));
$output = $perceptron->test($inputs);
$error = $correctOutput - $output;
if (abs($error) > $minError) {
$iterationErrorCounter++;
}
if ($error !== 0) { // Update synaptic weights if needed
$synaptic_weights = $perceptron->getSynapticWeights();
$inputs_with_bias = array_merge([1], $inputs); // Add bias input
$new_weights = array_map(fn($weight, $input) => $weight + $learningRate * $error * $input, $synaptic_weights, $inputs_with_bias);
$perceptron->setSynapticWeights($new_weights);
}
return [$error, $perceptron->getSynapticWeights(), $iterationErrorCounter, $state];
};
break;
default:
return response()->json(['error' => 'Invalid perceptron type'], 400);
}
$iteration = 0;
$error = 1.0; // Initial error
do {
$iterationErrorCounter = 0;
$iteration++;
while ($nextRow = $dataSetReader->getRandomLine()) {
$inputs = array_slice($nextRow, 0, -1);
$correctOutput = end($nextRow);
$trainFunctionState['inputs'] = $inputs;
$trainFunctionState['correctOutput'] = $correctOutput;
$trainFunctionState['iterationErrorCounter'] = $iterationErrorCounter;
[$error, $synaptic_weights, $iterationErrorCounter, $trainFunctionState] = $iterationFunction($trainFunctionState);
$error = abs($error); // Use absolute error
// Broadcast the training iteration event
event(new \App\Events\PerceptronTrainingIteration($iteration, $dataSetReader->getLastReadLineIndex(), $error, $synaptic_weights, $sessionId));
}
$dataSetReader->reset(); // Reset the dataset for the next iteration
} while ($iteration < $MAX_ITERATIONS && !$stopCondition($iteration, $iterationErrorCounter));
if ($iteration >= $MAX_ITERATIONS) {
event(new \App\Events\PerceptronTrainingEnded('Le nombre maximal d\'itérations a été atteint', $sessionId));
}
$networkTraining->start();
return response()->json([
'message' => 'Training completed',
'iterations' => $iteration,
'final_error' => $error,
'final_synaptic_weights' => isset($trainFunctionState['perceptron']) ? $trainFunctionState['perceptron']->getSynapticWeights() : [0],
]);
}
}

View File

@@ -16,7 +16,7 @@ class HandleAppearance
*/
public function handle(Request $request, Closure $next): Response
{
View::share('appearance', $request->cookie('appearance') ?? 'system');
View::share('appearance', 'dark');
return $next($request);
}

View File

@@ -0,0 +1,10 @@
<?php
namespace App\Models;
enum ActivationsFunctions: string
{
case STEP = 'step';
case SIGMOID = 'sigmoid';
case RELU = 'relu';
}

View File

@@ -0,0 +1,40 @@
<?php
namespace App\Models;
use App\Events\PerceptronTrainingEnded;
use App\Services\DataSetReader;
use App\Services\PerceptronIterationEventBuffer;
abstract class NetworkTraining
{
protected int $iteration = 0;
/**
* @abstract
* @var ActivationsFunctions
*/
public ActivationsFunctions $activationFunction;
public function __construct(
protected DataSetReader $datasetReader,
protected int $maxIterations,
protected PerceptronIterationEventBuffer $iterationEventBuffer,
protected string $sessionId,
protected string $trainingId,
) {
}
abstract public function start() : void;
abstract protected function stopCondition(): bool;
protected function checkPassedMaxIterations() {
if ($this->iteration >= $this->maxIterations) {
event(new PerceptronTrainingEnded('Le nombre maximal d\'itérations a été atteint', $this->sessionId, $this->trainingId));
}
}
protected function addIterationToBuffer(float $error, array $synapticWeights) {
$this->iterationEventBuffer->addIteration($this->iteration, $this->datasetReader->getLastReadLineIndex(), $error, $synapticWeights);
}
}

View File

@@ -9,7 +9,7 @@ abstract class Perceptron extends Model
public function __construct(
private array $synaptic_weights,
) {
$this->synaptic_weights = $synaptic_weights; // Add bias weight
$this->synaptic_weights = $synaptic_weights;
}
public function test(array $inputs): int
@@ -24,7 +24,7 @@ abstract class Perceptron extends Model
return $this->activationFunction($weighted_sum);
}
abstract protected function activationFunction(float $weighted_sum): int;
abstract public function activationFunction(float $weighted_sum): int;
public function getSynapticWeights(): array
{

View File

@@ -10,7 +10,7 @@ class SimplePerceptron extends Perceptron {
parent::__construct($synaptic_weights);
}
protected function activationFunction(float $weighted_sum): int
public function activationFunction(float $weighted_sum): int
{
return $weighted_sum >= 0 ? 1 : 0;
}

View File

@@ -0,0 +1,87 @@
<?php
namespace App\Models;
use App\Events\PerceptronTrainingEnded;
use App\Services\DataSetReader;
use App\Services\ISynapticWeightsProvider;
use App\Services\PerceptronIterationEventBuffer;
use Illuminate\Support\Facades\Log;
class SimplePerceptronTraining extends NetworkTraining
{
private Perceptron $perceptron;
private int $iterationErrorCounter = 0;
public ActivationsFunctions $activationFunction = ActivationsFunctions::STEP;
public const MIN_ERROR = 0;
public function __construct(
DataSetReader $datasetReader,
protected float $learningRate,
int $maxIterations,
protected ISynapticWeightsProvider $synapticWeightsProvider,
PerceptronIterationEventBuffer $iterationEventBuffer,
string $sessionId,
string $trainingId,
) {
parent::__construct($datasetReader, $maxIterations, $iterationEventBuffer, $sessionId, $trainingId);
$this->perceptron = new SimplePerceptron($synapticWeightsProvider->generate(2));
}
public function start(): void
{
$this->iteration = 0;
$error = 0;
do {
$this->iterationErrorCounter = 0;
$this->iteration++;
while ($nextRow = $this->datasetReader->getRandomLine()) {
$inputs = array_slice($nextRow, 0, -1);
$correctOutput = end($nextRow);
$correctOutput = $correctOutput > 0 ? 1 : 0; // Modify labels for non binary datasets
$error = $this->iterationFunction($inputs, $correctOutput);
$error = abs($error); // Use absolute error
// Broadcast the training iteration event
$this->addIterationToBuffer($error, [[$this->perceptron->getSynapticWeights()]]);
}
$this->datasetReader->reset(); // Reset the dataset for the next iteration
} while ($this->iteration < $this->maxIterations && !$this->stopCondition());
$this->iterationEventBuffer->flush(); // Ensure all iterations are sent to the frontend
$this->checkPassedMaxIterations();
}
protected function stopCondition(): bool
{
$condition = $this->iterationErrorCounter == 0;
if ($condition === true) {
event(new PerceptronTrainingEnded('Le perceptron ne commet plus d\'erreurs sur aucune des données', $this->sessionId, $this->trainingId));
}
return $this->iterationErrorCounter == 0;
}
private function iterationFunction(array $inputs, int $correctOutput)
{
$output = $this->perceptron->test($inputs);
$error = $correctOutput - $output;
if (abs($error) > $this::MIN_ERROR) {
$this->iterationErrorCounter++;
}
if ($error !== 0) { // Update synaptic weights if needed
$synaptic_weights = $this->perceptron->getSynapticWeights();
$inputs_with_bias = array_merge([1], $inputs); // Add bias input
$new_weights = array_map(fn($weight, $input) => $weight + $this->learningRate * $error * $input, $synaptic_weights, $inputs_with_bias);
$this->perceptron->setSynapticWeights($new_weights);
}
return $error;
}
}

View File

@@ -2,7 +2,7 @@
namespace App\Providers;
use App\Services\ISynapticWeights;
use App\Services\ISynapticWeightsProvider;
use App\Services\RandomSynapticWeights;
use Illuminate\Support\ServiceProvider;
@@ -13,7 +13,7 @@ class InitialSynapticWeightsProvider extends ServiceProvider
*/
public function register(): void
{
$this->app->singleton(ISynapticWeights::class, function ($app) {
$this->app->singleton(ISynapticWeightsProvider::class, function ($app) {
return new RandomSynapticWeights();
});
}

View File

@@ -12,7 +12,7 @@ class CsvReader {
public string $filename,
)
{
$this->file = fopen(public_path($filename), "r");
$this->file = fopen($filename, "r");
if (!$this->file) {
throw new \RuntimeException("Failed to open file: " . $filename);
}

View File

@@ -2,6 +2,6 @@
namespace App\Services;
interface ISynapticWeights {
interface ISynapticWeightsProvider {
public function generate(int $input_size): array;
}

View File

@@ -0,0 +1,50 @@
<?php
namespace App\Services;
use Illuminate\Support\Facades\Log;
class PerceptronIterationEventBuffer {
private $data;
private int $nextSizeIncreaseThreshold;
private int $underSizeIncreaseCount = 0;
private int $MAX_SIZE = 50;
public function __construct(
private string $sessionId,
private string $trainingId,
private int $sizeIncreaseStart = 10,
private int $sizeIncreaseFactor = 2,
) {
$this->data = [];
$this->nextSizeIncreaseThreshold = $sizeIncreaseStart;
}
public function flush(): void {
event(new \App\Events\PerceptronTrainingIteration($this->data, $this->sessionId, $this->trainingId));
$this->data = [];
}
public function addIteration(int $iteration, int $exampleIndex, float $error, array $synaptic_weights): void {
$this->data[] = [
"iteration" => $iteration,
"exampleIndex" => $exampleIndex,
"error" => $error,
"weights" => $synaptic_weights,
];
if ($this->underSizeIncreaseCount <= $this->sizeIncreaseStart) { // We can still send a single date because we are under the increase start threshold
$this->underSizeIncreaseCount++;
$this->flush();
}
else if (count($this->data) >= $this->nextSizeIncreaseThreshold) {
$this->flush();
$this->nextSizeIncreaseThreshold *= $this->sizeIncreaseFactor;
if ($this->nextSizeIncreaseThreshold > $this->MAX_SIZE) {
$this->nextSizeIncreaseThreshold = $this->MAX_SIZE; // Cap the threshold to the maximum size
}
}
}
}

View File

@@ -2,7 +2,7 @@
namespace App\Services;
class RandomSynapticWeights implements ISynapticWeights {
class RandomSynapticWeights implements ISynapticWeightsProvider {
public function generate(int $input_size): array
{
$weights = [];

View File

@@ -0,0 +1,14 @@
<?php
namespace App\Services;
class ZeroSynapticWeights implements ISynapticWeightsProvider {
public function generate(int $input_size): array
{
$weights = [];
for ($i = 0; $i < $input_size + 1; $i++) { // +1 for bias weight
$weights[] = 0; // Zero weights
}
return $weights;
}
}