神经网络可以说是目前人工智能算法中应用最为广泛的算法之一,相比Linear regression 、logistics regression、decision tree等机器学习算法等会复杂不少,变化也很多。本文从向量矩阵的角度去理解BP神经网络的数学原理,这个过程会显得更加清晰、简洁。我们以最经典的神经网络三层网络二分类模型为例,逐步推导数学公式,三层网络简单说明如下:
其中(1)式即为信息正向传导的向量化公式相应的python代码实现:
#forward
Lh = np.dot(X, Wh)
Yh = funcActivation.cal(Lh) #隐藏层输出
Yh = np.insert(Yh, np.shape(Yh)[1], values=np.ones(m), axis=1)#add all 1 col
Lo = np.dot(Yh, Wo)
Yo = funcOut.cal(Lo)
其中 funcActivation 为隐藏层激活函数的封装,这里使用的Relu,funcOut为输出层激活函数的封装,使用的是sigmoid。
xxxxxxxxxx
class ReLU:
def cal(self, z):
return np.clip(z, 0, np.inf)
def grad(self, x):
return (x > 0).astype(int)
class Sigmoid:
def cal(self, z):
return 1 / (1 + np.exp(-z))
def grad(self, x):
z = self.cal(x)
return z*(1-z)
最常用的有Mean Square Error均方差损失函数,用于分类的Cross Entropy等。本文以经典的MSE为例。如下(2)式是MSE损失函数的向量化表示。
我们目的是最小化损失函数,通常使用梯度下降法,逐渐逼近最小极值点。需要逐渐求解参数主要是两个:
和 ,下面分别计算对应损失函数的偏导数。
其中(3)式利用迹 的性质,所以前后两项一样(4)式到(5)式利用了迹的性质,其中表示矩阵各个元素相乘,对应numpy的multiply。神经网络梯度下降求解参数的时候,是从输出层到隐藏层逆着计算的,所以称之为“反向传播”,因此首先求对的偏导,此时相当于常数,故(6)式的 忽略,继续推导:
这里(7)式到(8)式利用了标量对向量或矩阵微分 的性质。对应python实现:
xxxxxxxxxx
delta = np.multiply(self.lossFunc.grad(Yo, Y) , funcOut.grad(Lo))
dO = np.dot(Xo.T, delta)
同理继续推导 :
对应的python的实现:
xxxxxxxxxx
delta = np.multiply(self.lossFunc.grad(Yo, Y) , funcOut.grad(Lo))
dH = np.dot(X.T, np.multiply(np.dot(delta, Wo.T[:,:-1]) , funcActivation.grad(Lh)))
以上是使用通过 增加全1的列的方式,省略偏置,下面再推导一下使用偏置的方式
其中 跟(8)式的结果相同,继续推导 :
下面推导 和 :
显然 跟(11)式的结果相同,继续推导 :
多层网络
本文实现的是基于输入层、隐藏层、输出层经典的三层神经网络架构,在此基础上可以实现更多隐藏层的神经
网络。我们观察 隐藏层的参数基于损失函数的偏导(11)式与输出层的 (8) 式区别:
我们观察到规律, 可以分成3项, 项是自己这一层的输入,
为上一层的反馈,第3项 为本层激活函数的导数,同理,
也可以看出三项,其中 为损失函数的导数,我们可以把他看成输出层的上一层的反馈值,所以
每层的参数矩阵的偏导可以统一公式为:
linear regression
如果把隐藏层去掉,输出层的激活函数也去掉,那么只有一层的神经网络就是多元线性回归。
Logistics regression
如果把隐藏层去掉,输出层的激活函数使用sigmoid,那么只有一层的神经网络就是就变成了逻辑回归。
关于隐藏层数量的选择
有一些启发式的公式选择隐藏层数量,但是主要还是通过实验选择最恰当的参数,这里可以使用交叉验证的
方式。如将训练样本3/7分割为验证数据集和训练数据集,然后先从少的隐藏层数量开始试,选择在训练
数据集上拟合效果好,同时在验证数据集上同样具有接近准确度的参数作为最终参数结果。
使用python库sklearn自带的数据集breast_cancer,共569行数据,30个维度。完整python实现代码:
ximport numpy as np
from sklearn import datasets
from sklearn.datasets import load_breast_cancer
from sklearn import preprocessing
import matplotlib.pyplot as plt
from sklearn.metrics import r2_score
dataset = None
DT_FLAG = 1 #1测试预测,2测试回归拟合
if DT_FLAG:
dataset = load_breast_cancer()
else:
dataset = datasets.load_boston()
class ReLU:
def __init__(self):
return
def cal(self, z):
return np.clip(z, 0, np.inf)
def grad(self, x):
return (x > 0).astype(int)
class Sigmoid:
def __init__(self):
return
def cal(self, z):
return 1 / (1 + np.exp(-z))
def grad(self, x):
z = self.cal(x)
return z*(1-z)
class LossMSE:
def __init__(self):
return
def loss(self, y_pred, y):
return np.sum(np.multiply((y_pred - y) , (y_pred - y))) / 2 /int(y.shape[0])
def grad(self, y_pred, y):
return (y_pred - y)/y.shape[0]
class LossCrossEntropy:
def loss(self, y_pred, y):
eps = np.finfo(float).eps
cross_entropy = -np.sum(y * np.log(y_pred + eps))
return cross_entropy
def grad(self, y_pred, y):
grad = y_pred - y
return grad
class LinearOut:#如果用于回归,只是线性输出,不做任何转换,是的可以有统一的结构
def cal(self, z):
return z
def grad(self, x):
self.gradCache = np.ones(np.shape(x))
return self.gradCache
class NN:
def __init__(self):
self.Wh = None #隐藏层参数w
self.Wo = None #输出层参数w
self.numHideUnit = 5
self.funcActivation = ReLU() #隐藏层激活函数
if DT_FLAG:
self.lossFunc = LossMSE()
self.funcOut = Sigmoid()
else:
self.lossFunc = LossMSE()
self.funcOut = LinearOut()
return
def fit(self, X, Y):
scalerX = preprocessing.StandardScaler().fit(X)#StandardScaler
Y = Y.reshape(-1, 1)
X = scalerX.transform(X)
if not DT_FLAG:
scalerY = preprocessing.StandardScaler().fit(Y)#MinMaxScaler
Y = scalerY.transform(Y)
m, n = np.shape(X)
funcActivation = self.funcActivation
funcOut = self.funcOut
# 10 1000 0.01 for breat cancer
numLayerHide = 5
maxIterTimes = 2000
eta = 0.01
if DT_FLAG:
eta = 10
else:
#maxIterTimes = 100000
eta = 0.001#eta = 0.00001
pass
outputNodeNum = 1
Wh = np.random.rand(n, numLayerHide) * 0.01
Bh = np.random.rand(numLayerHide) * 0.01
Wo = np.random.rand(numLayerHide, outputNodeNum) * 0.01 #Why +1? for reserver b
Bo = np.random.rand(outputNodeNum) * 0.01
errLog = []
dO_Old = 0
dH_Old = 0
for i in range(maxIterTimes):
#forward
Lh = np.dot(X, Wh) + Bh
Yh = funcActivation.cal(Lh) #隐藏层输出
Lo = np.dot(Yh, Wo) + Bo
Yo = funcOut.cal(Lo)
loss = self.lossFunc.loss(Yo, Y)
errLog.append(loss)
if loss < 0.001:
print('fit finish', i)
break
delta = np.multiply(self.lossFunc.grad(Yo, Y) , funcOut.grad(Lo))
dBo = delta
dWo = np.dot(Yh.T, dBo)
dBh = np.multiply(np.dot(delta, Wo.T) , funcActivation.grad(Lh))
dWh = np.dot(X.T, dBh)
Wo = Wo - eta * dWo
Wh = Wh - eta * dWh
Bo = Bo - eta * dBo
Bh = Bh - eta * dBh
score = 0
if DT_FLAG == 0:
score = r2_score(Yo, Y)
else:
score = r2_score((Yo > 0.5).astype(int), Y)
print('score', score)
return
if __name__ == '__main__':
nn = NN()
print('data', dataset.data.shape)
nn.fit(dataset.data, dataset.target)