前言
專案中有一個查重的需求,就類似論文查重這種的需求,我的組長已經寫好了這個 Demo 了,我也挺感興趣的,所以也看了看是如何實作的,看完后,感慨一聲,噢!原來是這樣實作的啊!現在呢,就記錄下我從中學到的知識!
需求
輸入:需要查重的內容,通常是非常長的文本,對于論文來說,可能上萬字,
輸出:顯示重復的句子,將重復句子標紅,以及整體內容的重復率,
標紅是次要矛盾,查重是主要矛盾,需要先解決,
發揮想象
我們想象一下,純人工查重的辦法,作業人員拿到一篇論文,閱讀這篇論文(假設該作業人員的大腦是超強大腦,作業人員對論文庫中的論文非常熟悉,基本能倒背如流的程度),每閱讀一句就與大腦中的論文進行對比,如果發現重復的內容太多了(即重復的句子很多),那么計算下重復的內容大概占全文的多少,進而得出整篇論文的重復率,
很明顯,人工查重,效率肯定是不高的,
如何通過代碼實作?
已有資源:
- 一篇待查重的論文,假設論文內容兩萬字,
- 論文資料庫中大量的論文資料,假設資料庫中的每篇論文的內容也兩萬字左右,
思路:將輸入的論文內容與論文資料庫中存在的論文內容進行一一對比,
思考:
- 如何對比?是一句一句進行對比,還是一段一段的進行對比?
- 對比的時候,如何才能說明對比的內容是重復的?也就是說判斷重復的標準是什么?
接觸新領域:
自然語言處理(NLP),自然語言處理任務中,我們經常需要判斷兩篇檔案是否相似、計算兩篇檔案的相似程度,
文本相似度演算法
對于如何說明對比的內容是重復的,那么這里就涉及到文本相似度演算法了,通過查找資料,我了解到文本相似度演算法有挺多的,
掘金-如何計算兩個字串之間的文本相似度?
掘金-文本相似度計算之余弦定理
下面我列舉了幾種:
- Jaccard 相似度演算法
- Sorensen Dice 相似度系數
- Levenshtein
- 漢明距離(海明距離)(Hamming Distance)
- 余弦相似性
對于這些文本相似度的演算法,主要就是對文本進行分詞,然后再對分好的詞進行相關的計算,得出兩個文本的相似度,
所以,對于兩個文本,計算相似度的思路是:分詞->通過某種演算法計算得到相似度
斷句
當然,這些演算法,都是兩個文本進行的,這兩個文本可以是句子,也可以是段落,還可以是超長文本,假設我們直接是超長文本,直接使用相似度演算法去匹配相似度,那么可能會誤判,畢竟超長文本,分詞出來的詞語,相同的數量肯定是很多的,所以重復性也就會越高,
所以,首先要解決的問題就是,對于超長的文本,我們該如何進行中文斷句?
經過了解,得知 BreakIterator 這個類可以完成這件事,
BreakIterator:https://docs.oracle.com/javase/7/docs/api/java/text/BreakIterator.html
CSDN-Java國際化:BreakIterator
分詞
分詞,將一個句子中的詞語進行劃分,分出有意義的詞語,這里主要使用 IK 分詞器,
實作
準備作業
Maven依賴
<!-- IK分詞器 -->
<dependency>
<groupId>com.janeluo</groupId>
<artifactId>ikanalyzer</artifactId>
<version>2012_u6</version>
</dependency>
<!-- 漢語言處理包 Han Natural Language Processing -->
<dependency>
<groupId>com.hankcs</groupId>
<artifactId>hanlp</artifactId>
<version>portable-1.5.4</version>
</dependency>
<!-- 阿帕奇 集合工具 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.4</version>
</dependency>
<!-- 糊涂工具包 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.10</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.16</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.8.1</version>
</dependency>
Sentence 類
把句子抽象出來,寫成一個 Sentence 類去代表句子,
@Data
public class Sentence {
/**
* 文本
*/
private String text;
/**
* 相似度
*/
private Double similar;
/**
* 是否重復,0否,1是,默認0,重復標準就是,當相似度大于60%時,就認為該句子是重復的
*/
private Integer duplicatesState = 0;
/**
* 與該句子最相似的句子
*/
private Sentence maxSimilarSentence;
/**
* 重復句子下標,可能存在多個重復句子,所以使用集合記錄
*/
private List<Integer> duplicatesIndex = new ArrayList<>();
}
策略模式
由于這里有多種演算法,考慮可以使用策略模式,來選擇不同的演算法實作,
public interface SimDegreeAlgorithm {
/**
* 計算兩個句子的相似度
* @param a
* @param b
* @return double
**/
double getSimDegree(String a, String b);
}
/**
* @author god23bin
* @description Jaccard 相似度演算法,集合的交集與集合的并集的比例.
*/
public class Jaccard implements SimDegreeAlgorithm {
@Override
public double getSimDegree(String a, String b) {
}
}
/**
* @author god23bin
* @description 余弦相似性演算法
* 怎么用它來計算兩個字串之間的相似度呢?
* 首先我們將字串向量化(向量就是并集中的每個字符在各自中出現的頻率),之后就可以在一個平面空間中,求出他們向量之間夾角的余弦值即可,
*/
public class CosSim implements SimDegreeAlgorithm {
@Override
public double getSimDegree(String a, String b) {
}
}
/**
* @author god23bin
* @description 相似度演算法的策略
*/
public class SimDegreeStrategy {
private SimDegreeAlgorithm simDegreeAlgorithm;
public SimDegreeStrategy(SimDegreeAlgorithm simDegreeAlgorithm) {
this.simDegreeAlgorithm = simDegreeAlgorithm;
}
public double getSimDegree(String a, String b) {
return simDegreeAlgorithm.getSimDegree(a, b);
}
}
本簡單實作中,將選擇使用余弦相似性演算法來作為文本相似度演算法的實作,
斷句
寫一個工具類來實作斷句,簡單說明一下,如何通過 BreakIterator 這個類實作斷句,
- 呼叫
getSentenceInstance()就可以獲取能判斷句子邊界的實體物件, - 通過實體物件呼叫
setText()方法設定需要判斷的句子字串, - 通過實體物件呼叫
first()和next()方法判斷邊界點, - 根據邊界點進行分割字串,
public class SentenceUtil {
/**
* 將長文本進行斷句
* @param content 長文本
* @return
*/
public static List<Sentence> breakSentence(String content) {
// 獲取實體物件
BreakIterator iterator = BreakIterator.getSentenceInstance(Locale.CHINA);
// 設定文本,待斷句的長文本
iterator.setText(content);
// 存盤斷好的句子
List<Sentence> list = new ArrayList<>();
// 斷句的邊界
int firstIndex;
int lastIndex = iterator.first();
// lastIndex 不等于 -1 (BreakIterator.DONE的值為 -1),說明還沒斷完,還沒結束
while (lastIndex != BreakIterator.DONE) {
firstIndex = lastIndex;
lastIndex = iterator.next();
if (lastIndex != BreakIterator.DONE) {
Sentence sentence = new Sentence();
sentence.setText(content.substring(firstIndex, lastIndex));
list.add(sentence);
}
}
return list;
}
}
分詞
寫一個工具類來實作分詞,使用 IK 分詞器對文本進行分詞,
public class IKUtil {
/**
* 以List的形式回傳經過IK分詞器處理的文本分詞的結果
* @param text 需要分詞的文本
* @return
*/
public static List<String> divideText(String text) {
if (null == text || "".equals(text.trim())) {
return null;
}
// 分詞結果集
List<String> resultList = new ArrayList<>();
// 文本串 Reader
StringReader re = new StringReader(text);
// 智能分詞: 合并數詞和量詞,對分詞結果進行歧義判斷
IKSegmenter ik = new IKSegmenter(re, true);
// Lexeme 詞元物件
Lexeme lex = null;
try {
// 分詞,獲取下一個詞元
while ((lex = ik.next()) != null) {
// 獲取詞元的文本內容,存入結果集中
resultList.add(lex.getLexemeText());
}
} catch (IOException e) {
System.out.println("分詞IO例外:" + e.getMessage());
}
return resultList;
}
}
余弦相似性演算法
邏輯
整個演算法的邏輯是這樣的,那么我們一一實作,
@Override
public double getSimDegree(String a, String b) {
if (StringUtils.isBlank(a) || StringUtils.isBlank(b)) {
return 0f;
}
// 將句子進行分詞
// 計算句子中詞的詞頻
// 向量化
// a、b 一維向量
// 分別計算三個引數,再結合公式計算
}
統計詞頻
分詞上面已經實作,那現在是需要對句子中分好的詞進行詞頻的統計,分詞工具回傳的是一個 List<String> 集合,我們可以通過哈希表對集合中的詞語的出現次數進行統計,就是我們要的詞頻了,
public static Map<String, Integer> getWordsFrequency(List<String> words) {
Map<String, Integer> wordFrequency = new HashMap<>(16);
// 統計詞的出現次數,即詞頻
for (String word : words) {
wordFrequency.put(word, wordFrequency.getOrDefault(word, 0) + 1);
}
return wordFrequency;
}
向量化
向量化,我們看看 @呼延十 大佬是如何說的:
字串向量化怎么做呢?我舉一個簡單的例子:
A: 呼延十二 B: 呼延二十三 他們的并集 [呼,延,二,十,三] 向量就是并集中的每個字符在各自中出現的頻率, A 的向量:[1,1,1,1,0] B 的向量:[1,1,1,1,1]掘金-如何計算兩個字串之間的文本相似度?-余弦相似性
所以
兩個句子是這樣的:
句子1:你笑起來真好看,像春天的花一樣!
句子2:你贊起來真好看,像夏天的陽光!
進行分詞,分詞結果及頻率:
[你, 笑起來, 真, 好看, 像, 春天, 的, 花, 一樣],出現頻率都是1
[你, 贊, 起來, 真, 好看, 像, 夏天, 的, 陽光],出現頻率都是1
它們的并集:
[你,笑起來,贊,起來,真,好看,像,春天,夏天,的,花,一樣,陽光]
它們的向量:
[你,笑起來,贊,起來,真,好看,像,春天,夏天,的,花,一樣,陽光]
句子1向量:[1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 0 ]
句子2向量:[1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1 ]
代碼表示:
// 向量化,先并集,然后遍歷在并集中對應詞語,在自己的分詞集合中對應詞語出現次數,組成的數就是向量
Set<String> union = new HashSet<>();
union.addAll(aWords);
union.addAll(bWords);
// a、b 一維向量
int[] aVector = new int[union.size()];
int[] bVector = new int[union.size()];
List<String> collect = new ArrayList<>(union);
for (int i = 0; i < collect.size(); ++i) {
aVector[i] = aWordsFrequency.getOrDefault(collect.get(i), 0);
bVector[i] = bWordsFrequency.getOrDefault(collect.get(i), 0);
}
計算余弦相似度
最后,計算余弦相似度,結合公式計算,

/**
* 分別計算三個引數
* @param aVec a 一維向量
* @param bVec b 一維向量
*/
public static double similarity(int[] aVec, int[] bVec) {
int n = aVec.length;
double p1 = 0;
double p2 = 0f;
double p3 = 0f;
for (int i = 0; i < n; i++) {
p1 += (aVec[i] * bVec[i]);
p2 += (aVec[i] * aVec[i]);
p3 += (bVec[i] * bVec[i]);
}
p2 = Math.sqrt(p2);
p3 = Math.sqrt(p3);
// 結合公式計算
return (p1) / (p2 * p3);
}
代碼
CosSim
public class CosSim implements SimDegreeAlgorithm {
/**
* 計算兩個句子的相似度:余弦相似度演算法
* @param a 句子1
* @param b 句子2
**/
@Override
public double getSimDegree(String a, String b) {
if (StringUtils.isBlank(a) || StringUtils.isBlank(b)) {
return 0f;
}
// 將句子進行分詞
List<String> aWords = IKUtil.divideText(a);
List<String> bWords = IKUtil.divideText(b);
// 計算句子中詞的詞頻
Map<String, Integer> aWordsFrequency = getWordsFrequency(aWords);
Map<String, Integer> bWordsFrequency = getWordsFrequency(bWords);
// 向量化,先并集,然后遍歷在并集中對應詞語,在自己的分詞集合中對應詞語出現次數,組成的數就是向量
Set<String> union = new HashSet<>();
union.addAll(aWords);
union.addAll(bWords);
// a、b 一維向量
int[] aVector = new int[union.size()];
int[] bVector = new int[union.size()];
List<String> collect = new ArrayList<>(union);
for (int i = 0; i < collect.size(); ++i) {
aVector[i] = aWordsFrequency.getOrDefault(collect.get(i), 0);
bVector[i] = bWordsFrequency.getOrDefault(collect.get(i), 0);
}
// 分別計算三個引數,再結合公式計算
return similarity(aVector, bVector);
}
public static Map<String, Integer> getWordsFrequency(List<String> words) {
Map<String, Integer> wordFrequency = new HashMap<>(16);
// 統計詞的出現次數,即詞頻
for (String word : words) {
wordFrequency.put(word, wordFrequency.getOrDefault(word, 0) + 1);
}
return wordFrequency;
}
/**
* 分別計算三個引數
* @param aVec a 一維向量
* @param bVec b 一維向量
**/
public static double similarity(int[] aVec, int[] bVec) {
int n = aVec.length;
double p1 = 0;
double p2 = 0f;
double p3 = 0f;
for (int i = 0; i < n; i++) {
p1 += (aVec[i] * bVec[i]);
p2 += (aVec[i] * aVec[i]);
p3 += (bVec[i] * bVec[i]);
}
p2 = Math.sqrt(p2);
p3 = Math.sqrt(p3);
// 結合公式計算
return (p1) / (p2 * p3);
}
}
回顧思考
思考:
- 如何對比?是一句一句進行對比,還是一段一段的進行對比?
- 對比的時候,如何才能說明對比的內容是重復的?也就是說判斷重復的標準是什么?
通過文本相似度演算法,我們可以得到兩個句子的相似度,那么相似度多少,我們才能認為它重復了呢?這個就由我們來決定了,在這里,當相似度達到60%以上,那么就認為當前句子是重復的,
現在,整體的查重邏輯應該是比較明了了:
我們可以拿到長文本,對長文本進行斷句,得到句子集合,將這個句子集合與資料庫中的資料(也進行斷句,得到句子集合)進行相似度計算,記錄相似度大于標準的句子,即記錄重復句子及重復句子的數量,這樣我們就能夠判斷,這長文本里面到底有多少個句子是重復的,進而得出重復率,
分析文本工具類
我們可以再封裝一下,寫一個分析文本工具類 AnalysisUtil
public class AnalysisUtil {
public static BigDecimal getAnalysisResult(List<Sentence> sentencesA, List<Sentence> sentencesB, SimDegreeAlgorithm algorithm) {
int simSentenceCnt = getSimSentenceCnt(sentencesA, sentencesB, algorithm);
BigDecimal analysisResult = null;
if (CollectionUtil.isNotEmpty(sentencesA)) {
analysisResult = BigDecimal.valueOf((double) simSentenceCnt / sentencesA.size()).setScale(4, BigDecimal.ROUND_HALF_UP);
} else {
analysisResult = new BigDecimal(0);
}
return analysisResult;
}
/**
* 回傳 A 在 B 中的相似句子數量,同時記錄相似句子的相似度及其所在位置(在進行處理的程序中,通過對 A 中資料進行相關操作實作),
* @param sentencesA 原始文本集合,即斷好的句子集合
* @param sentencesB 模式文本集合,即斷好的句子集合
* @param algorithm 相似度演算法
**/
public static int getSimSentenceCnt(List<Sentence> sentencesA, List<Sentence> sentencesB, SimDegreeAlgorithm algorithm) {
return null;
}
}
計算相似的句子數量
/**
* 回傳 A 在 B 中的相似句子數量,同時記錄相似句子的相似度及其所在位置(在進行處理的程序中,通過對 A 中資料進行相關操作實作),
* @param sentencesA 原始文本集合,即斷好的句子集合
* @param sentencesB 模式文本集合,即斷好的句子集合
* @param algorithm 相似度演算法
**/
public static int getSimSentenceCnt(List<Sentence> sentencesA, List<Sentence> sentencesB, SimDegreeAlgorithm algorithm) {
// 當前句子相似度
double simDegree = 0f;
// 相似的句子數量
int simSentenceCnt = 0;
// 計算相似度的策略
SimDegreeStrategy simDegreeStrategy = new SimDegreeStrategy(algorithm);
for (Sentence sentence1 : sentencesA) {
// 當前句子匹配到的最大的相似度
double maxSimDegree = 0f;
// 記錄 B 里的,與 A 中最大相似度的那個句子
Sentence temp = null;
for (Sentence sentence2 : sentencesB) {
// 計算相似度
simDegree = simDegreeStrategy.getSimDegree(sentence1.getText(), sentence2.getText());
// 列印資訊
printSim(sentence1, sentence2, simDegree, algorithm);
// 相似度大于60,認為文本重復
if (simDegree * 100 > 60) {
sentence1.setDuplicatesState(1);
// 記錄該句子在 B 中的位置
sentence1.getDuplicatesIndex().add(sentencesB.indexOf(sentence2));
}
// 記錄最大的相似度
if (simDegree * 100 > maxSimDegree) {
maxSimDegree = simDegree * 100;
temp = sentence2;
}
}
// 如果當前句子匹配到的最大相似度是大于60%的,那么說明該句子在 B 中至少有一個句子是相似的,即該句子是重復的
if (maxSimDegree > 60) {
++simSentenceCnt;
}
sentence1.setSimilar(maxSimDegree);
sentence1.setMaxSimilarSentence(temp);
}
return simSentenceCnt;
}
完整代碼
public class AnalysisUtil {
/**
* 計算出與專案庫內容重復的句子在當前內容下所占的比例
* @param sentencesA 待查重的句子集合
* @param sentencesB 專案庫中的專案內容句子集合
* @param algorithm 相似度演算法
* @return java.math.BigDecimal
**/
public static BigDecimal getAnalysisResult(List<Sentence> sentencesA, List<Sentence> sentencesB, SimDegreeAlgorithm algorithm) {
int simSentenceCnt = getSimSentenceCnt(sentencesA, sentencesB, algorithm);
BigDecimal analysisResult = null;
if (CollectionUtil.isNotEmpty(sentencesA)) {
analysisResult = BigDecimal.valueOf((double) simSentenceCnt / sentencesA.size()).setScale(4, BigDecimal.ROUND_HALF_UP);
} else {
analysisResult = new BigDecimal(0);
}
return analysisResult;
}
/**
* 根據相似度演算法,分析句子集合,回傳 A 在 B 中的相似句子數量,同時記錄相似句子的相似度及其所在位置(在進行處理的程序中,通過對 A 中資料進行相關操作實作),
* @param sentencesA 原始文本集合,即斷好的句子集合
* @param sentencesB 模式文本集合,即斷好的句子集合
* @param algorithm 相似度演算法
* @return int
**/
public static int getSimSentenceCnt(List<Sentence> sentencesA, List<Sentence> sentencesB, SimDegreeAlgorithm algorithm) {
// 當前句子相似度
double simDegree = 0f;
// 相似的句子數量
int simSentenceCnt = 0;
// 計算相似度的策略
SimDegreeStrategy simDegreeStrategy = new SimDegreeStrategy(algorithm);
for (Sentence sentence1 : sentencesA) {
// 當前句子匹配到的最大的相似度
double maxSimDegree = 0f;
// 記錄 B 里的,與 A 中最大相似度的那個句子
Sentence temp = null;
for (Sentence sentence2 : sentencesB) {
// 計算相似度
simDegree = simDegreeStrategy.getSimDegree(sentence1.getText(), sentence2.getText());
// 列印資訊
printSim(sentence1, sentence2, simDegree, algorithm);
// 相似度大于60,認為文本重復
if (simDegree * 100 > 60) {
sentence1.setDuplicatesState(1);
// 記錄該句子在 B 中的位置
sentence1.getDuplicatesIndex().add(sentencesB.indexOf(sentence2));
}
// 記錄最大的相似度
if (simDegree * 100 > maxSimDegree) {
maxSimDegree = simDegree * 100;
temp = sentence2;
}
}
// 如果當前句子匹配到的最大相似度是大于60的,那么說明該句子在 B 中至少有一個句子是相似的,即該句子是重復的
if (maxSimDegree > 60) {
++simSentenceCnt;
}
// 記錄句子的相似度以及與哪條相似
sentence1.setSimilar(maxSimDegree);
sentence1.setMaxSimilarSentence(temp);
}
return simSentenceCnt;
}
private static void printSim(Sentence sentence1, Sentence sentence2, double simDegree, SimDegreeAlgorithm algorithm) {
BigDecimal bigDecimal = new BigDecimal(simDegree);
DecimalFormat decimalFormat = new DecimalFormat("0.00%");
String format = decimalFormat.format(bigDecimal);
System.out.println("----------------------------------------------------------------");
System.out.println(algorithm.getClass().getSimpleName());
System.out.println("句子1:" + sentence1.getText());
System.out.println("句子2:" + sentence2.getText());
System.out.println("相似度:" + format);
}
}
測驗
測驗兩個句子,
public static void testLogic() {
String content = "你笑起來真好看,像春天的花一樣!";
String t = "你贊起來真好看,像夏天的陽光!";
List<Sentence> sentencesA = SentenceUtil.breakSentence(content);
List<Sentence> sentencesB = SentenceUtil.breakSentence(t);
BigDecimal analysisResult = AnalysisUtil.getAnalysisResult(sentencesA, sentencesB, new CosSim());
System.out.println("重復率:" + analysisResult);
}
輸出結果:
句子1:你笑起來真好看,像春天的花一樣!
句子2:你贊起來真好看,像夏天的陽光!
相似度:55.56%
重復率:0.0000
public static void testLogic() {
String content = "你笑起來真好看,像春天的花一樣!";
String t = "你笑起來真好看,像夏天的花一樣!";
List<Sentence> sentencesA = SentenceUtil.breakSentence(content);
List<Sentence> sentencesB = SentenceUtil.breakSentence(t);
BigDecimal analysisResult = AnalysisUtil.getAnalysisResult(sentencesA, sentencesB, new CosSim());
System.out.println("相似度:" + analysisResult);
}
輸出結果:
句子1:你笑起來真好看,像春天的花一樣!
句子2:你笑起來真好看,像夏天的花一樣!
相似度:88.89%
重復率:1.0000
總結
思路:將輸入的論文內容與論文資料庫中存在的論文內容進行一一對比,
思考:
- 如何對比?是一句一句進行對比,還是一段一段的進行對比?
- 對比的時候,如何才能說明對比的內容是重復的?也就是說判斷重復的標準是什么?
查重的基本思路就是,把待查重的內容進行短句,然后一條一條句子與資料庫中的進行對比,計算相似度,當然,這里的實作是比較簡單粗暴的,兩層 for 回圈,外層遍歷帶查重的句子,內層遍歷對比的句子,時間復雜度為 $O(n^2)$ ,
進一步的想法,就是使用多執行緒,這個后續再更新吧,
目前還沒想到還能如何進一步優化,如果螢屏前的你有什么寶貴的建議或者想法,非常歡迎留下你的評論~~~
最后的最后
由本人水平所限,難免有錯誤以及不足之處, 螢屏前的靚仔靚女們 如有發現,懇請指出!
最后,謝謝你看到這里,謝謝你認真對待我的努力,希望這篇博客對你有所幫助!
你輕輕地點了個贊,那將在我的心里世界增添一顆明亮而耀眼的星!
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/518827.html
標籤:Java
上一篇:day03-2-拓展
下一篇:js php 簡單聊天室
