-
Notifications
You must be signed in to change notification settings - Fork 340
/
chap6.tex
1605 lines (1407 loc) · 117 KB
/
chap6.tex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
% file: chap6.tex
\chapter{深度学习}
\label{ch:Deeplearning}
在\hyperref[ch:WhyHardToTrain]{上一章},我们学习了深度神经网络通常比浅层神经网络
更加难以训练。我们有理由相信,若是可以训练深度网络,则能够获得比浅层网络更加强大
的能力,但是现实很残酷。从上一章我们可以看到很多不利的消息,但是这些困难不能阻止
我们使用深度神经网络。本章,我们将给出可以用来训练深度神经网络的技术,并在实战中
应用它们。同样我们也会从更加广阔的视角来看神经网络,简要地回顾近期有关深度神经网
络在图像识别、语音识别和其他应用中的研究进展。然后,还会给出一些关于未来神经网络
又或人工智能的简短的推测性的看法。
这一章比较长。为了更好地让你们学习,我们先粗看一下整体安排。本章的小节之间关联并
不太紧密,所以如果读者熟悉基本的神经网络的知识,那么可以任意跳到自己最感兴趣的部
分。
\hyperref[sec:convolutional_networks]{本章主要的部分}是对最为流行神经网络之一的
深度卷积网络的介绍。我们将细致地分析一个使用卷积网络来解决 MNIST 数据集的手写数
字识别的例子(包含了代码和讲解):
\begin{center}
\includegraphics[width=64pt]{digits}
\end{center}
我们将从浅层的神经网络开始来解决上面的问题。通过多次的迭代,我们会构建越来越强大
的网络。在这个过程中,也将要探究若干强大技术:卷积、\gls*{pooling}、使用 GPU 来
更好地训练、训练数据的算法性扩展(避免\gls*{overfitting})、\gls*{dropout}技术的
使用(同样为了防止\gls*{overfitting}现象)、网络的综合使用和其他技术。最终的结果
能够接近人类的表现。在 10,000 幅 MNIST 测试图像上~——~模型从未在训练中接触的图
像~——~该系统最终能够将其中 9,967 幅正确分类。这儿我们看看错分的 33 幅图像。注意
正确分类是右上的标记;系统产生的分类在右下:
\begin{center}
\includegraphics[width=.75\textwidth]{ensemble_errors}
\end{center}
可以发现,这里面的图像即使对于人类来说都是非常困难区分的。例如,在第一行的第三幅
图。我看的话,看起来更像是 “9” 而非 “8”,而 “8” 却是给出的真实的结果。我们
的网络同样能够确定这个是 “9”。这种类型的“错误”最起码是容易理解的,可能甚至值
得我们赞许。最后用对最近使用深度(卷积)神经网络在图像识别上的研究进展作为关于图
像识别的讨论的总结。
本章剩下的部分,我们将会从一个更加宽泛和宏观的角度来讨论深度学习。概述一些神经网
络的其他模型,例如\gls*{rnn}和\gls*{lstm},以及这些网络如何在语音识别、自然语言
处理和其他领域中应用的。最后会试着推测一下,神经网络和深度学习未来发展的方向,会
从\gls*{idui}谈谈深度学习在人工智能的角色。这章内容建立在本书前面章节的基础之上,
使用了前面介绍的诸如\gls*{bp}、\gls*{regularization}、\gls*{softmax-func},等等。
然而,要想阅读这一章,倒是不需要太过细致地掌握前面章节中内容的所有的细节。当然读
完第一章关于神经网络的基础是非常有帮助的。本章提到第二章到第五章的概念时,也会在
文中给出链接供读者去查看这些必需的概念。
需要注意的一点是,本章所没有包含的那一部分。这一章并不是关于最新和最强大的神经网
络库。我们也不是想训练数十层的神经网络来处理最前沿的问题。而是希望能够让读者理解
深度神经网络背后核心的原理,并将这些原理用在一个 MNIST 问题的解决中,方便我们的
理解。换句话说,本章目标不是将最前沿的神经网络展示给你看。包括前面的章节,我们都
是聚焦在基础上,这样读者就能够做好充分的准备来掌握众多的不断涌现的深度学习领域最
新工作。
\section{介绍卷积网络}
\label{sec:convolutional_networks}
在前面的章节中,我们教会了神经网络能够较好地识别手写数字:
\begin{center}
\includegraphics[width=64pt]{digits}
\end{center}
我们使用了全连接的邻接关系的网络来完成这个工作。即,网络中的神经元与相邻的层上的
每个神经元均连接:
\begin{center}
\includegraphics{tikz41}
\end{center}
特别地,对输入图像中的每个像素点,我们将其光强度作为对应输入层神经元的值。对于
$28 \times 28$ 像素的图像,这意味着我们的网络有 784($= 28 \times 28$)个输入神
经元。我们然后训练网络的\gls*{weight}和\gls*{bias},使得网络输出能够~——~如我们希望地~——~正确地辨
认输入图像:'0', '1', '2', $\ldots$, '8', 或 '9'。
我们之前的网络工作得相当好:我们已经\hyperref[98percent]{得到了超过 98\% 的分类
准确率},使用来自 \hyperref[sec:learning_with_gradient_descent]{MNIST 手写数字
数据集}的训练和测试数据。但是仔细推敲,使用全连接层的网络来分类图像是很奇怪的。
原因是这样的一个网络架构不考虑图像的空间结构。例如,它在完全相同的基础上去对待相
距很远和彼此接近的输入像素。这样的空间结构的概念必须从训练数据中推断。但是如果我
们使用一个设法利用空间结构的架构,而不是从一个\textbf{白板状态}的网络架构开始,
会怎样?在这一节中,我会描述\textbf{\gls{cnn}}\footnote{最初的\gls*{cnn}要追溯到
1970 年代。但是建立起现代卷积网络学科的开创性论文是一篇1998 年的
“\href{http://yann.lecun.com/exdb/publis/pdf/lecun-98.pdf}{Gradient-based
learning applied to document recognition}”,作者为 Yann LeCun, Léon
Bottou, Yoshua Bengio, 和 Patrick Haffner。LeCun 自那以后做了一个关于卷积网
络术语的有趣评论:“以类似卷积网络模型的[生物学上的]神经系统原型是非常牵强的。
这正是为什么我把它们称为‘卷积网络’而不是‘\gls*{cnn}’,以及为什么我们称这些
节点为‘单元’而不是‘神经元’”。尽管有这样的评论,卷积网络使用许多我们目前为
止学习到的神经网络相同的思想:诸如反向传播,梯度下降,\gls*{regularization},
非线性激活函数等等。所以我们会遵循常规惯例,认为它们是神经网络的一种类型。我会
交换地使用术语“\gls*{cnn}”和“卷积网络(行为)”。我也会交换地使用术语“[人
工]神经元”和“单元”。}。这些网络使用一个特别适用于分类图像的特殊架构。使
用这个架构使得卷积网络能更快训练。相应的,这帮助我们训练深度的、多层的网络,它非
常擅长于分类图像。今天,深度卷积网络或者一些近似的变化形式,被用在大多数图像识别
的神经网络中。
\gls*{cnn}采用了三种基本概念:\textbf{\gls{lrf}},\textbf{\gls{shared-weights}},
和\textbf{\gls{pooling}}。让我们逐个看下:\\
\textbf{\gls*{lrf}:} 在之前看到的全连接层的网络中,输入被描绘成纵向排列的神经元。
但在一个卷积网络中,把输入看作是一个 $28 \times 28$ 的方形排列的神经元更有帮助,
其值对应于我们用作输入的 $28 \times 28$ 的像素光强度:
\begin{center}
\includegraphics{tikz42}
\end{center}
和通常一样,我们把输入像素连接到一个隐藏神经元层。但是我们不会把每个输入像素连接
到每个隐藏神经元。相反,我们只是把输入图像进行小的,局部区域的连接。
说的确切一点,第一个\gls*{hidden-layer}中的每个神经元会连接到一个输入神经元的一
个小区域,例如,一个 $5 \times 5$ 的区域,对应于 25 个输入像素。所以对于一个特定
的隐藏神经元,我们可能有看起来像这样的连接:
\begin{center}
\includegraphics{tikz43}
\end{center}
这个输入图像的区域被称为隐藏神经元的\textbf{\gls*{lrf}}。它是输入像素上的一个小
窗口。每个连接学习一个\gls*{weight}。而隐藏神经元同时也学习一个总的\gls*{bias}。你可以把这个特定
的隐藏神经元看作是在学习分析它的\gls*{lrf}。
我们然后在整个输入图像上交叉移动\gls*{lrf}。对于每个\gls*{lrf},在第一个\gls*{hidden-layer}中
有一个不同的隐藏神经元。为了正确说明,让我们从左上角开始一个\gls*{lrf}:
\begin{center}
\includegraphics{tikz44}
\end{center}
然后我们往右一个像素(即一个神经元)移动\gls*{lrf},连接到第二个隐藏神经元:
\begin{center}
\includegraphics{tikz45}
\end{center}
如此重复,构建起第一个\gls*{hidden-layer}。注意如果我们有一个 $28 \times 28$ 的输入图像,$5
\times 5$ 的\gls*{lrf},那么\gls*{hidden-layer}中就会有 $24 \times 24$ 个神经元。这是因为在抵
达右边(或者底部)的输入图像之前,我们只能把\gls*{lrf}横向移动 23 个神经元(或
者往下 23 个神经元)。
我显示的\gls*{lrf}每次移动一个像素。实际上,有时候会使用不同的\textbf{跨距}。例
如,我可以往右(或下)移动 $2$ 个像素的\gls*{lrf},这种情况下我们使用了 $2$ 个跨
距。在这章里我们大部分时候会固定使用 $1$ 的跨距,但是值得知道人们有时用不同的跨
距试验\footnote{正如在前面章节中做过的,如果我们对尝试不同跨距感兴趣,那么我们可
以使用验证数据来挑选可以提供最佳性能的跨距。详细内容参见前面关于如何选择神经网
络的超参数的%
\hyperref[sec:how_to_choose_a_neural_network's_hyper-parameters]{讨论}。相同的
方法也可以用来选择\gls*{lrf}的大小~——~当然,对于使用一个 $5 \times 5$ 的%
\gls*{lrf}没有什么特别的。通常,当输入图像显著大于 $28 \times 28$ 像素的 MNIST
图像时,更大的\gls*{lrf}往往是有益的。}。\\
\textbf{\gls*{shared-weights}和\gls*{bias}:} 我已经说过每个隐藏神经元具有一个\gls*{bias}和连
接到它的\gls*{lrf}的 $5 \times 5$ \gls*{weight}。我没有提及的是我们打算对 $24 \times 24$
隐藏神经元中的每一个使用\textbf{相同的}\gls*{weight}和\gls*{bias}。换句话说,对第 $j, k$ 个隐藏
神经元,输出为:
\begin{equation}
\sigma\left(b + \sum_{l=0}^4 \sum_{m=0}^4 w_{l,m} a_{j+l, k+m} \right)
\label{eq:125}\tag{125}
\end{equation}
这里 $\sigma$ 是神经元的激活函数~——~可以是我们在前面章里使用过的
\hyperref[sec:sigmoid_neurons]{\gls*{sigmoid-func}}。$b$ 是\gls*{bias}的共享值。
$w_{l,m}$ 是一个共享\gls*{weight}的 $5 \times 5$ 数组。最后,我们使用 $a_{x, y}$ 来表示
位置为 $x, y$ 的输入激活值。
这意味着第一个\gls*{hidden-layer}的所有神经元检测完全相同的特征\footnote{我还没有精确定义特征
的概念。非正式地,把一个隐藏神经元检测的特征看作一种引起神经元激活的输入模式:
例如,它可能是图像的一条边,或者一些其它的形状。},只是在输入图像的不同位置。
要明白为什么是这个道理,把\gls*{weight}和\gls*{bias}设想成隐藏神经元可以挑选的东西,例如,在一个
特定的\gls*{lrf}的垂直边缘。这种能力在图像的其它位置也很可能是有用的。因此,在图
像中应用相同的特征检测器是非常有用的。用稍微更抽象的术语,卷积网络能很好地适应图
像的平移不变性:例如稍稍移动一幅猫的图像,它仍然是一幅猫的图像\footnote{实际上,
对于我们已经在学习的 MNIST 数字分类问题,这些图像被置于中心,而且大小被归一化。
所以 MNIST 比“在自然环境中”找到的图像有更少的平移不变性。很多输入空间中的诸
如边缘和角点的特征仍然可能是有用的。}。
因为这个原因,我们有时候把从输入层到\gls*{hidden-layer}的映射称为一个\textbf{特征映射}。我们
把定义特征映射的\gls*{weight}称为\textbf{\gls*{shared-weights}}。我们把以这种方式定义特征
映射的\gls*{bias}称为\textbf{共享\gls*{bias}}。\gls*{shared-weights}和\gls*{bias}经常被称为一个%
\textbf{卷积核}或者\textbf{滤波器}。在文献中,人们有时以稍微不同的方式使用这些术
语,对此我不打算去严格区分;稍后我们会看一些具体的例子。
目前我描述的网络结构只能检测一种局部特征的类型。为了完成图像识别我们需要超过一个
的特征映射。所以一个完整的卷积层由几个不同的特征映射组成:
\begin{center}
\includegraphics{tikz46}
\end{center}
在这个例子中,有 3 个特征映射。每个特征映射定义为一个 $5 \times 5$
\gls*{shared-weights}和单个共享\gls*{bias}的集合。其结果是网络能够检测 3 种不同的特征,
每个特征都在整个图像中可检测。
为了让上面的图示简单些,我仅仅展示了 3 个特征映射。然而,在实践中卷积网络可能使
用很多(也许多得多)的特征映射。一种早期的识别 MNIST 数字的卷积网络,LeNet-5,使
用 6 个特征映射,每个关联到一个 $5 \times 5$ 的\gls*{lrf}。所以上面的插图例子实
际和 LeNet-5 很接近。而在我们在本章后面要开发的例子里,我们将使用具有 20 和40 个
特征映射的卷积层。让我们快速看下已经学到的一些特征\footnote{图示说明的特征映射来
自我们最后的训练卷积网络,参见\hyperref[final_conv]{这里}。}:
\begin{center}
\includegraphics[width=.65\textwidth]{net_full_layer_0}
\end{center}
这 20 幅图像对应于 20 个不同的特征映射(或滤波器、核)。每个映射有一幅 $5 \times
5$ 块的图像表示,对应于\gls*{lrf}中的 $5 \times 5$ \gls*{weight}。白色块意味着一个小(典
型的,更小的负数)\gls*{weight},所以这样的特征映射对相应的输入像素有更小的响应。更暗的
块意味着一个更大的\gls*{weight},所以这样的特征映射对相应的输入像素有更大的响应。非常粗略
地讲,上面的图像显示了卷积层作出响应的特征类型。
所以我们能从这些特征映射中得到什么结论?很明显这里有超出了我们期望的空间结构:这
些特征许多有清晰的亮和暗的子区域。这表示我们的网络实际上正在学习和空间结构相关的
东西。然而,除了那个,看清这些特征检测器在学什么是很困难的。当然,我们并不是在学
习(例如)\href{http://en.wikipedia.org/wiki/Gabor_filter}{Gabor 滤波器},它已经
被用在很多传统的图像识别方法中。实际上,现在有许多关于通过卷积网络来更好理解特征
的工作成果。如果你感兴趣,我建议从 Matthew Zeiler 和 Rob Fergus 的(2013)论文
\href{http://arxiv.org/abs/1311.2901}{Visualizing and Understanding
Convolutional Networks} 开始。
\gls*{shared-weights}和\gls*{bias}的一个很大的优点是,它大大减少了参与的卷积网络的参数。
对于每个特征映射我们需要 $25 = 5 \times 5$ 个\gls*{shared-weights},加上一个共享
\gls*{bias}。所以每个特征映射需要 $26$ 个参数。如果我们有 $20$ 个特征映射,那么总共有
$20 \times 26 = 520$ 个参数来定义卷积层。作为对比,假设我们有一个全连接的第一层,
具有 $784 = 28 \times 28$ 个输入神经元,和一个相对适中的 $30$ 个隐藏神经元,正如
我们在本书之前的很多例子中使用的。总共有 $784 \times 30$ 个\gls*{weight},加上额外的 $30$
个\gls*{bias},共有 $23,550$个参数。换句话说,这个全连接的层有多达 $40$ 倍于卷积层的参
数。
当然,我们不能真正做一个参数数量之间的直接比较,因为这两个模型的本质是不同的径。
但是,直观地,使用卷积层的平移不变性似乎很可能减少全连接模型中达到同样性能的参数
数量。反过来,这将导致更快的卷积模型的训练,并最终,将有助于我们使用卷积层建立深
度网络。
顺便提一下,\textbf{卷积}(\textit{convolutional})这一名称源自方程~\eqref{eq:125}
中的操作符有时被称为一个\textbf{卷积}(\textit{convolution})。稍微更精确些,人
们有时把这个方程写成 $a^1 = \sigma(b + w * a^0)$,其中 $a^1$ 表示出自一个特征映
射的输出激活值集合,$a^0$ 是输入激活值的集合,而 $*$ 被称为一个卷积操作。我们不
打算深入使用卷积数学,所以你不用对此太担心。但是至少值得知道这个名称从何而来。\\
\textbf{\gls*{pooling}层:} 除了刚刚描述的卷积层,\gls*{cnn}也包含%
\textbf{\gls*{pooling}层}(\textit{pooling layers})。\gls*{pooling}层通常紧接着
在卷积层之后使用。它要做的是简化从卷积层输出的信息。
详细地说,一个\gls*{pooling}层取得从卷积层输出的每一个特征映射\footnote{这里使用
的术语是不严格的。特别地,我用“特征映射”表示的意思不是通过卷积层计算出的函数,
而是隐藏神经元从该层输出的激活值。这种术语的轻度滥用在研究文献中是相当普遍的。}并
且从它们准备一个凝缩的特征映射。例如,\gls*{pooling}层的每个单元可能概括了前一层
的一个(比如)$2 \times 2$ 的区域。作为一个具体的例子,一个常见的\gls*{pooling}
的程序被称为\textbf{最大值混合}(\textit{max-pooling})。在最大值\gls*{pooling}
中,一个\gls*{pooling}单元简单地输出其 $2 \times 2$ 输入区域的最大激活值,正如下
图说明的:
\begin{center}
\includegraphics{tikz47}
\end{center}
注意既然从卷积层有 $24 \times 24$ 个神经元输出,\gls*{pooling}后我们得到 $12
\times 12$ 个神经元。
正如上面提到的,卷积层通常包含超过一个特征映射。我们将最大值\gls*{pooling}分别应
用于每一个特征映射。所以如果有三个特征映射,组合在一起的卷积层和最大值%
\gls*{pooling}层看起来像这样:
\begin{center}
\includegraphics{tikz48}
\end{center}
我们可以把最大值\gls*{pooling}看作一种网络询问是否有一个给定的特征在一个图像区域
中的哪个地方被发现的方式。然后它扔掉确切的位置信息。直观上,一旦一个特征被发现,
它的确切位置并不如它相对于其它特征的大概位置重要。一个很大的好处是,这样可以有很
多被更少地\gls*{pooling}的特征,所以这有助于减少在以后的层所需的参数的数目。
最大值\gls*{pooling}并不是用于\gls*{pooling}的仅有的技术。另一个常用的方法是
\textbf{L2 \gls*{pooling}}。这里我们取 $2 \times 2$ 区域中激活值的平方和的平方根,
而不是最大激活值。虽然细节不同,但其直观上和最大值\gls*{pooling}是相似的:L2
\gls*{pooling}是一种凝缩从卷积层输出的信息的方式。在实践中,两种技术都被广泛应用。
而且有时候人们使用其它\gls*{pooling}操作的类型。如果你正在尝试优化性能,你可以使
用验证数据来比较\gls*{pooling}的不同方法,并选择一个工作得最好的。但我们不打算惦
记这种细节上的优化。\\
\textbf{综合在一起:} 我们现在可以把这些思想都放在一起来构建一个完整的卷积神经网
络。它和我们刚看到的架构相似,但是有额外的一层 $10$ 个输出神经元,对应于 $10$ 个
可能的 MNIST 数字('0','1','2'等):
\begin{center}
\includegraphics{tikz49}
\end{center}
这个网络从 $28 \times 28$ 个输入神经元开始,这些神经元用于对 MNIST 图像的像素强
度进行编码。接着的是一个卷积层,使用一个 $5 \times 5$ \gls*{lrf}和 $3$ 个特征映
射。其结果是一个 $3 \times 24 \times 24$ 隐藏特征神经元层。下一步是一个最大值%
\gls*{pooling}层,应用于 $2 \times 2$ 区域,遍及 $3$ 个特征映射。结果是一个 $3
\times 12 \times 12$ 隐藏特征神经元层。
网络中最后连接的层是一个全连接层。更确切地说,这一层将最大值\gls*{pooling}层的%
\textbf{每一个}神经元连接到每一个输出神经元。这个全连接结构和我们之前章节中使用
的相同。然而,注意上面的图示,为了简化,我只使用了一个箭头,而不是显示所有的连接。
当然,你可以很容易想象到这些连接。
这个卷积架构和之前章节中使用的架构相当不同。但是总体的描述是相似的:一个由很多简
单的单元构成的网络,这些单元的行为由它们的\gls*{weight}和\gls*{bias}确定。而总体的目标仍然是一样
的:用训练数据来训练网络的\gls*{weight}和\gls*{bias},使得网络可以胜任分类输入数字。
特别的,正如本书中前面那样,我们将用\gls*{sgd}和\gls*{bp}训练我们的网络。这大部
分按照前面章节中完全相同的方式来处理。然而,我们确实需要对\gls*{bp}程序做些修改。
原因是我们之前的\hyperref[ch:HowThebackpropagationalgorithmworks]{\gls*{bp}的推
导}是针对全连接层的网络。幸运的是,针对卷积和最大值\gls*{pooling}层的推导是简
单的。如果你想理解细节,那么我请你完成下面的问题。注意这个问题会花费些时间来完成,
除非你确实已经吸收了\hyperref[ch:HowThebackpropagationalgorithmworks]{前面的反向
传播的推导}(这种情况下是容易的)。
\subsection*{问题}
\begin{itemize}
\item \textbf{卷积网络中的\gls*{bp}}\quad 在一个具有全连接层的网络中,\gls*{bp}
的核心方程是 \eqref{eq:bp1}--\eqref{eq:bp4}(\hyperref[backpropsummary]{链接})。
假设我们有这样一个网络,它包含有一个卷积层,一个最大值\gls*{pooling}层,和一个
全连接的输出层,正如上面讨论的那样。\gls*{bp}的方程式要如何修改?
\end{itemize}
\section{卷积神经网络在实际中的应用}
\label{seq:convolutional_neural_networks_in_practice}
我们现在已经明白了\gls*{cnn}后面的核心思想。让我们通过实现一些卷积网络,并将它们
应用于 MNIST 数字分类问题,来看看它们如何在实践中工作。我们将使用的程序是
\lstinline!network3.py!,它是前面章节开发的 \lstinline!network.py! 和
\lstinline!network2.py! 的强化版本\footnote{注意 \lstinline!network3.py! 包含了
源自 Theano 库文档中关于\gls*{cnn}(尤其是
\href{http://deeplearning.net/tutorial/lenet.html}{LeNet-5} 的实现),Misha
Denil 的\href{https://github.com/mdenil/dropout}{弃权的实现},以及
\href{http://colah.github.io/}{Chris Olah} 的概念。}。如果你想跟着学,代码可以
从
\href{https://github.com/mnielsen/neural-networks-and-deep-learning/blob/master/src/network3.py}{GitHub}
上下载。注意我们将在下一节中解决 \lstinline!network3.py! 需要的代码。在这一节中,
我们将把 \lstinline!network3.py! 作为库来构建卷积网络。
程序 \lstinline!network.py! 和 \lstinline!network2.py! 是用 Python 和矩阵库Numpy
实现的。这些程序从最初的原理工作,并致力于\gls*{bp}、\gls*{sgd}等细节。但是现在
我们已经理解了这些细节,对于 \lstinline!network3.py! 我们打算使用一个称为
\href{http://deeplearning.net/software/theano/}{Theano} 的机器学习库\footnote{参
见
\href{http://www.iro.umontreal.ca/~lisa/pointeurs/theano_scipy2010.pdf}{Theano:
A CPU and GPU Math Expression Compiler in Python},作者为 James Bergstra,
Olivier Breuleux, Frederic Bastien, Pascal Lamblin, Ravzan Pascanu,
Guillaume Desjardins, Joseph Turian, David Warde-Farley, 和 Yoshua Bengio
(2010)。 Theano 也是流行的
\href{http://deeplearning.net/software/pylearn2/}{Pylearn2} 和
\href{http://keras.io/}{Keras} 神经网络库的基础。其它在本文写作时流行的神经网
路库包括\href{http://caffe.berkeleyvision.org/}{Caffe} 和
\href{http://torch.ch/}{Torch}。}。使用 Theano 使得实现针对\gls*{cnn}的%
\gls*{bp}很容易,因为它自动计算涉及到的映射。Theano 也比我们前面代码更快(那些代
码是为了容易理解,不是为了运行速度),这使它可实际用于训练更复杂的网络。特别地,
Theano 的一个非常好的特性是它能够运行于 CPU 或者,如果可以,GPU 上。运行于 GPU上
可以提供显著的增速,而且,有助于实际用于更复杂的网络。
如果你想要跟着学,你需要可运行在你的系统上的 Theano。按照项目%
\href{http://deeplearning.net/software/theano/}{主页}上的说明来安装 Theano。接下
来的例子使用 Theano 0.6\footnote{当我发布这一章时,Theano 的当前版本变成了 0.7。
我实际上已经在 Theano 0.7 版本中重新运行过这些例子并取得了和文中非常相似的结
果。} 运行过。有些在没有 GPU 支持的 Mac OS X Yosemite 运行过。有些在有 NVIDIA
GPU 支持的 Ubuntu 14.04 中运行过。有些实验在两个系统中都运行过。为了让
\lstinline!networks3.py! 运行,你需要(适当地)把 \lstinline!networks3.py! 源码
中的 \lstinline!GPU! 标志设置为 \lstinline!True! 或者 \lstinline!False!。此外,
为了让 Theano 运行于 GPU 上,你可能会发现%
\href{http://deeplearning.net/software/theano/tutorial/using_gpu.html}{这份指导
说明}有帮助。互联网上也有教程,很容易用 Google 搜索到,同样能帮助你让 Theano
工作。如果你手上的系统没有可用的 GPU,那么你可能想要看下
\href{http://aws.amazon.com/ec2/instance-types/}{Amazon Web Services} EC2 G2实例
类型。注意即使有 GPU 支持,代码仍然需要一些时间执行。许多实验要花费从几分钟到几
个小时的时间来运行。在 CPU 上可能需要花费数天时间来运行最复杂的实验。正如前面章
节里说的,我建议让程序运行着,同时继续阅读,偶尔回来检查下代码的输出。如果你用的
是 CPU,你可能需要对更复杂的实验减少训练\\gls*{epoch}的数量,或者整个忽略它们。
为了取得一个基线,我们将从一个浅层架构开始,它仅仅使用一个\gls*{hidden-layer},包含 $100$ 个
隐藏神经元。我们会训练 60 个\gls*{epoch},使用\gls*{learning-rate}为:$\eta =
0.1$,\gls*{mini-batch} 大小为 $10$,没有\gls*{regularization}。这样运
行\footnote{本节中的实验代码可以在%
\href{https://github.com/mnielsen/neural-networks-and-deep-learning/blob/master/src/conv.py}{%
这个脚本中}找到。注意,脚本中的代码只是简单地重复并相对于本节中的讨论。}:
\begin{lstlisting}[language=Python]
>>> import network3
>>> from network3 import Network
>>> from network3 import ConvPoolLayer, FullyConnectedLayer, SoftmaxLayer
>>> training_data, validation_data, test_data = network3.load_data_shared()
>>> mini_batch_size = 10
>>> net = Network([
FullyConnectedLayer(n_in=784, n_out=100),
SoftmaxLayer(n_in=100, n_out=10)], mini_batch_size)
>>> net.SGD(training_data, 60, mini_batch_size, 0.1,
validation_data, test_data)
\end{lstlisting}
我获得的一个最好的分类准确率是 $97.80$\%。这是 \lstinline!test_data! 上的分类准
确率,在这个取值的训练\gls*{epoch}的地方,我们在\lstinline!validation_data!上得到了
最好的分类准确率。使用验证数据来决定在何时对测试准确率估值有助于避免测试数据上的
\gls*{overfitting}(见前面关于验证数据使用的\hyperref[validation_explanation]{讨
论})。我们将在下面遵循这个习惯。你的结构可能稍有不同,因为网络的\gls*{weight}和\gls*{bias}
是随机初始化的\footnote{实际上,在这个实验中我其实对这个架构的网络运行了三次独立
的训练。然后我从这三次运行中报告了对应于最佳验证准确率的测试准确率。利用多次运
行有助于减少结果中的变动,它在比较许多个架构时是很有用的,正如我们正在做的。除
非明确指出,我在下面已经遵循了这个程序。在实践中,它对于所获得的结果不会带来什
么区别。}。
这个 $97.80$\% 的准确率接近于\hyperref[chap3_98_04_percent]{第三章}中获得的
$98.04$\% 的准确率,使用一个相似的网络架构和学习超参数。特别地,两个例子都使用一
个浅层网络,具有单个包含有 $100$ 个隐藏神经元的\gls*{hidden-layer}。两者都训练 $60$ 个
\gls*{epoch},\gls*{mini-batch}大小为 $10$,\gls*{learning-rate} 为 $\eta = 0.1$。
然而,在之前的网络中有两个不同的地方。首先,我们%
\hyperref[sec:overfitting_and_regularization]{\gls*{regularization}}了之前的网络,
来帮助降低\gls*{overfitting}带来的影响。\gls*{regularization}当前的网络确实可以
提高准确率,但是得到的只是很小,所以我们将推迟到后面再来惦记%
\gls*{regularization}。第二,虽然之前的网络中的最终层使用了S型激活值和交叉熵代价
函数,当前网络使用一个\gls*{softmax}的最终层,以及对数似然代价函数。正如第三章中%
\hyperref[subsec:softmax]{解释}的,这不是一个大的改变。我没有为了任何特别深刻的
原因来做出这样的改变~——~主要是因为\gls*{softmax}和\gls*{log-likelihood}代价在现
代的图像分类网络中很常见。
我们能用一个更深的网络架构来做得比这些结果更好吗?
让我们从在网络开始位置的右边插入一个卷积层开始。我们将使用 $5 \times 5$ 局部感受
野,跨距为 $1$,$20$ 个特征映射。我们也会插入一个最大值\gls*{pooling}层,它用一
个 $2 \times 2$ 的\gls*{pooling}窗口来合并特征。所以总体的网络架构看起来很像上一
节讨论的架构,但是有一个额外的全连接层:
\begin{center}
\includegraphics{simple_conv}
\end{center}
在这个架构中,我们可以把卷积和\gls*{pooling}层看作是在学习输入训练图像中的%
\gls*{lrf},而后面的全连接层则在一个更抽象的层次学习,从整个图像整合全局信息。这
是一种常见的卷积神经网络模式。
让我们训练这样的一个网络,看看它表现怎样\footnote{这里我继续使用一个大小为 $10$
的\gls*{mini-batch}。正如我们\hyperref[mini_batch_size]{前面讨论过的},使用更
大的\gls*{mini-batch}可能提高训练速度。我继续使用相同的\gls*{mini-batch},主要
是为了和前面章节中的实验保持一致。}:
\begin{lstlisting}[language=Python]
>>> net = Network([
ConvPoolLayer(image_shape=(mini_batch_size, 1, 28, 28),
filter_shape=(20, 1, 5, 5),
poolsize=(2, 2)),
FullyConnectedLayer(n_in=20*12*12, n_out=100),
SoftmaxLayer(n_in=100, n_out=10)], mini_batch_size)
>>> net.SGD(training_data, 60, mini_batch_size, 0.1,
validation_data, test_data)
\end{lstlisting}
我们得到了 $98.78$\% 的准确率,这是相当大的改善,超过了我们以前结构的任何一个。
事实上,我们已经减少了超过三分之一的错误率,这是一个很大的进步。
在指定网络结构时,我把卷积和\gls*{pooling}层作为一个单一层对待。不管他们是被视为
分开的层还是作为一个单一的层在一定程度上是一个个人喜好的问题。
\lstinline!network3.py! 视他们为单个层,因为它使得 \lstinline!network3.py! 的代
码更紧凑。然而,如果需要的话,很容易修改 \lstinline!network3.py! 使得这些层可以
单独指定。
\subsection*{练习}
\begin{itemize}
\item 如果你删除了全连接层,只使用卷积--\gls*{pooling}层和\gls*{softmax}层,你得
到了什么样的分类准确率?全连接层的加入有帮助吗?
\end{itemize}
我们能改进 $98.78$\% 的分类准确率吗?
让我们试着插入第二个卷积--\gls*{pooling}层。把它插在已有的卷积--\gls*{pooling}层
和全连接\gls*{hidden-layer}之间。我们再次使用一个 $5 \times 5$ \gls*{lrf},\gls*{pooling} $2
\times 2$ 的区域。让我们看看用前面相似的\gls*{hyper-params}训练会发生什么:
\begin{lstlisting}[language=Python]
>>> net = Network([
ConvPoolLayer(image_shape=(mini_batch_size, 1, 28, 28),
filter_shape=(20, 1, 5, 5),
poolsize=(2, 2)),
ConvPoolLayer(image_shape=(mini_batch_size, 20, 12, 12),
filter_shape=(40, 20, 5, 5),
poolsize=(2, 2)),
FullyConnectedLayer(n_in=40*4*4, n_out=100),
SoftmaxLayer(n_in=100, n_out=10)], mini_batch_size)
>>> net.SGD(training_data, 60, mini_batch_size, 0.1,
validation_data, test_data)
\end{lstlisting}
再一次,我们得到了改善:现在我们达到了 $99.06$\% 的分类准确率。
在这里有两个很自然想到的问题。第一个问题是:应用第二个卷积--\gls*{pooling}层意味
着什么?实际上,你可以认为第二个卷积--\gls*{pooling}层输入 $12 \times 12$ 幅“图
像”,其“像素”代表原始输入图像中特定的局部特征的存在(或不存在)。所以你可以认
为这一层输入原始输入图像的一个版本。这个版本是经过抽象和凝缩过的,但是仍然有大量
的空间结构,所以使用第二个卷积--\gls*{pooling}层是有意义的。
这是一个令然满意的观点,但是引出了第二个问题。从前面层的输出涉及 $20$ 个独立的特
征映射,所以对第二个卷积--\gls*{pooling}层有 $20 \times 20 \times 12$ 个输入。就
好像我们有 $20$ 幅单独的图像输入给卷积--\gls*{pooling}层,而不是第一个卷
积--\gls*{pooling}层情况下的单幅图像。第二个卷积--\gls*{pooling}层里的神经元应该
如何响应这些多重的输入图像呢?实际上,我们将允许这一层中的每个神经元从它的%
\gls*{lrf}中的\textbf{所有} $20 \times 5 \times 5$ 输入神经元学习。更非正式的:
第二个卷积--\gls*{pooling}层中的特征检测器可访问\textbf{所有}前面层的特征,但仅
在其特定的\gls*{lrf}中\footnote{如果输入图像是有颜色的,这个问题会在第一层中出现。
在这种情况下,对于每一个像素我们会有 3 个输入特征,对应于输入图像中的红色、绿
色和蓝色通道。因此我们将允许特征检测器可访问所有颜色信息,但仅仅在一个给定的%
\gls*{lrf}中。}。
\subsection*{问题}
\begin{itemize}
\item \textbf{使用\gls*{tanh}激活函数}\quad 在本书前面我已经几次提起过%
\hyperref[subsec:other_models_of_artificial_neuron]{\gls*{tanh-func}}可以是一
个比 \gls*{sigmoid-func}更好的激活函数。我们还没有实际采用过这些建议,因为我们
已经用 S 型取得了大量进展。但现在让我们试试一些用\gls*{tanh}作为我们激活函数的
实验。试着训练卷积和全连接层中具有\gls*{tanh}激活值的网络\footnote{注意你可以
将 \lstinline!activation_fn=tanh! 作为一个参数传递给
\lstinline!ConvPoolLayer! 和 \lstinline!FullyConnectedLayer! 类。}。开始时使
用 S 型网络中使用的相同的超参数,但是训练 $20$ 个\gls*{epoch},而不是 $60$个。你
的网络表现得怎么样?如果你继续训练到 $60$ 个\gls*{epoch}会怎样?试着将\gls*{tanh}
和 S 型网络的每个\gls*{epoch}的验证准确率都绘制出来,都绘制到 $60$ 个\gls*{epoch}。如
果你的结果和我的相似,你会发现\gls*{tanh}网络训练得稍微快些,但是最终的准确率
非常相似。你能否解释为什么\gls*{tanh}网络可以训练得更快?你能否用 S型取得一个
相似的训练速度,也许通过改变\gls*{learning-rate},或者做些调整\footnote{你也许可以
回想 $\sigma(z) = (1+\tanh(z/2))/2$ 来找灵感。}?试着用五六个迭代学习超参数
和网络架构,寻找\gls*{tanh}优于 S 型的方面。\textbf{注意:这是一个开放式问题。
就我个人而言,我并没有找到太多切换为\gls*{tanh}的优势,虽然我没全面地做过实
验,也许你会找到一个方法。无论如何,我们马上会发现切换到修正线性激活函数的一
个优势,所以我们不会去深入使用\gls*{tanh}函数。}。
\end{itemize}
\textbf{使用\gls*{relu}:} 到现在为止,我们开发的网络实际上是一篇开创性的 1998论
文\footnote{\href{http://yann.lecun.com/exdb/publis/pdf/lecun-98.pdf}{"Gradient-based
learning applied to document recognition"},作者为 Yann LeCun, Léon Bottou,
Yoshua Bengio, 和 Patrick Haffner (1998)。细节上有很多不同,但大体上讲,我
们的网络和论文中描述的网络非常相似。} 中使用的众多网络中一种的变化形式,这个网
络被称为 LeNet-5,并引入了 MNIST 问题。这为进一步实验并构筑理解和直观感受打下很
好的基础。特别是,有很多种我们可以改变网络来改善结果的方式。
作为开始,让我们改变我们的神经元,我们使用%
\hyperref[sec:other_models_of_artificial_neuron]{修正线性单元}而不是 S 型激活函
数。确切地说,我们将使用激活函数 $f(z) \equiv \max(0, z)$。我们将训练 $60$ 个%
\gls*{epoch},\gls*{learning-rate}为$\eta = 0.03$。我也发现用一些
\hyperref[sec:overfitting_and_regularization]{L2 \gls*{regularization}}也有点帮
助,使用\gls*{regularization}参数 $\lambda = 0.1$:
\begin{lstlisting}[language=Python]
>>> from network3 import ReLU
>>> net = Network([
ConvPoolLayer(image_shape=(mini_batch_size, 1, 28, 28),
filter_shape=(20, 1, 5, 5),
poolsize=(2, 2),
activation_fn=ReLU),
ConvPoolLayer(image_shape=(mini_batch_size, 20, 12, 12),
filter_shape=(40, 20, 5, 5),
poolsize=(2, 2),
activation_fn=ReLU),
FullyConnectedLayer(n_in=40*4*4, n_out=100, activation_fn=ReLU),
SoftmaxLayer(n_in=100, n_out=10)], mini_batch_size)
>>> net.SGD(training_data, 60, mini_batch_size, 0.03,
validation_data, test_data, lmbda=0.1)
\end{lstlisting}
我得到一个 99.23\% 的分类准确率。它稍微超过了 S 型的结果(99.06)。然而,在我所有
实验中我发现基于修正线性单元的网络,其性能始终优于基于 S 型激活函数的网络。似乎对
于这个问题切换到修正线性单元确实有收益。
是什么使得修正线性激活函数好于 S 型或者\gls*{tanh}函数?目前,我们对这个问题的答案有一
个很差的理解。实际上,修正线性单元只在过去几年才开始被广泛使用。最近才采用的原因
是以经验为依据的:一些人经常基于直觉或者启发式的理由试着用修正线性单
元\footnote{一个通常的理由是 $\max(0, z)$ 在 $z$ 取最大极限时不会饱和,不像 S 型
神经元,而这有助于修正线性单元持续学习。到目前为止,这一辩解很好,但不是一个详
细的理由,更多的是一个“就这样”的故事。注意我们在\hyperref[saturation]{第二章}里
讨论过饱和的问题。}。他们在分类基准数据集时取得了很好的结果,并且其实践传播开了。
在一个理想的世界中,我们有一个理论告诉我们为什么样的应用选择什么样的激活函数。但
目前我们我们离这样的理想世界还有一条很长的路。如果通过选择一个更好的激活函数来取
得了进一步的重大改进,我一点也不会感到惊讶,我还期待在未来的几十年里,一个强大的
激活函数理论将被开发。今天,
我们仍然不得不依靠单凭经验的不足的理解。\\
\textbf{扩展训练数据:} 另一种我们可能希望改进结果的方法是以算法形式扩展训练数据。
扩展训练数据的一个简单的方法是将每个训练图像由一个像素来代替,无论是上一个像素,
一个像素,左边一个像素,或右边一个像素。我们可以通过在 shell 提示符中运行程
序 \lstinline!expand_mnist.py! 来这样做\footnote{\lstinline!expand_mnist.py! 的代
码可以从%
\href{https://github.com/mnielsen/neural-networks-and-deep-learning/blob/master/src/expand_mnist.py}{%
这里}获取。}:
\input{snippets/run_expand_mnist} % move to separate file to avoid syntax error caused by '$' in EMACS.
运行这个程序取得 $50,000$ 幅 MNIST 训练图像并扩展为具有
$250,000$ 幅训练图像的训练集。然后我们可以使用这些训练图像来训练我们的网络。我们
将使用和上面一样的具有修正线性单元的网络。在我初始的实验中我减少了训练\gls*{epoch}的
数量~——~这讲得通,因为我们在训练 $5$ 倍的数据。但是实际上,扩展数据结果是相当多地
减少了\gls*{overfitting}的影响。所有,在做了一些实验后,我最终回到训练
$60$ 个\gls*{epoch}。不管怎样,让我们训练:
\begin{lstlisting}[language=Python]
>>> expanded_training_data, _, _ = network3.load_data_shared(
"../data/mnist_expanded.pkl.gz")
>>> net = Network([
ConvPoolLayer(image_shape=(mini_batch_size, 1, 28, 28),
filter_shape=(20, 1, 5, 5),
poolsize=(2, 2),
activation_fn=ReLU),
ConvPoolLayer(image_shape=(mini_batch_size, 20, 12, 12),
filter_shape=(40, 20, 5, 5),
poolsize=(2, 2),
activation_fn=ReLU),
FullyConnectedLayer(n_in=40*4*4, n_out=100, activation_fn=ReLU),
SoftmaxLayer(n_in=100, n_out=10)], mini_batch_size)
>>> net.SGD(expanded_training_data, 60, mini_batch_size, 0.03,
validation_data, test_data, lmbda=0.1)
\end{lstlisting}
使用扩展后的训练数据我取得了一个 99.37\% 的训练准确率。所以这个几乎是微不足道的改变在分类准确率上给出了一个显著的改进。事实上,正如我们%
\hyperref[sec:other_techniques_for_regularization]{前面所讨论的},这种以算法形式
扩展数据的想法可以更进一步。提醒你一些早期讨论的结果:
在 2003 年,Simard,Steinkraus 和
Platt\footnote{\href{http://dx.doi.org/10.1109/ICDAR.2003.1227801}{Best
Practices for Convolutional Neural Networks Applied to Visual Document
Analysis},作者为 Patrice Simard, Dave Steinkraus, 和 John
Platt (2003)。} 使用一个神经网络改进了他们的 MNIST 性能,达到了
$99.6$\%,这个网络以其它方式和我们的非常相似,使用两个卷积--\gls*{pooling}层,跟着一个具
有 $100$ 个神经元的隐藏的全连接层。在他们的架构中有一些细节上的不同~——~例如他们没
有利用修正线性单元~——~但是他们改进性能的关键是扩展训练数据。他们通过旋转,位移和
扭曲 MNIST 训练图像来扩展。他们还开发了一个“弹性扭曲”的流程,一种模拟当一个人写
字时手部肌肉随机振动的方式。通过组合所有这些流程,他们相当大地增加了训练数据的有
效规模,而这就是他们如何达到$99.6$\% 准确率的。
\subsection*{问题}
\begin{itemize}
\item 卷积层的想法是以一种横跨图像不变的方式作出反应。它看上去令人惊奇,然而,当
我们做完所有输入数据的转换,网络能学习得更多。你能否解释为什么这实际上很合理?
\end{itemize}
\textbf{插入一个额外的全连接层:} 我们还能做得更好吗?一种可能性是使用和上面完全
相同的程序,但是扩展全连接层的规模。我试过 $300$ 和 $1,000$ 个神经元,分别取得
了 $99.46$\% 和 $99.43$\%。这很有趣,但对于前面的结果(99.37\%)并不是一个令人信
服的超越。
增加一个额外的全连接层怎样?让我们试着插入一个全连接层,这样我们就有两个 $100$ 个
隐藏神经元的全连接层:
\begin{lstlisting}[language=Python]
>>> net = Network([
ConvPoolLayer(image_shape=(mini_batch_size, 1, 28, 28),
filter_shape=(20, 1, 5, 5),
poolsize=(2, 2),
activation_fn=ReLU),
ConvPoolLayer(image_shape=(mini_batch_size, 20, 12, 12),
filter_shape=(40, 20, 5, 5),
poolsize=(2, 2),
activation_fn=ReLU),
FullyConnectedLayer(n_in=40*4*4, n_out=100, activation_fn=ReLU),
FullyConnectedLayer(n_in=100, n_out=100, activation_fn=ReLU),
SoftmaxLayer(n_in=100, n_out=10)], mini_batch_size)
>>> net.SGD(expanded_training_data, 60, mini_batch_size, 0.03,
validation_data, test_data, lmbda=0.1)
\end{lstlisting}
这样做我取得一个 99.43\% 的测试准确率。再一次,扩展后的网络并没有帮助太多。运行
类似的试验,用包含$300$ 和 $1,000$ 个隐藏神经元的全连接层产生 $99.48$\% 和
$99.47$\% 的结果。这是令人鼓舞的,但仍然缺乏一个真正决定性的胜利。
\label{final_conv}
这里发生了什么事?扩展的,或者额外的全连接层真的对 MNIST 没帮助吗?或者说,我们
的网络有能力做得更好,但我们在用错误的方式学习?例如,也许我们可以用更强有力的\gls*{regularization}%
技术来减小\gls*{overfitting}的趋势。一种可能性是第三章介绍的%
\hyperref[sec:other_techniques_for_regularization]{弃权}技术。回想弃权的基本思想
是在训练网络时随机地移除单独的激活值。这使得模型对单独依据的丢失更为强劲,因此不
太可能依赖于训练数据的特质。让我们试着应用弃权到最终的全连接层:
\begin{lstlisting}[language=Python]
>>> net = Network([
ConvPoolLayer(image_shape=(mini_batch_size, 1, 28, 28),
filter_shape=(20, 1, 5, 5),
poolsize=(2, 2),
activation_fn=ReLU),
ConvPoolLayer(image_shape=(mini_batch_size, 20, 12, 12),
filter_shape=(40, 20, 5, 5),
poolsize=(2, 2),
activation_fn=ReLU),
FullyConnectedLayer(
n_in=40*4*4, n_out=1000, activation_fn=ReLU, p_dropout=0.5),
FullyConnectedLayer(
n_in=1000, n_out=1000, activation_fn=ReLU, p_dropout=0.5),
SoftmaxLayer(n_in=1000, n_out=10, p_dropout=0.5)],
mini_batch_size)
>>> net.SGD(expanded_training_data, 40, mini_batch_size, 0.03,
validation_data, test_data)
\end{lstlisting}
使用它,我们取得了 $99.60$\% 的准确率,这是一个显著的超越我们前面结果的进步,尤其
是我们主要的基准,具有 $100$ 个隐藏神经元的网络,其中我们达到了 $99.37$\%。
有两个值得注意的变化。
首先,我减少了训练\gls*{epoch}的数量到 $40$:弃权减少了\gls*{overfitting},所以我们学习得更
快。
其次,全连接\gls*{hidden-layer}有 $1,000$ 个神经元,不是之前使用的 $100$ 个。当然,在训练时弃
权有效地忽略了很多神经元。所以一些扩充是可以预期的。实际上,我试过
用 $300$ 和 $1,000$ 个隐藏神经元的实验,用 $1,000$ 个隐藏神经元(非常略微地)取得
了更好的验
证性能。\\
\textbf{使用一个组合的网络:} 一个简单的进一步提高性能的方法是创建几个神经网络,
然后让它们投票来决定最好的分类。例如,假设我们使用上述的方式训练了 $5$ 个不同的神
经网络,每个达到了接近于 $99.6$\% 的准确率。尽管网络都会有相似的准确率,他们很可
能因为不同的随机初始化产生不同的错误。在这 $5$ 个网络中进行一次投票来取得一个优于
单个网络的分类,似乎是合理的。
这听上去太好了,不像是真的,但是这种组合的方式是神经网络和其它机器学习技术都惯用
的伎俩。而它确实产生了更进一步的改善:我们最终得到了 99.67\% 的准确率。换句话说,
我们的网络组合正确分类了除了 $33$ 个之外所有的 $10,000$ 个测试图像。
剩余的测试集中的错误显示在下面。右上角的标签是按照 NMIST 数据的正确的分类,而右下
角的标签是我们组合网络的输出。
\begin{center}
\includegraphics[width=.75\textwidth]{ensemble_errors}
\end{center}
值得去仔细看看这些。开头两个数字,一个 6 和一个 5,是我们的组合犯的真正的错误。然
而,它们也是可以理解的错误,人类也会犯。那个 6 确实看上去更像一个 0,而那个 5看上
去更像一个 3。第三幅图像,据称是一个 8,在我看来实际上更像一个 9。所以这里我站在
网络组合这边:我认为它比最初画出这些数字的人做得更好。另一方面,第四幅图像,那
个 6,确实看上去是我们网络分类错了。
如此等等。在大多数情况下我们网络的选择看上去至少是合理的,而在一些情况下我们比最
初写这些数字的人做得更好。总体而言,我们的网络提供卓越的性能,特别是当你认为它们
正确分类的 9,967 张图片,这是没有显示。在这种背景下,这里的不清晰的错误似乎是可以
理解的。甚至一个细心的人也会偶尔犯错误。因此我认为只有一个非常细心和有条理的人
才会做得更好。我们的网络正在接近人类的性能。\\
\textbf{为什么我们只对全连接层应用弃权:} 如果你仔细看上面的代码,你会注意到我们
只在网络的全链接部分应用了弃权,而不是卷积层。原则上我们可以在卷积层上应用一个类
似的程序。但是,实际上那没必要:卷积层有相当大的先天的对于\gls*{overfitting}的抵抗。原因是
\gls*{shared-weights}意味着卷积滤波器被强制从整个图像中学习。这使他们不太可能去选择在训练数据
中的局部特质。于是就很少有必要来应用其它\gls*{regularization},例如弃权。\\
\textbf{进一步:} 仍然有可能在 MNIST 上提高性能。Rodrigo Benenson 汇编了一份%
\href{http://rodrigob.github.io/are_we_there_yet/build/classification_datasets_results.html}{%
信息汇总页面},显示这几年的进展,提供了论文的链接。这些论文许多使用深度卷积网络,
与我们已经使用的网络相似。如果你深入挖掘这些论文你会发现许多有趣的技术,并且你可
能乐于实现其中一些。如果你这样做,明智的做法是从一个简单的能被快速训练的网络开始
实现,这将有助于你更快地了解正在发生的事。
我不会概述这份近期成果的大部分内容。但是我忍不住有一个例外。它是一
篇 Cireșan、Meier、 Gambardella、 和 Schmidhuber 所著的 2010 年论
文\footnote{\href{http://arxiv.org/abs/1003.0358}{Deep, Big, Simple Neural Nets
Excel on Handwritten Digit Recognition},作者为 Dan Claudiu Cireșan, Ueli
Meier,Luca Maria Gambardella, 和 Jürgen Schmidhuber (2010)。}。我喜欢这篇
论文的地方是它是如此简单。其中的网络是一个许多层的神经网络,仅使用全连接层(没有
卷积层)。他们最成功的网络有分别包含
有 $2,500$,$2,000$,$1,500$,$1,000$ 和 $500$ 神经元的\gls*{hidden-layer}。他们使用和 Simard
等人类似的想法来扩展他们的训练数据。除了这些,他们没有使用其它的技巧,包括没有卷
积层:这是一个清晰的,简单的网络,这样的网络如果有足够的耐心,可以在 80 年代将其
训练(如果 MNIST 数据集已经有了),假设那时有足够的计算能力。他们达到了一
个99.65\% 的分类准确率,或多或少和我们一样。其关键是使用一个非常大,非常深的网络,
并且使用一块 GPU 来加速训练。这让他们训练了很多个\gls*{epoch}。他们也利用了他们的长
训练时间来逐渐地将\gls*{learning-rate}从 $10^{-3}$ 减小到 $10^{-6}$。试着用一个相似的
架构来匹配他们的结果是个很有趣的联系。\\
\textbf{为什么我们能够训练?} 我们在\hyperref[ch:WhyHardToTrain]{上一章}看到了深
的、多层的神经网络中的基本障碍。特别是,我们看到的梯度往往是相当不稳定的:当我们
从输出层移动到前面层,梯度趋于消失(消失的梯度问题)或爆炸(爆炸的梯度问题)。由
于梯度是我们用来训练的动机,这会导致问题。
我们如何避免这些结果?
当然,答案是我们没有回避这些结果。相反,我们已经做了一些事情,帮助我们继续进行。
特别地:(1)使用卷积层极大地减少了这些层中的参数的数目,使学习的问题更容易;(2)
使用更多强有力的\gls*{regularization}技术(尤其是弃权和卷积层)来减少\gls*{overfitting},否则它在更复杂的
网络中是更多的问题;(3)使用修正线性单元而不是 S 型神经元,来加速训练~——~依据经
验通常是 $3$--$5$ 倍;(4)使用 GPU 并愿意长时间的训练。特别是,在我们最后的实验
中,我们训练了 $40$ 个\gls*{epoch},使用一个 $5$ 倍于未经处理的 MNIST 训练数据的数据
集。在本书前面,我们主要用原始训练数据训练了 $30$ 个\gls*{epoch}。结合因素(3)和
(4),仿佛我们训练了比以前 $30$ 倍长的时间。
你的反应可能是:“就这样?这就是我们为了训练深度网络所要做的全部事情?为什么要小
题大做?”
当然,我们也已经使用了其它主意:利用充分大的数据集(为了避免\gls*{overfitting});使用正确
的代价函数(为了避免\hyperref[sec:the_cross-entropy_cost_function]{学习减速});
使用\hyperref[how_to_choose_a_neural_network's_hyper-parameters]{好的\gls*{weight}初始
化}(也是为了避免因为神经元饱和引起的学习减速);%
\hyperref[sec:other_techniques_for_regularization]{以算法形式扩展训练数据}。我们
在前面章节中讨论了这些和其它想法,并在本章中的大部分已经可以重用这些想法了,而不
需要太多注解。
有了这样说法,这真的是一套相当简单的想法。在组合使用时简单,但功能强大。入门深度
学习变得非常容易!\\
\textbf{这些网络有多深?} 把卷积--\gls*{pooling}层算作一个层,我们最终的架构有 $4$ 个\gls*{hidden-layer}。
这样的一个网络真的应该被称为一个深度网络吗?当然,$4$ 个\gls*{hidden-layer}远远多于我们前面学习的
浅层网络。那些网络大部分只有一个\gls*{hidden-layer},或者偶尔有 $2$ 个\gls*{hidden-layer}。另一方面,2015年
使用最先进技术的深度网络有时候有几十个\gls*{hidden-layer}。我偶尔听到有人采取“更比你更深”的
态度,认为如果你没有跟上在隐层数目方面的攀比,那么你真的没有在做深度学习。我不赞
同这样的态度,部分因为它使得深度学习的定义像是时刻就有结果的事。深度学习中实际的
突破是认识到它超过浅的 $1$、$2$ 层的网络是切实可行的,这样的浅层网络直到 00 年代
中期都占据优势。这确实是一个重大的突破,开启了更多有特殊意义的模型的探索。但除这之
外,层的数目并不是主要的基本利益关系。更确切地说,使用更深层的网络是一种用来帮助
实现其他目标工具~——~例如更好的分类精确率。
\\
\textbf{一些按部就班的话:} 在这一节中,我们从单个\gls*{hidden-layer}的浅层网络顺利转换到多层
卷积网络。这一切似乎很容易!我们做了一个改变,其中大部分,我们得到了改进。如果你
开始尝试,我可以保证事情不会总是那么顺利。原因是,我呈现的是一个清理过的叙述,省
略了许多实验~——~包括许多失败的实验。这个清理过的叙述,希望能帮助你清楚认识基本思
想。但它也有风险,传达着不完整的感觉。取得一个好的,可工作的网络会涉及到大量的试
验和错误,偶尔有挫折。在实践中,你应该预计会处理相当多的实验。为了加快这一进程,
你可能会发现回顾第三章关
于\hyperref[sec:how_to_choose_a_neural_network's_hyper-parameters]{如何选择一个神
经网络的超参数}的讨论会有帮助,或许也看一些那一小节中的进一步阅读的建议。
\section{卷积网络的代码}
\label{sec:the_code_for_our_convolutional_networks}
好了,现在来看看我们的卷积网络代码,\lstinline!network3.py!。整体看来,程序结构类
似于 \lstinline!network2.py!,尽管细节有差异,因为我们使用了 Theano。首先我们来
看 \lstinline!FullyConnectedLayer! 类,这类似于我们之前讨论的那些神经网络层。下面
是代码
\begin{lstlisting}[language=Python]
class FullyConnectedLayer(object):
def __init__(self, n_in, n_out, activation_fn=sigmoid, p_dropout=0.0):
self.n_in = n_in
self.n_out = n_out
self.activation_fn = activation_fn
self.p_dropout = p_dropout
# Initialize weights and biases
self.w = theano.shared(
np.asarray(
np.random.normal(
loc=0.0, scale=np.sqrt(1.0/n_out), size=(n_in, n_out)),
dtype=theano.config.floatX),
name='w', borrow=True)
self.b = theano.shared(
np.asarray(np.random.normal(loc=0.0, scale=1.0, size=(n_out,)),
dtype=theano.config.floatX),
name='b', borrow=True)
self.params = [self.w, self.b]
def set_inpt(self, inpt, inpt_dropout, mini_batch_size):
self.inpt = inpt.reshape((mini_batch_size, self.n_in))
self.output = self.activation_fn(
(1-self.p_dropout)*T.dot(self.inpt, self.w) + self.b)
self.y_out = T.argmax(self.output, axis=1)
self.inpt_dropout = dropout_layer(
inpt_dropout.reshape((mini_batch_size, self.n_in)), self.p_dropout)
self.output_dropout = self.activation_fn(
T.dot(self.inpt_dropout, self.w) + self.b)
def accuracy(self, y):
"Return the accuracy for the mini-batch."
return T.mean(T.eq(y, self.y_out))
\end{lstlisting}
\lstinline!__init__! 方法中的大部分都是可以自解释的,这里再给出一些解释。我们根据
正态分布随机初始化了\gls*{weight}和偏差。代码中对应这个操作的一行看起来可能很吓人,但其实
只在进行载入\gls*{weight}和偏差到 Theano 中所谓的共享变量中。这样可以确保这些变量可
在 GPU中进行处理。对此不做过深的解释。如果感兴趣,可以查
看 \href{http://deeplearning.net/software/theano/index.html}{Theano 的文档}。而这
种初始化的方式也是专门为 sigmoid 激活函数设计的(参
见\hyperref[sec:weight_initialization]{这里})。理想的情况是,我们初始化\gls*{weight}和偏
差时会根据不同的激活函数(如\gls*{tanh}和 Rectified Linear Function)进行调整。这个在
下面的问题中会进行讨论。初始方法 \lstinline!__init__! 以
\lstinline!self.params = [self.W, self.b]! 结束。这样将该层所有需要学习的参数都归在一起。后
面,\lstinline!Network.SGD! 方法会使用 \lstinline!params! 属性来确定网络实例中什
么变量可以学习。
\lstinline!set_inpt! 方法用来设置该层的输入,并计算相应的输出。我使
用 \lstinline!inpt! 而非 \lstinline!input! 因为在python 中 \lstinline!input! 是一
个内置函数。如果将两者混淆,必然会导致不可预测的行为,对出现的问题也难以定位。注
意我们实际上用两种方式设置输入
的:\lstinline!self.input! 和 \lstinline!self.inpt_dropout!。因为训练时我们可能要
使用 dropout。如果使用 dropout,就需要设置对应丢弃的概
率 \lstinline!self.p_dropout!。这就是在 \lstinline!set_inpt! 方法的倒数第二
行 \lstinline!dropout_layer! 做的事。所
以 \lstinline!self.inpt_dropout! 和 \lstinline!self.output_dropout! 在训练过程中
使用,而 \lstinline!self.inpt! 和 \lstinline!self.output! 用作其他任务,比如衡量
验证集和测试集模型的准确度。
\lstinline!ConvPoolLayer! 和 \lstinline!SoftmaxLayer! 类定义和
\lstinline!FullyConnectedLayer! 定义差不多。所以我这儿不会给出代码。如果你感兴趣,
可以参考本节后面的 \lstinline!network3.py! 的代码。
尽管这样,我们还是指出一些重要的微弱的细节差别。明显一点的是,
在 \lstinline!ConvPoolLayer! 和 \lstinline!SoftmaxLayer! 中,我们采用了相应的合适
的计算输出激活值方式。幸运的是,Theano 提供了内置的操作让我们计算卷
积、max-pooling和 softmax 函数。
不大明显的,在我们引入\hyperref[sec:softmax]{softmax layer} 时,我们没有讨论如何
初始化\gls*{weight}和偏差。其他地方我们已经讨论过对 sigmoid 层,我们应当使用合适参数的正态
分布来初始化\gls*{weight}。但是这个启发式的论断是针对 sigmoid 神经元的(做一些调整可以用
于\gls*{tanh}神经元上)。但是,并没有特殊的原因说这个论断可以用在 softmax 层上。所以没
有一个先验的理由应用这样的初始化。与其使用之前的方法初始化,我这里会将所有权值和
偏差设置为 $0$。这是一个 ad hoc 的过程,但在实践使用过程中效果倒是很不错。
好了,我们已经看过了所有关于层的类。那么 Network 类是怎样的呢?让我们看
看 \lstinline!__init__! 方法:
\begin{lstlisting}[language=Python]
class Network(object):
def __init__(self, layers, mini_batch_size):
"""Takes a list of `layers`, describing the network architecture, and
a value for the `mini_batch_size` to be used during training
by stochastic gradient descent.
"""
self.layers = layers
self.mini_batch_size = mini_batch_size
self.params = [param for layer in self.layers for param in layer.params]
self.x = T.matrix("x")
self.y = T.ivector("y")
init_layer = self.layers[0]
init_layer.set_inpt(self.x, self.x, self.mini_batch_size)
for j in xrange(1, len(self.layers)):
prev_layer, layer = self.layers[j-1], self.layers[j]
layer.set_inpt(
prev_layer.output, prev_layer.output_dropout, self.mini_batch_size)
self.output = self.layers[-1].output
self.output_dropout = self.layers[-1].output_dropout
\end{lstlisting}
这段代码大部分是可以自解释的。\lstinline!self.params = [param for layer in ...]!
此行代码对每层的参数捆绑到一个列表中。\lstinline!Network.SGD! 方法会使
用 \lstinline!self.params! 来确定 \lstinline!Network! 中哪些变量需要学习。
而 \lstinline!self.x = T.matrix("x")! 和 \lstinline!self.y = T.ivector("y")! 则定
义了 Theano 符号变量 x 和 y。这些会用来表示输入和网络得到的输出。
这里不是 Theano 的教程,所以不会深度讨论这些变量指代什么东西。但是粗略的想法就是
这些代表了数学变量,而非显式的值。我们可以对这些变量做通常需要的操作:加减乘除,
作用函数等等。实际上,Theano 提供了很多对符号变量进行操作方法,如卷积、最大值\gls*{pooling}
等等。但是最重要的是能够进行快速符号微分运算,使用反向传播算法一种通用的形式。这
对于应用随机梯度下降在若干种网络结构的变体上特别有效。特别低,接下来几行代码定义
了网络的符号输出。我们通过下面这行
\begin{lstlisting}[language=Python]
init_layer.set_inpt(self.x, self.x, self.mini_batch_size)
\end{lstlisting}
设置初始层的输入。
请注意输入是以每次一个 mini-batch 的方式进行的,这就是\gls*{mini-batch}大小为何要指定
的原因。还需要注意的是,我们将输入 \lstinline!self.x! 传了两次:这是因为我们我们
可能会以两种方式(有dropout和无dropout)使用网络。\lstinline!for! 循环将符号变
量 \lstinline!self.x! 通过 \lstinline!Network! 的层进行前向传播。这样我们可以定义
最终的输出 \lstinline!output! 和 \lstinline!output_dropout! 属性,这些都
是 \lstinline!Network! 符号式输出。
现在我们理解了 \lstinline!Network! 是如何初始化了,让我们看看它如何使
用 \lstinline!SGD! 方法进行训练的。代码看起来很长,但是它的结构实际上相当简单。代
码后面也有一些注解。
\begin{lstlisting}[language=Python]
def SGD(self, training_data, epochs, mini_batch_size, eta,
validation_data, test_data, lmbda=0.0):
"""Train the network using mini-batch stochastic gradient descent."""
training_x, training_y = training_data
validation_x, validation_y = validation_data
test_x, test_y = test_data
# compute number of minibatches for training, validation and testing
num_training_batches = size(training_data)/mini_batch_size
num_validation_batches = size(validation_data)/mini_batch_size
num_test_batches = size(test_data)/mini_batch_size
# define the (regularized) cost function, symbolic gradients, and updates
l2_norm_squared = sum([(layer.w**2).sum() for layer in self.layers])
cost = self.layers[-1].cost(self)+\
0.5*lmbda*l2_norm_squared/num_training_batches
grads = T.grad(cost, self.params)
updates = [(param, param-eta*grad)
for param, grad in zip(self.params, grads)]
# define functions to train a mini-batch, and to compute the
# accuracy in validation and test mini-batches.
i = T.lscalar() # mini-batch index
train_mb = theano.function(
[i], cost, updates=updates,
givens={
self.x:
training_x[i*self.mini_batch_size: (i+1)*self.mini_batch_size],
self.y:
training_y[i*self.mini_batch_size: (i+1)*self.mini_batch_size]
})
validate_mb_accuracy = theano.function(
[i], self.layers[-1].accuracy(self.y),
givens={
self.x:
validation_x[i*self.mini_batch_size: (i+1)*self.mini_batch_size],
self.y:
validation_y[i*self.mini_batch_size: (i+1)*self.mini_batch_size]
})
test_mb_accuracy = theano.function(
[i], self.layers[-1].accuracy(self.y),
givens={
self.x:
test_x[i*self.mini_batch_size: (i+1)*self.mini_batch_size],
self.y:
test_y[i*self.mini_batch_size: (i+1)*self.mini_batch_size]
})
self.test_mb_predictions = theano.function(
[i], self.layers[-1].y_out,
givens={
self.x:
test_x[i*self.mini_batch_size: (i+1)*self.mini_batch_size]
})
# Do the actual training
best_validation_accuracy = 0.0
for epoch in xrange(epochs):
for minibatch_index in xrange(num_training_batches):
iteration = num_training_batches*epoch+minibatch_index
if iteration