NLP点滴——文本相似度

前言

在自然语言处理过程中,经常会涉及到如何度量两个文本之间的相似性,我们都知道文本是一种高维的语义空间,如何对其进行抽象分解,从而能够站在数学角度去量化其相似性。而有了文本之间相似性的度量方式,我们便可以利用划分法的K-means、基于密度的DBSCAN或者是基于模型的概率方法进行文本之间的聚类分析;另一方面,我们也可以利用文本之间的相似性对大规模语料进行去重预处理,或者找寻某一实体名称的相关名称(模糊匹配)。而衡量两个字符串的相似性有很多种方法,如最直接的利用hashcode,以及经典的主题模型或者利用词向量将文本抽象为向量表示,再通过特征向量之间的欧式距离或者皮尔森距离进行度量。本文围绕文本相似性度量的主题,从最直接的字面距离的度量到语义主题层面的度量进行整理总结,并将平时项目中用到的文本相似性代码进行了整理,如有任何纰漏还请指出,我会第一时间改正^v^。(ps.平时用的Java和scala较多,本文主要以Java为例。)

字面距离

提到如何比较两个字符串,我们从最初编程开始就知道:字符串有字符构成,只要比较比较两个字符串中每一个字符是否相等便知道两个字符串是否相等,或者更简单一点将每一个字符串通过哈希函数映射为一个哈希值,然后进行比较。但是这种方法有一个很明显的缺点,就是过于“硬”,对于相似性的度量其只有两种,0不相似,1相似,哪怕两个字符串只有一个字符不相等也是不相似,这在NLP的很多情况是无法使用的,所以下文我们就“软”的相似性的度量进行整理,而这些方法仅仅考虑了两个文本的字面距离,无法考虑到文本内在的语义内容。

common lang库

文中在部分代码应用中使用了Apache提供的common lang库,该库包含很多Java标准库中没有的但却很实用的函数。其maven引用如下:

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.4</version>
</dependency>

相同字符数

在传统的字符串比较过程中,我们考虑字符串中每个字符是否相等,并且考虑了字符出现的顺序,如果不考虑字符出现的顺序,我们可以利用两个文本之间相同的字符数量,很简单不再赘述,可以利用common lang中的getFuzzyDistance:

int dis = StringUtils.getFuzzyDistance(term, query, Locale.CHINA);

莱文斯坦距离(编辑距离)

定义

我们在学习动态规划的时候,一个很经典的算法便是计算两个字符串的编辑距离,即:

莱文斯坦距离,又称Levenshtein距离,是编辑距离(edit distance)的一种。指两个字串之间,由一个转成另一个所需的最少编辑操作次数。许可的编辑操作包括将一个字符替换成另一个字符,插入一个字符,删除一个字符。

例如将kitten一字转成sitting:

  1. sitten (k→s)
  2. sittin (e→i)
  3. sitting (→g)

那么二者的编辑距离为3。
俄罗斯科学家弗拉基米尔·莱文斯坦在1965年提出这个概念。

实现方式

我们可以利用common lang中StringUtils的函数来计算:

int dis = StringUtils.getLevenshteinDistance(s1, s2);
//实现
public static int getLevenshteinDistance(CharSequence s, CharSequence t) {
    if (s == null || t == null) {
        throw new IllegalArgumentException("Strings must not be null");
    }
    int n = s.length(); // length of s
    int m = t.length(); // length of t
    if (n == 0) {
        return m;
    } else if (m == 0) {
        return n;
    }
    if (n > m) {
        // swap the input strings to consume less memory
        final CharSequence tmp = s;
        s = t;
        t = tmp;
        n = m;
        m = t.length();
    }
    int p[] = new int[n + 1]; //'previous' cost array, horizontally
    int d[] = new int[n + 1]; // cost array, horizontally
    int _d[]; //placeholder to assist in swapping p and d
    // indexes into strings s and t
    int i; // iterates through s
    int j; // iterates through t
    char t_j; // jth character of t
    int cost; // cost
    for (i = 0; i <= n; i++) {
        p[i] = i;
    }
    for (j = 1; j <= m; j++) {
        t_j = t.charAt(j - 1);
        d[0] = j;
        for (i = 1; i <= n; i++) {
            cost = s.charAt(i - 1) == t_j ? 0 : 1;
            // minimum of cell to the left+1, to the top+1, diagonally left and up +cost
            d[i] = Math.min(Math.min(d[i - 1] + 1, p[i] + 1), p[i - 1] + cost);
        }
        // copy current distance counts to 'previous row' distance counts
        _d = p;
        p = d;
        d = _d;
    }
    // our last action in the above loop was to switch d and p, so p now
    // actually has the most recent cost counts
    return p[n];
}

Jaro距离

定义

Jaro Distance也是字符串相似性的一种度量方式,也是一种编辑距离,Jaro 距离越高本文相似性越高;而Jaro–Winkler distance是Jaro Distance的一个变种。据说是用来判定健康记录上两个名字是否相同,也有说是是用于人口普查。从最初其应用我们便可看出其用法和用途,其定义如下:

524764-20161205182932226-713124246.png

其中

  • 524764-20161205183008366-1009220282.png是匹配数目(保证顺序相同)
  • 524764-20161205183051147-1858966058.png字符串长度
  • 524764-20161205183158944-1536554248.png是换位数目

其中t换位数目表示:两个分别来自S1和S2的字符如果相距不超过

524764-20161205183225147-1461918083.png

我们就认为这两个字符串是匹配的;而这些相互匹配的字符则决定了换位的数目t,简单来说就是不同顺序的匹配字符的数目的一半即为换位的数目t,举例来说,MARTHA与MARHTA的字符都是匹配的,但是这些匹配的字符中,T和H要换位才能把MARTHA变为MARHTA,那么T和H就是不同的顺序的匹配字符,t=2/2=1。
而Jaro-Winkler则给予了起始部分就相同的字符串更高的分数,他定义了一个前缀p,给予两个字符串,如果前缀部分有长度为 的部分相同,则Jaro-Winkler Distance为:

524764-20161205222451647-1104025452.png

  • 524764-20161205183334913-1414541740.png是两个字符串的Jaro Distance
  • 524764-20161205222533382-2030502781.png是前缀的相同的长度,但是规定最大为4
  • 524764-20161205183507929-2111705481.png则是调整分数的常数,规定不能超过0.25,不然可能出现dw大于1的情况,Winkler将这个常数定义为0.1

举个简单的例子:
计算s_1=DIXON,s_2=DICKSONX的距离

524764-20161116224456326-154405081.png

我们利用\lfloor \frac{max(|s_1|,|s_2|)}{2}-1 \rfloor可以得到一个匹配窗口距离为3,图中黄色部分便是匹配窗口,其中1表示一个匹配,我们发现两个X并没有匹配,因为其超出了匹配窗口的距离3。我们可以得到:

  • 524764-20161205184330741-1993007662.png
  • 524764-20161205221731241-920112646.png
  • 524764-20161205221836147-1089771301.png
  • 524764-20161205184549444-213670759.png

其Jaro score为:

d_j=\frac{1}{3}(\frac{4}{5}+\frac{4}{8}+\frac{4-0}{4})=0.767

而计算Jaro–Winkler score,我们使用标准权重p=0.1,\ell=2,其结果如下:

  • 4
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值