
简介这份资源面向本科及以上、具备一定MATLAB基础的学习者与研究人员提供一套基于长短期记忆神经网络LSTM的城市人口预测完整实现方案。城市人口属于典型时间序列数据相比普通神经网络LSTM在捕捉长期依赖与趋势变化上更具优势因此该方案适合用于人口规模走势建模、区域规划预测等场景也可作为课程设计或科研扩展的起点。压缩包共38个文件约553KB其中4个.m脚本承担主程序与误差评估如MSE、RMSE、MBE、MAE、R²等指标计算1个xlsx存放人口原始数据另有32张jpg与1张png用于展示运行结果与对比图便于直观核对预测效果。目前已有525人学习下载。代码结构完整、注释清晰读者可直接运行复现并在此基础上替换数据、调整网络层数或参数快速迁移到其他时序预测任务中。1. 城市人口预测为什么总在拐点上翻车LSTM 能补上哪块短板做城市人口预测的人大多经历过这种尴尬用线性回归或 ARIMA 拟合历史常住人口过去十年的曲线贴合得漂漂亮亮一到预测未来三五年就集体失灵——要么高估了流入速度要么完全没接住政策调整带来的骤降。问题不在模型不够复杂而在于城市人口本身是一个被出生率、死亡率、迁移流、产业周期多股力量拉扯的长短期耦合系统传统时序模型只盯住了其中一条惯性线。LSTM长短期记忆网络之所以在这类任务里被反复提起核心是它的门控结构能同时保留长期趋势和短期扰动把“过去二十年缓慢爬升”和“近三年突然回落”这两类信号塞进同一个隐状态里。这篇笔记面向的是手里有统计年鉴或公开人口数据、想用 PyTorch 把 LSTM 城市人口预测跑通并落地评估的工程师从数据整理、滑窗构造、模型搭建到训练排错给一套能直接复现的路径也会说清楚它在哪些边界上并不可靠。2. 把人口数据整理成 LSTM 能吃的张量字段、滑窗与归一化2.1 城市人口预测的数据形态与字段选择城市人口预测的原始数据通常来自统计年鉴一张表里可能同时有年末常住人口、户籍人口、出生率、死亡率、自然增长率、城镇化率、人均 GDP 等字段。做 LSTM 时间序列预测时第一件事是决定用单变量还是多变量。单变量只用历年常住人口结构简单、容易收敛适合先跑通基线多变量把出生率、死亡率、人均 GDP 作为外生输入一起喂进去理论上能捕捉迁移和自然增长的分化但对数据完整度要求高缺失值处理不好反而拖垮模型。我一般会先做一张年度对齐表主键是年份每个城市一行字段按下面这张表整理字段名含义是否必选缺失处理year年份是不允许缺失city城市标识是不允许缺失population年末常住人口万人是线性插值后仍缺则剔除该年birth_rate出生率‰多变量时必选前向填充death_rate死亡率‰多变量时必选前向填充gdp_per_capita人均 GDP元可选前向填充字段选完要检查量纲。人口是几十到几千万级出生率是个位数千分号直接拼在一起送进网络梯度会被大数值字段主导。常见做法是对每个字段单独做 Min-Max 归一化把值压到 0 到 1 之间预测完再反归一化回原始量纲。import pandas as pd import numpy as np # 读取整理好的年度面板数据 df pd.read_csv(city_population.csv) df df.sort_values([city, year]).reset_index(dropTrue) # 对每个城市单独做前向填充避免跨城市串值 df[population] df.groupby(city)[population].ffill() df[birth_rate] df.groupby(city)[birth_rate].ffill() df[death_rate] df.groupby(city)[death_rate].ffill() # Min-Max 归一化保留每列的 min/max 用于反归一化 feat_cols [population, birth_rate, death_rate] scaler {} for col in feat_cols: lo, hi df[col].min(), df[col].max() scaler[col] (lo, hi) df[col] (df[col] - lo) / (hi - lo) print(df.head())这段代码的关键点有三个groupby(city)保证填充和归一化不跨城市污染ffill只做前向填充不用均值填充是因为人口类指标有强趋势均值会把趋势抹平scaler字典保存每列的极值后面预测输出必须用它反算否则得到的数字没有物理意义。参数上归一化范围固定用全局 min/max 而不是按城市分别归一化是为了让模型学到城市之间的可比性如果每个城市单独归一化小城市和大城市的曲线会被拉到同一尺度迁移规律就丢了。2.2 滑窗构造用多少年预测下一年LSTM 吃的是序列要把面板数据切成(样本数, 时间步长, 特征数)的三维张量。时间步长的选择直接决定模型能看到多长的历史。城市人口数据一般一年一条步长取 5 到 10 比较常见太短比如 3模型看不到长期趋势太长比如 20样本数会急剧减少而且早期数据和现在的人口结构差异大反而引入噪声。def make_sequences(data, feat_cols, window8, horizon1): X, y [], [] for city, group in data.groupby(city): arr group[feat_cols].values for i in range(len(arr) - window - horizon 1): X.append(arr[i:i window]) # 预测目标取人口列horizon 步之后的值 y.append(arr[i window horizon - 1, 0]) return np.array(X, dtypenp.float32), np.array(y, dtypenp.float32) X, y make_sequences(df, feat_cols, window8, horizon1) print(X.shape, y.shape) # 例如 (样本数, 8, 3) (样本数,)window8表示用连续 8 年数据预测第 9 年horizon1表示只预测一步。如果想做多年预测把horizon调大即可但要注意误差会随步数累积通常三年以上的预测需要单独评估。y取的是arr[..., 0]也就是人口列因为归一化时人口放在特征列表第一位这个下标要和feat_cols的顺序严格对应顺序写错是新手最常见的静默 bug——模型能训练、loss 能下降但预测的是出生率而不是人口。提示滑窗切完后先打印几个样本人工核对X[0]的最后一行的第一个值应该等于y[0]对应年份的前一年人口确认没有错位再进训练。3. 用 PyTorch 搭一个能收敛的 LSTM 人口预测模型3.1 网络结构层数、隐藏单元与输出层怎么定城市人口预测的样本量通常不大一个城市几十年数据几十个城市加起来也就几千条样本。这种规模下 LSTM 不宜堆太深常见配置是 1 到 2 层、隐藏单元 32 到 128。层数多了参数翻倍几千条样本根本喂不饱训练 loss 会一路降到很低但验证集发散也就是过拟合。我一般先用单层 64 隐藏单元跑基线再根据验证集表现决定要不要加到两层。import torch import torch.nn as nn class PopulationLSTM(nn.Module): def __init__(self, input_size, hidden_size64, 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 out[:, -1, :] return self.fc(last).squeeze(-1) model PopulationLSTM(input_sizelen(feat_cols)) print(model)batch_firstTrue让输入维度是(batch, seq_len, feature)和前面构造的张量对齐不加这个参数就得手动转置容易出错。dropout只在num_layers 1时生效单层 LSTM 加 dropout 在 PyTorch 里会报警告且不生效这是很多人以为加了正则其实没加的原因。输出层用Linear(hidden_size, 1)做单值回归squeeze(-1)把(batch, 1)压成(batch,)和标签形状一致否则算 loss 时广播会出问题。3.2 训练循环与损失函数MSE 之外还要看什么人口预测是回归任务损失函数默认用 MSE。但 MSE 对异常年份敏感如果某年因为行政区划调整人口突变MSE 会被这一个点主导模型为了压低它牺牲整体拟合。稳妥做法是先用 MSE 跑观察验证集误差如果发现某几个年份误差特别大再考虑换成 HuberLoss它对离群点更鲁棒。from torch.utils.data import TensorDataset, DataLoader # 按时间顺序切分前 80% 训练后 20% 验证不能随机打乱 split int(len(X) * 0.8) X_train, y_train torch.tensor(X[:split]), torch.tensor(y[:split]) X_val, y_val torch.tensor(X[split:]), torch.tensor(y[split:]) train_loader DataLoader(TensorDataset(X_train, y_train), batch_size32, shuffleTrue) val_loader DataLoader(TensorDataset(X_val, y_val), batch_size32, shuffleFalse) device torch.device(cuda if torch.cuda.is_available() else cpu) model model.to(device) optimizer torch.optim.Adam(model.parameters(), lr1e-3) criterion nn.MSELoss() for epoch in range(200): model.train() train_loss 0.0 for xb, yb in train_loader: xb, yb xb.to(device), yb.to(device) optimizer.zero_grad() pred model(xb) loss criterion(pred, yb) loss.backward() # 梯度裁剪防止 LSTM 梯度爆炸 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() train_loss loss.item() * len(xb) model.eval() val_loss 0.0 with torch.no_grad(): for xb, yb in val_loader: xb, yb xb.to(device), yb.to(device) val_loss criterion(model(xb), yb).item() * len(xb) if (epoch 1) % 20 0: print(fepoch {epoch1}, train {train_loss/len(X_train):.6f}, val {val_loss/len(X_val):.6f})几个参数值得说清楚。lr1e-3是 Adam 的常用起点LSTM 对学习率敏感超过 1e-2 容易震荡不收敛低于 1e-4 收敛太慢。clip_grad_norm_的max_norm1.0是 LSTM 训练的后悔药循环结构反向传播时梯度容易爆炸裁剪后训练稳定性明显提升。切分数据时按时间顺序切而不是随机切是因为时序任务里随机切会让未来信息泄漏到训练集验证 loss 虚低上线就翻车。训练轮数 200 是经验值实际要看验证 loss 是否已经平稳如果 50 轮就不降了继续训只会过拟合。3.3 反归一化与预测结果还原模型输出的是归一化后的人口值必须用前面保存的scaler反算回万人。这一步经常被忽略导致预测出来是 0.3 这种数字完全没法解读。model.eval() with torch.no_grad(): pred_norm model(torch.tensor(X_val).to(device)).cpu().numpy() lo, hi scaler[population] pred_real pred_norm * (hi - lo) lo true_real y_val.numpy() * (hi - lo) lo mae np.mean(np.abs(pred_real - true_real)) print(f验证集 MAE: {mae:.2f} 万人)反归一化公式就是 Min-Max 的逆运算lo和hi必须和训练时用的是同一组如果中途重新算了 scaler结果会整体偏移。MAE 用万人做单位比 MSE 直观报告结果时建议同时给出 MAPE方便跨城市比较。4. 城市人口预测的避坑清单五个真实翻车现场4.1 现象训练 loss 一路降验证 loss 从第 10 轮开始涨原因样本量小加上模型容量偏大典型过拟合。城市人口数据一个城市就几十年几十个城市拼起来样本也就几千条两层 128 隐藏单元的 LSTM 参数量轻松过十万模型把训练集背下来了。解决先把hidden_size降到 32 或 64num_layers保持 1再加 dropout注意单层 LSTM 的 dropout 不生效要么加到两层要么在fc前手动加nn.Dropout。如果还不行用早停验证 loss 连续 20 轮不降就停。4.2 现象预测曲线整体平移形状对但数值差一截原因反归一化用错了 scaler或者训练时对人口列做了归一化但预测目标取的是别的列。前面强调过feat_cols顺序和y下标必须对应顺序错位时模型学的是出生率反算回人口量纲自然整体偏移。解决在make_sequences里打印feat_cols和y的对应关系确认arr[..., 0]就是人口列。反归一化时核对scaler[population]的 lo/hi 是否和训练时一致。4.3 现象某几个城市预测误差特别大其他城市正常原因这些城市在历史上发生过行政区划调整、撤县设区或统计口径变化人口序列出现阶跃模型把它当成趋势学进去了。解决在数据整理阶段标记这类断点要么把断点前的数据截断要么对断点做平滑处理。如果断点太多考虑把这些城市单独建模不要和稳定城市混在一个训练集里。4.4 现象多变量模型比单变量还差原因外生变量缺失值多前向填充引入了虚假的平稳段模型学到了填充模式而不是真实规律。另外出生率、死亡率量纲小归一化后数值集中在 0.1 附近梯度贡献被人口列压制。解决先检查每个外生变量的缺失比例超过 20% 的字段直接弃用。保留的字段确认归一化后分布不要过度集中必要时对出生率这类小量纲字段做标准化而不是 Min-Max。4.5 现象换一台机器或重跑一次结果差异明显原因没有固定随机种子PyTorch 的权重初始化、DataLoader 的 shuffle 都带随机性小样本下不同初始化的收敛结果差异会被放大。解决在训练脚本开头固定种子torch.manual_seed(42)、np.random.seed(42)DataLoader 的shuffle在验证集上关掉。固定后重跑结果可复现方便对比不同超参的效果。5. 把预测做成能用的工具滚动预测与误差区间的实操技巧单步预测跑通只是起点实际业务里要的是未来三到五年的曲线还要知道这个曲线有多可信。我一般用滚动预测先用模型预测下一年把预测值拼回输入序列末尾再预测下一年如此迭代。这样做的问题是误差会累积三年后的预测基本只能看趋势不能看绝对值。缓解办法是每滚动一步就重新用真实数据校正一次也就是只在没有真实值的年份才用预测值填充。def rolling_forecast(model, last_window, steps3): model.eval() window last_window.copy() # (seq_len, feat) preds [] with torch.no_grad(): for _ in range(steps): x torch.tensor(window[None, :, :], dtypetorch.float32).to(device) p model(x).item() preds.append(p) # 用预测值替换窗口最后一行的人口列其余特征保持不变 new_row window[-1].copy() new_row[0] p window np.vstack([window[1:], new_row]) lo, hi scaler[population] return [v * (hi - lo) lo for v in preds]这段代码里new_row[0] p只更新人口列出生率和死亡率保持最后一年不变这是一个简化假设。如果外生变量本身也有趋势需要单独建一个预测器否则多变量滚动预测会退化成单变量。window[1:]加new_row实现窗口滑动顺序不能反反了时间就倒流了。误差区间我用两种方式估一是看验证集上滚动预测的 MAE把它作为固定带宽二是训练多个不同种子的模型取预测分布的 10% 和 90% 分位数作为区间。前者简单后者更贴近真实不确定性但训练成本翻几倍。实际交付时我倾向给区间而不是单点因为城市人口预测的决策方——规划、财政、教育——需要知道最坏情况单点数字反而误导。最后说个我自己的习惯每次跑完模型先把预测值和真实值画在同一张图上肉眼过一遍拐点位置。LSTM 在平稳段拟合得很好但遇到政策导致的突变它往往滞后一年才反应过来。这个滞后不是调参能解决的是数据里没有先验信息。所以我现在做城市人口预测模型输出只作为参考线真正的拐点判断还是靠对当地产业和政策的了解。希望这套流程能帮你把 LSTM 城市人口预测跑通也帮你认清它的边界。本文还有配套的精品资源点击获取