机器学习中包含三大类问题:有监督学习、无监督学习以及强化学习。训练数据集中主要包含2类元素:数据x以及标签y。当数据集中x、y均为已知时,待解决的问题为有监督学习;当已知数据x但不知道标签y时,待解决的问题即为无监督学习。前几章介绍的分类、检测、分割均为有监督学习。
自编码器
自编码器(Autoencoder)从不带标签的数据中学习低维特征表达。其中,z的维度小于x,因为z是待学习的,更能抓住数据的主要变化特征。在没有标签数据的情况下,可以使用“自编码器”,即通过对原图进行编码一解码的过程来构造z,同时使得解码后重构的x应尽可能与x相同。通过这种无监督(无标签数据)的方式训练后再将解码器去掉,留下的z即为特征提取器。通常来说,特征提取器可以用作有监督学习的初始化,这些有监督学习的带标记样本数量通常很少,但由于经过了大量的“自学习”,其已经具备了一定的特征提取能力,所以能使只有少量标注数据的任务更快收敛。
对抗生成网络
对抗生成网络(Generative Adversarial Nets,GAN)是一种通过博弈(自我学习)来得到目标的学习方式"。它通过学习“产生器”和“判别器”来产生与训练数据分布一样的图片。其中,产生器(有时也称“生成器”)将尽可能地生成与训练集分布一致的数据,使得生成的数据(“假”数据)尽可能地像真实数据:“判别器”将尽可能地区分真实数据和产生器生成的“假”数据,随机图片z通过产生器生成假数据(造出来的图片),而与之对应的则是从训练数据集中获取的真实图片,判别模型的任务是判断一张图片是从产生式模型生成的“假”数据还是从训练数据集中得到的“真”数据。一个完美的GAN产生的假动作会使得判别器无法分辨真假。
GAN 的训练结构
假设x表示图像,D(x)表示判别网络,是一个二元分类器,那么它的输出即为图片x来自训练数据(而不是产生网络输出的假图片)的概率。对于产生网络,首先定义从标准正态分布中采样的向量z,则 G(z)表示将向量z映射到图像空间的生成器函数。G的目标是估计训练数据的分布(pa),以便生成假样本。因此,D(G(z))是产生网络G的输出是真实图像的概率。即判别网络D和产生网络G在做一个极小极大的博弈,其中D试图最大化它正确分器真假数据(logD(x)的概率,而G试图最小化D预测其输出是假的概率(log(1-d(G(X)))
DGGAN
DCGAN将 GAN中的产生器G和判别器D都换成了卷积神经网络,并对其中的卷积做了一些改动以提高收敛速度
1)用不同步长的卷积层替换所有 Pooling层。
2)在D和G中均使用 BatchNorm 层。
3)在G网络中,除最后一层使用 tanh 以外,其余层均使用 ReLU 作为激活函数,
4)D网络均使用LeakyRelu作为激活函数
# -*- coding: utf-8 -*- from __future__ import print_function #%matplotlib inline import argparse import os import random import torch import torch.nn as nn import torch.nn.parallel import torch.backends.cudnn as cudnn import torch.optim as optim import torch.utils.data import torchvision.datasets as dset import torchvision.transforms as transforms import torchvision.utils as vutils import numpy as np import matplotlib.pyplot as plt import matplotlib.animation as animation from IPython.display import HTML # 设置随机种子 manualSeed = 999 #manualSeed = random.randint(1, 10000) # 想获取新结果时使用 print("Random Seed: ", manualSeed) random.seed(manualSeed) torch.manual_seed(manualSeed) dataroot = "data/celeba" #数据集的根目录 workers = 2 #载数据的线程数量 batch_size = 128 #训练过程batch大小 image_size = 64 #训练图片大小,所有图片均需要缩放到这个尺寸 nc = 3 #通道数量,通常彩色图就是rgb三个值 nz = 100 #产生网络输入向量的大小 ngf = 64 #产生网络特征层的大小 ndf = 64 #判别网络的特征层的大小 num_epochs = 5 #训练数据集迭代次数 lr = 0.0002 #学习率 beta1 = 0.5 #Adam最优化方法中的超参 beta1 ngpu = 1 #可用的gpu数量(0为cpu模式) # 创建数据集(包含各种初始化) dataset = dset.ImageFolder(root=dataroot, transform=transforms.Compose([ transforms.Resize(image_size), transforms.CenterCrop(image_size), transforms.ToTensor(), transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)), ])) # 创建数据载入器 DataLoader dataloader = torch.utils.data.DataLoader(dataset, batch_size=batch_size, shuffle=True, num_workers=workers) # 设置训练需要的处理器 device = torch.device("cuda:0" if (torch.cuda.is_available() and ngpu > 0) else "cpu")
网络设置
对网络权重进行初始化设置,权重均值为0,标准为0.2
def weights_init(m): classname =m._class_._name_ if classname.find('Conv')!= -1: nn.init.normal_(m.weight.data,0.0,0.02) elif classname.find('BatchNorm')!=-1: nn.init.normal_(m.weight.data,1.0,0.02) nn.init.constant_(m.bias.data,0)
构建产生网络
由于我们的数据是图像,因此随机噪声向量z应该是一个与训练图像大小相同的RGB图像,其大小为3*64*64。产生网络是通过-系列二维反卷积层 +BatchNorm 层 +ReLU 激活层构建的。产生网络的输出经过tanh函数使最终的数据范围为[-1,1]。这里需要注意一下的是,反卷积之后需要有 BatchNorm层(主要是在训练期间有助于深度网络形成更稳定的梯度流)。代码中将输入向量长度nz、特征层大小ngf和输出图片的通道数量 nc都设置为变量。。
构建产生网络的代码具体如下:
# 产生网络代码 class Generator(nn.Module): def __init__(self, ngpu): super(Generator, self).__init__() self.ngpu = ngpu self.main = nn.Sequential( # 输入向量z,通过第一个反卷积 # 将100的向量z输入,输出channel设置为(ngf*8),经过如下操作 # class torch.nn.ConvTranspose2d(in_channels, out_channels, kernel_size, stride=1, padding=0, output_padding=0, groups=1, bias=True, dilation=1) # 后得到(ngf*8) x 4 x 4,即长宽为4,channel为ngf*8的特征层 nn.ConvTranspose2d( nz, ngf * 8, 4, 1, 0, bias=False), #这里的ConvTranspose2d类似于deconv,前面第 章介绍过原理 nn.BatchNorm2d(ngf * 8), nn.ReLU(True), # 继续对特征层进行反卷积,得到长宽为8,channel为ngf*4的特征层 (ngf*4) x 8 x 8 nn.ConvTranspose2d(ngf * 8, ngf * 4, 4, 2, 1, bias=False), nn.BatchNorm2d(ngf * 4), nn.ReLU(True), # 继续对特征层进行反卷积,得到长宽为16,channel为ngf*2的特征层 (ngf*2) x 16 x 16 nn.ConvTranspose2d( ngf * 4, ngf * 2, 4, 2, 1, bias=False), nn.BatchNorm2d(ngf * 2), nn.ReLU(True), # 继续对特征层进行反卷积,得到长宽为32,channel为ngf的特征层 (ngf) x 32 x 32 nn.ConvTranspose2d( ngf * 2, ngf, 4, 2, 1, bias=False), nn.BatchNorm2d(ngf), nn.ReLU(True), # 继续对特征层进行反卷积,得到长宽为64,channel为nc的特征层 (nc) x 64 x 64 nn.ConvTranspose2d( ngf, nc, 4, 2, 1, bias=False), nn.Tanh() ) def forward(self, input): return self.main(input) ################################################################ #### 将产生网络实例化 #### # 创建生成器 netG = Generator(ngpu).to(device) # 处理多gpu情况 if (device.type == 'cuda') and (ngpu > 1): netG = nn.DataParallel(netG, list(range(ngpu))) # 应用weights_init函数对随机初始化进行重置,改为服从mean=0, stdev=0.2的正态分布的初始化 netG.apply(weights_init)
构建判别网络
# 判别网络代码 class Discriminator(nn.Module): def __init__(self, ngpu): super(Discriminator, self).__init__() self.ngpu = ngpu self.main = nn.Sequential( # 输入为一张宽高为64,channel为nc的一张图片,得到宽高为32,channel为ndf的一张图片 (ndf) x 32 x 32 nn.Conv2d(nc, ndf, 4, 2, 1, bias=False), nn.LeakyReLU(0.2, inplace=True), # 经过第2次卷积 得到宽高为16,channel为ndf*2的一张图片 (ndf*2) x 16 x 16 nn.Conv2d(ndf, ndf * 2, 4, 2, 1, bias=False), nn.BatchNorm2d(ndf * 2), #使用大尺度的步长来代替下采样(pooling),这样可以更好地学习降采样的方法 nn.LeakyReLU(0.2, inplace=True), # 经过第3次卷积 得到宽高为8,channel为ndf*4的一张图片 (ndf*4) x 8 x 8 nn.Conv2d(ndf * 2, ndf * 4, 4, 2, 1, bias=False), nn.BatchNorm2d(ndf * 4), nn.LeakyReLU(0.2, inplace=True), # 经过第4次卷积 得到宽高为4,channel为ndf*8的一张图片 (ndf*8) x 4 x 4 nn.Conv2d(ndf * 4, ndf * 8, 4, 2, 1, bias=False), nn.BatchNorm2d(ndf * 8), nn.LeakyReLU(0.2, inplace=True), # 经过第5次卷积并过sigmoid层,得最终一个概率输出值 nn.Conv2d(ndf * 8, 1, 4, 1, 0, bias=False), nn.Sigmoid() #最终通过Sigmoid激活函数输出该张图片是真实图片的概率 ) def forward(self, input): return self.main(input) #### 将判别网络实例化 #### # 创建判别器 netD = Discriminator(ngpu).to(device) # 处理多gpu情况 if (device.type == 'cuda') and (ngpu > 1): netD = nn.DataParallel(netD, list(range(ngpu))) # 应用weights_init函数对随机初始化进行重置,改为服从mean=0, stdev=0.2的正态分布的初始化 netD.apply(weights_init)
定义损失函数
定义损失函数并定义产生网络和判别网络的优化方法。在这里,损失函数使用的是二元交叉熵,优化方法使用的是Adam。定义损失函数的代码具体如下:
#初始化二元交叉熵损失函数 criterion =nn.BCELosS() #创建一个batch大小的向量z,即产生网络的输入数据 fixed_noise =torch.randn(64,nz,1,1,device=device) #定义训练过程的真图片/假图片的标签 real_label =1 fake_label =0 #为产生网络和判别网络设置Adam优化器 optimizerD = optim.Adam(netD.parameters(),lr=lr, betas=(betal, 0.999)) optimizerG = optim.Adam(netG.parameters(),lr=lr, betas=(betal,0.999))
训练过程
为了更好地训练GAN,这里对于真图片和假图片构建不同的batch并且分别进行训练。整个训练分为两个部分:第一部分更新判别网络,第二部分更新产生网络。训练过程的代码具体如下:
# 训练过程:主循环 img_list = [] G_losses = [] D_losses = [] iters = 0 print("Starting Training Loop...") for epoch in range(num_epochs): # 训练集迭代的次数 for i, data in enumerate(dataloader, 0): #循环每个dataloader中的batch ############################ # (1) 更新判别网络:最大化 log(D(x)) + log(1 - D(G(z))) ########################### ## 用全部都是真图片的batch训练 netD.zero_grad() # 格式化batch real_cpu = data[0].to(device) b_size = real_cpu.size(0) label = torch.full((b_size,), real_label, device=device) # 将带有正样本的batch,输入到判别网络 中进行前向计算,得到结果放到变量output中 output = netD(real_cpu).view(-1) # 计算loss errD_real = criterion(output, label) # 计算梯度 errD_real.backward() D_x = output.mean().item() ## 用全部都是假图片的batch训练 # 先产生网络的输入向量 noise = torch.randn(b_size, nz, 1, 1, device=device) # 通过产生网络生成假的样本图片 fake = netG(noise) label.fill_(fake_label) # 将生成的全部假图片输入到判别网络中进行前向计算,得到结果放到变量output中 output = netD(fake.detach()).view(-1) # 在假图片batch中计算刚刚判别网络的loss errD_fake = criterion(output, label) # 计算该batch的梯度 errD_fake.backward() D_G_z1 = output.mean().item() # 将真图片与假图片的误差加和 errD = errD_real + errD_fake # 更新判别网络D optimizerD.step() ############################ # (2) 更新产生网络: 最大化 log(D(G(z))) ########################### netG.zero_grad() label.fill_(real_label) # 产生网络的标签是真实的图片 # 由于刚刚更新了判别网络,这里让假数据再过一遍判别网络,用来计算产生网络的loss并回传 output = netD(fake).view(-1) errG = criterion(output, label) errG.backward() D_G_z2 = output.mean().item() # 更新产生网络G optimizerG.step() # 打印训练状态 if i % 50 == 0: print('[%d/%d][%d/%d]\tLoss_D: %.4f\tLoss_G: %.4f\tD(x): %.4f\tD(G(z)): %.4f / %.4f' % (epoch, num_epochs, i, len(dataloader), errD.item(), errG.item(), D_x, D_G_z1, D_G_z2)) # 保存loss,用于后续画图 G_losses.append(errG.item()) D_losses.append(errD.item()) # 保留产生网络生成的图片,后续用来看生成的图片效果 if (iters % 500 == 0) or ((epoch == num_epochs-1) and (i == len(dataloader)-1)): with torch.no_grad(): fake = netG(fixed_noise).detach().cpu() img_list.append(vutils.make_grid(fake, padding=2, normalize=True)) iters += 1
LSGAN
使用最小二乘损失函数代替 DCGAN中的交叉熵损失函数。虽然交叉熵损失能使得网络进行正确分类,但它仅仅关心分类是否正确,而不关心距离(也就是生成的假图片与真实图片之间的差别有多大)。这就使得一些生成的假图片,虽然它们仍然距离真实数据分布有着较大的差距,但由于它们骗过了判别器,因此不会再进入后续的迭代优化。所以,我们直观上看到的就是,只要能骗过判别器,即使假图片的质量不高其也不会继续优化。LSGAN的想法就是将决策边界作为中间媒介,将那些远离决策面的样本尽量拖进决策边界(这里假定真数据和假数据的距离是由它们和决策边界的距离来反映的)。当然,最直观的方法也可以直接尝试将生成的数据拉向真实数据(不通过决策边界这个媒介)
WGAN
1)去掉判别器最后一层的 Sigmoid。
2)生成网络和判别网络的损失不取log。
3)每次更新判别网络的参数后,将它们强制截断到指定范围。
4)推荐使用RMSProp或者SGD的方式进行优化,而不是DCGAN中的momentum或Adam 方法
GAN训练不稳定的原因:
我们假定有两个分布,一个是真实数据的分布,一个是产生器生成假数据的分布,训练的过程就是尽可能地让第二组假数据的分布接近第一组真实数据的分布。由于产生网络是从低维白噪声产生图片数据,因此实际上两组分布很难有交叠,就算有一定的交叠,比例也很小。这时候、如果判别器训练得特别好,那么对于本来就没有交叠的两组而言,分布梯度就会消失。但是、如果判别器训练得不好,就会使优化的方向产生偏差。因此,需要得到一个“不好也不坏”的判别器,这就导致让GAN很好地收敛是很困难的。
2)另外、GAN本身的多样性并不好,因为一旦生成器生成的假样本被判别器发现惩罚会很大(通过KL散度分析而得,感兴趣的读者可以参看论文),因此生成器会比较向于生成一些类似但是“安全”的样本,而不是冒风险生成多样性的样本。
3)没有办法判断生成器的好坏
PG-GAN
它的核心思想是从低分辨率图像开始,逐渐增大生成器和判别器网络、添加更高分辨率需要的细节,从而得到高清的图片。
1)使用平滑的方式增加训练过程图像的分辨率
2)为了提升生成样本的多样性(避免mode colapse问题),使得判别器的后几个特征层x与一个多样性度量变量y结合到一起作为下一层的输人,而y的定义为:先计算当前 batch的标准差得到一个二维的数组,然后再对这个二维数组求均值,该均值即为当前batch 的多样性度量变量y。
3)用卷积加上采样来代替反卷积,去掉产生器的tanh函数
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· .NET10 - 预览版1新功能体验(一)