0

R でのループの高速化は、apply ファミリーの関数を使用して簡単に行うことができます。以下のコードで適用関数を使用して高速化するにはどうすればよいですか? ループ内では、反復ごとに 1 つの列が並べ替えられ、関数が新しいデータ フレーム (つまり、1 つの列が並べ替えられた最初のデータ フレーム) に適用されることに注意してください。新しいデータフレームをループ内で構築する必要があるため、適用できないようです。

#x <- data.frame(a=1:10,b=11:20,c=21:30) #small example
x <- data.frame(matrix(runif(50*100),nrow=50,ncol=100)) #larger example
y <- rowMeans(x)

start <- Sys.time()  

totaldiff <- numeric()

for (i in 1:ncol(x)){
    x.after <- x

    x.after[,i] <- sample(x[,i])

    diff <- abs(y-rowMeans(x.after))

    totaldiff[i] <- sum(diff)

}

colnames(x)[which.max(totaldiff)]

Sys.time() - start
4

3 に答える 3

7

この返信や他の返信に取り組んだ後、ここでの最適化戦略 (およびおおよそのスピードアップ) は次のようになります。

  • (30x) 適切なデータ表現 (data.frame ではなくマトリックス) を選択します。
  • (1.5x) 不要なデータ コピーを削減 -- 行平均ではなく列の違い
  • 関数としてのループの構造*apply(コード構造を強調し、メモリ管理を簡素化し、型の一貫性を提供するため)
  • (2x) ループ外でベクトル演算をホイスト -- 列の abs と sum は、行列の abs と colSum になります。

全体で約 100 倍の高速化。このサイズとコードの複雑さでは、コンパイラまたは並列パッケージの使用は効果的ではありません。

あなたのコードを関数に入れます

f0 <- function(x) {
    y <- rowMeans(x)
    totaldiff <- numeric()
    for (i in 1:ncol(x)){
        x.after <- x
        x.after[,i] <- sample(x[,i])
        diff <- abs(y-rowMeans(x.after))
        totaldiff[i] <- sum(diff)
    }
    which.max(totaldiff)
}

ここにある

x <- data.frame(matrix(runif(50*100),nrow=50,ncol=100)) #larger example
set.seed(123)
system.time(res0 <- f0(x))
##   user  system elapsed 
##  1.065   0.000   1.066 

データは行列として表すことができ、R 行列での操作は data.frames での操作よりも高速です。

m <- matrix(runif(50*100),nrow=50,ncol=100)
set.seed(123)
system.time(res0.m <- f0(m))
##   user  system elapsed 
##  0.036   0.000   0.037 
identical(res0, res0.m)
##[1] TRUE

これがおそらく最大の高速化です。ただし、ここでの特定の操作では、更新された行列の行平均を計算する必要はありません。1 つの列をシャッフルすることによる平均の変化だけです。

f1 <- function(x) {
     y <- rowMeans(x)
    totaldiff <- numeric()
    for (i in 1:ncol(x)){
        diff <- abs(sample(x[,i]) - x[,i]) / ncol(x)
        totaldiff[i] <- sum(diff)
    }
    which.max(totaldiff)
}

forループは、結果ベクトルを埋めるための正しいパターンに従いません( totaldiff「事前に割り当てて埋める」必要があるためtotaldiff <- numeric(ncol(x))) が、 を使用しsapplyて R にそれを心配させることができます (このメモリ管理は、R の利点の 1 つです)。関数の apply ファミリを使用)

f2 <- function(x) {
    totaldiff <- sapply(seq_len(ncol(x)), function(i, x) {
        sum(abs(sample(x[,i]) - x[,i]) / ncol(x))
    }, x)
    which.max(totaldiff)
}
set.seed(123); identical(res0, f1(m))
set.seed(123); identical(res0, f2(m))

タイミングは

> library(microbenchmark)
> microbenchmark(f0(m), f1(m), f2(m))
Unit: milliseconds
  expr      min       lq   median       uq      max neval
 f0(m) 32.45073 33.07804 33.16851 33.26364 33.81924   100
 f1(m) 22.20913 23.87784 23.96915 24.06216 24.66042   100
 f2(m) 21.02474 22.60745 22.70042 22.80080 23.19030   100

@flodelは、より高速になる可能性があることを指摘していvapplyます(およびタイプセーフを提供します)

f3 <- function(x) {
    totaldiff <- vapply(seq_len(ncol(x)), function(i, x) {
        sum(abs(sample(x[,i]) - x[,i]) / ncol(x))
    }, numeric(1), x)
    which.max(totaldiff)
}

そしてそれ

f4 <- function(x)
    which.max(colSums(abs((apply(x, 2, sample) - x))))

はまだ高速です (ncol(x)は一定の要素であるため、削除されます)。abssumは の外側に持ち上げられsapplyます。メモリの使用量が増える可能性があります。関数をコンパイルするためのコメントのアドバイスは、一般的には良いものです。ここにいくつかのさらなるタイミングがあります

>     microbenchmark(f0(m), f1(m), f1.c(m), f2(m), f2.c(m), f3(m), f4(m))
Unit: milliseconds
    expr      min       lq   median       uq       max neval
   f0(m) 32.35600 32.88326 33.12274 33.25946  34.49003   100
   f1(m) 22.21964 23.41500 23.96087 24.06587  24.49663   100
 f1.c(m) 20.69856 21.20862 22.20771 22.32653 213.26667   100
   f2(m) 20.76128 21.52786 22.66352 22.79101  69.49891   100
 f2.c(m) 21.16423 21.57205 22.94157 23.06497  23.35764   100
   f3(m) 20.17755 21.41369 21.99292 22.10814  22.36987   100
   f4(m) 10.10816 10.47535 10.56790 10.61938  10.83338   100

「.c」はコンパイルされたバージョンで、

コンパイルは、for ループで記述されたコードでは特に役立ちますが、ベクトル化されたコードではあまり効果がありません。これはここに示されています。f1 の for ループをコンパイルすると、小さいながらも一貫して改善されますが、f2 の sapply では改善されません。

于 2013-05-10T17:01:12.243 に答える
4

効率/最適化を検討しているrbenchmarkため、比較目的でパッケージを使用することから始めます。

与えられた例を関数として書き直します(複製して比較できるようにするため)

forFirst <- function(x) {
    y <- rowMeans(x)
    totaldiff <- numeric()
    for (i in 1:ncol(x)){
        x.after <- x
        x.after[,i] <- sample(x[,i])
        diff <- abs(y-rowMeans(x.after))
        totaldiff[i] <- sum(diff)
    }
    colnames(x)[which.max(totaldiff)]
}

いくつかの標準的な最適化 (適切なサイズへの事前割り当てtotaldiff、一度だけ使用される中間変数の排除) を適用すると、

forSecond <- function(x) {
    y <- rowMeans(x)
    totaldiff <- numeric(ncol(x))
    for (i in 1:ncol(x)){
        x.after <- x
        x.after[,i] <- sample(x[,i])
        totaldiff[i] <- sum(abs(y-rowMeans(x.after)))
    }
    colnames(x)[which.max(totaldiff)]
}

ループ内のアルゴリズム自体を改善するためにこれ以上できることはありません。より良いアルゴリズムが最も役に立ちますが、この特定の問題は単なる例であるため、その時間を費やす価値はありません。

適用バージョンは非常によく似ています。

applyFirst <- function(x) {
      y <- rowMeans(x)
    totaldiff <- sapply(seq_len(ncol(x)), function(i) {
        x[,i] <- sample(x[,i])
        sum(abs(y-rowMeans(x)))
      })
    colnames(x)[which.max(totaldiff)]
}

それらをベンチマークすると、次のようになります。

> library("rbenchmark")
> benchmark(forFirst(x),
+           forSecond(x),
+           applyFirst(x),
+           order = "relative")
           test replications elapsed relative user.self sys.self user.child
1   forFirst(x)          100   16.92    1.000     16.88     0.00         NA
2  forSecond(x)          100   17.02    1.006     16.96     0.03         NA
3 applyFirst(x)          100   17.05    1.008     17.02     0.01         NA
  sys.child
1        NA
2        NA
3        NA

これらの違いは単なるノイズです。実際、ベンチマークを再度実行すると、順序が異なります。

> benchmark(forFirst(x),
+           forSecond(x),
+           applyFirst(x),
+           order = "relative")
           test replications elapsed relative user.self sys.self user.child
3 applyFirst(x)          100   17.05    1.000     17.02        0         NA
2  forSecond(x)          100   17.08    1.002     17.05        0         NA
1   forFirst(x)          100   17.44    1.023     17.41        0         NA
  sys.child
3        NA
2        NA
1        NA

したがって、これらのアプローチは同じ速度です。真の改善は、中間結果を作成するための単純なループとコピーよりも優れたアルゴリズムを使用することから得られます。

于 2013-05-10T17:06:49.517 に答える
1

適用関数は、R のループを必ずしも高速化するとは限りません。これを適用ファミリー関数に変えることで、かなりの速度が向上すると信じる理由はありません。

余談ですが、このコードは比較的無意味な試みのように思えます。ランダムな列を選択するだけです。そもそもそれをするだけで同じ結果を得ることができました。おそらく、これはディストリビューションを探す大きなループにネストされているのでしょうか?

于 2013-05-10T16:28:27.610 に答える