更新预告
今天开始看GAN生成对抗样本的相关工作,这几天应该会出个AdvGAN(++)
今天开始看GAN生成对抗样本的相关工作,这几天应该会出个AdvGAN(++)
参考博客GAN学习指南:从原理入门到制作生成Demo - 知乎 (zhihu.com)
DCGAN的原理和GAN是一样的,这里就不在赘述。它只是把G和D换成了两个卷积神经网络(CNN)。DCGAN对卷积神经网络的结构做了一些改变,以提高样本的质量和收敛的速度,这些改变有:
DCGAN中的G网络示意:

作为伪二次元,选择参考这篇立本友人的工作Chainerで顔イラストの自動生成 - Qiita
突然发现是15年的帖子,8年前的旧东西力(悲

写一个简单爬虫,我们爬这个动漫网站http://konachan.net/. 注意需要翻墙,不然需要人机认证

原理就是遍历网站的不同页面,先把html下载下来,用BeautifulSoup解析html,找到缩略图的src属性,直接下载就好了.也没有针对反爬虫做什么东西(快说谢谢konachan)
import requests
from bs4 import BeautifulSoup
import os
import traceback
def download(url, filename):
if os.path.exists(filename):
print('file exists!')
return
try:
r = requests.get(url, stream=True, timeout=60)
r.raise_for_status()
with open(filename, 'wb') as f:
for chunk in r.iter_content(chunk_size=1024):
if chunk: # filter out keep-alive new chunks
f.write(chunk)
f.flush()
return filename
except KeyboardInterrupt:
if os.path.exists(filename):
os.remove(filename)
raise KeyboardInterrupt
except Exception:
traceback.print_exc()
if os.path.exists(filename):
os.remove(filename)
if os.path.exists('../imgs') is False:
os.makedirs('../imgs')
start = 1
end = 8000
for i in range(start, end + 1):
url = 'http://konachan.net/post?page=%d&tags=' % i
print(url)
html = requests.get(url).text
soup = BeautifulSoup(html, 'html.parser')
for img in soup.find_all('img', class_="preview"):
target_url = img['src']
filename = os.path.join('../imgs', target_url.split('/')[-1])
# print(filename)
download(target_url, filename)
print('%d / %d' % (i, end))下载下来的图片还是太大了,据友人说整张生成的效果不是很好,所以需要用到人脸裁剪工具,把图片裁小一点(正常二次元都拿人头当头像,这很合理).工具同样用用人提到的lbpcascade_animeface,基于opencv的库.

import cv2
import sys
import os.path
from glob import glob
def detect(filename, cascade_file="../lbpcascade_animeface-master/lbpcascade_animeface.xml"):
if not os.path.isfile(cascade_file):
raise RuntimeError("%s: not found" % cascade_file)
cascade = cv2.CascadeClassifier(cascade_file)
image = cv2.imread(filename)
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
gray = cv2.equalizeHist(gray)
faces = cascade.detectMultiScale(gray,
# detector options
scaleFactor=1.1,
minNeighbors=5,
minSize=(48, 48))
for i, (x, y, w, h) in enumerate(faces):
face = image[y: y + h, x:x + w, :]
face = cv2.resize(face, (96, 96))
save_filename = '%s-%d.jpg' % (os.path.basename(filename).split('.')[0], i)
cv2.imwrite("../faces/" + save_filename, face)
if __name__ == '__main__':
if os.path.exists('../faces') is False:
os.makedirs('../faces')
file_list = glob('../imgs/*.jpg')
for filename in file_list:
detect(filename)最后效果就是这样,很糊是因为每张图片只有64x64.友人用的是96x96.而我为了加快速度就直接用DCGAN本来的64x64了.(好处是可以直接扒Pytorch写的DCGAN)

立本友人在博客原文用的是Chainer,我压根没听过的框架.所以我们就用Pytorch吧(笑)
这部分代码我们直接去Pytorch官方库上去找.链接在这examples/dcgan at main · pytorch/examples (github.com)
我们只需要稍微修改一下项目结构和dataset即可.接下来就稍微说一下主要修改的地方吧.
dataset其实大部分也是抄的Pytorch,比如transform那部分,最后一个Normalize我就第一次见(标准差都是0.5???),虽然很像改成经常见面的ImageNet的数据,但是二次元好像和ImageNet也不搭(
具体来说就是把图片整到img文件夹下,然后直接用ImageFolder创建dataset.这里注意img下面还需要子文件夹放图片,这个子文件夹对于ImageFolder来说就是不同的类别.当然我们这里不需要用到类别标签,因为所有图片的label都是1,意味着是真实图片.
class ReadData():
def __init__(self,data_path,image_size=64):
self.root = data_path
self.image_size = image_size
self.dataset = self.getdataset()
def getdataset(self):
dataset = datasets.ImageFolder(
root=self.root,
transform=transforms.Compose([
transforms.Resize(self.image_size),
transforms.CenterCrop(self.image_size),
transforms.ToTensor(),
transforms.Normalize((0.5,0.5,0.5),(0.5,0.5,0.5)),
])
)
print(f'Total size of dataset:{len(dataset)}')
return dataset
def getdataloader(self,batch_size=128):
dataloader = DataLoader(
self.dataset,
batch_size=batch_size,
shuffle=True,
num_workers=8
)
return dataloader
if __name__ == '__main__':
dset = ReadData('./imgs')
print("ok")
dloader = dset.getdataloader()Generator和Discriminator两个类没什么好说的,都是Pytorch的(甚至变量名都没改) 注意这里还有一个权重初始化函数weights_init
class Generator(nn.Module):
def __init__(self,nz,ngf,nc):
super(Generator,self).__init__()
self.nz = nz
self.ngf = ngf
self.nc = nc
self.main = nn.Sequential(
# input is Z, going into a convolution
nn.ConvTranspose2d(self.nz, self.ngf * 8, 4, 1, 0, bias=False),
nn.BatchNorm2d(self.ngf * 8),
nn.ReLU(True),
# state size. (ngf*8) x 4 x 4
nn.ConvTranspose2d(self.ngf * 8, self.ngf * 4, 4, 2, 1, bias=False),
nn.BatchNorm2d(self.ngf * 4),
nn.ReLU(True),
# state size. (ngf*4) x 8 x 8
nn.ConvTranspose2d(self.ngf * 4, self.ngf * 2, 4, 2, 1, bias=False),
nn.BatchNorm2d(self.ngf * 2),
nn.ReLU(True),
# state size. (ngf*2) x 16 x 16
nn.ConvTranspose2d(self.ngf * 2, self.ngf, 4, 2, 1, bias=False),
nn.BatchNorm2d(self.ngf),
nn.ReLU(True),
# state size. (ngf) x 32 x 32
nn.ConvTranspose2d(self.ngf, self.nc, 4, 2, 1, bias=False),
nn.Tanh()
# state size. (nc) x 64 x 64
)
def forward(self,input):
return self.main(input)
class Discriminator(nn.Module):
def __init__(self,ndf,nc) -> None:
super().__init__()
self.ndf=ndf
self.nc=nc
self.main=nn.Sequential(
# input is (nc) x 64 x 64
nn.Conv2d(self.nc, self.ndf, 4, 2, 1, bias=False),
nn.LeakyReLU(0.2, inplace=True),
# state size. (ndf) x 32 x 32
nn.Conv2d(self.ndf, self.ndf * 2, 4, 2, 1, bias=False),
nn.BatchNorm2d(self.ndf * 2),
nn.LeakyReLU(0.2, inplace=True),
# state size. (ndf*2) x 16 x 16
nn.Conv2d(self.ndf * 2, self.ndf * 4, 4, 2, 1, bias=False),
nn.BatchNorm2d(self.ndf * 4),
nn.LeakyReLU(0.2, inplace=True),
# state size. (ndf*4) x 8 x 8
nn.Conv2d(self.ndf * 4, self.ndf * 8, 4, 2, 1, bias=False),
nn.BatchNorm2d(self.ndf * 8),
nn.LeakyReLU(0.2, inplace=True),
# state size. (ndf*8) x 4 x 4
nn.Conv2d(self.ndf * 8, 1, 4, 1, 0, bias=False),
# state size. (1) x 1 x 1
nn.Sigmoid()
)
def forward(self,input):
return self.main(input)
def weights_init(m):
classname = m.__class__.__name__
if classname.find('Conv') != -1:
nn.init.normal_(m.weight.data,0.0,0.02)
elif classname.find('BatchNorm') != -1:
nn.init.normal_(m.weight.data,1.0,0.02)
nn.init.constant_(m.bias.data, 0)模型训练主要是之前提到的V(G,D)函数的实现,我们其实可以发现它的形式很像二元交叉熵.所以这里直接用BCELoss就可以实现.BCELoss — PyTorch 2.0 documentation
$$ BCELoss = -(y\log(x)+(1-y)\log(1-x)) $$
$$ BCELoss_{D(x)} = -\log(D(x)) $$
$$ BCELoss_{D(G(z))}=-\log(1-D(G(z))) $$
$$ BCELoss_{D(G(z))}=-log(D(G(z))) $$
其他的就没什么不一样啦~大家直接看Pytorch源码即可.







上述对比警示我们: D模型太好G模型是会摆烂的
最后代码放在仓库里了.JJJYmmm/Pytorch-DCGAN-Anime (github.com)
做了5题蓝桥杯中等题的JJJYmmm身心十分疲倦,决定入门一下生成式对抗网络(GAN).本次看的是开山之作: Ian Goodfellow的Generative Adversarial Nets
摘要指出本篇工作提出了一个新的框架,通过两个模型相互对抗来提高他们的生成(检测)效果.具体来说:一个是捕获数据分布(这个会在价值函数中体现)的生成模型G,还有一个是判断样本来自真实数据还是G的判别模型D.
G的训练目标是使D出错的概率最大化,最后希望达到的效果就是G生成的数据足够"以假乱真",D无法分别其真实性,在这种情况下,生成器G的分布$P_g=P_{data}$(P为概率密度函数),且D的输出恒为1/2.
这里的G/D可以简单都是用MLP,那么模型训练只需要反向更新即可,也不需要使用马尔科夫链或者近似推理网络.
GAN属于隐式生成网络,它不需要显示表示真实数据的分布$P_{data}$,这个分布蕴含在G中,我们只关注从这个分布采样出来的样本点即可,即G(z).
引言基本是摘要的一个补充,主要谈了一下当时深度学习做生成模型效果不太好.之前做生成模型的思想主要是通过一些可学习参数去构造真实的数据分布$P_{data}$,这些参数通过调整尽量使$P_g=P_{data}$,调整的方式就是通过最大化对数似然函数.具体可以见我之前的博客~
本次提出的GAN则属于隐式生成模型,不需要知道$P_g$的具体形式,只需要最后检测模型D分不出来就可以了.
讲了一下VAEs/NCE,还有jurgen在92年提到的predictability minimization.据说jurgen作为这篇论文的审稿人review很有意思~
接下来就讲到价值函数啦,非常关键的一个地方.当然这里还需要对模型的输入进行进一步解释.
当G和D都是MLP的时候,GAN非常容易应用.首先是为了学习生成器在数据x上的分布$P_g$,先要定义输入噪声z的先验分布$p_z(z)$,然后通过MLP得到一个映射关系$G(z;\theta_g)$.接下来对于检验器D,MLP的映射$D(x,\theta_d)$输出一个向量,1表示x来自真实样本,0表示来自G.G的工作是让$log(1-D(G(z)))$尽可能小,而D的工作是让$log(D(x))$尽可能大的同时让$log(1-D(G(z)))$也尽可能大.根据上述描述可以得到价值函数V(G,D)
$$ \mathop{min}\limits_{G}\mathop{max}\limits_{D}V(G,D)=\mathbb{E}_{x\sim p_{data(X)}[logD(x)]} + \mathbb{E}_{z\sim p_{z(z)}log[1-D(G(z))]} $$
GAN的训练过程见算法1,先k步迭代检测器D,在迭代生成器G.

训练过程可以总结为一下四张图,假设数据x和噪声z都是一维向量.x的分布是灰色的正态分布,G(z)是绿色的正态分布,而检测器D是虚线.首先如图b训练D,它对x和g(z)做了一定区分;接下来训练G,让G(z)的分布曲线往x靠近,如图c.最后当两个分布曲线足够贴合时,检测器D无法分辩任何东西,所以是一条直线.

理论结果有两个:首先给出固定G的最佳辨别器$D^*$,其次是证明了价值函数的有效性.
对于固定的G,最佳分辨器$D^*$为
$$ D^*_G(x) = \frac{p_{data}(x)}{p_{data}(x)+p_g(X)} $$
证明通过对V(G,D)进行变换得到,首先将g(z)换元成x
$$ \begin{align*} V(G,D)&=\int_{x}p_{\mathrm{data}}(x)\log(D(x))d x+\int_{\cdot}p_{z}(z)\log(1-D(g(z)))dz\\ &=\int_{x}p_{\mathrm{data}}(x)\log(D(x))+p_{g}(x)\log(1-D(x)) dx \end{align*} $$
上式则可以转变成以下形式$\ y\to a\log(y)+b\log(1-y)$,其中a,b代表着data和G的数据分布,这个函数是凸函数,可以得到最优点时$y=\frac{a}{a+b}$.那么$D^*$得证
接下来我们可以把$D^*$带入V(G,D)中,得到
$$ \begin{align*} C(G)&=\mathop{max}\limits_{D}V(G,D) \\ \\ &=\mathbb{E}_{x\sim p_{\mathrm{data}}}[\log D_{G}^{*}(x)]+\mathbb{E}_{z\sim p_{z}}[\log(1-D_{G}^{*}(G(z)))]\\ \\ &=\mathbb{E}_{x\sim p_{\mathrm{data}}}[\log D_{G}^{*}(x)]+\mathbb{E}_{x\sim p_{g}}[\log(1-D_{G}^{*}(x))]\\ \\ &=\mathbb{E}_{x\sim p_{\mathrm{{data}}}}\left[\log{\frac{p_{data}(x)}{p_{\mathrm{data}}(x)+p_{g}(x)}}\right]+{\mathbb{E}_{x\sim p_{g}}}\left[\log{\frac{p_{g}(x)}{p_{\mathrm{data}}(x)+p_{g}(x)}}\right] \end{align*} $$
接下来我们考虑在D*的情况下V(G,D)如何达到最小,即G也训练到极致,我们对刚刚的C(G)做如下变形,具体来说就是在分式那个地方乘1/2,使其也变成一个分布.(因为之前两个分布求积分结果是2,如果/2积分结果就是1,可以看成是一个新分布)
再根据KL散度的定义可以把C(G)表示成如下形式,KL>=0,取等条件就是两个分布相等,根据两个KL散度可以得到$p_g=p_{data}$时,C(G)有最小值$-log(4)$
$$ C(G)=-\log(4)+K L\left(p_{\mathrm{data}}\Big|\Big|\frac{p_{\mathrm{data}}+p_{g}}{2}\right)+K L\left(p_{g}\Big|\Big|\frac{p_{\mathrm{data}}+p_{g}}{2}\right) $$
对于这种对称性的KL散度,我们还有一个名称称呼它:琴生-香农散度,所以C(G)进一步表示成如下
$$ C(G)=-\log(4)+2\cdot J S D\left(p_{\mathrm{data}}||p_{g}\right) $$
第二个理论证明是假定在G/D具有充分能力的情况下,通过算法1,每次D都能迭代到D*.这样G最终可以使$p_g$收敛到$p_{data}$.这个主要是通过说明函数的凹凸性来证明的.具体来说就是把V(G,D)写成$p_g$的函数,即函数的函数,然后balaba....不是非常懂,直接贴原文了

实验就是生成数字/人脸这些....没什么好说的(不过在当时应该算是效果挺好的吧)

本节主要是学GAN的时候附带了解一下显式生成模型的基本原理~
生成模型分为显式和隐式两种,显示就是具体求出(拟合)数据分布$P_{data}$,可以使用极大似然法.假设$P_g$是生成模型产生的数据分布,$\theta$是模型的参数.
首先对所有数据样本计算似然函数$L(\theta)$:
$$ L(\theta) = \prod_{i=1}^N{P_g(x^{(i)};\theta)} $$
似然函数是一个关于模型参数$\theta$的函数,当选择不同的参数$\theta$时,似然函数的值是不同的,它描述了在当前参数下,使用模型分布$P_g(x;\theta)$产生数据集中所有样本的概率。一个朴素的想法是:在最好的模型参数$\theta_{ML}$下,产生数据集中的所有样本的概率是最大的,即
$$ \theta_{ML}=argmaxL(\theta) $$
但实际在计算机中,多个概率的乘积结果并不方便储存,例如计算过程中可能发生数值下溢的问题,即对比较小的、接近于0的数进行四舍五入后成为0。我们可以对似然函数取对数来缓解该问题,并且仍然求解最好的模型参数使对数似然函数最大,即
$$ \theta_{ML}=argmax \sum_{i=1}^Nlog[P_g(x^{(i)};\theta)] $$
可以发现,使用极大似然估计时,每个样本$x_i$都希望拉高它所对应的模型概率值$P_g(x^{(i});\theta)$,如图所示,但是由于所有样本的密度函数的总和必须是1,所以不可能将所有样本点都拉高到最大的概率;一个样本点的概率密度函数值被拉高将不可避免的使其他点的函数值被拉低,最终将达到一个平衡态。
对于刚刚的$log$和,我们可以除以$N$,那么$\theta_{ML}$的含义就是尽可能达到经验分布$\hat{P}_{data}$下样本概率对数的期望值.
$$ \theta_{ML}=argmax\space\mathbb{E}_{\hat{P}_{data}} log[P_g(x^{(i)};\theta)] $$
还有一种对极大似然估计的理解:最小化训练集上的经验分布$\hat{P}_{data}(x)$和模型分布$P_g(x;\theta)$之间的KL散度值(这个在之前的机器学习里也提到过),即
$$ \theta_{ML}=argminD_{KL}(\hat{p}_{data}||P_g) $$
KL散度的表达式是
$$ D_{KL}(\hat{p}_{data}||P_g) = \mathbb{E}_{\hat{P}_{data}}[ log\hat{p}_{data}(x)-logP_g(x;\theta)] $$
因为第一项与$\theta$无关,所以可以写成
$$ \theta_{ML}=argmax \sum_{i=1}^Nlog[P_g(x^{(i)};\theta)] $$
可以发现两者是完全一样的,也就是说极大似然估计就是希望$P_g$和$P_{data}$尽量相似,最好相似到无任何差异(KL散度值为0),这与生成模型的思想是一致的。但实际的生成模型一般不可能提前知道$P_g$的表达式形式而只需要估计表达式中的参数,实际中的生成模型非常复杂,往往对$P_g$无任何先验知识,只能对其进行一些形式上的假设或近似。

RPNHead的主要功能是
- 给定图片的各个特征预测层,通过conv3x3滑动窗口,预测每个锚框的前景概率和回归参数
__init__通过滑动窗口预测目标概率和bbox regression参数

forward 在多个特征层预测,把3x3conv作为滑动窗口生成预测结果.

AnchorsGenerator类的主要功能是
- 给定图片的各个特征层,在每个特征层上都生成特定大小和尺寸的锚框
__init__ 处理anchor的sizes和ratios

forward 首先生成anchor模板,再获取每个特征图上的anchor在原图上的坐标信息.随后遍历每张图像,将所有anchor存入anchors中,anchors是list of list,第一个维度是不同图片,第二个维度是不同特征层.最后通过cat操作消除第二个维度,即把一个图片的不同特征图的anchor全部汇总起来.至此AnchorGenerator生成anchor的工作结束.

generate_anchors 生成anchors模板,首先拓展ratios和scales的维度并相乘(广播机制),获得不同尺度不同比例的anchor对应的宽和高,再将坐标摞起来,得到anchor模板的左上角(右下角)坐标.

cache_grid_anchors 对于anchor在原图上的位置信息做了一个cache缓存操作

grid_anchors 得到anchor模板在原图上的坐标信息,这里主要用到了meshgrid函数生成网格坐标,先生成特征图的每个像素中心点在原图上的坐标信息,再和anchor模板坐标相加即可(因为anchor模板默认中心点是(0,0))

BoxCoder类主要工作是
- 已知两个Box计算regression回归参数
- 已知一个Box和regression计算回归后的Box
encode 根据anchor和对应的GTBox计算regression参数。这里先把所有图片的reference_boxes和proposals拼接在一起,最后返回的时候再根据每张图片的anchor个数分离,加快运算效率。

encode_single/encode_boxes 根据proposal坐标得到其宽高(ex_widths/ex_heights)和中心点坐标(ex_ctr_x/ex_ctr_y).同理计算GTBox的宽高和中心点.

最后计算四个回归参数dx/dy/dw/dh.参照以下公式.

decode 思路和encode相反,参数为proposal和回归参数regresssion,计算最终的proposal坐标.

decode_single 思路与encode_single相反.

计算公式如下.

Matcher的主要功能是
- 将真实框分配给锚框,并划分正负样本
_call_ 根据IoU矩阵为每个proposal分配真实框,并根据IoU大小划分正负样本.

BalancedPositiveNegativeSample的主要功能:
- 随机采样正负样本,挑选指定比例的样本参与损失计算
__call__ call函数的参数是matched_idxs,数据类型是List of Tensor,List代表不同图像,Tensor代表每幅图像中每个Proposals的正负样本类型,为-1时代表样本被忽略;为0时代表负样本;>0时代表正样本

首先通过条件过滤出正负样本的下标,再根据正样本的比例(positive_fraction)选取正负样本的数量.注意如果正/负样本不足时,需要采样所有正/负样本.
正负样本的随机采样使用randperm函数,

最后根据选出的正负样本下标制作掩码并返回.

RegionProposalNetwork类是RPN网络的主体,调用了Matcher/BoxCoder等类来完成RPN的功能
__init__
forward 首先将特征图丢入head中获得预测类别和边界框偏移的结果,再通过anchor_generator生成anchors.
接下来计算每个特征层上的anchors的数量,用于之后的过滤操作.
利用Box_coder的decode操作将pred_bbox_deltas应用在生成的anchors上,获得propoals的坐标信息,在这里做NMS操作.

最后为预测的proposal分配GTBox,并计算损失.

concat_box_prediction_layers 对box_cls和box_regression两个list中的每个预测特征层的预测信息的tensor排列顺序以及shape进行调整 -> [N, -1, C]
C是num_classes or 4(bbox regression)

permute_and_flatten 主要工作是交换维度,把C维度换到最后.利于之后计算损失.

filter_proposals 此方法调用topk方法过滤每个特征层的proposals.
首先用level变量存储不同特征层上的anchors索引信息(因为这个时候不同特征层的anchor在之前的处理中存储在一起了,这里需要用level来区分不同的特征层anchor)
随后调用_get_top_n_idx获取每层特征图中预测概率排前pre_nms_top_n的anchor的索引值,存入top_n_idx.利用双重索引获得这些anchor的objectness和proposal.
最后过滤得到的proposal对应的objectness记得做一次sigmod~

最后对于过滤的proposals进行边缘的裁剪,并去除小概率的proposal.最后再按照不同次执行一次batch_nms.
batch_nms是按照某个类别/预测层分别执行nms,当然底层实现更加高效.具体见下文

clip_boxes_to_image 利用clamp函数将边界框限制在图片范围内

remove_small_boxes 过滤宽高不满足条件的proposal

batch_nms 为了高效实现不同类别分别做NMS,首先为不同的类别生成一个足够大的offset,保证不同类别之间的boxes不会重叠,从而保证了在各自类别内做NMS的效果.

assign_targets_to_anchors 将真实框分配给锚框,并划分正负样本.主要使用proposal_matcher来实现匹配.具体方法见Matcher类实现.

compute_loss 首先通过BalancedPositiveNegativeSampler类选择正负样本,并将正负样本的索引拼接在一起,用来计算目标预测损失.

随后计算正样本的边界框回归损失(smooth_L1_Loss)和所有样本的物体预测损失(BCE_Loss)
