R:查找并计算嵌套在列表中的字符向量之间按位置(添加、减去或替换的一个元素)的所有差异
R: find and count all differences by position (of one element added, subtracted or substituted) between character vectors nested in a list
我有一个字符向量列表,表示按音素拆分的单词:
> head(words)
[[1]]
[1] "UU"
[[2]]
[1] "EY" "Z"
[[3]]
[1] "T" "R" "IH" "P" "UU" "L" "EY"
[[4]]
[1] "AA" "B" "ER" "G"
[[5]]
[1] "AA" "K" "UU" "N"
[[6]]
[1] "AA" "K" "ER"
对于列表中的每个单词,我想找到与所考虑的单词相差一个音素(添加、减去或替换一个音素)并且具有相同数量的单词的数量
相同位置的音素。
从这个意义上说,对于单词 "EY" "Z"
可接受的情况是:
[1] "M" "EY" "Z"
[1] "AY" "Z"
[1] "EY" "D"
[1] "EY" "Z" "AH"
但应拒绝以下情况:
[1] "EY" "D" "Z"
[1] "Z" "EY" "D"
[1] "HH" "EY"
基本上,我想找出一个元素在向量中音素位置方面的差异。
目前,我找到的最佳解决方案是:
diffs <- c()
for (i in seq_along(words)) {
diffs <- c(diffs, sum(sapply(words, function(y) {
count <- 0
elements <- list(words[[i]], y)
len <- c(length(words[[i]]), length(y))
if (identical(elements[which(len==max(len))][[1]][-1], elements[which(len==min(len))][[1]]) == 1) {
count + identical(elements[which(len==max(len))][[1]][-1], elements[which(len==min(len))][[1]])
} else {
length(elements[which(len==min(len))][[1]]) <- length(elements[which(len==max(len))][[1]])
elements <- rapply(elements, f=function(x) ifelse(is.na(x),"$$",x), how="replace" )
count + sum(elements[[1]] != elements[[2]])
}
})== 1))
}
但是,这个解决方案需要很长时间,因为我的列表 words
有 120.000 个元素 (words/vectors),所以我想问问你是否知道其他解决方案来加速这个过程。
非常感谢您的回答
这是一个使用编辑距离和 Wagner-Fischer 算法的版本。
vecLeven <- function(s, t) {
d <- matrix(0, nrow = length(s) + 1, ncol=length(t) + 1)
d[, 1] <- (1:nrow(d)) - 1
d[1,] <- (1:ncol(d))-1
for (i in 1:length(s)) {
for (j in 1:length(t)) {
d[i+1, j+1] <- min(
d[i, j+1] + 1, # deletion
d[i+1, j] + 1, # insertion
d[i, j] + if (s[i] == t[j]) 0 else 1 # substitution
)
}
}
d[nrow(d), ncol(d)]
}
onediff <- sapply(words[1:10], function(x) {
lengthdiff <- sapply(words, function(word) abs(length(word) - length(x)))
sum(sapply(words[lengthdiff == 0], function(word) sum(word != x) == 1)) +
sum(mapply(vecLeven, list(x), words[lengthdiff == 1]) == 1)
})
我在 CMU 词典上测试了两个版本,它们的大小相似。它比您的版本快一点(大约 30 秒而不是 10 个单词的 50 秒),并且应该很好地并行化。尽管如此,运行 它在完整的数据集上仍需要几天时间。
一个重要的性能因素是所有对都计算两次,一次用于第一个单词,一次用于第二个;相反,进行查找会将其减半。但是,有超过 70 亿对,因此您需要一个数据库来存储它们。
因此,这里的关键是根据单词的长度将单词分开,这样我们就可以仅在感兴趣的子集上测试每个假设 (substitution/addition/deletion)。
get_one_diff <- function(words) {
K <- max(le <- lengths(words))
i_chr <- as.character(seq_len(K))
words.spl <- split(words, le)
test_substitution <- function(i) {
word1 <- words[[i]]
do.call(sum, lapply(words.spl[[i_chr[le[i]]]], function(word2) {
sum(word1 != word2) == 1
}))
}
test_addition <- function(i) {
if ((le <- le[i]) == K) return(0)
word1 <- words[[i]]
do.call(sum, lapply(words.spl[[i_chr[le + 1]]], function(word2) {
isOneDiff(word1, word2)
}))
}
test_deletion <- function(i) {
if ((le <- le[i]) == 1) return(0)
word1 <- words[[i]]
do.call(sum, lapply(words.spl[[i_chr[le - 1]]], function(word2) {
isOneDiff(word2, word1)
}))
}
sapply(seq_along(words), function(i) {
test_substitution(i) + test_addition(i) + test_deletion(i)
})
}
其中 isOneDiff
是一个 Rcpp 函数:
#include <Rcpp.h>
using namespace Rcpp;
// [[Rcpp::export]]
bool isOneDiff(const StringVector& w1,
const StringVector& w2) {
int i, n = w1.size();
for (i = 0; i < n; i++) if (w1[i] != w2[i]) break;
for ( ; i < n; i++) if (w1[i] != w2[i+1]) return false;
return true;
}
这比您的版本快 20 倍,因为它只是一个 sapply
,可以很容易地并行化。
还有一个不同的答案,使用常规 Levenshtein 距离(即允许在任何点插入),但这次是快速 - 15 秒内快速 1000 个单词。
诀窍是使用 R 包中提供的一种快速 Levenshtein 实现;在这种情况下,我使用的是 stringdist
但任何一个都应该有效。问题是它们对字符串和字符进行操作,而不是对多字符音素表示进行操作。但是有一个简单的解决方案:因为字符比音素多,我们可以将音素翻译成单个字符。生成的字符串作为音位转录是不可读的,但作为邻域密度算法的输入工作得很好。
library(stringdist)
phonemes <- unique(unlist(words))
# add a few buffer characters
targets <- c(letters, LETTERS, 0:9, "!", "§", "%", "&", "/", "=",
"#")[1:length(phonemes)]
ptmap <- targets
names(ptmap) <- phonemes
wordsT <- sapply(words, function(i) paste0(ptmap[i], collapse=""))
wordlengths <- nchar(wordsT)
onediffs.M <- function(x) {
lengthdiff <- abs(wordlengths - nchar(x))
sum(stringdist(x, wordsT[lengthdiff == 0], method="hamming") == 1) +
sum(stringdist(x, wordsT[lengthdiff == 1], method="lv") == 1)
}
我有一个字符向量列表,表示按音素拆分的单词:
> head(words)
[[1]]
[1] "UU"
[[2]]
[1] "EY" "Z"
[[3]]
[1] "T" "R" "IH" "P" "UU" "L" "EY"
[[4]]
[1] "AA" "B" "ER" "G"
[[5]]
[1] "AA" "K" "UU" "N"
[[6]]
[1] "AA" "K" "ER"
对于列表中的每个单词,我想找到与所考虑的单词相差一个音素(添加、减去或替换一个音素)并且具有相同数量的单词的数量
相同位置的音素。
从这个意义上说,对于单词 "EY" "Z"
可接受的情况是:
[1] "M" "EY" "Z"
[1] "AY" "Z"
[1] "EY" "D"
[1] "EY" "Z" "AH"
但应拒绝以下情况:
[1] "EY" "D" "Z"
[1] "Z" "EY" "D"
[1] "HH" "EY"
基本上,我想找出一个元素在向量中音素位置方面的差异。 目前,我找到的最佳解决方案是:
diffs <- c()
for (i in seq_along(words)) {
diffs <- c(diffs, sum(sapply(words, function(y) {
count <- 0
elements <- list(words[[i]], y)
len <- c(length(words[[i]]), length(y))
if (identical(elements[which(len==max(len))][[1]][-1], elements[which(len==min(len))][[1]]) == 1) {
count + identical(elements[which(len==max(len))][[1]][-1], elements[which(len==min(len))][[1]])
} else {
length(elements[which(len==min(len))][[1]]) <- length(elements[which(len==max(len))][[1]])
elements <- rapply(elements, f=function(x) ifelse(is.na(x),"$$",x), how="replace" )
count + sum(elements[[1]] != elements[[2]])
}
})== 1))
}
但是,这个解决方案需要很长时间,因为我的列表 words
有 120.000 个元素 (words/vectors),所以我想问问你是否知道其他解决方案来加速这个过程。
非常感谢您的回答
这是一个使用编辑距离和 Wagner-Fischer 算法的版本。
vecLeven <- function(s, t) {
d <- matrix(0, nrow = length(s) + 1, ncol=length(t) + 1)
d[, 1] <- (1:nrow(d)) - 1
d[1,] <- (1:ncol(d))-1
for (i in 1:length(s)) {
for (j in 1:length(t)) {
d[i+1, j+1] <- min(
d[i, j+1] + 1, # deletion
d[i+1, j] + 1, # insertion
d[i, j] + if (s[i] == t[j]) 0 else 1 # substitution
)
}
}
d[nrow(d), ncol(d)]
}
onediff <- sapply(words[1:10], function(x) {
lengthdiff <- sapply(words, function(word) abs(length(word) - length(x)))
sum(sapply(words[lengthdiff == 0], function(word) sum(word != x) == 1)) +
sum(mapply(vecLeven, list(x), words[lengthdiff == 1]) == 1)
})
我在 CMU 词典上测试了两个版本,它们的大小相似。它比您的版本快一点(大约 30 秒而不是 10 个单词的 50 秒),并且应该很好地并行化。尽管如此,运行 它在完整的数据集上仍需要几天时间。
一个重要的性能因素是所有对都计算两次,一次用于第一个单词,一次用于第二个;相反,进行查找会将其减半。但是,有超过 70 亿对,因此您需要一个数据库来存储它们。
因此,这里的关键是根据单词的长度将单词分开,这样我们就可以仅在感兴趣的子集上测试每个假设 (substitution/addition/deletion)。
get_one_diff <- function(words) {
K <- max(le <- lengths(words))
i_chr <- as.character(seq_len(K))
words.spl <- split(words, le)
test_substitution <- function(i) {
word1 <- words[[i]]
do.call(sum, lapply(words.spl[[i_chr[le[i]]]], function(word2) {
sum(word1 != word2) == 1
}))
}
test_addition <- function(i) {
if ((le <- le[i]) == K) return(0)
word1 <- words[[i]]
do.call(sum, lapply(words.spl[[i_chr[le + 1]]], function(word2) {
isOneDiff(word1, word2)
}))
}
test_deletion <- function(i) {
if ((le <- le[i]) == 1) return(0)
word1 <- words[[i]]
do.call(sum, lapply(words.spl[[i_chr[le - 1]]], function(word2) {
isOneDiff(word2, word1)
}))
}
sapply(seq_along(words), function(i) {
test_substitution(i) + test_addition(i) + test_deletion(i)
})
}
其中 isOneDiff
是一个 Rcpp 函数:
#include <Rcpp.h>
using namespace Rcpp;
// [[Rcpp::export]]
bool isOneDiff(const StringVector& w1,
const StringVector& w2) {
int i, n = w1.size();
for (i = 0; i < n; i++) if (w1[i] != w2[i]) break;
for ( ; i < n; i++) if (w1[i] != w2[i+1]) return false;
return true;
}
这比您的版本快 20 倍,因为它只是一个 sapply
,可以很容易地并行化。
还有一个不同的答案,使用常规 Levenshtein 距离(即允许在任何点插入),但这次是快速 - 15 秒内快速 1000 个单词。
诀窍是使用 R 包中提供的一种快速 Levenshtein 实现;在这种情况下,我使用的是 stringdist
但任何一个都应该有效。问题是它们对字符串和字符进行操作,而不是对多字符音素表示进行操作。但是有一个简单的解决方案:因为字符比音素多,我们可以将音素翻译成单个字符。生成的字符串作为音位转录是不可读的,但作为邻域密度算法的输入工作得很好。
library(stringdist)
phonemes <- unique(unlist(words))
# add a few buffer characters
targets <- c(letters, LETTERS, 0:9, "!", "§", "%", "&", "/", "=",
"#")[1:length(phonemes)]
ptmap <- targets
names(ptmap) <- phonemes
wordsT <- sapply(words, function(i) paste0(ptmap[i], collapse=""))
wordlengths <- nchar(wordsT)
onediffs.M <- function(x) {
lengthdiff <- abs(wordlengths - nchar(x))
sum(stringdist(x, wordsT[lengthdiff == 0], method="hamming") == 1) +
sum(stringdist(x, wordsT[lengthdiff == 1], method="lv") == 1)
}