2016-06-24 9 views
1

Gibt es eine Möglichkeit zur Optimierung pnorm? Ich habe einen Engpass in meinem Code und nach einer Optimierung und einem Benchmark habe ich festgestellt, dass es von dem Aufruf an pnorm auf wirklich großen Vektoren kommt.Fast pnorm() Berechnung auf wirklich langen Vektor (von Länge ~ 1e + 7 bis ~ 1e + 8)

Mit microbenchmarking habe ich auf meiner Maschine, dass wenn length(u) ~ 5e+7 dann pnorm(u) 11 Sekunden dauert.

Gibt es eine Möglichkeit, Rcpp hier zu verwenden, oder ist die integrierte pnorm bereits optimiert?

Alle Ideen willkommen.

Ich habe diese Beiträge auf SO gefunden: Use pnorm from Rmath.h with Rcpp und How can I use qnorm on Rcpp?. Aber soweit ich verstanden habe, ist ihr Zweck, die R-Funktionen in Cpp-Code zu verwenden.

+1

Abhängig von Ihrem Anwendungsfall und Ihrer Genauigkeitsanforderung können Sie (1) eine adäquate, aber schnellere Annäherung an "pnorm" (2) "memoise" verwenden, d. H. Einige Ergebnisse in einer Nachschlagetabelle speichern. z.B. eine Old-School-Approximation zu "pnorm" ist "plogis" mit einer entsprechend angepassten Varianz ... –

+0

z.B. [this] (http://www.jiem.org/index.php/jiem/article/viewFile/60/27) sieht nützlich aus ... –

+0

Approximation ist eine gute Idee, denke ich, vor allem nach @ ZheyuanLi Antwort. – clemlaflemme

Antwort

5

In dieser Sitzung werde ich eine schnelle, aber genaue Annäherung an pnorm() demonstrieren.

Bevor wir beginnen, müssen wir uns klar machen: Was wollen wir erreichen, indem wir Approximation verwenden? Effizienz/Geschwindigkeit/Leistung, oder? Aber woher sollte diese Effizienz kommen?

Wie oben diskutiert, pnorm() Berechnung ist speichergebunden; Selbst wenn wir die Berechnung annähern, ist sie immer noch speichergebunden (daher berücksichtigen wir keine weitere Parallelisierung). Speichergebundene Probleme haben

number of floating point operations : memory access = O(1) 

Mit anderen Worten, dieses Verhältnis eine Konstante C. Unser Ziel sollte daher sein, diese Konstante zu reduzieren, d. H. Wir wollen Gleitkommaoperationen reduzieren.

Die Anzahl der Gleitkommaoperationen wird oft als Anzahl der Gleitkommaadditionen und Multiplikationen angegeben. Andere Arten von Gleitkommaoperationen werden zu einem solchen Maß "umgewandelt". Vergleichen wir nun die Kosten mehrerer allgemeiner Gleitkommaoperationen.

u <- sample(1:10/10, 5e+7, replace = TRUE) 

system.time(u + u) 
# user system elapsed 
# 0.468 0.168 0.639 
system.time(u * u) 
# user system elapsed 
# 0.424 0.212 0.638 
system.time(u/u) 
# user system elapsed 
# 0.504 0.204 0.710 
system.time(u^1.1) 
# user system elapsed 
# 7.240 0.212 7.458 
system.time(sqrt(u)) 
# user system elapsed 
# 2.044 0.176 2.224 
system.time(exp(u)) 
# user system elapsed 
# 4.336 0.208 4.550 
system.time(log(u)) 
# user system elapsed 
# 2.748 0.172 2.925 
system.time(round(u)) 
# user system elapsed 
# 6.836 0.188 7.034 

Beachten Sie, dass Addition und Multiplikation ist billig, Wurzel und Logarithmus ist teurer, während einige Operationen sehr teuer sind, einschließlich Energie, exponentielle und Rundung.

Jetzt zurück zu pnorm(), oder sogar dnorm(), usw., wo wir einen exponentiellen Term zu berechnen haben. Da:

system.time(pnorm(u)) 
# user system elapsed 
# 11.016 0.160 11.193 
system.time(dnorm(u)) 
# user system elapsed 
# 8.844 0.164 9.022 

wir sehen, dass die Mehrheit der Zeit zu berechnen pnorm() und dnorm() ist Attribut Berechnung exponentiell. pnorm() dauert länger als dnorm() weil es weiter ein Integral hat!

Jetzt ist unser Ziel ziemlich klar: Wir wollen die teure pnorm() Auswertung durch etwas wirklich billiges ersetzen, im Idealfall nur Addition/Multiplikation. Können wir??

Es gab viele Näherungsmethoden in der Geschichte. @Ben hat die logistische Approximation erwähnt. R-Funktion plogis() tut dies. Aber ein sorgfältiges Lesen auf ?plogis zeigt, dass es immer noch auf Exponentialfunktionen basiert.

Können wir nun anstelle der parametrischen Approximation eine nichtparametrische Approximation durchführen?Aber wir sollten hier keine Regression machen; stattdessen wollen wir eine Interpolationsfunktion von präzisen Daten mit feiner Auflösung verwenden, um pnorm() vorherzusagen. Nun, lineare Interpolation ist die beste Wahl, da sie super effizient ist (aufgrund von Sparsität: die lineare Prädiktormatrix ist tri-diagonal). In R macht approx dies. Ich verweise Leser, der damit nicht vertraut ist, auf ?approx, und ich werde einfach fortfahren.

Das OP sagt, er braucht nur Standard-Normalverteilung, also konzentrieren wir uns darauf. Betrachten Sie die folgende Näherungsfunktion (ich habe nicht approxfun verwenden, weil ich anpassbare h wollen):

approx.pnorm <- function(u, h = 0.2) { 
    x <- seq(from = -4, to = 4, by = h) 
    approx(x, pnorm(x), yleft = 0, yright = 1, xout = u)$y 
    } 

Die genauen Daten auf einem Raster von Auflösung h zwischen [-4, 4] getroffen werden. Vorhersagen unter -4 ist 0, während Vorhersagen über 4 1 ist. Dies erfüllt die Anforderung einer CDF. Bei neuen Werten u nähern wir uns pnorm(u) durch lineare Interpolation basierend auf bekannten genauen Daten.

Offensichtlich steuert die Auflösung h Genauigkeit. Betrachten Sie die folgende Funktion RMSE und Anzeigenäherungskurve zu berechnen:

RMSEh <- function(h) { 
    x <- sort(rnorm(1000)) 
    y <- pnorm(x) 
    y1 <- approx.pnorm(x, h) 
    plot(x, y, type = "l", lwd = 2); lines(x, y1, col = 2, lwd = 2) 
    mean((y - y1)^2)^0.5 
    } 

par(mfrow = c(1, 3)) 
RMSEh(1) # 0.01570339 
RMSEh(0.5) # 0.003968882 
RMSEh(0.2) # 0.000639888 

approx

Eigentlich wenn h = 0.2, Annäherung schon ziemlich gut ist. Also werden wir im folgenden h = 0.2 verwenden.


Benchmarking

Dies sollte der spannendste Teil sein. In oben haben wir gesehen, dass die genaue Berechnung von pnorm(u) 11 Sekunden dauert. Jetzt

system.time(approx.pnorm(u, h = 0.2)) 
# user system elapsed 
# 2.656 0.172 2.833 

Wow, wir sind fast 4 mal schneller !!

+0

Ansatz interessant, aber mit 'Microbenchmark' habe ich die gleiche Größenordnung für beide, dh 3.≈ Sekunden: '> Microbenchmark :: Microbenchmark (pnorm (u), approx.pnorm (u)) Einheit: Sekunden expr min lq Mittelwert median uq max neval pnorm (u) 2.966394 3.149768 3.571884 3.326649 3.696174 6.855846 100 approx.pnorm (u) 2,73,890056 3,225221 3,012693 3,197080 6,546609 100' – clemlaflemme

+1

, die viele andere Form bei Ihnen in der Tat ist: '> system.time (u + u) 0,178 0,787 1,290 > system.time (u u *) 0.140 0,148 0,312 > Systemzeit (u/u) 0,466 0,139 0,611 > Systemzeit (u^1,1) 2,303 0,145 2,460 > Systemzeit (sqrt (u)) 0,448 0,157 0,767 > system.time (exp (u)) 0.709 0.163 0.918 > system.time (log (u)) 0.849 0.146 1.006 > system.time (round (u)) 1.015 0.140 1.160' – clemlaflemme

+0

dasselbe 'u' wie Ihres. Ich bin auf 2,4 GHz Intel Core i5 von 2011 mit 4Go Ram und 128Go SSD für die Festplatte – clemlaflemme

2

Ich bin nicht hier, um Sie zu enttäuschen, aber pnorm ist bereits optimiert. Wenn Sie "Pnorm" in der R-Konsole eingeben, sehen Sie, wie geschrieben steht:

function (q, mean = 0, sd = 1, lower.tail = TRUE, log.p = FALSE) 
.Call(C_pnorm, q, mean, sd, lower.tail, log.p) 
<bytecode: 0x98712e0> 
<environment: namespace:stats> 

Es ist bereits in C geschrieben ist (siehe Rmath.h).

Einige Leute könnten Ihnen dann vorschlagen, parallel zu rechnen. R-Level-Parallelität kann zum Beispiel mclapply/parLapply/parSapply Funktion von parallel Paket verwenden. Aber ob Sie das tun sollten, hängt davon ab, welche Hardware Sie haben.

Es ist eine schlechte Idee, pnorm() auf einem einfachen Multi-Core-Rechner zu parallelisieren, wie es speichergebunden ist. Das Verhältnis zwischen CPU-Berechnung und Speicherreferenz ist O(1) (unter Verwendung der großen Notation O). Außerdem ist die Parallelität auf R-Ebene keine Parallelisierung auf Thread-Ebene, sondern durch das Einrichten unabhängiger R-Prozesse. Dies bedeutet, dass paralleler Overhead größer und nicht einfach zu amortisieren ist.

Wenn Sie einen Cluster haben, können Sie paralleles Rechnen auf verschiedenen Knoten für ein wirklich großes Problem durchführen. Sie erhalten eine gute parallele Skalierbarkeit.


weitere Klärung auf Parallelverarbeitung

u sei angenommen, ist ein langer Vektor: u[1], u[2], .... und wir wollen pnorm(u) berechnen. Jedes Element u[i] wird nur einmal ohne eine zweite Verwendung von RAM zu CPU gebracht. Daher erfordert die Berechnung von pnorm() das konstante Lesen von Daten.

Betrachten Sie nun eine Multi-Core-Maschine mit 4 physischen CPUs (d. H. Mit jeweils nicht gemeinsam genutzten Ausführungseinheiten wie Registern, ALU, FPU, L1-Cache usw.). Wir erstellen 4 Threads oder Prozesse in der Hoffnung, 4 parallele pnorm() Berechnung auf 4 verschiedenen Teilen von u zu laufen. Während der Berechnung ist jede CPU "datenhungrig" und fragt nach einem konstanten Datenfluss. Es gibt jedoch nur einen einzigen Bus. Wenn eine CPU den Bus besetzt, wird der Datenfluss für die restlichen drei abgeschnitten, so dass sie nichts zu tun haben. Mit anderen Worten, diese 4 CPUs können fast nie gleichzeitig arbeiten, und sie sind nicht besser als eine Einzel-CPU-Berechnung.

Jetzt gehen wir zu 4 Knoten auf einem Cluster. Nachdem die anfänglichen Daten auf 4 verschiedene Knoten aufgeteilt wurden, arbeitet jeder Knoten in einem einzigen CPU-Modus.Es gibt weder gemeinsam genutzte Ausführungsressourcen noch Speicherressourcen zwischen 4 Knoten. Sie können vollständig parallel arbeiten. Am Ende werden Ergebnisse von 4 Knoten zusammengeschmiedet. Auf diese Weise kann für ein wirklich großes Problem eine gute/vernünftige Skalierbarkeit garantiert werden.

Parallele Berechnung auf Multi-Core-Maschine ist nur nützlich für CPU-gebunden Aufgabe (bis zu einem gewissen Grad, bevor der Bus gesättigt wird). Insbesondere sollten wir einen Blockalgorithmus für das L1-Caching verwenden. Durch Caching wird eine beträchtliche Datenwiederverwendung erreicht. Für die Blockmatrix-Multiplikation der Blockgröße nb beträgt das Verhältnis zwischen CPU-Arbeit und Speicherauslese beispielsweise O(nb). Nachdem eine CPU einen Block von Daten in ihren exklusiven L1-Cache gelesen hat, benötigt sie in vergleichsweise einer langen Zeitperiode (in CPU-Zyklen) keinen Zugriff auf den RAM, so dass der Bus frei wird. Dann können die anderen Kerne eine solche Lücke/Pause nehmen, um die Daten zu lesen, die sie benötigen. Da nur eine begrenzte Anzahl von CPUs verwendet wird, können sie in einer verschachtelten Weise ohne gegenseitige Interferenz arbeiten.

+0

nette Antwort Woher weißt du, dass 'pnorm()' speichergebunden ist ...? –

+0

Sehr interessant, ich benutze 'foreach' Paket zu, aber konnte mir nicht vorstellen, dass' doMC' nicht helfen kann, während 'doMPI' Ist – clemlaflemme

Verwandte Themen