Отримання найближчого збігу рядків


397

Мені потрібен спосіб порівняння декількох рядків з тестовою рядком та повернення рядка, який дуже нагадує його:

TEST STRING: THE BROWN FOX JUMPED OVER THE RED COW

CHOICE A   : THE RED COW JUMPED OVER THE GREEN CHICKEN
CHOICE B   : THE RED COW JUMPED OVER THE RED COW
CHOICE C   : THE RED FOX JUMPED OVER THE BROWN COW

(Якщо я це зробив правильно) Найближчим рядком до "TEST STRING" повинен бути "CHOICE C". Який найпростіший спосіб зробити це?

Я планую реалізувати це на кількох мовах, включаючи VB.net, Lua та JavaScript. На даний момент псевдокод прийнятний. Якщо ви можете навести приклад для певної мови, це також оцінено!


3
Алгоритми, які зазвичай роблять цей тип роботи, визначають, скільки змін потрібно, щоб перетворити досліджуваний рядок у цільовий рядок. Ці типи алгоритмів взагалі не працюють добре в такій ситуації. Я думаю, що отримати комп’ютер, щоб зняти це, буде дуже важко.
Метт Грір

3
Вихідний код Левенштейна відстані на багатьох мовах: Java, Ruby, Python, PHP тощо. En.wikibooks.org/wiki/Algorithm_Implementation/Strings/…
joelparkerhenderson

9
Загалом, те, що вважається "найближчим рядком", залежатиме від застосовуваного заходу подібності та штрафних санкцій, що застосовуються за введення прогалин у вирівнюванні. Наприклад, чи вважаєте ви "корову" та "курку" більш схожими на "корову" та "червону" (бо вони пов'язані між собою поняття), чи це навпаки (бо "курка" має більше букв, ніж "корова" )? Але з огляду на міру подібності та штрафний пробіл, можна показати, що алгоритм Левенштайна нижче гарантовано знайде вам найближчий рядок. Те ж саме стосується Needleman-Wunsch та Smith-Waterman (далі нижче).
Sten L

Робіть групування символів або групування слів. Дайте бал.
Кейсі

Відповіді:


952

Мені було представлено цю проблему близько року тому, коли справа доходила до того, щоб користувач ввів інформацію про нафтову установку в базу даних різної інформації. Мета полягала в тому, щоб зробити якийсь нечіткий пошук рядків, який міг би визначити запис бази даних з найпоширенішими елементами.

Частина досліджень передбачала реалізацію алгоритму відстані Левенштейна , який визначає, скільки змін потрібно внести до рядка або фрази, щоб перетворити його в інший рядок або фразу.

Реалізація, яку я придумав, була порівняно простою і передбачала зважене порівняння довжини двох фраз, кількості змін між кожною фразою та того, чи можна знайти кожне слово в цільовому записі.

Стаття розміщена на приватному веб-сайті, тому я докладу всіх зусиль, щоб додати тут відповідний вміст:


Нечітке узгодження рядків - це процес визначення людської оцінки подібності двох слів або словосполучень. У багатьох випадках воно передбачає виявлення слів або фраз, які найбільш схожі між собою. Ця стаття описує внутрішнє вирішення проблеми нечіткого узгодження рядків та її корисність у вирішенні різноманітних проблем, які можуть дозволити нам автоматизувати завдання, які раніше вимагали стомленого залучення користувача.

Вступ

Необхідність спочатку нечіткого узгодження рядків виникла під час розробки інструменту Validator Мексиканської затоки. Існувала база даних про відомі мексиканські затоки нафтових платформ і платформ, і люди, які купують страхування, дали б нам дещо погано набрану інформацію про свої активи, і нам довелося відповідати її базі даних відомих платформ. Коли було надано дуже мало інформації, найкраще, що ми могли зробити, - це розраховувати на андеррайтера, щоб він "розпізнав" ту, про яку вони зверталися, та викликати належну інформацію. Ось тут це корисне автоматизоване рішення.

Я провів день, вивчаючи методи нечіткого узгодження рядків, і врешті-решт натрапив на дуже корисний алгоритм відстані Левенштейна у Вікіпедії.

Впровадження

Прочитавши теорію, що стоїть за нею, я реалізував і знайшов способи її оптимізації. Ось так виглядає мій код у VBA:

'Calculate the Levenshtein Distance between two strings (the number of insertions,
'deletions, and substitutions needed to transform the first string into the second)
Public Function LevenshteinDistance(ByRef S1 As String, ByVal S2 As String) As Long
    Dim L1 As Long, L2 As Long, D() As Long 'Length of input strings and distance matrix
    Dim i As Long, j As Long, cost As Long 'loop counters and cost of substitution for current letter
    Dim cI As Long, cD As Long, cS As Long 'cost of next Insertion, Deletion and Substitution
    L1 = Len(S1): L2 = Len(S2)
    ReDim D(0 To L1, 0 To L2)
    For i = 0 To L1: D(i, 0) = i: Next i
    For j = 0 To L2: D(0, j) = j: Next j

    For j = 1 To L2
        For i = 1 To L1
            cost = Abs(StrComp(Mid$(S1, i, 1), Mid$(S2, j, 1), vbTextCompare))
            cI = D(i - 1, j) + 1
            cD = D(i, j - 1) + 1
            cS = D(i - 1, j - 1) + cost
            If cI <= cD Then 'Insertion or Substitution
                If cI <= cS Then D(i, j) = cI Else D(i, j) = cS
            Else 'Deletion or Substitution
                If cD <= cS Then D(i, j) = cD Else D(i, j) = cS
            End If
        Next i
    Next j
    LevenshteinDistance = D(L1, L2)
End Function

Простий, швидкий і дуже корисний показник. Використовуючи це, я створив дві окремі метрики для оцінки подібності двох рядків. Один я називаю "valuePhrase", а один - "valueWords". valuePhrase - це лише відстань Левенштейна між двома фразами, а valueWords розбиває рядок на окремі слова, засновані на роздільниках, таких як пробіли, тире та все інше, що ви хочете, і порівнює кожне слово між собою, підсумовуючи найкоротший Левенштайн відстань, що з'єднує будь-які два слова. По суті, він вимірює, чи інформація в одній «фразі» справді міститься в іншій, як переслідування, що сприймає слово. Я провів кілька днів як побічний проект, придумавши найефективніший спосіб розщеплення струни на основі роздільників.

valueWords, valuePhrase та розділити функцію:

Public Function valuePhrase#(ByRef S1$, ByRef S2$)
    valuePhrase = LevenshteinDistance(S1, S2)
End Function

Public Function valueWords#(ByRef S1$, ByRef S2$)
    Dim wordsS1$(), wordsS2$()
    wordsS1 = SplitMultiDelims(S1, " _-")
    wordsS2 = SplitMultiDelims(S2, " _-")
    Dim word1%, word2%, thisD#, wordbest#
    Dim wordsTotal#
    For word1 = LBound(wordsS1) To UBound(wordsS1)
        wordbest = Len(S2)
        For word2 = LBound(wordsS2) To UBound(wordsS2)
            thisD = LevenshteinDistance(wordsS1(word1), wordsS2(word2))
            If thisD < wordbest Then wordbest = thisD
            If thisD = 0 Then GoTo foundbest
        Next word2
foundbest:
        wordsTotal = wordsTotal + wordbest
    Next word1
    valueWords = wordsTotal
End Function

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' SplitMultiDelims
' This function splits Text into an array of substrings, each substring
' delimited by any character in DelimChars. Only a single character
' may be a delimiter between two substrings, but DelimChars may
' contain any number of delimiter characters. It returns a single element
' array containing all of text if DelimChars is empty, or a 1 or greater
' element array if the Text is successfully split into substrings.
' If IgnoreConsecutiveDelimiters is true, empty array elements will not occur.
' If Limit greater than 0, the function will only split Text into 'Limit'
' array elements or less. The last element will contain the rest of Text.
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Function SplitMultiDelims(ByRef Text As String, ByRef DelimChars As String, _
        Optional ByVal IgnoreConsecutiveDelimiters As Boolean = False, _
        Optional ByVal Limit As Long = -1) As String()
    Dim ElemStart As Long, N As Long, M As Long, Elements As Long
    Dim lDelims As Long, lText As Long
    Dim Arr() As String

    lText = Len(Text)
    lDelims = Len(DelimChars)
    If lDelims = 0 Or lText = 0 Or Limit = 1 Then
        ReDim Arr(0 To 0)
        Arr(0) = Text
        SplitMultiDelims = Arr
        Exit Function
    End If
    ReDim Arr(0 To IIf(Limit = -1, lText - 1, Limit))

    Elements = 0: ElemStart = 1
    For N = 1 To lText
        If InStr(DelimChars, Mid(Text, N, 1)) Then
            Arr(Elements) = Mid(Text, ElemStart, N - ElemStart)
            If IgnoreConsecutiveDelimiters Then
                If Len(Arr(Elements)) > 0 Then Elements = Elements + 1
            Else
                Elements = Elements + 1
            End If
            ElemStart = N + 1
            If Elements + 1 = Limit Then Exit For
        End If
    Next N
    'Get the last token terminated by the end of the string into the array
    If ElemStart <= lText Then Arr(Elements) = Mid(Text, ElemStart)
    'Since the end of string counts as the terminating delimiter, if the last character
    'was also a delimiter, we treat the two as consecutive, and so ignore the last elemnent
    If IgnoreConsecutiveDelimiters Then If Len(Arr(Elements)) = 0 Then Elements = Elements - 1

    ReDim Preserve Arr(0 To Elements) 'Chop off unused array elements
    SplitMultiDelims = Arr
End Function

Заходи подібності

Використовуючи ці дві метрики, і третю, яка просто обчислює відстань між двома рядками, у мене є ряд змінних, за допомогою яких я можу запустити алгоритм оптимізації для досягнення найбільшої кількості збігів. Нечітка відповідність рядків сама по собі є нечіткою наукою, і тому, створюючи лінійно незалежні метрики для вимірювання подібності рядків, і маючи відомий набір рядків, які ми хочемо відповідати один одному, ми можемо знайти параметри, які для наших конкретних стилів рядки, дають найкращі нечіткі результати матчу.

Спочатку ціль показника полягала в тому, щоб мати низьке значення для точного відповідності та збільшити значення пошуку для все більш перерваних заходів. У непрактичному випадку це було досить легко визначити, використовуючи набір чітко визначених перестановок, і сконструювати остаточну формулу таким чином, щоб вони мали бажані результати пошуку значень.

Нечіткі рядки, що відповідають перестановкам

На наведеному вище скріншоті я налаштував свою евристику, щоб придумати щось, що мені було чудово масштабовано до моєї сприйнятої різниці між пошуковим терміном та результатом. Евристика, яку я використовував Value Phraseу вищевказаній таблиці, була такою =valuePhrase(A2,B2)-0.8*ABS(LEN(B2)-LEN(A2)). Я ефективно зменшив штраф на відстань Левенштейна на 80% різниці в довжині двох "фраз". Таким чином, "фрази", що мають однакову довжину, зазнають повного покарання, але "фрази", які містять "додаткову інформацію" (довше), але окрім цієї, все ще переважно мають однакові символи, зазнають зменшеного штрафу. Я використовував Value Wordsфункцію як є, і тоді моє остаточне SearchValевристичне значення було визначене як=MIN(D2,E2)*0.8+MAX(D2,E2)*0.2- середньозважений показник. Незалежно від двох балів, вони отримали вагу 80% та 20% вищої оцінки. Це був просто евристичний, який підходив до мого випадку використання, щоб отримати хороший показник відповідності. Ці ваги - це те, що можна було потім налаштувати, щоб отримати найкращий показник відповідності зі своїми тестовими даними.

Нечітка строка, що відповідає значенням фрази

Нечіткі рядки, що відповідають значенням слів

Як бачимо, останні дві метрики, які є нечіткими показниками відповідності рядків, вже мають природну тенденцію давати низькі бали рядкам, які повинні відповідати (вниз по діагоналі). Це дуже добре.

Застосування Щоб дозволити оптимізацію нечіткого зіставлення, я зважую кожен показник. Отже, кожне застосування нечіткої відповідності рядків може змінювати параметри по-різному. Формула, яка визначає кінцевий результат, - це просто поєднання показників та їх ваги:

value = Min(phraseWeight*phraseValue, wordsWeight*wordsValue)*minWeight
      + Max(phraseWeight*phraseValue, wordsWeight*wordsValue)*maxWeight
      + lengthWeight*lengthValue

Використовуючи алгоритм оптимізації (нейронна мережа найкраще тут, оскільки це дискретна багатовимірна проблема), тепер мета - максимально збільшити кількість збігів. Я створив функцію, яка визначає кількість правильних відповідностей кожного набору один одному, як це видно на цьому заключному скріншоті. Стовпчик або рядок отримує бал, якщо найнижчий бал присвоюється рядку, який повинен був відповідати, а часткові бали даються, якщо є кратне значення для найнижчого балу, а правильне відповідність - між пов'язаними рядками. Потім я її оптимізував. Ви можете бачити, що зелена комірка - це стовпець, який найкраще відповідає поточному рядку, а синій квадрат навколо комірки - це рядок, який найкраще відповідає поточному стовпцю. Оцінка в нижньому куті - це приблизно кількість успішних матчів, і це те, що ми говоримо, що наша проблема оптимізації буде максимальною.

Нечітка струна, що відповідає оптимізованій метриці

Алгоритм мав чудовий успіх, а параметри рішення багато говорять про цей тип проблеми. Ви помітите, що оптимізований бал становив 44, а найкращий можливий бал - 48. 5 стовпців у кінці є приманками, і вони взагалі не співпадають із значеннями рядків. Чим більше манок там, тим важче буде, природно, знайти найкращу відповідність.

У цьому конкретному випадку відповідності довжина рядків не має значення, оскільки ми очікуємо скорочень, які представляють більш довгі слова, тому оптимальна вага для довжини становить -0,3, а це означає, що ми не караємо рядки, які різняться за довжиною. Ми зменшуємо бал в очікуванні цих абревіатур, надаючи більше місця для часткових збігів слів, щоб замінити несловесні збіги, які просто вимагають меншої кількості підстановок, оскільки рядок коротший.

Вага слова - 1,0, тоді як вага фрази - лише 0,5, а це означає, що ми караємо цілі слова, пропущені з одного рядка, і більше цінуємо всю фразу, яка є недоторканою. Це корисно, оскільки для багатьох цих рядків є одне спільне слово (небезпека), де важливо, чи підтримується поєднання (область та небезпека) чи ні.

Нарешті, мінімальну вагу оптимізують у 10, а максимальну - на 1. Це означає, що якщо найкраще з двох балів (значення фрази та слів значення) не дуже добре, відповідність сильно штрафується, але ми Я не буду штрафувати найгірший з двох балів. По суті, це робить акцент на тому, щоб вимагати або valueWord, або valuePhrase, щоб мати хороший бал, але не обидва. Своєрідний менталітет "прийміть те, що ми можемо отримати".

Це дійсно захоплююче, що оптимізоване значення цих 5 ваг говорить про те, що відбувається нечітке узгодження рядків. Для абсолютно різних практичних випадків нечіткого узгодження рядків ці параметри дуже різні. Я використовував його для 3-х окремих додатків досі.

Не використовуючи при остаточній оптимізації, було створено аркуш тестування, який відповідає стовпцям для всіх ідеальних результатів вниз по діагоналі і дозволяє користувачеві змінювати параметри, щоб контролювати швидкість, з якою бали розходяться від 0, і відзначати вроджену схожість між пошуковими фразами ( який теоретично може бути використаний для компенсації помилкових позитивних результатів)

Нечітка струна, що відповідає орієнтиру

Подальші програми

Це рішення може використовуватись у будь-якому місці, де користувач бажає, щоб комп'ютерна система ідентифікувала рядок у наборі рядків, де немає ідеального відповідності. (На кшталт приблизного пошуку збігів для рядків).


Тож, що ви повинні взяти з цього, це те, що ви, ймовірно, хочете використовувати комбінацію евристики високого рівня (пошук слів з однієї фрази в іншій фразі, довжина обох фраз тощо) разом з реалізацією алгоритму відстані Левенштейна. Оскільки вирішити, що є "найкращим" матчем, є евристичне (нечітке) визначення - вам доведеться придумати набір ваг для будь-яких показників, які ви придумали, щоб визначити подібність.

Завдяки відповідному набору евристики і ваг, ви зможете, щоб ваша програма порівняння швидко приймала рішення, які ви б прийняли.


13
Бонус: Якщо хтось хоче включити додаткові показники до своєї зваженої евристики (оскільки я надав лише 3, які не були все лінійно незалежними) - ось цілий список у wikipedia: en.wikipedia.org/wiki/String_metric
Ален

1
Якщо у S2 багато слів (а створення багатьох маленьких об'єктів не надто повільне у вашій вибраній мові), трійка може прискорити роботу. Швидка та легка відстань Левенштейна за допомогою Trie - це чудова стаття про спроби.
JanX2

1
@Alain Це цікавий підхід! Я просто трохи граюся з вашою ідеєю (на C ++), але не розумію жодного моменту, значення valuePhrase. Якщо я бачу правильно у вашому коді, його значення повернення функції відстані Левенштейна. Звідки це значення подвійного / плаваючого в таблиці пошуку "abcd efgh"? Відстань Левенштейна - ціле значення, і я не бачу подальших обчислень у вашому коді, що робить його плаваючою. Що я сумую?
Andreas W. Wylach

1
@ AndreasW.Wylach Чудове спостереження. Я показав, що VBA був лише для обчислення відстані Левенштейна, але евристичний, який я використовував у своїй таблиці, був таким =valuePhrase(A2,B2)-0.8*ABS(LEN(B2)-LEN(A2))чином, я зменшив штраф на відстань Левенштейна на 80% від різниці в довжині двох "фраз". Таким чином, "фрази", що мають однакову довжину, зазнають повного покарання, але "фрази", які містять "додаткову інформацію" (довше), але окрім цієї, все ще переважно мають однакові символи, зазнають зменшеного штрафу.
Ален

1
@Alain Дякую, що повернувся до мого запитання, я це вдячний. Ваше пояснення робить речі тепер зрозумілішими. Тим часом я реалізував метод value_phrase, який дещо глибше аналізує лексеми фрази трохи більше, тобто порядок / позиції лексеми фрази, послідовності лексем без запитів, і він приймає трохи більше нечіткості, коли мова йде про щось як "acbd" порівняно з "abcd". Тенденція балів фразової величини дорівнює вашій, але дещо нижче тут і там. Ще раз чудова тренування, і це дало мені натхнення для нечіткого алгоритму пошуку!
Andreas W. Wylach

88

Ця проблема постійно виникає в біоінформатиці. Вище прийнята відповідь (що до речі було чудово) в біоінформатиці відома як алгоритми Needleman-Wunsch (порівняйте два рядки) та Smith-Waterman (знайдіть приблизну підрядку в більш довгій строці). Вони чудово працюють і десятки років були робочими конями.

Але що робити, якщо у вас мільйон рядків для порівняння? Це трильйон парних порівнянь, кожне з яких O (n * m)! Сучасні ДНК-секвенсери легко генерують мільярд коротких послідовностей ДНК, довжиною близько 200 «літер» ДНК. Як правило, ми хочемо знайти для кожного такого рядка найкращу відповідність геному людини (3 мільярди букв). Зрозуміло, що алгоритм Needleman-Wunsch та його родичів цього не робитимуть.

Ця так звана «проблема вирівнювання» - це поле активних досліджень. Найпопулярніші алгоритми в даний час можуть знайти неточні збіги між 1 мільярдом коротких рядків і геномом людини за лічені години на розумному апаратному забезпеченні (скажімо, восьми ядрах і 32 ГБ оперативної пам’яті).

Більшість із цих алгоритмів працюють, швидко знаходячи короткі точні збіги (насіння), а потім поширюючи їх на повну рядок, використовуючи повільніший алгоритм (наприклад, Сміт-Уотерман). Причина цього працює в тому, що нас насправді цікавлять лише кілька близьких поєдинків, тож окупається, щоб позбутися 99,9 ...% пар, що не мають нічого спільного.

Як пошук точних збігів допомагає знайти неточні збіги? Ну, скажімо, ми допускаємо лише одну різницю між запитом та ціллю. Неважко помітити, що ця різниця повинна виникати або в правій, або в лівій половині запиту, і тому інша половина повинна точно відповідати. Ця ідея може бути розповсюджена на декілька невідповідностей і є основою для алгоритму ELAND , який зазвичай використовується у послідовниках ДНК Illumina.

Існує багато дуже хороших алгоритмів для точного зіставлення рядків. Враховуючи рядок запиту довжиною 200 та цільову рядок довжиною 3 млрд (геном людини), ми хочемо знайти будь-яке місце в цілі, де є підрядка довжиною k, яка точно відповідає підрядковій запиту. Простий підхід розпочнеться з індексації цілі: візьміть усі k-довгі підрядки, помістіть їх у масив і сортуйте їх. Потім візьміть кожну k-довгу підрядку запиту та шукайте відсортований індекс. Сортування та пошук можна здійснити за час O (log n).

Але зберігання може бути проблемою. Індекс цільової літери в 3 мільярди повинен містити 3 мільярди покажчиків і 3 мільярди кілограмових слів. Здавалося б, важко помістити це менше ніж у кілька десятків гігабайт оперативної пам’яті. Але дивно, що ми можемо сильно стиснути індекс, використовуючи перетворення Берроуса -Уілера , і це все ще буде ефективно піддаватися керуванню. Індекс геному людини може вміщувати менше 4 ГБ оперативної пам’яті. Ця ідея є основою популярних вирівнювачів послідовностей, таких як Bowtie та BWA .

Крім того, ми можемо використовувати суфіксний масив , який зберігає лише покажчики, але являє собою одночасний індекс усіх суфіксів у цільовому рядку (по суті, одночасний індекс для всіх можливих значень k; те саме стосується перетворення Берроуса-Уілера ). Індекс суфіксного масиву геному людини займе 12 ГБ оперативної пам’яті, якщо ми будемо використовувати 32-бітні покажчики.

Посилання, наведені вище, містять багато інформації та посилань на первинні наукові праці. Посилання ELAND переходить до PDF з корисними малюнками, що ілюструють залучені концепції, та показує, як поводитися із вставками та видаленнями.

Нарешті, хоча ці алгоритми вирішили в основному проблему (повторного) послідовності одиничних геномів людини (мільярд коротких рядків), технологія послідовності ДНК вдосконалюється навіть швидше, ніж закон Мура, і ми швидко наближаємося до наборів даних на трильйон літер. Наприклад, зараз ведуться проекти по послідовності геномів 10 000 видів хребетних , довжиною кожен мільярд літер або близько того. Природно, ми захочемо зробити парне неточне узгодження рядків на даних ...


3
Дійсно хороший пробіг. Пара виправлень: Сортування інфіксів має принаймні O (n), а не O (log n). А оскільки пошук O (log n) насправді занадто повільний на практиці, ви зазвичай будуєте додаткову таблицю для отримання пошуку O (1) (індекс q-грам). Крім того, я не впевнений, чому ви ставитесь до цього не так, як із суфіксним масивом - це лише оптимізація останнього, ні (сортування інфіксів фіксованої довжини замість суфіксів, оскільки насправді нам не потрібно більше фіксованої довжини).
Конрад Рудольф

1
Крім того, ці алгоритми досі недоцільні для послідовного де-новажу . Вони вирішили послідовність геномів людини лише настільки, наскільки у нас є еталонна послідовність, яку можна використовувати для зіставлення. Але для складання de novo потрібні інші алгоритми (ну, є деякі вирівнювання, які базуються на картографуванні, але зшивання контигтів разом є цілою "новою проблемою". Нарешті, безсоромний модуль: моя дипломна робота містить детальний опис алгоритму ELAND.
Конрад Рудольф

1
Дякую. Я відредагував помилку. Я почав описувати масив фіксованої довжини через те, що це легко зрозуміти. Суфіксні масиви та BWT трохи складніше зрозуміти, але насправді ми іноді хочемо використовувати індекс із різними значеннями k. Наприклад, ЗІРКА використовує суфікс масиви ефективно знаходити зрощування вирівнювання. Звичайно, це корисно для приведення РНК у геном.
Sten L

30

Я оспорюю, що вибір B ближче до тестового рядка, оскільки його початковим рядком є ​​лише 4 символи (і 2 видалення). Тоді як ви бачите С як ближче, оскільки він включає як коричневий, так і червоний. Однак це було б більше відстань редагування.

Існує алгоритм під назвою відстань Левенштейна, який вимірює відстань редагування між двома входами.

Ось інструмент для цього алгоритму.

  1. Вибір курсу А як відстань 15.
  2. Вибір B визначається як відстань 6.
  3. Вибір C визначається як відстань 9.

EDIT: Вибачте, я продовжую змішувати рядки в інструменті Levenshtein. Оновлено для виправлення відповідей.


2
Гаразд, я думаю, це правда. Я погляну на це. Мені особисто байдуже, наскільки це близько до цілі, доки вона досить бовтається. Немає необхідності в удосконаленні;) Бали за вас, поки я не зможу перевірити результати вашої відповіді :)
Freesnöw

18

Реалізація Луа для нащадків:

function levenshtein_distance(str1, str2)
    local len1, len2 = #str1, #str2
    local char1, char2, distance = {}, {}, {}
    str1:gsub('.', function (c) table.insert(char1, c) end)
    str2:gsub('.', function (c) table.insert(char2, c) end)
    for i = 0, len1 do distance[i] = {} end
    for i = 0, len1 do distance[i][0] = i end
    for i = 0, len2 do distance[0][i] = i end
    for i = 1, len1 do
        for j = 1, len2 do
            distance[i][j] = math.min(
                distance[i-1][j  ] + 1,
                distance[i  ][j-1] + 1,
                distance[i-1][j-1] + (char1[i] == char2[j] and 0 or 1)
                )
        end
    end
    return distance[len1][len2]
end

14

Можливо, вас зацікавить це повідомлення в блозі.

http://seatgeek.com/blog/dev/fuzzywuzzy-fuzzy-string-matching-in-python

Fuzzywuzzy - це бібліотека Python, яка забезпечує прості міри відстані, такі як відстань Левенштейна для узгодження рядків. Він побудований поверх дифлібу в стандартній бібліотеці і використовуватиме C-реалізацію Python-levenshtein, якщо вона є.

http://pypi.python.org/pypi/python-Levenshtein/


Для інших, хто читає це, Fuzzywuzzy насправді реалізує багато ідей у ​​чудовій публікації Алена. Якщо ви насправді хочете використовувати деякі з цих ідей, це чудове місце для початку.
Григорій Ареній

12

Можливо, ця бібліотека стане корисною! http://code.google.com/p/google-diff-match-patch/

Наразі він доступний у Java, JavaScript, Dart, C ++, C #, Objective C, Lua та Python

Це також дуже добре працює. Я використовую це в кількох своїх проектах Lua.

І я не думаю, що було б надто складно перенести його на інші мови!


2

Якщо ви робите це в контексті пошукової системи або інтерфейсу з базою даних, ви можете розглянути можливість використання такого інструмента, як Apache Solr , із плагіном ComplexPhraseQueryParser . Ця комбінація дозволяє шукати по індексу рядків з результатами, відсортованими за релевантністю, що визначаються відстані Левенштейна.

Ми використовуємо його для великої колекції виконавців і назв пісень, коли вхідний запит може мати один або кілька друкарських помилок, і він спрацьовує досить добре (і надзвичайно швидко, враховуючи, що колекції є мільйонними рядками).

Крім того, за допомогою Solr ви можете шукати по індексу на вимогу за допомогою JSON, тому вам не доведеться винаходити рішення між різними мовами, на які ви шукаєте.


1

Дуже, дуже хорошим ресурсом для таких видів алгоритмів є Simmetrics: http://sourceforge.net/projects/simmetrics/

На жаль, відсутній дивовижний веб-сайт, який містить багато документації :( Якщо він знову створюється, його попередня адреса була такою: http://www.dcs.shef.ac.uk/~sam/simmetrics.html

Voila (люб’язно надано "Wayback Machine"): http://web.archive.org/web/20081230184321/http://www.dcs.shef.ac.uk/~sam/simmetrics.html

Ви можете вивчити джерело коду, є десятки алгоритмів для таких видів порівнянь, кожен з яких відрізняється компромісом. Реалізації знаходяться в Java.


1

Для ефективного запиту великого набору тексту можна скористатися поняттям Редагувати відстань / префікс редагування відстані.

Змінити відстань ED (x, y): мінімальна кількість перетворень, щоб дістати від терміна x до терміна y

Але обчислення ЕД між кожним терміном і текстом запиту вимагає ресурсів та часу. Тому замість обчислення ЕД для кожного терміна спочатку ми можемо витягти можливі відповідні терміни, використовуючи техніку під назвою Qgram Index. а потім застосувати обчислення ЕД до цих вибраних умов.

Перевага методу індексу Qgram - це підтримка нечіткого пошуку.

Один з можливих підходів для адаптації індексу QGram - це побудова перевернутого індексу за допомогою Qgrams. Там ми зберігаємо всі слова, що складаються з певного Qgram, під цим Qgram (замість того, щоб зберігати повну рядок, ви можете використовувати унікальний ідентифікатор для кожної рядки). Для цього можна використовувати структуру даних Tree Map на Java. Далі наводимо невеликий приклад зберігання термінів

col: col mbia, col ombo, gan col a, ta col ama

Тоді при запиті ми обчислюємо кількість загальних Qgram між текстом запиту та наявними термінами.

Example: x = HILLARY, y = HILARI(query term)
Qgrams
$$HILLARY$$ -> $$H, $HI, HIL, ILL, LLA, LAR, ARY, RY$, Y$$
$$HILARI$$ -> $$H, $HI, HIL, ILA, LAR, ARI, RI$, I$$
number of q-grams in common = 4

загальна кількість q-грамів = 4.

Для термінів з великою кількістю загальних Qграм ми обчислюємо ED / PED відповідно до терміна запиту, а потім пропонуємо термін кінцевому користувачеві.

Ви можете знайти реалізацію цієї теорії в наступному проекті (Див. "QGramIndex.java"). Сміливо задайте будь-які питання. https://github.com/Bhashitha-Gamage/City_Search

Щоб дізнатися більше про редагування відстані, префікс редагування відстані Qgram індекс, будь ласка, перегляньте наступне відео проф. Д-ра Ханні Баст https://www.youtube.com/embed/6pUg2wmGJRo (Урок починається з 20:06)


1

Проблему важко реалізувати, якщо вхідні дані занадто великі (скажімо мільйони рядків). Для вирішення цього питання я використав еластичний пошук.

Швидкий старт: https://www.elastic.co/guide/en/elasticsearch/client/net-api/6.x/elasticsearch-net.html

Просто вставте всі вхідні дані в БД, і ви зможете швидко шукати будь-який рядок на основі будь-якої відстані редагування. Ось фрагмент C #, який надасть вам список результатів, відсортований за відстані редагування (менший до вищого)

var res = client.Search<ClassName>(s => s
    .Query(q => q
    .Match(m => m
        .Field(f => f.VariableName)
        .Query("SAMPLE QUERY")
        .Fuzziness(Fuzziness.EditDistance(5))
    )
));

Яку бібліотеку ви використовуєте? Ще трохи інформації потрібно, щоб це було корисно.
ставки

0

Тут ви можете мати голанг POC для обчислення відстаней між даними словами. Ви можете налаштувати minDistanceта differenceдля інших областей.

Ігровий майданчик: https://play.golang.org/p/NtrBzLdC3rE

package main

import (
    "errors"
    "fmt"
    "log"
    "math"
    "strings"
)

var data string = `THE RED COW JUMPED OVER THE GREEN CHICKEN-THE RED COW JUMPED OVER THE RED COW-THE RED FOX JUMPED OVER THE BROWN COW`

const minDistance float64 = 2
const difference float64 = 1

type word struct {
    data    string
    letters map[rune]int
}

type words struct {
    words []word
}

// Print prettify the data present in word
func (w word) Print() {
    var (
        lenght int
        c      int
        i      int
        key    rune
    )
    fmt.Printf("Data: %s\n", w.data)
    lenght = len(w.letters) - 1
    c = 0
    for key, i = range w.letters {
        fmt.Printf("%s:%d", string(key), i)
        if c != lenght {
            fmt.Printf(" | ")
        }
        c++
    }
    fmt.Printf("\n")
}

func (ws words) fuzzySearch(data string) ([]word, error) {
    var (
        w      word
        err    error
        founds []word
    )
    w, err = initWord(data)
    if err != nil {
        log.Printf("Errors: %s\n", err.Error())
        return nil, err
    }
    // Iterating all the words
    for i := range ws.words {
        letters := ws.words[i].letters
        //
        var similar float64 = 0
        // Iterating the letters of the input data
        for key := range w.letters {
            if val, ok := letters[key]; ok {
                if math.Abs(float64(val-w.letters[key])) <= minDistance {
                    similar += float64(val)
                }
            }
        }

        lenSimilarity := math.Abs(similar - float64(len(data)-strings.Count(data, " ")))
        log.Printf("Comparing %s with %s i've found %f similar letter, with weight %f", data, ws.words[i].data, similar, lenSimilarity)
        if lenSimilarity <= difference {
            founds = append(founds, ws.words[i])
        }
    }

    if len(founds) == 0 {
        return nil, errors.New("no similar found for data: " + data)
    }

    return founds, nil
}

func initWords(data []string) []word {
    var (
        err   error
        words []word
        word  word
    )
    for i := range data {
        word, err = initWord(data[i])
        if err != nil {
            log.Printf("Error in index [%d] for data: %s", i, data[i])
        } else {
            words = append(words, word)
        }
    }
    return words

}

func initWord(data string) (word, error) {
    var word word

    word.data = data
    word.letters = make(map[rune]int)
    for _, r := range data {
        if r != 32 { // avoid to save the whitespace
            word.letters[r]++
        }

    }
    return word, nil
}
func main() {
    var ws words
    words := initWords(strings.Split(data, "-"))
    for i := range words {
        words[i].Print()
    }
    ws.words = words

    solution, _ := ws.fuzzySearch("THE BROWN FOX JUMPED OVER THE RED COW")
    fmt.Println("Possible solutions: ", solution)

}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.