读取数据后,将数据划分为标签(y)与特征(x)两类。
这里假设数据存储在excel表格中(为了尽可能与实际情况相符,不直接使用sklearn或者pytorch自带的数据集)。数据来源是sklearn中的波士顿房价,不过作者先将数据转存到了excel中,这与工作中的应用场景比较相符。
使用pandas读取数据:
import pandas as pd
'''导入数据'''
data = pd.read_excel('波士顿房价预测.xlsx') # 一共506组数据,每组数据13个特征,13个特征对应一个输出
x = data.loc[:, 0:12] # 将特征数据存储在x中,表格前13列为特征,
y = data.loc[:, 13:13] # 将标签数据存储在y中,表格最后一列为标签
1.2 归一化(Min-Max Scaling)与标准化(Standardization )
对1.1划分好的标签与特征执行归一化或者标准化操作。归一化与标准化均不改变数据分布,归一化后数据会在0,1之间(也可以设置成其他任意范围),标准化后数据的均值为0,方差为1。
大多数机器学习算法中,会选择标准化进行特征缩放,因为归一化对异常值非常敏感。在神经网络算法中也是采用标准化处理数据。
归一化:
标准化:
使用sklearn实现归一化:
'''对每列(特征)归一化''' from sklearn.preprocessing import MinMaxScaler # 导入归一化模块 # feature_range控制压缩数据范围,默认[0,1] scaler = MinMaxScaler(feature_range=[0,1]) # 实例化,调整0,1的数值可以改变归一化范围 X = scaler.fit_transform(x) # 归一化到0,1之间 Y = scaler.fit_transform(y) # 归于化到0,1之间 # x = scaler.inverse_transform(X) # 将数据恢复至归一化之前
使用sklearn实现标准化:
'''对每列数据执行标准化''' from sklearn.preprocessing import StandardScaler scaler = StandardScaler() # 实例化 X = scaler.fit_transform(x) # 标准化 Y = scaler.fit_transform(y) # 标准化 # x = scaler.inverse_transform(X) # 这行代码可以将数据恢复至标准化之前1.3 划分数据集
数据执行标准化后,需要划分测试集,训练集与验证集,还需要设置数据批次。测试集用于训练数据,验证集用于调整超参数,测试集用于验证模型效果(绝对不能根据测试集的表现来调节模型,这属于数据污染!!!)。
使用pytorch划分数据集及批次:
需要特别注意的是pytorch只能处理tensor类型的数据,因此需要将标准化后的array数组转化为tensor格式。
import torch
X = torch.tensor(X, dtype=torch.float32) # 将数据集转换成torch能识别的格式
Y = torch.tensor(Y, dtype=torch.float32)
torch_dataset = torch.utils.data.TensorDataset(X, Y) # 组成torch专门的数据库
batch_size = 6 # 设置批次大小
# 划分训练集测试集与验证集
torch.manual_seed(seed=2021) # 设置随机种子分关键,不然每次划分的数据集合都不一样,不利于结果复现
train_validaion, test = torch.utils.data.random_split(
torch_dataset,
[450, 56],
) # 先将数据集拆分为训练集+验证集(共450组),测试集(56组)
train, validation = torch.utils.data.random_split(
train_validaion, [400, 50]) # 再将训练集+验证集拆分为训练集400,测试集50
# 将训练集划分批次,每batch_size个数据一批(测试集与验证集不划分批次)
train_data = torch.utils.data.DataLoader(train,
batch_size=batch_size,
shuffle=True)
到此为止,数据预处理部分结束,下面做个小结:
(1)使用pandas读取数据并拆分标签与特征;
(2)使用sklearn对数据执行归一化或者标准化;
(3)使用pytorch对数据划分批次及训练集、测试集与验证集。
对于数据预处理,作者之所以pandas、sklearn等工具包,仅仅是因为使用习惯不同,这些工具包并不是数据处理的唯一选择,其实使用任何能够实现相同效果的工具包均可。
2 训练模型 2.1 搭建神经网络模型神经网络需要搭建输入层、隐藏层与输出层,搭建完成后还需要考虑误差计算函数,激活函数以及误差反向传播中权重、偏差更新规则的选择。
神经网络的计算可以分为输入数据的正向传播过程、误差的方向传播过程、权重及偏差矩阵的更新过程。
正向传播过程:数据从输入层出来后需要先非线性激活,再进入隐藏层;从一个隐藏层出来之后,需要先非线性激活,再到进入下一个隐藏层;经过了所有隐藏层之后,再进入输出层;输出层输出数据后使用误差函数计算预测值与实际值的误差。
反向传播过程:反向传播过程与正向传播过程相反,而且也不是矩阵求积运算,而是通过链式法则求偏导运算(误差对偏差矩阵以及误差对权重矩阵的偏导数)。
更新权重及偏差矩阵:根据反向传播过程计算出的偏导数更新偏差矩阵以及权重矩阵,最终使得偏差最小或者达到结束条件后停止计算。
在使用pytorch求解回归问题的实践中,其实只需要考虑输入层的释放神经元数目、输出层的接收神经元数目、隐藏层接收和释放神经元数目以及隐藏层数的选择,还有激活函数、误差计算函数以及权重和偏差更新规则的选择即可。至于如何实现正向传播、反向传播以及权重偏差更新,这并不需要专门研究,使用一段简单的代码就可借助pytorch实现。
搭建神经网络模型需要注意以下几点:
(1)输入层的接收神经元个数必须与特征数目相同;输出层的释放神经元个数必须与标签数目相同;
(2)隐藏层的神经元数目可以自行调整,但是必须遵守以下规则:第一个隐藏层的接收神经元数目必须与输入层的释放神经元数目相同,最后一个隐藏层的释放神经元数目必须与输出端的接收神经元数目相同,中间隐藏的释放神经元数目必须与下一个隐藏层的接收神经元数目相同;
(3)对于回归问题,输出层之后不需要任何激活函数。
2.2 激活函数的选择激活函数在神经网络中作用有很多,主要作用是给神经网络提供非线性建模能力。如果没有激活函数,那么再多层的神经网络也只能处理线性可分问题。常用的激活函数有sigmoid、tanh、relu、softmax等。它们的图形、表达式、导数等信息如下图所示:
如果搭建的神经网络层数不多,选择sigmoid、tanh、relu、softmax都可以;而如果搭建的网络层次较多,一般不宜选择sigmoid、tanh激活函数,因它们的导数都小于1,尤其是sigmoid的导数在[0,1/4]之间,多层叠加后,根据微积分链式法则,随着层数增多,导数或偏导将指数级变小。所以层数较多的激活函数需要考虑其导数不宜小于1当然也不能大于1,大于1将导致梯度爆炸。导数为1最好,而激活函数relu正好满足这个条件。所以,搭建比较深的神经网络时,一般使用relu激活函数,当然一般神经网络也可使用。一般选择relu函数即可。
2.3 误差函数的选择对于回归问题,选择均方误差(Mean squared error,MSE)即可:
2.4 权重偏差更新规则的选择权重偏差更新规则的选择实际上就是优化器的选择。最基本的优化器算法是SGD(随机梯度下降),这种算法梯度更新算法简洁,当学习率取值恰当时,可以收敛到全局最优点,但是对学习率很敏感(过小导致收敛速度过慢,过大又越过极值点),容易陷入局部最优。一般不会考虑使用SGD算法。在SGD算法基础上进行改进,可以得到带动量的SGD算法、NAG算法、AdaGrad算法、RMSProp算法以及Adam算法。
AdaGrad算法、RMSProp算法以及Adam算法是自适应优化算法,可以自动更新学习率。有时可以考虑综合使用这些优化算法,如采用先使用Adam,然后使用SGD+动量的优化方法,这个想法,实际上是由于在训练的早期阶段SGD对参数调整和初始化非常敏感。因此,可以通过先使用Adam优化算法来进行训练,这将大大地节省训练时间,且不必担心初始化和参数调整,一旦用Adam训练获得较好的参数后,就可以切换到SGD+动量优化,以达到最佳性能。
2.5 神经元数目及隐藏层数的选择目前仍然没有较好的方法确定神经元数目以及隐藏层数目,一般采用的方法就是遍历法,比如可以在10-200个神经元数目以及5-20的隐藏层数目范围内遍历,选取使得误差函数最小的结构。
2.6 代码实现'''训练部分'''
import torch.optim as optim
feature_number = 13 # 设置特征数目
out_prediction = 1 # 设置输出数目
learning_rate = 0.01 # 设置学习率
epochs = 50 # 设置训练代数
class Net(torch.nn.Module):
def __init__(self, n_feature, n_output, n_neuron1, n_neuron2,n_layer): # n_feature为特征数目,这个数字不能随便取,n_output为特征对应的输出数目,也不能随便取
self.n_feature=n_feature
self.n_output=n_output
self.n_neuron1=n_neuron1
self.n_neuron2=n_neuron2
self.n_layer=n_layer
super(Net, self).__init__()
self.input_layer = torch.nn.Linear(self.n_feature, self.n_neuron1) # 输入层
self.hidden1 = torch.nn.Linear(self.n_neuron1, self.n_neuron2) # 1类隐藏层
self.hidden2 = torch.nn.Linear(self.n_neuron2, self.n_neuron2) # 2类隐藏
self.predict = torch.nn.Linear(self.n_neuron2, self.n_output) # 输出层
def forward(self, x):
'''定义前向传递过程'''
out = self.input_layer(x)
out = torch.relu(out) # 使用relu函数非线性激活
out = self.hidden1(out)
out = torch.relu(out)
for i in range(self.n_layer):
out = self.hidden2(out)
out = torch.relu(out)
out = self.predict( # 回归问题最后一层不需要激活函数
out
) # 除去feature_number与out_prediction不能随便取,隐藏层数与其他神经元数目均可以适当调整以得到最佳预测效果
return out
net = Net(n_feature=feature_number,
n_output=out_prediction,
n_layer=1,
n_neuron1=20,
n_neuron2=20) # 这里直接确定了隐藏层数目以及神经元数目,实际操作中需要遍历
optimizer = optim.Adam(net.parameters(), learning_rate) # 使用Adam算法更新参数
criteon = torch.nn.MSELoss() # 误差计算公式,回归问题采用均方误差
for epoch in range(epochs): # 整个数据集迭代次数
for batch_idx, (data, target) in enumerate(train_data):
logits = net.forward(data) # 前向计算结果(预测结果)
loss = criteon(logits, target) # 计算损失
optimizer.zero_grad() # 梯度清零
loss.backward() # 后向传递过程
optimizer.step() # 优化权重与偏差矩阵
logit = [] # 这个是验证集,可以根据验证集的结果进行调参,这里根据验证集的结果选取最优的神经网络层数与神经元数目
target = []
for data, targets in validation: # 输出验证集的平均误差
logits = net.forward(data).detach().numpy()
targets=targets.detach().numpy()
target.append(targets[0])
logit.append(logits[0])
average_loss = criteon(logit,
target) # 计算损失
print('nTrain Epoch:{} for the Average loss of VAL
3 测试模型及可视化
使用测试集对训练好的模型进行测试,并且绘制真实值与预测值的对比图。测试集的数据不能用于超参数调整,否则会造成数据污染。
代码如下:
import matplotlib.pyplot as plt
import numpy as np
prediction = []
test_y = []
for test_x, test_ys in test:
predictions = net(test_x)
predictions=predictions.detach().numpy()
prediction.append(predictions[0])
test_ys.detach().numpy()
test_y.append(test_ys[0])
prediction = scaler.inverse_transform(np.array(prediction).reshape(
-1, 1)) # 将数据恢复至归一化之前
test_y = scaler.inverse_transform(np.array(test_y).reshape(-1, 1))
# 均方误差计算
test_loss = criteon(torch.tensor(prediction ,dtype=torch.float32), torch.tensor(test_y, dtype=torch.float32))
print('测试集均方误差:',test_loss.detach().numpy())
# 可视化
plt.figure()
plt.scatter(test_y, prediction, color='red')
plt.plot([0, 52], [0, 52], color='black', linestyle='-')
plt.xlim([-0.05, 52])
plt.ylim([-0.05, 52])
plt.xlabel('true')
plt.ylabel('prediction')
plt.title('true vs prection')
plt.show()
以上内容为pytorch实现神经网络回归预测的基本内容,包括了数据预处理、神经网络模型搭建以及激活函数,误差函数的选取(回归问题激活函数均取relu、误差函数均取mse函数,不必考虑其它函数),但是上述内容不包括神经元数目以及隐藏层数目的选取、优化器的选取(通过作者的实践来看Adam算法作为优化器效果比较好)、如何使用GPU进行加速、如何防止过拟合等方面的内容。如果对于精确度要求不高,数据集比较简单,一般采用两个隐藏层,神经元数目在200以内、优化器选择Adam算法即可,如果没有明显的过拟合现象,则不需要采取过拟合措施。
如果依据前述内容构建的模型效果不佳或者对精度有较高要求,则需要参考以下部分。
4 防止过拟合 4.1 判断过拟合随着神经网络结构越来越复杂,数据在训练集的表现会越来越好,但是在测试集的表现却越来越差,这主要是因为神经网络学习到了训练集中的一些噪声规律,这种规律在整个数据集其实是不存在的。判断过拟合的方法:数据在训练集的效果持续上升,在验证集的效果持续下降。
判断过拟合的代码需要进行如下修改:
train_loss=[]
validation_loss=[]
for epoch in range(epochs): # 整个数据集迭代次数
for batch_idx, (data, target) in enumerate(train_data):
logits = net.forward(data) # 前向计算结果(预测结果)
loss = criteon(logits, target) # 计算损失
train_losses=loss.detach().numpy()
optimizer.zero_grad() # 梯度清零
loss.backward() # 后向传递过程
optimizer.step() # 优化权重与偏差矩阵
train_loss.append(train_losses[0]) # 记录历史测试误差(每代)
logit = [] # 这个是验证集,可以根据验证集的结果进行调参,这里根据验证集的结果选取最优的神经网络层数与神经元数目
target = []
for data, targets in validation: # 输出验证集的平均误差
logits = net.forward(data).detach().numpy()
targets=targets.detach().numpy()
target.append(targets[0])
logit.append(logits[0])
average_loss = criteon(logit,
target).detach().numpy() # 计算损失
validation_loss.append(average_loss[0]) # 记录历史验证误差(每代)
# 可视化验证集误差与测试集误差
plt.figure()
plt.plot([x+1 for x in range(epochs)], validation_loss, color='black', linestyle='-',label='validation loss')
plt.plot([x+1 for x in range(epochs)], train_loss, color='red', linestyle='-',label='train loss')
plt.xlabel('epoches')
plt.ylabel('loss')
plt.legend()
plt.title('judge overfitting')
plt.show()
4.2 L2正则化(权重衰减,weight decay)
上图中c出现了过拟合现象,要对c的结果进行修正,观察b与c的拟合函数可以发现,只需要将c中函数的高次项系数衰减到接近0即可,L2正则化正是依靠此思路的一种降低模型复杂度的方法。
依据上式,可得到权重更新规则:
根据,L2正则化也别叫做权重衰减,lamda越小,权重衰减得越大。一般的优化器,如SGD、Adadelta、Adam、Adagrad、RMSprop等都自带的一个参数weight_decay,用于指定权值衰减率,相当于L2正则化中的λ参数,也就是上式中的λ。weight_decay一般可以设置0.02以下。
代码实现:
optimizer = optim.SGD(net.parameters(), learning_rate, momentum=0.9, weight_decay=1e-2) # 带动量的SGD算法实现权重衰减 optimizer = optim.Adam(net.parameters(), learning_rate,weight_decay=1e-2) # Adam算法实现动量衰减,其余算法同理4.3 Dropout
Dropout的做法是在训练过程中按一定比例(比例参数可设置)随机忽略或屏蔽一些神经元,反向传播时该神经元也不会有任何权重的更新。加入了Dropout以后,输入的特征都是有可能会被随机清除的,所以该神经元不会再特别依赖于任何一个输入特征,也就是说不会给任何一个输入设置太大的权重。由于网络模型对神经元特定的权重不那么敏感。这反过来又提升了模型的泛化能力,不容易对训练数据过拟合。
Dropout需要注意的问题:
(1)在训练阶段和测试阶段是不同的,一般在训练中使用,测试时不使用;
(2)通常丢弃率控制在20%~50%比较好,可以从20%开始尝试。如果比例太低则起不到效果,比例太高则会导致模型的欠学习;
(3)当Dropout应用在较大的网络模型时,更有可能得到效果的提升,模型有更多的机会学习到多种独立的表征;
(4)输入层和隐藏层都使用Dropout。一般来说神经元较少的层,会设keep_prob为1.0或接近于1.0的数,神经元较多的层,则会将keep_prob设置得较小,如0.5或更小。
(5)使用dropout时,需要增加学习速率和动量。把学习速率扩大10~100倍,动量值调高到0.9~0.99。
代码实现:
class Net(torch.nn.Module):
def __init__(self, n_feature, n_output, n_neuron1, n_neuron2,n_layer): # n_feature为特征数目,这个数字不能随便取,n_output为特征对应的输出数目,也不能随便取
self.n_feature=n_feature
self.n_output=n_output
self.n_neuron1=n_neuron1
self.n_neuron2=n_neuron2
self.n_layer=n_layer
super(Net, self).__init__()
self.input_layer = torch.nn.Linear(self.n_feature, self.n_neuron1)
self.hidden1 = torch.nn.Linear(self.n_neuron1, self.n_neuron2)
self.hidden2 = torch.nn.Linear(self.n_neuron2, self.n_neuron2)
self.predict = torch.nn.Linear(self.n_neuron2, self.n_output)
self.dropout = torch.nn.Dropout(p=0.5) # 使用dropout防止过拟合(记得增大学习率与动量)
def forward(self, x):
out = self.input_layer(x)
out = self.dropout(out)
out = torch.relu(out)
out = self.hidden1(out)
out = self.dropout(out)
out = torch.relu(out)
for i in range(self.n_layer):
out = self.hidden2(out)
out = self.dropout(out)
out = torch.relu(out) # 回归问题最后一层不需要激活函数
out = self.predict(
out
) # 除去feature_number与out_prediction不能随便取,隐藏层数与其他神经元数目均可以适当调整以得到最佳预测效果
return out
4.4 Batch Normalization(批量归一化)
每一次参数迭代更新后,上一层网络的输出数据经过这一层网络计算后,数据的分布会发生变化,为下一层网络的学习带来困难(神经网络的任务本就是学习数据的分布规律,分布改变则学习更加困难),这种现象叫做Internal Covariate Shift,这种现象可以使用Batch Normalization解决;而Covariate Shift主要描述的是由于训练数据和测试数据存在分布的差异性,给网络的泛化性和训练速度带来了影响,这种现象可以使用标准化解决。
一般在神经网络训练时遇到收敛速度很慢,或梯度爆炸等无法训练的状况时,都可以尝试用BN来解决。另外,在一般情况下,也可以加入BN来加快训练速度,提高模型精度,还可以大大地提高训练模型的效率。BN具体功能如下所示:
(1)可以采用初始很大的学习率,因为这个算法收敛很快,当然,即使选择了较小的学习率,也比以前的收敛速度快,因为它具有快速训练收敛的特性;
(2)采用BN算法后,可以不再考虑使用dropout以及L2正则化防止过拟合,或者可以选择更小的L2正则约束参数,因为BN具有提高网络泛化能力的特性。
class Net(torch.nn.Module):
def __init__(self, n_feature, n_output, n_neuron1, n_neuron2,n_layer): # n_feature为特征数目,这个数字不能随便取,n_output为特征对应的输出数目,也不能随便取
self.n_feature=n_feature
self.n_output=n_output
self.n_neuron1=n_neuron1
self.n_neuron2=n_neuron2
self.n_layer=n_layer
super(Net, self).__init__()
self.input_layer = torch.nn.Linear(self.n_feature, self.n_neuron1) # 输入层
self.hidden1 = torch.nn.Linear(self.n_neuron1, self.n_neuron2) # 1类隐藏层
self.hidden2 = torch.nn.Linear(self.n_neuron2, self.n_neuron2) # 2类隐藏
self.predict = torch.nn.Linear(self.n_neuron2, self.n_output) # 输出层
self.bn1 = torch.nn.BatchNorm1d(self.n_neuron1)
self.bn2 = torch.nn.BatchNorm1d(self.n_neuron2)
def forward(self, x):
'''定义前向传递过程'''
out = self.input_layer(x)
out=self.bn1(out)
out = torch.relu(out) # 使用relu函数非线性激活
out = self.hidden1(out)
out=self.bn2(out)
out = torch.relu(out)
for i in range(self.n_layer):
out = self.hidden2(out)
out=self.bn2(out)
out = torch.relu(out)
out = self.predict( # 回归问题最后一层不需要激活函数
out
) # 除去feature_number与out_prediction不能随便取,隐藏层数与其他神经元数目均可以适当调整以得到最佳预测效果
return out
4.5 权重初始化
如果算法的初始化不适当,初始值过大可能会在前向传播或反向传播中产生爆炸的值;如果太小将导致丢失信息。对收敛的算法适当的初始化能加快收敛速度,初始值的选择也将影响模型收敛局部最小值还是全局最小值。常见的参数初始化有零值初始化、随机初始化、均匀分布初始、正态分布初始和正交分布初始等。一般采用正态分布或均匀分布的初始值,实践表明正态分布、正交分布、均匀分布的初始值能带来更好的效果。
实际上,pytorch中继承nn.Module的模块参数都采取了较合理的初始化策略,不需要使用者担心初始化问题。除了使用pytorch内嵌的初始化规则外,还可以考虑使用一些智能算法(TSA, GA等)对权重以及偏差矩阵初始化。
之后的部分以后再写(包括使用GA算法优化初始权重与偏差、使用GPU加速等部分)



