文章目录
- 一、神经元模型
- 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 单位阶跃函数
1.2.2 logistic函数(sigmoid)
1.2.3 tanh函数(双曲正切函数)
1.2.4 ReLU(修正线性单元)
1.2.5 激励函数对比
激励函数 | 相对优势 | 相对劣势 |
阶跃函数 | 当输入大于阙值返回1,小于阙值返回0,符合理想状态的神经元模型 | 曲线不光滑,不连续 |
logistic | 曲线光滑;能够用于表示正例的概率 | 可能造成梯度消失;中心点为0.5 |
tanh | 曲线光滑;中心点为0;收敛较logistic快 | 可能造成梯度消失 |
ReLU | 不会造成梯度消失;收敛更快 | 当训练迭代一定次数后可能导致权重无法继续更新 |
1.3 罗森布拉特感知器
罗森布拉特感知器(Perceptron)是最早最基础的神经元模型,它所采用的激励函数是单位阶跃函数。由于阶跃函数曲线不连续光滑,且可导区域导数为0,所以其有一套独特的学习规则:
如何理解这个学习规则?看下面的例子:
1)分类正确:
不再进行更新。
2)分类错误:
可见,在类标分类错误的情况下,感知器会让权值向正确的标记方向移动。
1.4 Adaline(自适应线性神经元)
自适应线性神经元是普通的感知器的改进。Adaline以线性函数为激励函数,提出了代价函数的概念,并且使用了梯度下降法来最小化代价函数。其采用均方误差来作为代价函数:
那么对参数的求解则等价于求解:。使对求偏导,易得:
那么则有:
二、神经网络模型
2.1 线性不可分问题
考虑以下问题:如何让计算机学得异或的计算能力?
通过绘制决策边界不难发现,对于以下数据集:
无法通过一个线性超平面画出该数据集的决策边界:
即,异或问题是一个线性不可分问题。
单个神经元模型只能通过划分线性超平面来进行分类,那么想要解决非线性可分问题,则可以考虑使用性能更强大的多层神经网络。
2.2 多层前馈神经网络
将多个神经元模型按照一定的次序进行组合便可以生成一个性能强大的神经网络(neural network,NN)。神经网络模型有很多种类,这里介绍最常见的多层前馈神经网络。
上图是一个具有一个输入层、一个隐藏层和一个输出层的三层前馈型神经网络。每一层分别有个神经元,其中,只有隐藏层和输出层的神经元是功能神经元(包含激励函数)。假设神经网络的输入为,输入层神经元到隐藏层神经元的权重表示为,隐藏层神经元到输出层神经元的权重表示为。那么便可以求得:
1)第个隐藏层神经元的输入和输出为:
2)第个输出层神经元的输入和输出为:
以上便是多层前馈神经网络模型的前向传播(forward propagation)过程。而前向传播需要的权值参数,则需要通过学习得到。
三、神经网络学习:误差逆传播
神经网络的学习过程比神经元模型复杂的多,但是也可以通过误差逆传播算法(Error BackPropagation,BP)较为轻松地实现。
下面先用通俗的概念阐述一下什么是误差逆传播算法。误差逆传播算法总体看来可以分为三个步骤,即:前向传播、反向传播,以及权值更新。
1)前向传播:从输入层到输出层逐层计算出每个功能神经元的激励函数输出,并缓存;
2)反向传播:从输出层到输入层逐层计算出每个功能神经元的计算误差,从而计算出梯度,这一过程需要使用在前向传播中缓存的激励函数输出值;
3)权值更新:按照的更新规则更新权重。
表示第层,从0开始计数;表示在前向传播中缓存的第层的值,其中表示的是输入层的输入;表示第层和第层之间的权值矩阵;激励函数为。
1)前向传播 :参考神经元模型的计算方法,后一层的值由前一层的值和权值计算得到:
2)反向传播:以均方误差为神经网络的代价函数,对于样本,假设输出层为第层,则有:
求输出层梯度:
求最后一层隐藏层梯度:
从上述的数学公式不难总结得到一般推导公式,对于第层神经元,可以计算梯度:
这里的被定义为当前层的误差。从前面的数学推导可以得到:
(1)输出层的误差,即激活函数输出值和真实标记的差;
(2)隐藏层的误差,即与的线性组合,系数为权值。
3)权值更新:对于矩阵,其更新规则如下:
四、Python实现
4.1 确定参数
这里尝试编写一个高自由度可定制的多层BP神经网络。既然是高自由度,那么先考虑可定制的参数:
1)网络规模:特征数(输入层神经元数)、隐藏层神经元数、类标数(输出层神经元数),深度(权值矩阵个数,层数-1);
2)网络学习速率:学习率、最大迭代次数;
3)激励函数:由于是分类器,那么输出层的激励函数固定为logistic较为合适,而隐藏层的激励函数则应当可以变动。
综上,可以得到以下参数:
def __init__(self, feature_n, hidden_n=10, deep=2, label_n=2, eta=0.1, max_iter=200, activate_func="tanh"):
# 说明一下:类标label_n默认为2说明是二分类任务,此时输出神经元个数按照1个处理
pass
4.2 内置数据预处理器
由于分类器可以同时进行二分类和多分类任务,所以需要有一个数据预处理器来对多分类数据集类标进行独热编码。这里可以使用sklearn库中的OneHotEncoder,而我是自己编写了一个编码器:
def encoder(self, y):
y_new = []
if y.ndim == 1: # 如果是一维向量,则编码为独热矩阵
if self.label_n > 2: # 多分类才进行编码
for yi in y:
yi_new = np.zeros(self.label_n)
yi_new[yi] = 1
y_new.append(yi_new)
y_new = np.array(y_new)
else:
y_new = y
elif y.ndim == 2: # 将独热矩阵转换为一维向量
if self.label_n > 2:
for yi in y:
for j in range(len(yi)):
if yi[j] == 1:
y_new.append(j)
break
y_new = np.array(y_new)
else:
y_new = y.ravel()
else:
raise Exception("argument value error: ndarray ndim should be 1 or 2")
return y_new
添加了常数列:
def preproccessing(self, X=None, y=None): # 这样编写可以单独处理X,y,也可以同时处理X和y
X_y = []
if isinstance(X, np.ndarray):
X0 = np.array([[1] for i in range(X.shape[0])])
X = np.hstack([X0, X])
X_y.append(X)
if isinstance(y, np.ndarray):
y = self.encoder(y)
X_y.append(y)
return tuple(X_y)
4.3 数据初始化
除了在通过构造函数参数初始化的类属性以外,我们还需要准备其他的一些变量。
首先是激励函数及其导数函数的指针。定义好激励函数及其导数函数后,将其指针存储在一个字典中,通过超参"activate_func"得到:
activate_funcs = {"tanh":(self.tanh, self.dtanh), "sigmoid":(self.sigmoid, self.dsigmoid)}
self.activate_func, self.dactivate_func = activate_funcs[activate_func]
然后使用随机浮点数初始化权值矩阵:
self.weights = []
for d in range(deep):
if d == 0: # input layer to hidden layer
weight = np.random.randn(hidden_n, feature_n + 1)
elif d == self.deep - 1: # hidden layer to output layer
label_n = 1 if label_n == 2 else label_n # 需要注意这里改变只是一个临时变量,类属性label_n还是2
weight = np.random.randn(label_n, hidden_n)
else: # the others
label_n = 1 if label_n == 2 else label_n
weight = np.random.randn(hidden_n, hidden_n)
self.weights.append(weight)
最后,需要准备数据结构来缓存BP算法中的必要数据:
self.gradients = list(range(deep)) # 存储每一层的g值,前面数学推导介绍了
self.values = [] # 存储每一层的激励函数输出
4.4 BP算法
先实现BP算法的第一部分:前向传播。
def forward_propagation(self, X):
self.values.clear()
value = None
for d in range(self.deep):
if d == 0: # input layer to hidden layer
value = self.activation(self.linear_input(d, X), self.activate_func)
elif d == self.deep - 1: # hidden layer to output layer, use sigmoid
value = self.activation(self.linear_input(d, value), self.sigmoid)
else: # the others
value = self.activation(self.linear_input(d, value), self.activate_func)
self.values.append(value)
return value # 返回最后一层输出,在预测时要用
这里需要先说明一下,这是我的activation()的构造:
def activation(self, z, func): # 使用函数指针func提供的方法来计算
return func(z)
这是线性函数linear_func()的构造:
def linear_input(self, deep, X):
weight = self.weights[deep]
return X @ weight.T
前向传播实现后需要实现反向传播,完全按照数学推导的公式编写:
def back_propagation(self, y_true):
for d in range(self.deep - 1, -1, -1):
if d == self.deep - 1: # hidden layer to output layer
self.gradients[d] = (y_true - self.values[d]) * self.dsigmoid(self.values[d])
else:
self.gradients[d] = self.gradients[d + 1] @ self.weights[d + 1] * self.dactivate_func(self.values[d])
最后便可以完成完整的BP算法和训练算法:
def standard_BP(self, X, y):
for l in range(self.max_iter):
for Xi, yi in zip(X, y):
# 前向传播
self.forward_propagation(Xi)
# 反向传播
self.back_propagation(yi)
# 更新权重
for d in range(self.deep):
if d == 0: # input layer to hidden layer
self.weights[d] += self.gradients[d].reshape(-1, 1) @ Xi.reshape(1, -1) * self.eta
else: # the others
self.weights[d] += self.gradients[d].reshape(-1, 1) @ self.values[d - 1].reshape(1, -1) * self.eta
def fit(self, X, y):
X, y = self.preproccessing(X, y)
self.standard_BP(X, y)
return self
4.6 预测类标
预测函数有两个考虑因素:1、要保证多分类任务最终只有一个类标输出;2、要求可以选择返回属于某项类标的概率。
def predict(self, X, probability=False):
X = self.preproccessing(X)[0]
prob = self.forward_propagation(X)
y = None
if self.label_n == 2: # 二分类
y = np.where(prob >= 0.5, 1, 0)
else: # 多分类,选择概率最大的
y = np.zeros(prob.shape)
for yi, i in zip(y, np.argmax(prob, axis=1)):
yi[i] = 1
y = self.preproccessing(y=y)[0] # 将y转换为一维向量
if probability:
return y, prob
else:
return y
五、测试模型
5.1 求解异或问题
下面用上面编写的神经网络模型求解异或问题:
# 求解异或问题
X = np.array([[0, 0], [0, 1], [1, 0], [1, 1]])
y = np.array([0, 1, 1, 0])
classifier = BPNNClassifier(feature_n=2, hidden_n=7, deep=3, max_iter=1000).fit(X, y)
y_pred, prob = classifier.predict(X, probability=True)
print(y_pred, "\n", prob)
这里说明一下模型参数的选取,隐藏神经元的数量hidden_n通常为时具有较好的效果,这里的为1到10的整数,我在这里取的5,计算后四舍五入就是7(我的实际label_n为1)。由于数据集小,所以我将深度设为3,最大迭代次数设置为1000,这个可以通过测试进行调整。
以下是结果:
通过概率可以看见,分类效果还是很不错的。
5.2 求解多分类问题
这里导入鸢尾花数据集来测试模型进行多分类任务的性能:
# 求解多分类问题
from sklearn import datasets
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import MinMaxScaler
from sklearn.metrics import classification_report
iris = datasets.load_iris()
X = MinMaxScaler().fit_transform(iris.data)
X_train, X_test, y_train, y_test = train_test_split(X, iris.target, train_size=0.7, test_size=0.3)
classifier = BPNNClassifier(feature_n=4, hidden_n=7, deep=3, label_n=3).fit(X_train, y_train)
y_pred = classifier.predict(X_test)
print(classification_report(y_test, y_pred))
结果如下:
效果还行。
ATTENTION:
这里需要强调一下,神经网络模型参数对于其性能的影响非常的大,尤其是隐藏层神经元的个数和层数,只有选择了合适的参数才能最大程度地发挥神经网络模型的性能,否则很可能出现其性能极不稳定的情况。
再次说明一下隐藏层神经元个数的选取方式: