sh1’s diary

プログラミング、読んだ本、資格試験、ゲームとか私を記録するところ

C# 文字列の類似度を計算する N-gram

  1. あいうえお
  2. あいうえの

上記のような文字列の類似度を計算するやり方には「n-gram」や「Jaro–Winkler distance」など色々なやり方があるみたいです。一番簡単と思ったやり方は「n-gram」でした。少し古いですが livedoor の技術ブログに紹介記事があります。

実装が簡単なのもあって ruby のコードで github公開されているリポジトリーがありました。

とりあえず、Usage に掲載されている以下例は、2つテキストの一致度 0.266... になるみたいです。githubruby コードを動かしてみます。(3-gram の例になります)

  • he is genius
  • she is cute

0が完全不一致、1が完全一致です。類似度は0~1の間に正規化された数値として表現されるみたいです。

class Test
    def trigramify(text)
        trigs = []
        text.chars.each_cons(3) { |v| trigs << v.join }
        trigs
    end
end

  test = Test.new
  data1 = test.trigramify('he is genius')
  print(data1)
  print("\n")
  data2 = test.trigramify('she is cute')
  print(data2)
  print("\n\n")

  print((data1 | data2))
  print(", count= #{(data1 | data2).size}") # 重複データを除いた合計数
  print("\n")
  print((data1 & data2))
  print(", count= #{(data1 & data2).size}") # 一致件数
["he ", "e i", " is", "is ", "s g", " ge", "gen", "eni", "niu", "ius"]
["she", "he ", "e i", " is", "is ", "s c", " cu", "cut", "ute"]

["he ", "e i", " is", "is ", "s g", " ge", "gen", "eni", "niu", "ius", "she", "s c", " cu", "cut", "ute"], count= 15
["he ", "e i", " is", "is "], count= 4

単純にデータ件数を出すと、data1 = 10 個、data2 = 9 個、一致データ数が 4 件です。なんで、計算式は  \dfrac{4}{(10+9-4)} で求めているみたいです。

ポイントは、全データの合計数を出すときに、一致テキストを data1 と data2 の2個分にしないことですね。なんで、分母から一致データ数を引いてやります。

間違いというわけではないのですが、曖昧検索 (Fuzzy-matching) を N-gram 方式で行う の記事は、最終の計算結果が 0.6666... になっていますが、これは分母の数は data1 の総数(重複なし)になっていると思います。

C# で実装する

GitHub に公開しました。

参考にしたリポジトリーは 3-gram 固定になっていたので、1 ~ 5 に対応させてみました。

一般的には 1, 2, 3 までが基本となっているみたいですが、それ以上の例もあるみたいです。wiki を参照してみてください。

参考