複数の文字列をテスト文字列と比較して、それによく似た文字列を返す方法が必要です。
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年前にこの問題を提示されました。目的は、最も一般的な要素を持つデータベースエントリを識別できる、ある種のファジー文字列検索を実行することでした。
研究の一部には、 Levenshtein distance アルゴリズムを実装することが含まれています。これは、文字列またはフレーズを別の文字列またはフレーズに変換するために変更を加える必要がある回数を決定します。
私が思いついた実装は比較的単純で、2つのフレーズの長さ、各フレーズ間の変更数、および各単語がターゲットエントリで見つかるかどうかの加重比較を含みました。
この記事は非公開のサイトに掲載されているので、関連するコンテンツをここに追加するようにします。
あいまい文字列マッチングは、2つの単語またはフレーズの類似性を人間のように推定するプロセスです。多くの場合、それは互いに最も類似している単語または句を識別することを含みます。この記事では、ファジィ文字列照合問題に対する社内での解決策と、これまで面倒なユーザーの関与を必要としていたタスクを自動化できるようにするさまざまな問題の解決における有用性について説明します。
はじめに
もともとファジィ文字列照合を行う必要性は、メキシコ湾検証ツールを開発している間に起こりました。存在していたのはメキシコ湾の石油リグとプラットフォームの既知のデータベースであり、保険を購入している人々は彼らの資産に関する情報をひどくタイプされた情報を私たちに提供してくれました。与えられた情報がほんの少ししかなかったとき、私たちができる最善のことは彼らが言及していたものを「認識」し、適切な情報を呼び出すために引受会社に頼ることです。これが、この自動化ソリューションが役立つところです。
私は一日かけてファジィ文字列マッチングの方法を調べ、そして最終的にはウィキペディアで非常に有用な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つのフレーズ間のLevenshteinの距離であり、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%で、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つの列はおとりで、行の値とまったく一致しません。デコイが多いほど、自然に最適なものを見つけることが難しくなります。
この特定の一致の場合、文字列の長さは関係ありません。長い単語を表す省略形が予想されるためです。したがって、lengthの最適な重みは-0.3です。これは、長さの異なる文字列にペナルティを与えないことを意味します。これらの省略形を見越してスコアを減らし、部分的なWordの一致が、文字列が短いために必要な置換が少なくて済むWord以外の一致に取って代わる余地を広くします。
単語の重みは1.0ですが、フレーズの重みはわずか0.5です。つまり、1つの文字列から欠落している単語全体にペナルティが科され、フレーズ全体が損なわれていないと評価されます。これらの文字列の多くは共通のWord(危険)を1つ持っているので便利です。ここで本当に重要なのは組み合わせ(地域と危険)が維持されているかどうかです。
最後に、最小の重みは10に、最大の重みは1に最適化されています。これは、2つのスコア(値句と値の単語)のうち最良のものがあまり良くない場合、一致に大きなペナルティが科されます。 2つのスコアのうち最悪のものに大きなペナルティを課すことはできません。基本的に、これはどちらか valueWordまたはvaluePhraseが良いスコアを持つことを要求することを強調していますが、両方ではありません。一種の「私たちが得ることができるものを取りなさい」という考え方。
これら5つの重みの最適化された値が、一種のあいまい文字列マッチングの実行に関してどのように表現しているかは、本当に魅力的です。ファジィ文字列照合のまったく異なる実際的な場合では、これらのパラメータは非常に異なります。私はこれまで3つの別々のアプリケーションにそれを使用しました。
最終的な最適化では使用されていませんが、対角線に沿ってすべての完全な結果を得るために列を自分自身に一致させ、スコアを0から分岐させる割合を制御するようにユーザーが変更できるようにします。理論的には、結果の誤検知を相殺するために使用できます。
その他の用途
この解決法は、完全に一致するものがない場合に、ユーザがコンピュータシステムに一連の文字列の中の文字列を識別させたい場合に使用する可能性がある。 (文字列に対する近似match vlookupのように).
したがって、これから考えるべきことは、Levenshtein距離アルゴリズムの実装と共に、高レベルの発見的方法(他のフレーズの一方のフレーズから単語を見つける、両方のフレーズの長さなど)を組み合わせて使用することです。どちらが「最良の」一致であるかを決定することはヒューリスティックな(あいまいな)決定です - 類似性を決定するためにあなたが思いついたすべての測定基準に対して一連の重みを考え出す必要があります。
適切な一連のヒューリスティックとウェイトを使用して、比較プログラムで迅速に決定を下すことができます。
この問題はバイオインフォマティクスにおいて常に現れています。上記の一般に認められた答え(これは素晴らしい方法です)は、バイオインフォマティクスではNeedleman-Wunsch(2つの文字列を比較)およびSmith-Waterman(長い文字列の中からおおよその部分文字列を見つける)アルゴリズムとして知られています。彼らは素晴らしい仕事をし、何十年もの間主役でした。
しかし、比較する文字列が100万個あるとしたらどうでしょう?これは、それぞれがO(n * m)である1兆対の比較です。現代のDNAシーケンサーは容易に10億の短いDNA配列を生成します。それぞれの長さは約200 DNAです。通常、このような文字列ごとに、ヒトゲノムに対する最良の一致(30億文字)を見つけます。明らかに、Needleman-Wunschアルゴリズムとその親戚はしないでしょう。
このいわゆる「アライメント問題」は活発な研究の分野です。最も人気のあるアルゴリズムは、現在、妥当なハードウェア(例えば、8コアと32 GB RAM)上で10時間の短い文字列とヒトゲノムの間の不正確な一致を数時間で見つけることができます。
これらのアルゴリズムのほとんどは、短い完全一致(シード)をすばやく見つけてから、遅いアルゴリズム(たとえば、Smith-Waterman)を使用してこれらを完全な文字列に拡張することによって機能します。これがうまくいく理由は、私たちはほんのいくつかの密接なマッチに関心があるだけなので、99.9 ...%という共通点がないペアを取り除くのが賢明です。
完全一致を見つけることは、不正確の一致を見つけるのにどのように役立ちますか?さて、クエリとターゲットの間の違いは1つだけにしてください。この違いは、クエリの右半分または左半分のどちらかで発生する必要があるため、残りの半分は正確に一致する必要があることがわかります。このアイデアは複数のミスマッチに拡張することができ、Illumina DNAシーケンサーで一般的に使用されている ELAND アルゴリズムの基礎となります。
正確な文字列マッチングを行うための非常に優れたアルゴリズムがたくさんあります。長さ200のクエリ文字列と、長さ30億のターゲット文字列(ヒトゲノム)があるとすると、クエリのサブ文字列と正確に一致する、長さkのサブ文字列がある場所をターゲット内で見つける必要があります。単純な方法は、ターゲットのインデックスを作成することから始めることです。k-longのすべての部分文字列を取り、それらを配列に入れてソートします。次に、クエリの長さkの各部分文字列を取り出し、ソートされたインデックスを検索します。 並べ替え 検索はO(log n)時間で実行できます。
しかし、ストレージが問題になる可能性があります。 30億文字のターゲットのインデックスには、30億個のポインタと30億個のk長の単語を含める必要があります。これを数十ギガバイト未満のRAMに収めるのは難しいようです。しかし驚くべきことに、 Burrows-Wheeler変換 を使用してインデックスを大幅に圧縮することができますが、それでも効率的なクエリが可能です。ヒトゲノムのインデックスは4 GB未満のRAMに収まります。このアイデアは、 Bowtie や BWA のような一般的なシーケンスアライナの基礎です。
あるいは、ポインタのみを格納し、ターゲット文字列内のすべての接尾辞の同時インデックスを表す 接尾辞配列 を使用することもできます(本質的に、kのすべての可能な値に対する同時インデックス。 Burrows-Wheeler変換にも当てはまります。 32ビットのポインタを使用する場合、ヒトゲノムのサフィックス配列インデックスには12 GBのRAMが必要です。
上記のリンクは豊富な情報と主要な研究論文へのリンクを含んでいます。 ELANDリンクはPDFに移動し、関連する概念を説明する有用な図と挿入および削除の扱い方を示しています。
最後に、これらのアルゴリズムは基本的に単一のヒトゲノム(10億個の短いストリング)を(再)配列決定する問題を解決しましたが、DNA配列決定技術はムーアの法則よりもさらに早く改善し、私たちは急速に1兆文字のデータセットに近づきます。例えば、 10,000脊椎動物種 、それぞれ10億文字程度の長さのゲノムを配列決定するプロジェクトが現在進行中です。当然、データに対してペアワイズの不正確な文字列マッチングを行いたいと思います。
私は、選択肢Bは元の文字列であることから4文字(および2文字の削除)しかないため、テスト文字列に近いと考えます。それは茶色と赤の両方が含まれているため、あなたはCが近いと思います。ただし、編集距離は長くなります。
2つの入力間の編集距離を測定する Levenshtein Distance というアルゴリズムがあります。
ここ はそのアルゴリズムのためのツールです。
編集:申し訳ありませんが、私はlevenshteinツールで文字列を混在させ続けています。正解を修正しました。
子孫のためのLuaの実装:
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
このブログ記事に興味があるかもしれません。
http://seatgeek.com/blog/dev/fuzzywuzzy-fuzzy-string-matching-in-python
Fuzzywuzzyは、文字列照合用のLevenshtein距離などの簡単な距離測定を提供するPythonライブラリです。標準ライブラリのdifflibの上に構築されており、利用可能であればCの実装であるPython-levenshteinを利用します。
あなたはこのライブラリが役に立つと思うかもしれません! http://code.google.com/p/google-diff-match-patch/
現在、Java、JavaScript、Dart、C++、C#、Objective C、Lua、Pythonで利用可能です。
それもかなりうまくいきます。私はそれを私のLuaプロジェクトのいくつかで使っています。
それを他の言語に移植するのはそれほど難しいことではないと思います。
検索エンジンまたはデータベースに対するフロントエンドのコンテキストでこれを実行している場合は、 ComplexPhraseQueryParser プラグインを使用して Apache Solr のようなツールを使用することを検討してください。この組み合わせでは、Levenshteinの距離によって決定されるように、関連性によってソートされた結果を持つ文字列のインデックスに対して検索できます。
入力クエリに1つ以上のタイプミスがある可能性がある場合、私たちはこれを多数のアーティストや曲のタイトルに対して使用しています。
さらに、Solrを使用すると、JSONを介してオンデマンドでインデックスを検索できるので、探しているさまざまな言語間でソリューションを作り直す必要はありません。
これらの種類のアルゴリズムに関する非常に優れたリソースはSimmetricsです。 http://sourceforge.net/projects/simmetrics/
残念ながら、たくさんのドキュメントを含む素晴らしいWebサイトは消えていました:(それがまた戻ってきた場合、以前のアドレスはこれでした: 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です。
入力データが大きすぎる場合(数百万の文字列と言うと)、この問題を実装するのは困難です。これを解決するためにエラスティックサーチを使用しました。
クイックスタート: https://www.elastic.co/guide/en/elasticsearch/client/net-api/6.x/elasticsearch-net.html
すべての入力データをDBに挿入するだけで、編集距離に基づいて任意の文字列をすばやく検索できます。これは編集距離でソートされた結果のリストを与える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))
)
));
大量のテキストを効率的に検索するには、距離の編集/距離の接頭辞の編集の概念を使用できます。
距離ED(x、y)の編集:項xから項yへの変換の最小数
しかし、各用語とクエリテキストの間でEDを計算することは、リソースと時間がかかります。したがって、最初に各用語のEDを計算する代わりに、Qgram Indexと呼ばれる手法を使用して、一致する可能性のある用語を抽出できます。次に、選択した用語にED計算を適用します。
Qgramインデックス手法の利点は、あいまい検索をサポートしていることです。
QGramインデックスを適応させるための1つの可能なアプローチはQgramsを使って逆インデックスを作ることです。そこには、そのQgramの下に、特定のQgramと一致するすべての単語を格納します(完全な文字列を格納する代わりに、各文字列に一意のIDを使用できます)。これには、Javaのツリーマップデータ構造を使用できます。以下は用語の保存に関する小さな例です。
col:colmbia、colombo、gancola、tacolAMA
次に、クエリを実行するときに、クエリテキストと利用可能な用語の間の一般的な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インデックスの詳細については、Hannah Bast教授 の次のビデオをご覧ください。https://www.youtube.com/embed/6pUg2wmGJRo (レッスン20:06から始まります