扩散模型DDPM

摘自:https://zhuanlan.zhihu.com/p/563661713

“What I cannot create, I do not understand.” — Richard Feynman

https://github.com/xiaohu2015/nngen/tree/main/models/diffusion_models

论文:https://arxiv.org/abs/2006.11239

近段时间最火的方向无疑是基于文本用AI生成图像,继OpenAI在2021提出的文本转图像模型DALLE之后,越来越多的大公司卷入这个方向,如谷歌在今年相继推出了ImagenParti。一些主流的文本转图像模型如DALL·E 2,stable-diffusion和Imagen采用了扩散模型Diffusion Model)作为图像生成模型,这也引发了对扩散模型的研究热潮。相比GAN来说,扩散模型训练更稳定,而且能够生成更多样的样本,OpenAI的论文Diffusion Models Beat GANs on Image Synthesis也证明了扩散模型能够超越GAN。简单来说,扩散模型包含两个过程:前向扩散过程反向生成过程,前向扩散过程是对一张图像逐渐添加高斯噪音直至变成随机噪音,而反向生成过程是去噪音过程,我们将从一个随机噪音开始逐渐去噪音直至生成一张图像,这也是我们要求解或者训练的部分。扩散模型与其它主流生成模型的对比如下所示:

目前所采用的扩散模型大都是来自于2020年的工作DDPM: Denoising Diffusion Probabilistic Models,DDPM对之前的扩散模型(具体见Deep Unsupervised Learning using Nonequilibrium Thermodynamics)进行了简化,并通过变分推断(variational inference)来进行建模,这主要是因为扩散模型也是一个隐变量模型(latent variable model),相比VAE这样的隐变量模型,扩散模型的隐变量是和原始数据是同维度的,而且推理过程(即扩散过程)往往是固定的。这篇文章将基于DDPM详细介绍扩散模型的原理,并给出具体的代码实现和分析。

扩散模型原理

扩散模型包括两个过程:前向过程(forward process)反向过程(reverse process),其中前向过程又称为为扩散过程(diffusion process),如下图所示。无论是前向过程还是反向过程都是一个参数化的马尔可夫链(Markov chain),其中反向过程可以用来生成数据,这里我们将通过变分推断来进行建模和求解。

扩散过程

扩散过程是指的对数据逐渐增加高斯噪音直至数据变成随机噪音的过程。对于原始数据

,总共包含T步的扩散过程的每一步都是对上一步得到的数据xt-1按如下方式增加高斯噪音:

这里{βt}t=1~T为每一步所采用的方差,它介于0~1之间。对于扩散模型,我们往往称不同step的方差设定为variance schedule或者noise schedule,通常情况下,越后面的step会采用更大的方差,即满足β1<β2<⋯<βT。在一个设计好的variance schedule下,的如果扩散步数T足够大,那么最终得到的xT就完全丢失了原始数据而变成了一个随机噪音。 扩散过程的每一步都生成一个带噪音的数据xt,整个扩散过程也就是一个马尔卡夫链

另外要指出的是, 扩散过程往往是固定的, 即采用一个预先定义好的variance schedule, 比 如DDPM就采用一个线性的variance schedule。扩散过程的一个重要特性是我们可以直接基 于原始数据 \(\mathbf{x}0\) 来对任意 \(t\)步的 \(\mathbf{x}_t\) 进行采样: \(\mathbf{x}_t \sim q\left(\mathbf{x}_t \mid \mathbf{x}_0\right)\) 。这里定义 \(\alpha_t=1-\beta_t\) 和 \(\bar{\alpha}_t=\prod{i=1}^t \alpha_i\) , 通过重参数技巧(和VAE类似), 那么有:

上述推到过程利用了两个方差不同的高斯分布\(\mathcal{N}\left(\mathbf{0}, \sigma_1^2 \mathbf{I}\right)\) 和 \(\mathcal{N}\left(\mathbf{0}, \sigma_2^2 \mathbf{I}\right)\) 相加等于一个新的高斯分 布 \(\mathcal{N}\left(\mathbf{0},\left(\sigma_1^2+\sigma_2^2\right) \mathbf{I}\right)\) 。反重参数化后, 我们得到:
\[
q\left(\mathbf{x}_t \mid \mathbf{x}_0\right)=\mathcal{N}\left(\mathbf{x}_t ; \sqrt{\bar{\alpha}_t} \mathbf{x}_0,\left(1-\bar{\alpha}_t\right) \mathbf{I}\right)
\]
扩散过程的这个特性很重要。首先, 我们可以看到 \(\mathbf{x}_t\) 其实可以看成是原始数据 \(\mathbf{x}_0\) 和随机噪音 \(\epsilon\) 的线性组合, 其中\(\sqrt{\bar{\alpha}_t}\) 和 \(\sqrt{1-\bar{\alpha}_t}\) 为组合系数, 它们的平方和等于 1 , 我们也可以称两者分别 为 signal_rate 和 noise_rate (见https://keras.io/examples/generative/ddim/#diffusionschedule和Variational Diffusion Models)。更近一步地,我们可以基于 \(\bar{\alpha}_t\) 而不是 \(\beta_t\) 来定义 noise schedule (见Improved Denoising Diffusion Probabilistic Models所设计的cosine schedule), 因为这样处理更直接, 比如我们直接将 \(\bar{\alpha}_T\) 设定为一个接近0的值, 那么就可以保 证最终得到的 \(\mathbf{x}_T\) 近似为一个随机噪音。其次, 后面的建模和分析过程将使用这个特性。

反向过程

扩散过程是将数据噪音化,那么反向过程就是一个去噪的过程,如果我们知道反向过程的每一步的真实分布q(xt−1|xt),那么从一个随机噪音xT∼N(0,I)开始,逐渐去噪就能生成一个真实的样本,所以反向过程也就是生成数据的过程

估计分布 \(q\left(\mathbf{x}{t-1} \mid \mathbf{x}_t\right)\) 需要用到整个训练样本, 我们可以用神经网络来估计这些分布。这里, 我们将反向过程也定义为一个马尔卡夫链, 只不过它是由一系列用神经网络参数化的高斯分布来组成:

\[p\theta\left(\mathbf{x}{0: T}\right)=p\left(\mathbf{x}_T\right) \prod{t=1}^T p_\theta\left(\mathbf{x}{t-1} \mid \mathbf{x}_t\right) \quad p\theta\left(\mathbf{x}{t-1} \mid \mathbf{x}_t\right)=\mathcal{N}\left(\mathbf{x}{t-1} ; \boldsymbol{\mu}\theta\left(\mathbf{x}_t, t\right), \mathbf{\Sigma}\theta\left(\mathbf{x}t, t\right)\right)\]

这里 \(p\left(\mathbf{x}_T\right)=\mathcal{N}\left(\mathbf{x}_T ; \mathbf{0}, \mathbf{I}\right)\), 而 \(p\theta\left(\mathbf{x}{t-1} \mid \mathbf{x}_t\right)\) 为参数化的高斯分布, 它们的均值和方差由训练的网络 \(\boldsymbol{\mu}\theta\left(\mathbf{x}t, t\right)\) 和 \(\boldsymbol{\Sigma}\theta\left(\mathbf{x}t, t\right)\) 给出。实际上, 扩散模型就是要得到这些训练好的网络, 因为它们构 成了最终的生成模型。虽然分布 \(q\left(\mathbf{x}{t-1} \mid \mathbf{x}t\right)\) 是不可直接处理的, 但是加上条件\(\mathbf{x}_0\) 的后验分布 \(q\left(\mathbf{x}{t-1} \mid \mathbf{x}t, \mathbf{x}_0\right)\) 却是可处理的, 这里有:

\[q\left(\mathbf{x}{t-1} \mid \mathbf{x}t, \mathbf{x}_0\right)=\mathcal{N}\left(\mathbf{x}{t-1} ; \tilde{\boldsymbol{\mu}}\left(\mathbf{x}_t, \mathbf{x}_0\right), \tilde{\beta}_t \mathbf{I}\right)
\]

下面我们来具体推导这个分布,首先根据贝叶斯公式,我们有:

由于扩散过程的马尔卡夫链特性,我们知道分布

,而由前面得到的扩散过程特性可知:

所以,我们有:

这里的 \(C\left(\mathbf{x}t, \mathbf{x}_0\right)\) 是一个和 \(\mathbf{x}{t-1}\) 无关的部分,所以省略。根据高斯分布的概率密度函数定义和上 述结果 (配平方),我们可以得到后验分布 \(q\left(\mathbf{x}t \mid \mathbf{x}{t-1}, \mathbf{x}0\right)\) 的均值和方差:


可以看到方差是一个定量 (扩散过程参数固定),而均值是一个依赖 \(\mathbf{x}_0\) 和 \(\mathbf{x}_t\) 的函数。这个分布将 会被用于推导扩散模型的优化目标。

优化目标

上面介绍了扩散模型的扩散过程和反向过程,现在我们来从另外一个角度来看扩散模型:如果我们把中间产生的变量看成隐变量的话,那么扩散模型其实是包含T个隐变量的隐变量模型(latent variable model),它可以看成是一个特殊的Hierarchical VAEs(见Understanding Diffusion Models: A Unified Perspective):

相比VAE来说,扩散模型的隐变量是和原始数据同维度的,而且encoder(即扩散过程)是固定的。既然扩散模型是隐变量模型,那么我们可以就可以基于变分推断来得到variational lower boundVLB,又称ELBO)作为最大化优化目标,这里有:

这里最后一步是利用了Jensen’s inequality(不采用这个不等式的推导见博客What are Diffusion Models?),对于网络训练来说,其训练目标为VLB取负

我们近一步对训练目标进行分解可得:

可以看到最终的优化目标共包含 \(T+1\) 项,其中 \(L_0\) 可以看成是原始数据重建,优化的是负对数似然, \(L_0\) 可以用估计的 \(\mathcal{N}\left(\mathbf{x}0 ; \boldsymbol{\mu}\theta\left(\mathbf{x}1, 1\right), \mathbf{\Sigma}\theta\left(\mathbf{x}1, 1\right)\right)\) 来构建一个离散化的decoder来计算(见 DDPM论文3.3部分);而 \(L_T\) 计算的是最后得到的噪音的分布和先验分布的KL散度,这个KL散度没有训练参数,近似为 0 ,因为先验 \(p\left(\mathbf{x}_T\right)=\mathcal{N}(\mathbf{0}, \mathbf{I})\) 而扩散过程最后得到的随机噪音 \(q\left(\mathbf{x}_T \mid \mathbf{x}_0\right)\) 也近似为 \(\mathcal{N}(\mathbf{0}, \mathbf{I})\) ;而 \(L{t-1}\) 则是计算的是估计分布 \(p_\theta\left(\mathbf{x}{t-1} \mid \mathbf{x}_t\right)\) 和真实后验分布 \(q\left(\mathbf{x}{t-1} \mid \mathbf{x}_t, \mathbf{x}_0\right)\) 的KL散度,这里希望䇝们估计的去噪过程和依赖真实数据的去噪过程近似一致:

之所以前面我们将 \(p_\theta\left(\mathbf{x}{t-1} \mid \mathbf{x}_t\right)\) 定义为一个用网络参数化的高斯分布 \(\mathcal{N}\left(\mathbf{x}{t-1} ; \boldsymbol{\mu}\theta\left(\mathbf{x}_t, t\right), \mathbf{\Sigma}\theta\left(\mathbf{x}t, t\right)\right)\), 是因为要匹配的后验分布 \(q\left(\mathbf{x}{t-1} \mid \mathbf{x}t, \mathbf{x}_0\right)\)也是一个高斯分布。对 于训练目标 \(L_0\) 和 \(L{t-1}\) 来说, 都是希望得到训练好的网络 \(\boldsymbol{\mu}\theta\left(\mathbf{x}_t, t\right)\) 和 \(\boldsymbol{\Sigma}\theta\left(\mathbf{x}t, t\right)\) (对于 \(L_0, t=1\) )。DDPM对 \(p\theta\left(\mathbf{x}{t-1} \mid \mathbf{x}_t\right)\) 做了近一步简化, 采用周定的方差: \(\boldsymbol{\Sigma}\theta\left(\mathbf{x}t, t\right)=\sigma_t^2 \mathbf{I}\), 这里的 \(\sigma_t^2\) 可以 设定为 \(\beta_t\) 或者 \(\tilde{\beta}_t\) (这其实是两个极端, 分别是上限和下限, 也可以采用可训练的方差, 见论文 Improved Denoising Diffusion Probabilistic Models 和Analytic-DPM: an Analytic Estimate of the Optimal Reverse Variance in Diffusion Probabilistic Models)。这里假定 \(\sigma_t^2=\tilde{\beta}_t\), 那么:

\[q\left(\mathbf{x}{t-1} \mid \mathbf{x}t, \mathbf{x}_0\right)=\mathcal{N}\left(\mathbf{x}{t-1} ; \tilde{\boldsymbol{\mu}}\left(\mathbf{x}t, \mathbf{x}_0\right), \sigma_t^2 \mathbf{I}\right) p\theta\left(\mathbf{x}{t-1} \mid \mathbf{x}_t\right)=\mathcal{N}\left(\mathbf{x}{t-1} ; \boldsymbol{\mu}_\theta\left(\mathbf{x}_t, t\right), \sigma_t^2 \mathbf{I}\right)
\]
对于两个高斯分布的KL散度, 其计算公式为(具体推导见生成模型之VAE):

那么就有:

那么优化目标即\(L{t-1}\)为:

从上述公式来看, 我们是希望网络学习到的均值 \(\boldsymbol{\mu}\theta\left(\mathbf{x}_t, t\right)\) 和后验分布的均值 \(\tilde{\boldsymbol{\mu}}\left(\mathbf{x}_t, \mathbf{x}_0\right)\) 一致。不 过DDPM发现预测均值并不是最好的选择。根据前面得到的扩散过程的特性, 我们有:

\(\mathbf{x}{\mathbf{t}}\left(\mathbf{x}_{\mathbf{0}}, \epsilon\right)=\sqrt{\bar{\alpha}_t} \mathbf{x}_0+\sqrt{1-\bar{\alpha}_t \epsilon} \quad \text { where } \epsilon \sim \mathcal{N}(\mathbf{0}, \mathbf{I})
\)

将这个公式带入上述优化目标,可以得到:

近一步地, 我们对 \(\boldsymbol{\mu}\theta\left(\mathbf{x}{\mathbf{t}}\left(\mathbf{x}0, \epsilon\right), t\right)\) 也进行重参数化, 变成:

\[\boldsymbol{\mu}\theta\left(\mathbf{x}{\mathbf{t}}\left(\mathbf{x}_0, \epsilon\right), t\right)=\frac{1}{\sqrt{\alpha_t}}\left(\mathbf{x}_t\left(\mathbf{x}_0, \epsilon\right)-\frac{\beta_t}{\sqrt{1-\bar{\alpha}_t}} \epsilon\theta\left(\mathbf{x}t\left(\mathbf{x}_0, \epsilon\right), t\right)\right)\]

这里的 \(\epsilon\theta\) 是一个基于神经网络的拟合函数, 这意味着我们由原来的预测均值而换成预测噪音 \(\epsilon\) 。我们将上述等式带入优化目标, 可以得到:

DDPM近一步对上述目标进行了简化, 即去掉了权重系数, 变成了: \(L{t-1}^{\text {simple }}=\mathbb{E}{\mathbf{x}_0, \epsilon \sim \mathcal{N}(0, \mathrm{I})}\left[\left|\epsilon-\epsilon\theta\left(\sqrt{\bar{\alpha}_t} \mathbf{x}_0+\sqrt{1-\bar{\alpha}_t} \epsilon, t\right)\right|^2\right]\) 这里的 \(t\) 在 \([1, \mathrm{~T}]\) 范围内取值(如前所述, 其中取 1 时对应 \(L_0\) )。由于去掉了不同 \(t\)的权重系数, 所以这个简化的目标其实是VLB优化 目标进行了 reweight。从DDPM的对比实验结果来看, 预测噪音比预测均值效果要好, 采用简 化版本的优化目标比VLB目标效果要好:

虽然扩散模型背后的推导比较复杂,但是我们最终得到的优化目标非常简单,就是让网络预测的噪音和真实的噪音一致。DDPM的训练过程也非常简单,如下图所示:随机选择一个训练样本->从1-T随机抽样一个t->随机产生噪音-计算当前所产生的带噪音数据(红色框所示)->输入网络预测噪音->计算产生的噪音和预测的噪音的L2损失->计算梯度并更新网络。

一旦训练完成,其采样过程也非常简单,如上所示:我们从一个随机噪音开始,并用训练好的网络预测噪音,然后计算条件分布的均值(红色框部分),然后用均值加标准差乘以一个随机噪音,直至t=0完成新样本的生成(最后一步不加噪音)。不过实际的代码实现和上述过程略有区别(见https://github.com/hojonathanho/diffusion/issues/5:先基于预测的噪音生成,并进行了clip处理(范围[-1, 1],原始数据归一化到这个范围),然后再计算均值。我个人的理解这应该算是一种约束,既然模型预测的是噪音,那么我们也希望用预测噪音重构处理的原始数据也应该满足范围要求。

模型设计

前面我们介绍了扩散模型的原理以及优化目标,那么扩散模型的核心就在于训练噪音预测模型,由于噪音和原始数据是同维度的,所以我们可以选择采用AutoEncoder架构来作为噪音预测模型。DDPM所采用的模型是一个基于residual block和attention block的U-Net模型。如下所示:

U-Net属于encoder-decoder架构,其中encoder分成不同的stages,每个stage都包含下采样模块来降低特征的空间大小(H和W),然后decoder和encoder相反,是将encoder压缩的特征逐渐恢复。U-Net在decoder模块中还引入了skip connection,即concat了encoder中间得到的同维度特征,这有利于网络优化。DDPM所采用的U-Net每个stage包含2个residual block,而且部分stage还加入了self-attention模块增加网络的全局建模能力。 另外,扩散模型其实需要的是个噪音预测模型,实际处理时,我们可以增加一个time embedding(类似transformer中的position embedding)来将timestep编码到网络中,从而只需要训练一个共享的U-Net模型。具体地,DDPM在各个residual block都引入了time embedding,如上图所示。

代码实现

最后,我们基于PyTorch框架给出DDPM的具体实现,这里主要参考了三套代码实现:

首先,是time embeding,这里是采用Attention Is All You Need中所设计的sinusoidal position embedding,只不过是用来编码timestep:

# use sinusoidal position embedding to encode time step (https://arxiv.org/abs/1706.03762)   
def timestep_embedding(timesteps, dim, max_period=10000):
    """
    Create sinusoidal timestep embeddings.
    :param timesteps: a 1-D Tensor of N indices, one per batch element.
                      These may be fractional.
    :param dim: the dimension of the output.
    :param max_period: controls the minimum frequency of the embeddings.
    :return: an [N x dim] Tensor of positional embeddings.
    """
    half = dim // 2
    freqs = torch.exp(
        -math.log(max_period) * torch.arange(start=0, end=half, dtype=torch.float32) / half
    ).to(device=timesteps.device)
    args = timesteps[:, None].float() * freqs[None]
    embedding = torch.cat([torch.cos(args), torch.sin(args)], dim=-1)
    if dim % 2:
        embedding = torch.cat([embedding, torch.zeros_like(embedding[:, :1])], dim=-1)
    return embedding

由于只有residual block才引入time embedding,所以可以定义一些辅助模块来自动处理,如下所示:

# define TimestepEmbedSequential to support `time_emb` as extra input
class TimestepBlock(nn.Module):
    """
    Any module where forward() takes timestep embeddings as a second argument.
    """

    @abstractmethod
    def forward(self, x, emb):
        """
        Apply the module to `x` given `emb` timestep embeddings.
        """


class TimestepEmbedSequential(nn.Sequential, TimestepBlock):
    """
    A sequential module that passes timestep embeddings to the children that
    support it as an extra input.
    """

    def forward(self, x, emb):
        for layer in self:
            if isinstance(layer, TimestepBlock):
                x = layer(x, emb)
            else:
                x = layer(x)
        return x

这里所采用的U-Net采用GroupNorm进行归一化,所以这里也简单定义了一个norm layer以方便使用:

# use GN for norm layer
def norm_layer(channels):
    return nn.GroupNorm(32, channels)

U-Net的核心模块是residual block,它包含两个卷积层以及shortcut,同时也要引入time embedding,这里额外定义了一个linear层来将time embedding变换为和特征维度一致,第一conv之后通过加上time embedding来编码time:

# Residual block
class ResidualBlock(TimestepBlock):
    def __init__(self, in_channels, out_channels, time_channels, dropout):
        super().__init__()
        self.conv1 = nn.Sequential(
            norm_layer(in_channels),
            nn.SiLU(),
            nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1)
        )
        
        # pojection for time step embedding
        self.time_emb = nn.Sequential(
            nn.SiLU(),
            nn.Linear(time_channels, out_channels)
        )
        
        self.conv2 = nn.Sequential(
            norm_layer(out_channels),
            nn.SiLU(),
            nn.Dropout(p=dropout),
            nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1)
        )

        if in_channels != out_channels:
            self.shortcut = nn.Conv2d(in_channels, out_channels, kernel_size=1)
        else:
            self.shortcut = nn.Identity()


    def forward(self, x, t):
        """
        `x` has shape `[batch_size, in_dim, height, width]`
        `t` has shape `[batch_size, time_dim]`
        """
        h = self.conv1(x)
        # Add time step embeddings
        h += self.time_emb(t)[:, :, None, None]
        h = self.conv2(h)
        return h + self.shortcut(x)

这里还在部分residual block引入了attention,这里的attention和transformer的self-attention是一致的:

# Attention block with shortcut
class AttentionBlock(nn.Module):
    def __init__(self, channels, num_heads=1):
        super().__init__()
        self.num_heads = num_heads
        assert channels % num_heads == 0
        
        self.norm = norm_layer(channels)
        self.qkv = nn.Conv2d(channels, channels * 3, kernel_size=1, bias=False)
        self.proj = nn.Conv2d(channels, channels, kernel_size=1)

    def forward(self, x):
        B, C, H, W = x.shape
        qkv = self.qkv(self.norm(x))
        q, k, v = qkv.reshape(B*self.num_heads, -1, H*W).chunk(3, dim=1)
        scale = 1. / math.sqrt(math.sqrt(C // self.num_heads))
        attn = torch.einsum("bct,bcs->bts", q * scale, k * scale)
        attn = attn.softmax(dim=-1)
        h = torch.einsum("bts,bcs->bct", attn, v)
        h = h.reshape(B, -1, H, W)
        h = self.proj(h)
        return h + x

对于上采样模块和下采样模块,其分别可以采用插值和stride=2的conv或者pooling来实现:

# upsample
class Upsample(nn.Module):
    def __init__(self, channels, use_conv):
        super().__init__()
        self.use_conv = use_conv
        if use_conv:
            self.conv = nn.Conv2d(channels, channels, kernel_size=3, padding=1)

    def forward(self, x):
        x = F.interpolate(x, scale_factor=2, mode="nearest")
        if self.use_conv:
            x = self.conv(x)
        return x

# downsample
class Downsample(nn.Module):
    def __init__(self, channels, use_conv):
        super().__init__()
        self.use_conv = use_conv
        if use_conv:
            self.op = nn.Conv2d(channels, channels, kernel_size=3, stride=2, padding=1)
        else:
            self.op = nn.AvgPool2d(stride=2)

    def forward(self, x):
        return self.op(x)

上面我们实现了U-Net的所有组件,就可以进行组合来实现U-Net了:

 The full UNet model with attention and timestep embedding
class UNetModel(nn.Module):
    def __init__(
        self,
        in_channels=3,
        model_channels=128,
        out_channels=3,
        num_res_blocks=2,
        attention_resolutions=(8, 16),
        dropout=0,
        channel_mult=(1, 2, 2, 2),
        conv_resample=True,
        num_heads=4
    ):
        super().__init__()

        self.in_channels = in_channels
        self.model_channels = model_channels
        self.out_channels = out_channels
        self.num_res_blocks = num_res_blocks
        self.attention_resolutions = attention_resolutions
        self.dropout = dropout
        self.channel_mult = channel_mult
        self.conv_resample = conv_resample
        self.num_heads = num_heads
        
        # time embedding
        time_embed_dim = model_channels * 4
        self.time_embed = nn.Sequential(
            nn.Linear(model_channels, time_embed_dim),
            nn.SiLU(),
            nn.Linear(time_embed_dim, time_embed_dim),
        )
        
        # down blocks
        self.down_blocks = nn.ModuleList([
            TimestepEmbedSequential(nn.Conv2d(in_channels, model_channels, kernel_size=3, padding=1))
        ])
        down_block_chans = [model_channels]
        ch = model_channels
        ds = 1
        for level, mult in enumerate(channel_mult):
            for _ in range(num_res_blocks):
                layers = [
                    ResidualBlock(ch, mult * model_channels, time_embed_dim, dropout)
                ]
                ch = mult * model_channels
                if ds in attention_resolutions:
                    layers.append(AttentionBlock(ch, num_heads=num_heads))
                self.down_blocks.append(TimestepEmbedSequential(*layers))
                down_block_chans.append(ch)
            if level != len(channel_mult) - 1: # don't use downsample for the last stage
                self.down_blocks.append(TimestepEmbedSequential(Downsample(ch, conv_resample)))
                down_block_chans.append(ch)
                ds *= 2
        
        # middle block
        self.middle_block = TimestepEmbedSequential(
            ResidualBlock(ch, ch, time_embed_dim, dropout),
            AttentionBlock(ch, num_heads=num_heads),
            ResidualBlock(ch, ch, time_embed_dim, dropout)
        )
        
        # up blocks
        self.up_blocks = nn.ModuleList([])
        for level, mult in list(enumerate(channel_mult))[::-1]:
            for i in range(num_res_blocks + 1):
                layers = [
                    ResidualBlock(
                        ch + down_block_chans.pop(),
                        model_channels * mult,
                        time_embed_dim,
                        dropout
                    )
                ]
                ch = model_channels * mult
                if ds in attention_resolutions:
                    layers.append(AttentionBlock(ch, num_heads=num_heads))
                if level and i == num_res_blocks:
                    layers.append(Upsample(ch, conv_resample))
                    ds //= 2
                self.up_blocks.append(TimestepEmbedSequential(*layers))

        self.out = nn.Sequential(
            norm_layer(ch),
            nn.SiLU(),
            nn.Conv2d(model_channels, out_channels, kernel_size=3, padding=1),
        )

    def forward(self, x, timesteps):
        """
        Apply the model to an input batch.
        :param x: an [N x C x H x W] Tensor of inputs.
        :param timesteps: a 1-D batch of timesteps.
        :return: an [N x C x ...] Tensor of outputs.
        """
        hs = []
        # time step embedding
        emb = self.time_embed(timestep_embedding(timesteps, self.model_channels))
        
        # down stage
        h = x
        for module in self.down_blocks:
            h = module(h, emb)
            hs.append(h)
        # middle stage
        h = self.middle_block(h, emb)
        # up stage
        for module in self.up_blocks:
            cat_in = torch.cat([h, hs.pop()], dim=1)
            h = module(cat_in, emb)
        return self.out(h)

对于扩散过程,其主要的参数就是timesteps和noise schedule,DDPM采用范围为[0.0001, 0.02]的线性noise schedule,其默认采用的总扩散步数为1000

# beta schedule
def linear_beta_schedule(timesteps):
    scale = 1000 / timesteps
    beta_start = scale * 0.0001
    beta_end = scale * 0.02
    return torch.linspace(beta_start, beta_end, timesteps, dtype=torch.float64)

我们定义个扩散模型,它主要要提前根据设计的noise schedule来计算一些系数,并实现一些扩散过程和生成过程:

class GaussianDiffusion:
    def __init__(
        self,
        timesteps=1000,
        beta_schedule='linear'
    ):
        self.timesteps = timesteps
        
        if beta_schedule == 'linear':
            betas = linear_beta_schedule(timesteps)
        elif beta_schedule == 'cosine':
            betas = cosine_beta_schedule(timesteps)
        else:
            raise ValueError(f'unknown beta schedule {beta_schedule}')
        self.betas = betas
            
        self.alphas = 1. - self.betas
        self.alphas_cumprod = torch.cumprod(self.alphas, axis=0)
        self.alphas_cumprod_prev = F.pad(self.alphas_cumprod[:-1], (1, 0), value=1.)
        
        # calculations for diffusion q(x_t | x_{t-1}) and others
        self.sqrt_alphas_cumprod = torch.sqrt(self.alphas_cumprod)
        self.sqrt_one_minus_alphas_cumprod = torch.sqrt(1.0 - self.alphas_cumprod)
        self.log_one_minus_alphas_cumprod = torch.log(1.0 - self.alphas_cumprod)
        self.sqrt_recip_alphas_cumprod = torch.sqrt(1.0 / self.alphas_cumprod)
        self.sqrt_recipm1_alphas_cumprod = torch.sqrt(1.0 / self.alphas_cumprod - 1)
        
        # calculations for posterior q(x_{t-1} | x_t, x_0)
        self.posterior_variance = (
            self.betas * (1.0 - self.alphas_cumprod_prev) / (1.0 - self.alphas_cumprod)
        )
        # below: log calculation clipped because the posterior variance is 0 at the beginning
        # of the diffusion chain
        self.posterior_log_variance_clipped = torch.log(self.posterior_variance.clamp(min =1e-20))
        
        self.posterior_mean_coef1 = (
            self.betas * torch.sqrt(self.alphas_cumprod_prev) / (1.0 - self.alphas_cumprod)
        )
        self.posterior_mean_coef2 = (
            (1.0 - self.alphas_cumprod_prev)
            * torch.sqrt(self.alphas)
            / (1.0 - self.alphas_cumprod)
        )
    
    # get the param of given timestep t
    def _extract(self, a, t, x_shape):
        batch_size = t.shape[0]
        out = a.to(t.device).gather(0, t).float()
        out = out.reshape(batch_size, *((1,) * (len(x_shape) - 1)))
        return out
    
    # forward diffusion (using the nice property): q(x_t | x_0)
    def q_sample(self, x_start, t, noise=None):
        if noise is None:
            noise = torch.randn_like(x_start)

        sqrt_alphas_cumprod_t = self._extract(self.sqrt_alphas_cumprod, t, x_start.shape)
        sqrt_one_minus_alphas_cumprod_t = self._extract(self.sqrt_one_minus_alphas_cumprod, t, x_start.shape)

        return sqrt_alphas_cumprod_t * x_start + sqrt_one_minus_alphas_cumprod_t * noise
    
    # Get the mean and variance of q(x_t | x_0).
    def q_mean_variance(self, x_start, t):
        mean = self._extract(self.sqrt_alphas_cumprod, t, x_start.shape) * x_start
        variance = self._extract(1.0 - self.alphas_cumprod, t, x_start.shape)
        log_variance = self._extract(self.log_one_minus_alphas_cumprod, t, x_start.shape)
        return mean, variance, log_variance
    
    # Compute the mean and variance of the diffusion posterior: q(x_{t-1} | x_t, x_0)
    def q_posterior_mean_variance(self, x_start, x_t, t):
        posterior_mean = (
            self._extract(self.posterior_mean_coef1, t, x_t.shape) * x_start
            + self._extract(self.posterior_mean_coef2, t, x_t.shape) * x_t
        )
        posterior_variance = self._extract(self.posterior_variance, t, x_t.shape)
        posterior_log_variance_clipped = self._extract(self.posterior_log_variance_clipped, t, x_t.shape)
        return posterior_mean, posterior_variance, posterior_log_variance_clipped
    
    # compute x_0 from x_t and pred noise: the reverse of `q_sample`
    def predict_start_from_noise(self, x_t, t, noise):
        return (
            self._extract(self.sqrt_recip_alphas_cumprod, t, x_t.shape) * x_t -
            self._extract(self.sqrt_recipm1_alphas_cumprod, t, x_t.shape) * noise
        )
    
    # compute predicted mean and variance of p(x_{t-1} | x_t)
    def p_mean_variance(self, model, x_t, t, clip_denoised=True):
        # predict noise using model
        pred_noise = model(x_t, t)
        # get the predicted x_0: different from the algorithm2 in the paper
        x_recon = self.predict_start_from_noise(x_t, t, pred_noise)
        if clip_denoised:
            x_recon = torch.clamp(x_recon, min=-1., max=1.)
        model_mean, posterior_variance, posterior_log_variance = \
                    self.q_posterior_mean_variance(x_recon, x_t, t)
        return model_mean, posterior_variance, posterior_log_variance
        
    # denoise_step: sample x_{t-1} from x_t and pred_noise
    @torch.no_grad()
    def p_sample(self, model, x_t, t, clip_denoised=True):
        # predict mean and variance
        model_mean, _, model_log_variance = self.p_mean_variance(model, x_t, t,
                                                    clip_denoised=clip_denoised)
        noise = torch.randn_like(x_t)
        # no noise when t == 0
        nonzero_mask = ((t != 0).float().view(-1, *([1] * (len(x_t.shape) - 1))))
        # compute x_{t-1}
        pred_img = model_mean + nonzero_mask * (0.5 * model_log_variance).exp() * noise
        return pred_img
    
    # denoise: reverse diffusion
    @torch.no_grad()
    def p_sample_loop(self, model, shape):
        batch_size = shape[0]
        device = next(model.parameters()).device
        # start from pure noise (for each example in the batch)
        img = torch.randn(shape, device=device)
        imgs = []
        for i in tqdm(reversed(range(0, timesteps)), desc='sampling loop time step', total=timesteps):
            img = self.p_sample(model, img, torch.full((batch_size,), i, device=device, dtype=torch.long))
            imgs.append(img.cpu().numpy())
        return imgs
    
    # sample new images
    @torch.no_grad()
    def sample(self, model, image_size, batch_size=8, channels=3):
        return self.p_sample_loop(model, shape=(batch_size, channels, image_size, image_size))
    
    # compute train losses
    def train_losses(self, model, x_start, t):
        # generate random noise
        noise = torch.randn_like(x_start)
        # get x_t
        x_noisy = self.q_sample(x_start, t, noise=noise)
        predicted_noise = model(x_noisy, t)
        loss = F.mse_loss(noise, predicted_noise)
        return loss

其中几个主要的函数总结如下:

  • q_sample:实现的从x0到xt扩散过程;
  • q_posterior_mean_variance:实现的是后验分布的均值和方差的计算公式;
  • predict_start_from_noiseq_sample的逆过程,根据预测的噪音来生成x0;
  • p_mean_variance:根据预测的噪音来计算pθ(xt−1|xt)的均值和方差;
  • p_sample:单个去噪step;
  • p_sample_loop:整个去噪音过程,即生成过程。

扩散模型的训练过程非常简单,如下所示:

# train
epochs = 10

for epoch in range(epochs):
    for step, (images, labels) in enumerate(train_loader):
        optimizer.zero_grad()
        
        batch_size = images.shape[0]
        images = images.to(device)
        
        # sample t uniformally for every example in the batch
        t = torch.randint(0, timesteps, (batch_size,), device=device).long()
        
        loss = gaussian_diffusion.train_losses(model, images, t)
        
        if step % 200 == 0:
            print("Loss:", loss.item())
            
        loss.backward()
        optimizer.step()
这里我们以mnist数据简单实现了一个mnist-demo,下面是一些生成的样本:

对生成过程进行采样,如下所示展示了如何从一个随机噪音生层一个手写字体图像:

另外这里也提供了CIFAR10数据集的demo:ddpm_cifar10,不过只训练了200epochs,生成的图像只是初见成效。

小结

相比VAE和GAN,扩散模型的理论更复杂一些,不过其优化目标和具体实现却并不复杂,这其实也让人感叹:一堆复杂的数据推导,最终却得到了一个简单的结论。要深入理解扩散模型,DDPM只是起点,后面还有比较多的改进工作,比如加速采样的DDIM以及DDPM的改进版本DDPM+和DDPM++。注:本人水平有限,如有谬误,欢迎讨论交流。

参考

  • Denoising Diffusion Probabilistic Models
  • Understanding Diffusion Models: A Unified Perspective
  • https://spaces.ac.cn/archives/9119
  • https://keras.io/examples/generative/ddim/
  • What are Diffusion Models?
  • https://cvpr2022-tutorial-diffusion-models.github.io/
  • https://github.com/openai/improved-diffusion
  • https://huggingface.co/blog/annotated-diffusion
  • https://github.com/lucidrains/denoising-diffusion-pytorch
  • https://github.com/hojonathanho/diffusion

图像分割框架

Unet框架

GitHub

介绍

nnuet

https://github.com/

MIC-DKFZ/nnUNet

十项全能冠军,自动构建分割任务

虽然基于UNet的系列编解码分割网络在各类医学图像分割上取得了长足的进展,并且部分基于相关模型的应用设计已经广泛用于临床分析中。但医学影像本身的复杂性和差异性也极度影响着分割模型的泛化性和通用性,主要体现在以下几个方面:

1)各类模态的医学影像之间差异大,如研究队列的大小、图像尺寸和维度、分辨率和体素(voxel)强度等。

2)分割的语义标签的极度不平衡。相较于影像中的正常组织,病变区域一般都只占极少部分,这就造成了正常组织的体素标签与病灶组织的体素标签之间极度的类不平常。

3)不同影像数据之间的专家标注差异大,并且一些图像的标注结果会存在模棱两可的情况。

4)一些数据集在图像几何和形状等属性上差异明显,切片不对齐和各向异性的问题也非常严重。

提出一种鲁棒的基于2D UNet3D UNet的自适应框架nnUMet。作者在各种任务上拿这个框架和目前的STOA方法进行了比较,且该方法不需要手动调参。最终nnUNet得到了最高的平均dice

作者提出一种nnUNetno-new-Net)框架,基于原始的UNet(很小的修改),不去采用哪些新的结构,如相残差连接、dense连接、注意力机制等花里胡哨的东西。相反的,把重心放在:预处理(resamplingnormalization)、训练(lossoptimizer设置、数据增广)、推理(patch-based策略、test-time-augmentations集成和模型集成等)、后处理(如增强单连通域等)。并且在10种数据集上进行测试,都能够达到很好的效果,而算法不能够针对某种数据集进行人为的调整,只能自动的去适应。

 

mmsegmentation

https://github.com/open-mmlab/mmsegmentation

mmSegmentatiopenmmlab项目下开源的图像语义分割框架,目前支持pytorch,由于其拥有pipeline加速,完善的数据增强体系,完善的模型库,作为大数据语义分割训练及测试的代码框架是再好不过了。

Efficient-Segmentation-Networks

https://github.com/xiaoyufenfei/

Efficient-Segmentation-Networks

该项目旨在为使用 PyTorch 的实时语义分割模型提供易于使用、可修改的参考实现

 

Pytorch Medical Segmentation

https://github.com/MontaEllis/

Pytorch-Medical-Segmentation

基于PyTorch的专注于医学图像分割的开源库,其支持模型丰富,方便易用。其可算为torchio的一个实例,作者将其综合起来,包含众多经典算法,实用性比较强。

支持2D3D医学图像分割,可以修改hparam.py文件来确定是2D分割还是3D分割以及是否可以进行多分类。

支持绝大数主流分割模型,几乎提供了所有的2D3D分割的算法。

兼容几乎所有的医学数据格式(例如 nii.gz, nii, mhd, nrrd, …),修改hparam.pyfold_arch即可。

作者提供了训练和测试推断的代码,简单配置后训练和推断都仅需要一行命令。

TernausNet

论文地址:https://arxiv.org/abs/1801.05746

github地址:https://github.com/ternaus/TernausNet

数据集:Kaggle Carvana

像素级分割在计算机视觉中是一项艰巨的任务,经典的UNet网络结构在医学影像和卫星图像中非常流行。一般来说神经网权重由一些大型数据集如ImageNet进行初始化后会有更好的效果。在一些实际应用中,尤其是在医学和交通安全方面,模型的精确是至关重要的,本文演示如何使用预训练编码器来改善UNet网络结构。

  1. 经典的UNet网络权重采用随机初始化方式来完成,众所周知训练一个未过拟合的网络需要大量的数据来完成。因此采用经过Imagenet训练后的权重来进行初始化这一方法被广泛应用。通过这种方式来加速学习过程。
  2. 此网络编码器部分采用VGG11(VGG11包含7个3×3卷积层,每个层后加一个ReLU,同时进行5次最大池化操作,具体如下图)

为构造编码器,这里移除了全连接层替换其为一个512通道的单卷积层来分离编码器和解码器。为构造解码器这里采用转置卷积层放大特征图尺寸并且减少一半原通道数。同时将转置卷积的输出与解码器的相应部分的输出串联。特征图的结果通过卷积操作使得其通道数与对应编码器部分相同。这一上采样过程重复5次对应5次池化操作。传统全连接层可接受任意大小图片输入,但因为此处有5个池化层,每次图像缩小到原来一半,即缩小$2^5=32$倍,因此当前网络要求输入图像大小需要能被32整除。下图为本文网络结构图。

3D U-Net

论文:3D U-Net: Learning Dense Volumetric Segmentation from Sparse Annotation

github: https://github.com/wolny/pytorch-3dunet

论文最早版本arXiv上的发表时间是2016.06,本文是论文v1版本笔记 MICCAI 2016收录

本文提出了一种从稀疏注释的立体数据中学习三维分割的网络。3D U-Net这篇论文的诞生主要是为了处理一些块状图(volumetric images),基本的原理跟U-Net其实并无大差,因为3D U-Net就是用3D卷积操作替换了2D的

3D数据对于生物医学数据分析来说显得非常冗余

  • 在三维层面上标注分割label比较困难,因为电脑屏幕上只能展示2D的切片
  • 同时,逐层标注大量的切片又很繁琐,且相邻层的信息几乎是相同的
  • 因此,完整注释3D数据并不是创建大而丰富的训练数据集的有效方法,尤其是对于需要大量标签数据的学习类算法

生物医学影像(biomedical images)很多时候都是块状的,也就是说是由很多个切片构成一整张图的存在。如果是用2D的图像处理模型去处理3D本身不是不可以,但是会存在一个问题,就是不得不将生物医学影像的图片一个slice一个slice成组的(包含训练数据和标注好的数据)的送进去设计的模型进行训练,在这种情况下会存在一个效率问题,因而很多时候处理块状图的时候会让任感到不适,并且数据预处理的方式也相对比较繁琐(tedious)。

所以,论文的作者就提出来了3D -Net模型,模型不仅解决了效率的问题,并且对于块状图的切割只要求数据中部分切片被标注即可(可参考下图说明)。

模型结构(Network Architecture)

整个3D U-Net的模型是基于之前U-Net(2D)创建而来,同样包含了一个encoder部分和一个decoder部分,encoder部分是用来分析整张图片并且进行特征提取与分析,而与之相对应的decoder部分是生成一张分割好的块状图。论文中使用的输入图像的大小是132 * 132 * 116,整个网络的结构前半部分(analysis path)包含及使用如下卷积操作:

a. 每一层神经网络都包含了两个 3 * 3 * 3的卷积(convolution)

b. Batch Normalization(为了让网络能更好的收敛convergence)

c. ReLU

d. Downsampling:2 * 2 * 2的max_polling,步长stride = 2

而与之相对应的合成路径(synthesis path)则执行下面的操作:

a. upconvolution: 2 * 2 * 2,步长=2

b. 两个正常的卷积操作:3 * 3 * 3

c. Batch Normalization

d. ReLU

于此同时,需要把在analysis path上相对应的网络层的结果作为decoder的部分输入,这样子做的原因跟U-Net博文提到的一样,是为了能采集到特征分析中保留下来的高像素特征信息,以便图像可以更好的合成。

整体的一个网络结构如下图所示,其实可以看出来跟2D结构的U-Net是基本一样,唯一不同的就是全部2D操作换成了3D,这样子做了之后,对于volumetric image就不需要单独输入每个切片进行训练,而是可以采取图片整张作为输入到模型中(PS:但是当图像太大的时候,此时需要运用random crop的技巧将图片随机裁切成固定大小模块的图片放入搭建的模型进行训练,当然这是后话,之后将会在其他文章中进行介绍)。除此之外,论文中提到的一个亮点就是,3D U-Net使用了weighted softmax loss function将未标记的像素点设置为0以至于可以让网络可以更多地仅仅学习标注到的像素点,从而达到普适性地特点。

训练细节(Training)

3D U-Net同样采用了数据增强(data augmentation)地手段,主要由rotation、scaling和将图像设置为gray,于此同时在训练数据上和真实标注的数据上运用平滑的密集变形场(smooth dense deformation field),主要是通过从一个正态分布的随机向量样本中选取标准偏差为4的网格,在每个方向上具有32个体素的间距,然后应用B样条插值(B-Spline Interpolation,不知道什么是B样条插值法的可以点连接进行查看,在深度学习模型的创建中有时候也不需要那么复杂,所以这里仅限了解,除非本身数学底子很好已经有所了解),B样条插值法比较笼统地说法就是在原本地形状上找到一个类似地形状来近似(approximation)。之后就对数据开始进行训练,训练采用的是加权交叉熵损失(weighted cross-entropy loss function)以至于减少背景的权重并增加标注到的图像数据部分的权重以达到平衡的影响小管和背景体素上的损失。

实验的结果是用IoU(intersection over union)进行衡量的,即比较生成图像与真实被标注部分的重叠部分。

论文针对肾脏的生物医学影像的分割结果达到了IoU=86.3%的结果。3D U-Net的诞生在医学影像分割,特别是那些volumetric images都是由很大帮助的,因为它很大程度上解决了3D图像一个个slice送入模型进行训练的尴尬局面,也大幅度的提升训练效率,并且保留了FCN和U-Net本来具备的优秀特征。

U2-Net

论文: U2-Net: Going Deeper with Nested U-Structure for Salient Object Detection

CVPR2020

github: https://github.com/xuebinqin/U-2-Net

U²-Net给我们带来了什么?

得益于在SOTA SOD方法取得了不错的竞争力,U²-Net可以应用在很多场景。首先,U²-Net现在已经是Python的抠图工具Rembg的基础算法。抠图就是将照片的主体人或物品从图片中抠出来,以便贴到别处使用除了被用来作为抠图外,素描肖像生成(Portrait Drawing)也是其非常有趣且流行的新应用。

显着物体检测(SOD)

显着物体检测(SOD)是基于视觉注意机制的任务,其中算法旨在探索比场景或图像周围区域更专注的物体或区域,因此很适合于做抠图应用。

从自然场景中检测和分割最具视觉吸引力的对象的过程,在计算机视觉领域称为显着对象检测(SOD)。现有的大多数SOD网络都具有类似的设计,并且专注于利用由AlexNet,VGG,ResNet,ResNeXt,DenseNet等骨干网络提取的深度特征。这些骨干网络最初是为图像分类任务而构建的,因此它们提取特征代表语义含义,而不是对显着物体检测至关重要的局部细节或全局参考信息。这样的网络还倾向于在ImageNet上进行数据效率低下的预训练。

U²-Net是一种简单而强大的深度网络体系结构,具有新颖的两层嵌套U形结构,旨在解决这些问题。提出的ReSidual U (RSU)具有各种不同大小的感受野,从而使其能够更好地捕获不同规模的上下文信息。 RSU还使用池化操作来增加总体体系结构深度,而不会显着增加计算成本。

Architecture

RSU

RSU具有三个主要组成部分:输入卷积层,类似U-Net的对称编码器-解码器结构,以及通过求和融合局部和多尺度特征的残差连接。

RSU和原始残差块之间的主要区别在于,RSU用类似U-Net的结构,替换了普通的单流卷积,并用通过加权层转换的局部特征替换了原始特征。

U-2-Net

在RSU的基础上开发了U²-Net,这是用于显着物体检测的新型堆叠U形结构。 U²-Net包括一个6级编码器,一个5级解码器和一个显着度图融合模块,该模块连接到解码器级和最后一个编码器级。

总体而言,U²-Net网络结构具有丰富的多尺度特征,以及较低的计算和内存成本。 另外,由于U²-Net体系结构仅建立在RSU块上,并且不使用任何经过预训练的骨干网络进行图像分类处理,因此可以灵活,轻松地适应不同的工作环境,而性能损失最小。

Dual Attention Network for Scene Segmentation

论文地址 : https://arxiv.org/abs/1809.02983

github:https://github.com/junfu1115/DANetCVPR2019)

 为了有效地完成场景分割的任务,我们需要区分一些混淆的类别,并考虑不同外观的对象。例如,草原与牧场有时候是很难区分的,公路上的车也存在尺度、视角、遮挡与亮度等的变化。因此,像素级识别需要提高特征表示的识别能力。

创新点:

通过基于Self Attention mechanism来捕获上下文依赖,并提出了Dual Attention Networks (DANet)来自适应地整合局部特征和全局依赖。该方法能够自适应地聚合长期上下文信息,从而提高了场景分割的特征表示。

  • 提出了Dual Attention Networks (DANet)在spatial和channle维度来捕获全局特征依赖。
  • 提出position attention module去学习空间特征的相关性,提出channel attention module去建模channle的相关性。

在一贯的dilated FCN中加入两种类型地attention module。其中position attention module选择性地通过所有位置的加权求和聚集每个位置的特征,channel attention module通过所有channle的feature map中的特征选择性地强调某个特征图。最后将两种attention module的output 求和得到最后的特征表达。

采用移除down-sampling的dilated ResNet(与DeepLab相同)的预训练网络基础网络为,最后得到的feature map大小为输入图像的1/8。之后是两个并行的attention module分别捕获spatial和channel的依赖性,最后整合两个attention module的输出得到更好的特征表达。

Position Attention Module

捕获特征图的任意两个位置之间的空间依赖,对于某个特定的特征,被所有位置上的特征加权和更新。权重为相应的两个位置之间的特征相似性。因此,任何两个现有相似特征的位置可以相互贡献提升,而不管它们之间的距离。

  • 特征图A(C×H×W)首先分别通过3个卷积层(BN和ReLU)得到3个特征图{B,C,D}.shape∈(CxHxW),然后reshape为C×N,其中N=H×W。
  • 矩阵C和B的转置相乘,再通过softmax得到spatial attention map S(N×N)。
  • 矩阵D和S的转置相乘,reshape result到(CxHxW)再乘以尺度系数 α 再reshape为原来形状,,最后与A相加得到最后的输出E 其中α初始化为0,并逐渐的学习分配到更大的权重。可以看出E的每个位置的值是原始特征每个位置的加权求和得到的。

 Channel Attention Module

每个高层次特征的通道映射都可以看作是一个特定于类的响应,不同的语义响应相互关联。通过探索通道映射之间的相互依赖关系,可以强调相互依赖的特征映射,提高特定语义的特征表示。

模型压缩

最近在做的yolo网络硬件加速项目,需要去对原始网络进行压缩,因此记录下相关知识:

相关综述:

A Survey of Model Compression and Acceleration for Deep Neural Networks

《A Comprehensive Survey on Model Compression and Acceleration》

目前,在模型压缩和加速方面常用的方法大致可以分为四类:剪枝与量化(parameter pruning and quantization)、低秩因子分解(low-rank factorization)、迁移/压缩卷积滤波器(transferred/compact convolutional filters)、蒸馏学习(knowledge distillation)。

模型压缩方法

背景

近年来,深度神经网络(deep neural networks,DNN)逐渐受到各行各业的关注。它是指具有更深层(不止一个隐藏层)的神经网络,是深度学习的基础。很多实际的工作通常依赖于数百万甚至数十亿个参数的深度网络,这样复杂的大规模模型通常对计算机的CPU和GPU有着极高的要求,并且会消耗大量内存,产生巨大的计算成本。随着一些便携式设备(如移动电话)的快速发展,如何将这些复杂的计算系统部署到资源有限的设备上就成为了需要应对的全新挑战。这些设备通常内存有限,而且计算能力较低,不支持大模型的在线计算。因此需要对模型进行压缩和加速,以求在基本不损失模型精度的条件下,节约参数并降低其计算时间。

剪枝与量化主要针对模型中的冗余参数进行删减;低秩因子分解使用张量分解的方法来估计神经网络的参数;迁移/压缩卷积滤波器则是设计了一个特殊结构的卷积滤波器,能够减少参数空间并且节约内存;蒸馏学习是先训练一个较大的模型,再训练一个较小的神经网络以达到跟大模型同样的效果。其中,低秩因子分解和迁移/压缩卷积滤波器两种方法提供了端到端的管道,可以在CPU/GPU环境中轻松实现;而剪枝与量化使用二进制及稀疏约束等方法来实现目标。此外,剪枝与量化和低秩因子分解方法可以从预训练的模型中提取或者是从头开始训练,而另外两种方法仅支持从头开始的训练。这四种方法大多是独立设计的,但又相互补充,在实际应用中常常可以一起使用,实现对模型进一步的压缩或加速。接下来将分别对这四种方法进行介绍。

剪枝与量化(parameter pruning and quantization)

早期的研究表明,对构建的网络进行剪枝和量化在降低网络复杂性以及解决过拟合问题方面是有效的(Gong et al. 2014)。同剪枝与量化有关的方法可以进一步分为三个子类:量化与二值化(quantization and binarization)、网络剪枝(network pruning)、结构矩阵(structural matrix)。

1.量化与二值化(quantization and binarization)

在DNN中,权重通常是以32位浮点数的形式(即32-bit)进行存储,量化法则是通过减少表示每个权重需要的比特数(the number of bits)来压缩原始网络。此时权重可以量化为16-bit、8-bit、4-bit甚至是1-bit(这是量化的一种特殊情况,权重仅用二进制表示,称为权重二值化)。8-bit的参数量化已经可以在损失小部分准确率的同时实现大幅度加速(Vanhoucke et al. 2011)。图2展示了基于修剪、量化和编码三个过程的压缩法:首先修剪小权重的连接,然后使用权重共享来量化权重,最后将哈夫曼编码应用于量化后的权重和码本上。

此方法的缺点是,在处理大型CNN(如GoogleNet)时,二值网络的精度明显降低。此外,现有的二值化方法大多基于简单的矩阵近似,忽略了二值化对精度损失产生的影响。

2.网络剪枝(network pruning)

剪枝是指通过修剪影响较小的连接来显著减少DNN模型的存储和计算成本,目前比较主流的剪枝方法主要有以下几种:

  • 权重剪枝(weight pruning):此方法主要应用于对不重要的连接权重进行修剪。如果连接权重低于预先设定的某个阈值,则该连接权重将会被修剪(Han et al. 2015)。
  • 神经元剪枝(neuron pruning):此方法与逐个修剪权重的方法不同,它直接移除某个冗余的神经元。这样一来,该神经元的所有传入和传出连接也将被移除(Srinivas and Babu 2015)。
  • 卷积核剪枝(filter pruning):此方法依据卷积核的重要程度将其进行排序,并从网络中修剪最不重要/排名最低的卷积核。卷积核的重要程度可以通过或范数或一些其他方法计算(Li et al. 2016)。
  • 层剪枝(layer pruning):此方法主要应用于一些非常深度的网络,可以直接修剪其中的某些层(Chen and Zhao 2018)。

按照剪枝的对象分类,可以分为在全连接层上剪枝和在卷积层上剪枝两种。DNN中的全连接层是存储密集的,对全连接层中的参数进行剪枝能够显著降低存储成本。对于卷积层而言,每个卷积层中都有许多卷积核,从卷积层修剪不重要的卷积核也能够减少计算成本并加速模型。

在全连接层上剪枝:考虑一个输入层、隐藏层和输出层分别具有3、2和1个神经元的前馈神经网络,如图3所示。

其中, x1​、x2​、x3​ 是网络的输入, wijl​ 是从当前层中节点 i 的层 l 到下一层中的节点 j 的权重。从图3(a)可以清楚地看出,目前总共有8个连接权重,如果删除两个橙色(虚线)的连接,那么总连接权重将减少到6个。类似地,从图3(b)中,如果移除红色神经元,那么其所有相关的连接权重(虚线)也将被移除,导致总连接权重减少到4个(参数数量减少50%)。

  • 在卷积层上剪枝: 在卷积神经网络中, 卷积核 WRh×w×ic×f 应用于每个输入的图像 I,IRm×n×ic, 并且经过卷积操作后输出特征映射 T,TRp×q×f 。其中, h 和 w 是卷积核的尺寸, ic 是输入图像中输入通道的数量, f 是应用的卷积核 的数量, m 和 n 是输入图像的尺寸, p 和 q 是结果特征映射的输出尺寸。输出特征映射的形状计算如下:

其中, s 为步长 (stride), p 为填充(padding)。图4显示了最简单的CNN形式,其 中输入图像的大小为 4×4×3, 应用的卷积核大小为 3×3×3×2 (2是卷积核的数 量)。

受到早期剪枝方法和神经网络过度参数化问题的启发,Han et al.(2015)提出了三步法来进行剪枝。其思想是,首先修剪激活小于某个预定义阈值的所有连接权重(不重要的连接),随后再识别那些重要的连接权重。最后,为了补偿由于修剪连接权重而导致的精度损失,再次微调/重新训练剪枝模型。这样的剪枝和再训练过程将重复数次,以减小模型的大小,将密集网络转换为稀疏网络。这种方法能够对全连接层和卷积层进行修剪,而且卷积层比全连接层对修剪更加敏感。

从卷积层修剪一些不重要的卷积核能够直接减少计算成本并且加速模型。但是,使用网络剪枝方法同样存在着一些问题。首先,使用或正则化进行剪枝比常规方法需要更多的迭代次数才能收敛。其次,所有的剪枝都需要手动设置神经网络层的灵敏度,这需要对参数进行微调,在某些应用中可能会十分复杂。最后,网络剪枝虽然通常能够使大模型变小,但是却不能够提高训练的效率。

3.结构矩阵(structural matrix)

神经网络各层之间使用的是非线性变换 f(x,N)=σ(Mx), 这里的 σ(⋅) 是对每个 元素特异的非线性算子, x 是输入向量, M 代表 m×n 维的参数矩阵, 此时的运算复 杂度为 O(mn) (V. Sindhwani et al. 2015) 。一个直观的剪枝方法就是使用参数化的 结构矩阵。一个大小为 m×n, 但是参数量却小于 mn 的矩阵就叫做结构矩阵。Cheng et al. ( 2015 ) 提 出了一种 基于循环预测的简单方法, 对于一个向量 r=(r0​,r1​,⋯,r(d−1)​), 其对应的 d×d 维循环矩阵定义如下:

这样一来存储的成本就从O (d2) 变成了O (d) 。给定 d 维 r 向量的条件下, 上式中的 一层循环神经网络的时间复杂度为 O(dlogd) 。

结构矩阵不仅能够降低内存成本,而且能够通过矩阵向量和梯度计算大幅度加快训练的速度。但是这种方法的缺点在于,结构约束通常会给模型带来偏差,从而损害模型的性能。再者,如何找到合适的结构矩阵也是一个难题,目前还没有理论上的方法能够推导出结构矩阵。

低秩因子分解(low-rank factorization)

低秩分解的思想是, 如果原始权重矩阵具有维数 m×n 和秩 r, 则满秩矩阵可以分 解为一个 m×r 的权重矩阵和一个 r×n 的权重矩阵。该方法通过将大矩阵分解为小矩 阵, 以减小模型的尺寸。CNN通常由许多层组成, 每层都有一组权重矩阵, 这些权重可以用张量 (Tensor) 来表示。图5展示了一个维数为 X×Y×Z 的三维张量。

给定一个维数为 N×N×D, 且有 K 个卷积核的卷积层, 其权重矩阵 W 可以表示为一个 N×N×D×K 维的张量 (Granés and Santamaria 2017) 。对于全连接层而言, W 可以用矩阵 (2阶张量) 来表示。因此对权重矩阵进行分解就是对张量进行分解。张量分解指的是, 用标量 (O阶张量) 、向量 (1阶张量) 、矩阵 (2阶张量) 和一些其他高阶的张量来表示原始张量的方法。对矩阵可以应用满秩分解 (full-rank decomposition) 和奇异值分解 (singular value decomposition, SVD), 对三维及三维以上张量可以应用 Tucker 分解和 CP分解 (Canonical Polyadic) (Deng et al.2020) 。

1.对矩阵的分解

  • 满秩分解。对任何给定的矩阵 AR(m×n), 其秩 rmin(m,n), 则 A 的满秩分解可以表示为 A=WH, 其中 WR(m×r),HR(r×n) 。如果 r 远小于 m 或 n,我们称 A 为低秩矩阵 (low-rank matrix) 。通过满秩分解可以将空间复杂度从O(mn) 显著减小到 O(r(m+n)) 。特别地, 当 m 和 n 非常接近, 并且原始矩阵是行(或列) 满秩时, 这种减小空间复杂度的作用会失效。满秩分解方法对于全连接层十分有效, 特别是当两层之间的神经元数量相差很大或权重矩阵低秩稀疏时。给定一个较小的正整数 k<r, 可以通过如下的式子求解最优的 WR(m×k),HR(k×n), 其中, F 表示Frobenius范数。
  • SVD。SVD是一种将原始权重矩阵分解为三个较小的矩阵以替换原始权重矩阵的 方法。对于任意的矩阵 AR(m×n), 存在分解 A=USVT, 其中, UR(m×r), SR(r×r),VTR(r×n) 。 U 和 V 是正交矩阵, S 是对角线上只有奇异值的对角矩 阵, 其中的每一个元素都比其下一个对角线上的元素大。这种方法可以使空间复 杂度从 O(mn) 减小到 O(r(m+n+1)) 。实际应用中, 可以用更小的 k 替换 r, 这 种方法称为截断奇异值分解 (truncated SVD, TSVD) 。在前馈神经网络和卷积神 经网络中, SVD是一种常用的分解方法, 主要用于减少参数的个数。

2.对三维及三维以上张量的分解

  • Tucker分解。该方法是将TSVD方法中的对角矩阵扩展为张量的一种方法。TSVD和Tucker分解之间的关系可以用图来表示:
  • CP分解。该分解是Tucker分解的一种特殊形式。如果Tucker分解中的每个 ri​ 等于正 整数 rC​, 并且核张量 K 满足, 除了 K(x1​,x2​,…,xd​),x1​=x2​=⋯=xd​ 之外 的所有元素都是 0 , 此时Tucker分解就成为了CP分解。与Tucker分解相比, CP分解 常用于解释数据的组成成分, 而前者主要用于数据压缩。图7展示了三阶张量 xR(I×J×K) 被 R 个组成部分分解的过程, 这个过程也可以用如下的公式来表示, 其中, ar​∈RI,br​∈RJ,cr​∈RK (Marcella Astrid and Seung- and Ik Lee 2018)。

基于低秩近似的方法虽然是模型压缩和加速的前沿,然而具体实现却并非易事。因为这涉及到分解操作,需要付出高昂的计算成本。此外,当前的方法仍集中于逐层执行低秩近似,因此无法执行全局的参数压缩。但全局的参数压缩十分重要,因为不同的层包含不同的信息。最后,与原始的模型相比,因子分解需要对大量的模型进行再训练以实现收敛。

迁移/压缩卷积滤波器(transferred/compact convolutional filters)

Cohen and Welling (2016) 提出了使用卷积滤波器压缩CNN模型的想法, 并在 研究中引入了等变群理论 (the equivariant group theory)。让 x 作为输入, Φ(⋅) 作为 一个神经网络或者网络层, Γ(⋅) 作为迁移矩阵, 则等价的概念定义如下:Γ′(Φ(x))=Φ(Γ(x))

这样的定义指的是, 迁移矩阵 Γ(⋅) 先对输入x进行变换, 再将其传输到 Φ(⋅) 所得到 的结果应该跟先将输入 x 映射到神经网络 Φ(⋅) 上再做变换 Γ(⋅) 得到的结果相同。值得注 意的是, Γ(⋅) 和 Γ′(⋅) 不一定相同, 因为它们作用在不同的对象上。根据这样的理论, 通过将变换应用于层或者滤波器 Φ(⋅) 来压缩整个网络模型就十分合理。从经验来看, 使用一组大的卷积滤波器也对深层CNN有益, 具体方法是将一些变换 Γ(⋅) 应用于一组 充当模型正则化器的小型基滤波器上。

沿着这一研究方向, 近期的许多研究提出了从一组基滤波器出发构建卷积层的思 想。它们的共同点是, 迁移矩阵 Γ(⋅) 是只在卷积滤波器的空间域中操作的一类函数。 例如, Shang et al. (2016) 发现, CNN的较低卷积层通过学习㐌余的滤波器来提取 输入信号的正负相位信息, 并将 Γ(⋅) 定义为简单的否定函数:

其中, Wx​ 是基础的卷积滤波器, Wx−​是由激活与 Wx​ 相反的移位 (shift) 构成的滤波 器, 并且这些移位是在最大池 (max-pooling) 操作后选择的。通过这样操作, 就可以 很容易的实现在所有卷积层上的二倍压缩率。它还表明, 否定变换作为一个强大的正 则化方法, 能够用以提高分类精度。一种直观的理解是, 具有成对正负约束的学习算 法可以产生实用而不是冗余的的卷积滤波器。此外, Zhai et al. (2016) 将 Γ(⋅) 定义为 应用于 2 维滤波器的平移函数集:Γ′Φ(x)=T(⋅,x,y)x,y∈{−k,…,k},(x,y)=(0,0)

其中, T(⋅,x,y) 表示第一个操作数沿其空间维度平移 (x,y), 并在边界处进行适当的零 填充以保持形状。提出的框架可用于公式 (1) 改善分类精度的问题, 进而作为 maxout网络的正则化版本。

对于将变换约束应用于卷积滤波器的方法,还有几个问题需要解决。首先,这些方法可以在宽/平的架构(如VGGNet,AlexNet)上实现有竞争力的性能,但是在窄/深的架构(如ResNet)上则不行。其次,迁移假设有时太强,无法指导学习过程,导致得到的结果在某些情况下不稳定。此外,使用紧凑的卷积滤波器虽然可以直接降低计算成本,但关键思想是要用紧凑的块替换松散的和过度参数化的滤波器以提高计算速度。

蒸馏学习(knowledge distillation)

蒸馏学习(knowledge distillation,KD)是指通过构建一个轻量化的小模型,利用性能更好的大模型的监督信息,来训练这个小模型,以期达到更好的性能和精度。KD与迁移学习(transfer learning)不同,在迁移学习中,我们使用相同的模型体系结构和学习的权重,仅根据应用的要求使用新层来替换部分全连接层。而在KD中,通过在大数据集上训练的更大的复杂网络(也称之为教师模型(teacher model))学习到的知识可以迁移到一个更小、更轻的网络上(也称之为学生模型(student model))。前一个大模型可以是单个的大模型,也可以是独立训练模型的集合。KD方法的主要思想是通过softmax函数学习课堂分布输出,将知识从大型教师模型转换为一个更小的学生模型。从教师模型训练学生模型的主要目的是学习教师模型的泛化能力。

在现有的KD方法中,学生模型的学习依赖于教师模型,是一个两阶段的过程。Lan et al.(2018)提出了实时本地集成(On-the-fly Native Ensemble,ONE),这是一种高效的单阶段在线蒸馏学习方法。在训练期间,ONE添加辅助分支以创建目标网络的多分支变体,然后从所有分支中创建本地集成教师模型。对于相同的目标标签约束,可以同时学习学生和每个分支。每个分支使用两个损失项进行训练,其中最常用的就是最大交叉熵损失(softmax cross-entropy loss)和蒸馏损失(distillation loss)。

在网络压缩这一步,可以使用深度神经网络方法来解决这个问题。Romero et al.(2015)提出了一种训练薄而深的网络的方法,称为FitNets,用以压缩宽且相对较浅(但实际上仍然很深)的网络。该方法扩展了原来的思想,允许得到更薄、更深的学生模型。为了学习教师网络的中间表示,FitNet让学生模仿老师的完全特征图。然而,这样的假设太过于严格,因为教师和学生的能力可能会有很大的差别。

基于蒸馏学习的方法可以使模型的深度变浅,并且能够显著降低计算成本。然而,这个方法也存在一些弊端。其中之一是KD方法只能应用于具有softmax损失函数的任务中。再者就是,与其他类型的方法相比,基于蒸馏学习的方法往往具有较差的竞争性能。

面临的问题

在文章的最后一部分,作者总结了现有的这些模型压缩和加速的方法仍然面临的一些问题与挑战,主要有以下几个方面:

  1. 当前的大多数先进方法建立在精心设计的CNN模型之上,这些模型限制了更改配置的自由度(例如,网络架构、超参数等)。为了处理更复杂的任务,未来应该提供更加合理的方法来配置压缩模型。
  2. 各种小型平台(例如移动设备、机器人、自动驾驶汽车等)的硬件限制仍然是阻碍深层CNN扩展的主要问题。如何充分利用有限的计算资源以及如何为这些平台设计特殊的压缩方法仍然是需要解决的问题。
  3. 剪枝是压缩和加速CNN的有效方法。目前的剪枝技术大多是为了修剪神经元之间的连接而设计的。此外,对通道进行剪枝能够直接减少特征映射的宽度并压缩模型。这种方法虽然很有效,但是修剪通道可能会显著地改变下一层的输入,因此也存在挑战性。
  4. 如前所述,结构矩阵和迁移卷积滤波器的方法必须使模型具有人类的先验知识,这将会显著影响模型的性能和稳定性。研究如何控制强加这些先验知识带来的影响至关重要。
  5. 蒸馏学习的方法具有很多的优点,比如无需特定的硬件就能够直接加速模型。开发基于KD的更多方法并且探索如何提高其性能是未来主要的发展方向。
  6. 尽管这些压缩方法取得了巨大的成就,但是黑箱机制(black box mechanism)仍然是其应用的关键障碍。比如,某些神经元/连接被修剪的原因尚不清楚。探索这些方法的解释能力仍然是一个重大挑战。

半监督学习综述

半监督学习(Semi-Supervised Learning,SSL) 使用标记和未标记的数据来执行有监督的学习或无监督的学习任务。

半监督学习可进一步划分为纯(pure)半监督学习直推学习(transductive learning)。前者假定训练数据中的未标记样本并非待预测的数据,而后者则假定学习过程中所考虑的未标记样本恰是待预测数据。纯半监督学习是基于“开放世界”假设,希望学得模型能适用于训练过程中未观察到的数据,而直推学习是基于“封闭世界”假设,仅试图对学习过程中观察到的未标记数据进行预测。下图直观的表现出主动学习纯半监督学习直推学习的区别:

虽然训练数据中含有大量无标签数据,但其实在很多半监督学习算法中用的训练数据还有挺多要求的,一般默认的有:无标签数据一般是有标签数据中的某一个类别的(不要不属于的,也不要属于多个类别的);有标签数据的标签应该都是对的;无标签数据一般是类别平衡的(即每一类的样本数差不多);无标签数据的分布应该和有标签的相同或类似等等。

一般,半监督学习算法可分为:self-training(自训练算法)、Graph-based Semi-supervised Learning(基于图的半监督算法)、Semi-supervised supported vector machine(半监督支持向量机,S3VM)。简单介绍如下:

1.简单自训练(simple self-training):用有标签数据训练一个分类器,然后用这个分类器对无标签数据进行分类,这样就会产生伪标签(pseudo label)或软标签(soft label),挑选你认为分类正确的无标签样本(此处应该有一个挑选准则),把选出来的无标签样本用来训练分类器。

2.协同训练(co-training):其实也是 self-training 的一种,但其思想是好的。假设每个数据可以从不同的角度(view)进行分类,不同角度可以训练出不同的分类器,然后用这些从不同角度训练出来的分类器对无标签样本进行分类,再选出认为可信的无标签样本加入训练集中。由于这些分类器从不同角度训练出来的,可以形成一种互补,而提高分类精度;就如同从不同角度可以更好地理解事物一样。

3.半监督字典学习:其实也是 self-training 的一种,先是用有标签数据作为字典,对无标签数据进行分类,挑选出你认为分类正确的无标签样本,加入字典中(此时的字典就变成了半监督字典了)

4.标签传播算法(Label Propagation Algorithm):是一种基于图的半监督算法,通过构造图结构(数据点为顶点,点之间的相似性为边)来寻找训练数据中有标签数据和无标签数据的关系。是的,只是训练数据中,这是一种直推式的半监督算法,即只对训练集中的无标签数据进行分类,这其实感觉很像一个有监督分类算法…,但其实并不是,因为其标签传播的过程,会流经无标签数据,即有些无标签数据的标签的信息,是从另一些无标签数据中流过来的,这就用到了无标签数据之间的联系

5.半监督支持向量机:监督支持向量机是利用了结构风险最小化来分类的,半监督支持向量机还用上了无标签数据的空间分布信息,即决策超平面应该与无标签数据的分布一致(应该经过无标签数据密度低的地方)(这其实是一种假设,不满足的话这种无标签数据的空间分布信息会误导决策超平面,导致性能比只用有标签数据时还差)

其实,半监督学习的方法大都建立在对数据的某种假设上,只有满足这些假设,半监督算法才能有性能的保证,这也是限制了半监督学习应用的一大障碍。

半监督深度学习

终于来到正题——半监督深度学习,深度学习需要用到大量有标签数据,即使在大数据时代,干净能用的有标签数据也是不多的,由此引发深度学习与半监督学习的结合。

如果要给半监督深度学习下个定义,大概就是,在有标签数据+无标签数据混合成的训练数据中使用的深度学习算法吧…orz.

半监督深度学习算法个人总结为三类:无标签数据预训练网络后有标签数据微调(fine-tune);有标签数据训练网络,利用从网络中得到的深度特征来做半监督算法;让网络 work in semi-supervised fashion。

1.无标签数据预训练,有标签数据微调

对于神经网络来说,一个好的初始化可以使得结果更稳定,迭代次数更少。因此如何利用无标签数据让网络有一个好的初始化就成为一个研究点了。

目前我见过的初始化方式有两种:无监督预训练,和伪有监督预训练

无监督预训练:一是用所有数据逐层重构预训练,对网络的每一层,都做重构自编码,得到参数后用有标签数据微调;二是用所有数据训练重构自编码网络,然后把自编码网络的参数,作为初始参数,用有标签数据微调。

伪有监督预训练:通过某种方式/算法(如半监督算法,聚类算法等),给无标签数据附上伪标签信息,先用这些伪标签信息来预训练网络,然后在用有标签数据来微调。(MAE: mask 编码器)

2.利用从网络得到的深度特征来做半监督算法

神经网络不是需要有标签数据吗?我给你造一些有标签数据出来!这就是第二类的思想了,相当于一种间接的 self-training 吧。一般流程是:

先用有标签数据训练网络(此时网络一般过拟合…),从该网络中提取所有数据的特征,以这些特征来用某种分类算法对无标签数据进行分类,挑选你认为分类正确的无标签数据加入到训练集,再训练网络;如此循环。

由于网络得到新的数据(挑选出来分类后的无标签数据)会更新提升,使得后续提出来的特征更好,后面对无标签数据分类就更精确,挑选后加入到训练集中又继续提升网络,感觉想法很好,但总有哪里不对…orz

个人猜测这个想法不能很好地 work 的原因可能是噪声,你挑选加入到训练无标签数据一般都带有标签噪声(就是某些无标签数据被分类错误),这种噪声会误导网络且被网络学习记忆。

3.让网络 work in semi-supervised fashion

前面的1.和2.虽然是都用了有标签数据和无标签数据,但就神经网络本身而言,其实还是运行在一种有监督的方式上。

哪能不能让深度学习真正地成为一种半监督算法呢,当然是可以啊。譬如下面这些方法:

Pseudo-Label : The Simple and Efficient Semi-Supervised Learning Method for Deep Neural Networks

这是一篇发表在 ICML 2013 的文章,是一个相当简单的让网络 work in semi-supervised fashion 的方法。就是把网络对无标签数据的预测,作为无标签数据的标签(即 Pseudo label),用来对网络进行训练,其思想就是一种简单自训练。但方法虽然简单,但是效果很好,比单纯用有标签数据有不少的提升。

网络使用的代价函数如下:

L=∑m=1n∑i=1CL(yim,fim)+α(t)∑m=1n′∑i=1CL(y′im,f′im)

代价函数的前面是有标签数据的代价,后面的无标签数据的代价,在无标签数据的代价中,y′无标签数据的 pseudo label,是直接取网络对无标签数据的预测的最大值为标签。

虽然思想简单,但是还是有些东西需要注意的,就是这个α(t),其决定着无标签数据的代价在网络更新的作用,选择合适的α(t)很重要,太大性能退化,太小提升有限。在网络初始时,网络的预测时不太准确的,因此生成的 pseudo label 的准确性也不高。在初始训练时,α(t)要设为 0,然后再慢慢增加,论文中给出其增长函数。在后面的介绍中,有两篇论文都使用了一种高斯型的爬升函数。

感觉这种无标签数据代价达到一种正则化的效果,其减少了网络在有限有标签数据下的过拟合,使得网络泛化地更好。

Semi-Supervised Learning with Ladder Networks

2015年诞生半监督 ladderNet,ladderNet是其他文章中先提出来的想法,但这篇文章使它 work in semi-supervised fashion,而且效果非常好,达到了当时的 state-of-the-art 性能。

ladderNet 是有监督算法和无监督算法的有机结合。前面提到,很多半监督深度学习算法是用无监督预训练这种方式对无标签数据进行利用的,但事实上,这种把无监督学习强加在有监督学习上的方式有缺点:两种学习的目的不一致,其实并不能很好兼容。

无监督预训练一般是用重构样本进行训练,其编码(学习特征)的目的是尽可能地保留样本的信息;而有监督学习是用于分类,希望只保留其本质特征,去除不必要的特征。

ladderNet 通过 skip connection 解决这个问题,通过在每层的编码器和解码器之间添加跳跃连接(skip connection),减轻模型较高层表示细节的压力,使得无监督学习和有监督学习能结合在一起,并在最高层添加分类器,ladderNet 就变身成一个半监督模型。

ladderNet 有机地结合了无监督学习和有监督学习,解决兼容性问题,发展出一个端对端的半监督深度模型。

PS:论文有给出代码

Temporal Ensembling for Semi-supervised Learning

Temporal ensembling 是 Pseudo label 的发展,目的是构造更好的 pseudo label(文中称为 target,我认为是一致的)。

多个独立训练的网络的集成可取得更好的预测,论文扩展了这个观点,提出自集成(self-ensembling),通过同一个模型在不同的迭代期,不同的数据增强和正则化的条件下进行集成,来构造更好的 target。

论文提出了两种不同的实现: Π model 和 temporal ensembling

两个模型的代价函数都是一样的,与 Pseudo Label 的代价函数类似,一个有监督 loss,一个无监督 loss,中间有个权系数函数,与 Pseudo Label 的区别在于,Pseudo Label 的第二项是无标签 loss,是只针对无标签数据的(如果我没理解错..orz),而 Temporal ensembling 的第二项是 无监督 loss,是面向全部数据的。

Π model 的无监督代价是对同一个输入在不同的正则和数据增强条件下的一致性。即要求在不同的条件下,模型的估计要一致,以鼓励网络学习数据内在的不变性。

缺点也是相当明显,每个迭代期要对同一个输入在不同的正则和数据增强的条件下预测两次,相对耗时。还好不同的正则可以使用 dropout 来实现,不然也很麻烦。

temporal ensembling 模型是对每一次迭代期的预测进行移动平均来构造更好的 target,然后用这个 target 来计算无监督 loss,继而更新网络。

缺点也有,记录移动平均的 target 需要较多空间。但 temporal ensembling 的潜力也更大,可以收集更多的信息,如二阶原始矩,可基于这些信息对不同的预测加权等。

Temporal ensembling 还对标签噪声具有鲁棒性,即使有标签数据的标签有误的话,无监督 loss 可以平滑这种错误标签的影响。

Mean teachers are better role models: Weight-averaged consistency targets improve semi-supervised deep learning results

Mean Teacher 这篇文章一上来就说“模型成功的关键在于 target 的质量”,一语道破天机啊。而提高 target 的质量的方法目前有两:1.精心选择样本噪声;2. 找到一个更好的 Teacher model。而论文采用了第二种方法。

Mean teacher 也是坚信“平均得就是最好的”(不知道是不是平均可以去噪的原因…orz),但是时序上的平均已经被 temporal ensembling 做了,因此 Mean teacher 提出了一个大胆的想法,我们对模型的参数进行移动平均(weight-averaged),使用这个移动平均模型参数的就是 teacher model 了,然后用 teacher model 来构造高质量 target。

一思索就觉得这想法好,对模型的参数进行平均,每次更新的网络的时候就能更新 teacher model,就能得到 target,不用像 temporal ensembling 那样等一个迭代期这么久,这对 online model 是致命的。

知识蒸馏(KD)综述

https://cloud.tencent.com/developer/article/1763873

https://www.cvmart.net/community/detail/5865

知识蒸馏总的思路:通过采用与训练好的复杂模型(teacher model)的输出作为监督信号,同label标签一起去做监督训练,训练一个简单的模型(student model)

摘要

近年来,深度神经网络在工业界和学术界都取得了成功,尤其是在计算机视觉任务方面。深度学习的巨大成功主要归因于其可扩展性以编码大规模数据并操纵数十亿个模型参数。但是,将这些繁琐的深度模型部署在资源有限的设备(例如,移动电话和嵌入式设备)上是一个挑战,这不仅是因为计算复杂性高,而且还有庞大的存储需求。为此,已经开发了多种模型压缩和加速技术。作为模型压缩和加速的代表类型,知识蒸馏有效地从大型教师模型中学习小型学生模型。它已迅速受到业界的关注。本文从知识类别,训练框架,师生架构,蒸馏算法,性能比较和应用的角度对知识蒸馏进行了全面的调查。此外,简要概述了知识蒸馏中的挑战,并讨论和转发了对未来研究的评论。

知识蒸馏简介

知识蒸馏,已经受到业界越来越多的关注。大型深度模型在实践中往往会获得良好的性能,因为当考虑新数据时,过度参数化会提高泛化性能。在知识蒸馏中,小模型(学生模型)通常是由一个大模型(教师模型)监督,算法的关键问题是如何从老师模型转换的知识传授给学生模型。一个知识蒸馏系统由三个主要部分组成:知识,蒸馏算法,和师生架构

知识蒸馏框架

用于模型压缩的知识蒸馏类似于人类学习的方式。受此启发,最近的知识蒸馏方法已扩展到师生学习,相互学习,辅助教学,终身学习和自学。知识蒸馏的大多数扩展都集中在压缩深度神经网络上。由此产生的轻量级学生网络可以轻松部署在视觉识别,语音识别和自然语言处理(NLP)等应用程序中。此外,知识蒸馏中的知识从一种模型到另一种模型的转移可以扩展到其他任务,例如对抗攻击,数据增强,数据隐私和安全性。通过知识蒸馏的动机进行模型压缩,知识转移的思想已被进一步用于压缩训练数据,即数据集蒸馏,这将知识从大型数据集转移到小型数据集以减轻深度模型的训练负担

早期知识蒸馏框架通常包含一个或多个大型的预训练教师模型和小型的学生模型。教师模型通常比学生模型大得多。主要思想是在教师模型的指导下训练高效的学生模型以获得相当的准确性。来自教师模型的监督信号(通常称为教师模型学到的“知识”)可以帮助学生模型模仿教师模型的行为。

在典型的图像分类任务中,logit(例如深层神经网络中最后一层的输出)被用作教师模型中知识的载体,而训练数据样本未明确提供该模型。例如,猫的图像被错误地归类为狗的可能性非常低,但是这种错误的可能性仍然比将猫误认为汽车的可能性高很多倍。另一个示例是,手写数字2的图像与数字3相比,与数字7更相似。这种由教师模型学习的知识也称为暗知识(“dark knowledge”)

早期的知识蒸馏中转移 dark knowledge 的方法如下。给定对数向量 z作为深度模型的最后一个全连接层的输出,则zi是第 i 类的对数,则输入属于第 i 类的概率 pi可以为 由softmax 函数估算:

因此,通过教师模型获得的软目标的预测包含暗知识,并且可以用作监督者,以将知识从教师模型转移到学生模型。同样,one-hot 标签也称为硬目标。关于软目标和硬目标的直观示例如图3所示。此外,引入温度因子T来控制每个软目标的重要性

较高的温度会在各个类别上产生较弱的概率分布。具体来说,当 T→∞时,所有类别都具有相同的概率。当 T→0时,软目标变为 one-hot 标记,即硬目标。教师模型提供的软目标(distillation loss)和ground-truth label提供的硬目标(student loss)对于提高学生模型的绩效都非常重要。

定义蒸馏损失以匹配教师模型和学生模型之间的 logits ,即:

其中 zt和 zs分别是教师和学生模型的logits。教师模型的logits通过交叉熵梯度与学生模型的 logits 匹配, 然后可以将相对于 logit zsi的梯度评估为:

如果温度 T 比 logits 高得多,

则可以根据其泰勒级数近似得出:

如果进一步假设每个转移训练样本的 logits 为零 (比如

则上式可以简化为:

因此,根据上式,在高温和零均值 logits 的情况下,蒸馏损失等于匹配教师模型和学生模型之间的 logit ,即最小化:(zsi−zti)

因此,通过与高温匹配的 logit 进行蒸馏可以传达非常有用的知识信息,这些信息是由教师模型学到的以训练学生模型。

学生损失(student loss)定义为 ground truth 标签和学生模型的软对数之间的交叉熵:

代表交叉熵损失,y 是一个 ground truth 向量,其中只有一个元素为1,它表示转移训练样本的 ground truth 标签,其他元素为0。在蒸馏和学生损失中,两者均使用学生模型的相同 logit,但温度不同。温度在学生损失中为T = 1,在蒸馏损失中为T = t。最后,传统知识蒸馏的基准模型是蒸馏和学生损失的结合:

其中 x 是转移集上的训练输入,W是学生模型的参数,并且是调节参数。为了轻松理解知识蒸馏,下图显示了传统知识蒸馏与教师和学生模型联合的特定体系结构。在下图所示的知识蒸馏中,始终首先对教师模型进行预训练,然后再进行训练。仅使用来自预训练教师模型的软目标的知识来训练学生模型。实际上,这就是离线知识提炼与基于响应的知识。

he specific architecture of the benchmark knowledge distillation(Hinton et al., 2015)

知识

知识的三种形式

Response-Based Knowledge

基于响应的知识通常是指教师模型最后输出层的神经响应。主要思想是直接模仿教师模型的最终预测。基于响应的知识蒸馏简单但有效地进行了模型压缩,已被广泛用于不同的任务和应用中。最流行的基于响应的图像分类知识被称为软目标。基于响应的知识的蒸馏损失可以表示为

其中LKL表示Kullback-Leibler(KL)散度损失。典型的基于响应的KD模型如下图所示。基于响应的知识可用于不同类型的模型预测。例如,对象检测任务中的响应可能包含logit以及边界框的偏移量。在语义地标定位任务中,例如人体姿态估计,教师模型的响应可能包括每个地标的热图。最近,基于响应的知识得到了进一步的探索,以解决将地面标签信息作为条件目标的问题。

基于响应的知识

基于响应的知识的概念是简单易懂的,尤其是在“黑暗知识(dark knowledge)”的情况下。从另一个角度看,软目标的有效性类似于标签平滑或正则化器。但是,基于响应的知识通常依赖于最后一层的输出(例如,软目标),因此无法解决教师模型在监督,这对于使用非常深层神经网络的表示学习非常重要。由于 soft logits 实际上是类概率分布,因此基于响应的知识蒸馏也仅限于监督学习。

Feature-Based Knowledge

深度神经网络擅长通过增加抽象来学习多个级别的特征表示。这就是代表性学习。因此,最后一层的输出和中间层的输出,即特征图,都可以用作监督学生模型训练的知识。具体来说,来自中间层的基于特征的知识是基于响应的知识的良好扩展,尤其是对于更薄和更深的网络的训练而言。

中间表示法首先在 Fitnets 中引入,通过提供 hints,以改善学生模型的训练。主要思想是直接匹配老师和学生的特征激活。受此启发,已经提出了多种其他方法来间接匹配特征从原始特征图中得出了一个“注意图”来表达知识。Huang和Wang(2017)使用神经元选择性转移对注意力图进行了概括。Passalis和Tefas(2018)通过匹配特征空间中的概率分布来传递知识。为了更容易地转移教师知识,Kim等人。(2018年)引入了所谓的“因素”,作为一种更易于理解的中间表示形式。为了缩小师生之间的绩效差距,Jin等人。(2019)提出了路线约束式提示学习,该方法通过教师提示层的输出来监督学生。最近,Heo等。(2019c)建议使用隐藏神经元的激活边界进行知识转移。有趣的是,教师模型中间层的参数共享以及基于响应的知识也可以被用作教师知识(Zhou et al。,2018)。

通常,基于特征的知识转移的蒸馏损失可以用公式表达为:

其中 ft(x),fs(x) 分别是教师模型和学生模型的中间层的特征图。转换函数Φt(ft(x)),Φs(fs(x)),通常在教师和学生模型的特征图不是同一形状时应用。LF(.)表示用于匹配老师和学生模型的特征图的相似度函数。一个通用的基于特征的KD模型如下图所示。

本文还从特征类型,源层和蒸馏损失的角度总结了不同类型的基于特征的知识,如下表所示。

具体地说,L2(.),L1(.),LCE(.),LMMD(.) 分别表示l2-范数距离,l1-范数距离,交叉熵损失和最大平均差异损失。尽管基于特征的知识转移为学生模型的学习提供了有利的信息,但是如何有效地从教师模型中选择提示层和从学生模型中选择引导层仍然有待进一步研究。由于 hint 层和 guided 层的大小之间存在显着差异,因此还需要探索如何正确匹配教师和学生的特征表示

Relation-Based Knowledge

基于响应的知识和基于特征的知识都使用教师模型中特定层的输出。基于关系的知识进一步探索了不同层或数据样本之间的关系

为了探索不同特征图之间的关系,Yim等人。(2017)提出了一种解决方案流程(FSP),该流程由两层之间的Gram矩阵定义。FSP 矩阵总结了特征图对之间的关系。它是使用两层要素之间的内积来计算的。利用特征图之间的相关性作为蒸馏的知识,(Lee et al。,2018)提出了通过奇异值分解的知识蒸馏来提取特征图中的关键信息。为了利用多位教师的知识,Zhang和Peng(2018)分别以每个教师模型的 logits 和特征为节点,形成了两个图。具体来说,在知识转移之前,不同的教师的重要性和关系通过 logits 和表示图进行建模(Zhang and Peng,2018)。Lee and Song(2019)提出了基于多头图的知识蒸馏。图知识是通过多头注意力网络在任意两个特征图之间的内部数据关系。为了探索成对的提示信息,学生模型还模拟了教师模型的成对的提示层之间的互信息(Passalis等,2020b)。通常,基于特征图的关系的知识的蒸馏损失可以表示为:

其中 ft和 fs分别是老师和学生模型的特征图。教师模型选取的成对特征图表达为:^ft,ˇft,学生模型选择的成对特征图表达为:^fs,ˇfs。Ψt(.)和Ψs(.)是来自教师和学生模型的成对特征图的相似性函数。LR1(.)

表示教师和学生特征图之间的相关函数。

传统的知识转移方法通常涉及个人知识的提炼。老师的软目标直接提炼给学生。实际上,提炼的知识不仅包含特征信息,还包含数据样本的相互关系。具体来说,刘等。(2019g)通过实例关系图提出了一种鲁棒而有效的知识提炼方法。实例关系图中传递的知识包含实例特征,实例关系和特征空间转换跨层。Park等。(2019)提出了一种关系知识蒸馏,该知识蒸馏了实例关系中的知识。基于流形学习的思想,通过特征嵌入来学习学生网络,这保留了教师网络中间层中样本的特征相似性(Chen等人,2020b)。使用数据的特征表示将数据样本之间的关系建模为概率分布(Passalis和Tefas,2018; Passalis等,2020a)。师生的概率分布与知识转移相匹配。(Tung and Mori,2019)提出了一种保留相似性的知识提炼方法。尤其是,将教师网络中输入对的相似激活所产生的保持相似性的知识转移到学生网络中,并保持成对相似性。Peng等。(2019a)提出了一种基于相关一致性的知识蒸馏方法,其中蒸馏的知识既包含实例级信息,又包含实例之间的相关性。使用关联一致性进行蒸馏,学生网络可以了解实例之间的关联。

典型的基于实例关系的KD模型如下图所示。

可以将提取的知识从不同的角度进行分类,例如数据的结构化知识,有关输入功能的特权信息。下表显示了基于关系的知识的不同网络类别的摘要。

尽管最近提供了一些类型的基于关系的知识,但是如何根据特征图或数据样本对关系信息进行建模(作为知识)仍然值得进一步研究

蒸馏

蒸馏的几种形式:

离线蒸馏(Offline Distillation)

大多数以前的知识蒸馏方法都可以脱机工作。在常见的知识蒸馏中,知识从预先训练的教师模型转移到学生模型。因此,整个训练过程有两个阶段,即:

  • 大型教师模型是在蒸馏之前首先在一组训练样本上训练的。
  • 教师模型用于提取logit或中间特征形式的知识,然后用于指导蒸馏过程中学生模型的训练。

离线蒸馏的第一阶段通常不作为知识蒸馏的一部分进行讨论,即,假定教师模型是预先定义的。很少关注教师模型结构及其与学生模型的关系。因此,离线方法主要集中于改进知识转移的不同部分,包括知识的设计以及用于匹配特征或分布匹配的损失函数。离线方法的主要优点在于它们简单易行。例如,教师模型可以包含使用可能位于不同机器上的不同软件包训练的一组模型。可以提取知识并将其存储在缓存中。

离线蒸馏方法通常采用单向知识转移和两阶段训练程序。然而,不可避免的是,复杂的高容量教师模型具有很长的训练时间,而离线蒸馏中对学生模型的训练通常在教师模型的指导下是有效的。此外,大型教师和小型学生之间的能力差距始终存在,而学生在很大程度上依赖于教师。

在线蒸馏(Online Distillation)

尽管离线蒸馏方法简单有效,但离线蒸馏中的一些问题已引起研究界的越来越多的关注。为了克服离线蒸馏的局限性,提出了在线蒸馏以进一步改善学生模型的性能,特别是在没有大容量高性能教师模型的情况下。在在线蒸馏中,教师模型和学生模型同时更新,并且整个知识蒸馏框架是端到端可训练的。

在最近三年中,已经提出了多种在线知识蒸馏方法。具体来说,在深度相互学习中(Zhang等人,2018b),多个神经网络以协作方式工作。在训练过程中,任何一个网络都可以作为学生模型,其他模型可以作为老师。为了提高泛化能力,通过使用 soft Logits 的集合来扩展深度相互学习(Guo等,2020)。Chen等。(2020a)进一步将辅助同伴(auxiliary peers)和小组负责人(group leader)引入深度相互学习中,以形成一套多样化的同伴模型。为了降低计算成本,Zhu和Gong(2018)提出了一种多分支架构,其中每个分支表示一个学生模型,不同分支共享相同的骨干网络。Kim等人(2019b)没有使用Logits,引入了特征融合模块来构建教师分类器。谢等。(2019)用便宜的卷积运算代替了卷积层以形成学生模型。Anil等。(2018)使用在线蒸馏来训练大规模分布式神经网络,并提出了在线蒸馏的一种变体,称为共蒸馏。并行共蒸馏以相同的架构训练多个模型,并且通过从其他模型转移知识来训练任何一个模型。最近,提出了一种在线对抗知识蒸馏方法,以利用来自类别概率和特征图的知识,同时由鉴别者训练多个网络(Chung等,2020)。

在线蒸馏是一种具有高效并行计算功能的单阶段端到端训练方案。然而,现有的在线方法(例如,相互学习)通常不能解决在线设置中的高能力教师,这使得在在线设置中进一步探索教师与学生模型之间的关系成为一个有趣的话题。

自我蒸馏(Self-Distillation)

在自我蒸馏中,教师和学生模型采用相同的网络。这可以视为在线蒸馏的特殊情况。具体来说,Zhang等。(2019b)提出了一种新的自蒸馏方法,其中将来自网络较深部分的知识蒸馏为浅层部分。与(Zhang et al。,2019b)中的自蒸馏相似,有人提出了一种自注意蒸馏方法进行车道检测(Hou et al。,2019)。该网络利用其自身层的注意力图作为其较低层的蒸馏目标。快照蒸馏(Yang et al。,2019b)是自我蒸馏的一种特殊变体,其中网络早期(教师)的知识被转移到其后期(学生)以支持在同一时期内的监督训练过程网络。为了进一步减少通过提前退出的推理时间,Phuong和Lampert(2019b)提出了基于蒸馏的训练方案,其中提前退出层尝试在训练过程中模仿后续退出层的输出。

另外,最近提出了一些有趣的自蒸馏方法。具体来说,袁等。提出了一种基于标签平滑规则化(label smoothing regularization)分析的无教师知识蒸馏方法(Yuan et al。,2020)。Hahn和Choi提出了一种新颖的自我知识蒸馏方法,其中自我知识由预测概率而不是传统的软概率组成(Hahn和Choi,2019)。这些预测的概率由训练模型的特征表示来定义。它们反映了特征嵌入空间中数据的相似性。Yun等。提出了分类自知识蒸馏,以匹配同一模型中同一来源内的类内样本和扩充样本之间的训练模型的输出分布(Yun et al。,2020)。此外,采用Lee等人(2019a)提出的自蒸馏进行数据增强,并将增强的自知性蒸馏为模型本身。还采用自我蒸馏中以一对一地优化具有相同架构的深度模型(教师或学生网络)(Furlanello等,2018; Bagherinezhad等,2018)。每个网络都使用教师优化来蒸馏先前网络的知识。

此外,还可以从人类师生学习的角度直观地了解离线,在线和自我蒸馏中。离线蒸馏是指知识渊博的老师向学生传授知识;在线蒸馏是指老师和学生互相学习;自我蒸馏是指学生自己学习知识。而且,就像人类学习一样,这三种蒸馏由于自身的优势可以结合起来互相补充。

师生架构

在知识蒸馏中,师生架构是形成知识转移的通用载体。换句话说,从老师到学生的知识获取和蒸馏的质量也取决于如何设计老师和学生的网络。在人类学习习惯方面,我们希望学生能够找到合适的老师。因此,如何在知识蒸馏中完成知识的提取和提取,如何选择或设计合适的师生结构是非常重要而又困难的问题。最近,在蒸馏过程中,教师和学生的模型设置几乎都预先设置了不变的大小和结构,从而容易造成模型容量差距。但是,几乎不存在如何特别设计教师和学生的体系结构以及为什么由这些模型设置确定其体系结构的方法。在本节中,将讨论下图所示的教师模型和学生模型的结构之间的关系。

师生架构关系

知识蒸馏以前曾被设计为压缩深度神经网络的方法之一。深度神经网络的复杂性主要来自两个维度:深度和宽度。通常需要将知识从更深和更广的神经网络转移到更浅和更薄的神经网络。学生网络通常选择为:

  • 教师网络的简化版本,每层中的层数更少且通道更少。
  • 教师网络的量化版本,其中保留了网络的结构。
  • 具有高效基本操作的小型网络。
  • 具有优化的全局网络结构的小型网络。
  • 与教师使用同一网络。

大型深层神经网络和小型学生神经网络之间的模型能力差距会降低知识转移的速度。为了有效地将知识转移到学生网络,已提出了多种方法来控制模型复杂度的可控降低。具体来说,Mirzadeh等。(2020)引入了助教来减轻教师模型和学生模型之间的训练差距。(Gao et al。,2020)通过残差学习进一步缩小了差距,即使用辅助结构来学习残差。另一方面,最近的几种方法也集中在最小化学生模型和教师模型的结构差异上。例如,Polino等。(2018)将网络量化与知识蒸馏相结合,即学生模型很小,是教师模型的量化版本。Nowak和Corso(2018)提出了一种结构压缩方法,该方法涉及将多层学习的知识转移到单层。Wang等。(2018a)逐步执行从教师网络到学生网络的块状知识转移,同时保留接受领域。在在线环境中,教师网络通常是学生网络的集合,其中学生模型彼此共享相似的结构(或相同的结构)。

最近,深度可分离卷积已被广泛用于为移动或嵌入式设备设计有效的神经网络。受神经架构搜索(或NAS)成功的启发,通过基于有效元操作或块的全局结构搜索,小型神经网络的性能得到了进一步改善。此外,动态搜索知识转移机制的想法也出现在知识蒸馏中,例如,使用强化学习以数据驱动的方式自动删除冗余层,并在给定教师网络条件下搜索最佳学生网络

以前的大多数工作都着重于设计教师和学生模型的结构或它们之间的知识转移方案。为了使小型学生模型与大型教师模型很好地匹配,以提高知识蒸馏的绩效,自适应的师生学习体系结构是必要的。最近,在知识蒸馏中进行神经体系结构搜索(NAS)的想法,即在教师模型的指导下联合搜索学生结构和知识转移,将是未来研究的一个有趣课题。

蒸馏算法

对抗蒸馏(Adversarial Distillation)

多教师蒸馏(Multi-Teacher Distillation)

跨模态蒸馏(Cross-Modal Distillation)

图蒸馏(Graph-Based Distillation)

注意力蒸馏(Attention-Based Distillation)

由于注意力可以很好地反映卷积神经网络的神经元激活,因此在知识蒸馏中使用了一些注意力机制来改善学生网络的性能。在这些基于注意力的KD方法中,定义了不同的注意力转移机制,用于从教师网络向学生蒸馏知识网络。注意转移的核心是定义用于特征嵌入神经网络各层的关注图。也就是说,使用关注图功能来传递关于特征嵌入的知识

无数据蒸馏(Data-Free Distillation)

量化蒸馏(Quantized Distillation)

网络量化通过将高精度网络(例如32位浮点)转换为低精度网络(例如2位和8位)来降低神经网络的计算复杂度。同时,知识蒸馏的目的是训练小型模型以产生与复杂模型相当的性能。目前已经有多篇文章提出了在量化过程使用教师-学生框架中的一些KD方法。量化蒸馏方法的框架如下图所示。

具体来说,Polino等。(2018)提出了一种量化蒸馏方法,将知识转移到权重量化的学生网络中。在(Mishra和Marr,2018年)中,提出的量化KD被称为“学徒”。高精度教师网络将知识转移到小型的低精度学生网络。为了确保小型学生网络准确地模仿大型教师网络,首先在特征图上对高精度教师网络进行量化,然后将知识从量化教师转移到量化学生网络(Wei等人,2018年) )。Kim等。(2019a)提出了基于量化学生网络的自学,以及基于师生网络与知识转移的共同研究的量化意识知识蒸馏。此外,Shin等。(2019)使用蒸馏和量化进行了深度神经网络的经验分析,同时考虑了知识蒸馏的超参数,例如教师网络的大小和蒸馏温度。

终身蒸馏(Lifelong Distillation)

终身学习,包括持续学习和元学习,旨在以与人类相似的方式进行学习。它积累了以前学到的知识,还将学到的知识转移到未来的学习中。知识蒸馏提供了一种有效的方法来保存和转移所学知识,而不会造成灾难性的遗忘。最近,基于终生学习的KD变体数量不断增加。

关于元学习:Jang等。(2019)设计了元转移网络,可以确定在师生架构中转移的内容和地点。Flennerhag等。(2019)提出了一个轻量级的框架,称为Leap,用于通过将知识从一种学习过程转移到另一种学习过程来对任务流形进行元学习。Peng等。(2019b)设计了一种用于少拍图像识别的新知识转移网络架构。该体系结构同时合并了来自图像和先验知识的视觉信息。刘等。(2019e)提出了一种用于图像检索的语义感知知识保存方法。从图像模态和语义信息中获得的教师知识将得到保存和转移。

此外,为了解决终身学习中的灾难性遗忘问题,全局蒸馏(Lee等人,2019b),基于知识蒸馏的终身GAN(Zhai等人,2019),多模型蒸馏(Zhou等人,2020) )和其他基于KD的方法(Li and Hoiem,2017; Shmelkov et al。,2017)已经开发出来,以提取学习到的知识并在新任务上教给学生网络。

NAS蒸馏(NAS-Based Distillation)

神经体系结构搜索(NAS)是最流行的自动机器学习(或AutoML)技术之一,旨在自动识别深度神经模型并自适应地学习适当的深度神经结构。在知识蒸馏中,知识转移的成功不仅取决于老师的知识,还取决于学生的架构。但是,大型教师模型和小型学生模型之间可能存在能力差距,从而使学生难以向老师学习。为了解决这个问题,已经有工作采用 NAS 来找到 oracle-based 和 architecture-aware 的合适的学生架构实现知识蒸馏。此外,知识蒸馏被用于提高神经架构搜索的效率,例如,具有蒸馏架构知识的 NAS(AdaNAS)以及教师指导的架构搜索(TGSA)。在TGSA中,指导每个体系结构搜索步骤以模仿教师网络的中间特征表示,通过有效搜索学生的可能结构,老师可以有效地监督特征转移。

性能对比

知识蒸馏是用于模型压缩的出色技术。通过捕获教师的知识并在教师学习中使用蒸馏策略,它可以提高轻量级学生模型的性能。近来,许多知识蒸馏方法致力于改善性能,尤其是在图像分类任务中。在本节中,为了清楚地证明知识蒸馏的有效性,总结了一些典型的KD方法在两个流行的图像分类数据集上的分类性能。

这两个数据集是 CIFAR10 和 CIFAR100,分别由分别来自 10 和 100 个类别的 32×32 RGB 图像组成。两者都具有 50000 个训练图像和 10000 个测试图像,并且每个类具有相同数量的训练和测试图像。为了公平比较,KD 方法的实验分类准确度结果(%)直接来自相应的原始论文,如 CIFAR10 的表5和 CIFAR100 的表6所示。当使用不同类型的知识,蒸馏方案和教师/学生模型的结构时,报告了不同方法的性能。具体而言,括号中的准确度是教师和学生模型的分类结果,它们是经过单独训练的。应该注意的是,DML 和 DCM 的成对精度是在线蒸馏后师生的表现。

总结和讨论

近年来,知识蒸馏及其应用引起了相当大的关注。本文从知识,蒸馏方案,师生架构,蒸馏算法,性能比较和应用的角度对知识蒸馏进行了全面综述。下面,讨论知识蒸馏的挑战,并对知识蒸馏的未来研究提供一些见识。

挑战

对于知识蒸馏,关键是:1)从教师那里提取丰富的知识;2)从教师那里转移知识以指导学生的训练。因此,本文从以下几个方面讨论知识蒸馏的挑战:知识的均等性,蒸馏的类型,师生体系结构的设计以及知识蒸馏的理论基础

大多数KD方法利用各种知识的组合,包括基于响应的知识,基于特征的知识和基于关系的知识。因此,重要的是要了解每种知识类型的影响,并知道不同种类的知识如何以互补的方式互相帮助。例如,基于响应的知识具有相似的动机来进行标签平滑和模型正则化; 基于特征的知识通常用于模仿教师的中间过程,而基于关系的知识则用于捕获不同样本之间的关系。为此,在统一和互补的框架中对不同类型的知识进行建模仍然是挑战。例如,来自不同提示层的知识可能对学生模型的训练有不同的影响:1)基于响应的知识来自最后一层;2)来自较深的提示/指导层的基于特征的知识可能会遭受过度规范化的困扰。

如何将丰富的知识从老师传授给学生是知识蒸馏的关键一步。通常,现有的蒸馏方法可分为离线蒸馏,在线蒸馏和自蒸馏。离线蒸馏通常用于从复杂的教师模型中转移知识,而教师模型和学生模型在在线蒸馏和自我蒸馏的设置中具有可比性。为了提高知识转移的效率,应进一步研究模型复杂性与现有蒸馏方案或其他新颖蒸馏方案之间的关系

目前,大多数KD方法都将重点放在新型知识或蒸馏损失函数上,而对师生体系结构的设计研究不足。实际上,除了知识和蒸馏算法之外,教师和学生的结构之间的关系也显着影响知识蒸馏的性能。例如,一方面,最近的一些研究发现,由于教师模型和学生模型之间的模型能力差距,学生模型无法从某些教师模型中学习到很多东西;另一方面,从对神经网络容量的一些早期理论分析来看,浅层网络能够学习与深层神经网络相同的表示。因此,设计有效的学生模型或构建合适的教师模型仍然是知识蒸馏中的难题。

尽管有大量的知识蒸馏方法和应用,但对知识蒸馏的理解(包括理论解释和实证评估)仍然不够。例如,蒸馏可以被视为一种获得特权信息的学习形式。线性教师模型和学生模型的假设使得能够通过蒸馏来研究学生学习特征的理论解释。此外,Cho和Hariharan(2019)对知识蒸馏的功效进行了一些实证评估和分析。但是,仍然很难获得对知识提升的可概括性的深刻理解,尤其是如何衡量知识的质量或师生架构的质量。

未来发展方向

为了提高知识蒸馏的性能,最重要的因素包括:怎样设计师生网络体系结构,从老师网络中学习什么样的知识,以及在何处提炼到学生网络中

深层神经网络的模型压缩和加速方法通常分为四个不同类别,即模型剪枝和量化,低秩分解,紧凑型卷积滤波器和知识蒸馏。在现有的知识蒸馏方法中,只有很少的相关工作讨论了知识蒸馏与其他压缩方法的结合。例如,量化知识蒸馏可以看作是一种参数修剪方法,它将网络量化整合到师生架构中。因此,为了学习用于在便携式平台上部署的高效轻巧的深度模型,由于大多数压缩技术都需要重新训练/微调过程,因此需要通过知识蒸馏和其他压缩技术进行混合压缩的方法。此外,如何决定使用不同压缩方法的正确顺序将是未来研究的有趣话题

除了用于深度神经网络加速的模型压缩之外,由于教师架构上知识转移的自然特性,知识蒸馏还可以用于其他问题。最近,知识蒸馏已应用于数据隐私和安全性,深度模型的对抗攻击,跨模态,多个域,灾难性遗忘,加速深度模型的学习,神经结构搜索的效率,自我监督和数据增强。另一个有趣的例子是,知识从小型教师网络向大型学生网络的转移可以加速学生的学习。这与传统的知识蒸馏有很大不同。大型模型从未标记的数据中学习的特征表示也可以通过蒸馏来监督目标模型。为此,将知识蒸馏扩展到其他目的和应用可能是有意义的未来方向。

知识蒸馏的学习类似于人类的学习。将知识转移推广到经典和传统的机器学习方法是可行的。例如,基于知识蒸馏的思想,传统的两阶段分类适用于单老师单学生问题。此外,知识蒸馏可以灵活地部署到各种学习方案中,例如对抗学习,自动机器学习,终身学习,和强化学习。因此,将来将知识蒸馏与其他学习方案整合起来以应对实际挑战将是有用的。

Cyclegan实现赛博朋克风格转换

偶然看到有个b站视频,是关于如何实现一个图片的赛博朋克风,通过调整色调就可以实现。然后就看到有博主使用python的opencv库实现这个效果。

原始
风格化

因此,我想使用Cyclegan生成对抗网络实现风格迁移。

cyclegan网络

先是准备数据集,我在https://wallhaven.cc/网站爬取了了大约2000张赛博朋克风的图片,因为设备条件有限,图片都是320大小的:

另外我在该网站爬取了了现实城市和风景的图片大概两千多张:

利用论文作者提供的github代码,并修改训练参数、准备数据集,进行训练:(训练的中间结果)

原始
赛博化
原始
转换

因为这个网络分辨率比较低,所以效果一般,此外,数据集也有一些问题,中间有些脏数据。有些图片并不是赛博朋克风格。