ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

PyTorch LSTM城市人口预测实战:滑窗构造、调参与避坑指南

PyTorch LSTM城市人口预测实战:滑窗构造、调参与避坑指南 简介这份资源面向本科及以上学历、具备一定MATLAB基础的学习者与研究人员提供一套基于长短期记忆神经网络LSTM的城市人口预测完整实现方案。城市人口属于典型时间序列数据相比普通神经网络LSTM在捕捉长期依赖与趋势变化上更具优势因此该方案适合用于人口规模走势建模、区域规划预测等场景也可作为时间序列预测的入门与扩展案例。压缩包共38个文件约553KB其中4个.m脚本承担主程序与误差评估功能1个.xlsx存放人口原始数据另有32张jpg与1张png图片用于结果可视化展示整体结构紧凑、便于查阅。资源内代码完整、注释清晰读者可据此复现训练与预测流程并借助MSE、RMSE、MBE、MAE及R²等指标评估模型效果同时方便替换数据或调整网络结构以适配其他时序预测任务。目前已有525人学习下载适合需要快速上手LSTM人口预测实践的学习者参考使用。1. 城市人口预测为什么总在“拐点年”翻车LSTM 能补上哪块短板做城市人口预测的人大多经历过这种尴尬线性回归在平稳年份拟合得漂漂亮亮一到生育政策调整、产业搬迁、轨道交通开通这类拐点年误差直接翻倍。原因不复杂——人口序列同时受长期趋势老龄化、城镇化率和短期扰动落户政策、疫情、大型项目用工双重驱动传统 ARIMA 把两者压成一个线性项拐点处必然失真。长短期神经网络LSTM的价值就在这里它用遗忘门、输入门、输出门三套机制分别决定“旧趋势记多久”“新扰动写多深”“当前输出露多少”天然适配人口这种长记忆叠加短冲击的序列。这篇笔记面向手里有十年以上常住人口或户籍人口数据、想用 PyTorch 把 LSTM 预测跑通并落地到年度规划里的从业者从数据构造讲到调参和避坑代码和数据组织方式都能直接抄。2. 把人口序列改造成 LSTM 能吃的监督样本滑窗、归一化与特征拼接2.1 为什么人口预测不能直接把年份丢进 LSTMLSTM 的输入张量形状是(batch, seq_len, input_size)它要求每个样本是一段连续时间步而不是单个年份。很多人第一次写lstm 时间序列预测 python代码时直接把[2010, 2011, ...]当特征喂进去结果模型学到的只是年份递增预测值永远贴着趋势线走短期波动全丢。正确做法是滑窗用前seq_len年的人口值预测下一年。比如seq_len5就用 2015–2019 预测 2020用 2016–2020 预测 2021。这样每个样本的input_size至少是 1仅历史人口如果加入出生率、死亡率、迁入迁出、GDP 增速等协变量input_size就变成 1N。城市人口预测里我一般至少拼三个外生变量前一年出生率、前一年净迁入率、前一年第三产业占比这三个和人口拐点相关性最高且统计年鉴里稳定可得。2.2 数据清洗与归一化的三个硬约束人口数据最常见的脏法是口径突变某年行政区划调整常住人口突然跳变几十万或者统计年鉴某年缺失用户籍人口顶替。这类点不处理LSTM 会把跳变当成真实模式学进去。处理原则是区划调整年份按调整后口径回溯修正缺失年份用前后两年线性插值并打标记异常跳变超过 5% 的年份单独核查。归一化必须用训练集的最小最大值不能全序列一起归一化否则验证集信息泄漏评估结果虚高。下面这段代码把清洗、滑窗、归一化一次做完。import numpy as np import pandas as pd from sklearn.preprocessing import MinMaxScaler def build_sequences(df, seq_len5, target_colpopulation, exog_colsNone): df: 按年份升序排列的 DataFrame含 population 及外生变量列 seq_len: 用前多少年预测下一年 返回: X (n, seq_len, 1len(exog)), y (n,), scaler exog_cols exog_cols or [] feat_cols [target_col] exog_cols data df[feat_cols].values.astype(float32) # 只用训练段拟合 scaler这里假设前 80% 为训练段 train_len int(len(data) * 0.8) scaler MinMaxScaler() scaler.fit(data[:train_len]) data_scaled scaler.transform(data) X, y [], [] for i in range(len(data_scaled) - seq_len): X.append(data_scaled[i:i seq_len, :]) y.append(data_scaled[i seq_len, 0]) # 只预测人口这一列 return np.array(X), np.array(y), scaler逻辑说明scaler.fit只吃训练段避免验证集统计量污染y取的是iseq_len时刻的第 0 列即人口值外生变量只作为输入不参与预测目标。参数上seq_len是第一个要调的城市人口年度数据通常只有 20–30 个点seq_len取 3–5 比较稳取 8 以上样本数骤减模型直接过拟合。外生变量列建议不超过 4 个每多一列就多一组权重小样本下噪声放大明显。2.3 训练集/验证集切分时间序列不能随机打乱这是血泪经验里最常见的一条用train_test_split(shuffleTrue)切人口数据验证集精度能到 0.99上线预测下一年直接崩。时间序列必须按时间先后切且验证集要留出至少一个完整波动周期。我一般用 70% 训练、15% 验证、15% 测试测试集就是最近 3–5 年模拟真实预测场景。如果数据只有 20 年测试集留 3 年验证集留 3 年剩下 14 年训练。切分后不要再做任何跨集的归一化或插值。3. PyTorch 搭 LSTM 预测模型层数、隐藏单元与 Dropout 怎么定3.1 模型结构单层还是双层隐藏单元给多少lstm模型代码网上很多但直接抄来的结构往往隐藏单元给 128、层数给 2放到城市人口这种小样本上就是灾难。我的经验是输入特征 4 个以内、样本量 30 以内隐藏单元 16–32、层数 1 层足够样本量上百比如区县级面板数据再考虑 2 层、隐藏单元 64。Dropout 只在层间加dropout0.2起步加到 0.5 会欠拟合。下面是一个可直接用的模型定义输出层只预测一个值。import torch import torch.nn as nn class PopulationLSTM(nn.Module): def __init__(self, input_size, hidden_size32, num_layers1, dropout0.2): super().__init__() self.lstm nn.LSTM( input_sizeinput_size, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, dropoutdropout if num_layers 1 else 0.0 ) self.fc nn.Linear(hidden_size, 1) def forward(self, x): # x: (batch, seq_len, input_size) out, (h_n, c_n) self.lstm(x) last_step out[:, -1, :] # 取最后一个时间步的隐藏状态 return self.fc(last_step).squeeze(-1)逻辑说明batch_firstTrue让输入维度是(batch, seq_len, feature)和前面构造的 X 对齐out[:, -1, :]取序列最后一步因为预测目标紧跟在窗口末尾squeeze(-1)把(batch, 1)压成(batch,)和 y 对齐。参数上num_layers1时dropout参数在 PyTorch 里不生效所以代码里做了条件判断避免误以为加了正则。hidden_size不是越大越好16 和 32 在小样本上差距通常不到 1%但训练时间差一倍。3.2 训练循环损失函数、优化器与早停人口预测是回归任务损失用 MSE 或 Huber。Huber 对拐点年的异常值更稳delta1.0起步。优化器用 Adam学习率 1e-3权重衰减 1e-4。早停 patience 设 20 轮监控验证集损失。下面这段训练代码把早停和最佳权重保存都写进去了。def train_model(model, X_train, y_train, X_val, y_val, epochs300, lr1e-3, patience20): device torch.device(cuda if torch.cuda.is_available() else cpu) model.to(device) X_train torch.tensor(X_train, dtypetorch.float32).to(device) y_train torch.tensor(y_train, dtypetorch.float32).to(device) X_val torch.tensor(X_val, dtypetorch.float32).to(device) y_val torch.tensor(y_val, dtypetorch.float32).to(device) criterion nn.HuberLoss(delta1.0) optimizer torch.optim.Adam(model.parameters(), lrlr, weight_decay1e-4) best_val float(inf) best_state None wait 0 for epoch in range(epochs): model.train() optimizer.zero_grad() pred model(X_train) loss criterion(pred, y_train) loss.backward() optimizer.step() model.eval() with torch.no_grad(): val_pred model(X_val) val_loss criterion(val_pred, y_val).item() if val_loss best_val: best_val val_loss best_state {k: v.clone() for k, v in model.state_dict().items()} wait 0 else: wait 1 if wait patience: print(fEarly stop at epoch {epoch}, best val loss {best_val:.6f}) break model.load_state_dict(best_state) return model逻辑说明每个 epoch 先训练再验证验证损失连续patience轮不降就停best_state保存验证集最优权重避免最后一轮过拟合。参数上epochs300是上限实际小样本通常 50–100 轮就早停lr1e-3对 Adam 是安全起点若损失震荡可降到 5e-4。注意HuberLoss的delta控制异常值阈值人口数据拐点年误差大delta1.0比 MSE 稳但太小会退化成 MAE收敛变慢。3.3 预测与反归一化别在最后一步翻车模型输出是归一化后的值必须用同一个 scaler 反变换回真实人口。很多人只对目标列做了 scaler反变换时维度对不上这里要注意如果 scaler 是对[population] exog一起拟合的反变换要构造一个同宽度的占位数组只把预测值填进第 0 列再inverse_transform。下面给出预测和反归一化的完整写法。def predict_next(model, last_window, scaler, exog_dim): last_window: (seq_len, 1exog_dim) 归一化后的最近窗口 返回: 下一年真实人口值 model.eval() with torch.no_grad(): x torch.tensor(last_window[None, :, :], dtypetorch.float32) pred_scaled model(x).item() # 构造占位数组反归一化只填第 0 列 dummy np.zeros((1, 1 exog_dim)) dummy[0, 0] pred_scaled real scaler.inverse_transform(dummy)[0, 0] return real逻辑说明last_window[None, :, :]增加 batch 维度dummy宽度必须和 scaler 拟合时一致否则inverse_transform报维度错误。参数上exog_dim就是外生变量个数构造窗口时保持一致。这一步翻车最多建议单独写单元测试用训练集最后一年反推看反归一化结果是否和原始值对得上。4. 城市人口预测的避坑与排查5 个真实翻车现场4.1 现象验证集损失比训练集还低预测却离谱原因归一化用了全序列统计量验证集信息泄漏模型在验证集上“见过”未来分布。解决scaler 只在训练段 fit验证和测试段只 transform切分严格按时间顺序禁止 shuffle。4.2 现象模型预测值几乎是一条直线波动全丢原因seq_len太短或隐藏单元太少模型退化成趋势外推或者外生变量没加短期扰动无信号可学。解决seq_len加到 5隐藏单元提到 32至少拼入净迁入率和出生率两个外生变量检查输入是否被错误地做了差分。4.3 现象拐点年误差突然放大 3 倍以上原因拐点年样本在训练集中占比极低损失被平稳年份主导。解决损失换 Huber对拐点年样本加权sample_weight设为 2–3或者把拐点年单独做一次误差分析确认是数据口径问题还是模型问题。4.4 现象训练损失降到 1e-5测试集一塌糊涂原因小样本 隐藏单元过大模型把训练集年份背下来了。解决隐藏单元降到 16加weight_decay1e-4早停 patience 降到 10样本量少于 20 时考虑用单层 LSTM 加线性回归残差修正而不是硬堆深度。4.5 现象多城市面板数据一起训练单城市预测反而变差原因不同城市人口量级差异大未做城市级归一化模型把大城市绝对量当特征。解决按城市分别归一化后再拼接或者用城市 ID 做 embedding 输入更稳的做法是每个城市单独训一个轻量 LSTM共享超参数但不共享权重。5. 把 LSTM 预测接进年度规划滚动预测与置信区间的土办法模型训完只是半成品真正落地要解决“明年预测值给多少、区间多宽”。我一般用滚动预测每次预测下一年后把预测值填回窗口再预测下下年连续推 3–5 年。但滚动误差会累积所以同时用验证集残差的标准差构造置信区间残差标准差乘以 1.96 作为 95% 区间简单但规划部门能看懂。下面这段滚动预测代码把区间也一起算了。def rolling_forecast(model, last_window, scaler, exog_dim, steps3, residual_std0.0): 滚动预测未来 steps 年返回预测值和 95% 区间 residual_std: 验证集反归一化后残差的标准差 window last_window.copy() preds [] for _ in range(steps): pred predict_next(model, window, scaler, exog_dim) preds.append(pred) # 用预测值更新窗口去掉最早一年末尾追加新预测外生变量沿用最后一年 new_row window[-1].copy() new_row[0] (pred - scaler.data_min_[0]) / (scaler.data_max_[0] - scaler.data_min_[0]) window np.vstack([window[1:], new_row[None, :]]) lower [p - 1.96 * residual_std for p in preds] upper [p 1.96 * residual_std for p in preds] return preds, lower, upper逻辑说明new_row复制最后一年外生变量只更新人口列并手动做 min-max 归一化保证和 scaler 一致residual_std来自验证集反归一化后的残差计算时注意单位是万人还是人。参数上steps建议不超过 5再往后区间宽到没有决策价值residual_std如果验证集只有 3 个点标准差本身不可靠可以改用训练集验证集合并残差。最后说个我自己的习惯每次跑完模型一定把预测值和真实值按年份画在同一张折线图上肉眼过一遍拐点年。图比指标诚实指标好看但拐点年偏 5% 以上这个模型就不能直接进规划报告。LSTM 在城市人口预测上不是万能药它擅长的是把长趋势和短扰动分开建模数据口径和切分方式错了再深的网络也救不回来。希望帮到你。本文还有配套的精品资源点击获取
返回列表