PyTorch 2.x 深度学习专题【左扬精讲】—— 张量(Tensor):从入门到精通的完全指南

PyTorch 2.x 深度学习专题【左扬精讲】—— 张量(Tensor):从入门到精通的完全指南

张量(Tensor)是 PyTorch 和深度学习的基石。

无论是构建神经网络、处理图像数据、还是训练大语言模型,一切都从张量开始。

很多初学者对张量感到困惑,是因为没有从根本上理解它的本质。

本文将用最通俗易懂的方式,带你彻底搞懂 PyTorch 张量的所有核心概念,包括:NumPy 与 PyTorch Tensor 的区别、四元身份、创建方式、索引切片、广播机制、自动求导、设备迁移、内存布局、类型转换、与 NumPy 互转,以及从各种文件加载张量的方法。

这是一篇面向零基础的超详细教程,学完本文,你将对 PyTorch 张量有全面而深入的理解。

PyTorch 2.x Tensor NumPy 深度学习 张量 自动求导 GPU

学习重点提示

  • 必须掌握:NumPy ndarray 与 PyTorch Tensor 的核心区别
  • 必须掌握:张量的四元身份:shape / dtype / device / layout
  • 必须掌握:张量的各种创建方式
  • 必须掌握:索引与切片、广播机制
  • 必须掌握:自动求导机制(requires_grad、grad_fn、backward)
  • 必须掌握:设备迁移(CPU/GPU/MPS)
  • 建议掌握:内存布局(stride、contiguous、memory_format)
  • 建议掌握:从各种文件加载张量的方法

第一章 从认识张量开始

1.1 什么是张量?(What)

张量(Tensor)是 PyTorch 中用于存储和计算数据的基本数据结构。你可以把它理解为多维数组的数学概念在计算机中的实现。

在数学中:

  • 标量(Scalar):0 维张量,一个单独的数,如 3.14
  • 向量(Vector):1 维张量,一维数组,如 [1, 2, 3, 4]
  • 矩阵(Matrix):2 维张量,二维数组,如一张灰度图像
  • 张量(Tensor):3 维及以上的数组,如彩色图像(RGB 三通道)、视频(多帧图像)

Why — 为什么深度学习需要张量?

神经网络本质上是对数据进行一系列数学运算(矩阵乘法、卷积等)。张量提供了:

  • 统一的数据容器:无论是一维文本、二维图像、还是三维视频,都可以用张量表示
  • 高效的计算:张量运算可以在 CPU、GPU、甚至 TPU 上高效执行
  • 自动求导:PyTorch 的张量支持自动微分,这是神经网络训练的基础

1.2 张量的直观理解

让我们用日常生活中的例子来理解张量的维度:

标量 (0D):  42                              # 一个温度值
    向量 (1D):  [1, 2, 3, 4, 5]              # 一天的气温变化
    矩阵 (2D):  [[1, 2, 3],                  # 一周的天气数据
                 [4, 5, 6],
                 [7, 8, 9]]
    3D张量:     [[[1,2], [3,4]],              # 一栋楼的温度传感器
                  [[5,6], [7,8]]]
    4D张量:     [[[[1,2], [3,4]],             # 一个小区所有房间的温度
                   [[5,6], [7,8]]]]

深度学习中的张量

  • 一维张量:文本的词向量、模型的参数向量
  • 二维张量:Excel 表格数据、词袋模型的特征向量集合
  • 三维张量:时间序列数据(如股票价格)、一句话的词向量序列
  • 四维张量:图像数据(batch × channel × height × width)
  • 五维张量:视频数据(batch × channel × frames × height × width)

1.3 快速体验:创建你的第一个张量

import torch
    
    # 创建一个简单的 1D 张量(类似 Python 列表)
    t1 = torch.tensor([1, 2, 3, 4, 5])
    print(f"1D 张量: {t1}")
    print(f"形状: {t1.shape}, 数据类型: {t1.dtype}")
    
    # 创建一个 2D 张量(类似 Excel 表格)
    t2 = torch.tensor([[1, 2, 3],
                       [4, 5, 6]])
    print(f"\n2D 张量:\n{t2}")
    print(f"形状: {t2.shape}")
    
    # 创建一个 3D 张量(类似魔方)
    t3 = torch.tensor([[[1, 2], [3, 4]],
                       [[5, 6], [7, 8]]])
    print(f"\n3D 张量:\n{t3}")
    print(f"形状: {t3.shape}")

第二章 NumPy ndarray vs PyTorch Tensor 深度对比

2.1 它们是什么?

NumPy ndarrayPyTorch Tensor 本质上都是多维数组,它们解决的问题是类似的,但在设计目标和使用场景上有显著差异。

What — 两者分别是什么?

NumPy ndarray 是 Python 科学计算的基础数据结构,由 NumPy 库提供,专门用于高效的数组运算,不支持 GPU 加速。

PyTorch Tensor 是 PyTorch 框架的核心数据结构,不仅支持高效的数组运算,还原生支持 GPU 加速和自动求导。

2.2 核心差异对比

对比项NumPy ndarrayPyTorch Tensor
GPU 加速 不支持,需要额外库(如 CuPy) 原生支持,一行代码切换设备
自动求导 不支持,需要手动实现 原生支持,神经网络训练必备
梯度追踪 有(requires_grad 属性)
设备切换 只能在 CPU 上运行 CPU/GPU/MPS 任意切换
计算图 有(动态计算图)
深度学习框架 通用科学计算 专为神经网络设计
社区生态 成熟的科学计算生态 PyTorch 深度学习生态

2.3 代码层面的对比

创建数组/张量

import numpy as np
    import torch
    
    # NumPy:创建 ndarray
    np_array = np.array([[1, 2, 3], [4, 5, 6]])
    print(f"NumPy ndarray: type={type(np_array)}, shape={np_array.shape}")
    
    # PyTorch:创建 Tensor
    pt_tensor = torch.tensor([[1, 2, 3], [4, 5, 6]])
    print(f"PyTorch Tensor: type={type(pt_tensor)}, shape={pt_tensor.shape}")

基本运算对比

import numpy as np
    import torch
    
    # NumPy 运算
    np_a = np.array([1, 2, 3])
    np_b = np.array([4, 5, 6])
    np_result = np_a + np_b
    print(f"NumPy: {np_a} + {np_b} = {np_result}")
    
    # PyTorch 运算
    pt_a = torch.tensor([1, 2, 3])
    pt_b = torch.tensor([4, 5, 6])
    pt_result = pt_a + pt_b
    print(f"PyTorch: {pt_a} + {pt_b} = {pt_result}")

GPU 运算对比

import numpy as np
    import torch
    
    # NumPy:只能 CPU
    np_arr = np.random.randn(1000, 1000)
    # np_arr = np_arr @ np_arr  # 纯 CPU 计算
    
    # PyTorch:轻松切换 GPU
    if torch.cuda.is_available():
        pt_tensor = torch.randn(1000, 1000).cuda()
        pt_result = pt_tensor @ pt_tensor  # GPU 并行计算
        print(f"GPU 上计算,设备: {pt_result.device}")
    elif torch.backends.mps.is_available():
        pt_tensor = torch.randn(1000, 1000).mps()
        pt_result = pt_tensor @ pt_tensor  # Apple M 系列 GPU
        print(f"MPS 上计算,设备: {pt_result.device}")

自动求导对比

import numpy as np
    import torch
    
    # NumPy:不支持自动求导
    np_a = np.array([1.0, 2.0, 3.0])
    np_b = np.array([4.0, 5.0, 6.0])
    # 如果要计算梯度,需要手动实现反向传播
    
    # PyTorch:原生支持自动求导
    pt_a = torch.tensor([1.0, 2.0, 3.0], requires_grad=True)
    pt_b = torch.tensor([4.0, 5.0, 6.0], requires_grad=True)
    pt_c = pt_a * pt_b  # 前向传播
    pt_loss = pt_c.sum()  # 计算损失
    pt_loss.backward()  # 反向传播
    print(f"pt_a 的梯度: {pt_a.grad}")

2.4 什么时候用哪个?

选择指南

  • 使用 NumPy 的场景
    • 数据预处理(Excel、CSV 数据分析)
    • 非深度学习的科学计算
    • 快速的原型验证(不考虑 GPU)
    • 与传统机器学习算法结合(Scikit-learn)
  • 使用 PyTorch Tensor 的场景
    • 深度学习模型开发
    • 需要 GPU 加速的计算
    • 神经网络训练和微调
    • 需要自动求导的优化问题

最佳实践

在实际项目中,通常的流程是:使用 NumPy 进行数据加载和预处理,然后转换为 PyTorch Tensor 进行模型训练。两者的转换非常方便。

第三章 张量的四元身份

3.1 每个张量都有四个核心属性

理解张量的四个核心属性,是掌握 PyTorch 的关键:

属性含义类比
shape 张量的形状/维度 房间的大小(长×宽×高)
dtype 元素的数据类型 房间地板的材质(木/砖/大理石)
device 存储设备 房间所在的楼层(CPU一楼/GPU二楼)
layout 内存布局方式 家具的排列方式(连续/稀疏)

3.2 shape - 张量的形状

shape 描述了张量在每个维度上的大小。

import torch
    
    # 标量(0 维)
    s = torch.tensor(42)
    print(f"标量: value={s}, shape={s.shape}")  # torch.Size([])
    
    # 向量(1 维)
    v = torch.tensor([1, 2, 3, 4, 5])
    print(f"向量: value={v}, shape={v.shape}")  # torch.Size([5])
    
    # 矩阵(2 维)
    m = torch.randn(3, 4)  # 3行4列
    print(f"矩阵: shape={m.shape}")  # torch.Size([3, 4])
    
    # 3D 张量
    t3d = torch.randn(2, 3, 4)  # 2个3x4的矩阵
    print(f"3D 张量: shape={t3d.shape}")  # torch.Size([2, 3, 4])
    
    # 4D 张量(常见于图像)
    # batch_size × channels × height × width
    batch = torch.randn(8, 3, 224, 224)  # 8张RGB图像,每张224x224
    print(f"4D 图像张量: shape={batch.shape}")  # torch.Size([8, 3, 224, 224])

shape 的维度顺序记忆口诀

图像张量:BCHW(Batch / Channel / Height / Width)

视频张量:BCTHW(Batch / Channel / Time / Height / Width)

3.3 dtype - 张量的数据类型

dtype 指定了张量中每个元素的数值类型。

dtype说明内存占用适用场景
torch.float32 / torch.float 32位浮点数 4 bytes 默认,神经网络权重和计算
torch.float64 / torch.double 64位浮点数 8 bytes 高精度科学计算
torch.float16 / torch.half 16位浮点数 2 bytes 混合精度训练、节省显存
torch.bfloat16 Brain Float 16 2 bytes 大模型训练,与 float32 指数范围相同
torch.int32 / torch.int 32位整数 4 bytes 一般整数运算
torch.int64 / torch.long 64位整数 8 bytes 索引、数组大小
torch.int16 / torch.short 16位整数 2 bytes 节省内存
torch.int8 8位整数 1 byte 量化、字符编码
torch.uint8 无符号8位整数 1 byte 图像像素(0-255)
torch.bool 布尔类型 1 byte 条件判断、掩码
import torch
    
    # 创建不同 dtype 的张量
    t_float32 = torch.tensor([1.0, 2.0, 3.0], dtype=torch.float32)
    t_float64 = torch.tensor([1.0, 2.0, 3.0], dtype=torch.float64)
    t_int32 = torch.tensor([1, 2, 3], dtype=torch.int32)
    t_int64 = torch.tensor([1, 2, 3], dtype=torch.int64)
    t_uint8 = torch.tensor([0, 127, 255], dtype=torch.uint8)  # 图像像素
    t_bool = torch.tensor([True, False, True], dtype=torch.bool)
    
    print(f"float32: {t_float32.dtype}")
    print(f"float64: {t_float64.dtype}")
    print(f"int32: {t_int32.dtype}")
    print(f"int64: {t_int64.dtype}")
    print(f"uint8: {t_uint8.dtype}")
    print(f"bool: {t_bool.dtype}")
    
    # dtype 转换
    t_converted = t_int32.float()  # 转为 float32
    print(f"转换后 dtype: {t_converted.dtype}")

3.4 device - 张量的存储设备

device 指定了张量存储在哪种硬件设备上。

device说明示例
cpu CPU 内存 默认设备
cuda:0, cuda:1, ... NVIDIA GPU GPU 加速计算
mps Apple M 系列芯片 GPU Mac 设备加速
xpu Intel GPU Intel 显卡
import torch
    
    # 查看当前可用的设备
    print(f"CUDA 可用: {torch.cuda.is_available()}")
    print(f"MPS 可用: {torch.backends.mps.is_available()}")
    
    # 默认在 CPU 上
    t_cpu = torch.tensor([1, 2, 3])
    print(f"默认设备: {t_cpu.device}")  # cpu
    
    # 创建时指定设备
    if torch.cuda.is_available():
        t_gpu = torch.tensor([1, 2, 3], device='cuda')
        print(f"GPU 设备: {t_gpu.device}")  # cuda:0
    
    # 设备切换
    t = torch.randn(3, 4)
    print(f"原始设备: {t.device}")  # cpu
    
    if torch.cuda.is_available():
        t_gpu = t.cuda()  # 或 t.to('cuda')
        print(f"GPU 设备: {t_gpu.device}")  # cuda:0
        t_back = t_gpu.cpu()  # 切回 CPU
        print(f"切回 CPU: {t_back.device}")

3.5 layout - 张量的内存布局

layout 描述了张量在内存中的存储方式。

layout说明适用场景
torch.strided 连续内存块,按步长访问(默认) 大多数场景,高效存储和计算
torch.sparse_coo 稀疏坐标格式,只存储非零元素 稀疏数据(如词向量、图数据)
torch.sparse_csr 稀疏压缩行格式 大型稀疏矩阵
torch.mkldnn Intel MKL-DNN 格式 特定优化场景
import torch
    
    # 默认的 strided 布局
    t = torch.tensor([[1, 2, 3], [4, 5, 6]])
    print(f"默认布局: {t.layout}")  # torch.strided
    
    # 创建稀疏张量(COO 格式)
    indices = torch.tensor([[0, 1, 2], [0, 1, 2]])  # 非零元素的索引
    values = torch.tensor([1, 2, 3])  # 非零元素的值
    size = (3, 3)
    sparse_t = torch.sparse_coo_tensor(indices, values, size)
    print(f"稀疏张量布局: {sparse_t.layout}")
    print(f"稀疏张量值:\n{sparse_t.to_dense()}")

3.6 四元身份的综合示例

import torch
    
    # 查看一个张量的完整四元身份
    t = torch.randn(2, 3, 4, dtype=torch.float32, device='cpu', layout=torch.strided)
    
    print("=" * 50)
    print("张量的四元身份")
    print("=" * 50)
    print(f"1. shape (形状):  {t.shape}")    # torch.Size([2, 3, 4])
    print(f"2. dtype (类型): {t.dtype}")     # torch.float32
    print(f"3. device (设备): {t.device}")   # cpu
    print(f"4. layout (布局): {t.layout}")   # torch.strided
    print(f"5. 元素总数: {t.numel()}")       # 2*3*4 = 24
    print(f"6. 内存占用: {t.element_size() * t.numel()} bytes")

第三章总结:张量的四元身份

  • shape:描述维度大小,用 torch.Size([...]) 表示
  • dtype:描述数据类型,常见有 float32/float16/int64/bool
  • device:描述存储设备,常见有 cpu/cuda/mps
  • layout:描述内存布局,常见有 strided(默认)/ sparse
  • 这四个属性决定了张量的全部特性

第四章 张量的创建方式

4.1 直接从数据创建

最直接的方式是从 Python 列表或 NumPy 数组创建张量。

import torch
    import numpy as np
    
    # 从 Python 列表创建
    t1 = torch.tensor([1, 2, 3, 4, 5])
    print(f"从列表创建: {t1}")
    
    # 从二维列表创建
    t2 = torch.tensor([[1, 2, 3], [4, 5, 6]])
    print(f"从二维列表创建:\n{t2}")
    
    # 从 NumPy 数组创建
    np_arr = np.array([[1.0, 2.0], [3.0, 4.0]])
    t3 = torch.from_numpy(np_arr)
    print(f"从 NumPy 创建:\n{t3}")
    
    # 指定 dtype
    t4 = torch.tensor([1, 2, 3], dtype=torch.float32)
    print(f"指定 dtype: {t4.dtype}")

torch.tensor() vs torch.Tensor() vs torch.as_tensor() 的区别

  • torch.tensor():推荐使用,复制数据,默认创建 leaf tensor
  • torch.Tensor():是 torch.FloatTensor() 的别名,创建指定类型的张量
  • torch.as_tensor():尽量共享内存(但不保证),适合大数组优化
import torch
    import numpy as np
    
    # torch.tensor() - 总是复制数据
    data = [1, 2, 3]
    t1 = torch.tensor(data)
    print(f"torch.tensor() 创建: id={id(t1)}")
    
    # torch.Tensor() - 创建 FloatTensor,等同于 torch.FloatTensor()
    t2 = torch.Tensor([1, 2, 3])  # 自动推断 dtype 为 float32
    print(f"torch.Tensor() 创建: dtype={t2.dtype}")
    
    # torch.as_tensor() - 尽量共享内存
    arr = np.array([1, 2, 3])
    t3 = torch.as_tensor(arr)
    t4 = torch.as_tensor(arr)
    print(f"as_tensor 共享内存: {np.shares_memory(arr, t3.numpy())}")

4.2 创建特殊值的张量

import torch
    
    # 全 0 张量
    zeros_1d = torch.zeros(5)
    zeros_2d = torch.zeros(3, 4)
    print(f"全 0 向量: {zeros_1d}")
    print(f"全 0 矩阵 shape: {zeros_2d.shape}")
    
    # 全 1 张量
    ones = torch.ones(2, 3)
    print(f"全 1 矩阵:\n{ones}")
    
    # 指定填充值
    full = torch.full((2, 3), fill_value=99)
    print(f"填充 99:\n{full}")
    
    # 单位矩阵
    eye = torch.eye(4)
    print(f"单位矩阵:\n{eye}")
    
    # 对角矩阵
    diag = torch.diag(torch.tensor([1, 2, 3]))
    print(f"对角矩阵:\n{diag}")

4.3 创建随机数张量

import torch
    
    # 均匀分布 [0, 1)
    rand = torch.rand(3, 4)
    print(f"均匀分布 [0,1):\n{rand}")
    
    # 均匀分布 [a, b)
    randint = torch.randint(low=1, high=10, size=(3, 4))
    print(f"整数均匀分布 [1,10):\n{randint}")
    
    # 标准正态分布 N(0, 1)
    randn = torch.randn(3, 4)
    print(f"标准正态分布:\n{randn}")
    
    # 正态分布 N(mean, std)
    normal = torch.normal(mean=5.0, std=2.0, size=(3, 4))
    print(f"正态分布 N(5,2):\n{normal}")
    
    # 生成随机排列
    perm = torch.randperm(10)  # 0到9的随机排列
    print(f"随机排列: {perm}")

随机数种子

为了结果可复现,需要设置随机种子:

import torch
    
    # 设置全局种子
    torch.manual_seed(42)
    
    # 每次运行结果相同
    t1 = torch.rand(3, 3)
    print(f"随机张量:\n{t1}")
    
    # 设置 CUDA 种子(如果有 GPU)
    if torch.cuda.is_available():
        torch.cuda.manual_seed(42)

4.4 基于现有张量创建

import torch
    
    # 创建全 0/1 张量,形状与现有张量相同
    x = torch.randn(3, 4)
    zeros_like = torch.zeros_like(x)
    ones_like = torch.ones_like(x)
    print(f"zeros_like shape: {zeros_like.shape}")
    print(f"ones_like shape: {ones_like.shape}")
    
    # 创建与现有张量相同 dtype 和 device 的张量
    y = torch.randn(2, 3, dtype=torch.float64)
    z = torch.zeros_like(y, dtype=torch.float32)  # 可以覆盖 dtype
    print(f"覆盖 dtype: {z.dtype}")
    
    # torch.new_ones() - 保留原有属性,但新维度
    t = torch.randn(2, 3)
    t_new = t.new_ones(4, 5)  # 保持 float32 和 device
    print(f"new_ones dtype: {t_new.dtype}")

4.5 创建序列/范围张量

import torch
    
    # torch.arange() - 包含 start,不包含 end
    arange = torch.arange(start=0, end=10, step=2)
    print(f"arange(0, 10, 2): {arange}")  # [0, 2, 4, 6, 8]
    
    # torch.linspace() - 线性间隔
    linspace = torch.linspace(start=0, end=10, steps=5)
    print(f"linspace(0, 10, 5): {linspace}")  # [0, 2.5, 5, 7.5, 10]
    
    # torch.logspace() - 对数间隔
    logspace = torch.logspace(start=-3, end=3, steps=7)
    print(f"logspace: {logspace}")

第五章 索引与切片

5.1 基本索引

PyTorch 支持与 Python 列表和 NumPy 数组类似的索引方式。

import torch
    
    # 创建 2D 张量
    t = torch.tensor([[1, 2, 3, 4],
                      [5, 6, 7, 8],
                      [9, 10, 11, 12]])
    print(f"原始张量:\n{t}")
    
    # 索引单个元素(0-based)
    print(f"t[0, 0] = {t[0, 0]}")   # 1
    print(f"t[2, 3] = {t[2, 3]}")   # 12
    
    # 负索引(从后往前)
    print(f"t[-1, -1] = {t[-1, -1]}")  # 12
    
    # 获取行
    print(f"第一行: {t[0]}")           # [1, 2, 3, 4]
    print(f"最后一行: {t[-1]}")        # [9, 10, 11, 12]
    
    # 获取列(需要用 :)
    print(f"第一列: {t[:, 0]}")        # [1, 5, 9]

5.2 切片操作

import torch
    
    t = torch.arange(1, 13).reshape(3, 4)
    print(f"原始张量:\n{t}")
    
    # 切片语法:[start:stop:step]
    print(f"前两行:\n{t[:2]}")        # 行 0, 1
    print(f"后两列:\n{t[:, 2:]}")     # 列 2, 3
    
    # 跳步切片
    print(f"奇数行:\n{t[::2]}")       # 行 0, 2
    
    # 逆序
    print(f"逆序行:\n{t[::-1]}")      # 行 2, 1, 0
    print(f"逆序列:\n{t[:, ::-1]}")    # 列 3, 2, 1, 0
    
    # 3D 张量切片
    t3d = torch.arange(1, 25).reshape(2, 3, 4)
    print(f"\n3D 张量 shape: {t3d.shape}")
    print(f"第一个矩阵:\n{t3d[0]}")
    print(f"所有矩阵的第二行:\n{t3d[:, 1, :]}")

5.3 高级索引

import torch
    
    t = torch.arange(1, 13).reshape(3, 4)
    print(f"原始张量:\n{t}")
    
    # 整数数组索引
    rows = torch.tensor([0, 2])
    cols = torch.tensor([1, 3])
    print(f"索引 [0,2] x [1,3]:\n{t[rows, cols]}")
    
    # 布尔索引(掩码)
    mask = t > 6
    print(f"大于 6 的掩码:\n{mask}")
    print(f"满足条件的值: {t[mask]}")
    
    # 使用条件筛选
    t_filtered = t[t % 2 == 0]  # 所有偶数
    print(f"所有偶数: {t_filtered}")
    
    # torch.where() 条件替换
    result = torch.where(t > 6, t, t * 10)
    print(f"大于6保留,否则乘10:\n{result}")

第六章 广播机制

6.1 什么是广播?(What)

广播(Broadcasting)是 NumPy 和 PyTorch 中一种强大的机制,允许在不同形状的张量之间进行逐元素运算,而无需显式复制数据。

Why — 为什么需要广播?

想象你要把一个偏置向量加到矩阵的每一行:

  • 传统做法:复制偏置向量到矩阵形状,耗时耗内存
  • 广播机制:让小张量"扩展"匹配大张量形状,自动完成计算

6.2 广播规则

PyTorch/NumPy 的广播规则:从右向左比较维度,满足以下条件可以广播:

  1. 两个维度相同
  2. 其中一个维度为 1
import torch
    
    # 案例1:标量 + 向量
    a = 10
    b = torch.tensor([1, 2, 3])
    result = a + b
    print(f"标量 + 向量: {result}")  # [11, 12, 13]
    
    # 案例2:向量 + 矩阵
    vec = torch.tensor([1, 2, 3])        # shape: (3,)
    mat = torch.tensor([[1, 2, 3],      # shape: (2, 3)
                         [4, 5, 6]])
    result = vec + mat  # vec 自动扩展为 (1, 3) 再广播到 (2, 3)
    print(f"向量 + 矩阵:\n{result}")
    
    # 案例3:添加批次维度
    batch = torch.randn(8, 3, 32, 32)   # 8张图像
    bias = torch.tensor([0.1, 0.2, 0.3]) # 3个通道的偏置
    result = batch + bias
    print(f"批次 + 偏置 result shape: {result.shape}")

6.3 广播的可视化理解

案例:向量 (3,) + 矩阵 (2, 3)
    
    向量:   [1, 2, 3]           → 扩展为 (1, 3)
    矩阵:  [[1, 2, 3],           (2, 3)
             [4, 5, 6]]
    
    对齐后:  [[1, 2, 3],   +  [[1, 2, 3],
              [4, 5, 6]]       [[1, 2, 3]]
           =  [[2, 4, 6],
              [5, 7, 9]]

6.4 广播的注意事项

无法广播的情况

当维度大小不相同且都不是 1 时,无法广播:

import torch
    
    a = torch.randn(3, 4)  # shape: (3, 4)
    b = torch.randn(2, 4)  # shape: (2, 4)
    # a + b  # RuntimeError: 不能广播!

错误原因:第一个维度 3 ≠ 2,且都不是 1。

import torch
    
    # 解决方案:用 unsqueeze 手动扩展维度
    a = torch.randn(3, 4)
    b = torch.randn(2, 4)
    
    # 方法:确保维度兼容
    result = a[:2] + b  # 取 a 的前两行
    
    # 或用 expand 扩展
    a_expanded = a.unsqueeze(0).expand(2, -1, -1)  # (2, 3, 4)
    result = a_expanded + b.unsqueeze(0)              # (2, 3, 4)

6.5 广播实战:图像处理

import torch
    
    # 批量图像归一化
    batch = torch.randn(16, 3, 224, 224)  # 16张 RGB 图像
    mean = torch.tensor([0.485, 0.456, 0.406])  # ImageNet 均值
    std = torch.tensor([0.229, 0.224, 0.225])   # ImageNet 标准差
    
    # 广播到每张图像的每个像素
    normalized = (batch - mean.view(1, 3, 1, 1)) / std.view(1, 3, 1, 1)
    print(f"归一化后 shape: {normalized.shape}")  # (16, 3, 224, 224)

第五章和第六章总结

  • 索引:从 0 开始,支持负索引
  • 切片:使用 start:stop:step 语法
  • 布尔索引:用于条件筛选
  • 广播规则:从右向左比较维度,相等或为 1 即可广播
  • 广播优势:节省内存,自动扩展,无需手动复制

第七章 自动求导机制

7.1 先搞懂几个核心概念(通俗理解)

在学习自动求导之前,先来理解几个基础概念。

7.1.1 什么是导数?(What)

导数描述的是"一个量变化时,另一个量跟着怎么变化"。

通俗理解导数

想象你开车:

  • 速度 = 位置对时间的导数(位置变化得多快)
  • 加速度 = 速度对时间的导数(速度变化得多快)

数学例子:

  • y = x²,当 x=3 时,导数 dy/dx = 2x = 6
  • 意思是:x 增加 1,y 大约增加 6

这和高数里的导数是同一个概念!

如果你学过高等数学,应该记得这些常见函数的导数:

函数 f(x)导数 f'(x)
2x
sin(x) cos(x)
cos(x) -sin(x)
e&supx; e&supx;
ln(x) 1/x

为什么深度学习要用导数?

神经网络训练的目标是最小化损失函数。假设损失 L = (预测值 - 真实值)²,我们要知道"每个参数怎么调能让损失变小"——这就需要对每个参数求偏导,得到梯度。

PyTorch 的作用:神经网络可能有上亿个参数,手动求导不现实。PyTorch 的 autograd 自动帮你求导,只需调用 .backward() 即可。

7.1.2 什么是梯度?(What)

梯度是导数在多维空间的推广,可以理解为"山坡最陡的方向"。

通俗理解梯度

想象你站在山坡上:

  • 梯度向量指向山坡最陡的上坡方向
  • 你要下山,就要往梯度的反方向
  • 梯度的大小表示山坡有多陡

在神经网络中:

  • 我们想知道"如何调整参数"能让损失变小
  • 梯度告诉我们参数应该往哪个方向调整

高数里的梯度定义

梯度是多元函数的偏导数组成的向量。

对于函数 f(x, y, z) = x² + y² + z²,梯度为:

∇f = (∂f/∂x, ∂f/∂y, ∂f/∂z) = (2x, 2y, 2z)

梯度的重要性质

  • 梯度向量指向函数增长最快的方向
  • 梯度的反方向(负梯度)是函数下降最快的方向
  • 梯度大小表示变化的剧烈程度

为什么叫"梯度"而不是"导数"?

一元函数叫"导数",多元函数叫"梯度"。神经网络有上万个参数,所以用"梯度"更准确。

7.1.3 什么是反向传播?(What)

反向传播(Backpropagation)是计算梯度的算法,让神经网络的"学习"成为可能。

通俗理解反向传播

想象你在考试中做错了题:

  • 前向传播:从输入到输出,就像做题得到分数
  • 反向传播:从分数倒推,找出每道题扣分的原因
  • 根据扣分原因,调整学习方法(参数更新)

在神经网络中:

  • 前向传播:输入数据 → 经过层层计算 → 得到预测结果
  • 计算损失:预测结果 vs 真实结果,差距有多大
  • 反向传播:从损失开始,反向计算每个参数对损失"贡献"了多少

反向传播的起源

反向传播算法由 David Rumelhart、Geoffrey Hinton、Ronald Williams 于 1986 年在论文《Learning representations by back-propagating errors》中提出。这是深度学习能够训练深层神经网络的关键突破。

反向传播的核心原理:链式法则

反向传播本质上是高数中的链式法则在神经网络中的应用。

假设有一个复合函数:y = f(g(x))

根据链式法则:dy/dx = dy/du · du/dx

在神经网络中,层与层之间的计算都是这种嵌套关系,反向传播就是从输出层开始,一层层用链式法则求出每个参数对损失的偏导数。

损失 L = loss(y_out, y_true)

反向传播过程:
∂L/∂W_n = ∂L/∂y_n · ∂y_n/∂W_n      (输出层)
∂L/∂W_{n-1} = ∂L/∂y_n · ∂y_n/∂y_{n-1} · ∂y_{n-1}/∂W_{n-1}  (隐藏层)
...逐层往前传播...

与高数概念的关系

  • 梯度:高数里的多元函数偏导数向量(数学基础)
  • 链式法则:高数里的复合函数求导法则(数学工具)
  • 反向传播:深度学习专用算法,结合梯度+链式法则+神经网络结构

7.1.4 什么是计算图?(What)

计算图是 PyTorch 用来记录"数据是怎么算出来的"的工具。

计算图示例:y = (a * b + c) * d
  
           a ──┐
           b ──┼──→ (*) ──→ (+) ──→ (*) ──→ y
           c ──┘                      ↑
           d ─────────────────────────┘
  
  步骤:
  1. a * b = e
  2. e + c = f
  3. f * d = y
  
  反向传播时:
  1. dy/dd = f
  2. dy/df = d
  3. dy/de = d, dy/dc = d
  4. dy/da = b * d, dy/db = a * d

计算图的作用

计算图就像一张"施工图纸",记录了:

  • 数据经过了哪些运算
  • 每个运算的输入是什么
  • 如何从输出反推每个输入的"贡献"(梯度)

7.1.5 什么是梯度追踪?(What)

梯度追踪是 PyTorch 记录计算图的能力,只有被追踪的张量才能计算梯度。

import torch
  
  # 默认不追踪梯度
  a = torch.tensor([1.0])
  print(f"默认 requires_grad: {a.requires_grad}")  # False
  
  # 开启梯度追踪
  b = torch.tensor([2.0], requires_grad=True)
  print(f"开启后 requires_grad: {b.requires_grad}")  # True
  
  # 追踪的计算会被记录
  c = b * 3  # 这个运算会被追踪
  print(f"运算后的 grad_fn: {c.grad_fn}")  # 记录了是乘法

为什么需要梯度追踪?

  • 节省内存:不需要追踪的参数不记录计算图
  • 提高效率:推理时不需要梯度,关闭追踪加速
  • 灵活控制:只想算某些张量的梯度

7.2 什么是自动求导?(What)

自动求导(Automatic Differentiation)是 PyTorch 的核心特性,它能自动计算函数的导数(梯度),无需手动编写求导代码。

Why — 为什么需要自动求导?

神经网络可能有上亿个参数,手动求导是不可能的!

  • 手动求导:每个参数都要写公式,亿级参数 = 不可能
  • 自动求导:PyTorch 自动构建计算图,一键 backward() 求导

7.3 requires_grad 属性

requires_grad 是张量的关键属性,控制是否追踪梯度。

import torch
    
    # 创建默认不追踪梯度的张量
    a = torch.tensor([1.0, 2.0, 3.0])
    print(f"requires_grad 默认: {a.requires_grad}")  # False
    
    # 创建追踪梯度的张量
    b = torch.tensor([1.0, 2.0, 3.0], requires_grad=True)
    print(f"requires_grad=True: {b.requires_grad}")  # True
    
    # 方式1:创建时指定
    c = torch.ones(3, requires_grad=True)
    
    # 方式2:使用 requires_grad_() 修改
    d = torch.ones(3)
    d.requires_grad_(True)
    print(f"修改后: {d.requires_grad}")  # True

7.3 grad_fn 反向传播函数

grad_fn 记录了创建这个张量的运算操作,用于反向传播时计算梯度。

import torch
    
    # 通过运算创建的张量会自动记录 grad_fn
    a = torch.tensor([1.0, 2.0, 3.0], requires_grad=True)
    b = a * 2
    print(f"b 的 grad_fn: {b.grad_fn}")  # 
    
    # 叶子节点 vs 非叶子节点
    print(f"a 是叶子节点: {a.is_leaf}")  # True
    print(f"b 是叶子节点: {b.is_leaf}")  # False
    
    # grad_fn 的常见类型
    # AddBackward0 - 加法
    # SubBackward0 - 减法
    # MulBackward0 - 乘法
    # DivBackward0 - 除法
    # MatMulBackward0 - 矩阵乘法
    # SumBackward0 - 求和
    # ReluBackward0 - ReLU 激活

7.4 反向传播实例

import torch
    
    # 示例:计算 y = (a * b + c) * d 的梯度
    a = torch.tensor([1.0], requires_grad=True)
    b = torch.tensor([2.0], requires_grad=True)
    c = torch.tensor([0.0], requires_grad=True)
    d = torch.tensor([3.0], requires_grad=True)
    
    # 前向传播
    y = (a * b + c) * d
    print(f"y = {y}")  # y = (1*2 + 0) * 3 = 6
    
    # 反向传播
    y.backward()
    print(f"dy/da = {a.grad}")  # dy/da = d(b*d)/da = 2*3 = 6
    print(f"dy/db = {b.grad}")  # dy/db = d(a*d)/db = 1*3 = 3
    print(f"dy/dc = {c.grad}")  # dy/dc = d*1 = 3
    print(f"dy/dd = {d.grad}")  # dy/dd = a*b + c = 2

重要:梯度会累积

调用 backward() 会累积梯度到 .grad 属性,而不是替换。如果需要重新计算梯度,先用 optimizer.zero_grad() 清零。

# 梯度累积示例
    loss.backward()  # 第一次
    print(f"梯度累积: {x.grad}")  # 有值
    
    loss.backward()  # 第二次
    print(f"梯度翻倍: {x.grad}")  # 值翻倍了!
    
    # 正确做法:每次反向传播前清零
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

7.5 梯度计算的高级用法

import torch
    
    # 多次梯度计算
    x = torch.tensor([2.0], requires_grad=True)
    y = x ** 2  # y = x^2
    
    # 第一次求导:dy/dx = 2x = 4
    gradients = torch.autograd.grad(y, x)
    print(f"dy/dx = {gradients[0]}")  # tensor([4.])
    
    # 第二次求导:d²y/dx² = 2
    gradients_2nd = torch.autograd.grad(gradients[0], x)
    print(f"d²y/dx² = {gradients_2nd[0]}")  # tensor([2.])
    
    # 梯度不追踪(no_grad)
    x = torch.tensor([1.0], requires_grad=True)
    with torch.no_grad():
        y = x * 2  # 这个运算不会追踪梯度
    print(f"no_grad 中: {y.requires_grad}")  # False
    
    # detach - 切断梯度追踪
    x = torch.tensor([1.0], requires_grad=True)
    y = x * 2
    z = y.detach()  # 创建一个新的张量,切断与计算图的连接
    print(f"detach 后: {z.requires_grad}")  # False

第八章 设备迁移

8.1 什么是设备迁移?(What)

设备迁移是指将张量在不同硬件设备(CPU、GPU、MPS)之间移动。GPU 可以大幅加速张量运算。

Why — 为什么需要设备迁移?

深度学习训练需要大量矩阵运算:

  • CPU:适合小数据量,灵活但速度慢
  • GPU:并行计算能力强,适合大规模矩阵运算,加速 10-100 倍
  • MPS:Apple M 系列芯片的 GPU 方案

8.2 检查可用设备

import torch
    
    # 检查 CUDA 是否可用
    print(f"CUDA 可用: {torch.cuda.is_available()}")
    
    if torch.cuda.is_available():
        # GPU 信息
        print(f"GPU 数量: {torch.cuda.device_count()}")
        print(f"当前 GPU: {torch.cuda.current_device()}")
        print(f"GPU 名称: {torch.cuda.get_device_name(0)}")
        print(f"CUDA 版本: {torch.version.cuda}")
    
    # 检查 MPS 是否可用(Apple Silicon)
    print(f"MPS 可用: {torch.backends.mps.is_available()}")
    if torch.backends.mps.is_available():
        print(f"MPS 可构建: {torch.backends.mps.is_built()}")

8.3 设备迁移方法

import torch
    
    # 默认在 CPU 上创建
    t_cpu = torch.randn(3, 4)
    print(f"默认设备: {t_cpu.device}")  # cpu
    
    # 方式1:.cuda() / .cpu()
    if torch.cuda.is_available():
        t_gpu = t_cpu.cuda()  # 移到 GPU
        t_back = t_gpu.cpu()  # 移回 CPU
    
    # 方式2:.to(device)
    t = torch.randn(3, 4)
    if torch.cuda.is_available():
        t_gpu = t.to('cuda')
        t_gpu = t.to(device='cuda:0')  # 指定 GPU 编号
    
    # 方式3:创建时指定设备
    if torch.cuda.is_available():
        t = torch.randn(3, 4, device='cuda')
        print(f"创建时指定设备: {t.device}")
    
    # MPS(Apple Silicon)
    if torch.backends.mps.is_available():
        t_mps = t_cpu.to('mps')
        print(f"MPS 设备: {t_mps.device}")

8.4 设备迁移的注意事项

设备不匹配错误

常见的错误是在 CPU 和 GPU 之间混合运算:

import torch
    
    a = torch.randn(3, 4)  # CPU
    if torch.cuda.is_available():
        b = torch.randn(3, 4).cuda()  # GPU
        # c = a + b  # RuntimeError! CPU + GPU 不兼容
    
    # 解决方案:确保在同一设备上
    c = a + b.cpu()  # 移回 CPU
    if torch.cuda.is_available():
        c = a.cuda() + b  # 都移到 GPU

8.5 跨设备迁移的最佳实践

import torch
    
    # 最佳实践1:设置默认设备
    if torch.cuda.is_available():
        torch.cuda.set_device(0)  # 设置默认 GPU
    
    # 最佳实践2:使用 device 上下文管理器
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    print(f"使用设备: {device}")
    
    # 创建张量时指定设备
    t = torch.randn(3, 4, device=device)
    
    # 迁移模型到设备
    model = torch.nn.Linear(4, 2)
    model = model.to(device)
    
    # 批次数据迁移
    batch = torch.randn(16, 4)
    batch = batch.to(device)
    output = model(batch)
    print(f"输出设备: {output.device}")

第九章 内存布局

9.1 什么是内存布局?(What)

内存布局(Memory Layout)描述了张量数据在内存中的存储方式。理解内存布局对于优化性能至关重要。

Why — 为什么理解内存布局重要?

  • 连续内存:访问效率高,某些操作需要连续内存
  • 非连续内存:切片、transpose 后会产生,访问效率低
  • 了解布局可以避免不必要的内存拷贝

9.2 stride - 步长

stride 表示在某个维度上移动到下一个元素需要跳过的元素数量。

import torch
    
    # 创建一个 2D 张量
    t = torch.arange(12).reshape(3, 4)
    print(f"张量:\n{t}")
    print(f"shape: {t.shape}")   # (3, 4)
    print(f"stride: {t.stride()}")  # (4, 1)
    
    # 内存理解:
    # stride[0] = 4:跳到下一行需要跳过 4 个元素
    # stride[1] = 1:跳到下一列需要跳过 1 个元素
    
    # 对比:行优先 vs 列优先
    t_row = torch.arange(12).reshape(3, 4)
    t_col = t_row.T  # 转置
    print(f"\n行优先 stride: {t_row.stride()}")  # (4, 1)
    print(f"列优先 stride: {t_col.stride()}")    # (1, 4)

stride 的直观理解

stride 就像一个"地图",告诉我们如何用"步长"在内存中导航:

  • stride[0] = 4:往下走一步 = 跳过 4 个元素
  • stride[1] = 1:往右走一步 = 跳过 1 个元素

9.3 contiguous vs 非连续

contiguous 表示张量在内存中是连续存储的。

import torch
    
    # 连续张量
    t = torch.arange(12).reshape(3, 4)
    print(f"连续: {t.is_contiguous()}")  # True
    
    # 非连续张量(通过切片)
    t_slice = t[:, ::2]  # 取偶数列
    print(f"切片后连续: {t_slice.is_contiguous()}")  # False
    print(f"切片后 stride: {t_slice.stride()}")  # (4, 2)
    
    # transpose 也会产生非连续张量
    t_t = t.T
    print(f"转置后连续: {t_t.is_contiguous()}")  # False

9.4 内存布局的性能影响

import torch
    
    t = torch.randn(1000, 1000)
    
    # 连续访问 vs 非连续访问
    t_cont = t.clone()  # 连续
    t_noncont = t[:, ::2]  # 非连续
    
    # view() 需要连续内存
    # t_noncont.view(500, 1000)  # RuntimeError!
    
    # 解决方案:调用 contiguous()
    t_contiguous = t_noncont.contiguous()
    print(f"contiguous 后: {t_contiguous.is_contiguous()}")  # True
    print(f"contiguous 后 shape: {t_contiguous.shape}")  # (1000, 500)
    
    # reshape() 更灵活,内部会自动处理
    result = t_noncont.reshape(500, 1000)  # 不会报错

9.5 memory_format - 内存格式

memory_format 指定张量的内存排列方式。

import torch
    
    # torch.contiguous_format - 标准行优先
    t1 = torch.randn(2, 3, 224, 224)
    print(f"默认格式: {t1.memory_format}")  # torch.contiguous_format
    
    # torch.channels_last - NHWC 格式(适合某些 GPU 操作)
    t_cl = t1.to(memory_format=torch.channels_last)
    print(f"channels_last 格式: {t_cl.memory_format}")
    
    # 验证 channels_last 的优势
    print(f"默认 stride: {t1.stride()}")      # (150528, 224, 1, 50176)
    print(f"channels_last stride: {t_cl.stride()}")  # (301056, 1, 150528, 224)

第七到第九章总结

  • 自动求导:requires_grad 开启追踪,backward() 求导,grad_fn 记录运算
  • 设备迁移:.to(device) 跨设备迁移,确保操作在同一设备上
  • 内存布局:stride 控制内存访问,is_contiguous() 检查连续性,contiguous() 确保连续

第十章 类型转换

10.1 dtype 转换方法

PyTorch 提供了多种 dtype 转换方法。

import torch
    
    # 创建不同类型的张量
    t_int = torch.tensor([1, 2, 3], dtype=torch.int32)
    t_float = torch.tensor([1.5, 2.5, 3.5])
    
    # 方法1:.dtype 属性访问
    print(f"当前 dtype: {t_int.dtype}")  # torch.int32
    
    # 方法2:直接指定 dtype 创建
    t_new = torch.tensor([1, 2, 3], dtype=torch.float32)
    
    # 方法3:类型转换方法
    print(f"int32 -> float32: {t_int.float()}")    # torch.float32
    print(f"float32 -> int32: {t_float.int()}")    # torch.int32
    print(f"float32 -> int64: {t_float.long()}")    # torch.int64
    print(f"int64 -> float32: {t_int.to(torch.float32)}")  # 通用方法
    
    # 方法4:使用 .type() 方法
    t = torch.tensor([1, 2, 3])
    t_float = t.type(torch.float32)
    print(f"type() 转换: {t_float.dtype}")  # torch.float32

10.2 常用 dtype 转换场景

import torch
    
    # 场景1:float32 与 float16(混合精度训练)
    t_fp32 = torch.randn(3, 4)
    t_fp16 = t_fp32.half()  # 转 float16,节省显存
    t_back_fp32 = t_fp16.float()  # 转回 float32
    print(f"fp32 -> fp16 -> fp32: {t_fp32.shape}")
    
    # 场景2:float32 与 bfloat16(大模型训练)
    t_bf16 = t_fp32.bfloat16()  # 转 bfloat16
    print(f"bfloat16 dtype: {t_bf16.dtype}")
    
    # 场景3:int 与 float 互转
    t_int = torch.tensor([1, 2, 3])
    t_float = t_int.float()  # int -> float
    t_back_int = t_float.long()  # float -> int(截断)
    print(f"int -> float: {t_float}, float -> int: {t_back_int}")
    
    # 场景4:bool 与数值类型
    t_bool = torch.tensor([True, False, True])
    t_int = t_bool.int()  # True -> 1, False -> 0
    print(f"bool -> int: {t_int}")  # [1, 0, 1]

float16 vs bfloat16 的选择

  • float16(FP16):显存节省一半,适合 RTX/消费级 GPU
  • bfloat16(BF16):与 FP32 相同的指数范围,数值稳定性更好,适合大模型训练

10.3 精度损失注意事项

类型转换可能丢失精度

import torch
    
    # float32 -> int 会截断小数部分
    t = torch.tensor([1.7, 2.5, 3.9])
    print(f"float -> int: {t.int()}")  # [1, 2, 3]
    
    # 大数值 float32 -> float16 可能溢出
    t_large = torch.tensor([65500.0, 66000.0])
    t_fp16 = t_large.half()
    print(f"大数值转 fp16: {t_fp16}")  # 可能出现 inf 或精度损失
    
    # 检查数值是否在范围内
    print(f"fp16 最大值: {torch.finfo(torch.float16).max}")  # 65504

第十一章 与 NumPy 互转

11.1 张量转 NumPy 数组

import torch
    import numpy as np
    
    # 张量 -> NumPy
    t = torch.randn(3, 4)
    arr = t.numpy()  # 返回共享内存的视图
    print(f"张量 dtype: {t.dtype}")
    print(f"NumPy dtype: {arr.dtype}")
    
    # 注意:返回的是视图,共享内存
    t[0, 0] = 999
    print(f"修改张量后 NumPy: {arr[0, 0]}")  # 也是 999!
    
    # CPU 张量直接转换
    t_cpu = torch.tensor([1.0, 2.0, 3.0])
    arr_cpu = t_cpu.numpy()
    print(f"NumPy 数组: {arr_cpu}")

11.2 NumPy 数组转张量

import torch
    import numpy as np
    
    # 方法1:torch.from_numpy() - 共享内存
    arr = np.array([1, 2, 3, 4])
    t = torch.from_numpy(arr)
    print(f"from_numpy: {t}")
    
    # 修改 NumPy 后张量也变(共享内存)
    arr[0] = 999
    print(f"修改 NumPy 后张量: {t[0]}")  # 也是 999
    
    # 方法2:torch.tensor() - 复制数据
    arr = np.array([1, 2, 3, 4])
    t = torch.tensor(arr)  # 复制一份
    arr[0] = 888
    print(f"修改 NumPy 后张量: {t[0]}")  # 还是 1,不变!
    
    # 方法3:torch.as_tensor() - 尽量共享内存(不保证)
    arr = np.array([1, 2, 3])
    t = torch.as_tensor(arr)
    arr[0] = 777
    print(f"as_tensor: {t[0]}")  # 可能变成 777

11.3 GPU 张量的 NumPy 转换

import torch
    import numpy as np
    
    # GPU 张量不能直接转 NumPy,需要先移回 CPU
    if torch.cuda.is_available():
        t_gpu = torch.randn(3, 4).cuda()
        # t_gpu.numpy()  # RuntimeError!
        
        # 正确做法:先转 CPU
        t_cpu = t_gpu.cpu()
        arr = t_cpu.numpy()
        print(f"GPU -> CPU -> NumPy: {arr.shape}")
    
    # 封装成工具函数
    def tensor_to_numpy(t):
        """安全地将张量转为 NumPy 数组"""
        if t.device.type == 'cuda':
            t = t.cpu()
        return t.numpy()
    
    if torch.cuda.is_available():
        t_gpu = torch.randn(3, 4).cuda()
        arr = tensor_to_numpy(t_gpu)
        print(f"安全转换: {arr.dtype}")

11.4 互转的最佳实践

import torch
    import numpy as np
    
    # 最佳实践1:数据预处理用 NumPy,模型训练用 PyTorch
    def preprocess_data(np_array):
        """NumPy 数据预处理"""
        mean = np_array.mean()
        std = np_array.std()
        normalized = (np_array - mean) / std
        return torch.from_numpy(normalized).float()
    
    def postprocess_output(tensor):
        """后处理转回 NumPy"""
        return tensor.detach().cpu().numpy()
    
    # 最佳实践2:使用 clone() 避免共享内存问题
    arr = np.array([1, 2, 3])
    t = torch.from_numpy(arr).clone()  # 复制一份
    arr[0] = 999
    print(f"张量不受影响: {t[0]}")  # 仍然是 1

第十二章 从文件加载张量

12.1 从 CSV 文件加载

import torch
    import numpy as np
    import pandas as pd
    
    # 方法1:使用 pandas 读取 CSV,然后用 torch.tensor()
    df = pd.read_csv('data.csv')
    t = torch.tensor(df.values, dtype=torch.float32)
    print(f"从 CSV 加载: {t.shape}")
    
    # 方法2:使用 NumPy 读取
    data = np.loadtxt('data.csv', delimiter=',')
    t = torch.from_numpy(data).float()
    print(f"NumPy 加载 CSV: {t.shape}")
    
    # 方法3:逐行读取(处理大文件)
    def load_csv_as_tensor(filepath):
        data = []
        with open(filepath, 'r') as f:
            next(f)  # 跳过表头
            for line in f:
                row = [float(x) for x in line.strip().split(',')]
                data.append(row)
        return torch.tensor(data)

12.2 从 NumPy 文件加载(.npy / .npz)

import torch
    import numpy as np
    
    # 保存为 .npy 文件(单个数组)
    arr = np.random.randn(100, 64)
    np.save('array.npy', arr)
    
    # 加载 .npy 文件
    loaded = np.load('array.npy')
    t = torch.from_numpy(loaded)
    print(f"加载 .npy: {t.shape}")
    
    # 保存为 .npz 文件(多个数组)
    arr1 = np.random.randn(100, 64)
    arr2 = np.random.randn(100, 10)
    np.savez('arrays.npz', features=arr1, labels=arr2)
    
    # 加载 .npz 文件
    data = np.load('arrays.npz')
    t_features = torch.from_numpy(data['features'])
    t_labels = torch.from_numpy(data['labels'])
    print(f"加载 .npz features: {t_features.shape}")

12.3 从 PyTorch 文件加载(.pt / .pth)

import torch
    
    # 保存张量
    t = torch.randn(100, 64)
    torch.save(t, 'tensor.pt')
    
    # 加载张量
    loaded = torch.load('tensor.pt')
    print(f"加载 .pt: {loaded.shape}")
    
    # 保存多个张量(字典形式)
    state = {
        'features': torch.randn(100, 64),
        'labels': torch.randn(100, 10),
        'epoch': 10
    }
    torch.save(state, 'checkpoint.pth')
    
    # 加载
    checkpoint = torch.load('checkpoint.pth')
    print(f"加载 checkpoint: epoch={checkpoint['epoch']}")
    
    # 模型参数保存和加载
    model = torch.nn.Linear(64, 10)
    torch.save(model.state_dict(), 'model_weights.pth')
    
    loaded_model = torch.nn.Linear(64, 10)
    loaded_model.load_state_dict(torch.load('model_weights.pth'))

12.4 从图像文件加载

import torch
    from PIL import Image
    import numpy as np
    
    # 使用 PIL 读取图像,转为张量
    def load_image_as_tensor(image_path):
        img = Image.open(image_path).convert('RGB')
        arr = np.array(img)  # (H, W, C)
        tensor = torch.from_numpy(arr).permute(2, 0, 1).float() / 255.0
        return tensor
    
    # 使用 torchvision(推荐)
    from torchvision import transforms
    
    transform = transforms.Compose([
        transforms.Resize((224, 224)),
        transforms.ToTensor(),
        transforms.Normalize(mean=[0.485, 0.456, 0.406],
                            std=[0.229, 0.224, 0.225])
    ])
    
    img = Image.open('image.jpg').convert('RGB')
    tensor = transform(img)
    print(f"图像张量: {tensor.shape}")  # (3, 224, 224)

12.5 从音频文件加载

import torch
    import torchaudio
    
    # 加载音频文件
    waveform, sample_rate = torchaudio.load('audio.wav')
    print(f"音频波形: {waveform.shape}")  # (channels, samples)
    print(f"采样率: {sample_rate}")
    
    # 重采样
    target_sr = 16000
    if sample_rate != target_sr:
        waveform = torchaudio.functional.resample(waveform, sample_rate, target_sr)
        print(f"重采样后: {waveform.shape}")
    
    # 转换为 Mel 频谱图
    mel_spec = torchaudio.transforms.MelSpectrogram(sample_rate=target_sr)
    mels = mel_spec(waveform)
    print(f"Mel 频谱: {mels.shape}")

12.6 从 JSON 文件加载

import torch
    import json
    
    # 保存张量为 JSON(需要转 NumPy)
    data = {
        'tensor_a': torch.randn(10, 5).numpy().tolist(),
        'tensor_b': torch.randint(0, 10, (5,)).numpy().tolist()
    }
    with open('data.json', 'w') as f:
        json.dump(data, f)
    
    # 加载 JSON
    with open('data.json', 'r') as f:
        loaded = json.load(f)
    t_a = torch.tensor(loaded['tensor_a'])
    t_b = torch.tensor(loaded['tensor_b'], dtype=torch.long)
    print(f"从 JSON 加载: {t_a.shape}, {t_b.shape}")

12.7 从二进制文件加载

import torch
    import numpy as np
    
    # 读取原始二进制数据
    def load_binary_tensor(filepath, dtype=np.float32, shape=(1, -1)):
        with open(filepath, 'rb') as f:
            data = f.read()
        arr = np.frombuffer(data, dtype=dtype)
        return torch.from_numpy(arr).reshape(shape)
    
    # 保存张量为二进制
    def save_binary_tensor(tensor, filepath):
        arr = tensor.numpy()
        arr.tofile(filepath)
    
    # 读取自定义格式
    def read_custom_binary(filepath):
        import struct
        with open(filepath, 'rb') as f:
            header = struct.unpack('iii', f.read(12))
            height, width, channels = header
            data = np.frombuffer(f.read(), dtype=np.uint8)
            arr = data.reshape(height, width, channels)
        return torch.from_numpy(arr)

12.8 从 Hugging Face 加载

import torch
    from transformers import AutoTokenizer, AutoModel
    
    # 加载预训练模型和分词器
    model_name = 'bert-base-chinese'
    tokenizer = AutoTokenizer.from_pretrained(model_name)
    model = AutoModel.from_pretrained(model_name)
    
    # 文本编码
    text = "今天天气真好"
    inputs = tokenizer(text, return_tensors='pt')
    print(f"输入张量: {inputs['input_ids'].shape}")
    
    # 获取模型输出
    with torch.no_grad():
        outputs = model(**inputs)
        embeddings = outputs.last_hidden_state
        print(f"嵌入向量: {embeddings.shape}")

第十到第十二章总结

  • 类型转换:.float()/.int()/.half()/.bfloat16() 等方法
  • NumPy 互转:.numpy() 转数组,torch.from_numpy() 转张量
  • 文件加载:CSV 用 pandas,NPZ 用 np.load,PT 用 torch.load,图像用 torchvision
  • HuggingFace:使用 transformers 库加载预训练模型

FAQ 常见问题(20 组)

关于张量基础概念

Q1. 张量和矩阵有什么区别?

一句话结论:张量是矩阵的推广,矩阵是 2 维张量。矩阵是行×列的二维数组,张量可以是任意维度:一维向量、二维矩阵、三维及以上的数组都叫张量。

Q2. PyTorch Tensor 和 NumPy 数组哪个更快?

一句话结论:在 CPU 上差不多,GPU 上 PyTorch Tensor 更快。PyTorch Tensor 支持 GPU 加速,NumPy 只能在 CPU 上运行。但对于纯 CPU 计算,两者性能差距不大。

Q3. 张量的 shape 和 shape() 有什么区别?

一句话结论:shape 是属性,shape() 是方法,效果相同。推荐使用 .shape,因为它既可以作为属性访问,也可以用于解包。

t = torch.randn(3, 4, 5)
    print(t.shape)       # torch.Size([3, 4, 5])
    print(t.shape[0])    # 3
    rows, cols = t.shape # 解包

Q4. 什么是叶子节点(leaf tensor)?

一句话结论:直接创建的、requires_grad=True 的张量是叶子节点。叶子节点的梯度会保留在 .grad 属性中,而非叶子节点的梯度在反向传播后会被释放。

关于张量创建

Q5. torch.tensor() 和 torch.Tensor() 有什么区别?

一句话结论:torch.tensor() 复制数据推断类型,torch.Tensor() 创建 float32 张量。推荐使用 torch.tensor(),它更明确且功能更丰富。

Q6. 如何创建指定形状的张量但不初始化值?

一句话结论:使用 torch.empty() 或 torch.empty_like()。但更推荐 torch.zeros()/torch.randn()。empty() 不保证初始值,可能包含随机垃圾数据。

# 不推荐:值未初始化
    t = torch.empty(3, 4)
    
    # 推荐:明确初始值
    t = torch.zeros(3, 4)  # 全 0
    t = torch.randn(3, 4)  # 随机

Q7. 如何创建一个与现有张量相同形状的张量?

一句话结论:使用 torch.zeros_like() 或 torch.ones_like()。

x = torch.randn(3, 4, dtype=torch.float64)
    zeros = torch.zeros_like(x)  # 形状和 dtype 都相同
    ones = torch.ones_like(x)

Q8. 如何设置随机种子保证结果可复现?

一句话结论:在训练开始前设置 torch.manual_seed()。

torch.manual_seed(42)
    t = torch.randn(3, 3)  # 每次运行相同

关于索引和切片

Q9. 张量切片会复制数据吗?

一句话结论:切片返回视图(共享内存),不是复制。修改切片会影响原张量。如需复制,使用 clone()。

t = torch.arange(12).reshape(3, 4)
    slice_t = t[0]  # 视图
    slice_t[0] = 999
    print(t[0, 0])  # 999,被改了!
    
    t2 = t[0].clone()  # 复制
    t2[0] = 888
    print(t[0, 0])  # 还是 999

Q10. 如何用条件筛选张量元素?

一句话结论:使用布尔索引或 torch.where()。

t = torch.arange(10)
    mask = t > 5
    filtered = t[mask]  # 布尔索引
    
    # 或使用 torch.where
    result = torch.where(t > 5, t, t * 0)  # 大于5保留,否则置0

关于广播机制

Q11. 什么情况下不能使用广播?

一句话结论:当维度大小不同且都不为 1 时。

# 报错!
    a = torch.randn(3, 4)
    b = torch.randn(2, 4)
    # a + b  # RuntimeError
    
    # 可以广播
    a = torch.randn(1, 4)
    b = torch.randn(2, 4)
    c = a + b  # OK

Q12. 广播是复制数据还是虚拟扩展?

一句话结论:逻辑上虚拟扩展,不实际复制数据。广播只改变索引方式,实际内存不复制,因此高效。

关于自动求导

Q13. requires_grad=True 和 False 有什么区别?

一句话结论:True 会追踪梯度,用于需要优化的参数。模型的权重通常 requires_grad=True,数据通常 False。

Q14. 为什么反向传播后梯度会消失?

一句话结论:可能是梯度为 0(sigmoid/tanh 饱和)或计算图被切断。检查是否使用了 detach()、no_grad() 或requires_grad=False。

Q15. 梯度累积是什么意思?

一句话结论:多次 backward() 会累加梯度到 .grad,不会覆盖。因此每个训练步骤前要清零梯度。

loss.backward()
    optimizer.step()
    optimizer.zero_grad()  # 清零,准备下一步

关于设备迁移

Q16. GPU 张量可以直接打印吗?

一句话结论:可以,但建议先移到 CPU 更直观。

t_gpu = torch.randn(3, 3).cuda()
    print(t_gpu)  # 可以打印
    
    # 更推荐
    print(t_gpu.cpu())  # 在 CPU 上显示

Q17. 如何判断代码在 GPU 还是 CPU 上运行?

一句话结论:检查张量的 .device 属性。

t = torch.randn(3, 4)
    print(t.device)  # cpu
    
    if torch.cuda.is_available():
        t = t.cuda()
        print(t.device)  # cuda:0

Q18. Apple M 系列芯片用哪个设备?

一句话结论:使用 MPS 后端。

if torch.backends.mps.is_available():
        t = torch.randn(3, 4).to('mps')

关于内存布局

Q19. 什么时候需要调用 contiguous()?

一句话结论:当需要用 view() 但张量不连续时。

t = torch.randn(3, 4)
    t_slice = t[:, ::2]  # 不连续
    
    # 报错
    # t_slice.view(6)  # RuntimeError
    
    # 解决
    t_cont = t_slice.contiguous()
    t_cont.view(6)  # OK

Q20. channels_last 内存格式有什么优势?

一句话结论:某些 GPU 的卷积操作对通道优先的数据更高效。适合图像数据(NCHW vs NHWC),可以提升推理速度。

全篇总结:张量学习的核心要点

  • 四元身份:shape / dtype / device / layout 决定张量全部特性
  • 创建方式:从数据、特殊值、随机数、范围、现有张量多种方式
  • 索引切片:0-based 索引,支持负索引和高级索引
  • 广播机制:从右向左比维度,相等或为1可广播
  • 自动求导:requires_grad 开启,backward() 求导,梯度累积需清零
  • 设备迁移:.to(device) 切换 CPU/GPU/MPS
  • 内存布局:stride 控制访问,contiguous() 确保连续
  • 类型转换:.float()/.int()/.half() 等方法
  • NumPy 互转:.numpy() 和 torch.from_numpy()
  • 文件加载:CSV/NPZ/PT/图像/音频/HuggingFace

posted @ 2026-07-09 17:14  左扬  阅读(2)  评论(0)    收藏  举报