Press "Enter" to skip to content

深度学习之模型拟合度概念介绍与欠拟合模型的结构调整

本站内容均来自兴趣收集,如不慎侵害的您的相关权益,请留言告知,我们将尽快删除.谢谢.

这是我参与8月更文挑战的第25天,活动详情查看:8月更文挑战

 

一 模型拟合度概念

 

在所有的模型优化问题中,最基础的也是最核心的问题,就是关于模型拟合程度的探讨与优化。模型如果能很好的捕捉总体规律,就能够有较好的未知数据的预测效果。但限制模型捕捉总体规律的原因主要有两点:

样本数据能否很好的反应总体规律,如果样本数据本身无法很好的反应总体规律,那建模的过程就算捕捉到了规律可能也无法适用于未知数据,举个极端的例子,在进行反欺诈检测时,如果要基于并未出现过欺诈案例的历史数据来进行建模,那模型就将是无可用规律可捕捉;或者,当扰动项过大时,噪声也将一定程度上掩盖真实规律。
样本数据能反应总体规律,但模型没有捕捉到,如果说要解决第一种情况需要在数据获取端多下功夫,那幺如果数据能反应总体规律而模型效果不佳,则核心原因就在模型本身了。模型评估主要依据模型在测试集上的表现,如果测试集效果不好,则认为模型还有待提升,但导致模型在测试集上效果不好的原因其实也主要有两点,其一是模型没捕捉到训练集上数据的规律,其二则是模型过分捕捉训练集上的数据规律,导致模型捕获了大量训练集独有的、无法适用于总体的规律(局部规律),而测试集也是从总体中来,这些规律也不适用于测试集。前一种情况我们称模型为欠拟合,后一种情况我们称模型为过拟合,我们可以通过以下例子进行进一步了解:

二 模型拟合度试验

 

2.1 模型过拟合

 

import random
import time
import math
import matplotlib.pyplot as plt
from mpl_toolkits .mplot3d import Axes3D
import numpy as np
import pandas as pd
import torch
from torch import nn,optim
import torch.nn.functional as F
from torch.utils .data import Dataset,TensorDataset,DataLoader
from torch.utils.data import random_split
from torch.utils .tensorboard import SummaryWriter
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity='all'

 

所用到的函数

 

# 回归类数据集创建函数
def tensorGenReg(num_examples=1000, w=[2, -1, 1], bias=True, delta=0.01, deg=1):
    """回归类数据集创建函数。
    :param num_examples: 创建数据集的数据量
    :param w: 包括截距的(如果存在)特征系数向量
    :param bias:是否需要截距
    :param delta:扰动项取值
    :param deg:方程次数
    :return: 生成的特征张和标签张量
    """
    if bias == True:
        num_inputs = len(w) - 1  # 特征张量
        features_true = torch.randn(num_examples, num_inputs)  # 不包含全是1的列的特征张量
        w_true = torch.tensor(w[:-1]).reshape(-1, 1).float()  # 自变量系数
        b_true = torch.tensor(w[-1]).float()  # 截距
        if num_inputs == 1:  # 若输入特征只有1个,则不能使用矩阵乘法
            labels_true = torch.pow(features_true, deg) * w_true + b_true
        else:
            labels_true = torch.mm(torch.pow(features_true, deg), w_true) + b_true
        features = torch.cat((features_true, torch.ones(len(features_true), 1)), 1)  # 在特征张量的最后添加一列全是1的列
        labels = labels_true + torch.randn(size=labels_true.shape) * delta
    else:
        num_inputs = len(w)
        features = torch.randn(num_examples, num_inputs)
        w_true = torch.tensor(w).reshape(-1, 1).float()
        if num_inputs == 1:
            labels_true = torch.pow(features, deg) * w_true
        else:
            labels_true = torch.mm(torch.pow(features, deg), w_true)
        labels = labels_true + torch.randn(size=labels_true.shape) * delta
    return features, labels
def split_loader(features, labels, batch_size=10, rate=0.7):
    data = GenData(features, labels)
    num_train = int(data.lens * 0.7)
    num_test = data.lens - num_train
    data_train, data_test = random_split(data, [num_train, num_test])
    train_loader = DataLoader(data_train, batch_size=batch_size, shuffle=True)
    test_loader = DataLoader(data_test, batch_size=batch_size, shuffle=False)
    return (train_loader, test_loader)

 

np.random.seed(123)
#创建数据
n_dots=20
x=np.linspace(0,1,n_dots)
y=np.sqrt(x)+0.2*np.random.rand(n_dots)-0.1

 

x是一个0到1之间等距分布20个点组成的ndarray,y=根号下x+r,其中r是人为制造的随机噪声,在[-0.1,0.1]之间服从均匀分布。使用numpy的polyfit函数来进行多项式拟合,polyfit函数会根据设置的多项式阶数,在给定数据的基础上利用最小二乘法进行拟合,并返回拟合后各阶系数。 进行多项式拟合。分别利用1阶x多项式、3阶x多项式和10阶x多项式来拟合y。并利用图形观察多项式的拟合度,首先定义一个辅助画图函数,方便观察。

 

y0=x**2
np.polyfit(x,y0,2)
p=np.poly1d(np.polyfit(x,y0,2))
print(p)
def plot_polynomial_fit(x,y,deg):
    p=np.poly1d(np.polyfit(x,y,deg))
    t=np.linspace(0,1,200)
    plt.plot(x,y,'ro',t,p(t),'-',t,np.sqrt(t),'r--')
plot_polynomial_fit(x,y,3)

 

t为[0,1]中等距分布的200个点,而p是deg参数决定的多项式回归拟合方程,p(t)即为拟合方程x输入t值时多项式输出结果,此处plot_polynomial_fit函数用于生成同时包含(x,y)原始值组成的红色点图、(t,p(t))组成的默认颜色的曲线图、(t,np.sqrt(t))构成的红色虚线曲线图。测试3阶多项式拟合结果 注意(x,y)组成的红色点图相当于带有噪声的二维空间数据分布,(t, p(t))构成的蓝色曲线相当于3阶多项式拟合原数据集((x, y)数据集)后的结果,而原始数据集包含的客观规律实际上是 =根号下x,因此最后红色的虚线(t, np.sqrt(t))实际上是代表红色点集背后的客观规律,即希望拟合多项式能够尽可能的拟合代表客观规律的红色虚线,而不是被噪声数据所吸引偏离红色虚线位置,同时也不希望完全没有捕捉到红色曲线的规律。接下来,我们尝试将1阶拟合、3阶拟合和10阶拟合绘制在一张图中。

 

plt.figure(figsize=(18,4),dpi=200)
titles=['under fitting','fitting','overfitting']
for index,deg in enumerate([1,3,10]):
    plt.subplot(1,3,index+1)
    plot_polynomial_fit(x,y,deg)
    plt.title(titles[index],fontsize=20)

 

根据最终的输出结果看到,1阶多项式拟合的时候蓝色拟合曲线即无法捕捉数据集的分布规律,离数据集背后客观规律也很远,而三阶多项式在这两方面表现良好,十阶多项式则在数据集分布规律捕捉上表现良好,但是同样偏离红色曲线较远。此时一阶多项式实际上就是欠拟合,而十阶多项式则过分捕捉噪声数据的分布规律,(如此处噪声分布为均匀分布),因此就算十阶多项式在当前训练数据集上拟合度很高,但其捕捉到的无用规律无法推广到新的数据集上,因此该模型在测试数据集上执行过程将会有很大误差。即模型训练误差很小,但泛化误差很大。
过拟合产生的根本原因,还是样本之间有“误差”,或者不同批次的数据规律不完全一致。
模型欠拟合:训练集上误差较大
模型过拟合:训练集上误差较小,但测试集上误差较大
而模型是否出现欠拟合或者过拟合,和模型复杂度有很大关系。模型越复杂,越有能力捕捉训练集上的规律,如果模型欠拟合,可以通过提高模型复杂度来进一步捕捉规律,但同时也会面临模型过于复杂而导致过拟合的风险。

2.2 模型欠拟合

 

模型欠拟合时,通过提升模型复杂度是提升模型效果的基本方法。当然,从神经网络整体模型结构来看,提升复杂度只有两种办法,

一是修改激活函数,在神经元内部对加权求和汇总之后的值进行更加复杂的处理,
二是添加隐藏层,包括隐藏层层数和每一层隐藏层的神经元个数。

创建多项式回归方程

 

#设置随机数种子
torch.manual_seed(420)
#创建最高项为2的多项式回归数据集
features,labels=tensorGenReg(w=[2,-1],bias=False,deg=2)
#绘制图查看数据分布
plt.subplot(1,2,1)
plt.scatter(features[:,0],labels)
plt.subplot(122)
plt.scatter(features[:,1],labels)

 

 

train_loader,test_loader=split_loader(features,labels)
#定义简单线性回归方程
class LR_class(nn.Module):
    def __init__(self,in_features=2,out_features=1):    #定义模型的点线结构
        super(LR_class,self).__init__()
        self.linear=nn.Linear(in_features,out_features)
        
    def forward(self,x):    #定义模型的正向传播规则
        out=self.linear(x)
        return out

 

2.2.1 模型训练

 

#设置随机种子
torch.manual_seed(420)
#实例化模型
LR=LR_class()
train_l=[]    #存储训练误差
test_l=[]     #存储测试误差
num_epochs=20
#执行循环
for epochs in range(num_epochs):
    
    fit(net=LR
       ,criterion=nn.MSELoss()
        ,optimizer=optim.SGD(LR.parameters(),lr=0.03)
        ,batchdata=train_loader
        ,epochs=epochs
       )
    train_l.append(mse_cal(train_loader,LR).detach().numpy())
    test_l.append(mse_cal(test_loader,LR).detach().numpy())
    
#绘制图像,查看mse
plt.plot(list(range(num_epochs)),train_l,label='train_mse')
plt.plot(list(range(num_epochs)),test_l,label='test_mse')
plt.legend(loc=1)

 

模型效果较差,且训练误差和测试误差均较大(此前是0.0001),模型存在欠拟合情况,接下来考虑增加模型复杂程度。

 

def model_train_test(model, 
                     train_data,
                     test_data,
                     num_epochs = 20, 
                     criterion = nn.MSELoss(), 
                     optimizer = optim.SGD, 
                     lr = 0.03, 
                     cla = False, 
                     eva = mse_cal):
    """模型误差测试函数:
    
    :param model_l:模型
    :param train_data:训练数据
    :param test_data: 测试数据   
    :param num_epochs:迭代轮数
    :param criterion: 损失函数
    :param lr: 学习率
    :param cla: 是否是分类模型
    :return:MSE列表
    """
    
    # 模型评估指标列表
    train_l = []
    test_l = []
    # 模型训练过程
    for epochs in range(num_epochs):
        fit(net = model, 
            criterion = criterion, 
            optimizer = optimizer(model.parameters(), lr = lr), 
            batchdata = train_data, 
            epochs = epochs, 
            cla = cla)
        train_l.append(eva(train_data, model).detach())
        test_l.append(eva(test_data, model).detach())
    return train_l, test_l

 

2.2.2 增加模型复杂度测试效果

 

#设置随机种子
torch.manual_seed(300)
#实例化模型
LR=LR_class()
#模型训练
train_l,test_l=model_train_test(LR
                               ,train_loader
                                ,test_loader
                                ,num_epochs=20
                                ,criterion=nn.MSELoss()
                                ,optimizer=optim.SGD
                                ,lr=0.03
                                ,cla=False
                                ,eva=mse_cal
                               )
plt.plot(list(range(num_epochs)),train_l,label='train_mse')
plt.plot(list(range(num_epochs)),test_l,label='test_mse')
plt.legend(loc=1)

 

在神经网络基本结构下,如何提升模型复杂度从而使得可以对多元线性回归数据进行建模呢?首先,在激活函数仅为线性变换y=x情况下,增加层数并不会对结果有显着提升,通过如下实验验证:

 

class LR_class1(nn.Module):
    def __init__ (self,in_features=2,n_hidden=4,out_features=1):  #设置隐藏层设置四个激活函数
        super (LR_class1,self).__init__()
        self.linear1=nn.Linear(in_features,n_hidden)
        self.linear2=nn.Linear(n_hidden,out_features)
        
    def forward(self,x):
        z1=self.linear1(x)
        out=self.linear2(z1)
        return out

 

# 设置随机数种子
torch.manual_seed(420)  
# 实例化模型
LR1 = LR_class1()
# 模型训练
train_l, test_l = model_train_test(LR1, 
                                   train_loader,
                                   test_loader,
                                   num_epochs = 20, 
                                   criterion = nn.MSELoss(), 
                                   optimizer = optim.SGD, 
                                   lr = 0.03, 
                                   cla = False, 
                                   eva = mse_cal)
# 绘制图像,查看MSE变化情况
plt.plot(list(range(num_epochs)), train_l, label='train_mse')
plt.plot(list(range(num_epochs)), test_l, label='test_mse')
plt.legend(loc = 1)

 

结果没有显着提升,但模型稳定性却有所提升。对于叠加线性层的神经网络模型来说,由于模型只是对数据仿射变换,因此并不能满足拟合高次项的目的。也就是说,在增加模型复杂度的过程中,首先需要激活函数的配合,然后增加模型的层数和每层的神经元个数。模型越复杂,输出结果越平稳,但这只是一个局部规律,其实在大多数时候,模型越复杂,输出结果越不一定平稳。

 

三 激活函数性能比较

 

3.1 常用激活函数对比

 

对于激活函数来说,不同激活函数效果差异非常明显,输出层的激活函数和隐藏层的激活函数应该分开对待,隐藏层的激活函数是为了对数据进行非线性变换,而输出层的激活函数一般都是为了满足某种特定的输出结果所设计的。

 

#sigmoid激活函数
class Sigmoid_class1(nn.Module):
    def __init__(self,in_features=2,n_hidden=4,out_features=1,bias=True):
        super(Sigmoid_class1,self).__init__()
        self.linear1=nn.Linear(in_features,n_hidden,bias=bias)
        self.linear2=nn.Linear(n_hidden,out_features,bias=bias)
        
    def forward(self,x):
        z1=self.linear1(x)
        p1=torch.sigmoid(z1)
        out=self.linear2(p1)
        return out
    
#tanh激活函数
class tanh_class1(nn.Module):
    def __init__(self,in_features=2,n_hidden=4,out_features=1,bias=True):
        super(tanh_class1,self).__init__()
        self.linear1=nn.Linear(in_features,n_hidden,bias=bias)
        self.linear2=nn.Linear(n_hidden,out_features,bias=bias)
        
    def forward(self,x):
        z1=self.linear1(x)
        p1=torch.tanh(z1)
        out=self.linear2(p1)
        return out
    
#RELu激活函数
class ReLU_class1(nn.Module):
    def __init__(self,in_features=2,n_hidden=4,out_features=1,bias=True):
        super(ReLU_class1,self).__init__()
        self.linear1=nn.Linear(in_features,n_hidden,bias=bias)
        self.linear2=nn.Linear(n_hidden,out_features,bias=bias)
    
    def forward(self,x):
        z1=self.linear1(x)
        p1=torch.relu(z1)
        out=self.linear2(p1)
        return out

 

3.1.1 实例化模型

 

torch.manual_seed(420)  
LR1 = LR_class1()
sigmoid_model1 = Sigmoid_class1()
tanh_model1 = tanh_class1()
relu_model1 = ReLU_class1()
model_l = [LR1, sigmoid_model1, tanh_model1, relu_model1]           # 将实例化后模型放在一个列表容器中
name_l = ['LR1', 'sigmoid_model1', 'tanh_model1', 'relu_model1']

 

3.1.2 定义核心参数,训练集,测试集MSE存储张量

 

num_epochs = 30
lr = 0.03
mse_train = torch.zeros(len(model_l), num_epochs)
mse_test = torch.zeros(len(model_l), num_epochs)

 

3.1.3 模型训练

 

for epochs in range(num_epochs):
    for i, model in enumerate(model_l):
        fit(net = model, 
            criterion = nn.MSELoss(), 
            optimizer = optim.SGD(model.parameters(), lr = lr), 
            batchdata = train_loader, 
            epochs = epochs)
        mse_train[i][epochs] = mse_cal(train_loader, model).detach()
        mse_test[i][epochs] = mse_cal(test_loader, model).detach()

 

训练误差绘图

 

# 训练误差
for i, name in enumerate(name_l):
    plt.plot(list(range(num_epochs)), mse_train[i], label=name)
plt.legend(loc = 1)
plt.title('mse_train')

 

测试误差绘图

 

# 测试误差
for i, name in enumerate(name_l):
    plt.plot(list(range(num_epochs)), mse_test[i], label=name)
plt.legend(loc = 1)
plt.title('mse_test')

 

相比其他激活函数,ReLU激活函数效果明显更好。

 

四 构建复杂神经网络

 

在初步判断ReLU激活函数效果好于Sigmoid激活函数和tanh激活函数之后,增加模型复杂度,也就是添加隐藏层来构建更加复杂的神经网络模型。 首先是ReLU激活函数的叠加,那幺我们考虑添加几层隐藏层并考虑在隐藏层中使用ReLU函数,也就是所谓的添加ReLU层。此处我们在ReLU_class1的基础上创建ReLU_class2结构如下:

 

 

class ReLU_class2(nn.Module):                                   
    def __init__(self, in_features=2, n_hidden_1=4, n_hidden_2=4, out_features=1, bias=True):       
        super(ReLU_class2, self).__init__()
        self.linear1 = nn.Linear(in_features, n_hidden_1, bias=bias)
        self.linear2 = nn.Linear(n_hidden_1, n_hidden_2, bias=bias)
        self.linear3 = nn.Linear(n_hidden_2, out_features, bias=bias)
        
    def forward(self, x):                                   
        z1 = self.linear1(x)
        p1 = torch.relu(z1)
        z2 = self.linear2(p1)
        p2 = torch.relu(z2)
        out = self.linear3(p2)
        return out

 

模型测试

 

# 创建随机数种子
torch.manual_seed(24)  
# 实例化模型
relu_model1 = ReLU_class1()
relu_model2 = ReLU_class2()
# 模型列表容器
model_l = [relu_model1, relu_model2]           
name_l = ['relu_model1', 'relu_model2']
# 核心参数
num_epochs = 20
lr = 0.03

 

def model_comparison(model_l, 
                     name_l, 
                     train_data,
                     test_data,
                     num_epochs = 20, 
                     criterion = nn.MSELoss(), 
                     optimizer = optim.SGD, 
                     lr = 0.03, 
                     cla = False, 
                     eva = mse_cal):
    """模型对比函数:
    
    :param model_l:模型序列
    :param name_l:模型名称序列
    :param train_data:训练数据
    :param test_data:测试数据    
    :param num_epochs:迭代轮数
    :param criterion: 损失函数
    :param lr: 学习率
    :param cla: 是否是分类模型
    :return:MSE张量矩阵 
    """
    # 模型评估指标矩阵
    train_l = torch.zeros(len(model_l), num_epochs)
    test_l = torch.zeros(len(model_l), num_epochs)
    # 模型训练过程
    for epochs in range(num_epochs):
        for i, model in enumerate(model_l):
            fit(net = model, 
                criterion = criterion, 
                optimizer = optimizer(model.parameters(), lr = lr), 
                batchdata = train_data, 
                epochs = epochs, 
                cla = cla)
            train_l[i][epochs] = eva(train_data, model).detach()
            test_l[i][epochs] = eva(test_data, model).detach()
    return train_l, test_l

 

train_l, test_l = model_comparison(model_l = model_l, 
                                   name_l = name_l, 
                                   train_data = train_loader, 
                                   test_data = test_loader,
                                   num_epochs = num_epochs, 
                                   criterion = nn.MSELoss(), 
                                   optimizer = optim.SGD, 
                                   lr = 0.03, 
                                   cla = False, 
                                   eva = mse_cal)

 

4.1 训练误差绘图

 

# 训练误差
for i, name in enumerate(name_l):
    plt.plot(list(range(num_epochs)), train_l[i], label=name)
plt.legend(loc = 1)
plt.title('mse_train')

 

测试误差绘图

 

# 测试误差
for i, name in enumerate(name_l):
    plt.plot(list(range(num_epochs)), test_l[i], label=name)
plt.legend(loc = 1)
plt.title('mse_train')

 

模型效果并没有明显提升,反而出现了更多的波动,迭代收敛速度也有所下降。模型效果无法提升猜测因为模型还不够复杂,尝试继续添加隐藏层

 

# 设置随机数种子
torch.manual_seed(420)  
# 创建最高项为2的多项式回归数据集
features, labels = tensorGenReg(w=[2, -1], bias=False, deg=2)
# 进行数据集切分与加载
train_loader, test_loader = split_loader(features, labels)

 

# 构建三个隐藏层的神经网络
class ReLU_class3(nn.Module):                                   
    def __init__(self, in_features=2, n_hidden1=4, n_hidden2=4, n_hidden3=4, out_features=1, bias=True):       
        super(ReLU_class3, self).__init__()
        self.linear1 = nn.Linear(in_features, n_hidden1, bias=bias)
        self.linear2 = nn.Linear(n_hidden1, n_hidden2, bias=bias)
        self.linear3 = nn.Linear(n_hidden2, n_hidden3, bias=bias)
        self.linear4 = nn.Linear(n_hidden3, out_features, bias=bias) 
        
    def forward(self, x):                                    
        z1 = self.linear1(x)
        p1 = torch.relu(z1)
        z2 = self.linear2(p1)
        p2 = torch.relu(z2)
        z3 = self.linear3(p2)
        p3 = torch.relu(z3)
        out = self.linear4(p3)
        return out
# 构建四个隐藏层的神经网络
class ReLU_class4(nn.Module):                                   
    def __init__(self, in_features=2, n_hidden1=4, n_hidden2=4, n_hidden3=4, n_hidden4=4, out_features=1, bias=True):       
        super(ReLU_class4, self).__init__()
        self.linear1 = nn.Linear(in_features, n_hidden1, bias=bias)
        self.linear2 = nn.Linear(n_hidden1, n_hidden2, bias=bias)
        self.linear3 = nn.Linear(n_hidden2, n_hidden3, bias=bias)
        self.linear4 = nn.Linear(n_hidden3, n_hidden4, bias=bias)
        self.linear5 = nn.Linear(n_hidden4, out_features, bias=bias) 
        
    def forward(self, x):                                    
        z1 = self.linear1(x)
        p1 = torch.relu(z1)
        z2 = self.linear2(p1)
        p2 = torch.relu(z2)
        z3 = self.linear3(p2)
        p3 = torch.relu(z3)
        z4 = self.linear4(p3)
        p4 = torch.relu(z4)
        out = self.linear5(p4)
        return out

 

# 创建随机数种子
torch.manual_seed(24)  
# 实例化模型
relu_model1 = ReLU_class1()
relu_model2 = ReLU_class2()
relu_model3 = ReLU_class3()
relu_model4 = ReLU_class4()
# 模型列表容器
model_l = [relu_model1, relu_model2, relu_model3, relu_model4]           
name_l = ['relu_model1', 'relu_model2', 'relu_model3', 'relu_model4']
# 核心参数
num_epochs = 20
lr = 0.03

 

train_l, test_l = model_comparison(model_l = model_l, 
                                   name_l = name_l, 
                                   train_data = train_loader, 
                                   test_data = test_loader,
                                   num_epochs = num_epochs, 
                                   criterion = nn.MSELoss(), 
                                   optimizer = optim.SGD, 
                                   lr = lr, 
                                   cla = False, 
                                   eva = mse_cal)

 

# 训练误差
for i, name in enumerate(name_l):
    plt.plot(list(range(num_epochs)), train_l[i], label=name)
plt.legend(loc = 4)
plt.title('mse_train')

 

 

# 测试误差
for i, name in enumerate(name_l):
    plt.plot(list(range(num_epochs)), test_l[i], label=name)
plt.legend(loc = 4)
plt.title('mse_test')

 

在堆叠ReLU激活函数的过程中,模型效果并没有朝向预想的方向发展,MSE不仅没有越来越低,model3和model4甚至出现了模型失效的情况!这充分的说明,模型构建并非越复杂越好。伴随模型复杂度增加,模型收敛速度变慢、收敛过程波动增加、甚至有可能出现模型失效的情况。当前实验复杂模型出现问题并不是算法理论本身出了问题,而是缺乏了解决这些问题的“技术手段”,就是模型优化方法。其实这也从侧面说明了优化算法的重要性。

Be First to Comment

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注