Функция разделения N-грамм для сравнения сходства строк

В рамках упражнения, чтобы лучше понять F#, который я сейчас изучаю, я написал функцию для разделения заданной строки на n-граммы.
1) Я хотел бы получить отзыв о моей функции: можно ли ее написать проще или эффективнее?

2) Моя общая цель - написать функцию, которая возвращает сходство строк (в масштабе 0,0..1,0) на основе сходства n-грамм; Подходит ли этот подход для сравнения коротких строк или этот метод можно надежно использовать для сравнения больших строк (например, статей).

3) Я знаю, что сравнения n-грамм игнорируют контекст двух строк. Какой метод вы бы предложили для достижения моей цели?

//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)
                                        ) 

person Michael    schedule 25.05.2010    source источник
comment
Жаль, что я не могу отметить несколько ответов как принятые: P Всем спасибо!   -  person Michael    schedule 26.05.2010


Ответы (4)


Ваш код выглядит нормально для меня. Поскольку извлечение ngram и сравнение подобия используются очень часто. Здесь следует рассмотреть некоторые вопросы эффективности.

Шаблон MapReduce очень подходит для вашей задачи подсчета частот:

  1. получить строку, выдать (слово, 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;] *)
    

Вам также необходимо поддерживать карту <word,int> во время построения ngram для набора строк. Поскольку гораздо эффективнее обрабатывать целые числа, а не строки во время последующей обработки.

(2) сравнить расстояние между двумя короткими строками. Общепринятой практикой является использование Редактировать расстояние с помощью простого динамического программирования. Чтобы вычислить сходство между статьями, современный метод заключается в использовании TFIDF представление признаков. На самом деле приведенный выше код предназначен для подсчета частоты терминов, извлеченных из моей библиотеки интеллектуального анализа данных.

(3) Существуют сложные методы НЛП, например. ядра дерева, основанные на дереве синтаксического анализа, для совместной работы с контекстной информацией.

person Yin Zhu    schedule 25.05.2010
comment
Если я вас правильно понял, вы имеете в виду, что перед подсчетом частоты я должен дать идентификатор каждой уникальной строке ngram, а затем выполнить подсчет частоты, используя идентификаторы вместо самих строк ngram? - person Michael; 25.05.2010
comment
Я думаю, что TFIDF может быть именно тем, что мне нужно — это определенно шаг в правильном направлении. - person Michael; 26.05.2010

Я мало что знаю об оценке сходства строк, поэтому не могу дать вам много отзывов по пунктам 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-грамм вашей строки. Функция Seq.groupBy собирает элементы последовательности (n-граммы) в последовательность групп, содержащих значения с одинаковым ключом. Мы используем id для вычисления ключа, что означает, что n-грамма сама является ключом (и таким образом мы получаем группы, где каждая группа содержит одинаковые n-граммы). Затем мы просто конвертируем n-граммы в строку и подсчитываем количество элементов в группе.

В качестве альтернативы вы можете написать всю функцию как единый конвейер обработки, например:

let ngramSplit n (s:string) = 
  s |> Seq.windowed n
    |> Seq.groupBy id 
    |> Seq.map (fun (ngram, occurrences) -> 
         String(ngram), Seq.length occurrences)
person Tomas Petricek    schedule 25.05.2010
comment
Ничего себе - это действительно большое упрощение - я думаю, отличные предложения. - person Michael; 25.05.2010
comment
Я думаю, что во втором блоке кода вам нужно избавиться от ngrams, так как он нигде не объявлен, но теперь передается. - person Joel Mueller; 25.05.2010

Я думаю, у вас есть хорошие ответы на вопрос (1).

Вопрос 2):

Вы, вероятно, хотите косинусное сходство для сравнения двух произвольных наборов n-грамм (чем больше, тем лучше). Это дает вам диапазон от 0,0 до 1,0 без необходимости масштабирования. На странице Википедии приводится уравнение, и перевод 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)

Для ввода вам нужно запустить что-то вроде ответа Томаса, чтобы получить две карты, а затем удалить ключи, которые существуют только в одной:

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-граммами я не знаю, насколько хорошими будут ваши результаты. Это зависит от того, какие особенности текста вас интересуют. Я занимаюсь обработкой естественного языка, поэтому обычно первым шагом является пометка частей речи. Затем я сравниваю по n-граммам частей речи. Я использую для этого T'n'T, но у него странные проблемы с лицензированием. Некоторые из моих коллег используют ACOPOST вместо бесплатной альтернативы (как в пиве И свободе). Я не знаю, насколько хороша точность, но маркировка POS в наши дни является хорошо изученной проблемой, по крайней мере, для английского и родственных языков.

Вопрос (3):

Лучший способ сравнить две почти идентичные строки – это расстояние Левенштейна. Я не знаю, так ли это в вашем случае, хотя вы можете ослабить предположения несколькими способами, например, для сравнения строк ДНК.

Стандартной книгой на эту тему является "Time Warps, Редактирование строк и маромолекулы". Он довольно старый (1983 г.), но дает хорошие примеры того, как адаптировать базовый алгоритм к ряду приложений.

person Nathan Shively-Sanders    schedule 25.05.2010
comment
для вычисления сходства необходим разреженный вектор. Также ваша формула cos кажется неверной. - person Yin Zhu; 25.05.2010
comment
Упс. Извините за это, я не должен был писать это с нуля. Я считаю, что отредактированная версия верна. - person Nathan Shively-Sanders; 25.05.2010
comment
спасибо за указание в правильном направлении. Также у меня были некоторые проблемы (из-за конкретных грамматических проблем) с расстоянием Левенштейна при попытке сравнить строки на иврите - это одна из моих мотиваций искать другой алгоритм для сравнения строк. - person Michael; 26.05.2010
comment
Если вы сравниваете списки коротких строк, вы можете посмотреть на работу Томаса Застрова. . Он приближается к сходству с точки зрения диалекта/произношения, но с различными статистическими подходами. Я не вижу никакой априорной причины, по которой Левенштейн не работал бы с ивритом, хотя вам, возможно, придется иметь специальную обработку диакритических знаков гласных (я не уверен, как они представлены в Unicode). - person Nathan Shively-Sanders; 26.05.2010

Вопрос 3:

Мой справочник — Вычисление шаблонов в строках Билла. Смит

person ssp    schedule 25.05.2010