机器学习-神经网络及反向传播算法
一、神经网络
1.1 神经网络的需求
看完了 Ng 的神经网络视频,还是半懂不懂,所以觉得自己梳理一下整个流程,文中的名字定义以及公式风格参考的 Ng 的教学材料。我们前面讲到使用???? [logistic 回归] 预测数据,这是一个 0-1 分类问题,如果想实现多分类该怎么办?比如对下面的手写数字进行识别,输入的维度 >400 ,输出的类别有 0-9 一共 10 个类别,你当然可以使用多个 logistic 回归来实现,一共训练 10 个 0-1 分类器,但是训练的代价成本非常大大。当我们需要解决特征量很大的非线性分类问题时( 比如计算机视觉问题 ),我们原本假设高次特征的方法会使得特征数异常庞大,从而引出新的方法神经网络。
1.2 什么是神经网络
神经网络(Neural Network)是一种很古老的算法,他的本质是通过模仿人类的神经元信息处理方式来处理现实中的数据。考虑一个最简单的神经元模型,输入数据为 (图中省略了偏置项 ),经过一个神经元之后输出值 。
神经元的处理可以使用 sigmoid
函数表示如下
单个神经元的能力非常有限,而神经网络强大的地方在于将这些神经元连接在一起共同工作( 类似于大脑中神经元的工作方式 ),所以我们来看一下神经网络模型是如何表示的。
上图是含有一层隐含层的神经网络,输入单元 将值传给隐含层( 每个输入单元传入的权值是不同的 )。然后隐含层将输出值再传给输出层的神经元。用数学语言表达如下
Cannot read property 'type' of undefined
其中, 表示中间变量, 表示第 层的权重参数, 表示神经元的输出, 输入数据加入了偏置项 ,最终输出的 为
这里同样加入了一个偏置项 ,输出层只有一个单元,但其实可以包含多个单元用于多分类。
在上图中,如果第 层有 个单元,第 层有 个单元,那么容易得出 为 矩阵。
二、反向传播
Ng 在视频中说即使这个算法出现了很多年同时效果也非常好,但是要理解里面的原理还是很难(谦虚的说法 ????,我就是不懂)。
2.1 代价函数
假设我们的多分类问题有 个分类,神经网络共有 层,每一层的神经元个数为 ,输入数据为 个,那么神经网络的代价函数为
其中第 2 项为正则化系数。
2.2 正向传播算法
要理解为什么需要使用反向传播(Backpropagation algorithm),首先就得了解一下正向传播中使用的梯度计算方法,假设神经网络模型如下图所示,一共包含 4 层,训练数据为 。
正向传播的表达式为
为了能够使用梯度下降算法来训练网络,我们需要计算代价函数的梯度。一种很直观的方法就是使用数值计算,对于某个 ,给它加上减去一个很小的量来 计算梯度,公式为
但稍微分析一下算法的复杂度就能知道,这样的方法十分缓慢。对于每一组数据,我们需要计算所有权值的梯度,总的计算次数 = 训练数据个数 x 网络权值个数 x 前向传播计算次数 。在通常情况下这样的复杂度是无法接受的,所以我们仅使用这个方法来验证反向传播算法计算的梯度是否正确。
2.3 反向传播算法
为了介绍反向传播算法,我们先复习一下微积分中的求导的链式法则????,对于多元函数 ,其中 ,那么
一般的,对于函数 , 如果他能看做 的函数,而 为 的函数,那么 关于 的梯度为
Ng 的视频中关于这一块的推导直接跳过了????,下面给出反向传播具体的公式推导,参考 [2] 中的作者给出的详细的证明过程。
定义 为第 层的第 个神经元的误差,其表达式为
输出层的误差为
其它层的误差为
其中
当 时,上式对 的偏导数才不为 0,所以
注意到, ,所以
所以
按照上述思路我们对每个参数 求导,使用链式法则为
根据式 ,可以得到
根据式 可以sel得到反向传播算法的步骤:
输入数据
对所有的 ,初始化 , 初始化权值
for
- 令
- 前向传播计算
- 使用 式计算
- 使用 式计算
- 使用 式计算
获取梯度矩阵(不对第一项加正则化项)
更新权值
这里说一下权值初始化,对于神经网络,不能像之前那样使用相同的 0 值来初始化,这会导致每层的逻辑单元都相同。因此我们使用随机化的初始化方法,使得 。
三、代码实现
3.1 Python 手动实现
我们使用 ???? [softmax回归] 中类似的数据,但是数据量少一些,主要是为了加快训练的速度,嘿嘿????。
数据集一共分为 4 个类别,分布情况如下
将数据集划分为训练数据集和测试数据,训练数据集的大小为 150 * 2,测试数据集的大小为 50 * 2。
首先定义几个工具函数:
def load_dataset(file_path):
dataMat = []
labelMat = []
fr = open(file_path)
for line in fr.readlines():
lineArr = line.strip().split()
dataMat.append([float(lineArr[0]), float(lineArr[1])])
labelMat.append(int(lineArr[2]))
return np.array(dataMat), np.array(labelMat).reshape((-1,1))
def sigmoid(x):
return 1.0 / (1 + np.exp(-x))
def one_hot(label_arr, n_samples, n_classes):
one_hot = np.zeros((n_samples, n_classes))
one_hot[np.arange(n_samples), label_arr.T] = 1
return one_hot
def plot_loss(loss):
fig = plt.figure(figsize=(8,5))
plt.plot(np.arange(len(all_loss)), all_loss)
plt.title("Development of loss during training")
plt.xlabel("Number of iterations")
plt.ylabel("Loss")
plt.show()
接下来是神经网络的主体部分
class NeuralNetWork():
def __init__(self, epsilon = 1, iters = 1000, alpha = 0.01, lam = 0.01):
self.hidden_dim = 10 # 默认取 4 个隐藏层神经元
self.n_hidden = 1 # 默认取 1 个隐藏层
self.epsilon = epsilon # 随机初始化权值矩阵的界限
self.iters = iters # 最大迭代次数
self.alpha = alpha # 学习率
self.lam = lam # 正则化项系数
self.weights = list() # 初始化权值矩阵
self.gradients = list() # 初始化梯度矩阵
self.bias = 1 # 偏置项
def init_weights(self, n_input, n_output):
# 第 1 → 2 层的权值矩阵
self.weights.append((2 * self.epsilon) * np.random.rand(self.hidden_dim, n_input + 1) - self.epsilon)
# 第 2 → n 层的权值矩阵
for i in range(self.n_hidden - 1):
self.weights.append((2 * self.epsilon) * np.random.rand(self.hidden_dim, self.hidden_dim + 1) - self.epsilon)
self.weights.append((2 * self.epsilon) * np.random.rand(n_output, self.hidden_dim + 1) - self.epsilon)
def init_gradients(self, n_input, n_output):
# 第 1 → 2 层的梯度矩阵
self.gradients.append(np.zeros((self.hidden_dim, n_input + 1)))
# 第 2 → n 层的梯度矩阵
for i in range(self.n_hidden - 1):
self.gradients.append(np.zeros((self.hidden_dim, self.hidden_dim + 1)))
self.gradients.append(np.zeros((n_output, self.hidden_dim + 1)))
def train(self, data_arr, label_arr):
n_samples, n_features = data_arr.shape
n_output = len(set(label_arr.flatten())) # 输出类别个数
y_one_hot = one_hot(label_arr, n_samples, n_output)
all_loss = list() # 损失函数记录
self.init_weights(n_features, n_output)
self.init_gradients(n_features, n_output)
for it in range(self.iters):
for index in range(n_samples):
# 计算前向传播每一层的输出值
layer_output = self.forward_propagation(data_arr[index])
# 计算每一层的误差
layer_error = self.cal_layer_error(layer_output, y_one_hot[index])
# 计算梯度矩阵
self.cal_gradients(layer_output, layer_error)
# 更新权值
self.update_weights(n_samples)
# 累加输出误差
#loss = self.cal_loss(data_arr, y_one_hot, n_samples)
#all_loss.append(loss)
return self.weights, all_loss
def forward_propagation(self, data):
layer_output = list()
a = np.insert(data, 0, self.bias)
layer_output.append(a)
for i in range(self.n_hidden + 1):
z = self.weights[i] @ a
a = sigmoid(z)
if i != self.n_hidden:
a = np.insert(a, 0, self.bias)
layer_output.append(a)
return np.array(layer_output)
def cal_layer_error(self, layer_output, y):
# 只有第 2 →n 层有误差,输入层没有误差
layer_error = list()
# 计算输出层的误差
error = layer_output[-1] - y
layer_error.append(error)
# 反向传播计算误差
for i in range(self.n_hidden, 0, -1):
error = self.weights[i].T @ error * layer_output[i] * (1 - layer_output[i])
# 删除第一项,偏置项没有误差
error = np.delete(error, 0)
layer_error.append(error)
return np.array(layer_error[::-1])
def cal_gradients(self, layer_output, layer_error):
for l in range(self.n_hidden + 1):
for i in range(self.gradients[l].shape[0]):
for j in range(self.gradients[l].shape[1]):
self.gradients[l][i][j] += layer_error[l][i] * layer_output[l][j]
def update_weights(self, n_samples):
for l in range(self.n_hidden + 1):
gradient = 1.0 / n_samples * self.gradients[l] + self.lam * self.weights[l]
gradient[:,0] -= self.lam * self.weights[l][:,0]
self.weights[l] -= self.alpha * gradient
def cal_loss(self, data_arr, y_one_hot, n_samples):
loss = 0 # 这里不用添加正则化项
for i in range(n_samples):
y = y_one_hot[i]
output = self.forward_propagation(data_arr[i])[-1]
loss += np.sum((y * np.log(output) + (1 - y) * np.log(1 - output)))
loss = (-1 / n_samples) * loss
return loss
def predict(self, data_arr):
n_samples = data_arr.shape[0]
ret = np.zeros(n_samples)
for i in range(n_samples):
output = self.forward_propagation(data_arr[i])[-1]
ret[i] = np.argmax(output)
return ret.reshape((-1,1))
def plot_decision_boundary(self, X, y):
# Set min and max values and give it some padding
x_min, x_max = X[:, 0].min() - .5, X[:, 0].max() + .5
y_min, y_max = X[:, 1].min() - .5, X[:, 1].max() + .5
h = 0.01
# Generate a grid of points with distance h between them
xx, yy = np.meshgrid(np.arange(x_min, x_max, h), np.arange(y_min, y_max, h))
# Predict the function value for the whole gid
Z = self.predict(np.c_[xx.ravel(), yy.ravel()])
Z = Z.reshape(xx.shape)
# Plot the contour and training examples
plt.contourf(xx, yy, Z, cmap=plt.cm.Spectral)
plt.scatter(X[:, 0], X[:, 1], c=np.squeeze(y))
plt.show()
主函数
if __name__ == "__main__":
# 加载数据
train_data_arr, train_label_arr = load_dataset('train_dataset.txt')
test_data_arr, test_label_arr = load_dataset('test_dataset.txt')
# 训练数据
nn = NeuralNetWork(iters = 1000)
weights, all_loss = nn.train(train_data_arr, train_label_arr)
#print(weights)
y_predict = nn.predict(test_data_arr)
accurcy = np.sum(y_predict == test_label_arr) / len(test_data_arr)
print(accurcy)
# 绘制决策边界
#nn.plot_decision_boundary(test_data_arr, test_label_arr)
# 绘制损失函数
#plot_loss(all_loss)
输出的准确率为
0.98
可以看到测试的效果还是很不错的,训练的时候保存了每一次迭代的损失值,损失函数的变化曲线如下图所示。
为了形象化的观察分类的情况,函数中添加了一个绘制决策边界的函数 plot_decision_boundary
,其分布情况如下
3.2 sklearn 库函数实现
sklearn
库中的包 MLPClassifier
,封装了神经网络的相关功能,我们用上节相同的数据,对比观察库函数生成的结果。
首先是导入数据,神经网络对数据尺度敏感,需要对数据进行标准化的转换。
train_data_arr, train_label_arr = load_dataset('train_dataset.txt')
test_data_arr, test_label_arr = load_dataset('test_dataset.txt')
scaler = StandardScaler() # 标准化转换
scaler.fit(train_data_arr) # 训练标准化对象
train_data_arr = scaler.transform(train_data_arr) # 转换数据集
scaler.fit(test_data_arr) # 训练标准化对象
test_data_arr = scaler.transform(test_data_arr) # 转换数据集
然后是训练数据,输出预测的准确率
from sklearn.neural_network import MLPClassifier
clf = MLPClassifier(solver='lbfgs', alpha=1e-5,hidden_layer_sizes=(5,), random_state=1)
clf.fit(train_data_arr, train_label_arr)
print('每层网络层系数矩阵维度:\n',[coef.shape for coef in clf.coefs_])
cengindex = 0
for wi in clf.coefs_:
cengindex += 1 # 表示底第几层神经网络。
print('第%d层网络层:' % cengindex)
print('权重矩阵维度:',wi.shape)
print('系数矩阵:\n',wi)
r = clf.score(train_data_arr, train_label_arr)
print("R值(准确率):", r) # 1.0
y_predict = clf.predict(test_data_arr).reshape((-1,1))
accurcy = np.sum(y_predict == test_label_arr) / len(test_data_arr)
print(accurcy) # 0.98
绘制决策边界
X = test_data_arr
y = test_label_arr
x_min, x_max = X[:, 0].min() - .5, X[:, 0].max() + .5
y_min, y_max = X[:, 1].min() - .5, X[:, 1].max() + .5
h = 0.01
# Generate a grid of points with distance h between them
xx, yy = np.meshgrid(np.arange(x_min, x_max, h), np.arange(y_min, y_max, h))
# Predict the function value for the whole gid
Z = clf.predict(np.c_[xx.ravel(), yy.ravel()])
Z = Z.reshape(xx.shape)
# Plot the contour and training examples
plt.contourf(xx, yy, Z, cmap=plt.cm.Spectral)
plt.scatter(X[:, 0], X[:, 1], c=np.squeeze(y))
plt.show()
我们看到训练的准确率为 0.98
,和我们上一节的手动 Python 输出的结果相同,而决策边界相比手动 Python 代码输出的效果要好很多,说明咱们的代码还是有优化的空间的。
3.3 可以优化的点
- **函数可以更换成
tanh
,RELU
等 - 设置不同的隐藏层维度和个数
- 改变梯度下降策略
- ……
本文的完整代码和数据可以去????[我的 github]查看
四、参考
[1] Andrew Ng. Machine Learning
[2] https://zhuanlan.zhihu.com/p/58068618
[3] https://zhuanlan.zhihu.com/p/57760693