优化R代码以根据自定义距离函数创建距离矩阵
Optimize R code to create distance matrix based on customized distance function
我正在尝试根据自定义的距离函数为字符串创建一个距离矩阵(用于聚类)。我 运行 6000 字列表中的代码,自上次 90 分钟以来它仍然是 运行。我有 8 GB RAM 和 Intel-i5,所以问题只出在代码上。
这是我的代码:
library(stringdist)
#Calculate distance between two monograms/bigrams
stringdist2 <- function(word1, word2)
{
#for bigrams - phrases with two words
if (grepl(" ",word1)==TRUE) {
#"Hello World" and "World Hello" are not so different for me
d=min(stringdist(word1, word2),
stringdist(word1, gsub(word2,
pattern = "(.*) (.*)",
repl="\2,\1")))
}
#for monograms(words)
else{
#add penalty of 5 points if first character is not same
#brave and crave are more different than brave and bravery
d=ifelse(substr(word1,1,1)==substr(word2,1,1),
stringdist(word1,word2),
stringdist(word1,word2)+5)
}
d
}
#create distance matrix
stringdistmat2 = function(arr)
{
mat = matrix(nrow = length(arr), ncol= length(arr))
for (k in 1:(length(arr)-1))
{
for (j in k:(length(arr)-1))
{
mat[j+1,k] = stringdist2(arr[k],arr[j+1])
}
}
as.dist(mat)
}
test = c("Hello World","World Hello", "Hello Word", "Cello Word")
mydmat = stringdistmat2(test)
> mydmat
1 2 3
2 1
3 1 2
4 2 3 1
我认为问题可能是我使用循环而不是应用 - 但后来我发现在很多地方循环并不是那么低效。更重要的是,我不够熟练,无法使用 apply 因为我的循环是嵌套循环,如 k in 1:n
和 j in k:n
。不知道还有没有其他可以优化的东西
循环确实非常低效,这里有一个简单的例子表明:
x=rnorm(1000000)
system.time({y1=sum(x)})
system.time({
y2=0
for(i in 1:length(x)){
y2=y2+x[i]
}
})
这是内部向量化函数 sum() 的简单比较,本质上只是在内部计算循环中所有元素的总和;第二个函数在 R 代码中做同样的事情,这使得它一遍又一遍地调用另一个内部函数 +
,效率不高。
首先,您的用户定义函数中有几个 mistakes/inconsistencies。
这部分:
gsub(word2, pattern = "(.*) (.*)", repl="\2,\1")
将所有白色 space 替换为逗号,这会自动将 +1 添加到距离分数(是故意的吗?)
其次,您不比较其中包含 space 的字符串的首字母,因为这样只会执行函数的第一部分。即使只有第一个比较的单词包含 space,也是如此,因此 "Hello " 和 "Cello" 比较将被计算为比 "Hello" 和 "Cello" 更近的距离.
除此之外,您的代码似乎很容易矢量化,因为您使用的所有函数都已经矢量化:stringdist()、grepl()、gsub()、substr() 等。基本上您执行 3 次计算每个单词对:简单的 'stringdist()',交换单词的 stringdist()(如果第一个单词中有 space),以及第一个字母的简单比较,如果它们不同则增加 +5 分。
这是以矢量化方式重现您的函数的代码,在计算 300x300 矩阵时速度提高了大约 50 倍:
stringdist3<-function(words1,words2){
m1<-stringdist(words1,words2)
m2<-stringdist(words1,gsub(words2,
pattern = "(.*) (.*)",
repl="\2,\1"))
m=mapply(function(x,y) min(x,y),m1,m2)
m3<-5*(substr(words1,1,1)!=substr(words2,1,1) & !grepl(" ",words1))
m3+m
}
stringdistmat3 = function(arr){
outer(arr,arr,function(x,y) stringdist3(x,y))
}
test = c("Hello World","World Hello", "Hello Word", "Cello Word")
arr=sample(test,size=300,replace=TRUE)
system.time({mat = stringdistmat2(arr)})
system.time({
mat2=stringdistmat3(arr)
})
有趣的问题。所以一步一步来:
1 - stringdist
函数已经向量化:
#> stringdist("byye", c('bzyte','byte'))
#[1] 2 1
#> stringdist(c('doggy','gadgy'), 'dodgy')
#[1] 1 2
但是给定两个长度相同的向量,stringdist
将导致在每个向量上并行循环(不会产生具有交叉结果的矩阵),如 Map
会做的那样:
#> stringdist(c("byye","alllla"), c('bzyte','byte'))
#[1] 2 6
2 - 重写你的函数,使你的新函数保持这个矢量化特征:
stringdistFast <- function(word1, word2)
{
d1 = stringdist(word1, word2)
d2 = stringdist(word1, gsub("(.+) (.+)", "\2 \1", word2))
ifelse(d1==d2,d1+5*(substr(d1,1,1)!=substr(d2,1,1)),pmin(d1,d2))
}
确实是一样的:
#> stringdistFast("byye", c('bzyte','byte'))
#[1] 2 1
#> stringdistFast("by ye", c('bzyte','byte','ye by'))
#[1] 3 2 0
3 - 重写 dismatrix 函数,只有一个循环,并且只在三角形部分(没有 outer
那里,它很慢!):
stringdistmatFast <- function(test)
{
m = diag(0, length(test))
sapply(1:(length(test)-1), function(i)
{
m[,i] <<- c(rep(0,i), stringdistFast(test[i],test[(i+1):length(test)]))
})
`dimnames<-`(m + t(m), list(test,test))
}
4 - 使用函数:
#> stringdistmatFast(test)
# Hello World World Hello Hello Word Cello Word
#Hello World 0 0 1 2
#World Hello 0 0 1 2
#Hello Word 1 1 0 1
#Cello Word 2 2 1 0
我也在尝试创建一种替代方法来改进我的答案。基本上我删除了创建距离的功能并直接创建了 distances.So 的矩阵,这就是我想出的。我知道这个解决方案可以改进。所以欢迎任何建议
strdistmat2 <- function(v1,v2,type="m"){
#for monograms
if (type=="m") {
penalty = sapply(substr(v1,1,1),stringdist,b=substr(v2,1,1)) * 5
d = sum(sapply(v1,stringdist,b=v2),penalty)
}
#for bigrams
else if(type=="b") {
d1 = sapply(v1,stringdist,b=v2)
d2 = sapply(v1,stringdist,b=gsub(v2,pattern = "(.*) (.*)", repl="\2 \1"))
d = pmin(d1,d2)
}
d
}
我在下面比较了各种解决方案的时间。
> test = c("Hello World","World Hello", "Hello Word", "Cello Word")
> arr=sample(test,size=6000,replace=TRUE)
> system.time({mat=strdistmat2(arr,arr,"b")})
user system elapsed
96.89 1.63 70.36
> system.time({mat2=stringdistmat3(arr)})
user system elapsed
469.40 5.69 439.96
> system.time({mat3=stringdistmatFast(arr)})
user system elapsed
57.34 0.72 41.22
因此-上校回答最快。
同样在实际数据上,我的和 Maksim 的代码都崩溃了,只有上校的回答有效。
这是结果
> system.time({mat3=stringdistmatFast(words)})
user system elapsed
314.63 1.78 291.94
当我 运行 我对实际数据的解决方案时 - 错误消息是 - 无法分配 684 MB 的向量
运行 Maksim 的解决方案 - R 停止工作。
我正在尝试根据自定义的距离函数为字符串创建一个距离矩阵(用于聚类)。我 运行 6000 字列表中的代码,自上次 90 分钟以来它仍然是 运行。我有 8 GB RAM 和 Intel-i5,所以问题只出在代码上。 这是我的代码:
library(stringdist)
#Calculate distance between two monograms/bigrams
stringdist2 <- function(word1, word2)
{
#for bigrams - phrases with two words
if (grepl(" ",word1)==TRUE) {
#"Hello World" and "World Hello" are not so different for me
d=min(stringdist(word1, word2),
stringdist(word1, gsub(word2,
pattern = "(.*) (.*)",
repl="\2,\1")))
}
#for monograms(words)
else{
#add penalty of 5 points if first character is not same
#brave and crave are more different than brave and bravery
d=ifelse(substr(word1,1,1)==substr(word2,1,1),
stringdist(word1,word2),
stringdist(word1,word2)+5)
}
d
}
#create distance matrix
stringdistmat2 = function(arr)
{
mat = matrix(nrow = length(arr), ncol= length(arr))
for (k in 1:(length(arr)-1))
{
for (j in k:(length(arr)-1))
{
mat[j+1,k] = stringdist2(arr[k],arr[j+1])
}
}
as.dist(mat)
}
test = c("Hello World","World Hello", "Hello Word", "Cello Word")
mydmat = stringdistmat2(test)
> mydmat
1 2 3
2 1
3 1 2
4 2 3 1
我认为问题可能是我使用循环而不是应用 - 但后来我发现在很多地方循环并不是那么低效。更重要的是,我不够熟练,无法使用 apply 因为我的循环是嵌套循环,如 k in 1:n
和 j in k:n
。不知道还有没有其他可以优化的东西
循环确实非常低效,这里有一个简单的例子表明:
x=rnorm(1000000)
system.time({y1=sum(x)})
system.time({
y2=0
for(i in 1:length(x)){
y2=y2+x[i]
}
})
这是内部向量化函数 sum() 的简单比较,本质上只是在内部计算循环中所有元素的总和;第二个函数在 R 代码中做同样的事情,这使得它一遍又一遍地调用另一个内部函数 +
,效率不高。
首先,您的用户定义函数中有几个 mistakes/inconsistencies。
这部分:
gsub(word2, pattern = "(.*) (.*)", repl="\2,\1")
将所有白色 space 替换为逗号,这会自动将 +1 添加到距离分数(是故意的吗?)
其次,您不比较其中包含 space 的字符串的首字母,因为这样只会执行函数的第一部分。即使只有第一个比较的单词包含 space,也是如此,因此 "Hello " 和 "Cello" 比较将被计算为比 "Hello" 和 "Cello" 更近的距离.
除此之外,您的代码似乎很容易矢量化,因为您使用的所有函数都已经矢量化:stringdist()、grepl()、gsub()、substr() 等。基本上您执行 3 次计算每个单词对:简单的 'stringdist()',交换单词的 stringdist()(如果第一个单词中有 space),以及第一个字母的简单比较,如果它们不同则增加 +5 分。
这是以矢量化方式重现您的函数的代码,在计算 300x300 矩阵时速度提高了大约 50 倍:
stringdist3<-function(words1,words2){
m1<-stringdist(words1,words2)
m2<-stringdist(words1,gsub(words2,
pattern = "(.*) (.*)",
repl="\2,\1"))
m=mapply(function(x,y) min(x,y),m1,m2)
m3<-5*(substr(words1,1,1)!=substr(words2,1,1) & !grepl(" ",words1))
m3+m
}
stringdistmat3 = function(arr){
outer(arr,arr,function(x,y) stringdist3(x,y))
}
test = c("Hello World","World Hello", "Hello Word", "Cello Word")
arr=sample(test,size=300,replace=TRUE)
system.time({mat = stringdistmat2(arr)})
system.time({
mat2=stringdistmat3(arr)
})
有趣的问题。所以一步一步来:
1 - stringdist
函数已经向量化:
#> stringdist("byye", c('bzyte','byte'))
#[1] 2 1
#> stringdist(c('doggy','gadgy'), 'dodgy')
#[1] 1 2
但是给定两个长度相同的向量,stringdist
将导致在每个向量上并行循环(不会产生具有交叉结果的矩阵),如 Map
会做的那样:
#> stringdist(c("byye","alllla"), c('bzyte','byte'))
#[1] 2 6
2 - 重写你的函数,使你的新函数保持这个矢量化特征:
stringdistFast <- function(word1, word2)
{
d1 = stringdist(word1, word2)
d2 = stringdist(word1, gsub("(.+) (.+)", "\2 \1", word2))
ifelse(d1==d2,d1+5*(substr(d1,1,1)!=substr(d2,1,1)),pmin(d1,d2))
}
确实是一样的:
#> stringdistFast("byye", c('bzyte','byte'))
#[1] 2 1
#> stringdistFast("by ye", c('bzyte','byte','ye by'))
#[1] 3 2 0
3 - 重写 dismatrix 函数,只有一个循环,并且只在三角形部分(没有 outer
那里,它很慢!):
stringdistmatFast <- function(test)
{
m = diag(0, length(test))
sapply(1:(length(test)-1), function(i)
{
m[,i] <<- c(rep(0,i), stringdistFast(test[i],test[(i+1):length(test)]))
})
`dimnames<-`(m + t(m), list(test,test))
}
4 - 使用函数:
#> stringdistmatFast(test)
# Hello World World Hello Hello Word Cello Word
#Hello World 0 0 1 2
#World Hello 0 0 1 2
#Hello Word 1 1 0 1
#Cello Word 2 2 1 0
我也在尝试创建一种替代方法来改进我的答案。基本上我删除了创建距离的功能并直接创建了 distances.So 的矩阵,这就是我想出的。我知道这个解决方案可以改进。所以欢迎任何建议
strdistmat2 <- function(v1,v2,type="m"){
#for monograms
if (type=="m") {
penalty = sapply(substr(v1,1,1),stringdist,b=substr(v2,1,1)) * 5
d = sum(sapply(v1,stringdist,b=v2),penalty)
}
#for bigrams
else if(type=="b") {
d1 = sapply(v1,stringdist,b=v2)
d2 = sapply(v1,stringdist,b=gsub(v2,pattern = "(.*) (.*)", repl="\2 \1"))
d = pmin(d1,d2)
}
d
}
我在下面比较了各种解决方案的时间。
> test = c("Hello World","World Hello", "Hello Word", "Cello Word")
> arr=sample(test,size=6000,replace=TRUE)
> system.time({mat=strdistmat2(arr,arr,"b")})
user system elapsed
96.89 1.63 70.36
> system.time({mat2=stringdistmat3(arr)})
user system elapsed
469.40 5.69 439.96
> system.time({mat3=stringdistmatFast(arr)})
user system elapsed
57.34 0.72 41.22
因此-上校回答最快。
同样在实际数据上,我的和 Maksim 的代码都崩溃了,只有上校的回答有效。 这是结果
> system.time({mat3=stringdistmatFast(words)})
user system elapsed
314.63 1.78 291.94
当我 运行 我对实际数据的解决方案时 - 错误消息是 - 无法分配 684 MB 的向量 运行 Maksim 的解决方案 - R 停止工作。