欢迎来到 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.mmtorch.matmul 都是做矩阵乘法,但在深度学习中,强烈建议使用 torch.matmul (或 @)

特性torch.mmtorch.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):

  1. 第 0 维(行):步幅是4。意味着你想从x[0,0](数字0)跳到 x[1,0] (数字4),你需要在内存中跨过四个元素。
  2. 第 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)完全没动。

避坑指南:
由于 transposepermuteslice 等操作共享底层 Storage,如果你修改了转置后的张量 y,原始的张量 x 也会跟着变!

如果你需要一个完全独立、互不干扰的副本,请务必使用 .contiguous() 强制复制数据:

z = y.contiguous() # 这才是真正的内存拷贝

七、优化

节省内存:原地操作

在深度学习中,数据量往往非常大。如果我们每做一次加法就开辟一块新内存,显存很快就会爆掉。

为什么 Y = Y + X 很慢?

在 Python 中,Y = Y + X 的执行过程是这样的:

  1. 计算 Y + X 的结果,申请一块新的内存来存储这个结果。
  2. 把变量名 Y 指向这块新内存。
  3. 把旧的 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() 这种封装好的机制)来节省宝贵的显存资源。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐