本文正在参加「Python主题月」,详情查看活动链接
前言
动机
今天有很多好用成熟的深度学习框架、如 PyTorch 、Tensorflow 和 mxnet 等,在这些框架中已经把当下大部分流行算法和模型都已经封装好供我们调用。那么我们还有必要自己手写一个神经网络吗。其实个人认为还是有必要的,首先学习了深度学习的理论后,特表示反向传播这一块,如果不自己动手去写一个,就很难对反向传播有一个更深刻的认识。不仅限于此,在实际工作中,有时候因为任务不是那么负责,没有必要引入大型的框架或者你的模型需要在小型设备上进行训练和预测,这时候你就可以用底层语言实现简单神经网络。
一点点要求
- 了解深度学习
- 了解 python 以及其常用库,例如 numpy pandas 和 matplotlab 等
分享目标
一步一步地和大家手写一个用 python 实现网络,对于一些关键步骤给出解释和说明,帮助大家理解
import csv
import pandas as pd
这里引入 pandas 主要是为读取数据集和查看数据集,Pandas是一个强大的分析结构化数据的工具集。在数据挖掘和分析上也会经常用到这个库,如果还不了解觉得还是有必要好好学习一下。
准备数据集
在开始写代码之前,让我们先谈谈任务,让任务来驱动我们去手写一个神经网络。那么只有对任务有一个更全面的了解,才能拿出对应任务或者说问题的解决方案。数据是预测一个人是否会得心脏病。将使用UCL数据库中的一个心脏病数据集。你可以在这里下载。
数据特征
headers = ['age','sex','chest_pain','resting_blood_pressure','serum_clolestoral',
'fasting_blood_sugar','resting_ecg_results','max_heart_rate_achieved','exercise_induced_angina',
'oldpeak','slope_of_the_peak','number_of_major_vessels','thal','heart_disease']
设置数据集每个样本都具有哪些属性、最后一个列是标签,也就是表示该记录是否患有心脏病。关于这些指标如果大家感兴趣可以自行查找,这里就不做过多介绍了。
heart_df = pd.read_csv('./data/heart.dat',sep=' ',names=headers)
heart_df.head()
| age | sex | chest_pain | resting_blood_pressure | serum_clolestoral | fasting_blood_sugar | resting_ecg_results | max_heart_rate_achieved | exercise_induced_angina | oldpeak | slope_of_the_peak | number_of_major_vessels | thal | heart_disease | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 70.0 | 1.0 | 4.0 | 130.0 | 322.0 | 0.0 | 2.0 | 109.0 | 0.0 | 2.4 | 2.0 | 3.0 | 3.0 | 2 |
| 1 | 67.0 | 0.0 | 3.0 | 115.0 | 564.0 | 0.0 | 2.0 | 160.0 | 0.0 | 1.6 | 2.0 | 0.0 | 7.0 | 1 |
| 2 | 57.0 | 1.0 | 2.0 | 124.0 | 261.0 | 0.0 | 0.0 | 141.0 | 0.0 | 0.3 | 1.0 | 0.0 | 7.0 | 2 |
| 3 | 64.0 | 1.0 | 4.0 | 128.0 | 263.0 | 0.0 | 0.0 | 105.0 | 1.0 | 0.2 | 2.0 | 1.0 | 7.0 | 1 |
| 4 | 74.0 | 0.0 | 2.0 | 120.0 | 269.0 | 0.0 | 2.0 | 121.0 | 1.0 | 0.2 | 1.0 | 1.0 | 3.0 | 1 |
对于这些数据特征我们作为开发人员不用了解太多,也可以通过模型预测出不错结果。这就是深度学习好处,不要需要专业人来数据进行处理和特征工程。值的注意时最后列为数据标签,1 表示无,而 2 表示有心脏病。
heart_df.isna().sum()
age 0
sex 0
chest_pain 0
resting_blood_pressure 0
serum_clolestoral 0
fasting_blood_sugar 0
resting_ecg_results 0
max_heart_rate_achieved 0
exercise_induced_angina 0
oldpeak 0
slope_of_the_peak 0
number_of_major_vessels 0
thal 0
heart_disease 0
dtype: int64
查看数据类型
#查看数据类型
heart_df.dtypes
age float64
sex float64
chest_pain float64
resting_blood_pressure float64
serum_clolestoral float64
fasting_blood_sugar float64
resting_ecg_results float64
max_heart_rate_achieved float64
exercise_induced_angina float64
oldpeak float64
slope_of_the_peak float64
number_of_major_vessels float64
thal float64
heart_disease int64
dtype: object
数据中没有丢失数据以及打样本属性的数据类型都是 float64
将数据分离为训练集和测试集
在对数据有一个大概了解、如属性、数据是否完整以及每个属性对应数据类型后,我们就来开始作切分数据集为训练集和测试集。这里在切分数据集和对数据进行标准化的工作,借助了 sklearn 提供两个两个方法分别是 train_test_split 和 StandardScaler
import numpy as np
import warnings
warnings.filterwarnings("ignore")
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
X = heart_df.drop(columns=['heart_disease'])
# 将 heart_disease 取之从 1 和 2 替换为 0 和 1 通作在作二分类问题更喜欢用 0 和 1 来表示两个类别
heart_df['heart_disease'] = heart_df['heart_disease'].replace(1,0)
heart_df['heart_disease'] = heart_df['heart_disease'].replace(2,1)
# print(heart_df['heart_disease'].values.shape)
# (270,) 转换为 (270,1) [[0],[0],...]
y_label =heart_df['heart_disease'].values.reshape(X.shape[0],1)
X_train, X_test, y_train,y_test = train_test_split(X,y_label,test_size=0.2,random_state=2)
sc = StandardScaler()
sc.fit(X_train)
X_train = sc.transform(X_train)
X_test = sc.transform(X_test)
print(f"Shape of train set: {X_train.shape}")
print(f"Shape of test set: {X_test.shape}")
print(f"Shape of train label set: {y_train.shape}")
print(f"Shape of test label set: {y_test.shape}")
Shape of train set: (216, 13)
Shape of test set: (54, 13)
Shape of train label set: (216, 1)
Shape of test label set: (54, 1)
定义层
我们早期神经网络结构比较简单,就是层堆叠,如果没有激活函数其实就是将我们数据(矩阵)作了一次线性变换。
神经网络中层主要是由输入、输出和参数(权重),如图结点之间边就是参数(权重)在全连接层就是,这是一个全连接层,所谓全连接也就是,输出向量每个元素(分量) 是所有输入所有分量经过线性变换得到
class Layer:
def __init__(self):
self.input = None
self.output = None
def forward(self, input):
raise NotImplementedError
def backward(self,output_error, learning_rate):
raise NotImplementedError
定义基础层
先定义层 Layer 基类也可以理解为接口,就是定义 Layer 大概什么样子,能够提供哪些功能,这里forward和backward 分别对应前向传播和反向传播,在forward定义在传播中层中进行了哪些操作,而backward中计算在层输入变量和参数的梯度。
class FCLayer(Layer):
def __init__(self,input_size,output_size):
self.weights = np.random.rand(input_size,output_size) - 0.5
self.bias = np.random.rand(1,output_size) - 0.5
def forward(self,input_data):
self.input = input_data
self.output = np.dot(self.input,self.weights) + self.bias
return self.output
def backward(self,output_error,learning_rate):
# print(output_error)
# print(learning_rate)
input_error = np.dot(output_error, self.weights.T)
weights_error = np.dot(self.input.T, output_error)
self.weights -= learning_rate * weights_error
self.bias -= learning_rate * output_error
return input_error
我们实现一个全连接层,前向传播 关键是反向传播,对于变量 input 、权重 weights 和偏置 bias 反向求导是关键,这个放到后面一起解释。
class ActivationLayer(Layer):
def __init__(self,activation, activation_prime):
self.activation = activation
self.activation_prime = activation_prime
def forward(self,input_data):
self.input = input_data
self.output = self.activation(self.input)
return self.output
def backward(self,output_error, learning_rate):
return self.activation_prime(self.input) * output_error
反向传播
其实神经网络前向传播比较好理解和实现,就是线性变换,可能用矩阵来表示运算大家对线性代数不算了解可能看上去有点陌生,不过只要简单翻一翻线性代数再回来看,问题不大,关键是反向传播就比较难理解,涉及到矩阵求导,今天例子还是比较简单。我接下来把推导过程都列出,只要有些底子也不难理解。感觉公式更有说服力。大家需要熟悉求导链式法则,然后当对某一个参数进行求导,顺藤摸瓜即可,需要考虑到从该参数出发所有到损失函数所有路径。
权重相当于损失函数求导
这里我们以 相对于损失函数求偏导为例, 路径从上图来看还是比较简单途径 与输出或者损失连接。
参数对于损失(输出)值求导,损失值是一个标量或者是一个向量,每一个参数矩阵每一个元素对 E 进行求导。
我们结合图来看如何计算个直观,对于输出 Y 向量第 j 维分量来说,是输入 X 样本所有元素(特征)乘以一个对应权重, 对于 就是其系数
然后利用乘法链式法则得到下面公式
然后将这个方法应用到 W 矩阵上每一个元素进行求导表示后,再用矩阵表示一下。
偏置相当于损失函数求导
接下来就是求偏置,因为 仅与 有关,所以计算梯度相对要比较简单。这里不做过多解释,如果大家感兴趣请给我留言。
变量 X 对于输出求导
神经网络反向传播不是为对参数求梯度来更新参数,为什么还要对变量进行求导呢?因为关于损失值梯度还需要一层一层向下传递,所以有必要对变量进行求梯度。
对 X 变量输入时,也需要对样本每一个元素分别进行求偏导,然后组合在一起就是 X 对于 E 偏导。
还需要看一下这张图,我们还是对每一个 X 每一个元素看到其到输出会经过多少路径,然后然每条路径进行求导后再汇总。
如果大家对这个推导过程还不了解,可以自己按着上面图查一查。
整理后我们用矩阵形式来表示实现上面矩阵求导。
激活函数方向求导
损失函数
def tanh(x):
return np.tanh(x)
def tanh_prime(x):
return 1-np.tanh(x)**2
def mse(y_true, y_pred):
return np.mean(np.power(y_true - y_pred,2))
def mse_prime(y_true,y_pred):
return 2*(y_pred - y_true)/y_true.size
定义网络结构
网络结构中,这里是一个简单逐层叠加网络,所以在构造函数中我们维护列表,然后通过 add 方法往神经网络添加全连接层和激活层,loss 和 loss_prime 分别接受两个方法,一个用于计算损失值一个用于在反向传播是计算梯度。fit 函数用于训练网络,根据模型输出结果和真实值之间差距更新网络中每层中参数,而 predict 就是利用模型对输入数据进行预测。
class Network:
def __init__(self):
self.layers = []
self.loss = None
self.loss_prime = None
def add(self, layer):
self.layers.append(layer)
def use(self, loss, loss_prime):
self.loss = loss
self.loss_prime = loss_prime
def predict(self, input_data):
samples = len(input_data)
result = []
for i in range(samples):
output = input_data[i]
for layer in self.layers:
output = layer.forward(output)
result.append(output)
return result
def fit(self, x_train, y_train, epochs, learning_rate):
samples = len(x_train)
for i in range(epochs):
err = 0
for j in range(samples):
output = x_train[j]
for layer in self.layers:
output = layer.forward(output)
err += self.loss(y_train[j],output)
error = self.loss_prime(y_train[j],output)
# print(f"error: {error}")
for layer in reversed(self.layers):
# print(f"error: {error}")
error = layer.backward(error, learning_rate)
err /= samples
print(f"epoch {(i+1)/epochs} error ={err}")
这里用于一个简单数据集 XOR ,然后让网络先在这个数据集运行看看效果。
x_train = np.array([[[0,0]],[[0,1]],[[1,0]],[[1,1]]])
y_train = np.array([[[0]],[[1]],[[1]],[[0]]])
基础设施都准备好了,接下里工作就是用这些模块来搭建一个网络,现在就为解决 XOR 这个任务来搭建一个神经网络。
net = Network()
net.add(FCLayer(2,3))
net.add(ActivationLayer(tanh,tanh_prime))
net.add(FCLayer(3,1))
net.add(ActivationLayer(tanh,tanh_prime))
net.use(mse,mse_prime)
net.fit(x_train,y_train,epochs=2000, learning_rate=0.01)
out = net.predict(x_train)
print(out)
epoch 0.0005 error =0.7136337374260526
epoch 0.001 error =0.5905739289052101
epoch 0.0015 error =0.49847803406876834
epoch 0.002 error =0.4315229169435869
epoch 0.0025 error =0.3833645522357618
epoch 0.003 error =0.34869455999659626
epoch 0.0035 error =0.3235475456763668
epoch 0.004 error =0.3051103124983356
epoch 0.0045 error =0.29142808210519544
epoch 0.005 error =0.28114855069402345
[array([[0.01603471]]), array([[0.884377]]), array([[0.89208809]]), array([[0.02946468]])]
从预测结构来看,神经网络通过 2000 次迭代的确学到东西,给出不错答案
[array([[0.01603471]]), array([[0.884377]]), array([[0.89208809]]), array([[0.02946468]])]
试一试
给大家留一个作业,呵呵,不敢当。自己动手写一个网络,然后跑跑这个数据集,我跑是否患有心脏病效果不是很好,大家可调一调,跑一跑。
X_train = X_train.reshape(X_train.shape[0],1,X_train.shape[1])
net2 = Network()
net2.add(FCLayer(13,8))
net2.add(ActivationLayer(tanh,tanh_prime))
net2.add(FCLayer(8,5))
net2.add(ActivationLayer(tanh,tanh_prime))
net2.add(FCLayer(5,2))
net2.add(ActivationLayer(tanh,tanh_prime))
net2.use(mse,mse_prime)
net2.fit(X_train,y_label,epochs=35, learning_rate=0.1)
X_test = X_test.reshape(X_test.shape[0],1,X_test.shape[1])
output = net2.predict(X_test[0:3])
output
[array([[0.82602593, 0.82602593]]),
array([[0.17209814, 0.17209814]]),
array([[0.95838074, 0.95838074]])]
y_test[:3]
这个看输出好像模型没有学到,随后在此基础进一步调整,这里还有没有 tensor ,随后可以参考
array([[1],
[0],
[0]])
如果大家对上面内容还有疑问,欢迎留言。由于写比较仓促,如果有遗漏或者出路地方还希望大家多多指正批评。