用于字符串相似性比较的N-gram分割函数

时间:2010-05-25 13:32:34

标签: f# n-gram

作为更好地理解我目前正在学习的F#的一部分,我写了函数 将给定的字符串分成n-gram。
1)我想收到有关我的功能的反馈:这可以更简单或更有效地编写吗?

2)我的总体目标是编写基于n-gram相似性返回字符串相似度(在0.0 ... 1.0范围内)的函数;这种方法是否适用于短字符串比较,或者这种方法可以可靠地用于比较大字符串(例如文章)。

3)我知道n-gram比较忽略了两个字符串的上下文。你建议用什么方法来实现我的目标?

//s:string - target string to split into n-grams
//n:int - n-gram size to split string into
let ngram_split (s:string, n:int) =
    let ngram_count = s.Length - (s.Length % n)
    let ngram_list = List.init ngram_count (fun i ->
        if( i + n >= s.Length ) then
        s.Substring(i,s.Length - i) + String.init ((i + n) - s.Length)
            (fun i -> "#")
        else
            s.Substring(i,n)
    )
    let ngram_array_unique = ngram_list
                            |> Seq.ofList
                            |> Seq.distinct
                            |> Array.ofSeq

//produce tuples of ngrams (ngram string,how much occurrences in original string)

    Seq.init ngram_array_unique.Length (fun i -> (ngram_array_unique.[i],
        ngram_list |> List.filter(fun item -> item = ngram_array_unique.[i])
                                   |> List.length)
                                        ) 

4 个答案:

答案 0 :(得分:5)

我对评估字符串的相似性知之甚少,所以我不能就第2点和第3点给出很多反馈。但是,这里有一些建议可能有助于简化实现。

您需要执行的许多操作已经在某些F#库函数中可用于处理序列(列表,数组等)。字符串也是(字符)序列,因此您可以编写以下内容:

open System

let ngramSplit n (s:string) = 
  let ngrams = Seq.windowed n s
  let grouped = Seq.groupBy id ngrams
  Seq.map (fun (ngram, occurrences) -> 
    String(ngram), Seq.length occurrences) grouped

Seq.windowed函数实现了一个滑动窗口,这正是您提取字符串的n-gram所需要的。 Seq.groupBy函数将序列元素(n-gram)收集到包含具有相同键的值的组序列中。我们使用id来计算密钥,这意味着n-gram本身就是密钥(因此我们得到组,其中每个组包含相同的n-gram)。然后我们只需将n-gram转换为字符串并计算组中元素的数量。

或者,您可以将整个函数编写为单个处理管道,如下所示:

let ngramSplit n (s:string) = 
  s |> Seq.windowed n
    |> Seq.groupBy id 
    |> Seq.map (fun (ngram, occurrences) -> 
         String(ngram), Seq.length occurrences)

答案 1 :(得分:1)

您的代码看起来不错。由于经常使用ngram提取和相似性比较。你应该考虑一些效率问题。

MapReduce模式非常适合您的频率计数问题:

  1. 获取一个字符串,发出(word,1)对
  2. 对单词进行分组并将所有计数加在一起。

    let wordCntReducer (wseq: seq<int*int>) =

       wseq 
       |> Seq.groupBy (fun (id,cnt) -> id) 
       |> Seq.map (fun (id, idseq) -> 
                (id, idseq |> Seq.sumBy(fun (id,cnt) -> cnt)))
    (* test: wordCntReducer [1,1; 2,1; 1,1; 2,1; 2,2;] *)
    
  3. 您还需要在ngram构建期间为一组字符串维护<word,int>映射。因为在后续处理过程中处理整数而不是字符串会更有效。

    (2)比较两个短弦之间的距离。通常的做法是使用简单的动态编程来使用Edit Distance。要计算文章之间的相似性,最先进的方法是使用TFIDF特征表示。实际上,上面的代码用于从我的数据挖掘库中提取的术语频率计数。

    (3)有复杂的NLP方法,例如基于解析树的树内核,用于配合上下文信息。

答案 2 :(得分:1)

我认为你对问题(1)有一些好的答案。

问题(2):

您可能希望余弦相似性来比较两个任意n-gram集合(越大越好)。这为您提供了0.0 - 1.0的范围,无需任何缩放。 Wikipedia page gives an equation和F#翻译非常简单:

let cos a b = 
  let dot = Seq.sum (Seq.map2 ( * ) a b)
  let magnitude v = Math.Sqrt (Seq.sum (Seq.map2 ( * ) v v))
  dot / (magnitude a * magnitude b)

对于输入,您需要运行类似Tomas的答案以获取两个映射,然后删除仅存在于一个映射中的键:

let values map = map |> Map.toSeq |> Seq.map snd
let desparse map1 map2 = Map.filter (fun k _ -> Map.containsKey k map2) map1
let distance textA textB =
  let a = ngramSplit 3 textA |> Map.ofSeq
  let b = ngramSplit 3 textB |> Map.ofSeq
  let aValues = desparse a b |> values
  let bValues = desparse b a |> values
  cos aValues bValues

基于字符的n-gram,我不知道你的结果会有多好。这取决于你感兴趣的文本的哪种功能。我做自然语言处理,所以通常我的第一步是词性标注。然后我比较了n-gram的词性。我使用T'n'T,但它有奇怪的许可问题。我的一些同事使用ACOPOST代替,免费替代(如啤酒和自由)。我不知道准确性有多好,但是现在POS标签是一个众所周知的问题,至少对于英语和相关语言而言。

问题(3):

比较几乎相同的两个字符串的最佳方法是Levenshtein distance。我不知道你的情况是否属于这种情况,尽管你可以通过多种方式放松假设,例如比较DNA字符串。

关于这个主题的标准书是Sankoff和Kruskal的"Time Warps, String Edits, and Maromolecules"。它已经很老了(1983年),但提供了很好的例子,说明如何使基本算法适应多种应用。

答案 3 :(得分:0)

问题3:

我的参考书是Bill Smyth的Computing Patterns in Strings