













<?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>
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);
}
}
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");
}
}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。