-
Notifications
You must be signed in to change notification settings - Fork 339
/
chap1.tex
1406 lines (1205 loc) · 91.4 KB
/
chap1.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: chap1.tex
\chapter{使用神经网络识别手写数字}
\label{ch:UsingNeuralNetsToRecognizeHandwrittenDigits}
人类视觉系统是世界上众多奇迹之一。看看下面的手写数字序列:
\begin{center}
\includegraphics[width=64pt]{digits}\label{fig:digits}
\end{center}
大多数人毫不费力就能够认出这些数字为504192。这么容易反而让人觉着迷惑了。在人类的
每个脑半球中,有着一个初级视觉皮层,常称为 {\serif V1},包含1亿4千万个神经元及数
百亿条神经元间的连接。但是人类视觉不是就只有 {\serif V1},还包括整个视觉皮层~——~
{\serif V2}、{\serif V3}、{\serif V4} 和 {\serif V5}~——~他们逐步地进行更加复杂的
图像处理。人类的头脑就是一台超级计算机,通过数十亿年的进化不断地演变,最终能够极
好地适应理解视觉世界的任务。识别手写数字也不是一件简单的事。尽管人类在理解我们眼
睛展示出来的信息上非常擅长,但几乎所有的过程都是无意识地。所以,我们通常并不能体
会自身视觉系统解决问题的困难。
如果你尝试写出计算机程序来识别诸如上面的数字,就会明显感受到视觉模式识别的困难。
看起来人类一下子就能完成的任务变得特别困难。关于我们识别形状~——~ “9顶上有一个圈,
右下方则是一条竖线”这样的简单直觉~——~实际上算法上就很难轻易表达出来了。而在你试
着让这些识别规则越发精准时,就会很快陷入各种混乱的异常或者特殊情形的困境中。看起
来毫无希望。
神经网络以另一种方式看待这个问题。其主要思想是获取大量的手写数字,常称作训练样本,
\begin{center}
\includegraphics[width=.6\textwidth]{mnist_100_digits}
\end{center}
然后开发出一个可以从这些训练样本中进行学习的系统。换言之,神经网络使用样本来自动
推断出识别手写数字的规则。另外,通过增加训练样本的数量,网络可以学到更多关于手写
数字的知识,这样就能够提升自身的准确性。所以,上面例子中我们只是展出了 100 个训
练数字样本,而通过使用数千或者数百万或者数十亿的训练样本我们也许能够得到更好的手
写数字识别器。
本章我们将实现一个可以识别手写数字的神经网络。这个程序仅仅 74 行,不使用特别的神
经网络库。然而,这个短小的网络不需要人类帮助便可以超过 96\% 的准确率识别数字。而
且,在后面的章节,我们会发展出将准确率提升到 99\% 的技术。实际上,最优的商业神经
网络已经足够好到被银行和邮局分别用在账单核查和识别地址上了。
手写识别常常被当成学习神经网络的原型问题,因此我们聚焦在这个问题上。作为一个原型,
它具备一个关键点:挑战性~——~识别手写数字并不轻松~——~但也不会难到需要超级复杂的解
决方法,或者超大规模的计算资源。另外,这其实也是一种发展出诸如深度学习更加高级的
技术的方法。所以,整本书我们都会持续地讨论手写数字识别问题。本书后面部分,我们会
讨论这些想法如何用在其他计算机视觉的问题或者语音、自然语言处理和其他一些领域中。
当然,如果仅仅为了编写一个计算机程序来识别手写数字,本章的内容可以简短很多!但前
进的道路上,我们将扩展出很多关于神经网络的关键的思想,其中包括两个重要的人工神经
元(\gls*{perceptron}和 \gls*{sigmoid-neuron}),以及标准的神经网络学习算法,即%
\gls*{sgd}算法。自始至终,我专注于解释事情的原委,并构筑你对神经网络的直观感受。
这需要一个漫长的讨论,而不是仅仅介绍些基本的技巧,但这对于更深入的理解是值得的。
作为收益,在本章的最后,我们会准备好了解什么是深度学习,以及它为什么很重要。
\section{感知机}
\label{sec:Perceptrons}
什么是神经网络?一开始,我将解释一种被称为\textbf{\gls{perceptron}}的人工神经元。%
\gls*{perceptron}在20世纪五、六十年代由科学家
\href{http://en.wikipedia.org/wiki/Frank_Rosenblatt}{Frank Rosenblatt}
\href{http://books.google.ca/books/about/Principles_of_neurodynamics.html?id=7FhRAAAAMAAJ}{%
发明},其受到 \href{http://en.wikipedia.org/wiki/Warren_McCulloch}{Warren
McCulloch} 和 \href{http://en.wikipedia.org/wiki/Walter_Pitts}{Walter Pitts}
早期\href{http://scholar.google.ca/scholar?cluster=4035975255085082870}{著作}的
影响。今天,使用其它人工神经元模型更为普遍~——~在这本书中,以及更多现代的神经网络
著作中,主要使用的是一种叫做 \textbf{\gls{sigmoid-neuron}}的神经元模型。我们很快
会讲到 \gls*{sigmoid-neuron}。但是要理解为什么 \gls*{sigmoid-neuron}被定义为那样的
方式,值得花点时间先来理解下\gls*{perceptron}。
\gls*{perceptron}是如何工作的呢?一个\gls*{perceptron}接受几个二进制输入,
$x_1,x_2,\ldots$,并产生一个二进制输出:
\begin{center}
\includegraphics{tikz0}
\end{center}
示例中的\gls*{perceptron}有三个输入,$x_1,x_2,x_3$。通常可以有更多或更少输入。
Rosenblatt 提议一个简单的规则来计算输出。他引入\textbf{\gls{weight}},
$w_1,w_2,\ldots$,表示相应输入对于输出重要性的实数。神经元的输出,$0$ 或者 $1$,
则由分配\gls*{weight}后的总和 $\sum_j w_j x_j$ 小于或者大于一些%
\textbf{\gls{threshold}}决定。和\gls*{weight}一样,\gls*{threshold}是一个实数,
一个神经元的参数。用更精确的代数形式:
\begin{equation}
\text{output} = \begin{cases}
0 & \quad \text{if } \sum_j w_j x_j \leq \text{ threshold} \\
1 & \quad \text{if } \sum_j w_j x_j > \text{ threshold} \\
\end{cases}
\tag{1}
\end{equation}
这就是一个\gls*{perceptron}所要做的所有事情!
这是基本的数学模型。你可以将\gls*{perceptron}看作依据\gls*{weight}来作出决定的设
备。让我举个例子。这不是非常真实的例子,但是容易理解,而且很快我们会有根多实际的
例子。假设这个周末就要来了,你听说你所在的城市有个奶酪节。你喜欢奶酪,正试着决定
是否去参加。你也许会通过给三个因素设置\gls*{weight}来作出决定:
\begin{enumerate}
\item 天气好吗?
\item 你的男朋友或者女朋友会不会陪你去?
\item 这个节日举办的地点是否靠近交通站点?(你没有车)
\end{enumerate}
你可以把这三个因素对应地用二进制变量 $x_1,x_2$ 和 $x_3$ 来表示。例如,如果天气好,
我们把 $x_1 = 1$,如果不好,$x_1 = 0$。类似地,如果你的男朋友或女朋友同去,$x_2
= 1$,否则 $x_2 = 0$。$x_3$ 也类似地表示交通情况。
现在,假设你是个嗜好奶酪的吃货,以至于即使你的男朋友或女朋友不感兴趣,也不管路有
多难走都乐意去。但是也许你确实厌恶糟糕的天气,而且如果天气太糟你也没法出门。你可
以使用\gls*{perceptron}来给这种决策建立数学模型。一种方式是给天气\gls*{weight}选
择为 $w_1 = 6$ ,其它条件为 $w_2 = 2$ 和 $w_3 = 2$。$w_1$ 被赋予更大的值,表示天
气对你很重要,比你的男朋友或女朋友陪你,或者最近的交通站重要的多。最后,假设你将%
\gls*{perceptron}的\gls*{threshold}设为 $5$。这样,\gls*{perceptron}实现了期望的
决策模型,只要天气好就输出 $1$,天气不好则为 $0$。对于你的男朋友或女朋友是否想去,
或者附近是否有公共交通站,其输出则没有差别。
随着\gls*{weight}和\gls*{threshold}的变化,你可以得到不同的决策模型。例如,假设
我们把\gls*{threshold}改为 $3$ 。那么\gls*{perceptron}会按照天气好坏,或者结合交
通情况和你男朋友或女朋友同行的意愿,来得出结果。换句话说,它变成了另一个不同的决
策模型。降低\gls*{threshold}则表示你更愿意去。
很明显,\gls*{perceptron}不是人做出决策使用的全部模型。但是这个例子说明了一个%
\gls*{perceptron}如何能权衡不同的依据来决策。这看上去也可以大致解释一个%
\gls*{perceptron}网络能够做出微妙的决定:
\begin{center}
\includegraphics{tikz1}
\end{center}
在这个网络中,第一列\gls*{perceptron}~——~我们称其为第一层\gls*{perceptron}~——~通
过权衡输入依据做出三个非常简单的决定。那第二层的\gls*{perceptron}呢?每一个都在
权衡第一层的决策结果并做出决定。以这种方式,一个第二层中的\gls*{perceptron}可以
比第一层中的做出更复杂和抽象的决策。在第三层中的\gls*{perceptron}甚至能进行更复
杂的决策。以这种方式,一个多层的\gls*{perceptron}网络可以从事复杂巧妙的决策。
顺便提一下,当我定义\gls*{perceptron}时我说的是\gls*{perceptron}只有一个输出。在
上面的网络中\gls*{perceptron}看上去像是有多个输出。实际上,他们仍然是单输出的。
多个\gls*{perceptron}输出箭头仅仅便于说明一个\gls*{perceptron}的输出被用于其它%
\gls*{perceptron}的输入。它和把单个输出线条分叉相比,显得讨巧些。
让我们简化\gls*{perceptron}的数学描述。条件 $\sum_j w_j x_j$ 看上去有些冗长,我
们可以创建两个符号的变动来简化。第一个变动是把 $\sum_j w_j x_j$ 改写成点乘,$w
\cdot x \equiv \sum_j w_j x_j$,这里 $w$ 和 $x$ 对应\gls*{weight}和输入的向量。
第二个变动是把\gls*{threshold}移到不等式的另一边,并用\gls*{perceptron}的%
\textbf{\gls{bias}} $b \equiv -threshold$ 代替。用\gls*{bias}而不是%
\gls*{threshold},那么\gls*{perceptron}的规则可以重写为:
\begin{equation}
\text{output} = \begin{cases}
0 & \quad \text{if } w\cdot x + b \leq 0 \\
1 & \quad \text{if } w\cdot x + b > 0
\end{cases}
\tag{2}
\end{equation}
我们可以把\gls*{bias}看作一种表示让\gls*{perceptron}输出 $1$(或者用生物学的术语,
即激活\gls*{perceptron})有多容易的估算。对于具有一个非常大\gls*{bias}的%
\gls*{perceptron}来说,输出 $1$ 是很容易的。但是如果偏置是一个非常小的负数,输出
$1$ 则很困难。很明显,引入\gls*{bias}只是我们描述\gls*{perceptron}的一个很小的变
动,但是我们后面会看到它引导更进一步的符号简化。因此,在这本书的后续部分,我们不
再用\gls*{threshold},而总是使用\gls*{bias}。
我已经描述过\gls*{perceptron}是一种权衡依据来做出决策的方法。\gls*{perceptron}被
采用的另一种方式,是计算基本的逻辑功能,即我们通常认为的运算基础,例如
“\textbf{与}”,“\textbf{或}”和“\textbf{与非}”。例如,假设我们有个两个输入
的感知器,每个\gls*{weight}为 $-2$,整体的\gls*{bias}为 $3$。这是我们的
\gls*{perceptron}:
\begin{center}
\includegraphics{tikz2}
\end{center}
这样我们得到:输入 $00$ 产生输出 $1$,即 $(-2)*0 + (-2)*0 + 3 = 3$ 是正数。这里
我用 $*$ 符号来显式地表示乘法。但是输入 $11$ 产生输出 $0$,即 $(-2)*1 + (-2)*1 +
3 = -1$ 是负数。如此我们的\gls*{perceptron}实现了一个与非门!
与非门的例子显示了我们可以用\gls*{perceptron}来计算简单的逻辑功能。实际上,我们
完全能用感知器网络来计算任何逻辑功能。原因是与非门是通用运算,那样,我们能在多个
与非门之上构建出任何运算。例如,我们能用与非门构建一个电路,它把两个二进制数
$x_1$ 和 $x_2$相加。这需要计算按位求和,$x_1 \oplus x_2$,同时当 $x_1$ 和 $x_2$
都为 $1$ 时进位设为 $1$,即进位位正好是按位乘积 $x_1x_2$:
\begin{center}
\includegraphics{tikz3}
\end{center}
为了得到相等的\gls*{perceptron}网络,我们把所有与非门替换为\gls*{perceptron},其
具有两个输入、每个\gls*{weight}设为 $-2$,整体\gls*{bias}为 $3$。结果我们得到这
样的网络。注意我已经把右下的与非门移动了一点,只是为了在图上更方便画箭头:
\begin{center}
\includegraphics{tikz4}
\end{center}
这个\gls*{perceptron}网络中有一个部分值得注意,最左边的\gls*{perceptron}的输出被
两次作为底部\gls*{perceptron}的输入。当我定义\gls*{perceptron}模型时,我没有说过
是否允许这种双输出到同一个地方。实际上这不重要。如果我们不想允许这种形式,那可以
简单把两条线合并为到一个\gls*{weight}为 $-4$ 的连接,而不是两个\gls*{weight}为
$-2$ 的连接。(如果你还没明白,应该停下来证明这是相等的。)随着这一改变,原先的
网络看起来像下面描绘的,所有未标记的\gls*{weight}等于 $-2$,所有\gls*{bias}等于
$3$,标记的单个\gls*{weight}为 $-4$:
\begin{center}
\includegraphics{tikz5}
\end{center}
目前为止我把像 $x_1$ 和 $x_2$ 这样的输入画成\gls*{perceptron}网络左边浮动的变量。
实际上,可以画一层额外的\gls*{perceptron}~——~输入层~——~来方便对输入编码:
\begin{center}
\includegraphics{tikz6}
\end{center}
这种对有一个输出但没有输入的\gls*{perceptron}的标记法,
\begin{center}
\includegraphics{tikz7}
\end{center}
是一种标准。它并不实际表示一个\gls*{perceptron}没有输入。为了看清它,假设我们确
实有一个没有输入的\gls*{perceptron}。那么加权和 $\sum_j w_j x_j$ 会总是为零,并
且\gls*{perceptron}在 $b > 0$ 时输出 $1$,当 $b \leq 0$时输出 $0$。那样,%
\gls*{perceptron}会简单输出一个固定值,而不是期望值(上例中的 $x_1$)。倒不如完
全不把输入\gls*{perceptron}看作\gls*{perceptron},而是简单定义为输出期望值的特殊
单元,$x_1, x_2,\ldots$。
通过这个加法器例子,我们演示了一个\gls*{perceptron}⽹络如何用于模拟包含很多与非
门的电路。因为与非门在计算机运算中的通用性,由此可以得出\gls*{perceptron}也同样
适用的结论。
\gls*{perceptron}运算的通用性既是令人鼓舞的,又是令人失望的。令人鼓舞是因为它告
诉我们\gls*{perceptron}网络能和其它计算设备一样强大。但是它也令人失望,因为它看
上去只不过是一种新的与非门。这简直不算个大新闻!
然而,实际情况比这一观点认为的更好。其结果是我们可以设计\textbf{学习算法},能够
自动调整人工神经元的\gls*{weight}和\gls*{bias}。这种调整可以自动响应外部的刺激,
而不需要一个程序员的直接干预。这些学习算法是我们能够以一种根本区别于传统逻辑门的
方式使用人工神经元。有别于显式地设计\textbf{与非}或其它门,我们的神经网络能简单
地学会解决问题,这些问题有时候直接用传统的电路设计是很难解决的。
\section{S 型神经元}
\label{seq:sigmoid_neurons}
学习算法听上去非常棒。但是我们怎样给一个神经网络设计这样的算法呢?假设我们有一个%
\gls*{perceptron}网络,想要用它来解决一些问题。例如,网络的输入可以是一幅手写数
字的扫描图像。我们想要网络能学习\gls*{weight}和\gls*{bias},这样网络的输出能正确
分类这些数字。为了看清学习是怎样工作的,假设我们把网络中的\gls*{weight}(或者%
\gls*{bias})做些微小的改动。就像我们马上会看到的,这一属性会让学习变得可能。
这里简要示意我们想要的(很明显这个网络对于手写识别还是太简单了!):
\begin{center}
\includegraphics{tikz8}
\end{center}
如果对\gls*{weight}(或者\gls*{bias})的微小的改动真的能够仅仅引起输出的微小变化,
那我们可以利用这一事实来修改\gls*{weight}和\gls*{bias},让我们的网络能够表现得像
我们想要的那样。例如,假设网络错误地把一个“9”的图像分类为“8”。我们能够计算出
怎么对\gls*{weight}和\gls*{bias}做些小的改动,这样网络能够接近于把图像分类为
“9”。然后我们要重复这个工作,反复改动\gls*{weight}和\gls*{bias}来产生更好的输
出。这时网络就在学习。
问题是当我们给实际网络加上\gls*{perceptron}时,结果并不像我们想象的那样。实际上,
网络中单个\gls*{perceptron}上一个\gls*{weight}或\gls*{bias}的微小改动有时候会引
起那个\gls*{perceptron}的输出完全翻转,如 $0$ 变到 $1$。那样的翻转可能接下来引起
其余网络的行为以极其复杂的方式完全改变。因此,虽然你的“9”可能被正确分类,网络
在其它图像上的行为很可能以一些很难控制的方式被完全改变。这使得逐步修改%
\gls*{weight}和\gls*{bias}来让网络接近期望行为变得困难。也许有其它聪明的方式来解
决这个问题。但是目前为止,我们还没发现有什么办法能让\gls*{perceptron}网络进行学
习。
我们可以引入一种称为 \gls*{sigmoid-neuron}的新的人工神经元来克服这个问题。%
\gls*{sigmoid-neuron}和\gls*{perceptron}类似,但是经过修改后,\gls*{weight}和%
\gls*{bias}的微小改动只引起输出的微小变化。这对于让神经元网络学习起来是很关键的。
好了, 让我来描述下 \gls*{sigmoid-neuron}。我们用描绘\gls*{perceptron}的相同方式
来描绘 \gls*{sigmoid-neuron}:
\begin{center}
\includegraphics{tikz9}
\end{center}
正如一个\gls*{perceptron},\gls*{sigmoid-neuron}有多个输入,$x_1,x_2,\ldots$。但
是这些输入可以取 $0$ 和$1$ 中的任意值,而不仅仅是 $0$ 或 $1$。例如,
$0.638\ldots$ 是一个 \gls*{sigmoid-neuron}的有效输入。同样,
\gls*{sigmoid-neuron}对每个输入有\gls*{weight},$w_1,w_2,\ldots$,和一个总的%
\gls*{bias},$b$。但是输出不是 $0$ 或 $1$。相反,它现在是 $\sigma(w \cdot x+b)$,
这里 $\sigma$ 被称为 S型函数\footnote{顺便提一下,$\sigma$ 有时被称为\textbf{逻
辑函数},而这种新的神经元类型被称为\textbf{逻辑神经元}。既然这些术语被很多从
事于神经元网络的人使用,记住它是有用的。然而,我们将继续使用 S 型这个术语。},
定义为:
\begin{equation}
\sigma(z) \equiv \frac{1}{1+e^{-z}}
\label{eq:3}\tag{3}
\end{equation}
把它们放在一起来更清楚地说明,一个具有输入 $x_1,x_2,\ldots$,\gls*{weight}
$w_1,w_2,\ldots$,和\gls*{bias} $b$ 的 \gls*{sigmoid-neuron}的输出是:
\begin{equation}
\frac{1}{1+\exp(-\sum_j w_j x_j-b)}
\label{eq:4}\tag{4}
\end{equation}
初看上去,\gls*{sigmoid-neuron}和\gls*{perceptron}有很大的差别。如果你不熟悉 S型
函数的代数形式,它看上去晦涩难懂又令人生畏。实际上,\gls*{perceptron}和
\gls*{sigmoid-neuron}之间有很多相似的地方,跨过理解上的障碍,\gls*{sigmoid-func}
的代数形式具有很多技术细节。
为了理解和\gls*{perceptron}模型的相似性,假设 $z \equiv w \cdot x + b$ 是一个很
大的正数。那么 $e^{-z} \approx 0$ 而 $\sigma(z) \approx 1$。即,当 $z = w \cdot
x+b$ 很大并且为正,\gls*{sigmoid-neuron}的输出近似为 $1$,正好和%
\gls*{perceptron}一样。相反地,假设 $z = w \cdot x+b$ 是一个很大的负数。那么
$e^{-z} \rightarrow \infty$,$\sigma(z) \approx 0$。所以当 $z = w \cdot x +b$ 是
一个很大的负数,\gls*{sigmoid-neuron}的行为也非常近似一个\gls*{perceptron}。只有
在 $w \cdot x+b$ 取中间值时,和\gls*{perceptron}模型有比较大的偏离。
$\sigma$ 的代数形式又是什么?我们怎样去理解它呢?实际上,$\sigma$ 的精确形式不重
要~——~重要的是这个函数绘制的形状。是这样:
\begin{center}
\includegraphics{sigmoid_function}
\label{fig:SigmoidFunction}
\end{center}
这个形状是阶跃函数平滑后的版本:
\begin{center}
\includegraphics{step_function}
\label{fig:StepFunction}
\end{center}
如果 $\sigma$ 实际是个阶跃函数,既然输出会依赖于 $w\cdot x+b$ 是正数还是负
数\footnote{实际上,当 $w \cdot x +b = 0$ ,\gls*{perceptron}输出 $0$,而同时阶
跃函数输出$1$。所以严格地说,我们需要修改阶跃函数来符合这点。但是你知道怎么
做。},那么S型神经元会成为一个\gls*{perceptron}。利用实际的 $\sigma$ 函数,我
们得到一个,就像上面说明的,平滑的\gls*{perceptron}。确实,$\sigma$ 函数的平滑特
性,正是关键因素,而不是其细部形式。$\sigma$ 的平滑意味着\gls*{weight}和%
\gls*{bias}的微小变化,即 $\Delta w_j$ 和 $\Delta b$,会从神经元产生一个微小的输
出变化 $\Delta \mbox{output}$。实际上,微积分告诉我们 $\Delta \mbox{output}$ 可
以很好地近似表示为:
\begin{equation}
\Delta \mbox{output} \approx \sum_j \frac{\partial \, \mbox{output}}{\partial w_j}
\Delta w_j + \frac{\partial \, \mbox{output}}{\partial b} \Delta b
\label{eq:5}\tag{5}
\end{equation}
其中求和是在所有\gls*{weight} $w_j$ 上进行的,而 $\partial \, \mbox{output} /
\partial w_j$ 和$\partial \, \mbox{output} /\partial b$ 符号表示 $output$ 分别对
于 $w_j$ 和 $b$ 的偏导数。如果偏导数这个概念让你不安,不必惊慌。上面全部用偏导数
的表达式看上去很复杂,实际上它的意思非常简单(这可是个好消息):$\Delta
\mbox{output}$ 是一个反映\gls*{weight}和\gls*{bias}变化~——~即 $\Delta w_j$ 和
$\Delta b$~——~的线性函数。利用这个线性特性,我们比较容易细微地修改\gls*{weight}
和\gls*{bias}的值,从而获得我们需要的细微的输出变化。所以,因为%
\gls*{sigmoid-neuron}具有与\gls*{perceptron}类似的本质行为,它们可以帮助我们了解%
\gls*{weight}和\gls*{bias}的变化如何影响输出值。
如果对 $\sigma$ 来说重要的是形状而不是精确的形式,那为什么要在公式~\eqref{eq:3}
中给 $\sigma$ 使用特定的形式呢?事实上,在下文我们还将不时地考虑一些神经元,它们
给其它激活函数 $f(\cdot)$ 输出是 $f(w \cdot x + b)$。当我们使用一个不同的激活函
数,最大的变化是公式~\eqref{eq:5} 中用于偏导数的特定值的改变。事实证明当我们后面
计算这些偏导数,用 $\sigma$ 会简化数学计算,这是因为指数在求导时有些可爱的属性。
无论如何,$\sigma$ 在神经网络的工作中被普遍使用,并且是这本书中我们最常使用的激
活函数。
我们应该如何解释一个 \gls*{sigmoid-neuron}的输出呢?很明显,\gls*{perceptron}和
\gls*{sigmoid-neuron}之间一个很大的不同是 \gls*{sigmoid-neuron}不仅仅输出 $0$ 或
$1$。它可以输出 $0$ 到 $1$ 之间的任何实数,所以诸如 $0.173\ldots$ 和
$0.689\ldots$ 的值是合理的输出。这是非常有用的,例如,当我们想要输出来表示一个神
经网络的图像像素输入的平均强度。但有时候这会是个麻烦。假设我们希望网络的输出表示
“输入图像是一个9”或“输入图像不是一个9”。很明显,如果输出是 $0$ 或 $1$ 是最简
单的,就像用\gls*{perceptron}。但是在实践中,我们可以设定一个约定来解决这个问题,
例如,约定任何至少为 $0.5$ 的输出为表示 “这是一个9”,而其它小于 $0.5$ 的输出为
表示“不是一个9”。当我们正在使用这样的约定时,我总会清楚地提出来,这样就不会引
起混淆。
\subsection*{练习}
\begin{itemize}
\item \textbf{\gls*{sigmoid-neuron}模拟\gls*{perceptron},第一部分}\\假设我们把
一个\gls*{perceptron}网络中的所有权重和\gls*{bias}乘以一个正的常数,$c>0$。证
明网络的行为并没有改变。
\item \textbf{\gls*{sigmoid-neuron}模拟\gls*{perceptron},第二部分}\\假设我们有
上题中相同的设置~——~一个\gls*{perceptron}网络。同样假设所有输入被选中。我们不
需要实际的输入值,仅仅需要固定这些输入。假设对于网络中任何特定%
\gls*{perceptron}的输入 $x$, \gls*{weight}和\gls*{bias}遵循 $w \cdot x + b
\neq 0$。现在用 \gls*{sigmoid-neuron}替换所有网络中的\gls*{perceptron},并且把%
\gls*{weight}和\gls*{bias}乘以一个正的常量 $c>0$。证明在 $c \rightarrow \infty$
的极限情况下,\gls*{sigmoid-neuron}网络的行为和\gls*{perceptron}网络的完全一致。
当一个\gls*{perceptron}的 $w \cdot x + b = 0$ 时又为什么会不同?
\end{itemize}
\section{神经网络的架构}
在下一节我会介绍一个神经网络,我们可以用它来很好地分类手写数字。准备进入下一节时,
解释一些可以让我们命名网络中不同部分的术语是很有帮助的。假设我们有这样的网络:
\begin{center}
\includegraphics{tikz10}
\end{center}
前面提过,这个网络中最左边的称为输入层,其中的神经元称为\textbf{输入神经元}。最
右边的,即\textbf{输出}层包含有\textbf{输出神经元},在本例中,输出层只有一个神经
元。中间层,既然这层中的神经元既不是输入也不是输出,则被称为%
\textbf{\gls{hidden-layer}}。“隐藏”这一术语也许听上去有些神秘~——~我第一次听到
这个词,以为它必然有一些深层的哲学或数学涵意~——~但它实际上仅仅意味着“既非输入也
非输出”。上面的网络仅有一个隐藏层,但有些网络有多个隐藏层。例如,下面的四层网络
有两个隐藏层:
\begin{center}
\includegraphics{tikz11}
\end{center}
有些令人困惑的是,由于历史的原因,尽管是由 \gls*{sigmoid-neuron}而不是%
\gls*{perceptron}构成,这种多层网络有时被称为\textbf{\gls{mlp}}或者\textbf{MLP}。
在这本书中我不会使用 MLP 这个术语,因为我认为这会引起混淆,但这里想提醒你它的存
在。
设计网络的输入输出层通常是比较直接的。例如,假设我们尝试确定一张手写数字的图像上
是否写的是“9”。很自然地,我们可以将图片像素的强度进行编码作为输入神经元来设计
网络。如果图像是一个 $64 \times 64$ 的灰度图像,那么我们会需要 $4096 = 64 \times
64$ 个输入神经元,每个强度取 $0$ 和 $1$ 之间合适的值。输出层只需要包含一个神经元,
当输出值小于 $0.5$ 时表示“输入图像不是一个 $9$”,大于 $0.5$ 的值表示“输入图像
是一个 $9$”。
相比于神经网络中输入输出层的直观设计,隐藏层的设计则堪称一门艺术。特别是,通过一
些简单的经验法则来总结隐藏层的设计流程是不可行的。相反,神经网络的研究人员已经为
隐藏层开发了许多设计最优法则,这有助于网络的行为能符合人们期望的那样。例如,这些
法则可以用于帮助权衡隐藏层数量和训练网络所需的时间开销。在本书后面我们会碰到几个
这样的设计最优法则。
目前为止,我们讨论的神经网络,都是以上一层的输出作为下一层的输入。这种网络被称为%
\textbf{前馈}神经网络。这意味着网络中是没有回路的~——~信息总是向前传播,从不反向
回馈。如果确实有回路,我们最终会有这样的情况:$\sigma$ 函数的输入依赖于输出。这
将难于理解,所以我们不允许这样的环路。
然而,也有一些人工神经网络的模型,其中反馈环路是可行的。这些模型被称为%
\href{http://en.wikipedia.org/wiki/Recurrent_neural_network}{\gls{rnn}}。这种
模型的设计思想,是具有休眠前会在一段有限的时间内保持激活状态的神经元。这种激活状
态可以刺激其它神经元,使其随后被激活并同样保持一段有限的时间。这样会导致更多的神
经元被激活,随着时间的推移,我们得到一个级联的神经元激活系统。因为一个神经元的输
出只在一段时间后而不是即刻影响它的输入,在这个模型中回路并不会引起问题。
\gls*{rnn}比前馈网络影响力小得多,部分原因是循环网络的学习算法(至少目前为止)不
够强大。但是循环网络仍然很有吸引力。它们原理上比前馈网络更接近我们大脑的实际工作。
并且循环网络能解决一些重要的问题,这些问题如果仅仅用前馈网络来解决,则更加困难。
然而为了篇幅,本书将专注于使用更广泛的前馈网络。
\section{一个简单的分类手写数字的网络}
定义神经网络后,让我们回到手写识别上来。我们可以把识别手写数字的问题分成两个子问
题。首先,我们希望有个方式把包含许多数字的图像分成一系列单独的图像,每个包含单个
数字。例如,我们想要把图像
\begin{center}
\includegraphics[width=64pt]{digits}
\end{center}
分成六个单独的图像,
\begin{center}
\includegraphics[height=32pt]{digits_separate}
\end{center}
我们人类可以很容易解决这个分割的问题,但是对于计算机程序来说却是个挑战。一旦图像
被分割,那么程序需要把每个单独的数字分类。例如,我们想要我们的程序能识别上面的第
一个数字
\begin{center}
\includegraphics[height=24pt]{mnist_first_digit}
\end{center}
是 $5$。
我们将专注于编程解决第二个问题,分类单独的数字。这样是因为,一旦你有分类单独数字
的有效方法,分割问题是不难解决的。有很多途径可以解决分割的问题。一种方法是尝试不
同的分割方式,用数字分类器对每一个切分片段打分。如果数字分类器对每一个片段的置信
度都比较高,那么这个分割方式就能得到较高的分数;如果数字分类器在一或多个片段中出
现问题,那么这种分割方式就会得到较低的分数。这种方法的思想是,如果分类器有问题,
那么很可能是由于图像分割出错导致的。这种思想以及它的变化形式能够比较好地解决分割
问题。因此,与其关心分割问题,我们不如把精力集中在设计一个神经网络来解决更有趣、
更困难的问题,即手写数字的识别。
我们将使用一个三层神经网络来识别单个数字:
\begin{center}
\includegraphics{tikz12}
\end{center}
网络的输入层包含给输入像素的值进行编码的神经元。就像下一节会讨论的,我们给网络的
训练数据会有很多扫描得到的 $28 \times 28$ 的手写数字的图像组成,所有输入层包含有
$784 = 28 \times 28$ 个神经元。为了简化,上图中我已经忽略了 $784$ 中大部分的输入
神经元。输入像素是灰度级的,值为 $0.0$ 表示白色,值为 $1.0$ 表示黑色,中间数值表
示逐渐暗淡的灰色。
网络的第二层是一个隐藏层。我们用 $n$ 来表示神经元的数量,我们将给 $n$ 实验不同的
数值。示例中用一个小的隐藏层来说明,仅仅包含 $n=15$ 个神经元。
网络的输出层包含有 $10$ 个神经元。如果第一个神经元激活,即输出 $\approx 1$,那么
表明网络认为数字是一个 $0$。如果第二个神经元激活,就表明网络认为数字是一个 $1$。
依此类推。更确切地说,我们把输出神经元的输出赋予编号 $0$ 到 $9$,并计算出那个神
经元有最高的激活值。比如,如果编号为 $6$ 的神经元激活,那么我们的网络会猜到输入
的数字是 $6$。其它神经元相同。
你可能会好奇为什么我们用 $10$ 个输出神经元。毕竟我们的任务是能让神经网络告诉我们
哪个数字( $0, 1, 2, \ldots, 9$ )能和输入图片匹配。一个看起来更自然的方式就是使
用 $4$ 个输出神经元,把每一个当做一个二进制值,结果取决于它的输出更靠近 $0$ 还是
$1$~。四个神经元足够编码这个问题了,因为 $2^4 = 16$ 大于 $10$ 种可能的输入。为什
么我们反而要用 $10$ 个神经元呢?这样做难道效率不低吗?最终的判断是基于经验主义的:
我们可以实验两种不同的网络设计,结果证明对于这个特定的问题而言,$10$ 个输出神经
元的神经网络比4个的识别效果更好。但是令我们好奇的是为什么使用 $10$ 个输出神经元
的神经网络更有效呢。有没有什么启发性的方法能提前告诉我们用10个输出编码比使用 $4$
个输出编码更有好呢?
为了理解为什么我们这么做,我们需要从根本原理上理解神经网络究竟在做些什么。首先考
虑有 $10$ 个神经元的情况。我们首先考虑第一个输出神经元,它告诉我们一个数字是不是
0。它能那么做是因为可以权衡从隐藏层来的信息。隐藏层的神经元在做什么呢?假设隐藏
层的第一个神经元只是用于检测如下的图像是否存在:
\begin{center}
\includegraphics[height=32pt]{mnist_top_left_feature}
\end{center}
为了达到这个目的,它通过对此图像对应部分的像素赋予较大\gls*{weight},对其它部分
赋予较小的\gls*{weight}。同理,我们可以假设隐藏层的第二,第三,第四个神经元是为
检测下列图片是否存在:
\begin{center}
\includegraphics[height=32pt]{mnist_other_features}
\end{center}
就像你能猜到的,这四幅图像组合在一起构成了\hyperref[fig:digits]{前面}显示的一行
数字图像中的 $0$:
\begin{center}
\includegraphics[height=32pt]{mnist_complete_zero}
\end{center}
如果所有隐藏层的这四个神经元被激活那么我们就可以推断出这个数字是 $0$。当然,这不
是我们推断出 $0$ 的唯一方式~——~我们能通过很多其他合理的方式得到 $0$ (举个例子来
说,通过上述图像的转换,或者稍微变形)。但至少在这个例子中我们可以推断出输入的
数字是 $0$。
假设神经网络以上述方式运行,我们可以给出一个貌似合理的理由去解释为什么用 $10$ 个
输出而不是 $4$ 个。如果我们有 $4$ 个输出,那么第一个输出神经元将会尽力去判断数字
的最高有效位是什么。把数字的最高有效位和数字的形状联系起来并不是一个简单的问题。
很难想象出有什么恰当的历史原因,一个数字的形状要素会和一个数字的最高有效位有什么
紧密联系。
上面我们说的只是一个启发性的方法。没有什么理由表明这个三层的神经网络必须按照我所
描述的方式运行,即隐藏层是用来探测数字的组成形状。可能一个聪明的学习算法将会找到
一些合适的\gls*{weight}能让我们仅仅用4个输出神经元就行。但是这个启发性的方法通常
很有效,它会节省你大量时间去设计一个好的神经网络结构。
\subsection*{练习}
\begin{itemize}
\item 通过在上述的三层神经网络加一个额外的一层就可以实现按位表示数字。额外的一层
把原来的输出层转化为一个二进制表示,如下图所示。为新的输出层寻找一些合适的%
\gls*{weight}和\gls*{bias}。假定原先的3层神经网络在第三层得到正确输出(即原来
的输出层)的激活值至少是 $0.99$,得到错误的输出的激活值至多是 $0.01$。
\begin{center}
\includegraphics{tikz13}
\end{center}
\end{itemize}
\section{使用梯度下降算法进行学习}
\label{sec:learning_with_gradient_descent}
现在我们有了神经网络的设计,它怎样可以学习识别数字呢?我们需要的第一样东西是一个
用来学习的数据集~——~称为训练数据集。我们将使用
\href{http://yann.lecun.com/exdb/mnist/}{MNIST 数据集},其包含有数以万计的连带着
正确分类器的手写数字的扫描图像。MNIST的名字来源于
\href{http://en.wikipedia.org/wiki/National_Institute_of_Standards_and_Technology}{NIST}
—— 美国国家标准与技术研究所~——~收集的两个数据集改进后的子集。这是取自 MNIST 的一
些图像:
\begin{center}
\includegraphics[height=32pt]{digits_separate}
\end{center}
正如你看到的,这些数字其实是和本章\hyperref[fig:digits]{开始}提到的一样。当然,
当测试我们的网络时我们将要求它识别不在训练集中的图像。
MNIST 数据分为两个部分。第一部分包含 60,000 幅用于训练数据的图像。这些图像扫描自
250 人的手写样本,他们中一半人是美国人口普查局的员工,一半人是高校学生。这些图像
是 $28 \times 28$ 大小的灰度图像。第二部分是 10,000 幅用于测试数据的图像,同样是
$28 \times 28$ 的灰度图像。我们将用这些测试数据来评估我们的神经网络学会识别数字
有多好。为了让其有好的测试表现,测试数据取自和原始训练数据不同的\textbf{另外}一
组 250 人(尽管仍然分别是美国人口普查局和高校学生)。这有助于确保我们的系统能识
别那些没有看到训练数据的人写的数字。
我们将用符号 $x$ 来表示一个训练输入。为了方便,把每个训练输入 $x$ 看作一个 $28
\times 28 = 784$ 维的向量。每个向量中的项目代表图像中单个像素的灰度值。我们用 $y
= y(x)$ 表示对应的期望输出,这里 $y$ 是一个 $10$ 维的向量。例如,如果有一个特定
的画成 $6$ 的训练图像,$x$,那么 $y(x) = (0, 0, 0, 0, 0, 0, 1, 0, 0, 0)^T$ 则是
网络的期望输出。注意这里 $T$ 是转置操作,把一个行向量转换成一个列向量。
我们希望有一个算法,能让我们找到\gls*{weight}和\gls*{bias},以至于网络的输出
$y(x)$ 能够拟合所有的训练输入$x$。为了量化我们如何实现这个目标,我们定义一个%
\textbf{\gls{cost-func}}\footnote{有时被称为\textbf{损失}或\textbf{目标}函数。我
们在这本书中使用了代价函数这个术语,但是你应该注意其他的术语,因为它经常被用于
研究论文和其他神经网络的讨论中。}:
\begin{equation}
C(w,b) \equiv \frac{1}{2n} \sum_x \| y(x) - a\|^2
\label{eq:6}\tag{6}
\end{equation}
这里 $w$ 表示所有的网络中\gls*{weight}的集合,$b$ 是所有的\gls*{bias},$n$ 是训
练输入数据的个数,$a$ 是表示当输入为 $x$ 时输出的向量,求和则是在总的训练输入
$x$ 上进行的。当然,输出 $a$ 取决于 $x$, $w$ 和 $b$,但是为了保持符号的简洁性,
我没有明确地指出这种依赖关系。符号 $\|v\|$ 是指向量 $v$ 的模。我们把 $C$ 称为%
\textbf{二次}\gls*{cost-func};有时也被称为\textbf{均方误
差}或者\textbf{MSE}。观察二次代价函数的形式我们可以
看到 $C(w,b)$ 是非负的,因为求和公式中的每一项都是非负的。此外,代价函数
$C(w,b)$ 的值相当小,即 $C(w,b) \approx 0$,精确地说,是当对于所有的训练输入 $x$,
$y(x)$ 接近于输出 $a$ 时。因此如果我们的学习算法能找到合适的\gls*{weight}和%
\gls*{bias},使得 $C(w,b) \approx 0$,它就能很好地工作。相反,当 $C(w,b)$ 很大时
就不怎么好了,那意味着对于大量地输入, $y(x)$ 与输出 $a$ 相差很大。因此我们的训
练算法的目的,是最小化\gls*{weight}和\gls*{bias}的代价函数 $C(w,b)$。换句话说,
我们想要找到一系列能让代价尽可能小的\gls*{weight}和\gls*{bias}。我们将采用称为%
\textbf{梯度下降}的算法来达到这个目的。
为什么要介绍二次代价呢?毕竟我们最初感兴趣的内容不是能正确分类的图像数量吗?为什
么不试着直接最大化这个数量,而是去最小化一个类似二次代价的间接评量呢?这么做是因
为在神经网络中,被正确分类的图像数量所关于\gls*{weight}和\gls*{bias}的函数并不是
一个平滑的函数。大多数情况下,对\gls*{weight}和\gls*{bias}做出的微小变动完全不会
影响被正确分类的图像的数量。这会导致我们很难去解决如何改变\gls*{weight}和%
\gls*{bias}来取得改进的性能。而用一个类似二次代价的平滑代价函数则能更好地去解决
如何用\gls*{weight}和\gls*{bias}中的微小的改变来取得更好的效果。这就是为什么我们
首先专注于最小化二次代价,只有这样,我们之后才能测试分类精度。
即使已经知道我们需要使用一个平滑的代价函数,你可能仍然想知道为什么我们在方
程~\eqref{eq:6} 中选择二次函数。这是\textbf{临时}想出来的吗?是不是我们选择另一
个不同的代价函数将会得到完全不同的最小化的\gls*{weight}和\gls*{bias}呢?这种顾虑
是合理的,我们后面会再次回到这个代价函数,并做一些修改。尽管如此,方
程~\eqref{eq:6} 中的二次代价函数让我们更好地理解神经网络中学习算法的基础,所以目
前我们会一直使用它。
重复一下,我们训练神经网络的目的是找到能最小化二次代价函数 $C(w,b)$ 的%
\gls*{weight}和\gls*{bias}。这是一个适定问题,但是现在它有很多让我们分散精力的结
构~——~对\gls*{weight} $w$ 和\gls*{bias} $b$的解释,晦涩不清的 $\sigma$ 函数,神
经网络结构的选择,MNIST 等等。事实证明我们可以忽略结构中大部分,把精力集中在最小
化方面来理解它。现在我们打算忘掉所有关于代价函数的具体形式、神经网络的连接等等。
现在让我们想象只要最小化一个给定的多元函数。我们打算使用一种被称为\textbf{梯度下
降}的技术来解决这样的最小化问题。然后我们回到在神经网络中要最小化的特定函数上
来。
好了,假设我们要最小化某些函数,$C(v)$。它可以是任意的多元实值函数,$v = v_1,
v_2, \ldots$。注意我们用 $v$ 代替了 $w$ 和 $b$ 以强调它可能是任意的函数~——~我们
现在先不局限于神经网络的环境。为了最小化 $C(v)$,想象 $C$ 是一个只有两个变量
$v1$ 和 $v2$ 的函数:
\begin{center}
\includegraphics{valley}
\end{center}
我们想要的是找到 $C$ 的全局最小值。当然,对于上图的函数,我们一眼就能找到最小值。
那只意味着,也许我展示的函数\textbf{过于}简单了!通常函数 $C$ 可能是一个复杂的多
元函数,看一下就能找到最小值是不可能的。
一种解决这个问题的方式是用微积分来解析最小值。我们可以计算导数去寻找 $C$ 的极值
点。运气好的话,$C$ 是一个只有一个或少数几个变量的函数。但是变量过多的话那就是噩
梦。而且神经网络中我们经常需要\textbf{大量}的变量——最大的神经网络有依赖数亿%
\gls*{weight}和\gls*{bias}的代价函数,极其复杂。用微积分来计算最小值已经不可行了。
(确定我们将可以通过有两个变量的函数 $C$ 来理解神经网络后,我已经两次提到:“嘿,
如果函数有远多于两个变量怎么办?”。对此我只能说很抱歉。请相信我把 $C$ 想象成
一个二元函数是有助于我们的理解的。有时候这种想象的画面会遇到障碍,这正是上面两
个段落在帮助你克服的。善于思考数学通常也涉及到有效地利用多种直觉上的想象画面,
学会什么时候用什么画面合适。)
好吧,微积分是不能用了。幸运的是,有一个漂亮的推导法暗示有一种算法能得到很好的效
果。首先把我们的函数想象成一个山谷。只要瞄一眼上面的绘图就不难理解。我们想象有一
个小球从山谷的斜坡滚落下来。我们的日常经验告诉我们这个球最终会滚到谷底。也许我们
可以用这一想法来找到函数的最小值?我们会为一个(假想的)球体随机选择一个起始位置,
然后模拟球体滚落到谷底的运动。我们可以通过计算 $C$ 的导数(或者二阶导数)来简单
模拟——这些导数会告诉我们山谷中局部“形状”的一切,由此知道我们的球将怎样滚动。
看到这里你可能会以为我们会写下球体的牛顿运动定理,考虑摩擦力、重力等影响。实际上,
我们不打算真的去实现这个球体滚落的推导~——~我们是在设计一个最小化 $C$ 的算法,而
不是在用物理定律做精确的仿真。对球体的肉眼观察是为了激发我们的想象而不是束缚我们
的思维。因此与其陷进物理学里凌乱的细节,不如我们就这样问自己:如果我们扮演一天的
上帝,能够构造自己的物理定律,能够支配球体可以如何滚动,那么我们将会采取什么样的
运动学定律来让球体能够总是滚落到谷底呢?
为了更精确地描述这个问题,让我们思考一下,当我们在 $v1$ 和 $v2$ 方向分别将球体移
动一个很小的量,即 $\Delta v1$ 和 $\Delta v2$ 时,球体将会发生什么情况。微积分告
诉我们 $C$ 将会有如下变化:
\begin{equation}
\Delta C \approx \frac{\partial C}{\partial v_1} \Delta v_1 +
\frac{\partial C}{\partial v_2} \Delta v_2
\label{eq:7}\tag{7}
\end{equation}
我们要寻找一种选择 $\Delta v_1$ 和 $\Delta v_2$ 的方法使得 $\Delta C$ 为负;即,
我们选择它们是为了让球体滚落。为了弄明白如何选择,需要定义 $\Delta v$ 为 $v$ 变
化的向量,$\Delta v \equiv (\Delta v_1, \Delta v_2)^T$,$T$ 是转置符号。我们也定
义 $C$ 的梯度为偏导数的向量,$\left(\frac{\partial C}{\partial v_1},
\frac{\partial C}{\partial v_2}\right)^T$。我们用 $\nabla C$ 来表示梯度向量,即:
\begin{equation}
\nabla C \equiv \left( \frac{\partial C}{\partial v_1}, \frac{\partial
C}{\partial v_2} \right)^T
\label{eq:8}\tag{8}
\end{equation}
我们马上会用 $\Delta v$ 和梯度 $\nabla C$ 来重写 $\Delta C$ 的变化。在这之前我想
先澄清一些令人困惑的关于梯度的事情。当第一次碰到 $\nabla C$ 这个符号,人们有时会
想知道怎么去理解 $\nabla$ 符号。$\nabla$ 究竟是什么意思?事实上你可以把 $\nabla
C$ 仅仅看做一个简单的数学记号~——~上面定义的向量~——~这样就不必写两个符号了。这样
来看,$\nabla$ 仅仅是一个符号,犹如风中摆动的旗帜,告诉你:“嘿,$\nabla C$ 是一
个梯度向量”。也有很多其它的数学上不同视角对于 $\nabla$ 的专业解释(比如,作为一
个微分操作),但我们不需要这些观点。
有了这些定义,$\Delta C$ 的表达式~\eqref{eq:7} 可以被重写为:
\begin{equation}
\Delta C \approx \nabla C \cdot \Delta v
\label{eq:9}\tag{9}
\end{equation}
这个表达式解释了为什么 $\nabla C$ 被称为梯度向量:$\nabla C$ 把 $v$ 的变化关联为
$C$ 的变化,正如我们期望的用梯度来表示。但是这个方程真正让我们兴奋的是它让我们看
到了如何选取 $\Delta v$ 才能让 $\Delta C$ 为负数。假设我们选取:
\begin{equation}
\Delta v = -\eta \nabla C
\label{eq:10}\tag{10}
\end{equation}
这里的 $\eta$ 是个很小的正数(称为\textbf{\gls{learning-rate}})。方
程~\eqref{eq:9} 告诉我们 $\Delta C \approx -\eta \nabla C \cdot \nabla C = -\eta
\|\nabla C\|^2$。由于 $\| \nabla C \|^2 \geq 0$,这保证了$\Delta C \leq 0$,即,
如果我们按照方程~\eqref{eq:10} 的规则去改变 $v$,那么 $C$ 会一直减小,不会增加。
(当然,要在方程~\eqref{eq:9} 的近似约束下)。这正是我们想要的特性!因此我们把方
程~\eqref{eq:10} 用于定义球体在梯度下降算法下的“运动定律”。也就是说,我们用方
程~\eqref{eq:10} 计算 $\Delta v$,来移动球体的位置 $v$:
\begin{equation}
v \rightarrow v' = v -\eta \nabla C
\label{eq:11}\tag{11}
\end{equation}
然后我们用它再次更新规则来计算下一次移动。如果我们反复持续这样做,我们将持续减小
$C$ 直到~——~正如我们希望的~——~获得一个全局的最小值。
总结一下,梯度下降算法工作的方式就是重复计算梯度 $\nabla C$,然后沿着\textbf{相
反}的方向移动,沿着山谷“滚落”。我们可以想象它像这样:
\begin{center}
\includegraphics{valley_with_ball}
\end{center}
注意具有这一规则的梯度下降并不是模仿实际的物理运动。在现实中一个球体有动量,使得
它岔开斜坡滚动,甚至(短暂地)往山上滚。只有在克服摩擦力的影响,球体才能保证滚到
山谷。相比之下,我们选择 $\Delta v$ 规则只是说:“往下,现在”。这仍然是一个寻找
最小值的非常好的规则!
为了使梯度下降能够正确地运行,我们需要选择足够小的\gls*{learning-rate} $\eta$ 使
得方程~\eqref{eq:9} 能得到很好的近似。如果不这样,我们会以 $\Delta C > 0$ 结束,
这显然不好。同时,我们也不想 $\eta$ 太小,因为这会使 $\Delta v$ 的变化极小,梯度
下降算法就会运行得非常缓慢。在真正的实现中,$\eta$ 通常是变化的,以至方
程~\eqref{eq:9} 能保持很好的近似度,但算法又不会太慢。我们后面会看这是如何工作的。
我已经解释了具有两个变量的函数 $C$ 的梯度下降。但事实上,即使 $C$ 是一个具有更多
变量的函数也能很好地工作。我们假设 $C$ 是一个有 $m$ 个变量 $v_1,\ldots,v_m$ 的多
元函数。那么对 $C$ 中自变量的变化 $\Delta v = (\Delta v_1, \ldots, \Delta
v_m)^T$,$\Delta C$ 将会变为:
\begin{equation}
\Delta C \approx \nabla C \cdot \Delta v
\label{eq:12}\tag{12}
\end{equation}
这里的梯度 $\nabla C$ 是向量
\begin{equation}
\nabla C \equiv \left(\frac{\partial C}{\partial v_1}, \ldots,
\frac{\partial C}{\partial v_m}\right)^T
\label{eq:13}\tag{13}
\end{equation}
正如两个变量的情况,我们可以选取
\begin{equation}
\Delta v = -\eta \nabla C
\label{eq:14}\tag{14}
\end{equation}
而且 $\Delta C$ 的(近似)表达式~\eqref{eq:12} 保证是负数。这给了我们一种方式从
梯度中去取得最小值,即使 $C$ 是任意的多元函数,我们也能重复运用更新规则
\begin{equation}
v \rightarrow v' = v-\eta \nabla C
\label{eq:15}\tag{15}
\end{equation}
你可以把这个更新规则看做\textbf{定义}梯度下降算法。这给我们提供了一种方式去通过
重复改变 $v$ 来找到函数 $C$ 的最小值。这个规则并不总是有效的~——~有几件事能导致错
误,让我们无法从梯度下降来求得函数 $C$ 的全局最小值,这个观点我们会在后面的章节
中去探讨。但在实践中,梯度下降算法通常工作地非常好,在神经网络中这是一种非常有效
的方式去求代价函数的最小值,进而促进网络自身的学习。
事实上,甚至有一种观点认为梯度下降法是求最小值的最优策略。假设我们正在努力去改变
$\Delta v$ 来让 $C$ 尽可能地减小。这相当于最小化 $\Delta C \approx \nabla C
\cdot \Delta v$。我们首先限制步长为小的固定值,即 $\| \Delta v \| = \epsilon$,
$\epsilon > 0$。当步长固定时,我们要找到使得 $C$ 减小最大的下降方向。可以证明,
使得 $\nabla C \cdot \Delta v$ 取得最小值的 $\Delta v$ 为 $\Delta v = - \eta
\nabla C$,这里 $\eta = \epsilon / \|\nabla C\|$ 是由步长限制 $\|\Delta v\| =
\epsilon$ 所决定的。因此,梯度下降法可以被视为一种在 $C$ 下降最快的方向上做微小
变化的方法。
\subsection*{练习}
\begin{itemize}
\item 证明上一段落的推断。提示:可以利用%
\href{http://en.wikipedia.org/wiki/Cauchy–Schwarz_inequality}{柯西-施瓦茨不等
式}。
\item 我已经解释了当 $C$ 是二元及其多元函数的情况。那如果 $C$ 是一个一元函数呢?
你能给出梯度下降法在一元函数的几何解释么?
\end{itemize}
人们已经研究出很多梯度下降的变化形式,包括一些更接近真实模拟球体物理运动的变化形
式。这些模拟球体的变化形式有很多优点,但是也有一个主要的缺点:它最终必需去计算
$C$ 的二阶偏导,这代价可是非常大的。为了理解为什么这种做法代价高,假设我们想求所
有的二阶偏导 $\partial^2 C/ \partial v_j \partial v_k$。如果我们有上百万的变量
$v_j$,那我们必须要计算数万亿(即百万次的平方)级别的二阶偏导\footnote{实际上,
更接近万亿次的一半,因为$\partial^2 C/ \partial v_j \partial v_k = \partial^2
C/ \partial v_k \partial v_j$。同样,你知道怎么做。}!这会造成很大的计算代价。
不过也有一些避免这类问题的技巧,寻找梯度下降算法的替代品也是个很活跃的研究领域。
但在这本书中我们将主要用梯度下降算法(包括变化形式)使神经网络学习。
我们怎么在神经网络中用梯度下降算法去学习呢?其思想就是利用梯度下降算法去寻找能使
得方程~\eqref{eq:6} 的代价取得最小值的\gls*{weight} $w_k$ 和\gls*{bias} $b_l$。
为了清楚这是如何工作的,我们将用\gls*{weight}和\gls*{bias}代替变量 $v_j$。也就是
说,现在“位置”变量有两个分量组成:$w_k$ 和 $b_l$,而梯度向量 $\nabla C$ 则有相
应的分量 $\partial C / \partial w_k$和$\partial C / \partial b_l$。用这些分量来
写梯度下降的更新规则,我们得到:
\begin{align}
\label{eq:16}w_k \rightarrow w_k' &= w_k-\eta \frac{\partial C}{\partial w_k}\tag{16}\\
\label{eq:17}b_l \rightarrow b_l' &= b_l-\eta \frac{\partial C}{\partial b_l}\tag{17}
\end{align}
通过重复应用这一更新规则我们就能“让球体滚下山”,并且有望能找到代价函数的最小值。
换句话说,这是一个能让神经网络学习的规则。
应用梯度下降规则有很多挑战。我们将在下一章深入讨论。但是现在只提及一个问题。为了
理解问题是什么,我们先回顾\eqref{eq:6} 中的二次代价。注意这个代价函数有着这样的
形式 $C = \frac{1}{n} \sum_x C_x$,即,它是遍及每个训练样本代价 $C_x \equiv
\frac{\|y(x)-a\|^2}{2}$ 的平均值。在实践中,为了计算梯度 $\nabla C$,我们需要为
每个训练输入 $x$ 单独地计算梯度值 $\nabla C_x$,然后求平均值,$\nabla C =
\frac{1}{n} \sum_x \nabla C_x$。不幸的是,当训练输入的数量过大时会花费很长时间,
这样会使学习变得相当缓慢。
有种叫做\textbf{\gls{sgd}}的算法能够加速学习。其思想就是通过随机选取小量训练输入
样本来计算 $\nabla C_x$,进而估算梯度 $\nabla C$。通过计算少量样本的平均值我们可
以快速得到一个对于实际梯度 $\nabla C$ 的很好的估算,这有助于加速梯度下降,进而加
速学习过程。
更准确地说,随机梯度下降通过随机选取小量的 $m$ 个训练输入来工作。我们将这些随机
的训练输入标记为$X_1, X_2, \ldots, X_m$,并把它们称为一个%
\textbf{\gls{mini-batch}}。假设样本数量 $m$ 足够大,我们期望 $\nabla C_{X_j}$ 的
平均值大致相等于整个 $\nabla C_x$的平均值,即,
\begin{equation}
\frac{\sum_{j=1}^m \nabla C_{X_{j}}}{m} \approx \frac{\sum_x \nabla C_x}{n} = \nabla C
\label{eq:18}\tag{18}
\end{equation}
这里的第二个求和符号是在整个训练数据上进行的。交换两边我们得到
\begin{equation}
\nabla C \approx \frac{1}{m} \sum_{j=1}^m \nabla C_{X_{j}}
\label{eq:19}\tag{19}
\end{equation}
证实了我们可以通过仅仅计算随机选取的\gls*{mini-batch}的梯度来估算整体的梯度。
为了将其明确地和神经网络的学习联系起来,假设 $w_k$ 和 $b_l$ 表示我们神经网络中权
重和\gls*{bias}。随机梯度下降通过随机地选取并训练输入的\gls*{mini-batch}来工作,
\begin{align}
\label{eq:20}w_k \rightarrow w_k' &= w_k-\frac{\eta}{m}
\sum_j \frac{\partial C_{X_j}}{\partial w_k}
\tag{20}\\
\label{eq:21}b_l \rightarrow b_l' &= b_l-\frac{\eta}{m}
\sum_j \frac{\partial C_{X_j}}{\partial b_l}
\tag{21}
\end{align}
其中两个求和符号是在当前\gls*{mini-batch}中的所有训练样本 $X_j$ 上进行的。然后我
们再挑选另一随机选定的\gls*{mini-batch}去训练。直到我们用完了所有的训练输入,这
被称为完成了一个训练\textbf{\gls{epoch}}。然后我们就会开始一个新的训练
\gls*{epoch}。
另外值得提一下,对于改变代价函数大小的参数,和用于计算\gls*{weight}和\gls*{bias}
的\gls*{mini-batch}的更新规则,会有不同的约定。在方程~\eqref{eq:6} 中,我们通过
因子 $\frac{1}{n}$ 来改变整个\gls*{cost-func}的大小。人们有时候忽略
$\frac{1}{n}$,直接取单个训练样本的代价总和,而不是取平均值。这对我们不能提前知
道训练数据数量的情况下特别有效。例如,这可能发生在有更多的训练数据是实时产生的情
况下。同样,\gls*{mini-batch}的更新规则~\eqref{eq:20}和~\eqref{eq:21} 有时也会舍
弃前面的 $\frac{1}{m}$。从概念上这会有一点区别,因为它等价于改变了%
\gls*{learning-rate} $\eta$ 的大小。但在对不同工作进行详细对比时,需要对它警惕。
我们可以把随机梯度下降想象成一次民意调查:在一个\gls*{mini-batch}上采样比对一个
完整数据集进行梯度下降分析要容易得多,正如进行一次民意调查比举行一次全民选举要更
容易。例如,如果我们有一个规模为 $n = 60,000$ 的训练集,就像 MNIST,并选取
\gls*{mini-batch}大小为 $m = 10$,这意味着在估算梯度过程中加速了 $6,000$ 倍!当
然,这个估算并不是完美的~——~存在统计波动~——~但是没必要完美:我们实际关心的是在某
个方向上移动来减少 $C$,而这意味着我们不需要梯度的精确计算。在实践中,随机梯度下
降是在神经网络的学习中被广泛使用、十分有效的技术,它也是本书中展开的大多数学习技
术的基础。
\subsection*{练习}
\begin{itemize}
\item 梯度下降算法一个极端的版本是把\gls*{mini-batch}的大小设为 $1$。即,假设一
个训练输入 $x$,我们按照规则 $w_k \rightarrow w_k' = w_k - \eta \partial C_x /
\partial w_k$ 和 $b_l \rightarrow b_l' = b_l - \eta \partial C_x / \partial
b_l$ 更新我们的\gls*{weight}和\gls*{bias}。然后我们选取另一个训练输入,再一次
更新\gls*{weight}和\gls*{bias}。如此重复。这个过程被称为\textbf{在线}、%
\textbf{online}、\textbf{on-line}、或者\textbf{递增}学习。在 online 学习中,神
经网络在一个时刻只学习一个训练输入(正如人类做的)。对比具有一个小批量输入大小
为 $20$ 的随机梯度下降,说出递增学习的一个优点和一个缺点。
\end{itemize}
让我们讨论一个令刚接触梯度下降的人困惑的问题来总结这部分的内容。在神经网络中,代
价函数 $C$ 是一个关于所有\gls*{weight}和\gls*{bias}的多元函数,因此在某种意义上
来说,就是在一个高维空间定义了一个平面。有些人可能会担心地想:“嘿,我必须要想象
其它多出的维度”。他们会开始发愁:“我不能想象出四维空间,更不用说五维(或者五百
万维)”。是不是他们缺少某种只有“超级”数学家才有的超能力?当然不是。即使大多
数专业的数学家也不能想象出四维空间的样子。他们用的技巧,是扩展出其它的方法来描绘
发生了什么事。正如我们上面所做的那样,我们用代数(而不是图像)描绘 $\Delta C$ 来
计算如何变化才能让 $C$ 减少。那些善于思考高维的人内心有着包含有许多不同的技术的
知识库;我们的代数技巧也是一个例子。这些技术可能没有我们习惯于思考三维时的那么简
单,但一旦你构建起这样的知识库,你能够更从容应对更高的维度。我不想在这里详细展开,
如果你感兴趣,你可以阅读这个关于专业的数学家如何思考高维空间
的%
\href{http://mathoverflow.net/questions/25983/intuitive-crutches-for-higher-dimensional-thinking}{
讨论}。我们讨论的一些技术可能会有点复杂,但很多最好的内容还是比较直观并容易理
解的,任何人都能熟练掌握。
\section{实现我们的网络来分类数字}
\label{sec:implementing_our_network_to_classify_digits}
好吧,现在让我们写一个学习如何识别手写数字的程序,使用随机梯度下降算法和 MNIST训
练数据。我们需要做的第一件事情是获取 MNIST 数据。如果你是一个 \lstinline!git! 用
户,那么你能够通过克隆这本书的代码仓库获得数据,
\begin{lstlisting}[language=sh]
git clone https://github.com/mnielsen/neural-networks-and-deep-learning.git
\end{lstlisting}
如果你不使用 \lstinline!git!,也可以从%
\href{https://github.com/mnielsen/neural-networks-and-deep-learning/archive/master.zip}{
这里}下载数据和代码。
顺便提一下,当我在之前描述 MNIST 数据时,我说它分成了 60,000 个训练图像和 10,000
个测试图像。这是官方的 MNIST 的描述。实际上,我们将用稍微不同的方法对数据进行划
分。我们将测试集保持原样,但是将 60,000 个图像的 MNIST 训练集分成两个部分:一部
分 50,000 个图像,我们将用来训练我们的神经网络,和一个单独的 10,000 个图像的%
\textbf{验证集}。在本章中我们不使用验证数据,但是在本书的后面我们将会发现它对于
解决如何去设置某些神经网络中的\textbf{\gls{hyper-params}}是很有用的~——~例如%
\gls*{learning-rate}等,这些参数不被我们的学习算法直接选择。尽管验证数据不是原始
MNIST 规范的一部分,然而许多人以这种方式使用 MNIST,并且在神经网络中使用验证数据
是很普遍的。从现在起当我提到“MNIST 训练数据”时,我指的是我们的 50,000 个图像数
据集,而不是原始的 60,000图像数据集\footnote{如前所述,MNIST数据集是基于NIST(美
国国家标准与技术研究院)收集的两个数据集合。为了构建MNIST,NIST数据集合被
Yann LeCun,Corinna Cortes和Christopher J. C. Burges拆分放入一个更方便的格式。
更多细节请看这个链接。我的仓库中的数据集是在一种更容易在Python中加载和操纵
MNIST数据的形式。我从蒙特利尔大学的LISA机器学习实验室获得了这个特殊格式的数据
(\href{http://www.deeplearning.net/tutorial/gettingstarted.html}{链接})}。
除了 MNIST 数据,我们还需要一个叫做 \href{http://numpy.org/}{Numpy} 的 Python 库,
用来做快速线性代数。如果你没有安装过 Numpy,你能够从%
\href{http://www.scipy.org/install.html}{这里}下载。
在列出一个完整的代码清单之前,让我解释一下神经网络代码的核心特性。核心片段是一个
\lstinline!Network! 类,我们用来表示一个神经网络。这是我们用来初始化一个
\lstinline!Network! 对象的代码:
\begin{lstlisting}[language=Python]
class Network(object):
def __init__(self, sizes):
self.num_layers = len(sizes)
self.sizes = sizes
self.biases = [np.random.randn(y, 1) for y in sizes[1:]]
self.weights = [np.random.randn(y, x)
for x, y in zip(sizes[:-1], sizes[1:])]
\end{lstlisting}
在这段代码中,列表 \lstinline!sizes! 包含各层神经元的数量。例如,如果我们想创建
一个在第一层有 2 个神经元,第二层有 3 个神经元,最后层有 1 个神经元的
\lstinline!Network! 对象,我们应这样写代码:
\begin{lstlisting}[language=Python]
net = Network([2, 3, 1])
\end{lstlisting}
\lstinline!Network! 对象中的\gls*{bias}和\gls*{weight}都是被随机初始化的,使用
Numpy 的 \lstinline!np.random.randn! 函数来生成均值为 0,标准差为 1 的高斯分布。
这样的随机初始化给了我们的随机梯度下降算法一个起点。在后面的章节中我们将会发现更
好的初始化\gls*{weight}和\gls*{bias}的方法,但是目前随机地将其初始化。注意
\lstinline!Network! 初始化代码假设第一层神经元是一个输入层,并对这些神经元不设置
任何\gls*{bias},因为\gls*{bias}仅在后面的层中用于计算输出。
另外注意,\gls*{bias}和\gls*{weight}以 Numpy 矩阵列表的形式存储。例如
\lstinline!net.weights[1]! 是一个存储着连接第二层和第三层神经元\gls*{weight}的
Numpy 矩阵。(不是第一层和第二层,因为 Python 列表的索引从 0 开始。)既然
\lstinline!net.weights[1]! 相当冗长,让我们用 $w$ 表示矩阵。矩阵的 $w_{jk}$ 是连
接第二层的 $k^{\rm th}$ 神经元和第三层的 $j^{\rm th}$ 神经元的\gls*{weight}。这
种 $j$ 和 $k$ 索引的顺序可能看着奇怪~——~交换 $j$和 $k$ 索引会更有意义,确定吗?
使用这种顺序的很大的优势是它意味着第三层神经元的激活向量是:
\begin{equation}
a' = \sigma(w a + b)
\label{eq:22}\tag{22}
\end{equation}
这个方程有点奇怪,所以让我们一块一块地理解它。$a$ 是第二层神经元的激活向量。为了
得到 $a'$,我们用\gls*{weight}矩阵 $w$ 乘以 $a$,加上\gls*{bias}向量 $b$,我们然
后对向量 $w a +b$ 中的每个元素应用函数 $\sigma$。(这称为将函数 $\sigma$
\textbf{向量化}。)很容易验证方程~\eqref{eq:22} 的结果和我们之前的计算一个 S
型神经元输出的方程~\eqref{eq:4} 相同。
\subsection*{练习}
\begin{itemize}
\item 以分量形式写出方程~\eqref{eq:22},并验证它和计算\gls*{sigmoid-neuron}输出
的规则~\eqref{eq:4} 结果相同。
\end{itemize}
有了这些,很容易写出从一个 \lstinline!Network! 实例计算输出的代码。我们从定义 S
型函数开始:
\begin{lstlisting}[language=Python]
def sigmoid(z):
return 1.0/(1.0+np.exp(-z))
\end{lstlisting}
注意,当输入 $z$ 是一个向量或者 Numpy 数组时,Numpy 自动地按元素应用
\lstinline!sigmoid! 函数,即以向量形式。
我们然后对 \lstinline!Network! 类添加一个 \lstinline!feedforward! 方法,对于网络
给定一个输入 $a$,返回对应的输出\footnote{这里假设输入 $a$ 是一个
\lstinline!(n,1)! 的 Numpy ndarray 类型,而不是一个 \lstinline!(n,)! 的向量。