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 string $reason,
|
||||
public string $sessionId,
|
||||
public string $trainingId,
|
||||
)
|
||||
{
|
||||
//
|
||||
@@ -41,6 +42,7 @@ class PerceptronTrainingEnded implements ShouldBroadcast
|
||||
{
|
||||
return [
|
||||
'reason' => $this->reason,
|
||||
'trainingId' => $this->trainingId,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
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(
|
||||
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
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
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();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,6 @@
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
interface ISynapticWeights {
|
||||
interface ISynapticWeightsProvider {
|
||||
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;
|
||||
|
||||
class RandomSynapticWeights implements ISynapticWeights {
|
||||
class RandomSynapticWeights implements ISynapticWeightsProvider {
|
||||
public function generate(int $input_size): array
|
||||
{
|
||||
$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": {
|
||||
"@inertiajs/vue3": "^2.3.7",
|
||||
"@vee-validate/zod": "^4.15.1",
|
||||
"@vueuse/core": "^12.8.2",
|
||||
"chart.js": "^4.5.1",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"laravel-vite-plugin": "^2.0.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",
|
||||
"tailwindcss": "^4.1.1",
|
||||
"tw-animate-css": "^1.2.5",
|
||||
"vee-validate": "^4.15.1",
|
||||
"vue": "^3.5.13",
|
||||
"vue-chartjs": "^5.3.3",
|
||||
"vue-input-otp": "^0.3.2"
|
||||
"vue-input-otp": "^0.3.2",
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@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,
|
||||
PointElement,
|
||||
LineElement,
|
||||
ChartDataset,
|
||||
ChartTypeRegistry,
|
||||
BubbleDataPoint,
|
||||
Point,
|
||||
ChartData,
|
||||
} from 'chart.js';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { Bar, Chart, Line } from 'vue-chartjs';
|
||||
import { computed, onMounted, ref, watch } from '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(
|
||||
Title,
|
||||
@@ -36,11 +40,52 @@ ChartJS.defaults.color = '#FFF';
|
||||
ChartJS.defaults.backgroundColor = '#AAA';
|
||||
|
||||
const props = defineProps<{
|
||||
type: string;
|
||||
dataset: number[][];
|
||||
type: PerceptronType;
|
||||
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);
|
||||
|
||||
useEcho(
|
||||
@@ -57,182 +102,71 @@ useEcho(
|
||||
[{}],
|
||||
'public',
|
||||
);
|
||||
useEcho(
|
||||
`${props.sessionId}-perceptron-training`,
|
||||
'PerceptronInitialization',
|
||||
perceptroninitialization,
|
||||
[{}],
|
||||
'public',
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
// 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 iterations = ref<Iteration[]>([]);
|
||||
|
||||
const trainingId = ref<string>('');
|
||||
function percpetronIteration(data: any) {
|
||||
console.log('Received perceptron iteration data:', data);
|
||||
iterations.value.push({
|
||||
iteration: data.iteration,
|
||||
exampleIndex: data.exampleIndex,
|
||||
weights: data.synaptic_weights,
|
||||
error: data.error,
|
||||
});
|
||||
if (data.trainingId !== trainingId.value) {
|
||||
console.warn(
|
||||
`Received iteration for training ID ${data.trainingId}, but current training ID is ${trainingId.value}. Ignoring this iteration.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
iterations.value.push(...data.iterations);
|
||||
}
|
||||
|
||||
const trainingEnded = ref(false);
|
||||
const trainingEndReason = ref('');
|
||||
function perceptronTrainingEnded(data: any) {
|
||||
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;
|
||||
trainingEndReason.value = data.reason;
|
||||
}
|
||||
|
||||
// Separate data into each dataset based on value of the last column (label)
|
||||
const cleanedDataset: { label: number; data: { x: number; y: number }[] }[] =
|
||||
[];
|
||||
props.dataset.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);
|
||||
const activationFunction = ref<string>('');
|
||||
function perceptroninitialization(data: any) {
|
||||
console.log('Perceptron training initialized:', data);
|
||||
if (data.trainingId !== trainingId.value) {
|
||||
console.warn(
|
||||
`Received initialization event for training ID ${data.trainingId}, but current training ID is ${trainingId.value}. Ignoring this event.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
dataset.data.push(dataPoint);
|
||||
});
|
||||
|
||||
function getPerceptronDecisionBoundaryDataset(
|
||||
weights: number[],
|
||||
): ChartDataset<
|
||||
keyof ChartTypeRegistry,
|
||||
number | Point | [number, number] | BubbleDataPoint | null
|
||||
> {
|
||||
const label = 'Ligne de décision du Perceptron';
|
||||
|
||||
if (weights.length == 3) {
|
||||
// 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',
|
||||
};
|
||||
activationFunction.value = data.activation_function;
|
||||
}
|
||||
function getActivationFunction(type: string): (x: number) => number {
|
||||
switch (type) {
|
||||
case 'step':
|
||||
return (x) => (x >= 0 ? 1 : 0);
|
||||
case 'sigmoid':
|
||||
return (x) => 1 / (1 + Math.exp(-x));
|
||||
case 'tanh':
|
||||
return (x) => Math.tanh(x);
|
||||
default:
|
||||
return (x) => x; // Identity function as fallback
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = [
|
||||
'#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;
|
||||
function resetTraining() {
|
||||
iterations.value = [];
|
||||
trainingEnded.value = false;
|
||||
trainingEndReason.value = '';
|
||||
activationFunction.value = '';
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -240,154 +174,51 @@ function getPerceptronErrorsPerIteration(): ChartData<
|
||||
<Head title="Perceptron Viewer"></Head>
|
||||
<main class="space-y-6">
|
||||
<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
|
||||
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">
|
||||
<table
|
||||
class="table w-full border-collapse border border-gray-300"
|
||||
>
|
||||
<tr class="text-left" v-if="iterations.length > 0">
|
||||
<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>
|
||||
<IterationTable
|
||||
:iterations="iterations"
|
||||
:trainingEnded="trainingEnded"
|
||||
:trainingEndReason="trainingEndReason"
|
||||
/>
|
||||
</div>
|
||||
<div class="h-full w-full">
|
||||
<div class="sticky top-0 h-full w-full">
|
||||
<div>
|
||||
<Chart
|
||||
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
|
||||
...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],
|
||||
),
|
||||
],
|
||||
}"
|
||||
<PerceptronDecisionGraph
|
||||
:cleanedDataset="cleanedDataset"
|
||||
:iterations="iterations"
|
||||
:activation-function="
|
||||
getActivationFunction(activationFunction)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<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: iterations.reduce((labels, iteration) => {
|
||||
if (
|
||||
!labels.includes(
|
||||
`Itération ${iteration.iteration}`,
|
||||
)
|
||||
) {
|
||||
labels.push(
|
||||
`Itération ${iteration.iteration}`,
|
||||
);
|
||||
}
|
||||
return labels;
|
||||
}, [] as string[]),
|
||||
datasets: getPerceptronErrorsPerIteration(),
|
||||
}"
|
||||
<PerceptronIterationsErrorsGraph
|
||||
:iterations="iterations"
|
||||
v-if="iterations.length > 0"
|
||||
/>
|
||||
</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