NNDL 实验三 线性回归

目录

前言

一、2.2 线性回归

2.2.1 数据集构建

2.2.2 模型构建

2.2.3 损失函数

2.2.4 模型优化

2.2.5 模型训练

2.2.6 模型评估

2.2.7 样本数量 & 正则化系数

二、2.3 多项式回归

2.3.1 数据集构建

2.3.2 模型构建

2.3.3 模型训练

2.3.4 模型评估

三、2.4 Runner类介绍

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

2.5.1 数据处理

2.5.1.2 数据清洗

2.5.1.3 数据集划分

2.5.1.4 特征工程

2.5.2 模型构建 

2.5.3 完善Runner类

2.5.4 模型训练

2.5.5 模型测试

2.5.6 模型预测

实验体会


前言

这次实验我感觉思想是十分重要的,之前学机器学习的时候老师给我说,如果你学一个算法只会照着搭,但是不明白思想,不明白啥意思,这不是很可悲的吗?

所以我每个代码段都写了思想,也写了注意事项,所以写了真不少,各位别嫌烦


一、2.2 线性回归

2.2.1 数据集构建

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

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

# coding=gbk
import torch

    这个是引入库,但是上边那个是解码的方式,最好要规定一个要不然在写注释和读取文件的时候是可能会报错

# 真实函数的参数缺省值为 w=1.2,b=0.5
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 = 0.0, add_outlier = False, outlier_ratio = 0.001):
    """
    根据给定的函数,生成样本
    输入:
       - func:函数
       - interval: x的取值范围
       - sample_num: 样本数目
       - noise: 噪声均方差
       - add_outlier:是否生成异常值
       - outlier_ratio:异常值占比
    输出:
       - X: 特征数据,shape=[n_samples,1]
       - y: 标签数据,shape=[n_samples,1]
    """
    # 均匀采样
    # 使用paddle.rand在生成sample_num个随机数
    X = torch.rand(sample_num) * (interval[1]-interval[0]) + interval[0]
    y = func(X) #1,100

    # 生成高斯分布的标签噪声
    # 使用paddle.normal生成0均值,noise标准差的数据
    epsilon = torch.normal(float(0),noise,size=(1,y.shape[0]))#这里注意它必须是这个格式,要不会报错,会出现传播机制
    y = y + epsilon
    if add_outlier:     # 生成额外的异常点
        outlier_num = int(len(y)*outlier_ratio)
        if outlier_num != 0:
            # 使用paddle.randint生成服从均匀分布的、范围在[0, len(y))的随机Tensor
            outlier_idx = torch.randint(len(y),size=[outlier_num])  #注意这里是size不是shape
            y[outlier_idx] = y[outlier_idx] * 5
    return X, y

    这个是生成样本的数据,这个函数的大体的思想是:先随机生成一些数据,这些数据就是X,然后利用上边写的线性函数,先生成Y,由于是由上边的函数生成的所以严格符合线性的定义,肯定在y=1.2*x+b这条直线上,所以这是就要让它离散一点,并且加入噪声espslion函数就是让它离散一点,if后边就是加入离群值(也叫异常值,噪声)。

    在写时,注意torch.normal,这个函数的用法,如果直接把paddle改成torch的话,会报错,

TypeError: normal() received an invalid combination of arguments - got (float, int, size=int), but expected one of:
 * (Tensor mean, Tensor std, *, torch.Generator generator, Tensor out)
 * (Tensor mean, float std, *, torch.Generator generator, Tensor out)
 * (float mean, Tensor std, *, torch.Generator generator, Tensor out)
 * (float mean, float std, tuple of ints size, *, torch.Generator generator, Tensor out, torch.dtype dtype, torch.layout layout, torch.device device, bool pin_memory, bool requires_grad)

 

    这是要注意torch.normal的用法了,从一开始函数定义的noise可以看出,noise是float,故是第四个用法,但是size那要注意,不能直接写size=(y.shape[0]),因为要求是一个元组,但是不能这么定义,(y.sahpe[0]),因为从定义中可以发现,这必须是(x,y)的这种形式的,所以只能写为(1,y.sahpe[0]),至于为什么是这个,在下一部分会有,这是问了老师之后,关于形状的问题,如果不这么定义会报错,无法输出。

func = linear_func
interval = (-10,10)
train_num = 100 # 训练样本数目
test_num = 50 # 测试样本数目
noise = 2
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_train_large, y_train_large = create_toy_data(func=func, interval=interval, sample_num=5000, noise = noise, add_outlier = False)

# paddle.linspace返回一个Tensor,Tensor的值为在区间start和stop上均匀间隔的num个值,输出Tensor的长度为num
X_underlying = torch.linspace(interval[0],interval[1],train_num)
y_underlying = linear_func(X_underlying)

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

运行结果为:

torch.Size([100]) torch.Size([1, 100])

代码的大体思想是:

     上边是定义的一些属性,下边第一个是建立100个训练集,第二个是建立50个测试集,第三个是5000个训练集,后边那个linspace及下边那一行是生成是画直线的点,后边是画训练集,测试集的点,画直线,以及画图例并保存在pdf中。

    这里说一下上边说的类型的问题,因为只有到这的时候才会报错,如果写成size=(y.shape[0],1),会报错

ValueError: x and y must be the same size 

    这是因为在上一部分rand函数中,生成的形状全是torch.Size([100]),而加的离散值形状为torch.Size([100,1]),在相加之后会出现传播机制,变成torch.Size([100, 100])。所以在输出时x,y的格式为torch.Size([100]) torch.Size([100, 100]),所以会无法输出,最直观的解释就是,100个x,10000个y所以无法画图,原理上来说就是形状不一样,无法对应输出来作图。

     这个的原因是在rand那只有一个数表示就是100个数,所以一开始y也是100个数,但是加上(100,1)的格式后就变成了(100,100)的了,相加的这会传播机制,相当于一开始是(1,100)的,所以改成(1,y.shape[0]),就会可以输出,但是这还要注意一点torch.Size([100])并不是torch.Size([1,100]),所以正在后边的时候好多时候我都加了一个x=x.reshape[1,-1],这样格式才对。

2.2.2 模型构建

                                                   

from Op import Op

    如果多文件调用的话,要这么写,前边指的是调用这个文件(文件名叫Op),后边指的调用这个文件里边的函数Op,如果不这么写的话就要写Op.函数名了。

# coding=gbk
import torch

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):
        """
        输入:
           - X: tensor, shape=[N,D]
           注意这里的X矩阵是由N个x向量的转置拼接成的,与原教材行向量表示方式不一致
        输出:
           - y_pred: tensor, shape=[N]
        """

        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  # 输入数据维度合法性验证

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

        return y_pred

     这个是Op函数,但是其中的linear函数是没有调用,所以也可以去掉

torch.manual_seed(10)  # 设置随机种子
# 线性算子
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):
        """
        输入:
           - X: tensor, shape=[N,D]
           注意这里的X矩阵是由N个x向量的转置拼接成的,与原教材行向量表示方式不一致
        输出:
           - y_pred: tensor, shape=[N]
        """

        N, D = X.shape

        if self.input_size == 0:
            return   torch.full(size=(N, 1), fill_value=self.params['b'].item())

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

        # 使用paddle.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)  # 生成2个维度为3的数据
model = Linear(input_size)
y_pred = model(X)
print("y_pred:", y_pred)  # 输出结果的个数也是2个

运行结果为;

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

    代码的大体思想是:

    先建一个类,也就是这个Linear,其中的params列表就是存储w和b的,然后forward函数,在判断可行性之后,进行矩阵的乘法,研究了矩阵的形状,发现是2x3乘以3x1的矩阵乘法。后边的就是正常的调用。

    这个要注意,pytorch中没有torch.seed函数,可以将其改为torch.manual_seed(),同时要注意这是python类的写法,其中forward在向类里传值的时候是自动调用的,也就是说y_pred = model(X)等于y_pred = model.forward(X)。

2.2.3 损失函数

回归任务中常用的评估指标是均方误差

均方误差(mean-square error, MSE)是反映估计量与被估计量之间差异程度的一种度量。

【注意:代码实现中没有除2】思考:没有除2合理么?谈谈自己的看法,写到实验报告。

def mean_squared_error(y_true, y_pred):
    """
    输入:
       - y_true: tensor,样本真实标签
       - y_pred: tensor, 样本预测标签
    输出:
       - error: float,误差值
    """

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

    # paddle.square计算输入的平方值
    # paddle.mean沿 axis 计算 x 的平均值,默认axis是None,则对输入的全部元素计算平均值。
    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合理么?谈谈自己的看法,写到实验报告。

答:首先跑了一下结果除以2,跑了一下结果。

error: 2.002500057220459

    感觉相差不大,感觉这个1/2,只是方便求导以后消掉出现的那个2,就像支持向量机和线性的最小二乘法、神经网络中,都有这个1/2,有一点简化计算的感觉,也就是当需要求导时,也就意味着大概率要进行迭代计算了,这时最好加上这个1/2因为这样可以简化运算,提高时间效率,当不需要求导时,感觉是相差不大。

2.2.4 模型优化

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

思考1. 为什么省略了\frac{1}{N}不影响效果?

思考 2.  什么是最小二乘法 ( Least Square Method , LSM )

回答以上问题,写到实验报告。 

def optimizer_lsm(model, X, y, reg_lambda=0):
    """
      输入:
         - model: 模型
         - X: tensor, 特征数据,shape=[N,D]
         - y: tensor,标签数据,shape=[N]
         - reg_lambda: float, 正则化系数,默认为0
      输出:
         - model: 优化好的模型
      """

    N, D = X.shape

    # 对输入特征数据所有特征向量求平均
    x_bar_tran = torch.mean(X, dim=0).T

    # 求标签的均值,shape=[1]
    y_bar = torch.mean(y)

    # paddle.subtract通过广播的方式实现矩阵减向量
    x_sub = torch.subtract(X, x_bar_tran)

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

    # paddle.inverse求方阵的逆
    tmp = torch.inverse(torch.matmul(x_sub.T, x_sub) +
                         reg_lambda * torch.eye(D))

    w = torch.matmul(torch.matmul(tmp, x_sub.T), (y - y_bar))

    b = y_bar - torch.matmul(x_bar_tran, w)

    model.params['b'] = b
    model.params['w'] = torch.squeeze(w, dim=-1)

    return model

函数的大体思路是:

    就是先求x的平均数,再求y的平均数,然后x-x的平均数,再转置相乘,就是均方误差的过程,这种优化的过程最好写成这种函数的形式,这种一般叫优化器,优化器一般有多个,最后看哪个效果好。

思考1. 为什么省略了\frac{1}{N}不影响效果?

    这个1/N可以理解成那个,在梯度下降的时候的步长,有点类似,但是,不是相同的概念,因为那个是人工变量,但是都可以理解为一个系数的东西,也就是它影响的只是到达最低点的时间,并不会影响,它会达到最低点,也就是有点像反向传播算法里边的α。

思考 2.  什么是最小二乘法 ( Least Square Method , LSM )

回答以上问题,写到实验报告。 

上学期机器学习的定义是,也就是西瓜书的定义是,基于均方误差来进行模型求解的方法称为最小二乘法(least square method)。

书上的铅笔是我写的推导的公式的求解过程,我推过这个,这个是可以推出来的

求老师和大佬们批评指正,因为推了好长时间,所以想放在这一下(谁能加我一下这个为啥是横的?)

 

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()
y_train_pred=y_train_pred.reshape(1,-1)#必须加一个一维到二维的转换
train_error = mean_squared_error(y_true=y_train, y_pred=y_train_pred).item()
print("train error: ",train_error)

运行结果为:

w_pred: 1.2271720170974731 b_pred:  0.37986236810684204
train error:  3.632181406021118 

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()
print(y_train_large.shape,y_train_pred_large.shape)
y_train_pred_large=y_train_pred_large.reshape(1,5000)
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 large: 1.1993680000305176 b_pred large:  0.5421561002731323
torch.Size([1, 5000]) torch.Size([5000])
train error large:  3.9921984672546387

代码的主要思想

   上边的是100,50的,后边的是5000的,都是调用优化器输出w和b,然后输出一下误差,这个是函数的调用。

2.2.6 模型评估

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

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

运行结果为;

test error:  4.306798458099365

y_test_pred_large = model_large(X_test.reshape([-1,1])).squeeze()
y_test=y_test.reshape(1,-1)
y_test_pred_large=y_test_pred_large.reshape(1,-1)
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 large:  4.420895099639893

2.2.7 样本数量 & 正则化系数

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

train_num = 5000 # 训练样本数目
test_num = 500 # 测试样本数目

运行结果为:

w_pred: 1.1975053548812866 b_pred:  0.4983460307121277
train error:  3.9169905185699463

test error:  4.159750938415527

这里最好把测试集也改一下,要不然50个测试集,用来评估是不太科学的​​​​​​​ 

这个可以看出当训练数据变多之后

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

def optimizer_lsm(model, X, y, reg_lambda=0.15):

这原本是0,现在调整成0.15 

运行结果为:

w_pred: 1.227117657661438 b_pred:  0.3798506259918213
train error:  3.632181406021118

test error:  4.306741714477539

从0.15到2

运行结果为:

w_pred: 1.2264435291290283 b_pred:  0.37970513105392456
train error:  3.6321990489959717

test error:  4.306056499481201 

可以看出模型的评价会比以前好,但是,几乎差不多,这个参数是防止过拟合的 ,这个函数要适度调整,这种参数有点类似在为了防止过拟合,在目标函数后边填加一些变量,影响比例原本函数所占比例的系数,有点岭回归的感觉。

二、2.3 多项式回归

2.3.1 数据集构建

构建训练和测试数据,其中:

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

import math

# sin函数: sin(2 * pi * x)
def sin(x):
    y = torch.sin(2 * math.pi * x)
    return y

运行结果:

# 生成数据
func = sin
interval = (0,1)
train_num = 15
test_num = 10
noise = 0.5 #0.1
X_train, y_train = create_toy_data(func=func, interval=interval, sample_num=train_num, noise = noise)
X_test, y_test = create_toy_data(func=func, interval=interval, sample_num=test_num, noise = noise)

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.scatter(X_test, y_test, facecolor="none", edgecolor="r", s=50, label="test 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):
    """
    输入:
       - x: tensor, 输入的数据,shape=[N,1]
       - degree: int, 多项式的阶数
       example Input: [[2], [3], [4]], degree=2
       example Output: [[2^1, 2^2], [3^1, 3^2], [4^1, 4^2]]
       注意:本案例中,在degree>=1时不生成全为1的一列数据;degree为0时生成形状与输入相同,全1的Tensor
    输出:
       - x_result: tensor
    """

    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), axis=-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(bbox_to_anchor=(1.05, 0.64), loc=2, borderaxespad=0.)
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.5144, -34.8418,  23.1784]), 'b': tensor([-0.3165])}
{'w': tensor([ 3.7056e+00,  2.0793e+02, -1.7501e+03,  5.1039e+03, -5.8384e+03,
         4.3806e+02,  3.8396e+03, -2.0028e+03]), 'b': tensor([-1.5398])}

     代码的主要思想:

     这个函数的思想和前边的差不多,只是注意一下多项式,然后基本就一样了 

2.3.4 模型评估

通过均方误差来衡量训练误差、测试误差以及在没有噪音的加入下sin函数值与多项式回归值之间的误差,更加真实地反映拟合结果。多项式分布阶数从0到8进行遍历。

对于模型过拟合的情况,可以引入正则化方法,通过向误差函数中添加一个惩罚项来避免系数倾向于较大的取值。

# 遍历多项式阶数
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()
    y_train=y_train.reshape(1,-1)
    y_train_pred=y_train_pred.reshape(1,-1)
    train_mse = mean_squared_error(y_true=y_train, y_pred=y_train_pred).item()
    training_errors.append(train_mse)
    y_test = y_test.reshape(1, -1)
    y_test_pred = y_test_pred.reshape(1, -1)
    test_mse = mean_squared_error(y_true=y_test, y_pred=y_test_pred).item()
    test_errors.append(test_mse)

    # distribution_mse = mean_squared_error(y_true=y_underlying, y_pred=y_underlying_pred).item()
    # distribution_errors.append(distribution_mse)

print("train errors: \n", training_errors)
print("test errors: \n", test_errors)
# print ("distribution errors: \n", distribution_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.plot(distribution_errors, '-', mfc="none", mec="#3D3D3F", ms=10, c="#3D3D3F", label="Distribution")
plt.legend(fontsize='x-large')
plt.xlabel("degree")
plt.ylabel("MSE")
plt.savefig('ml-mse-error.pdf')
plt.show()

运行结果为:

train errors: 
 [0.49659356474876404, 0.3854281008243561, 0.38537195324897766, 0.2073555439710617, 0.1958308219909668, 0.19370171427726746, 0.7928376793861389, 0.3426400125026703, 2.5518805980682373]
test errors: 
 [1.1408350467681885, 0.8089733123779297, 0.8042014837265015, 0.4072621464729309, 0.37220802903175354, 0.3991769850254059, 1.669513463973999, 0.8297686576843262, 3.1271579265594482]

 

对于模型过拟合的情况,可以引入正则化方法,通过向误差函数中添加一个惩罚项来避免系数倾向于较大的取值。


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 = y_test.reshape(1, -1)
y_test_pred = y_test_pred.reshape(1, -1)
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)
y_test = y_test.reshape(1, -1)
y_test_pred_reg = y_test_pred_reg.reshape(1, -1)
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: 3.1271579265594482
mse_with_l2_reg: 0.3700985610485077

MSE是均方误差, 后边误差上生的情况试过拟合,然后可以通过增加正则项的方式来防止这种情况。

三、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 个阶段:

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

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

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

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

2.5.1 数据处理

2.5.1.2 数据清洗

import pandas as pd # 开源数据分析和操作工具
from opitimizer import optimizer_lsm
from Op import Op
# 利用pandas加载波士顿房价的数据集
data=pd.read_csv("boston_house_prices.csv")
# 预览前5行数据
print(data.head())
print(data.isna().sum())

 运行结果为:

 CRIM    ZN  INDUS  CHAS    NOX  ...  RAD  TAX  PTRATIO  LSTAT  MEDV
0  0.00632  18.0   2.31     0  0.538  ...    1  296     15.3   4.98  24.0
1  0.02731   0.0   7.07     0  0.469  ...    2  242     17.8   9.14  21.6
2  0.02729   0.0   7.07     0  0.469  ...    2  242     17.8   4.03  34.7
3  0.03237   0.0   2.18     0  0.458  ...    3  222     18.7   2.94  33.4
4  0.06905   0.0   2.18     0  0.458  ...    3  222     18.7   5.33  36.2

[5 rows x 13 columns]

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

这个一定要记住,把函数print一下,要不输出不了(hahaha)

  • 异常值处理

通过箱线图直观的显示数据分布,并观测数据中的异常值。箱线图一般由五个统计值组成:最大值、上四分位、中位数、下四分位和最小值。一般来说,观测到的数据大于最大估计值或者小于最小估计值则判断为异常值,其中

                最大估计值=上四分位+1.5∗(上四分位−下四分位)

                最小估计值=下四分位−1.5∗(上四分位−下四分位)

# 箱线图查看异常值分布
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={"markersize": 1, "marker": "D", "markeredgecolor": '#f19ec2'},  # 均值的属性
                    medianprops={"color": '#e4007f'},  # 中位数线的属性
                    whiskerprops={"color": '#e4007f', "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 数据集划分

torch.manual_seed(10)


# 划分训练集和测试集
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 特征工程

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 ,x_min2= torch.min(X_train,dim=0)
X_max ,y_max2= torch.max(X_train,dim=0)
print(X_max)
print(X_min)
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)

运行结果为;

tensor([  9.0696,  31.2500,  27.7400,   1.0000,   0.8710,   7.7305, 100.0000,
          9.8208,  24.0000, 711.0000,  22.0000,  31.9625])
tensor([6.3200e-03, 0.0000e+00, 4.6000e-01, 0.0000e+00, 3.9200e-01, 4.7785e+00,
        2.9000e+00, 1.1296e+00, 1.0000e+00, 1.8800e+02, 1.3200e+01, 1.7300e+00])

 这个我是输出一下,x_min,x_max,这个下边会用到,并且不改这无法输出。

2.5.2 模型构建 

from Op import Linear
# 模型实例化
input_size = 12
model=Linear(input_size)
# coding=gbk
import torch

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):
        """
        输入:
           - X: tensor, shape=[N,D]
           注意这里的X矩阵是由N个x向量的转置拼接成的,与原教材行向量表示方式不一致
        输出:
           - y_pred: tensor, shape=[N]
        """

        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  # 输入数据维度合法性验证

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

        return y_pred

 这个注意之前说过的引入函数的方式。后边的是Op函数。

2.5.3 完善Runner类

模型定义好后,围绕模型需要配置损失函数、优化器、评估、测试等信息,以及模型相关的一些其他信息(如模型存储路径等)。

在本章中使用的Runner类为V1版本。其中训练过程通过直接求解解析解的方式得到模型参数,没有模型优化及计算损失函数过程,模型训练结束后保存模型参数。

训练配置中定义:

  • 训练环境,如GPU还是CPU,本案例不涉及;
  • 优化器,本案例不涉及;
  • 损失函数,本案例通过平方损失函数得到模型参数的解析解;
  • 评估指标,本案例利用MSE评估模型效果。
import os
from opitimizer import optimizer_lsm
import torch.nn as nn
mse_loss = nn.MSELoss()
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)
# coding=gbk
import torch


def optimizer_lsm(model, X, y, reg_lambda=0):
    """
      输入:
         - model: 模型
         - X: tensor, 特征数据,shape=[N,D]
         - y: tensor,标签数据,shape=[N]
         - reg_lambda: float, 正则化系数,默认为0
      输出:
         - model: 优化好的模型
      """

    N, D = X.shape

    # 对输入特征数据所有特征向量求平均
    x_bar_tran = torch.mean(X, dim=0).T

    # 求标签的均值,shape=[1]
    y_bar = torch.mean(y)

    # paddle.subtract通过广播的方式实现矩阵减向量
    x_sub = torch.subtract(X, x_bar_tran)

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

    # paddle.inverse求方阵的逆
    tmp = torch.inverse(torch.matmul(x_sub.T, x_sub) +
                         reg_lambda * torch.eye((D)))

    w = torch.matmul(torch.matmul(tmp, x_sub.T), (y - y_bar))

    b = y_bar - torch.matmul(x_bar_tran, w)

    model.params['b'] = b
    model.params['w'] = torch.squeeze(w, dim=-1)

    return model

 后边的是重写的optimizer,问了老师之后,后边的nn方法就直接调用吧

2.5.4 模型训练

#模型保存文件夹
saved_dir = '/home/aistudio/work/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.261089324951172
ZN weight: 1.362697958946228
INDUS weight: -0.024794816970825195
CHAS weight: 1.8001978397369385
NOX weight: -7.556751251220703
RM weight: 9.557075500488281
AGE weight: -1.3511643409729004
DIS weight: -9.96794605255127
RAD weight: 7.528500556945801
TAX weight: -5.0824761390686035
PTRATIO weight: -6.9966583251953125
LSTAT weight: -13.183669090270996
b: 32.6215934753418
MSE: 11.210776329040527

这个是打印的权重 

2.5.5 模型测试

# 加载模型权重
runner.load_model(saved_dir)

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

运行结果为:

MSE: 11.210776329040527 

这个是均方误差的损失 

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.52915382385254 

这个要是按我上边的方法会报一个错

TypeError: unsupported operand type(s) for -: 'Tensor' and 'torch.return_types.min'

这个就是形状的问题,只需要按我上边的方法来定义,即可正常输出

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

.类的好处主要有

  • 方便复用(如果你用函数写,就要复制整块代码,增加了代码量,增加了出错率)
  • 方便扩展(函数写段代码,若要升级、扩展,都十分复杂,容易出错,用类来扩展,则方便清晰)
  • 方便维护(因为类是把抽象的东西映射成我们常见的,摸得到的东西,容易理解,维护也方便)
  • 控制存取属性值的语句来避免对数据的不合理的操作
  • 一个封装好的类,是非常容易使用
  • 代码更加模块化,增强可读性
  • 隐藏类的实现细节,让使用者只能通过程序员规定的方法来访问数据

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

首先的点就是老师说过的方便代码的复用,正如老师说的每个人都应该有个自己的代码库

   ①避免了对相同程序段的重复编写;

   ②简化程序的逻辑结构,便于阅读、查错,同时也便于子程序调试;

   ③节省存储器空间。

 像我之前发的词云的代码就是互相调用的,就是当时不太清楚的时候互相调用,蒙蒙的 

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

其实从一开始看视频学深度学习的时候,就会给人们说回归用平方损失函数,分类用交叉熵损失函数

之前我问老师,老师说这就是一个经验的东西,就是人们实践出来的经验,但是理论是有的,可以证明哪个好,但是这种东西就是经验的东西,咱们要站在巨人的肩膀上,记住用就行了。

今天详细的研究了一下(hahaha)

直观上:

从平方损失函数运用到多分类场景下,可知平方损失函数对每一个输出结果都十分看重,而交叉熵损失函数只对正确分类的结果看重。例如,对于一个多分类模型其模型结果输出为( a , b , c ) (a,b,c)(a,b,c),而实际真实结果为( 1 , 0 , 0 ) (1, 0, 0)(1,0,0)。则根据两种损失函数的定义其损失函数可以描述为:

                         

 

从上述的结果中可以看出,交叉熵损失函数只和分类正确的预测结果有关。而平方损失函数还和错误的分类有关,该损失函数除了让正确分类尽量变大,还会让错误分类都变得更加平均,但实际中后面的这个调整使没必要的。但是对于回归问题这样的考虑就显得重要了,因而回归问题上使用交叉熵并不适合。


从理论的角度

 平方数损失函数假设最终结果都服从高斯分布,而高斯分布实际上是一个连续变量,并不是一个离散变量。如果假设结果变量服从均值u ,方差为σ ,那么利用最大似然法就可以优化它的负对数似然,公式最终变为了:

 

 

出去与y无关的项目,最后剩下的就是平方损失函数的形式。

 

 


实验体会

     先放一个照片证明一下写了多少字数(不包括开头和体会),这些真的都都是自己码的呀,哈哈哈,真的码这些字真的累,但是我每个代码段都是写了思想和注意事项,我感觉要是啥也不管只把paddle改成torch就没有意义了,哈哈哈,好了废话不说了,说正事。

      第一部分,首先更加理解了啥叫算子,使用了之后更加理解了,我感觉这一部分真的是,不用真是理解不够深,还是要实践呀,同时明白了用类和对象封装写模型的好处,这一部分我感觉要好好弄一弄,这一部分时候是后部分的基础,这一部分可以自己试调参,这一部分弄明白了,相当于自己搭了一遍,这真是很重要的,后边神经网络的话,复杂的自己搭一点一点调参,也就是这个过程了,感觉这部分和后边联系是很重要的,以前写神经网络自己也搭过,但是当时过程非常费劲,后来就尝试调库了,但是那个调参限制性是比较强的。所以感觉这个是比较重要的。

       第二部分,多项式回归部分,是相当于第一部分的一点变化,这很像咱们学了一个模型,去实践的时候,来进行一点变化,这也体现第一部分的重要性。同时之前写的时候,没有这么写过,之前都是针对特定数据集,相当于直接把数学过程转化一下。感觉这么写是十分重要的,这体现了类的重要性。

      第三部分,之前写Lenet的时候,在网上一点一点写这么写过,但是后来为了省事,就没这么写,也没体会到这么写的重要性,Runner是真的重要,当你结构复杂了以后,再随便写,就没法调试了,而且以后自己回来看,就不能很快看懂写的啥了。

      第四部分,就是真的实践了,这一部分把你之前做好的程序在这运行一下,看看效果,其实一般都是这个流程,之前写的就是把训练好的模型保存,测试时再调用,感觉这部分真的重要,是实践得到一部分。这个实验真的要好好写呀。

最后,当然是感谢老师,问了老师好多东西,才明白了,哈哈哈

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值