ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

遥感岩性识别中极端随机树的参数调优与工程实践

遥感岩性识别中极端随机树的参数调优与工程实践 简介一套基于极端随机树模型的遥感图像岩性识别Python源码及文档说明面向遥感、地学、计算机视觉相关专业学生及研究人员解决从遥感数据中自动识别岩性类别并优化模型参数的实践需求。项目整合了布谷鸟搜索、粒子群等优化算法用于参数调优并配有网格搜索对比方案训练完成的随机树模型以pickle格式保存便于直接加载推理。压缩包共10个文件以8个Python脚本为核心涵盖数据预处理、格式转换、模型训练、参数优化等完整流程另含1个Markdown文档说明和1个模型权重文件整体仅63KB轻量且结构清晰。目前已有159人学习或下载适合作为机器学习、遥感图像处理的高分课设、毕设参考项目。使用者可依据文档快速复现岩性识别流程也可基于现有代码扩展其他地物分类任务或替换优化算法开展对比实验。1. 岩性识别用极端随机树为什么它不是随机森林的替代品而是更稳的选择遥感岩性识别这事很多入门者第一反应是上随机森林而极端随机树Extra-Trees在国内教程里存在感一直不高但它其实在处理高光谱、多波段遥感数据时效果往往比随机森林更稳。原因不复杂极端随机树在节点分裂时不仅随机选特征还随机选切分阈值这相当于给模型多加了一层正则化对遥感图像里常见的噪声波段和标注误差没那么敏感。这份资源里把极端随机树和布谷鸟、粒子群优化算法串在一起做参数调优正好解决了极端随机树最让人头疼的max_features和n_estimators难配的问题。适合正在做遥感图像分类、地质填图课程设计或者想找一个能直接跑通的参数调优参考实现的人。2. 数据集整理与预处理convert_txt2csv.py 和 merge_csv.py 的分工逻辑2.1 从散装 TXT 到结构化 CSV先搞懂原始数据长什么样遥感岩性识别的数据通常不是直接能扔进模型的图像而是每个样本对应一条光谱或特征记录。这份资源的原始输入是 TXT 格式的样本文件每个文件里的行数代表样本数每一行是样本的多个特征值加上末尾的岩性类别标签。训练之前必须先把这些 TXT 转成统一的 CSV否则后面用 pandas 读数据、切训练集和测试集都会很别扭。convert_txt2csv.py 解决的问题就是这个。它的核心逻辑是遍历一个目录下所有 TXT 文件把每个文件读进来加上文件名作为来源标记最后写成一个 CSV。这么做的好处是万一某个 TXT 文件的类别分布明显异常你能通过来源标记快速定位是哪个文件出了问题而不是在一整张大表里大海捞针。import os import pandas as pd def convert_txt2csv(txt_dir, output_csv): frames [] for fname in os.listdir(txt_dir): if not fname.endswith(.txt): continue file_path os.path.join(txt_dir, fname) # 每一行都是特征 最后一列是岩性类别标签 df pd.read_csv(file_path, headerNone, sep\t) df[source_file] fname frames.append(df) print(f[INFO] {fname}: {df.shape[0]} 个样本) merged pd.concat(frames, ignore_indexTrue) merged.to_csv(output_csv, indexFalse) print(f[INFO] 合并完成总样本数: {merged.shape[0]}特征维度: {merged.shape[1] - 2})逻辑说明这里用 pd.read_csv 读 TXT 时指定 sep\t是因为原始数据大概率是制表符分隔如果实际是空格或逗号改成 sep 或 sep, 即可。给每个样本加一列 source_file 是这份资源做得比较细的地方它让后续做错误分析时能追溯到具体文件。特征维度用总列数减 2是扣掉了标签列和来源列。2.2 merge_csv.py把多个来源的 CSV 合成一个训练集convert_txt2csv.py 的输出是每个目录或每个区域一个 CSV但训练极端随机树需要的是把所有样本合成一张表。merge_csv.py 做的事就是这个它不是简单地把行堆起来而是做了两件关键的事一是检查不同 CSV 之间的特征列名是否一致二是去重。import pandas as pd import glob def merge_csv(csv_pattern, output_path): files glob.glob(csv_pattern) data_frames [] for f in files: df pd.read_csv(f) data_frames.append(df) # 检查列名一致性 cols data_frames[0].columns for df in data_frames[1:]: if list(df.columns) ! list(cols): raise ValueError(f列名不一致: {f}) combined pd.concat(data_frames, ignore_indexTrue) # 去掉完全重复的行 combined.drop_duplicates(inplaceTrue) combined.to_csv(output_path, indexFalse) print(f[INFO] 最终样本数: {len(combined)})这里的列名一致性检查比较重要。遥感特征文件经常被人手动改过表头或者不同来源的数据列顺序不同如果不检查直接 concatpandas 会按位置对齐而不是按列名对齐结果就是特征错位但模型训练不报错最后精度上不去还找不到原因。drop_duplicates 也值得留一下某些区域的数据可能被重复采集把完全一样的样本放进训练集会让验证集精度虚高。2.3 数据预处理归一化到底做不做先看特征分布data_preprocessing.py 处理的是特征缩放问题。遥感岩性数据里的波段反射率在量纲上差异不大但纹理特征、指数特征比如归一化植被指数的取值范围就可能差出几个数量级。极端随机树是树模型理论上不做归一化也能跑但特征值范围过大的列会在节点分裂时产生偏向导致 max_features 随机抽取时容易选到数值范围大的特征相当于隐性地降低了特征多样性。from sklearn.preprocessing import StandardScaler def preprocess_features(X_train, X_test): scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test) return X_train_scaled, X_test_scaled, scaler逻辑说明fit_transform 在训练集上计算均值和标准差transform 在测试集上用同一组参数做转换这是为了防止测试集信息泄漏到训练过程里。很多初学者会在整个数据集上先归一化再切分这个做法在树模型里问题不大但如果你后面要对比神经网络或 SVM就必须按上面的方式做。参数方面StandardScaler 的两个核心参数 with_mean 和 with_std 默认都是 True稀疏矩阵场景才需要把 with_mean 设为 False这份资源的场景是稠密特征矩阵保持默认即可。3. 参数调优的三种打开方式网格搜索、粒子群与布谷鸟3.1 为什么极端随机树的参数那么难配先理解 Extra-Trees 的超参数空间极端随机树和随机森林最大的区别在于分裂策略。随机森林在每个节点上会遍历特征的多个候选阈值选最优切分极端随机树是随机选一个阈值从所有特征中挑一个最好的切分。这个机制让它的单棵树比随机森林的树更弱但集成后方差更低对噪声更稳健。但同时它也带来一个问题——模型对 n_estimators 和 max_features 特别敏感。n_estimators 决定树的棵数太少会欠拟合太多就是纯浪费计算时间max_features 控制每棵树能看到的特征数量设得太大树的随机性下降设得太小每棵树都太弱。min_samples_split 和 min_samples_leaf 则控制树的生长深度遥感数据样本量通常不大如果不限制树的生长很容易过拟合到某些区域的特殊光谱上。这几个参数加起来手调基本不现实所以这份资源准备了三种自动化调参方案。3.2 train_GridSearchCV.py暴力搜索是穷举天团适合特征维度不高的场景网格搜索的思路最简单把每个参数的候选值列出来用交叉验证把所有组合全部跑一遍。这份资源里的网格搜索代码是把 n_estimators、max_features、min_samples_split 三个参数做了笛卡尔积组合。from sklearn.ensemble import ExtraTreesClassifier from sklearn.model_selection import GridSearchCV param_grid { n_estimators: [50, 100, 200], max_features: [sqrt, log2, None], min_samples_split: [2, 5, 10] } et ExtraTreesClassifier(random_state42, n_jobs-1) grid GridSearchCV( estimatoret, param_gridparam_grid, cv5, scoringf1_macro, verbose1, n_jobs-1 ) grid.fit(X_train, y_train) print(f[INFO] 最优参数: {grid.best_params_}) print(f[INFO] 最优交叉验证分数: {grid.best_score_:.4f})逻辑说明scoring 选 f1_macro 而不是默认的 accuracy是因为岩性识别任务里不同岩类的样本数量经常不平衡accuracy 会被大类别主导。f1_macro 对每个类别的 F1 取算术平均能更公平地反映小类别岩性的识别效果。参数组合总数是 3×3×3×5折135 次模型训练如果数据量大建议先跑小范围确定大致方向后再细化。GridSearchCV 有个很容易踩的坑n_jobs-1 配合 n_estimators200 时如果机器内存不够会直接 OOM。我一般会先把 n_estimators 的候选值降一档或者改用 HalvingGridSearchCV它先用少量样本筛选参数组合再逐步增加样本量能省不少计算资源。3.3 train_PSO.py粒子群优化的核心是把参数组合编码成粒子位置网格搜索在参数维度低的时候没问题但参数一多组合数就爆炸。粒子群优化PSO的思路是把每一组参数编码成一个粒子粒子在参数空间里飞行每次迭代根据个体最优和全局最优更新位置和速度。这份资源里的 PSO 实现是自定义的没有直接用 pyswarm 库好处是你能看到完整的迭代逻辑方便改成自己的优化目标。import numpy as np from sklearn.ensemble import ExtraTreesClassifier from sklearn.model_selection import cross_val_score def pso_objective(params): n_estimators int(params[0]) max_features float(params[1]) min_samples_split int(params[2]) et ExtraTreesClassifier( n_estimatorsn_estimators, max_featuresmax_features, min_samples_splitmin_samples_split, random_state42, n_jobs-1 ) scores cross_val_score(et, X_train, y_train, cv3, scoringf1_macro) return -np.mean(scores) # 粒子群默认求最小值所以取负 def pso_train(X_train, y_train, iterations20, swarm_size15): dim 3 # 参数边界: n_estimators, max_features, min_samples_split lb np.array([50, 1.0, 2]) ub np.array([300, 20.0, 15]) positions np.random.uniform(lowlb, highub, size(swarm_size, dim)) velocities np.zeros((swarm_size, dim)) pbest_pos positions.copy() pbest_score np.array([pso_objective(p) for p in positions]) gbest_idx np.argmin(pbest_score) gbest_pos pbest_pos[gbest_idx].copy() gbest_score pbest_score[gbest_idx] for it in range(iterations): w 0.9 - 0.5 * (it / iterations) # 惯性权重递减 c1, c2 1.5, 1.5 r1, r2 np.random.rand(swarm_size, dim), np.random.rand(swarm_size, dim) velocities (w * velocities c1 * r1 * (pbest_pos - positions) c2 * r2 * (gbest_pos - positions)) positions velocities positions np.clip(positions, lb, ub) scores np.array([pso_objective(p) for p in positions]) improved scores pbest_score pbest_pos[improved] positions[improved] pbest_score[improved] scores[improved] gbest_idx np.argmin(pbest_score) if pbest_score[gbest_idx] gbest_score: gbest_score pbest_score[gbest_idx] gbest_pos pbest_pos[gbest_idx].copy() print(f[INFO] 迭代 {it1}/{iterations}, 当前全局最优: {-gbest_score:.4f}) return gbest_pos, -gbest_score逻辑说明PSO 的更新公式里w 是惯性权重控制粒子保持原方向的程度这里做了线性递减从 0.9 降到 0.4前期全局探索、后期局部收敛。c1 和 c2 是学习因子分别代表向个体最优和全局最优学习的力度通常都设在 1.5 左右。n_estimators 和 min_samples_split 在更新后要转成 int因为树数量和小样本分裂数不可能是小数。max_features 这里允许浮点数sklearn 对 0 到 1 之间的浮点数会自动解释为特征总数的比例所以边界设为 1.0 到 20.0 覆盖了从单特征到全特征的比例。粒子群的一个实际问题是迭代后期容易陷入局部最优特别是参数空间本身不平滑时。一个常见补救是每过几代对部分粒子的位置加一点随机扰动或者重新初始化一部分粒子。这份资源没做这个处理如果你跑出来的结果明显不如网格搜索可以自己加。3.4 train_Cuckoo.py布谷鸟搜索的莱维飞行机制更适合跳出局部最优布谷鸟搜索CS算法的灵感来自布谷鸟寄生孵卵的行为核心是莱维飞行——一种步长服从重尾分布的随机游走偶尔会出现大步长跳跃。这种特性让布谷鸟搜索在参数空间里的探索能力比 PSO 强不容易被困在局部最优里。这份资源的 Cuckoo_search.py 实现了完整的布谷鸟搜索流程train_Cuckoo.py 是调用它的入口。import numpy as np def levy_flight(Lambda1.5): sigma (np.math.gamma(1 Lambda) * np.sin(np.pi * Lambda / 2) / (np.math.gamma((1 Lambda) / 2) * Lambda * 2**((Lambda - 1) / 2)))**(1 / Lambda) u np.random.normal(0, sigma, size3) v np.random.normal(0, 1, size3) step u / (np.abs(v)**(1 / Lambda)) return step def cuckoo_search(X_train, y_train, n_nests15, n_iter30, pa0.25): lb np.array([50, 1.0, 2]) ub np.array([300, 20.0, 15]) nests np.random.uniform(lowlb, highub, size(n_nests, 3)) fitness np.array([pso_objective(n) for n in nests]) best_idx np.argmin(fitness) best_nest nests[best_idx].copy() best_fit fitness[best_idx] for it in range(n_iter): for i in range(n_nests): step levy_flight() new_nest nests[i] 0.01 * step * (nests[i] - best_nest) new_nest np.clip(new_nest, lb, ub) new_fit pso_objective(new_nest) if new_fit fitness[i]: nests[i], fitness[i] new_nest, new_fit # 发现概率 pa抛弃部分巢穴并重新随机生成 for i in range(n_nests): if np.random.rand() pa: nests[i] np.random.uniform(lowlb, highub, size3) fitness[i] pso_objective(nests[i]) current_best np.min(fitness) if current_best best_fit: best_fit current_best best_idx np.argmin(fitness) best_nest nests[best_idx].copy() print(f[INFO] 布谷鸟迭代 {it1}/{n_iter}, 当前最优: {-best_fit:.4f}) return best_nest, -best_fit逻辑说明莱维飞行的步长由 u 和 v 两个正态随机变量组合生成指数 Lambda 取 1.5 是经验值决定了重尾程度。pa 是发现概率表示寄主发现外来鸟蛋的概率这里设 0.25每轮迭代有 25% 的巢穴会被重新初始化这个机制是布谷鸟搜索跳出局部最优的关键。注意这里复用 pso_objective 只是为了说明方便实际使用中应该把目标函数单独抽到一个公共模块里避免两份代码维护不同的评估逻辑。布谷鸟搜索比 PSO 慢因为莱维飞行生成步长涉及较多随机数运算而且每轮迭代要评估 n_nests 次模型。如果数据量大建议把交叉验证折数从 5 降到 3或者用早停机制——连续 5 轮最优值没有提升就终止。4. 避坑指南岩性识别里最容易翻车的四个细节4.1 坑一标签分布极不均衡精度虚高但实际不可用现象训练出来的模型在验证集上 f1_macro 有 0.85但看混淆矩阵发现某一类岩性的召回率只有 0.2。原因遥感岩性数据里大面积出露的岩类样本数量往往是小众岩类的几十倍极端随机树在训练时会偏向多数类少数类即使全分错也能拿到较高准确率。解决先用 value_counts 检查训练集的类别分布如果发现某一类样本占比超过 80%考虑用 class_weightbalanced_subsample 参数让每棵树的子样本里类别权重自动平衡或者用 SMOTE 做少数类过采样然后再跑网格搜索或其他调优流程。4.2 坑二波段特征重复或强相关导致 max_features 失效现象max_features 从 sqrt 改成 log2 之后交叉验证分数几乎不变模型表现像是一个固定结构。原因遥感数据的相邻波段光谱相关性极高极端随机树的特征随机选择机制依赖于特征之间的独立性如果大量特征是强相关的复制品随机抽特征时大概率抽到的都是同一组信息树的多样性就下降了。解决先做一步相关性分析把相关系数超过 0.95 的波段合并或直接剔除常见做法是用 PCA 降维后再送入模型但要注意 PCA 之后模型的可解释性会下降如果最终要输出特征重要性做地质解释建议用方差膨胀因子筛选而不是 PCA。4.3 坑三训练集和测试集来自同一区域空间自相关导致分数虚高现象训练集随机切分时精度很高但把模型应用到相邻区域的遥感图像上时精度断崖式下跌。原因遥感数据的空间自相关性很强同一区域的像元光谱相似度远高于不同区域的像元随机切分会把空间邻近的样本同时分到训练集和测试集模型相当于记住了位置而不是学到了岩性规律。解决按区域划分而不是按样本划分比如用三块区域的图像数据做训练用第四块做测试或者用 sklearn 的 GroupKFold 按图像块分组做交叉验证。这份资源的源码里没有做这一步建议你跑通之后自己加一个按 source_file 分组的分层抽样逻辑。4.4 坑四RF_model.pickle 直接加载就推理没有检查特征顺序现象下载资源后直接用 pickle 加载模型做推理得到的结果完全不对甚至报特征数量不匹配的错误。原因pickle 保存的是整个 ExtraTreesClassifier 对象但它不会记录训练时特征的顺序和名字如果推理时输入数据的列顺序和训练时不一致模型会在你不知道的情况下选错特征。解决加载模型后用 model.feature_importances_ 的长度确认特征数量再和输入数据的列数对比。更稳妥的做法是训练完就把特征列名存成一个 JSON 文件推理时按同样的顺序重新排输入。import json import pickle import numpy as np with open(feature_names.json, w) as f: json.dump(list(X_train.columns), f) with open(RF_model.pickle, rb) as f: model pickle.load(f) with open(feature_names.json, r) as f: feature_names json.load(f) X_input pd.DataFrame(raw_features, columnsfeature_names) y_pred model.predict(X_input)这段做两件事训练完把特征列名持久化推理时按列名构造输入 DataFrame而不是依赖位置匹配。这也是为什么这份资源里有个 RF_model.pickle 文件它可以直接用但前提是输入特征顺序必须和训练时一致。5. 从调参到落地教你用 RF_model.pickle 做一次完整的推理验证拿到这份资源我建议的第一步不是重训模型而是把 RF_model.pickle 加载起来在你的数据上跑一遍推理确认整个链路是通的。因为重训一次极端随机树加调优可能要几十分钟到几小时而加载预训练模型只需要几秒钟先验证推理逻辑再重训能省下大量排错时间。推理验证的完整流程分三步第一步用 pandas 读入待预测数据第二步从 pickle 加载模型第三步输出预测结果和概率。这里有一个容易被忽略的点ExtraTreesClassifier 的 predict_proba 返回的是每个类别的概率但概率的排序对应模型训练时的 classes_ 属性你可以用 model.classes_ 做映射避免预测结果的类别标签对不上。import pandas as pd import pickle import numpy as np with open(RF_model.pickle, rb) as f: model pickle.load(f) X_new pd.read_csv(new_samples.csv) y_prob model.predict_proba(X_new) y_pred model.predict(X_new) print(类别映射:, model.classes_) for i in range(min(5, len(X_new))): top_idx np.argsort(y_prob[i])[-3:][::-1] top_classes model.classes_[top_idx] top_probs y_prob[i][top_idx] print(f样本 {i}: 预测岩性{y_pred[i]}, 最可能的三个类别{list(zip(top_classes, top_probs))})这段代码输出预测岩性的同时打印每个样本概率最高的三个类别和对应概率。岩性识别里模型预测的单一类别不一定可靠但前三个候选类别往往包含真实答案这对地质人员做人工复核特别有用。验证完推理链路之后再考虑要不要用网格搜索、粒子群或布谷鸟重新训练。我的建议是先用 GridSearchCV 跑一个小范围看基线分数再用布谷鸟搜索继续精调因为布谷鸟的莱维飞行在参数空间里不容易陷入局部最优和极端随机树的高方差特性配合得比较好。最后说一个我从这份资源里学到的教训跑任何数据科学项目第一步永远是打开 README.md而不是直接跑 train.py因为你不知道对方的依赖版本是 sklearn 0.24 还是 1.3不知道数据文件放在哪个目录不知道特征列的名字是什么。这份资源里我按照 README.md 的说明操作数据预处理到模型训练一次跑通省下的时间比什么都值。从那以后我每次拿到新项目都强制自己先读文档、再跑最小示例、最后才动完整训练脚本希望帮到你。本文还有配套的精品资源点击获取
返回列表