嘿!您似乎在 United States,您想使用我们的 English 网站吗?
Switch to English site
Skip to main content

Pytorch深度学习框架X NVIDIA JetsonNano应用-cDCGAN生成手写数字

作者

张嘉钧

难度

中偏难

材料表

  1. Jetson Nano
  2. PC or Notebook

DCGAN 到 cDCGAN

先来稍微复习一下DCGAN (深度卷积生成对抗网络),字面上就是利用卷积网络的架构来做生成对抗,主要由生成器与鉴别器所构成,如下图所示:

dcgan3_44afc6e27000b4f0394cd03756f557bff906790e.png

生成器会将一组噪声或称做潜在空间的张量转换成一张照片,这张照片再经由鉴别器去判断图片是否够真实,越接近0越假;越接近1越真。

由于我们在训练的时候其实是没有加载标签的!所以他生成的时候都是随机生成,为了能限制特定的输出我们必须加载标签,概念图就会变成下面这张:

cdcgan3_94755324057281b862055a9c425d0752c94d6dc7.jpg

透过标签的导入,让生成器知道要生成的对象是哪一个数字,并且鉴别器训练的目标变成「图像是否真实」加上「是否符合该类别」,cDCGAN跟DCGAN相比,训练的结果通常会比较好,因为DCGAN神经网络是盲目的去生成,而cDCGAN则是会将生成的范围缩小,整体而言会收敛更快且更好。

将卷标合并于数据中

首先我们要先了解如何加入标签,对于DCGAN来说有两种加入标签的方法,第一个是一开始就将图片或噪声跟卷标合并;另一个方法则是在深层做合并,读者们在实作的时候可以自行调整看看差异,那较常见的做法是深层合并,而我写的也是!

2_b08fd8a37c53d58243574bb5568fae7ae84b183b.jpg 2_b08fd8a37c53d58243574bb5568fae7ae84b183b.jpg

潜层合并,先合并再输入网络

深层合并:各别输入后再合并

其中详细的差别我还没涉略到,不过选定了深层合并接着就可以先来实作生成器跟鉴别器了。首先先来建构生成器,可以参考上一篇DCGAN的程序代码,这边帮大家整理了一张概念图:

Generator_Structure2_eccb7d098ed92d57ac43f4d7a8f3a3d30d440856.jpg

输入的z是维度为 ( 100, 1, 1) 的噪声,为了将卷标跟噪声能合并,必须转换到相同大小也就是 (1, 1),可以看到这边 y 的维度是 ( 10, 1, 1 ) 原因在于我们将原先阿拉伯数字的卷标转成 onehot 编码格式,如下图所示。

onehot_%282%292_660039d6f95abc25197835f34a769ca681fb9f79.jpg

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)

print_model_directly2_ed179b2c4ddfbee85dc7e475cc4542b501803f16.jpg

Print_model_with_Torchsummary2_b75142e706f084b89c7e5c0d02a888809af1b01f.jpg

所以我决定使用更图像化一点的方式来可视化我们的网络架构,现在有不下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

tensorboard-logdir2_4b4034f94d471c534f2f199f37ce8326065250eb.jpg

一开始就可以看到 input > Generator 的箭头有写 2 tensor,而这些方块都可以打开:

tensorboard-12_98046ef0a8d56a14f39ff701c2af73b086aaf2de.jpg

开启后你可以看到更细部的信息,也很清楚就可以看到支线合并的状况。

tensorboard_g_graph2_15f22e83bb709527ce424c9fa25a3a2f850a295a.jpg

每一次卷积后的形状大小也都有显示出来:

tensorboard_g_graph_22_72ad01f609d940229a7a1e67fa0ad62902ae9b4d.jpg

接下来简单介绍一下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还要再开启服务。

hiddenlayer-flowchart13_55fc38d8585105c37bcb937b8eea0d1feeff390a.jpg

hiddenlayer-flowchart24_d33d79f334f517f8c46f7ef3f080dc145d6fd56e.jpg

建立Discriminatorc

Discriminator_Structure2_23d84bc61ca9599e2a74938b59d6177d5f764b68.jpg

跟建立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))

可视化的结果如下:

tensorboard_D_graph2_3fc23460f95db95efdf1edfacbdddf94e861c578.jpg

数据处理

神经网络都建置好就可以准备来训练啦!当然第一步要先将数据处理好,那我个人自学神经网络的过程我觉得最难的就是数据处理了,这次数据处理有2个部分:

  1. 宣告固定的噪声跟卷标用来预测用
  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)

onehot_code2_04832cafbdf5bf90f4640b20e257321f9f929b82.jpg

接下来我们将两个部分分开处理,先来处理测试用的噪声跟卷标,测试用图片为美个类别各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) 来方便做观察:

data_preprocess_fixednoise2_eac20701170967a9ebf4294d27a0d8e55e20984c.jpg

接下来要帮训练的数据做前处理,处理方式跟前面雷同,主要差别在要喂给鉴别器的标签 ( 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)

data_preprocess_trainlabel3_511ca2837f6712ad31c715be847ede272453c287.jpg

开始训练-起手式

一样从基本的参数开始宣告起,流程个别是:基本参数、数据加载、建立训练相关的东西(模型、优化器、损失)、开始训练。

""" 基本参数 """
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

result012_4462b1d978885ac9aeb1aeeffb22fede392d54ac.jpg result022_d0e4b6ebf5ab4c014ff6128c7728b75ec01ba1a9.jpg result032_16fd3cc5319f1231ac49118deaa16ba9d13d139b.jpg result043_c8d5a228eef2f6b4d86777b6f96a8d0dc4540875.jpg result052_6735038fccc15a124afabf6efb1e16216080c04c.jpg

6

7

8

9

10

result062_d33b9c5cb8eba37bc821fd14085785df50c1c74e.jpg result072_48580d9e02bc331207a80d217e68d3e0359e8cb9.jpg result082_cdca756baf8d3f79366db2633ec04ea358c68242.jpg result092_0c48fab4e3319c847395186f4905a9a1e2ed3b91.jpg result102_06a56fb60182f85321dde5e2c56c9b66f00086af.jpg

下面是迭代15次的成果,感觉上比参考的gihub还要差了一些,仔细看了一下应该是D的结构跟learning rate的调整有差,大家可以在自己调整看看。

1090907_cDCGAN2_c9a1b9a1aa523d3bd0de82eabf867748e56fe087.gif

训练时间比较

一样都是 10 个 epoch ,Jetson Nano所需要的时间大约是 1 小时 40 分钟,其实还算是蛮快的,大家可以试试看 CPU 去跑跑看就可以知道差异了。

generate_mnist_sample2_0028266e44ddbd3de6106d0b0b2aeef952a4baa4.jpg generate_mnist_sample_Tegra1_289e936f173fff712cd26cc52f20393a446ce74a.jpg

结语

最后相信大家到看完这篇以及上一篇DCGAN已经对生成对抗网络有一定的熟悉度了,接下来我们可以找些GAN的github的范例来玩玩看并且增加应用。

CAVEDU Education is devoted into robotics education and maker movement since 2008, and is intensively active in teaching fundamental knowledge and skills. We had published many books for readers in all ages, topics including Deep Learning, edge computing, App Inventor, IoT and robotics. Please check CAVEDU's website for more information: http://www.cavedu.com, http://www.appinventor.tw
DesignSpark Electrical Logolinkedin