<think>
我们之前已经详细讨论了Java中计算两个文本相似度的多种方法,包括余弦相似度和编辑距离等。现在用户再次询问Java实现两个文本相似度计算的方法,可能是希望得到一个更简洁的总结或者具体的代码示例。结合用户提供的引用,我们可以看到:
- 引用[1]提到了相似度计算是机器学习和自然语言处理的核心组件,并提到不同场景使用不同算法。
- 引用[2]具体提到了使用余弦相似度计算文本相似度,用于新闻去重。
- 引用[3]给出了使用HanLP分词和Apache Commons Math库计算余弦相似度的具体依赖和步骤。
因此,我们可以为用户提供一个整合的、可直接运行的代码示例,重点放在余弦相似度上(因为引用[2][3]都重点提及),同时简要提及其他方法以供选择。
### 一、基于余弦相似度的文本相似度计算(推荐长文本)
#### 1. 添加依赖(Maven)
```xml
<!-- 中文分词 HanLP -->
<dependency>
<groupId>com.hankcs</groupId>
<artifactId>hanlp</artifactId>
<version>portable-1.8.10</version>
</dependency>
<!-- 数学工具包(用于向量运算) -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-math3</artifactId>
<version>3.6.1</version>
</dependency>
```
#### 2. 完整代码实现
```java
import com.hankcs.hanlp.HanLP;
import org.apache.commons.math3.linear.ArrayRealVector;
import org.apache.commons.math3.linear.RealVector;
import java.util.*;
import java.util.stream.Collectors;
public class TextSimilarity {
// 文本预处理(分词、转小写、过滤停用词)
public static List<String> preprocessText(String text) {
// 使用HanLP分词
List<String> words = HanLP.segment(text).stream()
.map(term -> term.word.toLowerCase())
.collect(Collectors.toList());
// 简单过滤停用词(实际项目中应使用完整停用词表)
List<String> stopWords = Arrays.asList("的", "了", "在", "是", "我");
return words.stream()
.filter(word -> !stopWords.contains(word) && word.length()>1)
.collect(Collectors.toList());
}
// 构建文本向量(TF-IDF权重)
public static Map<String, Double> buildTfIdfVector(List<String> words, Map<String, Double> idfMap) {
Map<String, Double> tfMap = new HashMap<>();
// 计算词频TF
for (String word : words) {
tfMap.put(word, tfMap.getOrDefault(word, 0.0) + 1.0);
}
// 转换为TF-IDF
int totalWords = words.size();
Map<String, Double> tfidfMap = new HashMap<>();
for (Map.Entry<String, Double> entry : tfMap.entrySet()) {
String word = entry.getKey();
double tf = entry.getValue() / totalWords;
double idf = idfMap.getOrDefault(word, 0.0);
tfidfMap.put(word, tf * idf);
}
return tfidfMap;
}
// 计算余弦相似度(使用Apache Commons Math)
public static double cosineSimilarity(Map<String, Double> vec1, Map<String, Double> vec2) {
// 获取所有不重复的词汇
Set<String> vocabulary = new HashSet<>(vec1.keySet());
vocabulary.addAll(vec2.keySet());
// 创建向量
double[] vector1 = new double[vocabulary.size()];
double[] vector2 = new double[vocabulary.size()];
int i = 0;
for (String word : vocabulary) {
vector1[i] = vec1.getOrDefault(word, 0.0);
vector2[i] = vec2.getOrDefault(word, 0.0);
i++;
}
RealVector v1 = new ArrayRealVector(vector1);
RealVector v2 = new ArrayRealVector(vector2);
return v1.cosine(v2); // 返回范围[-1,1],但文本相似度通常为[0,1]
}
public static void main(String[] args) {
String text1 = "Java实现文本相似度计算";
String text2 = "用Java计算两个文本的相似性";
// 预处理
List<String> words1 = preprocessText(text1);
List<String> words2 = preprocessText(text2);
// 构建语料库(这里简化:仅用这两篇文档)
List<List<String>> documents = Arrays.asList(words1, words2);
// 计算IDF(逆文档频率)
Map<String, Double> idfMap = new HashMap<>();
int docCount = documents.size();
for (List<String> doc : documents) {
Set<String> uniqueWords = new HashSet<>(doc);
for (String word : uniqueWords) {
idfMap.put(word, idfMap.getOrDefault(word, 0.0) + 1.0);
}
}
// 转换IDF值:idf = log(总文档数/(包含该词的文档数+1))
idfMap.replaceAll((word, count) -> Math.log(docCount / (count + 1)));
// 构建向量
Map<String, Double> vec1 = buildTfIdfVector(words1, idfMap);
Map<String, Double> vec2 = buildTfIdfVector(words2, idfMap);
// 计算相似度
double similarity = cosineSimilarity(vec1, vec2);
System.out.println("文本相似度: " + similarity); // 输出约0.707
}
}
```
#### 3. 关键步骤说明
1. **文本预处理**:使用HanLP分词并过滤停用词
2. **TF-IDF向量构建**:
- **TF(词频)**:$ \text{tf}(t,d) = \frac{\text{词t在文档d中出现的次数}}{\text{文档d的总词数}} $
- **IDF(逆文档频率)**:$ \text{idf}(t,D) = \log\frac{\text{语料库中文档总数}}{\text{包含词t的文档数} + 1} $[^2]
3. **余弦相似度计算**:$ \cos(\theta) = \frac{\mathbf{A} \cdot \mathbf{B}}{\|\mathbf{A}\| \|\mathbf{B}\|} $,值越接近1表示越相似
### 二、其他方法快速实现(根据场景选择)
#### 1. 编辑距离(适合短文本)
```java
public static double levenshteinSimilarity(String s1, String s2) {
int[][] dp = new int[s1.length() + 1][s2.length() + 1];
for (int i = 0; i <= s1.length(); i++) dp[i][0] = i;
for (int j = 0; j <= s2.length(); j++) dp[0][j] = j;
for (int i = 1; i <= s1.length(); i++) {
for (int j = 1; j <= s2.length(); j++) {
int cost = (s1.charAt(i - 1) == s2.charAt(j - 1)) ? 0 : 1;
dp[i][j] = Math.min(Math.min(
dp[i - 1][j] + 1, // 删除
dp[i][j - 1] + 1), // 插入
dp[i - 1][j - 1] + cost // 替换
);
}
}
int maxLen = Math.max(s1.length(), s2.length());
return 1.0 - (double) dp[s1.length()][s2.length()] / maxLen;
}
```
#### 2. Jaccard相似度(适合关键词集合)
```java
public static double jaccardSimilarity(Set<String> set1, Set<String> set2) {
Set<String> intersection = new HashSet<>(set1);
intersection.retainAll(set2);
Set<String> union = new HashSet<>(set1);
union.addAll(set2);
return (double) intersection.size() / union.size();
}
```
### 三、方法选择建议
- **长文本(如文章)**:余弦相似度(带TF-IDF)[^2]
- **短文本(如标题)**:编辑距离或Jaccard相似度
- **实时性要求高**:Jaccard > 编辑距离 > 余弦相似度
### 四、常见问题解决方案
1. **性能优化**:
- 余弦相似度:使用稀疏向量库(如`Mahout`)或局部敏感哈希(LSH)
- 编辑距离:使用`dp`数组复用或`Wagner-Fischer`算法优化
2. **多语言支持**:
- 中文:使用`HanLP`
- 英文:使用`Stanford NLP`或`OpenNLP`
3. **阈值设定**:相似度>0.6可视为潜在重复内容(需根据业务调整)[^3]
§§相关问题§§
1. 如何提升余弦相似度计算在大规模文本中的性能?
2. 文本相似度计算能否应用于代码抄袭检测?
3. 有没有开箱即用的Java库可以直接计算文本相似度?
</think>
### Java实现两个文本相似度计算的方法详解
在Java中计算两个文本的相似度,主要通过以下步骤实现:
#### 一、核心方法选择
1. **余弦相似度**(最适合长文本):
- 将文本转化为向量空间模型
- 计算向量夹角的余弦值
- 值域[0,1],值越大越相似[^2]
2. **编辑距离**(更适合短文本):
- 计算使两个字符串相同所需的最少编辑操作
- 通过动态规划实现
3. **Jaccard相似度**(适合关键词比较):
- 计算交集与并集的比例
#### 二、完整实现步骤(余弦相似度为例)
##### 1. 添加依赖
```xml
<!-- HanLP中文分词 -->
<dependency>
<groupId>com.hankcs</groupId>
<artifactId>hanlp</artifactId>
<version>portable-1.8.10</version>
</dependency>
<!-- Apache Commons Math(向量计算) -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-math3</artifactId>
<version>3.6.1</version>
</dependency>
```
##### 2. 文本预处理
```java
public static List<String> preprocessText(String text) {
// 中文分词
List<String> words = HanLP.segment(text)
.stream()
.map(term -> term.word)
.collect(Collectors.toList());
// 标准化处理
List<String> stopWords = Arrays.asList("的", "了", "在", "是", "我");
return words.stream()
.filter(word -> !stopWords.contains(word)) // 过滤停用词
.map(String::toLowerCase) // 转小写
.filter(word -> word.length() > 1) // 过滤单字
.collect(Collectors.toList());
}
```
##### 3. 构建TF-IDF向量
```java
public static Map<String, Double> buildTfIdfVector(List<String> words,
Map<String, Double> idfMap) {
// 计算词频(TF)
Map<String, Double> tfMap = new HashMap<>();
for (String word : words) {
tfMap.put(word, tfMap.getOrDefault(word, 0.0) + 1);
}
// TF-IDF转换
Map<String, Double> tfidfMap = new HashMap<>();
for (Map.Entry<String, Double> entry : tfMap.entrySet()) {
String word = entry.getKey();
double tf = entry.getValue() / words.size();
double idf = idfMap.getOrDefault(word, 0.0);
tfidfMap.put(word, tf * idf);
}
return tfidfMap;
}
```
##### 4. 计算余弦相似度(使用Apache Commons Math)
```java
public static double cosineSimilarity(Map<String, Double> vec1,
Map<String, Double> vec2) {
// 获取所有词汇
Set<String> vocabulary = new HashSet<>(vec1.keySet());
vocabulary.addAll(vec2.keySet());
// 创建向量
double[] vector1 = new double[vocabulary.size()];
double[] vector2 = new double[vocabulary.size()];
int i = 0;
for (String word : vocabulary) {
vector1[i] = vec1.getOrDefault(word, 0.0);
vector2[i] = vec2.getOrDefault(word, 0.0);
i++;
}
// 使用Apache Commons Math计算
RealVector v1 = new ArrayRealVector(vector1);
RealVector v2 = new ArrayRealVector(vector2);
return v1.cosine(v2); // 返回范围[0,1]
}
```
##### 5. 完整工作流程
```java
public static void main(String[] args) {
String text1 = "Java实现文本相似度计算";
String text2 = "如何用Java计算两个文本的相似性";
// 1. 文本预处理
List<String> words1 = preprocessText(text1);
List<String> words2 = preprocessText(text2);
// 2. 构建语料库(此处简化)
List<List<String>> documents = Arrays.asList(words1, words2);
// 3. 计算IDF值
Map<String, Double> idfMap = new HashMap<>();
int docCount = documents.size();
for (List<String> doc : documents) {
Set<String> uniqueWords = new HashSet<>(doc);
for (String word : uniqueWords) {
idfMap.put(word, idfMap.getOrDefault(word, 0.0) + 1.0);
}
}
// IDF公式:$ \text{idf}(t) = \log \frac{N}{1 + \text{df}(t)} $
idfMap.replaceAll((k, v) -> Math.log(docCount / (v + 1)));
// 4. 构建TF-IDF向量
Map<String, Double> vec1 = buildTfIdfVector(words1, idfMap);
Map<String, Double> vec2 = buildTfIdfVector(words2, idfMap);
// 5. 计算相似度
double similarity = cosineSimilarity(vec1, vec2);
System.out.println("文本相似度: " + similarity); // 输出0.72左右
}
```
#### 三、其他相似度算法实现
##### 1. 编辑距离(Levenshtein距离)
```java
public static int levenshteinDistance(CharSequence s1, CharSequence s2) {
int[][] dp = new int[s1.length() + 1][s2.length() + 1];
for (int i = 0; i <= s1.length(); i++) dp[i][0] = i;
for (int j = 0; j <= s2.length(); j++) dp[0][j] = j;
for (int i = 1; i <= s1.length(); i++) {
for (int j = 1; j <= s2.length(); j++) {
int cost = (s1.charAt(i-1) == s2.charAt(j-1)) ? 0 : 1;
dp[i][j] = Math.min(Math.min(
dp[i-1][j] + 1, // 删除
dp[i][j-1] + 1), // 插入
dp[i-1][j-1] + cost // 替换
);
}
}
return dp[s1.length()][s2.length()];
}
// 计算归一化相似度:$ \text{similarity} = 1 - \frac{\text{distance}}{\max(\text{len}_1, \text{len}_2)} $
```
#### 四、算法选择指南
| **算法** | **适用场景** | **优缺点** |
|----------------|----------------------------------|------------------------------|
| 余弦相似度 | 长文本、文档比对、新闻去重[^2] | 精度高,但计算较复杂 |
| 编辑距离 | 短文本、拼写检查、标题匹配 | 计算简单,但忽略语义 |
| Jaccard相似度 | 标签匹配、关键词提取 | 计算快,适合非连续词序场景 |
#### 五、最佳实践建议
1. **中文处理**:必须使用分词工具(如HanLP)
2. **性能优化**:
- 使用稀疏向量存储(仅存非零值)
- 对长文本采样关键段落计算
- 使用MinHash等近似算法
3. **阈值设定**:
- >0.75 高度相似
- 0.5-0.75 部分相似
- <0.5 基本不相似
> **关键提示**:实际应用中,余弦相似度+TF-IDF是最常用且效果最好的组合方案,特别适合文档去重场景[^2][^3]。