learn_pytorch

安装

PyTorch 在 PyPI 上的包名是 torch,而不是 pytorch

1
2
# 仅安装 CPU 版本
uv add torch

安装 GPU 版本的 PyTorch 需要指定 CUDA 版本的索引。

1
2
# CUDA 12.1 版本(推荐,适用于较新的显卡)
uv add torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121

检查你的 NVIDIA 驱动支持的 CUDA 版本:

1
nvidia-smi

如何知道对应的cuda版本索引

访问 PyTorch 官网: https://pytorch.org/get-started/locally/

torchvisiontorchaudio 是 PyTorch 生态系统中的两个官方扩展库:

torchvision - 计算机视觉工具包:

  • 预训练模型(ResNet、VGG、YOLO 等)
  • 图像数据集(CIFAR-10、ImageNet、COCO 等)
  • 图像转换和增强功能
  • 图像读取和处理工具

torchaudio - 音频处理工具包:

  • 音频数据集
  • 音频转换和预处理
  • 音频特征提取(MFCC、梅尔频谱等)
  • 音频读取和保存

张量与向量

维度的区别 (最本质的区别)

这是区分它们的“金标准”。在数学和编程(如 NumPy, PyTorch)中,我们看有多少层“方括号” []

向量 (Vector)是一维的

  • 它只有 1 个轴 (Axis)
  • 对于向量而言,维度通常指:它里面包含了几个数字(元素的个数)。
  • 代码形状: [x, y, z] -> Shape: (3,)

张量 (Tensor)是多维的统称

  • 它可以是 0 维、1 维、2 维、3 维…甚至 N 维。
  • 0阶张量 = 标量 (Scalar)
  • 1阶张量 = 向量 (Vector) —— 看!向量在这里。
  • 2阶张量 = 矩阵 (Matrix)
  • 3阶+张量 = 通常直接叫张量。

Linear

1
2
3
from torch import nn
linear = nn.Linear(5, 3)
linear.state_dict()
1
2
3
4
5
OrderedDict([('weight',
tensor([[ 0.3763, -0.3488, 0.4359, 0.1161, 0.3337],
[ 0.2588, 0.1844, 0.1083, -0.1958, 0.2706],
[-0.0392, -0.0902, 0.3593, -0.2657, 0.3799]])),
('bias', tensor([-0.4142, -0.0444, -0.2487]))])

linear = nn.Linear(5, 3) 创建了一个线性层(全连接层)

参数含义:

  • 5 - 输入特征数(in_features)
  • 3 - 输出特征数(out_features)

内部结构: 这个层包含两个可学习的参数:

  • 权重矩阵 W:形状为 (3, 5)
  • 偏置向量 b:形状为 (3,)

数学运算: y = xWT + b

1
2
3
4
5
6
from torch import tensor
input=tensor(
[[1,2,3,4,5],
[2,3,4,5,6]]
).float()
linear(input)
  • linear 层的权重是 torch.float32(Float)
  • PyTorch 不允许不同数据类型的张量进行矩阵运算
1
2
3
4
5
6
7
8
9
10
11
12
13
input=tensor(
[
[
[1,2,3,4,5],
[2,3,4,5,6]
],
[
[3,4,5,6,7],
[4,5,6,7,8]
]
]
).float()
input.shape
1
torch.Size([2, 2, 5])
1
linear(input)
1
2
3
4
5
tensor([[[ 0.4754, -2.3285,  0.7223],
[ 0.4696, -3.1571, 0.9678]],

[[ 0.4638, -3.9856, 1.2132],
[ 0.4580, -4.8142, 1.4587]]], grad_fn=<ViewBackward0>)

输入:(2, 2, 5) ↓ nn.Linear(5, 3) ← 把最后一维从 5 变成 3 ↓ 输出:(2, 2, 3)

激活函数ReLU

image-20260123153706879

1. 什么是激活函数?

激活函数简单来说就是,线性层之间的非线性变换

2. 核心作用:为什么要用它?

你可能会问:“大家都是算数学,为什么非要插在这个 Linear 层中间?不能直接 Linear 接 Linear 吗?”

答案是:绝对不行。 如果没有激活函数,神经网络就是个“草包”。

引入非线性 (Non-linearity) —— 让网络学会“弯曲”

这是激活函数存在的最大意义。

  • 线性层只能画直线: y = wx + b 是直线的方程。不管你叠多少层线性层,直线叠加直线,最后还是一条直线(或者平面)。
  • 现实世界是弯曲的: 比如要把“猫”和“狗”的图片分开,分界线绝不是一条直线,而是一条极其复杂的曲线。
  • 激活函数的作用: 它就像一把钳子,把线性层画出的直线“掰弯”。有了它,神经网络才能拟合各种复杂的形状。

3. 常见的激活函数有哪些?

image-20260123154428477

尽管ReLU形式简单,但在实际的工程实践上,效果却相比其他激活函数更好,并且由于形式简单,计算效率也更高,因此,ReLU是目前最流行的激活函数

既然 ReLU 这么好,它有缺点吗?

为了客观,必须提一下它的一个著名缺陷:“Dead ReLU” (神经元死亡问题)

  • 现象: 因为负数区域梯度完全是 0。如果运气不好,某个神经元的参数被更新成了一个很大的负数,不管输入什么数据,它算出来都是负的。
  • 结果: 经过 ReLU 后全是 0,梯度也是 0。这个神经元从此“死掉了”,再也不会更新,对网络没有任何贡献。
  • 解决方案: 出现了一种变体叫 Leaky ReLU,给负数区域一点点斜率(比如 0.01x),让它别死透,还能有一点点梯度传回来。
image-20260123154940548

前馈神经网络FFN

前馈神经网络(Feedforward Neural Network, FNN)是深度学习中最基础、最经典的架构,也被称为多层感知机 (MLP)

可以用一句话来概括它:一条“绝不回头”的数据流水线。

1. 核心定义:为什么叫“前馈”?

“前馈” (Feedforward) 描述的是数据的流向

  • 单向通行: 信号从输入层进入,经过一层层的隐藏层处理,最后从输出层出来。
  • 无回路: 信号永远不会在这个网络里转圈圈,也不会从后一层传回前一层(那是循环神经网络 RNN 做的事)。
feedforward neural network diagram的图片

2. 它的解剖结构

一个典型的前馈神经网络由三部分组成“三明治”结构:

A. 输入层 (Input Layer)

  • 作用: 负责接收原始数据(向量)。
  • 特点: 这一层不进行任何计算,它只是数据的入口。

B. 隐藏层 (Hidden Layers)

  • 作用: “提取特征”的主力军。这是网络“深”的地方。
  • 组成: 就是我们刚才讲的 Linear (线性变换) + ReLU (非线性激活) 的组合。
  • 为什么叫隐藏? 因为你看不到它们。输入和输出是你可以直接观察的,但中间这些层处理出的特征(比如“圆弧”、“边缘”)是机器内部理解的“黑盒”数据。

C. 输出层 (Output Layer)

  • 作用: 给出最终结果。
  • 特点: 通常不需要激活函数(直接出 Logits),或者接 Softmax 出概率。

3. 数学本质:函数的嵌套

如果你把这个网络拆解成数学公式,它其实就是一个巨大的复合函数

假设你有两层网络:

  1. 第一层:h = ReLU(W1x + b1)
  2. 第二层:y = W2h + b2

把它们套在一起,整个神经网络就是:

$$y = W_2 \cdot \underbrace{ReLU(W_1 \cdot x + b_1)}_{\text{第一层的输出}} + b_2$$

前馈神经网络就是在做这件事:通过层层嵌套,把简单的 Wx + b 变成一个能拟合万物的超级函数。

识别手写数字

下载数据集

1
2
3
4
5
6
7
8
9
10
import torchvision

train_data = torchvision.datasets.MNIST(
root="./dataset",
train=True,#训练集
download=True)
test_data = torchvision.datasets.MNIST(
root="./dataset",
train=False,#测试集
download=True)

查看数据集

1
2
print(train_data.data.shape)  # torch.Size([60000, 28, 28])
print(test_data.data.shape) # torch.Size([10000, 28, 28])
1
train_data.targets[0]  # 获取第1张图片的标签(0-9的数字),表示这张图片是哪个数字
1
tensor(5)#说明代表数字五
1
train_data.data[0]#  # 获取第1张图片的像素数据
1
2
3
tensor([[  0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[ 0, 0, 0,

展平数据

1
2
3
4
flat_test_data = test_data.data.view(10000, 784)# 将每张28x28的图片展平成784维的向量

print(test_data.data.shape)
print(flat_test_data.shape)

把图片拉直是为了输入到全连接层(Linear层)!

原因:

  1. 图片的原始形状: (28, 28) - 2维矩阵
    • 这是图片的”空间结构”
  2. 全连接层的需求: 每个样本必须是1维特征向量

归一化数据

1
2
float_flat_test_data = flat_test_data.float() / 255.0  # 归一化到0-1之间
float_flat_test_data[0]

防止大数值可能导致梯度爆炸或消失

定义模型

image-20251221204157379
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import torch.nn as nn
from torch import Tensor

class MnistModel(nn.Module):
def __init__(self) -> None:
super().__init__()

# 第一层:输入 784 (28x28像素),输出 256
self.layer1 = nn.Linear(784, 256)
self.relu1 = nn.ReLU()

# 第二层:输入 256,输出 128
self.layer2 = nn.Linear(256, 128)
self.relu2 = nn.ReLU()

# 第三层 (输出层):输入 128,输出 10 (对应 0-9 十个数字)
self.layer3 = nn.Linear(128, 10)

def forward(self, x: Tensor) -> Tensor:
# 数据流向:Layer1 -> ReLU -> Layer2 -> ReLU -> Layer3
x = self.relu1(self.layer1(x))
x = self.relu2(self.layer2(x))
x = self.layer3(x) # 注意:最后一层直接输出 Logits,没有加 ReLU 或 Softmax
return x

定义损失函数

image-20260123172014631

这里使用CrossEntropyLoss

1
2
3
4
5
6
7
8
import torch.nn as nn                                                                 

model = MnistModel()
criterion = nn.CrossEntropyLoss()

# 假设 images: [batch, 784], labels: [batch]
logits = model(images)
loss = criterion(logits, labels)

模型训练

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import torch                                                                          
import torch.nn as nn
from torch.optim import Adam

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"Using device: {device}")
model = MnistModel().to(device)
criterion = nn.CrossEntropyLoss()
optimizer = Adam(model.parameters(), lr=1e-3)

epochs = 5
for epoch in range(epochs):
model.train()
total_loss = 0.0

for images, labels in dataloader:
images = images.to(device)
labels = labels.to(device)

# 前向
logits = model(images)
loss = criterion(logits, labels)

# 反向
optimizer.zero_grad()
loss.backward()
optimizer.step()

total_loss += loss.item()

avg_loss = total_loss / len(dataloader)
print(f"Epoch {epoch+1}/{epochs}, loss={avg_loss:.4f}")
1
2
3
4
5
Epoch 1/5, loss=0.2660
Epoch 2/5, loss=0.1019
Epoch 3/5, loss=0.0686
Epoch 4/5, loss=0.0494
Epoch 5/5, loss=0.0387

保存训练参数

1
2
# 保存                                                                                
torch.save(model.state_dict(), "mnist_model.pth")

加载模型与预测

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import torch                                                                          

model = MnistModel()
model.load_state_dict(torch.load("mnist_model.pth", map_location="cpu"))
model.eval()

# 取一个样本
image, label = mnist[0] # image 已经是 784 维张量(你的 transform 里展平了)

with torch.no_grad():
logits = model(image.unsqueeze(0)) # [1, 784]
pred = logits.argmax(dim=1).item()

print("pred:", pred, "label:", label)

参考资料

从零搭建神经网络,识别手写数字【PyTorch】【Transformer结构拆解】_哔哩哔哩_bilibili