源代码

import torch
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+=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[:,0],Y,1)
# plt.show()

def data_provider(data,label,batchsize):       #每次访问这个函数,就能提供一批数据
    length=len(data)
    indices=list(range(length))
    #我不能按顺序取, 把数据打乱
    random.shuffle(indices)
    for each in range(0,length,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=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))
print("真实的函数值是",true_w,true_b)
print("深度训练得到的参数值是",w_0,b_0)
idx=1
plt.plot(X[:,idx].detach().numpy(),X[:,idx].detach().numpy()*w_0[idx].detach().numpy()+b_0.detach().numpy())
plt.scatter(X[:,idx],Y,1)
plt.show()

代码解读

这段代码实现了一个完整的线性回归训练流程。简单来说,它先“假装”知道一个真实的函数( y=8.1x1+2x2+2x3+4x4+1.1y=8.1x1​+2x2​+2x3​+4x4​+1.1 ),生成一堆带噪声的数据点;然后让计算机“假装不知道”这个函数,通过梯度下降算法去学习和逼近这个函数。

以下是代码的逐段解读:

1. 数据生成 (creat_data)

这部分代码负责生成用于训练的数据集。

  • 输入:权重向量 w、偏置 b、数据数量 data_num
  • 过程
    • x = torch.normal(0, 1, (data_num, len(w))):生成 500 行数据,每行有 4 个特征(因为 w 的长度是 4),数据服从均值为 0、标准差为 1 的正态分布。
    • y = torch.matmul(x, w) + b:计算真实的标签值(矩阵乘法)。
    • noise = torch.normal(0, 0.01, y.shape):生成微小的随机噪声,模拟现实世界数据的不准确性。
    • y += noise:将噪声加到标签上。
  • 输出:特征矩阵 X 和标签向量 Y

2. 数据加载器 (data_provider)

在训练模型时,我们通常不一次性把所有数据(500条)喂给模型,而是分成一小批一小批(例如每批 16 条),这叫做小批量梯度下降

  • 功能:打乱数据顺序(防止模型学习到顺序偏差),然后按设定的 batchsize(16)依次返回数据。
  • yield:这是一个生成器,每次调用它会返回一个批次的数据,用完即停,节省内存。

3. 模型与损失函数

  • 模型 (fun):这就是我们要学习的函数 f(x)=wx+bf(x)=wx+b 。非常简单,就是矩阵乘法加偏置。
  • 损失函数 (maeloss):这里使用的是平均绝对误差。它计算预测值和真实值之间差的绝对值的平均数。损失越小,说明模型预测得越准。

4. 优化器 (sgd - 随机梯度下降)

这是模型“学习”的核心步骤。

  • 原理:利用 PyTorch 的自动求导机制(.grad),计算出损失函数对权重 w 和偏置 b 的梯度。
  • 更新:沿着梯度的反方向更新参数(para -= para.grad * lr),目的是让损失变小。
  • with torch.no_grad():告诉 PyTorch 这部分操作不需要记录计算图(因为这是人为修改参数,不是模型前向传播的一部分)。
  • 梯度清零 (para.grad.zero_()):非常重要!每次计算完梯度后必须清零,否则梯度会累积,导致更新方向错误。

5. 训练主循环

  • 初始化w_0 和 b_0 被随机初始化(requires_grad=True 表示需要计算梯度)。
  • 迭代:进行 50 轮(epochs)训练。
    • 对每一批数据,先计算预测值 pred_y
    • 计算这一批数据的损失 loss
    • 调用 loss.backward() 自动计算梯度。
    • 调用 sgd() 更新参数。
    • 累加损失用于打印。

6. 结果可视化

idx=1
plt.plot(X[:,idx].detach().numpy(),X[:,idx].detach().numpy()*w_0[idx].detach().numpy()+b_0.detach().numpy())
plt.scatter(X[:,idx], Y, 1)
plt.show()
  • 目的:画出学习结果。
  • idx=1:选择第 2 个特征(索引从 0 开始)进行可视化。
  • scatter:画出所有数据点(横轴是第 2 个特征的值,纵轴是真实标签 Y)。
  • plot:画出模型学到的针对该特征的线性关系
    • 注意:这里画的是 y=w2×x2+by=w2​×x2​+b 。虽然严格来说,完整的预测是 4 个特征共同作用的结果,但为了在 2D 图上展示,我们只取出了该特征的权重和偏置来画线。
    • detach().numpy():这是 PyTorch 的语法,用于将张量从计算图中分离并转换为 NumPy
    • 数组,以便用 Matplotlib 画图。

7.总结

为了方便记忆,可以把整个过程想象成一个工厂质检流水线

  1. 数据接口 (data_provider)
    送料员。他从仓库(XY)里随机抓一把零件(batchsize),送到工作台。
  2. 模型 (fun)
    工人。他拿着当前的工具(w_0b_0)去加工零件,产出一个预测结果。
  3. 损失函数 (maeloss)
    质检员。他对比工人的成品和标准样品,算出一个差错值(loss)。
  4. 自动求导 (backward)
    复盘分析。质检员分析刚才的差错,推导出是哪个工具(w 或 b)出了问题,以及问题有多大(grad)。
  5. SGD (sgd)
    修理工。他根据分析结果,把工具往正确的方向拧一下(-= grad * lr),并且把分析报告清空(zero_),准备下一轮质检。

代码中所有变量的类型详解

1. 数据生成阶段

变量名 类型 (Type) 形状 (Shape) 含义
true_w torch.Tensor (4,) 真实权重。包含 4 个数值的张量。
true_b torch.Tensor () 或 (1,) 真实偏置。通常是一个标量张量。
X torch.Tensor (500, 4) 特征矩阵。500 行数据,每行 4 个特征。
Y torch.Tensor (500,) 标签向量。500 个预测目标值。

2. 数据加载器 (data_provider) 内部

变量名 类型 (Type) 形状 (Shape) 含义
length int - 数据总量,数值为 500。
indices list 500 索引列表。内容是 [0, 1, ..., 499] 打乱后的结果。
get_indices list 16 当前批次的索引。从 indices 中切片取出的 16 个索引。
get_data torch.Tensor (16, 4) 取出的特征。根据索引从 X 中取出的 16 行特征数据。
get_label torch.Tensor (16,) 取出的标签。根据索引从 Y 中取出的 16 个标签。

3. 模型与训练阶段

变量名 类型 (Type) 形状 (Shape) 含义
w_0 torch.Tensor (4,) 待训练权重。初始随机值,带有梯度属性 (requires_grad=True)。
b_0 torch.Tensor () 待训练偏置。初始随机值,带有梯度属性。
batch_x torch.Tensor (16, 4) 喂给模型的输入。也就是 get_data
batch_y torch.Tensor (16,) 喂给模型的真实值。也就是 get_label
pred_y torch.Tensor (16,) 模型的预测输出。形状和 batch_y 一样。
loss torch.Tensor () 损失标量。一个代表当前误差大小的数字(0维张量)。

indices = list(range(length)) 详细拆解

假设你的数据总共有 length = 500 条。

  1. range(length)

    • range(500) 并不是一个列表,它是一个“范围对象”range object)。你可以把它理解成一个“生成数字的机器”,它知道如何产生 0, 1, 2, ..., 499 这些数字,但它目前还没有把它们全部存进内存。
    • 类型range object。
  2. list(...)

    • 这是一个转换函数。它把上面那个“机器”实际运行一遍,把产生的所有数字抓出来,放进一个列表(List)这种数据结构里。
    • 结果[0, 1, 2, 3, ..., 498, 499]
    • 类型list
  3. 为什么要这么做?

    • 目的:为了打乱顺序
    • 代码里紧接着有一句 random.shuffle(indices)。Python 的列表(list)是支持“原地打乱”这个操作的,但是 range 对象或者 Tensor 是不支持直接打乱的。
    • 逻辑:我们不直接打乱原始数据 X 和 Y(那样太耗时),而是打乱代表位置的“号码牌” indices。打乱后,indices 可能变成了 [2, 499, 100, 88, ...]
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐