|
|
| <style> |
| .pe-wrap { font-family: var(--font-sans); padding: 1rem 0; } |
| .pe-title { font-size: 15px; font-weight: 500; color: var(--color-text-primary); margin-bottom: 4px; } |
| .pe-sub { font-size: 12px; color: var(--color-text-secondary); margin-bottom: 16px; } |
| .pe-canvas-wrap { position: relative; width: 100%; } |
| .pe-axis-label { font-size: 12px; color: var(--color-text-secondary); text-align: center; } |
| .pe-y-label { writing-mode: vertical-rl; transform: rotate(180deg); font-size: 12px; color: var(--color-text-secondary); display: flex; align-items: center; justify-content: center; min-width: 20px; } |
| .pe-row { display: flex; align-items: stretch; gap: 8px; } |
| .pe-legend { display: flex; align-items: center; gap: 8px; margin-top: 10px; font-size: 11px; color: var(--color-text-secondary); } |
| .pe-legend-bar { flex: 1; height: 12px; border-radius: 3px; } |
| .annotation-box { background: var(--color-background-secondary); border: 0.5px solid var(--color-border-tertiary); border-radius: 8px; padding: 10px 14px; margin-top: 14px; font-size: 12px; color: var(--color-text-secondary); line-height: 1.6; } |
| .ann-row { display: flex; gap: 16px; } |
| .ann-col { flex: 1; } |
| .ann-head { font-weight: 500; font-size: 12px; color: var(--color-text-primary); margin-bottom: 3px; } |
| .highlight-col { border: 2px solid #EF9F27; border-radius: 3px; position: absolute; pointer-events: none; } |
| .highlight-row { border: 2px solid #1D9E75; border-radius: 3px; position: absolute; pointer-events: none; } |
| .pe-controls { display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 10px; align-items: center; } |
| .pe-btn { background: var(--color-background-secondary); border: 0.5px solid var(--color-border-secondary); border-radius: 6px; padding: 4px 12px; font-size: 12px; cursor: pointer; color: var(--color-text-primary); } |
| .pe-btn.active { background: var(--color-background-info); color: var(--color-text-info); border-color: var(--color-border-info); } |
| .tooltip { position: absolute; background: var(--color-background-primary); border: 0.5px solid var(--color-border-secondary); border-radius: 6px; padding: 6px 10px; font-size: 11px; color: var(--color-text-primary); pointer-events: none; opacity: 0; transition: opacity 0.15s; white-space: nowrap; z-index: 10; } |
| </style> |
| <div class="pe-wrap"> |
| <div class="pe-title">Positional Encoding Matrix</div> |
| <div class="pe-sub">Each cell = PE(pos, i). Hover to inspect. Click a column (dimension) or row (position) to highlight it.</div> |
|
|
| <div class="pe-controls"> |
| <span style="font-size:12px;color:var(--color-text-secondary)">Hover mode:</span> |
| <button class="pe-btn active" id="btn-cell" onclick="setMode('cell')">Cell value</button> |
| <button class="pe-btn" id="btn-col" onclick="setMode('col')">Column (dimension)</button> |
| <button class="pe-btn" id="btn-row" onclick="setMode('row')">Row (position)</button> |
| </div> |
|
|
| <div class="pe-row"> |
| <div class="pe-y-label" id="ylabel">Sequence position <em>(pos β)</em></div> |
| <div style="flex:1;position:relative;"> |
| <canvas id="pe-canvas" style="width:100%;display:block;cursor:crosshair;"></canvas> |
| <div class="tooltip" id="tt"></div> |
| </div> |
| </div> |
| <div style="margin-left:28px"> |
| <div class="pe-axis-label">Embedding dimension <em>(i β)</em></div> |
| <div style="display:flex;justify-content:space-between;font-size:10px;color:var(--color-text-secondary);margin-top:2px;margin-left:2px;margin-right:2px"> |
| <span>i=0 (fast)</span><span>i=128</span><span>i=256 (slow)</span> |
| </div> |
| </div> |
|
|
| <div class="pe-legend" style="margin-left:28px"> |
| <span>β1</span> |
| <canvas id="legend-bar" width="300" height="12" style="flex:1;border-radius:3px;"></canvas> |
| <span>+1</span> |
| </div> |
|
|
| <div class="annotation-box" id="ann-box"> |
| <div class="ann-row"> |
| <div class="ann-col"> |
| <div class="ann-head">β‘ Fast dimensions (small <em>i</em>, left)</div> |
| The sine wave completes many full cycles across just a few positions. Adjacent words look very different here β the model uses this to detect <strong>neighbors</strong>. |
| </div> |
| <div class="ann-col"> |
| <div class="ann-head">π’ Slow dimensions (large <em>i</em>, right)</div> |
| The wavelength is enormous (up to 10,000Β·2Ο). Values barely change between consecutive words, but differ noticeably over hundreds of positions β telling the model about <strong>long-range structure</strong>. |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| <script> |
| const ROWS = 50; |
| const COLS = 256; |
| const D_MODEL = 512; |
| |
| function pe(pos, i) { |
| const denom = Math.pow(10000, (2 * Math.floor(i / 2)) / D_MODEL); |
| return (i % 2 === 0) ? Math.sin(pos / denom) : Math.cos(pos / denom); |
| } |
| |
| const matrix = []; |
| for (let r = 0; r < ROWS; r++) { |
| matrix[r] = []; |
| for (let c = 0; c < COLS; c++) { |
| matrix[r][c] = pe(r, c); |
| } |
| } |
| |
| function valToColor(v, alpha) { |
| const t = (v + 1) / 2; |
| const isDark = matchMedia('(prefers-color-scheme: dark)').matches; |
| if (isDark) { |
| if (t > 0.5) { |
| const s = (t - 0.5) * 2; |
| const r = Math.round(30 + s * 200), g = Math.round(40 + s * 100), b = Math.round(80 + s * 80); |
| return `rgba(${r},${g},${b},${alpha})`; |
| } else { |
| const s = (0.5 - t) * 2; |
| const r = Math.round(30 + s * 180), g = Math.round(40 + s * 40), b = Math.round(80 + s * 120); |
| return `rgba(${r},${g},${b},${alpha})`; |
| } |
| } else { |
| if (t > 0.5) { |
| const s = (t - 0.5) * 2; |
| const r = Math.round(248 - s * 130), g = Math.round(248 - s * 30), b = Math.round(248 - s * 200); |
| return `rgba(${r},${g},${b},${alpha})`; |
| } else { |
| const s = (0.5 - t) * 2; |
| const r = Math.round(248 - s * 50), g = Math.round(248 - s * 140), b = Math.round(248 - s * 20); |
| return `rgba(${r},${g},${b},${alpha})`; |
| } |
| } |
| } |
| |
| const canvas = document.getElementById('pe-canvas'); |
| const ctx = canvas.getContext('2d'); |
| let cellW, cellH, dpr; |
| |
| function drawMatrix(highlightCol = -1, highlightRow = -1) { |
| const w = canvas.offsetWidth; |
| const h = Math.round(w * (ROWS / COLS) * 1.4); |
| dpr = window.devicePixelRatio || 1; |
| canvas.width = w * dpr; |
| canvas.height = h * dpr; |
| canvas.style.height = h + 'px'; |
| ctx.scale(dpr, dpr); |
| cellW = w / COLS; |
| cellH = h / ROWS; |
| |
| for (let r = 0; r < ROWS; r++) { |
| for (let c = 0; c < COLS; c++) { |
| const v = matrix[r][c]; |
| const isHL = (highlightCol >= 0 && c === highlightCol) || (highlightRow >= 0 && r === highlightRow); |
| ctx.fillStyle = valToColor(v, isHL ? 1 : (highlightCol >= 0 || highlightRow >= 0 ? 0.35 : 1)); |
| ctx.fillRect(c * cellW, r * cellH, cellW + 0.5, cellH + 0.5); |
| } |
| } |
| |
| if (highlightCol >= 0) { |
| ctx.strokeStyle = '#EF9F27'; |
| ctx.lineWidth = 2; |
| ctx.strokeRect(highlightCol * cellW + 1, 1, cellW - 1, ROWS * cellH - 2); |
| } |
| if (highlightRow >= 0) { |
| ctx.strokeStyle = '#1D9E75'; |
| ctx.lineWidth = 2; |
| ctx.strokeRect(1, highlightRow * cellH + 1, COLS * cellW - 2, cellH - 1); |
| } |
| } |
| |
| function drawLegend() { |
| const lb = document.getElementById('legend-bar'); |
| const lctx = lb.getContext('2d'); |
| const w = lb.offsetWidth || 300; |
| lb.width = w * (window.devicePixelRatio || 1); |
| lb.style.width = '100%'; |
| for (let x = 0; x < w; x++) { |
| const v = (x / w) * 2 - 1; |
| lctx.fillStyle = valToColor(v, 1); |
| lctx.fillRect(x * (window.devicePixelRatio || 1), 0, (window.devicePixelRatio || 1), 12); |
| } |
| } |
| |
| let mode = 'cell'; |
| function setMode(m) { |
| mode = m; |
| ['cell','col','row'].forEach(k => document.getElementById('btn-'+k).classList.toggle('active', k === m)); |
| drawMatrix(); |
| } |
| |
| canvas.addEventListener('mousemove', function(e) { |
| const rect = canvas.getBoundingClientRect(); |
| const x = e.clientX - rect.left; |
| const y = e.clientY - rect.top; |
| const col = Math.floor(x / (rect.width / COLS)); |
| const row = Math.floor(y / (rect.height / ROWS)); |
| if (col < 0 || col >= COLS || row < 0 || row >= ROWS) return; |
| |
| const tt = document.getElementById('tt'); |
| const v = matrix[row][col].toFixed(4); |
| |
| if (mode === 'cell') { |
| drawMatrix(); |
| tt.innerHTML = `<b>pos=${row}, i=${col}</b><br>value = ${v}<br>${col % 2 === 0 ? 'sin' : 'cos'} wave`; |
| } else if (mode === 'col') { |
| drawMatrix(-1, -1); |
| drawMatrix(col, -1); |
| const speed = col < 20 ? 'β‘ fast' : col < 100 ? 'β‘ moderate' : 'π’ slow'; |
| tt.innerHTML = `<b>Dimension i=${col}</b> (${speed})<br>wavelength β ${(2 * Math.PI * Math.pow(10000, col / D_MODEL)).toFixed(0)}`; |
| } else { |
| drawMatrix(-1, row); |
| tt.innerHTML = `<b>Position pos=${row}</b><br>Word at index ${row} in the sequence`; |
| } |
| |
| tt.style.left = Math.min(x + 10, rect.width - 180) + 'px'; |
| tt.style.top = Math.max(y - 36, 0) + 'px'; |
| tt.style.opacity = '1'; |
| }); |
| |
| canvas.addEventListener('mouseleave', function() { |
| document.getElementById('tt').style.opacity = '0'; |
| drawMatrix(); |
| }); |
| |
| canvas.addEventListener('click', function(e) { |
| const rect = canvas.getBoundingClientRect(); |
| const x = e.clientX - rect.left; |
| const y = e.clientY - rect.top; |
| const col = Math.floor(x / (rect.width / COLS)); |
| const row = Math.floor(y / (rect.height / ROWS)); |
| if (mode === 'col') { |
| const speed = col < 20 ? 'fast (high frequency)' : col < 100 ? 'moderate frequency' : 'slow (low frequency)'; |
| const ann = document.getElementById('ann-box'); |
| ann.innerHTML = `<div class="ann-row"><div class="ann-col"><div class="ann-head" style="color:#BA7517">Dimension i=${col} β ${speed}</div> |
| Looking down this column (vertical), the value at each row changes ${col < 30 ? '<strong>rapidly</strong> β adjacent words look very different. The model can distinguish neighbors.' : col > 150 ? '<strong>barely at all</strong> between neighbors β but significantly over hundreds of positions. Good for long-range context.' : 'at a moderate rate.'}<br><br>Wavelength β ${(2 * Math.PI * Math.pow(10000, col / D_MODEL)).toFixed(0)} radians.</div></div>`; |
| } else if (mode === 'row') { |
| const ann = document.getElementById('ann-box'); |
| ann.innerHTML = `<div class="ann-row"><div class="ann-col"><div class="ann-head" style="color:#0F6E56">Position pos=${row} β the lookup table row for word ${row}</div> |
| Reading across this row (horizontal), you see how the 512-dimensional encoding vector for <em>any word at position ${row}</em> looks: left side oscillates fast, right side is nearly flat. When the Transformer sees a word here, it adds this entire row to the word's embedding.</div></div>`; |
| } |
| }); |
| |
| setTimeout(() => { |
| drawMatrix(); |
| drawLegend(); |
| }, 50); |
| |
| window.addEventListener('resize', () => { drawMatrix(); drawLegend(); }); |
| </script> |
|
|