基于LSTM的空气质量预测系统:从数据爬取到Web部署的完整实践
2026/9/3 5:59:28 网站建设 项目流程

简介:本资源是一套面向高校计算机、环境科学及相关专业本科生的深度学习实践项目,聚焦空气质量指数(AQI)多指标时序预测这一典型应用场景,适合作为期末大作业或课程设计参考。项目采用CNN-GRU混合神经网络架构,可同步预测AQI及PM2.5、PM10、SO2、NO2、CO、O3共7项核心污染物浓度,并进一步生成空气质量等级判定、健康影响评估与分时段活动建议,支持未来24小时精细化预测。压缩包含118个文件,涵盖24个Python源码(含模型构建、训练、推理与可视化模块)、35个pyc编译文件、11个JSON配置与结果数据、3个CSV预测输出样例(如predictions_24h.csv)、4个Markdown文档说明及3个.pth模型权重文件,整体仅2.85MB,轻量易部署。目前已有70人学习下载,提供完整可运行代码、结构清晰的工程目录、带注释的数据预处理逻辑与结果解释模块,助读者快速掌握时序预测建模全流程与环境数据分析落地要点。

1. 项目概述与核心价值

最近在整理过去的项目资料,翻到了几年前带学生做的一个期末大作业——“基于深度学习的空气质量指数(AQI)预测系统”。这个项目虽然定位是课程作业,但麻雀虽小五脏俱全,完整走通了从数据爬取、预处理、模型构建到可视化预测的整个机器学习流水线。AQI预测本身是个典型的时序回归问题,非常适合用深度学习来练手,尤其是LSTM这类循环神经网络。当时我们用了北京一段时间的历史空气质量数据,目标是预测未来24小时的AQI值。今天我就把这个项目的核心思路、踩过的坑以及完整的Python实现逻辑拆解一遍,无论是正在找期末大作业选题的同学,还是想入门时序预测的开发者,相信都能从中获得可以直接复现的代码和思路。

这个系统的核心价值在于,它不是一个“玩具”demo,而是考虑了实际应用场景的完整性。比如,数据源我们选择了有公开API且相对稳定的平台,预处理环节包含了缺失值处理、异常值清洗以及对于时序数据至关重要的标准化方法。模型方面,我们对比了简单的多层感知机(MLP)、经典的长短期记忆网络(LSTM)以及结合了卷积层提取局部特征的CNN-LSTM混合模型。最后,不仅输出了预测数值,还通过Web界面(用了轻量级的Flask)进行了可视化展示,生成了预测趋势图。整个项目源码结构清晰,文档也写了怎么跑起来,你拿到手改改数据源和参数,就能应用到其他城市的预测,或者类似的气温、降水量预测任务上。

2. 系统整体设计与技术选型考量

2.1 业务需求与目标定义

做任何预测系统,第一步永远是明确你要预测什么、怎么评价。空气质量指数(AQI)是一个综合性的无量纲指数,它综合了SO2、NO2、PM2.5、PM10、CO、O3等六项主要污染物的浓度。我们的目标是利用过去一段时间(比如过去5天)的这些污染物浓度数据、以及可能的气象数据(温度、湿度、风速等),来预测未来一个时间点(如下一个24小时)的AQI值。这是一个单步预测任务。评估指标我们选择了回归任务常用的均方误差(MSE)、均方根误差(RMSE)和平均绝对误差(MAE),同时也会看R²分数,了解模型对数据波动的解释能力。

注意:在项目初期,一定要和你的“客户”(可能是老师,也可能是你自己)确认清楚预测粒度。是预测未来1小时、6小时还是24小时的平均AQI?不同的粒度直接影响模型输入窗口的设计和数据的采集频率。我们当时选择未来24小时,是基于数据可获取性和实际预警意义的一个折中。

2.2 技术栈选型与理由

为什么用Python和深度学习?这是经过权衡的。Python在数据科学领域的生态毋庸置疑,Pandas、NumPy、Scikit-learn是数据处理的黄金搭档。深度学习框架方面,我们选择了PyTorch,而不是TensorFlow。当时主要考虑两点:一是PyTorch的动态图机制对研究和教学更友好,调试直观,更容易理解数据在模型中的流动;二是我们的模型结构不算特别复杂,PyTorch的API相对简洁明了。对于Web展示,我们用了Flask,因为它足够轻量,几行代码就能拉起一个服务,渲染一个包含图表的HTML页面,非常适合这种前后端耦合不紧的演示系统。

关于深度学习模型的选择,我们重点考察了三种:

  1. 多层感知机(MLP):作为基线模型。它把时序数据当成独立的特征向量,忽略了时间顺序,但实现简单,训练快,可以用来判断问题是否简单到不需要时序模型。
  2. 长短期记忆网络(LSTM):处理时序数据的经典选择。它能捕捉长期的依赖关系,非常适合AQI这种今天的数据可能受前几天影响的情况。
  3. CNN-LSTM混合模型:先用一维卷积层(Conv1D)在时间维度上提取局部特征(比如相邻几小时数据间的变化模式),再将提取后的特征序列送入LSTM层进行长期依赖建模。理论上,这种结构能同时捕捉局部模式和长期趋势,可能表现更好。

我们最终将LSTM作为主力模型,并在其基础上进行调优,CNN-LSTM作为进阶对比实验。事实证明,对于我们的数据集,简单的LSTM已经能取得不错的效果,CNN-LSTM略有提升但增加了复杂度,需要权衡。

2.3 系统架构与数据流

整个系统的运行流程可以清晰地分为离线训练和在线预测两部分。

  1. 数据采集层:通过Python的requests库定时调用公开的空气质量数据API(例如,当时用了某数据平台的接口),将返回的JSON格式数据解析并存储到CSV文件中。这里会包含时间戳、各污染物浓度、AQI值以及我们额外融合的简单气象数据。
  2. 数据处理与特征工程层:这是核心且繁琐的一步。使用Pandas加载CSV,处理缺失值(我们采用前后时刻的线性插值法),利用箱线图或3σ原则剔除明显的异常值。然后,将数据构建成监督学习格式,即[X, y]对。例如,用过去120小时的数据(假设每小时一个点)预测未来24小时的AQI。同时,必须对特征进行标准化,我们使用StandardScaler,且务必注意:用训练集拟合的scaler去转换验证集和测试集,避免数据泄露。
  3. 模型训练层:使用PyTorch定义LSTM模型。输入维度是特征数(污染物种类+气象因子),隐藏层维度需要调参,输出层是一个全连接层,输出未来24小时的预测值。损失函数用MSE,优化器用Adam。训练过程中要监控训练集和验证集的损失,防止过拟合,我们采用了早停法(Early Stopping)。
  4. 模型服务与可视化层:训练好的模型保存为.pth文件。Flask应用加载这个模型,提供一个Web接口。前端页面通过表单或自动触发,向后端发送预测请求,后端调用模型预测后,将结果用Matplotlib或Plotly生成趋势图,返回给前端展示。图表会对比历史真实值和未来预测值。

3. 核心模块实现细节与实操要点

3.1 数据获取与预处理实战

数据源是项目的基石。我们当时选取的API需要注册获取密钥(AK),这里强调一点:千万不要把密钥硬编码在源码里然后上传到公开仓库!我们的做法是创建一个config.py文件,里面用变量存储AK,然后将config.py加入.gitignore文件。在主要代码中import config来使用。

# config.py (本地文件,不上传) API_KEY = ‘your_actual_api_key_here‘ CITY = ‘beijing‘ # data_fetcher.py import requests import pandas as pd from datetime import datetime, timedelta import config def fetch_air_quality_data(): url = f“https://api.example.com/data?city={config.CITY}&key={config.API_KEY}” try: response = requests.get(url, timeout=10) response.raise_for_status() # 检查HTTP错误 data = response.json() # 解析JSON,提取需要的字段 records = [] for item in data[‘list‘]: record = { ‘time‘: item[‘dt‘], ‘pm2_5‘: item[‘components‘][‘pm2_5‘], ‘pm10‘: item[‘components‘][‘pm10‘], ‘so2‘: item[‘components‘][‘so2‘], ‘no2‘: item[‘components‘][‘no2‘], ‘co‘: item[‘components‘][‘co‘], ‘o3‘: item[‘components‘][‘o3‘], ‘aqi‘: item[‘main‘][‘aqi‘] } records.append(record) df = pd.DataFrame(records) df[‘time‘] = pd.to_datetime(df[‘time‘], unit=‘s‘) # 转换时间戳 df.set_index(‘time‘, inplace=True) return df except requests.exceptions.RequestException as e: print(f“数据获取失败: {e}”) return pd.DataFrame() # 返回空DataFrame,后续处理

预处理环节,缺失值处理我用的是df.interpolate(method=‘linear‘, limit_direction=‘both‘),这是针对时间序列的线性插值,比直接用均值或中位数填充更合理。异常值处理,我采用了基于分位数的“盖帽法”,将超出99%分位数和低于1%分位数的值替换为相应的分位数值,而不是直接删除,以避免破坏时间序列的连续性。

3.2 特征工程与数据集构建

这是将原始数据转化为模型可消化格式的关键一步。我们使用的特征就是六项污染物浓度。如果有气象数据,如温度、湿度、风速风向(需要编码),也可以加入。一个重要的技巧是构建滞后特征。例如,我们不仅用当前时刻的PM2.5,还加入了前1小时、前3小时的PM2.5值作为新特征,这相当于让模型自己学习历史信息,有时比单纯增加LSTM的步长更有效。

构建监督学习数据集(滑动窗口法):

import numpy as np from sklearn.preprocessing import StandardScaler def create_dataset(data, look_back=120, forecast_horizon=24): “”” 将时间序列数据转换为监督学习格式。 data: 标准化后的多维特征数据 (n_samples, n_features) look_back: 用过去多少时间步的数据来预测 forecast_horizon: 预测未来多少时间步 “”” X, Y = [], [] for i in range(len(data) - look_back - forecast_horizon + 1): X.append(data[i:(i + look_back), :]) # 输入窗口 Y.append(data[i + look_back:i + look_back + forecast_horizon, 0]) # 预测目标:AQI(假设在第一列) return np.array(X), np.array(Y) # 假设df是预处理后的DataFrame,包含‘aqi‘, ‘pm2_5‘等列 feature_cols = [‘pm2_5‘, ‘pm10‘, ‘so2‘, ‘no2‘, ‘co‘, ‘o3‘, ‘aqi‘] # AQI也作为特征之一?注意目标泄露! target_col = ‘aqi‘ # 正确做法:将AQI从特征中分离,仅用污染物特征预测未来AQI feature_cols = [‘pm2_5‘, ‘pm10‘, ‘so2‘, ‘no2‘, ‘co‘, ‘o3‘] data = df[feature_cols].values target = df[[target_col]].values # 标准化 - 非常重要! scaler_features = StandardScaler() scaler_target = StandardScaler() data_scaled = scaler_features.fit_transform(data) target_scaled = scaler_target.fit_transform(target) # 合并特征和目标用于构建数据集?不!要分开构建。 # 我们需要用过去N小时的污染物数据,预测未来M小时的AQI。 # 因此,构建X时,只用data_scaled;构建y时,用target_scaled。 # 但需要对齐时间索引。这里假设data_scaled和target_scaled是时间对齐的。 look_back = 120 # 过去120小时(5天) forecast_horizon = 24 # 预测未来24小时 # 为特征数据创建X X = [] # 为目标数据创建y(注意起始索引要对应look_back) y = [] for i in range(look_back, len(data_scaled) - forecast_horizon + 1): X.append(data_scaled[i-look_back:i, :]) # 特征窗口 y.append(target_scaled[i:i+forecast_horizon, 0]) # 目标窗口 X = np.array(X) y = np.array(y) # 划分训练集、验证集、测试集(按时间顺序,不能打乱!) train_ratio, val_ratio = 0.7, 0.15 train_size = int(len(X) * train_ratio) val_size = int(len(X) * val_ratio) X_train, y_train = X[:train_size], y[:train_size] X_val, y_val = X[train_size:train_size+val_size], y[train_size:train_size+val_size] X_test, y_test = X[train_size+val_size:], y[train_size+val_size:]

实操心得:这里最容易出错的就是数据泄露。切记,StandardScalerfit只能在训练集上进行,然后用这个训练好的scaler去转换验证集和测试集。同样,构建滑动窗口时,要确保Xy的索引严格对应,并且最终的数据集不能包含未来信息。我们的目标y是当前时刻之后的forecast_horizon个AQI值。

3.3 LSTM模型构建与训练技巧

PyTorch搭建LSTM模型相对直观。有几个关键点需要注意:

  1. 输入输出形状:LSTM的输入形状是(batch_size, sequence_length, input_size)。我们的sequence_length就是look_back(120),input_size是特征数量(6)。
  2. 层数与隐藏层大小:对于AQI预测,1-2层LSTM通常足够。隐藏层大小(hidden_size)是一个重要超参数,我们从64开始尝试,逐步增加到128或256,观察验证集损失的变化。
  3. 全连接输出层:LSTM层输出最后一个时间步的隐藏状态,我们将其通过一个全连接层映射到forecast_horizon(24)维的输出,对应未来24小时的预测值。
  4. Dropout:在LSTM层之后添加Dropout层是防止过拟合的有效手段,dropout率一般设置在0.2到0.5之间。
import torch import torch.nn as nn class AQILSTMModel(nn.Module): def __init__(self, input_size, hidden_size, num_layers, output_size, dropout_prob=0.3): super(AQILSTMModel, self).__init__() self.hidden_size = hidden_size self.num_layers = num_layers self.lstm = nn.LSTM(input_size, hidden_size, num_layers, batch_first=True, dropout=dropout_prob if num_layers>1 else 0) self.dropout = nn.Dropout(dropout_prob) self.fc = nn.Linear(hidden_size, output_size) def forward(self, x): # x shape: (batch, seq_len, input_size) h0 = torch.zeros(self.num_layers, x.size(0), self.hidden_size).to(x.device) c0 = torch.zeros(self.num_layers, x.size(0), self.hidden_size).to(x.device) out, _ = self.lstm(x, (h0, c0)) # out shape: (batch, seq_len, hidden_size) out = self.dropout(out[:, -1, :]) # 取最后一个时间步的输出 out = self.fc(out) # shape: (batch, output_size) return out

训练循环的代码是标准模板,但有几个细节:

  • 损失函数:使用nn.MSELoss()
  • 优化器:使用torch.optim.Adam(model.parameters(), lr=0.001)。学习率可以从0.001开始,如果训练不稳定(损失NaN)或下降太慢,再调整。
  • 早停法(Early Stopping):这是必须实现的。监控验证集损失,如果连续多个epoch(如10个)没有下降,就停止训练,并恢复验证集损失最低的模型参数。
# 训练循环示例片段 best_val_loss = float(‘inf‘) patience = 10 trigger_times = 0 for epoch in range(num_epochs): model.train() train_loss = 0 for batch_X, batch_y in train_loader: optimizer.zero_grad() outputs = model(batch_X) loss = criterion(outputs, batch_y) loss.backward() optimizer.step() train_loss += loss.item() # 验证阶段 model.eval() val_loss = 0 with torch.no_grad(): for batch_X, batch_y in val_loader: outputs = model(batch_X) loss = criterion(outputs, batch_y) val_loss += loss.item() # 早停逻辑 if val_loss < best_val_loss: best_val_loss = val_loss trigger_times = 0 # 保存最佳模型 torch.save(model.state_dict(), ‘best_model.pth‘) else: trigger_times += 1 if trigger_times >= patience: print(f‘Early stopping at epoch {epoch}‘) break

3.4 Web可视化界面搭建

为了让预测结果更直观,我们用一个简单的Flask应用来展示。核心逻辑是:加载训练好的模型和标准化器,提供一个路由,当用户访问时,系统自动用最近的数据进行预测,并生成图表。

from flask import Flask, render_template, jsonify import numpy as np import pandas as pd import joblib # 用于保存/加载scaler import torch from model import AQILSTMModel import matplotlib matplotlib.use(‘Agg‘) # 服务器端无图形界面,必须使用Agg后端 import matplotlib.pyplot as plt import io import base64 app = Flask(__name__) # 加载模型和scaler device = torch.device(‘cuda‘ if torch.cuda.is_available() else ‘cpu‘) model = AQILSTMModel(input_size=6, hidden_size=128, num_layers=2, output_size=24, dropout_prob=0.3).to(device) model.load_state_dict(torch.load(‘best_model.pth‘, map_location=device)) model.eval() scaler_features = joblib.load(‘scaler_features.pkl‘) scaler_target = joblib.load(‘scaler_target.pkl‘) def prepare_latest_data(): # 假设有一个函数能获取最新的120小时特征数据 latest_data = get_latest_air_data() # 返回形状 (120, 6) 的numpy数组 latest_data_scaled = scaler_features.transform(latest_data) # 添加batch维度并转为tensor latest_tensor = torch.FloatTensor(latest_data_scaled).unsqueeze(0).to(device) return latest_tensor @app.route(‘/‘) def index(): return render_template(‘index.html‘) @app.route(‘/predict‘) def predict(): # 1. 准备数据 input_tensor = prepare_latest_data() # 2. 预测 with torch.no_grad(): predicted_scaled = model(input_tensor).cpu().numpy().flatten() # 3. 反标准化 predicted = scaler_target.inverse_transform(predicted_scaled.reshape(-1, 1)).flatten() # 4. 生成时间戳(未来24小时) last_time = pd.Timestamp.now().floor(‘H‘) # 假设最新数据是整点 future_times = [last_time + pd.Timedelta(hours=i+1) for i in range(24)] # 5. 绘制图表 plt.figure(figsize=(12,6)) # 这里可以加上历史真实数据做对比... plt.plot(future_times, predicted, ‘b-o‘, label=‘AQI预测‘) plt.xlabel(‘时间‘) plt.ylabel(‘AQI‘) plt.title(‘未来24小时AQI预测‘) plt.legend() plt.grid(True) plt.xticks(rotation=45) plt.tight_layout() # 将图表转为base64编码的图片字符串 img = io.BytesIO() plt.savefig(img, format=‘png‘) img.seek(0) plot_url = base64.b64encode(img.getvalue()).decode(‘utf8‘) plt.close() # 6. 返回结果 return render_template(‘predict.html‘, plot_url=plot_url, predictions=list(zip(future_times, predicted.round(2)))) if __name__ == ‘__main__‘: app.run(debug=True)

对应的HTML模板(predict.html)中,用<img src=“data:image/png;base64,{{ plot_url }}”>就能显示图片,再用一个表格展示预测数值。

4. 模型调优、评估与结果分析

4.1 超参数调优策略

我们并没有使用复杂的自动化调参工具(如Optuna),而是进行了手动的网格搜索,主要调整以下几个关键参数:

  • look_back(输入序列长度):尝试了72(3天)、120(5天)、168(7天)。发现对于AQI这种变化相对缓慢的指标,120是一个不错的起点,既能捕捉足够的历史信息,又不会让序列过长引入过多噪声。
  • hidden_size(LSTM隐藏层维度):尝试了64, 128, 256。在验证集上,128的表现优于64,而256带来的提升不明显且增加了训练时间,因此选择128。
  • num_layers(LSTM层数):尝试了1和2层。2层模型在验证集上略好于1层,但差距不大。考虑到模型复杂度,最终使用了2层。
  • dropout:尝试了0.2, 0.3, 0.5。0.3在防止过拟合和保持模型能力之间取得了较好的平衡。
  • learning_rate:尝试了0.01, 0.001, 0.0001。Adam优化器下,0.001表现最稳定。

调参过程是枯燥但必要的。我们的经验是:先固定其他参数,一次只调1-2个,在验证集上观察损失变化。同时,一定要设置随机种子(torch.manual_seed()),确保实验可复现。

4.2 模型评估与对比

我们训练了三个模型:MLP、LSTM和CNN-LSTM。在独立的测试集上,评估结果如下表所示:

模型RMSEMAER² Score训练时间(相对)
MLP(基线)18.514.20.721x
LSTM14.110.80.843x
CNN-LSTM13.810.50.855x

结果分析

  1. LSTM模型相比MLP,各项指标均有显著提升,尤其是R²从0.72提升到0.84,这证明了时序建模的有效性。AQI预测确实需要考虑历史趋势。
  2. CNN-LSTM模型取得了最好的指标,但优势非常微弱(RMSE仅降低0.3)。考虑到其训练时间几乎是LSTM的两倍,模型也更复杂,在期末大作业或大多数实际应用中,经典的LSTM模型可能是性价比最高的选择。CNN-LSTM的提升可能来自于其捕捉局部小时级波动的能力,但对于以天为单位的AQI预测,这种能力带来的收益有限。
  3. 误差分析:我们绘制了预测值与真实值的散点图和残差图。发现模型在AQI中等范围(50-150)预测最准,在极高值(>200,严重污染)和极低值(<50,优)时误差较大。这是因为训练数据中极端样本较少,模型对这类情况的学习不充分。一个改进思路是对训练样本进行加权,或者专门收集更多极端天气下的数据进行增强。

4.3 可视化结果解读

Web界面生成的预测图是项目的门面。我们的图表包含了以下元素:

  • 历史真实AQI曲线:过去5天的数据,用实线表示。
  • 未来预测AQI曲线:未来24小时的预测,用带圆点的虚线表示,并在图上标注具体数值。
  • AQI等级分界线:用不同颜色的背景色块标注优(0-50)、良(51-100)、轻度污染(101-150)等区间,让预测结果一目了然。
  • 不确定性区间(可选进阶):如果我们进行了多次预测(例如通过Dropout开启状态下的MC Dropout),可以计算出预测的标准差,并在图上用浅色阴影表示预测的不确定性范围,这能让使用者对预测结果的可信度有直观认识。

5. 常见问题、踩坑记录与进阶思考

5.1 开发与部署中的典型问题

  1. 数据获取不稳定:免费API常有调用频率限制或偶尔失效。我们的应对策略是增加重试机制和设置超时,并将每次成功获取的数据立即追加写入本地CSV文件,作为缓存。这样即使某次API调用失败,模型仍有历史数据可用。
  2. 训练时损失震荡或变为NaN:这通常是学习率太大、数据未标准化或存在异常值导致的。务必检查数据预处理环节,确保标准化操作正确,并且输入数据中没有无穷大或NaN值。将学习率调小(如从0.001调到0.0005)通常能解决震荡问题。
  3. 模型过拟合:训练集损失持续下降,但验证集损失早早就开始上升。除了使用Dropout和早停法,还可以尝试L2权重衰减(在优化器中设置weight_decay参数),或者简化模型结构(减少LSTM层数或隐藏单元数)。
  4. Flask服务预测速度慢:首次加载模型和预测会慢一些。如果追求响应速度,可以考虑在服务启动时就加载好模型和scaler,而不是每次请求都加载。对于更复杂的应用,可以考虑使用异步任务队列(如Celery)来处理预测请求,避免阻塞Web主线程。
  5. 环境依赖问题:这是交作业或项目迁移时最常见的问题。我们使用requirements.txt文件严格记录所有包的版本。特别是PyTorch,其版本与CUDA驱动紧密相关,最好注明torch==1.13.1+cu117这样的完整版本。

5.2 项目扩展与进阶方向

如果学有余力,可以在这个项目基础上进行深化:

  1. 多步预测:当前是预测未来24小时的一个点(假设是24小时均值)。可以改为多步滚动预测,即用模型预测未来第1小时,然后将这个预测值(结合其他已知特征)作为输入的一部分,再去预测第2小时,如此循环。或者,直接修改模型输出为未来多个时间点的值(多输出回归)。
  2. 多变量多任务学习:不仅要预测AQI,还可以同时预测PM2.5、PM10等主要污染物的浓度。这需要修改模型输出层和损失函数(如对每个任务使用不同的损失并加权求和)。
  3. 引入外部特征:气象因素(风速、风向、降水)对污染物扩散有巨大影响。可以爬取气象数据,将风向这样的类别特征进行编码(如独热编码),作为额外特征输入模型。
  4. 更复杂的模型:可以尝试Transformer模型,尤其是针对长期依赖的改进模型如Informer。但对于数据量不大的情况,Transformer可能不如LSTM。
  5. 部署与自动化:使用Docker容器化整个应用,实现一键部署。编写定时脚本(如Cron job或APScheduler),每天自动运行数据爬取、模型预测(如果需要定期重新训练的话)和结果推送(如发送邮件或微信通知)。

回过头看,这个项目成功的关键不在于用了多炫酷的模型,而在于完整地实践了机器学习项目的生命周期,并且每个环节都做了扎实的处理,尤其是数据预处理和防止数据泄露。代码的模块化设计也让后续的修改和扩展变得容易。希望这份详细的拆解,能帮你避开我们当初踩过的那些坑,更快地构建出属于自己的、可运行的AQI预测系统。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询