简介面向农作物产量预测的深度学习代码包源自AAAI 2017论文获奖方案适合农业遥感与机器学习交叉方向的研究者、算法工程师。资源以Python为核心完整覆盖从Google Earth Engine批量下载遥感图像、切片及三维直方图预处理到CNN、LSTM、高斯过程模型训练与结果分析的全流程。压缩包共54个文件包括38个Python脚本、3个CSV标签数据、3个NPZ结果文件、1个MAT矩阵文件及SVG可视化图表整体约1.25MB目录按下载数据、清洗数据、模型、批处理、半监督五个模块组织结构清晰。已有1543人查看学习。项目针对每部分给出实用说明例如下载阶段按年份拼接全部图像再整体拉取能获得数百倍提速批处理模块支持每年每月训练不同参数并包含半监督扩展实验。对于需要复现产量预测实验、构建时空特征或改进深度学习建模流程的读者这套代码提供了可直接运行的参考实现与结果对比工具能有效降低论文的复现门槛。1. 作物产量预测难的不是模型是数据里的“时序”和“空间”把“深度学习”和“农作物产量预测”放在一起很多人第一反应是换一个更强大的模型。但真正的问题不在模型——某公司在某区域做过一次对比实验同样的数据用线性回归和用深层网络差距不到5%。真正拉开差距的是两个维度时间上的物候节奏空间上的地块差异。产量预测的本质不是拟合一个静态函数而是让模型理解“这个季节热了多少天、那片地水分如何”这类动态过程。这篇文章面向的读者是做农业信息化、智慧农业平台、遥感应用或者粮食贸易决策支持的技术人员。目标是讲清楚一条可落地的路径用结构化数据气象、土壤、农艺记录还是遥感影像各有什么边界怎么做特征构造、模型选型和数据划分以及最容易翻车的数据泄漏和极端年份泛化问题。这里没有黑匣子也没有玄学每一步都能用代码和验证兜住。2. 两条技术路线怎么选结构化特征与遥感影像各自的适用边界2.1 结构化数据路线把产量预测当作回归任务的可行性结构化数据路线的基本思路是把每个地块、每个生长季的已知条件整理成一行样本目标是某一期产量。常见的特征包括播种日期、品种类型、各月的降水量和积温、土壤的有机质含量和pH值、施肥量和灌溉次数等。这条路线的优点在于可解释性强、工程成本低一张表就能开始建模也便于和业务系统的历史记录对接。但它的边界很明显——空间位置信息被压平了。同一行特征里很难表达“这块地东边靠河、西边靠坡北侧有一条防风林”这类空间关系。如果数据里没有包含这些信息模型就无从学习地块内部的异质性。另一个问题是物候节奏容易丢失如果把整个生长季的气象数据聚合成一个平均值那“六月高温”和“七月高温”对产量影响的差异就被抹掉了。所以结构化路线更适合样本量大、地块面积均匀、管理记录详细的场景比如农场级别的产量预测、区域统计部门的趋势估计。地块级别如果只有几十个样本深度学习和传统机器学习没有本质区别反而LightGBM这类树模型更稳。2.2 遥感影像路线空间分布信息不可替代的部分遥感影像路线把每个地块或者每个像元当作图像输入利用卷积网络提取空间特征再结合时间序列判断生长状况。直观的优势是能够捕捉植被指数如NDVI、EVI的时空变化。同一个地块如果南侧长势明显好于北侧卷积网络可以通过局部特征的差异感知到这一点而结构化数据完全做不到。更关键的是遥感影像天然携带物候节奏。每个月一张影像整个生长季叠起来就是一个三维张量高度×宽度×时间模型能够学习到“拔节期长势缓慢上升、灌浆期达到峰值、成熟期回落”这类动态规律。这种时间维度的信息正是产量形成的核心过程也是结构化数据最容易丢失的部分。当然这条路线的代价也很真实。影像数据的预处理链条长大气校正、云掩膜、坐标对齐、物候窗口裁剪任何一步出错都会带歪模型。数据量也比表格大几个数量级训练需要GPU。还有一个常见的问题是影像特征和产量的尺度不匹配——影像分辨率是10米产量数据却是整个地块的平均值这里存在一个“空间聚合”的一致性问题后续章节会展开讲。2.3 两条路线的对比与本地环境选型维度结构化数据遥感影像影像气象融合最小可用样本量数百行数千张影像数百个样本对应影像空间异质性表达弱强强时序动态表达弱需手动构造中依赖影像频率强工程复杂度低中高高可解释性高配合SHAP低中典型场景区域产量统计、农场管理记录地块级长势监测精细化产量预测从实践角度看我一般建议先跑通结构化基线再用影像做增量。不要一上来就上深度网络。原因很直接结构化基线可以帮你确认标签数据的质量和分布如果基线误差率超过30%说明数据本身有问题换模型也救不回来。影像模型应该作为结构化基线的增量模块而不是替代品。两个路线结合的方式可以很简单——把影像模型提取的特征向量拼接到结构化特征后面再一起训练一个输出层。3. 表格型数据落地LightGBM基线模型与特征构造要点3.1 数据准备与特征构造物候、气象、土壤怎么进模型先明确一点产量预测的标签是连续值不是分类所以评价指标用RMSE均方根误差和MAE平均绝对误差而不是准确率。判断模型好不好看的是误差占平均产量的比例。如果平均产量是每亩500公斤RMSE是50公斤误差率是10%这个模型在业务上可以辅助决策如果误差率超过20%只能做趋势判断。数据准备的第一步是把原始记录按“地块×年份”组织成一行。最常见的坑是同一个地块在不同年份有不同的种植品种如果直接把品种编号塞进模型模型会学到“品种A产量高”这种表面规律但真实的因果可能是“品种A恰好种在好年份”。处理方式是保留品种作为特征但用目标编码target encoding替换原始编号——用同一品种的历史平均产量作为特征值减少类别数过多带来的过拟合。气象特征不要只聚合整个生长季。我一般按物候期分段聚合播种到出苗、出苗到拔节、拔节到抽穗、抽穗到成熟。每一段分别算积温、降水总量、降水天数、极端高温天数。为什么要分段因为同一场高温出现在不同生育期的伤害完全不同花期遇高温可能直接导致授粉失败而苗期高温影响往往可以恢复。土壤特征相对简单大部分情况只有表层数据直接数值化即可。极个别地块如果有机质、pH值缺失用同区域平均值填充比用全局平均值更合理——因为土壤属性的空间相关性很强用邻域信息估计比用全局统计更接近真实值。3.2 用LightGBM组产量回归基线代码与调参这里完整给出一个可运行的LightGBM回归代码骨架。假设已经有了预处理好的DataFrame列名为feature_cols标签列名为yield。关键处理都写在注释里。import pandas as pd import numpy as np import lightgbm as lgb from sklearn.model_selection import GroupKFold from sklearn.metrics import mean_squared_error from sklearn.model_selection import train_test_split # 读取预处理后的数据 df pd.read_csv(field_features.csv) X df[feature_cols] y df[yield] groups df[field_id] # 同一个地块跨年份属于同一组 # 按地块分组划分训练/验证集防止同一地块出现在两边 group_kfold GroupKFold(n_splits5) train_idx, valid_idx next(iter(group_kfold.split(X, y, groups))) X_train, X_valid X.iloc[train_idx], X.iloc[valid_idx] y_train, y_valid y.iloc[train_idx], y.iloc[valid_idx] # 构造LightGBM数据集categorical_feature指定类别列 train_data lgb.Dataset(X_train, labely_train, categorical_feature[variety_code]) valid_data lgb.Dataset(X_valid, labely_valid, referencetrain_data) # 参数说明 # learning_rate较低配合较多数量的树减少过拟合 # num_leaves控制在31以下防止树过深 # feature_fraction在0.7-0.9之间增加随机性降低方差 params { objective: regression, metric: rmse, learning_rate: 0.05, num_leaves: 31, feature_fraction: 0.8, bagging_fraction: 0.8, bagging_freq: 1, verbose: -1, seed: 42, } model lgb.train( params, train_data, num_boost_round2000, valid_sets[valid_data], callbacks[lgb.early_stopping(stopping_rounds100)], ) pred model.predict(X_valid, num_iterationmodel.best_iteration) rmse np.sqrt(mean_squared_error(y_valid, pred)) print(fValidation RMSE: {rmse:.2f} kg/mu)逻辑说明整体流程是“分组划分 → 构造数据集 → 训练 → 早停 → 验证”。其中GroupKFold是按地块分组划分的核心——因为同一个地块不同年份的样本高度相关如果随机划分模型会在验证集上看到“见过”的地块导致误差被严重低估。categorical_feature告诉LightGBM哪些列是类别型它会做内部的处理不需要手动编码成one-hot这能显著减少类别特征带来的维度膨胀。参数说明num_leaves控制在31以下是防止过拟合的第一道防线一般我调整这个参数的频率最高。feature_fraction和bagging_fraction是LightGBM自带的随机化手段适当开启能在不明显增加偏差的前提下降低方差。learning_rate0.05配合2000棵树的容量上限再用早停自动决定实际用多少棵树——不用手动去猜轮数。seed固定是为了保证实验结果可复现这也是做实验记录的基本习惯。3.3 用SHAP验证特征合理性防止模型学“假规律”模型训练完不能直接部署至少要做一次特征合理性检查。SHAP是最直观的工具它能给出每个样本中每个特征的贡献值相当于把黑匣子打开一条缝。重点看两件事方向是否符合农学常识、排序是否稳定。import shap # 训练完成后计算SHAP值 explainer shap.TreeExplainer(model) shap_values explainer.shap_values(X_valid) # 输出特征重要性排序按平均绝对贡献 shap_importance np.abs(shap_values).mean(axis0) feature_importance pd.DataFrame({ feature: feature_cols, mean_abs_shap: shap_importance }).sort_values(mean_abs_shap, ascendingFalse) print(feature_importance.head(10)) # 分布图查看某一特征如何影响产量正贡献还是负贡献 shap.dependence_plot(accum_temp_seedling, shap_values, X_valid)逻辑说明feature_importance输出的是每个特征在所有验证样本上的平均影响幅度排序靠前的特征应该和农学知识对得上——比如灌浆期的积温、抽穗期的降水、土壤有机质。如果排序第一的是某个奇怪的ID字段或者和产量毫无物理关联的特征就需要警惕数据泄漏或者特征构造错误。dependence_plot展示的是单特征与预测值的关系曲线可以直观看出影响方向——比如“灌浆期极端高温天数”应该是负贡献如果SHAP曲线显示高温天数越多预测产量越高那大概率是特征符号写反了或者数据本身有logical错误。这些检查和调参本身一样重要。我见过不少案例模型在回测指标上非常漂亮但上线后完全失效——追溯原因时发现特征和目标之间隔了一层不必要的中间量模型学的是“这个中间量接近标签”的捷径而不是“农学因素影响产量”的真实机制。4. 遥感影像落地用ResNet提取冠层特征并融合气象序列4.1 时序影像数据的组织方式按月合成NDVI影像路线的数据组织和表格完全不同。最常用的数据源是哨兵2号Sentinel-2——10米分辨率、5天重访周期足够覆盖地块尺度的长势监测。但原始影像不能直接用因为云层遮挡会导致大量缺失值。常见做法是按月做最大值合成每月取该像元所有有效观测中的最大NDVI再拼接整个生长季的月度影像。月度合成能同时解决两个问题一是云污染一个月内总有几个清晰像元二是统一时间步长让模型输入维度固定。以玉米为例生长季大约是4月到9月6个月就是6个通道的NDVI影像。每个通道代表一个月。需要明确的边界如果只输入6个月NDVI的均值模型能学到的信息非常有限因为月度之间的变化趋势才是关键。所以在实际项目中通道的排列顺序保留了时间维度——第1通道是4月、第2通道是5月……模型通过卷积的通道维度隐式学习“NDVI随时间上升或下降的形态”。有人把这种方式叫做“时间作为通道”这也是影像时间序列建模最朴素的做法。4.2 模型结构与数据加载CNN提取空间特征模型结构的选择上不需要自己设计深层网络用预训练的ResNet18做特征提取器足够应付大多数场景。步骤是把月度NDVI合成影像当成一个6通道的输入经过ResNet的卷积层得到空间特征图再经过全局平均池化得到256维向量最后接一个全连接层输出预测产量。这里有一个关键改动ResNet预训练模型的输入是3通道RGB而我们的输入是6通道。处理方法有两种一种是把前3个通道初始化成预训练权重后面3个通道随机初始化另一种是先用1×1卷积把6通道压缩到3通道。实践中我偏向后者——前几个月的NDVI和生长后期的NDVI共享低层特征提取器模型更容易学到通用的“绿色度”特征。import torch import torch.nn as nn import torchvision.models as models class YieldCNN(nn.Module): def __init__(self, n_channels6, n_features3, hidden_dim128): super().__init__() # 用1x1卷积将6通道压缩到3通道保持空间信息 self.channel_reduce nn.Conv2d(n_channels, 3, kernel_size1) # 加载预训练ResNet18去掉最后的全连接层 backbone models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) self.backbone nn.Sequential(*list(backbone.children())[:-2]) # 自适应池化到1x1避免输入尺寸不一致 self.pool nn.AdaptiveAvgPool2d(1) self.head nn.Sequential( nn.Linear(512, hidden_dim), nn.ReLU(inplaceTrue), nn.Dropout(0.3), nn.Linear(hidden_dim, 1), ) def forward(self, x, meta): x self.channel_reduce(x) feat self.backbone(x) feat self.pool(feat).flatten(1) # 如果还有气象特征可以拼接到CNN特征后面 if meta is not None: feat torch.cat([feat, meta], dim1) return self.head(feat).squeeze(1)逻辑说明这个模型把空间特征提取和时间趋势压缩放在一起——channel_reduce先把6个月NDVI压缩成3通道让预训练权重能够直接迁移backbone输出的512维向量已经包含了空间上的长势分布信息最后拼接外部气象特征如果有再回归。好处是整体框架简单坏处是时间维度的信息仍停留在“通道”层面模型没有显式建模月份之间的先后关系——这一点在后续章节讲如何用Transformer改进。数据加载方面影像文件通常是大尺寸的GeoTIFF不能直接全量读入内存。建议在数据预处理阶段就将影像裁剪成固定尺寸的patch——比如128×128并和地块边界做空间对齐。每个patch的标签是该patch对应地块的产量。这里必须提醒如果一个地块被裁剪成多个patch需要记录patch和地块的对应关系验证时按地块分组否则会重复计算同一地块的样本导致指标虚高。4.3 训练配置与验证策略按年份划分而不是随机划分影像模型的训练配置相对固定。优化器用Adam初始学习率1e-4批次大小16或32损失函数用MSE。预训练层的学习率可以打折比如主干网络用1e-5新加的head用1e-4——前者只做微调后者从头学不同速率避免预训练权重被冲掉。验证策略比网络结构更值得花时间。按年份划分是产量预测的行业常识用历史年份训练最近年份验证。原因很实际——预测的是未来而随机划分会让模型提前“看见”未来年份的影像分布。比如用2021到2023年训练、2024年验证得出的是模型在新年份上的真实表现。这一点和表格路线的GroupKFold思路一致但程度更深。影像模型容易过拟合影像获取日期、传感器型号等无关变量。如果训练集全是某颗卫星的影像验证集换了一颗卫星模型几乎必然崩溃——这些数据分布差异是产量预测里最常见的性能杀手。5. 产量预测的常见问题与排查数据泄漏、过拟合、极端年份5.1 数据泄漏时间泄漏是产量预测最容易犯的错现象模型在验证集上RMSE非常好误差率低于3%明显好于农学家的经验判断给人一种“模型已经学会预测产量”的错觉。但一旦部署到新一年的数据上误差暴增到30%以上。原因训练集和验证集之间存在时间重叠或空间重叠。比如同一个地块2022年的数据在训练集里2023年的数据在验证集里或者气象特征是从包含验证期在内的完整气候记录里统计出来的。这些都是“未来数据混进了过去”的时间泄漏。解决构建特征时所有统计特征必须只使用该样本之前的数据。比如做“历史五年平均降水量”特征在2023年的样本上只能计算2008到2022年的均值不能把2023年之后的数据算进去。划分数据集用GroupKFold(field_id)或者直接按年份切分并且检查特征是否用了验证期之后的信息源。5.2 极端气候年份泛化差模型预测产量、模型也预测不确定性现象模型在正常年份误差8%但遇到高温干旱年份误差直接翻倍到20%以上。有的模型在极端高温下预测产量明显偏高理论上产量应该腰斩模型却只下调了10%。原因训练数据里极端年份样本太少。正常年份的样本占90%以上模型学到的规律是“温度升高对产量影响不大”因为常规年份温度波动本来就在适宜范围内。极端年份的温度远超训练分布模型外推能力天然不足。解决方向有两个——第一是收集尽可能多的历史极端年份样本哪怕数据质量差一点也比没有强产量预测对小样本极端事件的依赖程度远高于常规样本。第二是让模型输出不确定性区间用分位数损失或者Monte Carlo Dropout在极端条件下将预测结果以“区间”而不是“点值”的形式交给业务方避免单一数字误导决策。5.3 影像与气象时序对齐不一致数据管线的坑现象模型训练时loss正常下降但验证集上出现周期性的大误差——某些年份整体偏移某些月份特别严重。排查模型结构和参数没有发现问题最后定位到是影像日期和气象记录日期错位。原因影像的月度合成窗口和气象观测的月份定义不一致。比如影像用的是“每月1日到月底”的合成窗口而气象数据用的是“上月16日到本月15日”的物候月。当影像时间偏移半个月时模型会把两个不同生长阶段的特征关联起来学出错误的节律。解决在数据管线里统一时间口径。我通常在数据准备阶段生成一个时间字段对齐检查表——每个样本包含影像合成起止日期、气象统计起止日期、物候期判定结果三列逐一比对。任何不匹配的样本直接丢弃或者重新合成绝不抱着侥幸心理送进模型。5.4 类别不平衡与区域差异按生态区建模是常见解法现象同一套模型在全国范围内预测某些区域的误差明显高于其他区域。模型在A省表现好在B省却完全失灵但B省的地块数量也不少。按区域分别训练模型后各自误差下降显著。原因不同生态区的物候节律差异超出了单一模型的容量。比如北方一熟区和南方两熟区的生长季长度完全不同模型为了兼顾两者不得不学习一个“平均节奏”结果两边都不讨好。样本量上极端年份在不同区域出现的频率也不一致少数区域的异常年份主导了误差。解决按生态区拆数据训练多个模型比训练一个通用模型更稳。如果担心样本量不足可以用区域编码作为条件输入让模型学习区域之间的共享规律和差异规律。同时用区域级别的交叉验证评估每个子模型的稳定性而不是只看全局指标。6. 把预测结果用起来不确定性输出与业务阈值设定6.1 输出分位数而不是单点预测值单点预测值解决的是“产量大概是多少”的问题但业务决策往往需要知道“最差到什么程度”。比如粮食收购商决定是否提前锁定价格需要知道的是产量的下限而不是平均值。把模型从点预测改成分位数回归用pinball loss训练三个模型或一个多输出模型分别预测10%、50%、90%分位数。以LightGBM为例objective改为quantilealpha分别设成0.1、0.5、0.9训练三个模型。代价是训练时间乘以3但换来的信息量大幅增加。实操中我一般用这个方式做风险预警当10%分位数预测低于某个阈值时触发关注提醒而不等平均值出结果。# 训练三个分位数模型分别对应10%、50%、90%分位 quantiles [0.1, 0.5, 0.9] models [] for q in quantiles: params { objective: quantile, alpha: q, metric: quantile, learning_rate: 0.05, num_leaves: 31, verbose: -1, } model lgb.train(params, train_data, num_boost_round1000, valid_sets[valid_data]) models.append(model) # 推理 preds [m.predict(X_valid, num_iterationm.best_iteration) for m in models] low, mid, high preds # 输出区间宽度若区间过宽说明模型对这批样本信心不足 interval_width (high - low) / mid print(f区间宽度中位数: {np.median(interval_width):.2f})6.2 用历史误差校准业务决策线有了分位数预测还不够还需要把模型输出和业务动作挂钩。具体做法是用历史3年的回测数据计算模型预测值和实际值之间的误差分布然后设定业务阈值。比如如果10%分位数预测的产量低于盈亏平衡线那么系统发出“减产风险高”的警报。这里的“10%分位数”在统计学上意味着“有90%的把握实际产量高于这个值”——但前提是模型校准良好且历史误差分布相对稳定。校准的方法也简单——查看回测结果中预测区间是否真的覆盖了相应比例的真实值。如果10%分位数预测下真实值低于预测值的年份比例远高于10%说明模型过于乐观需要下调分位数模型的截距或者检查特征是否遗漏了关键限制因子。不存在完美的模型但可以通过持续校准让模型在已知的失效模式下保持诚实。这比追求一个不可能的精准点值更有工程价值。愿这个方案能帮你少走一段弯路希望帮到你。本文还有配套的精品资源点击获取