本人主页
三个阶段:
emoji集合~
[toc]
前言 在基本的GAN熟知了之后,我们先聊一聊上次的第一部分做了什么:首先,我们有一堆按照某个规律分布的数据,但是我们完全不知道它的特点,比如在这里我们就在这两条线之间生成了样例数据,显然,这个样例数据似乎也长着一个二次曲线的样子。
经过GAN的对抗之后,生成器生成的数据已经很接近二次曲线形状了。并且当我把样例数据的分布改变——比如改成在$y=x+1$附近的时候,也能够生成差不多对应的直线。
那么,如果我们的输入,不再是简单的线性分布,而是一张图片呢?看起来其实也就是把真实样本数据的输入维数64×15变成了64×15×3(rgb),但此时就应用到另一个针对图片的工具,CNN,所以就诞生了DCGAN。
数据集来源:scraped from www.getchu.com , which are then cropped using the anime face detection algorithm in https://github.com/nagadomi/lbpcascade_animeface .
代码中的小知识点 feature map是什么 在代码中,有ngf
和nz
两个变量,一开始不知道其作用的时候很是头疼。
在每个卷积层,数据都是以三维形式存在的。 你可以把它看成许多个二维图片叠在一起,其中每一个称为一个feature map。 在输入层,如果是灰度图片,那就只有一个feature map;如果是彩色图片,一般就是3个feature map(红绿蓝)。 层与层之间会有若干个卷积核(kernel),上一层和每个feature map跟每个卷积核做卷积,都会产生下一层的一个feature map。
不同的Filter (不同的 weight, bias) ,卷积以后得到不同的 feature map,提取不同的特征(得到对应 specialized neuro)
举例:同一层:
Fliter1 的w1,b1 运算后提取的是 形状边缘的特征: feature map1
Fliter2 的w2,b2 运算后提取的是 颜色深浅的特征: feature map2
下一层:
Fliter3 的w3,b3 运算后提取的是 直线形状的特征: feature map3
Fliter4 的w4,b4 运算后提取的是 弧线形状的特征: feature map4
Fliter5 的w5,b5 运算后提取的是 红色深浅的特征: feature map5
Fliter6 的w6,b6 运算后提取的是 绿色深浅的特征: feature map6
作者:花花儿 链接:https://www.zhihu.com/question/31318081/answer/182488143 来源:知乎 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
在卷积进行特征提取的时候,使用多个卷积核进行卷积再组合在一起,也会形成feature map
一些函数 1、nn.Conv2d和nn.ConvTranspose2d参数说明及区别
nn.Conv2d(in_channels,out_channels,kernel_size,stride=1,padding=0,dilation=1,groups=1,bias=True)
in_channels:输入维度 out_channels:输出维度 kernel_size:卷积核大小 stride:步长大小 padding:补0 dilation:kernel间距 groups(int, optional) : 从输入通道到输出通道的阻塞连接数
nn.Conv2d的功能是:对由多个输入平面组成的输入信号进行二维卷积。
经过卷积后的图像尺寸计算公式:N = (W − F + 2P )/S+1
w为原始图像大小、F为卷积核尺寸、P为padding大小、S是步长
2、pytorch中反卷积的函数为:
1 2 >class torch .nn .ConvTranspose2d (in_channels, out_channels, kernel_size, stride=1 , padding=0 , output_padding=0 , groups=1 , bias=True , dilation=1 )
参数的含义如下:
in_channels(int) – 输入信号的通道数
out_channels(int) – 卷积产生的通道数
kerner_size(int or tuple) - 卷积核的大小
stride(int or tuple,optional) - 卷积步长,即要将输入扩大的倍数。
padding(int or tuple, optional) - 输入的每一条边补充0的层数,高宽都增加2*padding
output_padding(int or tuple, optional) - 输出边补充0的层数,高宽都增加padding
groups(int, optional) – 从输入通道到输出通道的阻塞连接数
bias(bool, optional) - 如果bias=True,添加偏置
dilation(int or tuple, optional) – 卷积核元素之间的间距
对于每一条边输入输出的尺寸的公式如下:
$output=(input-1)stride+output_padding-2 padding+kernel_size$
重要: 关于卷积中几个参数的数学定义
很简单的例子:
1 nn.ConvTranspose2d(opt.nz, ngf* 8 , 4 , 1 , 0 , bias=False )
其中输入的opt.nz
是输入为nz维度(输入通道数为nz)的噪声:nz*1*1,那么这条语句实际上就是把一个nz
个通道的1×1的像素,扩展到了有nz×8
个通道,4×4的feature map
3、nn.BatchNorm2d()函数
机器学习中,进行模型训练之前,需对数据做归一化处理,使其分布一致。在深度神经网络训练过程中,通常一次训练是一个batch,而非全体数据。每个batch具有不同的分布产生了internal covarivate shift问题——在训练过程中,数据分布会发生变化,对下一层网络的学习带来困难。Batch Normalization将数据拉回到均值为0,方差为1的正态分布上(归一化),一方面使得数据分布一致,另一方面避免梯度消失、梯度爆炸。 ———————————————— 版权声明:本文为CSDN博主「宁静致远*」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/weixin_40522801/article/details/106185263
1 class torch .nn .BatchNorm2d (num_features, eps=1e-05 , momentum=0.1 , affine=True )
其中num_features
是指输入的channel数。
4、解决一个小报错
参考
在使用errorg_meter.add(error_g.data[0])
时会报错,经检查,是因为pytorch版本更新的原因,改成errorg_meter.add(error_g.item())
就好了。:kissing_heart:
5、LeakyRelu()函数
LeakrReLU和ReLU的区别就在于小于0的部分,仍有微小的梯度(下图右者)
参考,讲的很详细
Fire是什么 Google的python fire文档
Fire,是为了更好在命令行输入参数来控制程序的,具体内容参考另一篇博客:
Adam优化器是什么 简单的来说,就是“动量+自适应学习率” ——以下参考李宏毅的课
动量momentum 如下图所示,我们的梯度下降过程,就好像是一个小球滚落山谷最后达到最低点的过程,但是呢,就很容易被暂时的花花风景迷了眼(掉入局部最优中)。
想一想现实中是怎么做的,没错,小球的运动不仅收到当前梯度的影响,还存在着惯性,而动量momentum的思想就是赋予小球“冲过”局部最优的能力。
按照上图所示,在某一点的最终更新梯度是梯度矢量和动量矢量的和,动量矢量描述为当前的“速度”,而其中的动量: $$ \begin{gather} m^0=0\ m^1=-\eta g^0\ m^2=-\lambda\eta g^0-\eta g^1 \end{gather} $$ 从形式上看,动量的大小其实也是过去每一步的梯度的加权和,所以最终的效果就是梯度下降的方向不仅由$\partial L/\partial W$决定啦
自适应学习率 自适应学习率这一块,也有个很简单的原理:我们希望在平坦的路上步子迈大点,在陡峭的山坡上步子小一点
可以看到,有了$\sigma_i^t$的存在可以进行调整,第一步的时候,分子分母抵消,按照$\eta$进行下降,然后在第二步,可以看到,是对前面的梯度的模进行了一个平方平均。因此,在整体梯度比较小的时候,分子就小了,那么学习率就上去了,步长增大;反之步长减小。下图很形象;
代码详解 遇到两个大问题:
读取图片之torchvision 官方文档
1.transforms.Scale(size):改变图片的大小:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 crop = transforms.Scale(12 ) img = Image.open ('1.png' ) print(type (img)) print(img.size) croped_img = crop(img) print(type (croped_img)) print(croped_img.size) <class 'PIL .PngImagePlugin .PngImageFile '> (64 , 64 ) <class 'PIL .Image .Image '> (12 , 12 )
可见把64*64的图片变成了12*12的。
2.transforms.CenterCrop(size):中心切割
将给定的PIL.Image
进行中心切割,得到给定的size
,size
可以是tuple(元组)
:(target_height, target_width)
。size
也可以是一个Integer
,在这种情况下,切出来的图片的形状是正方形。
3.transforms.ToTensor():转化成tensor
把一个取值范围是[0,255]
的PIL.Image
或者shape
为(H,W,C)
的numpy.ndarray
,转换成形状为[C,H,W]
,取值范围是[0,1.0]
的torch.FloadTensor
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 data = np.random.randint(0 , 255 , size=75 ) img = data.reshape(5 , 5 , 3 ) print(img.shape) img_tensor = transforms.ToTensor()(img) print(img_tensor) (5 , 5 , 3 ) tensor([[[ 97 , 218 , 4 , 139 , 32 ], [ 65 , 231 , 181 , 86 , 197 ], [242 , 10 , 185 , 27 , 158 ], [ 95 , 0 , 47 , 88 , 46 ], [172 , 210 , 41 , 220 , 25 ]], [[176 , 225 , 14 , 189 , 164 ], [ 40 , 107 , 231 , 110 , 171 ], [247 , 128 , 161 , 128 , 43 ], [ 50 , 51 , 36 , 103 , 81 ], [107 , 25 , 66 , 171 , 202 ]], [[115 , 194 , 103 , 128 , 105 ], [ 78 , 75 , 181 , 2 , 87 ], [ 57 , 35 , 96 , 35 , 168 ], [191 , 143 , 235 , 86 , 139 ], [ 52 , 60 , 57 , 103 , 174 ]]], dtype=torch.int32)
4.transforms.Normalize(mean, std):将Tensor
正则化
给定均值:(R,G,B)
方差:(R,G,B)
,将会把Tensor
正则化。即Normalized_image=(image-mean)/std
。
代码太长了,不管了,直接上吧,写到啥是啥 网络构建 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 from torch import nnclass NetG (nn.Module ): ''' 生成器定义 ''' def __init__ (self, opt ): super (NetG, self).__init__() ngf = opt.ngf self.main = nn.Sequential( nn.ConvTranspose2d(opt.nz, ngf * 8 , 4 , 1 , 0 , bias=False ), nn.BatchNorm2d(ngf * 8 ), nn.ReLU(True ), nn.ConvTranspose2d(ngf * 8 , ngf * 4 , 4 , 2 , 1 , bias=False ), nn.BatchNorm2d(ngf * 4 ), nn.ReLU(True ), nn.ConvTranspose2d(ngf * 4 , ngf * 2 , 4 , 2 , 1 , bias=False ), nn.BatchNorm2d(ngf * 2 ), nn.ReLU(True ), nn.ConvTranspose2d(ngf * 2 , ngf, 4 , 2 , 1 , bias=False ), nn.BatchNorm2d(ngf), nn.ReLU(True ), nn.ConvTranspose2d(ngf, 3 , 4 , 2 , 1 , bias=False ), nn.Tanh() ) def forward (self, input ): return self.main(input )
在这里,做出了一个重要变化:原来的最后一个层的kernel是(5,3,1),这样就能根据公式$H_{out}=(H_{in}-1)stride-2 padding+kernel_size$使得图片从32×32变成96×96,但是我们用的图片数据是64×64,所以调整kernel为(4,2,1),最后发现修改正确!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 class NetD (nn.Module ): ''' 判别器定义 ''' def __init__ (self, opt ): super (NetD, self).__init__() ndf = opt.ndf self.main = nn.Sequential( nn.Conv2d(3 , ndf, 4 , 2 , 1 , bias=False ), nn.LeakyReLU(0.2 , inplace=True ), nn.Conv2d(ndf, ndf * 2 , 4 , 2 , 1 , bias=False ), nn.BatchNorm2d(ndf * 2 ), nn.LeakyReLU(0.2 , inplace=True ), nn.Conv2d(ndf * 2 , ndf * 4 , 4 , 2 , 1 , bias=False ), nn.BatchNorm2d(ndf * 4 ), nn.LeakyReLU(0.2 , inplace=True ), nn.Conv2d(ndf * 4 , ndf * 8 , 4 , 2 , 1 , bias=False ), nn.BatchNorm2d(ndf * 8 ), nn.LeakyReLU(0.2 , inplace=True ), nn.Conv2d(ndf * 8 , 1 , 4 , 1 , 0 , bias=False ), nn.Sigmoid() ) def forward (self, input ): return self.main(input ).view(-1 )
其实生成器和判别器的构造都比较简单的,生成器把一个nz*1*1
的噪声,逐步上卷积到3*64*64
,但是注意最后生成的矩阵每一个元素都是-1~1的,所以并不是直接的图像。
判别器则是不断卷积,并使用LeakyReLU代替的ReLU,最后是输出一个数,即判断是真图片的概率。
配置所需参数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 class Config (object ): data_path = 'data/' num_workers = 4 image_size = 64 batch_size = 256 max_epoch = 200 lr1 = 2e-4 lr2 = 2e-4 beta1 = 0.5 gpu = True nz = 100 ngf = 64 ndf = 64 save_path = 'imgs/' vis = True env = 'GAN' plot_every = 20 debug_file = '/tmp/debuggan' d_every = 1 g_every = 5 decay_every = 10 netd_path = None netg_path = None gen_img = 'result.png' gen_num = 64 gen_search_num = 512 gen_mean = 0 gen_std = 1 opt = Config()
以上超参数设置就先这样吧,放在这后面再说。
开始训练 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 def train (**kwargs ): for k_, v_ in kwargs.items(): setattr (opt, k_, v_) ''' 将对象opt的属性k_改变为v_,这是一个很实用的写法!!!!! ''' if opt.vis: from visualize import Visualizer vis = Visualizer(opt.env) transforms = tv.transforms.Compose([ tv.transforms.Scale(opt.image_size), tv.transforms.CenterCrop(opt.image_size), tv.transforms.ToTensor(), tv.transforms.Normalize((0.5 , 0.5 , 0.5 ), (0.5 , 0.5 , 0.5 )) ]) dataset = tv.datasets.ImageFolder(opt.data_path, transform=transforms) dataloader = t.utils.data.DataLoader(dataset, batch_size=opt.batch_size, shuffle=True , num_workers=opt.num_workers, drop_last=True )
torch.utils.data的官方文档
我们用已经用tv.datasets.ImageFolder
加载好了数据集,而classtorch.utils.data.DataLoader
是一个数据加载器。组合数据集和采样器,并在数据集上提供单进程或多进程迭代器。
dataset (Dataset ) – 加载数据的数据集。
batch_size (int , optional) – 每个batch加载多少个样本(默认: 1)。
shuffle (bool , optional) – 设置为True
时会在每个epoch重新打乱数据(默认: False).
num_workers (int , optional) – 用多少个子进程加载数据。0表示数据将在主进程中加载(默认: 0)
drop_last (bool , optional) – 如果数据集大小不能被batch size整除,则设置为True后可删除最后一个不完整的batch。如果设为False并且数据集的大小不能被batch size整除,则最后一个batch将更小。(默认: False)
1 2 3 4 5 6 7 netg, netd = NetG(opt), NetD(opt) def map_location (storage, loc ): return storageif opt.netd_path: netd.load_state_dict(t.load(opt.netd_path, map_location=map_location)) if opt.netg_path: netg.load_state_dict(t.load(opt.netg_path, map_location=map_location))
这是在加载预训练模型,我们在构造好了一个模型后,可能要加载一些训练好的模型参数,这时就可以。官方文档位置 ,很值得看的一个博客
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 optimizer_g = t.optim.Adam( netg.parameters(), opt.lr1, betas=(opt.beta1, 0.999 )) optimizer_d = t.optim.Adam( netd.parameters(), opt.lr2, betas=(opt.beta1, 0.999 )) criterion = t.nn.BCELoss() true_labels = Variable(t.ones(opt.batch_size)) fake_labels = Variable(t.zeros(opt.batch_size)) fix_noises = Variable(t.randn(opt.batch_size, opt.nz, 1 , 1 )) noises = Variable(t.randn(opt.batch_size, opt.nz, 1 , 1 )) errord_meter = AverageValueMeter() errorg_meter = AverageValueMeter()
顾名思义,Variable 就是 变量 的意思。实质上也就是可以变化的量,区别于int变量,它是一种可以变化的变量,这正好就符合了反向传播,参数更新的属性。
具体来说,在pytorch中的Variable就是一个存放会变化值的地理位置,里面的值会不停发生片花,就像一个装鸡蛋的篮子,鸡蛋数会不断发生变化。那谁是里面的鸡蛋呢,自然就是pytorch中的tensor了。(也就是说,pytorch都是有tensor计算的,而tensor里面的参数都是Variable的形式 )。如果用Variable计算的话,那返回的也是一个同类型的Variable。
https://www.jb51.net/article/177996.htm
AverageValueMeter(self)
该tnt.AverageValueMeter
返回的应该是统计量的均值。测量一组示例的平均损失很有用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 if opt.gpu: netd.cuda() netg.cuda() criterion.cuda() true_labels, fake_labels = true_labels.cuda(), fake_labels.cuda() fix_noises, noises = fix_noises.cuda(), noises.cuda() epochs = range (opt.max_epoch) for epoch in iter (epochs): for ii, (img, _) in tqdm.tqdm(enumerate (dataloader)): real_img = Variable(img) if opt.gpu: real_img = real_img.cuda() if ii % opt.d_every == 0 : optimizer_d.zero_grad() output = netd(real_img) error_d_real = criterion(output, true_labels) error_d_real.backward() noises.data.copy_(t.randn(opt.batch_size, opt.nz, 1 , 1 )) fake_img = netg(noises).detach() output = netd(fake_img) error_d_fake = criterion(output, fake_labels) error_d_fake.backward() optimizer_d.step() ''' !!!!!!!!!!!!!!!!!!! 注意看这里!先对网络进行了zero_grad()梯度清零, 然后netd(real_img)进行前向传播计算参数, 然后backward()反向传播计算梯度 最后两个反向传播都结束了,梯度都更新了, optimizer_d.step()权重更新 !!!!!!!!!!!!!!!!!!!! ''' error_d = error_d_fake + error_d_real errord_meter.add(error_d.item()) if ii % opt.g_every == 0 : optimizer_g.zero_grad() noises.data.copy_(t.randn(opt.batch_size, opt.nz, 1 , 1 )) fake_img = netg(noises) output = netd(fake_img) error_g = criterion(output, true_labels) error_g.backward() optimizer_g.step() errorg_meter.add(error_g.item())
可视化部分
visdom官方文档
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 if opt.vis and ii % opt.plot_every == opt.plot_every-1 : if os.path.exists(opt.debug_file): ipdb.set_trace() fix_fake_imgs = netg(fix_noises) vis.images(fix_fake_imgs.data.cpu().numpy() [:64 ]*0.5 +0.5 , win='fixfake' ) vis.images(real_img.data.cpu().numpy() [:64 ]*0.5 +0.5 , win='real' ) vis.plot('errord' , errord_meter.value()[0 ]) vis.plot('errorg' , errorg_meter.value()[0 ]) if epoch % opt.decay_every == 0 : tv.utils.save_image(fix_fake_imgs.data[:64 ], '%s/%s.png' % ( opt.save_path, epoch), normalize=True , range =(-1 , 1 )) t.save(netd.state_dict(), 'checkpoints/netd_%s.pth' % epoch) t.save(netg.state_dict(), 'checkpoints/netg_%s.pth' % epoch) errord_meter.reset() errorg_meter.reset() optimizer_g = t.optim.Adam( netg.parameters(), opt.lr1, betas=(opt.beta1, 0.999 )) optimizer_d = t.optim.Adam( netd.parameters(), opt.lr2, betas=(opt.beta1, 0.999 ))
生成结果 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 def generate (**kwargs ): ''' 随机生成动漫头像,并根据netd的分数选择较好的 ''' for k_, v_ in kwargs.items(): setattr (opt, k_, v_) netg, netd = NetG(opt).eval (), NetD(opt).eval () noises = t.randn(opt.gen_search_num, opt.nz, 1 , 1 ).normal_(opt.gen_mean, opt.gen_std) noises = Variable(noises, volatile=True ) def map_location (storage, loc ): return storage netd.load_state_dict(t.load(opt.netd_path, map_location=map_location)) netg.load_state_dict(t.load(opt.netg_path, map_location=map_location)) if opt.gpu: netd.cuda() netg.cuda() noises = noises.cuda() fake_img = netg(noises) scores = netd(fake_img).data indexs = scores.topk(opt.gen_num)[1 ] result = [] for ii in indexs: result.append(fake_img.data[ii]) tv.utils.save_image(t.stack(result), opt.gen_img, normalize=True , range =(-1 , 1 )) if __name__ == '__main__' : import fire fire.Fire()
最后结果
如图,我训练了100个eopch,可以看到生成的图片逐渐有了动漫头像的感觉,相信如果训练的更多最后能够达到比较满意的结果~
2021.8.5