Knowledge Lifecycle
Browse files
main.js
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// LLM Knowledge Lifecycle: browser-side measurement on GPT-2.
|
| 2 |
+
// The entire computation runs locally: the model is downloaded once from the
|
| 3 |
+
// Hugging Face Hub, and every forward pass happens in this tab.
|
| 4 |
+
|
| 5 |
+
import { AutoTokenizer, AutoModelForCausalLM } from
|
| 6 |
+
"https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.4.0";
|
| 7 |
+
|
| 8 |
+
const PRESETS = {
|
| 9 |
+
vioxx: {
|
| 10 |
+
query: "Question: Is Vioxx safe to prescribe? Answer: Vioxx is considered",
|
| 11 |
+
context: "Context: In September 2004, Merck voluntarily withdrew Vioxx after trials revealed increased cardiovascular risks.",
|
| 12 |
+
answer: " withdrawn",
|
| 13 |
+
reading: "The paper's headline case. The withdrawal notice is in the prompt, yet the correct answer stays near one in a hundred thousand. Expect D_sync deep past the 9.2-nat failure threshold; the paper's fp32 run measures 12.05 nats with I_ctx = 0.033.",
|
| 14 |
+
},
|
| 15 |
+
monarch: {
|
| 16 |
+
query: "Question: Who is the current British monarch? Answer: The current British monarch is",
|
| 17 |
+
context: "Context: Queen Elizabeth II died in September 2022. Charles III acceded to the throne and is the reigning King of the United Kingdom.",
|
| 18 |
+
answer: " Charles",
|
| 19 |
+
reading: "A subtler failure. The context raises P(Charles), but its strongest effect is boosting “ Queen”: merely mentioning the late monarch reinforces the stale association. Correct information can strengthen the wrong answer.",
|
| 20 |
+
},
|
| 21 |
+
twitter: {
|
| 22 |
+
query: "Question: What is the social network Twitter called today? Answer: Twitter is now called",
|
| 23 |
+
context: "Context: In July 2023, Twitter was rebranded as X under Elon Musk's ownership.",
|
| 24 |
+
answer: " X",
|
| 25 |
+
reading: "The context moves the distribution hard (the paper's fp32 run: I_ctx = 0.9 nats) and lifts the correct answer by orders of magnitude. The model still answers “Twitter”. Influence without resolution.",
|
| 26 |
+
},
|
| 27 |
+
};
|
| 28 |
+
|
| 29 |
+
const $ = (id) => document.getElementById(id);
|
| 30 |
+
let tokenizer = null;
|
| 31 |
+
let model = null;
|
| 32 |
+
|
| 33 |
+
// ---------- tabs ----------
|
| 34 |
+
document.querySelectorAll(".tab").forEach((btn) => {
|
| 35 |
+
btn.addEventListener("click", () => {
|
| 36 |
+
document.querySelectorAll(".tab").forEach((b) => b.classList.remove("active"));
|
| 37 |
+
document.querySelectorAll(".panel").forEach((p) => p.classList.remove("active"));
|
| 38 |
+
btn.classList.add("active");
|
| 39 |
+
$(btn.dataset.tab).classList.add("active");
|
| 40 |
+
});
|
| 41 |
+
});
|
| 42 |
+
|
| 43 |
+
// ---------- presets ----------
|
| 44 |
+
function applyPreset(key) {
|
| 45 |
+
const p = PRESETS[key];
|
| 46 |
+
$("query").value = p.query;
|
| 47 |
+
$("context").value = p.context;
|
| 48 |
+
$("answer").value = p.answer;
|
| 49 |
+
$("preset-reading").textContent = p.reading;
|
| 50 |
+
document.querySelectorAll(".preset").forEach((b) =>
|
| 51 |
+
b.classList.toggle("active", b.dataset.preset === key));
|
| 52 |
+
}
|
| 53 |
+
document.querySelectorAll(".preset").forEach((btn) =>
|
| 54 |
+
btn.addEventListener("click", () => applyPreset(btn.dataset.preset)));
|
| 55 |
+
applyPreset("vioxx");
|
| 56 |
+
|
| 57 |
+
// ---------- model loading ----------
|
| 58 |
+
$("load-btn").addEventListener("click", async () => {
|
| 59 |
+
$("load-btn").disabled = true;
|
| 60 |
+
$("load-bar-wrap").hidden = false;
|
| 61 |
+
const status = $("load-status");
|
| 62 |
+
const seen = {};
|
| 63 |
+
const progress = (info) => {
|
| 64 |
+
if (info.status === "progress" && info.total) {
|
| 65 |
+
seen[info.file] = info.loaded / info.total;
|
| 66 |
+
const vals = Object.values(seen);
|
| 67 |
+
const pct = (vals.reduce((a, b) => a + b, 0) / vals.length) * 100;
|
| 68 |
+
$("load-bar").style.width = pct.toFixed(1) + "%";
|
| 69 |
+
status.textContent = "downloading " + pct.toFixed(0) + "%";
|
| 70 |
+
}
|
| 71 |
+
};
|
| 72 |
+
try {
|
| 73 |
+
status.textContent = "downloading";
|
| 74 |
+
tokenizer = await AutoTokenizer.from_pretrained("Xenova/gpt2", { progress_callback: progress });
|
| 75 |
+
// The Xenova/gpt2 repo uses legacy file naming: dtype "q8" maps to the
|
| 76 |
+
// "_quantized" suffix, and the merged decoder is the 128 MB build.
|
| 77 |
+
model = await AutoModelForCausalLM.from_pretrained("Xenova/gpt2", {
|
| 78 |
+
model_file_name: "decoder_model_merged",
|
| 79 |
+
dtype: "q8",
|
| 80 |
+
progress_callback: progress,
|
| 81 |
+
});
|
| 82 |
+
status.textContent = "ready (GPT-2 base, 8-bit quantized, running locally)";
|
| 83 |
+
$("load-bar").style.width = "100%";
|
| 84 |
+
$("run").disabled = false;
|
| 85 |
+
} catch (e) {
|
| 86 |
+
status.textContent = "failed to load: " + e.message;
|
| 87 |
+
$("load-btn").disabled = false;
|
| 88 |
+
}
|
| 89 |
+
});
|
| 90 |
+
|
| 91 |
+
// ---------- measurement ----------
|
| 92 |
+
async function nextTokenDistribution(text) {
|
| 93 |
+
const inputs = await tokenizer(text);
|
| 94 |
+
const { logits } = await model(inputs);
|
| 95 |
+
const [, T, V] = logits.dims;
|
| 96 |
+
const row = logits.data.slice((T - 1) * V, T * V);
|
| 97 |
+
// stable softmax
|
| 98 |
+
let max = -Infinity;
|
| 99 |
+
for (let i = 0; i < V; i++) if (row[i] > max) max = row[i];
|
| 100 |
+
let sum = 0;
|
| 101 |
+
const probs = new Float64Array(V);
|
| 102 |
+
for (let i = 0; i < V; i++) { probs[i] = Math.exp(row[i] - max); sum += probs[i]; }
|
| 103 |
+
for (let i = 0; i < V; i++) probs[i] /= sum;
|
| 104 |
+
return probs;
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
function topK(probs, k) {
|
| 108 |
+
const idx = [];
|
| 109 |
+
const taken = new Set();
|
| 110 |
+
for (let n = 0; n < k; n++) {
|
| 111 |
+
let best = -1, bp = -1;
|
| 112 |
+
for (let i = 0; i < probs.length; i++) {
|
| 113 |
+
if (!taken.has(i) && probs[i] > bp) { bp = probs[i]; best = i; }
|
| 114 |
+
}
|
| 115 |
+
taken.add(best);
|
| 116 |
+
idx.push(best);
|
| 117 |
+
}
|
| 118 |
+
return idx;
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
function renderTop(tableId, probs, targetId) {
|
| 122 |
+
const rows = topK(probs, 8).map((i) => {
|
| 123 |
+
const tok = tokenizer.decode([i]);
|
| 124 |
+
const hit = i === targetId ? ' class="hit"' : "";
|
| 125 |
+
return `<tr${hit}><td>${JSON.stringify(tok).slice(1, -1)}</td><td>${(probs[i] * 100).toFixed(4)}%</td></tr>`;
|
| 126 |
+
});
|
| 127 |
+
$(tableId).innerHTML = "<tr><th>token</th><th>probability</th></tr>" + rows.join("");
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
$("run").addEventListener("click", async () => {
|
| 131 |
+
const runBtn = $("run");
|
| 132 |
+
runBtn.disabled = true;
|
| 133 |
+
runBtn.textContent = "measuring...";
|
| 134 |
+
try {
|
| 135 |
+
const query = $("query").value.trim();
|
| 136 |
+
const context = $("context").value.trim();
|
| 137 |
+
let answer = $("answer").value;
|
| 138 |
+
if (!answer.startsWith(" ")) answer = " " + answer.trim();
|
| 139 |
+
|
| 140 |
+
const pPlain = await nextTokenDistribution(query);
|
| 141 |
+
const pCtx = await nextTokenDistribution(context + "\n" + query);
|
| 142 |
+
|
| 143 |
+
const ids = tokenizer.encode(answer);
|
| 144 |
+
const pieces = ids.map((i) => tokenizer.decode([i]));
|
| 145 |
+
const target = ids[0];
|
| 146 |
+
|
| 147 |
+
const pt0 = Math.max(pPlain[target], 1e-12);
|
| 148 |
+
const pt1 = Math.max(pCtx[target], 1e-12);
|
| 149 |
+
const dsync = -Math.log(pt1);
|
| 150 |
+
let ictx = 0;
|
| 151 |
+
for (let i = 0; i < pCtx.length; i++) {
|
| 152 |
+
if (pCtx[i] > 0 && pPlain[i] > 0) ictx += pCtx[i] * Math.log(pCtx[i] / pPlain[i]);
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
let chipText, chipClass, detail;
|
| 156 |
+
if (dsync > 9.2) {
|
| 157 |
+
if (ictx < 0.05) {
|
| 158 |
+
chipText = "resolution failure"; chipClass = "chip-fail";
|
| 159 |
+
detail = "context ignored; no realistic decoding recovers the correct answer";
|
| 160 |
+
} else {
|
| 161 |
+
chipText = "drift, context losing"; chipClass = "chip-fail";
|
| 162 |
+
detail = "context influential but losing; no realistic decoding recovers the correct answer";
|
| 163 |
+
}
|
| 164 |
+
} else if (dsync > 4.6) {
|
| 165 |
+
chipText = "severe drift"; chipClass = "chip-drift";
|
| 166 |
+
detail = "correct answer below 1% probability";
|
| 167 |
+
} else if (dsync > 0.7) {
|
| 168 |
+
chipText = "drift"; chipClass = "chip-drift";
|
| 169 |
+
detail = "correct answer no longer holds most of the probability mass";
|
| 170 |
+
} else {
|
| 171 |
+
chipText = "synchronized"; chipClass = "chip-ok";
|
| 172 |
+
detail = "correct answer holds at least half the probability mass";
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
// Marker positions follow the drawn zone boundaries: D_sync thresholds
|
| 176 |
+
// 0.7 / 4.6 / 9.2 sit at 5% / 33% / 66%, scale capped at 14 nats.
|
| 177 |
+
const dPos = dsync <= 0.7 ? (dsync / 0.7) * 5
|
| 178 |
+
: dsync <= 4.6 ? 5 + ((dsync - 0.7) / 3.9) * 28
|
| 179 |
+
: dsync <= 9.2 ? 33 + ((dsync - 4.6) / 4.6) * 33
|
| 180 |
+
: Math.min(100, 66 + ((dsync - 9.2) / 4.8) * 34);
|
| 181 |
+
// I_ctx thresholds 0.05 / 0.5 sit at 10% / 50%, scale capped at 2 nats.
|
| 182 |
+
const iPos = ictx <= 0.05 ? (ictx / 0.05) * 10
|
| 183 |
+
: ictx <= 0.5 ? 10 + ((ictx - 0.05) / 0.45) * 40
|
| 184 |
+
: Math.min(100, 50 + ((ictx - 0.5) / 1.5) * 50);
|
| 185 |
+
|
| 186 |
+
const chip = $("verdict-chip");
|
| 187 |
+
chip.textContent = chipText;
|
| 188 |
+
chip.className = "chip " + chipClass;
|
| 189 |
+
$("r-verdict-detail").textContent = detail;
|
| 190 |
+
$("g-marker").style.left = dPos.toFixed(1) + "%";
|
| 191 |
+
$("g-ictx").style.left = iPos.toFixed(1) + "%";
|
| 192 |
+
|
| 193 |
+
$("r-tok").textContent = `${JSON.stringify(pieces)} (${ids.length} piece(s); first piece measured)`;
|
| 194 |
+
$("r-p0").textContent = pt0.toExponential(2);
|
| 195 |
+
$("r-p1").textContent = pt1.toExponential(2);
|
| 196 |
+
$("r-dsync").textContent = dsync.toFixed(2) + " nats";
|
| 197 |
+
$("r-ictx").textContent = ictx.toFixed(3) + " nats";
|
| 198 |
+
renderTop("t-plain", pPlain, target);
|
| 199 |
+
renderTop("t-ctx", pCtx, target);
|
| 200 |
+
$("results").hidden = false;
|
| 201 |
+
} finally {
|
| 202 |
+
runBtn.disabled = false;
|
| 203 |
+
runBtn.textContent = "Measure";
|
| 204 |
+
}
|
| 205 |
+
});
|