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

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

View File

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

View File

@@ -20,6 +20,7 @@ class PerceptronTrainingEnded implements ShouldBroadcast
public function __construct( public 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,
]; ];
} }
} }

View File

@@ -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,
]; ];
} }
} }

View File

@@ -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],
]); ]);
} }
} }

View File

@@ -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);
} }

View File

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

View File

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

View File

@@ -9,7 +9,7 @@ abstract class Perceptron extends Model
public function __construct( 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
{ {

View File

@@ -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;
} }

View File

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

View File

@@ -2,7 +2,7 @@
namespace App\Providers; 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();
}); });
} }

View File

@@ -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);
} }

View File

@@ -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;
} }

View File

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

View File

@@ -2,7 +2,7 @@
namespace App\Services; 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 = [];

View File

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

1877
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -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
View 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
1 x_1 x_2 d
2 1 6 1
3 7 9 -1
4 1 9 1
5 7 10 -1
6 2 5 -1
7 2 7 1
8 2 8 1
9 6 8 -1
10 6 9 -1
11 3 5 -1
12 3 6 -1
13 3 8 1
14 3 9 1
15 5 7 -1
16 5 8 -1
17 5 10 1
18 5 11 1
19 4 6 -1
20 4 7 -1
21 4 9 1
22 4 10 1

View File

@@ -0,0 +1,5 @@
x_1, x_2, d
0, 0, 1
0, 1, 0
1, 0, 0
1, 1, 1
1 x_1 x_2 d
2 0 0 1
3 0 1 0
4 1 0 0
5 1 1 1

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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"

View File

@@ -0,0 +1,4 @@
import type { InjectionKey } from "vue"
export const FORM_ITEM_INJECTION_KEY
= Symbol() as InjectionKey<string>

View 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,
}
}

View 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>

View File

@@ -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>

View File

@@ -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>

View 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"

View File

@@ -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>

View 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;

View 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';