pytorch 基础练习及螺旋数据分类


一、pytorch 基础练习

主要学习了如何使用torch定义数据以及对数据的一些操作
示例代码中进行点积运算(m @ v)要求数据类型是float,因此前面v定义的时候需要指定dtype,如下所示,

# Create tensor of numbers from 1 to 5
# 注意这里结果是1到4,没有5,即前闭后开
v = torch.arange(1, 5,dtype=torch.float)
print(v)
# tensor([1., 2., 3., 4.])
# Scalar product 点积
# 需要float类型 
m @ v
# tensor([30., 60.])

二、螺旋数据分类(sprial classification)

配套的文字版讲解理论,地址为: 中文讲解 英文讲解

2.1 生成用于分类的数据

1.下面代码是下载绘图函数到本地。(画点的过程中要用到里面的一些函数)

!wget https://raw.githubusercontent.com/Atcold/pytorch-Deep-Learning/master/res/plot_lib.py

2.引入基本的库,然后初始化重要参数

import random
import torch
from torch import nn, optim
import math
from IPython import display
from plot_lib import plot_data, plot_model, set_default

# 因为colab是支持GPU的,torch 将在 GPU 上运行
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
print('device: ', device)

# 初始化随机数种子。神经网络的参数都是随机初始化的,
# 不同的初始化参数往往会导致不同的结果,当得到比较好的结果时我们通常希望这个结果是可以复现的,
# 因此,在pytorch中,通过设置随机数种子也可以达到这个目的
seed = 12345
random.seed(seed)
torch.manual_seed(seed)

N = 1000  # 每类样本的数量
D = 2  # 每个样本的特征维度
C = 3  # 样本的类别
H = 100  # 神经网络里隐层单元的数量

3.初始化 X 和 Y。 X 可以理解为特征矩阵,每一行是一个样本,每一列代表一个特征,Y可以理解为样本标签。 结合代码可以看到,X的为一个 NxC 行, D 列的矩阵。C 类样本,每类样本是 N个,所以是 N*C 行。每个样本的特征维度是2,所以是2列。

在 python 中,调用 zeros 类似的函数,第一个参数是 y方向的,即矩阵的行;第二个参数是 x方向的,即矩阵的列,大家得注意下,不要搞反了。下面结合代码看看 3000个样本的特征是如何初始化的。

X = torch.zeros(N * C, D).to(device)
Y = torch.zeros(N * C, dtype=torch.long).to(device)
for c in range(C):
    index = 0
    t = torch.linspace(0, 1, N) # 在[0,1]间均匀的取10000个数,赋给t
    # 下面的代码不用理解太多,总之是根据公式计算出三类样本(可以构成螺旋形)
    # torch.randn(N) 是得到 N 个均值为0,方差为 1 的一组随机数,注意要和 rand 区分开
    inner_var = torch.linspace( (2*math.pi/C)*c, (2*math.pi/C)*(2+c), N) + torch.randn(N) * 0.2
    
    # 每个样本的(x,y)坐标都保存在 X 里
    # Y 里存储的是样本的类别,分别为 [0, 1, 2]
    for ix in range(N * c, N * (c + 1)):
        X[ix] = t[index] * torch.FloatTensor((math.sin(inner_var[index]), math.cos(inner_var[index])))
        Y[ix] = c
        index += 1

print("Shapes:")
print("X:", X.size())
print("Y:", Y.size())

运行结果:

Shapes:
X: torch.Size([3000, 2])
Y: torch.Size([3000])

看一下X和Y:

# X
tensor([[-0.0000e+00,  0.0000e+00],
        [ 1.0158e-04,  9.9583e-04],
        [-1.1208e-03,  1.6589e-03],
        ...,
        [ 9.0658e-01, -4.1727e-01],
        [ 8.7314e-01, -4.8540e-01],
        [ 7.9693e-01, -6.0407e-01]], device='cuda:0')
# Y
tensor([0, 0, 0,  ..., 2, 2, 2], device='cuda:0')

4.绘图

# visualise the data
plot_data(X, Y)

image

2.2 构建分类模型

这里记录下用 Pytorch 训练神经网络,包含的5个基础步骤:

preds = model(inputs)             ## inference
loss = criterion(preds, targets)  ## 求解loss
optimizer.zero_grad()             ## 梯度清零
loss.backward()                   ## 反向传播求解梯度
optimizer.step()                  ## 更新权重参数

image

2.2.1 构建线性模型分类

1.定义模型并训练

learning_rate = 1e-3
lambda_l2 = 1e-5

# nn 包用来创建线性模型
# 每一个线性模型都包含 weight 和 bias
model = nn.Sequential(
    nn.Linear(D, H),
    nn.Linear(H, C)
)
model.to(device) # 把模型放到GPU上

# nn 包含多种不同的损失函数,这里使用的是交叉熵(cross entropy loss)损失函数
criterion = torch.nn.CrossEntropyLoss()

# 这里使用 optim 包进行随机梯度下降(stochastic gradient descent)优化
optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate, weight_decay=lambda_l2)

# 开始训练
for t in range(1000):
    # 把数据输入模型,得到预测结果
    y_pred = model(X)
    # 计算损失和准确率
    loss = criterion(y_pred, Y)
    score, predicted = torch.max(y_pred, 1)
    acc = (Y == predicted).sum().float() / len(Y)
    print('[EPOCH]: %i, [LOSS]: %.6f, [ACCURACY]: %.3f' % (t, loss.item(), acc))
    display.clear_output(wait=True)

    # 反向传播前把梯度置 0 
    optimizer.zero_grad()
    # 反向传播优化 
    loss.backward()
    # 更新全部参数
    optimizer.step()

输出:

[EPOCH]: 999, [LOSS]: 0.861541, [ACCURACY]: 0.504

对上面的一些关键函数进行说明:

使用 print(y_pred.shape) 可以看到模型的预测结果,为[3000, 3]的矩阵。每个样本的预测结果为3个,保存在 y_pred 的一行里。值最大的一个,即为预测该样本属于的类别。但是计算交叉熵损失时并没有用这个预测类别predicted与真实标签Y计算,而是将y_pred与真实标签Y进行计算,我的理解在最后的后记中

score, predicted = torch.max(y_pred, 1) 是沿着第二个方向(即X方向)提取最大值。最大的那个值存在 score 中,所在的位置(即第几列的最大)保存在 predicted 中。

每一次反向传播前,都要把梯度清零,原因可以参考知乎回答
2.分类结果

# Plot trained model
print(model)
plot_model(X, Y, model)

image
上面使用 print(model) 把模型输出,可以看到有两层:

第一层输入为 2(因为特征维度为主2),输出为 100;
第二层输入为 100 (上一层的输出),输出为 3(类别数)
从上面图示可以看出,线性模型的准确率最高只能达到 50% 左右,对于这样复杂的一个数据分布,线性模型难以实现准确分类。

2.2.2 构建两层神经网络分类

1.定义模型并训练

learning_rate = 1e-3
lambda_l2 = 1e-5

# 这里可以看到,和上面模型不同的是,在两层之间加入了一个 ReLU 激活函数
model = nn.Sequential(
    nn.Linear(D, H),
    nn.ReLU(),
    nn.Linear(H, C)
)
model.to(device)

# 下面的代码和之前是完全一样的,这里不过多叙述
criterion = torch.nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate, weight_decay=lambda_l2) # built-in L2

# 训练模型,和之前的代码是完全一样的
for t in range(1000):
    y_pred = model(X)
    loss = criterion(y_pred, Y)
    score, predicted = torch.max(y_pred, 1)
    acc = ((Y == predicted).sum().float() / len(Y))
    print("[EPOCH]: %i, [LOSS]: %.6f, [ACCURACY]: %.3f" % (t, loss.item(), acc))
    display.clear_output(wait=True)
    
    # zero the gradients before running the backward pass.
    optimizer.zero_grad()
    # Backward pass to compute the gradient
    loss.backward()
    # Update params
    optimizer.step()

输出:

[EPOCH]: 999, [LOSS]: 0.178408, [ACCURACY]: 0.949

2.分类结果

# Plot trained model
print(model)
plot_model(X, Y, model)

image
在两层神经网络里加入 ReLU 激活函数以后,分类的准确率得到了显著提高,这是因为添加relu隐藏层后,相当于进行一次非线性变换,在新空间中线性可分了。

三、后记

1.关于nn.linear()可以查看这个博客 nn.Linear(20,30)理解是输入有20个特征,构建30个神经元,每个神经元用20个参数与特征关联

2.关于计算交叉熵损失时并没有用预测类别predicted与真实标签Y计算,而是将y_pred与真实标签Y进行计算的理解

不能直接用预测类别predicted与真实标签Y计算损失,因为假设样本真实类别是3,预测类别为1和为2对于损失而言应该是一致的(都是预测错误),但是3-1与3-2的距离是不一样的(这种距离是因为我们为类别编码的顺序导致的),在机器学习中需要计算两者之间的距离(欧氏距离),这种普通的编码方式并不能表示清楚距离,这也是为什么要采用独热编码的原因,独热编码可以保证任意两种类别之间的距离是相同的。独热编码

然后是为什么调用CrossEntropyLoss函数传入y_pred与真实标签Y就可以,CrossEntropyLoss函数内部实现时包含了LogSoftMax和 negative loglikelihood NLLLoss,传入的参数只需要是预测后的数据及正确的标签,该函数会对预测数据进行logSoftMax、对真实标签进行独热编码再计算NLLloss,可以参考这个博客

解读一下计算NLLloss损失的过程:
1.生成模拟样本及标签,对标签作 one_hot 编码

import torch.nn.functional as F
import torch
data = torch.randn(5, 5)  # 随机生成一组数据
target = torch.tensor([0, 2, 4, 3, 1])  # 标签
one_hot = F.one_hot(target).float()  # 对标签作 one_hot 编码

查看data、target、one_hot的结果

#data
tensor([[ 2.4916,  1.4547,  0.5978, -0.5240,  1.3889],
        [ 0.7268, -0.0692,  0.2602, -1.3590,  0.6856],
        [ 0.4045, -0.5785,  1.2365, -0.0354, -1.5054],
        [ 0.4664,  0.2799, -0.8431,  0.2577,  0.1388],
        [-0.6344, -0.0642, -1.2059,  1.4325, -0.1954]])
#target
tensor([0, 2, 4, 3, 1])
#one_hot
tensor([[1., 0., 0., 0., 0.],
        [0., 0., 1., 0., 0.],
        [0., 0., 0., 0., 1.],
        [0., 0., 0., 1., 0.],
        [0., 1., 0., 0., 0.]])

2.手动实现 NLLLoss() 函数功能


如果直接调用此函数不需要进行独热编码,这里只是展示截图中的公式,然后依据此公式计算该损失函数:

2.1假设上述data是已经经过模型计算得到的y_pred了,计算logsoftmax

exp = torch.exp(data)  # 以e为底作指数变换
sum = torch.sum(exp, dim=1).reshape(-1, 1)  # 按行求和
softmax = exp / sum  # 计算 softmax()
log_softmax = torch.log(softmax)  # 计算 log_softmax()

log_softmax结果:

log_softmax:
tensor([[-0.6345, -1.6714, -2.5283, -3.6500, -1.7372],
        [-1.1512, -1.9473, -1.6178, -3.2370, -1.1924],
        [-1.4961, -2.4791, -0.6641, -1.9360, -3.4060],
        [-1.2898, -1.4763, -2.5993, -1.4984, -1.6174],
        [-2.5483, -1.9781, -3.1197, -0.4813, -2.1093]])

2.2计算one_hot * log_softmax得到:

tensor([[-0.6345, -0.0000, -0.0000, -0.0000, -0.0000],
        [-0.0000, -0.0000, -1.6178, -0.0000, -0.0000],
        [-0.0000, -0.0000, -0.0000, -0.0000, -3.4060],
        [-0.0000, -0.0000, -0.0000, -1.4984, -0.0000],
        [-0.0000, -1.9781, -0.0000, -0.0000, -0.0000]])

可以看到这个结果相当于按照one_hot中真实的类别把logsoftmax的结果筛选出来,因为logsoftmax结果的每一行中的最大值是我们预测的类别,如果预测类别和真实类别是一样的,那么筛选出来的结果就是每一行中的最大值(此绝对值是每一行最小),按照NLLloss公式对这些数据求和除以N取相反数,则这个loss将会是最小的。反之如果某一行预测类别和真实类别不一样,那么这一行筛选出的数据不是这一行的最大值(此绝对值不是这一行的最小),那么最终的loss肯定不是最小的。
2.3计算nllloss

nllloss = -torch.sum(one_hot * log_softmax) / target.shape[0]  # 标签乘以激活后的数据,求平均值,取反


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM