public/math/training.mjs
与本次数学页面一起发布的源码快照。SHA-256: b8db8cb6882aa0eb396960da6d96c852f2ac9a4744b956a53858db7197003b12
1// A real, tiny full-batch SGD experiment. Analytic derivatives, fixed synthetic
2// targets, one camera, three times, two STG-Lite splats. No prerecorded losses.
3import { initialRecords, forward, gradient } from './trace.mjs';
4
5export const trainingSpec = Object.freeze({ width:12, height:10, left:68, top:63, times:[0.25,0.5,0.75] });
6export const trainingParameters = [0,8,20]; // A: mean x, linear motion x, opacity logit
7export const trainingNames = ['μ₀x','m₁x','β'];
8export const copyRecords = rows => rows.map(row=>[...row]);
9
10export function createTrainingData() {
11 const truth=initialRecords();
12 truth[0][0]=1.12; truth[0][8]=0.8; truth[0][20]=0.2;
13 const {width,height,left,top,times}=trainingSpec;
14 return times.map(time=>({time,samples:Array.from({length:width*height},(_,i)=>{
15 const pixel=[left+i%width+0.5,top+Math.floor(i/width)+0.5];
16 return {pixel,target:forward(truth,time,pixel).color};
17 })}));
18}
19
20export function evaluateTraining(records, data, withGradient=false) {
21 let loss=0; const gradients=[0,0,0],frames=[];
22 const count=data.reduce((n,frame)=>n+frame.samples.length,0);
23 for(const frame of data) {
24 const pixels=[];
25 for(const sample of frame.samples) {
26 const options={target:sample.target,finite:false};
27 const position=withGradient?gradient(records,0,frame.time,sample.pixel,options):null;
28 const result=position?.result??forward(records,frame.time,sample.pixel,options);
29 loss+=result.loss/count;
30 if(withGradient) {
31 gradients[0]+=position.analytic/count;
32 gradients[1]+=position.analytic*(frame.time-records[0][3])/count;
33 gradients[2]+=gradient(records,20,frame.time,sample.pixel,options).analytic/count;
34 }
35 pixels.push(result);
36 }
37 frames.push(pixels);
38 }
39 return {loss,gradients,frames};
40}
41
42export function trainingIteration(records, data, rate=1) {
43 if(!Number.isFinite(rate)||rate<=0)throw new RangeError('Learning rate must be positive');
44 const beforeRecords=copyRecords(records),before=evaluateTraining(beforeRecords,data,true);
45 const afterRecords=copyRecords(beforeRecords);
46 // All gradients come from the same pre-update batch; commit simultaneously.
47 trainingParameters.forEach((field,j)=>{afterRecords[0][field]-=rate*before.gradients[j];});
48 const after=evaluateTraining(afterRecords,data);
49 if(!Number.isFinite(after.loss)||!afterRecords.flat().every(Number.isFinite))throw new Error('Non-finite training state');
50 return {beforeRecords,before,afterRecords,after,rate};
51}
52
53// Moving through explanatory stages never repeats an update. Only the last
54// stage commits; rewinding from it restores the exact pre-update snapshot.
55export class TrainingSession {
56 constructor(rate=1) { this.data=createTrainingData(); this.rate=rate; this.reset(); }
57 reset() {
58 this.records=initialRecords(); this.stage=0; this.iteration=0;
59 this.history=[evaluateTraining(this.records,this.data).loss]; this.pending=null;
60 }
61 prepare() { return this.pending??=trainingIteration(this.records,this.data,this.rate); }
62 next() {
63 if(this.stage===7) { this.pending=null;this.stage=0;return; }
64 const trace=this.prepare(); this.stage++;
65 if(this.stage===7) {
66 this.records=copyRecords(trace.afterRecords);this.iteration++;
67 this.history.push(trace.after.loss);
68 }
69 }
70 previous() {
71 if(this.stage===0)return;
72 if(this.stage===7) {
73 this.records=copyRecords(this.pending.beforeRecords);this.iteration--;this.history.pop();
74 }
75 this.stage--;
76 }
77}
78