神经元(neuron)模型是神经网络的基本组成部分,它参考了生物神经元的工做原理:经过多个树突接收输入,在神经元进行处理后,若是电平信号超过某个阙值(threshold),那么该神经元就会被激活并经过一个轴突向其余神经元发送信号。对上述流程进行数学抽象,即可以获得以下的M-P神经元模型:python
神经元模型将接收到的总输入和与神经元的阙值进行比较,而后经过激励函数(activation function)处理以产生神经元的输出。算法
常见的激励函数一般有如下几种:缓存
激励函数 | 相对优点 | 相对劣势 |
---|---|---|
阶跃函数 | 当输入大于阙值返回1,小于阙值返回0,符合理想状态的神经元模型 | 曲线不光滑,不连续 |
Sigmoid | 曲线光滑;可以用于表示正例的几率 | 可能形成梯度消失;中心点为0.5 |
Tanh | 曲线光滑;中心点为0;收敛较logistic快 | 可能形成梯度消失 |
ReLU | 不会形成梯度消失;收敛更快 | 当训练迭代必定次数后可能致使权重没法继续更新 |
罗森布拉特感知器(Perceptron)是最先最基础的神经元模型,它所采用的激励函数是单位阶跃函数。因为阶跃函数曲线不连续光滑,且可导区域导数为0,因此其有一套独特的学习规则:网络
如何理解这个学习规则?看下面的例子:
1)分类正确:数据结构
再也不进行更新。
2)分类错误:app
可见,在类标分类错误的状况下,感知器会让权值向正确的标记方向移动。dom
自适应线性神经元是普通的感知器的改进。Adaline以线性函数\(h(x)=x\)为激励函数,提出了代价函数的概念,而且使用了梯度降低法来最小化代价函数。其采用均方偏差来做为代价函数:函数
那么对参数\(w\)的求解则等价于求解:\(w=\arg\min_wJ(x_i,w)\)。使\(J\)对\(w\)求偏导,易得:性能
那么则有:学习
考虑如下问题:如何让计算机学得异或的计算能力?
经过绘制决策边界不难发现,对于如下数据集:
没法经过一个线性超平面画出该数据集的决策边界:
即,异或问题是一个线性不可分问题。
单个神经元模型只能经过划分线性超平面来进行分类,那么想要解决非线性可分问题,则能够考虑使用性能更强大的多层神经网络。
将多个神经元模型按照必定的次序进行组合即可以生成一个性能强大的神经网络(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\)个隐藏层神经元的输入和输出为:
2)第\(l\)个输出层神经元的输入和输出为:
以上即是多层前馈神经网络模型的前向传播(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)前向传播 :参考神经元模型的计算方法,后一层的值由前一层的值和权值计算获得:
2)反向传播:以均方偏差为神经网络的代价函数,对于样本\(k\),假设输出层为第\(i+1\)层,则有:
求输出层梯度:
求最后一层隐藏层梯度:
从上述的数学公式不难总结获得通常推导公式,对于第\(i\)层神经元,能够计算梯度:
这里的\(\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\),其更新规则以下:
这里尝试编写一个高自由度可定制的多层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
因为要求模型可以完成多分类任务,因此须要有一个数据预处理器来对多分类数据集类标进行独热编码。这里可使用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
在构造函数中初始化参数时,须要注意一下几点:
首先是隐藏层规模,隐藏层规模默认为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]]))
先实现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
这一过程实现很简单,代码以下:
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)
下面用上面编写的神经网络模型求解异或问题:
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)
如下是结果:
能够看见,分类效果仍是很不错的。
这里导入鸢尾花数据集来测试模型进行多分类任务的性能:
# 求解多分类问题 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)
结果以下:
效果还行。