Java中集成Smile 技术教程:从入门到工程实践
本文是一篇结构化的 Smile 统计机器学习库技术教程,涵盖简介、核心功能、API 说明、基础使用示例、完整可运行示例工程、业务场景实战、常见问题与最佳实践,以及 Smile 与其它 ML 库的关系定位。
目录
- [一、Smile 简介](#一、Smile 简介)
- 二、核心功能概览
- 三、核心概念与数据结构
- [四、Java 环境准备](#四、Java 环境准备)
- [五、核心 API 说明](#五、核心 API 说明)
- 六、基础使用示例
- 七、完整可运行示例工程
- 八、业务场景实战
- 九、常见问题与最佳实践
- [十、Smile 与其它 ML 库的关系](#十、Smile 与其它 ML 库的关系)
- [附录:API 速查表](#附录:API 速查表)
一、Smile 简介
Smile(Statistical Machine Intelligence and Learning Engine) 是一个快速、全面的纯 JVM 统计机器学习与数据挖掘库,由 Haifeng Li 主导开发。它覆盖了分类、回归、聚类、关联规则、特征选择、可视化、数学/线性代数等全套能力。
1.1 关键认知
- 纯 JVM、无 native 也能跑大部分算法 :分类(逻辑回归)、聚类(KMeans)等用纯 Java 实现,无需加载原生库;只有回归/矩阵运算依赖 bytedeco openblas 原生后端(本工程已配好)。
- API 极其「函数式」 :算法几乎都是静态方法,如
LogisticRegression.fit(x, y)、KMeans.lloyd(data, k)、RidgeRegression.fit(formula, df)------像调数学函数一样训练模型,代码可读性高。 - 最适合「读源码学算法」:Smile 的实现非常直白易懂,是理解 LR/KMeans/决策树等算法原理的绝佳教材。
- 与 Weka(GUI 工作台)、Tribuo(类型安全)并列,是 Java 生态三大经典 ML 库之一。
1.2 资源
- 官网:
https://haifengl.github.io/ - GitHub:
https://github.com/haifengl/smile
注:
博客:
https://blog.csdn.net/badao_liumang_qizhi
二、核心功能概览
| 功能域 | 典型算法 | 核心 API |
|---|---|---|
| 分类 | 逻辑回归、SVM、随机森林、决策树 | smile.classification.*,如 LogisticRegression.fit |
| 聚类 | K-Means、层次聚类、DBSCAN | smile.clustering.KMeans.lloyd |
| 回归 | 岭回归、LASSO、线性回归 | smile.regression.RidgeRegression.fit |
| 数学/线性代数 | 矩阵、向量、特征值 | smile.math.matrix.Matrix |
| 可视化 | 散点/折线/曲面 | smile.plot(基于 Swing)+ 任意 JDK 画布 |
本工程演示分类、聚类、回归、可视化四条最常用主线。
三、核心概念与数据结构
| 概念 | 说明 |
|---|---|
静态 fit |
Smile 训练入口统一为静态方法 fit(...),返回模型对象(如 LogisticRegression、KMeans、LinearModel)。 |
double[][] 特征矩阵 |
分类/聚类的数据载体:x[i] 是第 i 个样本的特征向量,y[i] 是标签(分类)或被预测值(回归)。 |
DataFrame + Formula |
回归 API(2.x)走「公式」路线:Formula.lhs("y") 表示以 y 为响应变量,其余列自动作为自变量;DataFrame.of(data, "x1","y") 构造带列名的数据框。 |
predict |
模型推断:model.predict(x) 返回类别/预测值;kmeans.predict(d) 返回所属簇下标。 |
| openblas 后端 | 回归/矩阵运算依赖 bytedeco 的 openblas 原生库(org.bytedeco:openblas + 平台 classifier)。分类/聚类不需要它。 |
| 可视化 | Smile 自带 smile.plot 基于 Swing(需显示环境);离线场景可用 JDK BufferedImage 做等效散点图(本工程采用此法,必定可跑)。 |
四、Java 环境准备
4.1 依赖(Maven)
xml
<properties>
<java.version>17</java.version>
<smile.version>2.6.0</smile.version>
<openblas.version>0.3.26-1.5.10</openblas.version>
<javacpp.platform>windows-x86_64</javacpp.platform>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Smile 核心:分类 / 聚类 / 回归 / 数学 -->
<dependency>
<groupId>com.github.haifengl</groupId>
<artifactId>smile-core</artifactId>
<version>${smile.version}</version>
</dependency>
<!-- 线性代数后端:回归/矩阵需要 openblas 原生库(与 JavaCPP 工程一致) -->
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>openblas</artifactId>
<version>${openblas.version}</version>
</dependency>
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>openblas</artifactId>
<version>${openblas.version}</version>
<classifier>${javacpp.platform}</classifier>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
4.2 端口与应用名
src/main/resources/application.yml:
yaml
server:
port: 1000
spring:
application:
name: smile-demo
logging:
level:
com.badao: info
分类与聚类不需要 openblas 原生库 即可运行;只有回归(
RidgeRegression/LinearModel)在矩阵运算时需要它。若你只跑分类/聚类,可移除 openblas 依赖。
五、核心 API 说明
5.1 分类 LogisticRegression.fit
java
double[][] x = ...; // n×d 特征
int[] y = ...; // n 标签(0/1)
LogisticRegression model = LogisticRegression.fit(x, y);
int pred = model.predict(x[0]); // 预测类别
5.2 聚类 KMeans.lloyd
java
double[][] data = ...; // 仅特征,无标签
KMeans kmeans = KMeans.lloyd(data, 3); // 聚成 3 簇
int[] sizes = new int[3];
for (double[] d : data) sizes[kmeans.predict(d)]++;
5.3 回归 RidgeRegression.fit(DataFrame + Formula)
java
double[][] data = ...; // 列: x1, y
DataFrame df = DataFrame.of(data, "x1", "y");
Formula formula = Formula.lhs("y"); // 以 y 为响应变量
LinearModel model = RidgeRegression.fit(formula, df);
double pred = model.predict(new double[]{ xi }); // 注意只传自变量列
model.predict(new double[]{xi})只传自变量 (不含 y)。Formula.lhs("y")把y作为目标,其余列作特征。
5.4 散点可视化(JDK BufferedImage,离线可跑)
java
BufferedImage img = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
Graphics2D g = img.createGraphics();
g.setColor(Color.WHITE); g.fillRect(0, 0, w, h);
g.setColor(Color.RED);
g.fillOval(px - 3, py - 3, 6, 6); // 把数据点映射到像素
g.dispose();
ImageIO.write(img, "PNG", new File(outPath));
六、基础使用示例
6.1 逻辑回归分类(最小可用)
java
int n = 200;
double[][] x = new double[n][2];
int[] y = new int[n];
Random rnd = new Random(42);
for (int i = 0; i < n; i++) {
boolean pos = i < n / 2;
double cx = pos ? 2.0 : -2.0;
x[i][0] = cx + rnd.nextGaussian() * 0.5;
x[i][1] = cx + rnd.nextGaussian() * 0.5;
y[i] = pos ? 1 : 0;
}
LogisticRegression model = LogisticRegression.fit(x, y);
System.out.println("预测第0个样本: " + model.predict(x[0]));
6.2 KMeans 聚类
java
double[][] data = ...; // 仅特征
KMeans kmeans = KMeans.lloyd(data, 3);
System.out.println("簇0包含点: " + kmeans.predict(data[0]));
七、完整可运行示例工程
工程目录:
smileDemo/
├── pom.xml
├── src/main/java/com/badao/demo/
│ ├── DemoApplication.java
│ ├── config/SmileConfig.java # 就绪日志(纯JVM无需native)
│ ├── service/SmileService.java # 分类/聚类/回归/可视化
│ └── controller/SmileController.java
└── src/test/java/com/badao/demo/SmileAllFeatureTest.java
7.1 启动类
java
package com.badao.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
7.2 配置(纯 JVM 说明)
java
package com.badao.demo.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import jakarta.annotation.PostConstruct;
@Slf4j
@Configuration
public class SmileConfig {
@PostConstruct
public void init() {
log.info("Smile 统计机器学习库已就绪(纯 JVM 实现,无需加载 native 库;代码干净,适合读源码学算法)");
}
}
7.3 核心 Service(四类能力)
java
package com.badao.demo.service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import smile.classification.LogisticRegression;
import smile.clustering.KMeans;
import smile.data.DataFrame;
import smile.data.formula.Formula;
import smile.regression.LinearModel;
import smile.regression.RidgeRegression;
import javax.imageio.ImageIO;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.util.Random;
@Slf4j
@Service
public class SmileService {
public String info() {
return "Smile 2.6.0 统计机器学习库(纯 JVM,无需 native);覆盖分类/聚类/回归/可视化";
}
public String classificationDemo() {
int n = 200;
double[][] x = new double[n][2];
int[] y = new int[n];
Random rnd = new Random(42);
for (int i = 0; i < n; i++) {
boolean pos = i < n / 2;
double cx = pos ? 2.0 : -2.0;
double r = 0.5 + rnd.nextDouble() * 0.5;
double theta = rnd.nextDouble() * Math.PI * 2;
x[i][0] = cx + r * Math.cos(theta) + rnd.nextGaussian() * 0.3;
x[i][1] = cx + r * Math.sin(theta) + rnd.nextGaussian() * 0.3;
y[i] = pos ? 1 : 0;
}
LogisticRegression model = LogisticRegression.fit(x, y);
int correct = 0;
for (int i = 0; i < n; i++) if (model.predict(x[i]) == y[i]) correct++;
return String.format("逻辑回归分类:%d 样本,训练集准确率 %.2f%%", n, correct * 100.0 / n);
}
public String clusteringDemo() {
int n = 300;
double[][] data = new double[n][2];
Random rnd = new Random(7);
for (int i = 0; i < n; i++) {
int c = i % 3;
double cx = (c == 0) ? -3 : (c == 1) ? 0 : 3;
double cy = (c == 0) ? -3 : (c == 1) ? 3 : -3;
data[i][0] = cx + rnd.nextGaussian() * 0.4;
data[i][1] = cy + rnd.nextGaussian() * 0.4;
}
KMeans kmeans = KMeans.lloyd(data, 3);
int[] sizes = new int[3];
for (double[] d : data) sizes[kmeans.predict(d)]++;
return String.format("KMeans(k=3) 聚类:%d 点 -> 簇大小 [%d, %d, %d]", n, sizes[0], sizes[1], sizes[2]);
}
public String regressionDemo() {
int n = 100;
double[][] data = new double[n][2];
Random rnd = new Random(11);
for (int i = 0; i < n; i++) {
double xi = rnd.nextDouble() * 10 - 5;
data[i][0] = xi;
data[i][1] = 2.5 * xi + 1.0 + rnd.nextGaussian() * 1.5;
}
DataFrame df = DataFrame.of(data, "x1", "y");
Formula formula = Formula.lhs("y");
LinearModel model = RidgeRegression.fit(formula, df);
double sse = 0;
for (int i = 0; i < n; i++) {
double pred = model.predict(new double[]{data[i][0]});
sse += (pred - data[i][1]) * (pred - data[i][1]);
}
return String.format("岭回归(Ridge)拟合 y=2.5x+1:%d 样本,RMSE=%.3f", n, Math.sqrt(sse / n));
}
public String scatterPlot(String outPath) throws Exception {
int n = 300;
double[][] data = new double[n][2];
int[] label = new int[n];
Random rnd = new Random(7);
for (int i = 0; i < n; i++) {
int c = i % 3;
double cx = (c == 0) ? -3 : (c == 1) ? 0 : 3;
double cy = (c == 0) ? -3 : (c == 1) ? 3 : -3;
data[i][0] = cx + rnd.nextGaussian() * 0.4;
data[i][1] = cy + rnd.nextGaussian() * 0.4;
label[i] = c;
}
int w = 400, h = 400;
BufferedImage img = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
Graphics2D g = img.createGraphics();
g.setColor(Color.WHITE); g.fillRect(0, 0, w, h);
Color[] colors = {Color.RED, Color.GREEN, Color.BLUE};
for (int i = 0; i < n; i++) {
int px = (int) ((data[i][0] + 6) / 12.0 * w);
int py = (int) ((6 - data[i][1]) / 12.0 * h);
g.setColor(colors[label[i]]);
g.fillOval(px - 3, py - 3, 6, 6);
}
g.dispose();
File f = new File(outPath);
if (f.getParentFile() != null) f.getParentFile().mkdirs();
ImageIO.write(img, "PNG", f);
return String.format("散点可视化完成 -> %s", outPath);
}
}
7.4 HTTP 演示接口
java
package com.badao.demo.controller;
import com.badao.demo.service.SmileService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/smile")
public class SmileController {
@Autowired
private SmileService smileService;
@GetMapping("/info") public Map<String, Object> info() { return wrap(smileService::info); }
@GetMapping("/classify") public Map<String, Object> classify(){ return wrap(smileService::classificationDemo); }
@GetMapping("/cluster") public Map<String, Object> cluster() { return wrap(smileService::clusteringDemo); }
@GetMapping("/regress") public Map<String, Object> regress() { return wrap(smileService::regressionDemo); }
@GetMapping("/scatter") public Map<String, Object> scatter(@RequestParam String out) {
return wrap(() -> smileService.scatterPlot(out));
}
private Map<String, Object> wrap(Op op) {
Map<String, Object> resp = new HashMap<>();
try {
resp.put("success", true);
resp.put("message", op.run());
} catch (Exception e) {
resp.put("success", false);
resp.put("message", e.getMessage());
}
return resp;
}
@FunctionalInterface
private interface Op { String run() throws Exception; }
}
7.5 运行与调用
bash
cd smileDemo
mvn compile
mvn spring-boot:run # 端口 1000
curl "http://localhost:1000/smile/info"
curl "http://localhost:1000/smile/classify"
curl "http://localhost:1000/smile/cluster"
curl "http://localhost:1000/smile/regress"
curl "http://localhost:1000/smile/scatter?out=./out/scatter.png"
7.6 单元测试
全部用例纯内存、无网络/无原生依赖(回归依赖 openblas,已配好),mvn test 直接 BUILD SUCCESS。
八、业务场景实战
8.1 场景一:用户分群(KMeans 客户聚类)
业务:电商按「消费频次 / 客单价」把用户聚成高/中/低价值簇,做差异化运营。
java
double[][] userFeat = ...; // 每行: [消费频次, 客单价]
KMeans km = KMeans.lloyd(userFeat, 3);
// km.predict(userFeat[i]) == 0/1/2 -> 对应 高/中/低 价值群
8.2 场景二:信用评分 / 风控分类(逻辑回归)
业务:银行用逻辑回归对用户「好/坏」二分类,输出违约概率。
java
// x: [年龄, 收入, 负债比, 历史逾期次数], y: 0/1
LogisticRegression model = LogisticRegression.fit(x, y);
double prob = model.predict(xNew); // 概率/类别,用于自动化初审
8.3 场景三:销量预测(岭回归)
业务:零售按历史销量 + 促销力度预测下月销量,做备货。
java
double[][] d = ...; // 列: promo, season, sales
LinearModel m = RidgeRegression.fit(Formula.lhs("sales"), DataFrame.of(d, "promo", "season", "sales"));
double forecast = m.predict(new double[]{ promoNew, seasonNew });
8.4 场景四:异常检测(聚类离群点)
业务:服务器指标聚成正常簇,远离簇中心的点判为异常告警。
java
KMeans km = KMeans.lloyd(metrics, 5);
for (double[] point : metrics) {
int c = km.predict(point);
double dist = distanceToCentroid(point, km, c); // 计算到簇中心距离
if (dist > threshold) alert(point);
}
8.5 场景五:数据探索可视化(散点报告)
业务:数据科学家在生成报告时把高维样本投影到 2D 散点,标色后导出 PNG 给业务方看。
java
// 复用 SmileService.scatterPlot(outPath),JDK 原生绘制,无 GUI 依赖
smileService.scatterPlot("report/scatter.png");
九、常见问题与最佳实践
| 现象 | 原因 / 解决 |
|---|---|
回归报 UnsatisfiedLinkError / openblas 找不到 |
漏了 org.bytedeco:openblas 的平台 classifier (如 windows-x86_64);或分类/聚类不需要但误删。 |
RidgeRegression.fit 报列名错误 |
Formula.lhs("y") 的列名必须与 DataFrame.of(data, "x1","y") 的列名一致。 |
model.predict 维度报错 |
回归 predict 只传自变量 (new double[]{xi}),不要含 y 列。 |
| 分类/聚类很慢 | 数据规模大时可用 KMeans.kmeans(...) 其它初始化,或降采样;Smile 多为单机内存算法。 |
最佳实践:
- 区分依赖:分类/聚类纯 Java 即可;回归/矩阵才需要 openblas,按需引入。
- DataFrame + Formula 是 2.x 主流 :新代码优先走
DataFrame.of+Formula,可读性好且列名清晰。 - 读源码学算法 :Smile 实现直白,遇到不懂的算法直接看
LogisticRegression/KMeans源码。 - 可视化离线化 :生产无显示环境时,用 JDK
BufferedImage而非smile.plot(Swing)。
十、Smile 与其它 ML 库的关系
Java 三大经典 ML 库
┌─────────┬─────────┬─────────┐
│ Smile │ Weka │ Tribuo │
│ 函数式 │ GUI工作台│ 类型安全│
│ 易读源码│ 全流程 │ 泛型约束│
└─────────┴─────────┴─────────┘
均被 DJL/SmartJavaAI 调用或并列使用
- Smile:API 最像「数学函数」,适合学算法、做轻量预测服务。
- Weka:自带 GUI Explorer,适合探索式建模与教学。
- Tribuo :Oracle 出品,类型安全(
Dataset<Label>),适合工程化强约束场景。 - 三者都属于「经典 ML」;与「深度学习」的 DJL/DeepLearning4J 互补。
附录:API 速查表
| 能力 | 核心 API | 关键要点 |
|---|---|---|
| 分类 | LogisticRegression.fit(double[][] x, int[] y) |
返回 LogisticRegression;predict(x) 得类别 |
| 聚类 | KMeans.lloyd(double[][] data, int k) |
返回 KMeans;predict(d) 得簇下标 |
| 回归 | RidgeRegression.fit(Formula, DataFrame) |
Formula.lhs("y") 指定响应变量 |
| 数据框 | DataFrame.of(double[][] data, String... cols) |
列名用于 Formula 匹配 |
| 公式 | Formula.lhs("y") |
以 y 为目标,其余为自变量 |
| 预测 | model.predict(double[]) |
回归只传自变量列 |
| 可视化 | new BufferedImage(w,h,TYPE_INT_RGB) + Graphics2D |
离线散点方案 |
| 线性代数 | smile.math.matrix.Matrix |
矩阵运算后端为 openblas |
通用范式:
java
// 分类/聚类:特征矩阵 + 静态 fit
LogisticRegression m = LogisticRegression.fit(x, y);
// 回归:DataFrame + Formula 路线
LinearModel m2 = RidgeRegression.fit(Formula.lhs("y"), DataFrame.of(data, "x1", "y"));