简介:这份资源是第九届MathorCup数学建模挑战赛的获奖论文PDF,面向备战数模竞赛的高校学生与指导教师,聚焦钢水“脱氧合金化”配料方案优化这一典型赛题。论文完整呈现了从数据预处理、收得率计算到建模求解的全过程:先剔除异常数据并按钢种分类,建立一般收得率模型与基于参考炉次的自学习模型,得出C、Mn平均历史收得率分别为84.29%和91.18%,再通过灰色关联度分析筛选关键影响因素,继而用BP神经网络预测收得率并引入粒子群算法优化,最后建立最小成本模型,借助Linprog()求解使成本降低12.59%。资源包为1个PDF文件,约996KB,篇幅紧凑但内容完整,涵盖问题重述、模型假设、符号说明及各问建模求解章节。目前已有123人学习,适合希望学习数据处理、灰色预测、神经网络与线性规划综合应用的读者参考借鉴。
1. 从一份 40 页的 D 题论文说起:脱氧合金化配料优化到底在算什么
炼钢转炉出钢那几分钟,决定成本的地方不在炉子本身,而在往钢包里加什么、加多少。第 9 届 MathorCup 数学建模挑战赛 D 题给的就是这个场景:附件一是 1700 多炉历史数据,附件二是合金料价格表,要求算 C、Mn 收得率、预测收得率、再拿预测值做最小成本配料。这份获奖论文(队伍号 904604)把四个问题串成了一条完整链路——数据预处理、收得率建模、BP 神经网络加粒子群优化、线性规划配料,最后附了三大段 MATLAB 代码。它适合两类人:正在准备数学建模竞赛、想找一份带完整代码和推导的实战范本;以及做钢铁冶金数据分析、需要一套从收得率反算到配料优化的可复现流程。我拆完这份 PDF 最大的感受是,它的价值不在模型多高级,而在每一步都留了可验证的中间结果——收得率算出来是多少、哪些因素被选进来了、成本降了几个点,这些数字才是能抄作业的地方。
2. 数据预处理与收得率计算:从 1716 炉原始数据到可用的 C、Mn 收得率
2.1 收得率公式怎么落地成可算的表达式
论文给的收得率定义是「被钢水吸收的合金元素重量与加入该元素总重量之比」,落到公式上就是 5-1 式。这个式子看着简单,但真正动手算之前得先把每个符号对应到附件一的哪一列想清楚。钢水质量变化 ΔY 论文做了理想化处理,忽略出渣量,只算合金加入总量;连铸正样含量 N 和转炉终点含量 M 分别对应附件里两个不同工序的化验值。我一般会先把附件一按炉次号排序,确认每一炉的转炉终点数据和连铸正样数据能对上,再套公式。这里有个容易翻车的地方:合金加入量那一列如果有多种合金,得先按元素含量折算成该元素的总加入量,不能直接拿合金重量当分母。
import pandas as pd import numpy as np # 读取附件一,假设列名已按工序整理 df = pd.read_excel('附件1.xlsx') # 第一步:剔除缺失数据 # C 收得率计算:第 811 炉后连铸正样 C 缺失,剔除 df_c = df[df['炉次号'] <= 811].copy() # Mn 收得率计算:第 252 炉后转炉终点 Mn 缺失,剔除 df_mn = df[df['炉次号'] <= 252].copy() # 第二步:剔除异常数据 # 转炉终点温度为 0、转炉终点 C 或 Mn 含量为 0 的炉次 for col in ['转炉终点温度', '转炉终点C', '转炉终点Mn']: df_c = df_c[df_c[col] != 0] df_mn = df_mn[df_mn[col] != 0] # 第三步:按钢种钢号分类 df_c['钢种分类'] = df_c['钢号'].apply(lambda x: '普碳' if 'Q' in str(x) else '低合金')这段代码的逻辑是按论文 5.1 节的三步走:先剔缺失、再剔异常、最后分类。参数上要注意,论文明确写了 C 收得率计算剔除第 811 炉之后的数据,Mn 收得率剔除第 252 炉之后的数据,这两个截断点不能搞反。异常值判断里转炉终点温度为 0 是硬性异常,因为温度为零根本没法进行脱氧合金化;转炉终点 C、Mn 含量为 0 同理,实际冶炼中不可能出现。跑完这三步,C 收得率可用数据从 1716 炉降到 963 炉,Mn 收得率可用数据降到 1489 炉,这个损耗率在冶金数据里算正常的。
2.2 C 收得率大于 1 怎么处理:电极增碳与参考炉次自学习
算完一般收得率后,论文发现约六分之一的 C 收得率大于 1。这个现象在冶金里不玄学,原因是精炼加热时电极会增碳,钢水实际碳含量比加入合金带来的碳多,收得率自然可能超过 100%。论文的处理思路是建一个基于参考炉次的自学习模型:对新炉次,从历史数据里找生产条件最接近的 5 个炉次,用它们的收得率均值作为新炉次的收得率,同时把电极增碳量从分子里扣掉。参考炉次的选取用 5-2 式,本质是算新炉次和候选炉次在 C、Al、Si 三个元素上的偏差和,偏差和最小的 5 个就是参考炉次。
def select_reference_heats(new_heat, history_df, k=5): """ 选取参考炉次:计算新炉次与历史炉次在 C、Al、Si 上的偏差和 new_heat: dict, 包含 'C', 'Al', 'Si' 三个键 history_df: 历史炉次 DataFrame,同样包含这三列 """ history_df = history_df.copy() history_df['偏差和'] = ( abs(history_df['C'] - new_heat['C']) + abs(history_df['Al'] - new_heat['Al']) + abs(history_df['Si'] - new_heat['Si']) ) # 取偏差和最小的 k 个炉次 ref_heats = history_df.nsmallest(k, '偏差和') return ref_heats # 电极增碳量:论文给出理想化条件下吨钢平均增碳 0.0291%/h # 实际使用时按加热时长折算 def calc_c_yield_with_decarb(heat_data, ref_heats, heating_hours): avg_yield = ref_heats['C收得率'].mean() decarb_correction = 0.0291 * heating_hours / 100 # 折算成质量分数 corrected_yield = avg_yield - decarb_correction return corrected_yield参考炉次法的关键参数是 k=5,论文明确选了 5 个,这个数不是随便定的——太少则均值不稳,太多则参考炉次的生产条件差异过大。电极增碳量 0.0291%/h 是论文查资料后理想化的结果,实际应用时如果加热时长数据可得,按小时折算;如果不可得,论文的做法是直接把这个修正项当常数处理。优化后 C 收得率大于 1 的比例从六分之一降到约三十分之一,剩下那 20 个异常数据论文判断是合金加入量本身有问题,直接删除。最终 C 平均历史收得率 84.29%,Mn 平均历史收得率 91.18%,这两个数在后面 BP 神经网络预测时可以作为基准参考。
2.3 灰色关联度分析:7 个输入变量是怎么选出来的
论文用灰色关联度分析来筛影响收得率的因素,这个方法在数据量不大、分布规律不明显时比皮尔逊相关系数更稳。具体做法是以优化后的 C、Mn 收得率为参考序列,以转炉终点温度、转炉终点 C/Mn/S/P/Si 含量、各种合金加入量为比较序列,先做均值化无量纲处理,再算关联系数和关联度。分辨系数取 0.5,这是灰色关联度的常规取值,值越小分辨率越大,但太小会导致关联度区分度下降。论文最终为 C 收得率选了 7 个关联度较大的因素:转炉终点温度、转炉终点 C、转炉终点 S、转炉终点 Si、钒铁(FeV50-B)、锰硅合金、碳化硅(55%);为 Mn 收得率也选了 7 个:转炉终点温度、转炉终点 C、转炉终点 S、转炉终点 Si、硅铝合金 FeAl30Si25、石油焦增碳剂、锰硅合金。
| 因素 | C 收得率关联度 | Mn 收得率关联度 |
|---|---|---|
| 转炉终点温度 | 0.8589 | 0.8566 |
| 转炉终点 C | 0.8023 | 0.7466 |
| 转炉终点 S | 0.8546 | 0.7706 |
| 转炉终点 Si | 0.8225 | 0.7415 |
| 锰硅合金 | 0.8838 | 0.9178 |
| 碳化硅(55%) | 0.8388 | 0.7743 |
这张表里锰硅合金对 Mn 收得率的关联度高达 0.9178,是全部因素里最高的,说明锰硅合金加入量对 Mn 收得率的影响最直接。转炉终点 P 和转炉终点 Mn 的关联度都在 0.65 以下,论文没把它们选进预测模型的输入,这个取舍在后续 BP 神经网络训练时能减少输入维度,降低过拟合风险。做灰色关联度时有个坑:数据必须先无量纲化,论文用的是均值化算子,不是初值化也不是标准化,这个选择会影响关联度数值但不影响排序,如果复现时结果排序对不上,先检查无量纲方法是否一致。
3. BP 神经网络预测收得率:7 输入 1 输出怎么调,粒子群优化改了什么
3.1 网络结构确定:输入层 7 节点、输出层 1 节点、隐含层怎么定
问题二的核心是用问题一筛出的 7 个因素预测 C、Mn 收得率。论文的 BP 神经网络结构是输入层 7 个节点(对应 7 个因素),输出层 1 个节点(对应收得率),隐含层节点数靠反复调试确定。C 收得率预测用了前 620 组数据做样本,Mn 收得率预测用了前 248 组数据做样本,这个样本量差异是因为 Mn 数据在预处理后剩得更少。隐含层节点数的经验公式一般是sqrt(输入层+输出层)+a,a 取 1 到 10,但论文没写具体用了多少,只说了「反复调试确定」。我一般会从 4 开始试,逐步加到 15,看训练集和验证集的误差曲线,选验证误差最低的那个点。
import numpy as np from sklearn.neural_network import MLPRegressor from sklearn.preprocessing import StandardScaler from sklearn.model_selection import train_test_split # 假设 X_c 是 C 收得率的 7 个输入因素,y_c 是 C 收得率 # X_c 形状 (n_samples, 7),y_c 形状 (n_samples,) # 数据标准化:BP 神经网络对输入尺度敏感 scaler = StandardScaler() X_c_scaled = scaler.fit_transform(X_c) # 划分训练集和测试集,论文用前 620 组做样本 X_train, X_test, y_train, y_test = train_test_split( X_c_scaled, y_c, test_size=0.2, random_state=42, shuffle=False ) # 隐含层节点数调试:从 4 到 15 best_score = float('inf') best_hidden = None for hidden in range(4, 16): mlp = MLPRegressor( hidden_layer_sizes=(hidden,), activation='logistic', # 论文用的 Sigmoid 类激活函数 solver='adam', max_iter=2000, learning_rate_init=0.01, random_state=42 ) mlp.fit(X_train, y_train) score = mlp.score(X_test, y_test) if score < best_score: best_score = score best_hidden = hidden print(f'最佳隐含层节点数: {best_hidden}')这段代码里几个参数需要说明。activation='logistic'对应论文里的 Sigmoid 激活函数,BP 神经网络经典配置;solver='adam'是自适应学习率优化器,比原始梯度下降收敛快;max_iter=2000是最大迭代次数,论文里设了最大训练次数让网络自动停止。shuffle=False是因为论文按时间顺序取前 620 组做样本,不是随机打乱,这个细节在复现时如果搞错,训练集和测试集的分布会不一致。隐含层节点数调试时,如果验证误差曲线一直下降不反弹,说明节点数还可以再加;如果训练误差降但验证误差升,就是过拟合了,得往回减。
3.2 粒子群算法优化 BP:惯性权重和学习因子怎么设
论文在 BP 神经网络之后加了粒子群算法做优化,目的是提高预测精度、减少拟合误差。粒子群优化的本质是找 BP 网络的最优初始权值和阈值,因为 BP 对初始值敏感,初始值不好容易陷局部最优。粒子群里每个粒子代表一组权值阈值组合,粒子的位置更新靠速度,速度更新靠个体最优和全局最优。论文没给具体的粒子群参数,但常规配置是:种群规模 20 到 50,惯性权重从 0.9 线性降到 0.4,学习因子 c1=c2=2,最大迭代 100 到 200 代。
import numpy as np class PSO_BP: def __init__(self, n_particles=30, n_iter=100, w_start=0.9, w_end=0.4, c1=2.0, c2=2.0): self.n_particles = n_particles self.n_iter = n_iter self.w_start = w_start self.w_end = w_end self.c1 = c1 self.c2 = c2 def optimize(self, X, y, hidden_size): # 粒子维度 = 输入层到隐含层权值 + 隐含层阈值 + 隐含层到输出层权值 + 输出层阈值 n_input = X.shape[1] dim = n_input * hidden_size + hidden_size + hidden_size * 1 + 1 # 初始化粒子位置和速度 positions = np.random.uniform(-1, 1, (self.n_particles, dim)) velocities = np.random.uniform(-0.1, 0.1, (self.n_particles, dim)) pbest = positions.copy() pbest_score = np.array([self._fitness(p, X, y, hidden_size) for p in positions]) gbest = pbest[np.argmin(pbest_score)] gbest_score = np.min(pbest_score) for it in range(self.n_iter): # 惯性权重线性递减 w = self.w_start - (self.w_start - self.w_end) * it / self.n_iter for i in range(self.n_particles): r1, r2 = np.random.rand(dim), np.random.rand(dim) velocities[i] = (w * velocities[i] + self.c1 * r1 * (pbest[i] - positions[i]) + self.c2 * r2 * (gbest - positions[i])) positions[i] = positions[i] + velocities[i] score = self._fitness(positions[i], X, y, hidden_size) if score < pbest_score[i]: pbest[i] = positions[i].copy() pbest_score[i] = score if score < gbest_score: gbest = positions[i].copy() gbest_score = score return gbest, gbest_score def _fitness(self, particle, X, y, hidden_size): # 把粒子解码成权值阈值,前向传播算 MSE n_input = X.shape[1] idx = 0 w1 = particle[idx:idx + n_input * hidden_size].reshape(n_input, hidden_size) idx += n_input * hidden_size b1 = particle[idx:idx + hidden_size] idx += hidden_size w2 = particle[idx:idx + hidden_size].reshape(hidden_size, 1) idx += hidden_size b2 = particle[idx] # Sigmoid 前向 h = 1 / (1 + np.exp(-(X @ w1 + b1))) out = h @ w2 + b2 return np.mean((out.flatten() - y) ** 2)粒子群优化的参数里,惯性权重从 0.9 降到 0.4 是标准线性递减策略,前期偏全局搜索、后期偏局部收敛。学习因子 c1 和 c2 都取 2.0 是经典值,c1 控制个体认知、c2 控制社会认知,如果 c1 太大粒子容易散、c2 太大容易早熟。_fitness函数里把粒子解码成权值阈值后做一次前向传播算 MSE,这个 MSE 就是粒子的适应度。实际跑的时候,粒子群优化完的权值阈值再赋给 BP 网络做精细训练,论文里说的「提高预测准确性、减少拟合预测误差」就是这个意思。C 收得率预测误差整体在 -0.1 到 0.1 内,Mn 收得率误差在 -0.05 到 0.05 内,Mn 的预测精度比 C 高,因为 Mn 收得率本身波动小、没有大于 1 的异常情况。
3.3 预测结果怎么验证:误差图和拟合曲线看什么
论文给了 C 和 Mn 的收得率预测图和误差图。C 收得率预测值最高接近 1,最低 0.55 左右,大部分落在 0.75 到 0.95;误差整体在 -0.1 到 0.1,少数到 -0.2 到 0.2,极少数到 -0.3 到 0.3。Mn 收得率预测值最高 0.95 左右,最低 0.75 左右,大部分在 0.85 到 0.95;误差整体在 -0.05 到 0.05。看误差图时重点看两件事:一是误差有没有随样本序号出现趋势性偏移,如果有说明模型对时间维度的泛化不行;二是误差的绝对值有没有在某个区间突然放大,如果有说明那批炉次的生产条件可能和训练集差异大。论文的误差图没有明显趋势偏移,说明按时间顺序取前 620 组做训练是合理的。复现时如果误差比论文大,先检查输入因素的量纲是否统一、标准化是否做了、粒子群迭代次数够不够。
4. 最小成本配料模型:Linprog 求解与 12.59% 成本降幅怎么来的
4.1 约束条件怎么列:为什么只考虑三种元素
问题三要建最小成本模型,目标函数是合金配料总成本,约束条件是钢水成分达标。论文明确说了 S、P 是有害元素必须达国家标准,但在用浓度约束合金配料用量时不予考虑,只考虑余下三种元素。这个取舍的逻辑是:S、P 的达标主要靠铁水预处理和转炉冶炼控制,脱氧合金化阶段加入的合金对 S、P 含量的影响很小,把它们放进约束里反而会让模型复杂化且对结果影响不大。选用的 6 种主要合金配料做线性规划约束,目标函数是各合金加入量乘以单价求和。
from scipy.optimize import linprog import numpy as np # 假设 6 种合金的单价(元/吨) prices = np.array([price1, price2, price3, price4, price5, price6]) # 约束:钢水成分达标 # A_ub @ x <= b_ub 形式 # 每种合金对目标元素的贡献 = 加入量 * 合金中该元素含量 * 收得率 # 收得率用问题二预测值 # 以 C、Mn、Si 三种元素为例,每种元素有上下限 # 下限约束:-A_lower @ x <= -b_lower # 上限约束:A_upper @ x <= b_upper A_ub = np.vstack([-A_lower, A_upper]) b_ub = np.hstack([-b_lower, b_upper]) # 变量下界:合金加入量不能为负 bounds = [(0, None) for _ in range(6)] # 线性规划求解 result = linprog( c=prices, A_ub=A_ub, b_ub=b_ub, bounds=bounds, method='highs' # 单纯形法的现代实现 ) if result.success: print(f'最优配料方案: {result.x}') print(f'最低成本: {result.fun}') else: print(f'求解失败: {result.message}')linprog的method='highs'是 SciPy 现在推荐的求解器,底层用的是单纯形法变体,比老版本的simplex更稳。约束矩阵A_ub的构造是关键:每种元素的下限约束要取负号变成小于等于形式,上限约束直接是小于等于。合金对元素的贡献要乘收得率,C 和 Mn 的收得率用问题二的预测值,Si 的收得率论文没单独预测,一般用历史均值。变量下界全设 0,因为合金加入量不可能为负。论文用这个模型算出来成本减少 12.59%,再选达标的炉次分析,平均减少成本 10.32%。这两个数的差异说明优化模型对不达标炉次的成本改善更明显,因为不达标炉次原本可能加了过量合金来保成分。
4.2 单纯形法求解时怎么判断结果可信
线性规划求解完不能直接信结果,得做几项检查。第一看result.status,0 表示最优解找到,1 表示迭代次数超限,2 表示不可行,3 表示无界,4 表示数值困难。第二看最优解里有没有合金加入量为 0 的情况,如果有说明该合金在成本上不划算,可以进一步分析是不是可以替换。第三做灵敏度分析,看目标函数系数(合金单价)在多大范围内变化时最优解不变,这个范围叫最优基不变区间,超出这个区间配料方案就得重新算。论文没做灵敏度分析,但实际工程应用里这一步不能省,因为合金价格是波动的。第四拿优化后的配料方案回代到收得率模型里,验证钢水成分是否真的达标,因为线性规划用的是预测收得率,预测有误差,回代能发现预测误差导致的成分偏差。
| 检查项 | 判断标准 | 异常处理 |
|---|---|---|
| 求解状态 | status=0 | 非 0 时检查约束是否矛盾 |
| 变量取值 | 无负值 | 负值说明下界设置错误 |
| 成本降幅 | 与历史方案对比 | 降幅过大需复核收得率 |
| 成分回代 | 在标准范围内 | 超差则收紧约束重算 |
5. 避坑与排查:复现这份论文时最容易翻车的五个地方
5.1 数据截断点搞反导致收得率全错
现象:算出来的 C 收得率大面积大于 1,Mn 收得率也有异常高值。原因:C 收得率计算应该剔除第 811 炉之后的数据,Mn 收得率剔除第 252 炉之后的数据,这两个截断点容易记混。如果 C 用了 252 的截断,会把大量连铸正样 C 缺失的炉次算进去,分母缺失导致收得率虚高。解决:在代码里把两个截断点写成常量并加注释,跑完后打印两个数据集的炉次号范围确认。
5.2 灰色关联度分辨系数取错导致因素排序变化
现象:复现出来的关联度排序和论文对不上,选出来的 7 个因素不一样。原因:分辨系数 ρ 论文取 0.5,如果取了 0.1 或 0.9,关联度数值会变,虽然理论上排序不变,但实际数据里接近的关联度可能因为 ρ 变化而换位。解决:固定 ρ=0.5,并且无量纲化方法用均值化算子,不要用标准化或初值化。
5.3 BP 神经网络输入未标准化导致训练不收敛
现象:网络训练误差一直不降,或者降得很慢,预测结果和实际值偏差大。原因:7 个输入因素里转炉终点温度是 1600 多,合金加入量是几吨,量纲差异巨大,不标准化的话梯度下降会被大量纲特征主导。解决:用StandardScaler做 Z-score 标准化,注意标准化参数只能从训练集算,再应用到测试集,不能全量数据一起算。
5.4 粒子群优化早熟导致权值阈值不是最优
现象:粒子群优化后的 BP 网络预测精度和没优化差不多,甚至更差。原因:粒子群种群规模太小或迭代次数不够,粒子过早聚集到局部最优;或者惯性权重没有递减,后期还在大范围搜索。解决:种群规模至少 20,迭代至少 100 代,惯性权重从 0.9 线性降到 0.4,如果还早熟就加变异算子。
5.5 线性规划约束方向写反导致无可行解
现象:linprog返回 status=2 不可行,或者解出来的配料方案成分不达标。原因:下限约束A_lower @ x >= b_lower转成A_ub形式时要取负号变成-A_lower @ x <= -b_lower,漏了负号方向就反了。解决:构造A_ub时先写上限约束再写下限约束的负形式,用np.vstack堆叠,跑之前先用一个已知可行解验证约束矩阵。
6. 从论文到落地:把 MATLAB 代码转成 Python 时我固定走的几步
这份论文的附录给了三大段 MATLAB 代码,分别是问题一、问题二、问题三的求解脚本。MATLAB 转 Python 不是逐行翻译就完事,我一般固定走四步。第一步先跑通数据读取和预处理,确认 Python 读出来的数据维度和 MATLAB 一致,特别是 Excel 读取时列名里的空格和特殊字符,MATLAB 的readtable和 pandas 的read_excel处理方式不同。第二步把收得率计算和灰色关联度用 numpy 重写,这部分是纯矩阵运算,numpy 的广播机制比 MATLAB 的bsxfun更直观,但要注意 MATLAB 的./和.*在 numpy 里就是/和*,别多写点。第三步 BP 神经网络,MATLAB 的newff和 Python 的MLPRegressor参数对应关系要理清:newff的隐含层节点数对应hidden_layer_sizes,训练函数trainlm对应solver='lbfgs',学习率lr对应learning_rate_init。第四步线性规划,MATLAB 的linprog和 SciPy 的linprog参数顺序不同,MATLAB 是linprog(f, A, b, Aeq, beq, lb, ub),SciPy 是linprog(c, A_ub, b_ub, A_eq, b_eq, bounds),约束方向也相反,MATLAB 默认A*x <= b,SciPy 也是,但 MATLAB 的Aeq是等式约束,SciPy 对应A_eq。
| MATLAB | Python | 注意点 |
|---|---|---|
| readtable | pd.read_excel | 列名空格处理 |
| newff | MLPRegressor | 激活函数对应 |
| trainlm | solver='lbfgs' | 小数据集更快 |
| linprog(f,A,b) | linprog(c,A_ub,b_ub) | 参数顺序不同 |
| grayrela | 手写灰色关联度 | 无直接对应库 |
转完之后验证方法很简单:拿论文里表 5-3 的前十五炉 C、Mn 收得率对一遍,数值对上了说明预处理和收得率计算没问题;拿表 5-6 的关联度对一遍,排序对上了说明灰色关联度没问题;拿成本降幅 12.59% 对一遍,量级对上了说明线性规划没问题。这三个验证点过了,整套流程就算复现成功。从那以后我每次转 MATLAB 代码都强制走一遍这三个验证点,不跑通不往下做,省得后面返工。希望帮到你。
本文还有配套的精品资源,点击获取