【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:生成器架构的进化

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} zZ8MLP wW

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)+BN(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

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐