GAN对抗生成网络
训练流程![在这里插入图片描述](https://img-blog.csdnimg.cn/f8affb19d0854aa09a68964990386902.png#pic_center)
图片以及训练过程来源
训练这样的两个模型的大方法就是单独交替迭代训练。
-
我们人为的定义真假样本集的标签,因为我们希望真样本集的输出尽可能为1,假样本集为0,我们就已经默认真样本集所有的类标签都为1,而假样本集的所有类标签都为0。这样单就判别网络来说,此时问题就变成了一个再简单不过的有监督的二分类问题了。
-
如果我们把刚才的判别网络串接在生成网络的后面,这样我们就知道真假了,也就有了误差了,所以对于生成网络的训练其实是对生成-判别网络串接的训练。我们要把这些假样本的标签都设置为1,也就是认为这些假样本在生成网络训练的时候是真样本,这样才能起到迷惑判别器的目的,也才能使得生成的假样本逐渐逼近为正样本。现在对于生成网络的训练,我们有了样本集(只有假样本集,没有真样本集),有了对应的label(全为1),是不是就可以训练了?有人会问,这样只有一类样本,训练啥呀?谁说一类样本就不能训练了?只要有误差就行。
-
训练这个串接的网络的时候,一个很重要的操作就是不要判别网络的参数发生变化,也就是不让它参数发生更新,只是把误差一直传,传到生成网络那块后更新生成网络的参数。这样就完成了生成网络的训练了。
-
在完成生成网络训练后,我们可以根据目前新的生成网络再对先前的那些噪声Z生成新的假样本了,并且训练后的假样本更真了。然后又有了新的真假样本集(其实是新的假样本集),这样又可以重复上述过程了。
代码实现
os.makedirs("images", exist_ok=True)
创建子文件夹images,exist_ok取值为Ture时,如果已存在该文件夹,也不会报错。
# 初始化参数,如rpoch次数,batch_size的大小等,sample_interval表示后续间隔保存CGAN图片的保存间隔。
parser = argparse.ArgumentParser() # 声明一个parser
parser.add_argument("--n_epochs", type=int, default=200, help="number of epochs of training") # 添加如下参数,并设定默认值
parser.add_argument("--batch_size", type=int, default=64, help="size of the batches") # 后面的help是添加的描述
parser.add_argument("--lr", type=float, default=0.0002, help="adam: learning rate")
parser.add_argument("--b1", type=float, default=0.5, help="adam: decay of first order momentum of gradient")
parser.add_argument("--b2", type=float, default=0.999, help="adam: decay of first order momentum of gradient")
parser.add_argument("--n_cpu", type=int, default=8, help="number of cpu threads to use during batch generation")
parser.add_argument("--latent_dim", type=int, default=100, help="dimensionality of the latent space")
parser.add_argument("--img_size", type=int, default=28, help="size of each image dimension")
parser.add_argument("--channels", type=int, default=1, help="number of image channels")
parser.add_argument("--sample_interval", type=int, default=400, help="interval betwen image samples")
opt = parser.parse_args() # 读取命令行参数
print(opt) # 调用这些参数
img_shape = (opt.channels, opt.img_size, opt.img_size)
这些参数opt.channels, opt.img_size是需要去上一部分设定的参数的位置去找的, 图像的通道数为1,尺寸大小为28*28
class Generator(nn.Module):
def __init__(self):
super(Generator, self).__init__()
def block(in_feat, out_feat, normalize=True):
layers = [nn.Linear(in_feat, out_feat)]
if normalize:
layers.append(nn.BatchNorm1d(out_feat, 0.8))
layers.append(nn.LeakyReLU(0.2, inplace=True))
return layers
self.model = nn.Sequential(
*block(opt.latent_dim, 128, normalize=False),
*block(128, 256),
*block(256, 512),
*block(512, 1024),
nn.Linear(1024, int(np.prod(img_shape))),
nn.Tanh()
)
def forward(self, z):
img = self.model(z)
img = img.view(img.size(0), *img_shape)
return img
搭建生成器神经网络
inplace=True的意思是进行原地操作,例如x=x+5对x就是一个原地操作,虽然y=x+5,x=y完成了同样的功能但不是原地操作。
forward中的z是在程序后面的定义的高斯噪声信号,形状为64*100
img.size(0)为64,也就是一批次训练的数目batch_size的值。
class Discriminator(nn.Module):
def __init__(self):
super(Discriminator, self).__init__()
self.model = nn.Sequential(
nn.Linear(int(np.prod(img_shape)), 512),
nn.LeakyReLU(0.2, inplace=True),
nn.Linear(512, 256),
nn.LeakyReLU(0.2, inplace=True),
nn.Linear(256, 1),
nn.Sigmoid()
)
def forward(self, img):
img_flat = img.view(img.size(0), -1)
validity = self.model(img_flat)
return validity
adversarial_loss = torch.nn.BCELoss()
定义了损失函数nn.BCELoss(),输入(X,Y), X 需要经过sigmoid, Y元素的值只能是0或1的float值
dataloader = torch.utils.data.DataLoader(
datasets.MNIST('../../data/mnist', train=True, download=True,
transform=transforms.Compose([
transforms.ToTensor(),
])),
batch_size=opt.batch_size, shuffle=True)
PyTorch中数据读取的一个重要接口是torch.utils.data.DataLoader,该接口定义在dataloader.py脚本中,该接口主要用来将自定义的数据读取接口的输出或者PyTorch已有的数据读取接口的输入按照batch size封装成Tensor,后续只需要再包装成Variable即可作为模型的输入,因此该接口有点承上启下的作用。简单来说就是你训练的数据集不是一股脑的全部丢进来,而是分成了一批一批的,这个接口函数就是将数据集分批并转化成可以处理的Tensor类型。
transforms.Compose([transforms.ToTensor(),transforms.Normalize(std=(0.5,0.5,0.5),mean=(0.5,0.5,0.5))])
其作用就是先将输入归一化到(0,1),再使用公式”(x-mean)/std”,将每个元素分布到(-1,1)
for i, (imgs, _) in enumerate(dataloader):
# Adversarial ground truths
valid = Variable(Tensor(imgs.size(0), 1).fill_(1.0), requires_grad=False)
fake = Variable(Tensor(imgs.size(0), 1).fill_(0.0), requires_grad=False)
# Configure input 将真实的图片转化为神经网络可以处理的变量。
real_imgs = Variable(imgs.type(Tensor))
这部分定义的相当于是一个标准,vaild可以看成是64行1列的向量,为了在后面计算损失时和1比较;fake也是一样是全为0的向量,用法和1的用法相同。
# -----------------
# Train Generator
# -----------------
optimizer_G.zero_grad()
# Sample noise as generator input
z = Variable(Tensor(np.random.normal(0, 1, (imgs.shape[0], opt.latent_dim))))
# Generate a batch of images
gen_imgs = generator(z)
# Loss measures generator's ability to fool the discriminator
g_loss = adversarial_loss(discriminator(gen_imgs), valid)
g_loss.backward()
optimizer_G.step()
np.random.normal(0, 1, (imgs.shape[0], opt.latent_dim)的意思就是输入从0到1之间,形状为imgs.shape[0], opt.latent_dim的随机高斯数据。
# ---------------------
# Train Discriminator
# ---------------------
optimizer_D.zero_grad()
# Measure discriminator's ability to classify real from generated samples
real_loss = adversarial_loss(discriminator(real_imgs), valid)
fake_loss = adversarial_loss(discriminator(gen_imgs.detach()), fake)
d_loss = (real_loss + fake_loss) / 2
d_loss.backward()
optimizer_D.step()
GAN交替训练的过程:
- 重新计算假样本(假样本每次是需要更新的,产生越来越像的样本)
- 训练D网络,一个二分类的神经网络
- 训练G网络,一个串联起来的长网络,也是一个二分类的神经网络(不过只有假样本来训练),同时D部分参数在下一次的时候不能变了。
CGAN
- CGAN目的就是能指定生成什么样的数据。核心就是通过给原始的GAN生成器G和判别器D添加额外的条件信息,实现条件生成模型。额外信息可以是类别标签或者是其他的辅助信息,最直接的就是使用类别标签信息y
- 原始的GAN生成器的输入信息是一固定长度的噪声信息,那么CGAN中则是将噪声信息结合标签信息组合起来
- 作为输入,标签信息一般是采用one-hot编码构成。
- 原始的GAN判别器输入是图像数据(真实的训练样本和生成器生成的数据),在CGAN中则是将类别标签和图像数据进行组合作为判别器的输入。
class Generator(nn.Module):
def __init__(self):
super(Generator, self).__init__()
## n_classes是dataset的类别数
self.label_emb = nn.Embedding(opt.n_classes, opt.n_classes)
def block(in_feat, out_feat, normalize=True):
layers = [nn.Linear(in_feat, out_feat)]
if normalize:
layers.append(nn.BatchNorm1d(out_feat, 0.8))
layers.append(nn.LeakyReLU(0.2, inplace=True))
return layers
self.model = nn.Sequential(
*block(opt.latent_dim + opt.n_classes, 128, normalize=False),
*block(128, 256),
*block(256, 512),
*block(512, 1024),
nn.Linear(1024, int(np.prod(img_shape))),
nn.Tanh()
)
def forward(self, noise, labels):
# Concatenate label embedding and image to produce input
gen_input = torch.cat((self.label_emb(labels), noise), -1)
img = self.model(gen_input)
img = img.view(img.size(0), *img_shape)
return img
class Discriminator(nn.Module):
def __init__(self):
super(Discriminator, self).__init__()
self.label_embedding = nn.Embedding(opt.n_classes, opt.n_classes)
self.model = nn.Sequential(
nn.Linear(opt.n_classes + int(np.prod(img_shape)), 512),
nn.LeakyReLU(0.2, inplace=True),
nn.Linear(512, 512),
nn.Dropout(0.4),
nn.LeakyReLU(0.2, inplace=True),
nn.Linear(512, 512),
nn.Dropout(0.4),
nn.LeakyReLU(0.2, inplace=True),
nn.Linear(512, 1),
)
def forward(self, img, labels):
# Concatenate label embedding and image to produce input
d_in = torch.cat((img.view(img.size(0), -1), self.label_embedding(labels)), -1)
validity = self.model(d_in)
return validity
def sample_image(n_row, batches_done):
"""Saves a grid of generated digits ranging from 0 to n_classes"""
# Sample noise
z = Variable(FloatTensor(np.random.normal(0, 1, (n_row ** 2, opt.latent_dim))))
# Get labels ranging from 0 to n_classes for n rows
labels = np.array([num for _ in range(n_row) for num in range(n_row)])
labels = Variable(LongTensor(labels))
gen_imgs = generator(z, labels)
save_image(gen_imgs.data, "images02/%d.png" % batches_done, nrow=n_row, normalize=True)
DCGAN
-
DCGAN是将CNN 与 GAN结合,只是将G和D换成两个卷积神经网络(CNN),DCGAN将CNN做了一些改变,为提高样本质量和收敛速度。
-
strided convolution 替代确定性的pooling(从而可以让网络自己学习downsampling(下采样)
- G网络中使用微步幅度卷积(fractionally strided convolution)代替 pooling 层
- D网络中使用步幅卷积(strided convolution)代替 pooling 层。
-
在 D 和 G 中均使用 batch normalization批量归一化 ,去掉 FC 层,使网络变为全卷积网络 ,G 网络中使用ReLU 激活函数,最后一层使用tanh激活函数 ,D 网络中所有层都使用 LeakyReLU 作为激活函数
class Generator(nn.Module):
def __init__(self):
super(Generator, self).__init__()
self.init_size = opt.img_size // 4
self.l1 = nn.Sequential(nn.Linear(opt.latent_dim, 128 * self.init_size ** 2)) #l1函数进行Linear变换。线性变换的两个参数是变换前的维度,和变换之后的维度
self.conv_blocks = nn.Sequential( #nn.sequential{}是一个组成模型的壳子,用来容纳不同的操作
nn.BatchNorm2d(128), # BatchNorm2d的目的是使我们的一批(batch)feature map 满足均值0方差1,就是改变数据的量纲
nn.Upsample(scale_factor=2), #上采样,将图片放大两倍(这就是为啥class最先开始将图片的长宽除了4,下面还有一次放大2倍)
nn.Conv2d(128, 128, 3, stride=1, padding=1), #二维卷积函数,(输入数据channel,输出的channel,步长,卷积核大小,padding的大小)
nn.BatchNorm2d(128, 0.8),
nn.LeakyReLU(0.2, inplace=True), #relu激活函数
nn.Upsample(scale_factor=2), #上采样
nn.Conv2d(128, 64, 3, stride=1, padding=1),#二维卷积
nn.BatchNorm2d(64, 0.8), #BN
nn.LeakyReLU(0.2, inplace=True),
nn.Conv2d(64, opt.channels, 3, stride=1, padding=1),
nn.Tanh(), #Tanh激活函数
)
def forward(self, z):
out = self.l1(z) #l1函数进行的是Linear变换 (第50行定义了)
out = out.view(out.shape[0], 128, self.init_size, self.init_size)#view是维度变换函数,可以看到out数据变成了四维数据,第一个是batch_size(通过整个的代码,可明白),第二个是channel,第三,四是单张图片的长宽
img = self.conv_blocks(out)
return img
class Discriminator(nn.Module):
def __init__(self):
super(Discriminator, self).__init__()
def discriminator_block(in_filters, out_filters, bn=True):
block = [nn.Conv2d(in_filters, out_filters, 3, 2, 1), nn.LeakyReLU(0.2, inplace=True), nn.Dropout2d(0.25)]#Conv卷积,Relu激活,Dropout将部分神经元失活,进而防止过拟合
if bn:
block.append(nn.BatchNorm2d(out_filters, 0.8)) #如果bn这个参数为True,那么就需要在block块里面添加上BatchNorm的归一化函数
return block
self.model = nn.Sequential(
*discriminator_block(opt.channels, 16, bn=False),
*discriminator_block(16, 32),
*discriminator_block(32, 64),
*discriminator_block(64, 128),
)
# The height and width of downsampled image
ds_size = opt.img_size // 2 ** 4
self.adv_layer = nn.Sequential(nn.Linear(128 * ds_size ** 2, 1), nn.Sigmoid()) #先进行线性变换,再进行激活函数激活
#上一句中 128是指model中最后一个判别模块的最后一个参数决定的,ds_size由model模块对单张图片的卷积效果决定的,而2次方是整个模型是选取的长宽一致的图片
def forward(self, img):
out = self.model(img)
out = out.view(out.shape[0], -1) #将处理之后的数据维度变成batch * N的维度形式
validity = self.adv_layer(out) #第92行定义