惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

B
Blog RSS Feed
Martin Fowler
Martin Fowler
爱范儿
爱范儿
IT之家
IT之家
Last Week in AI
Last Week in AI
A
About on SuperTechFans
Google DeepMind News
Google DeepMind News
阮一峰的网络日志
阮一峰的网络日志
V
V2EX
aimingoo的专栏
aimingoo的专栏
G
Google Developers Blog
J
Java Code Geeks
Microsoft Azure Blog
Microsoft Azure Blog
美团技术团队
The Cloudflare Blog
MyScale Blog
MyScale Blog
T
The Blog of Author Tim Ferriss
Hugging Face - Blog
Hugging Face - Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
云风的 BLOG
云风的 BLOG
Y
Y Combinator Blog
The GitHub Blog
The GitHub Blog
腾讯CDC
Microsoft Security Blog
Microsoft Security Blog

博客园 - harrychinese

机器学习实战示例和基础知识查漏补缺 jupyter notebook上的java kernel 数据分析中几个常用的java类 我认为 C# 几个重要的设计缺陷 Jupyter notebook 最简安装方法 Java 语言常用的技巧 Java AI agent框架 java jbang 工具让java作为脚本语言 VS code + Jypyter 插件构建 Java notebook 环境 使用 VS code + Oracle java 插件搭建java语言原生的notebook环境 【计算机科学速成课】[40集全/精校] - Crash Course Computer Science Handy toolbox for IT developers 中学学校教材下载 深度学习模型训练5个关键步骤 使用SpringBoot实现一个执行通用SQL的Rest接口 学益升X1和国悦K3阅读器 状态机 使用场景规则匹配模式代替复杂的if else条件判断 vscode 数据处理的插件 iPhone XR 升级非最新版IOS duckdb nuget打包部署完整步骤 ML.Net机器学习基本知识 机器学习资料汇总 优化算法知识和类库 C# XML 序列化器 画草图和示意图的好工具 .net debug tools 运筹学/最优化讲义.pdf Lazarus
使用Tribuo分析波士顿房产数据
harrychinese · 2026-05-22 · via 博客园 - harrychinese

pom.xml 内容

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>4.0.6</version>
		<relativePath /> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.example</groupId>
	<artifactId>house</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>house</name>
	<description />
	<url />
	<licenses>
		<license />
	</licenses>
	<developers>
		<developer />
	</developers>
	<scm>
		<connection />
		<developerConnection />
		<tag />
		<url />
	</scm>
	<properties>
		<java.version>26</java.version>
		<maven.compiler.source>11</maven.compiler.source>
		<maven.compiler.target>11</maven.compiler.target>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<tribuo.version>4.3.2</tribuo.version>
		<slf4j.version>2.0.13</slf4j.version>
		<junit.version>5.10.0</junit.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter</artifactId>

		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>

		<!-- Tribuo 核心依赖 -->
		<dependency>
			<groupId>org.tribuo</groupId>
			<artifactId>tribuo-all</artifactId>
			<version>${tribuo.version}</version>
			<type>pom</type>  <!--必须设置为pom-->
		</dependency>


		<!-- 如需单独引入 -->
		<dependency>
			<groupId>org.tribuo</groupId>
			<artifactId>tribuo-regression-xgboost</artifactId>
			<version>4.3.2</version>
		</dependency>

		<!-- 日志框架 -->
		<dependency>
			<groupId>org.slf4j</groupId>
			<artifactId>slf4j-api</artifactId>
			<version>${slf4j.version}</version>
		</dependency>
		<dependency>
			<groupId>org.slf4j</groupId>
			<artifactId>slf4j-simple</artifactId>
			<version>${slf4j.version}</version>
		</dependency>

		<!-- 单元测试 -->
		<dependency>
			<groupId>org.junit.jupiter</groupId>
			<artifactId>junit-jupiter</artifactId>
			<version>${junit.version}</version>
			<scope>test</scope>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>

主入口程序 HouseApplication.java

package com.example.house;

import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class HouseApplication {

	public static void main(String[] args) throws Exception {
		BostonHousingRegression.test(args);
	}

}

模型训练和推理 BostonHousingRegression.java

package com.example.house;

import com.oracle.labs.mlrg.olcut.config.ConfigurationManager;
import com.oracle.labs.mlrg.olcut.provenance.ProvenanceUtil;
import com.oracle.labs.mlrg.olcut.util.Pair;

import org.tribuo.*;
import org.tribuo.data.columnar.*;
import org.tribuo.data.columnar.processors.field.*;
import org.tribuo.data.columnar.processors.response.*;
import org.tribuo.data.csv.CSVDataSource;
import org.tribuo.ensemble.EnsembleModel;
import org.tribuo.evaluation.CrossValidation;
import org.tribuo.evaluation.TrainTestSplitter;
import org.tribuo.provenance.ModelProvenance;
import org.tribuo.regression.*;
import org.tribuo.regression.ensemble.AveragingCombiner;
import org.tribuo.regression.evaluation.*;
import org.tribuo.regression.rtree.CARTRegressionTrainer;
import org.tribuo.regression.rtree.impurity.MeanSquaredError;
import org.tribuo.regression.sgd.linear.LinearSGDModel;
import org.tribuo.regression.sgd.linear.LinearSGDTrainer;
import org.tribuo.regression.sgd.objectives.SquaredLoss;
import org.tribuo.regression.xgboost.XGBoostRegressionTrainer;
import org.tribuo.transform.*;
import org.tribuo.transform.transformations.*;
import org.tribuo.common.xgboost.XGBoostFeatureImportance;
import org.tribuo.common.xgboost.XGBoostModel;
import org.tribuo.common.tree.TreeModel;
import org.tribuo.common.sgd.AbstractLinearSGDModel;
import org.tribuo.common.tree.RandomForestTrainer;

import java.io.*;
import java.nio.file.*;
import java.util.*;
import java.util.logging.Logger;

/*
BostonHousing 价格数据网上有很多版本, 这里选用rapaio库提供的数据集,不需要处理各种缺失值,字段清单也是最主流的那个
 https://github.com/padreati/rapaio/tree/master/rapaio-datasets/src/rapaio/provider/datasets
*/

/**
 * Production-grade Boston Housing Price Prediction using Oracle Tribuo 4.3.2
 *
 * Engineering considerations:
 * 1. Columnar data processing with CSVDataSource and RowProcessor
 * 2. Separate handling of categorical vs continuous features
 * 3. Feature transformation pipelines (binning, standardization, log transforms)
 * 4. Multiple model comparison (Linear SGD, CART, XGBoost)
 * 5. Provenance tracking for reproducibility
 * 6. Model serialization and deserialization
 * 7. Inference pipeline reconstruction from model provenance
 * 8. Comprehensive evaluation metrics
 */

/*
 * 构建特征转换pipeline的两种方式:
 * 1. TransformationMap + TransformerMap , 使用 TransformationMap 定义转换逻辑,
 * 然后作用于训练集进行拟合得到一个 TransformerMap , 它包含训练好的统计指标转换器, 可以将 TransformerMap
 * 作用于测试集或单个预测样本完成转换, 然后再将转换的结果送到模型中进行推理。
 * 2. TransformationMap + TransformTrainer ,使用 TransformationMap 定义转换逻辑, 然后使用
 * TransformTrainer 来封装原始的 Trainer, 封装过程中传入 TransformationMap
 * 参数,TransformTrainer 会自动在训练前完成数据转换, 并将转换器固化到 TransformedModel
 * 中,在测试或单个预测样本推理时直接使用 TransformedModel 即可。
 */
public class BostonHousingRegression {

        private static final Logger logger = Logger.getLogger(BostonHousingRegression.class.getName());

        // Feature categorization for Boston Housing dataset
        private static final List<String> CONTINUOUS_FEATURES = Arrays.asList("CRIM", "ZN", "INDUS", "NOX", "RM", "AGE",
                        "DIS", "TAX", "PTRATIO", "B", "LSTAT");

        private static final List<String> CATEGORICAL_FEATURES = Arrays.asList("CHAS", "RAD");

        private static final String TARGET_COLUMN = "MEDV";

        public static void test(String[] args) throws Exception {
                Path dataPath = Paths.get("D:\\machine_learn\\dataset\\housing.csv");

                // Step 1: Build RowProcessor with separate field processors for categorical/continuous
                RowProcessor<Regressor> rowProcessor = buildRowProcessor();

                // Step 2: Load data with CSVDataSource
                CSVDataSource<Regressor> csvSource = new CSVDataSource<>(dataPath, rowProcessor, true, ',', // separator
                                '"' // quote character
                );

                // Step 3: Split data (70% train, 30% test, seed=42 for reproducibility)
                TrainTestSplitter<Regressor> splitter = new TrainTestSplitter<>(csvSource, 0.7, // train proportion
                                42L // random seed
                );

                MutableDataset<Regressor> trainSet = new MutableDataset<>(splitter.getTrain());
                MutableDataset<Regressor> testSet = new MutableDataset<>(splitter.getTest());

                logger.info(String.format("Training set size: %d, Test set size: %d", trainSet.size(), testSet.size()));
                logger.info(String.format("Number of features: %d", trainSet.getFeatureMap().size()));

                // Step 4: Build transformation pipeline
                TransformationMap transformationMap = buildTransformationMap();

                //如果使用 TransformTrainer 训练出的 TransformedModel 在推理前会自动先做transformation, 所以无需我们手工做transformation
                //对于使用的不是 TransformedModel, 需要基于训练集做转换统计处理得到一个transformerMap对象, 然后使用transformerMap对象完成对测试集或Example的转换。
                //因为我们使用 TransformedModel, 所以下面4行代码没有用
                /*
                TransformerMap transformerMap = trainSet.createTransformers(transformationMap); //基于训练集完成数据转换,得到一个包含具体统计值的 transformerMap对象
                testSet.transform(transformerMap); //使用 transformerMap 对测试集做转换
                Example ex = null;
                ex.transform(transformerMap); //对一个 example 做转换。
                 */

                // Step 5: Train and evaluate multiple models
                List<Model<Regressor>> models = new ArrayList<>();

                // Model 1: Linear SGD with transformations
                // 线性回归,使用随机梯度下降优化,适合理解基础线性关系。
                logger.info("Training Linear SGD model...");
                Trainer<Regressor> linearTrainer = new LinearSGDTrainer(new SquaredLoss(), // objective
                                new org.tribuo.math.optimisers.AdaGrad(0.01, 0.1), // AdaGrad optimizer
                                100, // epochs
                                trainSet.size() / 10, // minibatch size
                                42L // seed
                );

                // Wrap with TransformTrainer to apply transformations
                TransformTrainer<Regressor> transformLinearTrainer = new TransformTrainer<>(linearTrainer,
                                transformationMap, false, // densify
                                true // includeImplicitZeroFeatures
                );

                Model<Regressor> linearModel = transformLinearTrainer.train(trainSet);
                models.add(linearModel);
                evaluateModel("LinearSGD+Transform", linearModel, trainSet, testSet);

                // Model 2: CART Regression Tree
                // 决策树回归,能够捕捉非线性特征。
                logger.info("Training CART Regression Tree...");
                CARTRegressionTrainer cartTrainer = new CARTRegressionTrainer(6, // max depth
                                10, // min examples per leaf
                                0.001f, // min impurity decrease
                                1.0f, // fraction of data in tree
                                new MeanSquaredError(), 42L // seed
                );

                TransformTrainer<Regressor> transformCartTrainer = new TransformTrainer<>(cartTrainer,
                                transformationMap, false, true);

                Model<Regressor> cartModel = transformCartTrainer.train(trainSet);
                //models.add(cartModel);
                evaluateModel("CART+Transform", cartModel, trainSet, testSet);

                // Model 3: RandomForestTrainer
                CARTRegressionTrainer subsamplingTree = new CARTRegressionTrainer(6, // max depth
                                10, // min examples per leaf
                                0.001f, // min impurity decrease
                                0.7f, // fraction of data in tree
                                new MeanSquaredError(), 42L // seed
                );

                Trainer<Regressor> randomForestTrainer = new RandomForestTrainer<>(subsamplingTree,
                                new AveragingCombiner(), 10);
                TransformTrainer<Regressor> transformRandomForestTrainer = new TransformTrainer<>(randomForestTrainer,
                                transformationMap, false, true);
                Model<Regressor> randomForestModel = transformRandomForestTrainer.train(trainSet);
                // models.add(randomForestModel);
                evaluateModel("RandomForest+Transform", randomForestModel, trainSet, testSet);

                // Model 4: XGBoost
                // 强大的梯度提升树算法,通常能获得最高的预测精度。
                try {
                        logger.info("Training XGBoost model...");

                        // XGBoostRegressionTrainer(
                        // XGBoostRegressionTrainer.RegressionType rType, // 回归损失类型
                        // int numTrees, // 树的数量
                        // double eta, // 学习率
                        // double gamma, // 最小分裂损失
                        // int maxDepth, // 最大深度
                        // double minChildWeight, // 叶子最小权重和
                        // double subsample, // 样本采样率
                        // double featureSubsample, // 特征采样率
                        // double lambda, // L2 正则化
                        // double alpha, // L1 正则化
                        // int nThread, // 线程数
                        // boolean silent, // 是否静默
                        // long seed // 随机种子
                        // )
                        XGBoostRegressionTrainer xgboostTrainer = new XGBoostRegressionTrainer(
                                        XGBoostRegressionTrainer.RegressionType.LINEAR, // 回归损失类型(均方误差)
                                        50, // numTrees
                                        0.3, // eta (learning rate)
                                        0.0, // gamma
                                        6, // maxDepth
                                        1.0, // minChildWeight
                                        1.0, // subsample
                                        1.0, // featureSubsample
                                        1.0, // lambda (L2 reg)
                                        0.0, // alpha (L1 reg)
                                        4, // nThread
                                        true, // silent
                                        42L // seed
                        );

                        TransformTrainer<Regressor> transformXgbTrainer = new TransformTrainer<>(xgboostTrainer,
                                        transformationMap, false, true);

                        Model<Regressor> xgbModel = transformXgbTrainer.train(trainSet);
                        //models.add(xgbModel);
                        evaluateModel("XGBoost+Transform", xgbModel, trainSet, testSet);
                } catch (Exception e) {
                        logger.warning("XGBoost not available: " + e.getMessage());
                }

                // Step 6: Select best model and serialize
                Model<Regressor> bestModel = selectBestModel(models, testSet);
                Path modelPath = Paths.get("boston_housing_model.ser");
                serializeModel(bestModel, modelPath);

                // Step 7: Demonstrate inference with provenance reconstruction
                logger.info("Demonstrating inference with provenance-based RowProcessor reconstruction...");
                demonstrateInference(modelPath);

                // Step 8: Export provenance for audit
                //exportProvenance(bestModel, Paths.get("model_provenance.json"));

                printFeatureImportance(bestModel, "BestModel", 10);

        }

        /**
         * 定义原始数据的行处理器 (RowProcessor), 用来解析CSV每行数据
         * 1. 定义了两种特征,分别是连续型和类别类型特征
         * 2. 定义了FieldResponseProcessor, 指定了MEDV列作为回归的目标
         * Build RowProcessor with separate handling for categorical and continuous
         * features.
         * Uses IdentityProcessor for categorical (one-hot encoding by default in
         * Tribuo)
         * and DoubleFieldProcessor for continuous features.
         */
        private static RowProcessor<Regressor> buildRowProcessor() {
                Map<String, FieldProcessor> fieldProcessors = new HashMap<>();
                Map<String, FieldProcessor> regexMappingProcessors = new HashMap<>();

                // Continuous features: use DoubleFieldProcessor
                for (String feature : CONTINUOUS_FEATURES) {
                        fieldProcessors.put(feature, new DoubleFieldProcessor(feature));
                }

                // Categorical features: use IdentityProcessor (preserves string values)
                for (String feature : CATEGORICAL_FEATURES) {
                        fieldProcessors.put(feature, new IdentityProcessor(feature));
                }

                // Response processor for regression target
                FieldResponseProcessor<Regressor> responseProcessor = new FieldResponseProcessor<>(TARGET_COLUMN, "0.0", // default value if missing
                                new RegressionFactory());

                // Metadata extractors (optional, for tracking)
                List<FieldExtractor<?>> metadataExtractors = new ArrayList<>();

                // Build RowProcessor
                return new RowProcessor<>(metadataExtractors, null, // weightExtractor
                                responseProcessor, fieldProcessors, regexMappingProcessors, Collections.emptySet() // featureProcessors
                );
        }

        /**
         * Build TransformationMap with feature-specific and global transformations.
         *
         * Engineering approach:
         * - Log transform for skewed continuous features (CRIM, ZN, DIS)
         * - Standardization (Z-score) for all continuous features
         * - Binning for specific continuous features to capture non-linear patterns
         * - Global transformations applied after local ones
         */
        /**
         * 定义特征工程的逻辑:
         * 在机器学习中,直接使用原始的数据输入到模型效果往往不佳,所以需要进行特征工程。
         * 使用 TransformationMap完成: 局部转换(特征级)+ 可选全局转换
         *
         * 工程策略:
         * - 对数变换 (Log Transform):针对 CRIM(犯罪率)和 DIS(距离)这类高度偏态分布的数据,通过取对数使其更接近正态分布。
         * - Z-score 标准化 (MeanStdDev):对 RM、TAX等特征进行均值归一化,使其均值为 0,标准差为 1。
         * - 缩放 (Min-Max Scaling):将 AGE、INDUS、B 等特征缩放到 [0, 1] 之间。
         */
        private static TransformationMap buildTransformationMap() {
                Map<String, List<Transformation>> featureTransformations = new HashMap<>();

                // tribuo支持每个特征可以定义多个转换

                // 1. 对数变换(处理偏态分布)
                List<Transformation> logTransforms = Arrays.asList(SimpleTransform.log());
                featureTransformations.put("CRIM", logTransforms);
                featureTransformations.put("DIS", logTransforms);

                // 2. Z-score 标准化
                List<Transformation> stdTransforms = Arrays.asList(new MeanStdDevTransformation(0.0, 1.0));
                for (String f : Arrays.asList("RM", "LSTAT", "PTRATIO", "NOX", "TAX")) {
                        featureTransformations.put(f, stdTransforms);
                }

                // 3. Min-Max 缩放
                List<Transformation> mmTransforms = Arrays.asList(new LinearScalingTransformation(0.0, 1.0));
                for (String f : Arrays.asList("AGE", "INDUS", "B")) {
                        featureTransformations.put(f, mmTransforms);
                }

                // 全局转换(在所有局部转换后应用于所有特征)
                List<Transformation> globalTransforms = Collections.emptyList();

                return new TransformationMap(globalTransforms, featureTransformations);
        }

        /**
         * Evaluate model performance with comprehensive regression metrics.
         * 三个核心指标
         * - RMSE (均方根误差):衡量预测值与真实值的平均偏离程度。
         * - MAE (平均绝对误差):对异常值较不敏感的误差度量。
         * - R方 (决定系数):衡量模型对数据变异性的解释能力,越接近 1 越好。
         */
        private static void evaluateModel(String modelName, Model<Regressor> model, Dataset<Regressor> trainSet,
                        Dataset<Regressor> testSet) {
                RegressionEvaluator evaluator = new RegressionEvaluator();

                RegressionEvaluation trainEval = evaluator.evaluate(model, trainSet);
                RegressionEvaluation testEval = evaluator.evaluate(model, testSet);
                var dimension0 = new Regressor("DIM-0", Double.NaN);
                logger.info(String.format("\n=== %s Evaluation ===", modelName));
                logger.info(String.format("Train RMSE: %.4f, Test RMSE: %.4f", trainEval.rmse(dimension0),
                                testEval.rmse(dimension0)));
                logger.info(String.format("Train MAE: %.4f, Test MAE: %.4f", trainEval.mae(dimension0),
                                testEval.mae(dimension0)));
                logger.info(String.format("Train R2: %.4f, Test R2: %.4f", trainEval.r2(dimension0),
                                testEval.r2(dimension0)));
                logger.info(String.format("Train Explained Variance: %.4f, Test Explained Variance: %.4f",
                                trainEval.explainedVariance(dimension0), testEval.explainedVariance(dimension0)));
        }

        /**
         * Select best model based on test set R2 score.
         * 自动对比所有模型的R方得分,选出最优模型。
         */
        private static Model<Regressor> selectBestModel(List<Model<Regressor>> models, Dataset<Regressor> testSet) {
                RegressionEvaluator evaluator = new RegressionEvaluator();
                Model<Regressor> bestModel = null;
                double bestR2 = Double.NEGATIVE_INFINITY;
                var dimension0 = new Regressor("DIM-0", Double.NaN);
                for (Model<Regressor> model : models) {
                        RegressionEvaluation eval = evaluator.evaluate(model, testSet);
                        double r2 = eval.r2(dimension0);
                        if (r2 > bestR2) {
                                bestR2 = r2;
                                bestModel = model;
                        }
                }

                logger.info(String.format("\nBest model selected with Test R2: %.4f", bestR2));
                logger.info("best model: " + bestModel.getName());
                //logger.info("Model provenance: " + bestModel.getProvenance().toString());
                return bestModel;
        }

        /**
         * Serialize model to disk using Java serialization.
         * In production, consider using protobuf serialization (serializeToFile) for
         * better compatibility.
         */
        private static void serializeModel(Model<Regressor> model, Path path) throws IOException {
                // Method 1: Java serialization
                try (ObjectOutputStream oos = new ObjectOutputStream(
                                new BufferedOutputStream(Files.newOutputStream(path)))) {
                        oos.writeObject(model);
                }
                logger.info("Model serialized to: " + path);

                // Method 2: Protobuf serialization (recommended for production)
                Path protoPath = Paths.get(path.toString().replace(".ser", ".protobuf"));
                model.serializeToFile(protoPath);
                logger.info("Model also serialized to protobuf: " + protoPath);
        }

        /**
         *训练时:保存模型的同时,把训练时用的 rowProcessor 也存下来
         * @throws FileNotFoundException
         * @throws IOException
         * @throws ClassNotFoundException
         */
        private static void saveRowProcessorToFile() throws FileNotFoundException, IOException, ClassNotFoundException {
                RowProcessor<Regressor> rowProcessor = buildRowProcessor();
                try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("processor.ser"))) {
                        oos.writeObject(rowProcessor);
                }

        }

        /**
         *推理时:直接读取,不需要用 ConfigurationManager 去 lookup
         * @throws FileNotFoundException
         * @throws IOException
         * @throws ClassNotFoundException
         */
        private static void restoreRowProcessorFromFile()
                        throws FileNotFoundException, IOException, ClassNotFoundException {
                RowProcessor<Regressor> inferenceRowProcessor;
                try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("processor.ser"))) {
                        inferenceRowProcessor = (RowProcessor<Regressor>) ois.readObject();
                }
        }

        /**
         * Demonstrate inference with RowProcessor reconstructed from model provenance.
         * This is critical for production deployment to ensure consistent data
         * processing.
         * 该方法最关键是从模型中提取  RowProcessor 组件, 并使用 RowProcessor 组件生成 Example。
         * 为什么需要从模型中提取 RowProcessor 组件, 而不是直接使用 buildRowProcessor() 方法获取 RowProcessor 组件?
         * 核心原因是 buildRowProcessor() 是训练时的代码,在实际项目中推理时代码肯定和训练代码不在同一个项目中, 所以我们需要从模型中提取 RowProcessor 组件,
         * 也可以在训练时候将 RowProcessor 组件序列化到文件(参考saveRowProcessorToFile), 在推理时从文件中反序列化(参考 restoreRowProcessorFromFile),代码更加简单。
         *
         */
        private static void demonstrateInference(Path modelPath) throws Exception {
                // Load model
                Model<Regressor> loadedModel;
                try (ObjectInputStream ois = new ObjectInputStream(
                                new BufferedInputStream(Files.newInputStream(modelPath)))) {
                        loadedModel = (Model<Regressor>) ois.readObject();
                }

                // Extract RowProcessor from model provenance
                ModelProvenance provenance = loadedModel.getProvenance();
                var dataProvenance = provenance.getDatasetProvenance();
                var provConfig = ProvenanceUtil.extractConfiguration(dataProvenance);

                ConfigurationManager cm = new ConfigurationManager();
                cm.addConfiguration(provConfig);
                // 动态查找 RowProcessor 类型的组件名
                String rowProcessorName = "";
                for (String componentName : cm.getComponentNames()) {
                        if (cm.lookup(componentName) instanceof RowProcessor) {
                                rowProcessorName = componentName;
                                break;
                        }
                }
                if (rowProcessorName.isEmpty()) {
                        throw new IllegalStateException("在模型来源中未找到 RowProcessor 组件!");
                } else {
                        // 输出 rowProcessorName
                        logger.info("##############" + rowProcessorName);
                }

                @SuppressWarnings("unchecked")
                RowProcessor<Regressor> inferenceRowProcessor = (RowProcessor<Regressor>) cm.lookup(rowProcessorName);

                // Expand regex mappings with actual headers from inference data
                List<String> headers = Arrays.asList("CRIM", "ZN", "INDUS", "CHAS", "NOX", "RM", "AGE", "DIS", "RAD",
                                "TAX", "PTRATIO", "B", "LSTAT", "MEDV");
                inferenceRowProcessor.expandRegexMapping(headers);

                // Create a sample input row
                Map<String, String> sampleRow = new HashMap<>();
                sampleRow.put("CRIM", "0.00632");
                sampleRow.put("ZN", "18.0");
                sampleRow.put("INDUS", "2.31");
                sampleRow.put("CHAS", "0");
                sampleRow.put("NOX", "0.538");
                sampleRow.put("RM", "6.575");
                sampleRow.put("AGE", "65.2");
                sampleRow.put("DIS", "4.09");
                sampleRow.put("RAD", "1");
                sampleRow.put("TAX", "296");
                sampleRow.put("PTRATIO", "15.3");
                sampleRow.put("B", "396.9");
                sampleRow.put("LSTAT", "4.98");
                sampleRow.put("MEDV", "24.0"); // Ground truth, will be ignored at inference

                ColumnarIterator.Row row = new ColumnarIterator.Row(1, headers, sampleRow);

                // Generate example (outputRequired=false for inference)
                // RowProcessor 的职责:只负责将文本、CSV 行等“原始异构数据”转换为标准的、带有特征名称的 Example 对象, 它不感知任何数学变换(如 Z-score、Log)
                // 因为我们使用的是 TransformedModel, 所以不需要对原始的 Example 对象做任何数学变换, 直接使用 TransformedModel 预测即可。
                Optional<Example<Regressor>> exampleOpt = inferenceRowProcessor.generateExample(row, false);

                if (exampleOpt.isPresent()) {
                        Example<Regressor> example = exampleOpt.get();
                        Prediction<Regressor> prediction = loadedModel.predict(example);
                        double predictedValue = prediction.getOutput().getValues()[0];

                        logger.info(String.format("Inference result: Predicted MEDV = $%.2f (x1000)", predictedValue));
                        logger.info(String.format("Actual MEDV = $%.2f (x1000)", 24.0));
                }
        }

        /**
         * Export model provenance to JSON for audit and compliance.
         * 来源追踪 (Provenance):代码通过 exportProvenance 将模型的“出生证明”导出为
         * JSON。这包含了训练时的参数、数据集来源、甚至转换规则。这意味着即使过了很久,你依然能知道这个模型是如何产生的。
         */
        private static void exportProvenance(Model<Regressor> model, Path path) throws IOException {
                String provenanceJson = ProvenanceUtil.formattedProvenanceString(model.getProvenance());
                Files.writeString(path, provenanceJson);
                logger.info("Model provenance exported to: " + path);
        }

        /**
         * 使用网格搜索(Grid Search)进行 XGBoost 参数调优
         * 思路: 定义一个参数空间(例如:尝试不同的树深度、学习率和树个数),然后让 Tribuo 自动遍历这些组合并找到R方得分最高的一组。
         *
         * 对于参数空间组合非常多的情况, 手写三层for循环可能不是很美观, 可以使用
         * org.tribuo.classification.ensemble.VotingCombiner 等更高级的组件
         *
         * @param trainSet
         * @param testSet
         * @return
         */
        public static Model<Regressor> tuneXGBoost(MutableDataset<Regressor> trainSet,
                        MutableDataset<Regressor> testSet) {
                logger.info("开始 XGBoost 自动化参数调优...");

                // 定义候选参数范围
                int[] depths = { 4, 6, 8 };
                double[] etas = { 0.1, 0.3 };
                int[] treeCounts = { 50, 100 };

                Model<Regressor> bestModel = null;
                double bestR2 = Double.NEGATIVE_INFINITY;
                var dimension0 = new Regressor("DIM-0", Double.NaN);
                RegressionEvaluator evaluator = new RegressionEvaluator();

                // 自动化网格搜索逻辑
                for (int depth : depths) {
                        for (double eta : etas) {
                                for (int count : treeCounts) {

                                        // 创建当前参数下的训练器
                                        XGBoostRegressionTrainer trainer = new XGBoostRegressionTrainer(
                                                        XGBoostRegressionTrainer.RegressionType.LINEAR, count, // numTrees
                                                        eta, // eta
                                                        0.0, // gamma
                                                        depth, // maxDepth
                                                        1.0, // minChildWeight
                                                        1.0, // subsample
                                                        1.0, // featureSubsample
                                                        1.0, // lambda
                                                        0.0, // alpha
                                                        4, // nThread
                                                        true, // silent
                                                        42L // seed
                                        );

                                        // 训练模型
                                        Model<Regressor> model = trainer.train(trainSet);

                                        // 在测试集上评估
                                        RegressionEvaluation eval = evaluator.evaluate(model, testSet);
                                        double currentR2 = eval.r2(dimension0);

                                        logger.info(String.format("尝试参数: depth=%d, eta=%.1f, trees=%d -> R2: %.4f",
                                                        depth, eta, count, currentR2));

                                        // 保存表现最好的模型
                                        if (currentR2 > bestR2) {
                                                bestR2 = currentR2;
                                                bestModel = model;
                                        }
                                }
                        }
                }

                logger.info(String.format("调优完成!最优 R2: %.4f", bestR2));
                return bestModel;
        }

        /**
         * 使用交叉验证的方式进行模型训练
         *  优点:
         *  - 对比完整的训练集, 交叉验证每次取完整训练集中的大部分进行训练,剩余部分作为val dataset, 这样就可防止模型学到完整 trainSet 所有特点导致的过拟合
         *  - 对于波士顿房价只有506条数据,如果直接对训练集训练,非常依赖训练集随机划分的效果,训练结果方差较大, 带有运气成分。
         *  - 经过多次交叉验证, 挑选出指标最好的模型,该模型集完整考虑到 trainSet的所有数据, 同时又避免了死记硬背trainSet的所有细节。
         * @param fullTrainSet, 已经切分好的训练集
         * @param kFolds, 一般取值 5, 或者 10
         * @return
         */
        public static Model<Regressor> tuneXGBoostWithCV(MutableDataset<Regressor> fullTrainSet, int kFolds) {
                logger.info(String.format("开始 XGBoost %d 折交叉验证超参调优...", kFolds));

                // 定义要搜索的超参数网格
                int[] depths = { 4, 6, 8 };
                double[] etas = { 0.1, 0.3 };
                int[] treeCounts = { 50, 100 };

                XGBoostRegressionTrainer bestTrainer = null;
                double bestMeanR2 = Double.NEGATIVE_INFINITY;

                // 用于提取 R² 评估指标的占位符(波士顿房价默认单维度标签为 "DIM-0")
                var dimension0 = new Regressor("DIM-0", Double.NaN);
                RegressionEvaluator evaluator = new RegressionEvaluator();

                // 网格搜索外循环
                for (int depth : depths) {
                        for (double eta : etas) {
                                for (int count : treeCounts) {

                                        // 1. 创建当前参数组合的训练器
                                        XGBoostRegressionTrainer trainer = new XGBoostRegressionTrainer(
                                                        XGBoostRegressionTrainer.RegressionType.LINEAR, count, eta, 0.0,
                                                        depth, 1.0, 1.0, 1.0, 1.0, 0.0, 4, true, 42L);

                                        // 2. 使用 Tribuo 内置的 CrossValidation 组件
                                        CrossValidation<Regressor, RegressionEvaluation> cv = new CrossValidation<>(
                                                        trainer, fullTrainSet, evaluator, kFolds, 42L);

                                        // 3. 执行交叉验证,返回 K 个【评估结果与对应模型】的键值对列表
                                        List<Pair<RegressionEvaluation, Model<Regressor>>> cvResults = cv.evaluate();

                                        // 4. 计算这 K 折实验的 R² 平均分
                                        double sumR2 = 0.0;
                                        for (Pair<RegressionEvaluation, Model<Regressor>> fold : cvResults) {
                                                sumR2 += fold.getA().r2(dimension0); // getA() 拿到 RegressionEvaluation
                                        }
                                        double meanR2 = sumR2 / kFolds;

                                        logger.info(String.format(
                                                        "CV 尝试 -> depth=%d, eta=%.1f, trees=%d | %d折平均 R2: %.4f", depth,
                                                        eta, count, kFolds, meanR2));

                                        // 5. 记录平均表现最好的那组参数的 Trainer
                                        if (meanR2 > bestMeanR2) {
                                                bestMeanR2 = meanR2;
                                                bestTrainer = trainer;
                                        }
                                }
                        }
                }

                logger.info(String.format("调优结束!最优 %d 折平均 R2: %.4f", kFolds, bestMeanR2));

                // 6. 关键的一步:使用找出的最优参数训练器,在完整的训练集上重新训练,产出最终模型
                logger.info("正在使用最优参数在全量训练集上训练最终模型...");
                return bestTrainer.train(fullTrainSet);
        }

        /**
        * 打印并输出支持该功能模型的特征重要性(Feature Importance)
        */
        public static void printFeatureImportance(Model<Regressor> model, String modelName, int topFeatureCount) {
                logger.info("========================================");
                logger.info(String.format("正在分析模型 [%s] 的特征重要性...", modelName));
                logger.info("========================================");

                // 1. 如果模型被 TransformedModel 包裹,先剥离外壳拿到内部核心算法模型
                Model<Regressor> coreModel = model;
                if (model instanceof org.tribuo.transform.TransformedModel) {
                        coreModel = ((org.tribuo.transform.TransformedModel<Regressor>) model).getInnerModel();
                }

                if (coreModel instanceof XGBoostModel) {
                        // 2. 情况 A:针对 XGBoost 模型的特征重要性提取(直接使用官方公开的 Map 接口)
                        @SuppressWarnings("unchecked")
                        XGBoostModel<Regressor> xgbModel = (XGBoostModel<Regressor>) coreModel;

                        // 拿到外层指标清单
                        List<XGBoostFeatureImportance> featureImportances = xgbModel.getFeatureImportance();

                        if (featureImportances != null && !featureImportances.isEmpty()) {
                                // 理论上列表包含不同维度的重要性,这里我们直接取第一个维度的重要性对象
                                XGBoostFeatureImportance importanceMetrics = featureImportances.get(0);

                                // 核心修复:直接通过官方公开方法获取已经降序排好序的特征增益 Map
                                LinkedHashMap<String, Double> gainMap = importanceMetrics.getGain();
                                LinkedHashMap<String, Double> weightMap = importanceMetrics.getWeight();

                                if (gainMap != null) {
                                        for (Map.Entry<String, Double> entry : gainMap.entrySet()) {
                                                String featureName = entry.getKey();
                                                Double gainScore = entry.getValue();
                                                // 顺便关联取出对应的 Weight(频次)得分
                                                Double weightScore = weightMap != null
                                                                ? weightMap.getOrDefault(featureName, 0.0)
                                                                : 0.0;

                                                logger.info(String.format(
                                                                "特征: %-10s | XGBoost 增益得分 (Gain): %.4f | 频次 (Weight): %.0f",
                                                                featureName, gainScore, weightScore));
                                        }
                                } else {
                                        logger.warning("XGBoost 模型的 Gain 指标为空。");
                                }
                        } else {
                                logger.warning("未获取到有效的 XGBoost 特征重要性数据。");
                        }

                } else if (coreModel instanceof EnsembleModel) {
                        // 3. 情况 B:针对 随机森林 模型(EnsembleModel 内部包裹了多棵单树)
                        @SuppressWarnings("unchecked")
                        EnsembleModel<Regressor> ensembleModel = (EnsembleModel<Regressor>) coreModel;

                        // 获取森林中所有的单棵树模型
                        List<Model<Regressor>> subModels = ensembleModel.getModels();

                        // 创建一个全新的存储 Map,用来累加所有树的特征贡献度
                        Map<String, Double> forestImportanceMap = new HashMap<>();
                        int validTreeCount = 0;

                        for (Model<Regressor> subModel : subModels) {
                                // 确保子模型是树模型(理论上随机森林内部必然是 TreeModel)
                                if (subModel instanceof TreeModel) {
                                        TreeModel<Regressor> treeModel = (TreeModel<Regressor>) subModel;
                                        Map<String, List<Pair<String, Double>>> topFeaturesMap = treeModel
                                                        .getTopFeatures(topFeatureCount);
                                        if (topFeaturesMap != null && !topFeaturesMap.isEmpty()) {
                                                validTreeCount++;
                                                List<Pair<String, Double>> singleTreeFeatures = topFeaturesMap.values()
                                                                .iterator().next();
                                                // 将这棵树中每个特征的分数累加到总大表中
                                                for (Pair<String, Double> pair : singleTreeFeatures) {
                                                        forestImportanceMap.put(pair.getA(), forestImportanceMap
                                                                        .getOrDefault(pair.getA(), 0.0) + pair.getB());
                                                }
                                        }
                                }
                        }

                        if (validTreeCount > 0) {
                                // 对累加后的分数计算平均分(平均不纯度减少量)
                                final double finalTreeCount = validTreeCount;
                                forestImportanceMap.replaceAll((feature, totalScore) -> totalScore / finalTreeCount);

                                // 按照平均分从高到低排序输出
                                forestImportanceMap.entrySet().stream()
                                                .sorted(Map.Entry.<String, Double>comparingByValue().reversed())
                                                .forEach(entry -> logger.info(
                                                                String.format("特征: %-10s | 随机森林平均贡献得分 (减少不纯度): %.4f",
                                                                                entry.getKey(), entry.getValue())));
                        } else {
                                logger.warning("未能从该集成模型中解析出有效的树模型组件。");
                        }

                }

                else if (coreModel instanceof TreeModel) {
                        // 4. 情况 C:针对 单棵 CART 决策树模型(使用合规的 getTopFeatures 方法)
                        @SuppressWarnings("unchecked")
                        TreeModel<Regressor> treeModel = (TreeModel<Regressor>) coreModel;

                        // 获取按重要性排列的特征
                        Map<String, List<Pair<String, Double>>> topFeaturesMap = treeModel
                                        .getTopFeatures(topFeatureCount);

                        if (topFeaturesMap != null && !topFeaturesMap.isEmpty()) {
                                // 单目标回归,直接取第一个维度的列表
                                List<Pair<String, Double>> featureImportanceList = topFeaturesMap.values().iterator()
                                                .next();

                                // 官方接口已排序,直接循环输出
                                for (Pair<String, Double> pair : featureImportanceList) {
                                        logger.info(String.format("特征: %-10s | 决策树不纯度贡献得分: %.4f", pair.getA(),
                                                        pair.getB()));
                                }
                        } else {
                                logger.warning(String.format("决策树模型 [%s] 未计算出有效的特征重要性。", modelName));
                        }

                } else if (coreModel instanceof LinearSGDModel) {
                        @SuppressWarnings("unchecked")
                        AbstractLinearSGDModel<Regressor> linearModel = (AbstractLinearSGDModel<Regressor>) coreModel;

                        // 5.1 直接获取官方标准的 ImmutableFeatureMap 对象
                        ImmutableFeatureMap featureIDMap = linearModel.getFeatureIDMap();

                        // 5.2 拿到第 0 个维度的权重密集向量
                        org.tribuo.math.la.DenseVector weightVector = linearModel.getWeightsCopy().getRow(0);

                        Map<String, Double> linearImportanceMap = new HashMap<>();

                        // 5.3 核心修复:ImmutableFeatureMap 实现了 Iterable<VariableInfo>
                        // 我们可以直接安全地通过增强 for 循环拿到所有已知特征的名称
                        for (VariableInfo info : featureIDMap) {
                                String featureName = info.getName();
                                // 5.4 调用官方提供的公开 getID() 方法获取索引位置,找不到会返回 -1
                                int featureID = featureIDMap.getID(featureName);

                                if (featureID != -1) {
                                        double weightValue = weightVector.get(featureID);
                                        linearImportanceMap.put(featureName, weightValue);
                                }
                        }

                        logger.info("提示:由于数据已在流水线中标准化,系数绝对值越大代表特征越重要。");
                        // 按照线性系数的【绝对值】从高到低进行降序排序输出
                        linearImportanceMap.entrySet().stream().sorted(
                                        (a, b) -> Double.compare(Math.abs(b.getValue()), Math.abs(a.getValue())))
                                        .forEach(entry -> logger.info(String.format(
                                                        "特征: %-10s | 线性回归系数 (Weight): %+10.4f | 重要度(绝对值): %.4f",
                                                        entry.getKey(), entry.getValue(), Math.abs(entry.getValue()))));
                } else {
                        // 4. 情况 C:不支持的场景
                        logger.warning(String.format("模型 [%s] 的类型为 %s,该算法原生不支持特征重要性接口。", modelName,
                                        coreModel.getClass().getSimpleName()));
                }
                logger.info("========================================\n");
        }
}