自编码器的结构

自编码器(Auto Encoder)是一种神经网络模型。它由两部分组成:编码器(Ecoder)和解码器(Decoder)。

编码器用于将输入数据(Input Data)进行编码,从而将输入数据映射到维度较低的隐空间(Latent Space),得到被编码的数据(Encoded Data)。

解码器用于将隐空间中被编码的数据还原(解码)成”输入数据”。这里之所以打引号,是因为还原得到的”输入数据”相比于一开始的输入数据来说会有一些损失,所以并不是真正意义上的输入数据。

下图展示了自编码器的结构
Alt text

自编码器的应用

数据降维/特征提取

从自编码器的结构很容易想到它的这一用途。在训练阶段,$X$经过编码器映射得到低维的$z$,$z$通过解码器还原出与$X$维度一致,内容相似的$X’$,通过反向传播来更新网络权重,以最小化输入$X$与输出$X’$之间的损失。

其中的$z$便是降维后的数据,因为如果可以通过$z$还原出输入$X$,那么可以说$z$已经学习到了$X$的大部分特征。

同时,$z$是一个被高度压缩(降维)的输入数据的表示,所以也可以将自编码器看作一个特征提取器,提取到的特征表示就是$z$。
Alt text

图像去噪

图像去噪,就是去除图像上的噪点。比如下图中左半部分是无噪点的图像,而右半部分是有噪点的。我们的目标是训练一个自编码器,它的输入是有噪点的图像,而输出是去噪点后的图像,这样就达到了去噪的目的。
Alt text

训练方法也很简单。准备一些无噪点的图像$X$,然后手动给这些图像添加噪点得到对应有噪点的图像$X_{noise}$。将$X$输入编码器,然后再经解码器得到输出$X’$,通过反向传播更新权重,以最小化$X’$和$X$之间的损失。这样,输出的$X’$会接近无噪点的$X$,这就达到了降噪的目的。

在应用中,只需将有噪点的图像输入训练好的自编码器,就可以得到无噪点的输出图像了。

图像“生成”

其实关于图像“生成”的介绍已经包含在前面两个应用的介绍中了。去除训练好的自编码器中的编码器,剩余部分就可以看作是一个”生成器”。输入隐空间的一个低维特征表示$z$,通过解码器就可以得到一张生成的图片。

但是,有一个问题。以图片为例,将之前编码后的隐空间特征$z$输入给解码器,解码器会输出和$z$对应的输入相似的图片,而如果我们尝试将与隐空间特征维度一致的随机噪声输入解码器,那么得到的将是无意义的噪声图片。

因此,这里的”生成”并不是真正意义上的生成,更准确的说应该是“重建”。

变分自编码器可以弥补这一缺陷,这一点将在后续文章中进行讨论。

用 Pytorch 写个自编码器

现在,使用手写数字数据集训练一个自编码器。

手写数字数据集本来是若干张$12828$的图片,这里我们训练的是全连接神经网络,因此使用的是打平(flatten)后的$12828=784$维的特征。也就是说,如果有$N$张图片,那么我们数据的shape就是$N$行$784$列。

首先导入所需库:

1
2
3
4
5
6
import torch
import torch.nn as nn
import torch.optim as optim
import torchvision
import matplotlib.pyplot as plt
import numpy as np

然后准备手写数字数据集:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
transform = torchvision.transforms.Compose([torchvision.transforms.ToTensor()])

train_dataset = torchvision.datasets.MNIST(
root="torch_datasets", train=True, transform=transform, download=True
)

test_dataset = torchvision.datasets.MNIST(
root="torch_datasets", train=False, transform=transform, download=True
)

train_loader = torch.utils.data.DataLoader(
train_dataset, batch_size=128, shuffle=True, num_workers=4, pin_memory=True
)

test_loader = torch.utils.data.DataLoader(
test_dataset, batch_size=32, shuffle=False, num_workers=4
)

上述代码会自动从网络下载数据集到你指定的root路径下。
Alt text

接下来搭建自编码器网络:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class AE(nn.Module):
def __init__(self, **kwargs):
super().__init__()
#编码器
self.encoder_hidden_layer = nn.Linear(
in_features=kwargs["input_shape"], out_features=128)
self.encoder_output_layer = nn.Linear(
in_features=128, out_features=128)
#解码器
self.decoder_hidden_layer = nn.Linear(
in_features=128, out_features=128)
self.decoder_output_layer = nn.Linear(
in_features=128, out_features=kwargs["input_shape"])

def forward(self, features):
#编码器
activation = torch.relu(self.encoder_hidden_layer(features))
code = torch.relu(self.encoder_output_layer(activation))
#解码器
activation = torch.relu(self.decoder_hidden_layer(code))
reconstructed = torch.relu(self.decoder_output_layer(activation))
#返回重建的图像
return reconstructed

再设置一些参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#设定随机种子,让每次的结果都一样
#原因:Pytorch的某些操作具有随机性
seed = 42
torch.manual_seed(seed)
torch.backends.cudnn.benchmark = False
torch.backends.cudnn.deterministic = True
#设置 迭代轮数,学习率
epochs = 20
learning_rate = 1e-3

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = AE(input_shape=784).to(device)
#使用Adam优化器
optimizer = optim.Adam(model.parameters(), lr=1e-3)
#使用均方误差作为损失函数
criterion = nn.MSELoss()

开始训练:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

for epoch in range(epochs):
loss = 0
for batch_features, _ in train_loader:
#将N张*28*28的图片打平成N张784维的特征向量
batch_features = batch_features.view(-1, 784).to(device)

optimizer.zero_grad()

# 前向传播
outputs = model(batch_features)

# 计算损失
train_loss = criterion(outputs, batch_features)

# 反向传播计算梯度
train_loss.backward()

# 更新网络权重
optimizer.step()

# 损失累加
loss += train_loss.item()

# 计算本轮损失
loss = loss / len(train_loader)

# 进度条
print("epoch : {}/{}, loss = {:.6f}".format(epoch + 1, epochs, loss))

Alt text
可以看到,损失是逐渐减小的。

训练完成之后,使用测试集的第一个batch进行测试:

1
2
3
4
5
6
7
8
9
10
11

#取第一个batch进行测试
with torch.no_grad():
for batch_features in test_loader:
# 只取图片,不取标签
batch_features = batch_features[0]
# 打平
test_examples = batch_features.view(-1, 784).to(device)
# 前向推理
reconstruction = model(test_examples)
break

Alt text

reconstruction包含了32张重建后的图片,每张图片都用784维的特征进行表示。现在来可视化对比一下重建前后的图片,注意对于重建后的图片,我们需要将其reshape回$12828$。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
with torch.no_grad():
number = 10
plt.figure(figsize=(20, 4))
for index in range(number):
# 可视化原始图片
ax = plt.subplot(2, number, index + 1)
plt.imshow(test_examples[index].cpu().numpy().reshape(28, 28))
plt.gray()
#不显示坐标轴
ax.get_xaxis().set_visible(False)
ax.get_yaxis().set_visible(False)

# 可视化重建后的图片
ax = plt.subplot(2, number, index + 1 + number)
plt.imshow(reconstruction[index].cpu().numpy().reshape(28, 28))# on gpu
plt.gray()
#不显示坐标轴
ax.get_xaxis().set_visible(False)
ax.get_yaxis().set_visible(False)
plt.show()

得到输出图片如下:
Alt text

第一行是原始图片,第二行是第一行对应的重建后图片。可以看出,重建后的图片与原始图片轮廓基本一致,只是有一些损失,在图中表现为小黑点。

参考:

[1][https://medium.com/pytorch/implementing-an-autoencoder-in-pytorch-19baa22647d1]
[2][https://www.compthree.com/blog/autoencoder/]
[3][http://www.pgrady.net/music-compression-web/]
[4][https://towardsdatascience.com/denoising-autoencoders-explained-dbb82467fc2]