Added configuration panel datasets, back-end refactor and others
This commit is contained in:
49
app/Events/PerceptronInitialization.php
Normal file
49
app/Events/PerceptronInitialization.php
Normal 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,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,6 +20,7 @@ class PerceptronTrainingEnded implements ShouldBroadcast
|
|||||||
public function __construct(
|
public function __construct(
|
||||||
public string $reason,
|
public string $reason,
|
||||||
public string $sessionId,
|
public string $sessionId,
|
||||||
|
public string $trainingId,
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
//
|
//
|
||||||
@@ -41,6 +42,7 @@ class PerceptronTrainingEnded implements ShouldBroadcast
|
|||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'reason' => $this->reason,
|
'reason' => $this->reason,
|
||||||
|
'trainingId' => $this->trainingId,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,11 +17,9 @@ class PerceptronTrainingIteration implements ShouldBroadcast
|
|||||||
* Create a new event instance.
|
* Create a new event instance.
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
public int $iteration,
|
public array $iterations, // ["iteration" => int, "exampleIndex" => int, "error" => float, "synaptic_weights" => array]
|
||||||
public int $exampleIndex,
|
|
||||||
public float $error,
|
|
||||||
public array $synaptic_weights,
|
|
||||||
public string $sessionId,
|
public string $sessionId,
|
||||||
|
public string $trainingId,
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
//
|
//
|
||||||
@@ -43,10 +41,8 @@ class PerceptronTrainingIteration implements ShouldBroadcast
|
|||||||
public function broadcastWith(): array
|
public function broadcastWith(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'iteration' => $this->iteration,
|
'iterations' => $this->iterations,
|
||||||
'exampleIndex' => $this->exampleIndex,
|
'trainingId' => $this->trainingId,
|
||||||
'error' => $this->error,
|
|
||||||
'synaptic_weights' => $this->synaptic_weights,
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,11 @@
|
|||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
use App\Models\SimplePerceptron;
|
use App\Models\SimplePerceptron;
|
||||||
|
use App\Models\SimplePerceptronTraining;
|
||||||
use App\Services\DataSetReader;
|
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\Http\Request;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
@@ -16,111 +19,86 @@ class PerceptronController extends Controller
|
|||||||
public function index(Request $request)
|
public function index(Request $request)
|
||||||
{
|
{
|
||||||
$perceptronType = $request->query('type', 'simple');
|
$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', [
|
return inertia('PerceptronViewer', [
|
||||||
'type' => $perceptronType,
|
'type' => $perceptronType,
|
||||||
'sessionId' => session()->getId(),
|
'sessionId' => session()->getId(),
|
||||||
'dataset' => $dataSetReader->lines,
|
'datasets' => $this->getDatasets(),
|
||||||
'csrf_token' => csrf_token(),
|
'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
|
private function getDataSetReader(string $dataSet): DataSetReader
|
||||||
{
|
{
|
||||||
$dataSetFileName = "data_sets/{$dataSet}.csv";
|
$dataSetFileName = "data_sets/{$dataSet}.csv";
|
||||||
return new DataSetReader($dataSetFileName);
|
return new DataSetReader($dataSetFileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function run(Request $request, ISynapticWeights $synapticWeightsProvider)
|
public function run(Request $request, ISynapticWeightsProvider $synapticWeightsProvider)
|
||||||
{
|
{
|
||||||
$perceptronType = $request->query('type', 'simple');
|
$perceptronType = $request->input('type', 'simple');
|
||||||
$minError = $request->query('min_error', 0.01);
|
$minError = $request->input('min_error', 0.01);
|
||||||
$dataSet = $request->input('data_set', 'logic_and');
|
$weightInitMethod = $request->input('weight_init_method', 'random');
|
||||||
$learningRate = $request->input('learning_rate', 0.1);
|
$dataSet = $request->input('dataset');
|
||||||
|
$learningRate = $request->input('learning_rate', 0.015);
|
||||||
|
$maxIterations = $request->input('max_iterations', 100);
|
||||||
$sessionId = $request->input('session_id', session()->getId());
|
$sessionId = $request->input('session_id', session()->getId());
|
||||||
|
$trainingId = $request->input('training_id');
|
||||||
|
|
||||||
|
if ($weightInitMethod === 'zeros') {
|
||||||
|
$synapticWeightsProvider = new ZeroSynapticWeights();
|
||||||
|
}
|
||||||
|
|
||||||
$dataSetReader = $this->getDataSetReader($dataSet);
|
$dataSetReader = $this->getDataSetReader($dataSet);
|
||||||
|
|
||||||
$MAX_ITERATIONS = 100;
|
$iterationEventBuffer = new PerceptronIterationEventBuffer($sessionId, $trainingId);
|
||||||
$stopCondition;
|
|
||||||
$trainFunction;
|
|
||||||
$trainFunctionState = [];
|
|
||||||
|
|
||||||
switch ($perceptronType) {
|
$networkTraining = match ($perceptronType) {
|
||||||
case 'simple':
|
'simple' => new SimplePerceptronTraining($dataSetReader, $learningRate, $maxIterations, $synapticWeightsProvider, $iterationEventBuffer, $sessionId, $trainingId),
|
||||||
$stopCondition = function($iteration, $iterationErrorCounter) use ($sessionId) {
|
default => null,
|
||||||
$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));
|
|
||||||
}
|
|
||||||
|
|
||||||
$perceptron = $state['perceptron'];
|
event(new \App\Events\PerceptronInitialization($dataSetReader->lines, $networkTraining->activationFunction, $sessionId, $trainingId));
|
||||||
$inputs = $state['inputs'];
|
|
||||||
$correctOutput = $state['correctOutput'];
|
|
||||||
$iterationErrorCounter = $state['iterationErrorCounter'] ?? 0;
|
|
||||||
|
|
||||||
$output = $perceptron->test($inputs);
|
$networkTraining->start();
|
||||||
|
|
||||||
$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));
|
|
||||||
}
|
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'message' => 'Training completed',
|
'message' => 'Training completed',
|
||||||
'iterations' => $iteration,
|
|
||||||
'final_error' => $error,
|
|
||||||
'final_synaptic_weights' => isset($trainFunctionState['perceptron']) ? $trainFunctionState['perceptron']->getSynapticWeights() : [0],
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ class HandleAppearance
|
|||||||
*/
|
*/
|
||||||
public function handle(Request $request, Closure $next): Response
|
public function handle(Request $request, Closure $next): Response
|
||||||
{
|
{
|
||||||
View::share('appearance', $request->cookie('appearance') ?? 'system');
|
View::share('appearance', 'dark');
|
||||||
|
|
||||||
return $next($request);
|
return $next($request);
|
||||||
}
|
}
|
||||||
|
|||||||
10
app/Models/ActivationsFunctions.php
Normal file
10
app/Models/ActivationsFunctions.php
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
enum ActivationsFunctions: string
|
||||||
|
{
|
||||||
|
case STEP = 'step';
|
||||||
|
case SIGMOID = 'sigmoid';
|
||||||
|
case RELU = 'relu';
|
||||||
|
}
|
||||||
40
app/Models/NetworkTraining.php
Normal file
40
app/Models/NetworkTraining.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@ abstract class Perceptron extends Model
|
|||||||
public function __construct(
|
public function __construct(
|
||||||
private array $synaptic_weights,
|
private array $synaptic_weights,
|
||||||
) {
|
) {
|
||||||
$this->synaptic_weights = $synaptic_weights; // Add bias weight
|
$this->synaptic_weights = $synaptic_weights;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test(array $inputs): int
|
public function test(array $inputs): int
|
||||||
@@ -24,7 +24,7 @@ abstract class Perceptron extends Model
|
|||||||
return $this->activationFunction($weighted_sum);
|
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
|
public function getSynapticWeights(): array
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ class SimplePerceptron extends Perceptron {
|
|||||||
parent::__construct($synaptic_weights);
|
parent::__construct($synaptic_weights);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function activationFunction(float $weighted_sum): int
|
public function activationFunction(float $weighted_sum): int
|
||||||
{
|
{
|
||||||
return $weighted_sum >= 0 ? 1 : 0;
|
return $weighted_sum >= 0 ? 1 : 0;
|
||||||
}
|
}
|
||||||
|
|||||||
87
app/Models/SimplePerceptronTraining.php
Normal file
87
app/Models/SimplePerceptronTraining.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Providers;
|
namespace App\Providers;
|
||||||
|
|
||||||
use App\Services\ISynapticWeights;
|
use App\Services\ISynapticWeightsProvider;
|
||||||
use App\Services\RandomSynapticWeights;
|
use App\Services\RandomSynapticWeights;
|
||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@ class InitialSynapticWeightsProvider extends ServiceProvider
|
|||||||
*/
|
*/
|
||||||
public function register(): void
|
public function register(): void
|
||||||
{
|
{
|
||||||
$this->app->singleton(ISynapticWeights::class, function ($app) {
|
$this->app->singleton(ISynapticWeightsProvider::class, function ($app) {
|
||||||
return new RandomSynapticWeights();
|
return new RandomSynapticWeights();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ class CsvReader {
|
|||||||
public string $filename,
|
public string $filename,
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
$this->file = fopen(public_path($filename), "r");
|
$this->file = fopen($filename, "r");
|
||||||
if (!$this->file) {
|
if (!$this->file) {
|
||||||
throw new \RuntimeException("Failed to open file: " . $filename);
|
throw new \RuntimeException("Failed to open file: " . $filename);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,6 @@
|
|||||||
|
|
||||||
namespace App\Services;
|
namespace App\Services;
|
||||||
|
|
||||||
interface ISynapticWeights {
|
interface ISynapticWeightsProvider {
|
||||||
public function generate(int $input_size): array;
|
public function generate(int $input_size): array;
|
||||||
}
|
}
|
||||||
50
app/Services/PerceptronIterationEventBuffer.php
Normal file
50
app/Services/PerceptronIterationEventBuffer.php
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Services;
|
namespace App\Services;
|
||||||
|
|
||||||
class RandomSynapticWeights implements ISynapticWeights {
|
class RandomSynapticWeights implements ISynapticWeightsProvider {
|
||||||
public function generate(int $input_size): array
|
public function generate(int $input_size): array
|
||||||
{
|
{
|
||||||
$weights = [];
|
$weights = [];
|
||||||
|
|||||||
14
app/Services/ZeroSynapticWeights.php
Normal file
14
app/Services/ZeroSynapticWeights.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
1877
package-lock.json
generated
1877
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -37,19 +37,23 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@inertiajs/vue3": "^2.3.7",
|
"@inertiajs/vue3": "^2.3.7",
|
||||||
|
"@vee-validate/zod": "^4.15.1",
|
||||||
"@vueuse/core": "^12.8.2",
|
"@vueuse/core": "^12.8.2",
|
||||||
"chart.js": "^4.5.1",
|
"chart.js": "^4.5.1",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"laravel-vite-plugin": "^2.0.0",
|
"laravel-vite-plugin": "^2.0.0",
|
||||||
"lucide-vue-next": "^0.468.0",
|
"lucide-vue-next": "^0.468.0",
|
||||||
"reka-ui": "^2.6.1",
|
"radix-ui": "^1.4.3",
|
||||||
|
"reka-ui": "^2.9.0",
|
||||||
"tailwind-merge": "^3.2.0",
|
"tailwind-merge": "^3.2.0",
|
||||||
"tailwindcss": "^4.1.1",
|
"tailwindcss": "^4.1.1",
|
||||||
"tw-animate-css": "^1.2.5",
|
"tw-animate-css": "^1.2.5",
|
||||||
|
"vee-validate": "^4.15.1",
|
||||||
"vue": "^3.5.13",
|
"vue": "^3.5.13",
|
||||||
"vue-chartjs": "^5.3.3",
|
"vue-chartjs": "^5.3.3",
|
||||||
"vue-input-otp": "^0.3.2"
|
"vue-input-otp": "^0.3.2",
|
||||||
|
"zod": "^3.25.76"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@rollup/rollup-linux-x64-gnu": "4.9.5",
|
"@rollup/rollup-linux-x64-gnu": "4.9.5",
|
||||||
|
|||||||
22
public/data_sets/2.9.csv
Normal file
22
public/data_sets/2.9.csv
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
x_1, x_2, d
|
||||||
|
1, 6, 1
|
||||||
|
7, 9, -1
|
||||||
|
1, 9, 1
|
||||||
|
7, 10, -1
|
||||||
|
2, 5, -1
|
||||||
|
2, 7, 1
|
||||||
|
2, 8, 1
|
||||||
|
6, 8, -1
|
||||||
|
6, 9, -1
|
||||||
|
3, 5, -1
|
||||||
|
3, 6, -1
|
||||||
|
3, 8, 1
|
||||||
|
3, 9, 1
|
||||||
|
5, 7, -1
|
||||||
|
5, 8, -1
|
||||||
|
5, 10, 1
|
||||||
|
5, 11, 1
|
||||||
|
4, 6, -1
|
||||||
|
4, 7, -1
|
||||||
|
4, 9, 1
|
||||||
|
4, 10, 1
|
||||||
|
5
public/data_sets/logic_xor.csv
Normal file
5
public/data_sets/logic_xor.csv
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
x_1, x_2, d
|
||||||
|
0, 0, 1
|
||||||
|
0, 1, 0
|
||||||
|
1, 0, 0
|
||||||
|
1, 1, 1
|
||||||
|
52
resources/js/components/IterationTable.vue
Normal file
52
resources/js/components/IterationTable.vue
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ComputedRef } from 'vue';
|
||||||
|
import type { Iteration } from '@/types/perceptron';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
iterations: Iteration[];
|
||||||
|
trainingEnded: boolean;
|
||||||
|
trainingEndReason: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
// All weight in a simple array
|
||||||
|
const allWeightPerIteration: ComputedRef<number[][]> = computed(() => {
|
||||||
|
return props.iterations.map((iteration) => {
|
||||||
|
// We flatten the weights
|
||||||
|
return iteration.weights.flat(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<table class="table w-full border-collapse border border-gray-300">
|
||||||
|
<tr class="text-left" v-if="props.iterations.length > 0">
|
||||||
|
<th>Itération</th>
|
||||||
|
<th>Exemple</th>
|
||||||
|
<th v-for="(weight, index) in allWeightPerIteration[allWeightPerIteration.length - 1]" v-bind:key="index">
|
||||||
|
X<sub>{{ index }}</sub>
|
||||||
|
</th>
|
||||||
|
<th>Erreur</th>
|
||||||
|
</tr>
|
||||||
|
<tr
|
||||||
|
v-for="(iteration, index) in props.iterations"
|
||||||
|
v-bind:key="index"
|
||||||
|
:class="{
|
||||||
|
'bg-gray-900': iteration.iteration % 2 === 0,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<td>{{ iteration.iteration }}</td>
|
||||||
|
<td>{{ iteration.exampleIndex }}</td>
|
||||||
|
<td v-for="(weight, index) in allWeightPerIteration[index]" v-bind:key="index">
|
||||||
|
{{ weight.toFixed(2) }}
|
||||||
|
</td>
|
||||||
|
<td>{{ iteration.error.toFixed(2) }}</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr v-if="props.trainingEnded" class="bg-red-900 text-center">
|
||||||
|
<td colspan="100%">
|
||||||
|
<strong>Entraînement terminé :</strong>
|
||||||
|
{{ props.trainingEndReason }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</template>
|
||||||
188
resources/js/components/PerceptronDecisionGraph.vue
Normal file
188
resources/js/components/PerceptronDecisionGraph.vue
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type {
|
||||||
|
ChartDataset,
|
||||||
|
ChartTypeRegistry,
|
||||||
|
BubbleDataPoint,
|
||||||
|
Point,
|
||||||
|
} from 'chart.js';
|
||||||
|
import { Chart } from 'vue-chartjs';
|
||||||
|
import type { Iteration } from '@/types/perceptron';
|
||||||
|
import { colors } from '@/types/graphs';
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
cleanedDataset: { label: number; data: { x: number; y: number }[] }[];
|
||||||
|
iterations: Iteration[];
|
||||||
|
activationFunction: (x: number) => number;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const farLeftDataPointX = computed(() => {
|
||||||
|
if (props.cleanedDataset.length === 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const minX = Math.min(...props.cleanedDataset.flatMap((d) => d.data.map((point) => point.x)));
|
||||||
|
return minX;
|
||||||
|
});
|
||||||
|
const farRightDataPointX = computed(() => {
|
||||||
|
if (props.cleanedDataset.length === 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const maxX = Math.max(...props.cleanedDataset.flatMap((d) => d.data.map((point) => point.x)));
|
||||||
|
return maxX;
|
||||||
|
});
|
||||||
|
|
||||||
|
function getPerceptronDecisionBoundaryDataset(
|
||||||
|
networkWeights: number[][][],
|
||||||
|
activationFunction: (x: number) => number = (x) => x,
|
||||||
|
): ChartDataset<
|
||||||
|
keyof ChartTypeRegistry,
|
||||||
|
number | Point | [number, number] | BubbleDataPoint | null
|
||||||
|
> {
|
||||||
|
const label = 'Ligne de décision du Perceptron';
|
||||||
|
console.log('Calculating decision boundary with weights:', networkWeights);
|
||||||
|
|
||||||
|
if (
|
||||||
|
networkWeights.length == 1 &&
|
||||||
|
networkWeights[0].length == 1 &&
|
||||||
|
networkWeights[0][0].length == 3
|
||||||
|
) {
|
||||||
|
// Unique, 3 weights perceptron
|
||||||
|
const perceptronWeights = networkWeights[0][0]; // We take the unique
|
||||||
|
|
||||||
|
function perceptronLine(x: number): number {
|
||||||
|
// w0 + w1*x + w2*y = 0 => y = -(w1/w2)*x - w0/w2
|
||||||
|
return -(perceptronWeights[1] / perceptronWeights[2]) * x - perceptronWeights[0] / perceptronWeights[2];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple line
|
||||||
|
return {
|
||||||
|
type: 'line',
|
||||||
|
label: label,
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
x: farLeftDataPointX.value - 1,
|
||||||
|
y: perceptronLine(farLeftDataPointX.value - 1),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x: farRightDataPointX.value + 1,
|
||||||
|
y: perceptronLine(farRightDataPointX.value + 1),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
borderColor: '#FFF',
|
||||||
|
borderWidth: 2,
|
||||||
|
pointRadius: 0,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
function forward(x1: number, x2: number): number {
|
||||||
|
let activations: number[] = [x1, x2];
|
||||||
|
|
||||||
|
for (const layer of networkWeights) {
|
||||||
|
const nextActivations: number[] = [];
|
||||||
|
|
||||||
|
for (const neuron of layer) {
|
||||||
|
const bias = neuron[0];
|
||||||
|
const weights = neuron.slice(1);
|
||||||
|
|
||||||
|
let sum = bias;
|
||||||
|
|
||||||
|
for (let i = 0; i < weights.length; i++) {
|
||||||
|
sum += weights[i] * activations[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
const activated = activationFunction(sum);
|
||||||
|
|
||||||
|
nextActivations.push(activated);
|
||||||
|
}
|
||||||
|
|
||||||
|
activations = nextActivations;
|
||||||
|
}
|
||||||
|
|
||||||
|
return activations[0]; // on suppose sortie unique
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------- 2️⃣ Échantillonnage grille --------
|
||||||
|
const decisionBoundary: Point[] = [];
|
||||||
|
const min = -2;
|
||||||
|
const max = 2;
|
||||||
|
const step = 0.03;
|
||||||
|
const epsilon = 0.01;
|
||||||
|
|
||||||
|
for (let x = min; x <= max; x += step) {
|
||||||
|
for (let y = min; y <= max; y += step) {
|
||||||
|
const value = forward(x, y);
|
||||||
|
|
||||||
|
if (Math.abs(value) < epsilon) {
|
||||||
|
decisionBoundary.push({ x, y });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------- 3️⃣ Dataset ChartJS --------
|
||||||
|
return {
|
||||||
|
type: 'scatter',
|
||||||
|
label: label,
|
||||||
|
data: decisionBoundary,
|
||||||
|
backgroundColor: '#FFFFFF',
|
||||||
|
pointRadius: 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Chart
|
||||||
|
v-if="props.cleanedDataset.length > 0 || props.iterations.length > 0"
|
||||||
|
class="flex"
|
||||||
|
:options="{
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: true,
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
position: 'top',
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: 'Ligne de décision du Perceptron',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
layout: {
|
||||||
|
padding: {
|
||||||
|
left: 10,
|
||||||
|
right: 10,
|
||||||
|
top: 10,
|
||||||
|
bottom: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
type: 'linear',
|
||||||
|
position: 'bottom',
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
type: 'linear',
|
||||||
|
position: 'left',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}"
|
||||||
|
:data="{
|
||||||
|
datasets: [
|
||||||
|
// Points from the dataset
|
||||||
|
...props.cleanedDataset.map((dataset, index) => ({
|
||||||
|
type: 'scatter',
|
||||||
|
label: `Label ${dataset.label}`,
|
||||||
|
data: dataset.data,
|
||||||
|
backgroundColor:
|
||||||
|
colors[index] || '#AAA',
|
||||||
|
})),
|
||||||
|
|
||||||
|
// Perceptron decision boundary
|
||||||
|
getPerceptronDecisionBoundaryDataset(
|
||||||
|
props.iterations.length > 0
|
||||||
|
? props.iterations[props.iterations.length - 1].weights
|
||||||
|
: [[[0, 0, 0]]],
|
||||||
|
props.activationFunction,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
84
resources/js/components/PerceptronIterationsErrorsGraph.vue
Normal file
84
resources/js/components/PerceptronIterationsErrorsGraph.vue
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { ChartData } from 'chart.js';
|
||||||
|
import { Bar } from 'vue-chartjs';
|
||||||
|
import { colors } from '@/types/graphs';
|
||||||
|
import type { Iteration } from '@/types/perceptron';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
iterations: Iteration[];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the datasets of the iterations with the form { label: `Exemple ${exampleIndex}`, data: [error for iteration 1, error for iteration 2, ...] }
|
||||||
|
*/
|
||||||
|
function getPerceptronErrorsPerIteration(): ChartData<
|
||||||
|
'bar',
|
||||||
|
(number | [number, number] | null)[]
|
||||||
|
>[] {
|
||||||
|
const datasets: ChartData<'bar', (number | [number, number] | null)[]>[] =
|
||||||
|
[];
|
||||||
|
|
||||||
|
const backgroundColors = colors;
|
||||||
|
|
||||||
|
props.iterations.forEach((iteration) => {
|
||||||
|
const exampleLabel = `Exemple ${iteration.exampleIndex}`;
|
||||||
|
let dataset = datasets.find((d) => d.label === exampleLabel);
|
||||||
|
if (!dataset) {
|
||||||
|
dataset = {
|
||||||
|
label: exampleLabel,
|
||||||
|
data: [],
|
||||||
|
backgroundColor:
|
||||||
|
backgroundColors[
|
||||||
|
iteration.exampleIndex % backgroundColors.length
|
||||||
|
],
|
||||||
|
};
|
||||||
|
datasets.push(dataset);
|
||||||
|
}
|
||||||
|
dataset.data.push(iteration.error);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sort dataset by label (Exemple 0, Exemple 1, ...)
|
||||||
|
datasets.sort((a, b) => {
|
||||||
|
const aIndex = parseInt(a.label.split(' ')[1]);
|
||||||
|
const bIndex = parseInt(b.label.split(' ')[1]);
|
||||||
|
return aIndex - bIndex;
|
||||||
|
});
|
||||||
|
|
||||||
|
return datasets;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Bar
|
||||||
|
class="flex"
|
||||||
|
:options="{
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: true,
|
||||||
|
plugins: {
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: 'Nombre d\'erreurs par itération',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
stacked: true,
|
||||||
|
min: 0,
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
stacked: true,
|
||||||
|
beginAtZero: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}"
|
||||||
|
:data="{
|
||||||
|
labels: props.iterations.reduce((labels, iteration) => {
|
||||||
|
if (!labels.includes(`Itération ${iteration.iteration}`)) {
|
||||||
|
labels.push(`Itération ${iteration.iteration}`);
|
||||||
|
}
|
||||||
|
return labels;
|
||||||
|
}, [] as string[]),
|
||||||
|
datasets: getPerceptronErrorsPerIteration(),
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
215
resources/js/components/PerceptronSetup.vue
Normal file
215
resources/js/components/PerceptronSetup.vue
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
// import { Form } from '@inertiajs/vue3';
|
||||||
|
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
} from '@/components/ui/form';
|
||||||
|
import {
|
||||||
|
NativeSelect,
|
||||||
|
NativeSelectOption,
|
||||||
|
} from '@/components/ui/native-select';
|
||||||
|
import type {
|
||||||
|
Dataset,
|
||||||
|
InitializationMethod,
|
||||||
|
PerceptronType,
|
||||||
|
} from '@/types/perceptron';
|
||||||
|
import Button from './ui/button/Button.vue';
|
||||||
|
import Card from './ui/card/Card.vue';
|
||||||
|
import CardContent from './ui/card/CardContent.vue';
|
||||||
|
import CardHeader from './ui/card/CardHeader.vue';
|
||||||
|
import CardTitle from './ui/card/CardTitle.vue';
|
||||||
|
import Input from './ui/input/Input.vue';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
type: PerceptronType;
|
||||||
|
datasets: Dataset[];
|
||||||
|
selectedDataset: string;
|
||||||
|
initializationMethod: InitializationMethod;
|
||||||
|
minError: number;
|
||||||
|
defaultLearningRate: number;
|
||||||
|
sessionId: string;
|
||||||
|
defaultMaxIterations: number;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const selectedDatasetCopy = ref(props.selectedDataset);
|
||||||
|
const selectedMethod = ref(props.initializationMethod);
|
||||||
|
const minError = ref(props.minError);
|
||||||
|
const learningRate = ref(props.defaultLearningRate);
|
||||||
|
const maxIterations = ref(props.defaultMaxIterations);
|
||||||
|
|
||||||
|
watch(selectedDatasetCopy, (newvalue) => {
|
||||||
|
const selectedDatasetCopy = props.datasets.find(
|
||||||
|
(dataset) => dataset.label === newvalue
|
||||||
|
) || null;
|
||||||
|
|
||||||
|
let defaultLearningRate = props.defaultLearningRate;
|
||||||
|
if (selectedDatasetCopy && selectedDatasetCopy.defaultLearningRate !== undefined) {
|
||||||
|
defaultLearningRate = selectedDatasetCopy.defaultLearningRate;
|
||||||
|
}
|
||||||
|
learningRate.value = defaultLearningRate;
|
||||||
|
maxIterations.value = props.defaultMaxIterations;
|
||||||
|
})
|
||||||
|
|
||||||
|
const trainingId = ref<string>('');
|
||||||
|
|
||||||
|
function startTraining() {
|
||||||
|
if (!selectedDatasetCopy.value) {
|
||||||
|
alert('Veuillez sélectionner un dataset avant de lancer l\'entraînement.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
trainingId.value = `${props.sessionId}-${Date.now()}`; // Unique training ID based on session and timestamp
|
||||||
|
emit('update:trainingId', trainingId.value); // Emit the training ID to the parent component
|
||||||
|
|
||||||
|
fetch('/api/perceptron/run', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
credentials: 'same-origin',
|
||||||
|
body: JSON.stringify({
|
||||||
|
type: 'simple',
|
||||||
|
dataset: selectedDatasetCopy.value,
|
||||||
|
weight_init_method: selectedMethod.value,
|
||||||
|
min_error: 0.01,
|
||||||
|
learning_rate: learningRate.value,
|
||||||
|
session_id: props.sessionId,
|
||||||
|
training_id: trainingId.value,
|
||||||
|
max_iterations: maxIterations.value,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Network response was not ok');
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then((data) => {
|
||||||
|
console.log('Perceptron training started:', data);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Error starting perceptron training:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:selectedDataset', 'update:trainingId']);
|
||||||
|
watch(selectedDatasetCopy, (newValue) => {
|
||||||
|
emit('update:selectedDataset', newValue);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Configuration du Perceptron</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<Form
|
||||||
|
class="grid auto-cols-max grid-flow-row grid-cols-1 gap-4 space-y-6 md:grid-cols-2"
|
||||||
|
>
|
||||||
|
<!-- DATASET -->
|
||||||
|
<FormField name="dataset">
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Dataset</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<NativeSelect
|
||||||
|
name="dataset"
|
||||||
|
id="dataset-select"
|
||||||
|
v-model="selectedDatasetCopy"
|
||||||
|
>
|
||||||
|
<NativeSelectOption value="" disabled
|
||||||
|
>Sélectionnez un dataset</NativeSelectOption
|
||||||
|
>
|
||||||
|
<NativeSelectOption
|
||||||
|
v-for="dataset in props.datasets"
|
||||||
|
v-bind:key="dataset.label"
|
||||||
|
:value="dataset.label"
|
||||||
|
>
|
||||||
|
{{ dataset.label }}
|
||||||
|
</NativeSelectOption>
|
||||||
|
</NativeSelect>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<!-- DEFAULT WEIGHTS -->
|
||||||
|
<FormField name="weight_init_method">
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel
|
||||||
|
>Méthode d'initialisation des poids</FormLabel
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<NativeSelect
|
||||||
|
name="weight_init_method"
|
||||||
|
id="weight_init_method"
|
||||||
|
v-model="selectedMethod"
|
||||||
|
>
|
||||||
|
<NativeSelectOption
|
||||||
|
v-for="method in ['zeros', 'random']"
|
||||||
|
v-bind:key="method"
|
||||||
|
:value="method"
|
||||||
|
>
|
||||||
|
{{ method }}
|
||||||
|
</NativeSelectOption>
|
||||||
|
</NativeSelect>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<!-- MIN ERROR -->
|
||||||
|
<FormField name="min_error" v-if="props.type !== 'simple'">
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Erreur minimale</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
v-model="minError"
|
||||||
|
min="0"
|
||||||
|
step="0.001"
|
||||||
|
class="w-min"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<!-- LEARNING RATE -->
|
||||||
|
<FormField name="learning_rate">
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Taux d'apprentissage</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
v-model="learningRate"
|
||||||
|
min="0"
|
||||||
|
step="0.001"
|
||||||
|
class="w-min"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<!-- MAX ITERATIONS -->
|
||||||
|
<FormField name="max_iterations">
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Nombre maximum d'itérations</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
v-model="maxIterations"
|
||||||
|
min="0"
|
||||||
|
step="1"
|
||||||
|
class="w-min"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
</Form>
|
||||||
|
<Button variant="outline" class="cursor-pointer mt-6" @click="startTraining">Lancer</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
17
resources/js/components/ui/form/FormControl.vue
Normal file
17
resources/js/components/ui/form/FormControl.vue
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { Slot } from "reka-ui"
|
||||||
|
import { useFormField } from "./useFormField"
|
||||||
|
|
||||||
|
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Slot
|
||||||
|
:id="formItemId"
|
||||||
|
data-slot="form-control"
|
||||||
|
:aria-describedby="!error ? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}`"
|
||||||
|
:aria-invalid="!!error"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</Slot>
|
||||||
|
</template>
|
||||||
21
resources/js/components/ui/form/FormDescription.vue
Normal file
21
resources/js/components/ui/form/FormDescription.vue
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { useFormField } from "./useFormField"
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { formDescriptionId } = useFormField()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<p
|
||||||
|
:id="formDescriptionId"
|
||||||
|
data-slot="form-description"
|
||||||
|
:class="cn('text-muted-foreground text-sm', props.class)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
23
resources/js/components/ui/form/FormItem.vue
Normal file
23
resources/js/components/ui/form/FormItem.vue
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { useId } from "reka-ui"
|
||||||
|
import { provide } from "vue"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { FORM_ITEM_INJECTION_KEY } from "./injectionKeys"
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const id = useId()
|
||||||
|
provide(FORM_ITEM_INJECTION_KEY, id)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
data-slot="form-item"
|
||||||
|
:class="cn('grid gap-2', props.class)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
25
resources/js/components/ui/form/FormLabel.vue
Normal file
25
resources/js/components/ui/form/FormLabel.vue
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { LabelProps } from "reka-ui"
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { Label } from '@/components/ui/label'
|
||||||
|
import { useFormField } from "./useFormField"
|
||||||
|
|
||||||
|
const props = defineProps<LabelProps & { class?: HTMLAttributes["class"] }>()
|
||||||
|
|
||||||
|
const { error, formItemId } = useFormField()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Label
|
||||||
|
data-slot="form-label"
|
||||||
|
:data-error="!!error"
|
||||||
|
:class="cn(
|
||||||
|
'data-[error=true]:text-destructive',
|
||||||
|
props.class,
|
||||||
|
)"
|
||||||
|
:for="formItemId"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</Label>
|
||||||
|
</template>
|
||||||
23
resources/js/components/ui/form/FormMessage.vue
Normal file
23
resources/js/components/ui/form/FormMessage.vue
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { ErrorMessage } from "vee-validate"
|
||||||
|
import { toValue } from "vue"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { useFormField } from "./useFormField"
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { name, formMessageId } = useFormField()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ErrorMessage
|
||||||
|
:id="formMessageId"
|
||||||
|
data-slot="form-message"
|
||||||
|
as="p"
|
||||||
|
:name="toValue(name)"
|
||||||
|
:class="cn('text-destructive text-sm', props.class)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
7
resources/js/components/ui/form/index.ts
Normal file
7
resources/js/components/ui/form/index.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export { default as FormControl } from "./FormControl.vue"
|
||||||
|
export { default as FormDescription } from "./FormDescription.vue"
|
||||||
|
export { default as FormItem } from "./FormItem.vue"
|
||||||
|
export { default as FormLabel } from "./FormLabel.vue"
|
||||||
|
export { default as FormMessage } from "./FormMessage.vue"
|
||||||
|
export { FORM_ITEM_INJECTION_KEY } from "./injectionKeys"
|
||||||
|
export { Form, Field as FormField, FieldArray as FormFieldArray } from "vee-validate"
|
||||||
4
resources/js/components/ui/form/injectionKeys.ts
Normal file
4
resources/js/components/ui/form/injectionKeys.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import type { InjectionKey } from "vue"
|
||||||
|
|
||||||
|
export const FORM_ITEM_INJECTION_KEY
|
||||||
|
= Symbol() as InjectionKey<string>
|
||||||
30
resources/js/components/ui/form/useFormField.ts
Normal file
30
resources/js/components/ui/form/useFormField.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { FieldContextKey } from "vee-validate"
|
||||||
|
import { computed, inject } from "vue"
|
||||||
|
import { FORM_ITEM_INJECTION_KEY } from "./injectionKeys"
|
||||||
|
|
||||||
|
export function useFormField() {
|
||||||
|
const fieldContext = inject(FieldContextKey)
|
||||||
|
const fieldItemContext = inject(FORM_ITEM_INJECTION_KEY)
|
||||||
|
|
||||||
|
if (!fieldContext)
|
||||||
|
throw new Error("useFormField should be used within <FormField>")
|
||||||
|
|
||||||
|
const { name, errorMessage: error, meta } = fieldContext
|
||||||
|
const id = fieldItemContext
|
||||||
|
|
||||||
|
const fieldState = {
|
||||||
|
valid: computed(() => meta.valid),
|
||||||
|
isDirty: computed(() => meta.dirty),
|
||||||
|
isTouched: computed(() => meta.touched),
|
||||||
|
error,
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
formItemId: `${id}-form-item`,
|
||||||
|
formDescriptionId: `${id}-form-item-description`,
|
||||||
|
formMessageId: `${id}-form-item-message`,
|
||||||
|
...fieldState,
|
||||||
|
}
|
||||||
|
}
|
||||||
50
resources/js/components/ui/native-select/NativeSelect.vue
Normal file
50
resources/js/components/ui/native-select/NativeSelect.vue
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { AcceptableValue } from "reka-ui"
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { reactiveOmit, useVModel } from "@vueuse/core"
|
||||||
|
import { ChevronDownIcon } from "lucide-vue-next"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
inheritAttrs: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const props = defineProps<{ modelValue?: AcceptableValue | AcceptableValue[], class?: HTMLAttributes["class"] }>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
"update:modelValue": AcceptableValue
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const modelValue = useVModel(props, "modelValue", emit, {
|
||||||
|
passive: true,
|
||||||
|
defaultValue: "",
|
||||||
|
})
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "class")
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="group/native-select relative w-fit has-[select:disabled]:opacity-50"
|
||||||
|
data-slot="native-select-wrapper"
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
v-bind="{ ...$attrs, ...delegatedProps }"
|
||||||
|
v-model="modelValue"
|
||||||
|
data-slot="native-select"
|
||||||
|
:class="cn(
|
||||||
|
'border-input placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 dark:hover:bg-input/50 h-9 w-full min-w-0 appearance-none rounded-md border bg-transparent px-3 py-2 pr-9 text-sm shadow-xs transition-[color,box-shadow] outline-none disabled:pointer-events-none disabled:cursor-not-allowed',
|
||||||
|
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
|
||||||
|
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
|
||||||
|
props.class,
|
||||||
|
)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</select>
|
||||||
|
<ChevronDownIcon
|
||||||
|
class="text-muted-foreground pointer-events-none absolute top-[25%] right-3.5 size-4 -translate-y-1/2 opacity-50 select-none"
|
||||||
|
aria-hidden="true"
|
||||||
|
data-slot="native-select-icon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<!-- @fallthroughAttributes true -->
|
||||||
|
<!-- @strictTemplates true -->
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<{ class?: HTMLAttributes["class"] }>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<optgroup data-slot="native-select-optgroup" :class="cn('bg-popover text-popover-foreground', props.class)">
|
||||||
|
<slot />
|
||||||
|
</optgroup>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<!-- @fallthroughAttributes true -->
|
||||||
|
<!-- @strictTemplates true -->
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<{ class?: HTMLAttributes["class"] }>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<option data-slot="native-select-option" :class="cn('bg-popover text-popover-foreground', props.class)">
|
||||||
|
<slot />
|
||||||
|
</option>
|
||||||
|
</template>
|
||||||
3
resources/js/components/ui/native-select/index.ts
Normal file
3
resources/js/components/ui/native-select/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export { default as NativeSelect } from "./NativeSelect.vue"
|
||||||
|
export { default as NativeSelectOptGroup } from "./NativeSelectOptGroup.vue"
|
||||||
|
export { default as NativeSelectOption } from "./NativeSelectOption.vue"
|
||||||
@@ -11,15 +11,19 @@ import {
|
|||||||
LinearScale,
|
LinearScale,
|
||||||
PointElement,
|
PointElement,
|
||||||
LineElement,
|
LineElement,
|
||||||
ChartDataset,
|
|
||||||
ChartTypeRegistry,
|
|
||||||
BubbleDataPoint,
|
|
||||||
Point,
|
|
||||||
ChartData,
|
|
||||||
} from 'chart.js';
|
} from 'chart.js';
|
||||||
import { onMounted, ref } from 'vue';
|
import { computed, onMounted, ref, watch } from 'vue';
|
||||||
import { Bar, Chart, Line } from 'vue-chartjs';
|
|
||||||
import LinkHeader from '@/components/LinkHeader.vue';
|
import LinkHeader from '@/components/LinkHeader.vue';
|
||||||
|
import type {
|
||||||
|
Dataset,
|
||||||
|
InitializationMethod,
|
||||||
|
Iteration,
|
||||||
|
PerceptronType,
|
||||||
|
} from '@/types/perceptron';
|
||||||
|
import IterationTable from '../components/IterationTable.vue';
|
||||||
|
import PerceptronDecisionGraph from '../components/PerceptronDecisionGraph.vue';
|
||||||
|
import PerceptronIterationsErrorsGraph from '../components/PerceptronIterationsErrorsGraph.vue';
|
||||||
|
import PerceptronSetup from '../components/PerceptronSetup.vue';
|
||||||
|
|
||||||
ChartJS.register(
|
ChartJS.register(
|
||||||
Title,
|
Title,
|
||||||
@@ -36,11 +40,52 @@ ChartJS.defaults.color = '#FFF';
|
|||||||
ChartJS.defaults.backgroundColor = '#AAA';
|
ChartJS.defaults.backgroundColor = '#AAA';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
type: string;
|
type: PerceptronType;
|
||||||
dataset: number[][];
|
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
|
datasets: Dataset[];
|
||||||
|
minError: number;
|
||||||
|
learningRate: number;
|
||||||
|
maxIterations: number;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const selectedDatasetName = ref<string>('');
|
||||||
|
const dataset = computed<number[][]>(() => {
|
||||||
|
const selected = props.datasets.find(
|
||||||
|
(d) => d.label === selectedDatasetName.value,
|
||||||
|
);
|
||||||
|
|
||||||
|
return selected ? selected.data : [];
|
||||||
|
});
|
||||||
|
const cleanedDataset = computed<
|
||||||
|
{
|
||||||
|
label: number;
|
||||||
|
data: { x: number; y: number }[];
|
||||||
|
}[]
|
||||||
|
>(() => {
|
||||||
|
if (!dataset.value) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleanedDataset: {
|
||||||
|
label: number;
|
||||||
|
data: { x: number; y: number }[];
|
||||||
|
}[] = [];
|
||||||
|
// Separate data into each dataset based on value of the last column (label)
|
||||||
|
dataset.value.forEach((row) => {
|
||||||
|
const label = row[row.length - 1];
|
||||||
|
const dataPoint = { x: row[0], y: row[1] };
|
||||||
|
|
||||||
|
let dataset = cleanedDataset.find((d) => d.label === label);
|
||||||
|
if (!dataset) {
|
||||||
|
dataset = { label, data: [] };
|
||||||
|
cleanedDataset.push(dataset);
|
||||||
|
}
|
||||||
|
dataset.data.push(dataPoint);
|
||||||
|
});
|
||||||
|
return cleanedDataset;
|
||||||
|
});
|
||||||
|
const initializationMethod = ref<InitializationMethod>('zeros');
|
||||||
|
|
||||||
console.log('Session ID:', props.sessionId);
|
console.log('Session ID:', props.sessionId);
|
||||||
|
|
||||||
useEcho(
|
useEcho(
|
||||||
@@ -57,182 +102,71 @@ useEcho(
|
|||||||
[{}],
|
[{}],
|
||||||
'public',
|
'public',
|
||||||
);
|
);
|
||||||
|
useEcho(
|
||||||
|
`${props.sessionId}-perceptron-training`,
|
||||||
|
'PerceptronInitialization',
|
||||||
|
perceptroninitialization,
|
||||||
|
[{}],
|
||||||
|
'public',
|
||||||
|
);
|
||||||
|
|
||||||
onMounted(() => {
|
const iterations = ref<Iteration[]>([]);
|
||||||
// make a POST request to start the perceptron training
|
|
||||||
fetch('/api/perceptron/run', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
credentials: 'same-origin',
|
|
||||||
body: JSON.stringify({
|
|
||||||
type: 'simple',
|
|
||||||
min_error: 0.01,
|
|
||||||
session_id: props.sessionId,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
.then((response) => {
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('Network response was not ok');
|
|
||||||
}
|
|
||||||
return response.json();
|
|
||||||
})
|
|
||||||
.then((data) => {
|
|
||||||
console.log('Perceptron training started:', data);
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error('Error starting perceptron training:', error);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const iterations = ref<
|
|
||||||
{
|
|
||||||
iteration: number;
|
|
||||||
exampleIndex: number;
|
|
||||||
weights: number[];
|
|
||||||
error: number;
|
|
||||||
}[]
|
|
||||||
>([]);
|
|
||||||
|
|
||||||
|
const trainingId = ref<string>('');
|
||||||
function percpetronIteration(data: any) {
|
function percpetronIteration(data: any) {
|
||||||
console.log('Received perceptron iteration data:', data);
|
console.log('Received perceptron iteration data:', data);
|
||||||
iterations.value.push({
|
if (data.trainingId !== trainingId.value) {
|
||||||
iteration: data.iteration,
|
console.warn(
|
||||||
exampleIndex: data.exampleIndex,
|
`Received iteration for training ID ${data.trainingId}, but current training ID is ${trainingId.value}. Ignoring this iteration.`
|
||||||
weights: data.synaptic_weights,
|
);
|
||||||
error: data.error,
|
return;
|
||||||
});
|
}
|
||||||
|
iterations.value.push(...data.iterations);
|
||||||
}
|
}
|
||||||
|
|
||||||
const trainingEnded = ref(false);
|
const trainingEnded = ref(false);
|
||||||
const trainingEndReason = ref('');
|
const trainingEndReason = ref('');
|
||||||
function perceptronTrainingEnded(data: any) {
|
function perceptronTrainingEnded(data: any) {
|
||||||
console.log('Perceptron training ended:', data);
|
console.log('Perceptron training ended:', data);
|
||||||
|
if (data.trainingId !== trainingId.value) {
|
||||||
|
console.warn(
|
||||||
|
`Received training ended event for training ID ${data.trainingId}, but current training ID is ${trainingId.value}. Ignoring this event.`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
trainingEnded.value = true;
|
trainingEnded.value = true;
|
||||||
trainingEndReason.value = data.reason;
|
trainingEndReason.value = data.reason;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Separate data into each dataset based on value of the last column (label)
|
const activationFunction = ref<string>('');
|
||||||
const cleanedDataset: { label: number; data: { x: number; y: number }[] }[] =
|
function perceptroninitialization(data: any) {
|
||||||
[];
|
console.log('Perceptron training initialized:', data);
|
||||||
props.dataset.forEach((row) => {
|
if (data.trainingId !== trainingId.value) {
|
||||||
const label = row[row.length - 1];
|
console.warn(
|
||||||
const dataPoint = { x: row[0], y: row[1] };
|
`Received initialization event for training ID ${data.trainingId}, but current training ID is ${trainingId.value}. Ignoring this event.`
|
||||||
|
);
|
||||||
let dataset = cleanedDataset.find((d) => d.label === label);
|
return;
|
||||||
if (!dataset) {
|
|
||||||
dataset = { label, data: [] };
|
|
||||||
cleanedDataset.push(dataset);
|
|
||||||
}
|
}
|
||||||
dataset.data.push(dataPoint);
|
activationFunction.value = data.activation_function;
|
||||||
});
|
}
|
||||||
|
function getActivationFunction(type: string): (x: number) => number {
|
||||||
function getPerceptronDecisionBoundaryDataset(
|
switch (type) {
|
||||||
weights: number[],
|
case 'step':
|
||||||
): ChartDataset<
|
return (x) => (x >= 0 ? 1 : 0);
|
||||||
keyof ChartTypeRegistry,
|
case 'sigmoid':
|
||||||
number | Point | [number, number] | BubbleDataPoint | null
|
return (x) => 1 / (1 + Math.exp(-x));
|
||||||
> {
|
case 'tanh':
|
||||||
const label = 'Ligne de décision du Perceptron';
|
return (x) => Math.tanh(x);
|
||||||
|
default:
|
||||||
if (weights.length == 3) {
|
return (x) => x; // Identity function as fallback
|
||||||
// Simple line
|
|
||||||
return {
|
|
||||||
type: 'line',
|
|
||||||
label: label,
|
|
||||||
data: [
|
|
||||||
{
|
|
||||||
x: -1,
|
|
||||||
y:
|
|
||||||
-(weights[1] / weights[2]) * -1 -
|
|
||||||
weights[0] / weights[2],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
x: 2,
|
|
||||||
y: -(weights[1] / weights[2]) * 2 - weights[0] / weights[2],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
borderColor: '#FFF',
|
|
||||||
borderWidth: 2,
|
|
||||||
pointRadius: 0,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
type: 'scatter',
|
|
||||||
label: label,
|
|
||||||
data: (() => {
|
|
||||||
const decisionBoundary = [];
|
|
||||||
const latestWeights =
|
|
||||||
iterations.value.length > 0
|
|
||||||
? iterations.value[iterations.value.length - 1].weights
|
|
||||||
: [0, 0, 0]; // default weights if no iterations yet
|
|
||||||
for (let x = -2; x <= 2; x += 0.05) {
|
|
||||||
for (let y = -2; y <= 2; y += 0.05) {
|
|
||||||
let value = 0;
|
|
||||||
for (let i = 0; i < latestWeights.length - 1; i++) {
|
|
||||||
value += latestWeights[i] * (i === 0 ? x : y); // TODO : Fix formula
|
|
||||||
}
|
|
||||||
value += latestWeights[0]; // bias
|
|
||||||
|
|
||||||
if (Math.abs(value) < 0.003) {
|
|
||||||
decisionBoundary.push({ x: x, y: y });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return decisionBoundary;
|
|
||||||
})(),
|
|
||||||
backgroundColor: '#FFF',
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
function resetTraining() {
|
||||||
* Return the datasets of the iterations with the form { label: `Exemple ${exampleIndex}`, data: [error for iteration 1, error for iteration 2, ...] }
|
iterations.value = [];
|
||||||
*/
|
trainingEnded.value = false;
|
||||||
function getPerceptronErrorsPerIteration(): ChartData<
|
trainingEndReason.value = '';
|
||||||
'bar',
|
activationFunction.value = '';
|
||||||
(number | [number, number] | null)[]
|
|
||||||
>[] {
|
|
||||||
const datasets: ChartData<'bar', (number | [number, number] | null)[]>[] =
|
|
||||||
[];
|
|
||||||
|
|
||||||
const backgroundColors = [
|
|
||||||
'#FF6384',
|
|
||||||
'#36A2EB',
|
|
||||||
'#FFCE56',
|
|
||||||
'#4BC0C0',
|
|
||||||
'#9D5C5C',
|
|
||||||
'#8B4513',
|
|
||||||
'#2E8B57',
|
|
||||||
'#800080',
|
|
||||||
];
|
|
||||||
|
|
||||||
iterations.value.forEach((iteration) => {
|
|
||||||
const exampleLabel = `Exemple ${iteration.exampleIndex}`;
|
|
||||||
let dataset = datasets.find((d) => d.label === exampleLabel);
|
|
||||||
if (!dataset) {
|
|
||||||
dataset = {
|
|
||||||
label: exampleLabel,
|
|
||||||
data: [],
|
|
||||||
backgroundColor:
|
|
||||||
backgroundColors[
|
|
||||||
iteration.exampleIndex % backgroundColors.length
|
|
||||||
],
|
|
||||||
};
|
|
||||||
datasets.push(dataset);
|
|
||||||
}
|
|
||||||
dataset.data.push(iteration.error);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Sort dataset by label (Exemple 0, Exemple 1, ...)
|
|
||||||
datasets.sort((a, b) => {
|
|
||||||
const aIndex = parseInt(a.label.split(' ')[1]);
|
|
||||||
const bIndex = parseInt(b.label.split(' ')[1]);
|
|
||||||
return aIndex - bIndex;
|
|
||||||
});
|
|
||||||
|
|
||||||
return datasets;
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -240,154 +174,51 @@ function getPerceptronErrorsPerIteration(): ChartData<
|
|||||||
<Head title="Perceptron Viewer"></Head>
|
<Head title="Perceptron Viewer"></Head>
|
||||||
<main class="space-y-6">
|
<main class="space-y-6">
|
||||||
<LinkHeader class="w-full" />
|
<LinkHeader class="w-full" />
|
||||||
|
<PerceptronSetup
|
||||||
|
:type="props.type"
|
||||||
|
:datasets="props.datasets"
|
||||||
|
:selectedDataset="selectedDatasetName"
|
||||||
|
:initializationMethod="initializationMethod"
|
||||||
|
:minError="props.minError"
|
||||||
|
:sessionId="props.sessionId"
|
||||||
|
:defaultLearningRate="props.learningRate"
|
||||||
|
:defaultMaxIterations="props.maxIterations"
|
||||||
|
@update:selected-dataset="
|
||||||
|
(newValue) => {
|
||||||
|
selectedDatasetName = newValue;
|
||||||
|
}
|
||||||
|
"
|
||||||
|
@update:training-id="
|
||||||
|
(newValue) => {
|
||||||
|
trainingId = newValue;
|
||||||
|
resetTraining();
|
||||||
|
}"
|
||||||
|
/>
|
||||||
<div
|
<div
|
||||||
class="align-items-start justify-content-center flex h-full min-h-dvh max-w-dvw"
|
class="align-items-start justify-content-center flex h-full min-h-dvh max-w-dvw"
|
||||||
|
v-if="selectedDatasetName || iterations.length > 0"
|
||||||
>
|
>
|
||||||
<div class="max-h-full w-full overflow-y-scroll">
|
<div class="max-h-full w-full overflow-y-scroll">
|
||||||
<table
|
<IterationTable
|
||||||
class="table w-full border-collapse border border-gray-300"
|
:iterations="iterations"
|
||||||
>
|
:trainingEnded="trainingEnded"
|
||||||
<tr class="text-left" v-if="iterations.length > 0">
|
:trainingEndReason="trainingEndReason"
|
||||||
<th>Itération</th>
|
/>
|
||||||
<th>Exemple</th>
|
|
||||||
<th
|
|
||||||
v-for="(weight, index) in iterations[0].weights"
|
|
||||||
v-bind:key="index"
|
|
||||||
>
|
|
||||||
X<sub>{{ index }}</sub>
|
|
||||||
</th>
|
|
||||||
<th>Erreur</th>
|
|
||||||
</tr>
|
|
||||||
<tr
|
|
||||||
v-for="(iteration, index) in iterations"
|
|
||||||
v-bind:key="index"
|
|
||||||
:class="{
|
|
||||||
'bg-gray-900': iteration.iteration % 2 === 0,
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<td>{{ iteration.iteration }}</td>
|
|
||||||
<td>{{ iteration.exampleIndex }}</td>
|
|
||||||
<td
|
|
||||||
v-for="(weight, index) in iteration.weights"
|
|
||||||
v-bind:key="index"
|
|
||||||
>
|
|
||||||
{{ weight.toFixed(2) }}
|
|
||||||
</td>
|
|
||||||
<td>{{ iteration.error.toFixed(2) }}</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<tr v-if="trainingEnded" class="bg-red-900 text-center">
|
|
||||||
<td colspan="100%">
|
|
||||||
<strong>Entraînement terminé :</strong>
|
|
||||||
{{ trainingEndReason }}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="h-full w-full">
|
<div class="sticky top-0 h-full w-full">
|
||||||
<div>
|
<div>
|
||||||
<Chart
|
<PerceptronDecisionGraph
|
||||||
class="flex"
|
:cleanedDataset="cleanedDataset"
|
||||||
:options="{
|
:iterations="iterations"
|
||||||
responsive: true,
|
:activation-function="
|
||||||
maintainAspectRatio: true,
|
getActivationFunction(activationFunction)
|
||||||
plugins: {
|
"
|
||||||
legend: {
|
|
||||||
position: 'top',
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
display: true,
|
|
||||||
text: 'Ligne de décision du Perceptron',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
layout: {
|
|
||||||
padding: {
|
|
||||||
left: 10,
|
|
||||||
right: 10,
|
|
||||||
top: 10,
|
|
||||||
bottom: 10,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
scales: {
|
|
||||||
x: {
|
|
||||||
type: 'linear',
|
|
||||||
position: 'bottom',
|
|
||||||
},
|
|
||||||
y: {
|
|
||||||
type: 'linear',
|
|
||||||
position: 'left',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}"
|
|
||||||
:data="{
|
|
||||||
datasets: [
|
|
||||||
// Points from the dataset
|
|
||||||
...cleanedDataset.map((dataset, index) => ({
|
|
||||||
type: 'scatter',
|
|
||||||
label: `Classe ${dataset.label}`,
|
|
||||||
data: dataset.data,
|
|
||||||
backgroundColor:
|
|
||||||
[
|
|
||||||
'#FF6384',
|
|
||||||
'#36A2EB',
|
|
||||||
'#FFCE56',
|
|
||||||
'#4BC0C0',
|
|
||||||
'#9D5C5C',
|
|
||||||
'#8B4513',
|
|
||||||
'#2E8B57',
|
|
||||||
'#800080',
|
|
||||||
][index] || '#AAA',
|
|
||||||
})),
|
|
||||||
|
|
||||||
// Perceptron decision boundary
|
|
||||||
getPerceptronDecisionBoundaryDataset(
|
|
||||||
iterations.length > 0
|
|
||||||
? iterations[iterations.length - 1]
|
|
||||||
.weights
|
|
||||||
: [0, 0, 0],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
}"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Bar
|
<PerceptronIterationsErrorsGraph
|
||||||
class="flex"
|
:iterations="iterations"
|
||||||
:options="{
|
v-if="iterations.length > 0"
|
||||||
responsive: true,
|
|
||||||
maintainAspectRatio: true,
|
|
||||||
plugins: {
|
|
||||||
title: {
|
|
||||||
display: true,
|
|
||||||
text: 'Nombre d\'erreurs par itération',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
scales: {
|
|
||||||
x: {
|
|
||||||
stacked: true,
|
|
||||||
min: 0,
|
|
||||||
},
|
|
||||||
y: {
|
|
||||||
stacked: true,
|
|
||||||
beginAtZero: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}"
|
|
||||||
:data="{
|
|
||||||
labels: iterations.reduce((labels, iteration) => {
|
|
||||||
if (
|
|
||||||
!labels.includes(
|
|
||||||
`Itération ${iteration.iteration}`,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
labels.push(
|
|
||||||
`Itération ${iteration.iteration}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return labels;
|
|
||||||
}, [] as string[]),
|
|
||||||
datasets: getPerceptronErrorsPerIteration(),
|
|
||||||
}"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
34
resources/js/types/graphs.ts
Normal file
34
resources/js/types/graphs.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
|
||||||
|
export const colors = [
|
||||||
|
'#FF6384',
|
||||||
|
'#36A2EB',
|
||||||
|
'#FFCE56',
|
||||||
|
'#4BC0C0',
|
||||||
|
'#9D5C5C',
|
||||||
|
'#8B4513',
|
||||||
|
'#2E8B57',
|
||||||
|
'#800080',
|
||||||
|
'#FF4500',
|
||||||
|
'#008080',
|
||||||
|
'#FF1493',
|
||||||
|
'#00CED1',
|
||||||
|
'#FFD700',
|
||||||
|
'#ADFF2F',
|
||||||
|
'#FF69B4',
|
||||||
|
'#20B2AA',
|
||||||
|
'#FF6347',
|
||||||
|
'#40E0D0',
|
||||||
|
'#EE82EE',
|
||||||
|
'#F08080',
|
||||||
|
'#00FA9A',
|
||||||
|
'#FFB6C1',
|
||||||
|
'#48D1CC',
|
||||||
|
'#C71585',
|
||||||
|
'#00FF7F',
|
||||||
|
'#FF00FF',
|
||||||
|
'#00FFFF',
|
||||||
|
'#FF8C00',
|
||||||
|
'#7B68EE',
|
||||||
|
'#DC143C',
|
||||||
|
'#00FF00',
|
||||||
|
] as const;
|
||||||
16
resources/js/types/perceptron.ts
Normal file
16
resources/js/types/perceptron.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
export type Iteration = {
|
||||||
|
iteration: number;
|
||||||
|
exampleIndex: number;
|
||||||
|
weights: number[][][];
|
||||||
|
error: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Dataset = {
|
||||||
|
label: string;
|
||||||
|
data: { x: number; y: number }[];
|
||||||
|
defaultLearningRate: number | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type InitializationMethod = 'zeros' | 'random';
|
||||||
|
|
||||||
|
export type PerceptronType = 'simple';
|
||||||
Reference in New Issue
Block a user