our_knp/viz/fast-model.js

60 lines
8.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use strict';
(()=>{
const N=784,H=256,C=10;
function decode(text,Type){const raw=atob(text),bytes=new Uint8Array(raw.length);for(let i=0;i<raw.length;i++)bytes[i]=raw.charCodeAt(i);return new Type(bytes.buffer);}
function normalizeGray(gray,width=28,height=28){
let x0=width,y0=height,x1=-1,y1=-1,ink=0;
for(let y=0;y<height;y++)for(let x=0;x<width;x++)if(gray[y*width+x]>16){x0=Math.min(x0,x);x1=Math.max(x1,x);y0=Math.min(y0,y);y1=Math.max(y1,y);ink++;}
const out=new Uint8Array(N);if(ink<2)return out;
const cw=x1-x0+1,ch=y1-y0+1,ratio=20/Math.max(cw,ch),nw=Math.max(1,Math.floor(cw*ratio+.5)),nh=Math.max(1,Math.floor(ch*ratio+.5)),ox=Math.floor((28-nw)/2),oy=Math.floor((28-nh)/2);
for(let y=0;y<nh;y++)for(let x=0;x<nw;x++){
const xx=Math.max(0,Math.min(cw-1,(x+.5)*cw/nw-.5)),yy=Math.max(0,Math.min(ch-1,(y+.5)*ch/nh-.5));
const lx=Math.floor(xx),ly=Math.floor(yy),hx=Math.min(lx+1,cw-1),hy=Math.min(ly+1,ch-1),ax=xx-lx,ay=yy-ly;
const p=(gray[(ly+y0)*width+lx+x0]*(1-ax)+gray[(ly+y0)*width+hx+x0]*ax)*(1-ay)+(gray[(hy+y0)*width+lx+x0]*(1-ax)+gray[(hy+y0)*width+hx+x0]*ax)*ay;
out[(y+oy)*28+x+ox]=Math.floor(p+.5);
}
let mass=0,mx=0,my=0;for(let y=0;y<28;y++)for(let x=0;x<28;x++){const v=out[y*28+x];mass+=v;mx+=v*x;my+=v*y;}
if(!mass)return out;
const dx=Math.floor(13.5-mx/mass+.5),dy=Math.floor(13.5-my/mass+.5),shifted=new Uint8Array(N);
for(let y=0;y<28;y++)for(let x=0;x<28;x++)if(x+dx>=0&&x+dx<28&&y+dy>=0&&y+dy<28)shifted[(y+dy)*28+x+dx]=out[y*28+x];
return shifted;
}
function simulate(model,pixels){
let mass=0;for(const p of pixels)mass+=p;
if(mass<255)return {blank:true,prediction:null,counts:Array(H).fill(0),scores:[]};
const start=performance.now(),drive=new Float64Array(H),potential=new Float64Array(H),counts=new Uint16Array(H);
for(let j=0;j<H;j++)drive[j]=model.b1[j];
for(let i=0;i<N;i++)if(pixels[i])for(let j=0;j<H;j++)drive[j]+=pixels[i]*model.w1[i*H+j];
for(let j=0;j<H;j++)drive[j]=Math.min(model.threshold,Math.max(0,drive[j]));
// Real finite IF simulation: one spike per neuron/step, subtractive reset.
for(let t=0;t<model.steps;t++)for(let j=0;j<H;j++){potential[j]+=drive[j];if(potential[j]>=model.threshold){counts[j]++;potential[j]-=model.threshold;}}
const scores=Array(C).fill(0);for(let c=0;c<C;c++){let sum=0;for(let j=0;j<H;j++)sum+=counts[j]*model.w2[j*C+c];scores[c]=(sum*model.cap/model.steps+model.b2[c])/model.scale;}
let prediction=0;for(let c=1;c<C;c++)if(scores[c]>scores[prediction])prediction=c;
return {blank:false,prediction,counts:Array.from(counts),scores,milliseconds:performance.now()-start};
}
const api={normalizeGray,simulate,model:null,ready:null};window.FastMNIST=api;
api.ready=Promise.all([fetch('fast-model.json').then(r=>{if(!r.ok)throw Error('Модель недоступна: '+r.status);return r.json();}),fetch('fast-gallery.json').then(r=>{if(!r.ok)throw Error('Галерея недоступна: '+r.status);return r.json();})]).then(([data,gallery])=>{
const model={...data,w1:decode(data.weights.w1,Int16Array),b1:decode(data.weights.b1,Int32Array),w2:decode(data.weights.w2,Int16Array),b2:decode(data.weights.b2,Int32Array)};
if(model.w1.length!==N*H||model.w2.length!==H*C||model.b1.length!==H||model.b2.length!==C)throw Error('Неверный размер модели');
api.model=model;setup(model,gallery);document.body.classList.remove('loading');return model;
}).catch(e=>{document.querySelector('#error').textContent='Не удалось загрузить модель. '+e.message;document.querySelector('#status').textContent='Обнови страницу после проверки соединения.';throw e;});
function setup(model,gallery){
const $=s=>document.querySelector(s),canvas=$('#draw'),ctx=canvas.getContext('2d',{willReadFrequently:true}),preview=$('#normalized').getContext('2d');
let drawing=false,last=null,timer=null,sample=null,samplePixels=null;
function fillBlack(){ctx.fillStyle='#000';ctx.fillRect(0,0,280,280);}
function showPixels(context,pixels){const im=context.createImageData(28,28);for(let i=0;i<N;i++){im.data[4*i]=im.data[4*i+1]=im.data[4*i+2]=pixels[i];im.data[4*i+3]=255;}context.putImageData(im,0,0);}
function infer(){clearTimeout(timer);const rgba=ctx.getImageData(0,0,280,280).data,gray=new Uint8Array(280*280);for(let i=0;i<gray.length;i++)gray[i]=rgba[4*i];const pixels=samplePixels?normalizeGray(samplePixels,28,28):normalizeGray(gray,280,280),answer=simulate(model,pixels);showPixels(preview,pixels);api.lastResult=answer;api.lastNormalized=Array.from(pixels);
$('#answer').textContent=answer.blank?'—':answer.prediction;$('#latency').textContent=answer.blank?'':answer.milliseconds.toFixed(1)+' мс';$('#status').textContent=answer.blank?'Нарисуй цифру — пустой фон не распознаётся.':'Ответ по взвешенным спайкам скрытого слоя.';
$('#scores').replaceChildren();if(!answer.blank){const sorted=answer.scores.map((v,c)=>({v,c})).sort((a,b)=>b.v-a.v).slice(0,5),minimum=Math.min(...answer.scores),span=Math.max(1e-6,sorted[0].v-minimum);for(const item of sorted){const row=document.createElement('div');row.className='row';const digit=document.createElement('span');digit.textContent=item.c;const track=document.createElement('div');track.className='track';const bar=document.createElement('div');bar.className='bar';bar.style.width=(100*(item.v-minimum)/span)+'%';track.append(bar);const score=document.createElement('span');score.className='score';score.textContent=item.v.toFixed(2);row.append(digit,track,score);$('#scores').append(row);}}
}
function clear(){fillBlack();sample=null;samplePixels=null;$('#sample').textContent='Твой рисунок';document.querySelectorAll('#gallery button').forEach(b=>b.setAttribute('aria-current','false'));infer();}
function pos(event){const r=canvas.getBoundingClientRect();return {x:(event.clientX-r.left)*280/r.width,y:(event.clientY-r.top)*280/r.height};}
canvas.addEventListener('pointerdown',event=>{if(event.button!==0)return;event.preventDefault();canvas.setPointerCapture(event.pointerId);drawing=true;sample=null;samplePixels=null;$('#sample').textContent='Твой рисунок';document.querySelectorAll('#gallery button').forEach(b=>b.setAttribute('aria-current','false'));last=pos(event);ctx.fillStyle='#fff';ctx.beginPath();ctx.arc(last.x,last.y,9,0,2*Math.PI);ctx.fill();timer=setTimeout(infer,80);});
canvas.addEventListener('pointermove',event=>{if(!drawing)return;event.preventDefault();const next=pos(event);ctx.strokeStyle='#fff';ctx.lineWidth=18;ctx.lineCap='round';ctx.lineJoin='round';ctx.beginPath();ctx.moveTo(last.x,last.y);ctx.lineTo(next.x,next.y);ctx.stroke();last=next;clearTimeout(timer);timer=setTimeout(infer,80);});
function finish(event){if(!drawing)return;drawing=false;last=null;if(canvas.hasPointerCapture(event.pointerId))canvas.releasePointerCapture(event.pointerId);infer();}
canvas.addEventListener('pointerup',finish);canvas.addEventListener('pointercancel',finish);$('#clear').onclick=clear;$('#recognize').onclick=infer;
$('#accuracy').textContent=(100*model.metrics.test_accuracy).toFixed(2).replace('.',',')+'%';$('#architecture').textContent=`256 спайковых IF-нейронов · ${model.steps} шагов · линейный выход · вычисления локально, без отправки рисунка на сервер.`;$('#version').textContent=model.version+' · train seed 20261015 · параметры выбраны только по validation.';
for(const item of gallery.items){const pixels=decode(item.pixels,Uint8Array),button=document.createElement('button'),mini=document.createElement('canvas');mini.width=mini.height=28;showPixels(mini.getContext('2d'),pixels);const label=document.createElement('small');label.textContent='№'+item.index;button.append(mini,label);button.title=`Test №${item.index}, метка ${item.label}`;button.setAttribute('aria-label',button.title);button.setAttribute('aria-current','false');button.onclick=()=>{fillBlack();ctx.imageSmoothingEnabled=false;ctx.drawImage(mini,0,0,280,280);ctx.imageSmoothingEnabled=true;sample=item.index;samplePixels=pixels;$('#sample').textContent=`Официальный MNIST test №${item.index}, правильная цифра ${item.label}. Не использовался при обучении и выборе модели.`;document.querySelectorAll('#gallery button').forEach(b=>b.setAttribute('aria-current',String(b===button)));infer();};$('#gallery').append(button);}
fillBlack();infer();
}
})();