第G4周:CGAN|生成手势图像 | 可控制生成

思维导图如下

python 复制代码
# ==================== 世界最详细注释版:CGAN手势生成代码(高中生友好版) ====================
# 🌟 核心思想:就像教一个"造假高手"(生成器)和一个"鉴宝专家"(判别器)玩猫鼠游戏!
# 🌟 条件控制:给造假高手一张"任务卡"(比如"画石头"),它必须按要求造假;鉴宝专家也看任务卡判断真假
# 🌟 本代码目标:让AI学会生成"石头/剪刀/布"三种手势图像,并能按指令生成指定手势

# 第一步:解决Windows系统常见报错(技术细节,高中生可跳过)
# 原因:多个科学计算库同时调用CPU多线程功能时的小冲突
# 解决方案:告诉系统"允许重复初始化"(安全临时方案)
import os
os.environ["KMP_DUPLICATE_LIB_OK"] = "TRUE"  # ✅ 这行必须放在最最最前面!

# 第二步:导入所有需要的"工具包"(就像厨师准备厨具)
import torch  # PyTorch:深度学习核心框架(AI的大脑)
import numpy as np  # NumPy:处理数字矩阵的瑞士军刀
import torch.nn as nn  # nn:神经网络积木库(搭积木造AI)
import torch.optim as optim  # optim:优化器(帮AI快速进步的教练)
from torchvision import datasets, transforms  # 处理图像数据的工具
from torchvision.utils import save_image, make_grid  # 保存和拼接图片的工具
import matplotlib.pyplot as plt  # 画图工具(生成损失曲线图)
import warnings  # 警告管理器

# ==================== 【重要修正】模型定义必须放在超参数之后! ====================
# ❗ 原始代码错误:模型类定义时用了还没声明的变量(n_classes等)
# ✅ 修正方案:所有模型定义移到__main__内,超参数定义之后(见下方详细注释)

if __name__ == '__main__':
    # =============== 0. 基础设置(准备工作)===============
    warnings.filterwarnings("ignore")  # 忽略非关键警告(让输出更清爽)
    
    # 设置中文显示(避免图片标题出现乱码□□□)
    plt.rcParams['font.sans-serif'] = ['SimHei']  # 使用黑体字体
    plt.rcParams['axes.unicode_minus'] = False   # 正确显示负号
    plt.rcParams['figure.dpi'] = 100             # 图片清晰度
    
    # 创建保存结果的文件夹(像整理书包:图片放images,模型放training_weights)
    os.makedirs('./images', exist_ok=True)           # exist_ok=True:如果文件夹已存在也不报错
    os.makedirs('./training_weights', exist_ok=True)
    print("📁 已创建结果保存文件夹:./images 和 ./training_weights")

    # 检测设备:有NVIDIA显卡用GPU(超快!),没有用CPU(稍慢但能跑)
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    print(f"🚀 使用设备: {device}(GPU加速能快10倍以上!)")

    # =============== 1. 超参数设置(AI训练的"规则说明书")===============
    batch_size = 128      # 每次喂给AI看128张图片(像一次批改128份作业)
    latent_dim = 100      # 随机噪声的长度(100个随机数 = 造假高手的"灵感源泉")
    n_classes = 3         # 手势类别数:0=石头(rock), 1=布(paper), 2=剪刀(scissors)
    embedding_dim = 100   # 类别信息的"翻译长度"(把"石头"翻译成100维数字密码)
    learning_rate = 0.0002 # 学习速度(太大学不会,太慢效率低,0.0002是黄金值)
    num_epochs = 100      # 训练轮数(看完整数据集100遍,像复习100次课本)

    # =============== 2. 数据预处理(给图片"化妆"标准化)===============
    train_transform = transforms.Compose([
        transforms.Resize(128),  # 把所有图片缩放到128x128像素(统一尺寸)
        transforms.ToTensor(),   # 转成PyTorch能处理的数字矩阵
        # 归一化:把像素值[0,255] → [-1,1](让AI计算更稳定)
        # 公式:(像素值/255 - 0.5) / 0.5
        transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5]) 
    ])

    # =============== 3. 加载手势数据集(准备"教科书")===============
    try:
        # 从"data/rps"文件夹读取图片(需提前准备!)
        # 文件夹结构必须是:
        # data/rps/
        #   ├── rock/      (放所有"石头"手势图片)
        #   ├── paper/     (放所有"布"手势图片)
        #   └── scissors/  (放所有"剪刀"手势图片)
        train_dataset = datasets.ImageFolder(root="data/rps", transform=train_transform)
        print(f"📚 成功加载数据集!共 {len(train_dataset)} 张手势图片")
        print(f"   类别说明: {train_dataset.classes} → [0=石头, 1=布, 2=剪刀]")
    except FileNotFoundError:
        raise FileNotFoundError(
            "❌ 数据集路径 'data/rps' 不存在!请按以下步骤操作:\n"
            "1️⃣ 创建文件夹: data/rps/rock, data/rps/paper, data/rps/scissors\n"
            "2️⃣ 每个文件夹放入至少100张对应手势的图片(手机拍即可)\n"
            "3️⃣ 确保图片是.jpg或.png格式\n"
            "💡 小技巧:可用手机拍自己做手势的照片,每类拍50张以上!"
        )

    # 打包数据:每次取batch_size张图片,打乱顺序(避免AI死记硬背)
    train_loader = torch.utils.data.DataLoader(
        dataset=train_dataset,
        batch_size=batch_size,
        shuffle=True,
        num_workers=0 if os.name == 'nt' else 4  # Windows系统设为0避免卡死
    )

    # =============== 4. 可视化示例(看看"教科书"长啥样)===============
    def show_images(dl):
        """从数据集中取64张图片拼成网格图保存"""
        for images, labels in dl:  # 取第一批数据
            # make_grid:把64张小图拼成8x8大图;permute:调整颜色通道顺序
            grid = make_grid(images[:64], nrow=8).permute(1, 2, 0)
            plt.figure(figsize=(10, 10))
            plt.axis('off')  # 隐藏坐标轴
            # 反归一化:把[-1,1]变回[0,1]才能正常显示
            plt.imshow(grid.numpy() * 0.5 + 0.5)  
            plt.savefig('./images/sample_batch.png', bbox_inches='tight')
            plt.close()
            print("🖼️  已保存示例图片:./images/sample_batch.png(快去打开看看!)")
            break  # 只看第一批

    show_images(train_loader)

    # =============== 5. 模型定义(核心!造假高手 vs 鉴宝专家)===============
    # 🔑 重要:所有模型定义必须放在超参数之后!否则会报错"n_classes未定义"
    
    def weights_init(m):
        """
        【权重初始化】给神经网络"热身"
        比喻:就像运动员赛前拉伸,避免训练时抽筋
        - 卷积层:用微小随机数初始化(标准差0.02)
        - BN层:权重设为1附近,偏置设为0
        """
        classname = m.__class__.__name__
        if classname.find('Conv') != -1:  # 如果是卷积层
            nn.init.normal_(m.weight, 0.0, 0.02)  # 生成均值为0、标准差0.02的随机数
        elif classname.find('BatchNorm') != -1:  # 如果是批归一化层
            nn.init.normal_(m.weight, 1.0, 0.02)
            nn.init.zeros_(m.bias)  # 偏置初始化为0

    class Generator(nn.Module):
        """
        【生成器 = 造假高手】
        输入:100个随机数(灵感) + 手势类别(任务卡:画"石头")
        输出:一张128x128的RGB手势图片
        工作流程:
        1️⃣ 把"任务卡"(类别标签)翻译成数字密码(Embedding)
        2️⃣ 把随机数"灵感"展开成512x4x4的特征图
        3️⃣ 把任务卡密码(1x4x4)拼到特征图旁边 → 513x4x4
        4️⃣ 用"反卷积"层层放大:4x4 → 8x8 → 16x16 → ... → 128x128
        5️⃣ 最后用Tanh把像素值压缩到[-1,1](符合归一化要求)
        """
        def __init__(self):
            super(Generator, self).__init__()
            # 【任务卡翻译器】把类别标签(0/1/2)转成16维向量
            self.label_conditioned_generator = nn.Sequential(
                nn.Embedding(n_classes, embedding_dim),  # 词嵌入:把数字标签变向量
                nn.Linear(embedding_dim, 16)             # 线性变换压缩到16维
            )
            # 【灵感处理】把100维随机噪声变512x4x4的"草图"
            self.latent = nn.Sequential(
                nn.Linear(latent_dim, 4 * 4 * 512),
                nn.LeakyReLU(0.2, inplace=True)  # 激活函数:让网络有非线性能力
            )
            # 【放大器】把4x4小图逐步放大到128x128
            self.model = nn.Sequential(
                # 每层:反卷积 → 批归一化 → 激活函数
                # 反卷积步长2:尺寸翻倍(4→8→16→32→64→128)
                nn.ConvTranspose2d(513, 512, 4, 2, 1, bias=False),  # 输入513=512(灵感)+1(任务卡)
                nn.BatchNorm2d(512, momentum=0.1, eps=0.8),  # 稳定训练
                nn.ReLU(True),
                nn.ConvTranspose2d(512, 256, 4, 2, 1, bias=False),
                nn.BatchNorm2d(256, momentum=0.1, eps=0.8),
                nn.ReLU(True),
                nn.ConvTranspose2d(256, 128, 4, 2, 1, bias=False),
                nn.BatchNorm2d(128, momentum=0.1, eps=0.8),
                nn.ReLU(True),
                nn.ConvTranspose2d(128, 64, 4, 2, 1, bias=False),
                nn.BatchNorm2d(64, momentum=0.1, eps=0.8),
                nn.ReLU(True),
                nn.ConvTranspose2d(64, 3, 4, 2, 1, bias=False),  # 输出3通道(RGB)
                nn.Tanh()  # 像素值压缩到[-1, 1]
            )

        def forward(self, inputs):
            """前向传播:输入→输出的完整流程"""
            noise_vector, label = inputs  # 拆分输入:随机噪声 + 类别标签
            # 处理任务卡:标签 → 16维向量 → 变成1x4x4的小图
            label_output = self.label_conditioned_generator(label)
            label_output = label_output.view(-1, 1, 4, 4)  # 重塑形状
            # 处理灵感:100维噪声 → 512x4x4特征图
            latent_output = self.latent(noise_vector)
            latent_output = latent_output.view(-1, 512, 4, 4)
            # 拼接:把任务卡小图(1通道)贴到灵感图(512通道)旁边 → 513通道
            concat = torch.cat((latent_output, label_output), dim=1)
            # 送入放大器生成最终图片
            return self.model(concat)

    class Discriminator(nn.Module):
        """
        【判别器 = 鉴宝专家】
        输入:一张图片 + 手势类别(任务卡:检查是否为"石头")
        输出:一个概率值(0~1,越接近1表示越像真图)
        工作流程:
        1️⃣ 把"任务卡"翻译成128x128的彩色图(和输入图一样大)
        2️⃣ 把任务卡图和输入图上下叠在一起 → 6通道图(原图3通道+任务卡3通道)
        3️⃣ 用卷积层层压缩:128x128 → 64x64 → ... → 小特征向量
        4️⃣ 最后输出一个概率值(Sigmoid:把结果压到0~1之间)
        """
        def __init__(self):
            super(Discriminator, self).__init__()
            # 【任务卡可视化】把类别标签变128x128的"提示图"
            self.label_condition_disc = nn.Sequential(
                nn.Embedding(n_classes, embedding_dim),
                nn.Linear(embedding_dim, 3 * 128 * 128)  # 输出=3通道×128×128像素
            )
            # 【鉴定器】压缩图片提取特征
            self.model = nn.Sequential(
                # 每层:卷积 → 激活 → (批归一化)
                # 卷积步长2/3:尺寸缩小(128→64→21→7→2)
                nn.Conv2d(6, 64, 4, 2, 1, bias=False),  # 输入6通道=3(图片)+3(任务卡图)
                nn.LeakyReLU(0.2, inplace=True),  # LeakyReLU:负数也有微小梯度
                nn.Conv2d(64, 128, 4, 3, 2, bias=False),
                nn.BatchNorm2d(128, momentum=0.1, eps=0.8),
                nn.LeakyReLU(0.2, inplace=True),
                nn.Conv2d(128, 256, 4, 3, 2, bias=False),
                nn.BatchNorm2d(256, momentum=0.1, eps=0.8),
                nn.LeakyReLU(0.2, inplace=True),
                nn.Conv2d(256, 512, 4, 3, 2, bias=False),
                nn.BatchNorm2d(512, momentum=0.1, eps=0.8),
                nn.LeakyReLU(0.2, inplace=True),
                nn.Flatten(),        # 拉平成一维向量
                nn.Dropout(0.4),     # 随机丢弃40%神经元(防过拟合,像随机抽查)
                nn.Linear(4608, 1),  # 全连接到1个输出
                nn.Sigmoid()         # 压缩到0~1(概率值)
            )

        def forward(self, inputs):
            img, label = inputs  # 拆分输入:图片 + 类别标签
            # 处理任务卡:标签 → 3×128×128的"提示图"
            label_output = self.label_condition_disc(label)
            label_output = label_output.view(-1, 3, 128, 128)
            # 拼接:原图(3通道) + 任务卡图(3通道) → 6通道
            concat = torch.cat((img, label_output), dim=1)
            # 送入鉴定器输出真假概率
            return self.model(concat)

    # 损失函数:二元交叉熵(判断"是/否"问题的标准损失)
    def generator_loss(fake_output, label):
        """生成器希望判别器把假图判为真(label=1)"""
        return nn.BCELoss()(fake_output, label)
    
    def discriminator_loss(output, label):
        """判别器希望:真图判为真(1),假图判为假(0)"""
        return nn.BCELoss()(output, label)

    # =============== 6. 初始化模型(召唤造假高手和鉴宝专家)===============
    generator = Generator().to(device).apply(weights_init)  # 造假高手
    discriminator = Discriminator().to(device).apply(weights_init)  # 鉴宝专家
    print("🤖 模型初始化完成!")
    print(f"   生成器参数量: {sum(p.numel() for p in generator.parameters()):,}")
    print(f"   判别器参数量: {sum(p.numel() for p in discriminator.parameters()):,}")

    # =============== 7. 优化器(给两位配专属教练)===============
    # Adam优化器:智能调整学习速度(比普通SGD更高效)
    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))
    print("🏋️ 优化器配置完成(两位专家的私人教练已就位)")

    # =============== 8. 训练循环(核心!猫鼠游戏开始)===============
    print("\n" + "="*60)
    print("🎮 CGAN训练启动!石头剪刀布手势生成器")
    print("💡 游戏规则:")
    print("   1️⃣ 造假高手按'任务卡'生成手势图")
    print("   2️⃣ 鉴宝专家对比真图/假图+任务卡,判断真假")
    print("   3️⃣ 两位互相较劲,越练越强!")
    print("="*60 + "\n")
    
    D_loss_plot, G_loss_plot = [], []  # 记录损失变化(画进步曲线用)

    for epoch in range(1, num_epochs + 1):
        D_epoch_loss, G_epoch_loss = 0.0, 0.0
        num_batches = len(train_loader)

        for real_images, labels in train_loader:
            batch_size_curr = real_images.size(0)
            real_images = real_images.to(device)
            # labels原为[batch],需变为[batch,1]匹配模型输入
            labels = labels.unsqueeze(1).long().to(device)

            # 创建真假标签(用于计算损失)
            real_target = torch.ones(batch_size_curr, 1, device=device)   # 真图标签=1
            fake_target = torch.zeros(batch_size_curr, 1, device=device)  # 假图标签=0

            # ========== 第一回合:训练判别器(强化鉴宝专家)==========
            D_optimizer.zero_grad()  # 清空上轮梯度(避免累积错误)
            
            # 1. 用真图+正确任务卡训练:希望输出接近1
            D_real_loss = discriminator_loss(
                discriminator((real_images, labels)),  # 输入:真图+对应标签
                real_target
            )
            
            # 2. 用假图+相同任务卡训练:希望输出接近0
            noise = torch.randn(batch_size_curr, latent_dim, device=device)  # 生成随机灵感
            fake_images = generator((noise, labels))  # 造假高手生成假图
            # .detach():冻结生成器梯度(本轮只训练判别器)
            D_fake_loss = discriminator_loss(
                discriminator((fake_images.detach(), labels)), 
                fake_target
            )
            
            # 3. 综合损失 = (真图损失 + 假图损失)/2
            D_loss = (D_real_loss + D_fake_loss) / 2
            D_loss.backward()      # 反向传播:计算梯度
            D_optimizer.step()     # 更新判别器参数
            D_epoch_loss += D_loss.item()

            # ========== 第二回合:训练生成器(强化造假高手)==========
            G_optimizer.zero_grad()
            # 用假图+任务卡训练:希望判别器输出接近1(骗过专家!)
            G_loss = generator_loss(
                discriminator((fake_images, labels)),  # 注意:这次不detach,要更新生成器
                real_target
            )
            G_loss.backward()
            G_optimizer.step()
            G_epoch_loss += G_loss.item()

        # 计算本epoch平均损失
        D_epoch_loss /= num_batches
        G_epoch_loss /= num_batches
        D_loss_plot.append(D_epoch_loss)
        G_loss_plot.append(G_epoch_loss)

        # 打印进度(每轮显示一次)
        print(f" Epoch [{epoch:3d}/{num_epochs}] | "
              f"📉 判别器损失: {D_epoch_loss:.4f} | "
              f"📈 生成器损失: {G_epoch_loss:.4f} | "
              f"🎯 理想状态:两者接近0.5(势均力敌!)")

        # 每10轮保存成果
        if epoch % 10 == 0:
            # 生成50张测试图(5行10列)
            with torch.no_grad():  # 关闭梯度计算(节省内存)
                test_noise = torch.randn(50, latent_dim, device=device)
                # 随机生成50个类别标签(0/1/2均匀分布)
                test_labels = torch.randint(0, n_classes, (50, 1), device=device).long()
                gen_imgs = generator((test_noise, test_labels))
            # 保存为网格图(normalize=True:自动反归一化显示)
            save_image(gen_imgs.data, f'./images/epoch_{epoch}.png', nrow=10, normalize=True)
            
            # 保存模型(方便以后直接生成图片)
            torch.save(generator.state_dict(), f'./training_weights/generator_epoch_{epoch}.pth')
            torch.save(discriminator.state_dict(), f'./training_weights/discriminator_epoch_{epoch}.pth')
            print(f"✅ 已保存:生成样本(./images/epoch_{epoch}.png) + 模型权重")

    # =============== 9. 训练后可视化(画出进步曲线)===============
    plt.figure(figsize=(10, 4))
    plt.plot(G_loss_plot, label='生成器损失', linewidth=2, color='green')
    plt.plot(D_loss_plot, label='判别器损失', linewidth=2, color='red')
    plt.title('CGAN训练损失曲线', fontsize=16, fontweight='bold')
    plt.xlabel('训练轮数(Epoch)', fontsize=12)
    plt.ylabel('损失值', fontsize=12)
    plt.legend(fontsize=12)
    plt.grid(True, linestyle='--', alpha=0.7)
    plt.savefig('./images/loss_curve.png', bbox_inches='tight', dpi=150)
    plt.close()
    print("\n📊 训练完成!损失曲线已保存:./images/loss_curve.png")
    print("💡 曲线解读:")
    print("   - 两条线逐渐靠近0.5 → 训练成功(势均力敌)")
    print("   - 判别器损失≈0 → 造假高手太菜(一直被识破)")
    print("   - 生成器损失≈0 → 鉴宝专家太菜(真假不分)")

    # =============== 10. 生成指定手势(终极测试!)===============
    print("\n" + "="*60)
    print("🎨 生成指定手势图像(按需定制!)")
    print("="*60)
    
    try:
        # 尝试加载最终模型(epoch 100)
        gen_path = './training_weights/generator_epoch_100.pth'
        if os.path.exists(gen_path):
            generator.load_state_dict(torch.load(gen_path, map_location=device))
            generator.eval()  # 切换到评估模式(关闭Dropout等)
            print(f"✅ 成功加载训练完成的生成器模型")
        else:
            print(f"⚠️  未找到最终模型,使用当前训练权重(请确保训练完成100轮)")

        # 生成指定类别(修改target_class即可:0=石头,1=布,2=剪刀)
        target_class = 0  
        class_names = ["rock(石头)", "paper(布)", "scissors(剪刀)"]
        
        with torch.no_grad():
            noise = torch.randn(1, latent_dim, device=device)  # 1个随机灵感
            label = torch.tensor([[target_class]], device=device, dtype=torch.long)
            gen_img = generator((noise, label))
            # 反归一化:[-1,1] → [0,255](转为正常图片)
            img_np = gen_img.squeeze().permute(1, 2, 0).cpu().numpy()
            img_np = (img_np * 0.5 + 0.5) * 255
            img_np = np.clip(img_np, 0, 255).astype(np.uint8)  # 限制在0-255

        # 保存结果
        plt.figure(figsize=(5, 5))
        plt.imshow(img_np)
        plt.title(f'✨ 生成的手势: {class_names[target_class]}', fontsize=14, pad=20)
        plt.axis('off')
        save_path = f'./images/generated_{class_names[target_class].split()[0]}.png'
        plt.savefig(save_path, bbox_inches='tight', dpi=150)
        plt.close()
        
        print(f"\n✅ 成功生成 {class_names[target_class]} 手势!")
        print(f"   保存路径: {save_path}")
        print(f"💡 小实验:修改代码中 target_class = 1 或 2,重新运行即可生成'布'或'剪刀'!")
        print("\n🎉 恭喜!你已掌握条件生成对抗网络(CGAN)核心原理与实践!")
        print("   下一步建议:")
        print("   1️⃣ 用手机拍自己的手势扩充数据集")
        print("   2️⃣ 尝试修改网络结构(如增加层数)")
        print("   3️⃣ 用训练好的模型做'手势猜拳'小游戏!")

    except Exception as e:
        print(f"\n❌ 生成图像时出错: {str(e)}")
        print("🔍 排查建议:")
        print("   - 检查是否完成至少10轮训练(需有保存的模型)")
        print("   - 确认target_class值在0~2之间")
        print("   - 查看错误信息定位具体问题")

    # =============== 附:CGAN原理通俗总结(高中生秒懂版)===============
    """
    🌰 举个栗子:教AI画"指定水果"
    - 传统GAN:给AI一盒蜡笔(随机噪声),让它随便画水果 → 可能画出苹果/香蕉/橘子(无法控制)
    - CGAN:给AI蜡笔 + 一张"任务卡"(写着"画苹果")
        • 生成器:必须按任务卡画苹果
        • 判别器:同时看画作和任务卡,判断"这张画真的是苹果吗?"
    - 结果:训练完成后,你给"画香蕉"任务卡,AI立刻生成香蕉!
    
    💡 本项目应用:
        任务卡 = "石头/剪刀/布" → AI按指令生成对应手势
        应用场景:游戏开发、手势识别数据增强、虚拟助手等
    
    📚 知识延伸(来自知识库):
    • CGAN是GAN的"条件版"(2014年提出),把无监督学习转为有监督学习
    • 变体家族:DCGAN(用卷积网络提升质量)、InfoGAN(控制更多属性)等
    • 实际应用:老照片修复、视频预测、风格迁移(如把照片变梵高风格)
    """

生成的最后结果如下:

相关推荐
工程师老罗4 小时前
Pytorch完整的模型训练流程
人工智能·pytorch·深度学习
工程师老罗4 小时前
优化器、反向传播、损失函数之间是什么关系,Pytorch中如何使用和设置?
人工智能·pytorch·python
果粒蹬i6 小时前
基于PyTorch的CNN-BiLSTM时序预测:从数据滑窗到模型部署
人工智能·pytorch·cnn
工程师老罗6 小时前
Pytorch新版本如何使用现有网络模型,怎么修改现有模型
人工智能·pytorch·深度学习
2301_764441337 小时前
基于python与PyQt5对本地部署Qwen3-ASR的7B模型语音转文本
pytorch·python·qt
工程师老罗7 小时前
Pytorch中的损失函数及其用法
人工智能·pytorch·python
工程师老罗8 小时前
Pytorch如何保存和加载模型
人工智能·pytorch·python
大模型玩家七七19 小时前
微调与安全隐私:为什么微调会放大风险
数据库·人工智能·pytorch·python·深度学习·安全
大模型玩家七七1 天前
向量数据库实战:从建库到第一次翻车
数据库·人工智能·pytorch·python·深度学习·oracle