在《机器学习》第五章学习了反向传播算法与BP网络的结构,在此试着用Python搭建一个BP网络。数据集分别使用之前所涉及的西瓜数据集和马疝病数据集:
1. 机器学习数据集——西瓜数据集.zip
2. 机器学习经典二分类数据集——马疝病数据集.zip
同时附上本项目完整文件:
BP网络Python实践.zip
关于具体的算法实现逻辑参考《机器学习》书上的流程:
1 载入数据
由于数据集文件中的内容的每一行都代表一条数据,其中最后面的元素代表样本的正/负,即标签。前面其他的元素都是属性取值,故我们需要将数据集文件进行读取并将属性与标签分开存放
fp = open(filename)
# 存放数据
dataset = []
# 存放标签
labelset = []
for i in fp.readlines():
a = i.strip().split()
# 每个数据行的最后一个数据作为标签
dataset.append([float(j) for j in a[:len(a)-1]])
labelset.append(int(float(a[-1])))
return dataset, labelset
在 f o r for for循环中,我们依次对文件内容的每一行进行处理。包括对其进行划分,然后取前 n − 1 n-1 n−1个元素作为特征值添加到 d a t a s e t dataset dataset中,而最后一个元素作为标签添加到 l a b e l s e t labelset labelset中,并最后返回这两个列表。
2 网络参数初始化
在这里我定义了一个
B
P
N
e
t
BPNet
BPNet类,用于初始化网络、网络训练以及测试。另外我还在该类里面定义了
S
i
g
m
o
i
d
Sigmoid
Sigmoid函数与均方损失计算函数,方便后续的训练与测试过程。
在对这个类生成对象的时候,我们需要传入输入层的神经元数量、隐层神经元数量、输出层神经元数量、迭代训练的轮数、学习率,以及训练集与测试集的文件路径。
class BPNet:
# BP网络初始化,输入层神经元数量,隐层神经元数量,输出层神经元数量,迭代训练轮数,学习率,训练集文件路径,测试集文件路径
def __init__(self, x_num, y_num, z_num, epochs, lr, trainning_set_path, testing_set_path):
self.x_num = x_num
self.y_num = y_num
self.z_num = z_num
self.epochs = epochs
self.lr = lr
self.trainning_set_path = trainning_set_path
self.testing_set_path = testing_set_path
# sigmoid函数
def sigmoid(self, inx):
return 1 / (1 + np.exp(-inx))
# 计算均方误差
def SquareError(self, y, label):
return np.sum((y-label)**2) / 2
# BP网络的训练以及测试
def GradDescent(self):
# 载入训练/测试数据集与标签集
trainning_dataset, trainning_labelset = self.loaddataset(self.trainning_set_path)
testing_dataset, testing_labelset = self.loaddataset(self.testing_set_path)
# 初始化参数
# 输入层与隐层的连接权重
weight1 = np.random.randint(-5, 5, (self.x_num, self.y_num)).astype(np.float64)
# 隐层与输出层的连接权重
weight2 = np.random.randint(-5, 5, (self.y_num, self.z_num)).astype(np.float64)
# 隐层阈值
value1 = np.random.randint(-5, 5, (1, self.y_num)).astype(np.float64)
# 输出层阈值
value2 = np.random.randint(-5, 5, (1, self.z_num)).astype(np.float64)
可以看到,上面这个代码是不完整的,其只展示到参数初始化就结束了。剩下的部分会在下面进行详述。
在
G
r
a
d
D
e
s
c
e
n
t
(
)
GradDescent()
GradDescent()函数中,我们先将训练集与测试集的数据载入程序,然后根据输入层、隐层以及输出层的神经元数量构建输入层与隐层的连接权、隐层与输出层的连接权、隐层阈值以及输出层阈值的数据结构并对其随机赋值。
3 开始训练
# BP网络的训练以及测试
def GradDescent(self):
# 载入训练/测试数据集与标签集
trainning_dataset, trainning_labelset = self.loaddataset(self.trainning_set_path)
testing_dataset, testing_labelset = self.loaddataset(self.testing_set_path)
# 初始化参数
# 输入层与隐层的连接权重
weight1 = np.random.randint(-5, 5, (self.x_num, self.y_num)).astype(np.float64)
# 隐层与输出层的连接权重
weight2 = np.random.randint(-5, 5, (self.y_num, self.z_num)).astype(np.float64)
# 隐层阈值
value1 = np.random.randint(-5, 5, (1, self.y_num)).astype(np.float64)
# 输出层阈值
value2 = np.random.randint(-5, 5, (1, self.z_num)).astype(np.float64)
# 迭代训练
for epoch in range(self.epochs):
# 依次训练每条数据
for i in range(len(trainning_dataset)):
# 输入数据
train_input = np.mat(trainning_dataset[i]).astype(np.float64)
# 数据标签
train_label = np.mat(trainning_labelset[i]).astype(np.float64)
# 隐层输入
input1 = np.dot(train_input, weight1).astype(np.float64)
# 隐层输出
output1 = self.sigmoid(input1 - value1).astype(np.float64)
# 输出层输入
input2 = np.dot(output1, weight2).astype(np.float64)
# 输出层输出
output2 = self.sigmoid(input2 - value2).astype(np.float64)
# 由链式法则更新参数
a = np.multiply(output2, 1 - output2)
g = np.multiply(a, train_label - output2)
b = np.dot(g, np.transpose(weight2))
c = np.multiply(output1, 1 - output1)
e = np.multiply(b, c)
weight1_change = self.lr * np.dot(np.transpose(train_input), e)
weight2_change = self.lr * np.dot(np.transpose(output1), g)
value1_change = -self.lr * e
value2_change = -self.lr * g
# 更新参数
weight1 += weight1_change
weight2 += weight2_change
value1 += value1_change
value2 += value2_change
if epoch % 25 == 0:
print(self.SquareError(output2, train_label))
这里只展示了
G
r
a
d
D
e
s
c
e
n
t
(
)
GradDescent()
GradDescent()函数中对于参数初始化与模型训练的过程。在模型训练的过程中,我们先是将各层的前向传播的输入输出结果计算出来,然后根据链式法则对各个参数进行更新。具体的参数更新算法如下
Δ
w
h
j
=
η
g
j
b
h
\Delta w_{hj}=\eta g_jb_h
Δwhj=ηgjbh
Δ θ j = − η g j \Delta \theta_j=-\eta g_j Δθj=−ηgj
Δ v i h = η e h x i \Delta v_{ih}=\eta e_hx_i Δvih=ηehxi
Δ γ h = − η e h \Delta \gamma_h=-\eta e_h Δγh=−ηeh
其中 e h = b h ( 1 − b h ) ∑ j = 1 l w h j g j e_h=b_h(1-b_h)\sum_{j=1}^lw_{hj}g_j eh=bh(1−bh)∑j=1lwhjgj、 g j = y ^ j k ( 1 − y ^ j k ) ( y j k − y ^ j k ) g_j=\hat y_j^k(1-\hat y_j^k)(y_j^k-\hat y_j^k) gj=y^jk(1−y^jk)(yjk−y^jk)
并且每训练25轮就输出一次平方损失结果。
3 开始测试
# 记录正确分类的数量
rightcount = 0
# 开始测试
for i in range(len(testing_dataset)):
#计算每一个样例通过该神经网路后的预测值
test_input = np.mat(testing_dataset[i]).astype(np.float64)
test_label = np.mat(testing_labelset[i]).astype(np.float64)
# 隐层输出
output1 = self.sigmoid(np.dot(test_input, weight1) - value1)
# 输出层输出
output2 = self.sigmoid(np.dot(output1, weight2) - value2)
#确定其预测标签
if output2 > 0.5:
flag = 1
else:
flag = 0
if test_label == flag:
rightcount += 1
print('预测结果:' + str(flag) + ' 标签:' + str(int(test_label)))
print('正确率:' + str(rightcount / len(testing_dataset)))
这里只展示了测试过程。测试过程就比较简单,直接计算最终的结果,然后与样本标签进行比对即可知道结果的正确与否。最后统计输出测试过程的正确率。
4 测试结果
首先对于西瓜数据集,每个西瓜数据共有8个属性值与一个标签值,故
WM_TRAINNING_PATH = 'C:/Users/Desktop/西瓜训练集.txt'
WM_TESTING_PATH = 'C:/Users/Desktop/西瓜测试集.txt'
BPNet(8, 10, 1, 100, 0.02, WM_TRAINNING_PATH, WM_TESTING_PATH).GradDescent()
我定义输出层神经元数量为8,隐层神经元数量为10,输出层神经元数量为1,共训练100轮,学习率为0.02。输出结果如下
可以看到均方损失确实一直在下降,预测结果错了一个,所以正确率为0.8。
对于马疝病数据集的话
TRAINNING_SET_PATH = 'C:/Users/waao_wuyou/Desktop/horseColicTraining.txt'
TESTING_SET_PATH = 'C:/Users/waao_wuyou/Desktop/horseColicTraining.txt'
BPNet(21, 45, 1, 1400, 0.05, TRAINNING_SET_PATH, TESTING_SET_PATH).GradDescent()
我定义其输入层神经元数量是21,隐层神经元数量是45,输出层神经元数量是1,共训练1400轮,学习率为0.05,其部分结果如下
正确率可达83%
5 完整代码
#coding=utf8
import numpy as np
# 训练集
TRAINNING_SET_PATH = 'C:/Users/Desktop/horseColicTraining.txt'
# 测试集
TESTING_SET_PATH = 'C:/Users/Desktop/horseColicTraining.txt'
class BPNet:
# BP网络初始化,输入层神经元数量,隐层神经元数量,输出层神经元数量,迭代训练轮数,学习率,训练集文件路径,测试集文件路径
def __init__(self, x_num, y_num, z_num, epochs, lr, trainning_set_path, testing_set_path):
self.x_num = x_num
self.y_num = y_num
self.z_num = z_num
self.epochs = epochs
self.lr = lr
self.trainning_set_path = trainning_set_path
self.testing_set_path = testing_set_path
# 从指定路径文件生成数据集与标签
def loaddataset(self, filename):
fp = open(filename)
# 存放数据
dataset = []
# 存放标签
labelset = []
for i in fp.readlines():
a = i.strip().split()
# 每个数据行的最后一个数据作为标签
dataset.append([float(j) for j in a[:len(a)-1]])
labelset.append(int(float(a[-1])))
return dataset, labelset
# sigmoid函数
def sigmoid(self, inx):
return 1 / (1 + np.exp(-inx))
# 计算均方误差
def SquareError(self, y, label):
return np.sum((y-label)**2) / 2
# BP网络的训练以及测试
def GradDescent(self):
# 载入训练/测试数据集与标签集
trainning_dataset, trainning_labelset = self.loaddataset(self.trainning_set_path)
testing_dataset, testing_labelset = self.loaddataset(self.testing_set_path)
# 初始化参数
# 输入层与隐层的连接权重
weight1 = np.random.randint(-5, 5, (self.x_num, self.y_num)).astype(np.float64)
# 隐层与输出层的连接权重
weight2 = np.random.randint(-5, 5, (self.y_num, self.z_num)).astype(np.float64)
# 隐层阈值
value1 = np.random.randint(-5, 5, (1, self.y_num)).astype(np.float64)
# 输出层阈值
value2 = np.random.randint(-5, 5, (1, self.z_num)).astype(np.float64)
# 迭代训练
for epoch in range(self.epochs):
# 依次训练每条数据
for i in range(len(trainning_dataset)):
# 输入数据
train_input = np.mat(trainning_dataset[i]).astype(np.float64)
# 数据标签
train_label = np.mat(trainning_labelset[i]).astype(np.float64)
# 隐层输入
input1 = np.dot(train_input, weight1).astype(np.float64)
# 隐层输出
output1 = self.sigmoid(input1 - value1).astype(np.float64)
# 输出层输入
input2 = np.dot(output1, weight2).astype(np.float64)
# 输出层输出
output2 = self.sigmoid(input2 - value2).astype(np.float64)
# 由链式法则更新参数
a = np.multiply(output2, 1 - output2)
g = np.multiply(a, train_label - output2)
b = np.dot(g, np.transpose(weight2))
c = np.multiply(output1, 1 - output1)
e = np.multiply(b, c)
weight1_change = self.lr * np.dot(np.transpose(train_input), e)
weight2_change = self.lr * np.dot(np.transpose(output1), g)
value1_change = -self.lr * e
value2_change = -self.lr * g
# 更新参数
weight1 += weight1_change
weight2 += weight2_change
value1 += value1_change
value2 += value2_change
if epoch % 25 == 0:
print(self.SquareError(output2, train_label))
# 记录正确分类的数量
rightcount = 0
# 开始测试
for i in range(len(testing_dataset)):
#计算每一个样例通过该神经网路后的预测值
test_input = np.mat(testing_dataset[i]).astype(np.float64)
test_label = np.mat(testing_labelset[i]).astype(np.float64)
# 隐层输出
output1 = self.sigmoid(np.dot(test_input, weight1) - value1)
# 输出层输出
output2 = self.sigmoid(np.dot(output1, weight2) - value2)
#确定其预测标签
if output2 > 0.5:
flag = 1
else:
flag = 0
if test_label == flag:
rightcount += 1
print('预测结果:' + str(flag) + ' 标签:' + str(int(test_label)))
print('正确率:' + str(rightcount / len(testing_dataset)))
#返回正确率
return rightcount / len(testing_dataset)
if __name__ == '__main__':
BPNet(21, 45, 1, 1400, 0.05, TRAINNING_SET_PATH, TESTING_SET_PATH).GradDescent()