3.4 残差网络与现代卷积网络

1.网络越深越好的美梦,撞上了墙

说起来,按常理推断,网络层数越多,能做的特征变换就越多,模型也该越聪明才对。就好比一支交响乐团,原本只有几把弦乐,慢慢加入了木管、铜管、打击乐,音色一层层丰富起来,整支曲子的表现力也就跟着上去了。而且从直觉上看,深层网络还有另一个好处,它可以一层一层地把特征从底往高摞起来,浅层先抠出边缘、纹理这种小细节,深层再把它们拼成眼睛、轮子这种零件,最后再拼出一整辆车或者一整张脸。这套从粗到细的层级结构,听起来就格外动人。

可真把网络摞到几十上百层之后,一件让人意外的事发生了,训练误差不降反升,网络反倒比那些没它深的还要笨。这就很反直觉了,明明给它添了更多的层、更多的本事,它怎么反而退步了呢?

病根出在梯度传播上。还记得反向传播吧,梯度要从最后的损失那一头出发,一层一层往回乘,才能传到靠近输入的那些浅层。这个过程有点像一栋高层宿舍楼里传话,六楼有个消息要传到一楼,每层楼都得有一个人接力,消息每往下传一层就可能变味一点。层数一多,这一路上要乘的次数太多,传到浅层的时候,梯度要么缩得几乎没影了(这叫梯度消失,浅层根本收不到学习信号,直接瘫痪下来),要么膨胀得到处乱蹦(这叫梯度爆炸,参数更新彻底失控)。结果就是,深层网络反而比浅层更难训练,深度这个本该是优势的东西,反倒成了累赘。

那个年代大家试了好多办法想要救场,比如换更好的激活函数、小心翼翼地初始化参数,能稍微缓一缓,可层数一旦堆到三四十层往上,训练还是稳不住。怎么才能让深层网络真正训起来,这就成了当年最让人头疼的难题之一,直到ResNet横空出世。

2.残差学习:在原稿上动笔改

2015年,微软的一群研究员给出了一个特别巧妙的解法,做出了残差网络(ResNet)。这个解法的思路说起来简单得很。普通网络让每一层去学完整的输出表示,相当于每一层都从头写一篇新文章,特别费劲。ResNet换了个思路,让每一层只去学输入到输出之间需要修正的那点变化量,相当于不重写,只在原稿上动笔改几个字。

说穿了,这就好比我之前看过的一本讲编辑工作的书里提到的道理,老编辑改稿子从来不推倒重来,他拿着红笔在原作者的稿子上勾勾画画,哪里多一个字、哪里少一句,只在关键处动刀,这么改又快又准。再换个行当来说,小张在医院放射科写影像报告,遇到一份片子要复核,把整份报告推翻重写那得花一下午,只在存疑的地方补几笔备注就轻松多了,残差网络干的就是后面这种活儿。

设某一层的输入是 xxxx 就是这一层进来的特征图),层里那一段变换记作 F(x)F(x)F(x)F(x) 就是这一层里卷积、激活那些操作干完之后的结果),那么这一层的输出 yy 写成:

y=F(x)+xy=F(x)+x

这里的 xx 通过一条叫跳跃连接(也叫捷径)的旁路,直接加到输出上,绕过了 F(x)F(x) 那一整段变换。F(x)F(x) 才是这一层真正要学的东西,它就是所谓的残差,也就是当前输出和输入之间的那个差。

这个结构巧妙在哪里呢,如果某一层最理想的做法其实是原样把信息传过去(输入什么就输出什么,一字不差),那网络只需要把 F(x)F(x) 学到接近零就行了,这可比从头去学一个一模一样的变换轻松太多了。

更关键的是,因为有了 xx 这条直通的快车道,梯度也能顺着它一路顺畅地回到浅层,不用非得挤过 F(x)F(x) 那一段变换。每加一个跳跃连接,就等于给梯度多修了一条高速公路,浅层终于又能稳定地收到学习信号了,深层网络一下变得能训练起来。这让我想起日本有位很有名的马拉松运动员叫大迫杰,他训练时常提到的一个意思是,沿途补给站设得勤一些,后半程才跑得稳,残差连接对深层网络来说,大概就有点这个味道。

就这么一个加法,直接把网络的深度从二十几层推到了上百层甚至上千层,当年比赛里最深的版本一口气摞到了152层。ResNet一出场就震动了整个领域,在ImageNet比赛里成绩一骑绝尘,把一众老模型远远甩在身后,这一仗也让微软的研究名声大振,可谓四两拨千斤。

3.归一化:给数值的脾气把把关

光有残差连接还不够稳,深层网络还得面对一个数值尺度的问题。某一层的输出要是太大,后面那层可能直接被推进激活函数的饱和区(在那片区域里输出几乎不再变化,梯度也几乎为零,等于这层直接歇了下来),输出要是太小,信号又会微弱得传不下去,跟上课坐最后一排听不清老师讲话是一个道理。一层的尺度没管好,后面层层都会跟着遭殃。

归一化方法就是来管这件事的。它会调整中间结果的数值范围,让每一层收到的输入都保持在一个合适的分布上,训练因此稳当很多。不妨把它理解成流水线上的一道质检工序,每隔一段就把数值的脾气校准一下,别让它跑偏。也可以想成专业摄影师调相机的感光度,太高了过曝一片白,太低了又欠曝一片黑,归一化就是帮网络把感光度锁在一个舒服的区间里。

最出名的归一化操作叫批归一化(Batch Normalization),它在训练时会拿一小批样本的数值做一次校准,先把它们拉到均值零、方差一附近,再让网络自己决定最终要缩放多少、平移多少。这一手下来,学习率能开得大一点,训练也跑得更快,深层网络直接吃到了红利。

残差连接加上归一化,这两位搭档一配合,深层网络的训练才算真正稳住。后来大家搞出来的各种深层模型,基本都离不开这两样东西。

4.结构设计是一门取舍的手艺

设计一个现代卷积网络,要在好多地方反复做选择:卷积核开多大、一层用多少个通道、在哪里缩小尺寸、算力预算给多少。这些选择交织在一起,共同决定了网络三个方面的表现,分别是表示能力(能学到多复杂的东西)、训练稳定性(好不好训)和推理速度(用起来快不快)。

这三个方面经常互相打架。想让它更聪明,就得加层加通道,可那样又慢又难训,想让它更快,就得精简,可精简过头又可能变笨。这就有点像小张配电脑,预算就那么多,显卡买好了CPU可能就得省,内存拉满了固态又得缩水,总得有个取舍。所以工程师干的活儿,就是在三者之间反复找平衡,给定的任务和数据不同,最合适的那个点也不同。

那么在我看来,整个结构的核心目标就两个,让信息顺畅地流动、让梯度稳定地传播。ResNet教会我们的,是怎么给信息修好路,让数据在里面跑得不堵车、不迷路。堆层数这件事,只是达成这个目标的一个手段罢了。理解了这一点,再看后来那些网络结构,不管是Inception还是MobileNet还是EfficientNet,本质上都是在琢磨同一件事,怎么把这条路修得更顺、更省油。

练习

Q1. 网络摞得越深,为什么训练误差反而可能不降反升?病根出在哪?

病根在梯度传播。梯度要从损失出发一层层往回乘,层数一多,要么梯度缩到几乎没影(梯度消失,浅层收不到学习信号),要么膨胀到处乱蹦(梯度爆炸,参数更新失控)。结果是深层网络反而比浅层更难训练,深度成了累赘。换更好的激活函数、小心初始化能缓一缓,但摞到三四十层以上还是稳不住。

Q2. 残差块为什么写成 y=F(x)+xy=F(x)+x?跳跃连接解决了什么问题?

ResNet 让每一层只学输入到输出之间需要修正的残差 F(x)F(x),而不是从头学完整输出。如果某层最理想是把信息原样传过去,网络只需把 F(x)F(x) 学到接近零,比从头学一个恒等变换轻松太多。更关键的是 xx 这条捷径绕过了 F(x)F(x),给梯度也修了一条直通的高速公路,浅层又能稳定收到学习信号,深层网络一下变得能训练,深度从二十几层推到上百甚至上千层。

Q3. 批归一化(Batch Normalization)在做什么?它和残差连接为什么是搭档?

批归一化拿一小批样本把中间结果先拉到均值零、方差一附近,再让网络自己决定缩放和平移多少,给数值尺度把关,避免输出太大推进激活函数饱和区(梯度归零)或太小信号传不动。它让学习率能开大点、训练更快更稳。残差连接负责让信息和梯度顺畅流动,批归一化负责让每层数值分布稳当,两位搭档一配合,深层网络的训练才真正稳住,后来各种深层模型基本都离不开它们。

相关标签
深度学习残差网络卷积神经网络