汇总:对超分辨/重建常用的网络结构&&数据处理方法
神经网络篇
CNN(但是一些特殊结构)
通道空间注意模块CSAM 与 层注意模块HAN
其实就是1格卷积核但是多通道;以及单通道的n*n卷积核。
注意力机制
这边参考参考李宏毅的免费公开课。Attention is All You Need的含金量。。
自注意力(self-attention)
考虑输入是向量序列的具体情况(多个向量且向量的数量不定)。
输入:向量组:
比如对单词的编码——(word embedding)词嵌入编码,保留了词语的语义信息关系。
再或者是图,声音讯号。
eg.对词性标注的task: I saw a saw.这句话词性标注为名词,动词,冠词,名词。对FC而言两个saw是一模一样的——所以可以单独修剪一下变成对词语每一个前后的单词输入进去来考虑上下文的关系(然而我们不知道整个sequence的长度大小,所以不好选择对整个句式的窗口连接大小)
相比之下:self-attention直接考虑整个向量序列。下面是原理:
对输入(a1,a2,a3....an)输出到(b1,b2....bm),每个 b 都是考虑了所有的 a 以后才生成出来的。
自注意力的目的是考虑整个序列,但是又不希望把整个序列所有的信息包在一个窗口里面。所以有一个特别的机制,这个机制是根据向量 a 找出整个很长的序列里面哪些部分是重要的,哪些部分跟判断 a 是哪一个标签是有关系的。每一个向量跟 a 的关联的程度可以用数值 α 来表示。
怎么计算 α?比较常见的做法是用点积。eg. 判断a1和a4是否相关,把a1和a4分别乘以Wq,Wk矩阵得到q1和k4向量,然后点积得到α1,4。这边记录为(Dot)方法。下面汇总方法列表: 1. Dot 2. Addictive
如何套用进自注意力?比较常见的做法是用(QKV)。对所有α1,i进行一个softmax得到α'1,i。最后用Wv矩阵得到vi,与对应的α'1,i相乘之后,对每一个i相加得到b1。
简化来说就是QK'V的矩阵相乘。而其中需要网络学习的参数就是Wq,Wk,Wv这三个矩阵。
import torch
import torch.nn as nn
import torch.nn.functional as F
class SelfAttention(nn.Module):
def __init__(self,seq_length):
super(SelfAttention,self).__init__()
self.input_size = seq_length
# 定义三个权重矩阵
self.Wq=nn.Linear(seq_length,seq_length)#不改变形状的线性变换
self.Wk=nn.Linear(seq_length,seq_length)
self.Wv=nn.Linear(seq_length,seq_length)
def forward(self,input):
# 计算Q,K,V 三个矩阵
q = self.Wq(input)
k = self.Wk(input)
v = self.Wv(input)
# 计算QK^T,即向量之间的相关度 ; 这里可以理解dk了:torch.tensor(float(self.input_size)),是Wk的维度。
attention_scores = torch.matmul(q, k.transpose(-1,-2))/torch.sqrt(torch.tensor(float(self.input_size)))
# 计算向量权重,softmax归一化
attention_weight = F.softmax(attention_scores, dim=-1)
# 计算输出
output = torch.matmul(attention_weight, v)
return output
多头自注意力&&截断自注意力
多头注意力简单来说就是一组QKV得到的b变成了多组(原先的qkv在分别×多个矩阵得到不同的qkv,再分别做QKV的乘法过程)。一些task比如翻译、语音识别,用比较多的头可以得到比较好的结果
截断自注意力是为了防止QKV这三个矩阵过大,所以只选取前后一定范围的信息做运算用于加快运算速度。
位置编码
虽然注意力机制衡量了全局的一个关系,但是缺少了每一个参数的位置信息。所以对一开始的输入a要增广加入一个位置的向量e。最早的Attention Is All You Need论文里面用正弦和余弦函数产生位置向量(避免人为设定向量的长度)。
有关位置编码的参考论文有:“Learning to Encode Position for Transformer with Continuous Dynamical Model”
与CNN,RNN对比
如果把公式直接套用在图像上面会发现,注意力相当于一个先全局感受的大卷积核,且自动能训练出感受野。但是卷积核对于每一个a的Query矩阵Wq是固定的,然后按空间关系分配Wk。最后相乘相加得到b。感觉就像一个人为设定的截断自注意力,而且Wk是多头设定的一个特殊空间关系分布的多头自注意力。
在数据量小的时候,这种空间分配的机制和容易训练的特点使得CNN的效果更好。反之,数据量更大时候灵活的自注意力则更加占优。
假设把循环神经网络的输出跟自注意力的输出拿来做对比,就算使用双向循环神经网络还是有一些差别的。对于循环神经网络,如果最右边黄色的向量要考虑最左边的输入,它就必须把最左边的输入存在记忆里面,才能不“忘掉”,一路带到最右边,才能够在最后一个时间点被考虑。但自注意力输出一个查询,输出一个键,只要它们匹配
(match)得起来,“天涯若比邻”。自注意力可以轻易地从整个序列上非常远的向量抽取信息。
这边贴自注意力的变形:
Efficient Transformers: A Survey
Transformer
主要应用有TTS等。。暂时挖个坑,还没看见在SR上的特殊应用
扩散模型
扩散模型这个降采样再升采样的操作就和SR有很多异曲同工之处了。
相比之下,扩散模型的一个重要工作是预测噪声。
输入 :等待去噪的图像 ,现在的噪声严重程度
处理: 预测噪声
输出: 去噪图像删减去预测的噪声得到去噪后的图像。
训练:前向过程——加噪声(一般为高斯分布)
反向过程——预测噪声
整体而言还是相当的简单粗暴。
去噪扩散概率模型(DDPM)
基本原理:
DDPM通过逐步将噪声添加到数据中,形成一系列带噪声的图像,然后学习一个去噪网络来逆向这些噪声过程,逐步还原出原始数据。
在每个时间步,数据都被加上一个高斯噪声,直到最后一步,数据完全变成纯噪声。
训练过程:
模型被训练来估计每一步中的条件概率,即如何从当前带噪声的图像生成更少噪声的图像。
训练目标是最大化整个逆向过程的对数似然估计。
生成过程:
生成新数据时,模型从纯噪声开始,通过逐步去噪的过程生成高质量的图像。
生成过程往往需要大量的步骤,通常在数百到上千步之间。
去噪扩散隐式模型(DDIM)
DDIM是对DDPM的一种改进,提供了一种确定性的生成过程,同时减少了生成步骤的数量。
DDIM利用了一个非马尔科夫(Non-Markovian)过程,从而能够在保持生成质量的同时显著减少生成步骤的数量。
PS:(马尔科夫过程):对于随机过程而言,其当前状态仅仅依赖于上一个状态,且从上一个状态以某种转移矩阵变换而来。
隐空间扩散
就是对图像先编码成一个维度相对较小的向量或者特征图,然后用这个维度较小的向量训练扩散模型
how to 编码?—— :VAE_点我跳转
此外还有一个离散的编码器VAGAN
训练目标:
对于输入x,经过编码器ζ,得到图像的压缩表征z:
损失函数:L = (ε-εθ(xt,t,y))其中y作为输入的条件,用作条件生成。当然x和y在实际输入到扩散模型时候都要编码成对应的参量,比如z和Ÿ。
网络结构:Unet
对噪声网络,采用交叉注意力机制把条件附加到网络当中。
条件生成
贴一个讲解的比较详细的
http://t.csdnimg.cn/LBQMa
Classifier Guidance是一种用于生成模型的方法,特别是在扩散模型(Diffusion Models)中,它通过引导生成过程来提高生成图像的质量和一致性。其基本思想是利用分类器(classifier)的梯度信息来指导生成模型的采样过程,从而产生更符合期望特征的图像。
以下是Classifier Guidance的基本步骤:
预训练分类器:首先训练一个分类器,使其能够对图像进行分类。这个分类器可以是任何能够输出图像类别概率的模型,例如卷积神经网络(CNN)。
生成初始样本:使用扩散模型生成初始的图像样本。这些样本可能并不完全符合期望的类别特征。
计算分类器梯度:将生成的图像输入预训练的分类器,计算分类器输出的类别概率。然后,通过反向传播计算关于图像的梯度,这个梯度表示图像如何在像素空间上变化以增加某个类别的概率。
调整图像:利用分类器梯度调整生成的图像。具体来说,通过梯度上升或下降使得图像在分类器的判断中更可能属于目标类别。这一步通过微调生成图像的像素值来实现。
重复过程:多次迭代上述过程,每次生成的新图像在分类器指导下越来越接近目标类别,从而提高生成图像的质量和一致性。
通过这种方式,Classifier Guidance可以将分类器的知识融入到生成模型中,使得生成的图像不仅在视觉上更逼真,而且更符合特定的类别特征。这种方法在图像生成、数据增强等应用中具有广泛的应用前景。
常见评估方法: FID
此外对于SD还加入了一个隐含空间,先编码降采样再对降采样的空间进行变换,最后再对得到的图片decoder。这样做的主要目的是减少计算量。这边可以参考VAE来着。
下面贴上一点demo项目的代码。
首先明确我们需要的是一个网络:一个噪声预测网络。
接受的输入是带有噪声的图片+图片对应的time_stamp,输出是预测出来的噪声。
通常是通过一个Unet进行实现。
DDPM
import torch
import torch.nn as nn
import torch.nn.functional as F
class Unet(nn.Module):
def __init__(self, in_channels, out_channels, ch=128):
super().__init__()
self.down = nn.ModuleList([
Block(in_channels, ch, ch * 2),
Block(ch * 2, ch * 2, ch * 4),
Block(ch * 4, ch * 4, ch * 8),
Block(ch * 8, ch * 8, ch * 16),
])
self.up = nn.ModuleList([
Block(ch * 16, ch * 8, ch * 8),
Block(ch * 8, ch * 4, ch * 4),
Block(ch * 4, ch * 2, ch * 2),
Block(ch * 2, ch, ch),
])
self.final_conv = nn.Conv2d(ch, out_channels, kernel_size=1)
def forward(self, x):
skip_connections = []
for down_block in self.down:
x = down_block(x)
skip_connections.append(x)
skip_connections = skip_connections[::-1]
for i, up_block in enumerate(self.up):
x = up_block(x, skip_connections[i])
return self.final_conv(x)
class Block(nn.Module):
def __init__(self, in_channels, out_channels, hidden_channels=None):
super().__init__()
hidden_channels = hidden_channels or out_channels
self.conv1 = nn.Conv2d(in_channels, hidden_channels, kernel_size=3, padding=1)
self.conv2 = nn.Conv2d(hidden_channels, out_channels, kernel_size=3, padding=1)
self.time_emb = nn.Linear(1, out_channels)
def forward(self, x, skip=None):
t = torch.randint(0, 1000, (x.shape[0], 1, 1, 1), device=x.device).float() / 1000.
t_emb = self.time_emb(t)
h = F.relu(self.conv1(x))
h = self.conv2(h)
if skip is not None:
h = torch.cat([h, skip], dim=1)
return F.relu(h + t_emb)
class DDPM(nn.Module):
def __init__(self, image_size, timesteps=1000, beta_start=1e-4, beta_end=0.02):
super().__init__()
self.image_size = image_size
self.timesteps = timesteps
self.beta = torch.linspace(beta_start, beta_end, timesteps)
self.alpha = 1. - self.beta
self.alpha_bar = torch.cumprod(self.alpha, dim=0)
self.unet = Unet(3, 3)
def forward(self, x_0, t):
# Sample noise from a Gaussian distribution
epsilon = torch.randn_like(x_0)
# Apply the forward diffusion process
x_t = torch.sqrt(self.alpha_bar[t]) * x_0 + torch.sqrt(1. - self.alpha_bar[t]) * epsilon
# Predict the noise using the U-Net
epsilon_theta = self.unet(x_t, t)
return epsilon_theta
def sample(self, batch_size=16):
# Sample noise from a Gaussian distribution
x_t = torch.randn(batch_size, 3, self.image_size, self.image_size)
# Iterate over the reverse diffusion process
for t in reversed(range(self.timesteps)):
# Predict the noise using the U-Net
epsilon_theta = self.unet(x_t, t)
# Apply the reverse diffusion process
x_t = (x_t - torch.sqrt(1. - self.alpha[t]) * epsilon_theta) / torch.sqrt(self.alpha[t])
return x_t
# Create a DDPM model
model = DDPM(image_size=32)
# Train the model
# ...
# Sample images from the model
samples = model.sample()
# Display the sampled images
# ...
自监督模型
自监督学习主要是利用辅助任务(pretext)从大规模的无监督数据中挖掘自身的监督信息,通过这种构造的监督信息对网络进行训练,从而可以学习到对下游任务有价值的表征。
自监督学习的方法主要可以分为 3 类:
基于上下文(Context based)
BERT:简而言之在一串文本当中添加掩码然后对比网络的输出和掩码掩盖的值是否一致等;或者是预测两个句子是否有顺序关系。 于是我们得到了一个会做句子填空的模型。 这一段成为预训练,属于无监督的部分,随后BERT需要进行模型的微调来进行下游任务。比如情感预测,词性标注等等。而 BERT 初始的参数是从学习了做填空题的 BERT来的. 在训练模型时,会随机初始化参数,接着利用梯度下降来更新这些参数,最小化损失.
但在 BERT 中不必随机初始化所有参数,随机初始化的参数只是线性变换的参数. BERT的骨干(backbone)是一个巨大的 Transformer 编码器,该网络的参数不是随机初始化的. 这里直接拿已经学会填空的 BERT 的参数当作初始化的参数,最直观和最简单的原因是它比随机初始化参数的网络表现更好. 把学会填空的 BERT 放在这里时,它会获得比随机初始化的BERT 更好的性能(论文对比里面可以看到相比随机初始化的网络,预训练的BERT在loss下降速度好和最终稳定下来的曲线当中都明显更优)。基于时序(Temporal Based)
相对于视频而言的一种处理,整体思路不过多赘述。不过类似BERT,获得一个预测下一个句子tocken的预处理模型,即为GPT。基于对比(Contrastive Based)
构建正负样本,并使得两个样本之间的距离越远越好。
自编码器
图片通过编码器压缩成一个向量(某种输入参量);向量再输入到解码器生成一个图像。目的是让解码器输出的图片和编码器编码之前的原图片足够相似。
特征解耦:编码器压缩出来的向量,包含了还原出原图片/声音等必要的信息。则我们通过承兑的数据观察向量值的对比可以对特征层面进行解耦。了解该向量各个维度包含的的信息是什么(这一部分可以通过自注意力机制进行学习)。
那么我们可以把特征向量部分进行拼接,生成特定风格的图像,或者模仿某一个人的声音。
进一步,如果强迫向量部分为独热向量,甚至可以还可以让机器自动学会分类和分类生成等等。
VAE(变分自编码器)
VAE的中间向量变成了一个潜在的变量概率分布Z空间。
学习的目标变化为优化thita参量描述这个空间,最大化原真实数据X的概率。即学习数据的分布并生成与输入数据相似的新样本。可以看出关键是将输入数据视为从潜在空间中采样的结果,然后从这个Z空间如何采样的问题。
核心是如何编码到这个Z空间(服从某种概率分布的空间)——有两个需要优化的地方:1)重构损失。2)KL散度(信息散度,衡量两个概率分布之间的差异程度)
步骤大致如下:
1. 编码器学习如何将数据压缩到Z空间的低维表示方法。
2. 从z空间当中采样得到一个点
3. 将采样的点输入到解码器,然后用解码器将潜在空间的点映射回原始的数据空间。但是由于随机采样的过程,解码器生成的样本与原始输入的数据相似但不同,造成了这种差异化使得VAE能生成更加多样化的样本,描述空间内在的一些特征。
4. 损失函数用重构损失和KL散度构成。目标是最小化这些总损失。
VAE的应用场景很多:从下生成到图像修复都有涵盖,我主要关注在医学图像处理当中的图像修复。
这边贴一下VAE的简单demo代码:
import torch
import torch.nn as nn
import torch.optim as optim
import torchvision
from torchvision import datasets, transforms
import matplotlib.pyplot as plt
#定义Encoder and Decoder(只用简单的线性层)
class Encoder(nn.Module):
def __init__(self, input_dim, hidden_dim, latent_dim):
super(Encoder, self).__init__()
self.fc1 = nn.Linear(input_dim, hidden_dim)
self.fc_mu = nn.Linear(hidden_dim, latent_dim)
self.fc_logvar = nn.Linear(hidden_dim, latent_dim)
def forward(self, x):
x = torch.relu(self.fc1(x))
mu = self.fc_mu(x)
logvar = self.fc_logvar(x)
return mu, logvar
class Decoder(nn.Module):
def __init__(self, latent_dim, hidden_dim, output_dim):
super(Decoder, self).__init__()
self.fc1 = nn.Linear(latent_dim, hidden_dim)
self.fc2 = nn.Linear(hidden_dim, output_dim)
def forward(self, z):
z = torch.relu(self.fc1(z))
x_hat = torch.sigmoid(self.fc2(z))
return x_hat
#定义模型
class VAE(nn.Module):
def __init__(self, encoder, decoder):
super(VAE, self).__init__()
self.encoder = encoder
self.decoder = decoder
def forward(self, x):
mu, logvar = self.encoder(x)
std = torch.exp(0.5 * logvar)
eps = torch.randn_like(std)
z = mu + eps * std
x_hat = self.decoder(z)
return x_hat, mu, logvar
#训练
def train_vae(model, dataloader, optimizer, criterion, device):
model.train()
running_loss = 0.0
for batch in dataloader:
batch = batch.to(device)
optimizer.zero_grad()
recon_batch, mu, logvar = model(batch.view(-1, 784))
loss = criterion(recon_batch, batch.view(-1, 784), mu, logvar)
loss.backward()
running_loss += loss.item()
optimizer.step()
return running_loss / len(dataloader.dataset)
顺便贴上CSDN上别人写的demo手写图像生成(采用的是MNIST的数据集),稍微加点备注
# 定义超参数
input_dim = 784
hidden_dim = 256
latent_dim = 20
output_dim = 784
batch_size = 128
epochs = 20
# 创建数据加载器
transform = transforms.Compose([transforms.ToTensor()])
# 这一步加载的是MNIST数据集,可以尝试更换其他想要的数据集。
train_dataset = datasets.MNIST('./data', train=True, download=True, transform=transform)
train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
# 创建VAE模型
encoder = Encoder(input_dim, hidden_dim, latent_dim)
decoder = Decoder(latent_dim, hidden_dim, output_dim)
vae = VAE(encoder, decoder)
# 定义优化器和损失函数
optimizer = optim.Adam(vae.parameters(), lr=0.001)
criterion = VAE_loss()
# 训练VAE模型
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
vae.to(device)
for epoch in range(epochs):
train_loss = train_vae(vae, train_loader, optimizer, criterion, device)
print(f"Epoch {epoch+1}/{epochs}, Loss: {train_loss:.4f}")
# 生成手写数字图像
with torch.no_grad():
z = torch.randn(64, latent_dim).to(device)
generated_images = vae.decoder(z).view(-1, 1, 28, 28).cpu()
# 显示生成的图像
fig, axes = plt.subplots(8, 8, figsize=(10, 10))
for i, ax in enumerate(axes.flatten()):
ax.imshow(generated_images[i][0], cmap='gray')
ax.axis('off')
plt.show()
迁移学习
领域自适应问题
一般我们的训练集和测试集的数据分布是比较相似的。这样能让模型在测试集上取得较好的结果。
我们把训练集对应的任务称为源领域,测试集对应的任务称为目标领域。
比如在实验室里面训练了一个模型,并想要把它用在真实的场景里面,于是将模型上线。上线后的模型确实有一些人来用,但得到的反馈很差,大家嫌弃系统正确率很低。这种情况就可以用领域自适应的技术,因为系统已经上线后会有人使用,就可以收集到一大堆未标注的数据。这些未标注的数据可以用在源领域上训练一个模型,并用在目标领域。
如何做到领域自适应呢?下面举一个例子。
有时候我们需要训练的模型处理一些稍微不同的任务,比如手写数字识别从MNIST的黑白数据集转向彩色图片的识别。
最基本的思想是训练一个特征提取器网络,把彩色和黑白手写数字都能提取到相似的特征向量。可以构造一个损失函数来判断两者的特征信息的相似程度。
同时,我们要训练一个领域分类器。领域分类器是一个二元的分类器,其输入是特征提取器输出的向量,其目标是判断这个向量是来自于源领域还是目标领域,而特征提取器学习的目标是要去想办法骗过领域分类器。
我们可以看到领域对抗训练非常像是生成对抗网络GAN,特征提取器可看成生成器,领域分类器可看成判别器。但在领域对抗训练里面,特征提取器优势太大了,其要骗过领域分类器很容易。比如特征提取器可以忽略输入,永远都输出一个零向量。这样做领域分类器的输入都是零向量,其也无法判断该向量的领域。但标签预测器也需要特征判断输入的图片的类别,如果特征提取器只会输出零向量,标签预测器无法判断是哪一张图片。特征提取器还是需要产生向量来让标签预测器可以输出正确的测。因此特征提取器不能永远都输出零向量。
故我们可以把特征提取器的损失函数设置为源领域分类任务的交叉熵损失减去领域分类器的二元判断损失。
网络压缩
多亏了某一篇看得我头皮发麻的交叉自蒸馏的医学图像处理的论文源代码。。这边顺便贴贴网络压缩的一些思路。
虽然工业界使用比较多,主要应用在一些小算力平台上的开发;不过这个交叉自蒸馏和压缩的关系倒是不大,还是给了我不少可以调整的思路(回头复现一下那一篇超分辨的论文,虽然总感觉哪里怪怪的...)
Incremental Cross-view Mutual Distillation for Self-supervised Medical CT Synthesis
不过实际网络压缩还算一个比较大的分支,这边简单引入。
知识蒸馏
一般分为教师网络和学生网络,以老师的输出作为学生网络的判断标准。
比如教师的输出可能是看到这张图片 1 的分数是 0.7,7 的分数是 0.2,9 这个数字的分数是 0.1 等等。接下来给学生一模一样的图片,但是学生不是去看这个图片的正确答案来学习,它把老师的输出就当做正确答案,也就是老师输出 1 要 0.7,7 要 0.2,9 要 0.1。学生的输出也就要尽量去逼近老师的输出,尽量去逼近 1 是 0.7、7 是 0.2、9 是 0.1 这样的答案。学生就是根据老师的答案学,就算老师的答案是错的,学生就去学一个错的东西。
可以很明显的看出这些软输出携带了不少额外的信息。
那么这个知识蒸馏在我这个SVR的课题里面有什么应用呢?前面贴的论文称自己的方法是交叉蒸馏(或者说相互蒸馏)机制。其实主要是三组并行的网络输出结果在某种维度上互相校正,属于是三人行,必有我师焉。。。不过本身和网络压缩机制还是相去甚远,反正他这么命名了那也是随他们的喜好了。
隐式神经表示(INR)
对于计算机来说,传统的表示信号的方式通常是离散化的。而INR则把信号参数转化为一个连续的函数。比如对于图像,就是构造f(x,y) = (r,g,b)的值,训练的是f,也就是构造出相应的函数。
这边强调这个INR主要还是因为它的应用场景:超分辨率,新视角合成,三维重建。
基于ReLU的多层感知器(MLP)是隐式神经表示常用的网络结构。Hornik在1989年证明,当中间隐含层的神经元数量趋于无穷多时,多层感知机可以拟合任何非线性函数 。
当然ReLU自身还是存在很多限制,分段线性导致其无法很好反应信号的梯度。所以也有很多改进方法,比如使用正弦函数替代ReLU的SIREN;或者是对强调高频信号的傅里叶位置编码(FPE)来改进MLP。贴一下后者的论文(因为涉及到的MRI重建的应用):
Fourier Features Let Networks Learn High Frequency Functions in Low Dimensional Domains