410 lines
13 KiB
Vue
410 lines
13 KiB
Vue
<script setup lang="ts">
|
|
import type {
|
|
ChartDataset,
|
|
ChartTypeRegistry,
|
|
BubbleDataPoint,
|
|
Point,
|
|
} from 'chart.js';
|
|
import { computed, ref } from 'vue';
|
|
import { Chart } from 'vue-chartjs';
|
|
import { colors, gridColor, gridColorBold } from '@/types/graphs';
|
|
import type { Iteration } from '@/types/perceptron';
|
|
|
|
type GraphDataset = ChartDataset<
|
|
keyof ChartTypeRegistry,
|
|
(number | Point | [number, number] | BubbleDataPoint | null)[]
|
|
>;
|
|
|
|
const props = defineProps<{
|
|
cleanedDataset: { label: number; data: { x: number; y: number }[] }[];
|
|
iterations: Iteration[];
|
|
activationFunction: (x: number) => number;
|
|
isRegression: boolean;
|
|
}>();
|
|
|
|
const examplesNumber = computed(() => {
|
|
return props.cleanedDataset.reduce((sum, dataset) => sum + dataset.data.length, 0);
|
|
});
|
|
|
|
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 farBottomDataPointY = computed(() => {
|
|
if (props.cleanedDataset.length === 0) {
|
|
return 0;
|
|
}
|
|
const minY = Math.min(
|
|
...props.cleanedDataset.flatMap((d) => d.data.map((point) => point.y)),
|
|
);
|
|
return minY;
|
|
});
|
|
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;
|
|
});
|
|
const farTopDataPointY = computed(() => {
|
|
if (props.cleanedDataset.length === 0) {
|
|
return 0;
|
|
}
|
|
const maxY = Math.max(
|
|
...props.cleanedDataset.flatMap((d) => d.data.map((point) => point.y)),
|
|
);
|
|
return maxY;
|
|
});
|
|
|
|
function getPerceptronOutput(
|
|
weightsNetwork: number[][][],
|
|
inputs: number[],
|
|
activationFunction: (x: number) => number = props.activationFunction,
|
|
): number[] {
|
|
for (const [layerIndex, layer] of weightsNetwork.entries()) {
|
|
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 isOutputLayer = layerIndex === weightsNetwork.length - 1;
|
|
const activated = isOutputLayer ? sum : activationFunction(sum);
|
|
|
|
nextInputs.push(activated);
|
|
}
|
|
|
|
inputs = nextInputs;
|
|
}
|
|
return inputs;
|
|
}
|
|
|
|
function normalizeNetworkWeights(weightsNetwork: number[][][][] | number[][][]): number[][][] {
|
|
if (
|
|
weightsNetwork.length === 1 &&
|
|
weightsNetwork[0].length === 1 &&
|
|
Array.isArray(weightsNetwork[0][0]) &&
|
|
Array.isArray(weightsNetwork[0][0][0])
|
|
) {
|
|
return weightsNetwork[0][0] as unknown as number[][][];
|
|
}
|
|
|
|
return weightsNetwork as number[][][];
|
|
}
|
|
|
|
const nonLinearGraph = ref<boolean>(false);
|
|
function getPerceptronDecisionBoundaryDataset(
|
|
rawNetworkWeights: number[][][] | number[][][][],
|
|
activationFunction: (x: number) => number = (x) => x,
|
|
): GraphDataset[] {
|
|
const networkWeights = normalizeNetworkWeights(rawNetworkWeights);
|
|
const label = 'Ligne de décision du Perceptron';
|
|
console.log('Calculating decision boundary with weights:', networkWeights);
|
|
|
|
if (props.isRegression) {
|
|
const hiddenActivation = (value: number) =>
|
|
1 / (1 + Math.exp(-value));
|
|
const inputCount = networkWeights[0]?.[0]?.length - 1;
|
|
|
|
if (inputCount === 1) {
|
|
if (networkWeights.length > 1) {
|
|
nonLinearGraph.value = true;
|
|
const data: Point[] = [];
|
|
const firstIntegerX = Math.ceil(farLeftDataPointX.value - 1);
|
|
const lastIntegerX = Math.floor(farRightDataPointX.value + 1);
|
|
|
|
for (
|
|
let x = firstIntegerX;
|
|
x <= lastIntegerX;
|
|
x+= 0.1
|
|
) {
|
|
data.push({
|
|
x,
|
|
y: getPerceptronOutput(
|
|
networkWeights,
|
|
[x],
|
|
hiddenActivation,
|
|
)[0],
|
|
});
|
|
}
|
|
|
|
return [
|
|
{
|
|
type: 'line',
|
|
label: 'Prédictions de régression',
|
|
data,
|
|
borderColor: '#FFF',
|
|
backgroundColor: '#FFF',
|
|
pointBackgroundColor: '#FFF',
|
|
pointRadius: 0,
|
|
borderWidth: 2,
|
|
tension: 0.4,
|
|
order: -1,
|
|
},
|
|
];
|
|
}
|
|
|
|
nonLinearGraph.value = false;
|
|
const data: Point[] = [
|
|
{
|
|
x: farLeftDataPointX.value - 1,
|
|
y: getPerceptronOutput(
|
|
networkWeights,
|
|
[farLeftDataPointX.value - 1],
|
|
hiddenActivation,
|
|
)[0],
|
|
},
|
|
{
|
|
x: farRightDataPointX.value + 1,
|
|
y: getPerceptronOutput(
|
|
networkWeights,
|
|
[farRightDataPointX.value + 1],
|
|
hiddenActivation,
|
|
)[0],
|
|
},
|
|
];
|
|
|
|
return [
|
|
{
|
|
type: 'line',
|
|
label: 'Régression du Perceptron',
|
|
data,
|
|
borderColor: '#FFF',
|
|
borderWidth: 2,
|
|
pointRadius: 0,
|
|
order: -1,
|
|
},
|
|
];
|
|
}
|
|
|
|
nonLinearGraph.value = true;
|
|
const predictionPoints = props.cleanedDataset.flatMap((dataset) =>
|
|
dataset.data.map((point) => ({
|
|
x: point.x,
|
|
y: point.y,
|
|
})),
|
|
);
|
|
|
|
return [
|
|
{
|
|
type: 'scatter',
|
|
label: 'Prédictions de régression',
|
|
data: predictionPoints,
|
|
backgroundColor: '#FFF',
|
|
pointBackgroundColor: '#FFF8',
|
|
pointRadius: 5,
|
|
borderWidth: 0,
|
|
order: -1,
|
|
},
|
|
];
|
|
}
|
|
|
|
if (
|
|
networkWeights.length == 1 &&
|
|
networkWeights[0].length == 1 &&
|
|
networkWeights[0][0].length <= 3
|
|
) {
|
|
nonLinearGraph.value = false;
|
|
// 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], activationFunction)[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 {
|
|
nonLinearGraph.value = true;
|
|
|
|
const bubbleTransparency = '30';
|
|
|
|
// -------- Construction des datasets --------
|
|
const datasets: {
|
|
type: 'scatter';
|
|
label: string;
|
|
data: Point[];
|
|
backgroundColor: string;
|
|
pointBackgroundColor: string;
|
|
pointRadius: number;
|
|
borderWidth: number;
|
|
order: number;
|
|
}[] = [];
|
|
// For the number of neuron in the last layer
|
|
const lastLayer = networkWeights[networkWeights.length - 1];
|
|
for (let i = 0; i < lastLayer.length; i++) {
|
|
const dataset: {
|
|
type: 'scatter';
|
|
label: string;
|
|
data: Point[];
|
|
backgroundColor: string;
|
|
pointBackgroundColor: string;
|
|
pointRadius: number;
|
|
borderWidth: number;
|
|
order: number;
|
|
} = {
|
|
type: 'scatter',
|
|
label: label,
|
|
data: [], // Will be filled with the decision boundary points
|
|
backgroundColor: colors[i] || '#AAA',
|
|
pointBackgroundColor: (colors[i] || '#AAA') + bubbleTransparency,
|
|
pointRadius: 15,
|
|
borderWidth: 0,
|
|
order: -1,
|
|
};
|
|
datasets.push(dataset);
|
|
}
|
|
|
|
// -------- Échantillonnage grille --------
|
|
const step =
|
|
Math.abs(
|
|
farRightDataPointX.value + 1 - (farLeftDataPointX.value - 1),
|
|
) / 50;
|
|
|
|
for (
|
|
let x = farLeftDataPointX.value - 1;
|
|
x <= farRightDataPointX.value + 1;
|
|
x += step
|
|
) {
|
|
for (
|
|
let y = farBottomDataPointY.value - 1;
|
|
y <= farTopDataPointY.value + 1;
|
|
y += step
|
|
) {
|
|
const values = getPerceptronOutput(
|
|
networkWeights,
|
|
[x, y],
|
|
activationFunction,
|
|
);
|
|
const dominantValue = Math.max(...values);
|
|
const dominantIndex = values.indexOf(dominantValue);
|
|
|
|
if (dominantIndex >= 0) {
|
|
datasets[dominantIndex].data.push({ x, y });
|
|
}
|
|
}
|
|
}
|
|
|
|
// -------- Dataset ChartJS --------
|
|
return datasets;
|
|
}
|
|
}
|
|
</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',
|
|
},
|
|
},
|
|
animation: {
|
|
duration: nonLinearGraph || examplesNumber > 10 ? 0 : 1000, // Disable animations for instant updates
|
|
},
|
|
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' as const,
|
|
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>
|