你觉得这篇文章怎么样? 帮助我们为您提供更好的内容。
Thank you! Your feedback has been received.
There was a problem submitting your feedback, please try again later.
你觉得这篇文章怎么样?
作者 |
张嘉钧 |
难度 |
中偏难 |
材料表 |
|
DCGAN 到 cDCGAN
先来稍微复习一下DCGAN (深度卷积生成对抗网络),字面上就是利用卷积网络的架构来做生成对抗,主要由生成器与鉴别器所构成,如下图所示:
生成器会将一组噪声或称做潜在空间的张量转换成一张照片,这张照片再经由鉴别器去判断图片是否够真实,越接近0越假;越接近1越真。
由于我们在训练的时候其实是没有加载标签的!所以他生成的时候都是随机生成,为了能限制特定的输出我们必须加载标签,概念图就会变成下面这张:
透过标签的导入,让生成器知道要生成的对象是哪一个数字,并且鉴别器训练的目标变成「图像是否真实」加上「是否符合该类别」,cDCGAN跟DCGAN相比,训练的结果通常会比较好,因为DCGAN神经网络是盲目的去生成,而cDCGAN则是会将生成的范围缩小,整体而言会收敛更快且更好。
将卷标合并于数据中
首先我们要先了解如何加入标签,对于DCGAN来说有两种加入标签的方法,第一个是一开始就将图片或噪声跟卷标合并;另一个方法则是在深层做合并,读者们在实作的时候可以自行调整看看差异,那较常见的做法是深层合并,而我写的也是!
潜层合并,先合并再输入网络 |
深层合并:各别输入后再合并 |
其中详细的差别我还没涉略到,不过选定了深层合并接着就可以先来实作生成器跟鉴别器了。首先先来建构生成器,可以参考上一篇DCGAN的程序代码,这边帮大家整理了一张概念图:
输入的z是维度为 ( 100, 1, 1) 的噪声,为了将卷标跟噪声能合并,必须转换到相同大小也就是 (1, 1),可以看到这边 y 的维度是 ( 10, 1, 1 ) 原因在于我们将原先阿拉伯数字的卷标转成 onehot 编码格式,如下图所示。
OneHot编码主要在于让卷标离散,如果将卷标都用阿拉伯数字表示,对于神经网络而言他们属于连续性的数值或许会将前后顺序、距离给考虑进去,但是用onehot之后将可以将各类标签单独隔开并且对于彼此的距离也会相同。
建立Generator
接下来是程序的部分,如何在神经网络中做分流又合并,其实对于PyTorch而言非常的简单只要在forward的地方做torch.cat就可以了。首先一样要先定义网络层,我们定义了三个 Sequential,其中input_x是给图像用的所以第一层deconv的输入维度是z_dim;而input_y则是标签用所以deconv的输入是label_dim,可以对照上面的图片看看:
def __init__(self, z_dim, label_dim):
super(Generator, self).__init__()
self.input_x = nn.Sequential(
# input is Z, going into a convolution
nn.ConvTranspose2d(z_dim, 256, 4, 1, 0, bias=False),
nn.BatchNorm2d(256),
nn.ReLU(True),
# image size = (1-1)*1 - 2*0 + 4 = 4
)
self.input_y = nn.Sequential(
# input is Z, going into a convolution
nn.ConvTranspose2d( label_dim, 256, 4, 1, 0, bias=False),
nn.BatchNorm2d(256),
nn.ReLU(True),
# image size = (1-1)*1 - 2*0 + 4 = 4
)
self.concat = nn.Sequential(
# 因为 x 跟 y 水平合并所以要再乘以 2
nn.ConvTranspose2d(256*2, 128, 4, 2, 1, bias=False),
nn.BatchNorm2d(128),
nn.ReLU(True),
# image size = (4-1)*2 - 2*1 + 4 = 8
nn.ConvTranspose2d( 128, 64, 4, 2, 1, bias=False),
nn.BatchNorm2d(64),
nn.ReLU(True),
# image size = (8-1)*2 - 2*1 + 4 = 16
nn.ConvTranspose2d( 64, 1, 4, 2, 3, bias=False),
nn.Tanh()
# image size = (16-1)*2 - 2*3 + 4 = 28
)
接下来看 forward的部分,可以看到我们在向前传递的时候要丢入两个数值,噪声跟卷标,将x跟y丢进各自的Sequential中,接着我们使用torch.cat将x, y从横向 ( dim=1 ) 合并后再进到concat中。
def forward(self, x, y):
x = self.input_x(x)
y = self.input_y(y)
out = torch.cat([x, y] , dim=1)
out = self.concat(out)
return out
接下来可以试着将网络架构显示出来,我们直接使用print也使用torchsummary来显示,你可以发现其实你没办法看出网络分支再合并的状况
def print_div(text):
div='\n'
for i in range(60): div += "="
div+='\n'
print("{} {:^60} {}".format(div, text, div))
""" Define Generator """
G = Generator(100, 10)
""" Use Print"""
print_div('Print Model Directly')
print(G)
""" Use Torchsummary """
print_div('Print Model With Torchsummary')
test_x = (100, 1, 1)
test_y = (10, 1, 1)
summary(G, [test_x, test_y], batch_size=64)
所以我决定使用更图像化一点的方式来可视化我们的网络架构,现在有不下10种的图形化方式,我举两个例子:Tensorboard、hiddenlayer。
可视化模型
Tensorboard 是Google 出的强大可视化工具,一般的文字、数值、影像、声音都可以动态的纪录在上面,一开始只支持Tensorflow 但是 PyTorch 1.2 之后都包含在其中 ( 但是要使用的话还是要先安装tensorboard ) ,你可以直接从 torch.utils.tensorboard 中呼叫 Tensorboard,首先需要先实体化 SummaryWritter,接着直接使用add_graph即可将图片存到服务器上
""" Initial Parameters"""
batch_size = 1
test_x = torch.rand(batch_size, 100, 1, 1)
test_y = torch.rand(batch_size, 10, 1, 1)
print_div('Print Model With Tensorboard')
print('open terminal and input "tensorboard --logdir=runs"')
print('open browser and key http://localhost:6006')
writer = SummaryWriter()
writer.add_graph(G, (test_x, test_y))
writer.close()
接下来要开启服务器,在终端机中移动到与程序代码同一层级的位置并且输入:
> tensorboard –logdir=./runs
一开始就可以看到 input > Generator 的箭头有写 2 tensor,而这些方块都可以打开:
开启后你可以看到更细部的信息,也很清楚就可以看到支线合并的状况。
每一次卷积后的形状大小也都有显示出来:
接下来简单介绍一下hiddenlayer ,它不能用来取代高级API像是tensorboard之类的,它仅仅就是用来显示神经网络模型,但是非常的轻巧所以我个人蛮爱使用它的,首先要先透过pip安装hiddenlayer、graphviz:
> pip install hiddenlayer
> Pip install graphviz
如果是用Jetson Nano的话,建议用 apt去装 graphviz
$ sudo apt-get install graphviz
接着用 build_graph就能产生图像也能直接储存:
""" Initial Parameters"""
batch_size = 1
test_x = torch.rand(batch_size, 100, 1, 1)
test_y = torch.rand(batch_size, 10, 1, 1)
print_div('Print Model With HiddenLayer')
g_graph = hl.build_graph(G, (test_x, test_y))
g_graph.save('./images/G_grpah', format="jpg")
g_graph
因为太长了所以我截成两半方便观察,这边就可以注意到前面的ConvTranspose、BatchNorm、ReLU是分开的,之后才合并这边还特别给了一个Concat的方块,我喜欢使用它的原因是简单明了,卷积后的维度也都有写下来,并且直接执行就可以看到结果,不用像Tensorboard还要再开启服务。
建立Discriminatorc
跟建立Generator的概念相似,我们要个别处理输入的图片跟卷标,所以依样宣告两个 Sequential 个别处理接着再将输出 concate 在一起,主要要注意的是 y 的输入为度为 (10, 28, 28):
import torch
import torch.nn as nn
from torchsummary import summary
class Discriminator(nn.Module):
def __init__(self, c_dim=1, label_dim=10):
super(Discriminator, self).__init__()
self.input_x = nn.Sequential(
# Input size = 1 ,28 , 28
nn.Conv2d(c_dim, 64, (4,4), 2, 1),
nn.LeakyReLU(),
)
self.input_y = nn.Sequential(
# Input size = 10 ,28 , 28
nn.Conv2d(label_dim, 64, (4,4), 2, 1),
nn.LeakyReLU(),
)
self.concate = nn.Sequential(
# Input size = 64+64 ,14 , 14
nn.Conv2d(64*2 , 64, (4,4), 2, 1),
nn.LeakyReLU(),
# Input size = (14-4+2)/2 +1 = 7
nn.Conv2d(64, 128, 3, 2, 1),
nn.LeakyReLU(),
# Input size = (7-3+2)/2 +1 = 4
nn.Conv2d(128, 1, 4, 2, 0),
nn.Sigmoid(),
# output size = (4-4)/2 +1 = 1
)
def forward(self, x, y):
x = self.input_x(x)
y = self.input_y(y)
out = torch.cat([x, y] , dim=1)
out = self.concate(out)
return out
D = Discriminator(1, 10)
test_x = torch.rand(64, 1,28,28)
test_y = torch.rand(64, 10,28,28)
writer = SummaryWriter()
writer.add_graph(D, (test_x, test_y))
writer.close()
hl.build_graph(D, (test_x, test_y))
可视化的结果如下:
数据处理
神经网络都建置好就可以准备来训练啦!当然第一步要先将数据处理好,那我个人自学神经网络的过程我觉得最难的就是数据处理了,这次数据处理有2个部分:
- 宣告固定的噪声跟卷标用来预测用
- 将标签转换成onehot格式 ( scatter )
Onehot数据处理,在torch中可以直接使用scatter的方式,我在程序批注的地方有推荐一篇文章大家可以去了解scatter的概念,至于这边我先附上实验的程序代码:
""" OneHot 格式 之 scatter 应用"""
""" 超好理解的图形化教学 https://medium.com/@yang6367/understand-torch-scatter-b0fd6275331c """
label =torch.tensor([1,5,6,9])
print(label, label.shape)
a = torch.zeros(10).scatter_(0, label, 1)
print(a)
print('\n\n')
label_=label.unsqueeze(1)
print(label_, label_.shape)
b = torch.zeros(4,10).scatter_(1, label_, 1)
print(b)
接下来我们将两个部分分开处理,先来处理测试用的噪声跟卷标,测试用图片为美个类别各10张,所以总共有100张图片代表是100组噪声及对应label:
""" 产生固定数据,每个类别10张图(噪声) 以及 对应的标签,用于可视化结果 """
temp_noise = torch.randn(label_dim, z_dim) # (10, 100) 10张图
fixed_noise = temp_noise
fixed_c = torch.zeros(label_dim, 1) # (10, 1 ) 10个标签
for i in range(9):
fixed_noise = torch.cat([fixed_noise, temp_noise], 0) #将每个类别的十张噪声依序合并,维度1会自动boardcast
temp = torch.ones(label_dim, 1) + i #依序将标签对应上 0~9
fixed_c = torch.cat([fixed_c, temp], 0) #将标签也依序合并
fixed_noise = fixed_noise.view(-1, z_dim, 1, 1) #由于是卷积所以我们要将形状转换成二维的
print('Predict Noise: ', fixed_noise.shape)
print('Predict Label (before): ', fixed_c.shape, '\t\t\t', fixed_c[50])
""" 针对 lael 做 onehot """
fixed_label = torch.zeros(100, label_dim) #先产生 [100,10] 的全0张量,100个卷标,每个卷标维度是 10
fixed_label.scatter_(1, fixed_c.type(torch.LongTensor), 1) #转成 onehot编码 (1, ) -> (10, )
fixed_label = fixed_label.view(-1, label_dim, 1, 1) #转换形状 (10, 1, 1 )
print('Predict Label (onehot): ',fixed_label.shape, '\t\t', fixed_label[50].view(1,-1), '\n')
我在显示的时候有将形状从 (10,1)变成(1,10) 来方便做观察:
接下来要帮训练的数据做前处理,处理方式跟前面雷同,主要差别在要喂给鉴别器的标签 ( fill ) 处理方式比较不同,从结果图就能看的出来彼此不同的地方:
""" 帮标签做前处理,onehot for g, fill for d """
onehot = torch.zeros(label_dim, label_dim) # 产生 (10,10) 10个标签,维度为10 (onehot)
onehot = onehot.scatter_(1, torch.LongTensor([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]).view(label_dim, 1), 1).view(label_dim, label_dim, 1, 1)
print('Train G label:',onehot[1].shape, '\n', onehot[1], '\n') # 假设我们要取得标签 1 的 onehot (10,1,1),直接输入索引 1
fill = torch.zeros([label_dim, label_dim, image_size, image_size]) # 产生 (10, 10, 28, 28) 意即 10个标签 维度都是 (10,28,28)
for i in range(label_dim):
fill[i, i, :, :] = 1 # 举例 标签 5,第一个[]代表标签5,第二个[]代表onehot为1的位置
print('Train D Label: ', fill.shape)
print('\n', fill[1].shape, '\n', fill[1]) # 假设我们要取得标签 1 的 onehot (10,28,28)
开始训练-起手式
一样从基本的参数开始宣告起,流程个别是:基本参数、数据加载、建立训练相关的东西(模型、优化器、损失)、开始训练。
""" 基本参数 """
epoch = 10
lr = 1e-5
batch = 4
device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')
z_dim = 100 # latent Space
c_dim = 1 # Image Channel
label_dim = 10 # label
""" 取得数据集以及DataLoader """
transform = trans.Compose([
trans.ToTensor(),
trans.Normalize((0.5,),(0.5,)),
])
train_set = dset.MNIST(root='./mnist_data/',
train=True, transform=transform,
download=True)
test_set = dset.MNIST(root='./mnist_data/',
train=False,
transform=transform,
download=False)
train_loader = torch.utils.data.DataLoader(
dataset = train_set,
batch_size = batch,
shuffle=True,
drop_last=True
)
test_loader = torch.utils.data.DataLoader(
dataset = test_set,
batch_size = batch,
shuffle=False
)
""" 训练相关 """
D = Discriminator(c_dim, label_dim).to(device)
G = Generator(z_dim, label_dim).to(device)
loss_fn = nn.BCELoss()
D_opt = optim.Adam(D.parameters(), lr= lr)
G_opt = optim.Adam(G.parameters(), lr= lr)
D_avg_loss = []
G_avg_loss = []
img = []
ls_time = []
开始训练 - 手动更新学习率
会手动更新主要原因在于其实GAN的训练并不是那么的顺利,如果速度太快会导致震荡严重训练生成效果极差,所以GAN普遍的学习率都会更新并且都蛮低的,这边我也稍微调整一下:
""" 看到很多范例都有手动调整学习率 """
if epoch == 8:
G_opt.param_groups[0]['lr'] /= 5
D_opt.param_groups[0]['lr'] /= 5
开始训练 - 训练D、G
一样参考上一篇的DCGAN来改良,主要差别在于需要引入label,并且需要将label转换成onehot格式,其中
鉴别器 (D) 的训练步骤一样先学真实图片给予卷标1 再学生成图片给予卷标 0,生成图片的部分要产生对应的随机数label,丢入G的时候是从先前写的 onehot 中提取对应的onehot格式标签而丢入D的时候是从 fill 中提取~
生成器 (G) 的训练方式就是把D的后半段拿出来用,但是标签需要改成 1,因为它的目的是要骗过D!
""" 训练 D """
D_opt.zero_grad()
x_real = data.to(device)
y_real = torch.ones(batch, ).to(device)
c_real = fill[label].to(device)
y_real_predict = D(x_real, c_real).squeeze() # (-1, 1, 1, 1) -> (-1, )
d_real_loss = loss_fn(y_real_predict, y_real)
d_real_loss.backward()
noise = torch.randn(batch, z_dim, 1, 1, device = device)
noise_label = (torch.rand(batch, 1) * label_dim).type(torch.LongTensor).squeeze()
noise_label_onehot = onehot[noise_label].to(device) #随机产生label (-1, )
x_fake = G(noise, noise_label_onehot) # 生成假图
y_fake = torch.zeros(batch, ).to(device) # 给予标签 0
c_fake = fill[noise_label].to(device) # 转换成对应的 10,28,28 的标签
y_fake_predict = D(x_fake, c_fake).squeeze()
d_fake_loss = loss_fn(y_fake_predict, y_fake)
d_fake_loss.backward()
D_opt.step()
""" 训练 G """
G_opt.zero_grad()
noise = torch.randn(batch, z_dim, 1, 1, device = device)
noise_label = (torch.rand(batch, 1) * label_dim).type(torch.LongTensor).squeeze()
noise_label_onehot = onehot[noise_label].to(device) #随机产生label (-1, )
x_fake = G(noise, noise_label_onehot)
#y_fake = torch.ones(batch, ).to(device) #这边的 y_fake 跟上述的 y_real 一样,都是 1
c_fake = fill[noise_label].to(device)
y_fake_predict = D(x_fake, c_fake).squeeze()
g_loss = loss_fn(y_fake_predict, y_real) #直接用 y_real 更直观
g_loss.backward()
G_opt.step()
D_loss.append(d_fake_loss.item() + d_real_loss.item())
G_loss.append(g_loss.item())
成果
起初我在第五次迭代的时候调整了学习率结果原本 1 到 5 学习的都不错,到第 6次的时候开始有了偏差,所以真的不能乱调学习率阿~
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
下面是迭代15次的成果,感觉上比参考的gihub还要差了一些,仔细看了一下应该是D的结构跟learning rate的调整有差,大家可以在自己调整看看。
训练时间比较
一样都是 10 个 epoch ,Jetson Nano所需要的时间大约是 1 小时 40 分钟,其实还算是蛮快的,大家可以试试看 CPU 去跑跑看就可以知道差异了。
结语
最后相信大家到看完这篇以及上一篇DCGAN已经对生成对抗网络有一定的熟悉度了,接下来我们可以找些GAN的github的范例来玩玩看并且增加应用。