1. 张量:PyTorch 的核心数据结构
PyTorch 中的所有数据都是以张量(Tensor) 的形式存在的。你可以把张量简单理解成一个多维数组:
- 一个单独的数字是 0 维张量(标量)
- 一列数字是 1 维张量(向量)
- 一张表格是 2 维张量(矩阵)
- 一张彩色图片有高度、宽度、颜色通道,所以是 3 维张量
理解张量的创建、操作和变换,是使用 PyTorch 的第一步。下面我们会从最基础的创建开始,逐步深入到自动微分,最后完成一个真实的线性回归训练。
本文所有代码都可以直接运行,建议打开 Jupyter Notebook 边看边敲。
2. 创建张量的常用方法
2.1 从列表或 NumPy 数组创建
最直接的方式是使用 torch.tensor():
import torch
import numpy as np
# 从一个数字创建标量张量
tensor_scalar = torch.tensor(10)
print(tensor_scalar) # tensor(10)
# 从 Python 列表创建一维张量
tensor_1d = torch.tensor([1, 2, 3])
print(tensor_1d) # tensor([1, 2, 3])
# 从 NumPy 数组创建二维张量
numpy_arr = np.array([[1, 2, 3], [4, 5, 6]])
tensor_2d = torch.tensor(numpy_arr)
print(tensor_2d)
# 输出:
# tensor([[1, 2, 3],
# [4, 5, 6]])
2.2 创建指定形状的张量
如果你只关心张量的形状,暂时不关心具体数值,可以用 torch.Tensor(注意大写 T)。它分配内存但不会初始化,里面的数值是随机的。
# 创建一个形状为 (3, 2, 4) 的三维张量,默认数据类型是 float32
tensor_shape = torch.Tensor(3, 2, 4)
print(tensor_shape.shape) # torch.Size([3, 2, 4])
print(tensor_shape.dtype) # torch.float32
# 也可以直接填入数据,效果类似 torch.tensor,但建议统一用 torch.tensor
tensor_with_data = torch.Tensor([[1, 2], [3, 4]])
print(tensor_with_data)
2.3 指定数据类型创建张量
PyTorch 支持多种数据类型,常见的有 int32、int64、float32、float64。有两种方式指定类型:
# 方式一:使用专用构造函数
tensor_int32 = torch.IntTensor(2, 3) # int32 类型
tensor_int64 = torch.LongTensor([1, 2, 3]) # int64 类型
tensor_f32 = torch.FloatTensor([9, 8, 7]) # float32 类型
tensor_f64 = torch.DoubleTensor(2, 3, 1) # float64 类型
# 方式二:使用 dtype 参数(推荐,更清晰)
tensor_a = torch.tensor([1, 2, 3], dtype=torch.int32)
tensor_b = torch.tensor([1, 2, 3], dtype=torch.float32)
# 注意:如果数据类型不匹配,小数会被直接截断
tensor_trunc = torch.IntTensor([[1.1, 2.2, 3.6]])
print(tensor_trunc) # tensor([[1, 2, 3]], dtype=torch.int32)
2.4 生成等差数列或等比数列
在构造输入数据或设置参数时,经常需要生成连续的数值序列。
# torch.arange(start, end, step) --- 按步长生成 [start, end) 区间内的数
tensor_arange = torch.arange(10, 30, 2)
print(tensor_arange) # tensor([10, 12, 14, 16, 18, 20, 22, 24, 26, 28])
# 只给 end,默认从 0 开始,步长为 1
tensor_arange_simple = torch.arange(6)
print(tensor_arange_simple) # tensor([0, 1, 2, 3, 4, 5])
# torch.linspace(start, end, steps) --- 在 [start, end] 区间内均匀生成指定数量的数
tensor_linspace = torch.linspace(10, 30, 5)
print(tensor_linspace) # tensor([10., 15., 20., 25., 30.])
# torch.logspace(start, end, steps, base) --- 生成等比数列
# 底数为 2,指数从 1 到 3,共 3 个数 → 2^1=2, 2^2=4, 2^3=8
tensor_logspace = torch.logspace(1, 3, 3, 2)
print(tensor_logspace) # tensor([2., 4., 8.])
2.5 用固定值填充
模型训练前初始化权重时,全 0、全 1 或者固定值的张量非常常用。
# 全 0 张量
zeros = torch.zeros(2, 3)
print(zeros)
# tensor([[0., 0., 0.],
# [0., 0., 0.]])
# 全 1 张量,形状与已有张量相同
ones_like = torch.ones_like(zeros)
print(ones_like)
# 所有元素都填成同一个值
full = torch.full((2, 3), 6)
print(full)
# 未初始化的空张量(内容随机,但分配速度很快)
empty = torch.empty(2, 3)
print(empty)
# 单位矩阵(对角线为 1)
eye = torch.eye(3)
print(eye)
# tensor([[1., 0., 0.],
# [0., 1., 0.],
# [0., 0., 1.]])
2.6 随机张量的创建
随机初始化是神经网络训练的关键步骤。PyTorch 提供了多种随机分布。
# 均匀分布 [0, 1) 上的随机数
rand_uniform = torch.rand(2, 3)
print(rand_uniform)
# 整数均匀分布 [low, high) 上的随机整数
rand_int = torch.randint(1, 10, (2, 3))
print(rand_int)
# 标准正态分布 N(0, 1) 上的随机数
rand_normal = torch.randn(4, 2)
print(rand_normal)
# 自定义正态分布 N(mean, std),形状可以指定
rand_custom = torch.normal(5, 1, (2, 3))
print(rand_custom)
# 随机排列(洗牌):生成 0 到 n-1 的随机顺序
rand_perm = torch.randperm(10)
print(rand_perm) # 每次运行结果不同,例如 tensor([4, 8, 0, 2, 3, 7, 1, 5, 9, 6])
# 固定随机种子,让结果可以复现
print(torch.random.initial_seed()) # 查看当前种子
torch.manual_seed(42) # 设置种子为 42
print(torch.random.initial_seed())
3. 张量的类型转换与数组互转
3.1 修改张量的数据类型
训练过程中有时需要切换精度(比如从 float64 转为 float32 以节省显存)。
tensor = torch.tensor([1, 2, 3])
print(tensor, tensor.dtype) # tensor([1, 2, 3]) torch.int64
# 方法一:使用 type() 方法
tensor = tensor.type(torch.float32)
print(tensor, tensor.dtype) # tensor([1., 2., 3.]) torch.float32
# 方法二:使用专用转换方法(更简洁)
tensor = tensor.double() # 转换为 float64
tensor = tensor.long() # 转换为 int64
3.2 Tensor 与 NumPy 数组互转
PyTorch 和 NumPy 可以非常方便地互相转换,但要注意内存共享的问题。
# Tensor → NumPy(共享内存)
tensor = torch.rand(3, 2)
numpy_arr = tensor.numpy()
print(type(tensor), type(numpy_arr)) # <class 'torch.Tensor'> <class 'numpy.ndarray'>
# 修改 Tensor,NumPy 数组也会跟着变
tensor[:, 0] = 4
print("修改后的 Tensor:\n", tensor)
print("同步变化的 NumPy 数组:\n", numpy_arr)
# 如果不希望共享内存,可以复制一份
numpy_arr_copy = tensor.numpy().copy()
tensor[:, 0] = -1
print("Tensor 再次修改后:\n", tensor)
print("NumPy 副本没有变化:\n", numpy_arr_copy)
# NumPy → Tensor(同样共享内存)
numpy_arr = np.random.randn(3)
tensor_from_np = torch.from_numpy(numpy_arr)
print("原始 NumPy:", numpy_arr)
print("转换得到的 Tensor:", tensor_from_np)
# 修改 NumPy,Tensor 也会变
numpy_arr[0] = 100
print("修改 NumPy 后:\n", numpy_arr)
print("Tensor 同步变化:\n", tensor_from_np)
# 如果想彻底独立,用 copy()
tensor_safe = torch.from_numpy(numpy_arr.copy())
# 使用 torch.tensor() 也会创建独立副本(深拷贝)
tensor_independent = torch.tensor(numpy_arr)
3.3 张量提取标量
如果张量只有一个元素,可以用 .item() 把它提取成 Python 的普通数字。
scalar_tensor = torch.tensor(1)
print(scalar_tensor) # tensor(1)
print(scalar_tensor.item()) # 1 (Python int)
4. 张量的数值计算
4.1 基本运算(加减乘除、幂、平方根、指数、对数)
PyTorch 的运算分为两种:
-
不修改原数据:add(), sub(), mul(), div() 等
-
就地修改原数据:带下划线的版本,如 add_(), sub_(), mul_(), div_()
tensor = torch.randint(1, 9, (2, 3))
print("原始张量:\n", tensor)加法(不修改原数据)
result = tensor.add(10)
print("加法结果:\n", result)
print("原张量不变:\n", tensor)加法(就地修改)
tensor.add_(10)
print("就地加法后:\n", tensor)类似地,减法 sub()/sub_(),乘法 mul()/mul_(),除法 div()/div_()
取负
print("取负:\n", tensor.neg())
幂运算
pow_tensor = torch.tensor([1, 2, 3])
pow_tensor.pow_(2) # 原地求平方
print("平方后:", pow_tensor) # tensor([1, 4, 9])平方根
sqrt_tensor = torch.tensor([1.0, 2.0, 3.0])
sqrt_tensor.sqrt_()
print("平方根后:", sqrt_tensor)指数运算 e^x
exp_tensor = torch.tensor([1.0, 2.0, 3.0])
print("e^tensor:", exp_tensor.exp())自然对数 ln(x)
print("ln(tensor):", exp_tensor.log())
4.2 哈达玛积(元素级乘法) vs 矩阵乘法
这两个概念经常被初学者混淆,一定要分清。
-
哈达玛积:两个形状相同的张量,对应位置元素相乘,结果形状不变。
-
矩阵乘法:按照线性代数的规则相乘(行 × 列),形状会改变。
哈达玛积(对应元素相乘)
a = torch.tensor([[1, 2], [3, 4]])
b = torch.tensor([[1, 2], [3, 4]])
hadamard = a * b # 也可以用 a.mul(b)
print("哈达玛积:\n", hadamard)输出:
tensor([[1, 4],
[9, 16]])
矩阵乘法
A = torch.tensor([[1, 2, 3], [4, 5, 6]]) # 形状 (2, 3)
B = torch.tensor([[1, 2], [3, 4], [5, 6]]) # 形状 (3, 2)
matmul_result = A.mm(B) # 二维矩阵专用,也可以用 A @ B 或 A.matmul(B)
print("矩阵乘法结果:\n", matmul_result)输出:
tensor([[22, 28],
[49, 64]])
4.3 节省内存的技巧
在训练循环中,如果直接写 X = X @ Y,Python 会先计算 X @ Y 得到一个新张量,然后把变量名 X 指向这个新张量。原来的内存会被丢弃,这会导致频繁的内存分配和释放,影响性能。
更好的做法是使用切片赋值,把结果直接写回到原内存中:
X = torch.randint(1, 9, (3, 2, 4))
Y = torch.randint(1, 9, (3, 4, 1))
print("原始 id:", id(X))
X[:] = X @ Y # 原地更新,id 不会变
print("原地更新后 id:", id(X))
5. 常用的张量运算函数
PyTorch 内置了许多统计和操作函数,可以大大简化代码。
# 创建一个 3×2×4 的随机整数张量,并转为 float 类型以便求均值
tensor = torch.randint(1, 9, (3, 2, 4)).float()
print("原始形状:", tensor.shape)
# 求和
print("所有元素之和:", tensor.sum())
print("按第 0 维求和后的形状:", tensor.sum(dim=0).shape) # (2, 4)
# 求均值
print("所有元素的均值:", tensor.mean())
print("按第 1 维求均值后的形状:", tensor.mean(dim=1).shape) # (3, 4)
# 最大值及其索引
max_val = tensor.max()
print("全局最大值:", max_val)
# 按维度求最大值,返回 (最大值, 索引)
max_vals, max_idxs = tensor.max(dim=2)
print("沿第 2 维的最大值:", max_vals)
print("对应的索引:", max_idxs)
# 最小值索引(展平后的位置)
print("最小值索引(一维位置):", tensor.argmin())
# 标准差
print("标准差:", tensor.std())
# 去重
print("去重后的元素:", tensor.unique())
# 排序(返回排序后的值和原始索引)
sorted_vals, sorted_idxs = tensor.sort()
print("排序后的值:", sorted_vals)
dim 参数的含义:dim 表示你要沿着哪个维度进行压缩。例如,形状为 (3, 2, 4) 的张量,sum(dim=0) 会沿着第 0 维(大小为 3)求和,结果形状变成 (2, 4)。
6. 索引与切片
6.1 基本索引和范围索引
PyTorch 的索引语法和 NumPy 几乎一样,非常直观。
tensor = torch.randint(1, 9, (3, 5, 4))
print("原始形状:", tensor.shape) # (3, 5, 4)
# 取第 0 维的第 0 个元素(得到 5×4 的矩阵)
print(tensor[0])
# 取所有第 0 维,第 1 维的第 1 个元素(得到 3×4 的矩阵)
print(tensor[:, 1])
# 精确取一个标量:第 2 维的第 1 个,第 1 维的第 3 个,第 2 维的第 4 个(索引从 0 开始)
print(tensor[2, 1, 3])
# 范围切片
print(tensor[-1, 1:4, 0:3]) # 最后一批,第 1~3 行,前 3 列
6.2 列表索引和布尔索引
列表索引可以同时取多个不连续的位置。布尔索引则非常强大,可以按条件筛选元素。
# 列表索引:取 (0,1) 和 (1,2) 位置的值
print(tensor[[0, 1], [1, 2]])
# 布尔索引:找出满足条件的元素
# 条件:第 2 维的第 0 个通道的值大于 5
mask = tensor[:, :, 0] > 5
print("布尔掩码形状:", mask.shape) # (3, 5)
print("满足条件的元素:", tensor[mask])
# 更精细的筛选:特定位置大于 5
mask2 = tensor[:, 1, 2] > 5
print("满足条件的整批数据:", tensor[mask2])
7. 改变张量的形状
7.1 交换维度
transpose 交换两个维度,permute 可以按任意顺序重排所有维度。
tensor = torch.randint(1, 9, (2, 3, 6))
print("原始形状:", tensor.shape) # (2, 3, 6)
# 交换第 1 维和第 2 维
transposed = tensor.transpose(1, 2)
print("transpose(1,2) 后形状:", transposed.shape) # (2, 6, 3)
# 重排所有维度:原始 (2,3,6) → (6,2,3)
permuted = tensor.permute(2, 0, 1)
print("permute(2,0,1) 后形状:", permuted.shape) # (6, 2, 3)
7.2 reshape 与 view 的区别
-
reshape:通用方法,总能成功。
-
view:要求张量在内存中是连续存储的,但共享内存,效率更高。如果内存不连续,会报错。
tensor = torch.randint(1, 9, (3, 5, 4))
reshape 可以自动推断维度(-1 表示自动计算)
reshaped = tensor.reshape(6, 10)
print("reshape(6,10) 形状:", reshaped.shape)reshaped_auto = tensor.reshape(3, -1)
print("reshape(3, -1) 形状:", reshaped_auto.shape) # (3, 20)view 要求内存连续
print("是否连续:", tensor.is_contiguous()) # True
viewed = tensor.view(-1, 10)
print("view(-1,10) 形状:", viewed.shape)经典坑:transpose 之后内存不再连续
tensor_t = tensor.T
print("转置后是否连续:", tensor_t.is_contiguous()) # False下面这行会报错:view 要求连续内存
tensor_t.view(-1)
解决方法:先调用 contiguous()
contiguous_t = tensor_t.contiguous()
print("contiguous 后可以 view:", contiguous_t.view(-1).shape)
为什么会有连续性问题?
PyTorch 的张量是"逻辑视图"与"物理存储"分离的设计。像 transpose 这样的操作只是改变了维度映射关系,并没有实际移动内存中的元素,因此张量变得"不连续"。view 需要元素在物理上是连续排列的,所以必须先调用 contiguous() 强制整理内存。
7.3 增加或删除维度
unsqueeze 在指定位置插入一个大小为 1 的新维度,squeeze 删除所有大小为 1 的维度。
tensor = torch.tensor([1, 2, 3, 4, 5])
print("原始形状:", tensor.shape) # (5,)
# 在位置 0 插入新维度
unsqueezed_0 = tensor.unsqueeze(dim=0)
print("unsqueeze(0) 形状:", unsqueezed_0.shape) # (1, 5)
# 在位置 1 插入新维度
unsqueezed_1 = tensor.unsqueeze(dim=1)
print("unsqueeze(1) 形状:", unsqueezed_1.shape) # (5, 1)
# 在最后插入(dim=-1)
unsqueezed_last = tensor.unsqueeze(dim=-1)
print("unsqueeze(-1) 形状:", unsqueezed_last.shape) # (5, 1)
# 删除所有大小为 1 的维度
squeezed = unsqueezed_0.squeeze()
print("squeeze 后形状:", squeezed.shape) # (5,)
8. 拼接与堆叠
-
cat:沿已有维度拼接,不增加总维度数。要求除拼接维度外,其他维度大小相同。
-
stack:沿新维度堆叠,会增加一个维度。要求所有张量形状完全相同。
cat 示例
a = torch.randint(1, 9, (2, 2, 5))
b = torch.randint(1, 9, (2, 1, 5))
catted = torch.cat([a, b], dim=1)
print("cat 后形状:", catted.shape) # (2, 3, 5)stack 示例
x = torch.randint(1, 9, (3, 1, 5))
y = torch.randint(1, 9, (3, 1, 5))
stacked = torch.stack([x, y], dim=2)
print("stack 后形状:", stacked.shape) # (3, 1, 2, 5)
9. 自动微分(Autograd)
9.1 什么是自动微分
PyTorch 最强大的功能之一就是自动计算梯度。你只需要把张量的 requires_grad 属性设为 True,PyTorch 就会记录所有基于这个张量的操作,构建一个计算图。然后调用 backward(),梯度就会自动计算出来,并保存在对应张量的 .grad 属性中。
下面是一个最简单的例子,模拟一个神经元:z = w * x + b,然后计算损失关于 w 和 b 的梯度。
import torch
# 输入 x 和目标值 y
x = torch.tensor(10.0)
y = torch.tensor(3.0)
# 初始化权重 w 和偏置 b,并告诉 PyTorch 需要追踪它们的梯度
w = torch.rand(1, 1, requires_grad=True)
b = torch.rand(1, 1, requires_grad=True)
# 前向传播:计算预测值
z = w * x + b
# 定义损失函数(均方误差)
loss_fn = torch.nn.MSELoss()
loss = loss_fn(z, y)
# 反向传播:自动计算 w 和 b 的梯度
loss.backward()
# 输出梯度
print("w 的梯度:\n", w.grad)
print("b 的梯度:\n", b.grad)
# 叶子节点:由用户直接创建,不是计算得到的
print("x 是叶子节点:", x.is_leaf) # True
print("w 是叶子节点:", w.is_leaf) # True
print("z 是叶子节点:", z.is_leaf) # False(由计算得到)
print("loss 是叶子节点:", loss.is_leaf) # False
关键点:
- requires_grad=True 开启梯度追踪。
- backward() 执行反向传播。
- .grad 存储计算好的梯度。
- 叶子节点(用户直接创建的张量)的梯度在反向传播后会保留;非叶子节点的梯度默认会被释放(可以设置 retain_grad=True 保留)。
9.2 动态图的优势
PyTorch 采用的是动态计算图:每次执行前向传播时,计算图都是实时构建的。这意味着你可以在循环、条件判断中动态改变计算图的结构,就像写普通 Python 代码一样灵活。这也是 PyTorch 在科研领域特别受欢迎的原因------调试方便,修改模型也极其简单。
9.3 隔离计算图:detach 的用法
有时候我们希望把某些计算从计算图中剥离出来(例如在对抗训练中固定生成器的参数),可以用 detach()。
x = torch.ones(2, 2, requires_grad=True)
y = x * x
# 分离 y,新变量 u 不再记录计算历史
u = y.detach()
z = u * x
# 反向传播时,梯度不会经过 u 传播到 x
z.sum().backward()
# 验证:x.grad 应该等于 u(被当作常数),而不是 3*x^2
print(x.grad == u) # 所有元素为 True
10. 实战:用 PyTorch 实现线性回归
现在我们把前面的知识串起来,完成一个完整的机器学习任务:训练一个线性回归模型,拟合 y = 2.5 * x + 5.2 + 噪声。
标准的训练流程包含四步:
-
准备数据
-
构建模型
-
定义损失函数和优化器
-
训练循环
import torch
import matplotlib.pyplot as plt
from torch import nn, optim
from torch.utils.data import TensorDataset, DataLoader---------- 1. 准备数据 ----------
生成 100 个样本,每个样本 1 个特征
X = torch.randn(100, 1) # 输入
w_true = torch.tensor([2.5]) # 真实权重
b_true = torch.tensor([5.2]) # 真实偏置
noise = torch.randn(100, 1) * 0.1 # 添加噪声,模拟真实数据
y = w_true * X + b_true + noise # 目标值将数据包装成 Dataset 和 DataLoader
dataset = TensorDataset(X, y)
dataloader = DataLoader(
dataset,
batch_size=10, # 每次取 10 个样本训练
shuffle=True # 每个 epoch 打乱顺序
)---------- 2. 构建模型 ----------
nn.Linear 就是一个线性层:y = weight * x + bias
model = nn.Linear(in_features=1, out_features=1)
---------- 3. 定义损失函数和优化器 ----------
loss_fn = nn.MSELoss() # 均方误差损失,适合回归
optimizer = optim.SGD(model.parameters(), lr=1e-3) # 随机梯度下降,学习率 0.001---------- 4. 训练循环 ----------
loss_list = [] # 记录每个 epoch 的平均损失,用于绘图
num_epochs = 1000
for epoch in range(num_epochs):
total_loss = 0
num_samples = 0for x_batch, y_batch in dataloader: # 前向传播:计算预测值 y_pred = model(x_batch) # 计算损失 loss = loss_fn(y_pred, y_batch) total_loss += loss.item() num_samples += len(y_batch) # 反向传播前必须清零梯度(否则梯度会累积) optimizer.zero_grad() # 反向传播:计算梯度 loss.backward() # 更新参数 optimizer.step() avg_loss = total_loss / num_samples loss_list.append(avg_loss)打印结果
print(f"训练得到的权重: {model.weight.item():.4f} (真实值: 2.5)")
print(f"训练得到的偏置: {model.bias.item():.4f} (真实值: 5.2)")绘制损失曲线
plt.plot(loss_list)
plt.xlabel("Epoch")
plt.ylabel("Loss")
plt.title("线性回归训练损失曲线")
plt.show()
运行这段代码,你会看到损失不断下降,最终学到的权重和偏置非常接近真实值(2.5 和 5.2)。
代码要点解释:
- DataLoader 负责批量读取数据,batch_size=10 意味着每次迭代只用 10 个样本更新一次参数,这比用全部样本更高效,也更容易跳出局部最优。
- optimizer.zero_grad() 是必须的,因为 PyTorch 默认会累积梯度,不清零的话梯度会越来越大。
- loss.backward() 计算梯度,optimizer.step() 用梯度更新参数。
- 每个 epoch 结束后记录平均损失,可以观察训练是否收敛。
11. 总结
通过这篇文章,你应该已经掌握了 PyTorch 的核心概念:
- 张量是 PyTorch 的基本数据单元,创建方式多种多样:从数据创建、指定形状、随机生成、固定值填充等等。
- 张量运算包括基本算术、哈达玛积、矩阵乘法,以及各种统计函数。特别要注意 view 和 reshape 的区别,以及内存连续性的问题。
- 自动微分让梯度计算变得极其简单,只需要设置 requires_grad=True 并调用 backward()。
- 完整的训练流程包含了数据加载、模型定义、损失函数、优化器以及训练循环,线性回归的完整代码就是一个标准的模板。
你可以在此基础上,把 nn.Linear 换成更复杂的网络(比如卷积层、循环层),把数据换成真实的图片或文本,就能解决各种各样的深度学习问题了。
建议你动手运行一遍文中的所有代码,并且尝试修改一些参数(比如学习率、batch size、模型结构),观察结果的变化。实践是掌握 PyTorch 最好的方式。