3. PyTorch基础回顾:张量操作、Dataset与DataLoader、模型构建三要素、训练循环模板

各位同学,欢迎来到第三章。在正式进入Transformer之前,咱们得先把PyTorch这把刀磨快。说实话,我见过太多同学模型结构背得滚瓜烂熟,结果一写训练循环就报错,一调张量维度就懵圈。这一章,咱们就把这些基本功砸实了。

3.1 张量操作:时间序列的基石

张量,说白了就是PyTorch里的数组。但时间序列预测里,张量的维度操作特别容易翻车。我个人习惯把张量想象成一个「多层抽屉的柜子」——每个维度就是一层抽屉的编号。

3.1.1 创建与基本属性

import torch

# 创建一个3x4的张量
x = torch.randn(3, 4)
print(x.shape)  # torch.Size([3, 4])
print(x.ndim)   # 2

# 时间序列常用:创建连续序列
seq = torch.arange(0, 100, step=1)  # 0到99
print(seq.shape)  # torch.Size([100])

嗯,这里要注意:时间序列数据通常是三维的——(batch_size, seq_len, input_dim)。我在做电力负荷预测时,就吃过这个亏。一开始把数据搞成了(seq_len, batch_size, input_dim),结果模型训练了三天,精度一塌糊涂。后来才发现是维度顺序搞反了。

3.1.2 维度变换:最常用的三板斧

操作 函数 说明
重塑形状 view() / reshape() 不改变数据,只改变视图
增加维度 unsqueeze() 在指定位置插入一个维度
交换维度 transpose() / permute() 调换两个或多个维度的顺序
# 实战场景:把一维序列变成三维输入
seq = torch.randn(100)  # 原始序列
# 变成 (batch=1, seq_len=100, input_dim=1)
seq_3d = seq.unsqueeze(0).unsqueeze(-1)
print(seq_3d.shape)  # torch.Size([1, 100, 1])

# 交换维度:Transformer里经常用
x = torch.randn(2, 3, 4)  # (batch, seq, feature)
x_permuted = x.permute(0, 2, 1)  # (batch, feature, seq)
print(x_permuted.shape)  # torch.Size([2, 4, 3])
我曾经踩过的坑:view()和reshape()看起来一样,但view()要求张量在内存中是连续的。如果你刚做完transpose(),直接调view()会报错。我的习惯是:不确定是否连续时,一律用reshape()。

3.2 Dataset与DataLoader:数据管线的艺术

做时间序列预测,数据预处理占了60%的工作量。PyTorch的Dataset和DataLoader就是帮你把这60%的活干利索的。

3.2.1 自定义Dataset

我建议你养成一个习惯:所有时间序列数据都写成Dataset类。为什么?因为滑动窗口、归一化、缺失值处理,这些逻辑封装在一起,后期调试起来特别爽。

from torch.utils.data import Dataset, DataLoader

class TimeSeriesDataset(Dataset):
    def __init__(self, data, seq_len=24, pred_len=1):
        self.data = torch.FloatTensor(data)
        self.seq_len = seq_len
        self.pred_len = pred_len
    
    def __len__(self):
        # 总共有多少个样本
        return len(self.data) - self.seq_len - self.pred_len + 1
    
    def __getitem__(self, idx):
        # 返回 (输入序列, 目标序列)
        x = self.data[idx:idx + self.seq_len]
        y = self.data[idx + self.seq_len:idx + self.seq_len + self.pred_len]
        return x, y

# 使用示例
raw_data = [i for i in range(1000)]
dataset = TimeSeriesDataset(raw_data, seq_len=24, pred_len=1)
print(len(dataset))  # 976个样本

3.2.2 DataLoader的实用技巧

DataLoader有几个参数,我每次写代码都会仔细检查:

  • batch_size:别贪大,显存不够就调小。我一般从32开始试。
  • shuffle:训练集用True,验证集和测试集用False。时间序列里尤其要注意——打乱顺序会破坏时间依赖关系。
  • num_workers:数据加载的并行进程数。我习惯设成4或8,但Windows上设成0更稳。
dataloader = DataLoader(
    dataset, 
    batch_size=32, 
    shuffle=True, 
    num_workers=4,
    drop_last=True  # 丢掉最后一个不完整的batch
)

for batch_x, batch_y in dataloader:
    print(batch_x.shape)  # torch.Size([32, 24, 1])
    print(batch_y.shape)  # torch.Size([32, 1, 1])
    break
小技巧:drop_last=True这个参数,很多人会忽略。如果你的数据量不能被batch_size整除,最后一个batch会很小,导致梯度更新不稳定。我一般都会加上。

3.3 模型构建三要素

构建一个PyTorch模型,说白了就三件事:继承nn.Module、定义层、实现forward。我见过有人把这三件事搞混,结果模型跑起来各种报错。

3.3.1 三要素详解

import torch.nn as nn

class SimpleTransformer(nn.Module):
    # 要素一:继承nn.Module
    def __init__(self, input_dim, d_model, nhead):
        # 要素二:定义层
        super().__init__()
        self.embedding = nn.Linear(input_dim, d_model)
        self.encoder_layer = nn.TransformerEncoderLayer(
            d_model=d_model, 
            nhead=nhead,
            batch_first=True  # 这个参数很重要!
        )
        self.output = nn.Linear(d_model, 1)
    
    # 要素三:实现forward
    def forward(self, x):
        # x shape: (batch, seq_len, input_dim)
        x = self.embedding(x)
        x = self.encoder_layer(x)
        x = self.output(x[:, -1, :])  # 取最后一个时间步
        return x

重点:batch_first=True这个参数,我建议你每次都加上。PyTorch的Transformer默认是(seq_len, batch, feature)的格式,但咱们做时间序列预测,习惯用(batch, seq_len, feature)。加上这个参数,就不用每次手动permute了。

3.3.2 参数初始化

模型初始化这事儿,很多人觉得无所谓。但我告诉你,初始化不好,模型可能根本不收敛。我一般会这样写:

def init_weights(m):
    if isinstance(m, nn.Linear):
        nn.init.xavier_uniform_(m.weight)
        nn.init.zeros_(m.bias)
    elif isinstance(m, nn.LayerNorm):
        nn.init.ones_(m.weight)
        nn.init.zeros_(m.bias)

model = SimpleTransformer(input_dim=1, d_model=64, nhead=4)
model.apply(init_weights)

3.4 训练循环模板

训练循环,说白了就是「前向传播 → 算损失 → 反向传播 → 更新参数」这四个步骤的循环。我写过一个通用模板,用了好几年,改改就能用。

3.4.1 标准训练模板

def train_one_epoch(model, dataloader, optimizer, criterion, device):
    model.train()
    total_loss = 0
    
    for batch_x, batch_y in dataloader:
        batch_x, batch_y = batch_x.to(device), batch_y.to(device)
        
        # 1. 前向传播
        pred = model(batch_x)
        
        # 2. 算损失
        loss = criterion(pred, batch_y)
        
        # 3. 反向传播
        optimizer.zero_grad()
        loss.backward()
        
        # 4. 更新参数
        optimizer.step()
        
        total_loss += loss.item()
    
    return total_loss / len(dataloader)

def validate(model, dataloader, criterion, device):
    model.eval()
    total_loss = 0
    
    with torch.no_grad():
        for batch_x, batch_y in dataloader:
            batch_x, batch_y = batch_x.to(device), batch_y.to(device)
            pred = model(batch_x)
            loss = criterion(pred, batch_y)
            total_loss += loss.item()
    
    return total_loss / len(dataloader)

3.4.2 完整训练流程

# 配置
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = SimpleTransformer(input_dim=1, d_model=64, nhead=4).to(device)
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
criterion = nn.MSELoss()

# 训练
num_epochs = 100
best_loss = float('inf')

for epoch in range(num_epochs):
    train_loss = train_one_epoch(model, train_loader, optimizer, criterion, device)
    val_loss = validate(model, val_loader, criterion, device)
    
    # 保存最佳模型
    if val_loss < best_loss:
        best_loss = val_loss
        torch.save(model.state_dict(), 'best_model.pth')
    
    if (epoch + 1) % 10 == 0:
        print(f'Epoch {epoch+1:3d} | Train Loss: {train_loss:.4f} | Val Loss: {val_loss:.4f}')
我曾经踩过的坑:训练时忘记调model.train(),验证时忘记调model.eval()。Dropout和BatchNorm在训练和验证时的行为不一样,不切换的话,验证结果会非常诡异。我因为这个bug排查了整整两天。

3.5 本章知识体系

下面这张图,是我自己总结的PyTorch时间序列预测的核心流程。你把它存下来,写代码时对照着看,能少走很多弯路。

PyTorch时间序列预测核心流程 数据准备 Dataset + DataLoader 模型构建 继承·定义层·forward 训练循环 前向→损失→反向→更新 核心知识点 张量操作 view / reshape / unsqueeze transpose / permute 模型三要素 nn.Module继承 __init__ + forward 训练模板 train/val模式切换 梯度清零·反向传播 避坑总结 维度顺序别搞反 · 记得切换train/eval · 初始化不能省 · batch_first=True

嗯,这一章的内容就到这里。这些基础操作,你写代码时肯定会反复用到。别嫌烦,多练几遍,形成肌肉记忆就好了。


公众号:蓝海资料掘金营,微信deep3321