高效的字符串相似度分组

2023-12-24

Setting: 我有有关人员及其父母姓名的数据,并且我想找到兄弟姐妹(父母姓名相同的人)。

 pdata<-data.frame(parents_name=c("peter pan + marta steward",
                                 "pieter pan + marta steward",
                                 "armin dolgner + jane johanna dough",
                                 "jack jackson + sombody else"))

这里的预期输出将是一列,表明前两个观测值属于 X 族,而第三列和第四列分别属于一个单独的族。例如:

person_id    parents_name                           family_id
1            "peter pan + marta steward",           1
2            "pieter pan + marta steward",          1
3            "armin dolgner + jane johanna dough",  2
4            "jack jackson + sombody else"          3

目前的方法: 我对距离度量很灵活。目前,我使用 Levenshtein 编辑距离来匹配 obs,允许两个字符的差异。但是其他变体,例如“最大公共子字符串”,如果它们运行得更快,那就没问题了。

对于较小的子样本,我使用stringdist::stringdist在循环中或stringdist::stringdistmatrix,但随着样本量的增加,这变得越来越低效。

一旦使用了一定的样本量,矩阵版本就会爆炸。我的循环尝试效率极低:

#create data of the same complexity using random last-names
#(4mio obs and ~1-3 kids per parents) 
pdata<-data.frame(parents_name=paste0(rep(c("peter pan + marta ",
                                "pieter pan + marta ",
                                "armin dolgner + jane johanna ",
                                "jack jackson + sombody "),1e6),stringi::stri_rand_strings(4e6, 5)))

for (i in 1:nrow(pdata)) {
  similar_fatersname0<-stringdist::stringdist(pdata$parents_name[i],pdata$parents_name[i:nrow(pdata)],nthread=4)<2
  #[create grouping indicator]
}

我的问题:应该会显着提高效率,例如因为一旦我发现它们在更容易评估的东西上有足够的不同,我就可以停止比较字符串,例如。字符串长度,或第一个单词。字符串长度变体已经可以工作并将复杂性降低约 3 倍。但这还太少了。任何减少计算时间的建议都值得赞赏。

Remarks:

  • 这些字符串实际上采用 unicode,而不是拉丁字母 (Devnagari)
  • 完成预处理以删除未使用的字符等

有两个挑战:

A. Levenshtein distance的并行执行——而不是顺序循环

B. 比较次数:如果我们的源列表有 400 万个条目,理论上我们应该运行 16 万亿次 Levenstein 距离度量,这是不现实的,即使我们解决了第一个挑战。

为了使我对语言的使用更加清晰,以下是我们的定义

  • 我们想要测量表达式之间的编辑距离。
  • 每个表达式都有两个部分,父级 A 全名和父级 B 全名,用加号分隔
  • 各部分的顺序很重要(即,如果表达式 1 的父级 A = 表达式 2 的父级 A 且父级 B 或表达式 1= 表达式 2 的父级 B,则两个表达式 (1, 2) 是相同的。如果父级表达式 1 的 A = 表达式 2 的父级 B 且表达式 1 的父级 B = 表达式 2 的父级 A)
  • 部分(或全名)是一系列单词,由空格或破折号分隔,对应于一个人的名字和姓氏
  • 我们假设一个部分中的最大单词数为 6(您的示例有 2 或 3 个单词的部分,我假设我们最多可以有 6 个) 部分中的单词顺序很重要(该部分始终是名字后面是姓氏,而不是姓氏在前,例如 Jack John 和 John Jack 是两个不同的人)。
  • 有400万个表达方式
  • 假定表达式仅包含英文字符。数字、空格、标点符号、破折号和任何非英文字符都可以忽略
  • 我们假设简单匹配已经完成(如精确表达式匹配),并且我们不必搜索精确匹配

从技术上讲,目标是在 400 万个表达式列表中找到一系列匹配的表达式。如果两个表达式的 Levenstein 距离小于 2,则它们被视为匹配表达式。

实际上,我们创建了两个列表,它们是最初 400 万个表达式列表的精确副本。我们称之为左列表和右列表。在复制列表之前,每个表达式都会被分配一个表达式 id。 我们的目标是找到 Right 列表中与 Left 列表的条目的 Levenstein 距离小于 2 的条目,不包括相同的条目(相同的表达式 id)。

我建议采用两步法来分别解决这两个挑战。第一步将减少可能匹配表达式的列表,第二步将简化 Levenstein 距离测量,因为我们只查看非常接近的表达式。使用的技术是任何传统的数据库服务器,因为我们需要对数据集进行索引以提高性能。

挑战A

挑战 A 包括减少距离测量的数量。我们从最大值大约开始。 16万亿(400万的2次方),我们不应该超过几千万或几亿。 这里使用的技术包括在完整的表达式中搜索至少一个相似的单词。根据数据的分布方式,这将大大减少可能的匹配对的数量。或者,根据结果所需的准确性,我们还可以搜索具有至少两个相似单词或至少一半相似单词的对。

从技术上讲,我建议将表达式列表放在表格中。添加标识列以为每个表达式创建唯一的 ID,并创建 12 个字符列。然后解析表达式并将每个部分的每个单词放在单独的列中。这看起来像(我没有表示所有 12 列,但想法如下):

|id | expression | sect_a_w_1 | sect_a_w_2 | sect_b_w_1 |sect_b_w_2 |
|1 | peter pan + marta steward | peter | pan | marta |steward      |

虽然有空列(因为 12 个单词的表达很少),但这并不重要。

然后我们复制该表并在每个部分...列上创建一个索引。 我们运行 12 个连接来尝试查找相似的单词,例如

SELECT L.id, R.id 
FROM left table L JOIN right table T 
ON L.sect_a_w_1 = R.sect_a_w_1
AND L.id <> R.id 

我们收集 12 个临时表中的输出,并对这 12 个表运行联合查询,以获取所有表达式的简短列表,这些表达式具有至少一个相同单词的潜在匹配表达式。这是我们的挑战 A 的解决方案。我们现在有一个最可能匹配对的简短列表。该列表将包含数百万条记录(左右条目对),但不是数十亿条。

挑战B

挑战 B 的目标是批量处理简化的 Levenstein 距离(而不是循环运行)。 首先,我们应该就什么是简化的莱文斯坦距离达成一致。 首先我们同意两个表达式的levenstein距离是两个表达式中具有相同索引的所有单词的levenstein距离之和。我的意思是两个表达式的 Levenstein 距离是它们的两个第一个单词的距离,加上它们的两个第二个单词的距离,等等。 其次,我们需要发明一个简化的莱文斯坦距离。我建议使用 n-gram 方法,仅包含 2 个字符的克,其索引绝对差小于 2 。

例如peter和pieter之间的距离计算如下

Peter       
1 = pe          
2 = et          
3 = te          
4 = er
5 = r_           

Pieter
1 = pi
2 = ie
3 = et
4 = te
5 = er
6 = r_ 

Peter 和 Pieter 有 4 个常见的 2-gram,索引绝对差小于 2 'et'、'te'、'er'、'r_'。两个单词中最大的一个有 6 个可能的 2-gram,则距离为 6-4 = 2 - Levenstein 距离也将为 2,因为有 1 个“eter”移动和一个字母插入“i”。

这是一个近似值,并不适用于所有情况,但我认为在我们的情况下它会工作得很好。如果我们对结果的质量不满意,我们可以尝试使用 3 克或 4 克或允许大于 2 克的序列差异。但其想法是每对执行的计算量比传统的 Levenstein 算法要少得多。

然后我们需要将其转化为技术解决方案。我之前所做的如下: 首先隔离单词:由于我们只需要测量单词之间的距离,然后将每个表达式的这些距离相加,我们可以通过在单词列表上运行不同的选择来进一步减少计算次数(我们已经准备好了单词列表)上一节中的单词)。

这种方法需要一个映射表来跟踪表达式id、部分id、单词id和单词的单词序列号,以便可以在过程结束时计算原始表达式距离。

然后我们得到一个更短的新列表,并且包含与 2 克距离度量相关的所有单词的交叉连接。 然后我们想要批量处理这个 2-gram 距离测量,我建议在 SQL 连接中进行。这需要一个预处理步骤,其中包括创建一个新的临时表,将每个 2-gram 存储在单独的行中,并跟踪单词 Id、单词序列和部分类型

从技术上讲,这是通过使用一系列(或循环)子字符串选择对单词列表进行切片来完成的,如下所示(假设单词列表表 - 有两个副本,一个左副本和一个右副本 - 包含 2 列 word_id 和 word):

INSERT INTO left_gram_table (word_id, gram_seq, gram)
SELECT word_id, 1 AS gram_seq, SUBSTRING(word,1,2) AS gram
FROM left_word_table 

进而

INSERT INTO left_gram_table (word_id, gram_seq, gram)
SELECT word_id, 2 AS gram_seq, SUBSTRING(word,2,2) AS gram
FROM left_word_table 

Etc.

使“steward”看起来像这样的东西(假设单词 id 是 152)

|  pk  | word_id | gram_seq | gram | 
|  1   |  152       |  1          | st |
|  2   |  152       |  2          | te |
|  3   |  152       |  3          |  ew |
|  4   |  152       |  4          |  wa |
|  5   |  152       |  5          |  ar |
|  6   |  152       |  6          |  rd |
|  7   |  152       |  7          |  d_ |

不要忘记在 word_id、gram 和 gram_seq 列上创建索引,距离可以通过左右 gram 列表的连接来计算,其中 ON 看起来像

ON L.gram = R.gram 
AND ABS(L.gram_seq + R.gram_seq)< 2 
AND L.word_id <> R.word_id 

距离是两个单词中最长的单词的长度减去匹配克的数量。 SQL 进行此类查询的速度非常快,我认为一台具有 8 GB RAM 的简单计算机可以在合理的时间范围内轻松执行数亿行。

然后只需连接映射表即可计算每个表达式中单词到单词的距离之和,从而得到总的表达式到表达式的距离。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

高效的字符串相似度分组 的相关文章

随机推荐