Files
Reseaux-de-neurones-artific…/resources/js/components/PerceptronDecisionGraph.vue
Matthias Guillitte f6620c2eca
All checks were successful
linter / quality (push) Successful in 8m11s
tests / ci (8.4) (push) Successful in 10m8s
tests / ci (8.5) (push) Successful in 12m59s
Fixed graph showing wrong decision border for regression
2026-03-24 17:33:58 +01:00

241 lines
7.2 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import type {
ChartDataset,
ChartTypeRegistry,
BubbleDataPoint,
Point,
} from 'chart.js';
import { computed } from 'vue';
import { Chart } from 'vue-chartjs';
import { colors, gridColor, gridColorBold } from '@/types/graphs';
import type { Iteration } from '@/types/perceptron';
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 getPerceptronOutput(weightsNetwork: number[][], inputs: number[]): number[] {
for (const layer of weightsNetwork) {
const nextInputs: 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] * inputs[i];
}
const activated = props.activationFunction(sum);
nextInputs.push(activated);
}
inputs = nextInputs;
}
return inputs;
}
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]]; // Copy of the unique perceptron weights
function perceptronLine(x: number): number {
if (perceptronWeights.length < 3) {
// If we have less than 3 weights, we assume missing weights are zero
return getPerceptronOutput(networkWeights, [x])[0];
}
// w0 + w1*x + w2*y = 0 => y = -(w1/w2)*x - w0/w2
const w2 = perceptronWeights[2] == 0 ? 1e-6 : perceptronWeights[2]; // Avoid division by zero
return -(perceptronWeights[1] / w2) * x - perceptronWeights[0] / w2;
}
// 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 bg-primary dark:bg-transparent!"
type="scatter"
: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',
grid: {
color: function (context) {
if (context.tick.value == 0) {
return gridColorBold;
}
return gridColor;
},
},
},
y: {
type: 'linear',
position: 'left',
grid: {
color: function (context) {
if (context.tick.value == 0) {
return gridColorBold;
}
return gridColor;
},
},
},
},
}"
: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>