mirror of
https://github.com/ValentinDuflot/perceptron-visuel.git
synced 2026-01-25 23:40:38 +00:00
947 lines
No EOL
25 KiB
HTML
947 lines
No EOL
25 KiB
HTML
<!--
|
||
===============================================================================
|
||
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;
|
||
position: relative;
|
||
}
|
||
|
||
.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 .2s, box-shadow .2s;
|
||
--halo: 0;
|
||
--haloAlpha: calc(.15 + .65*var(--halo));
|
||
--haloBlur1: calc(6px + 12px*var(--halo));
|
||
--haloSpread1: calc(2px + 6px*var(--halo));
|
||
--haloBlur2: calc(12px + 24px*var(--halo));
|
||
--haloSpread2: calc(6px + 12px*var(--halo));
|
||
}
|
||
|
||
.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.halo-pos {
|
||
box-shadow:
|
||
inset -3px -3px 6px rgba(0, 0, 0, .6),
|
||
inset 2px 2px 4px rgba(255, 255, 255, .05),
|
||
0 0 var(--haloBlur1) var(--haloSpread1) rgba(0, 255, 0, var(--haloAlpha)),
|
||
0 0 var(--haloBlur2) var(--haloSpread2) rgba(0, 255, 0, calc(var(--haloAlpha)*.6));
|
||
}
|
||
|
||
.knob.halo-neg {
|
||
box-shadow:
|
||
inset -3px -3px 6px rgba(0, 0, 0, .6),
|
||
inset 2px 2px 4px rgba(255, 255, 255, .05),
|
||
0 0 var(--haloBlur1) var(--haloSpread1) rgba(255, 0, 0, var(--haloAlpha)),
|
||
0 0 var(--haloBlur2) var(--haloSpread2) rgba(255, 0, 0, calc(var(--haloAlpha)*.6));
|
||
}
|
||
|
||
.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.50</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 = [];
|
||
|
||
const poidsKnobs = [];
|
||
function majHaloIndex(i) {
|
||
const w = parseFloat(poids[i].value) || 0;
|
||
const k = poidsKnobs[i];
|
||
if (!k) return;
|
||
const mag = Math.min(1, Math.abs(w) / 0.5); // map [-0.5,0.5] -> [0,1]
|
||
k.style.setProperty('--halo', mag.toFixed(3));
|
||
if (w > 0) {
|
||
k.classList.add('halo-pos'); k.classList.remove('halo-neg');
|
||
} else if (w < 0) {
|
||
k.classList.add('halo-neg'); k.classList.remove('halo-pos');
|
||
} else {
|
||
k.classList.remove('halo-pos', 'halo-neg');
|
||
}
|
||
}
|
||
// === 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)`;
|
||
majHaloIndex(i);
|
||
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)`;
|
||
majHaloIndex(i);
|
||
majSortie();
|
||
});
|
||
|
||
p.appendChild(knob);
|
||
p.appendChild(label);
|
||
poidsDiv.appendChild(p);
|
||
poidsKnobs.push(knob);
|
||
|
||
}
|
||
|
||
|
||
|
||
// === 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.5;
|
||
|
||
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);
|
||
majHaloIndex(i);
|
||
}
|
||
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);
|
||
majHaloIndex(i);
|
||
}
|
||
potarBiais.setValue(0);
|
||
potarLR.setValue(0.5);
|
||
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.5, 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)`;
|
||
majHaloIndex(i);
|
||
}
|
||
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 = 100;
|
||
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)`;
|
||
majHaloIndex(i);
|
||
}
|
||
biais += learningRate * err;
|
||
potarBiais.setValue(biais);
|
||
}
|
||
}
|
||
|
||
// mise à jour UI et LR
|
||
const avgErr = totalErr / chiffres.length;
|
||
learningRate *= 0.999; // 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> |