perceptron-visuel/index.html

901 lines
24 KiB
HTML
Raw Normal View History

2025-10-14 10:51:30 +00:00
<!--
===============================================================================
Perceptron de Rosenblatt Simulation interactive
Auteur : Valentin Duflot (2025)
------------------------------------------------------------------------------
Description :
Ce projet est une reconstitution visuelle et fonctionnelle du perceptron
de Rosenblatt (1958), le premier modèle d'apprentissage supervisé
artificiel fondé sur la règle de correction d'erreur.
Le but est d'illustrer, de manière concrète et intuitive, le fonctionnement
du perceptron monocouche :
- 64 entrées (LED) représentant les pixels dun chiffre 8x8
- un neurone unique pondéré (64 poids + 1 biais)
- une fonction dactivation seuil (step)
- apprentissage supervisé par itération et ajustement du poids
proportionnel à lerreur (Δw = η × (y ŷ) × x)
Linterface permet :
• dobserver la propagation avant (somme pondérée + biais)
• de modifier manuellement les poids via des potentiomètres rotatifs
• dajuster le biais et le taux dapprentissage (η)
• dentraîner le modèle sur 100 chiffres bruités
soit pas à pas (« Ajuster une fois »)
soit automatiquement sur plusieurs époques
Le code est entièrement autonome (HTML + CSS + JS pur) et ne dépend daucune
bibliothèque externe. Il a une finalité exclusivement pédagogique.
Licence :
Libre de réutilisation, de diffusion et de modification à des fins éducatives
ou de recherche, à condition de mentionner lauteur original.
===============================================================================
-->
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Perceptron de Rosenblatt Simulation</title>
<style>
body {
display: flex;
justify-content: space-between;
align-items: flex-start;
background: #111;
color: #fff;
font-family: monospace;
height: 100vh;
margin: 0;
overflow: hidden;
padding: 10px;
}
.colonne {
width: 33%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
gap: 10px;
}
/* Zone des entrées */
#entrees {
display: grid;
grid-template-columns: repeat(8, 40px);
grid-template-rows: repeat(8, 40px);
gap: 8px;
}
.led {
width: 40px;
height: 40px;
border-radius: 50%;
background: radial-gradient(circle at 30% 30%, #222, #000);
box-shadow: 0 0 10px #000 inset;
cursor: pointer;
transition: all 0.2s;
border: 2px solid #333;
}
.led.on {
background: radial-gradient(circle at 30% 30%, #0f0, #030);
box-shadow: 0 0 25px #0f0;
border: 2px solid #0f0;
}
/* Potentiomètres */
#poids {
display: grid;
grid-template-columns: repeat(8, 55px);
grid-template-rows: repeat(8, 70px);
gap: 8px;
}
.potar {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
color: #ccc;
user-select: none;
}
.knob {
position: relative;
width: 30px;
height: 30px;
border-radius: 100%;
background: radial-gradient(circle at 30% 30%, #444, #222);
border: 2px solid #666;
box-shadow: inset -3px -3px 6px rgba(0, 0, 0, 0.6), inset 2px 2px 4px rgba(255, 255, 255, 0.05);
cursor: grab;
transition: background 0.2s;
}
.knob::after {
content: '';
position: absolute;
top: 5px;
left: 50%;
width: 3px;
height: 10px;
background: #ff0;
border-radius: 2px;
transform-origin: bottom center;
transform: rotate(0deg) translateX(-50%);
}
.knob.active {
background: radial-gradient(circle at 30% 30%, #666, #222);
cursor: grabbing;
}
.knob-value {
font-size: 0.8em;
text-align: center;
width: 100%;
color: #aaa;
}
/* Biais */
#biais-container {
margin-top: 30px;
display: flex;
flex-direction: column;
align-items: center;
}
#biais-label {
margin-top: 5px;
color: #ccc;
}
/* Sortie */
#sortie {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
gap: 30px;
}
/* Aiguille (jauge) */
#jauge {
position: relative;
width: 120px;
height: 60px;
border-top-left-radius: 120px;
border-top-right-radius: 120px;
background: radial-gradient(circle at bottom, #333, #111);
border: 2px solid #555;
overflow: hidden;
}
#aiguille {
position: absolute;
bottom: 0;
left: 50%;
width: 2px;
height: 55px;
background: #ff0;
transform-origin: bottom center;
transform: rotate(-90deg);
transition: transform 0.2s ease-out;
}
#ampoule {
width: 80px;
height: 80px;
border-radius: 50%;
background: radial-gradient(circle at 30% 30%, #555, #111);
box-shadow: 0 0 10px #000 inset;
transition: background 0.3s, box-shadow 0.3s;
}
#ampoule.on {
background: radial-gradient(circle at 30% 30%, #ff8, #550);
box-shadow: 0 0 30px #ff0;
}
#valeur-sortie {
font-size: 1.2em;
}
#learningRate {
accent-color: #ff0;
cursor: pointer;
}
#training-controls button {
background: #222;
color: #eee;
border: 1px solid #555;
padding: 6px 12px;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
}
#training-controls button:hover {
background: #333;
border-color: #ff0;
}
.big-potar {
width: 70px !important;
height: 70px !important;
}
.big-potar::after {
top: 8px;
height: 20px;
width: 4px;
}
.big-label {
font-size: 1em;
color: #ff0;
}
</style>
</head>
<body>
<div class="colonne" id="col-gauche">
<h2>Entrées</h2>
<div id="entrees"></div>
<div style="margin-top:20px; display:flex; flex-direction:column; align-items:center;">
<div>
<button id="prev">◀ Précédent</button>
<select id="selectChiffre"></select>
<button id="next">Suivant ▶</button>
</div>
<div id="labelChiffre" style="margin-top:5px;color:#ccc;">Chiffre #0</div>
</div>
</div>
<div class="colonne" id="col-centre">
<h2>Poids</h2>
<div id="poids"></div>
<div id="controls-center"
style="margin-top:30px;display:flex;flex-direction:column;align-items:center;gap:40px;">
</div>
</div>
<div class="colonne" id="col-droite">
<div id="training-controls"
style="margin-top:40px;display:flex;flex-direction:column;align-items:center;gap:20px;">
<div class="potar">
<div id="knob-biais" class="knob big-potar"></div>
<div id="biais-label" class="big-label">0.00</div>
<div style="color:#888;">Biais global</div>
</div>
<div class="potar">
<div id="knob-lr" class="knob big-potar"></div>
<div id="learningRateLabel" class="big-label">0.10</div>
<div style="color:#888;">Taux d'apprentissage</div>
</div>
<div style="display:flex;gap:10px;">
<button id="randomize">Hasard initial</button>
<button id="reset">Réinitialiser</button>
</div>
<div style="display:flex;flex-direction:column;align-items:center;">
<label for="targetDigit" style="margin-bottom:5px;">Chiffre désiré</label>
<select id="targetDigit">
<option value="0">0</option>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">5</option>
<option value="6">6</option>
<option value="7">7</option>
<option value="8">8</option>
<option value="9">9</option>
</select>
</div>
</div>
<h2>Sortie</h2>
<div id="sortie">
<div id="jauge">
<div id="aiguille"></div>
</div>
<div id="ampoule"></div>
<div id="valeur-sortie">0.00</div>
</div>
</div>
<script>
// ====================
// Jeu de 100 chiffres (8x8)
// ====================
const baseDigits = [
// 0
[0, 1, 1, 1, 1, 1, 1, 0,
1, 1, 0, 0, 0, 0, 1, 1,
1, 0, 0, 0, 0, 0, 0, 1,
1, 0, 0, 0, 0, 0, 0, 1,
1, 0, 0, 0, 0, 0, 0, 1,
1, 1, 0, 0, 0, 0, 1, 1,
0, 1, 1, 1, 1, 1, 1, 0,
0, 0, 0, 0, 0, 0, 0, 0], // 0
// 1
[0, 0, 0, 1, 1, 0, 0, 0,
0, 0, 1, 1, 1, 0, 0, 0,
0, 1, 0, 1, 1, 0, 0, 0,
0, 0, 0, 1, 1, 0, 0, 0,
0, 0, 0, 1, 1, 0, 0, 0,
0, 0, 0, 1, 1, 0, 0, 0,
0, 1, 1, 1, 1, 1, 1, 0,
0, 0, 0, 0, 0, 0, 0, 0], // 1
// 2
[0, 1, 1, 1, 1, 1, 1, 0,
1, 0, 0, 0, 0, 0, 1, 1,
0, 0, 0, 0, 0, 0, 0, 1,
0, 0, 0, 0, 0, 0, 1, 1,
0, 0, 0, 0, 0, 1, 1, 0,
0, 0, 0, 0, 1, 1, 0, 0,
1, 1, 1, 1, 1, 1, 1, 1,
0, 0, 0, 0, 0, 0, 0, 0], // 2
// 3
[0, 1, 1, 1, 1, 1, 1, 0,
1, 0, 0, 0, 0, 0, 1, 1,
0, 0, 0, 0, 0, 0, 0, 1,
0, 0, 1, 1, 1, 1, 1, 1,
0, 0, 0, 0, 0, 0, 0, 1,
1, 0, 0, 0, 0, 0, 1, 1,
0, 1, 1, 1, 1, 1, 1, 0,
0, 0, 0, 0, 0, 0, 0, 0], // 3
// 4
[0, 0, 0, 0, 1, 1, 1, 0,
0, 0, 0, 1, 0, 1, 1, 0,
0, 0, 1, 0, 0, 1, 1, 0,
0, 1, 0, 0, 0, 1, 1, 0,
1, 1, 1, 1, 1, 1, 1, 1,
0, 0, 0, 0, 0, 1, 1, 0,
0, 0, 0, 0, 0, 1, 1, 0,
0, 0, 0, 0, 0, 0, 0, 0], // 4
// 5
[1, 1, 1, 1, 1, 1, 1, 1,
1, 0, 0, 0, 0, 0, 0, 0,
1, 1, 1, 1, 1, 1, 1, 0,
0, 0, 0, 0, 0, 0, 1, 1,
0, 0, 0, 0, 0, 0, 0, 1,
1, 0, 0, 0, 0, 0, 0, 1,
0, 1, 1, 1, 1, 1, 1, 0,
0, 0, 0, 0, 0, 0, 0, 0], // 5
// 6
[0, 1, 1, 1, 1, 1, 1, 0,
1, 0, 0, 0, 0, 0, 1, 1,
1, 0, 0, 0, 0, 0, 0, 0,
1, 1, 1, 1, 1, 1, 1, 0,
1, 0, 0, 0, 0, 0, 0, 1,
1, 0, 0, 0, 0, 0, 1, 1,
0, 1, 1, 1, 1, 1, 1, 0,
0, 0, 0, 0, 0, 0, 0, 0], // 6
// 7
[1, 1, 1, 1, 1, 1, 1, 1,
0, 0, 0, 0, 0, 0, 1, 1,
0, 0, 0, 0, 0, 1, 1, 0,
0, 0, 0, 0, 1, 1, 0, 0,
0, 0, 0, 1, 1, 0, 0, 0,
0, 0, 1, 1, 0, 0, 0, 0,
0, 1, 1, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0], // 7
// 8
[0, 1, 1, 1, 1, 1, 1, 0,
1, 0, 0, 0, 0, 0, 1, 1,
1, 0, 0, 0, 0, 0, 1, 1,
0, 1, 1, 1, 1, 1, 1, 0,
1, 0, 0, 0, 0, 0, 1, 1,
1, 0, 0, 0, 0, 0, 1, 1,
0, 1, 1, 1, 1, 1, 1, 0,
0, 0, 0, 0, 0, 0, 0, 0], // 8
// 9
[0, 1, 1, 1, 1, 1, 1, 0,
1, 0, 0, 0, 0, 0, 1, 1,
1, 0, 0, 0, 0, 0, 1, 1,
0, 1, 1, 1, 1, 1, 1, 1,
0, 0, 0, 0, 0, 0, 1, 1,
1, 0, 0, 0, 0, 0, 1, 1,
0, 1, 1, 1, 1, 1, 1, 0,
0, 0, 0, 0, 0, 0, 0, 0], // 9
];
// Variantes bruitées
const chiffres = [];
for (let i = 0; i < 10; i++) {
for (let v = 0; v < 10; v++) {
const noisy = baseDigits[i].map(bit => {
if (Math.random() < 0.05) return bit ? 0 : 1;
return bit;
});
chiffres.push(noisy);
}
}
let indexChiffre = 0;
const N = 64;
// === Fonctions ===
function afficherChiffre(idx) {
const data = chiffres[idx];
for (let i = 0; i < N; i++) {
entrees[i].dataset.on = data[i].toString();
entrees[i].classList.toggle('on', data[i] === 1);
}
majSortie();
}
// === Initialisation des LED, potars, biais ===
const entreesDiv = document.getElementById('entrees');
const poidsDiv = document.getElementById('poids');
const ampoule = document.getElementById('ampoule');
const valeurSortie = document.getElementById('valeur-sortie');
const aiguille = document.getElementById('aiguille');
let biais = 0;
const biaisLabel = document.getElementById('biais-label');
const entrees = [];
const poids = [];
// === LED 8x8 ===
for (let i = 0; i < N; i++) {
const led = document.createElement('div');
led.className = 'led';
led.dataset.on = '0';
led.addEventListener('click', () => {
const etat = led.dataset.on === '1';
led.dataset.on = etat ? '0' : '1';
led.classList.toggle('on', !etat);
majSortie();
});
entreesDiv.appendChild(led);
entrees.push(led);
}
// === Potars rotatifs 8x8 ===
for (let i = 0; i < N; i++) {
const p = document.createElement('div');
p.className = 'potar';
const knob = document.createElement('div');
knob.className = 'knob';
const label = document.createElement('div');
label.className = 'knob-value';
label.textContent = '0.00';
let angle = 0; // -135° à +135° = -0.5 à +0.5
const minAngle = -135;
const maxAngle = 135;
// Conversion angle <-> valeur
function angleToValue(a) {
return ((a - minAngle) / (maxAngle - minAngle)) * 1 - 0.5;
}
function valueToAngle(v) {
return minAngle + (v + 0.5) * (maxAngle - minAngle);
}
// Mise à jour graphique
function majKnob() {
const val = parseFloat(poids[i].value);
const ang = valueToAngle(val);
knob.style.setProperty('--angle', `${ang}deg`);
knob.style.transform = `rotate(${ang}deg)`;
knob.querySelector('::after');
knob.style.setProperty('--needle-angle', `${ang}deg`);
knob.style.transform = `rotate(0deg)`; // le fond
knob.querySelector(':after');
knob.style.setProperty('--needle-angle', `${ang}deg`);
knob.style.setProperty('--needle-angle', `${ang}deg`);
knob.style.setProperty('--angle', `${ang}deg`);
knob.style.setProperty('--angle', `${ang}deg`);
}
// Crée une valeur invisible liée à ce potar (pour calculs)
const hiddenRange = document.createElement('input');
hiddenRange.type = 'hidden';
hiddenRange.value = '0';
poids.push(hiddenRange);
// Rotation avec la souris
let dragging = false;
let startY = 0;
let startAngle = 0;
knob.addEventListener('mousedown', (e) => {
dragging = true;
knob.classList.add('active');
startY = e.clientY;
startAngle = angle;
document.body.style.cursor = 'grabbing';
});
window.addEventListener('mouseup', () => {
if (dragging) {
dragging = false;
knob.classList.remove('active');
document.body.style.cursor = '';
}
});
window.addEventListener('mousemove', (e) => {
if (!dragging) return;
const dy = startY - e.clientY;
angle = Math.max(minAngle, Math.min(maxAngle, startAngle + dy));
const val = angleToValue(angle);
hiddenRange.value = val.toFixed(2);
label.textContent = val.toFixed(2);
knob.style.transform = `rotate(${angle}deg)`;
majSortie();
});
// Molette fine
knob.addEventListener('wheel', (e) => {
e.preventDefault();
const delta = e.deltaY < 0 ? 0.02 : -0.02;
let val = parseFloat(hiddenRange.value) + delta;
val = Math.max(-0.5, Math.min(0.5, val));
hiddenRange.value = val.toFixed(2);
label.textContent = val.toFixed(2);
const ang = valueToAngle(val);
knob.style.transform = `rotate(${ang}deg)`;
majSortie();
});
p.appendChild(knob);
p.appendChild(label);
poidsDiv.appendChild(p);
}
// === Calcul sortie ===
function majSortie() {
let somme = biais;
for (let i = 0; i < N; i++) {
const x = entrees[i].dataset.on === '1' ? 1 : 0;
somme += x * parseFloat(poids[i].value);
}
valeurSortie.textContent = somme.toFixed(2);
ampoule.classList.toggle('on', somme > 0.5);
const angle = Math.max(-90, Math.min(90, somme * 180));
aiguille.style.transform = `rotate(${angle}deg)`;
}
// === Navigation ===
const select = document.getElementById('selectChiffre');
for (let i = 0; i < chiffres.length; i++) {
const opt = document.createElement('option');
opt.value = i;
opt.textContent = `Exemple ${i}`;
select.appendChild(opt);
}
const labelChiffre = document.getElementById('labelChiffre');
function majLabel() {
labelChiffre.textContent = `Chiffre #${indexChiffre} → ${Math.floor(indexChiffre / 10)}`;
}
select.addEventListener('change', e => {
indexChiffre = parseInt(e.target.value);
afficherChiffre(indexChiffre);
majLabel();
});
document.getElementById('prev').onclick = () => {
indexChiffre = (indexChiffre - 1 + chiffres.length) % chiffres.length;
select.value = indexChiffre;
afficherChiffre(indexChiffre);
majLabel();
};
document.getElementById('next').onclick = () => {
indexChiffre = (indexChiffre + 1) % chiffres.length;
select.value = indexChiffre;
afficherChiffre(indexChiffre);
majLabel();
};
function majPotarGraphique(knob, val) {
const minAngle = -135, maxAngle = 135;
const ang = minAngle + (val + 0.5) * (maxAngle - minAngle);
knob.style.transform = `rotate(${ang}deg)`;
}
// === Contrôles dentraînement ===
const randomBtn = document.getElementById('randomize');
const resetBtn = document.getElementById('reset');
const targetDigitSelect = document.getElementById('targetDigit');
const lrLabel = document.getElementById('learningRateLabel');
let learningRate = 0.1; // valeur initiale avant que le gros potar ne la change
let desiredDigit = parseInt(targetDigitSelect.value);
// --- événements ---
targetDigitSelect.addEventListener('change', () => {
desiredDigit = parseInt(targetDigitSelect.value);
});
// --- randomize ---
randomBtn.addEventListener('click', () => {
const knobs = document.querySelectorAll('#poids .knob');
const labels = document.querySelectorAll('#poids .knob-value');
for (let i = 0; i < N; i++) {
const val = (Math.random() - 0.5);
poids[i].value = val.toFixed(2);
labels[i].textContent = val.toFixed(2);
majPotarGraphique(knobs[i], val);
}
const bVal = (Math.random() * 2 - 1);
potarBiais.setValue(bVal);
majSortie();
});
// --- reset ---
resetBtn.addEventListener('click', () => {
const knobs = document.querySelectorAll('#poids .knob');
const labels = document.querySelectorAll('#poids .knob-value');
for (let i = 0; i < N; i++) {
poids[i].value = "0";
labels[i].textContent = "0.00";
majPotarGraphique(knobs[i], 0);
}
potarBiais.setValue(0);
potarLR.setValue(0.1);
majSortie();
});
// === Potars spéciaux : biais & learning rate ===
function creerPotar(idKnob, labelEl, min, max, step, initial, onChange) {
const knob = document.getElementById(idKnob);
let angle = 0;
const minAngle = -135, maxAngle = 135;
let value = initial;
function angleToValue(a) {
return min + ((a - minAngle) / (maxAngle - minAngle)) * (max - min);
}
function valueToAngle(v) {
return minAngle + ((v - min) / (max - min)) * (maxAngle - minAngle);
}
function majAffichage() {
const ang = valueToAngle(value);
knob.style.transform = `rotate(${ang}deg)`;
labelEl.textContent = value.toFixed(2);
onChange(value);
}
// Souris
let dragging = false, startY = 0, startAngle = 0;
knob.addEventListener('mousedown', e => {
dragging = true;
knob.classList.add('active');
startY = e.clientY;
startAngle = angle;
document.body.style.cursor = 'grabbing';
});
window.addEventListener('mouseup', () => {
if (dragging) {
dragging = false;
knob.classList.remove('active');
document.body.style.cursor = '';
}
});
window.addEventListener('mousemove', e => {
if (!dragging) return;
const dy = startY - e.clientY;
angle = Math.max(minAngle, Math.min(maxAngle, startAngle + dy));
value = angleToValue(angle);
majAffichage();
});
// Molette
knob.addEventListener('wheel', e => {
e.preventDefault();
const delta = e.deltaY < 0 ? step : -step;
value = Math.max(min, Math.min(max, value + delta));
majAffichage();
});
function setValue(v) {
value = Math.max(min, Math.min(max, v));
majAffichage();
}
function getValue() { return value; }
majAffichage();
return { setValue, getValue };
}
// Biais global
const potarBiais = creerPotar('knob-biais', document.getElementById('biais-label'), -1, 1, 0.01, 0, v => {
biais = v;
majSortie();
});
// learning rate
const potarLR = creerPotar('knob-lr', document.getElementById('learningRateLabel'), 0.001, 1, 0.005, 0.1, v => {
learningRate = v;
});
// === Lancement ===
window.addEventListener('DOMContentLoaded', () => {
afficherChiffre(0);
majLabel();
});
// --- Bouton d'entraînement unique (manuel) ---
// --- Fonctions utilitaires ---
function predictSum(x) {
let s = biais;
for (let i = 0; i < N; i++) {
if (x[i]) s += parseFloat(poids[i].value);
}
return s;
}
function stepFn(z) {
return z >= 0 ? 1 : 0;
}
// élément pour afficher le statut
const statusEl = document.createElement('div');
statusEl.style.color = '#aaa';
statusEl.style.marginTop = '10px';
statusEl.style.fontSize = '0.9em';
statusEl.textContent = 'Prêt.';
document.getElementById('training-controls').appendChild(statusEl);
// bouton "ajuster une fois"
const btnTrainOne = document.createElement('button');
btnTrainOne.textContent = 'Ajuster une fois';
btnTrainOne.addEventListener('click', () => {
const x = chiffres[indexChiffre];
const y = (Math.floor(indexChiffre / 10) === desiredDigit) ? 1 : 0;
const s = predictSum(x);
const yhat = stepFn(s);
const err = y - yhat;
if (err !== 0) {
const knobs = document.querySelectorAll('#poids .knob');
const labels = document.querySelectorAll('#poids .knob-value');
for (let i = 0; i < N; i++) {
if (!x[i]) continue;
let w = parseFloat(poids[i].value);
w += learningRate * err;
w = Math.max(-0.5, Math.min(0.5, w));
poids[i].value = w.toFixed(2);
labels[i].textContent = poids[i].value;
const ang = -135 + (w + 0.5) * (270);
knobs[i].style.transform = `rotate(${ang}deg)`;
}
biais += learningRate * err;
potarBiais.setValue(biais);
}
majSortie();
statusEl.textContent = `Sortie ${yhat} — attendu ${y} — err ${err >= 0 ? '+' : ''}${err.toFixed(2)}`;
});
document.getElementById('training-controls').appendChild(btnTrainOne);
// --- Entraînement automatique (par époques) ---
const epochInput = document.createElement('input');
epochInput.type = 'number';
epochInput.min = 1;
epochInput.max = 1000;
epochInput.value = 5;
epochInput.style.width = '60px';
epochInput.style.textAlign = 'center';
epochInput.style.marginTop = '10px';
epochInput.style.background = '#222';
epochInput.style.color = '#ff0';
epochInput.style.border = '1px solid #555';
epochInput.style.borderRadius = '4px';
epochInput.title = 'Nombre dépoques à exécuter';
const btnTrainAuto = document.createElement('button');
btnTrainAuto.textContent = '▶ Entraînement auto';
btnTrainAuto.style.marginTop = '6px';
document.getElementById('training-controls').appendChild(epochInput);
document.getElementById('training-controls').appendChild(btnTrainAuto);
let isTraining = false;
let currentEpoch = 0;
btnTrainAuto.addEventListener('click', () => {
if (isTraining) {
isTraining = false;
btnTrainAuto.textContent = '▶ Reprendre';
return;
}
isTraining = true;
btnTrainAuto.textContent = '⏸ Pause';
const totalEpochs = parseInt(epochInput.value) || 1;
currentEpoch = 0;
function trainEpoch() {
if (!isTraining || currentEpoch >= totalEpochs) {
isTraining = false;
btnTrainAuto.textContent = '▶ Entraînement auto';
statusEl.textContent = `✔ Terminé (${currentEpoch} époques)`;
return;
}
const knobs = document.querySelectorAll('#poids .knob');
const labels = document.querySelectorAll('#poids .knob-value');
let totalErr = 0;
// passe sur tous les chiffres
for (let idx = 0; idx < chiffres.length; idx++) {
const x = chiffres[idx];
const y = (Math.floor(idx / 10) === desiredDigit) ? 1 : 0;
const s = predictSum(x);
const yhat = stepFn(s);
const err = y - yhat;
totalErr += Math.abs(err);
if (err !== 0) {
for (let i = 0; i < N; i++) {
if (!x[i]) continue;
let w = parseFloat(poids[i].value);
w += learningRate * err;
w = Math.max(-0.5, Math.min(0.5, w));
poids[i].value = w.toFixed(2);
labels[i].textContent = poids[i].value;
const ang = -135 + (w + 0.5) * 270;
knobs[i].style.transform = `rotate(${ang}deg)`;
}
biais += learningRate * err;
potarBiais.setValue(biais);
}
}
// mise à jour UI et LR
const avgErr = totalErr / chiffres.length;
learningRate *= 0.9; // décroissance douce
potarLR.setValue(learningRate);
currentEpoch++;
statusEl.textContent = `Époque ${currentEpoch}/${totalEpochs} — erreur moyenne ${avgErr.toFixed(3)} — LR ${learningRate.toFixed(3)}`;
majSortie();
setTimeout(trainEpoch, 5000 / totalEpochs); // 5 second max, quel que soit le nombre choisi d'époques
}
trainEpoch();
});
</script>
</body>
</html>