1 数据预处理

1.1 读取数据

读取数据后,将数据划分为标签(y)与特征(x)两类。

这里假设数据存储在excel表格中(为了尽可能与实际情况相符,不直接使用sklearn或者pytorch自带的数据集)。数据来源是sklearn中的波士顿房价,不过作者先将数据转存到了excel中,这与实际工作中的应用场景比较相符。

所用数据可在波士顿房价预测.xlsx-讲义文档类资源-CSDN下载下载。

使用pandas读取数据:

import pandas as pd
'''导入数据'''
data = pd.read_excel('波士顿房价预测.xlsx',header=None,index_col=None)  # 一共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。值得注意的是,归一化以及标准化均不会改变数据本身的分布,标准化后的数据并不一定是标准是标准正态分布,具体分布与标准化之前的分布一致。

在大多数机器学习算法中,由于归一化对异常值非常敏感,所以通常会选择标准化进行特征缩放,在神经网络算法中也是采用标准化处理数据。

归一化:

 标准化:

x^{*}=\frac{x-\mu}{\sigma}

使用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类型的数据,因此需要将标准化后ndarray格式的数据先转化为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求解回归问题的实践中,只需要考虑输入层的释放神经元数目、输出层的接收神经元数目、隐藏层接收和释放神经元数目以及隐藏层数的选择,还有激活函数、误差计算函数以及权重和偏差更新规则的选择即可。至于如何实现正向传播、反向传播以及权重偏差更新,这并不需要专门研究,仅仅使用一段简单的python代码就可借助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函数。对于回归问题,如果不知如何选择激活函数,统统使用relu即可。

2.3 误差函数的选择

对于回归问题,选择均方误差(Mean squared error,MSE)即可:

\mathrm{MSE}=\frac{\sum_{i=1}^{n}\left(y_{i}-y_{i}^{\prime}\right)^{2}}{n}

2.4 权重偏差更新规则的选择

权重偏差更新规则的选择实际上就是优化器的选择。最基本的优化器算法是SGD( 随机梯度下降 ),这种算法的梯度更新规则十分简洁,当学习率取值恰当时,可以收敛到全局最优点,但其对学习率很敏感( 过小导致收敛速度过慢,过大又越过极值点 ),容易陷入局部最优。故深度学习实践中一般不会考虑使用SGD算法。通过改进SGD算法,可以得到带动量的SGD算法、NAG算法、AdaGrad算法、RMSProp算法以及Adam算法等等。

AdaGrad算法、RMSProp算法以及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):  # 整个数据集迭代次数
    net.train() # 启动训练模式
    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 = []
    net.eval() # 启动测试模式
    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(torch.tensor(logit), torch.tensor(target))
    print('\nTrain Epoch:{} for the Average loss of VAL

3 测试模型及可视化

使用测试集对训练好的模型进行测试,并且绘制真实值与预测值的对比图。测试集的数据不能用于超参数调整,否则会造成数据污染。

代码如下:

import matplotlib.pyplot as plt
import numpy as np
prediction = []
test_y = []
net.eval() # 启动测试模式
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):  # 整个数据集迭代次数
    net.train()
    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 = []
    net.eval()
    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(torch.tensor(logit),torch.tensor(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出现了过拟合现象。对比b与c的拟合函数可以发现,要对c进行修正,只需要将c中函数的高次项系数衰减到接近0即可,L2正则化正是依靠此思路的一种降低模型复杂度的方法:

 依据上式,可得到权重更新规则:

w_{i+1}=w_{i} *(1-\lambda)-\frac{\partial \operatorname{Loss}}{\partial w_{i}}

根据w_{i} *(1-\lambda),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。对于神经元较少的层,神经元的丢弃率设置要尽量小( 0.5以下 ),对于神经元较多的层,神经元的设置可以大一点( 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, giving_up1, giving_up2):  # 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
        self.giving_up1 = giving_up1
        self.giving_up2 = giving_up2
        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.dropout1 = torch.nn.Dropout(giving_up1) # 使用dropout防止过拟合(记得增大学习率与动量)
        self.dropout2 = torch.nn.Dropout(giving_up2) 

    def forward(self, x):
        out = self.input_layer(x)
        out = torch.relu(out)
        out = self.dropout1(out)
        out = self.hidden1(out)
        out = torch.relu(out)
        out = self.dropout2(out)
        for i in range(self.n_layer):
            out = self.hidden2(out)
            out = torch.relu(out) # 回归问题最后一层不需要激活函数
            out = self.dropout2(out)
        out = self.predict(
            out
        )  # 除去feature_number与out_prediction不能随便取,隐藏层数与其他神经元数目均可以适当调整以得到最佳预测效果
        return out

4.4 Batch Normalization(批量归一化)

每一次参数迭代更新后,上一层网络的输出数据经过该层网络的计算后,数据的分布将会发生变化,这会为下一层网络的学习带来困难( 神经网络的任务本就是学习数据的分布规律,分布改变则会导致学习更加困难 ),这种现象叫做Internal Covariate Shift,该现象可以使用Batch Normalization解决;与Internal Covariate Shift相似的一个现象叫做Covariate Shif,该现象主要描述的是训练数据和测试数据之间的分布差异性给网络泛化性和训练速度带来的影响,该现象可以使用标准化解决。

在神经网络训练时如果遇到收敛速度很慢或梯度爆炸导致无法训练的情况,可以尝试使用BN解决。当然,即使没有遇到上述问题,也可以考虑加入BN来加快训练速度,提高模型精度,还可以大大地提高训练模型的效率。BN具体优势如下:

(1)因为这BN算法收敛很快,所以可以采用初始很大的学习率。当然,即使选择了较小的学习率,也会比以前的收敛速度快;

(2)因为BN具有提高网络泛化能力的特性,采用本算法后,可以不再考虑使用dropout以及L2正则化来防止过拟合,或者可以选择更小的L2正则约束参数。

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 net.train()与net.eval()方法

net.train()方法的作用是告诉模型进行的是训练操作,训练模型前应该加上此代码;

net.eval()方法的作用是告诉模型进行的是测试操作,验证以及测试模型前应该加上此代码。

之前之所以没有特别强调net.train()以及net.eval()方法的使用,主要是因为对于一般的多层神经网络( 主要是指不带BN算法或者dropout算法的神经网络 )而言 ,net.train()以及net.eval()方法没有实质性的作用,添加与否对模型不会有任何影响。但BN算法或者dropout算法都只是在训练起作用,测试时应该将其冻结。因此,在测试以及验证模型时,net.train()以及net.eval()方法必须保留以告诉模型何时在做训练,何时在做测试。我的建议是无论模型中是否有BN算法或者dropout算法,都不要省略net.train()和net.eval()方法。

4.6 权重初始化

权重初始值过大可能会在前向传播或反向传播中产生爆炸的值;如果太小将导致丢失信息。对收敛的算法来说,适当的初始化能加快收敛速度。另外,初始值的选择也将影响模型是收敛到局部最小值还是全局最小值。常见的参数初始化方法有零值初始化、随机初始化、均匀分布初始、正态分布初始和正交分布初始等。实践表明,正态分布、正交分布、均匀分布初始化能带来更好的效果。

实际上,pytorch中凡是继承nn.Module的模块参数都采取了较合理的初始化策略,不需要使用者担心初始化问题。另外,除了使用pytorch内嵌的初始化规则外,还可以考虑使用一些智能算法(TSA, GA等)对权重以及偏差矩阵初始化。

之后的部分以后再写(包括使用贝叶斯优化方法选取超参数、使用GPU加速运算等部分)

5 超参数优化

超参数优化比较常见的方法是网格搜索。这是一种暴力搜索方法,耗费的资源极大,不过好处是以此法获得的超参数一定是最优的,没有局部最优的问题。网络搜索比较适用于待优化超参数较少( 最多3到4个 )、网络结构比较简单、数据集较小的场景,但是回归问题中的超参数至少会涉及学习率learing_rate;神经网络层数n_layer;循环学习代数epochs;每批次的大小batch_sizes;各层输入、释放的神经元个数n_neuron若干;如果使用了权重衰减,那么还有权重衰减系数weight_decay;如果使用了dropout,那么还有神经元舍弃率giving_up若干;如果使用了学习率自动衰减,那么还要设置每隔多少代才衰减的超参数step_size、衰减倍率beta等等。因此,一般不会考虑使用网格搜索去优化超参数,目前比较流行的做法采用贝叶斯优化方法。

贝叶斯优化是一种基于统计学理论的优化方法,具体原理这里不作介绍,会用就行。

import pandas as pd
'''导入数据'''
data = pd.read_excel('波士顿房价预测.xlsx', header=None,
                     index_col=None)  # 一共506组数据,每组数据13个特征,13个特征对应一个输出
y = data.loc[:, 13:13]  # 将标签数据存储在y中,表格最后一列为标签
x = data.loc[:, 0:12]  # 将特征数据存储在x中,表格前13列为特征,

from sklearn.preprocessing import StandardScaler
'''对每列数据执行标准化'''
scaler = StandardScaler()  # 实例化
X = scaler.fit_transform(x)  # 标准化特征
Y = scaler.fit_transform(y)  # 标准化标签

# x = scaler.inverse_transform(X) # 这行代码可以将数据恢复至标准化之前

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


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 _ in range(self.n_layer):
            out = self.hidden2(out)
            out = torch.relu(out)
        out = self.predict(  # 回归问题最后一层不需要激活函数
            out
        )  # 除去n_feature与out_prediction不能随便取,隐藏层数与其他神经元数目均可以适当调整以得到最佳预测效果
        return out


def structure_initialization(parameters):
    '''实例化神经网络'''
    n_layer = parameters.get('n_layer', 2)  # 若n_layer缺省则取默认值2
    n_neuron1 = parameters.get('n_neuron1', 140)
    n_neuron2 = parameters.get('n_neuron2', 140)
    learning_rate = parameters.get('learning_rate', 0.0001)
    net = Net(n_feature=13,
              n_output=1,
              n_layer=n_layer,
              n_neuron1=n_neuron1,
              n_neuron2=n_neuron2)  # 这里直接确定了隐藏层数目以及神经元数目,实际操作中需要遍历
    optimizer = torch.optim.Adam(net.parameters(),
                                 learning_rate)  # 使用Adam算法更新参数
    criteon = torch.nn.MSELoss()  # 误差计算公式,回归问题采用均方误差
    return net, optimizer, criteon


def train_evaluate(parameterization):
    '''此函数返回模型误差作为贝叶斯优化依据'''
    net, optimizer, criteon = structure_initialization(parameterization)
    batch_size = parameterization.get('batch_sizes', 6)
    epochs = parameterization.get('epochs', 100)
    # 将训练集划分批次,每batch_size个数据一批
    train_data = torch.utils.data.DataLoader(train,
                                             batch_size=batch_size,
                                             shuffle=True)
    net.train()  # 启动训练模式
    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 = []
    net.eval()  # 启动测试模式
    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(torch.tensor(logit), torch.tensor(target))  # 计算损失
    return float(average_loss)


from ax.service.managed_loop import optimize  # 使用贝叶斯优化超参数,可以使用pip install ax-platform命令安装,贝叶斯优化具体介绍见https://ax.dev/docs/bayesopt.html


def bayesian_optimization():
    best_parameters, values, experiment, model = optimize(
        parameters=[{
            "name": "learning_rate",
            "type": "range",
            "bounds": [1e-6, 0.1],
            "log_scale": True
        }, {
            "name": "n_layer",
            "type": "range",
            "bounds": [0, 4]
        }, {
            "name": "n_neuron1",
            "type": "range",
            "bounds": [40, 300]
        }, {
            "name": "n_neuron2",
            "type": "range",
            "bounds": [40, 300]
        }, {
            "name": "batch_sizes",
            "type": "range",
            "bounds": [6, 100]
        }, {
            "name": "epochs",
            "type": "range",
            "bounds": [300, 500]
        }],
        evaluation_function=train_evaluate,
        objective_name='MSE LOSS',
        total_trials=200,  # 执行200次优化
        minimize=True)  # 往最小值方向优化(默认往最大值方向优化)
    return best_parameters


best = bayesian_optimization() # 返回最优的结构

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐