(多代码实现版)PyTorch神经网络入门博客

前言

很多同学刚接触 PyTorch 神经网络时,都会遇到同一个问题:代码能跑,但不知道在写什么

nn.Linearnn.ReLUforward函数... 这些东西到底是什么关系?为什么要继承nn.Module?为什么CrossEntropyLoss总是报错?

别担心,这篇文章用大白话 + 万能口诀,带你彻底搞懂神经网络搭建的核心逻辑,看完就能独立写出一个完整的神经网络。


一、万能口诀:1 个继承,2 个重写

记住这句话,这辈子写神经网络都不会忘:

1 个继承 :继承 torch.nn.Module(PyTorch 官方给的神经网络模板)

2 个重写 :重写 __init__(准备零件)和 forward(设计流水线)

这就是 PyTorch 搭建神经网络的全部骨架,所有复杂网络都是这个套路。

python 复制代码
import torch
import torch.nn as nn
import torch.nn.functional as F

# 1个继承:继承nn.Module
class SimpleNet(nn.Module):
    # 重写1:__init__ ------ 准备所有零件
    def __init__(self):
        super().__init__()
        self.fc1 = nn.Linear(3, 3)   # 第一层:3输入→3输出
        self.fc2 = nn.Linear(3, 2)   # 第二层:3输入→2输出
        self.out = nn.Linear(2, 2)   # 输出层:2输入→2输出
    
    # 重写2:forward ------ 设计数据流动流水线
    def forward(self, x):
        x = torch.sigmoid(self.fc1(x))  # 第一层 + sigmoid激活
        x = F.relu(self.fc2(x))         # 第二层 + relu激活
        prob = F.softmax(self.out(x), dim=1)  # 输出层 + softmax
        return prob

就这么简单!一个完整的神经网络就写好了。


二、网络结构详解:每一层到底在做什么

我们这个网络是一个3 层全连接网络,结构如下:

图 1:3 层全连接神经网络整体结构

各层参数说明:

层名称 输入维度 输出维度 激活函数 作用
输入层 3 - - 接收原始数据
fc1 3 3 sigmoid 第一次特征变换
fc2 3 2 relu 第二次特征变换 + 降维
out 2 2 softmax 输出分类概率

三、核心概念大白话讲解

3.1 Linear 全连接层 = "打分员"

很多教程说 "全连接层就是线性变换",太抽象了!

大白话:Linear 就是一个打分员,给每个输入特征加权求和。

图 2:全连接层 Linear 工作原理

数学公式就是:
Y=XW+bY = XW + bY=XW+b

  • X:输入特征(你的数据)

  • W(weight 权重):每个特征的重要程度分数(神经网络要学习的参数)

  • b(bias 偏置):调整整体打分的偏移量

举个例子:判断一个水果是不是苹果

  • 输入特征:[红色程度,圆形程度,大小]

  • 权重:[0.8, 0.7, 0.2] (红色和圆形很重要,大小不重要)

  • 偏置:-0.5

  • 最终分数 = 红色 ×0.8 + 圆形 ×0.7 + 大小 ×0.2 - 0.5

这就是 Linear 在做的事情!

3.2 激活函数 = "筛选器"

如果只有 Linear,不管多少层都只是线性变换,表达能力有限。

激活函数就是筛选器:留下有用信息,过滤噪音,引入非线性。

常用激活函数对比:

激活函数 特点 适用场景
sigmoid 输出 0~1,能表示概率 二分类输出层、门控机制
relu 负数变 0,正数保留 隐藏层首选,计算快、防梯度消失
softmax 多输出归一化,概率和为 1 多分类输出层

3.3 权重初始化 = "合理的起跑线"

神经网络刚开始训练时,权重 W 是随机的。好的初始化能让训练更快更稳定:

  • Xavier 初始化:适合 sigmoid/tanh 激活函数

  • Kaiming 初始化:适合 relu 激活函数(PyTorch 默认)

python 复制代码
# 手动初始化示例
nn.init.xavier_uniform_(self.fc1.weight)
nn.init.kaiming_normal_(self.fc2.weight)

四、forward 函数:数据的完整旅行路线

forward函数就是神经网络的数据流水线,数据从输入 x 开始,经过每一层变换,最终输出结果。

图 3:前向传播完整数据流

让我们跟踪一个数据的完整旅程:

python 复制代码
def forward(self, x):
    # 第1站:x形状(3,) → fc1线性变换 → sigmoid激活 → 输出还是(3,)
    x = torch.sigmoid(self.fc1(x))
    
    # 第2站:形状(3,) → fc2线性变换 → relu激活 → 输出变成(2,)(降维了!)
    x = F.relu(self.fc2(x))
    
    # 第3站:形状(2,) → out线性层 → softmax归一化 → 输出(2,)概率
    prob = F.softmax(self.out(x), dim=1)
    
    return prob

关键点:每一层不仅改变数值,还可能改变张量形状!


五、损失计算:新手最容易踩的坑

5.1 损失函数 = "衡量差距的尺子"

损失函数就是衡量:模型预测答案标准答案 之间差了多少。

分类任务最常用:CrossEntropyLoss(交叉熵损失)

5.2 ⚠️ 90% 新手都会犯的错误

图 4:交叉熵损失计算流程与避坑指南

❌ 错误写法(90% 新手都会写错):

python 复制代码
# 错误:自己加了softmax
output = model(x)  # output已经是softmax后的prob了
loss = criterion(output, label)  # 错!输入应该是logits

✅ 正确写法:

python 复制代码
# 正确:直接输出原始logits,不要softmax!
class Net(nn.Module):
    def forward(self, x):
        # ...前面的层...
        logits = self.out(x)  # 只输出原始分数,不做softmax
        return logits

# 损失计算
criterion = nn.CrossEntropyLoss()
loss = criterion(logits, label)  # PyTorch内部自动做softmax

为什么?

PyTorch 的CrossEntropyLoss内部已经包含了LogSoftmaxNLLLoss,如果你自己再做一次 softmax,相当于:

  • 概率被压缩两次 → 数值不稳定

  • 梯度变小 → 训练变慢甚至不收敛


六、常见损失函数大全

上一章我们讲了多分类任务最常用的 CrossEntropyLoss,但实际工作中还有很多不同场景的损失函数。这一章我们系统讲解二分类和回归任务的常用损失。


6.1 BCELoss:二分类交叉熵损失(2种用法)

BCELoss = Binary Cross Entropy Loss,专门用于二分类任务。根据任务场景不同,有两种经典用法。


6.1.1 用法一:输出2个神经元 + sigmoid + BCELoss(多标签二分类)

适用场景:一个样本可以同时属于多个类别(多标签分类)

举个例子:一张图片可以同时包含"猫"和"狗"两个标签,而不是二选一

python 复制代码
import torch
import torch.nn as nn

# ===================== 准备数据 =====================
# 标签:2个样本,每个样本有2个标签位(1=有这个标签,0=没有)
# 样本1:有标签A,没有标签B → [1, 0]
# 样本2:没有标签A,有标签B → [0, 1]
labels = torch.tensor([[1, 0], [0, 1]], dtype=torch.float)

# 模型输出的原始分数(logits):2个样本,每个输出2个神经元的分数
logits = torch.tensor([[5, 2], [1.5, 7]])
print("模型输出原始分数:\n", logits)
# 输出:
# tensor([[5.0000, 2.0000],
#         [1.5000, 7.0000]])

# ===================== 关键步骤:sigmoid转概率 =====================
# 每个神经元独立计算概率,范围 [0, 1]
probs = torch.sigmoid(logits)
print("\n经过sigmoid后的概率:\n", probs)
# 输出:
# tensor([[0.9933, 0.8808],
#         [0.8176, 0.9991]])

# ===================== 计算BCELoss =====================
loss_fn = nn.BCELoss()
loss = loss_fn(probs, labels)
print("\nBCELoss损失值:", loss.item())
# 输出:BCELoss损失值:0.19534829258918762

💡 重点讲解:为什么用 sigmoid 而不是 softmax?

对比项 softmax sigmoid
概率关系 所有输出概率和为 1,互斥 每个输出独立,范围 0~1,互不影响
适用场景 多分类(只能选一个) 多标签(可以选多个)、二分类
物理含义 "这个样本是A的概率80%,是B的概率20%" "这个样本有A特征的概率90%,有B特征的概率85%"

多标签 vs 多分类的本质区别:

  • 多分类:类别之间互斥 → 用 softmax + CrossEntropyLoss
  • 多标签:类别之间独立 → 用 sigmoid + BCELoss

6.1.2 用法二:输出1个神经元 + sigmoid + BCELoss(单标签二分类)

适用场景:标准二分类任务(是/否,正/反)

举个例子:判断肿瘤是良性(0)还是恶性(1)

python 复制代码
import torch
import torch.nn as nn

# ===================== 准备数据 =====================
# 标签:2个样本,都是正例(恶性肿瘤)
labels = torch.tensor([1, 1], dtype=torch.float)

# 模型只输出1个神经元的原始分数
logits = torch.tensor([1.5, 2])
print("模型输出原始分数:", logits)
# 输出:tensor([1.5000, 2.0000])

# ===================== sigmoid转概率 =====================
probs = torch.sigmoid(logits)
print("经过sigmoid后的概率:", probs)
# 输出:tensor([0.8176, 0.8808])

"""
📊 概率解释:
- 第一个样本是正例(恶性)的概率 = 0.8176
- 第一个样本是反例(良性)的概率 = 1 - 0.8176 = 0.1824
- 第二个样本是正例的概率 = 0.8808
- 第二个样本是反例的概率 = 1 - 0.8808 = 0.1192
"""

# ===================== 计算BCELoss =====================
loss_fn = nn.BCELoss()
loss = loss_fn(probs, labels)
print("BCELoss损失值:", loss.item())
# 输出:BCELoss损失值:0.15071983635425568

💡 二分类任务的输出设计哲学:

为什么二分类可以只输出1个神经元?

因为二分类只有两种可能,它们的概率是互补的:

  • P(正例) = p
  • P(反例) = 1 - p

输出1个神经元 vs 输出2个神经元的对比:

方案 输出神经元数 激活函数 损失函数
方案1(推荐) 1个 sigmoid BCELoss
方案2 2个 softmax CrossEntropyLoss

推荐用方案1:参数更少,计算更快,物理含义更清晰!


6.2 回归损失函数对比:L1Loss、MSELoss、SmoothL1Loss

回归任务(预测连续值,比如预测房价、预测温度)和分类任务不同,使用的损失函数也完全不一样。

PyTorch 提供了三种最常用的回归损失,我们用同一个例子对比它们的区别。


6.2.1 三种损失函数代码对比
python 复制代码
import torch
import torch.nn as nn

# ===================== 准备数据 =====================
# 真实值:比如5天的真实温度 [1°C, 2°C, 3°C, 4°C, 5°C]
y_true = torch.tensor([1.0, 2.0, 3.0, 4.0, 5.0])
print("真实值:", y_true)

# 预测值:模型预测的温度
y_pred = torch.tensor([0.6, 1.7, 3.0, 4.2, 5.3])
print("预测值:", y_pred)

# 先看看每个样本的误差
print("\n每个样本的绝对误差:", torch.abs(y_pred - y_true))
# 输出:tensor([0.4000, 0.3000, 0.0000, 0.2000, 0.3000])

# ===================== 1. L1Loss = MAE 平均绝对误差 =====================
loss_fn = nn.L1Loss()
loss_l1 = loss_fn(y_pred, y_true)
print("\n✅ L1Loss(MAE平均绝对误差):", loss_l1.item())
# 计算过程:(0.4 + 0.3 + 0.0 + 0.2 + 0.3) / 5 = 1.2 / 5 = 0.24
# 输出:0.24000000953674316

# ===================== 2. MSELoss = MSE 均方误差 =====================
loss_fn = nn.MSELoss()
loss_mse = loss_fn(y_pred, y_true)
print("✅ MSELoss(MSE均方误差):", loss_mse.item())
# 计算过程:(0.16 + 0.09 + 0.0 + 0.04 + 0.09) / 5 = 0.38 / 5 = 0.076
# 输出:0.07600000500679016

# ===================== 3. SmoothL1Loss = Huber损失 =====================
loss_fn = nn.SmoothL1Loss()
loss_smooth = loss_fn(y_pred, y_true)
print("✅ SmoothL1Loss(Huber损失):", loss_smooth.item())
# 输出:0.07400000095367432

6.2.2 三种损失函数深度解析

1. L1Loss = MAE 平均绝对误差

公式 :L1=1n∑i=1n∣ytrue−ypred∣L1 = \frac{1}{n}\sum_{i=1}^n |y_{true} - y_{pred}|L1=n1∑i=1n∣ytrue−ypred∣

特点

  • 对异常值鲁棒:误差只取绝对值,不会平方放大
  • 梯度不稳定:在0点不可导,小误差时梯度也是1,收敛后期容易震荡

适用场景:数据中有很多异常值(outliers)时使用


2. MSELoss = MSE 均方误差

公式 :MSE=1n∑i=1n(ytrue−ypred)2MSE = \frac{1}{n}\sum_{i=1}^n (y_{true} - y_{pred})^2MSE=n1∑i=1n(ytrue−ypred)2

特点

  • 梯度稳定:处处可导,梯度随误差线性变化(dL/dx = 2x)
  • 惩罚大误差:误差 > 1 时会被平方放大,模型更关注大误差
  • 对异常值敏感:一个极端异常值会主导整个损失

适用场景:数据比较干净,异常值很少的回归任务


3. SmoothL1Loss = Huber损失

公式
SmoothL1(x)={0.5x2if ∣x∣<1∣x∣−0.5if ∣x∣≥1 SmoothL1(x) = \begin{cases} 0.5x^2 & \text{if } |x| < 1 \\ |x| - 0.5 & \text{if } |x| \geq 1 \end{cases} SmoothL1(x)={0.5x2∣x∣−0.5if ∣x∣<1if ∣x∣≥1

特点:完美结合了 L1 和 MSE 的优点!

  • 误差小时(|x| < 1):用 MSE,梯度稳定,收敛精确
  • 误差大时(|x| ≥ 1):用 L1,对异常值不敏感

为什么叫 "Smooth"?

  • 普通 L1 在 x=0 处有尖点,不可导
  • SmoothL1 在 x=0 处平滑过渡,处处可导

经典应用:Faster R-CNN、YOLO 等目标检测算法的边界框回归全部用这个!


6.2.3 三种损失函数曲线对比
复制代码
损失值
  ^
  |        ● MSE
  |       /
  |      /         ● L1
  |     /         /
  |    /    ● SmoothL1
  |   /    /
  |  /   /
  | /  /
  |/ /
  +------------------> 误差 |x|
  0        1

看图说话:

误差范围 损失大小关系 说明
小误差(| x | < 1) MSE < SmoothL1 < L1 MSE惩罚最小,收敛最快
误差 = 1 三者相等 三条曲线交汇点
大误差(|x| > 1) MSE > L1 > SmoothL1 MSE被平方放大,SmoothL1最温和

📌 选择口诀:

  • 数据脏、异常值多 → 选 L1Loss
  • 数据干净、要精确收敛 → 选 MSELoss
  • 目标检测、追求稳健 → 必选 SmoothL1Loss

七、完整可运行代码

python 复制代码
import torch
import torch.nn as nn
import torch.nn.functional as F

# 1. 定义网络
class SimpleNet(nn.Module):
    def __init__(self):
        super().__init__()
        self.fc1 = nn.Linear(3, 3)
        self.fc2 = nn.Linear(3, 2)
        self.out = nn.Linear(2, 2)
        
        # 可选:权重初始化
        nn.init.kaiming_normal_(self.fc1.weight)
        nn.init.kaiming_normal_(self.fc2.weight)
    
    def forward(self, x):
        x = torch.sigmoid(self.fc1(x))
        x = F.relu(self.fc2(x))
        logits = self.out(x)  # 注意:输出logits,不做softmax!
        return logits

# 2. 创建模型和数据
model = SimpleNet()
x = torch.randn(2, 3)  # 2个样本,每个3个特征
labels = torch.tensor([0, 1])  # 真实标签

# 3. 前向传播
logits = model(x)
print("模型输出logits:", logits)

# 4. 计算损失(正确方式)
criterion = nn.CrossEntropyLoss()
loss = criterion(logits, labels)
print("损失值:", loss.item())

# 5. 如果需要看概率,手动单独做softmax
prob = F.softmax(logits, dim=1)
print("预测概率:", prob)

八、优化器与训练技巧

8.1 指数加权平均与动量(Momentum)

所有深度学习优化器(SGD、Adam、RMSprop)的核心思想,都来自一个简单但强大的数学技巧:指数加权平均(Exponentially Weighted Average)

理解了它,你就理解了动量优化的本质。


8.1.1 什么是指数加权平均?

生活中的例子:看天气预报时,"过去10天平均气温"比"单日气温"更能反映真实的季节趋势。

指数加权平均就是:用过去的平均值来平滑当前的波动

核心公式
vt=β⋅vt−1+(1−β)⋅θtv_t = \beta \cdot v_{t-1} + (1-\beta) \cdot \theta_tvt=β⋅vt−1+(1−β)⋅θt

其中:

  • vtv_tvt = 第 t 天的加权平均值
  • vt−1v_{t-1}vt−1 = 前一天的加权平均值
  • θt\theta_tθt = 第 t 天的真实观测值
  • β\betaβ = 加权系数(通常取 0.9)

8.1.2 完整代码实现 + 可视化对比
python 复制代码
# 导包
import torch
import matplotlib.pyplot as plt
import matplotlib
matplotlib.use("TkAgg")
matplotlib.rcParams["font.sans-serif"] = ["SimHei"]
matplotlib.rcParams["axes.unicode_minus"] = False
# todo 模拟一个月的天和温度数据
x_day = torch.arange(1, 31)
print(x_day)
torch.manual_seed(66)
y_temp = torch.rand(30) * 20 + 5
print(y_temp)


# todo 指数加权平均后线性图
print("====================指数加权平均直线图=======================")
y_iwm = []
beida = 0.9
# 遍历原始温度,计算指数加权平均值
for i, t in enumerate(y_temp):
    if i == 0:
        y_iwm.append(t)
    else:
        y_iwm.append(y_iwm[-1] * beida + (1-beida)*t)

# todo 模拟原始温度及指数加权平均的线性图
plt.subplot(121)
plt.scatter(x_day, y_temp)
plt.plot(x_day, y_temp)
plt.subplot(122)
plt.scatter(x_day, y_temp)
plt.plot(x_day, y_iwm)
plt.show()

运行效果说明:

复制代码
左图(原始数据):
    ● 点上下跳得很厉害
    ● 看不出整体温度趋势

右图(加权后):
    ● 蓝色曲线相对平滑一些
    ● 清晰反映了温度的整体变化趋势
    ● 过滤掉了随机波动的噪音

8.1.3 β 参数的物理含义

β = 0.9 到底是什么意思?

数学上可以证明:
有效平均天数≈11−β\text{有效平均天数} \approx \frac{1}{1-\beta}有效平均天数≈1−β1

所以:

  • β = 0.9 → 1/(1-0.9) = 约10天的平均
  • β = 0.98 → 1/(1-0.98) = 约50天的平均
  • β = 0.5 → 1/(1-0.5) = 约2天的平均
β 值 平滑程度 记忆长度 适用场景
0.5 很不平滑 只记得最近2天 反应快,紧跟变化
0.9 适中 记得最近10天 平衡,最常用
0.98 非常平滑 记得最近50天 反应慢,趋势稳定

8.1.4 为什么深度学习优化器都用这个思想?

问题:普通 SGD 梯度下降的问题

复制代码
普通SGD的更新:
    w = w - lr * g_t
    
    问题:
    ● 梯度方向忽左忽右,震荡严重
    ● 收敛慢,容易卡在局部最优
    ● 像醉汉走路,东倒西歪

动量(Momentum)优化:加入指数加权平均!

复制代码
带动量的SGD更新:
    v_t = β * v_{t-1} + (1-β) * g_t   ← 梯度的指数加权平均
    w = w - lr * v_t                 ← 用平滑后的梯度更新
    
    好处:
    ✅ 过去的梯度方向会"积累"下来
    ✅ 震荡的方向会相互抵消
    ✅ 正确的方向会加速前进
    ✅ 像雪球一样越滚越快

类比理解

  • 普通 SGD = 每一步都重新决定方向,容易走弯路
  • 带动量 SGD = 有惯性,保持之前的运动方向,过滤噪音

这就是为什么:

  • SGD + Momentum 比普通 SGD 快得多
  • Adam、RMSprop 等所有先进优化器,本质都是对梯度梯度平方做指数加权平均!

8.1.5 可视化解读动量的威力
复制代码
普通SGD的优化路径:
    ↗ ↘ ↗ ↘ ↗ ↘  震荡前进,很慢
      \  /  \  /
       \/    \/

带动量的优化路径:
    → → → → → →  平滑直线,很快
    方向稳定,惯性前进

📌 核心结论:

  1. 指数加权平均 = 用过去的平均值平滑现在的波动
  2. β=0.9 ≈ 过去10天的平均,是最常用的设置
  3. 动量优化 = 对梯度做指数加权平均
  4. 所有现代优化器的核心都是这个简单的数学技巧

九、全文总结

核心知识点回顾

  1. 万能口诀 :1 个继承(nn.Module)+ 2 个重写(__init__forward

  2. Linear 层:就是加权求和的打分员,Y = XW + b

  3. 激活函数:引入非线性的筛选器,隐藏层首选 relu

  4. forward 函数:数据流动的流水线,注意形状变化

  5. 避坑提醒 :CrossEntropyLoss 输入是 logits,不要自己加 softmax

  6. BCELoss二分类

    • 多标签:输出N个神经元 + sigmoid
    • 单标签:输出1个神经元 + sigmoid
  7. 回归损失三兄弟

    • L1Loss:对异常值鲁棒
    • MSELoss:梯度稳定,惩罚大误差
    • SmoothL1Loss:结合两者优点,目标检测标配
  8. 指数加权平均:所有优化器动量的核心思想,β=0.9≈过去10天平均

下一步建议

  • 把这篇文章的代码默写 3 遍,直到不看参考也能写出来

  • 尝试修改网络结构:加层、改神经元数量、换激活函数

  • 尝试不同的损失函数,观察训练效果的差异

记住:神经网络没有那么神秘,本质就是可学习参数的流水线变换。掌握了这个核心思想,再复杂的网络也只是这个套路的延伸。

相关推荐
HSunR1 小时前
神经网络 从函数到transformer学习笔记
神经网络·学习·transformer
AI医影跨模态组学1 小时前
Insights Imaging(IF=4.5)郑州大学第一附属医院高剑波等团队:基于CT的影像组学预测不可切除胃癌PD-1/PD-L1抑制剂联合化疗治疗反应
人工智能·深度学习·论文·医学·医学影像·影像组学
youcans_1 小时前
【跟我学 AI 编程】(5) Claude Code 快速指南
人工智能·大语言模型·ai编程·claude code
小小测试开发1 小时前
AI Agent 重构单体应用实战:1Password 经验与避坑指南
人工智能·windows·重构
初心未改HD1 小时前
机器学习之梯度提升与XGBoost详解
人工智能·机器学习
您^_^1 小时前
专家(一):Claude Code 微服务实战——6 个服务从拆分到 K8s 部署,$0.45 全套 YAML 照抄
人工智能·windows·微服务·架构·kubernetes·个人开发·claude code
沪漂阿龙1 小时前
面试题:Transformer 模型详解——核心创新、编码器解码器结构、位置编码、因果掩码与大模型基础全解析
人工智能·深度学习·transformer
JunLa1 小时前
Agent Basic 上篇
大数据·人工智能·agent
Raink老师1 小时前
【AI面试临阵磨枪-57】如何防止 Prompt 注入、越狱、敏感信息泄露
人工智能·面试·prompt·ai 面试