
简介面向人工智能课程大作业与期末设计场景这份基于深度学习的纽约出租车流量预测项目提供了完整可运行的实现代码。项目以纽约出租车流量数据为对象覆盖数据加载、模型构建、训练评估与结果可视化等核心环节包含长短期记忆网络、门控循环单元、卷积神经网络结合长短期记忆等多种时序模型代码附有详细注释降低上手门槛便于新手借鉴并快速部署。资源共31个文件以源代码为主辅以训练数据集、数据说明文档、训练效果图以及配置文件等类型划分清晰压缩包整体仅1.21MB轻量便捷。目前已有355人学习下载适合需要完成深度学习课程设计、期末大作业或入门时序预测任务的读者参考也可作为搭建同类流量预测项目的模板。1. 纽约出租车流量预测为什么这是人工智能大作业里性价比很高的选题基于深度学习实现的纽约出租车流量预测是很多人在人工智能大作业里一眼相中、又容易选完就后悔的题目。数据公开、场景直观你只要拿到一份按月打包的纽约出租车行程记录清洗后聚合成小时级流量再用 LSTM 这类序列模型去拟合就能得到一条能画进报告里的预测曲线。这个选题适合刚走完深度学习入门流程、还在被 Python 环境配置折磨的新手也适合想要一份“代码说明数据集”完整方案去改造交差的人。真正做下来会发现难点不在模型而在数据处理和评估时能不能看穿“假预测”。2. 先把数据变成能喂给深度学习的流量序列从 TLC 原始行程表开始2.1 先想清楚你要预测的“流量”到底是什么标题里的“流量预测”如果从一开始就没定义清楚后面的模型结构都是在忽悠自己。很多作业里最大的问题是把出租车流量理解成了“一天发生了多少笔订单”然后直接拿原始行程记录去建模。那做出来的不是流量预测而是订单数量统计。一个正经的出租车流量预测预测的是“未来一个时间窗内某个地理范围内产生的上车订单数”。纽约出租车和轿车委员会TLC公开的黄色出租车行程记录每一行是一次实际行程里面有实际上车时间tpep_pickup_datetime、实际下车时间tpep_dropoff_datetime还有上下车经纬度、金额、里程等字段。我们要做的第一件事就是把几十万行行程记录变成一条固定间隔的时间序列。时间粒度的选择会直接影响模型复杂度和答辩观感。我一般建议大作业选小时级一小时一个采样点每天 24 个点一个月 720 个点左右。小时级流量本身就带明显的早高峰、晚高峰形状模型容易学出规律报告也画得出来。如果选分钟级凌晨时段会有大量值为 0 的样本LSTM 很容易学到“一直输出 0”这种无聊解如果选天级一个月只有 30 多个点深度学习还没开始就结束了。所以默认取小时除非老师明确要求其他粒度。数据集的获取也简单TLC 官方页面按月打包文件名一般是yellow_tripdata_2023-01.parquet这种形式新的月份用 parquet老一些的数据可能是 csv。拿到手先不要急着建模打印出列名看看再确认时间字段到底叫tpep_pickup_datetime还是pickup_datetime。这个细节非常容易被忽略我见过太多人因为列名不一样卡在读数据阶段半天。2.2 读取数据时间字段清洗、距离过滤和坐标异常剔除拿到一个月的数据做一个最小可用的清洗流程。这里的思路是只保留和流量计数相关的字段把不合理的行程记录删掉防止脏数据把小时统计带偏。import pandas as pd # 读取 TLC 黄色出租车行程数据按月文件 df pd.read_parquet(yellow_tripdata_2023-01.parquet) # 只保留后续聚合需要的时间与坐标列 df df[[tpep_pickup_datetime, tpep_dropoff_datetime, pickup_latitude, pickup_longitude, trip_distance, passenger_count]] # 记录原始条数 raw_count len(df) # 时间字段统一转成 datetime df[tpep_pickup_datetime] pd.to_datetime(df[tpep_pickup_datetime]) df[tpep_dropoff_datetime] pd.to_datetime(df[tpep_dropoff_datetime]) # 剔除时间逆序、距离小于等于 0 的脏记录 df df[df[tpep_dropoff_datetime] df[tpep_pickup_datetime]] df df[df[trip_distance] 0] # 剔除坐标明显不在纽约范围的异常点 df df[(df[pickup_latitude] 40.5) (df[pickup_latitude] 41.0)] df df[(df[pickup_longitude] -74.3) (df[pickup_longitude] -73.6)] print(f原始记录 {raw_count} 条清洗后 {len(df)} 条)这里的坐标上下界取的是纽约都会圈大范围目的是去掉坐标为 0、坐标漂移到海上的坏点。需要注意有些年月的 TLC 数据对经纬度做了偏移脱敏位置不会精确到街道但不影响按时间聚合流量。trip_distance 0能过滤掉被取消但还留在文件里的异常行程。如果你只预测全城总流量不按区域拆坐标过滤不是必需的但我习惯保留因为坐标字段偶尔会出现 0 值不处理会有几分钟的订单被意外计到异常区。2.3 聚合成小时级流量序列并按时间顺序拆训练集和测试集清洗完成后核心动作是把“一行一条行程”变成“一小时一个统计值”。原理很简单把每笔行程的上车时间向下取整到小时然后按小时计数。# 将上车时间按小时取整 df[pickup_hour] df[tpep_pickup_datetime].dt.floor(h) # 统计每个小时内产生的订单数 hourly df.groupby(pickup_hour).size().reset_index(namecount) hourly hourly.set_index(pickup_hour).sort_index() hourly hourly.resample(h).sum().fillna(0) # 按时间顺序切分不做随机打乱 train hourly.loc[:2023-01-20 23:00:00] test hourly.loc[2023-01-21 00:00:00:] print(train.shape, test.shape)这段逻辑值得多说两句。floor(h)会把 14:37:22 变成 14:00:00groupby之后就是每个小时的订单总数。为什么后面还要再resample(h).sum()因为原始表里可能存在完全没有订单的小时比如极端天气或系统故障这些时段在 groupby 结果里是缺失的。时间序列一旦缺孔后续滑窗构造会把缺失点当作普通值处理等于给模型喂了错位的“下一个值”。fillna(0)是把缺口补成 0表示那个小时没有车上客。拆分数据集必须按时间顺序。如果用了train_test_split默认随机模式测试样本会混进训练集时段相当于考试时提前看到了答案。这里选择前 20 天训练、后 11 天测试。有两点要提醒第一如果数据横跨多个周最好按整周切避免训练集全是工作日而测试集全是周末第二后续构造滑窗时训练集和测试集交界处会有窗口重叠严格说要砍掉look_back个点这个坑放到第 5 章展开。2.4 想升级成区域流量预测用经纬度粗分网格只预测全城总流量老师可能会说“看不出纽约特点”。一个折中方案是使用经纬度粗分网格把曼哈顿切成几个区域分别统计。比如按经度方向分成左中右、按纬度方向分成上中下形成 3×3 的网格把每笔行程的上车坐标映射到格子再按“格子 小时”聚合。# 定义曼哈顿范围的粗网格 lat_bins np.linspace(40.70, 40.88, 4) lon_bins np.linspace(-74.02, -73.90, 4) df[lat_bin] pd.cut(df[pickup_latitude], binslat_bins, labelsFalse) df[lon_bin] pd.cut(df[pickup_longitude], binslon_bins, labelsFalse) df[zone] df[lat_bin] * 3 df[lon_bin] zone_hourly df.groupby([pickup_hour, zone]).size().reset_index(namecount)这样就把“全城一条曲线”扩展成“9 个区域各自一条曲线”。但要注意格子太细会导致每个格子的流量大量为 0模型训练噪声非常大。我的经验是如果某个格子平均每天少于 50 单就合并到邻近区域不要硬预测。对大作业来说切成 4 个区域、训练 4 个 LSTM 已经足够体现“分区域”的工程意识后面的报告也更好解释。3. 用 LSTM 做预测模型滑动窗口、训练参数和一套能跑的代码3.1 为什么这个作业选 LSTM 而不是普通回归纽约出租车流量本质上是一条强周期时间序列。周一的早高峰和周末的聚会时段都遵循某种模式但如果用普通线性回归模型很难自动捕捉这种依赖。深度学习里的 LSTM 正好适合门控机制让它在时间步之间传递记忆看到“过去 24 小时”的变化趋势再预测下一个小时。相比 ARIMALSTM 不需要手工判断差分阶数也不要求序列平稳相比 MLPLSTM 输入天生带时间顺序模型结构就告诉它“先有 1 小时再有 2 小时而不是一堆无序特征”。当然LSTM 不是唯一选项。GRU 更轻训练更快Transformer 适合长序列但大作业里容易过拟合。我通常会选 LSTM 作为主模型在报告里补一句“对比了 GRU两者接近”这样既显得有工作量又不会把自己逼进太复杂的调参泥潭。对于答辩来说LSTM 是老师最熟悉的模型也是网上教程最多、最容易找到同款案例的模型。3.2 用滑动窗口把时间序列变成监督学习样本LSTM 的输入格式是(样本数, 时间步数, 特征数)所以不能直接把一整条时间序列喂进去要先切成重叠窗口。假设用过去 24 小时预测下一个小时那么第一组样本是 0 点到 23 点的流量标签是 24 点的流量第二组是 1 点到次日 0 点标签是次日 1 点依次滑动。import numpy as np # values 是从 pandas Series 转成的一维数组 values hourly[count].values.astype(float32) def make_sequences(values, look_back24): X, y [], [] for i in range(len(values) - look_back): X.append(values[i:i look_back]) y.append(values[i look_back]) return np.array(X), np.array(y) X_full, y_full make_sequences(values, look_back24) # 在样本层面继续按时间切分 split_index int(len(X_full) * 0.8) X_train, X_test X_full[:split_index], X_full[split_index:] y_train, y_test y_full[:split_index], y_full[split_index:] # LSTM 输入需要三维样本数、步长、特征数 X_train X_train.reshape((X_train.shape[0], X_train.shape[1], 1)) X_test X_test.reshape((X_test.shape[0], X_test.shape[1], 1)) print(X_train.shape, X_test.shape)look_back24是这里最核心的超参数。小时级数据取 24等于让模型看到昨天的同一时刻如果你的粒度是 15 分钟至少取 96也就是 24 小时。取太小模型看不到日周期取太大模型容易把随机噪声也记住。另外要留意滑动窗口得到的样本之间高度重叠所以训练集和测试集的切分点要留出多余窗口否则测试集最后几个样本会包含训练集末尾的数据。稳妥做法是在split_index前后各留出look_back个点只在中间区域做验证。3.3 LSTM 模型结构与训练参数设置模型不需要很深。一个带 64 个单元的 LSTM 加一个全连接层对小时级流量已经足够。下面是一段可以完整跑通训练的 Keras 代码from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense from tensorflow.keras.callbacks import EarlyStopping model Sequential([ LSTM(64, activationrelu, return_sequencesFalse, input_shape(24, 1)), Dense(32, activationrelu), Dense(1) ]) model.compile(optimizeradam, lossmse, metrics[mae]) early_stop EarlyStopping(monitorval_loss, patience5, restore_best_weightsTrue) history model.fit( X_train, y_train, validation_data(X_test, y_test), epochs50, batch_size32, callbacks[early_stop], verbose1 )几个参数的实际意义activationrelu在 LSTM 单元内部配合门控使用流量非负所以不会出现负数截断问题return_sequencesFalse表示只在最后一个时间步输出隐藏状态因为我们要的是“用整个窗口预测一个值”lossmse让模型更关注大误差但后续评估时我也会看 MAE。batch_size32是经验值一个月的数据量只有几百个样本太小容易抖动太大容易直接收敛到均值。epochs50配合patience5基本够用如果验证集 loss 连续 5 轮不降模型会自动回到最优权重这也是避免过拟合的后悔药。还有一个常被忽略的问题归一化。LSTM 用 MSE 做损失如果流量原始数值从 0 到几千梯度会非常不稳。常见做法是构造一个MinMaxScaler只对训练集做fit然后把测试集也放进去transform。这个环节放到了第 4 章和第 5 章具体展开因为一旦写错顺序评估结果就是假的。4. 评估阶段别自欺欺人MAE、RMSE 和“预测滞后”的检验4.1 为什么不能只看 loss流量预测要分时段看误差训练结束看到一个很低的 loss先不要高兴太早。MSE 对高峰时段的巨大误差非常敏感但低峰时段的误差会被平均效应藏起来反过来如果模型每小时的预测都偏低整体 MAE 也会显得很小。纽约出租车流量一天内的变化可以从凌晨的 200 单到晚高峰的 4000 单只看一个全局指标没有任何说服力。我的评估流程是固定的先反归一化得到真实流量单位再按小时分组统计误差看早高峰、晚高峰、凌晨三个时段的 MAE 分别多少。如果高峰时段的误差是平峰的 3 倍以上说明模型并没有真正学到峰值规律只是把曲线磨平了。作为人工智能大作业这个观察一定要写进报告否则老师只会觉得你在拿一个黑匣子交差。4.2 反归一化与指标计算的代码使用MinMaxScaler时要区分输入 X 和输出 y 两个缩放器。更关键的是fit只能发生在训练集范围内测试集只能transform。如果全量数据一起 fit测试集的 min/max 就提前进入了训练过程属于数据泄漏。from sklearn.preprocessing import MinMaxScaler # 对 y 单独做缩放只用训练集拟合 scaler_y MinMaxScaler().fit(y_train.reshape(-1, 1)) y_train_scaled scaler_y.transform(y_train.reshape(-1, 1)).ravel() y_test_scaled scaler_y.transform(y_test.reshape(-1, 1)).ravel() # 模型训练时使用 y_train_scaled预测后还原到真实数量级 y_pred_scaled model.predict(X_test) y_pred scaler_y.inverse_transform(y_pred_scaled).ravel() y_true y_test mae np.mean(np.abs(y_pred - y_true)) rmse np.sqrt(np.mean((y_pred - y_true) ** 2)) print(fMAE: {mae:.1f} 条/小时) print(fRMSE: {rmse:.1f} 条/小时)这段代码里scaler_y是专门为标签训练的缩放器。训练时传入y_train_scaled预测值和真实值比较前再做inverse_transform这样得到的 MAE、RMSE 单位就是“每小时订单数”答辩时可以直接和业务含义挂钩。比如全城小时流量在 2000 左右MAE 是 150说明平均每小时偏了 7.5%这属于可接受范围。但如果你是分区域预测单个区域的小时流量可能只有几十MAE 150 就完全不可用所以要再算分组指标。反归一化之外我习惯把误差按小时展开import pandas as pd test_index hourly.index[len(X_train) 24:len(X_train) 24 len(y_test)] df_eval pd.DataFrame({ hour: test_index.hour, true: y_true, pred: y_pred }) df_eval[abs_err] np.abs(df_eval[true] - df_eval[pred]) print(df_eval.groupby(hour)[abs_err].mean().round(1))这里test_index的构造逻辑是原序列索引中从训练样本结束位置往后偏移look_back个点才是第一个测试样本对应的预测时刻。如果你直接用test.index长度会和样本数不匹配。取hour字段做分组能看出误差是否集中在 7 点到 9 点、17 点到 19 点。4.3 画出预测曲线并用滞后检验判断模型有没有“作弊”最直观的评估始终是画图。画最后 72 小时的测试集真实值和预测值一眼就能看出问题。import matplotlib.pyplot as plt show 72 true_slice y_true[-show:] pred_slice y_pred[-show:] time_slice test_index[-show:] plt.figure(figsize(12, 5)) plt.plot(time_slice, true_slice, labelreal, linewidth1.5) plt.plot(time_slice, pred_slice, labelpred, linewidth1.5, linestyle--) plt.legend() plt.xlabel(time) plt.ylabel(pickup count / hour) plt.title(New York Taxi Pickup Prediction (last 72 hours)) plt.xticks(rotation30) plt.show()画完十有八九会看到预测曲线形态和真实曲线完全一致但峰值晚了一个小时这就是典型的“预测滞后”。为什么会出现因为单步预测模型发现“当前时刻的值最接近上一时刻的值”于是最安全的策略就是输出上一时刻的真实值。这在流量序列、股票序列、气温序列里都存在是结构性现象不是模型坏了。你的报告里可以明确写观察预测曲线与真实曲线的时滞为 1 小时推测由单步预测的损失函数偏向造成下一步计划采用多步预测或卷积输出层缓解。这样反而显得你懂评估。如果想量化滞后可以计算真实值右移 1 小时后与预测值的相关系数相关系数更高就证明滞后存在。5. 避坑指南纽约出租车流量预测里最常翻车的 5 个细节5.1 坑一把原始行程记录直接当作模型的输入序列现象我从不少作业代码里看到过同学把df读进来直接fit输入列包括上车时间、下车时间、坐标、金额。模型 loss 低得惊人但画出来的预测完全和流量对不上。原因原始行程每一行是一次独立事件不是时间序列。LSTM 要求输入是“时间步上的特征”把表格行当成连续帧模型学到的只是数据行数的波动而不是“时间窗内上客数”的变化规律。更严重的是时间戳列参与训练会让模型学到“下一行时间戳比这一行晚几秒”这对流量预测毫无意义。解决强制自己先把数据重采样成固定间隔的序列。聚合之后单独存成hourly.csv模型只从这个文件读。清洗阶段就把tpep_pickup_datetime以外的列删掉避免任何字符串或对象类型漏进模型。我自己的习惯是代码里加一行df df[[pickup_hour, count]]从源头挡住误用。5.2 坑二训练/测试切分时用了随机打乱现象用train_test_split(shuffleTrue)之后模型在测试集上误差极低但等拿到新月份数据预测时直接翻车。原因时间序列的 t 时刻和 t1 时刻高度相关随机打乱等于把未来的相邻样本放进了训练集。模型在测试集上看似表现好实际是记住了测试集样本的邻近点属于典型的数据泄漏。解决切分必须按时间顺序并且尽量留出窗口间隙。比如前 80% 训练后 20% 测试。构造滑窗样本后还要检查训练样本最后一个窗口与测试样本第一个窗口之间是否间隔至少一个look_back。如果你的答辩老师比较严格可以在报告里写“测试集由时间序列尾部独立构成训练集和测试集样本无重叠窗口。”这比任何模型创新都更能说明工程能力。5.3 坑三归一化时把整个数据集一起 fit现象用MinMaxScaler().fit(hourly[count].values.reshape(-1, 1))然后直接切分训练曲线非常好看但模型部署到新数据后预测值总是被压到一个很窄的区间。原因缩放器的min和max是在包含测试集的全量数据上计算的测试集的信息在训练阶段就暴露了。测试集一旦有极端峰值transform会把训练集大部分值压缩到 0.1 附近模型学到的是一个被“畸变”后的分布。解决严格分成两段先切分再对训练集fit然后用训练好的缩放器transform测试集和未来的真实输入。y 和 X 最好各用各的缩放器因为两者的量纲和分布不同。这个坑我几乎每次帮人看代码都会遇到属于大作业里最不值得丢的分。5.4 坑四预测曲线比真实曲线晚一个小时现象图里预测曲线和真实曲线形状完全一样但整体右移早高峰 8 点的峰值预测 9 点才出现。MAE 可能还不算差但现场画图会非常尴尬。原因这是单步预测在平滑序列上的固有时滞。模型用上一刻的真实值作为下一时刻的预测比去捕捉复杂的峰值形态更容易让损失下降。LSTM 并没有从逻辑上理解“这是早高峰”它只是学到了“这个时刻最接近刚才的时刻”。这个现象也出现在所有自回归式的流量预测作品里。解决在报告里主动讨论它。先算出平均时间滞后比如通过互相关函数找延迟步数然后指出这是单步预测的结构性特点不是模型失效。进一步的做法是改成多步预测让模型直接输出未来 24 小时序列滞后会明显降低。很多实际场景要求的就是未来多个时间窗单步预测本来就不完整所以这个坑也是你升华报告的机会。5.5 坑五只用全城总流量工作日和节假日差异被平均掉现象训练后模型对普通工作日预测效果还行但遇到周日晚间、节假日或突然下雨的日期预测值仍然是一根平滑曲线完全没有反应。原因全纽约小时流量序列里周一的早高峰、周五的晚高峰、周日的聚会时段差异很大。模型用全局统计规律去拟合每一个点遇到低频但强烈的特殊日就会自动忽略因为低频样本在损失里占比太低。解决作为大作业不需要追求绝对精确但至少要做特征工程。最简单的方法是给 LSTM 输入增加星期几特征比如用hourly.index.dayofweek归一化后作为第二个输入特征更进一步可以增加节假日标记甚至天气。常见做法是对特征做拼接在 LSTM 的输出层之后把星期几编码和预测值拼接再经过一个 Dense 层。这样模型虽然还是用历史流量预测却能区分周一和周六。答辩时这个点完全可以作为“创新性”来讲。6. 如果还想要更高分多步预测、外部特征和 baseline 对比6.1 把“预测下一个小时”升级成“预测未来 24 小时”单步预测在大作业里显得太单薄。一个自然的进阶是让模型直接输出未来 24 个值变成 sequence-to-sequence 任务。实现上不要马上追求 Transformer而是用 LSTM 的return_sequencesTrue输出每个时间步的预测或者用“迭代预测”把当前窗口扔进模型得到下一小时再把预测值拼到窗口尾部滑掉最老的值继续预测下下小时。后者实现成本低报告里也更容易解释。评估时画出“预测步长 vs RMSE”的曲线如果能展示前几小时误差平稳、越往后误差越大就很能说明模型的泛化边界。6.2 加入天气和节假日特征出租车流量和天气强相关这是纽约本地数据里一个常见但容易被忽略的因素。进阶做法是把温度、降雨量、是否节假日作为额外特征在滑窗的每个时间步上拼接。模型结构改成流量序列进入 LSTM外部特征在最后一个 LSTM 输出后与隐藏状态拼接再进入全连接层。这样不改变 LSTM 的时序结构又能让模型感知“今天下雨”或“今晚是圣诞节”。节假日特征可以直接从 Python 的holidays库或手动构建纽约公共假日列表。6.3 baseline 对比让报告说服力更强无论模型多简单都要放一个 baseline 对比。我常用的 baseline 是“昨天同一时刻预测”直接用t-24小时的真实值作为t时刻的预测值。这个 baseline 在强周期序列上效果并不差LSTM 只要比它好才能证明深度学习真的学到了东西。报告里放一张表格列三行昨天同时刻 baseline 的 MAE、LSTM 的 MAE、提升比例。这样评委一眼就能看出模型的增量价值。自己做过几次这类作业后我养成了一个习惯任何时间序列模型先跑一个最蠢的 baseline再谈深度学习。这样能帮我看清模型的真正能力而不是对着玄学般的低 loss 自我感动。这条经验也适用于你未来接手的各种预测题。如果你卡在数据处理或滞后解释上希望这篇里的坑和代码能让你少走一段路帮到你。本文还有配套的精品资源点击获取