- 🍨 本文为🔗365天深度学习训练营 中的学习记录博客
- 🍖 原作者:K同学啊
这段程序实现了一个条件生成对抗网络(Conditional GAN,简称CGAN),用于生成指定类别的石头剪刀布手势图像。与标准GAN不同,CGAN在生成器和判别器中都加入了条件标签信息(这里是三种手势类别),使得生成过程可以受控于特定类别。程序使用PyTorch框架,包含生成器和判别器两个核心组件:生成器将随机噪声向量和类别标签映射为128×128的RGB图像;判别器则同时接收图像和标签,判断图像的真实性。训练过程中,两者相互对抗优化------生成器试图生成以假乱真的图像欺骗判别器,而判别器则努力区分真假图像。训练完成后,程序还展示了通过在潜在空间插值生成的图像序列,直观展示了不同类别间图像的平滑过渡。整体上,这是一个完整的深度生成模型实现案例,涵盖了数据准备、模型构建、训练循环和结果可视化全流程。思维导图如下:

python
# 这是一个生成手势图像的程序,使用了CGAN(条件生成对抗网络)技术
# 我们将用最简单易懂的方式解释每个部分,就像给初中生讲道理一样
# 第一步:导入所有需要的工具(就像准备做菜需要各种厨具一样)
import torch # 一个强大的AI计算库,就像计算器但更厉害
import numpy as np # 一个处理数字和数组的工具
import torch.nn as nn # 用于构建神经网络的工具
import torch.optim as optim # 用于优化神经网络的工具
from torchvision import datasets, transforms # 用于获取图像数据和处理图像
from torch.autograd import Variable # 用于自动计算梯度(AI学习时的"思考"过程)
from torchvision.utils import save_image # 用于保存生成的图像
from torchvision.utils import make_grid # 用于将多张图像排成网格
from torch.utils.tensorboard import SummaryWriter # 用于可视化训练过程(就像看进度条)
from torchsummary import summary # 用于显示神经网络的结构
import matplotlib.pyplot as plt # 用于绘制图表
import datetime # 用于记录时间
# 设置随机种子,确保每次运行结果相同(就像做实验要重复一样)
torch.manual_seed(1)
# 设置设备:如果电脑有GPU(图形处理器),就用GPU加速计算;否则用CPU
# GPU就像超级计算器,能更快地处理AI任务
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
batch_size = 128 # 每次处理128张图片(就像一次处理128个苹果)
# 第二步:准备图像数据(就像准备做菜的食材)
# 定义图像处理步骤:
# 1. 将图像调整为128x128像素(就像把照片裁剪成统一大小)
# 2. 将图像转换为数字数组(电脑能理解的形式)
# 3. 将每个像素值归一化到-1到1之间(让数据更"整齐")
train_transform = transforms.Compose([
transforms.Resize(128), # 调整大小
transforms.ToTensor(), # 转换为张量(数字数组)
transforms.Normalize([0.5,0.5,0.5], [0.5,0.5,0.5]) # 归一化
])
# 加载手势图像数据集(从文件夹中读取)
# 这里假设我们有一个名为"rps"的文件夹,里面有石头、剪刀、布的手势图片
train_dataset = datasets.ImageFolder(root='./data/rps/', transform=train_transform)
# 创建数据加载器,可以方便地一次取一批图片
train_loader = torch.utils.data.DataLoader(dataset=train_dataset,
batch_size=batch_size,
shuffle=True, # 打乱顺序,避免AI只记住特定顺序
num_workers=6) # 使用6个进程同时处理数据
# 第三步:定义一些实用函数(就像给AI准备一些工具)
# 显示多张图片的函数
def show_images(images):
# 创建一个大画布(20x20英寸)
fig, ax = plt.subplots(figsize=(20, 20))
# 隐藏坐标轴(让图片看起来更干净)
ax.set_xticks([]); ax.set_yticks([])
# 将多张图片组合成一个大网格
ax.imshow(make_grid(images.detach(), nrow=22).permute(1, 2, 0))
# 显示一批图片的函数
def show_batch(dl):
for images, _ in dl: # 从数据加载器中取出一批图片
show_images(images) # 显示这些图片
break # 只显示一批就停止
# 第四步:定义模型参数(就像定义菜谱的食材和调料)
image_shape = (3, 128, 128) # 图像的形状:3个颜色通道(RGB),128x128像素
image_dim = int(np.prod(image_shape)) # 计算图像总像素数(3*128*128)
latent_dim = 100 # 潜在空间的维度(AI用来生成图片的"创意空间"大小)
n_classes = 3 # 手势类别数:石头、剪刀、布
embedding_dim = 100 # 类别嵌入维度(将类别标签转换为数字向量)
# 第五步:定义权重初始化函数(就像给AI的"大脑"做准备)
# 这个函数会为神经网络的每个层设置合适的初始值
def weights_init(m):
# 获取当前层的名称
classname = m.__class__.__name__
# 如果是卷积层(负责识别图像特征的层)
if classname.find('Conv') != -1:
# 用正态分布初始化权重(让AI开始时有点"随机性")
torch.nn.init.normal_(m.weight, 0.0, 0.02)
# 如果是批归一化层(负责让AI训练更稳定)
elif classname.find('BatchNorm') != -1:
# 用正态分布初始化权重(均值为1,标准差为0.02)
torch.nn.init.normal_(m.weight, 1.0, 0.02)
# 将偏置项初始化为0(让AI从"零"开始)
torch.nn.init.zeros_(m.bias)
# 第六步:定义生成器(Generator) - 生成图片的"艺术家"
class Generator(nn.Module):
def __init__(self):
super(Generator, self).__init__()
# 定义条件标签生成器:将类别标签转换为向量
# 例如:将"石头"转换为一个100维的数字向量
self.label_conditioned_generator = nn.Sequential(
nn.Embedding(n_classes, embedding_dim), # 类别嵌入层
nn.Linear(embedding_dim, 16) # 将嵌入向量转换为16维向量
)
# 定义潜在向量生成器:将随机噪声转换为图像特征
# 潜在向量就像AI的"创意种子"
self.latent = nn.Sequential(
nn.Linear(latent_dim, 4 * 4 * 512), # 将100维噪声转换为512x4x4的特征图
nn.LeakyReLU(0.2, inplace=True) # 激活函数,让AI更"聪明"
)
# 定义生成器的主要结构:将条件信息和潜在向量转换为图像
self.model = nn.Sequential(
# 反卷积层1:将513维输入转换为64x8x8的特征图
nn.ConvTranspose2d(513, 64 * 8, 4, 2, 1, bias=False),
nn.BatchNorm2d(64 * 8, momentum=0.1, eps=0.8), # 批归一化
nn.ReLU(True), # 激活函数
# 反卷积层2:将64x8x8转换为64x4x4
nn.ConvTranspose2d(64 * 8, 64 * 4, 4, 2, 1, bias=False),
nn.BatchNorm2d(64 * 4, momentum=0.1, eps=0.8),
nn.ReLU(True),
# 反卷积层3:将64x4x4转换为64x2x2
nn.ConvTranspose2d(64 * 4, 64 * 2, 4, 2, 1, bias=False),
nn.BatchNorm2d(64 * 2, momentum=0.1, eps=0.8),
nn.ReLU(True),
# 反卷积层4:将64x2x2转换为64x1x1
nn.ConvTranspose2d(64 * 2, 64 * 1, 4, 2, 1, bias=False),
nn.BatchNorm2d(64 * 1, momentum=0.1, eps=0.8),
nn.ReLU(True),
# 反卷积层5:将64x1x1转换为3x64x64(RGB图像)
nn.ConvTranspose2d(64 * 1, 3, 4, 2, 1, bias=False),
nn.Tanh() # 激活函数,将像素值限制在[-1, 1]之间
)
# 前向传播:定义输入如何转换为输出
def forward(self, inputs):
noise_vector, label = inputs # 输入:随机噪声和类别标签
# 将类别标签转换为嵌入向量
label_output = self.label_conditioned_generator(label)
# 调整向量形状,使其适合与潜在向量合并
label_output = label_output.view(-1, 1, 4, 4)
# 将噪声转换为潜在向量
latent_output = self.latent(noise_vector)
# 调整向量形状
latent_output = latent_output.view(-1, 512, 4, 4)
# 将类别信息和潜在向量合并
concat = torch.cat((latent_output, label_output), dim=1)
# 通过生成器的主要结构生成图像
image = self.model(concat)
return image
# 创建生成器并移动到GPU(如果可用)
generator = Generator().to(device)
# 应用权重初始化
generator.apply(weights_init)
print(generator) # 打印生成器结构
# 使用torchinfo显示生成器结构(更详细)
from torchinfo import summary
summary(generator)
# 第七步:定义判别器(Discriminator) - 评判图片真假的"裁判"
class Discriminator(nn.Module):
def __init__(self):
super(Discriminator, self).__init__()
# 定义条件标签嵌入层:将类别标签转换为特征向量
self.label_condition_disc = nn.Sequential(
nn.Embedding(n_classes, embedding_dim), # 类别嵌入层
nn.Linear(embedding_dim, 3 * 128 * 128) # 将嵌入向量转换为与图像匹配的尺寸
)
# 定义主要的判别器模型:判断图片是否真实
self.model = nn.Sequential(
# 卷积层1:输入6通道(3通道图像+3通道标签),输出64通道
nn.Conv2d(6, 64, 4, 2, 1, bias=False),
nn.LeakyReLU(0.2, inplace=True), # 激活函数
# 卷积层2:输入64通道,输出128通道
nn.Conv2d(64, 64 * 2, 4, 3, 2, bias=False),
nn.BatchNorm2d(64 * 2, momentum=0.1, eps=0.8), # 批归一化
nn.LeakyReLU(0.2, inplace=True),
# 卷积层3:输入128通道,输出256通道
nn.Conv2d(64 * 2, 64 * 4, 4, 3, 2, bias=False),
nn.BatchNorm2d(64 * 4, momentum=0.1, eps=0.8),
nn.LeakyReLU(0.2, inplace=True),
# 卷积层4:输入256通道,输出512通道
nn.Conv2d(64 * 4, 64 * 8, 4, 3, 2, bias=False),
nn.BatchNorm2d(64 * 8, momentum=0.1, eps=0.8),
nn.LeakyReLU(0.2, inplace=True),
nn.Flatten(), # 将特征图展平为一维向量
nn.Dropout(0.4), # 随机丢弃40%的神经元,防止过拟合
nn.Linear(4608, 1), # 全连接层,将特征映射到输出
nn.Sigmoid() # 激活函数,将输出限制在0-1之间(表示真假概率)
)
# 前向传播:定义输入如何转换为输出
def forward(self, inputs):
img, label = inputs # 输入:图像和类别标签
# 将类别标签转换为特征向量
label_output = self.label_condition_disc(label)
# 调整形状,使其与图像尺寸匹配
label_output = label_output.view(-1, 3, 128, 128)
# 将图像和标签特征合并
concat = torch.cat((img, label_output), dim=1)
# 通过判别器模型进行判断
output = self.model(concat)
return output
# 创建判别器并移动到GPU
discriminator = Discriminator().to(device)
# 应用权重初始化
discriminator.apply(weights_init)
print(discriminator) # 打印判别器结构
# 使用torchinfo显示判别器结构
summary(discriminator)
# 第八步:定义损失函数和优化器
adversarial_loss = nn.BCELoss() # 二元交叉熵损失函数(用于判断真假)
# 生成器损失函数:生成器希望判别器认为生成的图片是真实的
def generator_loss(fake_output, label):
gen_loss = adversarial_loss(fake_output, label)
return gen_loss
# 判别器损失函数:判别器希望真实图片被识别为真实,假图片被识别为假
def discriminator_loss(output, label):
disc_loss = adversarial_loss(output, label)
return disc_loss
# 设置学习率(AI学习的速度)
learning_rate = 0.0002
# 创建优化器(用于更新神经网络参数)
G_optimizer = optim.Adam(generator.parameters(), lr=learning_rate, betas=(0.5, 0.999))
D_optimizer = optim.Adam(discriminator.parameters(), lr=learning_rate, betas=(0.5, 0.999))
# 第九步:训练循环(AI学习的过程)
num_epochs = 100 # 训练100轮
D_loss_plot, G_loss_plot = [], [] # 用于存储每轮的损失
# 开始训练
for epoch in range(1, num_epochs + 1):
D_loss_list, G_loss_list = [], [] # 用于存储每轮的损失
# 遍历训练数据
for index, (real_images, labels) in enumerate(train_loader):
# 清空判别器梯度(准备更新参数)
D_optimizer.zero_grad()
# 将数据移动到GPU(如果可用)
real_images = real_images.to(device)
labels = labels.to(device)
# 调整标签形状
labels = labels.unsqueeze(1).long()
# 创建真实目标和假目标(用于损失计算)
real_target = Variable(torch.ones(real_images.size(0), 1).to(device))
fake_target = Variable(torch.zeros(real_images.size(0), 1).to(device))
# 计算判别器对真实图片的损失
D_real_loss = discriminator_loss(discriminator((real_images, labels)), real_target)
# 生成假图片(AI的"创作")
noise_vector = torch.randn(real_images.size(0), latent_dim, device=device)
generated_image = generator((noise_vector, labels))
# 计算判别器对假图片的损失
output = discriminator((generated_image.detach(), labels))
D_fake_loss = discriminator_loss(output, fake_target)
# 计算判别器总体损失
D_total_loss = (D_real_loss + D_fake_loss) / 2
D_loss_list.append(D_total_loss)
# 反向传播并更新判别器参数
D_total_loss.backward()
D_optimizer.step()
# 清空生成器梯度
G_optimizer.zero_grad()
# 计算生成器损失
G_loss = generator_loss(discriminator((generated_image, labels)), real_target)
G_loss_list.append(G_loss)
# 反向传播并更新生成器参数
G_loss.backward()
G_optimizer.step()
# 打印当前轮次的损失
print('Epoch: [%d/%d]: D_loss: %.3f, G_loss: %.3f' % (
(epoch), num_epochs, torch.mean(torch.FloatTensor(D_loss_list)),
torch.mean(torch.FloatTensor(G_loss_list))))
# 保存损失
D_loss_plot.append(torch.mean(torch.FloatTensor(D_loss_list)))
G_loss_plot.append(torch.mean(torch.FloatTensor(G_loss_list)))
# 每10轮保存一次结果
if epoch % 10 == 0:
# 保存生成的图片
save_image(generated_image.data[:50], './images/sample_%d' % epoch + '.png', nrow=5, normalize=True)
# 保存模型参数
torch.save(generator.state_dict(), './training_weights/generator_epoch_%d.pth' % (epoch))
torch.save(discriminator.state_dict(), './training_weights/discriminator_epoch_%d.pth' % (epoch))
# 第十步:加载训练好的模型并生成图片
generator.load_state_dict(torch.load('./training_weights/generator_epoch_100.pth'), strict=False)
generator.eval() # 设置为评估模式
# 第十一步:生成插值图片(从一种手势过渡到另一种)
# 生成潜在空间的点(AI的"创意种子")
def generate_latent_points(latent_dim, n_samples, n_classes=3):
x_input = randn(latent_dim * n_samples) # 从正态分布生成随机数
z_input = x_input.reshape(n_samples, latent_dim) # 调整形状
return z_input
# 在两个潜在点之间进行线性插值
def interpolate_points(p1, p2, n_steps=10):
ratios = linspace(0, 1, num=n_steps) # 生成插值比率
vectors = list()
for ratio in ratios:
v = (1.0 - ratio) * p1 + ratio * p2 # 线性插值
vectors.append(v)
return asarray(vectors)
# 生成两个潜在点
pts = generate_latent_points(100, 2)
# 在两点之间进行插值
interpolated = interpolate_points(pts[0], pts[1])
# 将数据转换为张量
interpolated = torch.tensor(interpolated).to(device).type(torch.float32)
output = None
# 为每个类别生成插值图片
for label in range(3):
# 创建类别标签
labels = torch.ones(10) * label
labels = labels.to(device)
labels = labels.unsqueeze(1).long()
# 生成图片
predictions = generator((interpolated, labels))
predictions = predictions.permute(0,2,3,1) # 调整维度
pred = predictions.detach().cpu() # 移回CPU
# 合并结果
if output is None:
output = pred
else:
output = np.concatenate((output, pred))
# 创建画布并显示图片
nrow = 3 # 行数
ncol = 10 # 列数
fig = plt.figure(figsize=(15,4))
gs = gridspec.GridSpec(nrow, ncol)
k = 0
for i in range(nrow):
for j in range(ncol):
# 将像素值从[-1, 1]转换回[0, 255]
pred = (output[k, :, :, :] + 1) * 127.5
pred = np.array(pred)
ax = plt.subplot(gs[i,j])
ax.imshow(pred.astype(np.uint8)) # 显示图片
ax.set_xticklabels([]) # 隐藏坐标轴
ax.set_yticklabels([])
ax.axis('off') # 关闭坐标轴
k += 1
plt.show() # 显示最终结果
最终显示如下:
