新闻详情

新闻详情

首页 / 资讯中心 / 详情

PSO-Elman回归预测:多变量输入下如何优化递归神经网络参数

发布时间:2026/10/2 19:28:04来源:尧图网络
PSO-Elman回归预测:多变量输入下如何优化递归神经网络参数
简介这是一份融合粒子群算法PSO与Elman递归神经网络的回归预测源码面向需要处理多变量输入、追求高精度预测结果的机器学习研究人员、数据分析从业者及研究生适用于时序预测、非线性回归等常见场景。压缩包共含7个文件6个MATLAB脚本覆盖PSO参数寻优、Elman网络构建、训练预测及结果可视化等完整流程另附1个xlsx示例数据集便于直接替换整包仅37KB体积轻巧便于交流与部署。目前已有120人浏览学习。代码内置R2、MAE、MSE、RMSE、MAPE等多项评价指标可多维度衡量模型性能主函数、适应度函数与评价脚本分工明确、逻辑清晰便于二次开发能够快速适配新数据也可作为论文实验或课程项目的参考实现。1. PSO-Elman回归预测多变量输入下为什么选中这个组合多变量输入回归预测在工业场景里几乎天天遇到温度、压力、流量几个变量放到一起要预测下一个周期的产量或能耗。Elman神经网络适合做这件事因为它有递归连接能用上下文层记住前几步的隐藏状态把静态回归变成带时间记忆的动态回归。但Elman的初始权重和阈值怎么定过去大多数是靠随机种子加多次试验凑出来的这其实是玄学。把这个黑匣子交给粒子群算法PSO让粒子群去搜索一组更合适的初始权重或关键结构参数再用R2这样的回归指标衡量效果就是标题里这个模型的核心思路。这篇笔记适合手里有几十到几千行多变量数据、想快速把递归回归预测跑起来的人从Elman的原理一路讲到可复现代码和四个高频翻车点重点放在数据切分、适应度函数和R2的使用方式上。2. Elman神经网络的记忆结构待优化的到底是哪些参数2.1 Elman的“递归”来自哪里普通BP神经网络在当前时刻的输出只由当前输入决定上一时刻的信息不会保留到下一时刻。Elman网络的改动看起来很小却非常关键它在隐藏层旁边增加了一个上下文层每个时间步把隐藏层的输出复制一份存进去下一个时间步再把它作为额外的输入重新送进隐藏层。结构上可以把Elman看作在经典三层网络中间加了一条“旁路”。按时间展开后隐藏层的更新公式是h_t tanh(U * x_t W * h_{t-1} b_h)输出层则是y_t V * h_t b_y注意第二个公式里的W这就是普通前馈网络没有的递归权重。h_{t-1}来自上一个时间步所以模型当前时刻的预测不仅依赖此刻的输入还依赖之前的隐藏状态。对多变量回归来说这等于让模型自动学了一段“历史模式”流量突升后产量跟着升这类滞后关系恰好是Elman能捕捉、而BP网络很难捕捉的。2.2 待优化参数的完整清单理解Elman之后问题就转化为“到底要优化什么”。很多人以为只调学习率或迭代次数就够了其实Elman需要确定的参数远不止这些。我把常见实现里需要人工设置的参数整理成一张表。参数符号形状含义Uinput_dim × hidden_dim输入层到隐藏层的连接权重Whidden_dim × hidden_dim上下文层到隐藏层的递归权重b_hhidden_dim × 1隐藏层偏置阈值Vhidden_dim × output_dim隐藏层到输出层的连接权重b_youtput_dim × 1输出层偏置阈值hidden_dim标量隐藏层神经元数量属于结构参数学习率标量梯度更新步长属于训练参数初始权重范围与U、W、V同形权重初始化的区间举个例子假设输入有3个变量、隐藏层设为5个神经元、输出1个变量那么需要优化的实数数量是3×55×555×11一共51个。如果输入变量增到8个隐藏层还是5个总量就变成8×55×555×11共71个。2.3 为什么默认初始化方式容易翻车在Keras或MATLAB里直接建一个Elman网络权重默认是随机小数值。对简单问题这可能够用但对样本量不大、噪声明显的回归任务随机初始位置决定了模型最终落在哪个局部极小点附近。同一个网络换一个随机种子结果可能从R20.4跳到R20.8中间的差异几乎和模型本身结构无关。递归连接放大了这个问题。因为h_t依赖上一个时间步误差反向传播时沿着时间展开梯度路径更长随机初始化不好时梯度消失得更快。此时PSO的价值就出来了它不依赖梯度方向直接在参数空间或结构参数空间里做全局搜索能在一轮轮迭代中同时维护多个候选解最后给出一组更稳的初始点。把PSO的结果再交给梯度训练做局部精修是工程上最常见、也最稳妥的做法。3. PSO如何接管Elman调参公式、映射与R2适应度设计3.1 粒子位置与Elman参数的映射关系PSO的核心概念不复杂一群粒子在解空间里飞行每个粒子代表一组候选解它有当前位置和当前速度每次迭代根据个体最优和群体最优来调整飞行方向。要让PSO优化Elman必须先定义“解空间“就是什么。常见有两种映射方式。第一种是把第2.2节里的51个或71个权重阈值全部拼接成一个一维向量粒子位置就是这个向量适应度函数是“用这一组初始权重训练Elman后测试集上的误差”。第二种是只优化结构参数比如隐藏层神经元数量、学习率、时间步长度权重仍由梯度算法训练。前者更接近经典PSO-Elman论文里的做法后者在数据和计算资源有限时更实用。用公式表达假设待优化参数向量维度是D那么第i个粒子的位置是x_i (x_i1, x_i2, ..., x_iD)每个维度可能对应一个连接权重也可能对应一个超参数。初始化时在边界范围内随机生成。3.2 速度更新公式与标准参数粒子群迭代依赖两组公式。速度更新式是v_i_new w * v_i c1 * r1 * (pbest_i - x_i) c2 * r2 * (gbest - x_i)位置更新式是x_i_new x_i v_i_new其中w叫惯性权重控制粒子保持原来方向的程度c1是“个体学习因子”c2是“社会学习因子”r1和r2是0到1之间的随机数pbest_i是第i个粒子历史上找到的最优位置gbest是整个粒子群目前的最优位置。我常用的参数范围是粒子数20到40最大迭代次数50到100w线性递减从0.9降到0.4c1和c2都取1.5到2.0。如果搜索维度很大也就是权重参数很多时粒子数最好在30以上如果只优化3到4个结构参数20个粒子就能稳定收敛。3.3 适应度函数设计最大R2还是最小MSE回归预测的评价指标里R2是最直观的它表示模型解释了目标变量总方差的百分比。计算公式是R2 1 - SS_res / SS_totSS_res是残差平方和SS_tot是真实值与其均值之差的平方和。R2最大是1说明完美拟合R2接近0说明模型和直接取均值差不多如果R2为负说明模型比简单均值还差。在PSO里适应度函数负责告诉粒子“当前位置好不好”。如果想让PSO搜索到R2最大的参数组合可以直接把R2作为适应度粒子群迭代时找最大值。另一个做法是用均方误差MSE做适应度找最小值。我的习惯是PSO内部用MSE因为MSE对数值变化更敏感每轮迭代结束时再算一次R2用于记录和打印。但如果项目中明确要求“以R2为交付指标”那适应度直接写成最大R2更稳妥省去后面换算的困惑。4. 从零搭建PSO-Elman回归模型代码实现与参数设置4.1 数据预处理归一化是第一步不是可选步骤Elman内部使用tanh激活函数输入数值如果量纲不统一比如温度三位数、流量两位数、压力一位小数tanh的输入会被绝对值大的维度主导递归权重对小数维度几乎学不到信息。所以归一化是必须的不是锦上添花。下面这段代码完成数据读取和归一化import numpy as np import pandas as pd from sklearn.preprocessing import MinMaxScaler data pd.read_csv(multi_var_regression.csv, encodingutf-8) feature_cols [temperature, pressure, flow] target_col output scaler_x MinMaxScaler(feature_range(0, 1)) data[feature_cols] scaler_x.fit_transform(data[feature_cols]) # 目标变量单独归一化后续反归一化方便 scaler_y MinMaxScaler(feature_range(0, 1)) data[target_col] scaler_y.fit_transform(data[[target_col]])这段代码有两个细节值得说明。第一MinMaxScaler把每列压缩到0到1之间默认训练是fit_transform测试阶段只能用已经fit好的scaler做transform不能重新fit。第二目标变量单独用一个scaler是因为预测完成后要把结果反归一化回原始物理量单独保存scaler_y之后调用inverse_transform即可。4.2 构造时间步样本多变量输入如何变成序列Elman和普通网络在数据格式上有一个明显差异普通网络的样本是“一行特征对应一个标签”Elman的每个样本是“一个时间窗口的特征对应未来一个标签”。比如使用前3个时间步的温度、压力、流量预测第4个时刻的输出那么每个训练样本的形状是(3, 3)3个时间步乘3个特征。def create_sequences(data, feature_cols, target_col, n_steps): 把多变量时间序列切成(n_samples, n_steps, n_features)的输入 X, y [], [] arrays data[feature_cols].values targets data[target_col].values for i in range(n_steps, len(data)): X.append(arrays[i - n_steps:i, :]) # 取前n_steps个时刻的全部特征 y.append(targets[i]) # 预测当前时刻的目标 return np.array(X), np.array(y) n_steps 3 X, y create_sequences(data, feature_cols, target_col, n_steps) print(X shape:, X.shape) # 例如 (997, 3, 3) print(y shape:, y.shape) # 例如 (997,)这里的n_steps决定模型能回看多少历史信息。取2到6比较常见具体靠验证集尝试。注意切完后样本数会减少n_steps个数据总量只有几百条时这个损耗不能忽略。4.3 PSO优化Elman完整可运行代码接下来是通读部分。我用Keras搭建一个与Elman基本等价的SimpleRNN模型用PSO搜索两个关键参数隐藏层神经元数和学习率。适应度函数内部完成模型构建、训练和验证集R2计算。import tensorflow as tf from tensorflow.keras.models import Sequential from tensorflow.keras.layers import SimpleRNN, Dense from tensorflow.keras.optimizers import Adam from sklearn.metrics import r2_score from sklearn.model_selection import train_test_split # 固定随机种子保证复现 np.random.seed(42) tf.random.set_seed(42) # 按时间顺序切分不要shuffle原因见第5章 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, shuffleFalse ) X_val, X_train X_train[-100:], X_train[:-100] # 再切出验证集 y_val, y_train y_train[-100:], y_train[:-100] def build_elman(n_features, n_steps, hidden_units, learning_rate): model Sequential([ SimpleRNN(hidden_units, activationtanh, input_shape(n_steps, n_features)), Dense(1) ]) model.compile(optimizerAdam(learning_ratelearning_rate), lossmse) return model def fitness(params): hidden_units int(params[0]) learning_rate params[1] model build_elman(X_train.shape[2], n_steps, hidden_units, learning_rate) model.fit(X_train, y_train, epochs30, batch_size16, verbose0, validation_data(X_val, y_val)) y_pred model.predict(X_val, verbose0) val_r2 r2_score(y_val, y_pred) print(f hidden{hidden_units} lr{learning_rate:.5f} - R2{val_r2:.4f}) return val_r2 def pso_optimize(fitness_fn, bounds, num_particles15, max_iter15): dim len(bounds) # 在边界范围内初始化粒子位置和速度 positions np.array([ np.random.uniform(bounds[i][0], bounds[i][1], num_particles) for i in range(dim) ]).T velocities np.zeros_like(positions) pbest positions.copy() pbest_score np.full(num_particles, -np.inf) gbest positions[0].copy() gbest_score -np.inf w_max, w_min 0.9, 0.4 c1, c2 1.5, 1.5 for it in range(max_iter): w w_max - (w_max - w_min) * it / max_iter for i in range(num_particles): # 将粒子位置传给适应度函数 score fitness_fn(positions[i]) if score pbest_score[i]: pbest_score[i] score pbest[i] positions[i].copy() if score gbest_score: gbest_score score gbest positions[i].copy() # 更新速度与位置 r1, r2 np.random.rand(dim), np.random.rand(dim) velocities (w * velocities c1 * r1 * (pbest - positions) c2 * r2 * (gbest - positions)) positions positions velocities # 边界处理把超出边界的粒子拉回来并清零速度 for i in range(num_particles): for j in range(dim): if positions[i, j] bounds[j][0]: positions[i, j] bounds[j][0] velocities[i, j] 0 if positions[i, j] bounds[j][1]: positions[i, j] bounds[j][1] velocities[i, j] 0 print(f迭代 {it1}/{max_iter}当前gbest R2{gbest_score:.4f}) return gbest, gbest_score bounds [ (3, 20), # 隐藏层神经元数量搜索范围 (0.0001, 0.01) # 学习率搜索范围 ] best_params, best_r2 pso_optimize(fitness, bounds) print(f最优参数: hidden_units{int(best_params[0])}, lr{best_params[1]:.5f}) print(f验证集最优R2: {best_r2:.4f})代码里有几个参数需要刻意说明。粒子数这里设为15迭代15次是因为每次适应度都训练30个epoch的Elman粒子太多会让总训练时间翻倍。如果你的机器跑得动粒子数加到30、迭代数加到30收敛会更稳。学习率搜索范围用了对数量级0.0001到0.01跨度太大时PSO效率会下降可以缩小到0.001到0.005。隐藏层上限设20是因为多变量回归任务里隐藏单元过百很容易过拟合20已经能表达复杂映射。4.4 用最优参数训练最终模型并输出评价指标PSO找到最优参数后最后一件事是用全部训练数据重新训练模型然后在测试集上输出R2、RMSE和MAE。注意这里不要用PSO过程中保存的模型因为它在验证集上选出来的测试集还完全没见过。from sklearn.metrics import mean_squared_error, mean_absolute_error final_hidden_units int(best_params[0]) final_lr best_params[1] # 训练集包含原来的验证集扩大可用数据 X_all_train np.concatenate([X_train, X_val]) y_all_train np.concatenate([y_train, y_val]) model build_elman(X_all_train.shape[2], n_steps, final_hidden_units, final_lr) model.fit(X_all_train, y_all_train, epochs60, batch_size16, verbose0) y_pred model.predict(X_test, verbose0) y_test_inv scaler_y.inverse_transform(y_test.reshape(-1, 1)) y_pred_inv scaler_y.inverse_transform(y_pred) r2 r2_score(y_test_inv, y_pred_inv) rmse np.sqrt(mean_squared_error(y_test_inv, y_pred_inv)) mae mean_absolute_error(y_test_inv, y_pred_inv) print(f测试集 R2 {r2:.4f}) print(f测试集 RMSE {rmse:.4f}) print(f测试集 MAE {mae:.4f})最终模型epoch从30增加到60因为训练数据变多了训练轮数应该适当增加。如果R2在训练集上很高但测试集明显下降最可能的原因是样本量太小或隐藏单元数太大返回第5章的5.3节处理。5. PSO-Elman高频避坑数据泄漏、早熟与失效5.1 训练R2很高测试R2为负现象验证集R2达到0.9以上测试集R2却是负数预测值几乎平行于真实值曲线。有人会怀疑是代码写错了实际更常见的是数据泄漏。原因这里最典型的泄漏是对整个数据集做MinMaxScaler后再切训练测试集。如果归一化时fit了全部数据的最大值和最小值测试集的信息已经混进了训练数据。另一个泄漏是把时间序列打乱后切分。解决先把数据按时间顺序切成训练集和测试集再用训练集数据单独fit归一化参数。temp_df pd.DataFrame({temp: data[temperature].values, pressure: data[pressure].values, flow: data[flow].values, target: data[output].values}) temp_df.loc[:int(len(temp_df)*0.8), feature_cols] \ scaler_x.fit_transform(temp_df.loc[:int(len(temp_df)*0.8), feature_cols])顺序不能反过来。先把记录写好留存属于常见操作规程。5.2 PSO迭代几十次gbest纹丝不动现象前5代gbest的R2上升到某个值后后20代完全不变所有粒子向g位置挤成一小团从图上看就是粒子失去了探索能力。原因速度更新公式里如果我们把w设成固定0.9同时速度上限设得过大粒子会在最优解附近来回震荡如果w设成固定0.1粒子过早失去飞行惯性所有个体最优都复制了同一个gbest。更隐蔽的是学习方法上的边界处理——粒子每次撞到边界就被拉回并且速度清零清零次数多了整个群体都会失去活力。解决w线性递减是个稳妥选择从0.9逐步降到0.4前期的全局搜索和后期的局部精修各占一半。另外每代更新完速度后检查整个群体中粒子两两距离的平均值如果连续5代都在下降且已经小于边界范围的百分之五就对一部分粒子做随机重新初始化保留gbest和pbest这样既不破坏已有的好解又能重新激活搜索能力。5.3 隐藏层神经元数量陷入“越大越好”的错觉现象PSO在搜索隐藏层神经元数时最后给出的值正好落在边界上限比如设定的20。测试集R2不如预想的高训练时间倒是明显拉长。原因PSO只负责找最大化验证集R2的参数不会自动惩罚模型复杂度。10个神经元的Elman和20个神经元的Elman在两个不同随机种子下训练20个神经元可能让验证集分数略高但测试集的泛化能力不一定同步变好。这是典型的过拟合前兆。解决在适应度函数里加入惩罚项不要让隐藏单元数过大。常见做法是def fitness_with_penalty(params): score fitness(params) hidden_units params[0] if hidden_units 10: score - (hidden_units - 10) * 0.001 return score每多出1个隐藏单元就减去0.001的R2这个惩罚系数需要根据实际R2量级调整但方向是对的。样本数少于500时我一般直接把隐藏单元上限卡在8到12之间。5.4 变体只优化权重时需要“还原维度”的坑现象采用“粒子位置全部连接权重偏置”的经典映射时训练过程在Keras里报错提示权重形状不匹配或者能跑但结果明显不对。原因Fit函数输入的是平铺的一位向量比如51维但SimpleRNN层的权重不是一个二维矩阵。Keras里SimpleRNN有kernel和recurrent_kernel两个矩阵还带着各自的bias直接塞进model.set_weights()必然失败。解决需要先定义好参数向量每个分段的长度按输入维度、隐藏维度、输出维度动态切分。def unpack_weights(flat, n_input, n_hidden): total n_input * n_hidden n_hidden * n_hidden n_hidden total n_hidden 1 flat flat[:total] k 0 w_in flat[k:k n_input * n_hidden].reshape(n_input, n_hidden) k n_input * n_hidden w_rec flat[k:k n_hidden * n_hidden].reshape(n_hidden, n_hidden) k n_hidden * n_hidden b_h flat[k:k n_hidden] k n_hidden w_out flat[k:k n_hidden].reshape(n_hidden, 1) b_out flat[k:k 1] return w_in, w_rec, b_h, w_out, b_out如果不用Keras、想从零实现Elman的前向传播也需要同样风格的unpack函数来保证形状对应。这个错误很隐蔽报错后第一反应应该是打印各权重形状而不是怀疑算法。5.5 时间步n_steps取值不当导致滞后预测失效现象模型预测结果看起来和真实值曲线形状一致但整体向右偏移了一个时间周期R2还是很高RMSE也小实际使用中根本没预测作用。原因n_steps取值太大时网络把“参照上一时刻输出”当成了最优策略。如果工业过程本身的惯性很强当前输出和上一时刻输出高度相关Elman很容易学到“照抄前值”。这样得到的R2是假的。解决把测试集预测值和真实值画在一张图上肉眼检查是否存在延迟。更严谨的办法是计算预测值与真实值的互相关系数在滞后0时的相关系数才是有效指标。同时n_steps要取一个合理的短窗口不要超过问题本身的特征周期。6. 只看R2不够回归指标配合与收尾技巧6.1 R2、RMSE、MAE三件套怎么看R2能直观告诉你模型解释了多大比例的方差但它对偏差的“方向”不敏感。RMSE对预测误差中的大偏差很敏感因为先平方再开方几个离谱的离群点会瞬间拉高RMSE。MAE是平均绝对值误差能反映一般情况下的误差水平。指标公式侧重点R21 - SS_res / SS_tot解释方差比例适合横向对比模型A/BRMSEsqrt(mean((y - y_pred)^2))放大大误差适合关注峰值误差的场景MAEmean(abs(y - y_pred))平均误差水平对离群点不敏感我的习惯是PSO迭代过程中只看R2因为它的变化范围固定是0到1容易判断提升空间最终交付结果时三个指标一起给。如果R2达到0.85但RMSE比训练集的RMSE高一倍要警觉数据泄漏如果R2是0.98而MAE偏大通常是少数异常样本拖着RMSE上涨这时候需要去检查那些残差大的样本是不是采集噪声。6.2 收尾技巧用PSO做粗扫描再用梯度做精修PSO-Elman最后一步有个实践经验值得分享不要用PSO找到的参数直接作为最终训练配置后就不管了。PSO搜索出来的权重只代表一个大概率可靠的初始区域真正落地方案时标准的混合优化做法是先用PSO跑一轮粗搜索把gbest作为初始点再交给Elman自身的梯度训练继续微调十几到几十个epoch。这个做法我踩过几次亏后转移成了默认流程。单用PSO跑完就收工碰到多峰函数时容易停在边界附近测试集效果不稳定单用梯度训练又完全依赖初始种子。两者结合PSO定位到解的大致区域梯度负责局部收敛速度和稳定性能同时满足。如果你的项目数据量很小比如只有两三百条记录更高的实用路线是把PSO的迭代次数减半把省出来的时间用在多次独立实验取平均值上避免被单次实验的随机性误导。从实际交付的角度看这套方案的坑位基本都在数据切分和维度映射上至于隐藏层神经元是11还是13对最终R2的影响远没有数据泄漏大。把时间优先花在验证集设计和反归一化流程上模型的底线就有了。希望以上的实现路径和踩坑记录能帮你在自己的数据上少走几轮弯路。本文还有配套的精品资源点击获取
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

SpringBoot+Vue电影院订票系统实战:并发控制与前后端联调 2026/10/2 20:11:00

SpringBoot+Vue电影院订票系统实战:并发控制与前后端联调

简介:这是一套面向Java与Vue全栈初学者及课程设计者的电影院在线订票系统实战源码,完整覆盖前后端分离开发全流程,解决教学演示、毕业设计与小型影院业务系统快速搭建需求。资源含300个文件,7.95MB ZIP包,主体为82个Ja…

阅读更多 →
Python海康SDK接入实战:从登录取流到OpenCV处理全解析 2026/10/2 20:11:00

Python海康SDK接入实战:从登录取流到OpenCV处理全解析

两周前一个做工厂质检的兄弟问了我一个问题:“我用Python写好的模型,怎么接到海康摄像头的实时画面上?”这个问题我实在太熟了。过去几年不管是做智慧门店的人流统计、园区安防平台的视频接入,还是帮算法工程师搭建数据采集环境&a…

阅读更多 →
SpringBoot+Vue前后端分离医院网站系统实战解析 2026/10/2 20:10:59

SpringBoot+Vue前后端分离医院网站系统实战解析

1. 为什么医院网站要选这套前后端分离技术栈先说说我是怎么接下这个项目的。有一家二级规模的民营医院要做官网改版,最初的诉求特别简单:"把科室介绍和医生排班放到网上,患者能查得到就行"。结果需求评审时,陆续加进来预…

阅读更多 →
Anaconda环境管理实战:从虚拟环境到IDE配置与避坑 2026/10/2 20:10:53

Anaconda环境管理实战:从虚拟环境到IDE配置与避坑

先说结论:用Anaconda之后,Python环境不再是你机器上一个全局的解释器,而是变成了一堆可以随意创建、切换、删除的独立盒子。这个改变对于搞深度学习、做数据分析和写多项目的人来说,几乎是“治好了精神内耗”级别的体验。我自己早…

阅读更多 →
SQL刷题:1068产品销售分析,掌握JOIN与数据粒度细节 2026/10/2 20:10:45

SQL刷题:1068产品销售分析,掌握JOIN与数据粒度细节

要说SQL刷题里最容易被低估的题,1068.产品销售分析绝对排得上号。它在高频SQL 50题里属于第一档的简单题,不少同学看一眼表结构就觉得没难度,实际写起来却会在JOIN类型、聚合、排序这些细节上翻车。这篇文章我会从题目本身出发,把…

阅读更多 →
2026亲测10款降AIGC平台红黑榜!TaoToken统一Key接入实测优缺点无死角剖析 2026/10/2 20:10:38

2026亲测10款降AIGC平台红黑榜!TaoToken统一Key接入实测优缺点无死角剖析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉