
简介这是一份基于深度学习的纽约出租车流量预测项目面向人工智能期末大作业与课程设计场景覆盖长短时记忆网络、门控循环单元、卷积长短时记忆网络和卷积门控循环单元等常见模型。代码添加了详细注释结构清晰新手也能快速读懂属于个人手打且获导师认可的高分项目可为后续实验提供可靠参考。资源包共31个文件包含Python源码、Pyc编译文件、XML工程配置、训练数据文件、可视化结果图以及说明文档等整体压缩包仅1.21MB轻量易部署。目前已有355人学习浏览适合期末大作业、课程设计以及深度学习入门实践使用。压缩包内附有数据说明文档提供数据加载、模型训练与测试的完整代码并包含各模型训练过程的评估曲线图便于直观比较效果也可作为时空预测类任务的参考框架。1. 纽约出租车流量预测本质不是“预测”而是“序列构造”拿到“基于深度学习的纽约出租车流量预测”这个题目的人第一反应往往是去纠结模型是LSTM还是Transformer要不要上注意力机制。以我带大作业的经验看翻车的几乎全在数据侧时间没对齐、训练集混进测试集、深夜数据被当成噪声删掉模型再先进也救不回来。这个项目要做的事很具体把纽约出租车历史行程记录清洗、聚合成等间隔的流量时间序列再用LSTM这类深度学习模型学习它的周期规律输出未来一个或多个时间片的流量。它适合人工智能大作业选题也适合想从MNIST分类走向真实时序预测的初学者练手。整条链路不依赖显卡一台能跑Python的笔记本就能做完核心难点在数据处理不在模型。2. 先做数据把几百万条行程记录变成一条能训练的时间序列2.1 流量怎么定义全城聚合还是网格聚合标题写的是“纽约出租车流量预测”但“流量”这个词在不同人手里含义完全不同动手前必须把这个定义钉死。常见做法有两种。第一种是聚合到全城把每个小时内整个纽约市的上车记录数统计出来得到一条一维序列。优点是序列平滑、周期清晰、模型简单几百行数据就能训练适合把主干流程先跑通。第二种是切网格按经纬度把城市切成分块统计每个网格每小时的上车数得到二维数组。这种做法更接近“流量预测”的真实业务含义能看出拥堵在空间上怎么迁移但大部分网格在深夜是零流量数据稀疏训练难度和展示成本都会高一个量级。我的建议很直接大作业优先做全城聚合把模型和数据链路先跑通如果学有余力再按纽约TLC官方划分的taxi zone挑出Top 10的热门区域做区域级预测。TLC的Yellow Taxi Trip Records是公开数据集字段包含上车时间、经纬度等。无论你手上拿到的“数据集”是原始行程记录还是已经聚合好的一列时间加一列数值第一步都建议还原成标准时间序列后面所有训练和评估都基于这个标准序列别边做边改数据结构。2.2 用pandas把行程数据聚合成小时级序列如果拿到的是原始行程记录聚合代码是这么写的import pandas as pd import numpy as np # 只读取必要字段几百万行CSV能省不少内存 df pd.read_csv( yellow_tripdata.csv, usecols[tpep_pickup_datetime, pickup_longitude, pickup_latitude, passenger_count], parse_dates[tpep_pickup_datetime], ) # 剔除GPS漂移纽约市核心区域大致在这个经纬度框里 df df[ (df[pickup_longitude].between(-74.05, -73.70)) (df[pickup_latitude].between(40.55, 40.95)) (df[passenger_count] 0) ] # 按小时向下取整统计每小时的上车总数 df[pickup_hour] df[tpep_pickup_datetime].dt.floor(h) hourly df.groupby(pickup_hour).size().rename(count) # 把缺失的整点补0保证时间索引连续无断裂 hourly hourly.asfreq(h, fill_value0) print(hourly.describe()) print(hourly.head())这里最关键的三个动作值得展开说。第一dt.floor(h)把每条上车记录归入它所在的整点这是聚合的基准第二asfreq(h, fill_value0)把序列里缺失的整点补成0因为凌晨某些小时可能真的没有记录不补的话后面构造滑动窗口时索引会断裂第三经纬度过滤是为了剔除GPS漂移点每年TLC数据的字段名略有差别早期版本是经纬度两列新版本部分换成了分区ID如果手上的数据只有分区ID这个坐标过滤可以跳过。从原始行程到可用序列这一步大概会占用整个项目三分之一的时间。大作业最常见的翻车方式就是在这里图快——不补缺失、不过滤异常点、直接用原始记录当特征后面模型训练时到处报错再回头排查浪费的时间远超省下的那几分钟。如果做网格聚合把上面代码的groupby换成两列分箱即可df[grid_x] pd.cut(df[pickup_longitude], bins40, labelsFalse) df[grid_y] pd.cut(df[pickup_latitude], bins40, labelsFalse) df[grid_id] df[grid_x] * 40 df[grid_y] grid_hourly df.groupby([pickup_hour, grid_id]).size().rename(count) grid_hourly grid_hourly.unstack(fill_value0)2.3 训练集、验证集、测试集必须按时间切不能随机切时序预测最容易被答辩老师抓的一个点就是数据切分方式。很多人习惯性地用train_test_split的默认参数随机打乱后切分这在分类任务里没错但在时序任务里是致命错误。# 错误做法随机切分会把11月的数据塞进训练集、1月的数据塞进测试集 # from sklearn.model_selection import train_test_split # X_train, X_test train_test_split(...#, shuffleTrue) # 正确做法按时间先后硬切模拟“用过去预测未来”的真实场景 series_len len(scaled_train) cutoff_val int(series_len * 0.7) cutoff_test int(series_len * 0.9) train series[:cutoff_val] val series[cutoff_val:cutoff_test] test series[cutoff_test:]随机切分的风险在于出租车流量同时受日周期和星期周期影响随机切会让测试集里有大量与训练集“同星期同时刻”的样本模型等于提前偷看了答案测试集指标会虚高。按时间切分后训练集是连续的过去测试集是完整的未来评估结果才相对可信。这一条在答辩时被问到的概率极高能把切分逻辑讲清楚的人在老师眼里比多调两个点的人靠谱得多。2.4 归一化只能fit训练集测试集只能transform归一化是另一个高发踩坑点。正确写法是from sklearn.preprocessing import MinMaxScaler scaler MinMaxScaler() # 先切分、后归一化scaler 只在训练集上 fit scaled_train scaler.fit_transform(train.reshape(-1, 1)).ravel() scaled_val scaler.transform(val.reshape(-1, 1)).ravel() scaled_test scaler.transform(test.reshape(-1, 1)).ravel()如果先对整个序列求fit再做归一化再切分测试集的最大值和最小值就会参与scaler的统计量计算模型在训练时就间接知道了测试集的范围这在时序预测里属于信息泄漏。这种现象在代码上不报错指标也不会难看但答辩时被追问“你的scaler是怎么做的”回答不上来会非常被动。记住一个原则fit永远只作用于训练段验证集和测试集只transform。3. 模型选型为什么大作业场景下首选LSTM而不是Transformer3.1 流量序列的特性决定了模型选择纽约出租车流量数据有三个明显特征强周期性、非平稳、有突发脉冲。凌晨3点接近零早高峰8点和晚高峰18点出现尖峰周末的周期又和工作日叠加在一起。ARIMA这类统计模型擅长捕捉线性自相关一旦碰上“周期叠加非线性”就容易失效而且对长序列的预测误差会快速累积。普通RNN理论上有时序建模能力但反向传播时梯度随时间步连乘容易梯度消失或梯度爆炸时间一长就学不动。LSTM通过输入门、遗忘门、输出门和细胞状态让信息可以选择性穿越多个时间步本质上是给网络一个“记忆槽位”。流量预测需要模型记住“昨天这个时候大概有多少单”这种跨天记忆单靠最近几个时刻的数值远远不够这正是LSTM适合这个任务的原因。Transformer不是不行而是在大作业的量级上没有性价比。它的优势体现在长序列和大规模并行上而这个项目通常只有几千到几万条样本序列长度也就24到168Transformer的优势发挥不出来反而要做位置编码、调注意力头数调试成本高出一截。大作业的目标是用最少的时间把完整链路跑通并讲清楚LSTM是那个最稳的起点。3.2 窗口构造用过去24小时预测未来1小时深度学习时序预测的第一步是把序列切成“窗口-标签”对。input_len24表示用过去24小时的流量预测下一小时的流量output_len1表示单步预测。def make_sequences(data, input_len24, output_len1): X, y [], [] for i in range(len(data) - input_len - output_len 1): X.append(data[i : i input_len]) y.append(data[i input_len : i input_len output_len]) return np.array(X), np.array(y) X_train, y_train make_sequences(scaled_train, input_len24, output_len1) X_val, y_val make_sequences(scaled_val, input_len24, output_len1) X_test, y_test make_sequences(scaled_test, input_len24, output_len1) # LSTM 输入格式要求 [样本数, 时间步数, 特征数] print(X_train shape:, X_train.shape) # (样本数, 24, 1) print(y_train shape:, y_train.shape) # (样本数, 1)make_sequences用了一个简单的for循环做滑窗切片[i : i input_len]是窗口内的连续24个点y是紧随其后的1个点。这个函数的改动空间很大如果你想预测未来24小时把output_len改成24即可如果想把窗口扩大到一周把input_len改成168模型就有机会学到上周同一时刻的基线。窗口大小的确定原则是短窗口12~24轻量但学不到周周期长窗口168信息更全但训练更慢、对显存或内存要求更高。大作业从24起步最合理。3.3 最小可用的双层LSTM结构模型结构我一般用两层LSTM加Dropoutfrom tensorflow import keras from tensorflow.keras import layers model keras.Sequential([ layers.LSTM(64, return_sequencesTrue, input_shape(24, 1)), layers.Dropout(0.2), layers.LSTM(32, return_sequencesFalse), layers.Dropout(0.2), layers.Dense(1), ]) model.compile( optimizerkeras.optimizers.Adam(learning_rate1e-3), lossmse, metrics[mae], ) model.summary()三个参数值得单独说。return_sequencesTrue让第一层输出完整的隐藏状态序列给第二层堆叠LSTM能逐层抽象出更高层的时间模式units64和units32是这个数据量下比较稳的量级几万样本配几百个神经元已经足够再大就会开始过拟合Dropout放在LSTM层之间位置在return_sequences之后而不是放在LSTM内部——内部用recurrent_dropout会显著拖慢训练速度大作业没必要。损失函数用mse是因为MSE对大误差惩罚更强能推动模型把高峰时段预测准mae加进去是为了后续评估时直接用人类可读的单位。这个模型在CPU上训练几万条样本每个epoch大概几秒到十几秒完全不需要GPU。如果手上机器比较老把第一层units改成32训练时间能再降一半精度损失很小。4. 训练、评估和能直接写进说明文档的三张图4.1 用EarlyStopping和ReduceLROnPlateau管理训练过程训练时序模型最怕两件事loss还没收敛你就手动停了或者已经过拟合了你还在傻跑。两个回调函数可以解决callbacks [ keras.callbacks.EarlyStopping( monitorval_loss, patience10, restore_best_weightsTrue, ), keras.callbacks.ReduceLROnPlateau( monitorval_loss, factor0.5, patience4, min_lr1e-5, ), ] history model.fit( X_train, y_train, validation_data(X_val, y_val), epochs100, batch_size64, callbackscallbacks, verbose1, )EarlyStopping是时序预测的后悔药验证集loss连续10个epoch没有刷新最低值就自动停restore_best_weightsTrue让模型回滚到验证集最优的那个权重而不是停在最后一个epoch——后者往往是过拟合后的状态。ReduceLROnPlateau在loss进入平台期时把学习率减半比手动改learning_rate省事。epochs100只是上限加上早停后实际训练通常30到50轮就停。这两个回调组合起来基本能保证你睡觉的时候训练也不会跑飞。4.2 评估不能只看loss必须和“昨天同时刻”这个基线对比模型训练完先反归一化回到真实“每小时上车数”的单位再算MAE。但单独一个MAE数值没有任何说服力你得让老师看到一个参照系——用“昨天同一时刻的真实值”做预测这个朴素基线在流量预测里非常强。from sklearn.metrics import mean_absolute_error # 反归一化回到真实单位 y_test_real scaler.inverse_transform(y_test.reshape(-1, 1)).ravel() pred_real scaler.inverse_transform( model.predict(X_test).reshape(-1, 1) ).ravel() mae_model mean_absolute_error(y_test_real, pred_real) # 基线预测直接用24小时前的真实值当预测值 if len(y_test_real) 24: base_mae mean_absolute_error(y_test_real[24:], y_test_real[:-24]) else: base_mae float(nan) print(fLSTM MAE: {mae_model:.1f} 次/小时) print(fNaive(昨天同时刻) MAE: {base_mae:.1f} 次/小时)这个基线的逻辑是既然流量有日周期我不训练任何模型直接用昨天同一时刻的值作为今天的预测效果不会差。如果LSTM的MAE只比这个基线低一点点说明模型学到的主要是“复读”而不是真正的规律。以全城小时级流量为例Naive基线的MAE大概在几十次上下LSTM通常能再压掉20%到40%。报告里放这张对比表比单独贴一条loss曲线有力得多。4.3 报告里最有价值的图预测值与真实值的重叠曲线答辩时老师最爱看的一张图是连续一段时间内预测曲线和真实曲线的重叠情况。取测试集前120个小时画图横轴是小时纵轴是每小时上车数。import matplotlib.pyplot as plt n 120 plt.figure(figsize(12, 4)) plt.plot(y_test_real[:n], labelground truth, alpha0.8) plt.plot(pred_real[:n], labelLSTM prediction, alpha0.8) plt.legend() plt.xlabel(hour in test set) plt.ylabel(pickup count) plt.title(LSTM vs Ground Truth (test set)) plt.savefig(pred_vs_true.png, dpi150, bbox_inchestight)这张图的信息量很大。第一看相位偏移如果预测曲线整体比真实曲线向右滞后一两小时说明模型过于依赖“最近时刻”的输入对周期记忆利用不足这时可以尝试加大input_len。第二看峰值拟合如果早晚高峰的尖峰被压平了说明MSE对极端值过度惩罚模型选择了“均值回归”这种保守策略可以换成Huber损失试试。第三看夜间区间如果深夜段预测基本是一条平线不用慌这是基数太小导致的正常现象具体见下一章。4.4 训练参数建档说明文档里最值钱的一张表大作业的“说明”部分很多同学把它写成了环境安装教程罗列python版本和依赖包名称这其实是最不值钱的。老师想看到的是一张能复现你实验的参数表最好画成表格放在报告第一节参数项取值设定原因数据时间范围以实际读取为准说明数据覆盖的时间段聚合粒度1小时平衡样本量与周期可见性序列长度 input_len24覆盖日内周期标签长度 output_len1单步预测起步版本LSTM 单元数64 / 32小数据量防过拟合Dropout0.2增加泛化能力优化器 / lrAdam / 1e-3时序任务默认配置batch_size64梯度方向稳定且占用小训练轮数实际早停轮数由EarlyStopping决定test MAE填写实际数值与Naive基线对比这张表的关键在于“设定原因”这一列。每一项都能答出“为什么是这个值”比单纯罗列代码要强很多。数据来源、坐标过滤规则、切分比例也建议写进说明文档的数据章节这三个点基本覆盖了答辩时数据侧的所有提问。5. 避坑手册五次翻车换来的五条血泪经验5.1 测试集MAE远低于训练集别高兴——大概率是信息泄漏现象测试集MAE低到离谱甚至比训练集还低曲线图画出来预测值和真实值几乎重叠。原因最常见的两种一是先对整个序列做归一化再切分导致测试集的统计信息被模型偷看二是构造滑动窗口时窗口跨越了训练集和测试集的分界线训练样本里包含了测试期的数据点。此外随机切分也会造成时间泄漏。解决严格按“先切分、后归一化、再构造窗口”的顺序执行。scaler只在训练段上fit特征构造用make_sequences时对训练段、验证段、测试段分别独立调用保证任何训练样本的窗口都不包含验证或测试期的时间步。5.2 深夜时段预测值像一条平线不一定是模型坏了现象从下午6点开始误差很小但凌晨0点到5点的预测几乎恒定在某个低值分段看MAE时深夜高得吓人。原因深夜上车数基数太小凌晨3点曼哈顿每小时可能就几十单绝对误差当然低不到哪去。这是数据分布决定的不是模型缺陷。更关键的是深夜方差大——某天凌晨有一班飞机集体落地流量瞬间翻倍这种突发脉冲谁都难预测。解决评估时分时段统计白天的相对误差和深夜绝对值分开报告。报告里主动写一句“深夜时段受基数影响绝对误差偏高但实际业务意义有限”这比假装没看见强得多。答辩时主动讲这一条反而说明你理解数据分布。5.3 把日期字符串直接塞进特征数组模型loss不下降现象有人把pickup_datetime列转成字符串塞进训练特征结果模型loss在某个值附近来回震荡怎么调都下不去。原因模型根本没法消费字符串即使转成数值直接喂“0到23的整数”也是在误导模型——模型会把19点和8点当成数值距离11的两个点而不是周期上首尾相接的两个点。流量是强周期信号怎么编码时间特征直接决定模型能否理解“周期性”。解决用正弦余弦做周期编码而不是用原始整数。import numpy as np # 示例生成168个小时的时间戳编码小时和星期几 t pd.date_range(2024-01-01, periods168, freqh) hour_sin np.sin(2 * np.pi * t.hour / 24) hour_cos np.cos(2 * np.pi * t.hour / 24) week_sin np.sin(2 * np.pi * t.dayofweek / 7) week_cos np.cos(2 * np.pi * t.dayofweek / 7)这样0点和23点在编码空间里是相邻的周一和周日也相邻模型不需要自己硬学“23后面接0”这种周期边界。单变量序列预测可以不加这些只要你想加入时间上下文就必须用这种编码方式。5.4 batch_size太大模型只学到平均行为现象loss曲线很平滑训练过程看起来很顺利但最终MAE比Naive基线只低一点预测曲线被严重“钝化”。原因batch_size256甚至512时一个batch里混入了不同星期、不同时段的样本梯度方向被彼此抵消模型只能学到“平均流量”这种粗粒度规律学不到日内波峰波谷的细节。解决对于几千到几万条样本batch_size取32或64最稳。从64开始调如果验证集MAE不理想往32降而不是往128升。这是个小参数但影响非常直接。5.5 预测曲线整体被压扁峰谷全被磨平现象画出来的预测曲线像被压过的弹簧高峰期预测值明显低于真实值低谷期又高于真实值。原因MSE损失对峰值误差施加了过大的惩罚模型为了压低总loss会选择输出一个“中间地带”的保守估计另一个常见原因是输出层误加了激活函数比如sigmoid把无界的“次/小时”输出强行限制到了0到1区间。解决回归任务的输出层保持线性不加任何激活函数如果把MSE换成Huber损失对离群点更稳健峰值的拟合通常会改善。在Keras里一行搞定model.compile(losskeras.losses.Huber(delta1.0), ...)。先检查激活函数再尝试换损失这两个改动通常能解决80%的“压扁”问题。6. 多步预测与网格热力图把大作业从合格拉到优秀的具体做法答辩前的最后一晚可以做一个代价很小的升级把模型从“预测下一小时”改成“预测未来24小时”。工程上有两种策略。递归预测是把模型自己的输出拼回窗口末尾当作下一轮的输入循环24次直接多步预测则是把输出层改成Dense(24)一次吐出24个值。两者对比本身就是很好的答辩素材。def predict_future(model, init_window, steps24): window init_window.copy() # 形状 (1, input_len, 1) preds [] for _ in range(steps): p model.predict(window, verbose0)[0, 0] preds.append(p) # 窗口左移一位把新预测值塞到末尾 window np.roll(window, -1, axis1) window[0, -1, 0] p return np.array(preds)np.roll是关键它把整个窗口沿时间轴左移一格最老的值被丢弃新预测值补到末尾。递归预测的问题在误差累积——第24步的误差大概率比第1步大所以评估时要分步看MAE别只报第24小时的数。再往上走一步就是把全城序列换成网格序列经纬度各切几十份让模型输入从(24, 1)变成(24, K)K表示网格数每个时间步输入的是整个城市各网格的流量向量。这样模型能同时学到空间相关性和时间连续性答辩时讲的东西从“一条曲线”升级成“一张热力图在脸上”。我个人的习惯是每个模型训完都做一次滞后检验把预测序列平移0到6小时后与真实值算相关系数如果峰值相关性出现在滞后2步而不是0步说明系统存在系统性滞后这比看loss曲线更能提前暴露窗口设计的问题。这个习惯让我少走弯路省下的时间都花在了更有价值的地方。希望帮到你。本文还有配套的精品资源点击获取