mirror of
https://github.com/ValentinDuflot/perceptron-visuel.git
synced 2026-01-25 15:34:11 +00:00
<Feat> Première version
This commit is contained in:
commit
44c1459e1e
2 changed files with 984 additions and 0 deletions
901
index.html
Normal file
901
index.html
Normal 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 d’un chiffre 8x8
|
||||
- un neurone unique pondéré (64 poids + 1 biais)
|
||||
- une fonction d’activation seuil (step)
|
||||
- apprentissage supervisé par itération et ajustement du poids
|
||||
proportionnel à l’erreur (Δw = η × (y – ŷ) × x)
|
||||
|
||||
L’interface permet :
|
||||
• d’observer la propagation avant (somme pondérée + biais)
|
||||
• de modifier manuellement les poids via des potentiomètres rotatifs
|
||||
• d’ajuster le biais et le taux d’apprentissage (η)
|
||||
• d’entraî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 d’aucune
|
||||
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 l’auteur 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 d’entraî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
83
readme.md
Normal 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 d’un neurone artificiel de manière intuitive.
|
||||
|
||||
Le projet est **100 % autonome**, codé en **HTML / CSS / JavaScript pur**, sans dépendance externe.
|
||||
|
||||
---
|
||||
|
||||
## Fonctionnement
|
||||
|
||||
- 64 **LED d’entrée** représentent les pixels d’un chiffre 8×8.
|
||||
- 64 **potentiomètres** (potars) permettent de visualiser et d’ajuster les poids synaptiques.
|
||||
- Un **potar central** contrôle le **biais global**.
|
||||
- Un autre contrôle le **taux d’apprentissage (η)**.
|
||||
- Une **ampoule** et une **jauge** affichent la sortie du neurone.
|
||||
|
||||
L’application peut :
|
||||
- s’entraî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 0–9).
|
||||
|
||||
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 d’apprentissage,
|
||||
- \( y \) la sortie attendue,
|
||||
- \( ŷ \) la sortie prédite.
|
||||
|
||||
L’entraî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 l’auteur 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)
|
||||
|
||||
---
|
||||
|
||||
> « C’est le premier pas de la machine vers la pensée. »
|
||||
Loading…
Reference in a new issue