首先,由于我们没有原始数据,所以要先生成原始数据,这里使用使用random库生成随机的数据

def creat_data(w, b, data_num):  
    x = torch.normal(0, 1, (data_num, len(w)))  
    y = torch.matmul(x, w) + b  
    noise = torch.normal(0, 0.01, y.shape)  
    y += noise
    return x, y

这里定义了一个类create_data用于生成随机数据
x是特征(feature),y是标签(label)

x = torch.normal(0, 1, (data_num, len(w)))中,0表示平均值为0,1表示方差为1,()中的是x的形状

根据矩阵运算,x的行数是数据的个数,列数是权重的个数

利用公式得到y,这里需要注意,这里的x是一个形状为(500,4)的张量,w是一个形状为(4,)的张量,而b是一个零维的标量,这里利用了pytorch的广播规则,使得代码更简洁。

下面我们按照数据量num = 500生成原始数据

num = 500
true_w = torch.tensor([8.1, 2, 2, 4])
true_b = torch.tensor(1.1)
X, Y = creat_data(true_w, true_b, num)

注意这里我们给出了 true_w 和 true_b,这两个参数表示的是真实的权重和偏差,但在实际应用中是根本不存在这两个参数,或者说即使存在也无法确定其唯一性,我们找的是一组“最优的参数”。

而在此处,我们给出这两个参数只是为了生成一组“有明确规律、可控、带噪声”的合成数据(synthetic data)

这里我们可以画出我们生成的数据

plt.scatter(X[:, 3], Y, 1)  
plt.show()

这里涉及到plt.scatter()的使用,这个函数是画散点图,但是由于x是多维的,我们只能取其中某个维度与y进行比较,最后的1表示点的大小

下面一步,是设计一个数据批量处理生成器,将整个数据集打乱后,按批量提供数据。

def data_provider(data, label, batchsize):  # 每次访问这个函数就能提供一批数据
    length = len(label)  # 求出有多少个样本
    indices = list(range(length))  # indices是一个0-500按顺序存储的列表
    # 为了保证数据的普适性和随机性,要把数据打乱
    random.shuffle(indices)

    for each in range(0, length, batchsize):  # batchsize是这一批数据的数量,每次从0到500这么多数据中取batchsize这么多数据
        get_indices = indices[each: each + batchsize]
        get_data = data[get_indices]
        get_label = label[get_indices]

        yield get_data, get_label  # 有存档点的return

那么为什么要有这样一个数据生成器呢?为什么要分成小批次提供数据呢?而不是一次用全部数据,或一次只用一个样本?

这里涉及到了梯度下降的知识

在这段代码中,我们使用了 yield 函数,而没有使用 return 。这是因为,在普通函数中,用 return 返回结果后,函数就结束并销毁。而在 yield 函数中,函数不会立刻执行完,每次遇到yield 时,就暂停运行,把值吐出来,下次调用时,就从上次暂停的地方继续执行,知道函数自然结束或是再次遇到yield。

含有 yield 的函数,称为 生成器函数(generator function),调用它会返回一个 生成器对象(generator object),这个对象是可迭代的(iterable)

什么意思呢?我们的目的是不断从这批数据中抽出一小批次数据,使用了 yield 之后,就可以满足这一要求,按需提供。就像一个“自助售货机”,我们需要多少就拿多少,机器会保存内部状态。等我们下次调用这个函数时,他会在上次调用的状态下运行。

前面的步骤都是在对数据的处理,下面我们就要开始涉及我们的模型函数了

首先是线性模型的前向计算函数,得到预测值pred_y

def fun(x, w, b):
    pred_y = torch.matmul(x, w) + b
    return pred_y

然后我们设计损失函数,这里使用的是平均绝对损失

def maeLoss(pre_y, y):  
    return torch.sum(abs(pre_y - y)) / len(y)

下面开始编写梯度下降优化器,这是深度学习循环中参数更新的核心步骤

def sgd(paras, lr):  # 梯度下降
    with torch.no_grad():  # 属于这句代码的部分,不计算梯度
        for para in paras:
            para -= para.grad * lr
            para.grad.zero_()  # 使用过的梯度,归0

paras:一个包含可学习参数的列表

lr:学习率(learning rate),控制每次更新的步长

在 Python 中,with 是一个上下文管理协议(Context Management Protocol) 的关键字,它的核心作用是:安全、自动地管理资源的获取与释放,确保即使发生异常,资源也能被正确清理。

这里 torch.no_grad() 是一个上下文管理器,它的作用是:

进入with块时 (__enter__)临时禁用 PyTorch 的自动求导(autograd)
退出 with 块时 (__exit__)自动恢复之前的梯度计算状态

由于torch具有自动求导机制,在前向计算的过程中,torch会自动记录计算过程

但是这里梯度下降的过程是在更新参数,而并非计算过程,所以我们用

with torch.no_grad(): 这句代码表示下面的过程不需要计算梯度,也就无需记录过程

这就相当于一个餐馆的学徒在学习大厨做菜的过程中,大厨做菜就是前向过程,学徒会记录这个前向过程(盐放多少,糖放多少等等),但是最后做出来的味道存在偏差,需要调整配方,而这个调整的过程是不需要记录的,需要记录的只是最后的配方。

这里需要注意,torch的自动求导机制并不是说在前向过程中完成了求导,它只是记录了过程,当我们调用 .backward() 时它才会根据记录的过程实现求导。

梯度下降的更新公式如下

新参数 = 旧参数 - 学习率 * 梯度
para.grad.zero_() 的作用是“擦掉旧笔记,准备记新建议

因为pytorch会默认累计梯度,如果不清零,下次 .backward() 会把新梯度 + 旧梯度加在一起,导致错误的更新。

那么为什么会有累计梯度的存在呢?这是为了支持“小批量合成大批次”的训练技巧,有时候我们的显存不够,但是想要使用更大的 batchsize(例如batchsize = 64),那么就可以在前向过程中用batchsize = 16,连续进行4次不更新参数、不清零梯度,最后将这4次的梯度累计起来,再更新参数,也就相当于实现了batchsize = 64

至此,我们就写完了函数的部分

下面我们设置一下学习率lr = 0.03,并设置一个初始的参数(w_0、b_0)

w_0 = torch.normal(0, 0.01, true_w.shape, requires_grad=True)

这句代码是从正态分布中随机采样一组数,作为模型的初始权重(weights),均值为0,标准差为0,形状与true_w一致,注意这里关键的参数 requires_grad=True,这是在告诉PyTorch,这个张量是我模型的可训练参数。后续计算中,只要用到 w_0,PyTorch 就会记录操作,并在 .backward() 时自动计算它的梯度,训练时你就能用 w_0.grad 来更新它

后面同理设置偏差

true_b = torch.tensor(1.1)

整体就搭建了一个可训练的线性模型

最后,终于开始了模型的训练

epochs = 50  # 训练的轮数

for epoch in range(epochs):
    data_loss = 0
    for batch_x, batch_y in data_provider(X, Y, batchsize):
        pred_y = fun(batch_x, w_0, b_0)
        loss = maeLoss(pred_y, batch_y)
        loss.backward()  # 梯度回传
        sgd([w_0, b_0], lr)  # 更新模型
        data_loss += loss

    print("epoch %03d: loss: %.6f" % (epoch, data_loss.detach()))

两层循环,外层大循环是训练的轮数,每一轮都完整过一遍数据,首先初始化本轮的损失为0,然后开始本轮的训练,每次利用 data_provider() 提供一批数据 batch_x ,进行前向过程计算预测值 pred_y,计算损失,反向传播计算梯度,更新模型,最后把这一小批数据的损失加入这一轮的总损失当中。

需要注意的是,PyTorch会隐式地把梯度“附着”在参数张量上,具体来说,PyTorch 在调用 loss.backward() 时,自动把梯度存到了每个 requires_grad=True 的张量的 .grad 属性里

所以更新模型的函数无需输入这批数据的梯度。

到此为止,我们整个模型就已经书写完毕了,我们通过画图看一下模型的拟合效果

这里选取idx = 0,也就是特征0与标签绘制图形

可以看到模型的拟合效果还不错

完整模型如下

import torch
import matplotlib
matplotlib.use('Qt5Agg')
import matplotlib.pyplot as plt  # 画图

import random  # 随机


def creat_data(w, b, data_num):  # 随机生成数据
    x = torch.normal(0, 1, (data_num, len(w)))  # 根据矩阵乘法,这个数据张量的行数就是数据的数量,列数是权重的数量
    y = torch.matmul(x, w) + b  # matmul矩阵相乘

    noise = torch.normal(0, 0.01, y.shape)  # 生成噪声,噪声是加在y上的,所以噪声的维度应该与y一致
    y += noise
    return x, y


num = 500

true_w = torch.tensor([8.1, 2, 2, 4])
true_b = torch.tensor(1.1)

X, Y = creat_data(true_w, true_b, num)

plt.scatter(X[:, 3], Y, 1)  # 画散点图,点的大小是1,取X的某一列与Y做对比,
plt.show()

def data_provider(data, label, batchsize):  # 每次访问这个函数就能提供一批数据
    length = len(label)  # 求出有多少个样本
    indices = list(range(length))  # indices是一个0-500按顺序存储的列表
    # 为了保证数据的普适性和随机性,要把数据打乱
    random.shuffle(indices)

    for each in range(0, length, batchsize):  # batchsize是这一批数据的数量,每次从0到500这么多数据中取batchsize这么多数据
        get_indices = indices[each: each + batchsize]
        get_data = data[get_indices]
        get_label = label[get_indices]

        yield get_data, get_label  # 有存档点的return

batchsize = 16


# for batch_x, batch_y in data_provider(X, Y, batchsize):
#     print(batch_x, batch_y)
#     break

def fun(x, w, b):
    pred_y = torch.matmul(x, w) + b
    return pred_y


def maeLoss(pre_y, y):  # 损失函数
    return torch.sum(abs(pre_y - y)) / len(y)


def sgd(paras, lr):  # 梯度下降
    with torch.no_grad():  # 属于这句代码的部分,不计算梯度
        for para in paras:
            para -= para.grad * lr
            para.grad.zero_()  # 使用过的梯度,归0


lr = 0.03
w_0 = torch.normal(0, 0.01, true_w.shape, requires_grad=True)
b_0 = torch.tensor(0.01, requires_grad=True)
print(w_0, b_0)

epochs = 50  # 训练的轮数

for epoch in range(epochs):
    data_loss = 0
    for batch_x, batch_y in data_provider(X, Y, batchsize):
        pred_y = fun(batch_x, w_0, b_0)
        loss = maeLoss(pred_y, batch_y)
        loss.backward()  # 梯度回传
        sgd([w_0, b_0], lr)  # 更新模型
        data_loss += loss

    print("epoch %03d: loss: %.6f" % (epoch, data_loss.detach()))

print("真实的函数值是", true_w, true_b)
print("训练得到的参数值是", w_0, b_0)

idx = 0
plt.plot(X[:, idx].detach().numpy(), X[:, idx].detach().numpy() * w_0[idx].detach().numpy() + b_0.detach().numpy())
plt.scatter(X[:, idx].detach().numpy(), Y, 1)
plt.show()

总结一下,这个模型实现了:

  1. 数据生成(含噪声)
  2. 模型定义(线性函数)
  3. 损失函数(MAE)
  4. 优化算法(SGD)
  5. 训练循环(mini-batch + epoch)
  6. 结果评估(参数对比 + 可视化)

更多推荐