资讯动态

LSTM城市人口预测:从数据预处理到滚动回测避坑指南

发布时间:2026/10/18 9:41:19 来源:尧图企业网站定制
简介一套基于MATLAB的LSTM城市人口预测完整代码包面向需要处理时间序列预测与神经网络建模的本科及以上学生或研究人员。城市人口数据具有明显时序特性长短期记忆网络能有效捕捉长期依赖关系相比普通神经网络更契合此类预测任务。压缩包内共38个文件包含main1.m、main2.m两个主程序以及MSE_RMSE_MBE_MAE.m、R_2.m等误差与拟合优度评估脚本配套人口数据xlsx表格和多张运行结果图jpg/png共553KB结构清晰、便于对照与迁移。代码带有详细注释数据完整可帮助读者快速跑通LSTM训练-验证-预测流程并进一步扩展至其他城市或相近时间序列场景。目前已有525人学习浏览适合作为课程设计、毕设或论文仿真基线。1. 基于 LSTM 的城市人口预测为什么是 LSTM以及谁能直接上手基于 LSTM 的城市人口预测本质是把一个城市过去几十年的常住人口序列当作带记忆的时间序列用循环神经网络去学习人口惯性、出生潮的滞后效应和城市化拐点再给出未来 3 到 5 年的预测值。比起 ARIMA 这类线性模型LSTM 不强制要求序列差分后平稳能自动保留“十几年前的出生高峰会影响今天的劳动年龄人口”这类长距离依赖这是人口预测里最值钱的能力。这套方案适合两类人一类是城市规划、交通、公共设施领域的技术人员需要把人口结果放进业务决策另一类是刚接触时序预测、手里有多年人口年鉴但不知道从哪一步开始写的开发者。它不需要 GPU单机 CPU 就能训练完真正的门槛不在模型结构而在数据切分、窗口设置和指标定义上。下面按一条完整落地链路讲先想清楚数据再写代码最后用滚动验证检验模型有没有学偏。2. 人口数据预处理与评价指标决定模型学的是趋势还是噪声2.1 人口序列的三个结构性特征惯性、滞后效应与政策断点城市人口序列和股票、天气这类高频时序最大的区别是低频、强惯性和弱周期性。常住人口每一年变化相对平缓除非发生行政区划调整或者大规模产业迁入迁出否则相邻年份的差值通常控制在百分之几以内。这种强惯性对预测是双刃剑它让短期预测容易做但也容易让模型偷懒——直接复制上一年数值就能拿到很低的误差根本没有学到趋势。滞后效应是人口预测里最需要窗口去覆盖的部分。一个城市在某一年的生育高峰会在十几年后变成劳动力供给上的一个凸起一次大规模人口导入会在入学率、购房需求上滞后显现。LSTM 的优势在于能通过循环结构保留这些历史信息但前提是输入窗口够长。如果只看过去 3 年模型永远不知道 15 年前的生育高峰正在进入劳动年龄这比任何网络结构问题都致命。政策断点则是人口预测里最难处理的部分。撤县设区、新区成立、重大产业项目落地都会让历史趋势被瞬间改写。LSTM 不能预测“还没发生的政策”但可以通过多情景输入把断点变成条件预测预测时把未来政策影响作为外生变量拼进特征比让模型自己硬猜靠谱得多。实际项目中我一般把“无重大政策扰动”作为基准情景再叠加一个“政策落地”情景做区间。数据处理上还要注意年鉴数据的质量。很多城市公开的常住人口数据存在缺年、口径调整、前后不一致的问题。缺一两年可以用线性插值补如果中间断了五年以上插值就是在编数据不如把断点前后拆成分段模型分别训练分别预测。另外人口口径要统一常住人口和户籍人口混用会让模型学到错误的跳变。2.2 训练集/测试集划分与归一化测试集绝不能提前参与统计时序预测最忌讳随机打乱划分。人口数据样本量本身就不大一年一条记录几十年才几十条如果随机抽取训练集和测试集模型等于看到了未来的数据测试集的误差会漂亮得很虚假。正确做法是按时间顺序切分前 70% 到 80% 做训练后 20% 到 30% 做测试测试周期通常对应最近 10 到 15 年。归一化也同样要按训练集来 fit再对测试集只做 transform而不是先把全部数据 MinMaxScaler 完再切分。原因在于测试集最大值参与了缩放映射相当于把未来的人口的规模信息提前告诉模型这属于数据泄漏。一旦往后做滚动预测这套泄漏会导致预测区间系统性偏离真实分布。我习惯先切分再在训练集上fit_transform然后在测试集上transform每一步都保持训练信息不向前渗透。from sklearn.preprocessing import MinMaxScaler split int(len(population) * 0.8) train_raw population[:split] test_raw population[split:] scaler MinMaxScaler() train_scaled scaler.fit_transform(train_raw.reshape(-1, 1)) test_scaled scaler.transform(test_raw.reshape(-1, 1))这段代码的关键在于test_raw只调用transform没有调用fit_transform。如果写成scaler.fit_transform(全体数据)测试集的最大最小值就已经参与训练样本的映射了这一点在人口这类小样本问题上影响尤其明显。2.3 指标用 MAPE、RMSE 还是 SMAPE人口预测中的选择人口预测常用的三个指标各有各的盲区。RMSE 的单位是“人”能直观告诉你平均差了多少人但它对极端年份比较敏感一个行政区划调整造成的异常跳变会把整体误差拉高。MAPE 是相对误差适合向业务方解释“预测偏差百分之几”但当真实人口值很小、基数低的时候分母小了误差会被放大成看似很糟糕的结果。更稳妥的是同时看两个层面整体误差看 RMSE 或 MAPE单点极端误差看最大偏差出现在哪一年并用 SMAPE 做对称化处理。SMAPE 把真实值和预测值都放进分母避免 MAPE 在低基数年份被放大。人口预测项目里我一般以最近 5 年的 MAPE 作为主指标因为远期误差必然累积只报全周期平均会掩盖“越预测越偏”的趋势。十次有八次模型调参半天不如检查指标怎么算的来得有效。固定一个 80/20 的时间顺序切分然后用 MAPE 对比 LSTM 和“未来五年均值”这种基线如果 LSTM 连均值基线都没跑赢说明网络根本没学到人口变化的增量信息。3. 基于 LSTM 的城市人口预测代码实现从 CSV 到未来五年预测曲线3.1 读取人口数据并补齐缺失年份先准备一份干净的 CSV常见格式是两列year和population年份按升序排列。我先用 pandas 读进来转成时间索引再检查缺失值和排序这一步比后续调参重要得多。import numpy as np import pandas as pd df pd.read_csv(city_population.csv) df[year] pd.to_datetime(df[year], format%Y) df.set_index(year, inplaceTrue) df df.sort_index() print(df.isnull().sum()) print(df.shape) # 缺一两年用线性插值补齐缺太多则考虑分段建模 if df[population].isnull().any(): df[population] df[population].interpolate(methodlinear)年份转成 datetime 索引是为了后续画图、切分都方便。interpolate只适合缺失一两年且前后趋势连续的情况中间断档超过三年插值出来的数据就是编故事不如把序列在断点处拆开。人口序列一般是逐年记录样本量不大处理完这一步把population转成 numpy 数组准备构造样本。3.2 构造监督学习样本look_back 窗口怎么定LSTM 不能直接吃一整条序列需要把时序变成“用过去 N 年预测接下来 1 年”的监督学习样本。这里的 N 就是look_back窗口。窗口值直接决定模型能看到多远的过去比 LSTM 的units还值得先定。def build_samples(series, look_back10): X, y [], [] for i in range(len(series) - look_back): X.append(series[i:i look_back]) y.append(series[i look_back]) return np.array(X), np.array(y)窗口太短比如 3 到 5模型只能看到最近的波动学不到人口滞后效应窗口太长比如 30样本数量会大幅缩水。一般 40 到 60 多年的数据取 10 到 15 是比较均衡的选择。函数返回的 X 是 (样本数, look_back, 1) 之前的二维数组接下来需要 reshape 成 LSTM 要求的三维输入。look_back 10 train_scaled train_scaled.reshape(-1) X_train, y_train build_samples(train_scaled, look_back) X_test, y_test build_samples(test_scaled, look_back) X_train X_train.reshape((-1, look_back, 1)) X_test X_test.reshape((-1, look_back, 1))注意test_scaled在进入build_samples之前已经是独立缩放过的数据长度比原始测试集短look_back年因为前look_back年只能作为输入窗口、没有对应标签。训练集同理最终参与训练的有效样本数是“总年数减去 look_back 再做切分”这个数据量心里要有数。3.3 搭建两层 LSTM 网络结构、dropout 与早停参数人口数据量不大网络不需要堆得很深。两层 LSTM 加 dropout 已经能覆盖大多数情况第一层返回完整序列让第二层继续提炼时间依赖最后一层接 Dense 输出单一数值。from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout from tensorflow.keras.callbacks import EarlyStopping model Sequential([ LSTM(64, return_sequencesTrue, input_shape(look_back, 1)), Dropout(0.2), LSTM(32), Dropout(0.2), Dense(1) ]) model.compile(optimizeradam, lossmse)第一层return_sequencesTrue是两层 LSTM 正确堆叠的前提写成 False 第二层只会拿到单个输出向量维度对不上。dropout 在人口小样本上必须开否则很容易过拟合到训练集的曲线细节上验证集误差会明显高于训练集。batch_size设小一点比如 8 到 16因为年数据本质上样本数很少大批量会让梯度更新过大、训练不稳定。训练时用早停卡验证集损失防止模型在训练集上反复磨细节early_stop EarlyStopping( monitorval_loss, patience50, restore_best_weightsTrue ) history model.fit( X_train, y_train, validation_split0.15, epochs500, batch_size8, callbacks[early_stop], verbose1 )validation_split0.15是在训练集末尾再切一段做验证严格按时间顺序不随机打乱。patience50意味着验证损失连续 50 轮不下降就停配合restore_best_weights把最优权重找回。人口预测特征少模型训练很快早停参数不需要过于激进给足 50 轮耐心反而更容易学到趋势而不是噪声。3.4 预测未来五年并反归一化递归输出与误差观察模型训练完之后要做的是把训练集最后look_back年作为种子一步步往下预测。每一步把预测值拼回窗口尾部丢掉最前面的旧年份形成递归预测。future_steps 5 seed train_scaled[-look_back:].reshape(1, look_back, 1) future_scaled [] for _ in range(future_steps): next_value model.predict(seed, verbose0)[0, 0] future_scaled.append(next_value) seed np.concatenate( [seed[:, 1:, :], np.array([[[next_value]]])], axis1 ) future_values scaler.inverse_transform( np.array(future_scaled).reshape(-1, 1) ).reshape(-1) print(future_values)这段代码的关键在seed更新的写法seed[:, 1:, :]丢掉第一个时间步然后拼接上刚预测出的值窗口长度始终保持look_back。反归一化用训练时的scaler做inverse_transform此时预测值已经回到“人”这个量纲才能和真实年鉴数据对比。评估时不要只画曲线要把测试期真实值和预测值逐个对应算误差from sklearn.metrics import mean_absolute_error pred_scaled [] seed train_scaled[-look_back:].reshape(1, look_back, 1) for real_value in test_scaled: p model.predict(seed, verbose0)[0, 0] pred_scaled.append(p) seed np.concatenate( [seed[:, 1:, :], real_value.reshape(1, 1, 1)], axis1 ) pred_values scaler.inverse_transform( np.array(pred_scaled).reshape(-1, 1) ).reshape(-1) real_values scaler.inverse_transform(test_scaled).reshape(-1) print(MAE:, mean_absolute_error(real_values, pred_values))这一段测试时用的是真实值做回填而不是把模型自己的预测继续喂回去。这种方式叫单步滚动评估能反映出模型“每一年看到真实人口后修正一次”的表现适合检验模型本身有没有学到规律。真正预测未来时没有真实值可回填才用上一小节的纯递归模式。4. LSTM 人口预测避坑指南5 个反复出现的翻车现场4.1 预测曲线整体右移一个时间步现象画出来的预测曲线和真实曲线形状几乎一致但整体错开了一年看起来像把真实曲线往右推了一格。很多新手以为这是预测提前或滞后实际是模型在专门偷懒。原因单步回归问题里上一年的数值本身就是一个极强的输入特征。人口变化平缓时复制上一年值就能把损失降到很低模型学到的最优策略就是恒等映射根本没去学趋势和拐点。这种情况在人口这类强惯性序列上发生率极高。解决不要只看曲线趋势去看预测值之间的一阶差分。如果模型输出的差分值几乎接近零说明它在复制输入而非预测变化。更直接的办法是用一阶差分序列作为训练目标也就是把连续两年人口的差值当作标签LSTM 只负责学“变化量”最后再把差值累加回真实值这样模型没法靠复制输入蒙混过关。4.2 在全量数据上做归一化导致数据泄漏现象单独看训练集误差不大但等到预测未来年份时输出一直落在历史数据的量级范围内尤其当未来人口突破了历史最大值时预测值会被压制在归一化上限以下。原因把整条人口序列放进MinMaxScaler再切分训练集和测试集测试集的最大值已经写进归一化器的scale_和min_里。模型预测未来时反归一化出来的值永远不可能超过当年历史最大值等于提前把未来上限暴露给了模型。解决严格按时间切分先train_raw做fit_transform再对test_raw和未来输入只做transform。如果发现未来预测值被顶在某个边界上第一反应就是去检查scaler是何时 fit 的这几乎是人口预测里最常见的隐性错误。4.3 预测结果是一条平直线现象训练过程正常损失也在下降但预测未来五年输出是一条水平线数值接近训练集均值完全看不出趋势。原因模型学到了一个“输出均值”的安全策略。人口曲线变化平缓时预测均值比预测任何波动都更不容易出错LSTM 的激活函数把输出压在一个区间内如果信号本身不强网络倾向于收敛到一个常数上。学习率过大、dropout 过高也会加剧这个问题但根本原因还是增量信息太弱。解决先用未来五年均值作为基线如果 LSTM 连基线都没超过就说明模型结构没有学到趋势信息。调整方向有三个把训练目标从绝对人口改成差分变化量、降低units减少模型自由度、把epochs给足并设置较小的learning_rate。另外检查一下归一化后的训练集方差如果所有值都在 0.4 到 0.6 之间模型很难从这么窄的区间提取有效规律这时差分处理比调结构更优先。4.4 多步预测后期误差急速放大现象第 1 年预测误差很小第 3 年开始明显跑偏到第 5 年已经偏离真实趋势误差呈现递增放大的喇叭状。原因递归预测时每一步预测误差都会作为下一步的输入误差沿着时间方向累积。早期预测偏高一点后面年份就会一直带着这个偏高记忆越滚越大。这不是 LSTM 特有的问题任何递归式多步预测都存在误差漂移。解决限制递归步数单次最多预测 3 到 5 年更长的预测用滚动回测不断修正。另一个做法是改用多步直接预测把模型的输出维度设计成未来 5 年的五个值用Dense(5)一次性输出这样误差不会逐年传递。实际项目里我很少做超过五年的递归预测因为城市规划通常以五年为一个规划周期三五年足够支撑业务决策。4.5 用打乱顺序的交叉验证评估时序模型现象使用 K 折交叉验证、随机抽取测试集后模型在验证集上表现不错但一放到真实滚动预测里就偏离明显。原因人口序列的时间顺序本身就是信息。随机打乱后训练集里混入了未来年份模型提前看过答案预测时真正面对的未来年份不在历史分布内自然全线翻车。解决评估时序模型只能用时间顺序切分不能随机 K 折。常见做法是扩展窗口回测固定训练起始点逐步加入新数据每次只预测下一个年份得到一组连续评估误差。这种方法比单次切分更能反映模型在真实场景中的稳定性对人口这类低频序列尤其友好。5. 让预测结果更可信的进阶验证滚动回测与多步输出5.1 滚动回测每来一年真实数据就重训一次单次 80/20 切分只能得到一条误差曲线不能证明模型在不同时期都稳定。我一般会再做一轮滚动回测从历史 70% 的位置开始逐年扩展训练集每扩展一年重训一次模型预测下一年真实值。这个循环在人口数据上成本很低样本小、训练快却能暴露模型在趋势拐点上的真实表现。def build_lstm_model(look_back10): model Sequential([ LSTM(64, return_sequencesTrue, input_shape(look_back, 1)), Dropout(0.2), LSTM(32), Dropout(0.2), Dense(1) ]) model.compile(optimizeradam, lossmse) return model actuals, predictions [], [] start_idx int(len(population) * 0.7) for t in range(start_idx, len(population)): train_block population[:t].reshape(-1, 1) scaler MinMaxScaler() train_block_scaled scaler.fit_transform(train_block).reshape(-1) X, y build_samples(train_block_scaled, look_back10) model build_lstm_model(look_back10) model.fit(X, y, epochs100, batch_size8, verbose0) seed train_block_scaled[-10:].reshape(1, 10, 1) p model.predict(seed, verbose0)[0, 0] predictions.append(scaler.inverse_transform([[p]])[0, 0]) actuals.append(population[t])这个循环里每个t都是一个新的训练起点scaler也要重新 fit保证任何一步都不会用到t之后的数据。最终得到一条逐年预测误差序列比单次切分更能说明模型是否值得上线。缺点是一次运行要训练几十次模型但人口样本量小训练速度完全能接受。对比单次切分和滚动回测的误差曲线后如果后者误差明显更大说明模型对训练集尾部的数据段特别敏感这时要回到数据预处理环节重新检查而不是继续调网络结构。滚动回测的价值就是逼你面对真实场景中的预测难度。5.2 多步预测输出与业务使用边界滚动回测确认模型稳定之后再用递归方式预测未来五年。我会把输出结果分成两列基准值和一个合理波动区间区间宽度参考最近五年的绝对误差来设。人口预测不该给出一个精确的单一数字因为行政区划调整、产业政策这些外生因素无法从历史序列中学到。我习惯把 LSTM 的结果当作“无重大政策扰动”下的自然增长曲线再结合规划部门给出的新城区人口导入目标做一高一低两个情景。业务决策里基础设施容量按照区间上沿来预留财政投入按照基准值来测算这样既不为极小概率事件过度投资也不至于漏掉明显的增长空间。用这套滚动回测加多步输出的流程做过几个不同城市的人口预测模拟之后我再看到任何“效果惊艳”的单次预测曲线都会先问一句它的评估方式是不是时间顺序切分。这个习惯帮我避开了不少不靠谱的模型方案。数据量小、趋势强、突变多是城市人口预测的三个天然难点LSTM 能解决的是前两个第三个要靠业务经验去修正。希望帮到你。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑