☰
基于BP神经网络的数据回归预测:Matlab完整实现与调参指南
2026/9/28 22:39:57 网站建设 项目流程

简介:面向需要借助MATLAB完成数据回归预测的科研人员和学生,这个基于BP神经网络的程序包提供端到端的回归建模方案,能有效处理非线性问题,可迁移至金融预测、工程估算、经济分析等场景。压缩包仅13KB,包含1个xlsx数据集和1个m主程序,前者提供直接可用的样本数据,后者涵盖数据预处理、网络创建、训练与预测的完整代码,并配有清晰注释,方便按需修改和移植。资源已有2180人次学习下载,尤其适合刚接触回归预测的初学者,也可作为课程实验或项目基线。运行后可得到预测输出与误差对比,通过调整隐藏层节点数、训练函数等参数,能直观体会BP网络对拟合精度的影响;同时代码自带归一化与反归一化步骤,便于直接套用至自定义数据集,快速掌握从数据整理到模型评估的完整思路。

1. 为什么用BP神经网络做数据回归预测:一个普通工程问题的成熟解法

拿到一组输入和输出数据,想建一个能预测的模型,BP神经网络往往是很多工程师的第一个选择。原因不复杂:它不需要你对数据分布做太多假设,非线性拟合能力强,Matlab里工具箱和底层函数都齐全,二十行左右就能跑通一个完整流程。相比高斯过程回归、支持向量回归这些同样适合小样本仿真数据预测的模型,BP的入门门槛最低,调参路径也最直观——你改隐层节点数、改学习率,训练误差会给出即时反馈。

这篇笔记围绕「基于BP神经网络的数据回归预测(Matlab完整程序和数据)」这个标题展开,适合刚接触神经网络回归、手头有一份mat或xlsx格式数据、想在Matlab里快速得到可复现结果的读者。我会把数据预处理、网络搭建、训练参数、结果评价和常见坑一次讲清楚,你照着抄就能用,改改参数就能迁移到自己的数据上。

2. 从数据到网络:BP回归预测的建模流程与关键抉择

2.1 数据怎么准备:归一化、划分与评价指标的确定

BP神经网络的核心计算依赖激活函数的输出范围,最常用的tansig和logsig分别把输出压到[-1,1]和[0,1]区间。如果你直接喂原始数据,量纲差异大的特征会让权值更新陷入震荡。所以第一步几乎都是归一化,常见做法是mapminmax,把每列特征映射到[-1,1]。

% 读取原始数据,假设最后一列是目标值 data = xlsread('dataset.xlsx'); X_raw = data(:, 1:end-1); % 特征 Y_raw = data(:, end); % 目标 % 归一化到[-1,1],返回归一化后的数据和映射结构体 [X_norm, ps_input] = mapminmax(X_raw', -1, 1); [Y_norm, ps_output] = mapminmax(Y_raw', -1, 1); % 转回行向量形式(Matlab神经网络默认要求样本按列排) X_norm = X_norm'; Y_norm = Y_norm';

这里有个容易搞混的点:mapminmax要求输入数据每列是一个特征、每行是一个样本,但它内部是按行做归一化的。所以先转置再归一化,归一化后再转置回来。ps_input和ps_output这两个结构体必须保存好,后面预测完要用它把结果反归一化回真实量纲。很多人忘了这一步,直接拿归一化后的数值当预测结果,画出来的图和数据完全对不上。

数据划分上,我一般按70%训练、15%验证、15%测试的比例随机划分。训练集用于更新权值,验证集用于监控过拟合和早停,测试集只做最终评估,不参与训练。Matlab里可以用dividerand函数,它会生成三个索引,并且支持设置随机种子保证结果可复现。

% 按索引划分数据集:训练集70%,验证集15%,测试集15% trainRatio = 0.7; valRatio = 0.15; testRatio = 0.15; [trainInd, valInd, testInd] = dividerand(size(X_norm, 1), trainRatio, valRatio, testRatio); X_train = X_norm(trainInd, :)'; X_val = X_norm(valInd, :)'; X_test = X_norm(testInd, :)'; Y_train = Y_norm(trainInd, :)'; Y_val = Y_norm(valInd, :)'; Y_test = Y_norm(testInd, :)';

注意这里又转置了一次,因为feedforwardnet和train函数要求输入是「特征×样本」的矩阵格式,即每列一个样本。这是Matlab神经网络工具箱和其他编程语言框架最不一样的地方,很多人刚接触时在这个维度问题上反复翻车。划分完后,训练集、验证集、测试集的样本数分别打印出来确认一下,避免某个集合是空的。

评价指标方面,回归预测最常用的是决定系数R²、均方根误差RMSE和平均绝对误差MAE。R²越接近1说明模型解释了越多的数据方差,RMSE和MAE越小说明误差越小。这三个指标要一起看,单看R²会被极端值误导,单看RMSE又看不出是否存在系统性偏移。计算时需要先反归一化得到真实值,再和原始Y比较。

2.2 网络结构怎么定:输入输出、隐层节点数与激活函数

BP神经网络的结构图很多人见过——输入层、隐层、输出层,每层若干神经元,层与层之间全连接。选型时最关键的是隐层节点数,它直接决定了模型的拟合能力。节点太少欠拟合,节点太多过拟合,还会拖慢训练速度。

隐层节点数没有解析解,工程上最常用的经验公式是:

hidden_nodes = floor(sqrt(input_nodes * output_nodes) + 1~10),或者取输入层节点数的2倍加1。

比如输入特征5个、输出1个,sqrt(5)约等于2.2,加1到10,常见取值就是3到12。我的习惯是从小到大扫一遍:先设3个节点,看训练集和测试集误差,再逐步加到10个,选测试集RMSE最小的那个。不要追求训练集误差最小,那是过拟合的信号。

% 尝试不同隐层节点数,记录测试集表现 input_nodes = size(X_train, 1); % 输入层节点数 = 特征数 output_nodes = size(Y_train, 1); % 输出层节点数 = 目标数 for hidden_nodes = 3:2:11 net = feedforwardnet(hidden_nodes); net.trainFcn = 'trainlm'; % 使用Levenberg-Marquardt训练 net.trainParam.showWindow = false; net = train(net, X_train, Y_train); Y_pred_norm = net(X_test); Y_pred = mapminmax('reverse', Y_pred_norm, ps_output); rmse = sqrt(mean((Y_pred - Y_raw(valInd == 1, :)).^2)); fprintf('Hidden=%d, RMSE=%.4f\n', hidden_nodes, rmse); end

这段代码里的feedforwardnet(hidden_nodes)会自动创建一个带一个隐层的BP网络,默认激活函数隐层是tansig、输出层是purelin。回归问题输出层用purelin是标准做法,因为tansig会限制输出范围到[-1,1],当目标值反归一化后超出这个范围时预测会被截断。如果你发现预测值是一条水平线,先检查输出层激活函数是不是被默认成了tansig。

训练函数trainlm是Levenberg-Marquardt算法,收敛快、精度高,适合中小规模数据集。它的问题是在数据集大或者权值数量多时内存占用高,而且容易陷入局部极值。另一个选择是trainbr(贝叶斯正则化),它通过引入权值衰减项来抑制过拟合,对小样本数据更友好。我通常先跑trainlm,如果验证集表现不理想再切trainbr。

2.3 训练参数怎么设:学习率、迭代次数与优化器

Matlab的feedforwardnet把学习率和迭代次数封装在net.trainParam里,新手经常不知道改哪里。核心参数有三个:lr(学习率)、epochs(最大迭代次数)、goal(目标误差)。默认lr是0.01,epochs是1000。多数情况下默认值能用,但数据噪声大或特征量级差异大时,需要手动调。

学习率过大,权值更新步长太大,loss曲线会震荡发散;学习率过小,收敛慢,1000次迭代跑完还没到最优解。我平时会准备一组候选学习率,比如0.001、0.005、0.01、0.05,分别训练相同迭代次数,比较测试集RMSE。这种网格搜索虽然笨,但在样本量不大时非常可靠。

另一个常被忽略的参数是max_fail,它控制验证集误差连续不下降多少次后停止训练。默认值是6,意思是验证集误差连续6次迭代没有改善就早停。对于噪声数据,这个值可以调大到10或15,避免因为验证集局部抖动而过早停止。

% 搭建网络并设置训练参数 net = feedforwardnet(6); net.trainFcn = 'trainlm'; net.trainParam.lr = 0.01; % 学习率 net.trainParam.epochs = 2000; % 最大迭代次数 net.trainParam.goal = 1e-5; % 目标误差 net.trainParam.max_fail = 10; % 验证集连续不改善容忍次数 net.divideFcn = 'divideind'; net.divideParam.trainInd = trainInd; net.divideParam.valInd = valInd; net.divideParam.testInd = testInd; % 训练网络 [net, tr] = train(net, X_norm', Y_norm');

注意这里的划分方式变了:前面我用dividerand手动划分,这里用divideind告诉train函数直接使用已有的索引。这样可以保证训练过程中验证集的划分完全可控,不会因为train内部重新随机划分而让结果不可复现。tr结构体里记录了每次迭代的训练误差和验证误差,用它画loss曲线可以判断是否收敛。

3. 在Matlab里跑通完整程序:代码结构与落地步骤

3.1 数据加载与预处理:从Excel到网络输入格式

实际工程中数据大概率存放在Excel或CSV里,第一行是列名还是直接从第一行开始,这直接影响读取方式。我一般要求数据文件不含表头,或者读取时指定行范围。把数据加载和预处理写成一个独立的脚本段,后面换数据只改文件路径和列范围,不碰建模逻辑。

%% 数据加载 clc; clear; close all; rng(42); % 固定随机种子,保证实验可复现 filename = 'data.xlsx'; data = xlsread(filename); % 读取数值矩阵,忽略文本列 % 检查数据是否有缺失值 if any(isnan(data(:))) error('数据包含NaN,请先处理缺失值'); end X_raw = data(:, 1:end-1); Y_raw = data(:, end); fprintf('样本数:%d,特征数:%d\n', size(X_raw, 1), size(X_raw, 2));

数据加载这一步最常踩的坑是Excel文件里混入了文本列,xlsread会把这些列读成NaN。如果某列是编号、日期这类不参与建模的字段,读取前先在Excel里删掉,或者在读取后用isnan检查。还有一种情况是特征列中包含0值或空值,mapminmax遇到极值会拉偏归一化范围,需要先做异常值处理。

%% 数据清洗与归一化 % 去除异常值:目标值超过均值±3倍标准差的样本视为异常,替换为边界值 mu = mean(Y_raw); sigma = std(Y_raw); Y_raw(Y_raw > mu + 3*sigma) = mu + 3*sigma; Y_raw(Y_raw < mu - 3*sigma) = mu - 3*sigma; % 归一化 [X_norm, ps_input] = mapminmax(X_raw', -1, 1); [Y_norm, ps_output] = mapminmax(Y_raw', -1, 1); X_norm = X_norm'; Y_norm = Y_norm';

异常值截断处理是回归预测里容易被忽视的一步。如果目标值里有几个因传感器故障或录入错误产生的极端值,训练时模型会把大量权值用来拟合这几个点,正常数据的预测精度反而下降。用3σ原则截断不是唯一方案,也可以用分位数截断,效果取决于数据分布。这一步做完后,打印一下Y_raw的分布范围,确认截断后的极值是否合理。

3.2 网络搭建与训练:feedforwardnet还是newff

Matlab里搭BP网络有两个入口:feedforwardnet和newff。newff是旧版接口,虽然还在用但官方推荐逐步迁移到feedforwardnet。feedforwardnet返回的是一个完整的网络对象,自带数据划分策略和训练参数默认值,结构清晰。如果你见过老代码里用newff,功能上两者等价,但newff的参数传递方式更繁琐。

%% 搭建BP神经网络并训练 hidden_nodes = 6; net = feedforwardnet(hidden_nodes); % 设置训练集、验证集、测试集划分 net.divideFcn = 'divideind'; net.divideParam.trainInd = trainInd; net.divideParam.valInd = valInd; net.divideParam.testInd = testInd; % 设置训练函数与参数 net.trainFcn = 'trainlm'; net.trainParam.epochs = 2000; net.trainParam.goal = 1e-5; net.trainParam.min_grad = 1e-7; net.trainParam.max_fail = 10; % 初始化权重(默认使用Nguyen-Widrow初始化,保持默认即可) net = init(net); % 开始训练 [net, tr] = train(net, X_norm', Y_norm');

net = init(net)这一步经常被省略,但如果你发现每次训练结果差异很大,多半是权值初始化随机性导致的。init用Nguyen-Widrow方法初始化权值和偏置,它能让隐层神经元的激活函数落在有效区间内,避免训练初期梯度饱和。即便设置了rng(42),执行init也能让初始权值更利于tansig激活,减少训练震荡。

训练过程中Matlab会弹出训练窗口,显示当前迭代次数、误差、梯度和验证集表现。这个窗口在脚本自动化跑大量实验时会拖慢速度,可以用set(0, 'DefaultFigureVisible', 'off')关掉图形窗口,训练完成后只读取结果。不过第一次跑建议开着窗口,直观感受一下loss曲线的下降过程。

3.3 预测、反归一化与可视化:别让结果图骗了你

训练完成后核心工作是预测和反归一化。常见的错误是直接net(X_test)得到归一化预测值,然后拿它和原始Y_test对比,画出来的图当然对不上。正确的流程分三步:用训练好的网络预测归一化目标值,用ps_output反归一化回真实量纲,再和真实值对比计算误差指标。

%% 测试集预测与反归一化 Y_pred_norm = net(X_test); Y_pred = mapminmax('reverse', Y_pred_norm, ps_output); % 提取测试集的真实目标值 Y_true = Y_raw(testInd); % 计算评价指标 R2 = 1 - sum((Y_true - Y_pred).^2) / sum((Y_true - mean(Y_true)).^2); RMSE = sqrt(mean((Y_true - Y_pred).^2)); MAE = mean(abs(Y_true - Y_pred)); fprintf('R2 = %.4f\nRMSE = %.4f\nMAE = %.4f\n', R2, RMSE, MAE); %% 绘图:真实值与预测值对比 figure; plot(1:length(Y_true), Y_true, 'b-o', 'LineWidth', 1.5); hold on; plot(1:length(Y_pred), Y_pred, 'r-^', 'LineWidth', 1.5); legend('真实值', '预测值'); xlabel('测试样本序号'); ylabel('目标值'); title('BP神经网络回归预测结果对比'); grid on;

绘制对比图是判断模型好坏最直观的手段。好的预测结果应该是一条与真实值几乎重合的折线,偏差大的样本会表现为尖峰。如果发现预测值整体比真实值低,可能存在系统性偏移,常见原因是归一化后输出层激活函数限制了范围;如果预测值波动剧烈而真实值平缓,则是过拟合。

误差分布图同样值得画:把预测误差(Y_pred - Y_true)用直方图展示,看误差是否近似正态分布且均值接近0。如果误差直方图明显偏斜或有双峰,说明模型遗漏了某个关键特征或数据中存在分组结构。这个诊断信息比R²数值更具体,值得每次实验都看一眼。

4. BP回归预测的避坑指南:5个让结果翻车的常见问题

4.1 预测值是一条水平线:激活函数与归一化的连锁反应

现象:测试集预测值几乎是一个常数,和真实值曲线完全脱节。
原因:输出层激活函数用了tansig,把预测值限制在[-1,1]区间且取值趋于饱和;或者输入数据归一化出了问题,特征值被压缩到极小的范围内,网络学不到有效信息。
解决:先确认输出层激活函数是purelin,在feedforwardnet默认配置下不会出错,但如果你手动改过layers属性要重点检查。然后检查归一化后的输入数据方差,标准做法是打印max(X_norm)和min(X_norm),正常情况下应该接近1和-1。如果数据本身方差很小,归一化后也会被拉满,这是正常的;但如果是特征列全为常数,那这一列等于没喂信息,可以直接删掉。

4.2 训练集拟合很好,测试集误差大:过拟合的识别与止损

现象:训练集R²接近0.99,测试集R²只有0.6,差值十分明显。
原因:隐层节点数过多,模型把训练集中的噪声也学进去了;或者数据量太小,不足以支撑大网络的训练。
解决:降低隐层节点数是最直接的手段,从当前值向上递减试几组。另一种做法是改用trainbr训练函数,它通过自动调整权值衰减强度,在每轮迭代中惩罚过大的权值,对抑制过拟合效果非常明显。修改只需一行:net.trainFcn = 'trainbr'。如果改了之后测试集R²明显上升而是训练集略有下降,这是正常现象,说明模型从死记硬背转向了泛化。

4.3 R²出现负值:评价指标与数据划分的双重陷阱

现象:R²计算结果为负,说明模型预测比简单用均值预测还要差。
原因:测试集样本数量太少,比如总共30个样本,测试集只分了3个,那么R²计算的分母(真实值与均值的平方和)极不稳定,一个极值点就可能让结果变成负的。另一种原因是数据划分前没有打乱,测试集恰好落在某段特殊区间,和训练集分布差异很大。
解决:检查测试集样本数和分布范围,样本数小于10时考虑增加测试集比例或改用交叉验证。划分前用randperm对样本索引做乱序,保证测试集覆盖全范围。如果做了乱序后R²仍然为负,说明模型确实没学到有效模式,回头检查特征选择和归一化。

4.4 程序跑很久不收敛:学习率与初始权重的玄学

现象:训练窗口里误差曲线长时间不下降,或者loss在某个值附近来回震荡。
原因:学习率设置过大,权值更新步长跨越了最优点;或者网络初始化时权值过大,导致tansig激活函数进入饱和区,梯度接近0,参数不再更新。
解决:把学习率从0.01降到0.001或0.005重新跑。如果仍然不收敛,重新执行net = init(net)再训练,很多时候换个初始点就好了。这里说一句血泪经验:BP训练的初始权值是玄学,同一份数据同一个参数配置,不同随机种子可能带来完全不同的结果。固定rng、多做几次重复实验取均值,是验证模型稳定性的唯一办法。

4.5 更换数据集后结果差异大:随机种子与数据划分的坑

现象:换了一组相似数据后,模型表现忽好忽坏,无法稳定复现。
原因:数据划分是靠随机索引完成的,每次运行train或dividerand都会重新生成随机数,导致训练集和测试集的组成不一样,结果自然不可比。
解决:在脚本开头固定rng(42)只解决了Matlab全局随机数状态,但dividerand内部可能使用不同的随机流。更可靠的做法是把划分好的索引保存到mat文件里,每次实验加载同一份索引。train函数里的divideParam只要设置为divideind并传入固定索引,就能保证数据集划分完全一致,这样模型差异只来自权值初始化随机性,便于定位问题。

5. 数据回归预测的精度上限:把BP的结果再推进一步

5.1 用贝叶斯正则化训练trainbr抑制过拟合

标准BP用trainlm追求最小化拟合误差,而trainbr在误差函数中加入了一项权值平方和,训练过程变成了同时最小化误差和权值大小的多目标优化。这对小样本仿真数据预测特别友好,因为数据少时网络有大量冗余自由度,不加约束必然过拟合。

%% 使用trainbr重新训练,适合小样本数据 net_br = feedforwardnet(hidden_nodes); net_br.trainFcn = 'trainbr'; net_br.divideFcn = 'divideind'; net_br.divideParam.trainInd = trainInd; net_br.divideParam.valInd = valInd; net_br.divideParam.testInd = testInd; net_br.trainParam.epochs = 3000; [net_br, tr_br] = train(net_br, X_norm', Y_norm'); Y_pred_br_norm = net_br(X_test); Y_pred_br = mapminmax('reverse', Y_pred_br_norm, ps_output);

用trainbr有个信号可以判断训练质量:训练结束后查看net_br.trainFcn的收敛信息,如果迭代在达到epochs上限前就停止,说明算法找到了一个稳定的权值分布。trainbr不需要单独设置验证集来早停,因为它通过正则化已经自动控制了复杂度,所以代码里valInd仍然划分了,但trainbr内部对验证集的使用方式不一样。实测中trainbr在小样本(几十到几百条)数据上往往能比trainlm获得更好的测试集R²,代价是训练时间略长。

5.2 用交叉验证选择隐层节点数:网格搜索的落地写法

前面讲过用for循环扫隐层节点数,但单次划分的测试集结果受随机性影响大。更稳妥的做法是K折交叉验证:把数据分成K份,轮流取其中1份做测试,其余K-1份训练,最后把K次测试指标的平均值作为该节点数的评分。

%% 5折交叉验证选择隐层节点数 rng(42); K = 5; indices = crossvalind('Kfold', size(X_norm, 1), K); hidden_options = 3:2:13; cv_rmse = zeros(length(hidden_options), K); for h = 1:length(hidden_options) for k = 1:K test_idx = (indices == k); train_idx = ~test_idx; net = feedforwardnet(hidden_options(h)); net.trainFcn = 'trainlm'; net.divideFcn = 'divideind'; net.divideParam.trainInd = find(train_idx); net.divideParam.valInd = []; net.divideParam.testInd = find(test_idx); net = train(net, X_norm', Y_norm'); y_pred_norm = net(X_norm(:, test_idx)); y_pred = mapminmax('reverse', y_pred_norm, ps_output); cv_rmse(h, k) = sqrt(mean((Y_raw(test_idx) - y_pred).^2)); end fprintf('Hidden=%d, CV RMSE=%.4f±%.4f\n', ... hidden_options(h), mean(cv_rmse(h,:)), std(cv_rmse(h,:))); end

这段代码有几个细节:验证集被设成空,因为K折里每一折本身就有测试集做评估,不需要再划验证集;indices向量是全局生成的,保证K折划分是同一套,否则每折比较时数据分布不同会让结果失真。最后选平均CV RMSE最小的隐层节点数,再固定这个值用全部数据训练最终模型。crossvalind函数在较新版本的Matlab里属于Bioinformatics Toolbox,如果你没有这个工具箱,可以用randperm手动实现等分划分。

5.3 和多元线性回归、高斯过程回归做对比:知道BP的上限在哪

BP不是万能的,线性关系强的数据用多元线性回归几行代码就能得到几乎相同的结果,非线性关系复杂但样本量很小的数据,高斯过程回归可能表现更好。做预测时把这几类模型放在一起对比,能帮你判断BP是否真的发挥了作用。

%% 对比:多元线性回归 X_design = [ones(size(X_raw, 1), 1), X_raw]; beta = X_design(trainInd, :) \ Y_raw(trainInd); Y_pred_lr = X_design(testInd, :) * beta; R2_lr = 1 - sum((Y_raw(testInd) - Y_pred_lr).^2) / sum((Y_raw(testInd) - mean(Y_raw(testInd))).^2); fprintf('线性回归 R2 = %.4f\n', R2_lr);

回归预测的精度上限不取决于模型复杂度,而取决于数据本身包含的信息量。如果线性回归的R²已经达到0.9,BP即便调参到最优也可能只提升0.01,这时候把精力花在特征工程上更有价值。如果线性回归R²低于0.5,而BP能到0.8以上,说明数据存在明显的非线性关系,BP的方案才算物有所值。高维数据、样本量过千的场景则优先考虑高斯过程回归或集成方法,BP的训练速度会拖后腿。

6. 我常用的验证手段:把预测结果用起来

模型训练完、指标也算了,但交出去之前我还会做三个验证。第一个是残差分析:画残差(Y_true - Y_pred)关于Y_pred的散点图,如果残差随机分布在0轴附近且没有喇叭口形发散,说明模型误差是均匀的,可以放心用;如果残差随预测值增大而增大,说明模型对高值区的拟合能力不足,需要检查数据是否经过对数变换。第二个是敏感性测试:把测试集里某个特征统一加上5%的噪声,观察预测值变化幅度,如果波动剧烈,说明模型对噪声敏感,部署时数据采集质量必须严格把关。

第三个是保存和复用模型。训练好的net对象可以保存为mat文件,部署时直接加载,不需要重新训练。

%% 保存模型与映射参数 save('bp_model.mat', 'net', 'ps_input', 'ps_output');

之后预测新数据只需要三步:用ps_input归一化新数据,net.predict得到归一化预测,ps_output反归一化输出。要注意新数据的特征列顺序必须和训练时完全一致,特征数也不能变。我踩过因为训练时特征顺序写错、换了个数据源后预测结果完全乱掉的坑。还有一个使用习惯:每次训练完顺手把tr里的最终误差也存一下,配合模型一起保存,下次回溯时能看到这个模型的训练终点是什么样。

%% 加载模型并预测新数据 model = load('bp_model.mat'); X_new = xlsread('new_data.xlsx'); X_new_norm = mapminmax('apply', X_new', model.ps_input); Y_new_norm = model.net(X_new_norm); Y_new = mapminmax('reverse', Y_new_norm, model.ps_output);

mapminmax的apply参数是关键,它对新数据使用已经保存好的ps_input做映射,而不是重新计算归一化范围。如果用了默认的mapminmax(X_new)而不是apply,归一化范围会和训练时不一致,预测结果完全失真。这条我反复提醒过自己无数次,写在这里希望帮到你。

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

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

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

立即咨询