HBU_神经网络与深度学习 实验4 线性分类


写在前面的一些内容

  1. 本文为HBU_神经网络与深度学习实验(2022年秋)实验4的实验报告,此文的基本内容参照 [1]Github/线性模型-上.ipynb[2]线性模型-下.ipynb,检索时请按对应序号进行检索。
  2. 本实验编程语言为Python 3.10,使用Pycharm进行编程。
  3. 本实验报告目录标题级别顺序:一、 1. (1)
  4. 水平有限,难免有误,如有错漏之处敬请指正。

一、基于Logistic回归二分类任务

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


1. 数据集构建

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

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

import math
import torch

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)
    # 使用'torch.linspace'在0到pi上均匀取n_samples_out个值
    # 使用'torch.cos'计算上述取值的余弦值作为特征1,使用'torch.sin'计算上述取值的正弦值作为特征2
    outer_circ_x = torch.cos(torch.linspace(0, math.pi, n_samples_out))
    outer_circ_y = torch.sin(torch.linspace(0, math.pi, n_samples_out))

    inner_circ_x = 1 - torch.cos(torch.linspace(0, math.pi, n_samples_in))
    inner_circ_y = 0.5 - torch.sin(torch.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)

    # 使用'torch.concat'将两类数据的特征1和特征2分别延维度0拼接在一起,得到全部特征1和特征2
    # 使用'torch.stack'将两类特征延维度1堆叠在一起
    X = torch.stack(
        [torch.concat([outer_circ_x, inner_circ_x]),
         torch.concat([outer_circ_y, inner_circ_y])],
        axis=1
    )

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

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

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

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

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

    return X, y

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

# 采样1000个样本
n_samples = 1000
X, y = make_moons(n_samples=n_samples, shuffle=True, noise=0.5)
# 可视化生产的数据集,不同颜色代表不同类别

import matplotlib.pyplot as plt

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: torch.Size([500]) outer_circ_y.shape: torch.Size([500])
inner_circ_x.shape: torch.Size([500]) inner_circ_y.shape: torch.Size([500])
after concat shape: torch.Size([1000])
X shape: torch.Size([1000, 2])
y shape: torch.Size([1000])

执行代码后得到下图:

将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)

# 打印一下前5个数据的标签
print(y_train[:5])

代码执行结果:

X_train shape:  torch.Size([640, 2]) y_train shape:  torch.Size([640, 1])
tensor([[1.],
        [1.],
        [1.],
        [0.],
        [0.]])

2. 模型构建

Logistic回归是一种常用的处理二分类问题的线性模型。
p ( y = 1 ∣ x ) = σ ( w T x + b ) p(y=1|\mathbf x) = \sigma(\mathbf w^ \mathrm{ T } \mathbf x+b) p(y=1∣x)=σ(wTx+b)其中判别函数 σ ( ⋅ ) \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 ) \sigma(x) = \frac{1}{1+\exp(-x)} σ(x)=1+exp(x)1[1]

(1) Logistic函数

Logistic函数的代码实现如下:

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

# 在[-10,10]的范围内生成一系列的输入值,用于绘制函数曲线
x = torch.linspace(-10, 10, 10000)
plt.figure()
plt.plot(x, logistic(x), label="Logistic Function")
# 设置坐标轴
ax = plt.gca()
# 取消右侧和上侧坐标轴
ax.spines['top'].set_color('none')
ax.spines['right'].set_color('none')
# 设置默认的x轴和y轴方向
ax.xaxis.set_ticks_position('bottom')
ax.yaxis.set_ticks_position('left')
# 设置坐标原点为(0,0)
ax.spines['left'].set_position(('data', 0))
ax.spines['bottom'].set_position(('data', 0))
# 添加图例
plt.legend()
plt.savefig('linear-logistic.pdf')
plt.show()

代码执行结果如下图所示:

从输出结果看,当输入在0附近时,Logistic函数近似为线性函数;而当输入值非常大或非常小时,函数会对输入进行抑制。输入越小,则越接近0;输入越大,则越接近1。

(2) Logistic回归算子

Logistic回归模型其实就是线性层与Logistic函数的组合,通常会将Logistic回归模型中的权重和偏置初始化为0,同时,为了提高预测样本的效率,我们将 N N N个样本归为一组进行成批地预测。
y ^ = p ( y ∣ x ) = σ ( X w + b ) \hat{\mathbf y} = p(\mathbf y|\mathbf x) = \sigma(\boldsymbol{X} \boldsymbol{w} + b) y^=p(yx)=σ(Xw+b)其中 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维向量。[1]

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

import op

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

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

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

(3) 测试

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

# 固定随机种子,保持每次运行结果一致
torch.manual_seed(0)
# 随机生成3条长度为4的数据
inputs = torch.randn(size=[3, 4])
print('Input is:', inputs)
# 实例化模型
model = model_LR(4)
outputs = model(inputs)
print('Output is:', outputs)

代码执行结果:

Input is: tensor([[ 1.5410, -0.2934, -2.1788,  0.5684],
        [-1.0845, -1.3986,  0.4033,  0.8380],
        [-0.7193, -0.4033, -0.5966,  0.1820]])
Output is: tensor([[0.5000],
        [0.5000],
        [0.5000]])

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

(4) 随堂小问题

问题1:Logistic回归在不同的书籍中,有许多其他的称呼,具体有哪些?你认为哪个称呼最好?

逻辑回归
对数几率回归(周志华:机器学习)
逻辑斯谛回归(Understanding Machine Learning:From Theory to Algorithms译本)
按照常用原则,使用逻辑回归更简单易懂。

问题2:什么是激活函数?为什么要用激活函数?常见激活函数有哪些?

神经元中,输入的 inputs 通过加权求和后被作用的一个函数是激活函数。
激活函数可以使得神经网络可以任意逼近任何非线性函数。因为非线性函数是我们更关注的问题。
常见的激活函数有Logistic函数、softmax函数、ReLU函数等。


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越远,交叉熵就越大。

使用交叉熵损失函数,Logistic回归的风险函数计算方式为:
R ( w , b ) = − 1 N ∑ n = 1 N ( y ( n ) log ⁡ y ^ ( n ) + ( 1 − y ( n ) ) log ⁡ ( 1 − y ^ ( n ) ) ) \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) \end{aligned} R(w,b)=N1n=1N(y(n)logy^(n)+(1y(n))log(1y^(n)))向量形式可以表示为:
R ( w , b ) = − 1 N ( y T log ⁡ y ^ + ( 1 − y ) T log ⁡ ( 1 − y ^ ) ) \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) \end{aligned} R(w,b)=N1(yTlogy^+(1y)Tlog(1y^))其中 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维向量。[1]

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

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 * (
                torch.matmul(self.labels.t(), torch.log(self.predicts)) + torch.matmul((1 - self.labels.t()),
                                                                                       torch.log(
                                                                                           1 - self.predicts)))
        loss = torch.squeeze(loss, axis=1)
        return loss

# 测试一下
# 生成一组长度为3,值为1的标签数据
labels = torch.ones(size=[3, 1])
# 计算风险函数
bce_loss = BinaryCrossEntropyLoss()
print(bce_loss(outputs, labels))

代码执行结果:

tensor([0.6931])

4. 模型优化

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

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

(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 ^ ) \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} wR(w,b)=N1n=1Nx(n)(y(n)y^(n))=N1XT(yy^) ∂ R ( w , b ) ∂ b = − 1 N ∑ n = 1 N ( y ( n ) − y ^ ( n ) ) = − 1 N s u m ( y − y ^ ) \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} bR(w,b)=N1n=1N(y(n)y^(n))=N1sum(yy^)[1]

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

class model_LR(op.Op):
    def __init__(self, input_dim):
        super(model_LR, self).__init__()
        # 存放线性层参数
        self.params = {}
        # 将线性层的权重参数全部初始化为0
        self.params['w'] = torch.zeros(size=[input_dim, 1])
        # self.params['w'] = torch.normal(mean=0, std=0.01, shape=[input_dim, 1])
        # 将线性层的偏置参数初始化为0
        self.params['b'] = torch.zeros(size=[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 = torch.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 * torch.matmul(self.X.t(), (labels - self.outputs))
        self.grads['b'] = -1 / N * torch.sum(labels - self.outputs)

(2) 参数更新

在计算参数的梯度之后,我们按照下面公式更新参数: w ← w − α ∂ R ( w , b ) ∂ w \mathbf w\leftarrow \mathbf w - \alpha \frac{\partial \cal R(\mathbf w,b)}{\partial \mathbf w} wwαwR(w,b) b ← b − α ∂ R ( w , b ) ∂ b b\leftarrow b - \alpha \frac{\partial \cal R(\mathbf w,b)}{\partial b} bbαbR(w,b)其中 α \alpha α为学习率。[1]

将上面的参数更新过程包装为优化器,首先定义一个优化器基类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]

5. 评价指标

在分类任务中,通常使用准确率(Accuracy)作为评价指标。如果模型预测的类别与真实类别一致,则说明模型预测正确。准确率即正确预测的数量与总的预测数量的比值: A = 1 N ∑ n = 1 N I ( y ( n ) = y ^ ( n ) ) \mathcal{A} = \frac{1}{N} \sum_{n=1}^N I (y^{(n)} = \hat{y}^{(n)}) A=N1n=1NI(y(n)=y^(n))其中 I ( ⋅ ) I(·) I()是指示函数。[1]

代码实现如下:

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
        # 使用'torch.cast'将preds的数据类型转换为float32类型
        preds = (preds >= 0.5).type(torch.float32)
    else:
        # 多分类时,使用'torch.argmax'计算最大元素索引作为类别
        preds = torch.argmax(preds, dim=1).to(torch.int64)
    return torch.mean(torch.eq(preds, labels).type(torch.float32))

代码执行结果:

accuracy is: tensor(0.5000)

6. 完善Runner类

基于RunnerV1,本章的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
        # 记录训练过程中的评价指标变化情况
        self.train_scores = []
        self.dev_scores = []
        # 记录训练过程中的损失函数变化情况
        self.train_loss = []
        self.dev_loss = []

    def train(self, train_set, dev_set, **kwargs):
        # 传入训练轮数,如果没有传入值则默认为0
        num_epochs = kwargs.get("num_epochs", 0)
        # 传入log打印频率,如果没有传入值则默认为100
        log_epochs = kwargs.get("log_epochs", 100)
        # 传入模型保存路径,如果没有传入值则默认为"best_model.pdparams"
        save_path = kwargs.get("save_path", "best_model.pdparams")
        # 梯度打印函数,如果没有传入则默认为"None"
        print_grads = kwargs.get("print_grads", None)
        # 记录全局最优指标
        best_score = 0
        # 进行num_epochs轮训练
        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 accuracy performence has been updated: {best_score:.5f} --> {dev_score:.5f}")
                best_score = dev_score
            if epoch % log_epochs == 0:
                print(f"[Train] epoch: {epoch}, loss: {trn_loss}, score: {trn_score}")
                print(f"[Dev] epoch: {epoch}, loss: {dev_loss}, score: {dev_score}")

    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):
        torch.save(self.model.params, save_path)

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

7. 模型训练

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

# 固定随机种子,保持每次运行结果一致
torch.manual_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")

代码执行结果:

best accuracy performence has been updated: 0.00000 --> 0.75000
[Train] epoch: 0, loss: 0.6931460499763489, score: 0.5
[Dev] epoch: 0, loss: 0.6844645738601685, score: 0.75
[Train] epoch: 50, loss: 0.4831995964050293, score: 0.807812511920929
[Dev] epoch: 50, loss: 0.5199085474014282, score: 0.75
[Train] epoch: 100, loss: 0.43985626101493835, score: 0.8140624761581421
[Dev] epoch: 100, loss: 0.4893949627876282, score: 0.75
best accuracy performence has been updated: 0.75000 --> 0.75625
[Train] epoch: 150, loss: 0.4231750965118408, score: 0.817187488079071
[Dev] epoch: 150, loss: 0.4799765646457672, score: 0.7562500238418579
best accuracy performence has been updated: 0.75625 --> 0.76250
[Train] epoch: 200, loss: 0.41500502824783325, score: 0.823437511920929
[Dev] epoch: 200, loss: 0.47652292251586914, score: 0.762499988079071
[Train] epoch: 250, loss: 0.4104517996311188, score: 0.8203125
[Dev] epoch: 250, loss: 0.47522956132888794, score: 0.7437499761581421
[Train] epoch: 300, loss: 0.407705694437027, score: 0.8218749761581421
[Dev] epoch: 300, loss: 0.4748341143131256, score: 0.75
[Train] epoch: 350, loss: 0.4059615135192871, score: 0.823437511920929
[Dev] epoch: 350, loss: 0.4748414158821106, score: 0.7562500238418579
[Train] epoch: 400, loss: 0.40481358766555786, score: 0.8265625238418579
[Dev] epoch: 400, loss: 0.47503310441970825, score: 0.75
[Train] epoch: 450, loss: 0.40403881669044495, score: 0.828125
[Dev] epoch: 450, loss: 0.4753051698207855, score: 0.75

代码实现可视化观察训练集与验证集的准确率损失的变化情况

# 可视化观察训练集与验证集的指标变化情况
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='#e4007f', label="Train loss")
    # 绘制评价损失变化曲线
    plt.plot(epochs, runner.dev_loss, color='#f19ec2', linestyle='--', label="Dev loss")
    # 绘制坐标轴和图例
    plt.ylabel("loss", fontsize='large')
    plt.xlabel("epoch", fontsize='large')
    plt.legend(loc='upper right', fontsize='x-large')
    plt.subplot(1, 2, 2)
    # 绘制训练准确率变化曲线
    plt.plot(epochs, runner.train_scores, color='#e4007f', label="Train accuracy")
    # 绘制评价准确率变化曲线
    plt.plot(epochs, runner.dev_scores, color='#f19ec2', linestyle='--', label="Dev accuracy")
    # 绘制坐标轴和图例
    plt.ylabel("score", fontsize='large')
    plt.xlabel("epoch", fontsize='large')
    plt.legend(loc='lower right', fontsize='x-large')
    plt.tight_layout()
    plt.savefig(fig_name)
    plt.show()

plot(runner, fig_name='linear-acc.pdf')

代码执行结果如下图所示:

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


8. 模型评价

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

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

代码执行结果:

[Test] score/loss: 0.8100/0.4706

可视化观察拟合的决策边界 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 = torch.linspace(-2, 3, 1000)
x2 = decision_boundary(w, b, x1)
# 绘制决策边界
plt.plot(x1.tolist(), x2.tolist(), color="red")
plt.show()

代码执行结果如下图所示:


二、基于Softmax回归多分类任务

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

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


1. 数据集构建

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

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

import numpy as np
import torch

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 = torch.zeros([n_samples, n_features])
    y = torch.zeros([n_samples], dtype=torch.int32)
    # 随机生成3个簇中心作为类别中心
    centroids = torch.randperm(2 ** n_features)[:n_classes]
    centroids_bin = np.unpackbits(centroids.numpy().astype('uint8')).reshape((-1, 8))[:, -n_features:]
    centroids = torch.tensor(centroids_bin, dtype=torch.float32)
    # 控制簇中心的分离程度
    centroids = 1.5 * centroids - 1
    # 随机生成特征值
    X[:, :n_features] = torch.randn(size=[n_samples, n_features])

    stop = 0
    # 将每个类的特征值控制在簇中心附近
    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 * torch.rand(size=[n_features, n_features]) - 1
        X_k[...] = torch.matmul(X_k, A)
        X_k += centroid
        X[start:stop, :n_features] = X_k

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

    return X, y

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

import matplotlib.pyplot as plt

# 固定随机种子,保持每次运行结果一致
torch.manual_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()

代码执行结果如下图所示:

将实验数据拆分成训练集、验证集和测试集。其中训练集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:  torch.Size([640, 2]) y_train shape:  torch.Size([640])

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

# 打印前5个数据的标签
print(y_train[:5])

代码执行结果:

tensor([0, 2, 0, 0, 1], dtype=torch.int32)

2. 模型构建

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

(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 ) \mathrm{softmax}(x_k) = \frac{\exp(x_k)}{\sum_{i=1}^K \exp(x_i)} softmax(xk)=i=1Kexp(xi)exp(xk)在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的项,从而也避免了下溢出的问题。[1]

Softmax函数的代码实现如下:

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

# 观察softmax的计算方式
X = torch.tensor([[0.1, 0.2, 0.3, 0.4], [1, 2, 3, 4]])
predict = softmax(X)
print(predict)

代码执行结果:

tensor([[0.2138, 0.2363, 0.2612, 0.2887],
        [0.0321, 0.0871, 0.2369, 0.6439]])

(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 ) \begin{aligned} p(y=c|\mathbf x) = \mathrm{softmax}(\mathbf w_c^T \mathbf x+b_c) \end{aligned} p(y=cx)=softmax(wcTx+bc)其中 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为所有类别的预测条件概率组成的矩阵。[1]

我们根据上述关于 Y ^ \hat{\mathbf Y} Y^的公式实现Softmax回归算子,代码实现如下:

import op

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

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

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

# 随机生成1条长度为4的数据
inputs = torch.randn(size=[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([[-0.6014, -1.0122, -0.3023, -1.2277]])
Output is: tensor([[0.3333, 0.3333, 0.3333]])

从输出结果可以看出,采用全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) 随堂小问题

Logistic函数是激活函数。Softmax函数是激活函数么?谈谈你的看法。

Softmax函数一般要与交叉熵损失函数一起使用,一般在进行网络构造时也不会使用Softmax函数。所以,就独立性和应用性而言,Softmax函数不算是激活函数。


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 \mathbf y = [I(1=c),I(2=c),…,I(C=c)]^T y=[I(1=c),I(2=c),,I(C=c)]T其中 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 ) \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)} R(W,b)=N1n=1N(y(n))Tlogy^(n)=N1n=1Nc=1Cyc(n)logy^c(n)观察上式, 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 ) \cal R(\mathbf W,\mathbf b) = -\frac{1}{N}\sum_{n=1}^N \log [\hat{\mathbf y}^{(n)}]_{y^{(n)}} R(W,b)=N1n=1Nlog[y^(n)]y(n)其中 y ( n ) y^{(n)} y(n)是第 n n n个样本的标签。[1]

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

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 -= torch.log(self.predicts[i][index])
        return loss / self.num

# 测试一下
# 假设真实标签为第1类
labels = torch.tensor([0])
# 计算风险函数
mce_loss = MultiCrossEntropyLoss()
print(mce_loss(outputs, labels))

代码执行结果:

tensor(1.0986)

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 ^ ) \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}) WR(W,b)=N1n=1Nx(n)(y(n)y^(n))T=N1XT(yy^) ∂ R ( W , b ) ∂ b = − 1 N ∑ n = 1 N ( y ( n ) − y ^ ( n ) ) T = − 1 N 1 ( y − y ^ ) \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}) bR(W,b)=N1n=1N(y(n)y^(n))T=N11(yy^)其中 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向量。[1]

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

class model_SR(op.Op):
    def __init__(self, input_dim, output_dim):
        super(model_SR, self).__init__()
        self.params = {}
        # 将线性层的权重参数全部初始化为0
        self.params['W'] = torch.zeros(size=[input_dim, output_dim])
        # self.params['W'] = torch.normal(mean=0, std=0.01, size=[input_dim, output_dim])
        # 将线性层的偏置参数初始化为0
        self.params['b'] = torch.zeros(size=[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 = torch.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 = torch.nn.functional.one_hot(labels.to(torch.int64), self.output_dim)
        self.grads['W'] = -1 / N * torch.matmul(self.X.t(), (labels-self.outputs))
        self.grads['b'] = -1 / N * torch.matmul(torch.ones(size=[N]), (labels-self.outputs))

(2) 参数更新

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

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

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]

5. 模型训练

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

# 固定随机种子,保持每次运行结果一致
torch.manual_seed(102)
 
# 特征维度
input_dim = 2
# 类别数
output_dim = 3
# 学习率
lr = 0.1
 
# 实例化模型
model = model_SR(input_dim=input_dim, output_dim=output_dim)
# 指定优化器
optimizer = SimpleBatchGD(init_lr=lr, model=model)
# 指定损失函数
loss_fn = MultiCrossEntropyLoss()
# 指定评价方式
metric = accuracy
# 实例化RunnerV2类
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')

代码执行结果:

best accuracy performence has been updated: 0.00000 --> 0.70625
[Train] epoch: 0, loss: 1.0986149311065674, score: 0.3218750059604645
[Dev] epoch: 0, loss: 1.0805636644363403, score: 0.706250011920929
best accuracy performence has been updated: 0.70625 --> 0.71250
best accuracy performence has been updated: 0.71250 --> 0.71875
best accuracy performence has been updated: 0.71875 --> 0.72500
best accuracy performence has been updated: 0.72500 --> 0.73125
best accuracy performence has been updated: 0.73125 --> 0.73750
best accuracy performence has been updated: 0.73750 --> 0.74375
best accuracy performence has been updated: 0.74375 --> 0.75000
best accuracy performence has been updated: 0.75000 --> 0.75625
best accuracy performence has been updated: 0.75625 --> 0.76875
best accuracy performence has been updated: 0.76875 --> 0.77500
best accuracy performence has been updated: 0.77500 --> 0.78750
[Train] epoch: 100, loss: 0.7155234813690186, score: 0.768750011920929
[Dev] epoch: 100, loss: 0.7977758049964905, score: 0.7875000238418579
best accuracy performence has been updated: 0.78750 --> 0.79375
best accuracy performence has been updated: 0.79375 --> 0.80000
[Train] epoch: 200, loss: 0.6921819448471069, score: 0.784375011920929
[Dev] epoch: 200, loss: 0.8020225763320923, score: 0.793749988079071
best accuracy performence has been updated: 0.80000 --> 0.80625
[Train] epoch: 300, loss: 0.6840381026268005, score: 0.7906249761581421
[Dev] epoch: 300, loss: 0.81141597032547, score: 0.8062499761581421
best accuracy performence has been updated: 0.80625 --> 0.81250
[Train] epoch: 400, loss: 0.680213987827301, score: 0.807812511920929
[Dev] epoch: 400, loss: 0.8198073506355286, score: 0.8062499761581421

执行代码后得到下图:


6. 模型评价

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

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

代码执行结果:

[Test] score/loss: 0.8400/0.7014

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

# 均匀生成40000个数据点
x1, x2 = torch.meshgrid(torch.linspace(-3.5, 2, 200), torch.linspace(-4.5, 3.5, 200))
x = torch.stack([torch.flatten(x1), torch.flatten(x2)], axis=1)
# 预测对应类别
y = runner.predict(x)
y = torch.argmax(y, axis=1)
# 绘制类别区域
plt.xlabel('x1')
plt.ylabel('x2')
plt.scatter(x[:,0].tolist(), x[:,1].tolist(), c=y.tolist(), cmap=plt.cm.Spectral)

torch.manual_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())
plt.show()

代码执行结果如下图所示:


三、实践:基于Softmax回归完成鸢尾花分类任务

在本节,我们用入门深度学习的基础实验之一“鸢尾花分类任务”来进行实践,使用经典学术数据集Iris作为训练数据,实现基于Softmax回归的鸢尾花分类任务。

实践流程主要包括以下7个步骤:数据处理、模型构建、损失函数定义、优化器构建、模型训练、模型评价和模型预测等。

  • 数据处理:根据网络接收的数据格式,完成相应的预处理操作,保证模型正常读取;
  • 模型构建:定义Softmax回归模型类;
  • 训练配置:训练相关的一些配置,如:优化算法、评价指标等;
  • 组装Runner类:Runner用于管理模型训练和测试过程;
  • 模型训练和测试:利用Runner进行模型训练、评价和测试。

本实践的主要配置如下:

  • 数据:Iris数据集;
  • 模型:Softmax回归模型;
  • 损失函数:交叉熵损失;
  • 优化器:梯度下降法;
  • 评价指标:准确率。

1. 数据处理

(1) 数据集介绍

Iris数据集,也称为鸢尾花数据集,包含了3种鸢尾花类别(Setosa、Versicolour、Virginica),每种类别有50个样本,共计150个样本。其中每个样本中包含了4个属性:花萼长度、花萼宽度、花瓣长度以及花瓣宽度,本实验通过鸢尾花这4个属性来判断该样本的类别。

鸢尾花属性

|属性1 |属性2 |属性3 |属性4 | |:–: |:–: |:–: |:–: | |sepal_length |sepal_width|petal_length|petal_width| |花萼长度 |花萼宽度|花瓣长度|花瓣宽度|

鸢尾花类别

|英文名 |中文名 |标签 | |:–: |:–: |:–: | |Setosa Iris |狗尾草鸢尾 | 0 |Versicolour Iris |杂色鸢尾 | 1 |Virginica Iris |弗吉尼亚鸢尾 | 2

鸢尾花属性类别对应预览

|sepal_length |sepal_width|petal_length|petal_width| species |:–: |:–: |:–: |:–: |:–: | |5.1 |3.5 |1.4 |0.2 |setosa |4.9 |3 |1.4 |0.2 |setosa |4.7 |3.2 |1.3 |0.2 |setosa |… |… |… |… |… |
[2]

(2) 数据清洗

缺失值分析

对数据集中的缺失值或异常值等情况进行分析和处理,保证数据可以被模型正常读取。代码实现如下:

from sklearn.datasets import load_iris
import pandas
import numpy as np

iris_features = np.array(load_iris().data, dtype=np.float32)
iris_labels = np.array(load_iris().target, dtype=np.int32)
print(pandas.isna(iris_features).sum())
print(pandas.isna(iris_labels).sum())

代码执行结果:

0
0

从输出结果看,鸢尾花数据集中不存在缺失值的情况。

异常值处理

通过箱线图直观的显示数据分布,并观测数据中的异常值。

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

# 箱线图查看异常值分布
def boxplot(features):
    feature_names = ['sepal_length', 'sepal_width', 'petal_length', 'petal_width']

    # 连续画几个图片
    plt.figure(figsize=(5, 5), dpi=200)
    # 子图调整
    plt.subplots_adjust(wspace=0.6)
    # 每个特征画一个箱线图
    for i in range(4):
        plt.subplot(2, 2, i + 1)
        # 画箱线图
        plt.boxplot(features[:, i],
                    showmeans=True,
                    whiskerprops={"color": "#E20079", "linewidth": 0.4, 'linestyle': "--"},
                    flierprops={"markersize": 0.4},
                    meanprops={"markersize": 1})
        # 图名
        plt.title(feature_names[i], fontdict={"size": 5}, pad=2)
        # y方向刻度
        plt.yticks(fontsize=4, rotation=90)
        plt.tick_params(pad=0.5)
        # x方向刻度
        plt.xticks([])
    plt.savefig('ml-vis.pdf')
    plt.show()

boxplot(iris_features)

代码执行结果如下图所示:

从输出结果看,数据中基本不存在异常值,所以不需要进行异常值处理。

(3) 数据读取

本实验中将数据集划分为了三个部分:

  • 训练集:用于确定模型参数;
  • 验证集:与训练集独立的样本集合,用于使用提前停止策略选择最优模型;
  • 测试集:用于估计应用效果(没有在模型中应用过的数据,更贴近模型在真实场景应用的效果)。

在本实验中,将80%的数据用于模型训练,10%的数据用于模型验证,10%的数据用于模型测试。代码实现如下:
代码执行结果:

import torch

# 加载数据集
def load_data(shuffle=True):
    """
    加载鸢尾花数据
    输入:
        - shuffle:是否打乱数据,数据类型为bool
    输出:
        - X:特征数据,shape=[150,4]
        - y:标签数据, shape=[150]
    """
    # 加载原始数据
    X = np.array(load_iris().data, dtype=np.float32)
    y = np.array(load_iris().target, dtype=np.int32)

    X = torch.tensor(X)
    y = torch.tensor(y)

    # 数据归一化
    X_min = torch.min(X, axis=0)[0]
    X_max = torch.max(X, axis=0)[0]
    X = (X - X_min) / (X_max - X_min)

    # 如果shuffle为True,随机打乱数据
    if shuffle:
        idx = torch.randperm(X.shape[0])
        X = X[idx]
        y = y[idx]
    return X, y

# 固定随机种子
torch.manual_seed(102)

num_train = 120
num_dev = 15
num_test = 15

X, y = load_data(shuffle=True)
print("X shape: ", X.shape, "y shape: ", y.shape)
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 shape:  torch.Size([150, 4]) y shape:  torch.Size([150])
# 打印X_train和y_train的维度
print("X_train shape: ", X_train.shape, "y_train shape: ", y_train.shape)

# 打印前5个数据的标签
print(y_train[:5])

代码执行结果:

X_train shape:  torch.Size([120, 4]) y_train shape:  torch.Size([120])
tensor([1, 2, 0, 1, 2], dtype=torch.int32)

2. 模型构建

使用Softmax回归模型进行鸢尾花分类实验,将模型的输入维度定义为4,输出维度定义为3。代码实现如下:

# 输入维度
input_dim = 4
# 类别数
output_dim = 3
# 实例化模型
model = model_SR(input_dim=input_dim, output_dim=output_dim)

3. 模型训练

实例化RunnerV2类,使用训练集和验证集进行模型训练,共训练80个epoch,其中每隔10个epoch打印训练集上的指标,并且保存准确率最高的模型作为最佳模型。代码实现如下:

# 学习率
lr = 0.2

# 梯度下降法
optimizer = SimpleBatchGD(init_lr=lr, model=model)
# 交叉熵损失
loss_fn = MultiCrossEntropyLoss()
# 准确率
metric = Logistic_op.accuracy

# 实例化RunnerV2
runner = Logistic_op.RunnerV2(model, optimizer, metric, loss_fn)

# 启动训练
runner.train([X_train, y_train], [X_dev, y_dev], num_epochs=200, log_epochs=10, save_path="best_model.pdparams")

代码执行结果:

best accuracy performence has been updated: 0.00000 --> 0.46667
[Train] epoch: 0, loss: 1.09861159324646, score: 0.375
[Dev] epoch: 0, loss: 1.089357614517212, score: 0.46666666865348816
[Train] epoch: 10, loss: 0.9777260422706604, score: 0.699999988079071
[Dev] epoch: 10, loss: 1.023618221282959, score: 0.46666666865348816
[Train] epoch: 20, loss: 0.8894370794296265, score: 0.699999988079071
[Dev] epoch: 20, loss: 0.9739664793014526, score: 0.46666666865348816
[Train] epoch: 30, loss: 0.8196598887443542, score: 0.699999988079071
[Dev] epoch: 30, loss: 0.9317176938056946, score: 0.46666666865348816
[Train] epoch: 40, loss: 0.7635203003883362, score: 0.699999988079071
[Dev] epoch: 40, loss: 0.8957117199897766, score: 0.46666666865348816
[Train] epoch: 50, loss: 0.7176517248153687, score: 0.7250000238418579
[Dev] epoch: 50, loss: 0.8649960160255432, score: 0.46666666865348816
[Train] epoch: 60, loss: 0.679577648639679, score: 0.7416666746139526
[Dev] epoch: 60, loss: 0.8386644721031189, score: 0.46666666865348816
[Train] epoch: 70, loss: 0.6474865078926086, score: 0.7583333253860474
[Dev] epoch: 70, loss: 0.8159360289573669, score: 0.46666666865348816
[Train] epoch: 80, loss: 0.6200525760650635, score: 0.7666666507720947
[Dev] epoch: 80, loss: 0.7961668372154236, score: 0.46666666865348816
[Train] epoch: 90, loss: 0.5962967276573181, score: 0.7833333611488342
[Dev] epoch: 90, loss: 0.7788369655609131, score: 0.46666666865348816
[Train] epoch: 100, loss: 0.5754876732826233, score: 0.8166666626930237
[Dev] epoch: 100, loss: 0.7635290026664734, score: 0.46666666865348816
best accuracy performence has been updated: 0.46667 --> 0.53333
[Train] epoch: 110, loss: 0.5570722818374634, score: 0.824999988079071
[Dev] epoch: 110, loss: 0.7499087452888489, score: 0.5333333611488342
best accuracy performence has been updated: 0.53333 --> 0.60000
[Train] epoch: 120, loss: 0.5406263470649719, score: 0.824999988079071
[Dev] epoch: 120, loss: 0.7377070188522339, score: 0.6000000238418579
[Train] epoch: 130, loss: 0.525819718837738, score: 0.8500000238418579
[Dev] epoch: 130, loss: 0.726706862449646, score: 0.6000000238418579
[Train] epoch: 140, loss: 0.5123931169509888, score: 0.8583333492279053
[Dev] epoch: 140, loss: 0.7167317271232605, score: 0.6000000238418579
[Train] epoch: 150, loss: 0.5001395344734192, score: 0.875
[Dev] epoch: 150, loss: 0.7076371312141418, score: 0.6000000238418579
best accuracy performence has been updated: 0.60000 --> 0.66667
[Train] epoch: 160, loss: 0.48889240622520447, score: 0.875
[Dev] epoch: 160, loss: 0.6993042826652527, score: 0.6666666865348816
[Train] epoch: 170, loss: 0.4785163998603821, score: 0.875
[Dev] epoch: 170, loss: 0.6916342973709106, score: 0.6666666865348816
[Train] epoch: 180, loss: 0.46889930963516235, score: 0.875
[Dev] epoch: 180, loss: 0.6845448017120361, score: 0.6000000238418579
[Train] epoch: 190, loss: 0.45994898676872253, score: 0.875
[Dev] epoch: 190, loss: 0.6779664158821106, score: 0.6000000238418579

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

plot(runner,fig_name='linear-acc3.pdf')

代码执行结果如下图所示:


4. 模型评价

使用测试数据对在训练过程中保存的最佳模型进行评价,观察模型在测试集上的准确率情况。代码实现如下:

# 加载最优模型
runner.load_model('best_model.pdparams')
# 模型评价
score, loss = runner.evaluate([X_test, y_test])
print("[Test] score/loss: {:.4f}/{:.4f}".format(score, loss))

代码执行结果:

[Test] score/loss: 0.7333/0.5928

5. 模型预测

使用保存好的模型,对测试集中的数据进行模型预测,并取出1条数据观察模型效果。代码实现如下:

# 预测测试集数据
logits = runner.predict(X_test)
# 观察其中一条样本的预测结果
pred = torch.argmax(logits[0]).numpy()
# 获取该样本概率最大的类别
label = y_test[0].numpy()
# 输出真实类别与预测类别
print("The true category is {} and the predicted category is {}".format(label, pred))

代码执行结果:

The true category is 2 and the predicted category is 2

6. 给模型和数据集上强度(确信)

(1) 调整参数

调整学习率训练轮数等超参数,观察是否能够得到更高的精度。

调整学习率

# 学习率
lr = 0.5
# lr = 0.01

代码执行结果:

[Test] score/loss: 0.8667/0.4477
[Test] score/loss: 0.6000/1.0979

学习率为0.5时score上升,loss下降
学习率为0.01时收敛慢,还会过拟合使得loss值升高

调整训练轮数

runner.train([X_train, y_train], [X_dev, y_dev], num_epochs=4000, log_epochs=10, save_path="best_model.pdparams")
# runner.train([X_train, y_train], [X_dev, y_dev], num_epochs=8000, log_epochs=10, save_path="best_model.pdparams")

代码执行结果:

[Test] score/loss: 0.9333/0.2399
[Test] score/loss: 0.9333/0.2399

和学习率相似,epoch为4000时score上升,loss下降
但是epoch为8000时相较于epoch为4000时没有改变,说明没有发生过拟合。

(2) 用其他模型进行识别任务

使用的当然是风靡一时的SVM

代码实现如下:

from math import exp # 数学
from random import shuffle # 随机
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.font_manager import FontProperties

def load_data(filename):
    data_row = []
    with open(filename, 'r') as f:
        for line in f.readlines():
            line = line.split()
            current_line = []
            for i in range(len(line)):
                if i != len(line) - 1:
                    current_line.append(float(line[i]))
                else:
                    if line[i] == '1':
                        current_line.append([1, 1])
                    elif line[i] == '2':
                        current_line.append([-1, 1])
                    else:
                        current_line.append([-1, -1])
            data_row.append(current_line)
    data_colomn = []
    for i in range(len(data_row[0])):
        line = [data_row[j][i] for j in range(len(data_row))]
        data_colomn.append(line)
    return data_colomn
data = load_data('Iris.txt')

def W(zhichi, xy, a):  # 计算更新 w
    w = [0, 0]
    if len(zhichi) == 0:  # 初始化的0
        return w
    for i in zhichi:
        w[0] += a[i] * xy[0][i] * xy[2][i]  # 更新w
        w[1] += a[i] * xy[1][i] * xy[2][i]
    return w
def B(zhichi, xy, a):  # 计算更新 b
    b = 0
    if len(zhichi) == 0:  # 初始化的0
        return 0
    for s in zhichi:  # 对任意的支持向量有 ysf(xs)=1 所有支持向量求解平均值
        sum = 0
        for i in zhichi:
            sum += a[i] * xy[2][i] * (xy[0][i] * xy[0][s] + xy[1][i] * xy[1][s])
        b += 1 / xy[2][s] - sum
    return b / len(zhichi)
def SMO(xy, m):
    a = [0.0] * len(xy[0])  # 拉格朗日乘子
    zhichi = set()  # 支持向量下标
    loop = 1  # 循环标记(符合KKT)
    w = [0, 0]  # 初始化 w
    b = 0  # 初始化 b
    while loop:
        loop += 1
        if loop == 150:
            print("达到早停标准")
            print("循环了", loop, "次")
            loop = 0
            break
        # 初始化=========================================
        fx = []  # 储存所有的fx
        yfx = []  # 储存所有yfx-1的值
        Ek = []  # Ek,记录fx-y用于启发式搜索
        E_ = -1  # 贮存最大偏差,减少计算
        a1 = 0  # SMO  a1
        a2 = 0  # SMO  a2
        # 初始化结束======================================
        # 寻找a1,a2======================================
        for i in range(len(xy[0])):  # 计算所有的 fx yfx-1 Ek
            fx.append(w[0] * xy[0][i] + w[1] * xy[1][i] + b)  # 计算 fx=wx+b
            yfx.append(xy[2][i] * fx[i] - 1)  # 计算 yfx-1
            Ek.append(fx[i] - xy[2][i])  # 计算 fx-y
            if i in zhichi:  # 之前看过的不看了,防止重复找某个a
                continue
            if yfx[i] <= yfx[a1]:
                a1 = i  # 得到偏离最大位置的下标(数值最小的)
        if yfx[a1] >= 0:  # 最小的也满足KKT
            print("一共循环", loop, "次")
            loop = 0  # 循环标记(符合KKT)置零(没有用到)
            break
        for i in range(len(xy[0])):  # 遍历找间隔最大的a2
            if i == a1:  # 如果是a1,跳过
                continue
            Ei = abs(Ek[i] - Ek[a1])  # |Eki-Eka1|
            if Ei < E_:  # 找偏差
                E_ = Ei  # 储存偏差的值
                a2 = i  # 储存偏差的下标
        # 寻找a1,a2结束===================================
        zhichi.add(a1)  # a1录入支持向量
        zhichi.add(a2)  # a2录入支持向量
        # 分析约束条件=====================================
        # c=a1*y1+a2*y2
        c = a[a1] * xy[2][a1] + a[a2] * xy[2][a2]  # 求出c
        # n=K11+k22-2*k12
        if m == 1:  # 线性核
            n = xy[0][a1] ** 2 + xy[1][a1] ** 2 + xy[0][a2] ** 2 + xy[1][a2] ** 2 - 2 * (
                    xy[0][a1] * xy[0][a2] + xy[1][a1] * xy[1][a2])
        elif m == 2:  # 多项式核(这里是二次)
            n = (xy[0][a1] ** 2 + xy[1][a1] ** 2) ** 2 + (xy[0][a2] ** 2 + xy[1][a2] ** 2) ** 2 - 2 * (
                    xy[0][a1] * xy[0][a2] + xy[1][a1] * xy[1][a2]) ** 2
        elif m == 3:  # 高斯核  取 2σ^2 = 1
            n = 2 * exp(-1) - 2 * exp(-((xy[0][a1] - xy[0][a2]) ** 2 + (xy[1][a1] - xy[1][a2]) ** 2))
        # 确定a1的可行域=====================================
        if xy[2][a1] == xy[2][a2]:
            L = max(0.0, a[a1] + a[a2] - 0.5)  # 下界
            H = min(0.5, a[a1] + a[a2])  # 上界
        else:
            L = max(0.0, a[a1] - a[a2])  # 下界
            H = min(0.5, 0.5 + a[a1] - a[a2])  # 上界
        if n > 0:
            a1_New = a[a1] - xy[2][a1] * (Ek[a1] - Ek[a2]) / n  # a1_New = a1_old-y1(e1-e2)/n
            # print("x1=",xy[0][a1],"y1=",xy[1][a1],"z1=",xy[2][a1],"x2=",xy[0][a2],"y2=",xy[1][a2],"z2=",xy[2][a2],"a1_New=",a1_New)
            # 越界裁剪============================================================
            if a1_New >= H:
                a1_New = H
            elif a1_New <= L:
                a1_New = L
        else:
            a1_New = min(H, L)
        # 参数更新=======================================
        a[a2] = a[a2] + xy[2][a1] * xy[2][a2] * (a[a1] - a1_New)  # a2更新
        a[a1] = a1_New  # a1更新
        w = W(zhichi, xy, a)  # 更新w
        b = B(zhichi, xy, a)  # 更新b
        # print("W=", w, "b=", b, "zhichi=", zhichi, "a1=", a[a1], "a2=", a[a2])
    # 标记支持向量======================================
    for i in zhichi:
        if a[i] == 0:  # 选了,但值仍为0
            loop = loop + 1
            e = 'silver'
        else:
            if xy[2][i] == 1:
                e = 'b'
            else:
                e = 'r'
        plt.scatter(x1[0][i], x1[1][i], c='none', s=100, linewidths=1, edgecolor=e)
    print("支持向量个数:", len(zhichi), "\na为零的支持向量个数:", loop)
    print("有效向量个数:", len(zhichi) - loop)
    # 返回数据 w b =======================================
    return [w, b]
def Def(xyz, w_b1, w_b2):
    c = 0
    for i in range(len(xyz[0])):
        if (xyz[0][i] * w_b1[0][0] + xyz[1][i] * w_b1[0][1] + w_b1[1]) * xyz[2][i][0] < 0:
            c = c + 1
            continue
        if (xyz[0][i] * w_b2[0][0] + xyz[1][i] * w_b2[0][1] + w_b2[1]) * xyz[2][i][1] < 0:
            c = c + 1
            continue
    return (1 - c / len(xyz[0])) * 100

# 选择数据===================================================
Attribute1 = eval(input("选取第一个属性(输入0~4的任意整数):"))
Attribute2 = eval(input("选取第二个属性(输入0~4的任意整数):"))
# 生成数据集==================================================
lt = list(range(150))  # 得到一个顺序序列
shuffle(lt)  # 打乱序列
x1 = [[], [], []]  # 初始化x1
x2 = [[], [], []]  # 初始化x2
for i in lt[0:100]:  # 截取部分做训练集
    x1[0].append(data[Attribute1][i])  # 加上数据集x属性
    x1[1].append(data[Attribute2][i])  # 加上数据集y属性
    x1[2].append(data[4][i])  # 加上数据集c标签
for i in lt[100:150]:  # 截取部分做测试集
    x2[0].append(data[Attribute1][i])  # 加上数据集x属性
    x2[1].append(data[Attribute2][i])  # 加上数据集y属性
    x2[2].append(data[4][i])  # 加上数据集c标签

print('\n训练开始')
def Plot(x1, x2, wb1, wb2, m):
    x = [x1[0][:], x1[1][:], x1[2][:]]
    for i in range(len(x[2])):  # 对训练集‘上色’
        if x[2][i] == [1, 1]:
            x[2][i] = 'r'  # 训练集  1  1 红色
        elif x[2][i] == [-1, 1]:
            x[2][i] = 'g'  # 训练集 -1  1 绿色
        else:
            x[2][i] = 'b'  # 训练集 -1 -1 蓝色
    plt.scatter(x[0], x[1], c=x[2], alpha=0.8)  # 绘点训练集
    x = [x2[0][:], x2[1][:], x2[2][:]]
    for i in range(len(x[2])):  # 对测试集‘上色’
        if x[2][i] == [1, 1]:
            x[2][i] = 'orange'  # 训练集  1   1 橙色
        elif x[2][i] == [-1, 1]:
            x[2][i] = 'y'  # 训练集 -1   1 黄色
        else:
            x[2][i] = 'm'  # 训练集 -1  -1 紫色
    plt.scatter(x[0], x[1], c=x[2], alpha=0.8)  # 绘点测试集
    plt.xlabel('x')  # x轴标签
    plt.ylabel('y')  # y轴标签
    font = FontProperties(fname=r"C:\windows\fonts\simsun.ttc", size=16)
    if m == 1:
        plt.title('线性核', fontproperties=font)  # 标题
    elif m == 2:
        plt.title('多项式核', fontproperties=font)
    elif m == 3:
        plt.title('高斯核', fontproperties=font)
    xl = np.arange(min(x[0]), max(x[0]), 0.1)  # 绘制分类线一
    yl = (-wb1[0][0] * xl - wb1[1]) / wb1[0][1]
    plt.plot(xl, yl, 'r')
    xl = np.arange(min(x[0]), max(x[0]), 0.1)  # 绘制分类线二
    yl = (-wb2[0][0] * xl - wb2[1]) / wb2[0][1]
    plt.plot(xl, yl, 'b')
for m in range(1,4):
    if m == 1:
        print('\n使用线性核训练')  # 标题
    elif m == 2:
        print('\n使用多项式核训练')
    elif m == 3:
        print('\n使用高斯核训练')
    # 计算 w b============================================
    plt.figure(m)  # 第m张画布
    x = [x1[0][:], x1[1][:], []]  # 第一次分类
    for i in x1[2]:
        x[2].append(i[0])  # 加上数据集标签
    wb1 = SMO(x, m)
    x = [x1[0][:], x1[1][:], []]  # 第二次分类
    for i in x1[2]:
        x[2].append(i[1])  # 加上数据集标签
    wb2 = SMO(x, m)
    print("w1 =", wb1[0], "\nb1 =", wb1[1])
    print("w2 =", wb2[0], "\nb2 =", wb2[1])
    # 计算正确率===========================================
    print("训练集上的正确率为:", Def(x1, wb1, wb2), "%")
    print("测试集上的正确率为:", Def(x2, wb1, wb2), "%")
    # 绘图 ===============================================
    # 圈着的是曾经选中的值,灰色的是选中但更新为0
    Plot(x1, x2, wb1, wb2, m)# 显示所有图
plt.show()

代码执行结果:

选取第一个属性(输入0~4的任意整数):

输入1

选取第二个属性(输入0~4的任意整数):

输入3

训练开始

使用线性核训练
一共循环 29 次
支持向量个数: 28 
a为零的支持向量个数: 17
有效向量个数: 11
一共循环 65 次
支持向量个数: 64 
a为零的支持向量个数: 40
有效向量个数: 24
w1 = [0.6435118046798376, -1.561166670558747] 
b1 = -0.7437199069208986
w2 = [0.010546446427642797, -1.0720509834243026] 
b2 = 1.8074665437926425
训练集上的正确率为: 97.0 %
测试集上的正确率为: 94.0 %

使用多项式核训练
达到早停标准
循环了 150 次
支持向量个数: 67 
a为零的支持向量个数: 5
有效向量个数: 62
达到早停标准
循环了 150 次
支持向量个数: 86 
a为零的支持向量个数: 22
有效向量个数: 64
w1 = [-1.086168298395296, -1.1564902479126118] 
b1 = 4.365290406790221
w2 = [0.003945943056392308, -0.4353856147398717] 
b2 = 0.844606561270203
训练集上的正确率为: 74.0 %
测试集上的正确率为: 54.0 %

使用高斯核训练
达到早停标准
循环了 150 次
支持向量个数: 49 
a为零的支持向量个数: 45
有效向量个数: 4
一共循环 65 次
支持向量个数: 64 
a为零的支持向量个数: 46
有效向量个数: 18
w1 = [0.34999999999999987, -0.95] 
b1 = -0.3311224489795913
w2 = [0.05000000000000093, -1.1759635443845813] 
b2 = 1.8659604131108798
训练集上的正确率为: 98.0 %
测试集上的正确率为: 94.0 %

执行代码后得到下列图像:



四、实验总结

这次做实验的主要内容还是做模型的拟合,还要注意函数的定义(多对一),所以分类问题和回归问题的 x x x y y y还是有区别的。选择模型时要注意模型的使用,避免选错模型事倍功半。

可算是找回一点儿学机器学习的感觉了大哥能不能别骗自己了

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值