最も近い文字列の一致を取得する 質問する

最も近い文字列の一致を取得する 質問する

複数の文字列をテスト文字列と比較し、それによく似た文字列を返す方法が必要です。

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 などの複数の言語に実装する予定です。現時点では、疑似コードでも問題ありません。特定の言語の例を提供していただければ、それもありがたいです。

ベストアンサー1

この問題に遭遇したのは、約 1 年前、さまざまな情報のデータベースで石油掘削装置に関するユーザーが入力した情報を検索するときでした。目的は、最も一般的な要素を含むデータベース エントリを識別できる、ある種のあいまい文字列検索を実行することでした。

研究の一部には、レーベンシュタイン距離文字列またはフレーズを別の文字列またはフレーズに変換するために、文字列またはフレーズに何回変更を加える必要があるかを決定するアルゴリズム。

私が考え出した実装は比較的単純なもので、2 つのフレーズの長さ、各フレーズ間の変更回数、および各単語がターゲット エントリで見つかるかどうかの加重比較が含まれていました。

この記事は非公開サイトにあるため、関連する内容をここに追加できるように最善を尽くします。


あいまい文字列マッチングは、2 つの単語またはフレーズの類似性を人間のような方法で推定するプロセスです。多くの場合、互いに最も類似している単語またはフレーズを特定します。この記事では、あいまい文字列マッチングの問題に対する社内ソリューションと、さまざまな問題を解決する上でのその有用性について説明します。これにより、これまでは面倒なユーザーの介入が必要だったタスクを自動化できます。

導入

あいまいな文字列のマッチングを行う必要性は、もともとメキシコ湾バリデータ ツールの開発中に生じました。当時存在していたのは、メキシコ湾の既知の石油掘削装置とプラットフォームのデータベースであり、保険を購入する人々は資産に関する不適切に入力された情報を提供し、私たちはそれを既知のプラットフォームのデータベースと照合する必要がありました。提供される情報が非常に少ない場合、私たちにできる最善のことは、彼らが言及しているものを「認識」して適切な情報を呼び出すために保険引受人に頼ることです。ここで、この自動化されたソリューションが役立ちます。

私はあいまいな文字列マッチングの方法を 1 日かけて調べ、最終的に Wikipedia で非常に役立つ Levenshtein 距離アルゴリズムを見つけました。

実装

背後にある理論について読んだ後、実装して最適化する方法を見つけました。これが私の 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 つの単語を結ぶ最短のレーベンシュタイン距離を合計します。基本的に、これは 1 つの「フレーズ」の情報が別のフレーズに実際に含まれているかどうかを、単語ごとの順列として測定します。私はサイド プロジェクトとして数日を費やし、区切り記号に基づいて文字列を分割する最も効率的な方法を考案しました。

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% だけ、Levenstein 距離のペナルティを効果的に減らしています。このようにして、同じ長さの「フレーズ」は完全なペナルティを受けますが、「追加情報」(長い) を含むものの、それ以外はほとんど同じ文字を共有している「フレーズ」は、ペナルティが軽減されます。関数を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 つ (peril) あり、実際に重要なのは組み合わせ (region と peril) が維持されているかどうかであるため、便利です。

最後に、最小重みは 10 に最適化され、最大重みは 1 に最適化されます。これは、2 つのスコア (値フレーズと値単語) のうち最高のスコアがあまり良くない場合、一致は大幅にペナルティを受けますが、2 つのスコアのうち最悪のスコアには大きなペナルティを受けないことを意味します。基本的に、これはvalueWord または valuePhrase のいずれかに良いスコアが必要であることを強調しますが、両方に良いスコアが必要であることを要求しません。一種の「得られるものを利用する」という考え方です。

これら 5 つの重みの最適化された値が、どのようなあいまいな文字列マッチングが実行されているかを示しているのは非常に興味深いことです。あいまいな文字列マッチングの実際のケースはそれぞれまったく異なるため、これらのパラメーターは非常に異なります。私はこれまで、これを 3 つの異なるアプリケーションで使用しました。

最終的な最適化では使用されませんでしたが、対角線上のすべての完璧な結果に対して列を一致させ、ユーザーがパラメータを変更してスコアが 0 から乖離する速度を制御し、検索フレーズ間の固有の類似性 (理論的には結果の誤検出を相殺するために使用できる) を記録できるベンチマーク シートが作成されました。

あいまい文字列マッチングベンチマーク

さらなる応用

このソリューションは、完全に一致する文字列がない文字列セット内の文字列をコンピュータ システムに識別させたい場合に、どこでも使用できる可能性があります (文字列の近似一致 vlookup など)。


したがって、このことから理解すべきことは、おそらく、高レベルのヒューリスティック (あるフレーズの単語を別のフレーズで検索する、両方のフレーズの長さなど) と、レーベンシュタイン距離アルゴリズムの実装を組み合わせて使用​​する必要があるということです。どれが「最適な」一致であるかを判断するのはヒューリスティック (あいまい) な決定であるため、類似性を判断するために思いついたメトリックに対して、一連の重み付けをする必要があります。

適切なヒューリスティックと重みのセットを使用すると、比較プログラムが、あなたが下したであろう決定を迅速に下すようになります。

おすすめ記事