Download setup_53_2d/finetune/cellmap_tfjs_train.js from cellmap/onnx-web-models: direct link, hf CLI and curl.
- Browser
- Download file 16.1 kB
-
https://huggingface.co/cellmap/onnx-web-models/resolve/main/setup_53_2d/finetune/cellmap_tfjs_train.js
- Command line
-
hf download hf://cellmap/onnx-web-models/setup_53_2d/finetune/cellmap_tfjs_train.js
-
curl -L -o cellmap_tfjs_train.js https://huggingface.co/cellmap/onnx-web-models/resolve/main/setup_53_2d/finetune/cellmap_tfjs_train.js
16.1 kB
| // Reference runtime for cellmap-flow browser finetuning ("full" mode, library tfjs). | |
| // Builds a trainable TF.js network from finetune/train_graph.json, takes its weights from the served .onnx | |
| // (finetune/initializers.json gives the byte offset of every initializer), and writes trained weights back into | |
| // the .onnx bytes in place, so the result is served by onnxruntime-web exactly as before. | |
| // | |
| // (served from Hugging Face this file has no JavaScript MIME type: fetch it and import() a Blob URL, or bundle it) | |
| // const g = await (await fetch(base + "finetune/train_graph.json")).json(); | |
| // const offs = (await (await fetch(base + "finetune/initializers.json")).json())[onnxFile]; | |
| // const onnxBytes = new Uint8Array(await (await fetch(base + onnxFile)).arrayBuffer()); | |
| // const net = buildNet(tf, g, readOnnxWeights(onnxBytes, offs, Object.keys(g.params))); | |
| // const opt = tf.train.adam(1e-4); | |
| // const loss = trainStep(tf, net, opt, x, target, mask); // x [1,H,W,1]; target/mask [1,H-ctx,W-ctx,C] | |
| // const newBytes = writeOnnxWeights(onnxBytes, offs, net.exportWeights()); | |
| // | |
| // Tensors are NHWC. ONNX conv weights [O,I,kh,kw] -> tf [kh,kw,I,O]; ConvTranspose [I,O,kh,kw] -> tf [kh,kw,O,I]. | |
| // | |
| // 3D models (train_graph layout "NDHWC"): params are the PyTorch state_dict tensors, rebuilt from the served .onnx | |
| // (Conv3d split per depth tap, ConvTranspose3d as 1x1 conv) with finetune/torch_params.json, and written back with | |
| // finetune/weight_map.json: | |
| // const tp = await (await fetch(base + "finetune/torch_params.json")).json(); | |
| // const wm = await (await fetch(base + "finetune/weight_map.json")).json(); | |
| // const net = buildNet(tf, g, readTorchParams(onnxBytes, offs, tp)); // x [1,D,H,W,1] | |
| // const newBytes = writeOnnxWeights(onnxBytes, offs, toOnnxWeights(wm, net.exportWeights(), offs)); | |
| // WebGPU workarounds in the 3D path: bias / norm affine on rank-2 views (a rank-5 add with a short 1-D operand fails | |
| // to compile), crops as rank-4/3 slices (rank-5 slice gradient fails), ConvTranspose3d k=s=2 as matmul + | |
| // depth-to-space (conv3dTranspose has no gradient), nearest upsample as rank-3 tiles. Plain autodiff keeps every | |
| // intermediate: only small blocks fit (see manifest note). | |
| function f16ToF32(h) { | |
| const s = (h & 0x8000) ? -1 : 1, e = (h >> 10) & 0x1f, f = h & 0x3ff; | |
| if (e === 0) return s * Math.pow(2, -14) * (f / 1024); | |
| if (e === 31) return f ? NaN : s * Infinity; | |
| return s * Math.pow(2, e - 15) * (1 + f / 1024); | |
| } | |
| const _f = new Float32Array(1), _u = new Uint32Array(_f.buffer); | |
| function f32ToF16(v) { | |
| _f[0] = v; const x = _u[0]; | |
| const s = (x >>> 16) & 0x8000; let e = ((x >>> 23) & 0xff) - 112; let m = x & 0x7fffff; | |
| if (e <= 0) { if (e < -10) return s; m |= 0x800000; const t = 14 - e; const r = (m >> t) + ((m >> (t - 1)) & 1); return s | r; } | |
| if (e >= 31) return s | 0x7c00 | (((x >>> 23) & 0xff) === 0xff && m ? 0x200 : 0); | |
| let r = s | (e << 10) | (m >> 13); | |
| if (m & 0x1000) r += 1; // round half up on the dropped bits (good enough for weights) | |
| return r; | |
| } | |
| export function readOnnxWeights(bytes, offs, names) { | |
| const out = {}; | |
| for (const n of names) { | |
| const o = offs[n]; | |
| if (!o) throw new Error(`initializer ${n} not in file`); | |
| const dv = new DataView(bytes.buffer, bytes.byteOffset + o.offset, o.nbytes); | |
| if (o.dtype === "float32") { | |
| const a = new Float32Array(o.nbytes / 4); | |
| for (let i = 0; i < a.length; i++) a[i] = dv.getFloat32(4 * i, true); | |
| out[n] = a; | |
| } else if (o.dtype === "float16") { | |
| const a = new Float32Array(o.nbytes / 2); | |
| for (let i = 0; i < a.length; i++) a[i] = f16ToF32(dv.getUint16(2 * i, true)); | |
| out[n] = a; | |
| } else throw new Error(`${n}: dtype ${o.dtype} is not writable`); | |
| } | |
| return out; | |
| } | |
| export function writeOnnxWeights(bytes, offs, weights) { | |
| const b = new Uint8Array(bytes); // copy | |
| for (const [n, a] of Object.entries(weights)) { | |
| const o = offs[n]; | |
| const dv = new DataView(b.buffer, b.byteOffset + o.offset, o.nbytes); | |
| if (o.dtype === "float32") { if (a.length * 4 !== o.nbytes) throw new Error(n); a.forEach((v, i) => dv.setFloat32(4 * i, v, true)); } | |
| else if (o.dtype === "float16") { if (a.length * 2 !== o.nbytes) throw new Error(n); a.forEach((v, i) => dv.setUint16(2 * i, f32ToF16(v), true)); } | |
| else throw new Error(`${n}: dtype ${o.dtype} is not writable`); | |
| } | |
| return b; | |
| } | |
| export function buildNet(tf, graph, weights, { trainable = null } = {}) { | |
| // trainable: optional Set of param names to train (default: graph's trainable flags); e.g. head only. | |
| if (graph.layout === "NDHWC") return buildNet3d(tf, graph, weights, { trainable }); | |
| const P = {}; | |
| const kind = {}; | |
| for (const op of graph.ops) { | |
| if (op.op === "conv") kind[op.w] = "conv"; | |
| if (op.op === "conv_t") kind[op.w] = "conv_t"; | |
| } | |
| for (const [n, meta] of Object.entries(graph.params)) { | |
| let t = tf.tensor(weights[n], meta.shape, "float32"); | |
| if (kind[n] === "conv") t = tf.transpose(t, [2, 3, 1, 0]); | |
| else if (kind[n] === "conv_t") t = tf.transpose(t, [2, 3, 1, 0]); | |
| const train = trainable ? trainable.has(n) : meta.trainable; | |
| P[n] = train ? tf.variable(t, true) : t; // unnamed: tf variable names are global, several nets may coexist | |
| if (train) t.dispose(); | |
| } | |
| function forward(x) { | |
| const T = { [graph.input]: x }; | |
| for (const op of graph.ops) { | |
| const a = T[op.x]; | |
| let y; | |
| switch (op.op) { | |
| case "conv": y = tf.conv2d(a, P[op.w], op.stride, "valid"); if (op.b) y = tf.add(y, P[op.b]); break; | |
| case "conv_t": { | |
| const [kh, kw, o] = P[op.w].shape, [n, h, w] = a.shape; | |
| y = tf.conv2dTranspose(a, P[op.w], [n, (h - 1) * op.stride[0] + kh, (w - 1) * op.stride[1] + kw, o], op.stride, "valid"); | |
| if (op.b) y = tf.add(y, P[op.b]); break; | |
| } | |
| case "maxpool": y = tf.maxPool(a, op.k, op.stride, "valid"); break; | |
| case "bn": y = tf.batchNorm(a, P[op.mean], P[op.var], P[op.bias], P[op.scale], op.eps); break; // frozen stats | |
| case "inorm": { const { mean, variance } = tf.moments(a, [1, 2], true); | |
| y = tf.add(tf.mul(tf.div(tf.sub(a, mean), tf.sqrt(tf.add(variance, op.eps))), P[op.scale]), P[op.bias]); break; } | |
| case "relu": y = tf.relu(a); break; | |
| case "add": y = tf.add(a, T[op.x2]); break; | |
| case "concat": y = tf.concat(op.xs.map(k => T[k]), 3); break; | |
| case "crop": { const s = a.shape, ax = op.axis === "h" ? 1 : 2, begin = [0, 0, 0, 0], size = [-1, -1, -1, -1]; | |
| begin[ax] = op.start; size[ax] = s[ax] - op.start - op.end_margin; y = tf.slice(a, begin, size); break; } | |
| case "upsample": { const [n, h, w, c] = a.shape, f = op.factor; // nearest = repeat; rank-3 tiles (tf has no | |
| const yw = tf.reshape(tf.tile(tf.reshape(a, [n * h * w, 1, c]), [1, f, 1]), [n * h, 1, w * f * c]); // rank-6 tile grad) | |
| y = tf.reshape(tf.tile(yw, [1, f, 1]), [n, h * f, w * f, c]); break; } | |
| default: throw new Error(`op ${op.op}`); | |
| } | |
| T[op.y] = y; | |
| } | |
| return T[graph.output]; | |
| } | |
| function exportWeights() { // back to ONNX layout, trainable params only | |
| const out = {}; | |
| for (const [n, t] of Object.entries(P)) { | |
| if (!(t instanceof tf.Variable)) continue; | |
| const v = kind[n] ? tf.transpose(t, [3, 2, 0, 1]) : t; | |
| out[n] = v.dataSync().slice(); | |
| if (v !== t) v.dispose(); | |
| } | |
| return out; | |
| } | |
| const vars = Object.values(P).filter(t => t instanceof tf.Variable); | |
| return { forward, exportWeights, vars, params: P, context: graph.context }; | |
| } | |
| // masked BCE on logits: mean over mask>0 of max(z,0) - z*t + log(1+exp(-|z|)) | |
| export function maskedBce(tf, z, t, m) { | |
| return tf.tidy(() => { | |
| const l = tf.add(tf.sub(tf.relu(z), tf.mul(z, t)), tf.log1p(tf.exp(tf.neg(tf.abs(z))))); | |
| return tf.div(tf.sum(tf.mul(l, m)), tf.maximum(tf.sum(m), 1)); | |
| }); | |
| } | |
| export function trainStep(tf, net, opt, x, target, mask) { | |
| return opt.minimize(() => maskedBce(tf, net.forward(x), target, mask), true, net.vars); | |
| } | |
| // ---------------------------------------------------------------- structured weight transforms (3D folders) | |
| // ops on {data: Float32Array, shape}: index [axis,k] | permute [...] | reshape [...] | repeat_interleave [axis,n]; | |
| // recipes may start with {from: name} or {stack: [names], axis}. Same semantics as finetune_spec.py apply_ops. | |
| const prod = a => a.reduce((x, y) => x * y, 1); | |
| function nd(a, ops, get) { | |
| for (const o of ops) { | |
| if ("from" in o) a = get(o.from); | |
| else if ("stack" in o) { | |
| const parts = o.stack.map(get), ax = o.axis, s = parts[0].shape, outer = prod(s.slice(0, ax)), inner = prod(s.slice(ax)); | |
| const d = new Float32Array(outer * inner * parts.length); | |
| for (let i = 0; i < outer; i++) parts.forEach((p, k) => d.set(p.data.subarray(i * inner, (i + 1) * inner), (i * parts.length + k) * inner)); | |
| a = { data: d, shape: [...s.slice(0, ax), parts.length, ...s.slice(ax)] }; | |
| } else if ("index" in o) { | |
| const [ax, k] = o.index, s = a.shape, outer = prod(s.slice(0, ax)), inner = prod(s.slice(ax + 1)), n = s[ax]; | |
| const d = new Float32Array(outer * inner); | |
| for (let i = 0; i < outer; i++) d.set(a.data.subarray((i * n + k) * inner, (i * n + k + 1) * inner), i * inner); | |
| a = { data: d, shape: [...s.slice(0, ax), ...s.slice(ax + 1)] }; | |
| } else if ("reshape" in o) { | |
| if (prod(o.reshape) !== a.data.length) throw new Error("reshape size"); | |
| a = { data: a.data, shape: o.reshape.slice() }; | |
| } else if ("permute" in o) { | |
| const p = o.permute, s = a.shape, r = s.length, os = p.map(i => s[i]); | |
| const st = new Array(r); st[r - 1] = 1; for (let i = r - 2; i >= 0; i--) st[i] = st[i + 1] * s[i + 1]; | |
| const pst = p.map(i => st[i]), d = new Float32Array(a.data.length), idx = new Array(r).fill(0); | |
| for (let j = 0; j < d.length; j++) { | |
| let src = 0; for (let i = 0; i < r; i++) src += idx[i] * pst[i]; | |
| d[j] = a.data[src]; | |
| for (let i = r - 1; i >= 0; i--) { if (++idx[i] < os[i]) break; idx[i] = 0; } | |
| } | |
| a = { data: d, shape: os }; | |
| } else if ("repeat_interleave" in o) { | |
| const [ax, n] = o.repeat_interleave, s = a.shape, outer = prod(s.slice(0, ax + 1)), inner = prod(s.slice(ax + 1)); | |
| const d = new Float32Array(a.data.length * n); | |
| for (let i = 0; i < outer; i++) for (let k = 0; k < n; k++) d.set(a.data.subarray(i * inner, (i + 1) * inner), (i * n + k) * inner); | |
| a = { data: d, shape: s.map((v, i) => (i === ax ? v * n : v)) }; | |
| } else throw new Error(`transform ${JSON.stringify(o)}`); | |
| } | |
| return a; | |
| } | |
| export function readTorchParams(bytes, offs, torchParams) { | |
| // {torch_name: Float32Array (PyTorch layout)} from one served .onnx, via torch_params.json from_onnx recipes | |
| const names = new Set(); | |
| for (const p of Object.values(torchParams)) for (const o of p.from_onnx) (o.from ? [o.from] : o.stack || []).forEach(n => names.add(n)); | |
| const raw = readOnnxWeights(bytes, offs, [...names]); | |
| const get = n => ({ data: raw[n], shape: offs[n].shape }); | |
| const out = {}; | |
| for (const [k, p] of Object.entries(torchParams)) { | |
| const a = nd(null, p.from_onnx, get); | |
| if (a.data.length !== prod(p.shape)) throw new Error(k); | |
| out[k] = a.data; | |
| } | |
| return out; | |
| } | |
| export function toOnnxWeights(weightMap, params, offs) { | |
| // trained PyTorch-layout params (net.exportWeights()) -> {onnx_initializer: Float32Array} for writeOnnxWeights, | |
| // only initializers of trained params that are float in this file (offs) | |
| const out = {}; | |
| for (const [n, e] of Object.entries(weightMap)) { | |
| const o = offs[n]; | |
| if (!(e.torch in params) || !o || (o.dtype !== "float32" && o.dtype !== "float16")) continue; | |
| const p = params[e.torch]; | |
| out[n] = nd(p, e.to_onnx).data; | |
| } | |
| return out; | |
| } | |
| function buildNet3d(tf, graph, weights, { trainable = null } = {}) { | |
| const P = {}, kind = {}, shape = {}; | |
| for (const op of graph.ops) { | |
| if (op.op === "conv") kind[op.w] = "conv"; | |
| if (op.op === "conv_t") { kind[op.w] = "conv_t"; kind[op.b] = "conv_t_b"; } | |
| } | |
| for (const [n, meta] of Object.entries(graph.params)) { | |
| shape[n] = meta.shape; | |
| let t = tf.tensor(weights[n], meta.shape, "float32"); | |
| if (kind[n] === "conv") t = tf.transpose(t, [2, 3, 4, 1, 0]); // [O,I,kd,kh,kw] -> [kd,kh,kw,I,O] | |
| else if (kind[n] === "conv_t") t = tf.reshape(t, [meta.shape[0], meta.shape[1] * 8]); // [I,O,2,2,2] -> [I, O*8] | |
| const train = trainable ? trainable.has(n) : meta.trainable; | |
| P[n] = train ? tf.variable(t, true) : t; | |
| if (train) t.dispose(); | |
| } | |
| const flat = (a, f) => { const s = a.shape, c = s[s.length - 1]; return tf.reshape(f(tf.reshape(a, [-1, c])), s); }; | |
| const addBias = (a, b) => flat(a, v => tf.add(v, b)); | |
| function crop(a, ax, start, endMargin) { // ax 1|2|3 (d|h|w) of [N,D,H,W,C] via a rank-4 / rank-3 view | |
| const [n, d, h, w, c] = a.shape, len = a.shape[ax] - start - endMargin; | |
| const v = ax === 1 ? [n, d, h * w, c] : ax === 2 ? [n * d, h, w, c] : [n * d * h, w, c]; | |
| const begin = v.map(() => 0), size = v.map(() => -1); | |
| begin[1] = start; size[1] = len; | |
| const out = a.shape.slice(); out[ax] = len; | |
| return tf.reshape(tf.slice(tf.reshape(a, v), begin, size), out); | |
| } | |
| function repeatAxis(a, ax, f) { // nearest upsample along one axis: rank-3 tile | |
| const s = a.shape, outer = prod(s.slice(0, ax + 1)), inner = prod(s.slice(ax + 1)); | |
| const out = s.slice(); out[ax] *= f; | |
| return tf.reshape(tf.tile(tf.reshape(a, [outer, 1, inner]), [1, f, 1]), out); | |
| } | |
| function forward(x) { | |
| const T = { [graph.input]: x }; | |
| for (const op of graph.ops) { | |
| const a = T[op.x]; | |
| let y; | |
| switch (op.op) { | |
| case "conv": y = tf.conv3d(a, P[op.w], op.stride, "valid"); if (op.b) y = addBias(y, P[op.b]); break; | |
| case "conv_t": { // k = s = 2: [P,I] x [I, O*8] (+bias repeated 8x), then depth-to-space (rank <= 6) | |
| const [n, d, h, w, ci] = a.shape, o = P[op.w].shape[1] / 8; | |
| if (op.stride.some(s => s !== 2) || shape[op.w].slice(2).some(k => k !== 2)) throw new Error("conv_t: k=s=2 only"); | |
| let z = tf.matMul(tf.reshape(a, [-1, ci]), P[op.w]); | |
| if (op.b) z = tf.add(z, tf.reshape(tf.tile(tf.reshape(P[op.b], [o, 1]), [1, 8]), [o * 8])); | |
| z = tf.transpose(tf.reshape(z, [n * d, h, w, o, 2, 4]), [0, 4, 1, 2, 5, 3]); // [nd,a,h,w,(b e),o] | |
| z = tf.transpose(tf.reshape(z, [n * d * 2 * h, w, 2, 2, o]), [0, 2, 1, 3, 4]); // [nd a h, b, w, e, o] | |
| y = tf.reshape(z, [n, 2 * d, 2 * h, 2 * w, o]); break; | |
| } | |
| case "maxpool": y = tf.maxPool3d(a, op.k, op.stride, "valid"); break; | |
| case "bn": y = flat(a, v => tf.batchNorm(v, P[op.mean], P[op.var], P[op.bias], P[op.scale], op.eps)); break; | |
| case "inorm": { const { mean, variance } = tf.moments(a, [1, 2, 3], true); | |
| y = flat(tf.div(tf.sub(a, mean), tf.sqrt(tf.add(variance, op.eps))), v => tf.add(tf.mul(v, P[op.scale]), P[op.bias])); break; } | |
| case "relu": y = tf.relu(a); break; | |
| case "add": y = tf.add(a, T[op.x2]); break; | |
| case "concat": y = tf.concat(op.xs.map(k => T[k]), 4); break; | |
| case "crop": y = crop(a, 1 + "dhw".indexOf(op.axis), op.start, op.end_margin); break; | |
| case "upsample": y = [1, 2, 3].reduce((v, ax) => repeatAxis(v, ax, op.factor), a); break; | |
| default: throw new Error(`op ${op.op}`); | |
| } | |
| T[op.y] = y; | |
| } | |
| return T[graph.output]; | |
| } | |
| function exportWeights() { // trainable params, PyTorch layout, keyed by state_dict name | |
| const out = {}; | |
| for (const [n, t] of Object.entries(P)) { | |
| if (!(t instanceof tf.Variable)) continue; | |
| const v = kind[n] === "conv" ? tf.transpose(t, [4, 3, 0, 1, 2]) : t; // conv_t: [I, O*8] is already [I,O,2,2,2] flat | |
| out[n] = { data: v.dataSync().slice(), shape: shape[n] }; | |
| if (v !== t) v.dispose(); | |
| } | |
| return out; | |
| } | |
| const vars = Object.values(P).filter(t => t instanceof tf.Variable); | |
| return { forward, exportWeights, vars, params: P, context: graph.context }; | |
| } | |