实验四 线性分类

pytorch实现

第3章 线性分类

目录

第3章 线性分类

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

3.1.2 模型构建

3.1.3 损失函数

3.1.4 模型优化

3.1.5 评价指标

3.1.6 完善Runner类

3.1.7 模型训练

3.1.8 模型评价

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

3.2.1 数据集构建

3.2.2 模型构建

3.2.3 损失函数

3.2.4 模型优化

3.2.5 模型训练

3.2.6 模型评价

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



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


3.1.1 数据集构建

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

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

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

生成1000个数据并进行可视化

import math
import copy
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.cat'将两类数据的特征1和特征2分别延维度0拼接在一起,得到全部特征1和特征2
    # 使用'torch.stack'将两类特征延维度1堆叠在一起
    X = torch.stack(
        [torch.cat([outer_circ_x, inner_circ_x]),
         torch.cat([outer_circ_y, inner_circ_y])],
        axis=1
    )

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

    # 使用'torch. zeros'将第一类数据的标签全部设置为0
    # 使用'torch. ones'将第一类数据的标签全部设置为1
    y = torch.cat(
        [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个样本
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])

# 打印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, 1])

3.1.2 模型构建

Logistic函数实现如下:

# 定义Logistic函数
def logistic(x):
    return 1 / (1 + torch.exp(-x))
# 在[-10,10]的范围内生成一系列的输入值,用于绘制函数曲线
x = torch.linspace(-10, 10, 10000)
plt.figure()
plt.plot(x.tolist(), logistic(x).tolist(), color="#e4007f", 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。正因为Logistic函数具有这样的性质,使得其输出可以直接看作为概率分布。

Logistic回归算子,代码实现如下,并在末尾加上测试代码,观察输出结果:

# 线性算子
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
op=Op

class model_LR(op):
    def __init__(self, input_dim):
        super(model_LR, self).__init__()
        self.params = {}
        # 将线性层的权重参数全部初始化为0
        self.params['w'] = torch.zeros([input_dim, 1])
        # self.params['w'] = torch.normal(mean=0, std=0.01, shape=[input_dim, 1])
        # 将线性层的偏置参数初始化为0
        self.params['b'] = torch.zeros([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
# 固定随机种子,保持每次运行结果一致
torch.seed()
# 随机生成3条长度为4的数据
inputs = torch.randn([3,4])
print('Input is:', inputs)
# 实例化模型
model = model_LR(4)
outputs = model(inputs)
print('Output is:', outputs)
Input is: tensor([[-0.1879, -0.1178, -1.2004,  0.4269],
        [ 0.3253, -0.3716, -1.6557,  1.3576],
        [-2.0686,  0.7803,  0.1425,  0.9552]])
Output is: tensor([[0.5000],
        [0.5000],
        [0.5000]])

Process finished with exit code 0


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

logistic回归在李航的《统计学习方法(第二版)》中被称为逻辑斯蒂分布,也在其他课本中被称为对数几率回归。
我认为logistic回归最好,不论是哪种汉语名称都是翻译过来的,而这个函数本身是由国外研究出来的,所以不翻译更准确点

问题2:什么是激活函数?为什么要用激活函数?常见激活函数有哪些?
激活函数就是在人工神经网络的神经元上运行的函数,负责将神经元的输入映射到输出端。
如果不用激活函数,每一层输出都是上层输入的线性函数,无论神经网络有多少层,输出都是输入的线性组合,这种情况就是最原始的感知机。如果使用的话,激活函数给神经元引入了非线性因素,使得神经网络可以任意逼近任何非线性函数,这样神经网络就可以应用到众多的非线性模型中

3.1.3 损失函数


交叉熵损失函数

# 实现交叉熵损失函数
class BinaryCrossEntropyLoss(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, 1)
        return loss

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

Process finished with exit code -1073741749 (0xC000004B)

3.1.4 模型优化


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

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

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

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

将上面的参数更新过程包装为优化器,首先定义一个优化器基类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)作为评价指标。如果模型预测的类别与真实类别一致,则说明模型预测正确。准确率即正确预测的数量与总的预测数量的比值:

 其中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 = torch.tensor(preds>=0.5, dtype=torch.float32)
    else:
        # 多分类时,使用'paddle.argmax'计算最大元素索引作为类别
        preds = torch.argmax(preds,1, int32)
    return torch.mean(torch.tensor(torch.eq(preds, labels), dtype=torch.float32))
# 假设模型的预测值为[[0.],[1.],[1.],[0.]],真实类别为[[1.],[1.],[0.],[0.]],计算准确率
preds = torch.tensor([[0.],[1.],[1.],[0.]])
labels = torch.tensor([[1.],[1.],[0.],[0.]])
print("accuracy is:", accuracy(preds, labels))

运行结果:

accuracy is: tensor(0.5000)

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
        # 记录训练过程中的评价指标变化情况
        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)

3.1.7 模型训练


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

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

# 特征维度
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.76875
[Train] epoch: 0, loss: 0.6931460499763489, score: 0.5015624761581421
[Dev] epoch: 0, loss: 0.6823009252548218, score: 0.768750011920929
best accuracy performence has been updated: 0.76875 --> 0.77500
best accuracy performence has been updated: 0.77500 --> 0.78125
best accuracy performence has been updated: 0.78125 --> 0.78750
best accuracy performence has been updated: 0.78750 --> 0.79375
best accuracy performence has been updated: 0.79375 --> 0.80000
best accuracy performence has been updated: 0.80000 --> 0.80625
best accuracy performence has been updated: 0.80625 --> 0.81250
[Train] epoch: 50, loss: 0.48767873644828796, score: 0.792187511920929
[Dev] epoch: 50, loss: 0.4758671224117279, score: 0.8125
best accuracy performence has been updated: 0.81250 --> 0.81875
[Train] epoch: 100, loss: 0.44838473200798035, score: 0.7953125238418579
[Dev] epoch: 100, loss: 0.43000078201293945, score: 0.8187500238418579
best accuracy performence has been updated: 0.81875 --> 0.82500
best accuracy performence has been updated: 0.82500 --> 0.83125
[Train] epoch: 150, loss: 0.4343798756599426, score: 0.800000011920929
[Dev] epoch: 150, loss: 0.4106923043727875, score: 0.831250011920929
[Train] epoch: 200, loss: 0.4280149042606354, score: 0.8062499761581421
[Dev] epoch: 200, loss: 0.4002835750579834, score: 0.831250011920929
[Train] epoch: 250, loss: 0.4247114360332489, score: 0.807812511920929
[Dev] epoch: 250, loss: 0.39388012886047363, score: 0.831250011920929
[Train] epoch: 300, loss: 0.422849178314209, score: 0.8109375238418579
[Dev] epoch: 300, loss: 0.3896119296550751, score: 0.831250011920929
[Train] epoch: 350, loss: 0.4217398762702942, score: 0.809374988079071
[Dev] epoch: 350, loss: 0.3866138458251953, score: 0.831250011920929
[Train] epoch: 400, loss: 0.42105332016944885, score: 0.8109375238418579
[Dev] epoch: 400, loss: 0.3844301700592041, score: 0.824999988079071
[Train] epoch: 450, loss: 0.4206165373325348, score: 0.8109375238418579
[Dev] epoch: 450, loss: 0.3827974200248718, score: 0.824999988079071

Process finished with exit code -1073741749 (0xC000004B)

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

# 可视化观察训练集与验证集的指标变化情况
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得到了收敛,同时准确率指标都达到了较高的水平,训练比较充分。

3.1.8 模型评价

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

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

可视化观察拟合的决策边界 Xw+b=0Xw+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()

可视化结果:


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


Logistic回归可以有效地解决二分类问题。

但在分类任务中,还有一类多分类问题,即类别数C大于2 的分类问题。

Softmax回归就是Logistic回归在多分类问题上的推广。

3.2.1 数据集构建


数据来自3个不同的簇,每个簇对一个类别。我们采集1000条样本,每个样本包含2个特征。

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='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.to_tensor(centroids_bin, dtype='float32')
    # 控制簇中心的分离程度
    centroids = 1.5 * centroids - 1
    # 随机生成特征值
    X[:, :n_features] = torch.randn(shape=[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(shape=[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, shape=[1]).astype('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.seed()
# 采样1000个样本
n_samples = 1000
X, y = make_multiclass_classification(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])

将这些打印出来

# 打印前5个数据的标签
print(y_train[:5])
tensor([0, 2, 0, 0, 1], dtype=torch.int32)

3.2.2 模型构建

Softmax函数的代码实现如下:


def softmax(X):
    """
    输入:
        - X:shape=[N, C],N为向量数量,C为向量维度
    """
    x_max = torch.max(X, dim=1, keepdim=True)[0]  #N, 1
    x_exp = torch.exp(X - x_max)
    partition = torch.sum(x_exp, 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]])

softmax回归算子

class model_SR(op):
    def __init__(self, input_dim, output_dim):
        super(model_SR, self).__init__()
        self.params = {}
        # 将线性层的权重参数全部初始化为0
        self.params['W'] = torch.zeros([input_dim, output_dim])
        # self.params['W'] = paddle.normal(mean=0, std=0.01, shape=[input_dim, output_dim])
        # 将线性层的偏置参数初始化为0
        self.params['b'] = torch.zeros([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 = torch.matmul(inputs, self.params['W']) + self.params['b']
        # Softmax 函数
        self.outputs = softmax(score)
        return self.outputs

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

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

思考题:Logistic函数是激活函数。Softmax函数是激活函数么?谈谈你的看法。

答:softmax也是激活函数,logistic具体针对的是二分类问题,而softmax解决的是多分类问题,因此从这个角度也可以理解logistic函数是softmax函数的一个特例。

3.2.3 损失函数

class MultiCrossEntropyLoss(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)

3.2.4 模型优化

class model_SR(op):
    def __init__(self, input_dim, output_dim):
        super(model_SR, self).__init__()
        self.params = {}
        # 将线性层的权重参数全部初始化为0
        self.params['W'] = torch.zeros([input_dim, output_dim])
        # self.params['W'] = paddle.normal(mean=0, std=0.01, shape=[input_dim, output_dim])
        # 将线性层的偏置参数初始化为0
        self.params['b'] = torch.zeros(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, 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(shape=[N]), (labels-self.outputs))

3.2.5 模型训练


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

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

# 特征维度
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')
plt.show()

best accuracy performence has been updated: 0.78750 --> 0.79375
best accuracy performence has been updated: 0.79375 --> 0.80000
[Train] epoch: 150, loss: 0.42924508452415466, score: 0.7875000238418579
[Dev] epoch: 150, loss: 0.4625350534915924, score: 0.800000011920929
[Train] epoch: 200, loss: 0.42312106490135193, score: 0.7890625
[Dev] epoch: 200, loss: 0.45930060744285583, score: 0.800000011920929
best accuracy performence has been updated: 0.80000 --> 0.80625
[Train] epoch: 250, loss: 0.4200384318828583, score: 0.792187511920929
[Dev] epoch: 250, loss: 0.4580235183238983, score: 0.8062499761581421
[Train] epoch: 300, loss: 0.41835641860961914, score: 0.792187511920929
[Dev] epoch: 300, loss: 0.45751485228538513, score: 0.8062499761581421
[Train] epoch: 350, loss: 0.4173872172832489, score: 0.7906249761581421
[Dev] epoch: 350, loss: 0.4573286175727844, score: 0.800000011920929
[Train] epoch: 400, loss: 0.41680675745010376, score: 0.7906249761581421
[Dev] epoch: 400, loss: 0.457279771566391, score: 0.800000011920929
[Train] epoch: 450, loss: 0.4164491593837738, score: 0.7906249761581421
[Dev] epoch: 450, loss: 0.45728760957717896, score: 0.800000011920929

3.2.6 模型评价


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


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


步骤:数据处理、模型构建、损失函数定义、优化器构建、模型训练、模型评价和模型预测等,

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

(说明:使用深度学习进行实践时的操作流程基本一致,后文不再赘述。)

主要配置:

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

  • 尝试调整学习率和训练轮数等超参数,观察是否能够得到更高的精度;(必须完成)

3.3.1 数据处理


3.3.1.1 数据集介绍


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


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

  • 训练集:用于确定模型参数;
  • 验证集:与训练集独立的样本集合,用于使用提前停止策略选择最优模型;
  • 测试集:用于估计应用效果(没有在模型中应用过的数据,更贴近模型在真实场景应用的效果)。
  • 在本实验中,将80%的数据用于模型训练,10%的数据用于模型验证,10%的数据用于模型测试。
    # 加载数据集
    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, 0)[0]
        X_max = torch.max(X, 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.seed()
    
    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])

    3.3.2 模型构建

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

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

 

3.3.3 模型训练

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

# 学习率
lr = 0.2

# 梯度下降法
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=200, log_epochs=10, save_path="best_model.pdparams")

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

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

 

3.3.4 模型评价

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

 

# 加载最优模型
runner.load_model('best_model.pdparams')
# 模型评价
score, loss = runner.evaluate([X_test, y_test])
[test] score/loss:0.7850/0.9471

3.3.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 ture category is 1 and the predict category is 1

3.4 实验拓展

尝试调整学习率和训练轮数等超参数,观察是否能够得到更高的精度;
1、将学习率调整维0.15,训练次数增加至200

[Test] score/loss:0.6455/0.6037

2、将学习率调整维0.2,训练次数增加至250

[Test] score/loss:0.6870/0.8734

3、将学习率调整维0.01,训练次数增加至300

[Test] score/loss:0.7870/1.2734
总结:通过这次实验深刻的认识到了激活函数的意义,利用两类激活函数进行了多分类回归。学习到了很多

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值