You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

百万级文档语料库最高相似度分数的快速计算方案咨询

大规模文档历史最高余弦相似度计算优化方案

针对你用quanteda处理超300万文档、400万特征的dfm矩阵,计算单文档与历史所有文档最高余弦相似度的性能问题,以下是几个可行的优化方向:

1. 用近似最近邻(ANN)替代全量相似度计算

因为你只需要最高相似度的文档,不需要计算所有历史文档的相似度,近似最近邻算法能在O(log n)时间内找到最相似的文档,比全量计算的O(n)效率提升几个数量级。

实现思路:

  • 先将所有文档按日期排序,确保处理顺序和时间先后一致;
  • 提前对文档向量做L2归一化(余弦相似度等价于归一化后的内积);
  • 逐步构建ANN索引:每处理完一个文档,就将其加入索引,后续文档直接从索引中查询最近邻。

R代码示例(用RcppAnnoy):

library(quanteda)
library(RcppAnnoy)

# 1. 预处理:按日期排序并归一化dfm
dfm_sorted <- dfm_sort(dfm, by = "date")  # 假设dfm带有date文档变量
dfm_normalized <- dfm_weight(dfm_sorted, scheme = "l2")

# 2. 初始化Annoy索引(余弦相似度对应Angular距离)
dim <- nfeat(dfm_normalized)
annoy_index <- new(AnnoyAngular, dim)

# 3. 遍历文档,逐步构建索引并查询
similarity_res <- data.frame(id = character(), highest_similarity = numeric(), stringsAsFactors = FALSE)

for (i in 1:nrow(dfm_sorted)) {
  # 获取当前文档信息
  doc_id <- docvars(dfm_sorted, "id")[i]
  doc_vec <- as.numeric(dfm_normalized[i, ])
  
  # 如果不是第一个文档,查询索引中的最近邻
  if (i > 1) {
    # 查询top1最近邻的距离(Angular距离与余弦相似度的转换:cos_sim = 1 - (distance^2)/2)
    nearest <- annoy_index$get_nns_by_vector(doc_vec, 1, search_k = 1000)  # search_k越大结果越准
    distance <- annoy_index$get_distance(nearest[[1]], i-1)  # Annoy索引从0开始计数
    cos_sim <- 1 - (distance^2)/2
  } else {
    cos_sim <- NA  # 第一个文档无历史数据
  }
  
  # 将当前文档加入索引
  annoy_index$add_item(i-1, doc_vec)
  
  # 保存结果
  similarity_res <- rbind(similarity_res, data.frame(id = doc_id, highest_similarity = cos_sim))
  
  # 每10000个文档优化一次索引(可选,提升后续查询速度)
  if (i %% 10000 == 0) {
    annoy_index$build(10)  # 树的数量,越大越准但占用内存更多
  }
}

2. 预归一化减少计算量

余弦相似度的计算公式为:
cos_sim(A,B) = (A·B) / (||A|| * ||B||)
如果提前对所有文档向量做L2归一化(使||A||=1、||B||=1),那么余弦相似度直接等于向量内积,省去了每次计算范数的额外开销。

用quanteda实现归一化:

dfm_normalized <- dfm_weight(dfm, scheme = "l2")

之后使用textstat_simil计算余弦相似度时,会自动利用归一化后的结果,计算速度会显著提升。

3. 分块处理历史文档

如果内存无法容纳全量历史dfm,可以将历史文档按日期分块,每次只加载一个块计算相似度,记录当前最大值,遍历所有块后得到最终的最高相似度。

示例代码:

library(quanteda)

# 按月份对日期分块
date_blocks <- split(docvars(dfm, "date"), format(docvars(dfm, "date"), "%Y-%m"))

similarity_res <- vector("list", nrow(to_find_docs))

for(row_i in 1:nrow(to_find_docs)){
  id_i <- to_find_docs$id[row_i]
  date_i <- to_find_docs$date[row_i]
  
  max_sim <- 0
  
  # 遍历所有早于date_i的块
  for (block_date in names(date_blocks)) {
    block_start <- as.Date(paste0(block_date, "-01"))
    block_end <- block_start %m+% months(1)
    if (block_start < date_i) {
      # 提取块内文档
      block_dfm <- dfm_subset(dfm, date >= block_start & date < block_end)
      # 计算相似度
      sim <- textstat_simil(block_dfm, dfm[id_i,], margin = "documents", method = "cosine")
      current_max <- max(sim, na.rm = TRUE)
      if (current_max > max_sim) max_sim <- current_max
    }
  }
  
  similarity_res[[row_i]] <- data.frame(id = id_i, highest_similarity = ifelse(max_sim == 0, NA, max_sim))
}

similarity_res <- do.call(rbind, similarity_res)

4. 并行计算加速循环

利用R的多核并行能力,将循环拆分到多个CPU核心处理,减少总耗时。建议结合分块处理使用,避免内存过载。

示例代码(用foreach):

library(quanteda)
library(foreach)
library(doParallel)

# 初始化并行集群(留一个核心给系统)
cl <- makeCluster(detectCores() - 1)
registerDoParallel(cl)

similarity_res <- foreach(row_i = 1:nrow(to_find_docs), .packages = "quanteda", .combine = rbind) %dopar% {
  id_i <- to_find_docs$id[row_i]
  date_i <- to_find_docs$date[row_i]
  
  one_simil <- textstat_simil(
    dfm_subset(dfm, date < date_i ),
    dfm[id_i,],
    margin = "documents", method = "cosine"
  )
  
  data.frame(id = id_i, highest_similarity = max(one_simil, na.rm = TRUE))
}

# 关闭并行集群
stopCluster(cl)

内容的提问来源于stack exchange,提问作者Ding Li

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.19 17:36:23