Skip to content

深度学习

感知机

感知机的发展

感知机,是给定一组输入数据,自动找到一条决策边界,讲不通类别的数据正确区分开来的机器。

它是人类历史上第一个真正落地的可学习机器方案,让机器不再依赖人工编写的规则,而是自动从数据中学会分类。

虽然诞生于1957年,但它麻雀虽小,五脏俱全。完整包含了神经网络所有核心要素的雏形:输入、权重、求和、激活、误差反馈、参数更新。真正理解了感知机,就相当于掌握了神经网络的完整骨架。


思想起源:M-P 神经元(1943)

麦卡洛克与皮茨 联合发表了论文。

  • 将生物神经元抽象位一个简单的逻辑单元。
  • 同时接收多个兴奋或抑制信号。
  • 当兴奋信号的总量超过某个阈值时,神经元激活并输出信号,否则保持沉默。
    缺陷:权重必须由人工设定,机器本身无法学习。它只是一个精巧的逻辑装置。

思想起源:赫布理论(1949)

转机出现在1949年,心理学家 唐纳德·赫布出版了《行为的组织》。

  • 如果两个神经元经常同时激活,它们之间的连接就会不断增强。
  • 反之,如果不常同时激活,连接则会逐渐减弱。
  • 这是人类历史上第一次从学习机制的角度解释神经连接的变化。
  • 第一次给出了一个关键问题的生物学依据————权重是可以改变的。

第一台会学习的机器(1957)

弗兰克·罗森布拉特 在M-P神经元的基础上,加入了至关重要的创新:让权重通过错误反馈自动调整。

  • 机器每做出一次错误预测,权重就自动修正一次。
  • 如此反复,直到学会正确分类。
  • 1958年,他亲手造出了一台真实的物理机器———— Mark I Perceptron。

让机器学会分类

感知机的工作原理非常简单:对输入做加权求和,与阈值比较做出判断,根据错误反馈微调修正权重,直到学会正确分类。

  • 如果把感知机的激活函数(阶跃函数)改成Sigmoid就是逻辑回归!

感知机执行步骤

感知机执行步骤

第一步:输入与加权

接收一组输入信号xi,每一个输入代表数据的某个特征。
每一个输入对应权重wi,权重的大小代表这个特征对最终判断的影响程度。

  • 权重越大,这个特征越关键。
  • 权重接近零,说明这个特征几乎可以忽略。
  • 这正是对赫布理论的直接呼应————连接越强,影响越大。

第二步:求和与判断

把加权后的输入全部加起来,得到数值z:

z=w1x1+w2x2+w3x3+b

其中b是偏置项(Bias),相当于对判断基准线的微调。得到z之后进行激活判断:

y^={1若 z00若 z<0

第三步:错误与修正

每做出一次预测,感知机就把结果和正确答案对比。如果预测正确,什么都不用做;如果预测错误,就按照下面的规则调整权重:

wiwi+η(yy^)xi
  • y是正确答案,y^是感知机的预测,两者之差是误差信号。
  • xi是特征的输入值,误差要按照输入的大小来分配责任。
  • η是学习率,控制每次调整的幅度。


案例:预测身材偏胖还是偏瘦

为了让感知机的工作过程更直观,我们来做一个具体的案例:判断一个人的身材是偏胖还是偏瘦。

  • 输入特征:收集一批人的数据,提取两个特征:x1(身高)和x2(体重)。
  • 数据处理:消除身高和体重的量级差异,将数据压缩到0~1之间(归一化),保证学习稳定。
  • 目标标签:偏瘦标记为0,偏胖标记为1。
  • 训练过程:让感知机从对特征一无所知(权重为0)开始,不断看数据、预测、对比、修正权重。

当感知机对所有样本都能给出正确的预测时,它就找到了那条完美的“决策边界”。

| 序号 | 特征1 (身高) | 特征2 (体重) | 标签 (体型) |
|------|-------------|-------------|------------|
|  1   |         175 |          71 | 偏瘦         |
|  2   |         160 |          63 | 偏胖         |
|  3   |         172 |          69 | 偏瘦         |
|  4   |         162 |          65 | 偏胖         |
|  5   |         170 |          66 | 偏瘦         |
|  6   |         164 |          67 | 偏胖         |
|  7   |         168 |          64 | 偏瘦         |
|  8   |         166 |          69 | 偏胖         |
|  9   |         165 |          62 | 偏瘦         |
| 10   |         168 |          72 | 偏胖         |

数据归一化

原始数据中身高的数值在160到170之间,而体重的数值在45到85之间,两个特征的量级差距很大。如果直接把原始数值塞进感知机,身高对权重更新的影响会远远大于体重,导致学习过程不稳定。因此,我们先对数据做一次归一化处理,把所有特征值统一压缩到0到1之间。

归一化后的数据 (X):
------------------------------------------------------------
| 序号 | 特征1 (身高) | 特征2 (体重) |
|------|--------------|--------------|
|  1   |       1.0000 |       0.9000 |
|  2   |       0.0000 |       0.1000 |
|  3   |       0.8000 |       0.7000 |
|  4   |       0.1333 |       0.3000 |
|  5   |       0.6667 |       0.4000 |
|  6   |       0.2667 |       0.5000 |
|  7   |       0.5333 |       0.2000 |
|  8   |       0.4000 |       0.7000 |
|  9   |       0.3333 |       0.0000 |
| 10   |       0.5333 |       1.0000 |

验证归一化范围:
  特征1范围: [0.0000, 1.0000]
  特征2范围: [0.0000, 1.0000]
============================================================

定义感知机类

__init__负责设定超参数,fit是训练的入口。权重初始化为0,意味着感知机一开始对两个特征一无所知。每轮遍历所有样本,犯错就更新权重,没错就跳过。当某一轮所有样本都预测正确时,训练结束。

python
class Perceptron:
    def __init__(self, learning_rate=1.0, n_iterations=100):
        self.lr = learning_rate             # 学习率
        self.n_iterations = n_iterations    # 最大训练轮数
        self.weights = None                 # 权重 w
        self.bias = None                    # 偏置 b
        self.errors_per_epoch = []          # 记录每轮错误数

    def fit(self, X, y):
        n_samples, n_features = X.shape

        # 权重和偏置全部初始化为0
        self.weights = np.zeros(n_features)
        self.bias = 0
        self.errors_per_epoch = []

        for epoch in range(self.n_iterations):
            errors = 0      # 本轮错误计数

            for idx in range(n_samples):
                x_i = X[idx]
                y_true = y[idx]

                # 1. 计算加权和: z = w1 * x1 + w2 * x2 + b
                z = np.dot(self.weights, x_i) + self.bias

                # 2. 激活判断: z >= 0 输出1,否则输出0
                y_pred = 1 if z >= 0 else 0

                # 3. 计算误差
                error = y_true - y_pred

                # 4. 如果预测错误,更新权重和偏置
                if error != 0: 
                    self.weights += self.lr * error * x_i   # 更新权重
                    self.bias += self.lr * error            # 更新偏置
                    errors += 1                             # 错误计数
                    print(f"第{epoch+1}轮,第{idx+1}个样本,更新权重:{self.weights},更新偏置:{self.bias}")
            
            self.errors_per_epoch.append(errors)

            # 本轮零错误 -> 收敛,训练结束
            if errors == 0:
                print(f"第{epoch+1}轮,错误数归零,训练收敛!")
                break
        
        return self
    
    def predict(self, X):
        # 计算加权和:z = w1 * x1 + w2 * x2 + b
        z = np.dot(X, self.weights) + self.bias # type: ignore

        # 激活判断:z >= 0 输出1,否则输出0
        return np.where(z >= 0, 1, 0)

开始训练

创建感知机实例,学习率设为0.5,最大训练100轮。调用fit开始训练,控制台会打印每一轮的权重变化过程。

python
model = Perceptron(learning_rate=0.5, n_iterations=100)
model.fit(X, labels)

验证所有样本

python
predictions = model.predict(X)

for i in range(len(X)):
    true_label = "偏胖" if labels[i] == 1 else "偏瘦"
    pred_label = "偏胖" if predictions[i] == 1 else "偏瘦"
    status = "✅" if predictions[i] == labels[i] else "❌"
    print(f"样本{i+1}: 真实={true_label}, 预测={pred_label}  {status}")

print(f"\n最终参数:w1={model.weights[0]:.4f}, w2={model.weights[1]:.4f}, b={model.bias:.4f}") # type: ignore

可视化决策边界与训练曲线

经过若干轮训练之后,感知机最终会收敛到一组合适的参数,能够正确区分所有样本。这条仔二维平面上把“偏瘦”和“偏胖”分开的线,就是感知机找到的决策边界。

左图展示感知机在二维平面上找到的那条“决策边界”————把偏胖和偏瘦分开的直线。右图展示每轮训练的错误数变化,可以直观看到模型的学习过程:从一开始错误不断,到最终错误归零。

用训练好的模型预测新样本

训练好的模型就像一个“已经学会判断的老师傅”,你给它新数据,它直接告诉你结果。注意新数据也要做同样的归一化,因为模型是在归一化后的数据上学到的权重,输入必须保持一致。

身高=168cm, 体重=48kg -> 偏瘦
身高=162cm, 体重=82kg -> 偏胖
身高=175cm, 体重=70kg -> 偏瘦

感知机的隐患

感知机只能解决线性可分的问题。但现实中的数据往往没有这么听话。逻辑运算中的XOR(异或)问题将这个缺陷暴露得淋漓尽致:

  • XOR规则:输入相同时输出0,不同时输出1。
  • 不存在一条直线能将其完整地分开。
  • 这不是技巧问题,而是数学上的根本限制————XOR是线性不可分的

多层感知机

多层结构

复杂功能可以由简单模块组合

XOR问题在逻辑电路中早已解决,计算机的异或门是由更基础的门电路组合而成的。

  • x1ORx2:至少一个输入为1。
  • NOT(x1ANDx2):两个输入不能同时为1

  • 核心思想:复杂功能可以由简单模块层层组合得到。

多条线围出复杂区域

把基础功能的感知机组合堆叠,就可以解决异或这样的复杂问题。

  • 单个感知机在特征空间里画一条线,只能做线性分割。
  • 如果有两个感知机,就有两条线;三个感知机就有三条线。
  • 多条线就可以围出任意复杂的区域。

用两层感知机解决XOR

  • 输入层:接收两个特征x1x2
  • 隐藏层:两个神经元使用不同的权重做并行运算,各自画一条分割线,输出0或1
  • 输出层:将隐藏层的两个输出作为输入,再做一次加权求和与激活,得到异或判断

第一个隐藏神经元学会了至少一个输入为1,第二个学会了不能同时为1,输出层把两个条件取交集。

多层感知机的一般结构

多层感知机按层排列、层间全连接,构成了多层感知机(MLP)。

  • 输入层接收原始特征,数个隐藏层进行中间运算,输出层给出最终结果。
  • 同一层里的神经元并行提取多个不同的特征或规则。
  • 后续层将前一层的输出作为输入,利用不同的权重进一步组合,逐层构造出更抽象、更有用的表示。

数学表达:函数的层层嵌套

output=hn(hn1(h2(h1(X))))

线性难题:

如果每一层只是线性变换,那么无论堆多少层都是徒劳的。

假设第一层输出为h=W1x+b1,第二层输出为y=W2h+b2

y=W2(W1x+b1)+b2=(W2W1)x+(W2b1+b2)

W=W2W1b=W2b1+b2,则y=Wx+b
💡线性变换的组合仍是线性变换。层层叠加,做的全是无用功。

解决方案是在每个神经元的加权求和输出上,套一个非线性函数,称为激活函数(Activation Function):

h=f(Wx+b)

有了非线性的f,层与层之间的组合就无法再被化简为单一线性变换。

训练难题

多层感知机的理论能力:

1989年正式证明了万能逼近定理(Universal Approximation Theorem):

一个含有足够多神经元的单隐藏层MLP,可以以任意精度逼近任意连续函数。

  • 这意味着MLP在理论上具备表达任何函数的能力。
  • 表达能力(足够多神经元)和训练能力(如何找到正确的权重)是两个不同的问题。

如何训练模型仍旧是个问题

  • MLP结构在1960年代就已经被提出。问题不是没人想到多层,而是没法训练。
  • 当时的感知机学习规则只适用于单层网络。
  • 核心难题:隐藏层的误差该怎么分配?输出层的错误我们看得到,但中间那些隐藏神经元,每个该为最终错误承担多少责任?没人知道怎么算这个梯度。

💡直到1986年,反向传播算法(Backpropagation)与多层网络结合,提供了一套高效计算多层梯度的方法。

神经网络训练流程

  1. 初始化各层神经元的权重和偏置参数;
  2. 输入数据做前向传播,得到预测结果;
  3. 用损失函数计算预测与真实标签的误差;
  4. 用反向传播从输出层逐层计算损失对参数的梯度;
  5. 用梯度下降根据梯度更新权重和偏置;
  6. 不断重复,让模型逐步收敛。

前向传播

数据沿网络从输入流向输出:线性加权 -> 加偏置 -> 激活函数,把信息重构为更高阶特征传递给下一层。

  • 正是这种逐层抽象的机制,让神经网络具备了自动学习特征的能力。

神经元操作

每个神经元都做同一套两步操作

  1. 线性变换
z=w1x1+w2x2++wnxn+b
  1. 非线性激活
a=σ(z)

差别只在于:每个神经元维护着自己的一套权重和偏置。以下图为例,仅参考(3输入 -> 4神经元):

a1=σ(w11x1+w12x2+w13x3+b1)a2=σ(w21x1+w22x2+w23x3+b2)a3=σ(w31x1+w32x2+w33x3+b3)a4=σ(w41x1+w42x2+w43x3+b4)

从单神经元到整层矩阵化

l层第i个神经元的输出(通项形式):

ai(l)=σ(j=1nwij(l)aj(l1)+bi(l))

为了运算效率,把同一层所有神经元打包成矩阵,一次乘法搞定整层:

A(l)=σ(W(l)A(l1)+b(l))

示例:

A(1)=σ([w11w12w13w21w22w23w31w32w33w41w42w43][a1(0)a2(0)a3(0)]+[b1(1)b2(1)b3(1)b4(1)])

同时解释了为什么神经网络计算天然适合GPU —— 它就是一连串大型矩阵乘法。

激活函数

前言

  1. 为什么需要激活函数?
展开查看

假设去掉所有激活函数,一个三层神经网络的输出:

y=W3(W2(W1x+b1)+b2)+b3

把括号展开后整体退化为一次线性变换:

y=(W3W2W1)Wx+(W3W2b1+W3b2+b3)b=Wx+b

💡无论堆多少层,没有激活函数的神经网络始终是一个线性模型。

激活函数的作用:在每一层插入一道非线性的“弯”,让神经网络得以拟合任意复杂的函数。

  1. 为什么不把Wx+b改成非线性?
展开查看
  1. 线性运算的工程红利
  • 硬件加速:GPU/TPU就是为矩阵乘法量身打造的,cuBLAS几毫秒就能算完亿级规模的Wx
  • 梯度极简:(Wx)W=x,反向传播一行公式搞定。

一旦把它换成复杂非线性,这些红利全部消失。

  1. 数值指数级膨胀

z=Wx2+b为例,仅仅叠加三层--> x8,叠加10层--> x1024

  • 前向:数值冲向无穷大或归零,模型瘫痪。
  • 反向:求导系数巨大,梯度彻底失控。

激活函数需求:

  1. 非线性:最基本门槛,否则深度毫无意义。
  2. 几乎处处可导:反向传播必须乘上它的导数。
  3. 梯度行为良好:不消失也不爆炸,最好接近1。
  4. 计算便宜:训练中会被调用上亿次。
  5. 输出零中心:避免梯度方向被绑死,优化更顺。
  6. 单调/平滑/有界
  • 一个好激活函数,是在表达能力、梯度传播、计算成本和数值稳定性之间取得平衡。
阶跃函数

阶跃函数(1958):最朴素的激活

1958年,罗森布拉特用阶跃函数搭出了感知机。

step(x)={1,x00,x<0

  • 梯度无法传播:除x=0外导数恒为0,误差信号一碰到阶跃函数就被直接清零。多层网络从根本上无法训练。
  • 信息损失太狠:输入是0.001还是1000,输出都是1。网络丧失对“程度”的感知,大量数值信息在激活的瞬间被永久丢弃。

Sigmoid

Sigmoid(1986):把开关变成旋钮

1986年辛顿等人把反向传播带回神经网络,它需要一个处处可导的激活函数————他们沿用了19世纪就用来描述人口增长的函数:

σ(x)=11+exσ(x)=σ(x)(1σ(x))

一条优雅的S形曲线,严格压在(0, 1),导数是一个简单乘法。

优势:

  • 处处可导:梯度下降终于能工作————这是反向传播能跑起来的硬性前提。
  • 天然概率解释:输出在(0, 1)间,x=0时取0.5————非常适合做二分类输出层。
  • 平滑过度:输入1.5和1.6输出有微小但有意义的差异,神经元终于有了“模拟感”。

劣势:

  • 梯度消失:导数最大值只有0.25,但反向传播每过一层梯度就要乘一次导数,深层梯度越来越小,几乎学不动。
  • 输出非零中心:输出永远是正数。下一层在反向传播时算出来的梯度往往同号。权重更新失去灵活性————优化器走Z字折线,无法直奔最优。
  • 计算贵 + 天然饱和:含指数项,比简单乘法贵得多;指数式压缩使两端斜率以指数速度衰减。

💡如今Sigmoid主要保留下来的位置,是二分类任务的输出层。


Tanh

Tanh(1990s):把Sigmoid摆正

90年代初,杨立昆在贝尔实验室研究手写数字识别,被Sigmoid的非零中心训练震荡困扰。1998年的Efficient BackProp中他系统总结了Tanh的优势:

tanh(x)=exexex+ex=2σ(2x)1tanh(x)=1tanh2(x)

输出范围被拉伸到(-1, 1),关于原点对称。

✅改进:

  • 导数最大值1,比Sigmoid大4倍,梯度衰减更慢。
  • 零中心输出,Z字震荡明显减轻,收敛更快。

❌仍然存在:

  • 本质仍是S形————两端饱和没有变。
  • 依赖指数函数,计算成本同样不低。

💡凭借Tanh,LeNet在90年代末已经能自动识别美国10%~20%的手写支票金额。但深层网络时代仍未到来————这只是一次改良,不是一次突破。


ReLU

ReLU(2010):懒得动脑的天才

2010年,辛顿和同事提出了一个简单到令人发指的激活函数:

ReLU(x)=max(0,x)ReLU(x)={1,x>00,x0

优势:

  • 大幅缓解梯度消失:正半轴导数恒为1,链式法则在这里几乎变成“梯度直通车”,深层网络第一次真正能训。
  • 计算极快:只需一次比较操作。没有指数、没有除法————大规模训练里被放大成可观的速度优势。
  • 天然稀疏激活:负输入直接置0————任意一次前向只有部分神经元真正激活,自带正则化效果。

代价:

  • 死亡ReLU:某次更新后预激活值恒为负,则输出永远0,导数永远0————神经元彻底死掉,救不回来。
  • 非零中心:输出要么0、要么正数。但在正半轴梯度直通车面前,这点Z字震荡基本可以接受。
  • 输出无上界:深层 + 大参数下激活值可能一路放大,引发数值不稳定————所以ReLU时代离不开BatchNorm。

💡ReLU很快取代Sigmoid和Tanh,称为深度学习时代最具代表性的激活函数。

在2012年的ImageNet竞赛上,AlexNet把图像分类错误率一举降到15.3%————这样的优势在ImageNet历史上前所未有,所用激活函数正是ReLU。

💡从那一刻起,整个CV圈迅速转向深度学习————ReLU几乎亲手开启了现代深度学习时代。


Leaky ReLU

Leaky ReLU:给负半轴留一条缝

既然ReLU在负半轴归零很“死”,那就给它留一个极小的斜率:

LeakyReLU(x)={x,x>0αx,x0

  • 负半轴导数永远不为0,神经元不会彻底死亡。
  • 当训练过程中出现大量神经元死亡时,可以一试。
  • α是手动超参,0.01不一定最优;多数情况下ReLU + 良好初始化 + BatchNorm 已经够用。

GELU

GELU(2016):把硬门换成软门

ReLU在x=0处硬性切断。GELU想换成一种柔和的软门————根据输入大小,以一定概率决定放行多少信号。

GELU(x)=xΦ(x)0.5x(1+tanh[2π(x+0.044715x3)])

Φ(x)是标准正态分布的累积分布函数。

2016年提出,2018年随BERT进入大众视野,此后GPT、ViT以及几乎所有主流Transformer类模型相继跟进。

💡GELU在大模型时代的地位,几乎相当于ReLU在CNN时代的地位。

✅适用场景:Transformer类模型(BERT/GPT/ViT)的前馈网络层。
❌代价:需要tanh、三次方、平方根————比ReLU慢得多。


Swish/SiLU

::: Swish/SiLU:机器自己挑出来的 2017年Google Brain 用神经架构搜索让模型自己挑激活函数,结果搜出来的形式和GELU几乎一模一样:

Swish(x)=xσ(x)

其实2016年Elfwing等人就独立提出过完全相同的函数————叫SiLU(Sigmoid Linear Unit)。PyTorch中即nn.SiLU
相比GELU的优势:只需一个Sigmoid,计算更轻。
:::

Swish实战场景:

  • 轻量级CNN:EfficientNet、MobileNetV3在精度与速度的权衡中都选择了Swish。
  • 大模型FFN:LLaMA引领Mistral/Qwen/PaLM等开源大模型FFN采用SwiGLU。
  • “想比ReLU多走一步”:在对计算稍敏感、又希望比ReLU更进一步的场景,Swish是高质量替代。

💡Swish GELU,但形式更简单————大模型时代的另一个常见选择。


总结

激活函数80年演化

💡演化方向不是越来越复杂,而是越来越懂得在表达能力、梯度传播和工程效率之间做权衡。


  • 阶跃函数:解决了“要不要激活”————但太硬,无法反向传播,信息丢得太狠。
  • Sigmoid:第一次让可导的非线性进入神经网络————但两端饱和、非零中心、深层学不动。
  • Tanh:Sigmoid的零中心改良————但本质仍是S形,饱和问题没根本上解决。
  • ReLU:用最直接的方式保住梯度————正半轴恒为1,真正开启了现代深度学习。
  • GELU / Swish:把ReLU的硬折角磨成平滑过度————大模型/Transformer时代的精修。

今天,到底该选谁?

  • 输出层:概率问题用Sigmoid/Softmax。
  • 普通隐藏层:很多场景下ReLU仍然够用。
  • Transformer / 大模型:GELU / SiLU(Swish)更常见。

💡激活函数提供“非线性”,梯度下降提供“学习能力”————一个让网络能拟合任意函数,一个让网络能找到正确参数。

损失函数

前向传播得到y^,但它和真实标签y总有差距。我们用损失函数把这个差距量化为一个数:

线性回归常用MSE(均方误差):

L=1Ni=1N(yiy^i)2

L越大,说明错得越离谱。问题随之而来:该怎么调整权重矩阵W和偏置b,才能让L变小?
线性回归、逻辑回归里的答案是梯度下降:算出损失对每个参数的偏导,组成向量——梯度,它指向损失上升最快的方向。

要让损失下降,沿负梯度方向走一小步即可:

WWηLW

反向传播

前向传播:

  • 预测值-6离目标0还差得远,损失高达18。下面靠反向传播更新。

反向传播:

梯度下降

用梯度下降更新参数

设学习率η=0.01,公式:

wwηLw
参数旧值梯度更新量 η新值
w12180.01×18=0.181.82
w23120.01×12=0.122.88
w3-1-360.01×(36)=+0.36-0.64

流程总结与实现

神经网络学习的本质:

前向传播 算误差 --> 反向传播 算梯度 --> 梯度下降 更新参数 --> 再次前向传播 .....

  • 如此循环成千上万次,直到损失降到接近0,模型就训练好了。

用PyTorch实现:自动微分的优雅

只需写出前向公式,PyTorch会在后台自动构建计算图,再用一行loss.backward()把所有梯度一次算出来。

python
import torch
import torch.nn as nn

# 数据
x = torch.tensor([[1.0]])   # 输入1
t = torch.tensor([[0.0]])   # 预测结果0

# 网络
model = nn.Sequential(
    nn.Linear(1, 1, bias=False),    # w1
    nn.ReLU(),
    nn.Linear(1, 1, bias=False),    # w2
    nn.ReLU(),
    nn.Linear(1, 1, bias=False),    # w3

)

# 初始化权重,与上面手算示例保持一致
with torch.no_grad():   # 关掉梯度后初始化权重
    model[0].weight.fill_(2.0)
    model[2].weight.fill_(3.0)
    model[4].weight.fill_(-1.0)

# 损失函数 + 随机梯度下降优化器
criterion = nn.MSELoss()
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)

# 训练100轮
for epoch in range(100):
    loss = criterion(model(x), t)       # 前向传播 + 计算损失
    optimizer.zero_grad()               # 清空上一轮残留的梯度
    loss.backward()                     # 反向传播,自动计算梯度
    optimizer.step()                    # 梯度下降更新权重

    if epoch % 10 == 0:
        print(f"Epoch {epoch+1:3d} | 预测值: {model(x).item():7.4f} | 损失: {loss.item():.4f}")

注意:PyTorch的nn.MSELoss()默认是(yt)2(不带12),所以第一轮损失是36而非手算的18,梯度也会大一倍。不影响最终收敛。