目录
一、引言
生成对抗网络(Generative Adversarial Network),相信想学习人工智能的大家对gan耳熟能详,gan是一种机器学习模型,由两个主要部分组成:生成器(Generator)和判别器(Discriminator)。生成器试图生成看起来像真实样本的数据,而判别器则试图区分生成器生成的数据和真实数据。它们通过对抗训练的方式相互竞争和提升,最终使得生成器能够生成足够逼真的数据,以至于判别器无法轻易区分生成的数据和真实的数据
对gan相关知识的讲解的博客非常多,笔者这里就不赘述,直接上代码讲解。
二、数据集
数据集仍然采用上一个博客中介绍的Fashion-MNIST,这里也不在介绍,具体可以看上一篇文章。
链接:CNN+FashionMNIST+分类+代码详解-CSDN博客
数据集获取链接:GitHub - zalandoresearch/fashion-mnist: A MNIST-like fashion product database. Benchmark
三、环境配置
首先是笔者使用的IDE为PyCharm2022.1.3专业版。
主要的虚拟环境的配置为:
- python 3.9
- torch 2.0.1+cu117
- torchaudio 2.0.2+cu117
- torchvision 0.15.2+cu117
- numpy 1.26.4
- pillow 10.4.0
虚拟环境最重要的是兼容性,电脑可以使用cuda的最好安装编译时与 CUDA 兼容的PyTorch,可以加快训练和推理的速度。
四、准备代码
1、包的导入
import os
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
import numpy as np
import matplotlib
matplotlib.use('TkAgg') # 或者 'Qt5Agg'
import matplotlib.pyplot as plt
import torchvision
from torchvision import transforms
2、数据集准备
# 定义复杂的预处理过程
transform = transforms.Compose([
transforms.ToTensor(), # 转换为张量
transforms.Normalize((0.5,), (0.5,)) # 归一化到[-1, 1]
])
train_ds = torchvision.datasets.FashionMNIST('data',
train=True,
transform=transform,
download=True) # 定义Fashion_MNIST数据集
dataloader = torch.utils.data.DataLoader(train_ds, batch_size=64, shuffle=True) # 加载数据集,打乱,batch_size设置为64
1)预处理:
-
transforms.Compose: 这是一个用于组合多个数据预处理操作的函数。
-
transforms.ToTensor(): 这个操作将PIL图像或numpy.ndarray转换为PyTorch的张量(tensor)。这是深度学习框架中常用的数据表示方式。
-
transforms.Normalize((0.5,), (0.5,)): 这个操作将图像张量进行归一化处理。对于每个通道,它将张量的数值从[0, 1]范围归一化到[-1, 1]范围。具体地,它执行以下转换:
input[channel] = (input[channel] - mean[channel]) / std[channel]
,其中mean是0.5,std是0.5。
2)加载数据集
train_ds: 使用torchvision.datasets.FashionMNIST
创建了一个FashionMNIST的训练数据集实例。传入的参数包括:
'data'
: 指定数据集存储的位置。train=True
: 表示加载训练集数据。transform=transform
: 将之前定义的transform
应用到数据集中的每个样本上,即在加载数据时进行预处理。
dataloader: 使用torch.utils.data.DataLoader
创建了一个数据加载器,用于实现数据的批处理和打乱顺序。具体参数包括:
train_ds
: 上面定义的FashionMNIST训练数据集。batch_size=64
: 指定每个批次加载的样本数为64。shuffle=True
: 表示在每个epoch开始时,对数据进行随机打乱,以增加训练过程的随机性,有助于模型的泛化能力。
五、Gan的主要结构
1、生成器
#定义生成器
class Generator(nn.Module):
def __init__(self):
super(Generator, self).__init__()
self.main = nn.Sequential(
nn.Linear(100, 256),
nn.LeakyReLU(),
nn.Linear(256, 512),
nn.LeakyReLU(),
nn.Linear(512, 28 * 28),
nn.Tanh() # 对于-1, 1之间的数据分布,Tanh效果最好。输出的取值范围是-1,1之间
)
def forward(self, x): # 前向传播,x 表示长度为100 的noise输入
img = self.main(x) # 将x输入到main模型中 得到img
img = img.view(-1, 28, 28) # 通过view函数reshape成(-1,28,28,1)
return img
输入是长度为100的正态分布随机数
-
nn.Linear: 这些层定义了全连接神经网络的线性变换。在这里,Generator包含三个线性层:
- 第一个
nn.Linear(100, 256)
接受一个长度为100的噪声向量作为输入,并输出一个大小为256的中间表示。 - 第二个
nn.Linear(256, 512)
将256维的中间表示映射到一个更大的512维空间。 - 第三个
nn.Linear(512, 28*28)
最后将512维的向量映射到一个长度为784的向量,对应于28x28像素的图像展开为784个像素值。
- 第一个
-
nn.LeakyReLU: 在每个线性层后面添加了LeakyReLU:激活函数,用于引入非线性特性,LeakyReLU是对ReLU的改进,通过引入一个小的负斜率来避免神经元死亡问题。。
-
nn.Tanh: 最后一层使用Tanh激活函数,将输出限制在[-1, 1]的范围内。这是因为MNIST数据集中的像素值通常在[0, 1]之间,因此将生成器的输出限制在[-1, 1]有助于匹配输入数据的范围。
2、判别器
class Discriminator(nn.Module):
def __init__(self):
super(Discriminator, self).__init__() # 继承父类的属性
self.main = nn.Sequential(
nn.Linear(28 * 28, 512), # 输入一张图片28,8,然后展平成28*28,再卷积到256
nn.BatchNorm1d(256), # 批标准化层
nn.LeakyReLU(),
nn.Linear(512, 256),
nn.BatchNorm1d(512), # 批标准化层
nn.LeakyReLU(),
nn.Linear(256, 1),
nn.Sigmoid()
)
def forward(self, x): # x输入的是28,28的图片
x = x.view(-1, 28 * 28) # 展平
x = self.main(x)
return x
输入为生成器生成的28*28的图片,输出为判别器对其打的分数,即输出为二分类的概率值,输出使用sigmoid激活 0-1。
1)初始化方法 (__init__
):
self.main
: 使用nn.Sequential
定义了判别器的主要神经网络结构,包含了几个线性层和激活函数层。
2)神经网络结构 (self.main
):
nn.Linear(28 * 28, 512)
: 输入层到第一个隐藏层,将28x28大小的图像展平成784维向量,然后映射到512维的隐藏表示。nn.LeakyReLU(0.2)
: LeakyReLU激活函数,斜率为0.2。LeakyReLU相比ReLU能够在输入为负时引入小的梯度,避免神经元死亡问题。nn.Linear(512, 256)
: 第二个隐藏层,将512维的隐藏表示映射到256维。nn.LeakyReLU(0.2)
: LeakyReLU激活函数。nn.Linear(256, 1)
: 输出层,将256维的向量映射到一个标量,用于表示真假概率。nn.Sigmoid()
: Sigmoid激活函数,将输出限制在[0, 1]之间,表示概率值,用于对二分类结果进行概率化输出。
3)前向传播方法 (forward
):
- 接收输入
x
,大小为(batch_size, 1, 28, 28),其中batch_size
是批处理大小。 x.view(-1, 28 * 28)
: 将输入的图像张量展平成一维向量,大小为(batch_size, 784)。self.main(x)
: 将展平后的向量输入到判别器的主模型中,得到输出结果,大小为(batch_size, 1)。
3、优化器和损失函数
#初始化模型、优化器及损失计算函数
device = 'cuda' if torch.cuda.is_available() else 'cpu'#默认使用cuda,否则cpu
#%%
gen = Generator().to(device)#初始化Generator模型
dis = Discriminator().to(device)#初始化Discriminator模型
#%%
d_optim = torch.optim.Adam(dis.parameters(), lr=0.0001)#定义优化器,学习率
g_optim = torch.optim.Adam(gen.parameters(), lr=0.0001)
# 定义损失函数
loss_fn = torch.nn.BCELoss()#二分类判别模型
1)设备选择
torch.cuda.is_available()
: 检查当前系统是否支持CUDA。- 如果支持CUDA,则将
device
设为'cuda',表示使用GPU加速;否则设为'cpu',使用CPU。
2)初始化生成器和判别器模型
Generator()
和Discriminator()
分别是之前定义的生成器和判别器模型。.to(device)
: 将模型移动到指定的设备,这里根据前面检测到的CUDA是否可用,选择将模型放在GPU上或者CPU上进行运算。
3)定义优化器
torch.optim.Adam
: 使用Adam优化算法dis.parameters()
和gen.parameters()
: 分别传入判别器和生成器模型的参数,用于优化器更新模型参数。lr=0.0001
: 学习率设置为0.0001,控制每次参数更新的步长大小。
4)定义损失函数
torch.nn.BCELoss()
: 二分类交叉熵损失函数,用于计算生成器和判别器在二分类任务中的损失。- 在GAN中,判别器的目标是最小化真实样本和生成样本之间的差别,生成器的目标是最大化判别器无法区分真实样本和生成样本的能力。BCELoss是常用的损失函数之一,适合处理二分类问题。
4、随机正态分布
#定义输入随机正态分布
test_input = torch.randn(16, 100, device=device)#生成长度为100的一个批次16张的随机噪声输入
六、绘图函数
#绘图函数
def gen_img_plot(model, test_input,epoch, save_path):#每次都给一个同样的test_input正态分布随机数
prediction = np.squeeze(model(test_input).detach().cpu().numpy())#detach用来截断梯度,放到cpu上,转换为numpy,squeeze用于去掉维度为一的值,鲁棒性更高===>28*28的数组
fig = plt.figure(figsize=(4, 4))#绘制16张图片
for i in range(16):#循环
plt.subplot(4, 4, i+1)#四行四列的第一张
plt.imshow((prediction[i] + 1)/2)#转换成0,1之间的数值(预测的结果恢复到0,1之间
plt.axis('off')#关闭
# plt.show()
plt.savefig(save_path.format(epoch))
plt.close(fig)
1)形参
def gen_img_plot(model, test_input,epoch, save_path)
model
: 输入生成器模型对象,用于生成图像。test_input
: 输入生成器的测试输入,长度为100的正态分布随机数。epoch
: 当前的epoch数,用于保存生成的图像时命名文件。save_path
: 保存图像的路径格式,包含占位符{}
用于插入epoch数。
2)模型预测
prediction = np.squeeze(model(test_input).detach().cpu().numpy())
model(test_input)
: 调用生成器模型,输入test_input
,生成图像的预测结果。.detach().cpu().numpy()
: 将生成的张量从计算图中分离,移动到CPU上,并转换为NumPy数组。np.squeeze()
: 去除数组中维度为1的维度,使得数组的维度更加符合预期(从 (1, 28, 28) 变为 (28, 28))。
3)绘图过程
for i in range(16):#循环
plt.subplot(4, 4, i+1)#四行四列的第一张
plt.imshow((prediction[i] + 1)/2)#转换成0,1之间的数值(预测的结果恢复到0,1之间
plt.axis('off')#关闭
# plt.show()
fig = plt.figure(figsize=(4, 4))
: 创建一个大小为 4x4 英寸的图像。for i in range(16):
: 循环绘制16张图像。plt.subplot(4, 4, i+1)
: 在4行4列的布局中,绘制第i+1张图像。plt.imshow((prediction[i] + 1) / 2)
: 显示预测的图像,prediction[i]
是生成器预测的第i张图像,+1
并除以2是将生成的图像数据从[-1, 1]范围恢复到[0, 1]范围。plt.axis('off')
: 关闭坐标轴显示。
4)保存图像
plt.savefig(save_path.format(epoch))
plt.close(fig)
plt.savefig(save_path.format(epoch))
: 将当前绘制的图像保存到指定路径,并根据当前epoch数命名文件。plt.close(fig)
: 关闭当前图像,释放资源。
七、Gan的训练
1、训练判别器
# 训练循环
for epoch in range(20): # 训练20轮
d_epoch_loss = 0
g_epoch_loss = 0 # 初始化损失函数为0
count = len(dataloader) # 返回批次数,len(dataset)返回样本数
for step, (img, _) in enumerate(dataloader): # _表示标签,这里生成模型用不到,enumerate用于对dataloader迭代
img = img.to(device) # 将照片上传到设备上
size = img.size(0) # 获批次大小根据这个大小来输入我们随机噪声的输入大小
random_noise = torch.randn(size, 100, device=device) # 生成噪声随机数,大小个数是size
# 训练判别器
d_optim.zero_grad() # 将梯度归0
real_output = dis(img) # 判别器输入真实的图片,real_output对真实图片的预测结果 真实图片为1,假图片为0
d_real_loss = loss_fn(real_output,
torch.ones_like(real_output)) # 得到判别器在真实图像上的损失 ones_like:全1数组
d_real_loss.backward() # 反向传播,计算梯度
gen_img = gen(random_noise)
# 判别器输入生成的图片,fake_output对生成图片的预测
fake_output = dis(
gen_img.detach()) # 这里阶段梯度是因为,这里通过对判别器输入生成图片去计算损失是用来优化判别器的。对生成器的参数暂时不做优化。所以梯度不用再传递到生成器模型当中了,我们希望fake_output被判定为0
d_fake_loss = loss_fn(fake_output,
torch.zeros_like(fake_output)) # 得到判别器在生成图像上的损失,zeros_like:全0数组
d_fake_loss.backward() # 同样计算梯度
d_loss = d_real_loss + d_fake_loss # 判别器的总损失(两部分)
d_optim.step() # 进行优化
2、训练生成器
# 训练生成器
g_optim.zero_grad() # 梯度归零
fake_output = dis(gen_img) # 将生成图片放到判别器当中--不要梯度截断
g_loss = loss_fn(fake_output, # 我们这里就希望fake_output被判定为1用来优化生成器
torch.ones_like(fake_output)) # 生成器的损失
g_loss.backward() # 计算梯度
g_optim.step() # 权重优化
with torch.no_grad(): # 两个模型的损失函数做累加(不需要计算梯度)---每个批次累加==一个epoch
d_epoch_loss += d_loss
g_epoch_loss += g_loss
3、计算平均loss并画图
with torch.no_grad(): # 得到平均loss
d_epoch_loss /= count
g_epoch_loss /= count
D_loss.append(d_epoch_loss.item())
G_loss.append(g_epoch_loss.item()) # 这样列表当中会保存每个epoch的平均loss
print('Epoch:', epoch) # 打印当前epoch
# 保存每个epoch的图片
save_path = './show/the generated images_2/epoch_{}.png'
os.makedirs(save_path, exist_ok=True) # 自动创建文件夹,如果不存在的话
gen_img_plot(gen, test_input, epoch, save_path)
# gen_img_plot(gen, test_input) # 绘图
4、保存模型
os.makedirs("./model_2", exist_ok=True) # 自动创建文件夹,如果不存在的话
torch.save(gen.state_dict(), './model_2/generator.pth')
torch.save(dis.state_dict(), './model_2/discriminator.pth')
print("Models saved.")
八、生成结果
九、总结
从上面的图中可以看出,从刚开始的全是随机的的噪声形成的图片,最后经过10轮训练之后衣物的形状和轮廓已经比较清晰了,二十轮训练之后,Gan生成的图片已经非常清晰了,衬衫,裤子,靴子已经非常明显,说明我们的Gan网络是搭建成功了。
在训练gan的时候有几个注意的点,首先是预处理阶段不可以加上随机旋转或者是翻转图片的函数,然后是损失函数可以选择更加合理的Wasserstein GAN(WGAN)损失函数,但是对生成器得有一定的更改。最后还可以对生成器和鉴别器的网络进行优化,有可能可以实现更加优秀的效果。
十、后续
后续可能文章的计划安排:
Mamba、loss function等等;
语音领域相关基础知识;
还可能会分享一些电路方面的内容。
敬请期待!