查找相似文本文档的最佳算法是什么?

机器算法验证 聚类 数据挖掘 相似之处 信息检索
2022-04-07 04:31:36

我有很多文本文档,我想在我的数据集中找到与每个文档相似的文档。潜在狄利克雷分配(LDA)做到这一点的最佳方式,还是有其他可能更好的算法?有任何想法吗?

由于我的数据集很大,我宁愿使用Apache Mahout来查找类似的文档。

4个回答

在非常大的文档集中找到相似的文档是局部敏感散列(LSH)。要点的一个指示是:“LSH 的一种一般方法是对项目进行多次“散列”,这样类似的项目比不同的项目更有可能被散列到同一个桶中。

有关详细信息,请参阅:http: //infolab.stanford.edu/~ullman/mmds/ch3a.pdf

为什么要集群?为什么选择 LDA?太昂贵、太复杂且不可靠。让我向您介绍一种令人难以置信的新技术来查找类似文档:

文本搜索

有一个很棒的工具叫做 Apache Lucene,它也可以让文本搜索快速且易于集成。

我在过去看到用于信息检索应用程序的一种很好的技术是将您的目标文档和查询文档组合在一起,然后将Jaccard 相似性应用于一组带状疱疹。

Shingling是一个过程,它在文档中的字符上采用滑动窗口,将字符串表示为字符集k-出现在强的克。现在我们有了一组文档表示,我们可以使用它们的 Jaccard 相似度来比较它们。

例如,字符串的 2-shingles"racecar"将是 set {ra, ac, ce, ec, ca, ar}

下面是一些简单的 Python 代码来说明这一点:

def shingle(doc, k=3):
    return { doc[i:i+k] for i in range(0, len(doc) - k + 1) }

def jaccard(a, b):
    return 1.0 * len(a.intersection(b)) / len(a.union(b))

documents = [...] # List of strings
query = "this week on twitter..."
query_shingles = shingle(query)

best_doc = -1
best_score = float("-inf")
for i, doc in enumerate(documents):
    doc_shingles = shingle(doc)
    similarity = jaccard(doc_shingles, query_shingles)
    if similarity > best_score:
        best_score = similarity
        best_doc = i

这个想法是具有相似的文档k-shingles 具有相似的内容,具有良好选择的值k. 对于大多数应用程序k几乎总是在 2 和 4 之间(3 是我见过的最流行的选择)。

有替代配方,例如w-shingling 使用单词n-grams 而不是字符n-grams,但想法仍然相同。

该程序使用词频分析(“词袋”)并按欧几里得距离到样本文本的频率向量对存储库文本进行排序。

给定一个示例文本,该程序列出了按相似度排序的存储库文本: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);
}

正如您在 GitHub 上的示例中所见,它已在 Project Gutenberg 书籍上进行了测试。