写在前面的一些内容
- 本文为HBU_神经网络与深度学习实验(2022年秋)实验4的实验报告,此文的基本内容参照 [1]Github/线性模型-上.ipynb 和 [2]线性模型-下.ipynb,检索时请按对应序号进行检索。
- 本实验编程语言为Python 3.10,使用Pycharm进行编程。
- 本实验报告目录标题级别顺序:一、 1. (1)
- 水平有限,难免有误,如有错漏之处敬请指正。
一、基于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(y∣x)=σ(Xw+b)其中 X ∈ R N × D \boldsymbol{X}\in \mathbb{R}^{N\times D} X∈RN×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(y∣x)表示预测的标签概率分布。训练目标是使得 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=1∑N(y(n)logy^(n)+(1−y(n))log(1−y^(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^+(1−y)Tlog(1−y^))其中 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} ∂w∂R(w,b)=−N1n=1∑Nx(n)(y(n)−y^(