目录
- 一、神经元模型
- 1.1 M-P神经元
- 1.2 激励函数
- 1.2.1 单位阶跃函数
- 1.2.2 logistic函数(sigmoid)
- 1.2.3 tanh函数(双曲正切函数)
- 1.2.4 ReLU(修正线性单元)
- 1.2.5 激励函数对比
- 1.3 罗森布拉特感知器
- 1.4 Adaline(自适应线性神经元)
- 二、神经网络模型
- 2.1 线性不可分问题
- 2.2 多层前馈神经网络
- 三、神经网络学习:误差逆传播
- 四、Python实现
- 4.1 确定参数
- 4.2 内置数据预处理器
- 4.3 数据初始化
- 4.4 BP算法
- 4.6 预测类标
- 五、测试模型
- 5.1 求解异或问题
- 5.2 求解多分类问题
一、神经元模型
1.1 M-P神经元
神经元(neuron)模型是神经网络的基本组成部分,它参考了生物神经元的工作原理:通过多个树突接收输入,在神经元进行处理后,如果电平信号超过某个阙值(threshold),那么该神经元就会被激活并通过一个轴突向其他神经元发送信号。对上述流程进行数学抽象,便可以得到如下的M-P神经元模型:
神经元模型将接收到的总输入和与神经元的阙值进行比较,然后通过激励函数(activation function)处理以产生神经元的输出。
1.2 激励函数
常见的激励函数通常有以下几种:
1.2.1 单位阶跃函数
\[f(z)=\begin{cases} 1,\qquad z>=0\\0,\qquad z<0\end{cases} \]
1.2.2 logistic函数(sigmoid)
\[f(z)=\frac{1}{1+e^{-z}} \qquad f'(z)=f(z)(1-f(z)) \]
1.2.3 tanh函数(双曲正切函数)
\[f(z)=\frac{e^z-e^{-z}}{e^z+e^{-z}} \qquad f'(z)=1-f(z)^2 \]
1.2.4 ReLU(修正线性单元)
\[f(z)=\frac{|z|+z}{2} \qquad f'(z)=\begin{cases}1,\qquad z>=0\\0,\qquad z<0 \end{cases} \]
1.2.5 激励函数对比
激励函数 | 相对优势 | 相对劣势 |
---|---|---|
阶跃函数 | 当输入大于阙值返回1,小于阙值返回0,符合理想状态的神经元模型 | 曲线不光滑,不连续 |
Sigmoid | 曲线光滑;能够用于表示正例的概率 | 可能造成梯度消失;中心点为0.5 |
Tanh | 曲线光滑;中心点为0;收敛较logistic快 | 可能造成梯度消失 |
ReLU | 不会造成梯度消失;收敛更快 | 当训练迭代一定次数后可能导致权重无法继续更新 |
1.3 罗森布拉特感知器
罗森布拉特感知器(Perceptron)是最早最基础的神经元模型,它所采用的激励函数是单位阶跃函数。由于阶跃函数曲线不连续光滑,且可导区域导数为0,所以其有一套独特的学习规则:
\[w_{new}=w_{old}+\Delta w\qquad \Delta w=\eta(y-y^*)x \]
如何理解这个学习规则?看下面的例子:
1)分类正确:
\[\Delta w=\eta(0-0)x=0\qquad\Delta w=\eta(1-1)x=0 \]
不再进行更新。
2)分类错误:
\[\Delta w=\eta(1-0)x=\eta x\qquad\Delta w=\eta(0-1)x=-\eta x \]
可见,在类标分类错误的情况下,感知器会让权值向正确的标记方向移动。
1.4 Adaline(自适应线性神经元)
自适应线性神经元是普通的感知器的改进。Adaline以线性函数\(h(x)=x\)为激励函数,提出了代价函数的概念,并且使用了梯度下降法来最小化代价函数。其采用均方误差来作为代价函数:
\[J(x_i,w)=\frac{1}{2m}\sum^{m}_{i=1}{(y_i-h(x_i))^2} \]
那么对参数\(w\)的求解则等价于求解:\(w=\arg\min_wJ(x_i,w)\)。使\(J\)对\(w\)求偏导,易得:
\[\frac{\partial J}{\partial w}=\frac{1}{m}\sum^{m}_{i=1}{(y_i-h(x_i))h'(x_i)}=\frac{1}{m}\sum^{m}_{i=1}{(h(x_i)-y_i)x_i} \]
那么则有:
\[\Delta w=-\eta\frac{\partial J}{\partial w}=\frac{\eta}{m}\sum^{m}_{i=1}(y_i-h(x_i))x_i\qquad w^*=w+\Delta w \]
二、神经网络模型
2.1 线性不可分问题
考虑以下问题:如何让计算机学得异或的计算能力?
通过绘制决策边界不难发现,对于以下数据集:
\[X=<(0,0),(0,1),(1,0),(1,1)>\qquad y=<0,1,1,0> \]
无法通过一个线性超平面画出该数据集的决策边界:
即,异或问题是一个线性不可分问题。
单个神经元模型只能通过划分线性超平面来进行分类,那么想要解决非线性可分问题,则可以考虑使用性能更强大的多层神经网络。
2.2 多层前馈神经网络
将多个神经元模型按照一定的次序进行组合便可以生成一个性能强大的神经网络(neural network,NN)。神经网络模型有很多种类,这里介绍最常见的多层前馈神经网络。
上图是一个具有一个输入层、一个隐藏层和一个输出层的三层前馈型神经网络。每一层分别有\(d,q,l\)个神经元,其中,只有隐藏层和输出层的神经元是功能神经元(包含激励函数)。假设神经网络的输入为\(x=(x_1,…,x_i,…,x_d)\),输入层神经元\(i\)到隐藏层神经元\(h\)的权重表示为\(w^0_{ih}\),隐藏层神经元\(h\)到输出层神经元\(j\)的权重表示为\(w^1_{hj}\)。那么便可以求得:
1)第\(h\)个隐藏层神经元的输入和输出为:
\[\alpha_h=\sum^{d}_{i=1}{w^0_{ih}x_i}\qquad b_h=h(\alpha_h) \]
2)第\(l\)个输出层神经元的输入和输出为:
\[\beta_j=\sum^{q}_{h=1}{w^1_{hj}b_h}\qquad y_j=h(\beta_j) \]
以上便是多层前馈神经网络模型的前向传播(forward propagation)过程。而前向传播需要的权值参数,则需要通过学习得到。
三、神经网络学习:误差逆传播
神经网络的学习过程比神经元模型复杂的多,但是也可以通过误差逆传播算法(Error BackPropagation,BP)较为轻松地实现。
下面先用通俗的概念阐述一下什么是误差逆传播算法。误差逆传播算法总体看来可以分为三个步骤,即:前向传播、反向传播,以及权值更新。
1)前向传播:从输入层到输出层逐层计算出每个功能神经元的激励函数输出,并缓存;
2)反向传播:从输出层到输入层逐层计算出每个功能神经元的计算误差,从而计算出梯度\(\nabla f(w)\),这一过程需要使用在前向传播中缓存的激励函数输出值;
3)权值更新:按照\(w^*=w-\eta\nabla f(w)\)的更新规则更新权重。
下面用数学公式推导如何进行上述步骤。首先定义一些数学符号:使用下标\(i\)表示第\(i\)层,从0开始计数;\(v_i\)表示在前向传播中缓存的第\(i\)层的值,其中\(v_0\)表示的是输入层的输入;\(w_i\)表示第\(i\)层和第\(i+1\)层之间的权值矩阵;激励函数为\(h(v_iw_i)\)。
1)前向传播 :参考神经元模型的计算方法,后一层的值由前一层的值和权值计算得到:
\[v_{i+1}=h(v_iw_i) \]
2)反向传播:以均方误差为神经网络的代价函数,对于样本\(k\),假设输出层为第\(i+1\)层,则有:
\[E_k=\frac{1}{2}(y^*_k-y_k)^T(y^*_k-y_k)=\frac{1}{2}(v_{i+1}-y_k)^T(v_{i+1}-y_k) \]
求输出层梯度:
\[\begin{aligned} \frac{\partial E_k}{\partial w_i}&=\frac{\partial E_k}{\partial v_{i+1}}\frac{\partial v_{i+1}}{\partial v_iw_i}\frac{\partial v_iw_i}{\partial w_i}\\&=(v_{i+1}-y_k)h'(v_iw_i)v_i\\&=\delta h'(v_iw_i)v_i \\&=g_{i+1}v_i \end {aligned} \]
\[g_{i+1}=\delta_ih'(v_iw_i)\qquad \delta_i=v_{i+1}-y_k \]
求最后一层隐藏层梯度:
\[\begin{aligned} \frac{\partial E_k}{\partial w_{i-1}}&=\frac{\partial E_k}{\partial v_i}\frac{\partial v_i}{\partial v_{i-1}w_{i-1}}\frac{\partial v_{i-1}w_{i-1}}{\partial w_{i-1}}\\&=\frac{\partial E_k}{\partial v_{i+1}}\frac{\partial v_{i+1}}{\partial v_iw_i}\frac{\partial v_iw_i}{\partial v_i}h'(v_{i-1}w_{i-1})v_{i-1}\\&=g_{i+1}w_ih'(v_{i-1}w_{i-1})v_{i-1}\\&=\delta h'(v_{i-1}w_{i-1})v_{i-1}\\&=g_iv_{i-1} \end{aligned} \]
\[g_i=\delta_ih'(v_{i-1}w_{i-1})\qquad \delta_i=g_{i+1}w_i \]
从上述的数学公式不难总结得到一般推导公式,对于第\(i\)层神经元,可以计算梯度:
\[\nabla E(w_{i-1})=\frac{\partial E_k}{\partial w_{i-1}}=g_iv_{i-1}\qquad g_i=\delta_ih'(v_{i-1}w_{i-1}) \]
这里的\(\delta_i\)被定义为当前层的误差。从前面的数学推导可以得到:
(1)输出层的误差\(\delta_i=v_i-y\),即激活函数输出值和真实标记的差;
(2)隐藏层的误差\(\delta_i=g_{i+1}w_i\),即\(g_{i+1}\)与\(w_i\)的线性组合,系数为权值\(w_i\)。
3)权值更新:对于矩阵\(w_i\),其更新规则如下:
\[w_i^*=w_i+\Delta w_i\qquad\Delta w_i=-\eta\frac{\partial E_k}{\partial w_i} \]
四、Python实现
4.1 确定参数
这里尝试编写一个高自由度可定制的多层BP神经网络。既然是高自由度,那么先考虑可定制的参数:
1)网络规模:特征数(输入层神经元数)、隐藏层神经元数、类标数(输出层神经元数),深度(权值矩阵个数,层数-1);
2)网络学习速率:学习率、最大迭代次数;
3)激励函数:由于是分类器,那么输出层的激励函数固定为logistic较为合适,而隐藏层的激励函数则应当可以变动。
综上,可以得到以下参数:
def __init__(self, feature_n, hidden=None, label_n=2, eta=0.1, max_iter=100, activate_func="tanh"):
# hidden表示隐藏层规模,即层数与每层神经元个数
pass
4.2 内置数据预处理器
由于要求模型能够完成多分类任务,所以需要有一个数据预处理器来对多分类数据集类标进行独热编码。这里可以使用sklearn库中的OneHotEncoder,而我是自己编写了一个编码器:
def _encoder(self, y):
y_new = []
for yi in y:
yi_new = np.zeros(self.label_n)
yi_new[yi] = 1
y_new.append(yi_new)
return y_new
4.3 数据初始化
在构造函数中初始化参数时,需要注意一下几点:
首先是隐藏层规模,隐藏层规模默认为None,在构造函数中,需要对hidden进行处理,防止使用者在未输入hidden参数时模型接收到的隐藏层规模为None。处理方法如下:
if not hidden:
self.hidden = [10]
else:
self.hidden = hidden
然后是激活函数及其导数函数的引用。定义好激活函数及其导数函数后,将其引用存储在一个字典中,通过超参”activate_func”获取:
# 函数字典
funcs = {
"sigmoid": (self._sigmoid, self._dsigmoid),
"tanh": (self._tanh, self._dtanh),
"relu": (self._relu, self._drelu)
}
# 获取激活函数及其导数
self.activate_func, self.dacticate_func = funcs[activate_func]
下一步需要定义神经网络前向传播和反向传播过程中的重要数据结构:
# 拟合缓存
self.W = [] # 权重
self.g = list(range(self.deep)) # 梯度
self.v = [] # 神经元输出值
最后初始化权重矩阵:
for d in range(self.deep):
if d == 0:
self.W.append(np.random.random([self.hidden[d], feature_n]))
elif d == self.deep - 1:
self.W.append(np.random.random([label_n, self.hidden[d - 1]]))
else:
self.W.append(np.random.random([self.hidden[d], self.hidden[d - 1]]))
4.4 BP算法
先实现BP算法的第一部分:前向传播。
def _forward_propagation(self, x): # 前向传播
self.v.clear()
value = None
for d in range(self.deep):
if d == 0:
value = self.activate_func(self._linear_input(x, d))
elif d == self.deep - 1:
value = self._sigmoid(self._linear_input(self.v[d - 1], d))
else:
value = self.activate_func(self._linear_input(self.v[d - 1], d))
self.v.append(value)
return value
前向传播实现后需要实现反向传播,完全按照数学推导的公式编写即可:
def _back_propagation(self, y): # 反向传播
for d in range(self.deep - 1, -1, -1):
if d == self.deep - 1:
self.g[d] = (y - self.v[d]) * self._dsigmoid(self.v[d])
else:
self.g[d] = self.g[d + 1] @ self.W[d + 1] * self.dacticate_func(self.v[d])
最后便可以实现完整的BP算法和训练算法:
def _bp(self, X, y):
for i in range(self.max_iter):
for x, yi in zip(X, y):
self._forward_propagation(x) # 前向传播
self._back_propagation(yi) # 反向传播
# 更新权重
for d in range(self.deep):
if d == 0:
self.W[d] += self.g[d].reshape(-1, 1) @ x.reshape(1, -1) * self.eta
else:
self.W[d] += self.g[d].reshape(-1, 1) @ self.v[d - 1].reshape(1, -1) * self.eta
def fit(self, X, y):
y = self._encoder(y)
self._bp(X, y)
return self
4.6 预测类标
这一过程实现很简单,代码如下:
def _predict(self, x):
y_c = self._forward_propagation(x)
return np.argmax(y_c)
def predict(self, X):
y = []
for x in X:
y.append(self._predict(x))
return np.array(y)
五、测试模型
5.1 求解异或问题
下面用上面编写的神经网络模型求解异或问题:
from model.model_demo import simple_data
from model.model_demo import demo
xor = simple_data("xor")
demo(MLPClassifier(2, label_n=2, activate_func="tanh", max_iter=500), xor, split=False, scaler=False)
以下是结果:
可以看见,分类效果还是很不错的。
5.2 求解多分类问题
这里导入鸢尾花数据集来测试模型进行多分类任务的性能:
# 求解多分类问题
from model.model_demo import iris_data
from model.model_demo import demo
iris = iris_data(3)
demo(MLPClassifier(4, hidden=[10, 10], label_n=3, activate_func="relu", max_iter=2000), iris)
结果如下:
效果还行。