web-dev-qa-db-ja.com

同様のテキストを持つ記事を見つけるためのアルゴリズム

データベースには多くの記事(タイトル、テキスト付き)があり、質問をするとStack Overflowの「関連する質問」のようなXの最も類似した記事を見つけるアルゴリズムを探しています。

私はこれのためにグーグルを試みましたが、他の「類似したテキスト」の問題についてのページのみを見つけました。 SOは、入力したテキストに対して「リアルタイム」でこれを行います。

どうやって?

60

距離の編集 は、スペルや単語の順序に依存するため、候補となる可能性は高くありません。また、ドキュメントのサイズと数を考慮すると、Willが考えているよりもはるかに計算コストが高くなります。 d実際に検索に興味がある。

Luceneのようなものが進むべき道です。すべてのドキュメントのインデックスを作成し、特定のドキュメントに類似するドキュメントを検索する場合は、特定のドキュメントをクエリに変換し、インデックスを検索します。内部的にLuceneは tf-idfinverted index を使用して、全体ではなく、一致する可能性のあるドキュメントの数に比例した時間をプロセス全体に与えますコレクション内のドキュメントの数。

34
Jay Kominek

それはあなたの類似の定義に依存します。

編集距離 アルゴリズムは(ラテン語)辞書の提案の標準アルゴリズムであり、テキスト全体で機能します。基本的に同じ単語(文字)が同じ順序である場合、2つのテキストは似ています。したがって、次の2つの書評はかなり似ています。

1)「これは素晴らしい本です」

2)「これらは素晴らしい本ではありません」

(削除、挿入、削除、または変更して(2)を(1)に変える文字の数は、「編集距離」と呼ばれます。)

これを実装するには、プログラムですべてのレビューにアクセスする必要があります。これはおそらく思ったほどコストがかかりません。コストが高すぎる場合は、バックグラウンドタスクとして比較を行い、データベースフィールド自体に最も類似したものを保存できます。

別のアプローチは、(ラテン)言語の構造の何かを理解することです。短い(大文字ではない、または引用された)単語を削除し、一般的または一意の単語(または接頭辞)に重みを割り当てると、ベイジアン様式の比較を行うことができます。次の2つの書評は類似しており、類似していることがわかります。

3)「フランス革命は、何とか戦争と平和の何とかフランスでした。」 -> France/French(2)Revolution(1)War(1)Peace(1)(フランスとフランス語を組み合わせるために辞書が使用されていることに注意してください)

4)「この本はなんとかフランス料理の革命です。」 ->フランス(1)革命(1)

これを実装するには、レビューが作成/更新されたときにレビューの「キーワード」を特定し、クエリのwhere句でこれらのキーワードを使用して類似のレビューを見つけます(データベースがサポートしている場合は「フルテキスト」検索が理想的です) )、おそらく、見つかった候補者を採点するために結果セットの後処理を行います。

本にもカテゴリがあります-フランスで設定されたスリラーは、フランスの歴史研究などに似ていますか?タイトルやテキストを超えたメタデータは、結果の関連性を維持するのに役立ちます。

14
Will

このチュートリアルは link 必要なもののようです。フォローするのは簡単で、とてもうまく機能します。

彼のアルゴリズムは、共通の部分文字列とそれらの部分文字列の共通の順序の両方に報酬を与えるため、同様のタイトルを非常にうまく選択する必要があります。

9
alex77

Apache Lucene完全にJavaで記述された高性能でフル機能のテキスト検索エンジンライブラリを使用して、記事のインデックスを作成することをお勧めします。全文検索を必要とするほぼすべてのアプリケーション、特にクロスプラットフォームに適した技術です。インデックスを作成すると、関連する記事を簡単に見つけることができます。

3
Guido

使用される一般的なアルゴリズムの1つは、- 自己組織化マップ です。これは、記事を自動的に分類するニューラルネットワークの一種です。次に、現在の記事がマップ内にあり、その近くのすべての記事が関連している場所を簡単に見つけることができます。アルゴリズムの重要な部分は、どのように 入力をベクトル量子化する になるかです。テキストを扱う方法はいくつかあります。ドキュメント/タイトルをハッシュ化し、単語を数え、それをn次元のベクトルなどとして使用できます。それが役に立てば幸いです。

2
mempko

SOは、質問の本文ではなく、タイトルでのみ比較を行うため、短い文字列でのみ比較します。

記事のタイトルとキーワードでアルゴリズムを使用できます(見た目はわかりません)。あなたの記事の抄録についても、燃やすより多くのCPU時間がある場合。

1
Treb

フルテキストに対するLuceneの提案の次にですが、Javaは必須ではありません; 。NETポートが利用可能ですメインLuceneページ も参照してください。これには、 Lucy、Cポート

1
b w

たぶん、あなたが探しているのは 言い換え を行うものです。私はこれについて大まかな知識しか持っていませんが、言い換えることは、テキストの2つのパッセージが実際に平均かどうかを判断するための 自然言語処理 概念です同じこと-完全に異なる単語を使用する場合がありますが。

残念ながら、あなたがこれを行うことができるツールは知りません(私はそれを見つけることに興味がありますが)

1
Vinnie

同様に傷つく言葉を探しているなら、soundexに変換して、soundexの言葉を一致させることができます...私のために働いた

0
spacemonkeys

サンプルテキストを指定すると、このプログラムはリポジトリテキストを類似性でソートしてリストします。 C++の単語の袋の単純な実装 。アルゴリズムは、サンプルテキストとリポジトリテキストの合計の長さが線形です。さらに、プログラムは、リポジトリテキストを並行して処理するマルチスレッド化されています。

コアアルゴリズムは次のとおりです。

class Statistics {
  std::unordered_map<std::string, int64_t> _counts;
  int64_t _totWords;

  void process(std::string& token);
public:
  explicit Statistics(const std::string& text);

  double Dist(const Statistics& fellow) const;

  bool IsEmpty() const { return _totWords == 0; }
};

namespace {
  const std::string gPunctStr = ".,;:!?";
  const std::unordered_set<char> gPunctSet(gPunctStr.begin(), gPunctStr.end());
}

Statistics::Statistics(const std::string& text) {
  std::string lastToken;
  for (size_t i = 0; i < text.size(); i++) {
    int ch = static_cast<uint8_t>(text[i]);
    if (!isspace(ch)) {
      lastToken.Push_back(tolower(ch));
      continue;
    }
    process(lastToken);
  }
  process(lastToken);
}

void Statistics::process(std::string& token) {
  do {
    if (token.size() == 0) {
      break;
    }
    if (gPunctSet.find(token.back()) != gPunctSet.end()) {
      token.pop_back();
    }
  } while (false);
  if (token.size() != 0) {
    auto it = _counts.find(token);
    if (it == _counts.end()) {
      _counts.emplace(token, 1);
    }
    else {
      it->second++;
    }
    _totWords++;
    token.clear();
  }
}

double Statistics::Dist(const Statistics& fellow) const {
  double sum = 0;
  for (const auto& wordInfo : _counts) {
    const std::string wordText = wordInfo.first;
    const double freq = double(wordInfo.second) / _totWords;
    auto it = fellow._counts.find(wordText);
    double fellowFreq;
    if (it == fellow._counts.end()) {
      fellowFreq = 0;
    }
    else {
      fellowFreq = double(it->second) / fellow._totWords;
    }
    const double d = freq - fellowFreq;
    sum += d * d;
  }
  return std::sqrt(sum);
}
0
Serge Rogatch

SQL Serverのフルテキストインデックスを使用してスマートな比較を取得できます。SOはajax呼び出しを使用し、同様の質問を返すクエリを実行すると考えています。

どのテクノロジーを使用していますか?

0
Mitchel Sellers

@ alex77の回答のリンクは、 Sorensen-Dice Coefficient を指しています。これは、その記事の著者が独自に発見したものです。記事は非常によく書かれており、読む価値があります。

私は自分のニーズに合わせてこの係数を使用することになりました。ただし、元の係数は、処理時に誤った結果をもたらす可能性があります

  • 1つのスペルミスを含む3文字の単語ペア。 [and,AMD]および
  • アナグラムである3文字の単語ペア。 [and,dan]

最初のケースでは、ダイスは誤ってゼロの係数を報告しますが、2番目のケースでは、係数は誤って高い0.5になります。

改善 提案されています これは本質的に、Wordの最初と最後の文字を取得し、追加のバイグラムを作成することで構成されます。

私の見解では、改善は本当に3文字の単語に対してのみ必要です-長い単語では、他のバイグラムには問題をカバーするバッファリング効果があります。この改善を実装する私のコードを以下に示します。

function wordPairCount(Word)
{
 var i,rslt = [],len = Word.length - 1;
 for(i=0;i < len;i++) rslt.Push(Word.substr(i,2));
 if (2 == len) rslt.Push(Word[0] + Word[len]);
 return rslt;
}

function pairCount(arr)
{
 var i,rslt = [];
 arr = arr.toLowerCase().split(' ');
 for(i=0;i < arr.length;i++) rslt = rslt.concat(wordPairCount(arr[i]));
 return rslt;
}

function commonCount(a,b)
{
 var t;
 if (b.length > a.length) t = b, b = a, a = t; 
 t = a.filter(function (e){return b.indexOf(e) > -1;});
 return t.length;
}

function myDice(a,b)
{
 var bigrams = [],
 aPairs = pairCount(a),
 bPairs = pairCount(b);
 debugger;
 var isct = commonCount(aPairs,bPairs);
 return 2*commonCount(aPairs,bPairs)/(aPairs.length + bPairs.length); 
}

$('#rslt1').text(myDice('WEB Applications','PHP Web Application'));
$('#rslt2').text(myDice('And','Dan'));
$('#rslt3').text(myDice('and','AMD'));
$('#rslt4').text(myDice('abracadabra','abracabadra'));
*{font-family:arial;}
table
{
 width:80%;
 margin:auto;
 border:1px solid silver;
}

thead > tr > td
{
 font-weight:bold;
 text-align:center;
 background-color:aqua;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.0.0/jquery.min.js"></script>
<table>
<thead>
<tr>
<td>Phrase 1</td>
<td>Phrase 2</td>
<td>Dice</td>
</tr>
<thead>
<tbody>
<tr>
<td>WEB Applications</td>
<td>PHP Web Application</td>
<td id='rslt1'></td>
</tr>
<tr>
<td>And</td>
<td>Dan</td>
<td id='rslt2'></td>
</tr>
<tr>
<td>and</td>
<td>AMD</td>
<td id='rslt3'></td>
</tr>
<tr>
<td>abracadabra</td>
<td>abracabadra</td>
<td id='rslt4'></td>
</tr>
</tbody>
</table>

最後の例の意図的なスペルミスに注意してください:abracadabravs abracabadra 。追加のバイグラム補正は適用されませんが、報告される係数は0.9です。修正すると、0.91になります。

うまくいけば、これがこのスレッドに出くわした人たちの助けになることを願っています。

0
DroidOS

いくつかの方法を試してみましたが、どれもうまくいきませんでした。次のような比較的満足のいく結果が得られる場合があります。 2番目:SimHashコードのインデックス。 3番目:上記のように比較するテキストを処理し、SimHashコードを取得し、SimHashインデックスで5-10のようなハミング距離を隔てるすべてのテキストを検索します。次に、単純性と項ベクトルを比較します。これは、ビッグデータで機能する場合があります。

0
Luna_one

次のいずれかを使用できます1)Minhash/LSH https://en.wikipedia.org/wiki/MinHash

(参照: http://infolab.stanford.edu/~ullman/mmds/book.pdf

または

2)協調フィルタリング: https://en.wikipedia.org/wiki/Collaborative_filtering

0
alex