将文档与 text2vec 匹配——缩放问题

Matching documens with text2vec -- scaling problems

我在缩放文本匹配程序时遇到了一些问题。我正在使用 text2vec,它提供了非常好的和快速的结果。

我遇到的主要问题是处理由 text2vec::sim2() 函数返回的大矩阵。

首先,我的硬件/OS 设置的一些细节:Windows 7 有 12 个内核,大约 3.5 GHz 和 128 Gb 内存。很不错的机器。

其次,我的 R 程序试图实现的一些基本细节。

我们有一个数据库,其中包含地址中每个房屋/企业的 1000 万个唯一规范地址。这些参考地址还包含每个条目的经纬度信息。

我正在尝试将这些参考地址与我们数据库中的客户地址相匹配。我们有大约 600,000 个客户地址。这些客户地址的质量不好。一点都不好!它们存储为单个字符串字段,对输入进行绝对零检查。

匹配这些地址的技术策略非常简单。创建客户地址和参考地址的两个文档术语矩阵 (DTM),并使用余弦相似度来查找与特定客户地址最相似的参考地址。一些客户地址非常差,导致余弦相似度非常低——因此,对于这些地址,将分配 "no match"。

尽管这是一个非常简单的解决方案,但获得的结果非常令人鼓舞。

但是,我在扩展方面遇到了问题....?我想知道是否有人有任何建议。

下面是我的代码的副本。它非常简单。显然,我不能包含真实数据,但它应该让读者清楚地了解我正在尝试做什么。

A 部分 - 即使在完整的 600,000 * 1000 万输入数据集上也能很好地工作。

SECTION B - text2vec::sim2() 函数导致 R studio 在词汇量超过大约 140,000 个标记(即列)时关闭。为避免这种情况,我以大约 200 个为单位处理客户地址。

C 部分 - 这是最昂贵的部分。当以 200 个块为单位处理地址时,SECTION A 和 SECTION B 大约需要 2 分钟。但是 C 部分,使用(我认为是超级快速的函数)大约需要 5 分钟来处理一个 1000 万行 * 200 列的矩阵。

合并后,SECIONS A:C 处理 200 个地址大约需要 7 分钟。由于有 600,000 个地址需要处理,这将需要大约 14 天的时间来处理。

它们是让这段代码 运行 更快的想法吗...?

rm(list = ls())
library(text2vec)
library(dplyr)


# Create some test data

# example is 10 entries.  
# but in reality we have 10 million addresses
vct_ref_address <- c("15 smith street beaconsfield 2506 NSW", 
"107 orange grove linfield 2659 NSW",
"88 melon drive calton 3922 VIC", 
"949 eyre street sunnybank 4053 QLD",
"12 black avenue kingston 2605 ACT", 
"5 sweet lane 2004 wynyard NSW",
"32 mugga way 2688 manuka ACT",
"4 black swan avenue freemantle 5943 WA",
"832 big street narrabeet 2543 NSW", 
"5 dust road 5040 NT")


# example is 4 entries
# but in reality, we have 1.5 million addresses
vct_test_address <- c("949 eyre street sunnybank 4053 QLD",  
"1113 completely invalid suburb with no post code QLD", 
"12 black road kingston 2605 ACT",  
"949 eyre roaod sunnybank 4053 QLD" )

# ==========================
# SECTION A ===== prepare data
# A.1 create vocabulary 
t2v_token <- text2vec::itoken(c(vct_test_address, vct_ref_address),  progressbar = FALSE)
t2v_vocab <- text2vec::create_vocabulary(t2v_token)
t2v_vectorizer <- text2vec::vocab_vectorizer(t2v_vocab)
# A.2 create document term matrices dtm
t2v_dtm_test <- text2vec::create_dtm(itoken(vct_test_address, progressbar = FALSE), t2v_vectorizer)
t2v_dtm_reference <- text2vec::create_dtm(itoken(vct_ref_address, progressbar = FALSE), t2v_vectorizer)

# ===========================
# SECTION B ===== similarity matrix
mat_sim <- text2vec::sim2(t2v_dtm_reference, t2v_dtm_test,  method = 'cosine', norm = 'l2')

# ===========================
# SECTION C ===== process matrix
vct_which_reference <- apply(mat_sim, 2, which.max)
vct_sim_score <- apply(mat_sim, 2, max)

# ============================
# SECTION D ===== apply results
# D.1 assemble results
df_results <- data.frame(
test_addr = vct_test_address,
matched_addr = vct_ref_address[vct_which_reference],
similarity =  vct_sim_score )

# D.2 print results
df_results %>% arrange(desc(similarity))

步骤 C 中的问题是 mat_sim 是稀疏的,所有 apply 调用都进行 column/row 子集化,这非常慢(并将稀疏向量转换为密集向量)。

可能有几种解决方案:

  1. 如果 mat_sim 不是很大,请使用 as.matrix 转换为密集型,然后使用 apply
  2. 更好的是,您可以使用 as(mat_sim, "TsparseMatrix")mat_sim 转换为三元组格式的稀疏矩阵,然后使用 data.table 获取最大元素的索引。这是一个例子:

    library(text2vec)
    library(Matrix)
    data("movie_review")
    it = itoken(movie_review$review, tolower, word_tokenizer)
    dtm = create_dtm(it, hash_vectorizer(2**14))
    
    
    mat_sim = sim2(dtm[1:100, ], dtm[101:5000, ])
    mat_sim = as(mat_sim, "TsparseMatrix")
    
    library(data.table)
    
    # we add 1 because indices in sparse matrices in Matrix package start from 1
    mat_sim_dt = data.table(row_index = mat_sim@i + 1L, col_index = mat_sim@j + 1L, value = mat_sim@x)
    
    res = mat_sim_dt[, 
            { k = which.max(value); list(max_sim = value[[k]], row_index = row_index[[k]]) }, 
            keyby = col_index]  
    res
    

另外作为附带建议 - 我建议尝试 char_tokenizer() 使用 ngram(例如大小 c(3, 3))以 "fuzzy" 匹配地址的不同拼写和缩写。