编辑距离(Edit Distance),又称Levenshtein距离,是指两个字串之间,由一个转成另一个所需的最少编辑操作次数。许可的编辑操作包括将一个字符替换成另一个字符,插入一个字符,删除一个字符。
俄罗斯科学家Vladimir Levenshtein在1965年提出这个概念。
- sitten (k→s)
- sittin (e→i)
- sitting (→g)
最小编辑距离通常作为一种相似度计算函数被用于多种实际应用中,详细如下: (特别的,对于中文自然语言处理,一般以词为基本处理单元)
- DNA分析:基因学的一个主要主题就是比较 DNA 序列并尝试找出两个序列的公共部分。如果两个 DNA 序列有类似的公共子序列,那么这些两个序列很可能是同源的。在比对两个序列时,不仅要考虑完全匹配的字符,还要考虑一个序列中的空格或间隙(或者,相反地,要考虑另一个序列中的插入部分)和不匹配,这两个方面都可能意味着突变(mutation)。在序列比对中,需要找到最优的比对(最优比对大致是指要将匹配的数量最大化,将空格和不匹配的数量最小化)。如果要更正式些,可以确定一个分数,为匹配的字符添加分数、为空格和不匹配的字符减去分数。
全局序列比对尝试找到两个完整的序列 S1和 S2之间的最佳比对。以下面两个 DNA 序列为例:
S1= GCCCTAGCG
S2= GCGCAATG
如果为每个匹配字符一分,一个空格扣两分,一个不匹配字符扣一分,那么下面的比对就是全局最优比对:
S1'= GCCCTAGCG
S2'= GCGC-AATG
连字符(-)代表空格。在 S2'中有五个匹配字符,一个空格(或者反过来说,在 S1'中有一个插入项),有三个不匹配字符。这样得到的分数是 (5 * 1) + (1 * -2) + (3 * -1) = 0,这是能够实现的最佳结果。
使用局部序列比对,不必对两个完整的序列进行比对,可以在每个序列中使用某些部分来获得最大得分。使用同样的序列 S1和 S2,以及同样的得分方案,可以得到以
下局部最优比对 S1''和 S2'':
S1 = GCCCTAGCG
S1''= GCG
S2''= GCG
S2 = GCGCAATG
这个局部比对的得分是 (3 * 1) + (0 * -2) + (0 * -1) = 3。
拼写纠错(Spell Correction):又拼写检查(Spell Checker),将每个词与词典中的词条比较,英文单词往往需要做词干提取等规范化处理,如果一个词在词典中不存在,就被认为是一个错误,然后试图提示N个最可能要输入的词——拼写建议。常用的提示单词的算法就是列出词典中与原词具有最小编辑距离的词条。
这里肯定有人有疑问:对每个不在词典中的词(假如长度为len)都与词典中的词条计算最小编辑距离,时间复杂度是不是太高了?的确,所以一般需要加一些剪枝策略,如:
- 因为一般拼写检查应用只需要给出Top-N的纠正建议即可(N一般取10),那么我们可以从词典中按照长度依次为len、len-1、len+1、len-2、len-3、...的词条比较;
- 限定拼写建议词条与当前词条的最小编辑距离不能大于某个阈值;
- 如果最小编辑距离为1的候选词条超过N后,终止处理;
- 缓存常见的拼写错误和建议,提高性能。
命名实体抽取(Named Entity Extraction):由于实体的命名往往没有规律,如品牌名,且可能存在多种变形、拼写形式,如“IBM”和“IBM Inc.”,这样导致基于词典完全匹配的命名实体识别方法召回率较低,为此,我们可以使用编辑距离由完全匹配泛化到模糊匹配,先抽取实体名候选词。
具体的,可以将候选文本串与词典中的每个实体名进行编辑距离计算,当发现文本中的某一字符串的编辑距离值小于给定阈值时,将其作为实体名候选词;获取实体名候选词后,根据所处上下文使用启发式规则或者分类的方法判定候选词是否的确为实体名。
实体共指(Entity Coreference):通过计算任意两个实体名之间的最小编辑距离判定是否存在共指关系?如“IBM”和“IBM Inc.”, "Stanford President John Hennessy "和"Stanford University President John Hennessy"。
机器翻译(Machine Translation):
识别平行网页对:由于平行网页通常有相同或类似的界面结构,因此平行网页在HTML结构上应该有很大近似度。首先将网页的HTML标签抽取出来,连接成一个字符串,然后用最小编辑距离考察两个字符串的近似度。实际中,此策略一般与文档长度比例、句对齐翻译模型等方法结合使用,以识别最终的平行网页对。
自动评测:首先存储机器翻译原文和不同质量级别的多个参考译文,评测时把自动翻译的译文对应到与其编辑距离最小的参考译文上,间接估算自动译文的质量,如下图所示:
自动评测:首先存储机器翻译原文和不同质量级别的多个参考译文,评测时把自动翻译的译文对应到与其编辑距离最小的参考译文上,间接估算自动译文的质量,如下图所示:
字符串核函数(String Kernel):最小编辑距离作为字符串之间的相似度计算函数,用作核函数,集成在SVM中使用。
自然语言表达(来着百度百科)
比如要计算cafe和coffee的编辑距离。cafe>caffe>coffe>coffee
先创建一个6×8的表(cafe长度为4,coffee长度为6,各加2)(1):
c | o | f | f | e | e | ||
c | |||||||
a | |||||||
f | |||||||
e | 表 | 1 |
接着,在如下位置填入数字(表2):
c | o | f | f | e | e | ||
0 | 1 | 2 | 3 | 4 | 5 | 6 | |
c | 1 | ||||||
a | 2 | ||||||
f | 3 | ||||||
e | 4 | 表 | 2 |
从3,3格开始,开始计算。取以下三个值的最小值:
-
如果最上方的字符等于最左方的字符,则为左上方的数字。否则为左上方的数字+1。(对于3,3来说为0)
-
左方数字+1(对于3,3格来说为2)
-
上方数字+1(对于3,3格来说为2)
因此为格3,3为0(表3)
c | o | f | f | e | e | ||
0 | 1 | 2 | 3 | 4 | 5 | 6 | |
c | 1 | 0 | |||||
a | 2 | ||||||
f | 3 | ||||||
e | 4 | 表 | 3 |
同理,推出下表
c | o | f | f | e | e | ||
0 | 1 | 2 | 3 | 4 | 5 | 6 | |
c | 1 | 0 | 1 | 2 | 3 | 4 | 5 |
a | 2 | 1 | 1 | 2 | 3 | 4 | 5 |
f | 3 | 2 | 2 | 1 | 2 | 3 | 4 |
e | 4 | 3 | 3 | 2 | 2 | 2 | 3 |
取右下角,得编辑距离为3
下面是 JAVA 语言的实现:
public final class LevenshteinDistance {
private static final int minimum(int a, int b, int c) {
int mi;
mi = a;
if (b < mi) {
mi = b;
}
if (c < mi) {
mi = c;
}
return mi;
}
/**
* Measure the similar of two String.
*
* @param s
* @param t
* @return float value from 0 to 1, 0 : totally different. 1: identical
* string
*/
public static float similar(String s, String t) {
int distance = distance(s, t);
int longer = Math.max(s.length(), t.length());
int same = longer - distance;
return (float) same / longer;
}
/**
*
* @param s
* @param t
* @return the distance of two string, 0 :identical string,
*/
public static int distance(String s, String t) {
int d[][]; // matrix
int n; // length of s
int m; // length of t
int i; // iterates through s
int j; // iterates through t
char s_i; // ith character of s
char t_j; // jth character of t
int cost; // cost
// Step 1
n = s.length();
m = t.length();
if (n == 0) {
return m;
}
if (m == 0) {
return n;
}
d = new int[n + 1][m + 1];
// Step 2
for (i = 0; i <= n; i++) {
d[i][0] = i;
}
for (j = 0; j <= m; j++) {
d[0][j] = j;
}
// Step 3
for (i = 1; i <= n; i++) {
s_i = s.charAt(i - 1);
// Step 4
for (j = 1; j <= m; j++) {
t_j = t.charAt(j - 1);
// Step 5
if (s_i == t_j) {
cost = 0;
} else {
cost = 1;
}
// Step 6
d[i][j] = minimum(d[i - 1][j] + 1, d[i][j - 1] + 1,
d[i - 1][j - 1] + cost);
}
}
// Step 7
return d[n][m];
}
}