考完研一段时间了,想着学点东西,看看能不能搞个项目应付一下复试。但作为深度学习小白,只能慢慢学了。今天学习的是手工线性回归的代码,浅浅记录一下。

线性回归的表示就是简单的一元函数y = w*x + b。

项目的核心目标

1 实现一个从零开始的线性回归算法
2 演示机器学习中参数估计的基本原理
3 验证模型能否准确恢复已知的真实参数

项目的具体功能

数据生成与批量处理

定义一个函数:create_data用于生成数据

def create_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

函数:data_provider是批量数据生成器

def data_provider(data, label, batchsize):       #每次访问这个函数, 就能提供一批数据
    length = len(label) # 数据集的长度
    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

疑惑

学习过程中,不明白pytorch的作用,上网查了一下深度学习中使用pytorch而不是使用numpy的原因:

1、保持数据类型一致,如果混用类型会很麻烦

2、pytorch支持GPU加速,而numpy只支持CPU,效率很重要

3、自动梯度追踪

4、统一的深度学习工作流,保持代码一致性

torch.normal(0, 1, (data_num, len(w)))

torch.normal是数学高斯分布的计算机实现,也就是正态分布,因为考研学过概率论,所有我就理解为在N(μ, σ²) 中抽样的过程,这里就是在标准正态分布中采样,采样的样本数为data_num,而每个样本的特征数为len(w)。

noise = torch.normal(0, 0.01, y.shape)
y += noise

添加高斯噪声模拟真实世界的随机误差

  • 噪声:均值0,标准差0.01

  • 噪声形状与 y 相同

  • 最终 y 包含了随机扰动

yield get_data, get_label

yield的作用与return相似,都是用于返回数据,但是有一定的区别。两者可类比为买东西付款,return就类似全款付款,一次性返回,而yield就类似分期付款,一次返回一部分。在函数中,for循环每次遍历都会通过yield返回本次的get_data和get_label。

模型与损失函数

模型:线性预测函数y=w*x+b

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)

疑惑

MAE

计算MAE (Mean Absolute Error)

$$ \text{MAE} = \frac{1}{n} \sum_{i=1}^{n} |y_i - \hat{y}_i| $$

拓展:MAE与MSE的区别

损失函数公式特点
MAE$$\frac{1}{n}\sum \left| y - \hat{y} \right|$$对异常值不敏感
MSE$$\frac{1}{n}\sum (y - \hat{y})^2$$惩罚大误差更严重

优化算法

函数sgd为随机梯度下降优化器

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

疑惑

with torch.no_grad()

torch.no_grad()意味着让张量不追踪梯度,它与张量.requires_grad=FALSE的作用有点类似,但是它们之间有一定的区别:

特性torch.no_grad()requires_grad=False
作用对象代码块内的所有运算单个张量
作用范围临时的、局部的永久的、全局的
是否可恢复✅ 自动恢复❌ 需手动改
使用场景推理、参数更新定义常量、输入数据

para -= para.grad * lr

para -= para.grad * lr 与 para = para - para.grad * lr 的区别:

操作效果是否修改原对象引用是否改变
para -= x就地修改✅ 是❌ 不变
para = para - x创建新对象❌ 否✅ 改变

para.grad.zero_()

para.grad.zero_()梯度归零,下划线表示就地操作。为什么要梯度清零呢?

假设不清零,第1次迭代:w.grad = 5,第2次反向传播:w.grad = w.grad + 3 = 8(累加!)
,错误的梯度会导致参数更新错误。

训练流程

参数初始化

lr = 0.03 # 学习率
w_0 = torch.normal(0, 0.01, true_w.shape, requires_grad=True)   #这个w需要计算梯度
b_0 = torch.tensor(0.01, requires_grad=True)
print(w_0, b_0)

lr是指学习率(Learning Rate),它的作用是控制参数更新的步长,若设置过大,可能错过最优解,导致震荡;若设置过小,会导致收敛速度慢,训练时间长。

对参数w_0使用小的随机值进行初始化的作用是避免梯度消失/爆炸以及打破对称性(如果初始化为0,所有神经元会学到相同的特征)

训练循环架构

epochs = 50

for epoch in range(epochs):
    data_loss = 0
    for batch_x, batch_y in data_provider(X, Y, batchsize):
        # 单个批次的训练步骤
        # 1. 前向传播
        pred_y = fun(batch_x,w_0, b_0)
        # 2. 计算损失
        loss = maeLoss(pred_y, batch_y)
        # 3. 反向传播
        loss.backward()
        # 4. 参数更新
        sgd([w_0, b_0], lr)
        data_loss += loss # 累积每个batch的损失

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

epochs = 50的意思是训练的轮次为50轮。

外部循环的次数就是遍历整个数据集的次数,每遍历一次(每一个epoch)都会打乱一次数据集。

数据集里面包含了很多样本,数据集又可以分为多个批次,而batchsize是指每一个批次的样本数量,因此数据集可分为(样本数量/batchsize)个批次。内部循环的作用就是对每个批次进行训练,步骤分为前向传播,计算损失,反向传播,参数更新。

完整的训练流程图

开始第1个Epoch
    ↓
打乱数据(data_provider内部)
    ↓
┌──────────────────────────┐
│  取第1批数据(batch_x, batch_y) │
├──────────────────────────┤
│  1. 前向传播:pred_y = fun(...)  │
│  2. 计算损失:loss = maeLoss(...) │
│  3. 反向传播:loss.backward()     │
│  4. 参数更新:sgd([w_0, b_0], lr) │
└──────────────────────────┘
    ↓
取第2批数据...
    ↓
取第3批数据...
    ↓
...(直到所有批次处理完)
    ↓
打印本轮总损失
    ↓
开始第2个Epoch...

结果输出

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

真实的函数值是 tensor([8.1000, 2.0000, 2.0000, 4.0000]) tensor(1.1000)
训练得到的参数值是 tensor([8.1129, 2.0121, 1.9975, 4.0061], requires_grad=True) tensor(1.0900, requires_grad=True)

疑惑

loss.backward()

loss.backward()的作用:

1、自动求导:沿计算图反向传播,计算损失对每个参数的梯度

        ∂loss/∂w_0 → 保存到 w_0.grad
        ∂loss/∂b_0 → 保存到 b_0.grad

2、梯度含义:

正梯度:参数增加会使损失增加 → 应该减小参数

负梯度:参数增加会使损失减小 → 应该增加参数

可视化反向传播:

        loss
         ↑
    maeLoss计算
         ↑
       pred_y
         ↑
   matmul + bias
       ↗   ↖
     w_0    b_0  ← 梯度沿箭头反向流动

可视化结果

更多推荐