This repository has been archived by the owner on Aug 21, 2022. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathcontent.json
1 lines (1 loc) · 137 KB
/
content.json
1
{"meta":{"title":"浙江大学超算队","subtitle":"Zhejiang University Supercomputer Team","description":"Infinit Cores & Possibility.","author":"ZJU · SCT","url":"https://zjusct.github.io","root":"/"},"pages":[{"title":"关于世界大学生超级计算机竞赛(ASC\\ISC\\SC)","date":"2018-10-01T07:12:16.000Z","updated":"2021-02-11T05:46:05.733Z","comments":true,"path":"about/index.html","permalink":"https://zjusct.github.io/about/index.html","excerpt":"","text":"竞赛目的ASC超算竞赛旨在通过大赛的平台推动各国及地区间超算青年人才交流和培养,提升超算应用水平和研发能力,发挥超算的科技驱动力,促进科技与产业创新。吸引本科生参加超级计算机竞赛以培养学生在超算并行、高性能计算方面的动手实践能力,促进课程理论知识与实践能力的结合。 竞赛内容竞赛总共由数道题目组成, 每一道赛题都是对现代科学难题的挑战,需要考验参赛队伍的分析解决问题以及团队协同作战的综合能力。比赛要求每个队伍完成超级计算机集群的设计与搭建,要求选手必须要具备驾驭计算机软硬件操作系统的整体能力,还要在自搭建的集群计算机系统上完成面向前沿科技领域难题的科学应用软件的高性能优化,要具备程序并行优化编码能力。竞赛分为多个流程:理论方案、设计模型、实现、现场展示、答辩等。 奖项设置 ASC 超算竞赛: 总冠军、亚军各1 项,ePrize 奖1 项(季军),最高计算性能HPL 冠军,最佳应用创新奖,最佳应用奖,最佳呈现奖及一等奖若干 ISC/SC 超算竞赛: 总冠军一名,授予在整体算例以及现场呈现过程中得分最高的队伍。最高计算性能:HPL 单项冠军一名,授予HPL 比赛成绩最高的队伍。最受欢迎奖一名,授予比赛期间得到ISC 参会者投票最多的队伍。难度一样,规模比ASC稍小。 浙江大学历年成绩 ASC2014 世界大学生超级计算机竞赛一等奖 ASC2015 世界大学生超级计算机竞赛一等奖 ASC2016 世界大学生超级计算机竞赛一等奖 ASC2016 世界最高计算性能冠军奖 ASC2016 世界最高计算性能打破世界纪录 ASC2018 世界大学生超级计算机竞赛初赛第四名 决赛一等奖 超算队还开设短学期课程 超算团队与信息安全竞赛、系统设计能力竞赛、程序设计竞赛一起,共同推进以计算机系统能力为基础的人才培养改革与课程建设。超算基地近5 年来的人才培养与成果积累,参与申报2017 年“系统能力建设教学成果奖评比”项目,获浙江省教学成果一等奖。与ACM算法竞赛同为浙江大学世界级学科竞赛项目 拥有保研名额。"},{"title":"Team Introduction","date":"2018-11-05T05:01:49.000Z","updated":"2021-02-11T06:02:33.956Z","comments":true,"path":"teamIntro/index.html","permalink":"https://zjusct.github.io/teamIntro/index.html","excerpt":"","text":"超算队指导老师介绍陈建海老师 陈建海,博士,浙江大学计算机学院副教授,硕士生导师。浙江大学计算机学院智能计算&系统实验室(Incas-lab)区块链负责人,浙江大学智能计算创新创业实验室(ICE-lab)负责人,浙江大学网络系统安全隐私实验室(NESA-lab)联合负责人,浙江大学超算竞赛基地(ZJU SPC)负责人,IEEE、ACM、CCF会员,CCF区块链专业委员会委员,澳大利亚斯威本科技大学访问学者。研究领域:计算机体系结构领域,涉及云计算、虚拟化、区块链,擅长区块链系统性能与安全,虚拟计算系统性能优化与调度,高性能计算并行应用优化,近似算法与博弈论运用等。 负责区块链、高性能计算方面的重点研发项目子课题2项,参与完成多项云计算虚拟化相关的国家科技部支撑计划、国家基金以及企业合作项目。累计发表SCI/EI论文20篇,申请授权专利超过60项。作为总教练,率领浙大超算队2014~2016年与2018年连续四届冲入ASC世界大学生超级计算机竞赛总决赛获一等奖,获2016世界最高Linpack计算性能冠军奖,打破世界最高计算性能世界纪录。指导的“浙大中科院粲夸克队”获2019全国并行应用开发大赛应用组银奖,指导的Designer项目获2018全球迅雷区块链应用开发大赛第一名,指导的Tako智能合约在线服务系统获2019第五届建行杯互联网+创新创业大赛银奖。 联系方式: [email protected] 王则可老师 王则可,博士,浙江大学百人计划研究员,隶属于浙大计算机学院智能所和人工智能协同创新中心。2011年获得浙大大学生仪学院的博士学位,从2012年至2013年在浙江大学生仪学院担任助理研究员。2013年至2017年在新加坡南洋理工大学和新加坡国立大学做博士后。2017年至2019年12月在苏黎世联邦理工学院系统组做博士后。担任多个国际会议(如KDD)的程序委员和多个国际期刊的评审 (例如TPDS、TCAD)。 联系方式: [email protected] 学校支持 设备介绍计算机学院超算竞赛基地 吸引本科生参加超级计算机竞赛以培养学生在超算并行、高性能计算方面的动手实践能力,促进课程理论知识与实践能力的结合 开设暑期超算集训短学期课程 学院提供超算竞赛基地,支持每年8万元经费 硬件强支撑:学校与学院支持建有13节点百万超算集群,含GPU\\MAC卡等异构设备 赞助与合作介绍 ASC2016 获英伟达硬件Tesla GPU K80 加速卡硬件赞助 ASC2018 获AMAX 赞助借用硬件Tesla GPU V100 8块 ······"},{"title":"Contact Us!","date":"2018-12-08T12:42:52.000Z","updated":"2021-02-11T05:46:05.749Z","comments":true,"path":"contact/index.html","permalink":"https://zjusct.github.io/contact/index.html","excerpt":"","text":"Send your email and Join ZJUSCT now!window.location.href =\"mailto:[email protected]\""}],"posts":[{"title":"ZJUSCT在ASC20-21预赛取得佳绩","slug":"asc21","date":"2021-02-11T05:50:05.000Z","updated":"2021-02-11T05:55:54.178Z","comments":true,"path":"2021/02/11/asc21/","link":"","permalink":"https://zjusct.github.io/2021/02/11/asc21/","excerpt":"2021年1月19日,2020-2021 ASC世界大学生超级计算机竞赛(ASC20-21)公布预赛成绩。浙江大学超算队(ZJUSCT)的三支队伍从来自全世界的300余支队伍中脱颖而出取得预赛佳绩。一支队伍以预赛总排名全球第四的成绩成功晋级决赛,另外两支队伍也获得了预赛二等奖的好成绩。","text":"2021年1月19日,2020-2021 ASC世界大学生超级计算机竞赛(ASC20-21)公布预赛成绩。浙江大学超算队(ZJUSCT)的三支队伍从来自全世界的300余支队伍中脱颖而出取得预赛佳绩。一支队伍以预赛总排名全球第四的成绩成功晋级决赛,另外两支队伍也获得了预赛二等奖的好成绩。 2020-2021 ASC世界大学生超级计算机竞赛(ASC Student Supercomputer Challenge)由亚洲超算协会、南方科技大学、浪潮集团合作举办,受亚洲及欧美相关专家和机构支持,旨在通过大赛平台推动各国及地区间超算青年人才交流和培养。 今年,由浙江大学计算机科学与技术学院陈建海副教授带队,高分子科学与工程学系的俞炯弛(队长),计算机科学与技术学院的张文捷、漆翔宇、范安东,地球科学学院的吴郁非五位同学组成的浙江大学超算队主力队在经过为期两个月的紧张激烈预赛后,以预赛全球第四的成绩晋级ASC总决赛并将于2021年5月8日前往南方科技大学参赛。由计算机科学与技术学院纪守领老师带队,来自计算机科学与技术学院的曾充(队长)、夏豪诚、薛伟、陈易、杨子晗同学组成的ASC067队与由计算机科学与技术学院沈睿老师带队,来自计算机科学与技术学院刘隽良(队长)、郭子扬、冯相、谢廷浩、史宇浩同学组成的ASC017队获得二等奖。 本届ASC20-21世界大学生超级计算机竞赛预赛除了常规HPL/HPCG等超算性能赛题外,还设有三道前沿科技相关的应用赛题:量子计算模拟赛题(QuEST),自然语言处理完型填空赛题(LE)与天体物理脉冲星搜索赛题(PRESTO)。 量子计算机被视为最有潜力的下一代计算设备。然而,量子比特的敏感性,使得量子计算机的发展非常缓慢。科学家很难使用真正的量子计算机,更深入地研究诸如粒子物理建模、密码学、基因工程、量子机器学习等重大问题。这些问题即便使用强大的超级计算机也无法有效解决。量子计算模拟赛题要求各参赛队伍在超级计算机上应用量子计算模拟常用软件QuEST(Quantum Exact Simulation Toolkit),模拟使用30个量子比特组成的量子随机线路和量子快速傅里叶变换量子线路进行计算。量子模拟可为人类在真正的量子计算机出现之前研究量子算法提供一个可靠的平台,对于研究新的算法和体系结构至关重要。 机器学习赛题则要求所有参赛队伍将基于组委会统一提供的英语完形填空考试数据集,自行训练能够理解人类语言的人工智能模型,并取得尽可能高的“考试分数”。此次使用的数据集涵盖了中国多层次的英语语言考试,包括高考、大学英语四六级考试等。教机器理解人类语言是人工智能领域最难以捉摸和长期存在的挑战之一。ASC20人工智能赛题使用面向人的任务来评估神经网络的性能,非常具备挑战性。 由于ASC20和ASC21合并举行的原因,组委会在本届ASC20-21竞赛中新增了一道极具吸引力的尖端天体物理赛题 – 脉冲星搜索赛题。此次ASC20-21提供了PRESTO官网和“中国天眼”全球最大的500米口径射电望远镜FAST的观测数据,要求参赛队在超算上实现脉冲星搜索过程的并行加速。参赛队员需要理解脉冲星搜索过程,完成搜索任务,并且对PRESTO运行过程进行分析和优化,尽可能地降低计算时间和所需资源。在预赛中,由张文捷、俞炯弛同学负责优化的脉冲星搜索赛题获得了预赛最高分。 2014年起浙江大学超算队(ZJUSCT)参加ASC世界大学生超算竞赛并累计获得一等奖4次,世界最高计算性能冠军奖并打破世界纪录1项。同时,浙江大学超算基地面向本科生开设暑期小学期课程,得到广泛好评。 浙江大学超算队(ZJUSCT)于2014年由浙江大学计算机学院发起成立,于2019年获批成为浙江大学本科生学科竞赛之一。感谢几年来浙大计算机学院、学校本科生院给予浙大超算队建设方面的大力支持。除此之外,特别感谢浙江大学计算机学院智能所和人工智能协同创新中心,浙江大学冷冻电镜中心,浙江大学定量生物中心,浙江大学物资中心,浙江大学智能计算与系统实验室,浙江大学网络系统与隐私安全实验室, 浙江大学智能计算创新创业实验室,浙江大学网络空间安全学院,英伟达公司,Intel技术有限公司,北京并行科技股份有限公司等对浙江大学超算队的大力支持。","categories":[],"tags":[{"name":"ASC","slug":"ASC","permalink":"https://zjusct.github.io/tags/ASC/"}],"author":"欧翌昕, TTFish"},{"title":"RSA加密算法及其数学思想","slug":"RSA算法","date":"2019-06-20T16:00:00.000Z","updated":"2021-02-11T05:46:05.713Z","comments":true,"path":"2019/06/21/RSA算法/","link":"","permalink":"https://zjusct.github.io/2019/06/21/RSA算法/","excerpt":"","text":"RSA加密算法及其数学思想像DES以及AES还有那些经典的古典加密算法,都属于对称密码体制,加密和解密必须要使用同一套密钥。这样一套对称的密码系统,发送信息和接收信息的双方都要掌握这个密钥才能加密和解密,任何一方泄露了密钥,数据的安全就会遭到威胁。 RSA算法是1977年由Ron Rivest, Adi Shamir, Leonard Adleman提出的公钥密码体制(public-key cryptosystem),公钥密码体制也称非对称密码体制(asymmetric cryptosystem)。 在公钥密码体制中,加密密钥与解密密钥是不同的,加密密钥简称公钥(public key),是可以公开的,解密密钥简称私钥(private key),是需要被保护的。 在一些场合下,比如说消息接收者在一个安保措施非常好的军事中心,而消息发送者是散布在世界各地的探员,我们可以只告诉这些探员公钥,他们只知道如何利用它加密,而不知道如何解密,就算被抓了,也无法威胁这套加密系统的安全性,因为能破译这些消息的人只有受到严格保护的拥有私钥的人。这也是这套非对称加密系统的强大之处。 看了看网上很多关于RSA的文章,也看了白洪欢的密码学讲义,甚至还翻了翻《离散数学及其应用》,发现对RSA的讲解都是证明完其正确性就点到为止了。 但是显然RSA的精髓是其构造过程,其设计思想。在我看来,光证明其正确性没有任何意义。除了知道它是对的,不能带来任何收获。 这篇文章除了简单的介绍RSA算法的过程,并对其正确性做一个数学证明,还会从构造法的角度仔细讨论RSA的核心设计思路。这个思路并不复杂,只要对基本的数论知识有所了解,就很容易顺着逻辑走通。文章会涉及到一些比较基础的数论知识,如果对相关知识不太熟悉,可以参考:RSA中的数论知识。 基本模式 随机选择两个不等的大素数p和q。 计算出n=p*q。 挑选一个数e,使得它和(p-1)(q-1)互素。 计算e在模(p-1)(q-1)意义下的逆元d。 公开(e,n)作为公钥。 保留(d,n)作为私钥。 加密一个数M:$Cipher=M^e\\ \\ (mod\\ n)$ 解密一条密文:$Plaintext=Cipher^d\\ \\ (mod\\ n)$ 分析RSA算法保证:在公开e和n的情况下,对于满足一定条件(小于n,不被p和q整除)的M,任何人都可以通过$Cipher=M^e \\ mod\\ n$ 来对M加密。通过保存的私钥d可以恢复:$Plaintext=Cipher^d\\equiv M\\ (mod\\ n)$。 RSA中,n是两个大素数p和q的乘积,e是挑选出来的一个与(p-1)(q-1)互素的数e,d是e在模(p-1)(q-1)意义下的乘法逆元。 这个设计非常的讲究,要理解其意义,我们先要能证明,上述解密变换的正确性。 变换的正确性$ M^{ed} \\equiv M \\ \\ (mod \\ n) $$$已知ed \\equiv 1 \\ [mod \\ (p-1)(q-1)]$$ $$则ed=k(p-1)(q-1)+1$$ $$M^{ed}=M * M^{ k(p-1)(q-1) }$$ 由费马小定理:$ p为素数,若a不能被p整除,则a^{p-1} \\equiv 1 \\ (mod\\ p) $ $$则只要M既不是p也不是q的倍数:M^{ed} \\equiv M \\ (mod\\ p) \\ \\ ; \\ \\ M^{ed} \\equiv M \\ (mod \\ q)$$ $$M^{ed}-M \\equiv 0 \\ (mod \\ p)$$ $$M^{ed}-M \\equiv 0 \\ (mod \\ q)$$ 由中国剩余定理: $$正整数m_1,m_2,m_3,……,m_n两两互素,a_1,a_2,a_3,……,a_n是任意整数。$$ $$x \\equiv a_1 \\ (mod;m_1)$$ $$x \\equiv a_2 \\ (mod;m_2)$$ $$……$$ $$x\\equiv a_n(mod \\ m_n)$$ $$有唯一的模m=m_1m_2……m_n的解。$$ $$令M_k=\\frac{m}{m_k},M_k在模m_k意义下的乘法逆元为y_k[即M_ky_k\\equiv1;(mod;m_k)]$$ $$x=a_1M_1y_1+…+a_nM_ny_n.$$ $$由于p和q互素。$$ $$因此,M^{ed}-M \\equiv 0 * (..) + 0 * (..) \\equiv 0 \\ (mod\\ n)$$ 得证 $ M^{ed}\\equiv M \\ (mod\\ n) $ 设计思想前面我们给出了RSA正确性的简单证明,可以看到如何证明RSA的正确性并不是一件困难的事。但是会证明RSA的正确性,只能说明你知道RSA“为什么对”,但是你可能并无法理解其真正的设计思想。 在RSA的设计中,我们发现有两个关键的要素:大素数p和q,(p-1)(q-1)。既然取了n=pq,为什么还要取(p-1)(q-1),为什么要取一个与之互素的e,逆元d又是为了什么?在我看来,只有能够回答上面这些问题,才能真正算得上对RSA有所理解。 这一小节,我们将会针对RSA的设计思想做一个简单的讨论。 RSA非对称加密的核心特征 其实RSA所谓的非对称无非就是利用了这样一个性质: $$ M^{ed}\\equiv M\\ (mod\\ n) $$ 这是一个怎样的性质呢?这其实就是一个恒等映射的性质:一个数字被幂函数变换后,总是等于自己。。。这里的函数$f(x)=x^{ed}$就是在模n的剩余系中的一个恒等映射罢了。 恒等映射?恒等映射意味着什么呢? 无论什么加密系统,对于加密函数$En(x)$和解密函数$De(x)$来说,它们的复合:$En \\cdot De$ 其实都是恒等映射。。。反过来讲,任何一个恒等映射,只要能被写成一对函数复合的形式,那么这对函数就可以分别作为加密函数和解密函数。。。 巧了,这里的$f(x)=x^{ed}$正好就可以做这样的分解:$x^e和x^d$。。。所以这对函数就能用来加解密。。 传统的对称加密,使用的一对加解密函数依赖于相同的密钥。但是这里函数的参数e和d可能是不同的,因此加解密依赖不同的密钥,所以就是非对称的。 恒等映射怎么来的? 了解了所谓的非对称加密的本质后。我们第二个问题自然是:这个恒等映射是怎样的? 对于RSA,它采用了幂函数的形式。 那么我们就来考察一下:幂函数的指数应该满足什么条件才能让这个映射在特定的剩余系中是一个恒等映射呢? $$ a^x\\equiv a\\ (mod\\ n) $$ 看到这个形式,很容易想到费马小定理,或者欧拉定理。 前者给出: $$ a^{p-1}\\equiv1\\ (mod\\ p) $$ 后者给出: $$ a^{\\Phi(n)}\\equiv 1\\ (mod\\ n) $$ 当然,很显然,前者是后者的一个特例。 欧拉定理的条件很宽松,只要求a和n互素。而一般来说,n取一个素数,a如果小于n,那么这个条件一定满足。或者n的素因子很少,且都很大,a比任意一个素因子都小,那么这个条件也一定能满足。 那么我们要构造这样的一个恒等映射再容易不过了,找一个比较好的n,指数x取$k*\\Phi(n)+1$就行了。 如何分解? 我们在一开始说了,一套密码系统,首先要有个恒等映射,其次这个恒等映射要可分解。 所以对于我们找到的n,它的欧拉函数必须要保证 $k*\\Phi(n)+1=ed$ 这样的分解是可操作的。 换句话说:$ed\\equiv1\\ [mod\\ \\Phi(n)]$ 再换句话说,e和d再模$\\Phi(n) $意义下互为乘法逆元? 卧槽?这句话是不是很耳熟?RSA不是求了e在模(p-1)(q-1)意义下的逆元d么? 但是你自己想想(p-1)(q-1)究竟是啥呢? $(p-1)(q-1)=\\Phi(p)*\\Phi(q)=\\Phi(pq)=\\Phi(n)$ 为啥e被要求和(p-1)(q-1)互素? 这个在之前的RSA中的数论知识也讲过,当一个数和模数互素的时候,可以保证逆元存在且唯一。所以其实我们可以看到,这里强迫e与(p-1)(q-1)互素,就是为了保证在欧拉函数$\\Phi(n) $为模的剩余中,存在这样一对乘法逆元。 既然n取一个大素数就可以很好的利用欧拉函数构造,为什么RSA还要用一对大素数的乘积作为n呢? 思考这样一个问题:现在你用一个大素数作为n,那么显然它的欧拉函数$\\Phi(n)=n-1$,你公钥里面给了e和n,我们知道d是e在模欧拉函数$\\Phi(n) $意义下的乘法逆元,那么我有了n-1,有了e,求d还不是分分钟钟的事情。。。这个根本没什么加密能力。。 于是我们退而求其:拿一个有两个大素数因子p和q的n来。现在n不是素数了,你有了e,还需要我的欧拉函数才能算出d。但是我的欧拉函数不再像素数那样就等于n-1了,要求我的欧拉函数,你必须得有我的两个素因子p和q。但是众所周知,大数的质因数分解是一个NP为题。嘿嘿嘿,现在你要破解就没那么容易了。 总结 总结起来,RSA的思路无非可以整理成这样几点: 1.我想要一个以剩余系为数域,形式为幂函数的加密系统。 2.那么我需要一个可以分解的幂函数恒等映射。 3.我发现这个恒等映射可以用欧拉函数构造。 4.构造这个恒等映射的时候,模数n要选一个稍微好点的,最好是一个质数或者是一个质因数很少且很大的数。 5.这个恒等映射还需要可分解。 6.我发现,可分解其实等价于找一对$\\Phi(n) $为模数的剩余系中的乘法逆元。这也并不是特别难的事情。。 7.好了,现在我想明白了。拿个大素数作为n试试。构造倒是构造出来了,但是发现好像公开了e和n,d很容易就算出来了。 8.那我再拿两个大素数的乘积作为n试试,现在我发现你好像要算出d必须得算出n的欧拉函数,而这必须先解决大数质因数分解,这是非常困难的,那么现在我的加密系统就显得非常安全了。 9.然后你还会想到,显然n不一定非要是两个素因子p和q的乘积,我们还可以取多个大素数的乘积来作为n,也能达到类似的效果。 安全性在设计思想中,我们已经梳理得很清楚了,RSA算法本质上利用了大数字质因数分解的困难性:对于一个N位的数字,对其进行质因数分解,时间复杂度都是关于N的指数数量级,不存在多项式复杂度的算法。","categories":[],"tags":[{"name":"密码学","slug":"密码学","permalink":"https://zjusct.github.io/tags/密码学/"},{"name":"数论","slug":"数论","permalink":"https://zjusct.github.io/tags/数论/"}],"author":"漆翔宇"},{"title":"基于GAN的动漫头像生成","slug":"Animation Avatar Generation","date":"2019-06-15T16:00:00.000Z","updated":"2021-02-11T05:46:05.525Z","comments":true,"path":"2019/06/16/Animation Avatar Generation/","link":"","permalink":"https://zjusct.github.io/2019/06/16/Animation Avatar Generation/","excerpt":"","text":"基于GAN的动漫头像生成Github Repo :https://github.com/Unispac/Animation-Avatar-Generation 项目来源本项目实践了基于GAN的动漫头像生成。 想法来源于一个名为 MakeGirlsMoe 的动漫人物生成项目: 该项目是由一组来自复旦大学和CMU的学生设计的,曾经一度在github trend上跻身Top 5,并且登上过日本雅虎的首页。项目基于GAN的图像生成技术,允许用户设置特征参数控制动漫人物的自动生成。 并且,项目原作者在最新发布的项目Crypko中更是实现了人物连续变化的精确控制,使得通过GAN生成风格连贯的动画成为可能: 除此之外,这一角色生成技术在2D绘图辅助设计上也表现不俗: 可以预见,在动画影视产业高速增长的今天,一个成熟的自动作画/辅助作画的AI系统有着非常大的市场空间。 项目内容概述在实现这个项目的过程中,我们通过阅读原论文和一些辅助材料,对GAN的技术有了一个初步的了解。受限于时间和人力,我们只实现了64*64动漫头像生成,先后基于DCGAN和ACGAN的方法实现了无条件生成和条件约束生成。 数据集来自台湾大学李宏毅老师开设的课程:MLDS。 在实验中,我们验证和研究了GAN在理论上存在的一些问题以及潜在的解决方法。我们使用原论文中提供的算法生成图像时也遇到了很多问题,通过一些分析,我们提出了一些改进手段,最后在一定程度优化了生成结果,实现了可接受的生成效果: 我们将会简单的介绍我们的实验内容和遇到的问题,给出一些分析和解决方法。最终我们将分析实验结果及其潜在的优化空间。 GAN及其固有的问题生成式对抗网络是2014年由Goodfellow等人在《Generative Adversarial Net》中提出的。 在GAN中一组Generator和Discriminator通过对下面这个式子进行minimax博弈,实现生成准确度的自强化学习: 原论文对此做了一个简单的证明,当Discriminator达到自己的纳什均衡的时候,Generator的目标函数等价于生成器产生结果的分布$p_g$和训练集数据分布$p_{data}$的JS散度。于是从理论上,我们可以得到G和D的对抗博弈本质上式一个$p_g$向$p_{data}$拟合的过程。 相对于经典的VAE方法,GAN摒弃向量差范数这种相对机械的评估方法,而采用了一个Discriminator网络来做出更加复杂的评估,因此Generator也能更好的学到像素点之间的correlation。 Goodfellow提出的初代GAN版本在应用中取得了很多出色的表现,但是也一直存在难以训练的问题。在训练的时候经常会发现当Discriminator训练得太好的时候,Generator训练时就会出现梯度消失的现象。Goodfellow也原文中也提到过这个问题,错误的把原因归结为saturation,并且提出把generator训练时使用的log(1-D(G(z)))换成-log(D(G(z))),结果是这个替代选项经常出现梯度不稳定的现象。 从2014年开始,就不断有各种各样的关于GAN的研究,大多数声称对于原始版本GAN的改进都是通过实验挑选出更加好的网络架构和超参数设置,但是都没有彻底解决上述问题。 2017年Arjovsky发表的两篇论文《TOWARDS PRINCIPLED METHODS FOR TRAINING GENERATIVE ADVERSARIAL NETWORKS》和 《Wasserstein GAN》引起了广泛的关注。前者从理论上给出了上述问题出现的原因,后者提出了WGAN来解决这个问题。 Arjovsky从数学上证明了JS散度对于$p_{data}$和$p_g$之间的距离衡量在两者重叠部分测度为0的时候是一个常数。而由于$p_{data}$和$p_g$的支撑集在高维图像空间中都是低维流形,因此两者重叠部分的期望测度也的确是0。。。这正是用JS散度来作为G的博弈均衡目标时梯度消失的根本原因。同时Goodfellow给出的替代目标函数在经过简单的数学变换后呈现处KL-JS的形式,其实是一对互相矛盾的目标,因此才会造成梯度不稳定。 因此,Arjovsky提出用Wasserstein Distance来替代JS散度。虽然Wasserstein Distance不可直接结算,Arjovsky使用了一个数学定理证明了其等价的对偶式,最后提出将Discriminator替换成一个拟合这个对偶式的网络,并通过weight clip的手段来满足这个对偶式必需的Lipschitz条件。 尽管后来有诸如WGAN-GP这样的方法提出用Gradient Penalty更好的来满足Lipschitz条件,但是我们在实验中发现,WGAN-GP虽然可能克服了DCGAN容易出现梯度消失的问题,但是在DCGAN正常训练的情况下,WGAN-GP和WGAN的生成的效果却不如DCGAN。 在采用同样的架构(WGAN-GP中仅仅是把Discriminator中的norm层去掉,且输出不再加上sigmoid限制)时,我们发现WGAN-GP生成的结果中,颜色强度很大,生成的结果不够柔和,清晰度也不如成功训练出来的DCGAN。我们认为可能存在的问题主要有:WGAN-GP中wasserstein distance对偶式的拟合需要更加精心设计的网络;需要更好的调整参数保持一个适当的Lipschitz常数。由于时间原因,我们最终没办法对这些改进的想法做验证。 在对GAN进行学习和研究的时候,我们也整理了另外两篇文档来详细的讨论GAN的思想和理论推导,分别收录在:GAN的基本想法,GAN的理论。 无条件生成无条件生成时,我们采用了一个DCGAN架构实验。 采用的基本的架构如下: 实验时采用的几个trick: Discriminator 和 Generator 都加上 BatchNormalization。 训练图片的通道分量统一除以127.5后减1,限制在-1和1之间,产生的图片由tanh将分量约束在-1~1之间。 输入的图片会以一定概率分布被随机的左右翻转以及正逆时针轻微旋转,培养模型对对称性的理解。 Z的从一个均值为0,标准差为$e^{\\frac{-1}{\\Pi}}$的正态分布中取样。 生成效果: After 1 Epoch After 3 Epoches After 10 Epoches After 30 Epoches After 50 Epoches 条件生成在无条件生成中,GAN做的是拟合由z分布以及generator参数共同隐式定义的生成分布$g_p$和数据集分布$d_{data}$。因此,生成器能做到的只是给定一个z分布中采样的向量,然后生成一个尽可能真实的图片。 Conditional GAN允许我们提供条件,对条件概率拟合,生成符合条件的结果。我们使用ACGAN实现了生成过程中对头发颜色以及眼睛颜色的控制。 ACGAN我们采用了ACGAN(《Conditional Image Synthesis With Auxiliary Classifier GANs》, Odena A et. al 2017)来实现。 如上所示,ACGAN的想法非常简单,Discriminator除了被用来给真实度评分,其提取的图像特征还被用来做分类,判定输入图片的类别。生成器除了提供一个噪音,还需要提供一个类条件。 Loss Function在无条件GAN的基础上添加了一个分类损失项,训练Discriminator的时候,会引导Discriminator对数据集中图片做出正确分类,训练Generator的时候会引导生成器产生与类条件一致的结果。 值得一提的是,原论文中没有考虑lambda的取值,我们添加了一个lambda值来平衡网络对真实度/类别的取舍。我们在实验中发现了lambda会明显的对生成结果产生影响。lambda太大,会导致分类失败,lambda太小,会影响图片生成的质量。 架构的选择我们在原先的DCGAN的discriminator架构上做了一点微调,使得它能产生两种输出: 实验结果显示,它成功的做出了分类,但是生成图片的质量却下降了: 如图,可以看到生成结果的清晰度严重降低,脸部崩坏的概率也提高了,并且再继续训练下去也没有明显的改变。 考虑到Discriminator和Generator都要处理class的信息,需要拟合的关系变得更加复杂,很有可能是因为原来的网络表示力不够。于是我们参考了采用了 MakeGirlsMoe 项目原作者使用的一套残差网络设计: 这套网络其实也是基本上基于SRResNet(《Photo-Realistic Single Image Super-Resolution Using a Generative Adversarial Network》, Christian Ledig et. al 2017)改造而成的。 但是发现产生的结果分类失败了: 如图,原本每一行应该是一样的发色和眼睛颜色,但是生成结果却没有体现出这种分类效果。不过我们发现图片质量的确非常高,如果仅用随机生成的标准来评判,它应该要优于原来的DCGAN网络结构。 我们推测很有可能是在这种网络设计下,lambda取太大,真实度的loss压过了分类的loss,使得真实度得到了很好的满足,分类却没有很好的得到保障。 我们把网络的lambda取回1后,又不幸的发现,分类的效果虽然恢复了,生成的质量又下降了。 把lambda在1~5之间都尝试后,我们发现两者有明显的冲突。质量高了,分类就变弱。分类变好了,质量又弱了。 这让我们意识到ACGAN的设计可能并不合理,ACGAN在判定真实度与判定类别的时共享一个特征提取器。但是这两个任务之间并没有必然的重叠性,就像要判断一个人是男性还是女性和判别一个对象到底是不是人可能用到的特征根本就没有太大交叠性。如果共享网络,则会导致两个任务竞争资源。 于是我们对网络做了一些折中的调整,将ResNet提取的特征分别送入两个更深(3层)的全连接层网络,再取lambda=5时。这样可以降低两个任务对模块依赖的耦合性。让两个任务的区别尽量体现在自己独占的全连接层网络上,让两者的重叠部分又ResNet去解决。 最后,我们得到了较为出色的效果: 实验结果展示250个epoch之后,我们选择了几组特征条件,随机产生: Aqua hair and Orange eyes Black hair and Yellow eyes Blond hair and Purple eyes Blue hair and Blue eyes White hair and Gray eyes Pink hair and Orange eyes 潜在的优化空间 我们发现ACGAN的设计可能并不合理,共享分类和真实度评估可能本来就不是一个好的主义。如果不考虑时间和空间,用两个网络分别分类和评估真实度应该可以期望得到更好的效果。采用其它设计类型的CGAN模型也是一个很好的选择。 采用更好的数据集。我们发现 MakeGirlsMoe 这个项目,它们之所以能生成很精致的图片,很大程度上可能是因为使用了优秀的数据集,根据原作者的介绍,它们采用的数据集中图片画风很一致,质量都很高。而我们使用的数据集是台湾大学李宏毅老师在MLDS这门课程中提供的一个toy data set,我们浏览了一下,发现里面不仅有女性人物的画像,还混入了一定比例的男性人物画像,而且有的图片本身就画得很崩。这很大程度上会为对我们的generator产生干扰。 我们由于只有64x64的训练数据,所以也只能产生64x64的结果。借助stackGAN技术和更大尺寸的训练图片,我们可以生成分辨率更高的图片。","categories":[],"tags":[{"name":"AI","slug":"AI","permalink":"https://zjusct.github.io/tags/AI/"}],"author":"漆翔宇"},{"title":"生成式对抗网络_理论","slug":"GAN_theory","date":"2019-06-13T16:00:00.000Z","updated":"2021-02-11T05:46:05.625Z","comments":true,"path":"2019/06/14/GAN_theory/","link":"","permalink":"https://zjusct.github.io/2019/06/14/GAN_theory/","excerpt":"","text":"GAN的理论在 对抗式生成网络-基础 这篇文章中,我们介绍了对抗式生成网络(Generative Adversarial Networks, GAN)的基本思想。我们已经知道,与最经典的Reflection Model不一样,GAN是通过一组用于生成的generator和用于判别的discriminator互相对抗来实现生成能力的自强化。上一篇文章中,我们只是很简单的介绍了它的intuition,但GAN本质上还是一个数学模型,为什么能实现自强化,为什么模型最后会向我们希望的方向收敛,它还需要一些更加严谨的理论推导。 这篇文章中,我们将根据《TOWARDS PRINCIPLED METHODS FOR TRAINING GENERATIVE ADVERSARIAL NETWORKS》(Arjovsky et al. 2017) 和 《Wasserstein GAN》(Arjovsky et al. 2017) 这两篇论文的推导和论述,简单的讨论一下GAN的数学理论。 在展开GAN的讨论之前,我们将会先简单的介绍几个需要用到的前置概念和知识。在这之后我们会介绍原始GAN的loss function的实际数学意义,并简单讨论其存在的问题,最后讨论解决的方法。 理论准备Jensen不等式众所周知,在一个凸函数,其定义域上的任意两点 $x_1,x_2$,在 $0\\leq t \\leq1$ 时,满足:$$tf(x_1)+(1-t)f(x_2) \\geq f[tx_1+(1-t)x_2]$$这就是Jensen不等式最著名的两点形式。Jensen的两点不等式可以被推广到任意多数量的点集${x_i}$上:当$\\lambda_i \\geq0$且$\\Sigma \\lambda_i=1$时,凸函数$f(x)$满足 $f(\\Sigma\\lambda_ix_i)\\leq \\Sigma\\lambda_if(x_i)$。用数学归纳法可以证明这一推广式。 很容易想到,当上式取极限时,我们则会得到连续域上的Jensen不等式:$$\\int p(x)dx=1;=>f[\\int p(x)xdx]\\leq \\int p(x)f(x)dx$$把上式放到概率论中,如果p(x)看作概率密度函数,那么上式显然等价于:$$f[E(x)]\\leq E[f(x)]$$ KL散度考虑一个真实的概率分布p(x)和一个对这个概率分布进行拟合的另一个近似分布q(x),我们如何评估这个近似分布好不好?换句话说,我们如何去量化这个近似评估和真实评估的差异? KL散度(Kullback-Leibler divergence)是一个选择。KL散度又常常被称为相对熵(relative entropy)或者信息散度(information divergence),是两个概率分布间差异的非对称性度量。 对于上面的两个概率分布p和q,KL散度定义如下:$$KL(p||q)=\\int p(x)ln[\\frac {p(x)}{q(x)}]dx$$直观的来看,对于点x处,KL散度将两个分布的差异损失定义为$p(x)ln[\\frac{p(x)}{q(x)}]$,显然只有两个分布都在此处取相同的概率密度,这个损失才为0,并且在真实分布p中出现频率更高的x处的损失所享有的权重也会更大。因此,从直观的角度来看,它确实在某种程度上反映了两个分布之间的拟合程度。 除了直观意义,它在数学上也有两个很好的性质。 性质1KL(p||q) >= 0, 当且仅当p=q时,等号成立.$$KL(p||q)=\\int p(x)ln[\\frac{p(x)}{q(x)}]dx\\令g(x)=\\frac{q(x)}{p(x)}\\KL(p||q)=\\int -p(x)ln[g(x)]dx\\-ln(x)为凸函数,由Jensen不等式=>\\KL(p||q)=E[-ln(g(x))]\\geq -ln[E(g(x))]\\=-ln{\\int p(x)\\frac{q(x)}{p(x)}dx}=-ln(1)=0\\得证$$ 性质2最小化KL散度等价于最大化似然函数$$q(x)是p(x)的近似\\=>q(x)=P(x;\\theta),\\theta是这个近似器的参数\\argmin;KL(p||q)=argmax;\\int p(x)ln[q(x)]-\\int p(x)ln[p(x)]\\=argmax;\\int p(x)ln[q(x)]\\\\approx argmax;\\frac{1}{N}\\Sigma;ln[q(x_i)]\\=argmax; \\frac{1}{N};\\Pi;q(x_i) \\=argmax;\\frac{1}{N} ;\\Pi;P(x_i;\\theta)$$如上所示,对于p(x)做参数估计,我们可以写出似然函数: $\\frac{1}{N};\\Pi;P(x;\\theta)$,最大化这个似然函数的直观意义就是使得这个近似分布中,尽量让实验中出现次数多的样本x取更大的概率值,这是极大似然的基本想法。 推导发现,最小化KL散度,实际上就等价于最大化这个似然函数。(这可以看作KL散度之所以是一个“良好”测度量的原因。) JS散度JS散度(Jensen-Shannon divergence),和KL散度一样,也是用来衡量两条曲线的相合性的。 在KL散度中p和q的地位不等价,交换p和q得到的结果是不一样的。如果把散度当作某种“距离”的衡量,那么在KL散度定义的距离空间中,两个对象之间的距离就是不对称的。但是在一些问题背景下,我们可能更希望使用具有对称性的“距离”。 JS散度的定义就是为了解决了KL散度的不对称问题:$$JS(P1||P2)=\\frac{1}{2}KL(P1||\\frac{P1+P2}{2})+\\frac{1}{2}KL(P2||\\frac{P1+P2}{2})$$如上所示,它直接从KL散度的定义中做了一个简单的变换,直观意义可以理解为P1和P2与这两者的中线”距离”的均值。关于其严格的数学性质,这里就不再赘述,有兴趣可以取查阅相关的资料。 初代GAN的收敛性 2014年时,Goodfellow在《Generative Adversarial Nets》中首次提出GAN的时候,使用了上面这个非常优雅的目标损失函数,这是一个非常简练的minimax博弈式,使用这套Loss,训练过程中,Discriminator会尽量的最大化真实样例的评分期望,并最小化生成的样例的评分期望,以筛选出假图片;Generator则尽量最大化自己生成的样例的期望评分,试图骗过Discriminator。其基本的思想我们在上一篇文章中已经介绍过了。 对于一个generator来说,我们一般是采样一个噪音向量z作为其输入,generator根据这个随机输入生成一个随机特征的图片。当我们使用满足$z\\sim p_z$的随机噪音的时候,也隐含的定义了生成的图片的分布$p_g$,理想的情况下,我们希望生成的图片分布尽量收敛于训练集中图像的概率分布$p_{data}$。 Goodfellow在GAN的开山之作中证明了上面式子中定义的minimax博弈在$p_g=p_{data}$时取得纳什均衡,并且使用其提出的算法(其实就是用梯度下降最优化上述目标函数)可以使得博弈达到这个纳什均衡,因此也就能达到$p_g = p_{data}$这个美好期望。 纳什均衡点对于任意一个样本x,它既有可能来自训练集,又有可能来自生成器,只不过是概率分布存在差异罢了。这个样本对于上面定义的判别器D损失的贡献为:$-P_{data}(x)logD(x)-P_g(x)log[1-D(x)]$。 固定generator的参数,上式对D求偏导 :$$Dif=\\frac{-P_{data}(x)}{D(x)}+\\frac{P_g(x)}{1-D(x)}$$导数为0时,判别器的损失取最优值,不难导出最优判别器: 这个最优判别器的直观意义很显然,一个数据x在训练集中出现的可能性是a,被生成器生成的概率是b,那么当一个最优的判别器要判断其到底是真图片还是假图片的时候,输出概率$\\frac{a}{a+b}$是非常自然的事情。 因此,对于任何一个固定的G,D能取得的最优如上。那么说白了,要求纳什均衡,也就是只需要再调整生成器的生成分布 $p_g(x)$ 的,使得上式取最小。 原论文中给出了下面的证明过程,证明了$p_g=p_{data}$时,取得纳什均衡: 前面我们已经介绍过了KL散度:$KL(p||q)=\\int p(x)ln[\\frac {p(x)}{q(x)}]dx$. 原论文中给出的证明其实就是对上面的$C(G)$做了一个变换,将其变换成散度形式:$$\\int p_{data}(x)log\\frac{p_{data}(x)}{p_{data}(x)+p_g(x)}dx+\\int p_g(x)log\\frac{p_g(x)}{p_{data}(x)+p_g(x)}dx\\=KL(p_{data}||p_{data}+p_g)+KL(p_g||p_{data}(x)+p_g(x))$$log里面分母再除个2,提出来就是它证明中的式子了:$$=KL(p_{data}||\\frac{p_{data}+p_g}{2})+KL(p_g||\\frac{p_{data}+p_g}{2})-log(4)$$然后我们就是要最小化它对吧? 我们在上面介绍KL散度的时候,已经证明了,KL>=0,当且仅当p,q相等时取0,所以上式取最小的条件就是:$p_{data}=\\frac{p_{data}+p_g}{2}=p_g<=>p_{data}=p_g$。 由此得证纳什均衡的取得条件为$p_{data}=p_g$。 优化算法的收敛性 在上一篇文章中,我们就已经介绍了这个训练算法,分别让D和G轮流针对Loss做梯度下降而已,直观上很好理解。Goodfellow在原论文中也对其收敛性做了证明: 其实感觉证了跟没证差不多吧。。大致就和我们在之前讨论的一样,最后要优化的本质上就是一个关于$p_g$的函数,然后证明了凸性,所以可以用梯度下降保证收敛到最优点。但是实际操作中,我们不能表示为一个$p_g$的函数,而是用多层网络来拟合,虽然缺乏理论保障,但是非常成功,2333。。。 总的来说,原论文中,这一部分,主要还是证明纳什均衡点的部分最精彩。上一篇文章中,我们用类比的方法解释了GAN的哲学,但是这里我们直接从数学上证明了定义的Loss的合理性:在这个Loss定义下,我们实际上是在让生成器的生成结果的概率分布和数据集分布拟合。 初代GAN存在的问题从14年GAN被提出以来,GAN在实践过程中,就一直存在训练困难等问题。 “However, they still remain remarkably difficult to train, with most current papers dedicated to heuristically finding stable architectures……Dispite their success, there is little to no theory explaining the unstable behaviour of GAN training.” 为了解决这个问题,大部分尝试都是通过改进GAN的生成器和判别器的架构,毕竟是CNN,优化优化架构总能让性能提升的。比较成功的如DCGAN,通过不断实验,找到一个比较好的网络架构设置。它们取得了一些成功,但是没有彻底解决GAN存在的问题,并且也一直缺乏对于GAN的表现的理论解释。 人们发现,在使用上面Goodfellow提出的那套minimax损失函数训练时,判别器训练得越好,生成器梯度消失越严重。也就是说当discriminator太强的时候,generator的能力就训练不上去了。 Goodfellow当年在写这篇论文的时候也发现了这个问题,于是在介绍完自己最开始提出的那套Loss后,又补充了几句:”In practive, equation 1 may not provide sufficient gradient for G to learn will……Rather than training G to minimize log(1-D(G(z))) we can train G to maxmize log D(G(z)). This objective function results in the same fixed point of the dynamics of G and D but provides much stronger gradients early in learning.” 其实就是把generator的目标函数简单的从log(1-D)换成了-log(D),直观来看,意义是一样的,都是在引导generator骗过discriminator,只不过数学形式发生了变化,在训练的时候梯度/收敛等方面发生了变化。但是后来人们发现,这种做法也很一般,首先梯度很不稳定,其次生成的样本多样性不足。 直到2017年,这位来自 Courant Institute of Mathematical Sciences 的 Martin Arjovsky 先生发表了文章开头提到的《TOWARDS PRINCIPLED METHODS FOR TRAINING GENERATIVE ADVERSARIAL NETWORKS》(Arjovsky et al. 2017) ,其中存在的理论问题才得到了回答。 原文中对于这部分问题的归纳得非常精彩: 寥寥几句话,就把前人遇到的问题的要害给抓住了:既然D最优的时候,训练G就能逼近纳什均衡,为啥D越好,G的训练却越糟糕?Goodfellow说这是因为saturation导致的,可以换个loss function,但是为啥换了过后发现更垃圾了。 于是提出了四个精髓的问题:为啥D越好,G训练越差?为什么GAN的训练不稳定?Goodfellow提出的替代选项也和JSD有关么,性质如何?有什么解决当下问题的办法吗? 问完后,夸下海口:The fundamental contributions of this paper are the answer to all these questions. 可谓是技惊四座。。。 这部分,我们主要整理了Arjovsky这篇论文中的关于初代GAN存在的问题的部分分析和推理。我们将分两个部分分别讨论Goodfellow原论文中提出的两种形式的Generator目标函数存在的问题。 但是我们只会给出一个简单的intuition层次上的讨论,严格的数学推导读者可以自行下载论文查看。因为,这位数学研究所的大哥,写的论文。。。嗯。。。 非常严谨: 要吃下这篇论文,最好得有一点测度论的基础。(反正我不会) 至于他在这篇论文中提出的解决方法,我们不再讨论,因为下一个部分,我们会介绍一个更加成熟的方法——WGAN。而提出者。。。还是这位Arjovsky先生。。。 log(1-D)存在的问题在上面证明GAN的纳什均衡点的时候,我们已经知道当Discriminator最优时,最优化Generator目标函数的等价表达式:$$KL(p_{data}||\\frac{p_{data}+p_g}{2})+KL(p_g||\\frac{p_{data}+p_g}{2})-log(4)\\=2JSD(p_{data}||p_g)-log(4)$$最小化这个目标函数,也就是最小化$p_{data}$和$p_g$的散度,也就是尽量让两个分布贴合,两者相等时达到纳什均衡,这些我们都已经讨论过了。 但是问题就出在这个散度上。 如果在训练过程中$p_{data}$和$p_g$这两个分布完全不重叠,或者重叠部分可忽略,那么这个JS散度会是多少呢? (这里的重叠指:两个分布在某个样本点上都有不为0的出现概率。) 还是先考虑单个x对这个JSD的贡献:$$[p_{data}(x)log\\frac{2p_{data}(x)}{p_{data}(x)+p_g(x)}+p_g(x)log\\frac{2p_g(x)}{p_{data}(x)+p_g(x)}]dx$$对于每个x来说,存在四种可能性:$$p_{data}(x)=0,p_g(x)=0\\p_{data}(x)\\neq0,p_g(x)=0\\p_{data}(x)=0,p_g(x)\\neq0\\p_{data}(x)\\neq0,p_g(x)\\neq0\\$$第一种情况,对散度无贡献,第二种情况和第三种情况则会贡献log2*dx。由于完全不重叠或者重叠忽略不计,第四种情况贡献也可算作0。因此,实际上,在$p_{data}和p_g$重叠量可以忽略不计的情况下,这个JS散度始终在log2附近轻微震动。使用这样的loss function来训练generator,其实就意味着:梯度=0。 也就是说,当discriminator最优的时候,如果生成器产生结果的分布$p_g$与训练集分布$p_{data}$的重叠量可以忽略不计,generator不能获得任何梯度信息(因为梯度=0),无法通过梯度来优化自己。 而$p_{data}$和$p_d$重叠量可忽略不计的概率是多大呢?几乎是1.. 原论文用测度论的方法证明了:当$p_{data}和p_g$的支撑集是高维空间中的低维流行时,这两个分布重叠部分的测度为0的概率为1。 这里简单解释一下概念: 支撑集:一个函数在其定义域上取值非0的定义点的点集。 流形:三维空间中我们有曲线,曲面。这些低维曲线/曲面都是这个三维空间中的一个低维流形。比如一三维空间中的曲面是这个空间中的一个二维流形,曲线是一个一维流形。本质上,我们知道,所谓维度就是指需要几个变量才能确定,流形就是高维空间中这些概念的推广。 测度:可以理解为长度/面积/体积这些经典空间中的概念在高维空间中的推广,可以理解为超体积。(如果对线性代数熟悉的话,我们知道行列式本质上也是在计算一个超体积。) 为什么要用支撑集这样的概念呢?因为像图片这样的高维向量,以64*64的图片为例(4096维),并不是随便拿一个4096维的向量,它就是一个对人类来说有“意义”的图片。当我们看到一张猫的图片(本质上是一个满足一定特征的向量)我们会说这是猫,当我们看到一个随机生成的4096维向量,我们大多时候看到的就是一堆乱七八糟的像素点。所以对于图片的概率分布来说,这个4096维的向量空间中,大部分的概率值应该都是0,是没有意义的。 那么,很容易就会想到,对于真实图片的概率分布,其支撑集可能只是这个高维空间中的一个低维流形(真实图片只有可能分布在一个这个空间中的一个低维度超几何面上,其它地方概率=0)。 更直观一点来说,就是:既然真实图片只是其所在的空间的一个非常小的集合中,那么可能我们根本就不需要这么高维度的向量来表达它,一个更低维度的编码就够了,用像素矩阵来表达一个图片对象是一个非常低效的手段。实际上已经有许多例子能够为此提供支持,其中最有代表性的应该是图像压缩。以JPEG压缩为例,在有些例子中,它可以将一个bmp格式的图片的大小压缩20倍,虽然它是有损压缩,但是解压后肉眼基本上看不出压缩前后的区别! 而原论文中想要表达的就是:既然图片分布是低位流形,那么两个低维流形的交叠相对于这个低维流形而言,期望测度是0。比如说在二维平面上随便取两条曲线段,这两条曲线段可能会有有限的相交点,但是这些有限的相交点的“长度”相比于这个曲线段微不足道。原论文的证明其实就是把这个直观的感受推广到了高维空间中。 至此,我们终于知道为啥D训练得太好,G就训练不动了。因为D训练得越好,训练G时的目标函数就越贴近上面我们推导出的JS散度。而这个散度虽然理论上看起来很好,只要不断降低,就能够让两个分布贴在一起,但是实际上由于两个分布的交叠几乎总是可以忽略不计,所以导致这个散度根本降不动。 因此,初代的GAN训练过程中需要非常小心的平衡D和G的训练,如果以来D就训练得太好,那么G根本训练不动,如果D质量很差,G从D那里得到的梯度指引也会不够准确,导致G的训练效果一般。 log(D)存在的问题那么Goodfellow提出的备选项 : logD 又如何呢? 还是按照上面的方法,做一个简单的变形,就可以导出,当D最优的时候,G的目标函数等价于:$$KL(p_g||p_{data})-2JS(p_{data}||p_g)$$最小化这个目标函数会变得很滑稽。 一方面我们希望最小化KL,另一方面又希望最大化JS。直观上来看,我们又希望将两个分布贴紧,又希望将它们两个拉开。。。 所以显然当年Goodfellow提出的这个“改进”完全是凭感觉瞎扯的。它避免了原来的那个裸的JS,所以解决了梯度消失的问题,但是这两个互相矛盾的目标也自然最后也会让我们得到不稳定的梯度波动。。 Wasserstein GANArjovsky在《TOWARDS PRINCIPLED METHODS FOR TRAINING GENERATIVE ADVERSARIAL NETWORKS》(Arjovsky et al. 2017) 还讨论了一些别的问题,并且也提出了一些解决这些问题的思路和方法。限于笔者实力和精力有限,这里不再赘述,有兴趣可以阅读原论文。 这一部分,我们将介绍WGAN。WGAN的提出者也是Arjovsky等人,在发了上面这篇paper后,他又立刻发表了 《Wasserstein GAN》(Arjovsky et al. 2017)。 Wasserstein GAN,顾名思义,其实就是把概率分布间的距离度量改成使用“Wasserstein Distance”来衡量的GAN。这一部分,我们会简单的介绍Wasserstein Distance及其相对于其它类型距离定义的优势。最后我们会介绍WGAN的基本思路和流程。 Wasserstein Distance 原论文中,作者简单的贴出了Wasserstein Distance的定义,乍一看反正就是一个数学表达式,求的是一个向量间距离的期望下确界。。。 WD也被称为推土机距离(Earth-Mover Distance),是信息论里面的一个定义。搜一下这个东西,你会发现还有很多应用中都用到了它,它本质上是从最优传输问题导出的。上面我们说了这个式子就是要求一个期望的下确界,对于这个期望来说,其所依赖的联合概率分布是一个参数,这里要求一个下确界,就是要求一个最优联合概率分布来使得整个传输时最优的。而这个最优概率传输代价可以作为两个概率分布之间的差异的某种形式的度量。有兴趣可以参考一下这篇文章:《传说中的推土机距离基础,最优传输理论了解一下》。 用最优传输代价为什么能衡量分布距离? 假设我们是一个电商平台,我们运营着自己的仓库,我们当然希望全国各地的仓库中商品的分布尽量和各地的需求贴合。比如说A城对商品需求量为x,我们当然希望A城的仓库正好有x件商品来满足需求。 如果我们把n个城市的仓库库存表示为一个n维向量X,商品需求也表示为一个n维向量Y,那么最容易想到的简单评估库存分配的方法就是计算$||X-Y||$,这个向量距离范数在一定程度上衡量了库存分布和商品需求分布的贴合程度。 但是,这个范数可能评估得并不那么精确。 假设现在n个城市里,有n-2个城市库存和需求都是一样的,但是有两个城市,比如西藏和北京,其中西藏需求量比库存大1,北京的库存比需求量大1。那么我们可以从北京运送1个单位的商品到西藏,此时得到范数值$||X-Y||=1$。 再考虑另一个例子,如果这两个城市不是北京和西藏,而是北京和天津,那么我们需要把一个单位的商品从北京运送到天津。此时$||X-Y||$依然是1。 我们很快就会发现一个问题。如果把货物从北京发送到天津所需要的代价肯定显著低于从北京发往西藏,从这个角度来看,后者的分布应该优于前者的分布,但是如果简单的用一个距离范数来表达,无法体现这种差别。 我们把本地发货的代价设为0,把北京到西藏的代价设为100,北京到天津的代价为10。那么用最优传输代价来评估,前者的代价是100,后者代价是10,我们便能体现出后者由于前者这一特性。 这就是最优传输代价用来衡量分布距离的直观理解。 WD的优势原论文在引出WGAN之前,先对比了四种距离定义的表现。考虑到其中一种TVD对于我们理解WGAN并没有什么特别必要,我们这里主要比较一下三种距离:KL Divergence,JS Divergence,Wasserstein Distance (Earth-Mover Distance)。 原作者举了上面这个简单的例子,这个例子中有两个不重叠的概率分布。(原论文没有给上面的示意图,这张示意图源自一篇知乎的文章《令人拍案叫绝的Wasserstein GAN》) 我们将看到JSD,KLD和WD在这个例子中表现出来的差异: 对于JS我们前面已经分析过了,只要不相交,不管距离是多少,始终等于log2,根本不能体现距离带来的差别,所以一旦分开了,就不能通过梯度拉回来,所以训练G的时候会出现梯度消失。 对于KL,也很容易验证,当完全不相交时,梯度会趋于无穷,导致梯度爆炸。 而WD却体现出了非常优秀的性质,它完全是和距离呈正相关的,提供了连续平滑的梯度。 由此可见,如果我们使用WD来作为目标函数引导$p_g$和$p_{data}$的拟合,就能很好的避免梯度消失或者梯度爆炸。 WGAN前面我们说了,WGAN就是把初代GAN的loss换掉,不再使用JS散度作为目标,取而代之,采用Wasserstein Distance来作为目标。 但是并没有这么简单。。。还记得Wasserstein Distance中要求一个下确界吗?这东西并不能直接求解。。 如上,虽然WD并不能直接算出来。但是作者在原文中使用了Kantorovich-Rubinstein duality(这个东西有兴趣可以自己去看这篇文章,原paper这部分引用了这本书:《Optimal Transport - Old and New》,据说是写个数学系phd看的,998页。”Have fun and good luck!!!”),于是WD距离的计算就归结为另一个等价对偶式的计算: 或者也可以用: 这里的$f$是一个满足Lipschitz连续的函数。 所谓的Lipschitz连续,就是对于一个常数K,函数在定义域中任意两个元素$x_1,x_2$上都要满足:$$|f(x_1)-f(x_2)|\\leq K|x_1-x_2|$$K被称为Lipschitz常数,上面的表达式中,要求 $f$ 满足对于一个固定大小的Lipschitz常数满足Lipschitz连续。从Lipschitz连续的定义上,可以看到,它限制了函数梯度的上限,使得一个函数的函数值局部变动幅度只能在一个有限的范围内。 所以,计算WD就成了,在 $f$ 的Lipschitz常数不超过K的条件下,找出那个特殊的满足条件的 $f$ 取到$E_{x\\sim p_r}[f(x)]-E_{x\\sim p_\\theta}[f(x)]$ 的上界。 似乎找到这个最优 $f$ 也不是一件容易的事情?神经网络最厉害的不就是拟合么。。我们拿一个神经网络来拟合。。但是神经网络在拟合过程中没办法限制f使得它满足Lipschitz条件呀?WGAN原论文中暴力的将拟合f的网络的w参数全部限制在了[-c,c]之间(小于-c就令为-c,大于c就令为c),来强行的满足了Lipschitz条件(f最终肯定是关于w的函数,w的范围只要被限制了,那么f的导数极限也被限制住了)。 于是在WGAN的设计中,原本的discriminator网络其实被换成了拟合 $f$ 的网络。对于这个 $f$ 网络的训练,它做的事情就是要最大化:$$L=E_{x\\sim p_{data}}[f_w(x)]-E_{x\\sim p_g}[f_w(x)]$$在训练充足的情况下,这个 $f$ 被期望能逼近使得WD对偶式取上确界时的$f$。 而generator要干什么呢?它就是负责最小化这个L,因为 $f$ 网络的训练主要是调整 $f$ 取上确界,使得L逼近WD,g网络则负责降低这个WD,使得两个分布被贴合。 你说巧不巧?明明discriminator已经不再是那个负责判别的discriminator,而是被用来拟合一个不明意义的$f$,两个网络之间竟然还是存在一个minimax博弈! 最后,附上WGAN的伪代码,其实变化不大: WGAN-GPWGAN论文中,作者也说了,采用clip来保证Lipschitz条件非常粗暴,简陋,鼓励大家提出一些新的想法。WGAN-GP就是一个替代策略,它在Loss里面添加了一个Gradient penalty项,有点类似正则化项,但是这里的GP项主要是为了限制f网络中的参数,让它满足Lipschitz条件。相对而言,更灵活一点。 具体的介绍可以参考这篇文章,我们不再展开讨论。","categories":[],"tags":[{"name":"AI","slug":"AI","permalink":"https://zjusct.github.io/tags/AI/"}],"author":"漆翔宇"},{"title":"生成式对抗网络_基础","slug":"GAN_Introduction","date":"2019-05-30T16:00:00.000Z","updated":"2021-02-11T05:46:05.525Z","comments":true,"path":"2019/05/31/GAN_Introduction/","link":"","permalink":"https://zjusct.github.io/2019/05/31/GAN_Introduction/","excerpt":"","text":"Reference : Machine Learning and having it deep and structured (2018,Spring) (李宏毅) Introduction of GAN生成式对抗网络(Generative Adversarial Network,GAN),是2014年被提出来的一种生成式深度学习网络技术。 由于在众多应用场合中的出色表现,近年来GAN的应用和研究已经成为一个非常热门的方向。 这篇文章将会对GAN做一个直观的介绍,帮助读者理解GAN的Basic Idea。 第一节中,我们将简单对机器学习的Basic Ideas做一个回顾。第二节和第三节会简单介绍一下什么是生成(Generation)。第四节和第五节会介绍两种生成模型。第六节我们将会看到GAN是如何结合这两种模型从而达到优秀的表现。 Review for ML在之前的文章( 机器学习 / 卷积神经网络)中,我们介绍了一些机器学习(Machine Learning)的思想和例子。 简单回顾一下在这些文章中的讨论,我们知道,机器学习本质上就是一个映射拟合 : 在这个过程中,模型(Model)是一个函数,我们希望对它输入一个自变量x,能从它那里得到一个我们想要的输出y。以房价预测为例,我们希望告诉这个模型一些当前的经济,政治等社会环境,它能反馈一个房价数值。以图像分类为例,我们希望告诉这个模型一张图片,它能反馈给我们这张图片属于什么类别。 在最简单的形式中,我们用一个简单的线性模型(Linear Model) $w*x=y$ 来建模这个函数。高级一点,选择一个更复杂的核函数(另一种说法也可以称特征函数)的话,这个线性模型可以表达更加复杂的关系。再高级一点,把这些线性模型多层复合起来,形成神经网络(Neural Network),表达力就更强,而这个用深度神经网络来建模的方法自成一派,也就是所谓的深度学习(Deep Learning)。 而常常所说的训练数据(Training Data)无非就是从真实的世界中采样的一组(x,y)样本。训练(Training)则是让我们的模型从训练数据中学习x和y之间存在的某种规律的模式(Pattern),模型的参数在学习过程中不断调整,最后模型代表的函数尽可能的去模拟了真实世界中的映射关系。 这个学习过程又是怎样的呢?通常就是定义了一个损失函数(Loss Function),这个损失函数的值是基于模型预测出来的结果和我们采样出来的真实结果的差别。所谓学习其实就是最小化这个差别。因此,所谓的学习,本质上就是这个函数最优化问题的一种拟人化表达而已。 因此机器学习实际上就是给定一个训练数据集,产生一个预测函数。这个预测函数能对输入的x给出期望的y,简单的可以表示为这样: What Is Generation机器学习中,有很多种类型的任务,比如最简单的回归(Regression),分类(Classification)等。 它们是机器学习入门时肯定会接触的例子,常见的回归/分类任务比如:给定一张图片,输出一个标量或者向量描述这个图片的性质。 而生成(Generation)实际上就可以简单的理解为上述任务的一种逆向过程。给机器一张图片,它给你一个向量描述它的类别或者特征,似乎很无聊?那么如果给机器一个向量后,它给你生成一张图片(老婆)呢? 这就是生成任务最直观最简单的例子。(除了生成图片以外,当然它也可以用来生成句子/音频等其它任何你能想到的东西,只要能保证可训练。) 如上图所示,只需要调整对生成器的输入向量,它就会相应的生成不同的图片。P2中增大了第一维的输入,生成的人物头发变长了;P3中改变向量倒数第二维的输入,人物头发变成了蓝色;P4中增大了向量的最后一维,生成的人像就变得笑口常开了。 Why Generation一个很自然的问题是:为什么我们需要生成?给我一个向量,我生成一张图片有什么用? 如果只是给一个很枯燥的向量,让机器生成一张图片确实没啥用。但是如果再组合上其它的功能模块,这个技术可能会非常有用。 比如说,我们可以不从向量开始,而从一个句子开始。训练这样一个模型,自然语言先被转成向量编码,向量编码再被转成这个自然语言句子描述的画面。 又比如说,我们可以从图片中修改一部分区域,画出一个简单的草图,它就自动帮我们生成更加真实的修改内容。 你甚至还可以根据马云的脸的表情信息的编码生成一张小罗伯特·唐尼同样表情的脸,然后替换上去,产生真实的换脸效果。 生成,在某种意义上来说,是人工智能最有意义的任务。回归和分类这样的任务仅仅是去理解和识别我们熟知的对象,比如识别一句话是不是脏话,识别一张图片是不是猫。但是生成却是化腐朽为神奇,仅仅根据一些非常空洞的描述就自动生成了我们人所熟知的对象,这才是我们真正最想让人工智能为我们做的。仅仅是理解和识别,人工智能终究只是作为一种辅助工具。但是当它能创造的时候,它就翻身做了主人。 上图是一个向量生成图片的例子。向量按照一种顺序在逐渐变化,我们而可以看到第四行生成的人脸也在变化。惊人的是,它的生成竟然是一个连续的将脸变换一个朝向的过程。这说明,这个模型确实学到了这个人的脸的深刻特征,能根据向量的微调,对产生的结果也生成连贯一致的微调。 不只是绘画,当它还能生成语言,生成电影,生成代码,生成电路设计,生成科学理论时,它体现出来的智能性是非常让人震撼的。学习到对象/概念的深层次内涵,并且能够使用这些学到的知识去创造未知的东西,这是人工智能的究极目标之一。 Auto-encoderBasic Idea 上图是生成模型的一种基本模式,我们希望输入一个向量得到一个目标输出作为图形。 一个简单问题是,我们如何训练它?容易想到的是事先准备一堆向量和图片之间的映射对,让这个生成器学得某种映射模式,如下图所示: 但面临的一个现实问题是,用于训练的图片很容易得到,但是这个向量code如何得到?需要我们手工设置吗?对于一些大型的项目,这个向量一般维度很高,样例也很多,手工设置这些向量标签可操作性并不强。并且,如果设计的向量不好,也很难保证模型收敛。 自编码器在一定程度上解决了这个问题: 如图,自编码器接收一张图片作为输入,先丢入一个神经网络中产生一个编码作为输出(一个高维向量),这个产生的编码再被丢入另外一个神经网络,恢复成图片。我们希望恢复出来的图片尽可能和输入的图片相近,于是可以定义一个loss来衡量图片在这一次编码和解码过程中的损失,根据这个loss,再使用像梯度下降之类的方法,就可以调整encoder和decoder的参数,使得其尽可能减少损失。 在这样一个自编码,自解码的过程中,我们就既有了编码器和解码器。我们可以只对解码器输入向量,就能得到一个生成图。需要注意到的是,在这样一个自编码器模型中,code和image的映射完全是由模型生成的。模型会自动帮我们调优这个映射的划分。 PS : 这个code的长度是一个超参数,我们需要设定一个适合的编码长度。一方面,它的长度小于原图才能达到我们希望的提取特征的效果。另一方面,如果它的长度太小,可能无法容纳图片中必须的一些关键信息,导致无法还原。 Pros and Cons自动编码器的优点很显然,只需要给模型展示一些正例,它就能学到编码与图片之间的映射,非常易于训练和生成。 上面我们说了,我们在训练一个自编码器的时候,希望的是输出图片和输入图片尽可能的相近,减少损失。这个相似如何去衡量呢?一般采用的方法都是把图片当作高维向量处理,求两个向量之间距离的L1或者L2范数。这个范数就表征了两张图片之间的差别,而这样的衡量方式局限性很明显: 如上图所示,如果我们让机器生成了四个版本的“2”的图片。其中v1和v2与原图只差了一个像素点,v3和v4查了足足6个像素点。但是v1和v2破绽很明显,v1在尾巴上出现了一个孤立的橘色块,而v2在中间留了一块白出来,这都是不自然的。而v3和v4虽然像素点差距更大,但是其实只是尾巴和头分别拉长了一点罢了,这都是很正常的情况。所以,用这种标准训练出来的模型,它显然缺少一个大局观。只是机械的分别关注全局对象的每一个小组成部分,很难学到组成部分之间的关联。 DiscriminatorBasic Idea 评估器接收一个对象输入,产生一个标量输出。(其实就是一个回归问题)如上图所示,以生成图片的任务为例,一个评估器会评估机器生成的图片是否和人类作出来的图有一样的效果,根据生成质量,评估器会给出一个0~1的评分。 假设我们已经有了一个很好的评估器,我们也可以用拿它来生成图片。给定一个向量,我们通过一些事先构造的假设,将其可能对应的图片锁定在一个集合(可能会很大)中。图片生成就成为了在这个集合中枚举,筛选出一个得分最高的图片: Train一个实际的问题是,如何训练一个好的评估器?它怎么能分辨出哪些是画得好的,哪些是画得糟糕的? 在网上,我们可以下载一大堆艺术家绘制的图片,他们都可以拿来作为正例,用来让模型学习什么是“好”图片。但是如果全都是正例,那么评估器将会收敛为一个恒输出1的常函数。所以,要训练一个评估器,面临的一个实际问题是:如何获得好的反例? 如果反例的采样分布没有选好,那么模型对反例的识别能力也会很差。就会像上图那样,一个画得很一般的图片却获得了0.9的高分。 我们可以通过一个迭代算法来不断强化这个评估器: 如上图所示。一开始,我们可以随机生成一批反例,然后我们用当前的正例和反例训练出来一个评估器。评估器是个什么东西呢?它本质上也是一个定义在图片域上的函数。我们现在就从这个定义域上采集一批评分很高的图片来作为反例,用新的反例结合之前的正例重新训练,得到下一个版本的评估器。最后不断迭代。 上图是一个迭代过程的示意图,绿色为真实图片的分布域,蓝色为生成的效果糟糕的图片的分布域,红色曲线是我们的评估函数。在第一轮的训练中,真实域的函数估值会被拉高,我们随机生成那些噪音图片所在的域的股指会被压低。但是这个估值函数可能还不够,它可能没有发现其它很糟糕的图片的分布域,给了它们很高的估值,我们称为“虚高”。所以我们会进行第二轮训练,第二轮中用到的反例是从第一轮训练出来的估值函数中采样的估值高的点,那些“虚高”的点因为被作为反例被压低,而那些本来就该得分高的点即使被采样为反例,但是因为有同区域的正例在支撑,所以这一区域的估值不会被压低。最后,多轮迭代后,那些虚高的点就会逐渐被压平,只有正例分布的区域还依旧坚挺。此时我们就认为得到的评估函数是优秀的。 这就像是制定一部法律。我们希望人们的行为被约束在一个我们预先定义的可接受的范围内。但是法律有漏洞,总有人钻空子,在制定者期望之外的区域获得很高的收益。法律制定者就会锁定这些突出的领域专门指定新的条令来打击。经过不断的实践和修订,这部法律也就完善起来。 Pros and Cons估值器的优点正好弥补了自动编码器的缺点。自动编码器是一个Button Up的模型,它根据编码对每一个像素点单独生成,组成一个完整的更高层的结果,这样的Button Up模型很难学到不同组成部分之间的关联。而估值器是一个Top Down模型,它直接获得正常图片作为输入,用全连接网络或者卷积网络很容易学习图片中的一些局部以及全局特征。 但是缺点也很明显。从上面看到,我们发现评估器很依赖于一个argmax的计算。如果要用它来生成模型,我们需要在一个域上找到得分最高的候选图。如果要用迭代法来训练一个评估器,我们也需要在当前的评估函数域采样估值高的点来作为新的反例。然而这个argmax怎么算呢?很多用discriminator来做生成的,都要事先做一些不准确的假设,根据这些假设对不同的输入向量在图片域中划分一个候选域,在这个可行域上做argmax。并且有的时候需要大量的枚举,非常耗时。并且要训练评估器也不容易,光有正例还不行,还得自己去合理的生成反例。 所以,显而易见,评估器虽然有了自编码器难以学得的全局观,但是却没了自编码器那种易于生成结果的特性。 GAN讨论了这么多,终于轮到主人公GAN登场了。 前面我们先讨论了Auto-encoder和Discriminator,分别介绍了用它们来做生成的基本想法。在讨论中,我们已经意识到了:Auto-encoder很容易产生生成图像,但是由于是button-up的模型,根据编码分别对每一个像素点预测,很难学得像素点之间的关系;Discriminator很容易学得图像中一些局部和全局的特征细节,可以很好的处理像素点之间的correlation,但是其主要能力还是在做评估上,在生成方面它还是缺乏一个有效的生成手段。 GAN集成了这两个方法:一方面,GAN使用一个类似Auto-encoder结构的Generator来生成图像,另一方面它用一个Discriminator来评估生成图片的质量。 Basic Idea考虑这样的一个过程: 一开始我们已经有了一个generator和discriminator,它们的参数都是随机设置的。generator生成的图片很糟糕,discriminator也无法判别什么图片是好的。 然后我们可以以一个适当的概率分布随机向这个generator中输入一组向量,然后得到一堆生成的图片,用这些图片作为反例,用艺术家绘制的图片作为正例训练discriminator。这轮训练后,得到的discriminator的能力得到了提升,能够学会给一些好的图片打高分,给一些差的图片打低分。 这之后,我们再固定这个discriminator的参数。此时如果我们给generator输入一个向量,再把它产生的图片送入discriminator中,我们会得到一个反馈的分数。这个反馈分数就可以作为LOSS,我们根据LOSS FUNCTION的梯度调整generator的参数,使得它尽可能产生可以骗过这个版本的discriminator,从它手下得到一个高分。这轮训练后,得到的generator的能力也得到了提升,能够产生一些像样的图片了。 然后我们又重复上面的过程,强化discriminator,discriminator强化后再强化generator。。。可以期望的是,多轮迭代后,我们的generator和discriminator都可以变得很强。 上图很好的从生物进化的角度很好的揭示了GAN的哲学。一开始蝴蝶颜色五颜六色的,停在树上的时候,捕食它的鸟根据颜色是否是棕色来区分它和叶子。在这个过程中,那些五颜六色的蝴蝶被淘汰了,棕色的蝴蝶脱颖而出,成功骗过了初代鸟。但是那些被骗过的初代鸟也会被淘汰,于是鸟也在这个过程中进化,学会了通过判别是否有叶脉来寻找猎物。而蝴蝶在这一过程中再次进化,成为了枯叶蝶。。。这是自然界中经典的良性竞争,互相强化的例子。而Generative Adversarial Network中的“Adversarial”也就是这样来的。在GAN中,我们让一个generator和一个discriminator互相对抗,generator努力的生成逼真的图片试图骗过discriminator,discriminator努力强化自己的辨别能力对抗generator的欺骗。模型的学习就是在两者的对抗之中互相强化而完成的。 Algorithm 上图是GAN的基本训练模式。$\\theta_d$ 和 $\\theta_g$ 分别是Discriminator和Generator的参数,一开始是随意初始化的。 每一轮中: 首先从数据库中采样m个真实样本${x_i}$,再让Generator随机生成m个虚假样本${\\hat{x_i}}$。 定义 $V=\\frac{1}{m}[;\\Sigma_{i=1}^{m}logD(x_i)+\\Sigma_{i=1}^{m}log(1-D(\\hat{x}_i));]$ 真实样本得分越高,虚假样本得分越低,这个V的值越大。于是我们只需要通过梯度上升法,最大化这个V,我们的Discriminator就能尽可能的对真实样本给出高分,尽可能的给虚假样本给出低分。这实际上就是完成了一个对Discriminator的强化。 在这之后,我们再取样m个随机向量${z_i}$,丢入Generator,再把生成的结果给Discriminator评估。 定义$V=\\frac{1}{m}\\Sigma_{i=1}^mlog(D(G(z_i)))$ 同样的,我们希望最大化这个V值,使得产生的结果尽可能骗过Discriminator。将V值对 $\\theta_g $ 求偏导,使用梯度上升来最大化这个V,从而实现对Generator的调优。 Comparision在GAN的设计中,我们可以看到其明显优于Auto-encoder和Discriminator的地方。Auto-encoder生成之后,缺乏一个强大评估反馈,简单的采用原图和恢复生成图的差向量范数来评估,无法考虑到各个component之间的correlation,缺乏一个大局观。Discriminator虽然能对图片产生很深的特征理解,但是缺少一个高效准确的proposal手段,训练时它非常依赖于反例的生成,生成时又需要选择一个候选区域来执行argmax,这都不是容易解决的问题。 GAN将这两者的特性做了一个结合。虽然还是采用button-up的生成方式,根据向量对每一个像素点单独生成,但是多了一个up-down评估模式的discriminator来对生成内容进行更加细致的评估反馈,从而generator能得到更好的训练指引。另一方面,由于有了一个更加强大的generator,训练discriminator时,能获得质量更高的反例,从而discriminator也能得到更好的训练。","categories":[],"tags":[{"name":"AI","slug":"AI","permalink":"https://zjusct.github.io/tags/AI/"}],"author":"漆翔宇"},{"title":"基于0-1乘性噪声的朴素图片降噪","slug":"Image-Restoration-SimpleVersion","date":"2019-05-10T16:00:00.000Z","updated":"2021-02-11T05:46:05.673Z","comments":true,"path":"2019/05/11/Image-Restoration-SimpleVersion/","link":"","permalink":"https://zjusct.github.io/2019/05/11/Image-Restoration-SimpleVersion/","excerpt":"项目内容给定3张受损图像,尝试恢复他们的原始图像。 原始图像包含1张黑白图像(A.png)和2张彩色图像(B.png, C.png)。 受损图像$X$是由原始图像$I \\in R ^ { H * W * C }$添加了不同噪声遮罩$M \\in R ^ { H * W * C }$得到的$x=I \\odot m$,其中$ \\odot $是逐元素相乘。 噪声遮罩仅包含{0,1}值。对应原图(A/B/C)的噪声遮罩的每行分别用0.8/0.4/0.6的噪声比率产生的,即噪声遮罩每个通道每行80%/40%/60%的像素值为0,其他为1。 评估误差为恢复图像与原始图像向量化后的差向量的2-范数,此误差越小越好。","text":"项目内容给定3张受损图像,尝试恢复他们的原始图像。 原始图像包含1张黑白图像(A.png)和2张彩色图像(B.png, C.png)。 受损图像$X$是由原始图像$I \\in R ^ { H * W * C }$添加了不同噪声遮罩$M \\in R ^ { H * W * C }$得到的$x=I \\odot m$,其中$ \\odot $是逐元素相乘。 噪声遮罩仅包含{0,1}值。对应原图(A/B/C)的噪声遮罩的每行分别用0.8/0.4/0.6的噪声比率产生的,即噪声遮罩每个通道每行80%/40%/60%的像素值为0,其他为1。 评估误差为恢复图像与原始图像向量化后的差向量的2-范数,此误差越小越好。 实现介绍 核心思想由于图片的像素点在空间上满足局部相似的特征,相邻的像素点通道值变化往往是平滑且有一定规则的。因此,我们可以用一个模型来拟合像素点通道值在空间上的关系。具体实现中,我们将图片切割成若干个小矩形块,然后使用一个二维线性回归模型来回归每个小矩形块中位置和像素通道值的函数关系。为了使结果更加平滑和可靠,我们采用了高斯函数作为基函数。 高斯函数当我们对一个 ss * ss 的像素矩阵块做回归的时候,我们要把所有没有被噪音损坏的点都提取出来。点位置用一个高斯核函数处理,这样原来的点坐标(x,y)就被转换成了$(e^{-\\frac{(x-mid)^2}{2}},e^{-\\frac{(y-mid)^2}{2}})$,在特征空间中用来刻画这个点与矩阵块中心的距离。这样,我们实际要回归的就是点心距与像素通道值的关系。 这样做,主要是因为我们的局部性原理本身就是不带方向性的,所谓的局部性就是指临近的点存在某种平滑的变化关系。使用这样一个衡量距离的核函数,可以使得我们的回归结果更加平滑: 上图左边是用坐标直接回归,右图是坐标经过高斯核处理后回归的结果。可以看到左边有大量的不平滑的交错的黑白点,看起来很“脏”。右边由于采用了高斯函数处理过的表征距离关系的核函数,结果更加平滑,清晰。 CODE : 训练提取完特征后的数据点拟合,就是一个简单的线性回归任务而已,我们采用最小二乘法回归。$$Loss = \\frac {\\Sigma_{i=0}^n(y_i-\\phi(x_i) * w^T)^2}{n}=\\frac{\\Sigma_{i=1}^{n}Loss_i}{n}$$使用随机梯度下降来最优化损失函数:$$w\\leftarrow w-\\eta * \\triangledown Loss_i=w+2\\eta(y_i-\\phi(x_i) * w^T) * \\phi(x_i)$$具体实现中,我们取步长=0.005,进行100轮随机梯度下降。 CODE : 迭代降噪我们前面提到了,我们要把图片切成若干个小矩形块,对每个小矩形块分别进行回归,那么这个小矩形块的尺寸取多少比较合适呢? 我们先取ss=2尝试一下: 噪音为0.8的时候,我们可以看到,如果取一个2*2的块,期望其中没损坏的通道只有0.8个,所以势必有大量的矩阵块里面都是全损坏的,这会使得一些全损坏的块得不到修复,产生大量的黑点。 直接取ss=5: 显然,黑块的数量变少了,但是实际上图片给人的颗粒赶很明显,更像是一堆模糊的马赛克拼图拼凑而成的。 我们的解决办法是先取ss=2,对图片做恢复,然后将恢复的图像再用更大的ss来恢复。 下面是用ss={2,3,4,5}迭代恢复四次的过程: 可以看到黑点逐渐被消除,并且最后经过ss=5的回归后得到的结果在视觉效果上明显优于直接用ss=5进行回归。这种想法本质上是一种贪心算法。先将损失密度小的局部块恢复好,再充分利用之前修复出来的信息将损失密度更大的块修复。 实验结果A (noise rate = 0.8) B(noise rate = 0.4) C(noise rate = 0.6) D(根据原图自己生成,测试迭代过程中损失的减少) 潜在的优化展望 由于每个块的修复是独立的,可以考虑使用CPU多线程计算或者在GPU上用CUDA进行并行优化,加速整个修复过程。 使用更复杂的网络来拟合。 使用马尔科夫随机场的方法来做图像降噪。","categories":[],"tags":[{"name":"AI","slug":"AI","permalink":"https://zjusct.github.io/tags/AI/"}],"author":"漆翔宇"},{"title":"Virtual Memory and TLB","slug":"Virtual-Memory-and-TLB","date":"2019-03-31T06:05:11.000Z","updated":"2021-02-11T05:46:05.713Z","comments":true,"path":"2019/03/31/Virtual-Memory-and-TLB/","link":"","permalink":"https://zjusct.github.io/2019/03/31/Virtual-Memory-and-TLB/","excerpt":"虚拟地址空间x86 CPU 的地址总选宽度为32位,理论寻址上限为4GB。而虚拟地址空间的大小就是4GB,占满总线,且空间中的每一个字节分配一个虚拟地址 其中高2G0x80000000 ~ 0xFFFFFFFF为内和空间,由操作系统调用; 低2G0x00000000 ~ 0x7FFFFFFF为用户空间,由用户使用。 在系统中运行的每一个进程都独自拥有一个虚拟空间,进城之间的虚拟空间不共用。","text":"虚拟地址空间x86 CPU 的地址总选宽度为32位,理论寻址上限为4GB。而虚拟地址空间的大小就是4GB,占满总线,且空间中的每一个字节分配一个虚拟地址 其中高2G0x80000000 ~ 0xFFFFFFFF为内和空间,由操作系统调用; 低2G0x00000000 ~ 0x7FFFFFFF为用户空间,由用户使用。 在系统中运行的每一个进程都独自拥有一个虚拟空间,进城之间的虚拟空间不共用。 虚拟地址空间是一种通过机制映射出来的空间,与实际物理空间大小无必然联系,在x86保护模式下,无论计算及实际主存是512MB还是8GB,虚拟地址空间总是4GB,这是由CPU和操作系统的宽度决定的,即: CPU地址总线宽度 → 物理地址范围CPU的ALU宽度 → 操作系统位数 → 虚拟地址范围 虚拟内存虚拟地址空间 = 主存 + 虚拟内存(交换空间 Swap Space) 虚拟内存:将硬盘的一部分作为存储器使用,来扩充物理内存。 利用了自动覆盖、交换技术。内存中存不下、暂时不用的内容会存在硬盘中。 Assume: 32位操作系统,32位寻址总线宽度 → 4G线性空间 保护模式下的进程运行虚拟地址空间是硬件行为,CPU自动完成(同时与操作系统协作)虚拟地址到物理地址(可能差熬过实际内存,这样会产生一个异常中断,揭晓来有操作系统处理(如从虚拟内存中调出对应的页框内容))。 所以,一个程序若运行在保护模式下,其汇编级、机器语言级的寻址都是用的虚拟地址,即在一般的编程中不会接触到物理一层。 在进程被加载时,系统为进程建立唯一的数据结构进程控制块(PCB = Process Control Block),直至进程结束。 PCB中描述了该进程的现状以及控制运行的全部信息,有了PCB,一个进程才可以在保护模式下和其他进程一起被并发地运行起来,操作系统通过PCB对进程进行控制。 PCB中的程序ID(PID(unix、linux)、句柄(windows))是进程的唯一标识;PCB中的一个指针指向 页表 ,这些都与地址转化有关。 地址转化地址转化的全过程可以用以下这张图来概括: 以下是具体步骤介绍。 1. 逻辑地址 → 线性地址 (段式内存管理,Intel早期策略的保留) 段内偏移地址(32位) 段选择符:16位长的序列,是索引值,定位段描述符;结构: 高13位为表内索引号 —— 但注意由于GDT第一项留空,所以索引要先加1; 而2位为TI表指示器,0是指GDT,1是指LDT; 0、1位是RPL请求者特权级,00最高,11最低 —— 在x86保护模式下修改寄存器是系统之灵,必须有对应的权限才能修改(当前执行权限和段寄存器中(被修改的)的RPL均不低于目标段的RPL) 段描述符:8x8=64位长的结构,用来描述一个段的各种属性。结构: 0、1字节+6字节低4位(20位) 段边界/段长度:最大1MB或者4G(看粒度位的单位) 2、3、4、7字节(32位) 段基址:4G线性地址的任意位置(不一定非要被16整除) 6、7字节的奇怪设计是为了兼容80286(24位地址总线) 剩下的那些是段属性,详见20180819143434 段描述表:多任务操作系统中,含有多个任务,而每个人物都有多个段,其段描述符存于段描述表中。IA-32处理器有3个段描述表:GDT、LDT和IDT。 GDT(Global Descripter Table) 全局段描述符表:一个系统一般只有一个GDT,含有每一个任务都可以访问的段;通常包含操作系统所使用的代码段、数据段和堆栈段,GDT同时包含各进程LDT数据段项,以及进程间通讯所需要的段。GDTR是CPU提供的寄存器,存储GDT的位置和边界;在32位模式下RGDT有48位长(高32位基地址+低16位边界),在32e模式下有80位长(高64位基地址+低16位边界)。GDT的第一个表项留空不用,是空描述符,所以索引号要加1。GDT最多128项。 LDT(Local Descripter Table) 局部段描述符表:16位长,属于某个进程。一个进程一个LDT,对应有RLDT寄存器,进程切换时RLDT改变。RLDT和RGDT不一样,RLDT是一个索引值而不是实际指向,指向GDT中某一个LDT描述项。所以如果要获取LDT中的某一项,先要访问GDT找到对应LDT,再找到LDT中的一项。编译程序时,程序内赋予了虚拟页号。在程序运行时,通过对应LDT转译成物理地址。故虚拟页号是局部性的、不同进程的页号会有冲突。LDT没有空选择子。 IDT(Interrupt Descripter Table) 中断段描述符表;一个系统一般也只有一个。 以下这个图能做一点解释: 2. 线性地址 → 物理地址 (页式内存管理)这一步由CPU的页式管理单元来负责转换。——MMU(内存管理单元)。 线性地址可以拆分为三部分(或者两部分): 页(Page):线性地址被划分为大小一致的若干内存区域,其对应映射到大小相同的与物理空间区域页框(Frame)上。这个映射不一定是连贯而有序的。 CR3:页目录基址寄存器。对于每一个进程,CR3的内容不同(有点像RLDT),页目录基址也不同,线性地址-物理地址的映射也不同。 页目录:占用一个4kb的内存页,最多存储1024个页目录表项(PDE),一个PDE有4字节。在没启用PAE时,有两种PDE,规格不同。 页目录表项(PDE):每个程序有多个页表,即拥有多个PDE。PDE的结构如下:12~31位(20位)表示页表起始物理地址的高20位(页表基址低12位为0,即一定以4kb对齐)。 页表:一个页表占4kb的内存页,最多存储1024个页表项(PTE),一个PTE是4字节。页表的基址是4kb对齐的,低12位是0。 采用对页表项的二级管理模式(也目录→页表→页)能够节约空间。因为不存在的页表就可以不分配空间,并且对于Windows来说只有一级页表才会存在主存中,二级可以存在辅存中——不过Linux中它们都常驻主存。 一些CPU会提供更多级的架构,如三级、四级。Linux中,有对应的高层次抽象,提供了一个四层页管理架构:把中间的某几个定为长度为0,就可以调整架构级数。如“四化二”:某地址0x08147258,对应的PUD、PMD里只有一个表项为PUD→PMD,PMD→PT;划分的时候,PGD=0000100000,PUD=PMD=0,PT=0101000111. 3. TLB (转换检测缓冲区、快表、转译后被缓冲区)处理器中,一个具有并行朝赵能力的特殊高速缓存器,存储最近访问过的一些页表项(时空局部性原理,减少页映射的内存访问次数)。 TLB较贵,通常能够存放16~512个页表项。 TLB命中:直接取出对应的页表项 TLB缺失:先淘汰TLB中的某一项(TLB替换策略,一些算法,可以由硬件或软件来实现) 硬件处理TLB Miss:CPU会遍历页表,找到正确的PTE;如果没有找到,CPU就会发起一个页错误并将控制权交给操作系统。 软件处理TLB Miss:CPU直接发出未命中错误,让操作系统来处理。 脏记录:当TLB中某个PTE项失效(如切换进程、进程退出、虚拟页换出到磁盘),PTE标记为不存在,此时映射已经不成立了。操作系统要保证即时刷新掉这些脏记录,不同的CPU有不同的刷新TLB方法,但每次都完全刷新TLB会很慢,所以现在有一些策略,扩展对一个PTE的描述(如针对某个进程、空间的标识,如果目前进程与PTE相关,就会忽略掉),这样可以让多个进程同时共存TLB Linux 段式管理Linux似乎没有理会Intel的那一套段的机制,而是做了一个高级的抽象。Linux对所有的进程使用了相同的段来对指令和数据寻址,让每个段寄存器都指向同一个段描述符,让这个段描述符的基址为0,长度为4G。即用这种方式略去了段式内存管理。对应多有用户代码段、用户数据段、内核代码段和内核数据段。可以在segment.h中看到,四种段对应的段基址都是0,这就是“平坦内存模型”,这样就有段内偏移地址=逻辑地址 且,四种段对应的都为GDT。即Linux大多数情况都不使用LDT,除非使用wine等Windows防真程序。 Linux 0.11中每个进程划分64MB的虚拟内存空间。故逻辑地址范围为0~0x4000000","categories":[],"tags":[{"name":"Tech","slug":"Tech","permalink":"https://zjusct.github.io/tags/Tech/"},{"name":"Operating System","slug":"Operating-System","permalink":"https://zjusct.github.io/tags/Operating-System/"}],"author":"王克"},{"title":"如何在墙内快速部署CentOS 7的MySQL","slug":"Install-MySQL-on-CentOS7-Inside-GFW","date":"2019-03-31T02:45:19.000Z","updated":"2021-02-11T05:46:05.713Z","comments":true,"path":"2019/03/31/Install-MySQL-on-CentOS7-Inside-GFW/","link":"","permalink":"https://zjusct.github.io/2019/03/31/Install-MySQL-on-CentOS7-Inside-GFW/","excerpt":"MySQL 被 Oracle 收购后,CentOS 的镜像仓库中提供的默认的数据库也变为了 MariaDB,所以默认没有 MySQL ,需要手动安装。 其实安装 MySQL 也并不是一件很难的事情,但是由于一些实际存在的问题(比如某墙),让默认通过 yum 安装 MySQL 的速度太慢。这里提出一种可行的方案来快速部署 MySQL ,此方案同样适用于其他 rpm 包软件的手动安装。 本文实际在讲的是,如何利用各种手段,加速和改善yum的安装过程。","text":"MySQL 被 Oracle 收购后,CentOS 的镜像仓库中提供的默认的数据库也变为了 MariaDB,所以默认没有 MySQL ,需要手动安装。 其实安装 MySQL 也并不是一件很难的事情,但是由于一些实际存在的问题(比如某墙),让默认通过 yum 安装 MySQL 的速度太慢。这里提出一种可行的方案来快速部署 MySQL ,此方案同样适用于其他 rpm 包软件的手动安装。 本文实际在讲的是,如何利用各种手段,加速和改善yum的安装过程。 传统方案……慢到怀疑人生根据官方指南,我们执行如下命令: 123456# 下载源wget \"https://dev.mysql.com/get/mysql80-community-release-el7-2.noarch.rpm\"# 安装源sudo rpm -ivh mysql80-community-release-el7-2.noarch.rpm# 检查源是否成功安装sudo yum repolist enabled | grep \"mysql80-community*\" 接下来就是正常的安装步骤: 1sudo yum install mysql-community-server mysql 但是由于一些原因,下载速度基本是几Byte/s,MySQL 服务器的大小(加上依赖服务)差不多有600MB,这种方法基本不可取。手头没有特别好的而且很新的软件源,就打算手动安装。 手动安装法首先依然需要下载并安装官方源。 1yum install mysql-community-server 利用该命令我们可以获取一些 MySQl Server 以来安装顺序及其版本: 1234567891011121314================================================================================================= Package Arch Version Repository Size=================================================================================================Reinstalling: mysql-community-client x86_64 8.0.15-1.el7 mysql80-community 25 M mysql-community-libs x86_64 8.0.15-1.el7 mysql80-community 2 M mysql-community-common x86_64 8.0.15-1.el7 mysql80-community 570 K mysql-community-server x86_64 8.0.15-1.el7 mysql80-community 360 MTransaction Summary================================================================================================= 解压并分析rpm源包: 12rpm2cpio mysql80-community-release-el7-2.noarch.rpm | cpio -divvim /etc/yum.repos.d/mysql-community.repo 从中我们可以找到对应版本的网络路径为http://repo.mysql.com/yum/mysql-8.0-community/el/7/x86_64/。 打开该地址,找到对应的几个安装包: mysql-community-client-8.0.15-1.el7.x86_64.rpm mysql-community-libs-8.0.15-1.el7.x86_64.rpm mysql-community-common-8.0.15-1.el7.x86_64.rpm mysql-community-server-8.0.15-1.el7.x86_64.rpm 使用某种下载工具(我使用的是迅雷)下载,然后使用scp指令上传到服务器上: 1234scp mysql-community-client-8.0.15-1.el7.x86_64.rpm [email protected]:/root/mysql-community-client-8.0.15-1.el7.x86_64.rpmscp mysql-community-libs-8.0.15-1.el7.x86_64.rpm [email protected]:/root/mysql-community-libs-8.0.15-1.el7.x86_64.rpmscp mysql-community-common-8.0.15-1.el7.x86_64.rpm [email protected]:/root/mysql-community-common-8.0.15-1.el7.x86_64.rpmscp mysql-community-server-8.0.15-1.el7.x86_64.rpm [email protected]:/root/mysql-community-server-8.0.15-1.el7.x86_64.rpm 按照先后顺序依次执行yum本地安装: 123456sudo yum localinstall mysql-community-common-8.0.15-1.el7.x86_64.rpmsudo yum localinstall mysql-community-libs-8.0.15-1.el7.x86_64.rpmsudo yum localinstall mysql-community-client-8.0.15-1.el7.x86_64.rpmsudo yum localinstall mysql-community-server-8.0.15-1.el7.x86_64.rpmsudo yum -y install mysql 安装成功,启动并测试服务: 12systemctl start mysqld.servicesystemctl status mysqld.service 找出默认密码: 1grep "password" /var/log/mysqld.log >> defalut_mysql_passwd.txt","categories":[],"tags":[{"name":"MySQL","slug":"MySQL","permalink":"https://zjusct.github.io/tags/MySQL/"},{"name":"CentOS","slug":"CentOS","permalink":"https://zjusct.github.io/tags/CentOS/"}],"author":"王克"},{"title":"BP算法","slug":"bp","date":"2018-12-24T13:03:59.000Z","updated":"2021-02-11T05:46:05.721Z","comments":true,"path":"2018/12/24/bp/","link":"","permalink":"https://zjusct.github.io/2018/12/24/bp/","excerpt":"本文探讨BP算法。","text":"本文探讨BP算法。 Feed forward neural network and back propagation1. Neuron structure 上图是一种典型的神经元结构,$x_n$是神经元的输入,将输入加权求和后再通过激活函数即可得到此神经元的输出:$$t = \\sum_{i=1}^{n}{w_ix_i} + b$$$$a = f(t)$$ 为计算方便,可将偏置$b$提到求和符号里面,相当于加入一个恒为1的输入值,对应的权重为$b$:$$t = \\sum_{i=0}^{n}{w_ix_i},(x_0 = 1, w_0 = b)$$$$a = f(t)$$此即为上图神经元结构对应的表达式 常用的激活函数有sigmoid, ReLU, tanh等。 2. Network structure 这是一个简单的3层网络,输入层有3个输入值,隐藏层包含3个隐藏神经元,最后是两个输出值隐藏层神经元的前向计算过程: $$z_i^{l} = \\sum_{i=0}^{n}w_{ij}^{l}x_j, (x_0 = 1, w_0 = b)$$ $$a_i^l = f(z_i^l)$$ $l$表示第几层。 这个网络的抽象数学表达式为:$$F(x) = f_3(f_2(x * W_2 + b_2) * W_3 + b_3)$$ 事实上,深度神经网络一般都能够抽象为一个复合的非线性多元函数,有多少隐藏层就有多少层复合函数:$$F(x) = f_n\\left(\\dots f_3(f_2(f_1(x) * w_1 + b_1) * w_2 + b_2)\\dots\\right)$$ 3. LossLoss,即损失,用来衡量神经网络的输出值与实际值的误差,对于不同的问题,通常会定义不同的loss函数 回归问题常用的均方误差:$$MSE = \\frac{1}{n}\\sum_{i=1}^{n}(Y - f(x))^2$$$Y$为实际值,$f(x)$为网络预测值 分类问题常用的交叉熵(m类):$$L = \\sum_{k=1}^{n}\\sum_{i=1}^{m}l_{ki}log(p_{ki})$$$l_{ki}$表示第k个样本实际是否属于第i类(0,1编码),$p_{ki}$表示第k个样本属于第i类的概率值 特别地,二分类问题的交叉熵损失函数形式为:$$L = \\sum_{i=1}^{n}[y_ilog(p_i) + (1 - y_i)log(1 - p_i)]$$$y_i$为第i个样本所属类别,$p_i$为第i个样本属于$y_i$类的概率 4. Back propagationBP 是用来将loss反向传播的算法,用来调整网络中神经元间连接的权重和偏置,整个训练的过程就是:前向计算网络输出->;根据当前网络输出计算loss->BP算法反向传播loss调整网络参数,不断循环这样的三步直到loss达到最小或达到指定停止条件 BP算法的本质是求导的链式法则,对于上面的三层网络,假设其损失函数为$C$,激活函数为$\\sigma$,第$l$第$i$个神经元的输入为$z_i^{(l)}$,输出为$a_i^{(l)}$ 则通过梯度下降来更新权值和偏置的公式如下:$$W_{ij}^{(l)} = W_{ij}^{(l)} - \\eta\\frac{\\partial}{\\partial W_{ij}^{(l)}}C\\tag1$$$$b_{i}^{(l)} = b_{i}^{(l)} - \\eta\\frac{\\partial}{\\partial b_{i}^{(l)}}C\\tag2$$ $W_{ij}^{(l)}$表示第$l$层第$i$个神经元与第$l - 1$层第$j$个神经元连接的权值,$b_i^{(l)}$表示第$l$层第$i$个神经元的偏置 $\\eta$表示学习率 由更新公式可见主要问题在于求解损失函数关于权值和偏置的偏导数 第$l$层第$i$个神经元的输入$z_i^{(l)}$为:$$z_i^{(l)} = \\sum_{j=1}^{n^{(l-1)}}{W_{ij}^{(l)}a_j^{(l-1)}} + b_i^{l}\\tag3$$ 则更新公式中偏导项可化为: $$\\frac{\\partial}{\\partial W_{ij}^{(l)}}C = \\frac{\\partial C}{\\partial z_i^{(l)}} \\bullet \\frac{\\partial z_i^{(l)}}{\\partial W_{ij}^{(l)}} = \\frac{\\partial C}{\\partial z_i^{(l)}} \\bullet a_i^{(l-1)}\\tag4$$ $$\\frac{\\partial}{\\partial b_{i}^{(l)}}C = \\frac{\\partial C}{\\partial z_i^{(l)}} \\bullet \\frac{\\partial z_i^{(l)}}{\\partial b_{i}^{(l)}} = \\frac{\\partial C}{\\partial z_i^{(l)}}\\tag5$$ 定义 $$\\delta_i^{(l)} = \\frac{\\partial}{\\partial z_i^{(l)}}C\\tag6$$ 现在问题转化为求解$\\delta_i^{(l)}$,对第$l$层第$j$个神经元有:$$\\delta_j^{(l)} = \\frac{\\partial C}{\\partial z_j^{(l)}} = \\sum_{i=1}^{n^{(l+1)}}\\frac{\\partial C}{\\partial z_i^{(l+1)}} \\bullet \\frac{\\partial z_i^{(l+1)}}{\\partial a_j^{(l)}} \\bullet \\frac{\\partial a_j^{(l)}}{\\partial z_j^{(l)}} \\=\\sum_{i=1}^{n^{(l+1)}}\\delta_i^{(l+1)} \\bullet \\frac{\\partial(W_{ij}^{l+1} + b_i^{(l+1)})}{\\partial a_j^{(l)}} \\bullet \\sigma^\\prime(z_j^{(l)})\\=\\sum_{i=1}^{n^{(l+1)}}\\delta_i^{(l+1)} \\bullet W_{ij}^{(l+1)} \\bullet \\sigma^\\prime(z_j^{(l)})\\tag7$$ 则:$$\\delta^{(l)} = ((W^{(l+1)})^T\\delta^{(l+1)})\\odot\\sigma^\\prime(z^{(l)})\\tag8$$ 损失函数关于权重和偏置的偏导分别为:$$\\frac{\\partial C}{\\partial W_{ij}^{(l)}} = a_i^{(l-1)}\\delta_i^{(l)}\\tag9$$$$\\frac{\\partial C}{\\partial b_{i}^{(l)}} =\\delta_i^{(l)}\\tag{10}$$ 误差根据8式由输出层向后传播,再结合1,2,9,10四式对权重和偏置进行更新 5.实现下面是一个简单3隐层神经网络的实现 In [ ]: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167import numpy as npdef loss(pred, y): return np.sum((pred - y) ** 2)def loss_prime(pred, y): return pred - yclass network: def __init__(self, input_size, hidden_size, num_layers, output_size, loss = loss, loss_prime = loss_prime): self.input_size = input_size self.hidden_size = hidden_size self.num_layers = num_layers self.output_size = output_size # activation function self.activation = self.sigmoid # derivative of activation function self.activation_prime = self.sigmoid_prime # loss funciton self.loss = loss # derivative of loss function self.loss_prime = loss_prime # input->hidden self.w_ih = np.random.randn(input_size, hidden_size) self.b_ih = np.random.randn(1, hidden_size) # hidden layers self.W_hh = [np.random.randn(hidden_size, hidden_size) for _ in range(num_layers - 1)] self.B_hh = [np.random.randn(1, hidden_size) for _ in range(num_layers - 1)] # hidden->output self.w_ho = np.random.randn(hidden_size, output_size) self.b_ho = np.random.randn(1, output_size) # assemble w and b self.W = [self.w_ih] self.W.extend(self.W_hh) self.W.append(self.w_ho) self.B = [self.b_ih] self.B.extend(self.B_hh) self.B.append(self.b_ho) # activation def sigmoid(self, x): return 1.0 / (1 + np.exp(-x)) def sigmoid_prime(self, x): return self.sigmoid(x) * (1 - self.sigmoid(x)) # forward pass, calculate the output of the network def forward(self, a): for w, b in zip(self.W, self.B): a = self.activation(np.dot(a, w) + b) return a # backpropagate error def backward(self, x, y): delta_w = [np.zeros(w.shape) for w in self.W] delta_b = [np.zeros(b.shape) for b in self.B] # get output of each layer in forward pass out = x outs = [] zs = [] for w, b in zip(self.W, self.B): z = np.dot(out, w) + b zs.append(z) out = self.activation(z) outs.append(out) # δ of last layer delta = self.loss_prime(outs[-1], y) * self.activation_prime(zs[-1]) delta_b[-1] = delta delta_w[-1] = np.dot(outs[-2].transpose(), delta) for i in range(2, len(delta_w)): delta = np.dot(delta, self.W[-i+1].transpose()) * self.activation_prime(zs[-i]) delta_b[-i] = delta delta_w[-i] = np.dot(outs[-i-1].transpose(), delta) return delta_w, delta_b # update w and b def update(self, batch, lr): delta_w = [np.zeros(w.shape) for w in self.W] delta_b = [np.zeros(b.shape) for b in self.B] for x, y in batch: d_w, d_b = self.backward(x, y) delta_w = [a + b for a, b in zip(delta_w, d_w)] delta_b = [a + b for a, b in zip(delta_b, d_b)] self.W = [w - lr * t for w, t in zip(self.W, delta_w)] self.B = [b - lr * t for b, t in zip(self.B, delta_b)] # SGD training def train(self, train_data, epochs, batch_size, lr): for i in range(epochs): np.random.shuffle(train_data) batches = [train_data[t : t + batch_size] for t in range(0, len(train_data), batch_size)] for batch in batches: self.update(batch, lr) loss = 0 for x, y in train_data: loss += self.loss(self.forward(x), y) loss /= len(train_data) print(\"Epoch %d done, loss: %f\" % (i + 1, loss)) # predict def predict(self, x): return self.forward(x)# use it for handwriting digits classificationimport tensorflow as tfmnist = tf.keras.datasets.mnistdef onehot(y): arr = np.zeros([y.shape[0], 10]) for i in range(y.shape[0]): arr[i][y[i]] = 1 return arr(x_train, y_train),(x_test, y_test) = mnist.load_data()x_train, x_test = x_train / 255.0, x_test / 255.0x_train = x_train.reshape([-1, 28 * 28])x_test = x_test.reshape([-1, 28 * 28])y_train = onehot(y_train)y_test = onehot(y_test)train_data = [t for t in zip(x_train, y_train)]test_data = [t for t in zip(x_test, y_test)]input_size = 28 * 28hidden_size = 100num_layers = 3output_size = 10net = network(input_size, hidden_size, num_layers, output_size)lr = 0.005epochs = 100batch_size = 100net.train(train_data, epochs, batch_size, lr)def softmax(x): exp = np.exp(x) return exp / np.sum(exp)correct = 0for x, y in test_data: ret = net.forward(x) pred = softmax(ret) if np.argmax(pred) == np.argmax(y): correct += 1acc = float(correct) / len(test_data)print('test accuracy: ', acc)","categories":[],"tags":[{"name":"Tech","slug":"Tech","permalink":"https://zjusct.github.io/tags/Tech/"},{"name":"Machine Learning","slug":"Machine-Learning","permalink":"https://zjusct.github.io/tags/Machine-Learning/"}],"author":"陈岩"},{"title":"Distributed Tensorflow","slug":"tensorflow","date":"2018-12-23T11:57:25.000Z","updated":"2021-02-11T05:46:05.729Z","comments":true,"path":"2018/12/23/tensorflow/","link":"","permalink":"https://zjusct.github.io/2018/12/23/tensorflow/","excerpt":"","text":"1.单机log_device_placement单机情况比较简单,不需要特殊配置,TensorFlow会自动将计算任务分配到可用的GPU上,在定义session时,可以通过log_device_placement参数来打印具体的计算任务分配: 123456789import tensorflow as tfa = tf.constant([1.0, 2.0, 3.0], shape=[3], name='a')b = tf.constant([1.0, 2.0, 3.0], shape=[3], name='b')c = a + bwith tf.Session(config = tf.ConfigProto(log_device_placement = True)) as sess: sess.run(tf.global_variables_initializer()) print(sess.run(c)) 指定设备如果需要让一些运算在特定的设备上执行,可以使用tf.device: 1234567891011import tensorflow as tfwith tf.device('/cpu:0'): a = tf.constant([1.0, 2.0, 3.0], shape=[3], name='a') b = tf.constant([1.0, 2.0, 3.0], shape=[3], name='b')with tf.device('/gpu:0'): c = a + bwith tf.Session(config = tf.ConfigProto(log_device_placement = True)) as sess: sess.run(tf.global_variables_initializer()) print(sess.run(c)) 环境变量尽管上面一个例子中我们只给CPU和GPU0指定了计算任务,但是两块显卡的显存都被占满了: 因为TensorFlow会默认占满所有可见GPU的显存,对于简单的计算任务,这样显然非常浪费,我们可以通过修改环境变量CUDA_VISIBLE_DEVICES解决这个问题: 12# 运行时指定环境变量CUDA_VISIBLE_DEVICES=0 python demo.py 1234# Python 代码中修改环境变量import osos.environ['CUDA_VISIBLE_DEVICES']='0'... 2.多机In-graph & Between-graphTensorFlow的分布式训练有两种模式:In-graph和Between-graph In-graph: 不同的机器执行计算图的不同部分,和单机多GPU模式类似,一个节点负责模型数据分发,其他节点等待接受任务,通过tf.device(“/job:worker/task:n”)来指定计算运行的节点 Between-graph:每台机器执行相同的计算图 Author: 陈岩PostDate: 2018.12.21","categories":[],"tags":[{"name":"Tech","slug":"Tech","permalink":"https://zjusct.github.io/tags/Tech/"},{"name":"Tensorflow","slug":"Tensorflow","permalink":"https://zjusct.github.io/tags/Tensorflow/"}],"author":"陈岩"},{"title":"Quick Guide to CUDA Profiling","slug":"cuprof","date":"2018-12-07T15:48:33.000Z","updated":"2021-02-11T05:46:05.725Z","comments":true,"path":"2018/12/07/cuprof/","link":"","permalink":"https://zjusct.github.io/2018/12/07/cuprof/","excerpt":"1. Brief Introduction在并行计算领域,很难通过纯理论的分析来确定程序的性能,GPGPU这种基于特定计算架构的计算任务更甚。事实上,很多制约并行算法性能的瓶颈很可能不在算法本身(比如资源调度障碍)。因此,对给定程序进行充分的性能测试与后续分析是相当必要的调优方法。 Nvidia提供了nvprof,nvvp,Nsight三种cuda可用的性能分析工具,本文将简述配合使用nvprof与nvvp的cuda程序性能分析方法。","text":"1. Brief Introduction在并行计算领域,很难通过纯理论的分析来确定程序的性能,GPGPU这种基于特定计算架构的计算任务更甚。事实上,很多制约并行算法性能的瓶颈很可能不在算法本身(比如资源调度障碍)。因此,对给定程序进行充分的性能测试与后续分析是相当必要的调优方法。 Nvidia提供了nvprof,nvvp,Nsight三种cuda可用的性能分析工具,本文将简述配合使用nvprof与nvvp的cuda程序性能分析方法。 2. Check Out Device Properties由于cuda程序的线程/块分配方案与程序运行的的硬件高度相关,故对目标平台的硬件参数有一定程度的了解是相当有必要的。我们可以使用cudaGetDeviceProperties()函数获取设备的各项属性,下述代码可以结合cuda_runtime_api.h#1218处struct cudaDeviceProp的定义和各属性的相应注解自行理解。 12345678int nDevices;cudaDeviceProp prop;cudaGetDeviceCount( &nDevices );for ( auto i = 0; i != nDevices; ++i ){ cudaGetDeviceProperties( &prop, i ); // check out interesting property} 3. Profile Using Nvprof3.1. Quick Start1nvprof --help 3.2. Metrics 使用--query-metrics列出所有可测试的性能指标。 使用--metrics sm_efficiency,warp_execution_efficiency,...指定要测试的性能指标。 3.3. PC Sampling在CC5.2或更高的设备上支持使用PC采样(PC sampling)技术。 PC采样技术通过Round-Robin方法对SM中所有活动线程束的PC状态进行采样,采样结果包含如下两种可能: 线程束完成了当前指令。 线程束被stall,不能完成当前指令,并可以给出stall的原因。 事实上线程束被stall并不代表指令流水线处于stall状态,因为其他正常运行的线程束可以利用计算资源。 CC6.0以上的设备对PC采样方法进行了改进,通过检查线程束调度器是否执行指令来确定指令流水线是否真正处于stall状态,从而能正确指示指令stall的原因。 4. Data Visualize Using Nvvpnvvp可以导入nvprof的分析结果,可视化显示统计图表,并且建议性地指出程序可能存在的瓶颈。 以饼状图显示各类stall比重 以频谱显示各类指令比例 通过source file mapping可视化指令stall状态,需要在编译选项中指定-lineinfo 4.1. Usage12nvprof -f --kernels \"kernelName\" --analysis-metrics -o a.nvvp <task> <args>nvvp a.nvvp 这里我使用的方法是在集群上用nvprof做性能测试,之后将分析结果*.nvvp传回本地用nvvp做可视化。 Ext. RemarksTradeoff Between Registers and Threads在实际Profiling中重新认识了这个问题。 在默认情况下,nvcc为每个线程分配maxRegsPerThread个数的寄存器,在Tesla K40上,这个值为64。同时,每个SM持有为65536个寄存器,这意味着单个SM中的线程数最多不超过1024。通过检查参数表,我们发现该设备单个SM可容纳线程数为2048。这意味着我们计算任务的GPU利用率最大只有50%(所有SM均满载的状态下)。 在这种情况下,如果我们将分配给单个线程的寄存器数目减半,则最大GPU利用率可以达到100%。但若发生寄存器溢出(register spilling),溢出的存储空间被放到片外的local memory,访问速度在(同在片外的)global memory级别。 在实际的CUDA核函数中,能全部利用64个寄存器的情况很少。寄存器的使用情况可以在nvvp中检查,如果发现有大量寄存器浪费,可以立即减少寄存器数量。在大多数情况下,可以结合计算任务的量级和性质来调节线程最大寄存器数,从而达到有针对性的性能调优。 在nvcc中指定单个线程最大寄存器数,可以添加编译选项-maxrregcount=N。如果限定不修改编译选项或需要逐核函数指定,则需要使用__launch_bounds__限定符,如下(隐式地指定了最大寄存器个数): 123456__global__ void__launch_bounds__(maxThreadsPerBlock, minBlocksPerMultiprocessor)MyKernel(...){ ...} 在我的path tracer中对上述方法进行测试,将每线程的寄存器数减半为32,SM线程数加倍并满载,GPU利用率由30+提升到70+,执行速度有1.5倍左右的提升。 Tradeoff Between BlockDim and BlockPerSM当一个块(block)中的所有线程束(warp)全部完成时,这个块才可以被SM调度。如果块的大小过大,则块的运行速度受单个线程束约束的开销就越大(如果算法并行度很高,增大块的大小不失为一个好选择);如果块的大小过小,则一方面SM可能无法达到其最大利用率(受maxBlocksPerSM的限制),另一方面SM调度块的额外开销也会增大。尤其是针对不同特点的计算任务有不同的更优选择,如divergency较高的任务更适合较小的BlockDim。所以在选择BlockDim时不仅要在算法的适应性上做考虑,还要通过多次性能测试来进行针对性的优化。 Beware of Ladder Effects注意计算资源分配时要注意分配的资源量要能够被组别整除,否则会出现断层状的资源浪费现象。 每块线程数与SM中最大线程束数的关系","categories":[],"tags":[{"name":"Tech","slug":"Tech","permalink":"https://zjusct.github.io/tags/Tech/"},{"name":"CUDA","slug":"CUDA","permalink":"https://zjusct.github.io/tags/CUDA/"},{"name":"Profile","slug":"Profile","permalink":"https://zjusct.github.io/tags/Profile/"}],"author":"小妹妹"},{"title":"CUDA内存管理总结(一)","slug":"cuda","date":"2018-11-24T16:46:08.000Z","updated":"2021-02-11T05:46:05.725Z","comments":true,"path":"2018/11/25/cuda/","link":"","permalink":"https://zjusct.github.io/2018/11/25/cuda/","excerpt":"本文将探讨CUDA中的内存管理机制。","text":"本文将探讨CUDA中的内存管理机制。 一、寄存器 GPU的每个SM(流多处理器)都有上千个寄存器,每个SM都可以看作是一个多线程的CPU核,但与一般的CPU拥有二、四、六或八个核不同,一个GPU可以有N个SM核;同样,与一般的CPU核支持一到两个硬件线程不同,每个SM核可能有8~192个SP(流处理器),亦即每个SM能同时支持这么多个硬件线程。事实上,一台GPU设备的所有SM中活跃的线程数目通常数以万计。 1.1 寄存器映射方式 CPU处理多线程:进行上下文切换,使用寄存器重命名机制,将当前所有寄存器的状态保存到栈(系统内存),再从栈中恢复当前需要执行的新线程在上一次的执行状态。这些操作通常花费上百个CPU时钟周期,有效工作吞吐量低。 GPU处理多线程:与CPU相反,GPU利用多线程隐藏了内存获取与指令执行带来的延迟;此外,GPU不再使用寄存器重命名机制,而是尽可能为每个线程分配寄存器,从而上下文切换就变成了寄存器组选择器(或指针)的更新,几乎是零开销。 1.2 寄存器空间大小 每个SM可提供的寄存器空间大小分别有8KB、16KB、32KB和64KB,每个线程中的每个变量占用一个寄存器,因而总共会占用N个寄存器,N代表调度的线程数量。当线程块上的寄存器数目是允许的最大值时,每个SM会只处理一个线程块。 1.3 SM调度线程、线程块 由于大多数内核对寄存器的需求量很低,所以可以通过降低寄存器的需求量来增加SM上线程块的调度数量,从而提高运行的线程总数,根据线程级并行“占用率越高,程序运行越快”,可以实现运行效率的优化。当线程级并行(Thread-Level Parallelism,TLP)足以隐藏存储延迟时会达到一个临界点,此后想要继续提高程序性能,可以在单个线程中实现指令级的并行(Instruction-Level Parallelism,ILP),即单线程处理多数据。 但在另一方面,每个SM所能调度的线程总量是有限制的,因此当线程总量达到最大时,再减少寄存器的使用量就无法达到提高占有率的目的(如下表中寄存器数目由20减小为16,线程块调度数量不变),所以在这种情况下,应增加寄存器的使用量到临界值。 1.4 寄存器优化方式 1)将中间结果累积在寄存器而非全局内存中。尽量避免全局内存的写操作,因为如果操作聚集到同一块内存上,就会强制硬件对内存的操作序列化,导致严重的性能降低; 2)循环展开。循环一般非常低效,因为它们会产生分支,造成流水线停滞。 1.5 总结 使用寄存器可以有效消除内存访问,或提供额外的ILP,以此实现GPU内核函数的加速,这是最为有效的方法之一。 二、共享内存2.1 基本概念 1、共享内存实际上是可以受用户控制的一级缓存,每个SM中的一级缓存和共享内存共用一个64KB的内存段。 2、共享内存的延迟很低,大约有1.5TB/s的带宽,而全局内存仅为160GB/s,换言之,有效利用共享内存有可能获得7倍的加速比。但它的速度依然只有寄存器的十分之一,并且共享内存的速度几乎在所有GPU中都相同,因为它由核时钟频率驱动。 3、只有当数据重复利用、全局内存合并,或者线程之间有共享数据(例如同时访问相同地址的存储体)的时候使用共享内存才更合适,否则将数据直接从全局内存加载到寄存器性能会更好。 4、共享内存是基于存储体切换的架构(bank-switched architecture),费米架构的设备上有32个存储体。无论有多少线程发起操作,每个存储体每个周期只执行一次操作。因此,如果线程束中的每个线程各访问一个存储体,那么所有线程的操作都可以在一个周期内同时执行,且所有操作都是独立互不影响的。此外,如果所有线程同时访问同一地址的存储体,会触发一个广播机制到线程束中的每个线程中。但是,如果是其他的访问方式,线程访问共享内存就需要排队,即一个线程访问时,其他线程将阻塞闲置。因此很重要的一点时,应该尽可能地获得零存储体冲突的共享内存访问。 2.2 Example:使用共享内存排序2.2.1 归并排序 假设待排序的数据集大小为N,现将数据集进行划分。根据归并排序的划分原则,最后每个数据包中只有两个数值需要排序,因此,在这一阶段,最大并行度可达到 $N \\over 2$ 个独立线程。例如,处理一个大小为512KB的数据集,共有128K个32位的元素,那么最多可以使用的线程个数为64K个(N=128K,N/2=64K),假设GPU上有16个SM,每个SM最多支持1536个线程,那么每个GPU上最多可以支持24K个线程,因此,按照这样划分,64K的数据对只需要2.5次迭代即可完成排序操作。 但是,如果采用上述划分排序方式再进行合并,我们需要从每个排好序的数据集中读出元素,对于一个64K的集合,需要64K次读操作,即从内存中获取256MB的数据,显然当数据集很大的时候不合适。 因此,我们采用通过限制对原始问题的迭代次数,通过基于共享内存的分解方式来获得更好的合并方案。因为在费米架构的设备上有32个存储体,即对应32个线程,所以当需要的线程数量减少为32(一个线程束)时,停止迭代,于是共需要线程束4K个(128K/32=4K),又因为GPU上有16个SM,所以这将为每个SM分配到256个线程束。然而由于费米架构设备上的每个SM最多只能同时执行48个线程束,因此多个块将被循环访问。 通过将数据集以每行32个元素的方式在共享内存中进行分布,每列为一个存储体,即可得到零存储体冲突的内存访问,然后对每一列实施相同的排序算法。(或者也可以理解为桶排序呀) 然后再进行列表的合并。 2.2.2 合并列表 先从串行合并任意数目的有序列表看起: 12345678910111213141516171819202122232425262728293031323334353637383940414243444546void merge_array(const u32 *const src_array, //待排序数组 u32 *const dest_array, //排序后的数组 const u32 num_lists, //列表总数 const u32 num_elements) //数据总数{ const u32 num_elements_per_list = (num_elements / num_lists);//每个列表中的数据个数 u32 list_indexes[MAX_NUM_LISTS]; //所有列表当前所在的元素下标 for(u32 list = 0; list < num_lists; list++) { list_indexes[list] = 0; } for(u32 i = 0; i<num_elements; i++) { dest_array[i] = find_min(scr_array, list_indexes, num_lists, num_elements_per_list); }}u32 find_min(const u32*cosnt src_array, u32 *const list_indexes, const u32 num_lists, const u32 num_elements_per_list)//寻找num_lists个元素中的最小值{ u32 min_val = 0xFFFFFFFF; u32 min_idx = 0; for(u32 i = 0; i < num_lists; i++) { if(list_indexes[i] < num_elements_per_list) { const u32 src_idx = i + (list_indexes[i]*num_lists); const u32 data = src_array[src_idx]; if(data <= min_val) { min_val = data; min_idx = i; } } } list_indexes[min_idx]++; return min_val;} 将上述算法用GPU实现 123456789101112__global__ void gpu_sort_array_array(u32 *const data, const u32 num_lists, const u32 num_elements){ const u32 tid = (blockIdx.x * blockDim.x) + threadIdx.x; __shared__ u32 sort_tmp[NUM_ELEM]; __shared__ u32 sort_tmp_1[NUM_ELEM]; copy_data_to_shared(data, sort_tmp, num_lists, num_elements, tid); radix_sort2(sort_tmp, num_lists, num_elements, tid, sort_tmp_1); merge_array6(sort_tmp, data, num_lists, num_elements, tid);} 第一个函数的实现: 123456789101112__device__ void copy_data_to_shared(const u32 *const data, u32 *sort_tmp, const u32 num_lists, const u32 num_elements, const u32 tid){ for(u32 i = 0; i < num_elements; i++) { sort_tmp[i+tid] = data[i+tid]; } __syncthreads();} 该函数中,程序按行将数据从全局内存读入共享内存。当函数调用一个子函数并传入参数时,这些参数必须以某种方式提供给被调用的函数,有两种方法可以采用。一种是通过寄存器传递所需的值,另一种方法是创建一个名为“栈帧”的内存区,但这种方法非常地不高效。出于这一原因,我们需要重新修改合并的程序(merge_array),以避免函数调用,修改后程序如下(单线程): 1234567891011121314151617181920212223242526272829303132333435363738__device__ void merge_array1(const u32 *const src_array, u32 *const dest_array, const u32 num_lists, const u32 num_elements, const u32 tid){ __shared__ u32 list_indexes[MAX_NUM_LISTS]; lists_indexes[tid] = 0;//从每个列表的第一个元素开始 __syncthreads(); //单线程 if(tid == 0) { const u32 num_elements_per_list = (num_elements / num_lists); for(u32 i = 0; i < num_elements; i++) { u32 min_val = 0xFFFFFFFF; u32 min_idx = 0; for(u32 list = 0; list < num_lists; list++) { if(list_indexes[list] < num_elements_per_list) { const u32 src_idx = i + (list_indexes[i]*num_lists); const u32 data = src_array[src_idx]; if(data <= min_val) { min_val = data; min_idx = i; } } } list_indexes[min_idx]++; dest_array[i]=min_val; } }} 这里只用一个线程进行合并,但显然,为了获得更好的性能,一个线程是远远不够的。因为数据被写到一个单一的列表中,所以多个线程必须进行某种形式的合作。 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162__device__ void merge_array6(const u32 *const src_array, u32 *const dest_array, const u32 num_lists, const u32 num_elements, const u32 tid){ //每个列表分到的元素个数 const u32 num_elements_per_list = (num_elements / num_lists); //创建一个共享列表数组,用来储存当前线程所访问的列表元素下标 __shared__ u32 list_indexes[MAX_NUM_LISTS]; list_indexes[tid] = 0; //创建所有线程共享的最小值与最小值线程号 __shared__ u32 min_val; __shared__ u32 min_tid; __syncthreads(); for(u32 i=0; i<num_elements; i++) { u32 data; //如果当前列表还未被读完,则从中读取数据 if(list_indexes[tid] < num_elements_per_list); { //计算出当前元素在原数组中的下标 const u32 src_idx = tid + (list_indexes[tid] * num_lists); data = src_array[src_idx]; } else { data = 0xFFFFFFFF; } //用零号线程来初始化最小值与最小值线程号 if(tid == 0) { min_val = 0xFFFFFFFF; min_tid = 0xFFFFFFFF; } __syncthreads(); //让所有线程都尝试将它们现在手上有的值写入min_val,但只有最小的数据会被保留 //利用__syncthreads()确保每个线程都执行了该操作 atomicMin(&min_val, data); __syncthreads(); //在所有data==min_val的线程中,选取最小线程号写入min_tid if(min_val == data) { atomicMin(&min_tid, tid); } __syncthreads(); //将满足要求的线程所在列表的当前元素往后移一位,进行下一轮比较 //并将筛选结果存入结果数组dest_array if(tid == min_tid) { list_indexes[tid]++; dest_array[i] = data; } }} 上面的函数中将num_lists个线程进行合并操作,但只用了一个线程一次将结果写入结果数据数组中,保证了结果的正确性,不会引起线程间的冲突。 其中使用到了 atomicMin 函数。每个线程以从列表中获取的数据作为入参调用该函数,取代了原先单线程访问列表中所有元素并找出最小值的操作。当每个线程调用 atomicMin 函数时,线程读取保存在共享内存中的最小值并于当前线程中的值进行比较,然后把比较结果重新写回最小值对应的共享内存中,同时更新最小值对应的线程号。然而,由于列表中的数据可能会重复,因此可能出现多个线程的值均为最小值的情况,保留的线程号却各不相同。因此需要执行第二步操作,保证保留的线程号为最小线程号。 虽然这种方法的优化效果很显著,但它也有一定的劣势。例如,atomicMin函数只能用在计算能力为1.2以上的设备上;另外,aotomicMin函数只支持整数型运算,但现实世界中的问题通常是基于浮点运算的,因此在这种情况下,我们需要寻找新的解决方法。 2.2.3 并行归约 并行归约适用于许多问题,求最小值只是其中的一种。它使用数据集元素数量一半的线程,每个线程将当前线程对应的元素与另一个元素进行比较,计算两者之间的最小值,并将得到的最小值移到前面。每进行一次比较,线程数减少一半,如此反复直到只剩一个元素为止,这个元素就是需要的最小值。 在选择比较元素的时候,应该尽量避免选择同一个线程束中的元素进行比较,因为这会明显地导致线程束内产生分支,而每个分支都将使SM做双倍的工作,继而影响程序的性能。因此我们选择将线程束中的元素与另一半数据集中的元素进行比较。如下图,阴影部分表示当前活跃的线程。 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071__device__ void merge_array5(const u32 *const src_array, u32 *const dest_array, const u32 num_lists, const u32 num_elements, const u32 tid){ const u32 num_elements_per_list = (num_elements / num_lists); __shared__ u32 list_indexes[MAX_NUM_LISTS]; __shared__ u32 reduction_val[MAX_NUM_LISTS]; __shared__ u32 reduction_idx[MAX_NUM_LISTS]; list_indexes[tid] = 0; reduction_val[tid] = 0; reduction_idx[tid] = 0; __syncthreads(); for(u32 i=0; i<num_elements; i++) { u32 tid_max = num_lists >> 1;//最大线程数为列表总数的一半 u32 data;//使用寄存器可以提高运行效率,将对共享内存的写操作次数减少为1 //当列表中还有未处理完的元素时 if(list_indexes[tid] < num_elements_per_list) { //计算该元素在原数组中的位置 cosnst u32 src_idx = tid + (list_indexes[tid] * num_lists); data = src_array[src_idx]; } //若当前列表已经处理完,将data赋值最大 else { data = 0xFFFFFFFF; } //将当前元素及线程号写入共享内存 reduction_val[tid] = data; reduction_idx[tid] = tid; __syncthreads; //当前活跃的线程数多于一个时 while(tid_max!=0) { if(tid < tid_max) { //将当前线程中的元素与另一半数据集中的对应元素进行比较 const u32 val2_idx = tid + tid_max; const u32 val2 = reduction_val[val2_idx]; //最后保留较小的那个元素 if(reduction_val[tid] > val2) { reduction_val[tid] = val2; reduction_idx[tid] = reduction_idx[val_idx]; } } //线程数减半,进入下一轮循环 tid_max >>= 1; __syncthreads(); } //在零号线程中将结果写入结果数组,并将相应线程所指的元素后移一位 if(tid == 0) { list_indexes[reduction_idx[0]]++; dest_array[i] = reduction_val[0]; } __syncthreads(); }} 同样,这种方法也在共享内存中创建了一个临时的列表 list_indexes 用来保存每次循环中从 num_list 个数据集列表中选取出来进行比较的数据。如果进行合并的列表已经为空,那么就将临时列表中的对应数据区赋最大值0xFFFFFFFF。而每轮while循环后,活跃的线程数都将减少一半,直到最后只剩一个活跃的线程,亦即零号线程。最后将结果复制到结果数组中并将最小值所对应的列表索引加一,以确保元素不会被处理两次。 2.2.4 混合算法 在了解atomicMin函数和并行归约两种方案后,我们可以利用这两种算法各自的优点,创造出一种新的混合方案。 简单的1~N个数据归约的一个主要问题就是当N增大时,程序的速度先变快再变慢,达到最高效的情形时N在8至16左右。混合算法将原数据集划分成诸多个小的数据集,分别寻找每块中的最小值,然后再将每块得到的结果最终归约到一个值中。这种方法和并行归约的思想非常相似,但同时又省略了并行归约中的多次迭代。代码更新如下: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384#define REDUCTION_SIZE 8#define REDUCTION_SIZE_BIT_SHIFT 3#define MAX_ACTIVE_REDUCTIONS ((MAX_NUM_LISTS) / (REDUCTION_SIZE))__device__ void merge_array(const u32 *const src_array, u32 *const dest_array, const u32 num_lists, const u32 num_elements, const u32 tid){ //每个线程都从原数组中读入一个数据,用作首次比较 u32 data = src_array[tid]; //当前线程所在的数据块编号(8个线程为一组,每个线程处理一个列表) const u32 s_idx = tid >> REDUCTION_SIZE_BIT_SHIFT; //首次进行分别归约的数据块总数 const u32 num_reductions = num_lists >> REDUCTION_SIZE_BIT_SHIFT; const u32 num_elements_per_list = num_elements / num_lists; //在共享内存中创建一个列表,指向每个线程当前所在的元素,并初始化为0 __shared__ u32 list_indexes[MAX_NUM_LISTS]; list_indexes[tid] = 0; //遍历所有数据 for(u32 i=0; i<num_elements; i++) { //每个数据块在内部归约后都会产生一个相应的最小值 //在共享内存中开辟一个列表,用来保存每组的最小值 __shared__ u32 min_val[MAX_ACTIVE_REDUCTIONS]; __shared__ u32 min_tid; //初始化每个数据块的内部最小值 if(tid < num_lists) { min_val[s_idx] = 0xFFFFFFFF; min_tid = 0xFFFFFFFF; } __syncthreads(); //将当前线程的数据与所处数据块的最小值进行比较,并保留较小的那一个 atomicMin(&min_val[s_idx], data); //进行归约的数据块总数不为零时 if(num_reductions > 0) { //确保每个线程都已经将上一步比较操作完成 __syncthreads(); //将每个数据块产生的最小值与零号数据块的最小值进行比较,保留较小的那一个 if(tid < num_reductions) { atomicMin(&min_val[0], min_val[tid]); __syncthreads(); } //如果当前线程的数据等于此次比较保留的最小值,记录最小线程号 if(data == min_val[0]) { atomicMin(&min_tid, tid); } //确保上一步操作每个线程都已经完成,才能执行下一句 __syncthreads(); //如果当前线程号恰为记录下的最小线程号 if(tid == min_tid) { //当前所指元素后移一位 list_indexes[tid]++; //将结果保存入结果数组 dest_array[i] = data; //若该线程对应的列表尚未被处理完 if(list_indexes[tid] < num_elements_per_list) //更新该线程的data,进行下一轮比较 data = src_array[tid + (list_indexes[tid] * num_lists)]; else data = 0xFFFFFFFF; } __syncthreads(); } }} 注意到: 1)原来的min_val由单一的数据扩展成为一个共享数据的数组,这是因为每个独立的线程都需要从它对应的数据集中获取当前的最小值来进行内部比较。每个最小值都是一个32位的数值,因此可以存储在独立的共享内存存储体中。 2)内核函数中的REDUCTION_SIZE的值被设置成8,意味着每个数据块中包含8个数据,程序分别找出每个数据块的最小值,然后再在这些最小值中寻找最终的最小值。 3)内核函数中最重要的一个变化是,只有每次比较的最小值所对应的那个线程的data才会更新,其他线程的data都不会更新。而在之前的内核函数中,每轮比较开始,所有线程都会从对应的列表中重新读入data 的值,随着N的增大,这将变得越来越低效。 2.2.5 总结 1)共享内存允许同一个线程块中的线程读写同一段内存,但线程看不到也无法修改其他线程块的共享内存。 2)共享内存的缓冲区驻留在物理GPU上,所以访问时的延迟远低于访问普通缓冲区的延迟,因此除了使用寄存器,还应更有效地使用共享内存,尤其当数据有重复利用,或全局内存合并,或线程间有共享数据的时候。 3)编写代码时,将关键字_shared__添加到声明中,使得该变量留驻在共享内存中,并且线程块中的每个线程都可以共享这块内存,使得一个线程块中的多个线程能够在计算上进行通信和协作。 4)调用 __syncthreads() 函数来实现线程的同步操作,尤其要注意确保在读取共享内存之前,想要写入的操作都已经完成。另外还需要注意,切不可将这个函数放置在发散分支(某些线程需要执行,而其他线程不需要执行),因为除非线程块中的每个线程都执行了该函数,没有任何线程能够执行之后的指令,从而导致死锁。 5)不妨尝试使用共享内存实现矩阵乘法的优化。 Author: 潘薇鸿PostDate: 2018.11.25","categories":[],"tags":[{"name":"Tech","slug":"Tech","permalink":"https://zjusct.github.io/tags/Tech/"},{"name":"CUDA","slug":"CUDA","permalink":"https://zjusct.github.io/tags/CUDA/"}],"author":"潘薇鸿"},{"title":"浙江大学超算队博客...活了?","slug":"first","date":"2018-09-30T13:25:55.000Z","updated":"2021-02-11T05:46:05.729Z","comments":true,"path":"2018/09/30/first/","link":"","permalink":"https://zjusct.github.io/2018/09/30/first/","excerpt":"写在前面这里是浙江大学超算队的官方博客","text":"写在前面这里是浙江大学超算队的官方博客 记录一些前沿的Tech姿势、大家平时的DeBug经历、还有ASC世界超算大赛的经验分享! 希望这个博客不仅仅是给以后的小盆友们提供宝贵的经验 更是诸君技术的记录与提升的大好机会 平时我们写着代码 总是不愿意写文档 遇到了非常Tricky又神奇的Bug 花了一下午终于解决了 却没有记录下来 很有可能很多的无谓的时间就会在以后被重复 这样的时间浪费 我们完全可以节省下来! 希望大家能享受在超算队一起学习的时光~ 从现在开始 把这段美好的时间变成字符 记录下来叭~~ Author: TTfishPostDate: 2018.9.30","categories":[],"tags":[{"name":"Tech","slug":"Tech","permalink":"https://zjusct.github.io/tags/Tech/"},{"name":"Spc","slug":"Spc","permalink":"https://zjusct.github.io/tags/Spc/"},{"name":"ZJU","slug":"ZJU","permalink":"https://zjusct.github.io/tags/ZJU/"}],"author":"TTfish"}]}