DCGAN的原理(附代码解读)

时间:2024-10-22 21:35:09

学习DCGAN之前需要了解一下转置卷积 可以参考学DCGAN对抗网络之前--转置卷积(附代码解读)-****博客

1.DCGAN对于GAN的改进之处

  1. 网络架构的优化
    • DCGAN在生成器和判别器中明确使用了卷积层和卷积转置层(也称为反卷积层或分数阶卷积层)。这一改变使得网络能够更好地捕捉图像的空间特征,从而生成更高质量的图像。
    • 生成器中使用转置卷积层进行上采样,而判别器中使用标准的卷积层进行下采样。这种设计有助于保持图像的空间结构,减少模糊和伪影。
  2. 训练稳定性的提升
    • DCGAN在每一层之后都使用了Batch Normalization(BN)层,这有助于处理初始化不良导致的训练问题,加速模型训练,并提升训练的稳定性。
    • 在生成器中,除输出层使用Tanh或Sigmoid激活函数外,其余层全部使用ReLU激活函数。而在判别器中,所有层都使用LeakyReLU激活函数,以防止梯度消失的问题。
  3. 生成图像质量的改善
    • 通过使用深度卷积结构和优化的训练策略,DCGAN能够生成更高分辨率、更清晰和更逼真的图像。这得益于其改进的网络架构和训练稳定性。

2.网络模型

        1.生成器网络模型

使用到了转置卷积 将一个100个长度的正态分布噪声经过转置卷积之后生成一张3*64*64的图片

        2.判别器网络模型

使用到了卷积 将3*64*64大小的图片经过多次卷积之后变成一个1*1*1大小的概率值

3.DCGAN的设计细节

1. 取消所有pooling层,G网络中使用转置卷积(transposed convolutional layer)进行上采样,D网络中加入stride的卷积(为防止梯度稀疏)代替pooling。

2. 去掉FC层(全连接),使网络变成全卷积网络。

3. G网络中使用Relu作为激活函数,最后一层用Tanh。

4. D网络中使用LeakyRelu激活函数。

5. 在generator和discriminator上都使用batchnorm,解决初始化差的问题,帮助梯度传播到每一层,防止generator把所有的样本都收敛到同一点。直接将BN应用到所有层会导致样本震荡和模型不稳定,因此在生成器的输出层和判别器的输入层不使用BN层,可以防止这种现象。

6. 使用Adam优化器。

4.代码解读

DCGAN和GAN代码的区别主要体现在生成器和判别器的定义中 训练什么的都大差不差

1.数据加载

数据加载的代码和GAN的代码一样可参考:GAN对抗网络(代码详细解读)_gan 生成对抗网络-****博客

代码如下

import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
import numpy as np
import  matplotlib.pyplot as plt
import torchvision
from torchvision import transforms
 
#对数据做归一化处理 -1 - 1 之间 这和Gan训练最后要用tanh函数有关 tanh函数取值范围就是-1 - 1之间
transforms =  transforms.Compose([
    #0-1归一化:将图像中的像素值从原范围(通常是 [0, 255] 或者具体的设备特定范围)转换到 [0, 1]的范围内,方便神经网络模型训练,加速收敛。
    #通道顺序调整:图像通常以 HWC(Height, Width, Channel)的形式存在
    #ToTensor() 函数会将通道(color channels)移动到最前面,变成标准的 CHW(Channel, Height, Width)格式,这是PyTorch张量的标准格式。
    transforms.ToTensor(),
    # mean:均值 std:方差  可以将数据转换成-1 - 1之间的数据
    transforms.Normalize(0.5,0.5)
])
 
#加载内置数据集
train_ds = torchvision.datasets.MNIST(root='E:\Pc项目\pythonProjectlw\数据集',
                                      train=True,
                                      transform=transforms,
                                      download=True)
 
dataloader = torch.utils.data.DataLoader(train_ds,batch_size=64,shuffle=True)

2.生成器模型代码

定义生成器(使用转置卷积进行传播)

#定义生成器
class Generator(nn.Module):
    def __init__(self):
        super(Generator,self).__init__()
        self.linear1 = nn.Linear(100, 7*7*256)
        self.bn1 = nn.BatchNorm1d(7*7*256)
        self.deconv1 = nn.ConvTranspose2d(256,128,
                                          kernel_size=(3,3),
                                          stride=1,
                                          padding=1)
        self.bn2 = nn.BatchNorm2d(128)
        self.deconv2 = nn.ConvTranspose2d(128,64,
                                          kernel_size=(4,4),
                                          stride=2,
                                          padding=1)
        self.bn3 = nn.BatchNorm2d(64)
        self.deconv3 = nn.ConvTranspose2d(64,1,
                                          kernel_size=(4,4),
                                          stride=2,
                                          padding=1)

1.self.linear1 = nn.Linear(100, 256 * 7 * 7):

定义了一个全连接层(也称为线性层),输入特征数为100,输出特征数为256 * 7 * 7。这通常是将一个随机噪声向量(大小为100)映射到一个更大的空间,以便后续的反卷积层可以将其转换为图像。

2.self.bn1 = nn.BatchNorm1d(256 * 7 * 7):

定义了一个一维批量归一化层,用于对linear1层的输出进行归一化,以加速训练过程并提高模型的稳定性。

3.self.decon1 = nn.ConvTranspose2d(in_channels=256, out_channels=128, kernel_size=(3, 3), stride=1, padding=1) # (128, 7, 7):

定义了一个反卷积层(也称为转置卷积层),用于将特征图的大小上采样(放大)。输入通道数为256,输出通道数为128,卷积核大小为3x3,步长为1,填充为1。注释中的(128, 7, 7)表示该层输出特征图的通道数和空间维度(假设输入特征图的空间维度也是7x7)。

定义生成器的前向传播


    def forward(self,x):
        x = F.relu(self.linear1(x))
        x = self.bn1(x)
        #维度维7*7*256的唯一向量 拉成长和宽都为7*7 通道数为256的图像 torch.size([64,256,7,7])
        x = x.view(-1,256,7,7)
        x = F.relu(self.deconv1(x))
        x =self.bn2(x)
        x = F.relu(self.deconv2(x))
        x = self.bn3(x)
        x = torch.tanh(self.deconv3(x))
        return x

1.x = F.relu(self.linear1(x)):

通过linear1层传递输入x,并应用ReLU激活函数。

2.x = self.bn1(x):

linear1层的输出进行批量归一化。

3.x = x.view(-1, 256, 7, 7):

bn1层的输出重塑为四维张量,以便可以传递给反卷积层。-1表示自动计算该维度的大小。

4.x = torch.tanh(self.decon3(x)):

通过decon3层传递归一化后的x,并应用tanh激活函数。tanh函数将输出值限制在-1到1之间,这对于生成图像数据是有用的。

为什么要对linear层的输出进行归一化?

对于linear层(一个全连接层),其输出可能会因为权重和偏置的随机初始化以及输入数据的分布差异而具有较大的方差。这种较大的方差可能会导致后续层的输入分布发生显著变化,从而影响模型的训练效果。通过对linear1层的输出进行归一化,可以使其输出保持在一个相对稳定的分布范围内,有助于后续层更好地学习和泛化。

具体来说,批量归一化层会对每个小批量(batch)的数据进行以下操作:

  1. 计算该小批量数据的均值和方差。
  2. 使用这些均值和方差对该小批量数据进行归一化,使其具有零均值和单位方差。
  3. 引入两个可学习的参数(缩放因子和偏移量),以便在归一化的基础上进行微调,以保持模型的表示能力。

3.判别器模型代码

定义判别器(使用卷积进行传播)

#定义判别器
class Discriminator(nn.Module):
    def __init__(self):
        super(Discriminator,self).__init__()
        self.conv1 = nn.Conv2d(in_channels=1, out_channels=64, kernel_size=3, stride=2)
        self.conv2 = nn.Conv2d(in_channels=64, out_channels=128, kernel_size=3, stride=2)
        self.bn = nn.BatchNorm2d(128)
        self.fc = nn.Linear(128*6*6,1)

1.self.conv1 = nn.Conv2d(in_channels=1, out_channels=64, kernel_size=3, stride=2):

  • 这行代码定义了一个二维卷积层conv1nn.Conv2d是PyTorch中用于二维卷积的类。
    • in_channels=1:表示输入图像的通道数为1,这通常用于灰度图像。
    • out_channels=64:表示输出特征图的通道数为64。
    • kernel_size=3:表示卷积核的大小为3x3。
    • stride=2:表示卷积时的步长为2,这会导致输出特征图的尺寸减半。

2.self.bn = nn.BatchNorm2d(128):

  • 这行代码定义了一个二维批归一化层bnnn.BatchNorm2d是PyTorch中用于二维数据的批归一化类。
    • 128:表示归一化的特征图通道数为128,这与conv2的输出通道数相同。

3.self.fc = nn.Linear(128*6*6,1):

  • 这行代码定义了一个全连接层fcnn.Linear是PyTorch中用于全连接的类。
    • 128*6*6:这是输入特征的数量。由于conv2的输出特征图在经过步长为2的两次卷积后,其高度和宽度都会减半(假设输入图像的高度和宽度均为28,则conv2的输出特征图尺寸为6x6),且通道数为128,因此输入特征的数量为12866。
    • 1:表示输出特征的数量,即判别器的输出是一个标量,用于表示输入图像是真实图像的概率(在GAN的上下文中)。

定义判别器的前向传播

    def forward(self,x):
        x = F.dropout2d(F.leaky_relu(self.conv1(x)),p = 0.3)
        x = F.dropout2d(F.leaky_relu(self.conv2(x)),p = 0.3)
        x = self.bn(x)
        x = x.view(-1,128*6*6)
        x = torch.sigmoid(self.fc(x))
        return x

1.x = F.dropout2d(F.leaky_relu(self.conv1(x)), p=0.3):

  • 这行代码执行了几个操作:
    • self.conv1(x):首先,数据x通过第一个卷积层conv1
    • F.leaky_relu(...):然后,卷积层的输出通过Leaky ReLU激活函数。Leaky ReLU是ReLU激活函数的一个变体,它在负输入值上有一个小的非零梯度,这有助于缓解ReLU的“死亡神经元”问题。
    • F.dropout2d(..., p=0.3):最后,Leaky ReLU的输出通过二维Dropout层。Dropout是一种正则化技术,用于减少神经网络过拟合。p=0.3表示在每个训练步骤中,每个元素有30%的概率被随机置零。

2.x = self.bn(x):

  • 这行代码将Dropout层的输出通过批归一化层bn

3.x = x.view(-1, 128*6*6):

  • 这行代码使用view方法重新塑形数据x-1表示自动计算该维度的大小,以保持数据的总元素数量不变。128*6*6是期望的输出形状,其中128是通道数,6x6是特征图的高度和宽度(假设输入图像经过两次步长为2的卷积后尺寸减半两次,且输入图像大小适合此假设)。这一步是为了准备数据以便输入到全连接层fc

4.x = torch.sigmoid(self.fc(x)):

  • 这行代码首先将数据x通过全连接层fc,然后通过sigmoid激活函数。Sigmoid函数将输出压缩到0和1之间,这在二分类问题中很有用,例如,在GAN中,判别器需要输出一个概率值来表示输入图像是真实图像的概率。

4.代码总览

剩下的代码还有分别初始化判别器和生成器的优化器,损失计算函数,绘图函数,GAN的训练

代码和GAN的代码一样 具体代码请参考GAN对抗网络(代码详细解读)_gan 生成对抗网络-****博客

对代码有详细的解读

以下是完整的代码:

import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
import numpy as np
import matplotlib.pyplot as plt
import torchvision
from torch import le
from torchvision import transforms
from torch.utils.data import DataLoader

# 对数据做归一化处理 -1 - 1 之间 这和Gan训练最后要用tanh函数有关 tanh函数取值范围就是-1 - 1之间
transforms = transforms.Compose([

    # 0-1归一化:将图像中的像素值从原范围(通常是 [0, 255] 或者具体的设备特定范围)转换到 [0, 1] 的范围内,方便神经网络模型训练,加速收敛。
    # 通道顺序调整:图像通常以 HWC(Height, Width, Channel)的形式存在
    # ToTensor() 函数会将通道(color channels)移动到最前面,变成标准的 CHW(Channel, Height, Width)格式,这是PyTorch张量的标准格式。
    transforms.ToTensor(),
    # mean:均值 std:方差  可以将数据转换成-1 - 1之间的数据
    transforms.Normalize(0.5, 0.5)
])

# 加载内置数据集
train_ds = torchvision.datasets.MNIST(root='E:\Pc项目\pythonProjectlw\数据集',
                                      train=True,
                                      transform=transforms,
                                      download=True)

train_dl = torch.utils.data.DataLoader(train_ds, batch_size=64, shuffle=True)

#定义生成器
class Generator(nn.Module):
    def __init__(self):
        super(Generator,self).__init__()
        self.linear1 = nn.Linear(100, 7*7*256)
        self.bn1 = nn.BatchNorm1d(7*7*256)
        self.deconv1 = nn.ConvTranspose2d(256,128,
                                          kernel_size=(3,3),
                                          stride=1,
                                          padding=1)
        self.bn2 = nn.BatchNorm2d(128)
        self.deconv2 = nn.ConvTranspose2d(128,64,
                                          kernel_size=(4,4),
                                          stride=2,
                                          padding=1)
        self.bn3 = nn.BatchNorm2d(64)
        self.deconv3 = nn.ConvTranspose2d(64,1,
                                          kernel_size=(4,4),
                                          stride=2,
                                          padding=1)


    def forward(self,x):
        x = F.relu(self.linear1(x))
        x = self.bn1(x)
        #维度维7*7*256的唯一向量 拉成长和宽都为7*7 通道数为256的图像 torch.size([64,256,7,7])
        x = x.view(-1,256,7,7)
        x = F.relu(self.deconv1(x))
        x =self.bn2(x)
        x = F.relu(self.deconv2(x))
        x = self.bn3(x)
        x = torch.tanh(self.deconv3(x))
        return x


#定义判别器
class Discriminator(nn.Module):
    def __init__(self):
        super(Discriminator,self).__init__()
        self.conv1 = nn.Conv2d(in_channels=1, out_channels=64, kernel_size=3, stride=2)
        self.conv2 = nn.Conv2d(in_channels=64, out_channels=128, kernel_size=3, stride=2)
        self.bn = nn.BatchNorm2d(128)
        self.fc = nn.Linear(128*6*6,1)

    def forward(self,x):
        x = F.dropout2d(F.leaky_relu(self.conv1(x)),p = 0.3)
        x = F.dropout2d(F.leaky_relu(self.conv2(x)),p = 0.3)
        x = self.bn(x)
        x = x.view(-1,128*6*6)
        x = torch.sigmoid(self.fc(x))
        return x

# 初始化模型 优化器及损失计算函数
device = 'cuda' if torch.cuda.is_available() else 'cpu'
# 初始化生成器
gen = Generator().to(device)
# 初始化判别器
dis = Discriminator().to(device)
# 判别器优化器
d_optim = torch.optim.Adam(dis.parameters(), lr=0.0001)
# 生成器优化器
g_optim = torch.optim.Adam(gen.parameters(), lr=0.0001)
# 损失计算函数 二分类计算交叉熵损失的loss函数为BCELoss
loss_fn = torch.nn.BCELoss()


# 定义绘图函数
# model:输入的模型 比如gen或者dis
# test_input:生成器生成模型的时候需要输入一个正态分布随机数
def gen_img_plot(model, test_input):
    # 预测结果
    # squeeze将维度为1的去掉
    prediction = np.squeeze(model(test_input).detach().cpu().numpy())
    # 初始化画布 画16张图片
    fig = plt.figure(figsize=(4, 4))
    for i in range(16):
        # 4行4列 i从0开始 所以i+1
        plt.subplot(4, 4, i + 1)
        # 预测输出结果 生成器最后使用的是tanh函数 范围是-1 - 1 无法绘图 需要转换成0-1之间的 所以( +1)/2
        plt.imshow((prediction[i] + 1) / 2,cmap="gray")
        plt.axis('off')
    plt.show()

# 因为要绘制16张 所以要绘制16*100的张量
test_input = torch.randn(16, 100, device=device)

# GAN训练
# 记录每次epoch产生的loss值
D_loss = []
G_loss = []

# 训练循环 100个epoch
for epoch in range(100):
    # 初始化损失值为0 将每一个批次产生的损失累加到d_epoch_loss中
    # 最后除以总的批次数 能够计算出每一个epoch的平均loss
    d_epoch_loss = 0
    g_epoch_loss = 0
    # len(dataloader):返回批次数   len(dataset):返回样本数
    count = len(train_dl)
    # 对dataloader进行迭代会返回一个批次的图片 根据这个图片的数量创建同样数量的noise 作为genertator的输入
    for step, (img, _) in enumerate(train_dl):
        img = img.to(device)
        # 获取批次的大小
        size = img.size(0)
        random_noise = torch.rand(size, 100, device=device)

        # 判别器训练
        # 判别器训练过程 有两部分损失 一部分是判断为真的损失 一部分是判断为假的损失
        d_optim.zero_grad()

        # 1.真实的损失
        # 判别器输入真实的图片 real_output就是对真实图片的预测结果
        real_output = dis(img)
        # 得到判别器在真实图像上的损失
        # 希望判别器对真实数据的输出real_output的值为1
        # 这个函数是用来衡量真实输出(real_output)与预期值torch.ones_like(real_output)
        # (在这里是与 real_output 形状相同的全1张量)之间的差异
        d_real_loss = loss_fn(real_output,
                              torch.ones_like(real_output))
        # 反向传播求梯度
        d_real_loss.backward()

        # 2.假的损失
        # 先将噪声传入生成器生成图片
        gen_img = gen(random_noise)
        # 判别器输入生成的图片 fake_output就是对生成图片的预测结果
        # 对于生成器产生的损失 优化的目标是判别器 生成器的参数是不需要进行优化的
        # 当训练判别器时,我们希望仅根据判别器自身的参数和输入(真实图像或生成图像)来计算梯度并更新参数
        # 如果生成图像的梯度传递到生成器,那么生成器的参数也会在判别器的训练过程中被更新,这不是我们想要的结果
        # 因此,我们使用.detach()方法来截断梯度,确保它们不会传播回生成器 .detach()会得到一个没有批次的tensor
        fake_output = dis(gen_img.detach())
        d_fake_loss = loss_fn(fake_output,
                              torch.zeros_like(fake_output))
        # 反向传播求梯度
        d_fake_loss.backward()

        # 损失求和
        d_loss = d_real_loss + d_fake_loss
        # 优化并更新参数
        d_optim.step()

        # 生成器的损失优化 只包含一部分
        # 将生成器所有的梯度归零
        g_optim.zero_grad()
        # 将生成器的图片传入到判别器中 没有做梯度的截断
        fake_output = dis(gen_img)
        # 生成器的损失 .ones 因为是对生成器进行优化 生成器想判别为1
        g_loss = loss_fn(fake_output,
                         torch.ones_like(fake_output))
        g_loss.backward()
        g_optim.step()

        # 计算在一个epoch当中总共的g_loss和d_loss
        with torch.no_grad():
            d_epoch_loss += d_loss
            g_epoch_loss += g_loss

    # 计算平均loss值
    with torch.no_grad():
        d_epoch_loss /= count
        g_epoch_loss /= count
        # 传入到列表当中
        D_loss.append(d_epoch_loss)
        G_loss.append(g_epoch_loss)
        # 打印信息
        print('Epoch:', epoch, 'D Loss:', d_epoch_loss, 'G Loss:', g_epoch_loss)
gen_img_plot(gen, test_input)

5.结果图

DCGAN:

GAN:

以上都是训练了5次的结果 可以明显地看到DCGAN的训练结果比GAN好太多了