【时序数据预测】Pytorch时序数据预测落地案例

使用 PyTorch 完成一个序列分类任务:模拟用户在一段时间内(如 30 天)的使用行为,然后预测该用户是否会“流失”(churn)。这与之前仅仅预测数值(回归)或对简单文本序列进行分类的例子都不同,可帮助你进一步理解 LSTM 在多特征时间序列分类中的应用。


案例:基于用户行为时间序列的流失预测(Churn Detection)

一、案例背景与思路

  • 我们模拟1000 个用户,每个用户在 30 天 内的使用行为,包括:

    1. daily_usage_time:每天使用APP的时长(小时),
    2. daily_access_count:每天登录/访问的次数,
    3. daily_spend:每天在APP内的消费金额。
  • 以上三项指标形成一个 (seq_len=30, feature_dim=3) 的时间序列,我们希望根据其整体走势来预测该用户在 30 天后是否“流失”(churn = 1)或“继续活跃”(churn = 0)。

  • 模型结构:Many-to-One,即序列输入,单标签输出(流失/不流失)。

  • 为了让模型有一定可学习的模式,我们在数据模拟时,假设:

    • 如果用户在后半程(第15~30天)的使用时长 & 消费金额 普遍较低,则更可能流失(label=1)。
    • 反之,如果后半程使用行为还不错,则倾向于不流失(label=0)。

这是一个简单的“人为设定”模拟逻辑,真实情况下需要你对业务场景有深入了解后再决定如何标注“流失”。


二、环境需求

  • Python 3.7+
  • PyTorch、Numpy、Matplotlib 等

在本地或 Google Colab 环境均可运行。


三、数据准备

3.1 数据生成

  1. 样本数量:1000(用户数)
  2. 时间序列长度:30
  3. 特征维度:3

下面的函数 generate_user_data 会随机生成用户每天的三项行为数据,并基于设定的条件来标注是否流失。

import numpy as np
import torch
import matplotlib.pyplot as plt
import random

def generate_user_data(num_users=1000, seq_len=30, seed=42):
    """
    生成用户在 seq_len 天内的使用行为数据:
      - daily_usage_time (随机 0~5小时)
      - daily_access_count (随机 0~10次)
      - daily_spend (随机 0~50元)
    根据后半段使用情况标注是否流失(1)或留存(0)。
    """
    np.random.seed(seed)
    random.seed(seed)

    X = []  # shape: (num_users, seq_len, feature_dim=3)
    Y = []  # shape: (num_users,)

    for _ in range(num_users):
        # 生成随机的时长、访问次数和消费金额
        usage_time = np.random.uniform(0, 5, seq_len)      # [0, 5) 小时
        access_count = np.random.randint(0, 10, seq_len)   # [0, 10) 次
        spend = np.random.uniform(0, 50, seq_len)          # [0, 50) 元
        
        features = np.stack([usage_time, access_count, spend], axis=-1)  # (seq_len, 3)

        # 简单逻辑:如果后半程(第15-30天)的 usage_time & spend 过低,则流失,否则留存
        mean_usage_later = usage_time[15:].mean()
        mean_spend_later = spend[15:].mean()
        
        # 设定一个经验阈值,比如时长 < 2小时 & 消费 < 10元 就可能流失
        if mean_usage_later < 2 and mean_spend_later < 10:
            label = 1  # 流失
        else:
            label = 0  # 留存
            
        X.append(features)
        Y.append(label)
    
    X = np.array(X)
    Y = np.array(Y)
    return X, Y

# 生成数据
X_all, Y_all = generate_user_data(num_users=1000, seq_len=30)
print("X_all shape:", X_all.shape)  # (1000, 30, 3)
print("Y_all shape:", Y_all.shape)  # (1000,)

3.2 训练集与测试集划分

# 8:2 划分
train_ratio = 0.8
train_size = int(len(X_all) * train_ratio)

train_X = X_all[:train_size]
train_Y = Y_all[:train_size]
test_X  = X_all[train_size:]
test_Y  = Y_all[train_size:]

print("Train X:", train_X.shape, "Train Y:", train_Y.shape)
print("Test  X:", test_X.shape,  "Test  Y:", test_Y.shape)

3.3 转成 PyTorch Dataset

我们将数据封装到 Dataset 里,以便后续使用 DataLoader 进行批量训练。

from torch.utils.data import Dataset, DataLoader

class UserBehaviorDataset(Dataset):
    def __init__(self, X, Y):
        super().__init__()
        self.X = X
        self.Y = Y
    
    def __len__(self):
        return len(self.X)
    
    def __getitem__(self, idx):
        x_seq = torch.tensor(self.X[idx], dtype=torch.float32)  # shape: (30, 3)
        y_label = torch.tensor(self.Y[idx], dtype=torch.long)   # 单个标签(0/1),分类任务用 long
        return x_seq, y_label

train_dataset = UserBehaviorDataset(train_X, train_Y)
test_dataset  = UserBehaviorDataset(test_X, test_Y)

train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)
test_loader  = DataLoader(test_dataset,  batch_size=32, shuffle=False)

四、模型构建:LSTM 分类

相比前面预测数值的回归任务,这里是二分类(流失 / 不流失),所以我们的输出层会有 2 个神经元,并使用交叉熵损失(CrossEntropy)。

import torch
import torch.nn as nn

class LSTMChurnClassifier(nn.Module):
    def __init__(self, input_dim=3, hidden_size=32, num_layers=1, num_classes=2):
        super(LSTMChurnClassifier, self).__init__()
        
        # LSTM 层
        self.lstm = nn.LSTM(
            input_size=input_dim, 
            hidden_size=hidden_size,
            num_layers=num_layers,
            batch_first=True  # (batch, seq_len, feature_dim)
        )
        
        # 输出层:将最后的隐藏状态映射到 2 个类别
        self.fc = nn.Linear(hidden_size, num_classes)

    def forward(self, x):
        # x: (batch, seq_len=30, input_dim=3)
        out, (h_n, c_n) = self.lstm(x)
        # 取最后时间步的隐藏状态
        last_out = out[:, -1, :]  # (batch, hidden_size)
        # 全连接分类
        logits = self.fc(last_out)  # (batch, num_classes=2)
        return logits

五、训练模型

5.1 实例化并定义优化器

model = LSTMChurnClassifier(input_dim=3, hidden_size=32, num_layers=1, num_classes=2)

criterion = nn.CrossEntropyLoss()  # 二分类可用
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)

5.2 训练循环

  • 损失函数CrossEntropyLoss
  • 优化器:Adam
  • 评价指标:本示例在训练过程中主要观察 loss,后面会在测试集上计算准确率。
num_epochs = 10
train_losses = []

for epoch in range(num_epochs):
    model.train()
    epoch_loss = 0
    for batch_x, batch_y in train_loader:
        # 前向计算
        logits = model(batch_x)  # (batch, 2)
        loss = criterion(logits, batch_y)  # batch_y shape=(batch,)
        
        # 反向传播
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        
        epoch_loss += loss.item()
    
    avg_loss = epoch_loss / len(train_loader)
    train_losses.append(avg_loss)
    if (epoch+1) % 2 == 0:
        print(f"Epoch [{epoch+1}/{num_epochs}], Loss: {avg_loss:.4f}")

5.3 训练曲线可视化

plt.figure(figsize=(8,4))
plt.plot(train_losses, label='Train Loss')
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.title('Training Curve')
plt.legend()
plt.show()

六、模型评估与预测

6.1 在测试集上计算准确率

model.eval()
correct = 0
total = 0
with torch.no_grad():
    for batch_x, batch_y in test_loader:
        logits = model(batch_x)
        preds = torch.argmax(logits, dim=1)  # (batch,)
        correct += (preds == batch_y).sum().item()
        total   += batch_y.size(0)

acc = correct / total
print(f"Test Accuracy: {acc*100:.2f}%")

由于模拟数据中我们人为设定了后半程使用行为与流失的关联关系,如果训练和模型结构合适,我们应该能得到**显著高于随机水平(50%)**的准确率。

6.2 预测示例

我们可以随机抽取一些测试样本做预测,查看模型输出的类别。

import random

model.eval()
sample_indices = random.sample(range(len(test_dataset)), 5)
print("Random 5 test users:")

for idx in sample_indices:
    x_seq, true_label = test_dataset[idx]
    with torch.no_grad():
        logits = model(x_seq.unsqueeze(0))  # 增加batch维度
        pred_label = torch.argmax(logits, dim=1).item()
    
    print(f" - User idx {idx}: True={true_label}, Predict={pred_label}")

七、改进与扩展思路

  1. 多层 LSTM / 加入 Dropout
    • 如果数据更复杂,可将 num_layers 设大于 1,并在 LSTM 层中使用 dropout 参数,防止过拟合。
  2. 增加更多特征
    • 现实中可以采集更多维度(如点击次数、互动行为、用户属性等)来提升模型判别能力。
  3. 平衡样本
    • 若实际数据中“流失”与“未流失”分布严重不平衡,可考虑加权损失或其他方式处理不均衡问题。
  4. 更复杂的判定方式
    • 本示例只根据“后半程平均值”来生成标签,实际业务中可结合更多条件或打分机制来确定流失定义。
  5. 其他模型
    • 如果序列很长,且对捕捉长距离依赖要求高,可考虑 GRUTransformerLSTM+Attention 等更高级结构。

八、小结

本案例展示了一个全新的时间序列分类场景:通过多维度用户行为序列,来预测用户流失与否。与之前的示例(如数值回归、文本分类或 sin 波预测)不同的是:

  • 我们在数据模拟阶段加入了多特征(usage_time, access_count, spend),并基于后半程表现决定流失标签。
  • 整体流程仍遵循 PyTorch 典型套路:Dataset & DataLoader -> LSTM 模型 -> 训练循环 -> 评估
  • 输出维度为 2(二分类),使用 nn.CrossEntropyLoss 进行训练。

在实际业务中,你可以:

  • 采集更丰富的时间序列特征(如设备信息、用户画像),
  • 定义更合理的“流失”标签规则,
  • 使用更先进的门控或注意力模型来提升预测性能,
  • 或者考虑更复杂的序列长度(如 90 天以上)。

希望这个全新案例能让你对LSTM 在多特征时间序列分类领域的应用有更直观的理解和操作经验。祝学习顺利,项目成功!

哈佛博后带小白玩转机器学习】 哔哩哔哩_bilibili

总课时超400+,时长75+小时