声明
这是作者在CSDN上的第二篇博客,关于阅读塔里克·拉希德的著作《Python神经网络编程(Make Your Own Neural Network)》之后的读书笔记。跟诸位大牛相比,笔者阅历尚浅、经验不足,笔记中若有错误,还需继续修正与增删。欢迎大家的批评与指正。
以下文章是笔者整理出的关于神经网络构建的清晰方法步骤,欢迎大家阅读:How To Make Your Own Neural Network?——根据《Python神经网络编程(Make Your Own Neural Network)》一书整理。
Preface
神经网络是一种模拟人脑的神经网络,以期能够实现类人工智能的机器学习技术。
What?计算机Can&Cannot
- Can:简单的任务
- 使用燧石打火
- 使用滑轮吊起沉重的岩石
- 使用计算器做算术
- Cannot:自动化执行具有挑战性、相对复杂的任务
- 对相似的照片进行分组
- 从健康细胞中识别出病变细胞
- 来一盘国际象棋博弈
人工智能的机制,即通过复制生物大脑工作的机制来构建人工大脑:真正的大脑具有神经元,而不是逻辑门。真正的大脑具有更优雅更有机的推理,而不是冰冷的、非黑即白的、绝对的传统算法。
What?人工智能的误区
人工智能只不过是用更高级、更复杂的数学指令,告诉计算机怎么做,怎么模拟人类行为,让计算机“佯装”理解人类的感情。
Truth:无需太高深的数学思想,仅凭高中数学,就可以打造出一个专家级别的“神经网络”。
“人工智能时代存在一个人类价值体现方式变革的问题。”换句话说,如果我们依旧指望课本里的那些知识求生存,不求创新与探索,那么对知识掌握得再好,也只是拾人牙慧,只能湮没于滚滚的历史车轮之下。
What?本书写了什么
本书揭示了神经网络背后的概念,并介绍了如何通过Python实现神经网络。
全书分为3个章节以及2个附录:
- 第1章:神经网络中所用到的数学思想
- 第2章:使用Python实现神经网络,识别手写数字,并测试神经网络的性能
- 第3章:进一步了解简单的神经网络,观察已受训练的神经网络内部,尝试进一步改善神经网络的性能,加深相关知识的理解
- 附录A:简单的微积分知识
- 附录B:树莓派知识
How?本书是如何写的
- 通俗易懂:无需编程经验或超出中学的数学能力,“以让中学生理解的方式向我解释神经网络”(作者的前言中写道)
- 理论与实践结合
- 生动形象:形象的比喻与生动的图例
文章目录
- 声明
- Preface
- 第1章 神经网络如何工作
- 第二章 使用Python进行DIY
- 第三章 趣味盎然
- 两个附录(略)
- 笔者感受
第1章 神经网络如何工作
“从你身边所有的小事情中,找到灵感。”
1.1 尺有所短,寸有所长
计算机的核心是计算器。计算机缺乏的是“智能”。
PRO:机器所缺乏的
问题 | 计算机 | 人类 |
---|---|---|
快速地对成千上万的大数字进行乘法运算 | easy | hard |
在一大群人的照片中查找面孔 | hard | easy |
- 图像识别需要人工智能。
SOL:研究人工智能的任务
- 找到新方法或新算法,使用新的工作方式,尝试求解这类相对困难的问题
- 即使计算机不能完美解决问题,只要求计算机足够出色,给人们“这是智能在起作用”的印象即可
1.2 一台简单的预测机
神经网络中学习的核心过程是持续细化误差值。注意代数法和“改进法”的不同点,并明白为什么我们会选择看似不怎么聪明的“改进法”。
What?机器的样子
How?如何“计算”——以千米与英里的换算为例
DIF:“改进法”与代数法
- 代数法:
- 一步到位
- 精确求解
- DAD:很多问题没有一个简单的数学公式将输出和输入关联起来
- “改进法”:
- 持续细化误差值(神经网络中学习的核心过程)
- 迭代——持续地、多次改进答案
1.3 分类器与预测其并无太大差别
我们可以使用直线将不同性质的事物分开,唯一需要调整的是斜率,这就是简单的分类器。
How?在简单的预测器中,使用线性函数对先前未知的数据进行分类——以划分花园中的小虫为例
PRO:如何改进分界线
1.4 训练简单的分类器
使用朴素的调整方法会出现一个问题,即改进后的模型只与最后一次训练样本最匹配,“有效地”忽略了所有以前的训练样本。解决这个问题的的一种好方法是使用学习率。
- 训练数据:用来训练预测器或分类器的真实实例
- 误差值:期望目标值 - 实际输出值
- 计算过程:
分类器的线性函数: y = A x y=Ax y=Ax
正确的期望值t: t = ( A + Δ A ) x t=(A+\Delta A)x t=(A+ΔA)x
误差值E: E = t − y = ( A + Δ A ) x − A x = ( Δ A ) x E=t-y=(A+\Delta A)x-Ax=(\Delta A)x E=t−y=(A+ΔA)x−Ax=(ΔA)x
因此斜率调整量: Δ A = E / x \Delta A=E/x ΔA=E/x
P&S:朴素的调整方法
- PRO:
- 最终改进的直线与最后一次训练样本非常匹配,而不会顾及所有先前的训练样本。
- 即抛弃了所有先前训练样本的学习结果,只是对最近的一个实例进行了学习。
- SOL:
- 不要使改进过于激烈——适度改进
- 学习率(learning rate)L: Δ A = L ( E / x ) \Delta A=L(E/x) ΔA=L(E/x)
- 强大的“副作用”:当训练数据不一定正确以及在现实世界中存在错误或噪声,有节制的调整可以抑制错误或噪声的影响
1.5 有时候一个分类器不足以求解问题
如果数据本身不是由单一线性过程支配,那么一个简单的线性分类器不能对数据进行划分。解决方案是使用多个线性分类器来划分,这也是神经网络的核心思想。
What?线性分类器的局限性
神经网络的一个重要的设计特征来源于对这个局限性的理解。
- 布尔逻辑函数
二维:两个输入,一个输出
作用:在一些观察和另一些观察之间找到因果关系或相关关系- 形如y=ax+b的简单线性分类器可以学习布尔AND和OR函数
- 形如y=ax+b的简单线性分类器无法学习布尔XOR函数
- 在一些任务中,根本性问题不是线性可分的
SOL:使用多个分类器一起工作——神经网络的核心思想
1.6 神经元——大自然的计算机器
相比于传统的计算机系统,生物大脑对损坏和不完善的信号具有难以置信的弹性(即模糊性),这就是神经元的神奇之处,也是我们构建神经网络的生物模型。
How?传统计算机与动物大脑工作模式
- 传统计算机:
- 串行顺序
- 准确处理
- 无模糊性或不确定性
- 动物大脑:
- 并行处理
- 模糊性
What?神经元
- 特征:
神 经 元 ≠ 简 单 的 线 性 函 数 神经元\neq 简单的线性函数 神经元=简单的线性函数
- 神经元不会立即反应,而是会抑制输入,直到输入增强到可以触发输出——阈值(threshold)
- 比喻:杯子装水——>溢出
- DES:神经元不希望传递微小的噪声信号,而只是传递有意识的明显信号。
- 阶跃函数:
- S函数:
y = 1 1 + e − x y=\cfrac{1}{1+e^{-x}} y=1+e−x1
- 神经元的工作机制:
多 对 多 的 I / O 方 式 多对多的I/O方式 多对多的I/O方式
- 生理机制:
- 人造模型:
类比于调整斜率——>调整节点之间的连接强度(权重) - 带权重的模型:
- Why?前后层的每一个神经元均互相连接
- 一致的完全链接形式容易编码成计算机指令
- 神经网络的学习过程将会弱化这些实际上不需要的连接(权重趋近于0)
1.7 在神经网络中追踪信号
计算神经网络的输出值只需要简单的数学方法,然而随着层数以及节点数的增多,简单的计算会显得无比繁琐。
- 权重:神经网络进行学习的内容,权重持续进行优化,得到越来越好的结果。
P&S:从神经网络中得到输出值
- PRO:
对于具有多层,每层具有众多节点的网络,编写计算机指令依然枯燥 - SOL:
数学可以简化计算所有输出值的指令
1.8 凭心而论,矩阵乘法大有用处
通过神经网络向前馈送信号所需的大量运算可以表示为矩阵乘法。矩阵乘法又称为点乘(dot product)或内积(inner product),不管神经网络的规模如何,使用矩阵乘法可以更简洁地进行书写,并且允许计算机高速高效地进行计算。
AD:矩阵乘法
- 简化计算
- 计算机编程语言可以理解矩阵的工作方式
PS:使用矩阵乘法需注意
两个矩阵需要互相兼容:第一个矩阵中的列数目=第二个矩阵中的行数目。
W
⋅
I
=
X
W·I=X
W⋅I=X
W是权重矩阵,I是输入矩阵,X是输入到第二层的结果矩阵。
O
=
s
i
g
m
o
i
d
(
X
)
O=sigmoid(X)
O=sigmoid(X)
O是应用激活函数后的输出矩阵。
1.9 使用矩阵乘法的三层神经网络实例
三层神经网络的第一层为输入层,第二层为隐藏层,最后一层为输出层。
- 三层神经网络模型:
* 输入矩阵I:
I = [ 0.9 0.1 0.8 ] I=\begin{bmatrix} 0.9\\ 0.1\\ 0.8 \end{bmatrix} I=⎣⎡0.90.10.8⎦⎤ - 输入层到隐藏层的权重矩阵:
W i n p u t _ h i d d e n = [ 0.9 0.3 0.4 0.2 0.8 0.2 0.1 0.5 0.6 ] W_{input\_hidden}=\begin{bmatrix} 0.9 & 0.3 & 0.4\\ 0.2 & 0.8 & 0.2\\ 0.1 & 0.5 & 0.6 \end{bmatrix} Winput_hidden=⎣⎡0.90.20.10.30.80.50.40.20.6⎦⎤ - 隐藏层到输出层的权重矩阵:
W i n p u t _ h i d d e n = [ 0.3 0.7 0.5 0.6 0.5 0.2 0.8 0.1 0.9 ] W_{input\_hidden}=\begin{bmatrix} 0.3 & 0.7 & 0.5\\ 0.6 & 0.5 & 0.2\\ 0.8 & 0.1 & 0.9 \end{bmatrix} Winput_hidden=⎣⎡0.30.60.80.70.50.10.50.20.9⎦⎤ - 输入到隐藏层的组合调节输入矩阵:
X h i d d e n = W i n p u t _ h i d d e n ⋅ I = [ 0.9 0.3 0.4 0.2 0.8 0.2 0.1 0.5 0.6 ] ⋅ [ 0.9 0.1 0.8 ] = [ 1.16 0.2 0.62 ] X_{hidden}=W_{input\_hidden}·I=\begin{bmatrix} 0.9 & 0.3 & 0.4\\ 0.2 & 0.8 & 0.2\\ 0.1 & 0.5 & 0.6 \end{bmatrix}·\begin{bmatrix} 0.9\\ 0.1\\ 0.8 \end{bmatrix}=\begin{bmatrix} 1.16\\ 0.2\\ 0.62 \end{bmatrix} Xhidden=Winput_hidden⋅I=⎣⎡0.90.20.10.30.80.50.40.20.6⎦⎤⋅⎣⎡0.90.10.8⎦⎤=⎣⎡1.160.20.62⎦⎤ - 隐藏层输出矩阵:
O h i d d e n = s i g m o i d ( X h i d d e n ) = s i g m o i d [ 1.16 0.2 0.62 ] = [ 0.761 0.603 0.650 ] O_{hidden}=sigmoid(X_{hidden})=sigmoid\begin{bmatrix} 1.16\\ 0.2\\ 0.62 \end{bmatrix}=\begin{bmatrix} 0.761\\ 0.603\\ 0.650 \end{bmatrix} Ohidden=sigmoid(Xhidden)=sigmoid⎣⎡1.160.20.62⎦⎤=⎣⎡0.7610.6030.650⎦⎤ - 处理第三层信号的方法与处理第二层信号相同:
不管有多少层神经网络,我们都“一视同仁”,即:
- 组合输入信号;
- 应用链接权重调节这些输入信号;
- 应用激活函数生成这些层的输出信号。
NEXT:调整神经网络的方法
将神经网络的输出值与训练样本的输出值比较以计算出误差,使用误差值来调整神将网络——最重要的事情
1.10 学习来自多个节点的权重
在神经网络中,我们在两件事情上使用了权重。第一件事情,向前馈送信号;第二件事情,反向传播误差。
What?两种思想
- 等分误差
- 不等分误差:按权重比例分割误差
1.11 多个输出节点反向传播误差
需要使用误差来指导在网络内部如何调整链接权重。问题在于,在距离输出层相对较远的层中,我们如何更新链接权重?
1.12 反向传播误差到更多层中
训练样本数据只告诉我们最终输出节点的输出应该为多少,而没有告诉我们任何其他层节点的输出应该是多少。我们需要找到获取每一个节点误差的可行方法。
第一个隐藏层节点的误差是与这个节点前向连接所有链接中分割误差的和。输入层同理。
1.13 使用矩阵乘法进行反向传播误差
前向馈送信号和反向传播误差都可以使用矩阵计算而变得高效,其基本思想是:将过程矢量化。
- 输出层误差:
e r r o r o u t p u t = [ e 1 e 2 ] error_{output}=\begin{bmatrix} e_1\\ e_2 \end{bmatrix} erroroutput=[e1e2] - 隐藏层误差:
e r r o r h i d d e n = [ w 1 , 1 w 1 , 1 + w 2 , 1 w 1 , 2 w 1 , 2 + w 2 , 2 w 2 , 1 w 1 , 1 + w 2 , 1 w 2 , 2 w 1 , 2 + w 2 , 2 ] ⋅ [ e 1 e 2 ] error_{hidden}=\begin{bmatrix} \cfrac{w_{1,1}}{w_{1,1}+w_{2,1}} & \cfrac{w_{1,2}}{w_{1,2}+w_{2,2}}\\ \cfrac{w_{2,1}}{w_{1,1}+w_{2,1}} & \cfrac{w_{2,2}}{w_{1,2}+w_{2,2}} \end{bmatrix}·\begin{bmatrix} e_1\\ e_2 \end{bmatrix} errorhidden=⎣⎢⎢⎡w1,1+w2,1w1,1w1,1+w2,1w2,1w1,2+w2,2w1,2w1,2+w2,2w2,2⎦⎥⎥⎤⋅[e1e2]
这些分数的分母是一种归一化因子。如果忽略之,那么我们仅仅失去了后馈误差的大小,这是可以允许的。 - 切除归一化因子:
e r r o r h i d d e n = [ w 1 , 1 w 1 , 2 w 2 , 1 w 2 , 2 ] ⋅ [ e 1 e 2 ] error_{hidden}=\begin{bmatrix} w_{1,1} & w_{1,2}\\ w_{2,1} & w_{2,2} \end{bmatrix}·\begin{bmatrix} e_1\\ e_2 \end{bmatrix} errorhidden=[w1,1w2,1w1,2w2,2]⋅[e1e2] - 转置矩阵:
[ w 1 , 1 w 1 , 2 w 2 , 1 w 2 , 2 ] = W h i d d e n _ o u t p u t T \begin{bmatrix} w_{1,1} & w_{1,2}\\ w_{2,1} & w_{2,2} \end{bmatrix}=W_{hidden\_output}^T [w1,1w2,1w1,2w2,2]=Whidden_outputT - 更新后的隐藏层误差:
e r r o r h i d d e n = W h i d d e n _ o u t p u t T ⋅ e r r o r o u t p u t error_{hidden}=W_{hidden\_output}^T·error_{output} errorhidden=Whidden_outputT⋅erroroutput
1.14 我们实际上如何更新权重
本节的问题:如何调整输入层第一个节点和隐藏层第二个节点之间链路的权重,以使得输出层第三个节点的输出增加0.5呢?
PRO:我们不能使用微妙的代数直接计算出的权重
SOL:梯度下降(gradient descent)
梯度下降法是求解函数最小值一种很好的办法,当函数不能轻易使用数学代数求解时适用。在梯度下降法中,我们可以使用更小的步子朝着实际的最小值方向迈进,优化答案,直到我们对于所得到的精度感到满意为止。
- 正梯度意味着减小x,负梯度意味着增加x
- 具有弹性,可以容忍不完善的数据
What?误差函数的候选项
-
(
目
标
值
−
实
际
值
)
(目标值-实际值)
(目标值−实际值)
DAD:正负误差会相互抵消 -
∣
目
标
值
−
实
际
值
∣
|目标值-实际值|
∣目标值−实际值∣
DAD:由于斜率在最小值附近不是连续的,使得梯度下降方法无法很好地发挥作用,有超调的风险 - 我们的选择:
(
目
标
值
−
实
际
值
)
2
(目标值-实际值)^2
(目标值−实际值)2
AD:- 容易使用代数计算
- 平滑连续,没有间断
- 超调风险较小
- 当权重改变时,误差E如何改变: ∂ E ∂ w j , k \frac{\partial E}{\partial w_{j,k}} ∂wj,k∂E
- 节点误差=目标值-实际值 e k = t k − o k e_k=t_k-o_k ek=tk−ok
- 展开误差函数: ∂ E ∂ w j , k = ∂ ∂ w j , k ∑ n ( t n − o n ) 2 \frac{\partial E}{\partial w_{j,k}}=\frac{\partial}{\partial w_{j,k}}\sum_{n} (t_n-o_n)^2 ∂wj,k∂E=∂wj,k∂n∑(tn−on)2
- 事实上节点的输出只取决于所连接的链接: ∂ E ∂ w j , k = ∂ ∂ w j , k ( t k − o k ) 2 \frac{\partial E}{\partial w_{j,k}}=\frac{\partial}{\partial w_{j,k}} (t_k-o_k)^2 ∂wj,k∂E=∂wj,k∂(tk−ok)2
- 使用链式法则: ∂ E ∂ w j , k = ∂ E ∂ o k ⋅ ∂ o k ∂ w j , k \frac{\partial E}{\partial w_{j,k}}=\frac{\partial E}{\partial o_k}·\frac{\partial o_k}{\partial w_{j,k}} ∂wj,k∂E=∂ok∂E⋅∂wj,k∂ok
- 进行展开: ∂ E ∂ w j , k = − 2 ( t k − o k ) ⋅ ∂ o k ∂ w j , k = − 2 ( t k − o k ) ⋅ ∂ ∂ w j , k s i g m o i d ( ∑ j w j , k ⋅ o j ) \frac{\partial E}{\partial w_{j,k}}=-2(t_k-o_k)·\frac{\partial o_k}{\partial w_{j,k}}=-2(t_k-o_k)·\frac{\partial}{\partial w_{j,k}}sigmoid(\sum_{j}w_{j,k}·o_j) ∂wj,k∂E=−2(tk−ok)⋅∂wj,k∂ok=−2(tk−ok)⋅∂wj,k∂sigmoid(j∑wj,k⋅oj)
- 微分S函数: ∂ ∂ x s i g m o i d ( x ) = s i g m o i d ( x ) ( 1 − s i g m o i d ( x ) ) \frac{\partial}{\partial x}sigmoid(x)=sigmoid(x)(1-sigmoid(x)) ∂x∂sigmoid(x)=sigmoid(x)(1−sigmoid(x))
- 继续微分: ∂ E ∂ w j , k = − 2 ( t k − o k ) ⋅ s i g m o i d ( ∑ j w j , k ⋅ o j ) ( 1 − s i g m o i d ( ∑ j w j , k ⋅ o j ) ) ⋅ ∂ ∂ w j , k ( ∑ j w j , k ⋅ o j ) = − 2 ( t k − o k ) ⋅ s i g m o i d ( ∑ j w j , k ⋅ o j ) ( 1 − s i g m o i d ( ∑ j w j , k ⋅ o j ) ) ⋅ o j \frac{\partial E}{\partial w_{j,k}}=-2(t_k-o_k)·sigmoid(\sum_{j}w_{j,k}·o_j)(1-sigmoid(\sum_{j}w_{j,k}·o_j))·\frac{\partial}{\partial w_{j,k}}(\sum_{j}w_{j,k}·o_j)\\=-2(t_k-o_k)·sigmoid(\sum_{j}w_{j,k}·o_j)(1-sigmoid(\sum_{j}w_{j,k}·o_j))·o_j ∂wj,k∂E=−2(tk−ok)⋅sigmoid(j∑wj,k⋅oj)(1−sigmoid(j∑wj,k⋅oj))⋅∂wj,k∂(j∑wj,k⋅oj)=−2(tk−ok)⋅sigmoid(j∑wj,k⋅oj)(1−sigmoid(j∑wj,k⋅oj))⋅oj
- 答案(隐藏层到输出层):
∂
E
∂
w
j
,
k
=
−
(
t
k
−
o
k
)
⋅
s
i
g
m
o
i
d
(
∑
j
w
j
,
k
⋅
o
j
)
(
1
−
s
i
g
m
o
i
d
(
∑
j
w
j
,
k
⋅
o
j
)
)
⋅
o
j
\frac{\partial E}{\partial w_{j,k}}=-(t_k-o_k)·sigmoid(\sum_{j}w_{j,k}·o_j)(1-sigmoid(\sum_{j}w_{j,k}·o_j))·o_j
∂wj,k∂E=−(tk−ok)⋅sigmoid(j∑wj,k⋅oj)(1−sigmoid(j∑wj,k⋅oj))⋅oj
分析:- 第一部分:(目标值-实际值)
- 第二部分:sigmoid中的求和表达式,即进入最后一层节点的信号
- 第三部分:前一隐藏层节点j的输出
- 类比(输入层到隐藏层): ∂ E ∂ w j , k = − e j ⋅ s i g m o i d ( ∑ i w i , j ⋅ o i ) ( 1 − s i g m o i d ( ∑ i w i , j ⋅ o i ) ) ⋅ o i \frac{\partial E}{\partial w_{j,k}}=-e_j·sigmoid(\sum_{i}w_{i,j}·o_i)(1-sigmoid(\sum_{i}w_{i,j}·o_i))·o_i ∂wj,k∂E=−ej⋅sigmoid(i∑wi,j⋅oi)(1−sigmoid(i∑wi,j⋅oi))⋅oi
- 注意:
- 权重改变的方向与梯度方向相反
- 建立线性分类器,避免被错误地训练样本拉得太远,保证权重不会由于持续的超调而在最小值附近来回摆动
- 更新后的权重: n e w w j , k = o l d w j , k − α ⋅ ∂ E ∂ w j , k , α 是 学 习 率 new\ w_{j,k}=old\ w_{j,k}- \alpha ·\frac{\partial E}{\partial w_{j,k}},\ \alpha是学习率 new wj,k=old wj,k−α⋅∂wj,k∂E, α是学习率
- 矩阵形式:
Δ
w
j
,
k
=
α
∗
E
k
∗
O
k
∗
(
1
−
O
k
)
⋅
O
j
T
\Delta w_{j,k}=\alpha *E_k*O_k*(1-O_k)·O_j^T
Δwj,k=α∗Ek∗Ok∗(1−Ok)⋅OjT
表达式中第一项是学习率,第二项是误差矩阵,第三项使用下一层的值,最后一项使用前一层的值。
1.15 权重更新成功范例
虽然一次更新只是一个相当小的变化量,但权重经过成百上千次的迭代,最终会确定下来,达到一种布局,这样训练有素的神经网络就会生成与训练样本中相同的输出。
1.16 准备数据
并不是所有使用神经网络的尝试都能够成功。但我们可以通过改进训练数据、初始权重、设计良好的输出方案来解决。
1. 输入值:
- 权重的改变取决于激活函数的梯度。小梯度意味着限制神经网络学习的能力。(饱和神经网络)
因此:尽量保持小的输入。
- 表达式取决于输入信号,因此:不应该让输入信号太小。
- SOL:重新调整输入值,将其范围控制在0.0到1.0(输入0会造成学习能力的丧失,因此将输入加上一个小小的偏移,如0.01)。
2. 输出值:
- 逻辑激活函数的输出值不可能大于1.0,小于0。
如果我们将目标值设置在不能达到的范围,训练网络将会驱使更大的权重,以获得越来越大的输出,导致网络饱和。 - SOL:重新调整目标值,匹配激活函数的可能输出。常见的使用范围是0.0~1.0。
(实际上是0.01~0.99)
3. 随机初始权重:
- 大的初始权重会造成大的信号传递给激活函数,导致网络饱和,因此:避免大的初始权重值。
- 可以从-1.0~1.0随机均匀地选择初始权重。
- 更好的选择:在一个节点传入链接数量平方根倒数的大致范围内随机采样,初始化权重 − 1 ( 传 入 链 接 ) ∼ + 1 ( 传 入 链 接 ) \frac{-1}{\sqrt{(传入链接)}} \thicksim \frac{+1}{\sqrt{(传入链接)}} (传入链接)−1∼(传入链接)+1
- 禁止将初始权重设定为相同的恒定值(正确训练的网络应该具有不等的权重)
- 禁止0权重(网络会完全丧失更新权重的能力)
第二章 使用Python进行DIY
“纸上得来终觉浅,绝知此事须躬行。”
“不积跬步,无以至千里。”
2.1 Python
Python是一种简单易学的计算机语言,是一种合适的入门语言。
2.2 交互式Python=IPython
我们将使用一个预先打包的解决方案,称为IPython。IPython中包含了Python编程语言以及几种常见的数字和数据绘图扩展包,这包括了我们需要的工具。IPython有一个显著的优势,如交互式Notebook。
2.3 优雅地开始使用Python
写出Python代码的平台——Notebook;
神经网络快速运转的基础——自动化工作;
代码的“解释师”——注释;
可重用的计算机指令——函数;
数学矩阵的超强载体——数组;
神经网络解决图像识别问题的基础——绘制数组;
神经网络的主体躯干——对象。
2.3.1 Notebook
Notebook是交互式的,这意味着它等待你提出要求,提出要做的事情,然后,Notebook执行这些命令,并在Notebook中给出答案。如果你想做的事情相对复杂,那么将这个问题分解为几个部分比较合理。对于IPython而言,我们称这些部分为单元格(cell)。
2.3.2 简单的Python
1. 简单的打印出短语“Hello World!”
- In:
print("Hello World!")
- Out:
Hello World!
2. 变量声明与简单计算
- In:
x = 10
print(x)
print(x + 5)
y = x + 7
print(y)
print(z)
- Out:
10
15
17
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
<ipython-input-5-7053aacdac4c> in <module>
6 print(y)
7
----> 8 print(z)
NameError: name 'z' is not defined
2.3.3 自动化工作
计算机的潜力在于,只使用一组很短的指令就快速地执行大量的工作。
这是神经网络得以快速运转的基础。
3. 数字列表
- In:
list(range(10))
- Out:
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
注:当我们以交互的方式与Python一起工作时,关键字"print"是可选的。
4. 循环1
- In:
for n in range(10):
print("The square of",n, "is", n * n)
pass
print("done")
- Out:
The square of 0 is 0
The square of 1 is 1
The square of 2 is 4
The square of 3 is 9
The square of 4 is 16
The square of 5 is 25
The square of 6 is 36
The square of 7 is 49
The square of 8 is 64
The square of 9 is 81
done
注:在Python中,缩进的使用是有意识地显示哪些指令在其他指令的管辖之下,因此缩进很重要。
2.3.4 注释
有意义的代码注释可以使得代码的含义更加清晰。
5. 使用注释
- In:
# 下面输出的是2的立方数
print(2 ** 3)
- Out:
8
2.3.5 函数
包括Python在内,计算机语言都尽量使得创建可重用的计算机指令变得容易。这就用到了函数。
6. 求两个数字平均数的函数
- In:
# 函数:接受两个数字作为输入,输出它们的平均数
def avg(x, y):
print("第一个输入值是", x)
print("第二个输入值是", y)
a = (x + y) / 2.0
print("平均数是", a)
return a
- Out(无输出,只定义未使用函数):
Python的优点:
一些计算机语言可能要让你明确这是什么类型的对象,但是Python不会;Python只会在你试图滥用变量时报错。
7. 调用函数(calling a function)
- In:
avg(2, 4)
- Out:
第一个输入值是 2
第二个输入值是 4
平均数是 3.0
3.0
2.3.6 数组
数组只是数值表格,非常便于使用。就像表格一样,可以根据行数和列数来指示特定的单元。
当我们要编码神经网络时,将使用数组来表示输入信号、权重、输出信号的矩阵,以及在信号前馈或误差反向传播时的信号和误差矩阵。
8. 导入numpy模块2
- In:
import numpy
- Out:
9. 创建3*2的零数组3
- In:
a = numpy.zeros([3, 2])
print(a)
- Out:
[[0. 0.]
[0. 0.]
[0. 0.]]
10. 指定特定单元,使用新值覆盖旧值
- In:
a[0, 0] = 1
a[0, 1] = 2
a[1, 0] = 9
a[2, 1] = 12
print(a)
- Out:
[[ 1. 2.]
[ 9. 0.]
[ 0. 12.]]
11. 直接查找数组单元格的值
- In:
print(a[0, 1])
v = a[1, 0]
print(v)
- Out:
2.0
9.0
2.3.7 绘制数组
可视化数组有助于我们快速获取数组的一般意义。绘制二维数字数组的一种方式是将它们视为二维平面,根据数组中单元格的值对单元格进行着色。
绘制数组是神经网络解决图像识别问题(如手写数字识别)的基础。
12. 导入图形绘制功能4
- In:
import matplotlib.pyplot
- Out:
- In5:
%matplotlib inline
- Out:
13. 绘制数组6
具有相同值的数组单元颜色也相同。
- In:
matplotlib.pyplot.imshow(a, interpolation = "nearest")
- Out:
注:interpolation参数:告诉Python不要为了让绘图看起来更加平滑而混合颜色(缺省设置)。
2.3.8 对象
对象类似于可重用函数,但相比简单的函数,对象可以做的事情要多得多。对象是类的实例。对象函数被称为方法(method)。
神经网络需要接受某些输入,进行一些计算并产生输出,同时我们可以训练神经网络。这些动作、训练和生成的答案,是神经网络的原生函数,即神经网络对象的函数。
神经网络内部有数据,也就是链接权重,这些数据是属于神经网络的。这就是我们把神经网络构建为对象的原因。
14. 类与对象
- In:
# class for a dog object
class Dog:
# 内部数据的初始化方法
def __init__(self, petname, temp):
self.name = petname
self.temperature = temp
# 获得当前状态
def status(self):
print("dog name is", self.name)
print("dog temperature is", self.temperature)
pass
# 设置温度
def setTemperature(self, temp):
self.temperature = temp
pass
# 狗可以叫
def bark(self):
print("woof!")
pass
pass
- Out:
类和对象之间的区别:
类只是定义,对象是所定义类的真正实例。(菜谱与菜品)
- In:
# 从狗class创造一个新的狗object
lassie = Dog('Lassie', 37)
lassie.status()
lassie.setTemperature(40)
lassie.status()
- Out:
dog name is Lassie
dog temperature is 37
dog name is Lassie
dog temperature is 40
2.4 使用Python制作神经网络
本节的任务是制作一个3层神经网络,完成对其类(class)的定义。
2.4.1 框架代码
神经网络的三个函数:
- 初始化函数
- 训练
- 查询
2.4.2 初始化网络
- 设置输入层、隐藏层和输出层节点的数量(不固定)
- 尽力创建一般代码
神经网络的四个参数:
- 输入层节点数
- 隐藏层节点数
- 输出层节点数
- 学习率
2.4.3 权重——网络的核心
下一步是创建网络的节点和链接。网络中最重要的部分是链接权重,我们使用权重计算前馈信号、反向传播误差,并且在试图改进网络时优化链接权重本身。
简单的初始随机权重的确定:
- numpy.random.rand()函数7(0~1)
- 将上面数组中每个值减去0.5(-0.5~0.5)
2.4.4 可选项:较复杂的权重
比较复杂的初始随机权重的确定(正态概率分布)8:
平均值为0,标准方差为节点传入链接数目的开方,即
1
传
入
链
接
数
目
\cfrac{1}{\sqrt{传入链接数目}}
传入链接数目1
2.4.5 查询网络
先编写简单的查询函数。
- 简单简洁的矩阵形式9
- S抑制函数10
注:由于我们可能希望实验、调整甚至完全改变激活函数,因此当神经网络对象初始化时,在神经网络对象内部只定义一次S函数。11
2.4.6 迄今为止的代码12
在训练神经网络的过程中有两个阶段:第一阶段是计算输出(如同query()所做的事情),第二阶段是反向传播误差,告知如何优化链接权重。我们已经完成了第一阶段的任务。
2.4.7 训练网络
我们即将接近神经网络工作的核心,即基于所计算输出与目标输出之间的误差,改进权重。
两个部分:
- 针对给定的训练样本计算输出(与query()函数中几乎相同)
- 将计算得到的输出与所需输出对比,使用差值指导网络权重的更新
1)计算误差
2)计算隐藏层节点反向传播的误差
3)优化各层之间的权重
注:*乘法是正常的对应元素的乘法,·点乘是矩阵点积。
2.4.8 完整的神经网络代码
当前代码可用于创建、训练和查询3层神经网络,进行几乎任何任务。
下一步将进行特定任务,学习识别手写数字。
# 神经网络class定义
class neuralNetwork():
# 初始化神经网络
def __init__(self, inputnodes, hiddennodes, outputnodes, learningrate):
# 设置输入、隐藏、输出层的节点数
self.inodes = inputnodes
self.hnodes = hiddennodes
self.onodes = outputnodes
# 链接权重矩阵,wih与who
# 矩形数组中的权重是w_i_j,即从i节点到下一层的j节点的链接
# w11 w21
# w12 w22 etx
self.wih = numpy.random.normal(0.0, pow(self.hnodes, -0.5), (self.hnodes, self.inodes))
self.who = numpy.random.normal(0.0, pow(self.onodes, -0.5), (self.onodes, self.hnodes))
# 学习率
self.lr = learningrate
# 激活函数是sigmoid函数(S函数)
self.activation_function = lambda x: scipy.special.expit(x)
pass
# 对神经网络进行训练
def train(self, inputs_list, targets_list):
# 将inputs_list转换为2维数组
inputs = numpy.array(inputs_list, ndmin = 2).T
targets = numpy.array(targets_list, ndmin = 2).T
# 计算进入隐藏层的信号
hidden_inputs = numpy.dot(self.wih, inputs)
# 计算隐藏层发出的信号
hidden_outputs = self.activation_function(hidden_inputs)
# 计算进入输出层的信号
final_inputs = numpy.dot(self.who, hidden_outputs)
# 计算输出层发出的信号
final_outputs = self.activation_function(final_inputs)
# 输出层误差为(target - actual)
output_errors = targets - final_outputs
# 隐藏层误差为按权重比例分割输出层误差后,在隐藏层节点的重组值
hidden_errors = numpy.dot(self.who.T, output_errors)
# 更新隐藏层与输出层之间链接的权重
self.who += self.lr * numpy.dot((output_errors * final_outputs * (1.0 - final_outputs)), numpy.transpose(hidden_outputs))
# 更新输入层与隐藏层之间链接的权重
self.wih += self.lr * numpy.dot((hidden_errors * hidden_outputs * (1.0 - hidden_outputs)), numpy.transpose(inputs))
pass
# 对神经网络进行查询
def query(self, inputs_list):
# 将inputs_list转换为2维数组
inputs = numpy.array(inputs_list, ndmin = 2).T
# 计算进入隐藏层的信号
hidden_inputs = numpy.dot(self.wih, inputs)
# 计算隐藏层发出的信号
hidden_outputs = self.activation_function(hidden_inputs)
# 计算进入输出层的信号
final_inputs = numpy.dot(self.who, hidden_outputs)
# 计算输出层发出的信号
final_outputs = self.activation_function(final_inputs)
return final_outputs
2.5 手写数字的数据集MNIST
让计算机准确区分图像中包含的内容,称之为图像识别问题。
- 训练集:
用来训练神经网络的若干个标记样本集。标记是指输入与期望的输出匹配,也就是答案应该是多少。 - 基本思想:
将训练和测试数据集分开。 - 数据集的内容:
第一个值是标签,即书写者希望表示的数字;
随后的值,由逗号分隔,是手写体数字的像素值。
- 打开文件并获取其中的内容
- 使用imshow()函数绘制数字矩形数组13
注:cmap="Greys"参数:选择灰度调色板,以更好地显示手写字符。
2.5.1 准备MNIST训练数据
- 第一件事情:
将输入颜色值从较大的0到255的范围,缩放至较小的0.01到1.0的范围。 - 神经网络的输出:
神经网络应该有10个输出层节点,每个节点对应一个可能的答案或标签。(激发态与抑制态)
用0.01和0.99代替0和1。 - 隐藏层节点数的确定:
- 选择使用比输入节点的数量小的值,强制网络尝试总结输入的主要特点;
- 选择太少的隐藏层节点,就限制了网络的能力,使网络难以找到足够的特征或模式,剥夺了网络表达其对MNIST数据理解的能力;
- 选择多少个隐藏层节点,不存在一个最佳方法。
- SOL:不断实验
2.5.2 测试网络
神经网络能够正确区分它从来没有见过的手写字符,这说明我们的实验成功了。
- 计分卡(scorecard)
- 网络获得答案(索引值)14
2.5.3 使用完整数据集进行训练和测试
此时改变文件名,这样就可以指向具有60000条记录的完整的训练数据集,以及具有10000条记录的测试数据集。这有效提升了神经网络的性能。
2.5.4 一些改进:调整学习率
学习率的甜蜜点:0.2
2.5.5 一些改进:多次运行
- 世代:训练一次称为一个世代。
训练次数的甜蜜点:5或7个世代
注:在更多世代的情况下,减小学习率能够得到更好的性能。
当前神经网络的甜蜜点:5个世代,0.1学习率
2.5.6 改变网络形状
隐藏层是学习发生的场所,隐藏层节点前后的链接权重具有学习能力。
- 隐藏层节点太少——学习容量小
- 隐藏层节点过多——难以训练网络
隐藏层节点数的甜蜜点:200
注:随着增加隐藏层节点的数量,结果有所改善,但不显著。明显的缺点是:训练网络所用的时间显著增加了。必须在可容忍的运行时间内选择某个数目的隐藏层节点。
2.5.7 大功告成
2.5.8 最终代码
# python notebook for Make Your Own Neural Network
# 代码用于一个3层的神经网络,以及学习手写数字识别的数据集
# Xiangtan, 2019/9/26
import numpy
# 导入scipy.special模块以使用sigmoid函数(S函数)expit()
import scipy.special
# 绘制数字矩形数组的库
import matplotlib.pyplot
# 确保绘制的图像在这个notebook内部,而不是外部窗口
%matplotlib inline
# 神经网络class定义
class neuralNetwork():
# 初始化神经网络
def __init__(self, inputnodes, hiddennodes, outputnodes, learningrate):
# 设置输入、隐藏、输出层的节点数
self.inodes = inputnodes
self.hnodes = hiddennodes
self.onodes = outputnodes
# 链接权重矩阵,wih与who
# 矩形数组中的权重是w_i_j,即从i节点到下一层的j节点的链接
# w11 w21
# w12 w22 etx
self.wih = numpy.random.normal(0.0, pow(self.hnodes, -0.5), (self.hnodes, self.inodes))
self.who = numpy.random.normal(0.0, pow(self.onodes, -0.5), (self.onodes, self.hnodes))
# 学习率
self.lr = learningrate
# 激活函数是sigmoid函数(S函数)
self.activation_function = lambda x: scipy.special.expit(x)
pass
# 对神经网络进行训练
def train(self, inputs_list, targets_list):
# 将inputs_list转换为2维数组
inputs = numpy.array(inputs_list, ndmin = 2).T
targets = numpy.array(targets_list, ndmin = 2).T
# 计算进入隐藏层的信号
hidden_inputs = numpy.dot(self.wih, inputs)
# 计算隐藏层发出的信号
hidden_outputs = self.activation_function(hidden_inputs)
# 计算进入输出层的信号
final_inputs = numpy.dot(self.who, hidden_outputs)
# 计算输出层发出的信号
final_outputs = self.activation_function(final_inputs)
# 输出层误差为(target - actual)
output_errors = targets - final_outputs
# 隐藏层误差为按权重比例分割输出层误差后,在隐藏层节点的重组值
hidden_errors = numpy.dot(self.who.T, output_errors)
# 更新隐藏层与输出层之间链接的权重
self.who += self.lr * numpy.dot((output_errors * final_outputs * (1.0 - final_outputs)), numpy.transpose(hidden_outputs))
# 更新输入层与隐藏层之间链接的权重
self.wih += self.lr * numpy.dot((hidden_errors * hidden_outputs * (1.0 - hidden_outputs)), numpy.transpose(inputs))
pass
# 对神经网络进行查询
def query(self, inputs_list):
# 将inputs_list转换为2维数组
inputs = numpy.array(inputs_list, ndmin = 2).T
# 计算进入隐藏层的信号
hidden_inputs = numpy.dot(self.wih, inputs)
# 计算隐藏层发出的信号
hidden_outputs = self.activation_function(hidden_inputs)
# 计算进入输出层的信号
final_inputs = numpy.dot(self.who, hidden_outputs)
# 计算输出层发出的信号
final_outputs = self.activation_function(final_inputs)
return final_outputs
# 输入、隐藏、输出层的节点数
input_nodes = 784
hidden_nodes = 200
output_nodes = 10
# 学习率
learning_rate = 0.1
# 创建神经网络的一个实例
n = neuralNetwork(input_nodes, hidden_nodes, output_nodes, learning_rate)
# 加载书写数字识别的训练数据CSV文件为一个列表
training_data_file = open(r"E:\Neural Network\mnist_dataset\mnist_train.csv", 'r')
training_data_list = training_data_file.readlines()
training_data_file.close()
# 训练神经网络
# 世代是指训练数据集被用来测试的次数
epochs = 5
for e in range(epochs):
# 遍历训练数据集中的所有记录
for record in training_data_list:
# 在','逗号处分割记录
all_values = record.split(',')
# 对输入值进行缩放和移位
inputs = (numpy.asfarray(all_values[1:]) / 255.0 * 0.99) + 0.01
# 创建目标输出值(除了期望值标签对应的是0.99,全部为0.01)
targets = numpy.zeros(output_nodes) + 0.01
# all_values[0]是此记录的目标标签
targets[int(all_values[0])] = 0.99
n.train(inputs, targets)
pass
pass
# 加载书写数字识别的测试数据CSV文件为一个列表
test_data_file = open(r"E:\Neural Network\mnist_dataset\mnist_test.csv", 'r')
test_data_list = test_data_file.readlines()
test_data_file.close()
# 测试神经网络
# 评价网络运转的良好程度的计分板,初始化为空
scorecard = []
# 遍历测试数据集中的所有记录
for record in test_data_list:
# 在','逗号处分割记录
all_values = record.split(',')
# 正确答案是第一个值
correct_label = int(all_values[0])
# print(correct_label, "correct label")
# 对输入值进行缩放和移位
inputs = (numpy.asfarray(all_values[1:]) / 255.0 * 0.99) + 0.01
# 查询网络
outputs = n.query(inputs)
# 最大值的索引对应的标签
label = numpy.argmax(outputs)
# print(label, "network's answer")
# 添加正确(1)或不正确(0)到列表中
if (label == correct_label):
# 网络的答案匹配正确的答案,计分板加1
scorecard.append(1)
else:
# 网络的答案不匹配正确的答案,计分板加0
scorecard.append(0)
pass
pass
# 计算性能分数,即正确答案的得分
scorecard_array = numpy.asarray(scorecard)
print("performance =", scorecard_array.sum() / scorecard_array.size)
运行结果:
performance = 0.9755
第三章 趣味盎然
“寓教于乐。”
3.1 自己的手写数字
不使用MNIST测试集对网络进行测试,而是使用自己创建的图像数据对网络进行测试。
神经网络将它们所学到的知识分布在几条链接权重上:
- 如果若干链接权重遭受了一定损害,神经网络也可以表现得相当好。
- 如果输入图像被损坏或不完整,神经网络也可以表现得相当好。
方法步骤:
- 从图像文件(PNG或JPG)中读取数据15
- MNIST数据集黑色与白色的灰度值完全相反——逆转值
- 缩放数据值(0.01到1.0)
代码:
# python notebook for Make Your Own Neural Network
# 代码用于一个3层的神经网络,以及学习手写数字识别的数据集
# 此版本用书写数字识别的数据集进行训练,然后测试我们自己的手写数字图像
# Xiangtan, 2019/9/28
# 从PNG图像文件中加载数据的助手
import imageio
# glob帮助你使用模式匹配选择多个文件
import glob
import numpy
# 导入scipy.special模块以使用sigmoid函数(S函数)expit()
import scipy.special
# 绘制数字矩形数组的库
import matplotlib.pyplot
# 确保绘制的图像在这个notebook内部,而不是外部窗口
%matplotlib inline
# 神经网络class定义
class neuralNetwork():
# 初始化神经网络
def __init__(self, inputnodes, hiddennodes, outputnodes, learningrate):
# 设置输入、隐藏、输出层的节点数
self.inodes = inputnodes
self.hnodes = hiddennodes
self.onodes = outputnodes
# 链接权重矩阵,wih与who
# 矩形数组中的权重是w_i_j,即从i节点到下一层的j节点的链接
# w11 w21
# w12 w22 etx
self.wih = numpy.random.normal(0.0, pow(self.hnodes, -0.5), (self.hnodes, self.inodes))
self.who = numpy.random.normal(0.0, pow(self.onodes, -0.5), (self.onodes, self.hnodes))
# 学习率
self.lr = learningrate
# 激活函数是sigmoid函数(S函数)
self.activation_function = lambda x: scipy.special.expit(x)
pass
# 对神经网络进行训练
def train(self, inputs_list, targets_list):
# 将inputs_list转换为2维数组
inputs = numpy.array(inputs_list, ndmin = 2).T
targets = numpy.array(targets_list, ndmin = 2).T
# 计算进入隐藏层的信号
hidden_inputs = numpy.dot(self.wih, inputs)
# 计算隐藏层发出的信号
hidden_outputs = self.activation_function(hidden_inputs)
# 计算进入输出层的信号
final_inputs = numpy.dot(self.who, hidden_outputs)
# 计算输出层发出的信号
final_outputs = self.activation_function(final_inputs)
# 输出层误差为(target - actual)
output_errors = targets - final_outputs
# 隐藏层误差为按权重比例分割输出层误差后,在隐藏层节点的重组值
hidden_errors = numpy.dot(self.who.T, output_errors)
# 更新隐藏层与输出层之间链接的权重
self.who += self.lr * numpy.dot((output_errors * final_outputs * (1.0 - final_outputs)), numpy.transpose(hidden_outputs))
# 更新输入层与隐藏层之间链接的权重
self.wih += self.lr * numpy.dot((hidden_errors * hidden_outputs * (1.0 - hidden_outputs)), numpy.transpose(inputs))
pass
# 对神经网络进行查询
def query(self, inputs_list):
# 将inputs_list转换为2维数组
inputs = numpy.array(inputs_list, ndmin = 2).T
# 计算进入隐藏层的信号
hidden_inputs = numpy.dot(self.wih, inputs)
# 计算隐藏层发出的信号
hidden_outputs = self.activation_function(hidden_inputs)
# 计算进入输出层的信号
final_inputs = numpy.dot(self.who, hidden_outputs)
# 计算输出层发出的信号
final_outputs = self.activation_function(final_inputs)
return final_outputs
# 输入、隐藏、输出层的节点数
input_nodes = 784
hidden_nodes = 200
output_nodes = 10
# 学习率
learning_rate = 0.1
# 创建神经网络的一个实例
n = neuralNetwork(input_nodes, hidden_nodes, output_nodes, learning_rate)
# 加载书写数字识别的训练数据CSV文件为一个列表
training_data_file = open(r"E:\Neural Network\mnist_dataset\mnist_train.csv", 'r')
training_data_list = training_data_file.readlines()
training_data_file.close()
# 训练神经网络
# 世代是指训练数据集被用来测试的次数
epochs = 5
for e in range(epochs):
# 遍历训练数据集中的所有记录
for record in training_data_list:
# 在','逗号处分割记录
all_values = record.split(',')
# 对输入值进行缩放和移位
inputs = (numpy.asfarray(all_values[1:]) / 255.0 * 0.99) + 0.01
# 创建目标输出值(除了期望值标签对应的是0.99,全部为0.01)
targets = numpy.zeros(output_nodes) + 0.01
# all_values[0]是此记录的目标标签
targets[int(all_values[0])] = 0.99
n.train(inputs, targets)
pass
pass
# 我们自己的图像测试数据集
our_own_dataset = []
# 加载PNG图像数据为测试数据集
for image_file_name in glob.glob(r'E:\Neural Network\mnist_dataset\test_my_own_*.png'):
print("loading ...", image_file_name)
# 使用文件名来设置正确的标签
label = int(image_file_name[-5:-4])
# 从PNG文件中加载数据为一个数组
img_array = imageio.imread(image_file_name, as_gray = True)
# 重塑数组,从28x28的方块数组变成很长的一串784个数值,将值取反
img_data = 255.0 - img_array.reshape(784)
# 然后将图像数据缩放到0.01与1.0之间
img_data = (img_data / 255.0 * 0.99) + 0.01
print(numpy.min(img_data))
print(numpy.max(img_data))
# 将标签和图像数据加入测试数据集
record = numpy.append(label, img_data)
our_own_dataset.append(record)
pass
# 用我们自己的图像测试神经网络
# 计分板,初始化为空
scorecard = []
# 遍历测试数据集中的所有记录
for record in our_own_dataset:
# 正确答案是第一个值
correct_label = int(record[0])
print(correct_label, "correct label")
# 数据为剩余的值
inputs = record[1:]
# 查询网络
outputs = n.query(inputs)
# 最大值的索引对应标签
label = numpy.argmax(outputs)
print(label, "network's answer")
# 添加正确(1)或不正确(0)到列表中
if (label == correct_label):
print("match!")
scorecard.append(1)
else:
print("no match!")
scorecard.append(0)
pass
pass
print(scorecard)
# 计算性能分数,即正确答案的得分
scorecard_array = numpy.asarray(scorecard)
print("performance =", scorecard_array.sum() / scorecard_array.size)
运行结果:
loading ... E:\Neural Network\mnist_dataset\test_my_own_2.png
0.01
1.0
loading ... E:\Neural Network\mnist_dataset\test_my_own_3.png
0.01
1.0
loading ... E:\Neural Network\mnist_dataset\test_my_own_4.png
0.01
0.93011767
loading ... E:\Neural Network\mnist_dataset\test_my_own_5.png
0.01
0.86800003
loading ... E:\Neural Network\mnist_dataset\test_my_own_6.png
0.01
1.0
loading ... E:\Neural Network\mnist_dataset\test_my_own_noisy_6.png
0.14588237
0.77482355
2 correct label
3 network's answer
no match!
3 correct label
3 network's answer
match!
4 correct label
4 network's answer
match!
5 correct label
5 network's answer
match!
6 correct label
6 network's answer
match!
6 correct label
6 network's answer
match!
[0, 1, 1, 1, 1, 1]
performance = 0.8333333333333334
3.2 神经网络大脑内部
本节问题:在神经网络的“眼中”,图像应该是怎样的?
3.2.1 神秘的黑盒子
虽然黑盒子(神经网络)已经学会如何求解问题,但是其所学习到的知识常常不能转化为对问题的理解和智慧。
3.2.2 向后查询
馈送一个标签到输出节点,通过已受训练的网络反向输入信号,直到输入节点弹出一个图像,这就是向后查询。
- 逆激活函数(对数函数)10: x = l n ( y 1 − y ) x=ln(\frac{y}{1-y}) x=ln(1−yy)
- 代码:
# python notebook for Make Your Own Neural Network
# 代码用于一个3层的神经网络,以及学习手写数字识别的数据集
# 此版本询问神经网络图像应该是怎样的,给定一个标签
# Xiangtan, 2019/9/28
import numpy
# 导入scipy.special模块以使用sigmoid函数(S函数)expit()和它的逆函数logit()
import scipy.special
# 绘制数字矩形数组的库
import matplotlib.pyplot
# 确保绘制的图像在这个notebook内部,而不是外部窗口
%matplotlib inline
# 神经网络class定义
class neuralNetwork():
# 初始化神经网络
def __init__(self, inputnodes, hiddennodes, outputnodes, learningrate):
# 设置输入、隐藏、输出层的节点数
self.inodes = inputnodes
self.hnodes = hiddennodes
self.onodes = outputnodes
# 链接权重矩阵,wih与who
# 矩形数组中的权重是w_i_j,即从i节点到下一层的j节点的链接
# w11 w21
# w12 w22 etx
self.wih = numpy.random.normal(0.0, pow(self.hnodes, -0.5), (self.hnodes, self.inodes))
self.who = numpy.random.normal(0.0, pow(self.onodes, -0.5), (self.onodes, self.hnodes))
# 学习率
self.lr = learningrate
# 激活函数是sigmoid函数(S函数),它的逆函数是对数函数
self.activation_function = lambda x: scipy.special.expit(x)
self.inverse_activation_function = lambda x: scipy.special.logit(x)
pass
# 对神经网络进行训练
def train(self, inputs_list, targets_list):
# 将inputs_list转换为2维数组
inputs = numpy.array(inputs_list, ndmin = 2).T
targets = numpy.array(targets_list, ndmin = 2).T
# 计算进入隐藏层的信号
hidden_inputs = numpy.dot(self.wih, inputs)
# 计算隐藏层发出的信号
hidden_outputs = self.activation_function(hidden_inputs)
# 计算进入输出层的信号
final_inputs = numpy.dot(self.who, hidden_outputs)
# 计算输出层发出的信号
final_outputs = self.activation_function(final_inputs)
# 输出层误差为(target - actual)
output_errors = targets - final_outputs
# 隐藏层误差为按权重比例分割输出层误差后,在隐藏层节点的重组值
hidden_errors = numpy.dot(self.who.T, output_errors)
# 更新隐藏层与输出层之间链接的权重
self.who += self.lr * numpy.dot((output_errors * final_outputs * (1.0 - final_outputs)), numpy.transpose(hidden_outputs))
# 更新输入层与隐藏层之间链接的权重
self.wih += self.lr * numpy.dot((hidden_errors * hidden_outputs * (1.0 - hidden_outputs)), numpy.transpose(inputs))
pass
# 对神经网络进行查询
def query(self, inputs_list):
# 将inputs_list转换为2维数组
inputs = numpy.array(inputs_list, ndmin = 2).T
# 计算进入隐藏层的信号
hidden_inputs = numpy.dot(self.wih, inputs)
# 计算隐藏层发出的信号
hidden_outputs = self.activation_function(hidden_inputs)
# 计算进入输出层的信号
final_inputs = numpy.dot(self.who, hidden_outputs)
# 计算输出层发出的信号
final_outputs = self.activation_function(final_inputs)
return final_outputs
# 向后查询神经网络
# 我们对于每一项使用相同的术语
# eg target是网络右侧的值,尽管在此用于输入
# eg hidden_output是网络中间节点发往右侧的信号
def backquery(self, targets_list):
# 将目标列表转置为垂直数组
final_outputs = numpy.array(targets_list, ndmin = 2).T
# 计算进入输出层的信号
final_inputs = self.inverse_activation_function(final_outputs)
# 计算隐藏层发出的信号
hidden_outputs = numpy.dot(self.who.T, final_inputs)
# 缩小到0.01到0.99
hidden_outputs -= numpy.min(hidden_outputs)
hidden_outputs /= numpy.max(hidden_outputs)
hidden_outputs *= 0.98
hidden_outputs += 0.01
# 计算进入隐藏层的信号
hidden_inputs = self.inverse_activation_function(hidden_outputs)
# 计算输入层发出的信号
inputs = numpy.dot(self.wih.T, hidden_inputs)
#缩小到0.01到0.99
inputs -= numpy.min(inputs)
inputs /= numpy.max(inputs)
inputs *= 0.98
inputs += 0.01
return inputs
# 输入、隐藏、输出层的节点数
input_nodes = 784
hidden_nodes = 200
output_nodes = 10
# 学习率
learning_rate = 0.1
# 创建神经网络的一个实例
n = neuralNetwork(input_nodes, hidden_nodes, output_nodes, learning_rate)
# 加载书写数字识别的训练数据CSV文件为一个列表
training_data_file = open(r"E:\Neural Network\mnist_dataset\mnist_train.csv", 'r')
training_data_list = training_data_file.readlines()
training_data_file.close()
# 训练神经网络
# 世代是指训练数据集被用来测试的次数
epochs = 5
for e in range(epochs):
# 遍历训练数据集中的所有记录
for record in training_data_list:
# 在','逗号处分割记录
all_values = record.split(',')
# 对输入值进行缩放和移位
inputs = (numpy.asfarray(all_values[1:]) / 255.0 * 0.99) + 0.01
# 创建目标输出值(除了期望值标签对应的是0.99,全部为0.01)
targets = numpy.zeros(output_nodes) + 0.01
# all_values[0]是此记录的目标标签
targets[int(all_values[0])] = 0.99
n.train(inputs, targets)
pass
pass
# 加载书写数字识别的测试数据CSV文件为一个列表
test_data_file = open(r"E:\Neural Network\mnist_dataset\mnist_test.csv", 'r')
test_data_list = test_data_file.readlines()
test_data_file.close()
# 测试神经网络
# 评价网络运转的良好程度的计分板,初始化为空
scorecard = []
# 遍历测试数据集中的所有记录
for record in test_data_list:
# 在','逗号处分割记录
all_values = record.split(',')
# 正确答案是第一个值
correct_label = int(all_values[0])
# print(correct_label, "correct label")
# 对输入值进行缩放和移位
inputs = (numpy.asfarray(all_values[1:]) / 255.0 * 0.99) + 0.01
# 查询网络
outputs = n.query(inputs)
# 最大值的索引对应的标签
label = numpy.argmax(outputs)
# print(label, "network's answer")
# 添加正确(1)或不正确(0)到列表中
if (label == correct_label):
# 网络的答案匹配正确的答案,计分板加1
scorecard.append(1)
else:
# 网络的答案不匹配正确的答案,计分板加0
scorecard.append(0)
pass
pass
# 计算性能分数,即正确答案的得分
scorecard_array = numpy.asarray(scorecard)
print("performance =", scorecard_array.sum() / scorecard_array.size)
- 运行结果:
performance = 0.9745
3.2.3 标签“0”
我们是在询问神经网络——对于答案“0”,最理想的问题是什么。
- 代码(以9为例):
# 向后运行网络,给出一个标签,查看它绘制出怎样的图像
# 测试标签
label = 9
# 创建对于这个标签的输出信号
targets = numpy.zeros(output_nodes) + 0.01
# all_values[0]是此记录的目标标签
targets[label] = 0.99
print(targets)
# 获取图像数据
image_data = n.backquery(targets)
# 绘制图像
matplotlib.pyplot.imshow(image_data.reshape(28, 28), cmap = 'Greys', interpolation = 'None')
- 运行结果:
[0.01 0.01 0.01 0.01 0.01 0.01 0.01 0.01 0.01 0.99]
<matplotlib.image.AxesImage at 0x20eec9d45f8>
3.2.4 更多的大脑扫描
- 其他数字向后查询的结果:
3.3 创建新的训练数据:旋转图像
关于创造更多的变化类型作为样本的一个很酷的想法,就是利用已有的样本,通过顺时针或逆时针旋转它们,比如说旋转10度,创建新的样本。
- 旋转角度:+10与-1016
甜蜜点:角度10,10个世代
注:如果旋转的角度过大,神经网络的性能会出现下降。由于旋转较大的角度意味着创建了实际上不能代表数字的图像,这可以理解。
- 代码:
# python notebook for Make Your Own Neural Network
# 代码用于一个3层的神经网络,以及学习手写数字识别的数据集
# 此版本创建了额外的训练样本通过旋转每个原始样本10度(顺时针与逆时针)
# Xiangtan, 2019/9/28
import numpy
# 导入scipy.special模块以使用sigmoid函数(S函数)expit()
import scipy.special
# scipy.ndimage用于旋转图像数组
import scipy.ndimage
# 神经网络class定义
class neuralNetwork():
# 初始化神经网络
def __init__(self, inputnodes, hiddennodes, outputnodes, learningrate):
# 设置输入、隐藏、输出层的节点数
self.inodes = inputnodes
self.hnodes = hiddennodes
self.onodes = outputnodes
# 链接权重矩阵,wih与who
# 矩形数组中的权重是w_i_j,即从i节点到下一层的j节点的链接
# w11 w21
# w12 w22 etx
self.wih = numpy.random.normal(0.0, pow(self.hnodes, -0.5), (self.hnodes, self.inodes))
self.who = numpy.random.normal(0.0, pow(self.onodes, -0.5), (self.onodes, self.hnodes))
# 学习率
self.lr = learningrate
# 激活函数是sigmoid函数(S函数)
self.activation_function = lambda x: scipy.special.expit(x)
pass
# 对神经网络进行训练
def train(self, inputs_list, targets_list):
# 将inputs_list转换为2维数组
inputs = numpy.array(inputs_list, ndmin = 2).T
targets = numpy.array(targets_list, ndmin = 2).T
# 计算进入隐藏层的信号
hidden_inputs = numpy.dot(self.wih, inputs)
# 计算隐藏层发出的信号
hidden_outputs = self.activation_function(hidden_inputs)
# 计算进入输出层的信号
final_inputs = numpy.dot(self.who, hidden_outputs)
# 计算输出层发出的信号
final_outputs = self.activation_function(final_inputs)
# 输出层误差为(target - actual)
output_errors = targets - final_outputs
# 隐藏层误差为按权重比例分割输出层误差后,在隐藏层节点的重组值
hidden_errors = numpy.dot(self.who.T, output_errors)
# 更新隐藏层与输出层之间链接的权重
self.who += self.lr * numpy.dot((output_errors * final_outputs * (1.0 - final_outputs)), numpy.transpose(hidden_outputs))
# 更新输入层与隐藏层之间链接的权重
self.wih += self.lr * numpy.dot((hidden_errors * hidden_outputs * (1.0 - hidden_outputs)), numpy.transpose(inputs))
pass
# 对神经网络进行查询
def query(self, inputs_list):
# 将inputs_list转换为2维数组
inputs = numpy.array(inputs_list, ndmin = 2).T
# 计算进入隐藏层的信号
hidden_inputs = numpy.dot(self.wih, inputs)
# 计算隐藏层发出的信号
hidden_outputs = self.activation_function(hidden_inputs)
# 计算进入输出层的信号
final_inputs = numpy.dot(self.who, hidden_outputs)
# 计算输出层发出的信号
final_outputs = self.activation_function(final_inputs)
return final_outputs
# 输入、隐藏、输出层的节点数
input_nodes = 784
hidden_nodes = 200
output_nodes = 10
# 学习率
learning_rate = 0.01
# 创建神经网络的一个实例
n = neuralNetwork(input_nodes, hidden_nodes, output_nodes, learning_rate)
# 加载书写数字识别的训练数据CSV文件为一个列表
training_data_file = open(r"E:\Neural Network\mnist_dataset\mnist_train.csv", 'r')
training_data_list = training_data_file.readlines()
training_data_file.close()
# 训练神经网络
# 世代是指训练数据集被用来测试的次数
epochs = 10
for e in range(epochs):
# 遍历训练数据集中的所有记录
for record in training_data_list:
# 在','逗号处分割记录
all_values = record.split(',')
# 对输入值进行缩放和移位
inputs = (numpy.asfarray(all_values[1:]) / 255.0 * 0.99) + 0.01
# 创建目标输出值(除了期望值标签对应的是0.99,全部为0.01)
targets = numpy.zeros(output_nodes) + 0.01
# all_values[0]是此记录的目标标签
targets[int(all_values[0])] = 0.99
n.train(inputs, targets)
# 创造旋转图像
# 逆时针方向旋转10度
inputs_plus10_img = scipy.ndimage.interpolation.rotate(inputs.reshape(28, 28), 10, cval = 0.01, reshape = False)
n.train(inputs_plus10_img.reshape(784), targets)
# 顺时针方向旋转10度
inputs_minus10_img = scipy.ndimage.interpolation.rotate(inputs.reshape(28, 28), -10, cval = 0.01, reshape = False)
n.train(inputs_minus10_img.reshape(784), targets)
pass
pass
# 加载书写数字识别的测试数据CSV文件为一个列表
test_data_file = open(r"E:\Neural Network\mnist_dataset\mnist_test.csv", 'r')
test_data_list = test_data_file.readlines()
test_data_file.close()
# 测试神经网络
# 评价网络运转的良好程度的计分板,初始化为空
scorecard = []
# 遍历测试数据集中的所有记录
for record in test_data_list:
# 在','逗号处分割记录
all_values = record.split(',')
# 正确答案是第一个值
correct_label = int(all_values[0])
# print(correct_label, "correct label")
# 对输入值进行缩放和移位
inputs = (numpy.asfarray(all_values[1:]) / 255.0 * 0.99) + 0.01
# 查询网络
outputs = n.query(inputs)
# 最大值的索引对应的标签
label = numpy.argmax(outputs)
# print(label, "network's answer")
# 添加正确(1)或不正确(0)到列表中
if (label == correct_label):
# 网络的答案匹配正确的答案,计分板加1
scorecard.append(1)
else:
# 网络的答案不匹配正确的答案,计分板加0
scorecard.append(0)
pass
pass
# 计算性能分数,即正确答案的得分
scorecard_array = numpy.asarray(scorecard)
print("performance =", scorecard_array.sum() / scorecard_array.size)
- 运行结果:
performance = 0.9781
3.4 结语
神经网络使图像识别以及广泛的其他各类难题,都获得了空前的进步。求解这类难题的早期动力的一个关键性部分是生物大脑。
今天,在人工智能中,神经网络是一些神奇的应用程序成功的关键部分。而事实上,神经网络背后的核心思想其实是非常简单的。
两个附录(略)
- 微积分简介
- 使用树莓派来工作
2019/10/7 16:42完成编辑
笔者感受
不得不说,《Python神经网络编程(Make Your Own Neural Network)》是一部杰出的作品,该书短小精炼,仅200多页,我用了大约6天的时间完成阅读。用时并不长的阅读却着实让我受益匪浅。如果要问我学到了什么,那便是七个字:什么是神经网络。这部作品成功揭开了“神经网络”的神秘面纱,非常通俗易懂地讲述了它的原理。原以为触不可及的“神经网络”原来如此的简单。作者是相当成功的。
与第一篇博客:编码:隐匿在计算机软硬件背后的语言(Code:The Hidden Language of Computer Hardware and Software)相比较,本篇博客所包含的信息量要少很多,前者审视了计算机技术的总体发展脉络,后者则是聚焦于神经网络这一主题。但正因为局限于神经网络这一主题,本篇博客又是详细的,不似前者重于轮廓勾勒而轻于细致描绘,它包含了很多细节性的内容,如公式、图表以及代码等等。
阅读与代码的编写在9月28日结束,然而本篇博客完成于10月7日,即十一假期的最后一天,总共历时约10天。因为假期的存在所以写作的速度比较缓慢,这一点是值得笔者反思的。
笔者另外计划完成一篇新博客,内容将不再以《Python神经网络编程(Make Your Own Neural Network)》一书的目录为序,而着重介绍神经网络(neural network)的构建方法流程(包括原理与实现两部分),敬请期待。
pass:标志循环的结束,下一行就回到正常的缩进,不再是循环的一部分(可省略)。 ↩︎
NumPy:Python的一种开源的数值计算扩展。这种工具可用来存储和处理大型矩阵,比Python自身的嵌套列表(nested list structure)结构要高效的多(该结构也可以用来表示矩阵(matrix))。 ↩︎
numpy.zeros():指定一个长度为2的正整数列表(分别指定了行与列),创建一个零数组。 ↩︎
matplotlib.pyplot:包含图形绘制功能的模块。 ↩︎
%matplotlib inline:要求在Notebook上绘制图形,不在独立的外部窗口中绘制图形。 ↩︎
matplotlib.pyplot.imshow():创建绘图的指令,第一个参数是我们要绘制的数组,后面有可选的其他参数。 ↩︎
numpy.random.rand(rows, columns):生成一个数组,数组中元素为0~1的随机值,数组大小为rows乘以columns。 ↩︎
numpy.random.normal():以正态分布的方式采样,参数分别为分布中心值、标准方差和numpy数组的大小。
pow(x, y):返回 xy(x的y次方)的值。 ↩︎numpy.dot():将两个矩阵进行点乘运算。 ↩︎
scipy.special:该模块包含sigmoid函数(S函数)expit()和它的逆函数logit()。 ↩︎ ↩︎
lambda:python使用lambda来创建匿名函数。所谓匿名,意即不再使用def语句这样标准的形式定义一个函数。本书代码中,这个函数接受了x,返回scipy.special.expit(x)。 ↩︎
numpy.array(object, ndmin):构造指定样式的矩阵(数组)。object指定原数组,ndmin指定结果数组应具有的最小维数。详情请见这里。array()类型对象后加.T表示数组的转置。 ↩︎
numpy.asfarray():将文本字符串转换成实数,并创建这些数字的数组。
.reshape((x, y)):确保数字列表每x个元素折返一次,形成x*y的方形矩阵。 ↩︎numpy.argmax():发现数组中的最大值,并返回它的位置(索引值)。 ↩︎
imageio.imread():从图像文件(PNG或JPG)中读取数据。参数“as_gray = True”会将图像变成灰度图。 ↩︎
ndimage.interpolation.rotate():将数组转过一个给定的角度。参数“reshape=False”防止将图像压扁,保持原有形状。
参数“cval=0.01”表示用来填充数组元素的值为0.01。 ↩︎