Рассчитать вектор, длина которого заранее не известна - мне его «вырастить»? - PullRequest
0 голосов
/ 21 сентября 2018

Мне нужно вычислить записи вектора , длина которого я не знаю заранее .Как сделать это эффективно?

Тривиальное решение - «вырастить» его: начните с малого или пустого вектора и последовательно добавляйте новые записи, пока не будет достигнут критерий остановки.Например:

foo <- numeric(0)
while ( sum(foo) < 100 ) foo <- c(foo,runif(1))
length(foo)
# 195

Тем не менее, «растущие» векторы не одобряются в R по соображениям производительности.

Конечно, я мог бы «вырастить его кусками»: предварительно выделить «"хорошего размера", заполните его, удвойте его длину, когда он заполнится, и, наконец, уменьшите его до размера.Но это может привести к ошибкам и приведет к неэлегатному коду.

Есть ли лучший или канонический способ сделать это?(В моем реальном приложении вычисления и критерий остановки немного сложнее, конечно.)


В ответ на некоторые полезные комментарии

Даже если вы заранее не знаете длину, знаете ли вы максимально возможную длину, которую она теоретически может иметь?В таких случаях я склонен инициализировать вектор с этой длиной и после того, как цикл обрезает NA или удаляет неиспользуемые записи на основе последнего значения индекса.

Нет, максимальная длина заранее неизвестна.

Нужно ли сохранять все значения по мере роста вектора?

Да, да.

Как насчет чего-то вроде rand_num <- runif(300); rand_num[cumsum(rand_num) < 100] где вы выбираете достаточно большой вектор, который вы знаете для высокой вероятности того, что условие будет выполнено?Вы можете, конечно, проверить его и использовать еще большее число, если оно не встречается.Я тестировал до runif(10000), это все еще быстрее, чем «растущий».

Мой реальный вариант использования включает в себя динамический расчет, который я не могу просто векторизовать (иначе я бы не спрашивал).

В частности, для аппроксимации свертки отрицательных биномиальных случайных величин мне нужно вычислить массы вероятностей целочисленной случайной величины $ K $ в теореме 2 в Furman, 2007 довысокая кумулятивная вероятность.Эти массы $ pr_k $ включают в себя некоторые сложные рекурсивные суммы.

1 Ответ

0 голосов
/ 21 сентября 2018

Я мог бы «увеличить его на куски»: предварительно выделить вектор «хорошего размера», заполнить его, удвоить его длину, когда он заполнится, и, наконец, сократить его до размера.Но это может привести к ошибкам и приведет к неэлегатному коду.

Похоже, вы ссылаетесь на принятый ответ Сбор неизвестного числа результатов в цикле .Вы кодировали и пробовали?Идея удвоения длины более чем достаточна (см. Конец этого ответа), так как длина будет расти геометрически.Я продемонстрирую свой метод следующим образом.


В целях тестирования оберните ваш код в функцию.Обратите внимание, как я избегаю выполнения sum(z) для каждого теста while.

ref <- function (stop_sum, timing = TRUE) {
  set.seed(0)                            ## fix a seed to compare performance
  if (timing) t1 <- proc.time()[[3]]
  z <- numeric(0)
  sum_z <- 0
  while ( sum_z < stop_sum ) {
    z_i <- runif(1)
    z <- c(z, z_i)
    sum_z <- sum_z + z_i
    }
  if (timing) {
    t2 <- proc.time()[[3]]
    return(t2 - t1)                      ## return execution time
    } else {
    return(z)                            ## return result
    }
  }

Разделение на части необходимо для уменьшения эксплуатационных расходов на сцепление.

template <- function (chunk_size, stop_sum, timing = TRUE) {
  set.seed(0)                            ## fix a seed to compare performance
  if (timing) t1 <- proc.time()[[3]]
  z <- vector("list")                    ## store all segments in a list
  sum_z <- 0                             ## cumulative sum
  while ( sum_z < stop_sum ) {
    segmt <- numeric(chunk_size)         ## initialize a segment
    i <- 1
    while (i <= chunk_size) {
      z_i <- runif(1)                    ## call a function & get a value
      sum_z <- sum_z + z_i               ## update cumulative sum
      segmt[i] <- z_i                    ## fill in the segment
      if (sum_z >= stop_sum) break       ## ready to break at any time
      i <- i + 1
      }
    ## grow the list
    if (sum_z < stop_sum) z <- c(z, list(segmt))
    else z <- c(z, list(segmt[1:i]))
    }
  if (timing) {
    t2 <- proc.time()[[3]]
    return(t2 - t1)                      ## return execution time
    } else {
    return(unlist(z))                    ## return result
    }
  }

Сначала проверим правильность.

z <- ref(1e+4, FALSE)
z1 <- template(5, 1e+4, FALSE)
z2 <- template(1000, 1e+4, FALSE)

range(z - z1)
#[1] 0 0

range(z - z2)
#[1] 0 0

Давайте тогда сравним скорость.

## reference implementation
t0 <- ref(1e+4, TRUE)

## unrolling implementation
trial_chunk_size <- seq(5, 1000, by = 5)
tm <- sapply(trial_chunk_size, template, stop_sum = 1e+4, timing = TRUE)

## visualize timing statistics
plot(trial_chunk_size, tm, type = "l", ylim = c(0, t0), col = 2, bty = "l")
abline(h = t0, lwd = 2)

Похоже, chunk_size = 200 достаточно хорошо, а коэффициент ускорения равен

t0 / tm[trial_chunk_size == 200]
#[1] 16.90598

Давайте, наконец, посмотрим, сколько времени уходит на растущий вектор с помощью c через профилирование.

Rprof("a.out")
z0 <- ref(1e+4, FALSE)
Rprof(NULL)
summaryRprof("a.out")$by.self
#        self.time self.pct total.time total.pct
#"c"          1.68    90.32       1.68     90.32
#"runif"      0.12     6.45       0.12      6.45
#"ref"        0.06     3.23       1.86    100.00

Rprof("b.out")
z1 <- template(200, 1e+4, FALSE)
Rprof(NULL)
summaryRprof("b.out")$by.self
#        self.time self.pct total.time total.pct
#"runif"      0.10    83.33       0.10     83.33
#"c"          0.02    16.67       0.02     16.67

Адаптивный chunk_size с линейным ростом

ref имеет O(N * N) сложность работы, где N - длина конечного вектора.template в принципе имеет O(M * M) сложность, где M = N / chunk_size.Для достижения линейной сложности O(N), chunk_size необходимо увеличить с N, но достаточно линейного роста: chunk_size <- chunk_size + 1.

template1 <- function (chunk_size, stop_sum, timing = TRUE) {
  set.seed(0)                            ## fix a seed to compare performance
  if (timing) t1 <- proc.time()[[3]]
  z <- vector("list")                    ## store all segments in a list
  sum_z <- 0                             ## cumulative sum
  while ( sum_z < stop_sum ) {
    segmt <- numeric(chunk_size)         ## initialize a segment
    i <- 1
    while (i <= chunk_size) {
      z_i <- runif(1)                    ## call a function & get a value
      sum_z <- sum_z + z_i               ## update cumulative sum
      segmt[i] <- z_i                    ## fill in the segment
      if (sum_z >= stop_sum) break       ## ready to break at any time
      i <- i + 1
      }
    ## grow the list
    if (sum_z < stop_sum) z <- c(z, list(segmt))
    else z <- c(z, list(segmt[1:i]))
    ## increase chunk_size
    chunk_size <- chunk_size + 1
    }
  ## remove this line if you want
  cat(sprintf("final chunk size = %d\n", chunk_size))
  if (timing) {
    t2 <- proc.time()[[3]]
    return(t2 - t1)                      ## return execution time
    } else {
    return(unlist(z))                    ## return result
    }
  }

Быстрый тест подтверждает, что мы достигли линейной сложности.

template1(200, 1e+4)
#final chunk size = 283
#[1] 0.103

template1(200, 1e+5)
#final chunk size = 664
#[1] 1.076

template1(200, 1e+6)
#final chunk size = 2012
#[1] 10.848

template1(200, 1e+7)
#final chunk size = 6330
#[1] 108.183
...