近期,上海人工智能实验室主办的首届“浦江AI学术年会”聚集了全球150余名人工智能领域的专家学者,讨论了诸如大模型的未来、智能定义、Scaling Law(尺度定律)等前沿议题。在与会者的深入讨论中,马毅教授提出了大模型的“白盒”理论,强调大模型的可解释性。与会者还探讨了人工智能在未来的发展方向,包括如何突破当前大模型在推理、情感、伦理等方面的瓶颈。 白盒大模型与黑盒大模型的对比 马毅认为,现有的深度学习模型,如Transformer架构,能够从高维信号中压缩、去噪,并找到核心规律,但其黑盒特性限制了对决策过程的解释。相比之下,白盒大模型则试图提供一个可解释的框架,从而减少试错成本并解决当前的大模型在“数据墙”和“算力墙”上的限制。 “Scaling Law”的挑战 论坛中,马毅和其他学者讨论了Scaling Law的局限性,特别是在数据和算力的瓶颈下,Scaling Law可能已经走到了尽头。OpenAI前首席科学家Ilya Sutskever也指出,依赖海量数据的预训练模型将面临难以为继的问题。对此,研究人员纷纷提出了新的可能方向,认为应该探索更有效的模型架构和学习方法。 面向2025年的“中国思考” 随着AI领域的竞争日益激烈,中国的AI研究者正提出自己的技术路线,以应对未来挑战。上海人工智能实验室发布了“书生InternThinker”模型,这一模型通过模拟人类学习方式进行复杂推理,展现出深度推理与专业泛化能力的结合。未来,大模型的发展将更多聚焦在高难度科学问题的解决,以及大模型在稳定性和实际应用方面的突破。 未来展望 中国人工智能的未来发展将侧重于提升模型的推理能力、情感理解和多模态的融合。清华大学的刘知远教授提出了大模型的“密度定律”,预计模型的能力密度将指数级增长,未来的模型将具有更高的计算能力和更加高效的制造工艺。 本次年会也提醒我们,人才仍然是推动人工智能发展的核心力量,年轻科研人员的培养和团队协作将是AI未来发展的关键因素。
1-机器学习介绍人工智能是我们想要达成的目标,而机器学习是想要达成目标的手段,希望机器通过学习方式,他跟人一样聪明。深度学习则是是机器学习的其中一种方法。简而言之就是:机器学习是一种实现人工智能的方法,深度学习是一种实现机器学习的技术。机器学习和深度学习机器学习机器学习最基本的做法,是使用算法来解析数据、从中学习,然后对真实世界中的事件做出决策和预测。与传统的为解决特定任务、硬编码的软件程序不同,机器学习是用大量的数据来“训练”,通过各种算法从数据中学习如何完成任务。这里有三个重要的信息:机器学习是模拟、延伸和扩展人的智能的一条路径,所以是人工智能的一个子集;“机器学习”是要基于大量数据的,也就是说它的“智能”是用大量数据喂出来的;正是因为要处理海量数据,所以大数据技术尤为重要;“机器学习”只是大数据技术上的一个应用。 常用的10大机器学习算法有:决策树、随机森林、逻辑回归、SVM、朴素贝叶斯、K最近邻算法、K均值算法、Adaboost算法、神经网络、马尔科夫。深度学习深度学习是用于建立、模拟人脑进行分析学习的神经网络,并模仿人脑的机制来解释数据的一种机器学习技术。 它的基本特点,是试图模仿大脑的神经元之间传递,处理信息的模式。深度学习本来并不是一种独立的学习方法,其本身也会用到有监督和无监督的学习方法来训练深度神经网络。但由于近几年该领域发展迅猛,一些特有的学习手段相继被提出(如残差网络),因此越来越多的人将其单独看作一种学习的方法。但是目前深度学习也存在着相应的问题:深度学习模型需要大量的训练数据,才能展现出神奇的效果,但现实生活中往往会遇到小样本问题,此时深度学习方法无法入手,传统的机器学习方法就可以处理。有些领域,采用传统的简单的机器学习方法,可以很好地解决了,没必要非得用复杂的深度学习方法。深度学习的思想,来源于人脑的启发,但绝不是人脑的模拟,人类的学习过程往往不需要大规模的训练数据,而现在的深度学习方法显然不是对人脑的模拟。机器学习相关技术机器学习可以主要分为:监督学习半监督学习迁移学习无监督学习监督学习中的结构化学习强化学习这张图主要是对机器学习进行的相应的细分,我们首先对图的左上角监督学习进行讲解监督学习定义:根据已有的数据集,知道输入和输出结果之间的关系。根据这种已知的关系,训练得到一个最优的模型。 也就是说,在监督学习中训练数据既有特征(feature) 又有标签(label),通过训练,让机器可以自己找到特征和标签之间的联系,在面对只有特征没有标签的数据时,可以判断出标签。 通俗一点,可以把机器学习理解为我们教机器如何做事情。监督学习分为:回归问题(Regression)分类问题(classification)经典的算法:支持向量机、线性判别、决策树、朴素贝叶斯回归问题回归问题是针对于连续型变量的。 回归通俗一点就是,对已经存在的点(训练数据)进行分析,拟合出适当的函数模型y=f(x),这里y就是数据的标签,而对于一个新的自变量x,通过这个函数模型得到标签y。分类问题回归问题Regression和分类问题Classification的差别就是我们要机器输出的东西的类型是不一样。在回归问题中机器输出的是一个数值,在分类问题里面机器输出的是类别,和回归最大的区别在于,分类是针对离散型的,输出的结果是有限的。其中分类问题分为两种:二分类,输出是或否。例如判断肿瘤为良性还是恶性多分类,在多个选项中选择正确的类别。例如输入一张图片判读是猫是狗还是猪 简单来说分类就是,要通过分析输入的特征向量,对于一个新的向量得到其标签。以上是让机器去解决的问题,解决问题的第一步就是选择解决问题的函数,也就是选择解决问题所需要的模型。 主要为线性模型和非线性模型半监督学习传统的机器学习技术分为两类,一类是无监督学习,一类是监督学习。无监督学习只利用未标记的样本集,而监督学习则只利用标记的样本集进行学习。但在很多实际问题中,只有少量的带有标记的数据,因为对数据进行标记的代价有时很高,比如在生物学中,对某种蛋白质的结构分析或者功能鉴定,可能会花上生物学家很多年的工作,而大量的未标记的数据却很容易得到。这就促使能同时利用标记样本和未标记样本的半监督学习技术迅速发展起来。简而言之,半监督学习就是去减少标签(label) 的用量。半监督学习是归纳式的,生成的模型可用做更广泛的样本半监督学习算法分类:self-training(自训练算法)generative models生成模型SVMs半监督支持向量机graph-basedmethods图论方法multiview learing多视角算法其他方法迁移学习目标: 将某个领域或任务上学习到的知识或模式应用到不同但相关的领域或问题中。主要思想: 从相关领域中迁移标注数据或者知识结构、完成或改进目标领域或任务的学习效果。例如人类学会了骑自行车,那么骑摩托车就会很简单,学会了C语言之后,学习其他语言也会很简单,这就是人类学习具有举一反三的能力,那么机器是否也可以具有举一反三的学习能力呢? 上图是一个商品评论情感分析的例子,图中包含两个不同的产品领域:books 图书领域和 furniture 家具领域;在图书领域,通常用“broad”、“quality fiction”等词汇来表达正面情感,而在家具领域中却由“sharp”、“light weight”等词汇来表达正面情感。可见此任务中,不同领域的不同情感词多数不发生重叠、存在领域独享词、且词汇在不同领域出现的频率显著不同,因此会导致领域间的概率分布失配问题。迁移学习的关键点:研究可以用哪些知识在不同的领域或者任务中进行迁移学习,即不同领域之间有哪些共有知识可以迁移。研究在找到了迁移对象之后,针对具体问题所采用哪种迁移学习的特定算法,即如何设计出合适的算法来提取和迁移共有知识。研究什么情况下适合迁移,迁移技巧是否适合具体应用,其中涉及到负迁移的问题。(负迁移是旧知识对新知识学习的阻碍作用,比如学习了三轮车之后对骑自行车的影响,和学习汉语拼音对学英文字母的影响研究如何利用正迁移,避免负迁移)已有的迁移学习方法大致可以分为三类:基于样本的迁移学习方法基于特征的迁移学习方法基于模型的迁移学习方法 以下分别介绍上述三种类型的迁移学习方法:基于样本的迁移学习方法 核心思想: 从源域数据集中筛选出部分数据,使得筛选出的部分数据与目标数据概率分布近似。基于样本选择的方法:假设源域与目标域样本条件分布不同但边缘分布相似,可以根据基于距离度量的方法和基于元学习的方法等方法进行样本选择。基于样本权重的方法:假设源域与目标域样本条件分布相似但边缘分布不同,通过概率密度比指导样本权重学习。基于特征的迁移学习方法 基于最大均值差异(MMD)的迁移学习方法:将源域与目标域样本映射到可再生和希尔特空间(RKHS),并最小化二者之间的差异。基于模型的迁移学习方法(深度学习) 深度神经网络浅层学习通用特征,深层学习与任务相关的特殊特征。神经网络迁移性总结:神经网络的前几层基本都是通用特征,迁移的效果比较好深度迁移网络中加入微调,效果提升比较大,可能会比原网络效果好微调可以比较好的克服数据之间的差异性深度迁移网络要比随机初始化权重效果好网络层数的迁移可以加速网络的学习和优化无监督学习定义: 我们不知道数据集中数据、特征之间的关系,而是要根据聚类或一定的模型得到数据之间的关系。 可以这么说,比起监督学习,无监督学习更像是自学,让机器学会自己做事情,是没有标签(label)的。无监督学习使我们能够在几乎不知道或根本不知道结果应该是什么样子的情况下解决问题。我们可以从不需要知道变量影响的数据中得到结构。我们可以根据数据中变量之间的关系对数据进行聚类,从而得到这种结构。在无监督学习中,没有基于预测结果的反馈。经典算法:聚类K-means算法(K均值算法),主成分分析监督学习中的结构化学习structured learning 中让机器输出的是要有结构性的。在分类的问题中,机器输出的只是一个选项;在有结构的类的问题里面,机器要输出的是一个复杂的物件。在语音识别的情境下,机器的输入是一个声音信号,输出是一个句子;句子是由许多词汇拼凑而成,它是一个有结构性的object机器翻译、人脸识别(标出不同的人的名称),比如GAN也是structured Learning的一种方法。强化学习定义: 强化学习是机器学习的一个重要分支,是多学科多领域交叉的一个产物,它的本质是解决 decision making 问题,即自动进行决策,并且可以做连续决策。它主要包含四个元素:agent,环境状态,行动,奖励强化学习的目标就是获得最多的累计奖励。强化学习和监督式学习的区别:监督式学习就好比在学习的时候有老师在指导老师怎么是对的怎么是错的,但在很多实际问题中,例如:西洋棋、围棋有几千万种博弈方式的情况,不可能有一个老师知道所有可能的结果。然而强化学习会在没有任何标签的情况下,通过先尝试做出一些行为得到一个结果,通过这个结果是对还是错的反馈,调整之前的行为,就这样不断的调整,算法能够学习到在什么样的情况下选择什么样的行为可以得到最好的结果。两种学习方式都会学习出输入到输出的一个映射,监督式学习出的是之间的关系,可以告诉算法什么样的输入对应着什么样的输出,强化学习出的是给机器的反馈,即用来判断这个行为是好是坏。另外强化学习的结果反馈有延时,有时候可能需要走了很多步以后才知道以前的某一步的选择是好还是坏,而监督学习做了比较坏的选择会立刻反馈给算法。而且强化学习面对的输入总是在变化,每当算法做出一个行为,它影响下一次决策的输入,而监督学习的输入是独立同分布的。通过强化学习,一个 agent 可以在探索和开发(exploration and exploitation)之间做权衡,并且选择一个最大的回报。exploration 会尝试很多不同的事情,看它们是否比以前尝试过的更好。exploitation 会尝试过去经验中最有效的行为。一般的监督学习算法不考虑这种平衡。 强化学习和非监督式学习的区别:非监督式不是学习输入到输出的映射,而是模式。例如在向用户推荐新闻文章的任务中,非监督式会找到用户先前已经阅读过类似的文章并向他们推荐其一。而强化学习将通过向用户先推荐少量的新闻,并不断获得来自用户的反馈,最后构建用户可能会喜欢的文章的“知识图”。主要算法和分类从强化学习的几个元素的角度划分的话,方法主要有下面几类:Policy based, 关注点是找到最优策略。Value based, 关注点是找到最优奖励总和。Action based, 关注点是每一步的最优行动。2-为什么要学习机器学习机器学习可以更快且自动的产生模型,以分析更大,更复杂的数据,而且传输更加迅速,结果更加精准——甚至是在非常大的规模中。在现实中无人类干涉时,高价值的预测可以产生更好的决定,和更明智的行为。
写在前面 前段时间,我已经写过一篇关于GAN的理论讲解,并且结合理论做了一个手写数字生成的小案例, 为唤醒大家的记忆,这里我再来用一句话对GAN的原理进行总结:GAN网络即是通过生成器和判别器的不断相互对抗,不断优化,直到判别器难以判断生成器生成图像的真假。 那么接下来我就要开始讲述DCGAN了喔,读到这里我就默认大家对GAN的原理已经掌握了,开始发车。🚖🚖🚖DCGAN简介 我们先来看一下DCGAN的全称——Deep Convolutional Genrative Adversarial Networks。这大家应该都能看懂叭,就是说这次我们将生成对抗网络和深度学习结合到一块儿了,现在看这篇文章的一些观点其实觉得是很平常的,没有特别出彩之处,但是这篇文章是在16年发布的,在当时能提出一些思想确实是难得。 其实呢,这篇文章的原理和GAN基本是一样的。不同之处只在生成网络模型和判别网络模型的搭建上,因为这篇文章结合了深度学习嘛,所以在模型搭建中使用了卷积操作【注:在上一篇GAN网络模型搭建中我们只使用的全连接层】。介于此,我不会再介绍DCGAN的原理,重点将放在DCGAN网络模型的搭建上。【注:这样看来DCGAN就很简单了,确实也是这样的。但是大家也不要掉以轻心喔,这里还是有一些细节的,我也是花了很长的时间来阅读文档和做实验来理解的,觉得理解差不多了,才来写了这篇文章。】 那么接下来就来讲讲DCGAN生成模型和判别模型的设计,跟我一起来看看叭!!!DCGAN生成模型、判别模型设计 在具体到生成模型和判别模型的设计前,我们先来看论文中给出的一段话,如下图所示:这里我还是翻译一下,如下图所示:上图给出了设计生成模型和判别模型的基本准则,后文我们搭建模型时也是严格按照这个来的。【注意上图黄色背景的分数卷积喔,后文会详细叙述】生成网络模型 话不多说,直接放论文中生成网络结构图,如下:图1 生成网络模型 看到这张图不知道大家是否有几秒的迟疑,反正我当时是这样的,这个结构给人一种熟悉的感觉,但又觉得非常的陌生。好了,不卖关子了,我们一般看到的卷积结构都是特征图的尺寸越来越小,是一个下采样的过程;而这个结构特征图的尺寸越来越大,是一个上采样的过程。那么这个上采样是怎么实现的呢,这就要说到主角分数卷积了。【又可以叫转置卷积(transposed convolution)和反卷积(deconvolution),但是pytorch官方不建议取反卷积的名称,论文中更是说这个叫法是错误的,所以我们尽量不要去用反卷积这个名称,同时后文我会统一用转置卷积来表述,因为这个叫法最多,我认为也是最贴切的】转置卷积理论 这里我将通过一个小例子来讲述转置卷积的步骤,并通过代码来验证这个步骤的正确性。首先我们先来看看转置卷积的步骤,如下:在输入特征图元素间填充s-1行、列0(其中s表示转置卷积的步距,注意这里的步长s和卷积操作中的有些不同)在输入特征图四周填充k-p-1行、列0(其中k表示转置卷积kernel_size大小,p为转置卷积的padding,注意这里的padding和卷积操作中的有些不同)将卷积核参数上下、左右翻转做正常卷积运算(padding=0,s=1) 是不是还是懵逼的状态呢,不用急,现在就通过一个例子来讲述这个过程。首先我们假设输入特征图的尺寸为2*2大小,s=2,k=3,p=0,如下图所示:第一步我们需要在特征图元素间填充s-1=1 行、列 0 (即填充1行0,1列0),变换后特征图如下:第二步我们需要在输入特征图四周填充k-p-1=2 行、列0(即填充2行0,2列0),变换后特征图如下: 第三步我们需要将卷积核上下、左右翻转,得到新的卷积核【卷积核尺寸为k=3】,卷积核变化过程如下:最后一步,我们做正常的卷积即可【注:拿第二步得到的特征图和第三步翻转后得到的卷积核做正常卷积】,结果如下: 至此我们就从完成了转置卷积,从一个2*2大小的特征图变成了一个5*5大小的特征图,如下图所示(忽略了中间步骤): 为了让大家更直观的感受转置卷积的过程,我从Github上down了一个此过程动态图供大家参考,如下:通过上文的讲述,相信你已经对转置卷积的步骤比较清楚了。这时候你就可以试试图1中结构,看看应用上述的方法能否得到对应的结构。需要注意的是,在第一次转置卷积时,使用的参数k=4,s=1,p=0,后面的参数都为k=4,s=2,p=1,如下图所示: 如果你按照我的步骤试了试,可能会发出一些吐槽,这也太麻烦了,我只想计算一下经过转置卷积后特征图的的变化,即知道输入特征图尺寸以及k、s、p算出输出特征图尺寸,这步骤也太复杂了。于是好奇有没有什么公式可以很方便的计算呢?enmmm,我这么说,那肯定有嘛,公式如下图所示:对于上述公式我做3点说明:在转置卷积的官方文档中,参数还有output_padding 和dilation参数也会影响输出特征图的大小,但这里我们没使用,公式就不加上这俩了,感兴趣的可以自己去阅读一下文档,写的很详细。对于stride[0],stride[1]、padding[0],padding[1]、kernel_size[0],kernel_size[1]该怎么理解?其实啊这些都是卷积的基本知识,这些参数设置时可以设置一个整数或者一个含两个整数的元组,*[0]表示在高度上进行操作,*[1]表示在宽度上进行操作。有关这部分在官方文档上也有写,大家可自行查看。为方便大家,我截了一下这部分的图片,如下:这点我带大家宏观的理解一下这个公式,在传统卷积中,往往卷积核k越小、padding越大,得到的特征图尺寸越大;而在转置卷积中,从公式可以看出,卷积核k越大,padding越小,得到的特征图尺寸越大,关于这一点相信你也能从前文所述的转置卷积理论部分有所感受。 现在有了这个公式,大家再去试试叭。转置卷积实验 接下来我将通过一个小实验验证上面的过程,代码如下:import torch import torch.nn as nn #转置卷积 def transposed_conv_official(): feature_map = torch.as_tensor([[1, 2], [0, 1]], dtype=torch.float32).reshape([1, 1, 2, 2]) print(feature_map) trans_conv = nn.ConvTranspose2d(in_channels=1, out_channels=1, kernel_size=3, stride=2, bias=False) trans_conv.load_state_dict({"weight": torch.as_tensor([[1, 0, 1], [1, 1, 0], [0, 0, 1]], dtype=torch.float32).reshape([1, 1, 3, 3])}) print(trans_conv.weight) output = trans_conv(feature_map) print(output) def transposed_conv_self(): feature_map = torch.as_tensor([[0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0], [0, 0, 1, 0, 2, 0, 0], [0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 1, 0, 0], [0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0]], dtype=torch.float32).reshape([1, 1, 7, 7]) print(feature_map) conv = nn.Conv2d(in_channels=1, out_channels=1, kernel_size=3, stride=1, bias=False) conv.load_state_dict({"weight": torch.as_tensor([[1, 0, 0], [0, 1, 1], [1, 0, 1]], dtype=torch.float32).reshape([1, 1, 3, 3])}) print(conv.weight) output = conv(feature_map) print(output) def main(): transposed_conv_official() print("---------------") transposed_conv_self() if __name__ == '__main__': main() 首先我们先通过transposed_conv_official()函数来封装一个转置卷积过程,可以看到我们的输入为[[1,2],[0,1]],卷积核为[[1,0,1],[1,1,0],[0,0,1]],采用k=3,s=2,p=0进行转置卷积【注:这些参数和我前文讲解转置卷积步骤的用例参数是一致的】,我们来看一下程序输出的结果:可以发现程序输出和我们前面理论计算得到的结果是一致的。 接着我们封装了transposed_conv_self函数,这个函数定义的是一个正常的卷积,输入是理论第2步得到的特征图,卷积核是第三步翻转后得到的卷积核,经过卷积后输出结果如下:结果和前面的一致。 那么通过这个例子就大致证明了转置卷积的步骤确实是我们理论步骤所述。判别模型网络 同样的,直接放出判别模型的网络结构图,如下:【注:这部分原论文中没有给出图例,我自己简单画了一个,没有论文中图示美观,但也大致能表示卷积的过程,望大家见谅】这里我给出程序执行的网络模型结构的结果,这部分就结束了:DCGAN人脸生成实战 这部分我们将来实现一个人脸生成的实战项目,我们先来看一下人脸一步步生成的动画效果,如下图所示:我们可以看到随着迭代次数增加,人脸生成的效果是越来越好的,说句不怎么恰当的话,最后生成的图片是像个人的。看到这里,是不是都兴致勃勃了呢,下面就让我们一起来学学叭。 秉持着授人以鱼不如授人以渔的原则,这里我就不带大家一句一句的分析代码了,都是比较简单的,官方文档写的也非常详细,我再叙述一篇也没有什么意义。哦,对了,这部分代码参考的是pytorch官网上DCGAN的教程,链接如下:DCGAN实战教程 我来简单介绍一下官方教程的使用,点击上文链接会进入下图的界面:这个界面正常滑动就是对这个项目的解释,包括原理、代码及代码运行结果,大家首先要做的应该是阅读一遍这个文档,基本可以解决大部分的问题。那么接下来对于不明白的就可以点击下图中绿框链接修改一些代码来调试我们不懂的问题,这样基本就都会明白了。【框1是google提供的一个免费的GPU运算平台,就类似是云端的jupyter notebook ,但这个需要梯子,大家自备;框2 是下载notebook到本地;框3是项目的Github地址】数据集加载 首先我来说一下数据集的加载,这部分不难,却十分重要。对于我们自己的数据集,我们先用ImageFolder方法创建dataset,代码如下:# Create the dataset dataset = dset.ImageFolder(root=dataroot, transform=transforms.Compose([ transforms.Resize(image_size), transforms.CenterCrop(image_size), transforms.ToTensor(), transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)), ])) 需要强调的是root=dataroot表示我们自己数据集的路径,在这个路径下必须还有一个子目录。怎么理解呢,我举个例子。比如我现在有一个人脸图片数据集,其存放在文件夹2下面,我们不能将root的路径指定为文件夹2,而是将文件夹2放入一个新文件夹1里面,root的路径指定为文件夹1。对于上面代码的transforms操作做一个简要的概括,transforms.Resize将图片尺寸进行缩放、transforms.CenterCrop对图片进行中心裁剪、transforms.ToTensor、transforms.Normalize最终会将图片数据归一化到[-1,1]之间,这部分不懂的可以参考我的这篇博文:pytorch中的transforms.ToTensor和transforms.Normalize理解🍚🍚🍚 有了dataset后,就可以通过DataLoader方法来加载数据集了,代码如下:# Create the dataloader dataloader = torch.utils.data.DataLoader(dataset, batch_size=batch_size, shuffle=True, num_workers=workers)生成模型搭建 接下来我们来说说生成网络模型的搭建,代码如下:不知道大家有没有发现pytorch官网此部分搭建的网络模型和论文中给出的是有一点差别的,这里我修改成了和论文中一样的模型,从训练效果来看,两者差别是不大的。【注:下面代码是我修改过的】# Generator Code class Generator(nn.Module): def __init__(self, ngpu): super(Generator, self).__init__() self.ngpu = ngpu self.main = nn.Sequential( # input is Z, going into a convolution nn.ConvTranspose2d( nz, ngf * 16, 4, 1, 0, bias=False), nn.BatchNorm2d(ngf * 16), nn.ReLU(True), # state size. (ngf*16) x 4 x 4 nn.ConvTranspose2d(ngf * 16, ngf * 8, 4, 2, 1, bias=False), nn.BatchNorm2d(ngf * 8), nn.ReLU(True), # state size. (ngf*8) x 8 x 8 nn.ConvTranspose2d( ngf * 8, ngf * 4, 4, 2, 1, bias=False), nn.BatchNorm2d(ngf * 4), nn.ReLU(True), # state size. (ngf*4) x 16 x 16 nn.ConvTranspose2d( ngf * 4, ngf * 2, 4, 2, 1, bias=False), nn.BatchNorm2d(ngf * 2), nn.ReLU(True), # state size. (ngf * 2) x 32 x 32 nn.ConvTranspose2d( ngf * 2, nc, 4, 2, 1, bias=False), nn.Tanh() # state size. (nc) x 64 x 64 ) def forward(self, input): return self.main(input) 我觉得这个模型搭建步骤大家应该都是较为清楚的,但我当时对这个第一步即从一个100维的噪声向量如何变成变成一个1024*4*4的特征图还是比较疑惑的。这里就为大家解答一下,我们可以看看在训练过程中传入的噪声代码,即输入为: noise = torch.randn(b_size, nz, 1, 1, device=device),这是一个100*1*1的特征图,这样是不是一下子恍然大悟了呢,那我们的第一步也就是从100*1*1的特征图经转置卷积变成1024*4*4的特征图。模型训练 这部分我在上一篇GAN网络讲解中已经介绍过,但是我没有细讲,这里我想重点讲一下BCELOSS损失函数。【就是二值交叉熵损失函数啦】我们先来看一下pytorch官网对这个函数的解释,如下图所示:import torch import math input = torch.randn(3,3) target = torch.FloatTensor([[0, 1, 1], [1, 1, 0], [0, 0, 0]]) 来看看输入数据和标签的结果: 接着我们要让输入数据经过Sigmoid函数将其归一化到[0,1]之间【BCELOSS函数要求】:m = torch.nn.Sigmoid() m(input) 输出的结果如下: 最后我们就可以使用BCELOSS函数计算输入数据和标签的损失了:loss =torch.nn.BCELoss() loss(m(input), target) 输出结果如下:大家记住这个值喔!!! 上文似乎只是介绍了BCELOSS怎么用,具体怎么算的好像并不清楚,下面我们就根据官方给的公式来一步一步手动计算这个损失,看看结果和调用函数是否一致,如下:r11 = 0 * math.log(0.8172) + (1-0) * math.log(1-0.8172) r12 = 1 * math.log(0.8648) + (1-1) * math.log(1-0.8648) r13 = 1 * math.log(0.4122) + (1-1) * math.log(1-0.4122) r21 = 1 * math.log(0.3266) + (1-1) * math.log(1-0.3266) r22 = 1 * math.log(0.6902) + (1-1) * math.log(1-0.6902) r23 = 0 * math.log(0.5620) + (1-0) * math.log(1-0.5620) r31 = 0 * math.log(0.2024) + (1-0) * math.log(1-0.2024) r32 = 0 * math.log(0.2884) + (1-0) * math.log(1-0.2884) r33 = 0 * math.log(0.5554) + (1-0) * math.log(1-0.5554) BCELOSS = -(1/9) * (r11 + r12+ r13 + r21 + r22 + r23 + r31 + r32 + r33) 来看看结果叭:你会发现调用BCELOSS函数和手动计算的结果是一致的,只是精度上有差别,这说明我们前面所说的理论公式是正确的。【注:官方还提供了一种函数——BCEWithLogitsLoss,其和BCELOSS大致一样,只是对输入的数据不需要再调用Sigmoid函数将其归一化到[0,1]之间,感兴趣的可以阅读看看】番外篇——使用服务器训练如何保存图片和训练损失 不知道大家运行这个代码有没有遇到这样尬尴的处境:无法科学上网,用不了google提供的免费GPU自己电脑没有GPU,这个模型很难跑完有服务器,但是官方提供的代码并没有保存最后生成的图片和损失,自己又不会改 前两个我没法帮大家解决,那么我就来说说怎么来保存图片和训练损失。首先来说说怎么保存图片,这个就很简单啦,就使用一个save_image函数即可,具体如下图所示:【在训练部分添加】接下来说说怎么保存训练损失,通过torch.save()方法保存代码如下:#保存LOSS G_losses = torch.tensor(G_losses) D_losses = torch.tensor(D_losses) torch.save(G_losses, 'LOSS\\GL') torch.save(D_losses, 'LOSS\\DL')代码执行完后,损失保存在LOSS文件夹下,一个文件为GL,一个为DL。这时候我们需要创建一个.py文件来加载损失并可视化,.py文件内容如下:import torch import torch.utils.data import matplotlib.pyplot as plt #绘制LOSS曲线 G_losses = torch.load('F:\\老师发放论文\\经典网络模型\\GAN系列\\DCGAN\\LOSS\\GL') D_losses = torch.load('F:\\老师发放论文\\经典网络模型\\GAN系列\\DCGAN\\LOSS\\DL') plt.figure(figsize=(10,5)) plt.title("Generator and Discriminator Loss During Training") plt.plot(G_losses,label="G") plt.plot(D_losses,label="D") plt.xlabel("iterations") plt.ylabel("Loss") plt.legend() plt.show() 最后来看看保存的图片和损失,如下图所示:小结 至此,DCGAN就全部讲完啦,希望大家都能有所收获。有什么问题欢迎评论区讨论交流!!!GAN系列近期还会出cycleGAN的讲解和四季风格转换的demo,后期会考虑出瑕疵检测方面的GAN网络,如AnoGAN等等,敬请期待。🏵🏵🏵
写在前面 随着深度学习的发展,已经有很多学者将深度学习应用到物体瑕疵检测中,如列车钢轨的缺陷检测、医学影像中各种疾病的检测。但是瑕疵检测任务几乎都存在一个共同的难题——缺陷数据太少了。我们使用这些稀少的缺陷数据很难利用深度学习训练一个理想的模型,往往都需要进行数据扩充,即通过某些手段增加我们的缺陷数据。 【数据扩充大家感兴趣自己去了解下,GAN网络也是实现数据扩充的主流手段】 上面说到的方法是基于缺陷数据来训练的,是有监督的学习,学者们在漫长的研究中,考虑能不能使用一种无监督的方法来实现缺陷检测呢?于是啊,AnoGAN就横空出世了,它不需要缺陷数据进行训练,而仅使用正常数据训练模型,关于AnoGAN的细节后文详细介绍。AnoGAN 原理详解 首先我们来看看AnoGAN的全称,即Anomaly Detection with Generative Adversarial Networks,中文是指使用生成对抗网络实现异常检测。这篇论文解决的是医学影像中疾病的检测,由于对医学相关内容不了解,本文将完全将该算法从论文中剥离,只介绍算法原理,而不结合论文进行讲述。接下来就随我一起来看看AnoGAN的原理。其实AnoGAN的原理是很简单的,但是我看网上的资料总是说的摸棱两可,我认为主要原因有两点:其一是没有把AnoGAN的原理分步来叙述,其二是有专家视角,它们认为我们都应该明白,但这对于新手来说理解也确实是有一定难度的。 在介绍AnoGAN的具体原理时,我先来谈谈AnoGAN的出发点,这非常重要,大家好好感受。我们知道,DCGAN是将一个噪声或者说一个潜在变量映射成一张图片,在我们训练DCGAN时,都是使用某一种数据进行的,如[2]中使用的数据都是人脸,那么这些数据都是正常数据,我们从一个潜在变量经DCGAN后生成的图片应该也都是正常图像。AnoGAN的想法就是我能否将一张图片M映射成某个潜在变量呢,这其实是较难做到的。但是我们可以在某个空间不断的查找一个潜在变量,使得这个潜在变量生成的图片与图片M尽可能接近。这就是AnoGAN的出发点,大家可能还不明白这么做的意义,下文为大家详细介绍。☘☘☘ AnoGAN其实是分两个阶段进行的,首先是训练阶段,然后是测试阶段,我们一点点来看:训练阶段 训练阶段仅使用正常的数据训练对抗生成网络。如我们使用手写数字中的数字8作为本阶段的数据进行训练,那么8就是正常数据。训练结束后我们输入一个向量z,生成网络会将z变成8。不知道大家有没有发现其实这阶段就是[2]中的DCGAN呢? 【注意:训练阶段已经训练好GAN网络,后面的测试阶段GAN网络的权重是不在变换的】测试阶段 在训练阶段我们已经训练好了一个GAN网络,在这一阶段我们就是要利用训练好的网络来进行缺陷检测。如现在我们有一个数据6,此为缺陷数据 【训练时使用8进行训练,这里的6即为缺陷数据】 。现在我们要做的就是搜索一个潜在变量并让其生成的图片与图片6尽可能接近,具体实现如下:首先我们会定义一个潜在变量z,然后经过刚刚训练的好的生成网络,得到假图像G(z),接着G(z)和缺陷数据6计算损失,这时候损失往往会比较大,我们不断的更新z值,会使损失不断的减少,在程序中我们可以设置更新z的次数,如更新500次后停止,此时我们认为将如今的潜在变量z送入生成网络得到的假图像已经和图片6非常像了,于是我们将z再次送入生成网络,得到G(z)。【注:由于潜在变量z送入的网络是生成图片8的,尽管通过搜索使G(z)和6尽可能相像,但还是存在一定差距,即它们的损失较大】 最后我们就可以计算G(z)和图片6的损失,记为loss1,并将这个损失作为判断是否有缺陷的重要依据。怎么作为判断是否有缺陷的重要依据呢?我再举个例子大家就明白了,现在在测试阶段我们传入的不是缺陷数据,而是正常的数据8,此时应用相同的方法搜索潜在变量z,然后将最终的z送入生成网络,得到G(z),最后计算G(z)和图片8的损失。 【注:由于潜在变量z送入的网络是生成图片8的,所以最后生成的G(z)可以和数据8很像,即它们的损失较小】 通过以上分析, 我们可以发现当我们在测试阶段传入缺陷图片时最终的损失大,传入正常图片时的损失小,这时候我们就可以设置一个合适的阈值来判断图像是否有缺陷了。 这一段是整个AnoGAN的重点,大家多思考思考,相信你可以理解。我也画了一个此过程的流程图,大家可以参考一下,如下: 读了上文,是不是对AnoGAN大致过程有了一定了解了呢!我觉得大家训练阶段肯定是没问题的啦,就是一个DCGAN网络。测试阶段的难点就在于我们如何定义损失函数来更新z值,我们直接来看论文中此部分的损失,主要分为两部分,分别是Residual Loss和Discrimination Loss,它们定义如下:Residual Loss上式z表示潜在变量,G(z)表示生成的假图像,x表示输入的测试图片。上式表示生成的假图像和输入图片之间的差距。如果生成的图片越接近x,则R(z)越小。Discrimination Loss 上式z表示潜在变量,G(z)表示生成的假图像,x表示输入的测试图片。f()表示将通过判别器,然后取判别器某一层的输出结果。 【注:这里使用的并非判别器的最终输出,而是判别器某层的输出,关于这一点,会在代码讲解时介绍】 这里可以把判别器当作一个特征提取网络,我们将生成的假图片和测试图片都输入判别器,看它们提取到特征的差异。同样,如果生成的图片越接近x,则D(z)越小。 求得R(z)和D(z)后,我们定义它们的线性组合作为最终的损失,如下:到这里,AnoGAN的理论部分都介绍完了喔!!!不知道你理解了多少呢?如果觉得有些地方理解还差点儿意思的话,就来看看下面的代码吧,这回对你理解AnoGAN非常有帮助。AnoGAN代码实战 如果大家和我一样找过AnoGAN代码的话,可能就会和我有一样的感受,那就是太乱了。怎么说呢,我认为从原理上来说,应该很好实现AnoGAN,但是我看Github上的代码写的挺复杂,不是很好理解,有的甚至起着AnoGAN的名字,实现的却是一个简单的DCGAN网络,着实让人有些无语。于是我打算按照自己的思路来实现一个AnoGAN,奈何却出现了各种各样的Bug,正当我心灰意冷时,看到了一篇外文的博客,写的非常对我的胃口,于是按照它的思路实现了AnoGAN。这里我还是想感概一下,我发现很多外文的博客确实写的非常漂亮,我想这是值得我们学习的地方!!代码下载地址 本次我将源码上传到我的Github了,大家可以阅读README文件了解代码的使用,Github地址如下:AnoGAN-pytorch实现 我认为你阅读README文件后已经对这个项目的结构有所了解,我在下文也会帮大家分析分析源码,但更多的时间大家应该自己动手去亲自调试,这样你会有不一样的收获。数据读取 本次使用的数据为mnist手写数字数据集,我们下载的是.csv格式的数据,这种格式方便读取。读取数据代码如下: ## 读取训练集数据 (60000,785) train = pd.read_csv(".\data\mnist_train.csv",dtype = np.float32) ## 读取测试集数据 (10000,785) test = pd.read_csv(".\data\mnist_test.csv",dtype = np.float32) 我们可以来看一下mnist数据集的格式是怎样的,先来看看train中的内容,如下: train的shape为(60000,785),其表示训练集中共有60000个数据,即60000张手写数字的图片,每个数据都有785个值。我们来分析一下这785个数值的含义,第一个数值为标签label,表示其表示哪个手写数字,后784个数值为对应数字每个像素的值,手写数字图片大小为28×28,故一共有784个像素值。 解释完训练集数据的含义,那测试集也是一样的啦,只不过数据较少,只有10000条数据,test的内容如下: 大家需要注意的是,上述的训练集和测试集中的数据我们今天并不会全部用到。我们取训练集中的前400个标签为7或8的数据作为AnoGAN的训练集,即7、8都为正常数据。取测试集前600个标签为2、7、8作为测试数据,即测试集中有正常数据(7、8)和异常数据(2),相关代码如下: # 查询训练数据中标签为7、8的数据,并取前400个 train = train.query("label in [7.0, 8.0]").head(400) # 查询训练数据中标签为7、8的数据,并取前400个 test = test.query("label in [2.0, 7.0, 8.0]").head(600) 可以看看此时的train和test的结果: 在AnoGAN中,我们是无监督的学习,因此是不需要标签的,通过以下代码去除train和test中的标签: # 取除标签后的784列数据 train = train.iloc[:,1:].values.astype('float32') test = test.iloc[:,1:].values.astype('float32') 去除标签后train和test的结果如下: 可以看出,此时train和test中已经没有了label类,它们的第二个维度也从785变成了784。 最后,我们将train和test reshape成图片的格式,即28×28,代码如下: # train:(400,784)-->(400,28,28) # test:(600,784)-->(600,28,28) train = train.reshape(train.shape[0], 28, 28) test = test.reshape(test.shape[0], 28, 28) 此时,train和test的维度发生变换,如下图所示: 至此,我们的数据读取部分就为大家介绍完了,是不是发现挺简单的呢,加油吧!!!模型搭建模型搭建真滴很简单!!!大家之间看代码吧。生成模型搭建 """定义生成器网络结构""" class Generator(nn.Module): def __init__(self): super(Generator, self).__init__() def CBA(in_channel, out_channel, kernel_size=4, stride=2, padding=1, activation=nn.ReLU(inplace=True), bn=True): seq = [] seq += [nn.ConvTranspose2d(in_channel, out_channel, kernel_size=kernel_size, stride=stride, padding=padding)] if bn is True: seq += [nn.BatchNorm2d(out_channel)] seq += [activation] return nn.Sequential(*seq) seq = [] seq += [CBA(20, 64*8, stride=1, padding=0)] seq += [CBA(64*8, 64*4)] seq += [CBA(64*4, 64*2)] seq += [CBA(64*2, 64)] seq += [CBA(64, 1, activation=nn.Tanh(), bn=False)] self.generator_network = nn.Sequential(*seq) def forward(self, z): out = self.generator_network(z) return out 为了帮助大家理解,我绘制 了生成网络的结构图,如下:判别模型搭建 """定义判别器网络结构""" class Discriminator(nn.Module): def __init__(self): super(Discriminator, self).__init__() def CBA(in_channel, out_channel, kernel_size=4, stride=2, padding=1, activation=nn.LeakyReLU(0.1, inplace=True)): seq = [] seq += [nn.Conv2d(in_channel, out_channel, kernel_size=kernel_size, stride=stride, padding=padding)] seq += [nn.BatchNorm2d(out_channel)] seq += [activation] return nn.Sequential(*seq) seq = [] seq += [CBA(1, 64)] seq += [CBA(64, 64*2)] seq += [CBA(64*2, 64*4)] seq += [CBA(64*4, 64*8)] self.feature_network = nn.Sequential(*seq) self.critic_network = nn.Conv2d(64*8, 1, kernel_size=4, stride=1) def forward(self, x): out = self.feature_network(x) feature = out feature = feature.view(feature.size(0), -1) out = self.critic_network(out) return out, feature 同样,为了方便大家理解,我也绘制了判别网络的结构图,如下:这里大家需要稍稍注意一下,判别网络有两个输出,一个是最终的输出,还有一个是第四个CBA BLOCK提取到的特征,这个在理论部分介绍损失函数时有提及。模型训练数据集加载 class image_data_set(Dataset): def __init__(self, data): self.images = data[:,:,:,None] self.transform = transforms.Compose([ transforms.ToTensor(), transforms.Resize(64, interpolation=InterpolationMode.BICUBIC), transforms.Normalize((0.1307,), (0.3081,)) ]) def __len__(self): return len(self.images) def __getitem__(self, idx): return self.transform(self.images[idx]) # 加载训练数据 train_set = image_data_set(train) train_loader = DataLoader(train_set, batch_size=batch_size, shuffle=True) 这部分不难,但我提醒大家注意一下这句:transforms.Resize(64, interpolation=InterpolationMode.BICUBIC),即我们采用插值算法将原来2828大小的图片上采样成了6464大小。 【感兴趣的这里也可以不对其进行上采样,这样的话大家需要修改一下上节的模型,可以试试效果喔】加载模型、定义优化器、损失函数等参数 # 指定设备 device = torch.device(args.device if torch.cuda.is_available() else "cpu") # batch_size默认128 batch_size = args.batch_size # 加载模型 G = Generator().to(device) D = Discriminator().to(device) # 训练模式 G.train() D.train() # 设置优化器 optimizerG = torch.optim.Adam(G.parameters(), lr=0.0001, betas=(0.0, 0.9)) optimizerD = torch.optim.Adam(D.parameters(), lr=0.0004, betas=(0.0, 0.9)) # 定义损失函数 criterion = nn.BCEWithLogitsLoss(reduction='mean')训练GAN网络 """ 训练 """ # 开始训练 for epoch in range(args.epochs): # 定义初始损失 log_g_loss, log_d_loss = 0.0, 0.0 for images in train_loader: images = images.to(device) ## 训练判别器 Discriminator # 定义真标签(全1)和假标签(全0) 维度:(batch_size) label_real = torch.full((images.size(0),), 1.0).to(device) label_fake = torch.full((images.size(0),), 0.0).to(device) # 定义潜在变量z 维度:(batch_size,20,1,1) z = torch.randn(images.size(0), 20).to(device).view(images.size(0), 20, 1, 1).to(device) # 潜在变量喂入生成网络--->fake_images:(batch_size,1,64,64) fake_images = G(z) # 真图像和假图像送入判别网络,得到d_out_real、d_out_fake 维度:都为(batch_size,1,1,1) d_out_real, _ = D(images) d_out_fake, _ = D(fake_images) # 损失计算 d_loss_real = criterion(d_out_real.view(-1), label_real) d_loss_fake = criterion(d_out_fake.view(-1), label_fake) d_loss = d_loss_real + d_loss_fake # 误差反向传播,更新损失 optimizerD.zero_grad() d_loss.backward() optimizerD.step() ## 训练生成器 Generator # 定义潜在变量z 维度:(batch_size,20,1,1) z = torch.randn(images.size(0), 20).to(device).view(images.size(0), 20, 1, 1).to(device) fake_images = G(z) # 假图像喂入判别器,得到d_out_fake 维度:(batch_size,1,1,1) d_out_fake, _ = D(fake_images) # 损失计算 g_loss = criterion(d_out_fake.view(-1), label_real) # 误差反向传播,更新损失 optimizerG.zero_grad() g_loss.backward() optimizerG.step() ## 累计一个epoch的损失,判别器损失和生成器损失分别存放到log_d_loss、log_g_loss中 log_d_loss += d_loss.item() log_g_loss += g_loss.item() ## 打印损失 print(f'epoch {epoch}, D_Loss:{log_d_loss / 128:.4f}, G_Loss:{log_g_loss / 128:.4f}') ## 展示生成器存储的图片,存放在result文件夹下的G_out.jpg z = torch.randn(8, 20).to(device).view(8, 20, 1, 1).to(device) fake_images = G(z) torchvision.utils.save_image(fake_images,f"result\G_out.jpg") 这部分就是训练一个DCGAN网络,到目前为止其实也都可以认为是DCGAN的内容。我们可以来看一下输出的G_out.jpg图片:这里我们可以看到训练是有了效果的,但会发现不是特别好。我分析有两点原因,其一是我们的模型不好,且GAN本身就容易出现模式崩溃的问题;其二是我们的数据选取的少,在数据读取时训练集我们只取了前400个数据,但实际上我们一共可以取12116个,大家可以尝试增加数据,我想数据多了后效果肯定比这个好,大家快去试试吧!!!缺陷检测 这部分才是AnoGAN的重点,首先我们先定义损失的计算,如下: ## 定义缺陷计算的得分 def anomaly_score(input_image, fake_image, D): # Residual loss 计算 residual_loss = torch.sum(torch.abs(input_image - fake_image), (1, 2, 3)) # Discrimination loss 计算 _, real_feature = D(input_image) _, fake_feature = D(fake_image) discrimination_loss = torch.sum(torch.abs(real_feature - fake_feature), (1)) # 结合Residual loss和Discrimination loss计算每张图像的损失 total_loss_by_image = 0.9 * residual_loss + 0.1 * discrimination_loss # 计算总损失,即将一个batch的损失相加 total_loss = total_loss_by_image.sum() return total_loss, total_loss_by_image, residual_loss 大家可以对比一下理论部分损失函数的介绍,看看是不是一样的呢。 接着我们就需要不断的搜索潜在变量z了,使其与输入图片尽可能接近,代码如下: # 加载测试数据 test_set = image_data_set(test) test_loader = DataLoader(test_set, batch_size=5, shuffle=False) input_images = next(iter(test_loader)).to(device) # 定义潜在变量z 维度:(5,20,1,1) z = torch.randn(5, 20).to(device).view(5, 20, 1, 1) # z的requires_grad参数设置成Ture,让z可以更新 z.requires_grad = True # 定义优化器 z_optimizer = torch.optim.Adam([z], lr=1e-3) # 搜索z for epoch in range(5000): fake_images = G(z) loss, _, _ = anomaly_score(input_images, fake_images, D) z_optimizer.zero_grad() loss.backward() z_optimizer.step() if epoch % 1000 == 0: print(f'epoch: {epoch}, loss: {loss:.0f}') 执行完上述代码后,我们得到了一个较理想的潜在变量,这时候再用z来生成图片,并基于生成图片和输入图片来计算损失,同时,我们也保存了输入图片和生成图片,并打印了它们之前的损失,相关代码如下: fake_images = G(z) _, total_loss_by_image, _ = anomaly_score(input_images, fake_images, D) print(total_loss_by_image.cpu().detach().numpy()) torchvision.utils.save_image(input_images, f"result/Nomal.jpg") torchvision.utils.save_image(fake_images, f"result/ANomal.jpg") 我们可以来看看最后的结果哦,如下:可以看到,当输入图像为2时(此为缺陷),生成的图像也是8,它们的损失最高为464040.44。这时候如果我们设置一个阈值为430000,高于这个阈值的即为异常图片,低于这个阈值的即为正常图片,那么我们是不是就可以通过AnoGAN来实现缺陷的检测了呢!!!总结 到这里,AnoGAN的所有内容就介绍完了,大家好好感受感受它的思想,其实是很简单的,但是又非常巧妙。最后我不知道大家有没有发现AnoGAN一个非常明显的缺陷,那就是我们每次在判断异常时要不断的搜索潜在变量z,这是非常耗时的。而很多任务对时间的要求还是很高的,所以AnoGAN还有许多可以改进的地方,后续博文我会带大家继续学习GAN网络在缺陷检测中的应用,我们下期见。
EGBAD原理详解 一直在说EGBAD,大家肯定一脸懵,到底什么才是EGBAD了?我们先来看看它的英文全称,即EFFICIENT GAN-BASED ANOMALY DETECTION ,中文译为基于GAN的高效异常检测。通过说明EGBAD的字面含义,相信大家知道了EGBAD是用来干什么的了。没错,它也是用于缺陷检测的网络,是对AnoGAN的优化。至于具体是怎么优化的,且听下文分解。 我们先来回顾一下AnoGAN是怎么设计的?AnoGAN分为训练和测试两个阶段进行,训练阶段使用正常数据训练一个DCGAN网络,在测试阶段,固定训练阶段的网络权重,不断更新潜在变量z,使得由z生成的假图像尽可能接近真实图片。【如果你对这个过程不熟悉的话,建议看看[1]中内容喔】 在介绍EGBAD是怎么设计的前,我们先来看看EGBAD主要解决了AnoGAN什么问题?其实这点我在写在前面已经提及,AnoGAN在测试阶段要不断搜索潜在变量z,这消耗了大量时间,EGBAD的提出就是为了解决AnoGAN时间消耗大的问题。接着我们来就来看看EGBAD具体是怎么做的呢?EGBAD也分为训练和测试两个阶段进行。在训练阶段,不仅要训练生成器和判别器,还会定义一个编码器(encoder)结构并对其训练,encoder主要用于将输入图像通过网络转变成一个潜在变量。在测试阶段,冻结训练阶段的所有权重,之后通过encoder将输入图像变为潜在变量,最后再将潜在变量送入生成器,生成假图像。可以发现EGBAD没有在测试阶段搜索潜在变量,而是直接通过一个encoder结构将输入图像转变成潜在变量,这大大节省了时间成本。 关于EGBAD训练过程模型示意图如下:【测试过程很简单啦,就不介绍了】EGBAD代码实战代码下载地址 同样,我将此部分的源码上传到Github上了,大家可以阅读README文件了解代码的使用,Github地址如下:EGBAD-pytorch实现 我认为你阅读README文件后已经对这个项目的结构有所了解,我在下文也会帮大家分析分析源码,但更多的时间大家应该自己动手去亲自调试,这样你会有不一样的收获。数据读取 这部分和AnoGAN中完全一致,就不带大家一行行看调试结果了,这里直接上代码:#导入相关包 import numpy as np import pandas as pd """ mnist数据集读取 """ ## 读取训练集数据 (60000,785) train = pd.read_csv(".\data\mnist_train.csv",dtype = np.float32) ## 读取测试集数据 (10000,785) test = pd.read_csv(".\data\mnist_test.csv",dtype = np.float32) # 查询训练数据中标签为7、8的数据,并取前400个 train = train.query("label in [7.0, 8.0]").head(400) # 查询训练数据中标签为7、8的数据,并取前400个 test = test.query("label in [2.0, 7.0, 8.0]").head(600) # 取除标签后的784列数据 train = train.iloc[:,1:].values.astype('float32') test = test.iloc[:,1:].values.astype('float32') # train:(400,784)-->(400,28,28) # test:(600,784)-->(600,28,28) train = train.reshape(train.shape[0], 28, 28) test = test.reshape(test.shape[0], 28, 28)模型搭建 这部分大家就潜心修行,慢慢调试代码吧,我也会给出每个模型的结构图辅助大家,就让我们一起来看看吧☘☘☘生成模型搭建"""定义生成器网络结构""" class Generator(nn.Module): def __init__(self): super(Generator, self).__init__() def CBA(in_channel, out_channel, kernel_size=4, stride=2, padding=1, activation=nn.ReLU(inplace=True), bn=True): seq = [] seq += [nn.ConvTranspose2d(in_channel, out_channel, kernel_size=kernel_size, stride=stride, padding=padding)] if bn is True: seq += [nn.BatchNorm2d(out_channel)] seq += [activation] return nn.Sequential(*seq) seq = [] seq += [CBA(20, 64*8, stride=1, padding=0)] seq += [CBA(64*8, 64*4)] seq += [CBA(64*4, 64*2)] seq += [CBA(64*2, 64)] seq += [CBA(64, 1, activation=nn.Tanh(), bn=False)] self.generator_network = nn.Sequential(*seq) def forward(self, z): out = self.generator_network(z) return out 生成模型的搭建其实很AnoGAN是完全一样的,我也给出生成网络的结构图,如下:编码器模型搭建"""定义编码器结构""" class encoder(nn.Module): def __init__(self): super(encoder, self).__init__() def CBA(in_channel, out_channel, kernel_size=4, stride=2, padding=1, activation=nn.LeakyReLU(0.1, inplace=True)): seq = [] seq += [nn.Conv2d(in_channel, out_channel, kernel_size=kernel_size, stride=stride, padding=padding)] seq += [nn.BatchNorm2d(out_channel)] seq += [activation] return nn.Sequential(*seq) seq = [] seq += [CBA(1, 64)] seq += [CBA(64, 64*2)] seq += [CBA(64*2, 64*4)] seq += [CBA(64*4, 64*8)] seq += [nn.Conv2d(64*8, 512, kernel_size=4, stride=1)] self.feature_network = nn.Sequential(*seq) self.embedding_network = nn.Linear(512, 20) def forward(self, x): feature = self.feature_network(x).view(-1, 512) z = self.embedding_network(feature) return z 这部分其实也很简单,就是一系列卷积的堆积,编码器的结构图如下:判别模型搭建"""定义判别器网络结构""" class Discriminator(nn.Module): def __init__(self): super(Discriminator, self).__init__() def CBA(in_channel, out_channel, kernel_size=4, stride=2, padding=1, activation=nn.LeakyReLU(0.1, inplace=True)): seq = [] seq += [nn.Conv2d(in_channel, out_channel, kernel_size=kernel_size, stride=stride, padding=padding)] seq += [nn.BatchNorm2d(out_channel)] seq += [activation] return nn.Sequential(*seq) seq = [] seq += [CBA(1, 64)] seq += [CBA(64, 64*2)] seq += [CBA(64*2, 64*4)] seq += [CBA(64*4, 64*8)] seq += [nn.Conv2d(64*8, 512, kernel_size=4, stride=1)] self.feature_network = nn.Sequential(*seq) seq = [] seq += [nn.Linear(20, 512)] seq += [nn.BatchNorm1d(512)] seq += [nn.LeakyReLU(0.1, inplace=True)] self.latent_network = nn.Sequential(*seq) self.critic_network = nn.Linear(1024, 1) def forward(self, x, z): feature = self.feature_network(x) feature = feature.view(feature.size(0), -1) latent = self.latent_network(z) out = self.critic_network(torch.cat([feature, latent], dim=1)) return out, feature 虽然判别器有两个输入,两个输出,但是结构也非常清晰,如下图所示:在模型搭建部分我还想提一点我们需要注意的地方,一般我们设计好一个网络结构后,我们往往会先设计一个tensor来作为网络的输入,看看网络输出是否是是我们预期的,如果是,我们再进行下一步,否则我们需要调整我们的结构以适应我们的输入。通常情况下,tensor的batch维度设为1就行,但是这里设置成1就会报错,提示我们需要设置一个batch大于1的整数,当将batch设置为2时,程序正常,至于产生这种现象的原因我目前也不是很清楚,大家注意一下,知道的也烦请告知一下。关于调试网络结构是否正常的代码如下,仅供参考:if __name__ == '__main__': x = torch.ones((2, 1, 64, 64)) z = torch.ones((2, 20, 1, 1)) Generator = Generator() Discriminator = Discriminator() encoder = encoder() output_G = Generator(z) output_D1, output_D2= Discriminator(x, z.view(2, -1)) output_E = encoder(x) print(output_G.shape) print(output_D1.shape) print(output_D2.shape) print(output_E.shape)模型训练数据集加载 这部分和AnoGAN一致,注意最终输入网络的图片尺寸都上采样成了64×64.class image_data_set(Dataset): def __init__(self, data): self.images = data[:,:,:,None] self.transform = transforms.Compose([ transforms.ToTensor(), transforms.Resize(64, interpolation=InterpolationMode.BICUBIC), transforms.Normalize((0.1307,), (0.3081,)) ]) def __len__(self): return len(self.images) def __getitem__(self, idx): return self.transform(self.images[idx]) # 加载训练数据 train_set = image_data_set(train) train_loader = DataLoader(train_set, batch_size=batch_size, shuffle=True)加载模型、定义优化器、损失函数等参数 这部分也基本和AnoGAN类似,只不过添加了encoder网络的定义和优化器定义部分,如下:# 指定设备 device = torch.device(args.device if torch.cuda.is_available() else "cpu") # batch_size默认128 batch_size = args.batch_size # 加载模型 G = Generator().to(device) D = Discriminator().to(device) E = Encoder().to(device) # 训练模式 G.train() D.train() E.train() # 设置优化器 optimizerG = torch.optim.Adam(G.parameters(), lr=0.0001, betas=(0.0, 0.9)) optimizerD = torch.optim.Adam(D.parameters(), lr=0.0001, betas=(0.0, 0.9)) optimizerE = torch.optim.Adam(E.parameters(), lr=0.0004, betas=(0.0,0.9)) # 定义损失函数 criterion = nn.BCEWithLogitsLoss(reduction='mean')训练GAN网络""" 训练 """ # 开始训练 for epoch in range(args.epochs): # 定义初始损失 log_g_loss, log_d_loss, log_e_loss = 0.0, 0.0, 0.0 for images in train_loader: images = images.to(device) ## 训练判别器 Discriminator # 定义真标签(全1)和假标签(全0) 维度:(batch_size) label_real = torch.full((images.size(0),), 1.0).to(device) label_fake = torch.full((images.size(0),), 0.0).to(device) # 定义潜在变量z 维度:(batch_size,20,1,1) z = torch.randn(images.size(0), 20).to(device).view(images.size(0), 20, 1, 1).to(device) # 潜在变量喂入生成网络--->fake_images:(batch_size,1,64,64) fake_images = G(z) # 使用编码器将真实图像变成潜在变量 image:(batch_size, 1, 64, 64)-->z_real:(batch_size, 20) z_real = E(images) # 真图像和假图像送入判别网络,得到d_out_real、d_out_fake 维度:都为(batch_size,1) d_out_real, _ = D(images, z_real) d_out_fake, _ = D(fake_images, z.view(images.size(0), 20)) # 损失计算 d_loss_real = criterion(d_out_real.view(-1), label_real) d_loss_fake = criterion(d_out_fake.view(-1), label_fake) d_loss = d_loss_real + d_loss_fake # 误差反向传播,更新损失 optimizerD.zero_grad() d_loss.backward() optimizerD.step() ## 训练生成器 Generator # 定义潜在变量z 维度:(batch_size,20,1,1) z = torch.randn(images.size(0), 20).to(device).view(images.size(0), 20, 1, 1).to(device) fake_images = G(z) # 假图像喂入判别器,得到d_out_fake 维度:(batch_size,1) d_out_fake, _ = D(fake_images, z.view(images.size(0), 20)) # 损失计算 g_loss = criterion(d_out_fake.view(-1), label_real) # 误差反向传播,更新损失 optimizerG.zero_grad() g_loss.backward() optimizerG.step() ## 训练编码器Encode # 使用编码器将真实图像变成潜在变量 image:(batch_size, 1, 64, 64)-->z_real:(batch_size, 20) z_real = E(images) # 真图像送入判别器,记录结果d_out_real:(128, 1) d_out_real, _ = D(images, z_real) # 损失计算 e_loss = criterion(d_out_real.view(-1), label_fake) # 误差反向传播,更新损失 optimizerE.zero_grad() e_loss.backward() optimizerE.step() ## 累计一个epoch的损失,判别器损失、生成器损失、编码器损失分别存放到log_d_loss、log_g_loss、log_e_loss中 log_d_loss += d_loss.item() log_g_loss += g_loss.item() log_e_loss += e_loss.item() ## 打印损失 print(f'epoch {epoch}, D_Loss:{log_d_loss/128:.4f}, G_Loss:{log_g_loss/128:.4f}, E_Loss:{log_e_loss/128:.4f}') 这里总结一下上述训练的步骤,不断循环下列过程:使用生成器从潜在变量z中创建假图像使用编码器从真实图像中创建潜在变量生成器和编码器结果送入判别器,进行训练使用生成器从潜在变量z中创建假图像训练生成器使用编码器从真实图像中创建潜在变量训练编码器关于第3步,我也简单画了个图帮大家理解下,如下: 最后我们来展示一下生成图片的效果,如下图所示:缺陷检测 EGBAD缺陷检测非常简单,首先定义一个就算损失的函数,如下:## 定义缺陷计算的得分 def anomaly_score(input_image, fake_image, z_real, D): # Residual loss 计算 residual_loss = torch.sum(torch.abs(input_image - fake_image), (1, 2, 3)) # Discrimination loss 计算 _, real_feature = D(input_image, z_real) _, fake_feature = D(fake_image, z_real) discrimination_loss = torch.sum(torch.abs(real_feature - fake_feature), (1)) # 结合Residual loss和Discrimination loss计算每张图像的损失 total_loss_by_image = 0.9 * residual_loss + 0.1 * discrimination_loss return total_loss_by_image 接着我们只需要用Encoder网络生成潜在变量,在再用生成器即可得到假图像,最后计算假图像和真图像的损失即可,如下:# 加载测试数据 test_set = image_data_set(test) test_loader = DataLoader(test_set, batch_size=5, shuffle=False) input_images = next(iter(test_loader)).to(device) # 通过编码器获取潜在变量,并用生成器生成假图像 z_real = E(input_images) fake_images = G(z_real.view(input_images.size(0), 20, 1, 1)) # 异常计算 anomality = anomaly_score(input_images, fake_images, z_real, D) print(anomality.cpu().detach().numpy()) 最后可以保存一下真实图像和假图像的结果,如下:torchvision.utils.save_image(input_images, f"result/Nomal.jpg") torchvision.utils.save_image(fake_images, f"result/ANomal.jpg") 我们来看一下结果: 通过上图你发现了什么呢?是不是发现输入图像为7的图片的生成图像不是7而变成了8呢,究其原因,应该是生成器学到了更多关于数据8的特征,也就是说这个网络的生成效果并没有很好。 我做了很多实验,发现EGBAD虽然测试时间上比AnoGAN快很多,但是它的稳定性似乎并没有很理想,很容易出现模式崩溃的问题。其实啊,GAN网络普遍存在着训练不稳定的现象,这也是一些大牛不断探索的方向,后面的文章我也会给大家介绍一些增加GAN训练稳定性的文章已,敬请期待吧!🍄🍄🍄AnoGAN和EGBAD测试时间对比✨✨✨ 我们一直说EGBAD的测试时间相较AnoGAN短,从原理上来说确实是这样,但是具体是不是这样我们还要以实验为准。测试代码也很简单,只需要在测试过程中使用time.time()函数即可,具体可以参考我上传github中的源码,这里给出我测试两种网络在测试阶段所用时间(以秒为单位),如下图所示: 通过上图数据可以看出,EGBAD比AnoGAN快的不是一点点,EGBAD的速度将近是AnoGAN的10000倍,这个数字还是很恐怖的。🍮🍮🍮总结 到此,EGBAD的全部内容就为大家介绍完了,如果你明白了AnoGAN的话,这篇文章对你来说应该是小菜一碟了。EGBAD大大的减少了测试所有时间,但是GAN网络普遍存在易模式崩溃、训练不稳定的现象, 下一篇博文我将为大家介绍一些让GAN训练更稳定的技巧,敬请期待吧。🍚🍚🍚
Spectral Normalization原理详解 由于原始GAN网络存在训练不稳定的现象,究其本质,是因为它的损失函数实际上是JS散度,而JS散度不会随着两个分布的距离改变而改变,这就会导致生成器的梯度会一直不变,从而导致模型训练效果很差。WGAN为了解决原始GAN网络训练不稳定的现象,引入了EM distance代替原有的JS散度,这样的改变会使生成器梯度一直变化,从而使模型得到充分训练。但是WGAN的提出伴随着一个难点,即如何让判别器的参数矩阵满足Lipschitz连续条件。 如何解决上述所说的难点呢?在WGAN中,我们采用了一种简单粗暴的方式来满足这一条件,即直接对判别器的权重参数进行剪裁,强制将权重限制在[-c,c]范围内。大家可以动动我们的小脑瓜想想这种权重剪裁的方式有什么样的问题——(滴,揭晓答案)如果权重剪裁的参数c很大,那么任何权重可能都需要很长时间才能达到极限,从而使训练判别器达到最优变得更加困难;如果权重剪裁的参数c很小,这又容易导致梯度消失。因此,如何确定权重剪裁参数c是重要的,同时这也是困难的。WGAN提出之后,又提出了WGAN-GP来实现Lipschitz 连续条件,其主要通过添加一个惩罚项来实现。【关于WGAN-GP我没有做相关教程,如果不明白的可以评论区留言】那么本文提出了一种归一化的手段Spectral Normalization来实现Lipschitz连续条件,这种归一化具体是怎么实现的呢,下面听我慢慢道来。 这样,其实我们的Spectral Normalization原理就讲的差不多了,最后我们要做的就是求得每层参数矩阵的谱范数,然后再进行归一化操作。要想求矩阵的谱范数,首先得求矩阵的奇异值,具体求法我放在附录部分。 但是按照正常求奇异值的方法会消耗大量的计算资源,因此论文中使用了一种近似求解谱范数的方法,伪代码如下图所示: 在代码的实战中我们就是按照上图的伪代码求解谱范数的,届时我们会为大家介绍。注:大家阅读这部分有没有什么难度呢,我觉得可能还是挺难的,你需要一些矩阵分析的知识,我已经尽可能把这个问题描述的简单了,有的文章写的很好,公式推导的也很详尽,我会在参考链接中给出。但是会涉及到最优化的一些理论,估计这就让大家更头疼了,所以大家慢慢消化吧!!!在最后的附录中,我会给出本节内容相关的矩阵分析知识,是我上课时的一些笔记,笔记包含本节的知识点,但针对性可能不是很强,也就是说可能包含一些其它内容,大家可以选择忽略,当然了,你也可以细细的研究研究每个知识点,说不定后面就用到了呢!!!Spectral Normalization源码解析源码下载地址:Spectral Normalization 这个代码使用的是CIFAR10数据集,实现的是一般生成对抗网络的图像生成任务。我不打算再对每一句代码进行详细的解释,有不明白的可以先去看看我专栏中的其它GAN网络的文章,都有源码解析,弄明白后再看这篇你会发现非常简单。那么这篇文章我主要来介绍一下Spectral Normalization部分的内容,其相关内容在spectral_normalization.py文件中,我们理论部分提到Spectral Normalization关键的一步是求解每个参数矩阵的谱范数,相关代码如下:def _update_u_v(self): u = getattr(self.module, self.name + "_u") v = getattr(self.module, self.name + "_v") w = getattr(self.module, self.name + "_bar") height = w.data.shape[0] for _ in range(self.power_iterations): u.data = l2normalize(torch.mv(w.view(height, -1).data, v.data)) v.data = l2normalize(torch.mv(torch.t(w.view(height,-1).data), u.data)) sigma = u.dot(w.view(height, -1).mv(v)) setattr(self.module, self.name, w / sigma.expand_as(w)) def l2normalize(v, eps=1e-12): return v / (v.norm() + eps) 对上述代码做一定的解释,6,7,8,9,10行做的就是理论部分伪代码的工作,最后会得到谱范数sigma。11行为使用参数矩阵除以谱范数sigma,以此实现归一化的作用。【torch.mv实现的是矩阵乘法的操作,里面可能还有些函数你没见过,大家百度一下用法就知道了,非常简单】其实关键的代码就这些,是不是发现特别简单呢🍸🍸🍸每次介绍代码时我都会强调自己动手调试的重要性,很多时候写文章介绍源码都觉得有些力不从心,一些想表达的点总是很难表述,总之,大家要是有什么不明白的就尽情调试叭,或者评论区留言,我天天在线摸鱼滴喔。后期我也打算出一些视频教学了,这样的话就可以带着大家一起调试,我想这样介绍源码彼此都会轻松很多。🛩🛩🛩小结 Spectral Normalization确实是有一定难度的,我也有许多地方理解的也不是很清楚,对于这种难啃的问题我是这样认为的。我们可以先对其有一个大致的了解,知道整个过程,知道代码怎么实现,能使用代码跑通一些模型,然后考虑能否将其用在自己可能需要使用的地方,如果加入的效果不好,我们就没必要深究原理了,如果发现效果好,这时候我们再回来慢慢细嚼原理也不迟。最后,希望各位都能获取新知识,能够学有所成叭!!!附录 这部分是我学习矩阵分析这门课程时的笔记,截取一些包含此部分的内容,有需求的感兴趣的可以看一看。
GAN简介 这里先来简单的介绍一下GAN,其完整的名称为Generative Adversarial Nets (生成对抗网络) 。其实这个起名还有个小故事,我简要的说一下,大家随便听听,就当放松了。当时作者Goodfellow 对于这篇文章其实是有好几个备选名字的,后来一个中国人说GAN(干)在中国有一种对抗的意思,作者一听,直接拍案选择了这个名称。 接下来让我们看看论文中对GAN的解释,如下图所示: 我简单的来翻译一下,其大致意思是说:在我们提出的对抗生成网络中,有一个生成模型,也有一个对抗模型,它们互相对抗,互相促进。文中也举了个小例子,生成模型可以被认为是一个假币伪造团队,试图生产假币并使用,而判别器类似于警察,试图发现假币。这就是一个互相博弈的过程,生成模型不断的产生伪造水平高的假币,而判别器不断提高警察识别假币水平,直至两者达到一个平衡。这个平衡是指什么呢?即判别器对于生成模型产生的假币辨别的成功率大致为50%,即很难辨别真假。生成对抗网络GAN损失函数 这部分我们主要结合生成对抗网络的损失函数来介绍网络的整个流程,首先呢,我们需要对一些字母做一些解释。如下:对上述字母有一定的了解后,下面就可以给出生成对抗网络的损失函数了,如下图所示: 乍一看这个公式你应该是懵逼的,下面就跟着我的思路来分解分解上述公式。首先这个公式应该有两部分,一部分为给定G,找到使V最大化的D;另一部分为给定D,找到使V最小化的G。 我们先来看第一部分,即给定G,找到使V最大化的D。如下图所示:【注:我们为什么想要找到使V最大化的D,是因为使V最大化的D会使判别器的效果最好】 接着我们来看第二部分,即给定D,找到使V最小化的G。如下图所示:【注:我们为什么想要找到使V最小化的G,是因为使V最小化的G会使生成器的效果最好】GAN流程 论文中在给出损失函数后,又给了一个图例来解释GAN的过程,用原文的话来说就是一个不怎么正式,却更具教学意义的解释。(See Figure 1 for a less formal, more pedagogical explanation of the approach ) 接下来论文中给出了训练GAN网络的伪代码,如下图所示:上面四个图中,注意黄框框住的并不是GAN生成的图片,它们表示与GAN生成图片最相似的原始真实图片。而GAN生成的图片为黄框左侧第一张图片,可以看出,GAN生成的效果还是挺好的。使用GAN生成手写数字小demo 上文算是把原理讲述清楚了,若你还不明白,慢慢的阅读每句话,加入自己的思考,或许会有不一样的收获。那么这节我讲来讲讲通过GAN网络生成手写数字的小demo,通过这部分你会了解搭建GAN网络的基本流程。下面就让我们一起来学学吧!!! 首先训练一个模型肯定少不了数据集,我们通过一下代码获取torch自带的MNIST数据集,代码如下:#MNIST数据集获取 dataset = torchvision.datasets.MNIST("mnist_data", train=True, download=True, transform=torchvision.transforms.Compose( [ torchvision.transforms.Resize(28), torchvision.transforms.ToTensor(), torchvision.transforms.Normalize([0.5], [0.5]), ] ) ) 之后我们通过DataLoader方法加载数据集,代码如下:dataloader = torch.utils.data.DataLoader(dataset, batch_size=batch_size, shuffle=True, drop_last=True) 这样数据就准备好了,下面就来构建我们的模型,分为生成器(Generator)和判别器(Discriminator)。【注:由于这期算是入门GAN,所以模型搭建只采用了全连接层】 生成器模型搭建:class Generator(nn.Module): def __init__(self): super(Generator, self).__init__() self.model = nn.Sequential( nn.Linear(latent_dim, 128), torch.nn.BatchNorm1d(128), torch.nn.GELU(), nn.Linear(128, 256), torch.nn.BatchNorm1d(256), torch.nn.GELU(), nn.Linear(256, 512), torch.nn.BatchNorm1d(512), torch.nn.GELU(), nn.Linear(512, 1024), torch.nn.BatchNorm1d(1024), torch.nn.GELU(), nn.Linear(1024, np.prod(image_size, dtype=np.int32)), nn.Sigmoid(), ) def forward(self, z): # shape of z: [batchsize, latent_dim] output = self.model(z) image = output.reshape(z.shape[0], *image_size) return image 判别器模型搭建: class Discriminator(nn.Module): def __init__(self): super(Discriminator, self).__init__() self.model = nn.Sequential( nn.Linear(np.prod(image_size, dtype=np.int32), 512), torch.nn.GELU(), nn.Linear(512, 256), torch.nn.GELU(), nn.Linear(256, 128), torch.nn.GELU(), nn.Linear(128, 64), torch.nn.GELU(), nn.Linear(64, 32), torch.nn.GELU(), nn.Linear(32, 1), nn.Sigmoid(), ) def forward(self, image): # shape of image: [batchsize, 1, 28, 28] prob = self.model(image.reshape(image.shape[0], -1)) return prob 模型搭建好后,我们会对损失函数、优化器等参数进行设置:g_optimizer = torch.optim.Adam(generator.parameters(), lr=0.0003, betas=(0.4, 0.8), weight_decay=0.0001) d_optimizer = torch.optim.Adam(discriminator.parameters(), lr=0.0003, betas=(0.4, 0.8), weight_decay=0.0001) loss_fn = nn.BCELoss()需要注意,这里采用的是BCELOSS损失函数,这个函数其实就对应着我们GAN理论部分的损失函数这些设置好后,我们就来训练我们的GAN网络了,相关代码如下:num_epoch = 200 for epoch in range(num_epoch): for i, mini_batch in enumerate(dataloader): gt_images, _ = mini_batch z = torch.randn(batch_size, latent_dim) pred_images = generator(z) g_optimizer.zero_grad() g_loss = loss_fn(discriminator(pred_images), labels_one) g_loss.backward() g_optimizer.step() d_optimizer.zero_grad() real_loss = loss_fn(discriminator(gt_images), labels_one) fake_loss = loss_fn(discriminator(pred_images.detach()), labels_zero) d_loss = (real_loss + fake_loss) # 观察real_loss与fake_loss,同时下降同时达到最小值,并且差不多大,说明D已经稳定了 d_loss.backward() d_optimizer.step() 最后,我来展示一下训练结果吧!!!我是在服务器上进行训练的,所以还是比较快的。先来看一下初始的图,都是一些随机的噪声,如下图所示:再来看训练一段时间的结果,发现效果还是蛮不错滴
写在前面 在前面我们已经介绍过了最原始的GAN网络和DCGAN,这篇文章我将来为大家介绍CycleGAN,并且基于CycleGAN实现一个小demo——将一张图片进行季节转换,即从冬天变换到夏天和从夏天变换到冬天。 大家已经看到了CycleGAN,应该对GAN已经有了一定的了解,因此我不会对GAN的原理进行详细的讲解,只会叙述CycleGAN的独到之处。 在正式讲解之前,我给大家先展示一下CycleGAN可以做哪些事: 普通马变斑可以看到,使用CycleGAN可以实现各种各样的风格转换,是非常有意思的一个算法。大家准备好了嘛,下面就正式发车了。CycleGAN核心思想 这一部分我会来介绍CycleGAN的核心思想,相信你了解后会和我有一样的感觉,那就是这个设计太巧妙了!!!首先我们还是来介绍一下这篇论文的全称—— Unpaired Image-to-Image Translationusing Cycle-Consistent Adversarial Networks,即非成对图像转换循环一致性对抗网络。我们一点点的来解释,首先什么是非对称图像呢?其实啊,这里的非对称图像指的是我们的训练样本是不相关的。在之前的一些GAN转换实验中,往往都需要成对的图片数据,例如pix2pix,而成对的图片数据是很难获取的,于是CycleGAN对数据的要求就大大降低,不需要成对图像,即非对称图像,这样就让CycleGAN的应用场景就变得非常丰富。下图展示了对称数据和非对称数据的区别: 接下来再来讲讲什么是循环一致性对抗网络?这个就是本文的核心思想,听懂这个那么这篇论文你就搞懂了,这就为大家慢慢道来!!! 我们先来明确一下这篇文章的目标,即有两个域的图像,分别为域X和域Y,例如域X表示夏季图片、域Y表示冬季图片,现期望将这两个域的图片互相转换,即输入域X的夏季图片生成器输出域Y的冬季图片或输入域Y的冬季图片生成器输出域X的夏季图片。我们来考虑考虑传统的GAN网络能否完成这项任务,示意图如下: 上图我们的确是将域X中图片转换成了域Y中冬季图片风格,但是你会发现转换后的图片和原始图片没有任何关系,即GAN网络只学到了把一张夏季图片传化为冬季图片,但至于转换后的冬季图片和原始夏季图片有没有关系没有学习到,这样的话这个网络肯定是不符合实际要求的。那么CycleGAN就提出了循环一致性网络,如下图所示: 现对上图做相关解释,首先我们先对相关字母做一定了解,如下表所示: 其实这样就把CycleGAN的核心思想都介绍完了,这里再贴上论文中关于这部分的一张完整的图供大家参考:CycleGAN损失函数其实介绍完理论部分,那么损失函数就很简单了,一共有三部分组成,如下表所示:【呜呜呜,这里编辑的markdown表格在网页中显示总是乱码,大家将就看一下图片吧】CycleGAN图像夏冬转换案例 实验论文中也给除了Github地址,连接如下:CycleGAN 这里我就不带大家一点点的解读代码了,相信你阅读了我之前的文章看这个代码应该能大致了解,我之前几期做过一些代码的解读,但是我自己觉得描述并不算很清晰,有的想要表达的点也没有表述清楚,所以我觉得代码部分大家还是看视频讲解比较高效,但是不论怎样,阅读代码你一定要自己亲自调试调试,这样你会有很大的收获!!! 这里我就放一张我运行的结果图片,从夏季转换到冬季,如下: 可以看出,变换的效果还是不错的。【注意:我只再Googleclab上训练了15个epoch就得动了这样的效果,大家可以增大epoch进行训练。】论文下载CycleGAN论文下载
Python数据可视化python数据可视化大杀器之Seaborn详解一张好的图胜过一千个字,一个好的数据分析师必须学会用图说话。python作为数据分析最常用的工具之一,它的可视化功能也很强大,matplotlib和seaborn库使得绘图变得更加简单。本章主要介绍一下Searborn绘图。学过matplotlib的小伙伴们一定被各种参数弄得迷糊,而seaborn则避免了这些问题,废话少说,我们来看看seaborn具体是怎样使用的。Seaborn中概况起来可以分为五大类图1.关系类绘图2.分类型绘图3.分布图4.回归图5.矩阵图接下来我们一一讲解这些图形的应用,首先我们要导入一下基本的库%matplotlib inline # 如果不添加这句,是无法直接在jupyter里看到图的 import seaborn as sns import numpy as np import pandas as pd import matplotlib.pyplot as plt如果上面报错的话需要安装相应的包pip install seaborn pip install numpy pip install pandas pip install matplotlib我们可以使用set()设置一下seaborn的主题,一共有:darkgrid,whitegrid,dark,white,ticks,大家可以根据自己的喜好设置相应的主题,默认是darkgrid。我这里就设置darkgrid风格sns.set(style="darkgrid")接下来导入我们需要的数据集,seaborn和R语言ggplot2(感兴趣欢迎阅读我的R语言ggplot2专栏)一样有许多自带的样例数据集# 导入anscombe数据集 df = sns.load_dataset('anscombe') # 观察一下数据集形式 df.head().dataframe tbody tr th:only-of-type { vertical-align: middle; } .dataframe tbody tr th { vertical-align: top; } .dataframe thead th { text-align: right; } datasetxy0I10.08.041I8.06.952I13.07.583I9.08.814I11.08.331.关系图1.1 lineplot绘制线段seaborn里的lineplot函数所传数据必须为一个pandas数组,这一点跟matplotlib里有较大区别,并且一开始使用较为复杂,sns.lineplot里有几个参数值得注意。x: plot图的x轴labely: plot图的y轴labelci: 置信区间data: 所传入的pandas数组绘制时间序列图# 导入数据集 fmri = sns.load_dataset("fmri") # 绘制不同地区不同时间 x和y的线性关系图 sns.lineplot(x="timepoint", y="signal", hue="region", style="event", data=fmri)<AxesSubplot:xlabel='timepoint', ylabel='signal'>rs = np.random.RandomState(365) values = rs.randn(365, 4).cumsum(axis=0) dates = pd.date_range("1 1 2016", periods=365, freq="D") data = pd.DataFrame(values, dates, columns=["A", "B", "C", "D"]) data = data.rolling(7).mean() sns.lineplot(data=data, palette="tab10", linewidth=2.5)<AxesSubplot:>1.2 relplot这是一个图形级别的函数,它用散点图和线图两种常用的手段来表现统计关系。# 导入数据集 dots = sns.load_dataset("dots") sns.relplot(x="time", y="firing_rate", hue="coherence", size="choice", col="align", size_order=["T1", "T2"], height=5, aspect=.75, facet_kws=dict(sharex=False), kind="line", legend="full", data=dots)<seaborn.axisgrid.FacetGrid at 0x1d7d3634e50>1.3 scatterplot(散点图)diamonds.head().dataframe tbody tr th:only-of-type { vertical-align: middle; } .dataframe tbody tr th { vertical-align: top; } .dataframe thead th { text-align: right; } caratcutcolorclaritydepthtablepricexyz00.23IdealESI261.555.0326.03.953.982.4310.21PremiumESI159.861.0326.03.893.842.3120.23GoodEVS156.965.0327.04.054.072.3130.29PremiumIVS262.458.0334.04.204.232.6340.31GoodJSI263.358.0335.04.344.352.75sns.set(style="whitegrid") # Load the example iris dataset diamonds = sns.load_dataset("diamonds") # Draw a scatter plot while assigning point colors and sizes to different # variables in the dataset f, ax = plt.subplots(figsize=(6.5, 6.5)) sns.despine(f, left=True, bottom=True) sns.scatterplot(x="depth", y="table", data=diamonds, ax=ax)<AxesSubplot:xlabel='depth', ylabel='table'>1.4 气泡图气泡图是在散点图的基础上,指定size参数,根据size参数的大小来绘制点的大小1.4.1 普通气泡图# 导入鸢尾花数据集 planets = sns.load_dataset("planets") cmap = sns.cubehelix_palette(rot=-.2, as_cmap=True) ax = sns.scatterplot(x="distance", y="orbital_period", hue="year", size="mass", palette=cmap, sizes=(10, 200), data=planets)1.4.2 彩色气泡图sns.set(style="white") #加载示例mpg数据集 mpg = sns.load_dataset("mpg") # 绘制气泡图 sns.relplot(x="horsepower", y="mpg", hue="origin", size="weight", sizes=(40, 400), alpha=.5, palette="muted", height=6, data=mpg)2. 分类型图表2.1 boxplot(箱线图)箱形图(Box-plot)又称为盒须图、盒式图或箱线图,是一种用作显示一组数据分散情况资料的统计图。它能显示出一组数据的最大值、最小值、中位数及上下四分位数。绘制分组箱线图# 导入数据集 tips = sns.load_dataset("tips") # 绘制嵌套的箱线图,按日期和时间显示账单 sns.boxplot(x="day", y="total_bill", hue="smoker", palette=["m", "g"], data=tips) sns.despine(offset=10, trim=True)2.2 violinplot(小提琴图)violinplot与boxplot扮演类似的角色,它显示了定量数据在一个(或多个)分类变量的多个层次上的分布,这些分布可以进行比较。不像箱形图中所有绘图组件都对应于实际数据点,小提琴绘图以基础分布的核密度估计为特征。绘制简单的小提琴图# 生成模拟数据集 rs = np.random.RandomState(0) n, p = 40, 8 d = rs.normal(0, 2, (n, p)) d += np.log(np.arange(1, p + 1)) * -5 + 10 # 使用cubehelix获得自定义的顺序调色板 pal = sns.cubehelix_palette(p, rot=-.5, dark=.3) # 如何使用小提琴和圆点进行每种分布 sns.violinplot(data=d, palette=pal, inner="point")<AxesSubplot:>绘制分组小提琴图tips = sns.load_dataset("tips") # 绘制一个嵌套的小提琴图,并拆分小提琴以便于比较 sns.violinplot(x="day", y="total_bill", hue="smoker", split=True, inner="quart", palette={"Yes": "y", "No": "b"}, data=tips) sns.despine(left=True)2.3 barplot(条形图)条形图表示数值变量与每个矩形高度的中心趋势的估计值,并使用误差线提供关于该估计值附近的不确定性的一些指示。绘制水平的条形图crashes = sns.load_dataset("car_crashes").sort_values("total", ascending=False) # 初始化画布大小 f, ax = plt.subplots(figsize=(6, 15)) # 绘出总的交通事故 sns.set_color_codes("pastel") sns.barplot(x="total", y="abbrev", data=crashes, label="Total", color="b") # 绘制涉及酒精的车祸 sns.set_color_codes("muted") sns.barplot(x="alcohol", y="abbrev", data=crashes, label="Alcohol-involved", color="b") # 添加图例和轴标签 ax.legend(ncol=2, loc="lower right", frameon=True) ax.set(xlim=(0, 24), ylabel="", xlabel="Automobile collisions per billion miles") sns.despine(left=True, bottom=True)绘制分组条形图titanic = sns.load_dataset("titanic") # 绘制分组条形图 g = sns.barplot(x="class", y="survived", hue="sex", data=titanic, palette="muted")2.4 pointplot(点图)点图代表散点图位置的数值变量的中心趋势估计,并使用误差线提供关于该估计的不确定性的一些指示。点图可能比条形图更有用于聚焦一个或多个分类变量的不同级别之间的比较。他们尤其善于表现交互作用:一个分类变量的层次之间的关系如何在第二个分类变量的层次之间变化。连接来自相同色调等级的每个点的线允许交互作用通过斜率的差异进行判断,这比对几组点或条的高度比较容易。sns.set(style="whitegrid") iris = sns.load_dataset("iris") # 将数据格式调整 iris = pd.melt(iris, "species", var_name="measurement") # 初始化图形 f, ax = plt.subplots() sns.despine(bottom=True, left=True) sns.stripplot(x="value", y="measurement", hue="species", data=iris, dodge=True, jitter=True, alpha=.25, zorder=1) # 显示条件平均数 sns.pointplot(x="value", y="measurement", hue="species", data=iris, dodge=.532, join=False, palette="dark", markers="d", scale=.75, ci=None) # 图例设置 handles, labels = ax.get_legend_handles_labels() ax.legend(handles[3:], labels[3:], title="species", handletextpad=0, columnspacing=1, loc="lower right", ncol=3, frameon=True)可以看出各种鸢尾花四个特征的分布情况,以setosa为例,发现其petal_width值集中分布在0.2左右2.5 swarmplot能够显示分布密度的分类散点图sns.set(style="whitegrid", palette="muted") # 加载数据集 iris = sns.load_dataset("iris") # 处理数据集 iris = pd.melt(iris, "species", var_name="measurement") # 绘制分类散点图 sns.swarmplot(x="measurement", y="value", hue="species", palette=["r", "c", "y"], data=iris)2.6 catplot(分类型图表的接口)可以通过指定kind参数分别绘制下列图形:stripplot() 分类散点图swarmplot() 能够显示分布密度的分类散点图boxplot() 箱图violinplot() 小提琴图boxenplot() 增强箱图pointplot() 点图barplot() 条形图countplot() 计数图3.分布图3.1 displot(单变量分布图)在seaborn中想要对单变量分布进行快速了解最方便的就是使用distplot()函数,默认情况下它将绘制一个直方图,并且可以同时画出核密度估计(KDE)图。具体用法如下:# 设置并排绘图,讲一个画布分为2*2,大小为7*7,X轴固定,通过ax参数指定绘图位置,可以看第六章具体怎么绘制多个图在一个画布中 f, axes = plt.subplots(2, 2, figsize=(7, 7), sharex=True) sns.despine(left=True) rs = np.random.RandomState(10) # 生成随机数 d = rs.normal(size=100) # 绘制简单的直方图,kde=False不绘制核密度估计图,下列其他图类似 sns.distplot(d, kde=False, color="b", ax=axes[0, 0]) # 绘制核密度估计图和地毯图 sns.distplot(d, hist=False, rug=True, color="r", ax=axes[0, 1]) # 绘制填充核密度估计图 sns.distplot(d, hist=False, color="g", kde_kws={"shade": True}, ax=axes[1, 0]) # 绘制直方图和核密度估计 sns.distplot(d, color="m", ax=axes[1, 1]) plt.setp(axes, yticks=[]) plt.tight_layout()3.2kdeplot(核密度估计图)核密度估计(kernel density estimation)是在统计学中用来估计未知分布的密度函数,属于非参数检验方法之一。通过核密度估计图可以比较直观的看出数据样本本身的分布特征。具体用法如下:简单的二维核密度估计图sns.set(style="dark") rs = np.random.RandomState(50) x, y = rs.randn(2, 50) sns.kdeplot(x, y) f.tight_layout()多个核密度估计图sns.set(style="darkgrid") iris = sns.load_dataset("iris") # 按物种对iris数据集进行子集划分 setosa = iris.query("species == 'setosa'") virginica = iris.query("species == 'virginica'") f, ax = plt.subplots(figsize=(8, 8)) ax.set_aspect("equal") # 画两个密度图 ax = sns.kdeplot(setosa.sepal_width, setosa.sepal_length, cmap="Reds", shade=True, shade_lowest=False) ax = sns.kdeplot(virginica.sepal_width, virginica.sepal_length, cmap="Blues", shade=True, shade_lowest=False) # 将标签添加到绘图中 red = sns.color_palette("Reds")[-2] blue = sns.color_palette("Blues")[-2] ax.text(2.5, 8.2, "virginica", size=16, color=blue) ax.text(3.8, 4.5, "setosa", size=16, color=red)3.3绘制山脊图rs = np.random.RandomState(1979) x = rs.randn(500) g = np.tile(list("ABCDEFGHIJ"), 50) df = pd.DataFrame(dict(x=x, g=g)) m = df.g.map(ord) df["x"] += m # 初始化FacetGrid对象 pal = sns.cubehelix_palette(10, rot=-.25, light=.7) g = sns.FacetGrid(df, row="g", hue="g", aspect=15, height=.5, palette=pal) # 画出密度 g.map(sns.kdeplot, "x", clip_on=Fals "?e, shade=True, alpha=1, lw=1.5, bw=.2) g.map(sns.kdeplot, "x", clip_on=False, color="w", lw=2, bw=.2) g.map(plt.axhline, y=0, lw=2, clip_on=False) # 定义并使用一个简单的函数在坐标轴中标记绘图 def label(x, color, label): ax = plt.gca() ax.text(0, .2, label, fontweight="bold", color=color, ha="left", va="center", transform=ax.transAxes) g.map(label, "x") # 将子地块设置为重叠 g.fig.subplots_adjust(hspace=-.25) # 删除与重叠不协调的轴 g.set_titles("") g.set(yticks=[]) g.despine(bottom=True, left=True)<seaborn.axisgrid.FacetGrid at 0x1d7da3567c0>3.4 joinplot(双变量关系分布图)用于绘制两个变量间分布图sns.set(style="white") # 创建模拟数据集 rs = np.random.RandomState(5) mean = [0, 0] cov = [(1, .5), (.5, 1)] x1, x2 = rs.multivariate_normal(mean, cov, 500).T x1 = pd.Series(x1, name="$X_1$") x2 = pd.Series(x2, name="$X_2$") # 使用核密度估计显示联合分布 g = sns.jointplot(x1, x2, kind="kde", height=7, space=0)rs = np.random.RandomState(11) x = rs.gamma(2, size=1000) y = -.5 * x + rs.normal(size=1000) sns.jointplot(x, y, kind="hex", color="#4CB391")tips = sns.load_dataset("tips") g = sns.jointplot("total_bill", "tip", data=tips, kind="reg", xlim=(0, 60), ylim=(0, 12), color="m", height=7)3.5 pairplot(变量关系图)变量关系组图,绘制各变量之间散点图df = sns.load_dataset("iris") sns.pairplot(df)4. 回归图4.1 lmplotlmplot是用来绘制回归图的,通过lmplot我们可以直观地总览数据的内在关系,lmplot可以简单通过指定x,y,data绘制# 绘制整体数据的回归图 sns.lmplot(x='x',y='y',data=df)<seaborn.axisgrid.FacetGrid at 0x1d7cdfbec10># 使用分面绘图,根据dataset分面 sns.lmplot(x="x", y="y", col="dataset", hue="dataset", data=df, col_wrap=2, ci=None)上面显示了每一张图内画一个回归线,下面我们来看如何在一张图中画多个回归线# 加载鸢尾花数据集 iris = sns.load_dataset("iris") g = sns.lmplot(x="sepal_length", y="sepal_width", hue="species", truncate=True, height=5, data=iris) # 使用truncate参数 # 设置坐标轴标签 g.set_axis_labels("Sepal length (mm)", "Sepal width (mm)")<seaborn.axisgrid.FacetGrid at 0x1d7d16cea60>可以看出setosa类型的鸢尾花主要集中在左侧,下面我们再来看一下怎么绘制logistic回归曲线# 加载 titanic dataset df = sns.load_dataset("titanic") # 显示不同性别年龄和是否存活的关系 g = sns.lmplot(x="age", y="survived", col="sex", hue="sex", data=df, y_jitter=.02, logistic=True) g.set(xlim=(0, 80), ylim=(-.05, 1.05))虽然仅仅使用一个变量来拟合logistic回归效果不好,但是为了方便演示,我们暂且这样做,从logistic回归曲线来看,男性随着年龄增长,存活率下降,而女性随着年龄上升,存活率上升4.2 residplot(残差图)线性回归残差图 绘制现象回归得到的残差回归图sns.set(style="whitegrid") # 模拟y对x的回归数据集 rs = np.random.RandomState(7) x = rs.normal(2, 1, 75) y = 2 + 1.5 * x + rs.normal(0, 2, 75) # 绘制残差数据集,并拟合曲线 sns.residplot(x, y, lowess=True, color="g")从结果来看,回归结果较好,这是因为我们的数据就是通过回归的形式生成的5.矩阵图5.1 heatmap(热力图)常见的我们使用热力图可以看数据表中多个变量间的相似度# 加载数据 flights_long = sns.load_dataset("flights") # 绘制不同年份不同月份的乘客数量 flights = flights_long.pivot("month", "year", "passengers") # 绘制热力图,并且在每个单元中添加一个数字 f, ax = plt.subplots(figsize=(9, 6)) sns.heatmap(flights, annot=True, fmt="d", linewidths=.5, ax=ax)绘制相关系数矩阵,绘制26个英文字母之间的相关系数矩阵from string import ascii_letters sns.set(style="white") # 随机数据集 rs = np.random.RandomState(33) d = pd.DataFrame(data=rs.normal(size=(100, 26)), columns=list(ascii_letters[26:])) # 计算相关系数 corr = d.corr() mask = np.zeros_like(corr, dtype=np.bool) mask[np.triu_indices_from(mask)] = True # 设置图形大小 f, ax = plt.subplots(figsize=(11, 9)) # 生成自定义颜色 cmap = sns.diverging_palette(220, 10, as_cmap=True) # 绘制热力图 sns.heatmap(corr, mask=mask, cmap=cmap, vmax=.3, center=0, square=True, linewidths=.5, cbar_kws={"shrink": .5})🌐5.2 clustermap聚类图sns.set() # 加载大脑网络示例数据集 df = sns.load_dataset("brain_networks", header=[0, 1, 2], index_col=0) # 选择网络的子集 used_networks = [1, 5, 6, 7, 8, 12, 13, 17] used_columns = (df.columns.get_level_values("network") .astype(int) .isin(used_networks)) df = df.loc[:, used_columns] # 创建一个分类调色板来识别网络 network_pal = sns.husl_palette(8, s=.45) network_lut = dict(zip(map(str, used_networks), network_pal)) # 将调色板转换为将在矩阵侧面绘制的向量 networks = df.columns.get_level_values("network") network_colors = pd.Series(networks, index=df.columns).map(network_lut) # 画出完整的聚类图 sns.clustermap(df.corr(), center=0, cmap="vlag", row_colors=network_colors, col_colors=network_colors, linewidths=.75, figsize=(13, 13))6.FacetGrid绘制多个图表是一个绘制多个图表(以网格形式显示)的接口。 步骤:1、实例化对象2、map,映射到具体的 seaborn 图表类型3、添加图例6.1 绘制多个直方图sns.set(style="darkgrid") tips = sns.load_dataset("tips") g = sns.FacetGrid(tips, row="sex", col="time", margin_titles=True) bins = np.linspace(0, 60, 13) g.map(plt.hist, "total_bill", color="steelblue", bins=bins)6.2 绘制多个折线图sns.set(style="ticks") # 创建一个包含许多短随机游动的数据集 rs = np.random.RandomState(4) pos = rs.randint(-1, 2, (20, 5)).cumsum(axis=1) pos -= pos[:, 0, np.newaxis] step = np.tile(range(5), 20) walk = np.repeat(range(20), 5) df = pd.DataFrame(np.c_[pos.flat, step, walk], columns=["position", "step", "walk"]) # 为每一次行走初始化一个带有轴的网格 grid = sns.FacetGrid(df, col="walk", hue="walk", palette="tab20c", col_wrap=4, height=1.5) # 画一条水平线以显示起点 grid.map(plt.axhline, y=0, ls=":", c=".5") # 画一个直线图来显示每个随机行走的轨迹 grid.map(plt.plot, "step", "position", marker="o") # 调整刻度位置和标签 grid.set(xticks=np.arange(5), yticks=[-3, 3], xlim=(-.5, 4.5), ylim=(-3.5, 3.5)) # 调整图形的布局 grid.fig.tight_layout(w_pad=1)
激活函数(Activation Function)是一种添加到人工神经网络中的函数,旨在帮助网络学习数据中的复杂模式。类似于人类大脑中基于神经元的模型,激活函数最终决定了要发射给下一个神经元的内容。在人工神经网络中,一个节点的激活函数定义了该节点在给定的输入或输入集合下的输出。标准的计算机芯片电路可以看作是根据输入得到开(1)或关(0)输出的数字电路激活函数。因此,激活函数是确定神经网络输出的数学方程式。首先我们来了解一下人工神经元的工作原理,大致如下:上述过程可以可视化为:1. Sigmoid 激活函数Sigmoid 函数的图像看起来像一个 S 形曲线。函数表达式如下:在什么情况下适合使用 Sigmoid 激活函数呢?Sigmoid 函数的输出范围是 0 到 1。由于输出值限定在 0 到 1,因此它对每个神经元的输出进行了归一化;用于将预测概率作为输出的模型。由于概率的取值范围是 0 到 1,因此 Sigmoid 函数非常合适;梯度平滑,避免「跳跃」的输出值;函数是可微的。这意味着可以找到任意两个点的 sigmoid 曲线的斜率;明确的预测,即非常接近 1 或 0。Sigmoid 激活函数有哪些缺点?倾向于梯度消失;函数输出不是以 0 为中心的,这会降低权重更新的效率;Sigmoid 函数执行指数运算,计算机运行得较慢。2. Tanh / 双曲正切激活函数tanh 激活函数的图像也是 S 形函数表达式如下:tanh 是一个双曲正切函数。tanh 函数和 sigmoid 函数的曲线相对相似。但是它比 sigmoid 函数更有一些优势。首先,当输入较大或较小时,输出几乎是平滑的并且梯度较小,这不利于权重更新。二者的区别在于输出间隔,tanh 的输出间隔为 1,并且整个函数以 0 为中心,比 sigmoid 函数更好;在 tanh 图中,负输入将被强映射为负,而零输入被映射为接近零。注意:在一般的二元分类问题中,tanh 函数用于隐藏层,而 sigmoid 函数用于输出层,但这并不是固定的,需要根据特定问题进行调整。3. ReLU 激活函数ReLU 激活函数图像如图所示函数表达式如下:ReLU 函数是深度学习中较为流行的一种激活函数,相比于 sigmoid 函数和 tanh 函数,它具有如下优点:当输入为正时,不存在梯度饱和问题。计算速度快得多。ReLU 函数中只存在线性关系,因此它的计算速度比 sigmoid 和 tanh 更快。当然,它也有缺点:Dead ReLU 问题。当输入为负时,ReLU 完全失效,在正向传播过程中,这不是问题。有些区域很敏感,有些则不敏感。但是在反向传播过程中,如果输入负数,则梯度将完全为零,sigmoid 函数和 tanh 函数也具有相同的问题;我们发现 ReLU 函数的输出为 0 或正数,这意味着 ReLU 函数不是以 0 为中心的函数。4. Leaky ReLU它是一种专门设计用于解决 Dead ReLU 问题的激活函数函数表达式如下:为什么 Leaky ReLU 比 ReLU 更好?Leaky ReLU 通过把 x 的非常小的线性分量给予负输入(0.01x)来调整负值的零梯度(zero gradients)问题;leak 有助于扩大 ReLU 函数的范围,通常 a 的值为 0.01 左右;Leaky ReLU 的函数范围是(负无穷到正无穷)。注意:从理论上讲,Leaky ReLU 具有 ReLU 的所有优点,而且 Dead ReLU 不会有任何问题,但在实际操作中,尚未完全证明 Leaky ReLU 总是比 ReLU 更好。5. ELUELU 的提出也解决了 ReLU 的问题。与 ReLU 相比,ELU 有负值,这会使激活的平均值接近零。均值激活接近于零可以使学习更快,因为它们使梯度更接近自然梯度。函数表达式如下:显然,ELU 具有 ReLU 的所有优点,并且:没有 Dead ReLU 问题,输出的平均值接近 0,以 0 为中心;ELU 通过减少偏置偏移的影响,使正常梯度更接近于单位自然梯度,从而使均值向零加速学习;ELU 在较小的输入下会饱和至负值,从而减少前向传播的变异和信息。一个小问题是它的计算强度更高。与 Leaky ReLU 类似,尽管理论上比 ReLU 要好,但目前在实践中没有充分的证据表明 ELU 总是比 ReLU 好。6. PReLU(Parametric ReLU)PReLU 也是 ReLU 的改进版本:看一下 PReLU 的公式:参数α通常为 0 到 1 之间的数字,并且通常相对较小。PReLU 的优点如下:在负值域,PReLU 的斜率较小,这也可以避免 Dead ReLU 问题。与 ELU 相比,PReLU 在负值域是线性运算。尽管斜率很小,但不会趋于 0。7. SoftmaxSoftmax 是用于多类分类问题的激活函数,在多类分类问题中,超过两个类标签则需要类成员关系。对于长度为 K 的任意实向量,Softmax 可以将其压缩为长度为 K,值在(0,1)范围内,并且向量中元素的总和为 1 的实向量。Softmax 与正常的 max 函数不同:max 函数仅输出最大值,但 Softmax 确保较小的值具有较小的概率,并且不会直接丢弃。我们可以认为它是 argmax 函数的概率版本或「soft」版本。Softmax 函数的分母结合了原始输出值的所有因子,这意味着 Softmax 函数获得的各种概率彼此相关。Softmax 激活函数的主要缺点是:在零点不可微;负输入的梯度为零,这意味着对于该区域的激活,权重不会在反向传播期间更新,因此会产生永不激活的死亡神经元。8. Swish函数表达式:Swish 的设计受到了 LSTM 和高速网络中 gating 的 sigmoid 函数使用的启发。我们使用相同的 gating 值来简化 gating 机制,这称为 self-gating。self-gating 的优点在于它只需要简单的标量输入,而普通的 gating 则需要多个标量输入。这使得诸如 Swish 之类的 self-gated 激活函数能够轻松替换以单个标量为输入的激活函数(例如 ReLU),而无需更改隐藏容量或参数数量。Swish 激活函数的主要优点如下:「无界性」有助于防止慢速训练期间,梯度逐渐接近 0 并导致饱和;(同时,有界性也是有优势的,因为有界激活函数可以具有很强的正则化,并且较大的负输入问题也能解决);导数恒 > 0;平滑度在优化和泛化中起了重要作用。9. Maxout在 Maxout 层,激活函数是输入的最大值,因此只有 2 个 maxout 节点的多层感知机就可以拟合任意的凸函数。单个 Maxout 节点可以解释为对一个实值函数进行分段线性近似 (PWL) ,其中函数图上任意两点之间的线段位于图(凸函数)的上方。Maxout 也可以对 d 维向量(V)实现:假设两个凸函数 h_1(x) 和 h_2(x),由两个 Maxout 节点近似化,函数 g(x) 是连续的 PWL 函数。因此,由两个 Maxout 节点组成的 Maxout 层可以很好地近似任何连续函数。10. Softplus Softplus 函数:也称为 logistic / sigmoid 函数。Softplus 函数类似于 ReLU 函数,但是相对较平滑,像 ReLU 一样是单侧抑制。