その他の情報のデータベースで石油リグに関するユーザー入力情報を検索することになると、約1年前にこの問題が発生しました。目標は、最も一般的な要素を持つデータベースエントリを特定できる、ある種のあいまい文字列検索を実行することでした。
レーベンシュタイン距離を実装することを含む研究の一部アルゴリズムの。これは、文字列またはフレーズを別の文字列またはフレーズに変換するために、いくつの変更を行う必要があるかを決定します。
私が思いついた実装は比較的単純で、2つのフレーズの長さ、各フレーズ間の変更の数、およびターゲットエントリで各単語が見つかるかどうかを加重比較しました。
記事は非公開サイトにあるので、ここに関連コンテンツを追加できるように最善を尽くします。
ファジー文字列マッチングは、2つの単語またはフレーズの類似性を人間のように推定するプロセスです。多くの場合、相互に最も類似している単語やフレーズを特定する必要があります。この記事では、ファジー文字列マッチングの問題に対する社内ソリューションと、以前は面倒なユーザーの関与を必要としたタスクを自動化できるさまざまな問題の解決におけるその有用性について説明します。
前書き
メキシコ湾の検証ツールの開発中に、最初はファジー文字列照合を行う必要がありました。存在していたのは、メキシコ湾の既知の石油掘削装置とプラットフォームのデータベースでした。保険を購入する人々は、資産について間違って入力された情報をいくつか提供し、既知のプラットフォームのデータベースと照合する必要がありました。提供された情報が非常に少ない場合、私たちができる最善のことは、引受人が参照していたものを「認識」して適切な情報を呼び出すことです。この場合、この自動化されたソリューションが役に立ちます。
私は1日かけてファジー文字列マッチングの方法を研究し、最終的にはWikipediaの非常に便利なレーベンシュタイン距離アルゴリズムに出会いました。
実装
その背後にある理論について読んだ後、私はそれを実装して最適化する方法を見つけました。これは私のコードが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
シンプルでスピーディー、そして非常に便利なメトリック。これを使用して、2つの文字列の類似性を評価するための2つの個別のメトリックを作成しました。1つは「valuePhrase」、もう1つは「valueWords」です。valuePhraseは、2つのフレーズ間のレーベンシュタイン距離であり、valueWordsは、スペース、ダッシュなどの区切り文字に基づいて文字列を個々の単語に分割し、各単語を他の単語と比較して、最短で合計します任意の2つの単語を結ぶレーベンシュタイン距離。基本的に、これは、単語ごとの順列と同様に、ある「フレーズ」の情報が実際に別の「フレーズ」に含まれているかどうかを測定します。デリミタに基づいて文字列を分割する最も効率的な方法を考え出すサイドプロジェクトとして数日を費やしました。
valueWords、valuePhrase、およびSplit関数:
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
類似性の測定
これらの2つのメトリックと、2つの文字列間の距離を単純に計算する3番目のメトリックを使用して、最適化アルゴリズムを実行して最大数の一致を達成できる一連の変数があります。ファジー文字列マッチングは、それ自体がファジーサイエンスであるため、文字列の類似性を測定するための線形に独立したメトリックを作成し、互いに一致させたい既知の文字列のセットを持つことで、特定のスタイルのパラメーターを見つけることができます文字列、最高のあいまい一致結果を与えます。
当初、このメトリックの目標は、完全一致の場合は検索値を低くし、順列が増えるほど検索値を増やすことでした。非現実的なケースでは、これは一連の明確に定義された順列を使用して定義し、必要に応じて検索値の結果が増加するように最終的な式を設計することはかなり簡単でした。
上のスクリーンショットでは、ヒューリスティックを微調整して、検索語と結果の違いを認識できるように、適切にスケーリングされていると感じられるものを考え出しました。Value Phrase
上記のスプレッドシートで使用したヒューリスティックはでした=valuePhrase(A2,B2)-0.8*ABS(LEN(B2)-LEN(A2))
。2つの「フレーズ」の長さの違いの80%だけ、レベンシュタイン距離のペナルティを効果的に減らしていました。このように、同じ長さの「フレーズ」は完全なペナルティを受けますが、「追加情報」(長い)を含むが、それでもなお、ほとんど同じ文字を共有する「フレーズ」はペナルティが軽減されます。Value Words
関数をそのまま使用し、最後にSearchVal
的なヒューリスティックを次のように定義しました=MIN(D2,E2)*0.8+MAX(D2,E2)*0.2
-加重平均。2つのスコアのうち低い方の方が80%、高い方のスコアの20%が加重されました。これは、適切な一致率を得るための私のユースケースに適したヒューリスティックでした。これらの重みは、テストデータとの最適な一致率を取得するために調整できるものです。
ご覧のとおり、あいまいな文字列マッチングメトリックである最後の2つのメトリックは、一致することを意図した文字列(対角線の下)に低いスコアを与える自然な傾向があります。これはとてもいい。
アプリケーション
あいまい一致の最適化を可能にするために、各メトリックに重みを付けます。そのため、ファジー文字列照合のすべてのアプリケーションは、異なる方法でパラメーターに重みを付けることができます。最終スコアを定義する式は、メトリックとその重みの単純な組み合わせです。
value = Min(phraseWeight*phraseValue, wordsWeight*wordsValue)*minWeight
+ Max(phraseWeight*phraseValue, wordsWeight*wordsValue)*maxWeight
+ lengthWeight*lengthValue
最適化アルゴリズム(ニューラルネットワークは離散的な多次元問題であるため、ここでは最適です)を使用して、目標は一致の数を最大化することです。この最終的なスクリーンショットに示すように、各セットの正しい一致の数を検出する関数を作成しました。最低スコアに一致するはずの文字列が割り当てられている場合、列または行にポイントが割り当てられ、最低スコアに同点があり、関連する一致した文字列が正しい一致である場合、部分的なポイントが与えられます。次に、それを最適化しました。緑のセルが現在の行に最も一致する列であり、セルの周りの青い四角が現在の列に最も一致する行であることがわかります。下隅のスコアはおおよその一致数であり、これが最大化するために最適化問題に伝えているものです。
アルゴリズムは素晴らしい成功でした、そして、解決パラメーターはこのタイプの問題について多くを言います。最適化されたスコアは44で、最高のスコアは48です。最後の5列はおとりであり、行の値とまったく一致していません。デコイが多いほど、自然にベストマッチを見つけることが難しくなります。
この特定の一致の場合、長い単語を表す略語を想定しているため、文字列の長さは関係ありません。したがって、長さの最適な重みは-0.3です。つまり、長さが異なる文字列にペナルティを課しません。これらの略語を見込んでスコアを下げ、文字列が短いために置換が少なくて済む非単語の一致よりも部分的な単語の一致を優先する余地を増やしています。
単語の重みは1.0ですが、フレーズの重みは0.5です。これは、1つの文字列から欠落している単語全体にペナルティを課し、フレーズ全体が損なわれていないことを評価します。これらの文字列の多くは共通の1つの単語(危険)を持っているので、組み合わせ(領域と危険)が維持されるかどうかが本当に重要であるので、これは便利です。
最後に、最小の重みは10で、最大の重みは1で最適化されます。これは、2つのスコア(値句と値語)の最高があまり良くない場合、一致に大きなペナルティが課されることを意味しますが、 2つのスコアの最悪のものを大幅に罰することはありません。本質的に、これは、valueWordまたはvaluePhraseのいずれかが良いスコアを持つことを要求し、両方ではないことを強調します。ある種の「私たちが得ることができるものを取りなさい」という考え方。
これらの5つの重みの最適化された値が、行われているファジー文字列マッチングの種類について何を言っているかは、本当に魅力的です。ファジー文字列マッチングの完全に異なる実際的なケースでは、これらのパラメーターは大きく異なります。これまでに3つの個別のアプリケーションに使用しました。
最終的な最適化では使用されていませんが、対角線上のすべての完璧な結果を得るために列をそれ自体に一致させるベンチマークシートが確立され、ユーザーがパラメーターを変更してスコアが0から逸脱する速度を制御し、検索フレーズ間の本質的な類似性を確認できます(理論的には、結果の偽陽性を相殺するために使用できます)
さらなるアプリケーション
このソリューションは、ユーザーがコンピュータシステムに、完全に一致しない文字列のセット内の文字列を識別させたい場所で使用できる可能性があります。(文字列の近似一致vlookupのように)。
したがって、これから取る必要があるのは、高レベルのヒューリスティック(一方のフレーズから他方のフレーズの単語を見つける、両方のフレーズの長さなど)と、レーベンシュタイン距離アルゴリズムの実装を組み合わせて使用することです。「最適な」一致を決定するのはヒューリスティック(ファジー)決定であるため、類似性を決定するには、思いついたメトリックの重みのセットを考え出す必要があります。
適切なヒューリスティックと重みのセットを使用すると、比較プログラムで、必要な決定をすばやく行うことができます。