《蒲公英书》第1章 实践基础 学习笔记
- [第1章 实践基础 · 核心整理](#第1章 实践基础 · 核心整理)
-
- [1. 为什么需要深度学习框架](#1. 为什么需要深度学习框架)
- [2. 两个基本概念](#2. 两个基本概念)
- [3. 环境与代码](#3. 环境与代码)
- [4. 张量的创建](#4. 张量的创建)
- [5. 张量的属性](#5. 张量的属性)
- [6. 张量与 NumPy 的转换](#6. 张量与 NumPy 的转换)
- [7. 张量的访问与修改](#7. 张量的访问与修改)
- [8. 张量的运算](#8. 张量的运算)
-
- [8.1 逐元素数学运算](#8.1 逐元素数学运算)
- [8.2 逻辑运算](#8.2 逻辑运算)
- [8.3 矩阵运算](#8.3 矩阵运算)
- [8.4 广播机制](#8.4 广播机制)
- [9. 原位操作注意点](#9. 原位操作注意点)
第1章 实践基础 · 核心整理
1. 为什么需要深度学习框架
- 从零搭建成本太高(模型定义、梯度推导、并行加速、显存管理等)
- 框架的价值:实现简单 (只描述模型逻辑)、使用高效(CPU/GPU/移动端灵活迁移)
- 本书选择 PyTorch:动态图、Python 接口、社区成熟
2. 两个基本概念
- 张量(Tensor):多维数组,数据的统一容器。关注形状、dtype、所在设备
- 算子(Operator):基本计算单元,自带前向与反向梯度,可像搭积木一样组合模型
3. 环境与代码
- Python 3.11+,PyTorch 2.7+,torchvision 对应版本
- CPU 安装:
pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu
- GPU 版需匹配 CUDA 版本,去官网获取命令
- 自检:
import torch → torch.__version__ → torch.cuda.is_available()
- 代码仓库:https://github.com/nndl/nndl-practice
4. 张量的创建
| 方式 |
常用 API |
说明 |
| 从数据创建 |
torch.tensor(list) |
支持嵌套列表,元素数量每维必须一致 |
| 按形状创建 |
torch.zeros/ones/full |
全0、全1、全指定值 |
| 按区间创建 |
torch.arange(start,end,step) |
不包含 end |
|
torch.linspace(start,end,steps) |
包含 end,均匀分点 |
import torch
# 1. 从列表创建(一维、二维、三维)
t1 = torch.tensor([2.0, 3.0, 4.0]) # 一维张量,shape [3]
t2 = torch.tensor([[1.0, 2.0], [3.0, 4.0]]) # 二维张量,shape [2,2]
t3 = torch.tensor([[[1, 2], [3, 4]], [[5, 6], [7, 8]]]) # 三维张量,shape [2,2,2]
# 注意:每维元素数量必须一致,否则报错
# torch.tensor([[1.0, 2.0], [3.0, 4.0, 5.0]]) # ValueError
# 2. 按形状创建(全0、全1、全指定值)
z = torch.zeros(2, 3) # 全0,shape [2,3]
o = torch.ones(2, 3) # 全1
f = torch.full((2, 3), 10) # 全10
# 3. 按区间创建
a = torch.arange(1, 5, 1) # [start, end) 步长1 → tensor([1, 2, 3, 4])
l = torch.linspace(1, 5, 5) # [start, end] 均匀5个点 → tensor([1., 2., 3., 4., 5.])
# arange 不包含 end,linspace 包含 end
5. 张量的属性
- 形状 :
Tensor.ndim(阶数)、Tensor.shape(各维长度)、Tensor.shape[n]、Tensor.numel()(总元素数)
- 形状修改 :
torch.reshape(tensor, new_shape):重组维度,元素不变。可用 -1 自动推断某一维
torch.unsqueeze(tensor, dim):在指定位置插入长度为 1 的新轴(常用于广播)
- 数据类型 :
Tensor.dtype,常见 torch.float32、torch.int64。创建时可指定,用 .to(dtype) 转换
- 设备位置 :
Tensor.device。tensor.to('cuda') 或 tensor.cuda() 移至 GPU;不同设备张量不能直接运算
import torch
# 创建一个四维全1张量,形状 [2, 3, 4, 5]
t = torch.ones(2, 3, 4, 5)
# 形状信息
print(t.ndim) # 4(维度个数)
print(t.shape) # torch.Size([2, 3, 4, 5])
print(t.shape[0]) # 2(第0维长度)
print(t.shape[-1]) # 5(最后一维长度)
print(t.numel()) # 120(总元素数,2*3*4*5)
# ------ 形状修改 ------
# reshape 重组维度
r = t.reshape(2, 5, 12) # 手动指定新形状
print(r.shape) # torch.Size([2, 5, 12])
# 用 -1 自动推断
r1 = t.reshape(-1) # 展平为一维,长度 120
print(r1.shape) # torch.Size([120])
r2 = t.reshape(3, -1) # 推断:120 / 3 = 40 → [3, 40]
print(r2.shape) # torch.Size([3, 40])
# unsqueeze 增加长度为1的轴
t2d = torch.ones(3, 4) # [3, 4]
print(t2d.unsqueeze(0).shape) # [1, 3, 4],在 dim=0 前插
print(t2d.unsqueeze(2).shape) # [3, 4, 1],在 dim=2 后插
# ------ 数据类型 ------
f = torch.tensor([1.0, 2.0])
print(f.dtype) # torch.float32(Python 浮点数默认)
i = f.to(torch.int64) # 转换为 int64
print(i.dtype) # torch.int64
# 创建时指定 dtype
t_int = torch.tensor([1, 2], dtype=torch.int32)
print(t_int.dtype) # torch.int32
# ------ 设备位置 ------
cpu_t = torch.tensor(1, device='cpu')
print(cpu_t.device) # cpu
if torch.cuda.is_available():
gpu_t = torch.tensor(1, device='cuda')
print(gpu_t.device) # cuda:0
# 把 CPU 张量移到 GPU
cpu_t_gpu = cpu_t.to('cuda')
print(cpu_t_gpu.device) # cuda:0
6. 张量与 NumPy 的转换
- 张量 → NumPy:
tensor.numpy()
- NumPy → 张量:
torch.from_numpy(array)(共享内存,用 torch.tensor(arr) 创建独立副本)
import torch
import numpy as np
# 1. 张量 → NumPy
t = torch.tensor([1.0, 2.0, 3.0])
arr = t.numpy()
print(type(arr)) # <class 'numpy.ndarray'>
print(arr) # [1. 2. 3.]
# 2. NumPy → 张量(共享内存)
arr2 = np.array([4.0, 5.0, 6.0])
t2 = torch.from_numpy(arr2)
print(t2) # tensor([4., 5., 6.], dtype=torch.float64)
# 修改 numpy 数组,张量跟着变(共享内存)
arr2[0] = 10.0
print(t2) # tensor([10., 5., 6.], dtype=torch.float64)
# 3. 创建独立副本(不共享内存)
arr3 = np.array([7.0, 8.0, 9.0])
t3 = torch.tensor(arr3) # 独立副本
arr3[0] = 100.0
print(t3) # tensor([7., 8., 9.]) 不受影响
7. 张量的访问与修改
- 索引规则 :从 0 开始,负数倒序;切片
start:stop:step,半开区间
- 多维索引 :逗号分隔各维度,
tensor[0, :] 取第一行,tensor[:, -1] 取最后一列
- 修改 :索引/切片可直接赋值,是原地操作。注意 :参与自动微分时建议先
clone(),避免影响梯度计算
import torch
# ----- 一维张量 -----
v = torch.tensor([0, 1, 2, 3, 4, 5, 6, 7, 8])
print(v[0]) # 单个元素 → tensor(0)
print(v[-1]) # 倒数第一个 → tensor(8)
print(v[:3]) # 前三个 → tensor([0, 1, 2])
print(v[::3]) # 步长3 → tensor([0, 3, 6])
print(v.flip(0)) # 反转(PyTorch不支持[::-1])→ tensor([8, 7, 6, 5, 4, 3, 2, 1, 0])
# ----- 二维张量 -----
m = torch.tensor([[0, 1, 2, 3],
[4, 5, 6, 7],
[8, 9, 10, 11]])
print(m[0]) # 第一行 → tensor([0, 1, 2, 3])
print(m[0, :]) # 同上
print(m[:, 0]) # 第一列 → tensor([0, 4, 8])
print(m[:, -1]) # 最后一列 → tensor([3, 7, 11])
print(m[0, 1]) # 第0行第1列 → tensor(1)
# 获取所有元素(等同于m[:])
print(m[:])
# 输出:
# tensor([[ 0, 1, 2, 3],
# [ 4, 5, 6, 7],
# [ 8, 9, 10, 11]])
# ----- 修改张量(原地操作)-----
t = torch.ones(2, 3)
print('原始:', t)
# 原始: tensor([[1., 1., 1.],
# [1., 1., 1.]])
t[0] = 0 # 修改第一行
print('修改第一行:', t)
# 修改第一行: tensor([[0., 0., 0.],
# [1., 1., 1.]])
t[:, 1:] = 5 # 修改第1列及之后的所有列
print('修改右侧列:', t)
# 修改右侧列: tensor([[0., 5., 5.],
# [1., 5., 5.]])
t[...] = 3 # 全部修改为3
print('全部修改:', t)
# 全部修改: tensor([[3., 3., 3.],
# [3., 3., 3.]])
# 注意:自动微分前如需保留原值,先 clone()
x = torch.tensor([1., 2., 3.], requires_grad=True)
y = x.clone().add_(1) # 在副本上修改,不影响 x
print(y) # tensor([2., 3., 4.])
8. 张量的运算
8.1 逐元素数学运算
- 常用:
abs, ceil, floor, round, exp, log, sqrt, sin, cos
- 四则运算:
+ / add, - / sub, * / mul, / / div, % / fmod, ** / pow
- 聚合:
max, min, sum, prod(可指定维度)
import torch
x = torch.tensor([-1.5, 2.3, 3.8])
y = torch.tensor([4.0, 5.0, 6.0])
# 一元运算
print(x.abs()) # tensor([1.5000, 2.3000, 3.8000])
print(x.ceil()) # tensor([-1., 3., 4.])
print(x.floor()) # tensor([-2., 2., 3.])
print(x.round()) # tensor([-2., 2., 4.])
print(torch.exp(x)) # e^x
print(torch.log(x.abs()+1)) # ln(|x|+1)
print(x.sqrt().conj()) # 对负数取平方根需先处理,这里仅演示非负情况
# 四则运算
print(x + y) # tensor([2.5000, 7.3000, 9.8000])
print(x - y) # tensor([-5.5000, -2.7000, -2.2000])
print(x * y) # tensor([ -6.0000, 11.5000, 22.8000])
print(x / y) # tensor([-0.3750, 0.4600, 0.6333])
print(x % y) # tensor([2.5000, 2.3000, 3.8000]) # 取余规则
print(x ** 2) # tensor([2.2500, 5.2900, 14.4400])
# 等价写法
print(torch.add(x, y)) # tensor([2.5000, 7.3000, 9.8000])
print(x.sub(y)) # tensor([-5.5000, -2.7000, -2.2000])
print(x.mul(y)) # tensor([ -6.0000, 11.5000, 22.8000])
print(x.div(y)) # tensor([-0.3750, 0.4600, 0.6333])
# 聚合运算
print(x.sum()) # tensor(4.6000) 所有元素和
print(x.prod()) # tensor(-13.1100) 所有元素积
print(x.max()) # tensor(3.8000) 最大值
print(x.min()) # tensor(-1.5000) 最小值
# 指定维度的聚合(二维例子)
A = torch.tensor([[1., 2., 3.],
[4., 5., 6.]])
print(A.sum(dim=0)) # 按列求和: tensor([5., 7., 9.])
print(A.sum(dim=1)) # 按行求和: tensor([ 6., 15.])
print(A.max(dim=1)) # 每行最大值及索引
# values=tensor([3., 6.]), indices=tensor([2, 2])
8.2 逻辑运算
- 逐元素比较:
eq, ne, lt, le, gt, ge(返回布尔张量)
- 整体判断:
torch.equal(x,y)(形状和值完全相同),torch.allclose(x,y)(近似相等)
import torch
x = torch.tensor([1.0, 2.0, 3.0])
y = torch.tensor([1.0, 0.0, 3.0])
# 逐元素比较(返回布尔张量)
print(x.eq(y)) # tensor([ True, False, True])
print(x.ne(y)) # tensor([False, True, False])
print(x.lt(y)) # tensor([False, False, False])
print(x.le(y)) # tensor([ True, False, True])
print(x.gt(y)) # tensor([False, True, False])
print(x.ge(y)) # tensor([ True, True, True])
# 也可以用魔法方法
print(x == y) # 同 eq: tensor([ True, False, True])
print(x > y) # 同 gt: tensor([False, True, False])
# 整体判断
z = torch.tensor([1.0, 2.0, 3.0])
print(torch.equal(x, y)) # False (形状相同但值不同)
print(torch.equal(x, z)) # True (形状和值完全相同)
# 近似相等(可容忍一定误差)
a = torch.tensor([1.0, 2.0])
b = torch.tensor([1.0001, 1.9999])
print(torch.allclose(a, b)) # True (默认 rtol=1e-05, atol=1e-08)
8.3 矩阵运算
- 转置:
x.T(仅2D),x.transpose(dim0, dim1)
- 范数:
torch.linalg.norm(x)、torch.linalg.vector_norm(x)
- 矩阵乘法:
x.matmul(y) 或 x @ y。高维时最后两维做矩阵乘,其余维度广播
import torch
# ----- 转置 -----
A = torch.tensor([[1., 2., 3.],
[4., 5., 6.]])
print(A.T) # tensor([[1., 4.],
# [2., 5.],
# [3., 6.]])
print(A.transpose(0, 1)) # 同 A.T
B = torch.randn(2, 3, 4)
print(B.transpose(0, 2).shape) # [4, 3, 2],交换0和2维
# ----- 范数 -----
X = torch.tensor([[1., 2.],
[3., 4.]])
# 弗罗贝尼乌斯范数(矩阵)
print(torch.linalg.norm(X, ord='fro')) # tensor(5.4772)
# L2 范数(向量)
v = torch.tensor([3., 4.])
print(torch.linalg.vector_norm(v, ord=2)) # tensor(5.)
# ----- 矩阵乘法 -----
A = torch.tensor([[1., 2.],
[3., 4.]])
B = torch.tensor([[5., 6.],
[7., 8.]])
print(torch.matmul(A, B)) # tensor([[19., 22.],
# [43., 50.]])
print(A @ B) # 等价写法
# 一维向量点积
u = torch.tensor([1., 2., 3.])
v = torch.tensor([4., 5., 6.])
print(torch.matmul(u, v)) # tensor(32.)
# 高维矩阵乘法(后两维做乘法,其余广播)
x = torch.randn(10, 1, 5, 2) # [10, 1, 5, 2]
y = torch.randn(3, 2, 5) # [3, 2, 5]
z = torch.matmul(x, y)
print(z.shape) # torch.Size([10, 3, 5, 5])
8.4 广播机制
- 规则:从最后维对齐,每维大小相等,或其中一个是1,或缺失。结果形状取每维最大值
- 应用 :逐元素运算、
matmul 的高维广播
- matmul 特殊规则:一维向量会自动视为矩阵,高维张量保留 batch 维度广播
import torch
# ----- 可广播的情况 -----
# 形状相同,直接运算
x = torch.ones(2, 3, 4)
y = torch.ones(2, 3, 4)
z = x + y
print('同形状广播:', z.shape) # torch.Size([2, 3, 4])
# 形状不同但符合规则
x = torch.ones(2, 3, 1, 5)
y = torch.ones( 3, 4, 1)
# 从后往前对齐:
# 最后一维:5 vs 1 → 可广播
# 倒数第二:1 vs 4 → 可广播
# 倒数第三:3 vs 3 → 相等
# 倒数第四:2 vs 缺失 → 可广播
z = x + y
print('不同形状广播:', z.shape) # torch.Size([2, 3, 4, 5])
# 标量与张量广播
a = torch.ones(3, 4)
b = 2.0 # 标量视为形状 [],与任意张量可广播
print((a + b).shape) # torch.Size([3, 4])
# ----- 不可广播的情况 -----
x = torch.ones(2, 3, 4)
y = torch.ones(2, 3, 6)
# 最后一维:4 vs 6,都不为1且不相等 → 报错
# z = x + y # RuntimeError
# ----- 结果形状推导 -----
x = torch.ones(2, 3, 1, 5)
y = torch.ones( 3, 4, 1)
# 1) y 前面补1:y形状变 [1, 3, 4, 1]
# 2) 逐维取max:max(2,1)=2, max(3,3)=3, max(1,4)=4, max(5,1)=5
# 结果形状:[2, 3, 4, 5]
# ----- matmul 广播 -----
x = torch.randn(10, 1, 5, 2)
y = torch.randn( 3, 2, 5)
z = torch.matmul(x, y)
print('matmul 广播:', z.shape) # torch.Size([10, 3, 5, 5])
# 最后两维 [5,2] x [2,5] → [5,5]
# 其余维度 [10,1] 与 [3] 广播 → [10,3]
9. 原位操作注意点
- 函数名以
_ 结尾为原位操作(如 x.add_(y)),直接修改原张量,节省内存但可能破坏计算图
- 需要保留原值的场景先
clone()
import torch
# 原位操作 vs 非原位操作
x = torch.tensor([1., 2., 3.])
y = torch.tensor([4., 5., 6.])
# 非原位:返回新张量,x 不变
z = x.add(y)
print('非原位 x:', x) # tensor([1., 2., 3.])
print('z:', z) # tensor([5., 7., 9.])
# 原位:x 直接被修改(函数名以下划线结尾)
x.add_(y)
print('原位后 x:', x) # tensor([5., 7., 9.])
# 常见原位操作举例
a = torch.ones(2, 2)
a.zero_() # 原位填0
print(a) # tensor([[0., 0.], [0., 0.]])
b = torch.tensor([1., 2.])
b.mul_(3) # 原位乘
print(b) # tensor([3., 6.])
# 自动微分场景下的注意事项
w = torch.tensor([2., 3.], requires_grad=True)
# 错误做法:直接修改
# w.add_(1) # 可能破坏计算图,运行时可能报错
# 正确做法:先克隆再修改
w_clone = w.clone().add_(1)
print('w:', w) # tensor([2., 3.], requires_grad=True) 原张量不变
print('w_clone:', w_clone) # tensor([3., 4.], grad_fn=<AddBackward0>)