NNDL 实验四 线性分类


pytorch实现

第3章 线性分类

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

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

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

3.1.1 数据集构建

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

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([n_samples_out]), torch.ones([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(0, noise, 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()

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

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

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

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回归算子

Logistic回归模型其实就是线性层与Logistic函数的组合,通常会将 Logistic回归模型中的权重和偏置初始化为0,同时,为了提高预测样本的效率,我们将N个样本归为一组进行成批地预测。
y ^ = p ( y ∣ x ) = σ ( X ω + b ) \hat{y}=p(y|x)=\sigma (X\omega +b) y^=p(yx)=σ(Xω+b)
其中
X ∈ R N × D 为 N 个样本的特征矩阵 , y ^ 为 N 个样本的预测值构成的 N 维向量。 X\in R^{N\times D}为N个样本的特征矩阵, \hat{y}为N个样本的预测值构成的N维向量。 XRN×DN个样本的特征矩阵,y^N个样本的预测值构成的N维向量。
这里,我们构建一个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

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

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

运行结果:
在这里插入图片描述
从运行结果看,模型最终的输出g(⋅)恒为0.5。这是由于采用全0初始化后,不论输入值的大小为多少,Logistic函数的输入值恒为0,因此输出恒为0.5。
问题1: Logistic回归在不同的书籍中,有许多其他的称呼,具体有哪些?你认为哪个称呼最好?
还有对数几率回归,在李航的《统计学习方法(第二版)》中被称为逻辑斯蒂分布。
对数几率回归更好。对于二分类问题,最理想的是单位阶跃函数,但是单位阶跃函数不连续,难以求导,所以用对数几率函数代替。对数几率函数(logistic function),简称对率函数。从对数几率回归这个名字就能得知它采用了对数几率函数,挺清晰明了的。

问题2:什么是激活函数?
激活函数是一种添加到人工神经网络中的函数,旨在帮助网络学习数据中的复杂模式。在神经元中,输入的input经过一系列加权求和后作用于另一个函数,这个函数就是这里的激活函数。
为什么要用激活函数?
因为神经网络中每一层的输入输出都是一个线性求和的过程,下一层的输出只是承接了上一层输入函数的线性变换,所以如果没有激活函数,那么无论你构造的神经网络多么复杂,有多少层,最后的输出都是输入的线性组合,纯粹的线性组合并不能够解决更为复杂的问题。而引入激活函数之后,我们会发现常见的激活函数都是非线性的,因此也会给神经元引入非线性元素,使得神经网络可以逼近其他的任何非线性函数,这样可以使得神经网络应用到更多非线性模型中。
常见激活函数有哪些?

(1)sigmoid

①定义:sigmoid函数用于影藏层神经元输出,能将数值映射到(0,1)区间,可用来做二分类,表达式为:
f ( x ) = 1 1 + e −

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值