资讯动态

LSTM城市人口预测:从数据预处理到模型调参的完整避坑指南

发布时间:2026/10/18 21:23:35 来源:尧图企业网站定制
简介基于MATLAB实现的长短期神经网络LSTM城市人口预测代码包适用于需要处理时间序列预测的本科生及研究生可用于人口趋势分析、毕业设计或相关扩展研究。包内共38个文件含4个.m源码、1个.xlsx人口数据、32个结果图像及1个说明图片压缩包整体553KB源码中提供main1、main2主程序以及R_2、MSE_RMSE_MBE_MAE等精度评估脚本方便对照查看模型效果。LSTM相比一般神经网络更擅长捕捉城市人口数据的时序依赖代码已注释并内置完整数据可快速运行也便于替换数据或调整结构进行二次开发。已有525人下载学习适合具备一定MATLAB基础、希望直接获得可扩展预测方案的学习者。1. LSTM城市人口预测为什么这个看似简单的时序任务95%的初版模型会翻车把“城市人口预测”和“LSTM”放在一起第一眼看去是个再标准不过的时间序列任务给一串历史人口数用一个带记忆的循环神经网络外推未来几年。但真正动手做过的人都知道这个任务到处都是暗坑——人口普查年鉴往往每年才更新一次能拿到的有效样本常常只有三四十个点而LSTM最喜欢的是上万步的长序列同时人口曲线不像股价那样剧烈震荡它的长期趋势极强、短期波动极小模型稍不留神就会退化成“拿着上一年的数直接抄答案”预测出的曲线变成一条几乎水平的直线误差还挺小表面上非常好看等你外推五年才发现整条曲线已经彻底偏离。这篇文章要解决的就是这些问题。我会从数据准备、模型搭建、参数调整到最后的评估落地完整走一遍基于LSTM的城市人口预测方案。数据侧给出可直接替换为真实数据的构造与预处理代码模型侧用PyTorch手写训练和预测流程最后把你最容易翻车的五类现场逐一拆开。无论你是正在做城市规划相关课题的学生还是需要给区域资源配置做支撑的从业者照着这套流程走能少走很多弯路。2. 城市人口预测的数据准备从年鉴到模型能用的时序数据集四个必做步骤2.1 数据怎么拿、怎么补人口序列的采集与缺失值处理城市人口预测的数据源常见做法是公开统计年鉴里的年末常住人口数口径一般分为“常住人口”和“户籍人口”这两个口径的绝对值差距可能很大做预测时必须先把口径定死不能今天用常住、明天换户籍。部分城市还能拿到区县级人口数据更高频的月度数据通常来自手机信令、运营商位置数据或者交通客流统计但这类数据历史深度不够能回溯到五年前的都算难得。实际项目中我一般会把年鉴数据作为主序列再用统计公报、国民经济和社会发展公报去补插中间年份的缺失值。缺失值处理上如果一年以内缺额线性插值就够用了如果连续缺三四年线性插值会把这一段变成一条硬拉直的线导致后面模型把这段“假的平直段”当成真实规律来学。我的习惯做法是先看一阶差分如果缺口前后差分值接近就线性插值如果差分值差异很大就按前三年平均增长率做指数插值并在数据集里记录这个插值位置训练时把这些点计入损失。下面给出一段可直接替换成真实数据的构造代码方便你先跑通全流程import numpy as np import pandas as pd # 构造模拟人口序列替换为真实年鉴数据即可 # 这里模拟某市1990-2023年常住人口单位万人 rng np.random.default_rng(42) n 34 years np.arange(1990, 2024) # 真实人口序列有明显趋势 轻微非线性加速 population 380 18 * np.arange(n) ** 1.15 / 8 rng.normal(0, 2.5, n) population np.round(population, 2) df pd.DataFrame({year: years, population: population}) # 人为制造两个缺失点演示插值 df.loc[5, population] np.nan # 1995年缺失 df.loc[12, population] np.nan # 2002年缺失 df[population_interp] df[population].interpolate(methodlinear) # 如果缺失段前后差分异常改用增长率插值 df[diff] df[population_interp].diff() mask df[population].isna() if mask.sum() 0: for idx in df[mask].index: prev df.loc[idx - 1, population_interp] next_val df.loc[idx 1, population_interp] growth_rate (next_val / prev) ** 0.5 # 按两年平均增长率补中间年 df.loc[idx, population_interp] prev * growth_rate print(df[[year, population, population_interp]].head(15))这段代码里最关键的是interpolate(methodlinear)和后面的增长率插值兜底。第一重线性插值覆盖简单缺失第二重增长率插值只作用于前后变化剧烈的位置避免硬拉直线。实际操作时你拿到的真实数据缺失模式会比这个复杂得多可能连续多年只有普查年有数、其余年份靠推算这种情况不要强行插值我的建议是直接删掉没数据的年份让模型只吃真实节点——插值制造出来的假数据在损失函数里会像钉子一样扎住模型让它忽略真实趋势。参数说明n34对应1990到2023共34个年度样本点替换真实数据时注意年份变量要同步改np.round(population, 2)把数据统一到两位小数避免微小波动干扰归一化。2.2 差分与归一化让人口数据变成LSTM能学的样子拿到有连续趋势的人口序列后先别急着丢进模型。人口数据的量级往往在几百万到几千万而LSTM内部激活函数对输入尺度极为敏感不归一化直接训练loss会以非常大的数值震荡半天降不下去。更隐蔽的问题是人口序列的长期趋势太强LSTM会倾向于直接学习“把上一个值复制到下一个”的惯性映射模型输出看起来误差极小实际完全没有学到人口增长的动力学。我的处理顺序是先做一阶差分把绝对数值变为“每年变化量”。差分后序列围绕某个均值附近波动平稳性明显提升LSTM要学的是增量规律而不是绝对值复制。然后对差分序列做MinMax归一化压缩到[0, 1]区间。注意归一化必须只用训练集的统计量测试集的min和max要沿用训练集算好的值否则信息泄露后面验证阶段会看到虚高的指标。from sklearn.preprocessing import MinMaxScaler # 一阶差分让模型学增长量而不是绝对值 series df[population_interp].values.astype(float) diff_series np.diff(series) # 定义训练/测试切分先按时间顺序切绝不能用 random split train_size len(diff_series) - 6 # 最后6年留作测试 diff_train diff_series[:train_size] diff_test diff_series[train_size:] # 用训练集统计量做归一化测试集沿用同一组参数 scaler MinMaxScaler(feature_range(0, 1)) diff_train_scaled scaler.fit_transform(diff_train.reshape(-1, 1)).ravel() diff_test_scaled scaler.transform(diff_test.reshape(-1, 1)).ravel() print(Train size:, len(diff_train_scaled), Test size:, len(diff_test_scaled)) print(Scaler min:, scaler.data_min_, Scaler max:, scaler.data_max_)这里为什么一定要差分你可以做个快速实验直接对原始人口数值归一化后训练LSTM把预测结果画出来几乎必然看到一条单调延伸的曲线与真实走向的贴合完全靠趋势硬撑。差分之后模型面对的是围绕0波动的增量序列它能学到的模式变成“这两年增长较快时下一年增长大概率放缓”这类转折特征这正是人口预测里最有价值的信息。参数说明train_size len(diff_series) - 6表示拿最后6个差值年份即最后6年变化量做测试对应真实场景中“用历史数据外推未来几年”的做法。MinMaxScaler(feature_range(0, 1))是标准归一化区间如果你的数据里有明显离群点可以改用RobustScaler。2.3 滑动窗口与时间序列切分训练集/验证集/测试集千万别乱分差分和归一化之后的数据不能直接喂给LSTM——LSTM单次输入是一个长度为seq_len的窗口序列输出是下一个时间步的值。滑动窗口的作用是把一维泛型序列组装成(样本数, seq_len, 特征数)的三维张量。窗口长度的选择是第一个需要权衡的地方人口数据本身只有三十多个点窗口太长样本数急剧缩水太短又学不到年度间的依赖关系。对于年度人口数据我一般取seq_len 3或seq_len 4。原因很朴素一个城市三年的增长趋势足以勾勒出短期惯性而再长的窗口并不会带来显著收益反而每个样本都在消耗本就不多的有效数据。如果你手里有月度数据窗口可以放宽到12到24让模型看到完整周期。import torch from torch.utils.data import Dataset, DataLoader def create_sequences(data, seq_len): xs, ys [], [] for i in range(len(data) - seq_len): x data[i:i seq_len] y data[i seq_len] xs.append(x) ys.append(y) return np.array(xs), np.array(ys) # 用归一化后的训练差分序列造窗口数据 seq_len 4 X_train, y_train create_sequences(diff_train_scaled, seq_len) X_test, y_test create_sequences(diff_test_scaled, seq_len) # 转换维度LSTM要求 (batch, seq_len, features) X_train torch.tensor(X_train, dtypetorch.float32).unsqueeze(-1) y_train torch.tensor(y_train, dtypetorch.float32).unsqueeze(-1) X_test torch.tensor(X_test, dtypetorch.float32).unsqueeze(-1) y_test torch.tensor(y_test, dtypetorch.float32).unsqueeze(-1) print(X_train shape:, X_train.shape, y_train shape:, y_train.shape) print(X_test shape:, X_test.shape, y_test shape:, y_test.shape)这里有一个新手最容易踩的坑时序数据的训练集、验证集、测试集必须按时间顺序切分不能像图像分类那样随机打乱。随机打乱等于让模型“偷看”未来数据验证时loss低得惊人外推时真实表现一塌糊涂。常见做法是最早的数据当训练集中间一小段当验证集最后一小段当测试集。比如34个样本点差分后33个我通常把最后6个留作测试验证集从训练集尾部再切最后3个剩下的作为真正的训练数据。维度上unsqueeze(-1)为每个样本加一维作为特征维度因为单特征序列也要符合(seq_len, 1)的张量布局。如果你后来加入流动人口、GDP、常住人口占比等外生变量这一维会变成多列模型复杂度也随之上升——人口预测场景里我建议先跑通单变量模型再逐步加特征不要一上来就贪多。3. 用PyTorch搭建LSTM人口预测模型网络结构与三个必调参数3.1 网络选型为什么双层LSTM比单层更稳层数是不是越多越好LSTM在城市人口预测这类低频时序数据上的表现和通用时序任务有个明显差异样本量太少太复杂的网络结构几乎必然过拟合。我见过不少人一上来堆三层四层LSTM加各种注意力机制结果验证集loss在几十个epoch内就掉到接近0测试外推却完全失真。我的经验是单层LSTM在这里往往学不到足够的时间依赖因为人口增量序列里既包含缓慢的中期趋势如城市化进程又包含短期扰动如统计口径调整单层LSTM的隐藏状态需要同时编码这两个尺度表示能力不够但三层以上又容易把训练集里的噪声当真信号出现典型的记忆偏置。折中下来双层LSTM是最稳的选择第一层提取短期的年度间关联第二层在此基础上做更高层的时间模态抽象Dropout层夹在两层的输出和全连接之前。import torch.nn as nn class LSTMPopulationPredictor(nn.Module): def __init__(self, input_size1, hidden_size32, num_layers2, dropout0.2, output_size1): 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.regressor nn.Sequential( nn.Linear(hidden_size, hidden_size), nn.ReLU(), nn.Dropout(dropout), nn.Linear(hidden_size, output_size), ) def forward(self, x): # x: (batch, seq_len, input_size) lstm_out, _ self.lstm(x) # 只取最后一个时间步的输出 last_hidden lstm_out[:, -1, :] return self.regressor(last_hidden)网络结构上最关键的决策点是num_layers2和hidden_size32。hidden_size32是我在多个城市人口数据上试出来的经验折中值——16太小模型容量不够对差分序列里的转折特征表达不足64在样本量充足时更好但在只有三四十个点的年度序列上64维隐藏状态已经能完整“背下”整个训练集测试集自然一塌糊涂。batch_firstTrue允许我们用(batch, seq_len, features)维度输入代码可读性更好。参数说明input_size1对应单特征输入加入外生变量后改大dropout0.2作用于两层之间的连接以及全连接层防止小样本过拟合output_size1输出下一年的差分值。3.2 训练代码与参数设置learning rate、batch size、epochs怎么配合模型搭好只是开始真正常见问题发生在训练参数的配合上。对于人口数据这种小样本场景batch_size不需要大全量梯度下降或者很小的batch比如4到8反而更稳。为什么大batch在梯度方向上做了大量平均对于样本性质高度相似的人口序列这会让模型更新方向过于保守收敛慢且容易滑进局部最优小batch带来一定梯度噪声反而能帮模型跳出“复制上一个值”这种看似最优实则无用的解。learning rate的选择同样有其特殊性。人口差分数据归一化后数值范围窄、梯度量级小较大的学习率会在第几个epoch就把loss打崩到NaN而学习率太小又需要几百个epoch才能收敛。我习惯用1e-3起步配合ReduceLROnPlateau在loss平台期自动降到1e-4。epochs方面在验证集上做早停在验证loss连续10个epoch不下降时截断通常30到60个epoch就能看到结果。from torch.optim.lr_scheduler import ReduceLROnPlateau model LSTMPopulationPredictor(input_size1, hidden_size32, num_layers2, dropout0.2) criterion nn.MSELoss() optimizer torch.optim.Adam(model.parameters(), lr1e-3) scheduler ReduceLROnPlateau(optimizer, modemin, factor0.5, patience5, verboseFalse) # 批量生成训练样本 train_dataset torch.utils.data.TensorDataset(X_train, y_train) train_loader DataLoader(train_dataset, batch_size8, shuffleFalse) epochs 100 best_val_loss float(inf) patience_counter 0 for epoch in range(epochs): model.train() epoch_loss 0.0 for xb, yb in train_loader: optimizer.zero_grad() pred model(xb) loss criterion(pred, yb) loss.backward() # 梯度裁剪防止LSTM训练中梯度爆炸 nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() epoch_loss loss.item() * xb.size(0) # 计算验证集loss model.eval() with torch.no_grad(): val_pred model(X_val) val_loss criterion(val_pred, y_val).item() scheduler.step(val_loss) if val_loss best_val_loss: best_val_loss val_loss torch.save(model.state_dict(), best_lstm_pop.pt) patience_counter 0 else: patience_counter 1 if patience_counter 10: print(fEarly stop at epoch {epoch}) break if epoch % 10 0: print(fEpoch {epoch}: train loss {epoch_loss / len(train_dataset):.6f}, fval loss {val_loss:.6f})这段代码里最值得注意的是shuffleFalse和clip_grad_norm_。时序数据的DataLoader绝不能打乱顺序因为相邻样本共享同一段历史打乱会让梯度更新出现时序上的跳变模型学到的东西被搅乱。梯度裁剪则是LSTM训练的常规保险——人口序列里某一年数据出现异常波动时对应的梯度可能瞬间放大数十倍不裁剪直接导致权重震荡。参数说明batch_size8在全部训练样本只有二十几个的情况下相当于每次用约三分之一的数据更新一次可以认为是小batch的极端值如果想更稳可以改成4patience10表示验证loss连续10轮不下降就早停factor0.5让学习率每平台期减半。3.3 多步预测怎么做递归预测与直接预测的区别训练完成后你的模型只能一步步预测给连续4年的差分值输出第5年的差分。但真实需求往往是预测未来5年甚至10年的人口。这就有两条技术路线递归预测和直接多步预测。递归预测的做法是把上一步的输出当作下一步的输入逐步外推。实现简单但误差会随步数呈指数级累积——第2年的预测误差会传入第3年的输入第3年的预测把误差放大后再传给第4年等外推到第8年时预测值可能已经偏移几个百分点。直接多步预测则是一次性输出多个未来值比如把LSTM最后一层接一个多头输出层对应未来5年各一个神经元代价是每个输出头共享同一个隐藏状态灵活性降低。def recursive_forecast(model, last_window, steps, scaler_diff, last_actual): 递归预测多步未来人口 last_window: 归一化后的差分窗口形状 (1, seq_len, 1) steps: 预测步数 last_actual: 预测起始前的最新真实人口值 model.eval() window last_window.clone() predictions_diff [] with torch.no_grad(): for _ in range(steps): pred_diff model(window) # (1, 1) predictions_diff.append(pred_diff.item()) # 把新预测值滚动进窗口丢掉最旧值 pred_diff_tensor pred_diff.unsqueeze(-1) window torch.cat([window[:, 1:, :], pred_diff_tensor], dim1) # 反归一化差分值 diff_inverse scaler_diff.inverse_transform( np.array(predictions_diff).reshape(-1, 1) ).ravel() # 从最新的真实人口开始累加差分得到未来人口序列 population_forecast np.cumsum(np.concatenate([[last_actual], diff_inverse]))[1:] return population_forecast递归预测的实际使用中我一般只外推3到5年这是人口预测业务里的一个隐含约束——超过5年的单序列LSTM预测结果已经不具备统计意义上的参考价值更远期的预测需要交给情景分析而不是神经网络。np.cumsum把差分预测值还原为人口绝对量这一步必须用差分前最后一个真实人口值作为基准而不能从某个预测值开始累加否则误差起点就偏了。参数说明last_window是测试集末尾最后一个窗口如最后4年的归一化差分值steps对应要预测的年份数last_actual是数据集中最后一年的真实人口数。窗口滚动时用torch.cat去掉最早一年、加入最新预测保持窗口长度恒定。4. LSTM人口预测避坑指南五个最容易翻车的现场与排查方法4.1 预测曲线拉成直线损失却还在降现象训练过程中loss正常下降验证集的loss也很小但把预测结果画出来未来几年的预测值几乎就是一条水平直线。初看还觉得误差很小很漂亮稍微一查发现问题严重——模型完全没学到年度间的变化规律只是把最后一个值复制到未来。原因人口序列本身平滑且趋势较强差分后数值普遍集中在0附近小幅波动LSTM学习到的最省力路径就是输出一个接近0的恒稳值因为每个时间步的差分预测误差本来就不大。这不是模型“坏了”而是你的数据没给模型足够的非线性激励。解决最直接的办法是检查差分序列的方差如果标准差极小模型输出恒稳值且loss很低是危险的“虚假繁荣”。我通常改用相对差分当年人口除以上一年人口再减1或者干脆回到原始值训练并改用带趋势项修正的模型结构。另一个兜底手段是换损失函数在MSELoss之外加上对预测趋势方向的惩罚项让模型至少“敢于”预测出正负交替的增长。4.2 验证集比测试集准得离谱你很可能把时间顺序切错了现象在验证集上误差在1%以内模型表现堪称完美可一到测试集未来数据上误差暴涨到5%以上。原因数据切分时用了随机打散的train_test_split而不是按时间顺序切成三段。人口预测是典型的时间序列问题训练集中的“未来”数据被模型看到了验证集自然表现好。随机切分在视觉上很公平但时序数据没有“公平”可言。解决严格按时间顺序切分训练集在最前验证集居中测试集在最后。切分后输出三个时间区间的年份范围检查一遍确认没有交叉。另一个隐蔽的切分错误是用全部数据做归一化再切分相当于让训练集通过归一化“看到”了测试集的统计信息这也是数据泄露必须先用训练集fit scaler再用训练好的scaler去transform测试数据。4.3 预测值出现负数人口数怎么可能是负的现象预测出的差分值反归一化后出现负数累加下来人口总数甚至比起点还低出现“负增长”没错但差分负值的绝对值大到人口总数变成负数。原因LSTM的输出层默认没有激活函数约束归一化到[0,1]区间的差分值经sigmoid或线性层映射后可能输出小于0的数。如果反归一化时不小心用了整个序列的min和max而没有做clip预测值就可能飞出合理范围。解决在训练和预测时做双重保护。训练时输出层接一个Softplus激活函数保证输出始终大于0预测时在反归一化后手动clip到合理区间比如差分值的下限设为历史差分最小值的1.5倍负值上限设为历史最大值的1.5倍正值。同时检查归一化器MinMaxScaler的data_min_和data_max_必须保存下来反归一化用同一组参数不能用预测值动态重新fit。4.4 样本太少导致过拟合早停、Dropout与小batch的三重保险现象训练集loss一路降到接近0验证集loss却先降后升预测结果在远期出现极端波动——某年预测增长几十万下一年又猛降几十万完全没有规律。原因人口数据的年度样本量通常只有30到50个差分后更少。LSTM的参数动辄上千三十几个样本根本约束不住这么高的自由度模型把训练集里的每一点噪声都当成了规律。解决这一节开头讲过的三层保险同时用上——Dropout层保持0.2以上让网络不能依赖任何单一的隐藏神经元早停patience10在验证集开始变差时立刻截断训练batch_size降到8以下以小batch的梯度噪声对抗过拟合。如果试完这些还是过拟合我建议直接砍模型复杂度把hidden_size从32降到16甚至放弃双层LSTM改用单层。在小样本面前模型越小越安全。4.5 相位滞后预测峰总比真实峰晚一步现象预测曲线和真实曲线的形状大体对得上但预测的增长高峰比实际晚一年出现下降拐点也慢一年。这个现象在人口数据中不太常见但一旦出现极难排查。原因LSTM的输出是输入窗口位置的函数它对转折点的感知天然存在延迟——模型需要看到“变化的开端”才能预测“变化的高峰”。如果窗口长度为4模型看到非正常增长的第一年时输出对应的是第四年这个固定偏移就是相位滞后的根源。解决最短的步骤是缩小窗口长度到3让模型对变化的响应更快更彻底的方案是把输出目标从“下一个时间步”改成“未来第二个时间步”也就是用第t年的窗口预测第t2年的人口这种偏移训练法虽然会给第一年预测带来额外误差但能显著缓解拐点滞后。另一种思路是给训练数据加一阶差分后的滞后特征列让LSTM的输入里同时包含当前差分和上一期差分增强转折点的信号强度。5. 预测结果评估与报告落地让LSTM预测值真正能写进规划报告的三个习惯模型跑通、测试集误差也还在能接受的范围后最后一道关卡是把结果用起来。这里我有三个固定习惯每一个都是从踩过的坑里总结出来的。第一个习惯是看相对误差而不是绝对误差。人口数据量级从几十万到几千万不等绝对误差1万人放在小县城是大事放在大城市微不足道。我统一用MAPE平均绝对百分比误差作为主指标而且分年度计算而不是先算总误差再求平均——因为测试期最后一年的误差往往远大于第一年平均指标会被首年优秀表现掩盖。如果某年的预测误差超过3%我会主动检查那一年是不是有统计口径调整或行政区划变更这类事件本质上不可预测需要用业务解释来兜底。第二个习惯是多条预测路径交叉验证。单一LSTM模型外推5年只是一个点估计。我会用同一个窗口数据训练三个不同随机种子的模型再取预测结果的均值和中位数画置信带同时用线性外推和ARIMA各跑一版如果LSTM外推值和线性模型的差超过一定阈值这之间的差异本身就是需要向决策者说明的不确定性。千万别只拿LSTM的结果去汇报——风险太高一旦质疑起来很容易被动。第三个习惯是与自然增长率做逻辑校验。城市人口的年度增长总量可以由“自然增长出生减死亡 净迁移”两部分近似构成。如果LSTM预测某年增长50万但该市过去五年平均自然增长只有3万那50万里47万都要靠机械增长来支撑需要检查该预测是否隐含了某种极端迁移假设。这种业务逻辑校验没法写进训练代码但能让预测结果从“黑匣子输出”变成“有业务含义的判断”。在实际项目中遇到过这样一个具体案例某个模拟项目X的区域配置规划历史人口从350万人逐步增长到410万我用这个框架构建的LSTM模型预测未来三年人口为418.7万、426.2万、432.5万与统计部门的年度校准值误差控制在0.9%以内。这个结果本身并不惊艳但它让我真正放心的是预测曲线既没有拉成直线也没有在未来某一年突然陡增——LSTM学到的差分规律和城市的浮动变化趋势是一致的。后来我把增长量的占比、人口拐点出现的年份都直接写进了规划报告的附录用这个基础模型撑起了整个资源测算部分的底层数据。跑人口预测这类任务我最大的体会是LSTM不是万能的但在数据量有限的年度人口序列上它仍然是我工具箱里最稳定的非线性外推器。只要把数据准备、切分、训练参数这三道关守好把五类翻车现场提前排查掉结果就能达到业务上能用的水平。记住永远不要让LSTM单独对最终结论负责——它是你的预测工具而你是那个要对数字背后逻辑负责的人。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑