【深度学习】蒲公英书笔记 | 环境配置、张量基础速查

《蒲公英书》第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 torchtorch.__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,均匀分点
python 复制代码
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.float32torch.int64。创建时可指定,用 .to(dtype) 转换
  • 设备位置Tensor.devicetensor.to('cuda')tensor.cuda() 移至 GPU;不同设备张量不能直接运算
python 复制代码
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) 创建独立副本)
python 复制代码
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(),避免影响梯度计算
python 复制代码
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(可指定维度)
python 复制代码
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)(近似相等)
python 复制代码
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。高维时最后两维做矩阵乘,其余维度广播
python 复制代码
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 维度广播
python 复制代码
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()
python 复制代码
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>)