深度建模目标与性能评估

215 阅读10分钟

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

一 简介

  • 对模型好坏进行评估,使用准确率、MSE等指标,模型评估是围绕某项指标在进行评估,指标好模型就好,指标不好模型就不好,其实并不完全如此。要了解模型的性能其实并不简单, 固然会使用某些指标去进行模型评估,但其实指标也只是了解模型性能的途径而不是模型性能本身。而要真实、深刻的评判模型性能,就必须首先了解机器学习的建模目标,并在此基础之上熟悉判断模型是否能够完成目标的一些方法,当然,只有真实了解的模型性能,才能进一步考虑如何提升模型性能。
  • 有监督学习则是希望在探索数字规律的基础上进一步对未来进行预测,这个预测未来,也就是预测未来某项事件的某项数值指标,首先,在统计分析领域,我们会假设现在的数据和未来的数据其实都属于某个存在但不可获得的总体,也就是说,现在和未来的数据都是从某个总体中抽样而来的,都是这个总体的样本。而正式因为这些数据属于同一个总体,因此具备某些相同的规律,而现在挖掘到的数据规律也就在某些程度上可以应用到未来的数据当中去,不过呢,不同抽样的样本之间也会有个体之间的区别,另外模型本身也无法完全捕获规律,而这些就是误差的来源。

image.png 而对于机器学习来说,并没有借助“样本-总体”的基本理论,而是简单的采用了一种后验的方法来判别模型有效性,前面说到,我们假设前后获取的数据拥有规律一致性,但数据彼此之间又略有不同,为了能够在捕捉规律的同时又能考虑到“略有不同”所带来的误差,机器学习会把当前能获取到的数据划分成训练集(trainSet)和测试集(testSet),在训练集上构建模型,然后带入测试集的数据,观测在测试集上模型预测结果和真实结果之间的差异。这个过程其实就是在模拟获取到真实数据之后模型预测的情况,模型能够在未知标签的数据集上进行预测,就是模型的核心价值,此时的测试集就是用于模拟未来的未知标签的数据集。如果模型能够在测试集上有不错的预测效果,我们就“简单粗暴”的认为模型可以在真实的未来获取的未知数据集上有不错的表现。其一般过程可以由下图表示。

image.png

二 手动实现训练集和测试集切分

import random
import time
import math
import matplotlib as mpl
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 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 tensorGenCla(num_examples=500, num_inputs=2, num_class=3, deg_dispersion=[4, 2], bias=False):
    """分类数据集创建函数。

    :param num_examples: 每个类别的数据数量
    :param num_inputs: 数据集特征数量
    :param num_class:数据集标签类别总数
    :param deg_dispersion:数据分布离散程度参数,需要输入一个列表,其中第一个参数表示每个类别数组均值的参考、第二个参数表示随机数组标准差。
    :param bias:建立模型逻辑回归模型时是否带入截距
    :return: 生成的特征张量和标签张量,其中特征张量是浮点型二维数组,标签张量是长正型二维数组。
    """

    cluster_l = torch.empty(num_examples, 1)  # 每一类标签张量的形状
    mean_ = deg_dispersion[0]  # 每一类特征张量的均值的参考值
    std_ = deg_dispersion[1]  # 每一类特征张量的方差
    lf = []  # 用于存储每一类特征张量的列表容器
    ll = []  # 用于存储每一类标签张量的列表容器
    k = mean_ * (num_class - 1) / 2  # 每一类特征张量均值的惩罚因子(视频中部分是+1,实际应该是-1)

    for i in range(num_class):
        data_temp = torch.normal(i * mean_ - k, std_, size=(num_examples, num_inputs))  # 生成每一类张量
        lf.append(data_temp)  # 将每一类张量添加到lf中
        labels_temp = torch.full_like(cluster_l, i)  # 生成类一类的标签
        ll.append(labels_temp)  # 将每一类标签添加到ll中

    features = torch.cat(lf).float()
    labels = torch.cat(ll).long()

    if bias == True:
        features = torch.cat((features, torch.ones(len(features), 1)), 1)  # 在特征张量中添加一列全是1的列
    return features, labels

# 小批量切分函数
def data_iter(batch_size, features, labels):
    """
    数据切分函数

    :param batch_size: 每个子数据集包含多少数据
    :param featurs: 输入的特征张量
    :param labels:输入的标签张量
    :return l:包含batch_size个列表,每个列表切分后的特征和标签所组成
    """
    num_examples = len(features)
    indices = list(range(num_examples))
    random.shuffle(indices)
    l = []
    for i in range(0, num_examples, batch_size):
        j = torch.tensor(indices[i: min(i + batch_size, num_examples)])
        l.append([torch.index_select(features, 0, j), torch.index_select(labels, 0, j)])
    return l
#简单线性回归向前传播函数
def linreg(X,w):
    """
    线性网络向前传播
    """
    return torch.mm(X,w)

#MSE计算函数
def MSE_loss(y_hat,y):
    """
    MSE计算公式
    """
    num_=y.numel()
    sse=torch.sum((y_hat.reshape(-1,1)-y.reshape(-1,1))**2)
    return sse/num_
def sgd(params,lr):
    """
    系数迭代函数
    """
    params.data-=lr*params.grad
    params.grad.zero_()
def  data_split(features,labels,rate=0.7):
    """
    训练集合测试集切分函数
    param features:输入的特征张量
    param labels :输入的标签张量
    param rate:训练集占所有数据的比例
    return x_train,x_test,y_train,y_test;返回特征张量的训练集,测试集,标签张量的训练集,测试集
    """
    num_examples=len(features)   #总数据量
    indices=list(range(num_examples))   #数据集行索引
    random.shuffle(indices)  #乱序调整
    num_train=int(num_examples*rate)  #训练集数据量
    indices_train=torch.tensor(indices[:num_train])   #在乱序的indices中挑出强num_train数量的行索引值
    indices_test=torch.tensor(indices[num_train:])
    x_train=features[indices_train]
    y_train=labels[indices_train]
    x_test=features[indices_test]
    y_test=labels[indices_test]
    return x_train,x_test,y_train,y_test
  • 一般来说,训练集和测试集可以按照8:2或7:3比例进行划分。在进行数据划分的过程中,如果测试集划分数据过多,参与模型训练的数据就会相应减少,而训练数据不足则会导致模型无法正常训练、损失函数无法收敛、模型过拟合等问题,但如果反过来测试集划分数据过少,则无法代表一般数据情况测试模型是否对未知数据也有很好的预测作用。因此,根据经验,我们一般来说会按照8:2或7:3比例进行划分。
  • 在机器学习领域,充斥着大量的“经验之谈”或者“约定俗成”的规则,一方面这些经验为建模提供了诸多便捷、也节省了很多算力,但另一方面,通过经验来决定影响模型效果的一些“超参数”取值的不严谨的做法,也被数理统计分析流派所诟病。

三 Dataset 与Dataloader

  • 在大多数调库建模过程中,都是先通过创建Dataset的子类并将数据保存为该子类类型,然后再使用DataLoader进行数据载入,因此更为通用的做法是先利用Dataset和DatasetLoader这两个类进行数据的读取、预处理和载入,然后再使用random_split函数进行切分。
  • Dataset类主要负责数据类的生成,在PyTorch中,所有数据集都是Dataset的子类;而DatasetLoader类则是加载模型训练的接口,二者基本使用流程如下:

image.png

3.1 数据集举例

创建一个用于表示该数据集的Dataset的子类。在创建Dataset的子类过程中,必须要重写getitem方法和len方法,其中getitem方法返回输入索引后对应的特征和标签,而len方法则返回数据集的总数据个数。当然,在必须要进行的init初始化过程中,我们也可输入可代表数据集基本属性的相关内容,包括数据集的特征、标签、大小等等,视情况而定。

class LBCDataset(Dataset):
    def __init__(self,data):     #需要输入sklearn导入的数据集
        self.features=data.data    #features返回数据集特征
        self.labels=data.target      #labels 返回数据集标签
        self.lens=len(data.data)   #返回数据集大小
        
    def __getitem__(self,index):
        #返回index对应的特征和标签
        return self.features[index,:] ,self.labels[index]
    
    def __len__(self):
        #返回数据集大小
        return self.lens
data=LBC()
LBC_data=LBCDataset(data)
# 确定训练集、测试集大小,此处以7:3划分训练集和测试集
num_train=int(LBC_data.lens*0.7)
num_test=LBC_data.lens-num_train
LBC_train,LBC_test=random_split(LBC_data,[num_train,num_test])
train_loader.dataset == LBC_train

image.png 此时切分的结果是一个映射式的对象,只有dataset和indices两个属性,其中dataset属性用于查看原数据集对象,indices属性用于查看切分后数据集的每一条数据的index(序号)。 市面上有很多教材在介绍PyTorch深度学习建模过程中的数据集划分过程,会推荐使用scikit-learn中的train_test_split函数。该函数是可以非常便捷的完成数据集切分,但这种做法只能用于单机运行的数据,并且切分之后还要调用Dataset、DataLoader模块进行数据封装和加载,切分过程看似简单,但其实会额外占用非常多的存储空间和计算资源,当进行超大规模数据训练时,所造成的影响会非常明显(当然,也有可能由于数据规模过大,本地无法运行)。因此,为了更好的适应深度学习真实应用场景,在使用包括数据切分等常用函数时,函数使用优先级是 Pytorch原生函数和类>依据张量及其常用方法手动创建的函数>Scikit-Learn函数

3.2 建模及评估过程

#生成数据
features,labels=tensorGenReg()
features=features[:,:-1]
#创建一个针对手动创建数据集的数据类
class GenData(Dataset):
    def  __init__(self,features,labels):
        self .features=features
        self.labels=labels
        self.lens=len(features)
        
    def __getitem__(self,index):
        return self.features[index,:], self.labels[index]
    
    def  __len__(self):
        #调用该方法不需要输入额外参数,方法最终范文慧数据集大小
        return self.lens
    
#实例化对象
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=10,shuffle=True)
test_loader=DataLoader(data_test,batch_size=10,shuffle=False)
#构建模型

#初始化核心参数
batch_size=10   #小批的数量
lr=0.03
num_epochs=3     #训练过程遍历的数据

#1,定义模型
class LR(nn.Module):
    def __init__(self,in_features=2,out_features=1):
        super(LR,self).__init__()
        self.linear=nn.Linear(in_features,out_features)
        
    def forward(self,x):   #正向传播规则
        out=self.linear(x)
        return out
    
#实例化模型
LR_model=LR()

#2,定义损失函数
criterion=nn.MSELoss()

#定义优化方法
optimizer=optim.SGD(LR_model.parameters(),lr=0.03)

#模型训练与测试
def fit(net,criterion ,optimizer,batchdata,epochs=3):
    for epoch in range(epochs):
        for X,y in batchdata:
            yhat=net.forward(X)
            loss=criterion(yhat,y)
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()
            
  #模型训练与测试          
fit(net=LR_model
   ,criterion=criterion
    ,optimizer=optimizer
    ,batchdata=train_loader
    ,epochs=num_epochs
   )

#查看训练模型
LR_model

#查看模型参数
list(LR_model.parameters())

image.png 计算训练集MSE

#计算训练集MSE
F.mse_loss(LR_model(data[data_train.indices][0]),data[data_train.indices][1])

image.png 计算测试集MSE

#计算测试集MSE
F.mse_loss(LR_model(data[data_test.indices][0]),data[data_test.indices][1])

image.png