《智能系统与技术丛书·AI安全之对抗样本入门》 对本书的赞誉 对本书的赞誉 机器学习在安全领域的应用越来越广泛,特别是近几年来,深度学习在安全漏洞检测、web应用防火墙、病毒检测等领域都有工业级的落地应用;但是黑客和黑产相应的入侵手法也发生了变化,其中一个手段就是从之前尝试绕过深度学习模型,变为攻击深度学习模型本身。兜哥在人工智能安全领域的实战和学术造诣深厚,本书从深度学习自身的脆弱性和遭受的一些攻击场景入手,讨论了如何加固深度学习模型和防范类似的攻击,对企业的安全工程师和从事安全人工智能的同仁,都有很好的指导和借鉴意义。 ——施亮,顶象技术首席科学家 & 合伙人 人工智能已经被证明在越来越多的细分领域达到甚至超过了人类的平均水平,中国、美国、俄罗斯等许多国家也把发展人工智能提升到国家战略层面。人们在大力发展人工智能的同时,对于人工智能自身的安全问题的研究却相对滞后,这将严重制约其在重要领域的应用。兜哥的这本书很好地介绍了ai安全领域非常基础且重要的对抗样本的基本原理,帮助大家了解人工智能自身的安全问题,以便开发出更加安全的ai应用。 ——胡影博士,中国电子技术研究院信息安全研究中心数据安全部主任 认识兜哥是从他的著作《企业安全建设入门》开始,在传统行业基于稳定性不断对商业软件进行深度改造时,他描述了互联网如何将开源用到了输出阶段。当传统安全遇到abcd的时候,兜哥选择沉下来做ai工程师并分享了大数据实践下的核心对抗样本调参思路,为他的工匠精神和分享精神点赞。 ——吕毅博士,中国人民银行金融信息中心信息安全部副主任 本书结合了作者在安全领域的多年实践经验,对对抗样本分析所面临的挑战进行了系统阐述,对业界常见的方式方法做了系统的归纳总结,有其独到的见解和主张。与其他机器学习系列丛书中的内容不同,本书针对的对象是人工智能本身,从对抗样本这一维度入手,深入浅出地叙述了对抗样本的基本原理、攻击方式和常见防御算法等内容。本书对信息安全和人工智能的从业者来说,都具有一流的参考价值。 ——王亿韬,北美互联网金融公司affirm信息安全主管,cissp/csslp/oscp 作者的系列书——ai安全三部曲,去年我已一一拜读,每本书都深入浅出,层层递进,读后大呼过瘾。前时应兜哥之邀为本书写荐语,欣然应允,不料今年工作繁忙拖了很久,甚为汗颜。近期终能挤出时间仔细研读。本书秉承兜哥的一贯风格,虽然锚定的是非常前沿的课题,仔细去看,依旧浅显易懂,分攻防两端,递进列举各类算法,并暖心提供基于不同ai框架的实现和评估。大巧若拙,递进的脉络在读者脑中自然形成,读后必然深受启发。我在此感谢兜哥勤奋执笔,又让我有先阅之乐。 ——王新刚,北美安全公司shape security数据平台负责人 序一 序一 此亦笃信之年,此亦大惑之年。此亦多丽之阳春,此亦绝念之穷冬。人或万事俱备,人或一事无成。我辈其青云直上,我辈其黄泉永坠。 ——《双城记》,狄更斯著,魏易译 如今是一个人工智能兴起的年代,也是一个黑产猖獗的年代。是一个机器学习算法百花齐放的年代,也是一个隐私泄露、恶意代码传播、网络攻击肆虐的年代。alphago碾压柯洁之后,不少人担心ai会抢了人类的工作,然而信息安全领域专业人才严重匮乏,极其需要ai来补充专业缺口。 兜哥的这本书展示了丰富多彩的机器学习算法在错综复杂的web安全中的应用,是一本非常及时的人工智能在信息安全领域的入门读物。没有最好的算法,只有最合适的算法。虽然这几年深度学习呼声很高,但各种机器学习算法依然在形形色色的应用场景中有着各自独特的价值,熟悉并用好这些算法在安全领域的实战中会起到重要的作用。 ——lenx,百度首席安全科学家,安全实验室负责人 序二 序二 兜哥写的“ai+安全”一系列书,每本我都让朋友从国内带来硅谷拜读。我和lenx等大佬混迹硅谷安全圈,一致很钦佩百度安全实验室对于“ai+安全”领域的贡献。 执笔为这本书写序的这天(2019年1月25日)恰逢人类ai历史又一伟大时刻:alphastar战胜了starcraft2人类高手玩家mana。我对这次战役的关注,甚至超过了alphago。starcraft即时战略游戏比围棋这种规则明确、棋盘信息透明的游戏,更加接近人类生存的环境:不完备的环境信息。安全领域一直是不完备的环境信息,在我看来,这意味着道高一尺魔高一丈强对抗的网络安全行业,也即将迎来一波新的方法论。 我从2012年起在美国硅谷的安全行业从事“ai+大数据”在安全和风控的企业级应用,有幸在emc/rsa、fireeye/mandiant等公司,与世界级的白帽一起成功地使用ai技术防御过若干个apt黑客军团(比如2017年在fireeye开发了nlp算法抵抗了越南apt32)。最近刚创办了anchain.ai公司,志在将ai用于区块链安全,并在历史上首次检测到了bapt(blockchain apt)黑客军团。 我也有幸直接参与了几次和黑客正面交锋的案例,比如2018年的bapt-fomo3d黑客,他们攻击fomo3d游戏dapp,居然启用了上万个攻击合约,短时间内盗走了两百万美元,而项目方束手无策。 “战略上藐视敌人,战术上重视敌人。”作为安全行业的多年从业者,我坚信我们可以利用技术的力量形成战略的优势。 对于广大安全产品研发人员,我建议在“战术上重视敌人”,学习ai技术将如虎添翼。增强学习、对抗学习、深度学习等领域近年来涌现了许多新算法,并且在asic、gpu这些强大的芯片基础上,实现了高效的人工智能落地应用,比如alphastar、alphago和硅谷满地跑的无人驾驶车。 我建议大家仔细研读兜哥的这本新书,因为黑客也在学习进化。 ——victor fang博士,anchain.ai创始人和ceo 自序 自序 这不是我第一次写序了,但是真拿起笔还是觉得挺难写的。记得我写第一本书时,女儿还在蹒跚学步,拿着我的书时还分不清书的前后正反,但是现在她已经在幼儿园学会做月饼了。据说现在人工智能的知识已经写入高中教材了,这方面我倒是对闺女比较有信心,至少在人工智能方面她启蒙得比较早。 机缘巧合,我负责了一个ai模型安全的开源项目advbox。虽然我之前接触过ai安全,但是主要集中在ai赋能传统安全的领域,简单讲就是我之前写的《web安全之机器学习入门》这类书里介绍的如何将ai技术应用到传统安全领域。相对传统安全领域,ai模型的安全是一个陌生但更加有趣的领域。从简单的把猫变成狗,到稍微复杂点的用奇怪的“鸟叫虫鸣”就可以唤醒智能音箱、点外卖,再到更加酷炫的换脸和欺骗无人车,这都是对抗样本的典型应用。开发和运营advbox是我工作的一部分,也是我生活的一部分,我写了个小脚本每天爬github上的排名并发送邮件给我,每天早上我都会刷刷邮箱看一下最新排名。advbox的关注数也从零一直慢慢爬到业内关注的一个排名。为了更好地运营advbox,我组建了qq群和微信群,在回答用户问题的过程中,我发现其实很多问题非常基础:从tensorflow/pytorch的使用到深度学习的基础知识,从对抗样本的基本原理到图像处理中的一些小技巧。于是我想到了可以写一本书来介绍这方面的基础知识。 很多人在网上抱怨,2018年是比较艰难的一年,我也有同样的感受。工作繁忙了许多,而且换了新环境,许多东西需要再去适应。这本书从构思到基本写完,花了将近一年的时间,几乎占据了我全部的休息时间,确实挺不容易。其实我也反思过几次,为啥不去做些轻车熟路的事,非要去折腾些big4的论文里介绍的东西,非要在一个新成果刚发布就可能要过时的赛道去追逐。如果非要找个理由的话,我个人的感受是:容易的事轮不到我,早已是千军万马过独木桥了;困难的事坚持做,没准就走下去了。 本书的定位是学习对抗样本的入门书籍,因此也简单介绍了相关领域的背景知识便于读者理解,使用的数据集和场景是比较典型的图像分类、目标识别等领域。第1章介绍了深度学习的基础知识,重点介绍了与对抗样本相关的梯度、优化器、反向传递等知识点。第2章介绍了如何搭建学习对抗样本的软硬件环境,虽然gpu不是必需的,但是使用gpu可以更加快速地验证你的想法。第3章概括介绍了常见的深度学习框架,从tensorflow、keras、pytorch到mxnet。第4章介绍了图像处理领域的基础知识,这部分知识对于理解对抗样本领域的一些常见图像处理技巧非常有帮助。第5章介绍了常见的白盒攻击算法,从最基础的fgsm、deepfool到经典的jsma和cw。第6章介绍了常见的黑盒攻击算法。第7章介绍了对抗样本在目标识别领域的应用。第8章介绍了对抗样本的常见抵御算法,与对抗样本一样,抵御对抗样本的技术也非常有趣。第9章介绍了常见的对抗样本工具以及如何搭建nips 2017对抗防御环境和轻量级攻防对抗环境robust-ml,通过这章读者可以了解如何站在巨人的肩膀上,快速生成自己的对抗样本,进行攻防对抗。 本书适合人工智能领域的从业人员、大专院校计算机相关专业学生,而不仅仅是信息安全领域的学生和从业人员。因为对抗样本的知识对于人工智能领域的从业人员非常重要,伴随着人工智能的遍地开花和逐步落地,从智能驾驶到人脸支付,从智能家居到智能安防,人工智能从一个学术名词变成了真正影响大家生活的技术,直接关系到大家的人身安全、财产安全还有个人隐私。对抗样本带来的问题,也是传统安全技术几乎难以解决的。了解对抗样本的基本原理,对于人工智能领域的从业人员开发出更加安全的应用是非常有帮助的。 我要感谢家人对我的支持,本来工作就很忙,没有太多时间处理家务,写书以后更是侵占了我大量的休息时间,我的妻子无条件地承担起了全部家务,尤其是照料孩子。我还要感谢我的女儿,写书这段时间几乎没有时间陪她玩,我也想用这本书作为她的生日礼物。我还要感谢编辑吴怡对我的支持和鼓励,让我可以坚持把这本书写完。lenx对于我写这本书帮助很大,他对ai安全的深刻理解,积极但又务实,让我在研究的道路上既信心满满又不至于过于狂热而迷失道路。还要感谢advbox团队以及在github上给advbox提交过代码的同学们。 最后还要感谢各位业内好友对我的支持,以下排名不分先后: 马杰@百度安全、lenx@百度安全、黄正@百度安全、包沉浮@百度安全、海棠姐@百度安全、edward@百度安全、贾云瀚@百度安全、云鹏@百度无人车、施亮@顶象、victor fang@anchain、谢忱@freebuf、大路@天际友盟、郭伟@数字观星、周涛@阿里、姚志武@借贷宝、刘静@安天、高磊@阿里、尹毅@sobug、吴圣@58、康宇@新浪、幻泉@i春秋、田老师@阳光保险、readonly@易宝支付、樊春亮@泰康、聂君@360、林伟@360、白教主@360、李滨@腾讯、张维垚@饿了吗、阿杜@优信、高磊@安巽科技、王延辉@平安、吕毅@人行、雷诚@武汉大学、鸟哥@阿里云、咸鱼@京东、梁知音@京东、小马哥@京东、张超@清华大学、徐恪@清华大学、李勇@清华大学、李琦@清华大学。我平时在freebuf专栏以及i春秋分享企业安全建设以及人工智能相关经验与最新话题,同时运营我的微信公众号“兜哥带你学安全”,欢迎大家关注并在线交流。 计算机是一门非常强调理论联系实践的学科,我经常在和读者交流时说:安全会议的ppt就像电影,浓缩、精彩、有趣;博客、公众号上的文章就像电视剧,有细节、有思路;技术书籍更像原著,系统、详细、原汁原味。但是就像看再多武侠小说也不一定真能耍出一招半式一样,真正掌握一门武艺还是要勤于练习。因此我在github上也把书里提到的示例代码开源出来,读者可以根据情况一边修改一边验证自己的理解,对应的地址为:https://github.com/duoergun0729/adversarial_examples。 前言 前言 生活中的深度学习 深度学习自2006年产生之后就受到科研机构、工业界的高度关注。最初,深度学习主要用于图像和语音领域。从2011年开始,谷歌研究院和微软研究院的研究人员先后将深度学习应用到语音识别,使识别错误率下降了20%~30%。2012年6月,谷歌首席架构师jeff dean和斯坦福大学教授andrew ng主导著名的google brain项目,采用16万个cpu来构建一个深层神经网络,并将其应用于图像和语音的识别,最终大获成功。 2016年3月,alphago与围棋世界冠军、职业九段棋手李世石进行围棋人机大战,以4比1的总比分获胜;2016年年末2017年年初,该程序在中国棋类网站上以“大师”(master)为注册账号与中日韩数十位围棋高手进行快棋对决,连续60局无一败绩;2017年5月,在中国乌镇围棋峰会上,它与排名世界第一的围棋世界冠军柯洁对战,以3比0的总比分获胜。alphago的成功更是把深度学习的热潮推向了全球,成为男女老少茶余饭后关注的热点话题。 现在,深度学习已经遍地开花,在方方面面影响和改变着人们的生活,比较典型的应用包括智能家居、智能驾驶、人脸支付和智能安防。 深度学习的脆弱性 深度学习作为一个非常复杂的软件系统,同样会面对各种黑客攻击。黑客通过攻击深度学习系统,也可以威胁到财产安全、个人隐私、交通安全和公共安全(见图0-1)。针对深度学习系统的攻击,通常包括以下几种。 图0-1 深度学习的脆弱性 1. 偷取模型 各大公司通过高薪聘请ai专家设计模型,花费大量资金、人力搜集训练数据,又花费大量资金购买gpu设备用于训练模型,最后得到深度学习模型。深度学习模型的最终形式也就是从几百kb到几百mb不等的一个模型文件。深度学习模型对外提供服务的形式也主要分为云模式的api,或者私有部署到用户的移动设备或数据中心的服务器上。针对云模式的api,黑客通过一定的遍历算法,在调用云模式的api后,可以在本地还原出一个与原始模型功能相同或者类似的模型;针对私有部署到用户的移动设备或数据中心的服务器上,黑客通过逆向等传统安全技术,可以把模型文件直接还原出来供其使用。偷取深度学习模型的过程如图0-2所示。 2. 数据投毒 针对深度学习的数据投毒主要是指向深度学习的训练样本中加入异常数据,导致模型在遇到某些条件时会产生分类错误。如图0-3所示。早期的数据投毒都存在于实验室环境,假设可以通过在离线训练数据中添加精心构造的异常数据进行攻击。这一攻击方式需要接触到模型的训练数据,而在实际环境中,绝大多数情况都是公司内部在离线数据中训练好模型再打包对外发布服务,攻击者难以接触到训练数据,攻击难以发生。于是攻击者把重点放到了在线学习的场景,即模型是利用在线的数据,几乎是实时学习的,比较典型的场景就是推荐系统。推荐系统会结合用户的历史数据以及实时的访问数据,共同进行学习和判断,最终得到推荐结果。黑客正是利用这一可以接触到训练数据的机会,通过一定的算法策略,发起访问行为,最终导致推荐系统产生错误。 图0-2 偷取深度学习模型 图0-3 针对深度学习的数据投毒 3. 对抗样本 对抗样本由christian szegedy等人提出,是指在数据集中通过故意添加细微的干扰所形成的输入样本,这种样本导致模型以高置信度给出一个错误的输出。在正则化背景下,通过对抗训练减少原有独立同分布的测试集的错误率,在对抗扰动的训练集样本上训练网络。 简单地讲,对抗样本通过在原始数据上叠加精心构造的人类难以察觉的扰动,使深度学习模型产生分类错误。以图像分类模型为例,如图0-4所示,通过在原始图像上叠加扰动,对于肉眼来说,扰动非常细微,图像看起来还是熊猫,但是图像分类模型却会以很大的概率识别为长臂猿。 图0-4 针对图像分类模型的对抗样本 下面以一个图像分类模型为例,更加直接地解释对抗样本的基本原理。通过在训练样本上学习,学到一个分割平面,在分割平面一侧的为绿球,在分割平面另外一侧的为红球。生成攻击样本的过程,就是在数据上添加一定的扰动,让其跨越分割平面,从而把分割平面一侧的红球识别为绿球,如图0-5所示。 图0-5 对抗样本的基本原理 对抗样本按照攻击后的效果分为targeted attack(定性攻击)和non-targeted attack(无定向攻击)。区别在于targeted attack在攻击前会设置攻击的目标,比如把红球识别为绿球,或者把面包识别为熊猫,也就是说在攻击后的效果是确定的;non-targeted attack在攻击前不用设置攻击目标,只要攻击后,识别的结果发生改变即可,可能会把面包识别为熊猫,也可能识别为小猪佩琪或者小猪乔治,如图0-6所示。 图0-6 targeted attack和non-targeted attack 对抗样本按照攻击成本分为white-box attack(白盒攻击)、black-box attack(黑盒攻击)和real-world attack/physical attack(真实世界/物理攻击)。 white-box attack(见图0-7)是其中攻击难度最低的一种,前提是能够完整获取模型的结构,包括模型的组成以及隔层的参数情况,并且可以完整控制模型的输入,对输入的控制粒度甚至可以到比特级别。由于white-box attack前置条件过于苛刻,通常作为实验室的学术研究或者作为发起black-box attack和real-world attack/physical attack的基础。 图0-7 white-box attack black-box attack相对white-box attack攻击难度具有很大提高,black-box attack完全把被攻击模型当成一个黑盒,对模型的结构没有了解,只能控制输入,通过比对输入和输出的反馈来进行下一步攻击,见图0-8。 图0-8 black-box attack real-world attack/physical attack(见图0-9)是这三种攻击中难度最大的,除了不了解模型的结构,甚至对于输入的控制也很弱。以攻击图像分类模型为例(见图0-10),生成的攻击样本要通过相机或者摄像头采集,然后经过一系列未知的预处理后再输入模型进行预测。攻击中对抗样本会发生缩放、扭转、光照变化、旋转等。 图0-9 real-world attack/physical attack 图0-10 图像分类模型的真实世界/物理攻击 常见检测和加固方法 1. 深度学习脆弱性检测 检测深度学习脆弱性的过程,其实就是发起攻击的过程,常见的白盒攻击算法列举如下。 ? ilcm(最相似迭代算法) ? fgsm(快速梯度算法) ? bim(基础迭代算法) ? jsma(显著图攻击算法) ? deepfool(deepfool算法) ? c/w(c/w算法) 常见的黑盒攻击方法列举如下。 ? single pixel attack(单像素攻击) ? local search attack(本地搜索攻击) 2. 深度学习脆弱性加固 针对深度学习脆弱性进行加固的常见方法主要包括以下几种,我们将重点介绍adversarial training。 ? feature squeezing(特征凝结) ? spatial smoothing(空间平滑) ? label smoothing(标签平滑) ? adversarial training(对抗训练) ? virtual adversarial training(虚拟对抗训练) ? gaussian data augmentation(高斯数据增强) adversarial training如图0-11所示,其基本思路是,常见的对抗样本生成算法是已知的,训练数据集也是已知的,那么可以通过常见的一些对抗样本工具箱,比如advbox或者foolbox,在训练数据的基础上生成对应的对抗样本,然后让深度学习模型重新学习,让它认识这些常见的对抗样本,这样新生成的深度学习模型就具有了一定的识别对抗样本的能力。 与adversarial training思路类似的是gaussian data augmentation。gaussian data augmentation的基本原理是,对抗样本是在原始数据上叠加一定的扰动,这些扰动非常接近随机的一些噪声。adversarial training虽然简单易于实现,但是技术上难以穷尽所有的攻击样本。gaussian data augmentation直接在原始数据上叠加高斯噪声,如图0-12所示,k为高斯噪声的系数,系数越大,高斯噪声越强,其他参数分别表示高斯噪声的均值和标准差。gaussian data augmentation把训练数据叠加了噪声后,重新输入给深度学习模型学习,通过增加训练轮数、调整参数甚至增加模型层数,在不降低原有模型准确度的情况下,让新生成的深度学习模型具有了一定的识别对抗样本的能力。 图0-11 adversarial training原理图 图0-12 图像增加高斯噪声 对抗样本领域的最新进展 对抗样本是ai安全研究的一个热点,新的攻击算法和加固方法层出不穷,而且攻击场景也从实验室中的简单图像分类,迅速扩展到智能音箱、无人驾驶等领域。百度安全实验室的最新研究报告《感知欺骗:基于深度神经网络(dnn)下物理性对抗攻击与策略》成功入选blackhat europe 2018。报告展现了让物体在深度学习系统的“眼”中凭空消失,在ai时代重现了大卫·科波菲尔的经典魔法。针对深度学习模型漏洞进行物理攻击可行性研究有着广泛的应用前景,在自动驾驶领域、智能安防领域、物品自动鉴定领域都有重要的实际意义。 如图0-13所示,在时间t0的时候,当在车后显示器中显示正常logo时,yolov3可以正确识别目标车辆,而在t1时,切换到扰动后的图片时,它可以立刻让目标车辆在yolov3面前变得无法辨识;在t2时,如图0-14所示切换回正常的图片,yolov3重新可以识别目标车辆。这是首次针对车辆的物理攻击的成功展示,与以往的学术论文相比,在攻击目标的大小、分辨率的高低以及物理环境的复杂性等方面,在影响和难度上都是一个巨大提升。 图0-13 t0时可以正常识别出车辆,t1时无法识别出车辆 图0-14 t2时可以正常识别出车辆 kan yuan和di tang等人在论文《stealthy porn:understanding real-world adversarial images for illicit online promotion》中介绍了黑产如何通过单色化、加噪声、增加文字、仿射变化、滤波模糊化和遮盖等方式让违规图片绕过目前主流的图片内容检测服务。这也标志着对抗样本技术已经从实验室环境真正进入了网络对抗实战。 国内安全人员在对抗样本领域的研究成果得到了国际的普遍认可。朱军等人指导的清华大学团队曾在nips 2017对抗样本攻防竞赛中夺冠,纪守领老师所在的nesa lab提出了一种新型的对抗性验证码,能防范来自打码平台等黑产的破解。 1.1.1 数据预处理 1.1.1 数据预处理 深度学习中非常重要的一个环节,就是要把物理世界中的实物特征化,最终表示为多维向量,这一过程称为数据预处理。比如图片就可以表示为一个多维数组,形状为长度、宽度和信道数。比如一个分辨率为28x28的灰度图像,就可以表示成形状为[28,28,1]的向量,一个分辨率为160x160的rgb图像,就可以表示成形状为[160,160,3]的向量。 1.1.2 定义网络结构 1.1.2 定义网络结构 一般认为,深度学习训练好的模型包括两部分,一个是对网络结构的描述,或者称为对网络结构的定义;另外一个是每层网络的具体参数值,这两部分加起来才是一个完整的深度学习模型。完成数据预处理后,就需要定义网络结构,或者说对我们的问题进行数学建模。 假设可以通过一条直线,把黑客和正常用户区分开,即我们认为这个二分类问题是线性问题,如图1-1所示。设特征向量为x,对应的标签为y,使用一个线性函数定义整个网络: y=w*x+b 其中w和b就是模型的参数,训练模型的过程就是迭代求解w和b的过程,通常x是一个多维向量,所以w和b通常也是多维向量。当完成了网络的定义后,输入x就可以获得确定的y,这一过程称为前向计算过程,或者称为前向传播。面对更加复杂的问题,需要使用更加复杂的层来定义网络。定义网络时的常用层包括:dense层、activation层、dropout层、flatten层、reshape层和permute层等。 图1-1 区分正常用户和黑客的二分类问题 1. dense层 dense层是最常见的网络层,用于构建一个全连接。一个典型的全连接结构由输入、求和、激活、权重矩阵、偏置和输出组成,如图1-2所示,训练的过程就是不断获得最优的权重矩阵和偏置(bias)的过程。 图1-2 全连接结构示意图 了解了全连接的结构后,也不难理解创建dense层的几个参数了,例如: keras.layers.core.dense(units, activation=none, use_bias=true, kernel_initializer='glorot_uniform', bias_initializer='zeros', kernel_regularizer=none, bias_regularizer=none, activity_regularizer=none, kernel_constraint=none, bias_constraint=none) 其中比较重要的几个参数含义如下。 ? units表示隐藏层节点数。 ? activation(激活函数)详细介绍请参见activation层的相关内容。 ? use_bias表示是否使用偏置。 2. activation层 actiration层对一个层的输出施加激活函数,常见的激活函数包括以下几种。 (1)relu relu函数当输入小于0时为0,当输入大于0时等于输入。使用代码绘制relu的图像,获得图像(见图1-3)。 def relu(x): if x > 0: return x else: return 0 def func4(): x = np.arange(-5.0, 5.0, 0.02) y=[] for i in x: yi=relu(i) y.append(yi) plt.xlabel('x') plt.ylabel('y relu(x)') plt.title('relu') plt.plot(x, y) plt.show() 图1-3 relu函数 (2)leakyrelu leakyrelu函数是从relu函数发展而来的,当输入小于0时为输入乘以一个很小的系数,比如0.1,当输入大于0时等于输入。使用代码绘制leakyrelu的图像,获得图像(见图1-4)。 def leakyrelu(x): if x > 0: return x else: return x*0.1 def func5(): x = np.arange(-5.0, 5.0, 0.02) y=[] for i in x: yi=leakyrelu(i) y.append(yi) plt.xlabel('x') plt.ylabel('y leakyrelu(x)') plt.title('leakyrelu') plt.plot(x, y) plt.show() 图1-4 leakyrelu图像 (3)tanh tanh也称为双切正切函数,取值范围为[–1,1]。tanh在特征相差明显时的效果会很好,在循环过程中会不断扩大特征效果。tanh的定义如下: 使用代码绘制tanh的图像,获得图像(见图1-5)。 x = np.arange(-5.0, 5.0, 0.02) y=(np.exp(x)-np.exp(-x))/(np.exp(x)+np.exp(-x)) plt.xlabel('x') plt.ylabel('y tanh(x)') plt.title('tanh') plt.plot(x, y) plt.show() 图1-5 tanh图像 (4)sigmoid sigmoid可以将一个实数映射到(0,1)的区间,可以用来做二分类。sigmoid的定义如下: 使用代码绘制sigmoid的图像,获得图像(见图1-6)。 图1-6 sigmoid图像 x = np.arange(-5.0, 5.0, 0.02) y=1/(1+np.exp(-x)) plt.xlabel('x') plt.ylabel('y sigmoid(x)') plt.title('sigmoid') plt.plot(x, y) plt.show() activation层可以单独使用,也可以作为其他层的参数,比如创建一个输入大小为784,节点数为32,激活函数为relu的全连接层的代码为: model.add(dense(32, input_shape=(784,))) model.add(activation('relu')) 等价于下列代码: model.add(dense(32, activation='relu',input_shape=(784,))) 3. dropout层 在深度学习中,动辄几万的参数需要训练,非常容易造成过拟合,通常为了避免过拟合,会在每次训练的时候随机选择一定的节点,使它们临时失效,形象的比喻是,好比每次识别图像的时候,随机地挡住一些像素,遮挡适当比例的像素不会影响图像的识别,但是却可以比较有效地抑制过拟合。dropout层的定义如下: keras.layers.core.dropout(rate, noise_shape=none, seed=none) 其中,常用的参数就是rate,表示临时失效的节点的比例,经验值为0.2~0.4比较合适。 4. embedding层 embedding层负责将输入的向量按照一定的规则改变维度,有点类似于word2vec的处理方式,把词可以映射到一个指定维度的向量中,其函数定义如下: keras.layers.embedding(input_dim, output_dim, embeddings_initializer='uniform', embeddings_regularizer=none, activity_regularizer=none, embeddings_constraint=none, mask_zero=false, input_length=none) 其中比较重要的参数为: ? input_dim:输入的向量的维度。 ? output_dim:输出的向量的维度。 ? embeddings_initializer:初始化的方式,通常使用glorot_normal或者uniform。 5. flatten层 flatten层用来将输入压平,即把多维的输入一维化。 6. permute层 permute层将输入的维度按照给定模式进行重排。一个典型场景就是在keras处理图像数据时,需要根据底层是tensorflow还是theano调整像素的顺序。在tensorflow中图像保存的顺序是(width,height,channels)而在theano中则为(channels,width,height),比如mnist图像,在tensorflow中的大小就是(28,28,1),而在theano中是(1,28,28)。示例代码如下: if k.image_dim_ordering() == 'tf': # (width, height, channels) model.add(permute((2, 3, 1), input_shape=input_shape)) elif k.image_dim_ordering() == 'th': # (channels, width, height) model.add(permute((1, 2, 3), input_shape=input_shape)) else: raise runtimeerror('unknown image_dim_ordering.') 7. reshape层 reshape层用于将输入shape转换为特定的shape。函数定义如下代码所示: keras.layers.core.reshape(target_shape) 其中target_shape为希望转换成的形状,比如图片的大小为(1,28,28,1),但是网络的输入大小为(1,784)时就需要使用reshape层。 1.1.3 定义损失函数 1.1.3 定义损失函数 完成了网络定义后,我们可以针对指定的输入x获得对应的预测值y,我们自然希望预测值y与真实值y_之间的差距越小越好,理想的情况就是在数据集上预测值y和真实值y_总是完全一样。但是事实上这几乎是无法做到的,我们需要定义预测值和真实值之间的差距,也就是理想和现实之间的差距。可以认为深度学习训练的过程,就是不断追求损失函数最小化的过程。以keras为例,常见的损失函数有以下几种: ? mean_squared_error或mse ? mean_absolute_error或mae ? mean_absolute_percentage_error或mape ? mean_squared_logarithmic_error或msle ? squared_hinge ? hinge ? categorical_hinge ? binary_crossentropy ? logcosh ? categorical_crossentropy ? sparse_categorical_crossentrop 其中二分类问题经常使用的是binary_crossentropy,多分类问题经常使用的是categorical_crossentropy,回归问题使用mse和mae。 1.1.4 反向传递与优化器 1.1.4 反向传递与优化器 深度学习训练过程如图1-7所示。 在深度学习模型里面,经常需要使用梯度算法,针对损失函数的反馈不断调整各层的参数,使得损失函数最小化。在训练阶段,真实值就是样本对应的真实标签,预测值就是机器学习模型预测的标签值,这些都是明确的,所以损失函数是可以定义和计算的。机器学习模型训练的过程就是不断调整参数追求损失函数最小的过程。梯度可以理解为多元函数的指定点上升的坡度,假设多元函数可以表示为f(x,y),那么对应的梯度的定义为: 可见梯度可以用偏导数来定义,通常损失函数就是这个多元函数,特征向量就可以看成这个多元函数的某个点。在训练过程中,针对参数的调整可以使用梯度和学习率来定义,其中学习率也叫作学习步长,物理含义就是变量在梯度方向上移动的长度,学习率是一个非常重要的参数,学习率过大会导致损失函数的震荡难以收敛,过小会导致计算缓慢,目前还没有很成熟的理论来推倒最合适的学习率,经验值是0.001~0.1。以表示学习率,那么迭代更新参数x的方法为: 图1-7 深度学习训练过程 在求函数的最大值时,我们会向梯度向上的方向移动,使用加号,也称为梯度向上算法。如果我们想求函数的最小值,则需要向梯度向下的方向移动,使用减号,也称为梯度下降算法,比如求损失函数最小值时,对应迭代求解的方法为: 我们通过一个非常简单的例子演示这个过程,假设我们只有一个变量x,对应的损失函数定义为: 根据梯度的定义,可以获得对应的梯度为: 我们随机初始化x,将学习率设置为0.1,整个过程如下: def demo(): import random a=0.1 x=random.randint(1,10) y = x * x + 2 index=1 while index < 100 and abs(y-2) > 0.01 : y=x*x+2 print "batch={} x={} y={}".format(index,x,y) x=x-2*x*a index+=1 整个迭代过程最多100步,由于我们预先知道函数的最小值为2,所以如果当计算获得的函数值非常接近2,我们也可以提前退出迭代过程,比如绝对值相差不超过0.01。最后果然没让我们失望,在迭代20次后就找到了接近理论上的最小点: batch=14 x=0.329853488333 y=2.10880332377 batch=15 x=0.263882790666 y=2.06963412721 batch=16 x=0.211106232533 y=2.04456584141 batch=17 x=0.168884986026 y=2.02852213851 batch=18 x=0.135107988821 y=2.01825416864 batch=19 x=0.108086391057 y=2.01168266793 batch=20 x=0.0864691128455 y=2.00747690748 keras里面提供相应的工具返回loss函数关于variables的梯度,variables为张量变量的列表,这里的loss函数即损失函数: from keras import backend as k k.gradients(loss, variables) keras也提供了function用于实例化一个keras函数,inputs是输入张量的列表,其元素为占位符或张量变量,outputs为输出张量的列表: k.function(inputs, outputs, updates=[]) 常用的优化器包括sgd、rmsprop和adam。 1. sgd sgd即随机梯度下降法,是最基础的优化方法。普通的训练方法需要重复不断地把整套数据放入神经网络中训练,这会消耗大量计算资源。sgd则会把数据拆分后再分批不断地放入神经网络中来计算。每次使用批数据,虽然不能反映整体数据的情况,不过却在很大程度上加速了神经网络的训练过程,而且也不会丢失太多准确率。 sgd支持动量参数,支持学习衰减率,函数的定义如下: keras.optimizers.sgd(lr=0.01, momentum=0.0, decay=0.0, nesterov=false) 其中比较重要的参数如下: ? lr:学习率。 ? momentum:动量参数。 ? decay:每次更新后的学习率衰减值。 2. rmsprop rmsprop是面对递归神经网络时的一个良好选择,函数的定义如下: keras.optimizers.rmsprop(lr=0.001, rho=0.9, epsilon=1e-06) 其中比较重要的参数如下: ? lr:学习率。 ? epsilon:大于或等于0的小浮点数,防止除0错误。 3. adam adam是一种可以替代sgd的一阶优化算法,它能基于训练数据迭代地更新神经网络权重,是目前最受欢迎的优化算法之一,定义如下: keras.optimizers.adam(lr=0.001, beta_1=0.9, beta_2=0.999, epsilon=1e-08) 其中比较重要的参数如下: ? lr:学习率。 ? epsilon:大于或等于0的小浮点数,防止除0错误。 下面我们以迭代生成对抗样本的例子来感性认识一下不同优化器的计算收敛速度,代码路径为: https://github.com/duoergun0729/adversarial_examples/code/1-case1-pytorch.ipynb 首先,定义全局变量,其中adam_original_loss、sdg_original_loss和rmsprop_original_loss分别代表迭代过程中不同优化算法对应的损失函数的值: sdg_original_loss=[] rmsprop_original_loss=[] epoch_range=[] 加载测试图片,并缩放到长和宽均为224: #获取计算设备,默认是cpu device = torch.device("cuda" if torch.cuda.is_available() else "cpu") #图像加载以及预处理 image_path="../picture/cropped_panda.jpg" orig = cv2.imread(image_path)[..., ::-1] orig = cv2.resize(orig, (224, 224)) img = orig.copy().astype(np.float32) 对图像数据进行标准化处理,由于攻击的图像分类模型是基于imagenet2012数据集进行预训练的,因此需要使用imagenet2012数据集特有的均值mean和标准差std进行标准化: mean = [0.485, 0.456, 0.406] std = [0.229, 0.224, 0.225] img /= 255.0 img = (img - mean) / std img = img.transpose(2, 0, 1) img=np.expand_dims(img, axis=0) img = variable(torch.from_numpy(img).to(device).float()) 实例化alexnet模型并加载预训练的参数。在使用迭代优化的过程中,整个模型的参数不变化,反向传递仅调整原始图像的内容: #使用预测模式主要影响dropout和bn层的行为 model = models.alexnet(pretrained=true).to(device).eval() #获取分类标签 label=np.argmax(model(img).data.cpu().numpy()) print("label={}".format(label)) #图像数据梯度可以获取 img.requires_grad = true #设置为不保存梯度值,自然也无法修改 for param in model.parameters(): param.requires_grad = false 使用定向攻击,攻击目标的标签值为288,最大迭代次数为100: loss_func = torch.nn.crossentropyloss() epochs=100 target=288 target=variable(torch.tensor([float(target)]).to(device).long()) 迭代优化的计算过程中,根据预测结果与定向攻击目标计算损失值,并通过手工调用反向传递过程,更新原始图像: for epoch in range(epochs): # 梯度清零 optimizer.zero_grad() # forward + backward output = model(img) loss = loss_func(output, target) label=np.argmax(output.data.cpu().numpy()) adam_original_loss+=[loss] epoch_range += [epoch] #手工调用反向传递计算,更新原始图像 loss.backward() optimizer.step() 分别实例化不同的优化器,记录100次迭代优化过程中损失值的变化,如图1-8所示,当使用相同的学习速率对同一图片进行迭代优化生成定向攻击样本时,rmsprop和adam明显快于sgd: fig, ax = plt.subplots() ax.plot(np.array(epoch_range), np.array(adam_original_loss), 'b--', label='adam') ax.plot(np.array(epoch_range), np.array(rmsprop_original_loss), 'b-', label='rmsprop') ax.plot(np.array(epoch_range), np.array(sdg_original_loss), 'b:', label='sgd') legend = ax.legend(loc='best', shadow=true, fontsize='large') legend.get_frame().set_facecolor('#ffffff') plt.xlabel('iteration step ') plt.ylabel('loss') plt.show() 图1-8 相同条件下不同优化算法的收敛速度 1.1.5 范数 1.1.5 范数 范数是一种强化了的距离概念,通常为了提高模型的抗过拟合能力被加入到损失函数中,下面介绍常见的几种范数的定义。 1. l0范数 l0范数并不是一个真正的范数,它主要用于度量向量中非零元素的个数。在对抗样本中,l0范数通常指的是对抗样本相对原始图片,所修改像素的个数。 2. l1范数 l1范数有很多的名字,例如曼哈顿距离、最小绝对误差等。使用l1范数可以度量两个向量间的差异,表示向量中非零元素的绝对值之和。 3. l2范数 l2范数是我们最常用的范数,欧氏距离就是一种l2范数,表示向量元素的平方和再开方。在对抗样本中,l2范数通常指的是对抗样本相对原始图片,所修改像素的变化量的平方和再开方。 4. 无穷范数 无穷范数也被记作linf,主要用于度量向量元素的最大值。在对抗样本中,linf范数通常指的是对抗样本相对原始图片,所修改像素的变化量绝对值的最大值。 1.2 传统的图像分类算法 1.2 传统的图像分类算法 对抗样本的一个重要应用场景就是在机器视觉领域,下面我们重点介绍一下其中的图像分类。图像分类是根据图像的原始信息将不同类别图像区分开来,是计算机视觉中重要的基本问题,也是图像检测、图像分割、物体跟踪、行为分析等其他高层视觉任务的基础。图1-9所示为图像分类识别不同的花的品种。图像分类在很多领域有广泛应用,包括安防领域的人脸识别和智能视频分析等,交通领域的交通场景识别,互联网领域基于内容的图像检索和相册自动归类,医学领域的图像识别等。 图1-9 图像分类识别不同的花的品种 在cnn出现之前,图像分类算法依赖于复杂的特征工程,常用的特征提取方法包括sift(scale-invariant feature transform,尺度不变特征转换)、hog(histogram of oriented gradient,方向梯度直方图)、lbp(local binray pattern,局部二值模式)等,常用的分类算法为svm。 1.3.1 局部连接 1.3.1 局部连接 人们尝试直接把原始图像作为输入,通过深度学习算法直接进行图像分类,从而绕过复杂的特征工程。常见的深度学习算法都是全连接形式,所谓全连接,就是第n–1层的任意一个节点,都和第n层所有节点有连接,如图1-10所示。 同时,临近输入层的隐藏层的任意节点与输入层的全部节点都有连接,如图1-11所示。 图1-10 全连接示意图 图1-11 输入层与隐藏层全连接示意图 以一个大小为1000x1000的灰度图像为例,输入层节点数量为1000x1000=1000000,隐藏层节点数量也为1000x1000=1000000,仅输入层与隐藏层的连接就需要1000000x1000000=1012,这几乎是个天文数字,如此巨大的计算量阻碍了深度学习在图像分类方向的应用。 事情的转机来源于生物学的一个发现。人们在研究猫的视觉神经细胞时发现,一个视觉神经元只负责处理视觉图像的一小块,这一小块称为感受野(receptive field),类比在图像分类领域,识别一个图像是何种物体时,一个点与距离近的点之间的关联非常紧密,但是和距离远的点之间关系就不大了,甚至足够远的时候就可以忽略不计。 全连接与局部连接示意图,如图1-12所示。 图1-12 全连接与局部连接示意图 具体到深度学习算法上,在隐藏层与输入层之间,隐藏层的一个节点只处理一部分输入层节点的数据,形成局部连接。继续上面的例子,假设每个隐藏层的节点只处理10x10大小的数据,也就是说每个隐藏层的节点只与输入层的100个节点连接,这样在隐藏层节点数量和输入层节点数量不变的情况下,输入层与隐藏层的连接需要1000000x100=108,是全连接的万分之一,虽然计算量下降不少,但是依然十分巨大。局部连接不会减少隐藏层的节点数量,减少的是隐藏层和输入层之间的连接数。 1.3.2 参数共享 1.3.2 参数共享 本质上隐藏层的一个节点与输入层一个节点的连接,对应的就是一个连接参数,大量的连接也就意味着大量的参数需要计算,仅依靠局部连接技术是无法进一步减少计算量的,于是人们又提出了参数共享。所谓的参数共享是基于这样一个假设,一部分图像的统计特性与完整图像的相同。回到上面的例子,每个隐藏层的节点只与输入层的100个节点连接,这样每个隐藏层节点就具有100个参数,全部隐藏层就具有1000000x100=108个参数,使用参数共享后,每个隐藏层的节点都具有完全相同的参数,全部隐藏层就只有100个参数需要计算了,这大大减少了计算量,而且即使处理更大的图像,只要单一隐藏层节点与输入层连接的个数不变,全部隐藏层的参数个数也不会变。这种共享参数的机制,可以理解为针对图像的卷积操作。假设如图1-13所示,具有一个4x4大小的图像和一个大小为2x2的卷积核。 大小为2x2的卷积核对原始图像第1块大小为2x2的图像进行卷积操作,得到卷积结果为2,如图1-14所示。 图1-13 原始图像与卷积核 图1-14 卷积核对原始图像第1个2x2的块进行处理 大小为2x2的卷积核对原始图像第2块大小为2x2的图像进行卷积操作,得到卷积结果为1,如图1-15所示。 图1-15 卷积核对原始图像第2个2x2的块进行处理 如果大小为2x2的卷积核依次对原始图像进行卷积操作,移动的步长为2,那么最终将获得一个2x2的新图像,如图1-16所示。 图1-16 卷积核对原始图像处理后的结果 可见卷积处理后图像的大小与卷积核的大小无关,仅与步长有关,对应的隐藏层的节点个数也仅与步长有关。另外需要说明的是,卷积核处理图像边际时会出现数据缺失,这个时候需要将图像补全,常见的补全方式有两种,分别是same模式和valid模式,same模式会使用0数据补全,而且保持生成图像与原始图像大小一致。 same模式会使用大小为2x2的卷积核依次对原始图像进行卷积操作,移动的步长为1,最终获得一个4x4的新图像如图1-17所示。这里需要再次强调的是,当步长为1时,无论卷积核大小如何,处理前后图像大小不变;只有当步长大于1时,处理后的图像才会变小。 图1-17 卷积核对原始图像处理后的结果 1.3.3 池化 1.3.3 池化 通过局部连接和参数共享后,我们针对1000x1000的图像,使用卷积核大小为10x10,卷积步长为1,进行卷积操作,得到的隐藏层节点个数为1000x1000=106,计算量还是太大了。为了解决这个问题,首先回忆一下,我们之所以决定使用卷积后的特征是因为图像具有一种“静态性”的属性,这也就意味着在一个图像区域有用的特征极有可能在另一个区域同样适用。因此,为了描述大的图像,一个很自然的想法就是对不同位置的特征进行聚合统计,例如,人们可以计算图像一个区域上的某个特定特征的平均值(或最大值)。这种聚合的操作就叫作池化,常见的池化大小为2x2、3x3等,假设隐藏层节点个数为4x4,使用2x2大小池化,取最大值,过程如图1-18所示。 图1-18 池化处理示例 隐藏层节点个数为1000x1000的神经网络,经过2x2池化后,得到的隐藏层节点个数为500x500。 1.3.4 典型的CNN结构 1.3.4 典型的cnn结构 典型的cnn包含卷积层、全连接层等组件,并采用softmax多类别分类器和多类交叉熵损失函数,一个典型的卷积神经网络如图1-19所示。 图1-19 典型的cnn结构 我们先介绍用来构造cnn的常见组件: ? 卷积层:执行卷积操作提取底层到高层的特征,挖掘出图片局部关联性质和空间不变性质。 ? 池化层:执行降采样操作。通过取卷积输出特征图中局部区块的最大值或者均值来实现。降采样也是图像处理中常见的一种操作,可以过滤掉一些不重要的高频信息。 ? 全连接层:输入层到隐藏层的神经元是全部连接的。 ? 非线性变化:卷积层、全连接层后面一般都会接非线性变化层,例如sigmoid、tanh、relu等来增强网络的表达能力,在cnn里最常使用的为relu激活函数。 ? dropout:在训练阶段随机让一些隐层节点不工作,提高神经网络的泛化能力,一定程度上防止过拟合,这一点就好比人眼在做图像识别时,适当遮住一部分像素不会影响识别结果一样。相对于浅层学习的svm、knn和朴素贝叶斯等,深度学习由于参数众多,更容易出现过拟合的现象,所以一般都需要使用dropout机制。 1.3.5 AlexNet的结构 1.3.5 alexnet的结构 alexnet是2012年发表的经典之作,并在当年取得了imagenet最好成绩,其官方提供的数据模型,准确率达到57.1%,top 1-5达到80.2%。这对于传统的机器学习分类算法而言,已经相当出色了。 alexnet一共由8层组成,其中包括3个卷积层和5个全连接层,其结构如图1-20所示。 图1-20 alexnet结构图 1.3.6 VGG的结构 1.3.6 vgg的结构 牛津大学vgg(visual geometry group)组在2014年ilsvrc提出了vgg模型。该模型相比以往模型进一步加宽和加深了网络结构,它的核心是五组卷积操作,每两组之间做max-pooling空间降维。同一组内采用多次连续的3x3卷积,卷积核的数目由较浅组的64增加至最深组的512,同一组内的卷积核数目是一样的。卷积之后接两层全连接层,之后是分类层。由于每组内卷积层的不同,有11、13、16、19层这几种模型,图1-21展示了一个16层的网络结构。vgg模型结构相对简洁,提出之后也有很多文章基于此模型进行研究,如在imagenet上首次公开的超过人眼识别的模型正是借鉴了vgg模型的结构。 图1-21 vgg结构图 1.3.7 ResNet50 1.3.7 resnet50 深度学习网络的深度对最后的分类和识别的效果有着很大的影响,所以正常想法就是把网络设计得越深越好,但事实并非如此。网络的堆叠在网络很深时,效果却越来越差了。resnet引入了残差网络结构,通过残差网络,可以把网络层设计得很深,据说现在达到了1000多层,最终的网络分类的效果也非常好。resnet在2015年名声大噪,而且影响了2016年深度学习在学术界和工业界的发展方向,resnet50就是resnet的一种,其结构如图1-22所示。 1.3.8 InceptionV3 1.3.8 inceptionv3 一般的卷积层只是一味增加卷积层的深度,但是在单层上卷积核却只有一种,这样特征提取的功能可能就比较弱。google增加单层卷积层的宽度,即在单层卷积层上使用不同尺度的卷积核,他们构建了inception这个基本单元。基本的inception中有1x1卷积核、3x3卷积核、5x5卷积核,还有一个3x3下采样,从而产生了inceptionv1模型,如图1-23所示。inceptionv3的改进是使用了2层3x3的小卷积核替代了5x5卷积核。《web安全之深度学习实战》一书中在识别webshell时,也用到了类似的思路,使用大小分别为3、4和5的一维卷积处理php的opcode序列,效果也非常不错。 1.3.9 可视化CNN 1.3.9 可视化cnn cnn虽然接近人类识别物体的过程,但是理解cnn的原理却是非常艰难的过程。人们试图用可视化的方式来理解cnn各层对图像特征提取的方法。keras之父、谷歌大脑人工智能和深度学习研究员francois chollet在他的《python深度学习》中给出一种可视化卷积的方法。卷积层通常由多个卷积核组成,每个卷积核都可以被视为一种特征提取方式。当使用一个卷积核处理图像数据后,卷积核会提取它关注的特征并形成新的图像,该图像也被称为特征图。输入的图像包含的特征与卷积核越接近,其特征图的值也越大。因此完全可以基于梯度,迭代调整输入图像的值,让特征图的值最大化。当特征图的值达到最大或者迭代求解趋于稳定时,可以认为这时的输入图像就是该卷积核的可视化图像。下面以keras为例介绍核心代码实现。 #获取输出层的tensor layer_output = model.get_layer(layer_name).output #获取指定卷积核filter_index的输出,并作为损失函数 loss = k.mean(layer_output[:, :, :, filter_index]) #根据损失函数和输入层定义梯度 grads = k.gradients(loss, model.input)[0] grads /= (k.sqrt(k.mean(k.square(grads))) + 1e-5) #实例化计算梯度的函数 iterate = k.function([model.input], [loss, grads]) #定义一个随机图像,图像底色为灰色,并叠加均值为0标准差为20的高斯噪声 input_img_data = np.random.random((1, size, size, 3)) * 20 + 128. #迭代40轮,使用梯度上升算法求解,学习速率(步长)为step step = 1. for i in range(40): loss_value, grads_value = iterate([input_img_data]) input_img_data += grads_value * step img = input_img_data[0] 图1-22 resnet50结构图 图1-23 inception单元结构 图1-24 keras下的vgg16结构图 以vgg16为例,如图1-24所示,在vgg16中具有多个卷积层,其中最典型的5个分别为block1_conv1、block2_conv1、block3_conv1、block4_conv1和block5_conv1。 francois chollet使用上述方法可视化了这5个卷积层,block1_conv1的可视化结果如图1-25所示,block3_conv1的可视化结果如图1-26所示,block5_conv1的可视化结果如图1-27所示,可见第1层卷积提取的主要是边缘和纹路特征,越往后的卷积提取的特征越高级,到了第5层卷积已经提取很抽象的高级特征了。 图1-25 block1_conv1可视化结果 下面我们以经典的小猪图像为例,展现在vgg16下各个卷积层处理的情况,相应的代码路径为: https://github.com/duoergun0729/adversarial_examples/code/1-case2-keras.ipynb 首先实例化keras下的vgg16模型,加载基于imagenet 2012数据集预训练的参数。 #使用vgg16 from keras.applications.vgg16 import vgg16 import matplotlib.pyplot as plt %matplotlib inline model = vgg16(weights='imagenet') 图1-26 block3_conv1可视化结果 之后加载经典的小猪图片(见图1-28),并进行预处理。 from keras.preprocessing import image from keras.applications.vgg16 import preprocess_input, decode_predictions import numpy as np #小猪的路径 img_path = "../picture/pig.jpg" #缩放到指定大小 224x224 img = image.load_img(img_path, target_size=(224, 224)) #展示图片 plt.imshow(img) plt.show() x = image.img_to_array(img) # 扩展维度,适配模型输入大小 (1, 224, 224, 3) x = np.expand_dims(x, axis=0) # 图像预处理 x = preprocess_input(x) 图1-27 block5_conv1可视化结果 对该图片进行预测,预测结果为小猪,满足预期。 preds = model.predict(x) print('predicted:', decode_predictions(preds, top=3)[0]) predicted: [('n03935335', 'piggy_bank', 0.6222573), ('n02395406', 'hog', 0.3228228), ('n02108915', 'french_bulldog', 0.013370045)] 图1-28 经典的小猪图片 获取block1_conv1、block3_conv1和block5_conv1对应的输出tensor,并基于vgg16定义新的模型,该模型的输入为图像,输出为以上三层的输出tensor。 from keras import models layer_names=['block1_conv1', 'block3_conv1','block5_conv1'] # 获取指定层的输出: layer_outputs = [model.get_layer(layer_name).output for layer_name in layer_names] # 创建新的模型,该模型的输出为指定的层的输出 activation_model = models.model(inputs=model.input, outputs=layer_outputs) 针对小猪图像进行预测,获得指定层的输出结果。 #获得小猪的输出 activations = activation_model.predict(x) 遍历block1_conv1、block3_conv1和block5_conv1的各个卷积核的输出并可视化。这里需要指出的是,卷积核的输出结果的范围并不是固定的,为了可以展示成图片,需要根据对应的均值channel_image.mean()和标准差channel_image.std()进行归一化,然后再转换到图片对应的像素范围[0,255]。为了便于显示,每层以8x8的格式展示前64个卷积核对应的图像。block1_conv1层可视化的结果如图1-29所示,block3_conv1层可视化的结果如图1-30所示,block5_conv1层可视化的结果如图1-31所示。可见第1层卷积层提取特征时尽可能保留了原图的细节,越往后的卷积层提取的特征越高级,保留原始图片的细节越来越少。到了最后几层,出现的空白越来越多,这意味着部分卷积核无法在图像中匹配特定的特征了,这也意味着越高级别的特征对应的矩阵越稀疏。 images_per_row = 8 for layer_name, layer_activation in zip(layer_names, activations): # 获取卷积核的个数 n_features = layer_activation.shape[-1] # 特征图的形状 (1, size, size, n_features) size = layer_activation.shape[1] #最多展现8行 n_cols=8 display_grid = np.zeros((size * n_cols, images_per_row * size)) for col in range(n_cols): for row in range(images_per_row): channel_image = layer_activation[0,:, :, col * images_per_row + row] # 归一化处理 channel_image -= channel_image.mean() channel_image /= channel_image.std() # 数据扩展到[0,255]范围 channel_image *= 128 channel_image += 128 channel_image = np.clip(channel_image, 0, 255).astype('uint8') display_grid[col * size : (col + 1) * size, row * size : (row + 1) * size] = channel_image # 展示图片 scale = 1. / size plt.figure(figsize=(scale * display_grid.shape[1], scale * display_grid.shape[0])) plt.title(layer_name) plt.grid(false) plt.imshow(display_grid, aspect='auto', cmap='viridis') plt.show() 图1-29 小猪图片在block1_conv1层的可视化结果 图1-30 小猪图片在block3_conv1层的可视化结果 图1-31 小猪图片在block5_conv1层的可视化结果 1.4.1 测试数据 1.4.1 测试数据 我们以scikit-learn环境介绍常见的性能衡量指标。为了便于演示,我们创建测试数据,测试数据一共有1000条记录,每条记录了100个特征,内容随机生成: x, y = datasets.make_classification(n_samples=1000, n_features=100, n_redundant=0, random_state = 1) 把数据集随机划分成训练集和测试集,其中测试集占40%: train_x, test_x, train_y, test_y = train_test_split(x, y, test_size=0.2, random_state=66) 使用knn算法进行训练和预测: knn = kneighborsclassifier(n_neighbors=5) knn.fit(train_x, train_y) pred_y = knn.predict(test_x) 1.4.2 混淆矩阵 1.4.2 混淆矩阵 混淆矩阵(confusion matrix)是一个将分类问题按照真实情况与判别情况两个维度进行归类的矩阵,在二分类问题中,可以用一个2x2的矩阵表示。如图1-32所示,tp表示实际为真预测为真,tn表示实际为假预测为假,fn表示实际为真预测为假,通俗讲就是漏报了,fp表示实际为假预测为真,通俗讲就是误报了。 图1-32 二分类问题的混淆矩阵 在scikit-learn中,使用metrics.confusion_matrix输出混淆矩阵。 print "confusion_matrix:" print metrics.confusion_matrix(test_y, pred_y) 输出结果如下,其中漏报36个,误报了25个。 confusion_matrix: [[70 25] [36 69]] 1.4.3 准确率与召回率 1.4.3 准确率与召回率 机器学习中最基本的指标是召回率(recall rate)和准确率(precision rate),召回率也叫查全率,准确率也叫查准率。 召回率=tp/(tp+fn) 准确率=tp/(tp+fp) 举例来解释这两个枯燥的概念。一个池塘有10条鱼和20只小龙虾,渔夫撒网打鱼,结果捞上来8条鱼和12只小龙虾,那么准确率为8/(8+12)=40%,召回率为8/10=80%。 在scikit-learn中,可以使用如下代码获得准确率和召回率: print "recall_score:" print metrics.recall_score(test_y, pred_y) print "precision_score:" print metrics.precision_score(test_y, pred_y) 输出结果如下,其中召回率为65.71%,准确率为73.40%: recall_score: 0.657142857143 precision_score: 0.734042553191 1.4.4 准确度与F1-Score 1.4.4 准确度与f1-score 准确度(accuracy)是对检测结果的一个均衡评价,表现的是全体预测正确的样本占全部样本的比例,它的定义如下: f1-score也是对准确率和召回率的一个均衡评价,国内外不少数据挖掘比赛都是重点关注f1-score的值,它的定义如下: 在scikit-learn中,可以使用如下代码获得准确度和f1-score: print "accuracy_score:" print metrics.accuracy_score(test_y, pred_y) print "f1_score:" print metrics.f1_score(test_y, pred_y) 输出结果如下,其中准确度为69.5%和f1-score为69.34%: accuracy_score: 0.695 f1_score: 0.693467336683 1.4.5 ROC与AUC 1.4.5 roc与auc roc(receiver operating characteristic,受试者工作特征)曲线是以真阳性率为纵坐标、假阳性率为横坐标绘制的曲线。它是反映灵敏性和特效性连续变量的综合指标。一般认为roc越光滑说明分类算法过拟合的概率越低,越接近左上角说明分类性能越好。auc(area under the receiver operating characteristic)曲线就是量化衡量roc分类性能的指标,如图1-33所示,物理含义是roc曲线的面积,auc越大越好。 图1-33 roc曲线示例 绘制roc曲线的方法如下: f_pos, t_pos, thresh = metrics.roc_curve(test_y, pred_y) auc_area = metrics.auc(f_pos, t_pos) plt.plot(f_pos, t_pos, 'darkorange', lw=2, label='auc = %.2f' % auc_area) plt.legend(loc='lower right') plt.plot([0, 1], [0, 1], color='navy', linestyle='--') plt.title('roc') plt.ylabel('true pos rate') plt.xlabel('false pos rate') plt.show() 在scikit-learn中,可以使用如下代码获得auc值: print "auc:" print metrics.roc_auc_score(test_y, pred_y) 计算获得的auc值为0.70: auc: 0.696992481203 1.5.1 Boosting算法 1.5.1 boosting算法 boosting系列算法的原理是在训练集用初始权重训练出一个分类器,根据分类器的表现来更新训练样本的权重,使得这些错误率高的样本在后面的训练中得到更多的重视。如此重复进行,直到分类器的数量达到事先指定的数目,最终将全部分类器通过集合策略进行整合,得到新的分类器。 boosting系列算法里最著名的算法主要有adaboost算法和梯度提升决策树gbdt(gradient boosting decision tree)算法,我们以adaboost和gbdt为例,介绍如何在scikit-learn中使用它们。 以adaboost为例,数据集使用随机生成的数据,使用adaboostclassifier,分类器个数设置为100: x, y = datasets.make_classification(n_samples=1000, n_features=100,n_redundant=0, random_state = 1) train_x, test_x, train_y, test_y = train_test_split(x, y, test_size=0.2, random_state=66) clf = adaboostclassifier(n_estimators=100) clf.fit(train_x, train_y) pred_y = clf.predict(test_x) 输出对应的性能指标,准确度为80.5%,f1为81.52%,准确率为81.13%,召回率为81.90%,auc为0.80: accuracy_score: 0.805 f1_score: 0.815165876777 recall_score: 0.819047619048 precision_score: 0.811320754717 confusion_matrix: [[75 20] [19 86]] auc: 0.804260651629 对应的roc曲线如图1-35所示,综合指标都优于之前的knn。 图1-35 adaboost的roc曲线 以gbdt为例,数据集依然使用随机生成的数据,使用gradientboostingclassifier,分类器个数设置为100: x, y = datasets.make_classification(n_samples=1000, n_features=100,n_redundant=0, random_state = 1) train_x, test_x, train_y, test_y = train_test_split(x, y, test_size=0.2, random_state=66) clf = gradientboostingclassifier(n_estimators=100) clf.fit(train_x, train_y) pred_y = clf.predict(test_x) report(test_y, pred_y) 输出对应的性能指标,准确度为84%,f1为84.76%,准确率为84.76%,召回率为84.76%,auc为0.84: accuracy_score: 0.84 f1_score: 0.847619047619 recall_score: 0.847619047619 precision_score: 0.847619047619 confusion_matrix: [[79 16] [16 89]] auc: 0.839598997494 对应的roc曲线如图1-36所示,综合指标优于之前的knn,也略优于adaboost,不过boosting系列算法都有大量参数可以优化,对性能有一定影响,本章的这个比较只是一个不太严谨的对比。 图1-36 gbdt的roc曲线 1.5.2 Bagging算法 1.5.2 bagging算法 与boosting算法不同,bagging算法的分类器之间没有依赖关系,可以并行生成。bagging使用自助采样法,即对于m个样本的原始训练集,我们每次先随机采集一个样本放入采样集,接着把该样本放回,也就是说下次采样时该样本仍有可能被采集到,这样采集m次,最终可以得到m个样本的采样集。由于是随机采样,每次的采样集不同于原始训练集和其他采样集,这样得到了多个不同的分类器。 下面举个例子,数据集使用随机生成的数据,使用baggingclassifier,分类器个数设置为100: x, y = datasets.make_classification(n_samples=1000, n_features=100,n_redundant=0, random_state = 1) train_x, test_x, train_y, test_y = train_test_split(x, y, test_size=0.2, random_state=66) clf = baggingclassifier(n_estimators=100) clf.fit(train_x, train_y) pred_y = clf.predict(test_x) report(test_y, pred_y) 输出对应的性能指标,准确度为83.5%,f1为84.21%,准确率为84.61%,召回率为83.81%,auc为0.83: accuracy_score: 0.835 f1_score: 0.842105263158 recall_score: 0.838095238095 precision_score: 0.846153846154 confusion_matrix: [[79 16] [17 88]] auc: 0.834837092732 对应的roc曲线如图1-37所示,综合指标优于之前的knn,也略优于adaboost。 图1-37 bagging的roc曲线 1.6 本章小结 1.6 本章小结 本章介绍了深度学习的训练过程,并结合一个实际的例子介绍了数据预处理、定义网络结构、损失函数、反向传递与优化器的知识,重点介绍了链式法则与梯度的使用。本章最后还结合具体例子介绍了几个常见的衡量指标,包括混淆矩阵、准确率、召回率、准确度、f1-score、roc和auc。另外本章还介绍了博采众家之长的集成学习,主要介绍了boosting算法和bagging算法。 2.1 Anaconda 2.1 anaconda python是深度学习领域的网红,几乎所有的深度学习框架都支持python,甚至仅支持python。python在给开发者带来各种便利的同时,其复杂的包管理与环境管理也一直困扰着开发者。本书推荐使用anaconda进行python环境的搭建,大量实践证明anaconda具有工业级的稳定性,同时使用便捷,下面我们详细介绍anaconda。 anaconda是一个用于科学计算的python开发平台,支持linux、mac和windows系统,提供了包管理与环境管理的功能,可以很方便地解决多版本python并存、切换以及各种第三方包安装问题。anaconda利用conda命令来进行包和环境的管理,并且已经包含了python和相关的配套工具。如图2-2所示,anaconda集成了大量的机器学习库以及数据处理必不可少的第三方库,比如numpy、scipy、scikit-learn以及tensorflow等。 图2-2 anaconda框架 anaconda的安装非常方便,如图2-3所示,从其官网的下载页面选择对应的安装包,以我的mac本为例,选择macos对应的图形化安装版本。 图2-3 anaconda下载页面 点击安装包,选择安装的硬盘,通常mac本也只有一块硬盘,使用默认安装即可,如图2-4所示。 图2-4 anaconda安装界面 使用默认配置进行安装,安装完成后出现如图2-5所示的界面。 使用如下命令查看当前用户的profile文件的内容: cat ~/.bash_profile 图2-5 anaconda安装结束界面 我们可以发现,在当前用户的profile文件的最后增加了如下内容: # added by anaconda2 5.0.0 installer export path="/anaconda2/bin:$path" 表明已经将anaconda的bin目录下的命令添加到了path变量中,可以像使用系统命令一样直接使用anaconda的命令行工具了。 anaconda强大的包管理以及多种python环境并存使用主要依赖于conda命令,常用的conda命令如下: # 创建一个名为python27的环境,指定python版本是2.7 conda create --name python27 python=2.7 # 查看当前环境下已安装的包 conda list # 查看某个指定环境的已安装包 conda list -n python27 # 查找package信息 conda search numpy # 安装package conda install -n python27 numpy # 更新package conda update -n python27 numpy # 删除package conda remove -n python27 numpy 假设我们已经创建一个名为python27的环境,指定python版本是2.7,激活该环境的方法如下: source activate python27 如果要退出该环境,命令如下所示: source deactivate 在python27的环境下查看python版本,果然是2.7版本: maidou:3book liu.yan$ source activate python27 (python27) maidou:5book liu.yan$ (python27) maidou:5book liu.yan$ python python 2.7.14 |anaconda, inc.| (default, oct 5 2017, 02:28:52) [gcc 4.2.1 compatible clang 4.0.1 (tags/release_401/final)] on darwin type "help", "copyright", "credits" or "license" for more information. >>> 查看python27环境下默认安装了哪些包,为了避免显示内容过多,过滤前6行查看: conda list | head -6 # packages in environment at /anaconda2/envs/python27: # ca-certificates 2017.08.26 ha1e5d58_0 certifi 2017.7.27.1 py27h482ffc0_0 libcxx 4.0.1 h579ed51_0 libcxxabi 4.0.1 hebd6815_0 统计包的个数,除去2行的无关内容,当前环境下有16个包: conda list | wc -l 18 查看目前一共有几个环境,发现除了系统默认的root环境,又多出了我们创建的python27环境: conda info --envs # conda environments: # python27 /anaconda2/envs/python27 root * /anaconda2 在python27环境下安装anaconda默认的全部安装包,整个安装过程会比较漫长,速度取决于你的网速: conda install anaconda fetching package metadata ........... solving package specifications: . package plan for installation in environment /anaconda2/envs/python27: 继续统计包的个数,除去两行的无关内容,当前环境下已经有238个包了: conda list | wc -l 240 anaconda默认安装的第三方包里没有包含tensorflow和keras,需要使用命令手工安装。以tensorflow为例,可以使用conda命令直接安装: conda install tensorflow 同时,也可以使用pip命令直接安装: pip install tensorflow 本书一共创建了两个环境,分别是python27和python36,顾名思义,对应的python版本分别为2.7和3.6,用于满足不同案例对python版本的不同要求。 2.2 APT更新源 2.2 apt更新源 如果读者使用的是ubuntu系统,会经常需要使用apt工具进行软件管理。apt工具默认的镜像源速度极其缓慢,强烈建议使用国内的镜像源。推荐的配置方式为,编辑/etc/apt/sources-list,添加如下内容即可: deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ xenial main restricted deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ xenial-updates main restricted deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ xenial universe deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ xenial-updates universe deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ xenial multiverse deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ xenial-updates multiverse deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ xenial-backports main restricted universe multiverse deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ xenial-security main restricted deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ xenial-security universe deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ xenial-security multiverse 2.3 Python更新源 2.3 python更新源 与apt类似,python的包安装与更新主要使用pip工具,推荐的pip更新源配置方式为,在用户目录下创建.pip文件夹,并创建pip.conf文件: [global] trusted-host = pypi.mirrors.ustc.edu.cn index-url = https://pypi.mirrors.ustc.edu.cn/simple 2.4 Jupyter notebook 2.4 jupyter notebook jupyter notebook源自fernando perez发起的ipython项目。ipython是一种交互式shell,与普通的python shell相似,但具有一些很好的功能,例如语法高亮显示和代码补全。jupyter notebook可以在任何地方运行notebook服务器,并且可通过互联网访问服务器。通常,你会在存储所有数据和notebook文件的自有计算机上运行jupyter notebook服务。jupyter notebook部署方便,使用简单,广受深度学习开发和研究者的喜爱,本书的示例代码默认都使用jupyter notebook编写。直接使用pip安装jupyter notebook即可。 pip install jupyter 启动jupyter notebook服务的方式为: jupyter notebook jupyter notebook的整个操作过程都可以通过web界面进行(见图2-6),启动jupyter notebook后,会调起系统默认的浏览器进行访问,默认访问地址为: https://localhost:8888/tree 图2-6 jupyter notebook界面 若要创建一个新的notebook,只需鼠标单击new,在下拉选项中选择一个你想启动的notebook类型即可,如图2-7所示。 在jupyter notebook中编写python,如图2-8所示,需要选中指定的行,使用code模式,编写完成后,按下shift+enter组合键即可运行。 jupyter notebook支持通过markdown语法编写注释等内容,如图2-9所示,只要选中指定的行使用markdown语法,编写完成后,按下shift+enter组合键即可运行。 jupyter notebook的代码补齐功能非常强大,不过需要单独安装插件,首先安装nbextensions: pip install jupyter_contrib_nbextensions -i https://pypi.mirrors.ustc.edu.cn/simple jupyter contrib nbextension install --user 图2-7 jupyter notebook创建新文件的界面 图2-8 在jupyter notebook中编写python 图2-9 在jupyter notebook中编写markdown 然后安装nbextensions_configurator: pip install --user jupyter_nbextensions_configurator jupyter nbextensions_configurator enable –user 最后启动jupyter notebook,如图2-10所示,进入jupyter notebook主页,然后在如图2-11所示的nbextensions配置页勾选hinterland,使能代码自动补齐功能。 在jupyter notebook中使用anaconda中的环境需要单独配置,默认情况下使用的是系统默认的python环境,以使用advbox环境为例。 首先在默认系统环境下执行以下命令,安装ipykernel: conda install ipykernel conda install -n advbox ipykernel 图2-10 jupyter notebook主页 图2-11 nbextensions配置页面 在advbox环境下激活,这样启动后就可以在界面上看到advbox了(参见图2-12): python -m ipykernel install --user --name advbox --display-name advbox 图2-12 advbox环境激活成功 2.5 TensorFlow 2.5 tensorflow tensorflow是谷歌的第二代人工智能学习系统,其命名来源于本身的运行原理。tensor意味着n维数组,flow意味着基于数据流图的计算,tensorflow为tensor从流图的一端流动到另一端的计算过程。所以也可以把tensorflow当作将复杂的数据结构传输至人工智能神经网中进行分析和处理过程的系统。 tensorflow可被用于语音识别或图像识别等多项机器深度学习领域,基于2011年开发的深度学习基础架构distbelief进行了全面改进,它可在小到一部智能手机、大到数千台数据中心服务器的各种设备上运行。 tensorflow支持多种安装方式。 1. ubuntu/linux # 仅使用 cpu 的版本 $ pip install tensorflow # 开启 gpu 支持的版本 (安装该版本的前提是已经安装了 cuda sdk) $ pip install tensorflow-gpu 2. mac os x 在mac os x系统上,我们推荐先安装homebrew,然后执行brew install python,以便能够使用homebrew中的python安装tensorflow。 pip install tensorflow 3. 基于docker的安装 如下命令将启动一个已经安装好tensorflow及相关依赖的容器。 $ docker run -it b.gcr.io/tensorflow/tensorflow 4. 基于virtualenv的安装 官方文档推荐使用virtualenv创建一个隔离的容器来安装tensorflow,这是可选的,但是这样做能使排查安装问题变得更容易。virtualenv通过创建独立python开发环境的工具,来解决依赖、版本以及间接权限问题,比如一个项目依赖django1.3而当前全局开发环境为django1.7,版本跨度过大,导致不兼容使项目无法正在运行,使用virtualenv就可以解决这些问题。 首先,安装所有必备工具: #在linux上: $ sudo apt-get install python-pip python-dev python-virtualenv # 在 mac 上: # 如果还没有安装 pip $ sudo easy_install pip $ sudo pip install --upgrade virtualenv 接下来,建立一个全新的virtualenv环境,为了将环境建在~/tensorflow目录下,执行如下代码: $ virtualenv --system-site-packages ~/tensorflow $ cd ~/tensorflow 然后,激活virtualenv: $ source bin/activate # 如果使用 bash $ source bin/activate.csh # 如果使用 csh (tensorflow)$ # 终端提示符应该发生变化 在virtualenv内,安装tensorflow: (tensorflow)$ pip install --upgrade <$url_to_binary.whl> 接下来,使用类似命令运行tensorflow程序: (tensorflow)$ cd tensorflow/models/image/mnist (tensorflow)$ python convolutional.py # 当使用完 tensorflow (tensorflow)$ deactivate 2.6 Keras 2.6 keras keras是一个高级别的python神经网络框架,能在tensorflow或者theano上运行。keras的作者、谷歌ai研究员francois chollet宣布了一条激动人心的消息,keras将会成为第一个被添加到tensorflow核心中的高级别框架,这将会让keras变成tensorflow的默认api。 keras的主要特点是: ? 可以快速简单地设计出原型。 ? 同时支持卷积网络和循环网络,以及两者的组合。 ? 支持任意的连接方案。 keras的在线文档内容非常丰富,地址为: https://keras.io/ keras的安装非常简便,使用pip工具即可: pip install keras 如果需要使用源码安装,可以直接从github上下载对应源码: https://github.com/fchollet/keras 然后进入keras目录安装即可: python setup.py install 2.7 PyTorch 2.7 pytorch pytorch是由facebook的ai研究团队发布的一个基于python的科学计算包,旨在服务两类场合: ? 替代numpy发挥gpu潜能。 ? 一个提供了高度灵活性和效率的深度学习实验性平台。 pytorch的安装方式很有特色,登录pytorch的主页https://pytorch.org/。 如图2-13所示,根据你的软硬件环境进行选择,主页上将显示对应的安装命令。 图2-13 pytorch的主页 2.8 PaddlePaddle 2.8 paddlepaddle paddlepaddle是百度提供的开源深度学习框架,它能够让开发者和企业安全、快速地实现自己的ai想法。paddlepaddle最简化的安装可以直接使用pip工具: pip install paddlepaddle 如果有特殊需求希望指定版本进行安装,可以使用参数: pip install paddlepaddle==0.12.0 如果希望使用gpu加速训练过程,可以安装gpu版本: pip install paddlepaddle-gpu 需要特别指出的是,paddlepaddle-gpu针对不同的cudnn和cuda具有不同的编译版本。以百度云上的gpu服务器为例,cuda为8.0.61,cudnn为5.0.21,对应的编译版本为paddlepaddle-gpu==0.14.0.post85。 pip install paddlepaddle-gpu==0.14.0.post85 2.9 AdvBox 2.9 advbox advbox是一款由百度安全实验室研发,在百度大范围使用的ai模型安全工具箱,目前原生支持paddlepaddle、pytorch、caffe2、mxnet、keras以及tensorflow平台,便于广大开发者和安全工程师使用自己熟悉的框架。advbox同时支持graphpipe,屏蔽了底层使用的深度学习平台,用户可以零编码,仅通过几个命令就可以对paddlepaddle、pytorch、caffe2、mxnet、cntk、scikitlearn以及tensorflow平台生成的模型文件进行黑盒攻击。笔者也是advbox的主要贡献者。 advbox的安装方式非常简单,直接从github上同步即可: git clone https://github.com/baidu/advbox.git 类似的框架还有以下几个: https://github.com/bethgelab/foolbox https://github.com/ibm/adversarial-robustness-toolbox https://github.com/topics/cleverhans 2.10 GPU服务器 2.10 gpu服务器 本书中会大量使用cnn、lstm和mlp,这些计算量都非常巨大,尤其是cnn几乎就是cpu杀手。 目前在深度学习领域,主流的商用gpu型号是nvidia tesla系列k40、m40以及m60,我们将对比这三款产品的关键性能参数,官方的参数对比如下: ? m60拥有两个gm204核芯,每个gm204核芯拥有2048个计算单元,拥有8g显存,单精度浮点性能可达4.85tflops。 ? m40拥有一个gm200核芯,该核芯拥有3072个计算单元,拥有12g显存,单精度浮点性能可达7tflops。 ? k40拥有一个gk110核芯,该核芯拥有2880个计算单元,拥有12g显存,单精度浮点性能可达4.29tflops。 一个m40的计算能力约为一个m60云主机的1.44倍,但是价格却超过m60的2倍;而k40云主机的计算能力不如m60,却比m60贵,所以从计算能力来讲,m60性价比最高。 这里我介绍如何使用某公有云上的m60 gpu服务器,强烈建议在验证阶段使用按需付费的gpu服务器,最好是按小时计费,这种比较划算。 1. 选择主机 根据需要选择服务器cpu、内存和硬盘等配置,最关键还要选择gpu,通常tesla m60足够我们使用了,如图2-14所示。 图2-14 选择主机 2. 其他设置 设置服务器名称以及登录密码,如图2-15所示。 图2-15 其他设置 3. 服务器概况 服务器安装完成后,界面显示使用了一块gpu tesla m60,如图2-16和图2-17所示。 图2-16 服务器概况(一) 图2-17 服务器概况(二) 4. 运行测试程序 我们在gpu服务器上运行经典的使用cnn识别mnist的例子,这个例子在我的mac本上训练12轮需要花费将近2个小时。我们发现程序运行时加载了cuda,它是在gpu上运行深度学习算法的基础: [root@keras001 ~]# python keras-demo.py using tensorflow backend. i tensorflow/stream_executor/dso_loader.cc:135] successfully opened cuda library libcublas.so.8.0 locally i tensorflow/stream_executor/dso_loader.cc:135] successfully opened cuda library libcudnn.so.5 locally i tensorflow/stream_executor/dso_loader.cc:135] successfully opened cuda library libcufft.so.8.0 locally i tensorflow/stream_executor/dso_loader.cc:135] successfully opened cuda library libcuda.so.1 locally i tensorflow/stream_executor/dso_loader.cc:135] successfully opened cuda library libcurand.so.8.0 locally 然后我们继续观察,发现程序提示信息显示,加载了gpu tesla m60,内存约8g: i tensorflow/core/common_runtime/gpu/gpu_device.cc:885] found device 0 with properties: name: tesla m60 major: 5 minor: 2 memoryclockrate (ghz) 1.1775 pcibusid 0000:00:15.0 total memory: 7.93gib free memory: 7.86gib i tensorflow/core/common_runtime/gpu/gpu_device.cc:906] dma: 0 i tensorflow/core/common_runtime/gpu/gpu_device.cc:916] 0: y 运行完整的程序大约需要3分钟,此速度完胜了我的mac本。 5. 手工安装深度学习库 有时候需要根据软硬件环境自己选择安装对应的深度学习库,其中最重要的是看cudnn和cuda的版本,查看服务器的cudnn和cuda版本的方法为: #cuda 版本 cat /usr/local/cuda/version.txt #cudnn 版本 cat /usr/local/cuda/include/cudnn.h | grep cudnn_major -a 2 #或者 cat /usr/include/cudnn.h | grep cudnn_major -a 2 2.11 本章小结 2.11 本章小结 本章介绍了搭建对抗样本工具箱的过程,包括如何安装anaconda,设置apt更新源、python更新源,以及如何安装jupyter notebook和常见的深度学习框架。最后还介绍了主流的对抗样本框架以及如何在云环境使用gpu服务器。 3.1 张量与计算图 3.1 张量与计算图 在python编程中,我们经常使用numpy表示向量(或者称为多维数组),在深度学习框架中使用了张量(tensor)这个概念,表示了在计算图中流动的数据。计算图又被称为数据流图,是深度学习框架中非常重要的一个概念。开发者通过计算图定义整个模型的网络结构以及对应的优化器和损失函数。数据流图用节点(node)和边(edge)的有向图来描述数学计算。节点一般用来表示施加的数学操作,但也可以表示数据输入的起点以及输出的终点。边表示节点之间的输入/输出关系。举个例子,假设存在以下计算关系: d=(a+b)*c 可见整个计算图的输入是节点a、b和c,输出是d,如图3-1所示。 图3-1 计算图示例(1) 定义好了计算图后,并不会真正发生计算,只有当往输入节点灌入数据(feed in)时,张量才会在计算图中按照定义的方向发生流动,计算才会发生。如图3-2所示,分别向输入节点a、b和c输入1、2和3,经过计算图运算后,得到结果9。 图3-2 计算图示例(2) 以tensorflow为例,介绍如何实现上面介绍的计算图,对应的代码路径为: https://github.com/duoergun0729/adversarial_examples/blob/master/code/2-1.ipynb 首先定义对应的输入节点a、b和c,限制为整数类型tf.int64,其中tf.placeholder表明其为输入节点。 import tensorflow as tf a = tf.placeholder(tf.int64) b = tf.placeholder(tf.int64) c = tf.placeholder(tf.int64) 然后定义对应的计算过程,即最终输出d是如何计算获得的。 d = (a+b)*c 最后输入对应的值,运行计算图并获取对应的结果。 with tf.session() as sess: print sess.run(d,feed_dict = {a:1,b:2,c:3}) 9 如果在jupyter中运行程序显示对应的库没有安装,很大可能是对应的jupyter核心没有设置,无法使用对应的anaconda环境。一个推荐的做法是配置你经常使用的anaconda环境到ipython核心。以本书为例,使用的环境名称为book5,进入jupyter核心对应的配置路径,比如: /users/maidou/anaconda2/share/jupyter/kernels/ 使用你的环境名建立目录,便于区分。 /users/maidou/anaconda2/share/jupyter/kernels/book5 在该目录下建立配置文件kernel.json,内容如下所示,其中只需替换你的环境中python可执行文件路径即可。 { "display_name": "book5", "language": "python", "argv": [ "/users/maidou/anaconda2/envs/book5/bin/python", "-m", "ipykernel_launcher", "-f", "{connection_file}" ] } 最后执行命令使得对应配置生效,以后执行jupyter时选择核心为book5即可。 conda install -n book5 ipykernel 3.2 TensorFlow 3.2 tensorflow tensorflow是被工业界和学术界使用最广泛的深度学习框架之一。我们以解决经典的手写数字识别的问题为例,介绍tensorflow的基本使用方法,代码路径为: https://github.com/duoergun0729/adversarial_examples/blob/master/code/2-tensorflow.ipynb 1. 加载相关库 加载处理经典的手写数字识别的问题相关的python库。 import tensorflow as tf from tensorflow.examples.tutorials.mnist import input_data from tensorflow.python.framework import graph_util import os 2. 加载数据集 mnist是一个入门级的计算机视觉数据集,它包含各种手写数字图片如图3-3所示。 它也包含每一张图片对应的标签,告诉我们这个是数字几。比如这四张图片的标签分别是5、0、4、1。数据集包括60000个的训练数据集和10000个的测试数据集,见表3-1。每一个mnist数据单元由两部分组成,即一张包含手写数字的图片和一个对应的标签。 图3-3 mnist图片示例 表3-1 mnist数据集详解 mnist的网址为:http://yann.lecun.com/exdb/mnist/。mnist官网如图3-4所示。 图3-4 mnist官网 mnist默认图像的形状为[28,28,1],为了便于处理,需要改变其形状为一维向量784。另外特征数据需要归一化为0到1,这可以通过除以255来完成。默认的标签的数据类型为整数,取值范围为0到9,为了便于深度学习网络训练,通常会把标签数据转换为独热编码(one hot)。所谓独热编码,又称一位有效编码,其方法是使用n位状态寄存器来对n个状态进行编码,每个状态都有它独立的寄存器位,并且在任意时候,其中只有一位有效。比如标签0到5就可以分别编码为: 000001,000010,000100,001000,010000,100000 3. 定义网络结构 本例中输入层大小为784,隐藏层节点数为300,激活函数为relu,中间为了避免过拟合,使用dropout层,输出层大小为10,激活函数为softmax。为了导出导入pb文件时方便,将输入命名为input,将输出命名为output。网络结构使用工具tensorboard查看,如图3-5所示。 in_units = 784 #输入节点数 h1_units = 300 #隐藏层节点数 #初始化隐藏层权重w1,服从默认均值为0,标准差为0.1的截断正态分布 w1 = tf.variable(tf.truncated_normal([in_units, h1_units], stddev=0.1)) b1 = tf.variable(tf.zeros([h1_units])) #隐藏层偏置b1全部初始化为0 w2 = tf.variable(tf.zeros([h1_units, 10])) b2 = tf.variable(tf.zeros([10])) x = tf.placeholder(tf.float32, [none, in_units],name="input") keep_prob = tf.placeholder(tf.float32,name="keep_prob") #定义模型结构 hidden1 = tf.nn.relu(tf.matmul(x, w1) + b1) hidden1_drop = tf.nn.dropout(hidden1, keep_prob) y = tf.nn.softmax(tf.matmul(hidden1_drop, w2) + b2,name="output") 图3-5 tensorflow处理mnist的网络结构图 4. 定义损失函数和优化器 完成了前向传播的定义,就需要定义损失函数和优化器,便于训练阶段进行反向传递。本例为多分类问题,故使用交叉熵定义损失函数,使用adagrad优化器。 y_ = tf.placeholder(tf.float32, [none, 10]) cross_entropy = tf.reduce_mean(-tf.reduce_sum(y_ * tf.log(y), reduction_indices=[1])) train_step = tf.train.adagradoptimizer(0.3).minimize(cross_entropy) 5. 训练与验证 初始化参数,迭代训练5000轮,每轮训练的批处理大小为100,为了抵御过拟合,每次训练时仅通过75%的数据。每训练200批次,打印中间结果。 sess.run(tf.global_variables_initializer()) correct_prediction = tf.equal(tf.arg_max(y, 1), tf.arg_max(y_, 1)) accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32)) for i in range(5000): batch_xs, batch_ys = mnist.train.next_batch(100) _,loss=sess.run([train_step,cross_entropy],{x: batch_xs, y_: batch_ys, keep_prob: 0.75}) if i % 200 == 0: acc=accuracy.eval(feed_dict={x:mnist.test.images, y_:mnist.test.labels,keep_prob:1}) print("loss={},acc={}".format(loss,acc)) 经过5000轮训练,准确度达到98%。 loss=0.021222606301307678,acc=0.9814000129699707 loss=0.04722728207707405,acc=0.9786999821662903 loss=0.024759886786341667,acc=0.9797000288963318 loss=0.009720790199935436,acc=0.9803000092506409 3.3 Keras 3.3 keras keras本质上还算不上一个深度学习框架,它的底层还是要依赖tensorflow这些深度学习框架,但是相对tensorflow复杂的语法,keras通过封装,提供了一套非常简洁的接口,让熟悉python开发的人可以快速上手。我们以解决经典的手写数字识别的问题为例,介绍keras的基本使用方法,代码路径为: https://github.com/duoergun0729/adversarial_examples/blob/master/code/2-keras.ipynb 1. 加载相关库 加载处理经典的手写数字识别问题相关的python库: import keras from keras.datasets import mnist from keras.models import sequential from keras.layers import dense, dropout from keras.optimizers import rmsprop 2. 加载数据集 keras中针对常见的数据集进行了封装,免去了用户手工下载的过程并简化了预处理的过程。在keras中直接调用to_categorical函数即可完成独热编码的转换: (x_train, y_train), (x_test, y_test) = mnist.load_data() x_train = x_train.reshape(60000, 784) x_test = x_test.reshape(10000, 784) x_train = x_train.astype('float32') x_test = x_test.astype('float32') x_train /= 255 x_test /= 255 y_train = keras.utils.to_categorical(y_train, num_classes) y_test = keras.utils.to_categorical(y_test, num_classes) 3. 定义网络结构 定义网络结构是指根据建模定义前向传播过程。本例中输入层大小为(784),第一层隐藏层节点数为512,激活函数为relu,第二层也是512,激活函数也为relu。中间为了避免过拟合,使用dropout层,随机丢失20%数据,输出层大小为10,激活函数为softmax: model = sequential() model.add(dense(512, activation='relu', input_shape=(784,))) model.add(dropout(0.2)) model.add(dense(512, activation='relu')) model.add(dropout(0.2)) model.add(dense(num_classes, activation='softmax')) model.summary() 最后可视化网络结构,细节如图3-6所示。 图3-6 keras处理mnist的网络结构图 4. 定义损失函数和优化器 完成了前向传播的定义,就需要定义损失函数和优化器,便于训练阶段进行反向传递。本例为多分类问题,故使用categorical_crossentropy定义损失函数,使用rmsprop优化器: model.compile(loss='categorical_crossentropy', optimizer=rmsprop(), metrics=['accuracy']) 5. 训练与验证 为了尽可能提高准确度,让网络中的众多参数得到充分训练,通常深度学习都会在同一数据集上结合dropout进行多轮训练,由于dropout会随机丢失一些特征,相当于增加了新的训练数据。本例中批处理大小为128,训练的轮数为20轮,如果我们观察训练第20轮时损失函数还有下降的趋势,可以适当增加训练轮数。 batch_size = 128 num_classes = 10 epochs = 20 在训练集上进行训练,并使用测试集进行效果验证,keras将这两个过程使用一个api完成,这也正是keras强大的地方,最终我们考核的是accuracy即准确度(预测正确的占总量的比例)。 history = model.fit(x_train, y_train, batch_size=batch_size, epochs=epochs, verbose=1, validation_data=(x_test, y_test)) score = model.evaluate(x_test, y_test, verbose=0) print('test loss:', score[0]) print('test accuracy:', score[1]) 经过20轮训练,在测试集上准确度达到了98.44%: epoch 20/20 60000/60000 [==============================] - 10s 165us/step - loss: 0.0186 - acc: 0.9950 - val_loss: 0.1122 - val_acc: 0.9844 ('test loss:', 0.11221564155901237) ('test accuracy:', 0.9844) 回顾整个过程,keras完全实现了自动化反向传递,屏蔽了大量底层细节,读者完全感觉不到梯度和反向传递的存在。 保存keras的模型十分方便,直接调用save方法即可,保存的格式为hdf5: model.save('models/keras-model.h5') hdf(hierarchical data format)是一种为存储和处理大容量科学数据设计的文件格式及相应库文件。hdf最早由美国国家超级计算应用中心ncsa开发,目前在非盈利组织hdf小组维护下继续发展。当前流行的版本是hdf5。hdf5拥有一系列的优异特性,使其特别适合进行大量科学数据的存储和操作,它支持非常多的数据类型,具有灵活、通用、跨平台、可扩展、高效的i/o性能、支持几乎无限量的单文件存储等特点,详见其官方介绍,网址为https://support.hdfgroup.org/hdf5/。 keras通过hdf5文件把网络结构和对应参数进行了持久化。 3.4 PyTorch 3.4 pytorch pytorch是torch的python版本,是由facebook开源的神经网络框架。pytorch虽然是深度学习框架中的后起之秀,但是发展极其迅猛。pytorch提供了numpy风格的tensor操作,熟悉numpy操作的用户非常容易上手。我们以解决经典的手写数字识别的问题为例,介绍pytorch的基本使用方法,代码路径为: https://github.com/duoergun0729/adversarial_examples/blob/master/code/2-pytorch.ipynb 1. 加载相关库 加载处理经典的手写数字识别问题相关的python库: import os import torch import torchvision from torch.autograd import variable import torch.utils.data.dataloader as data 2. 加载数据集 pytorch中针对常见的数据集进行了封装,免去了用户手工下载的过程并简化了预处理的过程。这里需要特别指出的是,pytorch中每个tensor包括输入节点,并且都可以有自己的梯度值,因此训练数据集要设置为train=true,测试数据集要设置为train=false: train_data = torchvision.datasets.mnist( 'dataset/mnist-pytorch', train=true, transform=torchvision.transforms.totensor(), download=true ) test_data = torchvision.datasets.mnist( 'dataset/mnist-pytorch', train=false, transform=torchvision.transforms.totensor() ) 如果需要对数据进行归一化,可以进一步使用transforms.normalize方法: transform=transforms.compose([torchvision.transforms.totensor(), torchvision.transforms.normalize([0.5], [0.5])]) 第一次运行该程序时,pytorch会从互联网直接下载数据集并处理: downloading http://yann.lecun.com/exdb/mnist/train-images-idx3-ubyte.gz downloading http://yann.lecun.com/exdb/mnist/train-labels-idx1-ubyte.gz downloading http://yann.lecun.com/exdb/mnist/t10k-images-idx3-ubyte.gz downloading http://yann.lecun.com/exdb/mnist/t10k-labels-idx1-ubyte.gz processing... done! 3. 定义网络结构 使用与keras类似的网络结构,即两层隐藏层结构,不过使用batchnorm层替换了dropout层,在抵御过拟合的同时加快了训练的收敛速度。在pytorch中定义网络结构,通常需要继承torch.nn.module类,重点是在forward中完成前向传播的定义,在init中完成主要网络层的定义: class net(torch.nn.module): def __init__(self): super(net, self).__init__() self.dense = torch.nn.sequential( #全连接层 torch.nn.linear(784, 512), #batchnorm层 torch.nn.batchnorm1d(512), torch.nn.relu(), torch.nn.linear(512, 10), torch.nn.relu() ) def forward(self, x): #把输出转换成大小为784的一维向量 x = x.view(-1, 784) x=self.dense(x) return torch.nn.functional.log_softmax(x, dim=1) 最后可视化网络结构,细节如图3-7所示。 图3-7 pytorch处理mnist的网络结构图 4. 定义损失函数和优化器 损失函数使用交叉熵crossentropyloss,优化器使用adam,优化的对象是全部网络参数: optimizer = torch.optim.adam(model.parameters()) loss_func = torch.nn.crossentropyloss() 5. 训练与验证 pytorch的训练和验证过程是分开的,在训练阶段需要把训练数据进行前向传播后,使用损失函数计算训练数据的真实标签与预测标签之间损失值,然后显示调用反向传递backward(),使用优化器来调整参数,这一操作需要调用optimizer.step(): for i, data in enumerate(train_loader): inputs, labels = data inputs, labels = inputs.to(device), labels.to(device) # 梯度清零 optimizer.zero_grad() # 前向传播 outputs = model(inputs) loss = loss_func(outputs, labels) #反向传递 loss.backward() optimizer.step() 每轮训练需要花费较长的时间,为了让训练过程可视化,可以打印训练的中间结果,比如每100个批次打印下平均损失值: # 每训练100个批次打印一次平均损失值 sum_loss += loss.item() if (i+1) % 100 == 0: print('epoch=%d, batch=%d loss: %.04f'% (epoch + 1, i+1, sum_loss / 100)) sum_loss = 0.0 验证阶段要手工关闭反向传递,需要通过torch.no_grad()实现: # 每跑完一次epoch,测试一下准确率进入测试模式,禁止梯度传递 with torch.no_grad(): correct = 0 total = 0 for data in test_loader: images, labels = data images, labels = images.to(device), labels.to(device) outputs = model(images) # 取得分最高的那个类 _, predicted = torch.max(outputs.data, 1) total += labels.size(0) correct += (predicted == labels).sum() print('epoch=%d accuracy=%.02f%%' % (epoch + 1, (100 * correct / total))) 经过20轮训练,在测试集上准确度达到了97.00%: epoch=20, batch=100 loss: 0.0035 epoch=20, batch=200 loss: 0.0049 epoch=20, batch=300 loss: 0.0040 epoch=20, batch=400 loss: 0.0042 epoch=20 accuracy=97.00% pytorch保存的模型文件后缀为pth: torch.save(model.state_dict(), 'models/pytorch-mnist.pth') 3.5 MXNet 3.5 mxnet mxnet是亚马逊开发的深度学习库,它拥有类似于theano和tensorflow的数据流图,并且可以在常见的硬件平台上运行。mxnet还提供了r、c++、scala等语言的接口。我们以解决经典的手写数字识别的问题为例,介绍mxnet的基本使用方法,代码路径为: https://github.com/duoergun0729/adversarial_examples/blob/master/code/2-mxnet.ipynb 1. 加载相关库 加载处理经典的手写数字识别问题相关的python库: import mxnet as mx import logging 2. 加载数据集 mxnet中也针对常见的数据集进行了封装,免去了用户手工下载的过程并简化了预处理的过程: mnist = mx.test_utils.get_mnist() batch_size = 128 train_iter = mx.io.ndarrayiter(mnist['train_data'], mnist['train_label'], batch_size, shuffle=true) val_iter = mx.io.ndarrayiter(mnist['test_data'], mnist['test_label'], batch_size) 3. 定义网络结构 使用与keras几乎完全相同的网络结构,只不过省略了dropout层。 data = mx.sym.var('data') data = mx.sym.flatten(data=data) #全连接 fc1 = mx.sym.fullyconnected(data=data, num_hidden = 512) act1 = mx.sym.activation(data=fc1, act_type="relu") fc2 = mx.sym.fullyconnected(data=act1, num_hidden = 512) act2 = mx.sym.activation(data=fc2, act_type="relu") fc3 = mx.sym.fullyconnected(data=act2, num_hidden=10) # softmax输出 mlp = mx.sym.softmaxoutput(data=fc3, name='softmax') mlp_model = mx.mod.module(symbol=mlp, context=ctx) 可视化网络结构,如图3-8所示,值得一提的是mxnet自带的可视化工具非常便于使用。 import matplotlib.pyplot as plt mx.viz.plot_network(mlp).view() 图3-8 mxnet处理mnist的网络结构图 4. 定义损失函数和优化器 损失函数使用交叉熵crossentropyloss,优化器使用sgd。 5. 训练与验证 mxnet的训练和验证过程是分开的,训练阶段加载优化器的配置,可以指定每训练100个批次,打印中间结果。 mlp_model.fit(train_iter, eval_data=val_iter, optimizer='sgd', optimizer_params={'learning_rate':0.1}, eval_metric='acc', batch_end_callback = mx.callback.speedometer(batch_size, 100), num_epoch=20) 经过20轮的训练后,在测试集上验证准确度。 test_iter = mx.io.ndarrayiter(mnist['test_data'], mnist['test_label'], batch_size) acc = mx.metric.accuracy() mlp_model.score(test_iter, acc) print(acc) 最终在测试集上准确度达到了97.62%。 info:root:epoch[19] train-accuracy=0.996438 info:root:epoch[19] time cost=4.017 info:root:epoch[19] validation-accuracy=0.976167 evalmetric: {'accuracy': 0.9761669303797469} mxnet保存的模型文件后缀为parms。 mlp_model.save_checkpoint('models/mxnet.parms',20) 3.6 使用预训练模型 3.6 使用预训练模型 在对深度学习模型生成对抗样本时,我们会经常使用预训练好的模型。主流的深度学习框架为了方便用户使用,都积极开放了经典模型以供下载。其中最多的还是机器视觉相关的图像分类与目标识别模型,比如: ? resnet50 ? vgg16 ? inceptionv3 下面我们将举例介绍如何使用预训练模型对指定的图片进行分类预测,预测的主角是一头小猪(见图3-9)。 from ipython.display import image,display path = "../picture/pig.jpg" display(image(filename=path)) 1. 使用keras进行图片分类 keras的应用模块application提供了带有预训练权重的keras模型,这些模型可以用来进行预测、特征提取。模型的预训练权重将下载到~/.keras/models/并在载入模型时自动载入。 图3-9 小猪示例图片 加载需要的python库,并对图像进行预处理。使用基于imagenet数据集训练的resnet50模型,图片大小转换成(224,224),由于是彩色图片,事实上输入模型的图片形状为(224,224,3)。 from keras.applications.resnet50 import resnet50 from keras.preprocessing import image from keras.applications.resnet50 import preprocess_input, decode_predictions import numpy as np model = resnet50(weights='imagenet') img_path = path img = image.load_img(img_path, target_size=(224, 224)) x = image.img_to_array(img) x = np.expand_dims(x, axis=0) x = preprocess_input(x) 对图片进行预测,打印top3的预测结果,预测概率最大的是hog,即猪。 preds = model.predict(x) print('predicted:', decode_predictions(preds, top=3)[0]) predicted: [('n02395406', 'hog', 0.98398596), ('n02396427', 'wild_boar', 0.0074134255), ('n03935335', 'piggy_bank', 0.006954492)] 2. 使用pytorch进行图片分类 pytorch通过torchvision库封装了对预训练模型的下载和使用,这些模型可以用来进行预测、特征提取。模型的预训练权重将下载到~/.torch/models/并在载入模型时自动载入。 加载需要的python库,并对图像进行预处理。使用基于imagenet数据集训练的resnet50模型,图片大小转换成(224,224),由于是彩色图片,并且pytorch在处理图片格式时信道放在第一个维度,所以事实上输入模型的图片形状为(3,224,224)。需要特别指出的是,pytorch加载预训练模型后默认是训练模式,所以在进行图片预测时需要手工调用eval方法进入预测模式,关闭反向传递。 import os import numpy as np import torch import torch.nn import torchvision.models as models from torch.autograd import variable import torch.cuda import torchvision.transforms as transforms from pil import image #手工调用eval方法进入预测模式 resnet50=models.resnet50(pretrained=true).eval() img=image.open(path) img=img.resize((224,224)) img = np.array(img).copy().astype(np.float32) 在keras处理图片时,我们没有进行任何标准化的处理,这是因为这一步被keras的keras.applications.resnet50.preprocess_input封装了,而在pytorch中需要我们手工进行标准化。 mean = [0.485, 0.456, 0.406] std = [0.229, 0.224, 0.225] img /= 255.0 img = (img - mean) / std img = img.transpose(2, 0, 1) img=np.expand_dims(img, axis=0) img = variable(torch.from_numpy(img).float()) 对图片进行预测,预测标签索引是341,对应的是hog,即猪。 label=np.argmax(resnet50(img).data.cpu().numpy()) print("label={}".format(label)) label=341 预测的标签与分类物体名称的对应关系可以参考下列内容。 https://s3.amazonaws.com/deep-learning-models/image-models/imagenet_class_index.json 也可以使用pytorch的api完成从分类标签值到物品名称之间的转换。 print('predicted:', decode_predictions(resnet50(img).data.cpu().numpy(), top=3)[0]) predicted: [('n02395406', 'hog', 15.661886), ('n02396427', 'wild_boar', 10.353137), ('n03935335', 'piggy_bank', 10.098382)] 3. 使用mxnet进行图片分类 mxnet通过gluon库封装了对预训练模型的下载和使用,这些模型可以用来进行预测、特征提取。模型的预训练权重将下载到~/.mxnet/models/并在载入模型时自动载入,更多模型可以参考以下链接。 http://mxnet.incubator.apache.org/versions/master/api/python/gluon/model_zoo.html 加载需要的python库,并对图像进行预处理。使用基于imagenet数据集训练的resnet50模型,图片大小转换成(224,224),由于是彩色图片,并且mxnet在处理图片格式时信道放在第一个维度,所以事实上输入模型的图片形状为(3,224,224)。 from mxnet import gluon import mxnet as mx from mxnet.gluon import nn from mxnet import ndarray as nd from mxnet import autograd import numpy as np resnet=mx.gluon.model_zoo.vision.resnet50_v2(pretrained=true) img=image.open(path) img=img.resize((224,224)) img = np.array(img).copy().astype(np.float32) mxnet对图像的预处理与pytorch一样需要手工进行。 mean = [0.485, 0.456, 0.406] std = [0.229, 0.224, 0.225] img /= 255.0 img = (img - mean) / std img = img.transpose(2, 0, 1) 对图片进行预测,预测标签索引是341,对应的是hog,即猪。 img=np.expand_dims(img, axis=0) array = mx.nd.array(img) outputs=resnet(array).asnumpy() label = np.argmax(outputs) print(label) 4. 使用tensorflow进行图片分类 tensorflow的模型多以pb文件形式保存。以inception为例,模型文件为pb格式,其中的classify_image_graph_def.pb文件就是训练好的inception模型,imagenet_synset_to_human_label_map.txt是类别文件。 wget http://download.tensorflow.org/models/image/imagenet/inception-2015-12-05.tgz tar -zxvf inception-2015-12-05.tgz x classify_image_graph_def.pb x cropped_panda.jpg x imagenet_2012_challenge_label_map_proto.pbtxt x imagenet_synset_to_human_label_map.txt x license 图片数据的预处理在inception的计算图中完成。 path = "../picture/pig.jpg" image_data = tf.gfile.fastgfile(path, "rb").read() 加载pb文件,在会话中还原完整的计算图以及网络中的各层参数。 session=tf.session() def create_graph(dirname): with tf.gfile.fastgfile(dirname, 'rb') as f: graph_def = session.graph_def graph_def.parsefromstring(f.read()) _ = tf.import_graph_def(graph_def, name='') create_graph("models/classify_image_graph_def.pb") session.run(tf.global_variables_initializer()) 获取输入节点和输出节点,运行计算图获得结果,预测标签是hog,即猪。 logits=session.graph.get_tensor_by_name('softmax/logits:0') x = session.graph.get_tensor_by_name('decodejpeg/contents:0') predictions=session.run(logits,feed_dict={x:image_data}) predictions = np.squeeze(predictions) top_k = predictions.argsort()[-3:][::-1] for node_id in top_k: human_string = node_lookup.id_to_string(node_id) score = predictions[node_id] print('%s (score = %.5f)' % (human_string, score)) 以下代码展示了一个技巧,可以把pb文件的结构打印出来,有利于识别整个计算图的输入/输出。 tensorlist=[n.name for n in session.graph_def.node] print(tensorlist) tensorflow还提供了大量ckpt格式的预训练模型,也需要手工下载后加载使用。ckpt文件相当于只保存了网络的参数,如果要完整使用,需要自行定义网络的结构。 wget http://download.tensorflow.org/models/resnet_v2_50_2017_04_14.tar.gz tar -zxvf resnet_v2_50_2017_04_14.tar.gz tensorflow提供了大量工具函数便于使用预训练模型,需要单独安装并设置到系统路径。 git clone https://github.com/tensorflow/models/ 通常设置系统路径最简单的方式是在代码里指定,比如: sys.path.append("models/models/research/slim/") tensorflow通过slim定义了大量常见的网络结构,以resnet为例,就在slim/nets/resnet_v2完成了其网络定义。 from nets import resnet_v2 slim对数据预处理以及归一化做了封装,可以直接调用相应的api完成。tensorflow的预训练模型在预测时需要显式指定is_training=false来关闭反向传递。 path = "../picture/pig.jpg" image = tf.image.decode_jpeg(tf.gfile.fastgfile(path,'rb').read(), channels=3) image_size = resnet_v2_50.default_image_size processed_image = inception_preprocessing.preprocess_image(image, image_size, image_size, is_training=false) processed_images = tf.expand_dims(processed_image, 0) 使用tf.train.saver从ckpt文件中加载网络参数值,需要指出的是resnet_v1版本是从caffe转换得来的,resnet_v2版本是google自己训练的,物体类别不是1000而是1001。 with slim.arg_scope(resnet_v2.resnet_arg_scope()): logits, _ = resnet_v2.resnet_v2_50(processed_images, num_classes=1001, is_training=false) probabilities = tf.nn.softmax(logits) saver=tf.train.saver() with tf.session() as sess: saver.restore(sess,'models/resnet_v2_50/resnet_v2_50.ckpt') 然后输入图像数据,运行计算图进行预测。 np_image, probabilities = sess.run([image, probabilities]) probabilities = probabilities[0, 0:] sorted_inds = [i[0] for i in sorted(enumerate(-probabilities), key=lambda x:x[1])] 输出对应的预测结果,预测为猪的概率为99.08%。 probability 99.08% => [hog, pig, grunter, squealer, sus scrofa] probability 0.84% => [piggy bank, penny bank] probability 0.03% => [wild boar, boar, sus scrofa] probability 0.01% => [french bulldog] probability 0.01% => [hippopotamus, hippo, river horse, hippopotamus amphibius] 值得一提的是,slim封装了将标签数据转换成物体名称的操作,省去了手工解析的过程。 names = imagenet.create_readable_names_for_imagenet_labels() for i in range(5): index = sorted_inds[i] print('probability %0.2f%% => [%s]' % (probabilities[index] * 100, names[index])) 3.7 本章小结 3.7 本章小结 读者通过本章可以掌握深度学习框架中的张量和计算图的概念,目前主流的深度学习框架在底层设计上几乎都是基于张量和计算图的。本章具体介绍如何基于tensorflow、keras、pytorch和mxnet来解决经典的手写数字识别的问题,详细介绍了如何构建前向计算过程和使用反向传递过程。本章最后介绍了如何使用预训练模型进行图片的预测。 4.1.1 通道数与像素深度 4.1.1 通道数与像素深度 在介绍图像格式之前,首先介绍图像的通道数和像素深度这两个概念。在计算机中通常使用一组像素表示一个图像,像素的位置对应的就是图像的坐标。对于灰度图像,一个像素使用一个8位的数字就可以表示对应的黑白程度,这里的通道数就是1,像素深度就是8位。对于常见的彩色图像,就需要分别使用rgb三个8位数字表示一个像素的颜色,分别表示该像素对应的红绿蓝颜色程度,这里的通道数就是3,像素深度就是8位。有的彩色图像甚至需要3个24位的数字表示其彩色程度,这时的通道数就是3,像素深度就是24位。部分图像使用bgr的顺序表示颜色程度,与rgb相比只在排列顺序上有所不同。计算机中图像的表示方法如图4-1所示。 深度学习平台在表示图像时也存在着差异:cwh格式中c代表通道维度,w表示宽,h表示高,一个典型的mnist图像,图像的形状为(1,28,28);whc格式中c代表通道维度,w表示宽,h表示高,一个典型的mnist图像,图像的形状为(28,28,1)。 图4-1 计算机中图像的表示方法 以opencv为例介绍图像的通道数和像素深度的概念,对应的代码路径为: https://github.com/duoergun0729/adversarial_examples/blob/master/code/4-demo.ipynb 使用opencv读取图像,由于我们的开发环境是ipython,因此无法直接使用opencv展示图片,需要使用matplotlib帮助展现。opencv默认格式为bgr,因此需要转换成rgb格式后再展现。图4-2为猪的彩色图像。 import cv2 from matplotlib import pyplot as plt img=cv2.imread("../picture/pig.jpg") show_img = cv2.cvtcolor(img, cv2.color_bgr2rgb) plt.imshow(show_img) plt.show() 打印原始图像,形状为(300,300,3) print(img.shape) (300, 300, 3) 图4-2 猪的图像(彩色图像) 通过设置cv2.color_bgr2gray参数,把图像转换成灰度图像来展示,灰度图像见图4-3。 img=cv2.imread("../picture/pig.jpg") show_img=cv2.cvtcolor(img, cv2.color_bgr2gray) plt.imshow(show_img,cmap=plt.cm.gray) plt.show() 图4-3 猪的图像(灰度图像) 打印原始图像,形状为(300,300),通道数为1。 print(img.shape) (300, 300) 还有一类特殊的图像,只有黑色和白色,称为二值图,像素深度是1位(见图4-4)。 img=cv2.imread("../picture/pig.jpg") show_img=cv2.cvtcolor(img, cv2.color_bgr2gray) ret, thresh = cv2.threshold(show_img, 220, 255,cv2.thresh_binary) plt.imshow(thresh,cmap=plt.cm.gray) plt.show() 图4-4 猪的图像(二值图像) 其中核心函数是cv2.threshold,通过它可以把超过阈值的像素点设置为新值,对应的函数原型为: cv2.threshold(src, x, y, methods) 其中各个参数的含义如下: ? src指原图像,该原图像为灰度图。 ? x指用来对像素值进行分类的阈值。 ? y指当像素值高于阈值时应该被赋予的新的像素值。 ? methods指不同的阈值方法,这些方法包括:cv2.thresh_binary、cv2.thresh_binary_inv、cv2.thresh_trunc、cv2.thresh_tozero、cv2.thresh_tozero_inv。 4.1.2 BMP格式 4.1.2 bmp格式 bmp是一种与硬件设备无关的无损压缩的图像文件格式,使用非常广泛。它采用位映射存储格式,除了图像深度可选以外,不进行其他任何压缩,因此bmp文件所占用的空间很大。bmp文件的像素深度可选l、4、8及24。bmp文件存储数据时,图像的扫描方式按照从左到右、从下到上的顺序。 4.1.3 JPEG格式 4.1.3 jpeg格式 jpeg也是最常见的一种图像格式,是一种有损压缩格式,能够将图像压缩在很小的存储空间,图像中重复或不重要的资料会被丢弃,因此容易造成图像数据的损伤。尤其是使用过高的压缩比例,将使最终解压缩后恢复的图像质量明显降低,如果追求高品质图像,不宜采用过高压缩比例。 4.1.4 GIF格式 4.1.4 gif格式 gif文件是一种基于lzw算法的连续色调的无损压缩格式,其压缩率一般在50%左右。gif图像文件的数据是经过压缩的,而且是采用了可变长度等压缩算法。所以gif的图像深度从l到8,即gif最多支持256种色彩的图像。gif格式的另一个特点是可在一个gif文件中存储多幅彩色图像,如果把存于一个文件中的多幅图像数据逐幅读出并显示到屏幕上,就可构成一种最简单的动画。 4.1.5 PNG格式 4.1.5 png格式 png格式是互联网上的最新图像文件格式。png能够提供长度比gif小30%的无损压缩图像文件,同时它支持24位和48位真彩色图像。 4.2.1 仿射变换 4.2.1 仿射变换 仿射变换(affine transformation)是空间直角坐标系的变换,从一个二维坐标变换到另一个二维坐标,如图4-5所示。仿射变换是一个线性变换,它保持了图像的“平行性”和“平直性”,即图像中原来的直线和平行线,变换后仍然保持原来的直线和平行线。仿射变换比较常用的变换有缩放、旋转、平移、剪切和翻转。 图4-5 仿射变换示意图 假设某点变换前的坐标为(x,y),变换后的坐标为(x',y'),那么仿射变换可以表示为: opencv支持仿射变换,读者可以通过warpaffine函数对图像进行指定的变化。 warpaffine(src, m, dsize, flags, bordermode, bordervalue) 其中主要参数的含义如下: ? src:输入变换前图像。 ? m:仿射变换矩阵。 ? dsize:设置输出图像大小。 ? flags:设置插值方式,默认方式为线性插值。 ? bordermode:边界像素模式。 ? bordervalue:边界填充值,默认情况下为0,即填充黑色。 直接填写仿射变换矩阵不是很直观,opencv还提供了getrotationmatrix2d函数用于生成变换矩阵,函数定义如下: getrotationmatrix2d(center, angle, scale) 其中主要参数的含义如下: ? center:表示旋转的中心点。 ? angle:表示旋转的角度。 ? scale:表示图像缩放系数。 下面我们结合实际例子介绍仿射变换。为了突显出图像变化前后的差别,我们会使用matplotlib进行子窗口绘图。在matplotlib中,使用subplot函数可以方便地设置窗口的个数、窗口的排列顺序等。 如图4-6所示,我们希望水平排列三个子窗口,plt.subplot(131)表示一共一行窗口,一行排列三个窗口,在第一个窗口中绘图;plt.subplot(132)表示一共一行窗口,一行排列三个窗口,在第二个窗口中绘图。 import numpy as np import cv2 from matplotlib import pyplot as plt img=cv2.imread("../picture/pig.jpg") img1 = cv2.cvtcolor(img, cv2.color_bgr2rgb) rows,cols,_ = img.shape plt.subplot(131) plt.imshow(img1) plt.subplot(132) plt.imshow(img1) plt.subplot(133) plt.imshow(img1) 图4-6 matplotlib使用举例 同理,如果我们希望垂直排列三个子窗口,plt.subplot(311)表示一共一列窗口,一列排列三个窗口,在第一个窗口中绘图;plt.subplot(312)表示一共一列窗口,一列排列三个窗口,在第二个窗口中绘图。 4.2.2 图像缩放 4.2.2 图像缩放 图像缩放的一个应用场景是为了适应深度学习框架的输入。多数深度学习模型的输入层大小是确定的,比如典型的(224,224,3)和(160,160,3),所以需要根据模型调整图像的大小,使用cv2.resize即可达到效果。 img=cv2.imread("../picture/pig.jpg") img = cv2.cvtcolor(img, cv2.color_bgr2rgb) img_200=cv2.resize(img,(200,200)) 如图4-7所示,图像的大小由(300,300,3)变成了(200,200,3),由于plt.subplot的每个子图大小是固定的,所以显示的时候看着两个图大小是一样的,不过图片显示的分辨率大小已经发生了变化,一个是300,另一个是200。 图4-7 图像缩放示例一 可以通过打印图片大小来进一步确认。 print(img.shape) print(img_200.shape) (300, 300, 3) (200, 200, 3) 图像缩放的另一个应用场景就是图像增强,比如把原图像缩小,但是图像大小不变,剩下的空间使用黑色充填。对于图像分类模型,图像缩小或者放大,对应的物理意义是摄像头或者相机和物体的距离变远或者变近,这不应该改变物体识别的结果,但是可以增加训练数据量。opencv提供了一个非常强大的api用于生成图像变换的矩阵。 matrix = cv2.getrotationmatrix2d((a,b),c,d) 其中各个参数的含义为: ? a:旋转中心的横坐标。 ? b:旋转中心的纵坐标。 ? c:旋转的角度。 ? d:缩放的比例。 如图4-8所示,把图像缩小到原来的一半,缩放的中心是原图像的中心,多余区域使用黑色填充。 img=cv2.imread("../picture/pig.jpg") img1 = cv2.cvtcolor(img, cv2.color_bgr2rgb) rows,cols,_ = img.shape matrix = cv2.getrotationmatrix2d((cols/2,rows/2),0,0.5) img2 = cv2.warpaffine(img1,matrix,(cols,rows)) plt.subplot(121) plt.imshow(img1) plt.subplot(122) plt.imshow(img2) plt.show() 图4-8 图像缩放示例二 4.2.3 图像旋转 4.2.3 图像旋转 图像旋转的物理意义,可以理解为摄像头或者相机和物体的夹角发生了变化。 img=cv2.imread("../picture/pig.jpg") img1 = cv2.cvtcolor(img, cv2.color_bgr2rgb) rows,cols,_ = img.shape matrix = cv2.getrotationmatrix2d((cols/2,rows/2),90,1) img2 = cv2.warpaffine(img1,matrix,(cols,rows)) matrix = cv2.getrotationmatrix2d((cols/2,rows/2),180,1) img3 = cv2.warpaffine(img1,matrix,(cols,rows)) matrix = cv2.getrotationmatrix2d((cols/2,rows/2),270,1) img4 = cv2.warpaffine(img1,matrix,(cols,rows)) 如图4-9所示,把图像分别旋转90度、180度和270度,旋转的中心是原图像的中心。 4.2.4 图像平移 4.2.4 图像平移 图像平移的物理意义,可以理解为摄像头或者相机和物体的水平及垂直距离发生了变化。 import numpy as np img=cv2.imread("../picture/pig.jpg") img1 = cv2.cvtcolor(img, cv2.color_bgr2rgb) rows,cols,_ = img.shape matrix = np.float32([[1,0,25],[0,1,25]]) img2 = cv2.warpaffine(img1,matrix,(cols,rows)) matrix = np.float32([[1,0,50],[0,1,50]]) img3 = cv2.warpaffine(img1,matrix,(cols,rows)) matrix = np.float32([[1,0,75],[0,1,75]]) img4 = cv2.warpaffine(img1,matrix,(cols,rows)) 图4-9 图像旋转示例 如图4-10所示,把图像分别移动不同的距离,移动的中心是原图像的原点。 4.2.5 图像剪切 4.2.5 图像剪切 图像的剪切更像是使用了截图工具,选择保留感兴趣的区域。如图4-11所示,通过分别指定横纵坐标的区域,即可剪切出对应的图像。 img=cv2.imread("../picture/pig.jpg") img1 = cv2.cvtcolor(img, cv2.color_bgr2rgb) img2=img1[50:150,200:300] 图4-10 图像平移示例 图4-11 图像剪切示例 4.2.6 图像翻转 4.2.6 图像翻转 图像翻转的物理意义,可以理解为摄像头或者相机和物体的夹角及距离发生了变化,就有点像我们正面看电视屏幕是个长方形,但是从侧面看是一个平行四边形。从仿射变换的角度讲,就是同时出现了旋转与缩放变换。 在opencv中,只需要知道变换前的三个点与其对应的变换后的点,就可以通过函数getaffinetransform求得变换矩阵。 getaffinetransform(src, dst, mapmatrix) 假设把原图中的三个点srcpoints分别移动到canvaspoints。 srcpoints = np.float32([[0,0],[0,150],[150,150]]) canvaspoints = np.float32([[0,0],[0,150],[150,200]]) 如图4-12所示,整个图像将发生翻转。 matrix = cv2.getrotationmatrix2d((0,0),0,0.5) img2 = cv2.warpaffine(img1,matrix,(cols,rows)) matrix = cv2.getaffinetransform(np.array(srcpoints),np.array(canvaspoints)) print(matrix) img3 = cv2.warpaffine(img2,matrix,(cols,rows)) 图4-12 图像翻转示例 4.2.7 亮度与对比度 4.2.7 亮度与对比度 同样一个物体,光照的角度和强弱不同,我们看到的效果也不一样。摄影师经常会使用专业的设备调整光照,让作品更加生动形象,如图4-13所示。 图4-13 摄影时调整光照强度和角度 在图像处理中,通过适当调整像素值大小也可以达到类似效果。采用按像素的方式改变图像对比度和亮度,公式如下: 其中α调整的是对比度,值越大对比度越大;β调整的是亮度,值越大亮度越大。如图4-14所示,一共有三个子图,其中第二个子图相对于第一个子图,亮度变大;第三个子图相对于第一个子图,对比度变大。 import numpy as np import cv2 from matplotlib import pyplot as plt img=cv2.imread("../picture/lena.jpeg") img1=img.copy() img1=cv2.cvtcolor(img, cv2.color_bgr2gray) img2 = np.uint8(np.clip((img1 + 20), 1, 254)) img3 = np.uint8(np.clip((1.5 * img1 ), 0, 254)) 图4-14 图像调整亮度与对比度示例 4.3.1 高斯噪声和椒盐噪声 4.3.1 高斯噪声和椒盐噪声 高斯噪声(gaussian noise)是指其概率密度函数服从高斯分布的一类噪声。椒盐噪声(salt-and-pepper noise)是指两种噪声,一种是盐噪声(salt noise),另一种是胡椒噪声(pepper noise)。盐噪声为白色,胡椒噪声为黑色。一般两种噪声同时出现,呈现在图像上就是黑白杂点。 对图像增加噪声的方法很多,这里介绍如何使用skimage库进行噪声叠加,skimage库的安装方法为: pip install scikit-image skimage库提供了skimage.util.random_noise函数用于增加噪声。 skimage.util.random_noise(image, mode= ’gaussian ’, seed=none, clip=true,**kwargs) 其中主要参数的含义如下: ? image:原始图像。 ? mode:叠加噪声的类型,常用的类型包括gaussian、salt、pepper和s & p。 ? seed:随机数种子。 ? clip:表示是否截断超出范围的值,默认为true。 ? amount:噪声点的比例,默认为0.05。 当使用高斯噪声时,还可以设置均值mean和方差var,默认均值为0,方差为0.01。 以高斯噪声为例,如图4-15所示,分为四个子图,第一个子图为原始图像,第二至四个子图分别叠加了mean为0,var为0.01、0.04和0.09的高斯噪声。可见当mean相同时,var越大噪声越明显。这里需要指出的是,skimage在random_noise内部实现时,已经将图像归一化到(0,1),生成的图像也归一化到(0,1),因此设置mean和var值时要考虑这点。 img=cv2.imread("../picture/bigpig.jpeg") img=cv2.cvtcolor(img, cv2.color_bgr2rgb) img1=img.copy() img2=skimage.util.random_noise(img2, mode="gaussian", seed=none, clip=true,mean=0,var=0.01) img3=skimage.util.random_noise(img3, mode="gaussian", seed=none, clip=true,mean=0,var=0.04) img4=skimage.util.random_noise(img4, mode="gaussian", seed=none, clip=true,mean=0,var=0.09) 增加椒盐噪声时更加灵活一些,可以设置只增加白色的盐噪声,或者只增加黑色的胡椒噪声,或者两者都增加。 img=cv2.imread("../picture/bigpig.jpeg") img=cv2.cvtcolor(img, cv2.color_bgr2rgb) img1=img.copy() img2=skimage.util.random_noise(img2, mode="salt", seed=none, clip=true) img3=skimage.util.random_noise(img3, mode="pepper", seed=none, clip=true) img4=skimage.util.random_noise(img4, mode="s&p", seed=none, clip=true) 图4-15 图像增加高斯噪声示例 4.3.2 中值滤波 4.3.2 中值滤波 滤波的概念来自数字信号处理,滤波器可以用来过滤掉无用的高频或者低频信号。在图像处理领域,滤波器的主要作用之一就是去噪。 中值滤波使用邻域内所有像素的中位数替换中心像素的值,可以在滤除异常值的情况下较好地保留纹理信息。如图4-16所示,通过使用中值滤波可以有效去除图像中的高斯噪声。这里需要注意的是,cv2.medianblur只能处理经过灰度处理的图像,且图像的数值必须是整数,归一化后的图像数据是无法直接处理的。 img=cv2.imread("../picture/bigpig.jpeg") img=cv2.cvtcolor(img, cv2.color_bgr2gray) img1=img.copy() img2=img.copy() #原图为增加噪声的图 img1=skimage.util.random_noise(img1, mode="gaussian", seed=none, clip=true,mean=0,var=0.01) #medianblur需要处理整型 img1=np.uint8(img1*255) img2=cv2.medianblur(img1,3) 图4-16 使用中值滤波去除高斯噪声示例 中值滤波在一定程度上造成图像模糊和失真,滤波窗口变大时会非常明显。如图4-17所示,针对同一原始图像进行中值滤波,滤波窗口大小依次为3、11和19,图像模糊和失真越来越严重。 img=cv2.imread("../picture/bigpig.jpeg") img=cv2.cvtcolor(img, cv2.color_bgr2gray) img1=img.copy() img2=img.copy() img3=img.copy() img4=img.copy() #medianblur需要处理整型 img2=cv2.medianblur(img2,3) img3=cv2.medianblur(img3,11) img4=cv2.medianblur(img4,19) 图4-17 中值滤波带来的模糊效果示例 4.3.3 均值滤波 4.3.3 均值滤波 均值滤波和中值滤波非常类似,只不过中值滤波使用邻域内所有像素的中位数替换中心像素的值,而均值滤波使用的是平均值。均值滤波不能很好地保护图像细节,在图像去噪的同时也破坏了图像的细节部分,从而使图像变得模糊。均值滤波和中值滤波的效果非常接近,这里不再赘述。 4.3.4 高斯滤波 4.3.4 高斯滤波 高斯滤波使用邻域内所有像素的加权平均值替换中心像素的值。高斯滤波适用于消除高斯噪声,广泛应用于图像处理的减噪过程。如图4-18所示,使用高斯滤波可以很好地过滤掉高斯噪声。 img=cv2.imread("../picture/bigpig.jpeg") img=cv2.cvtcolor(img, cv2.color_bgr2gray) img1=img.copy() img2=img.copy() #原图为增加噪声的图 img1=skimage.util.random_noise(img1, mode="gaussian", seed=none, clip=true,mean=0,var=0.01) #medianblur需要处理整型 img1=np.uint8(img1*255) img2=cv2.gaussianblur(img1,(7,7),0) 图4-18 使用高斯滤波去除高斯噪声示例 如图4-19所示,随着高斯滤波窗口的增大,图像模糊和失真越来越严重。 img=cv2.imread("../picture/bigpig.jpeg") img=cv2.cvtcolor(img, cv2.color_bgr2gray) img1=img.copy() #medianblur需要处理整型 img2=cv2.gaussianblur(img1,(3,3),0) img3=cv2.gaussianblur(img1,(11,11),0) img4=cv2.gaussianblur(img1,(19,19),0) 图4-19 高斯滤波带来的模糊效果示例 4.3.5 高斯双边滤波 4.3.5 高斯双边滤波 高斯双边滤波同时考虑空间域和值域,这里的空间域是指考虑空间位置关系,根据距离核心位置的远近给予不同的加权值,原理和高斯滤波一样。值域是指考虑邻域范围内的像素差值计算出滤波器系数。高斯双边滤波是结合图像的空间邻近度和像素值相似度的一种折中处理,达到保护边缘并去除噪声的目的。通常在图像处理领域,把保护边缘并去除噪声简称为保边去噪。 在opencv中使用bilateralfilter实现高斯双边滤波。 bilateralfilter(src, n, sigmacolor, sigmaspace, bordertype) 其中主要参数的含义如下: ? src:输入变换前图像。 ? n:过滤过程中每个像素邻域的直径范围。 ? sigmacolor:颜色空间过滤器的标准差值。 ? sigmaspace:坐标空间中滤波器的标准差值。 ? bordertype:用于推断图像外部像素的某种边界模式,默认值为border_default。 通常可以将两个标准差设置成相同的值。如果小于10,则对滤波器影响很小;如果大于150,则会对滤波器产生较大的影响。 如图4-20所示,使用中值、高斯、高斯双边三个滤波器对于同一个添加了高斯噪声的图像进行处理,滤波窗口都是11,高斯双边的保边去噪效果最好。 img=cv2.imread("../picture/bigpig.jpeg") img=cv2.cvtcolor(img, cv2.color_bgr2gray) img1=img.copy() #原图为增加噪声的图 img1=skimage.util.random_noise(img1, mode="gaussian", seed=none, clip=true,mean=0,var=0.01) img1=np.uint8(img1*255) #medianblur需要处理整型 img2=cv2.medianblur(img1,11) img3=cv2.gaussianblur(img1,(11,11),0) img4=cv2.bilateralfilter(img1,11,50,50) 图4-20 使用高斯双边滤波去除高斯噪声示例 如图4-21所示,滤波窗口保持为11不变时,标准差从10增加到100,去噪效果显著提升。 img1=skimage.util.random_noise(img1, mode="gaussian", seed=none, clip=true,mean=0,var=0.01) img1=np.uint8(img1*255) img2=cv2.bilateralfilter(img1,11,10,10) img3=cv2.bilateralfilter(img1,11,50,50) img4=cv2.bilateralfilter(img1,11,100,100) 图4-21 标准差变化下高斯双边滤波去除高斯噪声的示例 4.4 本章小结 4.4 本章小结 本章介绍了图像处理的基础知识,通过学习本章,读者可以初步掌握机器视觉领域中如何保存图像数据,了解常见的图像格式。通过学习图像转换,可以了解常见的图像增强手段,包括基于仿射变换的图像缩放、旋转、平移、剪切和翻转,以及如何调整图像的亮度和对比度。最后了解如何使用中值滤波、均值滤波、高斯滤波以及高斯双边滤波进行保边去噪。 5.1 对抗样本的基本原理 5.1 对抗样本的基本原理 对抗样本是机器学习模型的一个有趣现象,攻击者通过在原数据上增加机器学习模型可接受而人类难以通过感官辨识的细微改变,使得机器学习模型做出错误的分类决定。一个典型的场景就是图像分类模型的对抗样本,通过在图像上叠加精心构造的变化量,在肉眼难以察觉的情况下,让分类模型产生误判。如图5-1所示,原始图像被图像分类模型识别为熊猫,置信度达到了57.7%,通过在原始图像上叠加一个扰动,图像分类模型却以99.3%的置信度识别为长臂猿。 从数学角度来描述对抗样本,输入数据为x,分类器为f,对应的分类结果表示为f(x)。假设存在一个非常小的扰动,使得: 即分类结果发生了改变,那么x+e就是一个对抗样本。 以一个例子来说明对抗样本的原理,示例代码为: https://github.com/duoergun0729/adversarial_examples/blob/master/code/5-case1.ipynb 首先加载需要使用的库,其中需要重点讲的是随机生成样本的datasets库。 from sklearn import datasets from sklearn.preprocessing import minmaxscaler import keras from keras import backend as k from keras.models import sequential from keras.layers import dense from keras.utils import to_categorical from keras.optimizers import rmsprop import numpy as np import matplotlib.pyplot as plt 图5-1 图像领域的对抗样本 通过datasets库可以随机生成样本数据,其中n_samples表示生成的样本数量,n_features为每个样本的特征数,n_classes表示生成的样本对应的标签的种类数,random_state为随机种子值。本例中我们生成4000个维度(即特征数)为1000的样本,标签种类只有两种。为了方便处理,需要把样本归一化到(0,1)的数据,标签转换成独热编码。 n_features=1000 x,y=datasets.make_classification(n_samples=4000,n_features=n_features, n_classes=2,random_state=0) #转换成独热编码 y=to_categorical(y) #归一化到(0, 1)的数据 x=minmaxscaler().fit_transform(x) 攻击的分类模型是一个非常简单的多层感知机,输入层大小为1000,输出层为2,激活函数为softmax。 model = sequential() model.add(dense(2,activation='softmax', input_shape=(n_features,) ) ) model.compile(loss='categorical_crossentropy', optimizer=rmsprop(), metrics=['accuracy']) 打印模型的结构,其中需要训练的参数个数为2002个。 model.summary() dense_1 (dense) (none, 2) 2002 total params: 2,002 trainable params: 2,002 non-trainable params: 0 对模型进行训练,批大小为16,训练30轮,准确度稳定在86%左右。 model.fit(x,y,epochs=30,batch_size=16) epoch 29/30 4000/4000 - 0s 75us/step - loss: 0.3756 - acc: 0.8605 epoch 30/30 4000/4000 - 0s 74us/step - loss: 0.3764 - acc: 0.8580 针对第0号数据进行预测,如图5-2所示,预测结果为标签0。 x0=x[0] y0=y[0] x0 = np.expand_dims(x0, axis=0) y0_predict = model.predict(x0) print(y0_predict) index=[0,1] labels=["lable 0","lable 1"] probability=y0_predict[0] 图5-2 针对原始数据的预测结果 接着我们在原始数据的每个维度上都叠加一个0.01或者–0.01的扰动形成新的数据。针对新的数据进行预测,如图5-3所示,预测结果为标签1。具体每个维度是叠加0.01还是–0.01,是我们通过某种白盒攻击算法计算得来的,这里先不展开介绍。 e = 0.01 cost, gradients = grab_cost_and_gradients_from_model([x0, 0]) n = np.sign(gradients) x0 += n * e y0_predict = model.predict(x0) print(y0_predict) 图5-3 针对新数据的预测结果 相对归一化到(0,1)的数据,绝对值为0.01的扰动,就好比针对图像增加了人眼几乎无法觉察的扰动,几乎可以忽略这些扰动,但是却让分类结果产生了黑白颠倒的变化,这就是对抗样本的基本原理。 5.2.1 使用PyTorch生成对抗样本 5.2.1 使用pytorch生成对抗样本 下面介绍在pytorch平台生成对抗样本的基本过程,示例代码为: https://github.com/duoergun0729/adversarial_examples/blob/master/code/5-case2-pytorch.ipynb 在示例中,通过损失函数在反向传递过程中直接调整原始图像的值,直到满足最大迭代次数或者对抗样本预测值达到预期为止,如图5-6所示。 图5-6 使用pytorch生成对抗样本的流程 首先加载使用的库文件,其中被攻击的模型在torchvision中。 import torch import torchvision from torchvision import datasets, transforms from torch.autograd import variable import torch.nn as nn from torchvision import models import numpy as np import cv2 定义使用的计算设备,默认使用的是cpu资源,如果有可用的gpu资源也将会自动使用。 #获取计算设备,默认是cpu device = torch.device("cuda" if torch.cuda.is_available() else "cpu") 加载经典的熊猫图片并进行标准化处理。opencv默认加载图片的方式是bgr格式而不是rgb格式,需要手工进行转换。攻击的对象是alexnet,对应的预训练模型是基于imagenet 2012数据集进行训练的,因此对数据进行标准化时,不能简单地进行归一化处理,而是需要使用特定的均值mean和标准差std。另外需要强调的是,opencv默认保存图片时使用的是whc格式,即第三个维度是通道数据,比如本例中为[224,224,3],但是pytorch是cwh格式,因此需要转换成[3,224,224],这一过程是调用img.transpose完成的。 image_path="../picture/cropped_panda.jpg" orig = cv2.imread(image_path)[..., ::-1] orig = cv2.resize(orig, (224, 224)) img = orig.copy().astype(np.float32) #标准化 mean = [0.485, 0.456, 0.406] std = [0.229, 0.224, 0.225] img /= 255.0 img = (img - mean) / std #whc转换成cwh img = img.transpose(2, 0, 1) #[3,224,224]-> [1,3,224,224] img=np.expand_dims(img, axis=0) img = variable(torch.from_numpy(img).to(device).float()) 实例化alexnet模型,并设置为预测模式。预测模式和训练模式的主要区别是,dropout和bn层的行为不一样,比如预测模式时,dropout会让全部数据通过,训练模式时会按照设定值随机丢失一部分数据。对原始数据进行预测,预测的标签值为388,表示为熊猫。 #使用预测模式主要影响dropout和bn层的行为 model = models.alexnet(pretrained=true).to(device).eval() label=np.argmax(model(img).data.cpu().numpy()) print("label={}".format(label)) label=388 设置仅输入数据可以计算梯度并被反向传递调整,模型的其他参数锁定不修改。在pytorch中通过设置requires_grad值即可完成上述设置。优化器torch.optim.adam只可调整输入数据img。 #图像数据梯度可以获取 img.requires_grad = true #设置为不保存梯度值,自然也无法修改 for param in model.parameters(): param.requires_grad = false optimizer = torch.optim.adam([img]) 设置损失函数,在本例中要进行定向攻击,可以使用交叉熵torch.nn.crossentropyloss。 target=288 target=variable(torch.tensor([float(target)]).to(device).long()) loss_func = torch.nn.crossentropyloss() 下面进行迭代计算,优化的目标是让损失函数(或称目标函数)最小化。迭代过程中优化器不断调整输入数据img,这一过程需要手工调用loss.backward进行反向传递,通过optimizer.step调整img。整个迭代过程的退出条件是达到最大迭代次数或者img预测值达到预期。 for epoch in range(epochs): # 梯度清零 optimizer.zero_grad() # forward + backward output = model(img) loss = loss_func(output, target) label=np.argmax(output.data.cpu().numpy()) print("epoch={} loss={} label={}".format(epoch,loss,label)) #如果定向攻击成功 if label == target: break loss.backward() optimizer.step() 经过26轮迭代后,预测的label从388变成了288,即豹子。imagenet 2012可以识别1000种物品和动物,具体的对应列表可以参考https://blog.csdn.net/u010165147/article/details/72848497,需要指出的是label(标签)是从0开始编号,该文献是从1开始编号,因此label为288的物体,在文献中需要查找的id为289。 epoch=22 loss=3.264517307281494 label=388 epoch=23 loss=2.973933696746826 label=293 epoch=24 loss=2.72090482711792 label=290 epoch=25 loss=2.472418785095215 label=290 epoch=26 loss=2.244988441467285 label=288 最后我们将对抗样本数据还原成图像数据,并和原始数据进行对比。如图5-7所示,一共分为三个子图,分别表示原始图像、对抗样本和扰动量。 adv=img.data.cpu().numpy()[0] #[3,224,244]->[224,224,3] adv = adv.transpose(1, 2, 0) #还原成正常图片像素范围并取整 adv = (adv * std) + mean adv = adv * 255.0 adv = np.clip(adv, 0, 255).astype(np.uint8) show_images_diff(orig,388,adv,target.data.cpu().numpy()[0]) 图5-7 原始数据和对抗样本的对比示意图 5.2.5 使用TensorFlow生成对抗样本 5.2.5 使用tensorflow生成对抗样本 下面介绍在tensorflow平台生成对抗样本的基本过程,示例代码为: https://github.com/duoergun0729/adversarial_examples/blob/master/code/ 5-case2-tensorflow-pb.ipynb 在示例中,通过损失函数在反向传递过程中直接调整原始图像的值,直到满足最大迭代次数或者对抗样本预测值达到预期为止,这里需要指出的是inception和alexnet模型中相同物体对应的标签是不一样的,比如熊猫在inception中为169,在alexnet中为388,如图5-8所示。 图5-8 使用tensorflow生成对抗样本的流程 首先加载使用的库文件,被攻击的模型是pb格式,tensorflow把计算图以及模型参数都保存在一个pb文件中,因此不用额外引入模型计算图定义的库。 import numpy as np from pil import image import tensorflow as tf import re 创建会话并定义可以训练的变量adv,adv保存攻击样本,target为定向攻击的标签值。 session=tf.session() #adv为对抗样本可以被训练和修改的量 adv = tf.get_variable(name="adv", shape=[1,100,100,3], dtype=tf.float32, initializer=tf.zeros_initializer) target = tf.placeholder(tf.int32) 从pb文件中还原出计算图,并与当前会话关联。在还原计算图的过程中,把输入的tensor与adv关联起来,从而达到把变量adv变成计算图输入的目的。 def create_graph(dirname): with tf.gfile.fastgfile(dirname, 'rb') as f: graph_def = session.graph_def graph_def.parsefromstring(f.read()) _ = tf.import_graph_def(graph_def, name='adv', input_map={"expanddims:0":adv} ) #从'http://download.tensorflow.org/models/image/imagenet/inception-2015-12-05.tgz'下载并解压到指定路径 create_graph("models/classify_image_graph_def.pb") 完成计算图和tensor的定义后,也只是完成了定义全部变量,并没有完成初始化操作,需要通过tf.global_variables_initializer手工进行初始化。 # 初始化参数非常重要 session.run(tf.global_variables_initializer()) tensorlist=[n.name for n in session.graph_def.node] #这里注意,一定要查看当前tensor的名称,因为导入pb时指定了名称前缀adv softmax_tensor = session.graph.get_tensor_by_name('adv_1/softmax:0') logits_tensor=session.graph.get_tensor_by_name('adv_1/softmax/logits:0') 对原始图像进行预测,并显示top3的预测值。 imagename="../picture/cropped_panda.jpg" image=np.array(image.open(imagename).convert('rgb')).astype(np.float32) #[100,100,3]->[1,100,100,3] image=np.expand_dims(image, axis=0) predictions = session.run(softmax_tensor, {adv: image}) predictions = np.squeeze(predictions) # creates node id --> english string lookup. node_lookup = nodelookup() #获得top 3预测结果 top_k = predictions.argsort()[-3:][::-1] for node_id in top_k: human_string = node_lookup.id_to_string(node_id) score = predictions[node_id] print('%s (score = %.5f)(id = %d)' % (human_string, score,node_id)) 在图像分类领域,经常使用top3或者top5的预测值,衡量准确率和错误率时也使用top3或者top5的预测值来计算,比如top3预测值中有一个正确,也可以理解为预测正确,反之亦然。本例中的预测结果top1就是熊猫,满足预期。 giant panda, panda, panda bear, coon bear, ailuropoda melanoleuca (score = 0.89233)(id = 169) indri, indris, indri indri, indri brevicaudatus (score = 0.00859)(id = 75) lesser panda, red panda, panda, bear cat, cat bear, ailurus fulgens (score = 0.00264)(id = 7) 下面开始迭代求解,定义学习率lr和迭代的最大次数epochs。定义损失函数为交叉熵函数,具体以预测结果的logits值与攻击目标标签计算交叉熵。优化器使用adam,由于adam自身也包含需要根据训练过程调整的参数,所以还需要调用tf.global_variables_initializer进行一次参数初始化。 epochs=500 lr=0.1 target_label=288 cross_entropy = tf.nn.sparse_softmax_cross_entropy_with_logits(logits=logits_tensor, labels=[target]) optimizer = tf.train.adamoptimizer(lr) train_step=optimizer.minimize(loss=cross_entropy,var_list=[adv]) # 初始化参数非常重要,adam的参数也需要这样初始化,gradientdescent可以省略这一步 session.run(tf.global_variables_initializer()) 迭代开始前,把adv的值初始化为原始图像值。 #初始化 session.run(tf.assign(adv, image)) for epoch in range(epochs): #获得损失值、对抗样本和预测值 loss,_,adv_img,predictions=session.run([cross_entropy, train_step,adv,softmax_tensor], {target:target_label}) predictions = np.squeeze(predictions) label=np.argmax(predictions) print("epoch={} loss={} label={}".format(epoch,loss,label)) #如果定向攻击成功 if label == target_label: #打印top3预测结果 top_k = predictions.argsort()[-3:][::-1] for node_id in top_k: human_string = node_lookup.id_to_string(node_id) score = predictions[node_id] print('%s (score = %.5f)(id = %d)' % (human_string, score,node_id)) break 经过34轮迭代计算,攻击成功,预测标签为288。 epoch=32 loss=[2.6293871] label=169 epoch=33 loss=[2.3296964] label=169 epoch=34 loss=[2.0458288] label=288 snowmobile (score = 0.12927)(id = 288) giant panda, panda, panda bear, coon bear, ailuropoda melanoleuca (score = 0.12047)(id = 169) indri, indris, indri indri, indri brevicaudatus (score = 0.04191)(id = 75) 5.3 基于梯度的对抗样本生成算法 5.3 基于梯度的对抗样本生成算法 之前我们介绍了使用优化器在反向传递的过程中调整输入,最终生成对抗样本。这是一种通用的对抗样本生成算法,也被称为基于优化的对抗样本生成算法。还有一系列算法是基于梯度来生成对抗样本的,naveed akhtar和ajmal mian在论文中较为系统地介绍了基于梯度的对抗样本生成算法。 常见的对抗样本生成算法如图5-9所示,其中比较重要的列含义如下。 ? method列表示攻击算法的简称。 ? black/white box列表示该算法是白盒攻击算法还是黑盒攻击算法。 ? targeted/non-targeted列表示该算法是定向攻击还是无定向攻击算法,事实上即使是无定向攻击算法,通过一定的改进也可以支持定向攻击,比如deepfool;定向攻击算法,通过一定的改进也可以支持无定向攻击,比如fgsm。 ? image-specific/universal列表示该算法是否具有通用性,image-specific表示不同的图片生成的对抗样本不一样,universal表示是通用对抗扰动,不同图片叠加同一个扰动均可以较大概率欺骗成功。 ? perturbation norm列表示该算法支持的范数,通常l2和l∞主要是为了减小扰动量的大小,让人类感官难以察觉,l0主要是为了减少修改的原始数据的个数,比如减少修改的图像像素的个数,本书的举例主要使用l2。 ? learning列表明该算法是基于迭代优化的,还是通过一步操作就可以完成的。 ? strength列表示该算法的攻击强度,它通常代表了攻击成功率。 图5-9 常见的对抗样本生成算法 下面几节将举例介绍几种典型的基于梯度的对抗样本生成算法。 5.4.1 FGM/FGSM基本原理 5.4.1 fgm/fgsm基本原理 fgm(fast gradient method),有时也被称为fgsm,即快速梯度算法,是ian j.goodfellow在论文《explaining and harnessing adversarial examples》中提出的,可以作为无定向攻击和定向攻击算法使用。以最常见的图像识别为例,我们希望在原始图片上做肉眼难以识别的修改,但是却可以让图像识别模型产生误判。假设图片原始数据为x,图片识别的结果为y,原始图像上叠加细微的变化η,肉眼难以识别η,使用数学公式表示如下: 将修改后的图像输入分类模型中,x与参数矩阵wt相乘。 对分类结果的影响还要受到激活函数的作用,攻击样本的生成过程就是追求以微小的修改,通过激活函数的作用,对分类结果产生最大化的变化。goodfellow指出,如果我们的变化量与梯度的变化方向完全一致,那么将会对分类结果产生较大的变化。 如图5-10所示,sign函数可以保证与梯度函数方向一致。 图5-10 sign函数 当x的维数为n时,模型的参数在每个维度的平均值为m,η的无穷范数为,每个维度的微小修改与梯度函数方向一致,累计的效果为nme。当数据的维度n很大时,虽然η非常小,但是累计的效果却可以较大,该效果作用到激活函数上,有可能对分类结果产生较大影响。例如一般的图像数据维度都很大,即使是非常小的[32,32,3]的图片,维度也达到了3072,假设e非常小,仅为0.01,m为1,那么累计的效果为30.72。 5.4.2 使用PyTorch实现FGM 5.4.2 使用pytorch实现fgm 下面介绍在pytorch平台实现fgm算法的基本过程,示例代码位于: https://github.com/duoergun0729/adversarial_examples/blob/master/code/ 5-fgm-pytorch.ipynb 狭义的fgm算法计算梯度后仅对图像进行一次修改,改进后的fgm会对图像迭代多次,本书中介绍的fgm算法都是基于迭代的版本。 在示例中主要流程为,用原始图像的值初始化对抗样本,通过损失函数计算梯度,然后根据fgm算法迭代更新对抗样本,直到满足最大迭代次数或者对抗样本预测值达到预期为止,如图5-11所示。 图5-11 pytorch实现fgm示例(定向攻击) 图像的预处理与模型加载方式可以参考5.2.1节,下面重点介绍fgm迭代求解的过程。首先通过前向计算,获取img对应的输出,计算出当前对应的标签id。 for epoch in range(epochs): # forward + backward output = model(img) loss = loss_func(output, target) label=np.argmax(output.data.cpu().numpy()) print("epoch={} loss={} label={}".format(epoch,loss,label)) #如果定向攻击成功 if label == target: break 然后手工清零梯度值并触发反向传递,计算出对应的梯度值img.grad.data。使用该梯度值,按照fgm的算法取sign值并乘以,使用该值更新img.data。 #梯度清零 optimizer.zero_grad() #反向传递,计算梯度 loss.backward() img.data=img.data-e*torch.sign(img.grad.data) 经过21轮迭代后,攻击成功,对抗样本对应的预测标签为288。 epoch=18 loss=2.943108558654785 label=388 epoch=19 loss=2.5982251167297363 label=290 epoch=20 loss=2.252422332763672 label=290 epoch=21 loss=1.9535226821899414 label=288 5.4.3 使用TensorFlow实现FGM 5.4.3 使用tensorflow实现fgm 下面介绍在tensorflow平台实现fgm算法的基本过程,示例代码为: https://github.com/duoergun0729/adversarial_examples/blob/master/code/ 5-fgm-tensorflow-pb.ipynb 在示例中主要流程为,用原始图像的值初始化对抗样本,通过损失函数计算梯度,然后根据fgm算法迭代更新对抗样本,直到满足最大迭代次数或者对抗样本预测值达到预期为止,如图5-12所示。 首先加载使用的库文件。 import numpy as np from pil import image import tensorflow as tf import re 创建会话并从pb文件中加载预训练的模型,target为定向攻击的标签值。 session=tf.session() target = tf.placeholder(tf.int32) def create_graph(dirname): with tf.gfile.fastgfile(dirname, 'rb') as f: graph_def = session.graph_def graph_def.parsefromstring(f.read()) _ = tf.import_graph_def(graph_def, name='') create_graph("models/classify_image_graph_def.pb") 图5-12 tensorflow实现fgm示例(定向攻击) 加载模型的输入和输出tensor。 session.run(tf.global_variables_initializer()) tensorlist=[n.name for n in session.graph_def.node] softmax_tensor = session.graph.get_tensor_by_name('softmax:0') input_tensor=session.graph.get_tensor_by_name('expanddims:0') logits_tensor=session.graph.get_tensor_by_name('softmax/logits:0') 对原始图像进行预测,并显示top3的预测值。 imagename="../picture/cropped_panda.jpg" image=np.array(image.open(imagename).convert('rgb')).astype(np.float32) #[100,100,3]->[1,100,100,3] image=np.expand_dims(image, axis=0) predictions = session.run(softmax_tensor, {input_tensor: image}) predictions = np.squeeze(predictions) # creates node id --> english string lookup. node_lookup = nodelookup() #top 3 top_k = predictions.argsort()[-3:][::-1] for node_id in top_k: human_string = node_lookup.id_to_string(node_id) score = predictions[node_id] print('%s (score = %.5f)(id = %d)' % (human_string, score,node_id)) 本例中的预测结果top3就是熊猫、马达加斯加大狐猴和小熊猫。 giant panda, panda, panda bear, coon bear, ailuropoda melanoleuca (score = 0.89233)(id = 169) indri, indris, indri indri, indri brevicaudatus (score = 0.00859)(id = 75) lesser panda, red panda, panda, bear cat, cat bear, ailurus fulgens (score = 0.00264)(id = 7) 定义最大迭代次数epochs,定义e为e,原始图片的id为original_label,定向攻击的目标的id为target_label,损失函数为交叉熵。根据损失函数和输入tensor定义梯度。 epochs=50 e=1.0 original_label=169 target_label=288 #定义损失函数 loss = tf.nn.sparse_softmax_cross_entropy_with_logits(logits=logits_tensor, labels=[target]) #定义梯度 grad, = tf.gradients(loss, input_tensor) adv_img=image.copy() 开始迭代求解,输入目标id和对抗样本adv_img,返回对应的预测值、梯度以及损失函数。当满足最大迭代次数或者预测值等于目标id值时退出。 for epoch in range(epochs): #计算预测结果、梯度和损失值 s,g,l=session.run([softmax_tensor,grad,loss], {target:target_label,input_tensor:adv_img}) #计算对抗样本 adv_img=adv_img-e*np.sign(g) predictions = np.squeeze(s) #获得top3预测结果 top_k = predictions.argsort()[-3:][::-1] predictions_id=top_k[0] print("epoch={} loss={} label={}".format(epoch,l,predictions_id)) if predictions_id == target_label: #打印top3预测结果 for node_id in top_k: human_string = node_lookup.id_to_string(node_id) score = predictions[node_id] print('%s (score = %.5f)(id = %d)' % (human_string, score,node_id)) break 经过8轮迭代后,攻击成功,以67.81%的概率识别为snowmobile,即摩托雪橇。 epoch=6 loss=[3.4092696] label=169 epoch=7 loss=[2.482861] label=75 epoch=8 loss=[0.38850152] label=288 snowmobile (score = 0.67807)(id = 288) motor scooter, scooter (score = 0.01814)(id = 260) moped (score = 0.01161)(id = 277) 可视化原始图片和对抗样本,如图5-13所示几乎没有差别。 from matplotlib import pyplot as plt image=image.astype(np.uint8) adv_img=adv_img.astype(np.uint8) plt.subplot(121) plt.imshow(image[0]) plt.title("panda") plt.subplot(122) plt.imshow(adv_img[0]) plt.title("snowmobile") plt.tight_layout() 图5-13 使用tensorflow实现fgm的原始图片和对抗样本 5.5.1 DeepFool基本原理 5.5.1 deepfool基本原理 与fgm相同,deepfool也是一种基于梯度的白盒攻击算法,是seyed-mohsen moosavi-dezfooli等人在论文《deepfool:a simple and accurate method to fool deep neural networks》中提出的。deepfool通常作为一种无定向攻击算法使用,相对fgm而言,不用指定学习速率e,算法本身可以计算出相对fgm更小的扰动来达到攻击目的。以最简单的二分类问题为例,如图5-14所示,假设分割平面是一个直线,直线的两侧分别对应不同的分类结果。 图5-14 二分类问题的对抗样本原理 如果想改变其中某点x0的分类结果,一定要跨过分割平面。显然最短的移动距离就是垂直分割平面进行移动,我们把这个距离记作r*(x0),分类器为f,那么存在如下关系: 二分类的deepfool攻击算法可以用伪码描述为: 定义分类器f在第k种分类上的概率为fk(x),那么分类的最终标签可以表示为其中概率最大的那个分类: 多分类的deepfool攻击算法可以当成二分类的扩展,伪码描述为: 位于https://github.com/lts4/deepfool的论文给出了deepfool攻击算法的开源实现。 5.5.2 使用PyTorch实现DeepFool 5.5.2 使用pytorch实现deepfool 下面介绍在pytorch平台实现deepfool算法的基本过程,示例代码为: https://github.com/duoergun0729/adversarial_examples/blob/master/code/ 5-deepfool-pytorch.ipynb 在示例中主要流程为,用原始图像的值初始化对抗样本,通过损失函数计算梯度,然后根据deepfool算法迭代更新对抗样本,直到满足最大迭代次数或者对抗样本预测值达到预期为止,如图5-15所示。 在本例中将使用pytorch的一些特有用法,所以先简单介绍一下使用到的一些pytorch知识。pytorch中可以直接使用tensor调用backward方法来计算梯度,梯度值保存在对应的tensor的grad字段中。假设y是x的一个计算图,即y是x的函数,当调用y.backward(x)时,x的梯度值为: 假设a和b都是浮点类型的tensor,b=2,a为b的平方,?a/?b=2b,输出结果为b乘以2b等于8。 b=variable(torch.floattensor([2]), requires_grad=true) a=b*b a.backward(b) print(b.grad.data) 图5-15 pytorch实现deepfool(无定向攻击)示例 当直接调用y.backward()时,x的梯度值为: 继续上例,输出结果为4。 b=variable(torch.floattensor([2]), requires_grad=true) a=b*b a.backward() print(b.grad.data) backward函数还可以指定参数retain_graph,因为反向传播需要缓存一些中间结果,反向传播之后,这些缓存就被清空,可通过指定这个参数不清空缓存,实现多次反向传播。 下面我们介绍一下pytorch实现deepfool的核心代码,首先设置仅输入数据可以计算梯度,模型的其他参数不计算梯度。 #图像数据梯度可以获取 img.requires_grad = true #设置为不保存梯度值自然也无法修改 for param in model.parameters(): param.requires_grad = false 定义一些全局参数,包括最大迭代次数、增益系数和类别数。 #最大迭代次数 epochs=100 #用作终止条件以防止类别更新 overshoot=0.02 #类别数 num_classes=1000 定义前向计算过程,初始化算法中需要迭代计算的参数w和累计扰动量r_tot。 # 前向计算过程 output = model(img) input_shape = img.cpu().detach().numpy().shape w = np.zeros(input_shape) r_tot = np.zeros(input_shape) 下面开始迭代计算过程,迭代退出条件是达到最大迭代次数或者分类标签发生变化,无定向攻击成功。初始化最小移动距离pert为无穷大,计算当前输入下的梯度值。 for epoch in range(epochs): scores=model(img).data.cpu().numpy()[0] label=np.argmax(scores) print("epoch={} label={} score={}".format(epoch,label,scores[label])) #如果无定向攻击成功 if label != orig_label: break pert = np.inf output[0, orig_label].backward(retain_graph=true) grad_orig = img.grad.data.cpu().numpy().copy() 遍历各个分类,计算对应的梯度值,选择其中除了当前分类外,跨越分类边界距离最短的分类。 for k in range(1, num_classes): if k == orig_label: continue #梯度清零 zero_gradients(img) output[0, k].backward(retain_graph=true) cur_grad = img.grad.data.cpu().numpy().copy() # 设置w_k和f_k w_k = cur_grad - grad_orig f_k = (output[0, k] - output[0, orig_label]).data.cpu().numpy() pert_k = abs(f_k)/np.linalg.norm(w_k.flatten()) # 选择pert最小值 if pert_k < pert: pert = pert_k w = w_k 计算累计扰动量并更新对抗样本,为了避免pert为0,导致r_i为0,会加上一个非常小的数,比如1e-8。np.linalg.norm默认计算向量的欧氏大小,即l2范数。 # 计算 r_i 和 r_tot r_i = (pert+1e-8) * w / np.linalg.norm(w) r_tot = np.float32(r_tot + r_i) img.data=img.data + (1+overshoot)*torch.from_numpy(r_tot).to(device) 经过3轮迭代,无定向攻击成功,攻击效果如图5-16所示。 epoch=0 label=88 score=18.340900421142578 epoch=1 label=88 score=13.300100326538086 epoch=2 label=879 score=12.384634017944336 图5-16 原始数据和对抗样本的对比示意图(无定向攻击) 虽然一般认为deepfool是一种无定向攻击算法,但是可以通过少量修改算法支持定向攻击,示例代码为: https://github.com/duoergun0729/adversarial_examples/blob/master/code/ 5-deepfool-pytorch-t.ipynb 核心代码内容如下,首先定义最大迭代次数、增益系数以及定向攻击目标,由于是定向攻击,损失函数使用交叉熵便于查看攻击过程是否收敛。需要指出的是,这里的损失函数并不是用于优化器优化使用的,而是为了衡量目前的对抗样本识别情况与定向攻击的目标之间的差距。 epochs=100 overshoot=0.02 #攻击目标 target_label=288 target=variable(torch.tensor([float(target_label)]).to(device).long()) loss_func = torch.nn.crossentropyloss() 进行迭代计算,迭代退出条件是达到最大迭代次数或者分类标签等于定向攻击标签,定向攻击成功。 for epoch in range(epochs): # 前向计算过程 output = model(img) label=np.argmax(output.data.cpu().numpy()) loss = loss_func(output, target) print("epoch={} label={} loss={}".format(epoch,label,loss)) #如果定向攻击成功 if label == target_label: break 根据梯度计算定向攻击的最小扰动。 #梯度清零 zero_gradients(img) output[0, target_label].backward(retain_graph=true) w = img.grad.data.cpu().numpy().copy() f = output[0, target_label].data.cpu().numpy() pert = abs(f)/np.linalg.norm(w.flatten()) # 计算 r_i 和 r_tot r_i = (pert+1e-8) * w / np.linalg.norm(w) r_tot = np.float32(r_tot + r_i) img.data=img.data + (1+overshoot)*torch.from_numpy(r_tot).to(device) 经过8轮迭代,定向攻击成功,攻击效果如图5-17所示。 epoch=4 label=88 loss=12.552578926086426 epoch=5 label=88 loss=10.39006233215332 epoch=6 label=88 loss=8.43277359008789 epoch=7 label=88 loss=6.093036651611328 epoch=8 label=288 loss=0.3813018798828125 图5-17 原始数据和对抗样本的对比示意图(定向攻击) 5.5.3 使用TensorFlow实现DeepFool 5.5.3 使用tensorflow实现deepfool 下面介绍在tensorflow平台实现deepfool算法的基本过程,示例代码为: https://github.com/duoergun0729/adversarial_examples/blob/master/code/ 5-deepfool-tensorflow-pb.ipynb 在示例中主要流程为,用原始图像的值初始化对抗样本,通过损失函数计算梯度,然后根据deepfool算法迭代更新对抗样本,直到满足最大迭代次数或者对抗样本预测值达到预期为止,以定向攻击为例,对抗样本预测值达到预期的条件就是与定向攻击的目标的标签值相等,如图5-18所示。 图5-18 tensorflow实现deepfool示例(定向攻击) 下面我们介绍使用tensorflow实现deepfool定向攻击的核心代码,首先定义全局参数包括迭代的最大次数、增益系数、原始数据标签和定向攻击的目标的标签。 epochs=100 overshoot=0.02 original_label=409 target_label=288 定义损失函数和梯度,由于是定向攻击,损失函数使用交叉熵。 loss = tf.nn.sparse_softmax_cross_entropy_with_logits(logits=logits_tensor, labels=[target]) grad, = tf.gradients(loss, input_tensor) 初始化计算图的全局变量,对抗样本以及迭代叠加的扰动。 session.run(tf.global_variables_initializer()) adv_img=image.copy() r_tot = np.zeros(adv_img.shape) 迭代计算扰动值,输入对抗样本和目标标签,获得对应的预测输出、梯度值和损失值,根据deepfool算法,迭代计算最小的扰动量r_tot,并使用r_tot更新对抗样本。 for epoch in range(epochs): s,w,l=session.run([logits_tensor,grad,loss], {target:target_label,input_tensor:adv_img}) f=s[0,target_label] pert = abs(f)/np.linalg.norm(w.flatten()) # 计算 r_i 和 r_tot r_i = (pert+1e-8) * w / np.linalg.norm(w) r_tot = np.float32(r_tot + r_i) adv_img=adv_img-(1+overshoot)*r_tot adv_img=np.clip(adv_img,0,255) 预测对抗样本,如果与定向攻击目标的标签相同,则打印预测的top3结果并退出迭代。 predictions = np.squeeze(s) top_k = predictions.argsort()[-3:][::-1] predictions_id=top_k[0] print("epoch={} loss={} label={}".format(epoch,l,predictions_id)) if predictions_id == target_label: for node_id in top_k: human_string = node_lookup.id_to_string(node_id) score = predictions[node_id] print('%s (score = %.5f)(id = %d)' % (human_string, score,node_id)) break 经过23轮迭代,攻击成功,攻击结果如图5-19所示。 epoch=19 loss=[5.6513553] label=409 epoch=20 loss=[4.698491] label=409 epoch=21 loss=[3.5230265] label=409 epoch=22 loss=[2.230075] label=409 epoch=23 loss=[0.7720326] label=288 snowmobile (score = 7.22838)(id = 288) crash helmet (score = 5.16406)(id = 778) macaw (score = 4.89591)(id = 409) 图5-19 原始数据和对抗样本的对比示意图(定向攻击) 5.6.1 JSMA基本原理 5.6.1 jsma基本原理 jsma(jacobian-based saliency map attack)是nicolas papernot等人在论文《the limitations of deep learning in adversarial settings》中提出的。jsma是典型的l0范数的白盒定向攻击算法,追求的是尽量减少修改的像素个数。该论文指出,通过jsma攻击图像分类模型,攻击成功率达到了97%,修改的像素的数量平均为总量的4.02%。jsma的特点是引入了saliency map即显著图的概念,用以表征输入特征对预测结果的影响程度。如图5-20所示,在一个典型的深度学习网络中,输入为x,维度为m,输出为y,维度为n,隐藏层的个数为n,前向计算的结果为f(x)。需要特别指出的是,f(x)为输出层没有经过softmax处理的结果,即所谓的logits输出。logits的输出经过softmax处理后会隐藏其丰富的信息,对生成对抗样本的效果有一定影响。事实上,在常见对抗样本算法的工程实现时,都是尽量直接使用logits的输出结果。在logits的输出结果中,值越小表明概率越低,概率低的会出现负值。logits可以理解为一个事件发生与该事件不发生的比值的对数。假设事件发生的概率为p,不发生的概率则为1–p,对应的logits定义为: 图5-20 典型的深度学习网络结构 定向攻击的目标标签为t,从saliency map中查找需要扰动的像素点的坐标的算法如下: saliency map从输入的特征空间或者说搜索空间中,查找影响最大的点(p1,p2)。判断的标准max综合考虑了两个因素α和β。α代表了对预测为攻击目标t的贡献,β代表了对预测为非攻击目标t的贡献,贡献衡量的大小就是f(x)的梯度值。前向传播输出f(x)使用的是logits的输出结果,f(x)的梯度为正表明是正向贡献,反之为负向贡献,因此max=–αxβ,max越大越有利于预测为目标t。 完整的jsma算法可以表示如下。 jsma 输入:输入为x,输出为y,前向传播输出f(x),定向攻击目标输出为y*,输入的特征空间Γ,扰动为,最大可以接受的扰动为 x*← x Γ={1…|x|} while f(x*)≠y* and ‖δx‖< do 计算前向梯度 ?f(x*) s = saliency_map (?f(x*),Γ,y*) 选择满足imax =arg max s(x,y*)[i] 使用更新x* x = x* - x end while 5.6.2 使用PyTorch实现JSMA 5.6.2 使用pytorch实现jsma 下面介绍在pytorch平台实现jsma算法的基本过程,示例代码位于: https://github.com/duoergun0729/adversarial_examples/blob/master/code/ 5-jsma-pytorch.ipynb 在示例中主要流程为,用原始图像的值初始化对抗样本,通过前向函数计算梯度,然后根据jsma算法迭代更新对抗样本,直到满足最大迭代次数或者对抗样本预测值等于攻击目标的标签为止,如图5-21所示。这里需要指出的是,jsma中梯度是直接通过前向计算获得的logits的输出结果与输入之间计算产生的,不依赖于损失函数,这是jsma与fgm、deepfool的区别。 图5-21 使用pytorch实现jsma示例 下面我们介绍使用pytorch实现jsma的核心代码,首先设置仅输入数据可以计算梯度,模型的其他参数不计算梯度。 #图像数据梯度可以获取 img.requires_grad = true #设置为不保存梯度值,自然也无法修改 for param in model.parameters(): param.requires_grad = false 定义一些全局参数,包括最大迭代次数、扰动系数和像素值的边界。 epochs=500 #扰动系数 theta=0.3 #定义边界 max_=3.0 min_=-3.0 pytorch在进行图像数据标准化时,imagenet2012的处理方式略有不同,均值和标准差分别为[0.485,0.456,0.406]和[0.229,0.224,0.225],因此像素值的边界不是常见的[0,1]或者[–1,1],而是[–3,3]。事实上像素值的边界会比[–3,3]范围稍小,但是不会超过这个范围。 mean = [0.485, 0.456, 0.406] std = [0.229, 0.224, 0.225] img /= 255.0 img = (img - mean) / std 定义目标标签和损失函数,这里的损失函数仅仅是为了便于显示迭代计算过程中是否收敛,并不是用于反向传递计算梯度。 #攻击目标 target_label=288 target=variable(torch.tensor([float(target_label)]).to(device).long()) loss_func = torch.nn.crossentropyloss() 定义搜索空间mask,mask大小与输入一致,初始化时认为输入的每个像素都在搜索空间中,当迭代过程中某个像素的值超过边界,就把mask中对应的位置设置为0。 mask = np.ones_like(img.data.cpu().numpy()) 下面开始迭代计算过程,迭代退出条件是达到最大迭代次数或者分类标签与攻击目标的标签相同,定向攻击成功。 for epoch in range(epochs): # 向前计算过程 output = model(img) label=np.argmax(output.data.cpu().numpy()) loss = loss_func(output, target) print("epoch={} label={} loss={}".format(epoch,label,loss)) #如果定向攻击成功 if label == target_label: break 迭代计算的过程就是不断提高预测为攻击目标的概率。通过saliency_map可以计算出当前最有利于提高该概率的像素坐标idx以及对应的方向pix_sign。 #梯度清零 zero_gradients(img) idx, pix_sign=saliency_map(output, img,target_label, mask) #进行扰动 img.data[idx]=img.data[idx]+pix_sign * theta * (max_ - min_) 当修改的像素的数值超过边界时,从搜索空间mask中去掉该像素,即在mask中将其设置为0,后继的迭代将不再修改该像素。 #达到极限的点不再参与更新 if (img.data[idx]<=min_) or (img.data[idx]>=max_): print("idx={} over {}".format(idx,img.data[idx])) mask[idx]=0 img.data[idx]=np.clip(img.data[idx], min_, max_) saliency_map的具体实现方式为,根据前向计算获得的logits的输出结果f与输入x、攻击目标标签t,计算出f相对于x的梯度derivative。根据derivative计算出α参数,这里简化了β参数的计算,相当于只关注了α参数。 def saliency_map(f, x,t, mask): #通过梯度函数,计算出对目标分类贡献最大的像素 f[0, t].backward(retain_graph=true) derivative=x.grad.data.cpu().numpy().copy() alphas = derivative * mask #用于记录哪些像素点被修改过 betas = -np.ones_like(alphas) α参数和β参数本身是带有符号的,即derivative大的为正,反之为负,因此可将求解预测为攻击目标的概率贡献最大的点的问题,转换为求解α参数和β参数乘积最小的问题,即有: sal_map = np.abs(alphas) * np.abs(betas) * np.sign(alphas * betas) #查找对攻击最有利的像素点 idx = np.argmin(sal_map) idx是一维坐标,还需要通过np.unravel_index将其转换为像素位置坐标。 #转换成(p1,p2)格式 idx = np.unravel_index(idx, mask.shape) pix_sign = np.sign(alphas)[idx] return idx, pix_sign 如图5-22所示,经过166轮迭代,攻击成功,l0为106即只修改了106个像素,l2为对抗样本与原始图像之间的差别。 epoch=164 label=51 loss=1.828399658203125 epoch=165 label=51 loss=1.7898340225219727 epoch=166 label=288 loss=1.7511415481567383 l0=(106,) l2=1428.7942469089103 图5-22 原始数据和对抗样本的对比示意图 5.6.3 使用TensorFlow实现JSMA 5.6.3 使用tensorflow实现jsma 下面介绍在tensorflow平台实现jsma算法的基本过程,示例代码位于: https://github.com/duoergun0729/adversarial_examples/blob/master/code/ 5-jsma-tensorflow-pb.ipynb 在示例中主要流程为,用原始图像的值初始化对抗样本,通过前向函数计算梯度,然后根据jsma算法迭代更新对抗样本,直到满足最大迭代次数或者对抗样本预测值等于攻击目标的标签为止,如图5-23所示。 下面我们介绍使用tensorflow实现jsma定向攻击的核心代码,首先定义全局参数包括迭代的最大次数、扰动系数、定向攻击目标的标签以及像素值的边界。 epochs=500 #扰动系数 theta=0.1 target_label=288 #定义边界 max_=255 min_=0 定义损失函数便于显示迭代计算过程中是否收敛,定义前向计算获取针对攻击目标的导数。 loss = tf.nn.sparse_softmax_cross_entropy_with_logits(logits=logits_tensor, labels=[target]) #前向计算过程非常缓慢 derivative = tf.gradients(logits_tensor[0,target_label], input_tensor) 图5-23 使用tensorflow实现jsma示例(定向攻击) 定义搜索空间mask,mask大小与输入一致。 mask = np.ones_like(adv_img) 下面开始迭代计算过程,迭代退出条件是达到最大迭代次数或者分类标签与攻击目标的标签相同,定向攻击成功,攻击成功时打印预测结果的top3。 for epoch in range(epochs): s,d,l=session.run([logits_tensor,derivative,loss], {target:target_label,input_tensor:adv_img}) predictions = np.squeeze(s) top_k = predictions.argsort()[-3:][::-1] predictions_id=top_k[0] print("epoch={} loss={} label={}".format(epoch,l[0],predictions_id)) if predictions_id == target_label: for node_id in top_k: human_string = node_lookup.id_to_string(node_id) score = predictions[node_id] print('%s (score = %.5f)(id = %d)' % (human_string, score,node_id)) break 通过saliency_map可以计算出当前最有利于提高定向攻击成功概率的像素坐标idx以及对应的方向pix_sign。 idx, pix_sign=saliency_map(d, mask) # 进行扰动 adv_img[idx]+=pix_sign * theta * (max_ - min_) #达到极限的点不再参与更新 if (adv_img[idx]<=min_) or (adv_img[idx]>=max_): print("idx={} over {}".format(idx,adv_img[idx])) mask[idx]=0 adv_img[idx]=np.clip(adv_img[idx], min_, max_) saliency_map的具体实现方式为,根据前向计算获得的导数derivative计算出α参数,这里简化了β参数的计算,相当于只关注了α参数。 def saliency_map(derivative, mask): alphas = derivative * mask #用于记录哪些像素点被修改过 betas = -np.ones_like(alphas) sal_map = np.abs(alphas) * np.abs(betas) * np.sign(alphas * betas) #查找对攻击最有利的像素点 idx = np.argmin(sal_map) #转换成(p1,p2)格式 idx = np.unravel_index(idx, mask.shape) pix_sign = np.sign(alphas)[idx] return idx, pix_sign 如图5-24所示,经过239轮迭代,攻击成功,l0为150即只修改了150个像素,l2为对抗样本与原始图像之间的差别。 epoch=237 loss=3.4565577507019043 label=46 epoch=238 loss=3.4611544609069824 label=46 epoch=239 loss=3.3630993366241455 label=288 l0=150 l2=413.5770786685355 snowmobile (score = 4.45301)(id = 288) mexican hairless (score = 4.37416)(id = 46) arabian camel, dromedary, camelus dromedarius (score = 4.24274)(id = 121) 图5-24 原始数据和对抗样本的对比示意图 5.7.1 CW基本原理 5.7.1 cw基本原理 cw算法是nicholas carlini和david wagner在论文《towards evaluating the robustness of neural networks》中提出的。cw通常被认为是攻击能力最强的白盒攻击算法之一,同时也是一种基于优化的对抗样本生成算法,但是在大多数文献中还是把它和基于梯度的攻击算法归为一类进行介绍。假设原始数据为x,分类结果为c(x),对抗样本为x',x'和x非常接近,但是分类结果c(x')=t却和c(x)不同,在定向攻击中t是指定的,无定向攻击中t是不确定的。cw同时支持l0、l2和l∞攻击,三种攻击方式算法基本相同,本章介绍其中使用最为广泛的l2定向攻击。 cw是一种基于优化的对抗样本生成算法,它的创新之处在于对损失函数(目标函数)的定义上。在定向攻击中,目标函数经常使用交叉熵,迭代优化的过程就是不断减小目标函数的过程。cw对常见的7种目标函数(见图5-25)进行了测试,其中(e)+是max(e,0)的简写,lossf,s(x)是交叉熵的简写。 图5-25 cw列举的常见目标函数 假设在原始数据x上叠加扰动,生成对抗样本为x+δ,对抗样本和原始数据之间的距离定义为d(x,x+δ),那么整个优化函数可以定义为: minimize d(x,x+δ)+c·f(x+δ) 其中x+δ∈[0,1]n 其中c是引入的一个参数,后面我们将介绍c如何选择,先讨论如何选择目标函数。如图5-26所示,nicholas carlini和david wagner在论文中指出,在相同数据集的情况下攻击同一模型,mean代表平均的扰动大小,prob代表攻击成功率,f6在攻击mean和prob两项指标上都表现出色,即f6可以在尽量小的扰动的情况下取得较高的成功率。 图5-26 相同数据集和模型的情况下不同目标函数的攻击效果 下面一个问题是如何选择c,在相同数据集的情况下攻击同一模型,目标函数均使用f6的情况下,如图5-27所示,图片左侧坐标表示攻击的成功率,曲线为虚线,右侧坐标表示平均扰动大小,曲线为实线,横坐标为c的大小。该实验证明,c越大攻击成功率和平均扰动大小都会变大,其中c=0.1是个临界点,当c从0.1增大时,攻击成功率迅速上升,c小于0.1或者大于1时,攻击成功率曲线都十分平缓。可见,综合考虑攻击成功率和平均扰动大小,选择尽量小的c是必然结果。cw算法的一个核心点就是通过二分查找计算出尽量小的c值。 图5-27 相同数据集和模型的情况下取不同c值的攻击效果 二分查找计算出尽量小的c值的方法描述如下,假设搜索的c的上限为upper_bound下限为lower_bound,那么搜索流程如下: binary_search 输入:c的上限为upper_bound,下限为lower_bound,初始值为initial_const,最大迭代次数为 binary_search_steps 初始化 i← 0 while i < binary_search_steps do 使用adam迭代优化对抗样本 if 攻击成功 then upper_bound = min(upper_bound,c) c= (lower_bound + upper_bound)/2 else lower_bound = max(lower_bound,c) c=(lower_bound + upper_bound)/2 end if end while cw算法的另外一个特色就是对数据截断的处理。以图像文件为例,通常一个像素点的取值范围是[0,255]的整数,但是在进行对抗攻击时,经常会出现溢出现象,比如–0.1或者255.1,这个时候就需要进行截断处理。常见的截断处理方法有以下三种。 projected gradient descent,即梯度投影下降,这也是使用最多的一种方式,在每次迭代更新对抗样本后,直接将对抗样本进行截断,然后使用截断后的对抗样本进入下一轮迭代。projected gradient descent最大的问题是会把截断带来的误差带入下一轮迭代。 clipped gradient descent,即梯度截断下降,该方法不是直接截断对抗样本,而是优化了目标函数,在目标函数层面避免了数据溢出。假设原有目标函数为f(x+δ),优化后的目标函数为: f(min(max(x+δ,0),1)) clipped gradient descent解决了projected gradient descent中把截断带来的误差带入下一轮迭代的问题,但是引入了一个新的问题,就是容易出现梯度消失,即梯度非常小甚至为0,导致无法继续迭代优化。 change of variables,即变换变量,引入新的变量w,那么对抗样本可以表示为: 因为tanh(w)∈[–1,1],所以可以保证对抗样本不溢出。并且tanh(w)不会出现clipped gradient descent的梯度消失问题,change of variables也是cw使用的方法。 如图5-28所示,nicholas carlini和david wagner在论文中指出,在相同数据集的情况下攻击同一模型,在目标函数相同的情况下,change of variables在平均扰动大小mean和攻击成功概率prob上均表现优异。 具体到定向的l2攻击,假设攻击目标标签为t,被攻击模型的logits输出为z,那么目标函数定义为: 其中k为常数,如图5-29所示,横坐标为k,纵坐标为攻击的成功概率,实线为无目标攻击,虚线为定向攻击,曲线描述了无论是无目标攻击还是定向攻击,攻击的成功概率都会随k的增长而增长,当k=40时,概率几乎为100%。 图5-28 常见的截断处理方法的效果对比 整个优化的目标如下,其中w为新定义的变量。 图5-29 参数k对模型迁移攻击的成功概率的影响 5.7.2 使用TensorFlow实现CW 5.7.2 使用tensorflow实现cw 下面介绍在tensorflow平台实现cw算法的基本过程,示例代码为: https://github.com/duoergun0729/adversarial_examples/blob/master/code/ 5-cw-tensorflow-pb.ipynb 该代码参考了以下网址论文中的实现。 https://github.com/carlini/nn_robust_attacks/blob/master/l2_attack.py 在示例中主要流程为,用原始图像的值初始化对抗样本,通过前向函数和adam优化器根据cw算法迭代更新对抗样本,直到满足最大迭代次数或者对抗样本预测值等于攻击目标的标签为止,需要指出的是cw算法不会显示计算出梯度,梯度的计算和使用隐藏在了adam优化器中,如图5-30所示。 图5-30 使用tensorflow实现cw示例(定向攻击) 下面我们介绍使用tensorflow实现cw定向攻击的核心代码,首先定义全局参数包括adam优化器的最大迭代次数、学习率、二分查找最大次数、c的初始值、像素值的边界和定向攻击目标的标签,需要指出两点,第一是定向攻击目标的标签需要使用独热编码;第二是虽然imagenet2012的分类对象种类为1000,但是tensorflow下的inception模型的输出大小是1008不是1000。 #adam的最大迭代次数 max_iterations=1000 #adam的学习速率 learning_rate=0.01 #二分查找最大次数 binary_search_steps=10 #c的初始值 initial_const=1e-3 confidence=initial_const #像素值区间 boxmin = 0.0 boxmax = 255.0 #类别数 tf的实现里面是1008 num_labels=1008 #攻击目标标签必须使用独热编码 target_label=288 target_label=np.eye(num_labels)[target_label] 定义四个重要的tf变量,其中modifier表示扰动量,整个优化过程中迭代调整的就是modifier;timg表示原始数据,tlab表示独热编码后的攻击目标标签,const表示c值,这三个变量在全局初始化时会被设置为0,但是每轮迭代前会被设置为对应的值。 #我们需要优化调整的变量 modifier = tf.variable(np.zeros(shape,dtype=np.float32)) #用于给tf输入参数的变量 timg = tf.variable(np.zeros(shape), dtype=tf.float32) tlab = tf.variable(np.zeros((num_labels)), dtype=tf.float32) const = tf.variable(np.zeros([]), dtype=tf.float32) 根据像素值的边界计算标准差boxmul和均值boxplus,把对抗样本modifier+timg转换成新的输入newimg。 boxmul = (boxmax - boxmin) / 2. boxplus = (boxmin + boxmax) / 2. newimg = tf.tanh(modifier + timg) * boxmul + boxplus 从tensorflow官网下载inception模型文件并解压到指定路径,该模型基于imagenet2012数据集训练。 http://download.tensorflow.org/models/image/imagenet/inception-2015-12-05.tgz 从pb文件中还原计算图和对应的参数,并且把newimg映射到计算图的输入。 session=tf.session() def create_graph(dirname): with tf.gfile.fastgfile(dirname, 'rb') as f: graph_def = session.graph_def graph_def.parsefromstring(f.read()) _ = tf.import_graph_def(graph_def, name='', input_map={"expanddims:0":newimg}) create_graph("models/classify_image_graph_def.pb") # 初始化参数非常重要 session.run(tf.global_variables_initializer()) tensorlist=[n.name for n in session.graph_def.node] #注意一定要查看下当前tensor的名称再写 softmax_tensor = session.graph.get_tensor_by_name('softmax:0') logits_tensor=session.graph.get_tensor_by_name('softmax/logits:0') output=logits_tensor 定义目标函数loss,其中loss1表示识别为定向攻击目标的概率与识别其他分类的概率的差,loss2表示扰动的大小。 #计算对抗样本和原始数据之间的距离 l2dist = tf.reduce_sum(tf.square(newimg-(tf.tanh(timg) * boxmul + boxplus)),[1,2,3]) #挑选指定分类标签和剩下其他分类中概率最大者,计算两者之间的概率差 real = tf.reduce_sum((tlab)*output,1) other = tf.reduce_max((1-tlab)*output - (tlab*10000),1) loss1 = tf.maximum(0.0, other-real+k) #计算总的损失函数 loss2 = tf.reduce_sum(l2dist) loss1 = tf.reduce_sum(const*loss1) loss = loss1+loss2 定义优化器,cw算法使用的优化器是adam,主要原因是adam收敛速度快。 optimizer = tf.train.adamoptimizer(learning_rate) train_op = optimizer.minimize(loss, var_list=[modifier]) 把原始图像转换成tanh的形式,定义二分查找的边界lower_bound和upper_bound并初始化c。o_bestl2记录迭代过程中最小的距离,o_bestscore记录标签,o_bestattack记录对抗样本。 # 转换成tanh的形式 image = np.arctanh((image - boxplus) / boxmul * 0.999999) #c的初始化边界 lower_bound = 0 c=initial_const upper_bound = 1e10 # 保存最佳的l2值、预测概率值和对抗样本 o_bestl2 = 1e10 o_bestscore = -1 o_bestattack = [np.zeros(shape)] 迭代进行二分查找,每轮二分查找前都需要初始化全部全局参数,主要是为了重置adam的内置参数的状态。 for outer_step in range(binary_search_steps): print("o_bestl2={} confidence={}".format(o_bestl2,confidence) ) # 重置adam的内置参数的状态 session.run(tf.global_variables_initializer()) session.run(tf.assign(timg, image)) session.run(tf.assign(tlab, target_label)) session.run(tf.assign(const, confidence)) 使用adam迭代优化,论文中建议的迭代优化的次数为10000,如果对抗样本的预测值与定向攻击目标的标签一致,表明定向攻击成功,更新o_bestl2、o_bestscore和o_bestattack。与其他定向攻击算法不同的是,cw致力于迭代求解最优的对抗样本。在本例中最优的对抗样本的定义为对抗样本的预测值与定向攻击目标的标签一致,同时其对应的l2值最小。因此cw并不会因为某轮迭代时,对抗样本的预测值与定向攻击目标的标签一致就退出,而是继续迭代计算,求解对抗样本的预测值与定向攻击目标的标签一致的情况下,c的最小值。 for iteration in range(max_iterations): # 进行攻击 _, l, l2, sc, nimg = session.run([train_op, loss, l2dist, output, newimg]) # 迭代过程每完成10% 打印当时的损失函数的值 if iteration%(max_iterations//10) == 0: print(iteration,session.run((loss,loss1,loss2))) if (l2 < o_bestl2) and (np.argmax(sc) == np.argmax(target_label) ): print("attack success l2={} target_label={}".format(l2,np.argmax(target_label))) o_bestl2 = l2 o_bestscore = np.argmax(sc) o_bestattack = nimg 每轮二分查找结束后,更新c值以及对应的边界。 confidence_old=-1 if (o_bestscore == np.argmax(target_label)) and o_bestscore != -1: #攻击成功,减小c值 upper_bound = min(upper_bound,confidence) if upper_bound < 1e9: print() confidence_old=confidence confidence = (lower_bound + upper_bound)/2 else: lower_bound = max(lower_bound,confidence) confidence_old=confidence if upper_bound < 1e9: confidence = (lower_bound + upper_bound)/2 else: confidence *= 10 如图5-31所示,经过10轮二分查找,每轮adam优化10000次,攻击成功,c值为2125.0,l0为15828即只修改了15828个像素,l2为31138.6,即对抗样本与原始图像之间的差别。 l0=15828 l2=31138.633736887045 图5-31 原始数据和对抗样本的对比示意图(adam迭代10000次) 5.7.3 使用PyTorch实现CW 5.7.3 使用pytorch实现cw 下面介绍在pytorch平台实现cw算法的基本过程,示例代码位于: https://github.com/duoergun0729/adversarial_examples/blob/master/code/ 5-cw-pytorch.ipynb 首先定义全局参数,具体参数的含义可以参考5.7.2节中tensorflow的实现细节,其中需要指出的是pytorch中基于imagenet2012训练的alexnet模型,类别数为1000,并且图像预处理和的区间为–3.0到3.0,这两点与tensorflow有所差别。 #像素值区间 boxmin = -3.0 boxmax = 3.0 #类别数pytorch的实现里面是1000 num_labels=1000 #攻击目标标签必须使用独热编码 target_label=288 tlab=variable(torch.from_numpy(np.eye(num_labels)[target_label]). to(device).float()) 进行迭代及二分查找,定义需要训练的变量modifier,以及adam优化器。 for outer_step in range(binary_search_steps): print("o_bestl2={} confidence={}".format(o_bestl2,confidence) ) #把原始图像转换成图像数据和扰动的形态 timg = variable(torch.from_numpy(np.arctanh((img - boxplus) / boxmul * 0.999999)).to(device).float()) modifier=variable(torch.zeros_like(timg).to(device).float()) #图像数据的扰动量梯度可以获取 modifier.requires_grad = true #定义优化器,仅优化modifier optimizer = torch.optim.adam([modifier],lr=learning_rate) 根据modifier和原始图像定义新的输入newimg,并进行前向计算或者输出当前的模型。 for iteration in range(1,max_iterations+1): optimizer.zero_grad() #定义新输入 newimg = torch.tanh(modifier + timg) * boxmul + boxplus output=model(newimg) 定义损失函数,其中loss2直接使用torch.dist计算l2距离,通过torch.clamp计算loss1和0之间的最大值。 loss2=torch.dist(newimg,(torch.tanh(timg) * boxmul + boxplus),p=2) real=torch.max(output*tlab) other=torch.max((1-tlab)*output) loss1=other-real+k loss1=torch.clamp(loss1,min=0) loss1=confidence*loss1 loss=loss1+loss2 通过loss反向传递并优化变量modifier。 loss.backward(retain_graph=true) optimizer.step() 使用adam迭代优化如果对抗样本的预测值与定向攻击目标的标签一致,表明定向攻击成功,更新o_bestl2、o_bestscore和o_bestattack。 l2=loss2 sc=output.data.cpu().numpy() # print out the losses every 10% if iteration%(max_iterations//10) == 0: print("iteration={} loss={} loss1={} loss2={}". format(iteration,loss,loss1,loss2)) if (l2 < o_bestl2) and (np.argmax(sc) == target_label ): print("attack success l2={} target_label={}".format(l2,target_label)) o_bestl2 = l2 o_bestscore = np.argmax(sc) o_bestattack = newimg.data.cpu().numpy() 如图5-32所示,经过10轮二分查找,每轮adam优化1000次,攻击成功,c值为0.1953125,l0为90441即只修改了90441个像素,l2为69825.1,即对抗样本与原始图像之间的差别。 l0=90441 l2=69825.06902252228 图5-32 原始数据和对抗样本的对比示意图(adam迭代1000次) 5.8 本章小结 5.8 本章小结 本章介绍了白盒攻击的基本原理,并使用pytorch和tensorflow生成了优化后的对抗样本。本章还介绍了常见的fgm、deepfool、jsma和cw算法,并在pytorch和tensorflow环境下给出了完整的算法实现。通过本章的学习可以比较深刻地理解这几种常见的白盒攻击算法。在实际使用中,我们很少会从头实现常见的白盒攻击算法,在本书的第9章,我们将会介绍常见的对抗样本工具箱的使用。 6.1 单像素攻击算法 6.1 单像素攻击算法 单像素攻击(single pixel attack)是典型的黑盒攻击算法。nina narodytska和shiva prasad kasiviswanathan在论文《simple black-box adversarial perturbations for deep networks》中介绍了该算法。在白盒攻击中,我们根据一定的算法,在原始数据上叠加了精心构造的扰动,从而导致模型产生分类错误,而单像素攻击的基本思想是,可以通过修改原始数据上的一个像素的值,让模型产生分类错误。 以典型的图像分类模型为例,假设图像为i,分类标签为c(i),分类标签的集合表示为{1,…,c},图像i的通道数为l,通常的彩色相片的通道数为3,灰度图像为1,b为其中的通道序号,(b,x,y)就表示坐标为(x,y)的像素点的第b号通道,(*,x,y)表示坐标为(x,y)的像素点的全部通道。设图像的高为h,宽为w,lb为像素值的下限,ub为上限,那么有: 图6-1 通过修改一个像素点改变分类结果 定义攻击的模型为nn,nn的分类结果为: nn(i)=(o1,…,oc) 其中oi表示图像i识别为第i个标签的概率。比如分类标签为{1,2,3,4},针对图像i的分类结果nn(i)={0.02,0.2,0.7,0.08},说明分类为标签3的概率最大。 定义π(nn(i),k)表示模型nn针对图像i分类的top k个标签,在图像分类任务中,经常使用分类概率最大的几个标签表示分类结果,典型的为top 3和top 5。比如π(nn(i),3)表示top 3的分类标签。当nn(i)={0.02,0.2,0.7,0.08},π(nn(i),3)={3,2,4},即top 3的分类标签分别为3、2和4。 假设存在某个关键的像素点(*,x,y),通过修改其值让图像i变成图像ip,nn的分类结果产生错误: 我们定义针对点(*,x,y)的扰动函数为pert(i,p,x,y),其中p表示扰动系数,那么扰动的方法为: sign是符号函数,定义为: 其中扰动后的图像表示为。 单像素攻击算法可以用伪码描述为: randadv(nn) 输入:图像i,分类标签为c(i)∈{1,…,c},扰动系数为p,迭代最大次数为u i = 1, critical = 0 while i ≦ u do 随机选择点(*,x,y) 针对该点进行扰动,获得新图像ip(x,y)= pert(i,p,x,y) if c(i)?π(nn(ip),1) then critical= critical+1 end if i=i+1 end while 目前advbox和foolbox均实现了单像素攻击算法,下面以advbox为例介绍单像素攻击算法的具体实现。 首先定义尝试的像素的个数max_pixels以及像素的取值范围。通常像素的取值范围为[0,1],[–1,1]或者[0,255]。 max_pixels=1000 min_, max_ = (0,255) 强制拷贝原始数据避免针对adv_img的修改影响到adversary.original。 adv_img = np.copy(adversary.original) 其中adversary对象的各个字段含义如下。 ? adversary.original:原始数据。 ? adversary.original_label:原始数据的标签。 ? adversary.target_label:定向攻击的目标值。 ? adversary.__adversarial_example:保存生成的对抗样本。 ? adversary.adversarial_label:对抗样本的标签。 获取图像的宽w和高h,如果图像数据中无法解析宽和高的数据就直接退出。 axes = [i for i in range(adversary.original.ndim) if i != self.model.channel_axis()] #输入的图像必须具有高和宽属性 assert len(axes) == 2 h = adv_img.shape[axes[0]] w = adv_img.shape[axes[1]] 从原始图像中随机提取max_pixels个点。 pixels = np.random.permutation(h * w) pixels = pixels[:max_pixels] numpy.random中随机排列数组的函数有两个,分别为permutation和shuffle,这两个函数的功能完全相同,但是permutation不影响原始数据而shuffle是直接在原始数据上修改。 遍历随机挑选的max_pixels个点,并根据原始图像的通道数self.model.channel_axis,组成像素点的具体坐标。 for i, pixel in enumerate(pixels): x = pixel % w y = pixel // w location = [x, y] location.insert(self.model.channel_axis(), slice(none)) location = tuple(location) advbox在生成扰动值时,并没有完全参考论文中的实现,因为手工指定的扰动系数p会大大影响扰动效果。为了避免调参的烦琐过程,advbox通过从小到大遍历的方式自动尝试合适的扰动量。 for value in np.linspace(min_, max_, num=256): perturbed = np.copy(adv_img) #针对图像的每个信道的点[x,y]同时进行修改 perturbed[location] = value f = self.model.predict(perturbed) adv_label = np.argmax(f) if adversary.try_accept_the_example(adv_img, adv_label): return adversary 6.2 单像素攻击MNIST识别模型 6.2 单像素攻击mnist识别模型 advbox中提供了基于paddlepaddle平台的示例,介绍如何使用single pixel attack攻击基于cnn的mnist识别模型,代码路径为: https://github.com/baidu/advbox/blob/master/tutorials/mnist_tutorial_ singlepixelattack.py 首先加载需要使用的库文件,其中paddleblackboxmodel封装了paddlepaddle下的模型。 import sys import os sys.path.append("..") import numpy as np import paddle.fluid as fluid import paddle.v2 as paddle from advbox.adversary import adversary from advbox.attacks.localsearch import singlepixelattack from advbox.models.paddleblackbox import paddleblackboxmodel from tutorials.mnist_model import mnist_cnn_model 通过设置环境变量with_gpu来动态设置是否使用gpu资源,这特别适合在mac上开发但是在gpu服务器上运行的情况。比如在mac上不设置该环境变量,在gpu服务器上设置export with_gpu=1。 with_gpu = os.getenv('with_gpu', '0') != '0' 定义test_reader,从mnist的测试集中随机产生数据,每次产生一个。 batch_size = 1 test_reader = paddle.batch( paddle.reader.shuffle( paddle.dataset.mnist.test(), buf_size=128 * 10), batch_size=batch_size) 加载预训练的cnn模型,模型的目录在mnist下。 fluid.io.load_params(exe, "./mnist/", main_program=fluid.default_main_program()) 创建paddlepaddle下的模型对象以及single pixel attack算法对象。mnist数据集中的图像大小为[28,28,1],因此max_pixels最大不能超过28x28。 m = paddleblackboxmodel(fluid.default_main_program().clone(for_test=true), img_name,label_name,logits.name, (0, 255),channel_axis=0) #形状为[1,28,28] channel_axis=0 #形状为[28,28,1] channel_axis=2 attack = singlepixelattack(m) attack_config = {"max_pixels": 28*28} 遍历测试数据集,加载图像数据到img中,标签数据为data[0][1]。 # 使用测试数据生成对抗样本 total_count = 0 fooling_count = 0 for data in test_reader(): total_count += 1 img=data[0][0] img=np.reshape(img,[1,28,28]) adversary = adversary(img, data[0][1]) # singlepixelattack 无定向攻击 adversary = attack(adversary, **attack_config) 统计攻击结果,超出最大测试次数时退出。 if adversary.is_successful(): fooling_count += 1 print('attack success, original_label=%d, adversarial_label=%d, count=%d'% (data[0][1], adversary.adversarial_label, total_count)) else: print('attack failed, original_label=%d, count=%d' % (data[0][1], total_count)) if total_count >= total_num: print("[test_dataset]: fooling_count=%d, total_count=%d, fooling_rate=%f" % (fooling_count, total_count,float(fooling_count) / total_count)) break 下面介绍如何生成攻击用的模型,advbox的测试模型是一个识别mnist的cnn模型。 python mnist_model.py 运行攻击代码,以基于singlepixelattack算法的演示代码为例。 python mnist_tutorial_singlepixelattack.py 运行结果如下,随机选择了mnist测试集中的10张图片用于测试,singlepixelattack算法攻击成功率为100%。 [test_dataset]: fooling_count=10, total_count=10, fooling_rate=1.000000 singlepixelattack attack done 6.3 本地搜索攻击算法 6.3 本地搜索攻击算法 在实际使用中,单像素攻击对于比较简单的数据集有很好的攻击效果,比如mnist和cifar10,cifar10数据集如图6-2所示。这类数据集的图片大小普遍比较小,比如mnist形状为[28,28,1],cifar10形状为[32,32,3],针对一个像素点的修改可以对分类结果产生较大影响。但是当图像较大时,一个像素点的改变很难影响到分类结果。并且随着图像文件的增大,搜索空间也迅速增大,单像素攻击的效率也会快速下降。针对这一情况,有的单像素攻击在实现上允许同时修改一个以上的像素点,比如同时修改50个像素点,但是攻击效果并不明显。 本地搜索攻击算法(local search attack)改进了单像素攻击算法,论文《simple black-box adversarial perturbations for deep networks》重点介绍了该算法。单像素攻击算法没有很好地利用模型的反馈信息去优化扰动,很大程度上依赖随机选择像素和迭代调整像素点的值。本地搜索攻击算法的主要改进点就是根据模型的反馈信息去选择扰动的点,并随机选择对分类结果影响大的点周围的点,进一步进行选择。 首先用伪码描述新的扰动函数cyclic。 cyclic(r,b,x,y) 输入:图像i,r是扰动系数,并且r∈[0,2] if ri(b,x,y)< lb then return ri(b,x,y)+(ub - lb) else if ri(b,x,y)> ub then return ri(b,x,y)-(ub - lb) else return ri(b,x,y) else if 图6-2 cifar10数据集 本地搜索攻击算法可以用伪码描述为: locsearchadv(nn) 输入:图像i,分类标签为c(i)∈{1,…,c}, p和r是扰动系数,并且r∈[0,2] 正方形搜索空间的边长的一半为d,最大迭代次数为r,每次迭代选择的像素的个数为t i=1 随机选择10%的像素,初始化集合(px,py) while i ≤ r do 遍历(px,py)的像素,进行扰动pert(i,p,x,y),得到新图像集合 计算新图像集合中每个图像预测原分类标签的概率score(i) 选择概率最大的t个像素点,得到新的像素集合(px,py) 遍历(px,py),依次在原始图像上进行扰动cyclic(r,b,x,y),更新原始图像i if c(i)?π(nn(ip),1) then 攻击成功 end if 遍历(px,py),以每个像素点作为中心,画出边长为2d的正方形,正方形范围内的点都纳入(px,py) end while 目前advbox和foolbox均实现了单像素攻击算法,下面继续以advbox为例介绍本地搜索攻击算法的具体实现。 首先定义了标准化和逆标准化函数,为了处理方便,需要把数据标准化到[–0.5,0.5]之间。 def normalize(im): im = im -(min_ + max_) / 2 im = im / (max_ - min_) lb = -1 / 2 ub = 1 / 2 return im, lb, ub def unnormalize(im): im = im * (max_ - min_) im = im + (min_ + max_) / 2 return im adv_img, lb, ub = normalize(adv_img) 随机选择一部分像素点,总数不超过全部的10%,最大为128个。 def random_locations(): n = int(0.1 * h * w) n = min(n, 128) locations = np.random.permutation(h * w)[:n] p_x = locations % w p_y = locations // w pxy = list(zip(p_x, p_y)) pxy = np.array(pxy) return pxy 实现扰动函数cyclic。 def cyclic(r, ibxy): result = r * ibxy if result.any() < lb: result = result + (ub - lb) elif result.any() > ub: result = result - (ub - lb) result=result.clip(lb,ub) return result 初始化图像以及(px,py)集合。 ii = adv_img pxpy = random_locations() 从(px,py)集合中随机选择最多128个点,遍历每个点,针对每个点进行扰动并生成一个新的图像,得到一个新图像集合。对新图像集合中的每个图像进行计算,得到分类为原标签的概率值。 for try_time in range(r): #重新排序,随机选择不超过128个点 pxpy = pxpy[np.random.permutation(len(pxpy))[:128]] l = [pert(ii, p, x, y) for x, y in pxpy] #批量返回预测结果,获取原始图像标签的概率 def score(its): its = np.stack(its) its = unnormalize(its) scores=[ self.model.predict(unnormalize(ii))[original_ label] for it in its ] return scores 选择影响力最大的t个点组成新的(px,py)集合,np.argsort返回的是升序排列,因此需要取倒数t个,这里非常容易弄错。 scores = score(l) indices = np.argsort(scores)[:-t] pxpy_star = pxpy[indices] 遍历(px,py)集合,同时在原始图片上进行扰动。 for x, y in pxpy_star: #对每个颜色通道的[x,y]点进行扰动并截断,扰动算法即放大r倍 for b in range(channels): location = [x, y] location.insert(self.model.channel_axis(), b) location = tuple(location) ii[location] = cyclic(r, ii[location]) 针对新生成的图片进行预测,如果分类标签发生变化即说明攻击成功,反之继续。更新(px,py)集合,以每个像素点作为中心,画出边长为2d的正方形,正方形范围内的点都纳入集合(px,py),所谓的“本地搜索”也就体现在这里。 pxpy = [ (x, y) for _a, _b in pxpy_star for x in range(_a - d, _a + d + 1) for y in range(_b - d, _b + d + 1)] pxpy = [(x, y) for x, y in pxpy if 0 <= x < w and 0 <= y < h] pxpy = list(set(pxpy)) pxpy = np.array(pxpy) 6.4 本地搜索攻击ResNet模型 6.4 本地搜索攻击resnet模型 我们以foolbox为例,基于keras平台介绍如何使用本地搜索攻击resnet模型,代码路径为: https://github.com/duoergun0729/adversarial_examples/blob/master/code/ 6-localsearchattack.ipynb 首先加载需要使用的库文件,其中keras.applications.resnet50封装了keras下的resnet50模型。 import foolbox import keras import numpy as np from keras.applications.resnet50 import resnet50,decode_predictions 创建resnet50对象,由于imagenet下图像数据需要进行标准化,因此需要定义预处理参数,均值为[104,116,123]。 keras.backend.set_learning_phase(1) kmodel = resnet50(weights='imagenet') preprocessing = (np.array([104, 116, 123]), 1) fmodel = foolbox.models.kerasmodel(kmodel, bounds=(0, 255), preprocessing=preprocessing) 这里需要特别强调的是,需要设置为预测模式,因为某些层在预测和训练时表现不同,比如dropout层在预测模式时就不会随机丢失数据。set_learning_phase(0)代表训练模式,set_learning_phase(1)代表预测模式。 使用foolbox自带的测试图片,获取对应的图片数据和标签。 image, label = foolbox.utils.imagenet_example() 创建localsearchattack对象。因为keras的resnet50需要使用bgr格式,而模式图片是rgb格式,所以需要对图片进行格式转换。 attack = foolbox.attacks.localsearchattack(fmodel) adversarial = attack(image[:, :, ::-1], label) if adversarial is not none: adversarial_label=np.argmax(fmodel.predictions(adversarial)) print("adversarial label={},original label={}".format(adversarial_label,label)) else: print("fail to localsearchattack!") 图6-3分别展示了原始图像、对抗样本及两者之间的差异,可见两者的差异很小。 plt.figure() plt.subplot(1, 3, 1) plt.title('original') plt.imshow(image / 255) # division by 255 to convert [0, 255] to [0, 1] plt.axis('off') plt.subplot(1, 3, 2) plt.title('adversarial') plt.imshow(adversarial[:, :, ::-1] / 255) # ::-1 to convert bgr to rgb plt.axis('off') plt.subplot(1, 3, 3) plt.title('difference') difference = adversarial[:, :, ::-1] - image plt.imshow(difference / abs(difference).max() * 0.2 + 0.5) plt.axis('off') plt.show() 图6-3 原始图像和对抗样本的对比示意图 另外一方面,打印原始图像和对抗样本的标签值,分别为281和282。 adversarial label=281,original label=282 如果希望显示具体哪些点发生了变化,可以打印非零的像素。 print(np.where(difference!=0.0)) 6.5 迁移学习攻击算法 6.5 迁移学习攻击算法 在黑盒攻击中,迁移学习攻击算法是非常重要的一种攻击算法,它的基本思想是,结构类似的深度学习网络,在面对相同的对抗样本的攻击时,具有类似的表现。也就是说如果一个攻击样本可以攻击模型a,那么有一定的概率,可以用它攻击与模型a结构类似的模型b。在机器视觉领域,大量使用了结构类似的深度学习网络,并且大量知名的机器视觉领域的深度学习模型都已经开源,比如vgg、resnet等,这也为迁移学习提供了大量的资源。 如图6-4所示,以advbox/foolbox为例,假设要攻击模型b,并且我们并不知道模型b的具体细节,但是可以找到与模型b功能和结构类似的模型a。我们使用advbox/foolbox,基于测试数据对模型a进行白盒攻击,得到可以成功攻击模型a的对抗样本。之后我们再用这些攻击样本去攻击模型b,通过advbox/foolbox得到可以成功攻击模型b的对抗样本。 图6-4 基于advbox/foolbox的迁移学习攻击流程 安全研究人员yanpei liu、xinyun chen和dawn song等人在论文《delving into transferable adversarial examples and black-box attacks》中对迁移学习攻击算法进行了系统的阐述。如图6-5所示,横轴代表已知的模型a,纵轴代表被攻击的模型b,每格代表针对模型a生成的对抗样本可以被模型b正确识别的比例。可以看出,如果使用针对同一模型白盒攻击生成的对抗样本,已经无法被原模型正确识别,因此从左上角到右下角的对角线全部都是0%。这说明在白盒攻击模型中,构建对抗性图像的效果非常好,全部不能正确识别。当验证模型和构造模型并不一致时,大部分对抗性图像的百分比也在10%~40%浮动,该结果有效证明了对抗样本在不同算法之间有一定的迁移性,或者称对抗样本的传递性。其中rmsd是均方根偏差,是对抗样本相对原始数据的扰动量的平均值。 图6-5 对抗样本在不同图像识别模型之间的迁移性 虽然基于对抗样本的传递性可以构造出对抗样本,但是成功率并不能令人满意。基于此,yanpei liu、xinyun chen和dawn song等人提出了对抗样本领域的集成学习的方法,以多个深度神经网络模型为基础构造对抗样本。 假设已知k个模型,并且可以针对这k个模型进行白盒攻击。这k个模型的softmax层输出分别为j1,…,jk,原始图像为x,原始图像对应的分类标签为y,定向攻击的分类标签为y*,对抗样本为x*,集成学习的模型可以表示为: 其中i为集成学习的参数,并且有,那么生成对抗样本的问题就可以转换成一个优化问题,优化的目标就是损失函数: 损失函数主要由两部分组成,一个是各个模型分类结果与定向攻击的分类标签的差异,另一个是扰动量的大小。优化器使用adam,经过最多100轮迭代进行优化求解,反向传递调整的参数就是针对原始数据的扰动量。下面基于tensorflow介绍主要实现过程,论文中使用的工具核心文件位于: https://github.com/sunblaze-ucb/transferability-advdnn-pub/blob/master/ optimization.py 首先定义扰动量modifier,modifier是变量,可以用优化器调整其大小。batch_size代表批处理大小,spec.crop_size表示从原图中剪切的正方形图片的边长,spec.channels为图片通道数。 modifier = tf.variable( np.zeros( (batch_size,spec.crop_size,spec.crop_size,spec.channels), dtype=np.float32)) 定义输入的原始图像input_image和定向攻击的标签input_label。 input_image = tf.placeholder( tf.float32, (none, spec.crop_size, spec.crop_size, spec.channels)) input_label = tf.placeholder(tf.int32, (none)) true_image为原始图像叠加扰动后被截断后的数据,截断的上限和下限由spec.rescale[0]和spec.rescale[1]定义。 true_image = tf.minimum(tf.maximum(modifier + input_image, -spec.mean + spec.rescale[0]), -spec.mean + spec.rescale[1]) diff = true_image - input_image loss2表示对抗样本和原始数据之间的差异,损失函数包含loss2主要是为了让对抗样本尽可能地接近原始数据。 loss2 = tf.sqrt(tf.reduce_mean(tf.square(true_image - input_image))) 获得各个模型的softmax层输出true_label_prob。 probs, variable_set = models.get_model(sesh, true_image, model_name) true_label_prob = tf.reduce_mean( tf.reduce_sum( probs *tf.one_hot(input_label,1000),[1])) 定义整个损失函数,其中loss1为各个模型分类结果与攻击目标标签的差异,使用了1e-6是机器学习训练时的一个技巧,主要为了避免log函数处理0值导致异常。 loss1 = -tf.log(true_label_prob + 1e-6) loss = weight_loss1 * loss1 + weight_loss2 * loss2 如果是无定向攻击,损失函数的定义稍有变化。 loss1 = -tf.log(1 - true_label_prob + 1e-6) loss = weight_loss1 * loss1 + weight_loss2 * loss2 定义优化器,使用adam,优化对象为loss,调整的参数为modifier。 optimizer = tf.train.adamoptimizer(learning_rate) train = optimizer.minimize(loss, var_list=[modifier]) 实验结果如图6-6所示,一共有五种模型,每次攻击时都使用其中四种进行集成学习,然后攻击剩下的那种模型,可以明显看出攻击效果有了很大提升,仅有不超过6%的对抗样本可以被正确识别。 位于以下网址中的论文所使用到的实验工具也已经开源。 https://github.com/sunblaze-ucb/transferability-advdnn-pub 实验中使用的数据来自于imagenet 2012,可以通过该工具中的脚本进行下载。 cd scripts bash retrieve_data.sh 图6-6 基于集成学习算法的对抗样本攻击效果 可以通过以下命令进行测试,其中白盒攻击算法使用的是fgsm,被攻击对象目前仅支持googlenet,集成学习的模型目前支持resnet、vgg16和alexnet。 python fg_and_fgs.py -i test -o output/googlenet --model googlenet --file_list test/test_file_list.txt 主要的命令行参数含义如下: --input_dir: 测试数据目录 --output_dir: 对抗样本生成目录 --model: 黑盒攻击的对象,目前仅支持googlenet --file_list: 测试数据的文件列表 6.6 通用对抗样本 6.6 通用对抗样本 有这样一类对抗样本,它和原始图像没有关系,只与图像分类模型有关,只要是该图像分类模型可以识别的图片,叠加上该对抗样本,都有较大概率被识别错误。s.moosavi-dezfooli*、a.fawzi*、o.fawzi和p.frossard在论文《universal adversarial perturbations》中证明了通用对抗样本的存在,并且给出了计算方法。通用对抗样本事实上是一类精心构造的扰动,具有通用性,而且可以足够小,所以也被称为通用对抗扰动。 假设原始数据为x,模型的分类结果为,叠加的扰动为v。我们定义通用对抗扰动需要满足两个条件: ? 扰动足够小。 ? 扰动叠加到整个测试数据集上,可以较大概率欺骗分类模型。 用数学语言描述,p为范数,∈控制扰动的大小,?控制通用对抗扰动可以多大比例欺骗测试样本,假设原始数据的分布满足x~μ,那么有: ? ‖v‖p≤∈ ? 通用对抗扰动的获取是一个迭代求解的过程,通常需要准备一份测试数据,该测试数据需要覆盖全部分类类型,并且尽可能使各个分类的数量都比较均匀。如图6-7所示,遍历测试数据集,不断求解样本跨越决策平面的最小扰动,使用这个扰动不断优化通用对抗扰动。 图6-7 通用对抗扰动迭代求解过程 针对原始数据xi,求解跨越决策平面的最小扰动r,定义为: 定义投射操作: 那么使用最小扰动不断优化通用对抗扰动的操作可以表示为: 迭代求解通用对抗扰动v的过程可以用伪码描述为: 论文中给出非常详细的实验,以imagenet2012的数据集为例,在训练集上训练通用扰动,在校验集上验证效果,攻击模型为caffenet、vgg、googlenet和resnet,攻击成功率普遍达到80%以上,如图6-8所示。 图6-8 通用对抗扰动的迁移攻击效果 如图6-9所示,通用对抗扰动也具有良好的迁移性,攻击成功率普遍达到40%以上。 图6-9 通用对抗扰动的迁移攻击效果 论文中还指出,通用对抗扰动对分类器的欺骗具有一定的规律性,如图6-10所示,伪装成的对象具有一定的规律。 图6-10 通用对抗扰动倾向于让图片伪装成几种物体 通用对抗扰动的开源实现位于: https://github.com/lts4/universal 作者使用的数据集为imagenet2012,分类模型是resnet50,并且把训练好的通用对抗扰动也以universal.npy文件的形式发布出来了,运行demo程序,效果如图6-11所示,把陶盆识别为牛仔帽。 cd python python demo_inception.py -i data/test_img.png 图6-11 通用对抗扰动演示效果 6.7 针对MNIST生成通用对抗样本 6.7 针对mnist生成通用对抗样本 由于论文中使用的是imagenet2012数据集,压缩包大小都超过了100g,即使使用gpu资源也需要较长时间,下面我们以mnist为例介绍如何生成通用对抗扰动,代码路径为: https://github.com/duoergun0729/adversarial_examples/blob/master/code/ 6-universal.ipynb 首先加载需要使用的库文件,其中针对mnist数据集的封装都在tutorials.mnist中。 import tensorflow as tf import numpy as np from tensorflow.examples.tutorials.mnist import input_data from tensorflow.python.framework import graph_util from tensorflow.python.platform import gfile import os #mnist数据 mnist = input_data.read_data_sets("mnist_data/", one_hot=true) 加载mnist的预训练模型,本章使用的预训练模型在第2章介绍过,代码路径为: https://github.com/duoergun0729/adversarial_examples/blob/master/code/ 2-tensorflow.ipynb 加载pb文件,恢复预训练模型,其中最关键的三个tensor分别为: ? input:0,表示模型的输入,本例中mnist数据集输入的大小为[none,784],并且已经做了归一化,大小为[0,1]。 ? keep_prob:0,表示输入层和隐藏层之间保留的数据的比例,主要用于训练过程抵御过拟合,在训练阶段设置为0.75,预测阶段设置为1.0即可。 ? output:0,表示模型的输出,大小为[none,10],表示识别为标签0至9之间的概率分布。 #模型文件 pb_file_path="tensorflow_mnist_graph.pb" #预测用的会话 persisted_sess = tf.session() # 加载inception模型 with gfile.fastgfile(pb_file_path, 'rb') as f: graph_def = tf.graphdef() graph_def.parsefromstring(f.read()) persisted_sess.graph.as_default() tf.import_graph_def(graph_def, name='') persisted_sess.graph.get_operations() #记载输入和输出tensor persisted_input = persisted_sess.graph.get_tensor_by_name("input:0") persisted_keep_prob = persisted_sess.graph.get_tensor_by_name("keep_prob:0") persisted_output = persisted_sess.graph.get_tensor_by_name("output:0") 定义前向计算过程,即预测过程。 def f(image_inp,keep_prob=1.0): return persisted_sess.run(persisted_output, feed_dict={persisted_input: image_inp, persisted_keep_prob:keep_prob}) 在mnist的测试集上测试预训练模型的准确率以及错误率。 #批量验证测试集的准确率 from sklearn.metrics import accuracy_score test_x=mnist.test.images test_y=mnist.test.labels y_pred=np.argmax(f(test_x),axis=1) y_true=np.argmax(test_y,axis=1) print(accuracy_score(y_true, y_pred, normalize=true)) num_images=test_x.shape[0] fooling_rate = float(np.sum(y_pred != y_true) / float(num_images)) print('fooling rate = ', fooling_rate) 运行结果如下,可见预训练模型的准确率为98.19%,错误率为1.81%。 0.9819 fooling rate = 0.0181 定义投影函数,v表示迭代计算中的通用对抗扰动,p为范数,目前支持l2和无穷范数,xi即∈,控制扰动的大小。关于范数的定义可以参考1.1.5节的内容。 def proj_lp(v, xi, p): if p == 2: v = v * min(1, xi/np.linalg.norm(v.flatten(1))) elif p == np.inf: v = np.sign(v) * np.minimum(abs(v), xi) else: raise valueerror('values of p different from 2 and inf are currently not supported...') return v 迭代计算v,当欺骗率达到阈值或者达到最大迭代次数时退出。当发现当前样本叠加扰动v时没有欺骗模型,使用deepfool算法计算扰动dr,然后使用v+dr更新v,并且通过投影函数proj_lp让v的值同时也满足对范数的要求。deepfool算法的实现可以参考5.5节。 v = 0 fooling_rate = 0.0 num_images = np.shape(dataset)[0] print("x size:{}".format(num_images)) itr = 0 while fooling_rate < 1-delta and itr < max_iter_uni: #随机排列数据集 np.random.shuffle(dataset) print ('starting pass number ', itr) for k in range(0, num_images): cur_img = dataset[k:(k+1), :] if np.argmax(f(cur_img)) == np.argmax(f(cur_img+v)): #使用deepfool计算 dr,iter,_,_ = deepfool( cur_img + v, f, grads, num_classes=num_classes, overshoot=overshoot, max_iter=max_iter_df) v = v + dr v = proj_lp(v, xi, p) itr = itr + 1 使用测试集验证通用对抗样本的攻击效果,欺骗率达到90.26%。 y_pred=np.argmax(f(test_x+v),axis=1) y_true=np.argmax(test_y,axis=1) print(accuracy_score(y_true, y_pred, normalize=true)) num_images=test_x.shape[0] fooling_rate = float(np.sum(y_pred != y_true) / float(num_images)) print('fooling rate = ', fooling_rate) 6.8 本章小结 6.8 本章小结 本章介绍了黑盒攻击中经典的单像素攻击和本地搜索攻击,并分别介绍了如何使用单像素攻击和本地搜索攻击生成对抗样本。最后介绍了使用最广泛的迁移学习攻击算法,并结合经典的resnet、vgg16和alexnet详细介绍了迁移学习的攻击效果。 7.1 目标检测的概念 7.1 目标检测的概念 目标检测简单来讲就是目标识别和目标定位,在《web安全之机器学习入门》和《web安全之深度学习实战》中,我们介绍了如何使用深度学习识别手写数字,如何使用tensorflow或者keras平台预训练好的模型,识别小猪和猫。如图7-1所示,在一个典型的基于卷积神经网络的目标识别任务中,通过卷积、池化、全连接等层的计算,把一个输入的图片最终识别为猫。 图7-1 典型的目标识别任务 目标检测任务除了要完成图像的识别,还需要能够定位物体的位置。如图7-2所示,目标检测模型不光可以识别图中的猫,还可以准确地使用方框从背景中框出猫的范围。 图7-2 典型的目标检测任务 最典型的标记物体的范围就是使用方框,把方框左上角的原点的坐标记为(x,y),方框的宽为w,高为h,那么整个方框的坐标为(x,y,w,h),如图7-3所示。 图7-3 使用方框标记物体范围 在实际使用中,如图7-4所示,需要从背景图片中识别出多个物体,这个可以当作单物体识别的能力延伸。 图7-4 从背景图片中检测多个物体 在对物体定位精度要求不高的领域,基于方框的物体定位方式很好地表示出了物体的范围和位置。但是在部分领域,比如智能驾驶、智能安防领域,需要能够以更高的精度来表示物体的范围和位置。如图7-5所示,图像语义分割就可以非常准确地勾画出物体的轮廓,并识别出图像中的内容,标注出人和车的范围。 图7-5 典型的图像语义分割 7.2.1 车道偏离预警 7.2.1 车道偏离预警 如图7-6所示,在车道偏离预警方面,智能驾驶系统能够识别不同的车道线,在车辆偏离车道时进行报警提醒。在高速行驶、自动巡航时,这一功能非常实用。 图7-6 车道偏离预警 7.2.2 前向防碰撞预警 7.2.2 前向防碰撞预警 在拥挤的城市中驾驶时,追尾是最容易发生的交通事故之一。如图7-7所示,基于目标检测的前向防碰撞预警,就是利用前置的摄像头,识别前方的车辆并结合其他传感器进行综合判断。基于目标检测的前向防碰撞预警,有单目视觉和双目视觉两种。单目视觉采用摄像机的焦距和事先确定的参数来估算车距,而双目视觉测距是利用视差的原理,通过对两幅图像进行计算机分析和处理,确定物体的三维坐标。 图7-7 前向防碰撞预警 7.2.3 交通标志识别 7.2.3 交通标志识别 识别交通标志是智能驾驶最基础的功能之一,也是对抗样本最早应用到物理攻击的一个场景。通常智能驾驶会根据高精地图、交通标志识别以及雷达传感器的数据进行综合判断,但是由于城市建设地图未及时更新、临时交通管制等因素,交通标志识别的结果的优先级通常会高于其他传感器,因此识别交通标志的功能一旦出现问题,造成的影响不容小觑。有安全研究者通过实验证明,可以在一个stop的路牌上增加不明显的贴纸,最终让智能驾驶系统识别为60公里限速,如图7-8所示。 7.2.4 行人防碰撞预警系统 7.2.4 行人防碰撞预警系统 传统的行人防碰撞系统依赖于车载雷达等传感器,在小区、拥挤街道等复杂路况下,车载雷达难以处理。基于目标检测的行人防碰撞系统,结合传统传感器,可以最大限度地提高智能驾驶系统检测行人的能力,如图7-9所示。 图7-8 识别交通标志 图7-9 行人防碰撞预警系统 以某款智能驾驶车辆为例,它依靠180m距离的毫米波雷达和摄像头共同作用,可以探测身高为80cm以上的行人,在车速低于35km/h时,如果有行人进入车辆的行进路线,车辆会进行全力刹车,尽可能避免或者减小事故发生的概率,若是行驶速度高于35km/h,车辆也会自动减速来规避风险,该系统能够同时识别10个行人。 7.2.5 驾驶员疲劳监测预警 7.2.5 驾驶员疲劳监测预警 智能驾驶系统除了可以检测车外的危险,还可以发现车内的安全隐患,它通过摄像头拍摄驾驶员面部动态进行识别,在驾驶员打瞌睡、打电话等危险驾驶行为出现时发出警报。比如通过检测瞳孔状况并通过数据分析来判断驾驶员疲劳状态,一旦驾驶员处于疲劳、瞌睡状态该系统就会对驾驶员做出提前预警,例如发出警报、收缩安全带、震动座椅、在仪表盘上发出“咖啡杯”警示符号等,如图7-10所示。 图7-10 驾驶员疲劳监测预警 7.2.6 自动泊车 7.2.6 自动泊车 利用摄像头和其他传感器对车身四周进行动态的观察,是实现自动泊车的基本技术之一,如图7-11所示。 图7-11 自动泊车 7.3.1 人脸检索 7.3.1 人脸检索 2018年9月,张学友世界巡回演唱会石家庄站抓获涉嫌故意毁坏财物案的齐某某、涉嫌贩毒的宋某某和申某某共三名在逃人员。据悉,此前张学友在金华、南昌、嘉兴、赣州等地的演唱会中警方均抓获过在逃人员。 人脸检索是目标检测在智能安防领域的一大应用。通过处理监控视频数据,把视频中的人脸与逃犯数据库进行对比,可以及时发现逃犯,维护公共安全,如图7-12所示。 图7-12 人脸检索 7.3.2 行为识别 7.3.2 行为识别 如图7-13所示,现在基于目标识别的行人闯红灯自动抓拍系统也越来越普遍。 图7-13 行人闯红灯自动识别抓拍系统 如图7-14所示通过分析图像以及视频识别人物行为,匹配预先定义的高危动作,是智能安防的另外一个重点功能。 图7-14 基于视频的行为识别 7.4.1 Soble边缘检测 7.4.1 soble边缘检测 图像的彩色信息在进行边缘检测时通常是多余的,因此可以在进行边缘检测前先把彩色图像转换成灰度图像。直观的感觉,灰度图像中的边缘一定是变化相对比较剧烈的区域,那么这个区域灰度值的一阶导数便是取极大值或者极小值。 图7-15 小猪佩奇铅笔画 soble边缘检测(见图7-16)正是基于这一原理,在opencv中的函数定义如下: sobel(src, ddepth, dx, dy, ksize=none, scale=none, delta=none, bordertype=none) 其中主要的参数如下。 ? ddepth:深度类型,通常用cv2.cv_64f表示64位浮点数即64 float。 ? dx:x方向的导数,1表示取导,0表示不取导。 ? dy:y方向的导数,1表示取导,0表示不取导。 ? ksize:卷积核大小。 图7-16 soble边缘检测示意图 首先加载需要的库文件: import cv2 from matplotlib import pyplot as plt import numpy as np 加载测试图片(见图7-17a),并转换成灰度图像: img=cv2.imread("../picture/smallpig.jpeg") gray_img=cv2.cvtcolor(img, cv2.color_rgb2gray) 对图像进行边缘检测(见图7-17b): #sobel边缘检测 sobel = cv2.sobel(gray_img,cv2.cv_64f,0,1) 为了显示方便,对图像进行反转,黑色和白色会颠倒(见图7-17c),后面的案例默认都会进行翻转显示。 notsobel=cv2.bitwise_not(sobel) 图7-17 soble边缘检测示例(一) 需要指出的是,当dx设置为1表示在x轴方向上进行边缘检测,当dy设置为1表示在y轴方向上进行边缘检测。如图7-18所示,一共有四个子图,子图a是原始图像,子图b对y轴方向取导,仅在y轴方向的边缘会被忽略,子图c对x轴方向取导,仅在x轴方向的边缘会被忽略,子图d对x轴和y轴两个方向同时取导,在x轴和y轴方向的边缘都会被忽略。 img=cv2.imread("../picture/sobel.jpeg") gray_img=cv2.cvtcolor(img, cv2.color_rgb2gray) #sobel边缘检测 sobel01 = cv2.sobel(gray_img,cv2.cv_64f,0,1) #对二值图像进行反转,黑白颠倒 sobel01=cv2.bitwise_not(sobel01) #sobel边缘检测 sobel10 = cv2.sobel(gray_img,cv2.cv_64f,1,0) #对二值图像进行反转,黑白颠倒 sobel10=cv2.bitwise_not(sobel10) #sobel边缘检测 sobel11 = cv2.sobel(gray_img,cv2.cv_64f,1,1) #对二值图像进行反转,黑白颠倒 sobel11=cv2.bitwise_not(sobel11) 图7-18 soble边缘检测示例(二) 7.4.2 拉普拉斯边缘检测 7.4.2 拉普拉斯边缘检测 基于一阶导数的soble效果并不理想,人们在其基础上提出了取对应的二阶导数为0的点为边缘。拉普拉斯边缘检测正是利用了这一原理,如图7-19所示。 图7-19 拉普拉斯边缘检测示意图 拉普拉斯边缘检测在opencv中的函数定义如下: laplacian(src, ddepth, ksize=none, scale=none, delta=none, bordertype=none) 其中主要的参数为: ? ddepth:深度类型。 ? ksize:卷积核大小。 如图7-20所示,一共有三个子图,子图a是原始图像,子图b使用了soble算法,子图c使用了拉普拉斯算法。 img=cv2.imread("../picture/smallpig.jpeg") gray_img=cv2.cvtcolor(img, cv2.color_rgb2gray) #拉普拉斯边缘检测 lap = cv2.laplacian(gray_img,cv2.cv_64f)#拉普拉斯边缘检测 lap = np.uint8(np.absolute(lap))##对lap去绝对值 #对二值图像进行反转,黑白颠倒 lap=cv2.bitwise_not(lap) #sobel边缘检测 sobel = cv2.sobel(gray_img,cv2.cv_64f,0,1) #对二值图像进行反转,黑白颠倒 sobel=cv2.bitwise_not(sobel) 图7-20 拉普拉斯边缘检测示例 相对soble,拉普拉斯对边缘的识别效果更好,但是拉普拉斯对噪声敏感,会产生双边效果,不能检测出边的方向,通常不直接用于边的检测,只起辅助的角色。 7.4.3 Canny边缘检测 7.4.3 canny边缘检测 canny边缘检测是使用最广泛的边缘检测算法之一,可以有效减少噪音的影响,同时边缘处理的效果也不错。 canny边缘检测算法可以分为以下5个步骤: 1)使用高斯滤波器,以平滑图像,滤除噪声。 2)计算图像中每个像素点的梯度强度和方向。 3)应用非极大值抑制,以消除边缘检测带来的杂散响应。 4)应用双阈值检测来确定真实的和潜在的边缘。 5)通过抑制孤立的弱边缘最终完成边缘检测。 canny算法应用双阈值的技术,即设定一个阈值上界和阈值下界,图像中的像素点如果大于阈值上界则认为必然是边缘,小于阈值下界则认为必然不是边缘,两者之间的则认为是候选项,须进行进一步处理。canny的具体原理这里不再展开,有兴趣的读者可以阅读相关文献。 canny边缘检测在opencv中的函数定义如下: canny(image, threshold1, threshold2, edges=none, aperturesize=none, l2gradient=none) 其中主要的参数如下。 ? threshold1:阈值1,即低阈值。 ? threshold2:阈值2,即高阈值,低阈值用来控制边缘连接,高阈值用来控制强边缘的初始分割,低于低阈值的会被判定为不是边缘,高于高阈值的会被判定为边缘,中间区间的会通过计算判断是否为边缘。 如图7-21所示,一共有四个子图,子图a是原始图像,子图b在原始图像上叠加高斯噪声,子图c对混有高斯噪声的图像进行拉普拉斯边缘检测,子图d对混有高斯噪声的图像进行canny边缘检测。可见拉普拉斯边缘检测容易受到噪声干扰,canny边缘检测对噪声的干扰具有一定抵御能力。 img=cv2.imread("../picture/smallpig.jpeg") img=cv2.cvtcolor(img.copy(), cv2.color_bgr2gray) #高斯噪声 noise_img=skimage.util.random_noise(img.copy(), mode="gaussian", seed=none, clip=true,mean=0,var=0.0016) noise_img=np.uint8(noise_img*255) #拉普拉斯边缘检测 lap = cv2.laplacian(noise_img,cv2.cv_64f)#拉普拉斯边缘检测 lap = np.uint8(np.absolute(lap))##对lap去绝对值 #对二值图像进行反转,黑白颠倒 lap=cv2.bitwise_not(lap) #canny边缘检测 canny = cv2.canny(noise_img,150,300) #对二值图像进行反转,黑白颠倒 canny=cv2.bitwise_not(canny) 图7-21 canny边缘检测示例 7.5 直线检测算法 7.5 直线检测算法 在解决机器视觉方面的任务时,经常会涉及某些简单的直线、圆形、椭圆形的检测。在多数情况下,边缘检测算法会先做图片预处理,将原始图片变成只含有边缘的图片。因为图片不完美或边缘检测不完美,会导致一些像素缺漏,或是有噪声使得边缘检测算法所得的边界偏离了实际的边界。霍夫变换通过投票步骤,比较好地解决了上述问题。 以霍夫变换检测直线为例,在典型的直角坐标系中通过水平的x轴和垂直的y轴的坐标来描述点,直线方程可以表示为: y=kx+b 其中k称为斜率,b称为截距或者偏移,通过k和b可以唯一确定一条直线,或者说通过(k,b)可以唯一确定一条直线。 但是当直线平行y轴时,斜率不存在,这个时候通过k和b就无法表示该直线了,我们需要另外一个方式描述一条直线,如图7-22所示。 图7-22 通过r和描述直线 假设原点到直线做一条垂线段,垂线段的长度为原点到该直线的距离记为r,角度记为θ,通过(r,θ)可以唯一确定一条直线,并且存在以下关系: r=x cosθ+y sinθ 在极坐标中,r为纵轴,θ为横轴,该直线可以表示为其中的一个点(θ,r)。我们把r为纵轴,θ为横轴的空间称为霍夫空间,也就是说可以把原有的图像空间中的一条直线转换成霍夫空间的一个点。 在图像空间中,通过某一点(a,b)的直线有无数条,对应到霍夫空间,满足如下关系: r=a cosθ+b sinθ 由平面几何知识可知,点(θ,r)组成了一条曲线。假设a和b均为1,θ取值为0到π,那么: r=cosθ+sinθ 如图7-23所示,在霍夫空间画出该曲线。换言之,原有的图像空间中通过某一个点的全部直线转换成了霍夫空间的一条曲线。 import numpy as np import matplotlib.pyplot as plt o=np.arange(0,np.pi,0.01) r=np.cos()+np.sin() plt.xlabel('') plt.ylabel("r") plt.plot(,r) plt.show() 图7-23 霍夫空间中r和的关系 回到直线检测的问题,在图像空间中判断若干点是否在同一直线的问题,可以转换成霍夫空间中对应的多条曲线是否能相交于一点的问题。 假设图像平面中存在三个点(3,5),(6,8)和(7,9),我们想判断这三点是否在一条直线上,那么可以转换到霍夫空间,如图7-24所示,转换成霍夫空间平面中三条曲线是否相交于一点的问题。我们把边缘检测算法识别出的边缘所在点转换到霍夫空间,并在霍夫空间中对应的点进行加1操作,即给该点投票,当有一个点超过设定阈值票数时,我们就可以认定其为直线。这里需要指出的是,为了计算方便,霍夫空间中的θ取固定间隔的几个离散值。 =np.arange(0,np.pi,0.01) r1=3*np.cos()+5*np.sin() r2=6*np.cos()+8*np.sin() r3=7*np.cos()+9*np.sin() plt.xlabel('') plt.ylabel("r") plt.plot(,r1) plt.plot(,r2) plt.plot(,r3) plt.show() 图7-24 在图像空间中判断若干点是否在同一直线的问题 基于霍夫变换的直线检测在opencv中的函数定义如下: houghlines(image, rho, theta, threshold[, lines[, srn[, stn]]]) 其中主要的参数如下。 ? rh:以像素为单位的累加器的距离分辨率,通常取1。 ? theta:在弧度内的蓄能器的角度分辨率,通常取np.pi/180。 ? threshold:当累加器超过该阈值才看作是直线。 下面我们演示如何检测跑道,相应的示例代码位于: https://github.com/duoergun0729/adversarial_examples/blob/master/code/10-case2.ipynb 首先我们加载测试图片并转换成灰度图像,便于canny进行边缘检测,同时构造一个纯白的背景便于显示检测出的直线。 img=cv2.imread("../picture/road.jpg") gray_img=cv2.cvtcolor(img, cv2.color_rgb2gray) show_img=gray_img.copy() #canny边缘检测 edges = cv2.canny(gray_img, 150, 300) #对二值图像进行反转,黑白颠倒 notedges=cv2.bitwise_not(edges) #构造纯白背景 clean_img=np.ones_like(show_img) 然后运行直线检测算法,其中为了限制检测的直线的长度,设置阈值为100。如图7-25所示,一共有四个子图,子图a是原始图像,子图b是使用了canny算法进行了边缘检测,子图c是识别出的直线,子图d是在原始图像上画上了识别出的直线。 #检测直线 lines = cv2.houghlines(edges, 1, np.pi/180, 100) for line in lines: rho, theta = line[0] a = np.cos(theta) b = np.sin(theta) x0 = a * rho y0 = b * rho x1 = int(x0+1000*(-b)) y1 = int(y0+1000*(a)) x2 = int(x0-1000*(-b)) y2 = int(y0-1000*(a)) cv2.line(show_img, (x1, y1), (x2, y2), (0, 0, 0), 2) cv2.line(clean_img, (x1, y1), (x2, y2), (0, 0, 0), 2) 图7-25 直线检测示例 7.6 圆形检测算法 7.6 圆形检测算法 基于霍夫变换的圆形检测的原理与直线检测类似,都是把图像空间中的问题转换到霍夫空间中去解决。图像空间中圆心为点(a,b),半径为r的圆,圆上任意一点可以表示为: (x–a)2+(y–b)2=r2 在霍夫空间中,设横坐标为a,纵坐标为b,那么通过图像空间中点(x0,y0)的圆可以表示为: (a–x0)2+(b–y0)2=r2 图像空间中一个圆形边界上有很多个点,对应到霍夫空间中就会有很多个圆。由于原图像中这些点都在同一个圆形上,那么转换后a,b必定也满足霍夫空间所有圆形的方程式。直观表现为许多点对应的圆都会相交于一个点,那么这个交点就是圆心(a,b)。在实现过程中,可以设置r的范围逐渐迭代计算,减小计算复杂度。 基于霍夫变换的圆形检测在opencv中的函数定义如下: houghcircles(image, method, dp, mindist[, circles[, param1[, param2[, minradius[, maxradius]]]]]) 其中主要的参数如下。 ? method:使用检测方法。目前唯一实现的方法是cv_hough_gradient。 ? dp:累加器分辨率与图像分辨率的反比,通常取1。 ? mindist:检测到的圆的中心之间的最小距离。如果参数太小,误报会高,如果太大漏报会高。 ? param1:canny的阈值1。 ? param2:canny的阈值2,霍夫变换的圆形检测会先使用canny进行边缘检测,因此需要设置canny相关阈值。 ? minradius:最小圆半径。 ? maxradius,最大圆半径。 下面我们演示如何检测路牌,相应的示例代码位于: https://github.com/duoergun0729/adversarial_examples/blob/master/code/10- case3.ipynb 首先我们加载测试图片,并转换成灰度图像。 img=cv2.imread("../picture/sign.jpeg") gray_img=cv2.cvtcolor(img, cv2.color_rgb2gray) show_img=gray_img.copy() 虽然基于霍夫变换的圆形检测本身就会使用canny边缘检测,但是为了展现这一过程,同时便于设置canny相关阈值,我们也使用canny手工进行边缘检测。 #canny的阈值 param1=50 param2=100 #canny边缘检测 edges = cv2.canny(gray_img, param1, param2) #对二值图像进行反转,黑白颠倒 edges=cv2.bitwise_not(edges) #构造空白图像 clean_img=np.ones_like(show_img) 最后使用霍夫变换进行圆形检测,其中设置检测的圆的半径在20到60之间。如图7-26所示,一共有四个子图,子图a是原始图像,子图b是使用canny算法进行了边缘检测,子图c是识别出的圆形,子图d是在原始图像上画上了识别出的圆形,可见达到了识别圆形路牌的目的。 # 霍夫圆变换 circles = cv2.houghcircles(gray_img,cv2.hough_gradient,1, 80,param1=param1,param2=param2,minradius=20,maxradius=60) # 将检测的圆画出来 for i in circles[0, :]: cv2.circle(show_img, (i[0], i[1]), i[2], (0, 255, 0), 2) cv2.circle(clean_img, (i[0], i[1]), i[2], (0, 255, 0), 2) 图7-26 圆形检测示例 7.7.1 RCNN 7.7.1 rcnn 介绍rcnn之前先介绍几个目标检测领域的常用概念。 region proposals,即推荐框,如图7-27所示,目标检测算法通常会在图片中识别出物体的轮廓,并用一个长方形表示物体的范围。 图7-27 region proposals示例 ground truth,即标定过的真实数据,指的是人工标记的物体的真实范围。 detection result,即模型检测出的物体的范围。 iou,如图7-28所示,可以理解为系统预测出来的框与原来图片中标记的框的重合程度。计算方法即detection result与ground truth的交集比上它们的并集。 rcnn的预测阶段的基本流程如图7-29所示,分为三个步骤。 ? 产生2000个不依赖于特定类别的region proposals ? 通过卷积神经网络,对每个region proposals先转换成固定的大小227x227,产生一个固定长度的特征向量,特征向量的大小通常为4096维。 ? 通过一系列特定类别的线性svm分类器,得出物体分类结果。 这里需要指出的是,rcnn为每个类别都构造了一个svm的二分类器。比如要识别小猪,就把训练集中ground truth为小猪的作为正样本,iou大于0.5的均为正样本,iou小于0.3的均为负样本,负样本有可能是背景,也有可能包含了其他目标。svm的输入为卷积神经网络提取的固定长度的特征向量。 图7-28 iou示意图 图7-29 rcnn流程图 为了让预测的iou尽量大,也就是说预测的物体的范围更加准确,还需要使用回归算法对预测的detection result进行微调,尽可能接近ground truth。 从上述过程可以看到,rcnn需要训练多个svm分类器,也要训练多个回归器,训练的数据都是很高的维度(4096维),并且整个过程都是串行进行的,因此rcnn训练效率非常低。即使是在预测阶段,也需要在图像上产生2000个region proposals,然后针对每个region proposals进行卷积计算,在多个svm分类器上进行预测,因此预测性能也很差。科研人员使用gpu测试,平均每张图片的处理延时都达到了13秒,在cpu上甚至达到53秒。 7.7.2 Fast RCNN 7.7.2 fast rcnn rcnn最花费时间的阶段就是要针对每个region proposals经过cnn计算。fast rcnn创新地把cnn阶段提前,整张图片仅进行一次cnn计算,然后再计算region proposals。经过研究人员测试,在gpu环境下每张图片的处理延迟可以控制在1秒以内。rcnn与fast rcnn流程对比如图7-30所示。 图7-30 rcnn与fast rcnn流程对比图 7.7.3 Faster RCNN 7.7.3 faster rcnn ross b.girshick在rcnn和fast rcnn的基础上,提出了新的faster rcnn(见图7-31)。faster rcnn将特征抽取、region proposals提取、物体分类都整合在了一个网络中,使得综合性能有了较大提高,在检测速度方面尤为明显,gpu环境下简单物体检测任务达到了每秒17张图片。 图7-31 faster rcnn流程简化图 值得一提的是,faster rcnn把输入的大小为(224,224,3)的图片,经过cnn处理,提取特征的维度为51x39x256。可以把该特征看成一个尺度51x39的256通道图像,对于该图像的每一个位置,考虑9个可能的region proposals,如图7-32所示。在faster rcnn中region proposals也称为anchor。物体的分类结果取决于region proposals和图像数据,因此faster rcnn也可以理解为先检测出region proposals,然后再检测出物体分类,整个物体检测阶段还是分为了两阶段。这个是faster rcnn和后面将要介绍的yolo和ssd最大的区别,后者region proposals和物体分类结果是在一起生成的。 图7-32 faster rcnn候选区域示意图 7.7.4 TensorFlow目标检测库 7.7.4 tensorflow目标检测库 tensorflow中提供了目标检测算法的实现以及预训练模型,依赖的软件环境为: ? protobuf 3.0.0 ? python-tk ? pillow 1.0 ? lxml ? tf slim(包含在tensorflow/models/research/中) ? jupyter notebook ? matplotlib ? tensorflow (>=1.9.0) ? cython ? contextlib2 ? cocoapi 安装过程为,首先同步tensorflow的模型库。 git clone https://github.com/tensorflow/models.git 推荐安装路径为: adversarial_examples/code/models/models 然后安装protobuf工具,如果是linux环境,则执行以下命令。 # from tensorflow/models/research/ wget -o protobuf.zip https://github.com/google/protobuf/releases/download/v3.0.0/protoc-3.0.0- linux-x86_64.zip unzip protobuf.zip # from tensorflow/models/research/ ./bin/protoc object_detection/protos/*.proto --python_out=. 如果是mac环境,则执行以下命令或者直接执行brew install protobuf。 curl -ol https://github.com/google/protobuf/releases/download/v3.3.0/$protoc_zip sudo unzip -o $protoc_zip -d /usr/local/bin/protoc rm -f $protoc_zip # from tensorflow/models/research/ protoc object_detection/protos/*.proto --python_out=. 把当前库添加到系统环境变量中。 # from tensorflow/models/research/ export pythonpath=$pythonpath:`pwd`:`pwd`/slim 执行测试工具,验证安装是否成功。 # from tensorflow/models/research/ python object_detection/builders/model_builder_test.py run 22 tests in 0.145s ok tensorflow提供了大量的目标检测模型的实现以及预训练参数。 https://github.com/tensorflow/models/blob/master/research/object_detection/ g3doc/detection_model_zoo.md tensorflow提供的目标检测模型如图7-33所示。 图7-33 tensorflow提供的目标检测模型 tensorflow提供的目标检测模型的预训练参数主要基于mscoco数据集(也被简称coco数据集)进行训练。mscoco数据集是微软团队提供的一个可以用来完成机器视觉任务的数据集,其官方说明网址为http://mscoco.org/。mscoco数据集经典图片示例如图7-34所示。 下面以faster rcnn为例介绍tensorflow的目标检测模型的预训练参数,下载地址为:http://download.tensorflow.org/models/object_detection/faster_rcnn_resnet101_coco_2018_01_28.tar.gz。 解压到adversarial_examples/code/models: # adversarial_examples/code/models/faster_rcnn_resnet101_coco_2018_01_28 tar -zxvf faster_rcnn_resnet101_coco_2018_01_28.tar.gz 图7-34 mscoco数据集经典图片示例 查看目录下文件,其中frozen_inference_graph.pb为模型文件,包含模型的结构和参数信息: checkpoint model.ckpt.meta frozen_inference_graph.pb pipeline.config model.ckpt.data-00000-of-00001 saved_model model.ckpt.index 另外还有一个非常重要的文件,即mscoco数据集标签和物体名称的对应关系。 models/research/object_detection/data/mscoco_label_map.pbtxt 也可以直接下载并使用。下载网址为:https://github.com/tensorflow/models/blob/master/research/object_detection/data/mscoco_label_map.pbtxt。 另外,tensorflow的目标检测库官方推荐使用1.9及以上版本的tensorflow来运行,出于性能考虑也建议使用gpu版本。使用tensorflow gpu高版本时需要升级cuda库,下面我们介绍如何升级tensorflow需要使用的cuda库。 首先登录nvidia官网,网址为:https://developer.nvidia.com/cuda-downloads。 根据你的操作系统选择对应的cuda版本,如图7-35所示,我们的gpu服务器是linux x86_64的ubuntu 16,选择通过网络安装。 然后如图7-36所示,执行提示的相关升级命令安装cuda 10即可。 sudo dpkg -i cuda-repo-ubuntu1604-10-0-local-10.0.130-410.48_1.0-1_amd64.deb sudo apt-key add /var/cuda-repo-/7fa2af80.pub sudo apt-get update sudo apt-get install cuda 图7-35 升级cuda时选择平台 图7-36 升级cuda 10时执行相关升级命令 如果想安装cuda 9可以使用以下命令。 sudo dpkg -i cuda-repo-ubuntu1604_9.0.176-1_amd64.deb sudo apt-key adv --fetch-keys http://developer.download.nvidia.com/compute/cuda/repos/ubuntu1604/ x86_64/7fa2af80.pub sudo apt-get update sudo apt-get install cuda-9-0 查看当前安装的cuda的命令如下: cat /usr/local/cuda/version.txt 如果安装的是cuda 9,显示的内容如下: cuda version 9.0.176 升级cudnn到7,升级页面如下:https://developer.nvidia.com/rdp/cudnn-download。 下载对应的版本安装。 sudo dpkg -i libcudnn7_7.4.1.5-1+cuda10.0_amd64.deb sudo dpkg -i libcudnn7-dev_7.4.1.5-1+cuda10.0_amd64.deb 查看cudnn版本,已经升级到7。 cat /usr/include/cudnn.h | grep cudnn_major -a 2 #define cudnn_major 7 #define cudnn_minor 4 #define cudnn_patchlevel 1 推荐使用cuda 9和cudnn 7,安装最新的tensorflow-gpu版本1.11。 pip install tensorflow-gpu==1.11 7.7.5 Faster RCNN使用示例 7.7.5 faster rcnn使用示例 下面介绍如何使用tensorflow中提供的faster rcnn模型。首先需要加载使用到的库文件。 import numpy as np import os import sys import tensorflow as tf from distutils.version import strictversion from collections import defaultdict from io import stringio from matplotlib import pyplot as plt from pil import image 为了提高可移植性,需要在代码里手工指定tensorflow提供的目标检测模型的库文件到系统环境变量中。 # 把需要使用的代码库添加到系统路径中 sys.path.append("..") sys.path.append("models/models/research/") sys.path.append("models/models/research/slim/") sys.path.append("models/models/research/object_detection/") from object_detection.utils import ops as utils_ops faster rcnn需要1.9及以上版本的tensorflow,在代码里进行简单的版本判断。 if strictversion(tf.__version__) < strictversion('1.9.0'): raise importerror('please upgrade your tensorflow installation to v1.9.* or later!') 定义模型文件的相关路径,包括物体标签和物体名称之间的映射关系表。 path_to_frozen_graph = 'models/faster_rcnn_resnet101_coco_2018_01_28/frozen_inference_graph.pb' path_to_labels = 'models/models/research/object_detection/data/mscoco_label_map.pbtxt' 加载pb格式的模型文件,初始化tensorflow的计算图和会话。tensorflow把模型的定义以及模型的参数都保存在pb文件中,加载一次即可。 detection_graph = tf.graph() with detection_graph.as_default(): od_graph_def = tf.graphdef() with tf.gfile.gfile(path_to_frozen_graph, 'rb') as fid: serialized_graph = fid.read() od_graph_def.parsefromstring(serialized_graph) tf.import_graph_def(od_graph_def, name='') 加载物体标签和物体名称之间的映射关系表。 category_index = label_map_util.create_category_index_from_labelmap(path_to_labels, use_display_name=true) 定义需要加载的测试图片的列表,加载的策略是把指定文件夹下的全部文件当作测试图片。 import glob path_to_test_images_dir = '../picture/objectdetect/*' test_image_paths = glob.glob(path_to_test_images_dir) 定义需要使用的tensor,tensorflow中可以通过tensor的名称从计算图中直接获得指定tensor的引用,其中最主要的几个tensor的作用如下。 ? num_detections:检测到的物体的个数。 ? detection_boxes:检测到的region proposals,或者称为bbox,形状为[n,4]。 ? detection_scores:检测到的物体分类结果,形式为概率值,形状为[n]。 ? detection_classes:检测到的物体分类结果,形式为标签id,形状为[n]。 ? detection_masks:检测到物体的掩码。 ? image_tensor:输入的图像。 ops = tf.get_default_graph().get_operations() all_tensor_names = {output.name for op in ops for output in op.outputs} tensor_dict = {} for key in [ 'num_detections', 'detection_boxes', 'detection_scores', 'detection_classes', 'detection_masks' ]: tensor_name = key + ':0' if tensor_name in all_tensor_names: tensor_dict[key] = tf.get_default_graph().get_tensor_by_name(tensor_name) image_tensor = tf.get_default_graph().get_tensor_by_name('image_tensor:0') 需要指出的是,tensorflow中的tensor在名称后都有编号,通常获取时加上“:0”即可。调试时可以通过打印全部tensor名称all_tensor_names帮助理解。 如图7-37所示,运行计算图,输入图像并通过指定的tensor获取对应的输出。 # 进行预测 output_dict = sess.run(tensor_dict, feed_dict={image_tensor: np.expand_dims(image, 0)}) # 由于默认输出是浮点型,需要转换成整数型 output_dict['num_detections'] = int(output_dict['num_detections'][0]) output_dict['detection_classes'] = output_dict[ 'detection_classes'][0].astype(np.uint8) output_dict['detection_boxes'] = output_dict['detection_boxes'][0] output_dict['detection_scores'] = output_dict['detection_scores'][0] if 'detection_masks' in output_dict: output_dict['detection_masks'] = output_dict['detection_masks'][0] 图7-37 faster rcnn的计算图 依次加载测试图片文件夹下的图片,根据预测结果进行可视化,设置最多显示5个物体且只显示概率大于80%的物体,如图7-38所示。 for image_path in test_image_paths: image = image.open(image_path) image_np = load_image_into_numpy_array(image) output_dict = run_inference_for_single_image(image_np, detection_graph) vis_util.visualize_boxes_and_labels_on_image_array( image_np, output_dict['detection_boxes'], output_dict['detection_classes'], output_dict['detection_scores'], category_index, instance_masks=output_dict.get('detection_masks'), use_normalized_coordinates=true, #设置最多显示5个物体 max_boxes_to_draw=5, #只显示概率大于80%的物体 min_score_thresh=0.8, line_thickness=2, skip_scores=true, skip_labels=false 图7-38 使用tensorflow的faster rcnn检测结果可视化 tensorflow提供了一个用于目标检测结果可视化的api,在原始图像上使用方框显示图像分类的结果以及物体的范围。 vis_util.visualize_boxes_and_labels_on_image_array 其中主要参数含义如下。 ? image:原始图像。 ? boxes:检测到的region proposals,或者称为bbox,形状为[n,4]。 ? scores:检测到的物体分类结果,形式为概率值,形状为[n]。 ? classes:检测到的物体分类结果,形式为标签id,形状为[n]。 ? category_index:物体标签和物体名称之间的映射关系表。 ? max_boxes_to_draw:绘制的region proposals最大个数,如果检测到的物体个数大于max_boxes_to_draw,按照scores排序选取前max_boxes_to_draw个绘制。 ? min_score_thresh:阈值,scores大于该阈值的物体才显示。 ? line_thickness:绘制的region proposals的线条的宽度。 ? skip_scores:绘制region proposals时是否显示对应分数。 ? skip_labels:绘制region proposals时是否显示物体名称。 通过tensorflow中提供的faster rcnn模型,我们可以在比较复杂的环境背景下检测多种物体,调试阶段我们可以把显示的阈值降低,增加可以显示的物体的上限,同时显示物体的概率值。 max_boxes_to_draw=100, min_score_thresh=0.2, line_thickness=2, skip_scores=false, skip_labels=false 如图7-39所示,典型的场景之一就是使用faster rcnn识别交通标志,不过faster rcnn的实时性较差,在对实时性要求高的领域,通常被作为其他手段的补充。 图7-39 使用faster rcnn识别交通标志 如图7-40所示,典型的场景之一就是使用faster rcnn识别路上行人。 图7-40 使用faster rcnn识别路上行人 7.8.1 YOLO概述 7.8.1 yolo概述 yolo,全称为you only look once detector,是一个可以一次性预测多个检测框位置和类别的卷积神经网络,能够实现端到端的目标检测和识别,其最大的优势就是速度快。yolo发展到现在已经成为一系列目标检测算法的统称。yolo流程简化图如图7-41所示。 图7-41 yolo流程简化图 如图7-42所示,原始图像经过卷积网络处理后,生成了一个7x7x30维的特征向量,可以理解为把原图像分割成了大小相等的7x7个网格,每个网格生成2个检测框,称为bounding box,每个bounding box对应5个预测参数,即bounding box的中心点坐标、宽和高以及置信度评分。所谓置信度评分就是iou和目标分类每个类别的概率分数的综合指标。每个网格有30维的特征,其中前10维为两个bounding box的特征,后20维为20类物体的概率分数。 图7-42 每个网格对应2个bounding box yolo有众多版本,其中最著名是yolo v3。图7-43所示为yolo v3的预测结果,可以理解为把图像分别划分成13x13、26x26和52x52三组网格,每组网格也被称为特征图。 图7-43 yolo v3对图像的预测结果 如图7-44所示,每个网格都包括三个预测结果,即三个预测框。每个预测框都由三部分组成,分别是预测框的四个坐标以及识别为不同物体的分类概率。以ms coco数据集为例,ms coco包含80种物体,yolo v3的分类结果就是该预测框被识别为这80种物体的概率。需要指出的是,一个预测框中通常包含不止一种物体,因此每个分类结果是相互独立的,概率总和不为1,比如一个预测框中同时包含猫和狗,那么识别为猫和狗的概率可能都是0.9。 图7-44 yolo v3网格的结构 如图7-45所示,yolo v3中对物体检测框位置是通过四个坐标表示的,即(tx,ty,tw,th)。假设物体的检测框真实坐标为(bx,by,bw,bh),分别表示预测框的中心点的横纵坐标以及整个预测框的宽和高。 图7-45 yolo v3的检测框的表示方法 物体的检测框真实坐标计算方法如图7-46所示。 综上所述,通常把图片大小转换成416x416,经过yolo v3预测后输出的box个数为: 3x(13x13+26x26+52x52)=10 647 图7-46 yolo v3检测框的计算方法 每个预测框都由4+1+80=85个参数组成。如何从10 647个预测框中选择最合适的一个表示物体的范围呢?有兴趣的读者可以进一步阅读相关文献。 7.8.2 YOLO使用示例 7.8.2 yolo使用示例 yolo官方版本是使用darknet实现的,darknet是一个用c实现的深度学习框架,比较小众。下面我们介绍yolo v3基于pytorch的一个实现,开源地址为:https://github.com/ayooshkathuria/pytorch-yolo-v3。 安装方法很简单,首先同步代码。 git clone https://github.com/ayooshkathuria/pytorch-yolo-v3.git 然后下载预训练好的参数文件到安装目录即可。 wget https://pjreddie.com/media/files/yolov3.weights 如果是针对图像文件进行检测,指定文件路径以及生成的图像文件路径即可。 python detect.py --images imgs --det det 使用默认的测试图片进行预测,对应的文件夹为imgs,生成的图像文件保存于det目录。在我的mac本上平均每张图片的检测时间为1.672秒,检测效果如图7-47和图7-48所示。 reading addresses : 0.001 loading batch : 2.623 detection (11 images) : 15.614 output processing : 0.000 drawing boxes : 0.156 average time_per_img : 1.672 如果是处理视频文件,比如video.avi,命令如下: python video_demo.py --video video.avi 图7-47 使用yolo检测动物 图7-48 使用yolo检测车辆 7.9.1 SSD概述 7.9.1 ssd概述 ssd,全称为single shot multibox detector,是wei liu在eccv 2016上提出的一种目标检测算法,截至目前,它是主流的检测框架之一,相比faster rcnn有明显的速度优势,相比yolo又有明显的map优势,如图7-49所示。 图7-49 主流目标检测算法对比 ssd具有如下主要特点: ? 从yolo中继承了将检测问题转化为回归问题的思路,同时一次即可完成网络训练。 ? 基于faster rcnn中的anchor,提出了相似的prior box。 ? 加入基于特征金字塔(pyramidal feature hierarchy)的检测方式。 ssd的具体实现已经超出了本书的范围,有兴趣的读者可以阅读相关文献。 7.9.2 SSD使用示例 7.9.2 ssd使用示例 下面介绍如何使用tensorflow中提供的ssd模型,相应的示例代码位于: https://github.com/duoergun0729/adversarial_examples/blob/master/code/10-ssd.ipynb tensorflow中ssd的计算图定义和预训练参数下载地址为: http://download.tensorflow.org/models/object_detection/ssd_mobilenet_v1_coco_2018_01_28.tar.gz 解压到adversarial_examples/code/models: # adversarial_examples/code/models/faster_rcnn_resnet101_coco_2018_01_28 tar -zxvf ssd_mobilenet_v1_coco_2018_01_28.tar.gz ssd模型的使用和faster rcnn完全相同,只需修改定义模型文件的相关路径,包括物体标签和物体名称之间的映射关系表。 path_to_frozen_graph = 'models/ssd_mobilenet_v1_coco_2018_01_28/frozen_inference_graph.pb' path_to_labels = 'models/models/research/object_detection/data/mscoco_label_map.pbtxt' ssd在实时性和准确性上都较为突出,广泛应用至需要实时进行目标检测的领域。 图7-50所示为使用ssd实时检测路况。 图7-50 使用ssd实时检测路况 图7-51所示为使用ssd实时检测马路行人。 图7-51 使用ssd实时检测马路行人 图7-52所示为使用ssd在公共场所进行智能安防监控。 图7-52 使用ssd在公共场所进行智能安防监控 7.10 白盒攻击Faster RCNN 7.10 白盒攻击faster rcnn 以faster rcnn模型为例,我们介绍如何基于tensorflow平台对目标检测模型进行白盒攻击,让目前分类的结果产生错误,相应的示例代码位于: https://github.com/duoergun0729/adversarial_examples/blob/master/code/10-case4-faster-rcnn.ipynb 如图7-53所示,tensorflow实现的faster rcnn,整个计算图的输入是image_tensor,输出结果为检测出的region proposals即detection_boxes,物体分类得分即detection_scores,物体分类的标签值即detection_classes。 faster rcnn是先检测出region proposals,然后再检测出物体分类,整个物体检测阶段还是分为了两个阶段。我们的目标是让物体分类结果产生错误,那么可以在第二阶段进行攻击。如图7-54所示,整个计算图的输入是image_tensor,输出的为: ? rpn_nms_bboxes_和rpn_nms_indices_,表示第一阶段计算图检测出的region proposal。 ? patched_input_,表示经过第一阶段计算图处理后的图像数据。 如图7-55所示,计算图的输入tensorarraygatherv3:0表示经过第一阶段处理过的图像数据,reshape_7:0和reshape_8:0表示第一阶段检测出的region proposal。计算图的输出分别为: ? second_stage_cls_scores,表示目标分类的得分。 ? second_stage_loc_bboxes,表示经过第二阶段回归调整过的region proposal。 图7-53 faster rcnn的计算图 图7-54 faster rcnn的计算图(第一阶段) 图7-55 faster rcnn的计算图(第二阶段) 假设我们需要欺骗faster rcnn,让它把人识别为马。我们仍然基于迭代优化的思想,首先需要定义好哪些量是可以被优化器调整的,哪些量是输入量,损失函数是什么。由于我们是在第二阶段进行攻击,所以把原始图像作为输入,经过第一阶段计算图的处理,得到新的处理过的图像数据。我们定义扰动量为可以调整的变量,扰动量和处理过的图像数据组成了新的数据,并作为输入进入第二阶段的计算图。与攻击图像分类模型一样,这里的损失函数是计算分类结果和我们的攻击目标标签值之间的交叉熵,并使用优化器根据损失函数不断迭代调整扰动量的值,直到满足最大迭代次数或者损失值足够小,下面我们将介绍代码是如何实现的。图7-56为白盒攻击faster rcnn的流程图。 加载测试图片,并转换成固定大小,见图7-57。 image_path="../picture/objectdetect/adv-case2.jpg" image = image.open(image_path).convert("rgb") image=image.resize((320,200)) image_np = load_image_into_numpy_array(image.copy()) 如图7-58所示,正常情况下,faster rcnn可以识别图7-57中的人,并使用region proposal标记出人的范围。其中人的分类标签值为1,马的分类标签值为19。 定义第二阶段的输入/输出,定义的输入为self.image_input_和self.patch_placeholder_,其中self.image_input_输入的是第一阶段处理后的图像数据。定义可以被优化器迭代训练调整的参数self.patch_,可以理解为叠加到self.image_input_上的扰动,且形状完全一样。self.patch_placeholder_用于给变量赋值使用。 self.image_input_ = tf.placeholder(tf.float32, shape=self.patch_shape, name='image_input_placeholder') self.patch_ = tf.get_variable('patch', self.patch_shape, dtype=tf.float32, initializer=tf.zeros_initializer) self.patch_placeholder_ = tf.placeholder(dtype=tf.float32, shape=self.patch_shape, name='patch_placeholder') self.assign_patch_ = tf.assign(self.patch_, self.patch_placeholder_) 图7-56 白盒攻击faster rcnn的流程图 为了训练方便,把self.patch_定义为[–1,1]或者[0,1],因此需要把self.image_input_归一化处理。本例中把两者都假设为[0,1],叠加后的图像数据为self.patched_input_。因为第二阶段的计算图的输入必须在[0,255],需要使用tf.fake_quant_with_min_max_vars进行截断处理。tf.fake_quant_with_min_max_vars函数除了可以进行截断,还可以计算梯度。在使用优化器调整参数时,要求计算图中经过的全部tensor都可以计算梯度。 图7-57 目标检测使用的测试图片 图7-58 faster rcnn针对测试图片标记检测结果 patched_input_ = self.patch_+self.image_input_/255.0 self.patched_input_ = tf.fake_quant_with_min_max_vars(patched_input_*255.0, min=0, max=255) 定义第二阶段计算图的输入,其中与region proposal相关的代码如下: # 定义输入,便于在tf中给预测框输入数据 self.rpn_nms_bboxes_placeholder_ = tf.placeholder(tf.float32, shape=(none, 4), name='rpn_nms_bboxes') self.rpn_nms_indices_placeholder_ = tf.placeholder(tf.int32, shape=(none), name='rpn_nms_indices') 加载模型文件成计算图,并且将输入tensor进行映射替换,其中self.patched_input_为第二阶段计算图的输入。 def create_graph(dirname): with tf.gfile.fastgfile(dirname, 'rb') as f: graph_def = self.sess.graph_def graph_def.parsefromstring(f.read()) _ = tf.import_graph_def(graph_def, name='adv_model', input_map={ 'preprocessor/map/tensorarraystack/tensorarraygatherv3:0': self.patched_input_, 'reshape_7:0':self.rpn_nms_bboxes_placeholder_, 'reshape_8:0':self.rpn_nms_indices_placeholder_} ) create_graph(path_to_frozen_graph) 本质上我们攻击的还是图像分类模型,因此在定义损失函数时,还是计算图像预测值和定向攻击的标签值之间的交叉熵。 # 计算第二阶段的分类损失函数 self.second_stage_cls_scores_ = self.graph.get_tensor_by_name('adv_model/secondstagepostprocessor/convert_ scores:0') second_stage_cls_logits_ = self.graph.get_tensor_by_name('adv_model/secondstagepostprocessor/scale_ logits:0') self.second_stage_cls_labels_ = tf.placeholder(tf.float32, shape=second_stage_cls_logits_.shape, name='second_stage_cls_labels') 其中self.second_stage_cls_labels_表示我们期望的标签值,second_stage_cls_logits_表示目标分类的结果,损失函数是计算两者之间的交叉熵,如图7-59所示。 图7-59 损失函数计算方法 定义了损失函数,就可以进一步定义优化器以及需要迭代训练调整的参数self.patch_。由于tf.train.adamoptimizer在训练过程中也需要使用变量,因此需要调用全局的初始化函数tf.global_variables_initializer。 # 加权后的损失函数的总和 self.loss_ = self.second_stage_cls_loss_ self.train_op_ = tf.train.adamoptimizer(0.01).minimize(self.loss_, var_list=[self.patch_]) # 初始化参数,adam的参数也需要这样初始化,gradientdescent可以省略这一步 self.sess.run(tf.global_variables_initializer()) 使用第一阶段的计算图,输入原始图像数据得到region proposal相关的self.rpn_nms_bboxes_和self.rpn_nms_indices_。 def inference_rpn(self, images): feed_dict = { self.image_input_: images } tensors = [self.rpn_nms_bboxes_, self.rpn_nms_indices_ ] rpn_nms_bboxes, rpn_nms_indices = self.sess.run(tensors, feed_dict) return rpn_nms_bboxes, rpn_nms_indices 定义迭代优化的过程,输入是region proposal相关的self.rpn_nms_bboxes_和self.rpn_nms_indices_,原始图像和定向攻击的目标标签second_stage_cls_labels。 rpn_nms_bboxes, rpn_nms_indices = self.inference_rpn(images) feed_dict = { self.image_input_: images, self.second_stage_cls_labels_: second_stage_cls_labels, self.rpn_nms_bboxes_placeholder_: rpn_nms_bboxes, self.rpn_nms_indices_placeholder_: rpn_nms_indices, } tensors = [ self.train_op_,self.loss_] train_op, loss= self.sess.run(tensors, feed_dict) 定义目标标签,相对于一般的图像分类问题,目标检测模型的目标标签定义会复杂一些。scores代表是模型预测的region proposal的得分,目前faster rcnn会返回300个region proposal,每个region proposal会得到在91个物体分类上的得分。我们希望识别为人的region proposal最终识别为马,也就是要把原来识别为标签id为1的识别为19,那么需要先遍历scores,找到识别为人的得分最高的region proposal,把该region proposal其他分类的得分设置为0,马的得分设置为1。 target_class = 19 from_class = 1 def create_target_labels(scores, from_class, to_class): target_labels = np.zeros_like(scores) classes = np.argmax(scores[:, :, 1:], axis=2)+1 for i, _ in enumerate(classes): for j, cls in enumerate(classes[i]): cls = to_class target_labels[i, j, cls] = 1 return target_labels 迭代优化,最大迭代次数为300,通过优化器在反向传递中不断调整输入图像,最终得到图7-60,把人识别为马。细心的读者会发现,背景上有比较明显的扰动,事实上扰动默认会在整个图片上发生,从背景到需要攻击的物体。 #开始迭代求解 epochs=300 for epoch in range(epochs): loss = adv_model_instance.train_step( np.expand_dims(image_np.copy(),0),target_labels) #显示中间结果 if (epoch % 50) == 0: print("epoch={} loss={}".format(epoch,loss)) adv_model_instance.inference_draw(np.expand_dims(image_np.copy(),0)) 图7-60 faster rcnn把图片中的人识别为马 7.11 物理攻击YOLO概述 7.11 物理攻击yolo概述 在白盒攻击中,攻击者可以完全控制输入,可以直接修改输入模型的原始数据,并且完全了解模型的网络结构,根据自定义的损失函数计算梯度。但是在物理攻击中,攻击者无法完全控制输入模型的数据,对于模型如何预处理原始数据也一无所知。攻击者的对抗样本只能通过摄像头、麦克风这类物理设备,经过一系列黑盒般的预处理后才能真正进入模型。以智能驾驶车辆的交通标志识别为例,如图7-61所示,智能驾驶车辆通过前置摄像头拍摄车辆前部的画面,前置摄像头把视频数据转换成一帧一帧的图像;图像经过高斯滤波器处理,过滤掉明显的噪声数据,然后经过canny算法进行边缘检测,通过hough变换进行圆形检测,得到了圆形交通标志的坐标数据;接着按照圆形交通标志的坐标数据,从原始图像中截取交通标志的原始数据,缩放成交通标志分类模型的输入层大小后,输入该模型进行分类预测,得到分类结果。当分类结果的预测概率超过一定的阈值时,认为识别结果可信,比如识别为80公里限速的概率超过90%,则认为前方有80公里限速的交通标志,应该适当调整车速。当识别的概率低于阈值时,则忽略识别结果。 图7-61 智能驾驶车辆交通标志识别原理图 以物理攻击交通标志识别为例,如图7-62所示,对抗样本通常只能以交通标志或者一张海报的形式存在,通过在交通标志或者海报上打印上扰动来攻击深度学习模型。 图7-62 物理攻击时对抗样本的形态 dr.zhenyu(edward)和dr.yunhan jia在blackhat europe 2018上发表了演讲“perception deception:physical adversarial attack challenges and tactics for dnn-based object detection”,他们介绍了物理攻击遇到的诸多问题。如图7-63所示,对抗样本在真正输入到分类模型的过程中,需要考虑到以下因素: ? 控制扰动/对抗样本的区域。 ? 目标距离、角度的变化带来的干扰。 ? 光照条件的干扰。 ? 打印设备、摄像头采集的颜色范围。 图7-63 物理攻击中遇到的难点 克服这些干扰因素的方法的核心思路是,在生成对抗样本的迭代过程中,把打印设备的像素彩色输出范围和对抗样本的色差作为损失函数的一部分,并且在训练过程中引入仿射变换和光照变化。 最后达到的攻击效果如图7-64所示,在时间t0的时候,当在车后显示器中显示正常logo时,yolo v3可以正确识别目标车辆,而在t1时刻,切换到扰动后的图片时,它可以立刻让目标车辆在yolo v3面前变得无法辨识;在t2时刻,如图7-65所示切换回正常的图片,yolo v3可以重新识别目标车辆。 图7-64 t0时刻可以正常识别出车辆,t1时刻无法识别出车辆 图7-65 t2时刻可以正常识别出车辆 7.12 本章小结 7.12 本章小结 本章介绍了机器视觉领域非常重要的目标检测,介绍了目标检测的基本概念以及在智能驾驶、智能安防等领域的应用。边缘、直线和原型检测是目标检测的基础,本章以opencv为例介绍了相关的基础知识和示例。rcnn、yolo和ssd是经典的目标检测算法,本章介绍了这三种算法,并在最后给出了针对faster rcnn的白盒攻击算法实现,并概述了如何进行物理攻击。 8.1.1 图像旋转对鲁棒性的影响 8.1.1 图像旋转对鲁棒性的影响 下面我们结合实际的例子来介绍旋转对鲁棒性的影响,对应的代码路径为: https://github.com/duoergun0729/adversarial_examples/blob/master/code/8- case1.ipynb #图像加载以及预处理 image_path="../picture/left.jpg" img = cv2.imread(image_path)[..., ::-1] img = cv2.resize(img, (rows,cols)) matrix = cv2.getrotationmatrix2d((cols/2,rows/2),90,1) img1 = cv2.warpaffine(img,matrix,(cols,rows)) matrix = cv2.getrotationmatrix2d((cols/2,rows/2),180,1) img2 = cv2.warpaffine(img,matrix,(cols,rows)) matrix = cv2.getrotationmatrix2d((cols/2,rows/2),270,1) img3 = cv2.warpaffine(img,matrix,(cols,rows)) 如图8-1所示,直观的感觉,当图像发生旋转时,大多数情况下不会影响人对物体分类的结果。除了少数的角度/方向敏感的情况,如图8-2所示,原始图像为向左,图像分别逆时针旋转90度、180度、270度后,人对该图像会分别识别为向下、向右和向上。 图8-1 图像发生旋转不影响分类 图8-2 图像发生旋转影响分类 我们以经典的熊猫图像来验证我们的想法。首先我们定义一个工具函数infer_img,使用的预测模型是pytorch框架下基于imagenet2012数据集训练的alexnet模型。infer_img的输入为图像数据img以及标签t。t默认为0,表示返回预测结果中概率最大的分类标签的概率,当t不为0时,返回指定的分类标签的概率。需要指出的是,pytorch框架下的imagenet2012数据集,图像数据的标准化比较特殊,需要使用特定的均值mean和标准差std: def infer_img(img,t=0): #标准化 img=img.astype(np.float32) mean = [0.485, 0.456, 0.406] std = [0.229, 0.224, 0.225] img /= 255.0 img = (img - mean) / std img = img.transpose(2, 0, 1) img=np.expand_dims(img, axis=0) img = variable(torch.from_numpy(img).to(device).float()) #使用预测模式主要影响dropout和bn层的行为 model = models.alexnet(pretrained=true).to(device).eval() output=f.softmax(model(img),dim=1) label=np.argmax(output.data.cpu().numpy()) pro=output.data.cpu().numpy()[0][label] #当t不为0时返回指定类别的概率 if t != 0: pro=output.data.cpu().numpy()[0][t] return pro 我们对原始的熊猫图片进行预测。 print(infer_img(orig)) print(infer_img(orig,t=288)) print(infer_img(orig,t=388)) 预测结果为,分类为标签388(熊猫对应的分类标签)的概率为92.71%,分类为标签288的概率非常低。 0.9270878 3.1756701e-06 0.9270878 下面我们把原始图像逐渐旋转至180度,每次旋转的角度增加10度,并记录下旋转角度和预测为熊猫对应标签的概率。旋转的方法使用了4.2.1节的仿射变化。 #验证原始图片的旋转不变性 rotate_range = range(0,180,10) original_pro = [] for i in rotate_range: #构造仿射变换矩阵 matrix = cv2.getrotationmatrix2d((cols/2,rows/2),i,1) #进行仿射变化 rotate_img = cv2.warpaffine(orig.copy(),matrix,(cols,rows)) #获得预测为熊猫的概率值 pro=infer_img(rotate_img.copy(),388) print("rotate={} pro[388]={}".format(i,pro)) original_pro += [pro] #绘图,横轴代表旋转的角度,纵轴代表预测为熊猫对应标签的概率 fig, ax = plt.subplots() ax.plot(np.array(rotate_range), np.array(original_pro), 'b--', label='probability of class 388') legend = ax.legend(loc='upper center', shadow=true, fontsize='large') legend.get_frame().set_facecolor('#ffffff') plt.xlabel('rotate range') plt.ylabel('probability') plt.show() 如图8-3所示,横轴代表旋转的角度,纵轴代表预测为熊猫对应标签的概率。我们假设分类概率大于50%表示不影响分类结果,可见当旋转角度小于60度或者在90度和110度之间时不影响分类结果。如果以更加严格的要求,认为分类概率大于80%表示不影响分类结果,那么可以认为旋转30度以内不影响分类结果。通过本例可以看出,图像分类模型对于图像旋转具有一定的鲁棒性,当旋转角度较小时对分类结果影响不大。事实上在进行模型训练时,为了增加训练数据量,会人为地把训练数据随机旋转一定的角度,这一过程也叫作数据增强。大量的实验证明,图像预测阶段旋转的角度如果在数据增强的范围内,不会影响分类结果。 图8-3 图像分类模型对图像旋转的鲁棒性示例 我们针对熊猫图像,使用fgsm算法生成定向攻击的对抗样本,定向攻击的目标标签为288,并且概率大于一定的阈值才认为攻击成功。 epochs=100 e=0.007 target=288 target=variable(torch.tensor([float(target)]).to(device).long()) for epoch in range(epochs): # forward + backward output = f.softmax(model(img),dim=1) loss = loss_func(output, target) label=np.argmax(output.data.cpu().numpy()) pro=output.data.cpu().numpy()[0][label] print("epoch={} loss={} label={} pro={}".format(epoch,loss,label,pro)) #如果定向攻击成功并且概率大于阈值 if (label == target) and ( pro > 0.80): print("") break #梯度清零 optimizer.zero_grad() #反向传递,计算梯度 loss.backward() img.data=img.data-e*torch.sign(img.grad.data) 为了后继图像旋转处理方便,需要把归一化的对抗样本还原成正常图片格式。对抗样本的形状为[3,224,224],需要转换为[224,224,3]: #把对抗样本转换成正常图片格式 adv=img.data.cpu().numpy()[0] print(adv.shape) adv = adv.transpose(1, 2, 0) adv = (adv * std) + mean adv = adv * 255.0 adv = np.clip(adv, 0, 255).astype(np.uint8) print(adv.shape) 下面我们把对抗样本逐渐旋转至180度,每次旋转的角度增加10度,并记录下旋转角度和预测为熊猫和定向攻击目标对应标签的概率。 #验证旋转对于对抗样本的影响 rotate_range = range(0,180,10) adv_288_pro = [] adv_388_pro = [] for i in rotate_range: matrix = cv2.getrotationmatrix2d((cols/2,rows/2),i,1) rotate_img = cv2.warpaffine(adv.copy(),matrix,(cols,rows)) #记录熊猫的概率 pro_388=infer_img(rotate_img.copy(),388) #记录定向攻击目标的概率 pro_288=infer_img(rotate_img.copy(),288) print("rotate={} pro[388]={} pro[288]={}".format(i,pro_388,pro_288)) adv_288_pro += [pro_288] adv_388_pro += [pro_388] fig, ax = plt.subplots() ax.plot(np.array(rotate_range), np.array(adv_288_pro), 'b--', label='probability of class 288') ax.plot(np.array(rotate_range), np.array(adv_388_pro), 'r', label='probability of class 388') legend = ax.legend(loc='upper center', shadow=true, fontsize='large') legend.get_frame().set_facecolor('#ffffff') plt.xlabel('rotate range') plt.ylabel('probability') plt.show() 如图8-4所示,横轴代表旋转的角度,纵轴代表预测概率,实线代表预测为熊猫的概率,虚线代表预测为定向攻击目标的概率。我们假设分类概率大于50%的预测结果可信,那么可以认为当旋转角度小于5度时,该定向攻击的对抗样本有效,当角度大于10度时,定向攻击失效。如果以更加严格的要求,认为分类概率大于80%的结果可信,那么可以认为当旋转角度在30度到60度之间时,原有模型可以有效抵御定向攻击样本。 图8-4 对抗样本对图像旋转的鲁棒性示例 那么是否存在一种方式,把图像旋转一定角度后,既不影响对原始图像的分类,又可以抵御对抗样本的影响呢?下面我们把对抗样本和原始图片逐渐旋转至180度,每次旋转的角度增加10度,并记录下旋转角度和预测为熊猫的概率。 #综合分析验证旋转对于对抗样本和正常图片分类的影响 rotate_range = range(0,180,10) original_pro = [] adv_pro = [] for i in rotate_range: matrix = cv2.getrotationmatrix2d((cols/2,rows/2),i,1) rotate_adv_img = cv2.warpaffine(adv.copy(),matrix,(cols,rows)) pro_388=infer_img(rotate_adv_img.copy(),388) adv_pro+= [pro_388] rotate_img = cv2.warpaffine(orig.copy(),matrix,(cols,rows)) pro=infer_img(rotate_img.copy(),388) original_pro += [pro] print("rotate={} adv_pro[388]={} original_pro[388]={}".format(i,pro_388,pro)) fig, ax = plt.subplots() ax.plot(np.array(rotate_range), np.array(adv_pro), 'b--', label='probability of adversarial') ax.plot(np.array(rotate_range), np.array(original_pro), 'r', label='probability of original') legend = ax.legend(loc='upper center', shadow=true, fontsize='large') legend.get_frame().set_facecolor('#ffffff') plt.xlabel('rotate range') plt.ylabel('probability') plt.show() 如图8-5所示,横轴代表旋转的角度,纵轴代表预测概率,实线代表原始图像预测为熊猫的概率,虚线代表对抗样本预测为熊猫的概率。我们假设分类概率大于80%的结果可信,那么可以认为当旋转角度在30度到60度之间时,原有模型可以有效抵御定向攻击样本,同时不影响正常图片分类。 图8-5 图像旋转对原始图像和对抗样本的鲁棒性示例 8.1.2 滤波器对鲁棒性的影响 8.1.2 滤波器对鲁棒性的影响 滤波器是图像领域中非常重要的一个基础功能组件,通常用于过滤图像中的噪声。高斯滤波在相当长一段时间内都是最优秀的滤波器,但是因为噪声是高频信号,边缘、纹理也是高频信息,高斯滤波会在滤除噪声的同时使得边缘模糊,于是人们发明了双边滤波器,即bilateral filter。双边滤波器是一种典型的保边去噪的滤波器。可以滤除图像数据中的噪声,同时保留住图像的边缘、纹理等。下面我们结合实际的例子来介绍滤波器对鲁棒性的影响,对应的代码路径为: https://github.com/duoergun0729/adversarial_examples/blob/master/code/8- case2.ipynb 在opencv中使用bilateralfilter实现了高斯双边滤波。 bilateralfilter(src, n, sigmacolor, sigmaspace, bordertype) 其中主要参数的含义如下: ? src,输入变换前图像。 ? n,过滤过程中每个像素邻域的直径范围。 ? sigmacolor,颜色空间过滤器的标准差值。 ? sigmaspace,坐标空间中滤波器的标准差值。 ? bordertype,用于推断图像外部像素的某种边界模式,默认值为border_default。 通常可以将两个标准差的值设置成相同的值。小于10对滤波器影响很小,大于150则会对滤波器产生较大的影响。 分别对熊猫图像依次设置10、50和100的标准差进行双边滤波。 img2=cv2.bilateralfilter(orig.copy(),11,10,10) img3=cv2.bilateralfilter(orig.copy(),11,50,50) img4=cv2.bilateralfilter(orig.copy(),11,100,100) 如图8-6所示,双边滤波很好地保护了图像的边缘和纹理。 下面我们对原始图像进行双边滤波,每次标准差增加10,并记录下标准差和预测为熊猫对应标签的概率。 #验证原始图片对滤波器的鲁棒性 std_range = range(0,300,10) original_pro = [] #每次标准差增加10 记录下标准差和预测为熊猫对应标签的概率 for i in std_range: bilateralfilter_img=cv2.bilateralfilter(orig.copy(),11,i,i) #记录熊猫的概率 pro=infer_img(bilateralfilter_img.copy(),388) print("rotate={} pro[388]={}".format(i,pro)) original_pro += [pro] #横轴代表双边滤波器标准差,纵轴代表预测为熊猫对应标签的概率 fig, ax = plt.subplots() ax.plot(np.array(std_range), np.array(original_pro), 'b--', label='probability of class 388') legend = ax.legend(loc='best', shadow=true, fontsize='large') legend.get_frame().set_facecolor('#ffffff') plt.xlabel('rotate range') plt.ylabel('probability') plt.show() 图8-6 不同参数下双边滤波的处理效果 如图8-7所示,横轴代表双边滤波器标准差,纵轴代表预测为熊猫对应标签的概率。我们假设分类概率大于50%表示不影响分类结果,可见当标准差小于280时不影响分类结果。如果以更加严格的要求,可以认为分类概率大于80%表示不影响分类结果,标准差小于约50时不影响分类结果。 图8-7 图像分类模型对滤波器的鲁棒性影响示例 我们针对熊猫图像,使用fgsm算法生成定向攻击的对抗样本,定向攻击的目标标签为288,并且概率大于一定的阈值才认为攻击成功。下面我们把对抗样本进行双边滤波,每次标准差增加10,并记录下标准差和预测为熊猫及定向攻击目标的概率。 #验证滤波器对于对抗样本的影响 std_range = range(0,300,10) adv_288_pro = [] adv_388_pro = [] #每次标准差增加10 for i in std_range: bilateralfilter_img=cv2.bilateralfilter(orig.copy(),11,i,i) #记录下标准差和预测为熊猫及定向攻击目标的概率 pro_388=infer_img(bilateralfilter_img.copy(),388) pro_288=infer_img(bilateralfilter_img.copy(),288) print("rotate={} pro[388]={} pro[288]={}".format(i,pro_388,pro_288)) adv_288_pro += [pro_288] adv_388_pro += [pro_388] #横轴代表双边滤波器标准差,纵轴代表预测概率 #实线代表预测为熊猫的概率,虚线代表预测为定向攻击目标的概率 fig, ax = plt.subplots() ax.plot(np.array(std_range), np.array(adv_288_pro), 'b--', label= 'probability of class 288') ax.plot(np.array(std_range), np.array(adv_388_pro), 'r', label='probability of class 388') legend = ax.legend(loc='best', shadow=true, fontsize='large') legend.get_frame().set_facecolor('#ffffff') plt.xlabel('std range') plt.ylabel('probability') plt.show() 如图8-8所示,横轴代表双边滤波器标准差,纵轴代表预测概率,实线代表预测为熊猫的概率,虚线代表预测为定向攻击目标的概率。我们假设分类概率大于50%时结果可信,可见当标准差小于280时可以抵御对抗样本攻击,把对抗样本识别为熊猫。如果以更加严格的要求,可以认为分类概率大于80%表示不影响分类结果,标准差小于50时可以抵御对抗样本攻击,把对抗样本识别为熊猫。 那么是否存在一种方式,经过滤波后,既不影响对原始图像的分类,又可以抵御对抗样本的影响呢?下面我们把对抗样本和原始图片均进行双边滤波,每次标准差增加10,并记录下标准差和预测为熊猫的概率。 #综合分析滤波器对于对抗样本和正常图片分类的影响 std_range = range(0,300,10) original_pro = [] adv_pro = [] for i in std_range: #记录下标准差和预测为熊猫的概率 bilateralfilter_adv_img=cv2.bilateralfilter(adv.copy(),11,i,i) pro_388=infer_img(bilateralfilter_adv_img.copy(),388) adv_pro+= [pro_388] bilateralfilter_img=cv2.bilateralfilter(orig.copy(),11,i,i) pro=infer_img(bilateralfilter_img.copy(),388) original_pro += [pro] print("rotate={} adv_pro[388]={} original_pro[388]={}".format(i,pro_388,pro)) #横轴代表双边滤波器标准差,纵轴代表预测概率 #实线代表原始图像预测为熊猫的概率,虚线代表对抗样本预测为熊猫的概率 fig, ax = plt.subplots() ax.plot(np.array(std_range), np.array(adv_pro), 'b--', label='probability of adversarial') ax.plot(np.array(std_range), np.array(original_pro), 'r', label='probability of original') legend = ax.legend(loc='best', shadow=true, fontsize='large') legend.get_frame().set_facecolor('#ffffff') plt.xlabel('std range') plt.ylabel('probability') plt.show() 图8-8 滤波器对于对抗样本的鲁棒性影响示例(fgsm算法) 如图8-9所示,横轴代表双边滤波器标准差,纵轴代表预测概率,实线代表原始图像预测为熊猫的概率,虚线代表对抗样本预测为熊猫的概率。我们假设分类概率大于50%时结果可信,那么可以认为当双边滤波器标准差大于50且小于280时,原有模型可以有效抵御定向攻击样本,同时不影响正常图像分类。 使用攻击强度更高的cw算法重复以上攻击,如图8-10所示,结果相同。 图8-9 滤波器对原始图像和对抗样本的鲁棒性影响示例(fgsm算法) 图8-10 滤波器对原始图像和对抗样本的鲁棒性影响示例(cw算法) 8.1.3 对比度和亮度对鲁棒性的影响 8.1.3 对比度和亮度对鲁棒性的影响 在处理图像时,通过对像素值大小的适当调整也可以达到类似效果。采用按像素的方式改变图像对比度和亮度,公式如下: f(x')=αf(x)+β 其中α调整的是对比度,值越大对比度越大;β调整的是亮度,值越大亮度越大。下面我们结合实际的例子来介绍对比度和亮度对鲁棒性的影响,对应的代码路径为: https://github.com/duoergun0729/adversarial_examples/blob/master/code/8- case3-cw.ipynb 下面我们调整对抗样本的亮度,调整范围为–200到+200,每次增加10,并记录下亮度调整值和对抗样本预测为熊猫及定向攻击目标的概率,对抗样本使用cw算法生成,定向攻击目标标签为288,要求概率大于80%。 #验证亮度对于对抗样本的影响 std_range = range(-200,200,10) adv_288_pro = [] adv_388_pro = [] for i in std_range: #记录下亮度调整值和对抗样本预测为熊猫及定向攻击目标的概率 brightness_adv_img=np.clip((adv.copy()+i),0,255) pro_388=infer_img(brightness_adv_img.copy(),388) pro_288=infer_img(brightness_adv_img.copy(),288) print("std={} pro[388]={} pro[288]={}".format(i,pro_388,pro_288)) adv_288_pro += [pro_288] adv_388_pro += [pro_388] #横轴代表亮度调整值,纵轴代表预测概率 #实线代表对抗样本预测为熊猫的概率,虚线代表对抗样本预测为定向攻击目标的概率 fig, ax = plt.subplots() ax.plot(np.array(std_range), np.array(adv_288_pro), 'b--', label= 'probability of class 288') ax.plot(np.array(std_range), np.array(adv_388_pro), 'r', label='probability of class 388') legend = ax.legend(loc='best', shadow=true, fontsize='large') legend.get_frame().set_facecolor('#ffffff') plt.xlabel('brightness change range') plt.ylabel('probability') plt.show() 如图8-11所示,横轴代表亮度调整值,纵轴代表预测概率,实线代表对抗样本预测为熊猫的概率,虚线代表对抗样本预测为定向攻击目标的概率。我们假设概率大于50%时,分类结果可信,可见在该案例中,亮度调整范围小于–25或者大于25时,对抗样本定向攻击失效。当亮度调整范围小于–50或者大于50时,对抗样本预测为定向攻击目标的概率几乎为0。当亮度调整范围大于–125并且小于25时,模型可以把对抗样本识别为熊猫,没有被欺骗。 下面我们调整原始图像和对抗样本的亮度,调整范围为–200到+200,每次增加10,并记录下亮度调整值和预测为熊猫对应标签的概率,对抗样本使用cw算法生成,定向攻击目标标签为288,要求概率大于80%。 #综合分析亮度对于对抗样本和正常图片分类的影响 std_range = range(-200,200,10) original_pro = [] adv_pro = [] for i in std_range: brightness_adv_img=np.clip((adv.copy()+i),0,255) pro_388=infer_img(brightness_adv_img.copy(),388) adv_pro+= [pro_388] brightness_img=np.clip((orig.copy()+i),0,255) pro=infer_img(brightness_img.copy(),388) original_pro += [pro] print("std={} adv_pro[388]={} original_pro[388]={}".format(i,pro_388,pro)) #横轴代表亮度调整值,纵轴代表预测概率 #实线代表原始图片预测为熊猫的概率,虚线代表对抗样本预测为熊猫的概率 fig, ax = plt.subplots() ax.plot(np.array(std_range), np.array(adv_pro), 'b--', label='probability of adversarial') ax.plot(np.array(std_range), np.array(original_pro), 'r', label='probability of original') legend = ax.legend(loc='best', shadow=true, fontsize='large') legend.get_frame().set_facecolor('#ffffff') plt.xlabel('brightness change range') plt.ylabel('probability') plt.show() 图8-11 亮度对于对抗样本的鲁棒性影响示例(cw算法) 如图8-12所示,横轴代表亮度调整值,纵轴代表预测概率,实线代表原始图片预测为熊猫的概率,虚线代表对抗样本预测为熊猫的概率。我们假设概率大于80%时,分类结果可信,可见在该案例中,亮度调整范围大于–100且小于–25时,可以抵御对抗样本攻击,把原始图片和对抗样本均识别为熊猫。 图8-12 亮度对于对抗样本和原始图片的鲁棒性影响示例(cw算法) 下面我们调整对抗样本的对比度,调整范围为0.1到+2.0,每次增加0.1,并记录下对比度调整值和对抗样本预测为熊猫及定向攻击目标的概率,对抗样本使用cw算法生成,定向攻击目标标签为288,要求概率大于80%。 #验证对比度对于对抗样本的影响 std_range = np.arange(0.1,2.0,0.1) adv_288_pro = [] adv_388_pro = [] #调整范围为0.1到+2.0,每次增加0.1 for i in std_range: contrast_adv_img=np.clip((adv.copy()*i),0,255) pro_388=infer_img(contrast_adv_img.copy(),388) pro_288=infer_img(contrast_adv_img.copy(),288) print("std={} pro[388]={} pro[288]={}".format(i,pro_388,pro_288)) adv_288_pro += [pro_288] adv_388_pro += [pro_388] #横轴代表对比度调整值,纵轴代表预测概率 #实线代表对抗样本预测为熊猫的概率,虚线代表对抗样本预测为定向攻击目标的概率 fig, ax = plt.subplots() ax.plot(np.array(std_range), np.array(adv_288_pro), 'b--', label= 'probability of class 288') ax.plot(np.array(std_range), np.array(adv_388_pro), 'r', label='probability of class 388') legend = ax.legend(loc='best', shadow=true, fontsize='large') legend.get_frame().set_facecolor('#ffffff') plt.xlabel('contrast change range') plt.ylabel('probability') plt.show() 如图8-13所示,横轴代表对比度调整值,纵轴代表预测概率,实线代表对抗样本预测为熊猫的概率,虚线代表对抗样本预测为定向攻击目标的概率。我们假设概率大于50%时,分类结果可信,可见在该案例中,对比度调整范围小于0.75或者大于1.25时,对抗样本定向攻击失效。当亮度调整范围小于–0.5或者大于1.5时,对抗样本预测为定向攻击目标的概率几乎为0。 图8-13 对比度对于对抗样本的鲁棒性影响示例(cw算法) 下面我们调整原始图像和对抗样本的对比度,调整范围为0.1到2.0,每次增加0.1,并记录下对比度调整值和预测为熊猫对应标签的概率,对抗样本使用cw算法生成,定向攻击目标标签为288,要求概率大于80%。 #综合分析对比度对于对抗样本和正常图片分类的影响 std_range = np.arange(0.1,2.0,0.1) original_pro = [] adv_pro = [] #调整范围为0.1到2.0,每次增加0.1 for i in std_range: contrast_adv_img=np.clip((adv.copy()*i),0,255) pro_388=infer_img(contrast_adv_img.copy(),388) adv_pro+= [pro_388] contrast_img=np.clip((orig.copy()*i),0,255) pro=infer_img(contrast_img.copy(),388) original_pro += [pro] print("std={} adv_pro[388]={} original_pro[388]={}".format(i,pro_388,pro)) #横轴代表对比度调整值,纵轴代表预测概率 #实线代表原始图片预测为熊猫的概率,虚线代表对抗样本预测为熊猫的概率 fig, ax = plt.subplots() ax.plot(np.array(std_range), np.array(adv_pro), 'b--', label='probability of adversarial') ax.plot(np.array(std_range), np.array(original_pro), 'r', label='probability of original') legend = ax.legend(loc='best', shadow=true, fontsize='large') legend.get_frame().set_facecolor('#ffffff') plt.xlabel('contrast change range') plt.ylabel('probability') plt.show() 如图8-14所示,横轴代表对比度调整值,纵轴代表预测概率,实线代表原始图片预测为熊猫的概率,虚线代表对抗样本预测为熊猫的概率。我们假设概率大于50%时,分类结果可信,可见在该案例中,无法做到同时兼顾对抗样本和正常图片的识别。 图8-14 对比度对于对抗样本和原始图片的鲁棒性影响示例(cw算法) 8.1.4 噪声对鲁棒性的影响 8.1.4 噪声对鲁棒性的影响 第4章介绍了机器视觉领域常见的几种噪声,比如高斯噪声和椒盐噪声。下面我们结合实际的例子来介绍高斯噪声和椒盐噪声对鲁棒性的影响,对应的代码路径为: https://github.com/duoergun0729/adversarial_examples/blob/master/code/8- case4-cw.ipynb 首先使用cw算法生成对抗样本,定向攻击目标标签为288,要求概率大于80%。 skimage库提供了skimage.util.random_noise函数用于增加噪声。 skimage.util.random_noise(image, mode=’gaussian’, seed=none, clip=true, **kwargs) 其中主要参数的含义如下。 ? image:原始图像。 ? mode:叠加噪声的类型,常用的类型包括gaussian、salt、pepper和s & p。 ? seed:随机数种子。 ? clip:表示是否截断超出范围的值,默认为true。 ? amount:噪声点的比例,默认为0.05。 当使用高斯噪声时,还可以设置均值mean和方差var,默认均值为0,方差为0.01。需要特别指出的是,该函数的返回值是归一化后的图像数据。 如图8-15所示,针对熊猫图像分别叠加均值为0,标准差为0.05、0.10和0.15的高斯噪声,可见均值一定时,方差越大噪声效果越明显,图像越难分辨。 import skimage from scipy import * from matplotlib import pyplot as plt img=cv2.imread("../picture/cropped_panda.jpg") img=cv2.cvtcolor(img, cv2.color_bgr2rgb) img1=img.copy() img2=img.copy() img2=skimage.util.random_noise(img2, mode="gaussian", seed=none, clip=true,mean=0,var=0.05**2) img3=img.copy() img4=img.copy() img3=skimage.util.random_noise(img3, mode="gaussian", seed=none, clip=true,mean=0,var=0.10**2) img4=skimage.util.random_noise(img4, mode="gaussian", seed=none, clip=true,mean=0,var=0.15**2) 图8-15 高斯噪声叠加效果示例 下面我们在对抗样本上叠加高斯噪声,均值为0,标准差调整范围为0.01到0.20,每次增加0.01,并记录下标准差的值和对抗样本预测为熊猫及定向攻击目标的概率。 #验证高斯噪声对于对抗样本的影响 import skimage #标准差调整范围为0.01到0.20 std_range = np.arange(0.01,0.20,0.01) adv_288_pro = [] adv_388_pro = [] for i in std_range: #记录下标准差的值和对抗样本预测为熊猫及定向攻击目标的概率 gaussian_img=skimage.util.random_noise(adv.copy(), mode="gaussian", seed=none, clip=true,mean=0,var=i**2) pro_388=infer_img(gaussian_img.copy()*256.0,388) pro_288=infer_img(gaussian_img.copy()*256.0,288) print("std={} pro[388]={} pro[288]={}".format(i,pro_388,pro_288)) adv_288_pro += [pro_288] adv_388_pro += [pro_388] #横轴代表高斯噪声标准差的值,纵轴代表预测概率 #实线代表对抗样本预测为熊猫的概率,虚线代表对抗样本预测为定向攻击目标的概率 fig, ax = plt.subplots() ax.plot(np.array(std_range), np.array(adv_288_pro), 'b--', label= 'probability of class 288') ax.plot(np.array(std_range), np.array(adv_388_pro), 'r', label='probability of class 388') legend = ax.legend(loc='best', shadow=true, fontsize='large') legend.get_frame().set_facecolor('#ffffff') plt.xlabel('std range') plt.ylabel('probability') plt.show() 如图8-16所示,横轴代表高斯噪声标准差的值,纵轴代表预测概率,实线代表对抗样本预测为熊猫的概率,虚线代表对抗样本预测为定向攻击目标的概率。我们假设概率大于50%时,分类结果可信,可见在该案例中,高斯噪声的标准差大于0.01时,对抗样本定向攻击失效。当高斯噪声的标准差大于0.25时,对抗样本预测为定向攻击的目标的概率几乎为0。当亮度调整范围大于约0.01并且小于0.1时,模型可以把对抗样本识别为熊猫,没有被欺骗。 下面我们在原始图像和对抗样本上均叠加高斯噪声,均值为0,标准差调整范围为0.01到0.20,每次增加0.01,并记录下标准差的值和预测为熊猫对应标签的概率。 import matplotlib.pyplot as plt import skimage #综合分析高斯噪声对于对抗样本和正常图片分类的影响 std_range = np.arange(0.01,0.20,0.01) original_pro = [] adv_pro = [] for i in std_range: gaussian_adv_img=skimage.util.random_noise(adv.copy(), mode="gaussian", seed=none, clip=true,mean=0,var=i**2) pro_388=infer_img(gaussian_adv_img.copy()*256.0,388) adv_pro+= [pro_388] gaussian_img=skimage.util.random_noise(orig.copy(), mode="gaussian", seed=none, clip=true,mean=0,var=i**2) pro=infer_img(gaussian_img.copy()*256.0,388) original_pro += [pro] print("std={} adv_pro[388]={} original_pro[388]={}".format(i,pro_388,pro)) #横轴代表高斯噪声的标准差的值,纵轴代表预测概率 #实线代表原始图片预测为熊猫的概率,虚线代表对抗样本预测为熊猫的概率 fig, ax = plt.subplots() ax.plot(np.array(std_range), np.array(adv_pro), 'b--', label='probability of adversarial') ax.plot(np.array(std_range), np.array(original_pro), 'r', label='probability of original') legend = ax.legend(loc='best', shadow=true, fontsize='large') legend.get_frame().set_facecolor('#ffffff') plt.xlabel('std range') plt.ylabel('probability') plt.show() 图8-16 高斯噪声对对抗样本的鲁棒性影响示例(cw算法) 如图8-17所示,横轴代表高斯噪声的标准差的值,纵轴代表预测概率,实线代表原始图片预测为熊猫的概率,虚线代表对抗样本预测为熊猫的概率。我们假设概率大于50%时,分类结果可信,可见在该案例中,当高斯噪声的标准差大于0.01且小于0.08时,可以兼顾对抗样本和正常图片的识别,对抗样本和原始图片被识别为熊猫的概率均大于50%。 图8-17 高斯噪声对对抗样本和原始图片的鲁棒性影响示例(cw算法) 下面我们来看下椒盐噪声的情况。如图8-18所示,针对熊猫图像分别叠加比例为5%、10%和20%的均值和标准差为默认值的椒盐噪声,可见比例越大噪声效果越明显,图像越难分辨。 #演示增加椒盐噪声 import numpy as np import cv2 import skimage from scipy import * from matplotlib import pyplot as plt img=cv2.imread("../picture/cropped_panda.jpg") img=cv2.cvtcolor(img, cv2.color_bgr2rgb) img1=img.copy() img2=img.copy() img2=skimage.util.random_noise(img2, mode="s&p", seed=none, clip=true,amount=0.05) img3=img.copy() img4=img.copy() img3=skimage.util.random_noise(img3, mode="s&p", seed=none, clip=true,amount=0.10) img4=skimage.util.random_noise(img4, mode="s&p", seed=none, clip=true,amount=0.20) 图8-18 椒盐噪声叠加演示效果 下面我们在对抗样本上叠加椒盐噪声,均值和标准差为默认值,椒盐噪声占原始图像的比例调整范围为0.1%到2%,每次增加0.1%,并记录下椒盐噪声的比例和对抗样本预测为熊猫和定向攻击目标的概率。 #验证椒盐噪声对于对抗样本的影响 import skimage std_range = np.arange(0.001,0.02,0.001) adv_288_pro = [] adv_388_pro = [] #椒盐噪声占原始图像的比例调整范围为0.1%到2%,每次增加0.1% for i in std_range: sp_img=skimage.util.random_noise(adv.copy(), mode="s&p", seed=none, clip=true,amount=i) pro_388=infer_img(sp_img.copy()*256.0,388) pro_288=infer_img(sp_img.copy()*256.0,288) print("std={} pro[388]={} pro[288]={}".format(i,pro_388,pro_288)) adv_288_pro += [pro_288] adv_388_pro += [pro_388] #横轴代表椒盐噪声标准差的值,纵轴代表预测概率 #实线代表对抗样本预测为熊猫的概率,虚线代表对抗样本预测为定向攻击目标的概率 fig, ax = plt.subplots() ax.plot(np.array(std_range), np.array(adv_288_pro), 'b--', label= 'probability of class 288') ax.plot(np.array(std_range), np.array(adv_388_pro), 'r', label='probability of class 388') legend = ax.legend(loc='best', shadow=true, fontsize='large') legend.get_frame().set_facecolor('#ffffff') plt.xlabel('amount range') plt.ylabel('probability') plt.show() 如图8-19所示,横轴代表椒盐噪声标准差的值,纵轴代表预测概率,实线代表对抗样本预测为熊猫的概率,虚线代表对抗样本预测为定向攻击目标的概率。我们假设概率大于50%时,分类结果可信,可见在该案例中,椒盐噪声的比例大于0.25%时,对抗样本定向攻击失效,且对抗样本预测为定向攻击目标的概率几乎为0。当比例调整范围小于1%时,模型可以把对抗样本识别为熊猫,没有被欺骗。 图8-19 椒盐噪声对对抗样本的鲁棒性影响示例(cw算法) 下面我们在原始图像和对抗样本上叠加椒盐噪声,均值和标准差为默认值,椒盐噪声的占原始图像的比例调整范围为0.1%到2%,每次增加0.1%,并记录下标准差的值和预测为熊猫对应标签的概率。 import matplotlib.pyplot as plt import skimage #综合分析椒盐噪声对于对抗样本和正常图片分类的影响 std_range = np.arange(0.001,0.02,0.001) original_pro = [] adv_pro = [] for i in std_range: sp_adv_img=skimage.util.random_noise(adv.copy(), mode="s&p", seed=none, clip=true,amount=i) pro_388=infer_img(sp_adv_img.copy()*256.0,388) adv_pro+= [pro_388] sp_img=skimage.util.random_noise(orig.copy(), mode="s&p", seed=none, clip=true,amount=i) pro=infer_img(sp_img.copy()*256.0,388) original_pro += [pro] print("amount={} adv_pro[388]={} original_pro[388]={}".format(i,pro_388,pro)) #横轴代表椒盐噪声的比例,纵轴代表预测概率 #实线代表原始图片预测为熊猫的概率,虚线代表对抗样本预测为熊猫的概率 fig, ax = plt.subplots() ax.plot(np.array(std_range), np.array(adv_pro), 'b--', label='probability of adversarial') ax.plot(np.array(std_range), np.array(original_pro), 'r', label='probability of original') legend = ax.legend(loc='best', shadow=true, fontsize='large') legend.get_frame().set_facecolor('#ffffff') plt.xlabel('amount range') plt.ylabel('probability') plt.show() 如图8-20所示,横轴代表椒盐噪声的比例,纵轴代表预测概率,实线代表原始图片预测为熊猫的概率,虚线代表对抗样本预测为熊猫的概率。我们假设概率大于50%时,分类结果可信,可见在该案例中,当椒盐噪声比例大于0.2%且小于1.0%时,可以兼顾对抗样本和正常图片的鉴别,对抗样本和原始图片被识别为熊猫的概率均大于50%。 图8-20 椒盐噪声对对抗样本和原始图片的鲁棒性影响示例(cw算法) 8.2.1 图像预处理 8.2.1 图像预处理 在8.1节中我们介绍了对抗样本的鲁棒性,经过实验我们可以初步判断图像的旋转、滤波器、对比度、亮度以及噪声都会对对抗样本的鲁棒性造成影响,因此在抵御对抗样本的攻击时,可以在图像预处理环节适当增加以上步骤,具体的参数,比如旋转的角度、滤波器的设置等要根据实际的模型和数据集进行优化调整,这里不再赘述。 8.2.2 对抗训练 8.2.2 对抗训练 对抗训练(adversarial training)的基本流程如图8-21所示,首先使用常见的对抗样本算法,针对被攻击模型生成大量的对抗样本,然后把对抗样本和原始数据放到模型里重新训练,进行有监督学习,这样就获得了加固后的模型。对抗训练容易实现,读者可以使用advbox之类的对抗样本工具箱非常便捷地生成大量对抗样本。 图8-21 对抗训练的基础过程 8.2.3 高斯数据增强 8.2.3 高斯数据增强 高斯数据增强(gaussian data augmentation)最早由zantedeschi等人在论文《efficient defenses against adversarial attacks》中提出。高斯数据增强的原理非常简单,虽然对抗训练落地非常方便,但是问题也显而易见,就是难以穷尽所有的对抗样本,始终还是处于被动挨打的地位。那么是否有种方法可以尽量多地穷尽对抗样本呢?高斯数据增强算法认为,绝大多数的对抗样本相当于在原始图像上叠加了噪声,理想情况下可以用高斯噪声模拟这种噪声。高斯数据增强的流程如图8-22所示,在模型的训练环节中,在原始数据的基础上叠加高斯噪声,然后进行监督学习,这样训练出来的模型就是加固后的模型。 图8-22 高斯数据增强的基础过程 advbox上实现了高斯数据增强,并提供了演示示例。 https://github.com/baidu/advbox/tree/master/tutorials 首先需要生成攻击用的模型,advbox的测试模型是一个识别mnist的cnn模型,模型保存在mnist目录下。 python mnist_model.py 接着运行gaussianaugmentation加固的模型,模型保存在mnist-gad目录下。 python mnist_model_gaussian_augmentation_defence.py 最后运行攻击代码,攻击gaussianaugmentation加固后的cnn模型。 python mnist_tutorial_defences_gaussian_augmentation.py 运行结果如下,首先攻击没有加固的cnn模型,攻击成功率为54.6%。 [test_dataset]: fooling_count=282, total_count=500, fooling_rate=0.564000 fgsm attack done without any defence 攻击加固后的cnn模型,攻击成功率下降至36.2%。 [test_dataset]: fooling_count=181, total_count=500, fooling_rate=0.362000 fgsm attack done with gaussianaugmentationdefence 8.2.4 自编码器去噪 8.2.4 自编码器去噪 自编码器(auto encoders)是深度学习中常见的一种模型。人类在理解复杂事物的时候,总是先总结初级的特征,然后从初级特征中总结出高级的特征。如图8-23所示,以识别手写数字为例,通过学习总结,发现可以把手写数字表示为几个非常简单的子图案的组合。 图8-23 将数字图案拆分成几个小图案的组合 对大量黑白风景照片提取16x16的图像碎片,分析研究后发现几乎所有的图像碎片都可以由64种正交的边组合得到。声音也存在相同的情况,大量未标注的音频中可以得到20种基本结构,绝大多数声音都可以由这些基本的结构线性组合得到。这就是特征的稀疏表达,通过少量的基本特征组合、拼装得到更高层抽象的特征。自编码器模型正好可以用于自动化地完成这种特征提取和表达的过程,而且整个过程是无监督的。基本的自编码器模型是一个简单的三层神经网络结构,如图8-24所示,由一个输入层、一个隐藏层和一个输出层组成,其中输出层和输入层具有相同的维数。 自编码器如图8-25所示,输入层和输出层分别代表神经网络的输入层和输出层,隐藏层承担编码器和解码器的工作,编码的过程就是从高维度的输入层转化到低维度的隐藏层的过程,反之,解码过程就是低维度的隐藏层到高维度的输出层的转化过程,可见自编码器是个有损转化的过程,通过对比输入和输出的差别来定义损失函数。训练的过程不需要对数据进行标记,整个过程就是不断求解损失函数最小化的过程,这也是自编码器名字的由来。 图8-24 自编码器模型神经网络结构 图8-25 自编码器原理图 自编码器通过学习数据集自身的特征,在一定程度上能过滤掉叠加到原始数据上的不规则的噪音,因此自编码器也通常被用于去噪。我们正是利用了自编码器去噪这一特性,来去除对抗样本中误导模型识别的噪音。如图8-26所示,在一个典型的去噪自编码器中,输入的是原始图像加上随机生成的噪声,通常随机噪声使用高斯噪声,然后经过解码器和编码器处理后,获得还原后的图像,通过计算原始图像和还原图像的损失函数,优化器调整解码器和编码器的参数,让损失函数的值趋向最小。经过若干轮迭代训练后,去噪自编码器可以从输入中去除噪音,让还原的图像尽可能接近原始图像。 下面我们结合实际的例子来介绍如何使用去噪自编码器抵御对抗样本攻击,对应的代码路径为: https://github.com/duoergun0729/adversarial_examples/blob/master/code/8- case5.ipynb 首先我们需要定义去噪自编码器,数据集依然使用mnist,对应的编码器使用cnn结构,第一层使用64个3x3的卷积去处理,激活函数是relu;第二层是池化层,使用最大值池化,池化的大小为2x2;第三层使用128个3x3的卷积去处理,激活函数是relu;第四层是池化层,使用最大值池化,池化的大小为2x2。 self.encoder = nn.sequential( nn.conv2d(1,64,kernel_size=3,stride=1,padding=1), nn.relu(), nn.maxpool2d(kernel_size=2,stride=2), nn.conv2d(64,128,kernel_size=3,stride=1,padding=1), nn.relu(), nn.maxpool2d(kernel_size=2,stride=2), ) 图8-26 典型的去噪自编码器结构 编码器的结构与解码器十分类似,基本就是逆置的解码器,第一层是升采样层,放大系数为2;第二层是卷积层,输入的通道数为128,输出的通道数为64,卷积核大小为3x3,激活函数为relu;第三层是升采样层,放大系数为2;第四层是卷积层,输入的通道数为64,输出的通道数为1,卷积核大小为3x3。 self.decoder = nn.sequential( nn.upsample(scale_factor=2,mode="nearest"), nn.conv2d(128,64,kernel_size=3,stride=1,padding=1), nn.relu(), nn.upsample(scale_factor=2,mode="nearest"), nn.conv2d(64,1,kernel_size=3,stride=1,padding=1), ) 整个去噪自编码器的前向传递定义如下: def forward(self, x): output = self.encoder(x) output = self.decoder(output) return output 实例化去噪自编码器,损失函数使用mseloss,即均方差误差,优化器使用adam。 autoencoder = autoencoder().to(device) optimizer = torch.optim.adam(autoencoder.parameters(), lr=0.01) loss_func = nn.mseloss() 加载mnist的训练数据,批处理大小为128,随机打散顺序。 train_data=datasets.mnist('../advbox/tutorials/mnist-pytorch/data', train=true, download=true, transform=transforms.compose([ transforms.totensor(), ])) train_loader = torch.utils.data.dataloader( dataset=train_data, batch_size=128, shuffle=true) 迭代训练,在原始数据inputs上叠加高斯噪声,噪声的标准差为0.1,均值为0,并对超过(0.0,1.0)范围的数据进行截断。 #迭代训练10轮 for epoch in range(10): for i, data in enumerate(train_loader): inputs, labels = data inputs, labels = inputs.to(device), labels.to(device) #增加噪声 inputs_noise=inputs+0.1*torch.randn(inputs.shape).to(device) inputs_noise=torch.clamp(inputs_noise,0.0,1.0) 计算解码器的输出和原始数据之间的损失值,通过优化器调整去噪自编码器的各层参数。 output = autoencoder(inputs_noise) loss = loss_func(output, inputs) optimizer.zero_grad() loss.backward() optimizer.step() 下面我们验证去噪自编码器对正常图片识别的影响。使用的是预训练的mnist识别模型,数据集是mnist的测试集。 #使用mnist测试数据集随机挑选total_num个测试数据 # pytorch下的mnist数据集默认已归一化 test_loader = torch.utils.data.dataloader( datasets.mnist('../advbox/tutorials/mnist-pytorch/data', train=false, download=true, transform=transforms.compose([ transforms.totensor(), ])), batch_size=1, shuffle=true) 加载预训练的模型,并设置为预测模式。定义全局计数器,包括样本总数,原始图片正常识别的个数,经过去噪后的图片正确识别的个数。 # 网络初始化 model = net().to(device) # 加载模型 model.load_state_dict(torch.load(pretrained_model, map_location='cpu')) # 设置为预测模式 model.eval() #统计总数 total_count = 0 #去噪前正确识别个数 pre_count=0 #去噪后正确识别个数 decoded_count = 0 遍历测试数据集,分别统计原始图片正常识别的个数,经过去噪后的图片正确识别的个数。 for i, data in enumerate(test_loader): inputs, labels = data inputs, labels = inputs.to(device), labels.to(device) total_count += 1 #去噪前 pre_label=np.argmax(model(inputs).data.cpu().numpy()) if pre_label == labels[0]: pre_count+=1 #使用自编码器去噪 output = autoencoder(inputs) output=output.view(1,1,28,28) decoded_label=np.argmax(model(output).data.cpu().numpy()) if decoded_label == labels[0]: decoded_count+=1 随机选择1000个测试数据,原始图片的识别率为99.1%,去噪后的识别率为98.2%。 [test_dataset]: pre_count=991, total_count=1000, pre_count_rate=0.991000 decoded_count=982 decoded_count_rate=0.982000 最后使用advbox的fgsm对mnist的测试数据集进行无定向攻击,fgsm的攻击步长为0.01。 # advbox示例 m = pytorchmodel( model, loss_func,(0, 1), channel_axis=1) #实例化fgsm attack = fgsm(m) #设置攻击步长为0.1 attack_config = {"epsilons": 0.01} # 使用测试数据生成对抗样本 total_count = 0 # 去噪前的攻击成功个数 fooling_count = 0 # 去噪后的攻击成功个数 decoded_fooling_count = 0 #记录原始数据经过自编码器去噪后可以正常识别的个数 decoded_count = 0 随机选择1000个测试数据,fgsm无定向攻击原始数据的成功率为100%,经过去噪后成功率下降到11.6%。可以看出,在本例中去噪自编码器对原始图片的识别影响不大,但是可以有效抵御对抗样本的攻击。 [test_dataset]: fooling_count=1000, total_count=1000, fooling_rate=1.000000 decoded_fooling_count=116 decoded_fooling_count_rate=0.116000 fgsm attack done shixiang gu等人在论文“towards deep neural network architectures robust to adversarial examples”中进行了更加深入的研究,并提出了针对去噪自编码器的一种改进算法contractive autoencoder(cae),即收缩自编码器,有兴趣的读者可以阅读该论文进一步了解。 8.2.5 ICLR 2018提出的对抗样本抵御方法 8.2.5 iclr 2018提出的对抗样本抵御方法 iclr(international conference of learning representation)是由lecun、hinton和bengio三位ai领域的元老联手发起的。近年来随着深度学习在工程实践中的成功,iclr会议也在短短的几年中发展成为ai领域的顶会。在iclr 2018中提出了8篇介绍对抗样本的抵御方法,这些方法都有意或无意地使用了一种称为梯度掩蔽(gradient masking)的方法。大多数白盒攻击通过计算模型的梯度来运行,因此如果不能通过计算得到有效的梯度,那么攻击就会失效。梯度掩蔽使得梯度变得无用,这通常是通过在某种程度上改变模型,使其不可微分,或者使其在大多数情况下具有零梯度,或者梯度点远离决策边界。 以cihang xie等人在《mitigating adversarial effects through randomization》一文中提出的随机化方法为例。如图8-27所示,cihang xie针对输入图片做了两个随机化处理,一个是随机缩放图片的大小。原始图片的大小为[229,229,3],经过处理后随机缩放到大小为[rnd,rnd,3],其中rnd的取值范围为大于等于229且小于331。第二个随机化处理就是在图片周围随机填充白色或者黑色背景,最后得到的图片大小为[331,331,3],这样一共可以有12528种组合方式。 图8-27 随机化方法原理 防御效果如图8-28所示,表格第一列为攻击算法,包含fgsm、deepfool和cw,其中fgsm-2表示fgsm攻击算法的攻击步长eps参数为2。第一行表示被攻击的模型,分别为inception-v3、resnet-v2-101、inceptionresnet-v2和ens-adv-inceptionresnet-v2,其中ens-adv-inceptionresnet-v2是nips 2017中提供的经过对抗训练的inceptionresnet-v2模型。表格中显示的百分比指的是模型正确分类的比例,因此百分比越大表明模型可以正确分类的比例越大,抵御攻击的能力越强。实验结果表明,随机化防御算法可以提高模型的防御能力,尤其对于deepfool和cw提升效果明显。另外经过对抗训练的inceptionresnet-v2模型对于fgsm具有良好的防御能力。 但是很快就有研究人员使用iclr 2018接收论文中的防御对抗样本论文作为研究对象,研究发现梯度掩蔽的使用是一种普遍现象,在8篇论文里,有7篇研究依赖于该现象。研究者应用新开发的攻击技术,解决了梯度掩蔽问题,如图8-29所示成功攻破其中的7个,示例代码位于: https://github.com/anishathalye/obfuscated-gradients 图8-28 随机化方法防御效果 图8-29 研究者应用新的攻击技术成功攻破iclr 2018中的7种防御算法 8.3 本章小结 8.3 本章小结 本章介绍了对抗样本的鲁棒性,并详细介绍了图像的旋转、滤波器、对比度、亮度以及噪声对对抗样本的鲁棒性造成的不同影响,然后介绍了常见的抵御对抗样本攻击的算法,包括图像预处理、对抗训练和高斯数据增强。针对对抗样本的防御研究也一直是ai安全的一个热点,目前对抗样本工具箱advbox和art也一直在更新对抗算法,有兴趣的读者可以持续关注。 9.1.1 l0范数 9.1.1 l0范数 l0范数是指向量中非0的元素的个数,因此扰动的l0范数指的是扰动的非0的元素的个数。以图像数据为例,针对图像数据的扰动的l0范数指的就是修改的像素数据个数。 假设img和img_adv分别代表归一化后的原始图像和对抗样本,图像的像素数据个数为: #(1*224*224*3) size=(img.shape[0])*(img.shape[1])*(img.shape[2])*(img.shape[3]) print('image size {} shape {}'.format(size,img.shape) ) 一个典型的图像数据的像素数据个数为: image size 150528 shape (1, 224, 224, 3) 扰动量可以表示为: #计算该变量 deta=img[0] - img_adv[0] 扰动的l0范数大小为: #计算绝对量 _l0 = len(np.where(np.abs(deta)>0.0)[0]) 这里需要特别介绍一下np.where函数的特性,假设向量a定义如下: a = np.arange(27).reshape(3,3,3) a=np.expand_dims(a,axis=0) a的实际数据如图9-1所示,打印其中非零的元素,输出如图9-2所示,np.where返回的结果分为4个向量,每个向量分别表示了非零元素在a中某个维度的坐标,a一共有4个维度,因此返回4个向量,每个向量的大小都为26,即非零的元素的个数。 print(np.where(np.abs(a)>0)) 图9-1 np.where示例(一) 图9-2 np.where示例(二) 因此a中非零元素的个数可以表示为: len(np.where(np.abs(a)>0)[0]) 9.1.2 l2范数 9.1.2 l2范数 l2范数是指向量各元素的平方和然后求平方根,因此扰动的l2范数指的是扰动的各元素的平方和然后求平方根。默认情况下,衡量扰动的大小都使用l2范数。以图片数据为例,针对图片数据的扰动的l2范数接近人肉眼可以察觉的扰动大小。 扰动的l2范数大小为: _l2 = np.linalg.norm(deta) 通常会使用扰动的l2范数的相对值来量化扰动的程度,相对量是针对原始图像而言的。为了便于比较,会把l2范数的相对值取整并转换成百分比。 l2=int(99*_l2 / np.linalg.norm(img[0])) + 1 9.1.3 linf范数 9.1.3 linf范数 linf范数是指向量各元素中的最大值,因此扰动的linf范数指的是扰动的各元素的最大值。 9.2.1 AdvBox简介 9.2.1 advbox简介 advbox是一款由百度安全实验室研发,在百度大范围使用的ai模型安全工具箱,目前原生支持paddlepaddle、pytorch、caffe2、mxnet、keras以及tensorflow平台,方便广大开发者和安全工程师使用自己熟悉的框架。advbox同时支持graphpipe,屏蔽了底层使用的深度学习平台,用户可以零编码,仅通过几个命令就可以对paddlepaddle、pytorch、caffe2、mxnet、cntk、scikitlearn以及tensorflow平台生成的模型文件进行黑盒攻击。本书的作者也是advbox的主要贡献者。 advbox的安装方式非常简单,直接从github上同步即可,当前最新版本为0.4。 git clone https://github.com/baidu/advbox.git 推荐的做法是直接把advbox同步到本书配套的项目的根目录下,然后直接在代码里引用即可,或者直接安装到python的系统路径下。 9.2.2 在AdvBox中使用FGSM算法 9.2.2 在advbox中使用fgsm算法 下面我们以mnist为例介绍如何在advbox中使用fgsm算法,代码路径为: https://github.com/duoergun0729/adversarial_examples/blob/master/code/ 9-advbox-mnist-fgsm.ipynb 首先运行mnist_model_pytorch.py生成识别mnist的cnn/mlp模型。 cd advbox/tutorials python mnist_model_pytorch.py 经过10轮训练,在测试集上的准确率达到了99.00%。 epoch=10 accuracy=99.00% 然后加载需要使用的python库,使用的深度学习框架为pytorch。advbox中对各种深度学习框架的封装在advbox.models中,对攻击算法的封装在advbox.attacks中。通过sys.path.append把advbox的源码添加到python的系统路径中。 import sys #添加系统路径 sys.path.append("../advbox/") import torch import torchvision from torchvision import datasets, transforms from torch.autograd import variable import torch.utils.data.dataloader as data from advbox.adversary import adversary from advbox.attacks.gradient_method import fgsm from advbox.models.pytorch import pytorchmodel from tutorials.mnist_model_pytorch import net 定义全局变量,分别为测试集的大小以及预训练的pytorch模型的路径。 total_num = 10000 pretrained_model="../advbox/tutorials/mnist-pytorch/net.pth" 获取测试数据集,在pytorch中默认完成了mnist归一化。 #使用mnist测试数据集,随机挑选total_num个测试数据 test_loader = torch.utils.data.dataloader( datasets.mnist('../advbox/tutorials/mnist-pytorch/data', train=false, download=true, transform=transforms.compose([ transforms.totensor(), ])), batch_size=1, shuffle=true) 获取当前的计算设备,当gpu可用时返回gpu,反之返回cpu。加载预训练模型,并把模型设置为预测模式,因为dropout和bn层在预测模式和训练模式时功能不一样,需要手工设置为预测模式。 device = torch.device("cuda" if torch.cuda.is_available() else "cpu") # 网络初始化 model = net().to(device) # 加载预训练模型 model.load_state_dict(torch.load(pretrained_model, map_location='cpu')) # 针对dropout层,设置为预测模式 model.eval() 实例化advbox对象,在本例中选择pytorchmodel即可。实例化fgsm对象,并设置攻击参数。其中channel_axis指的是通道字段在图像数据中的位置,pytorch中mnist数据的形状为[none,1,28,28],损失函数loss_func使用常见的交叉熵函数。 loss_func = torch.nn.crossentropyloss() # advbox示例 m = pytorchmodel( model, loss_func,(0, 1), channel_axis=1) #实例化fgsm attack = fgsm(m) #设置攻击步长为0.1 attack_config = {"epsilons": 0.1} 遍历测试数据集,进行无定向fgsm攻击。 # 使用测试数据生成对抗样本 total_count = 0 fooling_count = 0 for i, data in enumerate(test_loader): inputs, labels = data inputs, labels=inputs.numpy(),labels.numpy() total_count += 1 adversary = adversary(inputs, labels[0]) # fgsm non-targeted attack adversary = attack(adversary, **attack_config) 经过10 000次攻击,攻击成功个数为10 000,攻击成功率为100%。 attack success, original_label=9, adversarial_label=4, count=10000 [test_dataset]: fooling_count=10000, total_count=10000, fooling_rate=1.000000 fgsm attack done 9.2.3 在AdvBox中使用DeepFool算法 9.2.3 在advbox中使用deepfool算法 下面我们以imagenet 2012为例介绍如何在advbox中使用deepfool算法,代码路径为: https://github.com/duoergun0729/adversarial_examples/blob/master/code/9-advbox-imagenet-deepfool.ipynb 首先加载需要使用的python库,使用的深度学习框架为tensorflow。通过sys.path.append把advbox的源码添加到python的系统路径中。 import sys #添加系统路径 sys.path.append("../advbox/") import numpy as np from pil import image #执行命令pip install pillow安装对应库 from advbox.adversary import adversary from advbox.attacks.deepfool import deepfoolattack from advbox.models.tensorflow import tensorflowmodel import tensorflow as tf from tensorflow.examples.tutorials.mnist import input_data from tools import show_d 从tensorflow的官网下载基于imagenet 2012预训练的inception模型,解压后获得对应的pb文件classify_image_graph_def.pb。 http://download.tensorflow.org/models/image/imagenet/inception-2015-12-05.tgz 定义全局变量,包括被攻击的图片和pb文件的路径。 #定义被攻击的图片 imagename="../advbox/tutorials/cropped_panda.jpg" dirname="../advbox/classify_image_graph_def.pb" 加载图片文件,并缩放到长和宽均为224,并把形状转换成[1,224,224,3],与模型的输入层大小一致。 image=np.array(image.open(imagename).convert('rgb').resize((224,224))). astype(np.float32) orig=image.copy().astype(np.uint8) #[224,224,3]->[1,224,224,3] image=np.expand_dims(image, axis=0) 创建会话并加载pb文件,初始化会话的全局变量。 session=tf.session() def create_graph(dirname): with tf.gfile.fastgfile(dirname, 'rb') as f: graph_def = session.graph_def graph_def.parsefromstring(f.read()) _ = tf.import_graph_def(graph_def, name='') create_graph(dirname) # 初始化参数 session.run(tf.global_variables_initializer()) 获取输出层和输入层的tensor,其中需要指出的是计算图中包含针对图像的预处理环节,即解码jpg文件,这部分没有梯度,需要直接处理解码后的数据,因此需要直接把'expanddims:0'当作输入tensor。 #获取logits logits=session.graph.get_tensor_by_name('softmax/logits:0') x = session.graph.get_tensor_by_name('expanddims:0') 实例化advbox对象,在本例中选择tensorflowmodel即可,其中channel_axis指的是通道字段在图像数据中的位置,tensorflow中imagenet 2012数据的形状为[none,224,224,3]。实例化deepfool对象,并设置攻击参数,其中iterations代表最大迭代次数,overshoot代表最后一次迭代的增益系数。 # advbox示例 # 因为原始数据没有归一化,所以bounds=(0, 255) m = tensorflowmodel(session,x,none,logits,none, bounds=(0, 255),channel_axis=3,preprocess=none) #实例化deepfool,进行定向攻击 attack = deepfoolattack(m) attack_config = {"iterations": 100, "overshoot": 0.05} 进行定向攻击,攻击目标的id为651。 adversary = adversary(image,none) #麦克风 tlabel = 651 adversary.set_target(is_targeted_attack=true, target_label=tlabel) # deepfool 定向攻击 adversary = attack(adversary, **attack_config) 经过迭代计算,攻击成功。如图9-3所示,量化的扰动量l0为60%,l2为1%,其中修改的像素个数为90081,但是l2大小仅为1.62。 deepfool.py[line:121] info iteration=26, f[pre_label]=-0.232765421271, f[target_label]=10.9849853516, f[adv_label]=10.9849853516, pre_label=169, adv_label=651 attack success, adversarial_label=651 deepfool attack done image size 150528 shape (1, 224, 224, 3) noise l_0 norm: 90081 60% noise l_2 norm: 1.619139447 1% noise l_inf norm: 0.046875 1% 图9-3 advbox下deepfool定向攻击效果图 9.2.4 在AdvBox中使用黑盒攻击算法 9.2.4 在advbox中使用黑盒攻击算法 首先介绍一下graphpipe,graphpipe是甲骨文开源的通用深度学习模型部署框架。官方对graphpipe的定义为,这是一种协议和软件集合,旨在简化机器学习模型部署并将其与特定于框架的模型实现分离。甲骨文表示,这一新工具可提供跨深度学习框架的模型通用api、开箱即用的部署方案以及强大的性能。 graphpipe为在网络上传递张量数据提供了一个标准、高性能的协议,以及客户端和服务器的简单实现,因而使得从任何框架部署和查询机器学习模型变得轻而易举。graphpipe的高性能服务器支持tensorflow、pytorch、mxnet、cntk和caffe2等,如图9-4所示。 图9-4 graphpipe架构图 advbox同时支持graphpipe,屏蔽了底层使用的深度学习平台,用户可以零编码,仅通过几个命令就可以对paddlepaddle、pytorch、caffe2、mxnet、cntk、scikitlearn以及tensorflow平台生成的模型文件进行黑盒攻击,如图9-5所示。 advbox提供了零编码黑盒攻击工具。以tensorflow为例,tensorflow提供了丰富的预训练模型,假设攻击常见的图像分类模型squeezenet。首先在docker环境下启动基于graphpipe的预测服务,graphpipe环境已经完全封装在docker镜像中,不用单独安装。 docker run -it --rm -e https_proxy=${https_proxy} -p 9000:9000 sleepsonthefloor/graphpipe-tf:cpu --model=https://oracle.github.io/graphpipe/models/squeezenet.pb --listen=0.0.0.0:9000 图9-5 advbox对graphpipe的支持原理 如果网速有限,可以先下载squeezenet.pb,使用本地模式启动。 docker run -it --rm -e https_proxy=${https_proxy} -v "$pwd:/models/" -p 9000:9000 sleepsonthefloor/graphpipe-tf:cpu --model=/models/squeezenet.pb --listen=0.0.0.0:9000 之后启动攻击脚本,使用默认参数即可,仅需指定攻击的url。目前提供的黑盒攻击算法为localsearch。 python advbox_tools.py -u http://your ip:9000 经过迭代攻击后,展现攻击结果如图9-6所示,具体运行时间依赖于网速,强烈建议在本机上运行docker服务,可以大大提升攻击速度。 localsearch.py[line:293] info try 3 times selected pixel indices:[ 0 23 24 25 26] localsearch.py[line:308] info adv_label=504 adv_label_pro=0.00148941285443 localsearch.py[line:293] info try 4 times selected pixel indices:[ 0 22 23 24 25] localsearch.py[line:308] info adv_label=463 adv_label_pro=0.00127408828121 attack success, original_label=504, adversarial_label=463 save file :adversary_image.jpg localsearchattack attack done. cost time 100.435777187s 图9-6 在advbox中进行黑盒攻击效果图 onnx(open neural network exchange),即开放的神经网络切换。顾名思义,该项目的目的是让不同的神经网络开发框架做到互通互用。开发者能更方便地在不同框架间切换,为不同任务选择最优工具。每个框架基本都会针对某个特定属性进行优化,比如训练速度、对网络架构的支持、能在移动设备上推理等。在大多数情况下,研发阶段最需要的属性和产品阶段是不一样的。这导致效率的降低,比如把模型转移到另一个框架导致额外的工作,造成进度延迟。使用支持onnx表示方式的框架,则大幅简化了切换过程,让开发者的工具选择更灵活。 目前paddlepaddle、pytorch、caffe2、mxnet、cntk、scikitlearn均支持把模型保存成onnx格式。对于onnx格式的文件,使用类似的命令启动docker环境即可。 docker run -it --rm -e https_proxy=${https_proxy} -p 9000:9000 sleepsonthefloor/graphpipe-onnx:cpu --value-inputs=https://oracle.github.io/graphpipe/models/squeezenet.value_ inputs.json --model=https://oracle.github.io/graphpipe/models/squeezenet.onnx --listen=0.0.0.0:9000 9.3.1 ART简介 9.3.1 art简介 art(adversarial robustness toolbox)是ibm研究团队开源的用于检测模型及对抗攻击的工具箱,帮助开发人员加强ai模型的防御性,让ai系统变得更加安全,项目地址为:https://github.com/ibm/adversarial-robustness-toolbox。 安装方法如下,当前最新版本为0.3.0: pip install adversarial-robustness-toolbox 9.3.2 在ART中使用FGSM算法 9.3.2 在art中使用fgsm算法 下面我们以mnist为例介绍如何在art中使用fgsm算法,代码路径为: https://github.com/duoergun0729/adversarial_examples/blob/master/code/9-art-mnist-fgsm.ipynb 首先加载需要使用的python库,使用的深度学习框架为keras+tensorflow。art中对各种深度学习框架的封装在art.classifiers中,对攻击算法的封装在art.attacks中。 import sys from os.path import abspath import keras.backend as k from keras.models import sequential from keras.layers import dense, flatten, conv2d, maxpooling2d, dropout import numpy as np from art.attacks.fast_gradient import fastgradientmethod from art.classifiers import kerasclassifier from art.utils import load_dataset 加载mnist数据集,训练集为x_train,测试集为x_test。 # 加载mnist数据集 (x_train, y_train), (x_test, y_test), min_, max_ = load_dataset(str('mnist')) 构造一个简单的双层cnn模型,第一层卷积核大小为3,信道数为32,第二层卷积核大小为3,信道数为64,经过2x2大小的最大值池化后,丢弃25%的数据,连接一个核数为128的全连接,再丢弃50%的数据,输出到核数为10的全连接层。 k.set_learning_phase(1) model = sequential() model.add(conv2d(32, kernel_size=(3, 3), activation='relu', input_shape=x_train.shape[1:])) model.add(conv2d(64, (3, 3), activation='relu')) model.add(maxpooling2d(pool_size=(2, 2))) model.add(dropout(0.25)) model.add(flatten()) model.add(dense(128, activation='relu')) model.add(dropout(0.5)) model.add(dense(10, activation='softmax')) model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy']) 使用adam优化器,损失函数为交叉熵,经过5轮训练后在测试集上准确率达到了98.37%。 classifier = kerasclassifier((min_, max_), model=model) classifier.fit(x_train, y_train, nb_epochs=5, batch_size=128) preds = np.argmax(classifier.predict(x_test), axis=1) acc = np.sum(preds == np.argmax(y_test, axis=1)) / y_test.shape[0] print("ntest accuracy: %.2f%%" % (acc * 100)) test accuracy: 98.37% 构造fgsm攻击算法实例,使用无定向攻击,在测试集上构造攻击样本,攻击参数eps设置为0.3,并使用原有模型进行预测。 # 用fgsm构造对抗样本 epsilon = .3 # maximum perturbation adv_crafter = fastgradientmethod(classifier) x_test_adv = adv_crafter.generate(x=x_test, eps=epsilon) # 计算对抗样本的分类结果 preds = np.argmax(classifier.predict(x_test_adv), axis=1) acc = np.sum(preds == np.argmax(y_test, axis=1)) / y_test.shape[0] print("ntest accuracy on adversarial sample: %.2f%%" % (acc * 100)) 预测结果表明,原有模型只能正确识别对抗样本中的29.53%,攻击成功率为70.47%。 test accuracy on adversarial sample: 29.53% 直观的认识,攻击扰动越大,原有模型正确识别的比例应该越低,攻击成功率应该越高。下面通过实验来验证我们的想法。我们从大到小列举常见的eps取值,然后分别计算原有模型的识别率。 eps_range = [0.01, 0.02, 0.03, 0.04, 0.05, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9] nb_correct_original = [] for eps in eps_range: x_test_adv = adv_crafter.generate(x_test, eps=eps) x_test_adv_pred = np.argmax(classifier.predict(x_test_adv), axis=1) nb_correct_original += [np.sum(x_test_adv_pred == np.argmax(y_test, axis=1))/y_test.shape[0]] 最后我们可视化识别率和eps之间的关系,其中横坐标为eps的值,纵坐标为识别率。如图9-7所示,识别率会随eps的增长而下降,并且当eps小于0.4时,识别率会随eps的增长迅速下降,当eps大于0.4后,识别率的下降十分平缓。 %matplotlib inline import matplotlib.pyplot as plt fig, ax = plt.subplots() ax.plot(np.array(eps_range), np.array(nb_correct_original), 'b--', label='original classifier') legend = ax.legend(loc='upper center', shadow=true, fontsize='large') legend.get_frame().set_facecolor('#ffffff') plt.xlabel('attack strength (eps)') plt.ylabel('correct predictions') plt.show() 图9-7 fgsm算法中识别率与eps的关系图 9.3.3 ART下使用CW算法 9.3.3 art下使用cw算法 下面我们以imagenet2012为例介绍如何在art中使用cw算法,代码路径为: https://github.com/duoergun0729/adversarial_examples/blob/master/code/ 9-art-imagenet-cw.ipynb 首先加载需要使用的python库,使用的深度学习框架为keras+tensorflow。攻击的模型是基于imagenet 2012训练的resnet50,在keras.applications.resnet50中定义。 %matplotlib inline import keras.backend as k from keras.applications import resnet50 from keras.preprocessing import image from keras.applications.imagenet_utils import decode_predictions from keras.utils import np_utils import numpy as np import tensorflow as tf import matplotlib.pyplot as plt # 加载模型 from keras.applications.resnet50 import resnet50, preprocess_input from art.classifiers import kerasclassifier 实例化基于imagenet训练的resnet50模型,其中图像数据每个像素的取值范围为0到255,迭代攻击过程中超过这个范围的值需要进行截断处理。 model = resnet50(weights='imagenet') classifier = kerasclassifier(clip_values=(0, 255), model=model) 读取测试图片,因为在resnet50中定义的输入层形状为[none,224,224,3],所以需要把图片转换成(224,224)大小,信道数保持为3不变。 image_file = "../picture/cropped_panda.jpg" image_ = image.load_img(image_file, target_size=(224, 224)) img = image.img_to_array(image_) 对测试图片(见图9-8)进行预测,keras中提供了decode_predictions把预测的标签转换成物体名称,预测的结果为熊猫(giant_panda)。 plt.imshow(img / 255) img = img[none, ...] # predict for clean image pred = classifier.predict(img) print(decode_predictions(pred)[0][0]) ('n02510455', 'giant_panda', 0.456376) 图9-8 在art中使用cw算法攻击的原始图片 首先我们尝试使用cw进行l2型无定向攻击,设置二分查找的轮数为10,每轮adam优化的最大迭代次数为100,学习速率为1e-3,c的初始值为3.125。 from art.attacks import carlinil2method # 创建cw无定向攻击 adv = carlinil2method(classifier, targeted=false, max_iter=100, binary_search_steps=10, learning_rate=1e-3, initial_const=3.125) # 生成攻击图片 img_adv = adv.generate(img) # 用模型评估 pred_adv = model.predict(img_adv) print(decode_predictions(pred_adv)[0][0]) ('n02113624', 'toy_poodle', 0.6728172) 经过10轮二分查找,cw无定向攻击成功,原模型识别为贵宾犬(toy_poodle)。如图9-9所示,量化的扰动量l0为1%,l2为1%。 from tools import show_d show_d(img/256.0,img_adv/256.0) noise l_0 norm: 1% noise l_2 norm: 1% noise l_inf norm: 1% 图9-9 在art中使用cw算法进行不定向攻击效果图 9.4.1 FoolBox简介 9.4.1 foolbox简介 foolbox由bethge lab的三名德国科学家开发,能够帮助用户在解析“黑匣子”时更轻松地构建起攻击模型,并且在名人面部识别与高知名度logo识别方面成功骗过美国热门的图片识别工具,项目地址为: https://github.com/bethgelab/foolbox 安装方法如下,当前最新版本为1.7.0: pip install foolbox 9.4.2 在FoolBox中使用JSMA算法 9.4.2 在foolbox中使用jsma算法 下面我们以imagenet 2012为例介绍如何在foolbox中使用jsma算法,代码路径为: https://github.com/duoergun0729/adversarial_examples/blob/master/code/9-foolbox-imagenet-jsma.ipynb 首先加载需要使用的python库,使用的深度学习框架为keras+tensorflow。foolbox中对各种深度学习框架的封装在foolbox.models中,对攻击算法的封装在foolbox.attacks中。攻击的模型是基于imagenet 2012训练的resnet50,在keras.applications.resnet50中定义。 import foolbox import keras import numpy as np from keras.applications.resnet50 import resnet50 实例化基于imagenet训练的resnet50模型,其中图像数据每个像素的取值范围为0到255,迭代攻击过程中超过这个范围的值需要进行截断处理。 kmodel = resnet50(weights='imagenet') preprocessing = (np.array([104, 116, 123]), 1) fmodel = foolbox.models.kerasmodel(kmodel, bounds=(0, 255), preprocessing=preprocessing) 加载foolbox自带的测试图片和对应的标签,并对其进行预测,预测的标签为282。 # 加载原始图片和对应的标签 image, label = foolbox.utils.imagenet_example() # 在keras中,resnet50 使用 bgr 而不是默认的 rgb pred = fmodel.predictions(image[:, :, ::-1]) print("label={}".format(np.argmax(pred))) 实例化jsma算法saliencymapattack,进行定向攻击,如果攻击失败会返回空,反之会返回生成的对抗样本,设置最大迭代次数为2000,扰动参数theta为0.3,每个像素最大扰动次数为7。 from foolbox.criteria import targetclassprobability #定向攻击标签值为22 target = targetclassprobability(22,p=0.5) #定向攻击 attack = foolbox.attacks.saliencymapattack(fmodel,criterion=target) # 在keras中,resnet50 使用 bgr 而不是默认的 rgb adversarial = attack(image[:, :, ::-1],label, max_iter=2000, fast=true, theta=0.3, max_perturbations_per_pixel=7) if adversarial is none: print("fail to adversarial") else: pred = fmodel.predictions(adversarial) print("label={}".format(np.argmax(pred))) 经过最多2000轮迭代,jsma定向攻击成功,原模型识别为标签22。如图9-10所示,量化的扰动量l0为1%,l2为4%,其中修改的像素个数为1286。 image size 150528 shape (1, 224, 224, 3) noise l_0 norm: 1286 1% noise l_2 norm: 6.738266468048096 4% noise l_inf norm: 0.51171875 1% 图9-10 在foolbox中使用jsma算法进行定向攻击效果图 9.4.3 在FoolBox中使用CW算法 9.4.3 在foolbox中使用cw算法 下面我们以imagenet 2012为例介绍如何在foolbox中使用cw算法,代码路径为: https://github.com/duoergun0729/adversarial_examples/blob/master/code/9-foolbox-imagenet-cw.ipynb 首先加载需要使用的python库,使用的深度学习框架为keras+tensorflow。 import foolbox import keras import numpy as np from keras.applications.resnet50 import resnet50 实例化基于imagenet训练的resnet50模型,其中图像数据每个像素的取值范围为0到255,迭代攻击过程中超过这个范围的值需要进行截断处理。 kmodel = resnet50(weights='imagenet') preprocessing = (np.array([104, 116, 123]), 1) fmodel = foolbox.models.kerasmodel(kmodel, bounds=(0, 255), preprocessing=preprocessing) 加载foolbox自带的测试图片和对应的标签,并对其进行预测,预测的标签为282。 # 加载原始图片和对应的标签 image, label = foolbox.utils.imagenet_example() # 在keras中,resnet50 使用 bgr 而不是默认的 rgb pred = fmodel.predictions(image[:, :, ::-1]) print("label={}".format(np.argmax(pred))) 实例化cw的l2算法carliniwagnerl2attack,尝试进行无定向攻击(见图9-11),如果攻击失败会返回空,反之会返回生成的对抗样本。对抗样本的预测结果为281。 #无定向攻击 attack = foolbox.attacks.carliniwagnerl2attack(fmodel) # 在keras中,resnet50 使用 bgr 而不是默认的 rgb adversarial = attack(image[:, :, ::-1],label) if adversarial is none: print("fail to adversarial") else: pred = fmodel.predictions(adversarial) print("label={}".format(np.argmax(pred))) 图9-11 在foolbox中使用cw算法进行无定向攻击效果图 然后尝试进行定向攻击,定向攻击需要指定攻击目标的标签及其对应的最小概率。 from foolbox.criteria import targetclassprobability #定向攻击标签值为22 target = targetclassprobability(22,p=0.5) #定向攻击 attack = foolbox.attacks.carliniwagnerl2attack(fmodel,criterion=target) # 在keras中,resnet50 使用 bgr 而不是默认的 rgb adversarial = attack(image[:, :, ::-1],label) if adversarial is none: print("fail to adversarial") else: pred = fmodel.predictions(adversarial) print("label={}".format(np.argmax(pred))) cw定向攻击成功,原模型识别为标签22。如图9-12所示,量化的扰动量l0为99%,l2为1%,其中修改的像素个数为150 461,但是l2大小仅为0.026。与jsma相比,定向攻击目标相同,cw的优势是l2小,jsma的优势是l0小,事实上cw也支持l0算法,但是jsma仅支持l0算法。 image size 150528 shape (1, 224, 224, 3) noise l_0 norm: 150461 99% noise l_2 norm: 0.02572029083967209 1% noise l_inf norm: 0.0007488429546356201 1% 图9-12 在foolbox中使用cw算法进行定向攻击效果图 9.5.1 Cleverhans简介 9.5.1 cleverhans简介 cleverhans是用于对机器学习模型进行对抗性攻击、防御和基准测试的python库。cleverhans的项目地址为:https://github.com/tensorflow/cleverhans。 安装方法如下,当前最新版本为2.1.0: pip install cleverhans 不过在实际使用中,我们发现cleverhans的pip安装包经常遗漏重要的文件,如图9-13所示。 图9-13 cleverhans的pip安装包错误 强烈建议读者还是直接clone整个项目。 git clone https://github.com/tensorflow/cleverhans 9.5.2 在Cleverhans中使用FGSM算法 9.5.2 在cleverhans中使用fgsm算法 下面我们以mnist为例介绍如何在cleverhans中使用fgsm算法,代码路径为: https://github.com/duoergun0729/adversarial_examples/blob/master/code/9-cleverhans-mnist-fgsm.ipynb 首先加载需要使用的python库,使用的深度学习框架为tensorflow。cleverhans中对攻击算法的封装在cleverhans.attacks中,识别mnist的模型使用modelbasiccnn。 import logging import numpy as np import tensorflow as tf from cleverhans.loss import crossentropy from cleverhans.dataset import mnist from cleverhans.utils_tf import model_eval from cleverhans.train import train from cleverhans.attacks import fastgradientmethod from cleverhans.utils import accuracyreport, set_log_level from cleverhans_tutorials.tutorial_models import modelbasiccnn 定义全局变量,其中包括训练的轮数、批处理的大小、学习速率和cnn模型的卷积核个数。 #定义全局变量 nb_epochs = 6 batch_size = 128 learning_rate = 0.001 clean_train = true backprop_through_attack = false nb_filters = 64 获取mnist数据集的训练集和测试集,以及图像数据的长宽及通道数据。 # 获取mnist数据 mnist = mnist(train_start=train_start, train_end=train_end, test_start=test_start, test_end=test_end) x_train, y_train = mnist.get_set('train') x_test, y_test = mnist.get_set('test') # 使用图像参数 img_rows, img_cols, nchannels = x_train.shape[1:4] nb_classes = y_train.shape[1] 定义模型的输入tensor以及训练参数。 # 定义输入的tf placeholder x = tf.placeholder(tf.float32, shape=(none, img_rows, img_cols, nchannels)) y = tf.placeholder(tf.float32, shape=(none, nb_classes)) # 训练一个mnist模型 train_params = { 'nb_epochs': nb_epochs, 'batch_size': batch_size, 'learning_rate': learning_rate } 定义校验函数,其中preds代表预测结果的tensor,y_set代表数据集x_set对应的真实标签。在tensorflow环境下,session加载了预先定义的计算图,输入x_set后,preds即为对应的预测结果。 def do_eval(preds, x_set, y_set, report_key, is_adv=none): acc = model_eval(sess, x, y, preds, x_set, y_set, args=eval_params) setattr(report, report_key, acc) if is_adv is none: report_text = none elif is_adv: report_text = 'adversarial' else: report_text = 'legitimate' if report_text: print('test accuracy on %s examples: %0.4f' % (report_text, acc)) 使用modelbasiccnn在训练集上进行训练,损失函数使用交叉熵。训练完毕后,在测试集上进行验证。 model = modelbasiccnn('model1', nb_classes, nb_filters) preds = model.get_logits(x) loss = crossentropy(model, smoothing=label_smoothing) def evaluate(): do_eval(preds, x_test, y_test, 'clean_train_clean_eval', false) train(sess, loss, x_train, y_train, evaluate=evaluate, args=train_params, rng=rng, var_list=model.get_params()) # 计算训练误差 if testing: do_eval(preds, x_train, y_train, 'train_clean_train_clean_eval') 经过6轮训练后,在测试集上获得了99.29%的准确率。 test accuracy on legitimate examples: 0.9929 设置fgsm的攻击参数,并初始化fastgradientmethod对象,使用测试集生成对抗样本,并使用训练好的modelbasiccnn对生成的对抗样本进行预测。 fgsm_params = { 'eps': 0.3, 'clip_min': 0., 'clip_max': 1. } # 初始化fastgradientmethod对象 fgsm = fastgradientmethod(model, sess=sess) adv_x = fgsm.generate(x, **fgsm_params) preds_adv = model.get_logits(adv_x) # evaluate the accuracy of the mnist model on adversarial examples do_eval(preds_adv, x_test, y_test, 'clean_train_adv_eval', true) 预测结果表明,modelbasiccnn仅能正确识别14.32%的对抗样本。 test accuracy on adversarial examples: 0.1432 9.5.3 在Cleverhans中进行对抗训练 9.5.3 在cleverhans中进行对抗训练 对抗训练的过程就是用生成的对抗样本和原始训练数据重新训练模型的过程,我们继续使用上例中生成的modelbasiccnn和对抗样本。为了与之前训练的模型区别开来,我们重新实例化modelbasiccnn和fgsm算法实例。 model2 = modelbasiccnn('model2', nb_classes, nb_filters) fgsm2 = fastgradientmethod(model2, sess=sess) def attack(x): return fgsm2.generate(x, **fgsm_params) loss2 = crossentropy(model2, smoothing=label_smoothing, attack=attack) preds2 = model2.get_logits(x) 生成对抗样本并重新训练模型。 adv_x2 = attack(x) train(sess, loss2, x_train, y_train, evaluate=evaluate2, args=train_params, rng=rng, var_list=model2.get_params()) 其中evaluate2函数用于对抗训练时打印中间结果。 def evaluate2(): # 计算对抗训练的模型在原始数据上的准确度 do_eval(preds2, x_test, y_test, 'adv_train_clean_eval', false) # 计算对抗训练的模型在对抗样本上的准确度 do_eval(preds2_adv, x_test, y_test, 'adv_train_adv_eval', true) 经过对抗训练,可以成功识别对抗样本中的93.55%。 test accuracy on legitimate examples: 0.9899 test accuracy on adversarial examples: 0.9355 9.6.1 NIPS对抗攻击防御赛简介 9.6.1 nips对抗攻击防御赛简介 nips对抗攻击防御是由ian goodfellow牵头组织的对抗样本领域顶级竞赛,并且该比赛也提供了一套环境供研究人员、开发人员在实际的攻防比拼中加深对对抗性样本现象和相关技术手段的理解。 该比赛包含了三个部分,分别是: ? 无目标对抗攻击,参赛者须提交无目标黑盒攻击方法。比如,给定一个输入图像,生成一个对抗图像,尽可能使一个未知分类器给出错误的分类结果。 ? 有目标对抗攻击,参赛者须提交有目标黑盒攻击方法。比如,给定一个输入图像和目标类别,生成一个对抗图像,尽可能使一个未知分类器将对抗图像分类为指定的目标类别。 ? 针对对抗攻击的防御,参赛者须提交一个对对抗样本鲁棒的分类器。 9.6.2 环境搭建方法 9.6.2 环境搭建方法 下面以nips 2017介绍如何搭建nips对抗攻击防御环境。nips 2017对抗攻击防御赛使用的数据集都是兼容imagenet 2012的图片,并且被分为两部分: ? dev数据集,在比赛开始时提供给参赛者,用来开发参赛算法,这部分包括1000张图片。 ? final数据集,此数据集并不会提供给参赛者,在最后用来评估参赛者所提交的算法。这部分包含了5000张图片。 nips 2017对抗攻击防御环境依赖docker,因此需要预先安装docker环境。如果希望使用gpu,还需要安装gpu版本的docker。 以没有gpu的ubuntu为例,可以直接通过pip工具安装docker ce for ubuntu。 sudo apt-get install docker-ce 如果需要使用gpu,应在安装了docker ce for ubuntu的基础上再安装nvidia-docker(见图9-14)。 以ubuntu和cuda 9.0为例,执行以下命令即可安装。 # 添加nvidia-docker2下载链接 curl -s -l https://nvidia.github.io/nvidia-docker/gpgkey | sudo apt-key add - distribution=$(. /etc/os-release;echo $id$version_id) curl -s -l https://nvidia.github.io/nvidia-docker/$distribution/nvidia-docker.list | sudo tee /etc/apt/sources.list.d/nvidia-docker.list sudo apt-get update # 安装 nvidia-docker2 并重启其服务 sudo apt-get install -y nvidia-docker2 sudo pkill -sighup dockerd 图9-14 nvidia-docker架构 直接从github上同步代码。 git clone https://github.com/tensorflow/cleverhans cd cleverhans/examples/nips17_adversarial_competition/ 执行初始化环境脚本,下载对应的数据集和初始化环境。 ./download_data.sh 测试代码目录下主要的三个目录分别为: ? dataset,包含dev和final数据集,默认初始化是只下载了dev数据集。 ? dev_toolkit,包含开发阶段需要使用的工具。 ? eval_infra,包含测试和提交结果使用的工具。 9.6.3 运行测试代码 9.6.3 运行测试代码 测试代码主要包含三个目录,分别为: ? sample_attacks,包含无定向攻击算法fgsm和随机添加噪声。 ? sample_targeted_attacks,包含基于单步和基于迭代优化的定向攻击算法。 ? sample_defenses,包含防御算法,有原始的inception模型base_inception_model、基于fgsm对抗训练过的inception模型adv_inception_v3以及通过集成对抗训练的ens_adv_inception_resnet_v2。 运行脚本针对dev生成对抗样本,并执行防御算法。 sh run_attacks_and_defenses.sh 运行测试脚本后,会创建临时目录,并从sample_attacks目录下加载无定向攻击算法,从sample_targeted_attacks加载定向攻击算法,从sample_defenses加载防御算法。 preparing working directory: /tmp/tmp.s2wnsf5gpj running attacks and defenses found attacks: ['fgsm', 'noop', 'random_noise'] found tageted attacks: ['iter_target_class', 'step_target_class'] found defenses: ['base_inception_model', 'ens_adv_inception_resnet_v2', 'adv_inception_v3'] 之后会通过docker环境加载对应的攻击算法和防御算法以及dev数据集。默认情况下仅使用cpu资源,如果需要使用gpu资源,需要修改run_attacks_and_defenses.sh文件,增加gpu参数。 python "${script_dir}/run_attacks_and_defenses.py" --attacks_dir="${working_dir}/attacks" --targeted_attacks_dir="${working_dir}/targeted_attacks" --defenses_dir="${working_dir}/defenses" --dataset_dir="${working_dir}/dataset" --intermediate_results_dir="${working_dir}/intermediate_results" --dataset_metadata="${working_dir}/dataset.csv" --output_dir="${working_dir}/output_dir" --epsilon="${max_epsilon}" --save_all_classification --gpu 需要特别指出的是,该环境使用metadata.json文件描述需要使用的docker镜像,以adv_inception_v3防御算法对应的metadata.json文件为例。 cleverhans/examples/nips17_adversarial_competition/dev_toolkit/sample_ defenses/adv_inception_v3/metadata.json 文件指定了cpu和gpu环境使用的docker镜像。 { "type": "defense", "container": "gcr.io/tensorflow/tensorflow:1.1.0", "container_gpu": "gcr.io/tensorflow/tensorflow:1.1.0-gpu", "entry_point": "run_defense.sh" } 在国内下载“gcr.io”域名下的镜像经常失败,可以修改为: { "type": "defense", "container": " tensorflow/tensorflow:1.1.0", "container_gpu": " tensorflow/tensorflow:1.1.0-gpu", "entry_point": "run_defense.sh" } 运行完测试代码后,结果保存在临时目录下,每次运行的临时目录都会变化。 output is saved in directory '/tmp/tmp.s2wnsf5gpj/output_dir' 查看该目录,结果保存为若干csv文件。 accuracy_on_attacks.csv all_classification.csv defense_ranking.csv targeted_attack_ranking.csv accuracy_on_targeted_attacks.csv attack_ranking.csv hit_target_class.csv 其中比较重要的几个文件内容如下: ? all_classification.csv,记录各个模型对各个攻击算法生成的对抗样本的识别结果(见图9-15)。 图9-15 all_classification.csv内容示例 ? accuracy_on_attacks.csv,记录各个无定向攻击算法生成的对抗样本被各个模型正确识别的个数(见图9-16)。 图9-16 accuracy_on_attacks.csv内容示例 ? attack_ranking.csv,记录各个无定向攻击算法的得分(见图9-17)。 图9-17 attack_ranking.csv内容示例 ? defense_ranking.csv,记录各个对抗算法的得分(见图9-18)。 图9-18 defense_ranking.csv内容示例 如图9-19所示,在kaggle上也可以直接下载对应的测试数据。下载网址为: https://www.kaggle.com/google-brain/nips-2017-adversarial-learning-development-set 图9-19 kaggle上的对抗样本测试数据集 其中images.zip包含原始图像,如图9-20所示,images.csv包含一系列图片相关数据,其中比较重要的几个字段含义如下: ? imageid,图像的id。 ? truelabel,真实的分类标签值。 ? targetclass,定向攻击对应的目标标签值 ? originallandingurl,下载该图片对应的url。 图9-20 images.csv各字段含义 9.7.1 robust-ml简介 9.7.1 robust-ml简介 robust-ml是一个轻量级的攻防对抗环境,可以很方便地在imagenet 2012这样的大型数据集上验证白盒攻击或者防御算法的有效性。 https://github.com/robust-ml/robustml 如果要实现防御算法,需要实现robustml.model.model类,如果需要实现攻击算法,需要实现robustml.attack.attack类,衡量攻防效果需要使用robustml.evaluate.evaluate函数。 robust-ml的安装方式方法非常简单,直接使用pip工具即可。 pip install robustml 通常robust-ml都在imagenet 2012数据集上进行验证,因此需要提前下载。本书提供了简易的下载脚本。 https://github.com/duoergun0729/adversarial_examples/blob/master/code/download-imagenet2012val.sh 执行该脚本后,将在data目录创建test_data目录,其中包含imagenet 2012的val数据: imagenet-classes.txt val val.txt 图片数据位于val目录,imagenet-classes.txt为分类标签对应的物体名称,val.txt包含图片文件名和对应的分类标签。 ilsvrc2012_val_00000004.jpeg 809 ilsvrc2012_val_00000005.jpeg 516 ilsvrc2012_val_00000006.jpeg 57 ilsvrc2012_val_00000007.jpeg 334 ilsvrc2012_val_00000008.jpeg 415 ilsvrc2012_val_00000009.jpeg 674 ilsvrc2012_val_00000010.jpeg 332 9.7.2 运行测试代码 9.7.2 运行测试代码 robust-ml提供了测试代码,下载路径为: 在测试代码中attack.py文件实现了pgd和fgsm攻击算法,攻击对象是inceptionv3模型。其中pgd算法是一个轻量级的攻击算法,基本思路是基于迭代优化,但是每次迭代更新时,会根据预先设置的阈值进行数据裁剪,我们结合代码详细介绍pgd算法的实现。首先继承robustml.attack.attack类实现pgd算法。 class inceptionv3pgdattack(robustml.attack.attack): 在类的初始化函数中,输入的参数分别为: #tensorflow的会话 self._sess = sess #被攻击的模型 self._model = model #每个像素允许的最大扰动,超过的将直接截断 self._epsilon = epsilon #最大迭代次数,默认为100 self._max_steps = max_steps #学习速率,默认为0.001 self._learning_rate = learning_rate #debug开关,默认为false self._debug = debug 然后定义对应的标签_label并转换成独热编码格式,损失函数定义为预测值与_label的交叉熵,并根据损失函数计算对应的梯度self._grad。 self._label = tf.placeholder(tf.int32, ()) one_hot = tf.expand_dims(tf.one_hot(self._label, 1000), axis=0) self._loss = tf.nn.softmax_cross_entropy_with_logits_v2(logits=model.logits, labels=one_hot) self._grad, = tf.gradients(self._loss, model.input) 在robust-ml中,攻击算法的主要实现集中在run函数中。如果进行定向攻击,设置定向目标为target,反之为none。 def run(self, x, y, target): mult = -1 if target is none: target = y mult = 1 计算图像扰动后的上限和下限,超过定义的最大扰动将被直接截断。迭代计算预测值、损失值和梯度值。 adv = np.copy(x) lower = np.clip(x - self._epsilon, 0, 1) upper = np.clip(x + self._epsilon, 0, 1) for i in range(self._max_steps): p, l, g = self._sess.run( [self._model.predictions, self._loss, self._grad], {self._model.input: adv, self._label: target} ) 如果满足退出条件即完成迭代,反之则使用梯度值和学习速率更新对抗样本。定向攻击时,预测标签与定向攻击目标一致时退出;无定向攻击时,预测标签与原分类标签不相同时退出。 if p != y: break adv += mult * self._learning_rate * np.sign(g) adv = np.clip(adv, lower, upper) 攻击的模型为inceptionv3,需要下载tensorflow对应的预训练版本,可直接运行setup.sh。 sh setup.sh downloading http://download.tensorflow.org/models/inception_v3_2016_08_28.tar.gz downloaded inception_v3.ckpt 调用攻击代码在run.py中进行,其中需要指定的参数主要为imagenet2012数据集的路径imagenet-path和攻击算法attack。默认情况下会攻击100张图片,如果希望修改攻击图片的范围,可以通过指定start和end来设置起始和结束的图片序号。 parser = argparse.argumentparser() parser.add_argument('--imagenet-path', type=str, required=true, help='directory containing `val.txt` and `val/` folder') parser.add_argument('--start', type=int, default=0) parser.add_argument('--end', type=int, default=100) parser.add_argument('--attack', type=str, default='pgd', help='pgd | fgsm | none') args = parser.parse_args() run.py会创建tensorflow会话并初始化inceptionv3模型。 # 创建tensorflow会话 sess = tf.session() # 初始化模型 model = inceptionv3(sess) robust-ml通过provider.imagenet封装了imagenet2012。整个攻击过程在robustml.evaluate.evaluate函数中调用并评估攻击和防御效果。其中需要特别指出的是deterministic参数表示是否随机打乱图片顺序和定向攻击的目标标签,如果想提高随机性可以设置为true,如果想让每次运行的结果保持稳定可以设置为false。 # 初始化imagenet图像文件的数据发生器 provider = robustml.provider.imagenet(args.imagenet_path, (299, 299, 3)) success_rate = robustml.evaluate.evaluate(model,attack,provider, start=args.start, end=args.end, deterministic=true, debug=true) print('attack success rate: %.2f%% (over %d data points)' % (success_ rate*100, args.end-args.start)) 运行fgsm攻击算法,攻击成功率为83%。 python run.py --imagenet-path ../data/ --attack fgsm attack success rate: 83.00% (over 100 data points) 运行pgd攻击算法,攻击成功率为100%。 python run.py --imagenet-path ../data/ --attack pgd attack success rate: 100.00% (over 100 data points) robust-ml运行的算法是定向攻击还是无定向攻击,由模型的攻击模式决定。在evaluate函数的实现中可以发现。 https://github.com/robust-ml/robustml/blob/master/robustml/evaluate.py evaluate函数会读取模型的攻击模式的targeted属性,默认都是无定向攻击。 threat_model = model.threat_model targeted = threat_model.targeted 本例中使用的攻击模式为linf,为无定向攻击。 self._threat_model =robustml.threat_model.linf(epsilon=0.01) 9.8 本章小结 9.8 本章小结 本章介绍了常见的对抗样本工具箱advbox、art、foolbox和cleverhans,并且结合实际案例演示了以上工具箱的常见使用方法。在9.2节,介绍了如何在advbox中使用fgsm算法和deepfool算法。在9.3节,介绍了如何在art中使用fgsm算法和cw算法。在9.4节,介绍了如何在foolbox中使用jsma算法和cw算法。在9.5节,介绍了如何在cleverhans中使用fgsm算法及如何进行对抗训练。最后9.6节和9.7节还介绍了如何搭建nips对抗防御环境和轻量级攻防对抗环境robust-ml。 《智能系统与技术丛书·ai安全之对抗样本入门》无错章节将持续在完结屋小说网更新,站内无任何广告,还请大家收藏和推荐完结屋!