文本相似度计算——Dice Similarity Coefficient(Dice相似系数)+ 最长公共子序列(LCS)

最长公共子序列是一个典型的动态规划问题,关于动态规划的相关知识可以查看动态规划(Dynamic programming)详解_Gent_倪的博客-CSDN博客

一、Dice相似系数

  • Dice相似系数(Dice Similarity Coefficient, DSC) 是一种集合相似度度量指标,通常用来计算两个样本的相似度。公式为:2 * |X ∩ Y| / (|X| + |Y|),其中 X Y 是两个集合,|X| 表示集合 X 中的元素个数,∩表示两个集合的交集,即两个集合中共有的元素。

        

         而DSC运用在文本相似度上时,不能简单的直接计算文本A和文本B的公共交集,比如牛奶和奶牛,如果单纯计算交集的话,DSC=2*2/(2+2)=1,即完全一致,这明显是错误的。因为语言是有顺序的,因此计算时需要取最长公共子序列来作为文本的交集。

二、最长公共子序列的相关概念

  • 子序列(subsequence):一个特定序列的子序列就是将给定序列中零个或多个元素去掉后得到的结果(不改变元素间相对次序)。比如序列<A,B,C,D,E>的子序列有:<A,B>、<A,C,E>、<A,D,E>等。而<E,D>不是子序列,因为元素间次序发生改变。
  • 子串(substring):序列中任意个连续的字符组成的子序列称为该序列的子串。注意:子序列可不连续,而子串必须是连续的。比如ABCDE的子串有AB、BC,而ACE不是子串,因为不连续。
  • 公共子序列(common subsequence):给定序列X和Y,序列Z是X的子序列,也是Y的子序列,则Z是X和Y的公共子序列。例如X=<A,B,C,B,D,A,B>,Y=<B,D,C,A,B,A>,那么序列Z=<B,C,A>为X和Y的公共子序列,其长度为3。但Z不是X和Y的最长公共子序列,而序列<B,C,B,A>和<B,D,A,B>也均为X和Y的最长公共子序列,长度为4,而X和Y不存在长度大于等于5的公共子序列。
  • 公共子串(common substring):给定序列X和Y,序列Z是X的子串,也是Y的子串,则Z是X和Y的公共子串。
  • 最长公共子序列(Longest Common Subsequence,简称LCS):两个序列X和Y的公共子序列中,长度最长的那个,定义为X和Y的最长公共子序列。
  • 最长公共子串(Longest common substring):同样是将X和Y视为由单个词组成的有序词串,在此基础上计算两个词串之间最大公共子串的长度,与最长公共子序列不同,该子串必须在原词串中连续。

三、动态规划求解最长公共子序列

最长公共子序列问题: 给定一个长度为n的序列A和一个长度为m的序列B(n,m<=5000),求出一个最长的序列,使得该序列既是A的子序列,也是B的子序列。

举例,序列A={A,B,C,B,D,A,B},序列B={B,D,C,A,B,A}。根据动态规划的基本思路:

1. 划分子问题并确定状态

        首先将原问题分割成一些子问题,用dp[i][j]表示只考虑A的前 i 个元素,B的前 j 个元素时的最长公共子序列的长度,求这时的最长公共子序列的长度就是子问题。dp[i][j]就是我们所说的状态,dp[n,m]则是最终要达到的状态,即为所求结果。

  • Ai 代表序列A的前i个字符组成的序列,Bj 代表序列B的前j个字符组成的序列。
  • A[x] 代表序列A的第x个字符,i>0;B[y] 代表序列B的第y个字符,j>0。
  • dp[i][j] 代表序列Ai与序列Bj的最长公共子序列的长度

2. 确定状态转移方程

        若序列Ai,Bj的最后一个元素相同时,则最后一个元素一定在最长公共子序列中。此时,序列Ai与序列Bj的最长公共子序列的长度就等于序列A_{i-1}与序列B_{j-1}的最长公共子序列的长度,再加上最后一个相同元素,用公式表达为:dp[i][j]=dp[i-1][j-1]+1

        而当最后一个元素不同时,这时有三种情况:

  1. Ai与Bj的最后一个元素都不在最长公共子序列中:那么此时序列Ai与序列Bj的最长公共子序列的长度就等于序列A_{i-1}与序列B_{j-1}的最长公共子序列的长度,因为最后一个元素都不在最长公共子序列中,因此直接舍弃。用公式表达为:dp[i][j]=dp[i-1][j-1]
  2. Ai的最后一个元素在最长公共子序列中,Bj不在:那么此时序列Ai与序列Bj的最长公共子序列的长度就等于序列Ai与序列B_{j-1}的最长公共子序列的长度,因为序列Bj的最后一个元素不在最长公共子序列中,因此舍弃。用公式表达为:dp[i][j]=dp[i][j-1]
  3. Bj的最后一个元素在最长公共子序列中,Ai不在: 那么此时序列Ai与序列Bj的最长公共子序列的长度就等于序列A_{i-1}与序列Bj的最长公共子序列的长度,因为序列Ai的最后一个元素不在最长公共子序列中,因此舍弃。用公式表达为:dp[i][j]=dp[i-1][j]

        此时,当Ai与Bj最后一个元素不同时的最长公共子序列的长度就等价于上面三种情况下的最长公共子序列的长度的最大值,又因为dp[i-1][j-1]一定比dp[i][j-1]和dp[i-1][j]都要小,所以取max的时候可以不考虑dp[i-1][j-1]。用公式表达为:dp[i][j]=max(dp[i][j-1], dp[i-1][j])

扫描二维码关注公众号,回复: 15833000 查看本文章

        综上,最长公共子序列的状态转移方式为:

        在这里插入图片描述

3. 确定开始以及边界条件

如果序列A或者序列B中,任意一个序列的长度为0,那么此时最长公共子序列的长度肯定为0.用公式表达为:dp[0][j] = 0;dp[i][0] = 0

4. 动态规划代码

求导出状态规划转移方程式后,我们就可以开始编写代码了。

// 最长公共子序列LCS
public static int lcs(String str1, String str2) {
        int len1 = str1.length();
        int len2 = str2.length();
        int c[][] = new int[len1+1][len2+1];
        for (int i = 0; i <= len1; i++) {
            for( int j = 0; j <= len2; j++) {
                if(i == 0 || j == 0) {
                    c[i][j] = 0;
                } else if (str1.charAt(i-1) == str2.charAt(j-1)) {
                    c[i][j] = c[i-1][j-1] + 1;
                } else {
                    c[i][j] = Math.max(c[i - 1][j], c[i][j - 1]);
                }
            }
        }
        return c[len1][len2];
    }

测试

    @Test
    public void testLCS() {
        String s1 = "ABCBDAB";
        String s2 = "BDCABA";
        System.out.println(lcs(s1, s2));
    }

 此时输出最长公共子序列的长度为4,结果正确。

如果我们想输出最长公共子序列的值,此时就需要用到回溯算法,相关链接:

小白带你学---回溯算法(Back Tracking) - 知乎

回溯算法套路详解 - 知乎

彻底搞懂回溯算法(本文真的很详细)_阿华的保温杯的博客-CSDN博客_回溯算法问题

算法:最长公共子序列(输出所有最长公共子序列)_顾道长生'的博客-CSDN博客_输出最长公共子序列

四、DSC完整代码

第三步计算出最长公共子序列后,我们来计算Dice相似度就十分简单了。完整代码如下:

    // 最长公共子序列LCS
    public static int lcs(String str1, String str2) {
        int len1 = str1.length();
        int len2 = str2.length();
        int c[][] = new int[len1+1][len2+1];
        for (int i = 0; i <= len1; i++) {
            for( int j = 0; j <= len2; j++) {
                if(i == 0 || j == 0) {
                    c[i][j] = 0;
                } else if (str1.charAt(i-1) == str2.charAt(j-1)) {
                    c[i][j] = c[i-1][j-1] + 1;
                } else {
                    c[i][j] = Math.max(c[i - 1][j], c[i][j - 1]);
                }
            }
        }
        return c[len1][len2];
    }

    // Dice 相似度算法
    public static double diceSimilarity(String str1, String str2) {
        return (double)(2*lcs(str1, str2)) / (double)(str1.length()+str2.length());
    }

测试:

    @Test
    public void testDiceSimilarity() {
        String s1 = "ABCBDAB";
        String s2 = "BDCABA";
        System.out.println("ABCBDAB 与 BDCABA 的相似度为:" + diceSimilarity(s1, s2));
    }

五、其他

1. 可参考 SourceForge 的 LCS 交互网页 来更好地理解 LCS 的实现过程。

2. Python官方的ratio方法用的也是Dice相似度算法,但是底层功能更强大,感兴趣的可以看下:

difflib — Helpers for computing deltas — Python 3.10.7 documentation

cpython/difflib.py at a0d0a77c1f78fee581294283a66bf198d69f2699 · python/cpython · GitHub

3. 其他文本相似度算法:

文本相似度算法 - 知乎

深度学习-5.短文本相似度计算 - 知乎

猜你喜欢

转载自blog.csdn.net/qq_37771475/article/details/126852205