CFG++ — CFG 코드에서 바뀌는 부분
CFG++는 CFG 공식을 새로 만드는 방식이 아니다.
기존 CFG 코드에서 guided 값을 어디까지 쓰는지 바꾸는 쪽에 가깝다.
guided noise를 만드는 부분은 그대로 둔다. 대신 다음 latent으로 넘어가는 업데이트에서 unconditional 값을 다시 사용한다.
그래서 코드를 보면 차이는 꽤 작다. DDIM에서는 마지막 업데이트 줄이 바뀌고, Euler 계열에서는 이동 방향을 계산하는 기준이 바뀐다.
기준이 되는 CFG 코드
일반 CFG에서는 conditional 예측과 unconditional 예측을 섞어서 guided noise를 만든다.
noise_uc, noise_c = self.predict_noise(zt, t, ...)
noise_pred = noise_uc + cfg_guidance * (noise_c - noise_uc)
여기까지는 CFG와 CFG++가 같다.
차이는 이 noise_pred를 어디까지 쓰느냐에서 생긴다.
DDIM에서 일반 CFG
DDIM 업데이트는 크게 두 단계로 볼 수 있다.
먼저 현재 latent zt에서 깨끗한 이미지에 가까운 값 z0t를 추정한다.
그 다음 z0t에 다시 노이즈를 섞어서 다음 timestep의 latent을 만든다.
noise_uc, noise_c = self.predict_noise(zt, t, ...)
noise_pred = noise_uc + cfg_guidance * (noise_c - noise_uc)
z0t = (zt - (1-at).sqrt() * noise_pred) / at.sqrt()
zt = at_next.sqrt() * z0t + (1-at_next).sqrt() * noise_pred
일반 CFG에서는 두 줄 모두 noise_pred를 쓴다.
z0t를 추정할 때도 guided noise를 쓰고, 다음 latent으로 이동할 때도 guided noise를 쓴다.
DDIM에서 CFG++
CFG++에서는 앞부분은 그대로 둔다.
noise_pred를 만드는 방식도 같고, z0t를 구하는 방식도 같다.
바뀌는 건 마지막 줄이다.
noise_uc, noise_c = self.predict_noise(zt, t, ...)
noise_pred = noise_uc + cfg_guidance * (noise_c - noise_uc)
z0t = (zt - (1-at).sqrt() * noise_pred) / at.sqrt()
zt = at_next.sqrt() * z0t + (1-at_next).sqrt() * noise_uc
마지막 줄에서 noise_pred 대신 noise_uc를 쓴다.
즉, CFG++의 DDIM 기준 차이는 이 한 줄이다.
# CFG
zt = at_next.sqrt() * z0t + (1-at_next).sqrt() * noise_pred
# CFG++
zt = at_next.sqrt() * z0t + (1-at_next).sqrt() * noise_uc
z0t는 왜 그대로 두나
CFG++에서도 프롬프트 방향은 필요하다.
그래서 목표값을 만드는 쪽에서는 guided noise를 그대로 쓴다.
그래서 z0t를 만들 때는 일반 CFG처럼 noise_pred를 쓴다.
z0t = (zt - (1-at).sqrt() * noise_pred) / at.sqrt()
이 줄은 이미 conditional 방향이 반영된 목표를 만든다.
다만 다음 latent으로 이동할 때까지 guided noise를 계속 쓰면, CFG가 강할수록 결과가 과하게 밀릴 수 있다.
CFG++는 이 이동 부분만 unconditional 예측으로 바꾼다.
zt = at_next.sqrt() * z0t + (1-at_next).sqrt() * noise_uc
정리하면 이렇다.
목표는 guided.
이동은 unconditional.
이 구조만 기억하면 DDIM이든 Euler든 코드 차이를 따라가기 쉽다.
Euler에서는 어디가 바뀌나
Euler 계열에서도 구조는 비슷하다.
먼저 guided denoised와 unconditional denoised를 둘 다 계산할 수 있어야 한다.
def kdiffusion_zt_to_denoised(self, x, sigma, uc, c, cfg_guidance, t, ...):
...
noise_pred = noise_uc + cfg_guidance * (noise_c - noise_uc)
denoised = self.calculate_denoised(x, noise_pred, sigma)
uncond_denoised = self.calculate_denoised(x, noise_uc, sigma)
return denoised, uncond_denoised
일반 Euler에서는 보통 guided 결과만 사용한다.
z0t, _ = self.kdiffusion_zt_to_denoised(...)
d = self.to_d(zt, sigma, z0t)
zt = zt + d * dt
CFG++에서는 guided 결과와 unconditional 결과를 둘 다 받는다.
z0t, z0t_uncond = self.kdiffusion_zt_to_denoised(...)
d = self.to_d(zt, sigma, z0t_uncond)
zt = z0t + d * sigmas[step + 1]
여기서도 원리는 같다.
z0t는 guided 값을 유지한다.
하지만 이동 방향 d는 unconditional denoised를 기준으로 계산한다.
코드로 보면 차이는 이렇게 정리된다
| 목표값 | 이동에 쓰는 값 | |
|---|---|---|
| 일반 CFG | guided | guided |
| CFG++ | guided | unconditional |
구현할 때는 이 부분만 확인하면 된다.
샘플러가 다음 latent을 만들 때, guided 값을 계속 쓰고 있는지 아니면 unconditional 값을 따로 쓰고 있는지 확인하면 된다.
실제로 구현할 때 주의할 점
CFG++를 넣으려면 unconditional 예측을 중간에 버리면 안 된다.
일반 CFG 코드에서는 noise_pred를 만든 뒤 noise_uc를 더 이상 쓰지 않는 경우가 많다.
noise_pred = noise_uc + cfg_guidance * (noise_c - noise_uc)
# 이후 noise_uc를 버림
CFG++에서는 마지막 업데이트에 noise_uc 또는 z0t_uncond가 필요하다.
따라서 함수 구조를 바꿀 때 guided 결과와 unconditional 결과를 둘 다 들고 있어야 한다.
# DDIM 쪽이면 noise_uc를 마지막 줄까지 유지
zt = at_next.sqrt() * z0t + (1-at_next).sqrt() * noise_uc
# Euler 쪽이면 uncond denoised를 따로 반환
return denoised, uncond_denoised
CFG 값이 낮아지는 이유
CFG++에서는 일반 CFG보다 낮은 guidance 값을 쓰는 경우가 많다.
일반 CFG는 보통 5~7 근처를 많이 쓴다.
CFG++는 1~3 정도에서도 충분히 강하게 반영되는 경우가 많다.
이건 CFG가 약해졌다는 뜻이 아니다.
목표를 잡는 부분에는 여전히 guided 값이 들어간다.
다만 이동 과정에서 guided 값을 계속 누적하지 않기 때문에, 낮은 값에서도 결과가 덜 망가지고 자연스럽게 나온다.
_CFG_DEFAULTS = {"sdxl": 1.0, "sd15": 7.0, "sd20": 7.0, "flux": 3.5}
_SAMPLER_DEFAULTS = {"sdxl": "dpm++_2m_cfg++", ...}
예를 들어 SDXL 기본 CFG가 1.0으로 잡혀 있다면, 샘플러가 CFG++ 계열인지 확인해야 한다.
일반 CFG 샘플러에서 1.0을 쓰는 것과 CFG++ 샘플러에서 1.0을 쓰는 것은 의미가 다르다.
정리
CFG++는 CFG를 완전히 다른 방식으로 바꾸는 기법이 아니다.
guided noise를 만드는 공식은 그대로 둔다.
noise_pred = noise_uc + cfg_guidance * (noise_c - noise_uc)
차이는 그 다음이다.
일반 CFG는 목표 추정과 이동에 모두 guided 값을 쓴다.
CFG++는 목표 추정에는 guided 값을 쓰고, 이동에는 unconditional 값을 쓴다.
DDIM에서는 마지막 업데이트 줄의 noise_pred가 noise_uc로 바뀐다.
Euler에서는 이동 방향 d를 guided denoised가 아니라 unconditional denoised 기준으로 계산한다.
코드 기준으로 정리하면 이렇게 보면 된다.
z0t는 guided 기준으로 만들고, 다음 이동에는 unconditional 기준을 쓴다.
예제 코드
전체 코드는 아래 GitHub 저장소에 같이 정리해두었다.