pytorch实现 用gan生成动漫头像

pytorch设置GPU id

import torch.backends.cudnn as cudnn
os.environ["cuda_visible_devices"]="3" #用第四块gpu跑
cudnn.benchmark=True

总代码

GPU实现

from tqdm import tqdm 
#tqdm是Python中专门用于进度条美化的模块,通过在非while的循环体内嵌入tqdm,可以得到一个能更好展现程序运行过程的提示进度条
import torch
import torchvision as tv
from torch.utils.data import DataLoader 
#PyTorch中数据读取的一个重要接口是torch.utils.data.DataLoader,该接口定义在dataloader.py脚本中,只要是用PyTorch来训练模型基本都会用到该接口,该接口主要用来将自定义的数据读取接口的输出或者PyTorch已有的数据读取接口的输入按照batch size封装成Tensor,后续只需要再包装成Variable即可作为模型的输入
import torch.nn as nn
#torch.nn是专门为神经网络设计的模块化接口。构建于autograd之上,可以用来定义和运行神经网络。
import os
#设置GPU id
import torch.backends.cudnn as cudnn
os.environ["cuda_visible_devices"]="3" #用第四块gpu跑
cudnn.benchmark=True

# config类中定义超参数,
class Config(object):
    """
    定义一个配置类
    """
    # 0.参数调整
    data_path = 'C:/Users/Administrator/Desktop/faces'
    virs = "result" #???
    num_workers = 4  # 多线程
#num_workers是加载数据(batch)的线程数目。当加载batch的时间 < 数据训练的时间,GPU每次训练完都可以直接从CPU中取到next batch的数据,无需额外的等待,因此也不需要多余的worker,即使增加worker也不会影响训练速度。
#当加载batch的时间 > 数据训练的时间,GPU每次训练完都需要等待CPU完成数据的载入,若增加worker,即使worker_1还未就绪,GPU也可以取worker_2的数据来训练。
#仅限单线程训练情况。
    img_size = 96  # 剪切图片的像素大小
    batch_size = 256  # 批处理数量
    max_epoch = 400  # 最大轮次
    lr1 = 2e-4  # 生成器学习率
    lr2 = 2e-4  # 判别器学习率
    beta1 = 0.5  # 正则化系数,Adam优化器参数
    gpu = True # 是否使用GPU运算(建议使用)
    nz = 100  # 噪声维度
    ngf = 64  # 生成器的卷积核个数
    ndf = 64  # 判别器的卷积核个数

    # 1.模型保存路径
    save_path = 'C:/Users/Administrator/Desktop/images' #opt.netg_path生成图片的保存路径
# 判别模型的更新频率要高于生成模型
    d_every = 1  # 每一个batch 训练一次判别器
    g_every = 5  # 每5个batch训练一次生成模型
    save_every = 5  # 每5个epoch保存一次模型
    netd_path = None  #???
    netg_path = None  #???

    # 测试数据(不训练)
    gen_img = "result.png"
    # 选择保存的照片
    #从512张生成的图片中保存最好的64张
    gen_num = 64
    gen_search_num = 512
    gen_mean = 0  # 生成模型的噪声均值
    gen_std = 1  # 噪声方差


# 实例化Config类,设定超参数,并设置为全局参数
opt = Config()


# 定义Generation生成模型,通过输入噪声向量来生成图片
#用转置卷积进行上采样
class NetG(nn.Module):
    # 构建初始化函数,传入opt类
    def __init__(self, opt):
        super(NetG, self).__init__()
        # self.ngf生成器特征图数目
        self.ngf = opt.ngf
        self.Gene = nn.Sequential(
            # 假定输入为opt.nz*1*1维的数据,opt.nz维的向量
            # output = (input - 1)*stride + output_padding - 2*padding + kernel_size
            # 把一个像素点扩充卷积,让机器自己学会去理解噪声的每个元素是什么意思。
            nn.ConvTranspose2d(in_channels=opt.nz, out_channels=self.ngf * 8, kernel_size=4, stride=1, padding=0,
                               bias=False),
# nn.ConvTranspose2d转置卷积
#in_channels(int):输入张量的通道数
#out_channels(int):输出张量的通道数
#kernel_size(int or tuple):卷积核大小
#stride(int or tuple,optional):卷积步长,决定上采样的倍数
#padding(int or tuple, optional):对输入图像进行padding,输入图像尺寸增加2*padding
#bias:是否加上偏置

            nn.BatchNorm2d(self.ngf * 8),
#对由3d数据组成的4d数据(N,C,X,Y)进行批量归一化
            nn.ReLU(inplace=True),
#inplace-是否进行覆盖运算

            # 输入一个4*4*ngf*8
            nn.ConvTranspose2d(in_channels=self.ngf * 8, out_channels=self.ngf * 4, kernel_size=4, stride=2, padding=1,
                               bias=False),
            nn.BatchNorm2d(self.ngf * 4),
            nn.ReLU(inplace=True),

            # 输入一个8*8*ngf*4
            nn.ConvTranspose2d(in_channels=self.ngf * 4, out_channels=self.ngf * 2, kernel_size=4, stride=2, padding=1,
                               bias=False),
            nn.BatchNorm2d(self.ngf * 2),
            nn.ReLU(inplace=True),

            # 输入一个16*16*ngf*2
            nn.ConvTranspose2d(in_channels=self.ngf * 2, out_channels=self.ngf, kernel_size=4, stride=2, padding=1,
                               bias=False),
            nn.BatchNorm2d(self.ngf),
            nn.ReLU(inplace=True),

            # 输入一张32*32*ngf
            nn.ConvTranspose2d(in_channels=self.ngf, out_channels=3, kernel_size=5, stride=3, padding=1, bias=False),

            # Tanh收敛速度快于sigmoid,远慢于relu,输出范围为[-1,1],输出均值为0
            nn.Tanh(),

        )  # 输出一张96*96*3

    #前向传播函数
def forward(self, x):
        return self.Gene(x)


# 构建Discriminator判别器
#用加入stride的卷积代替pooling
class NetD(nn.Module):
    def __init__(self, opt):
        super(NetD, self).__init__()

        self.ndf = opt.ndf
        # DCGAN定义的判别器,生成器没有池化层
        self.Discrim = nn.Sequential(
            # 卷积层
            # 输入通道数in_channels,输出通道数(即卷积核的通道数)out_channels,此处设定filer过滤器有64个,输出通道自然也就是64。
            # 因为对图片作了灰度处理,此处通道数为1,
            # 卷积核大小kernel_size,步长stride,对称填0行列数padding
            # input:(bitch_size, 3, 96, 96),bitch_size = 单次训练的样本量
            # output:(bitch_size, ndf, 32, 32), (96 - 5 +2 *1)/3 + 1 =32
            # LeakyReLu= x if x>0 else nx (n为第一个函数参数),开启inplace(覆盖)可以节省内存,取消反复申请内存的过程
            # LeakyReLu取消了Relu的负数硬饱和问题,是否对模型优化有效有待考证
            nn.Conv2d(in_channels=3, out_channels=self.ndf, kernel_size=5, stride=3, padding=1, bias=False),
#二维卷积,in_channel:输入数据的通道数,例RGB图片通道数为3;
          #out_channel: 输出数据的通道数,这个根据模型调整;
          #kennel_size: 卷积核大小,可以是int,或tuple;kennel_size=2,意味着卷积大小(2,2), kennel_size=(2,3),意味着卷积大小(2,3)即非正方形卷积
          #stride:步长,默认为1,与kennel_size类似,stride=2,意味着步长上下左右扫描皆为2, stride=(2,3),左右扫描步长为2,上下为3;
          #padding:填充
            nn.LeakyReLU(negative_slope=0.2, inplace=True),
#ReLU是将所有的负值都设为零,相反,Leaky ReLU是给所有负值赋予一个非零斜率。  negative_slope是超参数,控制x为负数时斜率的角度,默认为1e-2

            # input:(ndf, 32, 32)
            nn.Conv2d(in_channels=self.ndf, out_channels=self.ndf * 2, kernel_size=4, stride=2, padding=1, bias=False),
            nn.BatchNorm2d(self.ndf * 2),
            nn.LeakyReLU(0.2, True),

            # input:(ndf *2, 16, 16)
            nn.Conv2d(in_channels=self.ndf * 2, out_channels=self.ndf * 4, kernel_size=4, stride=2, padding=1,
                      bias=False),
            nn.BatchNorm2d(self.ndf * 4),
            nn.LeakyReLU(0.2, True),

            # input:(ndf *4, 8, 8)
            nn.Conv2d(in_channels=self.ndf * 4, out_channels=self.ndf * 8, kernel_size=4, stride=2, padding=1,
                      bias=False),
            nn.BatchNorm2d(self.ndf * 8),
            nn.LeakyReLU(0.2, True),

            # input:(ndf *8, 4, 4)
            # output:(1, 1, 1)
            nn.Conv2d(in_channels=self.ndf * 8, out_channels=1, kernel_size=4, stride=1, padding=0, bias=True),

            # 调用sigmoid函数解决分类问题
            # 因为判别模型要做的是二分类,故用sigmoid即可,因为sigmoid返回值区间为[0,1],
            # 可作判别模型的打分标准
            nn.Sigmoid()
        )

    def forward(self, x):
        # 展平后返回
        return self.Discrim(x).view(-1)
# X.view(-1)中的-1本意是根据另外一个数来自动调整维度,但是这里只有一个维度,因此就会将X里面的所有维度数据转化成一维的,并且按先后顺序排列。


def train(**kwargs):
# **kwargs允许将不定长度的键值对, 作为参数传递给一个函数。如果想在一个函数里处理带名字的参数, 应该使用**kwargs
    # 配置属性
    # 如果函数无字典输入则使用opt中设定好的默认超参数
    for k_, v_ in kwargs.items():
        setattr(opt, k_, v_) 
#setattr(object,name,value) object -- 对象,name -- 字符串,对象属性,value -- 属性值。

    # device(设备),分配设备
    if opt.gpu:
        device = torch.device("cuda")
    else:
        device = torch.device('cpu')

    # 数据预处理1
    # transforms 模块提供一般图像转换操作类的功能,最后转成floatTensor
    # tv.transforms.Compose用于组合多个tv.transforms操作,定义好transforms组合操作后,直接传入图片即可进行处理
    # tv.transforms.Resize,对PIL Image对象作调整大小运算,数值保存类型为float64
    # tv.transforms.CenterCrop, 中心裁剪
    # tv.transforms.ToTensor,将opencv读到的图片转为torch image类型(通道,像素,像素),且把像素范围转为[0,1]
    # tv.transforms.Normalize,执行image = (image - mean)/std 数据归一化操作,一参数是mean,二参数std
    # 因为是三通道,所以mean = (0.5, 0.5, 0.5),从而转成[-1, 1]范围
    transforms = tv.transforms.Compose([
        # 3*96*96
        tv.transforms.Resize(opt.img_size),  # 缩放到 img_size* img_size
        # 中心裁剪成96*96的图片。因为本实验数据已满足96*96尺寸,可省略
        tv.transforms.CenterCrop(opt.img_size),

        # ToTensor 和 Normalize 搭配使用
#(0−0.5)/0.5=−1,(1−0.5)/0.5=1,从而把像素范围从[0,1] 归一化到[-1, 1]
        tv.transforms.ToTensor(),
        tv.transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
    ])

    # 加载数据并使用定义好的transforms对图片进行预处理,这里用的是直接定义法
    # dataset是一个包装类,将数据包装成Dataset类,方便之后传入DataLoader中
    # 写法2:
    # 定义类Dataset(Datasets)包装类,重写__getitem__(进行transforms系列操作)、__len__方法(获取样本个数)
    # ### 两种写法有什么区别
    dataset = tv.datasets.ImageFolder(root=opt.data_path, transform=transforms)
#ImageFolder假设所有的文件按文件夹保存好,每个文件夹下面存贮同一类别的图片,文件夹的名字为分类的名字。
# root:在指定的root路径下面寻找图片;transform:对PIL Image进行转换操作,transform 输入是loader读取图片返回的对象

    # 数据预处理2
    # 查看drop_last操作,
    dataloader = DataLoader(
        dataset,  # 数据加载
        batch_size=opt.batch_size,  # 批处理大小设置
        shuffle=True,  # 是否进行洗牌操作
        # num_workers=opt.num_workers,     # 是否进行多线程加载数据设置
        drop_last=True  # 为True时,如果数据集大小不能被批处理大小整除,则设置为删除最后一个不完整的批处理。
    )

    # 初始化网络
    netg, netd = NetG(opt), NetD(opt)
    # 判断网络是否有权重数值
    # ### storage存储
    map_location = lambda storage, loc: storage
#把所有的张量加载到CPU中

    # torch.load模型加载,即有模型加载模型在该模型基础上进行训练,没有模型则从头开始
    # f:类文件对象,如果有模型对象路径,则加载返回
    # map_location:一个函数或字典规定如何remap存储位置
    # net.load_state_dict将加载出来的模型数据加载到构建好的net网络中去
    if opt.netg_path:  #是否指定训练好的预训练模型,加载模型参数
        netg.load_state_dict(torch.load(f=opt.netg_path, map_location=map_location))
    if opt.netd_path:
        netd.load_state_dict(torch.load(f=opt.netd_path, map_location=map_location))

    # 搬移模型到之前指定设备,本文采用的是cpu,分配设备
    netd.to(device)
    netg.to(device)

    # 定义优化策略
    # torch.optim包内有多种优化算法,
    # Adam优化算法,是带动量的惯性梯度下降算法
    optimize_g = torch.optim.Adam(netg.parameters(), lr=opt.lr1, betas=(opt.beta1, 0.999))
# torch.optim.Adam(params, lr=0.001),params(iterable):可用于迭代优化的参数或者定义参数组的dicts。lr (float, optional) :学习率(默认: 1e-3)
    optimize_d = torch.optim.Adam(netd.parameters(), lr=opt.lr2, betas=(opt.beta1, 0.999))

    # 计算目标值和预测值之间的交叉熵损失函数
    # BCEloss:-w(ylog x +(1 - y)log(1 - x))
    # y为真实标签,x为判别器打分(sigmiod,1为真0为假),加上负号,等效于求对应标签下的最大得分
    # to(device),用于指定CPU/GPU
    criterions = nn.BCELoss().to(device)

    # 定义标签,并且开始注入生成器的输入noise
# 真图片label为1,假图片label为0
true_labels = torch.ones(opt.batch_size).to(device)
    fake_labels = torch.zeros(opt.batch_size).to(device)

    # 生成满足N(1,1)标准正态分布,opt.nz维(100维),opt.batch_size个数的随机噪声
    noises = torch.randn(opt.batch_size, opt.nz, 1, 1).to(device)

    # 用于保存模型时作生成图像示例
    fix_noises = torch.randn(opt.batch_size, opt.nz, 1, 1).to(device)

    # 训练网络
    # 设置迭代
    for epoch in range(opt.max_epoch):
        # tqdm(iterator()),函数内嵌迭代器,用作循环的进度条显示
        for ii_, (img, _) in tqdm((enumerate(dataloader))):# enumerate() 函数用于将一个可遍历的数据对象(如列表、元组或字符串)组合为一个索引序列,同时列出数据和数据下标
            # 将处理好的图片赋值
            real_img = img.to(device)

            # 开始训练生成器和判别器
            # 注意要使得生成的训练次数小于一些
            # 每一轮更新一次判别器
            if ii_ % opt.d_every == 0:
                # 优化器梯度清零
                optimize_d.zero_grad()

                # 训练判别器
                # 把判别器的目标函数分成两段分别进行反向求导,再统一优化
                # 真图
                # 把所有的真样本传进netd进行训练,
                output = netd(real_img)
                # 用之前定义好的交叉熵损失函数计算损失
                error_d_real = criterions(output, true_labels)
                # 误差反向计算
                error_d_real.backward()

                # 随机生成的假图
                # .detach() 返回相同数据的 tensor ,且 requires_grad=False
                #  .detach()做截断操作,生成器不记录判别器采用噪声的梯度
                noises = noises.detach()
                # 通过生成模型将随机噪声生成为图片矩阵数据
                fake_image = netg(noises).detach()
                # 将生成的图片交给判别模型进行判别
                output = netd(fake_image)
                # 再次计算损失函数的计算损失
                error_d_fake = criterions(output, fake_labels)
                # 误差反向计算
                # 求导和优化(权重更新)是两个独立的过程,只不过优化时一定需要对应的已求取的梯度值。
                # 所以求得梯度值很关键,而且,经常会累积多种loss对某网络参数造成的梯度,一并更新网络。
                error_d_fake.backward()

                # 关于为什么要分两步计算loss:
                # 我们已经知道,BCEloss相当于计算对应标签下的得分,那么我们
                # 把真样本传入时,因为标签恒为1,BCE此时只有第一项,即真样本得分项
                # 要补齐成前文提到的判别器目标函数,需要再添置假样本得分项,故两次分开计算梯度,各自最大化各自的得分(假样本得分是log(1-D(x)))
                # 再统一进行梯度下降即可

                # 计算一次Adam算法,完成判别模型的参数迭代
                # 多个不同loss的backward()来累积同一个网络的grad,计算一次Adam即可
                optimize_d.step() #对判别器进行优化

            # 训练生成器
            if ii_ % opt.g_every == 0:
                optimize_g.zero_grad() #生成器梯度全部降为0
                # 用于netd作判别训练和用于netg作生成训练两组噪声需不同
                noises.data.copy_(torch.randn(opt.batch_size, opt.nz, 1, 1))
#更新noises中的data值
                fake_image = netg(noises)
# 根据噪声生成假图
                output = netd(fake_image)
                # 此时判别器已经固定住了,BCE的一项为定值,再求最小化相当于求二项即G得分的最大化
                error_g = criterions(output, true_labels)
                error_g.backward()

                # 计算一次Adam算法,完成判别模型的参数迭代
                optimize_g.step()

        # 保存模型
        if (epoch + 1) % opt.save_every == 0:
            fix_fake_image = netg(fix_noises)
            tv.utils.save_image(fix_fake_image.data[:64], "%s/%s.png" % (opt.save_path, epoch), normalize=True)
#torchvision.utils.save_image直接保存tensor为图片

            torch.save(netd.state_dict(), 'C:/Users/Administrator/Desktop/images' + 'netd_{0}.pth'.format(epoch))
            torch.save(netg.state_dict(), 'C:/Users/Administrator/Desktop/images' + 'netg_{0}.pth'.format(epoch))


# @torch.no_grad():数据不需要计算梯度,也不会进行反向传播
@torch.no_grad()
def generate(**kwargs):
    # 用训练好的模型来生成图片

    for k_, v_ in kwargs.items():
        setattr(opt, k_, v_)

    device = torch.device("cuda") if opt.gpu else torch.device("cpu")

    # 加载训练好的权重数据
    netg, netd = NetG(opt).eval(), NetD(opt).eval()
#eval()时,pytorch会自动把BN和DropOut固定住,不会取平均,而是用训练好的值。不然的话,一旦test的batch_size过小,很容易就会被BN层导致生成图片颜色失真极大。
#eval()在非训练的时候是需要加的,没有这句代码,一些网络层的值会发生变动,不会固定,神经网络每一次生成的结果也是不固定的,生成质量可能好也可能不好。
    #  两个参数返回第一个
    map_location = lambda storage, loc: storage

    # opt.netd_path等参数有待修改
netd.load_state_dict(torch.load('C:/Users/Administrator/Desktop/images/netd_399.pth', map_location=map_location),
                         False)
netg.load_state_dict(torch.load('C:/Users/Administrator/Desktop/images/netg_399.pth', map_location=map_location),
                         False)
    netd.to(device)
    netg.to(device)

    # 生成训练好的图片
    # 初始化512组噪声,选其中好的拿来保存输出。
    noise = torch.randn(opt.gen_search_num, opt.nz, 1, 1).normal_(opt.gen_mean, opt.gen_std).to(device)

    fake_image = netg(noise)
    score = netd(fake_image).detach()

    # 挑选出合适的图片
    # 取出得分最高的图片
    indexs = score.topk(opt.gen_num)[1]
#topk()方法用于返回输入数据中特定维度上的前k个最大的元素。

    result = []

    for ii in indexs:
        result.append(fake_image.data[ii])

    # 以opt.gen_img为文件名保存生成图片
    tv.utils.save_image(torch.stack(result), opt.gen_img, normalize=True, range=(-1, 1))


def main():
    # 训练模型
    train()
    # 生成图片
    generate()


if __name__ == '__main__':
    main()

代码文档

  • 1
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值