目录
尝试使用《神经网络与深度学习》中的其他模型进行鸢尾花识别任务,观察是否能够得到更高的精度。(选做)
前言
由于写的比较细,所以会有部分和前一个重的地方,但是我还是都写了思想,我认为是值得的,而且希望老师,大佬帮我看看选做最后一个,多教教我。
第3章 线性分类
一、3.1 基于Logistic回归的二分类任务
3.1.1 数据集构建
构建一个简单的分类任务,并构建训练集、验证集和测试集。
本任务的数据来自带噪音的两个弯月形状函数,每个弯月对一个类别。我们采集1000条样本,每个样本包含2个特征。
随机采集1000个样本,并进行可视化。
将1000条样本数据拆分成训练集、验证集和测试集,其中训练集640条、验证集160条、测试集200条。
# coding=gbk
import math
import copy
import torch
import matplotlib.pyplot as plt
这个是要导入库,还是之前说的要注意读取方式,最好全规定成上边的形式。
def make_moons(n_samples=1000, shuffle=True, noise=None):
"""
生成带噪音的弯月形状数据
输入:
- n_samples:数据量大小,数据类型为int
- shuffle:是否打乱数据,数据类型为bool
- noise:以多大的程度增加噪声,数据类型为None或float,noise为None时表示不增加噪声
输出:
- X:特征数据,shape=[n_samples,2]
- y:标签数据, shape=[n_samples]
"""
n_samples_out = n_samples // 2
n_samples_in = n_samples - n_samples_out
# 采集第1类数据,特征为(x,y)
# 使用'paddle.linspace'在0到pi上均匀取n_samples_out个值
# 使用'paddle.cos'计算上述取值的余弦值作为特征1,使用'paddle.sin'计算上述取值的正弦值作为特征2
outer_circ_x = 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)
# 使用'paddle.concat'将两类数据的特征1和特征2分别延维度0拼接在一起,得到全部特征1和特征2
# 使用'paddle.stack'将两类特征延维度1堆叠在一起
X = torch.stack(
[torch.cat([outer_circ_x, inner_circ_x]),
torch.cat([outer_circ_y, inner_circ_y])],
dim=1
)
print('after concat shape:', torch.cat([outer_circ_x, inner_circ_x]).shape)
print('X shape:', X.shape)
# 使用'paddle. zeros'将第一类数据的标签全部设置为0
# 使用'paddle. 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:
# 使用'paddle.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:
# 使用'paddle.normal'生成符合正态分布的随机Tensor作为噪声,并加到原始特征上
X += torch.normal(mean=0.0, std=noise, size=X.shape)
return X, y
函数的大体思想:
上边的两行代码,是产生数据集的个数额分配。后边的sin、cos是进行数值的计算(这一部分可以更改其他的数值计算,来生成不同形状的函数)。torch.cat是张量的拼接,torch.stack是张量的堆叠,这个是通过张量的拼接与堆叠,来实现咱们想要的张量形状。下边的y是同理,也是通过这样的张量拼接的方式,形成想要的格式。
要注意的函数用法:
torch.linspace(start, end, steps=100, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False) → Tensor
函数的作用是,返回一个一维的tensor(张量),这个张量包含了从start到end(包括端点)的等距的steps个数据点。
torch.cat是将两个张量(tensor)拼接在一起,cat是concatenate的意思,即拼接,联系在一起。
这个,去网上看了很多的示例,都在讨论二维数据(矩阵),单是对于做图像与深度学习的人来说均是三维起步,一般都是4维,所以最后自己试了试总结出来一个规律。
对于一个张量的维度,有几个括号就是几维,因此以第0维拼接就是将第一个中括号内的内容进行拼接,以此类推即可得出结论,只需要按括号来区分和连接,即可很好的区分结果。
至于,torch.satck(),这个函数的用法和cat基本就差不多了,就是注意这个是堆叠,但是分析结果的思路是一致的。
# 采样1000个样本
n_samples = 1000
X, y = make_moons(n_samples=n_samples, shuffle=True, noise=0.5)
# 可视化生产的数据集,不同颜色代表不同类别
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])
代码的大体思想为:
这个相当于是函数的调用,就是创建好了函数,这个调用一下,这个的核心还是在上边的函数创建部分,但是有几个小函数需要注意一下。
要注意的函数用法:
这个之中要注意的是%matplotlib inline,这个被称为魔法函数,作用是不用打plt.show(),但是最好不要这么用,删去后要记得加上plt.show(),要不会没有输出。
plt.scatter函数能够显现两种颜色,其实是靠后边y来控制的,当y一直变化,颜色也就会变化,也就是标签不同颜色不同。
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])
运行结果为:
X_train shape: torch.Size([640, 2]) y_train shape: torch.Size([640, 1])
tensor([[0.],
[1.],
[1.],
[0.],
[0.]])
代码的大体思想:
这一部分的主要作用是构建训练集、测试集和验证集,主要是通过切边实现的,上边的三个参数从变量名就可以看出哪个是哪个,然后重要的那个人py.reshape函数,这个是把一维的改成二维的,这样就对应上了。
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()
代码的大体思想:
其实这一部分相当于看一下logisitic是一个什么样的函数,什么样的函数形状,除了,上边定义了函数以外,其他的函数相当于是在调整画图的好看程度。
import Op
注意这个,看输入里边,所以要把调用改成这个,并且之前创建的是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'] = paddle.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
函数的大体思想:
这个是用类封装的做法,这个注意的问题就是类继承问题,其他的关于类的用法前边已经说过了,这个的思想就是按部就班的来进行线性的计算(这一部分上一节是有过程的),然后把y值带入到logistics函数中,通过输出的结果来,判断结果,这也是logistics回归的大致思想与大致流程。
需要注意的函数用法:
super(model_LR, self).__init__(),这个是继承父类的意思,也就是继承Op类。
# 固定随机种子,保持每次运行结果一致
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]]
代码的大体思想:
这个就是正常的函数调用,需要注意的是之前说过的forwad函数的问题,所以不要看不懂。
问题1:Logistic回归在不同的书籍中,有许多其他的称呼,具体有哪些?你认为哪个称呼最好?
我会把截图放在下边,有兴趣的可以去看一看,同时希望老师和各位大神多教教我。
首先,先对咱们学的神书致以崇高敬意,蒲公英书中叫Logistics回归。
然后,是对上学期学神书致以崇高敬意,西瓜书中叫对数几率回归。
其次,就要拜一拜李航大神了,在《统计学习方法第2版中》,叫逻辑斯谛回归。
然后,就是在《模式识别与机器学习》中,叫logistic回归。
最后,就是在数学建模中了,叫二分类的logit回归模型 ,多元的叫多分类Logistic回归分析。也被叫做虫口模型。因为校赛的时候考了这个模型了,所以知道,当时还比较震惊,第一次知道还叫虫口模型。据说还是从生物学上来的,哈哈哈哈哈哈(这个没图片了,我建模书被人拿走了。。。),但是感觉咱学的真的有用啊,好多都用到建模上了,致以崇高敬意。
我也查了查,像鱼书,《Deep Learing》等书上没有说这个模型,我列出来的应该基本就是大家比较认可的著作上大部分都是用的英文交Logistics回归,只是大小写的区别。我一开始知道的是对数几率回归和逻辑斯谛回归,因为一开始看的是西瓜书和《统计学方法》。但是现在感觉可能Logistics回归通用一点。
但是,我的感受是大神们想叫啥叫啥,我跟着他们学就行了,咱们不要纠结叫啥,咱们需要的是学会思想,学会方法,掌握本领。
问题2:什么是激活函数?为什么要用激活函数?常见激活函数有哪些?
首先,先把老师上课说的的话,写在最前边,这是最重要的
激活函数就是把线性的转化成非线性的的函数,为什么要转成非线性的呢?因为非线性的表达能力更强呀,你想呀,生活中的事物,不可能都是线性的,反而大部分都应该是线性的,这就是老师上课讲的话,其实我感觉这就概括全了,剩下的就是一些拓展了。
下边写一些,自己的看法
激活函数的定义:
激活函数(Activation functions)对于人工神经网络模型去学习、理解非常复杂和非线性的函数来说具有十分重要的作用。它们将非线性特性引入到我们的网络中。其主要目的是将A-NN模型中一个节点的输入信号转换成一个输出信号。该输出信号现在被用作堆叠中下一个层的输入。
但是,我更喜欢这句话,所谓激活函数(Activation Function),就是在人工神经网络的神经元上运行的函数,负责将神经元的输入映射到输出端。
这是百度百科的定义
激活函数(Activation functions)对于人工神经网络 [1] 模型去学习、理解非常复杂和非线性的函数来说具有十分重要的作用。它们将非线性特性引入到我们的网络中。如图1,在神经元中,输入的 inputs 通过加权,求和后,还被作用了一个函数,这个函数就是激活函数。引入激活函数是为了增加神经网络模型的非线性。没有激活函数的每层都相当于矩阵相乘。就算你叠加了若干层之后,无非还是个矩阵相乘罢了。
这个图中step function 就是激活函数,这和咱们上课的时候讲的,是一样的
它必须要满足一些特性
1、非线性,这样增加网络的深度才有意义
2、可导的,不然怎么做梯度下降
3、易于计算的
4、输出空间最好是有限的,这条好像也不是必须的,Relu就没有遵循这条
最后一条有的书上说了,有的书上没说
至于为什么要用激活函数,先来看看没有激活函数会怎么样
1. 对于y=ax+b 这样的函数,当x的输入很大时,y的输出也是无限大小的,经过多层网络叠加后,值更加膨胀的没边了,这显然不符合我们的预期,很多情况下我们希望的输出是一个概率。
2. 线性的表达能力太有限了,即使经过多层网络的叠加,y=ax+b无论叠加多少层最后仍然是线性的,增加网络的深度根本没有意义。线性回归连下面这个最简单的“异或”,都无法拟合:
那么在来做一下对比
这两张图可以完美的体现我想表达的意思,在学习框架的时候我还不太明白,啥叫线性,啥叫非线性,在上学期学机器学习的时候,讲感知机才明白了,最后解释一下上边两张图,其实就是老师的解释。
激活函数其实是对应到生物学上的定义,激活状态为有信号,非激活状态为无信号。然而对应到这里的激活函数,其实就有些变质了,如果还在想着激活为1,非激活为0,那么是无法理解神经网络中的激活函数的。
神经网络的激活函数其实是将线性转化为非线性的一个函数,并非只是简单地给予0,或者给予1。
这样可以很好的提升表达能力,这对一个模型是很重要的。
常见激活函数有哪些?(下边的一些部分来自我之前学激活函数的时候看的一个大神博客,找了好久才又找到了)
下边说一说咱们用的多几种
1. Sigmoid函数
Sigmoid函数也叫Logistic函数,用于隐层神经元输出,取值范围为(0,1),它可以将一个实数映射到(0,1)的区间,可以用来做二分类。在特征相差比较复杂或是相差不是特别大时效果比较好。sigmoid是一个十分常见的激活函数,函数的表达式如下:
图像类似一个S形曲线(我感觉就是S型曲线)
在什么情况下适合使用 Sigmoid 激活函数呢?
- Sigmoid 函数的输出范围是 0 到 1。由于输出值限定在 0 到1,因此它对每个神经元的输出进行了归一化;
- 用于将预测概率作为输出的模型。由于概率的取值范围是 0 到 1,因此 Sigmoid 函数非常合适;
- 梯度平滑,避免「跳跃」的输出值;
- 函数是可微的。这意味着可以找到任意两个点的 sigmoid 曲线的斜率;
- 明确的预测,即非常接近 1 或 0。
Sigmoid 激活函数存在的不足:
- 梯度消失:注意:Sigmoid 函数趋近 0 和 1 的时候变化率会变得平坦,也就是说,Sigmoid 的梯度趋近于 0。神经网络使用 Sigmoid 激活函数进行反向传播时,输出接近 0 或 1 的神经元其梯度趋近于 0。这些神经元叫作饱和神经元。因此,这些神经元的权重不会更新。此外,与此类神经元相连的神经元的权重也更新得很慢。该问题叫作梯度消失。因此,想象一下,如果一个大型神经网络包含 Sigmoid 神经元,而其中很多个都处于饱和状态,那么该网络无法执行反向传播。
- 不以零为中心:Sigmoid 输出不以零为中心的,,输出恒大于0,非零中心化的输出会使得其后一层的神经元的输入发生偏置偏移(Bias Shift),并进一步使得梯度下降的收敛速度变慢。
- 计算成本高昂:exp() 函数与其他非线性激活函数相比,计算成本高昂,计算机运行起来速度较慢。
2. Tanh/双曲正切激活函数
Tanh 激活函数又叫作双曲正切激活函数(hyperbolic tangent activation function)。与 Sigmoid 函数类似,Tanh 函数也使用真值,但 Tanh 函数将其压缩至-1 到 1 的区间内。与 Sigmoid 不同,Tanh 函数的输出以零为中心,因为区间在-1 到 1 之间。
函数表达式:
我们可以发现Tanh 函数可以看作放大并平移的Logistic 函数,其值域是(−1, 1)。Tanh与sigmoid的关系如下:
tanh 激活函数的图像也是 S 形,作为一个双曲正切函数,tanh 函数和 sigmoid 函数的曲线相对相似。但是它比 sigmoid 函数更有一些优势。
tanh存在的不足:
- 与sigmoid类似,Tanh 函数也会有梯度消失的问题,因此在饱和时(x很大或很小时)也会「杀死」梯度。
注意:在一般的二元分类问题中,tanh 函数用于隐藏层,而 sigmoid 函数用于输出层,但这并不是固定的,需要根据特定问题进行调整。
3. ReLU激活函数(可以说目前最火)
ReLU函数又称为修正线性单元(Rectified Linear Unit),是一种分段线性函数,其弥补了sigmoid函数以及tanh函数的梯度消失问题,在目前的深度神经网络中被广泛使用。ReLU函数本质上是一个斜坡(ramp)函数,公式及函数图像如下:
ReLU 函数是深度学习中较为流行的一种激活函数,相比于 sigmoid 函数和 tanh 函数,它具有如下优点:
- 当输入为正时,导数为1,一定程度上改善了梯度消失问题,加速梯度下降的收敛速度;
- 计算速度快得多。ReLU 函数中只存在线性关系,因此它的计算速度比 sigmoid 和 tanh 更快;
- 也被认为具有生物学合理性(Biological Plausibility),比如单侧抑制、宽兴奋边界(即兴奋程度可以非常高)。
ReLU函数的不足:
- Dead ReLU 问题。当输入为负时,ReLU 完全失效,在正向传播过程中,这不是问题。有些区域很敏感,有些则不敏感。但是在反向传播过程中,如果输入负数,则梯度将完全为零;
- 【Dead ReLU问题】ReLU神经元在训练时比较容易“死亡”。在训练时,如果参数在一次不恰当的更新后,第一个隐藏层中的某个ReLU 神经元在所有的训练数据上都不能被激活,那么这个神经元自身参数的梯度永远都会是0,在以后的训练过程中永远不能被激活.这种现象称为死亡ReLU问题,并且也有可能会发生在其他隐藏层.
- 不以零为中心:和 Sigmoid 激活函数类似,ReLU 函数的输出不以零为中心,ReLU 函数的输出为 0 或正数,给后一层的神经网络引入偏置偏移,会影响梯度下降的效率。4
4. Softmax激活函数
Softmax 是用于多类分类问题的激活函数,在多类分类问题中,超过两个类标签则需要类成员关系。对于长度为 K 的任意实向量,Softmax 可以将其压缩为长度为 K,值在(0,1)范围内,并且向量中元素的总和为 1 的实向量。
函数表达式如下:
Softmax 与正常的 max 函数不同:max 函数仅输出最大值,但 Softmax 确保较小的值具有较小的概率,并且不会直接丢弃。我们可以认为它是 argmax 函数的概率版本或「soft」版本。
Softmax 函数的分母结合了原始输出值的所有因子,这意味着 Softmax 函数获得的各种概率彼此相关。
Softmax 激活函数的不足:
- 在零点不可微;
- 负输入的梯度为零,这意味着对于该区域的激活,权重不会在反向传播期间更新,因此会产生永不激活的死亡神经元。
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):
"""
输入:
- 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, dim=1)
return loss
# 测试一下
# 生成一组长度为3,值为1的标签数据
labels = torch.ones(size=[3,1])
# 计算风险函数
bce_loss = BinaryCrossEntropyLoss()
print(bce_loss(outputs, labels))
运行结果为:
tensor([0.6931])
函数的大体思路:
这个其实和之前的均方误差的思路是差不多的,变化的是计算方式,其他的都没有变化,同时需要注意的就是之前说过的forward的问题,不要看不懂就行。
3.2.4 模型优化
不同于线性回归中直接使用最小二乘法即可进行模型参数的求解,Logistic回归需要使用优化算法对模型参数进行有限次地迭代来获取更优的模型,从而尽可能地降低风险函数的值。
在机器学习任务中,最简单、常用的优化算法是梯度下降法。
使用梯度下降法进行模型优化,首先需要初始化参数W和 b,然后不断地计算它们的梯度,并沿梯度的反方向更新参数。
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'] = paddle.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)
在计算参数的梯度之后,我们按照下面公式更新参数:
其中α 为学习率。
将上面的参数更新过程包装为优化器,首先定义一个优化器基类Optimizer
,方便后续所有的优化器调用。在这个基类中,需要初始化优化器的初始学习率init_lr
,以及指定优化器需要优化的参数。代码实现如下
rom 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=[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.can_cast((preds>=0.5),torch.float32)
else:
# 多分类时,使用'paddle.argmax'计算最大元素索引作为类别
preds = torch.argmax(preds,dim=1, dtype=torch.int32)
return torch.mean(torch.can_cast(torch.equal(preds, labels),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)
需要注意的函数:
一定要注意是torch.can_cast,这个函数,而不是其他的,其他的函数就好明白了
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)
运行结果为:
best accuracy performence has been updated: 0.00000 --> 0.73750
[Train] epoch: 0, loss: 0.693359375, score: 0.4999999701976776
[Dev] epoch: 0, loss: 0.6839081048965454, score: 0.737500011920929
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
[Train] epoch: 50, loss: 0.48008108139038086, score: 0.7906250357627869
[Dev] epoch: 50, loss: 0.5083038210868835, score: 0.7562500238418579
代码的大体思想 :
这个就是一个完整的训练的过程了,就好像是上一次作业写的那个,这个是实操一下,就是先训练一轮,输出正确率,评估,反向传播,迭代,然后再次训练一轮,就是这个过程,一直到评估符合标准为止。
3.1.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")
运行结果为:
[Train] epoch: 0, loss: 0.6931460499763489, score: 0.0
[Dev] epoch: 0, loss: 0.6844645738601685, score: 0.0[Train] epoch: 50, loss: 0.4831995964050293, score: 0.0
[Dev] epoch: 50, loss: 0.519908607006073, score: 0.0[Train] epoch: 100, loss: 0.43985626101493835, score: 0.0
[Dev] epoch: 100, loss: 0.4893949627876282, score: 0.0
# 可视化观察训练集与验证集的指标变化情况
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')
运行结果为:
代码的大体思想:
这个就是正常的训练过成的调用了,这个在前边的训练中也有体现。并且可视化的注意函数的运用,上边调参了,正常右边不是那样
3.1.8 模型评价
使用测试集对训练完成后的最终模型进行评价,观察模型在测试集上的准确率和loss数据。
score, loss = runner.evaluate([X_test, y_test])
print("[Test] score/loss: {:.4f}/{:.4f}".format(score, loss))
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()
运行结果为:
这个要注意这个是要调参的
至于是为什么上边我说了一下,我感觉学习率0.1太大,一半都是0.001
改之前:
[Test] score/loss: 0.5000/0.4706
改之后:
[Test] score/loss: 0.5734/0.4159
二、3.2 基于Softmax回归的多分类任务
Logistic回归可以有效地解决二分类问题。
但在分类任务中,还有一类多分类问题,即类别数C大于2 的分类问题。
Softmax回归就是Logistic回归在多分类问题上的推广。
3.2.1 数据集构建
数据来自3个不同的簇,每个簇对一个类别。我们采集1000条样本,每个样本包含2个特征。
import numpy as np
这个需要numpoy,如果不导入的话,会报错。
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]).astype(torch.int32)
# 如果shuffle为True,将所有数据打乱
if shuffle:
idx = torch.randperm(X.shape[0])
X = X[idx]
y = y[idx]
return X, y
# 固定随机种子,保持每次运行结果一致
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()
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)
# 打印前5个数据的标签
print(y_train[:5])
运行结果为:
X_train shape: torch.Size([640, 2]) y_train shape: torch.Size([640])
tensor([0, 2, 0, 0, 1], dtype=torch.int32)
代码的主要思想:
这个创建数据集和上边是差不多的,甚至和之前的是也是差不多的,这个几乎和上边的是差不多的,甚至输出的过程也是和上边一样的。
3.2.2 模型构建
# x为tensor
def softmax(X):
"""
输入:
- X:shape=[N, C],N为向量数量,C为向量维度
"""
x_max = torch.max(X, axis=1, keepdim=True)#N,1
x_exp = torch.exp(X - x_max.values)
partition = torch.sum(x_exp, axis=1, keepdim=True)#N,1
return x_exp / partition
# 观察softmax的计算方式
X = torch.as_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]])
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([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.outputs = None
def __call__(self, inputs):
return self.forward(inputs)
def forward(self, inputs):
# 线性计算
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]])
思考题:Logistic函数是激活函数。Softmax函数是激活函数么?谈谈你的看法。
其实,以前我都没考虑过这个问题,但是刚才一考虑发现,这个真分不太清,从功能上说,感觉作用是一样的的,但是又有点感觉不太一样。所以去查了好多资料,下边就是。
softmax通常来讲是激活函数,但是softmax函数要与交叉熵损失函数一起使用来避免数值溢出的问题。所以,在我们的深度学习框架中,在网络构造中通常是看不见softmax函数的,而在我们调用交叉熵损失函数时(entropy),框架会在entropy之前自动为我们添加softmax函数。
这是一种说法,另一种说法是
但个人认为它是一种归一化函数.激活同样意味着抑制, softmax只是归一化成概率,并没有抑制什么信息. 虽说只是个小问题,但本着严谨态度,个人仍不认为它是一种激活函数.
另外还有线性激活(不激活)函数.各种说法比较混乱,得过且过吧.
但是,我看了这么多之后,认为它应该不算是激活函数,我记得老师上课讲的也是激活函数,因为它大多数是为了做一个分类,而不是为了把线性化为非线性,他为的是出来一个分类的结果,或者可以说是概率,她并没有起到起到隐藏层中的激活函数作用,它是一个分类的作用,所以我认为他并不是一个激活函数,从上边激活函数定义中,也可以知道。
我理解的老师上课讲的应该是这个意思。
3.2.3 损失函数
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.as_tensor([0])
# 计算风险函数
mce_loss = MultiCrossEntropyLoss()
print(mce_loss(outputs, labels))
运行结果为:
tensor(1.0986)
代码的大体思路:
这其实和上边以及前边的思路,都是差不多的,就可以把前边的改改,相当于就是把前边的函数改一下函数关系。
3.2.4 模型优化
使用3.1.4.2中实现的梯度下降法进行参数更新
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([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):
"""
输入:
- 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([N]), (labels-self.outputs))
3.2.5 模型训练
实例化RunnerV2类,并传入训练配置。使用训练集和验证集进行模型训练,共训练500个epoch。每隔50个epoch打印训练集上的指标。
# 特征维度
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.71250
[Train] epoch: 0, loss: 1.098615050315857, score: 0.3453125059604645
[Dev] epoch: 0, loss: 1.0776067972183228, score: 0.7124999761581421
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.75625
3.2.6 模型评价
使用测试集对训练完成后的最终模型进行评价,观察模型在测试集上的准确率。
score, loss = runner.evaluate([X_test, y_test])
print("[Test] score/loss: {:.4f}/{:.4f}".format(score, loss))
# 均匀生成40000个数据点
x1, x2 = paddle.meshgrid(paddle.linspace(-3.5, 2, 200), paddle.linspace(-4.5, 3.5, 200))
x = paddle.stack([paddle.flatten(x1), paddle.flatten(x2)], axis=1)
# 预测对应类别
y = runner.predict(x)
y = paddle.argmax(y, axis=1)
# 绘制类别区域
plt.ylabel('x2')
plt.xlabel('x1')
plt.scatter(x[:,0].tolist(), x[:,1].tolist(), c=y.tolist(), cmap=plt.cm.Spectral)
paddle.seed(102)
n_samples = 1000
X, y = make_multiclass_classification(n_samples=n_samples, n_features=2, n_classes=3, noise=0.2)
plt.scatter(X[:, 0].tolist(), X[:, 1].tolist(), marker='*', c=y.tolist())
运行结果为:
[Test] score/loss: 0.7750/0.6583
和上一题一样,把学习率调成0.001
运行结果为:
[Test] score/loss: 0.7923/0.6127
三、3.3 实践:基于Softmax回归完成鸢尾花分类任务
步骤:数据处理、模型构建、损失函数定义、优化器构建、模型训练、模型评价和模型预测等,
数据处理:根据网络接收的数据格式,完成相应的预处理操作,保证模型正常读取;
模型构建:定义Softmax回归模型类;
训练配置:训练相关的一些配置,如:优化算法、评价指标等;
组装Runner类:Runner用于管理模型训练和测试过程;
模型训练和测试:利用Runner进行模型训练、评价和测试。
(说明:使用深度学习进行实践时的操作流程基本一致,后文不再赘述。)
主要配置:
数据:Iris数据集;
模型:Softmax回归模型;
损失函数:交叉熵损失;
优化器:梯度下降法;
评价指标:准确率。
为了加深对机器学习模型的理解,请自己动手完成以下实验:
- 缺失值分析
from sklearn.datasets import load_iris
import pandas
import numpy as np
这里要说一下,sklearn库,这个真的有必要好好了解一下,这库里边经典的神经网络都是封装好的,直接调用传参就可以,这个真的是好用的,当你来不及自己搭的时候,包括机器学习的算法都是封装好的。但是最好也不要调,要自己搭
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
代码的主要思想:
这个的思想是相当于,调用成型的库来看一下运行的结果,熟悉sklearn库即可。
- 异常值处理
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 数据读取
import copy
import torch
这个记得要把这个库导进去。其实主要是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.as_tensor(X)
y = torch.as_tensor(y)
# 数据归一化
X_min = torch.min(X, axis=0)
X_max = torch.max(X, axis=0)
X = (X-X_min.values) / (X_max.values-X_min.values)
# 如果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_train和y_train的维度
print("X_train shape: ", X_train.shape, "y_train shape: ", y_train.shape)
# 打印前5个数据的标签
print(y_train[:5])
运行结果为:
X shape: torch.Size([150, 4]) y shape: torch.Size([150])
X_train shape: torch.Size([120, 4]) y_train shape: torch.Size([120])
tensor([1, 2, 0, 1, 2], dtype=torch.int32)
3.3.2 模型构建
from nndl import model_SR
import opitimizer
import Op
import metric
import RunnerV2
需要的库为:
# coding=gbk
import torch
import math
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_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.outputs = None
def __call__(self, inputs):
return self.forward(inputs)
def forward(self, inputs):
# 线性计算
score = torch.matmul(inputs, self.params['W']) + self.params['b']
# Softmax 函数
self.outputs = torch.softmax(score)
return self.outputs
# 输入维度
input_dim = 4
# 类别数
output_dim = 3
# 实例化模型
model = model_SR(input_dim=input_dim, output_dim=output_dim)
3.3.3 模型训练
# 输入维度
input_dim = 4
# 类别数
output_dim = 3
# 实例化模型
model = model_SR(input_dim=input_dim, output_dim=output_dim)
# 学习率
lr = 0.2
# 梯度下降法
optimizer = opitimizer.SimpleBatchGD(init_lr=lr, model=model)
# 交叉熵损失
loss_fn = Op.MultiCrossEntropyLoss()
# 准确率
metric = 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")
运行结果为:
调参之后的结果为:
[Dev] epoch: 0,loss:6.350316047668457,score: 0.762499988079071
[Train] epoch: 10,loss:5.359757423400879,score: 0.7515624761581421
[ Dev] epoch: 10,loss: 5.245388507843018,score: 0.762499988079071
[Train] epoch: 20,loss: 4.287961006164551,score:0.7515624761581421
[Dev] epoch: 20,loss: 4.1756768226623535,score: 0.762499988079071
[Train] epoch: 30,loss: 3.321946620941162,score:0.746874988079071
[Dev] epoch: 30,loss: 3.2261555194854736,score: 0.762499988079071
[Train] epoch: 40,loss: 2.568786144256592,score: 0.7562500238418579
[Dev] epoch: 40,loss: 2.4996914863586426,score: 0.762499988079071
best accuracy performence has been updated: 0.76250 --> 0.76875
best accuracy performence has been updated: 0.76875 --> 0.77500
从上图结果可以看出:
这次调学习率0.001之后会导致,损失暴增,并且会报错严重,所以我调了训练训练轮数,这个是最传统的方法,可以看出大概在500轮左右就不怎么变了
3.3.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.7600/1.2292
3.3.5 模型预测
# 预测测试集数据
logits = runner.predict(X_test)
# 观察其中一条样本的预测结果
pred = paddle.argmax(logits[0]).numpy()
# 获取该样本概率最大的类别
label = y_test[0].numpy()
# 输出真实类别与预测类别
print("The true category is {} and the predicted category is {}".format(label[0], pred[0]))
运行结果为:
The true category is 2 and the predicted category is 2
尝试使用《神经网络与深度学习》中的其他模型进行鸢尾花识别任务,观察是否能够得到更高的精度。(选做)
设计思路:
①首先将文件与程序导入同一目录下,利用pandas里边的read函数读取,首先要处理数据,因为分成3类,要弄成low-hight类型,即[0,0,1],[0,1,0],[1,0,0],处理完数据之后,一定要用shuffle打乱数据,方便学习到所有的特征值,首先要搭建bp网络,w参数可利用二维数组,横坐标为当前结点,纵坐标为连接的节点,b参数可用一维数组,横坐标为当前结点,在(0,1)范围内初始化参数之后,前项传播只需要按书上及网络的步骤计算即可,只是要存储输出的参数,一个是隐藏层的输出,一个是输出层的输出,反向传播更新参数时,会发现,用输出层对连接隐藏层的w进行更新时,对于每一个输出层结点,由公式会发现他们都有相同的部分,只是每条线乘以对应w值,同样用隐藏层更新连接输入层时,原理一样,更新b值时,由公式即可得出,最后注意测试时,不是看哪个是1,是看哪个是最大的,正确率,用累计计算即可。
rom __future__ import division
import math
import random
import pandas as pd
flowerLables = {0: 'Iris-setosa',
1: 'Iris-versicolor',
2: 'Iris-virginica'}
random.seed(0)
# 生成区间[a, b)内的随机数
def rand(a, b):
return (b - a) * random.random() + a
# 生成大小 I*J 的矩阵,默认零矩阵
def makeMatrix(I, J, fill=0.0):
m = []
for i in range(I):
m.append([fill] * J)
return m
# 函数 sigmoid
def sigmoid(x):
return 1.0 / (1.0 + math.exp(-x))
# 函数 sigmoid 的导数
def dsigmoid(x):
return x * (1 - x)
class NN:
""" 三层反向传播神经网络 """
def __init__(self, ni, nh, no):
# 输入层、隐藏层、输出层的节点(数)
self.ni = ni + 1 # 增加一个偏差节点
self.nh = nh + 1
self.no = no
# 激活神经网络的所有节点(向量)
self.ai = [1.0] * self.ni
self.ah = [1.0] * self.nh
self.ao = [1.0] * self.no
# 建立权重(矩阵)
self.wi = makeMatrix(self.ni, self.nh)
self.wo = makeMatrix(self.nh, self.no)
#建立阈值
self.bi=makeMatrix(1,self.nh)
self.bo=makeMatrix(1,self.no)
# 设为随机值
for i in range(self.ni):
for j in range(self.nh):
self.wi[i][j] = rand(0,1)
for j in range(self.nh):
self.bi[0][j] = rand(0, 1)
for j in range(self.nh):
for k in range(self.no):
self.wo[j][k] = rand(0, 1)
for k in range(self.no):
self.wo[0][k] = rand(0, 1)
def update(self, inputs):
if len(inputs) != self.ni - 1:
raise ValueError('与输入层节点数不符!')
# 激活输入层
for i in range(self.ni - 1):
self.ai[i] = inputs[i]
# 激活隐藏层
for j in range(self.nh):
sum = 0.0
for i in range(self.ni):
sum = sum + self.ai[i] * self.wi[i][j]
self.ah[j] = sigmoid(sum-self.bi[0][j])
# 激活输出层
for k in range(self.no):
sum = 0.0
for j in range(self.nh):
sum = sum + self.ah[j] * self.wo[j][k]
self.ao[k] = sigmoid(sum-self.bo[0][k])
return self.ao[:]
def backPropagate(self, targets, lr):
""" 反向传播 """
# 计算输出层的误差
output_deltas = [0.0] * self.no
for k in range(self.no):
error = targets[k] - self.ao[k]
output_deltas[k] = dsigmoid(self.ao[k]) * error
# 计算隐藏层的误差
hidden_deltas = [0.0] * self.nh
for j in range(self.nh):
error = 0.0
for k in range(self.no):
error = error + output_deltas[k] * self.wo[j][k]
hidden_deltas[j] = dsigmoid(self.ah[j]) * error
# 更新输出层权重
for j in range(self.nh):
for k in range(self.no):
change = output_deltas[k] * self.ah[j]
self.wo[j][k] = self.wo[j][k] + lr * change
#更新输出层的阈值
for k in range(self.no):
self.bo[0][k] = self.bo[0][k] + lr*output_deltas[k]
# 更新输入层权重
for i in range(self.ni):
for j in range(self.nh):
change = hidden_deltas[j] * self.ai[i]
self.wi[i][j] = self.wi[i][j] + lr * change
# 更新输出层的阈值
for k in range(self.no):
self.bi[0][k] = self.bi[0][k] + lr * hidden_deltas[k]
# 计算误差
error = 0.0
error += 0.5 * (targets[k] - self.ao[k]) ** 2
return error
def test(self, patterns):
count = 0
for p in patterns:
target = flowerLables[(p[1].index(1))]
result = self.update(p[0])
index = result.index(max(result))
print(p[0], ':', target, '->', flowerLables[index])
if target == flowerLables[index]:
count=count+1
accuracy = float(count / len(patterns))
print('accuracy: %-.9f' % accuracy)
def weights(self):
print('输入层权重:')
for i in range(self.ni-1):
print(self.wi[i][0:7])
print('输入层阈值:')
print(self.bi[0])
print()
print('输出层权重:')
for j in range(self.nh-1):
print(self.wo[j])
print('输出层阈值:')
print(self.bo[0])
def train(self, patterns, iterations=1000, lr=0.01):
# lr: 学习速率(learning rate)
for i in range(iterations):
error = 0.0
for p in patterns:
inputs = p[0]
targets = p[1]
self.update(inputs)
error = error + self.backPropagate(targets, lr)
if i % 100 == 0:
print('error: %-.9f' % error)
def iris():
data = []
# 读取数据
raw = pd.read_csv('iris1.txt', header=None,encoding='gbk',sep='\s+')
raw_data = raw.values
raw_feature = raw_data[0:, 0:4]
for i in range(len(raw_feature)):
ele = []
ele.append(list(raw_feature[i]))
if raw_data[i][4] == 1:
ele.append([1, 0, 0])
elif raw_data[i][4] == 2:
ele.append([0, 1, 0])
else:
ele.append([0, 0, 1])
data.append(ele)
# 随机排列数据
random.shuffle(data)
print(data)
training = data
test = data
nn = NN(4, 7, 3)
nn.train(training, iterations=10000)
nn.test(test)
nn.weights()
if __name__ == '__main__':
iris()
总结
纪念一下,哈哈哈哈哈
这次是真的累,弄了建模之后,没休息就开始弄这个,真的累,最后选做问题三其实之前写过几次,拿神经网络写的,原本这次不想写了,但是这次都写到这里,还是要坚持,写完就有收获,所以自己按以前的思路又写了一遍
首先,第一部分是是和前边有关联的东西,但是我写的都是上次没有学到的东西,写了算法原理,写了老师的问题,从原理上写的,这次是真的累,但是真的收获很多,之前的函数,老是像我之前说站在巨人的肩膀上,直接记结论了,那样的话,初学还可以,但是当你要深入进去的话一定要弄懂原理。
第二部,是第一部分拓展,这个之前是真的没写过,但是这个和第一部分有联系,所以我两部分都是好好写的,然后第二部就感觉很好,也很有收获,所以一定要把基础打牢。这一部分的多分类感觉是真的有用,这部分学到了很多
第三部分,是相当于实战一下了,写完感觉懂了,但是还是有点蒙蒙的,实战了一下之后,真的感觉收获很多,因为实战的话,会有很多现实的问题让你解决,最后我也发现,调参真只能一部分上按规律调,但是也会有违反规律率的时候,就像最后一个调参,只有多试才行。
最后感谢老师,哈哈哈哈哈哈
借用老师的一句话
虽然学计算要是不懂思想和别专业有啥区别,所以我认为累是值得的