第3章(上)线性分类理论解读

第3章 线性分类

分类是机器学习中最常见的一类任务,其预测标签是一些离散的类别(符号)。根据分类任务的类别数量又可以分为二分类任务和多分类任务。

线性分类是指利用一个或多个线性函数将样本进行分类。常用的线性分类模型有Logistic回归和Softmax回归。
Logistic回归是一种常用的处理二分类问题的线性模型。Softmax回归是Logistic回归在多分类问题上的推广。

在学习本章内容前,建议您先阅读《神经网络与深度学习》第3章:线性模型的相关内容,关键知识点如 图3.1 所示,以便更好的理解和掌握相应的理论知识,及其在实践中的应用方法。


图3.1 线性模型关键知识点回顾

本章内容基于 《神经网络与深度学习》第3章:线性模型 相关内容进行设计,主要包含两部分:

  • 模型解读:介绍两个最常用的线性分类模型Logistic回归和Softmax回归的原理剖析和相应的代码实现。通过理论和代码的结合,加深对线性模型的理解;

  • 案例实践:基于Softmax回归算法完成鸢尾花分类任务。

3.1 基于Logistic回归的二分类任务

在本节中,我们实现一个Logistic回归模型,并对一个简单的数据集进行二分类实验。

3.1.1 数据集构建

我们首先构建一个简单的分类任务,并构建训练集、验证集和测试集。
本任务的数据来自带噪音的两个弯月形状函数,每个弯月对一个类别。我们采集1000条样本,每个样本包含2个特征。

数据集的构建函数make_moons的代码实现如下:

import math
import copy
import paddle

def make_moons(n_samples=1000, shuffle=True, noise=None):
    """
    生成带噪音的弯月形状数据
    输入:
        - n_samples:数据量大小,数据类型为int
        - shuffle:是否打乱数据,数据类型为bool
        - noise:以多大的程度增加噪声,数据类型为None或float,noise为None时表示不增加噪声
    输出:
        - X:特征数据,shape=[n_samples,2]
        - y:标签数据, shape=[n_samples]
    """
    n_samples_out = n_samples // 2
    n_samples_in = n_samples - n_samples_out

    # 采集第1类数据,特征为(x,y)
    # 使用'paddle.linspace'在0到pi上均匀取n_samples_out个值
    # 使用'paddle.cos'计算上述取值的余弦值作为特征1,使用'paddle.sin'计算上述取值的正弦值作为特征2
    outer_circ_x = paddle.cos(paddle.linspace(0, math.pi, n_samples_out))
    outer_circ_y = paddle.sin(paddle.linspace(0, math.pi, n_samples_out))

    inner_circ_x = 1 - paddle.cos(paddle.linspace(0, math.pi, n_samples_in))
    inner_circ_y = 0.5 - paddle.sin(paddle.linspace(0, math.pi, n_samples_in))
    
    print('outer_circ_x.shape:', outer_circ_x.shape, 'outer_circ_y.shape:', outer_circ_y.shape)
    print('inner_circ_x.shape:', inner_circ_x.shape, 'inner_circ_y.shape:', inner_circ_y.shape)
    
    # 使用'paddle.concat'将两类数据的特征1和特征2分别延维度0拼接在一起[1000],得到全部特征1和特征2
    # 使用'paddle.stack'将两类特征延维度1堆叠在一起X:[1000,2]
    X = paddle.stack(
        [paddle.concat([outer_circ_x, inner_circ_x]),
        paddle.concat([outer_circ_y, inner_circ_y])],
        axis=1
    )

    print('after concat shape:', paddle.concat([outer_circ_x, inner_circ_x]).shape)
    print('X shape:', X.shape)

    # 使用'paddle. zeros'将第一类数据的标签全部设置为0
    # 使用'paddle. ones'将第一类数据的标签全部设置为1
    y = paddle.concat(
        [paddle.zeros(shape=[n_samples_out]), paddle.ones(shape=[n_samples_in])]
    )

    print('y shape:', y.shape)

    # 如果shuffle为True,将所有数据打乱
    if shuffle:
        # 使用'paddle.randperm'生成一个数值在0到X.shape[0],随机排列的一维Tensor做索引值,用于打乱数据
        idx = paddle.randperm(X.shape[0])
        X = X[idx]
        y = y[idx]

    # 如果noise存在,则给特征值加入噪声
    if noise:
        # 使用'paddle.normal'生成符合正态分布的随机Tensor作为噪声,并加到原始特征上
        X += paddle.normal(mean=0.0, std=noise, shape=X.shape)

    return X, y

随机采集1000个样本,并进行可视化。

import matplotlib.pyplot as plt
%matplotlib inline

# 采样1000个样本
n_samples = 1000
X, y = make_moons(n_samples=n_samples, shuffle=True, noise=0.5)

# 可视化生产的数据集,y[0,1]选择不同颜色,代表不同类别
plt.figure(figsize=(5,5))
plt.scatter(x=X[:, 0].tolist(), y=X[:, 1].tolist(), marker='*', c=y.tolist())
plt.xlim(-3,4)
plt.ylim(-3,4)
plt.savefig('linear-dataset-vis.pdf')
plt.show()
outer_circ_x.shape: [500] outer_circ_y.shape: [500]
inner_circ_x.shape: [500] inner_circ_y.shape: [500]
after concat shape: [1000]
X shape: [1000, 2]
y shape: [1000]

png

将1000条样本数据拆分成训练集、验证集和测试集,其中训练集640条、验证集160条、测试集200条。代码实现如下:

num_train = 640
num_dev = 160
num_test = 200

X_train, y_train = X[:num_train], y[:num_train]
X_dev, y_dev = X[num_train:num_train + num_dev], y[num_train:num_train + num_dev]
X_test, y_test = X[num_train + num_dev:], y[num_train + num_dev:]

y_train = y_train.reshape([-1,1])
y_dev = y_dev.reshape([-1,1])
y_test = y_test.reshape([-1,1])

这样,我们就完成了Moon1000数据集的构建。

# 打印X_train和y_train的维度
print("X_train shape: ", X_train.shape, "y_train shape: ", y_train.shape)
X_train shape:  [640, 2] y_train shape:  [640, 1]
# 打印一下前5个数据的标签
print (y_train[:5])
Tensor(shape=[5, 1], dtype=float32, place=CUDAPlace(0), stop_gradient=True,
       [[1.],
        [0.],
        [1.],
        [1.],
        [1.]])

3.1.2 模型构建

Logistic回归是一种常用的处理二分类问题的线性模型。与线性回归一样,Logistic回归也会将输入特征与权重做线性叠加。不同之处在于,Logistic回归引入了非线性函数 g : R D → ( 0 , 1 ) g:\mathbb{R}^D \rightarrow (0,1) g:RD(0,1),预测类别标签的后验概率 p ( y = 1 ∣ x ) p(y=1|\mathbf x) p(y=1∣x) ,从而解决连续的线性函数不适合进行分类的问题。

p ( y = 1 ∣ x ) = σ ( w T x + b ) , ( 3.1 ) p(y=1|\mathbf x) = \sigma(\mathbf w^ \mathrm{ T } \mathbf x+b),(3.1) p(y=1∣x)=σ(wTx+b),3.1

其中判别函数 σ ( ⋅ ) \sigma(\cdot) σ()为Logistic函数,也称为激活函数,作用是将线性函数 f ( x ; w , b ) f(\mathbf x;\mathbf w,b) f(x;w,b)的输出从实数区间“挤压”到(0,1)之间,用来表示概率。Logistic函数定义为:

σ ( x ) = 1 1 + exp ⁡ ( − x ) 。( 3.2 ) \sigma(x) = \frac{1}{1+\exp(-x)}。(3.2) σ(x)=1+exp(x)1。(3.2

Logistic函数

Logistic函数的代码实现如下:

# 定义Logistic函数
def logistic(x):
    return 1 / (1 + paddle.exp(-x))

# 在[-10,10]的范围内生成一系列的输入值,用于绘制函数曲线
x = paddle.linspace(-10, 10, 10000)
plt.figure()
plt.plot(x.tolist(), logistic(x).tolist(), color="#E20079", label="Logistic Function")

# 设置坐标轴
ax = plt.gca()
# 取消右侧和上侧坐标轴
ax.spines['top'].set_color('none')
ax.spines['right'].set_color('none')

# 设置坐标原点为(0,0)
# 在这里,position位置参数有三种,这里用到了“按X轴刻度位置挪动”
# 'data'表示按数值挪动,其后数字代表挪动到X轴的刻度值
ax.spines['left'].set_position(('data',0))

# 添加图例
plt.legend()
plt.savefig('linear-logistic.pdf')
plt.show()

png

从输出结果看,当输入在0附近时,Logistic函数近似为线性函数;而当输入值非常大或非常小时,函数会对输入进行抑制。输入越小,则越接近0;输入越大,则越接近1。正因为Logistic函数具有这样的性质,使得其输出可以直接看作为概率分布。

Logistic回归算子

Logistic回归模型其实就是线性层与Logistic函数的组合,通常会将 Logistic回归模型中的权重和偏置初始化为0,同时,为了提高预测样本的效率,我们将 N N N个样本归为一组进行成批地预测。

y ^ = p ( y ∣ x ) = σ ( X w + b ) , ( 3.3 ) \hat{\mathbf y} = p(\mathbf y|\mathbf x) = \sigma(\boldsymbol{X} \boldsymbol{w} + b), (3.3) y^=p(yx)=σ(Xw+b),(3.3)

其中 X ∈ R N × D \boldsymbol{X}\in \mathbb{R}^{N\times D} XRN×D N N N个样本的特征矩阵, y ^ \hat{\boldsymbol{y}} y^ N N N个样本的预测值构成的 N N N维向量。

这里,我们构建一个Logistic回归算子,代码实现如下:

from nndl import op

class model_LR(op.Op):
    def __init__(self, input_dim):
        super(model_LR, self).__init__()
        self.params = {}
        self.params['w'] = paddle.zeros(shape=[input_dim, 1])   # 将线性层的权重参数全部初始化为0
        # self.params['w'] = paddle.normal(mean=0, std=0.01, shape=[input_dim, 1])
        self.params['b'] = paddle.zeros(shape=[1])   # 将线性层的偏置参数初始化为0

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

    def forward(self, inputs):
        """
        输入:
            - inputs: shape=[N,D], N是样本数量,D为特征维度
        输出:
            - outputs:预测标签为1的概率,shape=[N,1]
        """
        # 线性计算,matmul矩阵乘法
        score = paddle.matmul(inputs, self.params['w']) + self.params['b']
        # Logistic 函数
        outputs = logistic(score)
        return outputs

测试一下

随机生成3条长度为4的数据输入Logistic回归模型,观察输出结果。

# 固定随机种子,保持每次运行结果一致
paddle.seed(0)
inputs = paddle.randn(shape=[3,4]) # 随机生成[3,4]的数据
print('Input is:', inputs)

# 实例化模型
model = model_LR(4)
outputs = model(inputs)
print('Output is:', outputs)
Input is: Tensor(shape=[3, 4], dtype=float32, place=CUDAPlace(0), stop_gradient=True,
       [[-4.08041382, -1.37199533,  0.25684971,  1.23514259],
        [ 1.85156298, -0.87903994,  0.03754762, -0.25850555],
        [-0.63441145,  0.65421218, -1.34354496,  0.15195791]])
Output is: Tensor(shape=[3, 1], dtype=float32, place=CUDAPlace(0), stop_gradient=True,
       [[0.50000000],
        [0.50000000],
        [0.50000000]])

从输出结果看,模型最终的输出 g ( ⋅ ) g(·) g()恒为0.5。这是由于采用全0初始化后,不论输入值的大小为多少,Logistic函数的输入值恒为0,因此输出恒为0.5。

3.1.3 损失函数

在模型训练过程中,需要使用损失函数来量化预测值和真实值之间的差异。
给定一个分类任务, y \mathbf y y表示样本 x \mathbf x x的标签的真实概率分布,向量 y ^ = p ( y ∣ x ) \hat{\mathbf y}=p(\mathbf y|\mathbf x) y^=p(yx)表示预测的标签概率分布。
训练目标是使得 y ^ \hat{\mathbf y} y^尽可能地接近 y \mathbf y y,通常可以使用交叉熵损失函数
在给定 y \mathbf y y的情况下,如果预测的概率分布 y ^ \hat{\mathbf y} y^与标签真实的分布 y \mathbf y y越接近,则交叉熵越小;如果 p ( x ) p(\mathbf x) p(x) y \mathbf y y越远,交叉熵就越大。

对于二分类任务,我们只需要计算 y ^ = p ( y = 1 ∣ x ) \hat{y}=p(y=1|\mathbf x) y^=p(y=1∣x),用 1 − y ^ 1-\hat{y} 1y^来表示 p ( y = 0 ∣ x ) p(y=0|\mathbf x) p(y=0∣x)
给定有 N N N个训练样本的训练集 { ( x ( n ) , y ( n ) ) } n = 1 N \{(\mathbf x^{(n)},y^{(n)})\} ^N_{n=1} {(x(n),y(n))}n=1N,使用交叉熵损失函数,Logistic回归的风险函数计算方式为:

R ( w , b ) = − 1 N ∑ n = 1 N ( y ( n ) log ⁡ y ^ ( n ) + ( 1 − y ( n ) ) log ⁡ ( 1 − y ^ ( n ) ) ) 。( 3.4 ) \begin{aligned} \cal R(\mathbf w,b) &= -\frac{1}{N}\sum_{n=1}^N \Big(y^{(n)}\log\hat{y}^{(n)} + (1-y^{(n)})\log(1-\hat{y}^{(n)})\Big)。(3.4) \end{aligned} R(w,b)=N1n=1N(y(n)logy^(n)+(1y(n))log(1y^(n)))。(3.4

向量形式可以表示为:

R ( w , b ) = − 1 N ( y T log ⁡ y ^ + ( 1 − y ) T log ⁡ ( 1 − y ^ ) ) , ( 3.5 ) \begin{aligned} \cal R(\mathbf w,b) &= -\frac{1}{N}\Big(\mathbf y^ \mathrm{ T } \log\hat{\mathbf y} + (1-\mathbf y)^ \mathrm{ T } \log(1-\hat{\mathbf y})\Big),(3.5) \end{aligned} R(w,b)=N1(yTlogy^+(1y)Tlog(1y^)),3.5

其中 y ∈ [ 0 , 1 ] N \mathbf y\in [0,1]^N y[0,1]N N N N个样本的真实标签构成的 N N N维向量, y ^ \hat{\mathbf y} y^ N N N个样本标签为1的后验概率构成的 N N N维向量。

二分类任务的交叉熵损失函数的代码实现如下:

# 实现交叉熵损失函数
class BinaryCrossEntropyLoss(op.Op):
    def __init__(self):
        self.predicts = None
        self.labels = None
        self.num = None

    def __call__(self, predicts, labels):
        return self.forward(predicts, labels)

    def forward(self, predicts, labels):
        """
        输入:
            - predicts:预测值,shape=[N, 1],N为样本数量
            - labels:真实标签,shape=[N, 1]
        输出:
            - 损失值:shape=[1]
        """
        self.predicts = predicts
        self.labels = labels
        self.num = self.predicts.shape[0]
        loss = -1. / self.num * (paddle.matmul(self.labels.t(), paddle.log(self.predicts)) + paddle.matmul((1-self.labels.t()), paddle.log(1-self.predicts)))
        loss = paddle.squeeze(loss, axis=1)
        return loss

# 测试一下
# 生成一组长度为3,值为1的标签数据
labels = paddle.ones(shape=[3,1])
# 计算风险函数
bce_loss = BinaryCrossEntropyLoss()
print(bce_loss(outputs, labels))
Tensor(shape=[1], dtype=float32, place=CUDAPlace(0), stop_gradient=True,
       [0.69314718])

3.1.4 模型优化

不同于线性回归中直接使用最小二乘法即可进行模型参数的求解,Logistic回归需要使用优化算法对模型参数进行有限次地迭代来获取更优的模型,从而尽可能地降低风险函数的值。
在机器学习任务中,最简单、常用的优化算法是梯度下降法。

使用梯度下降法进行模型优化,首先需要初始化参数 W \mathbf W W b b b,然后不断地计算它们的梯度,并沿梯度的反方向更新参数。

3.1.4.1 梯度计算

在Logistic回归中,风险函数 R ( w , b ) \cal R(\mathbf w,b) R(w,b) 关于参数 w \mathbf w w b b b的偏导数为:

∂ R ( w , b ) ∂ w = − 1 N ∑ n = 1 N x ( n ) ( y ( n ) − y ^ ( n ) ) = − 1 N X T ( y − y ^ ) ,( 3.6 ) \begin{aligned} \frac{\partial \cal R(\mathbf w,b)}{\partial \mathbf w} = -\frac{1}{N}\sum_{n=1}^N \mathbf x^{(n)}(y^{(n)}- \hat{y}^{(n)}) = -\frac{1}{N} \mathbf X^ \mathrm{ T } (\mathbf y-\hat{\mathbf y}) \end{aligned},(3.6) wR(w,b)=N1n=1Nx(n)(y(n)y^(n))=N1XT(yy^),(3.6

∂ R ( w , b ) ∂ b = − 1 N ∑ n = 1 N ( y ( n ) − y ^ ( n ) ) = − 1 N s u m ( y − y ^ ) 。( 3.7 ) \begin{aligned} \frac{\partial \cal R(\mathbf w,b)}{\partial b} = -\frac{1}{N}\sum_{n=1}^N (y^{(n)}- \hat{y}^{(n)}) = -\frac{1}{N} \mathbf {sum}(\mathbf y-\hat{\mathbf y}) \end{aligned}。(3.7) bR(w,b)=N1n=1N(y(n)y^(n))=N1sum(yy^)。(3.7

通常将偏导数的计算过程定义在Logistic回归算子的backward函数中,代码实现如下:

class model_LR(op.Op):
    def __init__(self, input_dim):
        super(model_LR, self).__init__()
        self.params = {}              # 存放线性层参数
        self.params['w'] = paddle.zeros(shape=[input_dim, 1])
        # self.params['w'] = paddle.normal(mean=0, std=0.01, shape=[input_dim, 1])
        self.params['b'] = paddle.zeros(shape=[1])
        
        # 存放参数的梯度
        self.grads = {}
        self.X = None
        self.outputs = None

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

    def forward(self, inputs):
        self.X = inputs
        # 线性计算
        score = paddle.matmul(inputs, self.params['w']) + self.params['b']
        # Logistic 函数
        self.outputs = logistic(score)
        return self.outputs

    def backward(self, labels):
        """
        输入:
            - labels:真实标签,shape=[N, 1]
        """
        N = labels.shape[0]
        # 计算偏导数
        self.grads['w'] = -1 / N * paddle.matmul(self.X.t(), (labels - self.outputs))
        self.grads['b'] = -1 / N * paddle.sum(labels - self.outputs)
3.1.4.2 参数更新

在计算参数的梯度之后,我们按照下面公式更新参数:

w ← w − α ∂ R ( w , b ) ∂ w ,( 3.8 ) \mathbf w\leftarrow \mathbf w - \alpha \frac{\partial \cal R(\mathbf w,b)}{\partial \mathbf w},(3.8) wwαwR(w,b),(3.8

b ← b − α ∂ R ( w , b ) ∂ b ,( 3.9 ) b\leftarrow b - \alpha \frac{\partial \cal R(\mathbf w,b)}{\partial b},(3.9) bbαbR(w,b),(3.9

其中 α \alpha α 为学习率。

将上面的参数更新过程包装为优化器,首先定义一个优化器基类Optimizer,方便后续所有的优化器调用。在这个基类中,需要初始化优化器的初始学习率init_lr,以及指定优化器需要优化的参数。代码实现如下:

from abc import abstractmethod

# 优化器基类
class Optimizer(object):
    def __init__(self, init_lr, model):
        """
        优化器类初始化
        """
        # 初始化学习率,用于参数更新的计算
        self.init_lr = init_lr
        # 指定优化器需要优化的模型
        self.model = model

    @abstractmethod
    def step(self):
        """
        定义每次迭代如何更新参数
        """
        pass

然后实现一个梯度下降法的优化器函数SimpleBatchGD来执行参数更新过程。其中step函数从模型的grads属性取出参数的梯度并更新。代码实现如下:

class SimpleBatchGD(Optimizer):
    def __init__(self, init_lr, model):
        super(SimpleBatchGD, self).__init__(init_lr=init_lr, model=model)

    def step(self):
        # 参数更新
        # 遍历所有参数,按照公式(3.8)和(3.9)更新参数
        if isinstance(self.model.params, dict):
            for key in self.model.params.keys():
                self.model.params[key] = self.model.params[key] - self.init_lr * self.model.grads[key]

3.1.5 评价指标

在分类任务中,通常使用准确率(Accuracy)作为评价指标。如果模型预测的类别与真实类别一致,则说明模型预测正确。准确率即正确预测的数量与总的预测数量的比值:

A = 1 N ∑ n = 1 N I ( y ( n ) = y ^ ( n ) ) , ( 3.10 ) \mathcal{A} = \frac{1}{N} \sum_{n=1}^N I (y^{(n)} = \hat{y}^{(n)}),(3.10) A=N1n=1NI(y(n)=y^(n)),3.10

其中 I ( ⋅ ) I(·) I()是指示函数。代码实现如下:

def accuracy(preds, labels):
    """
    输入:
        - preds:预测值,二分类时,shape=[N, 1],N为样本数量,多分类时,shape=[N, C],C为类别数量
        - labels:真实标签,shape=[N, 1]
    输出:
        - 准确率:shape=[1]
    """
    # 判断是二分类任务还是多分类任务,preds.shape[1]=1时为二分类任务,preds.shape[1]>1时为多分类任务
    if preds.shape[1] == 1:
        # 二分类时,判断每个概率值是否大于0.5,当大于0.5时,类别为1,否则类别为0
        # 使用'paddle.cast'将preds的数据类型转换为float32类型
        preds = paddle.cast((preds>=0.5),dtype='float32')
    else:
        # 多分类时,使用'paddle.argmax'计算最大元素索引作为类别
        preds = paddle.argmax(preds,axis=1, dtype='int32')
    return paddle.mean(paddle.cast(paddle.equal(preds, labels),dtype='float32'))

# 假设模型的预测值为[[0.],[1.],[1.],[0.]],真实类别为[[1.],[1.],[0.],[0.]],计算准确率
preds = paddle.to_tensor([[0.],[1.],[1.],[0.]])
labels = paddle.to_tensor([[1.],[1.],[0.],[0.]])
print("accuracy is:", accuracy(preds, labels))
accuracy is: Tensor(shape=[1], dtype=float32, place=CUDAPlace(0), stop_gradient=True,
       [0.50000000])
# paddle.argmax(x)的用法详解
import paddle

x = paddle.to_tensor([[5,8,9,5],
                     [0,0,1,7],
                     [6,9,2,4]])
print(x.shape)
out1 = paddle.argmax(x)
print(out1) # 2
out2 = paddle.argmax(x, axis=0)
print(out2)
# [2, 2, 0, 1]
out3 = paddle.argmax(x, axis=-1)
print(out3)
# [2, 3, 1]
out4 = paddle.argmax(x, axis=1)
print(out4)
# [2, 3, 1]
[3, 4]
Tensor(shape=[1], dtype=int64, place=CUDAPlace(0), stop_gradient=True,
       [2])
Tensor(shape=[4], dtype=int64, place=CUDAPlace(0), stop_gradient=True,
       [2, 2, 0, 1])
Tensor(shape=[3], dtype=int64, place=CUDAPlace(0), stop_gradient=True,
       [2, 3, 1])
Tensor(shape=[3], dtype=int64, place=CUDAPlace(0), stop_gradient=True,
       [2, 3, 1])

3.1.6 完善Runner类

基于RunnerV1,本章的RunnerV2类在训练过程中使用梯度下降法进行网络优化,模型训练过程中计算在训练集和验证集上的损失及评估指标并打印,训练过程中保存最优模型。代码实现如下:

# 用RunnerV2类封装整个训练过程
class RunnerV2(object):
    def __init__(self, model, optimizer, metric, loss_fn):
        self.model = model
        self.optimizer = optimizer
        self.loss_fn = loss_fn
        self.metric = metric
        # 记录训练过程中的评价指标metric变化情况
        self.train_scores = []
        self.dev_scores = []
        # 记录训练过程中的损失函数loss_fn变化情况
        self.train_loss = []
        self.dev_loss = []

    def train(self, train_set, dev_set, **kwargs):
        # 传入训练轮数,传入log打印频率,传入模型保存路径,梯度打印函数
        num_epochs = kwargs.get("num_epochs", 0)
        log_epochs = kwargs.get("log_epochs", 100)
        save_path = kwargs.get("save_path", "best_model.pdparams")
        print_grads = kwargs.get("print_grads", None)
        # 记录全局最优指标
        best_score = 0
        for epoch in range(num_epochs):
            # 获取模型预测
            X, y = train_set
            logits = self.model(X)
            # 计算交叉熵损失
            trn_loss = self.loss_fn(logits, y).item()
            self.train_loss.append(trn_loss)
            # 计算评价指标
            trn_score = self.metric(logits, y).item()
            self.train_scores.append(trn_score)
            # 计算参数梯度,并打印每一层的梯度
            self.model.backward(y)
            if print_grads is not None:
                print_grads(self.model)
            # 更新模型参数
            self.optimizer.step()
            dev_score, dev_loss = self.evaluate(dev_set)
            # 如果当前指标为最优指标,保存该模型
            if dev_score > best_score:
                self.save_model(save_path)
                print(f"最佳精度模型已保存: {best_score:.5f} --> {dev_score:.5f}")
                best_score = dev_score
            if epoch % log_epochs == 0:
                print(f"[Train] epoch: {epoch}, loss: {trn_loss:.5f}, score: {trn_score:.5f}")
                print(f"[Dev] epoch: {epoch}, loss: {dev_loss:.5f}, score: {dev_score:.5f}")
                
    def evaluate(self, data_set):
        X, y = data_set
        # 计算模型输出
        logits = self.model(X)
        # 计算损失函数
        loss = self.loss_fn(logits, y).item()
        self.dev_loss.append(loss)
        # 计算评价指标
        score = self.metric(logits, y).item()
        self.dev_scores.append(score)
        return score, loss

    def predict(self, X):
        return self.model(X)

    def save_model(self, save_path):
        paddle.save(self.model.params, save_path)

    def load_model(self, model_path):
        self.model.params = paddle.load(model_path)

3.1.7 模型训练

下面进行Logistic回归模型的训练,使用交叉熵损失函数和梯度下降法进行优化。
使用训练集和验证集进行模型训练,共训练 500个epoch,每隔50个epoch打印出训练集上的指标。
代码实现如下:

# 固定随机种子,保持每次运行结果一致
paddle.seed(102)

# 特征维度
input_dim = 2
# 学习率
lr = 0.1

# 实例化模型,指定优化器,指定损失函数,指定评价方式
model = model_LR(input_dim=input_dim)
optimizer = SimpleBatchGD(init_lr=lr, model=model)
loss_fn = BinaryCrossEntropyLoss()
metric = accuracy

# 实例化RunnerV2类,并传入训练配置
runner = RunnerV2(model, optimizer, metric, loss_fn)

runner.train([X_train, y_train], [X_dev, y_dev], num_epochs=500, log_epochs=50, save_path="best_model.pdparams")
最佳精度模型已保存: 0.00000 --> 0.71250
[Train] epoch: 0, loss: 0.69315, score: 0.49219
[Dev] epoch: 0, loss: 0.68534, score: 0.71250
最佳精度模型已保存: 0.71250 --> 0.71875
最佳精度模型已保存: 0.71875 --> 0.72500
最佳精度模型已保存: 0.72500 --> 0.73125
最佳精度模型已保存: 0.73125 --> 0.73750
最佳精度模型已保存: 0.73750 --> 0.74375
最佳精度模型已保存: 0.74375 --> 0.75000
最佳精度模型已保存: 0.75000 --> 0.75625
[Train] epoch: 50, loss: 0.50106, score: 0.78594
[Dev] epoch: 50, loss: 0.52947, score: 0.75625
最佳精度模型已保存: 0.75625 --> 0.76250
最佳精度模型已保存: 0.76250 --> 0.76875
[Train] epoch: 100, loss: 0.46461, score: 0.79375
[Dev] epoch: 100, loss: 0.49335, score: 0.76875
最佳精度模型已保存: 0.76875 --> 0.78125
最佳精度模型已保存: 0.78125 --> 0.78750
[Train] epoch: 150, loss: 0.45221, score: 0.79844
[Dev] epoch: 150, loss: 0.47837, score: 0.78125
[Train] epoch: 200, loss: 0.44677, score: 0.80000
[Dev] epoch: 200, loss: 0.47064, score: 0.78125
[Train] epoch: 250, loss: 0.44401, score: 0.79844
[Dev] epoch: 250, loss: 0.46618, score: 0.78125
[Train] epoch: 300, loss: 0.44248, score: 0.79531
[Dev] epoch: 300, loss: 0.46342, score: 0.77500
[Train] epoch: 350, loss: 0.44159, score: 0.79687
[Dev] epoch: 350, loss: 0.46164, score: 0.78125
[Train] epoch: 400, loss: 0.44104, score: 0.80000
[Dev] epoch: 400, loss: 0.46045, score: 0.78750
[Train] epoch: 450, loss: 0.44069, score: 0.80156
[Dev] epoch: 450, loss: 0.45964, score: 0.78750

可视化观察训练集与验证集的准确率和损失的变化情况。

# 可视化观察训练集与验证集的指标变化情况
def plot(runner,fig_name):
    plt.figure(figsize=(10,5))
    plt.subplot(1,2,1)
    epochs = [i for i in range(len(runner.train_scores))]
    # 绘制训练损失变化曲线
    plt.plot(epochs, runner.train_loss, color='#8E004D', label="Train loss")
    # 绘制评价损失变化曲线
    plt.plot(epochs, runner.dev_loss, color='#E20079', linestyle='--', label="Dev loss")
    # 绘制坐标轴和图例
    plt.ylabel("loss")
    plt.xlabel("epoch")
    plt.legend(loc='upper right')

    plt.subplot(1,2,2)
    # 绘制训练准确率变化曲线
    plt.plot(epochs, runner.train_scores, color='#8E004D', label="Train accuracy")
    # 绘制评价准确率变化曲线
    plt.plot(epochs, runner.dev_scores, color='#E20079', linestyle='--', label="Dev accuracy")
    # 绘制坐标轴和图例
    plt.ylabel("score")
    plt.xlabel("epoch")
    plt.legend(loc='lower right')
    # tight_layout会自动调整子图参数,使之填充整个图像区域。
    plt.tight_layout()
    plt.show()
    plt.savefig(fig_name)
    
plot(runner,fig_name='linear-acc.pdf')

png

<Figure size 640x480 with 0 Axes>

从输出结果可以看到,在训练集与验证集上,loss得到了收敛,同时准确率指标都达到了较高的水平,训练比较充分。

3.1.8 模型评价

使用测试集对训练完成后的最终模型进行评价,观察模型在测试集上的准确率和loss数据。代码实现如下:

score, loss = runner.evaluate([X_test, y_test])
print(f"[Test] score/loss: {score:.4f}/{loss:.4f}")
[Test] score/loss: 0.8450/0.3813

可视化观察拟合的决策边界 X w + b = 0 \boldsymbol{X} \boldsymbol{w} + b=0 Xw+b=0

def decision_boundary(w, b, x1):
    w1, w2 = w
    x2 = (- w1 * x1 - b) / w2
    return x2
plt.figure(figsize=(5,5))
# 绘制原始数据
plt.scatter(X[:, 0].tolist(), X[:, 1].tolist(), marker='*', c=y.tolist())

w = model.params['w']
b = model.params['b']
x1 = paddle.linspace(-2, 3, 1000)
x2 = decision_boundary(w, b, x1)
# 绘制决策边界
plt.plot(x1.tolist(), x2.tolist(), color="red")
plt.show()

png

3.2 基于Softmax回归的多分类任务

Logistic回归可以有效地解决二分类问题,但在分类任务中,还有一类多分类问题,即类别数 C C C大于2 的分类问题。Softmax回归就是Logistic回归在多分类问题上的推广。

使用Softmax回归模型对一个简单的数据集进行多分类实验。

3.2.1 数据集构建

我们首先构建一个简单的多分类任务,并构建训练集、验证集和测试集。
本任务的数据来自3个不同的簇,每个簇对一个类别。我们采集1000条样本,每个样本包含2个特征。

数据集的构建函数make_multi的代码实现如下:

import paddle
import numpy as np

n_samples=100
n_features=2
n_classes=3
n_samples_per_class = [int(n_samples / n_classes) for k in range(n_classes)]
print(n_samples_per_class)

# [:n_classes]为切片操作,[3, 0, 1, 2]-->[3, 0, 1]
centroids = paddle.randperm(2 ** n_features)[:n_classes]
print(centroids)

print(centroids.numpy())

# [:, -n_features:]为切片操作,                 [[0 0 0 0 0 0 1 1]           [[1 1]
#                             [3, 0, 1]  -----> [0 0 0 0 0 0 0 0]    ----->  [0 0]
#                                               [0 0 0 0 0 0 0 1]]           [0 1]]
centroids_bin = np.unpackbits(centroids.numpy().astype('uint8')).reshape((-1, 8))[:, -n_features:]
print(centroids_bin)
[33, 33, 33]
Tensor(shape=[3], dtype=int64, place=CUDAPlace(0), stop_gradient=True,
       [0, 2, 1])
[0 2 1]
[[0 0]
 [1 0]
 [0 1]]
import numpy as np

def make_multiclass_classification(n_samples=100, n_features=2, n_classes=3, shuffle=True, noise=0.1):
    """
    生成带噪音的多类别数据
    输入:
        - n_samples:数据量大小,数据类型为int
        - n_features:特征数量,数据类型为int
        - shuffle:是否打乱数据,数据类型为bool
        - noise:以多大的程度增加噪声,数据类型为None或float,noise为None时表示不增加噪声
    输出:
        - X:特征数据,shape=[n_samples,2]
        - y:标签数据, shape=[n_samples,1]
    """
    # 计算每个类别的样本数量
    n_samples_per_class = [int(n_samples / n_classes) for k in range(n_classes)]
    for i in range(n_samples - sum(n_samples_per_class)):
        n_samples_per_class[i % n_classes] += 1
    # 将特征和标签初始化为0
    X = paddle.zeros([n_samples, n_features])
    y = paddle.zeros([n_samples], dtype='int32')
    # 随机生成3个簇中心作为类别中心,randperm返回一个数值在 0 到 n-1的随机数组,unpackbits函数可以把整数转化成8位2进制数。
    centroids = paddle.randperm(2 ** n_features)[:n_classes]
    centroids_bin = np.unpackbits(centroids.numpy().astype('uint8')).reshape((-1, 8))[:, -n_features:]
    centroids = paddle.to_tensor(centroids_bin, dtype='float32')
    # 控制簇中心的分离程度
    centroids = 1.5 * centroids - 1
    # 随机生成特征值
    X[:, :n_features] = paddle.randn(shape=[n_samples, n_features])

    stop = 0
    # 将每个类的特征值控制在簇中心附近, k = [0, 1, 2], centroid = [[0 1], [1 0], [1 1]]
    for k, centroid in enumerate(centroids):
        start, stop = stop, stop + n_samples_per_class[k]
        # 指定标签值
        y[start:stop] = k % n_classes
        X_k = X[start:stop, :n_features]
        # 控制每个类别特征值的分散程度
        A = 2 * paddle.rand(shape=[n_features, n_features]) - 1
        X_k[...] = paddle.matmul(X_k, A)
        X_k += centroid
        X[start:stop, :n_features] = X_k

    # 如果noise不为None,则给特征加入噪声
    if noise > 0.0:
        # 生成noise掩膜,用来指定给那些样本加入噪声
        noise_mask = paddle.rand([n_samples]) < noise
        for i in range(len(noise_mask)):
            if noise_mask[i]:
                # 给加噪声的样本随机赋标签值
                y[i] = paddle.randint(n_classes, shape=[1]).astype('int32')
    # 如果shuffle为True,将所有数据打乱
    if shuffle:
        idx = paddle.randperm(X.shape[0])
        X = X[idx]
        y = y[idx]

    return X, y

随机采集1000个样本,并进行可视化。

# 固定随机种子,保持每次运行结果一致
paddle.seed(102)
# 采样1000个样本
n_samples = 1000
X, y = make_multiclass_classification(n_samples=n_samples, n_features=2, n_classes=3, noise=0.2)

# 可视化生产的数据集,不同颜色代表不同类别
plt.figure(figsize=(5,5))
plt.scatter(x=X[:, 0].tolist(), y=X[:, 1].tolist(), marker='*', c=y.tolist())
plt.savefig('linear-dataset-vis2.pdf')
plt.show()

png

将实验数据拆分成训练集、验证集和测试集。其中训练集640条、验证集160条、测试集200条。

num_train = 640
num_dev = 160
num_test = 200

X_train, y_train = X[:num_train], y[:num_train]
X_dev, y_dev = X[num_train:num_train + num_dev], y[num_train:num_train + num_dev]
X_test, y_test = X[num_train + num_dev:], y[num_train + num_dev:]

# 打印X_train和y_train的维度
print("X_train shape: ", X_train.shape, "y_train shape: ", y_train.shape)
X_train shape:  [640, 2] y_train shape:  [640]

这样,我们就完成了Multi1000数据集的构建。

# 打印前5个数据的标签
print(y_train[:5])
Tensor(shape=[5], dtype=int32, place=CUDAPlace(0), stop_gradient=True,
       [1, 2, 2, 1, 2])

3.2.2 模型构建

在Softmax回归中,对类别进行预测的方式是预测输入属于每个类别的条件概率。与Logistic 回归不同的是,Softmax回归的输出值个数等于类别数 C C C,而每个类别的概率值则通过Softmax函数进行求解。

3.2.2.1 Softmax函数

Softmax函数可以将多个标量映射为一个概率分布。对于一个 K K K维向量, x = [ x 1 , ⋯   , x K ] \mathbf x=[x_1,\cdots,x_K] x=[x1,,xK],Softmax的计算公式为

s o f t m a x ( x k ) = exp ⁡ ( x k ) ∑ i = 1 K exp ⁡ ( x i ) 。( 3.11 ) \mathrm{softmax}(x_k) = \frac{\exp(x_k)}{\sum_{i=1}^K \exp(x_i)}。(3.11) softmax(xk)=i=1Kexp(xi)exp(xk)。(3.11

在Softmax函数的计算过程中,要注意上溢出下溢出的问题。假设Softmax 函数中所有的 x k x_k xk都是相同大小的数值 a a a,理论上,所有的输出都应该为 1 k \frac{1}{k} k1。但需要考虑如下两种特殊情况:

  • a a a为一个非常大的负数,此时 exp ⁡ ( a ) \exp(a) exp(a) 会发生下溢出现象。计算机在进行数值计算时,当数值过小,会被四舍五入为0。此时,Softmax函数的分母会变为0,导致计算出现问题;
  • a a a为一个非常大的正数,此时会导致 exp ⁡ ( a ) \exp(a) exp(a)发生上溢出现象,导致计算出现问题。

为了解决上溢出和下溢出的问题,在计算Softmax函数时,可以使用 x k − max ⁡ ( x ) x_k - \max(\mathbf x) xkmax(x)代替 x k x_k xk。 此时,通过减去最大值, x k x_k xk最大为0,避免了上溢出的问题;同时,分母中至少会包含一个值为1的项,从而也避免了下溢出的问题。

Softmax函数的代码实现如下:

# x为tensor
def softmax(X):
    """
    输入:
        - X:shape=[N, C],N为向量数量,C为向量维度
    """
    x_max = paddle.max(X, axis=1, keepdim=True)    #N,1
    x_exp = paddle.exp(X - x_max)
    partition = paddle.sum(x_exp, axis=1, keepdim=True)  #N,1
    return x_exp / partition

# 观察softmax的计算方式
X = paddle.to_tensor([[0.1, 0.2, 0.3, 0.4],[1,2,3,4]])
predict = softmax(X)
print(predict)
Tensor(shape=[2, 4], dtype=float32, place=CUDAPlace(0), stop_gradient=True,
       [[0.21383820, 0.23632780, 0.26118258, 0.28865141],
        [0.03205860, 0.08714432, 0.23688284, 0.64391428]])
3.2.2.2 Softmax回归算子

在Softmax回归中,类别标签 y ∈ { 1 , 2 , … , C } y\in\{1,2,…,C\} y{1,2,,C}。给定一个样本 x \mathbf x x,使用Softmax回归预测的属于类别 c c c的条件概率为

p ( y = c ∣ x ) = s o f t m a x ( w c T x + b c ) ,( 3.12 ) \begin{aligned} p(y=c|\mathbf x) &= \mathrm{softmax}(\mathbf w_c^T \mathbf x+b_c),(3.12) \end{aligned} p(y=cx)=softmax(wcTx+bc),(3.12

其中 w c \mathbf w_c wc是第 c c c 类的权重向量, b c b_c bc是第 c c c 类的偏置。

Softmax回归模型其实就是线性函数与Softmax函数的组合。

N N N个样本归为一组进行成批地预测。

Y ^ = s o f t m a x ( X W + b ) , ( 3.13 ) \hat{\mathbf Y} = \mathrm{softmax}(\boldsymbol{X} \boldsymbol{W} + \mathbf b), (3.13) Y^=softmax(XW+b),(3.13)

其中 X ∈ R N × D \boldsymbol{X}\in \mathbb{R}^{N\times D} XRN×D N N N个样本的特征矩阵, W = [ w 1 , … … , w C ] \boldsymbol{W}=[\mathbf w_1,……,\mathbf w_C] W=[w1,……,wC] C C C个类的权重向量组成的矩阵, Y ^ ∈ R C \hat{\mathbf Y}\in \mathbb{R}^{C} Y^RC为所有类别的预测条件概率组成的矩阵。

我们根据公式(3.13)实现Softmax回归算子,代码实现如下:

class model_SR(op.Op):
    def __init__(self, input_dim, output_dim):
        super(model_SR, self).__init__()
        # 将线性层的权重参数全部初始化为0,偏置参数初始化为0
        self.params = {}
        self.params['W'] = paddle.zeros(shape=[input_dim, output_dim])
        # self.params['W'] = paddle.normal(mean=0, std=0.01, shape=[input_dim, output_dim])
        self.params['b'] = paddle.zeros(shape=[output_dim])
        self.outputs = None

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

    def forward(self, inputs):
        """
        输入:
            - inputs: shape=[N,D], N是样本数量,D是特征维度
        输出:
            - outputs:预测值,shape=[N,C],C是类别数
        """
        # 线性计算
        score = paddle.matmul(inputs, self.params['W']) + self.params['b']
        # Softmax 函数
        self.outputs = softmax(score)
        return self.outputs

# 随机生成1条长度为4的数据
inputs = paddle.randn(shape=[1,4])
print('Input is:', inputs)

# 实例化模型,这里令输入长度为4,输出类别数为3
model = model_SR(input_dim=4, output_dim=3)
outputs = model(inputs)
print('Output is:', outputs)
Input is: Tensor(shape=[1, 4], dtype=float32, place=CUDAPlace(0), stop_gradient=True,
       [[-0.51168263, -0.10752964,  0.67060655,  0.26857111]])
Output is: Tensor(shape=[1, 3], dtype=float32, place=CUDAPlace(0), stop_gradient=True,
       [[0.33333334, 0.33333334, 0.33333334]])

从输出结果可以看出,采用全0初始化后,属于每个类别的条件概率均为 1 C \frac{1}{C} C1。这是因为,不论输入值的大小为多少,线性函数 f ( x ; W , b ) f(\mathbf x;\mathbf W,\mathbf b) f(x;W,b)的输出值恒为0。此时,再经过Softmax函数的处理,每个类别的条件概率恒等。

3.2.3 损失函数

Softmax回归同样使用交叉熵损失作为损失函数,并使用梯度下降法对参数进行优化。通常使用 C C C维的one-hot类型向量 y ∈ { 0 , 1 } C \mathbf y \in \{0,1\}^C y{0,1}C来表示多分类任务中的类别标签。对于类别 c c c,其向量表示为:

y = [ I ( 1 = c ) , I ( 2 = c ) , … , I ( C = c ) ] T ,( 3.14 ) \mathbf y = [I(1=c),I(2=c),…,I(C=c)]^T,(3.14) y=[I(1=c),I(2=c),,I(C=c)]T,(3.14

其中 I ( ⋅ ) I(·) I()是指示函数,即括号内的输入为“真”, I ( ⋅ ) = 1 I(·)=1 I()=1;否则, I ( ⋅ ) = 0 I(·)=0 I()=0

给定有 N N N个训练样本的训练集 { ( x ( n ) , y ( n ) ) } n = 1 N \{(\mathbf x^{(n)},y^{(n)})\} ^N_{n=1} {(x(n),y(n))}n=1N,令 y ^ ( n ) = s o f t m a x ( W T x ( n ) + b ) \hat{\mathbf y}^{(n)}=\mathrm{softmax}(\mathbf W^ \mathrm{ T } \mathbf x^{(n)}+\mathbf b) y^(n)=softmax(WTx(n)+b)为样本 x ( n ) \mathbf x^{(n)} x(n)在每个类别的后验概率。多分类问题的交叉熵损失函数定义为:

R ( W , b ) = − 1 N ∑ n = 1 N ( y ( n ) ) T log ⁡ y ^ ( n ) = − 1 N ∑ n = 1 N ∑ c = 1 C y c ( n ) log ⁡ y ^ c ( n ) . ( 3.15 ) \cal R(\mathbf W,\mathbf b) = -\frac{1}{N}\sum_{n=1}^N (\mathbf y^{(n)})^ \mathrm{ T } \log\hat{\mathbf y}^{(n)} = -\frac{1}{N}\sum_{n=1}^N \sum_{c=1}^C \mathbf y_c^{(n)} \log\hat{\mathbf y}_c^{(n)}.(3.15) R(W,b)=N1n=1N(y(n))Tlogy^(n)=N1n=1Nc=1Cyc(n)logy^c(n).3.15

观察上式, y c ( n ) \mathbf y_c^{(n)} yc(n) c c c为真实类别时为1,其余都为0。也就是说,交叉熵损失只关心正确类别的预测概率,因此,上式又可以优化为:

R ( W , b ) = − 1 N ∑ n = 1 N log ⁡ [ y ^ ( n ) ] y ( n ) , ( 3.16 ) \cal R(\mathbf W,\mathbf b) = -\frac{1}{N}\sum_{n=1}^N \log [\hat{\mathbf y}^{(n)}]_{y^{(n)}},(3.16) R(W,b)=N1n=1Nlog[y^(n)]y(n),3.16

其中 y ( n ) y^{(n)} y(n)是第 n n n个样本的标签。

因此,多类交叉熵损失函数的代码实现如下:

class MultiCrossEntropyLoss(op.Op):
    def __init__(self):
        self.predicts = None
        self.labels = None
        self.num = None

    def __call__(self, predicts, labels):
        return self.forward(predicts, labels)

    def forward(self, predicts, labels):
        """
        输入:
            - predicts:预测值,shape=[N, 1],N为样本数量
            - labels:真实标签,shape=[N, 1]
        输出:
            - 损失值:shape=[1]
        """
        self.predicts = predicts
        self.labels = labels
        self.num = self.predicts.shape[0]
        loss = 0
        for i in range(0, self.num):
            index = self.labels[i]
            loss -= paddle.log(self.predicts[i][index])
        return loss / self.num

# 测试一下
# 假设真实标签为第1类
labels = paddle.to_tensor([0])
# 计算风险函数
mce_loss = MultiCrossEntropyLoss()
print(mce_loss(outputs, labels))
Tensor(shape=[1], dtype=float32, place=CUDAPlace(0), stop_gradient=True,
       [1.09861231])

3.2.4 模型优化

使用梯度下降法进行参数学习。

3.2.4.1 梯度计算

计算风险函数 R ( W , b ) \cal R(\mathbf W,\mathbf b) R(W,b)关于参数 W \mathbf W W b \mathbf b b的偏导数。在Softmax回归中,计算方法为:

∂ R ( W , b ) ∂ W = − 1 N ∑ n = 1 N x ( n ) ( y ( n ) − y ^ ( n ) ) T = − 1 N X T ( y − y ^ ) , ( 3.17 ) \frac{\partial \cal R(\mathbf W,\mathbf b)}{\partial \mathbf W} = -\frac{1}{N}\sum_{n=1}^N \mathbf x^{(n)}(y^{(n)}- \hat{ y}^{(n)})^T = -\frac{1}{N} \mathbf X^ \mathrm{ T } (\mathbf y- \hat{\mathbf y}),(3.17) WR(W,b)=N1n=1Nx(n)(y(n)y^(n))T=N1XT(yy^),3.17

∂ R ( W , b ) ∂ b = − 1 N ∑ n = 1 N ( y ( n ) − y ^ ( n ) ) T = − 1 N 1 ( y − y ^ ) . ( 3.18 ) \frac{\partial \cal R(\mathbf W,\mathbf b)}{\partial \mathbf b} = -\frac{1}{N}\sum_{n=1}^N (y^{(n)}- \hat{y}^{(n)})^T = -\frac{1}{N} \mathbf 1 (\mathbf y- \hat{\mathbf y}).(3.18) bR(W,b)=N1n=1N(y(n)y^(n))T=N11(yy^).3.18

其中 X ∈ R N × D \mathbf X\in \mathbb{R}^{N\times D} XRN×D N N N个样本组成的矩阵, y ∈ R N \mathbf y\in \mathbb{R}^{N} yRN N N N个样本标签组成的向量, y ^ ∈ R N \hat{\mathbf y}\in \mathbb{R}^{N} y^RN N N N个样本的预测标签组成的向量, 1 \mathbf{1} 1 N N N维的全1向量。

将上述计算方法定义在模型的backward函数中,代码实现如下:

class model_SR(op.Op):
    def __init__(self, input_dim, output_dim):
        super(model_SR, self).__init__()
        # 将线性层的权重参数全部初始化为0,偏置参数初始化为0
        self.params = {}
        self.params['W'] = paddle.zeros(shape=[input_dim, output_dim])
        # self.params['W'] = paddle.normal(mean=0, std=0.01, shape=[input_dim, output_dim])
        self.params['b'] = paddle.zeros(shape=[output_dim])
        # 存放参数的梯度
        self.grads = {}
        self.X = None
        self.outputs = None
        self.output_dim = output_dim

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

    def forward(self, inputs):
        self.X = inputs
        # 线性计算
        score = paddle.matmul(self.X, self.params['W']) + self.params['b']
        # Softmax 函数
        self.outputs = softmax(score)
        return self.outputs

    def backward(self, labels):
        """
        输入:
            - labels:真实标签,shape=[N, 1],其中N为样本数量
        """
        # 计算偏导数
        N =labels.shape[0]
        labels = paddle.nn.functional.one_hot(labels, self.output_dim)
        self.grads['W'] = -1 / N * paddle.matmul(self.X.t(), (labels-self.outputs))
        self.grads['b'] = -1 / N * paddle.matmul(paddle.ones(shape=[N]), (labels-self.outputs))
3.2.4.2 参数更新

在计算参数的梯度之后,我们使用3.1.4.2中实现的梯度下降法进行参数更新。

3.2.5 模型训练

实例化RunnerV2类,并传入训练配置。使用训练集和验证集进行模型训练,共训练500个epoch。每隔50个epoch打印训练集上的指标。代码实现如下:

# 固定随机种子,保持每次运行结果一致
paddle.seed(102)

# 特征维度,类别数,学习率
input_dim = 2
output_dim = 3
lr = 0.1

# 实例化模型,指定优化器,指定损失函数,指定评价方式,实例化RunnerV2类
model = model_SR(input_dim=input_dim, output_dim=output_dim)
optimizer = SimpleBatchGD(init_lr=lr, model=model)
loss_fn = MultiCrossEntropyLoss()
metric = accuracy
runner = RunnerV2(model, optimizer, metric, loss_fn)

# 模型训练
runner.train([X_train, y_train], [X_dev, y_dev], num_epochs=500, log_eopchs=50, eval_epochs=1, save_path="best_model.pdparams")

# 可视化观察训练集与验证集的准确率变化情况
plot(runner,fig_name='linear-acc2.pdf')
最佳精度模型已保存: 0.00000 --> 0.68125
[Train] epoch: 0, loss: 1.09862, score: 0.36094
[Dev] epoch: 0, loss: 1.07567, score: 0.68125
最佳精度模型已保存: 0.68125 --> 0.69375
最佳精度模型已保存: 0.69375 --> 0.70000
最佳精度模型已保存: 0.70000 --> 0.70625
最佳精度模型已保存: 0.70625 --> 0.71250
最佳精度模型已保存: 0.71250 --> 0.72500
最佳精度模型已保存: 0.72500 --> 0.73750
最佳精度模型已保存: 0.73750 --> 0.75000
最佳精度模型已保存: 0.75000 --> 0.75625
最佳精度模型已保存: 0.75625 --> 0.76250
最佳精度模型已保存: 0.76250 --> 0.76875
最佳精度模型已保存: 0.76875 --> 0.77500
最佳精度模型已保存: 0.77500 --> 0.78750
最佳精度模型已保存: 0.78750 --> 0.79375
最佳精度模型已保存: 0.79375 --> 0.80000
最佳精度模型已保存: 0.80000 --> 0.81250
最佳精度模型已保存: 0.81250 --> 0.81875
最佳精度模型已保存: 0.81875 --> 0.82500
最佳精度模型已保存: 0.82500 --> 0.83125
最佳精度模型已保存: 0.83125 --> 0.83750
最佳精度模型已保存: 0.83750 --> 0.84375
最佳精度模型已保存: 0.84375 --> 0.85000
最佳精度模型已保存: 0.85000 --> 0.85625
[Train] epoch: 100, loss: 0.67264, score: 0.77188
[Dev] epoch: 100, loss: 0.63602, score: 0.84375
[Train] epoch: 200, loss: 0.65181, score: 0.77500
[Dev] epoch: 200, loss: 0.60753, score: 0.84375
[Train] epoch: 300, loss: 0.64689, score: 0.78125
[Dev] epoch: 300, loss: 0.60088, score: 0.84375
[Train] epoch: 400, loss: 0.64492, score: 0.78594
[Dev] epoch: 400, loss: 0.59891, score: 0.85625

png

3.2.6 模型评价

使用测试集对训练完成后的最终模型进行评价,观察模型在测试集上的准确率。代码实现如下:

score, loss = runner.evaluate([X_test, y_test])
print(f"[Test] score/loss: {score:.4f}/{loss:.4f}")
[Test] score/loss: 0.8100/0.6299

可视化观察类别划分结果。

# 均匀生成40000个数据点
x1, x2 = paddle.meshgrid(paddle.linspace(-3.5, 2, 200), paddle.linspace(-4.5, 3.5, 200))
x = paddle.stack([paddle.flatten(x1), paddle.flatten(x2)], axis=1)

# 预测对应类别
y = runner.predict(x)
y = paddle.argmax(y, axis=1)

# 绘制类别区域
plt.ylabel('x2')
plt.xlabel('x1')
plt.scatter(x[:,0].tolist(), x[:,1].tolist(), c=y.tolist(), cmap=plt.cm.Spectral)

paddle.seed(102)
n_samples = 1000
X, y = make_multiclass_classification(n_samples=n_samples, n_features=2, n_classes=3, noise=0.2)

plt.scatter(X[:, 0].tolist(), X[:, 1].tolist(), marker='*', c=y.tolist())
<matplotlib.collections.PathCollection at 0x7fec63e30510>

png


拓展

提前停止是在使用梯度下降法进行模型优化时常用的正则化方法。对于某些拟合能力非常强的机器学习算法,当训练轮数较多时,容易发生过拟合现象,即在训练集上错误率很低,但是在未知数据(或测试集)上错误率很高。为了解决这一问题,通常会在模型优化时,使用验证集上的错误代替期望错误。当验证集上的错误率不在下降时,就停止迭代。

在3.4.3节的实验中,模型训练过程中会按照提前停止的思想保存验证集上的最优模型。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

绿洲213

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值