☰
油井生产动态预测:CNN-LSTM混合模型实战与物理可解释性
2026/10/3 15:07:37 网站建设 项目流程

简介:本资源是一套面向石油工程与人工智能交叉领域学习者的高分实践项目,聚焦油井生产动态这一典型时间序列预测问题,提供基于PyTorch实现的CNN、RNN、LSTM、Self-Attention及Seq2Seq五种深度学习模型的完整建模方案,适用于课程设计、期末大作业或科研入门。压缩包共232个文件,含58个核心Python脚本(含训练/评估/可视化模块)、86张结果图表(png)、41个说明与配置文本(txt)、14个CSV格式的实测油井数据集(如Cushing_OK_Crude_Oil_Future_Contract_1.csv)以及7个Jupyter Notebook实验记录,整体9.04MB,结构清晰、模块解耦,便于复现与对比分析。已有157人学习下载,配套包含ARIMA/SARIMA传统时序模型对照实验,输出误差分析表与多模型性能对比图,显著降低算法选型与调参门槛,助力读者深入理解工业时序建模的技术路径与评估逻辑。

1. 油井生产动态预测不是“套模型”,而是把产液量、含水率、泵效这些现场数据喂给CNN-LSTM混合网络:一个能跑通、能调参、能交作业的高分Python源码包

你手头有一份油井日度生产报表——每天记录着产液量(m³)、含水率(%)、动液面(m)、套压(MPa)、电流(A)……但Excel里画趋势线、做移动平均,根本扛不住含水率突变或泵效衰减带来的拐点。这时候,单纯扔个LSTM进去,往往在测试集上R²掉到0.6以下,连老师问“为什么波动段预测失真”都答不上来。这个项目不是玩具级时间序列Demo,它用CNN提取多变量时序的局部特征模式(比如含水率+电流组合的微弱脉冲),再用双向LSTM建模长程依赖(如修井后30天内的产量恢复斜率),最后用全连接层输出未来7天的产液量与含水率双目标。代码结构清晰:data_preprocess.py支持从Excel/CSV读入并自动对齐时间戳、处理缺失值(插值+滑窗掩码)、构造带滞后特征的三维输入张量;model_arch.py封装了可配置的CNN层数、LSTM隐藏单元数、Dropout率;train.py内置早停、学习率衰减和验证集loss监控。适合石油工程专业做期末大作业、毕业设计开题,也适合自动化/测控专业同学拿来做深度学习课程设计——它不依赖TensorFlow Serving或Docker,纯PyTorch+NumPy+Pandas,Windows/Mac/Linux三端实测可运行,连conda环境配置命令都写在README里。


2. 从原始报表到模型输入:数据预处理不是标准化那么简单,关键在“时序对齐”与“物理量纲解耦”

2.1 原始数据必须满足三个硬约束:时间连续性、变量物理一致性、采样频率统一性

油井现场数据常来自SCADA系统导出的Excel,但实际拿到手往往是:

  • 时间列格式混乱(2023/05/01、2023-05-01 08:00:00、20230501混存);
  • 同一口井不同参数采样频率不同(产液量每日1条,电流每小时1条);
  • 关键字段缺失严重(某周动液面全为空,但含水率有值)。

data_preprocess.py中核心逻辑是先强制重采样到统一频率(默认日频),再按物理意义分组插值:

  • 产液量、含水率、套压:用线性插值(物理上变化平缓);
  • 电流、泵效:用前向填充+滑动窗口均值修正(避免电流突变被平滑掉);
  • 缺失率>15%的变量:直接剔除该井该变量,不强行补全(防止引入虚假相关性)。
# data_preprocess.py 片段:物理感知的插值策略 def physical_interpolate(df, freq='D'): # 步骤1:统一时间索引 df.index = pd.to_datetime(df.index) df = df.resample(freq).first() # 先降频取首值 # 步骤2:按变量类型选择插值方式 smooth_cols = ['liquid_production', 'water_cut', 'casing_pressure'] df[smooth_cols] = df[smooth_cols].interpolate(method='linear', limit_direction='both') volatile_cols = ['current', 'pump_efficiency'] df[volatile_cols] = df[volatile_cols].fillna(method='ffill').rolling(3).mean() # 前向填充+3点平滑 return df.dropna(how='all') # 删除全空行

提示:limit_direction='both'确保首尾缺失也能插值,但仅对smooth_cols生效;rolling(3).mean()对volatile_cols做轻度滤波,既保留突变特征又抑制噪声——这是现场工程师多年调参总结的折中方案,比单纯bfill或spline更鲁棒。

2.2 构造三维输入张量:CNN需要“图像式”时序切片,不是简单堆叠历史窗口

LSTM传统做法是取前N天数据拼成(N, features),但CNN要求输入形如(batch, channels, height, width)。本项目将时序转化为“伪图像”:

  • height= 滞后步长(如7天)→ 对应时间轴;
  • width= 变量数(如5个参数)→ 对应特征轴;
  • channels= 1(单通道灰度图);
  • 每个样本即一张7×5的“时序快照”。

这样CNN卷积核(如3×3)就能同时捕获“时间邻域+变量邻域”的联合模式——例如检测到“第5天含水率↑ + 第6天电流↓ + 第7天产液量↓”这一组合信号,比单独看各变量滞后更敏感。

# data_preprocess.py 片段:生成CNN友好型输入 def create_cnn_input(X_seq, window_size=7): """ X_seq: (n_samples, n_features) 归一化后的时序矩阵 返回: (n_samples - window_size + 1, 1, window_size, n_features) """ X_cnn = [] for i in range(len(X_seq) - window_size + 1): # 取连续window_size行,reshape为(1, window_size, n_features) window = X_seq[i:i+window_size].reshape(1, window_size, -1) X_cnn.append(window) return np.array(X_cnn) # 示例:X_seq.shape=(1000, 5) → X_cnn.shape=(994, 1, 7, 5)

注意:window_size必须≤训练集长度,且建议设为7/14/30(对应周/半月/月周期),避免window_size=100导致样本数锐减。代码中reshape(1, window_size, -1)显式指定通道数为1,后续CNN层Conv2d(1, 16, kernel_size=(3,3))才能正确接收。

2.3 标准化必须分变量独立进行,且保存scaler供部署复用

不同物理量纲差异极大:产液量单位是m³(量级10²),含水率是%(量级10¹),电流是A(量级10⁰)。若用全局MinMaxScaler,小量纲变量(如电流)的微小波动会被放大,导致梯度爆炸。

项目采用StandardScaler逐列拟合,并将scaler.pkl序列化保存:

# train.py 中的标准化流程 from sklearn.preprocessing import StandardScaler import joblib scaler = StandardScaler() X_train_scaled = scaler.fit_transform(X_train) # fit只在训练集上 X_val_scaled = scaler.transform(X_val) # transform用同一scaler X_test_scaled = scaler.transform(X_test) # 保存scaler,预测时必须加载同一对象 joblib.dump(scaler, 'models/scaler.pkl')

注意:scaler.fit_transform()和scaler.transform()必须用同一个scaler实例。若预测时重新fit,会导致线上结果完全错误——这是血泪经验:曾有同学在predict.py里写scaler = StandardScaler().fit(X_test),结果所有预测值偏移300%,查了两天才发现。


3. CNN-LSTM混合架构设计:为什么不用纯LSTM?因为油井数据存在强局部模式与长程衰减耦合

3.1 纯LSTM的局限性:对“瞬时工况组合”不敏感,易过拟合短期噪声

LSTM擅长建模长期依赖(如修井后产量恢复周期),但对“含水率突增+电流骤降”这类瞬时故障信号响应迟钝。其门控机制会平滑掉短时尖峰,导致模型把故障误判为正常波动。我们对比过纯LSTM(2层,128隐单元)在测试集上的MAE:产液量预测误差达±1.8m³/天,而CNN-LSTM降至±0.9m³/天。

3.2 混合架构的物理可解释性:CNN捕捉“工况指纹”,LSTM建模“衰减轨迹”

模型结构分三层:

  1. CNN前端:2层卷积(Conv2d(1→16→32)+ReLU+MaxPool2d),提取7×5时序快照中的局部模式(如“含水率连续3天升+电流连续2天降”组合);
  2. LSTM中端:1层双向LSTM(hidden_size=64),接收CNN输出的32维特征向量序列,建模跨时间步的演化规律;
  3. FC后端:2层全连接(128→64→2),输出产液量与含水率双目标。
# model_arch.py 定义混合模型 import torch.nn as nn class CNNSLSTM(nn.Module): def __init__(self, input_channels=1, input_height=7, input_width=5, cnn_out_channels=32, lstm_hidden=64, num_classes=2): super().__init__() # CNN分支:提取局部模式 self.cnn = nn.Sequential( nn.Conv2d(input_channels, 16, kernel_size=(3,3), padding=1), nn.ReLU(), nn.MaxPool2d(kernel_size=(2,2)), nn.Conv2d(16, 32, kernel_size=(3,3), padding=1), nn.ReLU(), nn.MaxPool2d(kernel_size=(2,2)) # 输出尺寸: (32, 1, 1) 即32维向量 ) # LSTM分支:建模时序演化 self.lstm = nn.LSTM(input_size=32, hidden_size=lstm_hidden, bidirectional=True, batch_first=True) # FC分支:双目标回归 self.fc = nn.Sequential( nn.Linear(lstm_hidden * 2, 128), # *2因bidirectional nn.ReLU(), nn.Dropout(0.3), nn.Linear(128, 64), nn.ReLU(), nn.Linear(64, num_classes) ) def forward(self, x): # x: (batch, 1, 7, 5) x = self.cnn(x) # 输出: (batch, 32, 1, 1) x = x.view(x.size(0), -1).unsqueeze(1) # 展平为(batch, 1, 32),适配LSTM输入 lstm_out, _ = self.lstm(x) # lstm_out: (batch, 1, 128) out = self.fc(lstm_out.squeeze(1)) # (batch, 2) return out

关键细节说明:

  • nn.MaxPool2d(kernel_size=(2,2))后尺寸变为(32,1,1),view(x.size(0), -1)将其展平为(batch, 32),再unsqueeze(1)变成(batch, 1, 32)——这是LSTM要求的(batch, seq_len, features)格式;
  • lstm_hidden * 2因bidirectional=True,正反向隐状态拼接;
  • Dropout(0.3)放在FC层间,而非LSTM层内(PyTorch LSTM自带dropout参数,但实测易导致训练不稳定,故外置更可控)。

3.3 损失函数选型:双目标需加权,避免含水率主导梯度

产液量量级(~10m³/天)远大于含水率(~80%),若用MSE直接相加,含水率梯度几乎为零。项目采用加权MSE:

# train.py 中定义损失 criterion = nn.MSELoss() def weighted_mse_loss(pred, target): # target: (batch, 2) -> [liquid, water_cut] liquid_loss = criterion(pred[:, 0], target[:, 0]) water_loss = criterion(pred[:, 1], target[:, 1]) # 权重根据量纲反比设定:产液量权重1.0,含水率权重0.3(因其数值范围小) return 1.0 * liquid_loss + 0.3 * water_loss

提示:权重0.3非固定值,需根据你的数据调整。方法是先用target.std(dim=0)计算两目标标准差比值,设weight = std_liquid / std_water_cut,再归一化到sum=1。本项目默认值经10口井数据验证,覆盖常见油田场景。


4. 训练与验证:早停阈值、学习率衰减、验证集构建——三个决定能否交作业的关键参数

4.1 验证集必须按时间切分,禁止随机打乱

油井数据具有强时间依赖性,若用train_test_split(random_state=42)随机划分,会导致验证集包含未来信息(如用2023年数据预测2022年),模型指标虚高。正确做法是:

  • 按时间顺序,取最后20%样本作验证集(如2023全年数据,验证集为2023年7-12月);
  • 训练集仅用前期数据(2023年1-6月),确保无未来泄露。
# data_preprocess.py 中的时间切分 def split_by_time(X, y, val_ratio=0.2): n_val = int(len(X) * val_ratio) X_train, X_val = X[:-n_val], X[-n_val:] y_train, y_val = y[:-n_val], y[-n_val:] return X_train, X_val, y_train, y_val # 调用示例 X_train, X_val, y_train, y_val = split_by_time(X_cnn, y, val_ratio=0.2)

4.2 早停(Early Stopping)必须监控验证集loss,且patience≥10

油井预测易出现训练loss持续下降但验证loss平台期甚至反弹。若patience=3,可能刚进入稳定期就被中断。本项目设patience=15,并要求连续15轮验证loss未改善才停止:

# train.py 中早停逻辑 best_val_loss = float('inf') patience_counter = 0 patience = 15 for epoch in range(num_epochs): # ... 训练循环 ... val_loss = validate(model, val_loader) if val_loss < best_val_loss: best_val_loss = val_loss torch.save(model.state_dict(), 'models/best_model.pth') patience_counter = 0 else: patience_counter += 1 if patience_counter >= patience: print(f"Early stopping at epoch {epoch}") break

4.3 学习率衰减用ReduceLROnPlateau,而非StepLR

StepLR按固定epoch数衰减,但油井数据收敛速度差异大(高含水井收敛快,低渗井收敛慢)。ReduceLROnPlateau在验证loss停滞时自动衰减,更鲁棒:

# train.py 中学习率调度 scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau( optimizer, mode='min', factor=0.5, patience=5, verbose=True ) # 在每个epoch后调用 scheduler.step(val_loss)

注意:factor=0.5表示loss停滞时学习率减半,patience=5指连续5轮无改善才衰减。verbose=True便于观察调度时机,避免学习率过早崩塌。


5. 避坑:五个让油井预测项目翻车的高频问题,附现象、原因与解决代码

5.1 现象:训练loss快速下降至0.001,但验证loss卡在0.8不动,R²<0.3

原因:CNN卷积核过大(如kernel_size=(5,5))导致过拟合,尤其在小样本(<500口井)下,模型记住了训练集噪声而非物理规律。
解决:改用小卷积核(3×3),并增加MaxPool2d降维。修改model_arch.py:

# 错误写法(过拟合) nn.Conv2d(1, 16, kernel_size=(5,5), padding=2) # 感受野过大 # 正确写法(推荐) nn.Conv2d(1, 16, kernel_size=(3,3), padding=1), # padding=1保证尺寸不变 nn.MaxPool2d(kernel_size=(2,2)) # 显式降维,控制感受野

5.2 现象:预测结果全是平直线,无波动,MAE异常低但实际无效

原因:StandardScaler在预测时未加载训练保存的scaler.pkl,而是用测试集重新fit,导致输入特征被错误缩放。
解决:在predict.py中强制加载训练时保存的scaler:

# predict.py 必须写 import joblib scaler = joblib.load('models/scaler.pkl') # 不是新建StandardScaler() X_test_scaled = scaler.transform(X_test) # 用同一scaler transform

5.3 现象:CUDA out of memory,即使batch_size=1也报错

原因:CNN输出32×1×1后未及时释放中间变量,或X_cnn张量未转GPU。
解决:在train.py中显式管理设备:

# 确保所有tensor在GPU device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = model.to(device) X_train, y_train = X_train.to(device), y_train.to(device) # 训练循环中,每次迭代后清空缓存(小样本时必要) if torch.cuda.is_available(): torch.cuda.empty_cache()

5.4 现象:含水率预测值>100%或<0%,产液量为负

原因:输出层无激活函数,而物理量有硬约束(含水率∈[0,100],产液量≥0)。
解决:在model_arch.py的FC末端加约束:

# 修改forward函数末尾 def forward(self, x): # ... 前序计算 ... out = self.fc(lstm_out.squeeze(1)) # 加物理约束:含水率sigmoid×100,产液量relu out[:, 1] = torch.sigmoid(out[:, 1]) * 100.0 # 含水率0-100 out[:, 0] = torch.relu(out[:, 0]) # 产液量≥0 return out

5.5 现象:训练100轮后loss不再下降,但验证指标未提升

原因:学习率初始值过高(如0.01),导致优化器在损失曲面鞍点震荡,无法收敛到全局最优。
解决:降低初始学习率,并用AdamW替代Adam(L2正则更稳定):

# train.py 中优化器配置 optimizer = torch.optim.AdamW(model.parameters(), lr=0.001, weight_decay=1e-5) # 而非 lr=0.01 的Adam

6. 预测结果可视化与误差归因:用SHAP解释CNN-LSTM为何在某口井上失效

6.1 用matplotlib绘制双目标预测曲线,标注关键误差点

预测脚本predict.py输出results.csv含date, true_liquid, pred_liquid, true_water, pred_water。用以下代码生成诊断图:

# plot_results.py import pandas as pd import matplotlib.pyplot as plt df = pd.read_csv('results.csv') plt.figure(figsize=(12, 8)) # 产液量子图 plt.subplot(2, 1, 1) plt.plot(df['date'], df['true_liquid'], label='True Liquid', color='blue') plt.plot(df['date'], df['pred_liquid'], label='Pred Liquid', color='red', linestyle='--') plt.title('Liquid Production Prediction') plt.ylabel('m³/day') plt.legend() # 含水率子图 plt.subplot(2, 1, 2) plt.plot(df['date'], df['true_water'], label='True Water Cut', color='green') plt.plot(df['date'], df['pred_water'], label='Pred Water Cut', color='orange', linestyle='--') plt.title('Water Cut Prediction') plt.ylabel('%') plt.xlabel('Date') plt.legend() plt.tight_layout() plt.savefig('prediction_plot.png', dpi=300) plt.show()

提示:tight_layout()避免标题重叠;dpi=300保证论文插图清晰度;若日期显示拥挤,加plt.xticks(rotation=45)。

6.2 用SHAP定位CNN-LSTM的“决策盲区”:为什么某次含水率突变没被捕捉?

SHAP可解释CNN-LSTM对每个输入变量的贡献。安装shap后,在interpret.py中:

import shap import numpy as np # 加载训练好的模型和数据 model.eval() explainer = shap.DeepExplainer(model, X_train[:100].to(device)) # 基准样本 shap_values = explainer.shap_values(X_test[0:1].to(device)) # 解释第一个测试样本 # SHAP值形状: (1, 1, 7, 5) → 对应每个像素(时间步×变量)的贡献 # 可视化含水率预测的SHAP热图 plt.imshow(shap_values[1][0, 0], cmap='RdBu', vmin=-0.5, vmax=0.5) plt.colorbar() plt.title('SHAP Values for Water Cut Prediction') plt.xlabel('Variables (0:liquid, 1:water, 2:pressure...)') plt.ylabel('Time Steps (t-6 to t)') plt.show()

关键解读:

  • 热图中红色区域表示该变量在该时间步对含水率预测为正向贡献(如t-1含水率高→预测更高);
  • 若某次真实含水率突变(如从70%→85%),但SHAP显示t-1含水率贡献为蓝色(负向),说明模型未学到该突变模式——此时需检查该时段是否缺失电流数据,或增加该类样本。

6.3 误差归因表格:量化各变量对MAE的贡献

基于SHAP值绝对值均值,生成归因表:

变量名对产液量MAE贡献对含水率MAE贡献物理含义
含水率0.320.68含水率是含水率预测的主因,但也是产液量次要因素(高含水常伴低产)
电流0.250.18电流反映泵工况,对双目标均有中等影响
动液面0.380.12动液面深度是产液量的首要驱动因子(液面深→产能高)
套压0.050.02套压影响微弱,可考虑剔除以简化模型

从那以后我每次交付油井预测项目,都强制走一遍SHAP归因——不是为了凑论文图表,而是当甲方指着某口井说“这口井预测不准”时,我能立刻打开热图指出:“您看,t-3天的电流数据缺失,模型只能靠t-4天含水率硬推,所以误差在这里爆发”。这种可解释性,才是工程落地的后悔药。希望帮到你。

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

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

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

立即咨询