PyTorch从零开始
欢迎来到 PyTorch 的世界!如果你正准备踏入深度学习的大门,PyTorch 是你绕不开的工具。别被它看似高深的名字吓到,其实它非常 Pythonic(符合 Python 的编程风格),上手难度并不高。
这篇文章将带你从零开始,理解 PyTorch 的核心概念。
一、为什么学PyTorch?
PyTorch 是目前最流行的深度学习框架,简单易用、动态图机制、Python 原生风格,对新手极其友好。
学习 PyTorch 第一步:先掌握核心数据结构 —— 张量(Tensor),它就是 PyTorch 里的 “数组”,所有运算都围绕它展开。
二、环境准备
1. 安装 PyTorch
最稳妥的方法是去 PyTorch 官网获取适合你系统的命令。如果你是初学者,且没有 NVIDIA 显卡(或者不想折腾 CUDA),先安装 CPU 版本即可:
pip install torch torchvision torchaudio
2. 验证安装
打开你的 Python 编辑器(如 Jupyter Notebook 或 PyCharm),输入以下代码。如果没报错并输出了版本号,恭喜你,第一步成功了!
import torch
print(f"PyTorch 版本: {torch.__version__}")
# 检查是否有可用的 GPU (如果没有显卡,这里会输出 False)
print(f"CUDA 是否可用: {torch.cuda.is_available()}")
三、PyTorch 核心基础:张量(Tensor)
1. 什么是张量?
在 PyTorch 中,张量(Tensor) 是绝对的主角。你可以把它简单理解为多维数组。
- 0 维张量(标量): 一个数字,比如
5。 - 1 维张量(向量): 一排数字,比如
[1, 2, 3]。 - 2 维张量(矩阵): 一个表格,比如
[[1, 2], [3, 4]]。 - 3 维及以上: 可以想象成一个“数据块”,比如 RGB 彩色图片就是一个 3 维张量(长 × 宽 × 3个颜色通道)。
张量 = PyTorch 中存储和运算的基本单位
2. 创建张量(最常用方法)
import torch
# 1. 从 Python 列表创建
data = [[1, 2], [3, 4]]
x_data = torch.tensor(data)
# 2. 创建全 0 或全 1 的张量 (常用于初始化)
zeros = torch.zeros(2, 3) # 2行3列的全0矩阵
ones = torch.ones(2, 3) # 2行3列的全1矩阵
# 3. 创建随机数 (神经网络初始化常用)
rand = torch.rand(2, 3) # 0到1之间的随机数
print(x_data.shape) # 查看形状,输出: torch.Size([2, 2])
print(x_data.dtype) # 查看数据类型,输出: torch.int64
3. 张量的数据类型
新手常用两种:
torch.float32(浮点型,训练模型默认)torch.int64(整型,标签常用)
import torch
x = torch.tensor([1,2,3])
x_float = x.float() # 转浮点型
x_int = x.long() # 转整型
4. 张量索引与切片
基础索引
PyTorch 的索引规则与 Python 原生的列表以及 NumPy 数组非常相似。对于一维张量,你可以直接使用方括号和数字来访问元素。需要注意的是,索引是从 0 开始的,同时也支持负数索引,-1 代表最后一个元素。
import torch
# 创建一个一维张量
x = torch.tensor([10, 20, 30, 40, 50])
# 访问第一个元素
print(x[0]) # 输出: tensor(10)
# 访问最后一个元素 (负数索引)
print(x[-1]) # 输出: tensor(50)
当我们将目光转向二维张量(矩阵)时,索引变得更加有趣。我们需要同时指定“行”和“列”。虽然你可以使用链式方括号(如 x[0][1]),但在 PyTorch 中,更推荐且高效的做法是在一个方括号内用逗号分隔行列索引,即 x[行, 列]。
import torch
# 创建一个 3x3 的矩阵
matrix = torch.tensor([
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
])
# 访问第 1 行(索引0),第 2 列(索引1)的元素 -> 数字 2
print(matrix[0, 1]) # 输出: tensor(2)
# 访问第 2 行(索引1)的所有元素
# 使用冒号 : 表示“所有”
print(matrix[1, :]) # 输出: tensor([4, 5, 6])
切片操作
如果说基础索引是“点”的访问,那么切片就是“线”或“面”的提取。切片允许我们提取张量的一部分,而不是单个元素。其核心语法是 start:stop:step(起始:结束:步长)。
这里有一个新手常犯的错误:切片是左闭右开的。这意味着当你写 0:2 时,你取到的是索引 0 和 1 的元素,不包含索引 2。
import torch
# 创建一个 1 到 10 的张量
x = torch.arange(1,11) # [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# 获取前三个元素 (索引 0, 1, 2)
print(x[0:3]) # 输出: tensor([1, 2, 3])
# 获取中间的元素 (从索引 3 到 7,不包含 7)
print(x[3:7]) # 输出: tensor([4, 5, 6, 7])
# 步长切片:每隔一个取一个 (从索引 0 开始,步长为 2)
print(x[::2]) # 输出: tensor([1, 3, 5, 7, 9])
易踩坑点:PyTorch 的原生切片操作不支持负步长(Negative Step)。
如果你习惯了 Python 中 [::-1] 这种优雅的倒序写法,在 PyTorch 中直接使用会直接报错:ValueError: step must be greater than zero。
错误示范:
import torch
# 创建一个一维张量
x = torch.tensor([1, 2, 3, 4, 5])
# 尝试使用负步长倒序
try:
print(x[::-1])
except ValueError as e:
print(f"报错啦: {e}")
# 输出: 报错啦: step must be greater than zero
既然不能用 [::-1],我们该怎么实现倒序(翻转)呢?PyTorch 提供了专门的函数和高级索引方法。
使用专用函数
这是最规范、最高效的写法。PyTorch 提供了 torch.flip() 函数,你可以指定沿着哪个维度进行翻转。
import torch
# 创建一个一维张量
x = torch.tensor([1, 2, 3, 4, 5])
# dims=[0] 表示沿着第0个维度(也就是唯一的那个维度)翻转
reversed_x = torch.flip(x, dims=[0])
print
PyTorch 禁止在切片中使用负步长,主要是为了维护内存视图的简单性和计算效率,因为反向切片会破坏内存的连续性布局,导致实现复杂且性能下降,因此官方推荐使用专门的 torch.flip() 函数来实现翻转操作。
负索引切片
可以使用负索引来从张量尾部进行切片
import torch
# 创建一个一维张量
x = torch.tensor([1, 2, 3, 4, 5])
# 使用负索引
slice_1 = x[-3:] # 获取最后三项 补全是从[-3::1] 也就是从-3位置取到末尾步长为1
print(slice_1) # 输出:tensor([3,4,5])
# 使用负索引切片
slice_2 = x[-5:-2] # 获取倒数第五到倒数第二的元素
print(slice_2) # 输出:tensor([1,2,3])
四、PyTorch 基础运算
PyTorch 的运算操作和 NumPy 非常像,但 PyTorch 可以在 GPU 上加速运算。张量运算和 Python 数学运算几乎一致,直接用符号即可,无需复杂语法。
1. 逐元素运算
这些是逐元素(Element-wise)的操作,,即对应位置的数字进行计算。这里遵循广播机制(Broadcasting)。
import torch
x = torch.tensor([1, 2, 3])
y = torch.tensor([4, 5, 6])
# 加法
print(x + y) # 结果: [5, 7, 9]
# 减法
print(x - y) # 结果: [-3, -3, -3]
# 乘法 (注意:这是对应位置相乘,不是矩阵乘法)
print(x * y) # 结果: [4, 10, 18]
# 除法
print(x / y) # 结果: [0.2500, 0.4000, 0.5000]
# 幂运算
print(x ** 2) # 结果: [1, 4, 9]
2. 矩阵乘法
神经网络的核心就是矩阵乘法。在 PyTorch 中,我们需要严格区分“逐元素相乘”和“线性代数乘法”。
*:逐元素相乘(如上所述)。@或torch.matmul():真正的矩阵乘法(推荐)。torch.mm():严格的矩阵乘法(仅支持2维)。
# 矩阵乘法示例
# 规则:前一个矩阵的列数必须等于后一个矩阵的行数 (N, K) @ (K, M) -> (N, M)
mat_a = torch.tensor([[1, 2], [3, 4]]) # 2x2
mat_b = torch.tensor([[5, 6], [7, 8]]) # 2x2
# 使用 @ 符号 (推荐,等价于 torch.matmul)
result = mat_a @ mat_b
# 计算过程:
# 第一行乘第一列 = 1*5 + 2*7 = 19
# 第一行乘第二列 = 1*6 + 2*8 = 22
# ...
print(result)
# 输出:
# tensor([[19, 22],
# [43, 50]])
虽然 torch.mm 和 torch.matmul 都是做矩阵乘法,但在深度学习中,强烈建议使用 torch.matmul (或 @)。
| 特性 | torch.mm | torch.matmul / @ |
|---|---|---|
| 维度支持 | 仅支持 2D (矩阵) | 支持 N维 (向量、3D张量等) |
| 广播机制 | 不支持 | 支持 (自动处理 Batch) |
| 向量处理 | 必须显式指定维度 | 自动处理 (自动补 1) |
| 推荐场景 | 极少使用 (仅当你确定不需要广播时) | 绝大多数情况 (神经网络层、Transformer等) |
关于向量的自动处理逻辑:
当使用 matmul 时,PyTorch 会智能处理 1维向量:
- 左乘
(3,) @ (3, 4):向量被视为行向量(1, 3),结果为(4,)。 - 右乘
(2, 3) @ (3,):向量被视为列向量(3, 1),结果为(2,)。
3. 聚合运算
维度的“轴”
在深度学习中,我们经常需要将一堆数据汇总成一个数值,或者沿着某个方向进行汇总。
什么是dim?
这是PyTorch中最容易让人晕头转向的概念。请记住一句话:dim 指的是“消失”的那个维度
当我们在执行聚合运算(如 sum, mean, max)时,我们需要告诉PyTorch沿着哪个方向 “挤压” 数据。
dim=0:沿着第 0 维(行)挤压。结果是行没了,剩下列的统计值。dim=1:沿着第 1 维(列)挤压。结果是列没了,剩下行的统计值。
import torch
# 创建一个 2x3 的矩阵
# [[1, 2, 3],
# [4, 5, 6]]
x = torch.tensor([[1, 2, 3], [4, 5, 6]], dtype=torch.float)
# 1. 全局聚合:把所有数字压成一个数
print(f"总和: {x.sum()}") # 21.0
# 2. 按列聚合 (dim=0):把行“拍扁”,计算每一列的和
# [1+4, 2+5, 3+6] -> [5, 7, 9]
print(f"按列求和 (dim=0): {x.sum(dim=0)}")
# 3. 按行聚合 (dim=1):把列“拍扁”,计算每一行的和
# [1+2+3, 4+5+6] -> [6, 15]
print(f"按行求和 (dim=1): {x.sum(dim=1)}")
常见的聚合函数
x = torch.tensor([[1,2,3], [4,5,6]])
# 总和
print(x.sum())
# 均值
print(x.float().mean())
# 最大值
print(x.max())
# 按维度求和(dim=0 列,dim=1 行)
print(x.sum(dim=0))
Softmax与Argmax
在分类任务中,有两个函数出镜率极高,它们也是聚合运算的一种特殊形式。
Softmax:将数值转化为概率
神经网络的输出通常是任意范围的实数(比如 [-2.0, 5.0, 1.3]),我们无法直接判断它属于哪一类。Softmax 的作用就是把这些数值“压缩”到 (0, 1) 之间,并且让它们的和为 1,从而变成概率分布。
关键点:Softmax 也是依赖 dim 的。通常在分类任务中,我们对特征维度(通常是 dim=1)做 Softmax。
# 假设这是模型输出的原始分数 (logits)
logits = torch.tensor([[2.0, 1.0, 0.1]])
# 沿着 dim=1 进行 Softmax
probs = torch.softmax(logits, dim=1)
print(probs)
# 输出类似: tensor([[0.6590, 0.2424, 0.0986]])
# 和为 1,表示属于第0类的概率最大
Argmax:寻找最大值的索引
如果我们不关心概率具体是多少,只关心**“到底是哪一类”**,我们就用 argmax。它会返回最大值的下标。
# 接着上面的例子
prediction = torch.argmax(probs, dim=1)
print(prediction)
# 输出: tensor([0])
# 表示模型预测这是第 0 类
Item:从张量到 Python 数值
用于提取张量中的单个元素,将其转换为标准的 Python 数值(float 或 int)。
loss = torch.tensor(2.56789)
# 直接打印张量
print(f"Loss张量: {loss}")
# 输出: Loss张量: 2.5679 (依然带有tensor的属性)
# 提取为Python数值
loss_value = loss.item()
print(f"Loss数值: {loss_value:.2f}")
# 输出: Loss数值: 2.57 (现在它是纯粹的浮点数了)
注意:.item() 只能用于只有一个元素的张量。如果张量里有多个数字(如 [1, 2]),调用 .item() 会报错。
五、嵌套与解套
这是新手最容易晕的地方。在深度学习中,数据经常需要改变形状才能送入网络层。我们常说的“嵌套”通常指增加维度,“解套”指减少维度。
1. 改变形状(Reshape/View)
如果你有一批数据,形状是 (2, 3)(2行3列),你想把它拉平成 (1, 6)(1行6列),可以使用 view() 或 reshape()。
tensor = torch.tensor([[1, 2, 3], [4, 5, 6]]) # 形状 (2, 3)
# 把它变成 (3, 2)
reshaped = tensor.view(3, 2)
print(reshaped)
2. 增加维度(Unsqueeze)——“嵌套”
假设你有一张图片,形状是 (3, 224, 224)(通道, 高, 宽)。但是神经网络通常要求输入是一个“批次(Batch)”,即使你只有一张图,也要变成 (1, 3, 224, 224)。这时就需要 unsqueeze。
x = torch.tensor([1, 2, 3]) # 1维向量,形状 (3,)
print(x.shape)
# 在第 0 维增加一个维度(变成行向量)
y = x.unsqueeze(0)
print(y.shape) # 输出: torch.Size([1, 3]),变成了 [[1, 2, 3]]
# 在第 1 维增加一个维度(变成列向量)
z = x.unsqueeze(1)
print(z.shape) # 输出: torch.Size([3, 1]),变成了 [[1], [2], [3]]
3. 去除维度(Squeeze)——“解套”
反过来,如果你的数据形状是 (1, 3),你想把它变回 (3,),就用 squeeze。它会去除所有长度为 1 的维度。
x = torch.tensor([[1, 2, 3]]) # 形状 (1, 3)
print(x.shape)
# 去除长度为1的维度
y = x.squeeze()
print(y.shape) # 输出: torch.Size([3])
print(y) # 输出: tensor([1, 2, 3])
小贴士: 如果维度长度不是 1(比如 (2, 3)),squeeze 是不会改变形状的。
六、底层揭秘:Storage 与 Stride
在前面的内容中我们提到了view()和reshape()可以改变张量的形状。你有没有想过,为什么改变形状通常非常快,而不需要重新复制数据?
这涉及到了PyTorch张量的两个核心底层概念:**存储(Storage)**和 步幅(Stride)。
1.什么是Storage?(数据的”本体“)
虽然我们眼中的张量可能是 2维的表格或 3维的立方体,但在计算机内存中,数据最终都是存储在一条连续的线性内存块中的。
- Storage 就是这块连续的内存区域。
- 无论你如何改变张量的形状(比如从2×3变为3×2),底层的 Storage 通常是不变的(除非发生了数据拷贝)。
2.什么是Stride?(数据的“导航图”)
既然数据在内存里是一条直线,PyTorch 怎么知道哪个数字属于哪一行、哪一列呢?这就需要Stride。
Stride是一个元组,它告诉 PyTorch:“为了移动到下一个维度的下一个元素,我需要在 Storage 中跳过多少个位置。”
公式: Stride = (移动到下一行需要的步长, 移动到下一列需要的步长)
3.实例
让我们通过一个具体的例子来看透它:
import torch
# 创建一个 3x4 的矩阵 (0-11)
# [[ 0, 1, 2, 3],
# [ 4, 5, 6, 7],
# [ 8, 9, 10, 11]]
x = torch.arange(12).reshape(3, 4)
print(f"形状 (Shape): {x.shape}") # 形状 (Shape): torch.Size([3, 4])
print(f"步幅 (Stride): {x.stride()}") # 步幅 (Stride): (4, 1)
解读Stride(4,1):
- 第 0 维(行):步幅是4。意味着你想从
x[0,0](数字0)跳到x[1,0](数字4),你需要在内存中跨过四个元素。 - 第 1 维(列):步幅是 1。意味着如果你想从
x[0,0](数字0) 跳到x[0,1](数字1),你只需要在内存中向后移动 1 个位置。
4. 为什么这很重要?(视图 vs 拷贝)
理解了 Stride,你就能理解为什么 transpose(转置)操作非常快。
当我们对张量进行转置时,PyTorch 并没有把数据在内存里搬来搬去,它只是修改了 Stride!
# 对上面的 x 进行转置
y = x.t()
print(f"转置后的形状: {y.shape}") # torch.Size([4, 3])
print(f"转置后的步幅: {y.stride()}") # (1, 4)
神奇的变化:
- 转置后,Stride 变成了
(1, 4)。 - 这意味着:现在“行”的步幅是 1,“列”的步幅是 4。
- PyTorch 只是换了一副“眼镜”去读取同一块内存数据,数据本身(Storage)完全没动。
避坑指南:
由于 transpose、permute、slice 等操作共享底层 Storage,如果你修改了转置后的张量 y,原始的张量 x 也会跟着变!
如果你需要一个完全独立、互不干扰的副本,请务必使用 .contiguous() 强制复制数据:
z = y.contiguous() # 这才是真正的内存拷贝
七、优化
节省内存:原地操作
在深度学习中,数据量往往非常大。如果我们每做一次加法就开辟一块新内存,显存很快就会爆掉。
为什么 Y = Y + X 很慢?
在 Python 中,Y = Y + X 的执行过程是这样的:
- 计算
Y + X的结果,申请一块新的内存来存储这个结果。 - 把变量名
Y指向这块新内存。 - 把旧的
Y扔给垃圾回收机制去清理。
这不仅慢,还浪费内存。
原地操作
为了解决这个问题,PyTorch 提供了“原地操作”。这些函数通常带有一个下划线后缀 _(例如 add_, copy_, zero_)。它们直接在原有的内存地址上修改数据,不申请新内存。
### **节省内存:原地操作**
# 初始化数据
X = torch.arange(10)
Y = torch.arange(10)
# 1. 非原地操作: Y = Y + X
# 这会创建一个新张量来存储结果
print("非原地操作 (Y = Y + X):")
id_old = id(Y)
Y = Y + X
id_new = id(Y)
print(f"内存地址是否改变: {id_old != id_new}") # 输出 True
print(f"结果: {Y}")
# 重置 Y 进行对比
Y = torch.arange(10)
# 2. 原地操作: Y.add_(X)
# 这直接修改 Y 的值,不创建新张量
print("\n原地操作 (Y.add_(X)):")
id_old_inplace = id(Y)
Y.add_(X) # 注意函数名后的下划线 "_"
id_new_inplace = id(Y)
print(f"内存地址是否改变: {id_old_inplace == id_new_inplace}") # 输出 True (地址未变)
print(f"结果: {Y}")
建议:在训练循环中,尽量利用原地操作(或者像 optimizer.step() 这种封装好的机制)来节省宝贵的显存资源。
更多推荐



所有评论(0)