Мені було представлено цю проблему близько року тому, коли справа доходила до того, щоб користувач ввів інформацію про нафтову установку в базу даних різної інформації. Мета полягала в тому, щоб зробити якийсь нечіткий пошук рядків, який міг би визначити запис бази даних з найпоширенішими елементами.
Частина досліджень передбачала реалізацію алгоритму відстані Левенштейна , який визначає, скільки змін потрібно внести до рядка або фрази, щоб перетворити його в інший рядок або фразу.
Реалізація, яку я придумав, була порівняно простою і передбачала зважене порівняння довжини двох фраз, кількості змін між кожною фразою та того, чи можна знайти кожне слово в цільовому записі.
Стаття розміщена на приватному веб-сайті, тому я докладу всіх зусиль, щоб додати тут відповідний вміст:
Нечітке узгодження рядків - це процес визначення людської оцінки подібності двох слів або словосполучень. У багатьох випадках воно передбачає виявлення слів або фраз, які найбільш схожі між собою. Ця стаття описує внутрішнє вирішення проблеми нечіткого узгодження рядків та її корисність у вирішенні різноманітних проблем, які можуть дозволити нам автоматизувати завдання, які раніше вимагали стомленого залучення користувача.
Вступ
Необхідність спочатку нечіткого узгодження рядків виникла під час розробки інструменту 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, і відзначати вроджену схожість між пошуковими фразами ( який теоретично може бути використаний для компенсації помилкових позитивних результатів)
Подальші програми
Це рішення може використовуватись у будь-якому місці, де користувач бажає, щоб комп'ютерна система ідентифікувала рядок у наборі рядків, де немає ідеального відповідності. (На кшталт приблизного пошуку збігів для рядків).
Тож, що ви повинні взяти з цього, це те, що ви, ймовірно, хочете використовувати комбінацію евристики високого рівня (пошук слів з однієї фрази в іншій фразі, довжина обох фраз тощо) разом з реалізацією алгоритму відстані Левенштейна. Оскільки вирішити, що є "найкращим" матчем, є евристичне (нечітке) визначення - вам доведеться придумати набір ваг для будь-яких показників, які ви придумали, щоб визначити подібність.
Завдяки відповідному набору евристики і ваг, ви зможете, щоб ваша програма порівняння швидко приймала рішення, які ви б прийняли.