디퓨전 디노이징

Diffusion denoising

순수한 노이즈에서 출발해, 모델이 그 안에 숨은 그림을 추측해 조금씩 걷어내며 이미지를 만드는 과정.

다른 이름: Denoising diffusionForward and reverse diffusion
···
html
<div class="wrap">
  <canvas id="cv"></canvas>
  <div class="panel" id="panel"></div>
</div>
css
.wrap{position:relative;width:100%;height:100%}
#cv{position:absolute;inset:0;width:100%;height:100%;image-rendering:pixelated}
.panel{position:absolute;bottom:10px;left:10px;right:10px;z-index:2;padding:7px 10px;border-radius:10px;
  background:rgba(13,13,18,0.55);backdrop-filter:blur(6px);border:1px solid rgba(255,255,255,0.15);
  font-size:11px;font-weight:600;color:#f1f0ec}
js
const cv = document.getElementById('cv'), ctx = cv.getContext('2d');
const panel = document.getElementById('panel');
const SW = 48, SH = 30;
const small = document.createElement('canvas'); small.width = SW; small.height = SH;
const sctx = small.getContext('2d');
function fit() { const dpr = Math.min(devicePixelRatio || 1, 2); cv.width = innerWidth * dpr; cv.height = innerHeight * dpr; ctx.setTransform(dpr, 0, 0, dpr, 0, 0); ctx.imageSmoothingEnabled = false; }
addEventListener('resize', fit); fit();

function mulberry32(a) { return function () { a |= 0; a = a + 0x6D2B79F5 | 0; let t = Math.imul(a ^ a >>> 15, 1 | a); t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t; return ((t ^ t >>> 14) >>> 0) / 4294967296; }; }

function paintTarget() {
  const r = mulberry32(31);
  const g = sctx.createLinearGradient(0, 0, 0, SH);
  g.addColorStop(0, 'hsl(280 45% 60%)'); g.addColorStop(1, 'hsl(320 40% 30%)');
  sctx.fillStyle = g; sctx.fillRect(0, 0, SW, SH);
  sctx.beginPath(); sctx.arc(SW * 0.3, SH * 0.22, SH * 0.12, 0, Math.PI * 2); sctx.fillStyle = 'hsl(50 90% 75%)'; sctx.fill();
  for (let l = 0; l < 2; l++) {
    sctx.beginPath(); sctx.moveTo(0, SH);
    for (let i = 0; i <= 6; i++) { const x = SW * i / 6; const y = SH * (0.55 + l * 0.16) - r() * SH * 0.12; sctx.lineTo(x, y); }
    sctx.lineTo(SW, SH); sctx.closePath();
    sctx.fillStyle = 'hsl(300 30% ' + (16 + l * 12) + '%)'; sctx.fill();
  }
}
function noiseCanvas(seed) {
  const r = mulberry32(seed);
  const tmp = document.createElement('canvas'); tmp.width = SW; tmp.height = SH;
  const tctx = tmp.getContext('2d');
  const id = tctx.createImageData(SW, SH);
  for (let i = 0; i < id.data.length; i += 4) { const v = Math.floor(r() * 255); id.data[i] = v; id.data[i + 1] = v; id.data[i + 2] = v; id.data[i + 3] = 255; }
  tctx.putImageData(id, 0, 0);
  return tmp;
}
function drawFrame(t) {
  paintTarget();
  if (t > 0.01) { sctx.globalAlpha = t; sctx.drawImage(noiseCanvas(500 + Math.floor(t * 40)), 0, 0); sctx.globalAlpha = 1; }
  ctx.clearRect(0, 0, innerWidth, innerHeight);
  ctx.drawImage(small, 0, 0, innerWidth, innerHeight);
}
let t = 0.95, dir = -1, label = '역방향(생성): 노이즈에서 이미지를 복원합니다';
function loop() {
  drawFrame(t); panel.textContent = label;
  t += dir * 0.012;
  if (t <= 0) { t = 0; dir = 1; label = '정방향(학습): 이미지에 노이즈를 더합니다'; }
  if (t >= 1) { t = 1; dir = -1; label = '역방향(생성): 노이즈에서 이미지를 복원합니다'; }
  setTimeout(loop, 30);
}
loop();

확산 모델은 두 방향의 과정으로 학습됩니다. 정방향(forward) 과정은 실제 이미지에 노이즈를 아주 조금씩, 여러 단계에 걸쳐 더해서 결국 완전한 잡음으로 만드는 절차입니다 — 이건 규칙이 정해진 수학 연산이라 학습이 필요 없습니다. 모델이 학습하는 건 그 반대인 역방향(reverse) 과정, 즉 "이 노이즈 낀 이미지에서 방금 더해진 노이즈가 무엇이었을지" 추측하는 일입니다.

생성할 때는 역방향 과정만 반복해서 씁니다. 완전한 노이즈에서 시작해 모델이 "여기 섞인 노이즈"를 예측하고 그만큼을 덜어내기를 정해진 횟수만큼 반복하면, 통계적으로 그럴듯한 이미지의 구조가 노이즈 속에서 서서히 드러납니다. 한 번의 순전파로 끝나는 생성 방식(예: GAN)과 달리 여러 단계를 거치는 이유가 여기 있습니다 — 한 번에 노이즈를 다 걷어내라고 시키면 모델이 훨씬 어려워합니다.

이 반복 구조 때문에 스텝 수(샘플링 스텝)라는 조절 가능한 파라미터가 생기고, 스텝마다 노이즈를 얼마나 걷어낼지 계산하는 방식이 샘플러 알고리즘입니다.

아래 데모는 실제 모델의 노이즈 예측 대신, 목표 그림과 시드 기반 노이즈를 알파값으로 오갔다 갔다 섞어서 정방향(노이즈 추가)과 역방향(노이즈 제거)을 번갈아 보여주는 시뮬레이션입니다.

언제 쓰나

샘플링 스텝·시드 같은 파라미터가 왜 존재하는지 그 메커니즘을 이해하고 싶을 때 참고합니다. 실무에서 직접 조절하는 값이라기보다 그 값들이 기대는 원리입니다.