【代码学习】扩散模型原理+代码

来源:超详细的扩散模型(Diffusion Models)原理+代码 - 知乎 (zhihu.com) 

代码:drizzlezyk/DDPM-MindSpore (github.com)

DDPM

1. Unet

1.1 正弦位置编码

class SinusoidalPosEmb(nn.Cell):
    def __init__(self, dim):
        super().__init__()
        half_dim = dim // 2  # 将给定的维度除以2得到半维度
        emb = math.log(10000) / (half_dim - 1)  # 计算位置编码的参数
        emb = np.exp(np.arange(half_dim) * -emb)  # 根据半维度创建正弦位置编码矩阵
        self.emb = Tensor(emb, mindspore.float32)  # 将矩阵转换为Tensor,并存储在类属性中
        self.Concat = _get_cache_prim(ops.Concat)(-1)  # 定义连接操作

    def construct(self, x):
        emb = x[:, None] * self.emb[None, :]  # 对输入张量进行位置编码计算
        emb = self.Concat((ops.sin(emb), ops.cos(emb)))  # 将正弦和余弦编码连接起来
        return emb

1.2 Attention

class Attention(nn.Cell):
    def __init__(self, dim, heads=4, dim_head=32):
        super().__init__()
        self.scale = dim_head ** -0.5  # 缩放因子,用于调整注意力权重
        self.heads = heads  # 头数,决定了注意力机制的复杂度
        hidden_dim = dim_head * heads  # 隐藏维度,等于头数乘以每个头的维度

        # 将输入进行线性变换得到查询、键和值
        self.to_qkv = _get_cache_prim(Conv2d)(dim, hidden_dim * 3, 1, pad_mode='valid', has_bias=False)
        # 将注意力加权后的结果再进行线性变换得到最终的输出
        self.to_out = _get_cache_prim(Conv2d)(hidden_dim, dim, 1, pad_mode='valid', has_bias=True)

        self.map = ops.Map()
        self.partial = ops.Partial()
        self.bmm = BMM()  # 矩阵相乘操作
        self.split = ops.Split(axis=1, output_num=3)  # 在指定维度上将张量分割成多个部分
        self.softmax = ops.Softmax(-1)  # 对最后一维进行softmax操作

    def construct(self, x):
        b, c, h, w = x.shape  # 获取输入张量的形状信息
        qkv = self.split(self.to_qkv(x))  # 将输入进行线性变换得到查询、键和值,并将其分割成三个部分
        q, k, v = self.map(self.partial(rearrange, self.heads), qkv)  # 对查询、键和值进行重排操作,以便多头注意力机制的计算
        q = q * self.scale  # 缩放查询向量

        sim = self.bmm(q.swapaxes(2, 3), k)  # 计算查询和键的相似度,使用矩阵乘法实现并行计算
        attn = self.softmax(sim)  # 使用softmax函数对相似度进行归一化,得到注意力权重
        out = self.bmm(attn, v.swapaxes(2, 3))  # 将注意力权重与值相乘,得到加权后的结果
        out = out.swapaxes(-1, -2).reshape((b, -1, h, w))  # 将结果进行维度转换和形状调整,得到最终的输出
        return self.to_out(out)  # 将输出进行线性变换得到最终的注意力机制输出

 1.3 Residual Block

class Residual(nn.Cell):
    """残差块"""
    def __init__(self, fn):
        super().__init__()
        self.fn = fn  # 残差块的函数或模型

    def construct(self, x, *args, **kwargs):
        return self.fn(x, *args, **kwargs) + x  # 将输入与通过函数或模型处理后的结果相加作为输出

2. GaussianDiffusion

定义相关的概率值,与公式相对应:

self.betas = betas  # 初始化betas参数
self.alphas_cumprod = alphas_cumprod  # 初始化alphas_cumprod参数
self.alphas_cumprod_prev = alphas_cumprod_prev  # 初始化alphas_cumprod_prev参数

# 计算扩散 q(x_t | x_{t-1}) 和其他参数
self.sqrt_alphas_cumprod = Tensor(np.sqrt(alphas_cumprod))  # 计算alphas_cumprod的平方根
self.sqrt_one_minus_alphas_cumprod = Tensor(np.sqrt(1. - alphas_cumprod))  # 计算1 - alphas_cumprod的平方根
self.log_one_minus_alphas_cumprod = Tensor(np.log(1. - alphas_cumprod))  # 计算log(1 - alphas_cumprod)
self.sqrt_recip_alphas_cumprod = Tensor(np.sqrt(1. / alphas_cumprod))  # 计算1 / alphas_cumprod的平方根
self.sqrt_recipm1_alphas_cumprod = Tensor(np.sqrt(1. / alphas_cumprod - 1))  # 计算1 / alphas_cumprod - 1的平方根

posterior_variance = betas * (1. - alphas_cumprod_prev) / (1. - alphas_cumprod)  # 计算后验方差

self.posterior_variance = Tensor(posterior_variance)  # 存储后验方差
self.posterior_log_variance_clipped = Tensor(
    np.log(np.clip(posterior_variance, 1e-20, None)))  # 计算后验方差的对数,并进行截断
self.posterior_mean_coef1 = Tensor(
    betas * np.sqrt(alphas_cumprod_prev) / (1. - alphas_cumprod))  # 计算后验均值的系数1
self.posterior_mean_coef2 = Tensor(
    (1. - alphas_cumprod_prev) * np.sqrt(alphas) / (1. - alphas_cumprod))  # 计算后验均值的系数2

p2_loss_weight = (p2_loss_weight_k + alphas_cumprod / (1 - alphas_cumprod)) ** - p2_loss_weight_gamma  # 计算p2_loss_weight
self.p2_loss_weight = Tensor(p2_loss_weight)  # 存储p2_loss_weight参数

计算损失:

基于Unet预测出noise,使用预测noise和真实noise计算损失:

self.betas = betas  # 初始化betas参数
self.alphas_cumprod = alphas_cumprod  # 初始化alphas_cumprod参数
self.alphas_cumprod_prev = alphas_cumprod_prev  # 初始化alphas_cumprod_prev参数

# 计算扩散 q(x_t | x_{t-1}) 和其他参数
self.sqrt_alphas_cumprod = Tensor(np.sqrt(alphas_cumprod))  # 计算alphas_cumprod的平方根
self.sqrt_one_minus_alphas_cumprod = Tensor(np.sqrt(1. - alphas_cumprod))  # 计算1 - alphas_cumprod的平方根
self.log_one_minus_alphas_cumprod = Tensor(np.log(1. - alphas_cumprod))  # 计算log(1 - alphas_cumprod)
self.sqrt_recip_alphas_cumprod = Tensor(np.sqrt(1. / alphas_cumprod))  # 计算1 / alphas_cumprod的平方根
self.sqrt_recipm1_alphas_cumprod = Tensor(np.sqrt(1. / alphas_cumprod - 1))  # 计算1 / alphas_cumprod - 1的平方根

posterior_variance = betas * (1. - alphas_cumprod_prev) / (1. - alphas_cumprod)  # 计算后验方差

self.posterior_variance = Tensor(posterior_variance)  # 存储后验方差
self.posterior_log_variance_clipped = Tensor(
    np.log(np.clip(posterior_variance, 1e-20, None)))  # 计算后验方差的对数,并进行截断
self.posterior_mean_coef1 = Tensor(
    betas * np.sqrt(alphas_cumprod_prev) / (1. - alphas_cumprod))  # 计算后验均值的系数1
self.posterior_mean_coef2 = Tensor(
    (1. - alphas_cumprod_prev) * np.sqrt(alphas) / (1. - alphas_cumprod))  # 计算后验均值的系数2

p2_loss_weight = (p2_loss_weight_k + alphas_cumprod / (1 - alphas_cumprod)) ** - p2_loss_weight_gamma  # 计算p2_loss_weight
self.p2_loss_weight = Tensor(p2_loss_weight)  # 存储p2_loss_weight参数

采样:

输出x_start,也就是原始图像,当sampling_time_steps< time_steps,用下方函数:

def ddim_sample(self, shape, clip_denoise=True):
    batch = shape[0]
    total_timesteps, sampling_timesteps, = self.num_timesteps, self.sampling_timesteps
    eta, objective = self.ddim_sampling_eta, self.objective

    # 创建采样时间步列表,[-1, 0, 1, 2, ..., T-1],当sampling_timesteps == total_timesteps时
    times = np.linspace(-1, total_timesteps - 1, sampling_timesteps + 1).astype(np.int32)
    # 创建时间对列表,[(T-1, T-2), (T-2, T-3), ..., (1, 0), (0, -1)]
    times = list(reversed(times.tolist()))
    time_pairs = list(zip(times[:-1], times[1:]))

    img = np.random.randn(*shape).astype(np.float32)  # 随机初始化图像
    x_start = None

    for time, time_next in tqdm(time_pairs, desc='sampling loop time step'):
        time_cond = np.full((batch,), time).astype(np.int32)  # 创建与批次大小相同的时间条件
        x_start = Tensor(x_start) if x_start is not None else x_start
        self_cond = x_start if self.self_condition else None
        predict_noise, x_start, *_ = self.model_predictions(Tensor(img, mindspore.float32),
                                                            Tensor(time_cond),
                                                            self_cond,
                                                            clip_denoise)
        predict_noise, x_start = predict_noise.asnumpy(), x_start.asnumpy()
        if time_next < 0:
            img = x_start
            continue

        alpha = self.alphas_cumprod[time]
        alpha_next = self.alphas_cumprod[time_next]

        sigma = eta * np.sqrt(((1 - alpha / alpha_next) * (1 - alpha_next) / (1 - alpha)))
        c = np.sqrt(1 - alpha_next - sigma ** 2)

        noise = np.random.randn(*img.shape)

        img = x_start * np.sqrt(alpha_next) + c * predict_noise + sigma * noise

    img = self.unnormalize(img)  # 反归一化图像

    return img

3. Trainer 训练器

data_iterator中每次取出的数据集就是一个batch_size大小,每训练一个batch,self.step就会加1。

DDPM的trainer采用ema(指数移动平均)优化,ema不参与训练,只参与推理,比对变量直接赋值而言,移动平均得到的值在图像上更加平缓光滑,抖动性更小。具体代码参考代码仓中ema.py

print('training start')
with tqdm(initial=self.step, total=self.train_num_steps, disable=False) as pbar:
    total_loss = 0.
    for (img,) in data_iterator:
        model.set_train()
        time_emb = Tensor(
            np.random.randint(0, num_timesteps, (img.shape[0],)).astype(np.int32))  # 随机生成时间向量
        noise = Tensor(np.random.randn(*img.shape), mindspore.float32)  # 生成噪声向量

        self_cond = random.random() < 0.5 if self.self_condition else False  # 根据self_condition参数决定是否进行自我条件训练
        loss = train_step(img, time_emb, noise, self_cond)  # 调用train_step函数返回损失值

        total_loss += float(loss.asnumpy())  # 累加损失值

        self.step += 1
        if self.step % gradient_accumulate_every == 0:
            self.ema.update()  # 更新EMA模型的参数
            pbar.set_description(f'loss: {total_loss:.4f}')
            pbar.update(1)
            total_loss = 0.

        accumulate_step = self.step // gradient_accumulate_every
        accumulate_remain_step = self.step % gradient_accumulate_every
        if self.step != 0 and accumulate_step % self.save_and_sample_every == 0 and accumulate_remain_step == 0:
            self.ema.set_train(False)
            self.ema.synchronize()
            batches = num_to_groups(self.num_samples, self.batch_size)
            all_images_list = list(map(lambda n: self.ema.online_model.sample(batch_size=n), batches))
            self.save_images(all_images_list, accumulate_step)  # 保存生成的图像
            self.save(accumulate_step)  # 保存模型的参数
            self.ema.desynchronize()

        if self.step >= gradient_accumulate_every * self.train_num_steps:
            break

print('training complete')

Stable Diffusion

来源:一文读懂Stable Diffusion 论文原理+代码超详细解读 - 知乎 (zhihu.com)

1. AutoEncoderKL 自编码器

AutoEncoderKL 编码器已提前训练好,参数是固定的。训练阶段该模块负责将输入数据集映射到latent space,然后latent space的样本再继续进入扩散模型进行扩散。这一过程在Stable Diffusion代码中被称为 encode_first_stage:

    def get_input(self, x, c):
        # 检查输入 x 的维度是否为3。如果是,则通过使用切片操作 [..., None] 在最后添加一个额外的维度,将其转换为4维张量。
        if len(x.shape) == 3:
            x = x[..., None]
        # 维度转置操作,将 x 的维度顺序从原来的 (batch_size, height, width, channels) 转换为 (batch_size, channels, height, width)。
        x = self.transpose(x, (0, 3, 1, 2))
        # 对输入 x 进行编码操作,并乘以一个名为 scale_factor 的常量。然后,使用 stop_gradient 方法对结果进行梯度停止,即在计算梯度时不会考虑这个部分。
        z = ops.stop_gradient(self.scale_factor * self.first_stage_model.encode(x))

        return z, c

2. FrozenCLIPEmbedder

/encoders/modules.py

将控制条件编码为向量。其核心模块class TextEncoder(nn.Cell)构建函数如下:

    def construct(self, text):
        bsz, ctx_len = text.shape
        flatten_id = text.flatten()  # 将输入文本 text 展平为一维张量。展平操作将多维数组转换为一维,保留原始元素顺序。
        gather_result = self.gather(self.embedding_table, flatten_id, 0) # 从 embedding_table 中根据 flatten_id 提取对应的嵌入向量。
        x = self.reshape(gather_result, (bsz, ctx_len, -1)) # 重塑操作,将 gather_result 重新调整为指定形状 (bsz, ctx_len, -1) 的张量。
        x = x + self.positional_embedding # 引入位置编码
        x = x.transpose(1, 0, 2) # 对 x 进行维度转置操作,将维度顺序从原来的 (bsz, ctx_len, -1) 转换为 (ctx_len, bsz, -1)。
        x = self.transformer_layer(x) # 再次对 x 进行维度转置操作,将维度顺序从 (ctx_len, bsz, -1) 转换回 (bsz, ctx_len, -1)。
        x = x.transpose(1, 0, 2)
        x = self.ln_final(x)
        return x

3. UNet 

    # 这段代码根据条件选择性地创建并添加 AttentionBlock 或 SpatialTransformer 对象到 layers 列表中,并将 layers 列表作为一个整体追加到 self.input_blocks 列表
   layers.append(AttentionBlock(
                            ch,
                            use_checkpoint=use_checkpoint,
                            num_heads=num_heads,
                            num_head_channels=dim_head,
                            use_new_attention_order=use_new_attention_order,
                        ) if not use_spatial_transformer else SpatialTransformer(
                            ch, num_heads, dim_head, depth=transformer_depth, context_dim=context_dim,
                            use_checkpoint=use_checkpoint, dtype=self.dtype, dropout=self.dropout, use_linear=use_linear_in_transformer
                        )
                    )
self.input_blocks.append(layers)

可以看出UNet的每个中间层都会拼接一次SpatialTransformer模块,该模块对应,使用 Attention 机制来更好的学习文本与图像的匹配关系。

    # 对输入数据进行一系列的处理和变换操作,并生成模型的输出
    def construct(self, x, timesteps=None, context=None, y=None):
        """
        Apply the model to an input batch.
        :param x: an [N x C x ...] Tensor of inputs.
        :param timesteps: a 1-D batch of timesteps.
        :param context: conditioning plugged in via crossattn
        :param y: an [N] Tensor of labels, if class-conditional.
        :return: an [N x C x ...] Tensor of outputs.
        """
        
        assert (y is not None) == (
            self.num_classes is not None
        ), "must specify y if and only if the model is class-conditional"

        # 计算了时间步嵌入(timestep embedding)。首先,使用 timestep_embedding 方法生成时间步嵌入向量 t_emb,并根据 self.model_channels 进行相应的通道调整;然后,使用 self.time_embed 方法对 t_emb 进行进一步的时间嵌入处理,得到最终的嵌入向量 emb。
        hs = []
        t_emb = timestep_embedding(timesteps, self.model_channels, repeat_only=False)
        emb = self.time_embed(t_emb)

        # 如果模型是类别条件的,则进一步处理嵌入向量 emb。首先,使用断言语句验证标签 y 的形状是否与输入 x 的批次维度匹配;然后,使用 self.label_emb 方法对标签 y 进行嵌入处理,并将其与嵌入向量 emb 相加。
        if self.num_classes is not None:
            assert y.shape == (x.shape[0],)
            emb = emb + self.label_emb(y)

        h = x
        for celllist in self.input_blocks:
            for cell in celllist:
                h = cell(h, emb, context)
            hs.append(h)

        for module in self.middle_block:
            h = module(h, emb, context)

        hs_index = -1
        for celllist in self.output_blocks:
            h = self.cat((h, hs[hs_index]))
            for cell in celllist:
                h = cell(h, emb, context)
            hs_index -= 1

        if self.predict_codebook_ids:
            return self.id_predictor(h)
        else:
            return self.out(h)
 

4. LDM

扩散模型,用于生成对应采样时间t的样本

    def p_losses(self, x_start, cond, t, noise=None):
        noise = ms.numpy.randn(x_start.shape) # 生成与 x_start 相同形状的随机噪声 noise
        x_noisy = self.q_sample(x_start=x_start, t=t, noise=noise) # 基于给定的输入 x_start、时间步 t 和噪声 noise 生成加噪后的样本 x_noisy
        model_output = self.apply_model(x_noisy, t, cond) // UNet预测的噪声,cond表示FrozenCLIPEmbedder生成的条件

        # 根据参数化方式(parameterization),选择目标值 target。如果参数化方式是 "x0",则将目标值设置为输入 x_start;如果是 "eps",则将目标值设置为随机噪声 noise。如果参数化方式不在这两种情况中,则抛出异常。
        if self.parameterization == "x0":
            target = x_start
        elif self.parameterization == "eps":
            target = noise
        else:
            raise NotImplementedError()

        loss_simple = self.get_loss(model_output, target, mean=False).mean([1, 2, 3]) //计算预测noise与真实noise的损失值。mean=False 表示不进行均值操作。然后,在维度 [1, 2, 3] 上进行均值操作,得到简单损失 loss_simple。

        logvar_t = self.logvar[t]
        loss = loss_simple / ops.exp(logvar_t) + logvar_t
        loss = self.l_simple_weight * loss.mean()

        loss_vlb = self.get_loss(model_output, target, mean=False).mean((1, 2, 3))
        loss_vlb = (self.lvlb_weights[t] * loss_vlb).mean()
        loss += (self.original_elbo_weight * loss_vlb) # 最后,将乘积结果与原始ELBO(Evidence Lower Bound)权重 self.original_elbo_weight 相加,更新总体损失 loss。
        
        return loss

self.apply_model代码如下:

    # 该方法将噪声样本输入模型进行处理,并生成重构的输出结果
    # 输入的噪声样本 x_noisy、时间步 t 和条件 cond。首先使用 ops.cast() 将输入的 x_noisy 和 cond 强制转换为指定的数据类型 self.dtype。
     def apply_model(self, x_noisy, t, cond, return_ids=False):
        x_noisy = ops.cast(x_noisy, self.dtype)
        cond = ops.cast(cond, self.dtype)

        # 检查条件 cond 是否为字典类型。如果是,则表示为混合情况,不做任何操作。
        # 如果条件不是字典类型,则根据模型的 conditioning_key 属性,选择键值对的键名 key。
        # 如果 conditioning_key 是 'concat',则将键名设置为 'c_concat';否则,将键名设置为 'c_crossattn'。然后,将条件 cond 转换为包含单个键值对的字典。
        if isinstance(cond, dict):
            # hybrid case, cond is expected to be a dict
            pass
        else:
            key = 'c_concat' if self.model.conditioning_key == 'concat' else 'c_crossattn'
            cond = {key: cond}

        x_recon = self.model(x_noisy, t, **cond) // self.model表示UNet模型。根据传递给模型的参数,模型将生成重构的输出结果 x_recon。

        # 对重构的输出进行处理,并返回最终的结果。如果 x_recon 是一个元组类型且 return_ids 为假(即不返回标识符),则返回元组中的第一个元素;否则,直接返回 x_recon。
        if isinstance(x_recon, tuple) and not return_ids:
            return x_recon[0]
        else:
            return x_recon

LDM将损失函数反向传播来更新UNet模型的参数,AutoEncoderKL 和 FrozenCLIPEmbedder的参数在该反向传播中不会被更新。

从上述代码可以看出UNet的每个中间层都会拼接一次SpatialTransformer模块,该模块对应,使用 Attention 机制来更好的学习文本与图像的匹配关系。

  • 8
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值