前言
在之前的一篇漂亮国的全球的基地博客中,我们曾经对漂亮国的全球基地进行了一些梳理。博文中使用的数据来源,重点是参考以为博主分享的KML的数据,同时针对其国内的基地部署信息,我们从互联网百科的数据中搜寻到一些。其实拿到这两份数据的时候,是存在一些问题的,比如,KML的数据它的基地名称都是英文的,而在互联网上公开的数据中,良莠不齐,有的是中文的,有的是英文,有的中英文都有,而且可能还存在同样的英文,它的描述不一样的情况,比如下面的场景:
XXX Air Base 空军基地
XXX Air Station 航空站
可能不同的信息来源,其主要的名称对应的真实位置表示的是同一个。再比如漂亮国驻小日子的基地列表。
Idesuna Jima Air Range
FAC 6078 出砂岛炸射练习场 Idesuna Jima Range 训练 冲绳渡名喜村
上面一行是从KML中解析出来的基地名称,而下面是从某百科中得到的数据。 当我们将句子的影响词(当出现在军事基地的领域中的某些特定词,如Air、Base、Station、Range等这些词去掉后)其主要的地点就是同一个,通过对比主要关键词的相似性,为我们实现将两个地名进行对齐,甚至可以设定一个阈值,当两个字符串的相似性超过多少(比如超过90%)就认为这是同一个地点,从而可以实现将基地的中文名和所驻地的国家、城市等信息进行一一匹配。从而减少人工对比的工作。
随着现代技术的快速发展,文本相似度计算有很多种方法,甚至可以用大数据、自然语言处理还可以结合人工智能的方法来实现精确的文本相似性判断。但是本文不想实现那么复杂的架构,只想简单快速的解决两个地址的快速匹配问题。博客以Java编程为例,讲解了在Java中求解两个字符串的几种方法。通过求解编辑距离、Q-gram Matching、还有余弦相似性计算,通过对比不同的方法,调用Apache 的Common-text中基于余弦的字符相似性得到了比较比错的结果。算法比较简单,主要想说明两个字符串的不同距离的计算,对于更复杂的模型,大家感兴趣的可以搜索相关的科研论文来学习。
一、字符串距离的几种计算方法
为了比较字符的相似性,我们通过通过对比距离(把两个字符串的距离计算出来,当然这个距离的计算是有讲究的,具体的算法就是我们说的距离算法,不同的距离算法,得到的值是不一样的)。因此这里我们打算讲将在Java当中,有哪些距离的计算方法以及如何编写相应的代码。
1、Levenshtein 距离
计算将一个字符串转换为另一个字符串所需的最少单字符编辑操作(插入、删除或替换)的数量。在下面这个示例中,levenshteinDistance方法使用动态规划来计算两个字符串之间的Levenshtein Distance。calculateNormalizedLevenshteinDistance方法则使用这个距离和两个字符串长度的最大值来计算标准化的Levenshtein Distance。 main方法中给出了两个示例字符串s1和s2,并调用calculateNormalizedLevenshteinDistance方法来计算并打印它们的Normalized Levenshtein Distance。根据这个得分,你可以设定一个阈值来判断两个字符串是否相似。例如,如果Normalized Levenshtein Distance小于0.2(即80%的字符是匹配的),你可以认为这两个字符串是相似的。这个阈值可以根据你的具体需求进行调整。
package com.yelang.project;
public class NormalizedLevenshteinDistance {
// 计算Levenshtein Distance
public static int levenshteinDistance(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);
}
}
return dp[s1.length()][s2.length()];
}
// 计算Normalized Levenshtein Distance
public static double calculateNormalizedLevenshteinDistance(String s1, String s2) {
int distance = levenshteinDistance(s1, s2);
int maxLength = Math.max(s1.length(), s2.length());
return (double) distance / maxLength;
}
public static void main(String[] args) {
String s1 = "Camp Humphreys";
String s2 = "United States Army Garrison-Humphreys";
double normalizedDistance = calculateNormalizedLevenshteinDistance(s1, s2);
System.out.println("Normalized Levenshtein Distance: " + normalizedDistance);
}
}
运行上述程序后,得到的结果如下,表示两个字符串的相似度是0.67:
Normalized Levenshtein Distance: 0.6756756756756757
2、Overlap Coefficient计算
Overlap Coefficient是通过计算两个字符串的最长公共子序列的长度来求两者的相似度的。这种算法有弊有利,在主要的关键词不同而基地的信息完一致的情况下,比如xxx air base 和xx1 air base,通过计算得到的结果一定及其相似,因为这两个字符串的最长公共序列很长啊,计算机看起来就像是完全一致的。
package com.yelang.project;
public class OverlapCoefficient {
// 计算最长公共子序列的长度
public static int longestCommonSubsequence(String s1, String s2) {
int len1 = s1.length();
int len2 = s2.length();
int[][] dp = new int[len1 + 1][len2 + 1];
for (int i = 1; i <= len1; i++) {
for (int j = 1; j <= len2; j++) {
if (s1.charAt(i - 1) == s2.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
return dp[len1][len2];
}
// 计算Overlap Coefficient
public static double calculateOverlapCoefficient(String s1, String s2) {
int lcsLength = longestCommonSubsequence(s1, s2);
int minLength = Math.min(s1.length(), s2.length());
return (double) lcsLength / minLength;
}
public static void main(String[] args) {
String s1 = "Camp Humphreys";
String s2 = "United States Army Garrison-Humphreys";
double overlapCoefficient = calculateOverlapCoefficient(s1, s2);
System.out.println("Overlap Coefficient: " + overlapCoefficient);
}
}
运行以上程序后,得到的结果是:
Overlap Coefficient: 0.8571428571428571
它判定是85%的相似度,貌似看起来比第一种距离很接近真相,你换个词去测试就发现,这种算法其实很蠢。
3、Q-gram Matching
Q-gram Matching是一种基于子串的字符串相似度计算方法,其中Q表示子串的长度。对于两个字符串,计算它们共有的Q-gram(长度为Q的连续子串)的数量,然后将这个数量除以两个字符串中Q-gram数量较少的那个, 得到相似度的比例。下面给出它的Java实现:
package com.yelang.project;
import java.util.HashSet;
import java.util.Set;
public class QGramMatching {
/**
* 在这个示例中,calculateQGrams方法接受一个字符串和一个整数Q,然后返回该字符串所有可能的Q-gram的集合。calculateQGramSimilarity方法接受两个字符串和一个整数Q,计算它们的Q-gram集合,并找出共有的Q-gram数量,然后计算相似度。main方法中给出了两个示例字符串s1和s2,以及Q-gram的长度Q,并调用calculateQGramSimilarity方法来计算并打印它们的Q-gram相似度。
请注意,Q-gram的长度Q是一个参数,可以根据具体应用场景进行调整。较短的Q值可以捕捉到更细粒度的相似性,而较长的Q值则可以捕捉到更宽泛的相似性。此外,相似度的阈值可以根据实际需求设定,以判断两个字符串是否相似。
*/
// 计算字符串的Q-gram集合
public static Set<String> calculateQGrams(String s, int q) {
Set<String> qGrams = new HashSet<>();
for (int i = 0; i <= s.length() - q; i++) {
qGrams.add(s.substring(i, i + q));
}
return qGrams;
}
// 计算两个字符串的Q-gram相似度
public static double calculateQGramSimilarity(String s1, String s2, int q) {
Set<String> qGrams1 = calculateQGrams(s1, q);
Set<String> qGrams2 = calculateQGrams(s2, q);
// 计算两个集合的交集
Set<String> intersection = new HashSet<>(qGrams1);
intersection.retainAll(qGrams2);
// 计算相似度
double similarity = intersection.size() / (double) Math.min(qGrams1.size(), qGrams2.size());
return similarity;
}
public static void main(String[] args) {
String s1 = "Camp Humphreys";
String s2 = "United States Army Garrison-Humphreys";
// Misawa Air Range<==>Misawa Air Base:similar = 0.9058216273156766
// Gun Training Area<==>Tsuken Jima Training Area:similar = 0.9202830114233174
int q = 3; // Q-gram的长度
double qGramSimilarity = calculateQGramSimilarity(s1, s2, q);
System.out.println("Q-gram Similarity (Q=" + q + "): " + qGramSimilarity);
}
}
运行以上的程序,得到以下的执行结果:
Q-gram Similarity (Q=3): 0.5833333333333334
4、余弦相似性计算
使用词嵌入(如word2vec或BERT)将单词转换为向量,并计算向量之间的余弦相似度。这里基于Apache 的common-text来进行余弦计算,看结果如何,首先在Pom.xml中引入以下资源。
<!-- 求解字符相似性 -->
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-text -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-text</artifactId>
<version>1.10.0</version>
</dependency>
然后在Java中实现以下方法,请注意,这里是一种比较简单的方法,更精准的实现还要结合实际场景进行优化。