【GAN 系列·第八篇】StyleGAN:风格解耦与精细控制,从映射网络到 AdaIN
【GAN 系列·第八篇】StyleGAN:风格解耦与精细控制,从映射网络到 AdaIN
作者:技术博主 | 更新时间:2026-05-24 | 阅读时长:约 25 分钟
系列:GAN 从入门到精通(共 12 篇)
环境:Python 3.12,PyTorch 2.x
标签:StyleGAN风格解耦AdaIN映射网络渐进式训练人脸生成风格混合高分辨率

🔥 本篇目标:NVIDIA 2019 年发布的 StyleGAN 把人脸生成推到了新的高度——生成的人脸几乎以假乱真,更重要的是,它可以精确控制发型、肤色、年龄等属性,支持风格混合(把 A 的年龄和 B 的发色合到一起)。StyleGAN 的三大核心创新:映射网络(把噪声映射到解耦的风格空间)、AdaIN(自适应实例归一化,风格注入的机制)、渐进式训练(从 4×4 到 1024×1024 逐步生长)。本篇深入每个创新背后的设计逻辑,完整实现 StyleGAN 的核心组件。
系列进度
| 篇次 | 主题 | 状态 |
|---|---|---|
| 第一篇~第七篇 | GAN基础 / 技巧 / DCGAN / WGAN / CGAN / Pix2Pix / CycleGAN | ✅ |
| 第八篇(本篇) | StyleGAN:风格解耦与精细控制 | — |
| 第九篇 | GAN 的评估指标:FID、IS | 即将发布 |
| 第十篇 | GAN vs VAE vs 扩散模型 | 即将发布 |
| … | … | … |
目录
- 一、从 DCGAN 到 StyleGAN:生成器架构的进化
- 二、映射网络:把 z 变成解耦的风格向量 w
- 三、AdaIN:风格注入的核心机制
- 四、渐进式训练:从 4×4 到 1024×1024
- 五、随机噪声输入:细节的随机性
- 六、风格混合:细粒度的属性控制
- 七、StyleGAN v2 的改进
- 八、代码实战:StyleGAN 核心组件实现
一、从 DCGAN 到 StyleGAN:生成器架构的进化
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
import warnings
warnings.filterwarnings('ignore')
print("生成器架构的进化:从 DCGAN 到 StyleGAN")
print()
evolution = [
{
"name": "DCGAN(2015)",
"arch": "z → FC → 转置卷积 × N → 图像",
"ctrl": "z 直接控制所有属性,高度纠缠",
"quality": "64×64,FID≈40(CIFAR-10)",
"limit": "生成质量有限,无法控制具体属性",
},
{
"name": "ProGAN(2018)",
"arch": "渐进式增长:4×4 → 8×8 → ... → 1024×1024",
"ctrl": "仍然是标准 DCGAN 式的 z 注入",
"quality": "1024×1024 人脸,FID≈8",
"limit": "分辨率提高了,但属性控制仍然困难",
},
{
"name": "StyleGAN(2019)",
"arch": "映射网络 + AdaIN 风格注入 + 渐进式训练",
"ctrl": "w 空间解耦,每层注入不同尺度风格",
"quality": "1024×1024 人脸,FID≈4.4",
"limit": "伪影问题(水滴形伪影),训练不稳定",
},
{
"name": "StyleGAN v2(2020)",
"arch": "去掉 AdaIN,改用权重调制,无渐进式",
"ctrl": "更好的解耦,更少伪影",
"quality": "1024×1024 人脸,FID≈2.84",
"limit": "需要大量算力",
},
{
"name": "StyleGAN v3(2021)",
"arch": "等变卷积,解决'纹理粘贴'问题",
"ctrl": "视频生成时不再有闪烁",
"quality": "FID≈2.79,更好的视频生成",
"limit": "更复杂的实现",
},
]
print(f" {'模型':^18} {'核心架构':^36} {'FID':^12}")
print(" " + "─" * 70)
for m in evolution:
fid = m['quality'].split(',')[-1] if ',' in m['quality'] else m['quality']
arch = m['arch'][:34]
print(f" {m['name']:^18} {arch:^36} {fid:^12}")
print()
print(" StyleGAN 的核心洞察:")
print()
print(" 传统 GAN 生成器:z → 直接控制图像特征")
print(" 问题:z 空间高度纠缠(改变鼻子大小也会影响发型)")
print()
print(" StyleGAN 的解决:")
print(" z → 映射网络 → w(解耦的风格空间)")
print(" w 在每个分辨率层分别注入(不同层控制不同尺度的风格)")
print()
print(" 不同层控制的属性:")
style_levels = [
("粗粒度层(4×4, 8×8)", "姿态、整体发型、脸型"),
("中粒度层(16×16, 32×32)", "发色细节、眼睛形状、肤色"),
("细粒度层(64×64+)", "发丝细节、皮肤纹理、眼睛高光"),
]
for level, attr in style_levels:
print(f" ✅ {level:^30}: {attr}")
二、映射网络:把 z 变成解耦的风格向量 w
print("\n映射网络(Mapping Network):解耦的关键")
print()
print(" 问题:为什么直接用 z 不行?")
print()
print(" z 空间通常是 N(0,I)(球形高斯)")
print(" 真实的人脸属性分布不是球形的!")
print()
print(" 例子:")
print(" '秃头' 和 '长发' 在 z 空间中的分布:")
print(" 如果 z 的某个分量控制发量,这个分量需要")
print(" 覆盖从秃头(0)到长发(1)的整个范围")
print(" 但'秃头'本身就很罕见,在 z 空间中密度很低")
print(" → 大量 z 值映射到'普通发量',导致生成多样性不足")
print()
print(" ⭐ 纠缠(Entanglement)的根本原因:")
print(" 真实数据的流形结构 ≠ z 的先验分布结构")
print(" 球形高斯 z 被'强迫'覆盖复杂的流形 → 属性相互纠缠")
print()
print(" 映射网络的解决方案:")
print(" z ∈ Z(球形高斯)→ 8层 MLP → w ∈ W(学习的风格空间)")
print(" W 空间的分布是被学习的,更好地匹配数据的真实流形")
print(" → w 的不同维度更好地对应独立的视觉属性")
print()
import torch
import torch.nn as nn
class MappingNetwork(nn.Module):
"""
StyleGAN 的映射网络:z → w
8 层全连接(原论文),带像素归一化
"""
def __init__(self, z_dim: int = 512,
w_dim: int = 512,
n_layers: int = 8,
lr_mul: float = 0.01):
"""
z_dim: 输入噪声维度
w_dim: 输出风格向量维度
n_layers: MLP 层数
lr_mul: 学习率乘子(映射网络用更小的学习率)
"""
super().__init__()
# 像素归一化(对 z 做归一化)
self.pixel_norm = PixelNorm()
# 8 层 FC + LeakyReLU
layers = []
in_dim = z_dim
for i in range(n_layers):
out_dim = w_dim
layers.append(EqualLinear(in_dim, out_dim,
lr_mul=lr_mul,
activation=True))
in_dim = out_dim
self.net = nn.Sequential(*layers)
def forward(self, z: torch.Tensor) -> torch.Tensor:
"""z → w"""
z = self.pixel_norm(z) # 归一化到单位球面
return self.net(z)
class PixelNorm(nn.Module):
"""
像素归一化:对特征向量的 L2 范数归一化
防止训练中特征向量无限增大
"""
def forward(self, x: torch.Tensor) -> torch.Tensor:
return x / (x.pow(2).mean(dim=1, keepdim=True) + 1e-8).sqrt()
class EqualLinear(nn.Module):
"""
等方差线性层(Equalized Learning Rate)
权重的方差在前向传播时归一化,保证每层学习率等效
"""
def __init__(self, in_features: int, out_features: int,
lr_mul: float = 1.0, activation: bool = False):
super().__init__()
self.weight = nn.Parameter(
torch.randn(out_features, in_features) / lr_mul
)
self.bias = nn.Parameter(torch.zeros(out_features))
self.lr_mul = lr_mul
self.activation = activation
self.scale = (1 / in_features) ** 0.5 * lr_mul
def forward(self, x: torch.Tensor) -> torch.Tensor:
out = F.linear(x, self.weight * self.scale,
self.bias * self.lr_mul)
if self.activation:
out = F.leaky_relu(out, 0.2) * (2 ** 0.5) # sqrt(2) 保持方差
return out
# 验证映射网络
torch.manual_seed(42)
mapping_net = MappingNetwork(z_dim=512, w_dim=512, n_layers=8)
z_test = torch.randn(4, 512)
with torch.no_grad():
w_test = mapping_net(z_test)
print(f" 映射网络验证:")
print(f" z: {tuple(z_test.shape)} → w: {tuple(w_test.shape)}")
print(f" z 均值:{z_test.mean():.4f},标准差:{z_test.std():.4f}")
print(f" w 均值:{w_test.mean():.4f},标准差:{w_test.std():.4f}")
print()
# w 空间的解耦性验证(概念演示)
print(" W 空间解耦性的直觉验证:")
print()
print(" 实验:在 w 空间中沿不同方向移动,观察影响的独立性")
print()
print(" StyleGAN 论文的测量:")
print(" 感知路径长度(PPL,Perceptual Path Length):")
print(" 在潜空间中均匀移动时,感知上变化应该均匀")
print(" 解耦的空间:每步感知变化大致相同(PPL 低)")
print(" 纠缠的空间:某些位置变化剧烈,某些平滑(PPL 高)")
print()
print(f" {'空间':^12} {'PPL(越低越解耦)':^20} {'含义':^20}")
print(" " + "─" * 56)
print(f" {'Z 空间':^12} {'高(~500)':^20} {'纠缠,属性相互影响':^20}")
print(f" {'W 空间':^12} {'低(~100)':^20} {'解耦,属性相对独立':^20}")
print()
print(" Z 空间 PPL ≈ 5× W 空间 PPL:映射网络显著提升了解耦性!")
n_params = sum(p.numel() for p in mapping_net.parameters())
print(f"\n 映射网络参数量:{n_params:,}({n_params/1e6:.1f}M)")
三、AdaIN:风格注入的核心机制
print("\nAdaIN(自适应实例归一化):风格注入的核心")
print()
print(" AdaIN 的来源:风格迁移(Huang & Belongie, 2017)")
print()
print(" 核心思想:图像的风格 = 特征统计量(均值和标准差)")
print()
print(" 标准实例归一化(IN):")
print(" IN(x) = (x - μ(x)) / σ(x)")
print()
print(" 自适应实例归一化(AdaIN):")
print(" AdaIN(x, y) = σ(y) · (x - μ(x)) / σ(x) + μ(y)")
print()
print(" ← 用风格图像 y 的统计量(μ,σ)替换内容图像 x 的统计量")
print(" ← 内容结构来自 x,风格(颜色、纹理统计)来自 y")
print()
print(" 在 StyleGAN 中的应用:")
print(" x = 当前层的特征图(内容)")
print(" y = 从 w 生成的风格向量(用仿射变换得到 μ 和 σ)")
print()
print(" StyleGAN 的 AdaIN:")
print(" w → 仿射变换 A → (y_s, y_b)(风格缩放和偏置)")
print(" AdaIN(x, w) = y_s · (x - μ(x)) / σ(x) + y_b")
print()
class AdaIN(nn.Module):
"""
StyleGAN 的自适应实例归一化(AdaIN)
从风格向量 w 生成每个通道的缩放和偏移
"""
def __init__(self, channels: int, w_dim: int):
"""
channels: 特征图通道数
w_dim: 风格向量维度
"""
super().__init__()
# 仿射变换:w → (γ, β)
# 每个通道一个 γ(缩放)和 β(偏移)
self.affine = EqualLinear(w_dim, channels * 2)
# 初始化:γ=1,β=0(开始时接近恒等变换)
self.affine.bias.data[:channels] = 1.0 # γ 初始化为 1
self.affine.bias.data[channels:] = 0.0 # β 初始化为 0
def forward(self, x: torch.Tensor,
w: torch.Tensor) -> torch.Tensor:
"""
x: (B, C, H, W) 特征图
w: (B, w_dim) 风格向量
"""
B, C, H, W = x.shape
# 生成风格参数
style = self.affine(w) # (B, 2C)
gamma, beta = style.chunk(2, dim=1) # (B, C), (B, C)
gamma = gamma.view(B, C, 1, 1) # 广播到 (B, C, H, W)
beta = beta.view(B, C, 1, 1)
# 实例归一化(对每张图像的每个通道)
x_norm = F.instance_norm(x)
# 用风格参数重新缩放
return gamma * x_norm + beta
# 验证 AdaIN
torch.manual_seed(42)
adain = AdaIN(channels=64, w_dim=512)
x_feat = torch.randn(2, 64, 16, 16) # (B=2, C=64, H=16, W=16)
w_style = torch.randn(2, 512) # 风格向量
with torch.no_grad():
out_adain = adain(x_feat, w_style)
print(f" AdaIN 验证:")
print(f" 输入特征: {tuple(x_feat.shape)}")
print(f" 风格向量: {tuple(w_style.shape)}")
print(f" 输出特征: {tuple(out_adain.shape)}")
print()
# 验证风格注入:不同 w 产生不同的统计量
w1 = torch.randn(2, 512)
w2 = torch.randn(2, 512) * 2 + 1 # 完全不同的风格
with torch.no_grad():
out1 = adain(x_feat, w1)
out2 = adain(x_feat, w2)
print(f" 不同风格向量 → 不同输出统计量:")
print(f" 风格1:均值={out1.mean():.4f},标准差={out1.std():.4f}")
print(f" 风格2:均值={out2.mean():.4f},标准差={out2.std():.4f}")
print(f" 差异:均值差={abs(out1.mean()-out2.mean()):.4f} ✓(风格不同→输出不同)")
print()
# AdaIN 与风格迁移的联系
print(" AdaIN 的风格迁移直觉:")
print()
print(" 内容图像的特征统计量 → 归一化清零 → 重新用风格的统计量赋值")
print(" 就像把内容图像的颜色/纹理统计'清空',再'填入'目标风格")
print()
print(" 参数量分析(每个 AdaIN 层):")
print(f" affine 层: {sum(p.numel() for p in adain.parameters()):,} 参数")
print(f" = 2 × channels × w_dim = 2 × 64 × 512 = {2*64*512:,}")
四、渐进式训练:从 4×4 到 1024×1024
print("\n渐进式训练(Progressive Growing)")
print()
print(" 直接训练 1024×1024 的问题:")
print(" ① 训练初期生成器产生纯噪声,判别器很容易区分")
print(" ② 梯度难以从 1024×1024 传递到底层")
print(" ③ 训练极不稳定,容易完全崩溃")
print()
print(" 渐进式训练的思路:")
print(" 从最低分辨率(4×4)开始,逐步增加分辨率")
print(" 每个阶段稳定后,再增加新的层")
print()
print(" 训练阶段:")
stages = [
(4, "⬜⬜⬜⬜", "学习整体结构(是否是人脸)"),
(8, "⬜⬜⬜⬜⬜⬜⬜⬜", "学习粗糙的特征(脸的朝向)"),
(16, "...", "学习中等细节(五官位置)"),
(32, "...", "学习细节(眼睛形状)"),
(64, "...", "学习精细细节(发丝方向)"),
(128, "...", "学习高频细节(皮肤纹理)"),
(256, "...", "学习超精细细节"),
(512, "...", "极高细节"),
(1024, "...", "每个毛孔都清晰可见"),
]
print(f" {'分辨率':^10} {'约需时间':^12} {'学习内容':^30}")
print(" " + "─" * 56)
for res, time_approx, content in stages:
time_str = f"~{res//4} 小时"
print(f" {res:^10} {time_str:^12} {content:^30}")
print()
print(" 平滑过渡(Fade-in):")
print()
print(" 直接切换分辨率 → 不稳定(旧层的知识被突然丢弃)")
print(" 平滑过渡:新层线性淡入")
print()
print(" 过渡期间的输出:")
print(" output = (1-α) × upscale(旧层输出) + α × 新层输出")
print(" α: 从 0 线性增加到 1")
print()
print(" ← 刚加入新层时,α=0,完全用旧层")
print(" ← 过渡完成后,α=1,完全用新层")
print(" ← 平滑过渡,防止新层破坏已学习的知识")
print()
# 渐进式训练的时间分配
import numpy as np
resolutions = [4, 8, 16, 32, 64, 128, 256, 512, 1024]
# 假设每阶段训练图像数(论文设定)
images_k = [600, 600, 600, 600, 600, 600, 600, 600, 600] # 600K each
print(" 渐进式训练的实际时间分配(FFHQ 1024×1024,V100):")
print(f" {'分辨率':^10} {'训练图像数':^14} {'累计总图像数':^16}")
print(" " + "─" * 44)
total = 0
for res, imgs in zip(resolutions, images_k):
total += imgs
print(f" {res:^10} {imgs*1000:^14,} {total*1000:^16,}")
print(f" 总计:约 {total//1000:.1f}M 张图像")
五、随机噪声输入:细节的随机性
print("\n随机噪声输入(Noise Inputs):让细节有随机变化")
print()
print(" 问题:网络生成的图像中,细节(发丝位置、雀斑)是确定的")
print(" 给定相同 w,永远生成完全相同的细节")
print(" 但真实人脸的细节是随机的,不应由语义决定")
print()
print(" StyleGAN 的解决:在每个卷积层后加入随机噪声")
print()
print(" noise_input = B·noise (B 是可学习的缩放系数)")
print(" output = conv(x) + B·noise")
print()
print(" 其中 noise ~ N(0,I),每次前向传播独立采样")
print()
print(" 效果:")
print(" ① 语义属性(脸型、肤色)由 w 控制(确定性)")
print(" ② 细节(发丝位置、皮肤纹理)由噪声控制(随机性)")
print(" ③ 固定 w,改变噪声 → 同一个人的不同随机细节")
print()
import torch
import torch.nn as nn
class NoiseInjection(nn.Module):
"""
StyleGAN 的随机噪声注入
learned_noise_scale × N(0, I) 加到特征图上
"""
def __init__(self, channels: int):
super().__init__()
# 每个通道一个可学习的缩放系数(初始化为 0)
self.weight = nn.Parameter(torch.zeros(1, channels, 1, 1))
def forward(self, x: torch.Tensor,
noise: torch.Tensor = None) -> torch.Tensor:
"""
x: (B, C, H, W) 特征图
noise: (B, 1, H, W) 随机噪声(None 时自动生成)
"""
if noise is None:
B, C, H, W = x.shape
noise = torch.randn(B, 1, H, W, device=x.device)
return x + self.weight * noise
# 验证噪声注入
torch.manual_seed(42)
noise_inj = NoiseInjection(channels=64)
x_feat = torch.randn(2, 64, 8, 8)
with torch.no_grad():
out1 = noise_inj(x_feat) # 随机噪声1
out2 = noise_inj(x_feat) # 随机噪声2(不同!)
out_fixed = noise_inj(x_feat, # 固定噪声
torch.zeros(2, 1, 8, 8))
diff_random = (out1 - out2).abs().mean().item()
diff_fixed = (out_fixed - x_feat).abs().mean().item()
print(f" 噪声注入验证:")
print(f" 同一输入两次随机噪声的差异:{diff_random:.6f}")
print(f" (固定 w,改变噪声 → 不同细节变化)")
print(f" 零噪声时与输入的差异:{diff_fixed:.8f}(≈0,weight 初始化为 0)✓")
print()
print(" 噪声对不同层的影响:")
noise_effects = [
("粗粒度层(4×4, 8×8)", "发旋的精确位置"),
("中粒度层(16×16, 32×32)", "发型的细节、小范围的皱纹"),
("细粒度层(64×64+)", "具体的发丝、毛孔、眼睛高光"),
]
for layer, effect in noise_effects:
print(f" 📌 {layer:^30}: {effect}")
六、风格混合:细粒度的属性控制
print("\n风格混合(Style Mixing):精细的属性控制")
print()
print(" StyleGAN 独有的能力:在不同层注入不同的 w")
print()
print(" 标准生成:w 在所有层都一样")
print(" 风格混合:前几层用 w₁(粗粒度风格),后几层用 w₂(细粒度风格)")
print()
print(" 可以实现:")
print(" 人脸 A 的整体结构 + 人脸 B 的发型细节")
print(" 人脸 A 的年龄/性别 + 人脸 B 的肤色/光照")
print()
# 风格混合的实现
def style_mixing_example():
"""
风格混合的数值演示(简化版)
假设生成器有 9 层(对应 4×4 到 1024×1024)
"""
n_layers = 9
# 两个不同的风格向量
w1 = torch.randn(1, 512) # 风格 A
w2 = torch.randn(1, 512) # 风格 B
# 不同的混合策略
strategies = [
("全用 w1", [0]*n_layers),
("全用 w2", [1]*n_layers),
("前4层w1+后5层w2", [0,0,0,0,1,1,1,1,1]),
("前4层w2+后5层w1", [1,1,1,1,0,0,0,0,0]),
("交替混合", [0,1,0,1,0,1,0,1,0]),
]
print(" 风格混合策略(0=w1风格A, 1=w2风格B):")
print()
print(f" {'策略':^22} {'层0-8的风格来源':^30} {'效果':^20}")
print(" " + "─" * 76)
effects = [
"纯风格A的人脸",
"纯风格B的人脸",
"A的脸型/B的发型细节",
"B的脸型/A的发型细节",
"奇怪的混合(不推荐)",
]
for i, (name, pattern, effect) in enumerate(
zip([s[0] for s in strategies],
[s[1] for s in strategies],
effects)):
pattern_str = "".join(str(p) for p in pattern)
print(f" {name:^22} {pattern_str:^30} {effect:^20}")
print()
print(" 使用示例(PyTorch 风格):")
mixing_code = '''
# 生成两个随机风格向量
z1 = torch.randn(1, z_dim)
z2 = torch.randn(1, z_dim)
w1 = mapping_network(z1) # (1, w_dim)
w2 = mapping_network(z2) # (1, w_dim)
# 构建混合后的 w 序列(每层一个 w)
n_layers = 9 # 9 个合成层
mix_layer = 4 # 前 4 层用 w1,后 5 层用 w2
# 广播到所有层
ws1 = w1.unsqueeze(1).repeat(1, n_layers, 1) # (1, 9, w_dim)
ws2 = w2.unsqueeze(1).repeat(1, n_layers, 1) # (1, 9, w_dim)
# 混合:前 mix_layer 层用 w1,其余用 w2
ws_mixed = ws1.clone()
ws_mixed[:, mix_layer:] = ws2[:, mix_layer:]
# 使用混合后的 w 序列生成图像
image = generator(ws_mixed)
'''
print(mixing_code)
style_mixing_example()
# 风格混合的训练技巧
print(" 风格混合作为正则化(Mixing Regularization):")
print()
print(" 训练期间随机混合风格(50% 概率),防止生成器过度依赖")
print(" 相邻层的 w 的相关性(否则相邻层倾向于生成相似风格)")
print()
print(" 效果:强制生成器每层都能独立地从 w 注入风格信息")
七、StyleGAN v2 的改进
print("\nStyleGAN v2(2020)的关键改进")
print()
print(" StyleGAN v1 的主要问题:")
print(" 水滴形伪影(Blob Artifacts)")
print()
print(" 原因分析:")
print(" AdaIN 对每个像素的均值和标准差归一化,但这会")
print(" 引入人工的统计量操作 → 某些特定激活模式形成水滴")
print(" 这些水滴在图像中的固定位置产生(通常在脸部中心)")
print()
changes_v2 = [
{
"what": "去掉 AdaIN 归一化",
"replace": "改用权重调制(Weight Modulation)",
"why": "AdaIN 的归一化操作是伪影的根源",
"effect": "消除了水滴伪影,生成质量显著提升",
},
{
"what": "去掉渐进式训练",
"replace": "直接全分辨率训练(skip connections 辅助)",
"why": "渐进式引入额外超参数,且分辨率切换不稳定",
"effect": "训练流程更简单,结果更稳定",
},
{
"what": "路径长度正则化(PPL Regularization)",
"replace": "新的正则化损失",
"why": "保证 W 空间的路径平滑(解耦指标)",
"effect": "W 空间更解耦,插值更平滑",
},
{
"what": "懒惰正则化(Lazy Regularization)",
"replace": "每 16 步才计算一次正则化损失",
"why": "正则化计算开销大,不必每步都算",
"effect": "训练速度提升 30%,效果几乎不变",
},
]
for ch in changes_v2:
print(f" ✅ [{ch['what']}]")
print(f" 改为:{ch['replace']}")
print(f" 原因:{ch['why']}")
print(f" 效果:{ch['effect']}")
print()
# 权重调制(Weight Modulation)的实现
print(" 权重调制 vs AdaIN:")
print()
print(" AdaIN:先卷积,再归一化特征图")
print(" y_i = y_s,i · (x_i - μ(x_i)) / σ(x_i) + y_b,i")
print()
print(" 权重调制:把风格直接融入卷积权重")
print(" w'ijk = y_s,i · w_ijk")
print(" (先对权重缩放,再做卷积,不对特征做归一化)")
print()
class ModulatedConv2d(nn.Module):
"""
StyleGAN v2 的权重调制卷积
风格向量 w → 缩放卷积权重,而非归一化特征图
"""
def __init__(self, in_channels: int, out_channels: int,
kernel_size: int, w_dim: int,
demodulate: bool = True):
"""
demodulate: 是否做反调制(保持权重方差)
"""
super().__init__()
self.out_channels = out_channels
self.kernel_size = kernel_size
self.demodulate = demodulate
# 可学习的卷积权重
self.weight = nn.Parameter(
torch.randn(out_channels, in_channels,
kernel_size, kernel_size)
)
# 风格向量 → 缩放系数(仿射变换)
self.style_mod = EqualLinear(w_dim, in_channels)
self.style_mod.bias.data.fill_(1.0) # 初始化为 1(恒等)
self.padding = kernel_size // 2
def forward(self, x: torch.Tensor,
w: torch.Tensor) -> torch.Tensor:
"""
x: (B, in_ch, H, W)
w: (B, w_dim)
"""
B, C, H, W = x.shape
# 从风格向量生成缩放系数 s
s = self.style_mod(w) # (B, in_ch)
s = s.view(B, 1, C, 1, 1) # 广播用
# 调制权重:w' = s · w
weight = self.weight.unsqueeze(0) # (1, out, in, k, k)
weight = weight * s # (B, out, in, k, k)
# 反调制(demodulation):保持权重方差 = 1
if self.demodulate:
d = (weight.pow(2).sum([2, 3, 4]) + 1e-8).sqrt()
weight = weight / d.view(B, self.out_channels, 1, 1, 1)
# 逐样本卷积(batch 中每张图像用不同的权重)
weight = weight.view(B * self.out_channels, C,
self.kernel_size, self.kernel_size)
x = x.view(1, B * C, H, W)
out = F.conv2d(x, weight, padding=self.padding, groups=B)
out = out.view(B, self.out_channels, H, W)
return out
# 验证调制卷积
torch.manual_seed(42)
mod_conv = ModulatedConv2d(in_channels=64, out_channels=64,
kernel_size=3, w_dim=512)
x_feat = torch.randn(2, 64, 8, 8)
w_style = torch.randn(2, 512)
with torch.no_grad():
out_mod = mod_conv(x_feat, w_style)
print(f" 调制卷积验证:")
print(f" x: {tuple(x_feat.shape)}, w: {tuple(w_style.shape)}")
print(f" 输出: {tuple(out_mod.shape)}")
print()
# StyleGAN v1 vs v2 的 FID 对比
print(" StyleGAN v1 vs v2 的质量对比(FFHQ 1024×1024):")
comparison = [
("FID(越低越好)", "4.40", "2.84"),
("PPL(路径长度)", "212", "145"),
("水滴伪影", "有", "无"),
("训练稳定性", "中", "高"),
("代码复杂度", "中", "较高"),
("训练时间(V100)", "~70天", "~70天"),
]
print(f" {'指标':^20} {'v1':^14} {'v2':^14}")
print(" " + "─" * 50)
for metric, v1, v2 in comparison:
print(f" {metric:^20} {v1:^14} {v2:^14}")
八、代码实战:StyleGAN 核心组件实现
import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as np
print("\n代码实战:StyleGAN 核心生成器块")
print()
class StyleBlock(nn.Module):
"""
StyleGAN 的核心生成器块
包含:卷积 + 噪声注入 + AdaIN + 激活
"""
def __init__(self, in_channels: int, out_channels: int,
w_dim: int = 512, upsample: bool = False):
"""
in_channels: 输入通道
out_channels: 输出通道
w_dim: 风格向量维度
upsample: 是否上采样(每两层上采样一次)
"""
super().__init__()
self.upsample = upsample
# 卷积(等方差)
self.conv = EqualConv2d(in_channels, out_channels,
kernel_size=3, padding=1)
# 噪声注入
self.noise_injection = NoiseInjection(out_channels)
# AdaIN(风格注入)
self.adain = AdaIN(out_channels, w_dim)
# 激活
self.activate = nn.LeakyReLU(0.2)
def forward(self, x: torch.Tensor,
w: torch.Tensor,
noise: torch.Tensor = None) -> torch.Tensor:
"""
x: (B, in_ch, H, W)
w: (B, w_dim)
"""
if self.upsample:
# 双线性上采样(比转置卷积更平滑)
x = F.interpolate(x, scale_factor=2,
mode='bilinear', align_corners=False)
x = self.conv(x)
x = self.noise_injection(x, noise)
x = self.activate(x)
x = self.adain(x, w) # 风格注入!
return x
class EqualConv2d(nn.Module):
"""等方差卷积(用于 StyleGAN 的权重初始化)"""
def __init__(self, in_channels, out_channels,
kernel_size, padding=0):
super().__init__()
self.weight = nn.Parameter(
torch.randn(out_channels, in_channels,
kernel_size, kernel_size)
)
self.bias = nn.Parameter(torch.zeros(out_channels))
self.scale = (1 / (in_channels * kernel_size ** 2)) ** 0.5
self.padding = padding
def forward(self, x):
return F.conv2d(x, self.weight * self.scale,
self.bias, padding=self.padding)
class StyleGANGenerator(nn.Module):
"""
简化版 StyleGAN 生成器
从 4×4 开始,逐步上采样到目标分辨率
"""
def __init__(self, w_dim: int = 512,
ngf: int = 256,
img_size: int = 64,
img_channels: int = 3):
"""
w_dim: 风格向量维度
ngf: 基础特征图数
img_size: 目标输出分辨率
img_channels: 输出通道数(RGB=3)
"""
super().__init__()
self.img_size = img_size
# 计算需要多少层
self.n_layers = int(np.log2(img_size)) - 1 # 4×4 开始
n_up = self.n_layers - 1 # 上采样次数
# 初始常量输入(4×4,可学习)
self.const = nn.Parameter(torch.randn(1, ngf, 4, 4))
# 各分辨率的特征图通道数(从 ngf 逐渐减少)
channels = {
4: ngf,
8: ngf,
16: ngf // 2,
32: ngf // 4,
64: ngf // 8,
128: ngf // 16,
}
# 生成器块
self.blocks = nn.ModuleList()
self.to_rgb = nn.ModuleList() # RGB 转换层
res = 4
in_ch = channels[4]
for i in range(self.n_layers):
upsample = (i > 0)
out_ch = channels.get(res * (2 if upsample else 1),
max(ngf // 32, 16))
self.blocks.append(StyleBlock(
in_channels = in_ch,
out_channels = out_ch,
w_dim = w_dim,
upsample = upsample,
))
self.to_rgb.append(EqualConv2d(out_ch, img_channels, 1))
if upsample:
res *= 2
in_ch = out_ch
def forward(self, w: torch.Tensor,
noise_list: list = None) -> torch.Tensor:
"""
w: (B, w_dim) 或 (B, n_layers, w_dim)(风格混合时)
noise_list: 每层的噪声(None 时自动生成)
"""
B = w.shape[0]
# 如果 w 是 2D(单一风格),扩展到所有层
if w.dim() == 2:
w = w.unsqueeze(1).repeat(1, self.n_layers, 1)
# 从可学习的常量开始(广播到 batch)
x = self.const.repeat(B, 1, 1, 1)
# 逐层前向传播
rgb_out = None
for i, (block, to_rgb) in enumerate(
zip(self.blocks, self.to_rgb)):
noise = None if noise_list is None else noise_list[i]
x = block(x, w[:, i], noise)
# 跳跃连接到 RGB 输出(跳跃连接防止梯度消失)
new_rgb = to_rgb(x)
if rgb_out is None:
rgb_out = new_rgb
else:
# 上采样旧 RGB + 新 RGB
rgb_out = F.interpolate(rgb_out, scale_factor=2,
mode='bilinear',
align_corners=False)
rgb_out = rgb_out + new_rgb
return torch.tanh(rgb_out)
# 测试完整流程
torch.manual_seed(42)
n_params_map = sum(p.numel() for p in mapping_net.parameters())
G_style = StyleGANGenerator(w_dim=512, ngf=64, img_size=64)
n_params_G = sum(p.numel() for p in G_style.parameters())
print(f" StyleGAN 参数量:")
print(f" 映射网络: {n_params_map:,}({n_params_map/1e6:.1f}M)")
print(f" 生成器: {n_params_G:,}({n_params_G/1e6:.1f}M)")
print(f" 总计: {(n_params_map+n_params_G):,}({(n_params_map+n_params_G)/1e6:.1f}M)")
print()
# 端到端测试
z = torch.randn(2, 512)
with torch.no_grad():
w = mapping_net(z)
img = G_style(w)
print(f" 端到端测试:")
print(f" z{tuple(z.shape)} → w{tuple(w.shape)} → img{tuple(img.shape)}")
print(f" 输出范围:[{img.min():.3f}, {img.max():.3f}](Tanh ✓)")
print()
# 风格混合测试
z1, z2 = torch.randn(2, 512), torch.randn(2, 512)
with torch.no_grad():
w1 = mapping_net(z1)
w2 = mapping_net(z2)
# 构建混合 w(前半层用 w1,后半层用 w2)
n_layers = G_style.n_layers
ws_mixed = w1.unsqueeze(1).repeat(1, n_layers, 1)
ws_mixed[:, n_layers//2:] = w2.unsqueeze(1).repeat(1, n_layers, 1)[:, n_layers//2:]
img_mixed = G_style(ws_mixed)
print(f" 风格混合测试:")
print(f" ws_mixed: {tuple(ws_mixed.shape)}(每层独立风格向量)")
print(f" 混合图像: {tuple(img_mixed.shape)} ✓")
print()
print(" 完整 StyleGAN 训练步骤(真实项目):")
training_steps = [
"1. 准备高质量人脸数据集(FFHQ: 70000张1024×1024)",
"2. 映射网络 z → w(8层MLP,lr=0.01×基础lr)",
"3. 渐进式训练:4×4 → 8×8 → ... → 1024×1024",
"4. 每个分辨率:稳定阶段 + 渐入阶段(各600K图像)",
"5. 损失:R1 正则化的非饱和 GAN 损失",
"6. 混合精度训练(FP16)节省显存",
"7. EMA 指数平均生成器参数",
"8. 评估:FID + PPL(感知路径长度)",
]
for step in training_steps:
print(f" {step}")
总结
StyleGAN 的四大核心创新:
① 映射网络:z → w,解耦风格空间
z ∈ Z → 8层MLP w ∈ W z \in \mathcal{Z} \xrightarrow{\text{8层MLP}} w \in \mathcal{W} z∈Z8层MLPw∈W
W \mathcal{W} W 空间的不同维度对应独立的视觉属性,感知路径长度(PPL)从 Z 空间降低 5×。
② AdaIN:风格注入的优雅机制
AdaIN ( x , w ) = γ ( w ) ⏟ 缩放 ⋅ x − μ ( x ) σ ( x ) + β ( w ) ⏟ 偏移 \text{AdaIN}(x, w) = \underbrace{\gamma(w)}_{\text{缩放}} \cdot \frac{x - \mu(x)}{\sigma(x)} + \underbrace{\beta(w)}_{\text{偏移}} AdaIN(x,w)=缩放 γ(w)⋅σ(x)x−μ(x)+偏移 β(w)
γ \gamma γ 和 β \beta β 从 w w w 线性变换得到,每层独立注入,不同层控制不同粒度的风格。
③ 随机噪声:确定性属性 + 随机细节
output = conv ( x ) + B ⋅ N ( 0 , I ) \text{output} = \text{conv}(x) + B \cdot \mathcal{N}(0,I) output=conv(x)+B⋅N(0,I)
w w w 控制语义(确定),噪声控制细节(随机),两者解耦。
④ 渐进式训练(v1)/ 权重调制(v2)
| StyleGAN v1 | StyleGAN v2 | |
|---|---|---|
| 风格注入 | AdaIN(特征归一化) | 权重调制(卷积权重缩放) |
| 训练方式 | 渐进式增长 | 直接全分辨率 |
| 伪影 | 有水滴伪影 | 无 |
| FID(FFHQ) | 4.40 | 2.84 |
下一篇预告:GAN 的评估指标——生成质量怎么量化?FID(Fréchet Inception Distance)和 IS(Inception Score)分别测量什么?为什么 FID 比 IS 更可靠?精确率(Precision)和召回率(Recall)如何同时评估质量和多样性?
💬 StyleGAN 的风格混合功能让你最想做什么实验?把两个人的特征混合,还是控制特定属性? 欢迎评论区分享!
🙏 如果这篇帮到你,点赞 + 收藏,系列持续更新!
本文为原创技术分享。代码在 Python 3.12 + PyTorch 2.x 下验证。最后更新:2026-05-24
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)