NNDL 实验三 线性回归


2.2 线性回归

2.2.1 数据集构建

构造一个小的回归数据集:

生成 150 个带噪音的样本,其中 100 个训练样本,50 个测试样本,并打印出训练数据的可视化分布。

import torch as tc
from matplotlib import pyplot as plt  # matplotlib 是 Python 的绘图库
from random import *


def linear_func(x, w=1.2, b=0.5):
    y = w * x + b
    return y


def create_toy_data(func, interval, sample_num, noise, add_outlier, outlier_ratio):
    
    # 均匀采样
    # 使用torch.rand在生成sample_num个随机数,这些数范围在interval[0]到interval[1]之间,并组成一个张量
    X = tc.rand(sample_num) * (interval[1] - interval[0]) + interval[0]
    y = func(X)

    # 生成高斯分布的标签噪声
    # 使用torch.normal生成0均值,noise标准差的数据
    epsilon = tc.normal(0, noise, size=y.shape)
    y = y + epsilon

    if add_outlier:  # 生成额外的异常点
        outlier_num = int(len(y) * outlier_ratio)
        if outlier_num != 0:
            # 使用torch.randint生成服从均匀分布的、范围在[0, len(y))的随机Tensor
            outlier_idx = tc.randint(outlier_num, (0, len(y)))
            y[outlier_idx] = y[outlier_idx] * 5
    return X, y


func = linear_func
interval = (-10, 10)
train_num = 100  # 训练样本数目
test_num = 50  # 测试样本数目
noise = 2
add_outlier = False
outlier_ratio = 0.001
X_train, y_train = create_toy_data(func, interval, train_num, noise, add_outlier, outlier_ratio)
X_test, y_test = create_toy_data(func, interval, test_num, noise, add_outlier, outlier_ratio)
X_train_large, y_train_large = create_toy_data(func, interval, 5000, noise, add_outlier, outlier_ratio)

# paddle.linspace返回一个Tensor,Tensor的值为在区间start和stop上均匀间隔的num个值,输出Tensor的长度为num
X_underlying = tc.linspace(interval[0], interval[1], train_num)  # 从interval[0]开始,到interval[1]结束,均匀生成train_num个点组成一个数组
y_underlying = linear_func(X_underlying)

# 绘制数据
plt.scatter(X_train, y_train, marker='*', color='r', s=20, label="train data")
plt.scatter(X_test, y_test, color='y', s=20, label="test data")
plt.plot(X_underlying, y_underlying, c='k', label=r"underlying distribution")
plt.legend(fontsize='x-large')  # 给图像加图例
# plt.savefig('ml-vis.pdf')  # 保存图像到PDF文件中
plt.show()

在这里插入图片描述

2.2.2 模型构建

在这里插入图片描述
代码如下:

# 线性算子
class Linear():
    def __init__(self, input_size):
        '''
        输入:
        - input_size:模型要处理的数据特征向量长度
        '''

        self.input_size = input_size

        # 模型参数
        self.params = {} # 定义为一个对象
        self.params['w'] = torch.randn(size=[self.input_size, 1], dtype=torch.float32) # 随机初始化一个数值
        self.params['b'] = torch.zeros(size=[1], dtype=torch.float32) # 全是1

    def __call__(self, X):
        return self.forward(X)

    # 前向函数
    def forward(self, X):
        '''
        :param X: tensor, shape=[N,D],这里的X矩阵是由N个x向量的转置拼接成的
        :return:
        - y_pred: tensor,shape=[N]
        '''
        N, D = X.shape
        
		# 大小为0
        if self.input_size == 0:
            return torch.full(size=[N, 1], fill_value=self.params['b'])

        assert D == self.input_size  # 输入数据维度合法性验证

        # 使用torch.matmul 计算两个tensor的乘积
        y_pred = torch.matmul(X, self.params['w']) + self.params['b']

        return y_pred
# 为了和后面章节统一,这里X矩阵是由N个x向量的转置拼接成的,
input_size = 3
N = 2
X = torch.randn(size=[N, input_size], dtype=torch.float32)
model = Linear(input_size)
y_pred = model(X)
print("y_pred:", y_pred)

运行结果:

y_pred: tensor([[1.8529],
        [0.6011]])

2.2.3 损失函数

回归任务中常用的评估指标是均方误差。均方误差(mean-square error, MSE)是反映估计量与被估计量之间差异程度的一种度量。
在这里插入图片描述
其中b为N维向量,所有元素取值都为b

思考:没有除2合理么?谈谈自己的看法。
合理,均方误差除2的作用是在模型优化时,消除因为计算均方误差中平方的梯度时存在的2倍系数,不除2依然能够在后续优化时正确计算。

代码如下:

import torch

# 定义一个函数
def mean_squared_error(y_true, y_pred):

    assert y_true.shape[0] == y_pred.shape[0]

    # torch.square计算输入的平方值
    # torch.mean沿dim计算 x的平均值,默认axis是None,则对输入的全部元素计算平均值。
    # 公式2.8的代码实现,没有除以2
    error = torch.mean(torch.square(y_true - y_pred))

    return error


# 构造一个简单的样例进行测试:[N,1],N=2
y_true = torch.tensor([[-0.2], [4.9]], dtype=torch.float32)
y_pred = torch.tensor([[1.3], [2.5]], dtype=torch.float32)

error = mean_squared_error(y_true=y_true, y_pred=y_pred).item()
print("error:", error)

运行结果:

error: 4.005000114440918

2.2.4 模型优化

经验风险 ( Empirical Risk ),即在训练集上的平均损失。

思考1. 为什么​​​​​​​​​​​​​​​​​​​​​省略了不影响效果?
在这里插入图片描述

思考 2. 什么是最小二乘法 ( Least Square Method , LSM )
最小二乘法是一种优化方法,它通过最小化误差(真实目标对象与拟合目标对象的差)的平方和寻找数据的最佳函数匹配。最小二乘法同梯度下降类似,都是一种求解无约束最优化问题的常用方法,并且也可以用于曲线拟合,来解决回归问题。
代码如下:

def optimizer_lsm(model, X, y, reg_lambda=0):
    
    N, D = X.shape

    # 对输入特征数据所有特征向量求平均
    x_bar_tran = torch.mean(X, dim=0).t() # tensor.t()转置
    # 对标签的均值,shape=[1]
    y_bar = torch.mean(y)
    # torch.subtract通过广播的方式实现矩阵减向量
    x_sub = torch.subtract(X, x_bar_tran)

    # 使用torch.all判断输入tensor是否全是0
    if torch.all(x_sub == 0):
        model.params['b'] = y_bar
        model.params['w'] = torch.zeros(size=[D])
        return model

    # torch.inverse求方阵的逆,这里对应于公式(2.14)
    tmp = torch.inverse(torch.matmul(x_sub.T, x_sub) + reg_lambda * torch.eye(n=(D)))
    w = torch.matmul(torch.matmul(tmp, x_sub.T), (y - y_bar))
	
	# 公式(2.13)
    b = y_bar - torch.matmul(x_bar_tran, w)
    
    model.params['b'] = b
    model.params['w'] = torch.squeeze(w, dim=-1) # 矩阵压缩
    return model

2.2.5 模型训练

在准备了数据、模型、损失函数和参数学习的实现之后,开始模型的训练。
在回归任务中,模型的评价指标和损失函数一致,都为均方误差。
代码如下:

input_size = 1 # 设置大小
model = Linear(input_size)
model = optimizer_lsm(model, X_train.reshape([-1, 1]), y_train.reshape([-1, 1]))
print("w_pred:", model.params['w'].item(), "b_pred: ", model.params['b'].item())

y_train_pred = model(X_train.reshape([-1, 1])).squeeze()
train_error = mean_squared_error(y_true=y_train, y_pred=y_train_pred).item()
print("train error: ", train_error)
model_large = Linear(input_size)
model_large = optimizer_lsm(model_large, X_train_large.reshape([-1, 1]), y_train_large.reshape([-1, 1]))
print("w_pred large:", model_large.params['w'].item(), "b_pred large: ", model_large.params['b'].item())

y_train_pred_large = model_large(X_train_large.reshape([-1, 1])).squeeze()
train_error_large = mean_squared_error(y_true=y_train_large, y_pred=y_train_pred_large).item()
print("train error large: ", train_error_large)

运行结果:

w_pred: 1.2093340158462524 b_pred:  0.8263794779777527
train error:  3.646909713745117
w_pred large: 1.2025229930877686 b_pred large:  0.4961366057395935
train error large:  4.008014678955078

2.2.6 模型评估

用训练好的模型预测一下测试集的标签,并计算在测试集上的损失。
代码如下:

y_test_pred = model(X_test.reshape([-1, 1])).squeeze()
test_error = mean_squared_error(y_true=y_test, y_pred=y_test_pred).item()
print("test error: ", test_error)

y_test_pred_large = model_large(X_test.reshape([-1, 1])).squeeze()
test_error_large = mean_squared_error(y_true=y_test, y_pred=y_test_pred_large).item()
print("test error large: ", test_error_large)

运行结果:

test error:  4.337008476257324
test error large:  4.394468307495117

2.2.7 样本数量 & 正则化系数

(1) 调整训练数据的样本数量,由 100 调整到 5000,观察对模型性能的影响。
代码如下:

train_num = 5000  # 训练样本数目
test_num = 50  # 测试样本数目
noise = 2
add_outlier = False
outlier_ratio = 0.001
X_train, y_train = create_toy_data(func, interval, train_num, noise, add_outlier, outlier_ratio)
X_test, y_test = create_toy_data(func, interval, test_num, noise, add_outlier, outlier_ratio)
X_train_large, y_train_large = create_toy_data(func, interval, 5000, noise, add_outlier, outlier_ratio)

# paddle.linspace返回一个Tensor,Tensor的值为在区间start和stop上均匀间隔的num个值,输出Tensor的长度为num
X_underlying = tc.linspace(interval[0], interval[1], train_num)  # 从interval[0]开始,到interval[1]结束,均匀生成train_num个点组成一个数组
y_underlying = linear_func(X_underlying)

# 绘制数据
plt.scatter(X_train, y_train, marker='*', color='r', s=20, label="train data")
plt.scatter(X_test, y_test, color='y', s=20, label="test data")
plt.plot(X_underlying, y_underlying, c='k', label=r"underlying distribution")
plt.legend(fontsize='x-large')  # 给图像加图例
# plt.savefig('ml-vis.pdf')  # 保存图像到PDF文件中
plt.show()

运行结果:
在这里插入图片描述

w_pred: 1.2008944749832153 b_pred:  0.5172439813613892
train error:  4.0114874839782715

(2) 调整正则化系数,观察对模型性能的影响。
代码如下:

input_size = 1
model = Linear(input_size)
model = optimizer_lsm(model, X_train.reshape([-1, 1]), y_train.reshape([-1, 1]),reg_lambda=50)
print("w_pred:", model.params['w'].item(), "b_pred: ", model.params['b'].item())

y_train_pred = model(X_train.reshape([-1, 1])).squeeze()
train_error = mean_squared_error(y_true=y_train, y_pred=y_train_pred).item()
print("train error: ", train_error)

model_large = Linear(input_size)
model_large = optimizer_lsm(model_large, X_train_large.reshape([-1, 1]), y_train_large.reshape([-1, 1]),reg_lambda=50)
print("w_pred large:", model_large.params['w'].item(), "b_pred large: ", model_large.params['b'].item())

y_train_pred_large = model_large(X_train_large.reshape([-1, 1])).squeeze()
train_error_large = mean_squared_error(y_true=y_train_large, y_pred=y_train_pred_large).item()
print("train error large: ", train_error_large)

运行结果:

w_pred: 1.2039399147033691 b_pred:  0.8823827505111694
train error:  2.8416028022766113

w_pred large: 1.1993708610534668 b_pred large:  0.425101637840271
train error large:  4.069814682006836

2.3 多项式回归

2.3.1 数据集构建

构建训练和测试数据,其中:
训练数样本 15 个,测试样本 10 个,高斯噪声标准差为 0.1,自变量范围为 (0,1)。

# sin函数: sin(2 * pi * x)
def sin(x):
    y = torch.sin(2 * math.pi * x)
    return y
# 这里仍然使用create_toy_data函数来构建训练和测试数据,
# 其中训练数样本 15 个,测试样本 10 个,高斯噪声标准差为 0.1,自变量范围为 (0,1)。
func = sin
interval = (0, 1)
train_num = 15
test_num = 10
noise = 0.5
X_train, y_train = create_toy_data(func=func, interval=interval, sample_num=train_num, noise=noise, add_outlier=False)
X_test, y_test = create_toy_data(func=func, interval=interval, sample_num=test_num, noise=noise, add_outlier=False)

X_underlying = torch.linspace(interval[0], interval[1], steps=100)
y_underlying = sin(X_underlying)

# 绘制图像
plt.rcParams['figure.figsize'] = (8.0, 6.0)
plt.scatter(X_train, y_train, facecolor="none", edgecolor='#e4007f', s=50, label="train data")
plt.plot(X_underlying, y_underlying, c='#000000', label=r"$\sin(2\pi x)$")
plt.legend(fontsize='x-large')
plt.savefig('ml-vis2.pdf')
plt.show()

运行结果:
在这里插入图片描述

2.3.2 模型构建

套用求解线性回归参数的方法来求解多项式回归参数

def polynomial_basis_function(x, degree=2):
	#如果阶数为0 
    if degree == 0:
        return torch.ones(size=x.shape, dtype=torch.float32)

    x_tmp = x
    x_result = x_tmp

    for i in range(2, degree + 1):
        x_tmp = torch.multiply(x_tmp, x)  # 逐元素相乘
        x_result = torch.concat((x_result, x_tmp), dim=-1)

    return x_result

# 简单测试
data = [[2], [3], [4]]
X = torch.tensor(data=data, dtype=torch.float32)
degree = 3
transformed_X = polynomial_basis_function(X, degree=degree)
print("转换前:", X)
print("阶数为", degree, "转换后:", transformed_X)

运行结果:

转换前: tensor([[2.],
        [3.],
        [4.]])
阶数为 3 转换后: tensor([[ 2.,  4.,  8.],
        [ 3.,  9., 27.],
        [ 4., 16., 64.]])

2.3.3 模型训练

对于多项式回归,我们可以同样使用前面线性回归中定义的LinearRegression算子、训练函数train、均方误差函数mean_squared_error。


plt.rcParams['figure.figsize'] = (12.0, 8.0)

for i, degree in enumerate([0, 1, 3, 8]):  # []中为多项式的阶数
    model = Linear(degree)
    X_train_transformed = polynomial_basis_function(X_train.reshape([-1, 1]), degree)
    X_underlying_transformed = polynomial_basis_function(X_underlying.reshape([-1, 1]), degree)

    model = optimizer_lsm(model, X_train_transformed, y_train.reshape([-1, 1]))  # 拟合得到参数

    y_underlying_pred = model(X_underlying_transformed).squeeze()

    print(model.params)

    # 绘制图像
    plt.subplot(2, 2, i + 1)
    plt.scatter(X_train, y_train, facecolor="none", edgecolor='#e4007f', s=50, label="train data")
    plt.plot(X_underlying, y_underlying, c='#000000', label=r"$\sin(2\pi x)$")
    plt.plot(X_underlying, y_underlying_pred, c='#f19ec2', label="predicted function")
    plt.ylim(-2, 1.5)
    plt.annotate("M={}".format(degree), xy=(0.95, -1.4))


plt.legend(loc='lower left', fontsize='x-large')
plt.savefig('ml-vis3.pdf')
plt.show()

运行结果:
在这里插入图片描述

{'w': tensor([0.]), 'b': tensor(-0.1441)}
{'w': tensor([-0.9384]), 'b': tensor([0.3150])}
{'w': tensor([ 12.5155, -34.8445,  23.1802]), 'b': tensor([-0.3165])}
{'w': tensor([   28.5825,  -102.2395,  -195.0573,  1368.8866, -1590.4924, -1107.0732,
         3001.4561, -1407.4734]), 'b': tensor([-0.8036])}

观察可视化结果,红色的曲线表示不同阶多项式分布拟合数据的结果:

当M=0 或 M=1 时,拟合曲线较简单,模型欠拟合;
当 M=8 时,拟合曲线较复杂,模型过拟合;
当 M=3 时,模型拟合最为合理。

2.3.4 模型评估

通过均方误差来衡量训练误差、测试误差以及在没有噪音的加入下sin函数值与多项式回归值之间的误差,更加真实地反映拟合结果。多项式分布阶数从0到8进行遍历。
对于模型过拟合的情况,可以引入正则化方法,通过向误差函数中添加一个惩罚项来避免系数倾向于较大的取值。

代码如下:

# 训练误差和测试误差
training_errors = []
test_errors = []
distribution_errors = []

# 遍历多项式阶数
for i in range(9):
    model = Linear(i)

    X_train_transformed = polynomial_basis_function(X_train.reshape([-1, 1]), i)
    X_test_transformed = polynomial_basis_function(X_test.reshape([-1, 1]), i)
    X_underlying_transformed = polynomial_basis_function(X_underlying.reshape([-1, 1]), i)

    optimizer_lsm(model, X_train_transformed, y_train.reshape([-1, 1]))

    y_train_pred = model(X_train_transformed).squeeze()
    y_test_pred = model(X_test_transformed).squeeze()
    y_underlying_pred = model(X_underlying_transformed).squeeze()

    train_mse = mean_squared_error(y_true=y_train, y_pred=y_train_pred).item()
    training_errors.append(train_mse)

    test_mse = mean_squared_error(y_true=y_test, y_pred=y_test_pred).item()
    test_errors.append(test_mse)

print("train errors: \n", training_errors)
print("test errors: \n", test_errors)

# 绘制图片
plt.rcParams['figure.figsize'] = (8.0, 6.0)
plt.plot(training_errors, '-.', mfc="none", mec='#e4007f', ms=10, c='#e4007f', label="Training")
plt.plot(test_errors, '--', mfc="none", mec='#f19ec2', ms=10, c='#f19ec2', label="Test")
plt.legend(fontsize='x-large')
plt.xlabel("degree")
plt.ylabel("MSE")
plt.savefig('ml-mse-error.pdf')
plt.show()

运行结果:
在这里插入图片描述

train errors: 
 [0.6044527888298035, 0.37083032727241516, 0.36677995324134827, 0.2636880576610565, 0.26361265778541565, 0.23196382820606232, 0.20245333015918732, 0.1918744593858719, 0.476275235414505]
test errors:
 [0.33158332109451294, 0.1769583523273468, 0.18401207029819489, 0.1404038369655609, 0.13875068724155426, 0.3831053078174591, 0.6612722277641296, 2.1085290908813477, 2.7414708137512207]

观察可视化结果:

当阶数较低的时候,模型的表示能力有限,训练误差和测试误差都很高,代表模型欠拟合;
当阶数较高的时候,模型表示能力强,但将训练数据中的噪声也作为特征进行学习,一般情况下训练误差继续降低而测试误差显著升高,代表模型过拟合。

引入正则化方法,通过向误差函数中添加一个惩罚项来避免系数倾向于较大的取值。
代码如下:

degree = 8 # 多项式阶数
reg_lambda = 0.0001 # 正则化系数

X_train_transformed = polynomial_basis_function(X_train.reshape([-1,1]), degree)
X_test_transformed = polynomial_basis_function(X_test.reshape([-1,1]), degree)
X_underlying_transformed = polynomial_basis_function(X_underlying.reshape([-1,1]), degree)

model = Linear(degree)

optimizer_lsm(model,X_train_transformed,y_train.reshape([-1,1]))

y_test_pred=model(X_test_transformed).squeeze()
y_underlying_pred=model(X_underlying_transformed).squeeze()

model_reg = Linear(degree)

optimizer_lsm(model_reg,X_train_transformed,y_train.reshape([-1,1]),reg_lambda=reg_lambda)

y_test_pred_reg=model_reg(X_test_transformed).squeeze()
y_underlying_pred_reg=model_reg(X_underlying_transformed).squeeze()

mse = mean_squared_error(y_true = y_test, y_pred = y_test_pred).item()
print("mse:",mse)
mes_reg = mean_squared_error(y_true = y_test, y_pred = y_test_pred_reg).item()
print("mse_with_l2_reg:",mes_reg)

# 绘制图像
plt.scatter(X_train, y_train, facecolor="none", edgecolor="#e4007f", s=50, label="train data")
plt.plot(X_underlying, y_underlying, c='#000000', label=r"$\sin(2\pi x)$")
plt.plot(X_underlying, y_underlying_pred, c='#e4007f', linestyle="--", label="$deg. = 8$")
plt.plot(X_underlying, y_underlying_pred_reg, c='#f19ec2', linestyle="-.", label="$deg. = 8, \ell_2 reg$")
plt.ylim(-1.5, 1.5)
plt.annotate("lambda={}".format(reg_lambda), xy=(0.82, -1.4))
plt.legend(fontsize='large')
plt.savefig('ml-vis4.pdf')
plt.show()

运行结果:
在这里插入图片描述

mse: 2.7414708137512207
mse_with_l2_reg: 0.2308414876461029

思考 如果训练数据中存在一些异常样本,会对最终模型有何影响?怎样处理可以尽可能减少异常样本对模型的影响?
会使最终模型偏离正确值
检测数据中的离群点,异常数据的特征值与正常数据的特征值距离较远,减少异常样本。

2.4 Runner类介绍

机器学习方法流程包括数据集构建、模型构建、损失函数定义、优化器、模型训练、模型评价、模型预测等环节。

为了更方便地将上述环节规范化,我们将机器学习模型的基本要素封装成一个Runner类。

除上述提到的要素外,再加上模型保存、模型加载等功能。

Runner类的成员函数定义如下:

__init__函数:实例化Runner类,需要传入模型、损失函数、优化器和评价指标等;
train函数:模型训练,指定模型训练需要的训练集和验证集;
evaluate函数:通过对训练好的模型进行评价,在验证集或测试集上查看模型训练效果;
predict函数:选取一条数据对训练好的模型进行预测;
save_model函数:模型在训练过程和训练结束后需要进行保存;
load_model函数:调用加载之前保存的模型。

代码如下:

class Runner(object):
    def __init__(self, model, optimizer, loss_fn, metric):
        self.model = model         # 模型
        self.optimizer = optimizer # 优化器
        self.loss_fn = loss_fn     # 损失函数   
        self.metric = metric       # 评估指标

    # 模型训练
    def train(self, train_dataset, dev_dataset=None, **kwargs):
        pass

    # 模型评价
    def evaluate(self, data_set, **kwargs):
        pass

    # 模型预测
    def predict(self, x, **kwargs):
        pass

    # 模型保存
    def save_model(self, save_path):
        pass

    # 模型加载
    def load_model(self, model_path):
        pass

Runner类的流程如图2.8所示,可以分为 4 个阶段:

初始化阶段:传入模型、损失函数、优化器和评价指标。
模型训练阶段:基于训练集调用train()函数训练模型,基于验证集通过evaluate()函数验证模型。通过save_model()函数保存模型。
模型评价阶段:基于测试集通过evaluate()函数得到指标性能。
模型预测阶段:给定样本,通过predict()函数得到该样本标签。
在这里插入图片描述

2.5 基于线性回归的波士顿房价预测

使用线性回归来对马萨诸塞州波士顿郊区的房屋进行预测。

实验流程主要包含如下5个步骤:

数据处理:包括数据清洗(缺失值和异常值处理)、数据集划分,以便数据可以被模型正常读取,并具有良好的泛化性;
模型构建:定义线性回归模型类;
训练配置:训练相关的一些配置,如:优化算法、评价指标等;
组装训练框架Runner:Runner用于管理模型训练和测试过程;
模型训练和测试:利用Runner进行模型训练和测试。

2.5.1 数据处理

代码如下:

import pandas as pd # 开源数据分析和操作工具

data = pd.read_csv("boston_house_prices.csv")
print(data.head()) # 输出前五行

运行结果:

      CRIM    ZN  INDUS  CHAS    NOX     RM   AGE     DIS  RAD  TAX  PTRATIO  LSTAT  MEDV
0  0.00632  18.0   2.31     0  0.538  6.575  65.2  4.0900    1  296     15.3   4.98  24.0
1  0.02731   0.0   7.07     0  0.469  6.421  78.9  4.9671    2  242     17.8   9.14  21.6
2  0.02729   0.0   7.07     0  0.469  7.185  61.1  4.9671    2  242     17.8   4.03  34.7
3  0.03237   0.0   2.18     0  0.458  6.998  45.8  6.0622    3  222     18.7   2.94  33.4
4  0.06905   0.0   2.18     0  0.458  7.147  54.2  6.0622    3  222     18.7   5.33  36.2

2.5.1.2 数据清洗
缺失值分析
代码如下:

data.isna().sum()

运行结果:

CRIM       0
ZN         0
INDUS      0
CHAS       0
NOX        0
RM         0
AGE        0
DIS        0
RAD        0
TAX        0
PTRATIO    0
LSTAT      0
MEDV       0
dtype: int64

异常值处理
代码如下:

import matplotlib.pyplot as plt # 可视化工具

# 箱线图查看异常值分布
def boxplot(data, fig_name):
    # 绘制每个属性的箱型图
    data_col = list(data.columns)

    # 连续画几个图片
    plt.figure(figsize=(5,5),dpi=300)
    # 子图调整
    plt.subplots_adjust(wspace=0.6)
    # 每个特征画一个箱型图
    for i, col_name in enumerate(data_col):
        plt.subplot(3,5,i+1)
        plt.boxplot(data[col_name],
                    showmeans=True,
                    meanprops={},
                    medianprops={"color":"#946279"}, # 中位数线的属性
                    whiskerprops={"color":"#8E004D","linewidth":0.4,'linestyle':"--"},
                    flierprops={"markersize":0.4}
                    )
        # 图名
        plt.title(col_name,fontdict={"size":5},pad=2)
        # y方向梯度
        plt.yticks(fontsize=4,rotation=90)
        plt.tick_params(pad=0.5)
        # x方向梯度
        plt.xticks([])
    plt.savefig(fig_name)
    plt.show()

boxplot(data, 'ml-vis5.pdf')

运行结果:
在这里插入图片描述

# 四分位处理异常值
num_features = data.select_dtypes(exclude=['object','bool']).columns.tolist()

for feature in num_features:
    if feature == 'CHAS':
        continue

    Q1 = data[feature].quantile(q=0.25) # 下四分位
    Q3 = data[feature].quantile(q=0.75) # 上四分位

    IQR = Q3-Q1
    top = Q3+1.5*IQR
    bot = Q1-1.5*IQR
    values = data[feature].values
    values[values>top] = top
    values[values<bot] = bot
    data[feature] = values.astype(data[feature].dtypes)

# 再次查看箱线图,异常值已被临界值替换(数据量较多或本身异常值较少时,箱线图展示会不容易体现出来)
boxplot(data, 'ml-vis6.pdf')

在这里插入图片描述

2.5.1.3 数据集划分
代码如下:

import torch

def train_test_split(X, y, train_percent=0.8):
    n = len(X)
    shuffled_indices = torch.randperm(n) # 返回以恶个数值在0到n-1、随机排列的1-D Tensor
    train_set_size = int(n*train_percent)
    train_indices = shuffled_indices[:train_set_size]
    test_indices = shuffled_indices[train_set_size:]


    X = X.values
    y = y.values

    X_train = X[train_indices]
    y_train = y[train_indices]

    X_test = X[test_indices]
    y_test = y[test_indices]

    return X_train,X_test,y_train,y_test

X = data.drop(['MEDV'], axis= 1)
y = data['MEDV']

X_train, X_test, y_train, y_test = train_test_split(X,y)# X_train每一行是个样本,shape[N,D]

2.5.1.4 特征工程
代码如下:

import torch

X_train = torch.tensor(X_train, dtype=torch.float32)
X_test = torch.tensor(X_test, dtype=torch.float32)
y_train = torch.tensor(y_train, dtype=torch.float32)
y_test = torch.tensor(y_test, dtype=torch.float32)

X_min = torch.min(X_train, 0).values
X_max = torch.max(X_train, 0).values

X_train = (X_train-X_min)/(X_max-X_min)

X_test  = (X_test-X_min)/(X_max-X_min)

# 训练集构造
train_dataset=(X_train,y_train)
# 测试集构造
test_dataset=(X_test,y_test)

2.5.2 模型构建

代码如下:

from op import Linear

# 模型实例化
input_size = 12
model=Linear(input_size)

import torch
import os
os.environ["KMP_DUPLICATE_LIB_OK"]="TRUE" # 这里报错
torch.manual_seed(10) #设置随机种子

class Op(object):
    def __init__(self):
        pass

    def __call__(self, inputs):
        return self.forward(inputs)

    def forward(self, inputs):
        raise NotImplementedError

    def backward(self, inputs):
        raise NotImplementedError

# 线性算子
class Linear(Op):
    def __init__(self,input_size):
        """
        输入:
           - input_size:模型要处理的数据特征向量长度
        """

        self.input_size = input_size

        # 模型参数
        self.params = {}
        self.params['w'] = torch.randn(size=[self.input_size,1],dtype=torch.float32) 
        self.params['b'] = torch.zeros(size=[1],dtype=torch.float32)

    def __call__(self, X):
        return self.forward(X)

    # 前向函数
    def forward(self, X):
  
        N,D = X.shape

        if self.input_size==0:
            return torch.full(size=[N,1], fill_value=self.params['b'])
        
        assert D==self.input_size # 输入数据维度合法性验证

        # 使用torch.matmul计算两个tensor的乘积
        y_pred = torch.matmul(X,self.params['w'])+self.params['b']
        
        return y_pred

2.5.3 完善Runner类

代码如下:

import torch
from op import Linear
from opitimizer import optimizer_lsm

class Runner(object):
    def __init__(self, model, optimizer, loss_fn, metric):
        # 优化器和损失函数为None,不再关注

        # 模型
        self.model=model
        # 评估指标
        self.metric = metric
        # 优化器
        self.optimizer = optimizer
    
    def train(self,dataset,reg_lambda,model_dir):
        X,y = dataset
        self.optimizer(self.model,X,y,reg_lambda)

        # 保存模型
        self.save_model(model_dir)
    
    def evaluate(self, dataset, **kwargs):
        X,y = dataset

        y_pred = self.model(X)
        result = self.metric(y_pred, y)

        return result

    def predict(self, X, **kwargs):
        return self.model(X)
    
    def save_model(self, model_dir):
        if not os.path.exists(model_dir):
            os.makedirs(model_dir)
        
        params_saved_path = os.path.join(model_dir,'params.pdtensor')
        torch.save(model.params,params_saved_path)

    def load_model(self, model_dir):
        params_saved_path = os.path.join(model_dir,'params.pdtensor')
        self.model.params=torch.load(params_saved_path)
 
optimizer = optimizer_lsm

# 实例化Runner
runner = Runner(model, optimizer=optimizer,loss_fn=None, metric=mse_loss)

2.5.4 模型训练

代码如下:

# 模型保存文件夹
saved_dir = 'models'

# 启动训练
runner.train(train_dataset,reg_lambda=0,model_dir=saved_dir)

# 打印出训练得到的权重:

columns_list = data.columns.to_list()
weights = runner.model.params['w'].tolist()
b = runner.model.params['b'].item()

for i in range(len(weights)):
    print(columns_list[i],"weight:",weights[i])

print("b:",b)

运行结果:

CRIM weight: -5.2611470222473145
ZN weight: 1.3627078533172607
INDUS weight: -0.02481861039996147
CHAS weight: 1.8001954555511475
NOX weight: -7.556739330291748
RM weight: 9.55705451965332
AGE weight: -1.3511563539505005
DIS weight: -9.967967987060547
RAD weight: 7.528563022613525
TAX weight: -5.082488059997559
PTRATIO weight: -6.9966607093811035
LSTAT weight: -13.183674812316895
b: 32.621612548828125

2.5.5 模型测试

代码如下:

runner.load_model(saved_dir)

mse = runner.evaluate(test_dataset)
print('MSE:', mse.item())

运行结果:

MSE: 11.21076488494873

2.5.6 模型预测

代码如下:

runner.load_model(saved_dir)
pred = runner.predict(X_test[:1])
print("真实房价:",y_test[:1].item())
print("预测的房价:",pred.item())

运行结果:

真实房价: 18.899999618530273
预测的房价: 21.529163360595703

问题1:使用类实现机器学习模型的基本要素有什么优点?

简单,易于理解
既可以用来做分类也可以用来做回归
提高效率

问题2:算子op、优化器opitimizer放在单独的文件中,主程序在使用时调用该文件。这样做有什么优点?

避免了重复定义的错误,便于处理。

问题3:线性回归通常使用平方损失函数,能否使用交叉熵损失函数?为什么?

不能,交叉熵损失函数
对于回归问题,均方误差求解比交叉熵求解更方便。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值