<Feat> Première version

This commit is contained in:
Valentin Duflot 2025-10-14 12:51:30 +02:00
commit 44c1459e1e
2 changed files with 984 additions and 0 deletions

901
index.html Normal file
View file

@ -0,0 +1,901 @@
<!--
===============================================================================
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>

83
readme.md Normal file
View file

@ -0,0 +1,83 @@
# Perceptron de Rosenblatt Simulation Interactive
### Auteur : Valentin Duflot
*(2025 Projet pédagogique et expérimental)*
---
## Objectif
Cette application est une **reconstitution visuelle du perceptron de Rosenblatt (1958)**, le tout premier modèle d'apprentissage supervisé artificiel.
Elle permet de comprendre, manipuler et observer le processus d'apprentissage dun neurone artificiel de manière intuitive.
Le projet est **100 % autonome**, codé en **HTML / CSS / JavaScript pur**, sans dépendance externe.
---
## Fonctionnement
- 64 **LED dentrée** représentent les pixels dun chiffre 8×8.
- 64 **potentiomètres** (potars) permettent de visualiser et dajuster les poids synaptiques.
- Un **potar central** contrôle le **biais global**.
- Un autre contrôle le **taux dapprentissage (η)**.
- Une **ampoule** et une **jauge** affichent la sortie du neurone.
Lapplication peut :
- sentraîner **manuellement**, un échantillon à la fois (`Ajuster une fois`),
- ou **automatiquement** sur un nombre défini dépoques (`▶ Entraînement auto`).
---
## Dataset
Le perceptron apprend à reconnaître un **chiffre cible** (sélectionné via menu déroulant)
parmi un ensemble de **100 chiffres bruités** (10 variantes de chaque 09).
Chaque chiffre est représenté sur une matrice 8×8 binaire, inspirée du dataset **Digits**.
---
## Rappel théorique
Le perceptron met à jour ses poids selon la règle :
\[
w_i ← w_i + η × (y ŷ) × x_i
\]
\[
b ← b + η × (y ŷ)
\]
où :
- \( η \) est le taux dapprentissage,
- \( y \) la sortie attendue,
- \( ŷ \) la sortie prédite.
Lentraînement vise à séparer les exemples positifs (le chiffre cible)
des autres, en ajustant les poids de manière linéaire.
---
## Démonstration
Ouvre simplement le fichier `perceptron.html` dans un navigateur moderne (Chrome, Firefox, Edge).
Aucune installation requise.
---
## Licence
Projet libre pour usage **éducatif, scientifique ou artistique**.
Mention de lauteur originale requise :
**© 2025 Valentin Duflot**
---
## Inspirations
- Frank Rosenblatt, *The Perceptron: A Probabilistic Model for Information Storage and Organization in the Brain* (1958)
- Visualisations pédagogiques modernes (TensorFlow Playground, 2016)
---
> « Cest le premier pas de la machine vers la pensée. »