<!-- LoRA demo: visualize frozen W, low-rank delta W = B*A, and adapted W + delta W -->
<div class="demo-wrap">
<div class="controls">
<label for="rank-slider">Rank r = <strong id="rank-val">4</strong>
<span class="dim-label">(d=<span id="lbl-d">8</span>, k=<span id="lbl-k">8</span>)</span>
</label>
<input id="rank-slider" type="range" min="1" max="8" value="4" step="1">
</div>
<div class="param-row">
<div class="param-box full-box">
<div class="param-title">Full params</div>
<div class="param-num" id="full-params">64</div>
<div class="param-sub">d × k</div>
</div>
<div class="param-box lora-box">
<div class="param-title">LoRA params</div>
<div class="param-num" id="lora-params">64</div>
<div class="param-sub">r(d + k)</div>
</div>
<div class="param-box save-box">
<div class="param-title">Reduction</div>
<div class="param-num" id="reduction">1×</div>
<div class="param-sub">fewer params</div>
</div>
</div>
<div class="matrix-section">
<div class="mat-wrap">
<div class="mat-title">W (frozen)</div>
<canvas id="canvas-W" width="128" height="128"></canvas>
</div>
<div class="mat-wrap">
<div class="mat-title">ΔW = B·A (trained)</div>
<canvas id="canvas-DW" width="128" height="128"></canvas>
</div>
<div class="mat-wrap">
<div class="mat-title">W + ΔW (adapted)</div>
<canvas id="canvas-WP" width="128" height="128"></canvas>
</div>
</div>
<div class="error-row">
<span>Relative approx. error: </span><strong id="approx-err">—</strong>
|
<span>Rank: </span><strong id="rank-label">—</strong>
</div>
<div class="legend">
<span class="leg-neg">negative</span>
<span class="leg-zero">0</span>
<span class="leg-pos">positive</span>
</div>
</div>
/* Styles for LoRA low-rank update visualizer */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; background: transparent; }
.demo-wrap { padding: .6rem .4rem; max-width: 520px; margin: 0 auto; }
.controls { margin-bottom: .6rem; }
label { font-size: .9rem; color: #444; display: block; margin-bottom: .3rem; }
.dim-label { font-size: .8rem; color: #777; }
input[type=range] { width: 100%; accent-color: #1d3557; }
.param-row { display: flex; gap: .5rem; margin-bottom: .8rem; }
.param-box { flex: 1; border-radius: 8px; padding: .5rem .4rem; text-align: center; border: 1px solid #cdd9e3; }
.full-box { background: #e8eef3; }
.lora-box { background: #d4edda; }
.save-box { background: #fff3cd; }
.param-title { font-size: .72rem; color: #555; margin-bottom: .15rem; }
.param-num { font-size: 1.3rem; font-weight: 700; color: #1d3557; }
.param-sub { font-size: .68rem; color: #888; }
.matrix-section { display: flex; gap: .6rem; justify-content: center; margin-bottom: .5rem; flex-wrap: wrap; }
.mat-wrap { text-align: center; }
.mat-title { font-size: .75rem; color: #444; margin-bottom: .2rem; }
canvas { border: 1px solid #cdd9e3; border-radius: 4px; image-rendering: pixelated; display: block; }
.error-row { font-size: .82rem; color: #444; margin-bottom: .4rem; }
.legend { display: flex; gap: .5rem; font-size: .75rem; align-items: center; }
.leg-neg { background: #c92f3c; color: #fff; padding: 1px 6px; border-radius: 3px; }
.leg-zero { background: #e8eef3; padding: 1px 6px; border-radius: 3px; }
.leg-pos { background: #0a7d33; color: #fff; padding: 1px 6px; border-radius: 3px; }
// LoRA low-rank update demo: approximate delta_W = B*A using truncated SVD of the true delta
const D = 8, K = 8; // weight matrix dimensions D x K
// Target weight matrix W* (what full fine-tuning would converge to)
const TARGET = [
[ 0.8, -0.3, 0.5, 0.1, -0.6, 0.2, 0.4, -0.1],
[-0.2, 0.7, -0.4, 0.6, 0.3, -0.5, 0.1, 0.8],
[ 0.5, 0.1, 0.9, -0.7, 0.2, 0.4, -0.3, 0.6],
[-0.3, 0.6, -0.2, 0.8, -0.5, 0.1, 0.7, -0.4],
[ 0.4, -0.8, 0.3, -0.1, 0.9, -0.6, 0.2, 0.5],
[-0.6, 0.2, 0.7, -0.4, 0.1, 0.8, -0.3, 0.3],
[ 0.1, 0.5, -0.6, 0.3, -0.4, 0.7, 0.8, -0.2],
[ 0.7, -0.1, 0.2, 0.5, -0.7, 0.3, -0.5, 0.9]
];
// Frozen base weight W (pre-trained, slightly perturbed from target)
const W = TARGET.map(row => row.map(v => v * 0.6 + (Math.random() - 0.5) * 0.1));
// Truncated SVD helpers to compute the rank-r approximation of delta W
function matMul(A, B) {
const rows = A.length, inner = B.length, cols = B[0].length;
const C = Array.from({length: rows}, () => new Array(cols).fill(0));
for (let i = 0; i < rows; i++)
for (let k = 0; k < inner; k++)
for (let j = 0; j < cols; j++)
C[i][j] += A[i][k] * B[k][j];
return C;
}
function powerIter(M, iters) {
// power iteration to find the top singular vector of M
let v = Array.from({length: M[0].length}, () => Math.random() - 0.5);
for (let t = 0; t < iters; t++) {
const Mv = M.map(row => row.reduce((s, x, j) => s + x * v[j], 0));
const MTMv = M[0].map((_, j) => M.reduce((s, row) => s + row[j] * Mv[M.indexOf(row)], 0));
const norm = Math.sqrt(MTMv.reduce((s, x) => s + x * x, 0));
v = norm > 1e-10 ? MTMv.map(x => x / norm) : v;
}
return v;
}
function deflate(M, u, s, v) {
// remove the contribution of the found component
return M.map((row, i) => row.map((x, j) => x - s * u[i] * v[j]));
}
function approxSVD(M, rank) {
// compute rank-r SVD approximation of M
let R = M.map(r => [...r]);
const Us = [], Ss = [], Vs = [];
for (let r = 0; r < rank; r++) {
const vv = powerIter(R, 60);
const Mv = R.map(row => row.reduce((s, x, j) => s + x * vv[j], 0));
const sigma = Math.sqrt(Mv.reduce((s, x) => s + x * x, 0));
if (sigma < 1e-10) break;
const uu = Mv.map(x => x / sigma);
Us.push(uu); Ss.push(sigma); Vs.push(vv);
R = deflate(R, uu, sigma, vv);
}
return { Us, Ss, Vs };
}
function buildDeltaW(M, rank) {
// build the rank-r approximation of the true delta W = W* - W
const DELTA = TARGET.map((row, i) => row.map((v, j) => v - M[i][j]));
const { Us, Ss, Vs } = approxSVD(DELTA, rank);
const approx = Array.from({length: D}, () => new Array(K).fill(0));
for (let r = 0; r < Us.length; r++)
for (let i = 0; i < D; i++)
for (let j = 0; j < K; j++)
approx[i][j] += Ss[r] * Us[r][i] * Vs[r][j];
return approx;
}
function frobErr(A, B) {
// Frobenius norm of the residual
let s = 0;
for (let i = 0; i < A.length; i++)
for (let j = 0; j < A[0].length; j++) {
const d = A[i][j] - B[i][j]; s += d * d;
}
return Math.sqrt(s);
}
// draw a matrix as a heatmap on the given canvas, range [-vmax, vmax]
function drawMatrix(canvasId, M, vmax) {
const canvas = document.getElementById(canvasId);
const ctx = canvas.getContext('2d');
const cw = canvas.width, ch = canvas.height;
const cellW = cw / K, cellH = ch / D;
for (let i = 0; i < D; i++) {
for (let j = 0; j < K; j++) {
const v = Math.max(-vmax, Math.min(vmax, M[i][j]));
const t = v / vmax;
let r, g, b;
if (t < 0) { r = Math.round(201 * (-t)); g = Math.round(47 * (-t)); b = Math.round(60 * (-t)); r = 232 + Math.round((r - 232) * (-t)); g = 238 + Math.round((g - 238) * (-t)); b = 243 + Math.round((b - 243) * (-t)); }
else { r = Math.round(232 + (10 - 232) * t); g = Math.round(238 + (125 - 238) * t); b = Math.round(243 + (51 - 243) * t); }
ctx.fillStyle = `rgb(${r},${g},${b})`;
ctx.fillRect(j * cellW, i * cellH, cellW, cellH);
}
}
}
function update() {
const rank = parseInt(document.getElementById('rank-slider').value, 10);
document.getElementById('rank-val').textContent = rank;
document.getElementById('lbl-d').textContent = D;
document.getElementById('lbl-k').textContent = K;
const loraParams = rank * (D + K);
const fullParams = D * K;
document.getElementById('full-params').textContent = fullParams;
document.getElementById('lora-params').textContent = loraParams;
const ratio = fullParams / loraParams;
document.getElementById('reduction').textContent = ratio.toFixed(1) + '×';
document.getElementById('rank-label').textContent = 'r = ' + rank;
const DW = buildDeltaW(W, rank);
const WP = W.map((row, i) => row.map((v, j) => v + DW[i][j]));
// relative error: how much of the delta is not captured by the rank-r approximation
const fullDW = TARGET.map((row, i) => row.map((v, j) => v - W[i][j]));
const err = frobErr(WP, TARGET) / frobErr(fullDW, Array.from({length: D}, () => new Array(K).fill(0)));
document.getElementById('approx-err').textContent = (err * 100).toFixed(1) + '%';
drawMatrix('canvas-W', W, 1.0);
drawMatrix('canvas-DW', DW, 0.5);
drawMatrix('canvas-WP', WP, 1.0);
}
document.getElementById('rank-slider').addEventListener('input', update);
update();
<!-- LoRA demo: visualize frozen W, low-rank delta W = B*A, and adapted W + delta W -->
<div class="demo-wrap">
<div class="controls">
<label for="rank-slider">Rank r = <strong id="rank-val">4</strong>
<span class="dim-label">(d=<span id="lbl-d">8</span>, k=<span id="lbl-k">8</span>)</span>
</label>
<input id="rank-slider" type="range" min="1" max="8" value="4" step="1">
</div>
<div class="param-row">
<div class="param-box full-box">
<div class="param-title">Full params</div>
<div class="param-num" id="full-params">64</div>
<div class="param-sub">d × k</div>
</div>
<div class="param-box lora-box">
<div class="param-title">LoRA params</div>
<div class="param-num" id="lora-params">64</div>
<div class="param-sub">r(d + k)</div>
</div>
<div class="param-box save-box">
<div class="param-title">Reduction</div>
<div class="param-num" id="reduction">1×</div>
<div class="param-sub">fewer params</div>
</div>
</div>
<div class="matrix-section">
<div class="mat-wrap">
<div class="mat-title">W (frozen)</div>
<canvas id="canvas-W" width="128" height="128"></canvas>
</div>
<div class="mat-wrap">
<div class="mat-title">ΔW = B·A (trained)</div>
<canvas id="canvas-DW" width="128" height="128"></canvas>
</div>
<div class="mat-wrap">
<div class="mat-title">W + ΔW (adapted)</div>
<canvas id="canvas-WP" width="128" height="128"></canvas>
</div>
</div>
<div class="error-row">
<span>Relative approx. error: </span><strong id="approx-err">—</strong>
|
<span>Rank: </span><strong id="rank-label">—</strong>
</div>
<div class="legend">
<span class="leg-neg">negative</span>
<span class="leg-zero">0</span>
<span class="leg-pos">positive</span>
</div>
</div>
/* Styles for LoRA low-rank update visualizer */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; background: transparent; }
.demo-wrap { padding: .6rem .4rem; max-width: 520px; margin: 0 auto; }
.controls { margin-bottom: .6rem; }
label { font-size: .9rem; color: #444; display: block; margin-bottom: .3rem; }
.dim-label { font-size: .8rem; color: #777; }
input[type=range] { width: 100%; accent-color: #1d3557; }
.param-row { display: flex; gap: .5rem; margin-bottom: .8rem; }
.param-box { flex: 1; border-radius: 8px; padding: .5rem .4rem; text-align: center; border: 1px solid #cdd9e3; }
.full-box { background: #e8eef3; }
.lora-box { background: #d4edda; }
.save-box { background: #fff3cd; }
.param-title { font-size: .72rem; color: #555; margin-bottom: .15rem; }
.param-num { font-size: 1.3rem; font-weight: 700; color: #1d3557; }
.param-sub { font-size: .68rem; color: #888; }
.matrix-section { display: flex; gap: .6rem; justify-content: center; margin-bottom: .5rem; flex-wrap: wrap; }
.mat-wrap { text-align: center; }
.mat-title { font-size: .75rem; color: #444; margin-bottom: .2rem; }
canvas { border: 1px solid #cdd9e3; border-radius: 4px; image-rendering: pixelated; display: block; }
.error-row { font-size: .82rem; color: #444; margin-bottom: .4rem; }
.legend { display: flex; gap: .5rem; font-size: .75rem; align-items: center; }
.leg-neg { background: #c92f3c; color: #fff; padding: 1px 6px; border-radius: 3px; }
.leg-zero { background: #e8eef3; padding: 1px 6px; border-radius: 3px; }
.leg-pos { background: #0a7d33; color: #fff; padding: 1px 6px; border-radius: 3px; }
// LoRA low-rank update demo: approximate delta_W = B*A using truncated SVD of the true delta
const D = 8, K = 8; // weight matrix dimensions D x K
// Target weight matrix W* (what full fine-tuning would converge to)
const TARGET = [
[ 0.8, -0.3, 0.5, 0.1, -0.6, 0.2, 0.4, -0.1],
[-0.2, 0.7, -0.4, 0.6, 0.3, -0.5, 0.1, 0.8],
[ 0.5, 0.1, 0.9, -0.7, 0.2, 0.4, -0.3, 0.6],
[-0.3, 0.6, -0.2, 0.8, -0.5, 0.1, 0.7, -0.4],
[ 0.4, -0.8, 0.3, -0.1, 0.9, -0.6, 0.2, 0.5],
[-0.6, 0.2, 0.7, -0.4, 0.1, 0.8, -0.3, 0.3],
[ 0.1, 0.5, -0.6, 0.3, -0.4, 0.7, 0.8, -0.2],
[ 0.7, -0.1, 0.2, 0.5, -0.7, 0.3, -0.5, 0.9]
];
// Frozen base weight W (pre-trained, slightly perturbed from target)
const W = TARGET.map(row => row.map(v => v * 0.6 + (Math.random() - 0.5) * 0.1));
// Truncated SVD helpers to compute the rank-r approximation of delta W
function matMul(A, B) {
const rows = A.length, inner = B.length, cols = B[0].length;
const C = Array.from({length: rows}, () => new Array(cols).fill(0));
for (let i = 0; i < rows; i++)
for (let k = 0; k < inner; k++)
for (let j = 0; j < cols; j++)
C[i][j] += A[i][k] * B[k][j];
return C;
}
function powerIter(M, iters) {
// power iteration to find the top singular vector of M
let v = Array.from({length: M[0].length}, () => Math.random() - 0.5);
for (let t = 0; t < iters; t++) {
const Mv = M.map(row => row.reduce((s, x, j) => s + x * v[j], 0));
const MTMv = M[0].map((_, j) => M.reduce((s, row) => s + row[j] * Mv[M.indexOf(row)], 0));
const norm = Math.sqrt(MTMv.reduce((s, x) => s + x * x, 0));
v = norm > 1e-10 ? MTMv.map(x => x / norm) : v;
}
return v;
}
function deflate(M, u, s, v) {
// remove the contribution of the found component
return M.map((row, i) => row.map((x, j) => x - s * u[i] * v[j]));
}
function approxSVD(M, rank) {
// compute rank-r SVD approximation of M
let R = M.map(r => [...r]);
const Us = [], Ss = [], Vs = [];
for (let r = 0; r < rank; r++) {
const vv = powerIter(R, 60);
const Mv = R.map(row => row.reduce((s, x, j) => s + x * vv[j], 0));
const sigma = Math.sqrt(Mv.reduce((s, x) => s + x * x, 0));
if (sigma < 1e-10) break;
const uu = Mv.map(x => x / sigma);
Us.push(uu); Ss.push(sigma); Vs.push(vv);
R = deflate(R, uu, sigma, vv);
}
return { Us, Ss, Vs };
}
function buildDeltaW(M, rank) {
// build the rank-r approximation of the true delta W = W* - W
const DELTA = TARGET.map((row, i) => row.map((v, j) => v - M[i][j]));
const { Us, Ss, Vs } = approxSVD(DELTA, rank);
const approx = Array.from({length: D}, () => new Array(K).fill(0));
for (let r = 0; r < Us.length; r++)
for (let i = 0; i < D; i++)
for (let j = 0; j < K; j++)
approx[i][j] += Ss[r] * Us[r][i] * Vs[r][j];
return approx;
}
function frobErr(A, B) {
// Frobenius norm of the residual
let s = 0;
for (let i = 0; i < A.length; i++)
for (let j = 0; j < A[0].length; j++) {
const d = A[i][j] - B[i][j]; s += d * d;
}
return Math.sqrt(s);
}
// draw a matrix as a heatmap on the given canvas, range [-vmax, vmax]
function drawMatrix(canvasId, M, vmax) {
const canvas = document.getElementById(canvasId);
const ctx = canvas.getContext('2d');
const cw = canvas.width, ch = canvas.height;
const cellW = cw / K, cellH = ch / D;
for (let i = 0; i < D; i++) {
for (let j = 0; j < K; j++) {
const v = Math.max(-vmax, Math.min(vmax, M[i][j]));
const t = v / vmax;
let r, g, b;
if (t < 0) { r = Math.round(201 * (-t)); g = Math.round(47 * (-t)); b = Math.round(60 * (-t)); r = 232 + Math.round((r - 232) * (-t)); g = 238 + Math.round((g - 238) * (-t)); b = 243 + Math.round((b - 243) * (-t)); }
else { r = Math.round(232 + (10 - 232) * t); g = Math.round(238 + (125 - 238) * t); b = Math.round(243 + (51 - 243) * t); }
ctx.fillStyle = `rgb(${r},${g},${b})`;
ctx.fillRect(j * cellW, i * cellH, cellW, cellH);
}
}
}
function update() {
const rank = parseInt(document.getElementById('rank-slider').value, 10);
document.getElementById('rank-val').textContent = rank;
document.getElementById('lbl-d').textContent = D;
document.getElementById('lbl-k').textContent = K;
const loraParams = rank * (D + K);
const fullParams = D * K;
document.getElementById('full-params').textContent = fullParams;
document.getElementById('lora-params').textContent = loraParams;
const ratio = fullParams / loraParams;
document.getElementById('reduction').textContent = ratio.toFixed(1) + '×';
document.getElementById('rank-label').textContent = 'r = ' + rank;
const DW = buildDeltaW(W, rank);
const WP = W.map((row, i) => row.map((v, j) => v + DW[i][j]));
// relative error: how much of the delta is not captured by the rank-r approximation
const fullDW = TARGET.map((row, i) => row.map((v, j) => v - W[i][j]));
const err = frobErr(WP, TARGET) / frobErr(fullDW, Array.from({length: D}, () => new Array(K).fill(0)));
document.getElementById('approx-err').textContent = (err * 100).toFixed(1) + '%';
drawMatrix('canvas-W', W, 1.0);
drawMatrix('canvas-DW', DW, 0.5);
drawMatrix('canvas-WP', WP, 1.0);
}
document.getElementById('rank-slider').addEventListener('input', update);
update();
Notice the trade-off: at rank 1 you train almost nothing but the approximation is coarse; as rank approaches the full dimension d you recover full fine-tuning at full cost. In practice, r∈{4,8,16} captures most of what a task needs while keeping the parameter count tiny.
Comments
Loading comments...