主题
字号
CHAPTER 02 ≈ 60 MIN READ

PyTorch基础函数详解

作者:USTC学生 | 适用人群:零基础 / 深度学习初学者 特别说明:本笔记面向中科大大一大二同学,即使你之前没有接触过PyTorch或深度学习框架,只要具备Python基础,就能完全理解本笔记的所有内容。 更新时间:2026年3月15日

前言:为什么学习PyTorch

0.1 深度学习框架的重要性

在正式学习PyTorch之前,让我们先理解一个根本性的问题:为什么要学习深度学习框架?

想象一下,如果让你从零开始写一个程序来识别图片中的猫,你需要做什么?

首先,你需要理解猫的特征:耳朵的形状、眼睛的大小、毛发的纹理...然后你需要用数学公式来描述这些特征。当你想要优化这个系统时,你需要计算梯度来调整参数。这整个过程极其复杂且耗时。

深度学习框架正是为了解决这个问题而诞生的。它帮我们封装好了:

有了框架,我们就可以像搭积木一样构建自己的神经网络,把精力集中在模型设计数据处理上,而不是底层实现。

0.2 为什么选择PyTorch?

PyTorch是由Facebook(现Meta)的AI研究团队开发的深度学习框架。在2017年推出后,它迅速成为学术研究领域的首选框架

PyTorch的核心特点

  1. 动态计算图(Dynamic Computation Graph)

    • 什么是计算图?简单来说,它是用来记录我们执行了哪些操作的"账本"
    • 动态图的特点是:每次运行代码时,计算图都会重新构建
    • 这让调试变得非常直观——你可以像调试普通Python代码一样调试PyTorch代码
    • 想象一下:你在写一段代码,中途想print一个变量的值看看对不对,这在PyTorch中完全没问题!
  2. Python优先的设计理念

    • PyTorch的API设计非常Pythonic(符合Python的风格)
    • 如果你熟悉Python,学PyTorch会感觉非常自然
    • 不需要学习额外的"框架特定语言"
  3. GPU加速

    • 深度学习涉及大量的矩阵运算
    • GPU(显卡)特别擅长并行处理这类运算
    • 使用GPU可以让训练速度提升10倍甚至100倍
    • 举例:CPU训练一个模型需要几天,GPU可能只需要几小时
  4. 自动微分(Automatic Differentiation)

    • 这是深度学习框架最核心的功能
    • 想象一下:你写了一个很复杂的神经网络(可能几百层)
    • 如果让你手动计算梯度...这简直是不可能的任务
    • 自动微分让我们只需要定义前向传播,框架会自动处理反向传播和梯度计算

0.3 张量:PyTorch的核心数据结构

PyTorch的核心数据结构是张量(Tensor)。听起来很高大上,其实它就是我们熟悉的多维数组的学术说法:

张量类似于NumPy的ndarray,但它额外支持:

0.4 本笔记的学习路线

本笔记是整个深度学习笔记系列的基础篇,目标是:

第一章:掌握PyTorch的核心数据结构——张量(Tensor)的创建与基本操作

第二章:理解张量的属性

第三章:掌握张量的操作与变换

第四章:理解自动求导机制

第五章:掌握神经网络的基础模块

前置知识:本笔记假设你已经有Python基础,会写循环、条件语句、函数等。如果你还不太熟悉Python,建议先学习Python基础课程。另外,了解一点线性代数(知道什么是矩阵、向量)会有帮助,但不是必须的——我们会在需要时解释相关概念。


第一章:张量的创建与基本操作

本章目标:掌握PyTorch中最基本的张量创建方法,理解张量这一核心数据结构

1.1 什么是张量(Tensor)?

在开始学习PyTorch之前,我们首先要理解一个核心概念:张量(Tensor)

1.1.1 从熟悉的概念出发

回想一下你学过的数据结构:

1.1.2 张量的维度

PyTorch中,张量的维度(dimension)也称为轴(axis)。让我们用具体例子来理解:

import torch

# 0维张量:标量
scalar = torch.tensor(5)
print(f"标量: {scalar}, 维度: {scalar.ndim}, 形状: {scalar.shape}")
# 输出:标量: tensor(5), 维度: 0, 形状: torch.Size([])

# 1维张量:向量
vector = torch.tensor([1, 2, 3, 4, 5])
print(f"向量: {vector}, 维度: {vector.ndim}, 形状: {vector.shape}")
# 输出:向量: tensor([1, 2, 3, 4, 5]), 维度: 1, 形状: torch.Size([5])

# 2维张量:矩阵
matrix = torch.tensor([[1, 2, 3],
                       [4, 5, 6]])
print(f"矩阵维度: {matrix.ndim}, 形状: {matrix.shape}")
# 输出:矩阵维度: 2, 形状: torch.Size([2, 3])
# 解释:有2行3列

# 3维张量
tensor_3d = torch.tensor([[[1, 2], [3, 4]],
                          [[5, 6], [7, 8]]])
print(f"3维张量维度: {tensor_3d.ndim}, 形状: {tensor_3d.shape}")
# 输出:3维张量维度: 3, 形状: torch.Size([2, 2, 2])
# 解释:2个2x2的矩阵

# Mav’s tips:快速判断高维张量是几维,只需看开头有几个'['嵌套即可。

1.1.3 为什么要用张量?

你可能会问:Python已经有列表(list)和NumPy数组了,为什么还需要张量?

关键区别

特性 Python列表 NumPy数组 PyTorch张量
执行速度
GPU加速
自动求导
深度学习 不适用 可用 专为深度学习设计

简单来说:PyTorch张量 = NumPy数组 + GPU加速 + 自动求导,它就是为深度学习“定制”的。

这就是为什么深度学习需要专门的张量数据结构!


1.2 torch.tensor:创建张量的核心函数

现在我们正式学习如何创建张量。torch.tensor是PyTorch中创建张量最直接的方式。

import torch

# 创建一维张量(向量)
vec = torch.tensor([1, 2, 3, 4, 5])
print(vec)  # tensor([1, 2, 3, 4, 5])

# 创建二维张量(矩阵)
matrix = torch.tensor([[1, 2, 3],
                       [4, 5, 6]])
print(matrix)
# tensor([[1, 2, 3],
#         [4, 5, 6]])

# 创建三维张量
tensor_3d = torch.tensor([[[1, 2], [3, 4]],
                          [[5, 6], [7, 8]]])
print(tensor_3d.shape)  # torch.Size([2, 2, 2])

关键参数说明

参数 作用 示例
data 张量的数据,可以是列表 [1, 2, 3]
dtype 数据类型 torch.float32, torch.int64
device 存储设备 'cpu', 'cuda'
requires_grad 是否追踪梯度 True, False

**Mav’s tips:**函数传入参数的顺序无所谓,因为实际写代码时,除了data是直接传数据的,其他参数通常都会明确指定名字(比如requires_grad=True)

# 指定数据类型
float_tensor = torch.tensor([1, 2, 3], dtype=torch.float32)
print(float_tensor.dtype)  # torch.float32

# 开启梯度追踪(后续神经网络训练需要)
x = torch.tensor([1.0, 2.0, 3.0], requires_grad=True)
print(x.requires_grad)  # True

# 创建在GPU上的张量(需要GPU环境)
# device_tensor = torch.tensor([1, 2, 3], device='cuda')

注意事项

小贴士:创建张量的另一种方式 如果你已经有了一个NumPy数组,可以直接把它转换成PyTorch张量:

import numpy as np

# NumPy数组
np_array = np.array([1, 2, 3, 4, 5])

# 转换成PyTorch张量
tensor_from_numpy = torch.from_numpy(np_array)
print(tensor_from_numpy)  # tensor([1, 2, 3, 4, 5], dtype=torch.int64)

重要:这种转换是共享内存的!修改NumPy数组会影响PyTorch张量,反之亦然。如果你想创建独立副本,使用.clone()方法。


1.3 torch.randn、torch.zeros、torch.ones:创建特殊张量

除了使用torch.tensor从列表创建张量外,PyTorch还提供了多个便捷函数来创建具有特定值的张量。

torch.randn:随机正态分布

torch.randn用于创建服从标准正态分布(均值0,方差1)的随机张量。"randn"中的"n"代表"normal",即高斯分布。

# 创建形状为(3, 4)的随机正态分布张量
random_tensor = torch.randn(3, 4)
print(random_tensor)
# tensor([[ 0.3412, -0.1234,  0.5678, -0.9012],
#         [ 1.2345, -0.5678,  0.9012, -0.3456],
#         [-0.7890,  0.1234, -0.4567,  0.7890]])

# 多维随机张量
random_3d = torch.randn(2, 3, 4)  # 2个3x4的矩阵
print(random_3d.shape)  # torch.Size([2, 3, 4])

应用场景:初始化神经网络权重时经常用到,好的权重初始化对模型训练至关重要。

Mav’s tips:torch.randn只能用来生成标准正态分布,torch.normal可以自定义均值和标准差,其余格式一致。

torch.zeros 和 torch.ones

torch.zeros创建全零张量,torch.ones创建全一张量

# 创建全零张量
zeros = torch.zeros(2, 3)
print(zeros)
# tensor([[0., 0., 0.],
#         [0., 0., 0.]])

# 创建全一张量
ones = torch.ones(3, 2)
print(ones)
# tensor([[1., 1.],
#         [1., 1.],
#         [1., 1.]])

# 创建与已有张量形状相同的全零张量
existing = torch.randn(4, 5)
zeros_like = torch.zeros_like(existing)
print(zeros_like.shape)  # torch.Size([4, 5])

应用场景:创建占位符、初始化偏置项、创建掩码等。

torch.full 和 torch.eye

# 创建全为7的张量
full = torch.full((2, 3), 7)
print(full)
# tensor([[7., 7., 7.],
#         [7., 7., 7.]])

# 创建3x3单位矩阵
identity = torch.eye(3)
print(identity)
# tensor([[1., 0., 0.],
#         [0., 1., 0.],
#         [0., 0., 1.]])

1.4 torch.arange和torch.linspace:创建序列张量

在深度学习中,经常需要创建等差数列等比数列的张量,例如学习率衰减、图像坐标网格等场景。

torch.arange:不含结束值的等差数列

# 创建0到9的张量(不含10)
arange_1d = torch.arange(10)
print(arange_1d)  # tensor([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

# 指定起始和结束值
arange_custom = torch.arange(5, 15)
print(arange_custom)  # tensor([5, 6, 7, 8, 9, 10, 11, 12, 13, 14])

# 指定步长
arange_step = torch.arange(0, 10, 2)
print(arange_step)  # tensor([0, 2, 4, 6, 8])

注意:与Python的range不同,arange返回的是张量而非列表。

torch.linspace:含结束值的等差数列

# 创建包含5个元素的等差数列,从0到1
linspace_1d = torch.linspace(0, 1, 5)
print(linspace_1d)  # tensor([0.0000, 0.2500, 0.5000, 0.7500, 1.0000])

# 创建用于学习率调度的序列
learning_rates = torch.linspace(0.1, 0.001, 100)
print(learning_rates[-1])  # tensor(0.0010)

应用场景:linspace保证包含起始和结束值,适合需要精确控制点数量的场景。


第二章:理解张量的属性

本章目标:掌握查看和理解张量属性的方法

2.1 shape:查看张量的形状

shape是张量最重要的属性之一,表示张量在各个维度上的大小。

# 创建不同形状的张量
vec = torch.tensor([1, 2, 3])
matrix = torch.randn(3, 4)
tensor_3d = torch.randn(2, 3, 4)
tensor_4d = torch.randn(5, 2, 3, 4)

print(vec.shape)       # torch.Size([3])
print(matrix.shape)    # torch.Size([3, 4])
print(tensor_3d.shape) # torch.Size([2, 3, 4])
print(tensor_4d.shape) # torch.Size([5, 2, 3, 4])

深度学习中张量形状的约定

数据类型 形状 含义
图像数据 (N, C, H, W) 批量大小N,通道C,高H,宽W
文本数据 (N, L) 批量N,序列长度L
文本embedding (N, L, D) 批量N,长度L,维度D
批量矩阵 (N, M) 批量N,特征M

示例

# 32张RGB图像,每张224x224
images = torch.randn(32, 3, 224, 224)
print(images.shape)  # torch.Size([32, 3, 224, 224])
# N=32(32张图), C=3(RGB三通道), H=224, W=224

调试技巧:当形状不匹配时,PyTorch会抛出错误。理解每个张量在各维度的含义是调试神经网络的基础。


2.2 dtype:查看张量的数据类型

dtype表示张量中元素的数据类型。不同的数据类型占用不同的内存,也影响计算的精度和速度。

# 查看张量的数据类型
int_tensor = torch.tensor([1, 2, 3])
float_tensor = torch.tensor([1.0, 2.0, 3.0])
bool_tensor = torch.tensor([True, False, True])

print(int_tensor.dtype)   # torch.int64
print(float_tensor.dtype) # torch.float32
print(bool_tensor.dtype)  # torch.bool

常用数据类型速查表

dtype 名称 用途
torch.float32 / torch.float 32位浮点 默认类型,训练常用
torch.float64 / torch.double 64位浮点 高精度计算
torch.float16 / torch.half 16位浮点 GPU加速、混合精度训练
torch.int32 32位整数 通用整数
torch.int64 / torch.long 64位整数 索引、大整数
torch.int8 8位整数 量化
torch.uint8 无符号8位整数 图像像素(0-255)
torch.bool 布尔 条件判断

实践建议:训练时通常使用float32,推理时可用float16加速,处理图像时用uint8。


2.3 device:查看张量存储的设备

device表示张量存储的设备,可以是CPUGPU

# 查看张量所在的设备
cpu_tensor = torch.randn(3, 4)
print(cpu_tensor.device)  # cpu

# 尝试移动到GPU(如果有GPU的话)
if torch.cuda.is_available():
    gpu_tensor = cpu_tensor.cuda()  # 或 cpu_tensor.to('cuda')
    print(gpu_tensor.device)  # cuda:0

设备处理最佳实践

# 推荐的设备处理方式
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

x = torch.randn(3, 4, device=device)
model = MyModel().to(device)

# 数据也需要移到相同设备
data = data.to(device)

第三章:张量的操作与变换

本章目标:掌握张量的核心操作,为后续神经网络打下基础

3.1 torch.matmul:矩阵乘法的核心函数

torch.matmul是PyTorch中进行矩阵乘法的核心函数。这是深度学习中最基本的运算,无论是神经网络的线性层、全连接层,还是注意力机制,都离不开矩阵乘法。

维度规则:如果矩阵A的形状是(m, n),矩阵B的形状是(n, k),则结果矩阵的形状是(m, k)。

# 基本的矩阵乘法
A = torch.randn(2, 3)  # 2行3列
B = torch.randn(3, 4)  # 3行4列
C = torch.matmul(A, B)  # 结果是2行4列
print(C.shape)  # torch.Size([2, 4])

# 向量与矩阵相乘
vector = torch.randn(3)      # 形状 (3,)
matrix = torch.randn(3, 4)  # 形状 (3, 4)
result = torch.matmul(vector, matrix)  # 结果形状 (4,)
print(result.shape)  # torch.Size([4])

# 批量矩阵乘法(用于CNN、RNN等)
A_batch = torch.randn(10, 3, 4)  # 10个3x4的矩阵
B_batch = torch.randn(10, 4, 5)  # 10个4x5的矩阵
C_batch = torch.matmul(A_batch, B_batch)  # 10个3x5的矩阵
print(C_batch.shape)  # torch.Size([10, 3, 5])

相关函数对比

函数 作用 示例
torch.matmul 通用矩阵乘法(推荐) 支持批量、广播
torch.mm 二维矩阵乘法 只支持2D
torch.mul 元素-wise乘法(逐元素) [1,2] * [3,4] = [3,8]
torch.dot 向量点积 1*4 + 2*5 + 3*6 = 32
a = torch.tensor([1, 2, 3])
b = torch.tensor([4, 5, 6])

# 点积:对应元素相乘后求和
print(torch.dot(a, b))      # tensor(32)

# 元素-wise乘法:逐元素相乘
print(torch.mul(a, b))       # tensor([4, 10, 18])

# 矩阵乘法(二维)
A = torch.tensor([[1, 2], [3, 4]])
B = torch.tensor([[5, 6], [7, 8]])
print(torch.mm(A, B))
# tensor([[19, 22],
#         [43, 50]])

3.2 @ 运算符:矩阵乘法的便捷写法

在Python中,@运算符是矩阵乘法的简写形式(PEP 465)。它就是torch.matmul的运算符重载版本。

A = torch.randn(3, 4)
B = torch.randn(4, 5)

# 两种写法完全等价
C1 = torch.matmul(A, B)
C2 = A @ B

print(torch.allclose(C1, C2))  # True

实际应用

# 示例:手动实现一个简单的线性层
class LinearLayer:
    def __init__(self, input_dim, output_dim):
        self.weights = torch.randn(input_dim, output_dim) * 0.01
        self.bias = torch.zeros(output_dim)

    def forward(self, x):
        return x @ self.weights + self.bias

# 使用
linear = LinearLayer(784, 256)
x = torch.randn(32, 784)  # batch_size=32, input_dim=784
output = linear.forward(x)
print(output.shape)  # torch.Size([32, 256])

3.3 reshape和view:改变张量形状

reshape和view都用于改变张量的形状,但不改变底层的数据。

# 创建原始张量
original = torch.randn(2, 3, 4)
print(original.shape)  # torch.Size([2, 3, 4])

# 使用reshape改变形状
reshaped = original.reshape(2, 12)
print(reshaped.shape)  # torch.Size([2, 12])

# 用-1让PyTorch自动推断维度
auto_shape = original.reshape(2, -1)
print(auto_shape.shape)  # torch.Size([2, 12])

# 展平为一维
flat = original.reshape(-1)
print(flat.shape)  # torch.Size([24])

# view的用法类似(但需要内存连续)
viewed = original.view(2, 12)
print(viewed.shape)  # torch.Size([2, 12])

重要区别

实际应用 - 图像展平

# 32张RGB图像,每张224x224
images = torch.randn(32, 3, 224, 224)

# 展平为(batch_size, 784)以便输入全连接层
images_flattened = images.reshape(32, -1)
print(images_flattened.shape)  # torch.Size([32, 150528])

# 重新reshape回原来的形状
images_restored = images_flattened.reshape(32, 3, 224, 224)
print(images_restored.shape)  # torch.Size([32, 3, 224, 224])

3.4 item和numpy:提取张量中的数值

item():张量转Python标量

# 单元素张量转Python标量
scalar_tensor = torch.tensor(42)
print(scalar_tensor)        # tensor(42)
print(scalar_tensor.item()) # 42 (Python int)

# 在训练循环中获取损失值
loss = torch.tensor(0.5)
loss_value = loss.item()
print(f"Loss: {loss_value}")  # Loss: 0.5

# 注意:如果张量有多个元素,item()会报错
multi_tensor = torch.tensor([1, 2, 3])
# multi_tensor.item()  # RuntimeError: a Tensor with 1 element

numpy():张量转NumPy数组

# 张量转NumPy数组
tensor = torch.randn(3, 4)
numpy_array = tensor.numpy()

print(type(tensor))      # <class 'torch.Tensor'>
print(type(numpy_array)) # <class 'numpy.ndarray'>)

# ⚠️ 重要:转换是共享内存的!
numpy_array[0, 0] = 999
print(tensor[0, 0])  # tensor(999.) - 同步修改

# NumPy数组转PyTorch张量
import numpy as np
np_array = np.array([1, 2, 3, 4, 5])
torch_tensor = torch.from_numpy(np_array)
print(torch_tensor)  # tensor([1, 2, 3, 4, 5], dtype=torch.int64)

创建不共享内存的副本

# 分离梯度追踪并转为NumPy
tensor = torch.randn(3, 4)
numpy_copy = tensor.detach().numpy()  # detach()切断梯度追踪

第四章:自动求导机制

本章目标:理解PyTorch的核心优势——自动微分

这是PyTorch最强大的特性!有了它,你无需手动计算复杂梯度,只需要定义前向传播,PyTorch会自动处理反向传播。

4.0 梯度是什么?为什么重要?

在深入学习PyTorch的自动求导机制之前,我们先来理解一个核心概念:梯度(Gradient)

4.0.1 直观理解梯度

想象一下你在一座山上,想找到山顶(或者最低点——山谷)。梯度就像是告诉你应该往哪个方向走才能最快到达目的地。

数学定义:梯度是函数在某一点上变化最快的方向

4.0.2 梯度在深度学习中的作用

在深度学习中,我们的目标是最小化损失函数(Loss Function)。

训练过程

  1. 前向传播(Forward Pass):输入数据通过网络,计算输出
  2. 计算损失(Compute Loss):比较网络输出和真实标签,得到一个"错误程度"
  3. 反向传播(Backward Pass):计算损失对每个参数的梯度
  4. 更新参数:根据梯度调整参数,让损失变小
  5. 重复以上步骤...

这就是梯度下降(Gradient Descent)的核心思想

$w_{new} = w_{old} - \alpha \cdot \nabla L$

其中 $\alpha$ 是学习率(learning rate),控制每一步走多远。

4.0.3 手动计算梯度的痛苦

假设你有一个两层神经网络:

输入 -> 第一层(权重W1) -> 激活函数 -> 第二层(权重W2) -> 输出

要手动计算损失对W1的梯度,你需要:

  1. 写出完整的数学公式
  2. 应用链式法则(Chain Rule)
  3. 一步步求导...

对于一个现代神经网络,可能有几亿个参数!手动计算完全不现实。

这就是为什么需要自动求导!


4.1 requires_grad:开启梯度追踪

requires_grad决定PyTorch是否自动跟踪对该张量的操作。

# 默认创建的张量不追踪梯度
x = torch.tensor([1.0, 2.0, 3.0])
print(x.requires_grad)  # False

# 创建时开启梯度追踪
x = torch.tensor([1.0, 2.0, 3.0], requires_grad=True)
print(x.requires_grad)  # True

# 后续修改
x = torch.tensor([1.0, 2.0, 3.0])
x.requires_grad = True
print(x.requires_grad)  # True

实际使用:通常不需要手动设置,而是通过神经网络的参数自动获得。

# 神经网络层会自动追踪梯度
linear = nn.Linear(3, 1)
print(linear.weight.requires_grad)  # True
print(linear.bias.requires_grad)    # True

4.2 backward:自动计算梯度

backward()根据计算图自动计算所有requires_grad=True的张量的梯度。

**Mav's Tips:**一个注意点,backward()只能作用于标量,常见的办法是像下文中对张量y进行sum()操作,得到标量z,最后对z进行反向传播。

# 简单的梯度计算示例
x = torch.tensor([1.0, 2.0, 3.0], requires_grad=True)
y = x ** 2       # y = x^2
z = y.sum()      # z = 1^2 + 2^2 + 3^2 = 14

# 反向传播
z.backward()

# 查看梯度:dz/dx = 2x
print(x.grad)  # tensor([2., 4., 6.])
# d(z)/d(x[0]) = 2 * 1 = 2
# d(z)/d(x[1]) = 2 * 2 = 4
# d(z)/d(x[2]) = 2 * 3 = 6

模拟神经网络梯度计算

# 模拟一个简单的神经网络前向传播
x = torch.randn(1, 3, requires_grad=True)  # 输入
w = torch.randn(3, 1, requires_grad=True)  # 权重
b = torch.randn(1, requires_grad=True)      # 偏置

# 前向传播
y = x @ w + b  # 矩阵乘法 + 偏置
loss = y.sum()  # 损失

# 反向传播
loss.backward()

# 查看梯度
print("x的梯度:", x.grad)
print("w的梯度:", w.grad)
print("b的梯度:", b.grad)

4.3 grad:查看计算出的梯度

grad属性存储通过backward()计算出的梯度值。

# 创建需要梯度的张量
x = torch.tensor([2.0, 3.0], requires_grad=True)

# 进行一些计算
y = x[0] ** 2 + 2 * x[1]
y.backward()

# 查看梯度
print(x.grad)  # tensor([4., 2.])
# dy/dx[0] = 2*x[0] = 4
# dy/dx[1] = 2 = 2

⚠️ 重要:梯度累积特性

在PyTorch中,梯度会累积而不是被替换。每次调用backward()时,新计算的梯度会加到现有的grad值上。

# 梯度累积示例
x = torch.tensor([1.0], requires_grad=True)

for i in range(3):
    y = x ** 2
    y.backward()
    print(f"第{i+1}次反向传播后, grad = {x.grad}")

# 输出:
# 第1次反向传播后, grad = tensor([2.])
# 第2次反向传播后, grad = tensor([4.])  # 2 + 2
# 第3次反向传播后, grad = tensor([6.])  # 4 + 2

4.4 zero_grad:清零梯度

由于PyTorch默认累积梯度,训练循环中每次参数更新前必须清零梯度

# 标准的训练循环
optimizer = torch.optim.SGD(net.parameters(), lr=0.01)

for data, target in dataloader:
    # 1. 前向传播
    output = net(data)
    loss = criterion(output, target)

    # 2. 清零梯度(重要!!!)
    optimizer.zero_grad()

    # 3. 反向传播
    loss.backward()

    # 4. 更新参数
    optimizer.step()

三种清零梯度的方式

# 方法1:使用优化器(推荐)
optimizer.zero_grad()

# 方法2:直接对参数操作
for param in net.parameters():
    param.grad.zero_()

# 方法3:将grad设为None(更高效)
for param in net.parameters():
    param.grad = None

4.5 no_grad:临时关闭梯度计算

torch.no_grad()是上下文管理器,用于临时关闭梯度计算。这在评估阶段特别重要,可以节省内存和计算资源。

net = nn.Linear(3, 1)
x = torch.randn(2, 3)

# 训练模式:需要计算梯度
output = net(x)
print(output.requires_grad)  # True

# 推理模式:不需要计算梯度
with torch.no_grad():
    output = net(x)
    print(output.requires_grad)  # False

典型应用场景

# 评估模式
net.eval()  # 设置为评估模式
with torch.no_grad():
    for data, target in test_loader:
        output = net(data)
        # ... 计算准确率等指标

补充torch.inference_mode()no_grad()的更严格版本,在PyTorch 1.9+中推荐使用,性能更好。


第五章:神经网络基础模块

本章目标:掌握PyTorch中构建神经网络的核心组件

5.0 神经网络是什么?

在开始学习PyTorch的神经网络模块之前,我们先来理解一个根本性的问题:神经网络到底是什么?

5.0.1 神经网络的直观理解

想象一下你有一堆乐高积木。每个积木块只能做很简单的事情(比如把输入乘以某个数然后加起来)。但是当你把这些积木块堆叠在一起时,它们就能完成非常复杂的事情!

神经网络就是这样一个"积木系统":

5.0.2 神经元的工作原理

一个最简单的神经元(也称为感知机线性单元)是这样工作的:

输入 x -> 乘以权重 w -> 加上偏置 b -> 输出 y

数学公式: $y = w \cdot x + b$

或者用向量形式: $y = \mathbf{w}^T \mathbf{x} + b$

其中:

5.0.3 为什么要多层?

单层神经元只能做线性变换——也就是说,它只能学会画一条直线(或者一个平面)。

但现实世界的问题往往不是线性的!比如:

解决方法:堆叠多层!每加一层,网络就能学习更复杂的模式。

这就是为什么我们需要深度学习——"深度"指的就是层数多!

5.0.4 PyTorch中的神经网络

PyTorch为神经网络提供了完整的工具箱:

组件 作用
nn.Linear 线性变换(全连接层)
nn.Conv2d 卷积层(处理图像)
nn.RNN / nn.LSTM 循环层(处理序列)
nn.ReLU / nn.Sigmoid 激活函数
nn.CrossEntropyLoss 损失函数
torch.optim 优化器

接下来,让我们逐一学习这些组件!


5.1 nn.Linear:线性变换层

nn.Linear是PyTorch中最基本的神经网络层,也称为全连接层(Fully Connected Layer)密集层(Dense Layer)

数学公式: $\mathbf{y} = \mathbf{x} \mathbf{W}^T + \mathbf{b}$

或者更详细地: $y_i = \sum_{j=1}^{n} x_j \cdot W_{ij} + b_i$

其中:

import torch.nn as nn

# 创建一个线性层
# 输入维度3,输出维度2
linear = nn.Linear(3, 2)

# 查看线性层的参数
print("权重形状:", linear.weight.shape)  # torch.Size([2, 3])
print("偏置形状:", linear.bias.shape)    # torch.Size([2])

# 前向传播
x = torch.randn(4, 3)  # 批量大小4,输入维度3
output = linear(x)      # 输出维度2
print("输出形状:", output.shape)  # torch.Size([4, 2])

手动验证

# 验证nn.Linear的计算
linear = nn.Linear(3, 2)
x = torch.randn(4, 3)

# PyTorch的实现
output_pytorch = linear(x)

# 手动计算:y = xW^T + b
xWt = x @ linear.weight.t()
output_manual = xWt + linear.bias

print(torch.allclose(output_pytorch, output_manual))  # True

实际应用

# 示例:简单的多层神经网络
class SimpleNet(nn.Module):
    def __init__(self):
        super().__init__()
        self.layer1 = nn.Linear(784, 256)
        self.layer2 = nn.Linear(256, 10)

    def forward(self, x):
        x = x.reshape(x.size(0), -1)  # 展平
        x = torch.relu(self.layer1(x))  # 激活函数
        x = self.layer2(x)
        return x

net = SimpleNet()
print(net)
# SimpleNet(
#   (layer1): Linear(in_features=784, out_features=256, bias=True)
#   (layer2): Linear(in_features=256, out_features=10, bias=True)
# )

Mav's Tips:

Python 类与继承基础(结合 PyTorch)

1. 第三方库

Python 的第三方库类似 C 的 #include,是别人封装好的功能,直接调用即可。区别在于 Python 的第三方库需要先用 pip install 安装,自带的标准库(osmath 等)不需要。import as 是简写用途,比如 import torch.nn as nn,之后用 nn 代替 torch.nntorchtorch.nn 是包含关系,torch.nntorch 里专门负责神经网络的子模块,可以单独引入。

2. 类的继承

class Dog(Animal):
    pass

括号内写父类名,表示继承——子类自动拥有父类的所有方法和结构,不需要重写。如果子类只写 pass,直接可以调用父类的方法。

3.super() 的作用

如果子类自己定义了 __init__,会覆盖父类的 __init__,父类的初始化逻辑就被跳过了。super().__init__() 的作用是手动把父类的初始化补回来。

所以"继承 + 自己还要新增内容"的标准写法是:

class SimpleNet(nn.Module):
    def __init__(self):
        super().__init__()   # 先跑父类的初始化
        self.layer1 = nn.Linear(784, 256)  # 再加自己的东西

4. 为什么 PyTorch 要用继承而不是直接调用

import 只是让你在文件里能使用某个库的名字。新建一个类时,类本身是空壳,不会自动拥有 nn.Module 的内部结构。

() 里写 nn.Module 是声明继承关系,super().__init__() 是让那套结构真正在对象里初始化起来,两步缺一不可。


5.2 nn.Module:神经网络基类

nn.Module是所有神经网络模块的基类。当你定义自己的神经网络时,需要继承nn.Module并实现forward方法。

class MyNetwork(nn.Module):
    def __init__(self):
        super().__init__()  # 调用父类初始化

        # 定义网络层(会自动注册为子模块)
        self.linear1 = nn.Linear(10, 20)
        self.linear2 = nn.Linear(20, 5)
        self.relu = nn.ReLU()

    def forward(self, x):
        # 定义前向传播
        x = self.linear1(x)
        x = self.relu(x)
        x = self.linear2(x)
        return x

# 创建网络实例
net = MyNetwork()

# 查看网络结构
print(net)

# 查看所有可学习参数
for name, param in net.named_parameters():
    print(f"{name}: {param.shape}")

nn.Module的常用方法

方法 作用
net.parameters() 返回所有参数迭代器
net.named_parameters() 返回参数名称和迭代器
net.state_dict() 返回参数字典(用于保存模型)
net.load_state_dict() 加载参数字典
net.train() 切换到训练模式
net.eval() 切换到评估模式
net.to(device) 移动到指定设备

模型保存与加载

import torch
from torch import nn

# 定义模型
model = nn.Linear(3, 1)
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)

# 保存模型和优化器状态
torch.save({
    'model_state_dict': model.state_dict(),
    'optimizer_state_dict': optimizer.state_dict(),
    'epoch': 10
}, 'checkpoint.pth')
print("模型已保存!")

# 加载检查点
checkpoint = torch.load('checkpoint.pth')
model.load_state_dict(checkpoint['model_state_dict'])
optimizer.load_state_dict(checkpoint['optimizer_state_dict'])
epoch = checkpoint['epoch']

print(f"模型已加载,恢复到第 {epoch} 轮训练!")

Mav's Tips: parameters() 简单说就是返回所有 requires_grad=True 的参数。 name_parameters() 是返回参数和结构 state_dict() 以字典形式返回所有参数 load_state_dict() 加载所有参数到新模型中 torch.save() 把数据保存到文件中,搭配 state_dict() 使用 torch.load() 是把文件数据加载出来


5.3 nn.Sequential:快速搭建网络

nn.Sequential是一个简单的容器,用于快速搭建顺序执行的神经网络。

# 方法1:直接传入层
net = nn.Sequential(
    nn.Linear(10, 20),
    nn.ReLU(),
    nn.Linear(20, 5)
)

print(net)
# Sequential(
#   (0): Linear(in_features=10, out_features=20, bias=True)
#   (1): ReLU()
#   (2): Linear(in_features=20, out_features=5, bias=True)
# )

x = torch.randn(3, 10)
output = net(x)
print(output.shape)  # torch.Size([3, 5])

结合OrderedDict可以给每一层命名

from collections import OrderedDict

net = nn.Sequential(OrderedDict([
    ('fc1', nn.Linear(10, 20)),
    ('relu', nn.ReLU()),
    ('fc2', nn.Linear(20, 5))
]))

print(net.fc1.weight.shape)  # 可以用名字访问

适用场景:适合构建简单的线性网络。复杂网络(跳跃连接、多分支等)需要使用nn.Module手动定义。


5.4 nn.CrossEntropyLoss:交叉熵损失

nn.CrossEntropyLoss是分类问题中最常用的损失函数。它结合了LogSoftmax和NLLLoss。

import torch.nn as nn

# 创建损失函数
criterion = nn.CrossEntropyLoss()

# 模拟预测输出(未归一化的分数,也叫logits)
predictions = torch.randn(4, 10)  # 4个样本,10个类别

# 真实标签
labels = torch.tensor([3, 0, 5, 7])

# 计算损失
loss = criterion(predictions, labels)
print("损失值:", loss.item())

注意:使用CrossEntropyLoss时不需要显式地应用softmax,因为损失函数内部已经处理了这一点。

手动验证交叉熵

def manual_cross_entropy(pred, label):
    # 1. softmax归一化
    exp_pred = torch.exp(pred)
    probs = exp_pred / exp_pred.sum(dim=1, keepdim=True)

    # 2. 取正确类别的概率
    label_prob = probs[range(len(label)), label]

    # 3. 计算负对数似然
    loss = -torch.log(label_prob).mean()
    return loss

predictions = torch.randn(4, 10)
labels = torch.tensor([3, 0, 5, 7])

loss_pytorch = nn.CrossEntropyLoss()(predictions, labels)
loss_manual = manual_cross_entropy(predictions, labels)

print("PyTorch损失:", loss_pytorch.item())
print("手动计算:", loss_manual.item())
print("相近:", torch.allclose(loss_pytorch, loss_manual))

**Mav's Tips:**如果你已经对数据使用了 Softmax,那就应该使用 nn.NLLLoss


5.5 torch.optim:优化器

torch.optim提供了各种优化算法,用于更新神经网络的参数。

import torch.optim as optim

# 创建网络
net = nn.Linear(10, 2)

# 创建SGD优化器
optimizer = optim.SGD(net.parameters(), lr=0.01)

# 或者创建Adam优化器(通常收敛更快)
optimizer = optim.Adam(net.parameters(), lr=0.001)

标准训练循环

for epoch in range(100):
    for data, target in dataloader:
        # 1. 前向传播
        output = net(data)
        loss = criterion(output, target)

        # 2. 清零梯度
        optimizer.zero_grad()

        # 3. 反向传播
        loss.backward()

        # 4. 更新参数
        optimizer.step()

常用优化器对比

优化器 优点 缺点 适用场景
SGD 简单、理论基础好 收敛慢、需要调参 研究基线
Adam 自适应学习率、易调参 可能泛化稍差 实际应用首选
AdamW 正则化效果好 - 推荐使用
RMSprop 自适应学习率 - RNN等

学习率调度器

# 每10个epoch降低学习率
scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.1)

for epoch in range(100):
    # 训练...
    scheduler.step()
    print(f"Epoch {epoch}, 学习率: {scheduler.get_last_lr()[0]}")

常见错误与解决方案

1. 形状不匹配错误

# 错误:矩阵乘法维度不匹配
A = torch.randn(2, 3)
B = torch.randn(5, 4)
C = A @ B  # RuntimeError: mat1 and mat2 shapes cannot be multiplied

# 解决:确保维度匹配
B = torch.randn(3, 4)
C = A @ B  # 正确:(2,3) @ (3,4) -> (2,4)

2. 设备不匹配错误

# 错误:模型在GPU,数据在CPU
model = nn.Linear(10, 2).cuda()
data = torch.randn(2, 10)  # 默认在CPU
output = model(data)  # RuntimeError: Expected all tensors to be on the same device

# 解决:将数据移到GPU
data = data.cuda()
# 或
data = data.to('cuda')

3. 梯度未清零

# 错误:梯度累积导致训练异常
for epoch in range(10):
    loss = net(x)
    loss.backward()
    optimizer.step()  # 忘记清零!

# 解决:每次迭代前清零
for epoch in range(10):
    optimizer.zero_grad()
    loss = net(x)
    loss.backward()
    optimizer.step()

4. 类型不匹配

# 错误:类型不匹配
x = torch.randn(3, 4, dtype=torch.float32)
weight = torch.randn(4, 2, dtype=torch.float64)
output = x @ weight  # RuntimeError

# 解决:确保类型一致
weight = weight.float()  # 或 dtype=torch.float32

总结与下一步

本篇笔记核心要点

  1. 张量创建:torch.tensor, torch.randn, torch.zeros, torch.ones
  2. 张量属性:shape, dtype, device
  3. 张量操作:矩阵乘法(@, matmul)、形状变换(reshape)、类型转换(item, numpy)
  4. 自动求导:requires_grad, backward, grad, zero_grad, no_grad
  5. 神经网络模块:nn.Linear, nn.Module, nn.Sequential, CrossEntropyLoss, optim

下一步学习建议

快速查阅表

# 创建张量
torch.tensor([1,2,3])           # 从列表创建
torch.randn(3, 4)                # 随机正态
torch.zeros(3, 4)                # 全零
torch.ones(3, 4)                 # 全一

# 属性
tensor.shape                     # 形状
tensor.dtype                     # 类型
tensor.device                    # 设备

# 操作
x @ w                           # 矩阵乘法
x.reshape(-1)                   # 形状变换
x.item()                        # 转标量
x.numpy()                       # 转numpy

# 自动求导
x.requires_grad = True          # 开启追踪
loss.backward()                 # 反向传播
optimizer.zero_grad()           # 清零梯度
with torch.no_grad():           # 推理模式

# 神经网络
nn.Linear(784, 256)             # 全连接层
nn.Sequential(...)               # 顺序容器
nn.CrossEntropyLoss()           # 损失函数
optim.SGD(params, lr=0.01)      # 优化器

本笔记是USTC学生深度学习笔记系列第一篇


习题

以下习题覆盖本笔记的核心知识点,建议先独立思考,再对照解答。题目难度由易到难排列。

习题 1(基础):下列代码的输出结果是什么?请说明理由。   [ 查看解答 ]

import torch
a = torch.tensor([[1, 2, 3], [4, 5, 6]])
print(a.ndim, a.shape, a.dtype)

习题 2(基础)torch.arangetorch.linspace 有什么关键区别?分别给出一个适合使用各自函数的场景。   [ 查看解答 ]

习题 3(理解):以下代码的输出是什么?为什么?如何避免这种"惊喜"?   [ 查看解答 ]

import numpy as np, torch
np_arr = np.array([1.0, 2.0, 3.0])
t = torch.from_numpy(np_arr)
np_arr[0] = 999
print(t[0])

习题 4(计算):给定形状为 (3, 4) 的张量 A 和形状为 (4, 2) 的张量 B,A @ B 的结果形状是多少?   [ 查看解答 ] 若将 B 改为 (3, 4)torch.mul(A, B) 的结果形状又是多少?两者有何本质区别?

习题 5(实践):用 reshape 将形状为 (8, 3, 4) 的张量依次变换为以下形状,写出对应代码,哪些是无法转换的?   [ 查看解答 ]

  1. (8, 12)
  2. (2, -1)
  3. (8, 3, 5)

习题 6(推导):阅读下面的代码,手动推导 x.grad 的值,然后运行代码验证。   [ 查看解答 ]

x = torch.tensor([2.0, 3.0], requires_grad=True)
y = 3 * x[0] ** 2 + x[1] ** 3
y.backward()
print(x.grad)

习题 7(陷阱):下面的训练循环存在一个经典错误,请找出并修正,并解释为什么会出问题。   [ 查看解答 ]

x = torch.tensor([1.0], requires_grad=True)
optimizer = torch.optim.SGD([x], lr=0.1)
for i in range(5):
    loss = x ** 2
    loss.backward()
    optimizer.step()  
    print(f"step {i}: x={x.item():.4f}, grad={x.grad.item():.4f}")

习题 8(计算)nn.Linear(in_features=4, out_features=3) 内部有多少个可学习参数?请结合线性变换的数学公式说明。   [ 查看解答 ]

习题 9(综合):用 nn.Sequential 搭建一个用于 MNIST 分类的三层全连接网络,并写出损失函数和优化器。   [ 查看解答 ]

习题 10(综合·思考):在推理阶段,我们通常会同时使用 net.eval()with torch.no_grad():。请解释这两者的各自目的与区别。   [ 查看解答 ]


习题参考解答

解答 1
输出:2 torch.Size([2, 3]) torch.int64
理由:a 是 2x3 矩阵,维度为 2;整数输入默认为 int64。   [ 返回习题 ]

解答 2

解答 3
输出:tensor(999.)
原因:from_numpy 共享内存。修正:使用 torch.tensor(np_arr) 复制数据。   [ 返回习题 ]

解答 4

解答 5

  1. t.reshape(8, 12) (OK)
  2. t.reshape(2, -1) (OK)
  3. t.reshape(8, 3, 5) (无法转换:元素总数不符)。   [ 返回习题 ]

解答 6
导数:$6x_0 = 12$,$3x_1^2 = 27$。结果:tensor([12., 27.])。   [ 返回习题 ]

解答 7
错误:缺少 optimizer.zero_grad()。PyTorch 梯度默认累积,不清零会导致训练爆炸。   [ 返回习题 ]

解答 8
$4 \times 3$ (权重矩阵) + $3$ (偏置) = 15个参数。   [ 返回习题 ]

解答 9

net = nn.Sequential(
    nn.Linear(784, 256), 
    nn.ReLU(), 	
    nn.Linear(256, 128), 
    nn.ReLU(), 
    nn.Linear(128, 10)
)
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(net.parameters(), lr=1e-3)

  [ 返回习题 ]

解答 10