我来为你撰写第2讲的完整教程。这篇教程将覆盖张量的核心操作,并以手写线性回归作为实战收尾。


第2讲:张量操作与线性回归实战


目录

  1. 张量的创建
  2. 索引与切片
  3. 张量的变形
  4. 张量的拼接与拆分
  5. 广播机制:自动对齐的魔法
  6. 实战:手写线性回归
  7. 小结

1. 张量的创建

1.1 从数据创建

import torch

# 从列表创建
x = torch.tensor([1.0, 2.0, 3.0])
print(x)          # tensor([1., 2., 3.])
print(x.dtype)    # torch.float32(默认浮点类型)

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

# 创建多维张量(矩阵)
matrix = torch.tensor([
    [1.0, 2.0, 3.0],
    [4.0, 5.0, 6.0]
])
print(matrix.shape)  # torch.Size([2, 3]) — 2行3列

1.2 创建特殊张量

# 全0张量 — 常用于初始化偏置
zeros = torch.zeros(2, 3)
# tensor([[0., 0., 0.],
#         [0., 0., 0.]])

# 全1张量 — 常用于掩码
ones = torch.ones(2, 3)

# 单位矩阵 — 线性代数常用
eye = torch.eye(3)
# tensor([[1., 0., 0.],
#         [0., 1., 0.],
#         [0., 0., 1.]])

# 随机张量 — 权重初始化最常用
rand = torch.rand(2, 3)       # 均匀分布 [0, 1)
randn = torch.randn(2, 3)     # 标准正态分布 N(0,1)

# 等差数列 — 生成坐标轴或时间序列
linspace = torch.linspace(0, 10, 5)  # 从0到10,均匀取5个点
# tensor([ 0.0000,  2.5000,  5.0000,  7.5000, 10.0000])

# 等比数列
logspace = torch.logspace(0, 2, 3)   # 10^0, 10^1, 10^2
# tensor([  1.,  10., 100.])

1.3 创建与现有张量同类型的张量

x = torch.tensor([[1.0, 2.0], [3.0, 4.0]])

# 同形状的全0张量
zeros_like = torch.zeros_like(x)
# tensor([[0., 0.],
#         [0., 0.]])

# 同形状的全1张量
ones_like = torch.ones_like(x)

# 同形状的随机张量
rand_like = torch.rand_like(x)

💡 记忆口诀torch.zeros + _like = 照着别人的样子来


2. 索引与切片

2.1 基础索引

x = torch.tensor([
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9]
])

# 取单个元素 — 类似Python列表
print(x[0, 1])    # tensor(2) — 第0行第1列

# 取整行
print(x[0])       # tensor([1, 2, 3])

# 取整列
print(x[:, 0])    # tensor([1, 4, 7]) — 所有行的第0列

2.2 切片操作

# 切片语法:[start:end:step]
# 取第0-1行,第1-2列
print(x[0:2, 1:3])
# tensor([[2, 3],
#         [5, 6]])

# 省略start/end表示从头/到尾
print(x[:, :2])   # 所有行,前2列
# tensor([[1, 2],
#         [4, 5],
#         [7, 8]])

# 步长为2(隔一个取一个)
print(x[::2])     # 第0行和第2行
# tensor([[1, 2, 3],
#         [7, 8, 9]])

2.3 高级索引

# 布尔索引 — 筛选满足条件的元素
mask = x > 5
print(mask)
# tensor([[False, False, False],
#         [False, False,  True],
#         [ True,  True,  True]])

print(x[mask])    # tensor([6, 7, 8, 9]) — 只保留True位置的元素

# 整数数组索引 — 花式索引
rows = torch.tensor([0, 2])
cols = torch.tensor([1, 2])
print(x[rows, cols])  # tensor([2, 9]) — 取(0,1)和(2,2)

# 组合索引
print(x[[0, 2], :])   # 取第0行和第2行,所有列
# tensor([[1, 2, 3],
#         [7, 8, 9]])

⚠️ 注意:索引出来的结果与原始张量共享内存,修改会影响原张量

y = x[0, :]      # y是x第0行的视图
y[0] = 999       # 修改y
print(x[0, 0])   # 999 — x也被改了!

如果需要复制一份独立的数据,用 .clone()

y = x[0, :].clone()  # 深拷贝,修改y不影响x

3. 张量的变形

3.1 view vs reshape

x = torch.randn(2, 3, 4)  # 形状 [2, 3, 4],共24个元素
print(x.shape)  # torch.Size([2, 3, 4])

# view:改变形状,不改变数据,要求内存连续
y = x.view(2, 12)         # [2, 3*4] = [2, 12]
z = x.view(-1, 4)         # -1表示自动计算,24/4=6,所以是[6, 4]
w = x.view(24)            # 拉平为一维

# reshape:更灵活,必要时自动复制数据保证连续性
y2 = x.reshape(2, 12)

💡 什么时候用view,什么时候用reshape?

  • 确定内存连续时 → view(更快,不复制数据)
  • 不确定或可能不连续时 → reshape(更安全,会自动处理)

3.2 升维与降维

x = torch.tensor([1, 2, 3])  # 形状 [3]

# 升维:unsqueeze 在指定位置插入一个维度
x1 = x.unsqueeze(0)   # 在第0维前插入 → [1, 3](行向量)
x2 = x.unsqueeze(1)   # 在第1维前插入 → [3, 1](列向量)

print(x1.shape)  # torch.Size([1, 3])
print(x2.shape)  # torch.Size([3, 1])

# 降维:squeeze 删除长度为1的维度
x3 = x1.squeeze(0)    # 删除第0维 → [3]
print(x3.shape)       # torch.Size([3])

# 自动删除所有长度为1的维度
x4 = torch.randn(1, 3, 1, 4, 1)
x5 = x4.squeeze()     # → [3, 4]

3.3 转置与维度交换

x = torch.randn(2, 3, 4)

# 转置:交换最后两个维度
y = x.transpose(1, 2)   # [2, 4, 3]

# 交换任意两个维度
z = x.transpose(0, 2)   # [4, 3, 2]

# permute:任意重排所有维度
w = x.permute(2, 0, 1)   # [4, 2, 3]

# 矩阵转置(二维专用)
matrix = torch.randn(3, 4)
matrix_t = matrix.t()    # [4, 3]

🎯 实战场景:图像数据格式转换

  • PyTorch默认:[N, C, H, W](Batch, Channel, Height, Width)
  • OpenCV读取:[H, W, C] — 需要permute转换
# 图像格式转换示例
image_hwc = torch.randn(480, 640, 3)   # [H, W, C]
image_chw = image_hwc.permute(2, 0, 1)  # [C, H, W]
batch_image = image_chw.unsqueeze(0)    # [1, C, H, W]

4. 张量的拼接与拆分

4.1 拼接:cat vs stack

# cat:沿现有维度拼接(维度数不变)
a = torch.randn(2, 3)
b = torch.randn(2, 3)

c = torch.cat([a, b], dim=0)   # 沿第0维拼接 → [4, 3]
d = torch.cat([a, b], dim=1)   # 沿第1维拼接 → [2, 6]

# stack:新增一个维度后拼接(维度数+1)
e = torch.stack([a, b], dim=0)  # 新增第0维 → [2, 2, 3]
# 相当于:先把a,b都unsqueeze(0)成[1,2,3],再cat

💡 区别记忆cat是"并排贴",维度不变;stack是"叠罗汉",多一层

4.2 拆分

x = torch.randn(6, 4)

# chunk:均匀分成几块
chunks = torch.chunk(x, 3, dim=0)  # 沿第0维分成3块,每块[2, 4]
# 返回元组:(tensor[2,4], tensor[2,4], tensor[2,4])

# split:按指定大小拆分
splits = torch.split(x, 2, dim=0)  # 每块大小为2 → 3块,每块[2, 4]

5. 广播机制:自动对齐的魔法

5.1 什么是广播?

广播(Broadcasting) 是PyTorch(以及NumPy)中自动扩展张量形状的机制,让不同形状的张量能进行运算。

通俗比喻:就像Excel中,一列数据可以和单个数字做运算,Excel会自动把数字"复制"成一列。广播机制就是这个原理,但更高效——不真正复制数据,只是虚拟扩展

5.2 广播规则

两个张量要能广播,必须满足以下规则(从最后一个维度开始往前比较):

  1. 维度相等,或
  2. 其中一个维度为1,或
  3. 其中一个张量没有该维度(视为1)
# 示例1:标量广播到矩阵
a = torch.ones(3, 4)
b = 2.0
print(a + b)   # 每个元素都加2,结果 [3, 4]

# 示例2:向量广播到矩阵
a = torch.ones(3, 4)      # [3, 4]
b = torch.arange(4)       # [4] — 自动视为 [1, 4],然后复制成 [3, 4]
print(a + b)
# tensor([[1., 2., 3., 4.],
#         [1., 2., 3., 4.],
#         [1., 2., 3., 4.]])

# 示例3:列向量广播到矩阵
a = torch.ones(3, 4)      # [3, 4]
c = torch.arange(3).unsqueeze(1)  # [3, 1] — 复制成 [3, 4]
print(a + c)
# tensor([[1., 1., 1., 1.],
#         [2., 2., 2., 2.],
#         [3., 3., 3., 3.]])

5.3 广播的形状计算

# 手动验证广播后的形状
a = torch.randn(2, 1, 3, 1)   # 形状
b = torch.randn(   4, 1, 5)   # 形状(前面补1变成 [1, 4, 1, 5])

# 逐维比较:
# 维度3: 1 vs 5 → 取5
# 维度2: 3 vs 1 → 取3
# 维度1: 1 vs 4 → 取4
# 维度0: 2 vs 1 → 取2
# 结果形状:[2, 4, 3, 5]

result = a + b
print(result.shape)  # torch.Size([2, 4, 3, 5])

5.4 广播的应用场景

1.BatchNorm中的均值计算和广播机制

2.通道级权重应用和广播机制

3.样本到中心点的距离计算和广播机制

# 场景1:批量归一化 — 减去均值
data = torch.randn(100, 3, 32, 32)   # [N, C, H, W]
mean = data.mean(dim=[0, 2, 3], keepdim=True)  # [1, 3, 1, 1]
normalized = data - mean   # 广播:[100,3,32,32] - [1,3,1,1] → [100,3,32,32]

# 场景2:权重应用 — 每通道乘不同系数
weights = torch.tensor([0.5, 1.0, 0.8]).view(1, 3, 1, 1)  # [1, 3, 1, 1]
weighted = data * weights   # 广播到 [100, 3, 32, 32]

# 场景3:距离计算 — 计算每个样本到中心的距离
samples = torch.randn(100, 10)   # 100个样本,10维
center = torch.randn(1, 10)      # 中心点
distances = torch.norm(samples - center, dim=1)  # [100]

⚠️ 常见错误:形状不匹配

a = torch.randn(3, 4)
b = torch.randn(5)  # 错误!3≠5且 neither is 1
# a + b  # RuntimeError: 无法广播

什么是分类任务和回归任务


从0开始掌握FCNN-全连接神经网络


6. 实战:手写线性回归

6.1 问题定义

线性回归模型: y = w x + b y = wx + b y=wx+b

目标:给定数据 ( x i , y i ) (x_i, y_i) (xi,yi),找到最优的 w w w b b b,使得预测值 y ^ i \hat{y}_i y^i 与真实值 y i y_i yi 的误差最小。

6.2 完整代码

import torch
import torch.nn as nn
import matplotlib.pyplot as plt

# ========== 1. 准备数据 ==========
torch.manual_seed(42)  # 保证可复现

# 真实参数
true_w = 2.0
true_b = 1.0

# 生成训练数据:100个样本
n_samples = 100
x = torch.linspace(0, 10, n_samples)          # [100]
noise = torch.randn(n_samples) * 2              # 噪声,标准差为2
y = true_w * x + true_b + noise                 # y = 2x + 1 + 噪声

# 可视化数据
plt.scatter(x.numpy(), y.numpy(), alpha=0.5, label='Data')
plt.plot(x.numpy(), true_w * x + true_b, 'r-', label='True Line')
plt.legend()
plt.title('Training Data')
plt.show()

# ========== 2. 定义模型 ==========
class LinearRegression(nn.Module):
    def __init__(self):
        super().__init__()
        # 初始化参数:w和b
        self.weight = nn.Parameter(torch.randn(1))   # 可学习参数
        self.bias = nn.Parameter(torch.zeros(1))     # 可学习参数
    
    def forward(self, x):
        return self.weight * x + self.bias

model = LinearRegression()
print(f"初始参数: w={model.weight.item():.3f}, b={model.bias.item():.3f}")

# ========== 3. 定义损失函数和优化器 ==========
criterion = nn.MSELoss()                          # 均方误差损失
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)  # 随机梯度下降

# ========== 4. 训练循环 ==========
n_epochs = 1000
losses = []

for epoch in range(n_epochs):
    # 前向传播
    y_pred = model(x)          # [100]
    
    # 计算损失
    loss = criterion(y_pred, y)
    losses.append(loss.item())
    
    # 反向传播
    optimizer.zero_grad()      # 清空旧梯度(重要!)
    loss.backward()            # 计算梯度(Autograd自动完成)
    
    # 参数更新
    optimizer.step()           # 按梯度更新w和b
    
    # 打印进度
    if (epoch + 1) % 100 == 0:
        print(f"Epoch [{epoch+1}/{n_epochs}], Loss: {loss.item():.4f}, "
              f"w={model.weight.item():.3f}, b={model.bias.item():.3f}")

# ========== 5. 结果可视化 ==========
print(f"\n训练完成!真实参数: w={true_w}, b={true_b}")
print(f"估计参数: w={model.weight.item():.3f}, b={model.bias.item():.3f}")

# 绘制拟合结果
plt.figure(figsize=(12, 4))

plt.subplot(1, 2, 1)
plt.scatter(x.numpy(), y.numpy(), alpha=0.5, label='Data')
plt.plot(x.numpy(), (true_w * x + true_b).numpy(), 'r-', label='True Line')
plt.plot(x.numpy(), model(x).detach().numpy(), 'g--', label='Fitted Line')
plt.legend()
plt.title('Linear Regression Result')

plt.subplot(1, 2, 2)
plt.plot(losses)
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.title('Training Loss Curve')
plt.yscale('log')

plt.tight_layout()
plt.show()

6.3 代码关键点解析

步骤 代码 说明
参数定义 nn.Parameter(torch.randn(1)) 将张量标记为可学习参数,自动加入model.parameters()
前向传播 model(x) 调用forward(),计算预测值
损失计算 criterion(y_pred, y) MSE = 1 n ∑ ( y p r e d − y ) 2 \frac{1}{n}\sum(y_{pred} - y)^2 n1(ypredy)2
梯度清零 optimizer.zero_grad() 必须! 否则梯度会累加
反向传播 loss.backward() Autograd自动计算所有参数的梯度
参数更新 optimizer.step() 按学习率沿梯度反方向更新参数

6.4 使用PyTorch内置层简化

# 更简洁的写法:使用nn.Linear
class SimpleLinearRegression(nn.Module):
    def __init__(self):
        super().__init__()
        self.linear = nn.Linear(in_features=1, out_features=1)  # 输入1维,输出1维
    
    def forward(self, x):
        # x形状需要是 [N, 1],所以reshape
        return self.linear(x.unsqueeze(1)).squeeze()

# 或者更直接
model = nn.Linear(1, 1)  # 一行搞定!

# 注意:此时x需要是 [N, 1] 形状
x = x.unsqueeze(1)       # [100] → [100, 1]
y = y.unsqueeze(1)       # [100] → [100, 1]

# 训练代码相同...

6.5 训练过程动画理解

初始状态: w=随机, b=0    →  直线随机位置
Epoch 1-100:             →  直线快速接近数据
Epoch 100-500:           →  微调斜率和截距
Epoch 500-1000:          →  精细收敛,损失缓慢下降

最终: w≈2.0, b≈1.0      →  直线与真实直线重合

7. 小结

知识点 核心要点
创建张量 tensor/zeros/ones/rand/randn/linspace*_like系列
索引切片 与NumPy一致,注意clone()避免共享内存问题
变形操作 view/reshape换形状,unsqueeze/squeeze增减维度,transpose/permute换维度顺序
拼接拆分 cat沿现有维度拼,stack新增维度拼;chunk/split拆分
广播机制 自动对齐形状,逐维比较(相等或其一为1),不复制数据
线性回归 nn.Parameter定义参数 → forward预测 → MSELoss算误差 → backward算梯度 → optimizer.step更新

课后练习

  1. 广播练习:创建形状为 [3, 1, 4][1, 5, 1] 的两个张量,手动推导广播后的形状,再用代码验证。

  2. 索引练习:用布尔索引从随机矩阵中提取所有大于0.5的元素,并统计个数。

  3. 线性回归扩展:将单变量线性回归扩展为多变量( y = w 1 x 1 + w 2 x 2 + b y = w_1x_1 + w_2x_2 + b y=w1x1+w2x2+b),使用 nn.Linear(2, 1) 实现。

  4. 挑战:不借助 nn.Linear,只用张量操作实现一个批量线性回归(输入 [batch_size, features])。


下讲预告:第3讲《自动微分:PyTorch的灵魂——Autograd》,我们将深入计算图,理解PyTorch"边搭积木边画图纸"的动态图机制,并手写逻辑回归!

Logo

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

更多推荐