作为更好地理解我目前正在学习的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)
)
答案 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模式非常适合您的频率计数问题:
对单词进行分组并将所有计数加在一起。
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;] *)
您还需要在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