简介:基于LSTM神经网络的道路通行时间预测完整源码与数据包,面向交通大数据、时序回归及深度学习初学者或竞赛复现者。方案以LSTM结合3层全连接层构建回归模型,将各link的历史旅行时间及其上下游关系作为特征,挖掘通行时间的复杂非线性影响机制,适合作为天池道路通行时间预测类赛题的入门基线。压缩包体积61.78MB,共12个文件,核心为4个Python脚本(数据加载、LSTM模型定义、单轮训练与全量训练)和4个txt数据文档,另含工程配置与README说明,便于直接运行和二次改造。已有132人学习下载。资源内含未做事故剔除的原始训练数据,并提供前向填充处理空值的思路,读者可在此基础上尝试更优预处理与调参方案,是理解LSTM回归建模和数据特征工程的实用样例。
1. 基于LSTM神经网络的通行时间预测:先说说我为什么盯上这个方向
如果你做交通数据分析,一定遇到过这样一种场景:导航显示前方路段还要 18 分钟,结果你开到一半,它又跳成 32 分钟。问题不在导航,而在通行时间预测没有用好历史时间序列。道路通行时间受早晚高峰、红绿灯周期、突发拥堵影响,是一条典型的非线性时间序列,单纯用前馈神经网络把当前时刻特征直接映射到预测值,往往丢掉了“过去 20 分钟在发生什么”这个关键信息。这里要聊的方案,就是用 LSTM 神经网络建模一段历史窗口内的通行时间走势,再通过 3 层全连接层完成回归预测,输出下一个时间段的通行时间。这个方案适合两类人:一类是手里有卡口过车记录或公交 GPS 数据、想做路段通行时间预测的算法工程师;另一类是刚接触 LSTM 回归预测、想找一个能跑通并能看到效果的最小工程。接下来我按数据、模型、训练、验证的顺序把整个过程拆开讲。
2. 为什么是 LSTM 加 3 层全连接层:先看清楚输入输出再动手
2.1 通行时间预测的本质:用窗口内的历史序列预测下一个值
道路通行时间的预测,常见做法是把它建模成单步回归:给定过去一段时间窗口内每 5 分钟或每 15 分钟的平均通行时间,以及同时段的流量、占有率、天气等特征,预测下一个时间片的通行时间数值。这里输入是一个多维时间序列,输出是一个连续值,和分类任务完全不同。正因为输出是连续数值,模型最后一层不能加 Softmax,也不能用 Sigmoid 把输出压在 0 到 1 之间,而是用一个不带激活函数的线性层直接输出实数。
我在做这类预测时,最常被问的一句话是:既然输入包含多个时间步,为什么不把窗口拉平之后直接丢给一个全连接网络?答案是:可以做,但效果往往差一截。拉平操作把时间维度和特征维度混在一起,模型无法显式感知“第 3 个时间步和第 1 个时间步之间有先后关系”。LSTM 网络结构天生适合处理这种带顺序的数据,它内部的记忆单元会按时间步逐步更新状态,天然保留了时序依赖。这个区别在通行时间这种强周期、强自相关的数据上非常明显。
2.2 LSTM 神经网络模型结构:从 RNN 到 LSTM 解决了长期记忆问题
早期做时间序列回归预测,大家常用的是 RNN 循环神经网络。RNN 的隐藏状态理论上可以携带历史信息,但在反向传播过程中,梯度经过多个时间步连乘之后很容易消失,导致模型只能记住最近几步的信息。通行时间预测有个让人头疼的特点:当前时刻的通行时间,往往和前 1 到 2 个小时的拥堵状况相关,如果只记住最近 10 分钟,模型基本学不到“拥堵正在蔓延”这个趋势。
LSTM 神经网络引入了门控机制,包括输入门、遗忘门和输出门。遗忘门决定了上一时刻的记忆细胞要保留多少,输入门决定当前新信息写入多少,输出门决定当前时刻输出哪些信息。这三个门各自都是带 Sigmoid 激活的神经网络层,配合一个候选记忆单元,让梯度可以沿着时间步更顺畅地回传。这也是 LSTM 在长时间序列回归预测任务里比普通 RNN 稳的主要原因。要注意,LSTM 只能解决“缓解梯度消失”的问题,不是彻底消灭,所以隐藏单元数量和层数仍然需要控制。
2.3 3 层全连接层在这里扮演的角色:把高维时序特征压缩成预测值
LSTM 层输出的是最后一个时间步的隐藏状态,维度通常是隐藏单元数,比如 64 维。这个向量可以理解为模型对“过去一段时间的道路通行状态”的高维抽象。但直接把它映射成通行时间,往往不够平滑。原因在于 LSTM 的隐藏状态是为建模时序而优化的,并不保证对回归目标有良好的线性可分性。在 LSTM 后面接 3 层全连接层,本质上是做非线性回归头的设计。
第一层全连接层负责把 LSTM 输出的高维向量映射到中间表示空间,第二层进一步压缩,第三层输出一个标量。每一层之间用 ReLU 激活函数引入非线性。如果只接一层线性层,模型就只相当于 LSTM 输出做线性加权,拟合能力有限;如果层数太多比如 5 层以上,在通行时间这种数据量不算大的任务里反而容易过拟合。3 层是一个在拟合能力和训练稳定性之间的折中,这也是这个标题里“LSTM+3 层全连接层”这个组合最常见的原因。
2.4 整体网络结构:一张图看懂数据怎么流过去
我把完整结构梳理一遍,方便后面对代码。输入张量的形状是(batch_size, seq_len, feature_dim),seq_len 是时间窗口长度,feature_dim 是每个时间步的特征数量。数据先进入 LSTM 层,在 PyTorch 的默认设置下,循环处理完整个序列后,取出最后一个时间步的输出,形状是(batch_size, hidden_size)。接着依次经过 3 个全连接层,中间两层带 ReLU 激活,最后一层不带激活,输出形状是(batch_size, 1),也就是每个样本一个通行时间预测值。
还有一个细节值得注意:LSTM 层的输出有两条路可走,一条是取最后一个时间步的输出,另一条是取全部时间步的输出再做平均池化。在通行时间预测这种单步回归任务里,我建议取最后一个时间步的输出,因为最后一个隐藏状态已经包含了整个序列的编码信息,平均池化反而可能把最近时刻的拥堵趋势削弱掉。
3. 数据准备:把原始过车记录变成可训练的序列样本
3.1 原始数据长什么样:卡口过车数据怎么换算成通行时间
做道路通行时间预测,数据来源通常有两类。一类是卡口或 ETC 门架记录,同一辆车经过两个相邻断面会产生两条过车记录,时间戳相减就是这辆车在该路段的通行时间。另一类是浮动车 GPS 轨迹,通过地图匹配算出车辆经过某条路段的时间。无论哪种,原始数据都需要先聚合。我一般按固定时间片(比如 5 分钟)对路段通行时间做平均,得到一个时间序列。
聚合之后的数据表通常长这样:时间戳列、路段编号列、平均通行时间列、流量列、平均速度列、拥堵指数列。注意,样本不是把每一行单独拿出来训练,而是按滑动窗口切成(窗口内特征序列, 下一个时间片的通行时间)这样的监督样本。例如用过去 6 个时间片(30 分钟)的数据预测下 1 个时间片的通行时间,那么窗口长度为 6。
在处理缺失时间片时,尽量不要用整段删除的方式。交通数据常有设备故障导致的部分缺失,我惯用的做法是用前后时间片的平均值插值;如果连续缺失超过 3 个时间片,再考虑删除对应路段的数据。这个边界会影响模型训练的样本量,很多刚入手的同学在这里容易踩坑,后面我会专门讲。
3.2 特征构建:时间窗口长度怎么选,哪些特征必须带
窗口长度是模型效果的重要参数。窗口太短,模型看不到完整的拥堵形成过程;窗口太长,特征维度增加、训练成本上升,而且遥远的历史信息对当前预测的贡献很小。我在城市快速路场景下,一般先试seq_len=6(对应 30 分钟),再对比 12 和 24 的效果。在高速场景下,由于车速快、通行时间波动相对小,seq_len=4也够用。建议先把窗口长度作为超参数固定下来,等模型通跑之后再网格搜索。
除了通行时间本身,还有几个特征强烈建议带上:时间片内的车流量、平均速度、路段占用率、时间片所在的小时和星期几。后面两个是周期性特征,能帮助 LSTM 区分同一时刻工作日和周末的差异。小时和星期几需要做编码,常见做法是小时归一化到 0-1,星期几用 6 维 one-hot 编码。如果把星期几当作普通数值特征,模型会误以为星期一和星期二之间有数值大小关系,这会让预测出现偏差。
3.3 数据集划分:千万不要随机打乱时间顺序
训练集、验证集、测试集的划分,是做通行时间预测最容易出错的地方之一。很多同学习惯了图像分类里的随机划分,直接把样本随机打乱,这种做法在时间序列预测里是错的。原因很简单:同一个路段相邻时间片的样本高度相关,随机打乱会让训练集里混入测试集时段的信息,模型在验证阶段相当于“偷看”了答案。
正确做法是按时间顺序切分。我一般把数据按时间排序后,前 70% 作训练集,中间 15% 作验证集,最后 15% 作测试集。验证集用于调参和早停决策,测试集只在最终评估时使用一次。切分时必须保证训练集的最后一个时间片和验证集的第一个时间片之间有间隔,防止窗口跨越切分边界。我的做法是切分后在边界处丢弃一个窗口长度的样本。
3.4 数据标准化:让回归预测的数值在稳定范围内
通行时间数据的分布很不均匀,畅通时可能只要 30 秒,拥堵时能到 1500 秒。如果原始数值直接输入网络,LSTM 的遗忘门和输入门的计算会由于输入数值过大导致梯度异常。常见的做法是用 MinMaxScaler 把每个特征缩放到 0 到 1 之间。
这里有一个容易被忽略的关键点:必须先在训练集上拟合 scaler,再用这个已拟合的 scaler 变换验证集和测试集,绝对不能在验证集或测试集上重新调用fit。否则验证集的数据分布信息会泄漏到模型评估过程中。另外,如果测试集里出现了比训练集最大值还大的通行时间,MinMaxScaler 会把它缩放到 1 以上。这在预测时没关系,因为我们最终会做逆变换还原真实数值,但如果用的是 Sigmoid 这类输出受限的模型,就会出问题。所以我在回归头最后一层不使用任何激活函数,确保输出层可以给出任意实数值。
4. 用 PyTorch 实现 LSTM+3 层全连接层回归预测:从模型定义到训练闭环
4.1 定义 LSTM 层和 3 层全连接层:模型结构代码
标题里最核心的可复现部分就在这一节。我用 PyTorch 搭建这个网络,先定义一个标准的回归预测模型类。代码里我刻意把 LSTM 层和全连接层拆开写,方便调整参数。
import torch import torch.nn as nn class TravelTimeLSTM(nn.Module): """ 道路通行时间预测模型 LSTM + 3层全连接层 回归预测 输入: (batch_size, seq_len, feature_dim) 输出: (batch_size, 1) """ def __init__(self, feature_dim, hidden_size, num_layers, fc_dims=(128, 64)): super(TravelTimeLSTM, self).__init__() self.lstm = nn.LSTM( input_size=feature_dim, hidden_size=hidden_size, num_layers=num_layers, batch_first=True ) # 3层全连接层: hidden_size -> 128 -> 64 -> 1 self.fc = nn.Sequential( nn.Linear(hidden_size, fc_dims[0]), nn.ReLU(), nn.Linear(fc_dims[0], fc_dims[1]), nn.ReLU(), nn.Linear(fc_dims[1], 1) ) def forward(self, x): # x shape: (batch, seq_len, feature_dim) out, (h_n, c_n) = self.lstm(x) # 取序列最后一个时间步的隐藏输出 last_out = out[:, -1, :] pred = self.fc(last_out) return pred.squeeze(-1) # 输出形状 (batch,)这段代码里的batch_first=True让输入张量的第一个维度直接是 batch 维度,符合大多数人的书写习惯。out是所有时间步的输出,形状为(batch, seq_len, hidden_size),我通过out[:, -1, :]取出最后一个时间步的隐藏状态。h_n是最后一层 LSTM 的隐藏状态,形状是(num_layers, batch, hidden_size),也可以取h_n[-1],效果等价,但用out[:, -1, :]更直观。3 层全连接层的维度我默认设成 128 和 64,你可以根据数据规模调整,但建议第一层不要小于 hidden_size 的一半,否则信息压缩太猛容易丢失特征。
4.2 特征工程与数据加载:把数据集装进 DataLoader
在模型定义之前,数据要先转换成监督学习样本。下面这段代码完成滑动窗口切分和数据集封装。我把聚合好的原始数据(已经过 MinMaxScaler)传给create_sequences,返回输入序列和预测目标。
from torch.utils.data import TensorDataset, DataLoader import numpy as np def create_sequences(data, seq_len, feature_cols, target_col): """ data: 标准化后的二维数组,每行对应一个时间片 seq_len: 滑动窗口长度 feature_cols: 特征列索引列表 target_col: 目标列索引(通行时间) """ X, y = [], [] for i in range(len(data) - seq_len): X_window = data[i : i + seq_len, feature_cols] y_value = data[i + seq_len, target_col] X.append(X_window) y.append(y_value) return np.array(X), np.array(y) # 示例:假设 scaled_df 是标准化后的 DataFrame feature_cols = [0, 1, 2, 3, 4] # 比如 通行时间、流量、速度、小时、星期 target_col = 0 # 通行时间列索引 X, y = create_sequences(scaled_df.values, seq_len=6, feature_cols=feature_cols, target_col=target_col) # 按时间顺序切分,禁止随机 shuffle split_idx1 = int(len(X) * 0.7) split_idx2 = int(len(X) * 0.85) X_train, y_train = X[:split_idx1], y[:split_idx1] X_val, y_val = X[split_idx1:split_idx2], y[split_idx1:split_idx2] X_test, y_test = X[split_idx2:], y[split_idx2:] train_dataset = TensorDataset(torch.FloatTensor(X_train), torch.FloatTensor(y_train)) val_dataset = TensorDataset(torch.FloatTensor(X_val), torch.FloatTensor(y_val)) train_loader = DataLoader(train_dataset, batch_size=64, shuffle=False) val_loader = DataLoader(val_dataset, batch_size=64, shuffle=False)注意,我这里create_sequences里做了一个处理:每个样本是用data[i : i + seq_len]生成窗口,也就是说第 0 到第 seq_len-1 行构成第一个样本,预测第 seq_len 行。这样相邻样本之间在时间上有重叠,但因为都来自同一路段的时间序列,这个重叠是正常的。如果希望相邻样本完全不重叠,可以用步长为 seq_len 取窗口,但那样样本量会大幅减少。对于回归预测任务,滑动窗口重叠是主流做法,不用刻意回避。
4.3 训练循环:损失函数、优化器和学习率怎么设
回归预测的损失函数直接用均方误差 MSE,公式不做推导,但有一点必须强调:如果数据标准化过,MSE 是在 0 到 1 区间上的误差,而不是真实秒数的误差。最终评估时需要通过逆标准化还原。优化器选择 Adam 是安全默认项,它自带自适应学习率,对 LSTM 这类参数尺度差异大的网络比较友好。初始学习率我通常设在 0.001,如果训练 loss 震荡剧烈,再降到 0.0005。
import torch.optim as optim # 模型初始化 model = TravelTimeLSTM( feature_dim=len(feature_cols), hidden_size=64, num_layers=2, fc_dims=(128, 64) ) criterion = nn.MSELoss() optimizer = optim.Adam(model.parameters(), lr=0.001) # LSTM 层参数往往需要更大的梯度范围,PyTorch 默认初始化通常够用 # 如果发现梯度爆炸,可以加梯度裁剪 def train_model(model, train_loader, val_loader, epochs=50, clip_value=1.0): train_losses, val_losses = [], [] for epoch in range(epochs): model.train() total_train_loss = 0.0 for X_batch, y_batch in train_loader: optimizer.zero_grad() pred = model(X_batch) loss = criterion(pred, y_batch) loss.backward() # 梯度裁剪,防止 LSTM 训练时梯度爆炸 nn.utils.clip_grad_norm_(model.parameters(), clip_value) optimizer.step() total_train_loss += loss.item() * X_batch.size(0) # 验证阶段不更新参数 model.eval() total_val_loss = 0.0 with torch.no_grad(): for X_batch, y_batch in val_loader: pred = model(X_batch) loss = criterion(pred, y_batch) total_val_loss += loss.item() * X_batch.size(0) train_loss = total_train_loss / len(train_loader.dataset) val_loss = total_val_loss / len(val_loader.dataset) train_losses.append(train_loss) val_losses.append(val_loss) if (epoch + 1) % 5 == 0: print(f"Epoch {epoch+1:02d}/{epochs} | Train Loss: {train_loss:.6f} | Val Loss: {val_loss:.6f}") return train_losses, val_lossesclip_grad_norm_是 LSTM 训练里非常实用的一行代码。梯度爆炸在 LSTM 时间步叠加后时有发生,现象就是 loss 突然变成 nan。梯度裁剪把所有参数的梯度范数限制在 1.0 之内,能有效避免这种情况。如果你不想每次训练都盯着输出,可以在验证 loss 连续若干个 epoch 不再下降时提前停止训练,这个机制我后面讲避坑时会再提到。
4.4 预测与反标准化:如何还原真实秒数
训练完成后,测试集上的预测值需要先通过 scaler 逆变换还原成实际通行时间。这里要注意,如果训练时用的是MinMaxScaler.fit_transform(scaled_df),那么还原时要提取出目标列的 scaler,单独做inverse_transform。
# 提取目标列的 scaler(假设 scaler 是 MinMaxScaler 对象) target_scaler = scaler # 如果目标列也在同一scaler中,直接整体逆变换 # 取测试集最后一个样本做单条预测演示 model.eval() sample_idx = -1 sample_x = torch.FloatTensor(X_test[sample_idx:sample_idx+1]) # (1, seq_len, feature_dim) sample_true = y_test[sample_idx] with torch.no_grad(): pred_normalized = model(sample_x).item() # 逆变换还原为真实通行时间 # 需要把预测值放到原数据的形状里再逆变换 def inverse_transform_value(scaler, value, col_idx): dummy = np.zeros((1, scaler.scale_.shape[0])) dummy[0, col_idx] = value inverse = scaler.inverse_transform(dummy) return inverse[0, col_idx] pred_real = inverse_transform_value(target_scaler, pred_normalized, 0) true_real = inverse_transform_value(target_scaler, sample_true, 0) print(f"真实通行时间: {true_real:.1f} 秒, 预测通行时间: {pred_real:.1f} 秒")这段代码里的dummy数组是我常用的技巧,因为inverse_transform需要输入原始特征维度,把预测值放在目标列位置、其余列填 0,逆变换后取目标列就是还原后的真实值。如果不做这一步,直接拿预测值去跟真实秒数比较,数值会完全对不上,新手在这里特别容易产生“模型效果怎么这么差”的误解。
5. 训练 LSTM 通行时间预测模型时踩过的 5 个坑
5.1 现象:预测曲线比真实曲线滞后一个时间窗口
这个问题在时间序列预测里几乎人人都会遇到,预测值和真实值的曲线形状看起来很像,但整体向右平移了一到两个时间步,导致 RMSE 偏高。原因在于 LSTM 学到的其实不是真正的预测,而是一个接近“把上一个时间步的值复制过来”的模式。当数据本身具有较强的自相关性时,延迟一个时间步的误差在训练时不太大,模型就偷懒了。
解决思路有两条。第一,在训练时加入差分特征,也就是把当前时间步与上一个时间步的差值作为额外特征输入,迫使模型学习变化趋势而非静态复制。第二,在损失函数里加入对预测趋势的惩罚项,比如计算预测差分与真实差分之间的误差。我一般优先尝试差分特征,因为改动最小、效果可解释性也强。
5.2 现象:LSTM 输出接全连接层时报维度不匹配的错
报错信息通常是mat1 and mat2 shapes cannot be multiplied。原因是对out张量取了多个时间步的输出,直接送进了全连接层。全连接层的输入维度是hidden_size,而out的形状是(batch, seq_len, hidden_size),矩阵乘法维度对不上。
解决方法是明确要不要取最后一个时间步:要么像我前面代码那样用out[:, -1, :],要么改为out[:, -1, :].contiguous().view(batch_size, -1)。另外还有一种常见做法是取所有时间步再做最大池化,但维度和信息保留方式都不一样,建议不要混用。检查维度最直接的办法是在 forward 里临时打印out.shape,确认是(batch, seq_len, hidden_size)再做切片。
5.3 现象:数据划分时随机打乱,验证集效果异常好但线上效果崩
典型场景是训练集 loss 一路下降,验证集 loss 也降得很低,模型看起来完美,但一部署到新路段数据上效果就崩。原因就是在切分训练集和验证集时用了随机 shuffle,导致验证样本的时间点前面其实包含了未来数据。通行时间预测的本质是外推,不是内插,随机打乱相当于把时间信息泄漏到了验证集。
解决方法是严格按时间顺序切分,并且保证训练集和验证集之间没有时间交集。如果你的实验需要和其他模型对比,建议把切分代码写成一个固定函数,传入时间段索引而不是随机索引,保证每次实验使用相同的切分位置。
5.4 现象:标准化方式不当,测试集预测值全部异常
有一类现象是训练和验证都很正常,但测试集预测值在还原后出现负值或几百倍的偏差。先说负值的情况:如果在全连接层中间使用了 ReLU,最后一层没有激活函数,输出理论上可以为负,但标准化后目标值都在 0 到 1 之间,负预测值的概率很小。如果出现大量负值,多半是反向传播出了问题,比如学习率过大导致权重振荡。
还有一种情况是测试集里出现了远大于训练集的拥堵事件,比如事故导致的 30 分钟通行时间。MinMaxScaler 按训练集的极值缩放,测试集的大值会被缩放到 1 以上,LSTM 没学过这样的分布,预测值会被压低。解决方法是训练时对目标列做针对性的截断,比如把超过 99 分位数的值替换为 99 分位数的值,降低极端值对模型的影响。
5.5 现象:训练 loss 持续下降但验证 loss 在后期突然飙升
这个现象在 LSTM 加上 3 层全连接层后经常出现,原因是过拟合。通行时间数据的样本量通常不会特别大,模型容量却不小,训练到后半程网络开始记住训练集中的噪声,比如某一天偶发的误记录数据,导致验证集误差反弹。
解决办法是早停,我会保存每个 epoch 的模型权重,然后选择验证 loss 最小的那一个作为最终模型,而不是训练完所有 epoch 后的权重。配合早停还有一个实用技巧:对 LSTM 层的参数使用更大的权重衰减,比如weight_decay=1e-4,因为 LSTM 参数比全连接层更复杂,更容易过拟合。如果仍然过拟合,考虑降低 LSTM 的隐藏单元数,从 64 降到 32 往往就有明显改善。
6. 验证模型可用性的一个闭环:多模型对比与滑动预测效果评估
模型训练到能跑只是第一步,真正投入使用前,我习惯用一个闭环来验证。多模型对比是标配:同一个数据集上,用 3 层全连接层单独做回归预测的 MLP、单层 LSTM 接一层线性输出、本方案 LSTM+3 层全连接层,三者在相同划分和相同窗口下对比。对比时统一使用真实尺度下的 RMSE 和 MAE,不要用标准化后的数值。我还会额外算一个 MAPE 平均绝对百分比误差,通行时间预测对百分比误差更敏感,因为高峰期的 100 秒误差和畅通期的 100 秒误差对用户体验完全不同。
滑动预测的验证思路也很简单:用过去 N 个时间步依次预测未来 1 步,把预测值作为下一步输入的一部分,继续预测下下步。这种做法模拟了实际系统的滚动预测机制。第一次预测可能准,滚动到第 5 步之后误差会放大。如果你的业务只需要预埋 15 分钟内的通行时间,单步预测就够;如果要做 1 小时级的高速路况预览,那必须在训练阶段就引入多步预测的损失计算,否则上线后滚动预测误差会让人怀疑模型白训了。我自己的经验是,先确认业务到底要预测几分钟后的状态,再去决定训练方式,很多项目都是在这里绕了弯路。
最后说一个我自己的训练习惯:把 PyTorch 的训练 seed 固定下来,包括 Python 随机种子、NumPy 随机种子和 torch 的 seed,否则同一份代码跑两遍结果不同,对比实验完全没法做。固定 seed 之后,再对比不同窗口长度、隐藏单元数和学习率。整个方向值不值得投入,看你有没有持续更新的数据管道。通行时间模型本质上是吃数据时效性的,一个季度前的模型放到今天,拥堵模式可能已经变了。如果数据是静态的一次性文件,那这个方案适合做离线分析和预研;如果数据能每周更新,那部署成定时重训练的流程,投入产出比会明显更高。希望这些踩坑经验对你有帮助,也祝你的模型上线之后不被路况打脸。
本文还有配套的精品资源,点击获取