NLP笔记:浅谈字符串之间的距离

0. 引言

故事起源于工作的一个实际问题,要分析两个文本序列间的相似性,然后就想着干脆把一些常见的字符串相似性内容一并整理一下好了。

于是就大概写了一下这篇文章,大致涵盖了我所知的全部字符串相似度比较的方法,大致包括:

  1. 汉明距离
  2. 最长公共子串
  3. 编辑距离
  4. jaccard距离
  5. bleu & rouge & ……
  6. ……

下面,我们来一个个考察一些这些内容。

1. 汉明距离

汉明距离(Hamming Distance)算是计算文本相似度的最简单的方式,他考察的是等长的字符串之间的距离,其具体定义就是两字符串之间不相同字符的个数

我们可以快速地给出hamming距离的计算函数如下:

def hamming_distance(s1, s2):
    return len([1 for c1, c2 in zip(s1, s2) if c1 == c2])

显然,hamming距离的算法复杂度就只有 O ( N ) O(N) O(N)而已。

2. 最长公共子序列

最长公共子序列(longest common subsequence)也是常用的一种用于评估两段文本间相似度的方法。故名思意,他就是求取两个字符串之间最长的共有子序列长度。

因此,显而易见的,较之汉明距离,他不受句长限制,允许两字符串不同长度,但是它受到顺序的影响,当两个句子意思大致相同但是有两个子串位置相反时,就会导致问题,比如不但...而且...这样的内容。

最长公共子串的求取算法同样是一个经典的动态规划算法问题,它的递推公式可以很轻松的表达为:

d p ( i , j ) = { m a x ( d p ( i + 1 , j ) , d p ( i , j + 1 ) ) if s1[i] != s2[j] d p ( i + 1 , j + 1 ) + 1 if s1[i] == s2[j] dp(i, j)= \begin{cases} max(dp(i+1, j), dp(i, j+1)) & \text{if s1[i] != s2[j]} \\ dp(i+1, j+1) + 1 & \text{if s1[i] == s2[j]} \end{cases} dp(i,j)={max(dp(i+1,j),dp(i,j+1))dp(i+1,j+1)+1if s1[i] != s2[j]if s1[i] == s2[j]

如此,我们就可以给出最长公共子序列长度计算的python代码实现脚本:

def lcs(s1, s2):
    l1 = len(s1)
    l2 = len(s2)
    
    dp = [[0 for _ in range(l2+1)] for _ in range(l1+1)]
    for i in range(l1-1, -1, -1):
        for j in range(l2-1, -1, -1):
            if s1[i] == s2[j]:
                dp[i][j] = dp[i+1][j+1] + 1
            else:
                dp[i][j] = max(dp[i][j+1], dp[i+1][j])
    return dp[0][0]

可以看到:lcs算法的算法复杂度为 O ( N 2 ) O(N^2) O(N2)

3. 编辑距离

最长公共子串虽然一定程度上可以衡量两个句子的相似性,但是他有一个缺点就是只关注了两者公共的部分,而并没有考虑两者不相同的部分,这就导致字符不同的部分无法在其中得到体现,比如aaaaba以及abacccccc两个字符串的lcs都是2,但是aba较之abacccccc显然更接近于原字符串aaa

而编辑距离(edit distance)则对这一点进行了优化,他的定义是:

  • 将字符串(s1)通过下述三种变换方式转换为另一个字符串(s2)所需要的最少操作次数:
    1. 插入
    2. 删除
    3. 替换

他的算法实现和最长公共子串的算法实现有一定的雷同,都是使用动态规划算法进行计算,他的递推公式可以表达为:

d p ( i , j ) = m i n ( 1 + d p ( i − 1 , j ) , 1 + d p ( i , j − 1 ) , d p ( i − 1 , j − 1 ) + ( 1 − δ ( s 1 [ i ] = = s 2 [ j ] ) ) ) dp(i, j) = min(1 + dp(i-1, j), 1+dp(i, j-1), dp(i-1, j-1) + (1-\delta(s_1[i] == s_2[j]))) dp(i,j)=min(1+dp(i1,j),1+dp(i,j1),dp(i1,j1)+(1δ(s1[i]==s2[j])))

其中,第一项表示增加一个字符,第二项表示减少一个字符,第三项表示替换一个字符。

给出相应的python脚本实现如下:

def edit_distance(s1, s2):
    n = len(s1)
    m = len(s2)
    
    dp = [[0 for _ in range(m+1)] for _ in range(n+1)]
    for i in range(n, -1, -1):
        for j in range(m, -1, -1):
            # print(dp)
            if i == n:
                dp[i][j] = m-j
            elif j == m:
                dp[i][j] = n-i
            else:
                d1 = 1 + dp[i+1][j]
                d2 = 1 + dp[i][j+1]
                d3 = dp[i+1][j+1] if s1[i] == s2[j] else 1 + dp[i+1][j+1]
                dp[i][j] = min(d1, d2, d3)
    return dp[0][0]

显然,编辑距离的算法复杂度也同样是 O ( N 2 ) O(N^2) O(N2)量级的。

4. jaccard距离

在大多数情况下,编辑距离事实上足够用于比较字符串之间的相似度了,但是,编辑距离还是存在一定的缺陷的,一个典型的例子就是它依赖于顺序,这就导致一些语义相同但是顺序不同的文本就会遭到误判,针对这样的数据,jaccard距离相对而言会是一个更好的判断方法,他是顺序无关的,只考虑两个字符串之间的token重合率。

给出jaccard距离的定义如下:
j a c c a r d ( s 1 , s 2 ) = s 1 ∩ s 2 s 1 ∪ s 2 jaccard(s_1, s_2) = \frac{s_1 \cap s_2}{s_1 \cup s_2} jaccard(s1,s2)=s1s2s1s2

我们给出字符层级下的jaccard距离计算脚本如下:

def jaccard(s1, s2):
    return len(set(s1) & set(s2)) / len(set(s1) | set(s2))

显然,jaccard距离的算法复杂度就只有 O ( N ) O(N) O(N)而已。

不过,完全不考虑顺序关系会是一个双刃剑,尤其对于文本内容时,因为他的顺序是有意义的。

5. bleu & rouge & ……

当然,比较两个字符串之间的相似度也可以使用bleu以及rouge等指标,虽然会有点怪异就是了,因为bleu以及rouge指标的计算是不满足交换律的, s 1 s_1 s1 s 2 s_2 s2不对易,因此不算是单纯的字符串相似度的比较。

但是,如果明确就是问 s 1 s_1 s1 s 2 s_2 s2之间的距离的话,那么bleu、rouge等指标也可以用于评估两个字符串之间的距离。

有关bleu、rouge等指标的计算具体可以参考我之前的博客:NLP笔记:生成问题常用metrics整理,这里就不多做展开了。

6. 总结

综上,我们可以整理出字符串相似度比较的一些常用方法如下:

method定义算法复杂度特点
hamming distance两等长字符串中不同字符的个数 O ( N ) O(N) O(N)简单,但是表达能力有限
lcs两字符串的最长公共子序列 O ( N 2 ) O(N^2) O(N2)对顺序敏感,且无法表达不相同字符的区别程度
edit distance将s1变换为s2所需要的最小编辑数目 O ( N 2 ) O(N^2) O(N2)相对最为常用的一种字符串相似度衡量方法,同样对顺序敏感
jaccard s 1 ∩ s 2 / s 1 ∪ s 2 s_1 \cap s_2 / s_1 \cup s_2 s1s2/s1s2 O ( N ) O(N) O(N)不考虑顺序信息,只考虑字符重复比例
  • 4
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值