잠재 공간

Latent space

이미지를 픽셀이 아니라 훨씬 작은 숫자 벡터로 압축해 표현하는 추상적인 공간. 이 공간에서 두 점 사이를 이동하면 이미지도 그 중간 형태로 부드럽게 바뀝니다.

다른 이름: Latent vector spaceEmbedding space
···
html
<div class="wrap">
  <canvas id="cv"></canvas>
  <div class="plane" id="plane">
    <span class="corner tl">산</span>
    <span class="corner tr">바다</span>
    <span class="corner bl">숲</span>
    <span class="corner br">사막</span>
    <span class="dot" id="dot"></span>
  </div>
</div>
css
.wrap{position:relative;width:100%;height:100%}
#cv{position:absolute;inset:0;width:100%;height:100%}
.plane{position:absolute;right:10px;bottom:10px;width:96px;height:96px;border:1px solid rgba(255,255,255,0.35);
  border-radius:8px;background:rgba(255,255,255,0.06);z-index:2}
.corner{position:absolute;font-size:8.5px;color:rgba(255,255,255,0.8);font-weight:700}
.tl{top:3px;left:4px}
.tr{top:3px;right:4px}
.bl{bottom:3px;left:4px}
.br{bottom:3px;right:4px}
.dot{position:absolute;width:8px;height:8px;border-radius:50%;background:#fff;box-shadow:0 0 8px 2px rgba(255,255,255,0.7);
  transform:translate(-50%,-50%);transition:left 1.6s ease,top 1.6s ease}
js
const cv = document.getElementById('cv'), ctx = cv.getContext('2d');
const dot = document.getElementById('dot');
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); render(currentHue()); }
addEventListener('resize', fit);

const corners = [{ u: 0, v: 0, hue: 20 }, { u: 1, v: 0, hue: 200 }, { u: 0, v: 1, hue: 140 }, { u: 1, v: 1, hue: 35 }];
const waypoints = [[0.08, 0.08], [0.92, 0.08], [0.92, 0.92], [0.08, 0.92]];
let wi = 0, u = 0.08, v = 0.08;
function hueAt(uu, vv) {
  const a = (1 - uu) * (1 - vv), b = uu * (1 - vv), c = (1 - uu) * vv, d = uu * vv;
  return corners[0].hue * a + corners[1].hue * b + corners[2].hue * c + corners[3].hue * d;
}
function currentHue() { return hueAt(u, v); }
function paintScene(w, h, hue) {
  const g = ctx.createLinearGradient(0, 0, 0, h);
  g.addColorStop(0, 'hsl(' + hue + ' 55% 60%)'); g.addColorStop(1, 'hsl(' + ((hue + 40) % 360) + ' 45% 28%)');
  ctx.fillStyle = g; ctx.fillRect(0, 0, w, h);
  ctx.beginPath(); ctx.moveTo(0, h);
  for (let i = 0; i <= 7; i++) { const x = w * i / 7; const y = h * 0.6 + Math.sin(i * 1.4 + hue * 0.05) * h * 0.08; ctx.lineTo(x, y); }
  ctx.lineTo(w, h); ctx.closePath();
  ctx.fillStyle = 'hsl(' + hue + ' 40% 20%)'; ctx.fill();
}
function render(hue) { paintScene(innerWidth, innerHeight, hue); }
function moveTo() {
  [u, v] = waypoints[wi];
  dot.style.left = (u * 100) + '%'; dot.style.top = (v * 100) + '%';
  render(hueAt(u, v));
  wi = (wi + 1) % waypoints.length;
  setTimeout(moveTo, 1900);
}
fit();
moveTo();

인코더가 이미지를 훨씬 작은 숫자 격자(잠재 벡터)로 압축하면, 디코더가 그걸 다시 픽셀로 복원할 수 있습니다. "잠재 확산(latent diffusion)" 모델은 노이즈-투-이미지 전체 과정을 픽셀이 아니라 이 압축된 공간 안에서 돌리고, 맨 마지막에 딱 한 번만 픽셀로 디코딩합니다 — 그래서 전체 해상도에서 직접 돌리는 것보다 계산이 훨씬 가볍습니다.

이 공간에서 가까운 점들은 대체로 비슷한 이미지로 복원되는 경향이 있습니다. 그래서 두 잠재 벡터 사이를 일직선으로 이동(보간)하면 뚝 끊기는 전환이 아니라 부드럽게 중간 형태를 거치는 이미지가 나옵니다 — 두 생성 이미지 사이를 모핑하는 애니메이션이 여기서 나옵니다.

다만 이 공간은 실제로는 수백~수천 차원이라 "2차원 지도" 같은 게 아닙니다. 가까움이 늘 의미상 비슷함으로 이어진다는 보장도 없어서, 보간이 항상 매끄럽게 그럴듯한 결과만 내는 건 아닙니다.

아래 데모는 실제 잠재 공간을 2축으로 강제로 납작하게 눌러서 시각화한 단순화입니다 — 네 모서리에 서로 다른 개념을 고정해두고, 그 사이를 오가는 점의 위치로 색상·형태를 가중 평균해서 보여주는 절차적 시뮬레이션입니다.

언제 쓰나

두 생성 결과 사이를 부드럽게 잇는 모핑, 혹은 "이 개념과 저 개념 사이 어딘가"를 탐색하고 싶을 때 참고합니다. 정밀한 제어가 필요하면 컨트롤넷 같은 명시적 조건화가 더 낫습니다.