
简介基于深度学习的油井生产动态预测源码面向石油行业数据科学人员与深度学习开发者提供一套从数据加载、模型搭建到超参数调优的完整时序预测方案。项目整合CNN、RNN、LSTM、Self-Attention与Seq2Seq五种模型并利用PyTorch与Optuna完成训练和优化可对比不同网络在油井产量等动态数据上的表现。压缩包共232个文件以Python脚本58个py和可视化结果86个png为主辅以CSV数据、IPYNB示例、PyTorch权重pth及说明文档整体仅9.1MB结构紧凑便于查阅。随包内容涵盖Cushing原油期货等示例数据、误差输出文件及多个独立模型实验笔记读者可直接运行调参脚本、观察预测效果也可参考现有权重快速验证模型。目前已有134人学习适合希望将深度学习方法落地到油气生产预测场景的工程师与研究者在实际项目或论文实验中参考使用。1. 油井生产动态预测为什么传统时序方法在递减期油田上失灵把油井生产动态预测这件事交给深度学习之前我先用 ARIMA 和灰色预测硬扛了两个月结论很直接处于递减阶段的油田产量曲线带有明显的非平稳特征和频繁的工况扰动线性模型根本追不上实际波动节奏。后来切到 LSTM 这类循环神经网络才把预测误差从十几个点压到五个点以内。这套基于深度学习的油井生产动态预测源码解决的核心问题就是输入历史日产量、油压、套压、含水率等动静态参数输出未来 7 到 30 天的产量曲线为排采制度调整和措施井优选提供量化依据。它适合油气田开发工程师、算法工程师以及做时序预测研究的学生拿回去改改数据路径就能接到自己的产量数据上不需要从零开始搭网络。2. 把产量历史变成监督学习样本数据构造与预处理边界2.1 工业数据到手先别急着训练先对齐时间口径油井生产数据一般长成 CSV 表格的样子日期列、日产量列、油压、套压、含水率、动液面偶尔还有气油比和温度。这里第一个坑不是模型选型而是时间口径——有的井按自然日产记录有的井按班次记录还有的井修井期间直接留空。我会先把所有数据统一重采样到日度再去看缺失和异常。下面的代码用于读入和探查数据结构import pandas as pd import numpy as np df pd.read_csv(well_data.csv, parse_dates[date]) df df.sort_values(date).set_index(date) # 检查时间连续性和缺失情况 full_range pd.date_range(df.index.min(), df.index.max(), freqD) missing_dates full_range.difference(df.index) print(f日期范围: {df.index.min()} ~ {df.index.max()}) print(f缺失日期数量: {len(missing_dates)}) print(df[[oil_rate, tubing_pressure, water_cut]].describe())这段代码做了两件事按日期排序并检查时间轴上有没有缺口同时输出关键字段的基本统计量。注意oil_rate是日产油量吨/天tubing_pressure是油压MPawater_cut是含水率%。如果missing_dates不为空说明中间有停机或漏录不能直接进模型。工业数据里日期缺失是常态后面插值时要想清楚是自然停井还是设备问题两者处理方式不同。2.2 缺失值插补与异常值剔除别把停机日当零产量产量序列里最典型的问题是停机日、修井日产量记 0 或者整行空白。直接删掉会让序列断裂直接填 0 会把模型带偏——模型会学到“隔几天就有一个 0”预测结果在波谷处异常下探。我一般用滚动窗口内的中位数配合前向填充来处理而不是简单用均值因为产量受措施作业影响会出现突变均值会被极端值拉歪from scipy.ndimage import median_filter def clean_production_series(s, window7, z_thresh3.5): # 1. 缺失值先用前向填充兜底 s s.fillna(methodffill).fillna(s.median()) # 2. 滚动中位数作为基线 baseline pd.Series( median_filter(s.values, sizewindow), indexs.index ) # 3. 基于绝对偏差做异常检测 mad np.abs(s - baseline).rolling(window).median() normalized_dev (s - baseline) / (mad 1e-8) outliers normalized_dev z_thresh s[outliers] baseline[outliers] return s df[oil_rate] clean_production_series(df[oil_rate])这里window7对应一周的生产周期如果油田有月检制度建议改成 30 让基线更稳。z_thresh3.5是经验阈值现场数据噪声大的话放到 4.0 更稳妥避免把真实的产量抬升误判为异常。处理缺失值时用了ffill再补中位数实际中如果连续缺失超过 3 天我会额外标记一个is_shutdown特征送进模型让网络自己学习“停机后恢复”的规律而不是强行用一个值填平。2.3 滑窗切分把时序变成监督学习的样本矩阵LSTM 不能直接吃一维序列需要把历史窗口组织成(样本数, 时间步, 特征数)的三维张量。这里的关键参数是window_size用多少天历史和horizon预测未来多少天。我一般用 30 天历史预测未来 7 天既保留足够的递减趋势信息又不至于让输入维度太大导致训练变慢。滑窗构造的完整做法def create_sequences(features, target, window_size30, horizon7): X, y [], [] for i in range(len(features) - window_size - horizon 1): X.append(features[i:(i window_size)]) y.append(target[i window_size:i window_size horizon]) return np.array(X), np.array(y) # features 是归一化后的多特征矩阵target 是产量列 feature_cols [oil_rate, tubing_pressure, water_cut, casing_pressure] features df[feature_cols].values target df[oil_rate].values X, y create_sequences(features, target, window_size30, horizon7) print(f样本形状: X {X.shape}, y {y.shape})滑窗之后样本数量大约是总天数 - 30 - 7 1一口 1000 天的井能产生 960 多个样本完全够训练。如果horizon从 7 改成 30注意样本数量会进一步减少并且预测跨度越大误差累积越明显。step参数这里默认是 1样本重叠度很高训练时容易过拟合我通常会在数据量充足时把step设为 5 来降冗余。y的形状是(样本数, 7)也就是一条样本对应未来七天的产量序列用 MSE 做损失时相当于对七天内的每一天等权。2.4 数据集划分与归一化时序数据严禁随机洗牌很多第一次做产量预测的人会在这一步翻车用train_test_split默认的随机划分。时序数据一旦打乱未来信息就泄露到训练集里验证集误差会低得离谱但真实部署时预测精度立刻现原形。正确做法是按时间顺序硬切分并用训练集的统计量做归一化验证集和测试集只做变换不参与拟合from sklearn.preprocessing import MinMaxScaler # 按时间顺序划分前 70% 训练中间 15% 验证后 15% 测试 split1 int(len(df) * 0.7) split2 int(len(df) * 0.85) train_df df.iloc[:split1] val_df df.iloc[split1:split2] test_df df.iloc[split2:] scaler MinMaxScaler(feature_range(0, 1)) # 只 fit 训练集 scaled_train scaler.fit_transform(train_df[feature_cols].values) scaled_val scaler.transform(val_df[feature_cols].values) scaled_test scaler.transform(test_df[feature_cols].values)归一化的必要性与产量量纲直接相关日产油从 1 吨到 80 吨不等不压缩到 01 区间LSTM 里的 tanh 激活函数很容易饱和梯度趋近于零网络怎么训都学不动。油压和含水率虽然量纲不同但量级都在 0100 以内统一交给 MinMaxScaler 处理没问题。特别注意scaler对象在反归一化预测结果时还要用需要保留到模型落盘时一起保存。如果是做多井模型每口井单独 fit 归一化参数会让井间产量水平信息丢失更合理的做法是全部井放在一起 fit 一次保证同一套变换标准。3. 模型选型与核心实现LSTM、GRU 与超参调优3.1 为什么是 LSTM时序依赖是产量预测的关键结构油井产量本质上是一个强自相关的时序过程——今天的产量受过去几十天的工作制度、递减规律和地层能量共同影响。全连接网络把每个时间步当作独立特征完全丢失顺序关系CNN 做时序需要堆叠空洞卷积TCN 的做法实现复杂度偏高而且感受野需要人工设计。LSTM 通过门控机制把历史信息编码进隐藏状态正好匹配产量预测这个问题结构。热搜词里看到的“深度学习模型 cnn 识别恶意软件”那种场景处理的是二维输入它跟一维时序预测的选型逻辑完全不同不适合直接套用。LSTM 和 GRU 的选择上数据量几千条这个量级两者精度几乎没差距GRU 参数更少、训练速度明显更快适合做快速迭代。我一般先用 GRU 跑通全流程确认数据和预处理没问题再换 LSTM 精调。下面的模型类是完整可跑的 PyTorch 实现核心是两层 LSTM 加一个回归头import torch import torch.nn as nn class ProductionLSTM(nn.Module): def __init__(self, n_features, hidden_size64, num_layers2, horizon7, dropout0.2): super().__init__() self.lstm nn.LSTM( input_sizen_features, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, dropoutdropout ) self.regressor nn.Sequential( nn.Linear(hidden_size, 32), nn.ReLU(), nn.Dropout(dropout), nn.Linear(32, horizon) ) def forward(self, x): # x 形状: (batch, seq_len, n_features) out, _ self.lstm(x) # 取最后一个时间步的隐藏状态做预测 last_step out[:, -1, :] return self.regressor(last_step)几个参数值得展开说。batch_firstTrue让输入张量形状从(seq_len, batch, features)变成(batch, seq_len, features)省去转置的麻烦。hidden_size64是隐藏单元数数据量不大时 64 足够加大到 128 收益有限反而容易过拟合。num_layers2堆叠两层 LSTM 让网络捕捉更抽象的时序模式但超过 3 层在小数据集上会出现优化困难。dropout0.2放在层间和回归头里防止滑窗重叠造成的数据冗余引发过拟合。取out[:, -1, :]是常规做法因为最后一个时间步的隐状态包含了整个窗口的编码信息。如果要做更精细的预测可以改用注意力机制加权所有时间步的隐状态但在这个数据集规模上收益不明显。3.2 训练循环与早停看验证集损失而不是训练集产量数据量小训练集 loss 降到很低不代表模型学到了递减规律很可能只是把训练样本背了下来。我习惯在训练循环里监控验证集 loss连续多个 epoch 不下降就提前终止把最优权重存下来。训练循环的关键版本如下def train_model(model, train_loader, val_loader, epochs100, lr1e-3): optimizer torch.optim.Adam(model.parameters(), lrlr) criterion nn.MSELoss() best_val_loss float(inf) patience 15 trigger 0 for epoch in range(epochs): model.train() train_loss_sum 0.0 for X_batch, y_batch in train_loader: optimizer.zero_grad() pred model(X_batch) loss criterion(pred, y_batch) loss.backward() # 梯度裁剪防爆炸 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() train_loss_sum loss.item() * X_batch.size(0) model.eval() val_loss_sum 0.0 with torch.no_grad(): for X_batch, y_batch in val_loader: pred model(X_batch) loss criterion(pred, y_batch) val_loss_sum loss.item() * X_batch.size(0) val_loss val_loss_sum / len(val_loader.dataset) if val_loss best_val_loss: best_val_loss val_loss torch.save(model.state_dict(), best_model.pt) trigger 0 else: trigger 1 if trigger patience: print(f早停于 epoch {epoch}) break学习率lr1e-3搭配 Adam 是这个场景比较稳的起点。clip_grad_norm_这行不是摆设归一化后的数据偶尔仍会出现梯度异常裁剪到 1.0 能避免 loss 直接飞掉。patience15意味着连续 15 个 epoch 验证集不刷新就直接停省时间。如果你在本地跑batch size 建议 32 64太大会让梯度方向过于平滑太小则更新方向噪声大、训练不稳定。3.3 超参配置速查从 window_size 到 horizon 的调参顺序参数调整有个次序先把window_size定下来再谈网络宽度和深度。window_size决定模型能看到多长的历史依赖对递减期油井而言 30 天能覆盖短期工况波动如果要捕捉月检、措施效果等中期规律加到 60 天。horizon是预测长度7 天内误差可控30 天以上预测曲线趋于均值回退属于模型能力的物理边界。参数推荐区间调参方向说明window_size3060数据量大可加大覆盖更多历史工况horizon7307 天以内误差最小30 天以上曲线趋向平滑hidden_size64128小数据用 64数据超 5000 条可试 128num_layers132 层起步过拟合明显就退回 1 层dropout0.10.3滑窗重叠大时调到 0.3 防过拟合lr1e-3 5e-4loss 震荡就降不收敛就升到 2e-3batch_size3264序列长时选 32 省显存短序列用 64调参时不要所有参数一起动。先固定网络结构只调window_size找到合适的窗口后再调hidden_size和num_layers最后才动学习率和 dropout。一次只动一个变量出问题才判断得出是谁引起的。3.4 评价指标回归到原始量纲才有意义训练时用的是归一化后的 MSE向业务汇报时必须反归一化还原成吨/天。产量预测常规指标用 RMSE、MAE 和 MAPE。这里有个容易踩的细节MAPE 对产量极小值极其敏感——停机日产量为 0MAPE 直接是无穷大。所以算 MAPE 前要把测试期内产量低于某个阈值比如 1 吨的时间点剔除否则一个点就能把指标拉爆from sklearn.metrics import mean_squared_error, mean_absolute_error def evaluate_in_original_scale(model, X_test, y_test, scaler, target_idx0): model.eval() with torch.no_grad(): pred_norm model(X_test).numpy() # 把预测列和目标列拼回原特征空间反归一化 dummy np.zeros((len(pred_norm), scaler.n_features_in_)) dummy[:, target_idx] pred_norm[:, 0] pred_original scaler.inverse_transform(dummy)[:, target_idx] dummy_y np.zeros((len(y_test), scaler.n_features_in_)) dummy_y[:, target_idx] y_test[:, 0] y_original scaler.inverse_transform(dummy_y)[:, target_idx] rmse np.sqrt(mean_squared_error(y_original, pred_original)) mae mean_absolute_error(y_original, pred_original) valid y_original 1.0 mape np.mean(np.abs(y_original[valid] - pred_original[valid]) / y_original[valid]) * 100 return rmse, mae, mape反归一化这步是常见的知识盲区。预测输出只是产量一个通道但scaler.inverse_transform要求输入的特征数与 fit 时一致所以要用 dummy 矩阵填充其他特征维度只替换产量那一列。y_test的第 0 列是未来第 1 天的产量这里为了演示只评价了第一天。实际评估 7 天预测时应该逐天计算指标并画出逐天的误差衰减曲线观察第几天开始误差突增。4. 产量预测落地中的四个高频翻车点与排查清单4.1 梯度爆炸与 loss 变成 NaN现象训练跑到第 23 个 epochloss 直接变成nan之后再也回不来。原因我在第一次跑这个项目时偷懒没有归一化直接用日产油量原始值几十这个量级喂给 LSTM。LSTM 内部计算包含多个矩阵乘法和非线性激活输入量级过大导致梯度迅速膨胀。即便归一化了滑窗样本重叠度高同一个极端样本被反复学习也可能引发梯度异常。解决先确认特征全部做了归一化再在训练循环里加上clip_grad_norm_(model.parameters(), max_norm1.0)如果仍然不稳定把学习率从 1e-3 降到 5e-4。这三个手段同时用上基本能根治。我曾排查过一个神秘的nan最后定位到是某个特征列里混入了字符串类型的缺失标记MinMaxScaler直接报错被 try 吞掉数据里留下了垃圾值。所以异常排查第一步永远先看df.info()确认所有特征列都是数值类型。4.2 模型预测曲线趋于平缓变成均值的平移现象验证集误差不大但把预测结果画出来是一条近似水平线完全没有跟随实际产量的起伏。原因这是时序预测里最经典的“回归到均值”问题。LSTM 在 MSE 损失下最优策略是预测训练集所有样本的平均趋势。当输入特征与目标相关性弱、或者网络容量不足以捕捉波动模式时模型就选择这个最保守的策略。我有一口井的含水率特征几乎不变模型完全靠它做预测结果整条曲线就是一条直线。解决把油压、套压、动液面这些有真实波动的动态特征加进输入让模型有更多信息可用。另一个有效手段是修改损失函数对最近几天的预测误差给更大的权重强迫模型关注近期变化def weighted_mse_loss(pred, target, horizon7): # 对越近的日期误差惩罚越大 weights torch.linspace(1.5, 1.0, horizon) weights weights.view(1, -1) / weights.mean() diff pred - target return torch.mean(diff ** 2 * weights.to(pred.device))torch.linspace(1.5, 1.0, horizon)表示未来第 1 天的误差权重 1.5 倍第 7 天降到 1.0 倍。这个策略背后的逻辑是近期预测在生产决策中价值更高同时也缓解长时序预测中误差累积带来的波形钝化。4.3 时间泄漏随机划分让验证集指标虚高现象训练好的模型在验证集上 MAPE 只有 3%到现场部署后预测误差成了 10% 以上前后对不上。原因使用了train_test_split默认的随机划分模式。产量数据相邻日期的样本高度相关随机划分后验证集里混入了大量与训练集时间重叠的样本模型实际上已经见过了“未来”的数据。这是时间序列预测中最隐蔽的错误因为训练过程完全正常指标也不会报错。解决严格按时间顺序切分或者用TimeSeriesSplit做交叉验证。我现在的固定流程是先按时间排序前 70% 训练中间 15% 验证最后 15% 测试中间不做任何打乱。如果数据来自多口井还要注意把同一口井的数据切在同一段时间内避免一口井同时出现在训练集和测试集。4.4 新井历史数据不足模型完全学不动现象新投产井只有 35 个月生产数据滑窗后样本量不到一百条LSTM 怎么调都过拟合验证集误差波动极大。原因样本量太小模型参数远多于有效样本数模型直接记住了训练样本。这不是调参能解决的是数据结构的问题。解决用多井联合训练加迁移微调。具体做法是先用区块内所有老井的数据训练一个基础模型然后冻结 LSTM 层只微调最后的回归头几个 epoch。如果新井有独特的地层特征再解冻 LSTM 层用很小的学习率5e-5全量微调。亲测效果比单井硬训好很多相当于让模型先学会区块通用的递减规律再针对单井修正。这个思路和深度学习实战项目案例里常见的迁移学习做法完全一致只是领域从图像换成了油藏工程。5. 从训练到落地滚动预测、批量脚本与模型持久化5.1 滚动回测模拟真实生产环境的验证方式一次性预测测试集 100 天的数据跟现场每天接收新数据、滚动外推的使用方式并不一致。滚动回测的做法是每次只用截止到当前日期的数据做预测第二天把真实值并入历史再次预测如此反复。下面的代码实现完整的滚动评估def walk_forward_eval(model, X_full, y_full, scaler, start_idx500, step7): model.eval() preds, trues [], [] for i in range(start_idx, len(X_full) - 7, step): x torch.FloatTensor(X_full[i:i1]) with torch.no_grad(): pred model(x).numpy().flatten() true y_full[i:i1].flatten() preds.append(pred) trues.append(true) return np.array(preds), np.array(trues)start_idx表示从第几个样本开始回测建议留出足够长的历史供模型参考。step7表示每 7 天滚动一次模拟每周做一次生产计划。滚动回测评估出的误差一般比一次性预测略大这个偏大的数值才更接近真实部署水平。我把滚动回测结果作为是否上线的硬门槛滚动 MAPE 超过 10% 的井宁可不做预测也不硬上免得误导排采决策。5.2 多井批量预测与配产建议输出单井模型验证通过后现场需要的是批量处理工具。我的做法是准备一个well_config.csv列出井号、模型路径、窗口大小脚本逐井加载预测并写回结果。这里给出简化但可运行的版本import os, glob import pandas as pd import torch configs pd.read_csv(well_config.csv) results [] for _, row in configs.iterrows(): well_id row[well_id] model_path row[model_path] window int(row[window_size]) df load_and_preprocess(well_id) # 复用第 2 章的预处理逻辑 model ProductionLSTM(n_featureslen(feature_cols)) model.load_state_dict(torch.load(model_path)) model.eval() last_sequence torch.FloatTensor(df[feature_cols].values[-window:]).unsqueeze(0) with torch.no_grad(): pred_norm model(last_sequence).numpy() dummy np.zeros((1, scaler.n_features_in_)) dummy[:, 0] pred_norm[:, 0] pred_original scaler.inverse_transform(dummy)[:, 0] results.append({ well_id: well_id, predict_date: df.index.max() pd.Timedelta(days1), pred_7d_avg: pred_original.mean(), pred_7d_trend: 上升 if pred_original[-1] pred_original[0] else 下降 }) pd.DataFrame(results).to_csv(production_forecast.csv, indexFalse)批量脚本注意几个工程细节每口井要单独加载自己的scaler我把归一化器随模型一起用torch.save打包存储避免这个环节出现不一致预测时关闭梯度否则显存会被计算图占满。输出字段里pred_7d_avg是未来 7 天的平均日产油pred_7d_trend给一个直观的趋势判断这是给现场调度看的比直接甩一组数字更友好。现场反馈说“有没有趋势”比“具体多少吨”更影响他们的排采决策。5.3 上线前的最后验证预测误差分布与可解释性模型上线前我必做一件事画残差图。横轴是时间纵轴是真实值与预测值的差。如果残差里能看到周期性波动说明模型漏掉了某个周期性输入因素值得回到特征工程补特征。如果残差随产量增大而增大说明模型对高产井拟合不足需要检查该井的训练样本权重。评估误差时不要只看平均值把未来 7 天逐天的误差列成一条线看第几天开始误差跳变。理想情况下第 1 天误差最小到第 7 天仍然是第 1 天的 1.5 倍以内超出这个范围说明窗口长度或模型深度不足回到第 3 章的参数表去调整。从那以后我每换一口新井的数据都会强制走一遍今天的完整流程先拉日期对齐、处理缺失和异常再做滑窗和时序划分训练完成后用滚动回测模拟现场节奏最后画残差图确认没有系统性偏差。这套流程看起来繁琐但就是它帮我避开了“验证集好看、现场一用就废”的尴尬。如果你拿到的数据井况更复杂——比如有压裂措施导致的产量突变建议在特征里加一个“措施后第 N 天”的计数特征让模型意识到当前处于措施有效期。这个资源里的源码整体结构是完整的把数据路径替换掉即可运行。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。