NNDL 实验四 线性分类

Posted 笼子里的薛定谔

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了NNDL 实验四 线性分类相关的知识,希望对你有一定的参考价值。

第3章 线性分类

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

3.1.1 数据集构建

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

import math
import copy
import torch
def make_moons(n_samples=1000, shuffle=True, noise=None):
    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.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(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()

运行结果

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

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

运行结果

3.1.2 模型构建

# 定义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()

运行结果

Logistic回归算子:

from nndl import op.Op
op=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):
        # 线性计算
        score = torch.matmul(inputs, self.params['w']) + self.params['b']
        # Logistic 函数
        outputs = logistic(score)
        return outputs

op.Op的代码如下:

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

测试

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

运行结果

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

  • 《机器学习》:对数几率回归
  • 《统计学习方法》:逻辑斯蒂回归
  • 《数学建模》:二元的叫二分类logit回归模型 ,多元的叫多分类Logistic回归分析。也被叫做虫口模型,因为它在流行病学中应用较多,比较常用的情形是探索某疾病的危险因素,根据危险因素预测某疾病发生的概率。刚刚过去的数模国赛还使用了一下,但因为自变量和因变量的关系一点也不显著,被迫放弃
  • 剩余的查了一下,貌似都喜欢logistic回归的叫法,包括我在入门时候,一直也是听的logistic回归

我个人更喜欢《数学建模》的叫法,但是也适应回归叫法,因为本身就是分类模型,所以我更喜欢它带分类字眼,但是SPSS把logistic模型归到回归模型里面不归到分类模型还挺迷惑的。

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

1.定义:
百度定义:激活函数(Activation Function),就是在人工神经网络的神经元上运行的函数,负责将神经元的输入映射到输出端。

激活函数(Activation functions)对于人工神经网络模型去学习、理解非常复杂和非线性的函数来说具有十分重要的作用。它们将非线性特性引入到我们的网络中。

它的两个特性:

  • 在人工神经网络上运行的函数,既然是一个函数,就能够普通的函数一样使用。
  • 把非线性特性引入网络中,简单说就是激活函数有“掰弯”的能力。能够增强我们关注的特征,减弱不关注的特征。

比如线性函数:f(x)=x+1

添加一个绝对值的激活函数:|fx|=|x+1|


如果把x表示特征,y=x+1表示神经网络的一个处理层,y越大表示特征越明显;

显然,添加绝对值激活函数后,x越小和越大的特征,越被强化。
2.为什么要用激活函数:

神经网络中,如果不加入激活函数,那么每一层的输入输出都是函数均是线性的,网络的逼近能力有限,于是就引入非线性函数作为激活函数,使网络的表达能力更强。


激活函数在上一层隐藏层的输出和下一层隐藏层输入之前。经过激活函数之后,上一层隐藏层的输出就被“掰弯”了,使得网络结构的表示能力大大增强。

3.常见激活函数有哪些

(1).sigmoid函数
图像:

特点:它能够把输入的连续实值变换为0和1之间的输出。如果是非常大的负数,那么输出就是0;如果是非常大的正数,输出就是1。可以用来做二分类。
缺点:

  • 在深度神经网络中梯度反向传递时导致梯度爆炸和梯度消失,其中梯度爆炸发生的概率非常小,而梯度消失发生的概率比较大。具体表现为:如果我们初始化神经网络的权值为 [0,1]之间的随机值,由反向传播算法的数学推导可知,梯度从后向前传播时,每传递一层梯度值都会减小为原来的0.25倍,如果神经网络隐层特别多,那么梯度在穿过多层后将变得非常小接近于0,即出现梯度消失现象;当网络权值初始化为 (1,+∞)区间内的值,则会出现梯度爆炸情况。
  • 计算中含有幂运算,求解时比较耗时。对于规模比较大的深度网络,会增加训练时间。
  • Sigmoid 的 output 不是0均值(即zero-centered)。会导致后一层的神经元将得到上一层输出的非0均值的信号作为输入。产生的一个结果就是:如x>0,f(x)>0, 那么对w求局部梯度则都为正,这样在反向传播的过程中w要么都往正方向更新,要么都往负方向更新,导致有一种捆绑的效果,使得收敛缓慢。

sigmoid是我最熟悉的函数了,因为是上学期机器学习的重点,BP反向传播推导了无数次,他的导数性质也很好用。

(2).tanh函数

图像:

特点:

  • 解决了在正区间梯度消失的问题
  • 计算速度非常快,只需要判断输入是否大于0
  • 收敛速度远快于sigmoid和tanh

缺点:

  • 输出不是非0均值(zero-centered)
  • 某些神经元(如小于0的特征)可能永远不会被激活,导致相应的参数永远不能被更新。

(3) relu函数

图像:
优点:

1) 解决了gradient vanishing问题 (在正区间)
2)计算速度非常快,只需要判断输入是否大于0
3)收敛速度远快于sigmoid和tanh

注意问题:

1)ReLU的输出不是zero-centered
2)Dead ReLU Problem,指的是某些神经元可能永远不会被激活,导致相应的参数永远不能被更新。有两个主要原因可能导致这种情况产生: (1) 非常不幸的参数初始化,这种情况比较少见 (2) learning rate太高导致在训练过程中参数更新太大,不幸使网络进入这种状态。解决方法是可以采用Xavier初始化方法,以及避免将learning rate设置太大或使用adagrad等自动调节learning rate的算法。

尽管存在这两个问题,ReLU目前仍是最常用的activation function,在搭建人工神经网络的时候推荐优先尝试!

(4)Leaky Relu函数
图像:
解决了Dead Relu Promble。
理论上来讲,Leaky ReLU有ReLU的所有优点,外加不会有Dead ReLU问题,但是在实际操作当中,并没有完全证明Leaky ReLU总是好于ReLU。

(5)softmax函数(这个多说点,因为很重要)
Softmax 是用于多类分类问题的激活函数,在多类分类问题中,超过两个类标签则需要类成员关系。对于长度为 K 的任意实向量,Softmax 可以将其压缩为长度为 K,值在(0,1)范 围内,并且向量中元素的总和为 1 的实向量,公式如下:

图像:

例如:

Softmax从字面上来说,可以分成soft和max两个部分。max顾名思义就是最大值的意思。Softmax的核心在于soft,而soft有软的含义,与之相对的是hard硬。很多场景中需要我们找出数组所有元素中值最大的元素,实质上都是求的hardmax。
hardmax最大的特点就是只选出其中一个最大的值,即非黑即白。但是往往在实际中这种方式是不合情理的,比如对于文本分类来说,一篇文章或多或少包含着各种主题信息,我们更期望得到文章对于每个可能的文本类别的概率值(置信度),可以简单理解成属于对应类别的可信度。所以此时用到了soft的概念,Softmax的含义就在于不再唯一的确定某一个最大值,而是为每个输出分类的结果都赋予一个概率值,表示属于每个类别的可能性。
Softmax 确保较小的值具有较小的概率,并且不会直接丢弃。我们可以认为它是 argmax 函数的概率版本或「soft」版本。
Softmax 函数的分母结合了原始输出值的所有因子,这意味着 Softmax 函数获得的各种概率彼此相关。
Softmax激活函数的缺点:

  • 在零点不可微。
  • 负输入的梯度为零,这意味着对于该区域的激活,权重不会在反向传播期间更新,因此会产生永不激活的死亡神经元。

更多详细见解请见:

  1. https://blog.csdn.net/tyhj_sf/article/details/79932893?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522164570203816780357215346%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fall.%2522%257D&request_id=164570203816780357215346&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2allfirst_rank_ecpm_v1~rank_v31_ecpm-1-79932893.pc_search_result_cache&utm_term=%E6%BF%80%E6%B4%BB%E5%87%BD%E6%95%B0&spm=1018.2226.3001.4187

2.https://blog.csdn.net/hy592070616/article/details/120618490

4.损失函数在pytorch中的定义:
基本的用法如下:
torch.nn.ReLU()
torch.nn.LeakyReLU()
torch.nn.Sigmoid()
torch.nn.Tanh()
torch.nn.Softmax()

更多的还是需要参考pytorch官方文档,学pytorch不会查文档可不行,链接如下:

https://pytorch.org/docs/stable/torch.html

3.1.3 损失函数

交叉熵损失函数

# 实现交叉熵损失函数
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):
        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([3,1])
# 计算风险函数
bce_loss = BinaryCrossEntropyLoss()
print(bce_loss(outputs, labels))

运行结果

3.1.4 模型优化

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

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

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'] = torch.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):
        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([N]), (labels-self.outputs))

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

其中α 为学习率。
代码实现如下:

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]

3.1.5 评价指标

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

def accuracy(preds, labels):
    # 判断是二分类任务还是多分类任务,preds.shape[1]=1时为二分类任务,preds.shape[1]>1时为多分类任务
    if preds.shape[1] == 1:
        # 二分类时,判断每个概率值是否大于0.5,当大于0.5时,类别为1,否则类别为0
        # 使用'torch.cast'将preds的数据类型转换为float32类型
        print(preds.dtype)
        # preds = torch.can_cast((preds>=0.5),torch.float32)
    else:
        # 多分类时,使用'torch.argmax'计算最大元素索引作为类别
       preds = torch.argmax(preds,dim=1).int()


    return torch.mean((preds == labels).NNDL实验五前馈神经网络-二分类任务

NNDL 实验六 卷积神经网络LeNet实现MNIST

NNDL第三次作业

cs231n笔记:线性分类器

计算机视觉与深度学习线性分类器

深度学习—线性分类器理解